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.
204 lines
9.5 KiB
Python
204 lines
9.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Tests for the HunyuanVideo-1.5 padded-text attention trim.
|
|
|
|
``_trim_stream`` / ``_hunyuan_trim_pre_hook`` / ``install_hunyuan_attention_trim`` use real torch
|
|
tensor ops, so unlike the attention-backend policy tests in ``test_diffusion_attention.py`` these
|
|
require torch. Kept in a separate module so that file stays collectable without torch installed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import types
|
|
|
|
import torch
|
|
|
|
import core.inference.diffusion_attention as att
|
|
|
|
|
|
def test_trim_stream_drops_trailing_padding():
|
|
# right-padded (valid prefix): drop the globally-invalid tail, keep valid, flag all_valid.
|
|
states = torch.arange(6.0).reshape(1, 6, 1)
|
|
mask = torch.tensor([[1, 1, 1, 0, 0, 0]])
|
|
out_s, out_m, all_valid = att._trim_stream(states, mask)
|
|
assert out_s.shape == (1, 3, 1)
|
|
assert torch.equal(out_s[0, :, 0], torch.tensor([0.0, 1.0, 2.0]))
|
|
assert out_m.shape == (1, 3) and all_valid is True
|
|
|
|
|
|
def test_trim_stream_layout_agnostic_drops_only_global_padding():
|
|
# left-padded (valid suffix): any(dim=0) keeps positions valid for at least one element,
|
|
# so the leading globally-invalid columns are dropped regardless of padding side.
|
|
states = torch.arange(4.0).reshape(1, 4, 1)
|
|
mask = torch.tensor([[0, 0, 1, 1]])
|
|
out_s, out_m, all_valid = att._trim_stream(states, mask)
|
|
assert torch.equal(out_s[0, :, 0], torch.tensor([2.0, 3.0])) and all_valid is True
|
|
|
|
|
|
def test_trim_stream_full_mask_is_noop():
|
|
states = torch.ones(1, 4, 2)
|
|
mask = torch.ones(1, 4, dtype = torch.long)
|
|
out_s, out_m, all_valid = att._trim_stream(states, mask)
|
|
assert out_s.shape == (1, 4, 2) and all_valid is True
|
|
|
|
|
|
def test_trim_stream_none_mask_passthrough():
|
|
states = torch.ones(1, 4, 2)
|
|
out_s, out_m, all_valid = att._trim_stream(states, None)
|
|
assert out_s is states and out_m is None and all_valid is True
|
|
|
|
|
|
def test_trim_stream_mixed_batch_not_all_valid():
|
|
# batch>1 with different valid sets: the union is kept, but a column valid for only one
|
|
# element remains partially padded -> all_valid False -> caller keeps the dense mask.
|
|
states = torch.ones(2, 4, 1)
|
|
mask = torch.tensor([[1, 1, 0, 0], [1, 1, 1, 0]]) # elem1 has 2 valid, elem2 has 3
|
|
out_s, out_m, all_valid = att._trim_stream(states, mask)
|
|
assert out_s.shape == (2, 3, 1) # dropped the last col (invalid for both)
|
|
assert all_valid is False
|
|
|
|
|
|
def _fake_dit(n_blocks = 2):
|
|
blocks = [types.SimpleNamespace(attn = types.SimpleNamespace()) for _ in range(n_blocks)]
|
|
return types.SimpleNamespace(transformer_blocks = blocks)
|
|
|
|
|
|
def test_trim_pre_hook_empties_t2v_image_and_trims_and_flags():
|
|
dit = _fake_dit()
|
|
kwargs = {
|
|
"image_embeds": torch.zeros(1, 5, 3), # all-zero -> t2v -> emptied
|
|
"encoder_hidden_states": torch.arange(4.0).reshape(1, 4, 1),
|
|
"encoder_attention_mask": torch.tensor([[1, 1, 0, 0]]),
|
|
"encoder_hidden_states_2": torch.arange(3.0).reshape(1, 3, 1),
|
|
"encoder_attention_mask_2": torch.tensor([[1, 0, 0]]),
|
|
}
|
|
args, out = att._hunyuan_trim_pre_hook(dit, (), kwargs)
|
|
assert out["image_embeds"].shape == (1, 0, 3) # image tokens dropped
|
|
assert out["encoder_hidden_states"].shape == (1, 2, 1) # mllm trimmed to 2 valid
|
|
assert out["encoder_hidden_states_2"].shape == (1, 1, 1) # byt5 trimmed to 1 valid
|
|
assert all(getattr(b.attn, att._NULL_ATTN_FLAG) is True for b in dit.transformer_blocks)
|
|
|
|
|
|
def test_trim_stream_all_invalid_yields_empty_but_valid():
|
|
# A fully-padded secondary stream (e.g. unused byt5 in t2v) trims to 0 length and reports
|
|
# all_valid True (vacuous) so it does NOT drop the fast path -- it just contributes no tokens.
|
|
states = torch.ones(1, 5, 2)
|
|
mask = torch.zeros(1, 5, dtype = torch.long)
|
|
out_s, out_m, all_valid = att._trim_stream(states, mask)
|
|
assert out_s.shape == (1, 0, 2) and all_valid is True
|
|
|
|
|
|
def test_trim_pre_hook_byt5_all_invalid_keeps_fast_path():
|
|
# The real t2v case: byt5 is entirely padding (valid=0). It must be emptied WITHOUT dropping
|
|
# the null-mask fast path, since mllm still carries the prompt.
|
|
dit = _fake_dit()
|
|
kwargs = {
|
|
"image_embeds": torch.zeros(1, 5, 3),
|
|
"encoder_hidden_states": torch.arange(4.0).reshape(1, 4, 1),
|
|
"encoder_attention_mask": torch.tensor([[1, 1, 1, 0]]),
|
|
"encoder_hidden_states_2": torch.ones(1, 6, 1),
|
|
"encoder_attention_mask_2": torch.zeros(1, 6, dtype = torch.long), # all padding
|
|
}
|
|
_, out = att._hunyuan_trim_pre_hook(dit, (), kwargs)
|
|
assert out["encoder_hidden_states"].shape == (1, 3, 1)
|
|
assert out["encoder_hidden_states_2"].shape == (1, 0, 1) # byt5 emptied
|
|
assert all(getattr(b.attn, att._NULL_ATTN_FLAG) is True for b in dit.transformer_blocks)
|
|
|
|
|
|
def test_trim_pre_hook_empty_primary_reverts_and_disables():
|
|
# Pathological empty prompt: mllm has 0 valid tokens. The TokenRefiner must not get a
|
|
# 0-length sequence -> revert all inputs to original and take the stock dense-mask path.
|
|
dit = _fake_dit()
|
|
mllm = torch.ones(1, 4, 1)
|
|
kwargs = {
|
|
"image_embeds": torch.zeros(1, 5, 3),
|
|
"encoder_hidden_states": mllm,
|
|
"encoder_attention_mask": torch.zeros(1, 4, dtype = torch.long), # 0 valid
|
|
}
|
|
_, out = att._hunyuan_trim_pre_hook(dit, (), kwargs)
|
|
assert out["encoder_hidden_states"] is mllm # reverted (not emptied)
|
|
assert out["image_embeds"].shape == (1, 5, 3) # image revert too
|
|
assert all(getattr(b.attn, att._NULL_ATTN_FLAG) is False for b in dit.transformer_blocks)
|
|
|
|
|
|
def test_trim_pre_hook_keeps_i2v_image():
|
|
dit = _fake_dit()
|
|
img = torch.ones(1, 5, 3) # nonzero -> i2v -> kept
|
|
kwargs = {
|
|
"image_embeds": img,
|
|
"encoder_hidden_states": torch.arange(4.0).reshape(1, 4, 1),
|
|
"encoder_attention_mask": torch.tensor([[1, 1, 1, 1]]),
|
|
}
|
|
_, out = att._hunyuan_trim_pre_hook(dit, (), kwargs)
|
|
assert out["image_embeds"] is img # not emptied
|
|
assert all(getattr(b.attn, att._NULL_ATTN_FLAG) is True for b in dit.transformer_blocks)
|
|
|
|
|
|
def test_trim_pre_hook_mixed_batch_flags_false():
|
|
dit = _fake_dit()
|
|
kwargs = {
|
|
"image_embeds": torch.zeros(2, 2, 3),
|
|
"encoder_hidden_states": torch.ones(2, 4, 1),
|
|
"encoder_attention_mask": torch.tensor([[1, 1, 0, 0], [1, 1, 1, 0]]),
|
|
}
|
|
_, out = att._hunyuan_trim_pre_hook(dit, (), kwargs)
|
|
assert all(getattr(b.attn, att._NULL_ATTN_FLAG) is False for b in dit.transformer_blocks)
|
|
|
|
|
|
def test_trim_pre_hook_never_raises_sets_flag_false():
|
|
# A malformed mask (not a tensor) must not break the forward: flag False, no exception.
|
|
dit = _fake_dit()
|
|
kwargs = {"encoder_hidden_states": torch.ones(1, 2, 1), "encoder_attention_mask": "oops"}
|
|
args, out = att._hunyuan_trim_pre_hook(dit, (), kwargs)
|
|
assert all(getattr(b.attn, att._NULL_ATTN_FLAG) is False for b in dit.transformer_blocks)
|
|
|
|
|
|
def test_trim_pre_hook_restores_inputs_on_midtrim_failure():
|
|
# A later stream trips the trim AFTER earlier inputs were already mutated (image emptied, mllm
|
|
# trimmed). The fallback must restore the caller's ORIGINAL kwargs so the stock dense-mask path
|
|
# (flag False) runs on exactly what it expects -- never a half-trimmed mix.
|
|
dit = _fake_dit()
|
|
img = torch.zeros(1, 5, 3)
|
|
mllm = torch.arange(4.0).reshape(1, 4, 1)
|
|
mllm_mask = torch.tensor([[1, 1, 0, 0]])
|
|
byt5 = torch.ones(1, 3, 1)
|
|
kwargs = {
|
|
"image_embeds": img,
|
|
"encoder_hidden_states": mllm,
|
|
"encoder_attention_mask": mllm_mask,
|
|
"encoder_hidden_states_2": byt5,
|
|
"encoder_attention_mask_2": "oops", # malformed -> _trim_stream raises after mllm is trimmed
|
|
}
|
|
_, out = att._hunyuan_trim_pre_hook(dit, (), kwargs)
|
|
assert out["image_embeds"] is img # emptied then restored
|
|
assert out["encoder_hidden_states"] is mllm # trimmed then restored
|
|
assert out["encoder_attention_mask"] is mllm_mask
|
|
assert out["encoder_hidden_states_2"] is byt5
|
|
assert out["encoder_attention_mask_2"] == "oops"
|
|
assert all(getattr(b.attn, att._NULL_ATTN_FLAG) is False for b in dit.transformer_blocks)
|
|
|
|
|
|
def test_trim_pre_hook_absent_stream_not_written_back():
|
|
# If encoder_hidden_states is absent from kwargs (a caller passing it positionally), the hook
|
|
# must NOT write it back as None (that would collide: "got multiple values for argument") and
|
|
# must drop the fast path (flag False) rather than null a mask it never verified.
|
|
dit = _fake_dit()
|
|
kwargs = {"image_embeds": torch.zeros(1, 4, 3)} # no encoder_hidden_states key
|
|
_, out = att._hunyuan_trim_pre_hook(dit, (torch.ones(1, 5, 1),), kwargs)
|
|
assert "encoder_hidden_states" not in out
|
|
assert all(getattr(b.attn, att._NULL_ATTN_FLAG) is False for b in dit.transformer_blocks)
|
|
|
|
|
|
def test_install_trim_noop_for_non_hunyuan_family():
|
|
fam = types.SimpleNamespace(transformer_class = "WanTransformer3DModel")
|
|
pipe = types.SimpleNamespace(transformer = types.SimpleNamespace())
|
|
assert att.install_hunyuan_attention_trim(pipe, fam) is False
|
|
|
|
|
|
def test_install_trim_noop_when_transformer_class_mismatch():
|
|
# Family claims Hunyuan but the loaded module isn't -> no processors touched, no diffusers
|
|
# import; returns False rather than swapping an unknown attention processor.
|
|
fam = types.SimpleNamespace(transformer_class = "HunyuanVideo15Transformer3DModel")
|
|
pipe = types.SimpleNamespace(transformer = types.SimpleNamespace()) # class name mismatch
|
|
assert att.install_hunyuan_attention_trim(pipe, fam) is False
|