The transfer gap
Measure what an embodiment change costs before your customer does, then close the gap cluster by cluster.
A policy that scores well on the embodiment it was trained on has told you
nothing about the one your customer runs.
transfer() measures the drop directly — same
task, same perturbation schedule, source embodiment against target — so the
gap is a number with an interval, not a hunch.
import roborama # reads ROBORAMA_API_KEY from the environment
policy = roborama.Policy.checkpoint(hf="acme/skill-v4", runtime="openpi")
gap = roborama.transfer(
policy=policy,
source="aloha-bimanual@fw3.1", # the embodiment it was trained on
target="g1-edu-pro@fw2.3",
task="fold_towel@v2",
)
print(gap.report())
# source: n=380 rate=0.942 ci95=(0.914, 0.961)
# target: n=340 rate=0.715 ci95=(0.665, 0.760)
# failure clusters opened in transfer: [grasp_slip, wrist_singularity]Reading the report
On fold_towel@v2, the canonical policy scores 0.942 (n=380, ci95
0.914–0.961) on aloha-bimanual@fw3.1, the embodiment it was trained on. On
g1-edu-pro@fw2.3 it scores 0.715 (n=340, ci95 0.665–0.760) — a 22.7-point
gap, and the intervals are nowhere near overlapping. This is not noise.
The number says how much. The clusters say what: grasp_slip and
wrist_singularity opened in transfer — failure modes the source embodiment
never exhibited. A different gripper geometry slips where the source's
didn't; the target's wrist reaches joint configurations the policy never had
to steer around. That is a diagnosis, not just a grade, and it is where the
playbook starts.
The playbook
- Read the clusters first
They partition the gap into causes you can act on. A gap that is mostly
grasp_slipis usually a data problem;wrist_singularitypoints at a contract or retargeting problem. - Pull the failing episodes
Filter episode artifacts by cluster and read the evidence: video for the what, MCAP for the why —
/joint_states_measuredagainst/joint_targets_commandedshows exactly where execution diverged from intent. - Fix what broke
Fine-tune on target-embodiment episodes (
run.export(format="lerobot")feeds most training stacks), or fix the declared contract — anaction_spaceorobservation_contractmismatch produces exactly these clusters. See policies. - Re-measure
Re-run
transfer()with the same pins. For before/after fine-tune deltas,compare()withpaired=Trueruns both checkpoints against the same initial conditions per episode pair — pairing halves the n you need to separate them. - Pin it so it can't reopen
Once the gap closes, add the target embodiment as a row in your
matrix()and to a CI gate. A transfer gap you closed once is a regression waiting for a firmware bump.
Measure before your customer does. The gap does not care which of you finds it first, but your release process should.