AlexWortega commited on
Commit
51ce5b3
·
verified ·
1 Parent(s): 6a943fd

tinyvla_b200: B200 streaming training on nvidia/physical-ai (745 samples/s e2e, 7x ViT fast path, hub episode streaming, full instructions)

Browse files
tinyvla_b200/B200.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # B200: дообучение tv2_C_scaled на nvidia/physical-ai стримингом с хаба
2
+
3
+ Полная инструкция запуска. Все числа ниже — **измеренные** на реальной B200-ноде
4
+ RunPod 28.08.2026, не оценки.
5
+
6
+ ## Итог в одну строку
7
+
8
+ **745 семплов/с end-to-end** (стрим с хаба + обучение): **1M кадров = 0.37 ч = $2.5**;
9
+ бюджет C-scaled (60k шагов × batch 256 = 15.4M семплов) = **5.7 ч ≈ $39**;
10
+ эпоха всего пула (~86M семплов @ 10 Гц) = **32 ч ≈ $220**.
11
+
12
+ ## Что в пуле
13
+
14
+ `make_stream_specs.py` находит в коллекции **163 LeRobot-v2.x датасета, 214.7M
15
+ исходных кадров**: GR00T-X-Embodiment-Sim (127: GR1 arms/waist, single/bimanual
16
+ panda, sim_behavior R1-Pro), GR00T-Teleop-Sim, GR00T-Tuned-Tasks, Teleop-G1,
17
+ AppleToPlate. Схемы разные (state 32–256, action 12–44, 1–3 камеры, fps 20–30),
18
+ всё гармонизируется паддингом + `action_dim_mask`, как в TinyVLA-2.
19
+
20
+ НЕ входят (формат v3, нужен `build_shards.py`, оба уже были в претрейне C-scaled):
21
+ `BridgeData2_LeRobot_v3`, `LIBERO_LeRobot_v3`.
22
+
23
+ ## Почему стриминг, а не скачивание
24
+
25
+ Пул — 1.9 ТБ видео; шарды были бы ~1.3 ТБ. v2-формат хранит **один mp4 + один
26
+ parquet на эпизод** (2–10 МБ): воркер скачивает случайный эпизод в память,
27
+ последовательно декодирует (ноль seek'ов), выдаёт ~150–500 семплов, берёт
28
+ следующий. Диск не используется вообще. Замерено: стрим-смесь держит 1019
29
+ семплов/с на 16 воркерах — модель (745/с) не голодает. ВАЖНО: воркеров именно
30
+ 16, на 32–64 пропускная способность ПАДАЕТ (677/584) — конкуренция HTTP.
31
+
32
+ ## Обязательные оптимизации (без них B200 бессмысленна)
33
+
34
+ Всё уже в этом репозитории; числа — вклад каждой (B200, batch 256, bf16):
35
+
36
+ | что | было → стало | файл |
37
+ |---|---|---|
38
+ | **fla-core + causal-conv1d** (линейное внимание Qwen3.5; без них torch-фоллбэк) | 63 → 236 с/с (3.7×) | pip install |
39
+ | **дедуп ViT**: cam0 через башню 1 раз/шаг вместо 2 (fwd через `image_embeds` в LM) | +23–30% | `modeling_tinyvla.py`, `modules/semantic.py` |
40
+ | **плотный fixed-grid путь ViT**: HF гоняет B отдельных SDPA на слой (3072 вызова/проход) + пересобирает rotary питоном; у нас сетка всегда [1,16,16] → батчевый SDPA, константы кэшированы, torch.compile | encode_image 182 → 26 мс (**7×**), шаг 398 → **738 с/с** | `modules/semantic.py` (`_encode_image_fixed`) |
41
+ | SDPA в эксперте (имена параметров сохранены — чекпойнт грузится) | в составе | `modules/expert.py` (`_MHA`) |
42
+ | M-RoPE position_ids при `inputs_embeds` (фикс бага stale `rope_deltas`) | корректность | `modules/semantic.py` |
43
+ | fused AdamW, bf16 autocast, TF32, токен-кэш инструкций | в составе | `train_fast.py` |
44
+
45
+ Эквивалентность проверена: fixed-grid ViT **бит-в-бит** совпадает с HF в fp32;
46
+ SDPA-эксперт бит-в-бит с nn.MultiheadAttention на общих весах.
47
+
48
+ ## Запуск с нуля
49
+
50
+ ### 1. Нода
51
+
52
+ RunPod GraphQL (или консоль): `NVIDIA B200`, образ
53
+ `runpod/pytorch:2.8.0-py3.11-cuda12.8.1-cudnn-devel-ubuntu22.04` (torch с sm_100
54
+ из коробки), 1 GPU, ≥28 vCPU, ≥128 ГБ RAM, диск 50 ГБ (данные не хранятся!),
55
+ `ports: "22/tcp"`. $6.79/ч с диском.
56
+
57
+ Альтернатива по деньгам: H100 SXM $3.29/ч даёт ~0.5–0.55 ч/1M кадров с теми же
58
+ патчами — **на доллар почти то же самое**; B200 берёт скоростью на узле.
59
+
60
+ ### 2. Окружение (5 минут)
61
+
62
+ ```bash
63
+ pip install pyarrow pillow "transformers==5.5.4" accelerate av webdataset \
64
+ "huggingface_hub[hf_transfer]" pyyaml safetensors requests msgpack fla-core
65
+ pip install causal-conv1d --no-build-isolation # колесо ставится на sm_100
66
+ hf auth login --token hf_... # доступ к AlexWortega/tinyvla
67
+ ```
68
+
69
+ Проверка, что быстрые ядра живы (иначе всё в 3.7 раза медленнее):
70
+ ```bash
71
+ python -c "import fla, causal_conv1d; print('ok')"
72
+ ```
73
+
74
+ ### 3. Код и чекпойнт
75
+
76
+ ```bash
77
+ cd /workspace
78
+ hf download AlexWortega/tinyvla --local-dir tinyvla_repo \
79
+ --include "tinyvla_b200/*" "tinyvla2/pretrain_C_scaled/*"
80
+ mv tinyvla_repo/tinyvla_b200 tinyvla_h100
81
+ mkdir -p ckpt && ln -s /workspace/tinyvla_repo/tinyvla2/pretrain_C_scaled ckpt/tv2_C_scaled_final
82
+ export PYTHONPATH=/workspace/tinyvla_h100/src
83
+ ```
84
+
85
+ `lerobot` НЕ нужен для бенчей (`scripts/bench_model.py` собирает модель из
86
+ модулей); для полного `train_fast.py` нужен `lerobot==0.6.1` (тянет
87
+ `TinyVLAConfig`/`PreTrainedPolicy`).
88
+
89
+ ### 4. Спеки стрима (уже сгенерированы, можно перегенерить)
90
+
91
+ ```bash
92
+ python scripts/make_stream_specs.py --out configs/physical_ai_stream.yaml
93
+ # 163 датасета, веса = sqrt(frames), embodiment_id по семействам роботов (10-15)
94
+ ```
95
+
96
+ ### 5. Смоук → полный запуск
97
+
98
+ ```bash
99
+ # 200 шагов: заменяет оценку измерением за ~3 минуты
100
+ python scripts/train_fast.py --config configs/physical_ai_ft.yaml --profile-steps 200
101
+
102
+ # полный прогон (60k шагов, ~5.7 ч)
103
+ PYTHONUNBUFFERED=1 nohup python scripts/train_fast.py \
104
+ --config configs/physical_ai_ft.yaml > train.log 2>&1 &
105
+ ```
106
+
107
+ При старте `load_compatible` напечатает, что перенесено из C-scaled, а что
108
+ переинициализировано из-за смены размерностей — **проверь этот список**:
109
+ ожидаемо перенесены ViT + LM + блоки эксперта, переинициализированы
110
+ `state_proj` (16→256), `expert.action_in`/`action_out` (8→64),
111
+ `embodiment_emb` частично.
112
+
113
+ ## Что честно сказать про это дообучение
114
+
115
+ - `conditioning: id`, `action_space: native` — у сим-роботов пула нет URDF/EE в
116
+ state, канонизация C недоступна. Переносится **ствол** C-scaled, не голова.
117
+ - Первый вызов ViT-компиляции на новый batch-size — рекомпиляция ~20–40 с; в
118
+ stderr сыплются нефатальные dynamo-warning'и на HF-коде — это шум, скорость
119
+ реальная.
120
+ - Стрим бесконечный, эпоха не определена; шаги считает train-loop.
121
+ `shuffle_buffer` 4096/воркер разбивает внутриэпизодную корреляцию.
122
+ - fps-конвенции пула проверены (30 и 20 — честные); децимация до 10 Гц через
123
+ stride, action-chunk 50 = 5 с, как в претрейне.
124
+ - Хочешь r1_pro (state 256d) исключить — убери его блоки из
125
+ `physical_ai_stream.yaml` и опусти `max_state_dim` до 64.
126
+
127
+ ## Замеры (B200, полная таблица)
128
+
129
+ | конфигурация | семплов/с | ч на 1M кадров |
130
+ |---|---|---|
131
+ | модель, HF ViT, без fla | 63 | 4.4 |
132
+ | + fla/causal-conv1d | 236–305 | 0.9–1.2 |
133
+ | + дедуп cam0 | 398 | 0.70 |
134
+ | + плотный fixed-grid ViT + compile | **738** | **0.38** |
135
+ | **end-to-end со стримом с хаба** | **745** | **0.37** |
136
+ | стрим-загрузчик отдельно (16 воркеров) | 1019 | — |
137
+ | webdataset из локальных таров (для сравнения) | 1600–2400 | — |
138
+
139
+ Профиль шага после оптимизаций (batch 256): ViT 2×26 мс, LM fwd+bwd ~остальное,
140
+ эксперт 7 мс. Следующий резерв — LM-путь (fwd 170 мс до оптимизаций): плотный
141
+ фикс применён только к `encode_image`; внутренний вызов башни в невыгодном
142
+ не-дедуп пути остался HF-шным (дедуп это обходит).
143
+
144
+ ## Файлы
145
+
146
+ ```
147
+ scripts/train_fast.py цикл: shard- или hub-источник, fused AdamW, bf16, compile
148
+ scripts/make_stream_specs.py генератор спеков стрима по коллекции
149
+ scripts/bench_model.py бенч модели без lerobot
150
+ scripts/build_shards.py LeRobot v3 -> parquet-шарды (Bridge/LIBERO/trex)
151
+ scripts/build_shards_v2.py LeRobot v2 -> parquet-шарды (если нужен офлайн)
152
+ scripts/pack_wds.py шарды -> self-contained webdataset тары
153
+ src/tinyvla/data/streaming_hub.py HubEpisodeStream: стрим эпизодов с хаба
154
+ src/tinyvla/data/shards.py ShardSource (локальные parquet-шарды)
155
+ src/tinyvla/data/wds_mix.py wds-смесь (локальные тары)
156
+ src/tinyvla/{modeling_tinyvla,modules/*}.py модель со всеми патчами скорости
157
+ configs/physical_ai_stream.yaml 163 датасета пула
158
+ configs/physical_ai_ft.yaml конфиг дообучения C-scaled
159
+ patches/*.diff те же патчи диффами к tinyvla2/src
160
+ ```
tinyvla_b200/README.md ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Дообучение C-scaled на trex_dataset, H100: оценка и ускорение
2
+
3
+ Оценка бюджета `tv2_C_scaled` (60k шагов) на [`zekaiwang/trex_dataset`](https://huggingface.co/datasets/zekaiwang/trex_dataset)
4
+ одной H100, плюс код, без которого эта оценка недостижима.
5
+
6
+ **Короткий ответ: 2.9–4.4 ч чистого обучения (~$8–12 на H100 SXM) плюс ~1 ч одноразовой
7
+ подготовки данных. Если пайплайн данных не менять — 8–40 ч при простаивающем GPU.**
8
+
9
+ ---
10
+
11
+ ## 1. Что за датасет (проверено по метаданным, не по карточке)
12
+
13
+ | | |
14
+ |---|---|
15
+ | эпизодов / кадров | 5 464 / **5 473 459** (сходится с `info.json`) |
16
+ | частота | **30 Гц, и это правда** — `length/(to_ts−from_ts)` = 30.0000 на всех эпизодах |
17
+ | длина эпизода | медиана 887 кадров (29.6 с), максимум 6744 (225 с) |
18
+ | задач | 5370 уникальных инструкций |
19
+ | `observation.state` | **58** float: 2 × (7 DoF рука + 22 DoF кисть Sharpa) |
20
+ | `action` | **58** float, целевые углы суставов |
21
+ | видеопотоков | **23**: 3 RGB 360×640 + 20 тактильных |
22
+ | размер | **1.53 ТБ**, из них 383 ГБ — 3 RGB-камеры |
23
+
24
+ Урок LIBERO (объявленный fps ≠ реальный) здесь **не повторяется** — проверил первым
25
+ делом, `build_shards.py` проверяет это же и падает при расхождении.
26
+
27
+ Эпизоды внутри каждого mp4 идут подряд, по возрастанию времени, без разрывов, и число
28
+ кадров точно совпадает с таймштампами. Поэтому последовательный проход по видео работает.
29
+
30
+ ## 2. Три блокера, которые надо решить до запуска
31
+
32
+ **2.1. 58 измерений против `max_action_dim: 8`.** `CanonicalSource._pad`:
33
+
34
+ ```python
35
+ def _pad(x, dim):
36
+ if x.shape[-1] >= dim:
37
+ return x[..., :dim] # <- МОЛЧА обрезает 58 -> 8
38
+ ```
39
+
40
+ С конфигом C-scaled вы обучитесь на левой руке плюс одном суставе кисти и не узнаете об
41
+ этом: лосс будет падать, метрики будут выглядеть нормально. Поднято до 64 в
42
+ `configs/trex_ft.yaml`; `ShardSource._pad` вместо обрезки бросает исключение.
43
+
44
+ Побочный эффект: `state_proj`, `expert.action_in`, `expert.action_out` меняют форму.
45
+ `load_state_dict(strict=False)` при смене формы **не пропускает тензор, а падает**, поэтому
46
+ `train_fast.py::load_compatible` фильтрует по форме и печатает каждый переинициализированный
47
+ тензор.
48
+
49
+ **2.2. Канонические действия недоступны.** `action_space: canonical` — дельты EE в базовой
50
+ системе координат; их строит `build_ee_sidecar.py` через FK по URDF (для SO-101 — placo) или
51
+ читает готовую EE-позу из состояния. Для Vega-1 + Sharpa в репозитории нет URDF, а
52
+ состояние — чистые углы суставов. Значит `native`. C-scaled обучался в каноническом
53
+ пространстве, поэтому переносится ствол (ViT + LM + трансформер эксперта), но не семантика
54
+ выхода. Это не «дообучение C-scaled», это «новая голова действий на его стволе» — стоит
55
+ называть вещи своими именами при сравнении с 0.81/0.88.
56
+
57
+ **2.3. Архитектура выбрасывает то, ради чего датасет собран.** TinyVLA берёт cam0 + cam1;
58
+ регексы `CanonicalSource` дают `head_left` + `left_wrist`. `right_wrist` и все 20 тактильных
59
+ потоков (это **тактильный бимануальный** датасет) не используются никак. `tactile_force`
60
+ (60 чисел) тоже некуда положить — вход состояния уже занят 58 углами. Если тактильность
61
+ нужна, это отдельный входной путь, а не вопрос конфига.
62
+
63
+ ## 3. Почему сейчас медленно
64
+
65
+ Обучение берёт **один случайный кадр на семпл**. В LeRobot v3 это h264-seek внутрь mp4 на
66
+ ~195 МБ, где лежит ~10 склеенных эпизодов — на к��ждую камеру, на каждый семпл. Это ровно то,
67
+ что записано в `CLAUDE.md`: «video decode is the training bottleneck». Никакая H100 это не
68
+ чинит: GPU просто ждёт.
69
+
70
+ | стоимость случайного кадра | семплов/с (12 воркеров, 2 камеры) | 60k шагов |
71
+ |---|---|---|
72
+ | 20 мс | 300 | 8.0 ч |
73
+ | 50 мс | 120 | **20.0 ч** |
74
+ | 100 мс | 60 | 40.0 ч |
75
+
76
+ ## 4. Что сделано
77
+
78
+ ### 4.1. Шарды вместо случайного seek (главное)
79
+
80
+ `scripts/build_shards.py` — одноразовый проход: декодирует каждое видео **последовательно**
81
+ (дешёвое направление), ресайзит до 256×256 ровно так же, как `CanonicalSource._img`
82
+ (без сохранения пропорций — совпадение с обучающей конвенцией важнее геометрии), кодирует в
83
+ JPEG и пишет в parquet-шарды. Умеет `--part i --num-parts N` для распараллеливания по
84
+ машинам и удаляет mp4 после обработки, так что диск не растёт.
85
+
86
+ `src/tinyvla/data/shards.py` — `ShardSource`: `IterableDataset`, последовательное чтение
87
+ row-групп + shuffle-буфер, раздача шардов по воркерам и рангам. Выдаёт **ровно ту же схему**,
88
+ что `CanonicalSource`, поэтому дальше по коду ничего не меняется. State/action целиком
89
+ живут в RAM (58 float × 2 × 1.8M кадров < 1 ГБ), так что случайных обращений в горячем пути
90
+ не остаётся вовсе.
91
+
92
+ Стоимость: 47 ГБ шардов при 10 Гц, ~1 ч на подготовку (скачать 245 ГБ 20 мин при 200 МБ/с +
93
+ последовательный декод 10.9M кадров ~16 мин на 16 ядрах).
94
+
95
+ Дальше пайплайну нужно 1100–1650 JPEG-декодов 256×256 в секунду — это **меньше одного ядра**.
96
+ Данные перестают быть узким местом.
97
+
98
+ ### 4.2. Один проход vision tower вместо двух
99
+
100
+ `_encode_spatial` гоняет ViT по cam0, а затем `_semantic_latent` вызывает VLM, который
101
+ внутри гоняет ViT **по тому же cam0 ещё раз**. Это 45 из 180 GFLOPs forward — четверть,
102
+ выбрасываемая каждый шаг. Патч считает фичи один раз и передаёт их в LM через
103
+ `image_embeds` (тот же `masked_scatter`, который уже был написан для `morph_to_slow`).
104
+ При включённой staleness-аугментации кадр латента другой, и дедупликация корректно
105
+ отключается. — `patches/modeling_tinyvla.py.diff`, `patches/modules_semantic.py.diff`
106
+
107
+ ### 4.3. SDPA в эксперте
108
+
109
+ `nn.MultiheadAttention` → `F.scaled_dot_product_attention` с сохранением имён параметров
110
+ (`in_proj_weight`/`in_proj_bias`/`out_proj.*`), поэтому чекпойнт грузится без изменений.
111
+ Проверено численно: на общем `state_dict` выход совпадает **бит в бит** (max|Δ| = 0.00e+00)
112
+ для self-attn, cross-attn и cross-attn с маской. — `patches/modules_expert.py.diff`
113
+
114
+ ### 4.4. Остальное в `scripts/train_fast.py`
115
+
116
+ Предварительная токенизация всех 5370 инструкций один раз вместо токенизации каждого батча;
117
+ fused AdamW; bf16 autocast + TF32; `grad_accum: 1` при batch 144 (80 ГБ вместо 48);
118
+ `torch.compile` на эксперте (12 блоков из мелких операций — там компиляция даёт больше
119
+ всего); лог с samples/s, оценкой MFU и ETA.
120
+
121
+ ## 5. Оценка
122
+
123
+ Разложение forward из `ablation.md` §6: ViT 45 GFLOPs на камеру, 12 слоёв LM 46, эксперт 44.
124
+ Backward ≈ 2× forward для того, что учится; замороженный ViT — только forward.
125
+
126
+ | конфигурация | GFLOPs/семпл (fwd+bwd) |
127
+ |---|---|
128
+ | ViT заморожен, дедуп cam0 | **360** |
129
+ | ViT заморожен, как сейчас | 405 |
130
+ | ViT разморожен, дедуп cam0 | 540 |
131
+
132
+ Бюджет: 60 000 шагов × eff batch 144 = **8.64M семплов** = 3.11 EFLOPs (при 360).
133
+ На trex при 10 Гц это 1.82M кадров → **4.7 эпохи**.
134
+
135
+ H100 SXM, пик bf16 989 TFLOPS:
136
+
137
+ | конфигурация | MFU 15% | 20% | 25% | 30% | 40% |
138
+ |---|---|---|---|---|---|
139
+ | **ViT заморожен, дедуп** | 5.8 ч | 4.4 ч | 3.5 ч | **2.9 ч** | 2.2 ч |
140
+ | ViT замо��ожен, как сейчас | 6.6 ч | 4.9 ч | 3.9 ч | 3.3 ч | 2.5 ч |
141
+ | ViT разморожен, дедуп | 8.7 ч | 6.6 ч | 5.2 ч | 4.4 ч | 3.3 ч |
142
+
143
+ **Рабочая вилка: 2.9–4.4 ч** (MFU 20–30% — реалистично для модели из ViT + короткого LM +
144
+ мелкого эксперта после `torch.compile` и SDPA).
145
+
146
+ Перекрёстная проверка: тот же расчёт для A6000, на котором это и обучали, даёт 31–42 ч
147
+ чистого счёта без дедупликации. Отношение пиков H100/A6000 = 6.4×, оценки согласуются.
148
+ Что исходный запуск шёл заметно дольше — ожидаемо, он был data-bound.
149
+
150
+ Цены RunPod (взяты из API вашим ключом, аккаунт `alexdragannikolich@gmail.com`, баланс $240.62):
151
+
152
+ | GPU | $/ч | обучение 2.9–4.4 ч | + подготовка |
153
+ |---|---|---|---|
154
+ | **H100 NVL** | 2.59 | $8–11 | +$3 |
155
+ | H100 SXM | 2.69 | $8–12 | +$3 |
156
+ | H200 SXM | 3.59 | $10–16 | +$4 |
157
+ | B200 | 5.98 | $17–26 | +$6 |
158
+
159
+ ## 6. Порядок запуска
160
+
161
+ ```bash
162
+ # 1. Шарды (можно на дешёвом CPU-поде; 4 части параллельно)
163
+ for p in 0 1 2 3; do
164
+ python scripts/build_shards.py --repo-id zekaiwang/trex_dataset \
165
+ --out /data/trex_shards --hz 10 --image-size 256 \
166
+ --part $p --num-parts 4 &
167
+ done; wait
168
+
169
+ # 2. Смоук: 200 шагов, померить samples/s вместо того чтобы верить оценке
170
+ python scripts/train_fast.py --config configs/trex_ft.yaml --profile-steps 200
171
+
172
+ # 3. Полный прогон
173
+ python scripts/train_fast.py --config configs/trex_ft.yaml
174
+ ```
175
+
176
+ Шаг 2 — важный: он превращает вилку 2.9–4.4 ч в измеренное число за пять минут.
177
+
178
+ ## 7. Чего я не делал
179
+
180
+ - **Ничего не запускал на GPU** — здесь нет ни CUDA, ни `lerobot`. Проверено: все файлы
181
+ компилируются; `ShardSource` прогнан end-to-end на синтетических шардах (схема, формы,
182
+ паддинг хвоста эпизода, отказ от усечения 58→8); замена attention сверена численно с
183
+ `nn.MultiheadAttention`. Патч дедупликации ViT проверен только чтением и компиляцией.
184
+ - **Не трогал MFU-предположение.** 20–30% — оценка, а не замер. Шаг 2 выше её заменяет.
185
+ - **Дескриптор морфологии для Vega-1 не сверен со спекой** — значения в
186
+ `patches/descriptors_dexmate.yaml` правдоподобны, но помечены как требующие проверки.
187
+ Модель читает дескриптор зеро-шот, так что ошибка стоит качества.
188
+ - **Не делал ничего с тактильностью** — см. 2.3, это отдельная работа.
tinyvla_b200/configs/physical_ai_ft.yaml ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Дообучение tv2_C_scaled на LeRobot-части коллекции nvidia/physical-ai,
2
+ # СТРИМИНГОМ с хаба (никаких локальных данных). B200 x1.
3
+ #
4
+ # Что переносится из C-scaled: ViT, 12 слоёв LM, трансформер эксперта.
5
+ # Что переинициализируется (train_fast печатает список): state_proj (16->256),
6
+ # expert.action_in / action_out (8->64) — размерности пула больше.
7
+ # conditioning=id: у 163 сим-датасетов нет ни URDF, ни осмысленных дескрипторов
8
+ # морфологии на семейство; id по семействам роботов (6 семейств, ids 10-15).
9
+ # action_space=native: канонизация требует FK/EE — для этих роботов не построена.
10
+
11
+ output_dir: /workspace/outputs/physical_ai_ft
12
+ wandb: null
13
+ seed: 42
14
+ resume_from: /workspace/ckpt/tv2_C_scaled_final
15
+
16
+ source:
17
+ type: hub
18
+ specs: /workspace/tinyvla_h100/configs/physical_ai_stream.yaml
19
+
20
+ policy:
21
+ chunk_size: 50 # 5 с @ 10 Гц
22
+ n_action_steps: 50
23
+ image_size: 256
24
+ max_state_dim: 256 # sim_behavior_r1_pro
25
+ max_action_dim: 64 # реальный максимум пула 44, запас до 64
26
+ num_embodiments: 16
27
+ conditioning: id
28
+ action_space: native
29
+ freeze_lm: false # главный рычаг (v1: 60%->86%)
30
+ freeze_vision_encoder: true
31
+
32
+ backbone_lr_mult: 0.1
33
+ batch_size: 256 # B200 183 ГБ; замерено 46.5 ГБ на 256
34
+ grad_accum: 1
35
+ num_workers: 16 # НЕ 32: конкуренция HTTP-потоков душит стрим (замерено)
36
+ prefetch_factor: 4
37
+ shuffle_buffer: 4096 # на воркер; интерливинг эпизодов
38
+ lr: 1.0e-4
39
+ warmup_steps: 1000
40
+ steps: 60000 # 60k x 256 = 15.4M семплов = ~0.18 эпохи пула
41
+ grad_clip: 10.0
42
+ log_freq: 100
43
+ save_freq: 5000
44
+ compile: true
45
+
46
+ gflops_per_sample: 360.0
47
+ peak_flops: 2250.0e12 # B200 bf16 dense
tinyvla_b200/configs/physical_ai_stream.yaml ADDED
@@ -0,0 +1,820 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Autogenerated by make_stream_specs.py — streaming mixture over the
2
+ # LeRobot-v2.x part of the nvidia/physical-ai collection.
3
+ # 163 datasets, 214,725,127 source frames.
4
+ # max_state_dim needed: 256; max_action_dim needed: 44.
5
+ datasets:
6
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
7
+ prefix: "sim_behavior_r1_pro.task-0026_assembling_gift_baskets"
8
+ weight: 2283.0 # sqrt(5,212,011 fr); fps=30.0 state=256 act=23 cams=3
9
+ embodiment_id: 14
10
+ hz: 10.0
11
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
12
+ prefix: "sim_behavior_r1_pro.task-0023_boxing_books_up_for_storage"
13
+ weight: 2201.2 # sqrt(4,845,500 fr); fps=30.0 state=256 act=23 cams=3
14
+ embodiment_id: 14
15
+ hz: 10.0
16
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
17
+ prefix: "gr1_arms_waist.WineToCabinet"
18
+ weight: 2181.7 # sqrt(4,759,865 fr); fps=20.0 state=44 act=44 cams=1
19
+ embodiment_id: 10
20
+ hz: 10.0
21
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
22
+ prefix: "sim_behavior_r1_pro.task-0048_canning_food"
23
+ weight: 2143.6 # sqrt(4,595,098 fr); fps=30.0 state=256 act=23 cams=3
24
+ embodiment_id: 14
25
+ hz: 10.0
26
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
27
+ prefix: "gr1_arms_waist.PotatoToMicrowave"
28
+ weight: 2106.4 # sqrt(4,436,785 fr); fps=20.0 state=44 act=44 cams=1
29
+ embodiment_id: 10
30
+ hz: 10.0
31
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
32
+ prefix: "sim_behavior_r1_pro.task-0029_clean_up_your_desk"
33
+ weight: 2070.2 # sqrt(4,285,704 fr); fps=30.0 state=256 act=23 cams=3
34
+ embodiment_id: 14
35
+ hz: 10.0
36
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
37
+ prefix: "sim_behavior_r1_pro.task-0024_storing_food"
38
+ weight: 1993.4 # sqrt(3,973,824 fr); fps=30.0 state=256 act=23 cams=3
39
+ embodiment_id: 14
40
+ hz: 10.0
41
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
42
+ prefix: "sim_behavior_r1_pro.task-0013_loading_the_car"
43
+ weight: 1960.9 # sqrt(3,845,274 fr); fps=30.0 state=256 act=23 cams=3
44
+ embodiment_id: 14
45
+ hz: 10.0
46
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
47
+ prefix: "sim_behavior_r1_pro.task-0049_make_pizza"
48
+ weight: 1958.9 # sqrt(3,837,385 fr); fps=30.0 state=256 act=23 cams=3
49
+ embodiment_id: 14
50
+ hz: 10.0
51
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
52
+ prefix: "sim_behavior_r1_pro.task-0021_collecting_childrens_toys"
53
+ weight: 1958.9 # sqrt(3,837,265 fr); fps=30.0 state=256 act=23 cams=3
54
+ embodiment_id: 14
55
+ hz: 10.0
56
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
57
+ prefix: "gr1_arms_waist.PlaceMilkToMicrowave"
58
+ weight: 1957.9 # sqrt(3,833,290 fr); fps=20.0 state=44 act=44 cams=1
59
+ embodiment_id: 10
60
+ hz: 10.0
61
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
62
+ prefix: "sim_behavior_r1_pro.task-0007_picking_up_toys"
63
+ weight: 1943.7 # sqrt(3,778,110 fr); fps=30.0 state=256 act=23 cams=3
64
+ embodiment_id: 14
65
+ hz: 10.0
66
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
67
+ prefix: "gr1_arms_waist.PlaceBottleToCabinet"
68
+ weight: 1878.1 # sqrt(3,527,122 fr); fps=20.0 state=44 act=44 cams=1
69
+ embodiment_id: 10
70
+ hz: 10.0
71
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
72
+ prefix: "gr1_arms_waist.CanToDrawer"
73
+ weight: 1791.8 # sqrt(3,210,426 fr); fps=20.0 state=44 act=44 cams=1
74
+ embodiment_id: 10
75
+ hz: 10.0
76
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
77
+ prefix: "sim_behavior_r1_pro.task-0027_sorting_household_items"
78
+ weight: 1778.1 # sqrt(3,161,551 fr); fps=30.0 state=256 act=23 cams=3
79
+ embodiment_id: 14
80
+ hz: 10.0
81
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
82
+ prefix: "sim_behavior_r1_pro.task-0028_getting_organized_for_work"
83
+ weight: 1770.4 # sqrt(3,134,202 fr); fps=30.0 state=256 act=23 cams=3
84
+ embodiment_id: 14
85
+ hz: 10.0
86
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
87
+ prefix: "sim_behavior_r1_pro.task-0043_slicing_vegetables"
88
+ weight: 1723.1 # sqrt(2,968,988 fr); fps=30.0 state=256 act=23 cams=3
89
+ embodiment_id: 14
90
+ hz: 10.0
91
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
92
+ prefix: "sim_behavior_r1_pro.task-0016_moving_boxes_to_storage"
93
+ weight: 1708.6 # sqrt(2,919,245 fr); fps=30.0 state=256 act=23 cams=3
94
+ embodiment_id: 14
95
+ hz: 10.0
96
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
97
+ prefix: "sim_behavior_r1_pro.task-0014_carrying_in_groceries"
98
+ weight: 1689.7 # sqrt(2,854,961 fr); fps=30.0 state=256 act=23 cams=3
99
+ embodiment_id: 14
100
+ hz: 10.0
101
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
102
+ prefix: "sim_behavior_r1_pro.task-0041_cook_cabbage"
103
+ weight: 1680.4 # sqrt(2,823,597 fr); fps=30.0 state=256 act=23 cams=3
104
+ embodiment_id: 14
105
+ hz: 10.0
106
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
107
+ prefix: "sim_behavior_r1_pro.task-0002_putting_away_Halloween_decorations"
108
+ weight: 1663.3 # sqrt(2,766,429 fr); fps=30.0 state=256 act=23 cams=3
109
+ embodiment_id: 14
110
+ hz: 10.0
111
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
112
+ prefix: "sim_behavior_r1_pro.task-0009_putting_up_Christmas_decorations_inside"
113
+ weight: 1656.4 # sqrt(2,743,752 fr); fps=30.0 state=256 act=23 cams=3
114
+ embodiment_id: 14
115
+ hz: 10.0
116
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
117
+ prefix: "sim_behavior_r1_pro.task-0003_cleaning_up_plates_and_food"
118
+ weight: 1655.1 # sqrt(2,739,238 fr); fps=30.0 state=256 act=23 cams=3
119
+ embodiment_id: 14
120
+ hz: 10.0
121
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
122
+ prefix: "sim_behavior_r1_pro.task-0015_bringing_in_wood"
123
+ weight: 1645.3 # sqrt(2,707,131 fr); fps=30.0 state=256 act=23 cams=3
124
+ embodiment_id: 14
125
+ hz: 10.0
126
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
127
+ prefix: "gr1_arms_waist.CupToDrawer"
128
+ weight: 1632.4 # sqrt(2,664,870 fr); fps=20.0 state=44 act=44 cams=1
129
+ embodiment_id: 10
130
+ hz: 10.0
131
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
132
+ prefix: "sim_behavior_r1_pro.task-0025_clearing_food_from_table_into_fridge"
133
+ weight: 1616.7 # sqrt(2,613,632 fr); fps=30.0 state=256 act=23 cams=3
134
+ embodiment_id: 14
135
+ hz: 10.0
136
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
137
+ prefix: "sim_behavior_r1_pro.task-0047_freeze_pies"
138
+ weight: 1578.3 # sqrt(2,491,046 fr); fps=30.0 state=256 act=23 cams=3
139
+ embodiment_id: 14
140
+ hz: 10.0
141
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
142
+ prefix: "sim_behavior_r1_pro.task-0036_clean_a_patio"
143
+ weight: 1553.8 # sqrt(2,414,162 fr); fps=30.0 state=256 act=23 cams=3
144
+ embodiment_id: 14
145
+ hz: 10.0
146
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
147
+ prefix: "gr1_arms_waist.CuttingboardToBasket"
148
+ weight: 1553.0 # sqrt(2,411,674 fr); fps=20.0 state=44 act=44 cams=1
149
+ embodiment_id: 10
150
+ hz: 10.0
151
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
152
+ prefix: "gr1_arms_waist.PlacematToBasket"
153
+ weight: 1552.8 # sqrt(2,411,290 fr); fps=20.0 state=44 act=44 cams=1
154
+ embodiment_id: 10
155
+ hz: 10.0
156
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
157
+ prefix: "sim_behavior_r1_pro.task-0020_sorting_vegetables"
158
+ weight: 1543.0 # sqrt(2,380,740 fr); fps=30.0 state=256 act=23 cams=3
159
+ embodiment_id: 14
160
+ hz: 10.0
161
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
162
+ prefix: "sim_behavior_r1_pro.task-0004_can_meat"
163
+ weight: 1539.3 # sqrt(2,369,415 fr); fps=30.0 state=256 act=23 cams=3
164
+ embodiment_id: 14
165
+ hz: 10.0
166
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
167
+ prefix: "gr1_arms_waist.TrayToTieredShelf"
168
+ weight: 1524.6 # sqrt(2,324,367 fr); fps=20.0 state=44 act=44 cams=1
169
+ embodiment_id: 10
170
+ hz: 10.0
171
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
172
+ prefix: "gr1_arms_waist.PlacematToTieredShelf"
173
+ weight: 1522.5 # sqrt(2,318,114 fr); fps=20.0 state=44 act=44 cams=1
174
+ embodiment_id: 10
175
+ hz: 10.0
176
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
177
+ prefix: "sim_behavior_r1_pro.task-0033_wash_dog_toys"
178
+ weight: 1498.2 # sqrt(2,244,560 fr); fps=30.0 state=256 act=23 cams=3
179
+ embodiment_id: 14
180
+ hz: 10.0
181
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
182
+ prefix: "gr1_arms_waist.TrayToTieredBasket"
183
+ weight: 1495.2 # sqrt(2,235,593 fr); fps=20.0 state=44 act=44 cams=1
184
+ embodiment_id: 10
185
+ hz: 10.0
186
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
187
+ prefix: "gr1_arms_waist.CuttingboardToTieredBasket"
188
+ weight: 1488.5 # sqrt(2,215,695 fr); fps=20.0 state=44 act=44 cams=1
189
+ embodiment_id: 10
190
+ hz: 10.0
191
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
192
+ prefix: "sim_behavior_r1_pro.task-0018_tidying_bedroom"
193
+ weight: 1485.8 # sqrt(2,207,489 fr); fps=30.0 state=256 act=23 cams=3
194
+ embodiment_id: 14
195
+ hz: 10.0
196
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
197
+ prefix: "sim_behavior_r1_pro.task-0011_putting_dishes_away_after_cleaning"
198
+ weight: 1480.1 # sqrt(2,190,686 fr); fps=30.0 state=256 act=23 cams=3
199
+ embodiment_id: 14
200
+ hz: 10.0
201
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
202
+ prefix: "sim_behavior_r1_pro.task-0044_chopping_wood"
203
+ weight: 1466.4 # sqrt(2,150,397 fr); fps=30.0 state=256 act=23 cams=3
204
+ embodiment_id: 14
205
+ hz: 10.0
206
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
207
+ prefix: "sim_behavior_r1_pro.task-0019_outfit_a_basic_toolbox"
208
+ weight: 1458.6 # sqrt(2,127,594 fr); fps=30.0 state=256 act=23 cams=3
209
+ embodiment_id: 14
210
+ hz: 10.0
211
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
212
+ prefix: "gr1_arms_waist.CuttingboardToPan"
213
+ weight: 1454.0 # sqrt(2,114,010 fr); fps=20.0 state=44 act=44 cams=1
214
+ embodiment_id: 10
215
+ hz: 10.0
216
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
217
+ prefix: "gr1_arms_waist.PlateToPan"
218
+ weight: 1453.0 # sqrt(2,111,187 fr); fps=20.0 state=44 act=44 cams=1
219
+ embodiment_id: 10
220
+ hz: 10.0
221
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
222
+ prefix: "gr1_arms_waist.PlacematToBowl"
223
+ weight: 1443.4 # sqrt(2,083,466 fr); fps=20.0 state=44 act=44 cams=1
224
+ embodiment_id: 10
225
+ hz: 10.0
226
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
227
+ prefix: "gr1_arms_waist.PlateToBowl"
228
+ weight: 1442.7 # sqrt(2,081,304 fr); fps=20.0 state=44 act=44 cams=1
229
+ embodiment_id: 10
230
+ hz: 10.0
231
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
232
+ prefix: "sim_behavior_r1_pro.task-0005_setting_mousetraps"
233
+ weight: 1426.3 # sqrt(2,034,344 fr); fps=30.0 state=256 act=23 cams=3
234
+ embodiment_id: 14
235
+ hz: 10.0
236
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
237
+ prefix: "gr1_arms_waist.TrayToCardboardBox"
238
+ weight: 1424.1 # sqrt(2,027,980 fr); fps=20.0 state=44 act=44 cams=1
239
+ embodiment_id: 10
240
+ hz: 10.0
241
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
242
+ prefix: "gr1_arms_waist.PlateToCardboardBox"
243
+ weight: 1417.1 # sqrt(2,008,179 fr); fps=20.0 state=44 act=44 cams=1
244
+ embodiment_id: 10
245
+ hz: 10.0
246
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
247
+ prefix: "gr1_arms_waist.CuttingboardToCardboardBox"
248
+ weight: 1414.9 # sqrt(2,002,060 fr); fps=20.0 state=44 act=44 cams=1
249
+ embodiment_id: 10
250
+ hz: 10.0
251
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
252
+ prefix: "gr1_arms_waist.TrayToPlate"
253
+ weight: 1404.1 # sqrt(1,971,615 fr); fps=20.0 state=44 act=44 cams=1
254
+ embodiment_id: 10
255
+ hz: 10.0
256
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
257
+ prefix: "gr1_arms_waist.PlacematToPlate"
258
+ weight: 1401.1 # sqrt(1,963,149 fr); fps=20.0 state=44 act=44 cams=1
259
+ embodiment_id: 10
260
+ hz: 10.0
261
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
262
+ prefix: "gr1_arms_waist.PlateToPlate"
263
+ weight: 1401.1 # sqrt(1,963,078 fr); fps=20.0 state=44 act=44 cams=1
264
+ embodiment_id: 10
265
+ hz: 10.0
266
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
267
+ prefix: "gr1_arms_waist.TrayToPot"
268
+ weight: 1396.6 # sqrt(1,950,435 fr); fps=20.0 state=44 act=44 cams=1
269
+ embodiment_id: 10
270
+ hz: 10.0
271
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
272
+ prefix: "gr1_arms_waist.CuttingboardToPot"
273
+ weight: 1394.7 # sqrt(1,945,224 fr); fps=20.0 state=44 act=44 cams=1
274
+ embodiment_id: 10
275
+ hz: 10.0
276
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
277
+ prefix: "sim_behavior_r1_pro.task-0017_bringing_water"
278
+ weight: 1373.9 # sqrt(1,887,709 fr); fps=30.0 state=256 act=23 cams=3
279
+ embodiment_id: 14
280
+ hz: 10.0
281
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
282
+ prefix: "sim_behavior_r1_pro.task-0045_cook_hot_dogs"
283
+ weight: 1352.4 # sqrt(1,828,971 fr); fps=30.0 state=256 act=23 cams=3
284
+ embodiment_id: 14
285
+ hz: 10.0
286
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
287
+ prefix: "sim_behavior_r1_pro.task-0030_setting_the_fire"
288
+ weight: 1350.4 # sqrt(1,823,626 fr); fps=30.0 state=256 act=23 cams=3
289
+ embodiment_id: 14
290
+ hz: 10.0
291
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
292
+ prefix: "sim_behavior_r1_pro.task-0008_rearranging_kitchen_furniture"
293
+ weight: 1337.4 # sqrt(1,788,603 fr); fps=30.0 state=256 act=23 cams=3
294
+ embodiment_id: 14
295
+ hz: 10.0
296
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
297
+ prefix: "sim_behavior_r1_pro.task-0032_wash_a_baseball_cap"
298
+ weight: 1292.2 # sqrt(1,669,882 fr); fps=30.0 state=256 act=23 cams=3
299
+ embodiment_id: 14
300
+ hz: 10.0
301
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
302
+ prefix: "sim_behavior_r1_pro.task-0039_spraying_fruit_trees"
303
+ weight: 1292.0 # sqrt(1,669,174 fr); fps=30.0 state=256 act=23 cams=3
304
+ embodiment_id: 14
305
+ hz: 10.0
306
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
307
+ prefix: "sim_behavior_r1_pro.task-0012_preparing_lunch_box"
308
+ weight: 1284.2 # sqrt(1,649,060 fr); fps=30.0 state=256 act=23 cams=3
309
+ embodiment_id: 14
310
+ hz: 10.0
311
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
312
+ prefix: "sim_behavior_r1_pro.task-0031_clean_boxing_gloves"
313
+ weight: 1283.4 # sqrt(1,647,072 fr); fps=30.0 state=256 act=23 cams=3
314
+ embodiment_id: 14
315
+ hz: 10.0
316
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
317
+ prefix: "sim_behavior_r1_pro.task-0022_putting_shoes_on_rack"
318
+ weight: 1240.4 # sqrt(1,538,490 fr); fps=30.0 state=256 act=23 cams=3
319
+ embodiment_id: 14
320
+ hz: 10.0
321
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
322
+ prefix: "sim_behavior_r1_pro.task-0046_cook_bacon"
323
+ weight: 1239.3 # sqrt(1,535,909 fr); fps=30.0 state=256 act=23 cams=3
324
+ embodiment_id: 14
325
+ hz: 10.0
326
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
327
+ prefix: "sim_behavior_r1_pro.task-0006_hiding_Easter_eggs"
328
+ weight: 1234.5 # sqrt(1,523,945 fr); fps=30.0 state=256 act=23 cams=3
329
+ embodiment_id: 14
330
+ hz: 10.0
331
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
332
+ prefix: "single_panda_gripper.PnPCounterToSink"
333
+ weight: 1149.3 # sqrt(1,320,967 fr); fps=20.0 state=53 act=12 cams=3
334
+ embodiment_id: 13
335
+ hz: 10.0
336
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
337
+ prefix: "sim_behavior_r1_pro.task-0038_spraying_for_bugs"
338
+ weight: 1138.4 # sqrt(1,295,895 fr); fps=30.0 state=256 act=23 cams=3
339
+ embodiment_id: 14
340
+ hz: 10.0
341
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
342
+ prefix: "single_panda_gripper.PnPCounterToMicrowave"
343
+ weight: 1131.9 # sqrt(1,281,129 fr); fps=20.0 state=53 act=12 cams=3
344
+ embodiment_id: 13
345
+ hz: 10.0
346
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
347
+ prefix: "sim_behavior_r1_pro.task-0042_chop_an_onion"
348
+ weight: 1131.4 # sqrt(1,279,960 fr); fps=30.0 state=256 act=23 cams=3
349
+ embodiment_id: 14
350
+ hz: 10.0
351
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
352
+ prefix: "sim_behavior_r1_pro.task-0010_set_up_a_coffee_station_in_your_kitchen"
353
+ weight: 1119.5 # sqrt(1,253,243 fr); fps=30.0 state=256 act=23 cams=3
354
+ embodiment_id: 14
355
+ hz: 10.0
356
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
357
+ prefix: "single_panda_gripper.CloseDoubleDoor"
358
+ weight: 1099.8 # sqrt(1,209,559 fr); fps=20.0 state=53 act=12 cams=3
359
+ embodiment_id: 13
360
+ hz: 10.0
361
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
362
+ prefix: "single_panda_gripper.OpenDoubleDoor"
363
+ weight: 1038.1 # sqrt(1,077,708 fr); fps=20.0 state=53 act=12 cams=3
364
+ embodiment_id: 13
365
+ hz: 10.0
366
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
367
+ prefix: "sim_behavior_r1_pro.task-0037_clean_a_trumpet"
368
+ weight: 1028.8 # sqrt(1,058,409 fr); fps=30.0 state=256 act=23 cams=3
369
+ embodiment_id: 14
370
+ hz: 10.0
371
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
372
+ prefix: "sim_behavior_r1_pro.task-0001_picking_up_trash"
373
+ weight: 1026.4 # sqrt(1,053,550 fr); fps=30.0 state=256 act=23 cams=3
374
+ embodiment_id: 14
375
+ hz: 10.0
376
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
377
+ prefix: "single_panda_gripper.PnPCabToCounter"
378
+ weight: 1007.8 # sqrt(1,015,757 fr); fps=20.0 state=53 act=12 cams=3
379
+ embodiment_id: 13
380
+ hz: 10.0
381
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
382
+ prefix: "single_panda_gripper.CoffeeServeMug"
383
+ weight: 1005.3 # sqrt(1,010,648 fr); fps=20.0 state=53 act=12 cams=3
384
+ embodiment_id: 13
385
+ hz: 10.0
386
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
387
+ prefix: "single_panda_gripper.PnPCounterToStove"
388
+ weight: 978.4 # sqrt(957,229 fr); fps=20.0 state=53 act=12 cams=3
389
+ embodiment_id: 13
390
+ hz: 10.0
391
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
392
+ prefix: "single_panda_gripper.PnPMicrowaveToCounter"
393
+ weight: 971.8 # sqrt(944,476 fr); fps=20.0 state=53 act=12 cams=3
394
+ embodiment_id: 13
395
+ hz: 10.0
396
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
397
+ prefix: "single_panda_gripper.CoffeeSetupMug"
398
+ weight: 913.0 # sqrt(833,482 fr); fps=20.0 state=53 act=12 cams=3
399
+ embodiment_id: 13
400
+ hz: 10.0
401
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
402
+ prefix: "single_panda_gripper.PnPSinkToCounter"
403
+ weight: 906.9 # sqrt(822,477 fr); fps=20.0 state=53 act=12 cams=3
404
+ embodiment_id: 13
405
+ hz: 10.0
406
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
407
+ prefix: "single_panda_gripper.PnPStoveToCounter"
408
+ weight: 903.5 # sqrt(816,366 fr); fps=20.0 state=53 act=12 cams=3
409
+ embodiment_id: 13
410
+ hz: 10.0
411
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
412
+ prefix: "single_panda_gripper.OpenSingleDoor"
413
+ weight: 899.0 # sqrt(808,202 fr); fps=20.0 state=53 act=12 cams=3
414
+ embodiment_id: 13
415
+ hz: 10.0
416
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
417
+ prefix: "single_panda_gripper.CloseSingleDoor"
418
+ weight: 886.5 # sqrt(785,853 fr); fps=20.0 state=53 act=12 cams=3
419
+ embodiment_id: 13
420
+ hz: 10.0
421
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
422
+ prefix: "sim_behavior_r1_pro.task-0035_attach_a_camera_to_a_tripod"
423
+ weight: 884.5 # sqrt(782,315 fr); fps=30.0 state=256 act=23 cams=3
424
+ embodiment_id: 14
425
+ hz: 10.0
426
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
427
+ prefix: "single_panda_gripper.PnPCounterToCab"
428
+ weight: 881.9 # sqrt(777,674 fr); fps=20.0 state=53 act=12 cams=3
429
+ embodiment_id: 13
430
+ hz: 10.0
431
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
432
+ prefix: "single_panda_gripper.TurnOnSinkFaucet"
433
+ weight: 847.9 # sqrt(718,880 fr); fps=20.0 state=53 act=12 cams=3
434
+ embodiment_id: 13
435
+ hz: 10.0
436
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
437
+ prefix: "single_panda_gripper.TurnOffMicrowave"
438
+ weight: 838.8 # sqrt(703,625 fr); fps=20.0 state=53 act=12 cams=3
439
+ embodiment_id: 13
440
+ hz: 10.0
441
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
442
+ prefix: "single_panda_gripper.TurnOnMicrowave"
443
+ weight: 838.1 # sqrt(702,401 fr); fps=20.0 state=53 act=12 cams=3
444
+ embodiment_id: 13
445
+ hz: 10.0
446
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
447
+ prefix: "single_panda_gripper.OpenDrawer"
448
+ weight: 821.2 # sqrt(674,374 fr); fps=20.0 state=53 act=12 cams=3
449
+ embodiment_id: 13
450
+ hz: 10.0
451
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
452
+ prefix: "sim_behavior_r1_pro.task-0040_make_microwave_popcorn"
453
+ weight: 804.7 # sqrt(647,555 fr); fps=30.0 state=256 act=23 cams=3
454
+ embodiment_id: 14
455
+ hz: 10.0
456
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
457
+ prefix: "single_panda_gripper.TurnOnStove"
458
+ weight: 798.5 # sqrt(637,617 fr); fps=20.0 state=53 act=12 cams=3
459
+ embodiment_id: 13
460
+ hz: 10.0
461
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
462
+ prefix: "single_panda_gripper.CloseDrawer"
463
+ weight: 787.0 # sqrt(619,411 fr); fps=20.0 state=53 act=12 cams=3
464
+ embodiment_id: 13
465
+ hz: 10.0
466
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
467
+ prefix: "single_panda_gripper.TurnOffSinkFaucet"
468
+ weight: 773.1 # sqrt(597,645 fr); fps=20.0 state=53 act=12 cams=3
469
+ embodiment_id: 13
470
+ hz: 10.0
471
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
472
+ prefix: "single_panda_gripper.TurnOffStove"
473
+ weight: 738.1 # sqrt(544,815 fr); fps=20.0 state=53 act=12 cams=3
474
+ embodiment_id: 13
475
+ hz: 10.0
476
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
477
+ prefix: "bimanual_panda_hand.LiftTray"
478
+ weight: 707.2 # sqrt(500,131 fr); fps=20.0 state=40 act=24 cams=3
479
+ embodiment_id: 12
480
+ hz: 10.0
481
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
482
+ prefix: "sim_behavior_r1_pro.task-0034_hanging_pictures"
483
+ weight: 691.4 # sqrt(478,034 fr); fps=30.0 state=256 act=23 cams=3
484
+ embodiment_id: 14
485
+ hz: 10.0
486
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
487
+ prefix: "gr1_unified.PnPWineToCabinetClose_GR1ArmsAndWaistFourierHands_1000"
488
+ weight: 687.8 # sqrt(473,076 fr); fps=20.0 state=44 act=44 cams=1
489
+ embodiment_id: 10
490
+ hz: 10.0
491
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
492
+ prefix: "LeRobot/gr1_unified.PnPWineToCabinetClose"
493
+ weight: 664.4 # sqrt(441,387 fr); fps=20.0 state=44 act=44 cams=1
494
+ embodiment_id: 10
495
+ hz: 10.0
496
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
497
+ prefix: "gr1_unified.PnPPotatoToMicrowaveClose_GR1ArmsAndWaistFourierHands_1000"
498
+ weight: 662.9 # sqrt(439,435 fr); fps=20.0 state=44 act=44 cams=1
499
+ embodiment_id: 10
500
+ hz: 10.0
501
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
502
+ prefix: "bimanual_panda_gripper.Transport"
503
+ weight: 639.8 # sqrt(409,386 fr); fps=20.0 state=32 act=14 cams=3
504
+ embodiment_id: 11
505
+ hz: 10.0
506
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
507
+ prefix: "single_panda_gripper.TurnSinkSpout"
508
+ weight: 627.4 # sqrt(393,622 fr); fps=20.0 state=53 act=12 cams=3
509
+ embodiment_id: 13
510
+ hz: 10.0
511
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
512
+ prefix: "LeRobot/gr1_unified.PnPMilkToMicrowaveClose"
513
+ weight: 622.3 # sqrt(387,316 fr); fps=20.0 state=44 act=44 cams=1
514
+ embodiment_id: 10
515
+ hz: 10.0
516
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
517
+ prefix: "gr1_unified.PnPMilkToMicrowaveClose_GR1ArmsAndWaistFourierHands_1000"
518
+ weight: 616.4 # sqrt(379,992 fr); fps=20.0 state=44 act=44 cams=1
519
+ embodiment_id: 10
520
+ hz: 10.0
521
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
522
+ prefix: "LeRobot/gr1_unified.PnPPotatoToMicrowaveClose"
523
+ weight: 611.0 # sqrt(373,353 fr); fps=20.0 state=44 act=44 cams=1
524
+ embodiment_id: 10
525
+ hz: 10.0
526
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
527
+ prefix: "single_panda_gripper.CoffeePressButton"
528
+ weight: 610.3 # sqrt(372,446 fr); fps=20.0 state=53 act=12 cams=3
529
+ embodiment_id: 13
530
+ hz: 10.0
531
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
532
+ prefix: "gr1_unified.PnPBottleToCabinetClose_GR1ArmsAndWaistFourierHands_1000"
533
+ weight: 593.9 # sqrt(352,726 fr); fps=20.0 state=44 act=44 cams=1
534
+ embodiment_id: 10
535
+ hz: 10.0
536
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
537
+ prefix: "LeRobot/gr1_unified.PnPBottleToCabinetClose"
538
+ weight: 591.2 # sqrt(349,476 fr); fps=20.0 state=44 act=44 cams=1
539
+ embodiment_id: 10
540
+ hz: 10.0
541
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
542
+ prefix: "sim_behavior_r1_pro.task-0000_turning_on_radio"
543
+ weight: 570.1 # sqrt(324,964 fr); fps=30.0 state=256 act=23 cams=3
544
+ embodiment_id: 14
545
+ hz: 10.0
546
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
547
+ prefix: "gr1_full_upper_body.Coffee"
548
+ weight: 569.7 # sqrt(324,517 fr); fps=20.0 state=44 act=44 cams=1
549
+ embodiment_id: 10
550
+ hz: 10.0
551
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
552
+ prefix: "gr1_unified.PnPCanToDrawerClose_GR1ArmsAndWaistFourierHands_1000"
553
+ weight: 565.4 # sqrt(319,656 fr); fps=20.0 state=44 act=44 cams=1
554
+ embodiment_id: 10
555
+ hz: 10.0
556
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
557
+ prefix: "LeRobot/gr1_unified.PnPCanToDrawerClose"
558
+ weight: 564.4 # sqrt(318,546 fr); fps=20.0 state=44 act=44 cams=1
559
+ embodiment_id: 10
560
+ hz: 10.0
561
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
562
+ prefix: "gr1_arms_only.CanSort"
563
+ weight: 562.9 # sqrt(316,878 fr); fps=20.0 state=44 act=44 cams=1
564
+ embodiment_id: 10
565
+ hz: 10.0
566
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
567
+ prefix: "LeRobot/gr1_unified.PnPCupToDrawerClose"
568
+ weight: 559.8 # sqrt(313,404 fr); fps=20.0 state=44 act=44 cams=1
569
+ embodiment_id: 10
570
+ hz: 10.0
571
+ - repo_id: nvidia/PhysicalAI-GR00T-Tuned-Tasks
572
+ prefix: "Nut-Pouring-task"
573
+ weight: 555.3 # sqrt(308,383 fr); fps=20 state=26 act=26 cams=1
574
+ embodiment_id: 15
575
+ hz: 10.0
576
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
577
+ prefix: "bimanual_panda_hand.DrawerCleanup"
578
+ weight: 539.2 # sqrt(290,716 fr); fps=20.0 state=40 act=24 cams=3
579
+ embodiment_id: 12
580
+ hz: 10.0
581
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
582
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromTrayToTieredshelfSplitA"
583
+ weight: 523.8 # sqrt(274,326 fr); fps=20.0 state=44 act=44 cams=1
584
+ embodiment_id: 10
585
+ hz: 10.0
586
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
587
+ prefix: "gr1_full_upper_body.Pouring"
588
+ weight: 517.5 # sqrt(267,780 fr); fps=20.0 state=44 act=44 cams=1
589
+ embodiment_id: 10
590
+ hz: 10.0
591
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
592
+ prefix: "gr1_unified.PnPCupToDrawerClose_GR1ArmsAndWaistFourierHands_1000"
593
+ weight: 515.8 # sqrt(266,040 fr); fps=20.0 state=44 act=44 cams=1
594
+ embodiment_id: 10
595
+ hz: 10.0
596
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
597
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromPlacematToTieredshelfSplitA"
598
+ weight: 508.8 # sqrt(258,920 fr); fps=20.0 state=44 act=44 cams=1
599
+ embodiment_id: 10
600
+ hz: 10.0
601
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
602
+ prefix: "gr1_unified.PosttrainPnPNovelFromCuttingboardToBasketSplitA_GR1ArmsAndWaistFourierHands_1000"
603
+ weight: 489.2 # sqrt(239,307 fr); fps=20.0 state=44 act=44 cams=1
604
+ embodiment_id: 10
605
+ hz: 10.0
606
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
607
+ prefix: "gr1_unified.PosttrainPnPNovelFromPlacematToBasketSplitA_GR1ArmsAndWaistFourierHands_1000"
608
+ weight: 488.4 # sqrt(238,539 fr); fps=20.0 state=44 act=44 cams=1
609
+ embodiment_id: 10
610
+ hz: 10.0
611
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
612
+ prefix: "bimanual_panda_gripper.ThreePieceAssembly"
613
+ weight: 488.2 # sqrt(238,372 fr); fps=20.0 state=32 act=14 cams=3
614
+ embodiment_id: 11
615
+ hz: 10.0
616
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
617
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromTrayToTieredbasketSplitA"
618
+ weight: 484.2 # sqrt(234,452 fr); fps=20.0 state=44 act=44 cams=1
619
+ embodiment_id: 10
620
+ hz: 10.0
621
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
622
+ prefix: "gr1_unified.PosttrainPnPNovelFromTrayToTieredshelfSplitA_GR1ArmsAndWaistFourierHands_1000"
623
+ weight: 481.7 # sqrt(232,039 fr); fps=20.0 state=44 act=44 cams=1
624
+ embodiment_id: 10
625
+ hz: 10.0
626
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
627
+ prefix: "gr1_unified.PosttrainPnPNovelFromPlacematToTieredshelfSplitA_GR1ArmsAndWaistFourierHands_1000"
628
+ weight: 480.5 # sqrt(230,861 fr); fps=20.0 state=44 act=44 cams=1
629
+ embodiment_id: 10
630
+ hz: 10.0
631
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
632
+ prefix: "bimanual_panda_hand.BoxCleanup"
633
+ weight: 480.3 # sqrt(230,689 fr); fps=20.0 state=40 act=24 cams=3
634
+ embodiment_id: 12
635
+ hz: 10.0
636
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
637
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromPlateToCardboardboxSplitA"
638
+ weight: 474.9 # sqrt(225,540 fr); fps=20.0 state=44 act=44 cams=1
639
+ embodiment_id: 10
640
+ hz: 10.0
641
+ - repo_id: nvidia/PhysicalAI-GR00T-Tuned-Tasks
642
+ prefix: "Exhaust-Pipe-Sorting-task"
643
+ weight: 473.1 # sqrt(223,823 fr); fps=20 state=26 act=26 cams=1
644
+ embodiment_id: 15
645
+ hz: 10.0
646
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
647
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromPlateToPlateSplitA"
648
+ weight: 472.0 # sqrt(222,740 fr); fps=20.0 state=44 act=44 cams=1
649
+ embodiment_id: 10
650
+ hz: 10.0
651
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
652
+ prefix: "gr1_unified.PosttrainPnPNovelFromTrayToTieredbasketSplitA_GR1ArmsAndWaistFourierHands_1000"
653
+ weight: 471.9 # sqrt(222,664 fr); fps=20.0 state=44 act=44 cams=1
654
+ embodiment_id: 10
655
+ hz: 10.0
656
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
657
+ prefix: "gr1_unified.PosttrainPnPNovelFromCuttingboardToTieredbasketSplitA_GR1ArmsAndWaistFourierHands_1000"
658
+ weight: 469.5 # sqrt(220,405 fr); fps=20.0 state=44 act=44 cams=1
659
+ embodiment_id: 10
660
+ hz: 10.0
661
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
662
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromTrayToPlateSplitA"
663
+ weight: 468.1 # sqrt(219,082 fr); fps=20.0 state=44 act=44 cams=1
664
+ embodiment_id: 10
665
+ hz: 10.0
666
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
667
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromPlateToPanSplitA"
668
+ weight: 466.7 # sqrt(217,848 fr); fps=20.0 state=44 act=44 cams=1
669
+ embodiment_id: 10
670
+ hz: 10.0
671
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
672
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromCuttingboardToTieredbasketSplitA"
673
+ weight: 464.7 # sqrt(215,965 fr); fps=20.0 state=44 act=44 cams=1
674
+ embodiment_id: 10
675
+ hz: 10.0
676
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
677
+ prefix: "bimanual_panda_gripper.Threading"
678
+ weight: 462.1 # sqrt(213,493 fr); fps=20.0 state=32 act=14 cams=3
679
+ embodiment_id: 11
680
+ hz: 10.0
681
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
682
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromCuttingboardToPotSplitA"
683
+ weight: 462.0 # sqrt(213,457 fr); fps=20.0 state=44 act=44 cams=1
684
+ embodiment_id: 10
685
+ hz: 10.0
686
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
687
+ prefix: "gr1_unified.PosttrainPnPNovelFromPlateToPanSplitA_GR1ArmsAndWaistFourierHands_1000"
688
+ weight: 458.2 # sqrt(209,968 fr); fps=20.0 state=44 act=44 cams=1
689
+ embodiment_id: 10
690
+ hz: 10.0
691
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
692
+ prefix: "gr1_unified.PosttrainPnPNovelFromCuttingboardToPanSplitA_GR1ArmsAndWaistFourierHands_1000"
693
+ weight: 458.1 # sqrt(209,896 fr); fps=20.0 state=44 act=44 cams=1
694
+ embodiment_id: 10
695
+ hz: 10.0
696
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
697
+ prefix: "gr1_unified.PosttrainPnPNovelFromPlacematToBowlSplitA_GR1ArmsAndWaistFourierHands_1000"
698
+ weight: 454.8 # sqrt(206,826 fr); fps=20.0 state=44 act=44 cams=1
699
+ embodiment_id: 10
700
+ hz: 10.0
701
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
702
+ prefix: "gr1_unified.PosttrainPnPNovelFromPlateToBowlSplitA_GR1ArmsAndWaistFourierHands_1000"
703
+ weight: 454.6 # sqrt(206,643 fr); fps=20.0 state=44 act=44 cams=1
704
+ embodiment_id: 10
705
+ hz: 10.0
706
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
707
+ prefix: "gr1_unified.PosttrainPnPNovelFromPlateToCardboardboxSplitA_GR1ArmsAndWaistFourierHands_1000"
708
+ weight: 446.9 # sqrt(199,723 fr); fps=20.0 state=44 act=44 cams=1
709
+ embodiment_id: 10
710
+ hz: 10.0
711
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
712
+ prefix: "gr1_unified.PosttrainPnPNovelFromTrayToCardboardboxSplitA_GR1ArmsAndWaistFourierHands_1000"
713
+ weight: 446.7 # sqrt(199,505 fr); fps=20.0 state=44 act=44 cams=1
714
+ embodiment_id: 10
715
+ hz: 10.0
716
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
717
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromTrayToCardboardboxSplitA"
718
+ weight: 446.5 # sqrt(199,399 fr); fps=20.0 state=44 act=44 cams=1
719
+ embodiment_id: 10
720
+ hz: 10.0
721
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
722
+ prefix: "gr1_unified.PosttrainPnPNovelFromCuttingboardToCardboardboxSplitA_GR1ArmsAndWaistFourierHands_1000"
723
+ weight: 446.5 # sqrt(199,398 fr); fps=20.0 state=44 act=44 cams=1
724
+ embodiment_id: 10
725
+ hz: 10.0
726
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
727
+ prefix: "gr1_unified.PosttrainPnPNovelFromTrayToPlateSplitA_GR1ArmsAndWaistFourierHands_1000"
728
+ weight: 442.4 # sqrt(195,734 fr); fps=20.0 state=44 act=44 cams=1
729
+ embodiment_id: 10
730
+ hz: 10.0
731
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
732
+ prefix: "gr1_unified.PosttrainPnPNovelFromPlateToPlateSplitA_GR1ArmsAndWaistFourierHands_1000"
733
+ weight: 442.1 # sqrt(195,446 fr); fps=20.0 state=44 act=44 cams=1
734
+ embodiment_id: 10
735
+ hz: 10.0
736
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
737
+ prefix: "gr1_unified.PosttrainPnPNovelFromPlacematToPlateSplitA_GR1ArmsAndWaistFourierHands_1000"
738
+ weight: 441.3 # sqrt(194,706 fr); fps=20.0 state=44 act=44 cams=1
739
+ embodiment_id: 10
740
+ hz: 10.0
741
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
742
+ prefix: "gr1_unified.PosttrainPnPNovelFromTrayToPotSplitA_GR1ArmsAndWaistFourierHands_1000"
743
+ weight: 440.6 # sqrt(194,126 fr); fps=20.0 state=44 act=44 cams=1
744
+ embodiment_id: 10
745
+ hz: 10.0
746
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
747
+ prefix: "gr1_unified.PosttrainPnPNovelFromCuttingboardToPotSplitA_GR1ArmsAndWaistFourierHands_1000"
748
+ weight: 439.7 # sqrt(193,347 fr); fps=20.0 state=44 act=44 cams=1
749
+ embodiment_id: 10
750
+ hz: 10.0
751
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
752
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromTrayToPotSplitA"
753
+ weight: 433.4 # sqrt(187,852 fr); fps=20.0 state=44 act=44 cams=1
754
+ embodiment_id: 10
755
+ hz: 10.0
756
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
757
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromCuttingboardToCardboardboxSplitA"
758
+ weight: 427.1 # sqrt(182,379 fr); fps=20.0 state=44 act=44 cams=1
759
+ embodiment_id: 10
760
+ hz: 10.0
761
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
762
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromCuttingboardToBasketSplitA"
763
+ weight: 421.4 # sqrt(177,569 fr); fps=20.0 state=44 act=44 cams=1
764
+ embodiment_id: 10
765
+ hz: 10.0
766
+ - repo_id: nvidia/GR00T-N1.7-AppleToPlate
767
+ prefix: ""
768
+ weight: 414.3 # sqrt(171,625 fr); fps=30 state=43 act=43 cams=1
769
+ embodiment_id: 15
770
+ hz: 10.0
771
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
772
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromPlateToBowlSplitA"
773
+ weight: 414.1 # sqrt(171,460 fr); fps=20.0 state=44 act=44 cams=1
774
+ embodiment_id: 10
775
+ hz: 10.0
776
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
777
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromPlacematToPlateSplitA"
778
+ weight: 412.6 # sqrt(170,207 fr); fps=20.0 state=44 act=44 cams=1
779
+ embodiment_id: 10
780
+ hz: 10.0
781
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
782
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromCuttingboardToPanSplitA"
783
+ weight: 407.8 # sqrt(166,293 fr); fps=20.0 state=44 act=44 cams=1
784
+ embodiment_id: 10
785
+ hz: 10.0
786
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
787
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromPlacematToBowlSplitA"
788
+ weight: 397.9 # sqrt(158,324 fr); fps=20.0 state=44 act=44 cams=1
789
+ embodiment_id: 10
790
+ hz: 10.0
791
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim
792
+ prefix: "LeRobot/gr1_unified.PosttrainPnPNovelFromPlacematToBasketSplitA"
793
+ weight: 375.5 # sqrt(140,982 fr); fps=20.0 state=44 act=44 cams=1
794
+ embodiment_id: 10
795
+ hz: 10.0
796
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
797
+ prefix: "unitree_g1.LMPnPAppleToPlateDC"
798
+ weight: 279.9 # sqrt(78,369 fr); fps=50.0 state=43 act=43 cams=1
799
+ embodiment_id: 15
800
+ hz: 10.0
801
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-G1
802
+ prefix: "g1-pick-starfruit"
803
+ weight: 186.1 # sqrt(34,640 fr); fps=20 state=43 act=43 cams=1
804
+ embodiment_id: 15
805
+ hz: 10.0
806
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-G1
807
+ prefix: "g1-pick-pear"
808
+ weight: 179.8 # sqrt(32,319 fr); fps=20 state=43 act=43 cams=1
809
+ embodiment_id: 15
810
+ hz: 10.0
811
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-G1
812
+ prefix: "g1-pick-apple"
813
+ weight: 172.8 # sqrt(29,864 fr); fps=20 state=43 act=43 cams=1
814
+ embodiment_id: 15
815
+ hz: 10.0
816
+ - repo_id: nvidia/PhysicalAI-Robotics-GR00T-Teleop-G1
817
+ prefix: "g1-pick-grapes"
818
+ weight: 164.2 # sqrt(26,970 fr); fps=20 state=43 act=43 cams=1
819
+ embodiment_id: 15
820
+ hz: 10.0
tinyvla_b200/configs/trex_ft.yaml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Дообучение C-scaled на zekaiwang/trex_dataset (Dexmate Vega-1 + Sharpa Wave).
2
+ #
3
+ # ВНИМАНИЕ, три вещи отличаются от всех предыдущих запусков TinyVLA-2:
4
+ #
5
+ # 1. state/action = 58 измерений (2 x (7 DoF рука + 22 DoF кисть)), а не 8/16.
6
+ # max_action_dim/max_state_dim подняты до 64. Без этого CanonicalSource._pad
7
+ # МОЛЧА обрежет действие до первых 8 чисел (левая рука + 1 сустав кисти).
8
+ # Побочный эффект: state_proj, expert.action_in и expert.action_out меняют
9
+ # форму и переинициализируются -- train_fast.py печатает это явно.
10
+ #
11
+ # 2. action_space: native, а не canonical. Канонические действия -- дельты EE в
12
+ # базовой системе координат, их строит build_ee_sidecar.py через FK по URDF.
13
+ # Для Vega-1 + Sharpa в репозитории нет ни URDF, ни EE-позы в состоянии
14
+ # (58 чисел -- это чистые углы суставов). Значит смысл выходного пространства
15
+ # меняется, и от C-scaled переносится ствол (ViT, LM, трансформер эксперта),
16
+ # но не голова действий.
17
+ #
18
+ # 3. Данные читаются из заранее построенных шардов (scripts/build_shards.py),
19
+ # а не из LeRobotDataset. 1.53 ТБ h264 со случайным seek -- это то, что
20
+ # держало A6000 на ~10% загрузки.
21
+
22
+ output_dir: /workspace/outputs/trex_ft
23
+ wandb: null
24
+ seed: 42
25
+ morphology_descriptors: /workspace/tinyvla/configs/morphology/descriptors.yaml
26
+ resume_from: /workspace/ckpt/tv2_C_scaled_final
27
+
28
+ source:
29
+ root: /workspace/data/trex_shards
30
+ embodiment_id: 10 # свободный id: C-scaled использует 0..9
31
+ morph_key: dexmate_vega1 # см. patches/descriptors_dexmate.yaml
32
+ robot_prompt: null # текстовое описание робота ВРЕДИТ (RESULTS.md)
33
+
34
+ policy:
35
+ chunk_size: 50 # 50 шагов @ 10 Гц = 5 с, как в претрейне
36
+ n_action_steps: 50
37
+ image_size: 256
38
+ max_state_dim: 64 # 58 -> паддинг до 64
39
+ max_action_dim: 64
40
+ num_embodiments: 16
41
+ conditioning: morph # единственный канал, который реально работает
42
+ action_space: native # см. пункт 2 выше
43
+ freeze_lm: false # самый крупный рычаг в обоих поколениях
44
+ freeze_vision_encoder: true
45
+
46
+ backbone_lr_mult: 0.1
47
+ batch_size: 144 # 80 ГБ H100: grad_accum больше не нужен
48
+ grad_accum: 1
49
+ num_workers: 12
50
+ prefetch_factor: 6
51
+ shuffle_buffer: 16384
52
+ lr: 1.0e-4
53
+ warmup_steps: 1000
54
+ steps: 60000
55
+ grad_clip: 10.0
56
+ log_freq: 50
57
+ save_freq: 10000
58
+ compile: true
59
+
60
+ # для строки MFU в логе (ablation.md §6: ViT 45 + LM 46 + expert 44 GFLOPs)
61
+ gflops_per_sample: 360.0
62
+ peak_flops: 989.0e12
tinyvla_b200/patches/descriptors_dexmate.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Дописать в configs/morphology/descriptors.yaml.
2
+ # Порядок полей ОБЯЗАН совпадать с MORPH_FIELDS в modules/embodiment.py.
3
+ #
4
+ # ЗНАЧЕНИЯ НУЖНО СВЕРИТЬ СО СПЕКОЙ Dexmate Vega-1 / Sharpa Wave -- ниже
5
+ # правдоподобные, но не проверенные по документации числа. Дескриптор читается
6
+ # моделью зеро-шот (RESULTS.md: правильный дескриптор бьёт неправильный на
7
+ # 5-45%), так что ошибка здесь стоит качества, а не падения.
8
+ dexmate_vega1: # бимануальный, 2 x (7-DoF рука + 22-DoF кисть Sharpa)
9
+ arm_dof: 7 # на руку; 16-полевая схема не выражает бимануальность
10
+ reach_m: 0.70
11
+ gripper_width_m: 0.10 # многопалая кисть, не параллельный захват
12
+ num_cameras: 3 # head_left + 2 запястных (обучаем на 2 из них)
13
+ is_mobile: 1 # ПРОВЕРИТЬ: есть ли у Vega-1 подвижное основание
14
+ control_hz: 30
15
+ joint_lo_mean: -2.6
16
+ joint_hi_mean: 2.6
17
+ workspace_x: 0.50
18
+ workspace_y: 0.60
19
+ workspace_z: 0.70
20
+ payload_kg: 3.0
21
+ ee_type_parallel: -1
22
+ ee_type_multi: 1 # единственное поле, кодирующее многопалую кисть
23
+ base_holonomic: -1 # ПРОВЕРИТЬ
24
+ reserved: 0
tinyvla_b200/patches/modeling_tinyvla.py.diff ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- ../tinyvla/tinyvla2/src/tinyvla/modeling_tinyvla.py 2026-08-28 13:41:44
2
+ +++ src/tinyvla/modeling_tinyvla.py 2026-08-28 15:59:41
3
+ @@ -121,18 +121,28 @@
4
+ """Drop the n_obs_steps dim if present: (B, S, ...) -> (B, ...)."""
5
+ return x[:, -1] if x.dim() == ndim + 1 else x
6
+
7
+ - def _encode_spatial(self, batch: dict) -> torch.Tensor:
8
+ - """Per-camera spatial tokens for the fast path: (B, num_cams*64, d)."""
9
+ + def _encode_spatial(self, batch: dict, feats: dict | None = None) -> torch.Tensor:
10
+ + """Per-camera spatial tokens for the fast path: (B, num_cams*64, d).
11
+ +
12
+ + `feats` lets the caller pass vision-tower outputs computed once per step
13
+ + (see `_vision_features`) instead of re-running the tower here.
14
+ + """
15
+ cond = []
16
+ for i, key in enumerate(self.image_keys):
17
+ - tokens = self.semantic.encode_image(self._last_obs(batch[key], 4)) # (B, 64, C)
18
+ + tokens = (feats or {}).get(key)
19
+ + if tokens is None:
20
+ + tokens = self.semantic.encode_image(self._last_obs(batch[key], 4)) # (B, 64, C)
21
+ proj = self.spatial_proj(tokens) + self.camera_emb.weight[i][None, None, :]
22
+ if i == 0:
23
+ self._cam0_proj = proj # consumed by the spatial-distillation head
24
+ cond.append(proj)
25
+ return torch.cat(cond, dim=1)
26
+
27
+ - def _semantic_latent(self, batch: dict, image=None) -> torch.Tensor:
28
+ + def _vision_features(self, batch: dict, keys) -> dict:
29
+ + """One vision-tower pass per camera, shared by the fast and slow paths."""
30
+ + return {k: self.semantic.encode_image(self._last_obs(batch[k], 4)) for k in keys}
31
+ +
32
+ + def _semantic_latent(self, batch: dict, image=None, image_embeds=None) -> torch.Tensor:
33
+ """Slow-path latent: (B, K, d), cacheable, conditions the fast path.
34
+
35
+ vlm_native=True makes this latent RICH — the LM sequence also carries the
36
+ @@ -163,23 +173,35 @@
37
+ batch[OBS_LANGUAGE_TOKENS],
38
+ batch[OBS_LANGUAGE_ATTENTION_MASK],
39
+ morphology=morph,
40
+ + image_embeds=image_embeds,
41
+ )
42
+
43
+ def _conditioning(self, batch: dict) -> torch.Tensor:
44
+ b = batch[self.image_keys[0]].shape[0]
45
+ device = batch[self.image_keys[0]].device
46
+ +
47
+ + # Run the shared Qwen vision tower ONCE per camera per step. Previously
48
+ + # cam0 went through it twice: here for the spatial tokens, and again
49
+ + # inside the LM call in `_semantic_latent`. The tower is ~45 GFLOPs per
50
+ + # camera, i.e. a quarter of the forward pass thrown away every step.
51
+ + feats = None
52
+ + if self.config.use_spatial_tokens:
53
+ + feats = self._vision_features(batch, self.image_keys)
54
+ + elif self.config.use_semantic_latent and not self.config.vlm_native:
55
+ + feats = self._vision_features(batch, self.image_keys[:1])
56
+
57
+ parts = []
58
+ if self.config.use_semantic_latent:
59
+ latent = batch.get("semantic_latent")
60
+ if latent is None:
61
+ - if "latent_image" in batch: # staleness augmentation: latent from an older frame
62
+ + if "latent_image" in batch: # staleness aug: latent from an older frame,
63
+ latent = self._semantic_latent(batch, image=batch["latent_image"])
64
+ - else:
65
+ - latent = self._semantic_latent(batch)
66
+ + else: # ...so the cached cam0 features do NOT apply
67
+ + cam0 = None if (feats is None or self.config.vlm_native) else feats[self.image_keys[0]]
68
+ + latent = self._semantic_latent(batch, image_embeds=cam0)
69
+ parts.append(latent)
70
+ if self.config.use_spatial_tokens:
71
+ - parts.append(self._encode_spatial(batch))
72
+ + parts.append(self._encode_spatial(batch, feats))
73
+ if self.state_dim:
74
+ state = pad_vector(self._last_obs(batch[OBS_STATE], 2), self.config.max_state_dim)
75
+ parts.append(self.state_proj(state)[:, None, :])
tinyvla_b200/patches/modules_expert.py.diff ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- ../tinyvla/tinyvla2/src/tinyvla/modules/expert.py 2026-08-28 13:41:44
2
+ +++ src/tinyvla/modules/expert.py 2026-08-28 15:58:45
3
+ @@ -11,6 +11,7 @@
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ +import torch.nn.functional as F
8
+
9
+
10
+ def sincos_time_embedding(t: torch.Tensor, dim: int, min_period: float = 4e-3, max_period: float = 4.0) -> torch.Tensor:
11
+ @@ -23,6 +24,47 @@
12
+ return torch.cat([angles.sin(), angles.cos()], dim=-1)
13
+
14
+
15
+ +class _MHA(nn.Module):
16
+ + """Drop-in replacement for torch's batch-first multi-head attention, routed
17
+ + through F.scaled_dot_product_attention (flash / mem-efficient kernels, and
18
+ + far friendlier to torch.compile than the eager fallback).
19
+ +
20
+ + Parameter names are kept identical (in_proj_weight / in_proj_bias /
21
+ + out_proj.weight / out_proj.bias) so existing checkpoints load unchanged.
22
+ + `key_padding_mask` keeps torch semantics: True = ignore that key.
23
+ + """
24
+ +
25
+ + def __init__(self, dim: int, n_heads: int):
26
+ + super().__init__()
27
+ + assert dim % n_heads == 0
28
+ + self.dim, self.n_heads, self.head_dim = dim, n_heads, dim // n_heads
29
+ + self.in_proj_weight = nn.Parameter(torch.empty(3 * dim, dim))
30
+ + self.in_proj_bias = nn.Parameter(torch.zeros(3 * dim))
31
+ + self.out_proj = nn.Linear(dim, dim)
32
+ + nn.init.xavier_uniform_(self.in_proj_weight)
33
+ + nn.init.zeros_(self.out_proj.bias)
34
+ +
35
+ + def forward(self, query, key, value, key_padding_mask=None, need_weights=False):
36
+ + b, lq, d = query.shape
37
+ + w_q, w_k, w_v = self.in_proj_weight.split(d, dim=0)
38
+ + b_q, b_k, b_v = self.in_proj_bias.split(d, dim=0)
39
+ + q = F.linear(query, w_q, b_q)
40
+ + k = F.linear(key, w_k, b_k)
41
+ + v = F.linear(value, w_v, b_v)
42
+ +
43
+ + def split(t):
44
+ + return t.view(b, -1, self.n_heads, self.head_dim).transpose(1, 2)
45
+ +
46
+ + q, k, v = split(q), split(k), split(v)
47
+ + attn_mask = None
48
+ + if key_padding_mask is not None:
49
+ + # torch convention: True = ignore. SDPA bool mask: True = attend.
50
+ + attn_mask = (~key_padding_mask)[:, None, None, :]
51
+ + o = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
52
+ + o = o.transpose(1, 2).reshape(b, lq, d)
53
+ + return self.out_proj(o), None
54
+ +
55
+ +
56
+ class _MLP(nn.Module):
57
+ def __init__(self, dim: int, ratio: int = 4):
58
+ super().__init__()
59
+ @@ -40,7 +82,7 @@
60
+ def __init__(self, dim: int, n_heads: int):
61
+ super().__init__()
62
+ self.norm1 = nn.LayerNorm(dim)
63
+ - self.attn = nn.MultiheadAttention(dim, n_heads, batch_first=True)
64
+ + self.attn = _MHA(dim, n_heads)
65
+ self.norm2 = nn.LayerNorm(dim)
66
+ self.mlp = _MLP(dim)
67
+
68
+ @@ -57,7 +99,7 @@
69
+ super().__init__()
70
+ self.norm1 = nn.LayerNorm(dim)
71
+ self.norm_cond = nn.LayerNorm(dim)
72
+ - self.attn = nn.MultiheadAttention(dim, n_heads, batch_first=True)
73
+ + self.attn = _MHA(dim, n_heads)
74
+ self.norm2 = nn.LayerNorm(dim)
75
+ self.mlp = _MLP(dim)
76
+
tinyvla_b200/patches/modules_semantic.py.diff ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- ../tinyvla/tinyvla2/src/tinyvla/modules/semantic.py 2026-08-28 13:41:44
2
+ +++ src/tinyvla/modules/semantic.py 2026-08-28 17:23:51
3
+ @@ -69,6 +69,7 @@
4
+
5
+ self.out_proj = nn.Linear(self.hidden_size, out_dim)
6
+ self.num_readout = num_readout
7
+ + self._fixed_fn = None # lazily compiled fixed-grid tower path
8
+ self.morph_to_slow = morph_to_slow
9
+ if morph_to_slow:
10
+ # inject robot morphology into the slow LM prefix so the semantic
11
+ @@ -112,14 +113,95 @@
12
+ """Qwen vision tower spatial tokens: (B, 3, H, W) -> (B, 64, out_hidden).
13
+
14
+ Shared encoder for both paths; the fast path cross-attends to these
15
+ - directly, bypassing the language stack.
16
+ + directly, bypassing the language stack. The SAME tensor can be handed to
17
+ + `forward(..., image_embeds=...)` so cam0 goes through the tower once per
18
+ + step instead of twice (once here, once inside the LM call).
19
+ +
20
+ + Uses a dense fixed-grid fast path: every image here is the same
21
+ + (1, g, g) grid, so the HF packed-varlen tower (which runs B separate
22
+ + SDPA calls PER LAYER and rebuilds pos/rotary tables in Python on every
23
+ + call) is replaced by one batched SDPA per layer over (B, L, D), with the
24
+ + per-image positional constants computed once and cached. Numerically
25
+ + equivalent — per-image attention with equal lengths IS batched attention.
26
+ """
27
+ b = images.shape[0]
28
+ pixel_values, grid_thw = self._patchify(images)
29
+ + fn = self._fixed_fn
30
+ + if fn is None:
31
+ + import os
32
+ +
33
+ + if os.environ.get("TINYVLA_NO_COMPILE_VIT"):
34
+ + fn = self._encode_image_fixed
35
+ + else:
36
+ + # static shapes -> compile is safe and worth ~2x on top of the
37
+ + # dense rewrite; recompiles once per new batch size
38
+ + fn = torch.compile(self._encode_image_fixed, dynamic=False)
39
+ + self._fixed_fn = fn
40
+ + try:
41
+ + return fn(b, pixel_values)
42
+ + except Exception:
43
+ + if fn is not self._encode_image_fixed: # compile blew up -> eager forever
44
+ + self._fixed_fn = self._encode_image_fixed
45
+ + return self._encode_image_fixed(b, pixel_values)
46
+ + raise
47
+ +
48
+ + def _encode_image_hf(self, images: torch.Tensor) -> torch.Tensor:
49
+ + """Reference path through the stock HF tower (kept for verification)."""
50
+ + b = images.shape[0]
51
+ + pixel_values, grid_thw = self._patchify(images)
52
+ out = self.vlm.get_image_features(pixel_values.to(self.pixel_mean.dtype), grid_thw)
53
+ embeds = torch.cat(out.pooler_output, dim=0) # per-image tuple -> (B*64, C)
54
+ return embeds.reshape(b, self.num_image_tokens, -1)
55
+
56
+ + def _fixed_grid_ctx(self, device):
57
+ + """Per-image positional constants for the fixed (1, g, g) grid, computed
58
+ + ONCE via the tower's own functions (so the layout can't drift) and cached."""
59
+ + g = self.image_size // self.patch_size
60
+ + ctx = getattr(self, "_fixed_ctx", None)
61
+ + if ctx is not None and ctx[0] == (g, str(device)):
62
+ + return ctx[1]
63
+ + visual = self.vlm.visual
64
+ + grid1 = torch.tensor([[1, g, g]], device=device)
65
+ + with torch.no_grad():
66
+ + pos = visual.fast_pos_embed_interpolate(grid1) # (g*g, hidden)
67
+ + rot = visual.rot_pos_emb(grid1) # (g*g, head_dim/2)
68
+ + emb = torch.cat((rot, rot), dim=-1)
69
+ + val = (pos.detach(), emb.cos().detach(), emb.sin().detach())
70
+ + self._fixed_ctx = ((g, str(device)), val)
71
+ + return val
72
+ +
73
+ + def _encode_image_fixed(self, b: int, pixel_values: torch.Tensor) -> torch.Tensor:
74
+ + from transformers.models.qwen3_5.modeling_qwen3_5 import rotate_half
75
+ +
76
+ + visual = self.vlm.visual
77
+ + g = self.image_size // self.patch_size
78
+ + L = g * g
79
+ + x = visual.patch_embed(pixel_values.to(self.pixel_mean.dtype)) # (B*L, D)
80
+ + pos, cos, sin = self._fixed_grid_ctx(x.device)
81
+ + d = x.shape[-1]
82
+ + x = x.view(b, L, d) + pos[None].to(x.dtype)
83
+ + # (1, 1, L, hd) fp32 — same upcast-to-fp32 rotary as the HF path
84
+ + cosf = cos[None, None].float()
85
+ + sinf = sin[None, None].float()
86
+ + for blk in visual.blocks:
87
+ + attn = blk.attn
88
+ + h = blk.norm1(x)
89
+ + qkv = attn.qkv(h).view(b, L, 3, attn.num_heads, attn.head_dim)
90
+ + q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0) # (B, nh, L, hd)
91
+ + qf, kf = q.float(), k.float()
92
+ + q = ((qf * cosf) + (rotate_half(qf) * sinf)).to(q.dtype)
93
+ + k = ((kf * cosf) + (rotate_half(kf) * sinf)).to(k.dtype)
94
+ + o = F.scaled_dot_product_attention(q, k, v, scale=attn.scaling)
95
+ + o = o.transpose(1, 2).reshape(b, L, d)
96
+ + x = x + attn.proj(o)
97
+ + x = x + blk.mlp(blk.norm2(x))
98
+ + merged = visual.merger(x.reshape(b * L, d)) # (B*L/m^2, out_hidden)
99
+ + return merged.reshape(b, self.num_image_tokens, -1)
100
+ +
101
+ + def _grid_thw(self, b: int, device) -> torch.Tensor:
102
+ + g = self.image_size // self.patch_size
103
+ + return torch.tensor([[1, g, g]], device=device).expand(b, -1)
104
+ +
105
+ def _patchify(self, images: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
106
+ """(B, 3, H, W) in [0,1] -> Qwen-VL flattened patches + grid_thw (B, 3).
107
+
108
+ @@ -147,16 +229,24 @@
109
+
110
+ def forward(
111
+ self,
112
+ - images: torch.Tensor, # (B, 3, H, W) in [0, 1]
113
+ + images: torch.Tensor | None, # (B, 3, H, W) in [0, 1]; may be None with image_embeds
114
+ lang_tokens: torch.Tensor, # (B, L)
115
+ lang_mask: torch.Tensor, # (B, L) bool, True = valid
116
+ morphology: torch.Tensor | None = None, # (B, morph_dim) for morph_to_slow
117
+ + image_embeds: torch.Tensor | None = None, # (B, n_img, C) already through the tower
118
+ ) -> torch.Tensor:
119
+ - """Returns semantic latent (B, num_readout, out_dim)."""
120
+ - b = images.shape[0]
121
+ - device = images.device
122
+ + """Returns semantic latent (B, num_readout, out_dim).
123
+
124
+ - pixel_values, grid_thw = self._patchify(images)
125
+ + `image_embeds` lets the caller reuse the vision-tower output the fast path
126
+ + already computed for the same frame — the tower is ~45 GFLOPs/camera and
127
+ + cam0 was otherwise encoded twice per training step.
128
+ + """
129
+ + if image_embeds is not None:
130
+ + b, device = image_embeds.shape[0], image_embeds.device
131
+ + pixel_values, grid_thw = None, self._grid_thw(b, device)
132
+ + else:
133
+ + b, device = images.shape[0], images.device
134
+ + pixel_values, grid_thw = self._patchify(images)
135
+ n_img = self.num_image_tokens
136
+
137
+ # <vision_start> <image>*n <vision_end> instruction <pad>*K
138
+ @@ -180,19 +270,34 @@
139
+ )
140
+ mm_type = (ids == self.image_token_id).int() # text 0, image 1
141
+
142
+ - if self.morph_to_slow and morphology is not None:
143
+ - # embed tokens, scatter image features, add morphology to readout slots,
144
+ - # then run the LM on inputs_embeds (keeps M-RoPE via mm_token_type_ids)
145
+ + use_embeds = image_embeds is not None or (self.morph_to_slow and morphology is not None)
146
+ + if use_embeds:
147
+ + # embed tokens, scatter image features, optionally add morphology to the
148
+ + # readout slots, then run the LM on inputs_embeds (keeps M-RoPE via
149
+ + # mm_token_type_ids). This is also the path that consumes precomputed
150
+ + # `image_embeds` — identical maths, one fewer vision-tower pass.
151
+ inputs_embeds = self.vlm.get_input_embeddings()(ids)
152
+ - img = self.vlm.get_image_features(pixel_values.to(self.pixel_mean.dtype), grid_thw)
153
+ - img = torch.cat(img.pooler_output, dim=0).to(inputs_embeds.dtype) # (B*n_img, hidden)
154
+ + if image_embeds is not None:
155
+ + img = image_embeds.reshape(-1, image_embeds.shape[-1])
156
+ + else:
157
+ + img = self.vlm.get_image_features(pixel_values.to(self.pixel_mean.dtype), grid_thw)
158
+ + img = torch.cat(img.pooler_output, dim=0) # (B*n_img, hidden)
159
+ + img = img.to(inputs_embeds.dtype)
160
+ imask = (ids == self.image_token_id).unsqueeze(-1).expand_as(inputs_embeds)
161
+ inputs_embeds = inputs_embeds.masked_scatter(imask, img)
162
+ - morph_tok = self.morph_to_lm(morphology.to(inputs_embeds.dtype)) # (B, hidden)
163
+ - inputs_embeds[:, -self.num_readout:] = inputs_embeds[:, -self.num_readout:] + morph_tok[:, None, :]
164
+ + if self.morph_to_slow and morphology is not None:
165
+ + morph_tok = self.morph_to_lm(morphology.to(inputs_embeds.dtype)) # (B, hidden)
166
+ + inputs_embeds[:, -self.num_readout:] = inputs_embeds[:, -self.num_readout:] + morph_tok[:, None, :]
167
+ + # With inputs_embeds and no input_ids the model cannot recompute
168
+ + # M-RoPE and falls back to a CACHED rope_deltas from a previous call
169
+ + # (crashes on a different batch size; silently wrong on the same one).
170
+ + # Compute the 3D position ids explicitly from the token layout.
171
+ + position_ids, _ = self.vlm.get_rope_index(
172
+ + ids, image_grid_thw=grid_thw, attention_mask=attn, mm_token_type_ids=mm_type,
173
+ + )
174
+ out = self.vlm(
175
+ inputs_embeds=inputs_embeds, image_grid_thw=grid_thw, attention_mask=attn,
176
+ - mm_token_type_ids=mm_type, use_cache=False,
177
+ + mm_token_type_ids=mm_type, position_ids=position_ids, use_cache=False,
178
+ )
179
+ else:
180
+ out = self.vlm(
tinyvla_b200/scripts/bench_model.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Measure the TinyVLA training step on real hardware, synthetic data.
3
+
4
+ Builds the model out of the raw modules (SemanticPath / FlowMatchingExpert /
5
+ projections) so lerobot isn't needed, and reproduces _conditioning + the flow
6
+ loss exactly as modeling_tinyvla.py does. Sweeps: precision, dedup of the cam0
7
+ vision-tower pass, torch.compile, batch size.
8
+
9
+ Prints samples/s -> hours per 1M frames and per the 8.64M-sample C-scaled budget.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import time
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+ import torch.nn.functional as F
20
+
21
+ from tinyvla.modules.expert import FlowMatchingExpert
22
+ from tinyvla.modules.semantic import SemanticPath
23
+
24
+
25
+ class Bench(nn.Module):
26
+ """_conditioning + forward of TinyVLAPolicy, minus the lerobot wrapper."""
27
+
28
+ def __init__(self, d=512, max_state=64, max_action=64, chunk=50,
29
+ lm_layers=12, readout=8, freeze_lm=False, freeze_vision=True,
30
+ model_name="Qwen/Qwen3.5-0.8B"):
31
+ super().__init__()
32
+ self.semantic = SemanticPath(
33
+ model_name=model_name, num_layers=lm_layers, num_readout=readout,
34
+ out_dim=d, image_size=256, freeze_lm=freeze_lm, freeze_vision=freeze_vision,
35
+ )
36
+ self.spatial_proj = nn.Linear(self.semantic.visual_hidden_size, d)
37
+ self.camera_emb = nn.Embedding(3, d)
38
+ self.state_proj = nn.Linear(max_state, d)
39
+ self.embodiment_emb = nn.Embedding(16, d)
40
+ self.expert = FlowMatchingExpert(action_dim=max_action, chunk_size=chunk, d_model=d)
41
+ self.chunk, self.max_action = chunk, max_action
42
+
43
+ def forward(self, batch, dedup=True):
44
+ cam0, cam1 = batch["cam0"], batch["cam1"]
45
+ f0 = self.semantic.encode_image(cam0)
46
+ f1 = self.semantic.encode_image(cam1)
47
+ if dedup:
48
+ latent = self.semantic(None, batch["tok"], batch["mask"], image_embeds=f0)
49
+ else:
50
+ latent = self.semantic(cam0, batch["tok"], batch["mask"])
51
+ spatial = torch.cat([
52
+ self.spatial_proj(f0) + self.camera_emb.weight[0][None, None],
53
+ self.spatial_proj(f1) + self.camera_emb.weight[1][None, None],
54
+ ], dim=1)
55
+ state_tok = self.state_proj(batch["state"])[:, None]
56
+ emb = self.embodiment_emb(batch["emb_id"])[:, None]
57
+ cond = torch.cat([latent, spatial, state_tok, emb], dim=1)
58
+
59
+ actions = batch["actions"]
60
+ b = actions.shape[0]
61
+ t = torch.rand(b, device=actions.device) * 0.999 + 0.001
62
+ noise = torch.randn_like(actions)
63
+ x_t = t[:, None, None] * noise + (1 - t[:, None, None]) * actions
64
+ pred = self.expert(x_t, t, cond)
65
+ return F.mse_loss(pred, noise - actions)
66
+
67
+
68
+ def make_batch(b, dev, max_state=64, max_action=64, chunk=50, lm_len=48):
69
+ return {
70
+ "cam0": torch.rand(b, 3, 256, 256, device=dev),
71
+ "cam1": torch.rand(b, 3, 256, 256, device=dev),
72
+ "tok": torch.randint(1000, 5000, (b, lm_len), device=dev),
73
+ "mask": torch.ones(b, lm_len, dtype=torch.bool, device=dev),
74
+ "state": torch.randn(b, max_state, device=dev),
75
+ "actions": torch.randn(b, chunk, max_action, device=dev),
76
+ "emb_id": torch.randint(0, 16, (b,), device=dev),
77
+ }
78
+
79
+
80
+ def run(model, opt, batch, dedup, steps, warmup, autocast):
81
+ for i in range(warmup + steps):
82
+ if i == warmup:
83
+ torch.cuda.synchronize()
84
+ t0 = time.time()
85
+ opt.zero_grad(set_to_none=True)
86
+ with torch.autocast("cuda", dtype=torch.bfloat16, enabled=autocast):
87
+ loss = model(batch, dedup=dedup)
88
+ loss.backward()
89
+ opt.step()
90
+ torch.cuda.synchronize()
91
+ return (time.time() - t0) / steps
92
+
93
+
94
+ def main():
95
+ ap = argparse.ArgumentParser()
96
+ ap.add_argument("--model-name", default="Qwen/Qwen3.5-0.8B")
97
+ ap.add_argument("--steps", type=int, default=30)
98
+ ap.add_argument("--warmup", type=int, default=10)
99
+ ap.add_argument("--batches", type=int, nargs="+", default=[48, 96, 144, 192])
100
+ ap.add_argument("--compile", action="store_true")
101
+ ap.add_argument("--freeze-lm", action="store_true")
102
+ args = ap.parse_args()
103
+
104
+ torch.backends.cuda.matmul.allow_tf32 = True
105
+ torch.backends.cudnn.allow_tf32 = True
106
+ dev = torch.device("cuda")
107
+
108
+ model = Bench(freeze_lm=args.freeze_lm, model_name=args.model_name).to(dev)
109
+ n_train = sum(p.numel() for p in model.parameters() if p.requires_grad)
110
+ n_all = sum(p.numel() for p in model.parameters())
111
+ print(f"params: {n_all/1e6:.1f}M total, {n_train/1e6:.1f}M trainable "
112
+ f"(freeze_lm={args.freeze_lm}, vision frozen)")
113
+
114
+ opt = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad],
115
+ lr=1e-4, betas=(0.9, 0.95), weight_decay=1e-10, fused=True)
116
+ if args.compile:
117
+ model.expert = torch.compile(model.expert, dynamic=False)
118
+
119
+ BUDGET = 8_640_000 # C-scaled: 60k steps x eff batch 144
120
+ print(f"\n{'batch':>6} {'dedup':>6} {'ms/step':>9} {'samples/s':>10} "
121
+ f"{'ч на 1M кадров':>15} {'ч на 8.64M':>11} {'VRAM GB':>8}")
122
+ for b in args.batches:
123
+ batch = make_batch(b, dev)
124
+ for dedup in (True, False):
125
+ torch.cuda.reset_peak_memory_stats()
126
+ try:
127
+ dt = run(model, opt, batch, dedup, args.steps, args.warmup, autocast=True)
128
+ except torch.cuda.OutOfMemoryError:
129
+ print(f"{b:>6} {str(dedup):>6} OOM")
130
+ torch.cuda.empty_cache()
131
+ continue
132
+ sps = b / dt
133
+ mem = torch.cuda.max_memory_allocated() / 1e9
134
+ print(f"{b:>6} {str(dedup):>6} {dt*1000:>9.1f} {sps:>10.0f} "
135
+ f"{1e6/sps/3600:>15.2f} {BUDGET/sps/3600:>11.2f} {mem:>8.1f}", flush=True)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
tinyvla_b200/scripts/build_shards.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """One-time shard builder: LeRobot v3 video dataset -> sequential JPEG shards.
3
+
4
+ Why this exists
5
+ ---------------
6
+ Training reads ONE random frame per sample. On a LeRobot v3 repo that means an
7
+ h264 seek into a ~195 MB mp4 holding ~10 concatenated episodes, per camera, per
8
+ sample. That is the bottleneck the project notes already call out ("video decode
9
+ is the training bottleneck, use 12+ dataloader workers"), and no amount of GPU
10
+ fixes it.
11
+
12
+ This pass decodes every video ONCE, sequentially (the cheap direction), at a
13
+ target rate, resizes to the training resolution, and writes JPEG bytes into
14
+ parquet shards. Training then does sequential reads + JPEG decode, which is
15
+ ~100x cheaper per frame and trivially parallel.
16
+
17
+ Layout produced in --out:
18
+ shards/shard-{i:05d}.parquet episode_index, frame_index, cam0, cam1 (JPEG bytes)
19
+ frames.parquet episode_index, frame_index, state[D], action[D], task_index
20
+ tasks.json task_index -> instruction string
21
+ stats.json per-source mean/std for state and action
22
+ manifest.json fps, image_size, dims, camera keys, counts
23
+
24
+ Usage (parallel across boxes: each takes a disjoint episode range):
25
+ python build_shards.py --repo-id zekaiwang/trex_dataset --out /data/trex_shards \
26
+ --hz 10 --image-size 256 --part 0 --num-parts 4
27
+
28
+ Verified against zekaiwang/trex_dataset metadata: episodes are contiguous and
29
+ ordered inside every mp4, frame counts match the timestamps exactly, and the
30
+ declared fps (30) is the real one.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import argparse
36
+ import io
37
+ import json
38
+ import os
39
+ from pathlib import Path
40
+
41
+ import numpy as np
42
+ import pyarrow as pa
43
+ import pyarrow.parquet as pq
44
+
45
+
46
+ # --------------------------------------------------------------------- metadata
47
+
48
+
49
+ def load_metadata(repo_id: str, revision: str | None):
50
+ from huggingface_hub import hf_hub_download
51
+
52
+ def grab(path):
53
+ return hf_hub_download(repo_id, path, repo_type="dataset", revision=revision)
54
+
55
+ info = json.loads(Path(grab("meta/info.json")).read_text())
56
+
57
+ from huggingface_hub import HfApi
58
+
59
+ api = HfApi()
60
+ files = api.list_repo_files(repo_id, repo_type="dataset", revision=revision)
61
+ ep_files = sorted(f for f in files if f.startswith("meta/episodes/") and f.endswith(".parquet"))
62
+ tables = [pq.read_table(grab(f)) for f in ep_files]
63
+ episodes = pa.concat_tables(tables) if len(tables) > 1 else tables[0]
64
+ return info, episodes, files
65
+
66
+
67
+ def pick_cameras(info: dict, cam0: str | None, cam1: str | None):
68
+ """Mirror CanonicalSource's cam0/cam1 choice unless the caller overrides it.
69
+
70
+ CanonicalSource sorts the image keys, takes PRIMARY_RE (or the first key) as
71
+ cam0 and the first WRIST_RE match as cam1. On trex that resolves to
72
+ head_left / left_wrist — right_wrist and all 20 tactile streams are dropped.
73
+ Made explicit here rather than left to a regex.
74
+ """
75
+ import re
76
+
77
+ PRIMARY_RE = re.compile(r"top|front|base|main|scene|image_0$|images\.image$|image2$")
78
+ WRIST_RE = re.compile(r"wrist|gripper|hand|image_1$")
79
+ keys = sorted(k for k, v in info["features"].items() if v.get("dtype") == "video")
80
+ if cam0 is None:
81
+ cam0 = next((k for k in keys if PRIMARY_RE.search(k)), keys[0])
82
+ if cam1 is None:
83
+ cam1 = next((k for k in keys if k != cam0 and WRIST_RE.search(k)), None)
84
+ assert cam0 in keys, f"{cam0} not among {keys}"
85
+ assert cam1 is None or cam1 in keys, f"{cam1} not among {keys}"
86
+ return cam0, cam1
87
+
88
+
89
+ # ------------------------------------------------------------------ video decode
90
+
91
+
92
+ class VideoReader:
93
+ """Opens one mp4 at a time, seeks to an episode, decodes its frames forward.
94
+
95
+ Episodes inside a file are contiguous and time-ordered (verified), so walking
96
+ episodes in order keeps this effectively sequential: one seek per episode,
97
+ costing at most one extra GOP.
98
+ """
99
+
100
+ def __init__(self, repo_id, revision, key, cache_dir, evict: bool):
101
+ self.repo_id, self.revision, self.key = repo_id, revision, key
102
+ self.cache_dir, self.evict = cache_dir, evict
103
+ self._path = None
104
+ self._ident = None
105
+ self._container = None
106
+ self._stream = None
107
+
108
+ def _open(self, chunk_index: int, file_index: int):
109
+ ident = (chunk_index, file_index)
110
+ if ident == self._ident:
111
+ return
112
+ self._close(drop=True)
113
+ from huggingface_hub import hf_hub_download
114
+
115
+ rel = f"videos/{self.key}/chunk-{chunk_index:03d}/file-{file_index:03d}.mp4"
116
+ self._path = hf_hub_download(
117
+ self.repo_id, rel, repo_type="dataset", revision=self.revision, cache_dir=self.cache_dir
118
+ )
119
+ import av
120
+
121
+ self._container = av.open(self._path)
122
+ self._stream = self._container.streams.video[0]
123
+ self._stream.thread_type = "AUTO" # frame+slice threading; the whole point
124
+ self._ident = ident
125
+
126
+ def _close(self, drop: bool):
127
+ if self._container is not None:
128
+ self._container.close()
129
+ self._container = None
130
+ if drop and self.evict and self._path and os.path.exists(self._path):
131
+ real = os.path.realpath(self._path) # hf cache stores a symlink to the blob
132
+ for p in {self._path, real}:
133
+ try:
134
+ os.remove(p)
135
+ except OSError:
136
+ pass
137
+ self._path = self._ident = self._stream = None
138
+
139
+ def close(self):
140
+ self._close(drop=True)
141
+
142
+ def read_episode(self, chunk_index, file_index, from_ts, n_frames, fps, stride):
143
+ """Yield (frame_index_in_episode, PIL.Image) for every stride-th frame."""
144
+ self._open(chunk_index, file_index)
145
+ tb = self._stream.time_base
146
+ target = int(from_ts / tb)
147
+ self._container.seek(target, stream=self._stream, backward=True, any_frame=False)
148
+ wanted = set(range(0, n_frames, stride))
149
+ eps = 0.5 / fps # half a frame of slack against float timestamps
150
+ got = 0
151
+ for frame in self._container.decode(self._stream):
152
+ if frame.pts is None:
153
+ continue
154
+ t = float(frame.pts * tb)
155
+ if t < from_ts - eps:
156
+ continue
157
+ idx = int(round((t - from_ts) * fps))
158
+ if idx >= n_frames:
159
+ break
160
+ if idx in wanted:
161
+ wanted.discard(idx)
162
+ got += 1
163
+ yield idx, frame.to_image()
164
+ if not wanted:
165
+ break
166
+ if wanted:
167
+ raise RuntimeError(
168
+ f"{self.key} chunk{chunk_index}/file{file_index} @{from_ts:.3f}: "
169
+ f"decoded {got}, still missing {len(wanted)} frames"
170
+ )
171
+
172
+
173
+ def encode_jpeg(img, size: int, quality: int) -> bytes:
174
+ """Resize to (size, size) and JPEG-encode.
175
+
176
+ Plain resize, no crop: this reproduces CanonicalSource._img, which squashes
177
+ the aspect ratio with F.interpolate. Matching it matters more than being
178
+ geometrically nicer, otherwise the shards are off-distribution for the
179
+ pretrained checkpoint.
180
+ """
181
+ from PIL import Image
182
+
183
+ if img.size != (size, size):
184
+ img = img.resize((size, size), Image.BILINEAR)
185
+ buf = io.BytesIO()
186
+ img.save(buf, format="JPEG", quality=quality, subsampling=0)
187
+ return buf.getvalue()
188
+
189
+
190
+ # ------------------------------------------------------------------------ state
191
+
192
+
193
+ def load_frame_table(repo_id, revision, files, episodes, keep_mask, stride, cache_dir):
194
+ """Pull state/action/task for the kept frames out of the data parquets."""
195
+ from huggingface_hub import hf_hub_download
196
+
197
+ ep = {c: np.asarray(episodes[c]) for c in
198
+ ["episode_index", "length", "dataset_from_index", "data/chunk_index", "data/file_index"]}
199
+ want = {} # (chunk, file) -> list of episode rows
200
+ for i in np.nonzero(keep_mask)[0]:
201
+ want.setdefault((int(ep["data/chunk_index"][i]), int(ep["data/file_index"][i])), []).append(i)
202
+
203
+ out_ep, out_fr, out_state, out_action, out_task = [], [], [], [], []
204
+ tasks: dict[str, int] = {}
205
+ for (ci, fi), rows in sorted(want.items()):
206
+ rel = f"data/chunk-{ci:03d}/file-{fi:03d}.parquet"
207
+ if rel not in files:
208
+ raise FileNotFoundError(rel)
209
+ path = hf_hub_download(repo_id, rel, repo_type="dataset", revision=revision, cache_dir=cache_dir)
210
+ t = pq.read_table(path, columns=["episode_index", "frame_index", "observation.state", "action", "task_index"])
211
+ d = {c: t[c].to_numpy(zero_copy_only=False) for c in t.column_names}
212
+ for r in rows:
213
+ e = int(ep["episode_index"][r])
214
+ sel = np.nonzero(d["episode_index"] == e)[0]
215
+ if len(sel) != int(ep["length"][r]):
216
+ raise RuntimeError(f"episode {e}: parquet has {len(sel)} rows, meta says {ep['length'][r]}")
217
+ order = np.argsort(d["frame_index"][sel])
218
+ sel = sel[order][::stride]
219
+ out_ep.append(np.full(len(sel), e, dtype=np.int32))
220
+ out_fr.append(np.arange(len(sel), dtype=np.int32))
221
+ out_state.append(np.stack([np.asarray(x, dtype=np.float32) for x in d["observation.state"][sel]]))
222
+ out_action.append(np.stack([np.asarray(x, dtype=np.float32) for x in d["action"][sel]]))
223
+ task = episodes["tasks"][r].as_py()
224
+ task = task[0] if isinstance(task, list) and task else ""
225
+ out_task.append(np.full(len(sel), tasks.setdefault(task, len(tasks)), dtype=np.int32))
226
+ del t, d
227
+ return (
228
+ np.concatenate(out_ep), np.concatenate(out_fr),
229
+ np.concatenate(out_state), np.concatenate(out_action),
230
+ np.concatenate(out_task), tasks,
231
+ )
232
+
233
+
234
+ # ------------------------------------------------------------------------- main
235
+
236
+
237
+ def main():
238
+ ap = argparse.ArgumentParser()
239
+ ap.add_argument("--repo-id", default="zekaiwang/trex_dataset")
240
+ ap.add_argument("--revision", default=None)
241
+ ap.add_argument("--out", type=Path, required=True)
242
+ ap.add_argument("--hz", type=float, default=10.0, help="target rate; 0 = keep native fps")
243
+ ap.add_argument("--image-size", type=int, default=256)
244
+ ap.add_argument("--jpeg-quality", type=int, default=92)
245
+ ap.add_argument("--rows-per-shard", type=int, default=20000)
246
+ ap.add_argument("--cam0", default=None, help="override the primary camera key")
247
+ ap.add_argument("--cam1", default=None, help="override the wrist camera key")
248
+ ap.add_argument("--part", type=int, default=0)
249
+ ap.add_argument("--num-parts", type=int, default=1)
250
+ ap.add_argument("--max-episodes", type=int, default=None, help="smoke test on the first N")
251
+ ap.add_argument("--cache-dir", default=None)
252
+ ap.add_argument("--keep-videos", action="store_true", help="do NOT delete mp4s after use")
253
+ args = ap.parse_args()
254
+
255
+ info, episodes, files = load_metadata(args.repo_id, args.revision)
256
+ fps = float(info["fps"])
257
+ stride = 1 if args.hz <= 0 else max(1, int(round(fps / args.hz)))
258
+ eff_hz = fps / stride
259
+ cam0, cam1 = pick_cameras(info, args.cam0, args.cam1)
260
+
261
+ n_ep = episodes.num_rows
262
+ lengths = np.asarray(episodes["length"])
263
+ # sanity: the LIBERO lesson — never trust declared fps
264
+ for key in (cam0, cam1):
265
+ if key is None:
266
+ continue
267
+ f = np.asarray(episodes[f"videos/{key}/from_timestamp"])
268
+ t = np.asarray(episodes[f"videos/{key}/to_timestamp"])
269
+ measured = lengths / (t - f)
270
+ if not np.allclose(measured, fps, atol=1e-3):
271
+ raise SystemExit(
272
+ f"declared fps={fps} but {key} measures {measured.min():.3f}..{measured.max():.3f} — "
273
+ "fix the rate before building shards"
274
+ )
275
+
276
+ order = np.arange(n_ep) if args.max_episodes is None else np.arange(min(n_ep, args.max_episodes))
277
+ mine = order[args.part :: args.num_parts]
278
+ keep = np.zeros(n_ep, dtype=bool)
279
+ keep[mine] = True
280
+
281
+ print(f"repo {args.repo_id}: {n_ep} episodes, {lengths.sum():,} frames @ {fps} Hz")
282
+ print(f"cam0={cam0}\ncam1={cam1}")
283
+ print(f"stride {stride} -> {eff_hz:g} Hz, part {args.part}/{args.num_parts}: {len(mine)} episodes")
284
+
285
+ args.out.mkdir(parents=True, exist_ok=True)
286
+ (args.out / "shards").mkdir(exist_ok=True)
287
+
288
+ print("collecting state/action ...")
289
+ ep_i, fr_i, state, action, task_i, tasks = load_frame_table(
290
+ args.repo_id, args.revision, files, episodes, keep, stride, args.cache_dir
291
+ )
292
+ print(f" {len(ep_i):,} frames kept, state {state.shape[1]}d, action {action.shape[1]}d, {len(tasks)} tasks")
293
+
294
+ readers = {
295
+ "cam0": VideoReader(args.repo_id, args.revision, cam0, args.cache_dir, not args.keep_videos),
296
+ "cam1": None if cam1 is None else VideoReader(args.repo_id, args.revision, cam1, args.cache_dir, not args.keep_videos),
297
+ }
298
+
299
+ schema = pa.schema([
300
+ ("episode_index", pa.int32()), ("frame_index", pa.int32()),
301
+ ("cam0", pa.binary()), ("cam1", pa.binary()),
302
+ ])
303
+ buf = {k: [] for k in ["episode_index", "frame_index", "cam0", "cam1"]}
304
+ shard_id, written, in_shard = args.part * 100000, 0, 0
305
+
306
+ writer = None
307
+
308
+ def flush(final=False):
309
+ nonlocal writer, shard_id, buf, written, in_shard
310
+ if buf["episode_index"]:
311
+ if writer is None:
312
+ path = args.out / "shards" / f"shard-{shard_id:05d}.parquet"
313
+ writer = pq.ParquetWriter(path, schema, compression="zstd", compression_level=1)
314
+ writer.write_table(pa.table(buf, schema=schema))
315
+ written += len(buf["episode_index"])
316
+ in_shard += len(buf["episode_index"])
317
+ buf = {k: [] for k in buf}
318
+ if writer is not None and (final or in_shard >= args.rows_per_shard):
319
+ writer.close()
320
+ writer = None
321
+ shard_id += 1
322
+ in_shard = 0
323
+
324
+ import time
325
+
326
+ t0 = time.time()
327
+ for n, r in enumerate(mine):
328
+ e = int(episodes["episode_index"][r].as_py())
329
+ L = int(lengths[r])
330
+ frames = {}
331
+ for slot, key in (("cam0", cam0), ("cam1", cam1)):
332
+ if key is None:
333
+ continue
334
+ rd = readers[slot]
335
+ ci = int(episodes[f"videos/{key}/chunk_index"][r].as_py())
336
+ fi = int(episodes[f"videos/{key}/file_index"][r].as_py())
337
+ ts = float(episodes[f"videos/{key}/from_timestamp"][r].as_py())
338
+ frames[slot] = {
339
+ i // stride: encode_jpeg(img, args.image_size, args.jpeg_quality)
340
+ for i, img in rd.read_episode(ci, fi, ts, L, fps, stride)
341
+ }
342
+ n_kept = len(range(0, L, stride))
343
+ blank = b""
344
+ for j in range(n_kept):
345
+ buf["episode_index"].append(e)
346
+ buf["frame_index"].append(j)
347
+ buf["cam0"].append(frames["cam0"][j])
348
+ buf["cam1"].append(frames.get("cam1", {}).get(j, blank))
349
+ if len(buf["episode_index"]) >= 2048:
350
+ flush()
351
+ if (n + 1) % 50 == 0:
352
+ el = time.time() - t0
353
+ print(f" {n+1}/{len(mine)} episodes, {written:,} frames, "
354
+ f"{written/el:.0f} fr/s, eta {(len(mine)-n-1)*el/(n+1)/60:.1f} min", flush=True)
355
+ flush(final=True)
356
+ for rd in readers.values():
357
+ if rd is not None:
358
+ rd.close()
359
+
360
+ # frames.parquet holds only the kept rows, in shard order
361
+ pq.write_table(
362
+ pa.table({
363
+ "episode_index": pa.array(ep_i, pa.int32()),
364
+ "frame_index": pa.array(fr_i, pa.int32()),
365
+ "state": pa.array(list(state), pa.list_(pa.float32(), state.shape[1])),
366
+ "action": pa.array(list(action), pa.list_(pa.float32(), action.shape[1])),
367
+ "task_index": pa.array(task_i, pa.int32()),
368
+ }),
369
+ args.out / f"frames-part{args.part:02d}.parquet",
370
+ compression="zstd",
371
+ )
372
+ (args.out / f"tasks-part{args.part:02d}.json").write_text(
373
+ json.dumps({v: k for k, v in tasks.items()}, ensure_ascii=False)
374
+ )
375
+ (args.out / f"stats-part{args.part:02d}.json").write_text(json.dumps({
376
+ "observation.state": {"mean": state.mean(0).tolist(), "std": state.std(0).tolist()},
377
+ "action": {"mean": action.mean(0).tolist(), "std": action.std(0).tolist()},
378
+ "count": int(len(ep_i)),
379
+ }))
380
+ (args.out / f"manifest-part{args.part:02d}.json").write_text(json.dumps({
381
+ "repo_id": args.repo_id, "revision": args.revision,
382
+ "source_fps": fps, "stride": stride, "fps": eff_hz,
383
+ "image_size": args.image_size, "jpeg_quality": args.jpeg_quality,
384
+ "cam0": cam0, "cam1": cam1,
385
+ "state_dim": int(state.shape[1]), "action_dim": int(action.shape[1]),
386
+ "episodes": int(len(mine)), "frames": int(written),
387
+ }, indent=2))
388
+ print(f"done: {written:,} frames in {(time.time()-t0)/60:.1f} min -> {args.out}")
389
+
390
+
391
+ if __name__ == "__main__":
392
+ main()
tinyvla_b200/scripts/build_shards_v2.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Shard builder for LeRobot v2.0/v2.1 repos (one parquet + one mp4 PER EPISODE),
3
+ including nested repos like nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim
4
+ where each subdataset lives under <prefix>/.
5
+
6
+ v2 layout (paths come from meta/info.json templates):
7
+ data_path : data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet
8
+ video_path: videos/chunk-{episode_chunk:03d}/{video_key}/episode_{episode_index:06d}.mp4
9
+
10
+ No seeking at all: every episode is its own mp4, decoded start to finish.
11
+ Output layout matches build_shards.py, so ShardSource reads it unchanged.
12
+
13
+ Usage:
14
+ python build_shards_v2.py \
15
+ --repo-id nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim \
16
+ --prefix gr1_arms_waist.CanToDrawer \
17
+ --out /data/shards/CanToDrawer --hz 10 \
18
+ --max-episodes 100 --workers 16
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import io
25
+ import json
26
+ import os
27
+ import time
28
+ from concurrent.futures import ProcessPoolExecutor
29
+ from pathlib import Path
30
+
31
+ import numpy as np
32
+ import pyarrow as pa
33
+ import pyarrow.parquet as pq
34
+
35
+ # module-level so ProcessPoolExecutor workers inherit them post-fork
36
+ ARGS = None
37
+ INFO = None
38
+ CAMS = None
39
+
40
+
41
+ def hf_get(repo_id, path, revision=None):
42
+ from huggingface_hub import hf_hub_download
43
+
44
+ return hf_hub_download(repo_id, path, repo_type="dataset", revision=revision)
45
+
46
+
47
+ def pick_cameras(features: dict, cam0, cam1, cam_keys_re=None):
48
+ import re
49
+
50
+ PRIMARY_RE = re.compile(r"ego_view|top|front|base|main|scene|image_0$|images\.image$|image2$")
51
+ WRIST_RE = re.compile(r"wrist|gripper|hand|image_1$")
52
+ keys = sorted(k for k, v in features.items() if v.get("dtype") == "video")
53
+ if cam0 is None:
54
+ cam0 = next((k for k in keys if PRIMARY_RE.search(k)), keys[0])
55
+ if cam1 is None:
56
+ cam1 = next((k for k in keys if k != cam0 and WRIST_RE.search(k)), None)
57
+ if cam1 is None and len(keys) > 1:
58
+ cam1 = next(k for k in keys if k != cam0)
59
+ return cam0, cam1
60
+
61
+
62
+ def encode_jpeg(arr: np.ndarray, size: int, quality: int) -> bytes:
63
+ from PIL import Image
64
+
65
+ img = Image.fromarray(arr)
66
+ if img.size != (size, size):
67
+ img = img.resize((size, size), Image.BILINEAR)
68
+ buf = io.BytesIO()
69
+ img.save(buf, format="JPEG", quality=quality, subsampling=0)
70
+ return buf.getvalue()
71
+
72
+
73
+ def process_episode(job):
74
+ """One episode -> (rows for the shard, state/action arrays, task string).
75
+
76
+ Runs in a worker process. Downloads are hf-cache-deduplicated; the decode is
77
+ a plain forward pass over a per-episode mp4 (~200-2000 frames).
78
+ """
79
+ ep_idx, ep_chunk, task = job
80
+ repo, rev, prefix = ARGS.repo_id, ARGS.revision, ARGS.prefix
81
+ pfx = f"{prefix}/" if prefix else ""
82
+ stride = STRIDE
83
+
84
+ dp = INFO["data_path"].format(episode_chunk=ep_chunk, episode_index=ep_idx)
85
+ t = pq.read_table(hf_get(repo, pfx + dp, rev))
86
+ cols = {c: t[c].to_numpy(zero_copy_only=False) for c in t.column_names}
87
+ n = t.num_rows
88
+ keep = np.arange(0, n, stride)
89
+
90
+ state = np.stack([np.asarray(x, np.float32) for x in cols["observation.state"][keep]])
91
+ action = np.stack([np.asarray(x, np.float32) for x in cols["action"][keep]])
92
+
93
+ import av
94
+
95
+ jpgs = {}
96
+ for slot, key in (("cam0", CAMS[0]), ("cam1", CAMS[1])):
97
+ if key is None:
98
+ jpgs[slot] = None
99
+ continue
100
+ vp = INFO["video_path"].format(episode_chunk=ep_chunk, video_key=key, episode_index=ep_idx)
101
+ path = hf_get(repo, pfx + vp, rev)
102
+ out = []
103
+ with av.open(path) as c:
104
+ st = c.streams.video[0]
105
+ st.thread_type = "AUTO"
106
+ i = 0
107
+ for fr in c.decode(st):
108
+ if i % stride == 0:
109
+ out.append(encode_jpeg(fr.to_rgb().to_ndarray(), ARGS.image_size, ARGS.jpeg_quality))
110
+ i += 1
111
+ if ARGS.evict:
112
+ real = os.path.realpath(path)
113
+ for p in {path, real}:
114
+ try:
115
+ os.remove(p)
116
+ except OSError:
117
+ pass
118
+ # A LONGER video is benign (recorders often append tail frames after the
119
+ # last logged step) — truncate to the parquet row count. A SHORTER one
120
+ # means missing observations: hard-fail beyond 1 frame of slack.
121
+ if len(out) + 1 < len(keep):
122
+ raise RuntimeError(f"ep {ep_idx} {key}: {len(out)} decoded vs {len(keep)} parquet rows")
123
+ jpgs[slot] = out[: len(keep)]
124
+
125
+ m = min(len(keep), *(len(v) for v in jpgs.values() if v is not None))
126
+ return ep_idx, jpgs["cam0"][:m], (jpgs["cam1"][:m] if jpgs["cam1"] else None), state[:m], action[:m], task
127
+
128
+
129
+ def main():
130
+ global ARGS, INFO, CAMS, STRIDE
131
+ ap = argparse.ArgumentParser()
132
+ ap.add_argument("--repo-id", required=True)
133
+ ap.add_argument("--prefix", default=None, help="subdataset dir inside the repo (nested repos)")
134
+ ap.add_argument("--revision", default=None)
135
+ ap.add_argument("--out", type=Path, required=True)
136
+ ap.add_argument("--hz", type=float, default=10.0)
137
+ ap.add_argument("--image-size", type=int, default=256)
138
+ ap.add_argument("--jpeg-quality", type=int, default=92)
139
+ ap.add_argument("--rows-per-shard", type=int, default=5000)
140
+ ap.add_argument("--cam0", default=None)
141
+ ap.add_argument("--cam1", default=None)
142
+ ap.add_argument("--max-episodes", type=int, default=None)
143
+ ap.add_argument("--workers", type=int, default=16)
144
+ ap.add_argument("--evict", action="store_true", help="delete each mp4 after decoding")
145
+ args = ap.parse_args()
146
+ ARGS = args
147
+
148
+ pfx = f"{args.prefix}/" if args.prefix else ""
149
+ INFO = json.loads(Path(hf_get(args.repo_id, pfx + "meta/info.json", args.revision)).read_text())
150
+ fps = float(INFO["fps"])
151
+ STRIDE = max(1, int(round(fps / args.hz))) if args.hz > 0 else 1
152
+ CAMS = pick_cameras(INFO["features"], args.cam0, args.cam1)
153
+
154
+ # episodes + tasks: jsonl in v2
155
+ episodes = []
156
+ with open(hf_get(args.repo_id, pfx + "meta/episodes.jsonl", args.revision)) as f:
157
+ for line in f:
158
+ episodes.append(json.loads(line))
159
+ if args.max_episodes:
160
+ episodes = episodes[: args.max_episodes]
161
+ chunk_size = INFO.get("chunks_size", 1000)
162
+
163
+ jobs = []
164
+ for e in episodes:
165
+ i = e["episode_index"]
166
+ task = e.get("tasks", [""])
167
+ task = task[0] if isinstance(task, list) and task else str(task)
168
+ jobs.append((i, i // chunk_size, task))
169
+
170
+ sd = INFO["features"]["observation.state"]["shape"][0]
171
+ ad = INFO["features"]["action"]["shape"][0]
172
+ print(f"{args.repo_id}{'/' + args.prefix if args.prefix else ''}: {len(jobs)} эпизодов, "
173
+ f"fps={fps} stride={STRIDE} -> {fps/STRIDE:g} Hz | state={sd} action={ad} | cam0={CAMS[0]} cam1={CAMS[1]}")
174
+
175
+ args.out.mkdir(parents=True, exist_ok=True)
176
+ (args.out / "shards").mkdir(exist_ok=True)
177
+ schema = pa.schema([("episode_index", pa.int32()), ("frame_index", pa.int32()),
178
+ ("cam0", pa.binary()), ("cam1", pa.binary())])
179
+ buf = {k: [] for k in ("episode_index", "frame_index", "cam0", "cam1")}
180
+ shard_id, in_shard, written = 0, 0, 0
181
+ writer = None
182
+
183
+ def flush(final=False):
184
+ nonlocal writer, shard_id, buf, in_shard, written
185
+ if buf["episode_index"]:
186
+ if writer is None:
187
+ writer = pq.ParquetWriter(args.out / "shards" / f"shard-{shard_id:05d}.parquet",
188
+ schema, compression="zstd", compression_level=1)
189
+ writer.write_table(pa.table(buf, schema=schema))
190
+ written += len(buf["episode_index"])
191
+ in_shard += len(buf["episode_index"])
192
+ buf = {k: [] for k in buf}
193
+ if writer is not None and (final or in_shard >= args.rows_per_shard):
194
+ writer.close()
195
+ writer, in_shard = None, 0
196
+ shard_id += 1
197
+
198
+ all_ep, all_fr, all_state, all_action, all_ti = [], [], [], [], []
199
+ tasks: dict[str, int] = {}
200
+ t0 = time.time()
201
+ done = 0
202
+ with ProcessPoolExecutor(args.workers) as ex:
203
+ for ep_idx, c0, c1, state, action, task in ex.map(process_episode, jobs, chunksize=1):
204
+ m = len(c0)
205
+ for j in range(m):
206
+ buf["episode_index"].append(ep_idx)
207
+ buf["frame_index"].append(j)
208
+ buf["cam0"].append(c0[j])
209
+ buf["cam1"].append(c1[j] if c1 else b"")
210
+ if len(buf["episode_index"]) >= 2048:
211
+ flush()
212
+ all_ep.append(np.full(m, ep_idx, np.int32))
213
+ all_fr.append(np.arange(m, dtype=np.int32))
214
+ all_state.append(state[:m])
215
+ all_action.append(action[:m])
216
+ all_ti.append(np.full(m, tasks.setdefault(task, len(tasks)), np.int32))
217
+ done += 1
218
+ if done % 20 == 0:
219
+ el = time.time() - t0
220
+ print(f" {done}/{len(jobs)} eps, {written:,} fr, {written/el:.0f} fr/s, "
221
+ f"eta {(len(jobs)-done)*el/done/60:.1f} min", flush=True)
222
+ flush(final=True)
223
+
224
+ state = np.concatenate(all_state); action = np.concatenate(all_action)
225
+ ep_i = np.concatenate(all_ep); fr_i = np.concatenate(all_fr); ti = np.concatenate(all_ti)
226
+ pq.write_table(pa.table({
227
+ "episode_index": pa.array(ep_i, pa.int32()), "frame_index": pa.array(fr_i, pa.int32()),
228
+ "state": pa.array(list(state), pa.list_(pa.float32(), state.shape[1])),
229
+ "action": pa.array(list(action), pa.list_(pa.float32(), action.shape[1])),
230
+ "task_index": pa.array(ti, pa.int32())}), args.out / "frames-part00.parquet", compression="zstd")
231
+ (args.out / "tasks-part00.json").write_text(json.dumps({v: k for k, v in tasks.items()}, ensure_ascii=False))
232
+ (args.out / "stats-part00.json").write_text(json.dumps({
233
+ "observation.state": {"mean": state.mean(0).tolist(), "std": state.std(0).tolist()},
234
+ "action": {"mean": action.mean(0).tolist(), "std": action.std(0).tolist()},
235
+ "count": int(len(ep_i))}))
236
+ (args.out / "manifest-part00.json").write_text(json.dumps({
237
+ "repo_id": args.repo_id, "prefix": args.prefix, "source_fps": fps, "stride": STRIDE,
238
+ "fps": fps / STRIDE, "image_size": args.image_size, "jpeg_quality": args.jpeg_quality,
239
+ "cam0": CAMS[0], "cam1": CAMS[1], "state_dim": int(state.shape[1]),
240
+ "action_dim": int(action.shape[1]), "episodes": len(jobs), "frames": int(written)}, indent=2))
241
+ el = time.time() - t0
242
+ print(f"done: {written:,} кадров за {el/60:.1f} мин = {written/el:.0f} fr/s -> {args.out}")
243
+
244
+
245
+ if __name__ == "__main__":
246
+ main()
tinyvla_b200/scripts/make_stream_specs.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Probe the nvidia/physical-ai collection and emit a streaming specs YAML.
3
+
4
+ Walks the robotics repos, finds every LeRobot v2.x (sub)dataset that
5
+ HubEpisodeStream can consume, assigns embodiment ids BY ROBOT FAMILY (not per
6
+ dataset — 127 ids would defeat the embedding) and weights proportional to
7
+ sqrt(frames) (plain proportional lets the two biggest GR1 tasks dominate the
8
+ mixture; sqrt keeps diversity without starving the big sources).
9
+
10
+ v3.0 repos (BridgeData2_LeRobot_v3, LIBERO_LeRobot_v3) are NOT emitted — they
11
+ need the v3 shard path (build_shards.py); both were already in the C-scaled
12
+ pretraining mixture anyway.
13
+
14
+ Usage:
15
+ python make_stream_specs.py --out configs/physical_ai_stream.yaml
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import math
23
+ import os
24
+ import urllib.request
25
+ from concurrent.futures import ThreadPoolExecutor
26
+
27
+ FAMILIES = [ # ordered: first match wins; ids continue after C-scaled's 0..9
28
+ ("gr1", 10), ("bimanual_panda_gripper", 11), ("bimanual_panda_hand", 12),
29
+ ("single_panda_gripper", 13), ("sim_behavior_r1_pro", 14), ("unitree_g1", 15),
30
+ ("g1", 15),
31
+ ]
32
+
33
+ ROOTS = [
34
+ ("nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim", None), # nested
35
+ ("nvidia/PhysicalAI-Robotics-GR00T-Teleop-Sim", "LeRobot"), # nested under LeRobot/
36
+ ("nvidia/PhysicalAI-GR00T-Tuned-Tasks", None), # nested
37
+ ("nvidia/PhysicalAI-Robotics-GR00T-Teleop-G1", None), # nested
38
+ ("nvidia/GR00T-N1.7-AppleToPlate", ""), # root-level v2.1
39
+ ]
40
+
41
+
42
+ def hf_json(url: str, token: str | None):
43
+ req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"} if token else {})
44
+ try:
45
+ return json.load(urllib.request.urlopen(req, timeout=60))
46
+ except Exception:
47
+ return None
48
+
49
+
50
+ def main():
51
+ ap = argparse.ArgumentParser()
52
+ ap.add_argument("--out", default="configs/physical_ai_stream.yaml")
53
+ ap.add_argument("--hz", type=float, default=10.0)
54
+ args = ap.parse_args()
55
+ token = None
56
+ tp = os.path.expanduser("~/.cache/huggingface/token")
57
+ if os.path.exists(tp):
58
+ token = open(tp).read().strip()
59
+
60
+ jobs = [] # (repo_id, prefix)
61
+ for repo, sub in ROOTS:
62
+ if sub == "": # root-level dataset
63
+ jobs.append((repo, ""))
64
+ continue
65
+ base = f"https://huggingface.co/api/datasets/{repo}/tree/main" + (f"/{sub}" if sub else "")
66
+ tree = hf_json(base, token) or []
67
+ for e in tree:
68
+ if e.get("type") == "directory":
69
+ jobs.append((repo, e["path"]))
70
+
71
+ def probe(job):
72
+ repo, pfx = job
73
+ p = f"{pfx}/" if pfx else ""
74
+ info = hf_json(f"https://huggingface.co/datasets/{repo}/resolve/main/{p}meta/info.json", token)
75
+ if not info or "total_frames" not in info:
76
+ return None
77
+ ver = str(info.get("codebase_version", ""))
78
+ if not ver.startswith("v2"):
79
+ return None # v3 needs the shard path, v1 untested
80
+ return dict(
81
+ repo_id=repo, prefix=pfx, frames=info["total_frames"], fps=info["fps"],
82
+ state_dim=info["features"]["observation.state"]["shape"][0],
83
+ action_dim=info["features"]["action"]["shape"][0],
84
+ ncam=sum(1 for v in info["features"].values() if v.get("dtype") == "video"),
85
+ )
86
+
87
+ with ThreadPoolExecutor(16) as ex:
88
+ found = [r for r in ex.map(probe, jobs) if r]
89
+
90
+ max_state = max(r["state_dim"] for r in found)
91
+ max_action = max(r["action_dim"] for r in found)
92
+ total = sum(r["frames"] for r in found)
93
+ lines = [
94
+ "# Autogenerated by make_stream_specs.py — streaming mixture over the",
95
+ "# LeRobot-v2.x part of the nvidia/physical-ai collection.",
96
+ f"# {len(found)} datasets, {total:,} source frames.",
97
+ f"# max_state_dim needed: {max_state}; max_action_dim needed: {max_action}.",
98
+ "datasets:",
99
+ ]
100
+ for r in sorted(found, key=lambda x: (-x["frames"])):
101
+ name = r["prefix"] or r["repo_id"].split("/")[-1]
102
+ emb = next((i for k, i in FAMILIES if name.startswith(k) or f"/{k}" in name.lower() or k in name.lower()), 15)
103
+ w = math.sqrt(r["frames"])
104
+ lines += [
105
+ f" - repo_id: {r['repo_id']}",
106
+ f" prefix: \"{r['prefix']}\"",
107
+ f" weight: {w:.1f} # sqrt({r['frames']:,} fr); fps={r['fps']} state={r['state_dim']} act={r['action_dim']} cams={r['ncam']}",
108
+ f" embodiment_id: {emb}",
109
+ f" hz: {args.hz}",
110
+ ]
111
+ os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
112
+ with open(args.out, "w") as f:
113
+ f.write("\n".join(lines) + "\n")
114
+ print(f"{len(found)} датасетов, {total:,} кадров -> {args.out}")
115
+ print(f"max_state_dim={max_state} max_action_dim={max_action}")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
tinyvla_b200/scripts/pack_wds.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Pack one shard-builder output dir into self-contained webdataset tars.
3
+
4
+ Each wds sample carries EVERYTHING training needs — no sidecar lookups in the
5
+ loader, which is what makes the mixture loader trivial and robust:
6
+
7
+ __key__ "{dsname}_{ep:06d}_{fr:05d}"
8
+ cam0.jpg primary camera JPEG (as produced by build_shards*)
9
+ cam1.jpg wrist camera JPEG, or b"" for single-camera datasets
10
+ meta.npz state float32[S] normalized (dataset mean/std)
11
+ action_chunk float32[50,A] normalized FUTURE actions from this
12
+ step; episode tail padded by repeating the last row
13
+ action_is_pad bool[50]
14
+ action_dim / state_dim / task_index / embodiment_id int32
15
+
16
+ Normalization happens HERE, at pack time, with the per-dataset stats the shard
17
+ builder wrote — the loader never touches stats. The stats used are copied into
18
+ the output manifest so unnormalization at eval time uses the same numbers.
19
+
20
+ JPEG bytes are copied verbatim (no re-encode); tars are uncompressed because
21
+ the payload already is.
22
+
23
+ Usage:
24
+ python pack_wds.py --src /workspace/data/CanToDrawer \
25
+ --out /workspace/data/wds/CanToDrawer --chunk 50 --maxcount 3000
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import io
32
+ import json
33
+ from pathlib import Path
34
+
35
+ import numpy as np
36
+ import pyarrow.parquet as pq
37
+
38
+
39
+ def main():
40
+ ap = argparse.ArgumentParser()
41
+ ap.add_argument("--src", type=Path, required=True, help="shard-builder output dir")
42
+ ap.add_argument("--out", type=Path, required=True)
43
+ ap.add_argument("--name", default=None, help="dataset name for keys (default: src dirname)")
44
+ ap.add_argument("--chunk", type=int, default=50)
45
+ ap.add_argument("--maxcount", type=int, default=3000)
46
+ ap.add_argument("--embodiment-id", type=int, default=0)
47
+ args = ap.parse_args()
48
+ name = args.name or args.src.name
49
+
50
+ import webdataset as wds
51
+
52
+ manifest = json.loads(next(args.src.glob("manifest-part*.json")).read_text())
53
+ stats = json.loads(next(args.src.glob("stats-part*.json")).read_text())
54
+ s_mean = np.asarray(stats["observation.state"]["mean"], np.float32)
55
+ s_std = np.clip(np.asarray(stats["observation.state"]["std"], np.float32), 1e-6, None)
56
+ a_mean = np.asarray(stats["action"]["mean"], np.float32)
57
+ a_std = np.clip(np.asarray(stats["action"]["std"], np.float32), 1e-6, None)
58
+
59
+ # frames table -> per-episode contiguous state/action, RAM-resident
60
+ tabs = [pq.read_table(p) for p in sorted(args.src.glob("frames-part*.parquet"))]
61
+ import pyarrow as pa
62
+
63
+ t = pa.concat_tables(tabs) if len(tabs) > 1 else tabs[0]
64
+ ep = np.asarray(t["episode_index"], np.int64)
65
+ fr = np.asarray(t["frame_index"], np.int64)
66
+ order = np.lexsort((fr, ep))
67
+ state = np.asarray(t["state"].to_numpy(zero_copy_only=False).tolist(), np.float32)[order]
68
+ action = np.asarray(t["action"].to_numpy(zero_copy_only=False).tolist(), np.float32)[order]
69
+ task_i = np.asarray(t["task_index"], np.int32)[order]
70
+ ep, fr = ep[order], fr[order]
71
+ uniq, starts = np.unique(ep, return_index=True)
72
+ ep_start = dict(zip(uniq.tolist(), starts.tolist()))
73
+ ep_len = dict(zip(uniq.tolist(), np.diff(np.append(starts, len(ep))).tolist()))
74
+
75
+ state = (state - s_mean) / s_std
76
+ action = (action - a_mean) / a_std
77
+
78
+ S, A, CH = state.shape[1], action.shape[1], args.chunk
79
+ args.out.mkdir(parents=True, exist_ok=True)
80
+ sink = wds.ShardWriter(str(args.out / "shard-%05d.tar"), maxcount=args.maxcount, verbose=0)
81
+ n = 0
82
+ for shard in sorted((args.src / "shards").glob("shard-*.parquet")):
83
+ pf = pq.ParquetFile(shard)
84
+ for batch in pf.iter_batches(batch_size=512):
85
+ d = batch.to_pydict()
86
+ for e, f, c0, c1 in zip(d["episode_index"], d["frame_index"], d["cam0"], d["cam1"]):
87
+ e, f = int(e), int(f)
88
+ base, ln = ep_start[e], ep_len[e]
89
+ i = base + f
90
+ avail = min(CH, ln - f)
91
+ chunk = np.empty((CH, A), np.float32)
92
+ chunk[:avail] = action[i : i + avail]
93
+ if avail < CH:
94
+ chunk[avail:] = action[i + avail - 1]
95
+ is_pad = np.zeros(CH, bool)
96
+ is_pad[avail:] = True
97
+ buf = io.BytesIO()
98
+ np.savez(buf, state=state[i], action_chunk=chunk, action_is_pad=is_pad,
99
+ action_dim=np.int32(A), state_dim=np.int32(S),
100
+ task_index=task_i[i], embodiment_id=np.int32(args.embodiment_id))
101
+ sink.write({"__key__": f"{name}_{e:06d}_{f:05d}",
102
+ "cam0.jpg": c0, "cam1.jpg": bytes(c1 or b""),
103
+ "meta.npz": buf.getvalue()})
104
+ n += 1
105
+ sink.close()
106
+
107
+ tasks = {}
108
+ for p in sorted(args.src.glob("tasks-part*.json")):
109
+ tasks.update(json.loads(p.read_text()))
110
+ (args.out / "tasks.json").write_text(json.dumps(tasks, ensure_ascii=False))
111
+ (args.out / "manifest.json").write_text(json.dumps({
112
+ **manifest, "name": name, "samples": n, "chunk": CH,
113
+ "embodiment_id": args.embodiment_id,
114
+ "norm_stats": stats, # exactly what was applied, for unnormalization
115
+ "shards": sorted(p.name for p in args.out.glob("shard-*.tar")),
116
+ }, indent=2))
117
+ print(f"{name}: {n:,} семплов -> {len(list(args.out.glob('shard-*.tar')))} тарболов в {args.out}")
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
tinyvla_b200/scripts/train_fast.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Training loop for shard-streamed data, tuned for a single H100.
3
+
4
+ Differences from scripts/train.py (all of them are speed or correctness, none
5
+ change the objective):
6
+
7
+ data ShardSource (sequential parquet + JPEG) instead of LeRobotDataset
8
+ random access into h264. This is the change that matters.
9
+ attention expert routed through SDPA (see patches/modules_expert.py.diff)
10
+ vision cam0 goes through the Qwen tower ONCE per step, not twice
11
+ (patches/modeling_tinyvla.py.diff)
12
+ tokens task strings pre-tokenized once into a lookup, not per step
13
+ optimizer fused AdamW, foreach off, set_to_none
14
+ precision bf16 autocast + TF32 matmuls, grad_accum 1 at a large batch
15
+ compile torch.compile on the expert (many small ops; biggest compile win)
16
+ resume shape-aware load: tensors whose shape changed (state/action
17
+ projections when the action dim changes) are re-initialised and
18
+ REPORTED, instead of raising or silently loading garbage
19
+
20
+ Usage:
21
+ python train_fast.py --config configs/trex_ft.yaml
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import math
29
+ import time
30
+ from pathlib import Path
31
+
32
+ import torch
33
+ import yaml
34
+
35
+
36
+ # --------------------------------------------------------------------- policy
37
+
38
+
39
+ def make_policy(cfg: dict, state_dim: int, action_dim: int):
40
+ from lerobot.configs import FeatureType, PolicyFeature
41
+ from tinyvla.configuration_tinyvla import TinyVLAConfig
42
+ from tinyvla.modeling_tinyvla import TinyVLAPolicy
43
+
44
+ pcfg = TinyVLAConfig(**cfg.get("policy", {}))
45
+ if action_dim > pcfg.max_action_dim or state_dim > pcfg.max_state_dim:
46
+ raise SystemExit(
47
+ f"dataset has state {state_dim}d / action {action_dim}d but the config caps them at "
48
+ f"{pcfg.max_state_dim} / {pcfg.max_action_dim}. Raise max_state_dim/max_action_dim — "
49
+ f"CanonicalSource._pad would TRUNCATE silently."
50
+ )
51
+ s = pcfg.image_size
52
+ pcfg.input_features = {
53
+ "observation.images.cam0": PolicyFeature(type=FeatureType.VISUAL, shape=(3, s, s)),
54
+ "observation.images.cam1": PolicyFeature(type=FeatureType.VISUAL, shape=(3, s, s)),
55
+ "observation.state": PolicyFeature(type=FeatureType.STATE, shape=(pcfg.max_state_dim,)),
56
+ }
57
+ pcfg.output_features = {"action": PolicyFeature(type=FeatureType.ACTION, shape=(action_dim,))}
58
+ pcfg.validate_features()
59
+ return TinyVLAPolicy(pcfg), pcfg
60
+
61
+
62
+ def load_compatible(model, path: Path):
63
+ """Load a checkpoint, keeping only tensors whose shape still matches.
64
+
65
+ Continuing C-scaled on a 58-DoF bimanual robot changes the action/state
66
+ projections. torch's strict=False does NOT tolerate a shape change (it
67
+ raises), so filter explicitly and say out loud what was dropped — a silently
68
+ re-initialised action head is the difference between "fine-tuning" and
69
+ "training a new head on a frozen trunk".
70
+ """
71
+ from safetensors.torch import load_file
72
+
73
+ sd = load_file(path / "model.safetensors")
74
+ own = model.state_dict()
75
+ keep, reshaped, unexpected = {}, [], []
76
+ for k, v in sd.items():
77
+ if k not in own:
78
+ unexpected.append(k)
79
+ elif own[k].shape != v.shape:
80
+ reshaped.append((k, tuple(v.shape), tuple(own[k].shape)))
81
+ else:
82
+ keep[k] = v
83
+ missing = [k for k in own if k not in keep]
84
+ model.load_state_dict(keep, strict=False)
85
+ print(f"resume {path}: loaded {len(keep)}/{len(own)} tensors")
86
+ for k, a, b in reshaped:
87
+ print(f" RE-INIT (shape changed) {k}: {a} -> {b}")
88
+ if unexpected:
89
+ print(f" ignored {len(unexpected)} unexpected keys, e.g. {unexpected[:3]}")
90
+ left = [k for k in missing if all(k != r[0] for r in reshaped)]
91
+ if left:
92
+ print(f" {len(left)} tensors kept at init, e.g. {left[:3]}")
93
+ return len(keep)
94
+
95
+
96
+ # ----------------------------------------------------------------------- main
97
+
98
+
99
+ def main():
100
+ ap = argparse.ArgumentParser()
101
+ ap.add_argument("--config", type=Path, required=True)
102
+ ap.add_argument("--profile-steps", type=int, default=0,
103
+ help="run N steps, print throughput, exit (use to replace the estimate with a measurement)")
104
+ args = ap.parse_args()
105
+ cfg = yaml.safe_load(args.config.read_text())
106
+
107
+ torch.backends.cuda.matmul.allow_tf32 = True
108
+ torch.backends.cudnn.allow_tf32 = True
109
+ torch.backends.cudnn.benchmark = True
110
+
111
+ dev = torch.device("cuda")
112
+
113
+ src_cfg = cfg["source"]
114
+ morph = None
115
+ if cfg.get("morphology_descriptors") and cfg["policy"].get("conditioning") == "morph":
116
+ from tinyvla.modules.embodiment import MORPH_FIELDS
117
+
118
+ raw = yaml.safe_load(Path(cfg["morphology_descriptors"]).read_text())
119
+ sc = {"arm_dof": 0.1, "reach_m": 2, "gripper_width_m": 10, "num_cameras": 1 / 3,
120
+ "control_hz": 1 / 30, "joint_lo_mean": 1 / 3.1416, "joint_hi_mean": 1 / 3.1416,
121
+ "workspace_x": 2, "workspace_y": 2, "workspace_z": 2, "payload_kg": 0.2}
122
+ d = raw[src_cfg["morph_key"]]
123
+ morph = torch.tensor([d.get(f, 0) * sc.get(f, 1) for f in MORPH_FIELDS], dtype=torch.float32)
124
+
125
+ chunk = cfg["policy"]["chunk_size"]
126
+ if src_cfg.get("type") == "hub":
127
+ # stream episodes straight from HF hub (LeRobot v2.x), no local data at all
128
+ import os as _os
129
+
130
+ from tinyvla.data.streaming_hub import HubEpisodeStream
131
+
132
+ specs = yaml.safe_load(Path(src_cfg["specs"]).read_text())["datasets"]
133
+ tok_path = _os.path.expanduser("~/.cache/huggingface/token")
134
+ source = HubEpisodeStream(
135
+ specs,
136
+ token=open(tok_path).read().strip() if _os.path.exists(tok_path) else None,
137
+ chunk=chunk,
138
+ image_size=cfg["policy"].get("image_size", 256),
139
+ max_state_dim=cfg["policy"]["max_state_dim"],
140
+ max_action_dim=cfg["policy"]["max_action_dim"],
141
+ shuffle_buffer=cfg.get("shuffle_buffer", 4096),
142
+ seed=cfg.get("seed", 42),
143
+ )
144
+ print(f"source: HUB STREAM, {len(specs)} datasets (infinite mixture)")
145
+ else:
146
+ from tinyvla.data.shards import ShardSource
147
+
148
+ source = ShardSource(
149
+ root=src_cfg["root"],
150
+ embodiment_id=src_cfg.get("embodiment_id", 0),
151
+ chunk=chunk,
152
+ image_size=cfg["policy"].get("image_size", 256),
153
+ max_state_dim=cfg["policy"]["max_state_dim"],
154
+ max_action_dim=cfg["policy"]["max_action_dim"],
155
+ morphology=morph,
156
+ robot_prompt=src_cfg.get("robot_prompt"),
157
+ shuffle_buffer=cfg.get("shuffle_buffer", 8192),
158
+ seed=cfg.get("seed", 42),
159
+ )
160
+ print(f"source: {source.num_frames:,} frames @ {source.fps} Hz, "
161
+ f"state {source.state_dim}d action {source.action_dim}d, {len(source.shards)} shards")
162
+
163
+ if src_cfg.get("type") == "hub":
164
+ # per-dataset dims vary; the source pads everything to the config caps
165
+ policy, pcfg = make_policy(cfg, cfg["policy"]["max_state_dim"], cfg["policy"]["max_action_dim"])
166
+ else:
167
+ policy, pcfg = make_policy(cfg, source.state_dim, source.action_dim)
168
+ policy = policy.to(dev)
169
+
170
+ if cfg.get("resume_from"):
171
+ load_compatible(policy, Path(cfg["resume_from"]))
172
+
173
+ # ---- pre-tokenize every distinct instruction once -----------------------
174
+ from transformers import AutoTokenizer
175
+
176
+ tokenizer = AutoTokenizer.from_pretrained(pcfg.lm_model_name)
177
+ if src_cfg.get("type") == "hub":
178
+ _tok_cache: dict = {}
179
+
180
+ def _tok(tasks):
181
+ new = [t for t in set(tasks) if t not in _tok_cache]
182
+ if new:
183
+ e = tokenizer(new, padding="max_length", truncation=True,
184
+ max_length=pcfg.tokenizer_max_length, return_tensors="pt")
185
+ for i, t in enumerate(new):
186
+ _tok_cache[t] = (e["input_ids"][i], e["attention_mask"][i].bool())
187
+ return (torch.stack([_tok_cache[t][0] for t in tasks]),
188
+ torch.stack([_tok_cache[t][1] for t in tasks]))
189
+ else:
190
+ texts = sorted(set(source._tasks.values()))
191
+ enc = tokenizer(texts, padding="max_length", truncation=True,
192
+ max_length=pcfg.tokenizer_max_length, return_tensors="pt")
193
+ tok_ids, tok_mask = enc["input_ids"], enc["attention_mask"].bool()
194
+ tok_lookup = {t: i for i, t in enumerate(texts)}
195
+ print(f"pre-tokenized {len(texts)} instructions at fixed length {pcfg.tokenizer_max_length}")
196
+
197
+ def _tok(tasks):
198
+ idx = torch.tensor([tok_lookup.get(t, 0) for t in tasks])
199
+ return tok_ids[idx], tok_mask[idx]
200
+
201
+ def collate(items):
202
+ out = {}
203
+ for k in items[0]:
204
+ if k == "task":
205
+ ids, mask = _tok([it["task"] for it in items])
206
+ out["observation.language.tokens"] = ids
207
+ out["observation.language.attention_mask"] = mask
208
+ else:
209
+ out[k] = torch.stack([it[k] for it in items])
210
+ return out
211
+
212
+ loader = torch.utils.data.DataLoader(
213
+ source,
214
+ batch_size=cfg["batch_size"],
215
+ num_workers=cfg.get("num_workers", 12),
216
+ pin_memory=True,
217
+ persistent_workers=cfg.get("num_workers", 12) > 0,
218
+ prefetch_factor=cfg.get("prefetch_factor", 6) if cfg.get("num_workers", 12) > 0 else None,
219
+ drop_last=True,
220
+ collate_fn=collate,
221
+ )
222
+
223
+ # ---- optimizer: backbone at a lower lr, exactly as train.py does --------
224
+ backbone = [p for n, p in policy.named_parameters() if p.requires_grad and "semantic.vlm" in n]
225
+ head = [p for n, p in policy.named_parameters() if p.requires_grad and "semantic.vlm" not in n]
226
+ groups = [{"params": head, "lr": cfg["lr"]}]
227
+ if backbone:
228
+ groups.append({"params": backbone, "lr": cfg["lr"] * cfg.get("backbone_lr_mult", 0.1)})
229
+ opt = torch.optim.AdamW(groups, betas=(0.9, 0.95), weight_decay=1e-10, fused=True)
230
+ print(f"trainable: head {sum(p.numel() for p in head)/1e6:.1f}M, "
231
+ f"backbone {sum(p.numel() for p in backbone)/1e6:.1f}M at {cfg.get('backbone_lr_mult',0.1)}x lr")
232
+
233
+ steps, warmup = cfg["steps"], cfg.get("warmup_steps", 1000)
234
+
235
+ def lr_lambda(s):
236
+ if s < warmup:
237
+ return s / max(1, warmup)
238
+ p = (s - warmup) / max(1, steps - warmup)
239
+ return 0.025 + 0.975 * 0.5 * (1 + math.cos(math.pi * p))
240
+
241
+ sched = torch.optim.lr_scheduler.LambdaLR(opt, lr_lambda)
242
+
243
+ if cfg.get("compile", True):
244
+ policy.expert = torch.compile(policy.expert, dynamic=False)
245
+ print("torch.compile: expert")
246
+
247
+ out_dir = Path(cfg["output_dir"])
248
+ out_dir.mkdir(parents=True, exist_ok=True)
249
+ if hasattr(source, "manifest"):
250
+ (out_dir / "source_manifest.json").write_text(json.dumps(source.manifest, indent=2))
251
+ if cfg.get("wandb"):
252
+ import wandb
253
+
254
+ wandb.init(project=cfg["wandb"], config=cfg)
255
+
256
+ grad_accum = cfg.get("grad_accum", 1)
257
+ eff_batch = cfg["batch_size"] * grad_accum
258
+ log_freq = cfg.get("log_freq", 50)
259
+ target = args.profile_steps or steps
260
+
261
+ step, seen, t0 = 0, 0, time.time()
262
+ data_iter = iter(loader)
263
+ while step < target:
264
+ opt.zero_grad(set_to_none=True)
265
+ for _ in range(grad_accum):
266
+ try:
267
+ batch = next(data_iter)
268
+ except StopIteration:
269
+ source.set_epoch(source.epoch + 1)
270
+ data_iter = iter(loader)
271
+ batch = next(data_iter)
272
+ batch = {k: (v.to(dev, non_blocking=True) if torch.is_tensor(v) else v)
273
+ for k, v in batch.items()}
274
+ with torch.autocast("cuda", dtype=torch.bfloat16):
275
+ loss, info = policy(batch)
276
+ (loss / grad_accum).backward()
277
+ seen += cfg["batch_size"]
278
+ torch.nn.utils.clip_grad_norm_(policy.parameters(), cfg.get("grad_clip", 10.0))
279
+ opt.step()
280
+ sched.step()
281
+ step += 1
282
+
283
+ if step % log_freq == 0:
284
+ torch.cuda.synchronize()
285
+ el = time.time() - t0
286
+ sps = seen / el
287
+ gflops = cfg.get("gflops_per_sample", 360.0)
288
+ mfu = sps * gflops * 1e9 / (cfg.get("peak_flops", 989e12)) * 100
289
+ eta = (steps - step) * eff_batch / sps / 3600
290
+ print(f"step {step}/{steps} loss {info['loss']:.4f} | {sps:.0f} samples/s "
291
+ f"| {step/el:.2f} it/s | MFU~{mfu:.0f}% | eta {eta:.2f} h "
292
+ f"| mem {torch.cuda.max_memory_allocated()/1e9:.1f} GB", flush=True)
293
+ if cfg.get("wandb"):
294
+ wandb.log({"loss": info["loss"], "lr": sched.get_last_lr()[0],
295
+ "samples_per_s": sps}, step=step)
296
+ seen, t0 = 0, time.time()
297
+
298
+ if args.profile_steps == 0 and step % cfg.get("save_freq", 10000) == 0:
299
+ policy.save_pretrained(out_dir / f"step_{step}")
300
+
301
+ if args.profile_steps:
302
+ print(f"\nprofile done. Multiply the samples/s above by {steps * eff_batch:,} "
303
+ f"total samples to get the real wall clock.")
304
+ return
305
+ policy.save_pretrained(out_dir / "final")
306
+
307
+
308
+ if __name__ == "__main__":
309
+ main()
tinyvla_b200/src/tinyvla/data/shards.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streaming source over prebuilt JPEG shards (see scripts/build_shards.py).
2
+
3
+ Emits exactly the schema CanonicalSource emits, so it drops into the existing
4
+ WeightedMixtureDataset / train loop unchanged:
5
+
6
+ observation.images.cam0 (3, S, S) float [0,1]
7
+ observation.images.cam1 (3, S, S) float [0,1] (zeros if the source has one camera)
8
+ camera_mask (2,) bool
9
+ observation.state (max_state_dim,) normalized, zero-padded
10
+ action (chunk, max_action_dim) normalized, zero-padded
11
+ action_dim_mask (max_action_dim,) bool
12
+ action_is_pad (chunk,) bool
13
+ embodiment_id () long
14
+ task str
15
+ morphology (16,) float (variant C)
16
+
17
+ Difference from CanonicalSource: this is an IterableDataset doing SEQUENTIAL
18
+ reads with a shuffle buffer, instead of random row lookups that each trigger an
19
+ h264 seek. That is the whole speedup — the model side is untouched.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import io
25
+ import json
26
+ import random
27
+ from pathlib import Path
28
+
29
+ import numpy as np
30
+ import torch
31
+ import torch.nn.functional as F
32
+ from torch.utils.data import IterableDataset, get_worker_info
33
+
34
+
35
+ def _pad(x: torch.Tensor, dim: int) -> torch.Tensor:
36
+ """Zero-pad the last dim. Unlike CanonicalSource._pad this REFUSES to
37
+ truncate: silently cutting a 58-dim bimanual action down to 8 is exactly the
38
+ kind of convention bug that costs weeks."""
39
+ d = x.shape[-1]
40
+ if d == dim:
41
+ return x
42
+ if d > dim:
43
+ raise ValueError(
44
+ f"tensor has {d} dims but max is {dim} — raise max_action_dim/max_state_dim "
45
+ f"in the policy config instead of truncating"
46
+ )
47
+ return F.pad(x, (0, dim - d))
48
+
49
+
50
+ class ShardSource(IterableDataset):
51
+ def __init__(
52
+ self,
53
+ root: str | Path,
54
+ embodiment_id: int,
55
+ chunk: int = 50,
56
+ image_size: int = 256,
57
+ max_state_dim: int = 64,
58
+ max_action_dim: int = 64,
59
+ morphology: torch.Tensor | None = None,
60
+ robot_prompt: str | None = None,
61
+ shuffle_buffer: int = 8192,
62
+ seed: int = 42,
63
+ normalize: bool = True,
64
+ rank: int = 0,
65
+ world_size: int = 1,
66
+ ):
67
+ super().__init__()
68
+ self.root = Path(root)
69
+ self.embodiment_id = embodiment_id
70
+ self.chunk = chunk
71
+ self.image_size = image_size
72
+ self.max_state_dim = max_state_dim
73
+ self.max_action_dim = max_action_dim
74
+ self.morphology = morphology
75
+ self.robot_prompt = robot_prompt
76
+ self.shuffle_buffer = shuffle_buffer
77
+ self.seed = seed
78
+ self.normalize = normalize
79
+ self.rank, self.world_size = rank, world_size
80
+ self.epoch = 0
81
+
82
+ self.shards = sorted(str(p) for p in (self.root / "shards").glob("shard-*.parquet"))
83
+ if not self.shards:
84
+ raise FileNotFoundError(f"no shards under {self.root/'shards'}")
85
+
86
+ parts = sorted(self.root.glob("manifest-part*.json"))
87
+ self.manifest = json.loads(parts[0].read_text())
88
+ self.fps = self.manifest["fps"]
89
+ self.action_dim = self.manifest["action_dim"]
90
+ self.state_dim = self.manifest["state_dim"]
91
+
92
+ # per-episode state/action, resident in RAM: 58 floats x 2 x ~1.8M frames
93
+ # is well under 1 GB, and it removes every random lookup from the hot path
94
+ import pyarrow.parquet as pq
95
+
96
+ tabs = [pq.read_table(p) for p in sorted(self.root.glob("frames-part*.parquet"))]
97
+ import pyarrow as pa
98
+
99
+ t = pa.concat_tables(tabs) if len(tabs) > 1 else tabs[0]
100
+ ep = np.asarray(t["episode_index"]).astype(np.int64)
101
+ fr = np.asarray(t["frame_index"]).astype(np.int64)
102
+ order = np.lexsort((fr, ep))
103
+ self._state = np.asarray(t["state"].to_numpy(zero_copy_only=False).tolist(), dtype=np.float32)[order]
104
+ self._action = np.asarray(t["action"].to_numpy(zero_copy_only=False).tolist(), dtype=np.float32)[order]
105
+ self._task_index = np.asarray(t["task_index"]).astype(np.int64)[order]
106
+ ep, fr = ep[order], fr[order]
107
+ uniq, starts = np.unique(ep, return_index=True)
108
+ self._ep_start = dict(zip(uniq.tolist(), starts.tolist()))
109
+ self._ep_len = dict(zip(uniq.tolist(), np.diff(np.append(starts, len(ep))).tolist()))
110
+
111
+ tasks = {}
112
+ for p in sorted(self.root.glob("tasks-part*.json")):
113
+ tasks.update({int(k): v for k, v in json.loads(p.read_text()).items()})
114
+ self._tasks = tasks
115
+
116
+ stats = {}
117
+ for p in sorted(self.root.glob("stats-part*.json")):
118
+ s = json.loads(p.read_text())
119
+ for k in ("observation.state", "action"):
120
+ stats.setdefault(k, []).append((np.array(s[k]["mean"]), np.array(s[k]["std"]), s["count"]))
121
+ self.stats = {}
122
+ for k, entries in stats.items(): # pooled mean/std across parts
123
+ n = sum(e[2] for e in entries)
124
+ mean = sum(e[0] * e[2] for e in entries) / n
125
+ var = sum((e[1] ** 2 + (e[0] - mean) ** 2) * e[2] for e in entries) / n
126
+ self.stats[k] = (
127
+ torch.tensor(mean, dtype=torch.float32),
128
+ torch.tensor(np.sqrt(var), dtype=torch.float32).clamp(min=1e-6),
129
+ )
130
+
131
+ self.num_frames = int(len(ep))
132
+
133
+ def set_epoch(self, epoch: int):
134
+ self.epoch = epoch
135
+
136
+ # ------------------------------------------------------------------ helpers
137
+
138
+ def _decode(self, blob: bytes) -> torch.Tensor:
139
+ from PIL import Image
140
+
141
+ img = Image.open(io.BytesIO(blob))
142
+ img = img.convert("RGB")
143
+ x = torch.from_numpy(np.asarray(img, dtype=np.uint8).copy())
144
+ x = x.permute(2, 0, 1).float().div_(255.0)
145
+ if x.shape[-2:] != (self.image_size, self.image_size):
146
+ x = F.interpolate(x[None], size=(self.image_size, self.image_size),
147
+ mode="bilinear", align_corners=False)[0]
148
+ return x
149
+
150
+ def _sample(self, e: int, f: int, cam0: bytes, cam1: bytes) -> dict:
151
+ out = {"observation.images.cam0": self._decode(cam0)}
152
+ if cam1:
153
+ out["observation.images.cam1"] = self._decode(cam1)
154
+ cam_mask = torch.tensor([True, True])
155
+ else:
156
+ out["observation.images.cam1"] = torch.zeros_like(out["observation.images.cam0"])
157
+ cam_mask = torch.tensor([True, False])
158
+ out["camera_mask"] = cam_mask
159
+
160
+ base, n = self._ep_start[e], self._ep_len[e]
161
+ i = base + f
162
+ state = torch.from_numpy(self._state[i].copy())
163
+ avail = min(self.chunk, n - f)
164
+ act = torch.from_numpy(self._action[i : i + avail].copy())
165
+ is_pad = torch.zeros(self.chunk, dtype=torch.bool)
166
+ if avail < self.chunk:
167
+ # hold the last action, mark the tail as padding — the loss masks it
168
+ act = torch.cat([act, act[-1:].expand(self.chunk - avail, -1)], dim=0)
169
+ is_pad[avail:] = True
170
+
171
+ if self.normalize:
172
+ m, s = self.stats["observation.state"]
173
+ state = (state - m) / s
174
+ m, s = self.stats["action"]
175
+ act = (act - m) / s
176
+
177
+ out["observation.state"] = _pad(state, self.max_state_dim)
178
+ out["action"] = _pad(act, self.max_action_dim)
179
+ mask = torch.zeros(self.max_action_dim, dtype=torch.bool)
180
+ mask[: self.action_dim] = True
181
+ out["action_dim_mask"] = mask
182
+ out["action_is_pad"] = is_pad
183
+ out["embodiment_id"] = torch.tensor(self.embodiment_id, dtype=torch.long)
184
+ task = self._tasks.get(int(self._task_index[i]), "")
185
+ out["task"] = f"{self.robot_prompt} {task}" if self.robot_prompt else task
186
+ if self.morphology is not None:
187
+ out["morphology"] = self.morphology
188
+ return out
189
+
190
+ # -------------------------------------------------------------------- iter
191
+
192
+ def _my_shards(self):
193
+ wi = get_worker_info()
194
+ wid, nw = (wi.id, wi.num_workers) if wi is not None else (0, 1)
195
+ gid = self.rank * nw + wid
196
+ gn = self.world_size * nw
197
+ shards = list(self.shards)
198
+ random.Random(self.seed + self.epoch).shuffle(shards)
199
+ return shards[gid::gn], gid
200
+
201
+ def __iter__(self):
202
+ import pyarrow.parquet as pq
203
+
204
+ shards, gid = self._my_shards()
205
+ rng = random.Random(self.seed * 1000 + self.epoch * 97 + gid)
206
+ buf: list[dict] = []
207
+ for path in shards:
208
+ pf = pq.ParquetFile(path)
209
+ for batch in pf.iter_batches(batch_size=512):
210
+ d = batch.to_pydict()
211
+ for e, f, c0, c1 in zip(d["episode_index"], d["frame_index"], d["cam0"], d["cam1"]):
212
+ if e not in self._ep_start:
213
+ continue # frame table and shards built from different parts
214
+ item = self._sample(int(e), int(f), c0, c1)
215
+ if len(buf) < self.shuffle_buffer:
216
+ buf.append(item)
217
+ continue
218
+ j = rng.randrange(len(buf))
219
+ buf[j], item = item, buf[j]
220
+ yield item
221
+ rng.shuffle(buf)
222
+ yield from buf
tinyvla_b200/src/tinyvla/data/streaming_hub.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stream training samples straight from HF hub LeRobot v2.x repos. No disk.
2
+
3
+ Why this works: v2.0/v2.1 stores ONE parquet + ONE mp4 per camera PER EPISODE
4
+ (2-10 MB each). A DataLoader worker samples a (dataset, episode) pair by weight,
5
+ HTTP-GETs the files into memory, decodes the episode SEQUENTIALLY (the cheap
6
+ direction — no seeks at all), yields every stride-th frame as a full training
7
+ sample, then moves to the next episode. A downstream shuffle buffer breaks the
8
+ intra-episode correlation. Nothing ever touches disk, so the 1.3 TB
9
+ materialization step disappears entirely.
10
+
11
+ Bandwidth math (nvidia/PhysicalAI-Robotics-GR00T-X-Embodiment-Sim): source h264
12
+ is ~10 KB/frame over all cams; at 400 samples/s consumption and stride 2-3 the
13
+ stream needs ~10-25 MB/s and ~4 HTTP requests/s — trivial for the HF CDN.
14
+
15
+ Emits the exact CanonicalSource/ShardSource schema, so it drops into the
16
+ training loop unchanged.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import io
22
+ import json
23
+ import random
24
+
25
+ import numpy as np
26
+ import torch
27
+ import torch.nn.functional as F
28
+ from torch.utils.data import IterableDataset, get_worker_info
29
+
30
+
31
+ def _resolve(repo_id: str, path: str) -> str:
32
+ return f"https://huggingface.co/datasets/{repo_id}/resolve/main/{path}"
33
+
34
+
35
+ def _pad(x: torch.Tensor, dim: int, what: str) -> torch.Tensor:
36
+ d = x.shape[-1]
37
+ if d > dim:
38
+ raise ValueError(f"{what} has {d} dims > max {dim}; raise the cap, never truncate")
39
+ return F.pad(x, (0, dim - d)) if d < dim else x
40
+
41
+
42
+ class HubEpisodeStream(IterableDataset):
43
+ """Infinite weighted mixture over LeRobot v2.x datasets, streamed from HF.
44
+
45
+ spec per dataset:
46
+ repo_id HF dataset repo
47
+ prefix subdir for nested repos ("" for root-level)
48
+ weight mixture sampling weight
49
+ embodiment_id
50
+ hz target rate (stride = round(fps/hz))
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ specs: list[dict],
56
+ token: str | None = None,
57
+ chunk: int = 50,
58
+ image_size: int = 256,
59
+ max_state_dim: int = 64,
60
+ max_action_dim: int = 64,
61
+ shuffle_buffer: int = 4096,
62
+ seed: int = 42,
63
+ rank: int = 0,
64
+ world_size: int = 1,
65
+ ):
66
+ super().__init__()
67
+ self.specs = specs
68
+ self.token = token
69
+ self.chunk = chunk
70
+ self.image_size = image_size
71
+ self.max_state_dim = max_state_dim
72
+ self.max_action_dim = max_action_dim
73
+ self.shuffle_buffer = shuffle_buffer
74
+ self.seed = seed
75
+ self.rank, self.world_size = rank, world_size
76
+ self.epoch = 0
77
+ self._session = None # per-process requests.Session (created lazily post-fork)
78
+ self._meta: dict[int, dict] = {}
79
+
80
+ # ------------------------------------------------------------------- http
81
+
82
+ def _http(self):
83
+ if self._session is None:
84
+ import requests
85
+
86
+ s = requests.Session()
87
+ if self.token:
88
+ s.headers["Authorization"] = f"Bearer {self.token}"
89
+ a = requests.adapters.HTTPAdapter(pool_connections=8, pool_maxsize=8, max_retries=3)
90
+ s.mount("https://", a)
91
+ self._session = s
92
+ return self._session
93
+
94
+ def _fetch(self, repo_id: str, path: str) -> bytes:
95
+ r = self._http().get(_resolve(repo_id, path), timeout=120)
96
+ r.raise_for_status()
97
+ return r.content
98
+
99
+ # ------------------------------------------------------------------- meta
100
+
101
+ def _load_meta(self, si: int) -> dict:
102
+ """info.json + episodes list + normalization stats, once per worker."""
103
+ if si in self._meta:
104
+ return self._meta[si]
105
+ spec = self.specs[si]
106
+ rid, pfx = spec["repo_id"], spec.get("prefix", "")
107
+ p = f"{pfx}/" if pfx else ""
108
+ info = json.loads(self._fetch(rid, p + "meta/info.json"))
109
+ fps = float(info["fps"])
110
+ stride = max(1, int(round(fps / spec.get("hz", 10.0))))
111
+
112
+ episodes = []
113
+ for line in self._fetch(rid, p + "meta/episodes.jsonl").decode().splitlines():
114
+ if line.strip():
115
+ e = json.loads(line)
116
+ episodes.append((e["episode_index"], e.get("length", 0),
117
+ (e.get("tasks") or [""])[0] if isinstance(e.get("tasks"), list) else ""))
118
+
119
+ stats = self._load_stats(rid, p, info)
120
+ cam0, cam1 = self._pick_cams(info["features"])
121
+ meta = dict(info=info, fps=fps, stride=stride, episodes=episodes, stats=stats,
122
+ cams=(cam0, cam1), chunks_size=info.get("chunks_size", 1000),
123
+ state_dim=info["features"]["observation.state"]["shape"][0],
124
+ action_dim=info["features"]["action"]["shape"][0])
125
+ self._meta[si] = meta
126
+ return meta
127
+
128
+ def _load_stats(self, rid: str, p: str, info: dict) -> dict:
129
+ """v2.0: meta/stats.json. v2.1: aggregate meta/episodes_stats.jsonl."""
130
+ try:
131
+ raw = json.loads(self._fetch(rid, p + "meta/stats.json"))
132
+ return {k: (np.asarray(raw[k]["mean"], np.float32).reshape(-1),
133
+ np.asarray(raw[k]["std"], np.float32).reshape(-1))
134
+ for k in ("observation.state", "action")}
135
+ except Exception:
136
+ pass
137
+ acc = {k: [0.0, 0.0, 0] for k in ("observation.state", "action")} # sum, sumsq, n
138
+ for line in self._fetch(rid, p + "meta/episodes_stats.jsonl").decode().splitlines():
139
+ if not line.strip():
140
+ continue
141
+ st = json.loads(line)["stats"]
142
+ for k in acc:
143
+ m = np.asarray(st[k]["mean"], np.float32).reshape(-1)
144
+ s = np.asarray(st[k]["std"], np.float32).reshape(-1)
145
+ n = int(np.asarray(st[k]["count"]).reshape(-1)[0])
146
+ acc[k][0] = acc[k][0] + m * n
147
+ acc[k][1] = acc[k][1] + (s ** 2 + m ** 2) * n
148
+ acc[k][2] += n
149
+ out = {}
150
+ for k, (sm, sq, n) in acc.items():
151
+ mean = sm / n
152
+ out[k] = (mean.astype(np.float32),
153
+ np.sqrt(np.maximum(sq / n - mean ** 2, 1e-12)).astype(np.float32))
154
+ return out
155
+
156
+ @staticmethod
157
+ def _pick_cams(features: dict):
158
+ import re
159
+
160
+ PRIMARY = re.compile(r"ego_view|top|front|base|main|scene|image_0$|images\.image$|image2$")
161
+ WRIST = re.compile(r"wrist|gripper|hand|image_1$")
162
+ keys = sorted(k for k, v in features.items() if v.get("dtype") == "video")
163
+ cam0 = next((k for k in keys if PRIMARY.search(k)), keys[0])
164
+ cam1 = next((k for k in keys if k != cam0 and WRIST.search(k)), None)
165
+ if cam1 is None and len(keys) > 1:
166
+ cam1 = next(k for k in keys if k != cam0)
167
+ return cam0, cam1
168
+
169
+ # ---------------------------------------------------------------- episode
170
+
171
+ def _decode_video(self, blob: bytes, stride: int) -> list[torch.Tensor]:
172
+ import av
173
+
174
+ out = []
175
+ with av.open(io.BytesIO(blob)) as c:
176
+ st = c.streams.video[0]
177
+ st.thread_type = "AUTO"
178
+ i = 0
179
+ for fr in c.decode(st):
180
+ if i % stride == 0:
181
+ a = fr.to_rgb().to_ndarray()
182
+ x = torch.from_numpy(np.ascontiguousarray(a)).permute(2, 0, 1).float().div_(255.0)
183
+ if x.shape[-2:] != (self.image_size, self.image_size):
184
+ x = F.interpolate(x[None], size=(self.image_size, self.image_size),
185
+ mode="bilinear", align_corners=False)[0]
186
+ out.append(x)
187
+ i += 1
188
+ return out
189
+
190
+ def _stream_episode(self, si: int, ep_idx: int, task: str):
191
+ spec = self.specs[si]
192
+ meta = self._load_meta(si)
193
+ rid, pfx = spec["repo_id"], spec.get("prefix", "")
194
+ p = f"{pfx}/" if pfx else ""
195
+ info, stride = meta["info"], meta["stride"]
196
+ ch = ep_idx // meta["chunks_size"]
197
+
198
+ t = __import__("pyarrow.parquet", fromlist=["read_table"]).read_table(
199
+ io.BytesIO(self._fetch(rid, p + info["data_path"].format(
200
+ episode_chunk=ch, episode_index=ep_idx))))
201
+ keep = np.arange(0, t.num_rows, stride)
202
+ cols = {c: t[c].to_numpy(zero_copy_only=False) for c in ("observation.state", "action")}
203
+ sm, ss = meta["stats"]["observation.state"]
204
+ am, as_ = meta["stats"]["action"]
205
+ state = (np.stack([np.asarray(x, np.float32) for x in cols["observation.state"][keep]]) - sm) / np.maximum(ss, 1e-6)
206
+ action = (np.stack([np.asarray(x, np.float32) for x in cols["action"][keep]]) - am) / np.maximum(as_, 1e-6)
207
+
208
+ cams = {}
209
+ for slot, key in zip(("cam0", "cam1"), meta["cams"]):
210
+ if key is None:
211
+ cams[slot] = None
212
+ continue
213
+ blob = self._fetch(rid, p + info["video_path"].format(
214
+ episode_chunk=ch, video_key=key, episode_index=ep_idx))
215
+ frames = self._decode_video(blob, stride)
216
+ if len(frames) + 1 < len(keep): # video may run a frame or two long, never short
217
+ raise RuntimeError(f"{rid}/{pfx} ep{ep_idx} {key}: {len(frames)} frames vs {len(keep)} rows")
218
+ cams[slot] = frames[: len(keep)]
219
+
220
+ m = min(len(keep), *(len(v) for v in cams.values() if v is not None))
221
+ state_t = torch.from_numpy(state[:m])
222
+ action_t = torch.from_numpy(action[:m])
223
+ ad = meta["action_dim"]
224
+ dim_mask = torch.zeros(self.max_action_dim, dtype=torch.bool)
225
+ dim_mask[:ad] = True
226
+ cam_mask = torch.tensor([True, cams["cam1"] is not None])
227
+ zero_img = None
228
+
229
+ for j in range(m):
230
+ avail = m - j
231
+ if avail >= self.chunk:
232
+ chunk_a = action_t[j : j + self.chunk]
233
+ is_pad = torch.zeros(self.chunk, dtype=torch.bool)
234
+ else:
235
+ chunk_a = torch.cat([action_t[j:], action_t[-1:].expand(self.chunk - avail, -1)])
236
+ is_pad = torch.zeros(self.chunk, dtype=torch.bool)
237
+ is_pad[avail:] = True
238
+ if cams["cam1"] is None and zero_img is None:
239
+ zero_img = torch.zeros_like(cams["cam0"][j])
240
+ yield {
241
+ "observation.images.cam0": cams["cam0"][j],
242
+ "observation.images.cam1": cams["cam1"][j] if cams["cam1"] is not None else zero_img,
243
+ "camera_mask": cam_mask,
244
+ "observation.state": _pad(state_t[j], self.max_state_dim, "state"),
245
+ "action": _pad(chunk_a, self.max_action_dim, "action"),
246
+ "action_dim_mask": dim_mask,
247
+ "action_is_pad": is_pad,
248
+ "embodiment_id": torch.tensor(spec.get("embodiment_id", si), dtype=torch.long),
249
+ "task": task,
250
+ }
251
+
252
+ # ------------------------------------------------------------------- iter
253
+
254
+ def __iter__(self):
255
+ wi = get_worker_info()
256
+ wid, nw = (wi.id, wi.num_workers) if wi is not None else (0, 1)
257
+ gid = self.rank * nw + wid
258
+ rng = random.Random(self.seed * 7919 + self.epoch * 104729 + gid)
259
+ weights = [s["weight"] for s in self.specs]
260
+
261
+ buf: list[dict] = []
262
+ while True: # infinite stream; the training loop counts steps, not epochs
263
+ si = rng.choices(range(len(self.specs)), weights=weights, k=1)[0]
264
+ try:
265
+ meta = self._load_meta(si)
266
+ ep_idx, _, task = meta["episodes"][rng.randrange(len(meta["episodes"]))]
267
+ for item in self._stream_episode(si, ep_idx, task):
268
+ if len(buf) < self.shuffle_buffer:
269
+ buf.append(item)
270
+ continue
271
+ k = rng.randrange(len(buf))
272
+ buf[k], item = item, buf[k]
273
+ yield item
274
+ except Exception as e: # network hiccup / bad episode: log and resample
275
+ import logging
276
+
277
+ logging.warning(f"stream: skipping {self.specs[si]['repo_id']} ep after {type(e).__name__}: {e}")
tinyvla_b200/src/tinyvla/data/wds_mix.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Weighted mixture loader over self-contained webdataset tars (see pack_wds.py).
2
+
3
+ Yields the same batch schema as ShardSource + collate, so it drops into the
4
+ training loop unchanged:
5
+
6
+ observation.images.cam0 / cam1 (B, 3, S, S) float [0,1]
7
+ camera_mask (B, 2) bool
8
+ observation.state (B, max_state_dim) already normalized
9
+ action (B, chunk, max_action_dim) already normalized
10
+ action_dim_mask (B, max_action_dim) bool
11
+ action_is_pad (B, chunk) bool
12
+ embodiment_id (B,) long
13
+ task_index (B,) long -- per-dataset index; map via tasks.json
14
+
15
+ webdataset is easy to hold wrong. The choices here, and why:
16
+
17
+ * ``resampled=True`` on every per-dataset stream: each worker samples SHARDS
18
+ WITH REPLACEMENT, forever. This is the one mode where none of the classic
19
+ failure modes exist — no "fewer shards than workers" (empty_check), no
20
+ silent worker starvation when shards % workers != 0, no need for
21
+ nodesplitter/workersplitter at all (independent draws are already i.i.d.
22
+ across workers and nodes), and a weighted mixture needs an infinite stream
23
+ per source anyway because "epoch" is undefined for a mixture.
24
+ * ``wds.RandomMix(..., probs=weights)`` composes the per-dataset streams; the
25
+ mixture is sampled per-sample, so batches interleave datasets (important:
26
+ per-dataset normalization is baked into the tars, the model must see mixed
27
+ batches, not dataset-blocked ones).
28
+ * ``.shuffle(buffer)`` per stream AFTER resampling: shard-level sampling alone
29
+ leaves frames of one episode adjacent; the buffer breaks that correlation.
30
+ * ``with_epoch(steps_per_epoch)`` bounds the infinite stream so DataLoader
31
+ epoch semantics (and persistent_workers restarts) still work.
32
+ * empty_check=False everywhere: with resampling a worker can never prove a
33
+ stream empty, the check only produces false positives.
34
+
35
+ Deterministic-ish: pass seed; resampling uses it per worker.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import io
41
+ import json
42
+ from pathlib import Path
43
+
44
+ import numpy as np
45
+ import torch
46
+
47
+
48
+ def _decode_and_pad(sample, image_size, max_state_dim, max_action_dim):
49
+ from PIL import Image
50
+
51
+ meta = np.load(io.BytesIO(sample["meta.npz"]))
52
+ S = int(meta["state_dim"])
53
+ A = int(meta["action_dim"])
54
+ if S > max_state_dim or A > max_action_dim:
55
+ raise ValueError(
56
+ f"sample has state {S}d / action {A}d, caps are {max_state_dim}/{max_action_dim} — "
57
+ f"raise the caps instead of truncating"
58
+ )
59
+
60
+ def img(key):
61
+ raw = sample.get(key, b"")
62
+ if not raw:
63
+ return None
64
+ im = Image.open(io.BytesIO(raw)).convert("RGB")
65
+ x = torch.from_numpy(np.asarray(im, np.uint8).copy()).permute(2, 0, 1).float().div_(255.0)
66
+ return x
67
+
68
+ cam0 = img("cam0.jpg")
69
+ cam1 = img("cam1.jpg")
70
+ if cam1 is None:
71
+ cam1 = torch.zeros_like(cam0)
72
+ cam_mask = torch.tensor([True, False])
73
+ else:
74
+ cam_mask = torch.tensor([True, True])
75
+
76
+ state = torch.zeros(max_state_dim)
77
+ state[:S] = torch.from_numpy(meta["state"])
78
+ chunk = meta["action_chunk"]
79
+ act = torch.zeros(chunk.shape[0], max_action_dim)
80
+ act[:, :A] = torch.from_numpy(chunk)
81
+ adm = torch.zeros(max_action_dim, dtype=torch.bool)
82
+ adm[:A] = True
83
+
84
+ return {
85
+ "observation.images.cam0": cam0,
86
+ "observation.images.cam1": cam1,
87
+ "camera_mask": cam_mask,
88
+ "observation.state": state,
89
+ "action": act,
90
+ "action_dim_mask": adm,
91
+ "action_is_pad": torch.from_numpy(meta["action_is_pad"]),
92
+ "embodiment_id": torch.tensor(int(meta["embodiment_id"]), dtype=torch.long),
93
+ "task_index": torch.tensor(int(meta["task_index"]), dtype=torch.long),
94
+ }
95
+
96
+
97
+ def _stream(spec, image_size, max_state_dim, max_action_dim, shuffle_buffer, seed):
98
+ import webdataset as wds
99
+
100
+ d = Path(spec["dir"])
101
+ manifest = json.loads((d / "manifest.json").read_text())
102
+ tars = [str(d / s) for s in manifest["shards"]]
103
+ ds = wds.WebDataset(
104
+ tars,
105
+ resampled=True, # shards sampled with replacement, infinite
106
+ shardshuffle=False, # resampling already randomizes shard order
107
+ empty_check=False,
108
+ seed=seed,
109
+ )
110
+ if shuffle_buffer > 1:
111
+ ds = ds.shuffle(shuffle_buffer, initial=min(shuffle_buffer, 1000))
112
+ emb = spec.get("embodiment_id")
113
+ def mapper(sample):
114
+ out = _decode_and_pad(sample, image_size, max_state_dim, max_action_dim)
115
+ if emb is not None: # override what was packed, if the mixture says so
116
+ out["embodiment_id"] = torch.tensor(int(emb), dtype=torch.long)
117
+ return out
118
+ return ds.map(mapper), manifest
119
+
120
+
121
+ def make_mixture_loader(
122
+ specs: list[dict], # [{"dir": ..., "weight": ..., "embodiment_id": ...}]
123
+ batch_size: int,
124
+ num_workers: int = 16,
125
+ image_size: int = 256,
126
+ max_state_dim: int = 64,
127
+ max_action_dim: int = 64,
128
+ shuffle_buffer: int = 4096,
129
+ steps_per_epoch: int = 1000, # bounds the infinite stream per epoch
130
+ seed: int = 42,
131
+ prefetch_factor: int = 4,
132
+ ):
133
+ import webdataset as wds
134
+
135
+ # bound each infinite stream per worker; RandomMix (longest=False) then ends
136
+ # the epoch when the first stream is exhausted, keeping DataLoader epoch
137
+ # semantics intact. RandomMix itself takes no seed in wds 1.x — per-stream
138
+ # seeds provide the determinism.
139
+ per_worker = max(1, steps_per_epoch * batch_size // max(1, num_workers))
140
+ streams, manifests, weights = [], [], []
141
+ for i, spec in enumerate(specs):
142
+ ds, mf = _stream(spec, image_size, max_state_dim, max_action_dim,
143
+ shuffle_buffer, seed + 1000 * i)
144
+ streams.append(ds.with_epoch(per_worker))
145
+ manifests.append(mf)
146
+ weights.append(float(spec.get("weight", mf["samples"])))
147
+
148
+ if len(streams) == 1:
149
+ mix = streams[0]
150
+ else:
151
+ total = sum(weights)
152
+ mix = wds.RandomMix(streams, probs=[w / total for w in weights])
153
+
154
+ # wds re-injects __key__/__url__ after EVERY pipeline stage, so stripping
155
+ # them inside the pipeline is futile — drop them at collate instead
156
+ from torch.utils.data import default_collate
157
+
158
+ def collate(items):
159
+ return default_collate([
160
+ {k: v for k, v in it.items() if not k.startswith("__")} for it in items
161
+ ])
162
+
163
+ loader = torch.utils.data.DataLoader(
164
+ mix,
165
+ batch_size=batch_size,
166
+ num_workers=num_workers,
167
+ pin_memory=True,
168
+ persistent_workers=num_workers > 0,
169
+ prefetch_factor=prefetch_factor if num_workers > 0 else None,
170
+ drop_last=True,
171
+ collate_fn=collate,
172
+ )
173
+ return loader, manifests
tinyvla_b200/src/tinyvla/modeling_tinyvla.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TinyVLA policy: dual-rate VLA on a single shared Qwen3.5 vision encoder.
2
+
3
+ Slow path: image -> Qwen3.5 vision tower -> language stack (first 12 layers) +
4
+ instruction -> 8-token semantic latent (cached at deployment, refreshed at
5
+ 0.5-2 Hz).
6
+
7
+ Fast path: Qwen3.5 vision tower spatial tokens (64/camera, no language stack)
8
+ + proprio + embodiment token + cached semantic latent -> ~40M flow-matching
9
+ action expert -> 50-step action chunk.
10
+
11
+ During training both paths run jointly (single-rate); staleness augmentation
12
+ is added by the Stage-2 training loop.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections import deque
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+
23
+ from lerobot.configs import FeatureType
24
+ from lerobot.policies.pretrained import PreTrainedPolicy
25
+ from lerobot.utils.constants import (
26
+ ACTION,
27
+ OBS_LANGUAGE_ATTENTION_MASK,
28
+ OBS_LANGUAGE_TOKENS,
29
+ OBS_STATE,
30
+ )
31
+
32
+ from .configuration_tinyvla import TinyVLAConfig
33
+ from .modules.embodiment import EmbodimentEmbedding, pad_vector
34
+ from .modules.expert import FlowMatchingExpert
35
+ from .modules.semantic import SemanticPath
36
+
37
+
38
+ class TinyVLAPolicy(PreTrainedPolicy):
39
+ config_class = TinyVLAConfig
40
+ name = "tinyvla"
41
+
42
+ def __init__(self, config: TinyVLAConfig, dataset_stats=None, dataset_meta=None, **kwargs):
43
+ super().__init__(config)
44
+ self.config = config
45
+
46
+ self.image_keys = sorted(
47
+ k for k, v in config.input_features.items() if v.type is FeatureType.VISUAL
48
+ )
49
+ state_ft = config.input_features.get(OBS_STATE)
50
+ self.state_dim = state_ft.shape[0] if state_ft is not None else 0
51
+ self.action_dim = config.output_features[ACTION].shape[0]
52
+
53
+ d = config.expert_dim
54
+
55
+ self.semantic = SemanticPath(
56
+ model_name=config.lm_model_name,
57
+ num_layers=config.lm_num_layers,
58
+ num_readout=config.num_readout_tokens,
59
+ out_dim=d,
60
+ image_size=config.image_size,
61
+ freeze_lm=config.freeze_lm,
62
+ freeze_vision=config.freeze_vision_encoder,
63
+ morph_to_slow=config.morph_to_slow,
64
+ morph_dim=16,
65
+ morph_qwen=(config.conditioning == "morph_qwen"),
66
+ num_morph_readout=config.num_morph_readout,
67
+ vlm_native=config.vlm_native,
68
+ num_action_readout=config.num_action_readout,
69
+ max_state_dim=config.max_state_dim,
70
+ max_action_dim=config.max_action_dim,
71
+ )
72
+
73
+ # fast-path conditioning projections
74
+ self.spatial_proj = nn.Linear(self.semantic.visual_hidden_size, d)
75
+ self.camera_emb = nn.Embedding(config.max_cameras, d)
76
+ self.state_proj = nn.Linear(config.max_state_dim, d)
77
+ self.embodiment_emb = EmbodimentEmbedding(config.num_embodiments, d)
78
+ if config.conditioning == "morph":
79
+ from .modules.embodiment import MorphologyEncoder
80
+
81
+ self.morph_encoder = MorphologyEncoder(d, n_tokens=config.morph_tokens)
82
+ if config.use_demo_conditioning:
83
+ from .modules.embodiment import DemoConditioner
84
+
85
+ self.demo_encoder = DemoConditioner(
86
+ d,
87
+ visual_dim=self.semantic.visual_hidden_size,
88
+ action_dim=config.max_action_dim,
89
+ n_tokens_per_example=config.demo_tokens_per_example,
90
+ n_visual_groups=config.demo_visual_groups,
91
+ n_action_keys=config.demo_action_keys,
92
+ hidden_mult=config.demo_hidden_mult,
93
+ )
94
+
95
+ self.spatial_head = (
96
+ nn.Linear(d, config.spatial_vocab) if config.spatial_loss_weight > 0 else None
97
+ )
98
+
99
+ self.expert = FlowMatchingExpert(
100
+ action_dim=config.max_action_dim,
101
+ chunk_size=config.chunk_size,
102
+ d_model=d,
103
+ n_layers=config.expert_layers,
104
+ n_heads=config.expert_heads,
105
+ min_period=config.min_period,
106
+ max_period=config.max_period,
107
+ )
108
+
109
+ self._action_queue: deque = deque()
110
+
111
+ def reset(self):
112
+ self._action_queue.clear()
113
+
114
+ def get_optim_params(self):
115
+ return [p for p in self.parameters() if p.requires_grad]
116
+
117
+ # ---------------------------------------------------------------- features
118
+
119
+ @staticmethod
120
+ def _last_obs(x: torch.Tensor, ndim: int) -> torch.Tensor:
121
+ """Drop the n_obs_steps dim if present: (B, S, ...) -> (B, ...)."""
122
+ return x[:, -1] if x.dim() == ndim + 1 else x
123
+
124
+ def _encode_spatial(self, batch: dict, feats: dict | None = None) -> torch.Tensor:
125
+ """Per-camera spatial tokens for the fast path: (B, num_cams*64, d).
126
+
127
+ `feats` lets the caller pass vision-tower outputs computed once per step
128
+ (see `_vision_features`) instead of re-running the tower here.
129
+ """
130
+ cond = []
131
+ for i, key in enumerate(self.image_keys):
132
+ tokens = (feats or {}).get(key)
133
+ if tokens is None:
134
+ tokens = self.semantic.encode_image(self._last_obs(batch[key], 4)) # (B, 64, C)
135
+ proj = self.spatial_proj(tokens) + self.camera_emb.weight[i][None, None, :]
136
+ if i == 0:
137
+ self._cam0_proj = proj # consumed by the spatial-distillation head
138
+ cond.append(proj)
139
+ return torch.cat(cond, dim=1)
140
+
141
+ def _vision_features(self, batch: dict, keys) -> dict:
142
+ """One vision-tower pass per camera, shared by the fast and slow paths."""
143
+ return {k: self.semantic.encode_image(self._last_obs(batch[k], 4)) for k in keys}
144
+
145
+ def _semantic_latent(self, batch: dict, image=None, image_embeds=None) -> torch.Tensor:
146
+ """Slow-path latent: (B, K, d), cacheable, conditions the fast path.
147
+
148
+ vlm_native=True makes this latent RICH — the LM sequence also carries the
149
+ robot description and the demo (image, action) exemplars, so the readout
150
+ tokens can attend over them. The fast path is unchanged: it still gets its
151
+ own spatial tokens + proprio + expert, just a better-informed latent.
152
+ """
153
+ img = image if image is not None else self._last_obs(batch[self.image_keys[0]], 4)
154
+ if self.config.vlm_native:
155
+ state = None
156
+ if self.state_dim:
157
+ state = pad_vector(self._last_obs(batch[OBS_STATE], 2), self.config.max_state_dim)
158
+ return self.semantic.forward_unified(
159
+ image=img,
160
+ lang_tokens=batch[OBS_LANGUAGE_TOKENS],
161
+ lang_mask=batch[OBS_LANGUAGE_ATTENTION_MASK],
162
+ state=state,
163
+ # demo_only: no robot description — demos are the sole identity cue
164
+ robot_ids=None if self.config.demo_only else batch.get("morph_text_ids"),
165
+ robot_mask=None if self.config.demo_only else batch.get("morph_text_mask"),
166
+ demo_images=batch.get("support_images"),
167
+ demo_actions=batch.get("support_actions"),
168
+ n_demo_action_keys=self.config.demo_action_keys_lm,
169
+ )
170
+ morph = batch.get("morphology") if self.config.morph_to_slow else None
171
+ return self.semantic(
172
+ img,
173
+ batch[OBS_LANGUAGE_TOKENS],
174
+ batch[OBS_LANGUAGE_ATTENTION_MASK],
175
+ morphology=morph,
176
+ image_embeds=image_embeds,
177
+ )
178
+
179
+ def _conditioning(self, batch: dict) -> torch.Tensor:
180
+ b = batch[self.image_keys[0]].shape[0]
181
+ device = batch[self.image_keys[0]].device
182
+
183
+ # Run the shared Qwen vision tower ONCE per camera per step. Previously
184
+ # cam0 went through it twice: here for the spatial tokens, and again
185
+ # inside the LM call in `_semantic_latent`. The tower is ~45 GFLOPs per
186
+ # camera, i.e. a quarter of the forward pass thrown away every step.
187
+ feats = None
188
+ if self.config.use_spatial_tokens:
189
+ feats = self._vision_features(batch, self.image_keys)
190
+ elif self.config.use_semantic_latent and not self.config.vlm_native:
191
+ feats = self._vision_features(batch, self.image_keys[:1])
192
+
193
+ parts = []
194
+ if self.config.use_semantic_latent:
195
+ latent = batch.get("semantic_latent")
196
+ if latent is None:
197
+ if "latent_image" in batch: # staleness aug: latent from an older frame,
198
+ latent = self._semantic_latent(batch, image=batch["latent_image"])
199
+ else: # ...so the cached cam0 features do NOT apply
200
+ cam0 = None if (feats is None or self.config.vlm_native) else feats[self.image_keys[0]]
201
+ latent = self._semantic_latent(batch, image_embeds=cam0)
202
+ parts.append(latent)
203
+ if self.config.use_spatial_tokens:
204
+ parts.append(self._encode_spatial(batch, feats))
205
+ if self.state_dim:
206
+ state = pad_vector(self._last_obs(batch[OBS_STATE], 2), self.config.max_state_dim)
207
+ parts.append(self.state_proj(state)[:, None, :])
208
+ if self.config.demo_only:
209
+ pass # no identity token at all — demos in the LM latent are the only cue
210
+ elif self.config.conditioning == "morph":
211
+ desc = batch.get("morphology")
212
+ if desc is None:
213
+ from .modules.embodiment import MORPH_DIM
214
+
215
+ desc = torch.zeros(b, MORPH_DIM, device=device)
216
+ parts.append(self.morph_encoder(desc.float()))
217
+ elif self.config.conditioning == "morph_qwen":
218
+ ids = batch.get("morph_text_ids")
219
+ mask = batch.get("morph_text_mask")
220
+ if ids is None:
221
+ ids = torch.zeros(b, 1, dtype=torch.long, device=device)
222
+ mask = torch.ones(b, 1, dtype=torch.bool, device=device)
223
+ parts.append(self.semantic.encode_robot_text(ids, mask))
224
+ else:
225
+ emb_id = batch.get("embodiment_id")
226
+ if emb_id is None:
227
+ emb_id = torch.zeros(b, dtype=torch.long, device=device)
228
+ parts.append(self.embodiment_emb(emb_id))
229
+
230
+ if self.config.use_demo_conditioning:
231
+ sup_imgs = batch.get("support_images") # (B, K, 3, H, W)
232
+ sup_acts = batch.get("support_actions") # (B, K, T, A)
233
+ if sup_imgs is not None and sup_acts is not None:
234
+ bb, k = sup_imgs.shape[:2]
235
+ flat_imgs = sup_imgs.reshape(bb * k, *sup_imgs.shape[2:])
236
+ spatial = self.semantic.encode_image(flat_imgs) # (B*K, 64, visual_dim)
237
+ # keep the spatial structure — the encoder does grouped pooling
238
+ spatial = spatial.view(bb, k, spatial.shape[1], spatial.shape[2])
239
+ parts.append(self.demo_encoder(spatial, sup_acts))
240
+ return torch.cat(parts, dim=1)
241
+
242
+ # ---------------------------------------------------------------- training
243
+
244
+ def forward(self, batch: dict) -> tuple[torch.Tensor, dict]:
245
+ cond = self._conditioning(batch)
246
+
247
+ actions = pad_vector(batch[ACTION], self.config.max_action_dim) # (B, T, A)
248
+ b, t, a = actions.shape
249
+ device = actions.device
250
+
251
+ beta = torch.distributions.Beta(self.config.flow_beta_alpha, self.config.flow_beta_beta)
252
+ time = beta.sample((b,)).to(device) * 0.999 + 0.001
253
+ noise = torch.randn_like(actions)
254
+ x_t = time[:, None, None] * noise + (1 - time[:, None, None]) * actions
255
+ target = noise - actions
256
+
257
+ pred = self.expert(x_t, time, cond)
258
+
259
+ loss_mask = torch.ones(b, t, a, device=device)
260
+ if "action_dim_mask" in batch: # per-sample real dims (mixture training)
261
+ loss_mask = loss_mask * batch["action_dim_mask"][:, None, :].float()
262
+ else:
263
+ loss_mask[:, :, self.action_dim :] = 0 # padded action dims
264
+ if "action_is_pad" in batch: # episode-end padding from delta_timestamps
265
+ loss_mask = loss_mask * (~batch["action_is_pad"])[:, :, None].float()
266
+
267
+ loss = (F.mse_loss(pred, target, reduction="none") * loss_mask).sum() / loss_mask.sum()
268
+ info = {"flow_loss": loss.item()}
269
+
270
+ if (
271
+ self.spatial_head is not None
272
+ and "spatial_cell" in batch
273
+ and self.config.use_spatial_tokens
274
+ ):
275
+ cells = batch["spatial_cell"]
276
+ labeled = cells >= 0
277
+ if labeled.any():
278
+ logits = self.spatial_head(self._cam0_proj.mean(dim=1)) # (B, vocab)
279
+ ce = F.cross_entropy(logits[labeled], cells[labeled])
280
+ loss = loss + self.config.spatial_loss_weight * ce
281
+ info["spatial_ce"] = ce.item()
282
+
283
+ info["loss"] = loss.item()
284
+ return loss, info
285
+
286
+ # --------------------------------------------------------------- inference
287
+
288
+ @torch.no_grad()
289
+ def predict_action_chunk(self, batch: dict) -> torch.Tensor:
290
+ cond = self._conditioning(batch)
291
+ b = cond.shape[0]
292
+ device = cond.device
293
+
294
+ x = torch.randn(b, self.config.chunk_size, self.config.max_action_dim, device=device)
295
+ n = self.config.num_denoise_steps
296
+ dt = 1.0 / n
297
+ for i in range(n):
298
+ t = torch.full((b,), 1.0 - i * dt, device=device)
299
+ v = self.expert(x, t, cond)
300
+ x = x - dt * v
301
+ return x[:, :, : self.action_dim]
302
+
303
+ @torch.no_grad()
304
+ def select_action(self, batch: dict) -> torch.Tensor:
305
+ if len(self._action_queue) == 0:
306
+ chunk = self.predict_action_chunk(batch) # (B, T, A)
307
+ self._action_queue.extend(chunk.transpose(0, 1)[: self.config.n_action_steps])
308
+ return self._action_queue.popleft()
tinyvla_b200/src/tinyvla/modules/expert.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flow-matching action expert.
2
+
3
+ A small transformer over noisy action tokens with interleaved
4
+ cross-attention (to the conditioning set) and self-attention blocks,
5
+ following the SmolVLA expert pattern. Predicts the flow velocity.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+ import torch.nn.functional as F
15
+
16
+
17
+ def sincos_time_embedding(t: torch.Tensor, dim: int, min_period: float = 4e-3, max_period: float = 4.0) -> torch.Tensor:
18
+ """t: (B,) in [0, 1] -> (B, dim) sine-cosine embedding."""
19
+ half = dim // 2
20
+ freqs = torch.exp(
21
+ torch.linspace(math.log(min_period), math.log(max_period), half, device=t.device)
22
+ )
23
+ angles = 2 * math.pi * t[:, None] / freqs[None, :]
24
+ return torch.cat([angles.sin(), angles.cos()], dim=-1)
25
+
26
+
27
+ class _MHA(nn.Module):
28
+ """Drop-in replacement for torch's batch-first multi-head attention, routed
29
+ through F.scaled_dot_product_attention (flash / mem-efficient kernels, and
30
+ far friendlier to torch.compile than the eager fallback).
31
+
32
+ Parameter names are kept identical (in_proj_weight / in_proj_bias /
33
+ out_proj.weight / out_proj.bias) so existing checkpoints load unchanged.
34
+ `key_padding_mask` keeps torch semantics: True = ignore that key.
35
+ """
36
+
37
+ def __init__(self, dim: int, n_heads: int):
38
+ super().__init__()
39
+ assert dim % n_heads == 0
40
+ self.dim, self.n_heads, self.head_dim = dim, n_heads, dim // n_heads
41
+ self.in_proj_weight = nn.Parameter(torch.empty(3 * dim, dim))
42
+ self.in_proj_bias = nn.Parameter(torch.zeros(3 * dim))
43
+ self.out_proj = nn.Linear(dim, dim)
44
+ nn.init.xavier_uniform_(self.in_proj_weight)
45
+ nn.init.zeros_(self.out_proj.bias)
46
+
47
+ def forward(self, query, key, value, key_padding_mask=None, need_weights=False):
48
+ b, lq, d = query.shape
49
+ w_q, w_k, w_v = self.in_proj_weight.split(d, dim=0)
50
+ b_q, b_k, b_v = self.in_proj_bias.split(d, dim=0)
51
+ q = F.linear(query, w_q, b_q)
52
+ k = F.linear(key, w_k, b_k)
53
+ v = F.linear(value, w_v, b_v)
54
+
55
+ def split(t):
56
+ return t.view(b, -1, self.n_heads, self.head_dim).transpose(1, 2)
57
+
58
+ q, k, v = split(q), split(k), split(v)
59
+ attn_mask = None
60
+ if key_padding_mask is not None:
61
+ # torch convention: True = ignore. SDPA bool mask: True = attend.
62
+ attn_mask = (~key_padding_mask)[:, None, None, :]
63
+ o = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
64
+ o = o.transpose(1, 2).reshape(b, lq, d)
65
+ return self.out_proj(o), None
66
+
67
+
68
+ class _MLP(nn.Module):
69
+ def __init__(self, dim: int, ratio: int = 4):
70
+ super().__init__()
71
+ self.net = nn.Sequential(
72
+ nn.Linear(dim, dim * ratio),
73
+ nn.GELU(),
74
+ nn.Linear(dim * ratio, dim),
75
+ )
76
+
77
+ def forward(self, x):
78
+ return self.net(x)
79
+
80
+
81
+ class SelfAttnBlock(nn.Module):
82
+ def __init__(self, dim: int, n_heads: int):
83
+ super().__init__()
84
+ self.norm1 = nn.LayerNorm(dim)
85
+ self.attn = _MHA(dim, n_heads)
86
+ self.norm2 = nn.LayerNorm(dim)
87
+ self.mlp = _MLP(dim)
88
+
89
+ def forward(self, x, cond, cond_mask):
90
+ h = self.norm1(x)
91
+ h, _ = self.attn(h, h, h, need_weights=False)
92
+ x = x + h
93
+ x = x + self.mlp(self.norm2(x))
94
+ return x
95
+
96
+
97
+ class CrossAttnBlock(nn.Module):
98
+ def __init__(self, dim: int, n_heads: int):
99
+ super().__init__()
100
+ self.norm1 = nn.LayerNorm(dim)
101
+ self.norm_cond = nn.LayerNorm(dim)
102
+ self.attn = _MHA(dim, n_heads)
103
+ self.norm2 = nn.LayerNorm(dim)
104
+ self.mlp = _MLP(dim)
105
+
106
+ def forward(self, x, cond, cond_mask):
107
+ h = self.norm1(x)
108
+ c = self.norm_cond(cond)
109
+ # key_padding_mask: True = ignore
110
+ kpm = ~cond_mask if cond_mask is not None else None
111
+ h, _ = self.attn(h, c, c, key_padding_mask=kpm, need_weights=False)
112
+ x = x + h
113
+ x = x + self.mlp(self.norm2(x))
114
+ return x
115
+
116
+
117
+ class FlowMatchingExpert(nn.Module):
118
+ def __init__(
119
+ self,
120
+ action_dim: int = 8,
121
+ chunk_size: int = 50,
122
+ d_model: int = 512,
123
+ n_layers: int = 12,
124
+ n_heads: int = 8,
125
+ min_period: float = 4e-3,
126
+ max_period: float = 4.0,
127
+ ):
128
+ super().__init__()
129
+ self.action_dim = action_dim
130
+ self.chunk_size = chunk_size
131
+ self.d_model = d_model
132
+ self.min_period = min_period
133
+ self.max_period = max_period
134
+
135
+ self.action_in = nn.Linear(action_dim, d_model)
136
+ self.pos_emb = nn.Parameter(torch.zeros(1, chunk_size, d_model))
137
+ self.time_mlp = nn.Sequential(
138
+ nn.Linear(d_model, d_model),
139
+ nn.GELU(),
140
+ nn.Linear(d_model, d_model),
141
+ )
142
+ # even blocks cross-attend to the conditioning set, odd blocks self-attend
143
+ self.blocks = nn.ModuleList(
144
+ [
145
+ (CrossAttnBlock if i % 2 == 0 else SelfAttnBlock)(d_model, n_heads)
146
+ for i in range(n_layers)
147
+ ]
148
+ )
149
+ self.norm_out = nn.LayerNorm(d_model)
150
+ self.action_out = nn.Linear(d_model, action_dim)
151
+
152
+ nn.init.trunc_normal_(self.pos_emb, std=0.02)
153
+ nn.init.zeros_(self.action_out.weight)
154
+ nn.init.zeros_(self.action_out.bias)
155
+
156
+ def forward(
157
+ self,
158
+ noisy_actions: torch.Tensor, # (B, T, A)
159
+ time: torch.Tensor, # (B,) in [0, 1]
160
+ cond: torch.Tensor, # (B, N, d_model)
161
+ cond_mask: torch.Tensor | None = None, # (B, N) True = valid
162
+ ) -> torch.Tensor:
163
+ x = self.action_in(noisy_actions) + self.pos_emb[:, : noisy_actions.shape[1]]
164
+ t_emb = self.time_mlp(
165
+ sincos_time_embedding(time, self.d_model, self.min_period, self.max_period)
166
+ )
167
+ x = x + t_emb[:, None, :]
168
+ for block in self.blocks:
169
+ x = block(x, cond, cond_mask)
170
+ return self.action_out(self.norm_out(x))
tinyvla_b200/src/tinyvla/modules/semantic.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Slow semantic path: Qwen3.5-0.8B VLM over [image | instruction | readout slots].
2
+
3
+ Qwen3.5-0.8B has a native vision tower (12-layer ViT, patch 16, spatial merge 2:
4
+ a 256x256 image becomes 64 visual tokens), so the slow path consumes the raw
5
+ image directly — no hand-rolled projector. The language stack is truncated to
6
+ the first `num_layers` blocks (L/2 layer-skip, SmolVLA-style). The semantic
7
+ latent is the hidden state of K trailing readout slots (pad tokens after the
8
+ instruction; causal attention lets them aggregate the full prefix), projected
9
+ to the expert dimension. At deployment this runs at 0.5-2 Hz and the latent is
10
+ cached between refreshes.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+
19
+
20
+ class SemanticPath(nn.Module):
21
+ def __init__(
22
+ self,
23
+ model_name: str = "Qwen/Qwen3.5-0.8B",
24
+ num_layers: int = 12,
25
+ num_readout: int = 8,
26
+ out_dim: int = 512,
27
+ image_size: int = 256,
28
+ freeze_lm: bool = True,
29
+ freeze_vision: bool = True,
30
+ morph_to_slow: bool = False,
31
+ morph_dim: int = 16,
32
+ morph_qwen: bool = False,
33
+ num_morph_readout: int = 4,
34
+ vlm_native: bool = False,
35
+ num_action_readout: int = 16,
36
+ max_state_dim: int = 16,
37
+ max_action_dim: int = 8,
38
+ ):
39
+ super().__init__()
40
+ from transformers import AutoModelForImageTextToText, AutoProcessor
41
+
42
+ vlm = AutoModelForImageTextToText.from_pretrained(model_name, dtype=torch.float32)
43
+ model = vlm.model # Qwen3_5Model: visual tower + language stack
44
+ # L/2 layer-skip: keep the first num_layers language blocks; lm_head unused
45
+ model.language_model.layers = model.language_model.layers[:num_layers]
46
+ model.language_model.config.num_hidden_layers = num_layers
47
+ self.vlm = model
48
+ cfg = vlm.config
49
+ self.hidden_size = cfg.text_config.hidden_size
50
+ self.image_token_id = cfg.image_token_id
51
+ self.vision_start_id = cfg.vision_start_token_id
52
+ self.vision_end_id = cfg.vision_end_token_id
53
+
54
+ proc = AutoProcessor.from_pretrained(model_name)
55
+ self.pad_token_id = proc.tokenizer.pad_token_id or proc.tokenizer.eos_token_id
56
+ ip = proc.image_processor
57
+ self.patch_size = ip.patch_size # 16
58
+ self.merge_size = ip.merge_size # 2
59
+ self.temporal_patch_size = ip.temporal_patch_size # 2
60
+ self.register_buffer(
61
+ "pixel_mean", torch.tensor(ip.image_mean).view(1, 3, 1, 1), persistent=False
62
+ )
63
+ self.register_buffer(
64
+ "pixel_std", torch.tensor(ip.image_std).view(1, 3, 1, 1), persistent=False
65
+ )
66
+ self.image_size = image_size
67
+ grid = image_size // self.patch_size
68
+ self.num_image_tokens = (grid // self.merge_size) ** 2 # 64 @ 256px
69
+
70
+ self.out_proj = nn.Linear(self.hidden_size, out_dim)
71
+ self.num_readout = num_readout
72
+ self._fixed_fn = None # lazily compiled fixed-grid tower path
73
+ self.morph_to_slow = morph_to_slow
74
+ if morph_to_slow:
75
+ # inject robot morphology into the slow LM prefix so the semantic
76
+ # latent is morphology-aware ("grasp X" differs per embodiment)
77
+ self.morph_to_lm = nn.Linear(morph_dim, self.hidden_size)
78
+
79
+ # LLaVA-style unified mode: every modality becomes tokens inside the LM
80
+ # sequence, and the action policy reads latent tokens off the LM output.
81
+ self.vlm_native = vlm_native
82
+ self.num_action_readout = num_action_readout
83
+ if vlm_native:
84
+ self.state_to_lm = nn.Linear(max_state_dim, self.hidden_size)
85
+ self.action_to_lm = nn.Linear(max_action_dim, self.hidden_size)
86
+ self.action_readout_proj = nn.Linear(self.hidden_size, out_dim)
87
+ self.readout_emb = nn.Parameter(torch.zeros(num_action_readout, self.hidden_size))
88
+ nn.init.normal_(self.readout_emb, std=0.02)
89
+
90
+ self.morph_qwen = morph_qwen
91
+ self.num_morph_readout = num_morph_readout
92
+ if morph_qwen:
93
+ # encode the robot TEXT description through the SAME Qwen language
94
+ # stack (shared weights, text-only, no image) instead of an MLP over
95
+ # numeric fields — lets the (unfrozen) LM's language understanding
96
+ # do the morphology encoding instead of a from-scratch projector.
97
+ self.morph_out_proj = nn.Linear(self.hidden_size, out_dim)
98
+
99
+ if freeze_lm:
100
+ for p in self.vlm.language_model.parameters():
101
+ p.requires_grad = False
102
+ # vocab embeddings stay frozen either way (254M tied params, negligible gain)
103
+ self.vlm.get_input_embeddings().weight.requires_grad = False
104
+ if freeze_vision:
105
+ for p in self.vlm.visual.parameters():
106
+ p.requires_grad = False
107
+
108
+ @property
109
+ def visual_hidden_size(self) -> int:
110
+ return self.vlm.visual.config.out_hidden_size
111
+
112
+ def encode_image(self, images: torch.Tensor) -> torch.Tensor:
113
+ """Qwen vision tower spatial tokens: (B, 3, H, W) -> (B, 64, out_hidden).
114
+
115
+ Shared encoder for both paths; the fast path cross-attends to these
116
+ directly, bypassing the language stack. The SAME tensor can be handed to
117
+ `forward(..., image_embeds=...)` so cam0 goes through the tower once per
118
+ step instead of twice (once here, once inside the LM call).
119
+
120
+ Uses a dense fixed-grid fast path: every image here is the same
121
+ (1, g, g) grid, so the HF packed-varlen tower (which runs B separate
122
+ SDPA calls PER LAYER and rebuilds pos/rotary tables in Python on every
123
+ call) is replaced by one batched SDPA per layer over (B, L, D), with the
124
+ per-image positional constants computed once and cached. Numerically
125
+ equivalent — per-image attention with equal lengths IS batched attention.
126
+ """
127
+ b = images.shape[0]
128
+ pixel_values, grid_thw = self._patchify(images)
129
+ fn = self._fixed_fn
130
+ if fn is None:
131
+ import os
132
+
133
+ if os.environ.get("TINYVLA_NO_COMPILE_VIT"):
134
+ fn = self._encode_image_fixed
135
+ else:
136
+ # static shapes -> compile is safe and worth ~2x on top of the
137
+ # dense rewrite; recompiles once per new batch size
138
+ fn = torch.compile(self._encode_image_fixed, dynamic=False)
139
+ self._fixed_fn = fn
140
+ try:
141
+ return fn(b, pixel_values)
142
+ except Exception:
143
+ if fn is not self._encode_image_fixed: # compile blew up -> eager forever
144
+ self._fixed_fn = self._encode_image_fixed
145
+ return self._encode_image_fixed(b, pixel_values)
146
+ raise
147
+
148
+ def _encode_image_hf(self, images: torch.Tensor) -> torch.Tensor:
149
+ """Reference path through the stock HF tower (kept for verification)."""
150
+ b = images.shape[0]
151
+ pixel_values, grid_thw = self._patchify(images)
152
+ out = self.vlm.get_image_features(pixel_values.to(self.pixel_mean.dtype), grid_thw)
153
+ embeds = torch.cat(out.pooler_output, dim=0) # per-image tuple -> (B*64, C)
154
+ return embeds.reshape(b, self.num_image_tokens, -1)
155
+
156
+ def _fixed_grid_ctx(self, device):
157
+ """Per-image positional constants for the fixed (1, g, g) grid, computed
158
+ ONCE via the tower's own functions (so the layout can't drift) and cached."""
159
+ g = self.image_size // self.patch_size
160
+ ctx = getattr(self, "_fixed_ctx", None)
161
+ if ctx is not None and ctx[0] == (g, str(device)):
162
+ return ctx[1]
163
+ visual = self.vlm.visual
164
+ grid1 = torch.tensor([[1, g, g]], device=device)
165
+ with torch.no_grad():
166
+ pos = visual.fast_pos_embed_interpolate(grid1) # (g*g, hidden)
167
+ rot = visual.rot_pos_emb(grid1) # (g*g, head_dim/2)
168
+ emb = torch.cat((rot, rot), dim=-1)
169
+ val = (pos.detach(), emb.cos().detach(), emb.sin().detach())
170
+ self._fixed_ctx = ((g, str(device)), val)
171
+ return val
172
+
173
+ def _encode_image_fixed(self, b: int, pixel_values: torch.Tensor) -> torch.Tensor:
174
+ from transformers.models.qwen3_5.modeling_qwen3_5 import rotate_half
175
+
176
+ visual = self.vlm.visual
177
+ g = self.image_size // self.patch_size
178
+ L = g * g
179
+ x = visual.patch_embed(pixel_values.to(self.pixel_mean.dtype)) # (B*L, D)
180
+ pos, cos, sin = self._fixed_grid_ctx(x.device)
181
+ d = x.shape[-1]
182
+ x = x.view(b, L, d) + pos[None].to(x.dtype)
183
+ # (1, 1, L, hd) fp32 — same upcast-to-fp32 rotary as the HF path
184
+ cosf = cos[None, None].float()
185
+ sinf = sin[None, None].float()
186
+ for blk in visual.blocks:
187
+ attn = blk.attn
188
+ h = blk.norm1(x)
189
+ qkv = attn.qkv(h).view(b, L, 3, attn.num_heads, attn.head_dim)
190
+ q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0) # (B, nh, L, hd)
191
+ qf, kf = q.float(), k.float()
192
+ q = ((qf * cosf) + (rotate_half(qf) * sinf)).to(q.dtype)
193
+ k = ((kf * cosf) + (rotate_half(kf) * sinf)).to(k.dtype)
194
+ o = F.scaled_dot_product_attention(q, k, v, scale=attn.scaling)
195
+ o = o.transpose(1, 2).reshape(b, L, d)
196
+ x = x + attn.proj(o)
197
+ x = x + blk.mlp(blk.norm2(x))
198
+ merged = visual.merger(x.reshape(b * L, d)) # (B*L/m^2, out_hidden)
199
+ return merged.reshape(b, self.num_image_tokens, -1)
200
+
201
+ def _grid_thw(self, b: int, device) -> torch.Tensor:
202
+ g = self.image_size // self.patch_size
203
+ return torch.tensor([[1, g, g]], device=device).expand(b, -1)
204
+
205
+ def _patchify(self, images: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
206
+ """(B, 3, H, W) in [0,1] -> Qwen-VL flattened patches + grid_thw (B, 3).
207
+
208
+ Mirrors the Qwen VL image processor: resize to image_size, normalize,
209
+ repeat temporally, unfold into merge-ordered patches.
210
+ """
211
+ b = images.shape[0]
212
+ if images.shape[-2:] != (self.image_size, self.image_size):
213
+ images = F.interpolate(
214
+ images, size=(self.image_size, self.image_size), mode="bilinear", align_corners=False
215
+ )
216
+ images = (images - self.pixel_mean) / self.pixel_std
217
+
218
+ p, m, tp = self.patch_size, self.merge_size, self.temporal_patch_size
219
+ g = self.image_size // p
220
+ # (B, tp, C, H, W)
221
+ x = images.unsqueeze(1).expand(-1, tp, -1, -1, -1)
222
+ # split H,W into (g/m, m, p) blocks, order patches so that the m*m
223
+ # merge-group is contiguous: (gh/m, gw/m, m, m) patch order
224
+ x = x.reshape(b, tp, 3, g // m, m, p, g // m, m, p)
225
+ x = x.permute(0, 3, 6, 4, 7, 2, 1, 5, 8) # b, gh/m, gw/m, m, m, C, tp, p, p
226
+ x = x.reshape(b * g * g, 3 * tp * p * p)
227
+ grid_thw = torch.tensor([[1, g, g]], device=images.device).expand(b, -1)
228
+ return x, grid_thw
229
+
230
+ def forward(
231
+ self,
232
+ images: torch.Tensor | None, # (B, 3, H, W) in [0, 1]; may be None with image_embeds
233
+ lang_tokens: torch.Tensor, # (B, L)
234
+ lang_mask: torch.Tensor, # (B, L) bool, True = valid
235
+ morphology: torch.Tensor | None = None, # (B, morph_dim) for morph_to_slow
236
+ image_embeds: torch.Tensor | None = None, # (B, n_img, C) already through the tower
237
+ ) -> torch.Tensor:
238
+ """Returns semantic latent (B, num_readout, out_dim).
239
+
240
+ `image_embeds` lets the caller reuse the vision-tower output the fast path
241
+ already computed for the same frame — the tower is ~45 GFLOPs/camera and
242
+ cam0 was otherwise encoded twice per training step.
243
+ """
244
+ if image_embeds is not None:
245
+ b, device = image_embeds.shape[0], image_embeds.device
246
+ pixel_values, grid_thw = None, self._grid_thw(b, device)
247
+ else:
248
+ b, device = images.shape[0], images.device
249
+ pixel_values, grid_thw = self._patchify(images)
250
+ n_img = self.num_image_tokens
251
+
252
+ # <vision_start> <image>*n <vision_end> instruction <pad>*K
253
+ ids = torch.cat(
254
+ [
255
+ torch.full((b, 1), self.vision_start_id, dtype=torch.long, device=device),
256
+ torch.full((b, n_img), self.image_token_id, dtype=torch.long, device=device),
257
+ torch.full((b, 1), self.vision_end_id, dtype=torch.long, device=device),
258
+ lang_tokens,
259
+ torch.full((b, self.num_readout), self.pad_token_id, dtype=torch.long, device=device),
260
+ ],
261
+ dim=1,
262
+ )
263
+ attn = torch.cat(
264
+ [
265
+ torch.ones(b, n_img + 2, dtype=torch.bool, device=device),
266
+ lang_mask.bool(),
267
+ torch.ones(b, self.num_readout, dtype=torch.bool, device=device),
268
+ ],
269
+ dim=1,
270
+ )
271
+ mm_type = (ids == self.image_token_id).int() # text 0, image 1
272
+
273
+ use_embeds = image_embeds is not None or (self.morph_to_slow and morphology is not None)
274
+ if use_embeds:
275
+ # embed tokens, scatter image features, optionally add morphology to the
276
+ # readout slots, then run the LM on inputs_embeds (keeps M-RoPE via
277
+ # mm_token_type_ids). This is also the path that consumes precomputed
278
+ # `image_embeds` — identical maths, one fewer vision-tower pass.
279
+ inputs_embeds = self.vlm.get_input_embeddings()(ids)
280
+ if image_embeds is not None:
281
+ img = image_embeds.reshape(-1, image_embeds.shape[-1])
282
+ else:
283
+ img = self.vlm.get_image_features(pixel_values.to(self.pixel_mean.dtype), grid_thw)
284
+ img = torch.cat(img.pooler_output, dim=0) # (B*n_img, hidden)
285
+ img = img.to(inputs_embeds.dtype)
286
+ imask = (ids == self.image_token_id).unsqueeze(-1).expand_as(inputs_embeds)
287
+ inputs_embeds = inputs_embeds.masked_scatter(imask, img)
288
+ if self.morph_to_slow and morphology is not None:
289
+ morph_tok = self.morph_to_lm(morphology.to(inputs_embeds.dtype)) # (B, hidden)
290
+ inputs_embeds[:, -self.num_readout:] = inputs_embeds[:, -self.num_readout:] + morph_tok[:, None, :]
291
+ # With inputs_embeds and no input_ids the model cannot recompute
292
+ # M-RoPE and falls back to a CACHED rope_deltas from a previous call
293
+ # (crashes on a different batch size; silently wrong on the same one).
294
+ # Compute the 3D position ids explicitly from the token layout.
295
+ position_ids, _ = self.vlm.get_rope_index(
296
+ ids, image_grid_thw=grid_thw, attention_mask=attn, mm_token_type_ids=mm_type,
297
+ )
298
+ out = self.vlm(
299
+ inputs_embeds=inputs_embeds, image_grid_thw=grid_thw, attention_mask=attn,
300
+ mm_token_type_ids=mm_type, position_ids=position_ids, use_cache=False,
301
+ )
302
+ else:
303
+ out = self.vlm(
304
+ input_ids=ids, pixel_values=pixel_values.to(self.pixel_mean.dtype),
305
+ image_grid_thw=grid_thw, attention_mask=attn, mm_token_type_ids=mm_type,
306
+ use_cache=False,
307
+ )
308
+ readout_h = out.last_hidden_state[:, -self.num_readout :]
309
+ return self.out_proj(readout_h)
310
+
311
+ def encode_robot_text(self, desc_ids: torch.Tensor, desc_mask: torch.Tensor) -> torch.Tensor:
312
+ """Encode a robot-description text through the SAME (shared-weight) Qwen
313
+ language stack, text-only (no image, no M-RoPE needed). Trailing pad-token
314
+ readout slots aggregate the description via causal attention; their hidden
315
+ states are projected to `num_morph_readout` conditioning tokens.
316
+
317
+ desc_ids/desc_mask: (B, L) tokenized "Robot: X, a N-DOF arm on ..." text.
318
+ Returns (B, num_morph_readout, out_dim).
319
+ """
320
+ b = desc_ids.shape[0]
321
+ device = desc_ids.device
322
+ ids = torch.cat(
323
+ [desc_ids, torch.full((b, self.num_morph_readout), self.pad_token_id,
324
+ dtype=torch.long, device=device)],
325
+ dim=1,
326
+ )
327
+ attn = torch.cat(
328
+ [desc_mask.bool(), torch.ones(b, self.num_morph_readout, dtype=torch.bool, device=device)],
329
+ dim=1,
330
+ )
331
+ out = self.vlm.language_model(
332
+ input_ids=ids, attention_mask=attn, use_cache=False,
333
+ )
334
+ readout_h = out.last_hidden_state[:, -self.num_morph_readout:]
335
+ return self.morph_out_proj(readout_h)
336
+
337
+ def forward_unified(
338
+ self,
339
+ image: torch.Tensor, # (B, 3, H, W) current observation
340
+ lang_tokens: torch.Tensor, # (B, L) task instruction
341
+ lang_mask: torch.Tensor, # (B, L)
342
+ state: torch.Tensor | None = None, # (B, max_state_dim) proprio
343
+ robot_ids: torch.Tensor | None = None, # (B, Lr) robot description text
344
+ robot_mask: torch.Tensor | None = None,
345
+ demo_images: torch.Tensor | None = None, # (B, K, 3, H, W)
346
+ demo_actions: torch.Tensor | None = None, # (B, K, T, A)
347
+ n_demo_action_keys: int = 4,
348
+ ) -> torch.Tensor:
349
+ """LLaVA-style unified pass: ALL modalities become tokens in one LM
350
+ sequence, and the policy reads `num_action_readout` latent tokens off the
351
+ LM output. Demo exemplars live INSIDE the attention stream, so the readout
352
+ tokens can attend to them selectively (the thing that "conditioning-set"
353
+ placement could not do).
354
+
355
+ Sequence layout:
356
+ [<vis> main-image <-vis>] [instruction] [robot description]
357
+ [ <vis> demo_k image <-vis> demo_k action-keyframe tokens ]*K
358
+ [state token] [action readout slots]
359
+ Returns (B, num_action_readout, out_dim).
360
+ """
361
+ b = image.shape[0]
362
+ device = image.device
363
+ emb = self.vlm.get_input_embeddings()
364
+ n_img = self.num_image_tokens
365
+
366
+ # ---- encode all images (main + demos) in one vision pass -------------
367
+ imgs = [image]
368
+ k = 0
369
+ if demo_images is not None:
370
+ k = demo_images.shape[1]
371
+ imgs.append(demo_images.reshape(b * k, *demo_images.shape[2:]))
372
+ all_imgs = torch.cat(imgs, dim=0)
373
+ pixel_values, grid_thw = self._patchify(all_imgs)
374
+ feats = self.vlm.get_image_features(pixel_values.to(self.pixel_mean.dtype), grid_thw)
375
+ feats = torch.cat(feats.pooler_output, dim=0) # (B*(1+K)*n_img, hidden)
376
+ feats = feats.view(b * (1 + k), n_img, -1)
377
+ main_feat = feats[:b] # (B, n_img, hidden)
378
+ demo_feat = feats[b:].view(b, k, n_img, -1) if k else None
379
+
380
+ seq, mm, msk = [], [], []
381
+
382
+ def push(x, is_img=False, mask=None):
383
+ seq.append(x)
384
+ n = x.shape[1]
385
+ mm.append(torch.full((b, n), 1 if is_img else 0, dtype=torch.int, device=device))
386
+ msk.append(mask if mask is not None else torch.ones(b, n, dtype=torch.bool, device=device))
387
+
388
+ vs = emb(torch.full((b, 1), self.vision_start_id, dtype=torch.long, device=device))
389
+ ve = emb(torch.full((b, 1), self.vision_end_id, dtype=torch.long, device=device))
390
+
391
+ push(vs); push(main_feat, is_img=True); push(ve)
392
+ push(emb(lang_tokens), mask=lang_mask.bool())
393
+ if robot_ids is not None:
394
+ push(emb(robot_ids), mask=robot_mask.bool())
395
+ if demo_feat is not None:
396
+ keys = torch.linspace(0, demo_actions.shape[2] - 1, n_demo_action_keys,
397
+ device=device).long()
398
+ for i in range(k):
399
+ push(vs); push(demo_feat[:, i], is_img=True); push(ve)
400
+ act_tok = self.action_to_lm(demo_actions[:, i, keys].to(main_feat.dtype))
401
+ push(act_tok) # (B, n_keys, hidden)
402
+ if state is not None:
403
+ push(self.state_to_lm(state.to(main_feat.dtype))[:, None, :])
404
+ push(self.readout_emb[None].expand(b, -1, -1).to(main_feat.dtype))
405
+
406
+ inputs_embeds = torch.cat(seq, dim=1)
407
+ mm_type = torch.cat(mm, dim=1)
408
+ attn = torch.cat(msk, dim=1)
409
+ # image_grid_thw must cover only the images actually present in-sequence
410
+ grid_for_seq = grid_thw[: b * (1 + k)]
411
+
412
+ out = self.vlm(
413
+ inputs_embeds=inputs_embeds,
414
+ image_grid_thw=grid_for_seq,
415
+ attention_mask=attn,
416
+ mm_token_type_ids=mm_type,
417
+ use_cache=False,
418
+ )
419
+ readout = out.last_hidden_state[:, -self.num_action_readout:]
420
+ return self.action_readout_proj(readout)