Wan 2.2 T2V A14B diffusers PTQ: two-expert NVFP4-SVDQuant HF checkpoint export - #1957
Wan 2.2 T2V A14B diffusers PTQ: two-expert NVFP4-SVDQuant HF checkpoint export#1957jingyu-ml wants to merge 5 commits into
Conversation
…nt export - Quantize both experts by default for --model wan2.2-t2v-14b (transformer first, so transformer_2 calibrates against the quantized high-noise expert), producing a deployable two-expert checkpoint. - Apply the default Wan recipe (first-3/last-3 of 40 blocks, nothing outside blocks) BEFORE calibration via the block-range mechanism so SVDQuant leaves excluded weights bit-identical; VAE backbones keep their dedicated recipe. - Full SVDQuant on every quantized linear (self/cross-attn, FFN); no svdquant_skip_layers for Wan. - Tiny Wan fixture: 8 blocks (block-range needs >= 8) and hidden 48 (NVFP4 block-size divisible; head_dim stays 12 for even RoPE splits). - New wan22_14b_nvfp4_svdquant export test mirroring the Qwen SVDQuant structural assertions, per expert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Jingyu Xin <jingyux@nvidia.com>
📝 WalkthroughWalkthroughThe change replaces AWQ-Lite search with fixed-alpha SVDQuant smoothing, adds Wan 2.2 and MiniMax-H3 Diffusers quantization workflows, and extends unified HF export for modular pipelines. ChangesSVDQuant calibration and Wan 2.2 export
MiniMax-H3 quantization support
Modular Diffusers export
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant QuantizeCLI
participant PipelineManager
participant MiniMaxPipeline
participant Calibrator
QuantizeCLI->>PipelineManager: create MiniMax-H3 modular pipeline
PipelineManager->>MiniMaxPipeline: load T2VA components and configure offloading
QuantizeCLI->>Calibrator: dispatch calibration batch
Calibrator->>MiniMaxPipeline: run deterministic prompt-only T2V calibration
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1957 +/- ##
==========================================
- Coverage 77.76% 77.76% -0.01%
==========================================
Files 519 519
Lines 57955 58081 +126
==========================================
+ Hits 45067 45164 +97
- Misses 12888 12917 +29
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… search) - SVDQuantConfig.alpha (None = keep the AWQ-Lite search): when set, one SmoothQuant-style per-channel act-amax stats pass replaces AWQ-Lite's cache + 11-candidate search passes. The search optimizes pre-SVD, weight-only output MSE with activations unquantized -- a mismatched objective once the SVD low-rank branch absorbs the migrated outliers. alpha=1.0 migrates outliers fully to the weights (flat activations), per the SVDQuant paper. - Extract smoothquant's per-module smoothing into _smoothquant_postprocess and reuse it for the fixed-alpha pass (any quantizer format, not only int8; smoothquant() behavior unchanged). - diffusers example: --svdquant-alpha flag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Jingyu Xin <jingyux@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Jingyu Xin <jingyux@nvidia.com>
|
…gration SVDQuant calibration is now a single per-channel act-amax stats pass at a fixed migration strength (SVDQuantConfig.alpha, default 1.0 = migrate outliers fully to the weights, which the SVD low-rank branch absorbs, per the SVDQuant paper). The AWQ-Lite alpha search is removed from this path: its objective -- pre-SVD, weight-only output MSE with activations unquantized -- does not match the shipped decomposition, and it cost a full extra forward-loop pass with one quantized GEMM per alpha candidate per linear. SVDQuant now runs two forward-loop passes instead of three. Checkpoint format is unchanged; scale values change (paper-aligned). The diffusers example reuses --alpha for the strength (the transient --svdquant-alpha flag from the previous commit is removed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Jingyu Xin <jingyux@nvidia.com>
|
Offline A/B, rank 32, Wan2.2-T2V-A14B (searched AWQ vs SmoothQuant α=1.0):
Reading: weight-reconstruction error is structurally biased toward the search (it is the search's objective and ignores activation quantization entirely — the search runs with input quantizers disabled). The 3–4× larger 🤖 Generated with Claude Code |
|
Rank ladder complete — all three Wan2.2-T2V-A14B SVDQuant exports on the final (SmoothQuant-only) path, each passing the full structural verify gate (both experts NVFP4_SVD, blocks 3–36 only, lora/pre_quant_scale on all 340 linears per expert, excluded region bit-identical in export precision, complete pipeline dirs):
(calib 1 × 20 steps @ 720×1280×81f smoke calibration — pipe-clean scope; quality-grade calibration is a knob rerun.) 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Warning
CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.
Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/diffusers/README.md`:
- Around line 127-130: Update the `pre_quant_scale` description in the SVDQuant
documentation to identify it as SmoothQuant-style rather than an AWQ artifact,
while preserving the existing explanation of the quantization recipe.
In `@modelopt/torch/quantization/model_calib.py`:
- Around line 1150-1155: Update the scale calculation around scale_a and
act_amax to create a squeezed zero-activation mask before division, then apply
it unconditionally with torch.where so masked channels receive the neutral scale
of 1. Remove the tensor-value-based Python conditional and ensure zero-over-zero
cases cannot leave NaN values before the existing clamp.
In `@tests/examples/diffusers/test_export_diffusers_hf_ckpt.py`:
- Around line 459-468: Extend the export validation around the existing
weight_scale assertions to load excluded block tensors from tiny_wan22_path for
both experts and compare them with the exported tensors using torch.equal. Cover
every excluded first-3 and last-3 block weight, ensuring the test fails if
smoothing or SVDQuant mutates their full-precision values while preserving the
existing quantized-block checks.
- Line 180: Move import re from the test/function scope at
tests/examples/diffusers/test_export_diffusers_hf_ckpt.py:180-180 to the module
import section. Also move from safetensors import safe_open from
tests/examples/diffusers/test_export_diffusers_hf_ckpt.py:427-427 to module
scope, or explicitly document safetensors as optional and apply an explicit
dependency-based skip; do not leave either import inside a test or function.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 47c54b39-b7bc-4dca-b05b-19e750956610
📒 Files selected for processing (11)
CHANGELOG.rstexamples/diffusers/README.mdexamples/diffusers/quantization/config.pyexamples/diffusers/quantization/models_utils.pyexamples/diffusers/quantization/quantize.pyexamples/diffusers/quantization/quantize_config.pymodelopt/torch/quantization/config.pymodelopt/torch/quantization/model_calib.pytests/_test_utils/torch/diffusers_models.pytests/examples/diffusers/test_export_diffusers_hf_ckpt.pytests/unit/torch/quantization/test_calib.py
| precision. The exclusion is applied **before calibration** so that for SVDQuant | ||
| the excluded blocks' weights stay bit-identical to the original. Every quantized | ||
| linear keeps the full SVDQuant recipe (AWQ `pre_quant_scale` + low-rank | ||
| `svdquant_lora_a/b`). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the pre_quant_scale description.
Line 129 calls pre_quant_scale an AWQ artifact. This PR replaces the AWQ-Lite search with fixed SmoothQuant-style migration. Describe it as a SmoothQuant-style pre_quant_scale.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/diffusers/README.md` around lines 127 - 130, Update the
`pre_quant_scale` description in the SVDQuant documentation to identify it as
SmoothQuant-style rather than an AWQ artifact, while preserving the existing
explanation of the quantization recipe.
| # Some channel could have 0 amax which causes scale_a to overflow. Explicitly mask them out here | ||
| epsilon = 1.0 / (1 << 31) | ||
| if scale_a.min() <= epsilon: | ||
| zero_mask = act_amax <= epsilon | ||
| scale_a[zero_mask] = 1 | ||
| scale_a = scale_a.clamp(min=1e-4, max=1e4) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Mask zero activation channels before scale calculation.
Line 1152 does not detect a zero act_amax when it produces inf. Line 1155 then clamps that channel to 1e4 instead of the intended neutral scale of 1. A 0 / 0 scale can also remain NaN.
Create a squeezed zero mask before the division. Apply it with torch.where unconditionally.
Proposed fix
- scale_a = (weight_scale.pow(1 - alpha) / act_amax.pow(alpha)).squeeze()
+ zero_mask = act_amax.squeeze() <= epsilon
+ safe_act_amax = act_amax.clamp_min(epsilon)
+ scale_a = (weight_scale.pow(1 - alpha) / safe_act_amax.pow(alpha)).squeeze()
...
- if scale_a.min() <= epsilon:
- zero_mask = act_amax <= epsilon
- scale_a[zero_mask] = 1
- scale_a = scale_a.clamp(min=1e-4, max=1e4)
+ scale_a = torch.where(zero_mask, torch.ones_like(scale_a), scale_a)
+ scale_a = scale_a.clamp(min=1e-4, max=1e4)As per coding guidelines, “Avoid tensor-value-based Python branching when it can break CUDA graphs.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modelopt/torch/quantization/model_calib.py` around lines 1150 - 1155, Update
the scale calculation around scale_a and act_amax to create a squeezed
zero-activation mask before division, then apply it unconditionally with
torch.where so masked channels receive the neutral scale of 1. Remove the
tensor-value-based Python conditional and ensure zero-over-zero cases cannot
leave NaN values before the existing clamp.
Source: Coding guidelines
| """transformer_blocks indices referenced by a set of module prefixes.""" | ||
| def _block_indices(prefixes: set[str], block_re: str = r"transformer_blocks\.(\d+)\.") -> set[int]: | ||
| """Block indices referenced by a set of module prefixes.""" | ||
| import re |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Move test imports to module scope.
Both imports can fail after test collection. Move them to the module import section. If safetensors is intentionally optional, add a brief comment and use an explicit optional-dependency skip.
tests/examples/diffusers/test_export_diffusers_hf_ckpt.py#L180-L180: moveimport reto module scope.tests/examples/diffusers/test_export_diffusers_hf_ckpt.py#L427-L427: movefrom safetensors import safe_opento module scope, or document the optional-dependency exception.
As per path instructions, “Imports inside functions or test methods without explicit justification” are IMPORTANT issues.
📍 Affects 1 file
tests/examples/diffusers/test_export_diffusers_hf_ckpt.py#L180-L180(this comment)tests/examples/diffusers/test_export_diffusers_hf_ckpt.py#L427-L427
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/examples/diffusers/test_export_diffusers_hf_ckpt.py` at line 180, Move
import re from the test/function scope at
tests/examples/diffusers/test_export_diffusers_hf_ckpt.py:180-180 to the module
import section. Also move from safetensors import safe_open from
tests/examples/diffusers/test_export_diffusers_hf_ckpt.py:427-427 to module
scope, or explicitly document safetensors as optional and apply an explicit
dependency-based skip; do not leave either import inside a test or function.
Source: Path instructions
| # Recipe: only the middle `blocks` are quantized — first-3/last-3 are | ||
| # excluded, and nothing outside `blocks`. | ||
| weight_scale_prefixes = _module_prefixes(keys, ".weight_scale") | ||
| assert weight_scale_prefixes, f"{expert}: no quantized linears found in export" | ||
| assert all(p.startswith("blocks.") for p in weight_scale_prefixes), ( | ||
| f"{expert}: a non-blocks module was quantized: {weight_scale_prefixes}" | ||
| ) | ||
| assert _block_indices(weight_scale_prefixes, _WAN22_BLOCK_RE) == _WAN22_QUANTIZED_BLOCKS, ( | ||
| f"{expert}: expected only blocks {_WAN22_QUANTIZED_BLOCKS} quantized" | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Compare excluded weights with the source fixture.
These checks prove that excluded blocks have no weight_scale. They do not prove that SVDQuant left their full-precision weights bit-identical.
Load the excluded block tensors from tiny_wan22_path for both experts. Compare each exported tensor with torch.equal. This test must detect an accidental smoothing or SVD mutation of excluded blocks.
As per coding guidelines, “Tests must exercise the behavior they claim to validate.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/examples/diffusers/test_export_diffusers_hf_ckpt.py` around lines 459 -
468, Extend the export validation around the existing weight_scale assertions to
load excluded block tensors from tiny_wan22_path for both experts and compare
them with the exported tensors using torch.equal. Cover every excluded first-3
and last-3 block weight, ensuring the test fails if smoothing or SVDQuant
mutates their full-precision values while preserving the existing
quantized-block checks.
Source: Coding guidelines
There was a problem hiding this comment.
Warning
CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.
Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.
Actionable comments posted: 1
🧹 Nitpick comments (2)
examples/diffusers/quantization/pipeline_manager.py (1)
212-218: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDocument the justified local import.
The local import handles an optional Diffusers API. Add a brief comment before the import that states why it must remain local. The
ImportErrormessage does not satisfy the required source-level justification.Proposed change
def _create_minimax_h3_pipeline(self) -> Any: """Load only MiniMax-H3's prompt-only T2V workflow.""" try: + # ModularPipeline is optional in released Diffusers versions. from diffusers import ComponentsManager, ModularPipelineAs per coding guidelines: “Keep imports at the top of Python source and test files; use local imports only for justified circular dependencies, optional dependencies, or unusually heavy imports, with a brief explanatory comment.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/diffusers/quantization/pipeline_manager.py` around lines 212 - 218, Before the local import of ComponentsManager and ModularPipeline, add a brief comment explaining that it must remain local because these Diffusers APIs are optional and may be unavailable in standard releases. Keep the existing ImportError handling unchanged.Source: Coding guidelines
examples/diffusers/quantization/utils.py (1)
125-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDefine the module public API.
examples/diffusers/quantization/utils.pyhas no__all__declaration. Add one that includesfilter_func_minimax_h3and the other intended public utilities.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/diffusers/quantization/utils.py` around lines 125 - 130, Add a module-level __all__ declaration in utils.py listing filter_func_minimax_h3 and every other intentionally public utility defined by the module, while excluding private helpers and implementation details. Keep the existing utility implementations unchanged.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@modelopt/torch/export/unified_export_hf.py`:
- Around line 1407-1410: Update the pipeline index handling near the
is_diffusers_pipe export logic in modelopt/torch/export/unified_export_hf.py to
delete the alternate index before writing the selected one: remove
model_index.json for modular exports and modular_model_index.json for standard
exports. In tests/unit/torch/export/test_export_diffusers.py lines 175-199, seed
a stale model_index.json and assert that a modular export removes it.
---
Nitpick comments:
In `@examples/diffusers/quantization/pipeline_manager.py`:
- Around line 212-218: Before the local import of ComponentsManager and
ModularPipeline, add a brief comment explaining that it must remain local
because these Diffusers APIs are optional and may be unavailable in standard
releases. Keep the existing ImportError handling unchanged.
In `@examples/diffusers/quantization/utils.py`:
- Around line 125-130: Add a module-level __all__ declaration in utils.py
listing filter_func_minimax_h3 and every other intentionally public utility
defined by the module, while excluding private helpers and implementation
details. Keep the existing utility implementations unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: c7e5216e-dae4-4eb5-bde8-9b23d79528fd
📒 Files selected for processing (8)
examples/diffusers/quantization/calibration.pyexamples/diffusers/quantization/models_utils.pyexamples/diffusers/quantization/pipeline_manager.pyexamples/diffusers/quantization/quantize.pyexamples/diffusers/quantization/utils.pymodelopt/torch/export/diffusers_utils.pymodelopt/torch/export/unified_export_hf.pytests/unit/torch/export/test_export_diffusers.py
| # For pipelines, also save the pipeline index. | ||
| if is_diffusers_pipe: | ||
| model_index_path = export_dir / "model_index.json" | ||
| model_index_filename = "modular_model_index.json" if is_modular_pipe else "model_index.json" | ||
| model_index_path = export_dir / model_index_filename |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Remove the alternate pipeline index during retry.
A modular export can reuse a directory that contains model_index.json from a previous standard export. The code writes modular_model_index.json but leaves the standard index in place. The output then contains incompatible pipeline indexes.
modelopt/torch/export/unified_export_hf.py#L1407-L1410: Remove the alternate index before writing the selected index. Removemodel_index.jsonfor modular exports andmodular_model_index.jsonfor standard exports.tests/unit/torch/export/test_export_diffusers.py#L175-L199: Seed a stalemodel_index.jsonand assert that modular export removes it.
📍 Affects 2 files
modelopt/torch/export/unified_export_hf.py#L1407-L1410(this comment)tests/unit/torch/export/test_export_diffusers.py#L175-L199
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modelopt/torch/export/unified_export_hf.py` around lines 1407 - 1410, Update
the pipeline index handling near the is_diffusers_pipe export logic in
modelopt/torch/export/unified_export_hf.py to delete the alternate index before
writing the selected one: remove model_index.json for modular exports and
modular_model_index.json for standard exports. In
tests/unit/torch/export/test_export_diffusers.py lines 175-199, seed a stale
model_index.json and assert that a modular export removes it.
What does this PR do?
Type of change: New example feature
Add Wan 2.2 T2V A14B NVFP4-SVDQuant HuggingFace checkpoint export to the diffusers quantization example, mirroring the Qwen-Image SVDQuant mechanics (#1706):
--model wan2.2-t2v-14bnow quantizes both experts by default —transformer(high-noise) first, thentransformer_2(low-noise), so the low-noise expert calibrates against the already-quantized high-noise expert, matching deployment.filter_func_wan_video: exclude the first 3 / last 3 of the 40blocks, plus everything outsideblocks) is now applied before calibration via theblock_rangemechanism. This is required for SVDQuant: the algorithm folds AWQ smoothing scales and subtracts the low-rank residual from every enabled linear during calibration, so post-hoc disabling (the previous filter-func-only path) would leave "excluded" weights silently mutated. With this PR they stay bit-identical to the source.--backbone vae) skip the transformer block-range recipe and keep their dedicated Conv3D recipe.svdquant_skip_layersfor Wan: every quantized linear (self-attn, cross-attn incl.to_k/to_v, FFN) carries the full SVDQuant recipe (pre_quant_scale+ rank-rsvdquant_lora_a/b).Usage
python examples/diffusers/quantization/quantize.py \ --model wan2.2-t2v-14b \ --model-dtype BFloat16 --trt-high-precision-dtype BFloat16 \ --format fp4 --quant-algo svdquant --lowrank 32 \ --batch-size 1 --calib-size 16 --n-steps 20 \ --hf-ckpt-dir ./hf_ckptTesting
wan22_14b_nvfp4_svdquantcase intests/examples/diffusers/test_export_diffusers_hf_ckpt.pyasserting per expert (transformerandtransformer_2):NVFP4_SVDquant config +lora_rank; only blocks {3, 4} of the 8-block tiny fixture quantized and nothing outsideblocks;svdquant_lora_a/b+pre_quant_scalepresent on all 10 block linears with rank-consistent shapes; NVFP4weight_scale_2; no live-quantizer keys leaked.num_layers2→8 (the block-range recipe needs ≥ 8 blocks), heads 2→4 (hidden 48, divisible by the NVFP4 block size 16;head_dimstays 12 so the RoPE axis split stays even).--backbone vaefp8/fp4 (verifying the VAE gate), and theqwen_nvfp4_svdquantregression.NVFP4_SVD; blocks 3–36 quantized; excluded blocks and non-block modules bit-identical to the source BF16; full pipeline dirs). Rank-32 in progress; results will be posted on this PR.Before your PR is "Ready for review"
--model wan2.2-t2v-14bpreviously quantized onlytransformerby default; it now quantizes both experts (a single-expert checkpoint of the two-expert pipeline is half-quantized). Pass--backbone transformerto restore the old behavior. The tiny Wan test fixture also grew from 2 to 8 blocks / hidden 24 to 48.CONTRIBUTING.md: N/AAdditional Information
Draft until the real-model rank-32/64/128 smoke exports and verify gates complete.
🤖 Generated with Claude Code
Update: AWQ-Lite search removed from the SVDQuant path
SVDQuant calibration is now SmoothQuant-style fixed migration only (
SVDQuantConfig.alpha, default1.0= migrate outliers fully to the weights; the diffusers example reuses--alpha). The AWQ-Lite search is gone fromsvdquant: two forward-loop passes instead of three, no per-alpha quantized GEMM sweep. (The transient--svdquant-alphaopt-in from the second commit was superseded in the fourth commit.)Why: the search optimized pre-SVD, weight-only output MSE with activations unquantized — a mismatched objective twice over for W4A4 SVDQuant, where the SVD low-rank branch absorbs the migrated outliers and activation flatness is the point of the migration (SVDQuant paper design).
Validation on Wan2.2-T2V-A14B (rank 32, calib 1 x 20 steps @ 720x1280x81f, GB200):
Gate checks per expert:
NVFP4_SVD+lora_rank=32; only blocks 3-36 quantized; 340/340 linears withsvdquant_lora_a/b+pre_quant_scale; excluded region bit-identical in export precision (the Wan-AI repo ships FP32 shards, so excluded tensors are pure BF16 downcasts - 0 value changes). Unit: 16/16 calib/mode tests incl. newtest_svdquant_smoothquant_calibration(asserts 2 forward loops + the SmoothQuant scale formula). Example: Wan + Qwen SVDQuant export tests pass on the new path.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation