@@ -200,9 +200,12 @@ def _resolve_forward(
200200 transform_active = _should_run_modelopt_kernel (getattr (impl , "sparse_kw" , None ), quant_active )
201201
202202 if getattr (attn_metadata , "use_cascade" , False ):
203- if transform_active :
203+ # Cascade is unimplemented by the ModelOpt kernel. Quantization must not be
204+ # silently dropped (it would change numerics), so reject it; a sparse-only
205+ # transform is numerically safe to delegate to the native dense path.
206+ if transform_active and quant_active :
204207 raise NotImplementedError (
205- "vLLM cascade attention is incompatible with an active ModelOpt attention transform "
208+ "vLLM cascade attention is incompatible with active ModelOpt attention quantization "
206209 )
207210 return None
208211
@@ -350,6 +353,83 @@ def _forward_modelopt(
350353 return output
351354
352355
356+ def _dispatch_modelopt (
357+ impl ,
358+ * ,
359+ query : torch .Tensor ,
360+ block_table : torch .Tensor ,
361+ seq_lens : torch .Tensor ,
362+ cu_seqlens_q : torch .Tensor ,
363+ num_actual_tokens : int ,
364+ max_query_len : int ,
365+ output : torch .Tensor ,
366+ num_decodes : int ,
367+ num_prefills : int ,
368+ num_decode_tokens : int ,
369+ num_prefill_tokens : int ,
370+ ** common_kw ,
371+ ) -> torch .Tensor :
372+ """Run the ModelOpt path, splitting mixed decode+prefill batches by phase.
373+
374+ NVFP4 P-QDQ is schedule-sensitive by design, so a decode result must not
375+ depend on whether a prefill request is co-scheduled. When a batch mixes
376+ ``q_len==1`` decode rows with ``q_len>1`` (chunked-)prefill rows,
377+ ``max_query_len > 1`` and the whole batch would otherwise take the prefill
378+ skip-softmax path. Split so each phase runs its own schedule -- decode rows
379+ always take the fixed decode path. Both the FlashAttention and FlashInfer
380+ adapters share this dispatch.
381+ """
382+ if not (num_decodes and num_prefills ):
383+ return _forward_modelopt (
384+ impl ,
385+ query = query ,
386+ block_table = block_table ,
387+ seq_lens = seq_lens ,
388+ cu_seqlens_q = cu_seqlens_q ,
389+ num_actual_tokens = num_actual_tokens ,
390+ max_query_len = max_query_len ,
391+ output = output ,
392+ ** common_kw ,
393+ )
394+
395+ if num_decode_tokens % num_decodes :
396+ raise NotImplementedError ("Non-uniform mixed decode is unsupported" )
397+ if num_decode_tokens + num_prefill_tokens != num_actual_tokens :
398+ raise ValueError ("Mixed-batch token counts do not match common metadata" )
399+
400+ # Sparse-only launches may have an inactive phase (for example N:M sparsity
401+ # is prefill-only). Compute the native result once, then overwrite each
402+ # active phase with its ModelOpt result.
403+ if not common_kw .get ("quant_active" , False ):
404+ common_kw ["dense_fallback" ]()
405+
406+ _forward_modelopt (
407+ impl ,
408+ query = query [:num_decode_tokens ],
409+ block_table = block_table [:num_decodes ],
410+ seq_lens = seq_lens [:num_decodes ],
411+ cu_seqlens_q = cu_seqlens_q [: num_decodes + 1 ],
412+ num_actual_tokens = num_decode_tokens ,
413+ max_query_len = num_decode_tokens // num_decodes ,
414+ output = output [:num_decode_tokens ],
415+ ** common_kw ,
416+ )
417+ prefill_start = num_decode_tokens
418+ prefill_cu_seqlens_q = cu_seqlens_q [num_decodes :] - cu_seqlens_q [num_decodes ]
419+ _forward_modelopt (
420+ impl ,
421+ query = query [prefill_start : prefill_start + num_prefill_tokens ],
422+ block_table = block_table [num_decodes : num_decodes + num_prefills ],
423+ seq_lens = seq_lens [num_decodes : num_decodes + num_prefills ],
424+ cu_seqlens_q = prefill_cu_seqlens_q ,
425+ num_actual_tokens = num_prefill_tokens ,
426+ max_query_len = max_query_len ,
427+ output = output [prefill_start : prefill_start + num_prefill_tokens ],
428+ ** common_kw ,
429+ )
430+ return output
431+
432+
353433class ModelOptSparseAttentionImpl (FlashAttentionImpl ):
354434 """FlashAttention adapter for the compact ModelOpt Triton path."""
355435
@@ -396,18 +476,25 @@ def forward(
396476 if attn_metadata is None :
397477 return output .fill_ (0 )
398478
479+ native_result = None
480+
399481 def native_forward ():
400- return self ._forward_vllm_flash_attn (
401- layer ,
402- query ,
403- key ,
404- value ,
405- kv_cache ,
406- attn_metadata ,
407- output ,
408- output_scale ,
409- output_block_scale ,
410- )
482+ # Memoized: a split mixed batch may request the native dense result
483+ # for an inactive phase after it was already computed for the batch.
484+ nonlocal native_result
485+ if native_result is None :
486+ native_result = self ._forward_vllm_flash_attn (
487+ layer ,
488+ query ,
489+ key ,
490+ value ,
491+ kv_cache ,
492+ attn_metadata ,
493+ output ,
494+ output_scale ,
495+ output_block_scale ,
496+ )
497+ return native_result
411498
412499 resolved = _resolve_forward (
413500 self ,
@@ -421,26 +508,35 @@ def native_forward():
421508
422509 key_cache , value_cache = kv_cache .unbind (0 )
423510 is_decode_only = attn_metadata .max_query_len <= 1
424- return _forward_modelopt (
511+ common_kw = {
512+ "layer" : layer ,
513+ "key_cache" : key_cache ,
514+ "value_cache" : value_cache ,
515+ "max_seq_len" : attn_metadata .max_seq_len ,
516+ "is_causal" : getattr (attn_metadata , "causal" , not is_decode_only ),
517+ "p_qdq" : resolved .p_qdq ,
518+ "p_qdq_amax" : resolved .p_qdq_amax ,
519+ "v_qdq" : resolved .v_qdq ,
520+ "v_qdq_amax" : resolved .v_qdq_amax ,
521+ "quant_active" : resolved .quant_active ,
522+ "dense_fallback" : native_forward ,
523+ }
524+ # Split mixed decode+prefill batches so decode rows never fall into the
525+ # schedule-sensitive prefill skip-softmax path (see _dispatch_modelopt).
526+ return _dispatch_modelopt (
425527 self ,
426- layer = layer ,
427528 query = query ,
428- key_cache = key_cache ,
429- value_cache = value_cache ,
430529 block_table = attn_metadata .block_table ,
431530 seq_lens = attn_metadata .seq_lens ,
432531 cu_seqlens_q = attn_metadata .query_start_loc ,
433532 num_actual_tokens = attn_metadata .num_actual_tokens ,
434533 max_query_len = attn_metadata .max_query_len ,
435- max_seq_len = attn_metadata .max_seq_len ,
436- is_causal = getattr (attn_metadata , "causal" , not is_decode_only ),
437534 output = output ,
438- p_qdq = resolved .p_qdq ,
439- p_qdq_amax = resolved .p_qdq_amax ,
440- v_qdq = resolved .v_qdq ,
441- v_qdq_amax = resolved .v_qdq_amax ,
442- quant_active = resolved .quant_active ,
443- dense_fallback = native_forward ,
535+ num_decodes = getattr (attn_metadata , "num_decodes" , 0 ),
536+ num_prefills = getattr (attn_metadata , "num_prefills" , 0 ),
537+ num_decode_tokens = getattr (attn_metadata , "num_decode_tokens" , 0 ),
538+ num_prefill_tokens = getattr (attn_metadata , "num_prefill_tokens" , 0 ),
539+ ** common_kw ,
444540 )
445541
446542
@@ -616,62 +712,21 @@ def prepare_modelopt():
616712 "dense_fallback" : dense_fallback ,
617713 "prepare_modelopt" : prepare_modelopt ,
618714 }
619- num_decodes = getattr (attn_metadata , "num_decodes" , 0 )
620- num_prefills = getattr (attn_metadata , "num_prefills" , 0 )
621- if not (num_decodes and num_prefills ):
622- return _forward_modelopt (
623- impl ,
624- query = query ,
625- block_table = attn_metadata ._modelopt_block_table ,
626- seq_lens = attn_metadata ._modelopt_seq_lens ,
627- cu_seqlens_q = attn_metadata ._modelopt_query_start_loc ,
628- num_actual_tokens = attn_metadata ._modelopt_num_actual_tokens ,
629- max_query_len = max_query_len ,
630- output = output ,
631- ** common_kw ,
632- )
633-
634- num_decode_tokens = attn_metadata .num_decode_tokens
635- num_prefill_tokens = attn_metadata .num_prefill_tokens
636- if num_decode_tokens % num_decodes :
637- raise NotImplementedError ("Non-uniform mixed FlashInfer decode is unsupported" )
638- if num_decode_tokens + num_prefill_tokens != attn_metadata ._modelopt_num_actual_tokens :
639- raise ValueError ("FlashInfer mixed-batch token counts do not match common metadata" )
640-
641- # Sparse-only launches may have an inactive phase (for example N:M
642- # is prefill-only). Compute the native result once, then overwrite
643- # each active phase with its ModelOpt result.
644- if not resolved .quant_active :
645- dense_fallback ()
646-
647- block_table = attn_metadata ._modelopt_block_table
648- seq_lens = attn_metadata ._modelopt_seq_lens
649- cu_seqlens_q = attn_metadata ._modelopt_query_start_loc
650- _forward_modelopt (
715+ return _dispatch_modelopt (
651716 impl ,
652- query = query [:num_decode_tokens ],
653- block_table = block_table [:num_decodes ],
654- seq_lens = seq_lens [:num_decodes ],
655- cu_seqlens_q = cu_seqlens_q [: num_decodes + 1 ],
656- num_actual_tokens = num_decode_tokens ,
657- max_query_len = num_decode_tokens // num_decodes ,
658- output = output [:num_decode_tokens ],
659- ** common_kw ,
660- )
661- prefill_start = num_decode_tokens
662- prefill_cu_seqlens_q = cu_seqlens_q [num_decodes :] - cu_seqlens_q [num_decodes ]
663- _forward_modelopt (
664- impl ,
665- query = query [prefill_start : prefill_start + num_prefill_tokens ],
666- block_table = block_table [num_decodes : num_decodes + num_prefills ],
667- seq_lens = seq_lens [num_decodes : num_decodes + num_prefills ],
668- cu_seqlens_q = prefill_cu_seqlens_q ,
669- num_actual_tokens = num_prefill_tokens ,
717+ query = query ,
718+ block_table = attn_metadata ._modelopt_block_table ,
719+ seq_lens = attn_metadata ._modelopt_seq_lens ,
720+ cu_seqlens_q = attn_metadata ._modelopt_query_start_loc ,
721+ num_actual_tokens = attn_metadata ._modelopt_num_actual_tokens ,
670722 max_query_len = max_query_len ,
671- output = output [prefill_start : prefill_start + num_prefill_tokens ],
723+ output = output ,
724+ num_decodes = getattr (attn_metadata , "num_decodes" , 0 ),
725+ num_prefills = getattr (attn_metadata , "num_prefills" , 0 ),
726+ num_decode_tokens = getattr (attn_metadata , "num_decode_tokens" , 0 ),
727+ num_prefill_tokens = getattr (attn_metadata , "num_prefill_tokens" , 0 ),
672728 ** common_kw ,
673729 )
674- return output
675730
676731
677732def get_flashinfer_sparse_impl_cls () -> type :
0 commit comments