1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""GPU-resident Qwen4Exp position-learning enhancement layers."""
4
+
5
+import math
6
+from collections.abc import Iterable, Sequence
7
+
8
+import torch
9
+import torch.nn.functional as F
10
+from torch import nn
11
+
12
+from vllm.config import CacheConfig, ModelConfig, VllmConfig, get_current_vllm_config
13
+from vllm.forward_context import get_forward_context
14
+from vllm.model_executor.layers.linear import ReplicatedLinear
15
+from vllm.model_executor.layers.mamba.abstract import MambaBase
16
+from vllm.model_executor.layers.mamba.mamba_utils import (
17
+ MambaStateDtypeCalculator,
18
+ MambaStateShapeCalculator,
19
+ is_conv_state_dim_first,
20
+)
21
+from vllm.model_executor.layers.quantization.base_config import (
22
+ QuantizationConfig,
23
+ QuantizeMethodBase,
24
+)
25
+from vllm.model_executor.layers.quantization.fp8 import Fp8Config
26
+from vllm.model_executor.layers.quantization.utils.fp8_utils import (
27
+ create_fp8_scale_parameter,
28
+ create_fp8_weight_parameter,
29
+ is_fp8,
30
+)
31
+from vllm.model_executor.layers.quantization.utils.quant_utils import (
32
+ is_layer_skipped,
33
+)
34
+from vllm.model_executor.models.utils import AutoWeightsLoader
35
+from vllm.model_executor.parameter import PerTensorScaleParameter
36
+from vllm.transformers_utils.configs.qwen4_exp import (
37
+ Qwen4ExpTextConfig,
38
+)
39
+from vllm.utils.torch_utils import direct_register_custom_op
40
+from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
41
+from vllm.v1.attention.backends.short_conv_attn import (
42
+ PleShortConvAttentionBackend,
43
+ PleShortConvAttentionMetadata,
44
+)
45
+from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
46
+
47
+from ..common.ple import PLEVocabParallelEmbedding
48
+
49
+
50
+class Qwen4ExpPLEGroupedNorm(nn.Module):
51
+ def __init__(
52
+ self,
53
+ hidden_size: int,
54
+ eps: float,
55
+ group_size: int | None,
56
+ dtype: torch.dtype | None,
57
+ ) -> None:
58
+ super().__init__()
59
+ if group_size is not None and hidden_size % group_size:
60
+ raise ValueError(
61
+ f"hidden_size ({hidden_size}) must be divisible by "
62
+ f"group_size ({group_size})"
63
+ )
64
+ self.eps = eps
65
+ self.group_size = group_size
66
+ self.weight = nn.Parameter(torch.zeros(hidden_size, dtype=dtype))
67
+
68
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
69
+ input_dtype = hidden_states.dtype
70
+ hidden_states = hidden_states.float()
71
+ if self.group_size is None:
72
+ variance = hidden_states.square().mean(dim=-1, keepdim=True)
73
+ normalized = hidden_states * torch.rsqrt(variance + self.eps)
74
+ else:
75
+ grouped = hidden_states.unflatten(
76
+ -1, (hidden_states.shape[-1] // self.group_size, self.group_size)
77
+ )
78
+ variance = grouped.square().mean(dim=-1, keepdim=True)
79
+ normalized = (grouped * torch.rsqrt(variance + self.eps)).flatten(-2)
80
+ return (normalized * (1.0 + self.weight.float())).to(input_dtype)
81
+
82
+
83
+class Qwen4ExpPLEFp8EmbeddingMethod(QuantizeMethodBase):
84
+ """FP8 PLE embedding with one global checkpoint scale."""
85
+
86
+ def create_weights(
87
+ self,
88
+ layer: nn.Module,
89
+ input_size_per_partition: int,
90
+ output_partition_sizes: list[int],
91
+ input_size: int,
92
+ output_size: int,
93
+ params_dtype: torch.dtype,
94
+ **extra_weight_attrs,
95
+ ) -> None:
96
+ del input_size, output_size, params_dtype
97
+ weight_loader = extra_weight_attrs.get("weight_loader")
98
+ weight = create_fp8_weight_parameter(
99
+ sum(output_partition_sizes), input_size_per_partition, weight_loader
100
+ )
101
+ layer.register_parameter("weight", weight)
102
+
103
+ weight_scale = create_fp8_scale_parameter(
104
+ PerTensorScaleParameter,
105
+ output_partition_sizes,
106
+ input_size_per_partition,
107
+ None,
108
+ weight_loader,
109
+ scale_dtype=torch.bfloat16,
110
+ )
111
+ layer.register_parameter("weight_scale", weight_scale)
112
+
113
+ def apply(
114
+ self,
115
+ layer: nn.Module,
116
+ x: torch.Tensor,
117
+ bias: torch.Tensor | None = None,
118
+ ) -> torch.Tensor:
119
+ raise NotImplementedError("PLE FP8 weights only support embedding lookup")
120
+
121
+ def embedding(self, layer: nn.Module, input_: torch.Tensor) -> torch.Tensor:
122
+ return F.embedding(input_, layer.weight)
123
+
124
+
125
+def _get_ple_embedding_quant_method(
126
+ quant_config: QuantizationConfig | None,
127
+ prefix: str,
128
+) -> QuantizeMethodBase | None:
129
+ """Select global-scale FP8 only for quantized PLE checkpoint shards."""
130
+
131
+ if not isinstance(quant_config, Fp8Config):
132
+ return None
133
+ if not quant_config.is_checkpoint_fp8_serialized:
134
+ return None
135
+
136
+ ignored_layers = quant_config.ignored_layers
137
+ if is_layer_skipped(
138
+ prefix,
139
+ ignored_layers,
140
+ quant_config.packed_modules_mapping,
141
+ match_mode=quant_config.ignored_layers_match_mode,
142
+ ):
143
+ return None
144
+ # PLE checkpoint shards form one runtime embedding parameter.
145
+ shard_prefix = f"{prefix}.shard_"
146
+ if any(name.startswith(shard_prefix) for name in ignored_layers):
147
+ return None
148
+ return Qwen4ExpPLEFp8EmbeddingMethod()
149
+
150
+
151
+class Qwen4ExpNGramEmbedding(nn.Module):
152
+ _MASK64 = (1 << 64) - 1
153
+ _SPLITMIX_GAMMA = 0x9E3779B97F4A7C15
154
+ _SPLITMIX_M1 = 0xBF58476D1CE4E5B9
155
+ _SPLITMIX_M2 = 0x94D049BB133111EB
156
+ _PLE_LAYER_PRIME = 10007
157
+
158
+ @classmethod
159
+ def _splitmix64(cls, value: int) -> int:
160
+ """Mix an integer into a deterministic unsigned 64-bit value."""
161
+ value = (value + cls._SPLITMIX_GAMMA) & cls._MASK64
162
+ value = ((value ^ (value >> 30)) * cls._SPLITMIX_M1) & cls._MASK64
163
+ value = ((value ^ (value >> 27)) * cls._SPLITMIX_M2) & cls._MASK64
164
+ return (value ^ (value >> 31)) & cls._MASK64
165
+
166
+ @staticmethod
167
+ def _is_prime_64(value: int) -> bool:
168
+ """Return whether a 64-bit integer is prime."""
169
+ if value < 2:
170
+ return False
171
+ for prime in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
172
+ if value % prime == 0:
173
+ return value == prime
174
+ exponent = value - 1
175
+ shifts = 0
176
+ while exponent % 2 == 0:
177
+ exponent //= 2
178
+ shifts += 1
179
+ for base in (2, 325, 9375, 28178, 450775, 9780504, 1795265022):
180
+ if base % value == 0:
181
+ continue
182
+ witness = pow(base, exponent, value)
183
+ if witness in (1, value - 1):
184
+ continue
185
+ for _ in range(shifts - 1):
186
+ witness = pow(witness, 2, value)
187
+ if witness == value - 1:
188
+ break
189
+ else:
190
+ return False
191
+ return True
192
+
193
+ @classmethod
194
+ def _nth_prime_after(cls, start: int, count: int) -> int:
195
+ """Return the ``count``-th prime strictly greater than ``start``."""
196
+ prime = int(start)
197
+ for _ in range(count):
198
+ candidate = prime + 1
199
+ if candidate <= 2:
200
+ prime = 2
201
+ continue
202
+ if candidate % 2 == 0:
203
+ candidate += 1
204
+ while not cls._is_prime_64(candidate):
205
+ candidate += 2
206
+ prime = candidate
207
+ return prime
208
+
209
+ @classmethod
210
+ def _make_layer_multipliers(
211
+ cls,
212
+ *,
213
+ ngram_size: int,
214
+ unigram_vocab_size: int,
215
+ seed: int,
216
+ ple_dense_layer_id: int,
217
+ ) -> list[int]:
218
+ """Build deterministic hash multipliers for one PLE layer."""
219
+ max_multiplier = ((1 << 63) - 1) // unigram_vocab_size
220
+ half_bound = max(1, max_multiplier // 2)
221
+ base_seed = seed + cls._PLE_LAYER_PRIME * ple_dense_layer_id
222
+ multipliers = []
223
+ for index in range(ngram_size):
224
+ value = base_seed + cls._SPLITMIX_GAMMA * (index + 1)
225
+ multipliers.append(2 * (cls._splitmix64(value) % half_bound) + 1)
226
+ return multipliers
227
+
228
+ @classmethod
229
+ def _make_vocab_layout(
230
+ cls,
231
+ *,
232
+ ngram_vocab_size_base: int,
233
+ ngram_heads: int,
234
+ ple_dense_layer_id: int,
235
+ ) -> tuple[list[int], list[int], int]:
236
+ """Build per-head vocabulary sizes, offsets, and total row count."""
237
+ sizes: list[int] = []
238
+ offsets: list[int] = []
239
+ offset = 0
240
+ for local_head in range(ngram_heads):
241
+ global_head = ple_dense_layer_id * ngram_heads + local_head
242
+ size = cls._nth_prime_after(ngram_vocab_size_base - 1, global_head + 1)
243
+ sizes.append(size)
244
+ offsets.append(offset)
245
+ offset += size
246
+ return sizes, offsets, offset
247
+
248
+ def __init__(
249
+ self,
250
+ config: Qwen4ExpTextConfig,
251
+ embedding_dim: int,
252
+ ple_dense_layer_id: int,
253
+ max_total_tokens: int,
254
+ max_num_reqs: int,
255
+ prefix: str,
256
+ layer_name: str,
257
+ quant_config: QuantizationConfig | None = None,
258
+ params_dtype: torch.dtype | None = None,
259
+ ) -> None:
260
+ super().__init__()
261
+ self.layer_name = layer_name
262
+ self.embedding_dim = embedding_dim
263
+ self.ngram_size = int(config.ngram_size)
264
+ self.heads_per_ngram = int(config.heads_per_ngram)
265
+ self.ngram_heads = (self.ngram_size - 1) * self.heads_per_ngram
266
+ if self.ngram_size < 2:
267
+ raise ValueError(f"ngram_size must be >= 2, got {self.ngram_size}")
268
+ if self.heads_per_ngram <= 0:
269
+ raise ValueError(f"heads_per_ngram must be > 0, got {self.heads_per_ngram}")
270
+ if embedding_dim % self.ngram_heads:
271
+ raise ValueError(
272
+ "ple_embed_dim must be divisible by total ngram heads: "
273
+ f"{embedding_dim} % {self.ngram_heads} != 0"
274
+ )
275
+ self.head_dim = embedding_dim // self.ngram_heads
276
+ self.eos_token_id = int(config.eos_token_id)
277
+ self.unigram_vocab_size = int(config.vocab_size)
278
+ self.split_ngram_parts = int(getattr(config, "split_ngram_parts", 512))
279
+ if self.split_ngram_parts <= 0:
280
+ raise ValueError("split_ngram_parts must be positive")
281
+
282
+ multipliers = self._make_layer_multipliers(
283
+ ngram_size=self.ngram_size,
284
+ unigram_vocab_size=self.unigram_vocab_size,
285
+ seed=int(getattr(config, "seed", 1234)),
286
+ ple_dense_layer_id=ple_dense_layer_id,
287
+ )
288
+ self.register_buffer(
289
+ "layer_multipliers",
290
+ torch.tensor(multipliers, dtype=torch.long),
291
+ persistent=True,
292
+ )
293
+
294
+ sizes, offsets, total_vocab_size = self._make_vocab_layout(
295
+ ngram_vocab_size_base=int(config.ngram_vocab_size_base),
296
+ ngram_heads=self.ngram_heads,
297
+ ple_dense_layer_id=ple_dense_layer_id,
298
+ )
299
+ self.register_buffer(
300
+ "ngram_heads_vocab_sizes",
301
+ torch.tensor(sizes, dtype=torch.long),
302
+ persistent=True,
303
+ )
304
+ self.register_buffer(
305
+ "ngram_heads_offsets",
306
+ torch.tensor(offsets, dtype=torch.long),
307
+ persistent=True,
308
+ )
309
+ divisor = int(config.make_ngram_vocab_size_divisible_by)
310
+ padded_vocab_size = ((total_vocab_size + divisor - 1) // divisor) * divisor
311
+ self.ngram_embedding = PLEVocabParallelEmbedding(
312
+ padded_vocab_size,
313
+ self.head_dim,
314
+ params_dtype=params_dtype,
315
+ padding_size=divisor,
316
+ prefix=f"{prefix}.ngram_embedding",
317
+ quant_method=_get_ple_embedding_quant_method(
318
+ quant_config, f"{prefix}.ngram_embedding"
319
+ ),
320
+ )
321
+ self.register_buffer(
322
+ "positions_buffer",
323
+ torch.arange(max_total_tokens, dtype=torch.int64),
324
+ persistent=False,
325
+ )
326
+ self.register_buffer(
327
+ "padded_buffer",
328
+ torch.full(
329
+ (max_num_reqs, max_total_tokens),
330
+ self.eos_token_id,
331
+ dtype=torch.int64,
332
+ ),
333
+ persistent=False,
334
+ )
335
+
336
+ @staticmethod
337
+ def _shift_precompute(
338
+ tokens: torch.Tensor, eos_token_id: int
339
+ ) -> tuple[torch.Tensor, torch.Tensor]:
340
+ if tokens.dim() != 2:
341
+ raise ValueError("tokens must be a 2D tensor")
342
+ batch_size, seq_len = tokens.shape
343
+ positions = torch.arange(seq_len, device=tokens.device, dtype=torch.int64)
344
+ eos_positions = torch.where(tokens == eos_token_id, positions, -1)
345
+ previous_eos_inclusive = torch.cummax(eos_positions, dim=1).values
346
+ previous_eos = torch.cat(
347
+ [
348
+ eos_positions.new_full((batch_size, 1), -1),
349
+ previous_eos_inclusive[:, :-1],
350
+ ],
351
+ dim=1,
352
+ )
353
+ return positions, positions.unsqueeze(0) - previous_eos - 1
354
+
355
+ @staticmethod
356
+ def _shift_apply(
357
+ tokens: torch.Tensor,
358
+ positions: torch.Tensor,
359
+ position_in_segment: torch.Tensor,
360
+ shift: int,
361
+ eos_token_id: int,
362
+ ) -> torch.Tensor:
363
+ if shift == 0:
364
+ return tokens
365
+ source = positions - shift
366
+ gather_indices = source.clamp_min(0).unsqueeze(0).expand(tokens.shape[0], -1)
367
+ shifted = tokens.gather(1, gather_indices)
368
+ valid = (source.unsqueeze(0) >= 0) & (position_in_segment >= shift)
369
+ return torch.where(valid, shifted, tokens.new_full((), eos_token_id))
370
+
371
+ def compute_ngram_ids(
372
+ self,
373
+ input_ids: torch.Tensor,
374
+ query_start_loc: torch.Tensor,
375
+ ngram_context: torch.Tensor,
376
+ ) -> torch.Tensor:
377
+ """Compute n-gram embedding indices for the current request layout."""
378
+ input_ids = input_ids.reshape(-1).long()
379
+ query_start_loc = query_start_loc.long()
380
+ num_reqs = query_start_loc.numel() - 1
381
+ num_tokens = input_ids.shape[0]
382
+ if num_tokens > self.positions_buffer.numel():
383
+ raise ValueError(
384
+ f"PLE received {num_tokens} tokens, but its workspace supports "
385
+ f"at most {self.positions_buffer.numel()}"
386
+ )
387
+ if num_reqs > self.padded_buffer.shape[0]:
388
+ raise ValueError(
389
+ f"PLE received {num_reqs} requests, but its workspace supports "
390
+ f"at most {self.padded_buffer.shape[0]}"
391
+ )
392
+
393
+ positions = self.positions_buffer[:num_tokens]
394
+ packed = self.padded_buffer[:num_reqs, :num_tokens]
395
+ packed.fill_(self.eos_token_id)
396
+ request_indices = torch.searchsorted(query_start_loc, positions, right=True) - 1
397
+ request_indices.clamp_(max=num_reqs - 1)
398
+ columns = (positions - query_start_loc[request_indices]).clamp(
399
+ 0, packed.shape[1] - 1
400
+ )
401
+ packed[request_indices, columns] = input_ids
402
+ ngram_context = ngram_context[:num_reqs].to(
403
+ device=input_ids.device, dtype=torch.long
404
+ )
405
+
406
+ context = torch.cat([ngram_context, packed], dim=-1)
407
+ positions_2d, position_in_segment = self._shift_precompute(
408
+ context, self.eos_token_id
409
+ )
410
+ shifted = [context]
411
+ for shift in range(1, self.ngram_size):
412
+ shifted.append(
413
+ self._shift_apply(
414
+ context,
415
+ positions_2d,
416
+ position_in_segment,
417
+ shift,
418
+ self.eos_token_id,
419
+ )
420
+ )
421
+ adjusted_columns = columns + self.ngram_size - 1
422
+ id_blocks = []
423
+ for ngram in range(2, self.ngram_size + 1):
424
+ start = (ngram - 2) * self.heads_per_ngram
425
+ end = start + self.heads_per_ngram
426
+ mixed = shifted[0] * self.layer_multipliers[0]
427
+ for index in range(1, ngram):
428
+ mixed = torch.bitwise_xor(
429
+ mixed, shifted[index] * self.layer_multipliers[index]
430
+ )
431
+ sizes = self.ngram_heads_vocab_sizes[start:end]
432
+ offsets = self.ngram_heads_offsets[start:end]
433
+ ids = torch.remainder(mixed.unsqueeze(-1), sizes) + offsets
434
+ id_blocks.append(ids[request_indices, adjusted_columns])
435
+ return torch.cat(id_blocks, dim=-1)
436
+
437
+ def forward(
438
+ self,
439
+ input_ids: torch.Tensor,
440
+ query_start_loc: torch.Tensor,
441
+ ngram_context: torch.Tensor,
442
+ ) -> torch.Tensor:
443
+ ngram_ids = input_ids.new_empty(
444
+ (input_ids.shape[0], self.ngram_heads),
445
+ dtype=torch.long,
446
+ )
447
+ # Keep num_reqs-dependent ID generation outside PIECEWISE CUDA graphs,
448
+ # which dispatch only on the padded token count.
449
+ torch.ops.vllm.qwen4_exp_compute_ple_ngram_ids(
450
+ input_ids,
451
+ query_start_loc,
452
+ ngram_context,
453
+ ngram_ids,
454
+ self.layer_name,
455
+ )
456
+ return self.ngram_embedding(ngram_ids).flatten(-2)
457
+
458
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
459
+ """Load hash buffers and checkpoint-split embedding rows."""
460
+
461
+ persistent_buffers = {
462
+ "layer_multipliers": self.layer_multipliers,
463
+ "ngram_heads_offsets": self.ngram_heads_offsets,
464
+ "ngram_heads_vocab_sizes": self.ngram_heads_vocab_sizes,
465
+ }
466
+ loaded: set[str] = set()
467
+ regular_weights: list[tuple[str, torch.Tensor]] = []
468
+ shard_prefix = "ngram_embedding.shard_"
469
+
470
+ for name, loaded_weight in weights:
471
+ leaf_name = name.rsplit(".", 1)[-1]
472
+ if leaf_name.startswith("hashstats_") or leaf_name == "token_lookup":
473
+ continue
474
+ if name in persistent_buffers:
475
+ buffer = persistent_buffers[name]
476
+ if buffer.shape != loaded_weight.shape:
477
+ raise ValueError(
478
+ f"Shape mismatch for {name}: expected "
479
+ f"{tuple(buffer.shape)}, got {tuple(loaded_weight.shape)}"
480
+ )
481
+ buffer.copy_(loaded_weight.to(device=buffer.device, dtype=buffer.dtype))
482
+ loaded.add(name)
483
+ continue
484
+ if name.startswith(shard_prefix) and name.endswith(".weight"):
485
+ shard_text = name[len(shard_prefix) : -len(".weight")]
486
+ if not shard_text.isdigit():
487
+ regular_weights.append((name, loaded_weight))
488
+ continue
489
+ shard_index = int(shard_text)
490
+ if shard_index >= self.split_ngram_parts:
491
+ raise ValueError(
492
+ f"PLE embedding shard index {shard_index} exceeds "
493
+ f"split_ngram_parts={self.split_ngram_parts}"
494
+ )
495
+ embedding = self.ngram_embedding
496
+ shard_size = (
497
+ embedding.org_vocab_size + self.split_ngram_parts - 1
498
+ ) // self.split_ngram_parts
499
+ checkpoint_start = shard_index * shard_size
500
+ expected_rows = max(
501
+ 0,
502
+ min(shard_size, embedding.org_vocab_size - checkpoint_start),
503
+ )
504
+ expected_shape = (expected_rows, embedding.embedding_dim)
505
+ if tuple(loaded_weight.shape) != expected_shape:
506
+ raise ValueError(
507
+ f"Shape mismatch for PLE embedding shard {shard_index}: "
508
+ f"expected {expected_shape}, got "
509
+ f"{tuple(loaded_weight.shape)}"
510
+ )
511
+ embedding.weight.weight_loader(
512
+ embedding.weight,
513
+ loaded_weight,
514
+ checkpoint_start=checkpoint_start,
515
+ )
516
+ loaded.add("ngram_embedding.weight")
517
+ continue
518
+ regular_weights.append((name, loaded_weight))
519
+
520
+ if regular_weights:
521
+ loaded.update(AutoWeightsLoader(self).load_weights(regular_weights))
522
+ return loaded
523
+
524
+
525
+class Qwen4ExpPLELayer(nn.Module, MambaBase):
526
+ def __init__(
527
+ self,
528
+ config: Qwen4ExpTextConfig,
529
+ vllm_config: VllmConfig,
530
+ layer_idx: int = 0,
531
+ ple_dense_layer_id: int | None = None,
532
+ prefix: str = "",
533
+ ) -> None:
534
+ super().__init__()
535
+ model_config = vllm_config.model_config
536
+ cache_config = vllm_config.cache_config
537
+ quant_config = vllm_config.quant_config
538
+ self.model_config: ModelConfig = model_config
539
+ self.cache_config: CacheConfig = cache_config
540
+ self.layer_idx = layer_idx
541
+ self.ple_dense_layer_id = (
542
+ int(ple_dense_layer_id)
543
+ if ple_dense_layer_id is not None
544
+ else int(layer_idx)
545
+ )
546
+ self.prefix = prefix
547
+ self.hidden_size = int(config.hidden_size)
548
+ self.hc_count = config.hc_count
549
+ self.hc_hidden_size = self.hidden_size * self.hc_count
550
+ self.conv_kernel_size = int(config.ple_conv_kernel_size)
551
+ self.short_conv_dilation = int(config.ngram_size)
552
+ self.conv_state_len = (self.conv_kernel_size - 1) * self.short_conv_dilation
553
+ self.num_spec_tokens = vllm_config.num_speculative_tokens
554
+ self.activation = "silu"
555
+ self.ple_embedding = Qwen4ExpNGramEmbedding(
556
+ config,
557
+ int(config.ple_embed_dim),
558
+ self.ple_dense_layer_id,
559
+ vllm_config.scheduler_config.max_num_batched_tokens,
560
+ vllm_config.scheduler_config.max_num_seqs,
561
+ prefix=f"{prefix}.ple_embedding",
562
+ layer_name=prefix,
563
+ quant_config=quant_config,
564
+ params_dtype=model_config.dtype,
565
+ )
566
+ self.key_proj = ReplicatedLinear(
567
+ int(config.ple_embed_dim),
568
+ self.hc_hidden_size,
569
+ bias=False,
570
+ quant_config=quant_config,
571
+ prefix=f"{prefix}.key_proj",
572
+ )
573
+ self.value_proj = ReplicatedLinear(
574
+ int(config.ple_embed_dim),
575
+ self.hidden_size,
576
+ bias=False,
577
+ quant_config=quant_config,
578
+ prefix=f"{prefix}.value_proj",
579
+ )
580
+ norm_args = (
581
+ self.hc_hidden_size,
582
+ config.rms_norm_eps,
583
+ self.hidden_size,
584
+ model_config.dtype,
585
+ )
586
+ self.norm_key = Qwen4ExpPLEGroupedNorm(*norm_args)
587
+ self.norm_query = Qwen4ExpPLEGroupedNorm(*norm_args)
588
+ self.norm_conv = Qwen4ExpPLEGroupedNorm(*norm_args)
589
+ self.conv1d = nn.Conv1d(
590
+ self.hc_hidden_size,
591
+ self.hc_hidden_size,
592
+ self.conv_kernel_size,
593
+ groups=self.hc_hidden_size,
594
+ padding=self.conv_state_len,
595
+ dilation=self.short_conv_dilation,
596
+ bias=False,
597
+ dtype=model_config.dtype,
598
+ )
599
+ nn.init.zeros_(self.conv1d.weight)
600
+ self.conv1d.weight._no_reinit = True
601
+ self.kv_cache = (torch.tensor([]),)
602
+ compilation_config = get_current_vllm_config().compilation_config
603
+ if prefix in compilation_config.static_forward_context:
604
+ raise ValueError(f"Duplicate layer name: {prefix}")
605
+ compilation_config.static_forward_context[prefix] = self
606
+
607
+ def _get_embedding_weight_scale(self) -> torch.Tensor | None:
608
+ embedding = getattr(self.ple_embedding, "ngram_embedding", None)
609
+ return getattr(embedding, "weight_scale", None)
610
+
611
+ def _dequantize_embeddings(
612
+ self,
613
+ embeddings: torch.Tensor,
614
+ output_dtype: torch.dtype,
615
+ ) -> torch.Tensor:
616
+ """Dequantize PLE lookup output."""
617
+
618
+ if not is_fp8(embeddings):
619
+ return embeddings
620
+ weight_scale = self._get_embedding_weight_scale()
621
+ if weight_scale is None:
622
+ raise RuntimeError("FP8 PLE embedding is missing its global scale")
623
+ if weight_scale.device != embeddings.device:
624
+ raise RuntimeError("FP8 PLE embedding scale must be on the output device")
625
+ return embeddings.to(output_dtype) * weight_scale.to(output_dtype)
626
+
627
+ @property
628
+ def mamba_type(self) -> MambaAttentionBackendEnum:
629
+ return MambaAttentionBackendEnum.SHORT_CONV
630
+
631
+ @property
632
+ def is_kv_cache_tp_replicated(self) -> bool:
633
+ return True
634
+
635
+ def get_attn_backend(self) -> type[PleShortConvAttentionBackend]:
636
+ return PleShortConvAttentionBackend
637
+
638
+ def get_state_dtype(self) -> tuple[torch.dtype, ...]:
639
+ return MambaStateDtypeCalculator.short_conv_state_dtype(
640
+ self.model_config.dtype, self.cache_config.mamba_cache_dtype
641
+ )
642
+
643
+ def get_state_shape(self) -> Sequence[tuple[int, ...]]:
644
+ return MambaStateShapeCalculator.short_conv_state_shape(
645
+ tp_world_size=1,
646
+ intermediate_size=self.hc_hidden_size,
647
+ conv_kernel=self.conv_state_len + 1,
648
+ num_spec=self.num_spec_tokens,
649
+ )
650
+
651
+ def _apply_norm(
652
+ self, norm: Qwen4ExpPLEGroupedNorm, hidden_states: torch.Tensor
653
+ ) -> torch.Tensor:
654
+ shape = hidden_states.shape
655
+ return norm(hidden_states.flatten(-2)).reshape(shape)
656
+
657
+ def _short_conv_fallback(self, inputs: torch.Tensor) -> torch.Tensor:
658
+ # Profiling / CUDA graph capture only; conv state is not updated.
659
+ inputs_t = inputs.transpose(0, 1).unsqueeze(0)
660
+ output = self.conv1d(inputs_t)[..., : inputs_t.size(-1)]
661
+ return F.silu(output).squeeze(0).transpose(0, 1)
662
+
663
+ def _short_conv_dilated_decode_batched(
664
+ self,
665
+ x_d: torch.Tensor,
666
+ conv_state: torch.Tensor,
667
+ conv_weights: torch.Tensor,
668
+ state_indices_tensor_d: torch.Tensor,
669
+ has_initial_states_d: torch.Tensor | None,
670
+ ) -> torch.Tensor:
671
+ state_indices = state_indices_tensor_d.to(
672
+ device=conv_state.device, dtype=torch.int64
673
+ )
674
+ # FULL cudagraph padded decode rows use NULL_BLOCK_ID. Remap them to
675
+ # slot 0 for a safe gather, then zero output and skip write-back.
676
+ valid_state = state_indices != NULL_BLOCK_ID
677
+ state_indices = torch.where(
678
+ valid_state, state_indices, torch.zeros_like(state_indices)
679
+ )
680
+ if has_initial_states_d is None:
681
+ has_initial_state = valid_state
682
+ else:
683
+ if has_initial_states_d.numel() < state_indices_tensor_d.numel():
684
+ raise ValueError(
685
+ "has_initial_states_d size mismatch: "
686
+ f"got {has_initial_states_d.numel()}, "
687
+ f"need >= {state_indices_tensor_d.numel()}."
688
+ )
689
+ has_initial_state = has_initial_states_d[
690
+ : state_indices_tensor_d.numel()
691
+ ].to(device=conv_state.device, dtype=torch.bool)
692
+ has_initial_state = has_initial_state & valid_state
693
+
694
+ cached_state = conv_state.index_select(0, state_indices)
695
+ state = cached_state[..., : self.conv_state_len].to(x_d.dtype)
696
+ if self.conv_state_len > 0:
697
+ initial_state = torch.where(
698
+ has_initial_state.view(-1, 1, 1),
699
+ state,
700
+ torch.zeros_like(state),
701
+ )
702
+ history = torch.cat((initial_state, x_d.unsqueeze(-1)), dim=-1)
703
+ else:
704
+ history = x_d.unsqueeze(-1)
705
+
706
+ conv_output = F.conv1d(
707
+ history,
708
+ conv_weights.unsqueeze(1).contiguous(),
709
+ groups=history.size(1),
710
+ dilation=self.short_conv_dilation,
711
+ ).squeeze(-1)
712
+ output = F.silu(conv_output)
713
+ output = output * valid_state.view(-1, 1).to(output.dtype)
714
+
715
+ if self.conv_state_len > 0:
716
+ next_state = history[..., -self.conv_state_len :]
717
+ # Padded rows are remapped to the reserved null slot. Preserve its
718
+ # existing value while writing the new states for valid rows.
719
+ existing_base_state = cached_state[..., : self.conv_state_len]
720
+ safe_next_state = torch.where(
721
+ valid_state.view(-1, 1, 1),
722
+ next_state.to(conv_state.dtype),
723
+ existing_base_state,
724
+ )
725
+ cached_state[..., : self.conv_state_len] = safe_next_state
726
+ conv_state.index_copy_(0, state_indices, cached_state)
727
+
728
+ return output
729
+
730
+ def _short_conv_dilated_prefill_batched(
731
+ self,
732
+ x_p: torch.Tensor,
733
+ metadata: PleShortConvAttentionMetadata,
734
+ conv_state: torch.Tensor,
735
+ conv_weights: torch.Tensor,
736
+ state_indices_tensor_p: torch.Tensor,
737
+ num_prefills: int,
738
+ num_decode_tokens: int,
739
+ num_prefill_tokens: int,
740
+ ) -> torch.Tensor:
741
+ # ``non_spec_query_start_loc`` covers the non-spec (decode + prefill)
742
+ # requests and equals ``query_start_loc`` when spec-decode is inactive.
743
+ non_spec_query_start_loc = metadata.non_spec_query_start_loc
744
+ if non_spec_query_start_loc is None:
745
+ raise ValueError("query_start_loc is required for prefill short-conv")
746
+ query_start_loc_p = (
747
+ non_spec_query_start_loc[-num_prefills - 1 :] - num_decode_tokens
748
+ )
749
+ # The metadata builder guarantees that the prefill query offsets start
750
+ # at 0 and end at num_prefill_tokens. Avoid reading those values here,
751
+ # since doing so would force a device-to-host synchronization.
752
+ has_initial_states_p = metadata.has_initial_states_p
753
+ if has_initial_states_p is None:
754
+ raise ValueError("has_initial_states_p is required for prefill short-conv")
755
+
756
+ output = torch.empty_like(x_p)
757
+ q_starts = query_start_loc_p.to(torch.int64)
758
+ if state_indices_tensor_p.numel() < num_prefills:
759
+ raise ValueError(
760
+ "state_indices_tensor_p size mismatch: "
761
+ f"got {state_indices_tensor_p.numel()}, "
762
+ f"need >= {num_prefills}."
763
+ )
764
+ if has_initial_states_p.numel() < num_prefills:
765
+ raise ValueError(
766
+ "has_initial_states_p size mismatch: "
767
+ f"got {has_initial_states_p.numel()}, "
768
+ f"need >= {num_prefills}."
769
+ )
770
+ if num_prefills == 0 or x_p.numel() == 0:
771
+ return output
772
+ lengths = q_starts[1:] - q_starts[:-1]
773
+ # Use the CPU-computed packing width from the metadata builder instead
774
+ # of synchronizing on lengths.max().
775
+ max_len = metadata.max_prefill_query_len
776
+ if max_len <= 0:
777
+ return output
778
+
779
+ hidden_size = x_p.shape[1]
780
+ positions = torch.arange(
781
+ num_prefill_tokens, device=x_p.device, dtype=torch.int64
782
+ )
783
+ req_indices = torch.searchsorted(q_starts[1:], positions, right=True)
784
+ col_indices = positions - q_starts[req_indices]
785
+
786
+ packed_tokens = x_p.new_zeros((num_prefills, max_len, hidden_size))
787
+ packed_tokens[req_indices, col_indices] = x_p
788
+ packed_tokens = packed_tokens.transpose(1, 2).contiguous()
789
+
790
+ state_indices = state_indices_tensor_p[:num_prefills].to(
791
+ device=conv_state.device, dtype=torch.int64
792
+ )
793
+ valid_state = state_indices != NULL_BLOCK_ID
794
+ state_indices = torch.where(
795
+ valid_state, state_indices, torch.zeros_like(state_indices)
796
+ )
797
+ has_initial = has_initial_states_p[:num_prefills].to(
798
+ device=conv_state.device, dtype=torch.bool
799
+ )
800
+ if self.conv_state_len > 0:
801
+ if conv_state.shape[0] == 0:
802
+ state = conv_state.new_zeros(
803
+ (num_prefills, hidden_size, self.conv_state_len),
804
+ dtype=x_p.dtype,
805
+ )
806
+ else:
807
+ state = conv_state.index_select(0, state_indices)[
808
+ ..., : self.conv_state_len
809
+ ].to(x_p.dtype)
810
+ use_initial_mask = (valid_state & has_initial).view(num_prefills, 1, 1)
811
+ initial_state = torch.where(
812
+ use_initial_mask,
813
+ state,
814
+ torch.zeros_like(state),
815
+ )
816
+ history = torch.cat((initial_state, packed_tokens), dim=-1)
817
+ else:
818
+ history = packed_tokens
819
+
820
+ conv_output = F.conv1d(
821
+ history,
822
+ conv_weights.unsqueeze(1).contiguous(),
823
+ groups=history.size(1),
824
+ dilation=self.short_conv_dilation,
825
+ )
826
+ conv_output = F.silu(conv_output).transpose(1, 2).contiguous()
827
+
828
+ token_positions = torch.arange(max_len, device=x_p.device, dtype=torch.int64)
829
+ valid_tokens = token_positions.view(1, max_len) < lengths.view(num_prefills, 1)
830
+ valid_output_mask = valid_tokens & valid_state.to(device=x_p.device).view(
831
+ num_prefills, 1
832
+ )
833
+ conv_output.masked_fill_(~valid_output_mask.unsqueeze(-1), 0)
834
+ output.copy_(conv_output[req_indices, col_indices])
835
+
836
+ if self.conv_state_len > 0 and conv_state.shape[0] > 0:
837
+ state_starts = lengths.to(device=history.device, dtype=torch.int64).view(
838
+ num_prefills, 1, 1
839
+ )
840
+ state_offsets = torch.arange(
841
+ self.conv_state_len, device=history.device, dtype=torch.int64
842
+ ).view(1, 1, self.conv_state_len)
843
+ next_state = history.gather(
844
+ dim=2,
845
+ index=(state_starts + state_offsets).expand(-1, history.size(1), -1),
846
+ )
847
+ # Write back without a host synchronization. Valid, non-empty rows
848
+ # receive their new state; padding and zero-length rows keep the
849
+ # current cache value.
850
+ existing_state = conv_state.index_select(0, state_indices)
851
+ existing_base_state = existing_state[..., : self.conv_state_len]
852
+ update_mask = valid_state & (lengths.to(device=conv_state.device) > 0)
853
+ safe_next_state = torch.where(
854
+ update_mask.view(num_prefills, 1, 1),
855
+ next_state.to(conv_state.dtype),
856
+ existing_base_state,
857
+ )
858
+ existing_state[..., : self.conv_state_len] = safe_next_state
859
+ conv_state.index_copy_(0, state_indices, existing_state)
860
+ return output
861
+
862
+ def _short_conv_dilated_spec_batched(
863
+ self,
864
+ x_spec: torch.Tensor,
865
+ conv_state: torch.Tensor,
866
+ conv_weights: torch.Tensor,
867
+ spec_state_indices_tensor: torch.Tensor,
868
+ spec_query_start_loc: torch.Tensor,
869
+ num_accepted_tokens: torch.Tensor,
870
+ spec_query_len: int,
871
+ ) -> torch.Tensor:
872
+ """Dilated short-conv for speculative-decode (MTP) requests.
873
+
874
+ Each spec request feeds multiple (draft + 1) query tokens. The conv
875
+ outputs are computed causally after rolling back the previous draft
876
+ state by ``num_accepted_tokens - 1``. The current candidate inputs stay
877
+ in the extended cache for the next forward, matching
878
+ ``causal_conv1d_update``.
879
+
880
+ ``spec_query_len`` (== num_speculative_tokens + 1) is the maximum query
881
+ length and is a Python int, so no host synchronization is needed; this
882
+ keeps the path safe for full CUDA-graph capture/replay where the buffers
883
+ are padded at the request level.
884
+ """
885
+ num_reqs = spec_state_indices_tensor.numel()
886
+ hidden_size = x_spec.size(-1)
887
+ # Use a fixed packing width instead of synchronizing on lengths.max().
888
+ max_len = spec_query_len
889
+ # Full CUDA graphs can pad these buffers. Only the first num_reqs
890
+ # accepted-token counts belong to actual speculative requests.
891
+ num_accepted_tokens = num_accepted_tokens[:num_reqs]
892
+ q_starts = spec_query_start_loc[: num_reqs + 1].to(torch.int64)
893
+ # Keep the number of real speculative tokens on the device.
894
+ total_real_tokens = q_starts[num_reqs]
895
+
896
+ state_indices = spec_state_indices_tensor.to(
897
+ device=conv_state.device, dtype=torch.int64
898
+ )
899
+ valid_state = state_indices != NULL_BLOCK_ID
900
+ state_indices = torch.where(
901
+ valid_state, state_indices, torch.zeros_like(state_indices)
902
+ )
903
+ positions = torch.arange(
904
+ x_spec.size(0), device=x_spec.device, dtype=torch.int64
905
+ )
906
+ # Route graph-padded token rows to the discarded dummy request so that
907
+ # they cannot overwrite real packed data.
908
+ req_indices = torch.searchsorted(q_starts[1:], positions, right=True)
909
+ valid_tokens = (positions < total_real_tokens) & (req_indices < num_reqs)
910
+ clamped_req_indices = req_indices.clamp_max(max(num_reqs - 1, 0))
911
+ col_indices = (positions - q_starts[clamped_req_indices]).clamp_(0, max_len - 1)
912
+ pack_req_indices = torch.where(
913
+ valid_tokens,
914
+ clamped_req_indices,
915
+ torch.full_like(req_indices, num_reqs),
916
+ )
917
+ pack_col_indices = torch.where(
918
+ valid_tokens, col_indices, torch.zeros_like(col_indices)
919
+ )
920
+
921
+ # The last request row is the dummy sink for graph padding.
922
+ packed = x_spec.new_zeros((num_reqs + 1, max_len, hidden_size))
923
+ packed[pack_req_indices, pack_col_indices] = x_spec
924
+ packed = packed.transpose(1, 2).contiguous()
925
+
926
+ if self.conv_state_len > 0:
927
+ cached_state = conv_state.index_select(0, state_indices)
928
+ rollback_offsets = num_accepted_tokens.to(
929
+ device=conv_state.device, dtype=torch.int64
930
+ ).sub(1)
931
+ rollback_offsets = torch.where(
932
+ valid_state,
933
+ rollback_offsets.clamp_(0, max_len - 1),
934
+ torch.zeros_like(rollback_offsets),
935
+ )
936
+ state_offsets = torch.arange(
937
+ self.conv_state_len, device=conv_state.device, dtype=torch.int64
938
+ ).view(1, 1, self.conv_state_len)
939
+ rollback_indices = rollback_offsets.view(-1, 1, 1) + state_offsets
940
+ state = cached_state.gather(
941
+ 2, rollback_indices.expand(-1, hidden_size, -1)
942
+ ).to(x_spec.dtype)
943
+ state = torch.where(
944
+ valid_state.view(num_reqs, 1, 1),
945
+ state,
946
+ torch.zeros_like(state),
947
+ )
948
+ # Append a zeroed dummy-row state to match the [num_reqs + 1] pack.
949
+ dummy_state = state.new_zeros((1, hidden_size, self.conv_state_len))
950
+ state_full = torch.cat((state, dummy_state), dim=0)
951
+ history = torch.cat((state_full, packed), dim=-1)
952
+ else:
953
+ history = packed
954
+
955
+ conv_output = F.conv1d(
956
+ history,
957
+ conv_weights.unsqueeze(1).contiguous(),
958
+ groups=history.size(1),
959
+ dilation=self.short_conv_dilation,
960
+ )
961
+ conv_output = F.silu(conv_output).transpose(1, 2).contiguous()
962
+
963
+ output = conv_output[pack_req_indices, pack_col_indices]
964
+ output = output * valid_tokens.view(-1, 1).to(output.dtype)
965
+
966
+ # Keep all current candidate inputs in the extended state. On the next
967
+ # target forward, ``num_accepted_tokens - 1`` selects the rollback
968
+ # window before processing the newly scheduled tokens.
969
+ if self.conv_state_len > 0:
970
+ state_capacity = self.conv_state_len + max_len - 1
971
+ if conv_state.size(-1) < state_capacity:
972
+ raise RuntimeError(
973
+ "PLE short-conv cache cannot retain speculative tokens: "
974
+ f"got {conv_state.size(-1)}, need {state_capacity}."
975
+ )
976
+ candidate_state = history[:num_reqs, :, 1 : state_capacity + 1]
977
+ query_lengths = q_starts[1:] - q_starts[:-1]
978
+ state_positions = torch.arange(
979
+ state_capacity, device=history.device, dtype=torch.int64
980
+ ).view(1, 1, state_capacity)
981
+ update_lengths = (self.conv_state_len + query_lengths - 1).view(
982
+ num_reqs, 1, 1
983
+ )
984
+ update_mask = valid_state.view(num_reqs, 1, 1) & (
985
+ state_positions < update_lengths
986
+ )
987
+ existing_state = cached_state[..., :state_capacity]
988
+ next_state = torch.where(
989
+ update_mask,
990
+ candidate_state.to(conv_state.dtype),
991
+ existing_state,
992
+ )
993
+ cached_state[..., :state_capacity] = next_state
994
+ conv_state.index_copy_(0, state_indices, cached_state)
995
+
996
+ return output
997
+
998
+ def _short_conv_dilated_dispatch(
999
+ self,
1000
+ inputs: torch.Tensor,
1001
+ metadata: PleShortConvAttentionMetadata,
1002
+ conv_state: torch.Tensor,
1003
+ conv_weights: torch.Tensor,
1004
+ ) -> torch.Tensor:
1005
+ num_prefills = metadata.num_prefills
1006
+ num_decodes = metadata.num_decodes
1007
+ num_decode_tokens = metadata.num_decode_tokens
1008
+ num_prefill_tokens = metadata.num_prefill_tokens
1009
+ has_prefill = num_prefills > 0
1010
+ has_decode = num_decodes > 0
1011
+ has_spec = metadata.spec_sequence_masks is not None
1012
+ x = inputs[: metadata.num_actual_tokens]
1013
+
1014
+ # Split spec / non-spec tokens.
1015
+ if has_spec:
1016
+ if has_prefill or has_decode:
1017
+ assert metadata.spec_token_indx is not None
1018
+ assert metadata.non_spec_token_indx is not None
1019
+ x_spec = x.index_select(0, metadata.spec_token_indx.long())
1020
+ x_non_spec = x.index_select(0, metadata.non_spec_token_indx.long())
1021
+ else:
1022
+ x_spec = x
1023
+ x_non_spec = None
1024
+ else:
1025
+ x_spec = None
1026
+ x_non_spec = x
1027
+
1028
+ spec_output = None
1029
+ # 1. Run the multi-query speculative-decode part.
1030
+ if has_spec:
1031
+ assert metadata.spec_state_indices_tensor is not None
1032
+ assert metadata.spec_query_start_loc is not None
1033
+ assert metadata.num_accepted_tokens is not None
1034
+ spec_output = self._short_conv_dilated_spec_batched(
1035
+ x_spec=x_spec,
1036
+ conv_state=conv_state,
1037
+ conv_weights=conv_weights,
1038
+ spec_state_indices_tensor=metadata.spec_state_indices_tensor[
1039
+ : metadata.num_spec_decodes
1040
+ ],
1041
+ spec_query_start_loc=metadata.spec_query_start_loc,
1042
+ num_accepted_tokens=metadata.num_accepted_tokens,
1043
+ spec_query_len=metadata.spec_query_len,
1044
+ )
1045
+
1046
+ # 2. Run regular decode and prefill requests.
1047
+ conv_out_non_spec = None
1048
+ state_indices_tensor = metadata.state_indices_tensor
1049
+ if x_non_spec is not None:
1050
+ assert state_indices_tensor is not None
1051
+ if has_prefill:
1052
+ state_indices_tensor_d, state_indices_tensor_p = torch.split(
1053
+ state_indices_tensor,
1054
+ [num_decodes, num_prefills],
1055
+ dim=0,
1056
+ )
1057
+ x_d, x_p = torch.split(
1058
+ x_non_spec,
1059
+ [num_decode_tokens, num_prefill_tokens],
1060
+ dim=0,
1061
+ )
1062
+ non_spec_parts: list[torch.Tensor] = []
1063
+ if has_decode:
1064
+ non_spec_parts.append(
1065
+ self._short_conv_dilated_decode_batched(
1066
+ x_d=x_d,
1067
+ conv_state=conv_state,
1068
+ conv_weights=conv_weights,
1069
+ state_indices_tensor_d=state_indices_tensor_d,
1070
+ has_initial_states_d=metadata.has_initial_states_d,
1071
+ )
1072
+ )
1073
+ non_spec_parts.append(
1074
+ self._short_conv_dilated_prefill_batched(
1075
+ x_p=x_p,
1076
+ metadata=metadata,
1077
+ conv_state=conv_state,
1078
+ conv_weights=conv_weights,
1079
+ state_indices_tensor_p=state_indices_tensor_p,
1080
+ num_prefills=num_prefills,
1081
+ num_decode_tokens=num_decode_tokens,
1082
+ num_prefill_tokens=num_prefill_tokens,
1083
+ )
1084
+ )
1085
+ conv_out_non_spec = torch.vstack(non_spec_parts)
1086
+ else:
1087
+ conv_out_non_spec = self._short_conv_dilated_decode_batched(
1088
+ x_d=x_non_spec,
1089
+ conv_state=conv_state,
1090
+ conv_weights=conv_weights,
1091
+ state_indices_tensor_d=state_indices_tensor[: x_non_spec.size(0)],
1092
+ has_initial_states_d=metadata.has_initial_states_d,
1093
+ )
1094
+
1095
+ # 3. Merge both parts back into the original token order.
1096
+ if has_spec and conv_out_non_spec is not None:
1097
+ assert metadata.spec_token_indx is not None
1098
+ assert metadata.non_spec_token_indx is not None
1099
+ assert spec_output is not None
1100
+ output = x.new_empty((metadata.num_actual_tokens, x.size(-1)))
1101
+ output.index_copy_(0, metadata.spec_token_indx, spec_output)
1102
+ output.index_copy_(0, metadata.non_spec_token_indx, conv_out_non_spec)
1103
+ return output
1104
+ elif has_spec:
1105
+ assert spec_output is not None
1106
+ return spec_output
1107
+ if conv_out_non_spec is None:
1108
+ return x
1109
+ return conv_out_non_spec
1110
+
1111
+ def _short_conv(self, inputs: torch.Tensor) -> torch.Tensor:
1112
+ forward_context = get_forward_context()
1113
+ attn_metadata = forward_context.attn_metadata
1114
+ if attn_metadata is None:
1115
+ return self._short_conv_fallback(inputs)
1116
+
1117
+ if not isinstance(attn_metadata, dict):
1118
+ raise RuntimeError(
1119
+ "PLE short-conv expects per-layer attention metadata dict "
1120
+ f"during inference, got {type(attn_metadata).__name__}."
1121
+ )
1122
+
1123
+ layer_attn_metadata = attn_metadata.get(self.prefix)
1124
+ if layer_attn_metadata is None:
1125
+ # MRV2 omits Mamba-family metadata during profile warmup.
1126
+ return self._short_conv_fallback(inputs)
1127
+ if not isinstance(layer_attn_metadata, PleShortConvAttentionMetadata):
1128
+ raise TypeError(
1129
+ "Expected PleShortConvAttentionMetadata for layer "
1130
+ f"'{self.prefix}', got "
1131
+ f"{type(layer_attn_metadata).__name__}."
1132
+ )
1133
+
1134
+ conv_state = self.kv_cache[0]
1135
+ if not is_conv_state_dim_first():
1136
+ conv_state = conv_state.transpose(-1, -2)
1137
+ conv_weights = self.conv1d.weight.squeeze(1)
1138
+
1139
+ state_capacity = self.conv_state_len + self.num_spec_tokens
1140
+ if state_capacity > 0:
1141
+ if conv_state.size(-1) < state_capacity:
1142
+ raise RuntimeError(
1143
+ "PLE short-conv cache is smaller than expected for "
1144
+ f"dilated convolution: got {conv_state.size(-1)}, "
1145
+ f"expect at least {state_capacity}."
1146
+ )
1147
+ conv_state = conv_state[..., -state_capacity:]
1148
+ return self._short_conv_dilated_dispatch(
1149
+ inputs,
1150
+ layer_attn_metadata,
1151
+ conv_state,
1152
+ conv_weights.to(dtype=inputs.dtype),
1153
+ )
1154
+
1155
+ def forward(
1156
+ self,
1157
+ hidden_states: torch.Tensor,
1158
+ input_ids: torch.Tensor,
1159
+ query_start_loc: torch.Tensor,
1160
+ ngram_context: torch.Tensor,
1161
+ ) -> torch.Tensor:
1162
+ input_ids = input_ids.reshape(-1)
1163
+ if input_ids.shape[0] != hidden_states.shape[0]:
1164
+ raise ValueError(
1165
+ "PLE expects input_ids and hidden_states to have the same "
1166
+ f"token length, got {input_ids.shape[0]} and "
1167
+ f"{hidden_states.shape[0]}"
1168
+ )
1169
+ embeddings = self.ple_embedding(input_ids, query_start_loc, ngram_context)
1170
+ embeddings = self._dequantize_embeddings(embeddings, hidden_states.dtype)
1171
+ key, _ = self.key_proj(embeddings)
1172
+ value, _ = self.value_proj(embeddings)
1173
+ token_count = hidden_states.shape[0]
1174
+ key = key.reshape(token_count, self.hc_count, self.hidden_size)
1175
+ query = hidden_states.reshape(token_count, self.hc_count, self.hidden_size)
1176
+ key = self._apply_norm(self.norm_key, key)
1177
+ query = self._apply_norm(self.norm_query, query)
1178
+ gate = (key * query).sum(dim=-1, keepdim=True) / math.sqrt(self.hidden_size)
1179
+ gate = torch.sigmoid(gate.sign() * gate.abs().clamp_min(1e-6).sqrt())
1180
+ gated_value = gate * value.unsqueeze(-2)
1181
+ normalized = self._apply_norm(self.norm_conv, gated_value).flatten(-2)
1182
+ conv_output = torch.zeros_like(normalized)
1183
+ torch.ops.vllm.qwen4_exp_ple_short_conv(
1184
+ normalized,
1185
+ conv_output,
1186
+ self.prefix,
1187
+ )
1188
+ return gated_value.flatten(-2) + conv_output
1189
+
1190
+
1191
+def qwen4_exp_compute_ple_ngram_ids(
1192
+ input_ids: torch.Tensor,
1193
+ query_start_loc: torch.Tensor,
1194
+ ngram_context: torch.Tensor,
1195
+ output: torch.Tensor,
1196
+ layer_name: str,
1197
+) -> None:
1198
+ """Compute request-dependent PLE n-gram IDs outside piecewise graphs."""
1199
+ layer = get_forward_context().no_compile_layers[layer_name]
1200
+ ngram_ids = layer.ple_embedding.compute_ngram_ids(
1201
+ input_ids,
1202
+ query_start_loc,
1203
+ ngram_context,
1204
+ )
1205
+ output.copy_(ngram_ids)
1206
+
1207
+
1208
+def qwen4_exp_compute_ple_ngram_ids_fake(
1209
+ input_ids: torch.Tensor,
1210
+ query_start_loc: torch.Tensor,
1211
+ ngram_context: torch.Tensor,
1212
+ output: torch.Tensor,
1213
+ layer_name: str,
1214
+) -> None:
1215
+ return
1216
+
1217
+
1218
+def qwen4_exp_ple_short_conv(
1219
+ inputs: torch.Tensor,
1220
+ output: torch.Tensor,
1221
+ layer_name: str,
1222
+) -> None:
1223
+ layer = get_forward_context().no_compile_layers[layer_name]
1224
+ result = layer._short_conv(inputs)
1225
+ output[: result.shape[0]].copy_(result)
1226
+
1227
+
1228
+def qwen4_exp_ple_short_conv_fake(
1229
+ inputs: torch.Tensor,
1230
+ output: torch.Tensor,
1231
+ layer_name: str,
1232
+) -> None:
1233
+ return
1234
+
1235
+
1236
+direct_register_custom_op(
1237
+ op_name="qwen4_exp_compute_ple_ngram_ids",
1238
+ op_func=qwen4_exp_compute_ple_ngram_ids,
1239
+ mutates_args=["output"],
1240
+ fake_impl=qwen4_exp_compute_ple_ngram_ids_fake,
1241
+)
1242
+
1243
+
1244
+direct_register_custom_op(
1245
+ op_name="qwen4_exp_ple_short_conv",
1246
+ op_func=qwen4_exp_ple_short_conv,
1247
+ mutates_args=["output"],
1248
+ fake_impl=qwen4_exp_ple_short_conv_fake,
1249
+)
1250
+
1251
+
1252
+__all__ = [
1253
+ "Qwen4ExpNGramEmbedding",
1254
+ "Qwen4ExpPLEGroupedNorm",
1255
+ "Qwen4ExpPLELayer",
1256
+]