Skip to content

Diffusion: auto TE/VAE precision, video DiT quant correctness, and HunyuanVideo-1.5 attention speedup#7021

Open
danielhanchen wants to merge 55 commits into
image-generationfrom
video-diffusion-improvements
Open

Diffusion: auto TE/VAE precision, video DiT quant correctness, and HunyuanVideo-1.5 attention speedup#7021
danielhanchen wants to merge 55 commits into
image-generationfrom
video-diffusion-improvements

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Jul 9, 2026

Copy link
Copy Markdown
Member

A batch of accuracy-gated precision and speed improvements for the Studio diffusion backend, split out of #6763 for focused review. Every change here builds on the diffusion loader / quant / attention code in #6763, so this PR is stacked on the image-generation branch. All defaults were chosen from B200 accuracy sweeps; anything that could regress an output is gated or opt-in, and every other model family is left on its existing path.

Auto text-encoder and VAE precision

  • Auto-select text-encoder quantization by GPU and family (default on). The text encoder is picked per device and family from an accuracy-validated ladder instead of always running dense, so it shrinks where that is free and stays dense where it is not.
  • Auto-quantize the VAE decoder to fp8 (default on), then gate it two ways: by decoded-image accuracy (auto layerwise fp8, fp8_dynamic stays opt-in) and by size, so only the large video VAEs are quantized and the tiny image VAEs are left dense where the saving is not worth the risk.
  • Disable NVFP4 in the Blackwell auto ladder (explicit opt-in only) until its accuracy is validated, so the default path never silently drops to it.

Video DiT quantization correctness

  • Fix black frames on Wan video by denying fp8 DiT auto-quant and falling back to int8, then extend the fp8 black-frame deny to HunyuanVideo-1.5 while keeping LTX-2 on fp8 (it is clean). Each deny is derived from a per-family ablation, not a blanket rule.
  • Restore fp8 DiT quant for Wan via a per-family embedder exclude: the black frames trace to a specific embedder submodule, so excluding just that module brings fp8 back for the rest of the Wan DiT.
  • Run the video DiT dense+compile when it already fits resident instead of falling back to a slower int8 quant. On an fp8-capable data-center GPU an AUTO request for an fp8-denied family (HunyuanVideo-1.5) was quantizing to int8, which measured ~7% slower than dense+compile (268.3 vs 250.5 ms/forward) and less accurate (LPIPS 0.085 vs 0.037) for a weight saving it did not need. It now stays dense+compile there and quantizes only when memory-constrained.

HunyuanVideo-1.5 attention speedup

Hunyuan's DiT runs a joint [video; text] self-attention and, on every block and step, builds a dense [B,1,N,N] boolean mask so the video never attends to padded text. A dense bool attn_mask disables every fused SDPA kernel (flash rejects it; cuDNN and memory-efficient fall back), so the attention runs the slow math path: at 121 frames / 480p (N about 50k) one attention call is ~421ms with the mask vs ~19ms with attn_mask=None. The text is ~99.5% padding (a t2v prompt fills ~9 of ~1985 slots).

install_hunyuan_attention_trim adds an eager forward pre-hook that drops the all-zero image stream (t2v) and trims the mllm/byt5 text streams to their globally-valid columns, plus a null-mask attention processor that runs attn_mask=None once no partially-padded column remains (the batch-1 / per-guidance-branch case) and otherwise delegates to the stock dense-mask processor. The model already zeroes and masks the padded text and discards its attention output (only the video split feeds proj_out), so removing it is exact for the video; the only numeric change is the SDPA kernel (masked fallback to fused).

Measured on a B200: 23.3s to 1.3s per DiT forward at 121 frames (~18x with regional compile, 0 graph breaks); per-forward cosine 0.99998 vs stock; equal distance to an fp32 reference (LPIPS fp32-vs-stock 0.292, fp32-vs-trim 0.307), so it is not less accurate than the current bf16 default. Wired auto-on for HunyuanVideo-1.5 only (before the attention backend is set, so the requested kernel pins onto the new processors); a no-op for every other family and reversible (stock dense-mask path on any anomaly).

Benchmark tooling

  • A video speed/memory lever benchmark plus a DiT mode for the quant bench, made MoE-aware and extended with the Wan A14B and Hunyuan-720p families, so the precision and speed choices above are reproducible per family.

Housekeeping

  • Key the image-gen LoRA rows by index so typing a repo id keeps input focus (a UI fix on the Images page).
  • Add the missing AGPL-3.0 SPDX header to the krea2 / lora / controlnet sources.

Tests and validation

Hermetic CPU tests in studio/backend/tests/test_diffusion_attention.py cover the attention trim helper, the null-mask processor gating, idempotency, the t2v image-empty / i2v-keep paths, mixed-batch fallback, and the positional-call and empty-prompt guards. TE and VAE auto-precision and the black-frame denies have their own tests alongside the existing diffusion suites. Diagnostic and validation scripts under scripts/ reproduce the findings, including sdpa_mask_backend_probe.py (the dense-mask kernel tax), hunyuan_attn_diag.py (token breakdown), hunyuan_trim_validate.py (per-forward speed and accuracy), hunyuan_trim_e2e.py (end-to-end LPIPS and mean luma), and hunyuan_trim_fp32ref.py (accuracy vs fp32 ground truth).

Stacked on #6763.

