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 的全量微调链路
  • 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 里。

环境

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)。

数据

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 的内容

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}.
.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%。 做控制服从评测时要排除,否则「模型不听话」和「世界本来就慢」会混在一起。

数据体检

.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 分钟):

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)。生成与校验:

.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:

# 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 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 生成与校验

.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 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)。

推理

# 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)。

.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 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 卡训练占着,另起训练要换端口。。