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/vllm/model.py ADDED
@@ -0,0 +1,881 @@
1
+ """FlashMap draft model for vLLM.
2
+
3
+ The model is driven by ``FlashMapSpeculator`` (speculator.py), which reuses
4
+ vLLM's DFlash machinery: the target's aux hidden states are fused by ``fc``
5
+ (``combine_hidden_states``) and every committed token's cross-attention keys and
6
+ values go to the draft's paged KV cache; each draft step then runs ``forward`` on
7
+ ``block_size`` query rows per request, laid out as in DSpark: row 0 carries the
8
+ anchor token at the anchor position ``p`` and row ``k`` sits at ``p + k``.
9
+
10
+ A FlashMap draft step, for each request:
11
+
12
+ * writes the keys/values of the context committed since the last step and of the
13
+ anchor (its embedding through ``anchor_proj`` and ``anchor_norm``, at ``p``,
14
+ where the target's own entry for that token lands once it is verified), all
15
+ projected by one GEMM over every layer's weights;
16
+ * evaluates the DiT on ``block_size`` slots at positions ``p + 1 ..`` from noise,
17
+ their cross-attention reading positions ``0 .. p`` straight from the paged KV
18
+ cache with FlashAttention;
19
+ * returns the final hidden states, which the speculator maps to logits through
20
+ the DiT head folded into the target's LM head.
21
+
22
+ One jump (the default) needs no vocabulary-sized tensor: the state at ``t = 0``
23
+ embeds to a constant (its posterior is uniform) plus the noise's embedding,
24
+ drawn in the embedding's eigenbasis (fold.py). Everything that depends only on
25
+ the jump times (every AdaLN modulation) is computed once at load. More jumps
26
+ run the reference algorithm exactly, with a vocabulary-sized state.
27
+
28
+ Requirements: the V2 model runner, tensor parallel size 1, the FlashAttention
29
+ backend and a bf16 KV cache (checked at load).
30
+
31
+ The draft noise is keyed by each request's seed and the slot's position, so a
32
+ seeded request drafts the same blocks on every run. ``FLASHMAP_NUM_STEPS``
33
+ overrides the checkpoint's number of jumps.
34
+ """
35
+
36
+ import os
37
+ from collections.abc import Iterable
38
+
39
+ import torch
40
+ import torch.nn.functional as F
41
+ from torch import nn
42
+ from vllm import _custom_ops as ops
43
+ from vllm.config import VllmConfig
44
+ from vllm.distributed import get_tensor_model_parallel_world_size
45
+ from vllm.forward_context import get_forward_context
46
+ from vllm.logger import init_logger
47
+ from vllm.model_executor.layers.attention import Attention
48
+ from vllm.platforms import current_platform
49
+ from vllm.v1.attention.backends.fa_utils import flash_attn_varlen_func
50
+
51
+ from flashmap.vllm.fold import (
52
+ fold_head,
53
+ fold_noise,
54
+ quantize_fp8_per_channel,
55
+ timestep_embedding,
56
+ )
57
+ from flashmap.vllm.kernels import (
58
+ block_self_attention,
59
+ rms_norm,
60
+ rms_norm_rope,
61
+ rope_table,
62
+ seeded_normal,
63
+ write_context_kv,
64
+ )
65
+
66
+ try:
67
+ from vllm.vllm_flash_attn import get_scheduler_metadata
68
+ except ImportError: # no FlashAttention 3 build
69
+ get_scheduler_metadata = None
70
+
71
+ logger = init_logger(__name__)
72
+
73
+ __all__ = ["FlashMapDraftModel", "FlashMapModel"]
74
+
75
+
76
+ @torch.library.custom_op("flashmap_vllm::paged_cross_attention", mutates_args=())
77
+ def paged_cross_attention(
78
+ query: torch.Tensor,
79
+ kv_cache: torch.Tensor,
80
+ cu_seqlens_q: torch.Tensor,
81
+ seqused_k: torch.Tensor,
82
+ block_table: torch.Tensor,
83
+ scheduler_metadata: torch.Tensor | None,
84
+ descales: torch.Tensor | None,
85
+ max_seqlen_q: int,
86
+ max_seqlen_k: int,
87
+ softmax_scale: float,
88
+ fa_version: int,
89
+ ) -> torch.Tensor:
90
+ """Non-causal FlashAttention of ``query (tokens, heads, head_dim)`` onto the
91
+ first ``seqused_k`` cached positions of each request. An fp8 cache takes an
92
+ fp8 ``query`` and ``descales (3, requests, heads)`` for the query, keys and
93
+ values."""
94
+ head_dim = query.shape[-1]
95
+ key_cache, value_cache = kv_cache.transpose(1, 2).split(head_dim, dim=-1)
96
+ q_descale = k_descale = v_descale = None
97
+ if descales is not None:
98
+ key_cache = key_cache.view(query.dtype)
99
+ value_cache = value_cache.view(query.dtype)
100
+ q_descale, k_descale, v_descale = descales.unbind(0)
101
+ return flash_attn_varlen_func(
102
+ q=query,
103
+ k=key_cache,
104
+ v=value_cache,
105
+ cu_seqlens_q=cu_seqlens_q,
106
+ max_seqlen_q=max_seqlen_q,
107
+ seqused_k=seqused_k,
108
+ max_seqlen_k=max_seqlen_k,
109
+ softmax_scale=softmax_scale,
110
+ causal=False,
111
+ block_table=block_table,
112
+ scheduler_metadata=scheduler_metadata,
113
+ q_descale=q_descale,
114
+ k_descale=k_descale,
115
+ v_descale=v_descale,
116
+ fa_version=fa_version,
117
+ )
118
+
119
+
120
+ @paged_cross_attention.register_fake
121
+ def _(query, kv_cache, cu_seqlens_q, seqused_k, block_table, *args):
122
+ return torch.empty(query.shape, dtype=torch.bfloat16, device=query.device)
123
+
124
+
125
+ class RMSNorm(nn.Module):
126
+ """FlashMap's RMSNorm: ``x * bf16(weight * rsqrt(mean(x^2) + eps))``."""
127
+
128
+ def __init__(self, dim: int, eps: float):
129
+ super().__init__()
130
+ self.eps = eps
131
+ self.weight = nn.Parameter(torch.ones(dim), requires_grad=False)
132
+
133
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
134
+ return rms_norm(x, self.weight, self.eps)
135
+
136
+
137
+ class TimestepEmbedding(nn.Module):
138
+ def __init__(self, dim: int, frequency_dim: int | None = None):
139
+ super().__init__()
140
+ self.frequency_dim = frequency_dim or dim
141
+ self.linear_1 = nn.Linear(self.frequency_dim, dim)
142
+ self.linear_2 = nn.Linear(dim, dim)
143
+
144
+ def forward(self, t: torch.Tensor) -> torch.Tensor:
145
+ emb = timestep_embedding(t, self.frequency_dim)
146
+ emb = emb.to(self.linear_1.weight.dtype)
147
+ return self.linear_2(F.silu(self.linear_1(emb)))
148
+
149
+
150
+ class AdaLN(nn.Module):
151
+ """AdaLN-zero's condition projection; returns ``(scale, shift, gate)``."""
152
+
153
+ def __init__(self, cond_dim: int, dim: int):
154
+ super().__init__()
155
+ self.linear = nn.Linear(cond_dim, 3 * dim)
156
+
157
+ def forward(self, cond: torch.Tensor) -> tuple[torch.Tensor, ...]:
158
+ return tuple(self.linear(F.silu(cond)).chunk(3, dim=-1))
159
+
160
+
161
+ class FlashMapAttention(nn.Module):
162
+ def __init__(self, dim: int, num_heads: int, head_dim: int, eps: float):
163
+ super().__init__()
164
+ self.num_heads = num_heads
165
+ self.head_dim = head_dim
166
+ self.q_proj = nn.Linear(dim, num_heads * head_dim, bias=False)
167
+ self.k_proj = nn.Linear(dim, num_heads * head_dim, bias=False)
168
+ self.v_proj = nn.Linear(dim, num_heads * head_dim, bias=False)
169
+ self.o_proj = nn.Linear(num_heads * head_dim, dim, bias=False)
170
+ self.q_norm = RMSNorm(head_dim, eps)
171
+ self.k_norm = RMSNorm(head_dim, eps)
172
+
173
+
174
+ class FlashMapMLP(nn.Module):
175
+ def __init__(self, dim: int, hidden: int):
176
+ super().__init__()
177
+ self.fc1 = nn.Linear(dim, hidden)
178
+ self.fc2 = nn.Linear(hidden, dim)
179
+
180
+
181
+ class FlashMapLayer(nn.Module):
182
+ def __init__(self, config, layer_prefix: str, vllm_config: VllmConfig):
183
+ super().__init__()
184
+ dim, cond_dim = config.hidden_size, config.timestep_embed_dim
185
+ heads = config.num_attention_heads
186
+ head_dim = getattr(config, "head_dim", None) or dim // heads
187
+ self.self_attn_adaln = AdaLN(cond_dim, dim)
188
+ self.self_attn = FlashMapAttention(dim, heads, head_dim, config.qk_norm_eps)
189
+ self.cross_attn_adaln = AdaLN(cond_dim, dim)
190
+ self.cross_attn = FlashMapAttention(dim, heads, head_dim, config.qk_norm_eps)
191
+ self.mlp_adaln = AdaLN(cond_dim, dim)
192
+ self.mlp = FlashMapMLP(dim, config.intermediate_size)
193
+ # The cross-attention context lives in vLLM's paged KV cache; this layer
194
+ # only owns the cache (the draft step reads it with its own kernels).
195
+ self.cross_attn.attn = Attention(
196
+ heads,
197
+ head_dim,
198
+ head_dim**-0.5,
199
+ num_kv_heads=heads,
200
+ cache_config=vllm_config.cache_config,
201
+ prefix=f"{layer_prefix}.cross_attn.attn",
202
+ )
203
+
204
+
205
+ class FlashMapModel(nn.Module):
206
+ """The FlashMap weights under their canonical names. ``embed_tokens`` is the
207
+ target's, attached at load (``load_dflash_model``)."""
208
+
209
+ def __init__(self, config, vllm_config: VllmConfig, prefix: str, start: int):
210
+ super().__init__()
211
+ dim, target_hidden = config.hidden_size, config.target_hidden_size
212
+ eps = config.rms_norm_eps
213
+ num_aux = len(config.eagle_aux_hidden_state_layer_ids)
214
+ self.fc = nn.Linear(num_aux * target_hidden, dim, bias=False)
215
+ self.hidden_norm = RMSNorm(dim, eps)
216
+ self.anchor_proj = nn.Linear(target_hidden, dim)
217
+ self.anchor_norm = RMSNorm(dim, eps)
218
+ self.state_norm = RMSNorm(target_hidden, eps)
219
+ self.noise_norm = RMSNorm(target_hidden, eps)
220
+ self.noise_gate = nn.Parameter(torch.ones(1), requires_grad=False)
221
+ self.x_embed = nn.Linear(target_hidden, dim)
222
+ frequency_dim = getattr(config, "timestep_frequency_dim", None)
223
+ self.time_embed = TimestepEmbedding(config.timestep_embed_dim, frequency_dim)
224
+ self.jump_embed = TimestepEmbedding(config.timestep_embed_dim, frequency_dim)
225
+ self.layers = nn.ModuleList(
226
+ [
227
+ FlashMapLayer(config, f"{prefix}.layers.{start + i}", vllm_config)
228
+ for i in range(config.num_hidden_layers)
229
+ ]
230
+ )
231
+ self.final_adaln = AdaLN(config.timestep_embed_dim, dim)
232
+ self.head = nn.Linear(dim, target_hidden)
233
+
234
+
235
+ class FlashMapDraftModel(nn.Module):
236
+ # Scales the starting noise; tests set 0 to compare deterministic drafts.
237
+ noise_scale = 1.0
238
+ # torch.compile the draft step's prologue and DiT (fuses elementwise chains).
239
+ compile = True
240
+
241
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
242
+ super().__init__()
243
+ if get_tensor_model_parallel_world_size() != 1:
244
+ raise ValueError(
245
+ "FlashMap drafting supports tensor parallel size 1 only (its "
246
+ "load-time folds read the whole target embedding and LM head)."
247
+ )
248
+ if not vllm_config.use_v2_model_runner:
249
+ raise ValueError(
250
+ "FlashMap drafting needs vLLM's V2 model runner; unset "
251
+ "VLLM_USE_V2_MODEL_RUNNER=0."
252
+ )
253
+ speculative_config = vllm_config.speculative_config
254
+ assert speculative_config is not None
255
+ self.config = config = speculative_config.draft_model_config.hf_config
256
+ self.dtype = vllm_config.model_config.dtype
257
+ if self.dtype != torch.bfloat16:
258
+ raise ValueError(f"FlashMap drafting runs in bfloat16, not {self.dtype}")
259
+ self.max_model_len = vllm_config.model_config.max_model_len
260
+ self.block_size = int(config.block_size)
261
+ # FLASHMAP_NUM_STEPS overrides the checkpoint's number of jumps.
262
+ self.num_steps = int(os.environ.get("FLASHMAP_NUM_STEPS", config.num_steps))
263
+ self.time_grid = [float(t) for t in config.time_grids[self.num_steps - 1]]
264
+ start = vllm_config.model_config.get_total_num_hidden_layers()
265
+ self.model = FlashMapModel(
266
+ config, vllm_config, f"{prefix}.model" if prefix else "model", start
267
+ )
268
+ # Draft vocabulary: a restricted head drafts over d2t's tokens only.
269
+ self.vocab_size = int(config.vocab_size)
270
+ draft_vocab_size = int(
271
+ getattr(config, "draft_vocab_size", None) or self.vocab_size
272
+ )
273
+ self.d2t: nn.Parameter | None = None
274
+ if draft_vocab_size != self.vocab_size:
275
+ self.d2t = nn.Parameter(
276
+ torch.zeros(draft_vocab_size, dtype=torch.long), requires_grad=False
277
+ )
278
+ self._pending_context: tuple[torch.Tensor, ...] | None = None
279
+ self._cache_args = None
280
+ self._noise_keys: tuple[torch.Tensor, torch.Tensor] | None = None
281
+ # FLASHMAP_FP8=1: e4m3 copies of the large GEMM weights (per output
282
+ # channel), activations quantized per token (sm89+).
283
+ self.fp8_weights = os.environ.get("FLASHMAP_FP8", "0") == "1"
284
+ self.fp8_linears: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
285
+ self.kv_fp8 = False
286
+ self.kv_scales: torch.Tensor | None = None
287
+
288
+ @property
289
+ def layers(self) -> nn.ModuleList:
290
+ return self.model.layers
291
+
292
+ # ------------------------------------------------------------------ loading
293
+
294
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
295
+ params = dict(self.named_parameters())
296
+ loaded = set()
297
+ for checkpoint_name, tensor in weights:
298
+ if checkpoint_name in ("embed_tokens.weight", "lm_head.weight", "t2d"):
299
+ continue # the target's own, shared at load
300
+ if checkpoint_name in ("ids", "counts"):
301
+ # A release folder's draft_vocab.safetensors: the target's
302
+ # tokens, most frequent first; the draft head keeps the first
303
+ # draft_vocab_size of them.
304
+ if checkpoint_name == "ids" and self.d2t is not None:
305
+ size = self.d2t.shape[0]
306
+ hot = tensor[:size].to(torch.int64).sort().values
307
+ self.d2t.data.copy_(hot - torch.arange(size, device=hot.device))
308
+ loaded.add("d2t")
309
+ continue
310
+ name = (
311
+ checkpoint_name
312
+ if checkpoint_name == "d2t"
313
+ else "model." + (checkpoint_name)
314
+ )
315
+ param = params.get(name)
316
+ if param is None:
317
+ raise ValueError(f"unexpected FlashMap weight {checkpoint_name!r}")
318
+ if param.shape != tensor.shape:
319
+ raise ValueError(
320
+ f"FlashMap weight {checkpoint_name!r} has shape "
321
+ f"{tuple(tensor.shape)}, expected {tuple(param.shape)}"
322
+ )
323
+ param.data.copy_(tensor)
324
+ loaded.add(name)
325
+ # The attention layers' own (KV-cache scale) parameters are not weights.
326
+ missing = {name for name in params if ".attn." not in name} - loaded
327
+ if missing:
328
+ raise ValueError(f"FlashMap checkpoint is missing {sorted(missing)}")
329
+ return set(params)
330
+
331
+ @torch.no_grad()
332
+ def prepare(self) -> None: # noqa: C901
333
+ """Precompute what the draft step needs, once the target's embedding
334
+ (``model.embed_tokens``) and LM head (``lm_head``) are attached."""
335
+ model = self.model
336
+ embed_tokens = model.embed_tokens.weight[: self.vocab_size]
337
+ lm_head = self.lm_head.weight[: self.vocab_size]
338
+ device, dtype = model.fc.weight.device, self.dtype
339
+ attn = self.layers[0].cross_attn
340
+ self.num_heads, self.head_dim = attn.num_heads, attn.head_dim
341
+ self.dim = self.num_heads * self.head_dim
342
+ self.softmax_scale = self.head_dim**-0.5
343
+ self._check_attention()
344
+
345
+ # AdaLN modulations for each jump (s, t) of the grid.
346
+ self.modulations, self.final_modulations = [], []
347
+ grid = torch.tensor(self.time_grid, device=device, dtype=torch.float32)
348
+ scale = float(self.config.time_scale)
349
+ for step in range(self.num_steps):
350
+ cond = model.time_embed(grid[step : step + 1] * scale) + model.jump_embed(
351
+ grid[step + 1 : step + 2] * scale
352
+ )
353
+ per_layer = []
354
+ for layer in self.layers:
355
+ for adaln in (
356
+ layer.self_attn_adaln,
357
+ layer.cross_attn_adaln,
358
+ layer.mlp_adaln,
359
+ ):
360
+ mod_scale, shift, gate = adaln(cond)
361
+ per_layer += [(1 + mod_scale), shift, gate]
362
+ # (layers, 9, dim): (1 + scale, shift, gate) for self, cross, mlp.
363
+ self.modulations.append(
364
+ torch.cat(per_layer).view(len(self.layers), 9, -1).contiguous()
365
+ )
366
+ f_scale, f_shift, _ = model.final_adaln(cond)
367
+ self.final_modulations.append(
368
+ torch.cat([1 + f_scale, f_shift]).contiguous()
369
+ )
370
+ gate = model.noise_gate.float()
371
+ if self.config.noise_gate == "jump":
372
+ self.noise_gates = [
373
+ float(gate * (grid[s + 1] - grid[s])) for s in range(self.num_steps)
374
+ ]
375
+ else:
376
+ self.noise_gates = [float(gate)] * self.num_steps
377
+
378
+ # embed_state at t = 0: the uniform posterior's embedding (bf16, as the
379
+ # reference forms softmax(0) @ E) through state_norm.
380
+ vocab = embed_tokens.shape[0]
381
+ uniform = torch.full((1, vocab), 1.0 / vocab, device=device, dtype=dtype)
382
+ self.first_hidden = model.x_embed(model.state_norm(uniform @ embed_tokens))[0]
383
+ self.noise_embed, self.noise_eigvals = fold_noise(
384
+ embed_tokens, model.noise_norm.weight, model.x_embed.weight
385
+ )
386
+ self.embed_weight = embed_tokens
387
+
388
+ # Context keys/values of every layer in one GEMM; self-attention
389
+ # queries, keys and values in one GEMM per layer.
390
+ self.context_kv_weight = torch.cat(
391
+ [
392
+ torch.cat(
393
+ [layer.cross_attn.k_proj.weight, layer.cross_attn.v_proj.weight]
394
+ )
395
+ for layer in self.layers
396
+ ]
397
+ )
398
+ self.context_k_norm = torch.stack(
399
+ [layer.cross_attn.k_norm.weight.float() for layer in self.layers]
400
+ ).contiguous()
401
+ self.self_qkv_weights = [
402
+ torch.cat(
403
+ [
404
+ layer.self_attn.q_proj.weight,
405
+ layer.self_attn.k_proj.weight,
406
+ layer.self_attn.v_proj.weight,
407
+ ]
408
+ )
409
+ for layer in self.layers
410
+ ]
411
+ for layer in self.layers: # drop the now-duplicated projections
412
+ for proj in (
413
+ layer.cross_attn.k_proj,
414
+ layer.cross_attn.v_proj,
415
+ layer.self_attn.q_proj,
416
+ layer.self_attn.k_proj,
417
+ layer.self_attn.v_proj,
418
+ ):
419
+ proj.weight = nn.Parameter(
420
+ proj.weight.new_empty(0), requires_grad=False
421
+ )
422
+ rope = getattr(self.config, "rope_parameters", None) or {}
423
+ self.cos_sin = rope_table(
424
+ self.max_model_len + self.block_size + 1,
425
+ self.head_dim,
426
+ float(rope.get("rope_theta", getattr(self.config, "rope_theta", 1e4))),
427
+ device,
428
+ )
429
+
430
+ # The DiT head folded into the LM head (no nonlinearity between them).
431
+ self.hot_ids = None
432
+ if self.d2t is not None:
433
+ self.hot_ids = torch.arange(self.d2t.shape[0], device=device) + self.d2t
434
+ head = model.head
435
+ self.head_weight, self.head_bias = fold_head(
436
+ lm_head, head.weight, head.bias, self.hot_ids
437
+ )
438
+ self.jump_head = None
439
+ if self.num_steps > 1:
440
+ # Between jumps the state mixes in a full-vocabulary distribution.
441
+ self.jump_head = (
442
+ (self.head_weight, self.head_bias)
443
+ if self.hot_ids is None
444
+ else fold_head(lm_head, head.weight, head.bias)
445
+ )
446
+ self._prologue_fn = self._prologue
447
+ self._one_jump_fn = self._one_jump
448
+ self._dit_fn = self._dit
449
+ if self.compile:
450
+ self._prologue_fn = torch.compile(
451
+ self._prologue, dynamic=True, fullgraph=True
452
+ )
453
+ self._one_jump_fn = torch.compile(
454
+ self._one_jump, dynamic=True, fullgraph=True
455
+ )
456
+ self._dit_fn = torch.compile(self._dit, dynamic=True, fullgraph=True)
457
+ if self.fp8_weights:
458
+ self._quantize_weights()
459
+ torch.cuda.empty_cache()
460
+ logger.info(
461
+ "FlashMap draft: %d jump(s) on grid %s, draft vocabulary %d",
462
+ self.num_steps,
463
+ self.time_grid,
464
+ self.head_weight.shape[0],
465
+ )
466
+
467
+ def _quantize_weights(self) -> None:
468
+ """e4m3 copies (per output channel) of the draft step's large GEMM
469
+ weights, for ``_lin``."""
470
+ if not current_platform.has_device_capability(89):
471
+ raise ValueError("FLASHMAP_FP8=1 needs fp8 GEMMs (Ada or Hopper GPUs)")
472
+ weights = {
473
+ "fc": self.model.fc.weight,
474
+ "context_kv": self.context_kv_weight,
475
+ "noise": self.noise_embed,
476
+ "head": self.head_weight,
477
+ }
478
+ for i, layer in enumerate(self.layers):
479
+ weights[f"self_qkv.{i}"] = self.self_qkv_weights[i]
480
+ weights[f"self_o.{i}"] = layer.self_attn.o_proj.weight
481
+ weights[f"cross_q.{i}"] = layer.cross_attn.q_proj.weight
482
+ weights[f"cross_o.{i}"] = layer.cross_attn.o_proj.weight
483
+ weights[f"fc1.{i}"] = layer.mlp.fc1.weight
484
+ weights[f"fc2.{i}"] = layer.mlp.fc2.weight
485
+ fp8 = current_platform.fp8_dtype()
486
+ self.fp8_linears = {
487
+ name: quantize_fp8_per_channel(weight, fp8)
488
+ for name, weight in weights.items()
489
+ }
490
+
491
+ def _check_attention(self) -> None:
492
+ for layer in self.layers:
493
+ attn = layer.cross_attn.attn
494
+ version = getattr(attn.impl, "vllm_flash_attn_version", None)
495
+ if version is None:
496
+ raise ValueError(
497
+ "FlashMap drafting needs the FlashAttention backend, got "
498
+ f"{type(attn.impl).__name__}"
499
+ )
500
+ kv_dtype = str(getattr(attn, "kv_cache_dtype", "auto"))
501
+ if kv_dtype in ("fp8", "fp8_e4m3"):
502
+ if version < 3: # noqa: PLR2004
503
+ raise ValueError(
504
+ "An fp8 FlashMap draft KV cache needs FlashAttention 3 "
505
+ "(Hopper)."
506
+ )
507
+ self.kv_fp8 = True
508
+ elif kv_dtype not in ("auto", "bfloat16"):
509
+ raise ValueError(
510
+ "FlashMap drafting supports a bf16 or fp8_e4m3 draft KV cache, "
511
+ f"got kv_cache_dtype={kv_dtype!r}"
512
+ )
513
+ self.fa_version = version
514
+ if self.kv_fp8:
515
+ # Per-tensor scales of vLLM's fp8 KV caches (1 unless calibrated).
516
+ self.kv_scales = torch.stack(
517
+ [
518
+ torch.stack(
519
+ [
520
+ layer.cross_attn.attn._k_scale.float().reshape(()), # noqa: SLF001
521
+ layer.cross_attn.attn._v_scale.float().reshape(()), # noqa: SLF001
522
+ ]
523
+ )
524
+ for layer in self.layers
525
+ ]
526
+ ).to(self.model.fc.weight.device)
527
+ self.q_scales = [
528
+ float(layer.cross_attn.attn._q_scale.float()) # noqa: SLF001
529
+ for layer in self.layers
530
+ ]
531
+
532
+ def set_noise_keys(self, seeds: torch.Tensor, rows: torch.Tensor) -> None:
533
+ """Key each query row's noise by ``seeds[rows[i]]`` and its position."""
534
+ self._noise_keys = (seeds, rows)
535
+
536
+ # ------------------------------------------------------------------ context
537
+
538
+ def combine_hidden_states(self, hidden_states: torch.Tensor) -> torch.Tensor:
539
+ return self._lin(hidden_states, "fc", self.model.fc.weight)
540
+
541
+ def _lin(
542
+ self,
543
+ x: torch.Tensor,
544
+ name: str,
545
+ weight: torch.Tensor,
546
+ bias: torch.Tensor | None = None,
547
+ ) -> torch.Tensor:
548
+ """``F.linear``, or vLLM's fp8 scaled mm for a weight quantized at load
549
+ (per output channel, activations per token)."""
550
+ quantized = self.fp8_linears.get(name)
551
+ if quantized is None:
552
+ return F.linear(x, weight, bias)
553
+ w_q, w_scale = quantized
554
+ rows = x.reshape(-1, x.shape[-1]).contiguous()
555
+ x_q, x_scale = ops.scaled_fp8_quant(rows, use_per_token_if_dynamic=True)
556
+ out = ops.cutlass_scaled_mm(
557
+ x_q, w_q, scale_a=x_scale, scale_b=w_scale, out_dtype=x.dtype, bias=bias
558
+ )
559
+ return out.view(*x.shape[:-1], w_q.shape[1])
560
+
561
+ def cache_layout(self):
562
+ """Data pointers and strides of every layer's KV cache, or None before
563
+ the caches are allocated."""
564
+ caches = [layer.cross_attn.attn.kv_cache for layer in self.layers]
565
+ if not all(isinstance(c, torch.Tensor) and c.numel() for c in caches):
566
+ return None
567
+ ptrs = [c.data_ptr() for c in caches]
568
+ if self._cache_args is None or self._cache_args[0] != ptrs:
569
+ expected = (
570
+ (torch.uint8, torch.float8_e4m3fn) if self.kv_fp8 else (torch.bfloat16,)
571
+ )
572
+ if caches[0].dtype not in expected:
573
+ raise ValueError(
574
+ f"unexpected FlashMap draft KV cache dtype {caches[0].dtype}"
575
+ )
576
+ key, value = caches[0].transpose(1, 2).split(self.head_dim, dim=-1)
577
+ assert key.stride(-1) == 1
578
+ assert all(c.stride() == caches[0].stride() for c in caches)
579
+ strides = (
580
+ key.stride(0),
581
+ key.stride(2),
582
+ key.stride(1),
583
+ (value.data_ptr() - key.data_ptr()) // key.element_size(),
584
+ )
585
+ ptr_tensor = torch.tensor(ptrs, dtype=torch.int64, device=caches[0].device)
586
+ self._cache_args = (ptrs, ptr_tensor, strides, key.shape[1])
587
+ return self._cache_args
588
+
589
+ def _write(self, kv: torch.Tensor, positions, slots) -> None:
590
+ layout = self.cache_layout()
591
+ if layout is None:
592
+ return
593
+ _, ptrs, strides, block_size = layout
594
+ write_context_kv(
595
+ kv.view(-1, len(self.layers), 2, self.num_heads, self.head_dim),
596
+ positions,
597
+ slots,
598
+ self.context_k_norm,
599
+ self.cos_sin,
600
+ ptrs,
601
+ strides,
602
+ block_size,
603
+ self.layers[0].cross_attn.k_norm.eps,
604
+ self.kv_scales,
605
+ )
606
+
607
+ def precompute_and_store_context_kv(
608
+ self,
609
+ context_states: torch.Tensor,
610
+ context_positions: torch.Tensor,
611
+ context_slot_mapping: torch.Tensor | None = None,
612
+ ) -> None:
613
+ context = self.model.hidden_norm(context_states)
614
+ kv = self._lin(context, "context_kv", self.context_kv_weight)
615
+ if context_slot_mapping is not None:
616
+ self._write(kv, context_positions, context_slot_mapping)
617
+
618
+ def defer_context_kv(
619
+ self,
620
+ context_states: torch.Tensor,
621
+ context_positions: torch.Tensor,
622
+ context_slot_mapping: torch.Tensor,
623
+ ) -> None:
624
+ """``precompute_and_store_context_kv``, done by the next ``forward``
625
+ together with its anchors (one GEMM over the context K/V weight)."""
626
+ self._pending_context = (
627
+ context_states,
628
+ context_positions,
629
+ context_slot_mapping,
630
+ )
631
+
632
+ def _prologue(
633
+ self,
634
+ input_ids: torch.Tensor,
635
+ positions: torch.Tensor,
636
+ seq_lens: torch.Tensor,
637
+ slot_mapping: torch.Tensor,
638
+ context: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None,
639
+ ):
640
+ """Keys/values, positions and slots to write (the pending context, then
641
+ each request's anchor) and each request's context length ``p + 1``."""
642
+ model = self.model
643
+ blocks = input_ids.shape[0] // self.block_size
644
+ anchors = input_ids.view(blocks, self.block_size)[:, 0]
645
+ anchor_positions = positions.view(blocks, self.block_size)[:, 0]
646
+ slots = slot_mapping.view(blocks, self.block_size)[:, 0]
647
+ rows = model.anchor_norm(
648
+ model.anchor_proj(F.embedding(anchors, self.embed_weight))
649
+ )
650
+ write_positions = anchor_positions
651
+ if context is not None:
652
+ states, context_positions, context_slots = context
653
+ rows = torch.cat([model.hidden_norm(states), rows])
654
+ write_positions = torch.cat([context_positions, anchor_positions])
655
+ slots = torch.cat([context_slots, slots])
656
+ kv = self._lin(rows, "context_kv", self.context_kv_weight)
657
+ # The context is positions 0..p. (The DFlash metadata's seq_lens count
658
+ # the block's other rows too, clamped to the model length.)
659
+ seqused_k = torch.where(
660
+ seq_lens > 0, anchor_positions.to(seq_lens.dtype) + 1, 0
661
+ )
662
+ return kv, write_positions, slots, seqused_k
663
+
664
+ # ------------------------------------------------------------------ drafting
665
+
666
+ def _dit(
667
+ self,
668
+ hidden: torch.Tensor,
669
+ positions: torch.Tensor,
670
+ modulations: torch.Tensor,
671
+ final_modulations: torch.Tensor,
672
+ kv_caches: list[torch.Tensor],
673
+ attention: tuple | None,
674
+ ) -> torch.Tensor:
675
+ """The DiT on ``hidden (tokens, dim)`` for query rows at ``positions``
676
+ (slots at ``positions + 1``) with one jump's modulations; returns the
677
+ final AdaLN output."""
678
+ tokens = hidden.shape[0]
679
+ blocks = tokens // self.block_size
680
+ heads, head_dim, dim = self.num_heads, self.head_dim, self.dim
681
+ eps = self.config.adaln_eps
682
+ slot_positions = positions + 1
683
+ for layer_id, layer in enumerate(self.layers):
684
+ mods = modulations[layer_id]
685
+ self_attn, cross_attn = layer.self_attn, layer.cross_attn
686
+ normed = F.layer_norm(hidden, (dim,), eps=eps) * mods[0] + mods[1]
687
+ qkv = self._lin(
688
+ normed, f"self_qkv.{layer_id}", self.self_qkv_weights[layer_id]
689
+ )
690
+ attn = block_self_attention(
691
+ qkv.view(blocks, self.block_size, 3, heads, head_dim),
692
+ self_attn.q_norm.weight,
693
+ self_attn.k_norm.weight,
694
+ self.cos_sin,
695
+ slot_positions,
696
+ self_attn.q_norm.eps,
697
+ )
698
+ hidden = hidden + mods[2] * self._lin(
699
+ attn, f"self_o.{layer_id}", self_attn.o_proj.weight
700
+ )
701
+
702
+ normed = F.layer_norm(hidden, (dim,), eps=eps) * mods[3] + mods[4]
703
+ query = rms_norm_rope(
704
+ self._lin(normed, f"cross_q.{layer_id}", cross_attn.q_proj.weight).view(
705
+ tokens, heads, head_dim
706
+ ),
707
+ cross_attn.q_norm.weight,
708
+ self.cos_sin,
709
+ slot_positions,
710
+ cross_attn.q_norm.eps,
711
+ )
712
+ if attention is None: # profiling run: no KV cache yet
713
+ context = torch.zeros_like(query)
714
+ else:
715
+ cu_seqlens_q, seqused_k, block_table, schedule, queries = attention
716
+ descales = None
717
+ if self.kv_fp8:
718
+ q_scale = self.q_scales[layer_id]
719
+ query = (query.float() / q_scale).to(torch.float8_e4m3fn)
720
+ descales = torch.stack(
721
+ [
722
+ self.kv_scales.new_full((), q_scale),
723
+ self.kv_scales[layer_id, 0],
724
+ self.kv_scales[layer_id, 1],
725
+ ]
726
+ )[:, None, None].expand(3, seqused_k.shape[0], heads)
727
+ context = paged_cross_attention(
728
+ query,
729
+ kv_caches[layer_id],
730
+ cu_seqlens_q,
731
+ seqused_k,
732
+ block_table,
733
+ schedule,
734
+ descales,
735
+ queries,
736
+ self.max_model_len,
737
+ self.softmax_scale,
738
+ self.fa_version,
739
+ )
740
+ hidden = hidden + mods[5] * self._lin(
741
+ context.view(tokens, dim),
742
+ f"cross_o.{layer_id}",
743
+ cross_attn.o_proj.weight,
744
+ )
745
+
746
+ normed = F.layer_norm(hidden, (dim,), eps=eps) * mods[6] + mods[7]
747
+ mlp = layer.mlp
748
+ hidden = hidden + mods[8] * self._lin(
749
+ F.gelu(
750
+ self._lin(normed, f"fc1.{layer_id}", mlp.fc1.weight, mlp.fc1.bias)
751
+ ),
752
+ f"fc2.{layer_id}",
753
+ mlp.fc2.weight,
754
+ mlp.fc2.bias,
755
+ )
756
+ normed = F.layer_norm(hidden, (dim,), eps=eps)
757
+ return normed * final_modulations[0] + final_modulations[1]
758
+
759
+ def _one_jump(
760
+ self,
761
+ z: torch.Tensor,
762
+ positions: torch.Tensor,
763
+ kv_caches: list[torch.Tensor],
764
+ attention: tuple | None,
765
+ ) -> torch.Tensor:
766
+ """One jump from noise ``z (tokens, hidden)``: its embedded state (the
767
+ uniform posterior's constant plus the folded noise read-out), then the
768
+ DiT."""
769
+ rms = torch.rsqrt((z * z) @ self.noise_eigvals + self.model.noise_norm.eps)
770
+ noise = self._lin(z.to(self.dtype), "noise", self.noise_embed)
771
+ hidden = (
772
+ self.first_hidden
773
+ + (self.noise_gates[0] * rms)[:, None].to(self.dtype) * noise
774
+ )
775
+ return self._dit(
776
+ hidden,
777
+ positions,
778
+ self.modulations[0],
779
+ self.final_modulations[0],
780
+ kv_caches,
781
+ attention,
782
+ )
783
+
784
+ def _embed_state(self, state: torch.Tensor, step: int) -> torch.Tensor:
785
+ """``x_embed(embed_state(state, s, t))`` of a vocabulary-sized float32
786
+ ``state`` at jump ``step``, as the reference computes it."""
787
+ model, weight = self.model, self.embed_weight
788
+ start = self.time_grid[step]
789
+ evidence = start / max(1.0 - start, 1e-3) ** 2
790
+ posterior = (state * evidence).softmax(dim=-1)
791
+ both = torch.cat([posterior, state]).to(weight.dtype) @ weight
792
+ posterior_emb, state_emb = both.split(state.shape[0])
793
+ embedded = model.state_norm(posterior_emb) + (
794
+ self.noise_gates[step] * model.noise_norm(state_emb)
795
+ ).to(weight.dtype)
796
+ return model.x_embed(embedded)
797
+
798
+ def _noise(self, positions: torch.Tensor, n_cols: int) -> torch.Tensor:
799
+ tokens = positions.shape[0]
800
+ if self._noise_keys is None:
801
+ noise = torch.randn(
802
+ tokens, n_cols, device=positions.device, dtype=torch.float32
803
+ )
804
+ return noise if self.noise_scale == 1.0 else noise * self.noise_scale
805
+ seeds, rows = self._noise_keys
806
+ return seeded_normal(seeds, rows[:tokens], positions, n_cols, self.noise_scale)
807
+
808
+ def _cross_attention_schedule(self, metadata, seqused_k):
809
+ """FlashAttention 3's tile schedule for the step's cross-attention,
810
+ shared by every layer (it depends only on shapes and lengths)."""
811
+ if self.fa_version != 3 or get_scheduler_metadata is None: # noqa: PLR2004
812
+ return None
813
+ caches = self.layers[0].cross_attn.attn.kv_cache
814
+ return get_scheduler_metadata(
815
+ batch_size=seqused_k.shape[0],
816
+ max_seqlen_q=self.block_size,
817
+ max_seqlen_k=self.max_model_len,
818
+ num_heads_q=self.num_heads,
819
+ num_heads_kv=self.num_heads,
820
+ headdim=self.head_dim,
821
+ cache_seqlens=seqused_k,
822
+ qkv_dtype=current_platform.fp8_dtype() if self.kv_fp8 else self.dtype,
823
+ cu_seqlens_q=metadata.query_start_loc,
824
+ page_size=caches.shape[2],
825
+ causal=False,
826
+ )
827
+
828
+ def forward(
829
+ self,
830
+ input_ids: torch.Tensor,
831
+ positions: torch.Tensor,
832
+ inputs_embeds: torch.Tensor | None = None, # noqa: ARG002
833
+ ) -> torch.Tensor:
834
+ tokens = input_ids.shape[0]
835
+ kv_caches = [layer.cross_attn.attn.kv_cache for layer in self.layers]
836
+ metadata = get_forward_context().attn_metadata
837
+ pending, self._pending_context = self._pending_context, None
838
+ attention = None
839
+ if metadata is not None:
840
+ metadata = metadata[self.layers[0].cross_attn.attn.layer_name]
841
+ kv, write_positions, write_slots, seqused_k = self._prologue_fn(
842
+ input_ids,
843
+ positions,
844
+ metadata.seq_lens,
845
+ metadata.slot_mapping[:tokens],
846
+ pending,
847
+ )
848
+ self._write(kv, write_positions, write_slots)
849
+ attention = (
850
+ metadata.query_start_loc,
851
+ seqused_k,
852
+ metadata.block_table,
853
+ self._cross_attention_schedule(metadata, seqused_k),
854
+ self.block_size,
855
+ )
856
+
857
+ if self.num_steps == 1:
858
+ z = self._noise(positions, self.noise_embed.shape[1])
859
+ return self._one_jump_fn(z, positions, kv_caches, attention)
860
+
861
+ # Several jumps (Algorithm 8): x <- X_{t_n, t_n+1}(x) between jumps.
862
+ state = self._noise(positions, self.vocab_size)
863
+ for step in range(self.num_steps):
864
+ hidden = self._dit_fn(
865
+ self._embed_state(state, step),
866
+ positions,
867
+ self.modulations[step],
868
+ self.final_modulations[step],
869
+ kv_caches,
870
+ attention,
871
+ )
872
+ if step + 1 < self.num_steps:
873
+ weight, bias = self.jump_head
874
+ delta = F.linear(hidden, weight, bias).float().softmax(dim=-1)
875
+ start, end = self.time_grid[step], self.time_grid[step + 1]
876
+ state = ((1.0 - end) * state + (end - start) * delta) / (1.0 - start)
877
+ return hidden
878
+
879
+ def compute_draft_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
880
+ """Logits over the draft vocabulary (``hot_ids`` when restricted)."""
881
+ return self._lin(hidden_states, "head", self.head_weight, self.head_bias)