[Hardware][Ascend] Integrate MindIE dense and sparse quantized attention for Wan2.2 T2V (#7561)

Signed-off-by: zqxu <xuzhenqiang3@huawei.com>
Co-authored-by: zqxu <xuzhenqiang3@huawei.com>
This commit is contained in:
Zhenqiang Xuandzqxu authored and GitHub committed 2026-09-18 01:28:27 +00:00
1 parent f8197e120f
commit 242c3cddeb
15 files changed
+853 -368

No files matched your search

@@ -78,3 +78,35 @@ Protected video tails require a compatible MindIE-SD release.
For common configuration and selector behavior, see the
[attention backend overview](../attention_backends.md) and the
[backend selection design](../../../design/feature/attention_backend_selection.md).
## Wan2.2 T2V quantization on Ascend
`RAINFUSION_ATTN` accepts `diffusion_kv_cache_dtype: fp8` or `mxfp4` for
Wan2.2 T2V A14B.
MindIE-SD must expose `sparse_attention` with `rf_v3` and an explicit `precision`
argument. Omni passes the selected precision directly to this API; the installed
native operators must support that precision. Unsupported inputs or an
incompatible Python API raise an actionable error; Omni does not change BSA
precision automatically. Update `diffusion_kv_cache_dtype` to a precision
supported by the installed MindIE-SD, or unset it. Native execution errors also
propagate without retry.
BSA FP8 and MXFP4 retain MindIE-SD's default rotation policy.
`diffusion_kv_cache_skip_layers`/`diffusion_kv_cache_skip_steps` explicitly
select floating-point BSA and keep attention sparse. The existing `block_sparse`
warmup/layer exclusions and short-sequence threshold instead select Dense
attention, using the configured Dense precision. Leave `block_sparse.precision`
at its default when using `diffusion_kv_cache_dtype` as the shared Dense/BSA
precision setting.
Configure BSA quantization in the model's existing deployment YAML with
`backend: RAINFUSION_ATTN` and stage-level `diffusion_kv_cache_dtype: fp8` or
`mxfp4`. The precision and skip selectors are siblings of
`diffusion_attention_config`; they are not per-role `quant` fields.
For 40 denoising steps, `diffusion_kv_cache_skip_steps: "0,1,38,39"` and
`diffusion_kv_cache_skip_layers: "0,39"` are recommended starting points. These
fixed step indices do not automatically follow a changed step count. Selected
forwards remain floating-point BSA; unsupported BSA precision is an error rather
than an automatic fallback. See
[quantized attention](../../quantization/quantized_kvcache.md#wan22-t2v-quantized-attention-on-ascend)
for the shared configuration structure and index semantics.
+1 -1
View File
@@ -13,7 +13,7 @@ For the internal architecture and backend extension points, see the
| Mode | Guide | Description | Methods |
| ------ | ------- | ------------- | --------- |
| Online quantization | [Online Quantization](online.md) | vLLM-Omni computes quantized weights and scales while loading the model. | FP8 W8A8, Int8 W8A8, BitsAndBytes W4, MXFP8 W8A8, MXFP4 W4A4 |
| Runtime attention quantization | [Quantized KV Cache](quantized_kvcache.md) | vLLM-Omni dynamically quantizes eligible diffusion Flash Attention tensors during inference. | FP8 FA |
| Runtime attention quantization | [Quantized KV Cache](quantized_kvcache.md) | vLLM-Omni dynamically quantizes eligible diffusion attention tensors during inference. | FP8, MXFP8, MXFP4 |
| Pre-quantized checkpoints | Method-specific guides | The checkpoint or an offline quantizer provides quantized weights and scales before serving. | ModelOpt, AutoRound, TorchAO, msModelSlim, serialized Int8, offline MXFP8, offline MXFP4 DualScale |
## Hardware Support
@@ -2,41 +2,36 @@
## Overview
In DiT-based image and video generation, Flash Attention can take a large share
of denoising time, especially for high-resolution or long-frame workloads.
vLLM-Omni supports online FP8 quantization for eligible diffusion Flash
Attention (FA) to reduce FA latency while keeping model weights in their
original dtype.
In DiT-based image and video generation, attention can take a large share of
denoising time, especially for high-resolution or long-frame workloads.
vLLM-Omni supports online Q/K/V quantization for eligible diffusion attention
paths while keeping model weights in their original dtype.
This feature is configured through `diffusion_kv_cache_dtype` on
`OmniDiffusionConfig` (CLI: `--diffusion-kv-cache-dtype`). It is intentionally
**not** the same as vLLM's `--kv-cache-dtype`, which controls autoregressive
language-model KV cache storage and defaults to `"auto"`. Diffusion FA
quantization uses the dedicated diffusion flags so omni serve does not inherit
that default.
Runtime attention precision is configured through `diffusion_kv_cache_dtype`
on `OmniDiffusionConfig` (CLI: `--diffusion-kv-cache-dtype`). This setting is
separate from vLLM's `--kv-cache-dtype`, which controls autoregressive
language-model KV cache storage.
In vLLM-Omni diffusion pipelines, this is a runtime FA path: Q/K/V tensors are
dynamically quantized before the attention operator. It does not quantize model
weights and is separate from [FP8 W8A8](fp8.md), [Int8 W8A8](int8.md), or
pre-quantized checkpoint formats.
If `diffusion_kv_cache_dtype` is not set, behavior is unchanged and attention
runs in the native dtype.
If `diffusion_kv_cache_dtype` is not set, attention runs in the native dtype.
## Hardware Support
| Device | FP8 FA |
|--------|--------|
| Ascend NPU | ✅ |
| NVIDIA GPU | ❌ |
| AMD ROCm | ❌ |
| Intel XPU | ❌ |
| Device | Dense FP8 | Dense MXFP8/MXFP4 | BSA FP8/MXFP4 |
|--------|------------|-------------------|----------------|
| Ascend NPU | ✅ | ✅ | ✅ |
| NVIDIA GPU | ❌ | ❌ | ❌ |
| AMD ROCm | ❌ | ❌ | ❌ |
| Intel XPU | ❌ | ❌ | ❌ |
Legend: `✅` supported, `❌` unsupported.
FP8 FA is currently implemented only for the NPU Flash Attention backend. Other
backends do not support `diffusion_kv_cache_dtype="fp8"` for diffusion attention
and fall back to native dtype execution.
These runtime methods are implemented only by the NPU Flash Attention and BSA
backends. Other backends reject an incompatible explicit configuration.
## Model Type Support
@@ -44,7 +39,7 @@ and fall back to native dtype execution.
| Model | Scope | Status | Notes |
|-------|-------|--------|-------|
| Wan2.2 | Eligible DiT full-attention FA on Ascend NPU | Tested | Compare quality and latency against a BF16 baseline before production use |
| Wan2.2 | Self-attention on Ascend NPU | NPU interface and T2V smoke tested | Compare quality and latency against a BF16 baseline before production use |
| Other diffusion models | Eligible DiT full-attention FA on Ascend NPU | Not tested | You can try `diffusion_kv_cache_dtype="fp8"`; tune `diffusion_kv_cache_skip_steps` and `diffusion_kv_cache_skip_layers` when higher precision is needed |
### Multi-Stage Omni/TTS Model (Qwen3-Omni, Qwen3-TTS)
@@ -105,12 +100,13 @@ The legacy keyword aliases `kv_cache_dtype`, `kv_cache_skip_steps`, and
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `diffusion_kv_cache_dtype` | str \| None | `None` | Set to `"fp8"` to enable dynamic FP8 FA on supported attention backends |
| `diffusion_kv_cache_dtype` | str \| None | `None` | Runtime attention method. Dense FA supports `fp8`, `mxfp8`, and `mxfp4`; BSA supports `fp8` and `mxfp4` |
| `diffusion_kv_cache_skip_steps` | str \| None | `None` | Denoising step selector to keep in native dtype, for example `"0,1,4-6"` |
| `diffusion_kv_cache_skip_layers` | str \| None | `None` | Transformer layer selector to keep in native dtype, for example `"0-2,10"` |
Selectors use comma-separated integers and inclusive ranges. Listed steps or
layers skip FP8 FA; all other eligible full-attention forwards use the FP8 path.
layers run floating-point attention through the selected Dense or BSA backend;
all other eligible forwards use `diffusion_kv_cache_dtype`.
## Validation and Notes
@@ -123,3 +119,60 @@ layers skip FP8 FA; all other eligible full-attention forwards use the FP8 path.
4. Report both latency and quality results when enabling this option for a new
model. For image or video models, include visual comparison and quantitative
metrics when available, such as PSNR or SSIM.
## Wan2.2 T2V quantized attention on Ascend
Wan2.2 T2V A14B supports quantized self-attention through MindIE-SD. Model
weights are unchanged, and model-level opt-out keeps cross-attention in its
native dtype.
| Attention path | self-attention `backend` | Supported `diffusion_kv_cache_dtype` |
| --- | --- | --- |
| Dense FA | `FLASH_ATTN` | `fp8`, `mxfp8`, `mxfp4` |
| BSA | `RAINFUSION_ATTN` | `fp8`, `mxfp4` |
Configure the method and fallback selectors once at the diffusion stage. The
per-role attention config only selects the self-attention backend:
```yaml
stages:
- stage_id: 0
diffusion_kv_cache_dtype: "mxfp8"
diffusion_kv_cache_skip_layers: "0,39"
diffusion_kv_cache_skip_steps: "0,1,38,39"
diffusion_attention_config:
per_role:
self:
backend: FLASH_ATTN # Use RAINFUSION_ATTN with fp8 or mxfp4 for BSA.
```
For 40 denoising steps, `diffusion_kv_cache_skip_steps: "0,1,38,39"` and
`diffusion_kv_cache_skip_layers: "0,39"` are recommended starting points. Steps
are zero-based across the complete request and do not reset when Wan switches
transformers; layer indices are zero-based and local to each transformer.
Selectors accept comma-separated indices and inclusive ranges such as
`"0,3-5"`. A selected forward uses floating-point attention while preserving
the Dense or BSA path.
Unsupported precision/input combinations raise an error and must be corrected
in the configuration; operator errors are not retried with another precision.
For BSA, change the backend to `RAINFUSION_ATTN` and set
`diffusion_kv_cache_dtype` to `fp8` or `mxfp4`.
See [RainFusion attention](../diffusion/attention_backends/rainfusion.md) for
sparse-path configuration and behavior.
### Requirement and interface test
Until these APIs are available in a MindIE-SD release, use revision
[`8637b5333b0225381b215390fd09a8732e671cc4`](https://gitcode.com/zqxu/MindIE-SD/commit/8637b5333b0225381b215390fd09a8732e671cc4).
Build the Python package, PyTorch plugin, and custom operators from that same
checkout. With CANN sourced, run the NPU interface tests from the Omni checkout:
```bash
python -m pytest tests/platforms/npu/quant/test_kv_quant_npu.py \
-k real_npu -vv -s -o addopts=''
```
A skipped NPU test is not a pass. Validate generated video quality against a
floating-point run with the same prompt, seed, resolution, and step count.
@@ -0,0 +1,160 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Cross-repository CPU dispatch contracts, requiring the matching MindIE-SD source.
Only native operators are replaced. Public adapters, rotations, layouts, padding,
scale preparation, sequence metadata and output cropping execute their real code.
Operator substitutes carry floating tensors; these are not numerical quantization tests.
"""
import importlib
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import torch
from vllm_omni.diffusion.attention.backends import flash_attn, rainfusion_attn
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.platforms.npu.quant.kv_quant_npu import get_quant_attention_rotation
mindiesd = pytest.importorskip("mindiesd")
torch_npu = importlib.import_module("torch_npu")
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@pytest.mark.parametrize(
"precision,batch,q_len,kv_len,q_heads",
[
("fp8", 1, 130, 130, 2),
("mxfp8", 1, 130, 130, 2),
("mxfp4", 1, 130, 130, 2),
("mxfp8", 2, 17, 8, 4),
("mxfp8", 2, 8, 17, 4),
],
)
@pytest.mark.parametrize("layout", ["BSND", "BNSD"])
def test_omni_calls_real_dense_public_api(monkeypatch, precision, layout, batch, q_len, kv_len, q_heads):
assert callable(getattr(mindiesd, "quant_attention", None)), "MindIE-SD must export quant_attention"
monkeypatch.setattr(flash_attn, "current_omni_platform", SimpleNamespace(is_npu=lambda: True, device_name="npu"))
monkeypatch.setattr(flash_attn, "get_current_diffusion_config_or_none", lambda: None)
quantized_inputs = []
def dynamic_quant(tensor, **kwargs):
quantized_inputs.append((tensor.clone(), kwargs))
shape = list(tensor.shape)
axis = kwargs.get("axis", -1)
shape[axis] = max(1, shape[axis] // 32)
return tensor, torch.ones(shape, dtype=torch.uint8)
monkeypatch.setattr(torch_npu, "npu_dynamic_block_quant", dynamic_quant, raising=False)
monkeypatch.setattr(torch_npu, "npu_dynamic_mx_quant", dynamic_quant, raising=False)
execute = Mock(side_effect=lambda query, *args, **kwargs: (query, None))
metadata = Mock(return_value=torch.zeros(1, dtype=torch.int32))
if precision == "fp8":
monkeypatch.setattr(torch.ops.mindiesd, "fused_infer_attention_score_v2", execute, raising=False)
elif precision == "mxfp8":
monkeypatch.setattr(torch_npu, "npu_fused_infer_attention_score_v2", execute, raising=False)
else:
monkeypatch.setattr(torch.ops.mindiesd, "quant_flash_attn", execute, raising=False)
monkeypatch.setattr(torch.ops.mindiesd, "quant_flash_attn_metadata", metadata, raising=False)
query = torch.randn(batch, q_len, q_heads, 64, dtype=torch.bfloat16)
key = torch.randn(batch, kv_len, 2, 64, dtype=torch.bfloat16)
value = torch.randn_like(key)
if layout == "BNSD":
query, key, value = (tensor.transpose(1, 2) for tensor in (query, key, value))
impl = flash_attn.FlashAttentionImpl(
num_heads=q_heads,
head_size=64,
softmax_scale=0.37,
qkv_layout=layout,
)
output = impl.forward_npu(query, key, value, AttentionMetadata(extra={"kv_cache_dtype": precision}))
rotation = get_quant_attention_rotation(query.device, query.dtype, 64)
expected = query @ rotation
torch.testing.assert_close(output, expected)
assert output.shape == query.shape and len(quantized_inputs) == 3
execute.assert_called_once()
kwargs = execute.call_args.kwargs
assert kwargs["softmax_scale"] == 0.37
if precision == "fp8":
assert kwargs["input_layout"] == "BNSD"
assert quantized_inputs[0][0].shape == (2, 130, 64)
assert quantized_inputs[0][1]["row_block_size"] == 128
elif precision == "mxfp8":
args = execute.call_args.args
assert args[0].shape == (batch * q_len, q_heads, 64)
assert args[1].shape == args[2].shape == (batch * kv_len, 2, 64)
assert kwargs["input_layout"] == "TND"
assert kwargs["num_query_heads"] == q_heads and kwargs["num_key_value_heads"] == 2
assert kwargs["actual_seq_qlen"] == [q_len * (i + 1) for i in range(batch)]
assert kwargs["actual_seq_kvlen"] == [kv_len * (i + 1) for i in range(batch)]
assert [entry[1]["axis"] for entry in quantized_inputs] == [-1, -1, 0]
else:
seq_axis = 1 if layout == "BSND" else 2
assert quantized_inputs[0][0].shape[seq_axis] == 512
assert kwargs["seqused_q"].tolist() == kwargs["seqused_kv"].tolist() == [512]
assert kwargs["layout_q"] == kwargs["layout_out"] == layout
assert kwargs["q_dtype"] == torch_npu.float4_e2m1fn_x2
assert kwargs["q_descale_dtype"] == torch_npu.float8_e8m0fnu
assert execute.call_args.args[3].shape[:2] == (1, 2)
assert [entry[1]["axis"] for entry in quantized_inputs] == [-1, -1, seq_axis]
assert kwargs["metadata"] is metadata.return_value
metadata.assert_called_once()
@pytest.mark.parametrize("precision", ["fp8", "mxfp4"])
@pytest.mark.parametrize("length", [65, 600])
def test_omni_calls_real_sparse_public_api(monkeypatch, precision, length):
rf = importlib.import_module("mindiesd.layers.flash_attn.sparse_flash_attn_rf_v3")
monkeypatch.setattr(flash_attn, "current_omni_platform", SimpleNamespace(is_npu=lambda: True, device_name="npu"))
for module in (flash_attn, rainfusion_attn):
monkeypatch.setattr(module, "get_current_diffusion_config_or_none", lambda: None)
rainfusion_attn._mindiesd_supports_precision.cache_clear()
monkeypatch.setattr(rf, "do_tensor_rearrange_only", lambda q, k, v, *a, **kw: (q, k, v))
monkeypatch.setattr(rf, "avgpool", lambda *a, **kw: torch.ones(1))
monkeypatch.setattr(rf, "_generate_mask_direct", lambda *a, **kw: torch.ones(1, 2, 8, 8))
monkeypatch.setattr(rf, "_bsa_inv_rearrange", lambda out, *a: out)
def quantize(tensor, **kwargs):
shape = list(tensor.shape)
axis = kwargs.get("axis", -1)
shape[axis] = max(1, shape[axis] // 32)
return tensor, torch.ones(shape, dtype=torch.uint8)
monkeypatch.setattr(torch_npu, "npu_dynamic_mx_quant", quantize, raising=False)
monkeypatch.setattr(torch_npu, "npu_dynamic_block_quant", quantize, raising=False)
execute = Mock(side_effect=lambda **kw: (torch.ones_like(kw["query"]), None))
monkeypatch.setattr(torch.ops.mindiesd, "block_sparse_attention", execute, raising=False)
query = torch.randn(1, length, 2, 64, dtype=torch.bfloat16)
impl = rainfusion_attn.RainFusionAttentionImpl(
num_heads=2,
head_size=64,
softmax_scale=0.37,
qkv_layout="BSND",
backend_kwargs={"sparsity": 0.8},
)
plan = rainfusion_attn.RainFusionPlan(used_len=length, prefix_len=0, latent_shape=(1, 1, length))
try:
output = impl._forward_sparse_npu(
query,
query,
query,
plan,
AttentionMetadata(extra={"kv_cache_dtype": precision}),
)
finally:
rainfusion_attn._mindiesd_supports_precision.cache_clear()
torch.testing.assert_close(output, torch.ones_like(query))
execute.assert_called_once()
kwargs = execute.call_args.kwargs
assert kwargs["actual_seq_lengths"] == kwargs["actual_seq_lengths_kv"] == [length]
assert kwargs["q_input_layout"] == "BNSD"
assert kwargs["block_sparse_mask"] is not None
if precision == "mxfp4":
assert kwargs["query"].shape[2] == (length + 63) // 64 * 64
assert kwargs["quant_mode"] == 2
assert kwargs["q_dtype"] == torch_npu.float4_e2m1fn_x2
assert kwargs["v_scale_dtype"] == torch_npu.float8_e8m0fnu
@@ -0,0 +1,319 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""CPU contract tests for MindIE adapters; no NPU kernels are mocked in production."""
import sys
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock
import pytest
import torch
from vllm_omni.diffusion.attention import layer as layer_mod
from vllm_omni.diffusion.attention.backends import flash_attn, rainfusion_attn
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata, VideoTokenLayout
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
_DEFAULT_SKIP_STEPS = frozenset({0, 1, 38, 39})
@pytest.fixture
def runtime(monkeypatch):
mod = ModuleType("mindiesd")
def execute(q, k, v, *, precision, layout, scale, q_rot=None, k_rot=None):
return q
setattr(mod, "quant_attention", Mock(side_effect=execute))
monkeypatch.setitem(sys.modules, mod.__name__, mod)
for module in (flash_attn, layer_mod):
monkeypatch.setattr(module, "current_omni_platform", SimpleNamespace(is_npu=lambda: True, device_name="npu"))
monkeypatch.setattr(flash_attn, "get_current_diffusion_config_or_none", lambda: None)
monkeypatch.setattr(rainfusion_attn, "get_current_diffusion_config_or_none", lambda: None)
monkeypatch.setattr(rainfusion_attn, "is_forward_context_available", lambda: False)
supports_precision = rainfusion_attn._mindiesd_supports_precision
supports_precision.cache_clear()
yield mod
supports_precision.cache_clear()
def flash(layout="BSND"):
return flash_attn.FlashAttentionImpl(
num_heads=2,
head_size=64,
softmax_scale=0.37,
qkv_layout=layout,
)
def sparse(**kwargs):
return rainfusion_attn.RainFusionAttentionImpl(
num_heads=2,
head_size=64,
softmax_scale=0.37,
qkv_layout="BSND",
backend_kwargs={"sparsity": 0.8, **kwargs},
)
def video_metadata(**extra):
return AttentionMetadata(
video_layout=VideoTokenLayout(prefix_len=0, latent_grid=(4, 32, 32), used_len=4096),
extra={"max_seqlen_q": 4096, **extra},
)
@pytest.mark.parametrize("method", ["fp8", "mxfp8", "mxfp4"])
@pytest.mark.parametrize("layout", [None, "BSND", "BNSD"])
def test_exact_runtime_layout_scale_and_rotation(runtime, method, layout):
q = torch.randn(1, 128, 2, 64, dtype=torch.bfloat16)
if layout == "BNSD":
q = q.transpose(1, 2)
impl = flash(layout=layout)
metadata = AttentionMetadata(extra={"kv_cache_dtype": method})
assert impl.forward_npu(q, q, q, metadata) is q
args, kwargs = runtime.quant_attention.call_args
assert args[0] is q # no unconditional transpose or extra quantization
assert kwargs["layout"] == (layout or "BSND") and kwargs["scale"] == 0.37 and kwargs["precision"] == method
assert "attn_mask" not in kwargs
from vllm_omni.platforms.npu.quant.kv_quant_npu import get_quant_attention_rotation
assert kwargs["q_rot"] is kwargs["k_rot"]
torch.testing.assert_close(kwargs["q_rot"], get_quant_attention_rotation(q.device, q.dtype, 64))
def test_missing_runtime_requires_config_change(runtime):
q = torch.randn(1, 128, 2, 64, dtype=torch.bfloat16)
del runtime.quant_attention
with pytest.raises(ImportError, match="diffusion_kv_cache_dtype"):
flash().forward_npu(q, q, q, AttentionMetadata(extra={"kv_cache_dtype": "mxfp4"}))
def test_operator_failure_never_falls_back(runtime):
q = torch.randn(1, 128, 2, 64, dtype=torch.bfloat16)
runtime.quant_attention.side_effect = RuntimeError("operator failure")
with pytest.raises(RuntimeError, match="operator failure"):
flash().forward_npu(q, q, q, AttentionMetadata(extra={"kv_cache_dtype": "mxfp4"}))
runtime.quant_attention.assert_called_once()
@pytest.mark.parametrize("method", ["fp8", "mxfp8", "mxfp4"])
def test_unsupported_shape_requires_config_change(runtime, method):
q = torch.randn(1, 128, 2, 96, dtype=torch.bfloat16)
with pytest.raises(ValueError, match="diffusion_kv_cache_dtype"):
flash().forward_npu(q, q, q, AttentionMetadata(extra={"kv_cache_dtype": method}))
runtime.quant_attention.assert_not_called()
def test_packed_metadata_never_reaches_quant_runtime(runtime):
q = torch.randn(1, 128, 2, 64, dtype=torch.bfloat16)
metadata = AttentionMetadata(extra={"kv_cache_dtype": "mxfp8", "cu_seqlens_q": torch.tensor([0, 64, 128])})
with pytest.raises(ValueError, match="packed/varlen"):
flash().forward_npu(q, q, q, metadata)
runtime.quant_attention.assert_not_called()
def make_layer(runtime, monkeypatch, method="mxfp8", skip_steps=_DEFAULT_SKIP_STEPS):
layer = object.__new__(layer_mod.Attention)
torch.nn.Module.__init__(layer)
layer.attention = flash()
layer.attn_backend = flash_attn.FlashAttentionBackend
layer._disable_kv_quant = False
layer.layer_idx = 3
cfg = SimpleNamespace(
diffusion_kv_cache_dtype=method,
diffusion_kv_cache_skip_step_indices=skip_steps,
diffusion_kv_cache_skip_layer_indices=None,
parallel_config=SimpleNamespace(ring_degree=1),
)
layer._init_kv_cache_quantization(cfg)
return layer
@pytest.mark.parametrize("method", ["fp8", "mxfp8", "mxfp4"])
def test_global_quant_method_is_published_per_forward(runtime, monkeypatch, method):
layer = make_layer(runtime, monkeypatch, method=method, skip_steps=None)
assert layer._with_kv_cache_dtype(None).extra["kv_cache_dtype"] == method
@pytest.mark.parametrize("method", [None, "auto", "float"])
def test_optout_without_quant_policy_keeps_metadata_unchanged(runtime, monkeypatch, method):
layer = make_layer(runtime, monkeypatch)
layer._disable_kv_quant = True
layer._init_kv_cache_quantization(
SimpleNamespace(
diffusion_kv_cache_dtype=method,
diffusion_kv_cache_skip_step_indices=None,
diffusion_kv_cache_skip_layer_indices=None,
parallel_config=SimpleNamespace(ring_degree=1),
)
)
source = video_metadata()
assert layer._with_kv_cache_dtype(source) is source
def test_step_skip_is_request_local_and_cross_optout(runtime, monkeypatch):
layer = make_layer(runtime, monkeypatch)
ctx = SimpleNamespace(denoise_step_idx=0)
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: True)
monkeypatch.setattr(layer_mod, "get_forward_context", lambda: ctx)
source = video_metadata()
for step in list(range(40)) * 2:
ctx.denoise_step_idx = step
resolved = layer._with_kv_cache_dtype(source)
assert resolved.extra["kv_cache_dtype"] == ("float" if step in (0, 1, 38, 39) else "mxfp8")
assert resolved.video_layout is source.video_layout
assert source.extra == {"max_seqlen_q": 4096}
layer._disable_kv_quant = True
assert layer._with_kv_cache_dtype(source).extra["kv_cache_dtype"] == "float"
def test_ring_attention_is_rejected(runtime, monkeypatch):
layer = make_layer(runtime, monkeypatch)
cfg = SimpleNamespace(diffusion_kv_cache_dtype="mxfp8", parallel_config=SimpleNamespace(ring_degree=2))
with pytest.raises(ValueError, match="ring"):
layer._init_kv_cache_quantization(cfg)
def test_sparse_tail_steps_and_dense_quant_dispatch(runtime, monkeypatch):
ctx = SimpleNamespace(denoise_step_idx=49, total_denoise_steps=50)
monkeypatch.setattr(rainfusion_attn, "is_forward_context_available", lambda: True)
monkeypatch.setattr(rainfusion_attn, "get_forward_context", lambda: ctx)
impl = sparse(start_step=2, end_step=2)
metadata = video_metadata(kv_cache_dtype="fp8")
assert impl._resolve_plan(metadata) is None
q = torch.randn(1, 4096, 2, 64, dtype=torch.bfloat16)
impl.forward_npu(q, q, q, metadata)
assert runtime.quant_attention.call_args.kwargs["precision"] == "fp8"
@pytest.mark.parametrize("method", ["fp8", "mxfp4"])
def test_sparse_quant_uses_public_runtime(runtime, monkeypatch, method):
runtime.sparse_attention = Mock(side_effect=lambda q, k, v, **kwargs: q)
monkeypatch.setattr(rainfusion_attn, "_mindiesd_supports_precision", lambda: True)
q = torch.randn(1, 4224, 2, 64, dtype=torch.bfloat16)
out = sparse().forward_npu(q, q, q, video_metadata(kv_cache_dtype=method))
assert out.shape == q.shape and not out[:, 4096:].any()
kwargs = runtime.sparse_attention.call_args.kwargs
assert (kwargs["precision"], kwargs["sparse_type"], kwargs["inner_precise"]) == (method, "rf_v3", 4)
runtime.quant_attention.assert_not_called()
def test_sparse_custom_mask_stays_dense(runtime):
metadata = AttentionMetadata(
attn_mask=torch.ones(1, 4096, dtype=torch.bool),
video_layout=VideoTokenLayout(prefix_len=0, latent_grid=(4, 32, 32)),
extra={"max_seqlen_q": 4096},
)
assert sparse()._resolve_plan(metadata) is None
def test_sparse_model_padding_mask_is_equivalent_to_trimming(runtime):
metadata = video_metadata()
metadata.attn_mask = torch.arange(4224)[None] < 4096
assert sparse()._resolve_plan(metadata).used_len == 4096
def test_bsa_operator_failure_never_retries(runtime, monkeypatch):
runtime.sparse_attention = Mock(side_effect=RuntimeError("BSA operator failure"))
monkeypatch.setattr(rainfusion_attn, "_mindiesd_supports_precision", lambda: True)
q = torch.randn(1, 4096, 2, 64, dtype=torch.bfloat16)
with pytest.raises(RuntimeError, match="BSA operator failure"):
sparse().forward_npu(q, q, q, video_metadata(kv_cache_dtype="mxfp4"))
runtime.sparse_attention.assert_called_once()
@pytest.mark.parametrize(
"method,shape,supports_precision,error",
[
("mxfp8", (1, 4096, 2, 64), True, "sparse mxfp8 is not supported"),
("mxfp4", (1, 4096, 2, 64), False, "explicitly support precision"),
("fp8", (2, 4096, 2, 64), True, "batch size 1"),
("fp8", (1, 4096, 2, 32), True, "power-of-two head dimension"),
("mxfp4", (1, 4096, 2, 96), True, "power-of-two head dimension"),
],
)
def test_invalid_sparse_quant_requires_config_change(runtime, monkeypatch, method, shape, supports_precision, error):
runtime.sparse_attention = Mock(side_effect=lambda q, k, v, **kwargs: q)
monkeypatch.setattr(rainfusion_attn, "_mindiesd_supports_precision", lambda: supports_precision)
q = torch.randn(shape, dtype=torch.bfloat16)
with pytest.raises(ValueError, match=error):
sparse().forward_npu(q, q, q, video_metadata(kv_cache_dtype=method))
runtime.sparse_attention.assert_not_called()
def test_layer_selector_requires_index_except_cross_optout(runtime, monkeypatch):
layer = make_layer(runtime, monkeypatch)
layer.layer_idx = None
config = SimpleNamespace(
diffusion_kv_cache_dtype="mxfp8",
diffusion_kv_cache_skip_step_indices=None,
diffusion_kv_cache_skip_layer_indices={0, 3},
parallel_config=SimpleNamespace(ring_degree=1),
)
with pytest.raises(ValueError, match="parseable transformer block index"):
layer._init_kv_cache_quantization(config)
layer._disable_kv_quant = True
layer._init_kv_cache_quantization(config)
assert layer._with_kv_cache_dtype(None).extra["kv_cache_dtype"] == "float"
@pytest.mark.parametrize("prefix", ["transformer.blocks.3.attn1", "transformer_2.blocks.3.attn1"])
@pytest.mark.parametrize("method", ["fp8", "mxfp4"])
def test_expert_layer_and_step_skip_remain_sparse(runtime, monkeypatch, prefix, method):
def execute(q, k, v, *, precision="bf16", **kwargs):
return q
runtime.sparse_attention = Mock(wraps=execute)
monkeypatch.setattr(rainfusion_attn, "_mindiesd_supports_precision", lambda: True)
layer = make_layer(runtime, monkeypatch)
layer.layer_idx = rainfusion_attn._try_extract_layer_index(prefix)
assert layer.layer_idx == 3
config = SimpleNamespace(
diffusion_kv_cache_dtype=method,
diffusion_kv_cache_skip_step_indices={0, 1, 38, 39},
diffusion_kv_cache_skip_layer_indices={3},
parallel_config=SimpleNamespace(ring_degree=1),
)
impl = sparse()
layer.attention = impl
layer.attn_backend = rainfusion_attn.RainFusionAttentionBackend
layer._init_kv_cache_quantization(config)
ctx = SimpleNamespace(denoise_step_idx=0)
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: True)
monkeypatch.setattr(layer_mod, "get_forward_context", lambda: ctx)
source = video_metadata(kv_cache_dtype="mxfp8")
q = torch.randn(1, 4096, 2, 64, dtype=torch.bfloat16)
for layer_idx, step in ((3, 0), (3, 2), (4, 0), (4, 1), (4, 2), (4, 37), (4, 38), (4, 39), (4, 0)):
expected = "bf16" if layer_idx == 3 or step in (0, 1, 38, 39) else method
layer.layer_idx = layer_idx
ctx.denoise_step_idx = step
metadata = layer._with_kv_cache_dtype(source)
impl.forward_npu(q, q, q, metadata)
assert runtime.sparse_attention.call_args.kwargs["precision"] == expected
runtime.quant_attention.assert_not_called()
assert source.extra["kv_cache_dtype"] == "mxfp8"
def test_custom_attention_initialization_keeps_upstream_optout(monkeypatch):
monkeypatch.setattr(layer_mod, "get_current_diffusion_config_or_none", lambda: None)
monkeypatch.setattr(layer_mod, "build_parallel_attention_strategy", lambda **kw: object())
monkeypatch.setattr(layer_mod, "NoParallelAttention", lambda: object())
custom = torch.nn.Identity()
layer = layer_mod.Attention(
num_heads=2,
head_size=64,
softmax_scale=0.125,
causal=False,
custom_attention=custom,
skip_sequence_parallel=True,
)
assert layer.attention is custom and layer._kv_cache_dtype is None
def test_legacy_global_fp8_uses_implicit_bsnd(runtime):
q = torch.randn(1, 17, 2, 64, dtype=torch.bfloat16)
impl = flash(layout=None)
assert impl.forward_npu(q, q, q, AttentionMetadata(extra={"kv_cache_dtype": "fp8"})) is q
assert runtime.quant_attention.call_args.kwargs["layout"] == "BSND"
assert runtime.quant_attention.call_args.kwargs["precision"] == "fp8"
@@ -133,18 +133,17 @@ def test_invalid_ref2va_video_spans_fall_back_to_dense():
assert make_impl()._resolve_plan(AttentionMetadata(extra={"max_seqlen_q": 12000}, video_layout=layout)) is None
def test_validate_available_rejects_legacy_mindiesd(monkeypatch):
def test_validate_available_accepts_single_video_mindiesd(monkeypatch):
mindiesd = types.ModuleType("mindiesd")
def sparse_attention(query, key, value, **kwargs):
return query
mindiesd.sparse_attention = sparse_attention
monkeypatch.setattr(mindiesd, "sparse_attention", sparse_attention, raising=False)
monkeypatch.setitem(sys.modules, "mindiesd", mindiesd)
monkeypatch.setattr("importlib.util.find_spec", lambda _: object())
with pytest.raises(ValueError, match="video_spans"):
RainFusionAttentionBackend.validate_available()
RainFusionAttentionBackend.validate_available()
def test_validate_available_accepts_new_mindiesd(monkeypatch):
@@ -153,7 +152,7 @@ def test_validate_available_accepts_new_mindiesd(monkeypatch):
def sparse_attention(query, key, value, *, video_spans=None, **kwargs):
return query
mindiesd.sparse_attention = sparse_attention
monkeypatch.setattr(mindiesd, "sparse_attention", sparse_attention, raising=False)
monkeypatch.setitem(sys.modules, "mindiesd", mindiesd)
monkeypatch.setattr("importlib.util.find_spec", lambda _: object())
@@ -300,22 +299,21 @@ def _fake_mindiesd_module():
import types
fake = types.ModuleType("mindiesd")
fake.sparse_attention = lambda *args, **kwargs: None
setattr(fake, "sparse_attention", lambda *args, **kwargs: None)
return fake
def test_precision_non_bf16_requires_mindiesd_support():
"""precision != bf16 against a mindiesd lacking the kwarg must raise RuntimeError."""
"""Unsupported sparse precision must fail before any operator executes."""
import sys
impl = make_impl(precision="mix")
sys.modules["mindiesd"] = _fake_mindiesd_module()
try:
with mock.patch.dict(sys.modules, {"mindiesd": _fake_mindiesd_module()}):
with mock.patch.object(rainfusion_attn, "_mindiesd_supports_precision", return_value=False):
with pytest.raises(RuntimeError, match="requires MindIE-SD"):
impl._forward_sparse_npu(None, None, None, None)
finally:
sys.modules.pop("mindiesd", None)
with pytest.raises(ValueError, match="explicitly support precision"):
impl._forward_sparse_npu(
None, None, None, RainFusionPlan(prefix_len=0, used_len=8, latent_shape=[2, 2, 2])
)
def test_precision_non_bf16_passes_gate_when_supported():
@@ -323,8 +321,7 @@ def test_precision_non_bf16_passes_gate_when_supported():
import sys
impl = make_impl(precision="mix")
sys.modules["mindiesd"] = _fake_mindiesd_module()
try:
with mock.patch.dict(sys.modules, {"mindiesd": _fake_mindiesd_module()}):
with mock.patch.object(rainfusion_attn, "_mindiesd_supports_precision", return_value=True):
# q/k/v shapes: [B, S, N, D]; plan geometry must match S.
q = torch.randn(1, 8, 4, 128)
@@ -332,5 +329,3 @@ def test_precision_non_bf16_passes_gate_when_supported():
# The gate must pass; the fake mindiesd returns None so no crash.
out = impl._forward_sparse_npu(q, q, q, plan)
assert out is None
finally:
sys.modules.pop("mindiesd", None)
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Wan integration contracts required by quantized dense/sparse attention."""
from unittest.mock import Mock
import pytest
import torch
from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanTransformerBlock
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def test_block_publishes_unpadded_grid_without_partial_packed_metadata():
block = object.__new__(WanTransformerBlock)
torch.nn.Module.__init__(block)
block.scale_shift_table = torch.zeros(1, 6, 4)
block.norm1 = lambda x, *a: x
block.norm2 = lambda x: x
block.norm3 = lambda x, *a: x
block.attn1 = Mock(side_effect=lambda x, *a: torch.zeros_like(x))
block.attn2 = Mock(side_effect=lambda x, *a: torch.zeros_like(x))
block.ffn = lambda x: torch.zeros_like(x)
# Local SP tokens include alignment padding; grid describes the full valid video.
x = torch.zeros(1, 1152, 4)
block(
x,
x,
torch.zeros(1, 6, 4),
None,
hidden_states_mask=torch.ones(1, 1152, dtype=torch.bool),
vsa_dit_seq_shape=(5, 16, 27),
)
metadata = block.attn1.call_args.args[2]
assert metadata.video_layout.latent_grid == (5, 16, 27)
assert metadata.video_layout.used_len == 2160
assert metadata.video_layout.prefix_len == 0
assert "max_seqlen_q" not in metadata.extra # CUDA must not infer incomplete packed varlen.
assert block.attn2.call_args.args[2] is None
+35 -145
View File
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for NPU FP8 KV quantization helpers.
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Tests for seeded rotations and the quantized NPU attention backend.
These tests load ``kv_quant_npu`` from its source file via ``importlib`` so
the test module itself does not ``import vllm_omni`` (which would pull
@@ -10,10 +10,8 @@ the test module itself does not ``import vllm_omni`` (which would pull
from __future__ import annotations
import importlib.util
import math
from pathlib import Path
from types import ModuleType, SimpleNamespace
from typing import Any
from types import ModuleType
import pytest
import torch
@@ -61,147 +59,39 @@ def _npu_smoke_available() -> bool:
npu_smoke = pytest.mark.skipif(not _npu_smoke_available(), reason="NPU device or torch_npu not available.")
def test_is_quantized_kv_cache() -> None:
assert kv_quant_npu.is_quantized_kv_cache("fp8")
assert not kv_quant_npu.is_quantized_kv_cache(None)
assert not kv_quant_npu.is_quantized_kv_cache("int8")
class TestKVQuantNPUUnit:
@pytest.fixture(autouse=True)
def clear_rot_cache(self):
kv_quant_npu._ROT_MATRIXS.clear()
def test_get_rot_matrix_caches_by_device_dtype_and_head_dim(self) -> None:
calls = {"count": 0}
class FakeQuaRotMode:
HADAMARD = "hadamard"
def fake_create_rot(mode, head_dim, seed):
calls["count"] += 1
assert mode == FakeQuaRotMode.HADAMARD
assert seed == 425500
return torch.eye(head_dim, dtype=torch.float32)
device = torch.device("cpu")
rot_1 = kv_quant_npu._get_rot_matrix(device, torch.float16, 8, FakeQuaRotMode, fake_create_rot)
rot_2 = kv_quant_npu._get_rot_matrix(device, torch.float16, 8, FakeQuaRotMode, fake_create_rot)
rot_3 = kv_quant_npu._get_rot_matrix(device, torch.bfloat16, 8, FakeQuaRotMode, fake_create_rot)
rot_4 = kv_quant_npu._get_rot_matrix(device, torch.float16, 16, FakeQuaRotMode, fake_create_rot)
assert calls["count"] == 3
assert rot_1 is rot_2
assert rot_3.dtype == torch.bfloat16
assert rot_4.shape == (16, 16)
@pytest.fixture
def fake_quant_ops(self, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
captured: dict[str, Any] = {
"fa_calls": [],
"npu_kwargs": None,
"out_shape": None,
}
class FakeTorchNPU:
float8_e4m3fn = "fp8_marker"
@staticmethod
def npu_fused_infer_attention_score_v2(q, k, v, **kwargs):
del q, k, v
captured["npu_kwargs"] = kwargs
out_shape = captured["out_shape"]
return (torch.ones(out_shape, dtype=torch.float32),)
def fake_fa_block_quant_preprocess(x, block_size, dst_type, layout):
captured["fa_calls"].append(
{
"block_size": block_size,
"layout": layout,
"dst_type": dst_type,
"shape": tuple(x.shape),
}
)
scale = torch.full((1,), float(block_size), dtype=torch.float32)
return x, scale
fake_qua_rot_mode = SimpleNamespace(HADAMARD="hadamard")
def fake_create_rot(mode, head_dim, seed):
assert mode == "hadamard"
assert seed == 425500
return torch.eye(head_dim, dtype=torch.float32)
monkeypatch.setattr(
kv_quant_npu,
"_load_quant_ops",
lambda: (FakeTorchNPU, fake_fa_block_quant_preprocess, fake_qua_rot_mode, fake_create_rot),
)
return captured
@staticmethod
def _make_qkv(shape: tuple[int, int, int, int]) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
query = torch.randn(*shape, dtype=torch.float32)
key = torch.randn(*shape, dtype=torch.float32)
value = torch.randn(*shape, dtype=torch.float32)
return query, key, value
@pytest.mark.parametrize(
"layout,input_shape,out_shape,softmax_scale,expected_scale",
[
("BNSD", (2, 3, 4, 8), (2, 3, 6, 8), None, 1.0 / math.sqrt(8)),
("BSND", (2, 4, 3, 8), (2, 6, 3, 8), 0.125, 0.125),
],
)
def test_fp8_rotate_quant_fa_layouts_scale_and_crop(
self,
fake_quant_ops: dict[str, Any],
layout: str,
input_shape: tuple[int, int, int, int],
out_shape: tuple[int, int, int, int],
softmax_scale: float | None,
expected_scale: float,
) -> None:
query, key, value = self._make_qkv(input_shape)
fake_quant_ops["out_shape"] = out_shape
out = kv_quant_npu.fp8_rotate_quant_fa(query, key, value, layout=layout, softmax_scale=softmax_scale)
assert out.shape == query.shape
assert out.dtype == query.dtype
assert fake_quant_ops["npu_kwargs"]["input_layout"] == layout
# BNSD: shape[1]==heads, BSND: shape[2]==heads.
expected_heads = input_shape[1] if layout == "BNSD" else input_shape[2]
assert fake_quant_ops["npu_kwargs"]["num_query_heads"] == expected_heads
assert fake_quant_ops["npu_kwargs"]["softmax_scale"] == pytest.approx(expected_scale)
assert [call["block_size"] for call in fake_quant_ops["fa_calls"]] == [128, 256, 256]
def test_fp8_rotate_quant_fa_invalid_layout_raises(self, fake_quant_ops) -> None:
query = torch.randn(1, 2, 3, 4, dtype=torch.float32)
key = torch.randn(1, 2, 3, 4, dtype=torch.float32)
value = torch.randn(1, 2, 3, 4, dtype=torch.float32)
fake_quant_ops["out_shape"] = (1, 2, 3, 4)
with pytest.raises(ValueError, match="unsupported layout"):
kv_quant_npu.fp8_rotate_quant_fa(query, key, value, layout="INVALID")
def test_rotation_is_fixed_orthogonal_cached_and_preserves_rng():
kv_quant_npu.get_quant_attention_rotation.cache_clear()
before = torch.random.get_rng_state()
rot = kv_quant_npu.get_quant_attention_rotation(torch.device("cpu"), torch.float32, 64)
assert rot is kv_quant_npu.get_quant_attention_rotation(torch.device("cpu"), torch.float32, 64)
torch.testing.assert_close(rot @ rot.T, torch.eye(64))
assert torch.equal(before, torch.random.get_rng_state())
@npu_smoke
class TestKVQuantNPUSmoke:
"""Smoke tests using real torch_npu/mindiesd stack, only on NPU."""
@pytest.mark.npu
@pytest.mark.parametrize("method", ["fp8", "mxfp8", "mxfp4"])
@pytest.mark.parametrize("layout", ["BSND", "BNSD"])
@pytest.mark.parametrize("seq_len", [256, 75600])
def test_dense_runtime_backend_real_npu(method, layout, seq_len):
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.diffusion.attention.backends.flash_attn import FlashAttentionImpl
def test_fp8_rotate_quant_fa_real_npu_shape_contract(self):
try:
kv_quant_npu._load_quant_ops.cache_clear()
kv_quant_npu._load_quant_ops()
except ImportError:
pytest.skip("NPU quant dependencies are not fully installed.")
query = torch.randn(1, 2, 4, 64, dtype=torch.float16, device="npu")
key = torch.randn(1, 2, 4, 64, dtype=torch.float16, device="npu")
value = torch.randn(1, 2, 4, 64, dtype=torch.float16, device="npu")
out = kv_quant_npu.fp8_rotate_quant_fa(query, key, value, layout="BNSD")
assert out.shape == query.shape
assert out.dtype == query.dtype
# On NPU, missing Runtime symbols must fail this qualification test.
query = torch.randn(1, seq_len, 2, 128, dtype=torch.bfloat16, device="npu") * 0.1
# Distinct constant values per head detect head/layout mixing and tail writes.
value = torch.tensor([0.5, -0.5], dtype=query.dtype, device=query.device)
value = value.view(1, 1, 2, 1).expand_as(query).contiguous()
if layout == "BNSD":
query, value = query.transpose(1, 2), value.transpose(1, 2)
impl = FlashAttentionImpl(
num_heads=2,
head_size=128,
softmax_scale=128**-0.5,
qkv_layout=layout,
)
metadata = AttentionMetadata(extra={"kv_cache_dtype": method})
out = impl.forward_npu(query, query, value, metadata)
assert out.shape == query.shape and out.dtype == query.dtype
assert torch.isfinite(out).all()
torch.testing.assert_close(out.float().cpu(), value.float().cpu(), rtol=0.02, atol=0.02)
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import os
from collections.abc import Callable
from functools import cache, partial
import torch
@@ -75,18 +76,18 @@ class FlashAttentionBackend(AttentionBackend):
class FlashAttentionImpl(AttentionImpl[AttentionMetadata]):
# Per-platform FP8 KV quantization support.
# To enable FP8 on a new platform, add its OmniPlatformEnum value here
# Per-platform attention quantization support.
# To enable a method on a new platform, add its OmniPlatformEnum value here
# and handle kv_cache_dtype in the corresponding forward_{platform}().
#
# TODO(quant-backend): The FP8 quant path currently lives inside
# TODO(quant-backend): The quantized path currently lives inside
# FlashAttentionImpl gated by ``attn_metadata.extra["kv_cache_dtype"]``.
# Eventually extract it into a dedicated FlashAttentionQuantBackend so
# backend selection (not metadata) decides quant. Until then, model
# authors can opt a specific Attention layer out via
# backend selection decides quant.
# Until then, model authors can opt a specific Attention layer out via
# ``Attention(disable_kv_quant=True)``.
_supported_kv_cache_dtypes = {
"npu": {"fp8"},
"npu": {"fp8", "mxfp8", "mxfp4"},
}
def __init__(
@@ -526,11 +527,47 @@ class FlashAttentionImpl(AttentionImpl[AttentionMetadata]):
) -> torch.Tensor:
"""NPU attention implementation using mindiesd."""
kv_cache_dtype = attn_metadata.extra.get("kv_cache_dtype") if attn_metadata else None
if kv_cache_dtype is not None:
extra = attn_metadata.extra if attn_metadata else {}
method = extra.get("kv_cache_dtype")
if method not in (None, "float", "auto"):
return self.forward_fa_quant_npu(query, key, value, attn_metadata)
return self.forward_fa_npu(query, key, value, attn_metadata)
@staticmethod
def _load_quant_runtime(method: str) -> Callable[..., torch.Tensor]:
if method not in ("fp8", "mxfp8", "mxfp4"):
raise ValueError(f"Unsupported NPU attention quantization method {method!r}.")
from mindiesd import quant_attention
return quant_attention
def _validate_quant_request(
self,
method: str,
query: torch.Tensor,
attn_metadata: AttentionMetadata | None,
) -> None:
extra = attn_metadata.extra if attn_metadata else {}
reason = None
if self.causal:
reason = "causal quantized FA is not supported"
elif any(name in extra for name in ("cu_seqlens_q", "cu_seqlens_k")) or extra.get("npu_attn_varlen"):
reason = "packed/varlen metadata requires the float attention path"
elif attn_metadata is not None and attn_metadata.full_attn_spans is not None:
reason = "piecewise attention requires the float attention path"
elif attn_metadata is not None and attn_metadata.attn_mask is not None:
reason = "caller masks require the float attention path"
elif method in ("fp8", "mxfp8", "mxfp4") and (
query.ndim != 4 or query.shape[-1] <= 0 or query.shape[-1] & (query.shape[-1] - 1)
):
reason = "generated Hadamard rotations require a four-dimensional input and power-of-two head size"
if reason is not None:
raise ValueError(
f"NPU attention precision {method!r} is unavailable: {reason}. "
"Set diffusion_kv_cache_dtype to a supported method, or set it to "
"'auto'. Automatic precision fallback is not performed."
)
def forward_fa_quant_npu(
self,
query: torch.Tensor,
@@ -538,18 +575,29 @@ class FlashAttentionImpl(AttentionImpl[AttentionMetadata]):
value: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
from vllm_omni.platforms.npu.quant.kv_quant_npu import fp8_rotate_quant_fa
extra = attn_metadata.extra if attn_metadata else {}
method = extra.get("kv_cache_dtype", "fp8")
layout = self.qkv_layout or "BSND"
self._validate_quant_request(method, query, attn_metadata)
try:
runtime = self._load_quant_runtime(method)
except ImportError as exc:
raise ImportError(
f"NPU attention precision {method!r} requires a compatible MindIE-SD "
"quant Runtime. Install a compatible MindIE-SD build, select another "
"diffusion_kv_cache_dtype, or set it to 'auto'. Automatic precision "
"fallback is not performed."
) from exc
kwargs = dict(precision=method, layout=layout, scale=self.softmax_scale)
# Apply Omni's deterministic Q/K rotation before every Dense quantized path.
if method in ("fp8", "mxfp8", "mxfp4"):
from vllm_omni.platforms.npu.quant.kv_quant_npu import get_quant_attention_rotation
layout = self.qkv_layout or "BNSD"
# Models pass (B, S, H, D); NPU fused op expects (B, N, S, D).
out = fp8_rotate_quant_fa(
query.transpose(1, 2),
key.transpose(1, 2),
value.transpose(1, 2),
layout=layout,
softmax_scale=self.softmax_scale,
)
return out.transpose(1, 2)
rotation = get_quant_attention_rotation(query.device, query.dtype, query.shape[-1])
kwargs.update(q_rot=rotation, k_rot=rotation)
logger.info_once("NPU attention uses MindIE-SD %s Runtime, layout=%s.", method, layout)
# Execution errors propagate. Never retry with another precision.
return runtime(query, key, value, **kwargs)
def forward_fa_npu(
self,
@@ -146,8 +146,10 @@ class RainFusionAttentionBackend(AttentionBackend):
from mindiesd import sparse_attention
except ImportError as exc:
raise ValueError(_MISSING_MINDIESD) from exc
if not _supports_video_spans(sparse_attention):
raise ValueError(_INCOMPATIBLE_MINDIESD)
if not callable(sparse_attention):
raise ValueError(_MISSING_MINDIESD)
# Single-video Wan only needs the legacy geometry. Check video_spans
# when a forward actually carries multiple videos.
@staticmethod
def get_supported_head_sizes() -> list[int]:
@@ -174,6 +176,8 @@ class RainFusionAttentionImpl(AttentionImpl):
irregular video tail internally, retaining it outside the sparse blocks.
"""
_supported_kv_cache_dtypes = {"npu": {"fp8", "mxfp4"}}
def __init__(
self,
num_heads: int,
@@ -222,6 +226,8 @@ class RainFusionAttentionImpl(AttentionImpl):
def _validate_parallel_config(self) -> None:
config = get_current_diffusion_config_or_none()
parallel_config = getattr(config, "parallel_config", None)
if getattr(parallel_config, "allgather_degree", 1) > 1:
raise ValueError("RAINFUSION_ATTN requires full Q/K/V sequences; AllGather-KV SP is unsupported.")
ring_degree = getattr(parallel_config, "ring_degree", 1)
if ring_degree > 1:
# Ring gives each rank a slice of the sequence, so block selection
@@ -262,7 +268,9 @@ class RainFusionAttentionImpl(AttentionImpl):
plan = self._resolve_plan(attn_metadata)
if plan is None:
return self.dense_fallback.forward_npu(query, key, value, attn_metadata)
return self._forward_sparse_npu(query, key, value, plan)
if query.ndim != 4 or key.shape != query.shape or value.shape != query.shape or plan.used_len > query.shape[1]:
raise ValueError("RainFusion video geometry must fit identical full-sequence BSND Q/K/V tensors.")
return self._forward_sparse_npu(query, key, value, plan, attn_metadata)
def _resolve_plan(self, attn_metadata: AttentionMetadata | None) -> RainFusionPlan | None:
"""Return the rf_v2 geometry, or None when this forward must stay dense."""
@@ -271,18 +279,17 @@ class RainFusionAttentionImpl(AttentionImpl):
return None
if self.layer_idx is not None and self.layer_idx in rf.skip_layers:
return None
step_idx = total_steps = None
if is_forward_context_available():
step_idx = get_forward_context().denoise_step_idx
total_steps = get_forward_context().total_denoise_steps
if step_idx is not None and step_idx < rf.start_step:
if (rf.start_step and step_idx is None) or (rf.end_step and (step_idx is None or total_steps is None)):
return None
if step_idx is not None:
if step_idx < rf.start_step:
return None
# Tail fallback: keep the last ``end_step`` denoise steps dense.
if (
rf.end_step > 0
and step_idx is not None
and total_steps is not None
and step_idx >= total_steps - rf.end_step
):
if rf.end_step > 0 and total_steps is not None and step_idx >= total_steps - rf.end_step:
return None
if self.qkv_layout is None:
# The sparse path reads the sequence off dim 1, which the tensors alone
@@ -308,7 +315,16 @@ class RainFusionAttentionImpl(AttentionImpl):
"model must publish AttentionMetadata.video_layout for the sequence to be sparsified."
)
return None
max_seqlen_q = attn_metadata.extra.get("max_seqlen_q")
# The sparse Runtime constructs its own block mask. It cannot preserve
# an arbitrary caller-supplied mask or piecewise visibility constraints.
mask = attn_metadata.attn_mask
padding_only = layout.used_len is not None
if attn_metadata.full_attn_spans is not None or (
mask is not None and not (padding_only and mask.ndim == 2 and mask.dtype == torch.bool)
):
return None
max_seqlen_q = attn_metadata.extra.get("max_seqlen_q", layout.used_len)
if max_seqlen_q is None:
logger.warning_once(
"RAINFUSION_ATTN staying dense: attention metadata is missing max_seqlen_q, so the "
@@ -459,19 +475,39 @@ class RainFusionAttentionImpl(AttentionImpl):
key: torch.Tensor,
value: torch.Tensor,
plan: RainFusionPlan,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
try:
from mindiesd import sparse_attention
except ImportError:
raise ImportError(_MISSING_MINDIESD)
if self.rainfusion.precision != "bf16" and not _mindiesd_supports_precision():
raise RuntimeError(
f"block_sparse.precision={self.rainfusion.precision!r} requires MindIE-SD "
"with sparse_attention(precision=...) support; the installed mindiesd "
"silently ignores it and would run the BF16 path. Install a compatible "
"MindIE-SD release or use precision='bf16'."
if plan.video_spans is not None and not _supports_video_spans(sparse_attention):
raise ValueError(_INCOMPATIBLE_MINDIESD)
extra = attn_metadata.extra if attn_metadata else {}
requested = extra.get("kv_cache_dtype", self.rainfusion.precision)
reason = None
if requested in ("float", "bf16"):
precision = "bf16"
else:
precision = requested
if requested not in ("fp8", "mxfp4", "mix"):
reason = f"sparse {requested} is not supported"
elif not _mindiesd_supports_precision():
reason = "MindIE-SD sparse_attention must explicitly support precision"
elif plan.video_spans is not None:
reason = "quantized multi-video RainFusion is not supported"
elif requested in ("fp8", "mxfp4") and query.shape[0] != 1:
reason = "quantized Wan BSA requires batch size 1"
elif requested in ("fp8", "mxfp4") and (query.shape[-1] < 64 or query.shape[-1] & (query.shape[-1] - 1)):
reason = "BSA Hadamard rotations require a power-of-two head dimension of at least 64"
if reason is not None:
raise ValueError(
f"RainFusion precision {requested!r} is unavailable: {reason}. "
"Set diffusion_kv_cache_dtype or block_sparse.precision to a method supported by the installed "
"MindIE-SD, or disable attention quantization. Automatic precision fallback is not performed."
)
logger.info_once("RainFusion uses MindIE-SD sparse attention, precision=%s.", precision)
used = plan.used_len
q, k, v = (tensor[:, :used] for tensor in (query, key, value))
# Ulysses has already gathered the full sequence onto this rank and split
@@ -480,10 +516,10 @@ class RainFusionAttentionImpl(AttentionImpl):
"scale": self.softmax_scale,
"head_num": query.shape[-2],
"input_layout": _INPUT_LAYOUT,
"inner_precise": _INNER_PRECISE,
"inner_precise": 4 if precision != "bf16" else _INNER_PRECISE,
"block_size": _BLOCK_SIZE,
"sparsity": self.rainfusion.sparsity,
"precision": self.rainfusion.precision,
"precision": precision,
}
if plan.video_spans is not None:
out = sparse_attention(
@@ -497,7 +533,9 @@ class RainFusionAttentionImpl(AttentionImpl):
else:
assert plan.prefix_len is not None and plan.latent_shape is not None
common_kwargs.update(
sparse_type="rf_v2",
# rf_v2 ignores precision on non-A5 devices. Select the explicit
# quantized high-level path so mask/rearrangement remain in MindIE.
sparse_type="rf_v3" if precision != "bf16" else "rf_v2",
txt_len=plan.prefix_len,
latent_shape_q=plan.latent_shape,
latent_shape_k=plan.latent_shape,
+18 -13
View File
@@ -321,7 +321,7 @@ class Attention(nn.Module):
dtype = None
parallel_config = getattr(config, "parallel_config", None)
ring_degree = getattr(parallel_config, "ring_degree", 1)
if dtype:
if dtype and dtype != "float":
if ring_degree > 1:
raise ValueError(
"KV quantization is not compatible with ring attention "
@@ -338,13 +338,15 @@ class Attention(nn.Module):
self._kv_cache_dtype = dtype
self._kv_cache_skip_steps = getattr(config, "diffusion_kv_cache_skip_step_indices", None)
self._kv_cache_skip_layers = getattr(config, "diffusion_kv_cache_skip_layer_indices", None)
if self._kv_cache_skip_layers and self.layer_idx is None and not self._disable_kv_quant:
raise ValueError("Attention quantization skip_layers requires a parseable transformer block index.")
def _should_apply_kv_cache_quant(self) -> bool:
skip_steps = self._kv_cache_skip_steps
skip_layers = self._kv_cache_skip_layers
if skip_steps is not None:
step_idx = get_forward_context().denoise_step_idx if is_forward_context_available() else None
if step_idx is not None and step_idx in skip_steps:
if skip_steps and (step_idx is None or step_idx in skip_steps):
return False
if skip_layers is not None:
if self.layer_idx is not None and self.layer_idx in skip_layers:
@@ -352,18 +354,21 @@ class Attention(nn.Module):
return True
def _with_kv_cache_dtype(self, attn_metadata: AttentionMetadata | None) -> AttentionMetadata | None:
kv_cache_dtype = self._kv_cache_dtype
if kv_cache_dtype is None or self._disable_kv_quant or not self._should_apply_kv_cache_quant():
if attn_metadata is None or "kv_cache_dtype" not in attn_metadata.extra:
return attn_metadata
extra = dict(attn_metadata.extra)
extra.pop("kv_cache_dtype", None)
return replace(attn_metadata, extra=extra)
disabled = self._disable_kv_quant or not self._should_apply_kv_cache_quant()
dtype = self._kv_cache_dtype
if dtype in (None, "float"):
dtype = None
elif disabled:
dtype = "float"
if dtype is None and (attn_metadata is None or "kv_cache_dtype" not in attn_metadata.extra):
return attn_metadata
extra = dict(attn_metadata.extra) if attn_metadata is not None else {}
# Recompute per forward so shared metadata cannot retain another step's policy.
extra.pop("kv_cache_dtype", None)
if dtype is not None:
extra["kv_cache_dtype"] = dtype
if attn_metadata is None:
return AttentionMetadata(extra={"kv_cache_dtype": kv_cache_dtype})
extra = dict(attn_metadata.extra)
extra["kv_cache_dtype"] = kv_cache_dtype
return AttentionMetadata(extra=extra) if extra else None
return replace(attn_metadata, extra=extra)
def forward(
+2 -5
View File
@@ -1028,11 +1028,8 @@ class OmniDiffusionConfig:
# has already resolved to vLLM's ModelOpt FP8 linear method.
force_cutlass_fp8: bool = False
# Diffusion attention KV cache dtype (not vLLM's --kv-cache-dtype for AR models).
# None = native dtype (no quantization).
# "fp8" = dynamic FP8 (float8_e4m3fn) quantization per forward pass.
# On Hopper+FA3: native FP8 attention (memory + compute savings).
# On other backends: no benefit, backends skip quantization.
# Runtime diffusion attention method (not vLLM's --kv-cache-dtype for AR models).
# None/"auto" keeps native dtype; other values are validated by the selected backend.
diffusion_kv_cache_dtype: str | None = None
# Optional skip selectors for KV-cache quantization. Format: "0-9,20,25-30".
# Listed steps/layers skip quantization; others keep quantized execution.
@@ -29,7 +29,7 @@ from vllm.model_executor.models.utils import (
)
from vllm.sequence import IntermediateTensors
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata, VideoTokenLayout
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.distributed.parallel_state import (
get_pipeline_parallel_world_size,
@@ -417,6 +417,7 @@ class WanSelfAttention(nn.Module):
softmax_scale=1.0 / (head_dim**0.5),
causal=False,
role="self",
qkv_layout="BSND",
prefix=prefix,
)
@@ -609,8 +610,8 @@ class WanCrossAttention(nn.Module):
qkv_layout="BSND",
prefix=prefix,
skip_sequence_parallel=True,
# Wan2.2 cross-attn operates on short text-encoder sequences; per-block
# FP8 quant offers no perf win and degrades quality. Opt out until a
# Wan2.2 cross-attn operates on short text-encoder sequences; runtime
# Q/K/V quantization offers no perf win and degrades quality. Opt out until a
# dedicated quant backend handles this case.
disable_kv_quant=True,
)
@@ -764,7 +765,15 @@ class WanTransformerBlock(nn.Module):
self_attn_extra["vsa_dit_seq_shape"] = vsa_dit_seq_shape
if preserve_vsa_all_blocks:
self_attn_extra["preserve_vsa_all_blocks"] = True
self_attn_metadata = AttentionMetadata(attn_mask=hidden_states_mask, extra=self_attn_extra)
video_layout = None
if vsa_dit_seq_shape is not None:
grid = tuple(int(dim) for dim in vsa_dit_seq_shape)
# Publish geometry independently of backend selection. Do not add
# partial cu_seqlens/max_seqlen metadata to dense CUDA attention.
video_layout = VideoTokenLayout(prefix_len=0, latent_grid=grid, used_len=math.prod(grid))
self_attn_metadata = AttentionMetadata(
attn_mask=hidden_states_mask, extra=self_attn_extra, video_layout=video_layout
)
attn_output = self.attn1(norm_hidden_states, rotary_emb, self_attn_metadata)
hidden_states = (hidden_states + attn_output * gate_msa).type_as(hidden_states)
+2 -1
View File
@@ -941,7 +941,8 @@ class OmniServeCommand(CLISubcommand):
"--diffusion-kv-cache-dtype",
type=str,
default=None,
help="Diffusion attention KV cache dtype (e.g. fp8). Separate from vLLM --kv-cache-dtype.",
help="Diffusion Q/K/V precision: fp8, mxfp8, mxfp4, or float (NPU). "
"Separate from vLLM --kv-cache-dtype. Use --diffusion-attention-config for per-role fallback.",
)
omni_config_group.add_argument(
"--diffusion-kv-cache-skip-steps",
+16 -118
View File
@@ -1,127 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""FP8 quantization utilities for diffusion attention tensors.
Provides per-tensor dynamic quantization of Q/K/V tensors to
float8_e4m3fn format. Designed for diffusion models where Q/K/V are
computed fresh each forward pass (no persistent KV cache).
"""
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Seeded rotation matrices for MindIE-SD quantized attention."""
from __future__ import annotations
import functools
import math
import threading
from functools import lru_cache
import torch
# Hadamard rotation matrix for QuaRot-style preprocessing
# keyed by (device, dtype, head_dim) to avoid matmul dtype mismatch.
_ROT_MATRIXS: dict[tuple[torch.device, torch.dtype, int], torch.Tensor] = {}
_ROT_MATRIX_LOCK = threading.Lock()
_FP8_KV_LABELS = frozenset({"fp8"})
def is_quantized_kv_cache(kv_cache_dtype: str | None) -> bool:
"""True if config requests FP8-style KV / QKV quantization for the NPU FA path."""
return kv_cache_dtype in _FP8_KV_LABELS
@lru_cache(maxsize=1)
def _load_quant_ops():
try:
import torch_npu
from mindiesd.layers.quant.block_quant import fa_block_quant_preprocess
from msmodelslim.processor.quarot.common.quarot_utils import QuaRotMode, create_rot
except ImportError as e:
raise ImportError(
"fp8_rotate_quant_fa requires torch_npu, MindIE-SD (mindiesd), and MSModelSlim. "
"See https://gitcode.com/Ascend/MindIE-SD and https://gitcode.com/Ascend/msmodelslim"
) from e
return torch_npu, fa_block_quant_preprocess, QuaRotMode, create_rot
def _get_rot_matrix(
device: torch.device,
dtype: torch.dtype,
head_dim: int,
qua_rot_mode,
create_rot,
) -> torch.Tensor:
key = (device, dtype, head_dim)
with _ROT_MATRIX_LOCK:
rot = _ROT_MATRIXS.get(key)
if rot is None:
rot = create_rot(qua_rot_mode.HADAMARD, head_dim, seed=425500).to(device=device, dtype=dtype)
_ROT_MATRIXS[key] = rot
return rot
def fp8_rotate_quant_fa(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
*,
layout: str = "BNSD",
softmax_scale: float | None = None,
) -> torch.Tensor:
"""Run NPU fused attention with dynamic FP8 Q/K/V and optional QuaRot preprocess.
Args:
query: Query tensor in ``layout`` order (default BNSD: batch, heads, seq, dim).
key: Key tensor in ``layout`` order (default BNSD: batch, heads, seq, dim).
value: Value tensor in ``layout`` order (default BNSD: batch, heads, seq, dim).
layout: ``BNSD`` or ``BSND`` for ``npu_fused_infer_attention_score_v2``.
softmax_scale: If None, uses ``1 / sqrt(head_dim)``.
Returns:
Attention output in the same layout as inputs.
"""
torch_npu, fa_block_quant_preprocess, qua_rot_mode, create_rot = _load_quant_ops()
out_dtype = query.dtype
device = query.device
if layout == "BNSD":
_, n, s, d = query.shape
elif layout == "BSND":
_, s, n, d = query.shape
else:
raise ValueError(f"fp8_rotate_quant_fa: unsupported layout {layout!r}, expected BNSD or BSND")
rot = _get_rot_matrix(device, query.dtype, d, qua_rot_mode, create_rot)
q_f = torch.matmul(query, rot)
k_f = torch.matmul(key, rot)
q, q_scale = fa_block_quant_preprocess(q_f, block_size=128, dst_type=torch_npu.float8_e4m3fn, layout=layout)
k, k_scale = fa_block_quant_preprocess(k_f, block_size=256, dst_type=torch_npu.float8_e4m3fn, layout=layout)
v, v_scale = fa_block_quant_preprocess(value, block_size=256, dst_type=torch_npu.float8_e4m3fn, layout=layout)
scale = softmax_scale if softmax_scale is not None else 1.0 / math.sqrt(d)
out = torch_npu.npu_fused_infer_attention_score_v2(
q,
k,
v,
input_layout=layout,
num_query_heads=n,
softmax_scale=scale,
pre_tokens=2147483647, # INT32_MAX: no left-context truncation.
next_tokens=2147483647, # INT32_MAX: no right-context truncation.
query_quant_mode=7, # NPU mode id for block FP8 dequant path.
key_quant_mode=7, # Same quant mode as query branch.
value_quant_mode=7, # Same quant mode as key/query branches.
dequant_scale_query=q_scale,
dequant_scale_key=k_scale,
dequant_scale_value=v_scale,
out_dtype=out_dtype,
)[0]
if out.shape[2] != s:
if layout == "BNSD":
out = out[:, :, :s, :]
elif layout == "BSND":
out = out[:, :s, :, :]
return out
@functools.lru_cache(maxsize=128)
def get_quant_attention_rotation(device: torch.device, dtype: torch.dtype, head_dim: int) -> torch.Tensor:
"""Preserve Omni's fixed Walsh-Hadamard rotation without changing global RNG."""
if head_dim <= 0 or head_dim & (head_dim - 1):
raise ValueError("Generated attention rotations require a power-of-two head dimension.")
generator = torch.Generator(device="cpu").manual_seed(425500)
signs = torch.randint(0, 2, (head_dim,), generator=generator, dtype=torch.int64)
signs = signs.to(dtype=torch.float32).mul_(2).sub_(1)
hadamard = torch.ones(1, 1)
while hadamard.shape[0] < head_dim:
hadamard = torch.cat((torch.cat((hadamard, hadamard), dim=1), torch.cat((hadamard, -hadamard), dim=1)), dim=0)
rotation = signs[:, None] * hadamard / math.sqrt(head_dim)
return rotation.to(device=device, dtype=dtype).contiguous()