main
py 792 lines 32.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """GLM-5.3-Flash KDA layer with separate convolutions and a bounded safe gate."""
4
5 from dataclasses import replace
6
7 import torch
8 from torch import nn
9 from transformers import Glm5NextTextConfig
10
11 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
12 from vllm.config import VllmConfig, get_current_vllm_config
13 from vllm.distributed import divide
14 from vllm.forward_context import get_forward_context
15 from vllm.model_executor.layers.linear import (
16 ColumnParallelLinear,
17 MergedColumnParallelLinear,
18 RowParallelLinear,
19 )
20 from vllm.model_executor.layers.mamba.checkpoint import (
21 MambaPrefillCheckpointMetadata,
22 )
23 from vllm.model_executor.layers.mamba.gdn.base import GatedDeltaNetAttention
24 from vllm.model_executor.layers.mamba.kda_checkpoint import (
25 FlashKDAPrefillCheckpointExporter,
26 kda_prefill_checkpoint_alignment,
27 )
28 from vllm.model_executor.layers.mamba.mamba_utils import (
29 MambaStateDtypeCalculator,
30 MambaStateShapeCalculator,
31 is_conv_state_dim_first,
32 )
33 from vllm.model_executor.layers.mamba.ops.causal_conv1d import (
34 causal_conv1d_fn,
35 causal_conv1d_update,
36 )
37 from vllm.model_executor.layers.mamba.ops.gather_initial_states import (
38 gather_initial_states,
39 )
40 from vllm.model_executor.layers.mamba.ops.scatter_states import scatter_states
41 from vllm.model_executor.model_loader.weight_utils import sharded_weight_loader
42 from vllm.model_executor.utils import (
43 maybe_disable_graph_partition,
44 set_weight_attrs,
45 )
46 from vllm.platforms import current_platform
47 from vllm.third_party.flash_linear_attention.ops.kda import FusedRMSNormGated
48 from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
49 from vllm.v1.kv_cache_interface import MambaSpec
50 from vllm.v1.worker.workspace import current_workspace_manager
51
52 if current_platform.is_rocm():
53 from vllm.models.glm5next.amd.ops.third_party.kda import (
54 chunk_kda_with_fused_gate,
55 fused_recurrent_kda,
56 )
57 else:
58 from vllm.models.glm5next.nvidia.ops.third_party.kda import (
59 chunk_kda_with_fused_gate,
60 fused_recurrent_kda,
61 )
62
63
64 class _Glm5NextMergedColumnParallelLinear(MergedColumnParallelLinear):
65 """Merged projection with multiple replicated output shards.
66
67 Extends K3's ``_KimiGDNMergedColumnParallelLinear`` to support two
68 replicated shards (f_a, g_a) instead of one. Pre-multiplies each
69 replicated entry's output_size by tp_size so the per-rank shard
70 divides back to the full size, and forces tp_rank=0 during weight
71 loading for replicated shards.
72 """
73
74 def __init__(
75 self,
76 input_size: int,
77 output_sizes: list[int],
78 replicated_shard_ids: tuple[int, ...],
79 tp_size: int,
80 **kwargs,
81 ) -> None:
82 self.replicated_shard_ids = set(replicated_shard_ids)
83 output_sizes = output_sizes.copy()
84 for sid in self.replicated_shard_ids:
85 output_sizes[sid] *= tp_size
86 super().__init__(input_size, output_sizes, **kwargs)
87
88 def weight_loader(
89 self,
90 param: nn.Parameter,
91 loaded_weight: torch.Tensor,
92 loaded_shard_id: tuple[int, ...] | int | None = None,
93 ) -> None:
94 tp_rank = self.tp_rank
95 param_tp_rank = getattr(param, "tp_rank", None)
96 if loaded_shard_id in self.replicated_shard_ids:
97 self.tp_rank = 0
98 if param_tp_rank is not None:
99 param.tp_rank = 0
100 try:
101 super().weight_loader(param, loaded_weight, loaded_shard_id)
102 finally:
103 self.tp_rank = tp_rank
104 if param_tp_rank is not None:
105 param.tp_rank = param_tp_rank
106
107 def weight_loader_v2(
108 self,
109 param: nn.Parameter,
110 loaded_weight: torch.Tensor,
111 loaded_shard_id: tuple[int, ...] | int | None = None,
112 ) -> None:
113 tp_rank = self.tp_rank
114 param_tp_rank = getattr(param, "tp_rank", None)
115 if loaded_shard_id in self.replicated_shard_ids:
116 self.tp_rank = 0
117 if param_tp_rank is not None:
118 param.tp_rank = 0
119 try:
120 super().weight_loader_v2(param, loaded_weight, loaded_shard_id)
121 finally:
122 self.tp_rank = tp_rank
123 if param_tp_rank is not None:
124 param.tp_rank = param_tp_rank
125
126
127 @torch.compile(
128 dynamic=True,
129 backend=current_platform.simple_compile_backend,
130 options=maybe_disable_graph_partition(current_platform.simple_compile_backend),
131 )
132 def _cast_sigmoid(x: torch.Tensor) -> torch.Tensor:
133 """Fuse the fp32 cast + sigmoid into one Inductor kernel."""
134 return x.float().sigmoid()
135
136
137 def _resolve_kda_prefill_backend(
138 backend: str, head_dim: int, dtype: torch.dtype, lower_bound: float | None
139 ) -> str:
140 """Pick the chunked-prefill kernel: FlashKDA (fused CUDA, ~2-4x faster on
141 SM90/SM10x/SM12x for bf16, head_dim 128 and a bounded gate) or the Triton
142 ``chunk_kda_with_fused_gate`` path. ``backend`` comes from
143 ``additional_config.kda_prefill_backend`` (auto / triton / flashkda)."""
144 if backend not in ("auto", "triton", "flashkda"):
145 raise ValueError(f"Unsupported KDA prefill backend: {backend}")
146 capability = current_platform.get_device_capability()
147 supported = (
148 current_platform.is_cuda()
149 and capability is not None
150 and capability.major in (9, 10, 12)
151 and head_dim == 128
152 and dtype == torch.bfloat16
153 and lower_bound is not None
154 )
155 if backend == "flashkda" and not supported:
156 raise RuntimeError(
157 "FlashKDA requires CUDA SM90/SM10x/SM12x, bfloat16, head_dim=128 "
158 "and a bounded KDA gate."
159 )
160 return "flashkda" if supported and backend != "triton" else "triton"
161
162
163 class Glm5NextLinearAttention(GatedDeltaNetAttention):
164 head_dim: int
165 num_heads: int
166 conv_size: int
167
168 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> MambaSpec:
169 spec = super().get_kv_cache_spec(vllm_config)
170 assert isinstance(spec, MambaSpec)
171 alignment = kda_prefill_checkpoint_alignment(self.kda_prefill_backend)
172 return replace(
173 spec,
174 num_prefill_checkpoint_blocks=int(alignment is not None),
175 prefill_checkpoint_alignment=alignment,
176 )
177
178 def get_state_dtype(
179 self,
180 ) -> tuple[torch.dtype, torch.dtype]:
181 if self.model_config is None or self.cache_config is None:
182 raise ValueError("model_config and cache_config must be set")
183 return MambaStateDtypeCalculator.kda_state_dtype(
184 self.model_config.dtype, self.cache_config.mamba_cache_dtype
185 )
186
187 def get_state_shape(
188 self,
189 ) -> tuple[tuple[int, ...], tuple[int, ...]]:
190 # conv_state width must include num_spec so the spec-decode conv update
191 # (causal_conv1d_update with num_accepted_tokens + max_query_len) can
192 # slide the window across the draft-verify tokens without reading past
193 # the allocated width. Matches qwen_gdn_linear_attn.get_state_shape.
194 return MambaStateShapeCalculator.kda_state_shape(
195 self.tp_size,
196 self.num_heads,
197 self.head_dim,
198 conv_kernel_size=self.conv_size,
199 num_spec=self.num_spec,
200 )
201
202 def __init__(
203 self,
204 config: Glm5NextTextConfig,
205 vllm_config: VllmConfig,
206 prefix: str = "",
207 ) -> None:
208 # KDA projections remain BF16 because fp8 checkpoints omit their scales.
209 saved_quant_config = vllm_config.quant_config
210 try:
211 vllm_config.quant_config = None
212 super().__init__(config, vllm_config, prefix)
213 finally:
214 vllm_config.quant_config = saved_quant_config
215
216 self.head_dim = config.linear_head_dim
217 self.num_heads = config.linear_num_heads
218 self.conv_size = config.linear_conv_kernel_dim
219 assert self.num_heads % self.tp_size == 0
220 self.local_num_heads = divide(self.num_heads, self.tp_size)
221
222 projection_size = self.head_dim * self.num_heads
223 self.local_projection_size = divide(projection_size, self.tp_size)
224
225 _, recurrent_state_shape = self.get_state_shape()
226 _, recurrent_state_dtype = self.get_state_dtype()
227 scatter_states.register_warmup(
228 state_shape=recurrent_state_shape,
229 state_dtype=recurrent_state_dtype,
230 indices_dtype=torch.int32,
231 max_num_tokens=vllm_config.scheduler_config.max_num_seqs,
232 )
233
234 # Merge q, k, v, b, f_a, g_a projections into one GEMM (6→1 launches).
235 # Order matches checkpoint's fused_qkvbfg_a_proj convention.
236 # Shards 4 (f_a) and 5 (g_a) are replicated across TP ranks.
237 self.in_proj_qkvbfg_a = _Glm5NextMergedColumnParallelLinear(
238 self.hidden_size,
239 [
240 projection_size, # q (shard 0)
241 projection_size, # k (shard 1)
242 projection_size, # v (shard 2)
243 self.num_heads, # b (shard 3)
244 self.head_dim, # f_a (shard 4, replicated)
245 self.head_dim, # g_a (shard 5, replicated)
246 ],
247 replicated_shard_ids=(4, 5),
248 tp_size=self.tp_size,
249 bias=False,
250 quant_config=self.quant_config,
251 prefix=f"{prefix}.in_proj_qkvbfg_a",
252 )
253
254 self.f_b_proj = ColumnParallelLinear(
255 self.head_dim,
256 projection_size,
257 bias=False,
258 quant_config=self.quant_config,
259 prefix=f"{prefix}.f_b_proj",
260 )
261 self.dt_bias = nn.Parameter(
262 torch.empty(divide(projection_size, self.tp_size), dtype=torch.float32)
263 )
264
265 set_weight_attrs(self.dt_bias, {"weight_loader": sharded_weight_loader(0)})
266
267 self.q_conv1d = ColumnParallelLinear(
268 input_size=self.conv_size,
269 output_size=projection_size,
270 bias=False,
271 params_dtype=torch.float32,
272 prefix=f"{prefix}.q_conv1d",
273 )
274 self.k_conv1d = ColumnParallelLinear(
275 input_size=self.conv_size,
276 output_size=projection_size,
277 bias=False,
278 params_dtype=torch.float32,
279 prefix=f"{prefix}.k_conv1d",
280 )
281 self.v_conv1d = ColumnParallelLinear(
282 input_size=self.conv_size,
283 output_size=projection_size,
284 bias=False,
285 params_dtype=torch.float32,
286 prefix=f"{prefix}.v_conv1d",
287 )
288 # unsqueeze to fit conv1d weights shape into the linear weights shape.
289 # Can't do this in `weight_loader` since it already exists in
290 # `ColumnParallelLinear` and `set_weight_attrs`
291 # doesn't allow to override it
292 self.q_conv1d.weight.data = self.q_conv1d.weight.data.unsqueeze(1)
293 self.k_conv1d.weight.data = self.k_conv1d.weight.data.unsqueeze(1)
294 self.v_conv1d.weight.data = self.v_conv1d.weight.data.unsqueeze(1)
295 # Lazily-built merged q|k|v conv weight (built on first forward, after
296 # weights are loaded). See _forward.
297 self._merged_conv_weight: torch.Tensor | None = None
298
299 self.A_log = nn.Parameter(
300 torch.empty(1, 1, self.local_num_heads, 1, dtype=torch.float32)
301 )
302 set_weight_attrs(self.A_log, {"weight_loader": sharded_weight_loader(2)})
303
304 self.g_b_proj = ColumnParallelLinear(
305 self.head_dim,
306 projection_size,
307 bias=False,
308 quant_config=self.quant_config,
309 prefix=f"{prefix}.g_b_proj",
310 )
311 self.o_norm = FusedRMSNormGated(self.head_dim, activation="sigmoid")
312 self.o_proj = RowParallelLinear(
313 projection_size,
314 self.hidden_size,
315 bias=False,
316 quant_config=self.quant_config,
317 prefix=f"{prefix}.o_proj",
318 )
319
320 compilation_config = get_current_vllm_config().compilation_config
321 if prefix in compilation_config.static_forward_context:
322 raise ValueError(f"Duplicate layer name: {prefix}")
323 compilation_config.static_forward_context[prefix] = self
324
325 # Checkpoints store A_log as 1-D; the model parameter is 4-D.
326 def _a_log_weight_loader(param, loaded_weight):
327 if loaded_weight.dim() == 1:
328 loaded_weight = loaded_weight.view([1, 1, -1, 1])
329 return sharded_weight_loader(2)(param, loaded_weight)
330
331 self.A_log.weight_loader = _a_log_weight_loader
332
333 # GLM-5.3-Flash uses a bounded sigmoid gate instead of the default
334 # unbounded softplus gate.
335 self.kda_safe_gate = True
336 self.kda_lower_bound = config.linear_lower_bound
337 # Process-global conv-state layout, resolved once here instead of on
338 # every _forward call (it reads an env-derived flag each time).
339 self._conv_state_dim_first = is_conv_state_dim_first()
340
341 additional_config = vllm_config.additional_config
342 self.kda_prefill_backend = _resolve_kda_prefill_backend(
343 additional_config.get("kda_prefill_backend", "auto")
344 if isinstance(additional_config, dict)
345 else "auto",
346 self.head_dim,
347 vllm_config.model_config.dtype,
348 self.kda_lower_bound,
349 )
350 self._flashkda_buffer_specs: (
351 tuple[tuple[tuple[int, ...], torch.dtype], ...] | None
352 ) = None
353 self._checkpoint_exporter: FlashKDAPrefillCheckpointExporter | None = None
354 if self.kda_prefill_backend == "flashkda":
355 import vllm._flashkda_C # noqa: F401
356
357 max_tokens = vllm_config.scheduler_config.max_num_batched_tokens
358 max_seqs = vllm_config.scheduler_config.max_num_seqs
359 workspace_size = torch.ops._flashkda_C.get_workspace_size(
360 max_tokens, self.local_num_heads, max_seqs
361 )
362 self._flashkda_buffer_specs = (
363 (
364 (max_seqs, self.local_num_heads, self.head_dim, self.head_dim),
365 self.get_state_dtype()[1],
366 ),
367 (
368 (max_seqs, self.local_num_heads, self.head_dim, self.head_dim),
369 self.get_state_dtype()[1],
370 ),
371 ((workspace_size,), torch.uint8),
372 # Output buffer for steps that also carry spec-decode tokens:
373 # the non-spec tokens are then scattered by non_spec_token_indx.
374 (
375 (1, max_tokens, self.local_num_heads, self.head_dim),
376 vllm_config.model_config.dtype,
377 ),
378 )
379 self._checkpoint_exporter = FlashKDAPrefillCheckpointExporter(
380 state_len=self.conv_size - 1
381 )
382
383 def _flashkda_prefill(
384 self,
385 q: torch.Tensor,
386 k: torch.Tensor,
387 v: torch.Tensor,
388 g: torch.Tensor,
389 beta: torch.Tensor,
390 initial_state: torch.Tensor,
391 cu_seqlens: torch.Tensor,
392 out: torch.Tensor | None,
393 checkpoint: MambaPrefillCheckpointMetadata | None = None,
394 raw_qkv: torch.Tensor | None = None,
395 conv_state: torch.Tensor | None = None,
396 recurrent_state: torch.Tensor | None = None,
397 ) -> tuple[torch.Tensor, torch.Tensor]:
398 """Fused KDA chunked prefill (FlashKDA). Takes the raw gate logits ``g``
399 and raw ``beta`` logits, l2-normalizes q/k in-kernel and applies the
400 bounded gate ``lower_bound * sigmoid(exp(A_log) * (g + dt_bias))``,
401 matching ``chunk_kda_with_fused_gate(..., safe_gate=True)``. Writes the
402 attention output into ``out`` (a workspace buffer when ``None``) and
403 returns ``(out, final_state)``."""
404 assert self._flashkda_buffer_specs is not None
405 final_state, checkpoint_state, workspace, workspace_out = (
406 current_workspace_manager().get_simultaneous(*self._flashkda_buffer_specs)
407 )
408 final_state = final_state[: initial_state.shape[0]]
409 checkpoint_state = (
410 checkpoint_state[: initial_state.shape[0]]
411 if checkpoint is not None
412 else None
413 )
414 if out is None:
415 out = workspace_out[:, : q.shape[1]]
416 # FlashKDA hardcodes dense q/k/v/g strides; beta may be row-strided.
417 torch.ops._flashkda_C.fwd(
418 q.contiguous(),
419 k.contiguous(),
420 v.contiguous(),
421 g.contiguous(),
422 beta,
423 self.head_dim**-0.5,
424 out,
425 workspace,
426 self.A_log.view(-1),
427 self.dt_bias.view(-1, self.head_dim),
428 self.kda_lower_bound,
429 initial_state.contiguous(),
430 final_state,
431 cu_seqlens.contiguous(),
432 checkpoint_state,
433 checkpoint.checkpoint_offsets.contiguous()
434 if checkpoint is not None
435 else None,
436 )
437 if checkpoint is not None:
438 assert self._checkpoint_exporter is not None
439 assert raw_qkv is not None and conv_state is not None
440 assert recurrent_state is not None
441 self._checkpoint_exporter.export(
442 checkpoint,
443 raw_qkv=raw_qkv,
444 conv_state=conv_state,
445 recurrent_checkpoint=checkpoint_state,
446 recurrent_state=recurrent_state,
447 cu_seqlens=cu_seqlens,
448 )
449 return out, final_state
450
451 def forward(
452 self,
453 hidden_states: torch.Tensor,
454 positions: torch.Tensor,
455 ) -> torch.Tensor:
456 num_tokens = hidden_states.size(0)
457 # One merged GEMM for q, k, v, b, f_a, g_a (replaces 6 separate GEMMs).
458 projected = self.in_proj_qkvbfg_a(hidden_states)[0]
459 qkv, beta_raw, f_a, g_a = projected.split(
460 [
461 3 * self.local_projection_size,
462 self.local_num_heads,
463 self.head_dim,
464 self.head_dim,
465 ],
466 dim=-1,
467 )
468
469 # Beta stays raw (bf16) here: the recurrent kernel sigmoids it in fp32
470 # at load (SIGMOID_BETA), and only the chunked prefill path needs the
471 # pre-computed fp32 sigmoid — computed lazily in _forward. Pure decode
472 # / spec-verify steps then skip the _cast_sigmoid kernel and its fp32
473 # intermediate entirely.
474 beta = beta_raw.unsqueeze(0)
475 g1 = self.f_b_proj(f_a)[0]
476 g1 = g1.reshape(1, -1, self.local_num_heads, self.head_dim)
477
478 g_proj_states = self.g_b_proj(g_a)[0]
479 # Must stay 3D: rms_norm_gated reads H from g.shape[-2].
480 g2 = g_proj_states.reshape(-1, self.local_num_heads, self.head_dim)
481
482 core_attn_out = torch.empty(
483 (1, num_tokens, self.local_num_heads, self.head_dim),
484 dtype=hidden_states.dtype,
485 device=hidden_states.device,
486 )
487 # Call the decorated eager break directly so host-side prefill branches
488 # are not captured by PIECEWISE CUDA graphs.
489 self._forward(
490 qkv_proj_states=qkv,
491 g1=g1,
492 beta=beta,
493 core_attn_out=core_attn_out,
494 )
495 core_attn_out = self.o_norm(core_attn_out, g2)
496 core_attn_out = core_attn_out.reshape(core_attn_out.size(1), -1)
497 return self.o_proj(core_attn_out)[0]
498
499 @eager_break_during_capture
500 def _forward(
501 self,
502 qkv_proj_states: torch.Tensor,
503 g1: torch.Tensor,
504 beta: torch.Tensor,
505 core_attn_out: torch.Tensor,
506 ) -> None:
507 forward_context = get_forward_context()
508 attn_metadata_raw = forward_context.attn_metadata
509
510 if attn_metadata_raw is None:
511 return
512
513 assert isinstance(attn_metadata_raw, dict)
514 attn_metadata_narrowed = attn_metadata_raw.get(self.prefix)
515 if attn_metadata_narrowed is None:
516 # Profile/warmup dummy runs may omit mamba-family metadata.
517 return
518 assert isinstance(attn_metadata_narrowed, GDNAttentionMetadata)
519 has_initial_state = attn_metadata_narrowed.has_initial_state
520 non_spec_query_start_loc = attn_metadata_narrowed.non_spec_query_start_loc
521 non_spec_state_indices_tensor = (
522 attn_metadata_narrowed.non_spec_state_indices_tensor
523 ) # noqa: E501
524 num_actual_tokens = attn_metadata_narrowed.num_actual_tokens
525 # Spec-decode metadata (all None when speculative decoding is disabled).
526 spec_sequence_masks = attn_metadata_narrowed.spec_sequence_masks
527 spec_query_start_loc = attn_metadata_narrowed.spec_query_start_loc
528 spec_state_indices_tensor = attn_metadata_narrowed.spec_state_indices_tensor
529 spec_token_indx = attn_metadata_narrowed.spec_token_indx
530 non_spec_token_indx = attn_metadata_narrowed.non_spec_token_indx
531 num_accepted_tokens = attn_metadata_narrowed.num_accepted_tokens
532 num_spec_decodes = attn_metadata_narrowed.num_spec_decodes
533 use_spec = spec_sequence_masks is not None and num_spec_decodes > 0
534 # Safe-gate checkpoints use the bounded sigmoid variant.
535 safe_gate = self.kda_safe_gate
536 lower_bound = self.kda_lower_bound
537 constant_caches = self.kv_cache
538
539 qkv_proj_states = qkv_proj_states[:num_actual_tokens]
540 g1 = g1[:, :num_actual_tokens]
541 beta = beta[:, :num_actual_tokens]
542
543 (conv_state, recurrent_state) = constant_caches
544 # conv_state must be (..., dim, width-1) for the conv kernels.
545 # DS layout stores it that way directly; SD layout needs a transpose.
546 # Layout is process-global and resolved once at init (see __init__).
547 if not self._conv_state_dim_first:
548 conv_state = conv_state.transpose(-1, -2)
549
550 # One merged short-conv over q|k|v instead of three separate calls. The
551 # 1D conv is independent per channel, so concatenating q/k/v along the
552 # channel dim and running a single causal_conv1d is bit-identical to
553 # three calls. The merged weight is q|k|v conv weights concatenated;
554 # built once and cached (params are fixed after load). conv_state is
555 # already stored as the merged q|k|v state, so it is used directly.
556 if self._merged_conv_weight is None:
557
558 def _w(m):
559 return m.weight.view(m.weight.size(0), m.weight.size(2))
560
561 self._merged_conv_weight = torch.cat(
562 [_w(self.q_conv1d), _w(self.k_conv1d), _w(self.v_conv1d)],
563 dim=0,
564 ).contiguous()
565 conv_weights = self._merged_conv_weight
566 conv_bias = self.q_conv1d.bias
567
568 # Split projections / gating into spec (draft-verify) and non-spec token
569 # groups when speculative decoding is active. Spec tokens carry
570 # num_spec+1 recurrent-state columns each and are advanced with
571 # num_accepted_tokens for rejection-sampling rollback; non-spec tokens
572 # are one-per-request. Mirrors olmo_gdn_linear_attn.py. Projections are
573 # [n, *] (token dim 0); g1/beta are [1, n, h, d] (token dim 1).
574 if use_spec:
575 # In a pure spec-verify step (no non-spec tokens) the metadata
576 # builder sets spec_token_indx = arange(num_actual_tokens), making
577 # the index_select calls below identity copies. Skip them on this
578 # steady-state decode hot path. The outputs alias the inputs here;
579 # the downstream conv/recurrent kernels read them without mutating
580 # in place, so the aliasing is safe.
581 if non_spec_token_indx is None or non_spec_token_indx.numel() == 0:
582 qkv_spec = qkv_proj_states
583 g1_spec = g1
584 beta_spec = beta
585 else:
586 qkv_spec = qkv_proj_states.index_select(0, spec_token_indx)
587 g1_spec = g1.index_select(1, spec_token_indx)
588 beta_spec = beta.index_select(1, spec_token_indx)
589 if non_spec_token_indx is not None and non_spec_token_indx.numel() > 0:
590 qkv_ns = qkv_proj_states.index_select(0, non_spec_token_indx)
591 g1_ns = g1.index_select(1, non_spec_token_indx)
592 beta_ns = beta.index_select(1, non_spec_token_indx)
593 else:
594 qkv_ns = g1_ns = beta_ns = None
595 else:
596 qkv_spec = g1_spec = beta_spec = None
597 qkv_ns, g1_ns, beta_ns = qkv_proj_states, g1, beta
598
599 # --- causal conv1d: spec (draft-verify) path ---
600 if use_spec:
601 assert spec_state_indices_tensor is not None
602 assert num_accepted_tokens is not None
603 conv_idx = spec_state_indices_tensor[:, 0][:num_spec_decodes]
604 conv_mql = spec_state_indices_tensor.size(-1)
605 qkv_spec = causal_conv1d_update(
606 qkv_spec,
607 conv_state,
608 conv_weights,
609 conv_bias,
610 activation="silu",
611 conv_state_indices=conv_idx,
612 num_accepted_tokens=num_accepted_tokens,
613 query_start_loc=spec_query_start_loc,
614 max_query_len=conv_mql,
615 )
616 q_spec, k_spec, v_spec = qkv_spec.split(self.local_projection_size, dim=-1)
617
618 # --- causal conv1d: non-spec path (prefill or plain decode) ---
619 raw_qkv_ns = qkv_ns
620 q_ns = k_ns = v_ns = None
621 if attn_metadata_narrowed.num_prefills > 0:
622 assert qkv_ns is not None
623 qkv_ns = causal_conv1d_fn(
624 qkv_ns.transpose(0, 1),
625 conv_weights,
626 conv_bias,
627 activation="silu",
628 conv_states=conv_state,
629 has_initial_state=has_initial_state,
630 cache_indices=non_spec_state_indices_tensor,
631 query_start_loc=non_spec_query_start_loc,
632 metadata=attn_metadata_narrowed,
633 ).transpose(0, 1)
634 q_ns, k_ns, v_ns = qkv_ns.split(self.local_projection_size, dim=-1)
635 elif attn_metadata_narrowed.num_decodes > 0:
636 assert non_spec_state_indices_tensor is not None
637 decode_conv_indices = non_spec_state_indices_tensor[
638 : attn_metadata_narrowed.num_decodes
639 ]
640 qkv_ns = causal_conv1d_update(
641 qkv_ns,
642 conv_state,
643 conv_weights,
644 conv_bias,
645 activation="silu",
646 conv_state_indices=decode_conv_indices,
647 )
648 q_ns, k_ns, v_ns = qkv_ns.split(self.local_projection_size, dim=-1)
649
650 def _rearr(x):
651 return x.reshape(1, -1, self.local_num_heads, self.head_dim)
652
653 # --- core attention: spec (draft-verify) path ---
654 core_attn_out_spec = None
655 # In a pure spec-verify step (no non-spec tokens) the recurrent kernel
656 # can write straight into the layer output buffer, skipping the
657 # fresh allocation + copy below. Mixed steps must scatter via
658 # spec_token_indx, so they keep the kernel-managed output.
659 spec_out = (
660 core_attn_out[0, :num_actual_tokens].unsqueeze(0)
661 if non_spec_token_indx is None or non_spec_token_indx.numel() == 0
662 else None
663 )
664 if use_spec:
665 assert spec_state_indices_tensor is not None
666 assert num_accepted_tokens is not None
667 assert spec_query_start_loc is not None
668 # Gate computed inside the recurrent kernel (COMPUTE_GATE) from
669 # raw g1 — replicates fused_kda_gate's arithmetic bit-for-bit and
670 # skips its launch + fp32 [n, H, D] intermediate per layer.
671 core_attn_out_spec, _ = fused_recurrent_kda(
672 q=_rearr(q_spec),
673 k=_rearr(k_spec),
674 v=_rearr(v_spec),
675 g=g1_spec,
676 beta=beta_spec,
677 initial_state=recurrent_state,
678 use_qk_l2norm_in_kernel=True,
679 cu_seqlens=spec_query_start_loc[: num_spec_decodes + 1],
680 ssm_state_indices=spec_state_indices_tensor,
681 num_accepted_tokens=num_accepted_tokens,
682 out=spec_out,
683 sigmoid_beta=True,
684 a_log=self.A_log,
685 g_bias=self.dt_bias,
686 compute_gate=True,
687 lower_bound=lower_bound,
688 )
689
690 # --- core attention: non-spec path (prefill or plain decode) ---
691 core_attn_out_non_spec = None
692 # Only the plain-decode recurrent kernel can write straight into the
693 # layer output buffer; the chunked prefill kernel cannot, so this
694 # stays None there and the merge copy below runs as before.
695 ns_out = None
696 if attn_metadata_narrowed.num_prefills > 0:
697 assert q_ns is not None
698 assert non_spec_state_indices_tensor is not None
699 assert has_initial_state is not None
700 initial_state = gather_initial_states(
701 recurrent_state, non_spec_state_indices_tensor, has_initial_state
702 )
703 if self.kda_prefill_backend == "flashkda":
704 # Non-spec step: write straight into the layer output buffer
705 # (dense token order, no merge copy). Step with spec-decode
706 # tokens: write to the workspace buffer and scatter below.
707 ns_out = None if use_spec else core_attn_out[:, :num_actual_tokens]
708 core_attn_out_non_spec, last_recurrent_state = self._flashkda_prefill(
709 q=_rearr(q_ns),
710 k=_rearr(k_ns),
711 v=_rearr(v_ns),
712 g=g1_ns,
713 beta=beta_ns,
714 initial_state=initial_state,
715 cu_seqlens=non_spec_query_start_loc,
716 out=ns_out,
717 checkpoint=attn_metadata_narrowed.checkpoint,
718 raw_qkv=raw_qkv_ns,
719 conv_state=conv_state,
720 recurrent_state=recurrent_state,
721 )
722 else:
723 (
724 core_attn_out_non_spec,
725 last_recurrent_state,
726 ) = chunk_kda_with_fused_gate(
727 q=_rearr(q_ns),
728 k=_rearr(k_ns),
729 v=_rearr(v_ns),
730 raw_g=g1_ns,
731 # Chunk path wants the pre-sigmoided fp32 beta (its
732 # kernels don't sigmoid); beta_ns is raw bf16 from forward.
733 beta=_cast_sigmoid(beta_ns.squeeze(0)).unsqueeze(0),
734 A_log=self.A_log,
735 g_bias=self.dt_bias,
736 initial_state=initial_state,
737 output_final_state=True,
738 use_qk_l2norm_in_kernel=True,
739 cu_seqlens=non_spec_query_start_loc,
740 safe_gate=safe_gate,
741 lower_bound=lower_bound,
742 )
743 # Init cache
744 scatter_states(
745 recurrent_state,
746 last_recurrent_state,
747 non_spec_state_indices_tensor,
748 )
749 elif attn_metadata_narrowed.num_decodes > 0:
750 assert non_spec_query_start_loc is not None
751 assert non_spec_state_indices_tensor is not None
752 # Plain decode step (no spec tokens): token order is dense, so the
753 # kernel can write straight into the layer output buffer. A mixed
754 # step scatters non-spec output via non_spec_token_indx instead.
755 # Gate computed in-kernel (COMPUTE_GATE), beta sigmoided in-kernel.
756 if not use_spec:
757 ns_out = spec_out
758 core_attn_out_non_spec, _ = fused_recurrent_kda(
759 q=_rearr(q_ns),
760 k=_rearr(k_ns),
761 v=_rearr(v_ns),
762 g=g1_ns,
763 beta=beta_ns,
764 initial_state=recurrent_state,
765 use_qk_l2norm_in_kernel=True,
766 cu_seqlens=non_spec_query_start_loc[
767 : attn_metadata_narrowed.num_decodes + 1
768 ],
769 ssm_state_indices=non_spec_state_indices_tensor,
770 out=ns_out,
771 sigmoid_beta=True,
772 a_log=self.A_log,
773 g_bias=self.dt_bias,
774 compute_gate=True,
775 lower_bound=lower_bound,
776 )
777
778 # --- merge spec / non-spec outputs back into token order ---
779 if use_spec and core_attn_out_non_spec is not None:
780 assert core_attn_out_spec is not None
781 core_attn_out.index_copy_(1, spec_token_indx, core_attn_out_spec)
782 core_attn_out.index_copy_(1, non_spec_token_indx, core_attn_out_non_spec)
783 elif use_spec:
784 assert core_attn_out_spec is not None
785 if spec_out is None:
786 core_attn_out[0, :num_actual_tokens] = core_attn_out_spec.squeeze(0)
787 else:
788 assert core_attn_out_non_spec is not None
789 if ns_out is None:
790 core_attn_out[0, :num_actual_tokens] = core_attn_out_non_spec[
791 0, :num_actual_tokens
792 ]