Skip to content

Commit c4e2146

Browse files
committed
Add per-operand FP8 attention quant formats for Q/K/P/V
Signed-off-by: Kai Xu <kaix@nvidia.com>
1 parent ea585eb commit c4e2146

10 files changed

Lines changed: 323 additions & 27 deletions

File tree

examples/vllm_serve/sparse_attn_worker.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
__all__ = ["SparseAttnWorker", "QuantSparseAttnWorker"] # noqa: RUF022
2626

27+
_QUANT_FORMAT_KEYS = ("q_format", "k_format", "p_format", "v_format")
28+
2729

2830
def _unwrapped_model(worker):
2931
model = worker.model_runner.model
@@ -32,9 +34,9 @@ def _unwrapped_model(worker):
3234

3335
def _print_install_report(policy, report) -> None:
3436
if report.installed_count:
35-
if policy == "NVFP4 attention":
37+
if policy != "Sparse attention":
3638
print(
37-
"[ModelOpt] Installed NVFP4 quant+sparse attention on "
39+
f"[ModelOpt] Installed {policy} (quant+sparse) on "
3840
f"{report.installed_count} layers: {dict(report.backend_counts)}"
3941
)
4042
else:
@@ -68,13 +70,32 @@ def load_model(self, *args, **kwargs) -> None:
6870

6971

7072
class QuantSparseAttnWorker(BaseWorker):
71-
"""Install fixed NVFP4 attention plus optional checkpoint sparsity."""
73+
"""Install quantized attention plus optional checkpoint sparsity.
74+
75+
Per-operand formats come from vLLM's ``--additional-config``; absent keys
76+
default to NVFP4 on all four operands (Q/K/P/V)::
77+
78+
--additional-config '{"modelopt_attn_quant": {"p_format": "fp8", "v_format": "fp8"}}'
79+
"""
80+
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)
7291

7392
def load_model(self, *args, **kwargs) -> None:
74-
"""Load the model, then install NVFP4 attention."""
93+
"""Load the model, then install the configured attention quant recipe."""
7594
super().load_model(*args, **kwargs)
76-
report = install_vllm_nvfp4_attention(self.model_runner, sparse_cfg="checkpoint")
77-
_print_install_report("NVFP4 attention", report)
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)
7899

79100
def determine_available_memory(self) -> int:
80101
"""Profile memory without compiling the dynamically converted modules."""

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
_load_paged_v_tile,
3333
)
3434
from modelopt.torch.kernels.quantization.attention.bmm2_qdq import _p_qdq_nvfp4, _v_qdq_nvfp4
35+
from modelopt.torch.kernels.quantization.common.fp8_quant import fp8_scalar_qdq
3536

3637
__all__ = ["attention_decode"]
3738

3839
_BLOCK_N = 128
3940
_DEFAULT_KV_SPLITS = 32
4041
_MAX_KV_SPLITS = 32
41-
_QDQ_MODES = {None, "nvfp4"}
42+
_P_QDQ_MODES = {None: 0, "fp8": 1, "nvfp4": 2}
43+
_V_QDQ_MODES = {None, "nvfp4"}
4244

4345

