Maker ยท Technical Deep-Dive ยท Infrastructure

SmolVLA Remote Inference: From 0.55 Hz to 3 Hz

Mobility the other way โ€” instead of putting wheels under the arm, the model's own compute learned to move around the house

Result: SmolVLA's forward pass moved off the Pi 5's CPU and onto the ML Desktop's GPU over the home LAN, taking rollout cadence from 0.55 Hz to 3.02 Hz (measured over 2,513 control-loop ticks) and worst-case action latency from 97.4s to 1.16s โ€” with the first confirmed correct grasp-and-place through the full remote-inference pipeline. One straightforward bottleneck (uncompressed image transfer, 98.9% of per-tick work) is precisely identified and not yet fixed โ€” the next session's clear next step.
Date: 23 September 2026 ยท Status: working fix confirmed end-to-end, one optimization queued, session paused here by explicit decision

Starting point

SmolVLA training and a first Pi 5 rollout test (previous session) had measured on-Pi CPU inference at 0.55 Hz against a 30 Hz target โ€” roughly 54ร— too slow, 1.83s mean / 97.4s worst-case latency per action. Confirmed compute-bound, not memory-bound: the 4GB Pi loaded the 865MB / 450M-parameter checkpoint without issue, and since every Pi 5 RAM variant shares the identical CPU, more RAM wouldn't have helped. The task itself still completed despite the extreme slowness โ€” the trained policy was sound, this was purely a deployment-speed problem.

Goal for the day: get the model's forward pass off the Pi's CPU and onto the ML Desktop's GPU, without touching training, recording, or LeKiwi's separate Pi.

Attempt 1 โ€” USB/IP full-device passthrough (abandoned, but genuinely useful)

Idea: instead of splitting the code, split the hardware connection. Use USB/IP so the ML Desktop sees the Pi's USB-connected arm and cameras as if locally attached, and run the existing, unmodified lerobot-rollout command on the Desktop instead of the Pi โ€” zero reimplementation of LeRobot's internal preprocessing pipeline.

Validated before building anything

Setup

Installed usbip on both machines, identified and mapped all relevant USB devices by busid (follower arm, main camera, gripper camera โ€” one camera was mislabeled "Webcam Vitade AF" by lsusb's generic vendor DB, correctly identified as an Innomaker unit via v4l2-ctl --list-devices), bound all three on the Pi, attached all three from the Desktop. All three succeeded, confirmed via lsusb and device listings on the Desktop.

Result: cameras worked, the arm's motor bus did not

Running the existing ACT rollout command (deliberately used instead of SmolVLA, to isolate variables โ€” see below) against the USB/IP-attached devices: cameras connected fine, but the arm failed immediately on the very first low-level write to the motor bus:

ConnectionError: Failed to write 'Homing_Offset' on id_=1 with '0' after 1 tries. There is no status packet!

Consistent on retry and on a fresh first write โ€” not a calibration-timing issue, but the basic serial read/write mechanism failing over the network hop.

Diagnosis: the arm's CH340 USB-serial adapter uses a half-duplex handshake (write, then immediately expect a reply) that USB/IP's extra network hop doesn't reliably preserve โ€” a known rough edge for USB tunneling tools generally. Cameras (bulk/isochronous-tolerant video) were unaffected by the identical network hop.

Why ACT, not SmolVLA, for this test: a known-working policy (ACT, already at 86.3% in prior testing) meant a failure could only mean "the hardware connection doesn't work," not get tangled up with SmolVLA's separate, already-known CPU-speed issue. The USB/IP failure was in the hardware layer, below any policy โ€” SmolVLA would have hit the identical error for the identical reason.

Cleanup: all three devices detached/unbound, Pi restored to its prior working state, the v1 inference_server.py and iperf3 test server both stopped and retired.

Value despite not shipping: confirmed LAN bandwidth and camera compression are fine for a network-based approach, conclusively ruled out "just forward all the hardware," and โ€” reading LeRobot's actual rollout code to debug this attempt (sync.py, SyncInferenceEngine, the rollout/strategies/ and rollout/inference/ packages) is exactly what made Attempt 2 possible.

Attempt 2 โ€” Surgical single-line patch (working)

Design

Code reading (triggered by debugging Attempt 1) found a clean interception point: SyncInferenceEngine.get_action() in lerobot/src/lerobot/rollout/inference/sync.py, specifically:

action = self._policy.select_action(observation)

At this point observation is already a fully preprocessed dict of torch tensors โ€” camera images normalized, camera keys renamed, state normalized. A remote call could ship observation as-is (via torch.save/torch.load over HTTP) without reimplementing or guessing at any of LeRobot's own preprocessing logic โ€” unlike Attempt 1's original custom server design, which would have needed to track LeRobot's fast-moving internals by hand.

Two files

Deployment

Results

First live test: the arm actively searched and moved continuously โ€” no multi-second freezes, unlike the CPU baseline โ€” and completed a genuine grasp-and-place. The object was manually nudged slightly closer partway through to keep the session moving at the still-modest ~2โ€“3 Hz rate, but the grasp, transport, and placement into the target were performed entirely by the policy once the object was in reasonable reach. Confirms pipeline correctness: preprocessing โ†’ remote call โ†’ postprocessing โ†’ action execution all functioning together.

Full cadence run โ€” exact numbers from the control loop's own summary

Cadence summary โ€” whole run ยท target 30 Hz (33.3 ms budget per tick): 2513 ticks, 2512 judged
  effective cadence: 3.02 Hz over 832.0 s measured
  ticks over the 33.3 ms work budget: 2512/2512 (100.0%) โ€” work mean 330.8 ms, worst 1157.0 ms
  loop-body steps (share of measured work):
    observe      mean   3.18 ms ยท worst   3.67 ms ยท   1.0% of work ยท 2513 calls
    process_obs  mean   0.02 ms ยท worst   0.10 ms ยท   0.0% of work ยท 2513 calls
    infer        mean 327.42 ms ยท worst 1153.39 ms ยท  98.9% of work ยท 2513 calls
    send         mean   0.20 ms ยท worst   0.58 ms ยท   0.1% of work ยท 2513 calls
    telemetry    mean   0.01 ms ยท worst   0.12 ms ยท   0.0% of work ยท 2513 calls
    query        mean   0.01 ms ยท worst   0.14 ms ยท   0.0% of work ยท 2513 calls
  pacing headroom: 0.0 ms slept per tick on average โ€” loop is saturated
MetricBefore (local Pi CPU)After (remote GPU)
Effective cadence0.55 Hz3.02 Hz (2,513 ticks / 832s)
Worst-case single-action latency97.4 s1.16 s
Mean single-action latency1.83 s0.33 s
Pi CPU load during rollout~100%~5โ€“6%
Pi temperature during rollout60โ€“68ยฐC~50ยฐC
Task outcomesucceeded (slowly)succeeded โ€” first confirmed correct grasp-and-place through the full remote-inference pipeline

The Pi CPU/temperature numbers are an independent confirmation, not just a byproduct of the Hz measurement โ€” they directly show the heavy computation left the Pi, matching the architecture's intent exactly: Pi as thin client for sensing and actuation, Desktop GPU doing the actual model math.

Known remaining bottleneck โ€” precisely quantified, not yet fixed

infer (this server's round trip) accounts for 98.9% of all work time in the loop โ€” every other step is under 1% individually. Root cause: the current server sends full-resolution, uncompressed float32 image tensors over the network every tick โ€” a 640ร—480 + 320ร—240 float32 pair is roughly 4โ€“5 MB per observation, which at the measured ~173 Mbit/s LAN speed accounts for roughly 200ms of pure transfer time per tick, closely matching the measured 327ms mean infer time.

A well-understood, straightforward fix โ€” compress/downcast images before sending, decompress server-side โ€” flagged as the clear next step, not yet implemented. Should get materially closer to the 30 Hz target.

Explicitly deferred to next session

  1. Image-compression optimization โ€” the known lever to close most of the remaining gap to real-time.
  2. Decide the "default" path โ€” currently requires manually setting LEROBOT_REMOTE_INFERENCE_URL and separately starting the server; worth wrapping into a single convenience script/alias once the design is finalized.
  3. Whether/how to reuse this pattern for LeKiwi โ€” the same thin-client-Pi + GPU-inference-server architecture should apply directly once LeKiwi's own SmolVLA work starts, on its own dedicated Pi 5.
  4. Full rollout cadence confirmation with task-success tracking โ€” today's run measured cadence precisely but wasn't set up to track grasp-success rate across many trials (in the spirit of the lux/rollout investigation's methodology); worth doing once the compression fix is in.

Summary