Skip to content

vllm.models.kimi_k3.amd.mla

AMD-specific MLA wrapper for Kimi-K3.

Classes:

KimiK3MultiHeadLatentAttentionWrapper

Bases: MultiHeadLatentAttentionWrapper

Kimi-K3 MLA wrapper with eager AITER q/kv RMSNorm and output gate fusions and a fused decode Q-prep path.

Source code in vllm/models/kimi_k3/amd/mla.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
class KimiK3MultiHeadLatentAttentionWrapper(MultiHeadLatentAttentionWrapper):
    """Kimi-K3 MLA wrapper with eager AITER q/kv RMSNorm and output gate
    fusions and a fused decode Q-prep path."""

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self._use_eager_qk_rmsnorm_fusion = bool(rocm_aiter_ops.is_enabled())
        self._fused_qk_prep = self._fused_qk_prep_supported()
        self._identity_rope: tuple[torch.Tensor, torch.Tensor] | None = None
        self._qkv_gate_fused = isinstance(
            self.fused_qkv_a_proj, KimiK3MergedQKVGateLinear
        )
        self._fused_q_heads = self.num_heads
        if self._fused_qk_prep:
            from vllm.v1.attention.backends.mla.rocm_aiter_mla import AiterMLAHelper

            self._fused_q_heads = AiterMLAHelper.get_actual_mla_num_heads(
                self.num_heads
            )
        # Positions fed to the fused kernel's identity RoPE; see _fused_decode.
        # Allocated once here so every captured CUDA graph shares one zeroed
        # buffer instead of capturing a memset per layer.
        zero_positions = None
        if self._fused_qk_prep:
            vllm_config = get_current_vllm_config()
            max_tokens = max(
                vllm_config.scheduler_config.max_num_batched_tokens,
                vllm_config.compilation_config.max_cudagraph_capture_size or 0,
            )
            zero_positions = torch.zeros(max_tokens, dtype=torch.int64)
        self.register_buffer("_zero_positions", zero_positions, persistent=False)
        # The fold reads q_b_proj.weight as plain bf16/fp16 rows, so a quantized
        # q_b_proj keeps the unfused path.
        self._fold_qb_uk = (
            self._fused_qk_prep
            and self.q_b_proj is not None
            and isinstance(self.q_b_proj.quant_method, UnquantizedLinearMethod)
            and self.rotary_emb is None
            and not self.dcp_q_replicate
            and self.indexer is None
        )
        self._w_fold: torch.Tensor | None = None

    def _fused_qk_prep_supported(self) -> bool:
        attn = self.mla_attn
        if not (
            rocm_aiter_ops.is_mla_enabled()
            and attn.kv_cache_dtype.startswith("fp8")
            and self.kv_lora_rank == _OPT_KV_LORA_RANK
            and self.qk_rope_head_dim == _OPT_ROT_DIM
            and self.kv_lora_rank * self.num_heads >= _OPT_MIN_SIZE
        ):
            return False
        # The fused kernel writes q_out only for slot >= 0, so padded rows would
        # be garbage after a DCP query all-gather. PCP and HiSparse rewrite the
        # KV inputs/slots before the standalone write, which this path bypasses.
        if attn.impl.dcp_world_size > 1 or attn.use_pcp:
            return False
        if attn.hisparse_cache is not None or attn.q_pad_num_heads is not None:
            return False
        # Only the W_UK variants reproduced in _fused_decode are supported.
        return (
            attn.is_aiter_triton_fp4_bmm_enabled
            or not attn.is_aiter_triton_fp8_bmm_enabled
        )

    def get_input_quant_key(self) -> QuantKey | None:
        if self.indexer is not None:
            return None
        consumers = (
            [self.fused_qkv_a_proj]
            if self.q_lora_rank is not None
            else [self.kv_a_proj_with_mqa, self.q_proj]
        )
        if self.g_proj is not None:
            consumers.append(self.g_proj)
        if any(layer is None for layer in consumers):
            return None
        keys = [get_input_quant_key(layer) for layer in consumers if layer is not None]
        return keys[0] if all(key == keys[0] for key in keys) else None

    def _normalize_q_kv(
        self,
        q_c: torch.Tensor,
        kv_c: torch.Tensor,
    ) -> tuple[torch.Tensor, torch.Tensor]:
        q_layernorm = cast(RMSNorm, self.q_a_layernorm)
        kv_layernorm = cast(RMSNorm, self.kv_a_layernorm)

        if self._use_eager_qk_rmsnorm_fusion and not torch.compiler.is_compiling():
            return torch.ops.vllm.fused_mla_dual_rms_norm(
                q_c,
                q_layernorm.weight,
                kv_c,
                kv_layernorm.weight,
                q_layernorm.variance_epsilon,
                kv_layernorm.variance_epsilon,
            )

        return q_layernorm(q_c), kv_layernorm(kv_c)

    @staticmethod
    def _compute_w_fold(
        w_qb: torch.Tensor,
        w_uk: torch.Tensor,
        num_heads: int,
        qk_nope_head_dim: int,
        qk_rope_head_dim: int,
    ) -> torch.Tensor:
        """W_fold = [W_UK @ W_qb_nope ; W_qb_pe]: one GEMM gives [ql_nope | q_pe].

        ``w_qb`` is the q_b_proj weight [N * (P + R), Lq], ``w_uk`` is [L, N, P].
        Rows hold all heads' nope rows, then all pe rows, so the two output
        halves of ``x @ W_fold.T`` are contiguous: [B, N * L] and [B, N * R]."""
        N, P, R = num_heads, qk_nope_head_dim, qk_rope_head_dim
        L = w_uk.shape[0]
        w = w_qb.view(N, P + R, -1).float()
        nope = torch.einsum("lnp,npk->nlk", w_uk.float(), w[:, :P])  # [N, L, Lq]
        return (
            torch.cat([nope.reshape(N * L, -1), w[:, P:].reshape(N * R, -1)])
            .to(w_qb.dtype)
            .contiguous()
        )

    @staticmethod
    def _split_fold_output(
        y: torch.Tensor,
        num_heads: int,
        kv_lora_rank: int,
        qk_rope_head_dim: int,
    ) -> tuple[torch.Tensor, torch.Tensor]:
        B, N, L = y.shape[0], num_heads, kv_lora_rank
        return y[:, : N * L].view(B, N, L), y[:, N * L :].view(B, N, qk_rope_head_dim)

    @torch.no_grad()
    def _build_w_fold(self) -> None:
        assert self.q_b_proj is not None
        w_qb = self.q_b_proj.weight  # [N * (P + R), q_lora_rank]
        W_UK, _ = split_kv_b_proj(
            self.mla_attn.kv_b_proj,
            w_qb.dtype,
            self.kv_lora_rank,
            self.num_heads,
            self.qk_nope_head_dim,
            self.v_head_dim,
        )  # [L, N, P]
        self._w_fold = self._compute_w_fold(
            w_qb, W_UK, self.num_heads, self.qk_nope_head_dim, self.qk_rope_head_dim
        )

    def process_weights_after_loading(self) -> None:
        if self._fold_qb_uk:
            self._build_w_fold()

    def _fused_decode(
        self,
        q: torch.Tensor,
        kv_c_normed: torch.Tensor,
        k_pe: torch.Tensor,
        output_shape: tuple[int, int],
        attn_metadata,
        layer,
        kv_cache: torch.Tensor,
        slot_mapping: torch.Tensor,
    ) -> torch.Tensor:
        """Decode-only MLA through one fused AITER launch.

        Replaces the fp8 KV-cache write plus the ``[ql_nope | q_pe]`` concat and
        that query's static fp8 quant with a single
        ``fused_qk_rope_concat_and_cache_mla``. On an untouched upstream build
        the concat and quant already arrive as one inductor-fused kernel, so
        this collapses two launches into one.
        """
        from aiter.ops.cache import fused_qk_rope_concat_and_cache_mla

        attn = self.mla_attn
        if q.dim() == 2:
            from aiter.ops.triton.gemm.basic.gemm_a16w16 import gemm_a16w16

            assert self._w_fold is not None
            y = gemm_a16w16(q, self._w_fold)
            ql_nope, q_pe = self._split_fold_output(
                y, self.num_heads, self.kv_lora_rank, self.qk_rope_head_dim
            )
        else:
            q_nope, q_pe = q.split(
                [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1
            )
            q_nope_t = q_nope.transpose(0, 1)

            if attn.is_aiter_triton_fp4_bmm_enabled:
                from aiter.ops.triton.batched_gemm_a16wfp4 import (
                    batched_gemm_a16wfp4,
                )

                ql_nope = batched_gemm_a16wfp4(
                    q_nope_t,
                    attn.W_K,
                    attn.W_K_scale,
                    transpose_bm=True,
                    prequant=True,
                    y_scale=layer._q_scale,
                )
            else:
                assert attn.W_UK_T is not None  # guaranteed by _fused_qk_prep_supported
                B, N = q_nope.shape[0], q_nope.shape[1]
                L = attn.W_UK_T.shape[-1]
                ql_nope = q_nope_t.new_empty((B, N, L))
                torch.bmm(q_nope_t, attn.W_UK_T, out=ql_nope.transpose(0, 1))

        if self._identity_rope is None:
            # cos = 1, sin = 0 makes the kernel's RoPE the identity, which is
            # what a NoPE model needs. A full-length cache would cost ~268 MB at
            # K3's 1M max_position_embeddings, so it holds one row and the
            # kernel is given all-zero positions below.
            half = self.qk_rope_head_dim // 2
            opts = {"dtype": kv_c_normed.dtype, "device": kv_c_normed.device}
            self._identity_rope = (
                torch.ones(1, half, **opts),
                torch.zeros(1, half, **opts),
            )
        cos_cache, sin_cache = self._identity_rope

        # An fp8 KV cache is allocated as uint8 and re-viewed as fp8 before use;
        # the AITER kernel rejects the raw uint8 dtype outright.
        fp8_dtype = current_platform.fp8_dtype()
        if kv_cache.dtype != fp8_dtype:
            kv_cache = kv_cache.view(fp8_dtype)

        head_size = attn.kv_lora_rank + self.qk_rope_head_dim
        num_tokens, num_heads = ql_nope.shape[:2]
        q_out_padded = torch.empty(
            (num_tokens, self._fused_q_heads, head_size),
            dtype=fp8_dtype,
            device=ql_nope.device,
        )
        q_out = q_out_padded[:, :num_heads]
        # The kernel must not see the real positions: not every AITER variant
        # clamps them into the one-row cos/sin cache. The head-grouped kernel
        # (AITER v0.1.26) reads cos = sin = 0 past the cache at one head per
        # block, zeroing q_pe and the cached k_pe, and faults at more heads per
        # block. RoPE is the identity, so position 0 is exact for every token.
        # The kernel takes its token count from slot_mapping.
        slot_mapping = slot_mapping.flatten()
        num_slots = slot_mapping.shape[0]
        assert num_slots <= self._zero_positions.shape[0]
        fused_qk_rope_concat_and_cache_mla(
            ql_nope,
            q_pe,
            kv_c_normed,
            k_pe.squeeze(1),
            kv_cache.view(kv_cache.shape[0], -1, head_size),
            q_out,
            slot_mapping,
            layer._k_scale,
            layer._q_scale,
            self._zero_positions[:num_slots],
            cos_cache,
            sin_cache,
            is_neox=True,
            is_nope_first=True,
        )

        # forward_mqa is MLA-specific and not on AttentionImpl; MLAAttention
        # annotates its own call to it the same way.
        attn_out, _ = attn.impl.forward_mqa(q_out, kv_cache, attn_metadata, attn)  # type: ignore[attr-defined]
        output = ql_nope.new_empty(output_shape)
        attn._v_up_proj(attn_out, out=output)
        return output

    def forward(
        self,
        positions: torch.Tensor,
        hidden_states: torch.Tensor | QuantizedActivation,
        llama_4_scaling: torch.Tensor | None = None,
        skip_o_proj: bool = False,
    ) -> torch.Tensor:
        q_c = None
        num_tokens = (
            hidden_states.orig_shape[0]
            if isinstance(hidden_states, QuantizedActivation)
            else hidden_states.shape[0]
        )
        gate = None

        if self.q_lora_rank is not None:
            assert self.fused_qkv_a_proj is not None, (
                "fused_qkv_a_proj is required when q_lora_rank is not None"
            )
            assert self.q_a_layernorm is not None, (
                "q_a_layernorm is required when q_lora_rank is not None"
            )
            assert self.q_b_proj is not None, (
                "q_b_proj is required when q_lora_rank is not None"
            )

            qkv_lora = self.fused_qkv_a_proj(hidden_states)[0]
            if self._qkv_gate_fused:
                qkv_lora, gate = qkv_lora.split(
                    [
                        self.q_lora_rank + self.kv_lora_rank + self.qk_rope_head_dim,
                        self.num_heads * self.v_head_dim,
                    ],
                    dim=-1,
                )
            q_c, kv_lora = qkv_lora.split(
                [self.q_lora_rank, self.kv_lora_rank + self.qk_rope_head_dim],
                dim=-1,
            )
            kv_c, k_pe = kv_lora.split(
                [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
            )
            q_proj_input, kv_c_normed = self._normalize_q_kv(q_c, kv_c)
            q_proj_layer = self.q_b_proj
        else:
            assert self.kv_a_proj_with_mqa is not None, (
                "kv_a_proj_with_mqa is required when q_lora_rank is None"
            )
            assert self.q_proj is not None, (
                "q_proj is required when q_lora_rank is None"
            )
            kv_lora = self.kv_a_proj_with_mqa(hidden_states)[0]
            kv_c, k_pe = kv_lora.split(
                [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
            )
            kv_c_normed = self.kv_a_layernorm(kv_c)
            q_proj_layer = self.q_proj
            q_proj_input = hidden_states

        # Add head dim of 1 to k_pe.
        k_pe = k_pe.unsqueeze(1)

        output_shape = (num_tokens, self.num_heads * self.v_head_dim)

        attn_metadata = layer = kv_cache = slot_mapping = None
        fuse = self._fused_qk_prep and not self.dcp_q_replicate
        if fuse:
            attn_metadata, layer, kv_cache, slot_mapping = get_attention_context(
                self.mla_attn.layer_name
            )
            fuse = (
                attn_metadata is not None
                and slot_mapping is not None
                and kv_cache.numel() > 0
                and attn_metadata.num_actual_tokens == attn_metadata.num_decode_tokens
            )

        if fuse and self._fold_qb_uk and llama_4_scaling is None:
            q = q_proj_input
        else:
            q = q_proj_layer(q_proj_input)[0]
            heads = self.num_heads
            if self.dcp_q_replicate:
                heads *= q_proj_layer.group_size
            q = q.view(-1, heads, self.qk_head_dim)

            if self.rotary_emb is not None:
                q[..., self.qk_nope_head_dim :], k_pe = self.rotary_emb(
                    positions, q[..., self.qk_nope_head_dim :], k_pe
                )

            if self.indexer and self.is_sparse and not self.skip_topk:
                self.indexer(hidden_states, q_c, positions, self.indexer_rope_emb)

            if llama_4_scaling is not None:
                q *= llama_4_scaling

        q_dcp_replicated = None
        if self.dcp_q_replicate:
            q_dcp_replicated, q = q, q_proj_layer._local_view(q)

        if fuse:
            attn_out = self._fused_decode(
                q,
                kv_c_normed,
                k_pe,
                output_shape,
                attn_metadata,
                layer,
                kv_cache,
                slot_mapping,
            )
        else:
            attn_out = self.mla_attn(
                q,
                kv_c_normed,
                k_pe,
                output_shape=output_shape,
                q_dcp_replicated=q_dcp_replicated,
            )

        if self.g_proj is not None:
            gate = self.g_proj(hidden_states)[0]
        if gate is not None:
            if rocm_aiter_ops.is_enabled():
                from aiter.ops.triton.fusions.fused_sigmoid_mul import fused_sigmoid_mul

                attn_out = fused_sigmoid_mul(attn_out, gate)
            else:
                attn_out = attn_out * gate.sigmoid()

        if skip_o_proj:  # the specialized decode runs o_proj in its kernel
            return attn_out
        return self.o_proj(attn_out)[0]

_compute_w_fold(w_qb, w_uk, num_heads, qk_nope_head_dim, qk_rope_head_dim) staticmethod

W_fold = [W_UK @ W_qb_nope ; W_qb_pe]: one GEMM gives [ql_nope | q_pe].

w_qb is the q_b_proj weight [N * (P + R), Lq], w_uk is [L, N, P]. Rows hold all heads' nope rows, then all pe rows, so the two output halves of x @ W_fold.T are contiguous: [B, N * L] and [B, N * R].

Source code in vllm/models/kimi_k3/amd/mla.py
@staticmethod
def _compute_w_fold(
    w_qb: torch.Tensor,
    w_uk: torch.Tensor,
    num_heads: int,
    qk_nope_head_dim: int,
    qk_rope_head_dim: int,
) -> torch.Tensor:
    """W_fold = [W_UK @ W_qb_nope ; W_qb_pe]: one GEMM gives [ql_nope | q_pe].

    ``w_qb`` is the q_b_proj weight [N * (P + R), Lq], ``w_uk`` is [L, N, P].
    Rows hold all heads' nope rows, then all pe rows, so the two output
    halves of ``x @ W_fold.T`` are contiguous: [B, N * L] and [B, N * R]."""
    N, P, R = num_heads, qk_nope_head_dim, qk_rope_head_dim
    L = w_uk.shape[0]
    w = w_qb.view(N, P + R, -1).float()
    nope = torch.einsum("lnp,npk->nlk", w_uk.float(), w[:, :P])  # [N, L, Lq]
    return (
        torch.cat([nope.reshape(N * L, -1), w[:, P:].reshape(N * R, -1)])
        .to(w_qb.dtype)
        .contiguous()
    )

_fused_decode(q, kv_c_normed, k_pe, output_shape, attn_metadata, layer, kv_cache, slot_mapping)

Decode-only MLA through one fused AITER launch.

Replaces the fp8 KV-cache write plus the [ql_nope | q_pe] concat and that query's static fp8 quant with a single fused_qk_rope_concat_and_cache_mla. On an untouched upstream build the concat and quant already arrive as one inductor-fused kernel, so this collapses two launches into one.

Source code in vllm/models/kimi_k3/amd/mla.py
def _fused_decode(
    self,
    q: torch.Tensor,
    kv_c_normed: torch.Tensor,
    k_pe: torch.Tensor,
    output_shape: tuple[int, int],
    attn_metadata,
    layer,
    kv_cache: torch.Tensor,
    slot_mapping: torch.Tensor,
) -> torch.Tensor:
    """Decode-only MLA through one fused AITER launch.

    Replaces the fp8 KV-cache write plus the ``[ql_nope | q_pe]`` concat and
    that query's static fp8 quant with a single
    ``fused_qk_rope_concat_and_cache_mla``. On an untouched upstream build
    the concat and quant already arrive as one inductor-fused kernel, so
    this collapses two launches into one.
    """
    from aiter.ops.cache import fused_qk_rope_concat_and_cache_mla

    attn = self.mla_attn
    if q.dim() == 2:
        from aiter.ops.triton.gemm.basic.gemm_a16w16 import gemm_a16w16

        assert self._w_fold is not None
        y = gemm_a16w16(q, self._w_fold)
        ql_nope, q_pe = self._split_fold_output(
            y, self.num_heads, self.kv_lora_rank, self.qk_rope_head_dim
        )
    else:
        q_nope, q_pe = q.split(
            [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1
        )
        q_nope_t = q_nope.transpose(0, 1)

        if attn.is_aiter_triton_fp4_bmm_enabled:
            from aiter.ops.triton.batched_gemm_a16wfp4 import (
                batched_gemm_a16wfp4,
            )

            ql_nope = batched_gemm_a16wfp4(
                q_nope_t,
                attn.W_K,
                attn.W_K_scale,
                transpose_bm=True,
                prequant=True,
                y_scale=layer._q_scale,
            )
        else:
            assert attn.W_UK_T is not None  # guaranteed by _fused_qk_prep_supported
            B, N = q_nope.shape[0], q_nope.shape[1]
            L = attn.W_UK_T.shape[-1]
            ql_nope = q_nope_t.new_empty((B, N, L))
            torch.bmm(q_nope_t, attn.W_UK_T, out=ql_nope.transpose(0, 1))

    if self._identity_rope is None:
        # cos = 1, sin = 0 makes the kernel's RoPE the identity, which is
        # what a NoPE model needs. A full-length cache would cost ~268 MB at
        # K3's 1M max_position_embeddings, so it holds one row and the
        # kernel is given all-zero positions below.
        half = self.qk_rope_head_dim // 2
        opts = {"dtype": kv_c_normed.dtype, "device": kv_c_normed.device}
        self._identity_rope = (
            torch.ones(1, half, **opts),
            torch.zeros(1, half, **opts),
        )
    cos_cache, sin_cache = self._identity_rope

    # An fp8 KV cache is allocated as uint8 and re-viewed as fp8 before use;
    # the AITER kernel rejects the raw uint8 dtype outright.
    fp8_dtype = current_platform.fp8_dtype()
    if kv_cache.dtype != fp8_dtype:
        kv_cache = kv_cache.view(fp8_dtype)

    head_size = attn.kv_lora_rank + self.qk_rope_head_dim
    num_tokens, num_heads = ql_nope.shape[:2]
    q_out_padded = torch.empty(
        (num_tokens, self._fused_q_heads, head_size),
        dtype=fp8_dtype,
        device=ql_nope.device,
    )
    q_out = q_out_padded[:, :num_heads]
    # The kernel must not see the real positions: not every AITER variant
    # clamps them into the one-row cos/sin cache. The head-grouped kernel
    # (AITER v0.1.26) reads cos = sin = 0 past the cache at one head per
    # block, zeroing q_pe and the cached k_pe, and faults at more heads per
    # block. RoPE is the identity, so position 0 is exact for every token.
    # The kernel takes its token count from slot_mapping.
    slot_mapping = slot_mapping.flatten()
    num_slots = slot_mapping.shape[0]
    assert num_slots <= self._zero_positions.shape[0]
    fused_qk_rope_concat_and_cache_mla(
        ql_nope,
        q_pe,
        kv_c_normed,
        k_pe.squeeze(1),
        kv_cache.view(kv_cache.shape[0], -1, head_size),
        q_out,
        slot_mapping,
        layer._k_scale,
        layer._q_scale,
        self._zero_positions[:num_slots],
        cos_cache,
        sin_cache,
        is_neox=True,
        is_nope_first=True,
    )

    # forward_mqa is MLA-specific and not on AttentionImpl; MLAAttention
    # annotates its own call to it the same way.
    attn_out, _ = attn.impl.forward_mqa(q_out, kv_cache, attn_metadata, attn)  # type: ignore[attr-defined]
    output = ql_nope.new_empty(output_shape)
    attn._v_up_proj(attn_out, out=output)
    return output