4446
@triton.jit
@@ -137,7 +139,11 @@ def _decode_split_kernel(
137139
running_sum = running_sum * correction + tl.sum(p, axis=0)
138140
acc *= correction
139141

140-
if P_QDQ:
142+
if P_QDQ == 1:
143+
# FP8 E4M3 per-tensor QDQ of the split-local unnormalized P
144+
# (elementwise -> split-count- and batch-shape-invariant).
145+
p = fp8_scalar_qdq(p, p_qdq_scale)
146+
elif P_QDQ == 2:
141147
p = tl.reshape(
142148
_p_qdq_nvfp4(
143149
tl.reshape(p.to(V_cache.dtype.element_ty).to(tl.float32), (1, BLOCK_N)),
@@ -238,15 +244,20 @@ def _decode_combine_kernel(
238244

239245

240246
def _qdq_scale(mode: str | None, amax: float | None, operand: str) -> float:
241-
if mode not in _QDQ_MODES:
242-
raise ValueError(f"{operand}_qdq must be 'nvfp4' or None, got {mode!r}")
247+
allowed = _P_QDQ_MODES if operand == "p" else _V_QDQ_MODES
248+
if mode not in allowed:
249+
raise ValueError(
250+
f"{operand}_qdq must be one of {sorted(m for m in allowed if m)} or None, got {mode!r}"
251+
)
243252
if mode is None:
244253
return 1.0
245254
if amax is None:
246255
return 1.0
247256
if not (math.isfinite(amax) and amax > 0.0):
248257
raise ValueError(f"{operand}_qdq_amax must be finite and positive, got {amax}")
249-
return amax / (6.0 * 448.0)
258+
# FP8 uses the per-tensor convention ``amax / 448``; NVFP4 the two-level
259+
# global scale ``amax / (6 * 448)`` (matches the prefill kernel).
260+
return amax / 448.0 if mode == "fp8" else amax / (6.0 * 448.0)
250261

251262

252263
def attention_decode(
@@ -337,7 +348,7 @@ def attention_decode(
337348
PAGE_SIZE=page_size,
338349
max_blocks_per_seq=block_table.shape[1],
339350
NUM_KV_SPLITS=num_kv_splits,
340-
P_QDQ=p_qdq == "nvfp4",
351+
P_QDQ=_P_QDQ_MODES[p_qdq],
341352
V_QDQ=v_qdq == "nvfp4",
342353
V_CACHE_QUANTIZED=v_cache_quantized,
343354
num_warps=4,

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,9 @@ def forward(
989989
"IS_CAUSAL": is_causal,
990990
"HEAD_DIM": HEAD_DIM,
991991
"STORE_LSE": True,
992-
"Q_IS_FP32": q.dtype == torch.float32 and (p_qdq_mode == 2 or v_qdq_mode == 2),
992+
# An fp32 Q (the dynamic-quant carrier) always needs the fp32 BMM1 dot;
993+
# gating on QDQ modes would miss recipes like fp8-BMM2 (P mode 1).
994+
"Q_IS_FP32": q.dtype == torch.float32,
993995
"SPARSITY_N": sparsity_n,
994996
"SPARSITY_M": sparsity_m,
995997
"DENSE_SINK_TOKENS": dense_sink_tokens,

modelopt/torch/quantization/plugins/vllm.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,39 @@
4747
for name in ("q", "k", "p", "v")
4848
),
4949
]
50+
_FP8_ATTENTION_QUANTIZER_CFG = {"num_bits": (4, 3)} # per-tensor E4M3, static scale amax/448
51+
_BMM2_FORMAT_CFGS = {"nvfp4": _NVFP4_ATTENTION_QUANTIZER_CFG, "fp8": _FP8_ATTENTION_QUANTIZER_CFG}
52+
53+
54+
def build_vllm_attention_quant_cfg(
55+
*,
56+
q_format: str = "nvfp4",
57+
k_format: str = "nvfp4",
58+
p_format: str = "nvfp4",
59+
v_format: str = "nvfp4",
60+
) -> list:
61+
"""Build the attention BMM quantizer config with per-operand formats.
62+
63+
Each of Q/K/P/V takes "nvfp4" (dynamic block-16, two-level scale) or
64+
"fp8" (per-tensor E4M3, static scale amax/448).
65+
"""
66+
formats = {"q": q_format, "k": k_format, "p": p_format, "v": v_format}
67+
for name, fmt in formats.items():
68+
if fmt not in _BMM2_FORMAT_CFGS:
69+
raise ValueError(
70+
f"{name}_format must be one of {sorted(_BMM2_FORMAT_CFGS)}, got {fmt!r}"
71+
)
72+
return [
73+
{"quantizer_name": "*_bmm_quantizer", "enable": False},
74+
*(
75+
{
76+
"quantizer_name": f"*{name}_bmm_quantizer",
77+
"cfg": _BMM2_FORMAT_CFGS[fmt],
78+
"enable": True,
79+
}
80+
for name, fmt in formats.items()
81+
),
82+
]
5083

5184

5285
def _import_attention_module():
@@ -224,11 +257,32 @@ def _set_vllm_attention_kv_default_amax(module, device: torch.device) -> None:
224257
quantizer.amax = torch.tensor(6.0 * 448.0, device=device, dtype=torch.float32)
225258

226259

260+
def _set_vllm_attention_fp8_bmm2_default_amax(module, device: torch.device) -> None:
261+
"""Set fixed amax on uncalibrated per-tensor FP8 BMM2 quantizers (P=1.0, V=448)."""
262+
for name, default in (
263+
("q_bmm_quantizer", 448.0),
264+
("k_bmm_quantizer", 448.0),
265+
("p_bmm_quantizer", 1.0),
266+
("v_bmm_quantizer", 448.0),
267+
):
268+
quantizer = getattr(module, name, None)
269+
if (
270+
not isinstance(quantizer, TensorQuantizer)
271+
or not quantizer.is_enabled
272+
or getattr(quantizer, "num_bits", None) != (4, 3)
273+
or getattr(quantizer, "block_sizes", None)
274+
or hasattr(quantizer, "_amax")
275+
):
276+
continue
277+
quantizer.amax = torch.tensor(default, device=device, dtype=torch.float32)
278+
279+
227280
def configure_vllm_nvfp4_attention_quantizers(
228281
module: torch.nn.Module,
229282
*,
230283
device: torch.device | str,
231284
dtype: torch.dtype,
285+
cfg: list | None = None,
232286
) -> torch.nn.Module:
233287
"""Configure one vLLM Attention module for fused NVFP4 fake quantization.
234288
@@ -256,10 +310,11 @@ def configure_vllm_nvfp4_attention_quantizers(
256310
if not hasattr(module, "p_bmm_quantizer"):
257311
module.p_bmm_quantizer = TensorQuantizer()
258312

259-
set_quantizer_by_cfg(module, _VLLM_NVFP4_ATTENTION_QUANT_CFG)
313+
set_quantizer_by_cfg(module, _VLLM_NVFP4_ATTENTION_QUANT_CFG if cfg is None else cfg)
260314
for name in ("q", "k", "p", "v"):
261315
getattr(module, f"{name}_bmm_quantizer").to(device=device)
262316
_set_vllm_attention_kv_default_amax(module, device)
317+
_set_vllm_attention_fp8_bmm2_default_amax(module, device)
263318
return module
264319

265320

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,23 +121,31 @@ def _build_sparse_kw(layer_cfg: dict) -> dict:
121121

122122

123123
def _bmm_qdq_from_layer(layer, attr: str, default_amax: float | None):
124-
"""Map an enabled BMM2 quantizer to the kernel's NVFP4 mode and scalar amax."""
124+
"""Map an enabled BMM2 quantizer to the kernel's QDQ mode and scalar amax."""
125125
quantizer = getattr(layer, attr, None)
126126
if quantizer is None or not getattr(quantizer, "is_enabled", False):
127127
return None, default_amax
128128
if (
129-
not getattr(quantizer, "is_nvfp4_dynamic", False)
130-
or (quantizer.block_sizes or {}).get(-1) != 16
129+
getattr(quantizer, "is_nvfp4_dynamic", False)
130+
and (quantizer.block_sizes or {}).get(-1) == 16
131131
):
132+
mode = "nvfp4"
133+
elif getattr(quantizer, "num_bits", None) == (4, 3) and not getattr(
134+
quantizer, "block_sizes", None
135+
):
136+
# Per-tensor FP8 E4M3 (static scale amax/448)
137+
mode = "fp8"
138+
else:
132139
raise NotImplementedError(
133-
f"{attr} is enabled with an unsupported format; only dynamic block-16 NVFP4 is supported"
140+
f"{attr} is enabled with an unsupported format; only dynamic block-16 NVFP4 "
141+
"or per-tensor FP8 E4M3 is supported"
134142
)
135143
amax = getattr(quantizer, "_amax", None)
136144
if amax is None:
137-
return "nvfp4", default_amax
145+
return mode, default_amax
138146
if getattr(amax, "numel", lambda: 1)() != 1:
139147
raise NotImplementedError(f"{attr} requires a scalar amax, got shape {tuple(amax.shape)}")
140-
return "nvfp4", float(amax)
148+
return mode, float(amax)
141149

142150

143151
def _p_qdq_from_layer(layer) -> tuple[str | None, float]:

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

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ class _InstallPlan:
9494
layers: tuple[_AttentionPlan, ...]
9595
quantize: bool
9696
sparse_algorithm: str | None
97+
q_format: str = "nvfp4"
98+
k_format: str = "nvfp4"
99+
p_format: str = "nvfp4"
100+
v_format: str = "nvfp4"
97101

98102

99103
def _unwrapped_model(model_runner):
@@ -315,7 +319,16 @@ def _raise_unsupported(errors: list[str], policy: str) -> None:
315319
)
316320

317321

318-
def _plan_vllm_attention(model_runner, *, quantize: bool, sparse_cfg) -> _InstallPlan:
322+
def _plan_vllm_attention(
323+
model_runner,
324+
*,
325+
quantize: bool,
326+
sparse_cfg,
327+
q_format: str = "nvfp4",
328+
k_format: str = "nvfp4",
329+
p_format: str = "nvfp4",
330+
v_format: str = "nvfp4",
331+
) -> _InstallPlan:
319332
model = _unwrapped_model(model_runner)
320333
resolved_sparse_cfg, sparse_algorithm = _resolve_sparse_config(model_runner, sparse_cfg)
321334
candidates = []
@@ -329,7 +342,9 @@ def _plan_vllm_attention(model_runner, *, quantize: bool, sparse_cfg) -> _Instal
329342
candidates.append((name, module, sparse_kw))
330343

331344
if not candidates and not quantize:
332-
return _InstallPlan(model_runner, (), False, sparse_algorithm)
345+
return _InstallPlan(
346+
model_runner, (), False, sparse_algorithm, q_format, k_format, p_format, v_format
347+
)
333348

334349
_require_supported_vllm()
335350
errors = _global_errors(model_runner) if quantize else []
@@ -373,7 +388,16 @@ def _plan_vllm_attention(model_runner, *, quantize: bool, sparse_cfg) -> _Instal
373388
if quantize and attention_count == 0:
374389
errors.append("no regular attention layers were found")
375390
_raise_unsupported(errors, "NVFP4 attention" if quantize else "sparse attention")
376-
return _InstallPlan(model_runner, tuple(plans), quantize, sparse_algorithm)
391+
return _InstallPlan(
392+
model_runner,
393+
tuple(plans),
394+
quantize,
395+
sparse_algorithm,
396+
q_format,
397+
k_format,
398+
p_format,
399+
v_format,
400+
)
377401

378402

379403
def _build_report(plan: _InstallPlan) -> VllmAttentionInstallReport:
@@ -405,15 +429,36 @@ def _apply_vllm_attention_plans(plan: _InstallPlan) -> VllmAttentionInstallRepor
405429
for layer in plan.layers:
406430
layer.new_impl.sparse_kw = layer.sparse_kw
407431
if plan.quantize:
432+
# Pass cfg only for non-default formats: keeps the default call
433+
# signature stable for callers/fakes that predate the cfg parameter.
434+
_cfg_kwargs = (
435+
{
436+
"cfg": quant_plugin.build_vllm_attention_quant_cfg(
437+
q_format=plan.q_format,
438+
k_format=plan.k_format,
439+
p_format=plan.p_format,
440+
v_format=plan.v_format,
441+
)
442+
}
443+
if (plan.q_format, plan.k_format, plan.p_format, plan.v_format)
444+
!= ("nvfp4", "nvfp4", "nvfp4", "nvfp4")
445+
else {}
446+
)
408447
converted = quant_plugin.configure_vllm_nvfp4_attention_quantizers(
409448
layer.module,
410449
device=layer.device,
411450
dtype=layer.dtype,
451+
**_cfg_kwargs,
412452
)
413453
if converted is not None and converted is not layer.module:
414454
raise RuntimeError("vLLM attention quantization must convert modules in place")
415455
p_qdq, p_qdq_amax = attention_plugin._p_qdq_from_layer(layer.module)
416456
v_qdq, v_qdq_amax = attention_plugin._v_qdq_from_layer(layer.module)
457+
if plan.v_format == "fp8":
458+
# Per-tensor FP8 V is quantized module-level BEFORE the cache
459+
# write (each token is self-contained; no block geometry), so
460+
# the kernel sees pre-QDQ'd V and needs no V transform at all.
461+
v_qdq, v_qdq_amax = None, None
417462
layer.new_impl.quant_kw = {
418463
"p_qdq": p_qdq,
419464
"p_qdq_amax": p_qdq_amax,
@@ -425,8 +470,10 @@ def _apply_vllm_attention_plans(plan: _InstallPlan) -> VllmAttentionInstallRepor
425470
old_query_flag = getattr(layer.module, "_query_quant_in_kernel", missing)
426471
old_value_flag = getattr(layer.module, "_value_quant_in_kernel", missing)
427472
if plan.quantize:
428-
layer.module._query_quant_in_kernel = True
429-
layer.module._value_quant_in_kernel = True
473+
# fp8 Q is module-level (bf16 losslessly carries E4M3 QDQ values);
474+
# the kernel then runs a plain bf16 BMM1 with no Q transform.
475+
layer.module._query_quant_in_kernel = plan.q_format != "fp8"
476+
layer.module._value_quant_in_kernel = plan.v_format != "fp8"
430477
try:
431478
# Publish the adapter last so a native impl never runs with in-kernel
432479
# quantization flags that only the ModelOpt adapter understands.
@@ -463,6 +510,10 @@ def install_vllm_nvfp4_attention(
463510
model_runner,
464511
*,
465512
sparse_cfg="checkpoint",
513+
q_format: str = "nvfp4",
514+
k_format: str = "nvfp4",
515+
p_format: str = "nvfp4",
516+
v_format: str = "nvfp4",
466517
) -> VllmAttentionInstallReport:
467518
"""Install fixed NVFP4 attention with optional checkpoint sparsity.
468519
@@ -471,6 +522,22 @@ def install_vllm_nvfp4_attention(
471522
sparse_cfg: ``"checkpoint"`` to consume optional exported metadata, a
472523
resolved sparse config dict, or ``None`` for NVFP4-only attention.
473524
"""
525+
for name, fmt in (
526+
("q_format", q_format),
527+
("k_format", k_format),
528+
("p_format", p_format),
529+
("v_format", v_format),
530+
):
531+
if fmt not in ("nvfp4", "fp8"):
532+
raise ValueError(f"{name} must be 'nvfp4' or 'fp8', got {fmt!r}")
474533
return _apply_vllm_attention_plans(
475-
_plan_vllm_attention(model_runner, quantize=True, sparse_cfg=sparse_cfg)
534+
_plan_vllm_attention(
535+
model_runner,
536+
quantize=True,
537+
sparse_cfg=sparse_cfg,
538+
q_format=q_format,
539+
k_format=k_format,
540+
p_format=p_format,
541+
v_format=v_format,
542+
)
476543
)

0 commit comments

Comments
 (0)