Skip to content

[Bugfix] QAD 5090: Torch.compile and other optimizations (15/12) - #1466

Merged
SolitaryThinker merged 4 commits into
mainfrom
pr1225_s15
Jun 23, 2026
Merged

[Bugfix] QAD 5090: Torch.compile and other optimizations (15/12)#1466
SolitaryThinker merged 4 commits into
mainfrom
pr1225_s15

Conversation

@loaydatrain

Copy link
Copy Markdown
Collaborator

Purpose

Continues the QAD 5090 stack (#1225). Inference-side companion to #1463 (FP4
QAT training): wires the FP4 linear path for Wan2.1 1.3B end-to-end under
torch.compile, pops the bf16 weight after FP4 packing, removes x_global_sf, and adds TAEHV scripts

With Sage3, running in 2it/s, 1.78s e2e.

Changes

  • layers/quantization/nvfp4_qat_config.py (+ loader/fsdp_load.py):
    inference path made torch.compile-friendly — precomputed x_global_sf
    (no per-call sync), single-level quant, and the dense bf16 weight is popped
    after FP4 pack (only _fp4_weight + scales survive).
  • attention/backends/sage_attn3.py: removed data-dependent control flow on
    tensor metadata so the backend traces cleanly under torch.compile.
  • examples/inference/optimizations/fp4_linear_wan2_1_1_3b.py and
    FastWan_QAD_TAEHV.py: end-to-end Wan2.1 1.3B inference with the FP4 linear
    path + sage_attn3 + torch.compile; the TAEHV variant swaps in the TAEHV
    decoder.

No new CLI flags; reuses --transformer-quant nvfp4_qat from #1463.

Test results (RTX 5090 / sm_120)

  • ✅ Inference: --transformer-quant nvfp4_qat Wan2.1 1.3B runs end-to-end
    with torch.compile enabled; sample output
    video_samples/raccoon_fp4_linear_taehv_compile.mp4.
  • ✅ Step time: DiT time: 2it/s // e2e time: 1.78s for a 5s 480p video
  • ✅ Memory: dense bf16 weight popping drops DiT resident memory after the
    first forward by TODO MB.

Depends on #1463 (--transformer-quant CLI + nvfp4_qat config). Part of #1225.

@mergify mergify Bot added type: bugfix Bug fix scope: inference Inference pipeline, serving, CLI scope: attention Attention backends (VSA, STA, Flash, etc.) scope: model Model architecture (DiTs, encoders, VAEs) labels Jun 16, 2026
@mergify

mergify Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

Your pull request matches the following merge protections and will not be merged until they are valid.

🔴 PR merge requirements

Waiting for

  • #approved-reviews-by>=1
  • check-success=fastcheck-passed
  • check-success=full-suite-passed
This rule is failing.
  • #approved-reviews-by>=1
  • check-success=fastcheck-passed
  • check-success=full-suite-passed
  • check-success~=pre-commit
  • title~=(?i)^\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\]

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces fast NVFP4 linear inference for Wan2.1 with TAEHV decoding, optimizes attention preprocessing/postprocessing in SageAttn3, and adds support for quantization-aware training (QAD) with a straight-through estimator. Key feedback includes resolving an environment-specific absolute path, rounding instead of truncating during float-to-uint8 conversion, avoiding backslashes for line continuation, and preventing division-by-zero errors when computing scale factors. Additionally, it is recommended to make NVFP4QATTrainConfig customizable to match its inference counterpart and to refactor the FSDP loader to handle mixed quantization methods without returning early.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.


# TAEHV checkpoint for Wan2.1. Clone https://github.com/madebyollin/taehv to get
# ``taew2_1.pth`` (Wan 2.1 / Wan 2.2-14B / Qwen-Image all use this VAE).
DEFAULT_TAEHV_CHECKPOINT = "/root/taehv/taew2_1.pth"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The absolute path /root/taehv/taew2_1.pth is environment-specific and will fail on other systems. It is better to use a relative path or default to a standard location, or allow it to be resolved dynamically.

Suggested change
DEFAULT_TAEHV_CHECKPOINT = "/root/taehv/taew2_1.pth"
DEFAULT_TAEHV_CHECKPOINT = "taew2_1.pth"

decoded = self.model.decode_video(
latents, parallel=True, show_progress_bar=False)
# decoded: [B, T, 3, H, W] in [0, 1]. Take batch 0, vectorize to uint8.
frames = (decoded[0].clamp(0, 1) * 255).to(torch.uint8)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly casting a float tensor in [0, 1] to uint8 after multiplying by 255 performs a floor operation (truncation), which can cause precision loss and slight color shifts. Rounding to the nearest integer before casting is recommended.

