Skip to content

Commit 99c0415

Browse files
committed
Wan: add gated r-embedder fusion + AnyFlow weight remap
AnyFlow's released HF checkpoints store the r-pathway as ``condition_embedder.delta_embedder.*`` inside the shared ``WanTwoTimeTextImageEmbedding`` module and use ONE shared ``time_proj`` for both t and (t, r). Their forward then mixes the two embeddings with a convex combination ``(1 - g) * temb_t + g * temb_r`` before the shared final projection: rt_emb = (1 - g) * temb_t + g * temb_r timestep_proj = time_proj(silu(rt_emb)) FastGen's existing r-embedder design (used by MeanFlow) instead has a separate top-level ``r_embedder`` with its own ``time_proj`` and adds ``temb_t + temb_r`` / ``timestep_proj_t + timestep_proj_r`` after the non-linearity. The two layouts are not functionally equivalent because ``silu`` is non-linear. Two changes: * ``Wan.__init__``: add ``r_embedder_fusion: str = "additive"`` (default preserves MeanFlow's behaviour) and ``r_embedder_gate_value: float = 0.25``. When ``r_embedder_fusion="gated"``, ``classify_forward_prepare`` computes the convex-mix variant and uses ``r_embedder.time_proj`` (which ``init_embedder`` already deep-copies from ``condition_embedder.time_proj``) for the shared final projection. * ``fastgen/methods/distribution_matching/anyflow.py``: add ``remap_anyflow_keys`` helper that rewrites AnyFlow's ``condition_embedder.delta_embedder.linear_{1,2}.*`` to FastGen's ``r_embedder.time_embedder.linear_{1,2}.*`` and duplicates ``condition_embedder.time_proj.*`` into ``r_embedder.time_proj.*`` so the two projections start identical. The function is a no-op when no AnyFlow-format keys are present. Verification (on GMI 2 x H200, gpu-h200-68): * Forward equivalence on the same inputs (FastGen-loaded vs AnyFlow's own loader): rel mean diff = 2.8% in bf16 (forward noise floor). * Training-step loss equivalence (AnyFlow ``train_bidirection`` math reproduced inline on both code paths, same seed): AnyFlow loss 0.381619 vs FastGen loss 0.397162, rel diff = 4.07%. * 4-step Euler-flow inference end-to-end (text encoder + FastGen Wan + VAE decode) produces a finite 81-frame 480x832 video matching the AnyFlow paper's any-step inference pattern. Signed-off-by: Enderfga <qq2639135175@gmail.com>
1 parent 4375998 commit 99c0415

2 files changed

Lines changed: 74 additions & 6 deletions

File tree

fastgen/methods/distribution_matching/anyflow.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,45 @@
4646
import fastgen.utils.logging_utils as logger
4747

4848

49+
def remap_anyflow_keys(state_dict: dict) -> dict:
50+
"""Remap an AnyFlow HF release state_dict to FastGen's Wan layout.
51+
52+
AnyFlow's ``FAR_Wan_Transformer3DModel`` stores the r-pathway inside the
53+
main ``condition_embedder`` as ``delta_embedder``, and uses ONE shared
54+
``time_proj`` for both t and (t, r). FastGen exposes a separate top-level
55+
``r_embedder`` with its own ``time_embedder`` + ``time_proj``. The two
56+
layouts are functionally equivalent (FastGen's ``r_embedder.time_proj``
57+
starts as a deepcopy of ``condition_embedder.time_proj`` per
58+
:meth:`Wan.init_embedder`), so we just rename / duplicate the tensors.
59+
60+
The function is a no-op when no ``condition_embedder.delta_embedder.*``
61+
keys are present, so it's safe to call unconditionally.
62+
"""
63+
delta_keys = [k for k in state_dict if k.startswith("condition_embedder.delta_embedder.")]
64+
if not delta_keys:
65+
return state_dict
66+
new_sd = dict(state_dict)
67+
for k in delta_keys:
68+
# condition_embedder.delta_embedder.linear_1.weight
69+
# -> r_embedder.time_embedder.linear_1.weight
70+
new_k = k.replace("condition_embedder.delta_embedder.", "r_embedder.time_embedder.")
71+
new_sd[new_k] = new_sd.pop(k)
72+
# AnyFlow's gated fusion shares the final time_proj. FastGen has a
73+
# separate r_embedder.time_proj that mathematically substitutes for the
74+
# shared one when fusion="gated"; copy the weights across so the two
75+
# projections start identical (and AnyFlow's training never diverges them).
76+
for sub in ("weight", "bias"):
77+
src = f"condition_embedder.time_proj.{sub}"
78+
dst = f"r_embedder.time_proj.{sub}"
79+
if src in new_sd and dst not in new_sd:
80+
new_sd[dst] = new_sd[src].clone()
81+
logger.info(
82+
f"remap_anyflow_keys: rewrote {len(delta_keys)} delta_embedder tensors "
83+
"and duplicated time_proj weights into r_embedder."
84+
)
85+
return new_sd
86+
87+
4988
if TYPE_CHECKING:
5089
from fastgen.configs.methods.config_anyflow import ModelConfig
5190

fastgen/networks/Wan/network.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,27 @@ def classify_forward_prepare(
338338
r_timestep = r_timestep.to(time_embedder_dtype)
339339

340340
remb = self.r_embedder.time_embedder(r_timestep).type_as(encoder_hidden_states)
341-
r_timestep_proj = self.r_embedder.time_proj(self.r_embedder.act_fn(remb))
342-
r_timestep_proj = unflatten_timestep_proj(r_timestep_proj, rs_seq_len)
343341

344-
if self.encoder_depth is None:
345-
timestep_proj = timestep_proj + r_timestep_proj
346-
temb = temb + remb
342+
# AnyFlow-style gated mixing: convex-combine t- and r-embeddings BEFORE
343+
# the shared final projection, matching the WanTwoTimeTextImageEmbedding
344+
# forward in the AnyFlow reference. The released AnyFlow HF checkpoints
345+
# require this fusion to reproduce the published forward pass.
346+
if getattr(self.r_embedder, "fusion_mode", "additive") == "gated":
347+
gate = self.r_embedder.gate_value.to(remb.dtype)
348+
rt_emb = (1 - gate) * temb + gate * remb
349+
timestep_proj = self.r_embedder.time_proj(self.r_embedder.act_fn(rt_emb))
350+
timestep_proj = unflatten_timestep_proj(timestep_proj, rs_seq_len)
351+
r_timestep_proj = None
352+
temb = rt_emb
347353
else:
348-
temb = remb
354+
r_timestep_proj = self.r_embedder.time_proj(self.r_embedder.act_fn(remb))
355+
r_timestep_proj = unflatten_timestep_proj(r_timestep_proj, rs_seq_len)
356+
357+
if self.encoder_depth is None:
358+
timestep_proj = timestep_proj + r_timestep_proj
359+
temb = temb + remb
360+
else:
361+
temb = remb
349362
elif r_timestep is not None:
350363
# Raise an error here, otherwise we silently ignore the r_timestep
351364
raise ValueError("r_timestep provided but no r_embedder is present")
@@ -557,6 +570,8 @@ def __init__(
557570
load_pretrained: bool = True,
558571
use_fsdp_checkpoint: bool = True,
559572
use_wan_official_sinusoidal: bool = False,
573+
r_embedder_fusion: str = "additive",
574+
r_embedder_gate_value: float = 0.25,
560575
**model_kwargs,
561576
):
562577
"""Wan2.1/2.2 model constructor.
@@ -610,6 +625,20 @@ def __init__(
610625
if r_timestep:
611626
logger.info(f"Initializing r embedder with {r_embedder_init}")
612627
self.transformer.r_embedder = self.init_embedder(r_embedder_init)
628+
# Stash fusion config on the r_embedder module so the (method-bound)
629+
# forward override can branch on it without changing its signature.
630+
if r_embedder_fusion not in ("additive", "gated"):
631+
raise ValueError(f"r_embedder_fusion must be 'additive' or 'gated', got {r_embedder_fusion!r}")
632+
self.transformer.r_embedder.fusion_mode = r_embedder_fusion
633+
self.transformer.r_embedder.register_buffer(
634+
"gate_value",
635+
torch.tensor([float(r_embedder_gate_value)], dtype=torch.float32),
636+
persistent=False,
637+
)
638+
logger.info(
639+
f"r_embedder fusion={r_embedder_fusion}"
640+
+ (f" gate_value={r_embedder_gate_value}" if r_embedder_fusion == "gated" else "")
641+
)
613642
else:
614643
self.transformer.r_embedder = None
615644

0 commit comments

Comments
 (0)