Skip to content

Commit 647a3c5

Browse files
committed
perf(fastwam): add switchable TE RMSNorm for DiT and VAE
1 parent cf172db commit 647a3c5

9 files changed

Lines changed: 242 additions & 29 deletions

File tree

configs/models/embodied/fastwam.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ model:
2222
action_dit_pretrained_path: checkpoints/ActionDiT_linear_interp_Wan22_alphascale_1024hdim.pt
2323
redirect_common_files: true
2424
dtype: bfloat16
25+
# RMSNorm kernel for DiT q/k and VAE: te for torch < 2.9, wan for torch >= 2.9
26+
rmsnorm_impl: te
2527

2628
data:
2729
num_video_frames: 9

loongforge/embodied/model/fastwam/action/dit.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import torch.nn as nn
2525

2626
from loongforge.embodied.model.fastwam.utils.gradient import gradient_checkpoint_forward
27+
from loongforge.embodied.model.fastwam.utils.state_dict import EXTRA_STATE_SUFFIX
2728
from loongforge.embodied.model.fastwam.wan.dit import (
2829
DiTBlock,
2930
precompute_freqs_cis,
@@ -79,6 +80,7 @@ def __init__(
7980
attn_head_dim: int,
8081
num_layers: int,
8182
use_gradient_checkpointing: bool = False,
83+
rmsnorm_impl: str = "wan",
8284
):
8385
"""Initialize the action DiT backbone and embedding layers."""
8486
super().__init__()
@@ -121,6 +123,7 @@ def __init__(
121123
num_heads=num_heads,
122124
ffn_dim=ffn_dim,
123125
eps=eps,
126+
rmsnorm_impl=rmsnorm_impl,
124127
)
125128
for _ in range(num_layers)
126129
]
@@ -135,11 +138,17 @@ def __init__(
135138

136139
@classmethod
137140
def backbone_key_set(cls, keys) -> set[str]:
138-
"""Return pretrained keys that belong to the shared action backbone."""
141+
"""Return pretrained keys that belong to the shared action backbone.
142+
143+
``_extra_state`` is excluded so the expected key set is the same under both
144+
RMSNorm implementations; the TE bookkeeping a module built for itself is kept
145+
by seeding the load from its own ``state_dict()``.
146+
"""
139147
return {
140148
key
141149
for key in keys
142150
if not any(key.startswith(prefix) for prefix in cls.ACTION_BACKBONE_SKIP_PREFIXES)
151+
and not key.endswith(EXTRA_STATE_SUFFIX)
143152
}
144153

145154
@classmethod

loongforge/embodied/model/fastwam/modeling_configuration_fastwam.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ class FastWAMModelConfig:
7070
)
7171
redirect_common_files: bool = True
7272
dtype: str = "bfloat16"
73+
# q/k RMSNorm implementation for both DiT experts: "wan" (upstream module built
74+
# on F.rms_norm) or "te" (TransformerEngine).
75+
# torch < 2.9.0 has no fused rms_norm CUDA kernel, so "te" is far faster there;
76+
# from torch 2.9.0 on the native kernel wins at the DiT's hidden size (3072),
77+
# which TE 2.9 has no tuned kernel for. See `wan.dit.make_rmsnorm`.
78+
rmsnorm_impl: str = "wan" # {"wan", "te"}
7379

