Skip to content

Commit 11f370b

Browse files
SolitaryThinkerjzhang38RandNMR73
committed
[refactor]: linear/mlp FP4 path additions for Wan-2.1 (Attn-QAT 6/12)
Slice 6 of 12 in the PR #1225 decomposition. Tier 2 — backward- compat additions, gated paths only. No activation in this slice. What this adds -------------- * fastvideo/layers/linear.py (+54): adds opt-in shape-tracking instrumentation to ``ReplicatedLinear`` so upcoming QAT-aware backends can discover which GEMM shapes need quantized kernels. Gated by the class attr ``enable_shape_tracking = False``; the default forward path is bit-identical to pre-slice behavior. Adds ``get_shape_mapping``, ``reset_shape_tracking``, ``_track_shape``, ``print_shape_summary``. No new constructor params. * fastvideo/layers/mlp.py (+22): adds an optional ``quant_config: QuantizationConfig | None = None`` kwarg to ``MLP.__init__`` and threads it (plus an explicit ``prefix``) into the two underlying ``ReplicatedLinear`` instances. When ``quant_config is not None``, runs ``process_weights_after_loading`` on each sub-layer's resolved quant method. When ``quant_config is None`` (default), behavior is unchanged: ``ReplicatedLinear`` falls back to ``UnquantizedLinearMethod`` exactly as before. * fastvideo/models/dits/wanvideo.py (+67): wires ``quant_config`` through ``WanSelfAttention``, ``WanI2VCrossAttention``, ``WanTransformerBlock``, ``WanTransformerBlock_VSA``, and ``WanTransformer3DModel`` constructors so a future ``NVFP4QAT``- configured Wan2.1 build can quantize its attention QKV/out projections and FFN. Reads ``config.quant_config`` from ``WanVideoConfig`` (the field is already present on the shared ``DiTBaseConfig``). All new kwargs default to ``None``; default Wan2.1 path stays bit-identical. Files in PR #1225 considered but NOT applied -------------------------------------------- The source-SHA ``fastvideo/layers/linear.py`` also contains several edits that pre-date current ``main`` and would silently regress it: * Removal of the ``NVFP4Config``-only-quantizes-a-curated-subset explanatory comments in ``LinearBase.__init__`` and ``ReplicatedLinear.__init__`` (added on main as part of slice 3 / PR #1336). * Removal of the ``if self.quant_method is None: self.quant_method = UnquantizedLinearMethod()`` fallback inside ``LinearBase.__init__`` (also part of the slice 3 hardening). * A constructor / ``create_weights`` reformat from multi-line to compact one-line style — pure style noise. * ``assert self.quant_method is not None`` → ``if self.quant_method is None: self.quant_method = UnquantizedLinearMethod()`` in ``ColumnParallelLinear.__init__/forward`` and ``RowParallelLinear.__init__/forward``. ``LinearBase.__init__`` on current ``main`` already guarantees ``quant_method`` is non-None, so the source PR's defensive checks would be no-ops; they pre-date the slice 3 base-class hardening. * The same ``if quant_method is None`` defensive insert in ``ReplicatedLinear.forward`` — also a no-op against current ``main`` for the same reason. None of the skipped edits affect the FP4 path; current ``main``'s behavior on those lines is strictly stronger than the source SHA's. This mirrors slice 5's intentional skip of the ``sage_attn3.py`` head_size removal (see PR #1383). Also dropped: an unused ``from contextlib import nullcontext`` import that the source PR staged in ``wanvideo.py`` for a deeper-stack slice (ruff would reject it as unused). Stacking -------- Base: ``main`` (slice 5 / PR #1383 merged at ``ba75ad82dbe4a7069412494c051c1c69155fdc9d``). No stack dependency — this is a clean linear PR off ``main``. Provenance ---------- Files extracted from PR #1225 (#1225) at source SHA ``3f818d0fc532ec6494b465967d5f485150917d0c`` and audited against current ``main``. ``mlp.py`` and ``wanvideo.py`` (modulo the dropped unused import) were applied directly — ``main`` had not diverged from the source's merge-base for those files. ``linear.py`` was hand-merged to preserve current ``main``'s slice-3 hardening (see the ``NOT applied`` list above); only the additive shape-tracking surface was carried over. Pre-commit gate (yapf + ruff + codespell + mypy) passes on all three changed files. Test plan --------- No new tests this slice. The shape-tracking surface is opt-in instrumentation (default disabled) and the ``quant_config`` plumbing is dormant until a future slice sets ``config.quant_config`` to a non-None value. The activation slice (12/12) will carry the contract test for the full FP4 Wan-2.1 path. Sequence -------- Attn-QAT-Stack: 6/12. Earlier merged slices: 4/12 (PR #1358), 5/12 (PR #1383). Out of scope for this slice: the actual FP4 activation switch, weight-loading conversion, and any cross-cutting config registration (later slices). Co-Authored-By: Peiyuan Zhang <a1286225768@gmail.com> Co-Authored-By: Matthew Noto <notomatthew31@gmail.com>
1 parent ba75ad8 commit 11f370b

