The AY-Robots try page showing the three ways to start without owning a robot: drive a real arm, compare models, rent a GPU
inferenceaction-chunkinglerobotlatencyso-100

Action Chunking at Inference Time: Queues, Thresholds, Seams

AY-Robots ResearchAugust 23, 202623 min read

How chunked policies actually run on a robot: queue depth, chunk_size_threshold, inference delay, and what happens when a new action chunk lands mid-motion.

What decides whether the arm flows or stutters

  • Three integers govern run-time behaviour: how many actions the model predicts per call (the prediction horizon), how many of them you execute before asking again (the execution horizon), and how many control steps pass while the model is thinking (the inference delay).
  • In lerobot async inference the dial is chunk_size_threshold. The client sends a fresh observation when queue length divided by actions_per_chunk falls to or below it. The dataclass default is 0.5.
  • An empty queue and a jerky seam are two different failures. An empty queue means the delay outran the actions in hand. A jerky seam means the new chunk disagreed with the one it replaced.
  • Real-time chunking freezes the actions that are already committed and inpaints the rest. lerobot ships RTCConfig with execution_horizon 10 and max_guidance_weight 10.0.
  • GR00T's rollout scripts execute 16 steps of a 40-step chunk by default, and the SO-100 example client executes 8 while blocking on the server in between. That block is the pause you can see.
  • AY-Robots provisions the inference pod and destroys it on idle, but it cannot shorten the network hop. Over the public internet, remote inference suits slow pick-and-place, not fast reactive motion.

Three integers and one clock

Training a policy is a batch job. Running one is a real-time system, and real-time systems are governed by arithmetic that has nothing to do with loss curves. Action chunking is the reason that arithmetic is tractable at all: instead of one action per forward pass, the model emits a sequence, and the robot has something to do while the next forward pass is running. Everything interesting at run time happens in the gap between the plan you are executing and the plan that is being computed.

The Physical Intelligence real-time chunking paper (arXiv 2506.07339, v2 dated 5 December 2025) gives the cleanest vocabulary for that gap, so this article borrows it. A policy emits a chunk of H actions. You execute s of them before asking for a new chunk. The controller ticks every delta t seconds. The model takes delta seconds to answer. That last quantity is what everyone means by inference latency, and expressed in control steps it is d = floor(delta / delta t). Every failure mode below is a violation of a relationship between those four numbers.

QuantityName in the papersWhat it isWhere you actually set it
HPrediction horizonActions the model emits per forward passchunk_size in lerobot, action_horizon in GR00T and openpi
sExecution horizonActions you consume before re-planningn_action_steps, --execution-horizon, open_loop_horizon, replan_steps
dInference delayControl steps that elapse while the model computesNot a setting. It is measured, and it includes the network
delta tControl periodOne tick of the robot loop, 33.3 ms at 30 Hzfps on the client, DEFAULT_FPS is 30 in lerobot
gQueue thresholdFraction of a chunk left when a new observation is sentchunk_size_threshold in RobotClientConfig

Two constraints matter. The real-time constraint from the RTC paper is d <= s <= H - d: the delay must be shorter than what you still hold, and the execution horizon must leave room for the overlap. The queue constraint from the SmolVLA paper is g >= (delta / delta t) / n, where n is the chunk length the client asked for. Below that value the queue drains faster than the server refills it, and the arm runs dry.

The synchronous loop, and what it does to the arm

Before tuning anything, it helps to see what the simple version does. This is the real-robot client that ships with Isaac-GR00T for the SO-100, trimmed to the loop body. It is the pattern most people start from, and it is fully synchronous.

python
while True:
    obs = robot.get_observation()
    obs["lang"] = cfg.lang_instruction

    actions = policy.get_action(obs)          # blocks on the policy server

    for i, action_dict in enumerate(_select_action_steps(actions, cfg.action_horizon)):
        tic = time.time()
        robot.send_action(action_dict)
        toc = time.time()
        if toc - tic < 1.0 / 30:
            time.sleep(1.0 / 30 - (toc - tic))
gr00t/eval/real_robot/SO100/eval_so100.py on Isaac-GR00T main, fetched 2026-08-24. The dataclass sets action_horizon: int = 8.

Eight actions at 30 Hz is 267 ms of motion. Then the loop returns to the top, grabs a fresh observation, and blocks. Nothing is sent to the servos during that call. If the round trip is 150 ms you have a 150 ms hole in the command stream every 267 ms, which is a duty cycle of about 64 percent. The arm does not stop dead, because position-controlled servos hold the last commanded target, but it does stop moving, and then it starts again. Whether that commanded target means what you think it means is a separate question, answered by calibration.

The pause is a distribution shift, not just lost time

A demonstrator never pauses for 150 ms every eight frames. Your policy has therefore never seen the state it lands in after a pause: zero velocity in the middle of a reach, with an object that has kept moving. The RTC authors make this point directly, and it matches what you see on a real arm. The stutter is not a cosmetic problem you can live with. It is the input to the next forward pass, and it compounds. See policy freezes mid-motion for the symptom in isolation.

Queue depth is the number to watch

The asynchronous version replaces the block with a queue. lerobot's async inference stack splits into a PolicyServer that holds the model and a RobotClient that holds the robot and a queue of timed actions. The client pops one action per tick. Separately, it decides when to send the next observation. The entire decision is one line.

python
def _ready_to_send_observation(self):
    """Flags when the client is ready to send an observation"""
    with self.action_queue_lock:
        return self.action_queue.qsize() / self.action_chunk_size <= self._chunk_size_threshold
src/lerobot/async_inference/robot_client.py, lerobot main (version 0.6.2 in pyproject.toml), fetched 2026-08-24.

That is the whole control policy for the control policy. Queue depth divided by chunk length, compared against one float. Everything else in the stack is plumbing around this comparison, so it is worth understanding what each end of the range does to the robot before you touch it.

chunk_size_thresholdBehaviourCostWhen it is right
0.0Client drains the entire chunk, then asks. Collapses to synchronous.One idle gap of length delta per chunkNever, in production. Useful as a baseline measurement
0.1 to 0.3Asks late, holds a thin bufferRuns dry the moment latency spikesVery fast models on a local link, for example ACT
0.5Consumes half the chunk, then asks. lerobot dataclass defaultRoughly two calls per chunk lengthThe sane starting point for most setups
0.5 to 0.6The range the lerobot docs recommend after tuningMore server calls, more overlap to blendMost SO-100 setups with a VLA on a nearby GPU
0.7The value the SmolVLA paper uses as its asynchronous operating pointAbout 1 / (1 - g) calls per chunkHigh latency, or a model you trust to be reactive
1.0One observation per tick. Maximally reactiveOne forward pass per control step. Usually unaffordableSmall models, local GPU, short horizons
The default is 0.5, whatever the table says

The lerobot async documentation contains an internal contradiction that costs people an afternoon. Its parameter table lists chunk_size_threshold with a default of 0.7 while the same row's description says the client fires when the queue is at or below 50 percent. The CLI example in the same page passes 0.5. In RobotClientConfig the field is chunk_size_threshold: float = 0.5, and actions_per_chunk has no default at all because it is a required field. Read the dataclass, not the table. Checked against lerobot main on 2026-08-24.

Worked numbers for an SO-100 at 30 Hz

Here is the arithmetic on the five policies AY-Robots trains. The latency column is what the platform publishes on the policies pages as the inference figure for each model. The remaining columns apply the formulas above at 30 Hz with a 50-action chunk, which is the lerobot default for Pi0.5 and SmolVLA.

PolicyPublished inference figureSteps lost to delay at 30 HzMinimum g at n = 50Verdict at the 0.5 default
ACT20 ms0.60.01Enormous margin. You can run near-synchronous
GR00T N1.7152 ms4.60.09Comfortable on a local link
GR00T N1.5165 ms5.00.10Comfortable on a local link
SmolVLA245 ms7.40.15Comfortable, tightens if you add a network hop
Pi0.5485 ms14.60.29Works, but a 200 ms round trip pushes it past 0.4

