A compressed allenai/MolmoAct2-SO100_101,
fine-tuned on an SO-101 cube pick-and-place dataset and built for on-device deployment
(Qualcomm IQ-9075, 2× Hexagon v73 NSP).
Two compressions are applied on top of the base checkpoint:
what
effect
layer prune
vision tower 25 → 10 blocks
5.44B → 5.22B parameters
token prune
grid token sampler, 16 tokens per crop
LLM prompt 496 → 136 tokens
Both are healed by training — the tower by embedding distillation, the token sampler by the
task fine-tune (it ships with no pretrained weights and is useless without one).
How it was built
Stage 1 — depth prune + embedding distillation. The vision tower keeps blocks
[0,1,2,3,5,6,7,8,9,24], chosen by per-layer SNR (10·log10(E‖in‖² / E‖out−in‖²); a high value
means the block barely changes the signal). Student block j inherits teacher block keep[j].
The connector taps move from [-3,-9] to [-1,-4] so the deep tap stays on the same teacher
layer (24) — which is why 24 is pinned in the keep set.
The pruned tower is then distilled against the teacher's connector output with
relative MSE + (1 − cosine). The MSE is normalised by target energy because these embeddings
carry massive activations (~1e2–1e3) and a raw MSE (~1e5) would drown the cosine term.
Held-out cosine, measured on an episode-level holdout rather than the training batch:
Stage 2 — task fine-tune. 6000 steps, effective batch 28 (4 × 7 GPUs), LoRA r=64 on the
VLM with a fully trainable action expert, use_grid_token_sampler=true so the sampler is
trained from its random initialisation. LoRA is merged into the base weights in this checkpoint.
Measured results
Open-loop action-chunk prediction on a held-out episode tail: 200 frames, identical frames for
every row (the hold baseline is 5.968 across all four, which is how you can tell). Frames whose
ground-truth chunk barely moves — episode-end padding — are excluded, since "hold still" scores
them perfectly and they say nothing about a policy.
checkpoint
MAE ↓
vs hold-still
step 0
step 29
base teacher, recalibrated, no fine-tune
10.125
1.696
4.333
14.828
10L student, distilled only
10.550
1.768
4.494
15.434
teacher fine-tuned — reference ceiling
4.737
0.794
2.813
6.200
this model
5.389
0.903
2.985
7.514
Task fine-tuning is what matters: it takes the teacher from 10.125 to 4.737 (−53%). Compression
then costs +13.8% on top of that, and the compressed model still beats holding still
(0.903 < 1.0).
Error grows along the chunk, so the cost depends on how much of the 30-step chunk you execute
before re-inferring:
steps executed
teacher
this model
gap
5 of 30
3.26
3.51
+7.7%
10 of 30
3.68
4.01
+9.1%
30 of 30
4.74
5.39
+13.8%
At 30 fps a 30-step chunk is a 1-second budget and the deployment pipeline runs well inside it,
so a shorter n_action_steps is a real lever rather than a compromise.
Limitations — read these
Open-loop MAE is a proxy, not a success rate. There is no simulator for this robot, so no
closed-loop number exists. The upstream compression study on the LIBERO track explicitly found
that its distillation cosine did not predict downstream success (two 0.970 configurations
scored worse than a 0.956 one) — treat any single scalar here with the same caution.
The two compressions are not separated. The +13.8% is layer pruning and token pruning
together against a reference that has neither. Which dominates has not been measured.
norm_stats.json was regenerated from the training dataset. The base checkpoint's joint
calibration is offset from this dataset by roughly 153° on shoulder_lift and 103° on
elbow_flex; feeding SO-101 states through the original statistics saturates the normaliser and
makes the model return its own q01 vector regardless of the image. If you use this model on a
different SO-101 setup, regenerate the statistics again.
Trained for one task family — 10 cube pick-and-place instructions, 624 episodes, 0.94 epochs.
The reference recipe's own data shows fine-tuning had not plateaued at that budget (its 12-layer
student went 88.5% → 94.5% between 2k and 5k steps), so this is likely under-trained.
Usage
python
1import torch
2from transformers import AutoModelForImageTextToText, AutoProcessor
34repo ="ukcastle/MolmoAct2-SO101-10L-VTP"5processor = AutoProcessor.from_pretrained(repo, trust_remote_code=True, extra_special_tokens={})6model = AutoModelForImageTextToText.from_pretrained(7 repo, trust_remote_code=True, dtype=torch.bfloat16
8).to("cuda").eval()910out = model.predict_action(11 processor=processor,12 images=[top_rgb, wrist_rgb],# PIL or RGB arrays13 task="Pick up the large blue cube and put it in the box.",14 state=joint_positions,# raw 6-dim, degrees15 norm_tag="so100_so101_molmoact2",16 inference_action_mode="continuous",17 enable_depth_reasoning=False,18 num_steps=10,19)20actions = out.actions # [1, 30, 6], robot scale
use_grid_token_sampler is baked into config.json and must stay on. The token sampler is
inlined into this checkpoint's own modeling_molmoact2.py and processing_molmoact2.py, so
trust_remote_code=True is enough and lerobot does not need to be installed. A checkpoint that
carries sampler weights next to base-model remote code instead loads without any error and is
silently wrong: the 8 sampler tensors are dropped as UNEXPECTED and a 32-token-trained model
runs at 392 tokens.
Licence
The base model allenai/MolmoAct2-SO100_101
carries no declared licence, on its model card or as a hub tag, so no licence is asserted here
either. The training data is Apache-2.0. Confirm the upstream terms with AllenAI before
redistributing this checkpoint or using it commercially.