[Attention] [P1] Keep H3 VSA policy in the model and share sparse primitives (#7535)

Signed-off-by: lishunyang12 <lishunyang12@users.noreply.github.com>
Co-authored-by: lishunyang12 <lishunyang12@users.noreply.github.com>
This commit is contained in:
SYLARandlishunyang12 authored and GitHub committed 2026-09-16 15:47:31 +08:00
1 parent 99ff4f3070
commit 507cb1d834
19 files changed
+1117 -473

No files matched your search

@@ -76,6 +76,34 @@ preserve common `self` or `cross` policies. A model also decides whether its
path is compatible with automatic TRTLLM selection, because only the model
knows whether masking and packing satisfy the kernel contract.
### Model-owned implementation specializations
Use `Attention(..., impl_overrides={backend_name: implementation_class})`
when a model needs different computation within an existing backend contract.
The model defines the implementation in its own directory and passes it when
constructing the layer:
```python
# MyModelVSAImpl is a model-owned subclass of FastVideoVSAImpl.
self.attention = Attention(
num_heads=num_heads,
head_size=head_size,
softmax_scale=head_size**-0.5,
causal=False,
role="self",
impl_overrides={"FASTVIDEO_VSA": MyModelVSAImpl},
)
```
The override is applied after role/platform selection, keyed by the selected
backend's `get_name()`. It must subclass the **selected implementation**;
incompatible platform implementations raise `TypeError`. Unselected overrides
have no effect. Backend capabilities, constructor options, and shared
parallel dispatch remain unchanged and must be honored by the specialization.
Use a new backend for a new provider/capability contract. `custom_attention`
instead owns communication and requires `skip_sequence_parallel=True`.
## Adding or changing a backend
An implementation change is complete only when it:
@@ -91,8 +119,9 @@ An implementation change is complete only when it:
## FastVideo VSA model metadata
FastVideo VSA support is scoped to `FastVideo/FastWan2.2-TI2V-5B-Diffusers`, whose text-to-video and image-to-video modes both use `Wan22Pipeline`. It operates on a flattened DiT sequence but partitions tokens in
the original latent video grid. Wan integrations therefore attach the
FastWan's text-to-video and image-to-video modes both use `Wan22Pipeline`.
It operates on a flattened DiT sequence but partitions tokens in the original
latent video grid. Wan integrations therefore attach the
post-patch `(T, H, W)` shape as `vsa_dit_seq_shape` attention metadata. The
backend validates that `T * H * W` equals the sequence length, derives the
runtime block count, and selects the configured top-k key/value blocks for
@@ -104,3 +133,8 @@ the checkpoint contains those weights. Checkpoints without the projection do
not allocate or execute it. When top-k selects every block, native checkpoints
route to SDPA, while FastVideo DMD checkpoints preserve the VSA all-block path
to retain checkpoint semantics.
MiniMax-H3 supplies `MiniMaxH3VSAImpl` through `impl_overrides`.
`models/minimax_h3/attention/fastvideo_h3.py` owns its prefix layout, video tiling, and
learned compression gate. Pooling, block-map construction, and tile64 provider
calls are shared through `attention/ops/block_sparse.py`.
@@ -1,24 +1,33 @@
# FastVideo VSA
FastVideo Variable Sparse Attention (VSA) accelerates the `FastVideo/FastWan2.2-TI2V-5B-Diffusers` model's
self-attention by partitioning the post-patch latent grid into spatiotemporal
FastVideo Variable Sparse Attention (VSA) partitions the post-patch latent grid into spatiotemporal
blocks. For every query block, VSA scores the key/value blocks and computes
attention only against the selected top-k blocks.
The supported checkpoint provides both text-to-video and image-to-video modes through `Wan22Pipeline`; the separate Wan I2V-14B, S2V, and VACE pipelines are outside this backend's supported scope.
## Supported models
MiniMax-H3 served with a FastH3 VSA adapter uses a second, H3-specific route
through the same backend. See the
[MiniMax-H3 recipe](https://recipes.vllm.ai/MiniMaxAI/MiniMax-H3) for its geometry,
supported topologies, and commands.
| Model / checkpoint | Required adapter | Tasks | Sequence parallelism |
| --- | --- | --- | --- |
| `FastVideo/FastWan2.2-TI2V-5B-Diffusers` | None | T2V, I2V through `Wan22Pipeline` | Disabled |
| `MiniMaxAI/MiniMax-H3` | [FastH3 VSA (recipe)](https://github.com/vllm-project/vllm-omni/blob/main/recipes/MiniMaxAI/MiniMax-H3.md#fasth3-adapter) | T2VA | Disabled or pure Ulysses |
| Wan I2V-14B, S2V, VACE | — | Unsupported | — |
VSA is a CUDA-only, explicitly selected backend. It requires the
`fastvideo-kernel` package and currently supports non-causal self-attention
with equal query and key/value sequence lengths. Unsupported shapes, masks,
dtypes, sequence-parallel execution, or kernel failures fall back to
`TORCH_SDPA` and emit a warning with the reason. On the Wan route, an active
sequence-parallel context is one of those fallbacks; the H3 route supports pure
Ulysses and rejects ring or all-gather sequence parallelism at startup.
## Installation
Install vLLM-Omni from this checkout with the `vsa` extra:
```bash
uv pip install -e '.[vsa]'
```
The extra installs the tested kernel dependency automatically. Prebuilt kernels require
Linux, Python 3.12, and glibc 2.34 or newer (x86-64 or aarch64). The full
FastVideo framework and provider environment variables are not required.
`FASTVIDEO_VSA` selects the attention algorithm. On SM120, the H3 integration
currently executes FastVideo's 64-token Triton block-sparse kernel; it does not
dispatch to FlashInfer. The `vsa` extra installs `fastvideo-kernel==0.3.4` for
this execution path.
## Enable the backend
@@ -27,24 +36,18 @@ flag. Use `--fastvideo-vsa-topk` to set the number of key/value blocks retained
for every query block:
```bash
vllm-omni serve <model> \
vllm serve <model> --omni \
--diffusion-attention-backend FASTVIDEO_VSA \
--fastvideo-vsa-topk 64
```
The backwards-compatible environment variable selects the backend with the
default `topk=64`:
<details markdown="1">
<summary>Alternative configuration formats</summary>
Equivalent structured configuration:
```bash
export DIFFUSION_ATTENTION_BACKEND=FASTVIDEO_VSA
vllm-omni serve <model>
```
To tune top-k, pass the CLI backend and top-k flags together as shown above,
or use the equivalent structured configuration:
```bash
vllm-omni serve <model> \
vllm serve <model> --omni \
--diffusion-attention-config \
'{"default":{"backend":"FASTVIDEO_VSA","fastvideo_vsa_topk":64}}'
```
@@ -73,8 +76,15 @@ stages:
fastvideo_vsa_topk: 64
```
</details>
## Choose top-k
H3 uses 64-token video blocks and keeps
all prefix blocks; see the [FastH3 VSA recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/MiniMaxAI/MiniMax-H3.md#fasth3-adapter).
### Wan top-k behavior
At runtime the backend logs the sequence shape and derived block count:
```text
@@ -117,8 +127,16 @@ selecting VSA does not turn a native checkpoint into a distilled model.
## Verify routing and fallback
Check the startup and first-forward logs instead of assuming that selecting
the backend guarantees sparse execution:
VSA is a CUDA-only, explicitly selected backend. It requires the
`fastvideo-kernel` package and currently supports non-causal self-attention
with equal query and key/value sequence lengths. Unsupported shapes, masks,
dtypes, sequence-parallel execution, or kernel failures fall back to
`TORCH_SDPA` and emit a warning with the reason. On the Wan route, an active
sequence-parallel context is one of those fallbacks; the H3 route supports pure
Ulysses and rejects ring or all-gather sequence parallelism at startup.
H3 accelerator faults propagate instead of attempting dense recovery.
On the Wan route, check the startup and first-forward logs:
- `route=VSA` means top-k block selection is active.
- `route=VSA_ALL_BLOCKS` means the FastVideo DMD checkpoint retained all
@@ -126,6 +144,10 @@ the backend guarantees sparse execution:
- `route=SDPA` or `FASTVIDEO_VSA falling back to SDPA: ...` means dense SDPA
executed; the warning includes the reason.
For H3, check `FastH3 adapter active` at startup and
`FASTVIDEO_VSA H3 routing` during DiT execution, as described in the
[model recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/MiniMaxAI/MiniMax-H3.md#fasth3-adapter).
The Wan route requires CUDA tensors in FP16 or BF16, 256-token blocks,
standard `head_size**-0.5` scaling, equal Q/K/V head counts, no attention mask,
and no active sequence-parallel context. The MiniMax-H3 route uses 64-token
+5
View File
@@ -123,6 +123,11 @@ fa4 = [
"flash-attn-4[cu13]==4.0.0b18",
]
# FastVideo VSA kernels for the supported Linux/Python 3.12 wheel environment.
vsa = [
"fastvideo-kernel==0.3.4",
]
docs = [
"mkdocs>=1.5.0",
"mkdocs-api-autonav",
@@ -0,0 +1,126 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import sys
import types
import pytest
import torch
from torch._subclasses.fake_tensor import FakeTensorMode
from vllm_omni.diffusion.attention.ops.block_sparse import (
build_prefix_dense_block_map,
fastvideo_block_sparse_attn_bshd,
mean_pool_tiles,
)
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@pytest.mark.parametrize("prefix_blocks,topk", [(0, 1), (2, 0), (2, 1), (2, 3), (2, 5)])
def test_block_map_keeps_prefix_dense_and_selects_non_prefix_keys(prefix_blocks, topk):
sparse_blocks = 3
blocks = prefix_blocks + sparse_blocks
scores = torch.arange(2 * blocks * blocks, dtype=torch.float32).reshape(1, 2, blocks, blocks)
block_map = build_prefix_dense_block_map(scores, prefix_blocks, sparse_blocks, topk)
expected = torch.zeros_like(scores, dtype=torch.bool)
# Scores increase with key index, so top-k always selects the last keys.
for query_block in range(blocks):
for key_block in range(blocks):
if (
query_block < prefix_blocks
or key_block < prefix_blocks
or key_block >= blocks - min(topk, sparse_blocks)
):
expected[..., query_block, key_block] = True
assert torch.equal(block_map, expected)
@pytest.mark.parametrize("block_size", [4, 32, 64])
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
def test_pooling_uses_valid_tile_sizes_and_fp32_accumulation(block_size, dtype):
sizes = torch.tensor([block_size, block_size - 1, 0], dtype=torch.int32)
tiles = torch.arange(2 * 3 * block_size * 2 * 3).reshape(2, 3, block_size, 2, 3).to(dtype)
tiles[:, 1, -1] = 0
tiles[:, 2] = 0
expected = torch.stack(
[tiles[:, 0].float().mean(dim=1), tiles[:, 1, :-1].float().mean(dim=1), torch.zeros(2, 2, 3)], dim=2
)
result = mean_pool_tiles(tiles.flatten(1, 2), sizes, block_size)
assert result.dtype == torch.float32
torch.testing.assert_close(result, expected, rtol=0, atol=0)
def test_provider_wrapper_restores_transport_rows_and_bshd_layout(monkeypatch):
monkeypatch.delenv("FASTVIDEO_VSA_SM100A", raising=False)
calls = []
def block_sparse_attn(q, k, v, block_map, sizes):
calls.append((q.shape, block_map.shape, sizes.tolist()))
assert q.is_contiguous() and k.is_contiguous() and v.is_contiguous()
assert sizes.dtype == torch.int32
return q + k + v, None
module = types.ModuleType("fastvideo_kernel.block_sparse_attn")
setattr(module, "block_sparse_attn", block_sparse_attn)
monkeypatch.setitem(sys.modules, "fastvideo_kernel", types.ModuleType("fastvideo_kernel"))
monkeypatch.setitem(sys.modules, "fastvideo_kernel.block_sparse_attn", module)
query = torch.randn(1, 4 * 64, 2, 8)
block_map = torch.ones(1, 2, 4, 4, dtype=torch.bool)
sizes = torch.tensor([64, 17, 64, 0])
output = fastvideo_block_sparse_attn_bshd(query, query, query, block_map, sizes, 3)
expected = torch.zeros_like(query)
expected[:, : 3 * 64] = query[:, : 3 * 64] * 3
torch.testing.assert_close(output, expected)
assert calls == [(torch.Size([1, 2, 3 * 64, 8]), torch.Size([1, 2, 3, 3]), [64, 17, 64])]
def test_custom_op_fake_preserves_metadata_without_loading_provider(monkeypatch):
monkeypatch.setitem(sys.modules, "fastvideo_kernel", None)
with FakeTensorMode():
query = torch.empty(1, 4 * 64, 2, 8, dtype=torch.bfloat16)
block_map = torch.empty(1, 2, 4, 4, dtype=torch.bool)
sizes = torch.empty(4, dtype=torch.int32)
output = fastvideo_block_sparse_attn_bshd(query, query, query, block_map, sizes, 3)
assert output.shape == query.shape
assert output.dtype == query.dtype
assert output.device == query.device
@pytest.mark.parametrize("error_type", [ImportError, RuntimeError, torch.AcceleratorError])
def test_native_provider_failure_only_recovers_without_accelerator_fault(monkeypatch, mocker, error_type):
monkeypatch.setenv("FASTVIDEO_VSA_SM100A", "1")
error = error_type("native provider failure")
native = mocker.Mock()
native.is_supported.return_value = True
native.block_sparse_attn_sm100a.side_effect = error
provider = types.ModuleType("fastvideo_kernel")
setattr(provider, "block_sparse_attn_sm100a", native)
index = types.ModuleType("fastvideo_kernel.triton_kernels.index")
setattr(
index,
"map_to_index",
mocker.Mock(return_value=(torch.zeros(1, dtype=torch.int32), torch.ones(1, dtype=torch.int32))),
)
triton = types.ModuleType("fastvideo_kernel.block_sparse_attn")
fallback = mocker.Mock(side_effect=lambda q, *args: (q.clone(), None))
setattr(triton, "block_sparse_attn", fallback)
monkeypatch.setitem(sys.modules, "fastvideo_kernel", provider)
monkeypatch.setitem(
sys.modules, "fastvideo_kernel.triton_kernels", types.ModuleType("fastvideo_kernel.triton_kernels")
)
monkeypatch.setitem(sys.modules, "fastvideo_kernel.triton_kernels.index", index)
monkeypatch.setitem(sys.modules, "fastvideo_kernel.block_sparse_attn", triton)
query = torch.arange(128 * 2 * 8, dtype=torch.float32).reshape(1, 128, 2, 8)
block_map = torch.ones(1, 2, 2, 2, dtype=torch.bool)
sizes = torch.tensor([64, 64], dtype=torch.int32)
if error_type is torch.AcceleratorError:
with pytest.raises(torch.AcceleratorError, match="native provider failure") as raised:
fastvideo_block_sparse_attn_bshd(query, query, query, block_map, sizes, 2)
assert raised.value is error
fallback.assert_not_called()
else:
output = fastvideo_block_sparse_attn_bshd(query, query, query, block_map, sizes, 2)
torch.testing.assert_close(output, query, rtol=0, atol=0)
fallback.assert_called_once()
+3 -161
View File
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import math
import sys
import types
from typing import Any
@@ -12,14 +11,10 @@ import torch
from vllm_omni.diffusion.attention.backends.abstract import (
AttentionMetadata,
PackedPaddingMetadata,
VideoTokenLayout,
VideoTokenSpan,
)
from vllm_omni.diffusion.attention.backends.fastvideo_vsa import (
FastVideoVSABackend,
FastVideoVSAImpl,
_build_h3_block_map,
_get_h3_tile_metadata,
)
from vllm_omni.diffusion.attention.backends.registry import (
DiffusionAttentionBackendEnum,
@@ -218,144 +213,8 @@ def test_fastvideo_vsa_allows_topk_equal_to_num_blocks():
assert too_many._fallback_reason(query, query, query, metadata) == "topk 3 > num_blocks 2"
def _h3_metadata(prefix_segments, video_shape, *, gate=None, packed_padding=None):
extra: dict[str, Any] = {
"vsa_h3_prefix_segments": prefix_segments,
}
if gate is not None:
extra["gate_compress"] = gate
return AttentionMetadata(
packed_padding=packed_padding,
video_layout=VideoTokenLayout(
used_len=sum(prefix_segments) + math.prod(video_shape),
video_spans=(VideoTokenSpan(start=sum(prefix_segments), latent_grid=video_shape, role="target"),),
),
extra=extra,
)
def _fake_block_sparse_kernel(monkeypatch, calls):
"""Stand in for fastvideo_kernel.block_sparse_attn with a mask-obeying SDPA."""
def block_sparse_attn(q, k, v, block_map, variable_block_sizes):
calls["block_map"] = block_map.clone()
calls["variable_block_sizes"] = variable_block_sizes.tolist()
calls["q_shape"] = tuple(q.shape)
blocks = block_map.shape[-1]
assert blocks * 64 == q.shape[2]
# Expand the per-block map to a per-token mask so the stand-in honours
# the sparsity contract the real kernel implements, and drop the rows
# each partial block pads with, which the real kernel skips through
# variable_block_sizes.
token_mask = block_map.repeat_interleave(64, dim=-1).repeat_interleave(64, dim=-2)
within = torch.arange(64, device=q.device)[None, :] < variable_block_sizes[:, None]
token_mask = token_mask & within.reshape(1, 1, 1, -1)
out = torch.nn.functional.scaled_dot_product_attention(q.float(), k.float(), v.float(), attn_mask=token_mask)
return out.to(q.dtype), None
module = types.ModuleType("fastvideo_kernel.block_sparse_attn")
setattr(module, "block_sparse_attn", block_sparse_attn)
monkeypatch.setitem(sys.modules, "fastvideo_kernel", types.ModuleType("fastvideo_kernel"))
monkeypatch.setitem(sys.modules, "fastvideo_kernel.block_sparse_attn", module)
def _h3_impl(**backend_kwargs):
kwargs = {"topk": 1, "min_seq_len": 1, "disable_when_sp_active": False}
kwargs.update(backend_kwargs)
return FastVideoVSAImpl(num_heads=2, head_size=8, softmax_scale=8**-0.5, backend_kwargs=kwargs)
def test_h3_forward_tiles_untiles_and_restores_the_original_row_order(monkeypatch):
calls: dict[str, Any] = {}
_fake_block_sparse_kernel(monkeypatch, calls)
impl = _h3_impl(topk=64)
prefix_segments, video_shape = (5, 70), (2, 4, 4)
seq_len = sum(prefix_segments) + 2 * 4 * 4
torch.manual_seed(0)
query = torch.randn(1, seq_len, 2, 8, dtype=torch.bfloat16)
output = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape))
assert output.shape == query.shape
# topk covers every video block, so the block map is dense and the tiled
# kernel must reproduce plain attention over the same rows.
reference = torch.nn.functional.scaled_dot_product_attention(
*(x.float().transpose(1, 2) for x in (query, query, query))
).transpose(1, 2)
torch.testing.assert_close(output.float(), reference, atol=2e-2, rtol=2e-2)
# 5 + 70 splits into segment-pure 5 | 64 | 6, then one (2, 4, 4) video tile.
assert calls["variable_block_sizes"] == [5, 64, 6, 32]
def test_h3_forward_keeps_prefix_dense_and_selects_top_k_video_tiles(monkeypatch):
calls: dict[str, Any] = {}
_fake_block_sparse_kernel(monkeypatch, calls)
impl = _h3_impl(topk=1)
prefix_segments, video_shape = (5,), (8, 4, 4)
seq_len = 5 + 8 * 4 * 4
torch.manual_seed(0)
query = torch.randn(1, seq_len, 2, 8, dtype=torch.bfloat16)
impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape))
# 1 prefix block + 2 video blocks. The odd count is padded to an even one
# for the paired-CTA kernel contract, and the Triton route slices that
# transport-only partner off again before launching.
block_map = calls["block_map"]
assert block_map.shape[-1] == 3
assert calls["variable_block_sizes"] == [5, 64, 64]
assert block_map[:, :, :1, :].all(), "prefix queries stay dense"
assert block_map[..., :1].all(), "prefix keys are exempt from selection"
assert (block_map[:, :, 1:, 1:].sum(dim=-1) == 1).all(), "one video tile per video query"
# The same odd geometry must still untile back onto the original rows.
dense = _h3_impl(topk=64).forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape))
reference = torch.nn.functional.scaled_dot_product_attention(
*(x.float().transpose(1, 2) for x in (query, query, query))
).transpose(1, 2)
torch.testing.assert_close(dense.float(), reference, atol=2e-2, rtol=2e-2)
def test_h3_forward_applies_the_learned_compression_gate(monkeypatch):
_fake_block_sparse_kernel(monkeypatch, {})
impl = _h3_impl(topk=64)
prefix_segments, video_shape = (5,), (2, 4, 4)
seq_len = 5 + 2 * 4 * 4
torch.manual_seed(0)
query = torch.randn(1, seq_len, 2, 8, dtype=torch.bfloat16)
without_gate = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape))
zero_gate = torch.zeros_like(query)
with_zero_gate = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape, gate=zero_gate))
live_gate = torch.full_like(query, 0.5)
with_live_gate = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape, gate=live_gate))
torch.testing.assert_close(with_zero_gate.float(), without_gate.float())
assert not torch.allclose(with_live_gate.float(), without_gate.float())
def test_h3_forward_restores_rows_the_packed_padding_excludes(monkeypatch):
_fake_block_sparse_kernel(monkeypatch, {})
impl = _h3_impl(topk=64)
prefix_segments, video_shape = (5,), (2, 4, 4)
valid = 5 + 2 * 4 * 4
torch.manual_seed(0)
query = torch.randn(1, valid + 24, 2, 8, dtype=torch.bfloat16)
padding = PackedPaddingMetadata(
q_length=valid,
kv_length=valid,
cu_seqlens_q=torch.tensor([0, valid], dtype=torch.int32),
cu_seqlens_k=torch.tensor([0, valid], dtype=torch.int32),
)
output = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape, packed_padding=padding))
assert output.shape == query.shape
assert torch.count_nonzero(output[:, valid:]) == 0
def test_sdpa_fallback_never_attends_the_structural_padding(monkeypatch):
# The backend advertises supports_packed_mask_free, so MiniMax-H3 skips
# The backend advertises supports_packed_mask_free, so a model may skip
# building the padding mask. Every fallback out of the VSA path must honour
# that contract itself; SDPA reads attn_mask and nothing else.
impl = FastVideoVSAImpl(
@@ -374,6 +233,8 @@ def test_sdpa_fallback_never_attends_the_structural_padding(monkeypatch):
cu_seqlens_k=torch.tensor([0, valid], dtype=torch.int32),
)
monkeypatch.setattr(impl.sdpa_fallback, "forward", impl.sdpa_fallback.forward_cuda)
baseline = impl.forward_cuda(query, query, query, AttentionMetadata(packed_padding=padding, extra={}))
perturbed_input = query.clone()
perturbed_input[:, valid:] += 100.0
@@ -384,22 +245,3 @@ def test_sdpa_fallback_never_attends_the_structural_padding(monkeypatch):
assert baseline.shape == query.shape
torch.testing.assert_close(baseline[:, :valid], perturbed[:, :valid])
assert torch.count_nonzero(baseline[:, valid:]) == 0
def test_h3_geometry_keeps_prefix_segments_pure_and_tiles_video_3d():
partition, sizes, non_pad, untile, prefix_blocks, video_blocks = _get_h3_tile_metadata(
(5, 70, 9), (5, 6, 6), torch.device("cpu")
)
assert sizes[:4].tolist() == [5, 64, 6, 9]
assert prefix_blocks == 4
assert video_blocks == 8
assert int(sizes.sum()) == 5 + 70 + 9 + 5 * 6 * 6
assert partition.numel() == non_pad.numel() == untile.numel() == int(sizes.sum())
def test_h3_block_map_makes_prefix_queries_dense_and_prefix_keys_exempt():
scores = torch.arange(1 * 2 * 5 * 5, dtype=torch.float32).reshape(1, 2, 5, 5)
block_map = _build_h3_block_map(scores, num_prefix_blocks=2, num_video_blocks=3, topk=1)
assert block_map[:, :, :2].all()
assert block_map[..., :2].all()
assert (block_map[:, :, 2:, 2:].sum(dim=-1) == 1).all()
@@ -0,0 +1,136 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
from vllm_omni.diffusion.attention import layer
from vllm_omni.diffusion.attention.backends.fastvideo_vsa import FastVideoVSABackend, FastVideoVSAImpl
from vllm_omni.diffusion.attention.backends.sdpa import SDPABackend
from vllm_omni.diffusion.attention.parallel.base import NoParallelAttention
from vllm_omni.diffusion.data import AttentionSpec
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
class _SpecializedImpl(FastVideoVSAImpl):
pass
@pytest.fixture
def local_attention(monkeypatch):
monkeypatch.setattr(layer, "get_current_diffusion_config_or_none", lambda: None)
monkeypatch.setattr(layer, "build_parallel_attention_strategy", lambda **kwargs: NoParallelAttention())
@pytest.mark.parametrize("explicit", [False, True])
@pytest.mark.parametrize("override", [False, True])
def test_specialization_preserves_selected_backend_and_options(mocker, local_attention, explicit, override):
spec = AttentionSpec(backend="FASTVIDEO_VSA", fastvideo_vsa_topk=7) if explicit else None
select = mocker.patch.object(layer, "get_attn_backend_for_role", return_value=(FastVideoVSABackend, spec))
attention = layer.Attention(
num_heads=2,
head_size=8,
softmax_scale=8**-0.5,
causal=False,
role="video.self",
role_category="self",
qkv_layout="BSND",
impl_overrides={"FASTVIDEO_VSA": _SpecializedImpl} if override else None,
)
assert attention.attn_backend is FastVideoVSABackend
assert type(attention.attention) is (_SpecializedImpl if override else FastVideoVSAImpl)
assert attention.attention.topk == (7 if explicit else 64)
assert attention.attention.qkv_layout == "BSND"
assert attention.backend_pref == "FASTVIDEO_VSA"
assert attention.backend_explicit is explicit
assert attention.attn_spec is spec
assert isinstance(attention.parallel_strategy, NoParallelAttention)
select.assert_called_once_with(
role="video.self", head_size=8, attention_config=None, role_category="self", allow_trtllm_default=False
)
def test_unselected_specialization_does_not_replace_dense_backend(mocker, local_attention):
mocker.patch.object(layer, "get_attn_backend_for_role", return_value=(SDPABackend, None))
attention = layer.Attention(
num_heads=2,
head_size=8,
softmax_scale=8**-0.5,
causal=False,
impl_overrides={"FASTVIDEO_VSA": _SpecializedImpl},
)
assert attention.attn_backend is SDPABackend
assert type(attention.attention) is SDPABackend.get_impl_cls()
def test_specialization_does_not_bypass_selection_errors(mocker, local_attention):
mocker.patch.object(layer, "get_attn_backend_for_role", side_effect=ImportError("optional kernel unavailable"))
with pytest.raises(ImportError, match="optional kernel unavailable"):
layer.Attention(
num_heads=2,
head_size=8,
softmax_scale=8**-0.5,
causal=False,
impl_overrides={"FASTVIDEO_VSA": _SpecializedImpl},
)
def test_specialization_must_extend_selected_implementation(mocker, local_attention):
mocker.patch.object(layer, "get_attn_backend_for_role", return_value=(FastVideoVSABackend, None))
with pytest.raises(TypeError, match="must subclass the selected implementation"):
layer.Attention(
num_heads=2,
head_size=8,
softmax_scale=8**-0.5,
causal=False,
impl_overrides={"FASTVIDEO_VSA": SDPABackend.get_impl_cls()},
)
def test_platform_backend_capabilities_survive_model_specialization(mocker, local_attention):
from vllm_omni.diffusion.models.minimax_h3.attention.fastvideo_h3 import MiniMaxH3VSAImpl
class PlatformBackend(FastVideoVSABackend):
@classmethod
def supports_packed_mask_free(cls):
return False
@staticmethod
def get_supported_head_sizes():
return [128]
# A registry/platform may refine capabilities without changing the
# implementation. The model must not replace those capability decisions.
mocker.patch.object(layer, "get_attn_backend_for_role", return_value=(PlatformBackend, None))
attention = layer.Attention(
num_heads=2,
head_size=128,
softmax_scale=128**-0.5,
causal=False,
impl_overrides={"FASTVIDEO_VSA": MiniMaxH3VSAImpl},
)
assert attention.attn_backend is PlatformBackend
assert not attention.attn_backend.supports_packed_mask_free()
assert attention.attn_backend.get_supported_head_sizes() == [128]
assert type(attention.attention) is MiniMaxH3VSAImpl
def test_incompatible_platform_implementation_is_not_discarded(mocker, local_attention):
class PlatformImpl(FastVideoVSAImpl):
pass
class PlatformBackend(FastVideoVSABackend):
@staticmethod
def get_impl_cls():
return PlatformImpl
mocker.patch.object(layer, "get_attn_backend_for_role", return_value=(PlatformBackend, None))
with pytest.raises(TypeError, match="selected implementation .*PlatformImpl"):
layer.Attention(
num_heads=2,
head_size=8,
softmax_scale=8**-0.5,
causal=False,
impl_overrides={"FASTVIDEO_VSA": _SpecializedImpl},
)
+5 -5
View File
@@ -68,12 +68,12 @@ class BaseExtractorTest(ABC):
pass
@abstractmethod
def get_module(self):
def get_module(self, module, /):
"""Return model module instance."""
pass
@abstractmethod
def get_sample_inputs(self):
def get_sample_inputs(self, sample_inputs, /):
"""Return sample inputs for model."""
pass
@@ -737,10 +737,10 @@ class TestMiniMaxH3Extractor(BaseExtractorTest):
monkeypatch,
):
"""Strict SP must not mix local TeaCache state with gathered block rows."""
from vllm_omni.diffusion.attention.ops import minimax_h3_modulation
from vllm_omni.diffusion.cache.teacache.config import TeaCacheConfig
from vllm_omni.diffusion.cache.teacache.hook import TeaCacheHook
from vllm_omni.diffusion.distributed import parallel_state
from vllm_omni.diffusion.layers import indexed_modulation
seq_len = sample_inputs["x"].shape[1]
local_len = seq_len // 2
@@ -844,13 +844,13 @@ class TestMiniMaxH3Extractor(BaseExtractorTest):
)
monkeypatch.setattr(parallel_state, "get_sp_group", lambda: sp_group)
original_indexed_scale_shift = minimax_h3_modulation.indexed_scale_shift_
original_indexed_scale_shift = indexed_modulation.indexed_scale_shift_
def capture_indexed_scale_shift(hidden, shift, scale, indices):
captured_modulation_indices.append(indices.clone())
return original_indexed_scale_shift(hidden, shift, scale, indices)
monkeypatch.setattr(minimax_h3_modulation, "indexed_scale_shift_", capture_indexed_scale_shift)
monkeypatch.setattr(indexed_modulation, "indexed_scale_shift_", capture_indexed_scale_shift)
local_img_pos = torch.tensor([0, 1])
local_audio_pos = torch.tensor([2, 3])
@@ -6,7 +6,7 @@ import torch
from vllm.triton_utils import HAS_TRITON
from tests.helpers.mark import hardware_marks
from vllm_omni.diffusion.attention.ops.minimax_h3_modulation import (
from vllm_omni.diffusion.layers.indexed_modulation import (
_MAX_1D_GRID_SIZE,
_iter_row_chunks,
_launch_row_chunks,
@@ -0,0 +1,221 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import math
import sys
import types
from typing import Any
import pytest
import torch
from vllm_omni.diffusion.attention.backends.abstract import (
AttentionMetadata,
PackedPaddingMetadata,
VideoTokenLayout,
VideoTokenSpan,
)
from vllm_omni.diffusion.models.minimax_h3.attention.fastvideo_h3 import (
MiniMaxH3VSAImpl,
_get_h3_tile_metadata,
)
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def _h3_metadata(prefix_segments, video_shape, *, gate=None, packed_padding=None):
extra: dict[str, Any] = {
"vsa_h3_prefix_segments": prefix_segments,
}
if gate is not None:
extra["gate_compress"] = gate
return AttentionMetadata(
packed_padding=packed_padding,
video_layout=VideoTokenLayout(
used_len=sum(prefix_segments) + math.prod(video_shape),
video_spans=(VideoTokenSpan(start=sum(prefix_segments), latent_grid=video_shape, role="target"),),
),
extra=extra,
)
def _fake_block_sparse_kernel(monkeypatch, calls):
"""Stand in for fastvideo_kernel.block_sparse_attn with a mask-obeying SDPA."""
def block_sparse_attn(q, k, v, block_map, variable_block_sizes):
calls["block_map"] = block_map.clone()
calls["variable_block_sizes"] = variable_block_sizes.tolist()
calls["q_shape"] = tuple(q.shape)
blocks = block_map.shape[-1]
assert blocks * 64 == q.shape[2]
# Expand the per-block map to a per-token mask so the stand-in honours
# the sparsity contract the real kernel implements, and drop the rows
# each partial block pads with, which the real kernel skips through
# variable_block_sizes.
token_mask = block_map.repeat_interleave(64, dim=-1).repeat_interleave(64, dim=-2)
within = torch.arange(64, device=q.device)[None, :] < variable_block_sizes[:, None]
token_mask = token_mask & within.reshape(1, 1, 1, -1)
out = torch.nn.functional.scaled_dot_product_attention(q.float(), k.float(), v.float(), attn_mask=token_mask)
return out.to(q.dtype), None
module = types.ModuleType("fastvideo_kernel.block_sparse_attn")
setattr(module, "block_sparse_attn", block_sparse_attn)
monkeypatch.setitem(sys.modules, "fastvideo_kernel", types.ModuleType("fastvideo_kernel"))
monkeypatch.setitem(sys.modules, "fastvideo_kernel.block_sparse_attn", module)
def _h3_impl(**backend_kwargs):
kwargs = {"topk": 1, "min_seq_len": 1, "disable_when_sp_active": False}
kwargs.update(backend_kwargs)
return MiniMaxH3VSAImpl(num_heads=2, head_size=8, softmax_scale=8**-0.5, backend_kwargs=kwargs)
def test_h3_forward_tiles_untiles_and_restores_the_original_row_order(monkeypatch):
calls: dict[str, Any] = {}
_fake_block_sparse_kernel(monkeypatch, calls)
impl = _h3_impl(topk=64)
prefix_segments, video_shape = (5, 70), (2, 4, 4)
seq_len = sum(prefix_segments) + 2 * 4 * 4
torch.manual_seed(0)
query = torch.randn(1, seq_len, 2, 8, dtype=torch.bfloat16)
output = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape))
assert output.shape == query.shape
# topk covers every video block, so the block map is dense and the tiled
# kernel must reproduce plain attention over the same rows.
reference = torch.nn.functional.scaled_dot_product_attention(
*(x.float().transpose(1, 2) for x in (query, query, query))
).transpose(1, 2)
torch.testing.assert_close(output.float(), reference, atol=2e-2, rtol=2e-2)
# 5 + 70 splits into segment-pure 5 | 64 | 6, then one (2, 4, 4) video tile.
assert calls["variable_block_sizes"] == [5, 64, 6, 32]
def test_h3_forward_keeps_prefix_dense_and_selects_top_k_video_tiles(monkeypatch):
calls: dict[str, Any] = {}
_fake_block_sparse_kernel(monkeypatch, calls)
impl = _h3_impl(topk=1)
prefix_segments, video_shape = (5,), (8, 4, 4)
seq_len = 5 + 8 * 4 * 4
torch.manual_seed(0)
query = torch.randn(1, seq_len, 2, 8, dtype=torch.bfloat16)
impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape))
# 1 prefix block + 2 video blocks. The odd count is padded to an even one
# for the paired-CTA kernel contract, and the Triton route slices that
# transport-only partner off again before launching.
block_map = calls["block_map"]
assert block_map.shape[-1] == 3
assert calls["variable_block_sizes"] == [5, 64, 64]
assert block_map[:, :, :1, :].all(), "prefix queries stay dense"
assert block_map[..., :1].all(), "prefix keys are exempt from selection"
assert (block_map[:, :, 1:, 1:].sum(dim=-1) == 1).all(), "one video tile per video query"
# The same odd geometry must still untile back onto the original rows.
dense = _h3_impl(topk=64).forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape))
reference = torch.nn.functional.scaled_dot_product_attention(
*(x.float().transpose(1, 2) for x in (query, query, query))
).transpose(1, 2)
torch.testing.assert_close(dense.float(), reference, atol=2e-2, rtol=2e-2)
def test_h3_forward_applies_the_learned_compression_gate(monkeypatch):
_fake_block_sparse_kernel(monkeypatch, {})
impl = _h3_impl(topk=64)
prefix_segments, video_shape = (5,), (2, 4, 4)
seq_len = 5 + 2 * 4 * 4
torch.manual_seed(0)
query = torch.randn(1, seq_len, 2, 8, dtype=torch.bfloat16)
without_gate = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape))
zero_gate = torch.zeros_like(query)
with_zero_gate = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape, gate=zero_gate))
live_gate = torch.full_like(query, 0.5)
with_live_gate = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape, gate=live_gate))
torch.testing.assert_close(with_zero_gate.float(), without_gate.float())
assert not torch.allclose(with_live_gate.float(), without_gate.float())
def test_h3_forward_restores_rows_the_packed_padding_excludes(monkeypatch):
_fake_block_sparse_kernel(monkeypatch, {})
impl = _h3_impl(topk=64)
prefix_segments, video_shape = (5,), (2, 4, 4)
valid = 5 + 2 * 4 * 4
torch.manual_seed(0)
query = torch.randn(1, valid + 24, 2, 8, dtype=torch.bfloat16)
padding = PackedPaddingMetadata(
q_length=valid,
kv_length=valid,
cu_seqlens_q=torch.tensor([0, valid], dtype=torch.int32),
cu_seqlens_k=torch.tensor([0, valid], dtype=torch.int32),
)
output = impl.forward_cuda(query, query, query, _h3_metadata(prefix_segments, video_shape, packed_padding=padding))
assert output.shape == query.shape
assert torch.count_nonzero(output[:, valid:]) == 0
def test_h3_geometry_keeps_prefix_segments_pure_and_tiles_video_3d():
partition, sizes, non_pad, untile, prefix_blocks, video_blocks = _get_h3_tile_metadata(
(5, 70, 9), (5, 6, 6), torch.device("cpu")
)
assert sizes[:4].tolist() == [5, 64, 6, 9]
assert prefix_blocks == 4
assert video_blocks == 8
assert int(sizes.sum()) == 5 + 70 + 9 + 5 * 6 * 6
assert partition.numel() == non_pad.numel() == untile.numel() == int(sizes.sum())
@pytest.mark.parametrize("fallback_on_error", [False, True])
def test_h3_kernel_failure_preserves_fallback_policy(monkeypatch, fallback_on_error):
impl = _h3_impl(fallback_on_error=fallback_on_error)
query = torch.randn(1, 37, 2, 8)
metadata = _h3_metadata((5,), (2, 4, 4))
def fail(*args, **kwargs):
raise RuntimeError("kernel failure")
monkeypatch.setattr(impl, "_forward_h3", fail)
monkeypatch.setattr(impl.sdpa_fallback, "forward", impl.sdpa_fallback.forward_cuda)
if fallback_on_error:
output = impl.forward_cuda(query, query, query, metadata)
expected = impl.sdpa_fallback.forward_cuda(query, query, query, metadata)
torch.testing.assert_close(output, expected, rtol=0, atol=0)
else:
with pytest.raises(RuntimeError, match="kernel failure"):
impl.forward_cuda(query, query, query, metadata)
def test_h3_accelerator_failure_is_not_swallowed(monkeypatch):
impl = _h3_impl(fallback_on_error=True)
query = torch.randn(1, 37, 2, 8)
def fail(*args, **kwargs):
raise torch.AcceleratorError("device failure")
monkeypatch.setattr(impl, "_forward_h3", fail)
with pytest.raises(torch.AcceleratorError, match="device failure"):
impl.forward_cuda(query, query, query, _h3_metadata((5,), (2, 4, 4)))
def test_h3_without_layout_delegates_to_shared_backend(monkeypatch):
from vllm_omni.diffusion.attention.backends.fastvideo_vsa import FastVideoVSAImpl
impl = _h3_impl()
query = torch.randn(1, 37, 2, 8)
metadata = AttentionMetadata()
expected = torch.empty_like(query)
calls = []
def forward(self, q, k, v, attn_metadata):
calls.append((self, q, k, v, attn_metadata))
return expected
monkeypatch.setattr(FastVideoVSAImpl, "forward_cuda", forward)
assert impl.forward_cuda(query, query, query, metadata) is expected
assert len(calls) == 1
assert all(actual is wanted for actual, wanted in zip(calls[0], (impl, query, query, query, metadata)))
+58 -4
View File
@@ -23,6 +23,7 @@ from typing import Any, NamedTuple
import numpy as np
import psutil
import yaml
from filelock import FileLock, Timeout
from vllm import TextPrompt
from vllm.logger import init_logger
@@ -170,9 +171,54 @@ class OmniServer:
self.use_omni = use_omni
self.proc: subprocess.Popen | None = None
self.host = "127.0.0.1"
self._auto_port = port is None
self._port_lock: FileLock | None = None
self.port = get_open_port() if port is None else port
def _reserve_port(self) -> None:
# bind(0)/close does not reserve a port across concurrent pytest workers.
# Keep an advisory lock until teardown, including the long import phase
# before the subprocess binds its HTTP socket. Never unlink lock files:
# another worker may already have the same inode open.
for attempt in range(128):
if attempt:
self.port = get_open_port(self.host)
lock_path = Path(tempfile.gettempdir()) / f"vllm-omni-test-port-{os.getuid()}-{self.port}.lock"
lock = FileLock(lock_path)
try:
lock.acquire(timeout=0)
except Timeout as error:
if not self._auto_port:
raise RuntimeError(f"HTTP test port {self.port} is already reserved") from error
continue
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
probe.bind((self.host, self.port))
except OSError as error:
lock.release()
if not self._auto_port or error.errno != errno.EADDRINUSE:
raise
continue
self._port_lock = lock
return
raise RuntimeError("Could not reserve an HTTP test port after 128 attempts")
def _owns_listening_port(self) -> bool:
assert self.proc is not None
try:
parent = psutil.Process(self.proc.pid)
processes = [parent, *parent.children(recursive=True)]
return any(
conn.status == psutil.CONN_LISTEN and conn.laddr.port == self.port
for process in processes
for conn in process.net_connections(kind="tcp")
)
except psutil.NoSuchProcess:
# A child can exit between enumeration and inspecting its sockets.
return False
def _start_server(self) -> None:
self._reserve_port()
env = os.environ.copy()
if self.env_dict is not None:
env.update(self.env_dict)
@@ -208,7 +254,10 @@ class OmniServer:
raise RuntimeError(f"Server processes exited with code {ret} before becoming ready.")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1)
if sock.connect_ex((self.host, self.port)) == 0:
if sock.connect_ex((self.host, self.port)) == 0 and self._owns_listening_port():
ret = self.proc.poll()
if ret is not None:
raise RuntimeError(f"Server processes exited with code {ret} before becoming ready.")
startup_s = time.perf_counter() - startup_t0
if self.log_stats:
print(
@@ -368,9 +417,14 @@ class OmniServer:
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.proc:
self._kill_process_tree(self.proc.pid)
cleanup_test_environment()
try:
if self.proc:
self._kill_process_tree(self.proc.pid)
finally:
if self._port_lock is not None:
self._port_lock.release()
self._port_lock = None
cleanup_test_environment()
class OmniServerStageCli(OmniServer):
@@ -0,0 +1,109 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""A listening port alone must not make a different test's server ready."""
import socket
import subprocess
import sys
import pytest
from filelock import FileLock
from tests.helpers import runtime
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_server_does_not_accept_another_process_listener(monkeypatch):
monkeypatch.setattr(runtime, "cleanup_test_environment", lambda: None)
server = runtime.OmniServer("fake-model", [])
popen = subprocess.Popen
# Occupy the chosen port after allocation, while the requested subprocess
# is still importing. The old TCP-only probe returns ready for this socket.
with socket.socket() as foreign_listener:
def launch(*args, **kwargs):
foreign_listener.bind((server.host, server.port))
foreign_listener.listen()
return popen([sys.executable, "-c", "import time; time.sleep(0.2); raise SystemExit(7)"])
monkeypatch.setattr(runtime.subprocess, "Popen", launch)
with pytest.raises(RuntimeError, match="exited with code 7"):
with server:
pass
# Failed __enter__ must also release the lease for a later server.
retry = runtime.OmniServer("retry-model", [], port=server.port)
try:
retry._reserve_port()
finally:
retry.__exit__(None, None, None)
def test_auto_ports_are_reserved_before_subprocesses_bind(monkeypatch, tmp_path):
monkeypatch.setattr(runtime, "cleanup_test_environment", lambda: None)
monkeypatch.setattr(runtime.tempfile, "gettempdir", lambda: str(tmp_path))
# Both constructors see the same free port before either server has bound.
first = runtime.OmniServer("first-model", [])
candidate = first.port
monkeypatch.setattr(runtime, "get_open_port", lambda *args: candidate)
second = runtime.OmniServer("second-model", [])
first._reserve_port()
with socket.socket() as occupied, socket.socket() as probe:
occupied.bind((first.host, candidate))
probe.bind((first.host, 0))
alternative = probe.getsockname()[1]
monkeypatch.setattr(runtime, "get_open_port", lambda *args: alternative)
try:
second._reserve_port()
assert first.port == candidate
assert second.port == alternative
# Check OS-level lock exclusion, not only the Python object's state.
lock_path = next(tmp_path.glob(f"*-{candidate}.lock"))
contender = FileLock(lock_path)
result = subprocess.run(
[
sys.executable,
"-c",
"import sys\nfrom filelock import FileLock, Timeout\n"
"try:\n FileLock(sys.argv[1]).acquire(timeout=0)\n"
"except Timeout:\n sys.exit(23)\n",
str(lock_path),
],
timeout=10,
check=False,
)
assert result.returncode == 23
explicit = runtime.OmniServer("explicit-model", [], port=candidate)
with pytest.raises(RuntimeError, match="already reserved"):
with explicit:
pass
finally:
first.__exit__(None, None, None)
second.__exit__(None, None, None)
with contender.acquire(timeout=0):
pass
@pytest.mark.parametrize("in_child", [False, True])
def test_server_accepts_its_own_listener(monkeypatch, in_child):
monkeypatch.setattr(runtime, "cleanup_test_environment", lambda: None)
server = runtime.OmniServer("fake-model", [])
popen = subprocess.Popen
def launch(*args, **kwargs):
code = (
"import socket,time; s=socket.socket(); "
f"s.bind(({server.host!r}, {server.port})); s.listen(); time.sleep(30)"
)
if in_child:
code = f"import subprocess,sys; child=subprocess.Popen([sys.executable,'-c',{code!r}]); child.wait()"
return popen([sys.executable, "-c", code])
monkeypatch.setattr(runtime.subprocess, "Popen", launch)
with server:
assert server.proc is not None
assert server.proc.poll() is None
with socket.create_connection((server.host, server.port), timeout=1):
pass
@@ -6,7 +6,6 @@ from __future__ import annotations
import functools
import importlib.util
import math
import os
from collections.abc import Mapping
from typing import Any
@@ -88,77 +87,6 @@ if not hasattr(torch.ops.vllm_omni, "fastvideo_vsa_bshd"):
_fastvideo_vsa_bshd_op = torch.ops.vllm_omni.fastvideo_vsa_bshd
# H3 needs an explicit per-query block map: prefix queries are dense, while
# video queries select prefix + top-k video tiles. The generic
# video_sparse_attn() entry point cannot express that contract.
if not hasattr(torch.ops.vllm_omni, "fastvideo_h3_vsa_bhsd"):
@torch.library.custom_op("vllm_omni::fastvideo_h3_vsa_bhsd", mutates_args=())
def _fastvideo_h3_vsa_bhsd_op(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
block_map: torch.Tensor,
variable_block_sizes: torch.Tensor,
logical_blocks: int,
) -> torch.Tensor:
q = query.transpose(1, 2).contiguous()
k = key.transpose(1, 2).contiguous()
v = value.transpose(1, 2).contiguous()
# Official FastVideo's measured FastH3 route opts into this native
# Blackwell forward. Wheels without the extension (including SM103
# builds today) retain the corrected explicit-mask Triton route.
if os.environ.get("FASTVIDEO_VSA_SM100A", "0") == "1":
try:
from fastvideo_kernel import block_sparse_attn_sm100a
from fastvideo_kernel.triton_kernels.index import map_to_index
if block_sparse_attn_sm100a.is_supported(q, variable_block_sizes):
q2k_idx, q2k_num = map_to_index(block_map)
out, _ = block_sparse_attn_sm100a.block_sparse_attn_sm100a(
q,
k,
v,
q2k_idx.to(torch.int32).contiguous(),
q2k_num.to(torch.int32).contiguous(),
variable_block_sizes.to(torch.int32).contiguous(),
need_lse=False,
)
return out.transpose(1, 2).contiguous()
except (ImportError, RuntimeError) as exc:
# Opting in explicitly and then silently getting a different
# numeric path is worse than the slower route it lands on.
logger.warning_once(
"FASTVIDEO_VSA_SM100A=1 requested but the native Blackwell forward is "
"unavailable (%s); using the Triton block-sparse route instead.",
exc,
)
from fastvideo_kernel.block_sparse_attn import block_sparse_attn
logical_len = logical_blocks * 64
out, _ = block_sparse_attn(
q[:, :, :logical_len].contiguous(),
k[:, :, :logical_len].contiguous(),
v[:, :, :logical_len].contiguous(),
block_map[..., :logical_blocks, :logical_blocks].contiguous(),
variable_block_sizes[:logical_blocks].to(torch.int32).contiguous(),
)
out = out.transpose(1, 2).contiguous()
if out.shape[1] != query.shape[1]:
out = torch.nn.functional.pad(out, (0, 0, 0, 0, 0, query.shape[1] - out.shape[1]))
return out
@_fastvideo_h3_vsa_bhsd_op.register_fake
def _(query, key, value, block_map, variable_block_sizes, logical_blocks):
del key, value, block_map, variable_block_sizes, logical_blocks
return torch.empty_like(query)
_fastvideo_h3_vsa_bhsd_op = torch.ops.vllm_omni.fastvideo_h3_vsa_bhsd
@functools.lru_cache(maxsize=32)
def _get_tile_partition_indices(
dit_seq_shape: tuple[int, int, int],
@@ -226,81 +154,6 @@ def _get_tile_metadata(
return tile_partition_indices, variable_block_sizes, non_pad_index, untile_combined_index
@functools.lru_cache(maxsize=32)
def _get_h3_tile_metadata(
prefix_segments: tuple[int, ...],
video_shape: tuple[int, int, int],
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int, int]:
"""Official FastVideo H3 geometry: pure prefix chunks + 3-D video tiles."""
block_size = (4, 4, 4)
block_elements = 64
prefix_len = sum(prefix_segments)
prefix_sizes: list[int] = []
for segment in prefix_segments:
full, remainder = divmod(segment, block_elements)
prefix_sizes.extend([block_elements] * full)
if remainder:
prefix_sizes.append(remainder)
video_indices = _get_tile_partition_indices(video_shape, block_size, device) + prefix_len
video_sizes = _construct_variable_block_sizes(video_shape, block_size, device)
partition = torch.cat([torch.arange(prefix_len, device=device, dtype=torch.long), video_indices])
sizes = torch.cat([torch.tensor(prefix_sizes, device=device, dtype=torch.int32), video_sizes.to(torch.int32)])
non_pad = _get_non_pad_index(sizes, block_elements)
untile = non_pad[torch.argsort(partition)]
total = prefix_len + math.prod(video_shape)
if int(sizes.sum()) != total or untile.numel() != total:
raise ValueError(
f"invalid H3 VSA geometry: prefix={prefix_segments}, video={video_shape}, "
f"sizes_sum={int(sizes.sum())}, total={total}"
)
return partition, sizes, non_pad, untile, len(prefix_sizes), int(video_sizes.numel())
def _get_h3_layout(
attn_metadata: AttentionMetadata | None,
) -> tuple[tuple[int, ...], tuple[int, int, int], int] | None:
if attn_metadata is None or attn_metadata.video_layout is None:
return None
prefix = attn_metadata.extra.get("vsa_h3_prefix_segments")
if not isinstance(prefix, (tuple, list)):
return None
target = next(
(span for span in reversed(attn_metadata.video_layout.video_spans) if span.role == "target"),
None,
)
if target is None:
return None
return tuple(int(x) for x in prefix if int(x) > 0), target.latent_grid, target.start
def _pool_h3_tiles(x: torch.Tensor, sizes: torch.Tensor) -> torch.Tensor:
batch, seq_len, heads, dim = x.shape
blocks = seq_len // 64
pooled = x.view(batch, blocks, 64, heads, dim).sum(dim=2, dtype=torch.float32)
pooled = pooled / sizes.view(1, -1, 1, 1).clamp_min(1)
return pooled.permute(0, 2, 1, 3)
def _build_h3_block_map(
scores: torch.Tensor,
num_prefix_blocks: int,
num_video_blocks: int,
topk: int,
) -> torch.Tensor:
"""Prefix K/V are exempt and prefix queries stay dense, as in FastVideo."""
keep_video = min(topk, num_video_blocks)
if keep_video == num_video_blocks:
return torch.ones_like(scores, dtype=torch.bool)
block_map = torch.zeros_like(scores, dtype=torch.bool)
indices = scores[..., num_prefix_blocks:].topk(keep_video, dim=-1).indices + num_prefix_blocks
block_map.scatter_(-1, indices, True)
block_map[..., :num_prefix_blocks] = True
block_map[:, :, :num_prefix_blocks, :] = True
return block_map
def _get_vsa_dit_seq_shape(attn_metadata: AttentionMetadata | None) -> tuple[int, int, int] | None:
if attn_metadata is None:
return None
@@ -343,7 +196,9 @@ class FastVideoVSABackend(AttentionBackend):
if importlib.util.find_spec("fastvideo_kernel") is None:
raise ImportError(
"FASTVIDEO_VSA requires the optional fastvideo-kernel package "
"(the installation must provide the 'fastvideo_kernel' Python module)."
"included in vllm-omni[vsa]. Install with `uv pip install 'vllm-omni[vsa]'` "
"(from source: `uv pip install -e '.[vsa]'`). "
"Prebuilt kernels require Linux, Python 3.12 and glibc >= 2.34."
)
@staticmethod
@@ -504,93 +359,6 @@ class FastVideoVSAImpl(AttentionImpl):
return "sequence parallel context is active"
return None
def _forward_h3(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
layout = _get_h3_layout(attn_metadata)
if layout is None:
raise ValueError("incomplete VSA-H3 layout metadata")
prefix_segments, video_shape, target_start = layout
if sum(prefix_segments) != target_start:
raise ValueError(f"VSA-H3 prefix segments sum to {sum(prefix_segments)}, target starts at {target_start}")
expected = target_start + math.prod(video_shape)
if query.shape[1] != expected:
raise ValueError(f"VSA-H3 layout has {expected} rows but attention received {query.shape[1]}")
if query.dtype not in (torch.float16, torch.bfloat16):
raise ValueError(f"VSA-H3 requires fp16/bf16 tensors, got {query.dtype}")
gate = _get_gate_compress(attn_metadata)
if gate is not None:
# H3 pads the packed document to 64 rows, while VSA operates on
# the valid prefix. Validate/slice before launching attention so a
# metadata error can never trigger an unsafe dense fallback after
# an asynchronous custom kernel.
if gate.shape[0] != query.shape[0] or gate.shape[2:] != query.shape[2:] or gate.shape[1] < query.shape[1]:
raise ValueError(f"gate_compress shape {gate.shape} cannot cover query shape {query.shape}")
gate = gate[:, : query.shape[1]]
partition, sizes, non_pad, untile, prefix_blocks, video_blocks = _get_h3_tile_metadata(
prefix_segments, video_shape, query.device
)
logical_blocks = int(sizes.numel())
# The native sm100a kernel assigns pairs of query blocks to CTAs. Its
# contract requires an even block count; the synthetic partner is
# transport-only and is removed before returning.
pair_pad = logical_blocks % 2
kernel_blocks = logical_blocks + pair_pad
target_shape = (query.shape[0], kernel_blocks * 64, query.shape[2], query.shape[3])
q_tiled = torch.zeros(target_shape, device=query.device, dtype=query.dtype)
k_tiled = torch.zeros_like(q_tiled)
v_tiled = torch.zeros_like(q_tiled)
q_tiled[:, non_pad] = query[:, partition]
k_tiled[:, non_pad] = key[:, partition]
v_tiled[:, non_pad] = value[:, partition]
q_pool = _pool_h3_tiles(q_tiled[:, : logical_blocks * 64], sizes)
k_pool = _pool_h3_tiles(k_tiled[:, : logical_blocks * 64], sizes)
scores = torch.matmul(q_pool, k_pool.transpose(-2, -1)) * self.softmax_scale
block_map = _build_h3_block_map(scores, prefix_blocks, video_blocks, self.topk)
kernel_sizes = sizes
if pair_pad:
block_map = torch.nn.functional.pad(block_map, (0, 1, 0, 1), value=False)
kernel_sizes = torch.nn.functional.pad(sizes, (0, 1), value=0)
logger.info_once(
"FASTVIDEO_VSA H3 routing: seq_len=%d, prefix_segments=%s, video_shape=%s, "
"prefix_blocks=%d, video_blocks=%d, topk=%d, kernel_blocks=%d",
query.shape[1],
prefix_segments,
video_shape,
prefix_blocks,
video_blocks,
min(self.topk, video_blocks),
kernel_blocks,
)
output = _fastvideo_h3_vsa_bhsd_op(
q_tiled.contiguous(),
k_tiled.contiguous(),
v_tiled.contiguous(),
block_map.contiguous(),
kernel_sizes.contiguous(),
logical_blocks,
)[:, : logical_blocks * 64]
if gate is not None:
gate_tiled = torch.zeros_like(q_tiled[:, : logical_blocks * 64])
gate_tiled[:, non_pad] = gate[:, partition]
v_pool = _pool_h3_tiles(v_tiled[:, : logical_blocks * 64], sizes)
compressed = torch.matmul(torch.softmax(scores, dim=-1), v_pool)
compressed = compressed.permute(0, 2, 1, 3).to(output.dtype)
output = (
output.view(output.shape[0], logical_blocks, 64, output.shape[2], output.shape[3])
+ compressed.unsqueeze(2)
* gate_tiled.view(gate_tiled.shape[0], logical_blocks, 64, gate_tiled.shape[2], gate_tiled.shape[3])
).view_as(output)
return output[:, untile].contiguous()
def forward_cuda(
self,
query: torch.Tensor,
@@ -611,26 +379,6 @@ class FastVideoVSAImpl(AttentionImpl):
key = key[:, :valid_seq_len]
value = value[:, :valid_seq_len]
if attn_metadata is not None and _get_h3_layout(attn_metadata) is not None:
try:
output = self._forward_h3(query, key, value, attn_metadata)
if valid_seq_len == original_seq_len:
return output
restored = torch.zeros_like(original_query)
restored[:, :valid_seq_len] = output
return restored
except Exception as exc:
# A CUDA fault poisons the process context; attempting SDPA
# afterwards obscures the original kernel failure and cannot
# recover the request.
if isinstance(exc, torch.AcceleratorError):
raise
if not self.fallback_on_error:
raise
return self._fallback(
original_query, original_key, original_value, attn_metadata, f"VSA-H3 kernel failed: {exc}"
)
reason = self._fallback_reason(query, key, value, attn_metadata)
if reason is not None:
return self._fallback(original_query, original_key, original_value, attn_metadata, reason)
+26 -8
View File
@@ -7,7 +7,9 @@
# https://github.com/feifeibear/long-context-attention/blob/main/yunchang/attention/layer.py
from collections.abc import Mapping
from dataclasses import replace
from typing import cast
import torch
import torch.nn as nn
@@ -16,7 +18,7 @@ from vllm.logger import init_logger
from vllm.model_executor.models.utils import extract_layer_index
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheSpec
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.diffusion.attention.backends.abstract import AttentionBackend, AttentionImpl, AttentionMetadata
from vllm_omni.diffusion.attention.backends.sdpa import SDPABackend
from vllm_omni.diffusion.attention.parallel import build_parallel_attention_strategy
from vllm_omni.diffusion.attention.parallel.base import NoParallelAttention
@@ -83,6 +85,10 @@ class Attention(nn.Module):
custom_attention: nn.Module | None = None,
# Preserve dense FP32 inference for models opting into CUDA auto fallback.
allow_fp32_fallback: bool = False,
# Model-owned implementation subclasses, keyed by backend name.
# Keep the platform-selected backend and its capabilities unchanged.
# Each override must preserve the selected implementation's contract.
impl_overrides: Mapping[str, type[AttentionImpl]] | None = None,
):
super().__init__()
@@ -181,8 +187,18 @@ class Attention(nn.Module):
self.backend_pref = attn_backend_cls.get_name()
logger.debug("Attention(role=%s) → platform default (%s)", role, self.backend_pref)
self.attn_backend = attn_backend_cls
self.attn_backend: type[AttentionBackend] | None = attn_backend_cls
self.attn_impl_cls = self.attn_backend.get_impl_cls()
if impl_overrides is not None:
override = impl_overrides.get(attn_backend_cls.get_name())
if override is not None:
if not issubclass(override, self.attn_impl_cls):
raise TypeError(
f"Attention implementation override {override.__qualname__} must subclass "
f"the selected implementation {self.attn_impl_cls.__qualname__} "
f"for backend {attn_backend_cls.__qualname__}"
)
self.attn_impl_cls = override
self.attention = self.attn_impl_cls(
num_heads=num_heads,
head_size=head_size,
@@ -197,7 +213,7 @@ class Attention(nn.Module):
)
# Compatibility kernels run inside shared dispatch, between the
# parallel strategy's input preparation and output restoration.
self.sdpa_fallback = SDPABackend.get_impl_cls()(
self.sdpa_fallback: AttentionImpl | None = SDPABackend.get_impl_cls()(
num_heads=num_heads,
head_size=head_size,
softmax_scale=softmax_scale,
@@ -299,6 +315,7 @@ class Attention(nn.Module):
def _init_kv_cache_quantization(self, config) -> None:
if config is None or self._has_custom_attention:
return
assert self.attn_backend is not None
dtype = getattr(config, "diffusion_kv_cache_dtype", None)
if dtype == "auto":
dtype = None
@@ -400,6 +417,7 @@ class Attention(nn.Module):
)
use_paged_attention = paged_adapter is not None and self.paged_kv_cache_role is not None
if use_paged_attention and not getattr(self.attn_backend, "supports_paged_kv", False):
assert self.attn_backend is not None
raise NotImplementedError(
f"Diffusion paged KV requires an Omni backend with paged support; "
f"selected {self.attn_backend.get_name()}"
@@ -496,7 +514,7 @@ class Attention(nn.Module):
def _run_local_attention(self, query, key, value, attn_metadata):
if self._has_custom_attention:
return self.attention(query, key, value, attn_metadata)
return cast(nn.Module, self.attention)(query, key, value, attn_metadata)
self._assert_metadata_compatible(attn_metadata)
@@ -506,7 +524,7 @@ class Attention(nn.Module):
and query.dtype == torch.float32
and query.ndim == 4
and not self.backend_explicit
and self.attn_backend.get_name() == "FLASH_ATTN"
and cast(type[AttentionBackend], self.attn_backend).get_name() == "FLASH_ATTN"
and (
attn_metadata is None
or (
@@ -519,7 +537,7 @@ class Attention(nn.Module):
)
):
logger.warning_once("Using SDPA for this layer's FP32 input with automatic CUDA FlashAttention selection.")
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
return cast(AttentionImpl, self.sdpa_fallback).forward(query, key, value, attn_metadata)
in_kv_memory_profile = is_forward_context_available() and get_forward_context().in_diffusion_kv_memory_profile
# The startup KV-capacity profile needs tensor shapes, not a paged
@@ -531,14 +549,14 @@ class Attention(nn.Module):
self._scheduler_paged_kv
and self.paged_kv_cache_role is not None
and in_kv_memory_profile
and self.attn_backend.get_name() == "FLASH_ATTN"
and cast(type[AttentionBackend], self.attn_backend).get_name() == "FLASH_ATTN"
and not current_omni_platform.supports_diffusion_dense_flash_attention()
):
logger.warning_once(
"The startup KV memory profile is using SDPA because dense FLASH_ATTN is unavailable. "
"Formal paged requests still use the platform-native paged attention backend."
)
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
return cast(AttentionImpl, self.sdpa_fallback).forward(query, key, value, attn_metadata)
return self.attention.forward(query, key, value, attn_metadata)
@@ -0,0 +1,115 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Shared pooling, routing and provider calls for explicit block-sparse attention."""
import os
import torch
from vllm.logger import init_logger
logger = init_logger(__name__)
if not hasattr(torch.ops.vllm_omni, "fastvideo_block_sparse_attn_bshd"):
@torch.library.custom_op("vllm_omni::fastvideo_block_sparse_attn_bshd", mutates_args=())
def fastvideo_block_sparse_attn_bshd(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
block_map: torch.Tensor,
variable_block_sizes: torch.Tensor,
logical_blocks: int,
) -> torch.Tensor:
"""Run explicit block-sparse attention on BSHD tensors in 64-row blocks.
Inputs and the block map may include transport-only blocks after
``logical_blocks``. The Triton provider sees only logical blocks and
the wrapper restores the original row count. Native provider selection
and its supported-input checks remain owned by FastVideo.
"""
q = query.transpose(1, 2).contiguous()
k = key.transpose(1, 2).contiguous()
v = value.transpose(1, 2).contiguous()
# Prefer the explicitly selected native provider when it supports
# these tensors; retain the existing Triton provider otherwise.
if os.environ.get("FASTVIDEO_VSA_SM100A", "0") == "1":
try:
from fastvideo_kernel import block_sparse_attn_sm100a
from fastvideo_kernel.triton_kernels.index import map_to_index
if block_sparse_attn_sm100a.is_supported(q, variable_block_sizes):
q2k_idx, q2k_num = map_to_index(block_map)
out, _ = block_sparse_attn_sm100a.block_sparse_attn_sm100a(
q,
k,
v,
q2k_idx.to(torch.int32).contiguous(),
q2k_num.to(torch.int32).contiguous(),
variable_block_sizes.to(torch.int32).contiguous(),
need_lse=False,
)
return out.transpose(1, 2).contiguous()
except torch.AcceleratorError:
# A device fault can poison the context; do not launch another kernel.
raise
except (ImportError, RuntimeError) as exc:
logger.warning_once(
"FASTVIDEO_VSA_SM100A=1 requested but the native Blackwell forward is "
"unavailable (%s); using the Triton block-sparse route instead.",
exc,
)
from fastvideo_kernel.block_sparse_attn import block_sparse_attn
logical_len = logical_blocks * 64
out, _ = block_sparse_attn(
q[:, :, :logical_len].contiguous(),
k[:, :, :logical_len].contiguous(),
v[:, :, :logical_len].contiguous(),
block_map[..., :logical_blocks, :logical_blocks].contiguous(),
variable_block_sizes[:logical_blocks].to(torch.int32).contiguous(),
)
out = out.transpose(1, 2).contiguous()
if out.shape[1] != query.shape[1]:
out = torch.nn.functional.pad(out, (0, 0, 0, 0, 0, query.shape[1] - out.shape[1]))
return out
@fastvideo_block_sparse_attn_bshd.register_fake
def _(query, key, value, block_map, variable_block_sizes, logical_blocks):
del key, value, block_map, variable_block_sizes, logical_blocks
return torch.empty_like(query)
fastvideo_block_sparse_attn_bshd = torch.ops.vllm_omni.fastvideo_block_sparse_attn_bshd
def mean_pool_tiles(x: torch.Tensor, sizes: torch.Tensor, block_size: int) -> torch.Tensor:
"""Mean-pool zero-padded BSHD tiles with FP32 accumulation.
``sizes`` gives the number of valid rows in each tile.
"""
batch, seq_len, heads, dim = x.shape
blocks = seq_len // block_size
pooled = x.view(batch, blocks, block_size, heads, dim).sum(dim=2, dtype=torch.float32)
pooled = pooled / sizes.view(1, -1, 1, 1).clamp_min(1)
return pooled.permute(0, 2, 1, 3)
def build_prefix_dense_block_map(
scores: torch.Tensor,
num_prefix_blocks: int,
num_sparse_blocks: int,
topk: int,
) -> torch.Tensor:
"""Keep prefix queries/keys dense and select top-k non-prefix keys per row."""
keep_sparse = min(topk, num_sparse_blocks)
if keep_sparse == num_sparse_blocks:
return torch.ones_like(scores, dtype=torch.bool)
block_map = torch.zeros_like(scores, dtype=torch.bool)
indices = scores[..., num_prefix_blocks:].topk(keep_sparse, dim=-1).indices + num_prefix_blocks
block_map.scatter_(-1, indices, True)
block_map[..., :num_prefix_blocks] = True
block_map[:, :, :num_prefix_blocks, :] = True
return block_map
+1 -1
View File
@@ -1262,7 +1262,7 @@ def extract_minimax_h3_context(
preprocessing via ``_embed``, cacheable ``blocks`` loop, and
``final_layer`` postprocessing with row selection and update masks.
"""
from vllm_omni.diffusion.attention.ops.minimax_h3_modulation import indexed_scale_shift_
from vllm_omni.diffusion.layers.indexed_modulation import indexed_scale_shift_
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
_FORWARD_SUPPORTED_KWARGS,
MINIMAX_H3_ADALN_MODALITY_NUM,
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Fused MiniMax H3 modulation with FP32 accumulation."""
"""Fused indexed modulation with FP32 accumulation."""
from __future__ import annotations
@@ -244,7 +244,7 @@ def rms_norm_indexed_scale_shift(
indices: torch.Tensor,
eps: float,
) -> torch.Tensor:
"""Fuse H3 RMSNorm with its indexed AdaLN affine transform."""
"""Fuse RMSNorm with an indexed affine transform."""
if x.is_cpu:
input_dtype = x.dtype
normalized = x.float()
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
@@ -0,0 +1,209 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""MiniMax-H3 VSA prefix routing, tile layout, and learned compression gate."""
from __future__ import annotations
import functools
import math
import torch
from vllm.logger import init_logger
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.diffusion.attention.backends.fastvideo_vsa import (
FastVideoVSAImpl,
_construct_variable_block_sizes,
_get_gate_compress,
_get_non_pad_index,
_get_tile_partition_indices,
)
from vllm_omni.diffusion.attention.ops.block_sparse import (
build_prefix_dense_block_map,
fastvideo_block_sparse_attn_bshd,
mean_pool_tiles,
)
logger = init_logger(__name__)
@functools.lru_cache(maxsize=32)
def _get_h3_tile_metadata(
prefix_segments: tuple[int, ...],
video_shape: tuple[int, int, int],
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int, int]:
"""Official FastVideo H3 geometry: pure prefix chunks + 3-D video tiles."""
block_size = (4, 4, 4)
block_elements = 64
prefix_len = sum(prefix_segments)
prefix_sizes: list[int] = []
for segment in prefix_segments:
full, remainder = divmod(segment, block_elements)
prefix_sizes.extend([block_elements] * full)
if remainder:
prefix_sizes.append(remainder)
video_indices = _get_tile_partition_indices(video_shape, block_size, device) + prefix_len
video_sizes = _construct_variable_block_sizes(video_shape, block_size, device)
partition = torch.cat([torch.arange(prefix_len, device=device, dtype=torch.long), video_indices])
sizes = torch.cat([torch.tensor(prefix_sizes, device=device, dtype=torch.int32), video_sizes.to(torch.int32)])
non_pad = _get_non_pad_index(sizes, block_elements)
untile = non_pad[torch.argsort(partition)]
total = prefix_len + math.prod(video_shape)
if int(sizes.sum()) != total or untile.numel() != total:
raise ValueError(
f"invalid H3 VSA geometry: prefix={prefix_segments}, video={video_shape}, "
f"sizes_sum={int(sizes.sum())}, total={total}"
)
return partition, sizes, non_pad, untile, len(prefix_sizes), int(video_sizes.numel())
def _get_h3_layout(
attn_metadata: AttentionMetadata | None,
) -> tuple[tuple[int, ...], tuple[int, int, int], int] | None:
if attn_metadata is None or attn_metadata.video_layout is None:
return None
prefix = attn_metadata.extra.get("vsa_h3_prefix_segments")
if not isinstance(prefix, (tuple, list)):
return None
target = next(
(span for span in reversed(attn_metadata.video_layout.video_spans) if span.role == "target"),
None,
)
if target is None:
return None
return tuple(int(x) for x in prefix if int(x) > 0), target.latent_grid, target.start
class MiniMaxH3VSAImpl(FastVideoVSAImpl):
"""Apply H3 tile64 routing after shared parallel attention dispatch."""
def _forward_h3(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
layout = _get_h3_layout(attn_metadata)
if layout is None:
raise ValueError("incomplete VSA-H3 layout metadata")
prefix_segments, video_shape, target_start = layout
if sum(prefix_segments) != target_start:
raise ValueError(f"VSA-H3 prefix segments sum to {sum(prefix_segments)}, target starts at {target_start}")
expected = target_start + math.prod(video_shape)
if query.shape[1] != expected:
raise ValueError(f"VSA-H3 layout has {expected} rows but attention received {query.shape[1]}")
if query.dtype not in (torch.float16, torch.bfloat16):
raise ValueError(f"VSA-H3 requires fp16/bf16 tensors, got {query.dtype}")
gate = _get_gate_compress(attn_metadata)
if gate is not None:
# H3 pads the packed document to 64 rows, while VSA operates on
# the valid prefix. Validate/slice before launching attention so a
# metadata error can never trigger an unsafe dense fallback after
# an asynchronous custom kernel.
if gate.shape[0] != query.shape[0] or gate.shape[2:] != query.shape[2:] or gate.shape[1] < query.shape[1]:
raise ValueError(f"gate_compress shape {gate.shape} cannot cover query shape {query.shape}")
gate = gate[:, : query.shape[1]]
partition, sizes, non_pad, untile, prefix_blocks, video_blocks = _get_h3_tile_metadata(
prefix_segments, video_shape, query.device
)
logical_blocks = int(sizes.numel())
# The native sm100a kernel assigns pairs of query blocks to CTAs. Its
# contract requires an even block count; the synthetic partner is
# transport-only and is removed before returning.
pair_pad = logical_blocks % 2
kernel_blocks = logical_blocks + pair_pad
target_shape = (query.shape[0], kernel_blocks * 64, query.shape[2], query.shape[3])
q_tiled = torch.zeros(target_shape, device=query.device, dtype=query.dtype)
k_tiled = torch.zeros_like(q_tiled)
v_tiled = torch.zeros_like(q_tiled)
q_tiled[:, non_pad] = query[:, partition]
k_tiled[:, non_pad] = key[:, partition]
v_tiled[:, non_pad] = value[:, partition]
q_pool = mean_pool_tiles(q_tiled[:, : logical_blocks * 64], sizes, block_size=64)
k_pool = mean_pool_tiles(k_tiled[:, : logical_blocks * 64], sizes, block_size=64)
scores = torch.matmul(q_pool, k_pool.transpose(-2, -1)) * self.softmax_scale
block_map = build_prefix_dense_block_map(scores, prefix_blocks, video_blocks, self.topk)
kernel_sizes = sizes
if pair_pad:
block_map = torch.nn.functional.pad(block_map, (0, 1, 0, 1), value=False)
kernel_sizes = torch.nn.functional.pad(sizes, (0, 1), value=0)
logger.info_once(
"FASTVIDEO_VSA H3 routing: seq_len=%d, prefix_segments=%s, video_shape=%s, "
"prefix_blocks=%d, video_blocks=%d, topk=%d, kernel_blocks=%d",
query.shape[1],
prefix_segments,
video_shape,
prefix_blocks,
video_blocks,
min(self.topk, video_blocks),
kernel_blocks,
)
output = fastvideo_block_sparse_attn_bshd(
q_tiled.contiguous(),
k_tiled.contiguous(),
v_tiled.contiguous(),
block_map.contiguous(),
kernel_sizes.contiguous(),
logical_blocks,
)[:, : logical_blocks * 64]
if gate is not None:
gate_tiled = torch.zeros_like(q_tiled[:, : logical_blocks * 64])
gate_tiled[:, non_pad] = gate[:, partition]
v_pool = mean_pool_tiles(v_tiled[:, : logical_blocks * 64], sizes, block_size=64)
compressed = torch.matmul(torch.softmax(scores, dim=-1), v_pool)
compressed = compressed.permute(0, 2, 1, 3).to(output.dtype)
output = (
output.view(output.shape[0], logical_blocks, 64, output.shape[2], output.shape[3])
+ compressed.unsqueeze(2)
* gate_tiled.view(gate_tiled.shape[0], logical_blocks, 64, gate_tiled.shape[2], gate_tiled.shape[3])
).view_as(output)
return output[:, untile].contiguous()
def forward_cuda(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
if _get_h3_layout(attn_metadata) is None:
return super().forward_cuda(query, key, value, attn_metadata)
original_query, original_key, original_value = query, key, value
original_seq_len = query.shape[1]
valid_seq_len = original_seq_len
if attn_metadata is not None and attn_metadata.packed_padding is not None:
valid_seq_len = attn_metadata.packed_padding.q_length
if attn_metadata.packed_padding.kv_length != valid_seq_len:
return self._fallback(
original_query, original_key, original_value, attn_metadata, "packed Q/KV lengths must match"
)
query = query[:, :valid_seq_len]
key = key[:, :valid_seq_len]
value = value[:, :valid_seq_len]
try:
output = self._forward_h3(query, key, value, attn_metadata)
if valid_seq_len == original_seq_len:
return output
restored = torch.zeros_like(original_query)
restored[:, :valid_seq_len] = output
return restored
except Exception as exc:
# A CUDA fault poisons the process context; attempting SDPA
# afterwards obscures the original kernel failure and cannot
# recover the request.
if isinstance(exc, torch.AcceleratorError):
raise
if not self.fallback_on_error:
raise
return self._fallback(
original_query, original_key, original_value, attn_metadata, f"VSA-H3 kernel failed: {exc}"
)
@@ -33,12 +33,6 @@ from vllm_omni.diffusion.attention.backends.abstract import (
VideoTokenLayout,
)
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.attention.ops.minimax_h3_modulation import (
indexed_gate,
indexed_gate_rms_norm_scale_shift,
indexed_scale_shift_,
rms_norm_indexed_scale_shift,
)
from vllm_omni.diffusion.cache.cachedit import CacheDiTAdapterConfig
from vllm_omni.diffusion.distributed.sp_plan import (
SequenceParallelInput,
@@ -50,6 +44,12 @@ from vllm_omni.diffusion.forward_context import (
)
from vllm_omni.diffusion.layers.activation import SiluAndMul
from vllm_omni.diffusion.layers.fused_qk_norm_rope import fused_qk_norm_rope
from vllm_omni.diffusion.layers.indexed_modulation import (
indexed_gate,
indexed_gate_rms_norm_scale_shift,
indexed_scale_shift_,
rms_norm_indexed_scale_shift,
)
from vllm_omni.diffusion.layers.norm import RMSNorm
from vllm_omni.diffusion.layers.rope import RotaryEmbedding
from vllm_omni.diffusion.models.host_weight_contract import FinalLayoutModelContract
@@ -442,6 +442,8 @@ class MiniMaxH3Attention(nn.Module):
self._gate_hidden_size = arch.hidden_size
self._gate_quant_config = quant_config
self._gate_prefix = f"{prefix}.to_gate_compress"
from .attention.fastvideo_h3 import MiniMaxH3VSAImpl
self.attention = Attention(
num_heads=self.num_heads,
num_kv_heads=self.num_kv_heads,
@@ -454,6 +456,7 @@ class MiniMaxH3Attention(nn.Module):
role_category=role_category,
skip_sequence_parallel=skip_sequence_parallel,
prefix=prefix,
impl_overrides={"FASTVIDEO_VSA": MiniMaxH3VSAImpl},
)
def enable_vsa_gate(self) -> None: