[Feature] Support MTP speculative decoding for Bailing hybrid models (#44880)

Signed-off-by: zc02384840 <zc02384840@antgroup.com> Co-authored-by: zc02384840 <zc02384840@antgroup.com>

alex101-ops committed Jul 6, 2026 at 10:38 UTC f2aaf5915102cd56b3a60f8e6e59c4a7f31268dd
11 files changed +1200 -6
tests/config/test_bailing_mtp_config.py new
+52
@@ -0,0 +1,52 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from transformers import PretrainedConfig
5 +
6 +from vllm.config.speculative import MTPModelTypes, SpeculativeConfig
7 +from vllm.transformers_utils.model_arch_config_convertor import (
8 + BailingHybridMTPModelArchConfigConvertor,
9 +)
10 +
11 +
12 +def _bailing_config() -> PretrainedConfig:
13 + config = PretrainedConfig(
14 + architectures=["BailingMoeV2_5ForCausalLM"],
15 + hidden_size=4096,
16 + kv_lora_rank=512,
17 + num_attention_heads=32,
18 + num_experts=256,
19 + num_hidden_layers=32,
20 + num_key_value_heads=32,
21 + num_nextn_predict_layers=1,
22 + qk_rope_head_dim=64,
23 + vocab_size=157184,
24 + )
25 + config.model_type = "bailing_hybrid"
26 + return config
27 +
28 +
29 +def test_bailing_hybrid_mtp_hf_config_override():
30 + config = _bailing_config()
31 +
32 + overridden = SpeculativeConfig.hf_config_override(config)
33 +
34 + assert overridden.model_type == "bailing_hybrid_mtp"
35 + assert overridden.architectures == ["BailingMoeV25MTPModel"]
36 + assert overridden.n_predict == 1
37 + assert "bailing_hybrid_mtp" in MTPModelTypes.__args__
38 +
39 +
40 +def test_bailing_hybrid_mtp_model_arch_config():
41 + config = _bailing_config()
42 + config.model_type = "bailing_hybrid_mtp"
43 + config.architectures = ["BailingMoeV25MTPModel"]
44 +
45 + model_arch_config = BailingHybridMTPModelArchConfigConvertor(
46 + config, config
47 + ).convert()
48 +
49 + assert model_arch_config.model_type == "bailing_hybrid_mtp"
50 + assert model_arch_config.architectures == ["BailingMoeV25MTPModel"]
51 + assert model_arch_config.total_num_hidden_layers == 1
52 + assert model_arch_config.is_deepseek_mla
tests/models/registry.py
+6
@@ -1557,6 +1557,12 @@ _SPECULATIVE_DECODING_EXAMPLE_MODELS = {
1557 use_original_num_layers=True,
1558 ),
1559 # [MTP]
1560 + "BailingMoeV25MTPModel": _HfExamplesInfo(
1561 + "inclusionAI/Ring-2.5-1T",
1562 + speculative_model="inclusionAI/Ring-2.5-1T",
1563 + trust_remote_code=True,
1564 + is_available_online=False,
1565 + ),
1566 "DeepSeekMTPModel": _HfExamplesInfo(
1567 "luccafong/deepseek_mtp_main_random",
1568 speculative_model="luccafong/deepseek_mtp_draft_random",
tests/v1/attention/test_linear_attention_metadata_builder.py new
+188
@@ -0,0 +1,188 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import torch
5 +
6 +from tests.v1.attention.utils import (
7 + BatchSpec,
8 + create_common_attn_metadata,
9 + create_vllm_config,
10 +)
11 +from vllm.config import CUDAGraphMode, SpeculativeConfig
12 +from vllm.v1.attention.backend import AttentionCGSupport
13 +from vllm.v1.attention.backends.linear_attn import (
14 + BailingLinearAttentionMetadataBuilder,
15 + LinearAttentionMetadataBuilder,
16 +)
17 +from vllm.v1.attention.backends.utils import PAD_SLOT_ID
18 +from vllm.v1.kv_cache_interface import MambaSpec
19 +
20 +BLOCK_SIZE = 16
21 +DEVICE = torch.device("cpu")
22 +
23 +
24 +def _create_mamba_spec(num_speculative_blocks: int = 1) -> MambaSpec:
25 + return MambaSpec(
26 + block_size=BLOCK_SIZE,
27 + shapes=((16, 64),),
28 + dtypes=(torch.float16,),
29 + num_speculative_blocks=num_speculative_blocks,
30 + )
31 +
32 +
33 +def test_bailing_linear_attention_reports_uniform_batch_cudagraph_support():
34 + vllm_config = create_vllm_config(
35 + hf_config_override={
36 + "architectures": ["BailingMoeV2_5ForCausalLM"],
37 + "model_type": "bailing_hybrid",
38 + }
39 + )
40 +
41 + support = BailingLinearAttentionMetadataBuilder.get_cudagraph_support(
42 + vllm_config, _create_mamba_spec()
43 + )
44 +
45 + assert support == AttentionCGSupport.UNIFORM_BATCH
46 +
47 +
48 +def test_non_bailing_linear_attention_keeps_single_token_cudagraph_support():
49 + vllm_config = create_vllm_config(
50 + hf_config_override={
51 + "architectures": ["MiniMaxText01ForCausalLM"],
52 + "model_type": "minimax_text_01",
53 + }
54 + )
55 +
56 + support = LinearAttentionMetadataBuilder.get_cudagraph_support(
57 + vllm_config, _create_mamba_spec()
58 + )
59 +
60 + assert support == AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
61 +
62 +
63 +def test_linear_attention_spec_decode_full_graph_metadata_pads_cache_slots():
64 + vllm_config = create_vllm_config(
65 + hf_config_override={
66 + "architectures": ["BailingMoeV2_5ForCausalLM"],
67 + "model_type": "bailing_hybrid",
68 + }
69 + )
70 + vllm_config.speculative_config = SpeculativeConfig(
71 + method="ngram",
72 + num_speculative_tokens=1,
73 + )
74 + vllm_config.compilation_config.cudagraph_mode = CUDAGraphMode.FULL_DECODE_ONLY
75 +
76 + builder = BailingLinearAttentionMetadataBuilder(
77 + kv_cache_spec=_create_mamba_spec(),
78 + layer_names=["model.layers.0.self_attn"],
79 + vllm_config=vllm_config,
80 + device=DEVICE,
81 + )
82 +
83 + common = create_common_attn_metadata(
84 + BatchSpec(seq_lens=[20, 20, 0], query_lens=[2, 2, 0]),
85 + BLOCK_SIZE,
86 + DEVICE,
87 + )
88 + common.block_table_tensor[2].fill_(-1)
89 +
90 + metadata = builder.build(
91 + common_prefix_len=0,
92 + common_attn_metadata=common,
93 + num_accepted_tokens=torch.tensor([1, 2, 1], dtype=torch.int32),
94 + )
95 +
96 + assert metadata.num_decodes == 3
97 + assert metadata.num_prefills == 0
98 + assert metadata.num_decode_tokens == 4
99 + assert metadata.state_indices_tensor_d is not None
100 + assert metadata.state_indices_tensor_d.shape == (3, 2)
101 + assert torch.equal(
102 + metadata.state_indices_tensor_d[2],
103 + torch.full((2,), PAD_SLOT_ID, dtype=torch.int32),
104 + )
105 + assert torch.equal(
106 + metadata.state_indices_tensor[2],
107 + torch.tensor(PAD_SLOT_ID, dtype=torch.int32),
108 + )
109 + assert metadata.query_start_loc_d is not None
110 + assert metadata.query_start_loc_d.tolist() == [0, 2, 4, 4]
111 + assert metadata.num_accepted_tokens is not None
112 + assert metadata.num_accepted_tokens.tolist() == [1, 2, 1]
113 +
114 +
115 +def test_linear_attention_full_graph_metadata_uses_stable_decode_buffers():
116 + vllm_config = create_vllm_config(
117 + hf_config_override={
118 + "architectures": ["BailingMoeV2_5ForCausalLM"],
119 + "model_type": "bailing_hybrid",
120 + }
121 + )
122 + vllm_config.speculative_config = SpeculativeConfig(
123 + method="ngram",
124 + num_speculative_tokens=1,
125 + )
126 + vllm_config.compilation_config.cudagraph_mode = CUDAGraphMode.FULL_DECODE_ONLY
127 +
128 + builder = BailingLinearAttentionMetadataBuilder(
129 + kv_cache_spec=_create_mamba_spec(),
130 + layer_names=["model.layers.0.self_attn"],
131 + vllm_config=vllm_config,
132 + device=DEVICE,
133 + )
134 +
135 + common = create_common_attn_metadata(
136 + BatchSpec(seq_lens=[20, 20, 0], query_lens=[2, 2, 0]),
137 + BLOCK_SIZE,
138 + DEVICE,
139 + arange_block_indices=True,
140 + )
141 + common.block_table_tensor = torch.tensor(
142 + [[10, 11], [12, 13], [-1, -1]],
143 + dtype=torch.int32,
144 + device=DEVICE,
145 + )
146 +
147 + first = builder.build(
148 + common_prefix_len=0,
149 + common_attn_metadata=common,
150 + num_accepted_tokens=torch.tensor([1, 2, 1], dtype=torch.int32),
151 + )
152 + assert first.state_indices_tensor_d is not None
153 + assert first.query_start_loc_d is not None
154 + assert first.num_accepted_tokens is not None
155 + state_ptr = first.state_indices_tensor_d.data_ptr()
156 + query_ptr = first.query_start_loc_d.data_ptr()
157 + accepted_ptr = first.num_accepted_tokens.data_ptr()
158 +
159 + common2 = create_common_attn_metadata(
160 + BatchSpec(seq_lens=[36, 0, 0], query_lens=[2, 0, 0]),
161 + BLOCK_SIZE,
162 + DEVICE,
163 + arange_block_indices=True,
164 + )
165 + common2.block_table_tensor = torch.tensor(
166 + [[20, 21], [-1, -1], [-1, -1]],
167 + dtype=torch.int32,
168 + device=DEVICE,
169 + )
170 + second = builder.build(
171 + common_prefix_len=0,
172 + common_attn_metadata=common2,
173 + num_accepted_tokens=torch.tensor([2, 1, 1], dtype=torch.int32),
174 + )
175 +
176 + assert second.state_indices_tensor_d is not None
177 + assert second.query_start_loc_d is not None
178 + assert second.num_accepted_tokens is not None
179 + assert second.state_indices_tensor_d.data_ptr() == state_ptr
180 + assert second.query_start_loc_d.data_ptr() == query_ptr
181 + assert second.num_accepted_tokens.data_ptr() == accepted_ptr
182 + assert second.state_indices_tensor_d.tolist() == [
183 + [20, 21],
184 + [PAD_SLOT_ID, PAD_SLOT_ID],
185 + [PAD_SLOT_ID, PAD_SLOT_ID],
186 + ]
187 + assert second.query_start_loc_d.tolist() == [0, 2, 2, 2]
188 + assert second.num_accepted_tokens.tolist() == [2, 1, 1]
vllm/config/speculative.py
+16
@@ -46,6 +46,7 @@ MTPModelTypes = Literal[
46 "qwen3_5_mtp",
47 "longcat_flash_mtp",
48 "minimax_m3_mtp",
49 + "bailing_hybrid_mtp",
50 "mtp",
51 "pangu_ultra_moe_mtp",
52 "step3p5_mtp",
@@ -463,6 +464,21 @@ class SpeculativeConfig:
464 {"n_predict": n_predict, "architectures": ["Qwen3NextMTP"]}
465 )
466
467 + architectures = getattr(hf_config, "architectures", []) or []
468 + if (
469 + hf_config.model_type == "bailing_hybrid"
470 + or "BailingMoeV2_5ForCausalLM" in architectures
471 + ):
472 + hf_config.model_type = "bailing_hybrid_mtp"
473 + if hf_config.model_type == "bailing_hybrid_mtp":
474 + n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
475 + hf_config.update(
476 + {
477 + "n_predict": n_predict,
478 + "architectures": ["BailingMoeV25MTPModel"],
479 + }
480 + )
481 +
482 if hf_config.model_type == "exaone_moe":
483 hf_config.model_type = "exaone_moe_mtp"
484 if hf_config.model_type == "exaone_moe_mtp":
vllm/model_executor/layers/mamba/linear/bailing_linear_attn.py
+322 -3
@@ -30,11 +30,10 @@ from vllm.model_executor.layers.mamba.linear.base import LinearAttention
30 from vllm.model_executor.layers.mamba.linear.minimax_linear_attn import (
31 MiniMaxText01LinearAttention,
32 MiniMaxText01LinearKernel,
33 - clear_linear_attention_cache_for_new_sequences,
33 linear_attention_decode,
35 - linear_attention_prefill_and_mix,
34 )
35 from vllm.model_executor.layers.rotary_embedding import get_rope
36 +from vllm.triton_utils import tl, triton
37 from vllm.v1.attention.backends.linear_attn import LinearAttentionMetadata
38
39
@@ -57,6 +56,290 @@ def _build_rope_parameters(config: PretrainedConfig) -> dict | None:
56 return rope_parameters or None
57
58
59 +def clear_linear_attention_cache_for_new_sequences(
60 + kv_cache: torch.Tensor,
61 + state_indices_tensor: torch.Tensor,
62 + attn_metadata: LinearAttentionMetadata,
63 +) -> None:
64 + num_prefills = getattr(attn_metadata, "num_prefills", 0)
65 + if num_prefills <= 0:
66 + return
67 +
68 + num_decodes = getattr(attn_metadata, "num_decodes", 0)
69 + prefill_state_indices = getattr(attn_metadata, "state_indices_tensor_p", None)
70 + for prefill_idx in range(num_prefills):
71 + if num_decodes + prefill_idx + 1 >= len(attn_metadata.query_start_loc):
72 + break
73 + q_start = attn_metadata.query_start_loc[num_decodes + prefill_idx]
74 + q_end = attn_metadata.query_start_loc[num_decodes + prefill_idx + 1]
75 + query_len = q_end - q_start
76 + context_len = attn_metadata.seq_lens[num_decodes + prefill_idx] - query_len
77 + if context_len == 0:
78 + if prefill_state_indices is not None:
79 + block_to_clear = prefill_state_indices[prefill_idx]
80 + else:
81 + block_to_clear = state_indices_tensor[num_decodes + prefill_idx]
82 + kv_cache[block_to_clear, ...] = 0
83 +
84 +
85 +def linear_attention_prefill_and_mix(
86 + q: torch.Tensor,
87 + k: torch.Tensor,
88 + v: torch.Tensor,
89 + kv_cache: torch.Tensor,
90 + state_indices_tensor: torch.Tensor,
91 + attn_metadata: LinearAttentionMetadata,
92 + slope_rate: torch.Tensor,
93 + block_size: int,
94 + decode_fn,
95 + prefix_fn,
96 + layer_idx: int | None = None,
97 +) -> torch.Tensor:
98 + hidden = []
99 + req_offset = getattr(attn_metadata, "num_decodes", 0)
100 + prefill_state_indices = getattr(attn_metadata, "state_indices_tensor_p", None)
101 + for _prefill_idx in range(getattr(attn_metadata, "num_prefills", 0)):
102 + if req_offset + _prefill_idx + 1 >= len(attn_metadata.query_start_loc):
103 + break
104 + if prefill_state_indices is not None and _prefill_idx >= len(
105 + prefill_state_indices
106 + ):
107 + break
108 + if prefill_state_indices is None and _prefill_idx >= len(state_indices_tensor):
109 + break
110 + _start = attn_metadata.query_start_loc[req_offset + _prefill_idx]
111 + _end = attn_metadata.query_start_loc[req_offset + _prefill_idx + 1]
112 + if prefill_state_indices is not None:
113 + slot_id = prefill_state_indices[_prefill_idx]
114 + else:
115 + slot_id = state_indices_tensor[req_offset + _prefill_idx]
116 + qs = q[_start:_end].transpose(0, 1).contiguous()
117 + ks = k[_start:_end].transpose(0, 1).contiguous()
118 + vs = v[_start:_end].transpose(0, 1).contiguous()
119 + slice_layer_cache = kv_cache[slot_id, ...]
120 + out_slice = prefix_fn(
121 + qs,
122 + ks,
123 + vs,
124 + slice_layer_cache,
125 + slope_rate,
126 + block_size,
127 + layer_idx=layer_idx,
128 + )
129 + hidden.append(out_slice.contiguous())
130 +
131 + if attn_metadata.num_decode_tokens > 0:
132 + hidden_decode = decode_fn(
133 + q, k, v, kv_cache, state_indices_tensor, attn_metadata
134 + )
135 + hidden.insert(0, hidden_decode)
136 +
137 + if not hidden:
138 + return torch.empty((0, q.size(1) * q.size(2)), device=q.device, dtype=q.dtype)
139 +
140 + hidden = torch.concat(hidden, dim=0).contiguous()
141 + return hidden
142 +
143 +
144 +@triton.jit
145 +def _bailing_linear_attn_decode_spec_step_kernel(
146 + q_ptr,
147 + k_ptr,
148 + v_ptr,
149 + kv_cache_ptr,
150 + slope_rate_ptr,
151 + state_indices_ptr,
152 + query_start_loc_ptr,
153 + num_accepted_tokens_ptr,
154 + output_ptr,
155 + q_start: tl.constexpr,
156 + D: tl.constexpr,
157 + q_b_stride,
158 + q_h_stride,
159 + q_d_stride,
160 + k_b_stride,
161 + k_h_stride,
162 + k_d_stride,
163 + v_b_stride,
164 + v_h_stride,
165 + v_d_stride,
166 + cache_b_stride,
167 + cache_h_stride,
168 + cache_d0_stride,
169 + cache_d1_stride,
170 + state_indices_b_stride,
171 + state_indices_t_stride,
172 + output_b_stride,
173 + output_d_stride,
174 + DRAFT_IDX: tl.constexpr,
175 + STATE_WIDTH: tl.constexpr,
176 + BLOCK_SIZE: tl.constexpr,
177 +):
178 + req_id = tl.program_id(0)
179 + head_id = tl.program_id(1)
180 + block_id = tl.program_id(2)
181 +
182 + req_start = tl.load(query_start_loc_ptr + req_id).to(tl.int64)
183 + req_end = tl.load(query_start_loc_ptr + req_id + 1).to(tl.int64)
184 + query_len = req_end - req_start
185 + if query_len <= DRAFT_IDX:
186 + return
187 +
188 + dst_slot = tl.load(
189 + state_indices_ptr
190 + + req_id * state_indices_b_stride
191 + + DRAFT_IDX * state_indices_t_stride
192 + ).to(tl.int64)
193 + if dst_slot == -1:
194 + return
195 +
196 + if DRAFT_IDX == 0:
197 + accepted_offset = tl.load(num_accepted_tokens_ptr + req_id).to(tl.int64) - 1
198 + accepted_offset = tl.maximum(accepted_offset, 0)
199 + accepted_offset = tl.minimum(accepted_offset, STATE_WIDTH - 1)
200 + src_slot = tl.load(
201 + state_indices_ptr
202 + + req_id * state_indices_b_stride
203 + + accepted_offset * state_indices_t_stride
204 + ).to(tl.int64)
205 + else:
206 + src_slot = tl.load(
207 + state_indices_ptr
208 + + req_id * state_indices_b_stride
209 + + (DRAFT_IDX - 1) * state_indices_t_stride
210 + ).to(tl.int64)
211 + if src_slot == -1:
212 + return
213 +
214 + token_idx = req_start - q_start + DRAFT_IDX
215 + qk_offsets = tl.arange(0, D)
216 + v_offsets = tl.arange(0, BLOCK_SIZE) + block_id * BLOCK_SIZE
217 + qk_mask = qk_offsets < D
218 + v_mask = v_offsets < D
219 + kv_mask = qk_mask[:, None] & v_mask[None, :]
220 +
221 + q = tl.load(
222 + q_ptr + token_idx * q_b_stride + head_id * q_h_stride + qk_offsets * q_d_stride,
223 + mask=qk_mask,
224 + other=0.0,
225 + )
226 + k = tl.load(
227 + k_ptr + token_idx * k_b_stride + head_id * k_h_stride + qk_offsets * k_d_stride,
228 + mask=qk_mask,
229 + other=0.0,
230 + )
231 + v = tl.load(
232 + v_ptr + token_idx * v_b_stride + head_id * v_h_stride + v_offsets * v_d_stride,
233 + mask=v_mask,
234 + other=0.0,
235 + )
236 +
237 + cache_offsets = (
238 + qk_offsets[:, None] * cache_d0_stride + v_offsets[None, :] * cache_d1_stride
239 + )
240 + src_cache_ptr = (
241 + kv_cache_ptr
242 + + src_slot * cache_b_stride
243 + + head_id * cache_h_stride
244 + + cache_offsets
245 + )
246 + dst_cache_ptr = (
247 + kv_cache_ptr
248 + + dst_slot * cache_b_stride
249 + + head_id * cache_h_stride
250 + + cache_offsets
251 + )
252 +
253 + slope = tl.load(slope_rate_ptr + head_id)
254 + decay = tl.exp(-slope)
255 + kv_old = tl.load(src_cache_ptr, mask=kv_mask, other=0.0)
256 + kv_new = k[:, None] * v[None, :] + decay * kv_old
257 +
258 + output = tl.sum(q[:, None].to(tl.float32) * kv_new, axis=0)
259 + tl.store(dst_cache_ptr, kv_new, mask=kv_mask)
260 + tl.store(
261 + output_ptr
262 + + token_idx * output_b_stride
263 + + (head_id * D + v_offsets) * output_d_stride,
264 + output,
265 + mask=v_mask,
266 + )
267 +
268 +
269 +def bailing_linear_attention_decode_spec(
270 + q: torch.Tensor,
271 + k: torch.Tensor,
272 + v: torch.Tensor,
273 + kv_cache: torch.Tensor,
274 + slope_rate: torch.Tensor,
275 + state_indices_tensor: torch.Tensor,
276 + query_start_loc: torch.Tensor,
277 + num_accepted_tokens: torch.Tensor,
278 + q_start: int,
279 + q_end: int | None,
280 + slot_start: int,
281 + slot_end: int | None,
282 + block_size: int,
283 +) -> torch.Tensor:
284 + q_decode = q[q_start:q_end]
285 + k_decode = k[q_start:q_end]
286 + v_decode = v[q_start:q_end]
287 + hidden = torch.empty(
288 + (q_decode.shape[0], q.shape[1] * q.shape[2]),
289 + device=q.device,
290 + dtype=q.dtype,
291 + )
292 + hidden.zero_()
293 +
294 + state_indices_tensor = state_indices_tensor[slot_start:slot_end]
295 + query_start_loc = query_start_loc.to(device=q.device)
296 +
297 + batch_size = state_indices_tensor.shape[0]
298 + num_heads = q_decode.shape[1]
299 + head_dim = q_decode.shape[2]
300 + assert k_decode.shape == (q_decode.shape[0], num_heads, head_dim)
301 + assert v_decode.shape == (q_decode.shape[0], num_heads, head_dim)
302 + state_width = state_indices_tensor.shape[1]
303 +
304 + grid = (batch_size, num_heads, triton.cdiv(head_dim, block_size))
305 + for draft_idx in range(state_width):
306 + _bailing_linear_attn_decode_spec_step_kernel[grid](
307 + q_decode,
308 + k_decode,
309 + v_decode,
310 + kv_cache,
311 + slope_rate,
312 + state_indices_tensor,
313 + query_start_loc,
314 + num_accepted_tokens[:batch_size],
315 + hidden,
316 + q_start,
317 + head_dim,
318 + q_decode.stride(0),
319 + q_decode.stride(1),
320 + q_decode.stride(2),
321 + k_decode.stride(0),
322 + k_decode.stride(1),
323 + k_decode.stride(2),
324 + v_decode.stride(0),
325 + v_decode.stride(1),
326 + v_decode.stride(2),
327 + kv_cache.stride(0),
328 + kv_cache.stride(1),
329 + kv_cache.stride(2),
330 + kv_cache.stride(3),
331 + state_indices_tensor.stride(0),
332 + state_indices_tensor.stride(1),
333 + hidden.stride(0),
334 + hidden.stride(1),
335 + DRAFT_IDX=draft_idx,
336 + STATE_WIDTH=state_width,
337 + BLOCK_SIZE=block_size,
338 + )
339 +
340 + return hidden
341 +
342 +
343 class BailingGroupRMSNormGate(RMSNormGated):
344 def __init__(
345 self,
@@ -208,6 +491,13 @@ class BailingMoELinearAttention(LinearAttention):
491 raise ValueError(f"Duplicate layer name: {prefix}")
492 compilation_config.static_forward_context[prefix] = self
493
494 + def get_attn_backend(self):
495 + from vllm.v1.attention.backends.linear_attn import (
496 + BailingLinearAttentionBackend,
497 + )
498 +
499 + return BailingLinearAttentionBackend
500 +
501 @staticmethod
502 def weight_direct_load(param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
503 """Load weight for linear attention layers.
@@ -368,13 +658,42 @@ class BailingMoELinearAttention(LinearAttention):
658
659 def _decode_infer(self, q, k, v, kv_cache, state_indices_tensor, attn_metadata):
660 """Handle decode (single token per sequence)."""
661 + decode_state_indices = getattr(attn_metadata, "state_indices_tensor_d", None)
662 + num_accepted_tokens = getattr(attn_metadata, "num_accepted_tokens", None)
663 + query_start_loc = getattr(attn_metadata, "query_start_loc_d", None)
664 + if (
665 + decode_state_indices is not None
666 + and decode_state_indices.dim() > 1
667 + and num_accepted_tokens is not None
668 + and query_start_loc is not None
669 + ):
670 + return bailing_linear_attention_decode_spec(
671 + q,
672 + k,
673 + v,
674 + kv_cache,
675 + self.tp_slope,
676 + decode_state_indices,
677 + query_start_loc,
678 + num_accepted_tokens,
679 + q_start=0,
680 + q_end=attn_metadata.num_decode_tokens,
681 + slot_start=0,
682 + slot_end=attn_metadata.num_decodes,
683 + block_size=32,
684 + )
685 + decode_state_indices = (
686 + state_indices_tensor
687 + if decode_state_indices is None
688 + else decode_state_indices
689 + )
690 hidden = linear_attention_decode(
691 q,
692 k,
693 v,
694 kv_cache,
695 self.tp_slope,
377 - state_indices_tensor,
696 + decode_state_indices,
697 q_start=0,
698 q_end=attn_metadata.num_decode_tokens,
699 slot_start=0,
vllm/model_executor/models/bailing_moe_linear.py
+4
@@ -533,6 +533,10 @@ class BailingMoeV25Model(nn.Module):
533 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
534 return self.word_embeddings(input_ids)
535
536 + @property
537 + def embed_tokens(self) -> nn.Module:
538 + return self.word_embeddings
539 +
540 def forward(
541 self,
542 input_ids: torch.Tensor | None,
vllm/model_executor/models/bailing_moe_mtp.py new
+380
@@ -0,0 +1,380 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Inference-only Bailing MoE v2.5 MTP model."""
4 +
5 +from collections.abc import Iterable
6 +
7 +import torch
8 +import torch.nn as nn
9 +from transformers.configuration_utils import PretrainedConfig
10 +
11 +from vllm.compilation.decorators import support_torch_compile
12 +from vllm.config import VllmConfig
13 +from vllm.distributed import get_pp_group
14 +from vllm.model_executor.layers.fused_moe import (
15 + fused_moe_make_expert_params_mapping,
16 +)
17 +from vllm.model_executor.layers.layernorm import RMSNorm
18 +from vllm.model_executor.layers.logits_processor import LogitsProcessor
19 +from vllm.model_executor.layers.vocab_parallel_embedding import (
20 + ParallelLMHead,
21 + VocabParallelEmbedding,
22 +)
23 +from vllm.model_executor.model_loader.weight_utils import (
24 + default_weight_loader,
25 + maybe_remap_kv_scale_name,
26 +)
27 +from vllm.model_executor.models.bailing_moe_linear import (
28 + BailingMoeV25,
29 + BailingMoeV25MLAAttention,
30 +)
31 +from vllm.sequence import IntermediateTensors
32 +
33 +from .utils import PPMissingLayer, is_pp_missing_parameter, maybe_prefix
34 +
35 +
36 +def _get_draft_hf_config(vllm_config: VllmConfig) -> PretrainedConfig:
37 + speculative_config = vllm_config.speculative_config
38 + if speculative_config is not None:
39 + draft_model_config = speculative_config.draft_model_config
40 + if draft_model_config is not None:
41 + return draft_model_config.hf_config
42 + return vllm_config.model_config.hf_config
43 +
44 +
45 +class BailingMTPSharedHead(nn.Module):
46 + def __init__(
47 + self,
48 + config: PretrainedConfig,
49 + prefix: str,
50 + vllm_config: VllmConfig,
51 + ) -> None:
52 + super().__init__()
53 + self.head = ParallelLMHead(
54 + config.vocab_size,
55 + config.hidden_size,
56 + quant_config=vllm_config.quant_config,
57 + prefix=maybe_prefix(prefix, "head"),
58 + )
59 +
60 + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
61 + return hidden_states
62 +
63 +
64 +class BailingMoeV25MultiTokenPredictorLayer(nn.Module):
65 + def __init__(
66 + self,
67 + vllm_config: VllmConfig,
68 + prefix: str,
69 + layer_id: int,
70 + ) -> None:
71 + super().__init__()
72 + config = _get_draft_hf_config(vllm_config)
73 + self.config = config
74 + self.layer_id = layer_id
75 + self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
76 + self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
77 + self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
78 + self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
79 + self.self_attn = BailingMoeV25MLAAttention(
80 + config,
81 + quant_config=vllm_config.quant_config,
82 + layer_id=layer_id,
83 + prefix=maybe_prefix(prefix, "self_attn"),
84 + cache_config=vllm_config.cache_config,
85 + )
86 + self.post_attention_layernorm = RMSNorm(
87 + config.hidden_size, eps=config.rms_norm_eps
88 + )
89 + self.mlp = BailingMoeV25(
90 + config,
91 + quant_config=vllm_config.quant_config,
92 + layer_id=layer_id,
93 + prefix=maybe_prefix(prefix, "mlp"),
94 + )
95 + self.final_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
96 + self.shared_head = BailingMTPSharedHead(
97 + config,
98 + maybe_prefix(prefix, "shared_head"),
99 + vllm_config,
100 + )
101 +
102 + def forward(
103 + self,
104 + input_ids: torch.Tensor,
105 + positions: torch.Tensor,
106 + previous_hidden_states: torch.Tensor,
107 + inputs_embeds: torch.Tensor | None = None,
108 + spec_step_index: int = 0,
109 + ) -> torch.Tensor:
110 + assert inputs_embeds is not None
111 + inputs_embeds = torch.where(positions.unsqueeze(-1) == 0, 0, inputs_embeds)
112 + inputs_embeds = self.enorm(inputs_embeds)
113 + previous_hidden_states = self.hnorm(previous_hidden_states)
114 +
115 + hidden_states = self.eh_proj(
116 + torch.cat([inputs_embeds, previous_hidden_states], dim=-1)
117 + )
118 + residual = hidden_states
119 + hidden_states = self.input_layernorm(hidden_states)
120 +
121 + hidden_states = self.self_attn(hidden_states, positions)
122 + hidden_states = residual + hidden_states
123 +
124 + residual = hidden_states
125 + hidden_states = self.post_attention_layernorm(hidden_states)
126 + hidden_states = self.mlp(hidden_states)
127 + hidden_states = residual + hidden_states.to(residual.device)
128 + return self.final_layernorm(hidden_states)
129 +
130 +
131 +class BailingMoeV25MultiTokenPredictor(nn.Module):
132 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
133 + super().__init__()
134 + config = _get_draft_hf_config(vllm_config)
135 + self.mtp_start_layer_idx = config.num_hidden_layers
136 + self.num_mtp_layers = config.num_nextn_predict_layers
137 + self.layers = nn.ModuleDict(
138 + {
139 + str(idx): BailingMoeV25MultiTokenPredictorLayer(
140 + vllm_config,
141 + f"{prefix}.layers.{idx}",
142 + idx,
143 + )
144 + for idx in range(
145 + self.mtp_start_layer_idx,
146 + self.mtp_start_layer_idx + self.num_mtp_layers,
147 + )
148 + }
149 + )
150 + if get_pp_group().is_first_rank:
151 + self.embed_tokens = VocabParallelEmbedding(
152 + config.vocab_size,
153 + config.hidden_size,
154 + org_num_embeddings=config.vocab_size,
155 + prefix=maybe_prefix(prefix, "embed_tokens"),
156 + )
157 + else:
158 + self.embed_tokens = PPMissingLayer()
159 + self.logits_processor = LogitsProcessor(config.vocab_size)
160 +
161 + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
162 + return self.embed_tokens(input_ids)
163 +
164 + def forward(
165 + self,
166 + input_ids: torch.Tensor,
167 + positions: torch.Tensor,
168 + previous_hidden_states: torch.Tensor,
169 + inputs_embeds: torch.Tensor | None = None,
170 + spec_step_idx: int = 0,
171 + ) -> torch.Tensor:
172 + if inputs_embeds is None:
173 + inputs_embeds = self.embed_tokens(input_ids)
174 + current_step_idx = spec_step_idx % self.num_mtp_layers
175 + return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
176 + input_ids,
177 + positions,
178 + previous_hidden_states,
179 + inputs_embeds,
180 + current_step_idx,
181 + )
182 +
183 + def compute_logits(
184 + self,
185 + hidden_states: torch.Tensor,
186 + spec_step_idx: int = 0,
187 + lm_head: nn.Module | None = None,
188 + ) -> torch.Tensor:
189 + current_step_idx = spec_step_idx % self.num_mtp_layers
190 + mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
191 + head = lm_head if lm_head is not None else mtp_layer.shared_head.head
192 + return self.logits_processor(
193 + head,
194 + mtp_layer.shared_head(hidden_states),
195 + )
196 +
197 +
198 +@support_torch_compile
199 +class BailingMoeV25MTPModel(nn.Module):
200 + packed_modules_mapping = {
201 + "gate_up_proj": ["gate_proj", "up_proj"],
202 + "fused_qkv_a_proj": ["q_a_proj", "kv_a_proj_with_mqa"],
203 + }
204 +
205 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
206 + super().__init__()
207 + self.config = _get_draft_hf_config(vllm_config)
208 + self.lm_head: nn.Module | None = None
209 + self.model = BailingMoeV25MultiTokenPredictor(
210 + vllm_config=vllm_config,
211 + prefix=maybe_prefix(prefix, "model"),
212 + )
213 +
214 + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
215 + return self.model.embed_input_ids(input_ids)
216 +
217 + def forward(
218 + self,
219 + input_ids: torch.Tensor | None,
220 + positions: torch.Tensor,
221 + hidden_states: torch.Tensor,
222 + intermediate_tensors: IntermediateTensors | None = None,
223 + inputs_embeds: torch.Tensor | None = None,
224 + spec_step_idx: int = 0,
225 + ) -> torch.Tensor:
226 + return self.model(
227 + input_ids,
228 + positions,
229 + hidden_states,
230 + inputs_embeds,
231 + spec_step_idx,
232 + )
233 +
234 + def compute_logits(
235 + self,
236 + hidden_states: torch.Tensor,
237 + spec_step_idx: int = 0,
238 + ) -> torch.Tensor:
239 + return self.model.compute_logits(hidden_states, spec_step_idx, self.lm_head)
240 +
241 + def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
242 + return fused_moe_make_expert_params_mapping(
243 + self,
244 + ckpt_gate_proj_name="gate_proj",
245 + ckpt_down_proj_name="down_proj",
246 + ckpt_up_proj_name="up_proj",
247 + num_experts=self.config.num_experts,
248 + num_redundant_experts=0,
249 + )
250 +
251 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
252 + stacked_params_mapping = [
253 + (".fused_qkv_a_proj", ".q_a_proj", 0),
254 + (".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
255 + (".gate_up_proj", ".gate_proj", 0),
256 + (".gate_up_proj", ".up_proj", 1),
257 + ]
258 + expert_params_mapping = list(self.get_expert_mapping())
259 + params_dict = dict(self.named_parameters(remove_duplicate=False))
260 + loaded_params: set[str] = set()
261 + loaded_mtp_layers: set[int] = set()
262 +
263 + def load_param(
264 + name: str,
265 + loaded_weight: torch.Tensor,
266 + shard_id=None,
267 + ) -> bool:
268 + name = maybe_remap_kv_scale_name(name, params_dict)
269 + if name is None:
270 + return False
271 + if name not in params_dict or is_pp_missing_parameter(name, self):
272 + return False
273 +
274 + param = params_dict[name]
275 + weight_loader = getattr(param, "weight_loader", default_weight_loader)
276 + if shard_id is None:
277 + weight_loader(param, loaded_weight)
278 + elif isinstance(shard_id, int):
279 + weight_loader(param, loaded_weight, shard_id)
280 + else:
281 + weight_loader(
282 + param,
283 + loaded_weight,
284 + name,
285 + expert_id=shard_id[0],
286 + shard_id=shard_id[1],
287 + )
288 + loaded_params.add(name)
289 + return True
290 +
291 + def get_spec_layer_idx(name: str) -> int | None:
292 + if not name.startswith("model.layers."):
293 + return None
294 + try:
295 + layer_idx = int(name.split("model.layers.", 1)[1].split(".", 1)[0])
296 + except (IndexError, ValueError):
297 + return None
298 + mtp_idx = layer_idx - self.config.num_hidden_layers
299 + if 0 <= mtp_idx < self.config.num_nextn_predict_layers:
300 + return layer_idx
301 + return None
302 +
303 + def normalize_name(name: str) -> str:
304 + name = name.replace(".attention.dense", ".self_attn.o_proj")
305 + name = name.replace(".attention.", ".self_attn.")
306 + return name.replace(
307 + "mlp.gate.e_score_correction_bias",
308 + "mlp.gate.expert_bias",
309 + )
310 +
311 + def load_lm_head(loaded_weight: torch.Tensor) -> None:
312 + for layer_idx in range(
313 + self.model.mtp_start_layer_idx,
314 + self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
315 + ):
316 + name = f"model.layers.{layer_idx}.shared_head.head.weight"
317 + load_param(name, loaded_weight)
318 +
319 + for name, loaded_weight in weights:
320 + if "rotary_emb.inv_freq" in name:
321 + continue
322 +
323 + if name == "model.word_embeddings.weight":
324 + load_param("model.embed_tokens.weight", loaded_weight)
325 + continue
326 + if name == "lm_head.weight":
327 + load_lm_head(loaded_weight)
328 + continue
329 +
330 + spec_layer = get_spec_layer_idx(name)
331 + if spec_layer is None:
332 + continue
333 + name = normalize_name(name)
334 +
335 + loaded = False
336 + for param_name, weight_name, shard_id in stacked_params_mapping:
337 + if weight_name not in name:
338 + continue
339 + if "mlp.experts." in name and name not in params_dict:
340 + continue
341 + mapped_name = name.replace(weight_name, param_name)
342 + if load_param(mapped_name, loaded_weight, shard_id):
343 + loaded = True
344 + break
345 + if loaded:
346 + loaded_mtp_layers.add(spec_layer)
347 + continue
348 +
349 + if "mlp.experts" in name:
350 + for mapping in expert_params_mapping:
351 + param_name, weight_name, expert_id, shard_id = mapping
352 + if weight_name not in name:
353 + continue
354 + mapped_name = name.replace(weight_name, param_name)
355 + if load_param(
356 + mapped_name,
357 + loaded_weight,
358 + (expert_id, shard_id),
359 + ):
360 + loaded = True
361 + break
362 + if loaded:
363 + loaded_mtp_layers.add(spec_layer)
364 + continue
365 +
366 + if load_param(name, loaded_weight):
367 + loaded_mtp_layers.add(spec_layer)
368 +
369 + for layer_idx in range(
370 + self.model.mtp_start_layer_idx,
371 + self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
372 + ):
373 + if layer_idx not in loaded_mtp_layers:
374 + raise ValueError(
375 + f"Bailing MTP speculative decoding layer {layer_idx} "
376 + "weights are missing from checkpoint. Use a checkpoint "
377 + "that includes MTP layer weights, or disable speculative "
378 + "decoding."
379 + )
380 + return loaded_params
vllm/model_executor/models/registry.py
+1
@@ -612,6 +612,7 @@ _SPECULATIVE_DECODING_MODELS = {
612 "DeepSeekMTPModel": ("deepseek_mtp", "DeepSeekMTP"),
613 "DeepSeekV4MTPModel": ("vllm.models.deepseek_v4", "DeepSeekV4MTP"),
614 "MiniMaxM3MTP": ("vllm.models.minimax_m3", "MiniMaxM3MTP"),
615 + "BailingMoeV25MTPModel": ("bailing_moe_mtp", "BailingMoeV25MTPModel"),
616 "Gemma4MTPModel": ("gemma4_mtp", "Gemma4MTP"),
617 "ErnieMTPModel": ("ernie_mtp", "ErnieMTP"),
618 "ExaoneMoeMTP": ("exaone_moe_mtp", "ExaoneMoeMTP"),
vllm/transformers_utils/model_arch_config_convertor.py
+7
@@ -271,6 +271,7 @@ class ModelArchConfigConvertorBase:
271 "pangu_ultra_moe",
272 "pangu_ultra_moe_mtp",
273 "bailing_hybrid",
274 + "bailing_hybrid_mtp",
275 ):
276 # check is deepseek_v4 model
277 if hasattr(self.hf_text_config, "compress_ratios"):
@@ -539,6 +540,11 @@ class Qwen3NextMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
540 return getattr(self.hf_text_config, "num_nextn_predict_layers", 0)
541
542
543 +class BailingHybridMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
544 + def get_num_hidden_layers(self) -> int:
545 + return getattr(self.hf_text_config, "num_nextn_predict_layers", 0)
546 +
547 +
548 class Qwen3_5MTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
549 def get_num_hidden_layers(self) -> int:
550 return getattr(self.hf_text_config, "mtp_num_hidden_layers", 0)
@@ -633,6 +639,7 @@ class MossAudioModelArchConfigConvertor(ModelArchConfigConvertorBase):
639
640 # hf_config.model_type -> convertor class
641 MODEL_ARCH_CONFIG_CONVERTORS = {
642 + "bailing_hybrid_mtp": BailingHybridMTPModelArchConfigConvertor,
643 "cohere_asr": CohereAsrModelArchConfigConvertor,
644 "dbrx": DbrxModelArchConfigConvertor,
645 "deepseek_mtp": DeepSeekMTPModelArchConfigConvertor,
vllm/v1/attention/backends/linear_attn.py
+212 -1
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
5 import torch
6
7 -from vllm.config import VllmConfig
7 +from vllm.config import CompilationConfig, VllmConfig
8 from vllm.v1.attention.backend import (
9 AttentionBackend,
10 AttentionCGSupport,
@@ -12,6 +12,7 @@ from vllm.v1.attention.backend import (
12 CommonAttentionMetadata,
13 )
14 from vllm.v1.attention.backends.utils import (
15 + PAD_SLOT_ID,
16 mamba_get_block_table_tensor,
17 split_decodes_and_prefills,
18 )
@@ -91,3 +92,213 @@ class LinearAttentionMetadataBuilder(AttentionMetadataBuilder[LinearAttentionMet
92 state_indices_tensor=state_indices_tensor,
93 )
94 return attn_metadata
95 +
96 +
97 +class BailingLinearAttentionBackend(LinearAttentionBackend):
98 + @staticmethod
99 + def get_name() -> str:
100 + return "BAILING_LINEAR_ATTN"
101 +
102 + @staticmethod
103 + def get_builder_cls() -> type["BailingLinearAttentionMetadataBuilder"]:
104 + return BailingLinearAttentionMetadataBuilder
105 +
106 +
107 +@dataclass
108 +class BailingLinearAttentionMetadata(LinearAttentionMetadata):
109 + state_indices_tensor_d: torch.Tensor | None = None
110 + state_indices_tensor_p: torch.Tensor | None = None
111 + num_accepted_tokens: torch.Tensor | None = None
112 + query_start_loc_d: torch.Tensor | None = None
113 +
114 +
115 +class BailingLinearAttentionMetadataBuilder(LinearAttentionMetadataBuilder):
116 + supports_spec_decode_metadata = True
117 + supports_update_block_table: bool = False
118 +
119 + @classmethod
120 + def get_cudagraph_support(
121 + cls,
122 + vllm_config: VllmConfig,
123 + kv_cache_spec: AttentionSpec,
124 + ) -> AttentionCGSupport:
125 + return AttentionCGSupport.UNIFORM_BATCH
126 +
127 + def __init__(
128 + self,
129 + kv_cache_spec: AttentionSpec,
130 + layer_names: list[str],
131 + vllm_config: VllmConfig,
132 + device: torch.device,
133 + ):
134 + super().__init__(kv_cache_spec, layer_names, vllm_config, device)
135 + self.compilation_config: CompilationConfig = vllm_config.compilation_config
136 + self.num_spec_tokens: int = vllm_config.num_speculative_tokens
137 + self.use_spec_decode: bool = self.num_spec_tokens > 0
138 + self.decode_cudagraph_max_bs: int = vllm_config.scheduler_config.max_num_seqs
139 + if self.compilation_config.max_cudagraph_capture_size is not None:
140 + self.decode_cudagraph_max_bs = min(
141 + self.decode_cudagraph_max_bs,
142 + self.compilation_config.max_cudagraph_capture_size,
143 + )
144 + self.decode_state_indices_tensor: torch.Tensor = torch.empty(
145 + (self.decode_cudagraph_max_bs, 1 + self.num_spec_tokens),
146 + dtype=torch.int32,
147 + device=device,
148 + )
149 + self.decode_legacy_state_indices_tensor: torch.Tensor = torch.empty(
150 + (self.decode_cudagraph_max_bs,),
151 + dtype=torch.int32,
152 + device=device,
153 + )
154 + self.decode_query_start_loc: torch.Tensor = torch.empty(
155 + (self.decode_cudagraph_max_bs + 1,),
156 + dtype=torch.int32,
157 + device=device,
158 + )
159 + self.decode_num_accepted_tokens: torch.Tensor = torch.empty(
160 + (self.decode_cudagraph_max_bs,),
161 + dtype=torch.int32,
162 + device=device,
163 + )
164 + self._init_reorder_batch_threshold(1, self.use_spec_decode)
165 +
166 + def build_for_cudagraph_capture(
167 + self,
168 + common_attn_metadata: CommonAttentionMetadata,
169 + ) -> BailingLinearAttentionMetadata:
170 + num_accepted_tokens = None
171 + if self.use_spec_decode:
172 + assert common_attn_metadata.max_query_len <= 1 + self.num_spec_tokens, (
173 + "Bailing linear attention only supports speculative decoding "
174 + "with query length <= 1 + number of speculative tokens."
175 + )
176 + num_accepted_tokens = torch.diff(common_attn_metadata.query_start_loc)
177 + return self.build(
178 + common_prefix_len=0,
179 + common_attn_metadata=common_attn_metadata,
180 + num_accepted_tokens=num_accepted_tokens,
181 + )
182 +
183 + def build( # type: ignore[override]
184 + self,
185 + common_prefix_len: int,
186 + common_attn_metadata: CommonAttentionMetadata,
187 + fast_build: bool = False,
188 + *,
189 + num_accepted_tokens: torch.Tensor | None = None,
190 + num_decode_draft_tokens_cpu: torch.Tensor | None = None,
191 + ) -> BailingLinearAttentionMetadata:
192 + query_start_loc = common_attn_metadata.query_start_loc
193 + seq_lens = common_attn_metadata.seq_lens
194 + num_reqs = common_attn_metadata.num_reqs
195 + use_spec_decode = self.use_spec_decode and num_accepted_tokens is not None
196 +
197 + state_indices_tensor = mamba_get_block_table_tensor(
198 + common_attn_metadata.block_table_tensor,
199 + common_attn_metadata.seq_lens,
200 + self.kv_cache_spec,
201 + self.vllm_config.cache_config.mamba_cache_mode,
202 + )
203 + if state_indices_tensor.dim() == 1:
204 + state_indices_tensor = state_indices_tensor.unsqueeze(-1)
205 +
206 + decode_threshold = self.reorder_batch_threshold if use_spec_decode else 1
207 + num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
208 + split_decodes_and_prefills(
209 + common_attn_metadata,
210 + decode_threshold=decode_threshold,
211 + )
212 + )
213 + state_indices_tensor_d, state_indices_tensor_p = torch.split(
214 + state_indices_tensor,
215 + [num_decodes, num_prefills],
216 + dim=0,
217 + )
218 + state_indices_tensor_p = state_indices_tensor_p[:, 0]
219 +
220 + query_start_loc_d = None
221 + if use_spec_decode:
222 + assert num_accepted_tokens is not None
223 + state_indices_tensor_d = state_indices_tensor_d[
224 + :, : 1 + self.num_spec_tokens
225 + ]
226 + query_start_loc_d = query_start_loc[: num_decodes + 1]
227 + num_accepted_tokens = num_accepted_tokens[:num_decodes]
228 + else:
229 + state_indices_tensor_d = state_indices_tensor_d[:, 0]
230 + num_accepted_tokens = None
231 +
232 + legacy_state_indices_tensor = state_indices_tensor[:, 0]
233 + cudagraph_mode = self.compilation_config.cudagraph_mode
234 + use_full_cudagraph = (
235 + cudagraph_mode is not None and cudagraph_mode.has_full_cudagraphs()
236 + )
237 + if (
238 + num_prefills == 0
239 + and num_decodes <= self.decode_cudagraph_max_bs
240 + and use_full_cudagraph
241 + ):
242 + padded_bs = num_reqs
243 + is_padded_decode = seq_lens[:num_decodes] == 0
244 + if state_indices_tensor_d.dim() > 1:
245 + state_indices_tensor_d = torch.where(
246 + is_padded_decode.unsqueeze(1),
247 + torch.full_like(state_indices_tensor_d, PAD_SLOT_ID),
248 + state_indices_tensor_d,
249 + )
250 + self.decode_state_indices_tensor[:num_decodes].copy_(
251 + state_indices_tensor_d,
252 + non_blocking=True,
253 + )
254 + state_indices_tensor_d = self.decode_state_indices_tensor[:padded_bs]
255 + state_indices_tensor_d[num_decodes:] = PAD_SLOT_ID
256 +
257 + self.decode_legacy_state_indices_tensor[:num_decodes].copy_(
258 + torch.where(
259 + is_padded_decode,
260 + torch.full_like(
261 + legacy_state_indices_tensor[:num_decodes],
262 + PAD_SLOT_ID,
263 + ),
264 + legacy_state_indices_tensor[:num_decodes],
265 + ),
266 + non_blocking=True,
267 + )
268 + legacy_state_indices_tensor = self.decode_legacy_state_indices_tensor[
269 + :padded_bs
270 + ]
271 + legacy_state_indices_tensor[num_decodes:] = PAD_SLOT_ID
272 + if state_indices_tensor_d.dim() == 1:
273 + state_indices_tensor_d = legacy_state_indices_tensor
274 +
275 + if use_spec_decode and num_accepted_tokens is not None:
276 + assert query_start_loc_d is not None
277 + self.decode_query_start_loc[: num_decodes + 1].copy_(
278 + query_start_loc_d,
279 + non_blocking=True,
280 + )
281 + decode_num_query_tokens = query_start_loc_d[-1]
282 + query_start_loc_d = self.decode_query_start_loc[: padded_bs + 1]
283 + query_start_loc_d[num_decodes + 1 :] = decode_num_query_tokens
284 +
285 + self.decode_num_accepted_tokens[:num_decodes].copy_(
286 + num_accepted_tokens,
287 + non_blocking=True,
288 + )
289 + num_accepted_tokens = self.decode_num_accepted_tokens[:padded_bs]
290 + num_accepted_tokens[num_decodes:] = 1
291 +
292 + return BailingLinearAttentionMetadata(
293 + num_prefills=num_prefills,
294 + num_prefill_tokens=num_prefill_tokens,
295 + num_decodes=num_decodes,
296 + num_decode_tokens=num_decode_tokens,
297 + query_start_loc=query_start_loc,
298 + seq_lens=seq_lens,
299 + state_indices_tensor=legacy_state_indices_tensor,
300 + state_indices_tensor_d=state_indices_tensor_d,
301 + state_indices_tensor_p=state_indices_tensor_p,
302 + num_accepted_tokens=num_accepted_tokens,
303 + query_start_loc_d=query_start_loc_d,
304 + )
vllm/v1/worker/gpu_model_runner.py
+12 -2
@@ -132,6 +132,9 @@ from vllm.v1.attention.backend import (
132 CommonAttentionMetadata,
133 )
134 from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadataBuilder
135 +from vllm.v1.attention.backends.linear_attn import (
136 + BailingLinearAttentionMetadataBuilder,
137 +)
138 from vllm.v1.attention.backends.mamba2_attn import Mamba2AttentionMetadataBuilder
139 from vllm.v1.attention.backends.utils import (
140 NULL_BLOCK_ID,
@@ -2442,9 +2445,16 @@ class GPUModelRunner(
2445
2446 extra_attn_metadata_args = {}
2447 if use_spec_decode and isinstance(
2445 - builder, (Mamba2AttentionMetadataBuilder, GDNAttentionMetadataBuilder)
2448 + builder,
2449 + (
2450 + Mamba2AttentionMetadataBuilder,
2451 + GDNAttentionMetadataBuilder,
2452 + BailingLinearAttentionMetadataBuilder,
2453 + ),
2454 ):
2447 - assert ubid is None, "UBatching not supported with GDN yet"
2455 + assert ubid is None, (
2456 + "UBatching not supported with GDN or linear attn yet"
2457 + )
2458 extra_attn_metadata_args = dict(
2459 num_accepted_tokens=self.num_accepted_tokens.gpu[:num_reqs_padded],
2460 num_decode_draft_tokens_cpu=self.num_decode_draft_tokens.cpu[