3 files changed

Lines changed: 115 additions & 27 deletions

File tree

fastvideo/layers/linear.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@ class ReplicatedLinear(LinearBase):
219219
(e.g. model.layers.0.qkv_proj)
220220
"""
221221

222+
# Opt-in instrumentation: when ``enable_shape_tracking`` is set to True,
223+
# ``forward`` records every unique ``(input_shape, output_shape)`` pair
224+
# observed across all ``ReplicatedLinear`` instances, along with the
225+
# subclass name that produced it. Used by upcoming QAT-aware backends
226+
# to discover which GEMM shapes need quantized kernels. Defaults to
227+
# False; default forward path is bit-identical to pre-slice behavior.
228+
enable_shape_tracking = False
229+
_unique_shapes: set[tuple[torch.Size, torch.Size]] = set()
230+
_shape_to_layer_types: dict[tuple[torch.Size, torch.Size], list[str]] = {}
231+
222232
def __init__(
223233
self,
224234
input_size: int,
@@ -285,6 +295,8 @@ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, Parameter | None]:
285295
bias = self.bias if not self.skip_bias_add else None
286296
assert self.quant_method is not None
287297
output = self.quant_method.apply(self, x, bias)
298+
if self.enable_shape_tracking:
299+
self._track_shape(x.shape, output.shape)
288300
output_bias = self.bias if self.skip_bias_add else None
289301
return output, output_bias
290302

@@ -294,6 +306,48 @@ def extra_repr(self) -> str:
294306
s += f", bias={self.bias is not None}"
295307
return s
296308

309+
@classmethod
310+
def get_shape_mapping(cls) -> dict:
311+
"""Get the mapping from (input_shape, output_shape) to layer types."""
312+
return cls._shape_to_layer_types.copy()
313+
314+
@classmethod
315+
def reset_shape_tracking(cls) -> None:
316+
"""Clear tracked shapes and layer type mappings."""
317+
cls._unique_shapes.clear()
318+
cls._shape_to_layer_types.clear()
319+
320+
def _track_shape(self, input_shape: torch.Size, output_shape: torch.Size) -> None:
321+
shape_key = (input_shape, output_shape)
322+
323+
if shape_key not in self._unique_shapes:
324+
self._unique_shapes.add(shape_key)
325+
self._shape_to_layer_types[shape_key] = []
326+
print(f"Layer: {self.prefix} | input shape: {input_shape} --> "
327+
f"output shape: {output_shape}, Quant Method: "
328+
f"{self.quant_method.__class__.__name__}")
329+
330+
layer_type = self.__class__.__name__
331+
if layer_type not in self._shape_to_layer_types[shape_key]:
332+
self._shape_to_layer_types[shape_key].append(layer_type)
333+
334+
@classmethod
335+
def print_shape_summary(cls) -> None:
336+
"""Print a summary of all unique shapes and their layer types."""
337+
if not cls._shape_to_layer_types:
338+
print("No shapes have been processed yet.")
339+
return
340+
341+
print("\n=== Matrix Multiplication Shape Summary ===")
342+
print(f"Total unique shapes: {len(cls._shape_to_layer_types)}")
343+
print()
344+
345+
for i, (shape_key, layer_types) in enumerate(cls._shape_to_layer_types.items(), 1):
346+
input_shape, output_shape = shape_key
347+
print(f"{i}. Input: {input_shape} → Output: {output_shape}")
348+
print(f" Layer types: {', '.join(layer_types)}")
349+
print()
350+
297351

298352
class ColumnParallelLinear(LinearBase):
299353
"""Linear layer with column parallelism.

fastvideo/layers/mlp.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from fastvideo.layers.activation import get_act_fn
77
from fastvideo.layers.linear import ReplicatedLinear
8+
from fastvideo.layers.quantization import QuantizationConfig
89

910

1011
class MLP(nn.Module):
@@ -21,18 +22,35 @@ def __init__(
2122
act_type: str = "gelu_pytorch_tanh",
2223
dtype: torch.dtype | None = None,
2324
prefix: str = "",
25+
quant_config: QuantizationConfig | None = None,
2426
):
2527
super().__init__()
2628
self.fc_in = ReplicatedLinear(
2729
input_dim,
2830
mlp_hidden_dim, # For activation func like SiLU that need 2x width
2931
bias=bias,
30-
params_dtype=dtype)
32+
params_dtype=dtype,
33+
quant_config=quant_config,
34+
prefix=f"{prefix}.fc_in",
35+
)
36+
if quant_config is not None:
37+
quant_method = self.fc_in.quant_config.get_quant_method(self.fc_in, f"{prefix}.fc_in")
38+
if quant_method is not None:
39+
quant_method.process_weights_after_loading(self.fc_in)
3140

