Skip to content

Commit 8b2bde2

Browse files
committed
[bugfix]: share compiled VSA graphs across H3 layers
1 parent d84eb58 commit 8b2bde2

4 files changed

Lines changed: 110 additions & 24 deletions

File tree

fastvideo/attention/backends/video_sparse_attn_h3.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -442,13 +442,22 @@ def __init__(
442442
self.prefix = prefix
443443
self.layer_idx = layer_idx_from_prefix(prefix, default=-1)
444444
self.head_size = head_size
445+
# Generic torch.compile must not specialize the shared VSA forward on
446+
# the Python ``layer_idx`` value of each of H3's 50 blocks. This
447+
# tensor is prepared after weights load and drives only the compiled
448+
# dense-layer decision; it does not opt the module into sm_100a.
449+
self._compile_layer_idx: torch.Tensor | None = None
445450
# None means the regional-compile preparation hook has not run. The
446451
# eager path deliberately ignores this cache and preserves its
447452
# request-time env/probe/fallback behavior; only Dynamo capture reads
448453
# the prepared, static route.
449454
self._regional_compile_sm100a_enabled: bool | None = None
450455
self._regional_compile_layer_idx: torch.Tensor | None = None
451456

457+
def prepare_for_compile(self, device: torch.device) -> None:
458+
"""Tensorize per-layer state shared by every torch.compile route."""
459+
self._compile_layer_idx = torch.tensor(self.layer_idx, device=device, dtype=torch.int64)
460+
452461
def prepare_for_regional_compile(self, device: torch.device) -> str | None:
453462
"""Resolve the inference-only sm_100a route before fullgraph capture.
454463
@@ -459,6 +468,7 @@ def prepare_for_regional_compile(self, device: torch.device) -> str | None:
459468
the loaded model's device now, then let ``forward`` specialize on the
460469
resulting plain bool while Dynamo is compiling.
461470
"""
471+
self.prepare_for_compile(device)
462472
requested = os.environ.get(VSA_SM100A_ENV, "0") == "1"
463473
enabled = False
464474
reason = None if requested else f"{VSA_SM100A_ENV}=1 is required for compile-safe VSA-H3 attention"
@@ -603,13 +613,14 @@ def forward( # type: ignore[override]
603613
logical_gate = gate_compress[:, :logical_seq_len] if gate_compress is not None else None
604614

605615
# Probe-guided per-layer opt-out: diffuse layers run dense (all-True
606-
# mask) while the rest keep the configured sparsity. During regional
607-
# capture, keep the layer decision tensor-valued so the 50 block
608-
# instances reuse one graph instead of specializing on layer_idx.
616+
# mask) while the rest keep the configured sparsity. During any
617+
# prepared capture, keep the layer decision tensor-valued so the 50
618+
# block instances reuse one graph instead of specializing on the
619+
# Python layer_idx attribute.
609620
force_dense = None
610-
if regional_compiling:
611-
assert self._regional_compile_layer_idx is not None
612-
force_dense = (attn_metadata.dense_layers_tensor == self._regional_compile_layer_idx).any()
621+
compile_layer_idx = self._compile_layer_idx if compiling else None
622+
if compile_layer_idx is not None:
623+
force_dense = (attn_metadata.dense_layers_tensor == compile_layer_idx).any()
613624
layer_sparsity = attn_metadata.VSA_sparsity
614625
else:
615626
layer_sparsity = 0.0 if self.layer_idx in attn_metadata.dense_layers else attn_metadata.VSA_sparsity

fastvideo/models/dits/minimax_h3.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -733,31 +733,49 @@ def __init__(self, config: MiniMaxH3Config, hf_config: dict[str, Any]) -> None:
733733
)
734734
self.__post_init__()
735735

736+
@staticmethod
737+
def _compile_setup_device(attention: MiniMaxH3Attention) -> torch.device:
738+
"""Return the loaded device even when FP8 replaced the query weight."""
739+
query_state = next(attention.to_q.parameters(), None)
740+
if query_state is None:
741+
query_state = next(attention.to_q.buffers(), None)
742+
if query_state is None:
743+
raise RuntimeError("MiniMax H3 to_q has no materialized parameter or buffer for compile setup.")
744+
return query_state.device
745+
736746
def prepare_for_compile(self) -> None:
737747
"""Pipeline hook, called once right before torch.compile wraps the blocks.
738748
739-
Resolve each loaded VSA compression gate eagerly. Generic and training
740-
compile retain their established attention dispatch; only the
741-
inference loader's separate ``prepare_for_regional_compile`` hook may
742-
preselect the inference-only sm_100a path.
749+
Resolve each loaded VSA compression gate eagerly and tensorize its
750+
layer identity so repeated blocks share one Dynamo graph. Generic and
751+
training compile retain their established attention dispatch; only
752+
the inference loader's separate ``prepare_for_regional_compile`` hook
753+
may preselect the inference-only sm_100a path.
743754
744755
The inference-only Triton fusions expose fake-backed custom operators,
745756
so Dynamo can keep them active as opaque nodes inside each fullgraph
746757
block instead of tracing into their launcher implementation.
747758
"""
748759
gate_states: list[bool] = []
760+
prepared_vsa_impls = 0
749761
for block in self.transformer_blocks:
750762
attention = block.attn
751763
if attention.to_gate_compress is not None:
752764
attention._resolve_gate_compress_for_compile()
753765
assert attention._gate_compress_active is not None
754766
gate_states.append(attention._gate_compress_active)
767+
prepare_vsa = getattr(attention.distributed_attention.attn_impl, "prepare_for_compile", None)
768+
if callable(prepare_vsa):
769+
prepare_vsa(self._compile_setup_device(attention))
770+
prepared_vsa_impls += 1
755771
if gate_states:
756772
logger.info(
757773
"Resolved MiniMax H3 VSA compression gates before torch.compile: %d active, %d inactive",
758774
sum(gate_states),
759775
len(gate_states) - sum(gate_states),
760776
)
777+
if prepared_vsa_impls:
778+
logger.info("Prepared %d MiniMax H3 VSA layer indices for torch.compile", prepared_vsa_impls)
761779
if self.enabled_fusions:
762780
logger.info(
763781
"MiniMax H3 inference fusions remain active under torch.compile through custom-op boundaries: %s",
@@ -774,14 +792,7 @@ def prepare_for_regional_compile(self) -> str | None:
774792
prepare_vsa = getattr(attention.distributed_attention.attn_impl, "prepare_for_regional_compile", None)
775793
if not callable(prepare_vsa):
776794
continue
777-
# Post-load FP8 conversion may replace to_q.weight with packed
778-
# buffers. Either representation identifies the local device.
779-
query_state = next(attention.to_q.parameters(), None)
780-
if query_state is None:
781-
query_state = next(attention.to_q.buffers(), None)
782-
if query_state is None:
783-
raise RuntimeError("MiniMax H3 to_q has no materialized parameter or buffer for compile setup.")
784-
unsupported = prepare_vsa(query_state.device)
795+
unsupported = prepare_vsa(self._compile_setup_device(attention))
785796
if unsupported:
786797
unsupported_reasons.add(str(unsupported))
787798
prepared_vsa_impls += 1

fastvideo/tests/attention/test_vsa_h3_sm100a_route.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,61 @@ def compile_safe_from_mask(q, k, v, block_map, variable_block_sizes):
308308
torch._dynamo.reset()
309309

310310

311+
def test_generic_compile_reuses_graph_across_layer_indices_and_stays_on_triton(monkeypatch):
312+
"""Pipeline compile must share one graph without selecting sm_100a."""
313+
fake_sm = _FakeSm100a(supported=True)
314+
monkeypatch.setattr(vsa_h3, "_sm100a", fake_sm)
315+
monkeypatch.setenv(VSA_SM100A_ENV, "1")
316+
monkeypatch.setattr(vsa_h3, "probe_enabled", lambda: None)
317+
meta = _build_meta(sparsity=0.5, dense_layers=(0, 17), prefix_segments=(64, 64))
318+
q, k, v = _tiled_qkv(meta)
319+
320+
def fake_triton(q, k, v, block_map, variable_block_sizes):
321+
del k, v, variable_block_sizes
322+
return q + block_map.all().to(q.dtype), None
323+
324+
def fail_sm100a(*args, **kwargs):
325+
raise AssertionError("generic torch.compile unexpectedly selected sm_100a")
326+
327+
monkeypatch.setattr(vsa_h3, "block_sparse_attn_64_bhsd", fake_triton)
328+
monkeypatch.setattr(fake_sm, "is_supported", fail_sm100a)
329+
monkeypatch.setattr(fake_sm, "block_sparse_attn_sm100a", fail_sm100a)
330+
monkeypatch.setattr(fake_sm, "block_sparse_attn_sm100a_from_mask", fail_sm100a)
331+
monkeypatch.setattr(vsa_h3, "_sm100a_unavailable_reason", fail_sm100a)
332+
333+
implementations = []
334+
for layer_idx in range(20):
335+
impl = MiniMaxH3VSAImpl(
336+
num_heads=_HEADS,
337+
head_size=_DIM,
338+
causal=False,
339+
softmax_scale=_DIM**-0.5,
340+
prefix=f"transformer_blocks.{layer_idx}.attn",
341+
)
342+
impl.prepare_for_compile(torch.device("cpu"))
343+
implementations.append(impl)
344+
345+
compiled_graphs = []
346+
347+
def recording_backend(graph_module, _example_inputs):
348+
compiled_graphs.append(graph_module)
349+
return graph_module.forward
350+
351+
torch._dynamo.reset()
352+
try:
353+
compiled = [torch.compile(impl.forward, backend=recording_backend, fullgraph=True)
354+
for impl in implementations]
355+
with torch.inference_mode():
356+
for layer_idx, run in enumerate(compiled):
357+
actual = run(q, k, v, None, meta)
358+
expected_delta = 1.0 if layer_idx in meta.dense_layers else 0.0
359+
torch.testing.assert_close(actual, q + expected_delta, atol=0, rtol=0)
360+
finally:
361+
torch._dynamo.reset()
362+
363+
assert len(compiled_graphs) == 1
364+
365+
311366
def test_default_off_routes_triton(routed, monkeypatch):
312367
fake_sm, fake_triton, run, _ = routed
313368
monkeypatch.delenv(VSA_SM100A_ENV, raising=False)

fastvideo/tests/inference/test_inference_regional_compile.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,15 @@ def test_h3_vsa_probe_degrades_regional_compile_to_eager(monkeypatch) -> None:
106106
class _RegionalPrepareProbe:
107107

108108
def __init__(self, unsupported: str | None = None) -> None:
109-
self.devices: list[torch.device] = []
109+
self.compile_devices: list[torch.device] = []
110+
self.regional_devices: list[torch.device] = []
110111
self.unsupported = unsupported
111112

113+
def prepare_for_compile(self, device: torch.device) -> None:
114+
self.compile_devices.append(device)
115+
112116
def prepare_for_regional_compile(self, device: torch.device) -> str | None:
113-
self.devices.append(device)
117+
self.regional_devices.append(device)
114118
return self.unsupported
115119

116120

@@ -157,7 +161,8 @@ def test_minimax_h3_prepare_for_compile_resolves_loaded_vsa_gates() -> None:
157161
assert [block.attn._gate_compress_active for block in model.transformer_blocks] == [False, True]
158162
for block in model.transformer_blocks:
159163
impl = block.attn.distributed_attention.attn_impl
160-
assert impl.devices == []
164+
assert impl.compile_devices == [next(block.parameters()).device]
165+
assert impl.regional_devices == []
161166

162167

163168
def test_training_compile_prepare_does_not_probe_inference_kernel() -> None:
@@ -168,7 +173,9 @@ def test_training_compile_prepare_does_not_probe_inference_kernel() -> None:
168173
assert reason is None
169174
attention = model.transformer_blocks[0].attn
170175
assert attention._gate_compress_active is True
171-
assert attention.distributed_attention.attn_impl.devices == []
176+
impl = attention.distributed_attention.attn_impl
177+
assert impl.compile_devices == [next(model.parameters()).device]
178+
assert impl.regional_devices == []
172179

173180

174181
def test_regional_compile_prepare_prefers_specialized_hook() -> None:
@@ -179,7 +186,8 @@ def test_regional_compile_prepare_prefers_specialized_hook() -> None:
179186

180187
assert reason is None
181188
impl = model.transformer_blocks[0].attn.distributed_attention.attn_impl
182-
assert impl.devices == [expected_device]
189+
assert impl.compile_devices == [expected_device]
190+
assert impl.regional_devices == [expected_device]
183191

184192

185193
def test_minimax_h3_prepare_for_regional_compile_does_not_require_quantized_q_weight() -> None:
@@ -193,7 +201,8 @@ def test_minimax_h3_prepare_for_regional_compile_does_not_require_quantized_q_we
193201

194202
assert reason is None
195203
impl = attention.distributed_attention.attn_impl
196-
assert impl.devices == [expected_device]
204+
assert impl.compile_devices == [expected_device]
205+
assert impl.regional_devices == [expected_device]
197206

198207

199208
def test_minimax_h3_prepare_for_regional_compile_propagates_backend_rejection() -> None:

0 commit comments

Comments
 (0)