Universal ONNX policy deployment: any trained control policy, any robot — bound by name, never by index.
The efferent pathway carries motor commands from the brain to the muscles. This package is that pathway for robot policies.
Above: the same trained policy, correct config vs. the classic off-by-one
joint-order bug — then efferent doctor catching that bug class from the
network alone, printing the exact mapping. Try it in 60 seconds, no robot:
A policy trained in MuJoCo, Isaac Lab, Isaac Gym, or mjlab is just an ONNX
network plus an implicit contract: what its observation vector contains, what
its actions mean, and which joints it drives. efferent makes that contract
explicit and portable, so deploying a policy on a new robot is configuration —
not another hand-written deploy script.
policy package (.app) robot descriptor (yaml)
policy.onnx joints in HARDWARE order
manifest.yaml + limits, safe gains -> efferent run
(obs recipe, action backend driver id
contract, joints in
POLICY order)
The runtime joins the two by joint name. The hand-maintained integer
joint_mapping arrays that deployment repos warn about become a derived,
validated artifact — a wrong joint name is a startup error, never a fallen
robot.
The bug class that fills every deployment repo's issue tracker — obs joint order permuted, angular velocity in the wrong frame, command dims wired to nothing, pipeline drift vs. the training-side rollout — none of it crashes, all of it makes robots fall, and it's usually debugged afterwards by staring at numbers. The doctor catches it beforehand by probing the actual network:
import efferent
report = efferent.doctor("policy.app", robot="g1.robot.yaml")
assert report.passed, str(report)Zero-config: efferent doctor policy.onnx works on a bare ONNX with no
manifest and no YAML at all — joint count comes from the action output, and
the obs layout is inferred by scanning the network's own sensitivity
structure for the joint-indexed blocks (validated on mujoco_playground's
LEAP-hand policy: locates joint_pos/joint_vel at 94% diagonal confidence with
zero configuration). When a policy has a fully-coupled Jacobian (dynamic
quadruped gaits do), the doctor says "not inferable" and reports the
high-influence dim groups instead of hallucinating a layout.
Checks include
finite-difference joint-order fingerprinting — a position policy must
respond to joint i's position chiefly with action i; if the response
pattern is instead a permutation, the doctor fails and prints the mapping it
observed so you can fix the wiring in one edit. Plus: dead-term detection
("the robot ignores the joystick"), numeric robustness under extreme inputs,
obs-recipe-vs-network size, robot binding, gain sanity, and — given an
npz of obs/actions recorded from your training framework's play script
(--reference rollout.npz) — bit-level replay divergence between training
and deployment pipelines.
The doctor probes the network; efferent audit checks the code in front of
it — the obs builder where the frame/order/scale bugs actually live. Log raw
states plus the obs vectors your pipeline built (npz with obs, q, dq,
and optionally quat/gyro/lin_vel/command/action/t), then:
efferent audit policy.app --log deploy_log.npz --robot robot.yamlEvery observation term is rebuilt from the raw states (efferent's obs builder acting as the reference implementation of the manifest semantics) and diffed per term. Mismatched terms get hypothesis-tested against the classic bugs, and the report names the fix:
[FAIL] base_ang_vel dims 0..2 max|err| 1.085
logged values match the WORLD-frame ang_vel - training expects the
BODY frame (apply quat_rotate_inverse before building the obs)
[ok] joint_pos_rel dims 3..6 max|err| 0
Detected hypotheses: world-vs-body frame on velocity terms, missing/extra scale factors (with the fitted factor), and permuted joint columns (with the observed mapping). No hardware risk — it runs on a log file.
pip install -e .[dev]
# build the self-contained demo (2-DOF arm, zero-action hold policy)
python examples/make_demo_policy.py
# what does this policy expect?
efferent inspect examples/demo_arm2.app
# run it in MuJoCo — same runtime, same code path as real hardware
efferent validate examples/demo_arm2.app --robot examples/robots/arm2.robot.yaml --duration 5
# full loop with nothing sent to the robot
efferent run examples/demo_arm2.app --robot examples/robots/arm2.robot.yaml --dry-runFor the G1 and Go2 examples, download the official Unitree MuJoCo models
first (~45 MB of meshes, not committed): python examples/fetch_models.py.
Import an existing rl_sar-style deployment (G1 example):
efferent import --from rl_sar --base base.yaml --config config.yaml -o out/
efferent validate out/policy.app --robot out/g1.robot.yaml- Policy manifest (
manifest.yaml, inside the.appzip, or embedded in the ONNXmetadata_props) declares the observation recipe as ordered semantic terms (base_ang_vel,projected_gravity,command,joint_pos_rel,joint_vel,last_action, … with per-term scale and history), the action contract (joint_position_deltaetc., scale, clip, PD gains), and the driven joints by name in policy order. - Robot descriptor declares hardware truth: joints by name in SDK order, position/torque limits (authoritative — enforced by the safety layer over anything the manifest claims), safe gains, and which backend driver to use.
- Binding validates and joins the two at startup and derives all permutations.
- Runtime runs soft-start -> obs build -> ONNX inference -> action mapping -> safety clamps -> backend write at the manifest's rate, with an action rate limiter, watchdog, and estop-on-failure.
- Backends implement five methods (
connect/read/write/estop/close) and are discovered via theefferent.backendsentry point, so vendor support ships as separate pip packages. Simulators are just backends (advances_time = Trueskips wall-clock pacing), which is why sim-to-sim validation is the same command with a different descriptor.
Real deployments are never "run one policy" — they are damp -> stand -> policy -> recover sequences, and that protocol traditionally lives in hand-written C++ per robot. In efferent it is YAML:
robot: g1_29dof.robot.yaml
initial: stand
states:
stand: {type: pose, target_from: jab, duration_s: 0.4, gain_scale: 3.5}
jab: {type: policy, policy: g1_jab.app, hook: jab_hook.py, duration_s: 11.0}
done: {type: damp, duration_s: 0.5}
transitions:
- {from: stand, to: jab}
- {from: jab, to: done}target_from: jab means "stand exactly where the jab policy expects to
start" — because policies now declare a start envelope in their manifest
(expected pose, joint tolerance, max velocity, max tilt). The runtime checks
the envelope at every handoff and refuses an out-of-envelope transition
before a single policy command is sent:
handoff refused - robot state is outside the policy's start envelope:
joint 'waist_roll_joint' at +0.398 rad, policy expects +0.032 (tol 0.3)
base tilted 1.63 rad from upright (max 0.35)
The gate for all of this (examples/g1_jab/validate_fsm_mujoco.py) spawns
the G1 in a perturbed pose and runs the full protocol in MuJoCo: stand snaps
to the envelope pose, the handoff is approved, and the jab runs its full 11 s
at 3 degrees final tilt. Two protocol lessons are encoded in the example
config's comments: a free-standing robot under pure damping topples, and a
joint-PD stand has no balance feedback, so the stand phase must be brief —
snap and hand over.
examples/g1_jab/ deploys a real motion-imitation policy (Unitree G1 29-DoF
jab, trained in a unitree_rl_mjlab fork — 154-dim obs, 29 actions) through
this runtime:
make_jab_package.pytranslates the training repo'sdeploy.yamlinto a manifest (with self-checks) and packsg1_jab.app.jab_hook.pyimplements the two motion-specific observation terms (custom.motion_command,custom.motion_anchor_ori_b) as custom-term hooks — a faithful port of the C++ deploy stack'sState_Mimic.cpp.validate_jab_mujoco.pyruns the sim-to-sim gate on the official G1 MJCF: spawn at the reference motion's frame 0, hand control to the policy, and check the deploy stack's own criteria (never exceeds the 1.0 rad bad-orientation estop threshold, stays standing).
Result: PASS — 11 s full motion, max tilt 6.3°, mean joint tracking error
0.095 rad, identical metrics on Windows and on a Modal Linux container
(modal run scripts/modal_verify.py).
examples/go2_walk/ deploys rl_sar's bundled Go2 velocity policy (robot_lab
/ Isaac Lab-trained, TorchScript converted to ONNX with bit-level equivalence
verified): configs imported automatically with efferent import, doctor
PASS, and the sim-to-sim gate walks the official Go2 MuJoCo model 1.9 m
forward in 8 s at 14.7° max tilt — same runtime, same commands, different
robot, different vendor model (whose MJCF even lists joints in a different
order than the SDK; by-name binding absorbs it).
Two spawn lessons are baked into the MuJoCo backend because of this policy:
auto_base_height / settle_s for floating-base spawns, and the knowledge
that mimic policies must start at the motion's frame 0 (they balance actively
— a pure PD hold of their default pose falls over).
| Component | State |
|---|---|
| Spec + binding + runtime + safety | Done - implemented, tested (76 tests, CI on 3 OS x Python 3.10-3.13) |
| Mock backend | Done |
| MuJoCo backend (any MJCF, actuator-free PD via qfrc) | Done |
| Importers: rl_sar configs, Isaac Lab / mjlab env.yaml | Done - both cross-validated against real training runs |
| CLI: doctor / audit / inspect / pack / import / validate / run (dry-run, hooks) | Done |
| doctor: zero-config fingerprinting, permutation detection, recurrent-export check | Done |
| audit: per-term obs verification vs deploy logs (frame / scale / permutation / sign / deg-rad / lag) | Done |
| Real-policy proofs (G1 29-DoF jab mimic, Go2 locomotion) + Modal cloud verification | Done |
| Recurrent policies | Done - multi-tensor state (LSTM h/c) + per-term history |
| Multi-policy FSM + start envelopes (refused handoffs) | Done - full stand-to-mimic protocol gated in MuJoCo |
| Unitree DDS backend (G1/Go2, unitree_sdk2py) | Validated against the vendor DDS simulator (real IDL, CRC, per-joint direction check); hardware session pending - see examples/g1_jab/RUNBOOK.md |
| ros2_control bridge backend | Planned (#1) |
- Binding refuses to start on any joint-name mismatch or a default pose outside hardware limits.
- Descriptor limits clamp every outgoing command (positions, feed-forward torque, and kp scaled so PD torque cannot exceed the limit at the current error).
- Soft-start interpolates to the default pose with safe gains before the policy gets control; estop drops to damping on any failure; a watchdog trips on loop/state stalls.
--dry-runruns the entire loop without sending a single command — always the first thing to run against a new robot.
Prior art this design learned from: kinfer's self-describing artifacts, rl_sar's deployment contract vocabulary, and unitree_rl_mjlab's deploy stack.

