The AY-Robots SO-100 hub page, the arm that LeKiwi mounts on a three-wheel holonomic mobile base
mobile manipulationLeKiwiMobile ALOHAimitation learningdataset designVLA policies

Mobile Manipulation: What Changes When the Robot Base Moves

AY-Robots ResearchAugust 23, 202619 min read

Bolting an arm onto a mobile base adds three action dimensions, swaps position control for velocity control and breaks table-top policies. What that costs, and how to fix it.

A table-top arm rests on one quiet assumption: the world holds still unless the gripper moves it. The camera sees the same background in frame 1 and frame 900, and a shoulder angle of 42 degrees means the same point in space every time. Much of the imitation learning stack leans on that without saying so.

Put the arm on wheels and it is gone. The background becomes a function of where you drove, joint angles no longer pin down a point in the room, and a new channel appears in the action vector that behaves nothing like the other six. What changes, using the LeKiwi implementation in LeRobot and the published results, and why the policy you trained on a fixed table will not carry over.

What you need to know

  • A LeKiwi frame has 9 channels, not 6: six arm positions plus x.vel, y.vel and theta.vel, mixing positions and velocities in one vector.
  • The base is commanded and read back in velocity. LeRobot never integrates it into a pose, so the dataset holds no absolute base position.
  • Mobile ALOHA measured the cost: 20 replays of one 6 second episode with a 180 degree turn landed about 10 cm off, along a 20 cm line.
  • SmolVLA and Pi0.5 pad state and action to 32 dims, so 9 fits untouched. ACT sizes its heads from your dataset instead.
  • GR00T needs a NEW_EMBODIMENT modality config with a third key group for the base.
  • Co-training is the key result: mixing fixed-table episodes into a mobile dataset lifted ACT sub-task success from 0 to 80 percent.

The action vector gets longer, and that is the smallest problem

In LeRobot 0.6 the LeKiwi class defines one state feature dictionary and reuses it for both the action space and the non-camera half of the observation. Since LeRobot dataset columns come straight from these keys, it is the fastest way to see what a mobile episode contains.

python
# src/lerobot/robots/lekiwi/lekiwi.py  (lerobot main, read 2026-08-24)
@property
def _state_ft(self) -> dict[str, type]:
    return dict.fromkeys(
        (
            "arm_shoulder_pan.pos",
            "arm_shoulder_lift.pos",
            "arm_elbow_flex.pos",
            "arm_wrist_flex.pos",
            "arm_wrist_roll.pos",
            "arm_gripper.pos",
            "x.vel",
            "y.vel",
            "theta.vel",
        ),
        float,
    )

@cached_property
def action_features(self) -> dict[str, type]:
    return self._state_ft          # action space == state space
Six positions in degrees, two linear velocities in m/s, one angular velocity in deg/s.

Six of those behave like a fixed SO-100: write a goal, the servo goes there, read back where it is. The last three do not. x.vel is a rate. Writing 0.25 does not mean go to 0.25, it means keep moving until told otherwise. Where the robot ends up is the integral of everything you wrote, plus whatever the floor did to the wheels.

PropertySO-100 on a tableLeKiwi (arm plus 3-wheel base)
Channels in state and action69
Control modeposition for all 6position for 6, velocity for 3
Units in one vectordegrees onlydegrees, m/s and deg/s mixed
Read backPresent_PositionPresent_Position and Present_Velocity
Absolute pose in the datasetimplicitnone, only rates are stored
Cameraswherever you clamped thembolted to the robot
Error accumulates over an episodenoyes, on the base
The base is not calibrated, on purpose

The LeRobot guide says it plainly: The wheel motors don't have to be calibrated. Arm calibration maps raw ticks onto a shared joint convention so a network trained on one arm works on another. A rotating wheel has no home position, so nothing guarantees two LeKiwis turn the same commanded x.vel into the same distance. Wheel wear, floor friction and battery charge all get a vote.

Velocity, not position: why drift is structural

LeKiwi's base observation is not a pose estimate. get_observation sync-reads Present_Velocity from wheel IDs 7, 8 and 9 and pushes the speeds through the inverse of a 3x3 matrix built from the mounting angles (240, 0, 120 degrees offset by -90), wheel radius 0.05 m, base radius 0.125 m. Nothing integrates the result into a pose, and the schema has no place to put one.

So the network never learns where the robot is. It learns what the wheels were doing and infers position from the image alone. Every source of base error is invisible to the state vector and gets corrected visually, or not at all.

Open-loop replay stops working the day you add wheels

