Files
hao-ai-lab-FastVideo/v2/tests/test_workflow_rl.py
SolitaryThinker db866dfb04 [misc] v2: simplify docstrings/comments + drop deleted-design-doc citations
Sweep all 296 v2 modules: simplify verbose docstrings/comments and remove 376 dangling
"(design_vN §X)" citations to the now-deleted design docs (v2/README.md is the source of
truth). Comment/docstring-only — AST-verified code-identical; the CPU suite holds at 237
passed / 5 pre-existing. Also applies yapf + ruff --fix auto-fixes (import ordering,
forward-ref annotation de-quoting under `from __future__ import annotations`; behavior-
neutral, suite-confirmed) and adds the legitimate domain terms mot/clen/te to the codespell
ignore-list. Remaining ruff (24) + mypy (68 no-untyped-call) findings are pre-existing v2
debt, untouched here.
2026-06-19 20:54:59 +00:00

79 lines
3.8 KiB
Python

"""End-to-end RL over a cross-model workflow.
Probes the training-plane / Workflow boundary: can "rollout == serve + capture" span *two model
instances*, and can one final reward train an *earlier* model? These tests train both stages of the
T2I→I2V workflow (`flux-t2i` and `wan-i2v`) from a single final-video reward and assert:
* the rollout spans both instances with SDE capture in each (a workflow-level rollout);
* the SAME final-video advantage trains BOTH generators — including the T2I one, which never sees
the video directly (end-to-end credit across the model boundary);
* that credit is *caused by the video reward* — a constant reward (zero advantage) moves nothing;
* the two stages version independently (two WeightSyncPlans on two instances).
"""
from __future__ import annotations
import numpy as np
from v2.recipes import build_flux_t2i_card, build_wan_i2v_card
from v2.training.methods import build_workflow_rl
def _method(**kw):
return build_workflow_rl(build_flux_t2i_card(), build_wan_i2v_card(),
num_samples_per_prompt=4, rollout_steps=4, t2i_lr=0.03, i2v_lr=0.03, **kw)
def _batch():
return {"prompts": ["a fox", "blue sky", "a city"], "seeds": [1, 2, 3]}
def test_rollout_spans_two_instances_with_sde_capture():
"""A workflow-level rollout: stage 1 on one instance, stage 2 on another, each capturing an SDE
trajectory (so FlowGRPO can credit both)."""
m = _method()
assert m.t2i is not m.i2v
assert m.t2i.component("transformer") is not m.i2v.component("transformer")
res_t, _emb_t, image = m._rollout_t2i("a fox", seed=1)
res_i, _emb_i, video = m._rollout_i2v("a fox", image, seed=1)
assert any("sde_logprob" in r for r in (res_t.behavior or [])) # T2I stage captured
assert any("sde_logprob" in r for r in (res_i.behavior or [])) # I2V stage captured
assert np.asarray(video).ndim == 4 # produced a video
def test_one_final_reward_trains_both_generators():
m = _method()
t2i_w0 = m.t2i.component("transformer").w_x.copy()
i2v_w0 = m.i2v.component("transformer").w_x.copy()
last = None
for it in range(20):
_, last = m.managed_train_step(_batch(), it)
# the EARLIER model (T2I) is trained by a reward on the FINAL video — credit across the boundary
assert not np.array_equal(t2i_w0, m.t2i.component("transformer").w_x)
assert not np.array_equal(i2v_w0, m.i2v.component("transformer").w_x)
assert last["grad_norm/t2i"] > 0.0 and last["grad_norm/i2v"] > 0.0
assert 0.5 < last["ppo_ratio_mean"] < 2.0
def test_t2i_credit_is_caused_by_the_video_reward():
"""Causal check: a *constant* final reward ⇒ zero group advantage ⇒ neither generator moves. So the
T2I movement in the test above is genuinely caused by the video reward, not by the rollout itself."""
m = _method()
m._reward = lambda video: 0.5 # constant ⇒ std 0 ⇒ advantage 0
t2i_w0 = m.t2i.component("transformer").w_x.copy()
i2v_w0 = m.i2v.component("transformer").w_x.copy()
for it in range(10):
m.managed_train_step(_batch(), it)
assert np.array_equal(t2i_w0, m.t2i.component("transformer").w_x) # no signal ⇒ no T2I update
assert np.array_equal(i2v_w0, m.i2v.component("transformer").w_x) # no signal ⇒ no I2V update
def test_stages_version_independently_on_two_instances():
m = _method()
_, last = m.managed_train_step(_batch(), 0)
assert last["t2i_weights_version"] != last["i2v_weights_version"]
assert m.t2i.version_of("transformer") != "v0"
assert m.i2v.version_of("transformer") != "v0"
# each stage's sync touched only its own instance's transformer (frozen text encoders intact)
assert m.t2i.version_of("text_encoder") == "v0"
assert m.i2v.version_of("text_encoder") == "v0"