Two readings fall out of that table. First, the default threshold of 0.5 is generous for every model on this list as long as the GPU sits next to the arm. Second, the margin is eaten by the network, not by the model. Adding a 200 ms public-internet round trip to Pi0.5 takes the minimum threshold from 0.29 to roughly 0.41, and the queue you thought was half full is now the only thing between you and a stall.

Measure your own delay before you trust any of this arithmetic

The figures above are spec-sheet numbers for the model. The quantity that sets d is the wall-clock time from the client capturing an observation to the client holding a usable chunk, which includes image encoding, serialisation, two network hops, queueing on the server and postprocessing. lerobot's policy server logs the split for you at debug level: prepare, preprocess, inference, postprocess. Run the client with --debug_visualize_queue_size=True and look at the plot before touching a threshold.

How much that measurement can move is worth internalising. NVIDIA publishes an end-to-end timing table for GR00T N1.7 at 4 denoising steps with a single camera: on an H100 80 GB the same model runs 85.8 ms in PyTorch eager, 48.6 ms with torch.compile and 27.9 ms with a full TensorRT pipeline. On a Jetson Orin the same three modes give 354.0 ms and 150.9 ms at the ends of the range. That is a factor of three in d from the runtime alone, before anyone touches a threshold, which is why the head-to-head model comparisons are only a starting point and not a deployment plan.

The AY-Robots policies comparison table showing parameter counts, GPU tier, inference latency and minimum episode counts for ACT, SmolVLA, GR00T N1.5, GR00T N1.7 and Pi0.5
The published latency figures used in the table above come from this page. They are per-model figures, not measurements of your pod on your network.

What actually happens at the seam

Now the interesting part. A new chunk arrives while the old one is still executing. The two chunks overlap in time, and they may disagree. In lerobot the merge is explicit and readable, and it turns on one field: every action carries a timestep index, assigned by the server from the timestep of the observation that produced it.

The client tracks latest_action, the index of the last action it actually sent to the servos. When a chunk lands, any incoming action whose timestep is at or below that index is dropped on the floor. Those are the actions that were overtaken by reality while the model was thinking. Actions with fresh timesteps go straight into the queue. Actions that collide with something already queued are blended.

python
AGGREGATE_FUNCTIONS = {
    "weighted_average": lambda old, new: 0.3 * old + 0.7 * new,
    "latest_only":      lambda old, new: new,
    "average":          lambda old, new: 0.5 * old + 0.5 * new,
    "conservative":     lambda old, new: 0.7 * old + 0.3 * new,
}
src/lerobot/async_inference/configs.py. aggregate_fn_name defaults to weighted_average.
aggregate_fn_nameFormulaWhat it does at the seamFailure mode
weighted_average0.3 old + 0.7 newLeans on the newer plan, keeps a trace of the old oneDefault. Still averages two incompatible plans
latest_onlynewHard switch to the new chunkMaximum reactivity, maximum jerk at the boundary
average0.5 old + 0.5 newSymmetric blendSplits the difference between two valid paths
conservative0.7 old + 0.3 newSticks with the committed planSmooth, but slow to react to a moved object

Every one of those is a linear interpolation between two trajectories, and that is the structural weakness. If the old chunk planned to go over an obstacle and the new chunk plans to go under it, the average goes through it. The RTC paper illustrates exactly this bifurcation and notes that naive smoothing is not guaranteed to produce a valid action. Blending is a smoothing filter applied to a multi-modal distribution, and smoothing a bimodal distribution gives you the trough between the modes.

Overlap-and-blend, as shipped in lerobot async inference
Advantages
  • Model agnostic. Works with ACT, SmolVLA, Pi0.5, diffusion policies and GR00T alike, because it operates on the output tensor
  • Costs nothing at inference time. It is two multiplications and an addition per action dimension
  • No retraining, no checkpoint changes, no extra denoising steps
  • Removes idle frames, which is the larger of the two problems in practice
  • Tunable in one flag, and reversible without touching the checkpoint