Suggested change
frames = (decoded[0].clamp(0, 1) * 255).to(torch.uint8)
frames = (decoded[0].clamp(0, 1) * 255).round().to(torch.uint8)

Comment on lines +222 to +223
taehv = TaehvDecoder(resolve_taehv_checkpoint(args.taehv_checkpoint)) \
if args.taehv else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

PEP 8 discourages the use of backslashes for line continuation. Wrapping the expression in parentheses is the preferred way to break long lines.

Suggested change
taehv = TaehvDecoder(resolve_taehv_checkpoint(args.taehv_checkpoint)) \
if args.taehv else None
taehv = (
TaehvDecoder(resolve_taehv_checkpoint(args.taehv_checkpoint))
if args.taehv
else None
)
References
  1. PEP 8: Avoid using backslashes for line continuation. Use parentheses or standard block structures instead. (link)

fp4_w, fp4_s = flashinfer_mod.nvfp4_quantize(

# Only the reduced scalar needs fp32; avoid a full fp32 copy.
weight_absmax = (weight_local.detach().abs().nan_to_num().amax().to(dtype=torch.float32))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If weight_absmax is 0 (e.g., for zero-initialized or pruned weights), weight_global_sf = (448 * 6) / weight_absmax will result in a division by zero, producing inf or NaN values. Clamping weight_absmax to a small positive epsilon prevents this.

Suggested change
weight_absmax = (weight_local.detach().abs().nan_to_num().amax().to(dtype=torch.float32))
weight_absmax = (weight_local.detach().abs().nan_to_num().amax().to(dtype=torch.float32)).clamp(min=1e-12)

Comment on lines +50 to +78
class NVFP4QATTrainConfig(QuantizationConfig):

def __init__(self) -> None:
super().__init__()

def get_name(self):
return "nvfp4_qat_train"

def get_supported_act_dtypes(self):
return [torch.bfloat16, torch.float16]

@classmethod
def get_min_capability(cls):
return 100

@staticmethod
def get_config_filenames():
return []

@classmethod
def from_config(cls, config: dict[str, Any]) -> "NVFP4QATTrainConfig":
return cls()

def get_quant_method(self, layer: torch.nn.Module, prefix: str):
from fastvideo.layers.linear import LinearBase
fp4_layers = ["ffn.fc_in", "ffn.fc_out", "to_q", "to_k", "to_v", "to_out"]
if isinstance(layer, LinearBase) and any(layer_name in prefix for layer_name in fp4_layers):
return NVFP4QATTrainQuantizeMethod()
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

NVFP4QATTrainConfig hardcodes fp4_layers and does not accept target_layers in its constructor, unlike NVFP4QATConfig. This inconsistency prevents users from customizing target layers during QAT training. Refactoring it to match NVFP4QATConfig ensures consistency and flexibility.

class NVFP4QATTrainConfig(QuantizationConfig):

    def __init__(self, target_layers: tuple[str, ...] | None = None) -> None: 
        super().__init__()
        from fastvideo.layers.quantization.nvfp4_qat_config import DEFAULT_FP4_LAYERS
        self.target_layers = (tuple(target_layers) if target_layers else DEFAULT_FP4_LAYERS)

    def get_name(self) -> str:
        return "nvfp4_qat_train"

    def get_supported_act_dtypes(self) -> list[torch.dtype]:
        return [torch.bfloat16, torch.float16]

    @classmethod
    def get_min_capability(cls) -> int:
        return 100

    @staticmethod
    def get_config_filenames() -> list[str]:
        return []

    @classmethod
    def from_config(cls, config: dict[str, Any]) -> NVFP4QATTrainConfig:
        target_layers = config.get("target_layers")
        if target_layers is not None:
            target_layers = tuple(target_layers)
        return cls(target_layers=target_layers)

    def get_quant_method(self, layer: torch.nn.Module, prefix: str):
        from fastvideo.layers.linear import LinearBase
        if isinstance(layer, LinearBase) and any(name in prefix for name in self.target_layers):
            return NVFP4QATTrainQuantizeMethod()
        return None

Comment on lines 57 to 69
for mod in model.modules():
if isinstance(getattr(mod, "quant_method", None),
NVFP4QuantizeMethod):
quant_method = getattr(mod, "quant_method", None)
# Wan-style NVFP4 linear: prequantize and pop dense weights.
if isinstance(quant_method, NVFP4QATQuantizeMethod):
logger.info(
"Converting loaded model weights for NVFP4 (QAT) linear layers")
convert_model_to_fp4(model)
return
# LTX-2 NVFP4 linear.
if isinstance(quant_method, NVFP4QuantizeMethod):
logger.info("Converting loaded model weights for NVFP4 linear layers")
convert_model_to_nvfp4(model)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current loop returns immediately after finding the first quantization method. If a model contains mixed quantization (e.g., both NVFP4QATQuantizeMethod and NVFP4QuantizeMethod), only one of them will be converted, and the other will be silently skipped. Tracking which conversions have run instead of returning early makes the loader much more robust.

Suggested change
for mod in model.modules():
if isinstance(getattr(mod, "quant_method", None),
NVFP4QuantizeMethod):
quant_method = getattr(mod, "quant_method", None)
# Wan-style NVFP4 linear: prequantize and pop dense weights.
if isinstance(quant_method, NVFP4QATQuantizeMethod):
logger.info(
"Converting loaded model weights for NVFP4 (QAT) linear layers")
convert_model_to_fp4(model)
return
# LTX-2 NVFP4 linear.
if isinstance(quant_method, NVFP4QuantizeMethod):
logger.info("Converting loaded model weights for NVFP4 linear layers")
convert_model_to_nvfp4(model)
return
converted_qat = False
converted_nvfp4 = False
for mod in model.modules():
quant_method = getattr(mod, "quant_method", None)
# Wan-style NVFP4 linear: prequantize and pop dense weights.
if isinstance(quant_method, NVFP4QATQuantizeMethod) and not converted_qat:
logger.info(
"Converting loaded model weights for NVFP4 (QAT) linear layers")
convert_model_to_fp4(model)
converted_qat = True
# LTX-2 NVFP4 linear.
elif isinstance(quant_method, NVFP4QuantizeMethod) and not converted_nvfp4:
logger.info("Converting loaded model weights for NVFP4 linear layers")
convert_model_to_nvfp4(model)
converted_nvfp4 = True

@mergify

mergify Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase:

git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts label Jun 17, 2026

@SolitaryThinker SolitaryThinker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @loaydatrain — automated review from Gob, one of @SolitaryThinker's AI reviewers. Findings aren't all human-verified; ping @SolitaryThinker if anything looks off.

Scope: training/quant + perf (the FP4-linear torch.compile enablement + optimizations). Open PR, mergeable=CONFLICTING / mergeStateStatus=DIRTY + needs-rebase. Verdict: COMMENT — the compile fix is correct and the core logic lands on files main hasn't touched, but rebase first and confirm two things below before merge.

Major

  • Needs rebase — the branch is stacked on the pre-merge #1463 commit (09f9b56). #1463 already squash-merged to main (6da206e1), and that base is also ~13k lines behind current main (the big deletion count GitHub shows is this artifact, not intended changes). The actual textual conflict is just fastvideo/models/loader/fsdp_load.py and fastvideo/fastvideo_args.py, and both PR versions are semantically equivalent to what #1463 already put on main (variable name / branch order / comment wording in fsdp_load.py; pure formatting reflow of the existing --transformer-quant arg in fastvideo_args.py). Rebase onto main and drop those two near-dup hunks — the substantive logic (sage_attn3, attention/layer.py, nvfp4_qat_config.py) is conflict-free.

  • Activation-quant numerics change with no quality evidence — nvfp4_qat_config.py:84,106. apply() switched the input scale from per-call dynamic (448*6)/x.absmax() to static x_global_sf = torch.tensor(1.0). The comment ("matches the production path") is correct — the LTX-2 NVFP4 method uses the same static 1.0 (nvfp4_config.py:270,338), and this is exactly what removes the per-call .max() sync blocking CUDA-graph capture. But it's still a real FP4 activation-quant change and the body ships no SSIM/quality number (the memory claim is a literal "TODO MB"). Per the repo's model-PR convention, please post an SSIM delta vs dense bf16. Also note it diverges from the training STE, which still quantizes activations with the dynamic scale (fp4linear.py:50) — confirm the train-vs-inference input-scale skew is intended for QAT.

  • Latent layout-contract break in the sage3 hook refactor. The old impl transposed q/k/v inside forward() after the chunk; the new preprocess_qkv (sage_attn3.py:58-70) does permute(0,2,1,3) on the stacked tensor, which layer.py:134 / ltx2.py:1303 call before the replicated-QKV concat (dim=1) and the post-attention replicated split. After the permute dim-1 is heads, not sequence, so the replicated-QKV concat/split no longer mean what they did. Safe for this PR's Wan target (replicated_q is never passed there, and it's never populated anywhere in-tree today), but LTX-2's non-VSA path carries the replicated-QKV plumbing and can select SAGE_ATTN_THREE, so the contract is silently wrong for any future caller. Either preserve the per-tensor transpose for the replicated path, or assert replicated_q is None in the sage3 path so a break is loud, not silent.

