ar-mine commited on
Commit
e41bef1
·
verified ·
1 Parent(s): 0c30991

Phase 2.1A Robometer-Low training kit v2

Browse files
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pt
5
+ *.pth
6
+ *.safetensors
7
+ *.npz
8
+ hf_cache/
9
+ runs/
10
+ outputs/
11
+ wandb/
12
+ *.log
13
+ .env
14
+ *token*
README.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ tags:
4
+ - robotics
5
+ - reward-modeling
6
+ - qwen3-vl
7
+ - peft
8
+ - private-training-code
9
+ ---
10
+
11
+ # Cosmos–Robometer Phase 2.1A training code
12
+
13
+ Private, portable training kit for the approved `Public-Proxy stage2_core_v1`
14
+ experiments:
15
+
16
+ - `Robometer-Low-LoRA`: implemented formal trainer with frozen ID-dev evaluation,
17
+ early stopping, and a 3,000-step hard cap.
18
+ - `Robometer-Low`: full language-decoder smoke test. Formal training remains
19
+ intentionally blocked until suitable multi-GPU/server memory is available.
20
+
21
+ No dataset files, model weights, checkpoints, Hugging Face cache, local virtual
22
+ environment, credentials, OOD evaluation data, or machine-specific run logs are
23
+ included.
24
+
25
+ ## Recreate the environment
26
+
27
+ ```bash
28
+ uv python install 3.10.12
29
+ uv sync --project environment/phase21a_robometer --frozen
30
+ uv run --project environment/phase21a_robometer \
31
+ python environment/phase21a_robometer/doctor.py
32
+ ```
33
+
34
+ The environment is locked for PyTorch 2.8.0/CUDA 12.8. The reference smoke
35
+ hardware was one RTX 5090 with 32,607 MiB VRAM.
36
+
37
+ ## External inputs
38
+
39
+ - Base model: `unsloth/Qwen3-VL-4B-Instruct` at revision
40
+ `252d592b59b0233b226875a44ac135cfa1d3f755`.
41
+ - Dataset route: `Public-Proxy stage2_core_v1` derived from
42
+ `robometer/processed_datasets` at revision
43
+ `1a8eb2d279c70964fadeaf8019a77c490c3abe02`.
44
+ - Frozen Phase 2.1A subset: 2,689 pair rows referencing 4,323 trajectory files.
45
+
46
+ The planned dataset repository is
47
+ `ar-mine/cosmos-robometer-public-dataset`.
48
+ Until it is available, the subset can be reconstructed from the pinned official
49
+ Robometer processed-data repository and the frozen dataset manifest.
50
+
51
+ ## Training
52
+
53
+ See [TRAINING_MANUAL.md](TRAINING_MANUAL.md). The frozen LoRA configuration is
54
+ `configs/phase21a_robometer_low_lora_train_v2.yaml`.
55
+
56
+ ## Current support boundary
57
+
58
+ The LoRA trainer is runnable and validated at micro-batch 32, gradient
59
+ accumulation 2, effective batch 64. The full-tune file is a capacity smoke test,
60
+ not a formal distributed trainer. Paged 8-bit AdamW is not permitted for the
61
+ formal `Robometer-Low` comparison.
62
+
63
+ ## Redistribution
64
+
65
+ This private repository contains experiment code and metadata only. It does not
66
+ grant redistribution rights for upstream datasets or model weights. Review the
67
+ terms of each upstream source before making any derived dataset public.
TRAINING_MANUAL.md ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 2.1A 训练手册
2
+
3
+ 日期:2026-08-27
4
+ 对象:`Robometer-Low-LoRA` 与 `Robometer-Low`
5
+ 数据:`Public-Proxy stage2_core_v1` 的冻结 5% pair manifest
6
+
7
+ ## 1. 名称和不可变边界
8
+
9
+ - `Robometer-Low-LoRA`:Qwen3-VL-4B 视觉塔和语言基座冻结;训练 attention LoRA、五个新增
10
+ token rows 和 progress/success/preference 三头。
11
+ - `Robometer-Low`:视觉塔冻结;语言 decoder 全量微调;训练三个 heads;`use_peft=false`。
12
+ - `Low` 只表示使用非完整版 Public-Proxy 数据,不表示量化或 LoRA。
13
+ - 两者使用同一 2,021-pair train split、同一 A/B 联合输入、同一 8+8 帧、同一三损失合同。
14
+ - Robometer-Low 正式训练只允许标准 `torch.optim.AdamW`;禁止 paged AdamW 8-bit。
15
+ - OOD benchmark 不进入训练 loop、checkpoint selection 或超参数选择。
16
+
17
+ 冻结数据统计:2,689 rows,其中 train/dev/confirmation-dev/excluded 分别为
18
+ 2,021/108/538/22;manifest SHA256 为
19
+ `c77642e3d34177d1bfbcebe8c97cdf0af952c83d95a677cf42d95426b6d49204`。
20
+
21
+ ## 2. 当前代码成熟度
22
+
23
+ | 分支 | 状态 | 可以做什么 |
24
+ |---|---|---|
25
+ | Robometer-Low-LoRA v1 | 已按用户要求停止 | 日志到 step 246;最新完整 checkpoint 为 step 4 |
26
+ | Robometer-Low-LoRA v2 | 已配置,未正式启动 | 3,000-step cap、在线 ID-dev、早停和最佳 checkpoint 恢复 |
27
+ | Robometer-Low | 等待服务器资源和正式 runner | 单卡容量 smoke;不能把当前 smoke 当正式训练入口 |
28
+
29
+ LoRA v1 已停止且不再续训。v2 使用全新 output root;禁止拿 v1 的 step-4 checkpoint 恢复到 v2,
30
+ 因为总预算、scheduler 和选择协议均已改变。
31
+
32
+ ## 3. 参考环境
33
+
34
+ 已验证环境:
35
+
36
+ | 项目 | 版本 |
37
+ |---|---|
38
+ | OS | Ubuntu 22.04.5 LTS |
39
+ | Python | 3.10.12 |
40
+ | NVIDIA driver | 580.173.02 |
41
+ | PyTorch / CUDA runtime | 2.8.0+cu128 / 12.8 |
42
+ | torchvision | 0.23.0+cu128 |
43
+ | transformers | 4.57.6 |
44
+ | PEFT | 0.19.1 |
45
+ | accelerate | 1.14.0 |
46
+ | flash-attn | 2.8.3.post1 |
47
+ | qwen-vl-utils | 0.0.14 |
48
+ | NumPy / Pillow | 1.26.4 / 11.3.0 |
49
+
50
+ 可部署环境固定在 `environment/phase21a_robometer/pyproject.toml` 和 `uv.lock`,不复制 `.venv`:
51
+
52
+ ```bash
53
+ cd /srv/phase21a-kit
54
+ uv python install 3.10.12
55
+ uv sync --project environment/phase21a_robometer --frozen
56
+ uv run --project environment/phase21a_robometer \
57
+ python environment/phase21a_robometer/doctor.py
58
+ ```
59
+
60
+ 如果上传包内保存了相同 `pyproject.toml` 和 `uv.lock`,仍要在目标 CUDA/driver 上重新创建环境。
61
+ 不要为上传工具升级训练 venv;`hf` CLI 可使用单独小环境。
62
+
63
+ ## 4. 下载并固定输入
64
+
65
+ 以下使用示例变量;按服务器目录修改:
66
+
67
+ ```bash
68
+ PHASE21A_KIT=/srv/phase21a-kit
69
+ PHASE21A_DATA=/srv/phase21a-data
70
+ PHASE21A_MODEL=/srv/models/qwen3-vl-4b-instruct-252d592
71
+ PHASE21A_RUNS=/srv/phase21a-runs
72
+ ```
73
+
74
+ 下载 training kit 和 dataset:
75
+
76
+ ```bash
77
+ hf download ar-mine/cosmos-robometer-private-code \
78
+ --revision <KIT_COMMIT> \
79
+ --local-dir "$PHASE21A_KIT"
80
+
81
+ hf download ar-mine/cosmos-robometer-public-dataset \
82
+ --repo-type dataset \
83
+ --revision <DATA_COMMIT> \
84
+ --local-dir "$PHASE21A_DATA"
85
+ ```
86
+
87
+ 下载精确基础模型;不要使用浮动 `main`:
88
+
89
+ ```bash
90
+ hf download unsloth/Qwen3-VL-4B-Instruct \
91
+ --revision 252d592b59b0233b226875a44ac135cfa1d3f755 \
92
+ --local-dir "$PHASE21A_MODEL"
93
+ ```
94
+
95
+ ## 5. 开跑前共同门禁
96
+
97
+ ### 5.1 文件完整性
98
+
99
+ ```bash
100
+ cd "$PHASE21A_DATA"
101
+ sha256sum --check SHA256SUMS
102
+ ```
103
+
104
+ 再运行 dataset bundle 工具的 dry-run,把 `--output-dir` 指向任意不会执行复制的路径:
105
+
106
+ ```bash
107
+ cd "$PHASE21A_KIT"
108
+ uv run --project environment/phase21a_robometer python scripts/build_phase21a_hf_dataset_bundle.py \
109
+ --manifest "$PHASE21A_DATA/manifests/phase2_remediation_pairs_v1.jsonl" \
110
+ --processed-root "$PHASE21A_DATA/processed_datasets" \
111
+ --output-dir /tmp/phase21a-dry-run
112
+ ```
113
+
114
+ 必须得到:2,689 rows、4,323 unique frame files、0 missing、7.908 GiB。
115
+
116
+ ### 5.2 GPU/软件门禁
117
+
118
+ ```bash
119
+ nvidia-smi
120
+ uv run --project environment/phase21a_robometer \
121
+ python environment/phase21a_robometer/doctor.py
122
+ ```
123
+
124
+ 必须确认:BF16 可用、FlashAttention 2 可加载、模型 snapshot 离线可读、目标输出盘有足够空间。
125
+
126
+ ### 5.3 数据和评测隔离
127
+
128
+ - formal trainer 只读取 `split=train` 且 `sample_type=preference`,精确为 2,021 pairs。
129
+ - dev/confirmation-dev 只能用于 ID 报告或预注册 checkpoint selection。
130
+ - `excluded_original_dev_component` 不得回流训练。
131
+ - RBM-EVAL-OOD 不下载到训练节点也可以;不得据其结果调参。
132
+
133
+ ## 6. Robometer-Low-LoRA
134
+
135
+ ### 6.1 冻结配置
136
+
137
+ | 参数 | 值 |
138
+ |---|---:|
139
+ | base | `unsloth/Qwen3-VL-4B-Instruct@252d592...` |
140
+ | frames per pair | A 8 + B 8 |
141
+ | canvas | 256×256 letterbox |
142
+ | LoRA | r=8, alpha=16, dropout=0.05 |
143
+ | LoRA targets | q/k/v/o projections |
144
+ | trainable | LoRA + 5 special-token rows + 3 heads |
145
+ | trainable params | 15,768,332 |
146
+ | micro batch | 32 pairs |
147
+ | gradient accumulation | 2 |
148
+ | effective batch | 64 pairs |
149
+ | precision | BF16 |
150
+ | attention | FlashAttention 2 |
151
+ | activation checkpointing | enabled |
152
+ | optimizer | standard AdamW |
153
+ | LoRA LR / heads LR | 2e-6 / 1e-4 |
154
+ | weight decay | 0.01 |
155
+ | scheduler | cosine, 5% warmup |
156
+ | grad clip | 1.0 |
157
+ | steps | 3,000 hard cap per seed;ID-dev early stop |
158
+ | seeds | 7, 17, 27 |
159
+ | checkpoint/eval | every 250 steps;min 1,000;patience 4;min delta 0.005 |
160
+
161
+ 新增 token 的顺序必须保持:
162
+
163
+ ```text
164
+ <|split_token|>, <|reward_token|>, <|pref_token|>, <|sim_token|>, <|prog_token|>
165
+ ```
166
+
167
+ 在当前 base revision 上对应 ID 为 151669–151673。
168
+
169
+ ### 6.2 单步 smoke
170
+
171
+ 在新服务器上必须先跑最长 prompt pool 的真实一步:
172
+
173
+ ```bash
174
+ cd "$PHASE21A_KIT"
175
+ uv run --project environment/phase21a_robometer python scripts/smoke_phase21a_robometer_low.py \
176
+ --snapshot "$PHASE21A_MODEL" \
177
+ --manifest "$PHASE21A_DATA/manifests/phase2_remediation_pairs_v1.jsonl" \
178
+ --processed-root "$PHASE21A_DATA/processed_datasets" \
179
+ --micro-batch-pairs 32 \
180
+ --gradient-accumulation 2 \
181
+ --optimizer-steps 1 \
182
+ --pair-pool-size 8 \
183
+ --seed 7 \
184
+ --lora-lr 2e-6 \
185
+ --head-lr 1e-4 \
186
+ --output /tmp/phase21a_lora_smoke.json
187
+ ```
188
+
189
+ 通过条件:
190
+
191
+ - `status=pass`;
192
+ - effective batch 精确 64;
193
+ - sequence max 不超过已知最长 1,319 tokens;
194
+ - 三个 loss 有限;
195
+ - LoRA、token rows、三个 heads 的梯度都有限且非零;
196
+ - 峰值 reserved 不超过本机显存安全线。RTX 5090 参考值为 28.150 GiB,但其他 GPU 不应机械套用。
197
+
198
+ ### 6.3 正式启动
199
+
200
+ portable runner 已支持显式模型、manifest 和数据路径:
201
+
202
+ ```bash
203
+ cd "$PHASE21A_KIT"
204
+ uv run --project environment/phase21a_robometer python scripts/run_phase21a_robometer_low_lora_seeds.py \
205
+ --snapshot "$PHASE21A_MODEL" \
206
+ --manifest "$PHASE21A_DATA/manifests/phase2_remediation_pairs_v1.jsonl" \
207
+ --processed-root "$PHASE21A_DATA/processed_datasets" \
208
+ --output-root "$PHASE21A_RUNS/robometer-low-lora-v2" \
209
+ --seeds 7 17 27 \
210
+ --optimizer-steps 3000 \
211
+ --micro-batch-pairs 32 \
212
+ --gradient-accumulation 2 \
213
+ --lora-lr 2e-6 \
214
+ --head-lr 1e-4 \
215
+ --checkpoint-every 250 \
216
+ --eval-every 250 \
217
+ --early-stop-min-steps 1000 \
218
+ --early-stop-patience-evals 4 \
219
+ --early-stop-min-delta 0.005
220
+ ```
221
+
222
+ 生产服务器上应由 Slurm、systemd 或其他作业调度器托管;不要依赖普通 SSH shell 的 `&`。每次只运行
223
+ 一个 runner,避免两个进程写同一 output root。
224
+
225
+ ### 6.4 恢复
226
+
227
+ runner 会检测每个 seed 的:
228
+
229
+ - 有 `summary.json`:跳过已完成 seed;
230
+ - 有 `latest_training_state.pt`:自动加 `--resume`;
231
+ - 两者都没有:从 step 0 开始。
232
+
233
+ 完整 checkpoint 包含 adapter、heads、AdamW、scheduler、NumPy/PyTorch/CUDA RNG、数据 permutation
234
+ 和 cursor。恢复前不得修改 manifest;trainer 会核对 manifest SHA256 和 seed。
235
+
236
+ ### 6.5 监控
237
+
238
+ ```bash
239
+ tail -f "$PHASE21A_RUNS/robometer-low-lora-v2/seed_7/progress.jsonl"
240
+ nvidia-smi
241
+ ```
242
+
243
+ 每条 progress record 应包含三个 loss、全局和分组梯度、LR、序列长度、step time 和显存。立即停止的
244
+ 异常包括:NaN/Inf、任一必要梯度组为零、manifest hash 改变、frame file 缺失或 OOM。
245
+
246
+ RTX 5090 当前参考吞吐约 23–24 秒/step、2.7 pairs/s;硬件和温控会改变 ETA。
247
+
248
+ ### 6.6 完成产物
249
+
250
+ 每个 seed 完成后应出现:
251
+
252
+ ```text
253
+ seed_<n>/
254
+ ├── adapter/
255
+ ├── reward_heads.pt
256
+ ├── best_model_state.pt
257
+ ├── latest_training_state.pt
258
+ ├── progress.jsonl
259
+ ├── id_dev_metrics.jsonl
260
+ └── summary.json
261
+ ```
262
+
263
+ 发布前另外保存 processor/tokenizer 和特殊 token map;对外权重优先使用 safetensors。v2 的
264
+ `adapter/` 和 `reward_heads.pt` 会恢复为 ID-dev 最佳 step,而不是机械使用最后一步。
265
+
266
+ ## 7. Robometer-Low 全量 decoder 训练
267
+
268
+ ### 7.1 当前禁止直接正式启动
269
+
270
+ 单张 RTX 5090 上,标准 AdamW 即使 `micro_batch_pairs=1` 也在第一次 optimizer-state 初始化时 OOM。
271
+ PagedAdamW8bit 虽能跑通容量 smoke,但用户已明确禁止其用于正式 Robometer-Low。
272
+
273
+ 当前 `smoke_phase21a_robometer_low_full.py` 只是容量和梯度审计工具,不具备:
274
+
275
+ - 多 GPU/FSDP;
276
+ - 正式 3,000-step cap 和 ID-dev 早停的可恢复状态;
277
+ - distributed RNG/data cursor 恢复;
278
+ - sharded optimizer checkpoint;
279
+ - 三 seed runner。
280
+
281
+ 因此在正式 full runner 实现并通过下述门禁前,Robometer-Low 状态保持
282
+ `waiting_for_server_resources`,不能把 smoke 循环延长后冒充正式训练。
283
+
284
+ ### 7.2 目标正式配置
285
+
286
+ | 参数 | 目标值 |
287
+ |---|---:|
288
+ | vision encoder | frozen |
289
+ | language decoder | full trainable,`use_peft=false` |
290
+ | heads | full trainable |
291
+ | language trainable params | 约 4.0218B |
292
+ | heads trainable params | 9.857M |
293
+ | optimizer | standard torch AdamW,禁止 bitsandbytes/paged |
294
+ | LR | 2e-5 |
295
+ | weight decay | 0.01 |
296
+ | precision/attention | BF16 / FlashAttention 2 |
297
+ | activation checkpointing | enabled |
298
+ | scheduler | cosine,5% warmup |
299
+ | grad clip | 10.0(与当前 full smoke/官方设置一致) |
300
+ | global effective batch | 至少且优先精确为 64 pairs |
301
+ | steps/seeds | 3,000 hard cap + 与 LoRA 相同 ID-dev 早停;7/17/27 |
302
+
303
+ 分布式 global batch 计算公式:
304
+
305
+ ```text
306
+ global_effective_batch = micro_batch_pairs_per_gpu × world_size × gradient_accumulation
307
+ ```
308
+
309
+ 例如 4 GPU、每卡 micro batch 8、accumulation 2 得到 64;这只是算术示例,必须以目标硬件 capacity
310
+ smoke 为准。
311
+
312
+ ### 7.3 正式 runner 的实现门禁
313
+
314
+ 正式代码必须满足:
315
+
316
+ 1. 复用 LoRA v1 的 portable manifest loader、联合 A/B prompt、五 token 顺序和三 heads;
317
+ 2. 只冻结视觉塔,语言 decoder 全量 `requires_grad=True`;
318
+ 3. optimizer 类型审计精确为 `torch.optim.AdamW`,代码路径不导入 bitsandbytes;
319
+ 4. FSDP `FULL_SHARD` 包裹 `Qwen3VLTextDecoderLayer`,并验证无未分片的巨型语言层;
320
+ 5. global effective batch 精确记录并达到 64;
321
+ 6. 每 rank 数据不重复,分布式 sampler 的 epoch/order/cursor 可恢复;
322
+ 7. checkpoint 包含模型、heads、optimizer、scheduler、scaler(如有)、全部 RNG 和 sampler state;
323
+ 8. 中间 checkpoint 优先 sharded state,最终再受控合并,避免 rank 0 保存时 OOM;
324
+ 9. rank 0 写日志,所有 rank 对 NaN/OOM/数据异常做同步失败;
325
+ 10. OOD 不进入训练或选择。
326
+
327
+ 上游 Robometer 的 `robometer/configs/distributed/fsdp.yaml` 可作为 FSDP 配置参考,但官方
328
+ `train.py` 不能直接替代本实验 runner:其默认 PEFT 为 r=32 且覆盖 MLP projections,数据采样和当前
329
+ 冻结 manifest 也不同。直接运行官方命令会形成另一个 protocol。
330
+
331
+ ### 7.4 服务器验收顺序
332
+
333
+ 1. 在隔离进程中加载模型,确认视觉 trainable=0、语言和 heads 数量正确;
334
+ 2. 用最长 prompt 跑一次 FSDP forward/backward/标准 AdamW step;
335
+ 3. 尝试相邻 micro batches,选择峰值显存不超过 90% 的最大安全值;
336
+ 4. 通过 accumulation 调整 global effective batch 到 64;
337
+ 5. 跑 100-step stability smoke,要求 0 NaN/Inf、0 OOM、0 empty mask;
338
+ 6. 在 step 50 保存,重启后恢复到 51,并与不中断参考运行核对 loss/LR/data cursor;
339
+ 7. 固定代码 commit、FSDP config hash、world size 和环境后,才允许三 seed 正式启动。
340
+
341
+ ### 7.5 多 GPU checkpoint 和发布
342
+
343
+ - 训练 checkpoint 保留 sharded optimizer state,供相同或兼容 world size 恢复。
344
+ - 发布 checkpoint 只在训练结束后合并;合并节点要有足够 CPU RAM 和磁盘临时空间。
345
+ - 模型卡必须注明 base revision、数据 commit、world size、global batch、FSDP strategy 和标准 AdamW。
346
+ - 任何使用 8-bit/paged optimizer 的结果必须另命名为实验性分支,不能称为 Robometer-Low。
347
+
348
+ ## 8. 最终开跑检查表
349
+
350
+ ### LoRA
351
+
352
+ - [ ] kit/data/model 三个 commit/revision 已固定
353
+ - [ ] SHA256 全通过,4,323/4,323 frame files 可解析
354
+ - [ ] 单步 smoke 通过且显存安全
355
+ - [ ] output root 全新或只有可信完整 checkpoint
356
+ - [ ] 作业调度器托管,只有一个 runner
357
+ - [ ] seeds 7/17/27、3,000-step cap、ID-dev 早停、effective batch 64
358
+ - [ ] OOD 未接入训练 loop
359
+
360
+ ### Full
361
+
362
+ - [ ] 服务器资源已分配
363
+ - [ ] 自定义正式 FSDP runner 已实现和 code review
364
+ - [ ] standard AdamW 类型审计通过,代码不走 bitsandbytes
365
+ - [ ] 100-step 和断点恢复一致性门禁通过
366
+ - [ ] global effective batch=64
367
+ - [ ] vision frozen、language full trainable
368
+ - [ ] 中间 sharded checkpoint 与最终合并流程均验证
369
+ - [ ] 完成上述全部条件后才从 `waiting_for_server_resources` 改为 `running`
configs/phase21a_robometer_low_full_smoke_v1.yaml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ protocol_id: phase21a_robometer_low_full_smoke_v1
2
+ status: smoke_only
3
+ formal_training_allowed: false
4
+ cosmos3_edge_modification_allowed: false
5
+
6
+ naming:
7
+ robometer_low: full_language_decoder_tuning_on_non_full_public_proxy_data
8
+ robometer_low_lora: attention_lora_on_non_full_public_proxy_data
9
+ low_meaning: dataset_scale_not_parameter_tuning_method
10
+
11
+ data:
12
+ route: Public-Proxy stage2_core_v1
13
+ manifest: manifests/phase2_remediation_pairs_v1.jsonl
14
+ split: train
15
+ frames_per_trajectory: 8
16
+ joint_frames_per_pair: 16
17
+ canvas: [256, 256]
18
+ smoke_pair_pool: 8
19
+ smoke_pair_pool_selection: longest_prompt_characters_in_train_split
20
+
21
+ architecture:
22
+ base: Qwen3-VL-4B-Instruct
23
+ vision_frozen: true
24
+ language_decoder_full_tune: true
25
+ peft_or_lora: false
26
+ new_special_token_rows_trainable: true
27
+ heads_trainable: [progress, success, preference]
28
+ joint_trajectory_a_b_forward: true
29
+ per_frame_progress_token: <|prog_token|>
30
+ pair_preference_token: <|pref_token|>
31
+ progress_bins: 10
32
+
33
+ optimization:
34
+ precision: BF16
35
+ attention: flash_attention_2
36
+ gradient_checkpointing: true
37
+ learning_rate: 2.0e-5
38
+ weight_decay: 0.01
39
+ gradient_clip: 10.0
40
+ minimum_effective_batch_pairs: 64
41
+ optimizer_trials:
42
+ - name: adamw_torch
43
+ official_default: true
44
+ foreach: false
45
+ - name: paged_adamw_8bit
46
+ official_default: false
47
+ purpose: single_gpu_full_parameter_fallback_if_adamw_state_oom
48
+
49
+ capacity_selection:
50
+ each_candidate_in_fresh_process: true
51
+ memory_headroom_rule: peak_reserved_fraction_lte_0.90
52
+ final_gradient_accumulation: ceil(64 / selected_micro_batch_pairs)
53
+ final_validation_optimizer_steps: 1
54
+
55
+ outputs:
56
+ root: runs/phase21a_robometer_low_full_smoke_v1
57
+ report_json: reports/phase21a_robometer_low_full_smoke_v1.json
58
+ report_markdown: reports/PHASE21A_ROBOMETER_LOW_FULL_SMOKE_V1.md
configs/phase21a_robometer_low_lora_train_v2.yaml ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ protocol_id: phase21a_robometer_low_lora_train_v2
2
+ status: configured_not_started
3
+ model_name: Robometer-Low-LoRA
4
+ low_meaning: non_full_public_proxy_dataset
5
+
6
+ data:
7
+ route: Public-Proxy stage2_core_v1
8
+ manifest: manifests/phase2_remediation_pairs_v1.jsonl
9
+ train_pairs: 2021
10
+ id_dev_pairs: 108
11
+ confirmation_dev_pairs: 538
12
+ frames_per_trajectory: 8
13
+ joint_frames_per_pair: 16
14
+ canvas: [256, 256]
15
+
16
+ architecture:
17
+ base: Qwen3-VL-4B-Instruct
18
+ base_revision: 252d592b59b0233b226875a44ac135cfa1d3f755
19
+ vision_frozen: true
20
+ language_base_frozen: true
21
+ language_lora:
22
+ rank: 8
23
+ alpha: 16
24
+ dropout: 0.05
25
+ targets: [q_proj, k_proj, v_proj, o_proj]
26
+ special_token_rows_trainable: true
27
+ heads_trainable: [progress, success, preference]
28
+
29
+ training:
30
+ seeds: [7, 17, 27]
31
+ optimizer_steps_cap_per_seed: 3000
32
+ pair_presentations_at_cap: 192000
33
+ nominal_epochs_at_cap: 95.0025
34
+ micro_batch_pairs: 32
35
+ gradient_accumulation: 2
36
+ effective_batch_pairs: 64
37
+ precision: BF16
38
+ gradient_checkpointing: true
39
+ attention: flash_attention_2
40
+ optimizer: AdamW
41
+ lora_lr: 2.0e-6
42
+ head_lr: 1.0e-4
43
+ weight_decay: 0.01
44
+ scheduler: cosine
45
+ warmup_fraction: 0.05
46
+ warmup_steps_at_cap: 150
47
+ gradient_clip: 1.0
48
+ checkpoint_every_steps: 250
49
+
50
+ model_selection:
51
+ split: dev
52
+ uses_confirmation_dev: false
53
+ uses_ood: false
54
+ evaluate_every_steps: 250
55
+ evaluate_both_pair_orientations: true
56
+ eval_micro_batch_pairs: 32
57
+ metric: summed_mean_id_dev_loss
58
+ mode: min
59
+ minimum_training_steps: 1000
60
+ patience_evaluations: 4
61
+ minimum_delta: 0.005
62
+ restore_best_checkpoint: true
63
+
64
+ budget_basis:
65
+ official_rbm_1m_steps: 15000
66
+ official_scale_interpretation: approximately_960000_presentations_at_effective_batch_64
67
+ low_dataset_interpretation_of_15000: approximately_475_nominal_epochs
68
+ v2_cap_interpretation: approximately_95_nominal_epochs
69
+ phase20a_reference_steps: 2000
70
+ decision: use_3000_as_a_hard_cap_and_select_or_stop_on_id_dev
71
+
72
+ robometer_low_full:
73
+ status: waiting_for_server_resources
74
+ optimizer_steps_cap_per_seed: 3000
75
+ model_selection: same_as_lora_for_matched_comparison
76
+ required_optimizer: official_adamw
77
+ paged_adamw_8bit_formal_training_allowed: false
78
+
79
+ environment:
80
+ project: environment/phase21a_robometer
81
+ lockfile: environment/phase21a_robometer/uv.lock
82
+
83
+ outputs:
84
+ root: runs/phase21a_robometer_low_lora_train_v2
85
+ progress_log_name: progress.jsonl
86
+ id_dev_log_name: id_dev_metrics.jsonl
87
+ checkpoint_name: latest_training_state.pt
88
+ best_model_name: best_model_state.pt
configs/phase21a_robometer_low_smoke_v1.yaml ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ protocol_id: phase21a_robometer_low_lora_smoke_v1
2
+ status: smoke_only
3
+ formal_training_allowed: false
4
+ cosmos3_edge_modification_allowed: false
5
+
6
+ scope:
7
+ phase: Phase 2.1A
8
+ model: Robometer-Low-LoRA
9
+ purpose: select_a_stable_gpu_saturating_micro_batch_before_formal_training
10
+
11
+ data:
12
+ route: Public-Proxy stage2_core_v1
13
+ manifest: manifests/phase2_remediation_pairs_v1.jsonl
14
+ split: train
15
+ real_pairs: true
16
+ frames_per_trajectory: 8
17
+ trajectories_per_pair: 2
18
+ canvas: [256, 256]
19
+ smoke_pair_pool: 8
20
+ smoke_pair_pool_selection: longest_prompt_characters_in_train_split
21
+
22
+ architecture:
23
+ base: Qwen3-VL-4B-Instruct
24
+ robometer_contract:
25
+ joint_trajectory_a_b_forward: true
26
+ per_frame_progress_token: <|prog_token|>
27
+ pair_preference_token: <|pref_token|>
28
+ split_token: <|split_token|>
29
+ progress_head: discrete_10_bin
30
+ success_head: binary
31
+ preference_head: direct_binary
32
+ head_topology: linear_half_layernorm_gelu_dropout_linear
33
+ preference_order_randomization: deterministic_alternation_for_smoke
34
+ low_resource_delta:
35
+ vision_frozen: true
36
+ language_lora:
37
+ rank: 8
38
+ alpha: 16
39
+ dropout: 0.05
40
+ targets: [q_proj, k_proj, v_proj, o_proj]
41
+ newly_added_special_token_rows_trainable: true
42
+ heads_trainable: [progress, success, preference]
43
+ full_language_model_training: false
44
+
45
+ optimization:
46
+ precision: BF16
47
+ attention: flash_attention_2
48
+ gradient_checkpointing: true
49
+ optimizer: AdamW
50
+ head_lr: 1.0e-4
51
+ lora_and_token_lr: 2.0e-6
52
+ weight_decay: 0.01
53
+ gradient_clip: 1.0
54
+ minimum_effective_batch_pairs: 64
55
+
56
+ capacity_selection:
57
+ candidates_micro_batch_pairs_tested: [1, 2, 3, 4, 6, 8, 16, 24, 32, 34, 35]
58
+ adaptive_expansion_reason: micro_batch_8_used_only_41_56_percent_peak_reserved
59
+ each_candidate_in_fresh_process: true
60
+ capacity_probe_optimizer_steps: 1
61
+ stable_requires:
62
+ - finite_total_and_component_losses
63
+ - finite_trainable_gradients
64
+ - all_three_heads_receive_gradients
65
+ - lora_receives_gradients
66
+ - special_token_rows_receive_gradients
67
+ - no_oom
68
+ memory_headroom_rule: peak_reserved_fraction_lte_0.90
69
+ selected_micro_batch_pairs: 32
70
+ selected_gradient_accumulation: 2
71
+ selected_effective_batch_pairs: 64
72
+ selection_override: user_confirmed_2026_08_27
73
+ rejected_adjacent_setting:
74
+ micro_batch_pairs: 33
75
+ gradient_accumulation: 2
76
+ reason: user_selected_micro_batch_32_for_exact_effective_batch_64
77
+ final_validation_optimizer_steps: 1
78
+
79
+ outputs:
80
+ filename_compatibility_note: legacy_paths_retain_robometer_low_prefix
81
+ root: runs/phase21a_robometer_low_smoke_v1
82
+ report_json: reports/phase21a_robometer_low_smoke_v1.json
83
+ report_markdown: reports/PHASE21A_ROBOMETER_LOW_SMOKE_V1.md
environment/phase21a_robometer/.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.10.12
environment/phase21a_robometer/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 2.1A uv environment
2
+
3
+ This directory is the deployable Python environment definition for
4
+ `Robometer-Low-LoRA` and `Robometer-Low`. Do not copy a local `.venv` between
5
+ machines.
6
+
7
+ ## Host requirements
8
+
9
+ - Linux x86_64
10
+ - NVIDIA GPU with BF16 support
11
+ - NVIDIA driver compatible with CUDA 12.8
12
+ - `uv` 0.11.3 or a compatible newer release
13
+ - A CUDA development toolkit is needed only if no compatible prebuilt
14
+ FlashAttention wheel can be selected and a source build is required.
15
+
16
+ ## Rebuild
17
+
18
+ From the experiment root:
19
+
20
+ ```bash
21
+ uv python install 3.10.12
22
+ uv sync --project environment/phase21a_robometer --frozen
23
+ uv run --project environment/phase21a_robometer python environment/phase21a_robometer/doctor.py
24
+ ```
25
+
26
+ The second `uv sync --frozen --dry-run` must report no changes. Run the formal
27
+ training smoke test before starting a seed.
28
+
29
+ The formal trainer sets `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True` before
30
+ initializing CUDA. Keep this setting for the validated micro-batch of 32 pairs;
31
+ removing it can cause allocator-fragmentation OOM near the RTX 5090 capacity
32
+ limit even when several GiB are reserved but unused.
33
+
34
+ The base model, public-proxy dataset, and run output are external artifacts and
35
+ must not be placed inside this environment directory. Pin their Hugging Face
36
+ revisions in the run manifest.
environment/phase21a_robometer/doctor.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Fail fast when a Phase 2.1A deployment cannot run the frozen trainer."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import importlib.metadata as metadata
7
+ import json
8
+ import platform
9
+ import sys
10
+
11
+ import torch
12
+
13
+
14
+ EXPECTED = {
15
+ "accelerate": "1.14.0",
16
+ "bitsandbytes": "0.49.2",
17
+ "flash-attn": "2.8.3.post1",
18
+ "numpy": "1.26.4",
19
+ "peft": "0.19.1",
20
+ "torch": "2.8.0",
21
+ "torchvision": "0.23.0",
22
+ "transformers": "4.57.6",
23
+ }
24
+
25
+
26
+ def normalized(version: str) -> str:
27
+ return version.split("+", maxsplit=1)[0]
28
+
29
+
30
+ def main() -> None:
31
+ actual = {name: metadata.version(name) for name in EXPECTED}
32
+ mismatches = {
33
+ name: {"expected": expected, "actual": actual[name]}
34
+ for name, expected in EXPECTED.items()
35
+ if normalized(actual[name]) != expected
36
+ }
37
+ if mismatches:
38
+ raise RuntimeError(f"package version mismatch: {mismatches}")
39
+ if sys.version_info[:2] != (3, 10):
40
+ raise RuntimeError(f"Python 3.10 is required, found {platform.python_version()}")
41
+ if not torch.cuda.is_available():
42
+ raise RuntimeError("CUDA is unavailable")
43
+ if torch.version.cuda != "12.8":
44
+ raise RuntimeError(f"PyTorch CUDA 12.8 is required, found {torch.version.cuda}")
45
+ if not torch.cuda.is_bf16_supported():
46
+ raise RuntimeError("GPU does not support BF16")
47
+
48
+ import flash_attn
49
+ from flash_attn import flash_attn_func
50
+
51
+ device = torch.device("cuda:0")
52
+ query = torch.randn(1, 16, 2, 64, device=device, dtype=torch.bfloat16)
53
+ key = torch.randn_like(query)
54
+ value = torch.randn_like(query)
55
+ output = flash_attn_func(query, key, value, causal=True)
56
+ if output.shape != query.shape or not torch.isfinite(output).all():
57
+ raise RuntimeError("FlashAttention CUDA probe failed")
58
+
59
+ print(
60
+ json.dumps(
61
+ {
62
+ "status": "pass",
63
+ "python": platform.python_version(),
64
+ "packages": actual,
65
+ "torch_cuda": torch.version.cuda,
66
+ "gpu": torch.cuda.get_device_name(0),
67
+ "compute_capability": torch.cuda.get_device_capability(0),
68
+ "bf16_supported": True,
69
+ "flash_attn": flash_attn.__version__,
70
+ },
71
+ indent=2,
72
+ sort_keys=True,
73
+ )
74
+ )
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
environment/phase21a_robometer/pyproject.toml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "cosmos-robometer-phase21a"
3
+ version = "0.1.0"
4
+ description = "Reproducible runtime for Phase 2.1A Robometer-Low and Robometer-Low-LoRA"
5
+ requires-python = "==3.10.*"
6
+ dependencies = [
7
+ "accelerate==1.14.0",
8
+ "bitsandbytes==0.49.2",
9
+ "flash-attn==2.8.3.post1",
10
+ "hf-xet==1.5.1",
11
+ "huggingface-hub==0.36.2",
12
+ "numpy==1.26.4",
13
+ "peft==0.19.1",
14
+ "pillow==11.3.0",
15
+ "pyyaml==6.0.3",
16
+ "qwen-vl-utils==0.0.14",
17
+ "safetensors==0.8.0",
18
+ "torch==2.8.0",
19
+ "torchvision==0.23.0",
20
+ "transformers==4.57.6",
21
+ ]
22
+
23
+ [tool.uv]
24
+ package = false
25
+
26
+ # flash-attn must build or select its wheel against the exact PyTorch runtime.
27
+ [tool.uv.extra-build-dependencies]
28
+ flash-attn = [{ requirement = "torch", match-runtime = true }]
29
+
30
+ [tool.uv.extra-build-variables]
31
+ flash-attn = { FLASH_ATTENTION_SKIP_CUDA_BUILD = "TRUE" }
32
+
33
+ [tool.uv.sources]
34
+ torch = { index = "pytorch-cu128", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }
35
+ torchvision = { index = "pytorch-cu128", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }
36
+
37
+ [[tool.uv.index]]
38
+ name = "pytorch-cu128"
39
+ url = "https://download.pytorch.org/whl/cu128"
40
+ explicit = true
environment/phase21a_robometer/system-manifest.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "environment_id": "phase21a_robometer_uv_v1",
3
+ "generated_at": "2026-08-27T19:48:21+08:00",
4
+ "lock_sha256": "69d9354b3197aa9e7d3b649c8cab76beb50f685bee96e28e7ad55e72421a3b8d",
5
+ "uv_version_used_to_lock": "0.11.3",
6
+ "python": "3.10.12",
7
+ "platform": "linux_x86_64",
8
+ "pytorch": "2.8.0+cu128",
9
+ "torch_cuda": "12.8",
10
+ "flash_attn": "2.8.3.post1",
11
+ "reference_host": {
12
+ "os": "Ubuntu 22.04",
13
+ "gpu": "NVIDIA GeForce RTX 5090",
14
+ "gpu_memory_mib": 32607,
15
+ "compute_capability": "12.0",
16
+ "nvidia_driver": "580.173.02"
17
+ },
18
+ "target_requirements": {
19
+ "bf16": true,
20
+ "nvidia_driver_compatible_with_cuda_12_8": true,
21
+ "flash_attention_cuda_probe_required": true
22
+ },
23
+ "video_decode_dependency": "not_installed_multi_image_training_only",
24
+ "base_model": {
25
+ "repo_id": "unsloth/Qwen3-VL-4B-Instruct",
26
+ "revision": "252d592b59b0233b226875a44ac135cfa1d3f755"
27
+ },
28
+ "robometer_upstream_revision": "a669dffc241d7d76bec12f36efd4084d914d017c",
29
+ "verification": {
30
+ "clean_uv_sync": "pass",
31
+ "second_frozen_sync_no_changes": "pass",
32
+ "uv_pip_check": "pass",
33
+ "bf16_flash_attention_doctor": "pass",
34
+ "phase21a_v2_one_step_and_full_id_dev_smoke": "pass",
35
+ "smoke_peak_reserved_gib": 27.193359375
36
+ }
37
+ }
environment/phase21a_robometer/uv.lock ADDED
@@ -0,0 +1,814 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 3
3
+ requires-python = "==3.10.*"
4
+ resolution-markers = [
5
+ "sys_platform == 'linux' or sys_platform == 'win32'",
6
+ "sys_platform != 'linux' and sys_platform != 'win32'",
7
+ ]
8
+
9
+ [[package]]
10
+ name = "accelerate"
11
+ version = "1.14.0"
12
+ source = { registry = "https://pypi.org/simple" }
13
+ dependencies = [
14
+ { name = "huggingface-hub" },
15
+ { name = "numpy" },
16
+ { name = "packaging" },
17
+ { name = "psutil" },
18
+ { name = "pyyaml" },
19
+ { name = "safetensors" },
20
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
21
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
22
+ ]
23
+ sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "av"
30
+ version = "17.1.0"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ sdist = { url = "https://files.pythonhosted.org/packages/5e/e3/477fa20578c284abeda08d91b63ee9abaebc93445d8feeb989d3d444bae1/av-17.1.0.tar.gz", hash = "sha256:7f1e71ff621b66253333926f948e00faae11d855b2442133c65128bca64cdeb3", size = 4288546, upload-time = "2026-06-07T05:52:55.999Z" }
33
+ wheels = [
34
+ { url = "https://files.pythonhosted.org/packages/ca/92/c9d0cea4f6f8f93f5b15a39f99d2d593f922484f22a2d98a8d482283e15b/av-17.1.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:19c84fd72af5ef81a20f18fbc6f9aedff9e1455e53a7062c1d4c95926d73da4e", size = 22622703, upload-time = "2026-06-07T05:51:40.405Z" },
35
+ { url = "https://files.pythonhosted.org/packages/dc/57/74399770aa103ee4b5ff6da1781440c91a41901d89abb2433fe88773246e/av-17.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:19264c9bb4bee404accc7ce9ec461f2044b7f577a70234d29aafde31ed17de46", size = 18273538, upload-time = "2026-06-07T05:51:43.078Z" },
36
+ { url = "https://files.pythonhosted.org/packages/eb/17/27c85b12e9ffa8f3f6854358b3eabcd91f3c29c7dac36843fa1376e833f4/av-17.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:22dff0ae582d10ef08c75c2150a4fd27cfc26653b54930c7c27b9f7b3aa20723", size = 34519101, upload-time = "2026-06-07T05:51:45.305Z" },
37
+ { url = "https://files.pythonhosted.org/packages/04/a4/542d4bfd9f4aec5f3265985b9dbc6b259d45c2e668f9714e5f4e05b71e64/av-17.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:90c49bc9608377d01e82e747377505419a229464873341db18202d5dddecce5a", size = 36647600, upload-time = "2026-06-07T05:51:48.57Z" },
38
+ { url = "https://files.pythonhosted.org/packages/63/1e/63bd5c59580f38109fa4c452b29b715a20c9a5eb3a078b3c447484593c40/av-17.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cc5a5247622cb77e24c342364eb68f88c1442ddfaab60c1f1f483359d3cc7879", size = 25786289, upload-time = "2026-06-07T05:51:51.674Z" },
39
+ { url = "https://files.pythonhosted.org/packages/70/30/78155cef0c9f8bc13f044130192c58bf962f2c9066982ff3593afe8d27f1/av-17.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff457ed419348e5b8e8c811d341389b052c5e4d5839da3794d019b125b9fe830", size = 35599848, upload-time = "2026-06-07T05:51:54.207Z" },
40
+ { url = "https://files.pythonhosted.org/packages/76/cb/ae1d7a735a5ad9dc502dba864c51d605cbe932a769218352fd570254c38e/av-17.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1370b11a697eb3f2555906f8ab3519b0cfe48425d7830a3996ad42e6bffafda5", size = 26776479, upload-time = "2026-06-07T05:51:56.788Z" },
41
+ { url = "https://files.pythonhosted.org/packages/fb/40/128429b9eb0c4a2beb122ed8d04b189515df68967987c2654a2e262a5c43/av-17.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3dcd41e53f53f9a3260751d9c3c11d34e93d70d61e506c81f13dbc1e3606e07b", size = 37763744, upload-time = "2026-06-07T05:51:59.222Z" },
42
+ { url = "https://files.pythonhosted.org/packages/01/6a/5980e7bbeeadfd7a9db8e38e9f1140a3e0c392fccc31bd7b1e4a75cf5a96/av-17.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:3453b06075c7bb973fdb6de52563f7692ff05cbc64c0bb45f4fd6e8709131f2f", size = 28126516, upload-time = "2026-06-07T05:52:01.658Z" },
43
+ { url = "https://files.pythonhosted.org/packages/ec/87/8036b5c781bc3639ea04ef42d4e26da253bd4bd4311d8705b6a1c8824047/av-17.1.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ad7b4aa011093324b7118245f50ac6db244cfe9900d4072508a5245a2b0d3f41", size = 22460847, upload-time = "2026-06-07T05:52:04.261Z" },
44
+ { url = "https://files.pythonhosted.org/packages/6d/af/dfdf6fc7b17814b50d0aa9e7a7e37b87be91be3890f44b0d525433cd1fd1/av-17.1.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:43ebbe977f19a7f2d2bd1a4e119675a0b15e05852cf7309846b6ab922ba7ffe9", size = 18159115, upload-time = "2026-06-07T05:52:06.64Z" },
45
+ { url = "https://files.pythonhosted.org/packages/ad/13/64f6c466471cea225b8b2f4cdc51a571f8a286984b55a08d169b932fda5d/av-17.1.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a20658ec7d96a70e14b1196eff00b7cdd8831ac3b99868e16b8ba8b24090847", size = 33224427, upload-time = "2026-06-07T05:52:09.165Z" },
46
+ { url = "https://files.pythonhosted.org/packages/77/43/96b35170bf2e64e00a41748c6400ff73232dc0fc62ded283679fb07c7fe0/av-17.1.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f9a65d1f48b818323fb411e80358f89d77dec340b01d27c6b2dfbb9cbf4b779f", size = 35370183, upload-time = "2026-06-07T05:52:11.959Z" },
47
+ { url = "https://files.pythonhosted.org/packages/2e/b3/8e8b4b6498731bfbd88e8399a756543f8088f1bd33d08eab678b5aebe728/av-17.1.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:58f7593726437cda5bd19793027e027768450b5c4a594777bf487798a33db702", size = 24459265, upload-time = "2026-06-07T05:52:14.66Z" },
48
+ { url = "https://files.pythonhosted.org/packages/14/ac/ceb84b7553db21f1143d817245c560d9267168e1e58b1a8eeae2b62c4d04/av-17.1.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bbab058bd965309f39962e53caac8126987c68c0be094fc4f9427e5615b0218f", size = 34283709, upload-time = "2026-06-07T05:52:17.389Z" },
49
+ { url = "https://files.pythonhosted.org/packages/59/f9/4115fd84148c9a1cf365096694be6ac882fd3cd3cdb7a2f35e71fecf1631/av-17.1.0-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9514cfda85180554c430695282faf4be3ffdf95775d8519733821244eecb58e0", size = 25397573, upload-time = "2026-06-07T05:52:20.012Z" },
50
+ { url = "https://files.pythonhosted.org/packages/e2/ac/92e52d5ed0e0b84d9d93e52b4338c2713d8a44082b8696e6516fdae7c4e4/av-17.1.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e1c90f85cd7431ede95b11e8e711571a896ebea433f298849c2c0f1594c8d86e", size = 36451495, upload-time = "2026-06-07T05:52:22.581Z" },
51
+ { url = "https://files.pythonhosted.org/packages/6b/f2/53a7cd34adb6a971d7e6d99663e74db286966c9db8afdca17472fdf0f98e/av-17.1.0-cp311-abi3-win_amd64.whl", hash = "sha256:5df5c1172ef1cf65a1529d612f7da7798ce2cf82c1ff7212466b538a6cc7214c", size = 28036393, upload-time = "2026-06-07T05:52:25.657Z" },
52
+ { url = "https://files.pythonhosted.org/packages/66/47/cd9ae0edf2206351c1251bb94b5ec58728e42c5f6ee16c03c412f3a1bb3e/av-17.1.0-cp311-abi3-win_arm64.whl", hash = "sha256:ee98534242a74da847af78624779ac5a3177dc7c69f956a4da9e6f0fdb37d7f6", size = 21174601, upload-time = "2026-06-07T05:52:28.077Z" },
53
+ ]
54
+
55
+ [[package]]
56
+ name = "bitsandbytes"
57
+ version = "0.49.2"
58
+ source = { registry = "https://pypi.org/simple" }
59
+ dependencies = [
60
+ { name = "numpy" },
61
+ { name = "packaging" },
62
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
63
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
64
+ ]
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/d8/7d/f1fe0992334b18cd8494f89aeec1dcc674635584fcd9f115784fea3a1d05/bitsandbytes-0.49.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:87be5975edeac5396d699ecbc39dfc47cf2c026daaf2d5852a94368611a6823f", size = 131940, upload-time = "2026-02-16T21:26:04.572Z" },
67
+ { url = "https://files.pythonhosted.org/packages/29/71/acff7af06c818664aa87ff73e17a52c7788ad746b72aea09d3cb8e424348/bitsandbytes-0.49.2-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:2fc0830c5f7169be36e60e11f2be067c8f812dfcb829801a8703735842450750", size = 31442815, upload-time = "2026-02-16T21:26:06.783Z" },
68
+ { url = "https://files.pythonhosted.org/packages/19/57/3443d6f183436fbdaf5000aac332c4d5ddb056665d459244a5608e98ae92/bitsandbytes-0.49.2-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:54b771f06e1a3c73af5c7f16ccf0fc23a846052813d4b008d10cb6e017dd1c8c", size = 60651714, upload-time = "2026-02-16T21:26:11.579Z" },
69
+ { url = "https://files.pythonhosted.org/packages/b6/d4/501655842ad6771fb077f576d78cbedb5445d15b1c3c91343ed58ca46f0e/bitsandbytes-0.49.2-py3-none-win_amd64.whl", hash = "sha256:2e0ddd09cd778155388023cbe81f00afbb7c000c214caef3ce83386e7144df7d", size = 55372289, upload-time = "2026-02-16T21:26:16.267Z" },
70
+ ]
71
+
72
+ [[package]]
73
+ name = "certifi"
74
+ version = "2026.7.22"
75
+ source = { registry = "https://pypi.org/simple" }
76
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
77
+ wheels = [
78
+ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
79
+ ]
80
+
81
+ [[package]]
82
+ name = "charset-normalizer"
83
+ version = "3.5.1"
84
+ source = { registry = "https://pypi.org/simple" }
85
+ sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" }
86
+ wheels = [
87
+ { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" },
88
+ { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" },
89
+ { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" },
90
+ { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" },
91
+ { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" },
92
+ { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" },
93
+ { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" },
94
+ { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" },
95
+ { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" },
96
+ { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" },
97
+ { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" },
98
+ { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" },
99
+ { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" },
100
+ { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" },
101
+ { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" },
102
+ { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" },
103
+ { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" },
104
+ { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" },
105
+ { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" },
106
+ { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" },
107
+ { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" },
108
+ { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" },
109
+ { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" },
110
+ { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" },
111
+ { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" },
112
+ { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" },
113
+ { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" },
114
+ { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" },
115
+ { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" },
116
+ { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" },
117
+ { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" },
118
+ { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" },
119
+ { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" },
120
+ ]
121
+
122
+ [[package]]
123
+ name = "colorama"
124
+ version = "0.4.6"
125
+ source = { registry = "https://pypi.org/simple" }
126
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
127
+ wheels = [
128
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
129
+ ]
130
+
131
+ [[package]]
132
+ name = "cosmos-robometer-phase21a"
133
+ version = "0.1.0"
134
+ source = { virtual = "." }
135
+ dependencies = [
136
+ { name = "accelerate" },
137
+ { name = "bitsandbytes" },
138
+ { name = "flash-attn" },
139
+ { name = "hf-xet" },
140
+ { name = "huggingface-hub" },
141
+ { name = "numpy" },
142
+ { name = "peft" },
143
+ { name = "pillow" },
144
+ { name = "pyyaml" },
145
+ { name = "qwen-vl-utils" },
146
+ { name = "safetensors" },
147
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
148
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
149
+ { name = "torchvision", version = "0.23.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
150
+ { name = "torchvision", version = "0.23.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
151
+ { name = "transformers" },
152
+ ]
153
+
154
+ [package.metadata]
155
+ requires-dist = [
156
+ { name = "accelerate", specifier = "==1.14.0" },
157
+ { name = "bitsandbytes", specifier = "==0.49.2" },
158
+ { name = "flash-attn", specifier = "==2.8.3.post1" },
159
+ { name = "hf-xet", specifier = "==1.5.1" },
160
+ { name = "huggingface-hub", specifier = "==0.36.2" },
161
+ { name = "numpy", specifier = "==1.26.4" },
162
+ { name = "peft", specifier = "==0.19.1" },
163
+ { name = "pillow", specifier = "==11.3.0" },
164
+ { name = "pyyaml", specifier = "==6.0.3" },
165
+ { name = "qwen-vl-utils", specifier = "==0.0.14" },
166
+ { name = "safetensors", specifier = "==0.8.0" },
167
+ { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32'", specifier = "==2.8.0" },
168
+ { name = "torch", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = "==2.8.0", index = "https://download.pytorch.org/whl/cu128" },
169
+ { name = "torchvision", marker = "sys_platform != 'linux' and sys_platform != 'win32'", specifier = "==0.23.0" },
170
+ { name = "torchvision", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = "==0.23.0", index = "https://download.pytorch.org/whl/cu128" },
171
+ { name = "transformers", specifier = "==4.57.6" },
172
+ ]
173
+
174
+ [[package]]
175
+ name = "einops"
176
+ version = "0.8.2"
177
+ source = { registry = "https://pypi.org/simple" }
178
+ sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" }
179
+ wheels = [
180
+ { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" },
181
+ ]
182
+
183
+ [[package]]
184
+ name = "filelock"
185
+ version = "3.32.4"
186
+ source = { registry = "https://pypi.org/simple" }
187
+ sdist = { url = "https://files.pythonhosted.org/packages/6d/30/03b03951873a1a0ffc7e8ca0e10c15597b59e8d0e39260704cd2ea087bc4/filelock-3.32.4.tar.gz", hash = "sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30", size = 222126, upload-time = "2026-08-23T17:37:55.363Z" }
188
+ wheels = [
189
+ { url = "https://files.pythonhosted.org/packages/01/a4/9b63d595d748e3aff8812b65eacc1a2c4bd90b7c2012e08e72373b4835eb/filelock-3.32.4-py3-none-any.whl", hash = "sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd", size = 99864, upload-time = "2026-08-23T17:37:53.913Z" },
190
+ ]
191
+
192
+ [[package]]
193
+ name = "flash-attn"
194
+ version = "2.8.3.post1"
195
+ source = { registry = "https://pypi.org/simple" }
196
+ dependencies = [
197
+ { name = "einops" },
198
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
199
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
200
+ ]
201
+ sdist = { url = "https://files.pythonhosted.org/packages/01/7a/92a46e7cd6bbb4d7b2855a457c3b855df54a97af5656d98fc92e58e61065/flash_attn-2.8.3.post1.tar.gz", hash = "sha256:55d5103ed846da8b56e0797acf4bde07dee4b1c7e8907fcfc6699c203030c348", size = 8451657, upload-time = "2026-06-11T04:35:21.318Z" }
202
+
203
+ [[package]]
204
+ name = "fsspec"
205
+ version = "2026.7.0"
206
+ source = { registry = "https://pypi.org/simple" }
207
+ sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" }
208
+ wheels = [
209
+ { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" },
210
+ ]
211
+
212
+ [[package]]
213
+ name = "hf-xet"
214
+ version = "1.5.1"
215
+ source = { registry = "https://pypi.org/simple" }
216
+ sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" }
217
+ wheels = [
218
+ { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" },
219
+ { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" },
220
+ { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" },
221
+ { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" },
222
+ { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" },
223
+ { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" },
224
+ { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" },
225
+ { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" },
226
+ ]
227
+
228
+ [[package]]
229
+ name = "huggingface-hub"
230
+ version = "0.36.2"
231
+ source = { registry = "https://pypi.org/simple" }
232
+ dependencies = [
233
+ { name = "filelock" },
234
+ { name = "fsspec" },
235
+ { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
236
+ { name = "packaging" },
237
+ { name = "pyyaml" },
238
+ { name = "requests" },
239
+ { name = "tqdm" },
240
+ { name = "typing-extensions" },
241
+ ]
242
+ sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" }
243
+ wheels = [
244
+ { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
245
+ ]
246
+
247
+ [[package]]
248
+ name = "idna"
249
+ version = "3.19"
250
+ source = { registry = "https://pypi.org/simple" }
251
+ sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
252
+ wheels = [
253
+ { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
254
+ ]
255
+
256
+ [[package]]
257
+ name = "jinja2"
258
+ version = "3.1.6"
259
+ source = { registry = "https://pypi.org/simple" }
260
+ dependencies = [
261
+ { name = "markupsafe" },
262
+ ]
263
+ sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
264
+ wheels = [
265
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
266
+ ]
267
+
268
+ [[package]]
269
+ name = "markupsafe"
270
+ version = "3.0.3"
271
+ source = { registry = "https://pypi.org/simple" }
272
+ sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
273
+ wheels = [
274
+ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
275
+ { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
276
+ { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
277
+ { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
278
+ { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
279
+ { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
280
+ { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
281
+ { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
282
+ { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
283
+ { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
284
+ { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
285
+ ]
286
+
287
+ [[package]]
288
+ name = "mpmath"
289
+ version = "1.3.0"
290
+ source = { registry = "https://pypi.org/simple" }
291
+ sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
292
+ wheels = [
293
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
294
+ ]
295
+
296
+ [[package]]
297
+ name = "networkx"
298
+ version = "3.4.2"
299
+ source = { registry = "https://pypi.org/simple" }
300
+ sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" }
301
+ wheels = [
302
+ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" },
303
+ ]
304
+
305
+ [[package]]
306
+ name = "numpy"
307
+ version = "1.26.4"
308
+ source = { registry = "https://pypi.org/simple" }
309
+ sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
310
+ wheels = [
311
+ { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" },
312
+ { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" },
313
+ { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" },
314
+ { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" },
315
+ { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" },
316
+ { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" },
317
+ { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" },
318
+ { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" },
319
+ ]
320
+
321
+ [[package]]
322
+ name = "nvidia-cublas-cu12"
323
+ version = "12.8.4.1"
324
+ source = { registry = "https://pypi.org/simple" }
325
+ wheels = [
326
+ { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" },
327
+ ]
328
+
329
+ [[package]]
330
+ name = "nvidia-cuda-cupti-cu12"
331
+ version = "12.8.90"
332
+ source = { registry = "https://pypi.org/simple" }
333
+ wheels = [
334
+ { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" },
335
+ ]
336
+
337
+ [[package]]
338
+ name = "nvidia-cuda-nvrtc-cu12"
339
+ version = "12.8.93"
340
+ source = { registry = "https://pypi.org/simple" }
341
+ wheels = [
342
+ { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" },
343
+ ]
344
+
345
+ [[package]]
346
+ name = "nvidia-cuda-runtime-cu12"
347
+ version = "12.8.90"
348
+ source = { registry = "https://pypi.org/simple" }
349
+ wheels = [
350
+ { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" },
351
+ ]
352
+
353
+ [[package]]
354
+ name = "nvidia-cudnn-cu12"
355
+ version = "9.10.2.21"
356
+ source = { registry = "https://pypi.org/simple" }
357
+ dependencies = [
358
+ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
359
+ ]
360
+ wheels = [
361
+ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
362
+ ]
363
+
364
+ [[package]]
365
+ name = "nvidia-cufft-cu12"
366
+ version = "11.3.3.83"
367
+ source = { registry = "https://pypi.org/simple" }
368
+ dependencies = [
369
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
370
+ ]
371
+ wheels = [
372
+ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
373
+ ]
374
+
375
+ [[package]]
376
+ name = "nvidia-cufile-cu12"
377
+ version = "1.13.1.3"
378
+ source = { registry = "https://pypi.org/simple" }
379
+ wheels = [
380
+ { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" },
381
+ ]
382
+
383
+ [[package]]
384
+ name = "nvidia-curand-cu12"
385
+ version = "10.3.9.90"
386
+ source = { registry = "https://pypi.org/simple" }
387
+ wheels = [
388
+ { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" },
389
+ ]
390
+
391
+ [[package]]
392
+ name = "nvidia-cusolver-cu12"
393
+ version = "11.7.3.90"
394
+ source = { registry = "https://pypi.org/simple" }
395
+ dependencies = [
396
+ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
397
+ { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
398
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
399
+ ]
400
+ wheels = [
401
+ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
402
+ ]
403
+
404
+ [[package]]
405
+ name = "nvidia-cusparse-cu12"
406
+ version = "12.5.8.93"
407
+ source = { registry = "https://pypi.org/simple" }
408
+ dependencies = [
409
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
410
+ ]
411
+ wheels = [
412
+ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
413
+ ]
414
+
415
+ [[package]]
416
+ name = "nvidia-cusparselt-cu12"
417
+ version = "0.7.1"
418
+ source = { registry = "https://pypi.org/simple" }
419
+ wheels = [
420
+ { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" },
421
+ ]
422
+
423
+ [[package]]
424
+ name = "nvidia-nccl-cu12"
425
+ version = "2.27.3"
426
+ source = { registry = "https://pypi.org/simple" }
427
+ wheels = [
428
+ { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" },
429
+ ]
430
+
431
+ [[package]]
432
+ name = "nvidia-nvjitlink-cu12"
433
+ version = "12.8.93"
434
+ source = { registry = "https://pypi.org/simple" }
435
+ wheels = [
436
+ { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" },
437
+ ]
438
+
439
+ [[package]]
440
+ name = "nvidia-nvtx-cu12"
441
+ version = "12.8.90"
442
+ source = { registry = "https://pypi.org/simple" }
443
+ wheels = [
444
+ { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" },
445
+ ]
446
+
447
+ [[package]]
448
+ name = "packaging"
449
+ version = "26.3"
450
+ source = { registry = "https://pypi.org/simple" }
451
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
452
+ wheels = [
453
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
454
+ ]
455
+
456
+ [[package]]
457
+ name = "peft"
458
+ version = "0.19.1"
459
+ source = { registry = "https://pypi.org/simple" }
460
+ dependencies = [
461
+ { name = "accelerate" },
462
+ { name = "huggingface-hub" },
463
+ { name = "numpy" },
464
+ { name = "packaging" },
465
+ { name = "psutil" },
466
+ { name = "pyyaml" },
467
+ { name = "safetensors" },
468
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
469
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
470
+ { name = "tqdm" },
471
+ { name = "transformers" },
472
+ ]
473
+ sdist = { url = "https://files.pythonhosted.org/packages/86/cf/037f1e3d5186496c05513a6754639e2dab3038a05f384284d49a9bd06a2d/peft-0.19.1.tar.gz", hash = "sha256:0d97542fe96dcdaa20d3b81c06f26f988618f416a73544ab23c3618ccb674a40", size = 763738, upload-time = "2026-04-16T15:46:45.105Z" }
474
+ wheels = [
475
+ { url = "https://files.pythonhosted.org/packages/e8/b6/f54d676ed93cc2dd2234c3b172ea9c8c3d7d29361e66b1b23dec57a67465/peft-0.19.1-py3-none-any.whl", hash = "sha256:2113f72a81621b5913ef28f9022204c742df111890c5f49d812716a4a301e356", size = 680692, upload-time = "2026-04-16T15:46:42.886Z" },
476
+ ]
477
+
478
+ [[package]]
479
+ name = "pillow"
480
+ version = "11.3.0"
481
+ source = { registry = "https://pypi.org/simple" }
482
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
483
+ wheels = [
484
+ { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" },
485
+ { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" },
486
+ { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" },
487
+ { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" },
488
+ { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" },
489
+ { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" },
490
+ { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" },
491
+ { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" },
492
+ { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" },
493
+ { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" },
494
+ { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" },
495
+ { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" },
496
+ { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" },
497
+ { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" },
498
+ { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" },
499
+ { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" },
500
+ { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" },
501
+ { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" },
502
+ ]
503
+
504
+ [[package]]
505
+ name = "psutil"
506
+ version = "7.2.2"
507
+ source = { registry = "https://pypi.org/simple" }
508
+ sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
509
+ wheels = [
510
+ { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
511
+ { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
512
+ { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
513
+ { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
514
+ { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
515
+ { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
516
+ { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
517
+ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
518
+ ]
519
+
520
+ [[package]]
521
+ name = "pyyaml"
522
+ version = "6.0.3"
523
+ source = { registry = "https://pypi.org/simple" }
524
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
525
+ wheels = [
526
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
527
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
528
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
529
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
530
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
531
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
532
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
533
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
534
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
535
+ ]
536
+
537
+ [[package]]
538
+ name = "qwen-vl-utils"
539
+ version = "0.0.14"
540
+ source = { registry = "https://pypi.org/simple" }
541
+ dependencies = [
542
+ { name = "av" },
543
+ { name = "packaging" },
544
+ { name = "pillow" },
545
+ { name = "requests" },
546
+ ]
547
+ sdist = { url = "https://files.pythonhosted.org/packages/b6/b1/ad4fc2260a3badd278b38d642f3b987412f1f6682f0ef2b31b0572d5caa8/qwen_vl_utils-0.0.14.tar.gz", hash = "sha256:9c7cad5ae803b3a10f8bb7194deb12aeacdd032f92f4224e880c73587a7346ad", size = 8453, upload-time = "2025-09-23T09:38:57.532Z" }
548
+ wheels = [
549
+ { url = "https://files.pythonhosted.org/packages/c4/43/80f67e0336cb2fc725f8e06f7fe35c1d0fe946f4d2b8b2175e797e07349e/qwen_vl_utils-0.0.14-py3-none-any.whl", hash = "sha256:5e28657bfd031e56bd447c5901b58ddfc3835285ed100f4c56580e0ade054e96", size = 8120, upload-time = "2025-09-23T09:38:56.297Z" },
550
+ ]
551
+
552
+ [[package]]
553
+ name = "regex"
554
+ version = "2026.7.19"
555
+ source = { registry = "https://pypi.org/simple" }
556
+ sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" }
557
+ wheels = [
558
+ { url = "https://files.pythonhosted.org/packages/24/13/bbf7d9d1887fe4a3693527c6caa232c197ea9da91f1212e9672eff60329d/regex-2026.7.19-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b", size = 494009, upload-time = "2026-07-19T00:16:13.602Z" },
559
+ { url = "https://files.pythonhosted.org/packages/a3/19/783688e75a2bec15d50aec0d5e7e317d363808bc82a6eb6750b897bfcd7b/regex-2026.7.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52", size = 295287, upload-time = "2026-07-19T00:16:15.702Z" },
560
+ { url = "https://files.pythonhosted.org/packages/16/01/cefe4f051302ca298d3f3e79ed6dbd933ac84485b9515acdd6a52d70cef7/regex-2026.7.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665", size = 290633, upload-time = "2026-07-19T00:16:17.182Z" },
561
+ { url = "https://files.pythonhosted.org/packages/ae/1e/1045ca2cabb12e8ec41ad0d138e9f3ff1eb079d30a6f51ccf7d709b44aad/regex-2026.7.19-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0", size = 785300, upload-time = "2026-07-19T00:16:18.505Z" },
562
+ { url = "https://files.pythonhosted.org/packages/d5/4a/20e5bca184e90bf1bd187efdb53363f4a7b7b34f01d54ced5740caf104bd/regex-2026.7.19-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951", size = 854079, upload-time = "2026-07-19T00:16:19.909Z" },
563
+ { url = "https://files.pythonhosted.org/packages/09/9b/5a2e59678be3b24aa6a42b2c6d66a48daa212593e9f4096fe7ba577fa9b1/regex-2026.7.19-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3", size = 899496, upload-time = "2026-07-19T00:16:21.453Z" },
564
+ { url = "https://files.pythonhosted.org/packages/48/9a/7317f14ed8ed9fd998d1978b4802b07bc4d79216353c435dbcc1ddd1301f/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c", size = 793541, upload-time = "2026-07-19T00:16:22.991Z" },
565
+ { url = "https://files.pythonhosted.org/packages/6f/53/833c2db3e274d3c191f4c42fe5bfa358e4c8b617d5d7312d31334965fc46/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6", size = 785515, upload-time = "2026-07-19T00:16:24.654Z" },
566
+ { url = "https://files.pythonhosted.org/packages/8d/b9/efb2f9fa151d71db09d4015e1fb92fee47416f01c12164836bbd23e2f3c2/regex-2026.7.19-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175", size = 769556, upload-time = "2026-07-19T00:16:26.207Z" },
567
+ { url = "https://files.pythonhosted.org/packages/81/4d/45610c263f8eadb84e4a1fabd904d81d5176226faa9104ef498bf8a8b285/regex-2026.7.19-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6", size = 774130, upload-time = "2026-07-19T00:16:27.786Z" },
568
+ { url = "https://files.pythonhosted.org/packages/40/95/1b40d87c7a9e5480bec7a87bce9fd67fc3f14b5f106c8ee66d660249072f/regex-2026.7.19-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095", size = 848694, upload-time = "2026-07-19T00:16:29.412Z" },
569
+ { url = "https://files.pythonhosted.org/packages/17/8b/bb45968addd5b394ef9cd9184bd9c65ade1a819dbb2b92b71ad52a0c7907/regex-2026.7.19-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0", size = 758505, upload-time = "2026-07-19T00:16:31.006Z" },
570
+ { url = "https://files.pythonhosted.org/packages/bf/6f/33386c672fbf43e21602135a0f29a97ee251a483f007fe51d10e9b2dbc93/regex-2026.7.19-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a", size = 836985, upload-time = "2026-07-19T00:16:32.459Z" },
571
+ { url = "https://files.pythonhosted.org/packages/22/f1/9112b86e9bb075619862e8e42b604794389f1958faa68fb69bde505dd90e/regex-2026.7.19-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902", size = 782610, upload-time = "2026-07-19T00:16:33.857Z" },
572
+ { url = "https://files.pythonhosted.org/packages/3a/f9/13d460d8a385ca0b0be9e6be80a90968b9293b3e30895543ad2d1d1653e4/regex-2026.7.19-cp310-cp310-win32.whl", hash = "sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e", size = 266772, upload-time = "2026-07-19T00:16:35.194Z" },
573
+ { url = "https://files.pythonhosted.org/packages/9f/90/29addd7a03e1aea402c1f31467e25c80caabc8c3735b88a23cf73b0aa9c2/regex-2026.7.19-cp310-cp310-win_amd64.whl", hash = "sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db", size = 277967, upload-time = "2026-07-19T00:16:36.847Z" },
574
+ { url = "https://files.pythonhosted.org/packages/8c/ba/ecfce06fe66c122bc6f77ae284887a9282e4411fe1e6268c5266611ca054/regex-2026.7.19-cp310-cp310-win_arm64.whl", hash = "sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6", size = 276963, upload-time = "2026-07-19T00:16:38.315Z" },
575
+ ]
576
+
577
+ [[package]]
578
+ name = "requests"
579
+ version = "2.34.2"
580
+ source = { registry = "https://pypi.org/simple" }
581
+ dependencies = [
582
+ { name = "certifi" },
583
+ { name = "charset-normalizer" },
584
+ { name = "idna" },
585
+ { name = "urllib3" },
586
+ ]
587
+ sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
588
+ wheels = [
589
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
590
+ ]
591
+
592
+ [[package]]
593
+ name = "safetensors"
594
+ version = "0.8.0"
595
+ source = { registry = "https://pypi.org/simple" }
596
+ sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" }
597
+ wheels = [
598
+ { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" },
599
+ { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" },
600
+ { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" },
601
+ { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" },
602
+ { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" },
603
+ { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" },
604
+ { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" },
605
+ { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" },
606
+ { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" },
607
+ { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" },
608
+ { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" },
609
+ { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" },
610
+ { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" },
611
+ { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" },
612
+ { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" },
613
+ { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" },
614
+ ]
615
+
616
+ [[package]]
617
+ name = "setuptools"
618
+ version = "84.0.0"
619
+ source = { registry = "https://pypi.org/simple" }
620
+ sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
621
+ wheels = [
622
+ { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
623
+ ]
624
+
625
+ [[package]]
626
+ name = "sympy"
627
+ version = "1.14.0"
628
+ source = { registry = "https://pypi.org/simple" }
629
+ dependencies = [
630
+ { name = "mpmath" },
631
+ ]
632
+ sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
633
+ wheels = [
634
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
635
+ ]
636
+
637
+ [[package]]
638
+ name = "tokenizers"
639
+ version = "0.22.2"
640
+ source = { registry = "https://pypi.org/simple" }
641
+ dependencies = [
642
+ { name = "huggingface-hub" },
643
+ ]
644
+ sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
645
+ wheels = [
646
+ { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
647
+ { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
648
+ { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
649
+ { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
650
+ { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
651
+ { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
652
+ { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
653
+ { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
654
+ { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
655
+ { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
656
+ { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
657
+ { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
658
+ { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
659
+ { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
660
+ { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
661
+ { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" },
662
+ { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" },
663
+ { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" },
664
+ { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" },
665
+ ]
666
+
667
+ [[package]]
668
+ name = "torch"
669
+ version = "2.8.0"
670
+ source = { registry = "https://pypi.org/simple" }
671
+ resolution-markers = [
672
+ "sys_platform != 'linux' and sys_platform != 'win32'",
673
+ ]
674
+ dependencies = [
675
+ { name = "filelock", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
676
+ { name = "fsspec", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
677
+ { name = "jinja2", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
678
+ { name = "networkx", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
679
+ { name = "sympy", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
680
+ { name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
681
+ ]
682
+ wheels = [
683
+ { url = "https://files.pythonhosted.org/packages/ef/d6/e6d4c57e61c2b2175d3aafbfb779926a2cfd7c32eeda7c543925dceec923/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760", size = 73611154, upload-time = "2025-08-06T14:53:10.919Z" },
684
+ ]
685
+
686
+ [[package]]
687
+ name = "torch"
688
+ version = "2.8.0+cu128"
689
+ source = { registry = "https://download.pytorch.org/whl/cu128" }
690
+ resolution-markers = [
691
+ "sys_platform == 'linux' or sys_platform == 'win32'",
692
+ ]
693
+ dependencies = [
694
+ { name = "filelock", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
695
+ { name = "fsspec", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
696
+ { name = "jinja2", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
697
+ { name = "networkx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
698
+ { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
699
+ { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
700
+ { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
701
+ { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
702
+ { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
703
+ { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
704
+ { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
705
+ { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
706
+ { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
707
+ { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
708
+ { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
709
+ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
710
+ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
711
+ { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
712
+ { name = "sympy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
713
+ { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
714
+ { name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
715
+ ]
716
+ wheels = [
717
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0c96999d15cf1f13dd7c913e0b21a9a355538e6cfc10861a17158320292f5954", upload-time = "2025-10-01T23:47:19Z" },
718
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:43938e9a174c90e5eb9e906532b2f1e21532bbfa5a61b65193b4f54714d34f9e", upload-time = "2025-10-01T23:47:42Z" },
719
+ ]
720
+
721
+ [[package]]
722
+ name = "torchvision"
723
+ version = "0.23.0"
724
+ source = { registry = "https://pypi.org/simple" }
725
+ resolution-markers = [
726
+ "sys_platform != 'linux' and sys_platform != 'win32'",
727
+ ]
728
+ dependencies = [
729
+ { name = "numpy", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
730
+ { name = "pillow", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
731
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
732
+ ]
733
+ wheels = [
734
+ { url = "https://files.pythonhosted.org/packages/4d/49/5ad5c3ff4920be0adee9eb4339b4fb3b023a0fc55b9ed8dbc73df92946b8/torchvision-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7266871daca00ad46d1c073e55d972179d12a58fa5c9adec9a3db9bbed71284a", size = 1856885, upload-time = "2025-08-06T14:57:55.024Z" },
735
+ ]
736
+
737
+ [[package]]
738
+ name = "torchvision"
739
+ version = "0.23.0+cu128"
740
+ source = { registry = "https://download.pytorch.org/whl/cu128" }
741
+ resolution-markers = [
742
+ "sys_platform == 'linux' or sys_platform == 'win32'",
743
+ ]
744
+ dependencies = [
745
+ { name = "numpy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
746
+ { name = "pillow", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
747
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
748
+ ]
749
+ wheels = [
750
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.23.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:460bc8d70f63bdb433a7351decc2c1ae1903f7f378e4a7614fc8e8c97a5c36aa", upload-time = "2025-08-05T20:11:51Z" },
751
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.23.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:8ec6f2281ef5d52471b01b99eb04243d0c2cccb1972ba43217085025fe5a6c3f", upload-time = "2025-08-05T20:11:51Z" },
752
+ ]
753
+
754
+ [[package]]
755
+ name = "tqdm"
756
+ version = "4.70.0"
757
+ source = { registry = "https://pypi.org/simple" }
758
+ dependencies = [
759
+ { name = "colorama", marker = "sys_platform == 'win32'" },
760
+ ]
761
+ sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
762
+ wheels = [
763
+ { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
764
+ ]
765
+
766
+ [[package]]
767
+ name = "transformers"
768
+ version = "4.57.6"
769
+ source = { registry = "https://pypi.org/simple" }
770
+ dependencies = [
771
+ { name = "filelock" },
772
+ { name = "huggingface-hub" },
773
+ { name = "numpy" },
774
+ { name = "packaging" },
775
+ { name = "pyyaml" },
776
+ { name = "regex" },
777
+ { name = "requests" },
778
+ { name = "safetensors" },
779
+ { name = "tokenizers" },
780
+ { name = "tqdm" },
781
+ ]
782
+ sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" }
783
+ wheels = [
784
+ { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" },
785
+ ]
786
+
787
+ [[package]]
788
+ name = "triton"
789
+ version = "3.4.0"
790
+ source = { registry = "https://pypi.org/simple" }
791
+ dependencies = [
792
+ { name = "setuptools", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
793
+ ]
794
+ wheels = [
795
+ { url = "https://files.pythonhosted.org/packages/62/ee/0ee5f64a87eeda19bbad9bc54ae5ca5b98186ed00055281fd40fb4beb10e/triton-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ff2785de9bc02f500e085420273bb5cc9c9bb767584a4aa28d6e360cec70128", size = 155430069, upload-time = "2025-07-30T19:58:21.715Z" },
796
+ ]
797
+
798
+ [[package]]
799
+ name = "typing-extensions"
800
+ version = "4.16.0"
801
+ source = { registry = "https://pypi.org/simple" }
802
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
803
+ wheels = [
804
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
805
+ ]
806
+
807
+ [[package]]
808
+ name = "urllib3"
809
+ version = "2.7.0"
810
+ source = { registry = "https://pypi.org/simple" }
811
+ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
812
+ wheels = [
813
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
814
+ ]
manifests/phase21a_training_environment_v1.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": "phase21a_training_environment_v1",
3
+ "captured_at": "2026-08-27",
4
+ "operating_system": "Ubuntu 22.04.5 LTS",
5
+ "python": "3.10.12",
6
+ "gpu_reference": {
7
+ "name": "NVIDIA GeForce RTX 5090",
8
+ "memory_mib": 32607,
9
+ "compute_capability": "12.0",
10
+ "driver": "580.173.02"
11
+ },
12
+ "cuda_runtime_from_torch": "12.8",
13
+ "packages": {
14
+ "torch": "2.8.0+cu128",
15
+ "torchvision": "0.23.0+cu128",
16
+ "transformers": "4.57.6",
17
+ "peft": "0.19.1",
18
+ "accelerate": "1.14.0",
19
+ "flash-attn": "2.8.3.post1",
20
+ "qwen-vl-utils": "0.0.14",
21
+ "numpy": "1.26.4",
22
+ "pillow": "11.3.0",
23
+ "safetensors": "0.8.0",
24
+ "huggingface-hub": "0.36.2",
25
+ "packaging": "26.2"
26
+ },
27
+ "base_model": {
28
+ "repo_id": "unsloth/Qwen3-VL-4B-Instruct",
29
+ "revision": "252d592b59b0233b226875a44ac135cfa1d3f755",
30
+ "license_reported_by_hub": "apache-2.0"
31
+ },
32
+ "upstream_robometer": {
33
+ "repository": "https://github.com/robometer/robometer",
34
+ "revision": "a669dffc241d7d76bec12f36efd4084d914d017c",
35
+ "environment_source": ["pyproject.toml", "uv.lock"]
36
+ },
37
+ "notes": [
38
+ "Do not upload or copy the local .venv; recreate it from a frozen lock or container.",
39
+ "Use a separate upload-only environment if upgrading huggingface-hub.",
40
+ "bitsandbytes is not required or allowed for formal Robometer-Low training."
41
+ ]
42
+ }
manifests/phase2_remediation_pairs_v1_summary.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "status": "pass",
3
+ "role": "phase2_five_percent_id_only_remediation",
4
+ "seed": 1701,
5
+ "requested_confirmation_fraction_of_full_stream": 0.2,
6
+ "pair_count": 2689,
7
+ "original_stream_pair_count": 538,
8
+ "train_pair_count": 2021,
9
+ "original_dev_pair_count": 108,
10
+ "confirmation_dev_pair_count": 538,
11
+ "excluded_original_dev_component_pair_count": 22,
12
+ "component_count": 2393,
13
+ "eligible_confirmation_component_count": 1874,
14
+ "selected_confirmation_component_count": 495,
15
+ "original_dev_preserved_verbatim": true,
16
+ "five_percent_is_exact_one_percent_prefix_extension": true,
17
+ "confirmation_ids_seen_in_original_stream": 0,
18
+ "active_split_trajectory_id_overlaps": {
19
+ "train_dev": 0,
20
+ "train_confirmation_dev": 0,
21
+ "dev_confirmation_dev": 0
22
+ },
23
+ "active_unique_feature_sides": 5181,
24
+ "manifest": "manifests/phase2_remediation_pairs_v1.jsonl",
25
+ "manifest_sha256": "c77642e3d34177d1bfbcebe8c97cdf0af952c83d95a677cf42d95426b6d49204"
26
+ }
provenance/source_revisions.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ schema_version: phase21a_source_revisions_v1
2
+ package_date: 2026-08-27
3
+ workspace_base_revision: 2053d617d3a3dab8d008395948337d20e8947f57
4
+ upstream_robometer:
5
+ repository: https://github.com/robometer/robometer
6
+ revision: a669dffc241d7d76bec12f36efd4084d914d017c
7
+ base_model:
8
+ repo_id: unsloth/Qwen3-VL-4B-Instruct
9
+ revision: 252d592b59b0233b226875a44ac135cfa1d3f755
10
+ processed_dataset:
11
+ repo_id: robometer/processed_datasets
12
+ revision: 1a8eb2d279c70964fadeaf8019a77c490c3abe02
13
+ local_profile: stage2_core_v1
14
+ exact_rbm1m_claim_allowed: false
15
+ protocol:
16
+ config: configs/phase21a_robometer_low_lora_train_v2.yaml
17
+ manifest_sha256: c77642e3d34177d1bfbcebe8c97cdf0af952c83d95a677cf42d95426b6d49204
reports/PHASE21A_ROBOMETER_LOW_SMOKE_V1.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 2.1A Robometer-Low-LoRA smoke test
2
+
3
+ ## 结论
4
+
5
+ Robometer-Low-LoRA 的真实数据 smoke 已通过。正式训练容量配置为:
6
+
7
+ ```text
8
+ micro_batch_pairs = 32
9
+ gradient_accumulation = 2
10
+ effective_batch_pairs = 64
11
+ ```
12
+
13
+ 该配置在 RTX 5090 31.335 GiB 上、使用训练集中最长指令的真实 pair,完整执行了两个累计
14
+ micro-step 和一次 optimizer update;峰值 allocated/reserved 为 26.114/28.150 GiB(reserved
15
+ 89.84%),吞吐 2.497 pair/s。总 loss、三个
16
+ component loss 和全部可训练梯度有限;LoRA、新增特殊 token 行、preference/progress/success 三个
17
+ head 的梯度范数均非零。
18
+
19
+ 本轮没有启动正式训练,也没有修改 Cosmos3-Edge。
20
+
21
+ ## 本轮 Robometer-Low-LoRA 定义
22
+
23
+ 保留真实 Robometer-on-Qwen 的结构操作:A/B 两条轨迹在同一次前向中联合输入;每帧后放置
24
+ `<|prog_token|>`;两轨迹之间放置 `<|split_token|>`;末尾用 `<|pref_token|>` 做直接二分类;
25
+ progress、success、preference head 均使用 `Linear(D,D/2) → LayerNorm → GELU → Dropout → Linear`。
26
+ 每个 pair 是 8+8=16 张真实 256×256 letterbox 图像。最终边界复验特意选取 train split 中任务
27
+ 文本最长的 8 个 pair(88–158 字符),自然序列长度为 1,302–1,319 token。
28
+
29
+ “LoRA”表示参数高效训练:冻结视觉塔与语言基座,仅在语言注意力
30
+ q/k/v/o 投影上训练 LoRA(r=8, alpha=16),同时训练五个新增特殊 token 的 embedding 行和三个
31
+ 预测头。总可训练参数 15,768,332,其中 LoRA+token rows 5,911,040,heads 9,857,292。
32
+
33
+ 命名中的 “Low” 只表示使用非完整版 Public-Proxy 数据;全量 decoder 版本命名为
34
+ Robometer-Low,当前版本明确命名为 Robometer-Low-LoRA。
35
+
36
+ ## 容量搜索
37
+
38
+ 每个容量 probe 均在独立进程加载同一 Qwen 快照,并执行联合 16 帧的真实前向、三损失反向与
39
+ optimizer update。显存安全规则预先固定为 peak reserved 不超过物理显存的 90%。
40
+
41
+ | micro batch | grad accum | effective batch | peak reserved GiB | reserved % | pair/s | 判定 |
42
+ |---:|---:|---:|---:|---:|---:|---|
43
+ | 2 | 1 | 2 | 10.172 | 32.46 | 1.464 | pass |
44
+ | 4 | 1 | 4 | 10.719 | 34.21 | 1.883 | pass |
45
+ | 8 | 1 | 8 | 13.023 | 41.56 | 2.099 | pass |
46
+ | 16 | 1 | 16 | 17.670 | 56.39 | 2.270 | pass |
47
+ | 24 | 1 | 24 | 22.889 | 73.05 | 2.426 | pass |
48
+ | 32 | 1 | 32 | 26.877 | 85.77 | 2.459 | pass |
49
+ | 34 | 1 | 34 | 28.006 | 89.38 | 2.451 | single-step pass |
50
+ | 35 | 1 | 35 | 28.658 | 91.46 | 2.477 | 越过安全线 |
51
+ | 34 | 2 | 68 | 28.518 | 91.01 | 1.773 | 最长指令复验通过但越线 |
52
+ | 33 | 2 | 66 | 27.982 | 89.30 | 2.446 | pass,后由用户改为 32 |
53
+ | **32** | **2** | **64** | **28.150** | **89.84** | **2.497** | **最长指令复验,用户确认** |
54
+
55
+ micro-batch 1 的首次运行包含 CUDA kernel/allocator warm-up,11.34 s 的耗时不用于吞吐比较;
56
+ micro-batch 3、6 及全部原始点仍保存在 machine-readable 报告和 run JSON 中。表中 1–35 的
57
+ 单步搜索使用前缀 smoke pool;最终 33×2 与相邻 34×2 使用全量 train split 的最长指令 pool,
58
+ 最终 32×2 使用相同的最长指令 pool,是当前权威配置。
59
+
60
+ ## 边界与下一步
61
+
62
+ 这只证明实现可训练、三条 loss/gradient 路径接通以及单卡容量满足要求,不证明模型收敛或指标
63
+ 提升。正式 Phase 2.1A、学习率/步数、checkpoint 选择和 Cosmos3-Edge 对应改造仍未开始,需用户
64
+ 确认后另行执行。
reports/PHASE21A_STEP_BUDGET_V2.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 2.1A step budget v2
2
+
3
+ Date: 2026-08-27
4
+
5
+ ## Decision
6
+
7
+ Replace the inherited 15,000-step fixed run with a 3,000-step hard cap and
8
+ pre-registered ID-dev early stopping. Apply the same cap and selection rule to
9
+ Robometer-Low-LoRA and the future full-decoder Robometer-Low comparison.
10
+
11
+ ## Scale calculation
12
+
13
+ - Frozen train set: 2,021 preference pairs.
14
+ - Effective batch: 64 pairs per optimizer step.
15
+ - 15,000 steps: 960,000 pair presentations, approximately 475 nominal passes.
16
+ - 3,000-step cap: 192,000 pair presentations, approximately 95 nominal passes.
17
+ - Minimum 1,000 steps: 64,000 pair presentations, approximately 32 nominal passes.
18
+
19
+ The official 15,000-step value was copied from the million-scale RBM-1M recipe;
20
+ it was not estimated from this 2,021-pair Low split.
21
+
22
+ ## Evidence from the stopped v1 run
23
+
24
+ The user-stopped seed-7 run logged through step 246. Mean total train loss fell
25
+ from 4.573 over steps 1-25 to 3.218 over steps 201-225; the last-100-step linear
26
+ trend was still negative by about 0.455 per 100 steps. It had not converged.
27
+ However, the 15,000-step scheduler used 750 warmup steps, so step 246 had reached
28
+ only about 33% of the target learning rate. This curve cannot be extrapolated to
29
+ justify 15,000 steps.
30
+
31
+ ## Selection protocol
32
+
33
+ - Scheduler: cosine with 5% warmup; 150 warmup steps at the cap.
34
+ - Evaluate every 250 steps on the original 108-pair `dev` split.
35
+ - Evaluate both A/B orientations for each pair.
36
+ - Select by the sum of mean preference, progress, and success ID-dev losses.
37
+ - Minimum training: 1,000 steps.
38
+ - Early stop after four consecutive evaluations without an improvement of at
39
+ least 0.005 ID-dev loss.
40
+ - Restore the best ID-dev checkpoint for export.
41
+ - Do not use `confirmation_dev` or RBM-EVAL-OOD for selection.
42
+
43
+ The cap is a safety bound, not a claim that every seed needs exactly 3,000
44
+ steps. The selected step is an experimental result recorded per seed.
45
+
46
+ ## Restart boundary
47
+
48
+ The v1 run is not resumed into v2. Its latest complete checkpoint is step 4,
49
+ and v2 changes both the scheduler horizon and checkpoint-selection protocol.
50
+ All v2 seeds must start from the same pinned base model in a new output root.
reports/PHASE21A_TRAINING_MANUAL.md ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 2.1A 训练手册
2
+
3
+ 日期:2026-08-27
4
+ 对象:`Robometer-Low-LoRA` 与 `Robometer-Low`
5
+ 数据:`Public-Proxy stage2_core_v1` 的冻结 5% pair manifest
6
+
7
+ ## 1. 名称和不可变边界
8
+
9
+ - `Robometer-Low-LoRA`:Qwen3-VL-4B 视觉塔和语言基座冻结;训练 attention LoRA、五个新增
10
+ token rows 和 progress/success/preference 三头。
11
+ - `Robometer-Low`:视觉塔冻结;语言 decoder 全量微调;训练三个 heads;`use_peft=false`。
12
+ - `Low` 只表示使用非完整版 Public-Proxy 数据,不表示量化或 LoRA。
13
+ - 两者使用同一 2,021-pair train split、同一 A/B 联合输入、同一 8+8 帧、同一三损失合同。
14
+ - Robometer-Low 正式训练只允许标准 `torch.optim.AdamW`;禁止 paged AdamW 8-bit。
15
+ - OOD benchmark 不进入训练 loop、checkpoint selection 或超参数选择。
16
+
17
+ 冻结数据统计:2,689 rows,其中 train/dev/confirmation-dev/excluded 分别为
18
+ 2,021/108/538/22;manifest SHA256 为
19
+ `c77642e3d34177d1bfbcebe8c97cdf0af952c83d95a677cf42d95426b6d49204`。
20
+
21
+ ## 2. 当前代码成熟度
22
+
23
+ | 分支 | 状态 | 可以做什么 |
24
+ |---|---|---|
25
+ | Robometer-Low-LoRA v1 | 已按用户要求停止 | 日志到 step 246;最新完整 checkpoint 为 step 4 |
26
+ | Robometer-Low-LoRA v2 | 已配置,未正式启动 | 3,000-step cap、在线 ID-dev、早停和最佳 checkpoint 恢复 |
27
+ | Robometer-Low | 等待服务器资源和正式 runner | 单卡容量 smoke;不能把当前 smoke 当正式训练入口 |
28
+
29
+ LoRA v1 已停止且不再续训。v2 使用全新 output root;禁止拿 v1 的 step-4 checkpoint 恢复到 v2,
30
+ 因为总预算、scheduler 和选择协议均已改变。
31
+
32
+ ## 3. 参考环境
33
+
34
+ 已验证环境:
35
+
36
+ | 项目 | 版本 |
37
+ |---|---|
38
+ | OS | Ubuntu 22.04.5 LTS |
39
+ | Python | 3.10.12 |
40
+ | NVIDIA driver | 580.173.02 |
41
+ | PyTorch / CUDA runtime | 2.8.0+cu128 / 12.8 |
42
+ | torchvision | 0.23.0+cu128 |
43
+ | transformers | 4.57.6 |
44
+ | PEFT | 0.19.1 |
45
+ | accelerate | 1.14.0 |
46
+ | flash-attn | 2.8.3.post1 |
47
+ | qwen-vl-utils | 0.0.14 |
48
+ | NumPy / Pillow | 1.26.4 / 11.3.0 |
49
+
50
+ 可部署环境固定在 `environment/phase21a_robometer/pyproject.toml` 和 `uv.lock`,不复制 `.venv`:
51
+
52
+ ```bash
53
+ cd /srv/phase21a-kit
54
+ uv python install 3.10.12
55
+ uv sync --project environment/phase21a_robometer --frozen
56
+ uv run --project environment/phase21a_robometer \
57
+ python environment/phase21a_robometer/doctor.py
58
+ ```
59
+
60
+ 如果上传包内保存了相同 `pyproject.toml` 和 `uv.lock`,仍要在目标 CUDA/driver 上重新创建环境。
61
+ 不要为上传工具升级训练 venv;`hf` CLI 可使用单独小环境。
62
+
63
+ ## 4. 下载并固定输入
64
+
65
+ 以下使用示例变量;按服务器目录修改:
66
+
67
+ ```bash
68
+ PHASE21A_KIT=/srv/phase21a-kit
69
+ PHASE21A_DATA=/srv/phase21a-data
70
+ PHASE21A_MODEL=/srv/models/qwen3-vl-4b-instruct-252d592
71
+ PHASE21A_RUNS=/srv/phase21a-runs
72
+ ```
73
+
74
+ 下载 training kit 和 dataset:
75
+
76
+ ```bash
77
+ hf download ar-mine/cosmos-robometer-private-code \
78
+ --revision <KIT_COMMIT> \
79
+ --local-dir "$PHASE21A_KIT"
80
+
81
+ hf download ar-mine/cosmos-robometer-public-dataset \
82
+ --repo-type dataset \
83
+ --revision <DATA_COMMIT> \
84
+ --local-dir "$PHASE21A_DATA"
85
+ ```
86
+
87
+ 下载精确基础模型;不要使用浮动 `main`:
88
+
89
+ ```bash
90
+ hf download unsloth/Qwen3-VL-4B-Instruct \
91
+ --revision 252d592b59b0233b226875a44ac135cfa1d3f755 \
92
+ --local-dir "$PHASE21A_MODEL"
93
+ ```
94
+
95
+ ## 5. 开跑前共同门禁
96
+
97
+ ### 5.1 文件完整性
98
+
99
+ ```bash
100
+ cd "$PHASE21A_DATA"
101
+ sha256sum --check SHA256SUMS
102
+ ```
103
+
104
+ 再运行 dataset bundle 工具的 dry-run,把 `--output-dir` 指向任意不会执行复制的路径:
105
+
106
+ ```bash
107
+ cd "$PHASE21A_KIT"
108
+ uv run --project environment/phase21a_robometer python scripts/build_phase21a_hf_dataset_bundle.py \
109
+ --manifest "$PHASE21A_DATA/manifests/phase2_remediation_pairs_v1.jsonl" \
110
+ --processed-root "$PHASE21A_DATA/processed_datasets" \
111
+ --output-dir /tmp/phase21a-dry-run
112
+ ```
113
+
114
+ 必须得到:2,689 rows、4,323 unique frame files、0 missing、7.908 GiB。
115
+
116
+ ### 5.2 GPU/软件门禁
117
+
118
+ ```bash
119
+ nvidia-smi
120
+ uv run --project environment/phase21a_robometer \
121
+ python environment/phase21a_robometer/doctor.py
122
+ ```
123
+
124
+ 必须确认:BF16 可用、FlashAttention 2 可加载、模型 snapshot 离线可读、目标输出盘有足够空间。
125
+
126
+ ### 5.3 数据和评测隔离
127
+
128
+ - formal trainer 只读取 `split=train` 且 `sample_type=preference`,精确为 2,021 pairs。
129
+ - dev/confirmation-dev 只能用于 ID 报告或预注册 checkpoint selection。
130
+ - `excluded_original_dev_component` 不得回流训练。
131
+ - RBM-EVAL-OOD 不下载到训练节点也可以;不得据其结果调参。
132
+
133
+ ## 6. Robometer-Low-LoRA
134
+
135
+ ### 6.1 冻结配置
136
+
137
+ | 参数 | 值 |
138
+ |---|---:|
139
+ | base | `unsloth/Qwen3-VL-4B-Instruct@252d592...` |
140
+ | frames per pair | A 8 + B 8 |
141
+ | canvas | 256×256 letterbox |
142
+ | LoRA | r=8, alpha=16, dropout=0.05 |
143
+ | LoRA targets | q/k/v/o projections |
144
+ | trainable | LoRA + 5 special-token rows + 3 heads |
145
+ | trainable params | 15,768,332 |
146
+ | micro batch | 32 pairs |
147
+ | gradient accumulation | 2 |
148
+ | effective batch | 64 pairs |
149
+ | precision | BF16 |
150
+ | attention | FlashAttention 2 |
151
+ | activation checkpointing | enabled |
152
+ | optimizer | standard AdamW |
153
+ | LoRA LR / heads LR | 2e-6 / 1e-4 |
154
+ | weight decay | 0.01 |
155
+ | scheduler | cosine, 5% warmup |
156
+ | grad clip | 1.0 |
157
+ | steps | 3,000 hard cap per seed;ID-dev early stop |
158
+ | seeds | 7, 17, 27 |
159
+ | checkpoint/eval | every 250 steps;min 1,000;patience 4;min delta 0.005 |
160
+
161
+ 新增 token 的顺序必须保持:
162
+
163
+ ```text
164
+ <|split_token|>, <|reward_token|>, <|pref_token|>, <|sim_token|>, <|prog_token|>
165
+ ```
166
+
167
+ 在当前 base revision 上对应 ID 为 151669–151673。
168
+
169
+ ### 6.2 单步 smoke
170
+
171
+ 在新服务器上必须先跑最长 prompt pool 的真实一步:
172
+
173
+ ```bash
174
+ cd "$PHASE21A_KIT"
175
+ uv run --project environment/phase21a_robometer python scripts/smoke_phase21a_robometer_low.py \
176
+ --snapshot "$PHASE21A_MODEL" \
177
+ --manifest "$PHASE21A_DATA/manifests/phase2_remediation_pairs_v1.jsonl" \
178
+ --processed-root "$PHASE21A_DATA/processed_datasets" \
179
+ --micro-batch-pairs 32 \
180
+ --gradient-accumulation 2 \
181
+ --optimizer-steps 1 \
182
+ --pair-pool-size 8 \
183
+ --seed 7 \
184
+ --lora-lr 2e-6 \
185
+ --head-lr 1e-4 \
186
+ --output /tmp/phase21a_lora_smoke.json
187
+ ```
188
+
189
+ 通过条件:
190
+
191
+ - `status=pass`;
192
+ - effective batch 精确 64;
193
+ - sequence max 不超过已知最长 1,319 tokens;
194
+ - 三个 loss 有限;
195
+ - LoRA、token rows、三个 heads 的梯度都有限且非零;
196
+ - 峰值 reserved 不超过本机显存安全线。RTX 5090 参考值为 28.150 GiB,但其他 GPU 不应机械套用。
197
+
198
+ ### 6.3 正式启动
199
+
200
+ portable runner 已支持显式模型、manifest 和数据路径:
201
+
202
+ ```bash
203
+ cd "$PHASE21A_KIT"
204
+ uv run --project environment/phase21a_robometer python scripts/run_phase21a_robometer_low_lora_seeds.py \
205
+ --snapshot "$PHASE21A_MODEL" \
206
+ --manifest "$PHASE21A_DATA/manifests/phase2_remediation_pairs_v1.jsonl" \
207
+ --processed-root "$PHASE21A_DATA/processed_datasets" \
208
+ --output-root "$PHASE21A_RUNS/robometer-low-lora-v2" \
209
+ --seeds 7 17 27 \
210
+ --optimizer-steps 3000 \
211
+ --micro-batch-pairs 32 \
212
+ --gradient-accumulation 2 \
213
+ --lora-lr 2e-6 \
214
+ --head-lr 1e-4 \
215
+ --checkpoint-every 250 \
216
+ --eval-every 250 \
217
+ --early-stop-min-steps 1000 \
218
+ --early-stop-patience-evals 4 \
219
+ --early-stop-min-delta 0.005
220
+ ```
221
+
222
+ 生产服务器上应由 Slurm、systemd 或其他作业调度器托管;不要依赖普通 SSH shell 的 `&`。每次只运行
223
+ 一个 runner,避免两个进程写同一 output root。
224
+
225
+ ### 6.4 恢复
226
+
227
+ runner 会检测每个 seed 的:
228
+
229
+ - 有 `summary.json`:跳过已完成 seed;
230
+ - 有 `latest_training_state.pt`:自动加 `--resume`;
231
+ - 两者都没有:从 step 0 开始。
232
+
233
+ 完整 checkpoint 包含 adapter、heads、AdamW、scheduler、NumPy/PyTorch/CUDA RNG、数据 permutation
234
+ 和 cursor。恢复前不得修改 manifest;trainer 会核对 manifest SHA256 和 seed。
235
+
236
+ ### 6.5 监控
237
+
238
+ ```bash
239
+ tail -f "$PHASE21A_RUNS/robometer-low-lora-v2/seed_7/progress.jsonl"
240
+ nvidia-smi
241
+ ```
242
+
243
+ 每条 progress record 应包含三个 loss、全局和分组梯度、LR、序列长度、step time 和显存。立即停止的
244
+ 异常包括:NaN/Inf、任一必要梯度组为零、manifest hash 改变、frame file 缺失或 OOM。
245
+
246
+ RTX 5090 当前参考吞吐约 23–24 秒/step、2.7 pairs/s;硬件和温控会改变 ETA。
247
+
248
+ ### 6.6 完成产物
249
+
250
+ 每个 seed 完成后应出现:
251
+
252
+ ```text
253
+ seed_<n>/
254
+ ├── adapter/
255
+ ├── reward_heads.pt
256
+ ├── best_model_state.pt
257
+ ├── latest_training_state.pt
258
+ ├── progress.jsonl
259
+ ├── id_dev_metrics.jsonl
260
+ └── summary.json
261
+ ```
262
+
263
+ 发布前另外保存 processor/tokenizer 和特殊 token map;对外权重优先使用 safetensors。v2 的
264
+ `adapter/` 和 `reward_heads.pt` 会恢复为 ID-dev 最佳 step,而不是机械使用最后一步。
265
+
266
+ ## 7. Robometer-Low 全量 decoder 训练
267
+
268
+ ### 7.1 当前禁止直接正式启动
269
+
270
+ 单张 RTX 5090 上,标准 AdamW 即使 `micro_batch_pairs=1` 也在第一次 optimizer-state 初始化时 OOM。
271
+ PagedAdamW8bit 虽能跑通容量 smoke,但用户已明确禁止其用于正式 Robometer-Low。
272
+
273
+ 当前 `smoke_phase21a_robometer_low_full.py` 只是容量和梯度审计工具,不具备:
274
+
275
+ - 多 GPU/FSDP;
276
+ - 正式 3,000-step cap 和 ID-dev 早停的可恢复状态;
277
+ - distributed RNG/data cursor 恢复;
278
+ - sharded optimizer checkpoint;
279
+ - 三 seed runner。
280
+
281
+ 因此在正式 full runner 实现并通过下述门禁前,Robometer-Low 状态保持
282
+ `waiting_for_server_resources`,不能把 smoke 循环延长后冒充正式训练。
283
+
284
+ ### 7.2 目标正式配置
285
+
286
+ | 参数 | 目标值 |
287
+ |---|---:|
288
+ | vision encoder | frozen |
289
+ | language decoder | full trainable,`use_peft=false` |
290
+ | heads | full trainable |
291
+ | language trainable params | 约 4.0218B |
292
+ | heads trainable params | 9.857M |
293
+ | optimizer | standard torch AdamW,禁止 bitsandbytes/paged |
294
+ | LR | 2e-5 |
295
+ | weight decay | 0.01 |
296
+ | precision/attention | BF16 / FlashAttention 2 |
297
+ | activation checkpointing | enabled |
298
+ | scheduler | cosine,5% warmup |
299
+ | grad clip | 10.0(与当前 full smoke/官方设置一致) |
300
+ | global effective batch | 至少且优先精确为 64 pairs |
301
+ | steps/seeds | 3,000 hard cap + 与 LoRA 相同 ID-dev 早停;7/17/27 |
302
+
303
+ 分布式 global batch 计算公式:
304
+
305
+ ```text
306
+ global_effective_batch = micro_batch_pairs_per_gpu × world_size × gradient_accumulation
307
+ ```
308
+
309
+ 例如 4 GPU、每卡 micro batch 8、accumulation 2 得到 64;这只是算术示例,必须以目标硬件 capacity
310
+ smoke 为准。
311
+
312
+ ### 7.3 正式 runner 的实现门禁
313
+
314
+ 正式代码必须满足:
315
+
316
+ 1. 复用 LoRA v1 的 portable manifest loader、联合 A/B prompt、五 token 顺序和三 heads;
317
+ 2. 只冻结视觉塔,语言 decoder 全量 `requires_grad=True`;
318
+ 3. optimizer 类型审计精确为 `torch.optim.AdamW`,代码路径不导入 bitsandbytes;
319
+ 4. FSDP `FULL_SHARD` 包裹 `Qwen3VLTextDecoderLayer`,并验证无未分片的巨型语言层;
320
+ 5. global effective batch 精确记录并达到 64;
321
+ 6. 每 rank 数据不重复,分布式 sampler 的 epoch/order/cursor 可恢复;
322
+ 7. checkpoint 包含模型、heads、optimizer、scheduler、scaler(如有)、全部 RNG 和 sampler state;
323
+ 8. 中间 checkpoint 优先 sharded state,最终再受控合并,避免 rank 0 保存时 OOM;
324
+ 9. rank 0 写日志,所有 rank 对 NaN/OOM/数据异常做同步失败;
325
+ 10. OOD 不进入训练或选择。
326
+
327
+ 上游 Robometer 的 `robometer/configs/distributed/fsdp.yaml` 可作为 FSDP 配置参考,但官方
328
+ `train.py` 不能直接替代本实验 runner:其默认 PEFT 为 r=32 且覆盖 MLP projections,数据采样和当前
329
+ 冻结 manifest 也不同。直接运行官方命令会形成另一个 protocol。
330
+
331
+ ### 7.4 服务器验收顺序
332
+
333
+ 1. 在隔离进程中加载模型,确认视觉 trainable=0、语言和 heads 数量正确;
334
+ 2. 用最长 prompt 跑一次 FSDP forward/backward/标准 AdamW step;
335
+ 3. 尝试相邻 micro batches,选择峰值显存不超过 90% 的最大安全值;
336
+ 4. 通过 accumulation 调整 global effective batch 到 64;
337
+ 5. 跑 100-step stability smoke,要求 0 NaN/Inf、0 OOM、0 empty mask;
338
+ 6. 在 step 50 保存,重启后恢复到 51,并与不中断参考运行核对 loss/LR/data cursor;
339
+ 7. 固定代码 commit、FSDP config hash、world size 和环境后,才允许三 seed 正式启动。
340
+
341
+ ### 7.5 多 GPU checkpoint 和发布
342
+
343
+ - 训练 checkpoint 保留 sharded optimizer state,供相同或兼容 world size 恢复。
344
+ - 发布 checkpoint 只在训练结束后合并;合并节点要有足够 CPU RAM 和磁盘临时空间。
345
+ - 模型卡必须注明 base revision、数据 commit、world size、global batch、FSDP strategy 和标准 AdamW。
346
+ - 任何使用 8-bit/paged optimizer 的结果必须另命名为实验性分支,不能称为 Robometer-Low。
347
+
348
+ ## 8. 最终开跑检查表
349
+
350
+ ### LoRA
351
+
352
+ - [ ] kit/data/model 三个 commit/revision 已固定
353
+ - [ ] SHA256 全通过,4,323/4,323 frame files 可解析
354
+ - [ ] 单步 smoke 通过且显存安全
355
+ - [ ] output root 全新或只有可信完整 checkpoint
356
+ - [ ] 作业调度器托管,只有一个 runner
357
+ - [ ] seeds 7/17/27、3,000-step cap、ID-dev 早停、effective batch 64
358
+ - [ ] OOD 未接入训练 loop
359
+
360
+ ### Full
361
+
362
+ - [ ] 服务器资源已分配
363
+ - [ ] 自定义正式 FSDP runner 已实现和 code review
364
+ - [ ] standard AdamW 类型审计通过,代码不走 bitsandbytes
365
+ - [ ] 100-step 和断点恢复一致性门禁通过
366
+ - [ ] global effective batch=64
367
+ - [ ] vision frozen、language full trainable
368
+ - [ ] 中间 sharded checkpoint 与最终合并流程均验证
369
+ - [ ] 完成上述全部条件后才从 `waiting_for_server_resources` 改为 `running`
reports/phase21a_robometer_low_smoke_v1.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "status": "pass_awaiting_user_confirmation_before_formal_work",
3
+ "protocol_id": "phase21a_robometer_low_lora_smoke_v1",
4
+ "phase": "Phase 2.1A",
5
+ "model": "Robometer-Low-LoRA",
6
+ "completed_at_local": "2026-08-27T16:46:00+08:00",
7
+ "scope": {
8
+ "smoke_only": true,
9
+ "formal_training_started": false,
10
+ "cosmos3_edge_modified": false
11
+ },
12
+ "contract": {
13
+ "data_route": "Public-Proxy stage2_core_v1",
14
+ "real_pair_pool_size": 8,
15
+ "final_pair_pool_selection": "longest_prompt_characters_in_train_split",
16
+ "final_pair_pool_prompt_character_range": [88, 158],
17
+ "frames_per_trajectory": 8,
18
+ "frames_per_joint_pair_forward": 16,
19
+ "canvas": [256, 256],
20
+ "preference": "direct BCE from <|pref_token|>",
21
+ "progress": "10-bin soft cross entropy from per-frame <|prog_token|>",
22
+ "success": "balanced BCE from per-frame <|prog_token|>",
23
+ "vision_frozen": true,
24
+ "language": "LoRA r8 alpha16 dropout0.05 on q/k/v/o",
25
+ "new_special_token_rows_trainable": true,
26
+ "trainable_heads": ["preference", "progress", "success"],
27
+ "precision": "BF16",
28
+ "gradient_checkpointing": true,
29
+ "attention": "flash_attention_2"
30
+ },
31
+ "official_robometer_training_reference": {
32
+ "train_vision_encoder": false,
33
+ "train_language_model": true,
34
+ "use_peft": false,
35
+ "interpretation": "official default fully fine-tunes the language decoder; this result is explicitly named Robometer-Low-LoRA"
36
+ },
37
+ "parameter_audit": {
38
+ "base_total": 4443056128,
39
+ "backbone_trainable_lora_and_token_rows": 5911040,
40
+ "heads_trainable": 9857292,
41
+ "all_trainable": 15768332
42
+ },
43
+ "gpu": {
44
+ "name": "NVIDIA GeForce RTX 5090",
45
+ "total_gib": 31.33453369140625,
46
+ "peak_reserved_limit_fraction": 0.90
47
+ },
48
+ "capacity_probes": [
49
+ {"micro_batch_pairs": 1, "peak_reserved_gib": 9.44921875, "peak_reserved_fraction": 0.3015592586460454, "pairs_per_second": 0.08816775947063833, "status": "pass_compile_warmup_timing_outlier"},
50
+ {"micro_batch_pairs": 2, "peak_reserved_gib": 10.171875, "peak_reserved_fraction": 0.32462187247387436, "pairs_per_second": 1.4644908823074159, "status": "pass"},
51
+ {"micro_batch_pairs": 3, "peak_reserved_gib": 10.171875, "peak_reserved_fraction": 0.32462187247387436, "pairs_per_second": 1.8326653558369463, "status": "pass"},
52
+ {"micro_batch_pairs": 4, "peak_reserved_gib": 10.71875, "peak_reserved_fraction": 0.3420746613165558, "pairs_per_second": 1.8833336172113138, "status": "pass"},
53
+ {"micro_batch_pairs": 6, "peak_reserved_gib": 11.853515625, "peak_reserved_fraction": 0.37828919816511974, "pairs_per_second": 2.0176592085188503, "status": "pass"},
54
+ {"micro_batch_pairs": 8, "peak_reserved_gib": 13.0234375, "peak_reserved_fraction": 0.4156257000107132, "pairs_per_second": 2.0987864768533617, "status": "pass"},
55
+ {"micro_batch_pairs": 16, "peak_reserved_gib": 17.669921875, "peak_reserved_fraction": 0.5639120737847814, "pairs_per_second": 2.2696475376426384, "status": "pass"},
56
+ {"micro_batch_pairs": 24, "peak_reserved_gib": 22.888671875, "peak_reserved_fraction": 0.7304615444549413, "pairs_per_second": 2.4263013301323024, "status": "pass"},
57
+ {"micro_batch_pairs": 32, "peak_reserved_gib": 26.876953125, "peak_reserved_fraction": 0.8577422402290679, "pairs_per_second": 2.4586052936303937, "status": "pass"},
58
+ {"micro_batch_pairs": 34, "peak_reserved_gib": 28.005859375, "peak_reserved_fraction": 0.8937697829114603, "pairs_per_second": 2.4506286870197433, "status": "pass_single_microstep"},
59
+ {"micro_batch_pairs": 35, "peak_reserved_gib": 28.658203125, "peak_reserved_fraction": 0.9145884667452302, "pairs_per_second": 2.4765409697399408, "status": "pass_but_rejected_over_memory_limit"}
60
+ ],
61
+ "accumulation_boundary_checks": [
62
+ {"micro_batch_pairs": 34, "gradient_accumulation": 2, "effective_batch_pairs": 68, "pair_pool_selection": "longest_prompt_characters_in_train_split", "sequence_tokens_max": 1319, "peak_reserved_gib": 28.517578125, "peak_reserved_fraction": 0.9101006067571121, "pairs_per_second": 1.7730906691413035, "status": "pass_but_rejected_over_memory_limit"},
63
+ {"micro_batch_pairs": 33, "gradient_accumulation": 2, "effective_batch_pairs": 66, "pair_pool_selection": "longest_prompt_characters_in_train_split", "sequence_tokens_max": 1319, "peak_allocated_gib": 26.66872262954712, "peak_reserved_gib": 27.982421875, "peak_reserved_fraction": 0.8930218062467739, "pairs_per_second": 2.445795899300712, "seconds_per_microstep": 13.492540407576598, "status": "pass_superseded_by_user_batch_selection"},
64
+ {"micro_batch_pairs": 32, "gradient_accumulation": 2, "effective_batch_pairs": 64, "pair_pool_selection": "longest_prompt_characters_in_train_split", "sequence_tokens_max": 1319, "peak_allocated_gib": 26.11378002166748, "peak_reserved_gib": 28.150390625, "peak_reserved_fraction": 0.898382305677026, "pairs_per_second": 2.496645440085846, "seconds_per_microstep": 12.817198424017988, "status": "selected_pass_user_confirmed"}
65
+ ],
66
+ "selected": {
67
+ "micro_batch_pairs": 32,
68
+ "gradient_accumulation": 2,
69
+ "effective_batch_pairs": 64,
70
+ "selection_reason": "user_confirmed_exact_effective_batch_64",
71
+ "optimizer_steps_validated": 1,
72
+ "microsteps_validated": 2,
73
+ "pair_pool_selection": "longest_prompt_characters_in_train_split",
74
+ "sequence_tokens_max": 1319,
75
+ "peak_allocated_gib": 26.11378002166748,
76
+ "peak_reserved_gib": 28.150390625,
77
+ "peak_reserved_fraction": 0.898382305677026,
78
+ "pairs_per_second": 2.496645440085846,
79
+ "loss_all_finite": true,
80
+ "gradient_all_finite": true,
81
+ "all_required_gradient_groups_nonzero": true
82
+ },
83
+ "artifacts": {
84
+ "selected_raw": "runs/phase21a_robometer_low_smoke_v1/worstcase_mb32_ga2_eff64.json",
85
+ "rejected_adjacent_raw": "runs/phase21a_robometer_low_smoke_v1/worstcase_mb34_ga2_eff68.json",
86
+ "script_sha256": "4d1ddd53cb26b1c9115808ff65ae21ecf8de36ca322bc0a2449ce4a9fd00fe89",
87
+ "config_sha256_before_final_result_annotation": "02ef3a8ff8dc35766ecd4c0800866522218f6ae0768a898b381b9ef6a18d7422"
88
+ }
89
+ }
scripts/build_phase21a_hf_dataset_bundle.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Materialize the minimal portable Phase 2.1A dataset bundle.
3
+
4
+ The source view may contain symlinks into cold storage. This script copies only
5
+ frame archives referenced by the frozen manifest and preserves their relative
6
+ paths so ``--processed-root <bundle>/processed_datasets`` remains valid.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import hashlib
13
+ import json
14
+ import shutil
15
+ from pathlib import Path
16
+
17
+
18
+ def sha256(path: Path) -> str:
19
+ digest = hashlib.sha256()
20
+ with path.open("rb") as stream:
21
+ for block in iter(lambda: stream.read(8 * 1024 * 1024), b""):
22
+ digest.update(block)
23
+ return digest.hexdigest()
24
+
25
+
26
+ def main() -> None:
27
+ parser = argparse.ArgumentParser()
28
+ parser.add_argument("--manifest", type=Path, required=True)
29
+ parser.add_argument("--processed-root", type=Path, required=True)
30
+ parser.add_argument("--output-dir", type=Path, required=True)
31
+ parser.add_argument(
32
+ "--execute",
33
+ action="store_true",
34
+ help="Copy files and write inventory; without this flag only print the plan.",
35
+ )
36
+ args = parser.parse_args()
37
+
38
+ manifest = args.manifest.resolve()
39
+ # Preserve the lexical parent because ``processed_root`` is commonly a
40
+ # symlink named ``processed_datasets`` and manifest paths include that name.
41
+ processed_root = args.processed_root.absolute()
42
+ source_root = processed_root.parent
43
+ output = args.output_dir.resolve()
44
+ resolved_processed_root = processed_root.resolve()
45
+ if output == resolved_processed_root or resolved_processed_root in output.parents:
46
+ raise ValueError("output directory must not be inside the source dataset view")
47
+
48
+ rows = [json.loads(line) for line in manifest.read_text(encoding="utf-8").splitlines() if line]
49
+ references: set[str] = set()
50
+ split_counts: dict[str, int] = {}
51
+ for row in rows:
52
+ split = str(row["split"])
53
+ split_counts[split] = split_counts.get(split, 0) + 1
54
+ for role in ("chosen", "rejected"):
55
+ value = str(row[role]["source_frame_file"])
56
+ relative = Path(value)
57
+ if relative.is_absolute() or ".." in relative.parts:
58
+ raise ValueError(f"non-portable frame path: {value}")
59
+ if relative.parts and relative.parts[0] == ".":
60
+ relative = Path(*relative.parts[1:])
61
+ if not relative.parts or relative.parts[0] != "processed_datasets":
62
+ raise ValueError(f"frame path is outside processed_datasets: {value}")
63
+ references.add(relative.as_posix())
64
+
65
+ sources: list[tuple[str, Path, int]] = []
66
+ for relative in sorted(references):
67
+ relative_path = Path(relative)
68
+ source = resolved_processed_root / Path(*relative_path.parts[1:])
69
+ if not source.is_file():
70
+ raise FileNotFoundError(source)
71
+ sources.append((relative, source, source.stat().st_size))
72
+
73
+ plan = {
74
+ "status": "ready" if args.execute else "dry_run",
75
+ "manifest": str(manifest),
76
+ "manifest_sha256": sha256(manifest),
77
+ "pair_rows": len(rows),
78
+ "split_counts": split_counts,
79
+ "unique_frame_files": len(sources),
80
+ "frame_bytes": sum(size for _, _, size in sources),
81
+ "frame_gib": sum(size for _, _, size in sources) / 2**30,
82
+ "output_dir": str(output),
83
+ }
84
+ if not args.execute:
85
+ print(json.dumps(plan, indent=2, sort_keys=True))
86
+ return
87
+
88
+ manifest_target = output / "manifests" / manifest.name
89
+ manifest_target.parent.mkdir(parents=True, exist_ok=True)
90
+ shutil.copy2(manifest, manifest_target)
91
+ summary_source = manifest.with_name(f"{manifest.stem}_summary.json")
92
+ if summary_source.is_file():
93
+ shutil.copy2(summary_source, manifest_target.parent / summary_source.name)
94
+
95
+ inventory = []
96
+ checksum_lines = []
97
+ for index, (relative, source, size) in enumerate(sources, start=1):
98
+ target = output / relative
99
+ target.parent.mkdir(parents=True, exist_ok=True)
100
+ if not target.is_file() or target.stat().st_size != size:
101
+ shutil.copy2(source, target)
102
+ checksum = sha256(target)
103
+ inventory.append({"path": relative, "bytes": size, "sha256": checksum})
104
+ checksum_lines.append(f"{checksum} {relative}\n")
105
+ if index % 100 == 0:
106
+ print(f"materialized {index}/{len(sources)}", flush=True)
107
+
108
+ inventory_payload = dict(plan)
109
+ inventory_payload["status"] = "complete"
110
+ inventory_payload["files"] = inventory
111
+ (output / "phase21a_data_inventory.json").write_text(
112
+ json.dumps(inventory_payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
113
+ )
114
+ (output / "SHA256SUMS").write_text("".join(checksum_lines), encoding="utf-8")
115
+ print(json.dumps({key: value for key, value in inventory_payload.items() if key != "files"}, indent=2))
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
scripts/run_phase21a_robometer_low_lora_seeds.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Run the three Phase 2.1A v2 Robometer-Low-LoRA seeds serially with resume."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ TRAINER = ROOT / "scripts" / "train_phase21a_robometer_low_lora.py"
14
+ def main() -> None:
15
+ parser = argparse.ArgumentParser()
16
+ parser.add_argument("--output-root", type=Path, required=True)
17
+ parser.add_argument("--snapshot", type=Path, required=True)
18
+ parser.add_argument(
19
+ "--manifest",
20
+ type=Path,
21
+ default=ROOT / "manifests" / "phase2_remediation_pairs_v1.jsonl",
22
+ )
23
+ parser.add_argument(
24
+ "--processed-root",
25
+ type=Path,
26
+ default=ROOT / "artifacts" / "data_views" / "phase1_official" / "processed_datasets",
27
+ )
28
+ parser.add_argument("--seeds", type=int, nargs="+", default=[7, 17, 27])
29
+ parser.add_argument("--optimizer-steps", type=int, default=3000)
30
+ parser.add_argument("--micro-batch-pairs", type=int, default=32)
31
+ parser.add_argument("--gradient-accumulation", type=int, default=2)
32
+ parser.add_argument("--lora-lr", type=float, default=2.0e-6)
33
+ parser.add_argument("--head-lr", type=float, default=1.0e-4)
34
+ parser.add_argument("--checkpoint-every", type=int, default=250)
35
+ parser.add_argument("--eval-every", type=int, default=250)
36
+ parser.add_argument("--eval-micro-batch-pairs", type=int, default=32)
37
+ parser.add_argument("--early-stop-min-steps", type=int, default=1000)
38
+ parser.add_argument("--early-stop-patience-evals", type=int, default=4)
39
+ parser.add_argument("--early-stop-min-delta", type=float, default=5.0e-3)
40
+ args = parser.parse_args()
41
+ for seed in args.seeds:
42
+ output = args.output_root / f"seed_{seed}"
43
+ if (output / "summary.json").is_file():
44
+ continue
45
+ command = [
46
+ sys.executable,
47
+ str(TRAINER),
48
+ "--snapshot", str(args.snapshot),
49
+ "--manifest", str(args.manifest),
50
+ "--processed-root", str(args.processed_root),
51
+ "--seed", str(seed),
52
+ "--optimizer-steps", str(args.optimizer_steps),
53
+ "--micro-batch-pairs", str(args.micro_batch_pairs),
54
+ "--gradient-accumulation", str(args.gradient_accumulation),
55
+ "--lora-lr", str(args.lora_lr),
56
+ "--head-lr", str(args.head_lr),
57
+ "--checkpoint-every", str(args.checkpoint_every),
58
+ "--eval-every", str(args.eval_every),
59
+ "--eval-micro-batch-pairs", str(args.eval_micro_batch_pairs),
60
+ "--early-stop-min-steps", str(args.early_stop_min_steps),
61
+ "--early-stop-patience-evals", str(args.early_stop_patience_evals),
62
+ "--early-stop-min-delta", str(args.early_stop_min_delta),
63
+ "--output-dir", str(output),
64
+ ]
65
+ if (output / "latest_training_state.pt").is_file():
66
+ command.append("--resume")
67
+ subprocess.run(command, check=True)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
scripts/smoke_phase21a_robometer_low.py ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """One isolated Robometer-Low smoke run on real Route-A preference pairs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import math
9
+ import os
10
+ import random
11
+ import sys
12
+ import time
13
+ from functools import lru_cache
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+ import torch
18
+ import torch.nn as nn
19
+ import torch.nn.functional as F
20
+ from PIL import Image
21
+
22
+
23
+ EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
24
+ if str(EXPERIMENT_ROOT) not in sys.path:
25
+ sys.path.insert(0, str(EXPERIMENT_ROOT))
26
+
27
+ from src.data_contract import letterbox_rgb # noqa: E402
28
+
29
+
30
+ SPECIAL_TOKENS = (
31
+ "<|split_token|>",
32
+ "<|reward_token|>",
33
+ "<|pref_token|>",
34
+ "<|sim_token|>",
35
+ "<|prog_token|>",
36
+ )
37
+
38
+
39
+ def parse_args() -> argparse.Namespace:
40
+ parser = argparse.ArgumentParser()
41
+ parser.add_argument("--snapshot", type=Path, required=True)
42
+ parser.add_argument("--manifest", type=Path, required=True)
43
+ parser.add_argument("--processed-root", type=Path, required=True)
44
+ parser.add_argument("--micro-batch-pairs", type=int, required=True)
45
+ parser.add_argument("--gradient-accumulation", type=int, required=True)
46
+ parser.add_argument("--optimizer-steps", type=int, default=1)
47
+ parser.add_argument("--pair-pool-size", type=int, default=8)
48
+ parser.add_argument("--seed", type=int, default=7)
49
+ parser.add_argument("--head-lr", type=float, default=1.0e-4)
50
+ parser.add_argument("--lora-lr", type=float, default=2.0e-6)
51
+ parser.add_argument("--output", type=Path, required=True)
52
+ return parser.parse_args()
53
+
54
+
55
+ class RobometerHeads(nn.Module):
56
+ """Exact Robometer prediction-head topology, with 10-bin progress."""
57
+
58
+ def __init__(self, hidden_size: int, dropout: float = 0.1) -> None:
59
+ super().__init__()
60
+ self.progress_head = self._head(hidden_size, 10, dropout)
61
+ self.success_head = self._head(hidden_size, 1, dropout)
62
+ self.preference_head = self._head(hidden_size, 1, dropout)
63
+
64
+ @staticmethod
65
+ def _head(hidden_size: int, output_size: int, dropout: float) -> nn.Sequential:
66
+ return nn.Sequential(
67
+ nn.Linear(hidden_size, hidden_size // 2),
68
+ nn.LayerNorm(hidden_size // 2),
69
+ nn.GELU(),
70
+ nn.Dropout(dropout),
71
+ nn.Linear(hidden_size // 2, output_size),
72
+ )
73
+
74
+
75
+ def read_train_rows(path: Path, count: int) -> list[dict]:
76
+ candidates = []
77
+ with path.open("r", encoding="utf-8") as stream:
78
+ for line in stream:
79
+ row = json.loads(line)
80
+ if row["split"] == "train" and row["sample_type"] == "preference":
81
+ candidates.append(row)
82
+ # Capacity must cover the worst text-padding case in the formal manifest.
83
+ rows = sorted(candidates, key=lambda row: len(row["prompt_task"]), reverse=True)[:count]
84
+ if len(rows) < count:
85
+ raise RuntimeError(f"requested {count} smoke rows, found {len(rows)}")
86
+ if not any(row["chosen"]["progress_supervised"] for row in rows):
87
+ raise RuntimeError("smoke pool has no progress-supervised chosen trajectory")
88
+ if not any(any(row["chosen"]["success_label"]) for row in rows):
89
+ raise RuntimeError("smoke pool has no positive success label")
90
+ return rows
91
+
92
+
93
+ def resolve_frame_file(value: str, processed_root: Path) -> Path:
94
+ path = Path(value)
95
+ if path.is_absolute():
96
+ return path
97
+ resolved = (processed_root.parent / path).resolve()
98
+ if not resolved.is_file():
99
+ raise FileNotFoundError(resolved)
100
+ return resolved
101
+
102
+
103
+ @lru_cache(maxsize=64)
104
+ def load_trajectory_cached(
105
+ frame_path_string: str, selected_indices: tuple[int, ...]
106
+ ) -> tuple[Image.Image, ...]:
107
+ with np.load(frame_path_string) as archive:
108
+ frames = archive["frames"][np.asarray(selected_indices, dtype=np.int64)]
109
+ return tuple(Image.fromarray(letterbox_rgb(frame, 256)) for frame in frames)
110
+
111
+
112
+ def load_images(record: dict, processed_root: Path) -> tuple[Image.Image, ...]:
113
+ frame_path = resolve_frame_file(record["source_frame_file"], processed_root)
114
+ return load_trajectory_cached(
115
+ str(frame_path), tuple(int(index) for index in record["selected_frame_indices"])
116
+ )
117
+
118
+
119
+ def build_conversation(
120
+ row: dict, processed_root: Path, chosen_is_a: bool
121
+ ) -> tuple[list[dict], dict, float]:
122
+ record_a = row["chosen"] if chosen_is_a else row["rejected"]
123
+ record_b = row["rejected"] if chosen_is_a else row["chosen"]
124
+ prompt = (
125
+ f"Given these two trajectories for the task '{row['prompt_task']}', evaluate "
126
+ "which one makes more progress towards the task. Return A for the first "
127
+ "trajectory and B for the second trajectory. Also predict the task progress "
128
+ "at each frame of the first trajectory, how far along the robot is towards "
129
+ "completing the task, a float between 0 and 1, where 0 is the starting state "
130
+ "and 1 is when the task is completed. If the robot is not performing the "
131
+ "same task, predict 0 progress."
132
+ )
133
+ content: list[dict] = [
134
+ {"type": "text", "text": prompt},
135
+ {"type": "text", "text": "This is Trajectory A. "},
136
+ ]
137
+ for image in load_images(record_a, processed_root):
138
+ content.append({"type": "image", "image": image})
139
+ content.append({"type": "text", "text": "<|prog_token|>"})
140
+ content.extend(
141
+ [
142
+ {"type": "text", "text": "<|split_token|>"},
143
+ {"type": "text", "text": "This is Trajectory B. "},
144
+ ]
145
+ )
146
+ for image in load_images(record_b, processed_root):
147
+ content.append({"type": "image", "image": image})
148
+ content.append({"type": "text", "text": "<|prog_token|>"})
149
+ content.extend(
150
+ [
151
+ {
152
+ "type": "text",
153
+ "text": "Now predict the preference between the two trajectories.",
154
+ },
155
+ {"type": "text", "text": "<|pref_token|>"},
156
+ ]
157
+ )
158
+ return [{"role": "user", "content": content}], record_a, float(chosen_is_a)
159
+
160
+
161
+ def process_batch(processor, conversations: list[list[dict]]) -> dict[str, torch.Tensor]:
162
+ from qwen_vl_utils import process_vision_info
163
+
164
+ texts = [
165
+ processor.apply_chat_template(
166
+ message,
167
+ tokenize=False,
168
+ add_generation_prompt=False,
169
+ add_vision_id=True,
170
+ enable_thinking=False,
171
+ fps=1,
172
+ )
173
+ for message in conversations
174
+ ]
175
+ image_inputs, video_inputs, video_kwargs = process_vision_info(
176
+ conversations,
177
+ return_video_kwargs=True,
178
+ return_video_metadata=True,
179
+ image_patch_size=processor.image_processor.patch_size,
180
+ )
181
+ if video_inputs:
182
+ raise RuntimeError("Robometer-Low smoke requires multi-image mode, not video mode")
183
+ kwargs = dict(
184
+ text=texts,
185
+ images=image_inputs,
186
+ padding=True,
187
+ truncation=False,
188
+ return_tensors="pt",
189
+ do_resize=False,
190
+ )
191
+ if video_kwargs:
192
+ kwargs.update(video_kwargs)
193
+ return processor(**kwargs)
194
+
195
+
196
+ def make_targets(records_a: list[dict], device: torch.device) -> dict[str, torch.Tensor]:
197
+ progress = torch.as_tensor(
198
+ np.asarray([record["target_progress"] for record in records_a]),
199
+ dtype=torch.float32,
200
+ device=device,
201
+ )
202
+ progress_mask = torch.as_tensor(
203
+ np.asarray(
204
+ [
205
+ np.asarray(record["frame_valid_mask"], dtype=bool)
206
+ & (np.asarray(record["predict_last_frame_mask"]) > 0)
207
+ & bool(record["progress_supervised"])
208
+ for record in records_a
209
+ ]
210
+ ),
211
+ dtype=torch.bool,
212
+ device=device,
213
+ )
214
+ success = torch.as_tensor(
215
+ np.asarray([record["success_label"] for record in records_a]),
216
+ dtype=torch.float32,
217
+ device=device,
218
+ )
219
+ success_mask = torch.as_tensor(
220
+ np.asarray([record["frame_valid_mask"] for record in records_a]),
221
+ dtype=torch.bool,
222
+ device=device,
223
+ )
224
+ return {
225
+ "progress": progress,
226
+ "progress_mask": progress_mask,
227
+ "success": success,
228
+ "success_mask": success_mask,
229
+ }
230
+
231
+
232
+ def finite_grad_audit(named_parameters) -> tuple[bool, float, int]:
233
+ squared_norm = 0.0
234
+ tensor_count = 0
235
+ for _name, parameter in named_parameters:
236
+ if not parameter.requires_grad or parameter.grad is None:
237
+ continue
238
+ tensor_count += 1
239
+ gradient = parameter.grad.detach().float()
240
+ if not torch.isfinite(gradient).all():
241
+ return False, math.nan, tensor_count
242
+ squared_norm += float(torch.sum(gradient * gradient))
243
+ return True, math.sqrt(squared_norm), tensor_count
244
+
245
+
246
+ def group_grad_norm(named_parameters, marker: str) -> float:
247
+ squared_norm = 0.0
248
+ found = 0
249
+ for name, parameter in named_parameters:
250
+ if marker not in name or parameter.grad is None:
251
+ continue
252
+ squared_norm += float(torch.sum(parameter.grad.detach().float() ** 2))
253
+ found += 1
254
+ return math.sqrt(squared_norm) if found else 0.0
255
+
256
+
257
+ def main() -> None:
258
+ args = parse_args()
259
+ if args.micro_batch_pairs <= 0 or args.gradient_accumulation <= 0:
260
+ raise ValueError("batch and accumulation must be positive")
261
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
262
+ random.seed(args.seed)
263
+ np.random.seed(args.seed)
264
+ torch.manual_seed(args.seed)
265
+ torch.cuda.manual_seed_all(args.seed)
266
+ torch.backends.cuda.matmul.allow_tf32 = True
267
+
268
+ from peft import LoraConfig, get_peft_model
269
+ from transformers import AutoModelForImageTextToText, AutoProcessor
270
+
271
+ started = time.perf_counter()
272
+ processor = AutoProcessor.from_pretrained(
273
+ args.snapshot, local_files_only=True, padding_side="right"
274
+ )
275
+ original_vocab = len(processor.tokenizer)
276
+ for token in SPECIAL_TOKENS:
277
+ if token not in processor.tokenizer.get_vocab():
278
+ processor.tokenizer.add_special_tokens(
279
+ {"additional_special_tokens": [token]}
280
+ )
281
+ special_token_ids = {
282
+ token: int(processor.tokenizer.convert_tokens_to_ids(token))
283
+ for token in SPECIAL_TOKENS
284
+ }
285
+ model = AutoModelForImageTextToText.from_pretrained(
286
+ args.snapshot,
287
+ local_files_only=True,
288
+ dtype=torch.bfloat16,
289
+ attn_implementation="flash_attention_2",
290
+ device_map={"": 0},
291
+ )
292
+ model.resize_token_embeddings(len(processor.tokenizer))
293
+ for parameter in model.parameters():
294
+ parameter.requires_grad_(False)
295
+
296
+ language_model = model.model.language_model
297
+ peft_config = LoraConfig(
298
+ r=8,
299
+ lora_alpha=16,
300
+ lora_dropout=0.05,
301
+ bias="none",
302
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
303
+ trainable_token_indices=list(special_token_ids.values()),
304
+ )
305
+ model.model.language_model = get_peft_model(language_model, peft_config)
306
+ model.config.use_cache = False
307
+ model.gradient_checkpointing_enable(
308
+ gradient_checkpointing_kwargs={"use_reentrant": False}
309
+ )
310
+ model.enable_input_require_grads()
311
+
312
+ hidden_size = int(model.config.text_config.hidden_size)
313
+ heads = RobometerHeads(hidden_size).to(device="cuda", dtype=torch.bfloat16)
314
+ backbone_trainable = [
315
+ parameter for parameter in model.parameters() if parameter.requires_grad
316
+ ]
317
+ head_parameters = list(heads.parameters())
318
+ optimizer = torch.optim.AdamW(
319
+ [
320
+ {"params": backbone_trainable, "lr": args.lora_lr},
321
+ {"params": head_parameters, "lr": args.head_lr},
322
+ ],
323
+ weight_decay=0.01,
324
+ )
325
+ rows = read_train_rows(args.manifest, args.pair_pool_size)
326
+ total_parameters = sum(parameter.numel() for parameter in model.parameters())
327
+ trainable_backbone_parameters = sum(p.numel() for p in backbone_trainable)
328
+ trainable_head_parameters = sum(p.numel() for p in head_parameters)
329
+
330
+ model.train()
331
+ heads.train()
332
+ torch.cuda.empty_cache()
333
+ torch.cuda.reset_peak_memory_stats()
334
+ load_seconds = time.perf_counter() - started
335
+ run_started = time.perf_counter()
336
+ component_history = []
337
+ sequence_lengths = []
338
+ optimizer.zero_grad(set_to_none=True)
339
+ named_trainable = list(model.named_parameters()) + [
340
+ (f"heads.{name}", parameter) for name, parameter in heads.named_parameters()
341
+ ]
342
+
343
+ for optimizer_step in range(args.optimizer_steps):
344
+ for accumulation_index in range(args.gradient_accumulation):
345
+ global_microstep = (
346
+ optimizer_step * args.gradient_accumulation + accumulation_index
347
+ )
348
+ batch_rows = [
349
+ rows[(global_microstep * args.micro_batch_pairs + offset) % len(rows)]
350
+ for offset in range(args.micro_batch_pairs)
351
+ ]
352
+ conversations = []
353
+ records_a = []
354
+ preference_labels = []
355
+ for offset, row in enumerate(batch_rows):
356
+ chosen_is_a = ((global_microstep * args.micro_batch_pairs + offset) % 2) == 0
357
+ conversation, record_a, label = build_conversation(
358
+ row, args.processed_root, chosen_is_a
359
+ )
360
+ conversations.append(conversation)
361
+ records_a.append(record_a)
362
+ preference_labels.append(label)
363
+ inputs = process_batch(processor, conversations)
364
+ sequence_lengths.extend(inputs["attention_mask"].sum(1).tolist())
365
+ inputs = {key: value.to("cuda") for key, value in inputs.items()}
366
+ targets = make_targets(records_a, torch.device("cuda"))
367
+ preference_target = torch.tensor(
368
+ preference_labels, dtype=torch.float32, device="cuda"
369
+ )
370
+ output = model.model(**inputs, output_hidden_states=False, use_cache=False)
371
+ hidden = output.last_hidden_state
372
+ prog_mask = inputs["input_ids"].eq(special_token_ids["<|prog_token|>"])
373
+ pref_mask = inputs["input_ids"].eq(special_token_ids["<|pref_token|>"])
374
+ prog_counts = prog_mask.sum(1)
375
+ pref_counts = pref_mask.sum(1)
376
+ if not torch.equal(
377
+ prog_counts, torch.full_like(prog_counts, 16)
378
+ ) or not torch.equal(pref_counts, torch.ones_like(pref_counts)):
379
+ raise RuntimeError(
380
+ f"token extraction mismatch: prog={prog_counts.tolist()} "
381
+ f"pref={pref_counts.tolist()}"
382
+ )
383
+ prog_hidden = hidden[prog_mask].reshape(args.micro_batch_pairs, 16, hidden_size)
384
+ pref_hidden = hidden[pref_mask].reshape(args.micro_batch_pairs, hidden_size)
385
+ progress_logits = heads.progress_head(prog_hidden[:, :8])
386
+ success_logits = heads.success_head(prog_hidden[:, :8]).squeeze(-1)
387
+ preference_logits = heads.preference_head(pref_hidden).squeeze(-1)
388
+
389
+ if targets["progress_mask"].any():
390
+ per_frame_progress = -(
391
+ targets["progress"] * F.log_softmax(progress_logits.float(), dim=-1)
392
+ ).sum(-1)
393
+ progress_loss = per_frame_progress[targets["progress_mask"]].mean()
394
+ else:
395
+ progress_loss = progress_logits.float().sum() * 0.0
396
+ valid_success = targets["success"][targets["success_mask"]]
397
+ valid_success_logits = success_logits.float()[targets["success_mask"]]
398
+ positives = valid_success.sum()
399
+ negatives = valid_success.numel() - positives
400
+ pos_weight = (negatives / positives.clamp_min(1.0)).clamp_min(1.0)
401
+ success_loss = F.binary_cross_entropy_with_logits(
402
+ valid_success_logits, valid_success, pos_weight=pos_weight
403
+ )
404
+ preference_loss = F.binary_cross_entropy_with_logits(
405
+ preference_logits.float(), preference_target
406
+ )
407
+ total_loss = progress_loss + success_loss + preference_loss
408
+ if not torch.isfinite(total_loss):
409
+ raise FloatingPointError(f"non-finite loss: {float(total_loss)}")
410
+ (total_loss / args.gradient_accumulation).backward()
411
+ component_history.append(
412
+ {
413
+ "total": float(total_loss.detach()),
414
+ "preference": float(preference_loss.detach()),
415
+ "progress": float(progress_loss.detach()),
416
+ "success": float(success_loss.detach()),
417
+ "progress_supervised_frames": int(targets["progress_mask"].sum()),
418
+ "success_supervised_frames": int(targets["success_mask"].sum()),
419
+ }
420
+ )
421
+
422
+ finite_gradients, preclip_grad_norm, gradient_tensor_count = finite_grad_audit(
423
+ named_trainable
424
+ )
425
+ if not finite_gradients:
426
+ raise FloatingPointError("non-finite trainable gradient")
427
+ grad_groups = {
428
+ "lora": group_grad_norm(named_trainable, "lora_"),
429
+ "token_rows": group_grad_norm(named_trainable, "trainable_tokens"),
430
+ "progress_head": group_grad_norm(named_trainable, "heads.progress_head"),
431
+ "success_head": group_grad_norm(named_trainable, "heads.success_head"),
432
+ "preference_head": group_grad_norm(named_trainable, "heads.preference_head"),
433
+ }
434
+ missing_grad_groups = [name for name, value in grad_groups.items() if not value > 0]
435
+ if missing_grad_groups:
436
+ raise RuntimeError(f"missing gradient groups: {missing_grad_groups}")
437
+ torch.nn.utils.clip_grad_norm_(backbone_trainable + head_parameters, 1.0)
438
+ optimizer.step()
439
+ optimizer.zero_grad(set_to_none=True)
440
+
441
+ torch.cuda.synchronize()
442
+ run_seconds = time.perf_counter() - run_started
443
+ device = torch.cuda.get_device_properties(0)
444
+ peak_allocated = torch.cuda.max_memory_allocated()
445
+ peak_reserved = torch.cuda.max_memory_reserved()
446
+ effective_batch = args.micro_batch_pairs * args.gradient_accumulation
447
+ payload = {
448
+ "status": "pass",
449
+ "protocol_id": "phase21a_robometer_low_smoke_v1",
450
+ "scope": {
451
+ "smoke_only": True,
452
+ "formal_training_started": False,
453
+ "cosmos3_edge_modified": False,
454
+ },
455
+ "micro_batch_pairs": args.micro_batch_pairs,
456
+ "gradient_accumulation": args.gradient_accumulation,
457
+ "effective_batch_pairs": effective_batch,
458
+ "optimizer_steps": args.optimizer_steps,
459
+ "microsteps": args.optimizer_steps * args.gradient_accumulation,
460
+ "real_pair_pool_size": len(rows),
461
+ "pair_pool_selection": "longest_prompt_characters_in_train_split",
462
+ "pair_pool_prompt_character_range": [
463
+ min(len(row["prompt_task"]) for row in rows),
464
+ max(len(row["prompt_task"]) for row in rows),
465
+ ],
466
+ "frames_per_pair": 16,
467
+ "precision": "BF16",
468
+ "gradient_checkpointing": True,
469
+ "architecture": {
470
+ "joint_a_b_forward": True,
471
+ "direct_preference_token_head": True,
472
+ "per_frame_progress_tokens": True,
473
+ "progress_bins": 10,
474
+ "lora_rank": 8,
475
+ "lora_alpha": 16,
476
+ "lora_targets": ["q_proj", "k_proj", "v_proj", "o_proj"],
477
+ "special_tokens": special_token_ids,
478
+ "original_vocab_size": original_vocab,
479
+ "resized_vocab_size": len(processor.tokenizer),
480
+ },
481
+ "parameters": {
482
+ "base_total": total_parameters,
483
+ "backbone_trainable_lora_and_token_rows": trainable_backbone_parameters,
484
+ "heads_trainable": trainable_head_parameters,
485
+ "all_trainable": trainable_backbone_parameters + trainable_head_parameters,
486
+ },
487
+ "loss_last": component_history[-1],
488
+ "loss_all_finite": all(
489
+ math.isfinite(value)
490
+ for item in component_history
491
+ for key, value in item.items()
492
+ if key in {"total", "preference", "progress", "success"}
493
+ ),
494
+ "gradient_audit": {
495
+ "finite": finite_gradients,
496
+ "preclip_global_norm": preclip_grad_norm,
497
+ "tensor_count": gradient_tensor_count,
498
+ "group_norms": grad_groups,
499
+ },
500
+ "sequence_tokens": {
501
+ "min": int(min(sequence_lengths)),
502
+ "max": int(max(sequence_lengths)),
503
+ "mean": float(np.mean(sequence_lengths)),
504
+ },
505
+ "timing": {
506
+ "model_load_seconds": load_seconds,
507
+ "run_seconds": run_seconds,
508
+ "seconds_per_microstep": run_seconds
509
+ / (args.optimizer_steps * args.gradient_accumulation),
510
+ "pairs_per_second": effective_batch * args.optimizer_steps / run_seconds,
511
+ },
512
+ "gpu": {
513
+ "name": device.name,
514
+ "total_gib": device.total_memory / 2**30,
515
+ "peak_allocated_gib": peak_allocated / 2**30,
516
+ "peak_reserved_gib": peak_reserved / 2**30,
517
+ "peak_reserved_fraction": peak_reserved / device.total_memory,
518
+ },
519
+ }
520
+ args.output.parent.mkdir(parents=True, exist_ok=True)
521
+ args.output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
522
+ print(json.dumps(payload, indent=2), flush=True)
523
+
524
+
525
+ if __name__ == "__main__":
526
+ main()
scripts/smoke_phase21a_robometer_low_full.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Single-GPU full-language-decoder Robometer-Low smoke on real pairs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import math
9
+ import os
10
+ import random
11
+ import sys
12
+ import time
13
+ from pathlib import Path
14
+
15
+ import numpy as np
16
+ import torch
17
+ import torch.nn.functional as F
18
+
19
+
20
+ EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
21
+ if str(EXPERIMENT_ROOT) not in sys.path:
22
+ sys.path.insert(0, str(EXPERIMENT_ROOT))
23
+
24
+ from scripts.smoke_phase21a_robometer_low import ( # noqa: E402
25
+ SPECIAL_TOKENS,
26
+ RobometerHeads,
27
+ build_conversation,
28
+ finite_grad_audit,
29
+ group_grad_norm,
30
+ make_targets,
31
+ process_batch,
32
+ read_train_rows,
33
+ )
34
+
35
+
36
+ def parse_args() -> argparse.Namespace:
37
+ parser = argparse.ArgumentParser()
38
+ parser.add_argument("--snapshot", type=Path, required=True)
39
+ parser.add_argument("--manifest", type=Path, required=True)
40
+ parser.add_argument("--processed-root", type=Path, required=True)
41
+ parser.add_argument("--micro-batch-pairs", type=int, required=True)
42
+ parser.add_argument("--gradient-accumulation", type=int, required=True)
43
+ parser.add_argument("--optimizer-steps", type=int, default=1)
44
+ parser.add_argument("--pair-pool-size", type=int, default=8)
45
+ parser.add_argument("--optimizer", choices=("adamw_torch", "paged_adamw_8bit"), required=True)
46
+ parser.add_argument("--learning-rate", type=float, default=2.0e-5)
47
+ parser.add_argument("--seed", type=int, default=7)
48
+ parser.add_argument("--require-all-gradient-groups", action="store_true")
49
+ parser.add_argument("--output", type=Path, required=True)
50
+ return parser.parse_args()
51
+
52
+
53
+ def tensor_bytes(value) -> tuple[int, int]:
54
+ """Return CUDA and CPU tensor bytes recursively for optimizer state."""
55
+ cuda_bytes = 0
56
+ cpu_bytes = 0
57
+ if torch.is_tensor(value):
58
+ size = value.numel() * value.element_size()
59
+ if value.device.type == "cuda":
60
+ cuda_bytes += size
61
+ elif value.device.type == "cpu":
62
+ cpu_bytes += size
63
+ elif isinstance(value, dict):
64
+ for item in value.values():
65
+ cuda, cpu = tensor_bytes(item)
66
+ cuda_bytes += cuda
67
+ cpu_bytes += cpu
68
+ elif isinstance(value, (list, tuple)):
69
+ for item in value:
70
+ cuda, cpu = tensor_bytes(item)
71
+ cuda_bytes += cuda
72
+ cpu_bytes += cpu
73
+ return cuda_bytes, cpu_bytes
74
+
75
+
76
+ def make_optimizer(name: str, parameters: list[torch.nn.Parameter], learning_rate: float):
77
+ if name == "adamw_torch":
78
+ return torch.optim.AdamW(
79
+ parameters,
80
+ lr=learning_rate,
81
+ weight_decay=0.01,
82
+ foreach=False,
83
+ )
84
+ import bitsandbytes as bnb
85
+
86
+ return bnb.optim.PagedAdamW8bit(
87
+ parameters,
88
+ lr=learning_rate,
89
+ weight_decay=0.01,
90
+ min_8bit_size=4096,
91
+ )
92
+
93
+
94
+ def main() -> None:
95
+ args = parse_args()
96
+ if args.micro_batch_pairs <= 0 or args.gradient_accumulation <= 0:
97
+ raise ValueError("batch and accumulation must be positive")
98
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
99
+ random.seed(args.seed)
100
+ np.random.seed(args.seed)
101
+ torch.manual_seed(args.seed)
102
+ torch.cuda.manual_seed_all(args.seed)
103
+ torch.backends.cuda.matmul.allow_tf32 = True
104
+
105
+ from transformers import AutoModelForImageTextToText, AutoProcessor
106
+
107
+ started = time.perf_counter()
108
+ processor = AutoProcessor.from_pretrained(
109
+ args.snapshot, local_files_only=True, padding_side="right"
110
+ )
111
+ original_vocab = len(processor.tokenizer)
112
+ for token in SPECIAL_TOKENS:
113
+ if token not in processor.tokenizer.get_vocab():
114
+ processor.tokenizer.add_special_tokens({"additional_special_tokens": [token]})
115
+ special_token_ids = {
116
+ token: int(processor.tokenizer.convert_tokens_to_ids(token))
117
+ for token in SPECIAL_TOKENS
118
+ }
119
+ model = AutoModelForImageTextToText.from_pretrained(
120
+ args.snapshot,
121
+ local_files_only=True,
122
+ dtype=torch.bfloat16,
123
+ attn_implementation="flash_attention_2",
124
+ device_map={"": 0},
125
+ )
126
+ model.resize_token_embeddings(len(processor.tokenizer))
127
+ for parameter in model.parameters():
128
+ parameter.requires_grad_(False)
129
+ for parameter in model.model.language_model.parameters():
130
+ parameter.requires_grad_(True)
131
+ model.config.use_cache = False
132
+ model.gradient_checkpointing_enable(
133
+ gradient_checkpointing_kwargs={"use_reentrant": False}
134
+ )
135
+ model.enable_input_require_grads()
136
+
137
+ hidden_size = int(model.config.text_config.hidden_size)
138
+ heads = RobometerHeads(hidden_size).to(device="cuda", dtype=torch.bfloat16)
139
+ language_parameters = [
140
+ parameter for parameter in model.parameters() if parameter.requires_grad
141
+ ]
142
+ head_parameters = list(heads.parameters())
143
+ trainable_parameters = language_parameters + head_parameters
144
+ vision_trainable = sum(
145
+ parameter.numel()
146
+ for name, parameter in model.named_parameters()
147
+ if parameter.requires_grad and ".visual." in name
148
+ )
149
+ if vision_trainable:
150
+ raise RuntimeError(f"vision boundary violation: {vision_trainable} trainable parameters")
151
+ optimizer = make_optimizer(args.optimizer, trainable_parameters, args.learning_rate)
152
+ rows = read_train_rows(args.manifest, args.pair_pool_size)
153
+
154
+ model.train()
155
+ heads.train()
156
+ torch.cuda.empty_cache()
157
+ torch.cuda.reset_peak_memory_stats()
158
+ load_seconds = time.perf_counter() - started
159
+ run_started = time.perf_counter()
160
+ component_history = []
161
+ sequence_lengths = []
162
+ optimizer.zero_grad(set_to_none=True)
163
+ named_trainable = list(model.named_parameters()) + [
164
+ (f"heads.{name}", parameter) for name, parameter in heads.named_parameters()
165
+ ]
166
+ last_gradient_audit = None
167
+
168
+ for optimizer_step in range(args.optimizer_steps):
169
+ for accumulation_index in range(args.gradient_accumulation):
170
+ global_microstep = optimizer_step * args.gradient_accumulation + accumulation_index
171
+ batch_rows = [
172
+ rows[(global_microstep * args.micro_batch_pairs + offset) % len(rows)]
173
+ for offset in range(args.micro_batch_pairs)
174
+ ]
175
+ conversations = []
176
+ records_a = []
177
+ preference_labels = []
178
+ for offset, row in enumerate(batch_rows):
179
+ chosen_is_a = ((global_microstep * args.micro_batch_pairs + offset) % 2) == 0
180
+ conversation, record_a, label = build_conversation(
181
+ row, args.processed_root, chosen_is_a
182
+ )
183
+ conversations.append(conversation)
184
+ records_a.append(record_a)
185
+ preference_labels.append(label)
186
+ inputs = process_batch(processor, conversations)
187
+ sequence_lengths.extend(inputs["attention_mask"].sum(1).tolist())
188
+ inputs = {key: value.to("cuda") for key, value in inputs.items()}
189
+ targets = make_targets(records_a, torch.device("cuda"))
190
+ preference_target = torch.tensor(
191
+ preference_labels, dtype=torch.float32, device="cuda"
192
+ )
193
+ output = model.model(**inputs, output_hidden_states=False, use_cache=False)
194
+ hidden = output.last_hidden_state
195
+ prog_mask = inputs["input_ids"].eq(special_token_ids["<|prog_token|>"])
196
+ pref_mask = inputs["input_ids"].eq(special_token_ids["<|pref_token|>"])
197
+ prog_counts = prog_mask.sum(1)
198
+ pref_counts = pref_mask.sum(1)
199
+ if not torch.equal(prog_counts, torch.full_like(prog_counts, 16)):
200
+ raise RuntimeError(f"progress token counts: {prog_counts.tolist()}")
201
+ if not torch.equal(pref_counts, torch.ones_like(pref_counts)):
202
+ raise RuntimeError(f"preference token counts: {pref_counts.tolist()}")
203
+ prog_hidden = hidden[prog_mask].reshape(args.micro_batch_pairs, 16, hidden_size)
204
+ pref_hidden = hidden[pref_mask].reshape(args.micro_batch_pairs, hidden_size)
205
+ progress_logits = heads.progress_head(prog_hidden[:, :8])
206
+ success_logits = heads.success_head(prog_hidden[:, :8]).squeeze(-1)
207
+ preference_logits = heads.preference_head(pref_hidden).squeeze(-1)
208
+
209
+ if targets["progress_mask"].any():
210
+ per_frame_progress = -(
211
+ targets["progress"] * F.log_softmax(progress_logits.float(), dim=-1)
212
+ ).sum(-1)
213
+ progress_loss = per_frame_progress[targets["progress_mask"]].mean()
214
+ else:
215
+ progress_loss = progress_logits.float().sum() * 0.0
216
+ valid_success = targets["success"][targets["success_mask"]]
217
+ valid_success_logits = success_logits.float()[targets["success_mask"]]
218
+ positives = valid_success.sum()
219
+ negatives = valid_success.numel() - positives
220
+ pos_weight = (negatives / positives.clamp_min(1.0)).clamp_min(1.0)
221
+ success_loss = F.binary_cross_entropy_with_logits(
222
+ valid_success_logits, valid_success, pos_weight=pos_weight
223
+ )
224
+ preference_loss = F.binary_cross_entropy_with_logits(
225
+ preference_logits.float(), preference_target
226
+ )
227
+ total_loss = progress_loss + success_loss + preference_loss
228
+ if not torch.isfinite(total_loss):
229
+ raise FloatingPointError(f"non-finite loss: {float(total_loss)}")
230
+ (total_loss / args.gradient_accumulation).backward()
231
+ component_history.append(
232
+ {
233
+ "total": float(total_loss.detach()),
234
+ "preference": float(preference_loss.detach()),
235
+ "progress": float(progress_loss.detach()),
236
+ "success": float(success_loss.detach()),
237
+ "progress_supervised_frames": int(targets["progress_mask"].sum()),
238
+ "success_supervised_frames": int(targets["success_mask"].sum()),
239
+ }
240
+ )
241
+
242
+ finite_gradients, preclip_grad_norm, gradient_tensor_count = finite_grad_audit(
243
+ named_trainable
244
+ )
245
+ if not finite_gradients:
246
+ raise FloatingPointError("non-finite trainable gradient")
247
+ embedding_gradient = model.get_input_embeddings().weight.grad
248
+ if embedding_gradient is None:
249
+ special_token_grad_norm = 0.0
250
+ else:
251
+ special_token_grad_norm = float(
252
+ torch.linalg.vector_norm(
253
+ embedding_gradient[list(special_token_ids.values())].float()
254
+ )
255
+ )
256
+ grad_groups = {
257
+ "language_decoder": group_grad_norm(named_trainable, "language_model"),
258
+ "special_token_rows": special_token_grad_norm,
259
+ "progress_head": group_grad_norm(named_trainable, "heads.progress_head"),
260
+ "success_head": group_grad_norm(named_trainable, "heads.success_head"),
261
+ "preference_head": group_grad_norm(named_trainable, "heads.preference_head"),
262
+ }
263
+ missing_grad_groups = [name for name, value in grad_groups.items() if not value > 0]
264
+ if args.require_all_gradient_groups and missing_grad_groups:
265
+ raise RuntimeError(f"missing gradient groups: {missing_grad_groups}")
266
+ last_gradient_audit = {
267
+ "finite": finite_gradients,
268
+ "preclip_global_norm": preclip_grad_norm,
269
+ "tensor_count": gradient_tensor_count,
270
+ "group_norms": grad_groups,
271
+ "missing_groups": missing_grad_groups,
272
+ }
273
+ torch.nn.utils.clip_grad_norm_(trainable_parameters, 10.0)
274
+ optimizer.step()
275
+ optimizer.zero_grad(set_to_none=True)
276
+
277
+ torch.cuda.synchronize()
278
+ run_seconds = time.perf_counter() - run_started
279
+ device = torch.cuda.get_device_properties(0)
280
+ peak_allocated = torch.cuda.max_memory_allocated()
281
+ peak_reserved = torch.cuda.max_memory_reserved()
282
+ optimizer_cuda_bytes, optimizer_cpu_bytes = tensor_bytes(optimizer.state)
283
+ effective_batch = args.micro_batch_pairs * args.gradient_accumulation
284
+ language_trainable_count = sum(parameter.numel() for parameter in language_parameters)
285
+ head_trainable_count = sum(parameter.numel() for parameter in head_parameters)
286
+ payload = {
287
+ "status": "pass",
288
+ "protocol_id": "phase21a_robometer_low_full_smoke_v1",
289
+ "scope": {
290
+ "smoke_only": True,
291
+ "formal_training_started": False,
292
+ "cosmos3_edge_modified": False,
293
+ },
294
+ "micro_batch_pairs": args.micro_batch_pairs,
295
+ "gradient_accumulation": args.gradient_accumulation,
296
+ "effective_batch_pairs": effective_batch,
297
+ "optimizer_steps": args.optimizer_steps,
298
+ "microsteps": args.optimizer_steps * args.gradient_accumulation,
299
+ "optimizer": args.optimizer,
300
+ "optimizer_official_default": args.optimizer == "adamw_torch",
301
+ "real_pair_pool_size": len(rows),
302
+ "pair_pool_selection": "longest_prompt_characters_in_train_split",
303
+ "pair_pool_prompt_character_range": [
304
+ min(len(row["prompt_task"]) for row in rows),
305
+ max(len(row["prompt_task"]) for row in rows),
306
+ ],
307
+ "frames_per_pair": 16,
308
+ "precision": "BF16",
309
+ "gradient_checkpointing": True,
310
+ "architecture": {
311
+ "joint_a_b_forward": True,
312
+ "vision_frozen": True,
313
+ "language_decoder_full_tune": True,
314
+ "peft_or_lora": False,
315
+ "direct_preference_token_head": True,
316
+ "per_frame_progress_tokens": True,
317
+ "progress_bins": 10,
318
+ "special_tokens": special_token_ids,
319
+ "original_vocab_size": original_vocab,
320
+ "resized_vocab_size": len(processor.tokenizer),
321
+ },
322
+ "parameters": {
323
+ "model_total": sum(parameter.numel() for parameter in model.parameters()),
324
+ "language_decoder_trainable": language_trainable_count,
325
+ "heads_trainable": head_trainable_count,
326
+ "all_trainable": language_trainable_count + head_trainable_count,
327
+ "vision_trainable": vision_trainable,
328
+ "trainable_fraction_of_model_and_heads": (
329
+ (language_trainable_count + head_trainable_count)
330
+ / (sum(parameter.numel() for parameter in model.parameters()) + head_trainable_count)
331
+ ),
332
+ },
333
+ "optimizer_state": {
334
+ "cuda_gib": optimizer_cuda_bytes / 2**30,
335
+ "cpu_gib": optimizer_cpu_bytes / 2**30,
336
+ },
337
+ "loss_last": component_history[-1],
338
+ "loss_all_finite": all(
339
+ math.isfinite(value)
340
+ for item in component_history
341
+ for key, value in item.items()
342
+ if key in {"total", "preference", "progress", "success"}
343
+ ),
344
+ "gradient_audit": last_gradient_audit,
345
+ "sequence_tokens": {
346
+ "min": int(min(sequence_lengths)),
347
+ "max": int(max(sequence_lengths)),
348
+ "mean": float(np.mean(sequence_lengths)),
349
+ },
350
+ "timing": {
351
+ "model_load_seconds": load_seconds,
352
+ "run_seconds": run_seconds,
353
+ "seconds_per_microstep": run_seconds
354
+ / (args.optimizer_steps * args.gradient_accumulation),
355
+ "pairs_per_second": effective_batch * args.optimizer_steps / run_seconds,
356
+ },
357
+ "gpu": {
358
+ "name": device.name,
359
+ "total_gib": device.total_memory / 2**30,
360
+ "peak_allocated_gib": peak_allocated / 2**30,
361
+ "peak_reserved_gib": peak_reserved / 2**30,
362
+ "peak_reserved_fraction": peak_reserved / device.total_memory,
363
+ },
364
+ }
365
+ args.output.parent.mkdir(parents=True, exist_ok=True)
366
+ args.output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
367
+ print(json.dumps(payload, indent=2), flush=True)
368
+
369
+
370
+ if __name__ == "__main__":
371
+ main()
scripts/train_phase21a_robometer_low_lora.py ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Resumable formal Robometer-Low-LoRA training on joint preference pairs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import math
10
+ import os
11
+ import random
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+
16
+ # Avoid large reserved-but-unallocated CUDA segments on near-capacity batches.
17
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
18
+
19
+ import numpy as np
20
+ import torch
21
+ import torch.nn.functional as F
22
+
23
+
24
+ EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
25
+ if str(EXPERIMENT_ROOT) not in sys.path:
26
+ sys.path.insert(0, str(EXPERIMENT_ROOT))
27
+
28
+ from scripts.smoke_phase21a_robometer_low import ( # noqa: E402
29
+ SPECIAL_TOKENS,
30
+ RobometerHeads,
31
+ build_conversation,
32
+ finite_grad_audit,
33
+ group_grad_norm,
34
+ make_targets,
35
+ process_batch,
36
+ )
37
+
38
+
39
+ def parse_args() -> argparse.Namespace:
40
+ parser = argparse.ArgumentParser()
41
+ parser.add_argument("--snapshot", type=Path, required=True)
42
+ parser.add_argument("--manifest", type=Path, required=True)
43
+ parser.add_argument("--processed-root", type=Path, required=True)
44
+ parser.add_argument("--seed", type=int, required=True)
45
+ parser.add_argument("--optimizer-steps", type=int, default=3000)
46
+ parser.add_argument("--micro-batch-pairs", type=int, default=32)
47
+ parser.add_argument("--gradient-accumulation", type=int, default=2)
48
+ parser.add_argument("--lora-lr", type=float, default=2.0e-6)
49
+ parser.add_argument("--head-lr", type=float, default=1.0e-4)
50
+ parser.add_argument("--checkpoint-every", type=int, default=250)
51
+ parser.add_argument("--eval-every", type=int, default=250)
52
+ parser.add_argument("--eval-micro-batch-pairs", type=int, default=32)
53
+ parser.add_argument("--early-stop-min-steps", type=int, default=1000)
54
+ parser.add_argument("--early-stop-patience-evals", type=int, default=4)
55
+ parser.add_argument("--early-stop-min-delta", type=float, default=5.0e-3)
56
+ parser.add_argument("--output-dir", type=Path, required=True)
57
+ parser.add_argument("--resume", action="store_true")
58
+ return parser.parse_args()
59
+
60
+
61
+ def read_rows(path: Path) -> list[dict]:
62
+ rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
63
+ train = [row for row in rows if row["split"] == "train" and row["sample_type"] == "preference"]
64
+ if len(train) != 2021:
65
+ raise RuntimeError(f"expected 2021 train preference pairs, found {len(train)}")
66
+ return train
67
+
68
+
69
+ def read_dev_rows(path: Path) -> list[dict]:
70
+ rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
71
+ dev = [row for row in rows if row["split"] == "dev" and row["sample_type"] == "preference"]
72
+ if len(dev) != 108:
73
+ raise RuntimeError(f"expected 108 ID-dev preference pairs, found {len(dev)}")
74
+ return dev
75
+
76
+
77
+ def global_success_pos_weight(rows: list[dict]) -> float:
78
+ positive = 0.0
79
+ total = 0
80
+ for row in rows:
81
+ for role in ("chosen", "rejected"):
82
+ record = row[role]
83
+ valid = np.asarray(record["frame_valid_mask"], dtype=bool)
84
+ labels = np.asarray(record["success_label"], dtype=np.float32)
85
+ positive += float(labels[valid].sum())
86
+ total += int(valid.sum())
87
+ if not 0 < positive < total:
88
+ raise RuntimeError("success labels require both classes")
89
+ return (total - positive) / positive
90
+
91
+
92
+ def atomic_torch_save(payload: dict, path: Path) -> None:
93
+ temporary = path.with_suffix(".tmp")
94
+ torch.save(payload, temporary)
95
+ temporary.replace(path)
96
+
97
+
98
+ def reconcile_progress_log(path: Path, completed_steps: int) -> None:
99
+ """Drop records newer than the checkpoint before a resumed run appends."""
100
+ if not path.is_file():
101
+ return
102
+ records = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
103
+ retained = [record for record in records if int(record["step"]) <= completed_steps]
104
+ temporary = path.with_suffix(".tmp")
105
+ payload = "".join(json.dumps(record, sort_keys=True) + "\n" for record in retained)
106
+ temporary.write_text(payload, encoding="utf-8")
107
+ temporary.replace(path)
108
+
109
+
110
+ def cpu_state_dict(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
111
+ return {name: tensor.detach().cpu().clone() for name, tensor in state.items()}
112
+
113
+
114
+ @torch.inference_mode()
115
+ def evaluate_dev(
116
+ *, model, processor, heads, rows: list[dict], processed_root: Path,
117
+ token_ids: dict[str, int], hidden_size: int, pos_weight: torch.Tensor,
118
+ micro_batch_pairs: int,
119
+ ) -> dict:
120
+ """Evaluate both A/B orientations on the frozen ID-dev split only."""
121
+ model.eval()
122
+ heads.eval()
123
+ presentations = [(row, chosen_is_a) for row in rows for chosen_is_a in (True, False)]
124
+ sums = {"preference": 0.0, "progress": 0.0, "success": 0.0}
125
+ counts = {"preference": 0, "progress": 0, "success": 0}
126
+ preference_correct = 0
127
+ for start in range(0, len(presentations), micro_batch_pairs):
128
+ batch = presentations[start : start + micro_batch_pairs]
129
+ conversations, records_a, preference_labels = [], [], []
130
+ for row, chosen_is_a in batch:
131
+ conversation, record_a, label = build_conversation(row, processed_root, chosen_is_a)
132
+ conversations.append(conversation)
133
+ records_a.append(record_a)
134
+ preference_labels.append(label)
135
+ inputs = process_batch(processor, conversations)
136
+ inputs = {key: value.to("cuda") for key, value in inputs.items()}
137
+ targets = make_targets(records_a, torch.device("cuda"))
138
+ preference_target = torch.tensor(preference_labels, dtype=torch.float32, device="cuda")
139
+ output = model.model(**inputs, output_hidden_states=False, use_cache=False)
140
+ hidden = output.last_hidden_state
141
+ prog_mask = inputs["input_ids"].eq(token_ids["<|prog_token|>"])
142
+ pref_mask = inputs["input_ids"].eq(token_ids["<|pref_token|>"])
143
+ batch_size = len(batch)
144
+ if not torch.equal(prog_mask.sum(1), torch.full_like(prog_mask.sum(1), 16)):
145
+ raise RuntimeError("ID-dev progress token extraction mismatch")
146
+ if not torch.equal(pref_mask.sum(1), torch.ones_like(pref_mask.sum(1))):
147
+ raise RuntimeError("ID-dev preference token extraction mismatch")
148
+ prog_hidden = hidden[prog_mask].reshape(batch_size, 16, hidden_size)
149
+ pref_hidden = hidden[pref_mask].reshape(batch_size, hidden_size)
150
+ progress_logits = heads.progress_head(prog_hidden[:, :8])
151
+ success_logits = heads.success_head(prog_hidden[:, :8]).squeeze(-1)
152
+ preference_logits = heads.preference_head(pref_hidden).squeeze(-1)
153
+ progress_frames = -(
154
+ targets["progress"] * F.log_softmax(progress_logits.float(), dim=-1)
155
+ ).sum(-1)
156
+ progress_values = progress_frames[targets["progress_mask"]]
157
+ success_sum = F.binary_cross_entropy_with_logits(
158
+ success_logits.float()[targets["success_mask"]],
159
+ targets["success"][targets["success_mask"]],
160
+ pos_weight=pos_weight,
161
+ reduction="sum",
162
+ )
163
+ preference_sum = F.binary_cross_entropy_with_logits(
164
+ preference_logits.float(), preference_target, reduction="sum"
165
+ )
166
+ sums["progress"] += float(progress_values.sum())
167
+ sums["success"] += float(success_sum)
168
+ sums["preference"] += float(preference_sum)
169
+ counts["progress"] += int(progress_values.numel())
170
+ counts["success"] += int(targets["success_mask"].sum())
171
+ counts["preference"] += batch_size
172
+ preference_correct += int(
173
+ ((preference_logits > 0) == (preference_target > 0.5)).sum()
174
+ )
175
+ components = {name: sums[name] / counts[name] for name in sums}
176
+ model.train()
177
+ heads.train()
178
+ return {
179
+ "loss": sum(components.values()),
180
+ "loss_components": components,
181
+ "preference_accuracy": preference_correct / counts["preference"],
182
+ "pair_count": len(rows),
183
+ "orientation_presentations": len(presentations),
184
+ "supervision_counts": counts,
185
+ }
186
+
187
+
188
+ def main() -> None:
189
+ args = parse_args()
190
+ if args.micro_batch_pairs * args.gradient_accumulation != 64:
191
+ raise ValueError("formal protocol requires exact effective batch 64")
192
+ if min(args.optimizer_steps, args.checkpoint_every, args.eval_every) <= 0:
193
+ raise ValueError("steps, checkpoint interval, and eval interval must be positive")
194
+ if args.eval_micro_batch_pairs <= 0 or args.early_stop_patience_evals <= 0:
195
+ raise ValueError("eval batch and early-stop patience must be positive")
196
+ if not 0 < args.early_stop_min_steps <= args.optimizer_steps:
197
+ raise ValueError("early-stop minimum must be in (0, optimizer_steps]")
198
+ if args.checkpoint_every != args.eval_every:
199
+ raise ValueError("v2 requires checkpoint_every == eval_every")
200
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
201
+ random.seed(args.seed)
202
+ np.random.seed(args.seed)
203
+ torch.manual_seed(args.seed)
204
+ torch.cuda.manual_seed_all(args.seed)
205
+ torch.backends.cuda.matmul.allow_tf32 = True
206
+
207
+ from peft import LoraConfig, get_peft_model, get_peft_model_state_dict, set_peft_model_state_dict
208
+ from transformers import AutoModelForImageTextToText, AutoProcessor
209
+
210
+ args.output_dir.mkdir(parents=True, exist_ok=True)
211
+ progress_path = args.output_dir / "progress.jsonl"
212
+ checkpoint_path = args.output_dir / "latest_training_state.pt"
213
+ best_model_path = args.output_dir / "best_model_state.pt"
214
+ eval_path = args.output_dir / "id_dev_metrics.jsonl"
215
+ manifest_sha256 = hashlib.sha256(args.manifest.read_bytes()).hexdigest()
216
+ rows = read_rows(args.manifest)
217
+ dev_rows = read_dev_rows(args.manifest)
218
+ processor = AutoProcessor.from_pretrained(args.snapshot, local_files_only=True, padding_side="right")
219
+ for token in SPECIAL_TOKENS:
220
+ if token not in processor.tokenizer.get_vocab():
221
+ processor.tokenizer.add_special_tokens({"additional_special_tokens": [token]})
222
+ token_ids = {token: int(processor.tokenizer.convert_tokens_to_ids(token)) for token in SPECIAL_TOKENS}
223
+ model = AutoModelForImageTextToText.from_pretrained(
224
+ args.snapshot,
225
+ local_files_only=True,
226
+ dtype=torch.bfloat16,
227
+ attn_implementation="flash_attention_2",
228
+ device_map={"": 0},
229
+ )
230
+ model.resize_token_embeddings(len(processor.tokenizer))
231
+ for parameter in model.parameters():
232
+ parameter.requires_grad_(False)
233
+ model.model.language_model = get_peft_model(
234
+ model.model.language_model,
235
+ LoraConfig(
236
+ r=8,
237
+ lora_alpha=16,
238
+ lora_dropout=0.05,
239
+ bias="none",
240
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
241
+ trainable_token_indices=list(token_ids.values()),
242
+ ),
243
+ )
244
+ model.config.use_cache = False
245
+ model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
246
+ model.enable_input_require_grads()
247
+ hidden_size = int(model.config.text_config.hidden_size)
248
+ heads = RobometerHeads(hidden_size).to(device="cuda", dtype=torch.bfloat16)
249
+ backbone_parameters = [parameter for parameter in model.parameters() if parameter.requires_grad]
250
+ head_parameters = list(heads.parameters())
251
+ optimizer = torch.optim.AdamW(
252
+ [
253
+ {"params": backbone_parameters, "lr": args.lora_lr},
254
+ {"params": head_parameters, "lr": args.head_lr},
255
+ ],
256
+ weight_decay=0.01,
257
+ )
258
+ warmup_steps = max(1, round(args.optimizer_steps * 0.05))
259
+
260
+ def lr_lambda(step: int) -> float:
261
+ if step < warmup_steps:
262
+ return (step + 1) / warmup_steps
263
+ progress = (step - warmup_steps) / max(1, args.optimizer_steps - warmup_steps)
264
+ return 0.5 * (1.0 + math.cos(math.pi * progress))
265
+
266
+ scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
267
+ generator = np.random.default_rng(args.seed)
268
+ order = generator.permutation(len(rows)).tolist()
269
+ cursor = 0
270
+ start_step = 0
271
+ step_seconds: list[float] = []
272
+ eval_history: list[dict] = []
273
+ best_eval_loss = math.inf
274
+ best_step = 0
275
+ bad_eval_count = 0
276
+ pos_weight = torch.tensor(global_success_pos_weight(rows), dtype=torch.float32, device="cuda")
277
+
278
+ if args.resume:
279
+ if not checkpoint_path.is_file():
280
+ raise FileNotFoundError(checkpoint_path)
281
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
282
+ if checkpoint.get("protocol_id") != "phase21a_robometer_low_lora_train_v2":
283
+ raise RuntimeError("refusing to resume a checkpoint from a different protocol")
284
+ if checkpoint["manifest_sha256"] != manifest_sha256 or checkpoint["seed"] != args.seed:
285
+ raise RuntimeError("resume manifest or seed mismatch")
286
+ set_peft_model_state_dict(model.model.language_model, checkpoint["adapter_state"])
287
+ heads.load_state_dict(checkpoint["heads_state"])
288
+ optimizer.load_state_dict(checkpoint["optimizer_state"])
289
+ scheduler.load_state_dict(checkpoint["scheduler_state"])
290
+ generator.bit_generator.state = checkpoint["numpy_generator_state"]
291
+ order = checkpoint["order"]
292
+ cursor = checkpoint["cursor"]
293
+ start_step = checkpoint["completed_steps"]
294
+ step_seconds = checkpoint["step_seconds"]
295
+ eval_history = checkpoint.get("eval_history", [])
296
+ best_eval_loss = float(checkpoint.get("best_eval_loss", math.inf))
297
+ best_step = int(checkpoint.get("best_step", 0))
298
+ bad_eval_count = int(checkpoint.get("bad_eval_count", 0))
299
+ torch.set_rng_state(checkpoint["torch_rng_state"])
300
+ torch.cuda.set_rng_state_all(checkpoint["cuda_rng_state_all"])
301
+ reconcile_progress_log(progress_path, start_step)
302
+ reconcile_progress_log(eval_path, start_step)
303
+ if start_step >= args.optimizer_steps:
304
+ print(json.dumps({"status": "already_complete", "completed_steps": start_step}), flush=True)
305
+ return
306
+
307
+ model.train()
308
+ heads.train()
309
+ torch.cuda.empty_cache()
310
+ torch.cuda.reset_peak_memory_stats()
311
+ named_trainable = list(model.named_parameters()) + [
312
+ (f"heads.{name}", parameter) for name, parameter in heads.named_parameters()
313
+ ]
314
+ completed = start_step
315
+ stop_reason = "max_steps"
316
+ for step in range(start_step, args.optimizer_steps):
317
+ if cursor + 64 > len(order):
318
+ order = generator.permutation(len(rows)).tolist()
319
+ cursor = 0
320
+ selected_indices = order[cursor : cursor + 64]
321
+ cursor += 64
322
+ selected_rows = [rows[index] for index in selected_indices]
323
+ optimizer.zero_grad(set_to_none=True)
324
+ components = {"preference": 0.0, "progress": 0.0, "success": 0.0}
325
+ sequence_min = 10**9
326
+ sequence_max = 0
327
+ torch.cuda.synchronize()
328
+ step_started = time.perf_counter()
329
+ for microstep in range(args.gradient_accumulation):
330
+ batch_rows = selected_rows[
331
+ microstep * args.micro_batch_pairs : (microstep + 1) * args.micro_batch_pairs
332
+ ]
333
+ conversations = []
334
+ records_a = []
335
+ preference_labels = []
336
+ for offset, row in enumerate(batch_rows):
337
+ global_presentation = step * 64 + microstep * args.micro_batch_pairs + offset
338
+ chosen_is_a = global_presentation % 2 == 0
339
+ conversation, record_a, label = build_conversation(row, args.processed_root, chosen_is_a)
340
+ conversations.append(conversation)
341
+ records_a.append(record_a)
342
+ preference_labels.append(label)
343
+ inputs = process_batch(processor, conversations)
344
+ lengths = inputs["attention_mask"].sum(1)
345
+ sequence_min = min(sequence_min, int(lengths.min()))
346
+ sequence_max = max(sequence_max, int(lengths.max()))
347
+ inputs = {key: value.to("cuda") for key, value in inputs.items()}
348
+ targets = make_targets(records_a, torch.device("cuda"))
349
+ preference_target = torch.tensor(preference_labels, dtype=torch.float32, device="cuda")
350
+ output = model.model(**inputs, output_hidden_states=False, use_cache=False)
351
+ hidden = output.last_hidden_state
352
+ prog_mask = inputs["input_ids"].eq(token_ids["<|prog_token|>"])
353
+ pref_mask = inputs["input_ids"].eq(token_ids["<|pref_token|>"])
354
+ if not torch.equal(prog_mask.sum(1), torch.full_like(prog_mask.sum(1), 16)):
355
+ raise RuntimeError("progress token extraction mismatch")
356
+ if not torch.equal(pref_mask.sum(1), torch.ones_like(pref_mask.sum(1))):
357
+ raise RuntimeError("preference token extraction mismatch")
358
+ prog_hidden = hidden[prog_mask].reshape(args.micro_batch_pairs, 16, hidden_size)
359
+ pref_hidden = hidden[pref_mask].reshape(args.micro_batch_pairs, hidden_size)
360
+ progress_logits = heads.progress_head(prog_hidden[:, :8])
361
+ success_logits = heads.success_head(prog_hidden[:, :8]).squeeze(-1)
362
+ preference_logits = heads.preference_head(pref_hidden).squeeze(-1)
363
+ if targets["progress_mask"].any():
364
+ progress_frames = -(
365
+ targets["progress"] * F.log_softmax(progress_logits.float(), dim=-1)
366
+ ).sum(-1)
367
+ progress_loss = progress_frames[targets["progress_mask"]].mean()
368
+ else:
369
+ raise RuntimeError("formal micro-batch has empty progress supervision")
370
+ success_loss = F.binary_cross_entropy_with_logits(
371
+ success_logits.float()[targets["success_mask"]],
372
+ targets["success"][targets["success_mask"]],
373
+ pos_weight=pos_weight,
374
+ )
375
+ preference_loss = F.binary_cross_entropy_with_logits(
376
+ preference_logits.float(), preference_target
377
+ )
378
+ total_loss = preference_loss + progress_loss + success_loss
379
+ if not torch.isfinite(total_loss):
380
+ raise FloatingPointError(f"non-finite loss at step {step + 1}")
381
+ (total_loss / args.gradient_accumulation).backward()
382
+ components["preference"] += float(preference_loss.detach()) / args.gradient_accumulation
383
+ components["progress"] += float(progress_loss.detach()) / args.gradient_accumulation
384
+ components["success"] += float(success_loss.detach()) / args.gradient_accumulation
385
+ finite, grad_norm, grad_tensors = finite_grad_audit(named_trainable)
386
+ if not finite:
387
+ raise FloatingPointError(f"non-finite gradient at step {step + 1}")
388
+ group_norms = {
389
+ "lora": group_grad_norm(named_trainable, "lora_"),
390
+ "token_rows": group_grad_norm(named_trainable, "trainable_tokens"),
391
+ "progress_head": group_grad_norm(named_trainable, "heads.progress_head"),
392
+ "success_head": group_grad_norm(named_trainable, "heads.success_head"),
393
+ "preference_head": group_grad_norm(named_trainable, "heads.preference_head"),
394
+ }
395
+ if any(value <= 0 for value in group_norms.values()):
396
+ raise RuntimeError(f"missing gradient group: {group_norms}")
397
+ torch.nn.utils.clip_grad_norm_(backbone_parameters + head_parameters, 1.0)
398
+ optimizer.step()
399
+ scheduler.step()
400
+ torch.cuda.synchronize()
401
+ elapsed = time.perf_counter() - step_started
402
+ step_seconds.append(elapsed)
403
+ record = {
404
+ "step": step + 1,
405
+ "loss": sum(components.values()),
406
+ "loss_components": components,
407
+ "gradient_norm_preclip": grad_norm,
408
+ "gradient_tensor_count": grad_tensors,
409
+ "gradient_group_norms": group_norms,
410
+ "step_seconds": elapsed,
411
+ "lora_lr": optimizer.param_groups[0]["lr"],
412
+ "head_lr": optimizer.param_groups[1]["lr"],
413
+ "sequence_tokens_min": sequence_min,
414
+ "sequence_tokens_max": sequence_max,
415
+ "peak_allocated_gib": torch.cuda.max_memory_allocated() / 2**30,
416
+ "peak_reserved_gib": torch.cuda.max_memory_reserved() / 2**30,
417
+ }
418
+ with progress_path.open("a", encoding="utf-8") as stream:
419
+ stream.write(json.dumps(record, sort_keys=True) + "\n")
420
+ print(json.dumps(record, sort_keys=True), flush=True)
421
+ completed = step + 1
422
+ should_evaluate = completed % args.eval_every == 0 or completed == args.optimizer_steps
423
+ if should_evaluate:
424
+ metrics = evaluate_dev(
425
+ model=model,
426
+ processor=processor,
427
+ heads=heads,
428
+ rows=dev_rows,
429
+ processed_root=args.processed_root,
430
+ token_ids=token_ids,
431
+ hidden_size=hidden_size,
432
+ pos_weight=pos_weight,
433
+ micro_batch_pairs=args.eval_micro_batch_pairs,
434
+ )
435
+ metrics["step"] = completed
436
+ eval_history.append(metrics)
437
+ with eval_path.open("a", encoding="utf-8") as stream:
438
+ stream.write(json.dumps(metrics, sort_keys=True) + "\n")
439
+ print(json.dumps({"id_dev": metrics}, sort_keys=True), flush=True)
440
+ if metrics["loss"] < best_eval_loss - args.early_stop_min_delta:
441
+ best_eval_loss = float(metrics["loss"])
442
+ best_step = completed
443
+ bad_eval_count = 0
444
+ atomic_torch_save(
445
+ {
446
+ "protocol_id": "phase21a_robometer_low_lora_train_v2",
447
+ "selected_step": best_step,
448
+ "id_dev_metrics": metrics,
449
+ "adapter_state": cpu_state_dict(
450
+ get_peft_model_state_dict(model.model.language_model)
451
+ ),
452
+ "heads_state": cpu_state_dict(heads.state_dict()),
453
+ },
454
+ best_model_path,
455
+ )
456
+ else:
457
+ bad_eval_count += 1
458
+
459
+ should_checkpoint = completed % args.checkpoint_every == 0 or completed == args.optimizer_steps
460
+ if should_checkpoint:
461
+ atomic_torch_save(
462
+ {
463
+ "protocol_id": "phase21a_robometer_low_lora_train_v2",
464
+ "completed_steps": completed,
465
+ "target_steps": args.optimizer_steps,
466
+ "seed": args.seed,
467
+ "manifest_sha256": manifest_sha256,
468
+ "adapter_state": get_peft_model_state_dict(model.model.language_model),
469
+ "heads_state": heads.state_dict(),
470
+ "optimizer_state": optimizer.state_dict(),
471
+ "scheduler_state": scheduler.state_dict(),
472
+ "numpy_generator_state": generator.bit_generator.state,
473
+ "order": order,
474
+ "cursor": cursor,
475
+ "step_seconds": step_seconds,
476
+ "eval_history": eval_history,
477
+ "best_eval_loss": best_eval_loss,
478
+ "best_step": best_step,
479
+ "bad_eval_count": bad_eval_count,
480
+ "torch_rng_state": torch.get_rng_state(),
481
+ "cuda_rng_state_all": torch.cuda.get_rng_state_all(),
482
+ },
483
+ checkpoint_path,
484
+ )
485
+ if (
486
+ should_evaluate
487
+ and completed >= args.early_stop_min_steps
488
+ and bad_eval_count >= args.early_stop_patience_evals
489
+ ):
490
+ stop_reason = "id_dev_early_stop"
491
+ break
492
+ if not best_model_path.is_file():
493
+ raise RuntimeError("training completed without an ID-dev model selection checkpoint")
494
+ best_model = torch.load(best_model_path, map_location="cpu", weights_only=False)
495
+ set_peft_model_state_dict(model.model.language_model, best_model["adapter_state"])
496
+ heads.load_state_dict(best_model["heads_state"])
497
+ adapter_dir = args.output_dir / "adapter"
498
+ model.model.language_model.save_pretrained(adapter_dir)
499
+ torch.save(heads.state_dict(), args.output_dir / "reward_heads.pt")
500
+ summary = {
501
+ "status": "training_complete",
502
+ "protocol_id": "phase21a_robometer_low_lora_train_v2",
503
+ "seed": args.seed,
504
+ "optimizer_steps_cap": args.optimizer_steps,
505
+ "completed_steps": completed,
506
+ "selected_step": best_step,
507
+ "best_id_dev_loss": best_eval_loss,
508
+ "stop_reason": stop_reason,
509
+ "effective_batch_pairs": 64,
510
+ "mean_step_seconds": float(np.mean(step_seconds)),
511
+ "total_training_hours": float(np.sum(step_seconds) / 3600),
512
+ "final_ood_used": False,
513
+ }
514
+ (args.output_dir / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
515
+ print(json.dumps(summary, indent=2), flush=True)
516
+
517
+
518
+ if __name__ == "__main__":
519
+ main()
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Experiment-local implementation modules."""
src/data_contract.py ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Backbone-neutral trajectory contract for Route A.
2
+
3
+ The module deliberately stops before model-specific normalization. Every backbone
4
+ therefore receives the same selected RGB frames, labels and masks.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ from dataclasses import dataclass, replace
12
+ from pathlib import Path
13
+ from typing import Any, Iterable, Literal, Sequence
14
+
15
+ import numpy as np
16
+ import torch
17
+ from PIL import Image
18
+
19
+
20
+ TemporalStrategy = Literal["forward", "reverse", "rewind", "different_task"]
21
+
22
+
23
+ def deterministic_frame_indices(
24
+ frame_count: int, target_frames: int = 8
25
+ ) -> tuple[np.ndarray, np.ndarray]:
26
+ """Return official-style rint(linspace) indices and a real-frame mask."""
27
+
28
+ if frame_count <= 0 or target_frames <= 0:
29
+ raise ValueError("frame_count and target_frames must be positive")
30
+ if frame_count >= target_frames:
31
+ indices = np.rint(
32
+ np.linspace(0, frame_count - 1, target_frames)
33
+ ).astype(np.int64)
34
+ indices[0] = 0
35
+ indices[-1] = frame_count - 1
36
+ return indices, np.ones(target_frames, dtype=np.bool_)
37
+
38
+ real = np.arange(frame_count, dtype=np.int64)
39
+ padding = np.full(target_frames - frame_count, frame_count - 1, dtype=np.int64)
40
+ valid = np.concatenate(
41
+ [
42
+ np.ones(frame_count, dtype=np.bool_),
43
+ np.zeros(target_frames - frame_count, dtype=np.bool_),
44
+ ]
45
+ )
46
+ return np.concatenate([real, padding]), valid
47
+
48
+
49
+ def temporal_permutation(length: int, strategy: TemporalStrategy) -> np.ndarray:
50
+ """Return a permutation shared by frames, labels, indices and masks."""
51
+
52
+ if length <= 0:
53
+ raise ValueError("length must be positive")
54
+ if strategy in ("forward", "different_task"):
55
+ return np.arange(length, dtype=np.int64)
56
+ if strategy == "reverse":
57
+ return np.arange(length - 1, -1, -1, dtype=np.int64)
58
+ if strategy == "rewind":
59
+ split = length // 2
60
+ return np.concatenate(
61
+ [
62
+ np.arange(split, dtype=np.int64),
63
+ np.arange(length - 1, split - 1, -1, dtype=np.int64),
64
+ ]
65
+ )
66
+ raise ValueError(f"unsupported temporal strategy: {strategy}")
67
+
68
+
69
+ def letterbox_rgb(frame: np.ndarray, canvas_size: int = 256) -> np.ndarray:
70
+ """Deterministically resize an RGB uint8 frame onto a square black canvas."""
71
+
72
+ if (
73
+ frame.ndim != 3
74
+ or frame.shape[-1] != 3
75
+ or frame.dtype != np.uint8
76
+ or canvas_size <= 0
77
+ ):
78
+ raise ValueError("frame must be uint8 [H,W,3] and canvas_size must be positive")
79
+ height, width = frame.shape[:2]
80
+ if height <= 0 or width <= 0:
81
+ raise ValueError("frame dimensions must be positive")
82
+ scale = min(canvas_size / width, canvas_size / height)
83
+ resized_width = max(1, min(canvas_size, round(width * scale)))
84
+ resized_height = max(1, min(canvas_size, round(height * scale)))
85
+ resized = Image.fromarray(frame, mode="RGB").resize(
86
+ (resized_width, resized_height), Image.Resampling.BILINEAR
87
+ )
88
+ canvas = Image.new("RGB", (canvas_size, canvas_size), (0, 0, 0))
89
+ offset = (
90
+ (canvas_size - resized_width) // 2,
91
+ (canvas_size - resized_height) // 2,
92
+ )
93
+ canvas.paste(resized, offset)
94
+ return np.asarray(canvas, dtype=np.uint8)
95
+
96
+
97
+ def _c51_targets(values: np.ndarray, bins: int) -> np.ndarray:
98
+ if bins < 2:
99
+ raise ValueError("progress bins must be at least 2")
100
+ positions = np.clip(values, 0.0, 1.0) * (bins - 1)
101
+ lower = np.floor(positions).astype(np.int64)
102
+ upper = np.ceil(positions).astype(np.int64)
103
+ output = np.zeros((len(values), bins), dtype=np.float32)
104
+ rows = np.arange(len(values))
105
+ output[rows, lower] += upper - positions
106
+ output[rows, upper] += positions - lower
107
+ exact = lower == upper
108
+ output[rows[exact], lower[exact]] = 1.0
109
+ return output
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class TrajectoryContract:
114
+ trajectory_id: str
115
+ task: str
116
+ data_source: str
117
+ quality_label: str
118
+ frames_uint8: np.ndarray
119
+ source_frame_indices: np.ndarray
120
+ frame_valid_mask: np.ndarray
121
+ progress: np.ndarray
122
+ progress_distribution: np.ndarray
123
+ progress_loss_mask: np.ndarray
124
+ success_labels: np.ndarray
125
+ success_loss_mask: np.ndarray
126
+ strategy: TemporalStrategy
127
+ metadata: dict[str, Any]
128
+
129
+ def validate(self) -> None:
130
+ frame_count = self.frames_uint8.shape[0]
131
+ if self.frames_uint8.dtype != np.uint8 or self.frames_uint8.ndim != 4:
132
+ raise ValueError("frames_uint8 must be uint8 [T,H,W,3]")
133
+ if self.frames_uint8.shape[-1] != 3:
134
+ raise ValueError("frames_uint8 must have three RGB channels")
135
+ aligned = (
136
+ self.source_frame_indices,
137
+ self.frame_valid_mask,
138
+ self.progress,
139
+ self.progress_distribution,
140
+ self.progress_loss_mask,
141
+ self.success_labels,
142
+ self.success_loss_mask,
143
+ )
144
+ if any(value.shape[0] != frame_count for value in aligned):
145
+ raise ValueError("all frame-level fields must have the same leading size")
146
+ if np.any(self.progress < 0) or np.any(self.progress > 1):
147
+ raise ValueError("progress values must be in [0,1]")
148
+ if np.any(self.progress_loss_mask & ~self.frame_valid_mask):
149
+ raise ValueError("padding may not enter progress loss")
150
+ if np.any(self.success_loss_mask & ~self.frame_valid_mask):
151
+ raise ValueError("padding may not enter success loss")
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class PreferenceContract:
156
+ trajectory_a: TrajectoryContract
157
+ trajectory_b: TrajectoryContract
158
+ preference_label: float
159
+ strategy: str
160
+
161
+ def swapped(self) -> PreferenceContract:
162
+ return replace(
163
+ self,
164
+ trajectory_a=self.trajectory_b,
165
+ trajectory_b=self.trajectory_a,
166
+ preference_label=1.0 - self.preference_label,
167
+ )
168
+
169
+
170
+ def prepare_trajectory(
171
+ *,
172
+ trajectory_id: str,
173
+ frames: np.ndarray,
174
+ task: str,
175
+ data_source: str,
176
+ quality_label: str,
177
+ partial_success: float | None = None,
178
+ strategy: TemporalStrategy = "forward",
179
+ target_frames: int = 8,
180
+ canvas_size: int = 256,
181
+ progress_bins: int = 10,
182
+ success_cutoff: float = 1.0,
183
+ metadata: dict[str, Any] | None = None,
184
+ ) -> TrajectoryContract:
185
+ """Materialize one auditable trajectory from an official sampler trajectory."""
186
+
187
+ if frames.ndim != 4 or frames.shape[-1] != 3 or frames.dtype != np.uint8:
188
+ raise ValueError("frames must be uint8 [T,H,W,3]")
189
+ if not trajectory_id or not task or not data_source:
190
+ raise ValueError("trajectory_id, task and data_source are required")
191
+ if partial_success is not None and not 0.0 <= partial_success <= 1.0:
192
+ raise ValueError("partial_success must be in [0,1]")
193
+ if not 0.0 <= success_cutoff <= 1.0:
194
+ raise ValueError("success_cutoff must be in [0,1]")
195
+
196
+ selected_indices, frame_valid = deterministic_frame_indices(
197
+ len(frames), target_frames
198
+ )
199
+ permutation = temporal_permutation(target_frames, strategy)
200
+ selected_indices = selected_indices[permutation]
201
+ frame_valid = frame_valid[permutation]
202
+ selected_frames = np.stack(
203
+ [letterbox_rgb(frames[index], canvas_size) for index in selected_indices]
204
+ )
205
+
206
+ denominator = max(len(frames) - 1, 1)
207
+ progress = selected_indices.astype(np.float32) / denominator
208
+ if partial_success is not None:
209
+ progress *= partial_success
210
+ if strategy == "different_task":
211
+ progress.fill(0.0)
212
+
213
+ quality = quality_label.lower()
214
+ dense_progress_allowed = quality not in {"failure", "failed", "suboptimal"}
215
+ progress_loss_mask = frame_valid & dense_progress_allowed
216
+ if strategy == "different_task":
217
+ progress_loss_mask = frame_valid.copy()
218
+ success_labels = (
219
+ (progress >= success_cutoff)
220
+ & (quality == "successful")
221
+ & frame_valid
222
+ ).astype(np.float32)
223
+ success_loss_mask = frame_valid.copy()
224
+ audit_metadata = dict(metadata or {})
225
+ audit_metadata.update(
226
+ {
227
+ "source_frame_count": int(len(frames)),
228
+ "selected_frame_indices": selected_indices.tolist(),
229
+ "frame_valid_mask": frame_valid.tolist(),
230
+ "temporal_strategy": strategy,
231
+ "canvas_size": canvas_size,
232
+ "letterbox_fill_rgb": [0, 0, 0],
233
+ }
234
+ )
235
+ result = TrajectoryContract(
236
+ trajectory_id=trajectory_id,
237
+ task=task,
238
+ data_source=data_source,
239
+ quality_label=quality_label,
240
+ frames_uint8=selected_frames,
241
+ source_frame_indices=selected_indices,
242
+ frame_valid_mask=frame_valid,
243
+ progress=progress,
244
+ progress_distribution=_c51_targets(progress, progress_bins),
245
+ progress_loss_mask=progress_loss_mask,
246
+ success_labels=success_labels,
247
+ success_loss_mask=success_loss_mask,
248
+ strategy=strategy,
249
+ metadata=audit_metadata,
250
+ )
251
+ result.validate()
252
+ return result
253
+
254
+
255
+ def adapt_official_trajectory(
256
+ trajectory: Any,
257
+ *,
258
+ strategy: TemporalStrategy,
259
+ progress_supervised: bool,
260
+ canvas_size: int = 256,
261
+ progress_bins: int = 10,
262
+ ) -> TrajectoryContract:
263
+ """Adapt an official Robometer sampler trajectory without changing its labels.
264
+
265
+ The sampler must expose its selected source indices and padding mask in
266
+ ``trajectory.metadata``. This is intentionally strict: silently re-sampling
267
+ here would invalidate the matched-backbone comparison.
268
+ """
269
+
270
+ metadata = dict(trajectory.metadata or {})
271
+ if "selected_frame_indices" not in metadata or "frame_valid_mask" not in metadata:
272
+ raise ValueError("official sampler metadata lacks frozen frame indices/mask")
273
+ frames = np.asarray(trajectory.frames)
274
+ if frames.ndim != 4 or frames.shape[-1] != 3 or frames.dtype != np.uint8:
275
+ raise ValueError("official sampler frames must be uint8 [T,H,W,3]")
276
+ selected_indices = np.asarray(metadata["selected_frame_indices"], dtype=np.int64)
277
+ frame_valid = np.asarray(metadata["frame_valid_mask"], dtype=np.bool_)
278
+ if len(selected_indices) != len(frames) or len(frame_valid) != len(frames):
279
+ raise ValueError("official sampler frame metadata is not aligned")
280
+
281
+ target_progress = trajectory.target_progress
282
+ if target_progress is None or len(target_progress) != len(frames):
283
+ raise ValueError("official target_progress is missing or misaligned")
284
+ first_target = target_progress[0]
285
+ if isinstance(first_target, torch.Tensor) or np.asarray(first_target).ndim > 0:
286
+ progress_distribution = np.stack(
287
+ [np.asarray(value, dtype=np.float32) for value in target_progress]
288
+ )
289
+ if progress_distribution.shape != (len(frames), progress_bins):
290
+ raise ValueError("official discrete progress target has unexpected shape")
291
+ support = np.linspace(0.0, 1.0, progress_bins, dtype=np.float32)
292
+ progress = progress_distribution @ support
293
+ else:
294
+ progress = np.asarray(target_progress, dtype=np.float32)
295
+ progress_distribution = _c51_targets(progress, progress_bins)
296
+
297
+ predict_mask = trajectory.predict_last_frame_mask
298
+ if predict_mask is None:
299
+ predict_valid = np.ones(len(frames), dtype=np.bool_)
300
+ else:
301
+ predict_valid = np.asarray(predict_mask, dtype=np.float32) > 0
302
+ progress_loss_mask = frame_valid & predict_valid & progress_supervised
303
+ success_labels = np.asarray(trajectory.success_label, dtype=np.float32)
304
+ if success_labels.shape != (len(frames),):
305
+ raise ValueError("official success labels are missing or misaligned")
306
+
307
+ metadata.update(
308
+ {
309
+ "adapter": "official_robometer_sampler_v1",
310
+ "temporal_strategy": strategy,
311
+ "canvas_size": canvas_size,
312
+ "letterbox_fill_rgb": [0, 0, 0],
313
+ }
314
+ )
315
+ result = TrajectoryContract(
316
+ trajectory_id=trajectory.id,
317
+ task=trajectory.task,
318
+ data_source=trajectory.data_source,
319
+ quality_label=trajectory.quality_label,
320
+ frames_uint8=np.stack(
321
+ [letterbox_rgb(frame, canvas_size) for frame in frames]
322
+ ),
323
+ source_frame_indices=selected_indices,
324
+ frame_valid_mask=frame_valid,
325
+ progress=progress,
326
+ progress_distribution=progress_distribution,
327
+ progress_loss_mask=progress_loss_mask,
328
+ success_labels=success_labels,
329
+ success_loss_mask=frame_valid.copy(),
330
+ strategy=strategy,
331
+ metadata=metadata,
332
+ )
333
+ result.validate()
334
+ return result
335
+
336
+
337
+ def make_preference_contract(
338
+ chosen: TrajectoryContract,
339
+ rejected: TrajectoryContract,
340
+ *,
341
+ strategy: str,
342
+ chosen_first: bool = True,
343
+ ) -> PreferenceContract:
344
+ if chosen_first:
345
+ return PreferenceContract(chosen, rejected, 1.0, strategy)
346
+ return PreferenceContract(rejected, chosen, 0.0, strategy)
347
+
348
+
349
+ class CosmosRBMCollator:
350
+ """Stack the frozen contract without invoking a backbone processor."""
351
+
352
+ schema_version = "cosmos_rbm_batch_v1"
353
+
354
+ @staticmethod
355
+ def _stack_trajectories(items: Sequence[TrajectoryContract]) -> dict[str, Any]:
356
+ for item in items:
357
+ item.validate()
358
+ return {
359
+ "frames_uint8": torch.from_numpy(
360
+ np.stack([item.frames_uint8 for item in items])
361
+ ),
362
+ "source_frame_indices": torch.from_numpy(
363
+ np.stack([item.source_frame_indices for item in items])
364
+ ),
365
+ "frame_valid_mask": torch.from_numpy(
366
+ np.stack([item.frame_valid_mask for item in items])
367
+ ),
368
+ "progress": torch.from_numpy(np.stack([item.progress for item in items])),
369
+ "progress_distribution": torch.from_numpy(
370
+ np.stack([item.progress_distribution for item in items])
371
+ ),
372
+ "progress_loss_mask": torch.from_numpy(
373
+ np.stack([item.progress_loss_mask for item in items])
374
+ ),
375
+ "success_labels": torch.from_numpy(
376
+ np.stack([item.success_labels for item in items])
377
+ ),
378
+ "success_loss_mask": torch.from_numpy(
379
+ np.stack([item.success_loss_mask for item in items])
380
+ ),
381
+ "trajectory_ids": [item.trajectory_id for item in items],
382
+ "tasks": [item.task for item in items],
383
+ "data_sources": [item.data_source for item in items],
384
+ "quality_labels": [item.quality_label for item in items],
385
+ "strategies": [item.strategy for item in items],
386
+ "metadata": [item.metadata for item in items],
387
+ }
388
+
389
+ def __call__(
390
+ self, samples: Sequence[TrajectoryContract | PreferenceContract]
391
+ ) -> dict[str, Any]:
392
+ trajectories = [item for item in samples if isinstance(item, TrajectoryContract)]
393
+ preferences = [item for item in samples if isinstance(item, PreferenceContract)]
394
+ output: dict[str, Any] = {
395
+ "schema_version": self.schema_version,
396
+ "num_trajectories": len(trajectories),
397
+ "num_preferences": len(preferences),
398
+ }
399
+ if trajectories:
400
+ output["trajectory_inputs"] = self._stack_trajectories(trajectories)
401
+ if preferences:
402
+ output["preference_inputs"] = {
403
+ "trajectory_a": self._stack_trajectories(
404
+ [item.trajectory_a for item in preferences]
405
+ ),
406
+ "trajectory_b": self._stack_trajectories(
407
+ [item.trajectory_b for item in preferences]
408
+ ),
409
+ "preference_labels": torch.tensor(
410
+ [item.preference_label for item in preferences], dtype=torch.float32
411
+ ),
412
+ "strategies": [item.strategy for item in preferences],
413
+ }
414
+ return output
415
+
416
+
417
+ def validate_span_bounds(
418
+ bounds: Sequence[tuple[int, int]], frame_count: int, sequence_length: int
419
+ ) -> None:
420
+ """Validate the one-frame/one-nonoverlapping-token-span contract."""
421
+
422
+ if len(bounds) != frame_count:
423
+ raise ValueError("visual span count must match frame count")
424
+ previous_stop = -1
425
+ for start, stop in bounds:
426
+ if start < 0 or stop <= start or stop > sequence_length:
427
+ raise ValueError("visual token span is empty or out of bounds")
428
+ if start < previous_stop:
429
+ raise ValueError("visual token spans overlap")
430
+ previous_stop = stop
431
+
432
+
433
+ def worker_invariant_sample_ids(
434
+ ids: Iterable[str], *, seed: int, worker_count: int
435
+ ) -> list[str]:
436
+ """Simulate round-robin worker sharding while preserving global sample order."""
437
+
438
+ if worker_count <= 0:
439
+ raise ValueError("worker_count must be positive")
440
+ materialized = list(ids)
441
+ unique = list(dict.fromkeys(materialized))
442
+ if len(unique) != len(materialized):
443
+ raise ValueError("sample ids must be unique")
444
+ ranked = sorted(
445
+ unique,
446
+ key=lambda value: hashlib.sha256(f"{seed}:{value}".encode()).digest(),
447
+ )
448
+ shards = [list(enumerate(ranked))[worker::worker_count] for worker in range(worker_count)]
449
+ return [value for _, value in sorted(item for shard in shards for item in shard)]
450
+
451
+
452
+ def audit_group_overlap(train_ids: Iterable[str], eval_ids: Iterable[str]) -> set[str]:
453
+ return set(train_ids) & set(eval_ids)
454
+
455
+
456
+ def dump_batch_audit(batch: dict[str, Any], output: Path) -> None:
457
+ """Write metadata and tensor shapes only; never dump trajectory pixels."""
458
+
459
+ def describe(value: Any) -> Any:
460
+ if isinstance(value, torch.Tensor):
461
+ return {"shape": list(value.shape), "dtype": str(value.dtype)}
462
+ if isinstance(value, dict):
463
+ return {key: describe(item) for key, item in value.items()}
464
+ return value
465
+
466
+ output.parent.mkdir(parents=True, exist_ok=True)
467
+ output.write_text(json.dumps(describe(batch), indent=2) + "\n", encoding="utf-8")
src/reward_heads.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared frame pooling, temporal pooling and reward heads for B1/C0/C1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SpanPoolResult:
14
+ frame_features: torch.Tensor
15
+ frame_mask: torch.Tensor
16
+ span_bounds: tuple[tuple[tuple[int, int], ...], ...]
17
+
18
+
19
+ def pool_image_token_spans(
20
+ hidden_states: torch.Tensor,
21
+ input_ids: torch.Tensor,
22
+ image_token_id: int,
23
+ image_grid_thw: torch.Tensor,
24
+ frames_per_sample: list[int],
25
+ spatial_merge_size: int,
26
+ ) -> SpanPoolResult:
27
+ """Mean-pool each image-token span and return padded per-frame features."""
28
+
29
+ if hidden_states.ndim != 3 or input_ids.ndim != 2:
30
+ raise ValueError("hidden_states/input_ids must have shapes [B,S,D] and [B,S]")
31
+ if hidden_states.shape[:2] != input_ids.shape:
32
+ raise ValueError("hidden_states and input_ids batch/sequence dimensions differ")
33
+ if len(frames_per_sample) != hidden_states.shape[0] or any(
34
+ count <= 0 for count in frames_per_sample
35
+ ):
36
+ raise ValueError(
37
+ "frames_per_sample must contain one positive count per batch item"
38
+ )
39
+ if sum(frames_per_sample) != image_grid_thw.shape[0]:
40
+ raise ValueError("frames_per_sample does not match image_grid_thw rows")
41
+ if spatial_merge_size <= 0:
42
+ raise ValueError("spatial_merge_size must be positive")
43
+
44
+ max_frames = max(frames_per_sample)
45
+ features = hidden_states.new_zeros(
46
+ (hidden_states.shape[0], max_frames, hidden_states.shape[-1])
47
+ )
48
+ frame_mask = torch.zeros(
49
+ (hidden_states.shape[0], max_frames),
50
+ dtype=torch.bool,
51
+ device=hidden_states.device,
52
+ )
53
+ all_bounds: list[tuple[tuple[int, int], ...]] = []
54
+ grid_offset = 0
55
+ merge_area = spatial_merge_size**2
56
+
57
+ for batch_index, frame_count in enumerate(frames_per_sample):
58
+ positions = torch.nonzero(
59
+ input_ids[batch_index] == image_token_id, as_tuple=False
60
+ ).flatten()
61
+ token_offset = 0
62
+ bounds: list[tuple[int, int]] = []
63
+ for frame_index in range(frame_count):
64
+ grid_t, grid_h, grid_w = (
65
+ int(value) for value in image_grid_thw[grid_offset + frame_index]
66
+ )
67
+ raw_tokens = grid_t * grid_h * grid_w
68
+ if raw_tokens % merge_area:
69
+ raise ValueError(
70
+ "image token grid is not divisible by spatial merge area"
71
+ )
72
+ span_length = raw_tokens // merge_area
73
+ frame_positions = positions[token_offset : token_offset + span_length]
74
+ if frame_positions.numel() != span_length:
75
+ raise ValueError("image token span is empty or truncated")
76
+ if frame_positions.numel() > 1 and not torch.all(
77
+ frame_positions[1:] == frame_positions[:-1] + 1
78
+ ):
79
+ raise ValueError("image token span is not contiguous")
80
+ start = int(frame_positions[0])
81
+ stop = int(frame_positions[-1]) + 1
82
+ features[batch_index, frame_index] = hidden_states[
83
+ batch_index, frame_positions
84
+ ].mean(dim=0)
85
+ frame_mask[batch_index, frame_index] = True
86
+ bounds.append((start, stop))
87
+ token_offset += span_length
88
+ if token_offset != positions.numel():
89
+ raise ValueError("unassigned image tokens remain after span pooling")
90
+ all_bounds.append(tuple(bounds))
91
+ grid_offset += frame_count
92
+
93
+ return SpanPoolResult(features, frame_mask, tuple(all_bounds))
94
+
95
+
96
+ class MaskedQueryPool(nn.Module):
97
+ """Single-query attention pooling over valid ordered-frame features."""
98
+
99
+ def __init__(self, hidden_size: int) -> None:
100
+ super().__init__()
101
+ self.query = nn.Parameter(torch.empty(hidden_size))
102
+ nn.init.normal_(self.query, mean=0.0, std=hidden_size**-0.5)
103
+
104
+ def forward(
105
+ self, frame_features: torch.Tensor, frame_mask: torch.Tensor
106
+ ) -> torch.Tensor:
107
+ if frame_features.ndim != 3 or frame_mask.shape != frame_features.shape[:2]:
108
+ raise ValueError("expected frame_features [B,T,D] and frame_mask [B,T]")
109
+ if not torch.all(frame_mask.any(dim=1)):
110
+ raise ValueError("every trajectory must contain at least one valid frame")
111
+ scores = torch.einsum("btd,d->bt", frame_features, self.query) / math.sqrt(
112
+ frame_features.shape[-1]
113
+ )
114
+ scores = scores.masked_fill(~frame_mask, torch.finfo(scores.dtype).min)
115
+ weights = torch.softmax(scores.float(), dim=1).to(frame_features.dtype)
116
+ return torch.einsum("bt,btd->bd", weights, frame_features)
117
+
118
+
119
+ class MLPHead(nn.Module):
120
+ def __init__(
121
+ self, hidden_size: int, output_size: int, dropout: float = 0.1
122
+ ) -> None:
123
+ super().__init__()
124
+ bottleneck = hidden_size // 2
125
+ self.net = nn.Sequential(
126
+ nn.Linear(hidden_size, bottleneck),
127
+ nn.LayerNorm(bottleneck),
128
+ nn.GELU(),
129
+ nn.Dropout(dropout),
130
+ nn.Linear(bottleneck, output_size),
131
+ )
132
+
133
+ def forward(self, inputs: torch.Tensor) -> torch.Tensor:
134
+ return self.net(inputs)
135
+
136
+
137
+ class SharedRewardHeads(nn.Module):
138
+ """The identical topology used for frozen probes and matched-LoRA runs."""
139
+
140
+ def __init__(
141
+ self, hidden_size: int, progress_bins: int = 10, dropout: float = 0.1
142
+ ) -> None:
143
+ super().__init__()
144
+ self.temporal_pool = MaskedQueryPool(hidden_size)
145
+ self.progress_head = MLPHead(hidden_size, progress_bins, dropout)
146
+ self.success_head = MLPHead(hidden_size, 1, dropout)
147
+ self.preference_head = MLPHead(hidden_size, 1, dropout)
148
+
149
+ def forward(
150
+ self, frame_features: torch.Tensor, frame_mask: torch.Tensor
151
+ ) -> dict[str, torch.Tensor]:
152
+ trajectory = self.temporal_pool(frame_features, frame_mask)
153
+ return {
154
+ "progress_logits": self.progress_head(frame_features),
155
+ "success_logits": self.success_head(frame_features).squeeze(-1),
156
+ "trajectory_features": trajectory,
157
+ "trajectory_scores": self.preference_head(trajectory).squeeze(-1),
158
+ }
159
+
160
+ def preference_logit(
161
+ self,
162
+ frame_features_a: torch.Tensor,
163
+ frame_mask_a: torch.Tensor,
164
+ frame_features_b: torch.Tensor,
165
+ frame_mask_b: torch.Tensor,
166
+ ) -> torch.Tensor:
167
+ score_a = self.preference_head(
168
+ self.temporal_pool(frame_features_a, frame_mask_a)
169
+ ).squeeze(-1)
170
+ score_b = self.preference_head(
171
+ self.temporal_pool(frame_features_b, frame_mask_b)
172
+ ).squeeze(-1)
173
+ return score_a - score_b
tests/conftest.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
8
+ if str(EXPERIMENT_ROOT) not in sys.path:
9
+ sys.path.insert(0, str(EXPERIMENT_ROOT))
tests/test_data_contract.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pytest
5
+ import torch
6
+ from types import SimpleNamespace
7
+
8
+ from src.data_contract import (
9
+ CosmosRBMCollator,
10
+ adapt_official_trajectory,
11
+ audit_group_overlap,
12
+ deterministic_frame_indices,
13
+ make_preference_contract,
14
+ prepare_trajectory,
15
+ validate_span_bounds,
16
+ worker_invariant_sample_ids,
17
+ )
18
+ from src.reward_heads import SharedRewardHeads
19
+
20
+
21
+ def synthetic_frames(count: int, height: int = 12, width: int = 20) -> np.ndarray:
22
+ frames = np.empty((count, height, width, 3), dtype=np.uint8)
23
+ for index in range(count):
24
+ frames[index].fill(index)
25
+ return frames
26
+
27
+
28
+ def prepared(
29
+ *,
30
+ strategy: str = "forward",
31
+ quality: str = "successful",
32
+ frame_count: int = 13,
33
+ trajectory_id: str = "trajectory-a",
34
+ ):
35
+ return prepare_trajectory(
36
+ trajectory_id=trajectory_id,
37
+ frames=synthetic_frames(frame_count),
38
+ task="insert cable",
39
+ data_source="unit_test",
40
+ quality_label=quality,
41
+ strategy=strategy,
42
+ canvas_size=16,
43
+ )
44
+
45
+
46
+ def test_forward_progress_is_nondecreasing_and_sampling_is_exact() -> None:
47
+ item = prepared()
48
+ expected, _ = deterministic_frame_indices(13, 8)
49
+ assert np.array_equal(item.source_frame_indices, expected)
50
+ assert np.all(np.diff(item.progress) >= 0)
51
+ assert item.frames_uint8.shape == (8, 16, 16, 3)
52
+
53
+
54
+ def test_reverse_keeps_all_frame_fields_synchronized() -> None:
55
+ forward = prepared()
56
+ reverse = prepared(strategy="reverse")
57
+ assert np.array_equal(reverse.source_frame_indices, forward.source_frame_indices[::-1])
58
+ assert np.array_equal(reverse.progress, forward.progress[::-1])
59
+ assert np.array_equal(reverse.frame_valid_mask, forward.frame_valid_mask[::-1])
60
+ assert np.array_equal(reverse.success_labels, forward.success_labels[::-1])
61
+
62
+
63
+ def test_rewind_has_a_real_progress_drop() -> None:
64
+ rewind = prepared(strategy="rewind")
65
+ assert np.any(np.diff(rewind.progress) < 0)
66
+
67
+
68
+ def test_different_task_is_zero_with_supervision() -> None:
69
+ item = prepared(strategy="different_task", quality="failure")
70
+ assert np.all(item.progress == 0)
71
+ assert np.array_equal(item.progress_loss_mask, item.frame_valid_mask)
72
+
73
+
74
+ def test_preference_swap_flips_label_and_bradley_terry_logit() -> None:
75
+ chosen = prepared(trajectory_id="chosen")
76
+ rejected = prepared(strategy="rewind", trajectory_id="rejected")
77
+ pair = make_preference_contract(chosen, rejected, strategy="rewind")
78
+ swapped = pair.swapped()
79
+ assert swapped.preference_label == 0.0
80
+ assert swapped.trajectory_a.trajectory_id == "rejected"
81
+
82
+ torch.manual_seed(7)
83
+ heads = SharedRewardHeads(hidden_size=8, dropout=0.0)
84
+ features_a = torch.randn(1, 8, 8)
85
+ features_b = torch.randn(1, 8, 8)
86
+ mask = torch.ones(1, 8, dtype=torch.bool)
87
+ ab = heads.preference_logit(features_a, mask, features_b, mask)
88
+ ba = heads.preference_logit(features_b, mask, features_a, mask)
89
+ assert torch.allclose(ab, -ba)
90
+
91
+
92
+ def test_padding_never_enters_loss_or_pooling() -> None:
93
+ item = prepared(frame_count=3)
94
+ batch = CosmosRBMCollator()([item])["trajectory_inputs"]
95
+ assert batch["frame_valid_mask"].tolist() == [
96
+ [True, True, True, False, False, False, False, False]
97
+ ]
98
+ assert not batch["progress_loss_mask"][0, 3:].any()
99
+ assert not batch["success_loss_mask"][0, 3:].any()
100
+
101
+ pool_features = torch.randn(1, 8, 4)
102
+ heads = SharedRewardHeads(hidden_size=4, dropout=0.0)
103
+ score_a = heads(pool_features, batch["frame_valid_mask"])["trajectory_scores"]
104
+ pool_features[:, 3:] = 1000
105
+ score_b = heads(pool_features, batch["frame_valid_mask"])["trajectory_scores"]
106
+ assert torch.allclose(score_a, score_b)
107
+
108
+
109
+ def test_failure_does_not_receive_dense_progress() -> None:
110
+ failure = prepared(quality="failure")
111
+ assert not failure.progress_loss_mask.any()
112
+ assert not failure.success_labels.any()
113
+
114
+
115
+ def test_official_sampler_adapter_preserves_labels_and_indices() -> None:
116
+ official_progress = np.linspace(0.0, 1.0, 8, dtype=np.float32)
117
+ official_success = [0.0] * 7 + [1.0]
118
+ trajectory = SimpleNamespace(
119
+ id="official-a",
120
+ task="insert cable",
121
+ data_source="official_unit_test",
122
+ quality_label="successful",
123
+ frames=synthetic_frames(8),
124
+ target_progress=official_progress.tolist(),
125
+ success_label=official_success,
126
+ predict_last_frame_mask=[1.0] * 8,
127
+ metadata={
128
+ "selected_frame_indices": [0, 4, 9, 13, 18, 22, 27, 31],
129
+ "frame_valid_mask": [True] * 8,
130
+ },
131
+ )
132
+ adapted = adapt_official_trajectory(
133
+ trajectory,
134
+ strategy="forward",
135
+ progress_supervised=True,
136
+ canvas_size=16,
137
+ )
138
+ assert np.allclose(adapted.progress, official_progress)
139
+ assert adapted.success_labels.tolist() == official_success
140
+ assert adapted.source_frame_indices.tolist() == trajectory.metadata[
141
+ "selected_frame_indices"
142
+ ]
143
+ assert adapted.metadata["adapter"] == "official_robometer_sampler_v1"
144
+
145
+
146
+ def test_all_backbones_consume_the_same_frozen_indices() -> None:
147
+ frozen, _ = deterministic_frame_indices(32, 8)
148
+ for _backbone in ("qwen3-vl", "cosmos3-edge", "frozen-cosmos"):
149
+ item = prepared(frame_count=32)
150
+ assert np.array_equal(item.source_frame_indices, frozen)
151
+
152
+
153
+ def test_visual_span_contract() -> None:
154
+ validate_span_bounds([(1, 4), (8, 11), (12, 15)], 3, 16)
155
+ with pytest.raises(ValueError, match="overlap"):
156
+ validate_span_bounds([(1, 5), (4, 8)], 2, 10)
157
+ with pytest.raises(ValueError, match="count"):
158
+ validate_span_bounds([(1, 5)], 2, 10)
159
+
160
+
161
+ def test_worker_count_does_not_change_seeded_sampling_list() -> None:
162
+ ids = [f"trajectory-{index}" for index in range(100)]
163
+ expected = worker_invariant_sample_ids(ids, seed=17, worker_count=1)
164
+ assert worker_invariant_sample_ids(ids, seed=17, worker_count=2) == expected
165
+ assert worker_invariant_sample_ids(ids, seed=17, worker_count=8) == expected
166
+
167
+
168
+ def test_group_overlap_gate() -> None:
169
+ assert audit_group_overlap(["a", "b", "c"], ["d", "e"]) == set()
170
+ assert audit_group_overlap(["a", "b"], ["b", "c"]) == {"b"}
171
+
172
+
173
+ def test_one_hundred_trajectories_cpu_collate_and_backward() -> None:
174
+ item = prepared()
175
+ batch = CosmosRBMCollator()([item] * 100)["trajectory_inputs"]
176
+ assert batch["frames_uint8"].shape == (100, 8, 16, 16, 3)
177
+ features = torch.randn(8, 8, 16, requires_grad=True)
178
+ heads = SharedRewardHeads(hidden_size=16, dropout=0.0)
179
+ output = heads(features, batch["frame_valid_mask"][:8])
180
+ loss = output["progress_logits"].float().mean() + output[
181
+ "success_logits"
182
+ ].float().mean()
183
+ loss.backward()
184
+ assert features.grad is not None and torch.isfinite(features.grad).all()