flashmap 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flashmap/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ from transformers import AutoConfig, AutoModel
2
+
3
+ from .configuration_flashmap import FlashMapConfig
4
+ from .modeling_flashmap import FlashMapCache, FlashMapDraftModel, FlashMapGenerateOutput
5
+
6
+ # With the package imported, AutoModel loads FlashMap folders without trust_remote_code,
7
+ # and save_pretrained writes the model code next to the weights, so that the folder
8
+ # loads with trust_remote_code=True alone.
9
+ AutoConfig.register(FlashMapConfig.model_type, FlashMapConfig, exist_ok=True)
10
+ AutoModel.register(FlashMapConfig, FlashMapDraftModel, exist_ok=True)
11
+ FlashMapConfig.register_for_auto_class()
12
+ FlashMapDraftModel.register_for_auto_class("AutoModel")
13
+
14
+ __all__ = ["FlashMapCache", "FlashMapConfig", "FlashMapDraftModel", "FlashMapGenerateOutput"]
@@ -0,0 +1,105 @@
1
+ """Configuration of a FlashMap draft model."""
2
+
3
+ from transformers import PretrainedConfig
4
+
5
+
6
+ # Jump grids of the "decoding_error" time schedule (Flow Map Language Models,
7
+ # eq. 25) at the Qwen3 vocabulary size, with the endpoints set to exactly 0 and 1.
8
+ # A draft with ``n`` steps jumps from grid[i] to grid[i + 1] for i < n.
9
+ DEFAULT_JUMP_GRIDS = {
10
+ "1": [0.0, 1.0],
11
+ "2": [0.0, 0.8172569274902344, 1.0],
12
+ "3": [0.0, 0.8010796308517456, 0.8310279846191406, 1.0],
13
+ "4": [0.0, 0.7906050682067871, 0.8172569274902344, 0.8379518389701843, 1.0],
14
+ }
15
+
16
+
17
+ class FlashMapConfig(PretrainedConfig):
18
+ r"""A FlashMap drafter for speculative decoding.
19
+
20
+ FlashMap drafts a block of ``block_size`` tokens after the last verified one
21
+ (the anchor) with a flow map: a DiT that jumps from Gaussian noise over the
22
+ target's vocabulary to the block's per-slot token distributions, conditioned on
23
+ the target model's hidden states through cross-attention.
24
+
25
+ The drafter borrows the target's token embedding and lm_head, which are not part
26
+ of its weights.
27
+
28
+ Args:
29
+ target_model (`str`): Hub id of the target model the drafter was trained for.
30
+ target_hidden_size (`int`): Hidden size of the target model.
31
+ vocab_size (`int`): Vocabulary size of the target model.
32
+ target_layer_ids (`list[int]`): Target decoder layers whose outputs (that is,
33
+ `hidden_states[i + 1]` of a transformers forward) are the drafter's context.
34
+ block_size (`int`): Number of draft tokens per block.
35
+ hidden_size (`int`): Width of the DiT.
36
+ num_hidden_layers (`int`): Number of DiT blocks.
37
+ num_attention_heads (`int`): Attention heads of the DiT.
38
+ intermediate_size (`int`): MLP width of the DiT.
39
+ cond_dim (`int`): Width of the time conditioning.
40
+ time_embed_dim (`int`): Size of the sinusoidal time features.
41
+ time_scale (`float`): Times in [0, 1] are multiplied by this before their
42
+ sinusoidal embedding.
43
+ rope_theta (`float`): RoPE base. RoPE rotates interleaved pairs of channels.
44
+ rms_norm_eps (`float`): Epsilon of the context, anchor and state RMSNorms.
45
+ qk_norm_eps (`float`): Epsilon of the query/key RMSNorms.
46
+ layer_norm_eps (`float`): Epsilon of the adaptive LayerNorms.
47
+ noise_gate (`str`): How the noise term of the state embedding is weighted:
48
+ "jump" by `noise_gate * (t - s)` for a jump from `s` to `t`, "constant" by
49
+ `noise_gate` alone.
50
+ time_schedule (`str`): The schedule the jump grids come from.
51
+ num_steps (`int`): Default number of jumps per draft.
52
+ jump_grids (`dict[str, list[float]]`): Jump times for each number of steps.
53
+ """
54
+
55
+ model_type = "flashmap"
56
+
57
+ def __init__(
58
+ self,
59
+ target_model: str = "Qwen/Qwen3-8B",
60
+ target_hidden_size: int = 4096,
61
+ vocab_size: int = 151936,
62
+ target_layer_ids=(1, 9, 17, 25, 33),
63
+ block_size: int = 16,
64
+ hidden_size: int = 2048,
65
+ num_hidden_layers: int = 6,
66
+ num_attention_heads: int = 16,
67
+ intermediate_size: int = 8192,
68
+ cond_dim: int = 256,
69
+ time_embed_dim: int = 256,
70
+ time_scale: float = 1000.0,
71
+ rope_theta: float = 10000.0,
72
+ rms_norm_eps: float = 1e-6,
73
+ qk_norm_eps: float = 1e-5,
74
+ layer_norm_eps: float = 1e-6,
75
+ noise_gate: str = "jump",
76
+ time_schedule: str = "decoding_error",
77
+ num_steps: int = 1,
78
+ jump_grids=None,
79
+ **kwargs,
80
+ ):
81
+ self.target_model = target_model
82
+ self.target_hidden_size = target_hidden_size
83
+ self.vocab_size = vocab_size
84
+ self.target_layer_ids = list(target_layer_ids)
85
+ self.block_size = block_size
86
+ self.hidden_size = hidden_size
87
+ self.num_hidden_layers = num_hidden_layers
88
+ self.num_attention_heads = num_attention_heads
89
+ self.intermediate_size = intermediate_size
90
+ self.cond_dim = cond_dim
91
+ self.time_embed_dim = time_embed_dim
92
+ self.time_scale = time_scale
93
+ self.rope_theta = rope_theta
94
+ self.rms_norm_eps = rms_norm_eps
95
+ self.qk_norm_eps = qk_norm_eps
96
+ self.layer_norm_eps = layer_norm_eps
97
+ self.noise_gate = noise_gate
98
+ self.time_schedule = time_schedule
99
+ self.num_steps = num_steps
100
+ self.jump_grids = dict(DEFAULT_JUMP_GRIDS if jump_grids is None else jump_grids)
101
+ kwargs.setdefault("tie_word_embeddings", False)
102
+ super().__init__(**kwargs)
103
+
104
+
105
+ __all__ = ["FlashMapConfig"]
@@ -0,0 +1,556 @@
1
+ """FlashMap: speculative decoding with a flow-map drafter.
2
+
3
+ FlashMap drafts the ``K`` tokens after the last verified token (the anchor) in one
4
+ network evaluation, like DFlash, but from a generative model that can couple the
5
+ slots of a block. A block is a point ``x`` in ``R^{K x V}`` (``V`` the target's
6
+ vocabulary) moving along the path ``x_s = (1 - s) x0 + s onehot(y)`` from Gaussian
7
+ noise ``x0`` at ``s = 0`` to the one-hot tokens ``y`` at ``s = 1``. The drafter is a
8
+ *flow map* (Flow Map Language Models, arXiv:2602.16813): a network that jumps along
9
+ this path from time ``s`` to time ``t``, distilled from a flow trained on it. Its
10
+ output is a distribution over the vocabulary for every slot. Drafting with one step
11
+ is a single jump from ``s = 0`` to ``t = 1``:
12
+
13
+ x0 ~ N(0, I) -> logits = map(x0; 0 -> 1) -> y_k ~ softmax(logits_k) per slot.
14
+
15
+ Given the noise the slots are independent draws, so ``softmax(logits)`` is an exact
16
+ proposal distribution for lossless speculative (rejection) sampling. Across noise
17
+ draws the slots can be correlated, since the same noise conditions all of them; one
18
+ jump captures this joint structure only partly, and more jumps capture more. More
19
+ steps jump over a grid ``0 = t_0 < t_1 < ... < t_n = 1``, moving the state to
20
+ ``x <- ((1 - t) x + (t - s) softmax(logits)) / (1 - s)`` between jumps.
21
+
22
+ The network is a small DiT. It reads the block's state through the target's token
23
+ embedding ``E`` (the noise enters as ``x0 @ E``), conditions on the jump's start and
24
+ end times through adaptive LayerNorms, self-attends across the block's slots, and
25
+ cross-attends to the target's hidden states at earlier positions (from a few of its
26
+ layers) and to the anchor's embedding. Its output goes through the target's lm_head.
27
+ The target's embedding and lm_head are not part of the drafter's weights.
28
+
29
+ ``FlashMapDraftModel.spec_generate`` runs lossless speculative decoding with a
30
+ transformers target model. This module is inference-only.
31
+ """
32
+
33
+ import math
34
+ import warnings
35
+ from contextlib import contextmanager
36
+ from dataclasses import dataclass, field
37
+ from typing import Optional
38
+
39
+ import torch
40
+ import torch.nn.functional as F
41
+ from torch import nn
42
+ from transformers import DynamicCache, PreTrainedModel
43
+
44
+ from .configuration_flashmap import FlashMapConfig
45
+
46
+
47
+ def rope_tables(positions: torch.Tensor, head_dim: int, theta: float) -> tuple[torch.Tensor, torch.Tensor]:
48
+ """``cos`` and ``sin`` of the rotary angles at ``positions (B, T)``, each
49
+ ``(B, T, 1, head_dim // 2)`` in float32; computed once and shared by all layers."""
50
+ inv_freq = 1.0 / theta ** (torch.arange(0, head_dim, 2, device=positions.device).float() / head_dim)
51
+ angles = positions.float()[..., None, None] * inv_freq
52
+ return angles.cos(), angles.sin()
53
+
54
+
55
+ def apply_rope(x: torch.Tensor, rope: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor:
56
+ """Rotate the interleaved channel pairs ``(x[2i], x[2i + 1])`` of ``x (B, T, heads,
57
+ head_dim)`` by the angles of ``rope_tables``, in float32."""
58
+ cos, sin = rope
59
+ x1, x2 = x.float().unflatten(-1, (-1, 2)).unbind(-1)
60
+ return torch.stack([x1 * cos - x2 * sin, x2 * cos + x1 * sin], dim=-1).flatten(-2).type_as(x)
61
+
62
+
63
+ class FlashMapRMSNorm(nn.Module):
64
+ """RMSNorm whose scale ``weight / rms(x)`` is rounded to the input's dtype before
65
+ it multiplies the input (the order the released weights were trained with)."""
66
+
67
+ def __init__(self, dim: int, eps: float):
68
+ super().__init__()
69
+ self.weight = nn.Parameter(torch.ones(dim))
70
+ self.eps = eps
71
+
72
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
73
+ rms = torch.rsqrt(x.float().pow(2).mean(dim=-1, keepdim=True) + self.eps)
74
+ return x * (self.weight.float() * rms).to(x.dtype)
75
+
76
+
77
+ class FlashMapTimeEmbedding(nn.Module):
78
+ """Sinusoidal features of a time in [0, 1], then an MLP."""
79
+
80
+ def __init__(self, config: FlashMapConfig):
81
+ super().__init__()
82
+ self.dim, self.scale = config.time_embed_dim, config.time_scale
83
+ self.linear_1 = nn.Linear(config.time_embed_dim, config.cond_dim)
84
+ self.linear_2 = nn.Linear(config.cond_dim, config.cond_dim)
85
+
86
+ def forward(self, t: torch.Tensor) -> torch.Tensor:
87
+ half = self.dim // 2
88
+ freqs = torch.exp(-math.log(10000.0) * torch.arange(half, device=t.device).float() / half)
89
+ args = (t.float() * self.scale)[..., None] * freqs
90
+ emb = torch.cat([args.cos(), args.sin()], dim=-1).to(self.linear_1.weight.dtype)
91
+ return self.linear_2(F.silu(self.linear_1(emb)))
92
+
93
+
94
+ class FlashMapAdaLayerNorm(nn.Module):
95
+ """LayerNorm modulated by the time conditioning (AdaLN-zero)."""
96
+
97
+ def __init__(self, config: FlashMapConfig):
98
+ super().__init__()
99
+ self.linear = nn.Linear(config.cond_dim, 3 * config.hidden_size)
100
+ self.eps = config.layer_norm_eps
101
+
102
+ def modulation(self, cond: torch.Tensor) -> tuple[torch.Tensor, ...]:
103
+ """``(scale, shift, gate)`` for the time conditioning ``cond``."""
104
+ return self.linear(F.silu(cond)).chunk(3, dim=-1)
105
+
106
+ def forward(self, x: torch.Tensor, modulation) -> tuple[torch.Tensor, torch.Tensor]:
107
+ """``norm(x) * (1 + scale) + shift``, and the gate of the residual branch."""
108
+ scale, shift, gate = modulation
109
+ return F.layer_norm(x, x.shape[-1:], eps=self.eps) * (1 + scale) + shift, gate
110
+
111
+
112
+ class FlashMapAttention(nn.Module):
113
+ """Multi-head attention with RMS-normed queries and keys and rotary positions.
114
+
115
+ Keys and values come from ``project_kv`` so that cross-attention can project its
116
+ context once and cache it."""
117
+
118
+ def __init__(self, config: FlashMapConfig):
119
+ super().__init__()
120
+ dim = config.hidden_size
121
+ self.num_heads = config.num_attention_heads
122
+ self.head_dim = dim // self.num_heads
123
+ self.q_proj = nn.Linear(dim, dim, bias=False)
124
+ self.k_proj = nn.Linear(dim, dim, bias=False)
125
+ self.v_proj = nn.Linear(dim, dim, bias=False)
126
+ self.o_proj = nn.Linear(dim, dim, bias=False)
127
+ self.q_norm = FlashMapRMSNorm(self.head_dim, config.qk_norm_eps)
128
+ self.k_norm = FlashMapRMSNorm(self.head_dim, config.qk_norm_eps)
129
+
130
+ def _heads(self, x: torch.Tensor) -> torch.Tensor:
131
+ return x.view(*x.shape[:2], self.num_heads, self.head_dim)
132
+
133
+ def project_kv(self, x: torch.Tensor, rope) -> tuple[torch.Tensor, torch.Tensor]:
134
+ """Keys and values ``(B, heads, T, head_dim)`` of ``x (B, T, dim)``."""
135
+ keys = apply_rope(self.k_norm(self._heads(self.k_proj(x))), rope)
136
+ return keys.transpose(1, 2), self._heads(self.v_proj(x)).transpose(1, 2)
137
+
138
+ def forward(self, x, rope, keys, values) -> torch.Tensor:
139
+ queries = apply_rope(self.q_norm(self._heads(self.q_proj(x))), rope)
140
+ out = F.scaled_dot_product_attention(queries.transpose(1, 2), keys, values)
141
+ return self.o_proj(out.transpose(1, 2).flatten(2))
142
+
143
+
144
+ class FlashMapMLP(nn.Module):
145
+ def __init__(self, config: FlashMapConfig):
146
+ super().__init__()
147
+ self.fc1 = nn.Linear(config.hidden_size, config.intermediate_size)
148
+ self.fc2 = nn.Linear(config.intermediate_size, config.hidden_size)
149
+
150
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
151
+ return self.fc2(F.gelu(self.fc1(x)))
152
+
153
+
154
+ class FlashMapLayer(nn.Module):
155
+ """A DiT block: self-attention across the block's slots, cross-attention to the
156
+ context, and an MLP, each behind a time-modulated LayerNorm and a gated residual."""
157
+
158
+ def __init__(self, config: FlashMapConfig):
159
+ super().__init__()
160
+ self.self_attn_adaln = FlashMapAdaLayerNorm(config)
161
+ self.self_attn = FlashMapAttention(config)
162
+ self.cross_attn_adaln = FlashMapAdaLayerNorm(config)
163
+ self.cross_attn = FlashMapAttention(config)
164
+ self.mlp_adaln = FlashMapAdaLayerNorm(config)
165
+ self.mlp = FlashMapMLP(config)
166
+
167
+ def forward(self, x, modulations, rope, context: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor:
168
+ self_mod, cross_mod, mlp_mod = modulations
169
+ h, gate = self.self_attn_adaln(x, self_mod)
170
+ x = x + gate * self.self_attn(h, rope, *self.self_attn.project_kv(h, rope))
171
+ h, gate = self.cross_attn_adaln(x, cross_mod)
172
+ x = x + gate * self.cross_attn(h, rope, *context)
173
+ h, gate = self.mlp_adaln(x, mlp_mod)
174
+ return x + gate * self.mlp(h)
175
+
176
+
177
+ class FlashMapCache:
178
+ """Cross-attention keys and values of the drafter's context, for every DiT layer.
179
+
180
+ Positions ``[0, length)`` hold the target's hidden states at the tokens verified
181
+ so far. Position ``length`` belongs to the anchor, the last token, whose hidden
182
+ state the target has not computed yet: drafting writes the anchor's embedding
183
+ there, and the next ``extend_context`` overwrites it with its hidden state.
184
+ Create it on the drafter's device and dtype.
185
+ """
186
+
187
+ def __init__(self, config: FlashMapConfig, batch_size: int, max_length: int, device=None, dtype=None):
188
+ head_dim = config.hidden_size // config.num_attention_heads
189
+ shape = (config.num_hidden_layers, 2, batch_size, config.num_attention_heads, max_length, head_dim)
190
+ self.kv = torch.zeros(shape, device=device, dtype=dtype)
191
+ self.length = 0
192
+
193
+ def write(self, layer: int, start: int, keys: torch.Tensor, values: torch.Tensor) -> None:
194
+ end = start + keys.shape[2]
195
+ if end > self.kv.shape[4]:
196
+ raise ValueError(f"FlashMapCache holds {self.kv.shape[4]} positions, writing up to {end}")
197
+ self.kv[layer, 0, :, :, start:end] = keys
198
+ self.kv[layer, 1, :, :, start:end] = values
199
+
200
+ def context(self, end: int) -> list[tuple[torch.Tensor, torch.Tensor]]:
201
+ return [(kv[0, :, :, :end], kv[1, :, :, :end]) for kv in self.kv]
202
+
203
+
204
+ @dataclass
205
+ class FlashMapGenerateOutput:
206
+ """``sequences``: prompt and generated tokens, ``(1, length)``.
207
+ ``acceptance_lengths``: tokens committed by each target forward pass (accepted
208
+ draft tokens plus the target's own token). The last pass counts in full even
209
+ where it runs past ``max_new_tokens``, as in the evaluation of the paper."""
210
+
211
+ sequences: torch.LongTensor
212
+ num_new_tokens: int
213
+ acceptance_lengths: list = field(default_factory=list)
214
+
215
+ @property
216
+ def acceptance_length(self) -> float:
217
+ """Mean tokens per target forward pass (the paper's gamma)."""
218
+ return sum(self.acceptance_lengths) / max(len(self.acceptance_lengths), 1)
219
+
220
+
221
+ class FlashMapDraftModel(PreTrainedModel):
222
+ config_class = FlashMapConfig
223
+ base_model_prefix = "flashmap"
224
+ _no_split_modules = ["FlashMapLayer"]
225
+ _supports_sdpa = True
226
+
227
+ def __init__(self, config: FlashMapConfig):
228
+ super().__init__(config)
229
+ dim, target_dim = config.hidden_size, config.target_hidden_size
230
+ self.head_dim = dim // config.num_attention_heads
231
+ # Context: the target's hidden states at target_layer_ids, and the anchor.
232
+ self.fc = nn.Linear(len(config.target_layer_ids) * target_dim, dim, bias=False)
233
+ self.hidden_norm = FlashMapRMSNorm(dim, config.rms_norm_eps)
234
+ self.anchor_proj = nn.Linear(target_dim, dim)
235
+ self.anchor_norm = FlashMapRMSNorm(dim, config.rms_norm_eps)
236
+ # State featurization (see embed_state).
237
+ self.state_norm = FlashMapRMSNorm(target_dim, config.rms_norm_eps)
238
+ self.noise_norm = FlashMapRMSNorm(target_dim, config.rms_norm_eps)
239
+ self.noise_gate = nn.Parameter(torch.ones(1))
240
+ # The DiT.
241
+ self.x_embed = nn.Linear(target_dim, dim)
242
+ self.time_embed = FlashMapTimeEmbedding(config) # the jump's start time s
243
+ self.jump_embed = FlashMapTimeEmbedding(config) # the jump's end time t
244
+ self.layers = nn.ModuleList(FlashMapLayer(config) for _ in range(config.num_hidden_layers))
245
+ self.final_adaln = FlashMapAdaLayerNorm(config)
246
+ self.head = nn.Linear(dim, target_dim)
247
+ self._clear_caches()
248
+ self.post_init()
249
+
250
+ # Values computed from the weights once and reused by every draft: the AdaLN
251
+ # modulations of each jump, and constants of the target's embedding. Moving or
252
+ # reloading the weights clears them.
253
+
254
+ def _clear_caches(self) -> None:
255
+ self._time_cache, self._target_cache = {}, {}
256
+
257
+ def _apply(self, fn, *args, **kwargs):
258
+ self._clear_caches()
259
+ return super()._apply(fn, *args, **kwargs)
260
+
261
+ def load_state_dict(self, *args, **kwargs):
262
+ self._clear_caches()
263
+ return super().load_state_dict(*args, **kwargs)
264
+
265
+ @torch.no_grad()
266
+ def time_conditioning(self, s: float, t: float):
267
+ """The AdaLN modulations of the jump from ``s`` to ``t``, for every norm of the
268
+ DiT: per layer (self-attention, cross-attention, MLP), then the final norm. They
269
+ depend on the two times alone, so each jump computes them once."""
270
+ if (s, t) not in self._time_cache:
271
+ times = torch.tensor([s, t], device=self.device)
272
+ cond = (self.time_embed(times[:1]) + self.jump_embed(times[1:]))[None]
273
+ layers = [
274
+ [norm.modulation(cond) for norm in (block.self_attn_adaln, block.cross_attn_adaln, block.mlp_adaln)]
275
+ for block in self.layers
276
+ ]
277
+ self._time_cache[(s, t)] = (layers, self.final_adaln.modulation(cond))
278
+ return self._time_cache[(s, t)]
279
+
280
+ def forward(self, inputs_embeds, s: float, t: float, position_ids, context) -> torch.Tensor:
281
+ """The jump from time ``s`` to ``t``: the featurized state ``inputs_embeds (B, K,
282
+ target_hidden)`` of blocks at ``position_ids (B, K)`` -> hidden states ``(B, K,
283
+ target_hidden)`` whose target lm_head logits are the distribution it lands on.
284
+ ``context`` holds each layer's cross-attention keys and values."""
285
+ layer_modulations, final_modulation = self.time_conditioning(s, t)
286
+ rope = rope_tables(position_ids, self.head_dim, self.config.rope_theta)
287
+ h = self.x_embed(inputs_embeds)
288
+ for layer, modulations, layer_context in zip(self.layers, layer_modulations, context):
289
+ h = layer(h, modulations, rope, layer_context)
290
+ h, _ = self.final_adaln(h, final_modulation)
291
+ return self.head(h)
292
+
293
+ # ---- context ------------------------------------------------------------------
294
+
295
+ def select_target_hidden(self, hidden_states) -> torch.Tensor:
296
+ """The drafter's features from a target forward's ``hidden_states`` tuple: the
297
+ outputs of layers ``target_layer_ids``, concatenated."""
298
+ return torch.cat([hidden_states[i + 1] for i in self.config.target_layer_ids], dim=-1)
299
+
300
+ @contextmanager
301
+ def target_features(self, target: PreTrainedModel):
302
+ """Forward hooks that keep the outputs of the target's layers ``target_layer_ids``.
303
+ Yields a function returning them, concatenated, for the target's last forward.
304
+ Unlike ``output_hidden_states``, this does not keep every layer's output."""
305
+ layers, outputs = target.get_decoder().layers, {}
306
+
307
+ def keep(i):
308
+ def hook(module, args, output):
309
+ outputs[i] = output[0] if isinstance(output, tuple) else output
310
+
311
+ return hook
312
+
313
+ hooks = [layers[i].register_forward_hook(keep(i)) for i in self.config.target_layer_ids]
314
+ try:
315
+ yield lambda: torch.cat([outputs[i] for i in self.config.target_layer_ids], dim=-1)
316
+ finally:
317
+ for hook in hooks:
318
+ hook.remove()
319
+
320
+ @torch.no_grad()
321
+ def extend_context(self, cache: FlashMapCache, target_hidden: torch.Tensor) -> None:
322
+ """Append the target's features ``(B, n, ...)`` at the next ``n`` verified positions."""
323
+ start, n = cache.length, target_hidden.shape[1]
324
+ context = self.hidden_norm(self.fc(target_hidden.to(self.dtype)))
325
+ positions = torch.arange(start, start + n, device=context.device).expand(context.shape[0], -1)
326
+ rope = rope_tables(positions, self.head_dim, self.config.rope_theta)
327
+ for i, layer in enumerate(self.layers):
328
+ cache.write(i, start, *layer.cross_attn.project_kv(context, rope))
329
+ cache.length += n
330
+
331
+ # ---- drafting -----------------------------------------------------------------
332
+
333
+ def embed_state(self, state, s: float, t: float, embedding: torch.Tensor) -> torch.Tensor:
334
+ """DiT input for a vocabulary-sized state ``(B, K, V)`` at time ``s`` jumping to ``t``.
335
+
336
+ The state is read through the target's token embedding twice: as the posterior
337
+ over clean tokens it implies, ``softmax(s x / (1 - s)^2)``, and linearly, weighted
338
+ by the jump. At ``s = 0`` the posterior is uniform whatever the noise, so the
339
+ linear term is how a jump from noise sees which noise it starts from."""
340
+ if s == 0:
341
+ posterior_embeds = self.target_constants(embedding)[0]
342
+ else:
343
+ posterior = (state.float() * (s / max(1.0 - s, 1e-3) ** 2)).softmax(dim=-1)
344
+ posterior_embeds = posterior.to(embedding.dtype) @ embedding
345
+ return self.mix_state(posterior_embeds, state.to(embedding.dtype) @ embedding, s, t)
346
+
347
+ def mix_state(self, posterior_embeds, state_embeds, s: float, t: float) -> torch.Tensor:
348
+ """``embed_state`` from the state's two projections onto the embedding."""
349
+ # the noise term's weight: noise_gate times the jump ("jump") or alone ("constant")
350
+ jump = 1.0 if self.config.noise_gate == "constant" else t - s
351
+ gate = (self.noise_gate.float() * jump).to(self.dtype)
352
+ posterior_term = self.state_norm(posterior_embeds.to(self.dtype))
353
+ return posterior_term + gate * self.noise_norm(state_embeds.to(self.dtype))
354
+
355
+ @torch.no_grad()
356
+ def target_constants(self, embedding: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
357
+ """What drafting needs of the target's embedding ``E (V, H)``: the embedded
358
+ uniform posterior (the posterior of any state at ``s = 0``), and ``L`` with
359
+ ``L L^T = E^T E``.
360
+
361
+ A one-step draft reads its noise only as ``x0 @ E`` with ``x0 ~ N(0, I_V)``,
362
+ a Gaussian with covariance ``E^T E``; ``z @ L^T`` with ``z ~ N(0, I_H)`` has the
363
+ same distribution and needs no vocabulary-sized noise."""
364
+ key = (embedding.data_ptr(), tuple(embedding.shape), embedding.device, embedding.dtype)
365
+ if key not in self._target_cache:
366
+ vocab_size = embedding.shape[0]
367
+ uniform = torch.full((1, vocab_size), 1.0 / vocab_size, device=embedding.device)
368
+ # E^T E in float32 where E is; its (H, H) factor in float64 on the CPU, as
369
+ # many accelerators lack fast (or any) float64.
370
+ gram = sum(rows.float().T @ rows.float() for rows in embedding.split(16384))
371
+ factor = torch.linalg.cholesky(gram.cpu().double())
372
+ uniform_embeds = uniform.to(embedding.dtype) @ embedding
373
+ self._target_cache = {key: (uniform_embeds, factor.to(self.device, self.dtype))}
374
+ return self._target_cache[key]
375
+
376
+ @torch.no_grad()
377
+ def draft(
378
+ self,
379
+ anchor_ids: torch.LongTensor,
380
+ cache: FlashMapCache,
381
+ embed_tokens: nn.Module,
382
+ lm_head: nn.Module,
383
+ num_steps: Optional[int] = None,
384
+ noise: Optional[torch.Tensor] = None,
385
+ generator: Optional[torch.Generator] = None,
386
+ ) -> torch.Tensor:
387
+ """Draft logits ``(B, block_size, V)`` for the block after ``anchor_ids (B,)``.
388
+
389
+ ``cache`` holds the target's features at every position before the anchor.
390
+ ``embed_tokens`` and ``lm_head`` are the target's. ``noise (B, block_size, V)``
391
+ is the starting state ``x0``; by default it is drawn, and a one-step draft draws
392
+ only its embedding (see ``target_constants``). Given the noise, the block's
393
+ tokens are independent draws from ``softmax(logits)``, one per slot."""
394
+ num_steps = num_steps or self.config.num_steps
395
+ if str(num_steps) not in self.config.jump_grids:
396
+ supported = ", ".join(sorted(self.config.jump_grids, key=int))
397
+ raise ValueError(f"num_steps must be one of {supported} (the config's jump_grids), got {num_steps}")
398
+ grid = self.config.jump_grids[str(num_steps)]
399
+ batch_size, block_size = anchor_ids.shape[0], self.config.block_size
400
+ embedding, device = embed_tokens.weight, anchor_ids.device
401
+
402
+ # The anchor's embedding enters the context at the anchor's position.
403
+ anchor = cache.length
404
+ anchor_context = self.anchor_norm(self.anchor_proj(embed_tokens(anchor_ids[:, None]).to(self.dtype)))
405
+ anchor_position = torch.full((batch_size, 1), anchor, device=device)
406
+ anchor_rope = rope_tables(anchor_position, self.head_dim, self.config.rope_theta)
407
+ for i, layer in enumerate(self.layers):
408
+ cache.write(i, anchor, *layer.cross_attn.project_kv(anchor_context, anchor_rope))
409
+ context = cache.context(anchor + 1)
410
+ positions = anchor + 1 + torch.arange(block_size, device=device).expand(batch_size, -1)
411
+
412
+ state = noise
413
+ if state is None and num_steps == 1:
414
+ uniform_embeds, factor = self.target_constants(embedding)
415
+ shape = (batch_size, block_size, factor.shape[0])
416
+ z = torch.randn(shape, device=device, dtype=factor.dtype, generator=generator)
417
+ inputs = self.mix_state(uniform_embeds, z @ factor.T, 0.0, grid[1])
418
+ elif state is None:
419
+ shape = (batch_size, block_size, embedding.shape[0])
420
+ state = torch.randn(shape, device=device, generator=generator)
421
+
422
+ for s, t in zip(grid[:-1], grid[1:]):
423
+ if state is not None:
424
+ inputs = self.embed_state(state, s, t, embedding)
425
+ logits = lm_head(self(inputs, s, t, positions, context).to(lm_head.weight.dtype))
426
+ if t < 1.0:
427
+ # move to where the jump lands (eq. 19 of arXiv:2602.16813)
428
+ state = ((1.0 - t) * state + (t - s) * logits.float().softmax(dim=-1)) / (1.0 - s)
429
+ return logits
430
+
431
+ # ---- speculative decoding -----------------------------------------------------
432
+
433
+ def check_target(self, target: PreTrainedModel) -> None:
434
+ """Raise if ``target`` cannot serve as this drafter's target; warn if it is not
435
+ the model the drafter was trained for (the output stays lossless, but drafts
436
+ from another model's hidden states are poor)."""
437
+ config = target.config.get_text_config()
438
+ embedding, lm_head = target.get_input_embeddings().weight, target.get_output_embeddings().weight
439
+ expected = (self.config.vocab_size, self.config.target_hidden_size)
440
+ if tuple(embedding.shape) != expected or tuple(lm_head.shape) != expected:
441
+ raise ValueError(
442
+ f"this drafter needs a target with vocabulary {expected[0]} and hidden size {expected[1]} "
443
+ f"({self.config.target_model}), got an embedding of shape {tuple(embedding.shape)}"
444
+ )
445
+ if max(self.config.target_layer_ids) >= config.num_hidden_layers - 1:
446
+ layers = config.num_hidden_layers
447
+ raise ValueError(f"target_layer_ids {self.config.target_layer_ids} need more than {layers} target layers")
448
+ name = (getattr(config, "_name_or_path", None) or "").rstrip("/")
449
+ if name and name.split("/")[-1] != self.config.target_model.split("/")[-1]:
450
+ warnings.warn(f"this drafter was trained for {self.config.target_model}, not {name}")
451
+ devices = {self.device, embedding.device, lm_head.device}
452
+ if len(devices) > 1:
453
+ raise ValueError(f"the drafter and the target's embedding and lm_head must share a device, got {devices}")
454
+
455
+ @torch.no_grad()
456
+ def spec_generate(
457
+ self,
458
+ target: PreTrainedModel,
459
+ input_ids: torch.LongTensor,
460
+ max_new_tokens: int = 256,
461
+ temperature: float = 0.0,
462
+ num_steps: Optional[int] = None,
463
+ stop_token_ids: Optional[list] = None,
464
+ generator: Optional[torch.Generator] = None,
465
+ ) -> FlashMapGenerateOutput:
466
+ """Lossless speculative decoding of ``target`` (a transformers causal LM) from
467
+ the prompt ``input_ids (1, P)``.
468
+
469
+ Every round drafts a block after the last token, has the target score it in one
470
+ forward pass, and keeps the longest prefix the target accepts plus one token of
471
+ the target's own. At ``temperature`` 0 the output is the target's greedy
472
+ decoding; above it, tokens are distributed exactly as the target samples them
473
+ (rejection sampling against the draft's per-slot probabilities). Generation stops
474
+ after ``max_new_tokens`` or at a token in ``stop_token_ids`` (default: the
475
+ target's ``eos_token_id``). The drafter, the target and ``generator`` (which
476
+ makes sampling reproducible) must be on one device."""
477
+ if input_ids.shape[0] != 1:
478
+ raise ValueError("spec_generate decodes one sequence at a time")
479
+ self.check_target(target)
480
+ if generator is not None: # a "cuda" generator (no index) is on the current device
481
+ device = generator.device
482
+ if device.type == "cuda" and device.index is None:
483
+ device = torch.device("cuda", torch.cuda.current_device())
484
+ if (device.type, device.index or 0) != (self.device.type, self.device.index or 0):
485
+ raise ValueError(f"generator must be on {self.device}, got one on {generator.device}")
486
+ if stop_token_ids is None:
487
+ eos = target.generation_config.eos_token_id
488
+ stop_token_ids = [] if eos is None else [eos] if isinstance(eos, int) else list(eos)
489
+ input_ids = input_ids.to(self.device)
490
+ stop = torch.tensor(stop_token_ids, device=self.device, dtype=torch.long)
491
+ embed_tokens, lm_head = target.get_input_embeddings(), target.get_output_embeddings()
492
+ block_size, prompt_length = self.config.block_size, input_ids.shape[1]
493
+
494
+ def sample(logits): # (1, n, V) -> tokens (1, n) and their distributions
495
+ if temperature <= 0:
496
+ return logits.argmax(dim=-1), None
497
+ probs = (logits.float() / temperature).softmax(dim=-1)
498
+ return torch.multinomial(probs[0], 1, generator=generator).T, probs
499
+
500
+ with self.target_features(target) as target_features:
501
+ target_cache = DynamicCache()
502
+ out = target(input_ids, past_key_values=target_cache, use_cache=True, logits_to_keep=1)
503
+ tokens = sample(out.logits[:, -1:])[0]
504
+ max_length = prompt_length + max_new_tokens + block_size + 1 # the last round may overshoot
505
+ cache = FlashMapCache(self.config, 1, max_length, device=self.device, dtype=self.dtype)
506
+ self.extend_context(cache, target_features())
507
+ acceptance_lengths = []
508
+
509
+ while tokens.shape[1] < max_new_tokens and not torch.isin(tokens[0, -1], stop):
510
+ anchor = tokens[:, -1]
511
+ draft_logits = self.draft(anchor, cache, embed_tokens, lm_head, num_steps, generator=generator)
512
+ draft, draft_probs = sample(draft_logits)
513
+ position = prompt_length + tokens.shape[1] - 1 # the anchor's
514
+ out = target(
515
+ torch.cat([anchor[:, None], draft], dim=1),
516
+ position_ids=torch.arange(position, position + block_size + 1, device=draft.device)[None],
517
+ past_key_values=target_cache,
518
+ use_cache=True,
519
+ )
520
+ if temperature <= 0:
521
+ target_tokens = out.logits.argmax(dim=-1)
522
+ accepted = int((draft == target_tokens[:, :-1]).cumprod(dim=1).sum())
523
+ bonus = target_tokens[:, accepted]
524
+ else:
525
+ target_probs = (out.logits.float() / temperature).softmax(dim=-1)[0]
526
+ p = target_probs[:-1].gather(-1, draft[0, :, None])[:, 0]
527
+ q = draft_probs[0].gather(-1, draft[0, :, None])[:, 0]
528
+ u = torch.rand(q.shape, device=q.device, generator=generator)
529
+ accepted = int((u * q < p).cumprod(dim=0).sum())
530
+ residual = target_probs[accepted]
531
+ if accepted < block_size: # resample the rejected slot from max(p - q, 0)
532
+ residual = (residual - draft_probs[0, accepted]).clamp_min(0)
533
+ if residual.sum() <= 0:
534
+ residual = target_probs[accepted]
535
+ bonus = torch.multinomial(residual, 1, generator=generator)
536
+ new_tokens = torch.cat([draft[:, :accepted], bonus.view(1, 1)], dim=1)
537
+ hits = torch.isin(new_tokens[0], stop).nonzero()
538
+ if len(hits):
539
+ new_tokens = new_tokens[:, : int(hits[0]) + 1]
540
+ tokens = torch.cat([tokens, new_tokens], dim=1)
541
+ acceptance_lengths.append(new_tokens.shape[1])
542
+ # keep the anchor and the accepted drafts; the bonus token is the next anchor
543
+ excess = target_cache.get_seq_length() - (position + accepted + 1)
544
+ if excess > 0: # crop(-n) drops n tokens in every transformers version
545
+ target_cache.crop(-excess)
546
+ self.extend_context(cache, target_features()[:, : accepted + 1])
547
+
548
+ tokens = tokens[:, :max_new_tokens]
549
+ return FlashMapGenerateOutput(
550
+ sequences=torch.cat([input_ids, tokens], dim=1),
551
+ num_new_tokens=tokens.shape[1],
552
+ acceptance_lengths=acceptance_lengths,
553
+ )
554
+
555
+
556
+ __all__ = ["FlashMapCache", "FlashMapDraftModel", "FlashMapGenerateOutput"]