Mobile ALOHA replayed a 300 step (6 second) demonstration containing a 180 degree turn of roughly 1 m radius, 20 times, objects restored to identical positions. All 20 end-effector landings sat about 10 cm to the left of the original, along a 20 cm line. Their summary: open-loop replaying a demonstration with objects restored to the same configurations will achieve zero whole-task success. On a base, replay tells you nothing.

The paper blames "the stochasticity of the ground contact and low-level controller", and states the general case up front: a small deviation in base pose leads to large drifts in the arm's end effector pose. The trigonometry is unforgiving. A 2 degree heading error is about 3.5 cm of lateral offset per metre of standoff, and centimetres are the difference between grasping a handle and knocking it over.

  • Wheel slip. Omni wheels are rollers on a hub, built to slide sideways, which is what makes dead reckoning from wheel speed optimistic.
  • Command latency. Mobile ALOHA measured a delay between target and actual base velocity far larger than on the arms. The two halves of your action vector do not land together.
  • Loop jitter. The host runs at max_loop_freq_hz = 30, with a comment telling you to lower it if the robot jitters. Every overrun is distance nobody accounted for.
SystemBase representationTotal action dimsMobile data used
Mobile ALOHA (2401.02117)linear and angular velocity16 (14 arm plus 2 base)50 demos per task, 20 for two tasks
Pi0.5 (2504.16054)linear 2D plus angular 1D velocity18 or 19, includes a torso liftabout 400 hours across about 100 homes
LeKiwi (LeRobot 0.6)x.vel, y.vel, theta.vel9 (6 arm plus 3 base)your call, docs suggest 50 episodes

Three groups, three budgets, one design decision. Mobile ALOHA concatenates 14 joint positions with the base's linear and angular velocity into a 16-dimensional action, reusing existing imitation code almost unchanged. Pi0.5 does the same at 18 or 19 dims, adding a torso lift. The flow-matching action expert that consumes those dimensions is taken apart in the pi-zero write-up. None of the three feeds the policy an absolute pose.

The AY-Robots policies comparison table showing GR00T N1.7, GR00T N1.5, Pi0.5, SmolVLA and ACT with parameter counts, GPU tier, inference latency and minimum episode counts
The five trainable policies side by side. The latency column decides how far a moving base travels between decisions.

Can the five policies even eat a 9-dimensional action?

Answer this before recording anything. Two pad the vector, one sizes itself from your data, two need a config edited by hand. Widths below come from the LeRobot and Isaac-GR00T sources read on 2026-08-24; episode counts and GPU tiers from the policy pages.

PolicyHow the wider vector is handledMin episodesGPU tier
ACTno padding fields, heads sized from the dataset50RTX 4090 or any 24 GB card
SmolVLAmax_state_dim 32, max_action_dim 32, so 9 is padded30RTX 4090 or any 24 GB card
Pi0.5same 32-dim padding50A100 80 GB or H100 80 GB
GR00T N1.7NEW_EMBODIMENT config declaring the extra key group50A100 80 GB or H100 80 GB
GR00T N1.5same, and no LeKiwi guide exists for it here50A100 80 GB or H100 80 GB

That padding is why SmolVLA and Pi0.5 are the least painful start: nine channels plus 23 zeros is a 32-vector, and the same checkpoint shape covers both. ACT is the opposite: the docs say it "will automatically adapt to the number of motor states, motor actions and cameras of your robot ... which have been saved in your dataset", which is convenient until you run a LeKiwi checkpoint on a plain SO-100 and the head has three columns too many.

GR00T does not guess your key groups

Fine-tuning takes --embodiment-tag NEW_EMBODIMENT with --modality-config-path, and the shipped SO100 example declares exactly two state and two action keys: single_arm and gripper. A LeKiwi dataset has a third group it knows nothing about. Declare it in your config and in meta/modality.json, or you train on a slice of your data. GR00T also wants LeRobot v2.x, so a v3.0 dataset must be converted to v2.1, its own failure mode.

python
# examples/SO100/so100_config.py in NVIDIA/Isaac-GR00T (read 2026-08-24)
so100_config = {
    "video": ModalityConfig(delta_indices=[0], modality_keys=["front", "wrist"]),
    "state": ModalityConfig(delta_indices=[0],
                            modality_keys=["single_arm", "gripper"]),
    "action": ModalityConfig(
        delta_indices=list(range(0, 16)),        # 16-step horizon
        modality_keys=["single_arm", "gripper"],
        action_configs=[
            ActionConfig(rep=ActionRepresentation.RELATIVE,  # delta from current state
                         type=ActionType.NON_EEF, format=ActionFormat.DEFAULT),
            ActionConfig(rep=ActionRepresentation.ABSOLUTE,  # gripper target
                         type=ActionType.NON_EEF, format=ActionFormat.DEFAULT),
        ],
    ),
    "language": ModalityConfig(delta_indices=[0],
                               modality_keys=["annotation.human.task_description"]),
}
register_modality_config(so100_config, embodiment_tag=EmbodimentTag.NEW_EMBODIMENT)
A LeKiwi version needs a third entry in the state and action key lists.

