1717
1818import importlib
1919import os
20+ from collections import Counter
2021from functools import cache
2122from types import SimpleNamespace
2223from typing import NamedTuple
6162
6263
6364class _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
7887def _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
280282def _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:
292292def _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
331329def _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
341338class _ModelOptAttentionWorker (BaseWorker ):
0 commit comments