Skip to content

Commit 6649c28

Browse files
committed
Simplify sparse and quant attention plan validation
Signed-off-by: Kai Xu <kaix@nvidia.com>
1 parent 93952da commit 6649c28

3 files changed

Lines changed: 108 additions & 107 deletions

File tree

examples/vllm_serve/sparse_attn_worker.py

Lines changed: 99 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import importlib
1919
import os
20+
from collections import Counter
2021
from functools import cache
2122
from types import SimpleNamespace
2223
from typing import NamedTuple
@@ -61,7 +62,6 @@
6162

6263

6364
class _AttentionPlan(NamedTuple):
64-
name: str
6565
module: object
6666
new_impl: object
6767
sparse_kw: dict
@@ -74,6 +74,15 @@ def _unwrapped_model(worker):
7474
return model.unwrap() if hasattr(model, "unwrap") else model
7575

7676

77+
def _sparse_kwargs(name: str, sparse_cfg: dict | None) -> dict:
78+
if sparse_cfg is None:
79+
return {}
80+
layer_cfg = match_sparse_config(name, sparse_cfg)
81+
if layer_cfg is None or not layer_cfg.get("enable", True):
82+
return {}
83+
return _build_sparse_kw(layer_cfg)
84+
85+
7786
@cache
7887
def _load_quant_api(vllm_version: str):
7988
# Keep sparse-only module loading independent of quant-specific vLLM APIs.
@@ -113,16 +122,16 @@ def _cudagraph_mode(worker, api):
113122
return mode if mode is not None else api.compilation.CUDAGraphMode.NONE
114123

115124

116-
def _global_errors(worker, api=None) -> list[str]:
117-
api = api or _quant_api()
125+
def _global_errors(worker, api) -> list[str]:
118126
config = worker.model_runner.vllm_config
119-
parallel, cache, model_config = config.parallel_config, config.cache_config, config.model_config
127+
parallel = config.parallel_config
128+
cache_config, model_config = config.cache_config, config.model_config
120129
errors = []
121130
if getattr(parallel, "decode_context_parallel_size", 1) != 1:
122131
errors.append("decode_context_parallel_size must be 1")
123132
if getattr(parallel, "enable_dbo", False) or getattr(parallel, "use_ubatching", False):
124133
errors.append("DBO/ubatching is unsupported")
125-
if getattr(cache, "enable_prefix_caching", False):
134+
if getattr(cache_config, "enable_prefix_caching", False):
126135
errors.append("prefix caching is unsupported")
127136
if getattr(config, "kv_transfer_config", None) is not None:
128137
errors.append("KV transfer is unsupported")
@@ -132,7 +141,7 @@ def _global_errors(worker, api=None) -> list[str]:
132141
errors.append("FULL mixed-batch cudagraph mode is unsupported")
133142
if getattr(model_config, "dtype", None) not in (api.torch.float16, api.torch.bfloat16):
134143
errors.append("resolved model/KV-cache dtype must be fp16 or bf16")
135-
cache_dtype = getattr(cache, "cache_dtype", "auto")
144+
cache_dtype = getattr(cache_config, "cache_dtype", "auto")
136145
if str(cache_dtype) not in {"auto", "bfloat16", "float16", "torch.bfloat16", "torch.float16"}:
137146
errors.append(f"resolved KV-cache dtype {cache_dtype!r} must be fp16 or bf16")
138147
return errors
@@ -166,21 +175,8 @@ def _quant_layer_errors(module, api) -> list[str]:
166175
return errors
167176

168177