Note the two ActionConfig entries. The arm is RELATIVE, a delta from the current state, and NON_EEF, which the file itself comments as joint-space rather than end-effector; the gripper is ABSOLUTE. N1.7's headline relative end-effector action space is the other setting of that type field, not what the SO100 example uses. A base group forces the same call, and RELATIVE is wrong there: x.vel is already a rate, so a delta from it is an acceleration. Get that backwards and the model trains cleanly, then drives inexplicably.

What breaks when you move a table-top policy onto a base

What you seeMechanismWhat to do
Checkpoint refuses to loadhead is 6 wide, robot wants 9retrain, no reshaping trick is worth trusting
It loads but the base never movesbase columns were zero in every training framerecord mobile episodes, padding creates no behaviour
Arm looks right, robot drifts out of reachno pose in the state, camera moved with the basevary the standoff across episodes
Works in one room onlybackground was constant, so the model used it as a position sensorrecord in several locations
Grasp 2 to 3 cm off in one directionheading error times reach lengthkeep the wrist camera in the observation
Base surges then stopsbase velocity lags the arm command inside a chunkstagger the base slice of the chunk

Row four deserves a moment. The LeRobot advice for a fixed arm, "Keep the cameras fixed and maintain consistent grasping behavior throughout the recordings", is right for a table and also produces a dataset where the background pixel at (10, 10) predicts the joint angle. On wheels that shortcut is harmful: the pixel now encodes where you drove, not where the object is. The fixed-arm version is at policy only works in one setup.

Putting the arm on wheels
What you gain
  • A workspace measured in rooms, not centimetres of reach
  • Tasks impossible on a clamped arm, such as fetching a towel from another surface
  • A base that backs away while both grippers hold something, which is how Mobile ALOHA opens a two-door cabinet
What it costs
  • Every fixed-arm checkpoint you own is now the wrong shape
  • Open-loop replay, your cheapest debugging tool, stops being informative
  • Drift is a property of velocity control on wheels, not a bug you fix once
  • The dataset must cover locations as well as object positions

The manual path: recording a whole-body LeKiwi dataset

This describes LeRobot main at 0.6.2; the latest tagged release when this was written was v0.6.1, 3 August 2026. LeKiwi splits across two machines: a Raspberry Pi 5 owning the servo bus and cameras, and your laptop owning the leader arm and the policy, over ZeroMQ on ports 5555 and 5556. The arm half of it is the ordinary SO-100 procedure, walked through end to end in the SO-100 setup guide.

  1. 1
    Install the LeKiwi extra on both machines

    It pulls in the Feetech SDK and ZeroMQ. Both ends of the link are LeRobot code.

    bash
    pip install -e ".[lekiwi]"
  2. 2
    Set motor IDs, arm first then wheels

    One control board carries everything. The command walks the arm from 6 down to 1, then the wheels as 9, 8, 7. The order is not optional.

    bash
    lerobot-setup-motors \
        --robot.type=lekiwi \
        --robot.port=/dev/ttyACM0
  3. 3
    Calibrate the arms, skip the wheels

    Over SSH on the Pi for the follower, then on the laptop with --teleop.type=so100_leader. Middle of range, Enter, then sweep every joint.

    bash
    lerobot-calibrate \
        --robot.type=lekiwi \
        --robot.id=my_awesome_kiwi
  4. 4
    Start the host on the robot

    This process owns the bus and both cameras. Its config carries a 500 ms watchdog that stops the base when no command arrives.

    bash
    python -m lerobot.robots.lekiwi.lekiwi_host \
        --robot.id=my_awesome_kiwi
  5. 5
    Teleoperate both channels at once

    Set remote_ip and port in the example first. The leader arm drives six joints, w/a/s/d the base, z/x rotate, r/f step through three speed modes. The docs table lists them as 0.4, 0.25 and 0.1 m/s; lekiwi_client.py defines 0.1, 0.2 and 0.3 and starts slow. Believe the code.

    bash
    python examples/lekiwi/teleoperate.py
  6. 6
    Record with both teleoperators in one loop

    The example instantiates an SO100Leader and a KeyboardTeleop and feeds both into record_loop. Defaults are small: NUM_EPISODES 2, FPS 30, EPISODE_TIME_SEC 30.

    bash
    python examples/lekiwi/record.py
  7. 7
    Train, then roll out

    lerobot-train reads the shape of your dataset, so the nine columns propagate on their own. lerobot-rollout needs the robot as well or it has nothing to drive: lekiwi_client takes the same remote_ip the teleop example uses. For slow VLAs, --inference.type=rtc keeps chunked execution smooth on the rollout side.

    bash
    lerobot-train \
      --dataset.repo_id=${HF_USER}/lekiwi_pick \
      --policy.type=act \
      --output_dir=outputs/train/act_lekiwi \
      --policy.device=cuda
    
    lerobot-rollout \
      --strategy.type=base \
      --policy.path=${HF_USER}/act_lekiwi \
      --robot.type=lekiwi_client \
      --robot.remote_ip=<your_pi_ip> \
      --task="Fetch the cube from the side table" \
      --duration=60
