
What to log while a robot policy runs: achieved action rate, per-joint deltas, camera frame age, action queue depth and servo load, plus the early signs of a run about to fail.
A policy that worked in the lab still fails on the bench, and the postmortem always goes the same way. The arm did something odd, somebody hit the power switch, and nobody has a record of what the control loop was doing in the ten seconds before. The information was there. It got thrown away one tick at a time.
This is about what to log while a policy drives an SO-100 class arm (if the arm itself is new to you, start with the SO-100 setup guide): achieved action rate, per-joint deltas, camera frame age, action queue depth, servo telemetry. Everything below was checked against LeRobot itself, and the version you install decides how much of it you get for free.
What to know before you instrument anything
- •Log five things: control rate, per-joint step deltas, camera frame age, queue depth, servo load and temperature.
- •LeRobot measures the loop for you on main: CycleTimer warns when the loop body busts the 1/fps budget and prints a per-section breakdown at the end of a run.
- •A failing run shows it in the cadence first: starved ticks, pacing headroom near zero, or one section eating the budget.
- •Camera health is frame age, not whether the device opened: read_latest rejects frames older than 500 ms, and a stale frame looks like a working one.
- •Published monitors (Sentinel, FAIL-Detect, FIPER) need no failure data, they calibrate on successful rollouts alone, and the cheapest reported score costs about 0.04 s per timestep.
- •None of this stops the arm: monitoring is a witness, not a safety layer.
The current PyPI release is lerobot 0.6.1, published 3 August 2026, and it does ship lerobot-rollout with --strategy.type. It does not ship the cadence summary: src/lerobot/utils/cycle_timer.py exists on main and 404s on the v0.6.1 tag, where the base strategy logs only a one-line "Record loop is running slower" warning. Install from a main checkout and pin the commit. Everything else here is in 0.6.1 too.
What a running policy actually hands you
One tick of a base rollout is six timed sections and the library names them: observe reads joints and cameras, process_obs runs the observation processors, infer pulls the next action, send writes goal positions to the bus, telemetry feeds the visualiser, query services the text channel. Those are the literal section labels in rollout/strategies/base.py and core.py, and they come back as a share-of-work breakdown at the end of the run. Most of a profiler, free.
Everything worth monitoring falls out of those six steps plus the motor bus. Per-model latency figures on the policies page set the budget the loop must fit inside, and only one of the five models fits a 30 Hz tick.
| Signal | Where it comes from | Healthy on a 30 Hz SO-100 loop | What a bad value means |
|---|---|---|---|
| Achieved control rate | cadence summary | within about 1% of target | loop over budget, commands land late |
| Work per tick, by section | the six timer sections | sum below the 33.3 ms budget | one section is the bottleneck; the report names it |
| Starved ticks | async and RTC backends | 0 after warmup | the engine cannot refill the queue in time |
| Camera frame age | latest_timestamp in the read thread | one frame period, about 33 ms | the policy is steering on a stale picture |
| Per-joint step delta | consecutive Present_Position reads | smooth, no step changes | chunk-boundary jump, or a joint at a limit |
| Present_Load | control table, address 60 | low and flat when moving freely | the arm is pushing against something |
Action rate: the rate you asked for versus the one you got
The most useful single number is the gap between --fps and what the loop achieved. Inference latency is not the only thing eating the budget; camera reads usually eat more.
lerobot-rollout \
--strategy.type=base \
--policy.path=${HF_USER}/my_act_so100 \
--robot.type=so100_follower \
--robot.port=/dev/ttyACM0 \
--robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, \
wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}}" \
--task="Put the lego brick in the box" \
--fps=30 \
--duration=120 \
--display_data=true 2>&1 | tee run-$(date +%Y%m%d-%H%M%S).logOne warning matters more than the rest. It fires when summed loop-body work for a closed cycle exceeds the 1/fps budget, and it names the three usual suspects:
WARNING Control loop is running slower (24.1 Hz) than the target FPS (30 Hz).
Robot control might be unstable. Common causes are: 1) Camera FPS not keeping up
2) Policy inference (action or text) taking too long 3) CPU starvationCycleTimer skips the first closed group before it judges anything (if self._groups_closed > 1). Lazy device init, camera ramp-up and the interpolator priming its buffer all land there, and counting it would warn on every healthy launch. Copy that in your own monitor: the first tick is always slow and it means nothing.
The diagnosis lives in the end-of-run block. It prints from the loop's finally clause, so Ctrl-C still produces it, every episode boundary emits a one-line digest, and the sink defaults to logger.info, so no log level needs raising. An example in the shape the formatter produces:
Cadence summary - whole run, 3 episodes - target 30 Hz (33.3 ms budget per tick): 1747 ticks, 1744 judged
effective cadence: 28.91 Hz over 60.4 s measured
ticks over the 33.3 ms work budget: 96/1744 (5.5%) - work mean 21.4 ms, worst 118.7 ms
ticks with no action to send (inference engine starved): 12 - each commanded nothing and recorded no frame
loop-body steps (share of measured work):
observe mean 12.10 ms - worst 41.30 ms - 56.5% of work - 1747 calls
process_obs mean 0.90 ms - worst 3.10 ms - 4.2% of work - 1747 calls
infer mean 6.40 ms - worst 22.10 ms - 29.9% of work - 1747 calls
send mean 2.00 ms - worst 9.80 ms - 9.3% of work - 1747 calls
telemetry mean 0.02 ms - worst 0.30 ms - 0.1% of work - 1747 calls
query mean 0.01 ms - worst 0.10 ms - 0.0% of work - 1747 calls
pacing headroom: 11.9 ms slept per tick on average (max 24.1 ms) - near zero means the loop is saturatedRead it from the bottom. Pacing headroom near zero means a saturated loop. A worst an order of magnitude above mean is a stall, not a slow step, and stalls in observe are almost always a camera. Twelve starved ticks in two minutes is a policy server that cannot keep up, a different fix than a slow local loop.
Joint deltas: the cheapest anomaly detector you will ever write
A per-joint step delta is the difference between consecutive Present_Position reads, in degrees. You already have the readings, and two patterns cover most bad rollouts. A joint pinned at a limit: delta collapses to near zero while the commanded goal keeps moving, so the servo loads up against a stop. A chunk-boundary jump: delta spikes on the tick the policy hands over to a new action chunk.
# Attach to any loop that has an observation dict. Runs in microseconds.
import collections
WINDOW = 30 # one second at 30 Hz
JUMP_DEG = 6.0 # per-tick delta that should never happen on an SO-100
STUCK_DEG = 0.15 # below this the joint is not really moving
hist = collections.defaultdict(lambda: collections.deque(maxlen=WINDOW))
prev = {}
def check(obs: dict, t: float) -> list[str]:
alerts = []
for key, value in obs.items():
if not key.endswith(".pos"):
continue
if key in prev:
d = abs(value - prev[key])
hist[key].append(d)
if d > JUMP_DEG:
alerts.append(f"{t:8.2f}s JUMP {key:24s} {d:6.2f} deg in one tick")
elif len(hist[key]) == WINDOW and max(hist[key]) < STUCK_DEG:
alerts.append(f"{t:8.2f}s STUCK {key:24s} < {STUCK_DEG} deg for {WINDOW} ticks")
prev[key] = value
return alertsSOFollowerConfig ships with max_relative_target = None: no cap on how far one commanded step may move a joint. Set it before running an unfamiliar checkpoint and ensure_safe_goal_position clamps every goal to that many degrees from the current position. It is the difference between a policy that misbehaves and one that slams a joint into a stop at full torque.
| Delta pattern | Likely cause | Where to look next |
|---|---|---|
| Spike every N ticks, N = chunk length | chunk boundary discontinuity | switch to RTC or async inference |
| One joint flat, others moving | mechanical limit, or a servo that stopped answering | joint stops early, servo not responding |
| All joints flat, loop still ticking | policy is outputting the current state | loss falls but the policy does nothing |
| Small oscillation around one pose | PID too stiff (the follower writes P 16, I 0, D 32 at connect) or shoulder sag | arm twitches then sags |
| Gripper delta near zero all run | the wrist camera is not showing what the policy trained on | gripper does not close |
Camera health is frame age, not device state
A USB camera that has stopped producing frames still reports as open and still returns an array. The policy sees a photograph of the past. LeRobot's OpenCV camera carries the timestamps you need: a background thread writes latest_frame and latest_timestamp together under a lock, and the read paths enforce different freshness contracts.
| Call | Default | Behaviour on failure |
|---|---|---|
async_read(timeout_ms=200) | 200 ms | TimeoutError if no new frame arrives in the window |
read_latest(max_age_ms=500) | 500 ms | TimeoutError if the newest frame is older than that |
read() | calls async_read with 10000 ms | convenience path, far too slack for a control loop |
| background read loop | failure_count <= 10 | warns on each of eleven consecutive failures, then raises "exceeded maximum consecutive read failures" |
warmup_s | 1 second | frames read and discarded during connect() |
So log the age of every frame you hand the policy, not just whether the read succeeded. Eleven tolerated failures means a camera can degrade for roughly four tenths of a second at 30 fps before anything raises, and a frozen camera never fails a read at all:
import numpy as np
def frame_health(frame, prev_frame, capture_ts, now, fps=30):
age_ms = (now - capture_ts) * 1e3
mad = float(np.mean(np.abs(frame.astype(np.int16) - prev_frame.astype(np.int16))))
mean_lum = float(frame.mean())
return {
"age_ms": age_ms,
"stale": age_ms > 2.5 * (1000.0 / fps), # ~83 ms at 30 fps
"frozen": mad < 0.5, # identical frames, sensor stalled
"black": mean_lum < 8.0, # exposure collapsed or lens capped
"blown": mean_lum > 245.0, # auto-exposure ran away
}Linux hands out /dev/video* indices in enumeration order, and that order is not stable across reboots or replugs. A run where front and wrist silently swapped looks like a regressed policy, not a wiring fault: every camera present, every read successful, the cadence perfect. Log a per-camera fingerprint at startup and compare it against the one from training. See camera not detected and policy only works in one setup.

