ActionRoPE / code /tests /test_infer.py
teawhite's picture
add docs+code
880dff9 verified
Raw History Blame Contribute Delete
8.19 kB
"""推理脚本的自测(一张 H200 足够:DiT bf16 ~10 GB + VAE,峰值 ~16 GB)。
CUDA_VISIBLE_DEVICES=4 /opt/dlami/nvme/zhiyangdeng/ActionRoPE/.venv/bin/python -m pytest \
/opt/dlami/nvme/zhiyangdeng/ActionRoPE/tests/test_infer.py -s -v
三件事各自独立,可用 -k 单跑:
a. VAE 尺度:用 pipe.vae 编码一条 clip 的 mp4,与 data/latent/pool 同名 latent 比相对误差
—— 训练 latent 与推理首帧 latent 必须同一尺度(mean/std 归一化)。
b. 位移实测函数在 GT clip 上自检:帧 0→80 的实测背景位移 ≈ −frame_offset_px[80]。
c. 端到端 smoke:基座权重 --replay 2 步采样,视频可解码、81 帧、832×480。
实测数字追加写到 $AROPE_TEST_NUMBERS(默认 $AROPE_TEST_DIR=outputs/test_artifacts 下的 json),供报告汇总。
"""
from __future__ import annotations
import json
import os
import numpy as np
import pytest
import torch
os.environ.setdefault("DIFFSYNTH_SKIP_DOWNLOAD", "True")
ROOT = "/opt/dlami/nvme/zhiyangdeng/ActionRoPE"
# 测试产物只落在项目内(outputs/test_artifacts),不写任何机器相关的临时目录
TEST_DIR = os.environ.get("AROPE_TEST_DIR", os.path.join(ROOT, "outputs", "test_artifacts"))
NUMBERS_PATH = os.environ.get("AROPE_TEST_NUMBERS", os.path.join(TEST_DIR, "infer_numbers.json"))
# val_eybx 里 21 个 cell 动作全为 7、rt_ratio=1、sidecar valid 的 clip(tidal_flats,户外纹理多,SIFT 好测)
CLIP = os.environ.get("AROPE_TEST_CLIP", "clip_Eybx_200000958_000273")
import sys # noqa: E402
sys.path.insert(0, ROOT)
from actionrope import geometry as G # noqa: E402
from actionrope import infer as I # noqa: E402
def record(**kv):
data = {}
try:
with open(NUMBERS_PATH) as fp:
data = json.load(fp)
except (FileNotFoundError, json.JSONDecodeError):
pass
data.update(kv)
os.makedirs(os.path.dirname(NUMBERS_PATH), exist_ok=True)
with open(NUMBERS_PATH, "w") as fp:
json.dump(data, fp, indent=2, ensure_ascii=False, default=float)
print("\n[numbers]", json.dumps(kv, ensure_ascii=False, default=float))
@pytest.fixture(scope="module")
def sidecar():
return torch.load(I.SIDECAR, map_location="cpu", weights_only=False)
@pytest.fixture(scope="module")
def gt_frames():
return I.read_frames(I.find_clip_mp4(CLIP))
# ---------------------------------------------------------------- 纯 CPU ----
def test_parse_actions(sidecar):
ws = np.array(sidecar["meta"]["walk_speed_px_s"])
fo, vel = I.parse_actions("right:1.0:21", ws)
assert fo.shape == (81, 2) and np.allclose(fo[0], 0)
assert np.allclose(fo[80], [ws[0] * 80 / 16, 0]) # i/16 × speed × 方向
assert np.allclose(fo[16], [ws[0], 0])
off = G.frames_to_cells(fo)
assert np.allclose(off[0], 0) and off[1, 0] > 0
# cell 0–9 = 帧 0–36 向右(36 步),cell 10–20 = 帧 37–80 向左(44 步)
fo2, _ = I.parse_actions("right:1.0:10,left:1.0:11", ws)
assert fo2[36, 0] == pytest.approx(ws[0] * 36 / 16) and fo2[80, 0] == pytest.approx(-ws[0] * 8 / 16)
fo3, _ = I.parse_actions("71,0:10,-71,0:11", ws) # px/s 直给,逗号形式
assert fo3[36, 0] == pytest.approx(71 * 36 / 16)
with pytest.raises(ValueError):
I.parse_actions("right:1.0:20", ws)
# plain 臂标签:真实向右 ⇒ 数据集的镜像标签是 3 "moving left";literal 则是 7
assert I.velocity_to_label(np.array([71.0, 0]), mirror=True) == 3
assert I.velocity_to_label(np.array([71.0, 0]), mirror=False) == 7
assert I.velocity_to_label(np.zeros(2), mirror=True) == 0
def test_compose_prompts():
p = I.compose_cell_prompts("tidal_flats", "burn:wills_farm:8:2", [7] * 21)
assert p[7].endswith("strewn with wreckage, the player is moving right.")
assert "burning away into a warm farmland hamlet" in p[8] and "burning away into" in p[9]
assert p[10] == "In a 2.5D top down view, in a warm farmland hamlet, the player is moving right."
# 三种串都在文本表里(转场表 342 条 + 纯场景 18 条),不必回退 T5
table = torch.load(I.TEXT_TABLE["arope"], map_location="cpu", weights_only=False)
from actionrope.prompts import strip_action
assert all(strip_action(x) in table for x in p)
table_eybx = torch.load(I.TEXT_TABLE["plain"], map_location="cpu", weights_only=False)
assert all(x in table_eybx for x in p)
def test_gt_displacement(sidecar, gt_frames):
"""b. GT clip 上实测 ≈ −frame_offset_px[80](sidecar 已乘 gain,符号为玩家真实位移)。"""
fo = sidecar["clips"][CLIP]["frame_offset_px"].numpy()
expect = -fo[80]
meas = I.measure_displacement(I.to_gray(gt_frames))
assert meas["sift"] is not None
m = np.array(meas["sift"])
record(gt_clip=CLIP, gt_expected_bg_shift_80=expect.tolist(), gt_measured_sift=m.tolist(),
gt_measured_phase=meas.get("phase_direct"), gt_ratio_x=float(m[0] / expect[0]))
assert np.sign(m[0]) == np.sign(expect[0])
assert abs(m[0] - expect[0]) <= 0.10 * abs(expect[0]) + 5 # 单 clip 增益散布 ±10%
assert abs(m[1] - expect[1]) <= 15
# ---------------------------------------------------------------- GPU ----
@pytest.fixture(scope="module")
def models():
return I.load_models(None, False, "cuda")
@pytest.fixture(scope="module")
def pipe(models):
return models[0]
def test_vae_scale(pipe, gt_frames):
"""a. 推理侧 VAE 编码与数据集 latent 同尺度。bf16 编码 ~0.8%,fp32 ~0.3%(数据集是 fp32 编码后存 bf16);
推理首帧默认 fp32 编码,与训练时看到的帧 0 latent 只差 bf16 存储舍入。"""
ref = torch.load(os.path.join(I.DATA, "latent", "pool", CLIP + ".pt"), map_location="cpu", weights_only=False)["latent"].float()
z = I.encode_video(pipe, gt_frames, tiled=False)[0].float().cpu()
assert z.shape == ref.shape == (48, 21, 30, 52)
rel = ((z - ref).norm() / ref.norm()).item()
z32 = I.encode_video(pipe, gt_frames, tiled=False, fp32=True)[0].float().cpu()
rel32 = ((z32 - ref).norm() / ref.norm()).item()
z0 = I.encode_first_frame(pipe, gt_frames[0])[0, :, 0].float().cpu() # 默认 fp32
rel0 = ((z0 - ref[:, 0]).norm() / ref[:, 0].norm()).item()
z0_bf16 = I.encode_first_frame(pipe, gt_frames[0], fp32=False)[0, :, 0].float().cpu()
rel0_bf16 = ((z0_bf16 - ref[:, 0]).norm() / ref[:, 0].norm()).item()
# 首帧单独编码 == 整段同精度编码的 cell 0(因果 VAE);z0 已 cast 到 bf16,所以整段也 cast 后比
same_as_video = ((z0 - z32[:, 0].to(torch.bfloat16).float()).norm() / z32[:, 0].norm()).item()
assert pipe.vae.model.encoder.conv1.weight.dtype == torch.bfloat16 # fp32 编码后 VAE 已切回 bf16
dec = I.decode_video(pipe, ref.unsqueeze(0).to(torch.bfloat16).cuda(), tiled=False)
ps = [I.psnr(dec[i], gt_frames[i]) for i in range(81)]
record(vae_encode_rel_l2_bf16=rel, vae_encode_rel_l2_fp32=rel32,
vae_first_frame_rel_l2_cell0_fp32=rel0, vae_first_frame_rel_l2_cell0_bf16=rel0_bf16,
vae_first_frame_vs_video_cell0=same_as_video, vae_ref_std=ref.std().item(),
vae_roundtrip_psnr_frame0=ps[0], vae_roundtrip_psnr_mean=float(np.mean(ps)))
assert rel < 0.02 and rel0_bf16 < 0.02 # 同一 mean/std 尺度;bf16 舍入量级
assert rel32 < 0.006 and rel0 < 0.006 # fp32 编码只剩 bf16 存储舍入(~0.3%)
assert same_as_video == 0.0 # 首帧单独编码 == 整段编码的 cell 0(因果 VAE)
assert ps[0] > 28
def test_smoke_replay(models):
"""c. 端到端:基座 + --replay,2 步采样,产出可解码的 81 帧 832×480 mp4。"""
out = os.path.join(TEST_DIR, "test_smoke_replay.mp4")
info = I.main(["--replay", CLIP, "--steps", "2", "--out", out], models=models)
frames = I.read_frames(out)
assert frames.shape == (81, 480, 832, 3)
assert info["psnr_frame0"] > 28
record(smoke_replay_steps=2, smoke_sample_sec_2steps=info["sample_sec"],
smoke_peak_mem_gb=info["peak_mem_gb"], smoke_measured=info["measured"]["sift"])