# 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// 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//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:::` 把 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//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//`:`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),`/state` 软链指向最新完整槽, `--resume outputs//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 `、`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//step-N.safetensors --arm arope ``` 正式训练的 `train.py` 关键参数:`--arm {arope,plain,linear,xattn,prompt,adaln}`(后四个见「五臂 baseline」,`scripts/train_baseline.sh `)、`--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//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//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//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/.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_.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 `(臂自动识别)核对 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 卡训练占着,另起训练要换端口。。