Skip to content

Commit 0eadd97

Browse files
Merge pull request #4051 from AI-Hypercomputer:mohit/gdn_support
PiperOrigin-RevId: 931217348
2 parents a8505ff + 7010c1a commit 0eadd97

11 files changed

Lines changed: 456 additions & 43 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@ logical_axis_rules: [
551551
['activation_stage', 'stage'],
552552
# General Weights
553553
['mlp', ['fsdp_transpose', 'tensor', 'tensor_sequence', 'autoregressive']],
554+
# GDN (linear-attention) projections shard like 'mlp' during training; the
555+
# vLLM serving config overrides this to match tpu-inference's ATTN_HEAD order.
556+
['gdn_head', ['fsdp_transpose', 'tensor', 'tensor_sequence', 'autoregressive']],
554557
['embed', ['fsdp', 'fsdp_transpose', 'tensor_transpose', 'context', 'expert']],
555558
['embed', ['fsdp', 'tensor_transpose', 'context', 'expert']],
556559
['embed', ['fsdp', 'fsdp_transpose', 'context', 'expert']],

src/maxtext/configs/inference/vllm.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ logical_axis_rules: [
3737
# Vocab Activations
3838
['activation_embed_and_logits_batch', ['data', 'attn_dp', 'attn_dp_expert']],
3939
['activation_embed_and_logits_batch_sequence', ['data', 'attn_dp', 'attn_dp_expert']],
40-
['activation_vocab', ['expert', 'model']],
40+
['activation_vocab', ['model', 'expert']],
4141
# Vocab Weights
4242
['vocab', []],
4343
['embed_vocab', []],
@@ -46,16 +46,17 @@ logical_axis_rules: [
4646
# ==========================================
4747
# Attention Activations
4848
['activation_batch_attn', ['data', 'attn_dp', 'attn_dp_expert']],
49-
['activation_heads', ['expert', 'model']],
50-
['activation_kv_heads', ['expert', 'model']],
49+
['activation_heads', ['model', 'expert']],
50+
['activation_kv_heads', ['model', 'expert']],
5151
['activation_embed_attn', []],
5252
['activation_kv', []],
5353
['activation_kv_batch', ['data', 'attn_dp', 'attn_dp_expert']],
5454
['activation_kv_head_dim', []],
5555
# Attention Weights
56-
['heads', ['expert', 'model']],
57-
['q_heads', ['expert', 'model']],
58-
['kv_heads', ['expert', 'model']],
56+
['heads', ['model', 'expert']],
57+
['gdn_head', ['model', 'expert']],
58+
['q_heads', ['model', 'expert']],
59+
['kv_heads', ['model', 'expert']],
5960
['qkv', []],
6061
['kv', []],
6162
['kv_head_dim', []],

src/maxtext/inference/vllm_decode.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from maxtext.utils import max_logging
4343
from maxtext.utils.globals import MAXTEXT_CONFIGS_DIR
4444
from maxtext.common.common_types import Config
45+
from maxtext.common import profiler
4546
from maxtext.integration.tunix.tunix_adapter import TunixMaxTextAdapter
4647
from tunix.rl.rollout import base_rollout
4748
from tunix.rl.rollout.vllm_rollout import VllmRollout
@@ -50,6 +51,13 @@
5051
from maxtext.configs import pyconfig
5152
import maxtext.integration.vllm.maxtext_vllm_adapter as adapter
5253

54+
# Force uses_mrope to False to disable 3D multimodal position IDs in text-only runs.
55+
# TODO(b/520142315): Class-level monkey patching is required here because ModelConfig.uses_mrope
56+
# is evaluated during the internal initialization of the LLM engine, making instance-level
57+
# configuration impossible. Check if a cleaner configuration option can be added in upstream vLLM.
58+
from vllm.config import ModelConfig
59+
60+
ModelConfig.uses_mrope = property(lambda _: False)
5361

5462
# --- DEFINE FLAGS GLOBALLY ---
5563
FLAGS = flags.FLAGS
@@ -110,6 +118,11 @@ def decode_with_vllm(config: Config) -> None:
110118
vllm_args["additional_config"]["sharding"]["sharding_strategy"]["expert_parallelism"] = config.ici_expert_parallelism
111119
vllm_args["enable_expert_parallel"] = enable_expert_parallel
112120

121+
if config.max_num_batched_tokens is not None:
122+
vllm_args["max_num_batched_tokens"] = config.max_num_batched_tokens
123+
if config.max_num_seqs is not None:
124+
vllm_args["max_num_seqs"] = config.max_num_seqs
125+
113126
max_logging.log(
114127
f"Initializing LLM with DP={config.ici_data_parallelism}, TP={config.ici_tensor_parallelism} "
115128
f"and EP={config.ici_expert_parallelism if enable_expert_parallel else 1}..."
@@ -157,7 +170,10 @@ def decode_with_vllm(config: Config) -> None:
157170
top_p=top_p,
158171
)
159172

173+
prof = profiler.Profiler(config)
174+
prof.activate()
160175
outputs = llm.generate(prompts, sampling_params)
176+
prof.deactivate()
161177

162178
# max_logging.log Outputs
163179
for output in outputs:

src/maxtext/integration/vllm/maxtext_vllm_adapter/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,11 @@ def register():
3030
"""
3131
logger.info("Registering MaxTextForCausalLM model with tpu_inference and vllm.")
3232
register_model("MaxTextForCausalLM", MaxTextForCausalLM)
33+
34+
# Dynamically apply KVCacheManager patch when registering the adapter
35+
# pylint: disable=import-outside-toplevel
36+
from .adapter import patch_kv_cache_manager
37+
38+
patch_kv_cache_manager()
39+
3340
logger.info("Successfully registered MaxTextForCausalLM model.")

src/maxtext/integration/vllm/maxtext_vllm_adapter/adapter.py

Lines changed: 190 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class AttentionMetadata:
4040

4141
from 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

4449
def 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.")

src/maxtext/layers/decoders.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,10 @@ def __call__(
11371137
layer_kwargs = {"layer_idx": lyr}
11381138
kv_cache = None
11391139
if kv_caches is not None:
1140+
# For all decoder blocks (including QWEN3_NEXT/QWEN3_5 with vLLM flat-list
1141+
# kv_caches), pass the per-layer cache directly. For hybrid attention+GDN
1142+
# models, kv_caches[lyr] is a regular attention cache for attention layers
1143+
# and a (conv_state, recurrent_state) paged-mamba tuple for GDN layers.
11401144
kv_cache = kv_caches[lyr]
11411145

11421146
if cfg.decoder_block == DecoderBlockType.GPT_OSS:

0 commit comments

Comments
 (0)