[refactor]: linear/mlp FP4 path additions for Wan-2.1 (Attn-QAT 6/12)#1390
Conversation
Merge ProtectionsYour pull request matches the following merge protections and will not be merged until they are valid. 🔴 PR merge requirementsWaiting for
This rule is failing.
|
There was a problem hiding this comment.
Code Review
This pull request introduces shape tracking capabilities for GEMM operations in the ReplicatedLinear layer and integrates quantization configuration support throughout the MLP and WanTransformer3DModel components. The changes enable the recording of unique input and output shape pairs to assist with future quantization-aware training. Feedback identifies opportunities to simplify the shape tracking logic by removing redundant data structures and suggests refining the quantization initialization in the MLP layer to avoid unnecessary method calls and premature weight processing.
| # to discover which GEMM shapes need quantized kernels. Defaults to | ||
| # False; default forward path is bit-identical to pre-slice behavior. | ||
| enable_shape_tracking = False | ||
| _unique_shapes: set[tuple[torch.Size, torch.Size]] = set() |
There was a problem hiding this comment.
The _unique_shapes set is redundant because the keys of the _shape_to_layer_types dictionary already track the unique (input_shape, output_shape) pairs.
| _unique_shapes: set[tuple[torch.Size, torch.Size]] = set() | |
| _shape_to_layer_types: dict[tuple[torch.Size, torch.Size], list[str]] = {} |
| def reset_shape_tracking(cls) -> None: | ||
| """Clear tracked shapes and layer type mappings.""" | ||
| cls._unique_shapes.clear() | ||
| cls._shape_to_layer_types.clear() |
There was a problem hiding this comment.
Since _unique_shapes is redundant, it can be removed from the reset logic.
| def reset_shape_tracking(cls) -> None: | |
| """Clear tracked shapes and layer type mappings.""" | |
| cls._unique_shapes.clear() | |
| cls._shape_to_layer_types.clear() | |
| @classmethod | |
| def reset_shape_tracking(cls) -> None: | |
| """Clear tracked shapes and layer type mappings.""" | |
| cls._shape_to_layer_types.clear() |
| if shape_key not in self._unique_shapes: | ||
| self._unique_shapes.add(shape_key) | ||
| self._shape_to_layer_types[shape_key] = [] |
There was a problem hiding this comment.
The existence check can be performed directly against the _shape_to_layer_types dictionary, eliminating the need for the separate _unique_shapes set.
| if shape_key not in self._unique_shapes: | |
| self._unique_shapes.add(shape_key) | |
| self._shape_to_layer_types[shape_key] = [] | |
| if shape_key not in self._shape_to_layer_types: | |
| self._shape_to_layer_types[shape_key] = [] |
| if quant_config is not None: | ||
| quant_method = self.fc_in.quant_config.get_quant_method(self.fc_in, f"{prefix}.fc_in") | ||
| if quant_method is not None: | ||
| quant_method.process_weights_after_loading(self.fc_in) |
There was a problem hiding this comment.
The call to get_quant_method is redundant because ReplicatedLinear.__init__ (via LinearBase) already performs this lookup and stores the result in self.fc_in.quant_method. Additionally, calling process_weights_after_loading inside __init__ is premature as the weights have only just been initialized (via torch.empty) and not yet loaded from a checkpoint. This processing should typically occur after the state dict has been loaded into the model.
| if quant_config is not None: | |
| quant_method = self.fc_in.quant_config.get_quant_method(self.fc_in, f"{prefix}.fc_in") | |
| if quant_method is not None: | |
| quant_method.process_weights_after_loading(self.fc_in) | |
| if quant_config is not None: | |
| self.fc_in.quant_method.process_weights_after_loading(self.fc_in) |
| if quant_config is not None: | ||
| quant_method = self.fc_out.quant_config.get_quant_method(self.fc_out, f"{prefix}.fc_out") | ||
| if quant_method is not None: | ||
| quant_method.process_weights_after_loading(self.fc_out) |
There was a problem hiding this comment.
Similar to fc_in, the explicit call to get_quant_method is redundant here as the method is already resolved and available via self.fc_out.quant_method. Also, calling process_weights_after_loading in the constructor is likely premature.
| if quant_config is not None: | |
| quant_method = self.fc_out.quant_config.get_quant_method(self.fc_out, f"{prefix}.fc_out") | |
| if quant_method is not None: | |
| quant_method.process_weights_after_loading(self.fc_out) | |
| if quant_config is not None: | |
| self.fc_out.quant_method.process_weights_after_loading(self.fc_out) |
651f4eb to
11f370b
Compare
Slice 6 of 12 in the PR hao-ai-lab#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 hao-ai-lab#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 hao-ai-lab#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 hao-ai-lab#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 hao-ai-lab#1383 merged at ``ba75ad82dbe4a7069412494c051c1c69155fdc9d``). No stack dependency — this is a clean linear PR off ``main``. Provenance ---------- Files extracted from PR hao-ai-lab#1225 (hao-ai-lab#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 hao-ai-lab#1358), 5/12 (PR hao-ai-lab#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>
622da42 to
ec6c2f5
Compare
…tracking (hao-ai-lab#1390) - mlp.py: remove the dormant process_weights_after_loading calls in MLP.__init__; every quant method resolves to the base-class no-op (FP4 conversion is loader-driven), so they were inert. Keeps MLP consistent with the attention layers. - linear.py: fold the redundant _unique_shapes set into _shape_to_layer_types (now set-valued); route shape diagnostics through the module logger instead of print. - test: drop the now-dead _unique_shapes refs and the cast/getattr indirection.
Pre-commit checks failedHi @SolitaryThinker, the pre-commit checks have failed. To fix them locally: # Install pre-commit if you haven't already
uv pip install pre-commit
pre-commit install
# Run all checks and auto-fix what's possible
pre-commit run --all-filesCommon fixes:
After fixing, commit and push the changes. The checks will re-run automatically. For future commits, |
|
/merge |
…tracking (hao-ai-lab#1390) - mlp.py: remove the dormant process_weights_after_loading calls in MLP.__init__; every quant method resolves to the base-class no-op (FP4 conversion is loader-driven), so they were inert. Keeps MLP consistent with the attention layers. - linear.py: fold the redundant _unique_shapes set into _shape_to_layer_types (now set-valued); route shape diagnostics through the module logger instead of print. - test: drop the now-dead _unique_shapes refs and the cast/getattr indirection.
a99af5d to
cf78216
Compare
Slice 6 of 12 in the PR #1225 decomposition. Tier 2 — backward-compat additions, gated paths only. No FP4 activation in this slice.
Summary
Plumbing-only slice that wires an optional
quant_config: QuantizationConfig | None = Nonethrough the Wan-2.1 DiT constructor chain so a futureNVFP4QAT-configured Wan-2.1 build (later slice in this stack) can quantize its attention QKV/out projections and FFN without further refactor. Also adds opt-in shape-tracking instrumentation onReplicatedLinearfor QAT-aware backends to discover which GEMM shapes need quantized kernels.Every new code path is gated. Defaults (
quant_config=None,enable_shape_tracking=False) preserve current behavior bit-identically.Files
fastvideo/layers/linear.pyenable_shape_tracking = Falseand_shape_to_layer_typesplus helper methodsget_shape_mapping,reset_shape_tracking,_track_shape,print_shape_summarytoReplicatedLinear. Inserts a singleif self.enable_shape_tracking: self._track_shape(...)call inforwardafter the existing GEMM; diagnostic output goes through the modulelogger. No constructor params changed; no new dependencies.fastvideo/layers/mlp.pyquant_config: QuantizationConfig | None = NonetoMLP.__init__and threads it (plus an explicitprefix) into bothReplicatedLinearsub-layers. ImportsQuantizationConfigfromfastvideo.layers.quantization.fastvideo/models/dits/wanvideo.pyquant_config(andprefixwhere missing) toWanSelfAttention,WanI2VCrossAttention,WanTransformerBlock,WanTransformerBlock_VSA, and readsconfig.quant_configinWanTransformer3DModel.__init__. Threads the kwarg through everyReplicatedLinearandMLPconstruction call.config.quant_configalready exists onDiTBaseConfig; no new config field is introduced in this slice.fastvideo/tests/layers/test_linear_mlp_quant_gating.pyFiles in PR #1225 considered but NOT applied
The source-SHA
fastvideo/layers/linear.pyalso contains several edits that pre-date currentmainand would silently regress it:NVFP4Config-only-quantizes-a-curated-subset explanatory comments inLinearBase.__init__andReplicatedLinear.__init__(added on main as part of slice 3 / PR [feat] Dreamverse 13/14: Activate LTX2 integration #1336).if self.quant_method is None: self.quant_method = UnquantizedLinearMethod()fallback insideLinearBase.__init__(also part of the slice 3 hardening).create_weightsreformat 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()inColumnParallelLinear.__init__/forwardandRowParallelLinear.__init__/forward.LinearBase.__init__on currentmainalready guaranteesquant_methodis non-None, so the source PR's defensive checks would be no-ops; they pre-date the slice 3 base-class hardening.if quant_method is Nonedefensive insert inReplicatedLinear.forward— also a no-op against currentmainfor 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 thesage_attn3.pyhead_sizeremoval (see PR #1383).Also dropped: an unused
from contextlib import nullcontextimport that the source PR staged inwanvideo.pyfor a deeper-stack slice (ruff would reject it as unused).Stacking
main(slice 5 / PR [refactor]: shared attention infra additions for QAT-compat (Attn-QAT 5/12) #1383 merged atba75ad82dbe4a7069412494c051c1c69155fdc9d).main.Provenance
Files extracted from PR #1225 (#1225) at source SHA
3f818d0fc532ec6494b465967d5f485150917d0cand audited against currentmain.mlp.pyandwanvideo.py(modulo the dropped unused import) were applied directly —mainhad not diverged from the source's merge-base for those files.linear.pywas hand-merged to preserve currentmain's slice-3 hardening (see the NOT applied list above); only the additive shape-tracking surface was carried over.Two cleanups were applied beyond the source extraction:
process_weights_after_loadingcalls the source staged inMLP.__init__were dropped. Every quant method currently in the tree resolves to the base-class no-op for that hook (FP4 weight conversion is loader-driven, not done per-layer at construction), so the calls were inert. Removing them also keepsMLPconsistent with the attention layers, which threadquant_configwithout any post-load hook.ReplicatedLinearwas tightened: the redundant_unique_shapesset was folded into_shape_to_layer_types(whose keys already encode uniqueness; entries are nowset-valued), and per-shape diagnostics moved fromprintto the modulelogger.Test plan
Adds
fastvideo/tests/layers/test_linear_mlp_quant_gating.py— a CPU-only regression test (no GPU required) for the gated surfaces this slice introduces:ReplicatedLinearshape tracking is off by default (no shapes recorded), and records distinct(input, output)shapes / resets cleanly whenenable_shape_trackingis explicitly enabled.MLPwithquant_config=None(the default) keeps both sub-layers onUnquantizedLinearMethodand produces correctly-shaped output.The
quant_configplumbing stays dormant until a future slice setsconfig.quant_configto a non-None value; the activation slice (12/12) will carry the contract test for the full FP4 Wan-2.1 path.Pre-commit (
yapf+ruff+codespell+mypy) coverslinear.py/mlp.py/wanvideo.py;fastvideo/tests/is excluded from those hooks by design.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).