169-
def _validated_device_dtype(module, model_config, api):
170-
device, dtype = api.plugin._get_device_dtype(module)
171-
model_dtype = getattr(model_config, "dtype", None)
172-
if model_dtype in (api.torch.float16, api.torch.bfloat16):
173-
dtype = model_dtype
174-
if device is None or dtype is None:
175-
return device, dtype, "device/dtype could not be resolved"
176-
if dtype not in (api.torch.float16, api.torch.bfloat16):
177-
return device, dtype, f"resolved dtype {dtype} must be fp16 or bf16"
178-
return device, dtype, None
179-
180-
181-
def _sparse_graph_error(sparse_kw: dict, mode, api=None) -> str | None:
178+
def _sparse_graph_error(sparse_kw: dict, mode, api) -> str | None:
182179
"""Reject decode calibration whose live length would be frozen by a full graph."""
183-
api = api or _quant_api()
184180
params = sparse_kw.get("threshold_scale_factor")
185181
if (
186182
mode.decode_mode() == api.compilation.CUDAGraphMode.FULL
@@ -191,99 +187,103 @@ def _sparse_graph_error(sparse_kw: dict, mode, api=None) -> str | None:
191187
return None
192188

193189

194-
def _validated_attention_plans(worker, *, quantize: bool):
195-
"""Validate and clone every selected attention adapter before mutating layers."""
196-
api = _quant_api() if quantize else None
190+
def _select_new_impl(module):
191+
"""Clone the module's attention impl into its sparse-capable subclass; return (impl, error)."""
192+
try:
193+
cls = select_sparse_impl_cls(module.impl)
194+
except (NotImplementedError, TypeError) as err:
195+
return None, str(err)
196+
if cls is None:
197+
return None, (
198+
f"backend {type(module.impl).__name__} is not supported; "
199+
"expected FlashAttentionImpl or FlashInferImpl"
200+
)
201+
return _clone_sparse_impl(module.impl, cls), None
202+
203+
204+
def _raise_unsupported(errors: list[str], policy: str) -> None:
205+
if errors:
206+
raise NotImplementedError(
207+
f"Unsupported ModelOpt {policy} plan:\n - " + "\n - ".join(errors)
208+
)
209+
210+
211+
def _sparse_plans(worker):
212+
"""Plans for checkpoint-driven sparse attention; skips layers without a sparse config."""
197213
model = _unwrapped_model(worker)
198-
model_config = worker.model_runner.model_config
199-
detected = load_from_checkpoint_metadata(getattr(model_config, "hf_config", None))
200-
if not quantize and detected is None:
214+
detected = load_from_checkpoint_metadata(
215+
getattr(worker.model_runner.model_config, "hf_config", None)
216+
)
217+
if detected is None:
201218
print(
202219
"[ModelOpt] No sparse_attention_config found in the checkpoint; "
203-
"skipping sparse attention. Run examples/llm_sparsity/"
204-
"attention_sparsity/hf_sa.py to calibrate and export a checkpoint "
205-
"with the config embedded."
220+
"skipping sparse attention. Run examples/llm_sparsity/attention_sparsity/"
221+
"hf_sa.py to calibrate and export a checkpoint with the config embedded."
206222
)
207223
return None
224+
sparse_cfg, sparse_algo = detected
225+
print(f"[ModelOpt] Sparse attention config: algo -> {sparse_algo}")
226+
plans, errors = [], []
227+
for name, module in model.named_modules():
228+
if not isinstance(module, VLLMAttention):
229+
continue
230+
sparse_kw = _sparse_kwargs(name, sparse_cfg)
231+
if not sparse_kw:
232+
continue
233+
new_impl, error = _select_new_impl(module)
234+
if error:
235+
errors.append(f"{name or '<root>'}: {error}")
236+
else:
237+
plans.append(_AttentionPlan(module, new_impl, sparse_kw, None, None))
238+
_raise_unsupported(errors, "sparse attention")
239+
return tuple(plans)
240+
208241

242+
def _quant_plans(worker):
243+
"""Plans for fixed-NVFP4 attention on every decoder self-attention layer (+ optional sparsity)."""
244+
api = _quant_api()
245+
model = _unwrapped_model(worker)
246+
model_config = worker.model_runner.model_config
247+
detected = load_from_checkpoint_metadata(getattr(model_config, "hf_config", None))
209248
sparse_cfg = detected[0] if detected is not None else None
210-
if quantize:
211-
assert api is not None
212-
errors = _global_errors(worker, api)
213-
mode = _cudagraph_mode(worker, api)
214-
attention_types = api.plugin._ATTENTION_TYPES
215-
else:
216-
assert detected is not None
217-
print(f"[ModelOpt] Sparse attention config: algo -> {detected[1]}")
218-
errors, mode, attention_types = [], None, (VLLMAttention,)
219-
plans = []
220-
attention_count = 0
249+
errors = _global_errors(worker, api)
250+
mode = _cudagraph_mode(worker, api)
251+
plans, attention_count = [], 0
221252
for name, module in model.named_modules():
222-
if not isinstance(module, attention_types):
253+
if not isinstance(module, api.plugin._ATTENTION_TYPES):
223254
continue
224-
if quantize:
225-
assert api is not None
226-
attention_count += 1
227-
reasons = _quant_layer_errors(module, api)
228-
device, dtype, dtype_error = _validated_device_dtype(module, model_config, api)
229-
if dtype_error:
230-
reasons.append(dtype_error)
231-
layer_cfg = match_sparse_config(name, sparse_cfg) if sparse_cfg is not None else None
232-
sparse_kw = (
233-
_build_sparse_kw(layer_cfg)
234-
if layer_cfg is not None and layer_cfg.get("enable", True)
235-
else {}
236-
)
237-
if graph_error := _sparse_graph_error(sparse_kw, mode, api):
238-
reasons.append(graph_error)
239-
else:
240-
assert sparse_cfg is not None
241-
layer_cfg = match_sparse_config(name, sparse_cfg)
242-
if layer_cfg is None or not layer_cfg.get("enable", True):
243-
continue
244-
sparse_kw = _build_sparse_kw(layer_cfg)
245-
if not sparse_kw:
246-
continue
247-
reasons, device, dtype = [], None, None
248-
249-
new_impl = None
250-
try:
251-
new_impl_cls = select_sparse_impl_cls(module.impl)
252-
if new_impl_cls is None:
253-
backend = type(module.impl).__name__
254-
message = (
255-
f"backend {backend} is not supported; expected FlashAttentionImpl or FlashInferImpl"
256-
if quantize
257-
else f"unsupported backend {backend}"
258-
)
259-
reasons.append(message)
260-
else:
261-
new_impl = _clone_sparse_impl(module.impl, new_impl_cls)
262-
except (NotImplementedError, TypeError) as err:
263-
reasons.append(str(err))
264-
layer_name = name or "<root>"
255+
attention_count += 1
256+
reasons = _quant_layer_errors(module, api)
257+
# Prefer the model compute dtype (fp16/bf16); _get_device_dtype's buffer scan
258+
# can otherwise report fp32 from the attention module's scale buffers.
259+
device, dtype = api.plugin._get_device_dtype(module)
260+
if getattr(model_config, "dtype", None) in (api.torch.float16, api.torch.bfloat16):
261+
dtype = model_config.dtype
262+
if device is None or dtype is None:
263+
reasons.append("device/dtype could not be resolved")
264+
elif dtype not in (api.torch.float16, api.torch.bfloat16):
265+
reasons.append(f"resolved dtype {dtype} must be fp16 or bf16")
266+
sparse_kw = _sparse_kwargs(name, sparse_cfg)
267+
if graph_error := _sparse_graph_error(sparse_kw, mode, api):
268+
reasons.append(graph_error)
269+
new_impl, error = _select_new_impl(module)
270+
if error:
271+
reasons.append(error)
265272
if reasons:
266-
errors.extend(f"{layer_name}: {reason}" for reason in reasons)
273+
errors.extend(f"{name or '<root>'}: {reason}" for reason in reasons)
267274
else:
268-
plans.append(_AttentionPlan(name, module, new_impl, sparse_kw, device, dtype))
269-
270-
if quantize and attention_count == 0:
275+
plans.append(_AttentionPlan(module, new_impl, sparse_kw, device, dtype))
276+
if attention_count == 0:
271277
errors.append("no regular attention layers were found")
272-
if errors:
273-
policy = "attention" if quantize else "sparse attention"
274-
raise NotImplementedError(
275-
f"Unsupported ModelOpt {policy} plan:\n - " + "\n - ".join(errors)
276-
)
278+
_raise_unsupported(errors, "attention")
277279
return tuple(plans)
278280

279281

280282
def _install_sparse_plans(plans) -> None:
281-
installed = {}
282283
for plan in plans:
283284
plan.new_impl.sparse_kw = plan.sparse_kw
284285
plan.module.impl = plan.new_impl
285-
impl_name = type(plan.new_impl).__name__
286-
installed[impl_name] = installed.get(impl_name, 0) + 1
286+
installed = dict(Counter(type(plan.new_impl).__name__ for plan in plans))
287287
print(
288288
f"[ModelOpt] Sparse attention: replaced impl on {len(plans)} attention layers: {installed}"
289289
)
@@ -292,7 +292,6 @@ def _install_sparse_plans(plans) -> None:
292292
def _install_quant_plans(worker, plans) -> None:
293293
api = _quant_api()
294294
quant_off = os.environ.get("MODELOPT_ATTN_QUANT_OFF") == "1"
295-
installed = {}
296295
for plan in plans:
297296
module = plan.module
298297
module.device, module.dtype = plan.device, plan.dtype
@@ -322,20 +321,18 @@ def _install_quant_plans(worker, plans) -> None:
322321
module._value_quant_in_kernel = not quant_off
323322
if quant_off:
324323
module._modelopt_force_kernel = True
325-
impl_name = type(plan.new_impl).__name__
326-
installed[impl_name] = installed.get(impl_name, 0) + 1
327324
worker.model_runner.cascade_attn_enabled = False
325+
installed = dict(Counter(type(plan.new_impl).__name__ for plan in plans))
328326
print(f"[ModelOpt] Installed NVFP4 quant+sparse attention on {len(plans)} layers: {installed}")
329327

330328

331329
def _install_attention(worker, *, quantize: bool) -> None:
332-
plans = _validated_attention_plans(worker, quantize=quantize)
333-
if plans is None:
334-
return
335330
if quantize:
336-
_install_quant_plans(worker, plans)
331+
_install_quant_plans(worker, _quant_plans(worker))
337332
else:
338-
_install_sparse_plans(plans)
333+
plans = _sparse_plans(worker)
334+
if plans is not None:
335+
_install_sparse_plans(plans)
339336

340337

341338
class _ModelOptAttentionWorker(BaseWorker):

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def _patch_conversion(monkeypatch):
9393
lambda: quant_plugin.ParallelState(data_parallel_group=None),
9494
)
9595
monkeypatch.setattr(worker_module, "load_from_checkpoint_metadata", lambda _: None)
96-
monkeypatch.setattr(worker_module, "_global_errors", lambda _worker, _api=None: [])
96+
monkeypatch.setattr(worker_module, "_global_errors", lambda _worker, _api: [])
9797

9898

9999
@pytest.mark.parametrize("impl_cls", [FlashAttentionImpl, FlashInferImpl])
@@ -217,19 +217,21 @@ def profile(actual_worker):
217217
[(CUDAGraphMode.FULL, True), (CUDAGraphMode.FULL_AND_PIECEWISE, False)],
218218
)
219219
def test_full_mixed_cudagraph_validation(mode, rejected):
220+
api = worker_module._quant_api()
220221
config = SimpleNamespace(
221222
parallel_config=SimpleNamespace(),
222223
cache_config=SimpleNamespace(),
223224
model_config=SimpleNamespace(dtype=torch.float16),
224225
compilation_config=SimpleNamespace(cudagraph_mode=mode),
225226
)
226227
errors = worker_module._global_errors(
227-
SimpleNamespace(model_runner=SimpleNamespace(vllm_config=config))
228+
SimpleNamespace(model_runner=SimpleNamespace(vllm_config=config)), api
228229
)
229230
assert any("mixed" in error for error in errors) is rejected
230231

231232

232233
def test_calibrated_decode_skip_softmax_rejects_full_decode_graphs():
234+
api = worker_module._quant_api()
233235
sparse_kw = {
234236
"threshold_scale_factor": {
235237
"prefill": {"a": 1.0, "b": 2.0},
@@ -238,13 +240,14 @@ def test_calibrated_decode_skip_softmax_rejects_full_decode_graphs():
238240
}
239241

240242
assert "calibrated decode skip-softmax" in worker_module._sparse_graph_error(
241-
sparse_kw, CUDAGraphMode.FULL_AND_PIECEWISE
243+
sparse_kw, CUDAGraphMode.FULL_AND_PIECEWISE, api
242244
)
243-
assert worker_module._sparse_graph_error(sparse_kw, CUDAGraphMode.PIECEWISE) is None
245+
assert worker_module._sparse_graph_error(sparse_kw, CUDAGraphMode.PIECEWISE, api) is None
244246
assert (
245247
worker_module._sparse_graph_error(
246248
{"threshold_scale_factor": {"prefill": {"a": 1.0, "b": 2.0}}},
247249
CUDAGraphMode.FULL_AND_PIECEWISE,
250+
api,
248251
)
249252
is None
250253
)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,8 @@ def test_sparse_worker_rejects_unsupported_backend_before_mutation(monkeypatch):
612612
worker_module._install_attention(state, quantize=False)
613613

614614
assert str(exc.value) == (
615-
"Unsupported ModelOpt sparse attention plan:\n - bad: unsupported backend SimpleNamespace"
615+
"Unsupported ModelOpt sparse attention plan:\n - bad: backend SimpleNamespace "
616+
"is not supported; expected FlashAttentionImpl or FlashInferImpl"
616617
)
617618
assert good.impl is original_good_impl
618619
assert bad.impl.__class__ is SimpleNamespace

0 commit comments

Comments
 (0)