Trade-offs
  • Averages two plans that may belong to different strategies, producing an action neither model would have chosen
  • Does not know which actions are already committed, so it can rewrite steps the robot is about to execute anyway
  • The blend weights are fixed constants, not a function of how far into the overlap you are
  • Provides no continuity guarantee in velocity or acceleration, only in position
  • Hides the disagreement instead of resolving it, so a bimodal policy still looks indecisive

Three ways across the boundary

There are three broad strategies in shipping code today, and they sit at different points on the cost curve. Pick by how much compute you can spend per control step and by whether your checkpoint is flow-based.

1. Blend the overlap

What the section above describes. Cheap, universal, and good enough for slow tasks. This is what you get by default when you run lerobot's async inference with any of the supported policy types: act, smolvla, diffusion, tdmpc, vqbet, pi0, pi05 and groot.

2. Temporal ensembling

The original ALOHA answer, from the ACT paper. Query the policy at every single timestep, so that at any moment you hold many predictions for the same future step, then take an exponentially weighted average with weights w_i = exp(-m * i), where w_0 is the weight on the oldest prediction. Smaller m incorporates new observations faster. The reference implementation uses m = 0.01.

python
# src/lerobot/policies/act/configuration_act.py, lerobot main
chunk_size: int = 100
n_action_steps: int = 100
temporal_ensemble_coeff: float | None = None   # ACT reference impl uses 0.01

# __post_init__ enforces this:
#   n_action_steps must be 1 when temporal_ensemble_coeff is set,
#   because ensembling needs one forward pass per control step.
Enabling temporal ensembling forces the execution horizon to 1, which means one full forward pass per tick.

Read that constraint carefully, because it is the whole story. Temporal ensembling is free of chunk boundaries because it has no chunk boundaries: it re-plans every tick. For an 80 M parameter ACT model at 20 ms that is affordable at 30 Hz. For a 3 B parameter vision-language-action model it is not affordable at any frequency you would want to drive an arm at, which is why nobody runs Pi0.5 with temporal ensembling.

3. Real-time chunking

The current best answer for flow-matching and diffusion policies, and the one that treats the boundary as a generation problem rather than a filtering problem. Freeze the first d actions of the new chunk to the values you know will execute, then inpaint the remainder so that it is consistent with that frozen prefix. It is an inference-time algorithm, requires no retraining, and lerobot ships it.

python
# src/lerobot/policies/rtc/configuration_rtc.py, lerobot main
@dataclass
class RTCConfig:
    enabled: bool = True
    mode: str = "guided"                 # "guided" or "trained"
    prefix_attention_schedule: RTCAttentionSchedule = RTCAttentionSchedule.LINEAR
    max_guidance_weight: float = 10.0
    execution_horizon: int = 10
The docs recommend EXP as the schedule to start with; the dataclass default is LINEAR. Both are real, they simply disagree.
StrategyCompute per control stepWorks withContinuity guaranteeIn lerobot today
Blend the overlapNegligibleAny chunked policyPosition only, no guaranteeDefault in async_inference
Temporal ensemblingOne full forward passAny chunked policyStrong, but slow to react if m is largetemporal_ensemble_coeff on ACT
Real-time chunkingGuidance term per denoising stepFlow and diffusion policies onlyFrozen prefix is exactRTCConfig, lerobot-rollout --inference.type=rtc

The RTC authors report the method running the same real robot motion about 20 percent faster than synchronous inference while staying smoother than temporal ensembling, and holding up under inference delays above 300 ms, which was more than 30 percent of the model's prediction horizon in their setup. The constraint to remember is d <= s <= H - d. If your delay is large enough that d exceeds H - s, no inference-time trick saves you; you need a shorter delay or a longer horizon.

Run it end to end on an SO-100