Three LeKiwi builds, and their servos are not interchangeable

SO-100 class arms run Feetech STS3215 at 7.4 V, and 12 V destroys them. LeKiwi ships as three builds and the bill of materials makes the split explicit: the 5 V and the wired build buy 7.4 V STS3215 units, while the 12 V build buys 12v ST3215 units for everything, 3 for the wheels and 12 for the two arms. It is not a mixed-rail chassis, it is a different parts list. One Motor Control Board carries all nine motors on the robot, so one supply feeds the whole bus. The expensive mistake is spares: dropping a 7.4 V STS3215 from an SO-100 parts bag into a 12 V chassis kills it on the first power-up. Check /robots/lekiwi against the build you actually bought.

The keyboard channel has a platform restriction

Recording control-flow keys work everywhere including headless SSH, but keyboard teleoperation of the base needs a global key backend: X11, Windows, or macOS with Accessibility granted. If the arm follows the leader and the base will not move, this is why.

The AY-Robots training matrix with five policy rows and four arm columns, each cell linking to a model and arm specific training guide
The find-your-combination matrix at /train. LeKiwi is a column, so ACT, SmolVLA, GR00T N1.7 and Pi0.5 each have a LeKiwi guide.

Co-training: the result that made mobile imitation work

Your old fixed-table data is worthless as a checkpoint and valuable as a co-training ingredient. The Mobile ALOHA recipe is almost insultingly simple: sample 0.5 from the static dataset and 0.5 from the mobile one, zero-pad the base columns of the static episodes, normalise on the mobile statistics alone, batch size 16. No image tricks, no domain adaptation, even though the static data was shot on a black tabletop with the arms facing each other.

Task and sub-task (ACT, 50 demos)Co-trainedNot co-trained
Rinse Pan: turn on faucet80%0%
Call Elevator: press button100%5%
Call Elevator: enter elevator95%0%
Call Elevator: whole task95%0%
Wipe Wine: whole task95%50%
Push Chairs: 5th chair (out of distribution)89%0%
Use Cabinet: whole task85%85%

Two things matter more than the headline. The gains sit in the precision sub-tasks: a faucet knob 0.7 cm in diameter, a 2 cm by 2 cm elevator button, the steps where the arm must visually servo to compensate for base error. And the last row is flat, 85 percent either way. Co-training fixes a specific weakness rather than multiplying performance, and reporting the row where it did nothing is why it is credible.

The practical version for a LeKiwi

Fixed SO-100 episodes for a similar task are training data for your mobile policy. Pad the three base columns with zeros, mix at roughly 50/50, normalise on the mobile set. Cheaper than recording 50 more mobile episodes. Borrow from the dataset directory; the workflow is at /learn/record-your-first-dataset.

Pi0.5 publishes the scaling curve: about 400 hours of mobile manipulator data from about 100 homes, yet 97.6 percent of first-stage training examples come from elsewhere, including non-mobile robots and web images. Their ablation trained on 3, 12, 22, 53, 82 and 104 environments, and average performance improves with more training locations, with the 104-location model matching a control trained directly on the test homes. Diversity of place bought the generalisation, which is why how you collect the data outranks which model you pick.

Two routes to a trained mobile policy

  1. Build the base. The LeKiwi bill of materials, read 2026-08-24, totals $482 for the 12 V build, $499 for 5 V and $184 for a wired base without arms.
  2. Flash the Pi, enable SSH, install LeRobot and the lekiwi extra on both machines.
  3. Set motor IDs, calibrate both arms, leave the wheels alone.
  4. Record, budgeting for locations and not object positions alone. The guidance of 50 episodes with 10 per location is optimistic once the base moves.
  5. Pick a policy your GPU can hold: ACT and SmolVLA fit 24 GB, GR00T and Pi0.5 want 80 GB.
  6. For GR00T, write your own modality config with the base key group and regenerate meta/modality.json.
  7. Train, then evaluate with lerobot-rollout. Do not use lerobot-replay as a success metric.
