Instructions to use tokimoa/groot-n1.7-mlx with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use tokimoa/groot-n1.7-mlx with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir groot-n1.7-mlx tokimoa/groot-n1.7-mlx
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
| """GR00T N1.7 (nvidia/GR00T-N1.7-3B) inference runtime for Apple Silicon / MLX. | |
| NON-COMMERCIAL USE ONLY: the base model is released under the NVIDIA license | |
| included in this repository (Section 3.3: research or evaluation purposes only). | |
| Faithful port of the LeRobot reference (which is parity-tested against NVIDIA's | |
| original gr00t package): | |
| - backbone: Cosmos-Reason2-2B (Qwen3-VL) truncated to 16 layers, pre-final-norm | |
| features, fp32 execution (matching the reference runtime's fp32 upcast) | |
| - action head: 32-layer AlternateVL-DiT (AdaLN conditioning, alternating | |
| image/text cross-attention) + per-embodiment encoders, 4-step flow matching | |
| - verified end-to-end vs PyTorch reference: cosine 1.000000 / max diff 0.00027 | |
| Preprocessing (Qwen3-VL chat template, image packing, state normalization) is | |
| produced with the LeRobot pipeline — see preprocess_lerobot.py. | |
| Usage: | |
| from groot_mlx import GrootMLX | |
| m = GrootMLX.from_pretrained(".") | |
| chunk = m.sample_actions_from_processed("processed.pt") # -> (B, 40, 132) | |
| """ | |
| import json | |
| import math | |
| from pathlib import Path | |
| import mlx.core as mx | |
| import mlx.nn as nn | |
| import numpy as np | |
| from mlx_vlm.models.qwen3_vl import Model, ModelConfig | |
| from mlx_vlm.models.qwen3_vl.language import create_attention_mask | |
| N_BB_LAYERS = 16 | |
| # ---------------- バックボーン(Qwen3-VL 16層・pre-norm出力・fp32) ---------------- | |
| def build_backbone(ckpt_dir): | |
| ckpt_dir = Path(ckpt_dir) | |
| cfg_dict = json.load(open(ckpt_dir / "cosmos_config.json")) | |
| cfg_dict["text_config"]["num_hidden_layers"] = N_BB_LAYERS | |
| cfg = ModelConfig.from_dict(cfg_dict) | |
| cfg.text_config = type(cfg.text_config).from_dict(cfg_dict["text_config"]) | |
| cfg.vision_config = type(cfg.vision_config).from_dict(cfg_dict["vision_config"]) | |
| model = Model(cfg) | |
| weights = {} | |
| for f in ckpt_dir.glob("model-*.safetensors"): | |
| weights.update(mx.load(str(f))) | |
| pref = "backbone.model." | |
| bb = {k[len(pref):]: v for k, v in weights.items() if k.startswith(pref)} | |
| bb = {k: v for k, v in bb.items() if ".layers." not in k or | |
| int(k.split(".layers.")[1].split(".")[0]) < N_BB_LAYERS or "visual" in k} | |
| # torch参照はbf16チェックポイントをfp32昇格して実行する。ここを揃えないと一致しない | |
| bb = {k: (v.astype(mx.float32) if v.dtype == mx.bfloat16 else v) for k, v in bb.items()} | |
| bb = model.sanitize(bb) | |
| if hasattr(model.vision_tower, "sanitize"): | |
| bb = model.vision_tower.sanitize(bb) | |
| model.load_weights(list(bb.items()), strict=False) | |
| mx.eval(model.parameters()) | |
| return model | |
| def backbone_features(model, input_ids, pixel_values, image_grid_thw): | |
| feats = model.get_input_embeddings(input_ids, pixel_values, image_grid_thw=image_grid_thw) | |
| lm = model.language_model.model | |
| h = feats.inputs_embeds | |
| mask = create_attention_mask(h, [None] * len(lm.layers)) | |
| position_ids = feats.position_ids | |
| position_embeddings = None | |
| if position_ids is not None and not lm.layers[0].self_attn.rotary_emb.fused_apply: | |
| position_embeddings = lm.layers[0].self_attn.rotary_emb(h, position_ids) | |
| dse = feats.deepstack_visual_embeds | |
| vpm = feats.visual_pos_masks | |
| for layer_idx, layer in enumerate(lm.layers): | |
| h = layer(h, mask, None, position_ids, position_embeddings) | |
| if dse is not None and layer_idx in range(len(dse)): | |
| h = lm._deepstack_process(h, vpm, dse[layer_idx]) | |
| return h # GR00Tはpre-final-norm出力を使う | |
| # ---------------- アクションヘッド(DiT + per-embodimentエンコーダ) ---------------- | |
| def layer_norm(x, w=None, b=None, eps=1e-5): | |
| x32 = x.astype(mx.float32) | |
| mu = mx.mean(x32, axis=-1, keepdims=True) | |
| var = mx.var(x32, axis=-1, keepdims=True) | |
| out = (x32 - mu) * mx.rsqrt(var + eps) | |
| if w is not None: | |
| out = out * w.astype(mx.float32) + b.astype(mx.float32) | |
| return out.astype(x.dtype) | |
| def timesteps_embed(t_vals, num_channels=256, max_period=10000.0, shift=1.0): | |
| half = num_channels // 2 | |
| exponent = -math.log(max_period) * np.arange(half, dtype=np.float64) / (half - shift) | |
| emb = np.exp(exponent)[None, :] * np.asarray(t_vals, dtype=np.float64)[:, None] | |
| return mx.array(np.concatenate([np.cos(emb), np.sin(emb)], axis=1).astype(np.float32)) | |
| def sinusoidal_time_action(timesteps_2d, dim): | |
| half = dim // 2 | |
| exponent = -np.arange(half, dtype=np.float32) * (math.log(10000.0) / half) | |
| freqs = np.asarray(timesteps_2d, dtype=np.float32)[..., None] * np.exp(exponent)[None, None, :] | |
| return mx.array(np.concatenate([np.sin(freqs), np.cos(freqs)], axis=-1)) | |
| class GrootHead: | |
| def __init__(self, weights, cfg): | |
| self.w = {k[len("action_head."):]: v for k, v in weights.items() if k.startswith("action_head.")} | |
| d = cfg["diffusion_model_cfg"] | |
| self.N_DIT = d["num_layers"] | |
| self.N_HEADS = d["num_attention_heads"] | |
| self.HEAD_DIM = d["attention_head_dim"] | |
| self.ATTEND_TEXT_N = cfg.get("attend_text_every_n_blocks") or 2 | |
| self.VL_CFG = cfg["vl_self_attention_cfg"] | |
| self.HORIZON = cfg["action_horizon"] | |
| self.NUM_STEPS = cfg["num_inference_timesteps"] | |
| self.BUCKETS = cfg["num_timestep_buckets"] | |
| def cs_linear(self, prefix, x, cat_id): | |
| return x @ self.w[prefix + ".W"][cat_id] + self.w[prefix + ".b"][cat_id][:, None, :] | |
| def cs_mlp(self, prefix, x, cat_id): | |
| h = mx.maximum(self.cs_linear(prefix + ".layer1", x, cat_id), 0) | |
| return self.cs_linear(prefix + ".layer2", h, cat_id) | |
| def attention(self, prefix, q_in, kv_in, n_heads, head_dim, mask=None): | |
| B, Lq = q_in.shape[:2] | |
| Lk = kv_in.shape[1] | |
| q = q_in @ self.w[prefix + ".to_q.weight"].T + self.w[prefix + ".to_q.bias"] | |
| k = kv_in @ self.w[prefix + ".to_k.weight"].T + self.w[prefix + ".to_k.bias"] | |
| v = kv_in @ self.w[prefix + ".to_v.weight"].T + self.w[prefix + ".to_v.bias"] | |
| q = q.reshape(B, Lq, n_heads, head_dim).transpose(0, 2, 1, 3) | |
| k = k.reshape(B, Lk, n_heads, head_dim).transpose(0, 2, 1, 3) | |
| v = v.reshape(B, Lk, n_heads, head_dim).transpose(0, 2, 1, 3) | |
| att = (q @ k.transpose(0, 1, 3, 2)) * (head_dim ** -0.5) | |
| if mask is not None: | |
| att = mx.where(mask[:, None, None, :], att, mx.finfo(mx.float32).min) | |
| probs = mx.softmax(att.astype(mx.float32), axis=-1).astype(v.dtype) | |
| out = (probs @ v).transpose(0, 2, 1, 3).reshape(B, Lq, n_heads * head_dim) | |
| return out @ self.w[prefix + ".to_out.0.weight"].T + self.w[prefix + ".to_out.0.bias"] | |
| def ff(self, prefix, x): | |
| h = x @ self.w[prefix + ".net.0.proj.weight"].T + self.w[prefix + ".net.0.proj.bias"] | |
| h = nn.gelu_approx(h) | |
| return h @ self.w[prefix + ".net.2.weight"].T + self.w[prefix + ".net.2.bias"] | |
| def vl_self_attention(self, feats): | |
| n, hd = self.VL_CFG["num_attention_heads"], self.VL_CFG["attention_head_dim"] | |
| h = feats | |
| for i in range(self.VL_CFG["num_layers"]): | |
| p = f"vl_self_attention.transformer_blocks.{i}" | |
| hn = layer_norm(h, self.w[p + ".norm1.weight"], self.w[p + ".norm1.bias"]) | |
| h = self.attention(p + ".attn1", hn, hn, n, hd) + h | |
| hn = layer_norm(h, self.w[p + ".norm3.weight"], self.w[p + ".norm3.bias"]) | |
| h = self.ff(p + ".ff", hn) + h | |
| return h | |
| def ada_norm(self, prefix, x, temb): | |
| t = nn.silu(temb) @ self.w[prefix + ".linear.weight"].T + self.w[prefix + ".linear.bias"] | |
| scale, shift = mx.split(t, 2, axis=1) | |
| return layer_norm(x) * (1 + scale[:, None]) + shift[:, None] | |
| def dit(self, sa_embs, vl_embs, temb, image_mask, bb_att_mask): | |
| image_att = image_mask & bb_att_mask | |
| text_att = (~image_mask) & bb_att_mask | |
| h = sa_embs | |
| for i in range(self.N_DIT): | |
| p = f"model.transformer_blocks.{i}" | |
| hn = self.ada_norm(p + ".norm1", h, temb) | |
| if i % 2 == 1: | |
| h = self.attention(p + ".attn1", hn, hn, self.N_HEADS, self.HEAD_DIM) + h | |
| else: | |
| m = text_att if i % (2 * self.ATTEND_TEXT_N) == 0 else image_att | |
| h = self.attention(p + ".attn1", hn, vl_embs, self.N_HEADS, self.HEAD_DIM, mask=m) + h | |
| h = self.ff(p + ".ff", layer_norm(h)) + h | |
| t = nn.silu(temb) @ self.w["model.proj_out_1.weight"].T + self.w["model.proj_out_1.bias"] | |
| shift, scale = mx.split(t, 2, axis=1) | |
| h = layer_norm(h, eps=1e-6) * (1 + scale[:, None]) + shift[:, None] | |
| return h @ self.w["model.proj_out_2.weight"].T + self.w["model.proj_out_2.bias"] | |
| def timestep_encoder(self, t_disc): | |
| e = timesteps_embed(t_disc) | |
| e = e @ self.w["model.timestep_encoder.timestep_embedder.linear_1.weight"].T \ | |
| + self.w["model.timestep_encoder.timestep_embedder.linear_1.bias"] | |
| e = nn.silu(e) | |
| return e @ self.w["model.timestep_encoder.timestep_embedder.linear_2.weight"].T \ | |
| + self.w["model.timestep_encoder.timestep_embedder.linear_2.bias"] | |
| def action_encoder(self, actions, t_disc, cat_id): | |
| B, T, _ = actions.shape | |
| a = self.cs_linear("action_encoder.W1", actions, cat_id) | |
| tt = np.broadcast_to(np.asarray(t_disc, dtype=np.float32)[:, None], (B, T)) | |
| te = sinusoidal_time_action(tt, a.shape[-1]).astype(a.dtype) | |
| x = self.cs_linear("action_encoder.W2", mx.concatenate([a, te], axis=-1), cat_id) | |
| x = x * mx.sigmoid(x) | |
| return self.cs_linear("action_encoder.W3", x, cat_id) | |
| def get_action(self, bb_raw, image_mask, bb_att_mask, state, embodiment_id, noise): | |
| feats = layer_norm(bb_raw, self.w["vlln.weight"], self.w["vlln.bias"]) | |
| vl_embs = self.vl_self_attention(feats) | |
| state_feats = self.cs_mlp("state_encoder", state.reshape(state.shape[0], 1, -1), embodiment_id) | |
| x_t = noise | |
| dt = 1.0 / self.NUM_STEPS | |
| for step in range(self.NUM_STEPS): | |
| t_disc = [int(step / float(self.NUM_STEPS) * self.BUCKETS)] * x_t.shape[0] | |
| temb = self.timestep_encoder(t_disc) | |
| act = self.action_encoder(x_t, t_disc, embodiment_id) | |
| act = act + self.w["position_embedding.weight"][mx.arange(act.shape[1])][None] | |
| pred = self.dit(mx.concatenate([state_feats, act], axis=1), vl_embs, temb, | |
| image_mask, bb_att_mask) | |
| pred = self.cs_mlp("action_decoder", pred, embodiment_id) | |
| x_t = x_t + dt * pred[:, -self.HORIZON:] | |
| mx.eval(x_t) | |
| return x_t | |
| # ---------------- 統合ランタイム ---------------- | |
| class GrootMLX: | |
| def __init__(self, path="."): | |
| self.dir = Path(path) | |
| self.cfg = json.load(open(self.dir / "config.json")) | |
| self.backbone = build_backbone(self.dir) | |
| weights = {} | |
| for f in self.dir.glob("model-*.safetensors"): | |
| weights.update(mx.load(str(f))) | |
| self.head = GrootHead(weights, self.cfg) | |
| self.image_token_id = json.load(open(self.dir / "cosmos_config.json"))["image_token_id"] | |
| def from_pretrained(cls, path): | |
| return cls(path) | |
| def sample_actions(self, input_ids, pixel_values, image_grid_thw, attention_mask, | |
| state, embodiment_id, noise=None, seed=42): | |
| bb = backbone_features(self.backbone, input_ids, pixel_values, image_grid_thw).astype(mx.float32) | |
| image_mask = input_ids == self.image_token_id | |
| att = attention_mask.astype(mx.bool_) | |
| if noise is None: | |
| np.random.seed(seed) | |
| noise = mx.array(np.random.standard_normal( | |
| (state.shape[0], self.head.HORIZON, 132)).astype("float32")) | |
| return np.array(self.head.get_action(bb, image_mask, att, state, embodiment_id, noise)) | |
| def sample_actions_from_processed(self, processed_pt, noise=None, seed=42): | |
| """preprocess_lerobot.pyの出力(.pt)から行動チャンクを生成する。""" | |
| import torch | |
| ref = torch.load(processed_pt, weights_only=False) | |
| proc = ref.get("proc", ref) | |
| def get(k): | |
| return proc[k] if k in proc else ref[k] | |
| if noise is None and "noise" in ref: | |
| noise = mx.array(ref["noise"].cpu().float().numpy()) | |
| return self.sample_actions( | |
| mx.array(proc["input_ids"].cpu().numpy()), | |
| mx.array(proc["pixel_values"].cpu().float().numpy()), | |
| mx.array(proc["image_grid_thw"].cpu().numpy()), | |
| mx.array(proc["attention_mask"].cpu().numpy()), | |
| mx.array(get("state").cpu().float().numpy()), | |
| mx.array(get("embodiment_id").cpu().numpy()), | |
| noise=noise, seed=seed, | |
| ) | |
| if __name__ == "__main__": | |
| import argparse | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--processed", required=True, help="preprocess_lerobot.pyの出力.pt") | |
| ap.add_argument("--out", default="actions.npy") | |
| args = ap.parse_args() | |
| m = GrootMLX.from_pretrained(Path(__file__).parent) | |
| chunk = m.sample_actions_from_processed(args.processed) | |
| np.save(args.out, chunk) | |
| print(f"action chunk {chunk.shape} -> {args.out}") | |