3241
self.act = get_act_fn(act_type)
3342
if output_dim is None:
3443
output_dim = input_dim
35-
self.fc_out = ReplicatedLinear(mlp_hidden_dim, output_dim, bias=bias, params_dtype=dtype)
44+
self.fc_out = ReplicatedLinear(mlp_hidden_dim,
45+
output_dim,
46+
bias=bias,
47+
params_dtype=dtype,
48+
quant_config=quant_config,
49+
prefix=f"{prefix}.fc_out")
50+
if quant_config is not None:
51+
quant_method = self.fc_out.quant_config.get_quant_method(self.fc_out, f"{prefix}.fc_out")
52+
if quant_method is not None:
53+
quant_method.process_weights_after_loading(self.fc_out)
3654

3755
def forward(self, x: torch.Tensor) -> torch.Tensor:
3856
x, _ = self.fc_in(x)

fastvideo/models/dits/wanvideo.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from fastvideo.logger import init_logger
2727
from fastvideo.models.dits.base import BaseDiT
2828
from fastvideo.platforms import AttentionBackendEnum, current_platform
29+
from fastvideo.layers.quantization import QuantizationConfig
2930

3031
from fastvideo.distributed.parallel_state import get_sp_world_size
3132

@@ -106,7 +107,9 @@ def __init__(self,
106107
window_size=(-1, -1),
107108
qk_norm=True,
108109
eps=1e-6,
109-
parallel_attention=False) -> None:
110+
parallel_attention=False,
111+
quant_config: QuantizationConfig | None = None,
112+
prefix: str = "") -> None:
110113
assert dim % num_heads == 0
111114
super().__init__()
112115
self.dim = dim
@@ -118,10 +121,10 @@ def __init__(self,
118121
self.parallel_attention = parallel_attention
119122

120123
# layers
121-
self.to_q = ReplicatedLinear(dim, dim)
122-
self.to_k = ReplicatedLinear(dim, dim)
123-
self.to_v = ReplicatedLinear(dim, dim)
124-
self.to_out = ReplicatedLinear(dim, dim)
124+
self.to_q = ReplicatedLinear(dim, dim, quant_config=quant_config, prefix=f"{prefix}.to_q")
125+
self.to_k = ReplicatedLinear(dim, dim, quant_config=quant_config, prefix=f"{prefix}.to_k")
126+
self.to_v = ReplicatedLinear(dim, dim, quant_config=quant_config, prefix=f"{prefix}.to_v")
127+
self.to_out = ReplicatedLinear(dim, dim, quant_config=quant_config, prefix=f"{prefix}.to_out")
125128
self.norm_q = RMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
126129
self.norm_k = RMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
127130

@@ -194,13 +197,15 @@ def __init__(
194197
qk_norm=True,
195198
eps=1e-6,
196199
supported_attention_backends: tuple[AttentionBackendEnum, ...]
197-
| None = None
200+
| None = None,
201+
quant_config: QuantizationConfig | None = None,
202+
prefix: str = "",
198203
) -> None:
199204
super().__init__(dim, num_heads, window_size, qk_norm, eps,
200-
supported_attention_backends)
205+
supported_attention_backends, quant_config=quant_config, prefix=prefix)
201206

202-
self.add_k_proj = ReplicatedLinear(dim, dim)
203-
self.add_v_proj = ReplicatedLinear(dim, dim)
207+
self.add_k_proj = ReplicatedLinear(dim, dim, quant_config=quant_config, prefix=f"{prefix}.add_k_proj")
208+
self.add_v_proj = ReplicatedLinear(dim, dim, quant_config=quant_config, prefix=f"{prefix}.add_v_proj")
204209
self.norm_added_k = RMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
205210
self.norm_added_q = RMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
206211

@@ -246,16 +251,17 @@ def __init__(self,
246251
added_kv_proj_dim: int | None = None,
247252
supported_attention_backends: tuple[AttentionBackendEnum, ...]
248253
| None = None,
254+
quant_config: QuantizationConfig | None = None,
249255
prefix: str = ""):
250256
super().__init__()
251257

252258
# 1. Self-attention
253259
self.norm1 = FP32LayerNorm(dim, eps, elementwise_affine=False)
254-
self.to_q = ReplicatedLinear(dim, dim, bias=True)
255-
self.to_k = ReplicatedLinear(dim, dim, bias=True)
256-
self.to_v = ReplicatedLinear(dim, dim, bias=True)
260+
self.to_q = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_q")
261+
self.to_k = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_k")
262+
self.to_v = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_v")
257263