7480
# ── Nested architecture configs (fixed for Wan2.2-5B, not in YAML) ────────
7581
video_dit_config: dict[str, Any] = field(default_factory=lambda: {
@@ -114,6 +120,16 @@ def __post_init__(self) -> None:
114120
f"got {self.variant!r}"
115121
)
116122

123+
# ── Validate rmsnorm_impl ─────────────────────────────────────────────
124+
# Mirrors the names accepted by `wan.dit.make_rmsnorm`; kept as a literal
125+
# here so importing this config does not pull in torch / TransformerEngine.
126+
valid_rmsnorm_impls = {"wan", "te"}
127+
if self.rmsnorm_impl not in valid_rmsnorm_impls:
128+
raise ValueError(
129+
f"FastWAMModelConfig.rmsnorm_impl must be one of {sorted(valid_rmsnorm_impls)}, "
130+
f"got {self.rmsnorm_impl!r}"
131+
)
132+
117133
# ── Validate num_video_frames constraint via data config ───────────────
118134
# (num_video_frames lives in DataConfig; validation happens there)
119135

@@ -134,3 +150,7 @@ def __post_init__(self) -> None:
134150
# ── Sync action_dim → nested dit configs ──────────────────────────────
135151
self.video_dit_config["action_dim"] = self.action_dim
136152
self.action_dit_config["action_dim"] = self.action_dim
153+
154+
# ── Sync rmsnorm_impl → nested dit configs ────────────────────────────
155+
self.video_dit_config["rmsnorm_impl"] = self.rmsnorm_impl
156+
self.action_dit_config["rmsnorm_impl"] = self.rmsnorm_impl

loongforge/embodied/model/fastwam/mot/fastwam.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import logging
2626

2727
from loongforge.embodied.model.fastwam.action.dit import ActionDiT
28+
from loongforge.embodied.model.fastwam.utils.state_dict import drop_extra_state
2829
from loongforge.embodied.model.fastwam.wan.loader import load_wan22_ti2v_5b_components
2930
from loongforge.embodied.model.fastwam.mot.model import MoT
3031
from loongforge.embodied.model.fastwam.action.schedulers import WanContinuousFlowMatchScheduler
@@ -1157,7 +1158,9 @@ def infer(
11571158
def save_checkpoint(self, path, optimizer=None, step=None):
11581159
"""Save MoT, optional proprio encoder, and optimizer checkpoint state."""
11591160
payload = {
1160-
"mot": self.mot.state_dict(),
1161+
# `_extra_state` is dropped so the file does not depend on the RMSNorm
1162+
# implementation it was trained with (see `utils.state_dict`).
1163+
"mot": drop_extra_state(self.mot.state_dict()),
11611164
"step": step,
11621165
"torch_dtype": str(self.torch_dtype),
11631166
}
@@ -1171,10 +1174,10 @@ def load_checkpoint(self, path, optimizer=None):
11711174
"""Load MoT, optional proprio encoder, and optimizer checkpoint state."""
11721175
payload = torch.load(path, map_location="cpu")
11731176
if "mot" in payload:
1174-
self.mot.load_state_dict(payload["mot"], strict=False)
1177+
self.mot.load_state_dict(drop_extra_state(payload["mot"]), strict=False)
11751178
elif "dit" in payload:
11761179
logger.warning("Loading legacy `dit` checkpoint into video expert only.")
1177-
self.video_expert.load_state_dict(payload["dit"], strict=False)
1180+
self.video_expert.load_state_dict(drop_extra_state(payload["dit"]), strict=False)
11781181
else:
11791182
raise ValueError(f"Checkpoint missing both `mot` and `dit` keys: {path}")
11801183
if self.proprio_encoder is not None:

loongforge/embodied/model/fastwam/utils/state_dict.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,43 @@
1515
# limitations under the License.
1616
"""State-dict conversion helpers for FastWAM Wan components."""
1717

18+
EXTRA_STATE_SUFFIX = "._extra_state"
19+
20+
21+
def drop_extra_state(state_dict):
22+
"""Return ``state_dict`` without Transformer Engine ``_extra_state`` entries.
23+
24+
Transformer Engine modules add a ``<prefix>._extra_state`` key holding FP8 recipe
25+
bookkeeping (an empty ``uint8`` tensor under bf16, so dropping it is lossless).
26+
Their learnable parameter is still named ``weight``, so this key is the only
27+
schema difference between the TE and Wan RMSNorm implementations.
28+
29+
Apply this on both sides of a checkpoint round trip: on save so the file is
30+
implementation-agnostic, and on load so an existing TE-saved file carries no
31+
keys the Wan implementation cannot place. A freshly built TE module already
32+
holds valid bookkeeping, so leaving it untouched is the correct outcome.
33+
34+
Filtering the key set up front is preferred over a
35+
``register_load_state_dict_post_hook`` that edits ``incompatible_keys``: the
36+
hook would also mask a genuinely absent key at every call site it is installed
37+
on, whereas here ``strict`` keeps its full meaning for real weights.
38+
39+
This works because every load site that can see a cross-implementation file
40+
passes ``strict=False`` (``mot.fastwam.load_checkpoint``, ``wan.core``,
41+
``wan.loader``). Under ``strict=True`` a TE module would still report its own
42+
``_extra_state`` as missing, so seed such a load from the module's own
43+
``state_dict()`` — this is what ``ActionDiT.from_pretrained`` does, together
44+
with ``ActionDiT.backbone_key_set`` filtering the same suffix out of the keys
45+
it expects the payload to provide.
46+
47+
Args:
48+
state_dict: Mapping of parameter name to tensor.
49+
50+
Returns:
51+
A new dict with every ``._extra_state`` key removed.
52+
"""
53+
return {key: value for key, value in state_dict.items() if not key.endswith(EXTRA_STATE_SUFFIX)}
54+
1855

1956
def wan_video_vae_state_dict_converter(state_dict):
2057
"""Convert Wan VAE checkpoint keys to the local module prefix layout."""

loongforge/embodied/model/fastwam/wan/core.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from PIL import Image
2525

2626
from loongforge.embodied.model.fastwam.action.schedulers import WanContinuousFlowMatchScheduler
27+
from loongforge.embodied.model.fastwam.utils.state_dict import drop_extra_state
2728
from loongforge.embodied.model.fastwam.wan.dit import WanVideoDiT
2829
from loongforge.embodied.model.fastwam.wan.loader import load_wan22_ti2v_5b_components
2930

@@ -437,7 +438,9 @@ def infer(
437438
def save_checkpoint(self, path, optimizer=None, step=None):
438439
"""Save DiT weights and optional optimizer state to a checkpoint path."""
439440
payload = {
440-
"dit": self.dit.state_dict(),
441+
# `_extra_state` is dropped so the file does not depend on the RMSNorm
442+
# implementation it was trained with (see `utils.state_dict`).
443+
"dit": drop_extra_state(self.dit.state_dict()),
441444
"step": step,
442445
"torch_dtype": str(self.torch_dtype),
443446
}
@@ -448,7 +451,7 @@ def save_checkpoint(self, path, optimizer=None, step=None):
448451
def load_checkpoint(self, path, optimizer=None):
449452
"""Load DiT weights and optional optimizer state from a checkpoint path."""
450453
payload = torch.load(path, map_location="cpu")
451-
self.dit.load_state_dict(payload["dit"], strict=False)
454+
self.dit.load_state_dict(drop_extra_state(payload["dit"]), strict=False)
452455
if optimizer is not None and "optimizer" in payload:
453456
optimizer.load_state_dict(payload["optimizer"])
454457
return payload

loongforge/embodied/model/fastwam/wan/dit.py

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import torch
2323
import torch.nn as nn
2424
import torch.nn.functional as F
25+
import transformer_engine.pytorch as te
2526
from einops import rearrange
2627

2728
from loongforge.embodied.model.fastwam.utils.gradient import gradient_checkpoint_forward
@@ -177,28 +178,75 @@ def create_group_causal_attn_mask(
177178
return attn_mask
178179

179180

180-
class RMSNorm(nn.Module):
181-
"""Root-mean-square normalization for Wan DiT projections."""
181+
class WanRMSNorm(nn.Module):
182+
"""Root-mean-square normalization for Wan DiT projections via ``F.rms_norm``.
183+
184+
This is upstream Wan's own implementation, hence the name.
185+
"""
182186

183187
def __init__(self, dim, eps=1e-5):
184188
"""Initialize RMSNorm scale and epsilon."""
185189
super().__init__()
186190
self.eps = eps
187191
self.weight = nn.Parameter(torch.ones(dim))
188192

189-
def norm(self, x):
190-
"""Normalize input by root mean square magnitude."""
191-
return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
192-
193193
def forward(self, x):
194-
"""Apply RMS normalization and learned scaling via the fused ATen kernel."""
194+
"""Apply RMS normalization and learned scaling via the ATen kernel."""
195195
# F.rms_norm only fuses when weight and x share a dtype; otherwise scale
196196
# separately to keep the original dtype promotion (fp32 weight -> fp32 out).
197197
if self.weight.dtype == x.dtype:
198198
return F.rms_norm(x, self.weight.shape, weight=self.weight, eps=self.eps)
199199
return F.rms_norm(x, self.weight.shape, eps=self.eps) * self.weight
200200

201201

202+
class TERMSNorm(te.RMSNorm):
203+
"""Transformer Engine RMSNorm for Wan DiT attention projections."""
204+
205+
def __init__(self, dim, eps=1e-5):
206+
"""Initialize a standard (non-zero-centered) RMSNorm."""
207+
# FastWAM constructs experts on CPU before moving them to the target GPU.
208+
super().__init__(dim, eps=eps, device="cpu", zero_centered_gamma=False)
209+
210+
211+
def make_rmsnorm(dim: int, eps: float, impl: str) -> nn.Module:
212+
"""Build the q/k RMSNorm module selected by ``impl``.
213+
214+
Both implementations expose a single ``weight`` parameter of shape ``(dim,)``,
215+
so checkpoints are interchangeable except for the ``_extra_state`` key that TE
216+
modules add (see ``utils.state_dict.drop_extra_state``).
217+
218+
Which one is faster depends on the torch build, so the choice is configuration
219+
rather than autodetection:
220+
221+
* **torch < 2.9.0** has no fused ``rms_norm`` CUDA kernel — ``F.rms_norm``
222+
decomposes into seven fp32 elementwise/reduction kernels, and TE is ~9.5x
223+
faster. Use ``te``.
224+
* **torch >= 2.9.0** ships ``vectorized_layer_norm_kernel<..., rms_norm=true>``
225+
(22 registers, 100% occupancy). TE 2.9 only precompiles tuned kernels for
226+
hidden sizes {512, 768, 1024, 2048, 4096, 8192}, so the DiT's 3072 falls back
227+
to ``rmsnorm_fwd_general_kernel`` (182 registers, 12.5% occupancy) and the
228+
native kernel is ~2.0x faster. Use ``wan``.
229+
230+
Args:
231+
dim: Normalized (last) dimension size.
232+
eps: Epsilon added to the mean square before the reciprocal square root.
233+
impl: ``"wan"`` (upstream ``F.rms_norm`` module) or ``"te"``.
234+
235+
Returns:
236+
The constructed RMSNorm module.
237+
238+
Raises:
239+
ValueError: If ``impl`` is not a recognized implementation name.
240+
"""
241+
if impl == "wan":
242+
return WanRMSNorm(dim, eps=eps)
243+
if impl == "te":
244+
return TERMSNorm(dim, eps=eps)
245+
raise ValueError(
246+
f"Unknown RMSNorm implementation {impl!r}; expected one of ['wan', 'te']."
247+
)
248+
249+
202250
class AttentionModule(nn.Module):
203251
"""Small wrapper around FastWAM flash attention."""
204252

@@ -216,7 +264,14 @@ def forward(self, q, k, v, ctx_mask=None):
216264
class SelfAttention(nn.Module):
217265
"""Wan DiT self-attention block with RoPE."""
218266

219-
def __init__(self, hidden_dim: int, attn_head_dim: int, num_heads: int, eps: float = 1e-6):
267+
def __init__(
268+
self,
269+
hidden_dim: int,
270+
attn_head_dim: int,
271+
num_heads: int,
272+
eps: float = 1e-6,
273+
rmsnorm_impl: str = "wan",
274+
):
220275
"""Initialize self-attention projections and RMS norms."""
221276
super().__init__()
222277
self.hidden_dim = hidden_dim
@@ -228,8 +283,8 @@ def __init__(self, hidden_dim: int, attn_head_dim: int, num_heads: int, eps: flo
228283
self.k = nn.Linear(hidden_dim, self.attn_hidden_dim)
229284
self.v = nn.Linear(hidden_dim, self.attn_hidden_dim)
230285
self.o = nn.Linear(self.attn_hidden_dim, hidden_dim)
231-
self.norm_q = RMSNorm(self.attn_hidden_dim, eps=eps)
232-
self.norm_k = RMSNorm(self.attn_hidden_dim, eps=eps)
286+
self.norm_q = make_rmsnorm(self.attn_hidden_dim, eps=eps, impl=rmsnorm_impl)
287+
self.norm_k = make_rmsnorm(self.attn_hidden_dim, eps=eps, impl=rmsnorm_impl)
233288

234289
# self.attn = AttentionModule(self.num_heads)
235290

@@ -247,7 +302,14 @@ def forward(self, x, freqs, self_attn_mask: Optional[torch.Tensor] = None):
247302
class CrossAttention(nn.Module):
248303
"""Wan DiT cross-attention block for text context."""
249304

250-
def __init__(self, hidden_dim: int, attn_head_dim: int, num_heads: int, eps: float = 1e-6):
305+
def __init__(
306+
self,
307+
hidden_dim: int,
308+
attn_head_dim: int,
309+
num_heads: int,
310+
eps: float = 1e-6,
311+
rmsnorm_impl: str = "wan",
312+
):
251313
"""Initialize cross-attention projections and RMS norms."""
252314
super().__init__()
253315
self.hidden_dim = hidden_dim
@@ -259,8 +321,8 @@ def __init__(self, hidden_dim: int, attn_head_dim: int, num_heads: int, eps: flo
259321
self.k = nn.Linear(hidden_dim, self.attn_hidden_dim)
260322
self.v = nn.Linear(hidden_dim, self.attn_hidden_dim)
261323
self.o = nn.Linear(self.attn_hidden_dim, hidden_dim)
262-
self.norm_q = RMSNorm(self.attn_hidden_dim, eps=eps)
263-
self.norm_k = RMSNorm(self.attn_hidden_dim, eps=eps)
324+
self.norm_q = make_rmsnorm(self.attn_hidden_dim, eps=eps, impl=rmsnorm_impl)
325+
self.norm_k = make_rmsnorm(self.attn_hidden_dim, eps=eps, impl=rmsnorm_impl)
264326

265327
# self.attn = AttentionModule(self.num_heads)
266328

@@ -288,16 +350,24 @@ def forward(self, x, gate, residual):
288350
class DiTBlock(nn.Module):
289351
"""Wan DiT transformer block with self-attention, cross-attention, and MLP."""
290352

291-
def __init__(self, hidden_dim: int, attn_head_dim: int, num_heads: int, ffn_dim: int, eps: float = 1e-6):
353+
def __init__(
354+
self,
355+
hidden_dim: int,
356+
attn_head_dim: int,
357+
num_heads: int,
358+
ffn_dim: int,
359+
eps: float = 1e-6,
360+
rmsnorm_impl: str = "wan",
361+
):
292362
"""Initialize one DiT block."""
293363
super().__init__()
294364
self.hidden_dim = hidden_dim
295365
self.attn_head_dim = attn_head_dim
296366
self.num_heads = num_heads
297367
self.ffn_dim = ffn_dim
298368

299-
self.self_attn = SelfAttention(hidden_dim, attn_head_dim, num_heads, eps)
300-
self.cross_attn = CrossAttention(hidden_dim, attn_head_dim, num_heads, eps)
369+
self.self_attn = SelfAttention(hidden_dim, attn_head_dim, num_heads, eps, rmsnorm_impl)
370+
self.cross_attn = CrossAttention(hidden_dim, attn_head_dim, num_heads, eps, rmsnorm_impl)
301371
self.norm1 = nn.LayerNorm(hidden_dim, eps=eps, elementwise_affine=False)
302372
self.norm2 = nn.LayerNorm(hidden_dim, eps=eps, elementwise_affine=False)
303373
self.norm3 = nn.LayerNorm(hidden_dim, eps=eps)
@@ -416,6 +486,7 @@ def __init__(
416486
action_group_causal_mask_mode: str = "causal",
417487
video_attention_mask_mode: str = "bidirectional",
418488
use_gradient_checkpointing: bool = False,
489+
rmsnorm_impl: str = "wan",
419490
):
420491
"""Initialize patch, text, time, transformer, and output modules."""
421492
super().__init__()
@@ -459,7 +530,10 @@ def __init__(
459530
)
460531
self.time_projection = nn.Sequential(nn.SiLU(), nn.Linear(hidden_dim, hidden_dim * 6))
461532
self.blocks = nn.ModuleList(
462-
[DiTBlock(hidden_dim, attn_head_dim, num_heads, ffn_dim, eps) for _ in range(num_layers)]
533+
[
534+
DiTBlock(hidden_dim, attn_head_dim, num_heads, ffn_dim, eps, rmsnorm_impl)
535+
for _ in range(num_layers)
536+
]
463537
)
464538
self.head = Head(hidden_dim, out_dim, patch_size, eps)
465539
self.freqs = precompute_freqs_cis_3d(attn_head_dim)

0 commit comments

Comments
 (0)