
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.
| Quantity | Name in the papers | What it is | Where you actually set it |
|---|---|---|---|
| H | Prediction horizon | Actions the model emits per forward pass | chunk_size in lerobot, action_horizon in GR00T and openpi |
| s | Execution horizon | Actions you consume before re-planning | n_action_steps, --execution-horizon, open_loop_horizon, replan_steps |
| d | Inference delay | Control steps that elapse while the model computes | Not a setting. It is measured, and it includes the network |
| delta t | Control period | One tick of the robot loop, 33.3 ms at 30 Hz | fps on the client, DEFAULT_FPS is 30 in lerobot |
| g | Queue threshold | Fraction of a chunk left when a new observation is sent | chunk_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.
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))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.
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.
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_thresholdThat 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_threshold | Behaviour | Cost | When it is right |
|---|---|---|---|
| 0.0 | Client drains the entire chunk, then asks. Collapses to synchronous. | One idle gap of length delta per chunk | Never, in production. Useful as a baseline measurement |
| 0.1 to 0.3 | Asks late, holds a thin buffer | Runs dry the moment latency spikes | Very fast models on a local link, for example ACT |
| 0.5 | Consumes half the chunk, then asks. lerobot dataclass default | Roughly two calls per chunk length | The sane starting point for most setups |
| 0.5 to 0.6 | The range the lerobot docs recommend after tuning | More server calls, more overlap to blend | Most SO-100 setups with a VLA on a nearby GPU |
| 0.7 | The value the SmolVLA paper uses as its asynchronous operating point | About 1 / (1 - g) calls per chunk | High latency, or a model you trust to be reactive |
| 1.0 | One observation per tick. Maximally reactive | One forward pass per control step. Usually unaffordable | Small models, local GPU, short horizons |
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.
| Policy | Published inference figure | Steps lost to delay at 30 Hz | Minimum g at n = 50 | Verdict at the 0.5 default |
|---|---|---|---|---|
| ACT | 20 ms | 0.6 | 0.01 | Enormous margin. You can run near-synchronous |
| GR00T N1.7 | 152 ms | 4.6 | 0.09 | Comfortable on a local link |
| GR00T N1.5 | 165 ms | 5.0 | 0.10 | Comfortable on a local link |
| SmolVLA | 245 ms | 7.4 | 0.15 | Comfortable, tightens if you add a network hop |
| Pi0.5 | 485 ms | 14.6 | 0.29 | Works, 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.
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.

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.
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,
}| aggregate_fn_name | Formula | What it does at the seam | Failure mode |
|---|---|---|---|
| weighted_average | 0.3 old + 0.7 new | Leans on the newer plan, keeps a trace of the old one | Default. Still averages two incompatible plans |
| latest_only | new | Hard switch to the new chunk | Maximum reactivity, maximum jerk at the boundary |
| average | 0.5 old + 0.5 new | Symmetric blend | Splits the difference between two valid paths |
| conservative | 0.7 old + 0.3 new | Sticks with the committed plan | Smooth, 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.
- 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
- 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.
# 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.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.
# 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| Strategy | Compute per control step | Works with | Continuity guarantee | In lerobot today |
|---|---|---|---|---|
| Blend the overlap | Negligible | Any chunked policy | Position only, no guarantee | Default in async_inference |
| Temporal ensembling | One full forward pass | Any chunked policy | Strong, but slow to react if m is large | temporal_ensemble_coeff on ACT |
| Real-time chunking | Guidance term per denoising step | Flow and diffusion policies only | Frozen prefix is exact | RTCConfig, 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.
- 1Install 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.
bashgit clone https://github.com/huggingface/lerobot.git cd lerobot pip install -e ".[async]" - 2Start 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.
bashpython -m lerobot.async_inference.policy_server \ --host=0.0.0.0 \ --port=8080 - 3Start 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.
bashpython -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 - 4Read 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 - 5Change 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 - 6Only 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.
bashlerobot-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

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.
- Provision a GPU with enough VRAM. Pi0 occupies about 14 GB at inference time and SmolVLA about 2 GB, per the lerobot docs.
- Install lerobot with the async extra, or Isaac-GR00T with its own uv environment, depending on the policy family.
- Start the policy server, start the client, and hold the two processes alive for the duration of the session.
- Measure delta with the server's per-stage debug logs, compute the minimum threshold, and set it with margin.
- Keep the GPU busy or shut it down yourself. An idle rented GPU bills exactly like a busy one.
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 platform removes the provisioning and teardown work, not the tuning work. The inference endpoint auto-provisions a cloud GPU pod that serves your policy, and your local robot client talks to that endpoint. The run your first policy walkthrough is the shortest path from a finished checkpoint to a moving arm.
- Pick a policy and a checkpoint. Base checkpoints are the vendors' own: nvidia/GR00T-N1.7-3B, nvidia/GR00T-N1.5-3B and lerobot/pi05_base.
- The backend rents a GPU by required VRAM and serves the policy behind an endpoint.
- Point the robot client at that endpoint and run your control loop against it.
- Pods carry an idle watchdog and destroy themselves after an idle period, so a forgotten tuning session does not bill silently.
- Cost, for reference, is about 4 to 12 USD for an A100 or H100 tier run and about 1 to 3 USD on the 24 GB tier. See pricing.
It cannot shorten the speed of light. A cloud pod adds a public-internet round trip to delta, and the honest position is the one stated on the product pages: the control loop is 20 to 485 ms per action step depending on the model, and adding public-internet round trips turns a working policy into a hesitant one. Remote inference is viable for slow pick-and-place. It is not viable for fast reactive motion. If your task needs reactivity, put the GPU next to the servos and use the platform for training instead.
Note that GR00T N1.7 and Pi0.5 are cloud-only here, while SmolVLA and ACT also run locally. If run-time latency is your binding constraint, that split is the most important sentence on this page.
The traps that eat a day
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.

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.
- 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
- 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 see | Most likely cause | First thing to change | Second thing to change |
|---|---|---|---|
| Arm pauses at a regular rhythm | Synchronous loop, or queue hitting zero | Raise chunk_size_threshold to 0.7 | Lower fps to 15 |
| Arm moves but visibly jerks between smooth segments | Chunk boundary, disagreeing plans | Switch aggregate_fn_name to conservative | Enable RTC if the policy is flow-based |
| Arm freezes and never resumes | Observations filtered as too similar, dead server | Check the server log for filtered-out observations | Confirm must_go fires when the queue empties |
| Motion is smooth but reacts late to a moved object | Execution horizon too long | Lower n_action_steps or --execution-horizon | Raise chunk_size_threshold |
| Motion is reactive but indecisive, oscillating | Blending two modes of the action distribution | Switch to latest_only and observe the raw disagreement | Retrain with more consistent demonstrations |
| Fine locally, hesitant against a cloud pod | Network round trip added to delta | Raise the threshold and accept the compute cost | Move 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 policiesSources
- LeRobot documentation: Asynchronous Inference (PolicyServer, RobotClient, actions_per_chunk, chunk_size_threshold)
- LeRobot documentation: Real-Time Chunking (RTC), RTCConfig parameters and lerobot-rollout usage
- lerobot: async_inference/configs.py, RobotClientConfig defaults and the AGGREGATE_FUNCTIONS registry
- lerobot: async_inference/robot_client.py, _ready_to_send_observation, _aggregate_action_queues and the must_go flag
- lerobot: async_inference/policy_server.py, GetActions, chunk truncation and per-stage timing logs
- lerobot: async_inference/helpers.py, observations_similar with atol=1 in joint space
- lerobot: ACT configuration, chunk_size 100, n_action_steps 100, temporal_ensemble_coeff
- lerobot: RTCConfig, execution_horizon 10, max_guidance_weight 10.0, prefix_attention_schedule
- Black, Galliker and Levine, Real-Time Execution of Action Chunking Flow Policies (NeurIPS 2025), v2 5 December 2025
- Zhao et al., Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware (ACT, action chunking and temporal ensembling)
- Shukor et al., SmolVLA, including the asynchronous inference stack and the queue threshold analysis
- Isaac-GR00T deployment README: --execution-horizon and --denoising-steps defaults, TensorRT end-to-end latency table
- NVIDIA Isaac-GR00T README: N1.6 to N1.7 changes, action_horizon expanded from 16 to 40, --action-horizon renamed to --execution-horizon
- Isaac-GR00T: SO-100 real-robot evaluation client with action_horizon 8 and a blocking policy call
- openpi: DROID example with open_loop_horizon 8 and its comment on 0.5 seconds of execution
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started