Concretely, on a machine with the arm attached and a GPU either local or reachable. lerobot's async stack supports so100_follower, so101_follower, bi_so_follower and omx_follower today. This is the manual path; the platform path is in the next section.

  1. 1
    Install the async extra

    The async stack needs gRPC and protobuf, which are not in the base install. Do this in the same environment your policy already runs in.

    bash
    git clone https://github.com/huggingface/lerobot.git
    cd lerobot
    pip install -e ".[async]"
  2. 2
    Start the policy server

    The server starts empty. It learns which checkpoint to load during the first handshake with the client, so you do not pass a model path here.

    bash
    python -m lerobot.async_inference.policy_server \
        --host=0.0.0.0 \
        --port=8080
  3. 3
    Start the client with the queue plot on

    The first run is a measurement run, not a tuning run. Leave the threshold at the default and watch what the queue does.

    bash
    python -m lerobot.async_inference.robot_client \
        --server_address=127.0.0.1:8080 \
        --robot.type=so100_follower \
        --robot.port=/dev/ttyACM0 \
        --robot.id=follower_so100 \
        --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}}" \
        --task="put the cube in the bowl" \
        --policy_type=smolvla \
        --pretrained_name_or_path=<your-user>/<your-checkpoint> \
        --policy_device=cuda \
        --actions_per_chunk=50 \
        --chunk_size_threshold=0.5 \
        --aggregate_fn_name=weighted_average \
        --debug_visualize_queue_size=True
  4. 4
    Read the queue plot, not the robot

    A healthy queue is a sawtooth that never touches zero and never sits at the top. Touching zero means the arm ran dry and you should raise the threshold or lower fps. Sitting near the top means you are paying for forward passes you are throwing away, and you can lower the threshold.

    bash
    # Same run, more detail on where the milliseconds go.
    # The server logs prepare / preprocess / inference / postprocess per chunk.
    python -m lerobot.async_inference.policy_server \
        --host=0.0.0.0 --port=8080 --fps=30 --inference_latency=0.033
  5. 5
    Change exactly one thing

    Threshold first, because it is the cheapest and most reversible. Then actions_per_chunk. Then fps, which is the blunt instrument: dropping from 30 to 15 Hz doubles the wall-clock value of every action in the queue and fixes most stalls at the cost of reactivity.

    bash
    # Queue kept hitting zero: buy more headroom.
    --chunk_size_threshold=0.7
    
    # Still hitting zero at 0.7: the model is simply too slow for 30 Hz here.
    --fps=15
  6. 6
    Only then reach for RTC

    If the queue is healthy and the motion is still jerky, the problem is the seam, not the depth. That is what RTC is for, and it only applies to flow-matching and diffusion checkpoints.

    bash
    lerobot-rollout \
        --strategy.type=base \
        --policy.path=<your-user>/<your-checkpoint> \
        --inference.type=rtc \
        --inference.rtc.execution_horizon=10 \
        --inference.rtc.max_guidance_weight=10.0 \
        --robot.type=so100_follower \
        --robot.port=/dev/ttyACM0 \
        --task="put the cube in the bowl" \
        --duration=120 \
        --device=cuda
The AY-Robots CLI page showing the install command and the run commands for the command line interface
The same operations are available from a terminal on the CLI page, which is useful when you want the pod lifecycle scripted around a tuning loop.

Doing it yourself against doing it here

The tuning above is client-side work that nobody can do for you, because the numbers depend on your network, your cameras and your GPU. What differs between the two paths is everything around it: who owns the GPU, who pays for it while you are thinking, and how long it takes to get from a checkpoint to an arm that moves.

You own the whole chain. That is the right choice when the GPU is already in the room, because nothing beats a local PCIe bus for keeping delta small.

  1. Provision a GPU with enough VRAM. Pi0 occupies about 14 GB at inference time and SmolVLA about 2 GB, per the lerobot docs.
  2. Install lerobot with the async extra, or Isaac-GR00T with its own uv environment, depending on the policy family.
  3. Start the policy server, start the client, and hold the two processes alive for the duration of the session.
  4. Measure delta with the server's per-stage debug logs, compute the minimum threshold, and set it with margin.
  5. Keep the GPU busy or shut it down yourself. An idle rented GPU bills exactly like a busy one.