258-
self.to_out = ReplicatedLinear(dim, dim, bias=True)
264+
self.to_out = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_out")
259265
self.attn1 = DistributedAttention(
260266
num_heads=num_heads,
261267
head_size=dim // num_heads,
@@ -290,13 +296,17 @@ def __init__(self,
290296
self.attn2 = WanI2VCrossAttention(dim,
291297
num_heads,
292298
qk_norm=qk_norm,
293-
eps=eps)
299+
eps=eps,
300+
quant_config=quant_config,
301+
prefix=f"{prefix}.attn2")
294302
else:
295303
# T2V
296304
self.attn2 = WanT2VCrossAttention(dim,
297305
num_heads,
298306
qk_norm=qk_norm,
299-
eps=eps)
307+
eps=eps,
308+
quant_config=quant_config,
309+
prefix=f"{prefix}.attn2")
300310
self.cross_attn_residual_norm = ScaleResidualLayerNormScaleShift(
301311
dim,
302312
norm_type="layer",
@@ -306,7 +316,7 @@ def __init__(self,
306316
compute_dtype=torch.float32)
307317

308318
# 3. Feed-forward
309-
self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh")
319+
self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh", quant_config=quant_config, prefix=f"{prefix}.ffn")
310320
self.mlp_residual = ScaleResidual()
311321

312322
self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
@@ -406,17 +416,17 @@ def __init__(self,
406416
added_kv_proj_dim: int | None = None,
407417
supported_attention_backends: tuple[AttentionBackendEnum, ...]
408418
| None = None,
419+
quant_config: QuantizationConfig | None = None,
409420
prefix: str = ""):
410421
super().__init__()
411422

412423
# 1. Self-attention
413424
self.norm1 = FP32LayerNorm(dim, eps, elementwise_affine=False)
414-
self.to_q = ReplicatedLinear(dim, dim, bias=True)
415-
self.to_k = ReplicatedLinear(dim, dim, bias=True)
416-
self.to_v = ReplicatedLinear(dim, dim, bias=True)
417-
self.to_gate_compress = ReplicatedLinear(dim, dim, bias=True)
418-
419-
self.to_out = ReplicatedLinear(dim, dim, bias=True)
425+
self.to_q = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_q")
426+
self.to_k = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_k")
427+
self.to_v = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_v")
428+
self.to_gate_compress = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_gate_compress")
429+
self.to_out = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config, prefix=f"{prefix}.to_out")
420430
self.attn1 = DistributedAttention_VSA(
421431
num_heads=num_heads,
422432
head_size=dim // num_heads,
@@ -451,13 +461,17 @@ def __init__(self,
451461
self.attn2 = WanI2VCrossAttention(dim,
452462
num_heads,
453463
qk_norm=qk_norm,
454-
eps=eps)
464+
eps=eps,
465+
quant_config=quant_config,
466+
prefix=f"{prefix}.attn2")
455467
else:
456468
# T2V
457469
self.attn2 = WanT2VCrossAttention(dim,
458470
num_heads,
459471
qk_norm=qk_norm,
460-
eps=eps)
472+
eps=eps,
473+
quant_config=quant_config,
474+
prefix=f"{prefix}.attn2")
461475
self.cross_attn_residual_norm = ScaleResidualLayerNormScaleShift(
462476
dim,
463477
norm_type="layer",
@@ -467,7 +481,7 @@ def __init__(self,
467481
compute_dtype=torch.float32)
468482

469483
# 3. Feed-forward
470-
self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh")
484+
self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh", quant_config=quant_config, prefix=f"{prefix}.ffn")
471485
self.mlp_residual = ScaleResidual()
472486

473487
self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
@@ -556,6 +570,7 @@ class WanTransformer3DModel(BaseDiT):
556570
def __init__(self, config: WanVideoConfig, hf_config: dict[str,
557571
Any]) -> None:
558572
super().__init__(config=config, hf_config=hf_config)
573+
self.quant_config = config.quant_config
559574

560575
inner_dim = config.num_attention_heads * config.attention_head_dim
561576
self.hidden_size = config.hidden_size
@@ -594,6 +609,7 @@ def __init__(self, config: WanVideoConfig, hf_config: dict[str,
594609
config.eps,
595610
config.added_kv_proj_dim,
596611
self._supported_attention_backends,
612+
quant_config=config.quant_config,
597613
prefix=f"{config.prefix}.blocks.{i}")
598614
for i in range(config.num_layers)
599615
])

0 commit comments

Comments
 (0)