main
py 608 lines 24.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import torch
5 import torch.nn.functional as F
6 from torch import nn
7 from transformers import Glm5NextTextConfig
8
9 from vllm.config import (
10 CacheConfig,
11 VllmConfig,
12 )
13 from vllm.distributed import (
14 get_tensor_model_parallel_world_size,
15 )
16 from vllm.logger import init_logger
17 from vllm.model_executor.layers.layernorm import LayerNorm, RMSNorm
18 from vllm.model_executor.layers.linear import (
19 ColumnParallelLinear,
20 MergedColumnParallelLinear,
21 ReplicatedLinear,
22 RowParallelLinear,
23 )
24 from vllm.model_executor.layers.mla import MLAModules, MultiHeadLatentAttentionWrapper
25 from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
26 from vllm.model_executor.layers.rotary_embedding import RotaryEmbedding, get_rope
27 from vllm.model_executor.models.deepseek_v2 import (
28 DeepSeekV2FusedQkvAProjLinear,
29 DeepseekV32IndexerCache,
30 yarn_get_mscale,
31 )
32 from vllm.model_executor.utils import maybe_disable_graph_partition
33 from vllm.models.glm5next.nvidia.ops.kpool_compress import fwht128_quant_fp8
34 from vllm.models.glm5next.sparse_indexer import SparseAttnIndexerKpool
35 from vllm.platforms import current_platform
36 from vllm.utils.deep_gemm import PAGED_MQA_PAGE_SIZES
37 from vllm.utils.math_utils import cdiv, next_power_of_2
38 from vllm.v1.kv_cache_interface import KpoolTailSpec, MLAAttentionSpec
39
40 logger = init_logger(__name__)
41
42 # Shared torch.compile config for the indexer's small-kernel leaves. The MLA
43 # indexer runs under breakable-CG (CompilationMode.NONE), which blocks FX-graph
44 # fusion of the surrounding eager ops; carving each cluster into its own
45 # @torch.compile leaf (backend==inductor) still fuses them. Matches the
46 # grouped_topk / _cast_sigmoid leaf pattern.
47 _INDEXER_COMPILE = dict(
48 dynamic=True,
49 backend=current_platform.simple_compile_backend,
50 options=maybe_disable_graph_partition(current_platform.simple_compile_backend),
51 )
52
53
54 @torch.compile(**_INDEXER_COMPILE)
55 def _fused_indexer_k_norm(
56 x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, dim: int, eps: float
57 ) -> torch.Tensor:
58 # Fuse fp32 cast + layer_norm + cast-back (was 3 kernels) into one.
59 return F.layer_norm(x.float(), (dim,), weight, bias, eps).type_as(x)
60
61
62 @torch.compile(**_INDEXER_COMPILE)
63 def _fused_indexer_weight_scale(
64 weights: torch.Tensor, q_scale: torch.Tensor, scale: float
65 ) -> torch.Tensor:
66 # Fuse the weight-scaling muls (was 2 kernels) into one. `scale` folds
67 # softmax_scale (head_dim**-0.5) and n_head**-0.5 into a single constant.
68 return (weights.unsqueeze(-1) * q_scale * scale).squeeze(-1)
69
70
71 @torch.compile(**_INDEXER_COMPILE)
72 def _pad_indexer_heads(x: torch.Tensor, pad: int) -> torch.Tensor:
73 # DeepGEMM MQA-logits needs num_heads in {32,64}; zero-pad the head dim.
74 # Fuse new_zeros + cat (was 2 kernels) into one. Pad values are zero (exact
75 # in fp8 e4m3 and zero-weight in the logits sum), so numerically a no-op.
76 return torch.cat([x, x.new_zeros(x.shape[0], pad, *x.shape[2:])], dim=1)
77
78
79 class Glm5NextIndexerCache(DeepseekV32IndexerCache):
80 """Indexer K cache that stores kpool-compressed entries.
81
82 Setting ``tokens_per_state = index_kpool`` on the KV cache spec makes vLLM's
83 indexer metadata builder emit pool-granular ``slot_mapping`` /
84 ``seq_lens`` / ``cu_seq_lens`` / ``page_table`` for free, and shrinks the
85 cache allocation store one state per ``index_kpool`` tokens. The pool
86 *content* (softmax-weighted sum vs keep-every-Nth) is computed by the
87 kpool compress kernel inside the indexer op — the cache only provides the
88 addressing, which is identical for both schemes.
89
90 The indexer shares one block with the co-located MLA (a single
91 ``MLAAttentionSpec`` / block_table), so ``block_size`` is the model-wide
92 ``cache_config.block_size``. DeepGEMM's paged-MQA kernel
93 (``csrc/apis/attention.hpp``) requires ``block_kv`` to be exactly 32 or
94 64, so the storage block is virtually split into pool pages of the
95 largest such size that tiles it (``storage_kernel_block_size``); this
96 needs ``block_size`` to be a multiple of ``index_kpool * 32`` (512 for
97 ``index_kpool = 16``). A smaller block (e.g. the default 64) silently
98 collapses ``storage_block_size`` (64 // 16 = 4) and only fails later at
99 the opaque C++ assert; ``get_kv_cache_spec`` guards this up front
100 instead.
101 """
102
103 def __init__(
104 self,
105 *,
106 head_dim: int,
107 dtype: torch.dtype,
108 prefix: str,
109 cache_config,
110 index_kpool: int,
111 ):
112 super().__init__(
113 head_dim=head_dim, dtype=dtype, prefix=prefix, cache_config=cache_config
114 )
115 assert index_kpool > 1, "Glm5NextIndexerCache expects index_kpool > 1"
116 # Keep chunked-prefill boundaries aligned to complete pools.
117 assert cache_config.block_size % index_kpool == 0, (
118 "Glm5NextIndexerCache: cache_config.block_size "
119 f"({cache_config.block_size}) must be a multiple of index_kpool "
120 f"({index_kpool}) so chunked-prefill boundaries stay pool-aligned."
121 )
122 self._index_kpool = index_kpool
123
124 def get_kv_cache_spec(self, vllm_config: VllmConfig):
125 from dataclasses import replace
126
127 spec = super().get_kv_cache_spec(vllm_config)
128 # ``tokens_per_state`` is the KV-spec representation of kpool
129 # compression in the current cache-layout API.
130 assert isinstance(spec, MLAAttentionSpec)
131 spec = replace(spec, tokens_per_state=self._index_kpool)
132
133 # DeepGEMM paged-MQA takes block_kv in {32, 64}; the storage block
134 # (= block_size // index_kpool) is virtually split into pool pages of
135 # the largest such size that tiles it, so it must be a multiple of 32.
136 storage_block_size = spec.block_size // self._index_kpool
137 assert (
138 spec.block_size % self._index_kpool == 0 and storage_block_size % 32 == 0
139 ), (
140 "Glm5NextIndexerCache: kpool indexer requires cache block_size to "
141 f"be a multiple of index_kpool * 32 ({self._index_kpool * 32}) so "
142 "that DeepGEMM paged-MQA pool pages (32 or 64 entries) tile the "
143 f"storage block, got block_size={spec.block_size} -> "
144 f"storage_block_size={storage_block_size}."
145 )
146 max_page_size = max(PAGED_MQA_PAGE_SIZES)
147 min_page_size = min(PAGED_MQA_PAGE_SIZES)
148 if storage_block_size <= max_page_size:
149 page_size = storage_block_size
150 elif storage_block_size % max_page_size == 0:
151 page_size = max_page_size
152 else:
153 page_size = min_page_size
154 return replace(
155 spec,
156 storage_block_size=page_size * self._index_kpool,
157 )
158
159
160 class Glm5NextTailCache(DeepseekV32IndexerCache):
161 """Paged circular buffer for the kpool indexer's in-progress (tail) pool.
162
163 Holds the trailing incomplete pool's raw K + gate score: one block of
164 ``ring`` slots per request, overwritten in place by ``pos % ring``
165 as decode/spec-decode advances. Prefill seeds it (instead of discarding the
166 tail raw K+gate); the connector transfers it across PD; decode reads it to
167 compress the boundary pool correctly. ``KpoolTailSpec`` /
168 ``KpoolTailManager`` provide the no-prune, 1-block/req allocation that lets
169 the in-progress pool survive across steps and across transfer.
170
171 Stores raw bf16 K (``head_dim``) as the "K" half of each block and the
172 bf16 gate score (``head_dim``) as the "V" half -- not the fp8-compressed
173 entry, which lives in ``Glm5NextIndexerCache``.
174 """
175
176 def __init__(
177 self,
178 *,
179 head_dim: int,
180 dtype: torch.dtype,
181 prefix: str,
182 cache_config,
183 index_kpool: int,
184 ):
185 super().__init__(
186 head_dim=head_dim, dtype=dtype, prefix=prefix, cache_config=cache_config
187 )
188 assert index_kpool > 1, "Glm5NextTailCache expects index_kpool > 1"
189 self._index_kpool = index_kpool
190
191 def get_kv_cache_spec(self, vllm_config: VllmConfig):
192 # The two head slots form [K, gate score] in the generic
193 # [block, head, state, content] cache view.
194 # Drafts are stashed before acceptance. With a one-pool ring, the
195 # drafts behind a rejected pool-completing draft overwrite the keys
196 # read by its redo.
197 span = self._index_kpool + vllm_config.num_speculative_tokens
198 ring = self._index_kpool * next_power_of_2(cdiv(span, self._index_kpool))
199 # ring must divide the attention block size (a multiple of 128).
200 assert self.cache_config.block_size % ring == 0, (
201 f"Glm5NextTailCache: cache_config.block_size "
202 f"({self.cache_config.block_size}) must be a multiple of the "
203 f"tail ring ({ring})"
204 )
205 return KpoolTailSpec(
206 block_size=ring,
207 num_kv_heads=2,
208 head_size=self.head_dim,
209 head_size_v=0,
210 dtype=torch.bfloat16,
211 sliding_window=ring,
212 )
213
214 def get_attn_backend(self):
215 from vllm.v1.attention.backends.mla.indexer import KpoolTailBackend
216
217 return KpoolTailBackend
218
219
220 class Indexer(nn.Module):
221 def __init__(
222 self,
223 vllm_config: VllmConfig,
224 config: Glm5NextTextConfig,
225 hidden_size: int,
226 q_lora_rank: int,
227 quant_config: QuantizationConfig | None,
228 cache_config: CacheConfig | None,
229 topk_indices_buffer: torch.Tensor | None,
230 prefix: str = "",
231 ):
232 super().__init__()
233 self.vllm_config = vllm_config
234 self.config = config
235 self.quant_config = quant_config
236 # self.indexer_cfg = config.attn_module_list_cfg[0]["attn_index"]
237 # Indexer is only constructed for v32 configs, where these sparse-indexer
238 # fields are guaranteed populated; narrow away the `int | None` declared
239 # on Glm5NextTextConfig for the optional-indexer case.
240 assert config.index_topk is not None
241 assert config.index_n_heads is not None
242 assert config.index_head_dim is not None
243 assert config.index_kpool is not None
244 self.topk_tokens = config.index_topk
245 self.n_head = config.index_n_heads # 64
246 self.head_dim = config.index_head_dim # 128
247 self.rope_dim = config.qk_rope_head_dim # 64
248 self.index_kpool = config.index_kpool
249 self.q_lora_rank = q_lora_rank # 1536
250
251 # kpool
252 self.index_kpool_compress_ape = nn.Parameter(
253 torch.zeros(self.index_kpool, self.head_dim, dtype=torch.float32)
254 )
255 # Keep the checkpoint name ``index_kpool_compress_gate`` without a
256 # ``.weight`` suffix. F.linear consumes its [head_dim, hidden_size] shape.
257 self.index_kpool_compress_gate = nn.Parameter(
258 torch.empty(self.head_dim, hidden_size, dtype=torch.bfloat16)
259 )
260
261 # no tensor parallel, just replicated
262 self.wq_b = ReplicatedLinear(
263 self.q_lora_rank,
264 self.head_dim * self.n_head,
265 bias=False,
266 quant_config=quant_config,
267 prefix=f"{prefix}.wq_b",
268 )
269 # Fused wk + weights_proj: single GEMM producing [head_dim + n_head].
270 # FP8 wk weights are upcasted to BF16 during loading to maintain fusion.
271 self.wk_weights_proj = MergedColumnParallelLinear(
272 hidden_size,
273 [self.head_dim, self.n_head],
274 bias=False,
275 quant_config=None,
276 disable_tp=True,
277 prefix=f"{prefix}.wk_weights_proj",
278 )
279 self.k_norm = LayerNorm(self.head_dim, eps=1e-6, dtype=torch.float32)
280 self.softmax_scale = self.head_dim**-0.5
281
282 # Hadamard-128 rotation of the indexer query is fused with the FP8
283 # quant (see forward: fwht128_quant_fp8) -- no precomputed matrix.
284
285 self.scale_fmt = "ue8m0"
286 self.quant_block_size = 128 # TODO: get from config
287 self.topk_indices_buffer = topk_indices_buffer
288 self._wp_fp32: torch.Tensor | None = None
289
290 # NOTE: (zyongye) we use fp8 naive cache,
291 # where we store value in fp8 and scale in fp32
292 # per self.quant_block_size element
293 self.k_cache = Glm5NextIndexerCache(
294 head_dim=self.head_dim + self.head_dim // self.quant_block_size * 4,
295 dtype=torch.uint8,
296 prefix=f"{prefix}.k_cache",
297 cache_config=cache_config,
298 index_kpool=self.index_kpool,
299 )
300 # Paged tail cache (in-progress pool's raw K + gate score). Written by
301 # prefill (seeds the boundary pool) and decode (per-step stash); read by
302 # the decode kernel to compress the boundary pool. Transferred across PD
303 # so the decode side sees the prefill tail. See KpoolTailSpec/Manager.
304 self.tail_cache = Glm5NextTailCache(
305 head_dim=self.head_dim,
306 dtype=torch.bfloat16,
307 prefix=f"{prefix}.tail_cache",
308 cache_config=cache_config,
309 index_kpool=self.index_kpool,
310 )
311 self.max_pool_len = vllm_config.model_config.max_model_len // self.index_kpool
312 self.prefix = prefix
313 from vllm.v1.attention.backends.mla.indexer import get_max_prefill_buffer_size
314
315 self.max_total_seq_len = (
316 get_max_prefill_buffer_size(vllm_config) // self.index_kpool
317 )
318 self.indexer_op = SparseAttnIndexerKpool(
319 self.k_cache,
320 self.quant_block_size,
321 self.scale_fmt,
322 self.topk_tokens,
323 self.head_dim,
324 self.max_pool_len,
325 self.max_total_seq_len,
326 self.topk_indices_buffer,
327 tail_cache=self.tail_cache,
328 )
329
330 def forward(
331 self, hidden_states: torch.Tensor, qr: torch.Tensor, positions, rotary_emb
332 ) -> torch.Tensor:
333 q, _ = self.wq_b(qr)
334 q = q.view(-1, self.n_head, self.head_dim)
335
336 # Compute the head gate in fp32; bf16 error can change near-tie pool
337 # rankings on long-context tasks. Cache it after weights are loaded.
338 kw, _ = self.wk_weights_proj(hidden_states)
339 k = kw[:, : self.head_dim]
340 if self._wp_fp32 is None:
341 self._wp_fp32 = (
342 self.wk_weights_proj.weight.data[self.head_dim :, :]
343 .t()
344 .contiguous()
345 .float()
346 )
347 weights = torch.mm(hidden_states.float(), self._wp_fp32)
348
349 k = _fused_indexer_k_norm(
350 k, self.k_norm.weight, self.k_norm.bias, self.head_dim, self.k_norm.eps
351 )
352
353 if self.rope_dim > 0:
354 q_pe, q_nope = torch.split(
355 q, [self.rope_dim, self.head_dim - self.rope_dim], dim=-1
356 )
357 k_pe, k_nope = torch.split(
358 k, [self.rope_dim, self.head_dim - self.rope_dim], dim=-1
359 )
360
361 q_pe, k_pe = rotary_emb(positions, q_pe, k_pe.unsqueeze(1))
362 # Note: RoPE (NeoX) can introduce extra leading dimensions during
363 # compilation so we need to reshape back to token-flattened shapes
364 q_pe = q_pe.reshape(-1, self.n_head, self.rope_dim)
365 k_pe = k_pe.reshape(-1, 1, self.rope_dim)
366
367 # `rotary_emb` is shape-preserving; `q_pe` is already
368 # [num_tokens, n_head, rope_dim].
369 q = torch.cat([q_pe, q_nope], dim=-1)
370 # `k_pe` is [num_tokens, 1, rope_dim] (MQA).
371 k = torch.cat([k_pe.squeeze(-2), k_nope], dim=-1)
372 # else: qk_rope_head_dim=0 — no rope component. q is already
373 # [num_tokens, n_head, head_dim] and k is [num_tokens, head_dim] (all
374 # nope), so skip the rope split / rotary / cat entirely; otherwise the
375 # split/reshape would build 0-element tensors (breaks dynamo tracing).
376
377 # Rotate Q into the cached K basis before computing fp8 MQA logits.
378 # Fusing the fp32 FWHT and quantization avoids an intermediate HBM
379 # round-trip and bf16 matrix-rounding bias.
380 assert self.head_dim == 128 and self.quant_block_size == 128
381 assert self.scale_fmt == "ue8m0"
382 q = q.view(-1, self.head_dim)
383 q_fp8, q_scale = fwht128_quant_fp8(q)
384 q_fp8 = q_fp8.view(-1, self.n_head, self.head_dim)
385 q_scale = q_scale.view(-1, self.n_head, 1)
386
387 weights = _fused_indexer_weight_scale(
388 weights, q_scale, self.softmax_scale * self.n_head**-0.5
389 )
390
391 # kpool: per-token gate score driving the softmax-weighted pool. Computed
392 # from the same hidden_states that produced `k`, so it stays token-aligned.
393 # F.linear(x, gate) = x @ gate.T with gate [head_dim, hidden_size].
394 gate_score = F.linear(hidden_states, self.index_kpool_compress_gate)
395
396 # DeepGEMM's MQA-logits kernels (fp8_mqa_logits /
397 # fp8_fp4_paged_mqa_logits) require num_heads in {32, 64}; this
398 # checkpoint uses index_n_heads=16. Zero-pad q and the per-head
399 # weights: logits are a weights-weighted sum over heads, so
400 # zero-weight padded heads contribute exactly nothing.
401 if self.n_head < 32:
402 pad = 32 - self.n_head
403 q_fp8 = _pad_indexer_heads(q_fp8, pad)
404 weights = _pad_indexer_heads(weights, pad)
405
406 return self.indexer_op(
407 hidden_states,
408 q_fp8,
409 k,
410 weights,
411 gate_score=gate_score,
412 compress_ape=self.index_kpool_compress_ape,
413 index_kpool=self.index_kpool,
414 positions=positions,
415 )
416
417
418 class Glm5NextMLAAttention(nn.Module):
419 def __init__(
420 self,
421 vllm_config: VllmConfig,
422 config: Glm5NextTextConfig,
423 hidden_size: int,
424 num_heads: int,
425 qk_nope_head_dim: int,
426 qk_rope_head_dim: int,
427 v_head_dim: int,
428 q_lora_rank: int | None,
429 kv_lora_rank: int,
430 max_position_embeddings: int = 8192,
431 cache_config: CacheConfig | None = None,
432 quant_config: QuantizationConfig | None = None,
433 prefix: str = "",
434 topk_indices_buffer: torch.Tensor | None = None,
435 input_size: int | None = None,
436 skip_rope: bool | None = False,
437 ) -> None:
438 super().__init__()
439 self.hidden_size = hidden_size
440 self.qk_nope_head_dim = qk_nope_head_dim
441 self.qk_rope_head_dim = qk_rope_head_dim
442 self.qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
443 self.v_head_dim = v_head_dim
444
445 self.q_lora_rank = q_lora_rank
446 self.kv_lora_rank = kv_lora_rank
447
448 self.num_heads = num_heads
449 tp_size = get_tensor_model_parallel_world_size()
450 assert num_heads % tp_size == 0
451 self.num_local_heads = num_heads // tp_size
452
453 self.scaling = self.qk_head_dim**-0.5
454 self.max_position_embeddings = max_position_embeddings
455
456 # Use input_size for projection input dimensions if provided,
457 # otherwise default to hidden_size (used in Eagle3 Deepseek with MLA)
458 proj_input_size = input_size if input_size is not None else self.hidden_size
459
460 if self.q_lora_rank is not None:
461 self.fused_qkv_a_proj = DeepSeekV2FusedQkvAProjLinear(
462 proj_input_size,
463 [self.q_lora_rank, self.kv_lora_rank + self.qk_rope_head_dim],
464 quant_config=quant_config,
465 prefix=f"{prefix}.fused_qkv_a_proj",
466 )
467 else:
468 self.kv_a_proj_with_mqa = ReplicatedLinear(
469 proj_input_size,
470 self.kv_lora_rank + self.qk_rope_head_dim,
471 bias=False,
472 quant_config=quant_config,
473 prefix=f"{prefix}.kv_a_proj_with_mqa",
474 )
475
476 if self.q_lora_rank is not None:
477 self.q_a_layernorm = RMSNorm(self.q_lora_rank, eps=config.rms_norm_eps)
478 self.q_b_proj = ColumnParallelLinear(
479 self.q_lora_rank,
480 self.num_heads * self.qk_head_dim,
481 bias=False,
482 quant_config=quant_config,
483 prefix=f"{prefix}.q_b_proj",
484 )
485 else:
486 self.q_proj = ColumnParallelLinear(
487 proj_input_size,
488 self.num_heads * self.qk_head_dim,
489 bias=False,
490 quant_config=quant_config,
491 prefix=f"{prefix}.q_proj",
492 )
493 self.kv_a_layernorm = RMSNorm(self.kv_lora_rank, eps=config.rms_norm_eps)
494 self.kv_b_proj = ColumnParallelLinear(
495 self.kv_lora_rank,
496 self.num_heads * (self.qk_nope_head_dim + self.v_head_dim),
497 bias=False,
498 quant_config=quant_config,
499 prefix=f"{prefix}.kv_b_proj",
500 )
501 self.o_proj = RowParallelLinear(
502 self.num_heads * self.v_head_dim,
503 self.hidden_size,
504 bias=False,
505 quant_config=quant_config,
506 prefix=f"{prefix}.o_proj",
507 )
508
509 # `Glm5NextTextConfig` is NoPE-only and so declares no `rope_parameters`;
510 # a checkpoint may still ship one.
511 rope_parameters = getattr(config, "rope_parameters", None)
512
513 if not skip_rope:
514 assert rope_parameters is not None
515 if rope_parameters["rope_type"] != "default":
516 rope_parameters["rope_type"] = (
517 "deepseek_llama_scaling"
518 if rope_parameters.get("attention_factor") == 1.0
519 else "deepseek_yarn"
520 )
521
522 self.rotary_emb: RotaryEmbedding | None = get_rope(
523 qk_rope_head_dim,
524 max_position=max_position_embeddings,
525 rope_parameters=rope_parameters,
526 is_neox_style=False,
527 )
528
529 if (
530 rope_parameters["rope_type"] != "default"
531 and rope_parameters["rope_type"] == "deepseek_yarn"
532 ):
533 mscale_all_dim = rope_parameters.get("mscale_all_dim", False)
534 scaling_factor = rope_parameters["factor"]
535 mscale = yarn_get_mscale(scaling_factor, float(mscale_all_dim))
536 self.scaling = self.scaling * mscale * mscale
537 else:
538 self.rotary_emb = None
539
540 self.is_v32 = config.index_topk is not None
541
542 if self.is_v32:
543 self.indexer_rope_emb: RotaryEmbedding | None = get_rope(
544 qk_rope_head_dim,
545 max_position=max_position_embeddings,
546 rope_parameters=rope_parameters,
547 is_neox_style=not config.indexer_rope_interleave,
548 )
549 # The sparse indexer projects from the MLA q-lora rank, which is
550 # always set for v32 MLA configs; narrow away the `int | None`.
551 assert q_lora_rank is not None
552 self.indexer: Indexer | None = Indexer(
553 vllm_config,
554 config,
555 hidden_size,
556 q_lora_rank,
557 quant_config,
558 cache_config,
559 topk_indices_buffer,
560 f"{prefix}.indexer",
561 )
562
563 else:
564 self.indexer_rope_emb = None
565 self.indexer = None
566
567 mla_modules = MLAModules(
568 kv_a_layernorm=self.kv_a_layernorm,
569 kv_b_proj=self.kv_b_proj,
570 rotary_emb=self.rotary_emb,
571 o_proj=self.o_proj,
572 fused_qkv_a_proj=self.fused_qkv_a_proj
573 if self.q_lora_rank is not None
574 else None,
575 kv_a_proj_with_mqa=self.kv_a_proj_with_mqa
576 if self.q_lora_rank is None
577 else None,
578 q_a_layernorm=self.q_a_layernorm if self.q_lora_rank is not None else None,
579 q_b_proj=self.q_b_proj if self.q_lora_rank is not None else None,
580 q_proj=self.q_proj if self.q_lora_rank is None else None,
581 indexer=self.indexer,
582 indexer_rotary_emb=self.indexer_rope_emb,
583 is_sparse=self.is_v32,
584 topk_indices_buffer=topk_indices_buffer,
585 )
586
587 self.mla_attn = MultiHeadLatentAttentionWrapper(
588 self.hidden_size,
589 self.num_local_heads,
590 self.scaling,
591 self.qk_nope_head_dim,
592 self.qk_rope_head_dim,
593 self.v_head_dim,
594 self.q_lora_rank,
595 self.kv_lora_rank,
596 mla_modules,
597 cache_config,
598 quant_config,
599 prefix,
600 skip_topk=False,
601 fuse_qkv_rmsnorm=True,
602 )
603
604 def forward(
605 self, hidden_states: torch.Tensor, positions: torch.Tensor
606 ) -> torch.Tensor:
607 # The wrapper also runs the sparse indexer before MLA attention.
608 return self.mla_attn(positions, hidden_states)