Two stacks, two vocabularies

If you are mixing families, the names collide. lerobot calls the predicted length chunk_size and the executed length n_action_steps. openpi calls them action_horizon and open_loop_horizon or replan_steps. GR00T calls the predicted length action_horizon in the model config and the executed length --execution-horizon on the CLI, and it renamed the flag from --action-horizon precisely because that collision was confusing people. The old flag still works with a deprecation warning.

The traps that eat a day

Your observations are being silently dropped

lerobot's policy server refuses to run inference on an observation that looks like the last one it processed. The check is observations_similar(obs, previous_obs, lerobot_features=self.lerobot_features), whose atol argument defaults to 1 and which compares the L2 norm of the difference between the two joint-space state vectors. If your arm has moved less than that tolerance since the last processed observation, the server logs Observation #N has been filtered out and does nothing. This is deliberate: it stops redundant forward passes when the arm is stationary. It is also the reason a policy that starts from rest, or one whose task involves holding still, can appear to hang. The escape hatch is the must_go flag, which the client sets when the queue is empty and which bypasses the filter entirely.

  • actions_per_chunk larger than the model's own chunk size does nothing useful. The server truncates with chunk[:, :actions_per_chunk, :], so you get the model's length, not yours.
  • Camera keys must match what the checkpoint expects, exactly. Not similar, exactly. Check config.json on the checkpoint before you argue with the queue.
  • A slow camera is an inference delay. get_observation() is inside the same loop as everything else, and a 1920x1080 USB capture at 30 fps can dominate a small model's forward pass.
  • Raising fps to make the motion smoother makes the stall worse, because every queued action is now worth less wall-clock time. Lowering fps is the fix for a dry queue, not raising it.
  • The client blocks on nothing, which means a dead server produces a robot that keeps executing a stale plan until the queue drains. Watch the queue, not the arm.
  • GR00T's fine-tuning entry point exposes no seed, so two runs of the same configuration are not bit-for-bit identical. If two checkpoints behave differently at the same threshold, that may be the reason and not your tuning.
The AY-Robots MCP server page listing the platform operations exposed as tools to AI agents
The MCP surface exposes the same operations as the CLI, which is how you script a tuning sweep without a human in the loop for each run.

Where none of this helps

Being honest about the boundary of the technique matters more than the technique. Queue tuning moves a fixed amount of latency around. It does not remove latency, and it cannot manufacture reactivity that the model does not have.

Asynchronous execution as a whole
What it genuinely fixes
  • Idle frames. The SmolVLA paper reports 9.7 s average task completion asynchronously against 13.75 s synchronously, and 19 completed pick-and-place cycles against 9 in a fixed window
  • The dynamics mismatch caused by pauses, which is a real source of distribution shift
  • Making a large model usable at a control rate its forward pass cannot sustain
  • Letting you move inference off the robot computer onto hardware that can actually run a 3 B parameter model
What it does not touch
  • A policy that was never reactive. If the model ignores the wrist camera, it will ignore it just as smoothly at 0.7 as at 0.3
  • Total loop latency. The observation that produced the action you are executing is still d steps old, whatever the queue looks like
  • Bad training data. A seam that jumps between two strategies usually means the demonstrations contained two strategies
  • Physical limits. A stalling servo, a wrong supply voltage or a slipping horn look like latency problems and are not

The last one is worth an explicit warning because it costs the most time. If the arm hesitates at the same point in every rollout regardless of threshold, the problem is upstream of inference: check the twitch-then-sag failure mode, confirm the leader-follower recordings were clean, and read policy only works in one setup before you spend another evening on hyperparameters. The failure-mode index is organised by symptom for exactly this reason.

Symptom to knob