danielhanchen and others added 16 commits July 8, 2026 07:19
The LoRA row div was keyed on sel.id, the editable repo-id field. Typing the
first character changed the key from the fallback index to that character, which
remounted the row and dropped focus on the input, so only one character was ever
captured and manual entry of a repo id was impossible (paste still worked). The
list is index-addressed (every mutation uses j === i) and rows are removed
explicitly via the delete button, so the index is the stable key, matching the
reference-image rows above.
The auto ladder still listed nvfp4 in the Blackwell tier even though the comment
above it says nvfp4 must never be the auto pick for diffusion: at the DiT's real
shapes it is slower (0.81x end-to-end on Z-Image 1024px) and less accurate (LPIPS
0.166 vs fp8's 0.044) than fp8. It was unreachable in practice (fp8 precedes it
and the only fp8-denied families also deny nvfp4), but the entry contradicted the
stated intent and left a latent path to the worse scheme. Drop nvfp4 from the
ladder so Blackwell auto is fp8 -> mxfp8 -> int8, keeping the previous line
commented with a TODO to restore it once the FP4 GEMM wins at these shapes. nvfp4
stays fully available as an explicit transformer_quant="nvfp4" request.
The transformer already defaults to auto-quant (fp8/int8); the companion text
encoder was opt-in and stayed dense bf16 unless a scheme was named, even though it
is often the largest resident component. Add an auto policy mirroring the
transformer's ladder: select_te_quant_scheme walks a per-capability ladder
(data-center fp8-GEMM: fp8_dynamic -> int8 -> layerwise fp8; Ampere: int8 -> fp8),
reorders int8 first on consumer GDDR parts, falls to layerwise fp8 under group
offload (the only offload-safe cast), only picks int8 for a family with a measured
keep-bf16 schedule, honors a per-family deny list, and smoke-probes the torchao
kernel so a missing build degrades gracefully. The image + video loaders now map an
unset text_encoder_quant to auto (explicit none/off stays dense; a named scheme is
forced), so the shipped default quantizes the encoder to the fastest accurate
scheme for the GPU. Records the engaged scheme in the image resolved-record too.
Verified on a B200: auto -> fp8_dynamic, offload -> layerwise fp8.
The transformer and text encoder auto-quantize; the VAE stayed dense. VAEs are
convolutional, so torchao int8 (Linear/2D-only) does not apply, but
Float8DynamicActivationFloat8WeightConfig quantizes Conv2d/Conv3d weights with
PerTensor granularity (auto-skipping convs whose channels are not a multiple of 16,
so the 3-channel RGB head stays dense). New diffusion_vae_quant.py offers two
schemes: fp8_dynamic (torchao conv compute fp8, cc>=8.9, resident) and fp8
(diffusers layerwise storage cast, any conv, survives offload); no int8 (no Conv3d
int8 kernel). select_vae_quant_scheme walks (fp8_dynamic, fp8) with a live conv
smoke probe, an offload gate, a per-family deny list, and a force_fp32 gate; the
image + video loaders map unset vae_quant to auto, skip the vae_force_fp32 Wan
families, and record the engaged scheme. Guards _align_vae_dtype to skip the
img2img/inpaint re-cast when the VAE is quantized (its fp8 tensor subclasses reject
.to(dtype=)). Verified on a B200: %16 Conv2d/Conv3d/Linear -> Float8Tensor, conv_out
dense, forward runs.
…_dynamic opt-in)

A B200 decoded-image LPIPS/SSIM sweep vs the dense bf16 VAE (new
scripts/quant_accuracy_sweep.py) settles the two VAE schemes:

- Layerwise fp8 (storage-only) holds across families (SSIM >= 0.977 on all but
  SDXL), so auto now engages layerwise fp8 ONLY. For a VAE decode (a few percent
  of end to end) fp8_dynamic's fp8-matmul speedup over storage fp8 is negligible,
  so auto never takes the accuracy risk.
- fp8_dynamic (torchao PerTensor conv compute) is in-bar on only FLUX.2 and
  Hunyuan and out-of-bar or catastrophic elsewhere (Qwen-Image SSIM 0.46), so it
  is now an explicit opt-in, re-gated by a per-family deny list derived from the
  sweep. SDXL denies both schemes (its small VAE stays dense).

Also fixes a real decode-time crash: torchao 0.17's fp8 conv kernel rejects
pointwise (1x1 / 1x1x1) convs ("Activation and filter channels must match"), so
an explicit fp8_dynamic request cast fine then threw at the first decode on most
families. The conv filter now keeps 1x1 convs dense, the smoke probe uses a
spatial 3x3 conv (so it exercises the path that actually runs), and an explicit
fp8_dynamic request runs that probe before casting.

Tests updated for the fp8-only auto ladder, the 1x1 exclusion, the explicit
probe gate, and the shipped deny list.
Five diffusion source and test files landed without the standard two-line SPDX
header the rest of studio/backend carries. Prepend it (matching the sibling
convention) so the whole backend is uniformly licensed. Header-only, no code
change.
… image VAEs

A B200 speed/memory sweep (new scripts/quant_speedmem_bench.py) shows the VAE quant
win is a video story. Image AutoencoderKLs are ~0.15-0.26 GB, so fp8 saves ~0.1 GB
and only slows their tiny decode (+6-16%); the video Conv3d VAEs are ~2.5 GB and
halve to ~1.2 GB at ~2% decode cost. So VAE auto now only engages above a ~1 GB size
floor: small image VAEs stay dense (faster decode, no quant quality risk), video VAEs
still quantize. An explicit fp8 / fp8_dynamic request skips the gate (opted in).

The same sweep confirmed the text-encoder default is already right: fp8_dynamic is
E2E-neutral (denoise per-step unchanged; +2% one-time encode) and, by hidden-state
cosine vs bf16, marginally more accurate than layerwise fp8 -- so that default is left
as is. Tests cover the gate (small skipped, large quantized, explicit bypasses).
The dense video default engages transformer auto-quant, and on Blackwell the
auto ladder leads with fp8. On the Wan DiT the production per-row fp8 path
(torch._scaled_mm) renders every frame black (mean luma 0.0 at 512x320 and
704x480, LPIPS ~0.80 vs bf16): Wan's activation outliers exceed per-row fp8's
range, the same failure already denied for qwen-image. First-Block-Cache then
over-caches the degenerate activations (per-step collapses to ~10ms),
compounding it.

Add the Wan families (wan2.2-ti2v-5b, wan2.2-t2v-a14b, same WanTransformer3DModel)
to _FAMILY_SCHEME_DENY for fp8/mxfp8/nvfp4 so auto falls through to int8, which is
clean on Wan (per-token, outlier-robust), saves the same weight memory on the DiT,
and lets First-Block-Cache engage normally instead of over-caching. mxfp8/nvfp4 are
denied alongside fp8 conservatively so auto lands on the battle-tested int8; they
can be re-enabled per family once validated in-bar, like the nvfp4 auto-ladder TODO.

Validated on B200: the shipped video default now selects int8 for the Wan DiT and
renders clean frames (mean 172.6) at 15.6 GB resident (down from 24.2 GB dense),
with First-Block-Cache engaged. Adds two deny tests; 48/48 transformer-quant tests pass.
video_speedmem_bench.py drives the real video-loader lever functions
(quantize_transformer / quantize_text_encoders / quantize_vae / apply_speed_optims
/ apply_attention_backend / apply_step_cache) with the loader's own defaults, so each
measured config reflects a real load. It decomposes the video speed/memory stack
(compile, cuDNN attention, First-Block-Cache, DiT/TE/VAE quant) with per-step latency,
peak resident GB, and per-frame LPIPS vs a bit-exact reference. This is the harness that
surfaced and validated the Wan fp8 black-frame fix.

quant_speedmem_bench.py gains the DiT-quant mode (dense vs fp8/int8/mxfp8 speed, peak
memory, and LPIPS vs the dense render) plus the shared LPIPS(AlexNet) helper.
Measured the fp8 DiT auto-quant path across the remaining dense-pipeline video families on
B200 (production torch._scaled_mm per-row fp8, no MSLK):
  - HunyuanVideo-1.5 (480p + 720p repacks): every frame black (mean luma 0.0, LPIPS 0.82);
    int8 is clean (mean 102.7 vs dense 99.9). Same failure as Wan / qwen-image.
  - LTX-2: fp8 renders clean (mean 153.7, matches int8's 157.7) -- NOT a black-frame family.

So deny fp8/mxfp8/nvfp4 for hunyuanvideo-1.5 and hunyuanvideo-1.5-720p (fall to int8), and
deliberately leave LTX-2 on fp8. The deny stays measured per family, not a blanket video rule:
a blanket deny would have wrongly forced LTX-2 off fp8. Adds a Hunyuan deny test that also
asserts LTX-2 keeps fp8; 49/49 transformer-quant tests pass.

video_speedmem_bench.py gains guidance_via_guider support (HunyuanVideo-1.5 sets CFG on a
guider component and its __call__ takes no guidance_scale / callback_on_step_end), so the
harness can drive Hunyuan the same way the loader does.
The Wan fp8 black frame was root-caused (scripts/fp8_layer_ablation.py,
measured on B200 with the production torch._scaled_mm path): per-row fp8
scales each activation row by row_amax/448, and the text prompt is padded to
512 tokens (~all padding for a short prompt), so condition_embedder's text
embedder divides a zero padding row by a zero scale, which infs and renders
every frame black. That embedder's bias makes every downstream row non-zero,
so the whole 30-block attn1/attn2/ffn stack is fp8-clean (fp8-except-
condition_embedder measured cosine 0.9998 vs bf16, 0 non-finite; fp8-
everywhere is 100% non-finite).

So the blanket fp8 deny was heavier than needed for Wan. Remove fp8 from the
Wan deny and keep only condition_embedder in bf16 via a new
_FP8_FAMILY_EXCLUDE_NAME_TOKENS; auto now restores fp8 (the Blackwell ladder
head) for Wan2.2-TI2V-5B and -T2V-A14B (shared DiT class and padded-text
conditioning). Full-generation check (512x320, 25 frames, 30 steps, cache on
and off): mixed-fp8 is non-black (mean luma 182.6 vs dense 181.2), more
accurate than int8 (LPIPS 0.129 vs 0.180 no-cache, 0.224 vs 0.251 with
FBCache), faster (49.9 vs 64.6 ms/step; int8 was a per-step regression vs the
59.8 ms/step dense), at the same memory (19.34 GB, both -20% vs dense).

HunyuanVideo-1.5 keeps the fp8 deny: its MMDiT masks the padding text tokens
to zero inside every block, so the per-block context stream (add_*_proj /
to_add_out / ff_context) regenerates zero rows layer after layer (fp8 on only
the main blocks is 100% non-finite) so no small exclude set exists and int8
stays. mxfp8 / nvfp4 remain denied for Wan (same per-row scaled_mm family, not
separately validated).

exclude_tokens_for_scheme now takes an optional family, threaded through the
runtime quantiser and the offline prequant builder + validator so offline ==
runtime (a stale Wan fp8 checkpoint baked without the exclude is rejected and
re-quantised rather than loaded). Adds scripts/fp8_layer_ablation.py (the
per-layer ablation probe) and a mean-luma black-frame metric plus mixed-fp8
vs int8 configs to the video bench.
…ilies

Adds wan2.2-t2v-a14b and hunyuanvideo-1.5-720p to the bench family table and makes _apply_levers
quantize / compile BOTH experts of a dual-expert MoE (Wan2.2-A14B) via a _SecondExpertView proxy
that mirrors the loader's _SecondDiTView, so A14B latency and accuracy are measured on the real
two-DiT path instead of only the first expert.

Used to validate that every video family is on the fastest DiT quant scheme that is not less
accurate than its alternative (B200, 512x320, 25 frames, 30 steps, no cache, LPIPS vs dense bf16):
- Wan2.2-TI2V-5B  fp8 49.9 ms/step vs int8 64.6 vs dense 59.8; LPIPS 0.129 vs 0.180
- Wan2.2-T2V-A14B fp8 195.8 ms/step vs int8 193.5 vs dense 369.1; LPIPS 0.288 vs 0.349 (both experts)
- LTX-2 / LTX-2.3  fp8 133.5 ms/step vs int8 138.4 vs dense 204.7; LPIPS 0.026 vs 0.027
- HunyuanVideo-1.5 fp8 is black (denied, not localizable); int8 21.2 s vs dense+compile 15.4 s -- a
  memory saving at a speed cost, so int8 stays only because fp8 is impossible there.

fp8 is faster than dense AND more accurate than int8 on all three Wan/LTX families (the two Wan ones
via the condition_embedder exclude; LTX-2 needs none -- its conditioning has no zero-amax padding
rows). The auto-ladder + deny + exclude already select exactly these schemes, so no scheme-selection
change was needed; this commit is the bench faithfulness improvement that let the campaign confirm it.
…lback

Root-caused "HunyuanVideo-1.5 int8 is slower than dense" with a per-forward profiler
(scripts/hunyuan_int8_profile.py, dynamo-reset, back-to-back on a clean B200): int8 compiles
cleanly (0 recompiles, 0 graph breaks, steady 268.3 ms/forward) and is only ~7% slower than dense
+ regional compile (250.5 ms/forward), not the 38% a contended-GPU bench run suggested. int8 is
also less accurate (LPIPS 0.085 vs dense+compile 0.037). So for a family where fp8 is denied
(Hunyuan black-frames on per-row fp8), int8 is a MEMORY lever, not a speed win, yet the auto-quant
default quantised it even when the dense DiT already fit resident.

Fix: is_int8_memory_fallback(target, family) is True only when AUTO quant lands on int8 as a
denied/black-frame fallback on a data-center, fp8-capable GPU (fp8 would be the arch pick but is
denied for the family). The video loader now skips the auto-quant and runs dense+compile when that
holds AND the bf16 memory plan already fits resident (offload_policy == none), so there is no new
OOM risk. Scoped tightly: only an AUTO request (explicit int8/fp8 honored), only int8-fallback
families (Wan / LTX resolve to fp8 -> keep quantising), only data-center fp8-capable parts (consumer
GPUs and pre-Ada, where int8 is a genuine accelerator, keep int8), and only when dense provably
fits; a memory-constrained plan still quantises. Result: Hunyuan on a resident-fit B200 now runs
faster AND more accurate, quantising only when memory is the constraint.

Also resets dynamo per config in the video bench (so compiled graphs cannot leak across configs in
one process) and adds the per-forward profiler used for the diagnosis.
HunyuanVideo-1.5's DiT runs a joint [video; text] self-attention and, on every
block and step, builds a dense [B,1,N,N] boolean mask so the video never attends
to the padded text. A dense bool attn_mask disables every fused SDPA kernel
(flash rejects it; cuDNN and memory-efficient fall back), so the attention runs
the slow math-style path: at the production shape (121 frames, 480p, N about 50k)
one attention call is ~421ms with the mask vs ~19ms with attn_mask=None. The text
is ~99.5% padding (a t2v prompt fills ~9 of ~1985 slots), so nearly all of that
cost is spent masking padding.

install_hunyuan_attention_trim installs an eager forward pre-hook that drops the
all-zero image stream (t2v) and trims the mllm/byt5 text streams to their
globally-valid columns, plus a null-mask attention processor that runs
attn_mask=None once no partially-padded column remains (the batch-1 /
per-guidance-branch case) and otherwise delegates to the stock dense-mask
processor. The model already zeroes and masks the padded text and discards its
attention output (only the video split feeds proj_out), so removing it is exact
for the video; the only numeric change is the SDPA kernel (masked fallback to
fused). Measured on a B200: 23.3s to 1.3s per DiT forward at 121 frames (~18x with
regional compile, 0 graph breaks); per-forward cosine 0.99998 vs stock; equal
distance to an fp32 reference (LPIPS fp32-vs-stock 0.292, fp32-vs-trim 0.307), so
it is not less accurate than the current bf16 default.

Wired auto-on for HunyuanVideo-1.5 in the video loader, before the attention
backend set so the requested kernel pins onto the new processors; a no-op for
every other family and reversible (stock dense-mask path on any anomaly). Adds
hermetic tests and the diagnostic/validation scripts.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5928064a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


# ── HunyuanVideo-1.5 padded-text attention trim ─────────────────────────────────────
# _trim_stream / _hunyuan_trim_pre_hook use real torch tensor ops, so these run on CPU torch.
import torch # noqa: E402

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate torch-dependent tests so collection still works

In environments that run these hermetic attention tests without torch installed (the module docstring explicitly says no torch/diffusers are needed), this top-level import aborts collection before pytest can run or skip anything, so all of the existing attention-backend tests fail to collect. Please use pytest.importorskip("torch") for just the new Hunyuan trim tests or move the import behind a skip fixture so the non-torch tests remain runnable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. Moved the 15 torch-dependent trim tests into a new test_diffusion_attention_trim.py (which declares the torch dependency), so test_diffusion_attention.py stays collectable and runnable without torch as its docstring promises. Fixed in a4a03fe.

@danielhanchen danielhanchen changed the title Video diffusion improvements Diffusion: auto TE/VAE precision, video DiT quant correctness, and HunyuanVideo-1.5 attention speedup Jul 9, 2026
…on tests torch-free)

test_diffusion_attention.py documents itself as hermetic with no torch/diffusers
needed, but the HunyuanVideo trim tests added a module-level 'import torch' that
aborted collection of the whole file when torch is absent. Move those 15 tests to
test_diffusion_attention_trim.py (which declares the torch dependency) so the
attention-backend policy tests stay collectable and runnable without torch.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4a03feca8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

memory_mode = request.memory_mode,
speed_mode = request.speed_mode,
text_encoder_quant = request.text_encoder_quant,
vae_quant = request.vae_quant,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add vae_quant to the native engine load interface

When the image load is routed to the native sd.cpp engine (CPU/no usable GPU, MPS-enabled native, or a forced native engine), this new keyword is still passed to engine.begin_load, but SdCppDiffusionBackend.begin_load does not accept vae_quant (it only has the other diffusers-only knobs). That raises TypeError: unexpected keyword argument 'vae_quant' before any native GGUF load can start, so native image loading is broken even when the request leaves vae_quant unset. Please add the ignored parameter to the native backend signature, as was done for text_encoder_quant.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 4fd93b2: the native SdCppDiffusionBackend.begin_load now accepts (and ignores) vae_quant like the other diffusers-only knobs. The image route passes vae_quant=request.vae_quant to both engines uniformly, so without this a native (CPU-only / MPS / forced-native) image GGUF load raised TypeError on every request. Added test_begin_load_accepts_diffusers_only_vae_quant.

The image load route calls engine.begin_load(..., vae_quant=request.vae_quant, ...)
uniformly for both engines, but the native SdCppDiffusionBackend.begin_load accepted
every other diffusers-only knob except vae_quant and had no **kwargs, so a native
(CPU-only / MPS / forced-native) image GGUF load raised TypeError on every request
(vae_quant is always passed, defaulting to None). Accept and ignore it like the other
diffusers-only knobs; sd.cpp has no torchao VAE quant.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4fd93b2c7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1225 to +1229
text_encoder_quant = TE_QUANT_AUTO
# vae_quant tri-state, same contract: UNSET -> auto (fp8_dynamic conv compute on resident
# fp8-GEMM silicon that passes the conv probe, else layerwise fp8, else dense); none/off ->
# dense; an explicit scheme forces it.
if vae_quant is None or str(vae_quant).strip() == "":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor image speed_mode=off before auto-quantizing companions

When an image load sets speed_mode="off" (the UI labels this bit-exact) and omits the companion quant fields, this still rewrites the unset text encoder request to auto and the following VAE default does the same. On CUDA/bf16 systems where the auto selector succeeds, quantize_text_encoders/quantize_vae later mutate those components even though the transformer path has a separate speed_off guard, so an explicit bit-exact/off load can still produce quantized, non-dense outputs. Please keep companion quant disabled under explicit speed off unless the user explicitly requested a scheme.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in be04ba0. An explicit Speed=off now pins the companions dense: the unset TE/VAE default resolves to off (not auto) when speed_mode is explicitly off, mirroring the transformer_quant suppression right below it. An unset speed still auto-quantises, and an explicit companion scheme still forces it. Test: test_speed_off_load_suppresses_auto_companion_quant.

Comment thread studio/backend/core/inference/video.py Outdated
Comment on lines +928 to +932
text_encoder_quant = TE_QUANT_AUTO
# vae_quant tri-state, same contract. The vae_force_fp32 families (Wan) keep the VAE dense
# regardless (quantize_vae's force_fp32 gate), so auto is safe as the shipped default.
if vae_quant is None or str(vae_quant).strip() == "":
vae_quant = VAE_QUANT_AUTO

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor video speed_mode=off before auto-quantizing companions

For video loads with speed_mode="off" and no explicit companion quant request, these defaults still turn both text-encoder and VAE quantization on via auto. The transformer auto default is suppressed for speed-off later in this loader, but the companion defaults are not, so Hunyuan/LTX-style loads can still quantize resident encoders/VAEs while the user asked for the dense bit-exact/off path. Please gate these auto defaults the same way, while still honoring explicit text_encoder_quant/vae_quant requests.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in be04ba0, same as the image backend: speed_off (already computed here for the DiT) now also gates the unset TE/VAE default to off. Test: test_video_speed_off_suppresses_auto_companion_quant.

Comment thread studio/backend/core/inference/video.py Outdated
# attention so it runs the fused (cuDNN/flash) SDPA kernel instead of the dense-mask
# fallback (~18x/DiT-forward at 121 frames, cosine ~1.0). Must precede the backend set
# so the requested kernel pins onto the new processors. No-op for every other family.
trim = install_hunyuan_attention_trim(view, fam, logger = logger)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate Hunyuan trim behind non-off speed mode

For HunyuanVideo-1.5 loads with speed_mode="off", this still installs the padded-token trim unconditionally before attention selection. The comment here notes that the trim makes attention run the fused SDPA path instead of the dense-mask fallback, so an explicit Off/bit-exact load still gets a different attention kernel and rounding behavior (and reports hunyuan_attn_trim in speed_optims). Please skip this optimization when effective_speed == SPEED_OFF unless the user has explicitly opted into a speed tier.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in be04ba0. The trim is a speed lever (it swaps the joint attention onto the fused SDPA kernel), so it is now gated on effective_speed != off exactly like the attention-backend selection right below it -- the bit-exact off path keeps the stock dense-mask attention. Test: test_video_speed_off_skips_hunyuan_trim.

danielhanchen and others added 2 commits July 9, 2026 08:25
Two fidelity fixes to the video speed/mem bench so its numbers match production:
- _build_pipe loaded the pipeline with a scalar bf16 torch_dtype and then upcast the
  VAE, which truncates the fp32-stored Wan VAE at load (a later .to(float32) only widens
  the lossy values). Pin the VAE fp32 per-component like the production loader
  ({"vae": fp32, "default": bf16}) so the bench decodes the same weights production does.
- The persisted reference frames were written/read as a single unkeyed ref_frames.npz in
  the fixed default --out dir, so a reference-less run of a different family/seed/steps/
  frames/resolution scored LPIPS against a stale baseline. Key the cache by those
  parameters so a run only reuses a reference computed for the same parameters.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

if not te_quant_supported(target, mode):
return None

P2 Badge Probe explicit torchao TE kernels before reporting quantized

When a user explicitly requests a torchao text-encoder mode such as fp8_dynamic, int8, or nvfp4, this path only checks the CUDA capability and skips the _te_scheme_probe smoke test that auto uses. On a torchao/PyTorch build where quantize_ can wrap the encoder but the actual GEMM kernel fails, the load will report text_encoder_quant as engaged and then crash on the first prompt-encoder forward instead of falling back to dense; please run the same probe for explicit torchao TE modes before selecting the caster.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +426 to +429
# Attention (per expert).
backend = select_attention_backend(tgt, cfg["attn"], speed_active = speed_active)
for v in views:
engaged["attn"] = apply_attention_backend(v, backend, logger = logger)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Install Hunyuan trim in the video benchmark

For hunyuanvideo-1.5 benchmark runs, this helper applies the attention backend without first installing install_hunyuan_attention_trim, while the production video loader installs that trim before backend selection. As a result shipped/speedmax Hunyuan rows still benchmark the dense-mask attention path and miss the PR's main fused-attention speedup, producing misleading latency and LPIPS comparisons for the optimized family; add the trim install in this per-expert section before applying the backend.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in be04ba0. The bench now installs the Hunyuan trim per expert BEFORE the backend set (the loader's order), gated on an active speed tier, so the shipped/attention rows measure the fused-kernel path the loader runs.

Comment thread scripts/video_speedmem_bench.py Outdated
Comment on lines +370 to +374
# DiT quant (pipeline kind, resident): mutates each expert's transformer in place.
if cfg["dit"] not in ("none", "off"):
schemes = [
quantize_transformer(v, tgt, mode = cfg["dit"], family = fam_name, logger = logger)
for v in views

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mirror the dense-fit int8 skip in the benchmark

For HunyuanVideo-1.5 on a data-center fp8-capable GPU, the production loader skips an AUTO DiT quantization when the dense bf16 DiT already fits resident, because the int8 fallback is slower and less accurate there. This benchmark has already loaded the dense pipeline resident, but still unconditionally calls quantize_transformer(..., mode="auto"), so shipped/ditquant rows measure an int8 path that production would not run and the speed/LPIPS numbers are misleading; apply the same dense-fit is_int8_memory_fallback skip here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in be04ba0. The bench now mirrors the loader's dense-fit skip: for an AUTO dit config on an int8-fallback family (is_int8_memory_fallback -> HunyuanVideo, where fp8 is denied so auto lands on int8, a memory-only lever), it runs the dense DiT instead of int8, since the bench always loads resident. Explicit int8/fp8 configs are still honored (the point of the sweep).

Comment on lines +604 to +609
except Exception: # noqa: BLE001 — optimisation only; never break the forward
for blk in getattr(module, "transformer_blocks", []):
attn = getattr(blk, "attn", None)
if attn is not None:
setattr(attn, _NULL_ATTN_FLAG, False)
return args, kwargs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore trimmed inputs on hook fallback

If this best-effort hook hits an unexpected error after it has already mutated kwargs (for example after emptying all-zero image_embeds and then encountering a changed/malformed text mask), the except path only clears the null-mask flag and returns the partially trimmed inputs. That no longer falls back to the stock path promised by the comment and can leave the model running with image/text lengths that no longer match the dense attention mask; restore original in this exception path before returning.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in be04ba0. The fallback now restores the caller's original kwargs (kwargs.clear(); kwargs.update(original)) before taking the stock dense-mask path, so a failure mid-trim (after image_embeds was emptied / a stream trimmed) never hands the dense path a half-trimmed mix -- same restore the empty-prompt guard already does. Test: test_trim_pre_hook_restores_inputs_on_midtrim_failure.

…plicit TE kernels, bench fidelity

Address the Codex review round on the video/quant work:

- Companion auto-quant now honors an explicit Speed=off. Both loaders already pin the DiT dense
  under an explicit off (bit-exact reference), but the unset text-encoder / VAE quant still promoted
  to auto and silently fp8/int8'd the companions, breaking the bit-exact request. An UNSET speed
  still auto-quantises; an explicit companion scheme still forces it.
- The HunyuanVideo joint-attention trim is a speed lever (it swaps to the fused SDPA kernel), so gate
  it on a non-off speed tier exactly like the adjacent attention-backend selection -- the off path
  keeps the stock dense-mask attention.
- Explicit torchao text-encoder modes (int8 / fp8_dynamic / nvfp4) now run the same kernel smoke
  test the auto ladder uses. They could clear the capability gate yet fail the real GEMM on a build
  where quantize_ wraps the encoder but the kernel is broken; the caster's try/except only covers the
  cast, not the first forward, so the load would report engaged then crash at generation. Now it
  falls back to dense. Layerwise fp8 has no torchao GEMM, so the probe is a no-op for it.
- The trim pre-hook's fallback restores the caller's original kwargs (it may have emptied the image
  stream / trimmed a text stream before failing), so the stock dense-mask path runs on exactly what
  it expects, matching the empty-prompt guard.
- video_speedmem_bench mirrors the loader: installs the Hunyuan trim before the backend set (gated on
  an active tier) and skips the auto int8 quant when it is the fp8-denied memory fallback and dense
  fits resident, so the shipped/auto rows measure what the loader actually runs.

Tests: TE explicit-mode kernel probe (+ layerwise-fp8 bypass), trim mid-trim restore, and loader-level
speed=off companion suppression + trim skip for both backends. 262 backend tests pass; ruff clean.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 28545b22f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d378bc496

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

normalize_cache_quality,
)

if cache_mode == "auto":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-size explicit MagCache rows to requested steps

When a benchmark config explicitly uses cache="magcache" (for example magcache_explicit) and --steps differs from the family default, _apply_levers installs MagCache using default_steps, but this generation-time path only rechecks cache_mode == "auto". With the script's default --steps 30 against 50-step calibrated defaults, the row keeps the #s50 curve/skip budget while timing a 30-step clip, unlike production which re-engages explicit MagCache for the actual step count. Please mirror that explicit-MagCache reconfiguration here before timing so the benchmark doesn't publish latency/LPIPS for a cache state users won't run.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The bench now mirrors the loader's explicit-MagCache re-engagement: when the actual step count differs from the configured marker, each expert's cache is disengaged and re-applied over the real schedule before timing, so magcache rows no longer publish latency or LPIPS for a curve users cannot run. Fixed in bd1c2e0.

Comment thread scripts/video_speedmem_bench.py Outdated
"te_scheme": engaged["te"] or "dense",
"vae_scheme": engaged["vae"] or "dense",
"attn": engaged["attn"] or "native",
"cache": engaged["cache"] or "off",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report the generation-time cache state

When cache="auto" is benchmarked with an actual --steps value that crosses the auto threshold (for example a Hunyuan/Wan default cache installed at 50 steps but a run with --steps 8), _timed_video toggles the transformer cache off/on for the real generation, but this row still records the load-time engaged["cache"]. That publishes timings/LPIPS under cache: magcache/fbcache even when the measured clip ran uncached (or vice versa); derive the field from the post-toggle marker/current transformer state, matching production status updates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The row's cache field is now derived from the post-toggle transformer marker, so an auto cache toggled off below the step threshold (or a re-sized explicit magcache) is reported as it actually ran; the load-time engagement stays available as cache_at_load. Fixed in bd1c2e0.

Comment thread scripts/video_speedmem_bench.py Outdated
_empty()

# ── optimisation layers ──
snapshot_backend_flags() # process-wide flags; benchmark process is short-lived so no restore.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore speed globals between benchmark rows

When --configs runs a speed-enabled row before a bit-exact row in the same process (for example speedmax,reference or epc_off,reference), this snapshot is discarded and the process-wide flags that apply_speed_optims mutates (cudnn.benchmark, TF32/fp16 accumulation, and emulate_precision_casts) are never restored. The later speed="off"/reference row can therefore inherit the previous row's backend settings, corrupting the baseline and LPIPS/speed comparisons; keep the snapshot and restore it after each config, as the production loader does on unload.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The snapshot is kept and restored after each config, like the production unload path, so a speedmax or epc_off row no longer leaks cudnn.benchmark, TF32/fp16 accumulation, or emulate_precision_casts into a later reference or off row in the same process. Fixed in bd1c2e0.

danielhanchen and others added 2 commits July 10, 2026 18:56
…he reporting, per-config flag restore

Mirror production's explicit-MagCache re-engagement in the video bench: a
magcache row installed at the family default step count now re-interpolates
its curve, retention window, and skip budget when --steps differs, instead of
timing a stale schedule users never run.

Report the generation-time cache state: the row's cache field is derived from
the post-toggle transformer marker rather than the load-time engagement, so an
auto cache toggled off below the step threshold (or a re-sized explicit
magcache) is published as it actually ran; the load-time value stays available
as cache_at_load.

Restore the process-wide backend flags (cudnn.benchmark, TF32 and fp16
accumulation, emulate_precision_casts) after each config, like the production
unload path, so a speed-enabled row cannot poison a later reference or off row
in the same --configs run.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87054e7f94

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +333 to +334
if mode == VAE_QUANT_FP8_DYNAMIC:
_cast_vae_fp8_dynamic(vae, target)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Place the VAE before applying dynamic FP8

When an API caller explicitly requests vae_quant="fp8_dynamic" on a resident load, the image and video loaders call quantize_vae before apply_memory_plan, so the VAE is still the freshly loaded CPU module here. This path probes CUDA conv kernels, then casts the CPU VAE and relies on the later pipe.to(device) to move torchao FP8 tensor subclasses; the surrounding code already notes those subclasses reject Module.to(), so this can make the explicit fp8_dynamic load fail during placement instead of producing a usable resident VAE. Move the VAE to the target before this cast or run the dynamic cast after resident placement.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Could not reproduce the mechanism: on the installed torchao a single Module.to moves the fp8_dynamic Float8Tensor weights fine (verified CPU-quantize then to-cuda then forward), which is the resident path's one placement; the reject-Module.to note applies to offload hooks' repeated moves, and fp8_dynamic is already offload-denied. A build lacking the fp8 conv kernel is caught by the per-device, per-dimensionality probe before the cast.

Comment on lines +230 to +233
self._primary.enable_cache(config)
try:
self._replica.enable_cache(config)
_invalidate_registry(self._replica)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate the primary cache registry when fanning out

When CFG-parallel is installed and the step cache is re-engaged through the proxy, such as a MagCache generation with a different step count after a prior run, apply_step_cache calls this proxy method. It invalidates only the replica registry, while the later _invalidate_child_registry_cache(transformer) sees the proxy object rather than _primary, so the primary DiT can keep the stale child-registry list from the previous hooks and the new primary cache hooks may not receive a context. Invalidate _primary here as well after self._primary.enable_cache(config).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The delegation goes the other way: the proxy's getattr forwards _diffusers_hook to the primary, so apply_step_cache's post-enable invalidation nulls the primary's child-registry cache through the proxy (verified by running that call against the real proxy), and the fan-out's explicit replica invalidation covers the one object delegation cannot reach. Both DiTs are invalidated on every enable.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

toggled = maybe_toggle_step_cache(
state.pipe,

P2 Badge Toggle step cache on the workflow pipe

When an auto-cached image load is reused for img2img/inpaint/upscale/ControlNet, the actual pipeline selected above is pipe, but this toggle still runs against state.pipe (the original txt2img pipeline). If the effective denoise count stays above FBCACHE_MIN_STEPS, the cache remains enabled because the txt2img pipeline opens cache_context, then the workflow pipe is invoked without that context (these workflows are exactly why _pipeline_opens_cache_context exists) and the cached transformer can fail with No context is set. Re-check/disable caching against the actual pipe that will be called.


for fn in setters:
try:
fn(backend)
engaged = True
except Exception as exc: # noqa: BLE001 — unavailable kernel -> restore native below
_warn(logger, backend, exc)
if engaged:

P2 Badge Roll back partial attention backend sets

When a multi-DiT pipe has one denoiser accept the requested backend and another denoiser's set_attention_backend raises, this leaves engaged true and returns the backend without restoring the successful module to native. That violates the helper's all-denoisers contract (_attention_dits includes transformer_2/unconditional_transformer) and makes status report a single attention backend while the clip runs with mixed kernels. Treat partial success as a fallback or roll back already-set denoisers when any setter fails.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +230 to +233
self._primary.enable_cache(config)
try:
self._replica.enable_cache(config)
_invalidate_registry(self._replica)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate the primary cache registry too

When CFG parallelism is installed and an auto cache toggles off for a short generation, the uncached run still enters proxy.cache_context(...), which can populate the primary DiT's HookRegistry child cache without block hooks. On a later generation that re-enables caching through the proxy, apply_step_cache only sees the proxy object and this fan-out invalidates only the replica, so the primary's newly installed cache hooks can miss the context and fail with the same stale-registry No context is set path that _invalidate_child_registry_cache is meant to prevent. Invalidate _primary after enabling it as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same as the sibling thread: getattr on the proxy forwards _diffusers_hook to the primary, so the post-enable invalidation reaches the primary through the proxy, and the fan-out explicitly invalidates the replica. Both registries are cleared on every enable, including the uncached-context-then-re-enable sequence.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87054e7f94

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +229 to +233
def enable_cache(self, config: Any) -> None:
self._primary.enable_cache(config)
try:
self._replica.enable_cache(config)
_invalidate_registry(self._replica)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate primary cache registry when proxy enables cache

When cfg_parallel is installed and the auto step cache is toggled off by a short generation, the primary transformer's HookRegistry can cache an empty child-registry list during that uncached cache_context. A later >=20-step generation re-enables cache through this proxy, but this path invalidates only the replica while apply_step_cache invalidates the proxy object, not _primary; the primary block hooks can then miss the cache context and fail on the first cached forward with the same “No context is set” issue the cache helper guards against. Please invalidate _primary as well after enable_cache.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same premise as the two sibling threads and it does not hold: the proxy's getattr forwards _diffusers_hook to the primary, so apply_step_cache's post-enable invalidation nulls the PRIMARY's child-registry cache through the proxy (verified by executing that call against the real proxy object); the fan-out's explicit replica invalidation covers the one object delegation cannot reach. A comment now documents this invariant at the fan-out in ce32c1e.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce32c1e1fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1231 to +1232
if text_encoder_quant is None or str(text_encoder_quant).strip().lower() in ("", "auto"):
text_encoder_quant = "off" if speed_off else TE_QUANT_AUTO

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Don't auto-quantize TE before resident placement

When text_encoder_quant is left unset on a resident CUDA image load, this now defaults to auto. For families where auto resolves to a torchao scheme such as fp8_dynamic or int8, quantize_text_encoders runs before apply_memory_plan, while the pipeline is still CPU-resident; the same helper skips torchao under offload because those tensor subclasses reject Module.to(). The resident path then calls pipe.to(device), so default loads can fail during placement unless the user disables TE quant. Please either quantize after the resident .to() or avoid torchao TE schemes before placement.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Verified on-GPU with the exact configs this path can resolve: int8 dynamic-activation, per-row fp8_dynamic and nvfp4 subclasses all accept the single resident Module.to() and forward correctly afterwards; the offload skip targets per-module hook moves, not the one-shot placement, and layerwise fp8 is not a torchao subclass. Repro at temp/repro_r31_cpu_quant_to_cuda.py.

Comment on lines +989 to +990
if text_encoder_quant is None or str(text_encoder_quant).strip().lower() in ("", "auto"):
text_encoder_quant = "off" if speed_off else TE_QUANT_AUTO

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Don't auto-quantize video TE before placement

This makes an unset video text_encoder_quant default to auto, but the video load path still calls quantize_text_encoders before apply_memory_plan places the freshly loaded pipeline. On resident CUDA loads where auto picks a torchao TE scheme, the encoder is quantized while still CPU-resident and then the later resident placement calls pipe.to(device) on the torchao tensor subclasses that this code already treats as incompatible with Module.to() under offload. Default video loads on those GPUs can therefore fail during placement; quantize after resident placement or keep the default dense until then.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same evidence as the image thread: every reachable TE scheme survives the video loader's single resident placement move on-GPU; the incompatibility note is about offload hooks' repeated per-module moves.

Comment on lines +373 to +375
quantize_(
vae, Float8DynamicActivationFloat8WeightConfig(granularity = PerTensor()), filter_fn = filter_fn
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Move fp8_dynamic VAE to CUDA before quantizing

For an explicit vae_quant="fp8_dynamic" request on a resident image or video load, both loaders call quantize_vae before the later apply_memory_plan(...).to(device) placement, but this caster ignores target and quantizes the VAE in its current CPU-resident state. That leaves torchao fp8 tensor subclasses in the VAE and the subsequent resident placement has to run Module.to() over them, the same operation this module already avoids under offload because it is unsupported. Move the VAE to the target device before this torchao cast or defer this mode until after resident placement.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The Float8Tensor conv weights survive the resident .to() on-GPU (Conv2d, Conv3d and Linear all verified); kernel availability is orthogonal to placement order and the per-device, per-dimensionality probe already keeps the VAE dense on builds without the fp8 conv path.

Comment on lines +486 to +489
mode = normalize_cfg_parallel(requested)
if mode == CFG_PARALLEL_OFF:
return None, "disabled by request"
explicit_on = mode == CFG_PARALLEL_ON

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect Speed=off before auto-enabling CFG parallel

When speed_mode="off" and cfg_parallel is left at auto, speed_active is passed as False but never gates the auto path here, so a resident two-GPU HunyuanVideo-1.5 load can still build and install the CFG-parallel proxy. That makes the explicit Speed=off reference load reserve a second GPU and run a speed lever unless the user also knows to set cfg_parallel="off"; auto should return off when speed_active is false, while still allowing an explicit cfg_parallel="on" if desired.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. The auto gate now returns off when speed_active is false, before any build or GPU reservation, so a Speed=off load stays a true single-device reference; an explicit cfg_parallel=on still overrides. Fixed in d7a5a01 with a test.

pred_uncond = None,
**kw,
):
return orig_forward(_resolve(pred_cond), _resolve(pred_uncond), **kw)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve predictions inside guider_state

When CFG parallel engages for HunyuanVideo-1.5, the real denoise loop stores each branch output on guider_state_batch.noise_pred and then calls self.guider(guider_state), so pred_cond here is the guider-state container rather than the prediction tensor. _resolve therefore returns the container unchanged, leaving a _PendingPred in thread mode or a cuda:1 tensor in inline mode for the original guider to combine, which either breaks arithmetic on the placeholder or mixes devices. Resolve the nested noise_pred entries before delegating to the original guider.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

On the installed diffusers, BaseGuidance.call unpacks each batch's noise_pred before calling forward (guider_utils.py:176-184), so the patched forward receives the per-branch prediction values, never the container; call does no tensor arithmetic. The round-2 bit-identical parallel-vs-sequential validation exercised exactly this path.

Speed=off is the reference contract: the loaders pin every auto speed
lever (transformer/TE/VAE quant tri-states) to off, but the cfg_parallel
auto path never consulted speed_active, so a resident two GPU
HunyuanVideo-1.5 load with Speed=off could still reserve a second GPU
and install the CFG-parallel proxy. Auto now returns off when
speed_active is false; an explicit cfg_parallel=on stays honored as a
deliberate override (the install-failure test now exercises exactly
that override path).
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: d7a5a01522

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The pre-warmed torch.compile cache (diffusion_compile_cache.py: fingerprinted
bundles over torch.compiler.save/load_cache_artifacts plus a persistent per-key
TORCHINDUCTOR_CACHE_DIR) was wired into the image backend only, so every video
load re-paid the full first-generation compile after every process restart (the
stock inductor dir lives in /tmp).

video.py now mirrors the image backend exactly: compile_cache.begin runs after
the attention-backend set (the fingerprint keys on the engaged kernel) and
before apply_speed_optims on a compile-eligible default/max tier, keyed on the
same fullgraph decision as the compile itself (an engaged or still-toggleable
step cache and a planned offload both drop it); the context is committed to
_VideoLoadState and compile_cache.save persists the bundle after the first
successful generation (env-gated distributor / first-run-warm mode);
_teardown_state restores the inductor dir, and a token-scoped
_rollback_precommit_compile_cache covers loads that die before the state
commit, mirroring the globals and CFG-parallel rollbacks.

Measured on HunyuanVideo-1.5-480p through the real VideoBackend (B200,
480x288/17f/30 steps): the first-generation extra drops from 107.5 s cold to
13.8 s when the 12.8 MB bundle loads into a fresh inductor dir (0.10 s load)
and to 11.7 s from the persistent per-key dir alone; through the wired
production path a restart lands at 10.5-10.8 s (bundle-only included) vs
86.5 s cold. Steady state is unchanged (2.4-2.6 s), and the loaded artifacts
are the same bits a local compile would produce, so numerics are untouched.

Tests: begin/save/restore lifecycle with the fullgraph keying, the Speed=off
and compile-ineligible skips, and the token-scoped pre-commit rollback.
Every current release of the kernels package (0.13 through 0.16) builds its
dependency tables with huggingface_hub >= 1.0's strict-dataclass API, and with
an older hub the breakage is not contained to the requested backend: import
kernels raises at module scope, and diffusers imports kernels whenever it is
installed, so a single on-demand install (an explicit flash3/flash4 attention
request on a stack pinned to hub < 1.0) permanently breaks every later
diffusers pipeline import on the box until the package is removed. Reproduced
against hub 0.36.2 with kernels 0.13.0 and 0.16.0: the HunyuanVideo-1.5
pipeline import dies in hub's strict-dataclass validator both times.

_ensure_attention_backend_installed now checks the resident hub version before
installing kernels (_kernels_hub_compatible) and refuses on < 1.0, logging why
and falling back to the native default, which is the best-effort contract the
installer already promises for an uninstallable wheel. The refusal is a policy
decision, not a failed attempt, so it is not memoised and a later request on a
fixed environment can still install. An undeterminable hub version keeps the
previous permissive behaviour, and the gate applies only to the kernels
package: sage/flash-attn/xformers wheels do not import hub at module scope.

Tests: the refusal (nothing memoised), the hub >= 1.0 allow, the
package-scoping, and the version-parse fallback.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

pre-commit-ci Bot and others added 2 commits July 11, 2026 06:09
… prewarm

vLLM and SGLang finish every compilation at server startup (dummy batches
through each compiled shape) so no request ever pays a compile mid-serving.
The video backend's compiled tier instead paid a first-generation extra after
every restart: ~54 s cold and ~11.3 s even with a warm Mega-cache bundle (the
residual is dynamo tracing plus cudnn.benchmark autotune, which the bundle
cannot carry).

After a compiled DEFAULT-tier resident load commits, a daemon thread now runs
one tiny throwaway generation (192x128 snapped, 4k+1-lattice 9 frames, 2
steps) under the generate lock. The default tier compiles dynamic=True, so
the small trace serves every later resolution. Measured through the real
backend (HunyuanVideo-1.5-480p, B200, 480x288/17f/30 steps):

  warm bundle: first-generation extra 11.3 s -> 2.1 s (9.6 s background warmup)
  cold start:  the full compile moves off the user's first request entirely
               (14.5 s background; first generation extra 2.1 s), and the
               warmup persists the Mega-cache bundle itself
  steady state: unchanged (2.4-2.5 s per 30-step clip in every phase)

Exactly lossless by construction: the warmup only changes when compilation
work happens. It resets its step-cache residuals, the real generation seeds
its own generator, and no process-wide flag is touched.

The warmup registers itself as the active cancellable job, so unload, a new
load, or cancel_generate abort it at a step boundary (verified: unload 2 s
into a running prewarm returns in 6.7 s with the warmup cancelled). It yields
untouched when a real request arrived first and is token-scoped against
superseded loads. Gated per family (supports_compile_prewarm), skipped for
speed=max (static per-shape graphs a warmup shape cannot serve), offload
(every warmup forward would stream the DiT over PCIe), and CFG parallel (its
planner owns compile-sensitive runs); the UNSLOTH_DIFFUSION_COMPILE_PREWARM
kill switch disables it. The decision and reason land in the resolved record.

Tests: +4 hermetic (decision gates, engaged-load spawn with snapped tiny
shape, skip-without-compile, yield to generations / stale tokens); related
backend set 551 passed; ruff clean.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6adcc34e1b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return # superseded/unloaded before the warmup could start
if self._generate_job_active or self._gen.get("active"):
return # a real request beat us; it absorbs the warmup itself
self._active_generate_cancel = cancel

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Let real generations preempt compile prewarm

On compiled video loads where the background prewarm has already entered _generate_lock, this only registers a cancel event but does not mark the warmup as the active generation. A user generation submitted during that window sees _generate_job_active == False, overwrites _active_generate_cancel with its own event, and then waits behind the warmup, so cancel/unload no longer signals the prewarm and the first real request can be delayed by the 9–54s warmup this path is meant to hide. Mark the prewarm as busy/preemptible, or have begin_generate cancel and wait for it before queuing the real job.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Genuine catch, fixed in 47c202e. The prewarm now registers its cancel event in a dedicated _prewarm_cancel slot (in addition to _active_generate_cancel, so unload/cancel keep working unchanged), and begin_generate signals that slot before installing the real job's event; direct generate() calls do the same. The warmup aborts at its next step boundary and the real job takes _generate_lock instead of waiting out the full warmup. Covered by test_begin_generate_preempts_running_prewarm.

danielhanchen and others added 2 commits July 11, 2026 08:10
The prewarm registered its cancel event in _active_generate_cancel, but a
begin_generate arriving mid-warmup overwrote that slot with its own event
and then queued its worker behind the full warmup on _generate_lock. From
that point unload and cancel_generate signalled the wrong event, so the
warmup could no longer be aborted and the first real request waited out
the 9-54s the prewarm exists to hide.

Track the prewarm's event in a dedicated _prewarm_cancel slot (cleared
identity-checked alongside _active_generate_cancel) and signal it from
begin_generate before registering the real job's event, and from direct
generate() calls that skip begin_generate. The warmup then aborts at its
next step boundary and the real job takes the lock, while unload/cancel
keep working against whichever run is actually active.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 35eb80c8c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant