Skip to content

Commit 37d1b17

Browse files
committed
Remove redundant NVFP4 attention paths
Signed-off-by: Kai Xu <kaix@nvidia.com>
1 parent 5ef7dea commit 37d1b17

10 files changed

Lines changed: 38 additions & 212 deletions

File tree

examples/vllm_serve/sparse_attn_worker.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"""Custom vLLM workers for checkpoint-driven sparse and fixed-NVFP4 attention."""
1717

1818
import importlib
19-
import os
2019
from collections import Counter
2120
from functools import cache
2221
from types import SimpleNamespace
@@ -291,21 +290,12 @@ def _install_sparse_plans(plans) -> None:
291290

292291
def _install_quant_plans(worker, plans) -> None:
293292
api = _quant_api()
294-
quant_off = os.environ.get("MODELOPT_ATTN_QUANT_OFF") == "1"
295293
for plan in plans:
296294
module = plan.module
297295
module.device, module.dtype = plan.device, plan.dtype
298296
api.nn.QuantModuleRegistry.convert(module)
299297
module.p_bmm_quantizer = api.nn.TensorQuantizer()
300298
api.conversion.set_quantizer_by_cfg(module, _BMM_CFG)
301-
if quant_off:
302-
# Isolation knob: keep the ModelOpt kernel fixed while disabling all
303-
# Q/K/P/V NVFP4 transforms, so quant-on minus quant-off isolates the
304-
# fakequant loss without triggering the native dense fallback.
305-
for name in ("q", "k", "p", "v"):
306-
quantizer = getattr(module, f"{name}_bmm_quantizer", None)
307-
if quantizer is not None:
308-
quantizer.disable()
309299
api.plugin._set_vllm_attention_kv_default_amax(module, plan.device)
310300
plan.new_impl.sparse_kw = plan.sparse_kw
311301
p_qdq, p_qdq_amax = _p_qdq_from_layer(module)
@@ -317,10 +307,8 @@ def _install_quant_plans(worker, plans) -> None:
317307
"v_qdq_amax": v_qdq_amax,
318308
}
319309
module.impl = plan.new_impl
320-
module._query_quant_in_kernel = not quant_off
321-
module._value_quant_in_kernel = not quant_off
322-
if quant_off:
323-
module._modelopt_force_kernel = True
310+
module._query_quant_in_kernel = True
311+
module._value_quant_in_kernel = True
324312
worker.model_runner.cascade_attn_enabled = False
325313
installed = dict(Counter(type(plan.new_impl).__name__ for plan in plans))
326314
print(f"[ModelOpt] Installed NVFP4 quant+sparse attention on {len(plans)} layers: {installed}")

modelopt/torch/kernels/common/attention/triton_fa.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ def _load_qdq_helpers() -> None:
7171
_v_qdq_nvfp4 = _v_nvfp4
7272

7373

74-
# Maps public P/V QDQ options to kernel constexpr values.
75-
_QDQ_MODES = {None: 0, "fp8": 1, "nvfp4": 2}
74+
# Maps public QDQ options to kernel constexpr values.
75+
_P_QDQ_MODES = {None: 0, "fp8": 1, "nvfp4": 2}
76+
_V_QDQ_MODES = {None: 0, "nvfp4": 2}
7677

7778

7879
LOG2E: float = 1.44269504088896
@@ -289,7 +290,7 @@ def _attn_fwd(
289290
SKIP_THRESHOLD_LOG2: tl.constexpr = 0.0, # log2(lambda) in the kernel's scaled log2 score space
290291
P_QDQ: tl.constexpr = 0, # Fake quant-dequant of softmax P: 0=off, 1=FP8 E4M3, 2=NVFP4
291292
p_qdq_scale=1.0, # Per-tensor scale for softmax qdq (runtime scalar; amax/448 or amax/(6*448))
292-
V_QDQ: tl.constexpr = 0, # Fake quant-dequant of V: 0=off, 1=FP8 E4M3, 2=NVFP4
293+
V_QDQ: tl.constexpr = 0, # Fake quant-dequant of V: 0=off, 2=NVFP4
293294
v_qdq_scale=1.0,
294295
V_CACHE_QUANTIZED: tl.constexpr = False, # complete block-16 groups are already QDQ
295296
Sparsity_total=None, # Optional int64 scalar for counting total tiles (atomic)
@@ -476,13 +477,10 @@ def _attn_fwd(
476477
mask=((kv_start + kv_pos[:, None]) < seq_len_kv) & d_mask[None, :],
477478
other=0.0,
478479
)
479-
if V_QDQ != 0 and (
480+
if V_QDQ == 2 and (
480481
(not V_CACHE_QUANTIZED) or (kv_start + BLOCK_N > v_quantized_boundary)
481482
):
482-
if V_QDQ == 1:
483-
v_qdq = _qdq_fp8(v.to(tl.float32), v_qdq_scale)
484-
else:
485-
v_qdq = _v_qdq_nvfp4(v.to(tl.float32), v_qdq_scale, BLOCK_N, BLOCK_D)
483+
v_qdq = _v_qdq_nvfp4(v.to(tl.float32), v_qdq_scale, BLOCK_N, BLOCK_D)
486484
v_qdq = v_qdq.to(v.dtype)
487485
if V_CACHE_QUANTIZED:
488486
use_qdq = (kv_start + kv_pos) >= v_quantized_boundary
@@ -1298,12 +1296,11 @@ def attention(
12981296
and the global scale ``amax / (6 * 448)`` for NVFP4. A runtime
12991297
scalar — user-set or calibrated values do not recompile the
13001298
kernel. Values above amax saturate.
1301-
v_qdq: Fake quant-dequant of V before ``P @ V``. ``"fp8"`` uses a
1302-
per-tensor E4M3 scale; ``"nvfp4"`` uses signed E2M1 values with
1303-
one E4M3 scale per 16 keys. ``None`` disables V QDQ.
1299+
v_qdq: Fake quant-dequant of V before ``P @ V``. ``"nvfp4"`` uses
1300+
signed E2M1 values with one E4M3 scale per 16 keys. ``None``
1301+
disables V QDQ.
13041302
v_qdq_amax: Optional per-tensor V amax. ``None`` uses global scale 1;
1305-
otherwise converts to ``amax / 448`` (FP8) or ``amax / (6 * 448)``
1306-
(NVFP4).
1303+
otherwise converts to the NVFP4 global scale ``amax / (6 * 448)``.
13071304
v_cache_quantized: Complete block-16 groups in the paged V cache are
13081305
already QDQ; only the pristine partial group is QDQ on read.
13091306
k_cache: Paged K cache [num_blocks, page_size, num_kv_heads, head_dim].
@@ -1330,11 +1327,11 @@ def attention(
13301327
# silently reuse stale compiled kernels from the on-disk cache.
13311328
_load_sparsity_helpers()
13321329
_load_qdq_helpers()
1333-
if p_qdq not in _QDQ_MODES:
1330+
if p_qdq not in _P_QDQ_MODES:
13341331
raise ValueError(
1335-
f"p_qdq must be one of {sorted(k for k in _QDQ_MODES if k)} or None, got {p_qdq!r}"
1332+
f"p_qdq must be one of {sorted(k for k in _P_QDQ_MODES if k)} or None, got {p_qdq!r}"
13361333
)
1337-
p_qdq_mode = _QDQ_MODES[p_qdq]
1334+
p_qdq_mode = _P_QDQ_MODES[p_qdq]
13381335
# Convert the per-tensor amax to the kernel's scale convention
13391336
# (``q = cast(p / scale) * scale``): FP8 uses ``amax / 448``; NVFP4 uses the
13401337
# global scale ``amax / (6 * 448)``. amax=1 (the default, the theoretical
@@ -1344,18 +1341,18 @@ def attention(
13441341
if not (math.isfinite(p_qdq_amax) and p_qdq_amax > 0):
13451342
raise ValueError(f"p_qdq_amax must be a finite positive value, got {p_qdq_amax}")
13461343
p_qdq_scale = p_qdq_amax / 448.0 if p_qdq == "fp8" else p_qdq_amax / (6.0 * 448.0)
1347-
if v_qdq not in _QDQ_MODES:
1344+
if v_qdq not in _V_QDQ_MODES:
13481345
raise ValueError(
1349-
f"v_qdq must be one of {sorted(k for k in _QDQ_MODES if k)} or None, got {v_qdq!r}"
1346+
f"v_qdq must be one of {sorted(k for k in _V_QDQ_MODES if k)} or None, got {v_qdq!r}"
13501347
)
1351-
v_qdq_mode = _QDQ_MODES[v_qdq]
1348+
v_qdq_mode = _V_QDQ_MODES[v_qdq]
13521349
if v_qdq_mode and any(t.requires_grad for t in (q, k, v)):
13531350
raise NotImplementedError("v_qdq is inference-only and does not support autograd")
13541351
v_qdq_scale = 1.0
13551352
if v_qdq_mode and v_qdq_amax is not None:
13561353
if not (math.isfinite(v_qdq_amax) and v_qdq_amax > 0):
13571354
raise ValueError(f"v_qdq_amax must be a finite positive value, got {v_qdq_amax}")
1358-
v_qdq_scale = v_qdq_amax / 448.0 if v_qdq == "fp8" else v_qdq_amax / (6.0 * 448.0)
1355+
v_qdq_scale = v_qdq_amax / (6.0 * 448.0)
13591356
if v_cache_quantized and v_qdq != "nvfp4":
13601357
raise ValueError("v_cache_quantized requires v_qdq='nvfp4'")
13611358
if v_cache_quantized and any(x is None for x in (k_cache, v_cache, block_table)):

modelopt/torch/kernels/quantization/attention/v_qdq.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,14 @@ def fake_quant_v_onwrite(
8686
max_new_tokens: int,
8787
page_size: int = 16,
8888
v_qdq_scale: float = 1.0,
89-
decode: bool = False,
9089
) -> None:
9190
"""NVFP4-finalize complete block-16 groups in ``[v_lo, v_hi)`` in place.
9291
9392
``max_new_tokens`` is host metadata used to size the masked launch grid.
94-
Decode uses one fixed group per request. Eager prefill covers every group
95-
that the largest query chunk can complete without reading device metadata.
96-
``v_lo`` and ``v_hi`` must describe aligned, completed block-16 boundaries;
97-
their device values are not host-validated.
93+
The grid covers every group that the largest query chunk can complete
94+
without reading device metadata. ``v_lo`` and ``v_hi`` must describe
95+
aligned, completed block-16 boundaries; their device values are not
96+
host-validated.
9897
"""
9998
if page_size != v_cache.shape[1]:
10099
raise ValueError(f"page_size {page_size} must match v_cache.shape[1] {v_cache.shape[1]}")
@@ -105,7 +104,7 @@ def fake_quant_v_onwrite(
105104

106105
batch, max_blocks = block_table.shape
107106
num_kv_heads, head_dim = v_cache.shape[2:]
108-
num_groups = 1 if decode else triton.cdiv(max_new_tokens, _BLOCK_N)
107+
num_groups = triton.cdiv(max_new_tokens, _BLOCK_N)
109108

110109
with torch.cuda.device(v_cache.device):
111110
_fake_quant_v_onwrite_kernel[(batch, num_kv_heads, num_groups)](

modelopt/torch/kernels/quantization/gemm/fp4_kernel_hopper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def fp4_fake_quant_kernel(
8181
block_max = tl.max(x_abs, axis=2, keep_dims=True)
8282

8383
block_max_quant = fp8_quantize_scale(block_max, global_scale_safe)
84+
block_max_quant = tl.where(block_max_quant >= 1e-5, block_max_quant, 1.0)
8485

8586
block_max_quant_broadcast = tl.broadcast_to(
8687
block_max_quant, (TILE_M, NUM_FP4_BLOCKS, BLOCK_SIZE)

modelopt/torch/sparsity/attention_sparsity/plugins/vllm.py

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,9 @@ def _any_quant_active(layer, p_qdq, v_qdq) -> bool:
171171
)
172172

173173

174-
def _should_run_modelopt_kernel(sparse_kw, quant_active: bool, force_kernel: bool) -> bool:
174+
def _should_run_modelopt_kernel(sparse_kw, quant_active: bool) -> bool:
175175
"""Return whether a launch has effective work for the ModelOpt kernel."""
176-
return bool(sparse_kw or quant_active or force_kernel)
177-
178-
179-
def _mixed_batch_needs_dense_base(quant_active: bool, force_kernel: bool) -> bool:
180-
"""Return whether sparse-only mixed phases need one native output seed."""
181-
return not quant_active and not force_kernel
176+
return bool(sparse_kw or quant_active)
182177

183178

184179
@dataclass(frozen=True, slots=True)
@@ -188,7 +183,6 @@ class _ResolvedForward:
188183
v_qdq: str | None
189184
v_qdq_amax: float | None
190185
quant_active: bool
191-
force_kernel: bool
192186

193187

194188
def _resolve_forward(
@@ -203,10 +197,7 @@ def _resolve_forward(
203197
"""Resolve shared transform state or request the backend's native path."""
204198
p_qdq, p_qdq_amax, v_qdq, v_qdq_amax = _quant_kw_from_impl(impl, layer)
205199
quant_active = _any_quant_active(layer, p_qdq, v_qdq)
206-
force_kernel = getattr(layer, "_modelopt_force_kernel", False)
207-
transform_active = _should_run_modelopt_kernel(
208-
getattr(impl, "sparse_kw", None), quant_active, force_kernel
209-
)
200+
transform_active = _should_run_modelopt_kernel(getattr(impl, "sparse_kw", None), quant_active)
210201

211202
if getattr(attn_metadata, "use_cascade", False):
212203
if transform_active:
@@ -234,7 +225,6 @@ def _resolve_forward(
234225
v_qdq=v_qdq,
235226
v_qdq_amax=v_qdq_amax,
236227
quant_active=quant_active,
237-
force_kernel=force_kernel,
238228
)
239229

240230

@@ -260,7 +250,6 @@ def _forward_modelopt(
260250
v_qdq: str | None,
261251
v_qdq_amax: float | None,
262252
quant_active: bool,
263-
force_kernel: bool,
264253
dense_fallback,
265254
prepare_modelopt=None,
266255
) -> torch.Tensor:
@@ -281,16 +270,9 @@ def _forward_modelopt(
281270
# N:M sparse softmax is prefill-only.
282271
for name in ("sparsity_n", "sparsity_m", "dense_sink_tokens", "dense_recent_tokens"):
283272
sparse_kw.pop(name, None)
284-
if not _should_run_modelopt_kernel(
285-
sparse_kw,
286-
quant_active,
287-
force_kernel,
288-
):
273+
if not _should_run_modelopt_kernel(sparse_kw, quant_active):
289274
# Dynamic calibration can disable sparse work for a launch. Preserve the
290275
# backend's native dense path when no ModelOpt transform remains active.
291-
# ``_modelopt_force_kernel`` (isolation knob) keeps the bf16 kernel path
292-
# even with all quant disabled, so the on/off PPL diff holds the kernel
293-
# constant.
294276
return dense_fallback()
295277
if prepare_modelopt is not None:
296278
prepare_modelopt()
@@ -309,7 +291,6 @@ def _forward_modelopt(
309291
max_new_tokens=max_query_len,
310292
page_size=page_size,
311293
v_qdq_scale=v_qdq_scale,
312-
decode=is_decode_only,
313294
)
314295

315296
q = query[:num_actual_tokens].contiguous()
@@ -459,7 +440,6 @@ def native_forward():
459440
v_qdq=resolved.v_qdq,
460441
v_qdq_amax=resolved.v_qdq_amax,
461442
quant_active=resolved.quant_active,
462-
force_kernel=resolved.force_kernel,
463443
dense_fallback=native_forward,
464444
)
465445

@@ -633,7 +613,6 @@ def prepare_modelopt():
633613
"v_qdq": resolved.v_qdq,
634614
"v_qdq_amax": resolved.v_qdq_amax,
635615
"quant_active": resolved.quant_active,
636-
"force_kernel": resolved.force_kernel,
637616
"dense_fallback": dense_fallback,
638617
"prepare_modelopt": prepare_modelopt,
639618
}
@@ -662,10 +641,7 @@ def prepare_modelopt():
662641
# Sparse-only launches may have an inactive phase (for example N:M
663642
# is prefill-only). Compute the native result once, then overwrite
664643
# each active phase with its ModelOpt result.
665-
if _mixed_batch_needs_dense_base(
666-
resolved.quant_active,
667-
resolved.force_kernel,
668-
):
644+
if not resolved.quant_active:
669645
dense_fallback()
670646

671647
block_table = attn_metadata._modelopt_block_table

tests/gpu/torch/kernels/common/attention/test_triton_fa_p_qdq.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ def test_invalid_mode_raises(self):
488488
("kwargs", "match"),
489489
[
490490
({"v_qdq": "int8"}, "v_qdq"),
491+
({"v_qdq": "fp8"}, "v_qdq"),
491492
({"v_qdq": "nvfp4", "v_qdq_amax": 0.0}, "v_qdq_amax"),
492493
({"v_qdq": "nvfp4", "v_cache_quantized": True}, "v_cache_quantized"),
493494
],

tests/gpu/torch/kernels/common/attention/test_triton_fa_paged.py

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -279,60 +279,6 @@ def test_paged_different_page_sizes(self, page_size):
279279

280280
torch.testing.assert_close(out_paged, out_contig, rtol=1e-2, atol=1e-2)
281281

282-
def test_paged_decode_ignores_sparse_nm(self):
283-
"""N:M sparse softmax is prefill-only; paged decode remains dense."""
284-
batch = 2
285-
seq_lens_k = [64, 128]
286-
num_heads, num_kv_heads, head_dim = 4, 2, 64
287-
page_size = 16
288-
scale = 1.0 / (head_dim**0.5)
289-
total_kv = sum(seq_lens_k)
290-
291-
torch.manual_seed(34)
292-
q_flat = torch.randn(batch, num_heads, head_dim, device="cuda", dtype=torch.float16)
293-
k_flat = torch.randn(total_kv, num_kv_heads, head_dim, device="cuda", dtype=torch.float16)
294-
v_flat = torch.randn(total_kv, num_kv_heads, head_dim, device="cuda", dtype=torch.float16)
295-
296-
b_start_loc_q = torch.arange(batch, device="cuda", dtype=torch.int32)
297-
b_seq_len_q = torch.ones(batch, device="cuda", dtype=torch.int32)
298-
cumsum = [0]
299-
for sl in seq_lens_k:
300-
cumsum.append(cumsum[-1] + sl)
301-
b_start_loc_k = torch.tensor(cumsum[:-1], device="cuda", dtype=torch.int32)
302-
b_seq_len_k = torch.tensor(seq_lens_k, device="cuda", dtype=torch.int32)
303-
k_cache, v_cache, block_table = _scatter_to_paged_cache(
304-
k_flat, v_flat, b_start_loc_k, b_seq_len_k, num_kv_heads, head_dim, page_size
305-
)
306-
307-
common_kwargs = {
308-
"is_causal": False,
309-
"softmax_scale": scale,
310-
"b_start_loc_k": b_start_loc_k,
311-
"b_seq_len_k": b_seq_len_k,
312-
"max_input_len_k": max(seq_lens_k),
313-
"k_cache": k_cache,
314-
"v_cache": v_cache,
315-
"block_table": block_table,
316-
"page_size": page_size,
317-
}
318-
out_dense = attention(
319-
q_flat, k_flat, v_flat, b_start_loc_q, b_seq_len_q, 1, **common_kwargs
320-
)
321-
out_sparse = attention(
322-
q_flat,
323-
k_flat,
324-
v_flat,
325-
b_start_loc_q,
326-
b_seq_len_q,
327-
1,
328-
**common_kwargs,
329-
sparsity_n=2,
330-
sparsity_m=4,
331-
dense_recent_tokens=64,
332-
)
333-
334-
torch.testing.assert_close(out_sparse, out_dense, rtol=1e-2, atol=1e-2)
335-
336282
def test_paged_mixed_prefill_decode_sparse_nm_keeps_decode_dense(self):
337283
"""Decode rows stay dense even when batched with sparse prefill rows."""
338284
q_lens = [64, 1]
@@ -447,7 +393,6 @@ def test_v_cache_finalizes_complete_groups_once(self):
447393
torch.tensor([48], device="cuda", dtype=torch.int32),
448394
max_new_tokens=1,
449395
page_size=page_size,
450-
decode=True,
451396
)
452397
torch.testing.assert_close(baked[:2], after_prefill[:2], rtol=0, atol=0)
453398
assert torch.all(baked[:3] == 0.015625)

tests/gpu_vllm/torch/sparsity/attention_sparsity/test_quant_sparse_attn_worker.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -134,29 +134,6 @@ def test_install_converts_only_attention_and_configures_fixed_recipe(monkeypatch
134134
assert state.model_runner.cascade_attn_enabled is False
135135

136136

137-
def test_quant_off_disables_recipe_but_keeps_modelopt_adapter(monkeypatch):
138-
_patch_conversion(monkeypatch)
139-
monkeypatch.setenv("MODELOPT_ATTN_QUANT_OFF", "1")
140-
attention = _attention()
141-
state = _worker(nn.ModuleDict({"attn": attention}))
142-
143-
worker_module._install_attention(state, quantize=True)
144-
145-
converted = state.model_runner.model["attn"]
146-
for name in ("q_bmm_quantizer", "k_bmm_quantizer", "p_bmm_quantizer", "v_bmm_quantizer"):
147-
assert not getattr(converted, name).is_enabled
148-
assert converted._query_quant_in_kernel is False
149-
assert converted._value_quant_in_kernel is False
150-
assert converted._modelopt_force_kernel is True
151-
assert type(converted.impl) is ModelOptSparseAttentionImpl
152-
assert converted.impl.quant_kw == {
153-
"p_qdq": None,
154-
"p_qdq_amax": 1.0,
155-
"v_qdq": None,
156-
"v_qdq_amax": None,
157-
}
158-
159-
160137
def test_validation_of_all_layouts_precedes_mutation(monkeypatch):
161138
_patch_conversion(monkeypatch)
162139
good, bad = _attention(), _attention(AttentionType.ENCODER)

0 commit comments

Comments
 (0)