Where the day disappears

Not in the training. It goes into the two-machine setup, the ZMQ link, and camera indices that shuffle between reboots. See /fix/camera-not-detected.

Latency stops being an abstraction when the robot is moving

On a fixed arm, inference latency costs smoothness. On a moving base it costs position. Check which speed you are actually getting before you do this arithmetic: the LeRobot LeKiwi page prints a table of 0.4, 0.25 and 0.1 m/s, while lekiwi_client.py defines speed_levels of 0.1, 0.2 and 0.3 m/s and starts at the slow one. The wheels follow the code. The table below uses the code's fast mode of 0.3 m/s and its slow mode of 0.1 m/s, multiplied by the per-step inference time from the policy specs.

PolicyPer action stepDistance at 0.3 m/s (fast)Distance at 0.1 m/s (slow)
ACT20 ms0.6 cm0.2 cm
GR00T N1.7152 ms4.6 cm1.5 cm
GR00T N1.5165 ms5.0 cm1.7 cm
SmolVLA245 ms7.4 cm2.5 cm
Pi0.5485 ms14.6 cm4.9 cm

Those are multiplications, not measurements, and action chunking cuts both ways. SmolVLA and Pi0.5 predict 50 steps per call and ACT predicts 100, so the model is not consulted every step and the figure is amortised. What chunking does not fix is staleness: the last action in a 50-step chunk came from an observation 50 frames old, taken somewhere else in the room. That is why Mobile ALOHA staggers a chunk, executing the first k minus d arm actions alongside the last k minus d base actions.

Do not put the public internet inside this loop

The control loop is 20 to 485 ms per action step depending on the model, and public-internet round trips turn a working policy into a hesitant one. Remote inference is viable for slow pick-and-place, not for a robot translating at 0.3 m/s while it decides. Pick ACT or SmolVLA and run them next to the servos.

Before you record 50 episodes

  1. Confirm all nine keys appear in a recorded frame. Constant zero in x.vel means the keyboard teleoperator never connected, and you are recording a fixed-arm dataset with three dead columns.
  2. Record in more than one location even when the task is identical.
  3. Keep the wrist camera in the observation set. It is what lets the arm correct base error visually.
  4. Keep a fixed-table dataset for the same task. You are going to co-train with it.

Every model and arm combination has its own guide

LeKiwi is a column in the training matrix, so ACT, SmolVLA, GR00T N1.7 and Pi0.5 each have a guide written for that exact arm: GPU tier, dataset format, the defaults the trainer really sends, and what a run costs.

Open the training matrix

Questions people actually ask

Can I fine-tune my existing SO-100 checkpoint on LeKiwi data?

Not directly. A fixed SO-100 policy has 6 output channels and a LeKiwi dataset needs 9. For SmolVLA and Pi0.5 the padding to 32 dimensions makes the shapes match, but the base columns were zero in every frame the model saw, so it outputs zero there. The usable version is co-training: mix the old episodes in with the base columns zero-padded and sample roughly 50/50.

How many episodes does a mobile task need?

More than the fixed-table version, and nobody has published a clean number for a LeKiwi. The platform floor is 50 episodes for ACT, Pi0.5 and both GR00T variants, 30 for SmolVLA. Mobile ALOHA used 50 demonstrations per task and still needed co-training to clear 80 percent. Treat locations as a second axis: at 10 per location, 50 episodes buys 5 locations.

Does the desktop client record the base channel?

The client records LeRobot format datasets, episodes, camera streams and joint states, from a teleoperation session. For the base velocity channel specifically, the documented path is the upstream LeKiwi client scripts, which wire a leader arm and a keyboard teleoperator into one record loop. Bring the finished dataset to the training form by repo id.

Which model should I try first on a mobile base?

SmolVLA, for reasons that have little to do with quality: fewest episodes at 30, padding to 32 dimensions so the wider vector needs no config work, and a 24 GB card at roughly 1 to 3 USD per run. ACT is far faster at inference at 20 ms per step, which matters on a moving base, but it trains from scratch with no base model.

Why does replaying a recorded episode no longer reproduce it?

Because the base is velocity controlled and nothing corrects its position. Mobile ALOHA quantified it: 20 replays of one 6 second episode, objects restored, every landing about 10 cm off in the same direction, and zero whole-task success across seven tasks. Replay is a hardware smoke test now; judge a policy with a closed-loop rollout.

Sources

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started