Queue depth, for anything bigger than ACT
Per the policy comparison, ACT needs about 20 ms per action step, GR00T N1.7 152 ms, SmolVLA 245 ms, Pi0.5 485 ms. Only ACT fits inside a 33.3 ms tick. Every vision-language-action model has to execute one chunk while the next is computing, which makes queue depth a first-class signal. A queue that hits zero is a robot that stops mid-motion.
Both LeRobot paths expose it. Async inference runs a PolicyServer and a RobotClient over gRPC and plots the queue for you; the RTC backend keeps chunk generation on a daemon thread, asks for a new chunk only once the queue has drained to --inference.queue_threshold or below, and reports empty pulls as starved ticks.
# Path A: async inference, gRPC policy server plus robot client
python -m lerobot.async_inference.policy_server --host=0.0.0.0 --port=8080
python -m lerobot.async_inference.robot_client \
--server_address=10.0.0.5:8080 \
--robot.type=so100_follower --robot.port=/dev/ttyACM0 --robot.id=follower_so100 \
--policy_type=smolvla --pretrained_name_or_path=${HF_USER}/smolvla_task \
--policy_device=cuda \
--actions_per_chunk=50 \
--chunk_size_threshold=0.5 \
--aggregate_fn_name=weighted_average \
--debug_visualize_queue_size=True
# Path B: RTC inside lerobot-rollout, starvation shows up in the cadence summary
lerobot-rollout --strategy.type=base --inference.type=rtc \
--inference.rtc.execution_horizon=10 \
--inference.rtc.max_guidance_weight=10.0 \
--inference.queue_threshold=30 \
--policy.path=${HF_USER}/pi05_task \
--robot.type=so100_follower --robot.port=/dev/ttyACM0 \
--task="Pick up the cube" --duration=120 --device=cuda| Knob | Default | What to watch | Direction to move it |
|---|---|---|---|
actions_per_chunk | 50 in the docs; the dataclass has no default, so you must pass it | ticks one chunk covers | raise if the queue empties; long horizons compound error |
chunk_size_threshold | 0.5 in code, 0.7 in the docs table | fill level that triggers a fresh observation | 0.5 to 0.6 per the tuning section; measure your own |
--inference.queue_threshold | 30 | queue level at or below which RTC computes the next chunk | raise when RTC warns about a short prefix |
--inference.rtc.execution_horizon | 10 in the docs example, "varies by policy" in the flag table | smoothness at chunk boundaries | raise for smoother handover, lower for reactivity |
The LeRobot async page lists chunk_size_threshold with a default of 0.7, describes it in the same table as firing at or below 50% full, then recommends 0.5 to 0.6 in the tuning section underneath. The dataclass settles it: chunk_size_threshold: float = field(default=0.5). The Hugging Face async post recommends g = 0.5 and reports roughly a 2x speed-up in task completion time at comparable success rate. Run --debug_visualize_queue_size=True once and read your own curve.

Servo telemetry: load, voltage, temperature
The Feetech STS3215 servos expose more than position, and LeRobot's control table maps the whole SRAM block. Poll these on a slow side channel, once a second, never inside the control loop: every read costs bus bandwidth your position reads need.
| Register | Address | Bytes | Why you want it |
|---|---|---|---|
Present_Position | 56 | 2 | the delta signal above; 4096 counts per revolution on the STS3215 |
Present_Load | 60 | 2 | rises before a joint stalls; sign-magnitude with the sign in bit 10, so raw bytes need decoding |
Present_Voltage | 62 | 1 | sag under load points at the supply, not the policy |
Present_Temperature | 63 | 1 | compare against Max_Temperature_Limit at address 13 |
Status | 65 | 1 | the servo's own error flags |
Moving | 66 | 1 | the servo's own view of whether it is still travelling |
Present_Current | 69 | 2 | STS and SMS series only, finer grained than Present_Load |
STS3215 servos run on 7.4 V. Feeding them 12 V destroys them, quietly enough that you will blame the policy first. A LeKiwi carries both rails in one robot: 12 V base, 7.4 V arm. Check the brick every time you swap hardware.
One read failure here is not news: the follower retries a failed sync_read twice by default (num_read_retries = 2) because Feetech buses return a corrupted status packet now and then. ROS solved the rest of this years ago: diagnostic_updater publishes device health on its own channel at its own rate, default period 1.0 seconds. Fast loop for control, slow loop for health, never both in one thread.
The early signs of a run about to fail
Everything above is mechanical health. Whether the policy itself is going wrong is harder, and it has become a real research area over the last two years. The shared insight: none of them need failure data, because the policy's own action distribution already carries much of the warning, and they calibrate on successful rollouts alone. Sentinel is the one that also watches the video, with a VLM for the slower task-progression failures.
| Method | Venue | Signal it watches | Reported result |
|---|---|---|---|
| Sentinel | CoRL 2024, arXiv 2410.04640 | action consistency across overlapping chunks (MMD with RBF kernels, KL via KDE) plus a VLM judging task progress | 18% more failures than either detector alone; over 97% of unknown failures; 95% accuracy on the real Push Chair task, 20 rollouts |
| FAIL-Detect | RSS 2025, arXiv 2503.08558 | sequential OOD detection over scalar scores from policy inputs and outputs, thresholded by conformal prediction | average best balanced accuracy about 78% in simulation, 72% on hardware; logpZO costs 0.04 s per timestep on an A6000 against 1.45 s for STAC |
| FIPER | NeurIPS 2025, arXiv 2510.09459 | random network distillation in the policy embedding space plus action-chunk entropy over short windows | calibrated on successful rollouts only; five simulation and real environments |
| RTC | NeurIPS 2025, arXiv 2506.07339 | not a monitor but a fix: freeze the actions certain to execute, inpaint the rest | 12 Kinetix tasks, 6 real bimanual tasks; robust to inference delay |
The version worth building on a hobby setup is narrower: keep the last two chunks that overlap in time and take the mean absolute difference across the overlap. A policy on track largely agrees with itself. One losing the plot disagrees, several hundred milliseconds before the arm does anything visibly wrong.
- Needs no failure data: calibration uses a small set of successful rollouts.
- Works on the policy you already have, with no retraining.
- Catches what mechanical checks cannot: the policy is out of distribution while the hardware is fine.
- Chunk-overlap disagreement is a few lines of numpy per tick.
- Thresholds are per-task and per-setup: one tuned at a given table height is wrong at another.
- The cheap version fires on legitimate replanning, so any task with a real decision point gives false alarms.
- At 0.04 s per timestep the published scores do not fit inside a 33.3 ms tick.
- Early detection is not recovery: almost all of this literature stops at the alarm.
A monitor you can build this afternoon
A structured log line per tick, a slow health thread beside it, a script that reads the result. The point is having the trace when something goes wrong, not building a platform.
- 1Record a clean baseline first
One teleoperated episode, same cameras and arm, no policy; keep the cadence summary.
lerobot-teleoperatedefaults to--fps=60, so pass 30 or the baseline is not comparable, and its sections read observe / teleop / send / telemetry. See record your first dataset.bashlerobot-teleoperate \ --robot.type=so100_follower --robot.port=/dev/ttyACM0 \ --teleop.type=so100_leader --teleop.port=/dev/ttyACM1 \ --fps=30 2>&1 | tee baseline.log grep -A 12 'Cadence summary' baseline.log - 2Turn on debug logging where it pays, not everywhere
The follower emits per-read timings at DEBUG (
read state: 4.2ms) plus one line per camera; the RTC engine emitsRTC inference latency=...s, queue=..., queue depth for free. Root-level DEBUG drowns you in library noise, so scope it.pythonimport logging logging.getLogger("lerobot.robots").setLevel(logging.DEBUG) logging.getLogger("lerobot.cameras").setLevel(logging.DEBUG) logging.getLogger("lerobot.rollout.inference.rtc").setLevel(logging.DEBUG) - 3Write one JSON line per tick
One line, one tick, machine readable. Never write from the control thread: queue it and let another thread hit the disk.
pythonimport json, queue, threading q: queue.Queue = queue.Queue(maxsize=10000) def writer(path): with open(path, "a", buffering=1) as fh: while (rec := q.get()) is not None: fh.write(json.dumps(rec) + "\n") threading.Thread(target=writer, args=("ticks.jsonl",), daemon=True).start() def log_tick(t, obs, action, frame_ages, queue_depth): q.put_nowait({ "t": round(t, 4), "pos": {k: round(v, 3) for k, v in obs.items() if k.endswith(".pos")}, "act": {k: round(v, 3) for k, v in action.items()}, "cam_age_ms": {k: round(v, 1) for k, v in frame_ages.items()}, "queue": queue_depth, }) - 4Poll servo health on a slow thread
Once a second, off the control thread: the ROS diagnostics pattern in twenty lines. If reads here fail, suspect the bus before the policy.
pythonimport time def health_loop(bus, motors, stop, period=1.0): while not stop.is_set(): rec = {"t": time.time(), "kind": "health"} for m in motors: rec[m] = { "load": bus.read("Present_Load", m), "volt": bus.read("Present_Voltage", m), "temp": bus.read("Present_Temperature", m), } q.put_nowait(rec) stop.wait(period) - 5Keep a rolling window, not the whole run
LeRobot's highlight strategy does this: a bounded ring buffer of the last N seconds, flushed to a LeRobot dataset episode on a key press, with a second key to push it.
bashlerobot-rollout \ --strategy.type=highlight \ --strategy.ring_buffer_seconds=30 \ --strategy.ring_buffer_max_memory_mb=2048 \ --strategy.save_key=s \ --strategy.push_key=h \ --policy.path=${HF_USER}/my_policy \ --robot.type=so100_follower --robot.port=/dev/ttyACM0 \ --dataset.repo_id=${HF_USER}/failure_clips \ --dataset.single_task="Pick up the red cube"
Failures are rare, and recording everything to catch them fills a disk. A ring buffer plus a key press means you hit the key after the failure and still keep the seconds that led to it. One caveat: the docs table says ring_buffer_seconds defaults to 30 and ring_buffer_max_memory_mb to 2048, while the dataclass on main says 10.0 and 1024. Pass both flags rather than trusting either.
Two ways to get this running
- Install from a
maincheckout for the cadence summary; 0.6.1 haslerobot-rolloutbut notcycle_timer.py. - Record a teleoperated baseline at the same fps.
- Wire the per-tick JSON logger and the slow health thread into the rollout loop.
- Run with
--display_data=truefor a live view; Rerun is the default backend,--display_mode=foxglovethe alternative. - For anything larger than ACT, add RTC or the async client and watch queue depth.
- Own the GPU, the pod lifecycle and the bill if inference runs off-box.
An afternoon of plumbing, then maintenance whenever upstream moves something. LeRobot added the cadence summary after its last release, so the module you import depends on your commit. Isaac-GR00T deprecated --action-horizon for --execution-horizon on main, with a shim that accepts the old spelling and warns, while its n1.6-release and n1.7-release tags still use the old name. Pin your version.
- Pick model and arm on the training matrix and train from a dataset, your machine, or a Hugging Face repo id.
- Start inference and the backend provisions a GPU pod serving the policy; the local robot client talks to that endpoint.
- The pod runs an idle watchdog and destroys itself, so a forgotten session does not keep billing.
- Drive the same operations from the CLI or the MCP server, which makes a monitoring wrapper scriptable.
- Map what your logs show onto the failure-mode pages, and check what a run costs before leaving one going.
It does not read your servo bus and it does not watch your cameras. The per-tick trace, the joint deltas and the frame-age checks are yours to build, and they run next to the arm because that is the only place the data exists. What the platform removes is GPU provisioning, checkpoint plumbing and forgotten pods.

What monitoring will not do for you
A monitor is a witness, not a safety system, and the gap between the two has bitten people who assumed otherwise.
- It does not stop the arm. By the time a Python check fires, the goal position is on the bus. Physical limits, a low
max_relative_targetand a reachable power switch are the safety layer. - It does not fix a bad dataset: a mislabelled wrist camera makes every rollout look healthy and behave wrong. See collecting high-quality VLA training data.
- It does not make remote inference viable for fast tasks. The control loop is 20 to 485 ms per action step depending on the model, and a public-internet round trip turns a working policy into a hesitant one.
- A per-tick log is not a metrics pipeline: dashboards across many arms want a time-series database.
- Alarms without a recovery policy just produce a stopped robot, and what follows the alarm is still open in the literature.
What is the single most useful thing to log if I only do one?▾
The cadence summary, and on a main checkout it is free. CycleTimer prints achieved rate, per-section work share and starved-tick counts at the end of every run plus a one-line digest per episode, at INFO, so no log level needs raising. If the effective cadence is well under target, nothing else you log is trustworthy: the loop is not running at the rate the policy trained at. On 0.6.1 you get the slow-loop warning but not the breakdown.
How often should I read servo temperature and load?▾
About once a second, on a thread that is not the control thread. Every register read costs serial bus bandwidth your Present_Position reads need at 30 Hz, so polling inline slows the loop you are measuring. ROS has used this split for years: diagnostic_updater defaults to a 1.0 second publish period on its own channel.
Does monitoring add enough latency to matter?▾
The cheap checks do not. Joint deltas, frame-age arithmetic and a JSON dump are microseconds, provided the disk write is on another thread. Research-grade monitors are different: FAIL-Detect reports its fastest learned score at 0.04 s per timestep on an A6000, which does not fit a 33.3 ms tick. Those run beside the loop and raise an alarm; they do not gate an action.
My cadence report says the loop is fine but the robot pauses. What is it?▾
Almost certainly starved ticks. With an async or RTC backend inference runs off the control thread, so its latency never shows as loop-body work: the infer section is only a queue pull. It surfaces as ticks with no action to send, counted separately. Raise actions_per_chunk, lower your fps, or move the policy server closer to the robot.
Can I monitor a GR00T policy the same way?▾
Mostly. Isaac-GR00T serves the policy over a ZeroMQ REP socket on port 5555 by default, and the SO-100 closed-loop client in gr00t/eval/real_robot/SO100/eval_so100.py pulls a chunk, executes the first action_horizon steps (default 8) and paces itself with a 30 Hz sleep. Loop cadence, joint deltas and camera age apply unchanged. Queue depth does not: that client runs a whole chunk before asking for another.
Your logs point at a failure mode. Now find it.
Stalled joints, frozen cameras, a policy that freezes mid-motion, a run that only works in one setup. Every symptom in this article maps onto a page that explains the cause and the fix.
Open the failure-mode indexSources
- lerobot CycleTimer on main: the 1/fps budget check, the first-group exemption, starved ticks and the cadence summary formatter
- lerobot BaseStrategy and rollout/strategies/core.py: the observe, process_obs, infer, send, telemetry and query timer sections, note_starved_tick, and log_run_summary in the finally clause
- lerobot RolloutConfig: fps, duration, display_data and display_mode defaults, and the highlight ring-buffer fields
- lerobot OpenCVCamera: async_read(timeout_ms=200), read_latest(max_age_ms=500) and the consecutive-failure read loop
- lerobot Feetech control table: Present_Load (60), Present_Voltage (62), Present_Temperature (63), the 4096-count resolution and the sign-magnitude encoding bits
- lerobot SOFollowerConfig: max_relative_target defaults to None, the position PID gains and num_read_retries
- lerobot async inference configs: chunk_size_threshold defaults to 0.5 in code, actions_per_chunk has no default
- LeRobot: Policy Deployment with lerobot-rollout, the five strategies, both inference backends and the Common Flags table
- LeRobot: Asynchronous inference, actions_per_chunk, chunk_size_threshold and debug_visualize_queue_size
- Hugging Face: Asynchronous robot inference, the ~2x task completion speed-up and the g = 0.5 recommendation
- Agia, Sinha, Yang, Cao, Antonova, Pavone, Bohg: Unpacking Failure Modes of Generative Policies (Sentinel, STAC), CoRL 2024
- Xu et al.: Can We Detect Failures Without Failure Data? Uncertainty-Aware Runtime Failure Detection for Imitation Learning Policies (FAIL-Detect), RSS 2025
- Roemer, Kobras, Worbis, Schoellig: Failure Prediction at Runtime for Generative Robot Policies (FIPER), NeurIPS 2025
- Black, Galliker, Levine: Real-Time Execution of Action Chunking Flow Policies (RTC), NeurIPS 2025
- NVIDIA Isaac-GR00T: the SO-100 closed-loop evaluation workflow, ZeroMQ policy server on port 5555
Sources
- lerobot CycleTimer on main: the 1/fps budget check, the first-group exemption, starved ticks and the cadence summary formatter
- lerobot BaseStrategy and rollout/strategies/core.py: the observe, process_obs, infer, send, telemetry and query timer sections, note_starved_tick, and log_run_summary in the finally clause
- lerobot RolloutConfig: fps, duration, display_data and display_mode defaults, and the highlight ring-buffer fields
- lerobot OpenCVCamera: async_read(timeout_ms=200), read_latest(max_age_ms=500) and the consecutive-failure read loop
- lerobot Feetech control table: Present_Load (60), Present_Voltage (62), Present_Temperature (63), the 4096-count resolution and the sign-magnitude encoding bits
- lerobot SOFollowerConfig: max_relative_target defaults to None, the position PID gains and num_read_retries
- lerobot async inference configs: chunk_size_threshold defaults to 0.5 in code, actions_per_chunk has no default
- LeRobot: Policy Deployment with lerobot-rollout, the five strategies, both inference backends and the Common Flags table
- LeRobot: Asynchronous inference, actions_per_chunk, chunk_size_threshold and debug_visualize_queue_size
- Hugging Face: Asynchronous robot inference, the ~2x task completion speed-up and the g = 0.5 recommendation
- Agia, Sinha, Yang, Cao, Antonova, Pavone, Bohg: Unpacking Failure Modes of Generative Policies (Sentinel, STAC), CoRL 2024
- Xu et al.: Can We Detect Failures Without Failure Data? Uncertainty-Aware Runtime Failure Detection for Imitation Learning Policies (FAIL-Detect), RSS 2025
- Roemer, Kobras, Worbis, Schoellig: Failure Prediction at Runtime for Generative Robot Policies (FIPER), NeurIPS 2025
- Black, Galliker, Levine: Real-Time Execution of Action Chunking Flow Policies (RTC), NeurIPS 2025
- NVIDIA Isaac-GR00T: the SO-100 closed-loop evaluation workflow, ZeroMQ policy server on port 5555
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started