Minor

  • No div-by-zero guard — nvfp4_qat_config.py:201-202: weight_global_sf = (448*6) / weight_absmax divides by an unclamped reduced absmax; an all-zero tile gives inf/nan. A .clamp_min(eps) is cheap insurance.
  • Hardcoded path — FastWan_QAD_TAEHV.py:53: DEFAULT_TAEHV_CHECKPOINT = "/root/taehv/taew2_1.pth" is environment-specific; default it to a repo-relative / env-driven value so the example runs out of the box.

Nits

  • TAEHV docstring (FastWan_QAD_TAEHV.py:3,21-25) references a sibling fp4_linear_wan2_1_1_3b.py and a python fp4_linear_taehv_wan2_1_1_3b.py usage line that don't match the actual filename (FastWan_QAD_TAEHV.py).
  • PR body "drops DiT resident memory ... by TODO MB" — fill in the number (it's the headline benefit of the weight-pop).

Verified positives: reusing the register_fake-backed fastvideo_fp4:: custom ops instead of the old @torch.compile + raw flashinfer is the correct graph-break fix (no silently-swallowed breaks); the weight-pop memory logic is sound (persistent=False buffers + _parameters.pop + grad=None + gc/empty_cache); the FASTVIDEO_DISABLE_ATTENTION_COMPILE env-gate defaults to the historical eager behavior; title matches the merge regex; pre-commit is green. CI microscope failures are uniform exit 124 timeouts consistent with the stale/conflicting base — rebase and re-run.

— Gob (@SolitaryThinker's AI reviewer).

loaydatrain and others added 4 commits June 23, 2026 06:44
…opping+torch compile+single level quant changes to nvfp4_qat_config
…LE_ATTENTION_COMPILE

DistributedAttention.forward (and the VSA subclass) are hard-decorated with
@torch.compiler.disable, which keeps attention out of the surrounding
torch.compile graph unconditionally. That blocks the inference compile path
even after the FP4 linear and SageAttention3 graph-break fixes land, since
the attention forward itself can never be traced.

Make the disable conditional on FASTVIDEO_DISABLE_ATTENTION_COMPILE:
- unset / "1" / "true" (default): keep torch.compiler.disable — current behavior
- "0" / "false" / "no" / "off": drop it so attention can fold into the graph

The env var is read at import time (decorators are applied at class
definition), which is the right granularity for the multiproc spawn path:
each worker re-imports and inherits the parent's env.

Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
… build

build.sh auto-detected Blackwell (sm_120) and exported TORCH_CUDA_ARCH_LIST=12.0 without the arch-conditional 'a' suffix. CMake's AUTO gate for the attn_qat_infer (modified SageAttention3 FP4) kernels only matches 12.0a/120a/sm_120a, so fp4attn_cuda/fp4quant_cuda were silently skipped and the ATTN_QAT_INFER backend fell back to Flash Attention at runtime. Exporting the env var also bypassed CMake's local-GPU fallback that would otherwise have enabled them.

Mirror the existing 9.0 -> 9.0a Hopper handling for 12.0 -> 12.0a.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mergify mergify Bot added scope: kernel CUDA kernels, fastvideo-kernel and removed needs-rebase PR has merge conflicts labels Jun 23, 2026
@SolitaryThinker

Copy link
Copy Markdown
Collaborator

/merge

@github-actions github-actions Bot added the ready PR is ready to merge label Jun 23, 2026
@SolitaryThinker
SolitaryThinker merged commit b2ade71 into main Jun 23, 2026
9 of 13 checks passed
@SolitaryThinker
SolitaryThinker deleted the pr1225_s15 branch June 23, 2026 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready PR is ready to merge scope: attention Attention backends (VSA, STA, Flash, etc.) scope: inference Inference pipeline, serving, CLI scope: kernel CUDA kernels, fastvideo-kernel scope: model Model architecture (DiTs, encoders, VAEs) type: bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants