Instructions to use AlexWortega/tinyvla with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use AlexWortega/tinyvla with LeRobot:
- Notebooks
- Google Colab
- Kaggle
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 +160 -0
- tinyvla_b200/README.md +188 -0
- tinyvla_b200/configs/physical_ai_ft.yaml +47 -0
- tinyvla_b200/configs/physical_ai_stream.yaml +820 -0
- tinyvla_b200/configs/trex_ft.yaml +62 -0
- tinyvla_b200/patches/descriptors_dexmate.yaml +24 -0
- tinyvla_b200/patches/modeling_tinyvla.py.diff +75 -0
- tinyvla_b200/patches/modules_expert.py.diff +76 -0
- tinyvla_b200/patches/modules_semantic.py.diff +180 -0
- tinyvla_b200/scripts/bench_model.py +139 -0
- tinyvla_b200/scripts/build_shards.py +392 -0
- tinyvla_b200/scripts/build_shards_v2.py +246 -0
- tinyvla_b200/scripts/make_stream_specs.py +119 -0
- tinyvla_b200/scripts/pack_wds.py +121 -0
- tinyvla_b200/scripts/train_fast.py +309 -0
- tinyvla_b200/src/tinyvla/data/shards.py +222 -0
- tinyvla_b200/src/tinyvla/data/streaming_hub.py +277 -0
- tinyvla_b200/src/tinyvla/data/wds_mix.py +173 -0
- tinyvla_b200/src/tinyvla/modeling_tinyvla.py +308 -0
- tinyvla_b200/src/tinyvla/modules/expert.py +170 -0
- tinyvla_b200/src/tinyvla/modules/semantic.py +420 -0
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)
|