Hikari07jp commited on
Commit
a493cdb
·
verified ·
1 Parent(s): 5821792

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ learning_curve.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ base_model: z-lab/Qwen3.6-27B-DFlash
4
+ tags:
5
+ - speculative-decoding
6
+ - draft-model
7
+ - dflash
8
+ - dspark
9
+ - qwen
10
+ ---
11
+
12
+ # DSpark-DFlash Draft Head for Qwen3.6-27B-AEON
13
+
14
+ A speculative-decoding **draft head** for the **Qwen3.6-27B-AEON** family, built by
15
+ reproducing and adapting DeepSeek's **DSpark** recipe on top of z-lab's public
16
+ **DFlash** block-diffusion drafter. It combines two drafting paths:
17
+
18
+ 1. **DFlash block-diffusion backbone** — the public [`z-lab/Qwen3.6-27B-DFlash`](https://huggingface.co/z-lab/Qwen3.6-27B-DFlash)
19
+ head (MIT), fine-tuned on on-policy AEON traces.
20
+ 2. **VanillaMarkov sequential head** — a rank-256 head with a bigram bias term, added
21
+ on the DSpark-style semi-autoregressive drafting path.
22
+
23
+ The head is distilled against the **self-generated (on-policy) logits** of a
24
+ Qwen3.6-27B-AEON target, so it is specialized to that target family rather than to
25
+ stock `Qwen/Qwen3.6-27B`.
26
+
27
+ ![Learning curve](learning_curve.png)
28
+
29
+ ---
30
+
31
+ ## Results
32
+
33
+ All numbers below are reported **with their measurement conditions**. `measured`
34
+ means observed on the described harness; nothing here is extrapolated.
35
+
36
+ ### Offline paired acceptance (draft quality)
37
+
38
+ Paired evaluation on **on-policy AEON-generated text** (the draft head scores the
39
+ same target continuations under both drafters), **n = 4052 anchors / 176 sequences**,
40
+ **cluster-bootstrap 95% CI**, **eval K = 8**.
41
+
42
+ Versus the stock z-lab DFlash head:
43
+
44
+ | Metric | This head vs z-lab DFlash |
45
+ |---|---|
46
+ | Accept, **T = 1.0 sampling** | **+32.4%** relative [CI +27%, +38%] |
47
+ | Accept, **greedy** | **+17.7%** relative |
48
+
49
+ Per-domain absolute accepted-length gain (all CIs exclude 0):
50
+
51
+ | Domain | Δ accepted length |
52
+ |---|---|
53
+ | toolcall | +2.83 |
54
+ | chat | +0.80 |
55
+ | code | +0.57 |
56
+ | math | +0.50 |
57
+
58
+ ### End-to-end serving throughput (measured)
59
+
60
+ **vLLM 0.23.0**, **ABBA × 3 rounds**, **K = 8**, **T = 1.0**, **NVFP4 target**,
61
+ **single RTX PRO 6000**.
62
+
63
+ | | This head | z-lab DFlash | Δ |
64
+ |---|---|---|---|
65
+ | Aggregate throughput | **194.8 tok/s** | 175.5 tok/s | **+11.0%** [CI +13.6, +26.5 tok/s] |
66
+ | Accept rate | 0.420 | 0.342 | — |
67
+
68
+ Per-domain throughput gain (all CIs exclude 0):
69
+
70
+ | Domain | Δ tok/s |
71
+ |---|---|
72
+ | code | +15.0% |
73
+ | toolcall | +14.1% |
74
+ | chat | +8.1% |
75
+ | math | +7.1% |
76
+
77
+ ---
78
+
79
+ ## Training
80
+
81
+ | Setting | Value |
82
+ |---|---|
83
+ | Loss | L1 distribution loss (0.9) + teacher-argmax CE (0.1) |
84
+ | `loss_decay_gamma` | 6.0 |
85
+ | `block_size` | 11 |
86
+ | `max_context` | 1024 |
87
+ | Anchors / sequence | 32 |
88
+ | Learning rate | 6e-4, cosine schedule |
89
+ | Steps | 6000 (converged at **~4500** in practice) |
90
+ | Head dtype | bf16 |
91
+ | Target data | **target self-generated, on-policy** |
92
+
93
+ **Corpus** — coding/toolcall-heavy mix, **15,936 sequences** total:
94
+
95
+ - **40%** toolcall (AEON self-play)
96
+ - **25%** real agent sessions (tool-use traces)
97
+ - **35%** general (of which **57%** is code)
98
+
99
+ ---
100
+
101
+ ## Usage
102
+
103
+ ### Target model
104
+
105
+ Designed for the **Qwen3.6-27B-AEON** family (**vocab 248320**). Verified against:
106
+
107
+ - [`AEON-7/Qwen3.6-27B-AEON-Ultimate-Uncensored`](https://huggingface.co/AEON-7/Qwen3.6-27B-AEON-Ultimate-Uncensored) (BF16)
108
+ - [`AEON-7/Qwen3.6-27B-AEON-Ultimate-Uncensored-NVFP4`](https://huggingface.co/AEON-7/Qwen3.6-27B-AEON-Ultimate-Uncensored-NVFP4) (NVFP4, used for the serving numbers above)
109
+
110
+ More broadly, this head is compatible with **Qwen3.6-27B-AEON merges (vocab 248320)**.
111
+ It is distilled to AEON logits and is **not** intended as a drop-in drafter for stock
112
+ `Qwen/Qwen3.6-27B`.
113
+
114
+ ### Install the vLLM patches (required)
115
+
116
+ This head uses a **Markov semi-autoregressive** drafting path that **stock vLLM does
117
+ not implement**, so the two bundled patch files are **required**. They are written
118
+ against **vLLM 0.23.0** — do not apply them to other versions.
119
+
120
+ Overwrite-copy the two files from `vllm_patches/` in this repo into your vLLM install:
121
+
122
+ ```bash
123
+ # from the root of this repo, into your vLLM 0.23.0 site-packages
124
+ cp vllm_patches/qwen3_dflash.py "$VLLM/vllm/model_executor/models/"
125
+ cp vllm_patches/llm_base_proposer.py "$VLLM/vllm/v1/spec_decode/"
126
+ ```
127
+
128
+ (`$VLLM` = the directory containing your installed `vllm` package.)
129
+
130
+ ### Serve
131
+
132
+ ```bash
133
+ vllm serve <target> \
134
+ --speculative-config '{"method":"dflash","model":"<this repo>","num_speculative_tokens":8,"draft_sample_method":"probabilistic"}' \
135
+ --mamba-cache-dtype float32 \
136
+ --attention-backend flash_attn
137
+ ```
138
+
139
+ Replace `<target>` with an AEON target (e.g.
140
+ `AEON-7/Qwen3.6-27B-AEON-Ultimate-Uncensored-NVFP4`) and `<this repo>` with this
141
+ draft-head repo id.
142
+
143
+ ### Optional environment variables
144
+
145
+ - **`DSPARK_MARKOV_TOPN`** — experimental. Truncates the Markov head to its top-N
146
+ candidates to cut per-step overhead in single-stream serving. It measurably
147
+ **lowers acceptance on toolcall**, so it is **off by default** and recommended to
148
+ leave off.
149
+
150
+ ---
151
+
152
+ ## Honest limitations
153
+
154
+ - **Target-specific.** The head is distilled to the logits of a Qwen3.6-27B-AEON
155
+ merge. Gains do not transfer to unrelated targets, including stock
156
+ `Qwen/Qwen3.6-27B`.
157
+ - **Evaluated at K = 8.** The block size is 11 (supports K ≤ 10); all reported
158
+ acceptance and throughput numbers are at K = 8 and are not guaranteed to hold at
159
+ other draft lengths.
160
+ - **Single-GPU numbers.** Serving throughput was measured on one RTX PRO 6000 with
161
+ the NVFP4 target; other hardware, batch regimes, or target quantizations will differ.
162
+ - **Sampling is the strong regime.** The largest wins are under **T = 1.0 sampling**
163
+ (accept +32.4% offline) rather than greedy (+17.7% offline). Greedy-heavy workloads
164
+ will see smaller gains.
165
+
166
+ ---
167
+
168
+ ## Acknowledgements
169
+
170
+ - **z-lab** — the [DFlash](https://github.com/z-lab/dflash) block-diffusion drafter
171
+ ([`z-lab/Qwen3.6-27B-DFlash`](https://huggingface.co/z-lab/Qwen3.6-27B-DFlash), MIT),
172
+ which this head fine-tunes.
173
+ DFlash: Block Diffusion for Flash Speculative Decoding
174
+ ([arXiv:2602.06036](https://arxiv.org/abs/2602.06036)).
175
+ - **DeepSeek** — the **DSpark** paper and the **DeepSpec** reference implementation,
176
+ whose recipe this work reproduces and adapts for the Qwen3.6-27B-AEON target.
177
+
178
+ ## License
179
+
180
+ MIT (inherited from the z-lab DFlash head).
config.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DFlashDraftModel"
4
+ ],
5
+ "auto_map": {
6
+ "AutoModel": "dflash.DFlashDraftModel"
7
+ },
8
+ "model_type": "qwen3",
9
+ "attention_bias": false,
10
+ "attention_dropout": 0.0,
11
+ "block_size": 11,
12
+ "dflash_config": {
13
+ "causal_head": false,
14
+ "causal": false,
15
+ "mask_token_id": 248070,
16
+ "target_layer_ids": [
17
+ 1,
18
+ 16,
19
+ 31,
20
+ 46,
21
+ 61
22
+ ]
23
+ },
24
+ "dtype": "bfloat16",
25
+ "eos_token_id": 248046,
26
+ "pad_token_id": 248044,
27
+ "head_dim": 128,
28
+ "hidden_act": "silu",
29
+ "hidden_size": 5120,
30
+ "initializer_range": 0.02,
31
+ "intermediate_size": 17408,
32
+ "layer_types": [
33
+ "full_attention",
34
+ "full_attention",
35
+ "full_attention",
36
+ "full_attention",
37
+ "full_attention"
38
+ ],
39
+ "max_position_embeddings": 8192,
40
+ "num_attention_heads": 32,
41
+ "num_hidden_layers": 5,
42
+ "num_key_value_heads": 8,
43
+ "num_target_layers": 64,
44
+ "rms_norm_eps": 1e-06,
45
+ "rope_theta": 10000000.0,
46
+ "rope_scaling": null,
47
+ "tie_word_embeddings": false,
48
+ "use_cache": true,
49
+ "use_sliding_window": false,
50
+ "vocab_size": 248320,
51
+ "markov_rank": 256,
52
+ "markov_head_type": "vanilla"
53
+ }
dflash.py ADDED
@@ -0,0 +1,494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, Optional
2
+
3
+ import torch
4
+ from torch import nn
5
+ from transformers import DynamicCache
6
+ from transformers.cache_utils import Cache
7
+ from transformers.modeling_outputs import CausalLMOutputWithPast
8
+ from transformers.models.qwen3.modeling_qwen3 import (
9
+ ALL_ATTENTION_FUNCTIONS,
10
+ FlashAttentionKwargs,
11
+ GradientCheckpointingLayer,
12
+ Qwen3Config,
13
+ Qwen3MLP,
14
+ Qwen3PreTrainedModel,
15
+ Qwen3RMSNorm,
16
+ Qwen3RotaryEmbedding,
17
+ eager_attention_forward,
18
+ rotate_half,
19
+ )
20
+ from typing_extensions import Tuple, Unpack
21
+
22
+ # Optional DSpark-style Markov sequential head (vendored next to this file).
23
+ # Guarded so the rank-0 path -- and any environment lacking markov_head.py --
24
+ # still imports and behaves byte-identically to the original DFlash head.
25
+ try:
26
+ from markov_head import build_markov_head
27
+ except Exception: # pragma: no cover - import guard
28
+ try:
29
+ from .markov_head import build_markov_head # type: ignore
30
+ except Exception:
31
+ build_markov_head = None
32
+
33
+
34
+ def sample(logits: torch.Tensor, temperature: float = 0.0) -> torch.Tensor:
35
+ if temperature < 1e-5:
36
+ return torch.argmax(logits, dim=-1)
37
+ bsz, seq_len, vocab_size = logits.shape
38
+ logits = logits.view(-1, vocab_size)
39
+ logits = logits / temperature
40
+ probs = torch.softmax(logits, dim=-1)
41
+ return torch.multinomial(probs, num_samples=1).view(bsz, seq_len)
42
+
43
+
44
+ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
45
+ cos = cos.unsqueeze(unsqueeze_dim)
46
+ sin = sin.unsqueeze(unsqueeze_dim)
47
+ q_len = q.size(-2)
48
+ q_embed = (q * cos[..., -q_len:, :]) + (rotate_half(q) * sin[..., -q_len:, :])
49
+ k_embed = (k * cos) + (rotate_half(k) * sin)
50
+ return q_embed, k_embed
51
+
52
+
53
+ def _to_additive_attention_mask(
54
+ attention_mask: torch.Tensor,
55
+ *,
56
+ query_dtype: torch.dtype,
57
+ device: torch.device,
58
+ key_len: int,
59
+ ) -> torch.Tensor:
60
+ if attention_mask.ndim == 4:
61
+ attention_mask = attention_mask[:, :, :, :key_len]
62
+ if attention_mask.dtype == torch.bool:
63
+ additive_mask = torch.zeros_like(attention_mask, dtype=query_dtype, device=device)
64
+ return additive_mask.masked_fill(
65
+ attention_mask.logical_not().to(device=device),
66
+ torch.finfo(query_dtype).min,
67
+ )
68
+ return attention_mask.to(device=device, dtype=query_dtype)
69
+
70
+
71
+ def _build_dflash_causal_attention_mask(
72
+ *,
73
+ query: torch.Tensor,
74
+ key: torch.Tensor,
75
+ cached_kv_len: int,
76
+ ctx_len: int,
77
+ ) -> torch.Tensor:
78
+ q_len = query.shape[-2]
79
+ kv_len = key.shape[-2]
80
+ key_positions = torch.arange(kv_len, device=query.device)
81
+ query_positions = cached_kv_len + ctx_len + torch.arange(q_len, device=query.device)
82
+ can_attend = key_positions.unsqueeze(0) <= query_positions.unsqueeze(1)
83
+ mask = torch.zeros((1, 1, q_len, kv_len), dtype=query.dtype, device=query.device)
84
+ return mask.masked_fill(can_attend.logical_not().unsqueeze(0).unsqueeze(0), torch.finfo(query.dtype).min)
85
+
86
+
87
+ class Qwen3DFlashAttention(nn.Module):
88
+ """Multi-headed attention from 'Attention Is All You Need' paper"""
89
+
90
+ def __init__(self, config: Qwen3Config, layer_idx: int):
91
+ super().__init__()
92
+ self.config = config
93
+ self.layer_idx = layer_idx
94
+ self.head_dim = getattr(
95
+ config, "head_dim", config.hidden_size // config.num_attention_heads
96
+ )
97
+ self.num_key_value_groups = (
98
+ config.num_attention_heads // config.num_key_value_heads
99
+ )
100
+ self.scaling = self.head_dim**-0.5
101
+ self.attention_dropout = config.attention_dropout
102
+ dflash_config = getattr(config, "dflash_config", {}) or {}
103
+ self.is_causal = bool(dflash_config.get("causal_head", False))
104
+ self.q_proj = nn.Linear(
105
+ config.hidden_size,
106
+ config.num_attention_heads * self.head_dim,
107
+ bias=config.attention_bias,
108
+ )
109
+ self.k_proj = nn.Linear(
110
+ config.hidden_size,
111
+ config.num_key_value_heads * self.head_dim,
112
+ bias=config.attention_bias,
113
+ )
114
+ self.v_proj = nn.Linear(
115
+ config.hidden_size,
116
+ config.num_key_value_heads * self.head_dim,
117
+ bias=config.attention_bias,
118
+ )
119
+ self.o_proj = nn.Linear(
120
+ config.num_attention_heads * self.head_dim,
121
+ config.hidden_size,
122
+ bias=config.attention_bias,
123
+ )
124
+ self.q_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps)
125
+ self.k_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps)
126
+ self.sliding_window = (
127
+ config.sliding_window
128
+ if config.layer_types[layer_idx] == "sliding_attention"
129
+ else None
130
+ )
131
+
132
+ def forward(
133
+ self,
134
+ hidden_states: torch.Tensor,
135
+ target_hidden: torch.Tensor,
136
+ position_embeddings: tuple[torch.Tensor, torch.Tensor],
137
+ attention_mask: Optional[torch.Tensor],
138
+ past_key_values: Optional[Cache] = None,
139
+ cache_position: Optional[torch.LongTensor] = None,
140
+ **kwargs: Unpack[FlashAttentionKwargs],
141
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
142
+ bsz, q_len = hidden_states.shape[:-1]
143
+ ctx_len = target_hidden.shape[1]
144
+ is_causal = kwargs.pop("is_causal", None)
145
+ if is_causal is None:
146
+ is_causal = self.is_causal
147
+ q = self.q_proj(hidden_states)
148
+ q = q.view(bsz, q_len, -1, self.head_dim)
149
+ q = self.q_norm(q).transpose(1, 2)
150
+ k_ctx = self.k_proj(target_hidden)
151
+ k_noise = self.k_proj(hidden_states)
152
+ v_ctx = self.v_proj(target_hidden)
153
+ v_noise = self.v_proj(hidden_states)
154
+ k = torch.cat([k_ctx, k_noise], dim=1).view(
155
+ bsz, ctx_len + q_len, -1, self.head_dim
156
+ )
157
+ v = torch.cat([v_ctx, v_noise], dim=1).view(
158
+ bsz, ctx_len + q_len, -1, self.head_dim
159
+ )
160
+ k = self.k_norm(k).transpose(1, 2)
161
+ v = v.transpose(1, 2)
162
+ cos, sin = position_embeddings
163
+ q, k = apply_rotary_pos_emb(q, k, cos, sin)
164
+ cached_kv_len = past_key_values.get_seq_length() if past_key_values is not None else 0
165
+ if past_key_values is not None:
166
+ cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
167
+ k, v = past_key_values.update(k, v, self.layer_idx, cache_kwargs)
168
+
169
+ attn_backend = self.config._attn_implementation
170
+ use_explicit_dflash_causal_mask = bool(is_causal) and attn_backend in {"eager", "sdpa"}
171
+ if use_explicit_dflash_causal_mask:
172
+ dflash_causal_mask = _build_dflash_causal_attention_mask(
173
+ query=q, key=k, cached_kv_len=cached_kv_len, ctx_len=ctx_len,
174
+ )
175
+ if attention_mask is not None:
176
+ dflash_causal_mask = dflash_causal_mask + _to_additive_attention_mask(
177
+ attention_mask, query_dtype=q.dtype, device=q.device, key_len=k.shape[-2],
178
+ )
179
+ attention_mask = dflash_causal_mask
180
+ is_causal = False
181
+
182
+ kwargs["is_causal"] = is_causal
183
+
184
+ attn_fn: Callable = eager_attention_forward
185
+ if self.config._attn_implementation != "eager":
186
+ attn_fn = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
187
+ attn_output, attn_weights = attn_fn(
188
+ self,
189
+ q,
190
+ k,
191
+ v,
192
+ attention_mask,
193
+ dropout=0.0 if not self.training else self.attention_dropout,
194
+ scaling=self.scaling,
195
+ sliding_window=self.sliding_window,
196
+ **kwargs,
197
+ )
198
+ attn_output = attn_output.reshape(bsz, q_len, -1)
199
+ attn_output = self.o_proj(attn_output)
200
+ return attn_output, attn_weights
201
+
202
+
203
+ class Qwen3DFlashDecoderLayer(GradientCheckpointingLayer):
204
+ def __init__(self, config: Qwen3Config, layer_idx: int):
205
+ super().__init__()
206
+ self.hidden_size = config.hidden_size
207
+ self.self_attn = Qwen3DFlashAttention(config=config, layer_idx=layer_idx)
208
+ self.mlp = Qwen3MLP(config)
209
+ self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
210
+ self.post_attention_layernorm = Qwen3RMSNorm(
211
+ config.hidden_size, eps=config.rms_norm_eps
212
+ )
213
+
214
+ def forward(
215
+ self,
216
+ target_hidden: Optional[torch.Tensor] = None,
217
+ hidden_states: Optional[torch.Tensor] = None,
218
+ attention_mask: Optional[torch.Tensor] = None,
219
+ position_ids: Optional[torch.LongTensor] = None,
220
+ past_key_value: Optional[Cache] = None,
221
+ output_attentions: Optional[bool] = False,
222
+ use_cache: Optional[bool] = False,
223
+ cache_position: Optional[torch.LongTensor] = None,
224
+ position_embeddings: Optional[
225
+ Tuple[torch.Tensor, torch.Tensor]
226
+ ] = None, # necessary, but kept here for BC
227
+ **kwargs: Unpack[FlashAttentionKwargs],
228
+ ) -> Tuple[
229
+ torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]]
230
+ ]:
231
+ residual = hidden_states
232
+ hidden_states = self.input_layernorm(hidden_states)
233
+ hidden_states = self.self_attn(
234
+ hidden_states=hidden_states,
235
+ target_hidden=target_hidden,
236
+ attention_mask=attention_mask,
237
+ position_ids=position_ids,
238
+ past_key_values=past_key_value,
239
+ output_attentions=output_attentions,
240
+ use_cache=use_cache,
241
+ cache_position=cache_position,
242
+ position_embeddings=position_embeddings,
243
+ **kwargs,
244
+ )[0]
245
+ hidden_states = residual + hidden_states
246
+ residual = hidden_states
247
+ hidden_states = self.post_attention_layernorm(hidden_states)
248
+ hidden_states = self.mlp(hidden_states)
249
+ hidden_states = residual + hidden_states
250
+ return hidden_states
251
+
252
+
253
+ def build_target_layer_ids(num_target_layers: int, num_draft_layers: int):
254
+ if num_draft_layers == 1:
255
+ return [(num_target_layers // 2)]
256
+ start = 1
257
+ end = num_target_layers - 3
258
+ span = end - start
259
+ target_layer_ids = [
260
+ int(round(start + (i * span) / (num_draft_layers - 1)))
261
+ for i in range(num_draft_layers)
262
+ ]
263
+ return target_layer_ids
264
+
265
+
266
+ def extract_context_feature(
267
+ hidden_states: list[torch.Tensor],
268
+ layer_ids: Optional[list[int]],
269
+ ) -> torch.Tensor:
270
+ offset = 1
271
+ selected_states = []
272
+ for layer_id in layer_ids:
273
+ selected_states.append(hidden_states[layer_id + offset])
274
+ target_hidden = torch.cat(selected_states, dim=-1)
275
+ return target_hidden
276
+
277
+
278
+ class DFlashDraftModel(Qwen3PreTrainedModel):
279
+ config_class = Qwen3Config
280
+ _no_split_modules = ["Qwen3DFlashDecoderLayer"]
281
+
282
+ def __init__(self, config) -> None:
283
+ super().__init__(config)
284
+ self.config = config
285
+ if not hasattr(self.config, "dflash_config") or self.config.dflash_config is None:
286
+ self.config.dflash_config = {}
287
+ self.causal_head = bool(self.config.dflash_config.get("causal_head", False))
288
+ self.layers = nn.ModuleList(
289
+ [
290
+ Qwen3DFlashDecoderLayer(config, layer_idx)
291
+ for layer_idx in range(config.num_hidden_layers)
292
+ ]
293
+ )
294
+ dflash_config = getattr(config, "dflash_config", {}) or {}
295
+ self.target_layer_ids = dflash_config.get(
296
+ "target_layer_ids",
297
+ build_target_layer_ids(config.num_target_layers, config.num_hidden_layers),
298
+ )
299
+ self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
300
+ self.rotary_emb = Qwen3RotaryEmbedding(config)
301
+ self.fc = nn.Linear(
302
+ len(self.target_layer_ids) * config.hidden_size,
303
+ config.hidden_size,
304
+ bias=False,
305
+ )
306
+ self.hidden_norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
307
+ self.block_size = config.block_size
308
+ self.mask_token_id = dflash_config.get("mask_token_id", None)
309
+ # Optional DSpark Markov sequential head. markov_rank == 0 (default)
310
+ # -> markov_head is None and the model is byte-identical to plain
311
+ # DFlash (same params, same forward, same checkpoint).
312
+ self.markov_rank = int(getattr(self.config, "markov_rank", 0) or 0)
313
+ self.markov_head_type = getattr(self.config, "markov_head_type", "vanilla")
314
+ self.markov_head = None
315
+ if self.markov_rank > 0:
316
+ if build_markov_head is None:
317
+ raise RuntimeError(
318
+ "config.markov_rank > 0 but markov_head.py could not be imported; "
319
+ "ensure markov_head.py sits next to dflash.py."
320
+ )
321
+ self.markov_head = build_markov_head(
322
+ markov_rank=self.markov_rank,
323
+ vocab_size=config.vocab_size,
324
+ hidden_size=config.hidden_size,
325
+ head_type=self.markov_head_type,
326
+ )
327
+ # Optional DSpark confidence (accept-rate) head. Disabled by default
328
+ # (config.confidence_head missing/False) -> confidence_head is None and
329
+ # the model is byte-identical to the markov/plain DFlash head (same
330
+ # params, same forward, same checkpoint, same config.json). When enabled
331
+ # it is a single Linear(H [+ markov_rank], 1), matching the official
332
+ # DeepSpec AcceptRatePredictor (common.py:43-49). The forward contract
333
+ # is UNCHANGED: the confidence logit is computed by the caller
334
+ # (train_head / feature-cache / vLLM), never inside forward().
335
+ self.confidence_head = None
336
+ if getattr(self.config, "confidence_head", False):
337
+ conf_in = config.hidden_size + (
338
+ self.markov_rank
339
+ if getattr(self.config, "confidence_head_with_markov", False)
340
+ else 0
341
+ )
342
+ self.confidence_head = nn.Linear(conf_in, 1, bias=True)
343
+ self.post_init()
344
+
345
+ def resolve_causal_head(self, head_type: str = "auto") -> bool:
346
+ if head_type == "auto":
347
+ return bool(self.causal_head)
348
+ if head_type == "bidirectional":
349
+ return False
350
+ if head_type == "causal":
351
+ return True
352
+ raise ValueError(
353
+ f"Unsupported head_type={head_type!r}. Expected one of: auto, bidirectional, causal."
354
+ )
355
+
356
+ def forward(
357
+ self,
358
+ position_ids: torch.LongTensor,
359
+ attention_mask: Optional[torch.Tensor] = None,
360
+ noise_embedding: Optional[torch.Tensor] = None,
361
+ target_hidden: Optional[torch.Tensor] = None,
362
+ past_key_values: Optional[Cache] = None,
363
+ use_cache: bool = False,
364
+ **kwargs,
365
+ ) -> CausalLMOutputWithPast:
366
+ hidden_states = noise_embedding
367
+ target_hidden = self.hidden_norm(self.fc(target_hidden))
368
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
369
+ for layer in self.layers:
370
+ hidden_states = layer(
371
+ hidden_states=hidden_states,
372
+ target_hidden=target_hidden,
373
+ attention_mask=attention_mask,
374
+ position_ids=position_ids,
375
+ past_key_value=past_key_values,
376
+ use_cache=use_cache,
377
+ position_embeddings=position_embeddings,
378
+ **kwargs,
379
+ )
380
+ return self.norm(hidden_states)
381
+
382
+ @torch.inference_mode()
383
+ def spec_generate(
384
+ self,
385
+ target: nn.Module,
386
+ input_ids: torch.LongTensor,
387
+ max_new_tokens: int,
388
+ stop_token_ids: list[int],
389
+ temperature: float,
390
+ ):
391
+ self.eval()
392
+ num_input_tokens = input_ids.shape[1]
393
+ max_length = num_input_tokens + max_new_tokens
394
+
395
+ block_size = self.block_size
396
+ output_ids = torch.full(
397
+ (1, max_length + block_size),
398
+ self.mask_token_id,
399
+ dtype=torch.long,
400
+ device=target.device,
401
+ )
402
+ position_ids = torch.arange(
403
+ output_ids.shape[1], device=target.device
404
+ ).unsqueeze(0)
405
+
406
+ past_key_values_target = DynamicCache()
407
+ past_key_values_draft = DynamicCache()
408
+
409
+ # Prefill stage
410
+ output = target(
411
+ input_ids,
412
+ position_ids=position_ids[:, :num_input_tokens],
413
+ past_key_values=past_key_values_target,
414
+ use_cache=True,
415
+ logits_to_keep=1,
416
+ output_hidden_states=True,
417
+ )
418
+
419
+ output_ids[:, :num_input_tokens] = input_ids
420
+ output_ids[:, num_input_tokens : num_input_tokens + 1] = sample(
421
+ output.logits, temperature
422
+ )
423
+ target_hidden = extract_context_feature(
424
+ output.hidden_states, self.target_layer_ids
425
+ )
426
+
427
+ # Decode stage
428
+ acceptance_lengths = []
429
+ start = input_ids.shape[1]
430
+ while start < max_length:
431
+ block_output_ids = output_ids[:, start : start + block_size].clone()
432
+ block_position_ids = position_ids[:, start : start + block_size]
433
+ noise_embedding = target.model.embed_tokens(block_output_ids)
434
+ draft_logits = target.lm_head(
435
+ self(
436
+ target_hidden=target_hidden,
437
+ noise_embedding=noise_embedding,
438
+ position_ids=position_ids[
439
+ :, past_key_values_draft.get_seq_length() : start + block_size
440
+ ],
441
+ past_key_values=past_key_values_draft,
442
+ use_cache=True,
443
+ is_causal=False,
444
+ )[:, -block_size + 1 :, :]
445
+ )
446
+ past_key_values_draft.crop(start)
447
+ block_output_ids[:, 1:] = sample(draft_logits)
448
+ output_ids[:, start + 1 : start + block_size] = block_output_ids[:, 1:]
449
+
450
+ output = target(
451
+ block_output_ids,
452
+ position_ids=block_position_ids,
453
+ past_key_values=past_key_values_target,
454
+ use_cache=True,
455
+ output_hidden_states=True,
456
+ )
457
+
458
+ posterior = sample(output.logits, temperature)
459
+ acceptance_length = (
460
+ (block_output_ids[:, 1:] == posterior[:, :-1])
461
+ .cumprod(dim=1)
462
+ .sum(dim=1)[0]
463
+ .item()
464
+ )
465
+ output_ids[:, start : start + acceptance_length + 1] = block_output_ids[
466
+ :, : acceptance_length + 1
467
+ ]
468
+ output_ids[:, start + acceptance_length + 1] = posterior[
469
+ :, acceptance_length
470
+ ]
471
+ start += acceptance_length + 1
472
+ past_key_values_target.crop(start)
473
+ target_hidden = extract_context_feature(
474
+ output.hidden_states, self.target_layer_ids
475
+ )[:, : acceptance_length + 1, :]
476
+ acceptance_lengths.append(acceptance_length + 1)
477
+ if stop_token_ids is not None and any(
478
+ stop_token_id in output_ids[:, num_input_tokens:]
479
+ for stop_token_id in stop_token_ids
480
+ ):
481
+ break
482
+ output_ids = output_ids[:, :max_length]
483
+ output_ids = output_ids[:, output_ids[0] != self.mask_token_id]
484
+ if stop_token_ids is not None:
485
+ stop_token_ids = torch.tensor(stop_token_ids, device=output_ids.device)
486
+ stop_token_indices = torch.isin(
487
+ output_ids[0][num_input_tokens:], stop_token_ids
488
+ ).nonzero(as_tuple=True)[0]
489
+ if stop_token_indices.numel() > 0:
490
+ output_ids = output_ids[
491
+ :, : num_input_tokens + stop_token_indices[0] + 1
492
+ ]
493
+
494
+ return output_ids
learning_curve.png ADDED

Git LFS Details

  • SHA256: eaed5f0cf062b42d94a5e3ed05d7a61f7e08e037f37d82f422b8146b8a9afba8
  • Pointer size: 131 Bytes
  • Size of remote file: 146 kB
markov_head.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DSpark-style low-rank Markov sequential head (vendored).
2
+
3
+ =============================================================================
4
+ ATTRIBUTION
5
+ =============================================================================
6
+ Adapted from DeepSpec (https://github.com/deepseek-ai/DeepSpec),
7
+ file ``deepspec/modeling/dspark/markov_head.py`` (DSpark / DeepSeek-V4 draft
8
+ head). DeepSpec is released under the MIT License, Copyright (c) 2026 The
9
+ DeepSpec Authors. Only the *VanillaMarkov* head is vendored here (the
10
+ +16-18% accepted-length default that ships in DeepSeek-V4); the gated / RNN
11
+ variants are intentionally omitted to keep the surface minimal.
12
+
13
+ =============================================================================
14
+ WHAT THIS IS
15
+ =============================================================================
16
+ A parallel block-drafter (our DFlashDraftModel) predicts every block position
17
+ in ONE forward from mask-token inputs, so position k cannot see what was
18
+ actually sampled at position k-1 -- this is the "suffix decay" we measured
19
+ ([72,57,45,35,27,22,19,16] top-1 by position).
20
+
21
+ The Markov head fixes that *cheaply* by adding a per-position logit bias that
22
+ conditions on the previous token only:
23
+
24
+ B(x_{k-1}, :) = W2( W1[x_{k-1}] ) W1 in R^{V x r}, W2 in R^{r x V}
25
+
26
+ The corrected logit for position k is U_k + B(x_{k-1}, :) where U_k is the
27
+ backbone's base logit (lm_head(hidden_k)). At TRAIN time x_{k-1} is the
28
+ teacher-forced ground-truth predecessor (apply_block_logits); at INFERENCE
29
+ time x_{k-1} is the actually-sampled draft token, so the block is sampled
30
+ LEFT-TO-RIGHT (sample_block_tokens). This is CHAIN mode (single-block verify)
31
+ -> hybrid-safe (no per-branch SSM-state-fork tax).
32
+
33
+ The head is fully self-contained: it carries its OWN W1/W2 and never touches
34
+ the backbone's (borrowed) embed_tokens / lm_head.
35
+ =============================================================================
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ from typing import Optional
41
+
42
+ import torch
43
+ from torch import nn
44
+
45
+
46
+ def _sample_tokens(logits: torch.Tensor, temperature: float = 0.0) -> torch.Tensor:
47
+ """Greedy (temperature < 1e-5) or temperature multinomial sample.
48
+
49
+ logits: (..., vocab) -> returns (...) long token ids. Mirrors dflash.sample
50
+ semantics so the Markov resample matches the rest of the pipeline."""
51
+ if temperature is None or temperature < 1e-5:
52
+ return torch.argmax(logits, dim=-1)
53
+ *lead, vocab = logits.shape
54
+ flat = (logits / temperature).reshape(-1, vocab)
55
+ probs = torch.softmax(flat, dim=-1)
56
+ return torch.multinomial(probs, num_samples=1).reshape(*lead)
57
+
58
+
59
+ class VanillaMarkov(nn.Module):
60
+ """Memoryless low-rank transition bias B(x_{k-1}) = W2(W1[x_{k-1}])."""
61
+
62
+ def __init__(self, *, vocab_size: int, markov_rank: int):
63
+ super().__init__()
64
+ self.vocab_size = int(vocab_size)
65
+ self.markov_rank = int(markov_rank)
66
+ self.markov_head_type = "vanilla"
67
+ assert self.markov_rank > 0, (
68
+ f"VanillaMarkov requires markov_rank > 0, got {self.markov_rank}."
69
+ )
70
+ self.markov_w1 = nn.Embedding(self.vocab_size, self.markov_rank)
71
+ self.markov_w2 = nn.Linear(self.markov_rank, self.vocab_size, bias=False)
72
+
73
+ def get_prev_embeddings(self, token_ids: torch.Tensor) -> torch.Tensor:
74
+ return self.markov_w1(token_ids.long())
75
+
76
+ def project_bias(self, latent_states: torch.Tensor) -> torch.Tensor:
77
+ return self.markov_w2(latent_states)
78
+
79
+ def compute_step_bias(
80
+ self,
81
+ token_ids: torch.Tensor,
82
+ hidden_states: Optional[torch.Tensor] = None,
83
+ ) -> torch.Tensor:
84
+ # vanilla head ignores hidden_states (pure function of the prev token)
85
+ del hidden_states
86
+ return self.project_bias(self.get_prev_embeddings(token_ids))
87
+
88
+ def apply_step_logits(
89
+ self,
90
+ logits: torch.Tensor, # (B, V)
91
+ *,
92
+ token_ids: torch.Tensor, # (B,)
93
+ hidden_states: Optional[torch.Tensor] = None,
94
+ ) -> torch.Tensor:
95
+ bias = self.compute_step_bias(token_ids, hidden_states)
96
+ return logits + bias.to(logits.dtype)
97
+
98
+ def apply_block_logits(
99
+ self,
100
+ base_logits: torch.Tensor, # (..., M, V)
101
+ *,
102
+ token_ids: torch.Tensor, # (..., M) teacher-forced prev tokens
103
+ hidden_states: Optional[torch.Tensor] = None,
104
+ ) -> torch.Tensor:
105
+ """Train-time teacher-forced bias. Shape-agnostic over leading dims:
106
+ works for our (R, M, V) layout AND the DSpark (B, num_blocks, bs, V)
107
+ layout, since W1/W2 act only on the last dim."""
108
+ if base_logits.numel() == 0 or base_logits.shape[-2] == 0:
109
+ return base_logits
110
+ bias = self.compute_step_bias(token_ids, hidden_states)
111
+ return base_logits + bias.to(base_logits.dtype)
112
+
113
+ @torch.no_grad()
114
+ def sample_block_tokens(
115
+ self,
116
+ base_logits: torch.Tensor, # (B, M, V) backbone base logits
117
+ *,
118
+ first_prev_token_ids: torch.Tensor, # (B,) verified token before pos 0
119
+ hidden_states: Optional[torch.Tensor] = None,
120
+ temperature: float = 0.0,
121
+ ) -> tuple[torch.Tensor, torch.Tensor]:
122
+ """Inference-time LEFT-TO-RIGHT block sampling. Each position's logit is
123
+ biased by the token actually sampled at the previous position.
124
+
125
+ Returns (sampled_tokens (B, M), corrected_logits (B, M, V))."""
126
+ batch_size, proposal_len = base_logits.shape[:2]
127
+ if proposal_len == 0:
128
+ empty = torch.empty(batch_size, 0, dtype=torch.long, device=base_logits.device)
129
+ return empty, base_logits
130
+
131
+ sampled_tokens = []
132
+ corrected_logits = []
133
+ prev_token_ids = first_prev_token_ids.long()
134
+ for step_idx in range(proposal_len):
135
+ step_logits = self.apply_step_logits(
136
+ base_logits[:, step_idx, :],
137
+ token_ids=prev_token_ids,
138
+ # Forward the per-position hidden so a GatedMarkovHead actually
139
+ # gates during LEFT-TO-RIGHT sampling. Was hard-coded None, which
140
+ # silently dropped the gate (vanilla fallback) even when the
141
+ # caller had the hidden -> offline accept sims measured gated as
142
+ # vanilla. None-passing callers keep the vanilla path unchanged.
143
+ hidden_states=(
144
+ hidden_states[:, step_idx, :]
145
+ if hidden_states is not None
146
+ else None
147
+ ),
148
+ )
149
+ corrected_logits.append(step_logits.unsqueeze(1))
150
+ next_token_ids = _sample_tokens(step_logits, temperature=temperature)
151
+ sampled_tokens.append(next_token_ids)
152
+ prev_token_ids = next_token_ids
153
+ return torch.stack(sampled_tokens, dim=1), torch.cat(corrected_logits, dim=1)
154
+
155
+
156
+ class GatedMarkovHead(VanillaMarkov):
157
+ """Gated DSpark Markov head (official DeepSpec GatedMarkovHead).
158
+
159
+ Uses a sigmoid gate conditioned on [hidden_state; prev_embedding] to
160
+ modulate the markov bias. Unlike VanillaMarkov which ignores hidden_states,
161
+ GatedMarkovHead uses the backbone hidden state to adaptively gate the
162
+ bigram bias -- stronger when the backbone is uncertain, weaker when it's
163
+ confident. This should help with the serve pos0 gap we observed.
164
+ """
165
+
166
+ def __init__(self, *, vocab_size: int, markov_rank: int, hidden_size: int):
167
+ super().__init__(vocab_size=vocab_size, markov_rank=markov_rank)
168
+ self.markov_head_type = "gated"
169
+ self.gate_proj = nn.Linear(hidden_size + markov_rank, markov_rank)
170
+
171
+ def compute_gate(
172
+ self,
173
+ token_ids: torch.Tensor,
174
+ hidden_states: torch.Tensor,
175
+ ) -> torch.Tensor:
176
+ prev_embeddings = self.get_prev_embeddings(token_ids)
177
+ # Defensive dtype align: nn.Linear (gate_proj) requires its input in the
178
+ # weight dtype, and torch.cat requires both operands to share a dtype.
179
+ # The caller may hand us hidden_states in a dtype that differs from the
180
+ # head params (e.g. a float32 --head-dtype head fed a bf16 forward hidden,
181
+ # or the serve-side bf16 gate fed an fp32 draft hidden). Cast BOTH cat
182
+ # operands to gate_proj.weight.dtype so neither the concat nor the matmul
183
+ # can raise a dtype mismatch. This is a no-op in the intended paths (whole
184
+ # head is a single dtype), so it changes no numerics.
185
+ w_dtype = self.gate_proj.weight.dtype
186
+ gate_inputs = torch.cat(
187
+ [hidden_states.to(w_dtype), prev_embeddings.to(w_dtype)], dim=-1
188
+ )
189
+ return torch.sigmoid(self.gate_proj(gate_inputs))
190
+
191
+ def compute_step_bias(
192
+ self,
193
+ token_ids: torch.Tensor,
194
+ hidden_states: Optional[torch.Tensor] = None,
195
+ ) -> torch.Tensor:
196
+ if hidden_states is None:
197
+ # Fallback to vanilla when hidden_states not available (e.g., offline eval)
198
+ return self.project_bias(self.get_prev_embeddings(token_ids))
199
+ prev_embeddings = self.get_prev_embeddings(token_ids)
200
+ gate = self.compute_gate(token_ids, hidden_states).to(dtype=prev_embeddings.dtype)
201
+ return self.project_bias(gate * prev_embeddings)
202
+
203
+
204
+ def build_markov_head(
205
+ *,
206
+ markov_rank: int,
207
+ vocab_size: int,
208
+ hidden_size: Optional[int] = None,
209
+ head_type: str = "vanilla",
210
+ ) -> Optional[nn.Module]:
211
+ """Return a Markov head, or None when markov_rank == 0 (head disabled)."""
212
+ markov_rank = int(markov_rank)
213
+ assert markov_rank >= 0, f"markov_rank must be >= 0, got {markov_rank}"
214
+ if markov_rank == 0:
215
+ return None
216
+ head_type = str(head_type).lower()
217
+ if head_type == "vanilla":
218
+ return VanillaMarkov(vocab_size=vocab_size, markov_rank=markov_rank)
219
+ if head_type == "gated":
220
+ assert hidden_size is not None, "GatedMarkovHead requires hidden_size"
221
+ return GatedMarkovHead(
222
+ vocab_size=vocab_size, markov_rank=markov_rank, hidden_size=hidden_size
223
+ )
224
+ raise ValueError(
225
+ f"Unsupported markov_head_type={head_type!r}; only 'vanilla' and 'gated' are vendored."
226
+ )
227
+
228
+
229
+ __all__ = ["VanillaMarkov", "build_markov_head"]
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1e201370eb0b0fe9a5256a5a2c6ed600503e81e4f1bdad3c6a0ac5dea84fc544
3
+ size 3714712368
vllm_patches/llm_base_proposer.py ADDED
@@ -0,0 +1,1907 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+ import os
4
+ from importlib.util import find_spec
5
+ from typing import Any, cast
6
+
7
+ import numpy as np
8
+ import torch
9
+ import torch.nn as nn
10
+
11
+ from vllm.config import (
12
+ CUDAGraphMode,
13
+ VllmConfig,
14
+ get_layers_from_vllm_config,
15
+ replace,
16
+ )
17
+ from vllm.distributed.parallel_state import get_pp_group
18
+ from vllm.forward_context import set_forward_context
19
+ from vllm.logger import init_logger
20
+ from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
21
+ from vllm.model_executor.model_loader import get_model
22
+ from vllm.model_executor.models import supports_multimodal
23
+ from vllm.model_executor.models.deepseek_eagle3 import Eagle3DeepseekV2ForCausalLM
24
+ from vllm.model_executor.models.interfaces import SupportsMultiModal
25
+ from vllm.model_executor.models.llama_eagle3 import Eagle3LlamaForCausalLM
26
+ from vllm.model_executor.models.qwen3_dflash import DFlashQwen3ForCausalLM
27
+ from vllm.multimodal import MULTIMODAL_REGISTRY
28
+ from vllm.platforms import current_platform
29
+ from vllm.utils.platform_utils import is_pin_memory_available
30
+ from vllm.v1.attention.backend import CommonAttentionMetadata
31
+ from vllm.v1.attention.backends.registry import AttentionBackendEnum
32
+ from vllm.v1.attention.backends.triton_attn import TritonAttentionMetadata
33
+ from vllm.v1.cudagraph_dispatcher import CudagraphDispatcher
34
+ from vllm.v1.kv_cache_interface import KVCacheConfig, UniformTypeKVCacheSpecs
35
+ from vllm.v1.sample.metadata import SamplingMetadata
36
+ from vllm.v1.sample.ops.topk_topp_sampler import (
37
+ empty_exponential_noise_like,
38
+ sample_with_exponential_noise,
39
+ )
40
+ from vllm.v1.sample.sampler import _SAMPLING_EPS
41
+ from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
42
+ from vllm.v1.spec_decode.utils import (
43
+ PADDING_SLOT_ID,
44
+ compute_new_slot_mapping,
45
+ copy_and_expand_eagle_inputs_kernel,
46
+ eagle_prepare_inputs_padded_kernel,
47
+ eagle_prepare_next_token_padded_kernel,
48
+ eagle_step_update_slot_mapping_and_metadata,
49
+ extend_all_queries_by_N,
50
+ next_power_of_2,
51
+ )
52
+ from vllm.v1.utils import CpuGpuBuffer
53
+ from vllm.v1.worker.dp_utils import coordinate_batch_across_dp
54
+ from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
55
+ from vllm.v1.worker.utils import AttentionGroup
56
+
57
+ logger = init_logger(__name__)
58
+
59
+
60
+ class SpecDecodeBaseProposer:
61
+ def __init__(
62
+ self,
63
+ vllm_config: VllmConfig,
64
+ device: torch.device,
65
+ pass_hidden_states_to_model: bool,
66
+ runner=None,
67
+ ):
68
+ self.vllm_config = vllm_config
69
+ assert vllm_config.speculative_config is not None
70
+ self.speculative_config = vllm_config.speculative_config
71
+ self.draft_model_config = self.speculative_config.draft_model_config
72
+ self.method = self.speculative_config.method
73
+ self.pass_hidden_states_to_model = pass_hidden_states_to_model
74
+ self._share_mtp_indices = False
75
+
76
+ self.device = device
77
+ self.dtype = vllm_config.model_config.dtype
78
+ self.max_model_len = vllm_config.model_config.max_model_len
79
+ self.dp_rank = vllm_config.parallel_config.data_parallel_rank
80
+ self.num_speculative_tokens = self.speculative_config.num_speculative_tokens
81
+
82
+ # We need to get the hidden size from the draft model config because
83
+ # the draft model's hidden size can be different from the target model's
84
+ # hidden size (e.g., Llama 3.3 70B).
85
+ self.hidden_size = self.draft_model_config.get_hidden_size()
86
+ self.inputs_embeds_size = self.draft_model_config.get_inputs_embeds_size()
87
+
88
+ # DeepSeek V4 MTP consumes the target's pre-hc_head residual stream,
89
+ # shape (T, hc_mult * hidden_size). Expand the hidden_states buffer
90
+ # so target_hidden_states fits; detect DeepseekV4 via draft hf_config.
91
+ draft_hf_config = self.draft_model_config.hf_config
92
+ if hasattr(draft_hf_config, "compress_ratios") and hasattr(
93
+ draft_hf_config, "hc_mult"
94
+ ):
95
+ self.hidden_size = self.hidden_size * draft_hf_config.hc_mult
96
+
97
+ # Unifying eagle, draft model, and parallel drafting support.
98
+ # DFlash always uses parallel drafting (all tokens in one pass),
99
+ # but has an additional slot for the next_token_id (does not shift like EAGLE)
100
+ self.parallel_drafting: bool = self.speculative_config.parallel_drafting
101
+ self.extra_slots_per_request = (
102
+ 1 if not self.parallel_drafting else self.num_speculative_tokens
103
+ )
104
+ self.net_num_new_slots_per_request = self.extra_slots_per_request - (
105
+ 1 if (self.pass_hidden_states_to_model and self.method != "dflash") else 0
106
+ )
107
+ self.needs_extra_input_slots = self.net_num_new_slots_per_request > 0
108
+
109
+ # When True, all draft steps reuse the same position as the
110
+ # first step instead of advancing by one each iteration.
111
+ # Used by draft models with Q-only attention that share KV
112
+ # with the target and always predict from the same position.
113
+ self.constant_draft_positions: bool = False
114
+
115
+ self.parallel_drafting_token_id: int = 0
116
+ self.parallel_drafting_hidden_state_tensor: torch.Tensor | None = None
117
+ if self.parallel_drafting:
118
+ self._init_parallel_drafting_params()
119
+ self.use_local_argmax_reduction: bool = (
120
+ self.speculative_config.use_local_argmax_reduction
121
+ )
122
+ self.use_fp64_gumbel = vllm_config.model_config.use_fp64_gumbel
123
+
124
+ self.max_batch_size = vllm_config.scheduler_config.max_num_seqs
125
+ self.max_num_tokens = vllm_config.scheduler_config.max_num_batched_tokens
126
+ self.token_arange_np = np.arange(self.max_num_tokens, dtype=np.int32)
127
+
128
+ # Can be specialized by methods like DFlash to reduce the limit
129
+ self.max_query_tokens = self.max_num_tokens
130
+ self.max_positions = self.max_num_tokens
131
+
132
+ # Multi-modal data support
133
+ self.mm_registry = MULTIMODAL_REGISTRY
134
+ self.supports_mm_inputs = self.mm_registry.supports_multimodal_inputs(
135
+ vllm_config.model_config
136
+ )
137
+
138
+ self.draft_attn_groups: list[AttentionGroup] = []
139
+ self.kv_cache_gid: int = -1
140
+ self.eagle3_use_aux_hidden_state: bool = (
141
+ self._get_eagle3_use_aux_hidden_state_from_config()
142
+ )
143
+
144
+ self.compilation_config = self.vllm_config.compilation_config
145
+
146
+ # Cudagraph dispatcher for PIECEWISE-only dispatching in eagle.
147
+ # Keys are initialized later via initialize_cudagraph_keys() called from
148
+ # gpu_model_runner._check_and_update_cudagraph_mode after
149
+ # adjust_cudagraph_sizes_for_spec_decode is called.
150
+ self.cudagraph_dispatcher = CudagraphDispatcher(self.vllm_config)
151
+
152
+ # persistent buffers for cuda graph
153
+ self.input_ids = torch.zeros(
154
+ self.max_num_tokens, dtype=torch.int32, device=device
155
+ )
156
+ # Use draft model's M-RoPE setting, not target model's
157
+ # Draft models may be text-only even if target is multimodal
158
+ self.uses_mrope = self.draft_model_config.uses_mrope
159
+ self.uses_xdrope_dim = self.vllm_config.model_config.uses_xdrope_dim
160
+ self.draft_uses_xdrope_dim = self.draft_model_config.uses_xdrope_dim
161
+ if self.uses_mrope:
162
+ # NOTE: `mrope_positions` is implemented with one additional dummy
163
+ # position on purpose to make it non-contiguous so that it can work
164
+ # with torch compile.
165
+ # See detailed explanation in https://github.com/vllm-project/vllm/pull/12128#discussion_r1926431923
166
+
167
+ # NOTE: When M-RoPE is enabled, position ids are 3D regardless of
168
+ # the modality of inputs. For text-only inputs, each dimension has
169
+ # identical position IDs, making M-RoPE functionally equivalent to
170
+ # 1D-RoPE.
171
+ # See page 5 of https://arxiv.org/abs/2409.12191
172
+ self.mrope_positions = torch.zeros(
173
+ (3, self.max_positions + 1), dtype=torch.int64, device=device
174
+ )
175
+ elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
176
+ self.xdrope_positions = torch.zeros(
177
+ (self.uses_xdrope_dim, self.max_positions + 1),
178
+ dtype=torch.int64,
179
+ device=device,
180
+ )
181
+ else:
182
+ # RoPE need (max_num_tokens,)
183
+ self.positions = torch.zeros(
184
+ self.max_positions,
185
+ dtype=torch.int64,
186
+ device=device,
187
+ )
188
+ self.hidden_states = torch.zeros(
189
+ (self.max_num_tokens, self.hidden_size), dtype=self.dtype, device=device
190
+ )
191
+
192
+ # Will be set when we initialize the attention backend
193
+ self.block_size: int = -1
194
+
195
+ # We need +1 here because the arange is used to set query_start_loc,
196
+ # which has one more element than batch_size.
197
+ max_num_slots_for_arange = max(self.max_batch_size + 1, self.max_num_tokens)
198
+ self.arange = torch.arange(
199
+ max_num_slots_for_arange, device=device, dtype=torch.int32
200
+ )
201
+
202
+ if self.needs_extra_input_slots:
203
+ self._raise_if_padded_drafter_batch_disabled()
204
+ self._warn_if_multimodal()
205
+ self._raise_if_mrope()
206
+
207
+ self.is_rejected_token_mask: torch.Tensor | None = None
208
+ self.is_masked_token_mask: torch.Tensor | None = None
209
+ if self.needs_extra_input_slots:
210
+ # For draft models and parallel drafting, we need to keep track of
211
+ # which tokens are rejected to update the slot mapping with padding slots.
212
+ self.is_rejected_token_mask = torch.zeros(
213
+ (self.max_num_tokens,), dtype=torch.bool, device=device
214
+ )
215
+ # For parallel drafting, we also need to keep track of which tokens
216
+ # are parallel-padding tokens used to sample at later positions.
217
+ # We populate this tensor even when using draft models for simplicity.
218
+ self.is_masked_token_mask = torch.zeros(
219
+ (self.max_num_tokens,), dtype=torch.bool, device=device
220
+ )
221
+
222
+ self.inputs_embeds = torch.zeros(
223
+ (self.max_num_tokens, self.inputs_embeds_size),
224
+ dtype=self.dtype,
225
+ device=device,
226
+ )
227
+
228
+ self.backup_next_token_ids = CpuGpuBuffer(
229
+ self.max_batch_size,
230
+ dtype=torch.int32,
231
+ pin_memory=is_pin_memory_available(),
232
+ device=device,
233
+ with_numpy=True,
234
+ )
235
+ self._enable_probabilistic_draft_probs = (
236
+ self.speculative_config.rejection_sample_method == "standard"
237
+ and self.speculative_config.draft_sample_method == "probabilistic"
238
+ )
239
+ self._last_draft_probs: torch.Tensor | None = None
240
+ # DSpark confidence-head dynamic-K threshold (serve-time knob, env var).
241
+ # 0.0 (default, unset) => no truncation: full num_spec block every step,
242
+ # i.e. byte-identical to the no-confidence behavior (official
243
+ # _confident_prefix_length returns block_size when threshold<=0). A
244
+ # positive threshold trims each draft row to its confident PREFIX.
245
+ try:
246
+ self._conf_threshold: float = float(
247
+ os.environ.get("DSPARK_CONF_THRESHOLD", "0.0") or 0.0
248
+ )
249
+ except (TypeError, ValueError):
250
+ self._conf_threshold = 0.0
251
+ # Per-row confident prefix lengths for the last proposed block, in
252
+ # input_batch row order ([B] list of ints), or None when no truncation.
253
+ self._last_draft_prefix_lengths: list[int] | None = None
254
+
255
+ self._slot_mapping_buffer = torch.zeros(
256
+ self.max_positions,
257
+ dtype=torch.int64,
258
+ device=device,
259
+ )
260
+
261
+ # Determine allowed attention backends once during initialization.
262
+ self.allowed_attn_types: tuple | None = None
263
+ if current_platform.is_rocm():
264
+ from vllm.models.deepseek_v4.amd.rocm import (
265
+ DeepseekV4ROCMAiterMLASparseMetadata,
266
+ DeepseekV4ROCMAiterSparseSWAMetadata,
267
+ )
268
+ from vllm.v1.attention.backends.mla.indexer import (
269
+ DeepseekV32IndexerMetadata,
270
+ )
271
+ from vllm.v1.attention.backends.mla.rocm_aiter_mla_sparse import (
272
+ ROCMAiterMLASparseMetadata,
273
+ )
274
+ from vllm.v1.attention.backends.rocm_attn import RocmAttentionMetadata
275
+
276
+ rocm_types = [
277
+ TritonAttentionMetadata,
278
+ RocmAttentionMetadata,
279
+ ROCMAiterMLASparseMetadata,
280
+ DeepseekV4ROCMAiterMLASparseMetadata,
281
+ DeepseekV4ROCMAiterSparseSWAMetadata,
282
+ DeepseekV32IndexerMetadata,
283
+ ]
284
+ # ROCM_AITER_FA is an optional backend
285
+ # We check is_enabled() here to avoid importing the backend module during
286
+ # auto-discovery when VLLM_ROCM_USE_AITER=0, which would trigger aiter
287
+ # import and JIT compilation warnings. Explicit backend selection via
288
+ # attention_config still works because the backend module is loaded
289
+ # directly when selected, not through this auto-discovery path.
290
+ # Check if backend module exists to allow explicit selection
291
+ if find_spec(
292
+ AttentionBackendEnum.ROCM_AITER_FA.get_path(include_classname=False)
293
+ ):
294
+ from vllm.v1.attention.backends.rocm_aiter_fa import (
295
+ AiterFlashAttentionMetadata,
296
+ )
297
+
298
+ rocm_types.append(AiterFlashAttentionMetadata)
299
+
300
+ # TRITON_MLA backend support for MLA models (e.g., DeepSeek)
301
+ from vllm.model_executor.layers.attention.mla_attention import (
302
+ MLACommonMetadata,
303
+ )
304
+
305
+ rocm_types.append(MLACommonMetadata)
306
+
307
+ # FlexAttention backend support
308
+ from vllm.v1.attention.backends.flex_attention import FlexAttentionMetadata
309
+
310
+ rocm_types.append(FlexAttentionMetadata)
311
+
312
+ self.allowed_attn_types = tuple(rocm_types)
313
+
314
+ def _raise_if_padded_drafter_batch_disabled(self):
315
+ if self.speculative_config.disable_padded_drafter_batch:
316
+ raise NotImplementedError(
317
+ "Speculative Decoding with draft models or parallel drafting only "
318
+ "supports padded drafter batch. Please unset "
319
+ "disable_padded_drafter_batch in the speculative_config."
320
+ )
321
+
322
+ def _warn_if_multimodal(self):
323
+ if self.supports_mm_inputs:
324
+ logger.warning(
325
+ "Speculative Decoding with draft models or parallel drafting "
326
+ "does not fully support multimodal models yet. "
327
+ "Proceeding with text-only speculative decoding."
328
+ )
329
+
330
+ def _raise_if_mrope(self):
331
+ if self.draft_model_config.uses_mrope:
332
+ raise NotImplementedError(
333
+ "Speculative Decoding with draft models or parallel drafting "
334
+ "does not support M-RoPE yet"
335
+ )
336
+
337
+ def _init_parallel_drafting_params(self):
338
+ # For parallel drafting, we need the token ID to use for masked slots
339
+ # And for EAGLE + parallel drafting, we need the hidden state tensor to use
340
+ # for those masked slots.
341
+
342
+ model_hf_config = self.draft_model_config.hf_config
343
+ # DFlash stores mask_token_id in dflash_config
344
+ dflash_config = getattr(model_hf_config, "dflash_config", None)
345
+ if dflash_config and "mask_token_id" in dflash_config:
346
+ self.parallel_drafting_token_id = dflash_config["mask_token_id"]
347
+ elif hasattr(model_hf_config, "pard_token"):
348
+ self.parallel_drafting_token_id = model_hf_config.pard_token
349
+ elif hasattr(model_hf_config, "ptd_token_id"):
350
+ self.parallel_drafting_token_id = model_hf_config.ptd_token_id
351
+ else:
352
+ raise ValueError(
353
+ "For parallel drafting, the draft model config must have "
354
+ "`pard_token`, `ptd_token_id`, or "
355
+ "`dflash_config.mask_token_id` specified in its config.json."
356
+ )
357
+
358
+ if self.pass_hidden_states_to_model:
359
+ self.parallel_drafting_hidden_state_tensor = torch.empty(
360
+ self.hidden_size, dtype=self.dtype, device=self.device
361
+ )
362
+
363
+ def _get_positions(self, num_tokens: int):
364
+ if self.uses_mrope:
365
+ return self.mrope_positions[:, :num_tokens]
366
+ if self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
367
+ return self.xdrope_positions[:, :num_tokens]
368
+ return self.positions[:num_tokens]
369
+
370
+ def _set_positions(self, num_tokens: int, positions: torch.Tensor):
371
+ if self.uses_mrope:
372
+ self.mrope_positions[:, :num_tokens] = positions
373
+ elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
374
+ self.xdrope_positions[:, :num_tokens] = positions
375
+ else:
376
+ # Convert M-RoPE positions if target model uses M-RoPE
377
+ # but draft doesn't, For text inputs, all M-RoPE
378
+ # dimensions are identical
379
+ if self.vllm_config.model_config.uses_mrope:
380
+ positions = positions[0]
381
+ self.positions[:num_tokens] = positions
382
+
383
+ def _get_slot_mapping(
384
+ self,
385
+ num_tokens: int,
386
+ slot_mapping: torch.Tensor | None = None,
387
+ ) -> dict[str, torch.Tensor]:
388
+ """Return slot_mapping dict for EAGLE layers.
389
+
390
+ If slot_mapping is provided, copies it into the buffer first.
391
+ """
392
+ if slot_mapping is not None:
393
+ num_actual = slot_mapping.shape[0]
394
+ self._slot_mapping_buffer[:num_actual].copy_(slot_mapping)
395
+ if num_tokens > num_actual:
396
+ self._slot_mapping_buffer[num_actual:num_tokens].fill_(PADDING_SLOT_ID)
397
+
398
+ view = self._slot_mapping_buffer[:num_tokens]
399
+ return {name: view for name in self._draft_attn_layer_names}
400
+
401
+ def initialize_cudagraph_keys(self, cudagraph_mode: CUDAGraphMode) -> None:
402
+ """Initialize cudagraph dispatcher keys for the drafter.
403
+
404
+ Only supports PIECEWISE cudagraphs (via mixed_mode).
405
+ This should be called after adjust_cudagraph_sizes_for_spec_decode.
406
+ """
407
+ if (
408
+ not self.speculative_config.enforce_eager
409
+ and cudagraph_mode.mixed_mode()
410
+ in [CUDAGraphMode.PIECEWISE, CUDAGraphMode.FULL]
411
+ ):
412
+ eagle_cudagraph_mode = CUDAGraphMode.PIECEWISE
413
+ else:
414
+ eagle_cudagraph_mode = CUDAGraphMode.NONE
415
+
416
+ self.cudagraph_dispatcher.initialize_cudagraph_keys(eagle_cudagraph_mode)
417
+
418
+ def _greedy_sample(self, hidden_states: torch.Tensor) -> torch.Tensor:
419
+ """Greedy-sample draft tokens from hidden states."""
420
+ if self.use_local_argmax_reduction:
421
+ return self.model.get_top_tokens(hidden_states)
422
+ return self.model.compute_logits(hidden_states).argmax(dim=-1)
423
+
424
+ def _sample_from_logits(
425
+ self,
426
+ logits: torch.Tensor,
427
+ sampling_metadata: SamplingMetadata,
428
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
429
+ if not self._enable_probabilistic_draft_probs:
430
+ return logits.argmax(dim=-1), None
431
+ if sampling_metadata.all_greedy:
432
+ return logits.argmax(dim=-1), None
433
+ return compute_probs_and_sample_next_token(
434
+ logits, sampling_metadata, self.use_fp64_gumbel
435
+ )
436
+
437
+ def _sample_draft_tokens(
438
+ self,
439
+ hidden_states: torch.Tensor,
440
+ sampling_metadata: SamplingMetadata,
441
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
442
+ if not self._enable_probabilistic_draft_probs or sampling_metadata.all_greedy:
443
+ return self._greedy_sample(hidden_states), None
444
+ logits = self.model.compute_logits(hidden_states)
445
+ return self._sample_from_logits(logits, sampling_metadata)
446
+
447
+ def take_last_draft_probs(self) -> torch.Tensor | None:
448
+ return self._last_draft_probs
449
+
450
+ def take_last_draft_prefix_lengths(self) -> list[int] | None:
451
+ """Per-row confident PREFIX lengths for the just-proposed block ([B] ints
452
+ in input_batch row order), or None when dynamic-K is inactive. Read by
453
+ the model runner right after propose(), mirroring take_last_draft_probs."""
454
+ return self._last_draft_prefix_lengths
455
+
456
+ def _compute_conf_prefix_lengths(
457
+ self,
458
+ sample_hidden_states: torch.Tensor,
459
+ draft_block: torch.Tensor,
460
+ first_prev_token_ids: torch.Tensor,
461
+ ) -> None:
462
+ """DSpark confidence-head dynamic-K: per-row confident PREFIX length.
463
+
464
+ Sets self._last_draft_prefix_lengths to a [B] list giving, for each row,
465
+ the number of LEADING draft positions whose accept-rate confidence is
466
+ >= threshold (== first position with sigmoid(conf) < threshold; official
467
+ draft_ops._confident_prefix_length). No-op (None) unless a positive
468
+ threshold is configured AND the draft model carries a confidence head.
469
+
470
+ The lengths are applied downstream as a PREFIX slice (drop the tail only)
471
+ in GPUModelRunner.take_draft_token_ids, which keeps the draft_probs
472
+ [row, :num_draft] slice and the ragged rejection sampler structurally
473
+ aligned -> lossless regardless of confidence quality.
474
+ """
475
+ self._last_draft_prefix_lengths = None
476
+ _dbg = bool(os.environ.get("DSPARK_DEBUG"))
477
+ # MEASURE mode: compute + log the head's per-position predicted accept
478
+ # (sigmoid) even when threshold<=0 (no truncation), to compare serve-path
479
+ # head calibration against the realized per-position acceptance rates that
480
+ # vLLM logs in its SpecDecoding metrics. Answers "is the head accurate at
481
+ # serve?" directly instead of trusting the offline AUROC.
482
+ _measure = bool(os.environ.get("DSPARK_MEASURE"))
483
+ if self._conf_threshold <= 0.0 and not _measure:
484
+ return
485
+ conf_head = getattr(getattr(self.model, "model", None), "confidence_head", None)
486
+ if conf_head is None:
487
+ return
488
+ conf_logits = self.model.predict_confidence_step(
489
+ sample_hidden_states,
490
+ draft_block,
491
+ first_prev_token_ids,
492
+ self.num_speculative_tokens,
493
+ ) # [B, num_spec] float32
494
+ probs = conf_logits.sigmoid()
495
+ if _measure:
496
+ # accumulate per-position predicted sigmoid (sum + count) and log a
497
+ # running per-position mean; offline-comparable to realized per-pos accept.
498
+ pos_sum = getattr(self, "_dspark_pos_sum", None)
499
+ if pos_sum is None:
500
+ pos_sum = probs.float().sum(0)
501
+ self._dspark_pos_cnt = probs.shape[0]
502
+ else:
503
+ pos_sum = pos_sum + probs.float().sum(0)
504
+ self._dspark_pos_cnt += probs.shape[0]
505
+ self._dspark_pos_sum = pos_sum
506
+ n = getattr(self, "_dspark_meas_n", 0)
507
+ if n < 40:
508
+ self._dspark_meas_n = n + 1
509
+ mean_pp = (pos_sum / self._dspark_pos_cnt).tolist()
510
+ logger.info(
511
+ "DSPARK measure perpos_pred_accept(mean)=%s (rows=%d)",
512
+ [round(x, 3) for x in mean_pp], self._dspark_pos_cnt,
513
+ )
514
+ # keep[:, k] = 1 while confident; confident PREFIX length = number of
515
+ # leading 1s = sum of the running cumprod (zeros out after the first 0).
516
+ if self._conf_threshold <= 0.0:
517
+ return # measure-only: no truncation
518
+ keep = (probs >= self._conf_threshold).to(torch.int32)
519
+ lengths = torch.cumprod(keep, dim=1).sum(dim=1) # [B] in [0, num_spec]
520
+ if _dbg:
521
+ n = getattr(self, "_dspark_dbg_n", 0)
522
+ if n < 8:
523
+ self._dspark_dbg_n = n + 1
524
+ p = probs.float()
525
+ logger.info(
526
+ "DSPARK conf: thr=%.3f sigmoid[min=%.3f mean=%.3f max=%.3f] "
527
+ "lengths=%s (num_spec=%d)",
528
+ self._conf_threshold,
529
+ p.min().item(),
530
+ p.mean().item(),
531
+ p.max().item(),
532
+ lengths.to("cpu").tolist(),
533
+ self.num_speculative_tokens,
534
+ )
535
+ self._last_draft_prefix_lengths = lengths.to(
536
+ "cpu", dtype=torch.int32
537
+ ).tolist()
538
+
539
+ def propose(
540
+ self,
541
+ # [num_tokens]
542
+ target_token_ids: torch.Tensor,
543
+ # [num_tokens] or [3, num_tokens] when M-RoPE is enabled
544
+ target_positions: torch.Tensor,
545
+ # [num_tokens, hidden_size]
546
+ target_hidden_states: torch.Tensor,
547
+ # [batch_size]
548
+ next_token_ids: torch.Tensor,
549
+ token_indices_to_sample: torch.Tensor | None,
550
+ common_attn_metadata: CommonAttentionMetadata,
551
+ sampling_metadata: SamplingMetadata,
552
+ mm_embed_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
553
+ num_rejected_tokens_gpu: torch.Tensor | None = None,
554
+ slot_mappings: dict[str, torch.Tensor]
555
+ | list[dict[str, torch.Tensor]]
556
+ | None = None,
557
+ ) -> torch.Tensor:
558
+ self._last_draft_probs = None
559
+ self._last_draft_prefix_lengths = None
560
+ batch_size = common_attn_metadata.batch_size()
561
+
562
+ if self.method in ("eagle3", "dflash"):
563
+ assert isinstance(
564
+ self.model,
565
+ (
566
+ Eagle3LlamaForCausalLM,
567
+ Eagle3DeepseekV2ForCausalLM,
568
+ DFlashQwen3ForCausalLM,
569
+ ),
570
+ )
571
+ target_hidden_states = self.model.combine_hidden_states(
572
+ target_hidden_states
573
+ )
574
+ assert target_hidden_states.shape[-1] == self.hidden_size
575
+
576
+ num_tokens, token_indices_to_sample, common_attn_metadata = (
577
+ self.set_inputs_first_pass(
578
+ target_token_ids=target_token_ids,
579
+ next_token_ids=next_token_ids,
580
+ target_positions=target_positions,
581
+ target_hidden_states=target_hidden_states,
582
+ token_indices_to_sample=token_indices_to_sample,
583
+ cad=common_attn_metadata,
584
+ num_rejected_tokens_gpu=num_rejected_tokens_gpu,
585
+ )
586
+ )
587
+
588
+ per_group_attn_metadata, per_layer_attn_metadata = (
589
+ self.build_per_group_and_layer_attn_metadata(common_attn_metadata)
590
+ )
591
+
592
+ cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
593
+ self._determine_batch_execution_and_padding(num_tokens)
594
+ )
595
+
596
+ model_kwargs, slot_mapping_size = self.build_model_inputs_first_pass(
597
+ num_tokens, num_input_tokens, mm_embed_inputs
598
+ )
599
+ # Step 0 of index_share_for_mtp_iteration: let the MTP layer
600
+ # compute its own indices (skip_topk=False) so subsequent steps
601
+ # can reuse them.
602
+ if self._share_mtp_indices and hasattr(self.model.model, "set_skip_topk"):
603
+ self.model.model.set_skip_topk(False)
604
+
605
+ with set_forward_context(
606
+ per_layer_attn_metadata,
607
+ self.vllm_config,
608
+ num_tokens=num_input_tokens,
609
+ num_tokens_across_dp=num_tokens_across_dp,
610
+ cudagraph_runtime_mode=cudagraph_runtime_mode,
611
+ slot_mapping=self._get_slot_mapping(
612
+ slot_mapping_size, common_attn_metadata.slot_mapping
613
+ ),
614
+ ):
615
+ ret_hidden_states = self.model(**model_kwargs)
616
+ if not self.model_returns_tuple():
617
+ last_hidden_states = ret_hidden_states
618
+ hidden_states = last_hidden_states
619
+ else:
620
+ last_hidden_states, hidden_states = ret_hidden_states
621
+
622
+ # After step 0: switch to reuse mode so steps 1+ skip the indexer
623
+ # and read the indices that step 0 just wrote into the shared buffer.
624
+ if self._share_mtp_indices and hasattr(self.model.model, "set_skip_topk"):
625
+ self.model.model.set_skip_topk(True)
626
+
627
+ sample_hidden_states = last_hidden_states[token_indices_to_sample]
628
+
629
+ # Early exit if there is only one draft token to be generated.
630
+ if self.num_speculative_tokens == 1 or self.parallel_drafting:
631
+ # DSpark VanillaMarkov semi-AR greedy shortcut: when the draft model
632
+ # carries a markov head and the whole batch is greedy, draft the
633
+ # parallel block LEFT-TO-RIGHT with a per-position markov logit bias
634
+ # instead of one parallel argmax. LOSSLESS under greedy: the target
635
+ # verify re-checks every token vs the target argmax, independent of
636
+ # how the draft was produced. next_token_ids is the verified bonus
637
+ # token before draft pos 0 (== markov first_prev). This path
638
+ # intentionally takes priority over the use_local_argmax_reduction
639
+ # get_top_tokens branch inside _sample_draft_tokens, which would
640
+ # bypass full logits and drop the markov bias.
641
+ mk = getattr(getattr(self.model, "model", None), "markov_head", None)
642
+ if (
643
+ self.parallel_drafting
644
+ and self.num_speculative_tokens > 1
645
+ and mk is not None
646
+ and sampling_metadata.all_greedy
647
+ ):
648
+ # [B, num_spec], already matching the .view(-1, num_spec) contract
649
+ draft_block = self.model.sample_draft_block_semiar(
650
+ sample_hidden_states,
651
+ next_token_ids,
652
+ self.num_speculative_tokens,
653
+ )
654
+ # DSpark confidence-head dynamic-K (PREFIX-ONLY truncate); no-op
655
+ # unless DSPARK_CONF_THRESHOLD>0. Greedy realized prev == argmax'd
656
+ # block (semi-AR), so confidence reads the true serve predecessor.
657
+ self._compute_conf_prefix_lengths(
658
+ sample_hidden_states, draft_block, next_token_ids
659
+ )
660
+ return draft_block
661
+ # DSpark VanillaMarkov semi-AR SAMPLING shortcut (temp>0 lossless).
662
+ # Only taken in the probabilistic regime where draft_probs ARE
663
+ # consumed by the rejection sampler (self._enable_probabilistic_draft_probs)
664
+ # AND the batch is not all-greedy (mixed/random). The L->R markov
665
+ # draw reports the EXACT q_k it sampled from, so standard rejection
666
+ # sampling stays lossless. CRITICAL GUARD: if the probabilistic gate
667
+ # is off, draft_probs would be None -- a markov-SAMPLED draft without
668
+ # a reported q is NON-lossless -- so we must fall through to the
669
+ # existing greedy-argmax draft (correctly one-hot) instead. The
670
+ # all_greedy case is handled by the greedy markov branch above.
671
+ if (
672
+ self.parallel_drafting
673
+ and self.num_speculative_tokens > 1
674
+ and mk is not None
675
+ and self._enable_probabilistic_draft_probs
676
+ and not sampling_metadata.all_greedy
677
+ ):
678
+ sampled_tokens, corrected_probs = (
679
+ self.model.sample_draft_block_semiar_sample(
680
+ sample_hidden_states,
681
+ next_token_ids,
682
+ self.num_speculative_tokens,
683
+ sampling_metadata.temperature,
684
+ sampling_metadata.all_random,
685
+ self.use_fp64_gumbel,
686
+ )
687
+ )
688
+ # Match the parallel path's _last_draft_probs contract EXACTLY
689
+ # (shape [B, num_spec, V], float32, contiguous); see the
690
+ # draft_probs.view(...).contiguous() population below.
691
+ self._last_draft_probs = corrected_probs.view(
692
+ -1, self.num_speculative_tokens, corrected_probs.shape[-1]
693
+ ).contiguous()
694
+ # DSpark confidence-head dynamic-K (PREFIX-ONLY truncate); no-op
695
+ # unless DSPARK_CONF_THRESHOLD>0. Realized prev == the L->R SAMPLED
696
+ # block, so confidence reads the true serve predecessor. PREFIX
697
+ # truncation stays lossless: q_0..q_{L-1} in _last_draft_probs are
698
+ # unchanged and the verifier slices draft_probs[row, :num_draft].
699
+ self._compute_conf_prefix_lengths(
700
+ sample_hidden_states, sampled_tokens, next_token_ids
701
+ )
702
+ return sampled_tokens.view(-1, self.num_speculative_tokens)
703
+ draft_token_ids, draft_probs = self._sample_draft_tokens(
704
+ sample_hidden_states, sampling_metadata
705
+ )
706
+ if draft_probs is not None:
707
+ self._last_draft_probs = draft_probs.view(
708
+ -1, self.num_speculative_tokens, draft_probs.shape[-1]
709
+ ).contiguous()
710
+ return draft_token_ids.view(-1, self.num_speculative_tokens)
711
+
712
+ if self.uses_mrope:
713
+ positions = self.mrope_positions[:, token_indices_to_sample]
714
+ else:
715
+ positions = self.positions[token_indices_to_sample]
716
+ hidden_states = hidden_states[token_indices_to_sample]
717
+
718
+ if self.constant_draft_positions:
719
+ # Write the sampling positions into the front of the
720
+ # positions buffer so that subsequent loop iterations
721
+ # (which read via _get_positions) use the correct values.
722
+ self.positions[:batch_size] = positions
723
+
724
+ draft_token_ids, draft_probs = self._sample_draft_tokens(
725
+ sample_hidden_states, sampling_metadata
726
+ )
727
+ draft_probs_list = None if draft_probs is None else [draft_probs]
728
+
729
+ if self.allowed_attn_types is not None:
730
+ for group_md in per_group_attn_metadata:
731
+ if not isinstance(group_md, self.allowed_attn_types):
732
+ raise ValueError(
733
+ f"Unsupported attention metadata type for speculative "
734
+ "decoding with num_speculative_tokens > 1: "
735
+ f"{type(group_md)}. Supported types are: "
736
+ f"{self.allowed_attn_types}"
737
+ )
738
+
739
+ # Generate the remaining draft tokens.
740
+ draft_token_ids_list = [draft_token_ids]
741
+
742
+ cudagraph_runtime_mode, input_batch_size, batch_size_across_dp = (
743
+ self._determine_batch_execution_and_padding(batch_size)
744
+ )
745
+
746
+ common_attn_metadata.num_actual_tokens = batch_size
747
+ common_attn_metadata.max_query_len = 1
748
+ common_attn_metadata.query_start_loc = self.arange[: batch_size + 1]
749
+ common_attn_metadata.query_start_loc_cpu = torch.from_numpy(
750
+ self.token_arange_np[: batch_size + 1]
751
+ ).clone()
752
+
753
+ # In padded drafter batch, we need to adjust the sequence lengths
754
+ # to remove the "padding" (i.e. rejected tokens).
755
+ # Only apply this adjustment when we have rejected tokens
756
+ # (i.e., not the first proposal).
757
+ if self.num_speculative_tokens > 1 and num_rejected_tokens_gpu is not None:
758
+ common_attn_metadata.seq_lens -= num_rejected_tokens_gpu
759
+ # Invalidate the CPU-side shadows to avoid H<>D sync.
760
+ common_attn_metadata._seq_lens_cpu = None
761
+ common_attn_metadata._num_computed_tokens_cpu = None
762
+
763
+ block_size = self.block_size
764
+ assert block_size > 0, "block_size has not been initialized."
765
+ for token_index in range(self.num_speculative_tokens - 1):
766
+ # Update the inputs.
767
+ # cast to int32 is crucial when eagle model is compiled.
768
+ # tensor.argmax() returns int64 by default.
769
+ input_ids = draft_token_ids_list[-1].int()
770
+
771
+ if not self.constant_draft_positions:
772
+ positions = self._update_positions_dependent_metadata(
773
+ positions,
774
+ common_attn_metadata,
775
+ batch_size,
776
+ input_batch_size,
777
+ block_size,
778
+ )
779
+
780
+ # Rebuild attention metadata. When draft positions are constant
781
+ # (e.g. Gemma4 MTP), common_attn_metadata is invariant across
782
+ # loop iterations so we build once and reuse.
783
+ if not self.constant_draft_positions or token_index == 0:
784
+ _, per_layer_attn_metadata = (
785
+ self.build_per_group_and_layer_attn_metadata(
786
+ common_attn_metadata, draft_index=token_index + 1
787
+ )
788
+ )
789
+
790
+ # copy inputs to buffer for cudagraph
791
+ self.input_ids[:batch_size] = input_ids
792
+ self.hidden_states[:batch_size] = hidden_states
793
+ if self.supports_mm_inputs:
794
+ self.inputs_embeds[:batch_size] = self.model.embed_input_ids(input_ids)
795
+
796
+ input_ids = None
797
+ inputs_embeds = self.inputs_embeds[:input_batch_size]
798
+ else:
799
+ input_ids = self.input_ids[:input_batch_size]
800
+ inputs_embeds = None
801
+
802
+ # Run the model.
803
+ model_kwargs = {
804
+ "input_ids": input_ids,
805
+ "positions": self._get_positions(input_batch_size),
806
+ "inputs_embeds": inputs_embeds,
807
+ }
808
+ if self.pass_hidden_states_to_model:
809
+ model_kwargs["hidden_states"] = self.hidden_states[:input_batch_size]
810
+
811
+ with set_forward_context(
812
+ per_layer_attn_metadata,
813
+ self.vllm_config,
814
+ num_tokens=input_batch_size,
815
+ num_tokens_across_dp=batch_size_across_dp,
816
+ cudagraph_runtime_mode=cudagraph_runtime_mode,
817
+ slot_mapping=self._get_slot_mapping(input_batch_size),
818
+ ):
819
+ ret_hidden_states = self.model(**model_kwargs)
820
+ if not self.model_returns_tuple():
821
+ last_hidden_states = ret_hidden_states
822
+ hidden_states = ret_hidden_states
823
+ else:
824
+ last_hidden_states, hidden_states = ret_hidden_states
825
+
826
+ hidden_states = hidden_states[:batch_size]
827
+ draft_token_ids, draft_probs = self._sample_draft_tokens(
828
+ last_hidden_states[:batch_size], sampling_metadata
829
+ )
830
+ if draft_probs is not None:
831
+ assert draft_probs_list is not None
832
+ draft_probs_list.append(draft_probs)
833
+ draft_token_ids_list.append(draft_token_ids)
834
+
835
+ # [batch_size, num_speculative_tokens]
836
+ draft_token_ids = torch.stack(draft_token_ids_list, dim=1)
837
+ if draft_probs_list is not None:
838
+ self._last_draft_probs = torch.stack(draft_probs_list, dim=1).contiguous()
839
+ return draft_token_ids
840
+
841
+ def _update_positions_dependent_metadata(
842
+ self,
843
+ positions: torch.Tensor,
844
+ common_attn_metadata,
845
+ batch_size: int,
846
+ input_batch_size: int,
847
+ block_size: int,
848
+ ) -> torch.Tensor:
849
+ """Update positions, slot mappings, and sequence metadata for the
850
+ next draft step. Returns the updated positions tensor."""
851
+ positions_1d = positions[0] if self.uses_mrope else positions
852
+ if self.uses_mrope:
853
+ out_pos = self.mrope_positions[0, :batch_size]
854
+ elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
855
+ out_pos = self.xdrope_positions[0, :batch_size]
856
+ else:
857
+ out_pos = self.positions[:batch_size]
858
+ eagle_step_update_slot_mapping_and_metadata(
859
+ positions_1d=positions_1d,
860
+ block_table_tensor=common_attn_metadata.block_table_tensor,
861
+ seq_lens=common_attn_metadata.seq_lens,
862
+ block_size=block_size,
863
+ max_model_len=self.max_model_len,
864
+ out_clamped_positions=out_pos,
865
+ out_slot_mapping=self._slot_mapping_buffer[:input_batch_size],
866
+ input_batch_size=input_batch_size,
867
+ )
868
+ common_attn_metadata.slot_mapping = self._slot_mapping_buffer[:batch_size]
869
+ if self.uses_mrope:
870
+ self.mrope_positions[1:, :batch_size] = self.mrope_positions[0, :batch_size]
871
+ positions = self.mrope_positions[:, :batch_size]
872
+ elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
873
+ self.xdrope_positions[1:, :batch_size] = self.xdrope_positions[
874
+ 0, :batch_size
875
+ ]
876
+ positions = self.xdrope_positions[0, :batch_size]
877
+ else:
878
+ positions = self.positions[:batch_size]
879
+ common_attn_metadata.max_seq_len = min(
880
+ common_attn_metadata.max_seq_len + 1,
881
+ self.max_model_len,
882
+ )
883
+
884
+ if common_attn_metadata._seq_lens_cpu is not None:
885
+ common_attn_metadata._seq_lens_cpu += 1
886
+ if common_attn_metadata._num_computed_tokens_cpu is not None:
887
+ common_attn_metadata._num_computed_tokens_cpu += 1
888
+ if common_attn_metadata.seq_lens_cpu_upper_bound is not None:
889
+ common_attn_metadata.seq_lens_cpu_upper_bound += 1
890
+
891
+ return positions
892
+
893
+ def set_inputs_first_pass(
894
+ self,
895
+ target_token_ids: torch.Tensor,
896
+ next_token_ids: torch.Tensor,
897
+ target_positions: torch.Tensor,
898
+ target_hidden_states: torch.Tensor,
899
+ token_indices_to_sample: torch.Tensor | None,
900
+ cad: CommonAttentionMetadata,
901
+ num_rejected_tokens_gpu: torch.Tensor | None,
902
+ ) -> tuple[int, torch.Tensor, CommonAttentionMetadata]:
903
+ if not self.needs_extra_input_slots:
904
+ # Default EAGLE pathway: no reshaping of input tensors needed.
905
+ # Simply rotate the input ids and leave the positions unchanged,
906
+ # Inserting the next token ids at the last slot in each request.
907
+ if token_indices_to_sample is None:
908
+ token_indices_to_sample = cad.query_start_loc[1:] - 1
909
+
910
+ num_tokens = target_token_ids.shape[0]
911
+ # Shift the input ids by one token.
912
+ # E.g., [a1, b1, b2, c1, c2, c3] -> [b1, b2, c1, c2, c3, c3]
913
+ self.input_ids[: num_tokens - 1] = target_token_ids[1:]
914
+ # Replace the last token with the next token.
915
+ # E.g., [b1, b2, c1, c2, c3, c3] -> [a2, b2, b3, c2, c3, c4]
916
+ self.input_ids[token_indices_to_sample] = next_token_ids
917
+
918
+ # copy inputs to buffer for cudagraph
919
+ if self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim == 0:
920
+ target_positions = target_positions[0]
921
+ self._set_positions(num_tokens, target_positions)
922
+
923
+ self.hidden_states[:num_tokens] = target_hidden_states
924
+
925
+ return num_tokens, token_indices_to_sample, cad
926
+ else:
927
+ assert self.is_rejected_token_mask is not None
928
+ assert self.is_masked_token_mask is not None
929
+ # 1.
930
+ # Call a custom triton kernel to copy input_ids and positions
931
+ # into the correct slots in the preallocated buffers self.input_ids,
932
+ # self.positions.
933
+ batch_size = cad.batch_size()
934
+ # Since we might have to copy a lot of data for prefills, we select the
935
+ # block size based on the max query length and limit to max 256 slots/block.
936
+ max_num_tokens_per_request = (
937
+ cad.max_query_len + self.net_num_new_slots_per_request
938
+ )
939
+ BLOCK_SIZE_TOKENS = min(256, next_power_of_2(max_num_tokens_per_request))
940
+ num_blocks = (
941
+ max_num_tokens_per_request + BLOCK_SIZE_TOKENS - 1
942
+ ) // BLOCK_SIZE_TOKENS
943
+ total_num_input_tokens = target_token_ids.shape[0]
944
+ total_num_output_tokens = total_num_input_tokens + (
945
+ self.net_num_new_slots_per_request * batch_size
946
+ )
947
+
948
+ token_indices_to_sample = torch.empty(
949
+ batch_size * self.extra_slots_per_request,
950
+ dtype=torch.int32,
951
+ device=self.device,
952
+ )
953
+
954
+ # Destination indices to write target_hidden_states into drafting buffer.
955
+ out_hidden_state_mapping = torch.empty(
956
+ total_num_input_tokens, dtype=torch.int32, device=self.device
957
+ )
958
+
959
+ # Kernel grid: one program per request (row)
960
+ grid = (batch_size, num_blocks)
961
+ query_start_loc = cad.query_start_loc
962
+ query_end_loc = cad.query_start_loc[1:] - 1
963
+ if num_rejected_tokens_gpu is not None:
964
+ query_end_loc = query_end_loc - num_rejected_tokens_gpu
965
+
966
+ copy_and_expand_eagle_inputs_kernel[grid](
967
+ # (Padded) Inputs from the target model
968
+ target_token_ids_ptr=target_token_ids,
969
+ target_positions_ptr=target_positions,
970
+ next_token_ids_ptr=next_token_ids, # sampled tokens, one per request
971
+ # Outputs to the drafting buffers
972
+ out_input_ids_ptr=self.input_ids,
973
+ out_positions_ptr=self.positions, # Doesn't support mrope for now
974
+ out_is_rejected_token_mask_ptr=self.is_rejected_token_mask,
975
+ out_is_masked_token_mask_ptr=self.is_masked_token_mask,
976
+ out_new_token_indices_ptr=token_indices_to_sample,
977
+ out_hidden_state_mapping_ptr=out_hidden_state_mapping,
978
+ # Input metadata
979
+ query_start_loc_ptr=query_start_loc,
980
+ query_end_loc_ptr=query_end_loc,
981
+ padding_token_id=0,
982
+ parallel_drafting_token_id=self.parallel_drafting_token_id,
983
+ # Sizing info
984
+ # Note that we can deduce batch_size for free from the grid size
985
+ total_input_tokens=total_num_input_tokens,
986
+ num_padding_slots_per_request=self.extra_slots_per_request,
987
+ shift_input_ids=self.pass_hidden_states_to_model,
988
+ BLOCK_SIZE_TOKENS=BLOCK_SIZE_TOKENS,
989
+ )
990
+ if self.pass_hidden_states_to_model:
991
+ assert self.parallel_drafting_hidden_state_tensor is not None
992
+ self.hidden_states[out_hidden_state_mapping] = target_hidden_states
993
+ # Use torch.where to avoid DtoH sync from boolean indexing
994
+ mask = self.is_masked_token_mask[:total_num_output_tokens]
995
+ torch.where(
996
+ mask.unsqueeze(1),
997
+ self.parallel_drafting_hidden_state_tensor,
998
+ self.hidden_states[:total_num_output_tokens],
999
+ out=self.hidden_states[:total_num_output_tokens],
1000
+ )
1001
+
1002
+ # 2.
1003
+ # Recompute the slot mapping based on the new positions and
1004
+ # rejection mask.
1005
+ assert self.block_size > 0, "block_size has not been initialized."
1006
+ new_slot_mapping = compute_new_slot_mapping(
1007
+ cad=cad,
1008
+ new_positions=self.positions[:total_num_output_tokens],
1009
+ is_rejected_token_mask=self.is_rejected_token_mask[
1010
+ :total_num_output_tokens
1011
+ ],
1012
+ block_size=self.block_size,
1013
+ num_new_tokens=self.net_num_new_slots_per_request,
1014
+ max_model_len=self.max_model_len,
1015
+ )
1016
+
1017
+ # 3. Update the common attention metadata with the new (meta)data
1018
+ new_cad = extend_all_queries_by_N(
1019
+ cad,
1020
+ N=self.net_num_new_slots_per_request,
1021
+ arange=self.arange,
1022
+ new_slot_mapping=new_slot_mapping,
1023
+ )
1024
+
1025
+ return total_num_output_tokens, token_indices_to_sample, new_cad
1026
+
1027
+ def build_model_inputs_first_pass(
1028
+ self,
1029
+ num_tokens: int,
1030
+ num_input_tokens: int,
1031
+ mm_embed_inputs: tuple[list[torch.Tensor], torch.Tensor] | None,
1032
+ ) -> tuple[dict[str, Any], int]:
1033
+ if self.supports_mm_inputs:
1034
+ mm_embeds, is_mm_embed = mm_embed_inputs or (None, None)
1035
+
1036
+ self.inputs_embeds[:num_tokens] = self.model.embed_input_ids(
1037
+ self.input_ids[:num_tokens],
1038
+ multimodal_embeddings=mm_embeds,
1039
+ is_multimodal=is_mm_embed,
1040
+ )
1041
+
1042
+ input_ids = None
1043
+ inputs_embeds = self.inputs_embeds[:num_input_tokens]
1044
+ else:
1045
+ input_ids = self.input_ids[:num_input_tokens]
1046
+ inputs_embeds = None
1047
+
1048
+ model_kwargs = {
1049
+ "input_ids": input_ids,
1050
+ "positions": self._get_positions(num_input_tokens),
1051
+ "inputs_embeds": inputs_embeds,
1052
+ }
1053
+ if self.pass_hidden_states_to_model:
1054
+ model_kwargs["hidden_states"] = self.hidden_states[:num_input_tokens]
1055
+
1056
+ return model_kwargs, num_input_tokens
1057
+
1058
+ def build_per_group_and_layer_attn_metadata(
1059
+ self, common_attn_metadata: CommonAttentionMetadata, draft_index: int = 0
1060
+ ) -> tuple[list[object], dict[str, object]]:
1061
+ per_group_attn_metadata: list[object] = []
1062
+ per_layer_attn_metadata: dict[str, object] = {}
1063
+ for attn_group in self.draft_attn_groups:
1064
+ attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
1065
+ common_attn_metadata=common_attn_metadata, draft_index=draft_index
1066
+ )
1067
+ per_group_attn_metadata.append(attn_metadata)
1068
+ for layer_name in attn_group.layer_names:
1069
+ per_layer_attn_metadata[layer_name] = attn_metadata
1070
+ return per_group_attn_metadata, per_layer_attn_metadata
1071
+
1072
+ def model_returns_tuple(self) -> bool:
1073
+ return self.method not in ("mtp", "draft_model", "dflash")
1074
+
1075
+ def prepare_next_token_ids_cpu(
1076
+ self,
1077
+ sampled_token_ids: list[list[int]],
1078
+ requests: dict[str, CachedRequestState],
1079
+ gpu_input_batch: InputBatch,
1080
+ num_scheduled_tokens: dict[str, int],
1081
+ ) -> torch.Tensor:
1082
+ """
1083
+ This function is used to prepare the inputs for speculative decoding.
1084
+ It calculates the next token ids for each request based on the sampled
1085
+ token ids from the CPU. If a request has no sampled token ids (e.g.,
1086
+ during the initial decoding steps), it falls back to using the request
1087
+ state to get the next token id.
1088
+ """
1089
+ req_ids = gpu_input_batch.req_ids
1090
+ next_token_ids: list[int] = []
1091
+ for i, token_ids in enumerate(sampled_token_ids):
1092
+ if token_ids:
1093
+ # Common case.
1094
+ next_token_id = token_ids[-1]
1095
+ else:
1096
+ # Partial prefill (rare case).
1097
+ # Get the next token id from the request state.
1098
+ req_id = req_ids[i]
1099
+ req_state = requests[req_id]
1100
+ seq_len = req_state.num_computed_tokens + num_scheduled_tokens[req_id]
1101
+ next_token_id = req_state.get_token_id(seq_len)
1102
+ next_token_ids.append(next_token_id)
1103
+ next_token_ids = torch.tensor(
1104
+ next_token_ids, dtype=torch.int32, device=self.input_ids.device
1105
+ )
1106
+ return next_token_ids
1107
+
1108
+ def prepare_next_token_ids_padded(
1109
+ self,
1110
+ sampled_token_ids: torch.Tensor,
1111
+ requests: dict[str, CachedRequestState],
1112
+ gpu_input_batch: InputBatch,
1113
+ discard_request_mask: torch.Tensor,
1114
+ ) -> tuple[torch.Tensor, torch.Tensor]:
1115
+ """
1116
+ This function is used to prepare the inputs for speculative decoding.
1117
+ It calculates the next token ids and the number of valid sampled tokens
1118
+ for each request, considering the "discarded" requests whose next token
1119
+ is not sampled and comes from `request.get_token_id()` instead. This is denoted
1120
+ the "backup" token id. It also counts rejected tokens via `sampled_token_ids`.
1121
+ """
1122
+ # Precompute backup token IDs for discarded requests.
1123
+ num_reqs = gpu_input_batch.num_reqs
1124
+ for i in range(num_reqs):
1125
+ self.backup_next_token_ids.np[i] = requests[
1126
+ gpu_input_batch.req_ids[i]
1127
+ ].get_token_id(gpu_input_batch.num_tokens_no_spec[i] - 1)
1128
+ self.backup_next_token_ids.copy_to_gpu(num_reqs)
1129
+ backup_tokens_gpu = self.backup_next_token_ids.gpu
1130
+
1131
+ batch_size, num_tokens = sampled_token_ids.shape
1132
+ device = sampled_token_ids.device
1133
+
1134
+ assert discard_request_mask.dtype == torch.bool
1135
+ assert backup_tokens_gpu.dtype == torch.int32
1136
+
1137
+ next_token_ids = torch.empty(batch_size, dtype=torch.int32, device=device)
1138
+ valid_sampled_tokens_count = next_token_ids.new_empty(batch_size)
1139
+
1140
+ # Kernel grid: one program per request (row)
1141
+ grid = (batch_size,)
1142
+
1143
+ # Find the next power of 2 for block sizes
1144
+ BLOCK_SIZE_TOKENS = next_power_of_2(num_tokens)
1145
+ eagle_prepare_next_token_padded_kernel[grid](
1146
+ sampled_token_ids,
1147
+ discard_request_mask,
1148
+ backup_tokens_gpu,
1149
+ next_token_ids,
1150
+ valid_sampled_tokens_count,
1151
+ gpu_input_batch.vocab_size,
1152
+ num_tokens,
1153
+ batch_size,
1154
+ sampled_token_ids.stride(0),
1155
+ BLOCK_SIZE_TOKENS=BLOCK_SIZE_TOKENS,
1156
+ )
1157
+
1158
+ return next_token_ids, valid_sampled_tokens_count
1159
+
1160
+ def prepare_inputs_padded(
1161
+ self,
1162
+ common_attn_metadata: CommonAttentionMetadata,
1163
+ spec_decode_metadata: SpecDecodeMetadata,
1164
+ valid_sampled_tokens_count: torch.Tensor,
1165
+ ) -> tuple[CommonAttentionMetadata, torch.Tensor, torch.Tensor]:
1166
+ """
1167
+ This function is used to prepare the inputs for speculative decoding
1168
+ It updates the common_attn_metadata for speculative decoding,
1169
+ but does not consider the rejected tokens. Instead, all tokens
1170
+ are included as inputs to the speculator, with the rejected tokens
1171
+ used as padding and filtered out later by `token_indices_to_sample`.
1172
+ No blocking CPU operations should be introduced in this function.
1173
+ """
1174
+ num_reqs = common_attn_metadata.num_reqs
1175
+ device = valid_sampled_tokens_count.device
1176
+
1177
+ token_indices_to_sample = torch.empty(
1178
+ (num_reqs,), dtype=torch.int32, device=device
1179
+ )
1180
+ num_rejected_tokens_gpu = torch.empty(
1181
+ (num_reqs,), dtype=torch.int32, device=device
1182
+ )
1183
+
1184
+ grid = (num_reqs,)
1185
+ eagle_prepare_inputs_padded_kernel[grid](
1186
+ spec_decode_metadata.cu_num_draft_tokens,
1187
+ valid_sampled_tokens_count,
1188
+ common_attn_metadata.query_start_loc,
1189
+ token_indices_to_sample,
1190
+ num_rejected_tokens_gpu,
1191
+ num_reqs,
1192
+ )
1193
+
1194
+ query_start_loc_cpu = common_attn_metadata.query_start_loc_cpu
1195
+ new_query_len_per_req = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]
1196
+
1197
+ total_num_tokens = query_start_loc_cpu[-1].item()
1198
+
1199
+ spec_common_attn_metadata = CommonAttentionMetadata(
1200
+ query_start_loc=common_attn_metadata.query_start_loc,
1201
+ seq_lens=common_attn_metadata.seq_lens,
1202
+ query_start_loc_cpu=query_start_loc_cpu,
1203
+ _seq_lens_cpu=common_attn_metadata._seq_lens_cpu,
1204
+ _num_computed_tokens_cpu=common_attn_metadata._num_computed_tokens_cpu,
1205
+ seq_lens_cpu_upper_bound=common_attn_metadata.seq_lens_cpu_upper_bound,
1206
+ num_reqs=common_attn_metadata.num_reqs,
1207
+ num_actual_tokens=total_num_tokens,
1208
+ max_query_len=new_query_len_per_req.max().item(),
1209
+ max_seq_len=common_attn_metadata.max_seq_len,
1210
+ block_table_tensor=common_attn_metadata.block_table_tensor,
1211
+ slot_mapping=common_attn_metadata.slot_mapping[:total_num_tokens],
1212
+ causal=True,
1213
+ dcp_local_seq_lens=common_attn_metadata.dcp_local_seq_lens,
1214
+ )
1215
+
1216
+ return (
1217
+ spec_common_attn_metadata,
1218
+ token_indices_to_sample,
1219
+ num_rejected_tokens_gpu,
1220
+ )
1221
+
1222
+ def prepare_inputs(
1223
+ self,
1224
+ common_attn_metadata: CommonAttentionMetadata,
1225
+ sampled_token_ids: list[list[int]],
1226
+ num_draft_tokens: list[int],
1227
+ ) -> tuple[CommonAttentionMetadata, torch.Tensor]:
1228
+ """
1229
+ This function is used to prepare the inputs for speculative decoding.
1230
+ It updates to the common_attn_metadata to account for the rejected
1231
+ tokens (and newly sampled tokens). It also returns the token indices
1232
+ of the tokens that should be fed to the speculator.
1233
+ """
1234
+ # E.g.
1235
+ # common_attn_metadata.query_start_loc{_cpu}:
1236
+ # [0, q1, q1 + q2, q1 + q2 + q3]
1237
+ # common_attn_metadata.seq_lens{_cpu}: [s1, s2, s3]
1238
+ # num_rejected_tokens: [n1, n2, n3]
1239
+ # This function computes the intermediate values:
1240
+ # num_tokens_per_req: [q1 - n1, q2 - n2, q3 - n3]
1241
+ # And returns:
1242
+ # common_attn_metadata.query_start_loc{_cpu}:
1243
+ # [0, q1 - n1, q1 + q2 - n1 - n2, q1 + q2 + q3 - n1 - n2 - n3]
1244
+ # common_attn_metadata.seq_lens{_cpu}:
1245
+ # [s1 - n1 + 1, s2 - n2 + 1, s3 - n3 + 1]
1246
+ # token_indices: [0, 1, ..., q1 - n1 - 1,
1247
+ # q1, q1 + 1, ..., q1 + q2 - n2 - 1,
1248
+ # q1 + q2, q1 + q2 + 1, ..., q1 + q2 + q3 - n3 - 1]
1249
+
1250
+ num_rejected_tokens = [
1251
+ n + 1 - len(sampled_token_ids[i]) if n > 0 else 0
1252
+ for i, n in enumerate(num_draft_tokens)
1253
+ ]
1254
+ num_rejected_tokens = torch.tensor(num_rejected_tokens, dtype=torch.int32)
1255
+
1256
+ device = common_attn_metadata.query_start_loc.device
1257
+ query_start_loc_cpu = common_attn_metadata.query_start_loc_cpu
1258
+ # upper_bound - rejected = actual post-rejection seq_lens (no D2H sync).
1259
+ assert common_attn_metadata.seq_lens_cpu_upper_bound is not None
1260
+ new_seq_lens_cpu = (
1261
+ common_attn_metadata.seq_lens_cpu_upper_bound - num_rejected_tokens
1262
+ )
1263
+
1264
+ # [0, q1, q1 + q2, q1 + q2 + q3] -> [q1, q2, q3]
1265
+ new_query_len_per_req = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]
1266
+ # [q1, q2, q3] -> [q1 - n1, q2 - n2, q3 - n3]
1267
+ new_num_tokens_per_req = new_query_len_per_req - num_rejected_tokens
1268
+ new_num_tokens_per_req_np = new_num_tokens_per_req.numpy()
1269
+
1270
+ # [q1 - n1, q2 - n2, q3 - n3] ->
1271
+ # [0, q1 - n1, q1 + q2 - n1 - n2, q1 + q2 + q3 - n1 - n2 - n3]
1272
+ new_query_start_loc_cpu = torch.zeros(
1273
+ query_start_loc_cpu.shape,
1274
+ dtype=torch.int32,
1275
+ pin_memory=is_pin_memory_available(),
1276
+ )
1277
+ new_query_start_loc_np = new_query_start_loc_cpu.numpy()
1278
+ np.cumsum(new_num_tokens_per_req_np, out=new_query_start_loc_np[1:])
1279
+
1280
+ total_num_tokens = new_query_start_loc_np[-1]
1281
+ # Example assuming num_tokens_per_req_np = [2, 4, 3]
1282
+ # this implies that `new_query_start_locs` is:
1283
+ # [0, 2, 6, 9] ->
1284
+ # [0, 0, 2, 2, 2, 2, 6, 6, 6]
1285
+ # _r1_ ____r2____ ___r3__
1286
+ new_query_start_locs_expanded = np.repeat(
1287
+ new_query_start_loc_np[:-1], new_num_tokens_per_req_np
1288
+ )
1289
+ # [0, 1, 2, 3, 4, 5, 6, 7, 8] ->
1290
+ # [0, 1, 0, 1, 2, 3, 0, 1, 2]
1291
+ # _r1_ ____r2____ ___r3__
1292
+ token_offsets = (
1293
+ self.token_arange_np[:total_num_tokens] - new_query_start_locs_expanded
1294
+ )
1295
+
1296
+ # Expand starting positions to match token pattern
1297
+ # [0, q1, q1 + q2] ->
1298
+ # [0, 0, q1, q1, q1, q1, q1 + q2, q1 + q2, q1 + q2]
1299
+ # _r1_ _____r2_______ ___________r3____________
1300
+ old_query_start_locs_expanded = np.repeat(
1301
+ query_start_loc_cpu[:-1].numpy(), new_num_tokens_per_req_np
1302
+ )
1303
+ # Final token indices are:
1304
+ # [0, 1, // req 1
1305
+ # q1 + 0, q1 + 1, q1 + 2, q1 + 3, // req 2
1306
+ # q1 + q2 + 0, q1 + q2 + 1, q1 + q2 + 2] // req 3
1307
+ token_indices_np = token_offsets + old_query_start_locs_expanded
1308
+ token_indices = torch.from_numpy(token_indices_np).to(device, non_blocking=True)
1309
+
1310
+ spec_common_attn_metadata = CommonAttentionMetadata(
1311
+ query_start_loc=new_query_start_loc_cpu.to(device, non_blocking=True),
1312
+ seq_lens=new_seq_lens_cpu.to(device, non_blocking=True),
1313
+ query_start_loc_cpu=new_query_start_loc_cpu,
1314
+ _seq_lens_cpu=new_seq_lens_cpu,
1315
+ _num_computed_tokens_cpu=common_attn_metadata._num_computed_tokens_cpu,
1316
+ seq_lens_cpu_upper_bound=new_seq_lens_cpu,
1317
+ num_reqs=common_attn_metadata.num_reqs,
1318
+ num_actual_tokens=total_num_tokens,
1319
+ max_query_len=new_query_len_per_req.max().item(),
1320
+ max_seq_len=new_seq_lens_cpu.max().item(),
1321
+ block_table_tensor=common_attn_metadata.block_table_tensor,
1322
+ slot_mapping=common_attn_metadata.slot_mapping[token_indices],
1323
+ causal=True,
1324
+ dcp_local_seq_lens=common_attn_metadata.dcp_local_seq_lens,
1325
+ )
1326
+
1327
+ return spec_common_attn_metadata, token_indices
1328
+
1329
+ def get_model_name(self, model: nn.Module) -> str:
1330
+ if hasattr(model, "module"): # multi-GPU
1331
+ model = model.module
1332
+ return model.__class__.__name__
1333
+
1334
+ def _create_draft_vllm_config(self) -> VllmConfig:
1335
+ """Return a VllmConfig with kernel-level overrides for the proposer.
1336
+ Subclasses may override to apply additional config changes.
1337
+ """
1338
+ spec_cfg = self.speculative_config
1339
+ base = self.vllm_config
1340
+
1341
+ if spec_cfg.moe_backend is not None:
1342
+ base = replace(
1343
+ base,
1344
+ kernel_config=replace(
1345
+ base.kernel_config,
1346
+ moe_backend=spec_cfg.moe_backend,
1347
+ ),
1348
+ )
1349
+
1350
+ # Note (matt): Never inherit the attention backend from base, because there are
1351
+ # many opportunities for incompatibility, so we always independently autoselect
1352
+ # unless explicitly specified in the speculative config.
1353
+ base = replace(
1354
+ base,
1355
+ attention_config=replace(
1356
+ base.attention_config,
1357
+ backend=spec_cfg.attention_backend,
1358
+ ),
1359
+ )
1360
+
1361
+ return base
1362
+
1363
+ def _get_model(self) -> nn.Module:
1364
+ """
1365
+ Default method to call get_model(). Can be overridden by subclasses which
1366
+ need to customize model loading.
1367
+ """
1368
+ from vllm.compilation.backends import set_model_tag
1369
+
1370
+ draft_vllm_config = self._create_draft_vllm_config()
1371
+ with set_model_tag("eagle_head"):
1372
+ model = get_model(
1373
+ vllm_config=draft_vllm_config,
1374
+ model_config=self.speculative_config.draft_model_config,
1375
+ load_config=self.speculative_config.draft_load_config,
1376
+ )
1377
+ return model
1378
+
1379
+ def load_model(self, target_model: nn.Module) -> None:
1380
+ target_attn_layer_names = set(
1381
+ get_layers_from_vllm_config(
1382
+ self.vllm_config,
1383
+ AttentionLayerBase, # type: ignore[type-abstract]
1384
+ ).keys()
1385
+ )
1386
+
1387
+ self.model = self._get_model()
1388
+
1389
+ # Find draft layers (attention layers added by draft model)
1390
+ all_attn_layers = get_layers_from_vllm_config(
1391
+ self.vllm_config,
1392
+ AttentionLayerBase, # type: ignore[type-abstract]
1393
+ )
1394
+ # Filter to only layers that have KV cache specs.
1395
+ self._draft_attn_layer_names = {
1396
+ name
1397
+ for name in (set(all_attn_layers.keys()) - target_attn_layer_names)
1398
+ if all_attn_layers[name].get_kv_cache_spec(self.vllm_config) is not None
1399
+ }
1400
+
1401
+ if self.supports_mm_inputs:
1402
+ # Even if the target model is multimodal, we can also use
1403
+ # text-only draft models
1404
+ try:
1405
+ dummy_input_ids = torch.tensor([[1]], device=self.input_ids.device)
1406
+ self.model.embed_input_ids(dummy_input_ids, multimodal_embeddings=None)
1407
+ except (NotImplementedError, AttributeError, TypeError):
1408
+ logger.warning(
1409
+ "Draft model does not support multimodal inputs, "
1410
+ "falling back to text-only mode"
1411
+ )
1412
+ self.supports_mm_inputs = False
1413
+
1414
+ if supports_multimodal(target_model):
1415
+ # handle multimodality
1416
+ assert hasattr(target_model, "config")
1417
+ if self.get_model_name(target_model) in [
1418
+ "Cohere2VisionForConditionalGeneration",
1419
+ "Exaone4_5_ForConditionalGeneration",
1420
+ "GlmOcrForConditionalGeneration",
1421
+ "HunYuanVLForConditionalGeneration",
1422
+ "InternS2PreviewForConditionalGeneration",
1423
+ "MiMoV2OmniForCausalLM",
1424
+ "Qwen2_5_VLForConditionalGeneration",
1425
+ "Qwen3_5ForConditionalGeneration",
1426
+ "Qwen3_5MoeForConditionalGeneration",
1427
+ "Qwen3VLForConditionalGeneration",
1428
+ "Qwen3VLMoeForConditionalGeneration",
1429
+ "Gemma4ForConditionalGeneration",
1430
+ "Gemma4UnifiedForConditionalGeneration",
1431
+ "Step3p7ForConditionalGeneration",
1432
+ ]:
1433
+ self.model.config.image_token_index = target_model.config.image_token_id
1434
+ elif self.get_model_name(target_model) == "PixtralForConditionalGeneration":
1435
+ self.model.config.image_token_index = (
1436
+ target_model.config.vision_config.image_token_id
1437
+ )
1438
+ elif self.get_model_name(target_model) == "KimiK25ForConditionalGeneration":
1439
+ self.model.config.image_token_index = (
1440
+ target_model.config.media_placeholder_token_id
1441
+ )
1442
+ else:
1443
+ self.model.config.image_token_index = (
1444
+ target_model.config.image_token_index
1445
+ )
1446
+ target_language_model = cast(
1447
+ SupportsMultiModal, target_model
1448
+ ).get_language_model()
1449
+ else:
1450
+ target_language_model = target_model
1451
+
1452
+ self._maybe_share_embeddings(target_language_model)
1453
+ self._maybe_share_lm_head(target_language_model)
1454
+
1455
+ if (
1456
+ self.parallel_drafting
1457
+ and self.pass_hidden_states_to_model
1458
+ and self.parallel_drafting_hidden_state_tensor is not None
1459
+ ):
1460
+ flat_mask = self.model.mask_hidden.view(-1)
1461
+ if self.eagle3_use_aux_hidden_state:
1462
+ # EAGLE3: mask_hidden stores all aux hidden states,
1463
+ # project through combine_hidden_states
1464
+ self.parallel_drafting_hidden_state_tensor.copy_(
1465
+ self.model.combine_hidden_states(flat_mask)
1466
+ )
1467
+ else:
1468
+ self.parallel_drafting_hidden_state_tensor.copy_(flat_mask)
1469
+
1470
+ def _maybe_share_embeddings(self, target_language_model: nn.Module) -> None:
1471
+ """
1472
+ Some draft models may not have their own embedding layers, and some may
1473
+ have a duplicate copy of the target model's embedding layers. In these cases,
1474
+ we share the target model's embedding layers with the draft model to save
1475
+ memory.
1476
+ """
1477
+ if get_pp_group().world_size == 1:
1478
+ inner_model = getattr(target_language_model, "model", None)
1479
+ if inner_model is None:
1480
+ raise AttributeError("Target model does not have 'model' attribute")
1481
+ if hasattr(inner_model, "embed_tokens"):
1482
+ target_embed_tokens = inner_model.embed_tokens
1483
+ elif hasattr(inner_model, "embedding"):
1484
+ target_embed_tokens = inner_model.embedding
1485
+ else:
1486
+ raise AttributeError(
1487
+ "Target model does not have 'embed_tokens' or 'embedding' attribute"
1488
+ )
1489
+
1490
+ share_embeddings = False
1491
+ if hasattr(self.model, "has_own_embed_tokens"):
1492
+ # EAGLE model
1493
+ if not self.model.has_own_embed_tokens:
1494
+ share_embeddings = True
1495
+ logger.info(
1496
+ "Detected EAGLE model without its own embed_tokens in the"
1497
+ " checkpoint. Sharing target model embedding weights with the"
1498
+ " draft model."
1499
+ )
1500
+ elif (
1501
+ isinstance(target_embed_tokens.weight, torch.Tensor)
1502
+ and isinstance(self.model.model.embed_tokens.weight, torch.Tensor)
1503
+ # TODO: Offload to CPU for comparison to avoid extra GPU memory
1504
+ # usage in CI testing environments with limited GPU memory
1505
+ and torch.equal(
1506
+ target_embed_tokens.weight.cpu(),
1507
+ self.model.model.embed_tokens.weight.cpu(),
1508
+ )
1509
+ ):
1510
+ share_embeddings = True
1511
+ logger.info(
1512
+ "Detected EAGLE model with embed_tokens identical to the target"
1513
+ " model. Sharing target model embedding weights with the draft"
1514
+ " model."
1515
+ )
1516
+ else:
1517
+ logger.info(
1518
+ "Detected EAGLE model with distinct embed_tokens weights. "
1519
+ "Keeping separate embedding weights from the target model."
1520
+ )
1521
+ else:
1522
+ # MTP model
1523
+ share_embeddings = True
1524
+ logger.info(
1525
+ "Detected MTP model. "
1526
+ "Sharing target model embedding weights with the draft model."
1527
+ )
1528
+
1529
+ if share_embeddings:
1530
+ if hasattr(self.model.model, "embed_tokens"):
1531
+ del self.model.model.embed_tokens
1532
+ self.model.model.embed_tokens = target_embed_tokens
1533
+ else:
1534
+ logger.info(
1535
+ "The draft model's vocab embedding will be loaded separately"
1536
+ " from the target model."
1537
+ )
1538
+
1539
+ def _maybe_share_lm_head(self, target_language_model: nn.Module) -> None:
1540
+ """
1541
+ Some draft models may not have their own LM head, and some may have a
1542
+ duplicate copy of the target model's LM head. In these cases, we share
1543
+ the target model's LM head with the draft model to save memory.
1544
+ """
1545
+ share_lm_head = False
1546
+ if hasattr(self.model, "has_own_lm_head"):
1547
+ # EAGLE model
1548
+ if not self.model.has_own_lm_head:
1549
+ share_lm_head = True
1550
+ logger.info(
1551
+ "Detected EAGLE model without its own lm_head in the checkpoint. "
1552
+ "Sharing target model lm_head weights with the draft model."
1553
+ )
1554
+ elif (
1555
+ hasattr(target_language_model, "lm_head")
1556
+ and hasattr(target_language_model.lm_head, "weight")
1557
+ and hasattr(self.model.lm_head, "weight")
1558
+ and isinstance(target_language_model.lm_head.weight, torch.Tensor)
1559
+ and isinstance(self.model.lm_head.weight, torch.Tensor)
1560
+ # TODO: Offload to CPU for comparison to avoid extra GPU memory
1561
+ # usage in CI testing environments with limited GPU memory
1562
+ and torch.equal(
1563
+ target_language_model.lm_head.weight.cpu(),
1564
+ self.model.lm_head.weight.cpu(),
1565
+ )
1566
+ ):
1567
+ share_lm_head = True
1568
+ logger.info(
1569
+ "Detected EAGLE model with lm_head identical to the target model. "
1570
+ "Sharing target model lm_head weights with the draft model."
1571
+ )
1572
+ else:
1573
+ logger.info(
1574
+ "Detected EAGLE model with distinct lm_head weights. "
1575
+ "Keeping separate lm_head weights from the target model."
1576
+ )
1577
+ else:
1578
+ # MTP model
1579
+ share_lm_head = True
1580
+ logger.info(
1581
+ "Detected MTP model. "
1582
+ "Sharing target model lm_head weights with the draft model."
1583
+ )
1584
+
1585
+ if share_lm_head and hasattr(target_language_model, "lm_head"):
1586
+ if hasattr(self.model, "lm_head"):
1587
+ del self.model.lm_head
1588
+ self.model.lm_head = target_language_model.lm_head
1589
+
1590
+ # MTP models call compute_logits via shared_head.head (a
1591
+ # ParallelLMHead inside each MTP layer), not self.model.lm_head.
1592
+ # If the checkpoint omits a copy of the lm_head weights at the
1593
+ # MTP layer path, shared_head.head stays uninitialised and
1594
+ # produces NaN logits. Always share it explicitly.
1595
+ inner = getattr(self.model, "model", None)
1596
+ layers = getattr(inner, "layers", None) if inner else None
1597
+ if layers is not None:
1598
+ items = layers.values() if isinstance(layers, nn.ModuleDict) else layers
1599
+ for layer in items:
1600
+ sh = getattr(layer, "shared_head", None)
1601
+ if sh is not None and hasattr(sh, "head"):
1602
+ del sh.head
1603
+ sh.head = target_language_model.lm_head
1604
+ logger.info(
1605
+ "Shared target model lm_head with MTP shared_head.head."
1606
+ )
1607
+
1608
+ if hasattr(target_language_model.model, "topk_indices_buffer"):
1609
+ target_buffer = target_language_model.model.topk_indices_buffer
1610
+ if hasattr(self.model.model, "topk_indices_buffer"):
1611
+ del self.model.model.topk_indices_buffer
1612
+ self.model.model.topk_indices_buffer = target_buffer
1613
+ # Also share at per-module level so that the indexer and
1614
+ # sparse-attention backends in each MTP layer read from
1615
+ # the target model's buffer.
1616
+ for _, module in self.model.model.named_modules():
1617
+ if hasattr(module, "topk_indices_buffer"):
1618
+ module.topk_indices_buffer = target_buffer
1619
+ logger.info(
1620
+ "Detected MTP model with topk_indices_buffer. "
1621
+ "Sharing target model topk_indices_buffer with the draft model."
1622
+ )
1623
+
1624
+ # Detect index_share_for_mtp_iteration: when True, the proposer
1625
+ # toggles skip_topk so step 0 computes MTP's own indices and
1626
+ # steps 1+ reuse them.
1627
+ spec_config = self.vllm_config.speculative_config
1628
+ draft_hf_config = (
1629
+ spec_config.draft_model_config.hf_config
1630
+ if spec_config is not None
1631
+ else None
1632
+ )
1633
+ self._share_mtp_indices = getattr(
1634
+ draft_hf_config, "index_share_for_mtp_iteration", False
1635
+ )
1636
+
1637
+ if self.use_local_argmax_reduction:
1638
+ if not hasattr(self.model, "get_top_tokens"):
1639
+ raise ValueError(
1640
+ "use_local_argmax_reduction is enabled but draft model "
1641
+ f"{self.model.__class__.__name__} does not implement "
1642
+ "get_top_tokens()."
1643
+ )
1644
+ # Warn if draft model has vocab remapping, which forces fallback
1645
+ # to the full-logits path (negating the optimization).
1646
+ if (
1647
+ hasattr(self.model, "draft_id_to_target_id")
1648
+ and self.model.draft_id_to_target_id is not None
1649
+ ):
1650
+ logger.warning(
1651
+ "use_local_argmax_reduction is enabled but draft model "
1652
+ "uses draft_id_to_target_id vocab remapping. The "
1653
+ "optimization will be bypassed (falling back to full "
1654
+ "logits gather + argmax)."
1655
+ )
1656
+ else:
1657
+ logger.info(
1658
+ "Using local argmax reduction for draft token generation "
1659
+ "(communication: O(2*tp_size) vs O(vocab_size))."
1660
+ )
1661
+
1662
+ @torch.inference_mode()
1663
+ def dummy_run(
1664
+ self,
1665
+ num_tokens: int,
1666
+ use_cudagraphs: bool = True,
1667
+ is_graph_capturing: bool = False,
1668
+ slot_mappings: dict[str, torch.Tensor] | None = None,
1669
+ ) -> None:
1670
+ # FIXME: when using tree-based specdec, adjust number of forward-passes
1671
+ # according to the depth of the tree.
1672
+ only_one_forward_pass = is_graph_capturing or self.parallel_drafting
1673
+ for fwd_idx in range(
1674
+ 1 if only_one_forward_pass else self.num_speculative_tokens
1675
+ ):
1676
+ if fwd_idx <= 1:
1677
+ cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
1678
+ self._determine_batch_execution_and_padding(
1679
+ num_tokens, use_cudagraphs=use_cudagraphs
1680
+ )
1681
+ )
1682
+
1683
+ # Make sure to use EAGLE's own buffer during cudagraph capture.
1684
+ if (
1685
+ self._draft_attn_layer_names
1686
+ and slot_mappings is not None
1687
+ and next(iter(self._draft_attn_layer_names)) in slot_mappings
1688
+ ):
1689
+ slot_mapping_dict = self._get_slot_mapping(num_input_tokens)
1690
+ else:
1691
+ slot_mapping_dict = slot_mappings or {}
1692
+
1693
+ with set_forward_context(
1694
+ None,
1695
+ self.vllm_config,
1696
+ num_tokens=num_input_tokens,
1697
+ num_tokens_across_dp=num_tokens_across_dp,
1698
+ cudagraph_runtime_mode=cudagraph_runtime_mode,
1699
+ slot_mapping=slot_mapping_dict,
1700
+ ):
1701
+ if self.supports_mm_inputs:
1702
+ input_ids = None
1703
+ inputs_embeds = self.inputs_embeds[:num_input_tokens]
1704
+ else:
1705
+ input_ids = self.input_ids[:num_input_tokens]
1706
+ inputs_embeds = None
1707
+
1708
+ kwargs = dict(
1709
+ input_ids=input_ids,
1710
+ positions=self._get_positions(num_input_tokens),
1711
+ inputs_embeds=inputs_embeds,
1712
+ )
1713
+ if self.pass_hidden_states_to_model:
1714
+ kwargs["hidden_states"] = self.hidden_states[:num_input_tokens]
1715
+ self.model(**kwargs)
1716
+
1717
+ def _get_eagle3_use_aux_hidden_state_from_config(self) -> bool:
1718
+ """
1719
+ Some eagle3 heads (e.g., nvidia/gpt-oss-120b-Eagle3-v2) do not use auxiliary
1720
+ hidden states and directly uses the last layer output just like eagle1.
1721
+ They might indicate this by setting "use_aux_hidden_state" to False
1722
+ inside the "eagle_config" dict of their hf_config.
1723
+ """
1724
+ if self.method != "eagle3":
1725
+ return False
1726
+ # Assume that eagle3 heads use aux hidden states by default
1727
+ use_aux_hidden_state = True
1728
+ eagle_config = getattr(self.draft_model_config.hf_config, "eagle_config", None)
1729
+ if eagle_config is not None:
1730
+ use_aux_hidden_state = eagle_config.get("use_aux_hidden_state", True)
1731
+ return use_aux_hidden_state
1732
+
1733
+ def validate_same_kv_cache_group(self, kv_cache_config: KVCacheConfig) -> None:
1734
+ """
1735
+ Validate that all drafting layers belong to the same KVCacheGroup.
1736
+ Need this assumption to ensure all drafting layers can use the
1737
+ same AttentionMetadata.
1738
+ May extend to multiple AttentionMetadata in the future.
1739
+ """
1740
+ kv_cache_groups: dict[str, int] = {}
1741
+ for id, kv_cache_group in enumerate(kv_cache_config.kv_cache_groups):
1742
+ for layer_name in kv_cache_group.layer_names:
1743
+ kv_cache_groups[layer_name] = id
1744
+ assert (
1745
+ len(
1746
+ set(
1747
+ [
1748
+ kv_cache_groups[layer_name]
1749
+ for layer_name in self._draft_attn_layer_names
1750
+ ]
1751
+ )
1752
+ )
1753
+ == 1
1754
+ ), "All drafting layers should belong to the same kv cache group"
1755
+
1756
+ def initialize_attn_backend(
1757
+ self,
1758
+ kv_cache_config: KVCacheConfig,
1759
+ kernel_block_sizes: list[int] | None = None,
1760
+ ) -> None:
1761
+ """
1762
+ Initialize AttentionGroups for draft layers using kv_cache_config.
1763
+ Called from the model runner's initialize_metadata_builders.
1764
+ """
1765
+ all_attn_layers = get_layers_from_vllm_config(
1766
+ self.vllm_config,
1767
+ AttentionLayerBase, # type: ignore[type-abstract]
1768
+ )
1769
+
1770
+ # Find which kv_cache_group the draft layers belong to
1771
+ self.validate_same_kv_cache_group(kv_cache_config)
1772
+ kv_cache_spec = None
1773
+ for gid, group in enumerate(kv_cache_config.kv_cache_groups):
1774
+ if self._draft_attn_layer_names & set(group.layer_names):
1775
+ self.kv_cache_gid = gid
1776
+ kv_cache_spec = group.kv_cache_spec
1777
+ break
1778
+
1779
+ attention_groups: dict[tuple[str, str], AttentionGroup] = {}
1780
+ if kv_cache_spec is not None:
1781
+ for layer_name in self._draft_attn_layer_names:
1782
+ attn_backend = all_attn_layers[layer_name].get_attn_backend()
1783
+ backend_key = attn_backend.full_cls_name()
1784
+ if backend_key not in attention_groups:
1785
+ layer_kv_cache_spec = kv_cache_spec
1786
+ if isinstance(layer_kv_cache_spec, UniformTypeKVCacheSpecs):
1787
+ layer_kv_cache_spec = layer_kv_cache_spec.kv_cache_specs[
1788
+ layer_name
1789
+ ]
1790
+
1791
+ kernel_block_size = (
1792
+ kernel_block_sizes[self.kv_cache_gid]
1793
+ if kernel_block_sizes is not None
1794
+ and self.kv_cache_gid < len(kernel_block_sizes)
1795
+ else None
1796
+ )
1797
+ attn_group = AttentionGroup(
1798
+ backend=attn_backend,
1799
+ layer_names=[layer_name],
1800
+ kv_cache_spec=layer_kv_cache_spec,
1801
+ kv_cache_group_id=self.kv_cache_gid,
1802
+ )
1803
+ attn_group.create_metadata_builders(
1804
+ self.vllm_config,
1805
+ self.device,
1806
+ kernel_block_size=kernel_block_size,
1807
+ )
1808
+ attention_groups[backend_key] = attn_group
1809
+ else:
1810
+ attention_groups[backend_key].layer_names.append(layer_name)
1811
+
1812
+ self.draft_attn_groups = list(attention_groups.values())
1813
+ self.block_size = (
1814
+ self.draft_attn_groups[0].get_metadata_builder().kv_cache_spec.block_size
1815
+ )
1816
+ logger.debug("Using block size %d for drafting layers", self.block_size)
1817
+
1818
+ def _determine_batch_execution_and_padding(
1819
+ self,
1820
+ num_tokens: int,
1821
+ use_cudagraphs: bool = True,
1822
+ ) -> tuple[CUDAGraphMode, int, torch.Tensor | None]:
1823
+ cudagraph_mode, batch_desc = self.cudagraph_dispatcher.dispatch(
1824
+ num_tokens,
1825
+ valid_modes=({CUDAGraphMode.NONE} if not use_cudagraphs else None),
1826
+ )
1827
+ num_tokens_padded = batch_desc.num_tokens
1828
+
1829
+ # Extra coordination when running data-parallel since we need to
1830
+ # coordinate across ranks
1831
+ # TODO(Flechman): support DBO ubatching
1832
+ should_ubatch, num_tokens_across_dp = False, None
1833
+ if self.vllm_config.parallel_config.data_parallel_size > 1:
1834
+ should_ubatch, num_tokens_across_dp, synced_cudagraph_mode = (
1835
+ coordinate_batch_across_dp(
1836
+ num_tokens_unpadded=num_tokens,
1837
+ parallel_config=self.vllm_config.parallel_config,
1838
+ allow_microbatching=False,
1839
+ num_tokens_padded=num_tokens_padded,
1840
+ cudagraph_mode=cudagraph_mode.value,
1841
+ )
1842
+ )
1843
+ assert not should_ubatch, "DBO ubatching not implemented for EAGLE"
1844
+
1845
+ # Extract DP-synced values
1846
+ if num_tokens_across_dp is not None:
1847
+ dp_rank = self.dp_rank
1848
+ num_tokens_padded = int(num_tokens_across_dp[dp_rank].item())
1849
+ # Re-dispatch with DP padding so we have the correct
1850
+ # batch_descriptor
1851
+ cudagraph_mode, batch_desc = self.cudagraph_dispatcher.dispatch(
1852
+ num_tokens_padded,
1853
+ valid_modes={CUDAGraphMode(synced_cudagraph_mode)},
1854
+ )
1855
+ # Assert to make sure the agreed upon token count is correct
1856
+ # otherwise num_tokens_across_dp will no-longer be valid
1857
+ assert batch_desc.num_tokens == num_tokens_padded
1858
+ num_tokens_across_dp[dp_rank] = num_tokens_padded
1859
+
1860
+ return cudagraph_mode, num_tokens_padded, num_tokens_across_dp
1861
+
1862
+
1863
+ # NOTE(woosuk): Currently, the below code is not used and we always use argmax
1864
+ # to sample the draft tokens. We will use this after we find a way to manage
1865
+ # the draft prob tensor.
1866
+ # Refer to https://github.com/vllm-project/vllm/pull/16899 for the details.
1867
+ # FIXME(woosuk): The logic here is duplicated with the main sampling code.
1868
+ # We should refactor this to reuse the same sampling implementation.
1869
+ def compute_probs_and_sample_next_token(
1870
+ logits: torch.Tensor,
1871
+ sampling_metadata: SamplingMetadata,
1872
+ use_fp64_gumbel: bool = False,
1873
+ ) -> tuple[torch.Tensor, torch.Tensor]:
1874
+ if sampling_metadata.all_greedy:
1875
+ # For greedy requests, draft_probs is not used in rejection sampling.
1876
+ # Therefore, we can just return the logits.
1877
+ probs = logits
1878
+ next_token_ids = logits.argmax(dim=-1)
1879
+ return next_token_ids, probs
1880
+
1881
+ assert sampling_metadata.temperature is not None
1882
+
1883
+ # Use epsilon comparison to detect greedy sampling (temperature ~ 0.0)
1884
+ # consistent with sampler.py's _SAMPLING_EPS threshold
1885
+ temperature = sampling_metadata.temperature
1886
+ # Avoid division by zero if there are greedy requests.
1887
+ if not sampling_metadata.all_random:
1888
+ is_greedy = temperature < _SAMPLING_EPS
1889
+ temperature = torch.where(is_greedy, 1.0, temperature)
1890
+ logits.div_(temperature.view(-1, 1))
1891
+ probs = logits.softmax(dim=-1, dtype=torch.float32)
1892
+
1893
+ # NOTE(woosuk): Currently, we ignore most of the sampling parameters in
1894
+ # generating the draft tokens. We only use the temperature. While this
1895
+ # could degrade the acceptance rate, it does not affect the distribution
1896
+ # of the generated tokens after rejection sampling.
1897
+
1898
+ # TODO(woosuk): Consider seeds.
1899
+ q = empty_exponential_noise_like(probs, use_fp64_gumbel)
1900
+ q.exponential_()
1901
+ # NOTE(woosuk): We shouldn't use `probs.div_(q)` because the draft_probs
1902
+ # will be used later for rejection sampling.
1903
+ next_token_ids = sample_with_exponential_noise(probs.clone(), q)
1904
+ if not sampling_metadata.all_random:
1905
+ greedy_token_ids = probs.argmax(dim=-1)
1906
+ next_token_ids = torch.where(is_greedy, greedy_token_ids, next_token_ids)
1907
+ return next_token_ids, probs
vllm_patches/qwen3_dflash.py ADDED
@@ -0,0 +1,1133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+ import os
5
+ from collections.abc import Iterable
6
+
7
+ import torch
8
+ import torch.nn.functional as F
9
+ from torch import nn
10
+ from transformers import Qwen3Config
11
+
12
+ from vllm import _custom_ops as ops
13
+ from vllm.compilation.decorators import support_torch_compile
14
+ from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
15
+ from vllm.distributed import get_tensor_model_parallel_world_size
16
+ from vllm.logger import init_logger
17
+ from vllm.model_executor.layers.attention import Attention
18
+ from vllm.model_executor.layers.layernorm import RMSNorm
19
+ from vllm.model_executor.layers.linear import (
20
+ QKVParallelLinear,
21
+ ReplicatedLinear,
22
+ RowParallelLinear,
23
+ )
24
+ from vllm.model_executor.layers.logits_processor import LogitsProcessor
25
+ from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
26
+ from vllm.model_executor.layers.rotary_embedding import get_rope
27
+ from vllm.model_executor.layers.vocab_parallel_embedding import (
28
+ ParallelLMHead,
29
+ VocabParallelEmbedding,
30
+ )
31
+ from vllm.model_executor.model_loader.weight_utils import (
32
+ default_weight_loader,
33
+ maybe_remap_kv_scale_name,
34
+ )
35
+ from vllm.multimodal.inputs import NestedTensors
36
+ from vllm.transformers_utils.config import set_default_rope_theta
37
+ from vllm.v1.attention.backend import AttentionType
38
+
39
+ from .qwen2 import Qwen2MLP as Qwen3MLP
40
+ from .qwen3 import Qwen3ForCausalLM
41
+ from .utils import (
42
+ AutoWeightsLoader,
43
+ get_draft_quant_config,
44
+ maybe_prefix,
45
+ process_eagle_weight,
46
+ )
47
+
48
+ logger = init_logger(__name__)
49
+
50
+
51
+ class _VanillaMarkov(nn.Module):
52
+ """Inlined DSpark VanillaMarkov low-rank transition-bias head.
53
+
54
+ Mirrors the training repo's markov_head.py VanillaMarkov param names EXACTLY
55
+ (markov_w1 = nn.Embedding(vocab, rank); markov_w2 = nn.Linear(rank, vocab,
56
+ bias=False)) so checkpoint tensors markov_head.markov_w1.weight /
57
+ markov_head.markov_w2.weight line up. Inlined (NOT imported from the repo)
58
+ because vLLM workers do not have the training repo root on sys.path.
59
+
60
+ B(x_{k-1}, :) = W2(W1[x_{k-1}]); the corrected logit for draft position k is
61
+ base_k + B(x_{k-1}, :), sampled left-to-right (semi-autoregressive).
62
+ """
63
+
64
+ def __init__(self, vocab_size: int, markov_rank: int) -> None:
65
+ super().__init__()
66
+ self.markov_w1 = nn.Embedding(vocab_size, markov_rank)
67
+ self.markov_w2 = nn.Linear(markov_rank, vocab_size, bias=False)
68
+
69
+ def compute_step_bias(
70
+ self, prev_token_ids: torch.Tensor, hidden_states: torch.Tensor | None = None
71
+ ) -> torch.Tensor:
72
+ # (B,) prev token ids -> (B, V) additive logit bias
73
+ # hidden_states ignored for vanilla (memoryless)
74
+ del hidden_states
75
+ return self.markov_w2(self.markov_w1(prev_token_ids.long()))
76
+
77
+ def compute_step_vec(
78
+ self, prev_token_ids: torch.Tensor, hidden_states: torch.Tensor | None = None
79
+ ) -> torch.Tensor:
80
+ """Pre-W2 low-rank step vector v s.t. compute_step_bias == markov_w2(v).
81
+
82
+ Returns (B, rank) so a top-N caller can gather only the N needed rows of
83
+ markov_w2.weight ([V, rank]) instead of reading the full V x rank matrix.
84
+ compute_step_bias(prev) == self.markov_w2(compute_step_vec(prev)) EXACTLY
85
+ (same w1 lookup, same gate for the gated subclass); this method is the
86
+ factored inner half, never a separate approximation.
87
+ """
88
+ del hidden_states
89
+ return self.markov_w1(prev_token_ids.long())
90
+
91
+
92
+ class _GatedMarkovHead(_VanillaMarkov):
93
+ """Inlined DSpark GatedMarkovHead (official DeepSpec).
94
+
95
+ Adds a sigmoid gate conditioned on [hidden_state; prev_embedding] to
96
+ modulate the markov bias. Uses backbone hidden state for adaptive gating.
97
+ Param names match the training repo: markov_w1, markov_w2, gate_proj.
98
+ """
99
+
100
+ def __init__(self, vocab_size: int, markov_rank: int, hidden_size: int) -> None:
101
+ super().__init__(vocab_size=vocab_size, markov_rank=markov_rank)
102
+ self.gate_proj = nn.Linear(hidden_size + markov_rank, markov_rank)
103
+
104
+ def compute_step_bias(
105
+ self, prev_token_ids: torch.Tensor, hidden_states: torch.Tensor | None = None
106
+ ) -> torch.Tensor:
107
+ prev_emb = self.markov_w1(prev_token_ids.long())
108
+ if hidden_states is None:
109
+ return self.markov_w2(prev_emb)
110
+ gate = torch.sigmoid(
111
+ self.gate_proj(torch.cat([hidden_states, prev_emb], dim=-1))
112
+ ).to(dtype=prev_emb.dtype)
113
+ return self.markov_w2(gate * prev_emb)
114
+
115
+ def compute_step_vec(
116
+ self, prev_token_ids: torch.Tensor, hidden_states: torch.Tensor | None = None
117
+ ) -> torch.Tensor:
118
+ """Pre-W2 low-rank step vector for the gated head (see base docstring).
119
+
120
+ compute_step_bias(prev, h) == self.markov_w2(compute_step_vec(prev, h))
121
+ EXACTLY: the gate (or the hidden_states-is-None passthrough) is applied
122
+ here, identically to compute_step_bias, so the only thing the top-N
123
+ caller changes is which rows of markov_w2.weight get multiplied in.
124
+ """
125
+ prev_emb = self.markov_w1(prev_token_ids.long())
126
+ if hidden_states is None:
127
+ return prev_emb
128
+ gate = torch.sigmoid(
129
+ self.gate_proj(torch.cat([hidden_states, prev_emb], dim=-1))
130
+ ).to(dtype=prev_emb.dtype)
131
+ return gate * prev_emb
132
+
133
+
134
+ def _markov_topn_from_env() -> int:
135
+ """Read DSPARK_MARKOV_TOPN (opt-in). 0 (default) / <=0 / unparsable -> off.
136
+
137
+ When >0 the markov transition-bias is computed only for the top-N base-logit
138
+ candidates per position, replacing the full V x rank markov_w2 GEMV
139
+ (~127 MB HBM read for V=248320) with an N x rank gather (~1 MB for N=2048).
140
+ Default 0 keeps the byte-identical full path; this is a pure serve knob.
141
+ """
142
+ try:
143
+ return int(os.environ.get("DSPARK_MARKOV_TOPN", "0") or "0")
144
+ except (TypeError, ValueError):
145
+ return 0
146
+
147
+
148
+ def _markov_topn_sparse_bias(
149
+ step_vec: torch.Tensor, # [B, rank] pre-W2 vector (compute_step_vec output)
150
+ w2_weight: torch.Tensor, # [V, rank] markov_w2.weight
151
+ topn_idx: torch.Tensor, # [B, N] long, per-row top-N base-logit indices
152
+ ) -> torch.Tensor:
153
+ """Sparse markov bias at only the top-N indices: bias[b, n] = <W2[idx], v>.
154
+
155
+ Gathers N rows of markov_w2.weight per row and dots them with the low-rank
156
+ step vector, so bias[b, n] == compute_step_bias(prev)[b, topn_idx[b, n]] up
157
+ to fp accumulation order (same sum over `rank`). Returns [B, N] in the head
158
+ dtype (caller casts to the base-logit dtype), mirroring compute_step_bias's
159
+ own dtype before its .to(base.dtype) cast. HBM read is B*N*rank vs B*V*rank.
160
+ """
161
+ # w2_weight[topn_idx] -> [B, N, rank]; bmm with v[B, rank, 1] -> [B, N, 1].
162
+ w2_rows = w2_weight[topn_idx]
163
+ return torch.bmm(w2_rows, step_vec.unsqueeze(-1)).squeeze(-1)
164
+
165
+
166
+ def _markov_semiar_sample_block(
167
+ base_logits: torch.Tensor, # [B, num_spec, V] base draft logits (model dtype)
168
+ first_prev_token_ids: torch.Tensor, # [B] verified token before pos 0
169
+ compute_step_bias, # callable: prev_ids[B] long -> bias[B, V]
170
+ temperature: torch.Tensor, # [B, num_spec] per-request-per-pos sampling temp
171
+ all_random: bool, # sampling_metadata.all_random
172
+ sample_fn, # callable: probs[B, V] float32 -> tokens[B] long (random draw)
173
+ sampling_eps: float = 1e-5,
174
+ topn: int = 0, # DSPARK_MARKOV_TOPN; >0 & <V -> sparse top-N bias path
175
+ compute_step_vec=None, # callable: prev_ids[B] long -> step_vec[B, rank]
176
+ w2_weight: torch.Tensor | None = None, # markov_w2.weight [V, rank]
177
+ ) -> tuple[torch.Tensor, torch.Tensor]:
178
+ """Pure-torch LEFT-TO-RIGHT semi-AR markov-biased SAMPLING block.
179
+
180
+ Self-contained (only torch + injected callables) so it is unit-testable on
181
+ CPU without vLLM/CUDA. Mirrors compute_probs_and_sample_next_token's math
182
+ EXACTLY -- per-row temperature division, fp32 softmax, and the mixed-batch
183
+ greedy-row torch.where override -- but folds in the markov bias and feeds the
184
+ realized predecessor back at each step.
185
+
186
+ At position k the realized previous token biases the logit; the returned
187
+ probs[:, k] is the EXACT softmax distribution position k sampled from
188
+ (q_k = softmax((base_k + bias(prev_{k-1})) / T_row)). This identity is what
189
+ makes standard rejection sampling lossless: accepted dist == target dist iff
190
+ reported q == the draft's actual sampling dist.
191
+
192
+ Returns (tokens [B, num_spec] long, probs [B, num_spec, V] float32).
193
+ """
194
+ B, S, V = base_logits.shape
195
+ if S == 0:
196
+ return (
197
+ base_logits.new_empty((B, 0), dtype=torch.long),
198
+ base_logits.new_empty((B, 0, V), dtype=torch.float32),
199
+ )
200
+ out = base_logits.new_empty((B, S), dtype=torch.long)
201
+ probs_out = base_logits.new_empty((B, S, V), dtype=torch.float32)
202
+ prev = first_prev_token_ids.long()
203
+ # Opt-in top-N sparse markov bias. topn>=V would cover the whole vocab, so it
204
+ # falls back to the exact full path (identical result, no reason to gather V
205
+ # rows); topn<=0 is off. Only 0<topn<V takes the gather/scatter branch.
206
+ # PERF GUARD: the gather reads B*topn rows of markov_w2.weight (no cross-row
207
+ # reuse) vs the full path streaming the [V, rank] weight ONCE for the whole
208
+ # batch. Break-even is B*topn == V; above it the "sparse" path reads MORE HBM
209
+ # than full (and gather is less bandwidth-efficient than a streamed GEMM), so
210
+ # at high concurrency we must fall back to full. Only take top-N when
211
+ # B*topn < V.
212
+ use_topn = (
213
+ topn > 0
214
+ and topn < V
215
+ and B * topn < V
216
+ and compute_step_vec is not None
217
+ and w2_weight is not None
218
+ )
219
+ for k in range(S):
220
+ if use_topn:
221
+ base_k = base_logits[:, k, :]
222
+ # top-N base-logit candidates; only these receive a bias, every other
223
+ # token keeps its exact base logit (full-V softmax preserved below).
224
+ _, idx = torch.topk(base_k, topn, dim=-1) # [B, N]
225
+ step_vec = compute_step_vec(prev)
226
+ sparse = _markov_topn_sparse_bias(step_vec, w2_weight, idx)
227
+ logits_k = base_k.clone()
228
+ logits_k.scatter_add_(-1, idx, sparse.to(logits_k.dtype))
229
+ else:
230
+ logits_k = base_logits[:, k, :] + compute_step_bias(prev).to(
231
+ base_logits.dtype
232
+ )
233
+ t_k = temperature[:, k]
234
+ is_greedy = None
235
+ if not all_random:
236
+ # Mixed batch: greedy rows divide by 1.0 (then argmax-override below),
237
+ # exactly as compute_probs_and_sample_next_token does.
238
+ is_greedy = t_k < sampling_eps
239
+ t_k = torch.where(is_greedy, torch.ones_like(t_k), t_k)
240
+ probs_k = (logits_k / t_k.unsqueeze(-1)).softmax(dim=-1, dtype=torch.float32)
241
+ # Record the reported q BEFORE drawing, so an in-place sampler cannot
242
+ # corrupt it (indexed assignment copies into probs_out's own storage).
243
+ probs_out[:, k, :] = probs_k
244
+ tok_k = sample_fn(probs_k).to(torch.long)
245
+ if is_greedy is not None:
246
+ greedy_tok = probs_k.argmax(dim=-1)
247
+ tok_k = torch.where(is_greedy, greedy_tok, tok_k)
248
+ out[:, k] = tok_k
249
+ prev = out[:, k]
250
+ return out, probs_out
251
+
252
+
253
+ class DFlashQwen3Attention(nn.Module):
254
+ """Attention for DFlash speculative decoding.
255
+
256
+ Context KVs are pre-inserted into the KV cache before the forward pass.
257
+ This layer handles only query tokens via standard attention.
258
+ Adapted from Qwen3Attention."""
259
+
260
+ def __init__(
261
+ self,
262
+ hidden_size: int,
263
+ num_heads: int,
264
+ num_kv_heads: int,
265
+ rope_parameters: dict,
266
+ max_position: int = 4096 * 32,
267
+ head_dim: int | None = None,
268
+ rms_norm_eps: float = 1e-06,
269
+ attention_bias: bool = False,
270
+ cache_config: CacheConfig | None = None,
271
+ quant_config: QuantizationConfig | None = None,
272
+ prefix: str = "",
273
+ attn_type: str = AttentionType.DECODER,
274
+ ) -> None:
275
+ super().__init__()
276
+ self.layer_name = prefix
277
+ self.hidden_size = hidden_size
278
+ tp_size = get_tensor_model_parallel_world_size()
279
+ self.total_num_heads = num_heads
280
+ assert self.total_num_heads % tp_size == 0
281
+ self.num_heads = self.total_num_heads // tp_size
282
+ self.total_num_kv_heads = num_kv_heads
283
+ if self.total_num_kv_heads >= tp_size:
284
+ assert self.total_num_kv_heads % tp_size == 0
285
+ else:
286
+ assert tp_size % self.total_num_kv_heads == 0
287
+ self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
288
+ self.head_dim = head_dim or hidden_size // self.total_num_heads
289
+ self.q_size = self.num_heads * self.head_dim
290
+ self.kv_size = self.num_kv_heads * self.head_dim
291
+ self.scaling = self.head_dim**-0.5
292
+
293
+ self.qkv_proj = QKVParallelLinear(
294
+ hidden_size,
295
+ self.head_dim,
296
+ self.total_num_heads,
297
+ self.total_num_kv_heads,
298
+ bias=attention_bias,
299
+ quant_config=quant_config,
300
+ prefix=f"{prefix}.qkv_proj",
301
+ )
302
+ self.o_proj = RowParallelLinear(
303
+ self.total_num_heads * self.head_dim,
304
+ hidden_size,
305
+ bias=attention_bias, # DFlash has o_proj bias when using attention bias
306
+ quant_config=quant_config,
307
+ prefix=f"{prefix}.o_proj",
308
+ )
309
+
310
+ self.rotary_emb = get_rope(
311
+ self.head_dim,
312
+ max_position=max_position,
313
+ rope_parameters=rope_parameters,
314
+ )
315
+ self.attn = Attention(
316
+ self.num_heads,
317
+ self.head_dim,
318
+ self.scaling,
319
+ num_kv_heads=self.num_kv_heads,
320
+ cache_config=cache_config,
321
+ quant_config=quant_config,
322
+ prefix=f"{prefix}.attn",
323
+ attn_type=attn_type,
324
+ )
325
+ self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
326
+ self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
327
+
328
+ def forward(
329
+ self,
330
+ positions: torch.Tensor,
331
+ hidden_states: torch.Tensor,
332
+ ) -> torch.Tensor:
333
+ """DFlash attention assumes that the KV cache is already populated
334
+ with the context K/V from the target model's hidden states. This forward op
335
+ computes attention for the query tokens only.
336
+ See also: precompute_and_store_context_kv"""
337
+ qkv = F.linear(hidden_states, self.qkv_proj.weight, self.qkv_proj.bias)
338
+ q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
339
+
340
+ # Per-head RMSNorm
341
+ q_shape, k_shape = q.shape, k.shape
342
+ q = self.q_norm(
343
+ q.view(*q_shape[:-1], q_shape[-1] // self.head_dim, self.head_dim)
344
+ ).view(q_shape)
345
+ k = self.k_norm(
346
+ k.view(*k_shape[:-1], k_shape[-1] // self.head_dim, self.head_dim)
347
+ ).view(k_shape)
348
+
349
+ q, k = self.rotary_emb(positions, q, k)
350
+
351
+ attn_output = self.attn(q, k, v)
352
+ output, _ = self.o_proj(attn_output)
353
+ return output
354
+
355
+
356
+ class DFlashQwen3DecoderLayer(nn.Module):
357
+ def __init__(
358
+ self,
359
+ vllm_config: VllmConfig,
360
+ *,
361
+ config: Qwen3Config,
362
+ cache_config: CacheConfig | None = None,
363
+ quant_config: QuantizationConfig | None = None,
364
+ prefix: str = "",
365
+ ) -> None:
366
+ super().__init__()
367
+ self.hidden_size = config.hidden_size
368
+ set_default_rope_theta(config, default_theta=1000000)
369
+ attn_type = AttentionType.DECODER
370
+
371
+ self.self_attn = DFlashQwen3Attention(
372
+ hidden_size=self.hidden_size,
373
+ num_heads=config.num_attention_heads,
374
+ max_position=config.max_position_embeddings,
375
+ num_kv_heads=config.num_key_value_heads,
376
+ rms_norm_eps=config.rms_norm_eps,
377
+ attention_bias=getattr(config, "attention_bias", False),
378
+ head_dim=getattr(config, "head_dim", None),
379
+ cache_config=cache_config,
380
+ quant_config=quant_config,
381
+ rope_parameters=config.rope_parameters,
382
+ prefix=f"{prefix}.self_attn",
383
+ attn_type=attn_type,
384
+ )
385
+ self.mlp = Qwen3MLP(
386
+ hidden_size=self.hidden_size,
387
+ intermediate_size=config.intermediate_size,
388
+ hidden_act=config.hidden_act,
389
+ quant_config=quant_config,
390
+ prefix=f"{prefix}.mlp",
391
+ )
392
+ self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
393
+ self.post_attention_layernorm = RMSNorm(
394
+ config.hidden_size, eps=config.rms_norm_eps
395
+ )
396
+
397
+ def forward(
398
+ self,
399
+ positions: torch.Tensor,
400
+ hidden_states: torch.Tensor,
401
+ residual: torch.Tensor | None,
402
+ ) -> tuple[torch.Tensor, torch.Tensor]:
403
+ if residual is not None:
404
+ hidden_states, residual = self.input_layernorm(hidden_states, residual)
405
+ else:
406
+ residual = hidden_states
407
+ hidden_states = self.input_layernorm(hidden_states)
408
+
409
+ hidden_states = self.self_attn(
410
+ positions=positions,
411
+ hidden_states=hidden_states,
412
+ )
413
+
414
+ hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
415
+ hidden_states = self.mlp(hidden_states)
416
+ return hidden_states, residual
417
+
418
+
419
+ @support_torch_compile
420
+ class DFlashQwen3Model(nn.Module):
421
+ def __init__(
422
+ self,
423
+ *,
424
+ vllm_config: VllmConfig,
425
+ start_layer_id: int = 0,
426
+ prefix: str = "",
427
+ ) -> None:
428
+ super().__init__()
429
+ self.config = vllm_config.speculative_config.draft_model_config.hf_config
430
+ self.vocab_size = self.config.vocab_size
431
+ self.quant_config = get_draft_quant_config(vllm_config)
432
+
433
+ drafter_config = getattr(self.config, "eagle_config", {})
434
+ drafter_config.update(getattr(self.config, "dflash_config", {}))
435
+
436
+ if drafter_config is not None and "use_aux_hidden_state" in drafter_config:
437
+ self.use_aux_hidden_state = drafter_config["use_aux_hidden_state"]
438
+ else:
439
+ self.use_aux_hidden_state = True
440
+
441
+ current_vllm_config = get_current_vllm_config()
442
+
443
+ self.embed_tokens = VocabParallelEmbedding(
444
+ self.config.vocab_size,
445
+ self.config.hidden_size,
446
+ prefix=maybe_prefix(prefix, "embed_tokens"),
447
+ )
448
+
449
+ self.layers = nn.ModuleList(
450
+ [
451
+ DFlashQwen3DecoderLayer(
452
+ current_vllm_config,
453
+ config=self.config,
454
+ cache_config=current_vllm_config.cache_config,
455
+ quant_config=self.quant_config,
456
+ prefix=maybe_prefix(prefix, f"layers.{layer_idx + start_layer_id}"),
457
+ )
458
+ for layer_idx in range(self.config.num_hidden_layers)
459
+ ]
460
+ )
461
+ if self.use_aux_hidden_state:
462
+ num_features_to_use = self.config.num_hidden_layers
463
+ if "target_layer_ids" in drafter_config:
464
+ num_features_to_use = len(drafter_config["target_layer_ids"])
465
+ elif "layer_ids" in drafter_config:
466
+ num_features_to_use = len(drafter_config["layer_ids"])
467
+ if hasattr(self.config, "target_hidden_size"):
468
+ fc_input_size = self.config.target_hidden_size * num_features_to_use
469
+ else:
470
+ fc_input_size = self.config.hidden_size * num_features_to_use
471
+ self.fc = ReplicatedLinear(
472
+ input_size=fc_input_size,
473
+ output_size=self.config.hidden_size,
474
+ bias=False,
475
+ params_dtype=vllm_config.model_config.dtype,
476
+ quant_config=self.quant_config,
477
+ prefix=maybe_prefix(prefix, "fc"),
478
+ return_bias=False,
479
+ )
480
+ self.hidden_norm = RMSNorm(
481
+ self.config.hidden_size,
482
+ eps=self.config.rms_norm_eps,
483
+ )
484
+ self.norm = RMSNorm(
485
+ self.config.hidden_size,
486
+ eps=self.config.rms_norm_eps,
487
+ )
488
+ # DSpark VanillaMarkov semi-AR draft head. Built only when the config
489
+ # declares markov_rank>0; otherwise None, so non-markov checkpoints
490
+ # (markov_rank absent -> 0) build nothing and stay byte-identical to
491
+ # before. Submodule path is model.markov_head.*, which the existing
492
+ # load_weights "model."+name rename loads automatically (no
493
+ # load_weights change needed).
494
+ markov_rank = int(getattr(self.config, "markov_rank", 0) or 0)
495
+ markov_head_type = str(getattr(self.config, "markov_head_type", "vanilla") or "vanilla").lower()
496
+ if markov_rank > 0:
497
+ if markov_head_type == "gated":
498
+ self.markov_head = _GatedMarkovHead(
499
+ self.config.vocab_size, markov_rank, self.config.hidden_size
500
+ )
501
+ else:
502
+ self.markov_head = _VanillaMarkov(self.config.vocab_size, markov_rank)
503
+ else:
504
+ self.markov_head = None
505
+ # DSpark confidence head (DeepSpec AcceptRatePredictor, common.py:43-49):
506
+ # a single Linear predicting the per-position accept-rate LOGIT
507
+ # ("is one more draft token worth it?"). Built ONLY when the config
508
+ # declares confidence_head=True; otherwise None, so checkpoints without
509
+ # it (key absent -> False) build nothing and stay byte-identical to
510
+ # before. Submodule path model.confidence_head.* is loaded by the same
511
+ # "model."+name rename in DFlashQwen3ForCausalLM.load_weights that
512
+ # already handles markov_head (no load_weights change needed) -- this is
513
+ # exactly what resolves the `KeyError: confidence_head.bias` on serve.
514
+ # in_dim = hidden_size (+ markov_rank when confidence_head_with_markov),
515
+ # matching the trained checkpoint (5120 + 256 = 5376). NO explicit dtype:
516
+ # the head inherits the draft-model init dtype (bf16) just like markov_head
517
+ # and the official DeepSpec AcceptRatePredictor (common.py:43-49). This is
518
+ # deliberate -- the head was TRAINED with a bf16 GEMM (train_head.py:594-616:
519
+ # feats=draft_hidden(bf16), conf_head(feats), output .float()), so a bf16
520
+ # serve GEMM reproduces the exact calibration the threshold was tuned for.
521
+ if bool(getattr(self.config, "confidence_head", False)):
522
+ conf_in = self.config.hidden_size + (
523
+ markov_rank
524
+ if bool(getattr(self.config, "confidence_head_with_markov", False))
525
+ else 0
526
+ )
527
+ self.confidence_head = nn.Linear(conf_in, 1, bias=True)
528
+ else:
529
+ self.confidence_head = None
530
+
531
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
532
+ return self.embed_tokens(input_ids)
533
+
534
+ def _build_fused_kv_buffers(self) -> None:
535
+ """Build fused weight buffers for precompute_and_store_context_kv.
536
+
537
+ Must be called after weights are loaded. Stacks the KV-projection
538
+ weights, K-norm weights, and RoPE parameters from every attention
539
+ layer so that precompute_and_store_context_kv can run one fused
540
+ GEMM for all layers at once. Also aliases the weight of the hidden_norm.
541
+ """
542
+ layers_attn = [layer.self_attn for layer in self.layers]
543
+ attn0 = layers_attn[0]
544
+ has_bias = attn0.qkv_proj.bias is not None
545
+
546
+ self._hidden_norm_weight = self.hidden_norm.weight.data
547
+
548
+ # KV projection weights: [num_layers * 2 * kv_size, hidden_size]
549
+ kv_weights = [a.qkv_proj.weight[a.q_size :] for a in layers_attn]
550
+ self._fused_kv_weight = torch.cat(kv_weights, dim=0)
551
+ if has_bias:
552
+ kv_biases = [a.qkv_proj.bias[a.q_size :] for a in layers_attn]
553
+ self._fused_kv_bias: torch.Tensor | None = torch.cat(kv_biases, dim=0)
554
+ else:
555
+ self._fused_kv_bias = None
556
+
557
+ # K-norm weights: list of [head_dim] tensors, one per layer.
558
+ self._k_norm_weights = [a.k_norm.weight.data for a in layers_attn]
559
+
560
+ # RoPE parameters
561
+ self._rope_head_size = attn0.rotary_emb.head_size
562
+ self._rope_cos_sin_cache = attn0.rotary_emb.cos_sin_cache
563
+ self._rope_is_neox = attn0.rotary_emb.is_neox_style
564
+ # Validation that RoPE params are the same across all layers
565
+ for attn in layers_attn[1:]:
566
+ assert (
567
+ attn.rotary_emb.head_size == self._rope_head_size
568
+ and attn.rotary_emb.is_neox_style == self._rope_is_neox
569
+ ), "All layers must have the same RoPE parameters for DFlash precomputation"
570
+
571
+ # Layer metadata
572
+ self._num_attn_layers = len(layers_attn)
573
+ self._kv_size = attn0.kv_size
574
+ self._head_dim = attn0.head_dim
575
+ self._num_kv_heads = attn0.num_kv_heads
576
+ self._rms_norm_eps = attn0.q_norm.variance_epsilon
577
+ # Validation that all layers have the same attention config
578
+ for attn in layers_attn[1:]:
579
+ assert (
580
+ attn.kv_size == self._kv_size
581
+ and attn.head_dim == self._head_dim
582
+ and attn.num_kv_heads == self._num_kv_heads
583
+ and attn.q_norm.variance_epsilon == self._rms_norm_eps
584
+ ), "All layers must have the same attn config for DFlash precomputation"
585
+
586
+ # References to inner Attention layers for direct cache writes
587
+ self._attn_layers = [layer.self_attn.attn for layer in self.layers]
588
+
589
+ def precompute_and_store_context_kv(
590
+ self,
591
+ context_states: torch.Tensor,
592
+ context_positions: torch.Tensor,
593
+ context_slot_mapping: torch.Tensor | None = None,
594
+ ) -> None:
595
+ """Precompute K/V for context states write them into each layer's KV cache.
596
+
597
+ Input context states are projected to K/V, normed, and have RoPE applied.
598
+ Since the context shape is different than the query shape, we can't rely on the
599
+ regular forward pass to apply torch.compile and CUDA graphs to this section.
600
+ As such, this function is optimized to minimize the number of torch ops present:
601
+ we use fused vLLM kernels for RMSNorm and RoPE, fuse the GEMM into one
602
+ large projection, and avoid cloning buffers (with .contiguous()) where possible.
603
+
604
+ When context_slot_mapping is None (e.g. during dummy_run) only
605
+ the computation runs, and no K/V is written to cache.
606
+ """
607
+ if not hasattr(self, "_num_attn_layers"):
608
+ logger.warning_once(
609
+ "DFlash buffer initialization was skipped. If dummy weights are not "
610
+ "in use, this may indicate an error in weight loading."
611
+ )
612
+ self._build_fused_kv_buffers()
613
+
614
+ num_ctx = context_states.shape[0]
615
+ L = self._num_attn_layers
616
+ kv = self._kv_size
617
+ hd = self._head_dim
618
+ nkv = self._num_kv_heads
619
+
620
+ # --- Fused KV projection (one GEMM for all layers) ---
621
+ normed_context_states = torch.empty_like(context_states)
622
+ ops.rms_norm(
623
+ normed_context_states,
624
+ context_states,
625
+ self._hidden_norm_weight,
626
+ self._rms_norm_eps,
627
+ )
628
+ all_kv_flat = F.linear(
629
+ normed_context_states, self._fused_kv_weight, self._fused_kv_bias
630
+ )
631
+ # Single contiguous copy that separates K/V and transposes to
632
+ # layer-major layout. Result: [2, L, num_ctx, nkv, hd] contiguous.
633
+ # Indexing dim-0 gives contiguous [L, num_ctx, nkv, hd] for K and V.
634
+ all_kv = (
635
+ all_kv_flat.view(num_ctx, L, 2, nkv, hd).permute(2, 1, 0, 3, 4).contiguous()
636
+ )
637
+ all_k = all_kv[0] # [L, num_ctx, nkv, hd], contiguous
638
+ all_v = all_kv[1] # [L, num_ctx, nkv, hd], contiguous
639
+
640
+ # --- Per-layer RMSNorm K (3D: [num_ctx, nkv, hd] per layer) ---
641
+ all_k_normed = torch.empty_like(all_k)
642
+ for i in range(L):
643
+ ops.rms_norm(
644
+ all_k_normed[i],
645
+ all_k[i],
646
+ self._k_norm_weights[i],
647
+ self._rms_norm_eps,
648
+ )
649
+
650
+ # --- Fused RoPE across all layers ---
651
+ # View as [L * num_ctx, kv] so RoPE sees one big batch (no copy).
652
+ # In-place RoPE: pass K as the "query" arg with key=None.
653
+ all_k_flat = all_k_normed.view(L * num_ctx, kv)
654
+ positions_repeated = context_positions.repeat(L)
655
+ cos_sin_cache = self._rope_cos_sin_cache
656
+ if cos_sin_cache.dtype != all_k_flat.dtype:
657
+ cos_sin_cache = cos_sin_cache.to(dtype=all_k_flat.dtype)
658
+ ops.rotary_embedding(
659
+ positions_repeated,
660
+ all_k_flat,
661
+ None,
662
+ self._rope_head_size,
663
+ cos_sin_cache,
664
+ self._rope_is_neox,
665
+ )
666
+
667
+ if context_slot_mapping is None:
668
+ return
669
+
670
+ # --- Per-layer cache insert ---
671
+ all_k_final = all_k_flat.view(L, num_ctx, nkv, hd)
672
+ for i in range(L):
673
+ attn = self._attn_layers[i]
674
+ kv_cache = attn.kv_cache
675
+ attn.impl.do_kv_cache_update(
676
+ attn,
677
+ all_k_final[i],
678
+ all_v[i],
679
+ kv_cache,
680
+ context_slot_mapping,
681
+ )
682
+
683
+ def forward(
684
+ self,
685
+ input_ids: torch.Tensor,
686
+ positions: torch.Tensor,
687
+ input_embeds: torch.Tensor | None = None,
688
+ ) -> torch.Tensor:
689
+ if input_embeds is None:
690
+ input_embeds = self.embed_input_ids(input_ids)
691
+
692
+ hidden_states = input_embeds
693
+
694
+ residual = None
695
+ for layer in self.layers:
696
+ hidden_states, residual = layer(
697
+ positions=positions,
698
+ hidden_states=hidden_states,
699
+ residual=residual,
700
+ )
701
+ hidden_states, _ = self.norm(hidden_states, residual)
702
+ return hidden_states
703
+
704
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
705
+ stacked_params_mapping = [
706
+ (".qkv_proj", ".q_proj", "q"),
707
+ (".qkv_proj", ".k_proj", "k"),
708
+ (".qkv_proj", ".v_proj", "v"),
709
+ (".gate_up_proj", ".gate_proj", 0),
710
+ (".gate_up_proj", ".up_proj", 1),
711
+ ]
712
+ params_dict = dict(self.named_parameters())
713
+ loaded_params: set[str] = set()
714
+ for name, loaded_weight in weights:
715
+ if "midlayer." in name:
716
+ name = name.replace("midlayer.", "layers.0.")
717
+ if "scale" in name:
718
+ name = maybe_remap_kv_scale_name(name, params_dict)
719
+ if name is None:
720
+ continue
721
+ for param_name, weight_name, shard_id in stacked_params_mapping:
722
+ if weight_name not in name:
723
+ continue
724
+ name = name.replace(weight_name, param_name)
725
+ param = params_dict[name]
726
+ weight_loader = param.weight_loader
727
+ weight_loader(param, loaded_weight, shard_id)
728
+ break
729
+ else:
730
+ param = params_dict[name]
731
+ weight_loader = getattr(param, "weight_loader", default_weight_loader)
732
+ weight_loader(param, loaded_weight)
733
+ loaded_params.add(name)
734
+ return loaded_params
735
+
736
+
737
+ class DFlashQwen3ForCausalLM(Qwen3ForCausalLM):
738
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
739
+ nn.Module.__init__(self)
740
+ self.config = vllm_config.speculative_config.draft_model_config.hf_config
741
+ if getattr(self.config, "draft_vocab_size", None) is None:
742
+ self.config.draft_vocab_size = getattr(self.config, "vocab_size", None)
743
+ target_layer_num = vllm_config.model_config.get_num_layers(
744
+ vllm_config.parallel_config
745
+ )
746
+ self.model = DFlashQwen3Model(
747
+ vllm_config=vllm_config,
748
+ prefix=maybe_prefix(prefix, "model"),
749
+ start_layer_id=target_layer_num,
750
+ )
751
+
752
+ logit_scale = getattr(self.config, "logit_scale", 1.0)
753
+ self.lm_head = ParallelLMHead(
754
+ self.config.draft_vocab_size,
755
+ self.config.hidden_size,
756
+ prefix=maybe_prefix(prefix, "lm_head"),
757
+ )
758
+ self.logits_processor = LogitsProcessor(
759
+ self.config.draft_vocab_size, scale=logit_scale
760
+ )
761
+ target_vocab_size = vllm_config.model_config.get_vocab_size()
762
+ if self.config.draft_vocab_size != target_vocab_size:
763
+ self.draft_id_to_target_id = nn.Parameter(
764
+ torch.zeros(self.config.draft_vocab_size, dtype=torch.long),
765
+ requires_grad=False,
766
+ )
767
+ else:
768
+ self.draft_id_to_target_id = None
769
+
770
+ def embed_input_ids(
771
+ self,
772
+ input_ids: torch.Tensor,
773
+ multimodal_embeddings: NestedTensors | None = None,
774
+ is_multimodal: torch.Tensor | None = None,
775
+ ) -> torch.Tensor:
776
+ return self.model.embed_input_ids(input_ids)
777
+
778
+ def forward(
779
+ self,
780
+ input_ids: torch.Tensor,
781
+ positions: torch.Tensor,
782
+ inputs_embeds: torch.Tensor | None = None,
783
+ ) -> torch.Tensor:
784
+ return self.model(input_ids, positions, inputs_embeds)
785
+
786
+ def compute_logits(
787
+ self,
788
+ hidden_states: torch.Tensor,
789
+ ) -> torch.Tensor | None:
790
+ logits = self.logits_processor(self.lm_head, hidden_states)
791
+ if self.draft_id_to_target_id is None:
792
+ return logits
793
+
794
+ base = torch.arange(self.config.draft_vocab_size, device=logits.device)
795
+ targets = base + self.draft_id_to_target_id
796
+ logits_new = logits.new_full(
797
+ (logits.shape[0], self.config.vocab_size),
798
+ float("-inf"),
799
+ )
800
+ logits_new[:, targets] = logits
801
+ return logits_new
802
+
803
+ @torch.no_grad()
804
+ def sample_draft_block_semiar(
805
+ self,
806
+ sample_hidden_states: torch.Tensor,
807
+ first_prev_token_ids: torch.Tensor,
808
+ num_spec: int,
809
+ ) -> torch.Tensor:
810
+ """LEFT-TO-RIGHT (semi-autoregressive) markov-biased greedy draft block.
811
+
812
+ Produces the parallel-drafted block one position at a time: position k's
813
+ argmax is taken over base_k + markov_bias(prev_token), where prev_token
814
+ is the token argmax'd at position k-1 (first_prev = the verified bonus
815
+ token before draft pos 0). LOSSLESS under greedy because the target
816
+ verify re-checks every drafted token against the target argmax,
817
+ independent of how the draft was produced.
818
+
819
+ Returns [B, num_spec] long tensor, matching the .view(-1, num_spec)
820
+ contract of the parallel-drafting early-exit path.
821
+ """
822
+ mk = self.model.markov_head
823
+ assert mk is not None, (
824
+ "sample_draft_block_semiar called without a markov_head"
825
+ )
826
+ # The markov head is trained in the (draft == target) vocab space. The
827
+ # d2t remap path is intentionally NOT supported here: with d2t != None
828
+ # the base logits live in draft-vocab space while the markov bias lives
829
+ # in target-vocab space (shape mismatch), and the L->R feedback loop
830
+ # would index markov_w1 with draft-vocab ids. Fail loudly rather than
831
+ # silently dropping the bias. (Follow-up could remap out at the end via
832
+ # out = self.draft_id_to_target_id[out] AND project the bias/feedback
833
+ # into draft space, but AEON has draft_vocab == vocab so d2t is None.)
834
+ assert self.draft_id_to_target_id is None, (
835
+ "VanillaMarkov semi-AR draft requires draft_vocab_size == target "
836
+ "vocab_size (draft_id_to_target_id is None); the d2t remap path is "
837
+ "not implemented."
838
+ )
839
+ # Base draft logits ONCE, draft-vocab space, mirroring compute_logits
840
+ # but WITHOUT the d2t scatter (guarded None above). Stay on-device:
841
+ # no .item()/D2H inside the loop.
842
+ base = self.logits_processor(self.lm_head, sample_hidden_states)
843
+ B = sample_hidden_states.shape[0] // num_spec
844
+ V = base.shape[-1]
845
+ assert V == mk.markov_w2.weight.shape[0], (
846
+ f"draft logits vocab {V} != markov bias vocab "
847
+ f"{mk.markov_w2.weight.shape[0]}"
848
+ )
849
+ assert first_prev_token_ids.shape[0] == B, (
850
+ f"first_prev_token_ids batch {first_prev_token_ids.shape[0]} != "
851
+ f"derived batch {B} (rows={sample_hidden_states.shape[0]}, "
852
+ f"num_spec={num_spec})"
853
+ )
854
+ base = base.view(B, num_spec, V)
855
+ # Fused semi-AR: use mk.compute_step_bias but with pre-allocated buffers
856
+ # and in-place add+argmax to reduce kernel launches per iteration.
857
+ # Original: 8 × (embed+linear+add+argmax) = 32 launches
858
+ # Optimized: 8 × (compute_step_bias+add+argmax) = 24, no intermediate allocs
859
+ # GatedMarkovHead: passes hidden_states per position for gate conditioning.
860
+ out = base.new_empty((B, num_spec), dtype=torch.long)
861
+ prev = first_prev_token_ids.long()
862
+ # Pre-allocate bias buffer to avoid per-step allocation
863
+ bias_buf = base.new_empty((B, V))
864
+ # Reshape sample_hidden_states to [B, num_spec, H] for gated head access
865
+ hs_per_pos = sample_hidden_states.view(B, num_spec, -1)
866
+ # Opt-in top-N sparse markov bias (DSPARK_MARKOV_TOPN). Every non-top-N
867
+ # token keeps its base logit (its bias is defined to be 0), so the top-N
868
+ # argmax equals the TRUE full-base+bias argmax ONLY WHEN the full-bias
869
+ # winner lies inside base-top-N; if a token outside base-top-N would have
870
+ # won under the full bias, top-N misses it -> an accept-affecting
871
+ # TRUNCATION (an approximation of the full-bias argmax), NOT an identity.
872
+ # This is safe: losslessness is guaranteed by verify (the target re-checks
873
+ # every drafted token); top-N can at most shift which token is proposed
874
+ # (accept rate), never correctness. topn>=V falls back to the full path.
875
+ # PERF GUARD (same as the block path): only worthwhile when B*topn < V,
876
+ # else the per-row gather reads more HBM than the streamed full weight.
877
+ topn = _markov_topn_from_env()
878
+ use_topn = 0 < topn < V and B * topn < V
879
+ w2w = mk.markov_w2.weight if use_topn else None
880
+ for k in range(num_spec):
881
+ hs_k = hs_per_pos[:, k, :] if hasattr(mk, 'gate_proj') else None
882
+ if use_topn:
883
+ base_k = base[:, k, :]
884
+ _, idx = torch.topk(base_k, topn, dim=-1) # [B, N]
885
+ step_vec = mk.compute_step_vec(prev, hidden_states=hs_k)
886
+ sparse = _markov_topn_sparse_bias(step_vec, w2w, idx).to(base.dtype)
887
+ bias_buf.copy_(base_k)
888
+ bias_buf.scatter_add_(-1, idx, sparse)
889
+ else:
890
+ bias = mk.compute_step_bias(prev, hidden_states=hs_k).to(base.dtype)
891
+ # Fused add + argmax via in-place add then argmax
892
+ torch.add(base[:, k, :], bias, out=bias_buf)
893
+ tok = bias_buf.argmax(dim=-1)
894
+ out[:, k] = tok
895
+ prev = tok
896
+ return out # [B, num_spec]
897
+
898
+ @torch.no_grad()
899
+ def sample_draft_block_semiar_sample(
900
+ self,
901
+ sample_hidden_states: torch.Tensor,
902
+ first_prev_token_ids: torch.Tensor,
903
+ num_spec: int,
904
+ temperature: torch.Tensor,
905
+ all_random: bool,
906
+ use_fp64_gumbel: bool = False,
907
+ ) -> tuple[torch.Tensor, torch.Tensor]:
908
+ """LEFT-TO-RIGHT (semi-AR) markov-biased SAMPLING draft block (temp>0).
909
+
910
+ LOSSLESS counterpart of sample_draft_block_semiar (the greedy variant):
911
+ each position k is SAMPLED -- not argmax'd -- from
912
+ q_k = softmax((base_k + markov_bias(prev_{k-1})) / T_row)
913
+ and that EXACT q_k is returned as the corrected proposal probs. Standard
914
+ rejection sampling is lossless iff the reported q equals the distribution
915
+ the draft actually sampled from; here they are the same tensor, computed
916
+ from the same logits, same per-row temperature, and the realized
917
+ left-to-right predecessor -- so the contract holds by construction.
918
+
919
+ Math mirrors compute_probs_and_sample_next_token EXACTLY (per-row temp
920
+ division, fp32 softmax, exponential-noise / Gumbel-max draw, and the
921
+ greedy-row argmax override for mixed batches), applied position-by-
922
+ position with the markov bias folded in and prev fed back.
923
+
924
+ temperature is sampling_metadata.temperature, length B*num_spec, laid out
925
+ request-major / position-minor (same layout as sample_hidden_states and
926
+ the parallel path's draft_probs.view(-1, num_spec, V)).
927
+
928
+ Returns (sampled_tokens [B, num_spec] long, corrected_probs
929
+ [B, num_spec, V] float32). corrected_probs matches the parallel path's
930
+ _last_draft_probs layout (request-major, position, vocab; float32;
931
+ contiguous).
932
+ """
933
+ from vllm.v1.sample.ops.topk_topp_sampler import (
934
+ empty_exponential_noise_like,
935
+ sample_with_exponential_noise,
936
+ )
937
+ from vllm.v1.sample.sampler import _SAMPLING_EPS
938
+
939
+ mk = self.model.markov_head
940
+ assert mk is not None, (
941
+ "sample_draft_block_semiar_sample called without a markov_head"
942
+ )
943
+ # Same d2t guard as the greedy path: the markov head lives in the
944
+ # draft==target vocab space; the remap path is not implemented.
945
+ assert self.draft_id_to_target_id is None, (
946
+ "VanillaMarkov semi-AR SAMPLING draft requires draft_vocab_size == "
947
+ "target vocab_size (draft_id_to_target_id is None); the d2t remap "
948
+ "path is not implemented."
949
+ )
950
+ base = self.logits_processor(self.lm_head, sample_hidden_states)
951
+ B = sample_hidden_states.shape[0] // num_spec
952
+ V = base.shape[-1]
953
+ assert V == mk.markov_w2.weight.shape[0], (
954
+ f"draft logits vocab {V} != markov bias vocab "
955
+ f"{mk.markov_w2.weight.shape[0]}"
956
+ )
957
+ assert first_prev_token_ids.shape[0] == B, (
958
+ f"first_prev_token_ids batch {first_prev_token_ids.shape[0]} != "
959
+ f"derived batch {B} (rows={sample_hidden_states.shape[0]}, "
960
+ f"num_spec={num_spec})"
961
+ )
962
+ assert temperature is not None, (
963
+ "sample_draft_block_semiar_sample requires a temperature tensor "
964
+ "(only reached when sampling_metadata is not all_greedy)."
965
+ )
966
+ base = base.view(B, num_spec, V)
967
+ # temperature may be per-request [B] (vLLM sampling_metadata.temperature),
968
+ # per-position [B*num_spec], or a scalar -> broadcast to [B, num_spec].
969
+ _t = temperature.reshape(-1)
970
+ if _t.numel() == B * num_spec:
971
+ temp = _t.view(B, num_spec)
972
+ elif _t.numel() == B:
973
+ temp = _t.view(B, 1).expand(B, num_spec)
974
+ else:
975
+ temp = _t.reshape(1, 1).expand(B, num_spec)
976
+
977
+ def _gumbel_sample(probs: torch.Tensor) -> torch.Tensor:
978
+ # Mirror compute_probs_and_sample_next_token's draw exactly:
979
+ # exponential (Gumbel-max) noise, sampled from probs.clone() so the
980
+ # returned probs (used as rejection-sampling q) stay intact.
981
+ noise = empty_exponential_noise_like(probs, use_fp64_gumbel)
982
+ noise.exponential_()
983
+ return sample_with_exponential_noise(probs.clone(), noise)
984
+
985
+ # Opt-in top-N sparse markov bias (DSPARK_MARKOV_TOPN). The sampling path
986
+ # keeps the full-V softmax; only the bias term is sparsified (top-N base
987
+ # candidates get the bias, all other tokens keep their exact base logit).
988
+ # The resulting q_k is a TRUNCATION of the full-bias distribution (tokens
989
+ # whose bias would have mattered but sit outside base-top-N are omitted) --
990
+ # an approximation that shifts accept, NOT correctness. Losslessness still
991
+ # holds because rejection sampling only needs the reported q_k to equal the
992
+ # distribution actually sampled from, and here they are the SAME tensor.
993
+ # NOTE: like the full path here, the gated head is fed prev only
994
+ # (hidden=None) -> compute_step_vec matches compute_step_bias(prev)
995
+ # bit-for-bit at the inner (pre-W2) vector.
996
+ # PERF: the block applies the same B*topn<V guard internally.
997
+ topn = _markov_topn_from_env()
998
+ return _markov_semiar_sample_block(
999
+ base,
1000
+ first_prev_token_ids,
1001
+ mk.compute_step_bias,
1002
+ temp,
1003
+ all_random,
1004
+ _gumbel_sample,
1005
+ sampling_eps=_SAMPLING_EPS,
1006
+ topn=topn,
1007
+ compute_step_vec=mk.compute_step_vec,
1008
+ w2_weight=mk.markov_w2.weight,
1009
+ )
1010
+
1011
+ @torch.no_grad()
1012
+ def predict_confidence_step(
1013
+ self,
1014
+ sample_hidden_states: torch.Tensor,
1015
+ draft_token_ids: torch.Tensor,
1016
+ first_prev_token_ids: torch.Tensor,
1017
+ num_spec: int,
1018
+ ) -> torch.Tensor:
1019
+ """Per-position confidence (accept-rate) LOGITS for the realized draft block.
1020
+
1021
+ Mirrors the official DeepSpec DSparkModel.predict_confidence_step
1022
+ (qwen3/modeling.py:293-308) + draft_ops._predict_confidence_logits EXACTLY:
1023
+
1024
+ prev_token_ids = cat([first_prev, realized_block[:, :-1]]) # SERVE prev
1025
+ prev_emb = markov_head.get_prev_embeddings(prev).to(hidden.dtype)
1026
+ features = cat([draft_hidden, prev_emb], dim=-1)
1027
+ logit = confidence_head(features).float()
1028
+
1029
+ draft_hidden == sample_hidden_states (the post-norm hidden fed to lm_head;
1030
+ same tensor used as draft_hidden in train_head.py:567-568). The predecessor
1031
+ is the SERVE-PATH realized token (sampled/argmax'd at k-1, first_prev for
1032
+ k=0), NOT the teacher-forced prev used in training -- this is the intended
1033
+ serve feature (design CONFIDENCE_HEAD_DESIGN.md §9). This logit is consumed
1034
+ ONLY for the dynamic-K prefix-length decision; it never changes which tokens
1035
+ are proposed, so it cannot affect losslessness.
1036
+
1037
+ Args:
1038
+ sample_hidden_states: [B*num_spec, H] request-major / position-minor.
1039
+ draft_token_ids: [B, num_spec] the realized drafted block.
1040
+ first_prev_token_ids: [B] the verified bonus token before draft pos 0.
1041
+ num_spec: block size (num_speculative_tokens).
1042
+ Returns:
1043
+ [B, num_spec] float32 accept-rate logits.
1044
+ """
1045
+ conf = self.model.confidence_head
1046
+ assert conf is not None, (
1047
+ "predict_confidence_step called without a confidence_head"
1048
+ )
1049
+ H = sample_hidden_states.shape[-1]
1050
+ B = sample_hidden_states.shape[0] // num_spec
1051
+ hidden = sample_hidden_states.view(B, num_spec, H)
1052
+ if bool(getattr(self.config, "confidence_head_with_markov", False)):
1053
+ mk = self.model.markov_head
1054
+ assert mk is not None, (
1055
+ "confidence_head_with_markov=True but markov_head is None"
1056
+ )
1057
+ # prev[:,0]=first_prev, prev[:,k]=realized draft token at k-1.
1058
+ prev = torch.cat(
1059
+ [
1060
+ first_prev_token_ids.view(B, 1).long(),
1061
+ draft_token_ids[:, :-1].long(),
1062
+ ],
1063
+ dim=1,
1064
+ ) # [B, num_spec]
1065
+ # get_prev_embeddings == markov_w1(prev) (markov_head.py:73-74).
1066
+ prev_emb = mk.markov_w1(prev).to(dtype=hidden.dtype) # [B, num_spec, r]
1067
+ features = torch.cat([hidden, prev_emb], dim=-1)
1068
+ else:
1069
+ features = hidden
1070
+ # Match the head's own param dtype for the GEMM (bf16 to mirror training);
1071
+ # cast guards against a float32 head built from an older checkpoint.
1072
+ features = features.to(conf.weight.dtype)
1073
+ return conf(features).squeeze(-1).float() # [B, num_spec]
1074
+
1075
+ def precompute_and_store_context_kv(
1076
+ self,
1077
+ context_states: torch.Tensor,
1078
+ context_positions: torch.Tensor,
1079
+ context_slot_mapping: torch.Tensor | None = None,
1080
+ ) -> None:
1081
+ """Precompute projected + RoPE'd K/V and write to cache."""
1082
+ self.model.precompute_and_store_context_kv(
1083
+ context_states, context_positions, context_slot_mapping
1084
+ )
1085
+
1086
+ def combine_hidden_states(
1087
+ self,
1088
+ hidden_states: torch.Tensor,
1089
+ ) -> torch.Tensor:
1090
+ if not self.model.use_aux_hidden_state:
1091
+ return hidden_states
1092
+ needs_squeeze = hidden_states.dim() == 1
1093
+ if needs_squeeze:
1094
+ hidden_states = hidden_states.unsqueeze(0)
1095
+ result = self.model.fc(hidden_states)
1096
+ if needs_squeeze:
1097
+ result = result.squeeze(0)
1098
+ return result
1099
+
1100
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
1101
+ model_weights = {}
1102
+ includes_draft_id_mapping = False
1103
+ includes_embed_tokens = False
1104
+ for name, loaded_weight in weights:
1105
+ assert "mask_hidden" not in name, (
1106
+ "DFlash should use mask_token_id to embed the padding hidden state"
1107
+ )
1108
+ if "t2d" in name:
1109
+ continue
1110
+ if "d2t" in name:
1111
+ name = name.replace("d2t", "draft_id_to_target_id")
1112
+ includes_draft_id_mapping = True
1113
+ elif "lm_head" not in name:
1114
+ name = "model." + name
1115
+ if "embed_tokens" in name:
1116
+ includes_embed_tokens = True
1117
+ model_weights[name] = loaded_weight
1118
+ process_eagle_weight(self, name)
1119
+
1120
+ skip_substrs = []
1121
+ if not includes_draft_id_mapping:
1122
+ skip_substrs.append("draft_id_to_target_id")
1123
+ if not includes_embed_tokens:
1124
+ skip_substrs.append("embed_tokens")
1125
+ if not self.model.use_aux_hidden_state:
1126
+ skip_substrs.append("fc.")
1127
+ loader = AutoWeightsLoader(
1128
+ self,
1129
+ skip_prefixes=None,
1130
+ skip_substrs=skip_substrs,
1131
+ )
1132
+ loader.load_weights(model_weights.items())
1133
+ self.model._build_fused_kv_buffers()