@@ -40,6 +40,11 @@ class AttentionMetadata:
4040
4141from vllm .config import VllmConfig
4242
43+ # Threshold to determine if the ratio of attention to mamba layers is highly imbalanced.
44+ # If max_count / min_count >= this threshold, we group KV cache allocations by the
45+ # smaller count to prevent excessive memory padding for the minority layer type.
46+ _HYBRID_LAYER_IMBALANCE_THRESHOLD = 1.5
47+
4348
4449def next_power_of_two (x : int ) -> int :
4550 """Finds the smallest power of 2 >= x using bit manipulation.
@@ -56,7 +61,7 @@ def next_power_of_two(x: int) -> int:
5661 return 1 << (x - 1 ).bit_length ()
5762
5863
59- def generate_maxtext_config (vllm_config : VllmConfig , mesh : Mesh ) -> pyconfig .HyperParameters :
64+ def generate_maxtext_config (vllm_config : VllmConfig ) -> pyconfig .HyperParameters :
6065 """Generates a MaxText configuration from a vLLM configuration.
6166
6267 This function takes a vLLM configuration object and translates relevant
@@ -67,7 +72,6 @@ def generate_maxtext_config(vllm_config: VllmConfig, mesh: Mesh) -> pyconfig.Hyp
6772 Args:
6873 vllm_config: The vLLM configuration object containing model and load
6974 parameters.
70- mesh: The JAX mesh device for model sharding.
7175
7276 Returns:
7377 A `pyconfig.HyperParameters` object configured for MaxText.
@@ -178,7 +182,7 @@ def __init__(self, vllm_config: VllmConfig, rng_key: jax.Array, mesh: Mesh):
178182 """
179183 self .vllm_config = vllm_config
180184 self .cfg = vllm_config .model_config
181- self .maxtext_config = generate_maxtext_config (vllm_config , mesh )
185+ self .maxtext_config = generate_maxtext_config (vllm_config )
182186
183187 # Model configuration
184188 self .mesh = mesh
@@ -228,6 +232,24 @@ def __call__(
228232 if not isinstance (self .model , nnx .Module ):
229233 raise ValueError ("Model must be an instance of type nnx.Module." )
230234
235+ # below, GDN layers don't touch block_tables — they index via
236+ # ``mamba_state_indices`` — and all full-attn layers belong to the same
237+ # kv_cache_group so they share one block_tables. Pick a metadata from a
238+ # full-attn (non-linear_attention) layer when possible; otherwise any
239+ # value works.
240+ if isinstance (attention_metadata , dict ):
241+ hf_text_config = getattr (self .cfg , "hf_text_config" , getattr (self .cfg , "hf_config" , None ))
242+ layer_types = getattr (hf_text_config , "layer_types" , None ) or []
243+ attention_metadata_picked = None
244+ for i , lt in enumerate (layer_types ):
245+ if lt != "linear_attention" :
246+ attention_metadata_picked = attention_metadata .get (f"layer.{ i } " )
247+ if attention_metadata_picked is not None :
248+ break
249+ if attention_metadata_picked is None :
250+ attention_metadata_picked = next (iter (attention_metadata .values ()))
251+ attention_metadata = attention_metadata_picked
252+
231253 # Ensure inputs are at least 2D with a batch dimension
232254 input_ids = jnp .expand_dims (input_ids , axis = 1 )
233255 input_positions = jnp .expand_dims (attention_metadata .input_positions , axis = 1 )
@@ -324,3 +346,168 @@ def load_weights(self, rng_key: jax.Array) -> None:
324346 self .maxtext_config , mesh = self .mesh , model_mode = self .model_mode , rng_key = rng_key
325347 )
326348 self .model = nnx .data (model )
349+
350+ def get_mrope_input_positions (
351+ self ,
352+ input_tokens : list [int ],
353+ mm_features : list = None ,
354+ ) -> tuple [jax .Array , int ]:
355+ """Get dummy mrope input positions and delta value for text-only MaxText."""
356+ seq_len = len (input_tokens )
357+ pos_range = jnp .arange (seq_len , dtype = jnp .int32 )
358+ # M-RoPE expects 3D position vectors (3, seq_len) and position_delta (int)
359+ positions = jnp .stack ([pos_range , pos_range , pos_range ], axis = 0 )
360+ return positions , 0
361+
362+
363+ # Monkey-patch KVCacheManager.get_kv_cache_spec to support GDN/Mamba layers in Pure JAX path.
364+ def patch_kv_cache_manager ():
365+ """Monkey-patches KVCacheManager to support hybrid Attention + GDN/Mamba models."""
366+ # pylint: disable=import-outside-toplevel,protected-access
367+ try :
368+ from tpu_inference .runner .kv_cache_manager import KVCacheManager
369+ from vllm .v1 .kv_cache_interface import MambaSpec
370+ import torch
371+ import numpy as np
372+ except ImportError as e :
373+ # Gracefully handle missing imports in standard JAX environments (e.g. unit tests on CPU)
374+ max_logging .log (f"Skipping KVCacheManager patch (tpu_inference or dependencies not installed): { e } " )
375+ return
376+
377+ try :
378+ original_get_kv_cache_spec = KVCacheManager .get_kv_cache_spec
379+ except AttributeError as e :
380+ # Raise a clear error if packages exist but patch target is missing (indicating API change or mismatch)
381+ raise RuntimeError (
382+ "Failed to apply KVCacheManager patch: KVCacheManager.get_kv_cache_spec not found. "
383+ "This usually indicates a vLLM / tpu-inference API change or version mismatch."
384+ ) from e
385+
386+ def patched_get_kv_cache_spec (self ):
387+ runner = self .runner
388+ if not hasattr (runner , "model" ):
389+ return original_get_kv_cache_spec (self )
390+
391+ model = runner .model
392+ if not hasattr (model , "maxtext_config" ):
393+ return original_get_kv_cache_spec (self )
394+
395+ cfg = model .maxtext_config
396+ decoder_block = getattr (cfg , "decoder_block" , "" )
397+
398+ decoder_block_str = ""
399+ if isinstance (decoder_block , str ):
400+ decoder_block_str = decoder_block
401+ elif hasattr (decoder_block , "value" ):
402+ decoder_block_str = decoder_block .value
403+
404+ if decoder_block_str in ("qwen3_next" , "qwen3_5" ):
405+ interval = cfg .inhomogeneous_layer_cycle_interval
406+
407+ num_v_heads = cfg .gdn_num_value_heads
408+ num_k_heads = cfg .gdn_num_key_heads
409+ head_k_dim = cfg .gdn_key_head_dim
410+ head_v_dim = cfg .gdn_value_head_dim
411+ conv_kernel_size = cfg .gdn_conv_kernel_dim
412+
413+ key_dim = head_k_dim * num_k_heads
414+ value_dim = head_v_dim * num_v_heads
415+ conv_dim = key_dim * 2 + value_dim
416+
417+ conv_state_shape = (conv_kernel_size - 1 , conv_dim )
418+ recurrent_state_shape = (num_v_heads , head_k_dim , head_v_dim )
419+
420+ mamba_shapes = (conv_state_shape , recurrent_state_shape )
421+
422+ torch_dtype = torch .bfloat16
423+ if str (cfg .dtype ) == "float32" :
424+ torch_dtype = torch .float32
425+ elif str (cfg .dtype ) == "float16" :
426+ torch_dtype = torch .float16
427+ mamba_dtypes = (torch_dtype , torch_dtype )
428+
429+ # Calculate unpadded mamba page size
430+ dtype_size = 4 if torch_dtype == torch .float32 else 2
431+ unpadded_mamba_page_size = sum (int (np .prod (shape )) * dtype_size for shape in mamba_shapes )
432+
433+ # Calculate attn_page_size_bytes
434+ from tpu_inference .layers .common .sharding import ShardingAxisName
435+ from tpu_inference import utils as common_utils
436+
437+ tp_axis_name = ShardingAxisName .ATTN_HEAD
438+ model_cnt = common_utils .get_mesh_shape_product (self .runner .mesh , tp_axis_name )
439+
440+ model_config = self .runner .model_config
441+ text_config = getattr (model_config , "hf_text_config" , getattr (model_config , "hf_config" , None ))
442+ base_num_kv_heads = model_config .get_total_num_kv_heads ()
443+ base_head_size = model_config .get_head_size ()
444+
445+ num_kv_heads = getattr (text_config , "num_global_key_value_heads" , None ) or base_num_kv_heads
446+ head_size = getattr (text_config , "global_head_dim" , None ) or base_head_size
447+
448+ num_kv_heads = common_utils .get_padded_num_heads (num_kv_heads , model_cnt )
449+ head_size = common_utils .get_padded_head_dim (head_size )
450+
451+ from tpu_inference .runner .kv_cache import get_attention_page_size_bytes
452+
453+ block_size = self .runner .cache_config .block_size
454+
455+ attn_page_size_bytes = get_attention_page_size_bytes (
456+ self .runner .mesh , block_size , num_kv_heads , head_size , self .runner .kv_cache_dtype , False
457+ )
458+
459+ # Calculate groups
460+ num_layers = cfg .base_num_decoder_layers
461+ num_attn = num_layers // interval
462+ num_mamba = num_layers - num_attn
463+
464+ # To allocate memory uniformly for a hybrid model's KV/recurrent cache page table,
465+ # we group layers together. The uniform page size must support both attention and
466+ # mamba layers.
467+ # If the ratio of attention to mamba layers is relatively balanced (less than _HYBRID_LAYER_IMBALANCE_THRESHOLD),
468+ # we use the larger count as the group size to minimize the total number of groups.
469+ # If they are highly imbalanced (>= _HYBRID_LAYER_IMBALANCE_THRESHOLD), we group by the smaller count to prevent
470+ # the page size from being inflated by excessive padding for the minority layer type.
471+ min_count = min (num_attn , num_mamba )
472+ max_count = max (num_attn , num_mamba )
473+ if max_count < min_count * _HYBRID_LAYER_IMBALANCE_THRESHOLD :
474+ group_size = max_count
475+ else :
476+ group_size = min_count
477+ num_attn_groups = (num_attn + group_size - 1 ) // group_size
478+ num_mamba_groups = (num_mamba + group_size - 1 ) // group_size
479+
480+ uniform_page_size_bytes = num_attn_groups * attn_page_size_bytes + num_mamba_groups * unpadded_mamba_page_size
481+
482+ # Set the padded page size on manager and config
483+ self ._hybrid_uniform_page_size_bytes = int (uniform_page_size_bytes )
484+ self .runner .cache_config .mamba_page_size_padded = int (uniform_page_size_bytes )
485+
486+ self ._maybe_set_compact_mamba_num_blocks_override (
487+ attn_page_size_bytes ,
488+ int (unpadded_mamba_page_size ),
489+ num_attn_groups ,
490+ num_mamba_groups ,
491+ num_attn ,
492+ num_mamba ,
493+ group_size ,
494+ )
495+
496+ kv_cache_spec = original_get_kv_cache_spec (self )
497+
498+ if decoder_block_str in ("qwen3_next" , "qwen3_5" ):
499+ for i in range (cfg .base_num_decoder_layers ):
500+ if (i + 1 ) % interval != 0 :
501+ layer_name = f"layer.{ i } "
502+ if layer_name in kv_cache_spec :
503+ kv_cache_spec [layer_name ] = MambaSpec (
504+ block_size = kv_cache_spec [layer_name ].block_size ,
505+ shapes = mamba_shapes ,
506+ dtypes = mamba_dtypes ,
507+ page_size_padded = self ._hybrid_uniform_page_size_bytes ,
508+ )
509+
510+ return kv_cache_spec
511+
512+ KVCacheManager .get_kv_cache_spec = patched_get_kv_cache_spec
513+ max_logging .log ("Successfully applied KVCacheManager patch for hybrid GDN models." )
0 commit comments