Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
fc4d96c
add eagle 3.1 support for vllm plugin
kliuae Jun 12, 2026
af0ad9a
format
kliuae Jun 12, 2026
3383617
format
kliuae Jun 12, 2026
3636951
format
kliuae Jun 12, 2026
b9bb066
format
kliuae Jun 12, 2026
f9f5929
Merge branch 'main'
kliuae Jun 17, 2026
f66cbc1
fix norm quant fusion consumer dtype inconsistency
kliuae Jun 17, 2026
e68e53a
add import fail warning
kliuae Jun 17, 2026
87382d9
put mla spec decode in cudagraph
kliuae Jun 24, 2026
52c6ffe
enable mla target mha draft
kliuae Jun 25, 2026
836ddb1
fix
kliuae Jun 25, 2026
2f70668
[MiniMax-M3] support sparse MHA serving in vLLM
Jun 25, 2026
1aa9b54
[MiniMax-M3] optimize sparse MHA vLLM output path
Jun 25, 2026
37358aa
[MiniMax-M3] initialize sparse MHA vLLM cache state
XiaobingSuper Jun 25, 2026
ca56978
[MiniMax-M3] register ATOM MXFP8 quant config
XiaobingSuper Jun 25, 2026
7fbf460
[MiniMax-M3] fix sparse MHA fp8 metadata alignment
XiaobingSuper Jun 26, 2026
56ff7fd
[MiniMax-M3] add vLLM attention correctness baseline
XiaobingSuper Jun 29, 2026
e21ea86
[MiniMax-M3] route vLLM attention through ATOM adapters
Jun 30, 2026
c14f222
[MiniMax-M3] clean up vLLM sparse attention adapter
Jun 30, 2026
e06fa99
[MiniMax-M3] stabilize vLLM graph attention path
Jun 30, 2026
80a868b
[MiniMax-M3] restore core files and stabilize sparse capture
Jun 30, 2026
a387c14
[MiniMax-M3] simplify vLLM attention adapter
Jun 30, 2026
cf30919
[MiniMax-M3] fix sparse fp8 KV cache scales
Jun 30, 2026
3c7261e
[MiniMax-M3] add vLLM recipe
Jun 30, 2026
6bc0bea
use glue ps for sparse attention
XiaobingSuper Jul 1, 2026
5ff1592
[MiniMax-M3] restore sparse attention kernel
Jul 1, 2026
36612ea
Merge branch 'main' into lirui/m3_vllm_0630
XiaobingSuper Jul 1, 2026
0ea3ec8
Merge branch 'kliuae/plugin_enable_eagle31_merge'
kliuae Jul 2, 2026
d6d5cb3
enable m3 eagle
kliuae Jul 3, 2026
73784c2
expose vlm eagle model chain
kliuae Jul 6, 2026
076e1b9
Merge branch 'main'
kliuae Jul 6, 2026
f39ef1e
Merge branch 'main'
kliuae Jul 6, 2026
b90fa27
Merge branch 'kliuae/plugin_enable_eagle31_merge_0706'
kliuae Jul 6, 2026
f398b7d
fix conflict
kliuae Jul 6, 2026
e763dbe
Merge branch 'main'
kliuae Jul 8, 2026
e661290
bump m3 cudagraph mode for spec decode
kliuae Jul 8, 2026
be2ea72
Merge branch 'main'
kliuae Jul 13, 2026
9e01fb0
Merge branch 'kliuae/plugin_enable_eagle31_merge'
kliuae Jul 13, 2026
8ca8d4c
black
kliuae Jul 20, 2026
e89e381
Merge branch 'kliuae/plugin_enable_eagle31_merge'
kliuae Jul 20, 2026
69f2bb1
Merge branch 'main'
kliuae Jul 23, 2026
4dadc9a
Merge branch 'kliuae/plugin_m3_eagle_merge_0724' into kliuae/plugin_m…
kliuae Jul 27, 2026
a28536e
black
kliuae Jul 27, 2026
1b01b39
ruff
kliuae Jul 27, 2026
4d0eba2
ruff
kliuae Jul 27, 2026
8fc8fe4
Merge branch 'main'
kliuae Jul 30, 2026
066bad4
revert llama
kliuae Jul 30, 2026
978b051
style: apply black formatting
zejunchen-zejun Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion atom/plugin/vllm/attention/backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import torch
from vllm.v1.attention.backend import MultipleOf
from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend

from atom.model_ops.minimax_m3.sparse_attn import SPARSE_BLOCK_SIZE
Expand Down Expand Up @@ -34,7 +35,19 @@ def get_name() -> str:

@staticmethod
def get_supported_kernel_block_sizes():
return [16]
# The AITER asm_pa kernel only ships a bf16/bf16 paged-attention variant
# for kernel block size 16, but the Triton paged-attention path reads
# block_size from the cache shape at runtime and handles any multiple of
# 16. AttentionForVllmMHA routes to the Triton path whenever the bf16 KV
# cache block size is not 16 (see layer_mha.use_triton_attn), so this
# backend genuinely supports any MultipleOf(16). Declaring it as such
# lets vLLM pick the kv-manager block size (e.g. 128) as the common
# kernel block size, so a layer using this backend (the Eagle3 draft)
# can share the uniform-type KV cache group with the block-128
# sparse/dense layers instead of forcing a singleton group or a
# "No common block size" failure. This matches native vLLM, whose draft
# attention also supports the model block size.
return [MultipleOf(16)]

@classmethod
def supports_block_size(cls, block_size: int | None) -> bool:
Expand Down
37 changes: 31 additions & 6 deletions atom/plugin/vllm/attention/layer_mha.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,19 @@ def rope_cache(
attn_metadata = attention_metadata
slot_mapping = attn_metadata.slot_mapping[: q.shape[0]]

use_triton_attn = self.sliding_window != -1 or self.head_dim != 128
# use_triton_attn = True
# The AITER asm paged-attention kernel only has a bf16/bf16 variant for
# kernel block size 16. When the KV cache uses a different block size
# (e.g. the Eagle3 draft sharing the model's block 128 so it can join
# vLLM's uniform-type per-layer-tensor KV cache group instead of
# collapsing to a singleton group), asm has no kernel and the worker
# crashes ("cannot get heuristic kernel"). The Triton path (insert +
# decode) is block-size agnostic, so route bf16 caches with a non-16
# block size through Triton.
use_triton_attn = (
self.sliding_window != -1
or self.head_dim != 128
or (not self.kv_cache_dtype.startswith("fp8") and block_size != 16)
)
self.use_triton_attn = use_triton_attn

if (
Expand Down Expand Up @@ -311,7 +322,11 @@ def rope_cache(
v_scale=v_scale,
)
elif use_triton_attn and self.rotary_emb is not None:
k_scale = v_scale = self.per_tensor_scale
# `per_tensor_scale` is only populated for fp8 KV caches (see
# forward_impl). For a bf16 cache (e.g. the Eagle3 draft routed here
# because its block size != 16) it is absent and unused, since
# apply_scale below is False for non-fp8 dtypes.
k_scale = v_scale = getattr(self, "per_tensor_scale", None)
self.per_token_quant = False
q, k, _k_cache, _v_cache = fused_qk_rope_reshape_and_cache(
q,
Expand Down Expand Up @@ -423,7 +438,9 @@ def paged_attention_triton(
query_group_size,
)
compute_type = (
torch.bfloat16 if self.kv_cache_dtype == "bf16" else aiter.dtypes.fp8
aiter.dtypes.fp8
if self.kv_cache_dtype.startswith("fp8")
else torch.bfloat16
)
exp_sums = torch.empty(intermediate_shape, dtype=torch.float32, device=q.device)
max_logits = torch.empty(
Expand Down Expand Up @@ -457,8 +474,8 @@ def paged_attention_triton(
context_partition_size=context_partition_size,
compute_type=compute_type,
q_scale=None,
k_scale=None if self.kv_cache_dtype == "bf16" else k_scale,
v_scale=None if self.kv_cache_dtype == "bf16" else v_scale,
k_scale=k_scale if self.kv_cache_dtype.startswith("fp8") else None,
v_scale=v_scale if self.kv_cache_dtype.startswith("fp8") else None,
exp_sums=exp_sums,
max_logits=max_logits,
temporary_output=temporary_output,
Expand Down Expand Up @@ -908,6 +925,14 @@ def get_kv_cache_spec(self, vllm_config):

assert self.attn_type == AttentionType.DECODER
block_size = vllm_config.cache_config.block_size
# `self.sliding_window` uses -1 (not None) as the "no sliding window"
# sentinel. Only emit a SlidingWindowSpec for a *real* window (> 0);
# otherwise emit FullAttentionSpec.
#
# As Eagle3 draft has no sliding window, it must be a FullAttentionSpec.
# MLAAttentionSpec (M3's sparse indexer cache) subclasses FullAttentionSpec,
# so a spec set of {full/sparse target, full draft} stays uniform-type and
# vLLM allocates a separate KV tensor per layer.
if self.sliding_window is not None and self.sliding_window > 0:
return SlidingWindowSpec(
block_size=block_size,
Expand Down
12 changes: 7 additions & 5 deletions atom/plugin/vllm/attention/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,11 +435,13 @@ class MinimaxM3SparseMetadata:


class MinimaxM3SparseAttentionMetadataBuilder(AttentionMetadataBuilder):
# Only uniform single-token decode is safe to capture. Prefill/mixed batches
# still use build(), where variable query lengths and CPU-side max reduction
# are allowed. The decode kernels consume per-step seq_lens/block_table from
# vLLM's fixed metadata buffers and keep their grids shape-constant.
_cudagraph_support = AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
# Uniform decode batches are safe to capture, including spec-decode verify
# (query_len == num_spec + 1): the decode index-topk and sparse-attn kernels
# thread MAX_Q with per-token causality (causal_len = seq_len - MAX_Q + tok +
# 1) and their grids depend only on shape constants, so a captured (batch,
# query_len) shape is fixed. Prefill/mixed batches still use build(), where
# variable query lengths and CPU-side max reduction are allowed.
_cudagraph_support = AttentionCGSupport.UNIFORM_BATCH
reorder_batch_threshold = 1

def __init__(
Expand Down
113 changes: 79 additions & 34 deletions atom/plugin/vllm/model_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,49 +541,94 @@ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
# Attributes whose writes on the outer model must propagate to the
# inner model so vLLM's weight-sharing reaches the forward path.
_WEIGHT_SHARED_ATTRS = frozenset({"embed_tokens", "embedding", "lm_head"})
# Attribute names under which ATOM models nest their inner backbone. Walked
# in order at each level to build the inner-model chain. To support a model
# that nests under a new name, add it here.
_INNER_MODEL_ATTRS = ("model", "language_model")

def _inner_model_chain(self, outer: nn.Module) -> list[nn.Module]:
"""`outer` followed by each nested backbone, deepest last.

ATOM models nest their language backbone at different names and depths:
- flat EAGLE3 draft (Eagle3LlamaModel): depth 0, attrs on `outer`
- MTP draft / text-only target: depth 1, under `.model`
- VL target (MiniMax-M3 ...TextOnly): depth 2,
`.language_model` then `.model`
Walking `_INNER_MODEL_ATTRS` at each level yields a single chain that
covers all of them, so a wanted attribute can be found wherever it lives.
"""
chain: list[nn.Module] = []
node: nn.Module | None = outer
seen: set[int] = set()
while node is not None and id(node) not in seen:
seen.add(id(node))
chain.append(node)
nxt = None
for name in self._INNER_MODEL_ATTRS:
cand = getattr(node, name, None)
if isinstance(cand, nn.Module) and id(cand) not in seen:
nxt = cand
break
node = nxt
return chain

def _expose_spec_decode_attrs(self) -> None:
"""Bridge the extra nesting level between vLLM and ATOM for spec decode.

ATOM wraps the HF model with one extra level:
vLLM sees: wrapper.model (DeepSeekMTP)
forward uses: .model (DeepSeekMultiTokenPredictor)

vLLM's EagleSpeculator reads/writes embed_tokens, lm_head, layers on
the outer model. The forward path reads them from the inner model.

We need two things:
1. Mirror inner → outer so vLLM can discover the attrs.
2. When vLLM later *replaces* embed_tokens / lm_head with shared
target-model weights, propagate the write to the inner model
so the forward path picks up the shared tensor.
vLLM reads embed_tokens / embedding / layers at ``wrapper.model.<attr>``
and the head at ``wrapper.lm_head``. ATOM nests these under one or more
backbone levels (see `_inner_model_chain`), so mirror the first holder
found in the chain onto the paths vLLM reads.

Draft vs target differ in one way:
- Draft/MTP: vLLM *replaces* embed_tokens / lm_head with shared target
weights, so register real submodules (`setattr`) and install a
`__setattr__` hook that propagates those writes down to the inner
module the forward path reads from.
- Target: vLLM only *reads* these attrs, so mirror them as plain aliases
(`object.__setattr__`) that stay invisible to the module tree and the
weight loader — no ownership change, no sync hook.
"""
model = self.model
inner = getattr(model, "model", None)
if inner is None:
if hasattr(model, "lm_head") and not hasattr(self, "lm_head"):
self.lm_head = model.lm_head
return
chain = self._inner_model_chain(model)
inner = chain[1] if len(chain) > 1 else None
is_draft = self.is_spec_draft_model
put = setattr if is_draft else object.__setattr__

def first_holder(attr: str) -> nn.Module | None:
for node in chain:
if hasattr(node, attr):
return node
return None

# ATOM DeepSeek-V4 names these shared modules `embed` / `head`, while
# vLLM's generic MTP proposer expects `embedding` / `lm_head`.
if not hasattr(model, "embedding") and hasattr(inner, "embed"):
model.embedding = inner.embed
if not hasattr(model, "lm_head") and hasattr(inner, "head"):
model.lm_head = inner.head
# ATOM DeepSeek-V4 names these shared modules `embed` / `head` on its
# immediate backbone child, while vLLM's generic MTP proposer expects
# `embedding` / `lm_head` on the outer model.
if inner is not None:
if not hasattr(model, "embedding") and hasattr(inner, "embed"):
put(model, "embedding", inner.embed)
if not hasattr(model, "lm_head") and hasattr(inner, "head"):
put(model, "lm_head", inner.head)

# (1) Mirror: make attrs visible on the outer model for vLLM discovery.
# (1) Mirror backbone attrs onto the outer model, and the head onto self.
for attr in (*self._WEIGHT_SHARED_ATTRS, "layers"):
if not hasattr(model, attr) and hasattr(inner, attr):
setattr(model, attr, getattr(inner, attr))

if not hasattr(self, "lm_head") and hasattr(model, "lm_head"):
self.lm_head = model.lm_head

# (2) Propagate: future writes on the outer model sync to the inner
# model. We create a one-off subclass so the hook only affects
# this particular draft-model instance, not the base class.
# Create the one-off subclass only once
if not hasattr(model, attr):
holder = first_holder(attr)
if holder is not None and holder is not model:
put(model, attr, getattr(holder, attr))

if not hasattr(self, "lm_head"):
holder = first_holder("lm_head")
if holder is not None:
put(self, "lm_head", holder.lm_head)

# (2) Draft only: propagate vLLM's later writes on the outer model down
# to the inner module the forward path reads from. Create the one-off
# subclass only once, and only when there is an inner level to sync.
if not is_draft:
return
if inner is None:
return
if getattr(model, "_atom_vllm_shared_attr_sync_patched", False):
return
shared = self._WEIGHT_SHARED_ATTRS
Expand Down
Loading