ActionRoPE / code /README.md
teawhite's picture
add docs+code
880dff9 verified
|
Raw History Blame Contribute Delete
46.3 kB
# ActionRoPE
在 Wan2.2-TI2V-5B 上做动作条件世界模型:动作**不走文本**,走 RoPE。
数据是《恶意不息》BOT 漫游录像重建的 per-frame 动作条件 latent。
* 训练分辨率 **832×480**,**81 帧 @16 fps**,对应 latent `[48, 21, 30, 52]`
* infra 用 [DiffSynth-Studio](https://github.com/modelscope/DiffSynth-Studio) 的全量微调链路
* 8×H200 (143 GB)
## 目录
| 路径 | 内容 | 来源 |
|---|---|---|
| `actionrope/` | 本项目代码 | — |
| `tools/` | 数据体检脚本 | — |
| `scripts/` | shell 入口 | — |
| `data/` | 数据集,189 GB(解包后) | `hf download teawhite/EYBX-processed` |
| `models/Wan2.2-TI2V-5B/` | 基座权重,32 GiB | `hf download Wan-AI/Wan2.2-TI2V-5B` |
| `DiffSynth-Studio/` | 训练 infra,v2.1.6 | `git clone` |
| `outputs/` | checkpoint / 日志 | — |
`data/` `models/` `DiffSynth-Studio/` `.venv/` 都在 .gitignore 里。
## 环境
```bash
python3.12 -m venv .venv
.venv/bin/pip install torch torchvision --index-url https://download.pytorch.org/whl/cu128
.venv/bin/pip install transformers "imageio[ffmpeg]" safetensors einops modelscope ftfy \
pandas accelerate peft deepspeed tensorboard av
.venv/bin/pip install -e ./DiffSynth-Studio --no-deps
```
实测版本:torch 2.11.0+cu128 · accelerate 1.14.0 · deepspeed 0.19.6 · peft 0.20.0 ·
transformers 5.16.1(CUDA 13.2 driver 595.71.05)。
## 数据
```bash
hf download teawhite/EYBX-processed --repo-type dataset --local-dir data
bash data/unpack.sh "$PWD/data" # 必须给绝对路径,脚本内部 cd 过
```
解包后:
```
data/latent/pool/ 22,782 个 .pt [48,21,30,52] bf16
data/latent/train_eybx/ 20,983 软链
data/latent/val_eybx/ 1,175 软链 按 leg 整体留出,不要用哈希划分
data/latent/pool_heldout/ 159 软链 组合泛化评测参照
三个划分合计 22,317,与 pool 差的 465 条是
buffer leg,按 HANDOFF 的划分策略两边都不进
data/clips/ 22,782 个 mp4 832×480 / 16fps / 81 帧
data/meta/*.jsonl 逐 clip 一行,含 21 个 cell 的逐 cell 真值
data/logs/<session>/ state.npz(20Hz 世界坐标) / input.npz(摇杆) / offsets.json(世界→屏幕)
```
### 每个 latent .pt 的内容
```python
d["latent"] # bf16 [48, 21, 30, 52]
d["prompts"] # list[str] 21 条,含动作从句(老方案,ActionRoPE 不用)
d["prompts_bossdrop"] # list[str] 21 条,场景 dropout
d["actions"] # list[int] 21 条,9 类动作索引
d["scenes"] # list[str] 21 条
d["actions"] / d["purity"] / d["rt_ratio"] / d["video_t0"] / d["start_cell"] ...
```
## 文本表
数据集自带的 `text_table_eybx.pt`(1,253 键)每一条都含 `the player is {动作}`,
是动作走文本的老方案。ActionRoPE 的动作走 RoPE,所以要重建一张只含场景的表:
```
老: In a 2.5D top down view, {scene}, the player is {action}.
新: In a 2.5D top down view, the player is {scene}.
```
```bash
.venv/bin/python -m actionrope.build_text_table # -> data/latent/text_table_actionrope.pt
```
361 键(18 纯场景 + 342 转场 + 1 空条件串),`dict[str, bf16[512,4096]]`。
编码走 DiffSynth 自己的 tokenizer + WanTextEncoder,与
`WanVideoUnit_PromptEmbedder` 逐字一致,训练时不必再挂 11 GB 的 UMT5。
## 会静默出错的地方
摘自数据集的 `HANDOFF.md`,加上本仓库实测:
1. **分辨率只能 832×480。** 848×480 会静默炸:52→53 是奇数,DiT 的 stride-2 丢掉最右一列 latent。
2. **不要用训练脚本自带的哈希划分做 val。** clip 步长 84 帧、本身 81 帧,相邻两个 clip 只差
3 帧,哈希划分会把同一段素材切到训练和验证两边。验证走离线的 `val_eybx`。
3. **`unpack.sh` 要传绝对路径。** 它 `cd "$ROOT"` 之后又用 `$ROOT/latent` 拼路径,
传相对路径时划分那一步会静默失败(tar 解包正常,软链 0 个)。
4. **号段不能撞**:1e8(s1) / 2e8(s2) / 3e8(burn) / 4e8(hardcut),1e7 是 vfx 段。
5. **低 rt_ratio 片段**(世界降速)没有被过滤,s1 里 rt<0.8 占 27.6%。
做控制服从评测时要排除,否则「模型不听话」和「世界本来就慢」会混在一起。
## 数据体检
```bash
.venv/bin/python -m tools.inspect_dataset # 全部检查
.venv/bin/python -m tools.inspect_dataset --check transitions
```
已核对过的结论:
* 22,782 个 clip / 478,422 个 cell 的 prompt 全部能剥离动作从句,零失败
* 94% 的 clip 内部逐 cell prompt 不同(变的只有动作从句),场景从句 90% 的 clip 内恒定
* 转场 1,900 条 **100% 是 `A → 过渡 → B`**,没有 `A → 过渡 → 过渡`
* 转场起始 cell 均匀落在 **3..9**(各 12.9%~15.9%),cell 0-2 / 10-20 无正样本 ——
成因是 `burn_corpus.py:44` 的 `N_PRE_CHOICES = [9,13,17,21,25,29,33]` 帧,
经 `cell_of()` 映射即 3..9。已确认按现状使用(要求是「不全在同一个 cell」)。
* 三个划分两两无交集,0 断链。但**转场 clip 是按 heldout 标志而不是按 src 归属划的**:train_eybx 里 1,741 条转场中
257 条的 `src_a` / `src_b` 在 val_eybx(123 条是 `src_b` ⇒ 该 val clip 的帧 n_pre..80 逐帧出现在训练集里,像素核对
mean|Δ| 0.69 灰度级),涉及 56 个 val clip(36 个 sidecar valid)。`dataset.py` 按 `data/meta/transition.jsonl` 把这些
转场剔出训练集(`exclude_src_in=val_dir`,sidecar valid 的有 73 条),划分软链本身不动。
* 200 条 latent 抽样:形状 `[48,21,30,52]` bf16 全对,无非有限值,
std 均值 1.247 —— 与 Wan2.2 VAE 的 mean/std 归一化后应有的量级一致
## ARoPE 模型补丁(`actionrope/arope.py`)
动作走 RoPE 的模型侧实现,不改 DiffSynth 源码,接口见 `actionrope/SPEC.md`「模型契约」:
* `build_world_freqs(dit, f, h, w, offset_tok)`:f 轴查 `dit.freqs[0]`,h/w 轴按世界坐标
`(i_h + dy_tok, j_w + dx_tok)` 用 `polar(1, pos·inv_freq)` 现算(允许小数)。
`offset_tok=None` 时与原查表逐位相同;零偏移走现算路径时与查表只差 ~1e-15
(表在 CPU 上算、现算在 GPU 上,cos/sin 库函数的 ULP 差;`dit.freqs` 实际是 complex128)。
* `known_mask` / `loss_weight_map`:世界坐标落在首帧足迹 `[-0.5, h-0.5]×[-0.5, w-0.5]`
(闭区间)内 ⇒ known,否则 new(loss × `new_weight`)。
* `install_arope(dit, mask_channel)`:`mask_channel=True` 只新增零初始化的
`arope_mask_embedding`(Conv3d(1, 3072, (1,2,2)),15,360 个参数);`False` 时 ckpt 键集与原模型完全一致。
* `arope_forward(...)`:复刻 `model_fn_wan_video` 的 `seperated_timestep + fuse_vae_embedding_in_latents`
分支(帧 0 的 t=0)。context 可以是整条 `[b,512,4096]` 或逐 cell `[b,21,512,4096]`(每帧 token
只 cross-attend 自己 cell 的文本,`context_ids=[b,21]` 给出去重分组)。
自测(一张 H200,约 1 分钟):
```bash
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=$PWD .venv/bin/python -m pytest tests/test_arope.py -s -v
```
实测:`offset_px=None` 时与 `model_fn_wan_video` **逐位相等**;逐 cell 文本(21 份相同)与整条模式逐位相等;
全参数训练一步(batch 1、全尺寸、梯度检查点开)峰值显存 ~22 GB、前向+反向 ~4 s。
## 相机偏移 sidecar(`actionrope/geometry.py` / `sidecar.py` / `verify_sidecar.py`)
动作走 RoPE 需要每帧的相机(玩家屏幕投影)位移,存在 `data/latent/arope_sidecar.pt`
(结构见 `actionrope/SPEC.md`)。生成与校验:
```bash
.venv/bin/python -m actionrope.sidecar # 未修正版(gain=[1,1]),约 10 s
.venv/bin/python -m actionrope.verify_sidecar # 对视频实测符号/增益并重写 sidecar,约 75 s(默认取全部 636 个候选 clip)
.venv/bin/python -m pytest tests/test_geometry.py # 几何单测(纯 CPU)
```
实测结论(`outputs/sidecar_check/gain_fit.json`,636 个高速直行 clip、帧 0 vs 帧 16/32/48、
SIFT+RANSAC 仿射场在玩家位置取值):
* **日志 M 的符号是反的**:实测背景位移 ≈ **+**(日志预测的玩家投影位移),两 session、
两轴、1,251 个样本 ≥96% 一致。旧流水线 `align_flow.flow_series` 把相位相关峰取反了一次
(峰本身已经是 −位移),M 用它拟合出来符号整体翻转;`verify_actions` 的闭环用的是同一个
测法,所以没抓到。**后果**:meta / prompt 里的动作标签是镜像的 ——「moving right」的 clip
里玩家实际向屏幕左走,「moving up」实际向下(2026-09-04 人工看视频确认:`clip_Eybx_100000022_000210`
标签 "moving left",画面里场景向左滑、人物朝右跑)。sidecar 已把符号乘进 `gain_correction`
(负值),ARoPE 臂不必再处理;`plain` 臂由 `prompts.mirror_action` 把动作词翻回真实方向
(训练 `--mirror_labels` 默认开,文本表用 `text_table_eybx_mirror.pt`,见下)。`data/HANDOFF.md`
里「世界 +x → 屏幕左、+z → 屏幕下」是这个翻转的产物,实际是 +x → 右、+z → 上(未改数据集文档)。
* 横向增益:g_x = 1.120 / 1.124(两 session 相差 0.4%,R² 0.99/0.98)⇒ `gain_correction[0] = −1.122`,
即 832 画幅下横向 29.8 px/世界单位(日志 26.6)。
* 纵向:g_y = 1.094 / 1.117(相差 2.1%)但 R² 0.90 / 0.84 < 0.9 ⇒ 按 SPEC 规则幅值保持 1.0,
`gain_correction[1] = −1.0`。原因是相机是斜视角透视,纵向位移随屏幕行变化 ~40%/100 px
(横向 ~15–20%/100 px),单一平移只是近似,k=48 时 R² 掉到 0.75。
* `valid`:最近邻日志时间差 ≤ **0.5 s**(`--max_sample_dt 0.5`,2026-09-04 人定放宽)+ 跨 offset 台阶(`log_jump`,
只中 `clip_Eybx_200000340_000168`)+ 传送 ⇒ n_valid = **22,781** / 22,782。原 0.15 s 规则只留 13,855(s1 丢一半);
放宽前做过实测:只在 0.15–0.5 s 规则之间的 clip(160 条纯日志缺口 + 160 条 rt<0.8 降速)过同一套视频校验,
x 增益 −1.117 / −1.101、R² 0.98 / 0.98、中位残差 6.9 / 5.9 px,与 valid 基线(−1.122 / 0.99 / 4.7 px)一致。
世界降速不再靠 valid 规则兜,改用 `rt_ratio` 闸(训练 <0.8 剔、val/评测 <0.9 剔,见训练一节)。
* `walk_speed_px_s = [69.2, 41.1]`(已乘 |gain|),供推理造指令用。
## 推理(`actionrope/infer.py`)
首帧 + 场景从句 + 动作脚本 ⇒ 81 帧 832×480 mp4,并对生成视频实测背景位移。示例见 `scripts/infer_demo.sh`:
```bash
# replay 一条 val clip:指令 = sidecar 真值,首帧 = 该 clip 帧 0,文本 = 原 prompt(剥掉动作),与 GT 比 PSNR
CUDA_VISIBLE_DEVICES=4 .venv/bin/python -m actionrope.infer --replay clip_Eybx_200000958_000273 \
--ckpt outputs/<run>/step-N.safetensors --out outputs/samples/replay.mp4
# 自造指令:玩家向屏幕右走 1.0× 步速、21 个 cell;速度取 sidecar meta 的 walk_speed_px_s
CUDA_VISIBLE_DEVICES=4 .venv/bin/python -m actionrope.infer --first_frame clip_Eybx_200000958_000273 \
--scene tidal_flats --actions "right:1.0:21" --out outputs/samples/right.mp4
```
* `--actions "dir:mult:cells,..."`(8 向 + idle,cell 数合计 21)或 `"dx,dy:cells"`(px/s);
**方向名指玩家在屏幕上的真实运动方向**(right ⇒ 背景左移)。`--trigger burn:<B>:<start>:<n>` 把 cell
换成转场从句、之后切成 B(文本表里有全部 342 条转场串,查不到才回退加载 UMT5)。
* `--arm plain`:普通 RoPE + 文本带动作。默认 `--label_mode literal`:动作词写真实屏幕方向(真实向右 ⇒
"moving right"),配 `--mirror_labels` 训出来的模型;`--replay` 时把数据集的镜像 prompt 也翻回来。
`--label_mode mirror` 只给 `--no-mirror_labels` 的旧约定模型用(原标签、原表)。
* `--ckpt` strict 加载,mask 通道按 ckpt 键集自动判断;不给就是基座权重。采样 50 步 shift 5,
帧 0 每步写回首帧 latent(首帧用 fp32 编码再存 bf16,与数据集 latent 同精度;bf16 编码会差 ~1%)。
`--cfg` 的负条件:arope 臂 `NULL_PROMPT`;plain 臂逐 cell 裸动作串 `In a 2.5D top down view, the player is {action}.`
(该臂训练 dropout 用的就是这 9 个串,`NULL_PROMPT` 不在 eybx 表里)。plain + CFG 未实测。
* 最后一行打印实测位移:帧 0→80 的背景位移(`verify_sidecar.measure_shift`,SIFT+RANSAC 仿射场在
玩家位置取值;直接测不出时每 16 帧链式累加,打印里标明采用的是 direct 还是 chain 及段数)与指令
`−frame_offset_px[80]` 对比;同名 `.json` 里有全部数字(相位相关值只作参考,见已知问题 4),`_frame0.png` 是首帧。
大位移场景(≥300 px)下 0→80 直接测基本都失败,头条数字来自 5 段链式累加。
自测(一张 H200,约 1.5 分钟):`CUDA_VISIBLE_DEVICES=4 .venv/bin/python -m pytest tests/test_infer.py -s -v`。
测试产物写到 `outputs/test_artifacts/`(`AROPE_TEST_DIR` 可改)。
实测:推理侧 VAE 编码与数据集 latent 相对误差 0.8%(bf16;fp32 编码 0.3%,数据集是 fp32 编码后存 bf16),
首帧 fp32 编码(默认)cell 0 相对误差 0.38%(bf16 编码 0.96%),首帧单独编码与整段同精度编码的 cell 0 逐位相同;GT clip 实测位移 (+385, +4) vs sidecar 的 −frame_offset[80] (+370, +2);
20 步采样 5.7 s、解码 2 s、峰值 16.4 GB。**基座权重(未微调)+ ARoPE** 对 right 1.0× 指令实测位移
(−353, +3) vs 指令 (−356, 0),replay 实测 (+332, +29) vs (+370, +2) —— 世界坐标 RoPE 零样本就把背景
推到了指令位置(画面有拖影,需微调);plain 臂基座对 "moving right" 文本实测 (−6, +1),基本不动。
## 训练(`actionrope/dataset.py` / `train.py`,配置 `configs/`,入口 `scripts/`)
只训 DiT(bf16,全参数),latent 与文本都预计算,不挂 T5 / VAE。两个原生臂(四个 baseline 臂见「五臂 baseline」一节):
| 臂 | RoPE | loss 权重 | mask 通道 | 文本 |
|---|---|---|---|---|
| `--arm arope` | 世界坐标(sidecar `offset_px`) | new 区 × `--new_weight`(默认 2) | 零初始化 conv(`--no_mask_channel` 关) | `text_table_actionrope.pt`(剥动作从句) |
| `--arm plain` | 普通 | 全 1 | 无 | `text_table_eybx.pt`(原 prompt,含动作从句) |
两臂都走逐 cell 文本 cross-attn(`context [b,21,512,4096]` + `context_ids` 去重),loss 与 DiffSynth
`FlowMatchSFTLoss` 同形(`FlowMatchScheduler("Wan")` 1000 个训练时间步均匀抽、帧 0 用干净 latent 替换并剔除、
`Σw·(pred−target)²/Σw` × `training_weight(t)`)。场景 dropout `--scene_dropout 0.1`:arope 臂 21 个 cell 全换
`NULL_PROMPT`,plain 臂换成 .pt 自带的 `prompts_bossdrop`。
```bash
bash scripts/smoke_train.sh arope # 4 卡 ZeRO-2,64 clip,12 步,第 6/12 步存权重 + 验证(约 3 分钟)
bash scripts/smoke_train.sh plain
bash scripts/train_arope.sh # 8 卡正式:40k 步、save_every 1000、val_every 500(RUN=xxx 改输出名)
bash scripts/train_plain.sh
.venv/bin/python tests/check_ckpt.py outputs/<run>/step-N.safetensors --arm arope # 键集 strict 核对(CPU)
```
* 数据:`train_eybx` 20,983 个 .pt 里 sidecar `valid=False` 剔 1,`src_a`/`src_b` 在 val_eybx 的转场剔 257,
`--min_rt_ratio 0.8`(默认)剔世界降速 2,197 ⇒ 可用 **18,528**(plain 17,044 / burn 1,162 / hardcut 322);
val_eybx 1,175 按 `--val_min_rt_ratio 0.9`(默认)剔 193 ⇒ **982**。40k 步 × 8 卡 ≈ 17 个 epoch。
* 输出 `outputs/<run>/`:`step-N.safetensors`(10.0 GB,只含 DiT 键,arope 臂多 `arope_mask_embedding.*`,
能直接 `load_state_dict(strict=True)`)、`train_log.csv`(loss/lr/grad_norm/step_time/mem)、`val_log.csv`
(固定 val clip × 固定 t∈{200,500,800} × 固定噪声)、`tensorboard/`、`config.json`(全部参数 + sidecar 增益 + 环境)。
* `--resume x.safetensors` 热启动权重(step 从 0 起,Adam 动量与 warmup 归零,且 bf16 导出会丢掉 fp32 master 里
小于 1 ULP 的更新 —— RMSNorm 权重 30 步内在 bf16 里逐位不变就是这个原因);`--save_state`(正式脚本默认开)时另存
DeepSpeed 状态到 `state_A` / `state_B` 两槽轮转(8 卡每份约 70 GB),`<output>/state` 软链指向最新完整槽,
`--resume outputs/<run>/state` 续训(数据顺序不复现)。
* 随机数:`set_seed(seed, device_specific=True)`,各 rank 的噪声 ε 与场景 dropout 抽签独立(2 进程实测:分片不相交、
ε 逐步不同、跨 epoch 重排);不加 device_specific 时 8 张卡每步共用一份 ε。
* `--grad_clip`(默认 1.0)在 DeepSpeed 下写进 `gradient_clipping`(启动时打印生效值);日志里的 `grad_norm` 是**裁剪前**的全局范数。
* 学习率手动 step(不交给 `accelerator.prepare`,否则每步会被 step `num_processes` 次),`--warmup_steps` 线性 warmup。
* 单进程 `python actionrope/train.py` 不走 DeepSpeed:AdamW 直接在 bf16 参数上更新、无 fp32 主权重(lr 1e-5 的更新大多被
bf16 舍入吞掉),只适合 smoke,数字不代表训练效果。
实测(4×H200,ZeRO-2,每卡 batch 1,梯度检查点开):**1.22 s/步**,峰值显存 32.8 GB/卡;单卡无 DeepSpeed 1.13 s/步、46.7 GB。
## ActionRoPE 实现
上面各节按模块写了细节,这一节是总览:模块表、各阶段命令、8 卡端到端 smoke 的实测数字与已知问题。
所有模块的接口契约在 `actionrope/SPEC.md`,不改 DiffSynth 源码,只在 `actionrope/` 里以函数 / 子类扩展。
### 模块
| 文件 | 作用 | 自测 |
|---|---|---|
| `actionrope/prompts.py` | `strip_action` 剥动作从句、`NULL_PROMPT` | `tools/inspect_dataset.py --check prompts` |
| `actionrope/build_text_table.py` | 只含场景的文本表 `text_table_actionrope.pt`(361 键) | `--check text_table` |
| `actionrope/build_mirror_table.py` | plain 臂用的文本表 `text_table_eybx_mirror.pt`(1,783 键 = eybx 表 + 530 条翻回真实方向的转场串,7.5 GB,GPU 0 约 1 分钟) | — |
| `actionrope/geometry.py` | 常量、cell↔帧、日志→屏幕投影、`sample_frame_offsets`、known/new 掩码 | `tests/test_geometry.py`(CPU) |
| `actionrope/sidecar.py` | 生成 `data/latent/arope_sidecar.pt`(逐 clip 的 `offset_px[21,2]` / `frame_offset_px[81,2]` / `valid`) | — |
| `actionrope/verify_sidecar.py` | 对视频实测符号 / 增益(SIFT+RANSAC 仿射场),把 `gain_correction` 乘回 sidecar | `outputs/sidecar_check/` |
| `actionrope/arope.py` | 世界坐标 RoPE、逐 cell 文本 cross-attn、mask 通道、`arope_forward` | `tests/test_arope.py`(GPU) |
| `actionrope/dataset.py` | latent + sidecar + 文本表 ⇒ 样本;valid 剔除;场景 dropout | — |
| `actionrope/train.py` | 各臂全参数微调(accelerate + DeepSpeed ZeRO-2),定步验证 / 导出(arm 参数带 `arm.` 前缀同文件) | `tests/check_ckpt.py` |
| `actionrope/infer.py` | 首帧 + 场景 + 动作脚本(或 `--replay`)⇒ mp4,并实测背景位移;臂按 ckpt 自动识别 | `tests/test_infer.py`(GPU) |
| `baseline/` | 四个对照臂 linear / xattn / prompt / adaln(`ActionArm` 钩子),`build_arm` / ckpt 键集识别;见「五臂 baseline」 | `tests/test_arm_*.py`(GPU)、`tests/test_baseline_integration.py`(CPU) |
| `configs/accelerate_zero2*.yaml` | 8 卡 / 4 卡 DeepSpeed ZeRO-2 bf16 配置 | — |
| `scripts/*.sh` | `smoke_all.sh`(端到端)、`smoke_train.sh`、`train_arope.sh`、`train_plain.sh`、`train_baseline.sh <arm>`、`smoke_baselines.sh`、`infer_demo.sh` | — |
### sidecar 生成与校验
```bash
.venv/bin/python -m actionrope.sidecar # 未修正版(gain=[1,1]),~10 s
.venv/bin/python -m actionrope.verify_sidecar # 实测符号 / 增益并重写 sidecar,~75 s
.venv/bin/python -m actionrope.sidecar --gain_fit outputs/sidecar_check/gain_fit.json --max_sample_dt 0.5 # 当前交付用的就是这条(0.5 s 规则)
.venv/bin/python -m pytest tests/test_geometry.py -q
```
当前 sidecar:`gain_correction = [-1.1223, -1.0]`(符号反转 + 横向 1.122 增益,806 个候选 clip 重测)、
`walk_speed_px_s = [69.24, 41.08]`、`max_sample_dt = 0.5`、`n_valid = 22,781 / 22,782`(1 条 log_jump)。
### 训练
```bash
bash scripts/smoke_all.sh # 8 卡端到端 smoke:单测 → 体检 → arope 30 步(含 save_state)→ plain 10 步 → 推理 → check_ckpt → 续训 2 步,约 9 分钟
FROM=c bash scripts/smoke_all.sh # 从第 c 步开始(a 单测 / b 体检 / c arope / d plain / e arope 推理 / f plain 推理 / g check_ckpt / h resume)
bash scripts/smoke_train.sh arope # 4 卡 12 步 smoke
bash scripts/train_arope.sh # 8 卡正式:40k 步、lr 1e-5、warmup 500、save_every 1000(+save_state)、val_every 500(RUN=xxx 改输出名)
bash scripts/train_plain.sh # 对照臂,同样节奏;崩溃后 bash scripts/train_arope.sh --resume outputs/arope_40k/state
.venv/bin/python tests/check_ckpt.py outputs/<run>/step-N.safetensors --arm arope
```
正式训练的 `train.py` 关键参数:`--arm {arope,plain,linear,xattn,prompt,adaln}`(后四个见「五臂 baseline」,`scripts/train_baseline.sh <arm>`)、`--mask_channel/--no_mask_channel`、`--new_weight 2.0`、
`--scene_dropout 0.1`、`--lr 1e-5`、`--grad_clip 1.0`、`--max_steps`、`--save_every --save_state`、`--val_every --val_n`、
`--resume`(safetensors 热启动 / `state` 软链续训,后者需 `--save_state`)、`--min_rt_ratio`、`--limit`(smoke)。
### 推理
```bash
# replay:指令 = sidecar 真值,首帧 = 该 clip 帧 0,与 GT 比 PSNR
CUDA_VISIBLE_DEVICES=0 .venv/bin/python -m actionrope.infer --ckpt outputs/<run>/step-N.safetensors \
--replay clip_Eybx_200000958_000273 --steps 50 --out outputs/samples/replay.mp4
# 自造指令:玩家向屏幕右走 1.0× 步速 21 个 cell(right ⇒ 背景左移 ≈ 356 px)
CUDA_VISIBLE_DEVICES=0 .venv/bin/python -m actionrope.infer --ckpt outputs/<run>/step-N.safetensors \
--first_frame clip_Eybx_200000958_000273 --scene tidal_flats --actions "right:1.0:21" --out outputs/samples/right.mp4
# 对照臂(动作走文本,动作词 = 真实屏幕方向)
CUDA_VISIBLE_DEVICES=0 .venv/bin/python -m actionrope.infer --arm plain --ckpt outputs/<plain_run>/step-N.safetensors \
--first_frame clip_Eybx_200000958_000273 --scene tidal_flats --actions "right:1.0:21" --out outputs/samples/right_plain.mp4
```
### 8 卡端到端 smoke 实测(`scripts/smoke_all.sh`,2026-09-04 审查修复后重跑,一次通过,总耗时 587 s)
| 步骤 | 结果 | 耗时 |
|---|---|---|
| a `pytest tests/ -x -q`(GPU 0) | 26 passed | 60 s |
| b `inspect_dataset --check layout text_table` | 通过 | 4 s |
| c arope 臂 8 卡 ZeRO-2,256 clip,30 步,val_n 8,`--save_state` | 见下 | 182 s(含 2 次 10 GB 导出 + 2 次 66 GB state) |
| d plain 臂 8 卡 ZeRO-2,10 步 | 见下 | 89 s |
| e arope step-30 推理:replay + right×1(50 步) | 见下 | 88 s(两条) |
| f plain step-10 推理:right×1(50 步) | 见下 | 42 s |
| g `check_ckpt` step-30(CPU) | strict_load_ok,827 张量,675 张量有改动,max\|Δ\| 4.9e-4 | 23 s |
| h 从 `outputs/smoke_arope/state`(→ `state_B`)续训 2 步 | 从 step 30 起到 32,step-32.safetensors | 99 s |
* **8 卡 step time**:arope 臂首步 5.68 s(含编译 / 分配),其后 **1.20 s/步**;plain 臂 1.20 s/步。
与 4 卡实测(1.22 s/步)一致 —— 每卡 batch 1、ZeRO-2 通信不是瓶颈。
* **显存**:8 卡下每卡峰值 **22.7 GB(主进程)/ 23.9 GB(各进程最大)**,比 4 卡的 32.8 GB 低 —— ZeRO-2 优化器状态按 8 份切。
* **arope 30 步 loss**(乘过 `training_weight(t)`,每步 t 随机、各 rank 噪声独立,单样本噪声大):
`0.299 0.331 0.294 0.265 0.177 0.210 0.278 0.259 0.305 0.237 0.168 0.265 0.254 0.197 0.213 0.158 0.235 0.264 0.275 0.343 0.109 0.110 0.345 0.182 0.320 0.252 0.261 0.329 0.237 0.169`;
lr warmup 5 步(2e-6 → 1e-5)正确,grad_norm(裁剪前)0.94 → 0.1–0.4;`[env] grad_clip=1.0(生效值 1.0)` 印证 CLI 值写进了 DeepSpeed。
* **val(8 个固定 val clip × t∈{201,501,800} × 固定噪声)**:arope step 15 → 30:loss_w 0.514/0.273/0.220 → 0.493/0.263/0.212
(均值 0.336 → 0.322);plain step 5 → 10:0.483/0.253/0.201 → 0.485/0.255/0.201(均值 0.312 → 0.314)。
两臂的 val 数值不可直接比:arope 的 loss_w 含 new 区 ×2 权重、文本剥了动作。
* **推理**(GPU 0,50 步 shift 5,首帧 fp32 编码):采样 12.8 s、VAE 解码 2.0 s、峰值 16.4 GB、单条含加载约 33–37 s。
* arope step-30 replay:实测背景位移 (+372.0, +32.5)(0→80 直接 SIFT;链式 (+367.3, +33.6))vs 指令 (+370.2, +2.1)
(GT 实测 (+384.9, +3.8),链式),帧 0 PSNR 31.4 dB,帧 1–80 平均 16.5 dB。dy 偏差 +30 px 是生成视频真实的纵向漂移
(模板匹配独立核对 GT 为 +4、生成为 +43),不是链式测量误差。
* arope step-30 right×1:实测 (−356.6, +22.3) vs 指令 (−355.7, 0),误差 (−0.9, +22.3) px。
* plain step-10 right×1(当时按旧约定:文本 "moving left" 镜像标签):实测 (−5.5, +2.3) vs 指令 (−355.7, 0) —— 30 步以内文本动作基本无响应。
这些数字与未微调基座上的结果同量级(基座 replay (+331.6, +28.7)、right×1 (−352.7, +2.7)),30 步微调不足以改变画质
(PSNR 16 dB 仍是拖影),只证明链路通。产物在 `outputs/samples/smoke_e2e_*.{mp4,json,png}`。
* **续训**:`state_A`(step 15)/ `state_B`(step 30)各 66 GB,`state` 软链指向 `state_B`;步骤 h 从软链 load_state 后 step 31 的
loss / grad_norm 与训练末尾连续(0.174 / 0.164),warmup 不重来。
* **正式 40k 步预估**(8 卡 1.20 s/步):纯训练 **13.3 h**,加 40 次导出(每次 ~25 s)≈ 17 min、40 次 save_state(每次 ~30 s)≈ 20 min、
80 次验证(16 clip × 3 t,~3 s)可忽略,合计约 **14 h/臂**;40k × 8 = 32 万样本 ≈ 12,579 个有效 clip 的 25 个 epoch。
### ARoPE 5k 步首次训练结果(2026-09-04,`outputs/arope_5k/`,`scripts/eval_5k_demo.sh`)
8 卡 ZeRO-2、lr 1e-5、warmup 200、训练集 18,528、1.20 s/步、1.7 h。val loss(16 clip,t=200/500/800 均值)0.2550 → 0.2482。
同一首帧(`clip_Eybx_200000958_000273`,tidal_flats)、50 步采样、文本只有场景,实测帧 0→80 背景位移(SIFT 链式):
| 指令 | 指令位移 px | 实测 px | 说明 |
|---|---|---|---|
| right ×0.5 | −173 | −184 | |
| right ×1.0 | −346 | −366 | |
| right ×1.5 | −519 | −543 | 三档斜率 ≈1.05,线性 |
| left ×1.0 | +346 | +385 | 与 right 对称 |
| up ×1.0 | +205 | +264 | 纵向超 27%:sidecar 纵向增益仍是 1.0,模型按真实画面(≈1.2×)学的 |
| down ×1.0 | −205 | −259 | 同上 |
| right 10 cell → left 11 cell | +35 | +34 | 折返回到出发画面 |
| replay GT 偏移 | +370 | +397 | GT 视频实测 +385;帧 1–80 PSNR 17.3 dB(新区域是生成内容,PSNR 无意义) |
画面干净、场景连贯,玩家钉屏。训练中 replay 的横向超调随步数收敛:step-1000 +445 → 2000 +405 → 5000 +397。
下一步:纵向增益应按实测(k=16 拟合 1.20/1.27,全候选 1.12)写进 sidecar 重训;plain 对照臂未训。
### bg50 四科(step-5000,`actionrope/eval_bg50.py`,2026-09-04,`outputs/eval_bg50/step5000/`)
复现旧报告《世界RoPE·四科判决》的口径,只跑 ARoPE 臂:考场 `data/eval/eybx_bg_50/`(50 个 GPT 生成的未见背景,中央已合成玩家),
文本条件 `NULL_PROMPT`(训练时场景 dropout 用的串;这些背景不属于 18 个训练场景,不配场景从句,`--scene` 可覆盖),首帧 26:15 居中裁剪后缩放,
50 步 shift 5、bf16、seed 0。150 条视频(增益 10 景 × 5 档 + 折返 50 + 直行 50)8 卡 312 s 生成完,失败 0;
度量只读 mp4、CPU 48 进程 102 s。位移量法与 sidecar 校验相同(`verify_sidecar.measure_shift`:SIFT+RANSAC 仿射场在玩家位置取值;
帧 0→80 直接测不出时 16 帧链式累加)。度量代码先在合成平移视频上自检(8 项全过:位移 / 逐帧速度 / 注入倒退帧 / 折返残差与补偿 / 明暗漂移 / 增益斜率,
见 `report.md`)。折返 / 直行只用横向指令,纵向为 0(纵向增益已知偏大 ~25%,见已知问题 3)。
```bash
.venv/bin/python -m actionrope.eval_bg50 --ckpt outputs/arope_5k/step-5000.safetensors --run step5000 --stages gen,measure --gpus 0-7 # 可续跑
.venv/bin/python -m actionrope.eval_bg50 --run step5000 --stages measure --no_selfcheck # 只重算度量
```
| 科目 | 指标 | ARoPE step-5000(本机 bg50) | 旧报告 世界RoPE@5k |
|---|---|---|---|
| 1 增益线性(0.5–1.5×,10 景) | 过原点斜率 / R² | **0.932 / 0.879**(剔除视差背景 01 后 0.960 / 0.962) | 1.06 / 0.95 |
| 2 折返闭环(1.5×,右 40 帧左 40 帧,50 景) | 终帧 vs 首帧 PSNR / SSIM / NCC 中位 | 19.17 dB / 0.401 / 0.787 | — |
| | 回归残差中位 / p90 · 补偿后 NCC 中位 | 8.4 / 20.4 px · 0.833 | — |
| | 守门:帧 0→40 实走距离中位(指令 256) | 240.5 px(0 条 <50%) | — |
| 3 运动平滑(1.0× 直行,50 景) | 倒退帧总数 | **16**(全部 ≤ 0.22 px 的停顿帧;>1 px 的 0) | 6 |
| | 归一速度波动 std/mean 中位 · 归一 jerk 中位 | 0.629(按 4 帧 cell 平均后 0.118)· 2.25 | — |
| | 平均速度 / 承诺 中位 | 1.014 | — |
| 4 方向服从(同一批直行) | \|角度误差\| 中位 / p90 · 幅度比中位 · 失控数 | 1.16° / 2.49° · 0.983 · 0 / 50 | — |
* 增益:各档幅度比中位 0.5× 1.015、0.75× 0.939、1.0× 0.973、1.25× 0.973、1.5× 0.908 —— 未见背景上高速档略欠冲。R² 被背景 01(赛博城市)拉低:
生成画面带纵深视差(远景楼群带比地面带慢 ~15%),SIFT 仿射场在玩家位置取值时各段锁到不同层,16 帧链式给 −235 px 而逐帧速度累加给 −391 px(指令 −433),
是量法在视差场景下失稳,不是模型不动;头条仍按旧报告口径全量报。
* 折返:终帧回到首帧 8 px 以内(中位),补偿位置后 NCC 只从 0.787 升到 0.833 —— 余下的差距是往返 80 帧里生成内容对首帧的重绘(PSNR 最低的 ukiyo-e / 水彩类背景整幅被重画)。
* 平滑:逐帧速度有稳定的帧内节奏(各视频均值 −6.2, −1.7, −4.3, −8.2, −1.3, −9.4 … px/帧,跨视频 std 0.4–1.1 px),来自因果 VAE 每 4 帧一个 latent cell 的时间压缩;
按 cell 平均后波动 0.118,接近合成整数平移的下限 0.108。倒退帧 16 帧比旧报告的 6 多,但全是 ≤0.22 px 的停顿(集中在帧 20→21、42→43 的 cell 边界),没有一帧真往回走。
* 方向:角度误差全部来自纵向漂移(dy 中位 -6.5 px / 346 px 横向),无失控。
### 已知问题
1. **valid 规则已从 0.15 s 放宽到 0.5 s(2026-09-04 人定)**:原规则丢一半 s1(训练集 12,579),且日志缺口与世界降速
高度相关,等于偷偷兼做了 rt 过滤。放宽前实测:只在两规则之间的 clip 过同一套视频校验,精度与 valid 基线一致
(见 sidecar 一节)。缺口帧内偏移是"保持上一位置"(每条中位 13 帧),逐 cell 取均值后的误差已包含在残差里。
世界降速改由 `rt_ratio` 闸处理:训练 `--min_rt_ratio 0.8`、val/评测 `--val_min_rt_ratio 0.9`(默认值),
控制服从评测必须用 ≥0.9 的口径,否则"模型不听话"和"世界本来就慢"分不开。
2. **日志 M 符号反了**:数据集 meta / prompt 里的动作标签是镜像的("moving right" 实际向屏幕左走,已人工看视频确认)。
sidecar 已用负 gain 修正,arope 臂不受影响;plain 臂训练默认 `--mirror_labels` 把动作词翻回真实方向
(文本表 `text_table_eybx_mirror.pt`,`python -m actionrope.build_mirror_table` 生成),推理默认 `--label_mode literal`。
`data/meta`、`data/HANDOFF.md`、`text_table_eybx.pt` 本身未改。
3. **纵向增益已改为 1.12(2026-09-04 人定,step-5000 推理 up/down 超调 27% 证实 1.0 欠标定;`outputs/sidecar_check/gain_fit_y112.json`)。以下是改前的记录:纵向增益保持 1.0,x / y 世界坐标尺度已不一致**:g_y 全候选拟合 1.094 / 1.117、R² 0.90 / 0.84 < 0.9(斜视角透视,纵向位移
随屏幕行变化 ~40%/100 px,k=48 时 R² 0.75),按 SPEC 规则退回 1.0;但 k=16(透视影响最小)下 g_y = 1.20 / 1.27,独立
SIFT 抽查也是 1.2–1.4。结果 ARoPE 的 dy/32 相对 dx/32 少算 10–25%,推理 up/down 指令用 `walk_speed_y = 42.25 px/s` 会系统性
欠冲。是否把 SPEC 的 R² 判据改为按 k=16 / k=32 判定并重跑 verify(得 g_y≈−1.13~−1.2)由人决定。同一透视原因下 ARoPE 的
"同一世界位置 ⇒ 同一编码" 对远离玩家行的 token 只是近似(顶部约 0.5×、底部约 1.5×)。
4. **位移指标不等于画质**:未微调基座 + ARoPE 已能把背景推到指令位置(SIFT 误差 < 10 px),但画面是拖影(PSNR 16 dB);
控制服从评测必须同时看 PSNR / LPIPS。全图相位相关在真实画面上恒给 ≈0(48 个样本 |dx|,|dy|<0.6 px;合成平移能正确返回),
json 里标了 `phase_note`,不是第二种实测。大位移下 0→80 直接 SIFT 都失败,头条数字来自 5 段链式累加(json 里有
`sift_source` / `sift_chain_n_segments`);replay 的 dy 偏差(+30 px 量级)是生成视频真实的纵向漂移,不是测量误差。
5. **视频比日志领先 1–2 帧**(≈0.06–0.12 s):10 个动作有变化的 valid clip 上逐帧比对,最佳 lag 全为 −1 或 −2 帧
(RMS 2.4–4.7 → 2.0–4.1 px)。来自 `data/logs/*/offsets.json` 的对齐精度,不是本仓库代码;折算 4–9 px 的系统偏差,与 x 拟合
中位残差 4.6 px 同量级。要修可在 `geometry.sample_frame_offsets` 加 `time_bias_s`(约 −0.06~−0.1 s),未做。
6. **转场 clip 的 B 段 known 掩码只按几何定义**:530 个 valid 转场里 cell ≥ cell_of(n_pre) 的 latent 像素 72.9% 被判 known
(首帧足迹几何上覆盖),但内容来自 src_b。SPEC 已明确这是有意的简化;要按语义把 B 段整体标 new 需在 dataset/train 里按
n_pre 改掩码。
7. `--save_state` 的 DeepSpeed 状态 8 卡每份约 70 GB,`state_A` / `state_B` 两槽轮转(共 140 GB)、`state` 软链指向最新完整槽,
不删旧槽;正式脚本默认开。
8. T5 回退(文本表查不到的串)已实现未实测;smoke 里所有串都命中文本表。plain 臂 + `--cfg` 的负条件已改为逐 cell 裸动作串
(在表里),但 plain + CFG 未实测。
9. 单进程 `python actionrope/train.py` 不走 DeepSpeed:AdamW 直接在 bf16 参数上更新、无 fp32 主权重(std 0.02 的参数 lr 1e-5 走
10 步只有 15% 元素变化),只能 smoke,数字不代表训练效果。
10. 模型看到的 timestep 是 bf16 舍入值(|Δt| ≤ 2),x_t 用精确 σ(t);DiffSynth 的 `FlowMatchSFTLoss` 是先转 bf16 再 add_noise
(706/1000 个 id 落到相邻 id,max|Δσ| 2.5e-3),两者不逐位一致,但 train.py 的做法与推理 pipeline 一致,不改。
11. `val_eybx` 里 0 条转场 clip,`val_log.csv` 对触发 / 燃烧能力毫无反映;转场评测要另写脚本消费 `pool_heldout`(159 条)。
12. 单样本 `context [21,512,4096]` 88 MB 走 DataLoader 多进程 IPC,num_workers 2 反而比 0 慢(16 vs 21 样本/s),但每卡每步只需
0.8 样本/s,不是瓶颈;要优化可让 Dataset 只返回去重后的 context + 下标。
## 五臂 baseline(`baseline/`,契约 `baseline/SPEC.md`)
同一个 Wan2.2-TI2V-5B、同一份数据、同一套配方,唯一自由变量是**动作以什么方式进模型**。四个对照臂都按上游代码移植
(`vender/` 只读),每个臂在 `baseline/<name>.py` 顶部 docstring 里逐条写了上游文件:行、对应关系、差异;
接入方式是 `actionrope/arope.py::arope_forward(arm=, action_inputs=)` 的固定钩子(`encode / modify_t_mod / extra_context /
after_patch / block_pre / block_mid / block_post`,`arm=None` 时与原实现逐位相同),所有新增路径零初始化——
装上瞬间输出与原模型逐位相同(各臂 `tests/test_arm_<name>.py` 测试 1 实测 max|Δ| = 0;`build_arm` 时再跑一次 `zero_init_check()`,不过就拒绝启动)。
### 机制表
| 臂 | 代表作 / 上游入口(文件:行) | 动作进模型的位置 | 用的信号 | 新增参数 | 文本 |
|---|---|---|---|---|---|
| `arope` | 本项目 `actionrope/arope.py` | h/w 轴 RoPE 位置 = 格坐标 + 累计偏移(token);known/new 掩码通道(零初始化 conv) | `offset_px` | 0(mask 通道 15,360) | scene |
| `linear` | ReactiveGWM `inference/models/dit.py` L207-209(`action_embedders`)、L228-234(`_inject_action`)、L284-286(每 block 前注入);`training/bidirectional/train.py` L134-137(zero/xavier 初始化) | 每个 block 入口残差流 `x += Linear_i(action)`(30 个 bias-free Linear,沿 h·w 广播),钩子 `block_pre` | `offset_tok` [b,21,2](上游是 10 键 one-hot 经 adaptive_max_pool 到 latent 帧) | 30×2×3072 = **184,320**(上游 10 键 921,600) | scene |
| `xattn` | Matrix-Game 3 `wan/modules/action_module.py` L39-333(ActionModule:键盘 L267-332 / 鼠标 L201-265 / 12 帧窗口 L207-214)、`wan/modules/model.py` L533-536(前 15 块)、L617-662(block 内注入)、L1097-1103(proj 置零);`utils/conditions.py` L73-107(KEYBOARD_IDX) | 前 15 块 `block_mid`(自注意力后):视频 token 作 q、逐 latent 帧键盘 token 作 k/v 沿时间轴 cross-attn;鼠标分支时间轴 self-attn;`proj_*` 零初始化 | 键盘 ← `action_idx`(9 类 → 4 位多热 up/down/left/right);鼠标 ← `delta_tok`(逐 cell 速度) | **299,531,520**(键盘 141.8M + 鼠标 157.7M;旧报告 98.6M 口径 = `--arm_kwargs '{"enable_mouse": false, "window_frames": 1}'`) | scene |
| `prompt` | Incantation `inference.py` :76-159(PromptGenerator,0.25 s = 1 latent 帧一条 prompt)、:247-260(唯一串编码一次)、:318-353(每帧只 attend 自己那帧的文本);`modules/causal_model.py` :434-450(无动作模块) | 逐 cell 文本 cross-attn(`arope_forward` 原生:q 重排 `(b f)(h w)`,k/v `(b f) L`),零钩子 | 文本(动作从句,`text_table_eybx_mirror.pt`,动作词翻回真实方向);不读 `action_inputs` | **0** | scene_action |
| `adaln` | AlayaWorld `alaya/control/action.py` :78-115(相对位姿 6 轴)、`ltx2/modules/model_ltx_2_3.py` :130-175(ActionAdaLNEmbedder 32 维正弦 + MLP)、:782-786(`action_adaln_projection` SiLU→Linear(dim,6·dim),std 1e-3)、:1171-1176 / :1252-1253(`timestep_emb += a0` 按帧广播) | `modify_t_mod`:`t_mod[b,S,6,dim] += proj(embedder(sinusoid(actions)))` 按帧广播,30 层共用(与 Wan `time_projection` 同路) | `[offset_tok, delta_tok]` 4 轴,每轴尺度 (10, 6.5, 0.6, 0.45) token,×freq_scale 1000 | **66,478,080**(embedder 9.8M + proj 56.6M;上游 LTX-2.3 dim 4096 约 118M) | scene |
`plain` 保留(= `prompt` 的旧名:`train.py --arm plain` 走 arm=None 的旧路径,`prompt` 走 `PromptArm` 的零钩子,前向逐位一致)。
`action_inputs` 由 `dataset.py` 统一给:`offset_px`(sidecar)、`offset_tok = offset_px/32`、`delta_tok`(相邻 cell 之差,cell 0 = 0)、
`action_idx`(.pt 的 `actions` 经 `prompts.MIRROR_ACTION` 翻回真实屏幕方向;64 条 clip 上与 `delta_tok` 方向的余弦均值 0.97)。
推理侧 `infer.py` 用 `baseline.make_action_inputs`(同一定义)从动作脚本生成,`action_idx = velocity_to_label(mirror=False)`,
replay 用 meta 标签经 MIRROR_ACTION 翻回。
### 接入(`baseline/__init__.py` / `train.py` / `infer.py`)
* `baseline.ARMS` / `build_arm(name, dit, arm_kwargs)`:建臂 → `install(dit)` → 搬到 dit 的 device / dtype;
`detect_arm_from_keys / detect_arm_from_ckpt` 按 ckpt 的 `arm.*` 键集识别臂名与结构(xattn 的 `window_frames` / `enable_mouse` / `blocks`
从张量形状反推),metadata 的 `arm` 名区分无 arm 键的 arope / plain / prompt。
* `train.py --arm {arope,plain,linear,xattn,prompt,adaln}`:新臂 = `install_arope(dit, mask_channel=False)` + 普通 RoPE + 权重全 1 +
`text_mode` 按臂(prompt → scene_action,其余 scene);臂是 `AropeTrainModule.arm` 子模块(DeepSpeed 引擎里、同一个 AdamW、同 lr);
导出的 safetensors = DiT 键(去 `dit.` 前缀)+ `arm.*` 键,metadata 记 `arm / arm_kwargs / n_new_params`,`config.json` 的 `model` 段
也记;`--resume x.safetensors` 按前缀拆开热启动(ckpt 无 arm 键 ⇒ 臂保持零初始化;臂不符 ⇒ 报错)。`--arm_kwargs` 传 JSON 给臂的构造函数。
* `infer.py --arm` 可省略:按 ckpt 自动识别并 strict 装臂;`--arm plain/prompt` 文本带动作从句,其余剥掉;prompt 臂的 context 全同时也
展成逐 cell(`PromptArm` 核对)。CFG 负条件:文本臂逐 cell 裸动作串,其余 `NULL_PROMPT`;正负两支都带动作。
* `tests/check_ckpt.py <ckpt>`(臂自动识别)核对 DiT + 臂都能 strict 装回并报告臂参数是否已非零。
```bash
bash scripts/train_baseline.sh linear # 8 卡 ZeRO-2,与 train_arope.sh 同配方(GA 2、40k 步、每 1000 步存、每 500 步验证)
bash scripts/train_baseline.sh xattn --max_steps 5000 --warmup_steps 200 # 5k 步实验(与 arope_5k_ga2 同)
ARM_KWARGS='{"enable_mouse": false, "window_frames": 1}' RUN=xattn_report_5k bash scripts/train_baseline.sh xattn --max_steps 5000 --warmup_steps 200 # 旧报告 98.6M 口径
bash scripts/train_baseline.sh prompt # = plain 臂(动作走逐 cell 文本)
CUDA_VISIBLE_DEVICES=0 .venv/bin/python -m actionrope.infer --ckpt outputs/linear_40k/step-N.safetensors \
--first_frame clip_Eybx_200000958_000273 --scene tidal_flats --actions "right:1.0:21" --out outputs/samples/linear_right.mp4 # --arm 自动识别
bash scripts/smoke_baselines.sh # 四臂单卡 2 步 + step-2 推理 20 步 + check_ckpt,汇总到 outputs/smoke_baselines/summary.json
.venv/bin/python -m pytest tests/ -q -x # 含 tests/test_arm_*.py(GPU)与 tests/test_baseline_integration.py(CPU)
```
### 单卡 smoke 实测(`scripts/smoke_baselines.sh`,2026-09-05,GPU 4–7 轮流,机器上另有 8 卡训练在跑)
单进程 `train.py`(不走 DeepSpeed,AdamW 直接更新 bf16 参数)`--limit 8 --max_steps 2`,梯度检查点开;step-2 ckpt 推理 `right:1.0:21` 20 步;
`check_ckpt` strict 装回。数字只证明链路通(2 步的 ckpt 与基座无异,位移不响应指令属正常)。
| 臂 | 新增参数 | step 1 / step 2 耗时 (s) | 训练峰值显存 | loss (step 1 / 2) | 推理 20 步采样 / 峰值 | 实测背景位移 帧 0→80(指令 (−346.2, 0)) | ckpt |
|---|---|---|---|---|---|---|---|
| `linear` | 184,320 | 7.08 / **2.71** | 46.7 GB | 0.029 / 0.104 | 12.0 s / 16.4 GB | (−5.3, +2.0) direct | 10.00 GB,825 DiT + 30 arm 张量,30 个 arm 张量已非零 |
| `xattn` | 299,531,520 | 9.87 / **3.03** | 49.5 GB | 0.026 / 0.103 | 13.7 s / 17.0 GB | (−29.0, +81.8) chain | 10.60 GB,825 + 225,225 非零 |
| `prompt` | 0 | 6.87 / **2.91** | 46.7 GB | 0.030 / 0.104 | 13.3 s / 16.5 GB | (−6.3, +0.9) direct | 10.00 GB,825 + 0 |
| `adaln` | 66,478,080 | 6.74 / **2.78** | 47.3 GB | 0.038 / 0.075 | 11.7 s / 16.5 GB | (−5.3, +1.7) direct | 10.13 GB,825 + 6,6 非零 |
(对照:同一脚本单卡 arope 臂 1.13 s/步、46.7 GB,那时机器空闲;本次 8 卡训练同时在跑,稳态 2.7–3.0 s/步是共享 CPU / PCIe 的结果,
不是臂的开销——四臂互相之间只差 ≤ 0.3 s。)
* 第 1 步含 CUDA 初始化 / 首次分配,第 2 步才是稳态;单卡峰值显存 = 权重 + 梯度 + 两份 Adam 状态各 10 GB + 激活,臂的参数按同样比例叠加
(xattn 300M ⇒ +2.4 GB)。8 卡 ZeRO-2 下每卡约 23 GB(`smoke_all.sh` 口径),臂参数分片后更小。
* `pytest tests/ -q -x`(GPU 4):**51 passed**,153 s(含 4 个 `test_arm_*.py`、`test_arope.py`、`test_infer.py` 各加载一次 DiT,新增 `test_baseline_integration.py` 4 个 CPU 用例)。
* `--resume outputs/smoke_linear_1gpu/step-2.safetensors` 热启动 linear 臂:`n_arm_tensors 30, arm_loaded True`,1 步后再导出 855 张量;另在 GPU 6,7 上跑了 adaln 臂 2 进程 DeepSpeed ZeRO-2(GA 2、val_n 2)2 步:每卡峰值 56.8 GB(2 卡分片,8 卡约 23 GB),导出 831 张量、`check_ckpt` strict 装回、6 个 arm 张量非零 —— 与正式配方同一条路。注意 accelerate 的 `--main_process_port` 默认 29500 被正在跑的 8 卡训练占着,另起训练要换端口。。