What you seeMost likely causeFirst thing to changeSecond thing to change
Arm pauses at a regular rhythmSynchronous loop, or queue hitting zeroRaise chunk_size_threshold to 0.7Lower fps to 15
Arm moves but visibly jerks between smooth segmentsChunk boundary, disagreeing plansSwitch aggregate_fn_name to conservativeEnable RTC if the policy is flow-based
Arm freezes and never resumesObservations filtered as too similar, dead serverCheck the server log for filtered-out observationsConfirm must_go fires when the queue empties
Motion is smooth but reacts late to a moved objectExecution horizon too longLower n_action_steps or --execution-horizonRaise chunk_size_threshold
Motion is reactive but indecisive, oscillatingBlending two modes of the action distributionSwitch to latest_only and observe the raw disagreementRetrain with more consistent demonstrations
Fine locally, hesitant against a cloud podNetwork round trip added to deltaRaise the threshold and accept the compute costMove inference next to the servos

If none of the first-column entries match what you are seeing, the problem is probably not at inference time at all. How VLA models are structured and what high-quality training data looks like are the two upstream pieces that most often explain a policy nobody can tune into working. For a hardware-first read of the same arm, the SO-100 complete guide covers the build.

What is the difference between chunk_size and n_action_steps?

chunk_size is how many actions the model predicts in one forward pass, and n_action_steps is how many of them you execute before predicting again. In lerobot's ACT config both default to 100, which means fully open-loop execution: one observation, one hundred actions, no re-planning for 3.3 seconds at 30 Hz. For Pi0.5 and SmolVLA both default to 50. n_action_steps can never exceed chunk_size; the config raises on that.

What should I set chunk_size_threshold to?

Start at 0.5, which is the dataclass default in RobotClientConfig, and run with --debug_visualize_queue_size=True. The minimum safe value is your measured round-trip latency divided by the control period, divided by actions_per_chunk. The lerobot docs recommend 0.5 to 0.6 after tuning; the SmolVLA paper runs its asynchronous experiments at 0.7. Values near 0 collapse to synchronous behaviour, and 1.0 means one forward pass per control tick.

Does asynchronous inference change my checkpoint?

No. Everything on this page is inference-time only. The threshold, the aggregation function, the execution horizon and real-time chunking all operate on the model's output, and none of them touch the weights. That is also why you can A/B them in a single session: stop the client, change one flag, restart, and compare on the same checkpoint.

Can I use real-time chunking with ACT or GR00T?

Not as shipped. RTC works by adding a guidance term inside an iterative denoising process, so it needs a diffusion or flow-matching action head. lerobot documents it for Pi0, Pi0.5 and SmolVLA. ACT is a transformer that emits a chunk in one pass, with nothing to guide. GR00T N1.7 does denoise, its action head is a flow-matching DiT, but the Isaac-GR00T rollout scripts implement a receding execution horizon rather than RTC; --execution-horizon defaults to 16 out of the base model's 40-step action_horizon.

Why does my arm stop moving when it is nearly stationary?

Most likely the policy server's near-duplicate filter. It compares consecutive observations in joint space with a tolerance of 1 and skips inference when they are closer than that, logging that the observation has been filtered out. The client's must_go flag bypasses the filter once the queue is empty, so the system recovers, but the recovery costs you one full round trip of standing still.

Is remote inference on a rented GPU workable at all?

For slow tasks, yes. For fast reactive motion, no, and the platform says so on its own product pages rather than pretending otherwise. The control loop is 20 to 485 ms per action step depending on model, and a public-internet round trip is added on top of that. Use a cloud pod to validate a checkpoint, to run a slow pick-and-place demonstration, or to avoid buying an A100. Put the GPU next to the arm for anything that has to catch, balance or correct.

Pick the policy before you tune the queue

Inference latency is a property of the model you chose. Five trainable policies compared on parameters, GPU tier, published latency and the minimum number of episodes each one needs before it does anything useful.

Compare the five policies

Sources

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started