Skip to content

Commit 85bc559

Browse files
authored
Add nvfp4 attention support for vLLM serving (#1898)
### What does this PR do? Type of change: New feature This PR adds an NVFP4+sparse attention serving path for vLLM and consolidates it with the existing checkpoint-driven sparse-attention integration. #### NVFP4 attention - Applies dynamic block-16 NVFP4 fake quantization to Q/K/P/V. - Quantizes K before the KV-cache write. V remains pristine until a complete 16-token group can be finalized once; an incomplete tail is QDQ on read. - Adds paged/chunked prefill support and a dedicated split-K decode kernel. Decode uses a fixed 32-split, 128-key-tile schedule because split-local P quantization is part of the numerical contract. #### vLLM integration - Provides one consolidated worker module: - `SparseAttnWorker` for checkpoint-driven sparse attention. - `QuantSparseAttnWorker` for fixed NVFP4 Q/K/P/V plus optional checkpoint sparsity. - Supports the native vLLM FlashAttention and FlashInfer backends. The worker replaces only the selected implementation and delegates inactive launches back to that backend. - Preserves FlashInfer NHD/HND cache layouts and separates mixed decode/prefill launches so each phase uses the correct kernel contract. - Restores checkpoint-driven N:M sparse-softmax and skip-softmax metadata. N:M remains a prefill transform; calibrated skip-softmax decode uses the shared paged kernel. - Validates the complete attention plan before modifying the model and reports unsupported configurations without leaving a partially converted model. Supported configurations are regular decoder self-attention with vLLM >= 0.15.0, FlashInfer or FlashAttention, fp16/bf16 model and KV cache, equal Q/K/V head dimensions divisible by 16, and DCP 1. The README documents unsupported features and CUDA-graph constraints. ### Usage Let vLLM select the backend for the model and platform. For example, Nemotron-H on Blackwell selects FlashInfer automatically. ```bash cd examples/vllm_serve python vllm_serve_sparse_attn.py <MODEL_PATH> -tp 8 \ --no-enable-prefix-caching \ --worker-cls sparse_attn_worker.QuantSparseAttnWorker ``` If `<MODEL_PATH>/config.json` contains `sparse_attention_config`, the same worker also applies its N:M or skip-softmax settings. Otherwise, it runs NVFP4 attention only. ### Testing Focused attention kernels: ```bash PYTEST_VERSION=1 PYTHONPATH=$PWD python -m pytest -q \ tests/gpu/torch/kernels/common/attention/test_triton_fa_p_qdq.py \ tests/gpu/torch/kernels/common/attention/test_decode_attention.py \ tests/gpu/torch/kernels/common/attention/test_triton_fa_paged.py ``` - B200 NVFP4 run before final test deduplication: 47 passed. - Current RTX A6000 run: 23 passed, 21 skipped. The skips require native E4M3 support (compute capability >= 8.9). Focused vLLM worker and adapter tests: ```bash PYTHONPATH=$PWD python -m pytest -q \ tests/gpu_vllm/torch/sparsity/attention_sparsity/test_sparse_attn_worker.py \ tests/gpu_vllm/torch/sparsity/attention_sparsity/test_quant_sparse_attn_worker.py ``` Result: 43 passed. CPU import and launch-contract tests: ```bash PYTHONPATH=$PWD python -m pytest -q \ tests/unit/torch/kernels/common/attention/test_triton_fa.py ``` Result: 9 passed. GitHub Linux, Windows, multi-version, code-quality, documentation, and required unit checks pass. ### Before your PR is "*Ready for review*" Make sure you read and follow [Contributor guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md) and your commits are signed (`git commit -s -S`). Make sure you read and follow the [Security Best Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors) (e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(..., weights_only=False)`, `pickle`, etc.). - Is this change backward compatible?: ✅ — both serving policies are opt-in; sparse-only remains the launcher default. - If you copied code from any other sources or added a new PIP dependency, did you follow guidance in `CONTRIBUTING.md`: N/A — no copied code or new dependency. - Did you write any new necessary tests?: ✅ - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: N/A — the example README documents this opt-in serving path; no stable public API changed. - Did you get Claude approval on this PR?: N/A ### Additional Information The fixed attention recipe intentionally does not expose the integration branch's environment-variable matrix. Backend selection is automatic unless an explicit vLLM backend override is needed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added quant+sparse vLLM serving worker support and clarified compact NVFP4 attention worker behavior. * Introduced paged split-K decode and NVFP4 QDQ, including on-write V-cache quantization and new decode attention API. * Expanded vLLM runtime installation to support NVFP4 quantization and sparse attention with FlashInfer metadata compatibility. * **Bug Fixes** * Improved NVFP4 degenerate/underflow scale handling to reliably zero out blocks. * Strengthened validation for paged-cache and NVFP4 quantization contracts. * **Documentation** * Updated the vLLM sparse-attention example docs with tested versions and explicit limitations. * **Tests** * Expanded GPU correctness and integration coverage for split-K decode, NVFP4 QDQ, vLLM workers, and runtime installation paths. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Kai Xu <kaix@nvidia.com>
1 parent 95ee9c4 commit 85bc559

22 files changed

Lines changed: 4577 additions & 712 deletions

File tree

examples/vllm_serve/README.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ This is a simple example to demonstrate calibrating and serving ModelOpt fakequa
44

55
Compared with realquant, fakequant is 2-5x slower, but doesn't require dedicated kernel support and facilitates research.
66

7-
This example is tested with vllm 0.9.0 and 0.19.1
7+
The general fakequant example is tested with vLLM 0.9.0 and 0.19.1. The compact
8+
NVFP4 attention worker documented below requires vLLM 0.15.0 or newer.
89

910
## Prepare environment
1011

@@ -101,9 +102,9 @@ QUANT_CFG=<quant_cfg> QUANT_FILE_PATH=<quantizer_state.pth> python vllm_serve_fa
101102

102103
## Serve a model with sparse attention in vLLM
103104

104-
Apply ModelOpt sparse attention at serve time. The launcher replaces vLLM's `FlashAttentionImpl` with `ModelOptSparseAttentionImpl` (Triton kernel with paged KV cache support) on every attention layer right after model load.
105+
Apply ModelOpt sparse attention at serve time. Right after model load, the launcher replaces each native attention implementation with its matching ModelOpt adapter: `ModelOptSparseAttentionImpl` for FlashAttention or `ModelOptSparseFlashInferImpl` for FlashInfer. Both adapters use the same Triton kernel with paged KV cache support.
105106

106-
The configuration is read from the checkpoint's `config.json` `sparse_attention_config` block, written by ModelOpt's HF export. The launcher restores calibrated skip-softmax metadata and N:M sparse-softmax metadata (`sparsity_n`, `sparsity_m`, `dense_sink_tokens`, `dense_recent_tokens`). Checkpoints exported with both metadata entries use ModelOpt Triton for sparse prefill launches; decode-only launches and launches without active sparse work delegate back to vLLM FlashAttention.
107+
The configuration is read from the checkpoint's `config.json` `sparse_attention_config` block, written by ModelOpt's HF export. The launcher restores calibrated skip-softmax metadata and N:M sparse-softmax metadata (`sparsity_n`, `sparsity_m`, `dense_sink_tokens`, `dense_recent_tokens`). Checkpoints exported with both metadata entries use ModelOpt Triton for sparse prefill launches; launches without active sparse work delegate back to the native backend selected by vLLM.
107108

108109
Workflow:
109110

@@ -114,12 +115,50 @@ Workflow:
114115
python vllm_serve_sparse_attn.py <EXPORT_DIR> --enforce-eager -tp 8 --host 0.0.0.0 --port 8000
115116
```
116117

117-
If the checkpoint has no `sparse_attention_config`, the worker logs a message and passes through — vLLM runs unchanged. Quant-only flows are handled by `vllm_serve_fakequant.py`; combined sparse + quant will land in a follow-up PR.
118+
If the checkpoint has no `sparse_attention_config`, the sparse-only installer passes through and vLLM runs unchanged. Whole-model fakequant flows remain handled by `vllm_serve_fakequant.py`; the compact attention-only path is below.
119+
120+
The reusable serving policies live in `modelopt/torch/sparsity/attention_sparsity/plugins/vllm_runtime.py`. `install_vllm_sparse_attention_from_checkpoint` installs checkpoint-driven sparse-only attention, while `install_vllm_nvfp4_attention` installs fixed NVFP4 Q/K/P/V with optional checkpoint sparsity. Both validate every selected layer before publishing any replacement implementation and return a `VllmAttentionInstallReport` with the installed layer names and backend counts.
121+
122+
`sparse_attn_worker.py` only invokes these APIs after vLLM loads the model. It retains `SparseAttnWorker` as the launcher's default and provides `QuantSparseAttnWorker` for the compact NVFP4 policy. Other vLLM integrations can invoke the same library APIs directly:
123+
124+
```python
125+
from modelopt.torch.sparsity.attention_sparsity.plugins.vllm_runtime import (
126+
install_vllm_nvfp4_attention,
127+
)
128+
129+
report = install_vllm_nvfp4_attention(model_runner, sparse_cfg="checkpoint")
130+
```
118131

119132
Limitations:
120133

121134
- vLLM V1 chunked prefill and prefix-cache suffix attention are supported by offsetting query positions into the longer KV span.
122-
- CUDA graph capture is not validated yet — use `--enforce-eager`.
135+
- `SparseAttnWorker` CUDA graph capture is not validated yet — use `--enforce-eager`.
136+
137+
### Compact NVFP4 attention worker
138+
139+
vLLM 0.15.0 or newer is required when either worker activates a ModelOpt attention transform. Importing `SparseAttnWorker`, or using it with no checkpoint sparse metadata, does not resolve quant-only APIs.
140+
141+
Use the same launcher with the compact worker. By default, vLLM selects the backend for the model and platform; NemotronH on Blackwell selects FlashInfer:
142+
143+
```bash
144+
python vllm_serve_sparse_attn.py <MODEL_PATH> -tp 8 \
145+
--no-enable-prefix-caching \
146+
--worker-cls sparse_attn_worker.QuantSparseAttnWorker
147+
```
148+
149+
The installer supports both FlashInfer and FlashAttention, and the worker prints the installed adapter counts. Pass `--attention-backend FLASHINFER` or `--attention-backend FLASH_ATTN` only when an explicit override is needed.
150+
151+
This attention-only path applies a fixed dynamic block-16 NVFP4 fakequant format to Q/K/P/V. Q is dynamic; missing K/V scales default to global scale 1.0, and P defaults to amax 1.0. Existing scalar attention amax values are preserved, but this path does not calibrate or restore them itself. It does not re-quantize realquant Linear or MoE weights. An optional checkpoint `sparse_attention_config` is still honored.
152+
153+
Decode uses a fixed 32-split, 128-key-tile schedule. P QDQ consumes split-local,
154+
unnormalized online-softmax probabilities, so changing that schedule can change
155+
quantized results; split count is part of the numerical contract.
156+
157+
K is QDQ before its cache write, while V is written pristine. Complete 16-token V groups are finalized once in cache; an incomplete tail remains pristine and is QDQ on read. P@V therefore sees uniform fakequant values without re-quantizing the tail.
158+
159+
Supported configurations are regular decoder self-attention with FlashInfer or FlashAttention, fp16/bf16 model and KV cache, equal Q/K/V head dimensions that are multiples of 16, and DCP 1. The FlashInfer adapter preserves both NHD and HND cache strides and separates mixed decode/prefill launches so each phase keeps its own kernel contract. The default `FULL_AND_PIECEWISE` mode remains enabled for fixed N:M and attention-only NVFP4; checkpoints with calibrated decode `threshold_scale_factor` must use a non-`FULL` decode graph mode such as `--enforce-eager` because the live sequence length is not replayed as a Python scalar.
160+
161+
Unsupported features are sliding window, ALiBi, softcap, sinks, FP8 KV cache, cross/encoder/MLA attention, KV sharing or transfer, prefix caching, speculative decoding, DBO/ubatching, and `FULL` mixed/prefill CUDA graphs.
123162

124163
## Known Problems
125164

examples/vllm_serve/sparse_attn_worker.py

Lines changed: 72 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -13,108 +13,96 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
"""Custom vLLM worker for sparse attention.
17-
18-
``SparseAttnWorker``: Replaces ``FlashAttentionImpl`` with
19-
``ModelOptSparseAttentionImpl`` on each Attention module after model loading.
20-
The sparse impl uses the ModelOpt Triton kernel for sparse prefill launches.
21-
Decode-only launches and launches without active sparse work delegate back to
22-
vLLM FlashAttention.
23-
24-
Configuration flows exclusively through the loaded checkpoint's
25-
``sparse_attention_config`` block (written by ModelOpt's HF export). If the
26-
checkpoint has no such block, the worker logs a message and passes through
27-
unchanged.
28-
29-
Quantization combined with sparse attention is not handled by this worker
30-
and will land in a follow-up PR once the combined path is tested.
31-
32-
Usage:
33-
python vllm_serve_sparse_attn.py <path/to/modelopt-exported-ckpt>
34-
"""
35-
36-
import importlib
37-
38-
try:
39-
_has_legacy_attention_layer = importlib.util.find_spec("vllm.attention.layer") is not None
40-
except (ModuleNotFoundError, ValueError):
41-
_has_legacy_attention_layer = False
42-
43-
if _has_legacy_attention_layer:
44-
from vllm.attention.layer import Attention as VLLMAttention
45-
else:
46-
from vllm.model_executor.layers.attention import Attention as VLLMAttention
16+
"""vLLM worker lifecycle wiring for ModelOpt attention transforms."""
4717

4818
from vllm.v1.worker.gpu_worker import Worker as BaseWorker
4919

50-
from modelopt.torch.sparsity.attention_sparsity.plugins.sparse_attn_config import (
51-
load_from_checkpoint_metadata,
52-
match_sparse_config,
53-
)
54-
from modelopt.torch.sparsity.attention_sparsity.plugins.vllm import (
55-
_build_sparse_kw,
56-
_clone_sparse_impl,
20+
from modelopt.torch.sparsity.attention_sparsity.plugins.vllm_runtime import (
21+
install_vllm_nvfp4_attention,
22+
install_vllm_sparse_attention_from_checkpoint,
5723
)
5824

25+
__all__ = ["SparseAttnWorker", "QuantSparseAttnWorker"] # noqa: RUF022
5926

60-
def _replace_attention_impl(worker):
61-
"""Replace FlashAttentionImpl with ModelOptSparseAttentionImpl on all Attention layers.
27+
_QUANT_FORMAT_KEYS = ("q_format", "k_format", "p_format", "v_format")
6228

63-
The sole configuration source is the checkpoint's ``sparse_attention_config``
64-
metadata. No-op if the checkpoint has no such block.
65-
"""
66-
hf_config = getattr(worker.model_runner.model_config, "hf_config", None)
67-
detected = load_from_checkpoint_metadata(hf_config)
68-
if detected is None:
29+
30+
def _unwrapped_model(worker):
31+
model = worker.model_runner.model
32+
return model.unwrap() if hasattr(model, "unwrap") else model
33+
34+
35+
def _print_install_report(policy, report) -> None:
36+
if report.installed_count:
37+
if policy != "Sparse attention":
38+
print(
39+
f"[ModelOpt] Installed {policy} (quant+sparse) on "
40+
f"{report.installed_count} layers: {dict(report.backend_counts)}"
41+
)
42+
else:
43+
if report.sparse_algorithm:
44+
print(f"[ModelOpt] Sparse attention config: algo -> {report.sparse_algorithm}")
45+
print(
46+
f"[ModelOpt] Sparse attention: replaced impl on {report.installed_count} "
47+
f"attention layers: {dict(report.backend_counts)}"
48+
)
49+
elif report.sparse_algorithm:
50+
print(
51+
f"[ModelOpt] Sparse attention config {report.sparse_algorithm} matched no active "
52+
"attention layers; vLLM remains unchanged"
53+
)
54+
else:
6955
print(
7056
"[ModelOpt] No sparse_attention_config found in the checkpoint; "
71-
"skipping sparse attention. Run examples/llm_sparsity/"
72-
"attention_sparsity/hf_sa.py to calibrate and export a checkpoint "
73-
"with the config embedded."
57+
"skipping sparse attention. Run examples/llm_sparsity/attention_sparsity/"
58+
"hf_sa.py to calibrate and export a checkpoint with the config embedded."
7459
)
75-
return
76-
cfg, preset_name = detected
77-
print(f"[ModelOpt] Sparse attention config: algo -> {preset_name}")
78-
79-
model = worker.model_runner.model
80-
if hasattr(model, "unwrap"):
81-
model = model.unwrap()
8260

83-
patched = 0
84-
for name, module in model.named_modules():
85-
if not isinstance(module, VLLMAttention):
86-
continue
8761

88-
layer_cfg = match_sparse_config(name, cfg)
89-
if layer_cfg is None or not layer_cfg.get("enable", True):
90-
continue
91-
92-
sparse_kw = _build_sparse_kw(layer_cfg)
93-
if not sparse_kw:
94-
# Keep vLLM's original impl when the exported layer config does not
95-
# enable any sparse feature.
96-
continue
97-
new_impl = _clone_sparse_impl(module.impl)
98-
new_impl.sparse_kw = sparse_kw
99-
module.impl = new_impl
100-
patched += 1
101-
print(f"[ModelOpt] Sparse attention: replaced impl on {patched} attention layers")
62+
class SparseAttnWorker(BaseWorker):
63+
"""Install checkpoint-driven sparse attention after model loading."""
10264

65+
def load_model(self, *args, **kwargs) -> None:
66+
"""Load the model, then install checkpoint-configured attention."""
67+
super().load_model(*args, **kwargs)
68+
report = install_vllm_sparse_attention_from_checkpoint(self.model_runner)
69+
_print_install_report("Sparse attention", report)
10370

104-
# ---------------------------------------------------------------------------
105-
# Workers
106-
# ---------------------------------------------------------------------------
10771

72+
class QuantSparseAttnWorker(BaseWorker):
73+
"""Install quantized attention plus optional checkpoint sparsity.
10874
109-
class SparseAttnWorker(BaseWorker):
110-
"""vLLM worker that uses the ModelOpt sparse attention backend.
75+
Per-operand formats come from vLLM's ``--additional-config``; absent keys
76+
default to NVFP4 on all four operands (Q/K/P/V)::
11177
112-
Replaces FlashAttentionImpl with ModelOptSparseAttentionImpl on each
113-
Attention module right after model loading — before any forward pass
114-
(including determine_available_memory profiling).
78+
--additional-config '{"modelopt_attn_quant": {"p_format": "fp8", "v_format": "fp8"}}'
11579
"""
11680

81+
def _quant_formats(self) -> dict[str, str]:
82+
additional = getattr(self.vllm_config, "additional_config", None) or {}
83+
formats = additional.get("modelopt_attn_quant", {})
84+
unknown = set(formats) - set(_QUANT_FORMAT_KEYS)
85+
if unknown:
86+
raise ValueError(
87+
f"unknown modelopt_attn_quant keys {sorted(unknown)}; "
88+
f"allowed: {list(_QUANT_FORMAT_KEYS)}"
89+
)
90+
return dict(formats)
91+
11792
def load_model(self, *args, **kwargs) -> None:
118-
"""Load model, then replace attention impl with sparse variant."""
93+
"""Load the model, then install the configured attention quant recipe."""
11994
super().load_model(*args, **kwargs)
120-
_replace_attention_impl(self)
95+
formats = self._quant_formats()
96+
report = install_vllm_nvfp4_attention(self.model_runner, sparse_cfg="checkpoint", **formats)
97+
policy = "NVFP4 attention" if not formats else f"Quant attention ({formats})"
98+
_print_install_report(policy, report)
99+
100+
def determine_available_memory(self) -> int:
101+
"""Profile memory without compiling the dynamically converted modules."""
102+
# Sparse-only imports must remain independent of quantization-specific APIs.
103+
import torch
104+
105+
from modelopt.torch.quantization.plugins.vllm import disable_compilation
106+
107+
with torch.inference_mode(), disable_compilation(_unwrapped_model(self)):
108+
return BaseWorker.determine_available_memory(self)

examples/vllm_serve/vllm_serve_sparse_attn.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515

1616
"""Launch vLLM with sparse attention.
1717
18-
Configuration is read exclusively from ``<ckpt>/config.json``'s
19-
``sparse_attention_config`` block, written during calibration by
18+
The default ``SparseAttnWorker`` reads configuration exclusively from
19+
``<ckpt>/config.json``'s ``sparse_attention_config`` block, written by
2020
``examples/llm_sparsity/attention_sparsity/hf_sa.py``. If the checkpoint has
21-
no such block, the worker logs a message and the server runs as standard
21+
no such block, that worker logs a message and the server runs as standard
2222
vLLM.
2323
24-
Combined sparse attention + quantization is not handled by this launcher; it
25-
will be added in a follow-up PR once the combined path is tested.
24+
The launcher defaults to ``sparse_attn_worker.SparseAttnWorker``. Pass
25+
``--worker-cls sparse_attn_worker.QuantSparseAttnWorker`` for quant+sparse.
2626
2727
Usage:
2828
python vllm_serve_sparse_attn.py <path/to/modelopt-exported-ckpt>

0 commit comments

Comments
 (0)