[bugfix] Fix FastH3 regional compile recompiling for every prompt length (#1942)

Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Aryan Kumar
2026-10-18 18:22:06 -07:00
committed by GitHub
co-authored by Aryan Kumar greptile-apps[bot]
parent 91a1c50909
commit 0033a9b11d
3 changed files with 179 additions and 20 deletions
@@ -157,6 +157,21 @@ def _h3_vsa_ptx_blackwell_from_mask_compat_fake(
return torch.empty_like(q)
@torch.library.custom_op("fastvideo::h3_vsa_block_mask", mutates_args=())
def _h3_vsa_block_mask(scores: torch.Tensor) -> torch.Tensor:
"""``_build_block_mask`` reading the per-request geometry at run time, so compiled graphs don't guard on it."""
from fastvideo.forward_context import get_forward_context
metadata = get_forward_context().attn_metadata
return _build_block_mask(scores, metadata.num_prefix_tiles, metadata.VSA_sparsity, metadata.exempt,
metadata.video_tile_spans, metadata.span_sparsities)
@torch.library.register_fake("fastvideo::h3_vsa_block_mask")
def _h3_vsa_block_mask_fake(scores: torch.Tensor) -> torch.Tensor:
return torch.empty_like(scores, dtype=torch.bool)
def _ptx_blackwell_has_compile_safe_mask_route(ptx_blackwell_mod: Any) -> bool:
return (callable(getattr(ptx_blackwell_mod, "block_sparse_attn_ptx_blackwell_from_mask", None))
or (callable(getattr(ptx_blackwell_mod, "block_sparse_attn_ptx_blackwell", None))
@@ -638,14 +653,14 @@ class MiniMaxH3VSAImpl(AttentionImpl):
additional all-zero tile internally; metadata and all observable
outputs retain the logical geometry.
"""
if x.shape[1] != attn_metadata.total_seq_length:
compiling = torch.compiler.is_compiling()
regional_compiling = compiling and self._regional_compile_ptx_blackwell_enabled is True
if not regional_compiling and x.shape[1] != attn_metadata.total_seq_length:
raise ValueError(f"VSA-H3 metadata was built for sequence length {attn_metadata.total_seq_length}, "
f"got {x.shape[1]}. A non-packed sequence (e.g. the token refiner) is "
"routed to the VSA-H3 backend; exclude it from the supported backends.")
n_tiles = attn_metadata.variable_block_sizes.numel()
grad_mode = torch.is_grad_enabled() and x.requires_grad
compiling = torch.compiler.is_compiling()
regional_compiling = compiling and self._regional_compile_ptx_blackwell_enabled is True
if regional_compiling:
ptx_blackwell_requested = bool(self._regional_compile_ptx_blackwell_enabled)
elif compiling:
@@ -656,8 +671,12 @@ class MiniMaxH3VSAImpl(AttentionImpl):
# Tile 128 has no route other than the ptx_blackwell CUDA kernel.
ptx_blackwell_route = attn_metadata.tile_elems == 128 or (attn_metadata.tile_elems == 64
and ptx_blackwell_requested)
needs_ptx_blackwell_pair = n_tiles % 2 != 0 and not grad_mode and ptx_blackwell_route
kernel_tiles = n_tiles + int(needs_ptx_blackwell_pair)
if regional_compiling:
# Pad arithmetically so odd and even tile counts share one graph.
needs_ptx_blackwell_pair, kernel_tiles = False, n_tiles + n_tiles % 2
else:
needs_ptx_blackwell_pair = n_tiles % 2 != 0 and not grad_mode and ptx_blackwell_route
kernel_tiles = n_tiles + int(needs_ptx_blackwell_pair)
target_shape = (x.shape[0], kernel_tiles * attn_metadata.tile_elems, x.shape[-2], x.shape[-1])
# A grad-tracking forward must not reuse the builder-owned buffer. The
@@ -668,7 +687,7 @@ class MiniMaxH3VSAImpl(AttentionImpl):
# autograd edges, and through them the activations they saved, for the
# rest of training. This is the VSA-H3 counterpart of the Wan tile-cache
# OOM (#1423), which training fixed with ``vsa_cache_tile_buf=False``.
if grad_mode:
if grad_mode or regional_compiling:
return scatter_into_tile_buf(x, target_shape, attn_metadata.untile_combined_index, None)
# ``untile_combined_index`` maps each packed row to a logical tile
@@ -753,14 +772,19 @@ class MiniMaxH3VSAImpl(AttentionImpl):
# the logical prefix, and reject every other shape before a kernel sees it.
n_tiles = attn_metadata.variable_block_sizes.numel()
logical_seq_len = n_tiles * tile_elems
pair_pad_seq_len = logical_seq_len + tile_elems
pair_pad_is_valid = tile_elems in _PTX_BLACKWELL_TILE_ELEMS and n_tiles % 2 != 0
allowed_seq_lengths = (logical_seq_len, pair_pad_seq_len) if pair_pad_is_valid else (logical_seq_len, )
if query.shape[1] not in allowed_seq_lengths:
expected = (f"the logical length {logical_seq_len} or one ptx_blackwell partner tile "
f"({pair_pad_seq_len})" if pair_pad_is_valid else f"the logical length {logical_seq_len}")
raise ValueError(f"VSA-H3 tiled query has length {query.shape[1]}, expected {expected}.")
has_ptx_blackwell_pair = query.shape[1] == pair_pad_seq_len
if regional_compiling:
pair_tiles = query.shape[1] // tile_elems - n_tiles
has_ptx_blackwell_pair = True
else:
pair_pad_seq_len = logical_seq_len + tile_elems
pair_pad_is_valid = tile_elems in _PTX_BLACKWELL_TILE_ELEMS and n_tiles % 2 != 0
allowed_seq_lengths = (logical_seq_len, pair_pad_seq_len) if pair_pad_is_valid else (logical_seq_len, )
if query.shape[1] not in allowed_seq_lengths:
expected = (f"the logical length {logical_seq_len} or one ptx_blackwell partner tile "
f"({pair_pad_seq_len})" if pair_pad_is_valid else f"the logical length {logical_seq_len}")
raise ValueError(f"VSA-H3 tiled query has length {query.shape[1]}, expected {expected}.")
has_ptx_blackwell_pair = query.shape[1] == pair_pad_seq_len
pair_tiles = int(has_ptx_blackwell_pair)
for name, tensor in (("key", key), ("value", value)):
if tensor.shape[1] != query.shape[1]:
raise ValueError(f"VSA-H3 tiled {name} length {tensor.shape[1]} does not match query "
@@ -798,6 +822,8 @@ class MiniMaxH3VSAImpl(AttentionImpl):
if scores is None:
mask = torch.ones(query.shape[0], query.shape[2], n_tiles, n_tiles, dtype=torch.bool, device=query.device)
elif regional_compiling:
mask = _h3_vsa_block_mask(scores)
else:
mask = _build_block_mask(
scores,
@@ -837,10 +863,10 @@ class MiniMaxH3VSAImpl(AttentionImpl):
# The synthetic tile is neither a logical query nor key. Its
# all-False row yields q2k_num=0, the all-False column keeps it
# out of real rows, and vbs=0 masks all of its key slots.
ptx_blackwell_mask = torch.nn.functional.pad(mask, (0, 1, 0, 1), value=False)
ptx_blackwell_mask = torch.nn.functional.pad(mask, (0, pair_tiles, 0, pair_tiles), value=False)
ptx_blackwell_variable_block_sizes = torch.nn.functional.pad(
attn_metadata.variable_block_sizes,
(0, 1),
(0, pair_tiles),
value=0,
)
+9 -2
View File
@@ -1035,8 +1035,15 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
prepared_vsa_impls)
if unsupported_reasons:
return "; ".join(sorted(unsupported_reasons))
self._regional_compile_inputs = True
return None
def _block_input(self, shard: torch.Tensor) -> torch.Tensor:
"""Give a sequence-parallel shard standard strides under regional compile, so block 0 shares one graph."""
if getattr(self, "_regional_compile_inputs", False):
return shard.clone(memory_format=torch.contiguous_format)
return shard
def materialize_non_persistent_buffers(
self,
device: torch.device,
@@ -1136,7 +1143,7 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
text_original_seq_len = text_embeds.shape[1]
sp_world_size = get_sp_world_size() if model_parallel_is_initialized() else 1
if sp_world_size > 1:
text_embeds, _ = sequence_model_parallel_shard(text_embeds, dim=1)
text_embeds = self._block_input(sequence_model_parallel_shard(text_embeds, dim=1)[0])
text_embeds = self.token_refiner(text_embeds, text_original_seq_len)
if sp_world_size > 1:
text_embeds = sequence_model_parallel_all_gather_with_unpad(
@@ -1201,7 +1208,7 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
original_seq_len = sequence_length
if sp_world_size > 1:
packed_hidden_states, _ = sequence_model_parallel_shard(packed_hidden_states, dim=1)
packed_hidden_states = self._block_input(sequence_model_parallel_shard(packed_hidden_states, dim=1)[0])
rotary_cos, _ = sequence_model_parallel_shard(rotary_emb[0], dim=0)
rotary_sin, _ = sequence_model_parallel_shard(rotary_emb[1], dim=0)
adaln_indices, _ = sequence_model_parallel_shard(adaln_indices, dim=0)
@@ -296,10 +296,12 @@ def test_prepared_route_reuses_graph_across_layer_indices_and_preserves_dense_ov
monkeypatch.setattr(vsa_h3, "_ptx_blackwell_unavailable_reason", fail_eager_dispatch)
monkeypatch.setattr(vsa_h3, "block_sparse_attn_64_bhsd", fail_eager_dispatch)
from fastvideo.forward_context import set_forward_context
torch._dynamo.reset()
try:
compiled = [torch.compile(impl.forward, backend="eager", fullgraph=True) for impl in implementations]
with torch.inference_mode():
with torch.inference_mode(), set_forward_context(current_timestep=0, attn_metadata=meta):
for layer_idx, run in enumerate(compiled):
actual = run(q, k, v, None, meta)
expected_delta = 1.0 if layer_idx in meta.dense_layers else 0.0
@@ -664,9 +666,11 @@ def test_real_ptx_blackwell_odd_preprocess_forward_postprocess_fullgraph(env_ove
output = impl.forward(query, key, value, None, meta)
return impl.postprocess_output(output, meta)
from fastvideo.forward_context import set_forward_context
torch._dynamo.reset()
try:
with torch.inference_mode():
with torch.inference_mode(), set_forward_context(current_timestep=0, attn_metadata=meta):
compiled = torch.compile(run, fullgraph=True)
output = compiled(raw_qkv)
torch.cuda.synchronize()
@@ -675,3 +679,125 @@ def test_real_ptx_blackwell_odd_preprocess_forward_postprocess_fullgraph(env_ove
assert output.shape == (1, meta.total_seq_length, _HEADS, _DIM)
assert torch.isfinite(output).all()
def test_regional_compile_reuses_graphs_across_prompt_lengths(monkeypatch, env_overrides):
"""Unseen prompt lengths reuse warm graphs and match the eager route exactly (#1940)."""
from torch._dynamo.testing import CompileCounter
from fastvideo.forward_context import set_forward_context
def mask_count(q, num):
return q + num.repeat_interleave(q.shape[2] // num.shape[-1], dim=-1).unsqueeze(-1).to(q.dtype)
fake_sm = _FakePtxBlackwell(supported=True)
monkeypatch.setattr(fake_sm, "block_sparse_attn_ptx_blackwell",
lambda q, k, v, q2k_idx, q2k_num, vbs, need_lse=True: (mask_count(q, q2k_num), None))
monkeypatch.setattr(fake_sm, "block_sparse_attn_ptx_blackwell_from_mask",
lambda q, k, v, block_map, vbs: (mask_count(q, block_map.sum(-1)), None))
monkeypatch.setattr(vsa_h3, "_ptx_blackwell", fake_sm)
monkeypatch.setattr(vsa_h3, "map_to_index", _fake_map_to_index)
env_overrides.enter_context(envs.FASTVIDEO_VSA_PTX_BLACKWELL.override(True))
monkeypatch.setattr(vsa_h3, "probe_enabled", lambda: None)
impl = MiniMaxH3VSAImpl(num_heads=_HEADS, head_size=_DIM, causal=False, softmax_scale=_DIM**-0.5)
assert impl.prepare_for_regional_compile(torch.device("cpu")) is None
builder = MiniMaxH3VSAMetadataBuilder()
counter = CompileCounter()
def run(raw, meta):
tiled = impl.preprocess_qkv(raw, meta)
query, key, value, gate = tiled.chunk(4, dim=0)
return impl.postprocess_output(impl.forward(query, key, value, gate, meta), meta)
def check(text_rows_list):
parities = set()
for text_rows in text_rows_list:
meta = _build_meta(prefix_segments=(text_rows, 30), sparsity=0.5, builder=builder)
parities.add(meta.variable_block_sizes.numel() % 2)
raw = torch.randn(4, meta.total_seq_length, _HEADS, _DIM, dtype=torch.bfloat16)
with set_forward_context(current_timestep=0, attn_metadata=meta):
actual = compiled(raw, meta)
expected = run(raw.clone(), meta)
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
return parities
torch._dynamo.reset()
try:
compiled = torch.compile(run, backend=counter, fullgraph=True)
with torch.no_grad():
# Dynamo may still specialize on bounded shape classes (tile-count
# parity); once both are warm, unseen lengths must reuse a graph.
assert check((17, 40, 64, 81, 100, 130)) == {0, 1}
warm_graphs = counter.frame_count
assert check((151, 200, 233, 300, 47, 113)) == {0, 1}
assert counter.frame_count == warm_graphs, (
f"unseen prompt lengths compiled {counter.frame_count - warm_graphs} new graphs")
finally:
torch._dynamo.reset()
def test_regional_compile_reuses_graphs_across_sp_padding_lengths(monkeypatch, env_overrides):
"""Sequence lengths that do and don't divide by the SP degree reuse warm graphs (#1940).
``DistributedAttention_VSA`` slices the SP padding off before the backend and pads the output back, so the
padded length (a function of ``total_seq_length % sp``) must not leak into the compiled graph's guards.
"""
from torch._dynamo.testing import CompileCounter
from fastvideo.attention import layer as attn_layer
from fastvideo.forward_context import set_forward_context
sp_degree = 4
def mask_count(q, num):
return q + num.repeat_interleave(q.shape[2] // num.shape[-1], dim=-1).unsqueeze(-1).to(q.dtype)
fake_sm = _FakePtxBlackwell(supported=True)
monkeypatch.setattr(fake_sm, "block_sparse_attn_ptx_blackwell",
lambda q, k, v, q2k_idx, q2k_num, vbs, need_lse=True: (mask_count(q, q2k_num), None))
monkeypatch.setattr(fake_sm, "block_sparse_attn_ptx_blackwell_from_mask",
lambda q, k, v, block_map, vbs: (mask_count(q, block_map.sum(-1)), None))
monkeypatch.setattr(vsa_h3, "_ptx_blackwell", fake_sm)
monkeypatch.setattr(vsa_h3, "map_to_index", _fake_map_to_index)
monkeypatch.setattr(attn_layer, "sequence_model_parallel_all_to_all_4D", lambda x, **_: x)
env_overrides.enter_context(envs.FASTVIDEO_VSA_PTX_BLACKWELL.override(True))
monkeypatch.setattr(vsa_h3, "probe_enabled", lambda: None)
impl = MiniMaxH3VSAImpl(num_heads=_HEADS, head_size=_DIM, causal=False, softmax_scale=_DIM**-0.5)
assert impl.prepare_for_regional_compile(torch.device("cpu")) is None
layer = attn_layer.DistributedAttention_VSA.__new__(attn_layer.DistributedAttention_VSA)
torch.nn.Module.__init__(layer)
layer.attn_impl = impl
layer._compile_forward_enabled = True
builder = MiniMaxH3VSAMetadataBuilder()
counter = CompileCounter()
def run(q, k, v, total):
return layer(q, k, v, total)[0]
def check(text_rows_list):
pads = set()
for text_rows in text_rows_list:
meta = _build_meta(prefix_segments=(text_rows, 30), sparsity=0.5, builder=builder)
total = meta.total_seq_length
pad = -total % sp_degree
pads.add(pad)
q, k, v = (torch.randn(1, total + pad, _HEADS, _DIM, dtype=torch.bfloat16) for _ in range(3))
with set_forward_context(current_timestep=0, attn_metadata=meta):
actual = compiled(q, k, v, total)
expected = run(q, k, v, total)
assert actual.shape == (1, total + pad, _HEADS, _DIM)
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
return pads
torch._dynamo.reset()
try:
compiled = torch.compile(run, backend=counter, fullgraph=True)
with torch.no_grad():
warm_pads = check((17, 40, 64, 81, 100, 130))
assert 0 in warm_pads and len(warm_pads) > 1, "need lengths that do and don't divide by the SP degree"
warm_graphs = counter.frame_count
check((151, 200, 233, 300, 47, 113, 18, 19, 20))
assert counter.frame_count == warm_graphs, (
f"unseen padded lengths compiled {counter.frame_count - warm_graphs} new graphs")
finally:
torch._dynamo.reset()