The AY-Robots tutorial for recording a first LeRobot dataset, where the camera keys and frame rate for a run are chosen
camerasdata-collectionso-100lerobot-datasetinference-latency

The Cost of a Second Camera: Bandwidth, Latency, Money

AY-Robots ResearchAugust 23, 202619 min read

A second camera on an SO-100 costs USB bandwidth, vision tokens, inference latency and a full re-record. Published ablations on when it pays for itself and when it does not.

A second camera on an SO-100 is a cheap decision at the hardware shop and an expensive one everywhere else. What you actually spend is USB isochronous bandwidth on the recording machine, extra vision tokens in every forward pass, milliseconds of inference latency on every control step, and a full re-record of your LeRobot dataset, because you cannot retrofit a view into episodes captured without it.

Below is each bill, priced from upstream code and published ablations. Short version: on a tabletop pick-and-place with one fixed third-person view, a wrist camera is usually the highest-return change available. Where the existing views already cover the workspace it can make things worse, and there is a 2026 SO-101 result where it did.

What a second camera costs, in one screen

  • Bandwidth. A USB 2.0 bus has about 384 Mbit/s for all isochronous streams, and one camera endpoint can reserve up to 196 Mbit/s of it. Two uncompressed 640x480 streams need 295 Mbit/s of real payload, but cameras reserve on a declared worst case that is often far above what they send, and then the second stream refuses to start.
  • Compute. GR00T N1 spends 64 image tokens per frame; a second camera makes it 128. ACT appends a whole ResNet-18 feature map per camera, about 300 tokens at 480x640.
  • Latency. In the OpenVLA-OFT study, adding a wrist image and proprioceptive state to an identical recipe moved A100 latency from 0.0729 s to 0.1120 s per query and throughput from 109.7 Hz to 71.4 Hz.
  • Payoff. robomimic measured relative drops of 9 percent on Square and 43 percent on Transport when the wrist image was removed; its study page rounds this to 10 to 45 percent. On a real Franka Can task, 43.3 percent success without the wrist view against 73.3 percent with it.
  • Money. Almost nothing. An A100-tier run costs about 4 to 12 USD and a 24 GB-tier run about 1 to 3 USD. A second view does not move those bands; re-recording is the expensive part.
  • Not automatic. On a mobile SO-101 whose body-fixed cameras already covered the workspace, a wrist camera dropped ACT from 90 to 86 percent and SmolVLA from 79 to 12 percent over 100 trials.

Bill one: USB bandwidth, and the error that pretends to be a disk problem

A UVC webcam does not negotiate a bitrate. It asks the host for an isochronous endpoint with a fixed payload per microframe, and the host either grants the whole reservation up front or refuses the stream. Two separate ceilings apply, and conflating them is why the arithmetic often looks like it should work. USB 2.0 high speed signals at 480 Mbit/s in 125 microsecond microframes, and the specification caps periodic transfers at 80 percent of each microframe, leaving about 384 Mbit/s for every camera on the bus to share. Separately, one high-bandwidth isochronous endpoint tops out at three 1024-byte transactions per microframe, about 196 Mbit/s, and that is the most a single camera can ever reserve.

The 384 Mbit/s figure is per bus, not per port. A hub adds no bandwidth; it multiplexes devices onto the same upstream reservation. So the number that decides whether your second camera opens is what each stream reserves, not what it sends, and for uncompressed formats the payload at least is exact arithmetic.

Stream at 30 fpsBytes per frameActual payloadTwo of them on one USB 2.0 bus?
640x480 YUYV, 16 bits per pixel614,400147.5 Mbit/sOnly if both reserve honestly. 295 Mbit/s of payload fits under 384, two worst-case reservations do not
1280x720 YUYV1,843,200442.4 Mbit/sNo. One stream alone is past the 196 Mbit/s an endpoint can reserve
1920x1080 YUYV4,147,200995.3 Mbit/sNo. Past the raw 480 Mbit/s signalling rate. USB 3.0 territory
640x480 MJPEGvaries with scenefar lower in real bytesUsually yes, but the reservation is still the trap
The error says 'No space left on device' and means bandwidth

One camera works. Open the second and VIDIOC_STREAMON returns ENOSPC, printed as No space left on device, with uvcvideo: Failed to submit URB 0 (-28) in the kernel log. Your disk is fine. In microframe terms the 80 percent budget is 6000 bytes per 125 microseconds; a 640x480 YUYV stream genuinely needs about 2304 of them, so two would fit at 4608. But the driver sizes the reservation from the camera's declared dwMaxPayloadTransferSize, and many cameras declare a value at or near the 3072-byte endpoint ceiling regardless of format, so the pair does not fit. The Good Penguin traced a 320x240 MJPEG stream that reserved 3060 bytes per microframe, 195 Mbit/s, for a payload that could not exceed about 46 Mbit/s even uncompressed. Fixes, cheapest first: move the second camera to a different USB root controller, drop the resolution or frame rate, or reload the driver with the bandwidth quirk. UVC_QUIRK_FIX_BANDWIDTH is 0x00000080 in uvcvideo.h, so the parameter is quirks=128. Note what that quirk does not cover: the kernel only recomputes the bandwidth for uncompressed formats, so it cannot rescue an MJPEG stream, and switching to MJPEG cuts the bytes actually sent without necessarily cutting the reservation. The Good Penguin's failing example was MJPEG.

  1. 1
    Ask what the host already sees

    LeRobot ships auto-discovery. The identifiers it prints are not stable across reboots.

    bash
    lerobot-find-cameras opencv
    # use 'realsense' instead of 'opencv' for Intel RealSense devices
  2. 2
    Find which controller each camera hangs off

    Separate root controllers get separate isochronous budgets. Two cameras behind one hub share one.

    bash
    lsusb -t   # look for Driver=uvcvideo, and whether both sit under the same Bus
  3. 3
    Check which formats the camera really offers

    No MJPEG in the list means an uncompressed stream, and the arithmetic above applies.

    bash
    v4l2-ctl --list-formats-ext -d /dev/video0
  4. 4
    Stream both at the resolution you intend to record

    The test that matters. Two minutes here, or an evening of episodes with a camera that drops out.

    bash
    v4l2-ctl -d /dev/video0 --set-fmt-video=width=640,height=480,pixelformat=MJPG \
      --stream-mmap --stream-count=300 &
    v4l2-ctl -d /dev/video2 --set-fmt-video=width=640,height=480,pixelformat=MJPG \
      --stream-mmap --stream-count=300
  5. 5
    If it still fails, reload uvcvideo with the quirk

    Uncompressed formats only. Close everything holding a camera or the module will not unload.

    bash
    sudo modprobe -r uvcvideo
    sudo modprobe uvcvideo quirks=128   # UVC_QUIRK_FIX_BANDWIDTH = 0x80
The AY-Robots SO-100 hub page, covering setup, data collection and imitation learning
The SO-100 hub. Camera decisions belong before the first episode.

Bill two: what each policy does with the second view

There is no single answer: the five trainable policies ingest images in four different ways. The table below is read out of upstream source in August 2026.

PolicyHow images enter the modelWhat the second camera addsRead from
ACTOne shared ResNet-18 backbone; each camera's layer4 feature map is flattened onto the encoder sequenceAbout 300 tokens at 480x640, since a stride-32 backbone yields a 15x20 grid. Two cameras is 600lerobot modeling_act.py
SmolVLAEach image feature is resized with padding to 512x512 and pushed through the frozen SigLIP encoder of SmolVLM2-500M-Video-InstructA full vision-encoder pass per step; the paper caps visual tokens at 64 per framelerobot configuration_smolvla.py
GR00T N1.5 and N1.7Frames go through a pretrained SigLIP-2 vision transformer and the image tokens of all frames are concatenated into one sequence; the GR00T N1 paper puts this at 224x224 plus pixel shuffle, 64 image token embeddings per frameAnother frame's worth of tokens per step, 64 more at the configuration the N1 paper describes. The Isaac-GR00T SO-100 example already declares two video keysGR00T N1 paper, Isaac-GR00T examples/SO100
Pi0.5Fixed 224x224 slots: one third-person view and two wrist views; missing slots are zero-filled and maskedNothing structural, it fills an existing slot. A third external view has no slot without editing the transformopenpi libero_policy.py
python
# NVIDIA/Isaac-GR00T, examples/SO100/so100_config.py (main branch, 24 Aug 2026)
so100_config = {
    # Video: current frame only; keys must match "video" entries in meta/modality.json
    "video": ModalityConfig(
        delta_indices=[0],
        modality_keys=["front", "wrist"],  # front third-person view + wrist egocentric
    ),
    "state": ModalityConfig(
        delta_indices=[0],
        modality_keys=["single_arm", "gripper"],
    ),
    ...
}
The reference SO-100 config in Isaac-GR00T assumes two cameras.
json
// NVIDIA/Isaac-GR00T, examples/SO100/modality.json
"video": {
    "front": { "original_key": "observation.images.front" },
    "wrist": { "original_key": "observation.images.wrist" }
}
The modality map ties view names to dataset feature keys, which your recording command has to produce.
Name the streams for what they see, not for the device

Hugging Face's guidance on what makes a good dataset is to name streams by location rather than by device: images.top, images.front, images.left, images.right, with an orientation suffix for wrist cameras such as images.wrist.left. Avoid images.laptop or images.phone. Two more things to settle before recording: GR00T's loader wants LeRobot v2.0 or v2.1 and a v3.0 dataset must be converted down first, which is the failure behind dataset rejected as v3; and the same keys must be passed at rollout, or the policy gets an observation layout it never saw. Format details are in the dataset docs.

Bill three: latency, paid on every control step

The cleanest published measurement of what an extra view costs at inference time is the OpenVLA-OFT study by Kim, Finn and Liang: throughput and latency for 7-dimensional actions on an NVIDIA A100, averaged over 100 queries, holding the recipe fixed and changing only the inputs.

ConfigurationThroughputLatency per queryLIBERO-Long success
OpenVLA, one third-person image4.2 Hz0.2396 s53.7 percent
plus parallel decoding and action chunking108.8 Hz0.0735 s86.5 percent
plus continuous actions with L1 regression109.7 Hz0.0729 s90.7 percent
plus wrist image and proprioceptive state71.4 Hz0.1120 s94.5 percent

The last row is the price tag: latency up about 54 percent, throughput down a third, LIBERO-Long success up 3.8 points. Note what is bundled there. That row adds the wrist image and the proprioceptive state together, so the delta is an upper bound on what the camera alone costs, not a camera-only measurement. Per-step figures here span an order of magnitude, from 20 ms for ACT through 152 ms for GR00T N1.7 and 245 ms for SmolVLA to 485 ms for Pi0.5. Half again on 20 ms is invisible. Half again on 485 ms is a different robot.

This is also where action chunking earns its keep. ACT predicts 100 steps per forward pass by default, Pi0 uses a horizon of 50, GR00T N1 uses 16. A long chunk amortises the second camera's encoder cost over many actuator commands.

The AY-Robots comparison table of the five trainable policies with parameters, GPU tier, latency and minimum episodes
Per-step latency. That column is the budget an extra camera fits inside.
Cameras will not break your control loop. The network will

A view costs tens of milliseconds. Moving inference off the machine wired to the servos costs a public-internet round trip on every step, the larger number by a wide margin. AY-Robots provisions a cloud GPU pod for serving, which works for slow pick-and-place, not for fast reactive motion. See policy freezes mid-motion.

Bill four: time and money, smaller than people expect

This bill surprises people in the good direction. A second camera changes the token count, but not the GPU tier, and the tier is what money is attached to.

GPU tierPoliciesTypical runPrice per hourCost per run
A100 80 GB or H100 80 GBGR00T N1.7, GR00T N1.5, Pi0.53 to 6 hours1.20 to 2.00 USDabout 4 to 12 USD
RTX 4090 or any 24 GB cardSmolVLA, ACT2 to 5 hours0.30 to 0.60 USDabout 1 to 3 USD

The real cost is recording, paid in arm time. LeRobot's episode defaults are 50 episodes, 60 seconds of recording and 60 of reset: 100 minutes at the leader arm and 90,000 frames per camera. A second camera adds none of that, provided it was mounted before you started. If it was not, you pay those 100 minutes again. That asymmetry is the whole argument for settling the layout first.

The AY-Robots cost table: which GPU each policy needs, run time, price per run, episodes needed
Run costs by tier. Camera count moves tokens, not tiers.

What the second view buys, according to people who measured

The literature agrees on one thing and splits on another. Agreed: a wrist view helps most where a third-person view is weakest, in the last few centimetres before contact. Split: whether it helps once your existing views already cover that region.

StudyWhat was comparedResult
robomimic, Mandlekar et al., 2021Image agents with and without wrist observations, simulated manipulationRelative drops of 9 percent on Square and 43 percent on Transport with the wrist image removed; the study page rounds the range to 10 to 45 percent
robomimic, real robotThe Can task on real hardware (Franka, BC-RNN, 30 rollouts), with and without the wrist view43.3 percent success without it, 73.3 percent with it
OpenVLA-OFT, Kim, Finn and Liang, 2025Same recipe and same dataset on LIBERO: third-person view and language, against third-person plus wrist image plus proprioceptive stateAverage over four suites 95.3 to 97.1 percent; on LIBERO-Long alone, 90.7 to 94.5 percent
Hsu et al., ICLR 2022Hand-centric against third-person perspective on Meta-World tasks and real armsHand-centric improves training efficiency and out-of-distribution generalization; where it is insufficient, third-person is needed but harms generalization
SEVO, 2026Mobile SO-101, two body-fixed cameras already covering the workspace, wrist added as a third view, 100 trials per conditionACT fell from 90 to 86 percent, SmolVLA from 79 to 12 percent
Read the counterexample before you mount anything

The SEVO authors offer two mechanisms as hypotheses, and say direct verification is left to future work. As the gripper closes in, the wrist view fills with the object surface and carries almost nothing about the gripper-to-object relationship. And on a moving base it sees rapid visual flow, from which ACT learned continuous oscillation and SmolVLA learned to grasp and release in open air. Note the setup before generalising: a mobile platform with active red illumination and two body-fixed cameras already covering the workspace. A static SO-100 with one front camera is not that. The lesson is narrower than 'wrist cameras are bad': a second view pays only when it shows what the first cannot.

Adding a wrist camera to a static SO-100
What it buys
  • Close-range evidence of where the gripper sits relative to the object, which is what a front view loses to occlusion.
  • robomimic measured relative drops of 9 percent on Square and 43 percent on Transport when the wrist image was removed.
  • Hsu et al. found hand-centric views improve out-of-distribution generalization, not only success.
  • The Isaac-GR00T SO-100 example and the Pi0 input layout already assume a wrist view.
  • It is the one view that survives moving the robot to a different table.
What it costs
  • Every episode recorded without it is unusable for the new configuration. No retrofit exists.
  • It doubles the isochronous reservation on the recording host.
  • It adds a vision-encoder pass per step. In the OFT measurements a wrist image plus proprioception cost about 54 percent more latency.
  • Mounted close to the gripper its field of view is narrow, easily swamped by the object at contact.
  • Two cameras is two more chances for a device index to shift and silently swap the views.

Where to put the second camera

Placement decides most of the payoff. The points below come from the Hugging Face dataset guidance and the LeRobot imitation-learning tutorial, and every one of them is something people get wrong on a first imitation learning run.

  1. Two views is the recommended default. More is more tokens, more bandwidth, more ways to disagree.
  2. Record at 480x640 or better, around 30 fps. Below that the wrist view stops resolving what it is for.
  3. Keep both cameras mechanically fixed. One nudged between sessions trains a policy that only works in one setup.
  4. Keep the leader arm out of frame. The policy will happily learn to watch the human instead of the scene.
  5. The only moving things in frame should be the follower arm and the objects it moves.
  6. The LeRobot rule of thumb: you should be able to do the task yourself by only looking at the camera images. If you cannot, neither can the policy.

Recording a two-camera dataset that will train

The manual path end to end, against upstream LeRobot 0.6.2. Substitute your own ports and device paths. The platform walkthrough is at record your first dataset.

  1. 1
    Teleoperate with both streams visible first

    Ten minutes here saves discovering after 50 episodes that the wrist camera is upside down.

    bash
    lerobot-teleoperate \
      --robot.type=so100_follower \
      --robot.port=/dev/ttyACM0 \
      --robot.id=my_follower_arm \
      --robot.cameras="{ front: {type: opencv, index_or_path: /dev/video0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: /dev/video2, width: 640, height: 480, fps: 30} }" \
      --teleop.type=so100_leader \
      --teleop.port=/dev/ttyACM1 \
      --teleop.id=my_leader_arm \
      --display_data=true
  2. 2
    Record the episodes

    The camera keys here become the dataset feature names. num_episodes defaults to 50, episode_time_s and reset_time_s to 60 s each.

    bash
    lerobot-record \
      --robot.type=so100_follower \
      --robot.port=/dev/ttyACM0 \
      --robot.id=my_follower_arm \
      --robot.cameras="{ front: {type: opencv, index_or_path: /dev/video0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: /dev/video2, width: 640, height: 480, fps: 30} }" \
      --teleop.type=so100_leader \
      --teleop.port=/dev/ttyACM1 \
      --teleop.id=my_leader_arm \
      --dataset.repo_id=${HF_USER}/so100_twocam \
      --dataset.num_episodes=50 \
      --dataset.single_task="Pick up the cube and drop it in the bin" \
      --dataset.streaming_encoding=true \
      --dataset.encoder_threads=2 \
      --display_data=true
  3. 3
    Check that both streams landed

    Encoder settings land in meta/info.json from the first episode. You want two video features with matching fps and resolution.

    bash
    python - <<'PY'
    import json, os, pathlib
    repo = f"{os.environ['HF_USER']}/so100_twocam"
    root = pathlib.Path.home() / ".cache/huggingface/lerobot" / repo
    info = json.loads((root / "meta/info.json").read_text())
    for key, feat in info["features"].items():
        if feat["dtype"] == "video":
            i = feat["info"]
            print(key, feat["shape"], i["video.codec"], i["video.fps"], "crf", i["video.crf"])
    PY
  4. 4
    Train

    ACT adapts to the camera count stored in the dataset, so there is no camera flag. That convenience is the trap: a broken second stream trains quietly and badly.

    bash
    lerobot-train \
      --dataset.repo_id=${HF_USER}/so100_twocam \
      --policy.type=act \
      --output_dir=outputs/train/act_so100_twocam \
      --job_name=act_so100_twocam \
      --policy.device=cuda \
      --policy.repo_id=${HF_USER}/act_so100_twocam
  5. 5
    Roll out with the same camera keys

    front and wrist must be spelled exactly as at recording time. The most common way a two-camera setup fails on demo day.

    bash
    lerobot-rollout \
      --strategy.type=base \
      --policy.path=${HF_USER}/act_so100_twocam \
      --robot.type=so100_follower \
      --robot.port=/dev/ttyACM0 \
      --robot.cameras="{ front: {type: opencv, index_or_path: /dev/video0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: /dev/video2, width: 640, height: 480, fps: 30} }" \
      --task="Pick up the cube and drop it in the bin" \
      --duration=60
The trap that eats a whole day: the indices swap

The LeRobot camera docs warn that device identifiers "may change after rebooting your computer or re-plugging the camera, depending on your operating system". With one camera that is harmless. With two, front and wrist can quietly trade places between sessions and nothing errors: half your episodes have the views swapped, training converges to a low loss, and the arm does something confident and wrong. On Linux, pin devices by their stable path under /dev/v4l/by-id/ instead of /dev/videoN, and open the dataset in the visualiser before paying for a run. If you are past that point, loss falls but the policy does nothing and camera not detected are the pages to read.

The AY-Robots download page for the desktop client that records LeRobot-format datasets
The desktop client download page. It is the client that writes the camera streams into the dataset.

Two routes to the same two-camera policy

You own the whole chain: USB topology, drivers, encoding, GPU rental, serving. Budget the first evening for bandwidth and device naming.

  1. Confirm both cameras stream together with v4l2-ctl before touching the arm.
  2. Record with lerobot-record and explicit camera keys. Video defaults to libsvtav1 at crf 30, keyframe every 2 frames, tunable via --dataset.rgb_encoder.*.
  3. For GR00T, convert to v2.1 and write a modality.json with both video keys before fine-tuning.
  4. Rent a GPU sized to the model: 80 GB for GR00T N1.7 and Pi0.5, 24 GB for SmolVLA and ACT.
  5. Serve the checkpoint on the machine wired to the servos if the task needs reaction time.
What you get in exchange

Full control of encoder settings, the option of three or four views if the bus can carry them, and no platform dependency. Keep the LeRobot pages for cameras and video encoding parameters open.

A decision rule that survives contact

Put the camera where the current view is blind. If the front camera loses the gripper behind the object at the moment of the grasp, a wrist view is worth the bandwidth and the latency. If the existing views already show the contact clearly, the second camera buys tokens and little else.

Unsure? Record two views and train both ways. A fine-tuning run on the 24 GB tier costs 1 to 3 USD, so training ACT once with both views and once with the front view alone is cheaper than a considered opinion. You can drop a camera from a dataset; you cannot add one, and the dataset, not the checkpoint, is the expensive artefact.

Record both views in the right format the first time

The AY-Robots desktop client records LeRobot-format datasets straight from a teleoperation session: every camera stream, the joint states and the episode boundaries, in the shape the trainers expect.

Get the desktop client
Do I need two cameras to train a policy on an SO-100?

No. One fixed third-person view is enough to train ACT or SmolVLA on a simple pick-and-place. Two views is the recommended default in the Hugging Face dataset guidance, and the Isaac-GR00T SO-100 example declares two video keys. Start with one if bandwidth forces it, but decide before recording.

Wrist camera or a second external view?

Wrist, in most cases. Close-range gripper-to-object information is what a third-person view is worst at: robomimic measured 43.3 percent success without the wrist view against 73.3 percent with it on a real Can task (Franka, BC-RNN, 30 rollouts), and Hsu et al. found it also improves out-of-distribution generalization. A second external view mostly buys redundancy against occlusion.

How much slower is inference with two cameras?

In the OpenVLA-OFT measurements on an A100, averaged over 100 queries, adding a wrist image and proprioceptive state to the same recipe moved latency from 0.0729 s to 0.1120 s per query, about 54 percent. Those two inputs are bundled in that row, so treat it as an upper bound on the camera alone. What it means depends on your budget: per-step figures here run from 20 ms for ACT to 485 ms for Pi0.5.

My second camera will not start and I get 'No space left on device'. What is wrong?

USB isochronous bandwidth, not disk. A USB 2.0 bus has about 384 Mbit/s for all isochronous streams and a single camera endpoint can reserve up to 196 Mbit/s of it, and cameras reserve on a declared worst case that is often far above what they actually send. Move the second camera to a different USB root controller, drop the resolution or frame rate, or reload the driver with modprobe uvcvideo quirks=128, which sets UVC_QUIRK_FIX_BANDWIDTH. That quirk only recomputes bandwidth for uncompressed formats, so it will not help an MJPEG stream, and switching to MJPEG lowers the bytes sent without necessarily lowering the reservation: the case The Good Penguin documented was a 320x240 MJPEG stream reserving 195 Mbit/s.

Does a second camera make training more expensive?

Not enough to change the tier. It adds tokens per step, but the A100 tier still lands in the 4 to 12 USD band and the 24 GB tier in the 1 to 3 USD band. The expensive consequence is re-recording: at LeRobot defaults, 100 minutes of arm time and 90,000 frames per camera.

Can a second camera make the policy worse?

Yes, and it has been measured. A 2026 study on a mobile SO-101 whose two body-fixed cameras already covered the workspace added a wrist camera as a third view: ACT fell from 90 to 86 percent and SmolVLA from 79 to 12 percent, over 100 trials per condition. The mechanisms given were the wrist view filling with the object surface at close range, and visual flow from the moving base. On a static arm with one front camera this is far less likely, but it is real.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started