2026-10-31 · 16 min · robotics · humanoid · nvidia · reinforcement-learning · onnx
Why read this
Notabletop 60%Licence and cards read; the real ONNX graph run on CPU: latency, determinism, mask gating, action probe measured. Synthetic inputs, no simulator.
- Original analysis
- Runs on a laptop CPU
- A new technique
Robotics & embodiedCustom licencePractitioner model
How this was scored
- Is it new?
- 2 of 3: A real new idea, method or capability
- Can I trust it?
- 2 of 3: Measures key facts from files, code or configs
- Can I run it?
- 2 of 3: Open code or weights with real limits
- Will I understand it?
- 2 of 3: Mechanism from first principles with figures
- Can I act on it?
- 2 of 3: A concrete recipe, numbers or comparison
- Will it last?
- 1 of 3: Relevant for months
- Does it affect many?
- 1 of 3: A specialist community
- Only here?
- 2 of 3: A teardown or measurement few others did
Score 63 of 100, ranked 314 of 666 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored
Metatron v1 is NVIDIA's controller for the Unitree G1 humanoid. The announcement (@HuggingPapers) calls it a unified policy that handles walking, scene interaction and recovery, with ONNX exports for real-time use. The weights are on Hugging Face as nvidia/Metatron-v1.
This article is a read and a run of that release. I read the licence and the model cards, inspected the ONNX graph, and ran the real policy with onnxruntime on a CPU, feeding it synthetic observations. Every claim carries one of three labels. Measured means I computed it from a file or ran it. Reported means it comes from NVIDIA's cards and I did not check it. Reasoned means arithmetic or inference from the first two.
What I did not do matters more than usual here, so first. There was no simulator and no robot. The policy is a closed loop: its output moves a body, the body's new state is its next input. I fed it made-up states and looked at single steps. That tells you what the network is made of, what it costs and how its inputs are wired. It tells you nothing about whether the G1 stays upright. I also found no paper for Metatron v1; the cards point at the earlier MaskedMimic work and at NVIDIA's ProtoMotions library, and I treat training claims as reported.
- task
- reinforcement-learning
- library
- onnx
- license
- other
- largest file
- 459.5 MB
- files
- 18
- downloads
- 1.2K
- likes
- 9
- languages
- en
repo last modified 2026-10-29
- license
- Apache-2.0
- branch
- main
- tests
- 289 files
- source
- 8.2 MB
- commit date
- 2026-10-30
by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded
local clone, 2026-10-31 at ebb656b — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount
shallow clone: counts describe the pinned tree, not the history
The short version
- One policy, 100.7 million parameters. The ONNX graph holds 100,740,114 parameters in 2,914 nodes, 97% of them in 1024-wide transformer matrices (measured). The file is 403,375,789 bytes in fp32, and its SHA256 matches the one in the repo's
config.json(measured). - It answers 29 joint targets plus gains. Inputs are 39 tensors holding 4,002 numbers; outputs are the PD target, stiffness, damping and the raw action (measured).
- The target is a plain affine map.
action = default_pose + scale * raw_action, with zero error over my runs, andscale * stiffnessis exactly one quarter of each joint's effort limit (measured, from config and graph). - It does not hit 50 Hz on my CPU. Median 78 ms with one thread, 53 ms with two, against a 20 ms budget (measured, shared box). Dynamic int8 quantization got the median to 22 ms but moved some actions by up to 0.53 in raw units (measured).
- Goal inputs are gated by masks, and the gating is exact. Noise in a masked-off tensor changed the action by exactly 0.0; switch the mask on and the same noise moved it by up to 0.76 (measured).
Licence
The repo tag says license: other, the card names the NVIDIA Open Model License, and the license file is the NVIDIA Open Model License Agreement. I read the two-page PDF (measured). It opens by saying that models are commercially usable, that you may create and distribute derivative models, and that NVIDIA claims no ownership of outputs. The grant is perpetual, worldwide, non-exclusive, royalty-free and revocable under section 2.1. Two things end it: suing anyone over the model's IP, and bypassing a guardrail "without a substantially similar Guardrail appropriate for your use case". If you redistribute, you pass on the agreement and a notice file saying "Licensed by NVIDIA Corporation under the NVIDIA Open Model License". Use has to follow NVIDIA's Trustworthy AI terms.
Three limits are specific to this release. The licence covers weights, configs and exports, and the source code (ProtoMotions) stays Apache-2.0 (reported, README). Motion datasets, recovery clips, robot meshes and game assets are not included and have their own terms (reported). And the card says that permission to show a clip inside a demo does not mean you may redistribute it as a dataset. So you can ship the policy; you cannot assume you can ship what it was trained on. I am not a lawyer, and the full agreement governs.
What is in the repo
The repo has three packages (measured, file listing and sizes).
| Package | What | Size |
|---|---|---|
G1/ | PyTorch checkpoint inference_last.ckpt, resolved config, experiment code | 459,455,983 bytes |
demo/model.onnx | single-robot browser pipeline, with observation processing and action conversion inside | 403,375,789 bytes |
racer/network.onnx | six-robot batched network for a browser game, observations computed outside | 403,036,698 bytes |
The card says the demo and Racer exports carry the same learned weights in different graph layouts, and are not interchangeable (reported). It says the six-robot path matched the single-robot export to a maximum absolute action error of 5.55e-6 on 60 test states (reported). I downloaded only demo/model.onnx and the 439 KB graph skeleton, and did not open the Racer file or the PyTorch checkpoint.
One more reported detail changes how to read the release: the native PyTorch preset masks the scene inputs, while the 39-input ONNX keeps them. The card says weights retain scene conditioning, but "a scene-capable host must supply the matching object measurements", and that no scene provider ships with ProtoMotions.
What the model is, as reported
The card describes a MaskedMimic-style student: a spherical conditional VAE with a 64-dimensional latent, and a six-layer, four-head, 1024-wide transformer for the prior and the conditioned action decoder (reported, G1 card and resolved_configs.json). It was trained in IsaacLab by supervised imitation from frozen experts for motion tracking, recovery and scene interaction, on BONES, AMASS/LAFAN, fall-recovery and SAMP interaction data (reported). Inference uses the learned prior with no teacher, and the browser graphs export that deterministic path only.
The config also fixes the control contract: 50 Hz control with a 1000 Hz MuJoCo timestep, a 29-joint G1 and use_implicit_pd: true (measured, from resolved_configs.json). The policy is told five future frames, the history is eight past states, and the robot's own body poses are not inputs; they are recomputed inside the policy from joint readings and the torso pose with a forward-kinematics kernel (reported).
The graph
I inspected the ONNX with the onnx package (1.23.2). The graph was exported by PyTorch 2.11.0 at opset 17 (measured).
The 39 inputs are 4,002 numbers: 3,808 floats and 194 booleans (measured). Those numbers are not spread evenly:
- State now, 75 numbers: 29 joint angles, 29 joint speeds, 3 angular velocities, a torso quaternion and positions.
- History, 1,013 numbers: eight past states of joints, speeds, rotation and raw action, plus nine past raw actions. Two raw-action histories appear, an eight-step one and a nine-step one, paired as the card describes.
- Pose goals, 2,450 numbers: five future frames, each with 33 bodies of position and rotation, once in a global frame and once in a local frame, plus masks and time offsets. That is 61% of the input.
- Style samples, 435 numbers, velocity command, 3 numbers, and scene boxes, 26 numbers.
So 61% of the input is a goal you may leave empty. A walking command uses only the velocity pair and its mask.
The operator histogram of the 2,914 nodes is mostly plumbing (measured): Mul 397, Unsqueeze 339, Add 321, Reshape 303, Gather 232, MatMul 223, Sub 213, Slice 162, Concat 141, Squeeze 127, Gemm 70. Twelve LayerNormalization nodes and six Softmax nodes are the transformer: one six-layer stack, each layer with a 1024-by-3072 attention input, a 1024-by-1024 output and a 1024-by-4096 feed-forward pair (measured). That stack alone is about 75.5 million of the 100.7 million parameters (reasoned, 6 layers of 12.6 million). The rest is the observation processing, sines, cosines and quaternion arithmetic that the export bakes in (Cos 10, Sin 10, Atan 7, ReduceL2 8), which is why the graph has so many small ops. The card says the demo export includes observation processing and action conversion, and the node list agrees.
For scale, the MotionBricks article counted 194.5 million parameters for NVIDIA's motion generator for the same robot, in three networks. Metatron is about half that size, and the two do different jobs (reasoned). MotionBricks writes a short clip of poses from recent and target poses, with replanning at 10 Hz in the paper. Metatron takes poses, speed commands and the robot's own state, and writes joint targets at the control rate. One is a planner of motion, the other is a tracker that turns motion into torque-level commands. They could be stacked, but I found no card that says NVIDIA does so, so that is my reading, not a claim about the release.
Reading the action
Inputs are half the contract. The outputs are action (29 joint position targets), stiffness and damping (29 each), and raw_action, which is fed back as history (measured).
I checked how they relate. With the config's pd_action_offset (the default standing pose: hips at -0.312, knees at 0.669, ankles at -0.363 rad and so on) and action_scale, the relation is exact:
action = pd_action_offset + action_scale * raw_action
The maximum difference over the run was 0.0 in fp32 (measured). The scales are not arbitrary either. For all 29 joints, action_scale * stiffness / effort_limit equals 0.25 to four decimals (measured, from the config). A raw action of 1.0 therefore asks the PD controller for a torque equal to a quarter of the joint's effort limit at zero velocity, in every joint (reasoned). The effort limits range from 5 Nm for the wrist pitch and yaw to 139 Nm for the hip roll and knee, and the gains scale with them: hip roll and knee at stiffness 99.1 and damping 6.31, shoulders at 14.25 and 0.907.
Stiffness and damping come out of the network as outputs, but in my runs they were the same constants as the config for the same joint (99.098 for the knee, 14.251 for the shoulder). I did not test whether they ever vary. Reading the graph, they look like per-joint constants emitted so that a browser host does not need the config (reasoned).
I did not get joint position limits. The only copy I could try was the robot description inside ProtoMotions, and the session could not reach that repository, so I do not compare targets to angle limits and I won't quote limits from memory. The torque view above is what the files give: over 400 random synthetic states the largest raw action seen was 1.13 in magnitude (the 99th percentile of all outputs was 0.70), which is at most 28% of an effort limit at zero velocity (measured, reasoned). The caveat is large. My synthetic histories were random noise, which feeds the network inputs it never sees in training, so these ranges describe my probe, not the robot in motion.
Running it
Setup: Python 3.13, onnxruntime 1.31.1, the CPU provider, on a shared 4 vCPU Intel Xeon at 2.10 GHz, which other jobs were using at the same time (load average above 4). Memory peak was about 700 MB. The inputs were synthetic: the default pose, identity-ish rotations, a velocity command, goals and scene masked off unless I say otherwise. I ran with nice -n 19.
Latency (measured). 20 warm-up calls, then 300 timed calls, batch 1, fp32:
| Threads | Median | 95th pct | 99th pct | Best | Over 20 ms |
|---|---|---|---|---|---|
| 1 | 78.1 ms | 93.4 ms | 99.0 ms | 69.3 ms | 300 of 300 |
| 2 | 52.6 ms | 92.3 ms | 115.6 ms | 40.1 ms | 300 of 300 |
At the 50 Hz the config specifies, a step has 20 ms. No call met it, not even the best at 40 ms. The numbers are noisy because the box was shared, so read them as "tens of milliseconds, not single digits". I take the second thread's gain as modest and the tail as worse, which is the usual sign of a shared machine.
Why so slow (reasoned). A single step reads all 403 MB of weights once, since every matrix is used. At 78 ms that is 5.2 GB/s, and at 53 ms it is 7.7 GB/s, which I expect is low for this hardware, so I suspect the pass is limited by how the box and runtime move weights rather than by arithmetic. To fit 50 Hz the same weights would need 20.2 GB/s. I did not profile it to confirm. The card suggests a way out that I did not use: WebGPU is recommended, and "CPU/WASM is a fallback and does not guarantee real-time six-robot operation" (reported). That sentence matches what I saw.
int8 (measured). I applied onnxruntime's dynamic quantization to the MatMul and Gemm nodes, which took 8 seconds and made a 101.8 MB file (a quarter of the size). On the same CPU with 2 threads the median fell from 53.1 to 22.1 ms, with a 95th percentile of 45.2 ms, over 100 random synthetic states. It still misses 20 ms at the tail. And accuracy is not free: the largest raw-action difference from fp32 was 0.53, the mean 0.092 and the 99th percentile 0.31, which is a joint-target difference of up to 0.138 rad (measured). The inputs were random noise, so a flat softmax would flip easily and a real state might fare better, but I cannot say that. For a robot I would treat this as a warning that a quantized policy needs its own closed-loop testing, not as a recipe.
Determinism (measured). The same input run 20 times gave bit-identical outputs, in both thread settings, and one thread and two threads gave identical raw_action (maximum difference 0.0). This fits the card's "deterministic learned prior": there is no sampling at inference, so the same state gives the same action. It is a property of this graph on this CPU provider, not a promise about WebGPU or other hardware.
Poking the inputs
Because the network is a function, you can test it like one. I wrote a probe (probe.py, probe2.py) that builds a random but plausible state: joints near the default pose plus noise of 0.08 rad, speeds of about 0.5, a slightly tilted torso, random action histories, and a velocity command in a box of plus or minus 1 m/s. Then it perturbs one input and records how the 29 raw actions move. All of this is measured on synthetic inputs.
Masks gate exactly. With goals, style and scene masked off, I added Gaussian noise of standard deviation 1 to the global and local reference positions and rotations, the style pose and velocity samples, the time offsets, and the scene centres and extents, one tensor at a time. The largest change in any action was 0.0, every time. With the first goal frame's masks switched on and a goal 0.5 m up, noise of 0.3 in the reference positions moved an action by up to 0.76, in the reference rotations by up to 0.29, and noise of 0.5 in the style pose by up to 0.52. A scene box that was valid and visible, against an empty scene, changed an action by up to 1.33. A box that was visible but not marked valid changed nothing (0.0). So an empty scene is truly empty, and "valid" is the switch that counts. This is how the card's claim that the browser demo feeds the empty scene when there is no scene works out in the graph.
The velocity command is strong. Turning the velocity mask off changed an action by up to 1.02, and setting a command of 1.5 m/s forward moved an action by up to 1.04. Gain, the L2 change in the 29 raw actions per unit change of the input, is 0.72 for the average velocity (measured).
What the state does. With a random-direction perturbation of norm 0.05 in each tensor, the gain was 5.75 for the torso quaternion, 1.07 for joint angles, 1.10 for the historical torso rotation, 0.37 for the nine-step action history, 0.20 for the eight-step historical raw actions, 0.16 for historical joint angles and only 0.037 for joint speeds (0.012 for the history of speeds). The torso orientation is by far the most sensitive input, which is what you would want for balance, and speeds are surprisingly quiet at this operating point. My perturbation of the quaternion leaves it non-unit, so that number is partly a statement about what the network does with an unnormalized rotation; read it as a ranking, not a Lipschitz constant. current_root_pos, current_root_rot and noisy_anchor_pos had zero gain with goals off, which fits them being used only to move goal frames into the body frame (reasoned).
Joints are coupled. The heat map above is the mean absolute central-difference Jacobian of the raw action with respect to the measured joint angle, over six states (step 0.01 rad). The mean diagonal entry is 0.242 and the mean off-diagonal 0.106; the largest single entry is 0.64. A joint's reading moves every other joint's action, which a hand-tuned per-joint controller would not do, and is the plain consequence of one transformer reading everything at once. The column with the biggest norm was the waist roll (1.40), followed by the right hip roll (1.28) and the left hip roll (1.15). I do not read much into the ordering with six states.
What this does and does not show
It shows that the release is real and checkable: the hash matches, the graph loads in a stock runtime, the outputs follow the documented mapping and the mask logic does what the card says. The sizes are modest enough to run on a laptop at a few steps a second.
It does not show that the policy walks, recovers or interacts well. For that you need rollouts, and NVIDIA's own evaluation is, in the card's words, "integration checks, not a comprehensive task-success or safety benchmark". The card also warns that this is "not a safety controller" and that hardware use needs independent validation, joint and torque limits, supervision and an emergency stop. Nothing I measured contradicts that, and a clean run of single steps would not tell me if it did.
Three practical things follow from the numbers.
- Budget your runtime before your robot. A 50 Hz loop needs the whole step in 20 ms, and on a plain CPU this graph took 53 to 78 ms. The G1 has an onboard computer I know nothing about; the card names WebGPU for browsers and the PyTorch package for simulator and hardware use, and I tested neither.
- Feed masks and values together. The goal tensors are inert when masked off and decisive when on, so a host that leaves stale numbers in a tensor with its mask on will steer the robot with them. Test the mask logic on your side.
- Pin the interface. The card says to preserve joint order, quaternion convention, history order, normalization and control rate. My probe depends on all of them, and I only checked the ones the config makes explicit.
What I did not run
- No simulator, no MuJoCo or IsaacLab, no closed loop. All results are single-step on synthetic inputs.
- No PyTorch checkpoint (459 MB), no Racer export, and no joint position limits.
- No GPU or WebGPU timing. All timings are CPU, shared, fp32 unless stated, with one machine.
- No training reproduction; the card says the release is not a training-reproduction package and includes no dataset.
- No paper: I looked for one and the release points only to MaskedMimic and the ProtoMotions code.
The probe scripts, the graph inspection and the raw numbers behind the tables are described above in enough detail to repeat on your own machine; the model is 403 MB and loads in under 2 seconds.