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.
@@ -0,0 +1,1076 @@
1
+ # coding=utf-8
2
+ """FlashMap as an sglang draft model, served by sglang's DSPARK worker.
3
+
4
+ A FlashMap release folder is served as it is (with ``flashmap[sglang]``
5
+ installed, its sglang plugin registers this model):
6
+
7
+ python -m sglang.launch_server --model-path Qwen/Qwen3-8B \\
8
+ --speculative-algorithm DSPARK \\
9
+ --speculative-draft-model-path j-shen/FlashMap-Qwen3-8B
10
+
11
+ DSPARK drives a block draft through a small protocol, implemented here:
12
+
13
+ * ``write_target_hidden_kv``: DSPARK hands over the target's captured hidden
14
+ states for every committed position. FlashMap's cross-attention context (the
15
+ keys and values each DiT layer projects from them) goes into the draft KV
16
+ pool, which sglang sizes from the config to the DiT's layers, heads and
17
+ head_dim. All
18
+ layers are projected in one matmul and written, k-normed and rotated, by
19
+ sglang's fused DSpark kernel, which also skips the uncommitted verify
20
+ columns, so the write never synchronizes with the host.
21
+ * ``forward``: one block of ``block_size`` drafts after each request's anchor
22
+ (the last verified token), for the whole batch in one pass that DSPARK
23
+ captures in a CUDA graph. It is compiled with torch.compile, and
24
+ - the anchor's own context entry is written into its verify slot, so the
25
+ cross-attention reads every request's paged context directly
26
+ (FlashAttention with sglang's ``req_to_token`` rows as page tables, one
27
+ tile schedule for all layers);
28
+ - self-attention within each block is one Triton kernel
29
+ (:mod:`.kernels`), its q/k/v one matmul;
30
+ - every AdaLN modulation depends only on the jump's times, which are fixed,
31
+ so they are computed once at load;
32
+ - one jump starts from noise at ``t = 0``, where the state's posterior is
33
+ uniform whatever the noise: its embedding is a constant, and the noise
34
+ enters only through ``x @ E ~ N(0, E^T E)``, drawn as ``z @ L^T`` with
35
+ ``L L^T = E^T E`` (Cholesky), so no vocabulary-sized noise is ever formed.
36
+ ``FLASHMAP_FOLDED_NOISE=1`` (opt-in, measured on Qwen3-8B only) draws it from the
37
+ eigendecomposition instead, which folds the noise's RMSNorm and ``x_embed``
38
+ into one GEMM (``_folded_noise``);
39
+ - more jumps (``FLASHMAP_NUM_STEPS``) take their vocabulary-sized inputs from
40
+ a bank of precomputed noise draws (:mod:`.jump_bank`) and read
41
+ only the embedding rows of each jump's top predictions.
42
+ * ``compute_base_logits``: the DiT's output head (dim -> hidden) folded into the
43
+ target's LM head (hidden -> vocab) as one smaller matmul, restricted to the
44
+ draft vocabulary (the target's most frequent tokens, the config's
45
+ ``draft_vocab_size`` of the ranking in ``draft_vocab.safetensors``); drafted
46
+ indices map back to token ids in the draft graph.
47
+
48
+ The draft borrows the target's token embedding and LM head (DSPARK attaches
49
+ them); its Markov head is an identity (:mod:`.sampler`), so DSPARK
50
+ samples and verifies the map's logits unchanged, in one pass per block. The
51
+ DiT's modules and weights are :class:`flashmap.FlashMapDraftModel`'s.
52
+
53
+ Environment knobs: ``FLASHMAP_NUM_STEPS`` (jumps per draft; default the
54
+ config's ``num_steps``), ``FLASHMAP_DRAFT_VOCAB`` (0: the full vocabulary,
55
+ ``<k>``: the ranking's top k), ``FLASHMAP_JUMP_BANK`` /
56
+ ``FLASHMAP_JUMP_TOPK`` (bank draws, 8192; support per jump, 16),
57
+ ``FLASHMAP_COMPILE`` (0 disables torch.compile), ``FLASHMAP_COMPILE_MODE``
58
+ (``max-autotune-no-cudagraphs``; modes that capture their own CUDA graphs
59
+ would nest inside sglang's), and two opt-in variants measured on Qwen3-8B
60
+ only: ``FLASHMAP_FOLDED_NOISE=1`` (above) and ``FLASHMAP_FUSED_SAMPLER=1``
61
+ (:mod:`.sampler`), ``FLASHMAP_FP8=1`` (FP8 weights) and ``FLASHMAP_TREE_N``
62
+ (tree drafting, :mod:`.tree_worker`).
63
+
64
+ Supported: sglang 0.5.20 (``SUPPORTED_SGLANG``), tensor parallelism 1, a bf16
65
+ or e4m3 draft KV pool, an unpadded target vocabulary without muP logit scaling.
66
+ """
67
+
68
+ from __future__ import annotations
69
+
70
+ import logging
71
+ import os
72
+ from typing import Iterable, Optional, Tuple
73
+
74
+ import torch
75
+ import torch.nn.functional as F
76
+
77
+ from . import check_sglang_version
78
+
79
+ check_sglang_version()
80
+
81
+ from sgl_kernel.flash_attn import ( # noqa: E402
82
+ flash_attn_with_kvcache,
83
+ get_scheduler_metadata,
84
+ )
85
+ from sglang.kernels.ops.speculative.dspark.fused_kv_write import ( # noqa: E402
86
+ fused_kv_norm_rope_write,
87
+ )
88
+ from sglang.srt.layers.logits_processor import LogitsProcessorOutput # noqa: E402
89
+ from sglang.srt.model_executor.forward_context import ( # noqa: E402
90
+ get_req_to_token_pool,
91
+ get_token_to_kv_pool,
92
+ )
93
+ from sglang.srt.runtime_context import get_exec, get_model # noqa: E402
94
+ from sglang.srt.speculative.dspark_components import dspark_worker_v2 # noqa: E402
95
+ from torch import nn # noqa: E402
96
+
97
+ from ..modeling_flashmap import FlashMapDraftModel as FlashMapDiT # noqa: E402
98
+ from .jump_bank import JumpNoiseBank, top_predictions # noqa: E402
99
+ from .kernels import ( # noqa: E402
100
+ block_self_attention,
101
+ fp8_linear,
102
+ kv_norm_rope_write_fp8,
103
+ quantize_weight_fp8,
104
+ rms_norm,
105
+ rms_norm_rope,
106
+ )
107
+ from .sampler import FlashMapDraftSampler, IdentityMarkov # noqa: E402
108
+ from .tree import build_tree_abi # noqa: E402
109
+
110
+ logger = logging.getLogger(__name__)
111
+
112
+
113
+ def rope_freqs(positions: torch.Tensor, head_dim: int, theta: float) -> torch.Tensor:
114
+ """The rotary ``(cos, sin)`` table ``(..., head_dim // 2, 2)`` at ``positions``,
115
+ in float32, as the kernels read it (the angles of ``modeling_flashmap.rope_tables``)."""
116
+ inv_freq = 1.0 / theta ** (
117
+ torch.arange(0, head_dim, 2, device=positions.device).float() / head_dim
118
+ )
119
+ angles = positions.float()[..., None] * inv_freq
120
+ return torch.stack([angles.cos(), angles.sin()], dim=-1)
121
+
122
+
123
+ # A custom op, so torch.compile traces the draft step through it in one graph.
124
+ @torch.library.custom_op("flashmap_sglang::paged_cross_attention", mutates_args=())
125
+ def _paged_cross_attention(
126
+ query: torch.Tensor,
127
+ key_buffer: torch.Tensor,
128
+ value_buffer: torch.Tensor,
129
+ cache_seqlens: torch.Tensor,
130
+ page_table: torch.Tensor,
131
+ scheduler_metadata: torch.Tensor,
132
+ ) -> torch.Tensor:
133
+ """``query (bs, K, heads, head_dim)`` onto each request's first
134
+ ``cache_seqlens`` pool rows, listed by ``page_table`` (page size 1)."""
135
+ return flash_attn_with_kvcache(
136
+ q=query,
137
+ k_cache=key_buffer.unsqueeze(1),
138
+ v_cache=value_buffer.unsqueeze(1),
139
+ cache_seqlens=cache_seqlens,
140
+ page_table=page_table,
141
+ causal=False,
142
+ scheduler_metadata=scheduler_metadata,
143
+ )
144
+
145
+
146
+ @_paged_cross_attention.register_fake
147
+ def _(query, key_buffer, value_buffer, cache_seqlens, page_table, scheduler_metadata):
148
+ return torch.empty_like(query)
149
+
150
+
151
+ @torch.library.custom_op(
152
+ "flashmap_sglang::paged_cross_attention_fp8", mutates_args=()
153
+ )
154
+ def _paged_cross_attention_fp8(
155
+ query: torch.Tensor,
156
+ key_buffer: torch.Tensor,
157
+ value_buffer: torch.Tensor,
158
+ cache_seqlens: torch.Tensor,
159
+ page_table: torch.Tensor,
160
+ scheduler_metadata: torch.Tensor,
161
+ ) -> torch.Tensor:
162
+ """``_paged_cross_attention`` onto an e4m3 pool (written with scale 1): the
163
+ bf16 query is quantized per (request, head) and every operand descaled."""
164
+ q_scale = query.float().abs().amax(dim=(1, 3)).clamp_min(1e-12) / 448.0
165
+ query8 = (query.float() / q_scale[:, None, :, None]).to(key_buffer.dtype)
166
+ ones = torch.ones_like(q_scale)
167
+ return flash_attn_with_kvcache(
168
+ q=query8,
169
+ k_cache=key_buffer.unsqueeze(1),
170
+ v_cache=value_buffer.unsqueeze(1),
171
+ cache_seqlens=cache_seqlens,
172
+ page_table=page_table,
173
+ causal=False,
174
+ scheduler_metadata=scheduler_metadata,
175
+ q_descale=q_scale,
176
+ k_descale=ones,
177
+ v_descale=ones,
178
+ ).to(query.dtype)
179
+
180
+
181
+ @_paged_cross_attention_fp8.register_fake
182
+ def _(query, key_buffer, value_buffer, cache_seqlens, page_table, scheduler_metadata):
183
+ return torch.empty_like(query)
184
+
185
+
186
+ def _cross_attention_schedule(query_shape, cache_seqlens, page_table, dtype):
187
+ """FlashAttention's tile schedule for ``_paged_cross_attention``, computed
188
+ once per draft step for all layers (it depends only on shapes and lengths)."""
189
+ bsz, block_size, heads, head_dim = query_shape
190
+ return get_scheduler_metadata(
191
+ batch_size=bsz,
192
+ max_seqlen_q=block_size,
193
+ max_seqlen_k=page_table.shape[1],
194
+ num_heads=heads,
195
+ num_heads_k=heads,
196
+ headdim=head_dim,
197
+ cache_seqlens=cache_seqlens,
198
+ qkv_dtype=dtype,
199
+ page_size=1,
200
+ causal=False,
201
+ )
202
+
203
+
204
+ def _server_setting(read, default):
205
+ """A setting from sglang's runtime context, or ``default`` outside a server
206
+ (offline checks)."""
207
+ try:
208
+ value = read()
209
+ except Exception:
210
+ return default
211
+ return default if value is None else value
212
+
213
+
214
+ class _FoldedHead(nn.Module):
215
+ """The draft's head as DSPARK reads it: the DiT head folded into the
216
+ target's LM head over the draft vocabulary. ``org_vocab_size`` is the full
217
+ vocabulary, which DSPARK's sampling buffers are sized to."""
218
+
219
+ def __init__(self, weight: torch.Tensor, bias: torch.Tensor, vocab_size: int):
220
+ super().__init__()
221
+ self.register_buffer("weight", weight, persistent=False)
222
+ self.register_buffer("bias", bias, persistent=False)
223
+ self.org_vocab_size = int(vocab_size)
224
+
225
+
226
+ class FlashMapDraftModel(FlashMapDiT):
227
+ """:class:`flashmap.FlashMapDraftModel` (its modules and weights, loaded from
228
+ a release folder as they are) with sglang's draft protocol and the fused
229
+ draft step below."""
230
+
231
+ # Read by the DSPARK worker and the hooks below.
232
+ flashmap = True
233
+ uses_own_vocab_modules = False
234
+ supports_pre_gather_target_hidden_projection = False
235
+
236
+ def __init__(self, config, quant_config=None, prefix: str = "") -> None:
237
+ del quant_config, prefix
238
+ if getattr(config, "flow_map", True) is False:
239
+ raise ValueError(
240
+ "this checkpoint is a stage-1 flow (flow_map: false), which drafts "
241
+ "only by integrating over many steps; serve the flow map instead"
242
+ )
243
+ super().__init__(config)
244
+ fm = self.config
245
+ self.block_size = int(fm.block_size)
246
+ self.vocab_size = fm.vocab_size
247
+ self.target_hidden_size = fm.target_hidden_size
248
+ self.dim = fm.hidden_size
249
+ self.num_heads = fm.num_attention_heads
250
+ self.gamma = self.block_size
251
+ self.num_stages = fm.num_hidden_layers
252
+ self.rope_theta = fm.rope_theta
253
+ self.rms_norm_eps = fm.rms_norm_eps
254
+ self.qk_norm_eps = fm.qk_norm_eps
255
+ self.layer_norm_eps = fm.layer_norm_eps
256
+
257
+ # DSPARK's draft protocol.
258
+ self.sample_from_anchor = True
259
+ self.markov_head = IdentityMarkov(vocab_size=self.vocab_size)
260
+ self.confidence_head = None
261
+ self.logits_mup_width_multiplier = None
262
+ self.lm_head = None # the folded head, once the target's is attached
263
+ self.embedding = None # the target's token embedding (not a submodule)
264
+ self.hot_ids = None # draft vocabulary -> token ids (None: all tokens)
265
+ self.folded_head = None
266
+
267
+ self.num_steps = int(os.environ.get("FLASHMAP_NUM_STEPS") or fm.num_steps)
268
+ self.grid = [float(t) for t in fm.jump_grids[str(self.num_steps)]]
269
+ assert self.grid[0] == 0.0 and self.grid[-1] == 1.0, self.grid
270
+ self.jump_bank_size = int(os.environ.get("FLASHMAP_JUMP_BANK", "8192"))
271
+ self.jump_topk = int(os.environ.get("FLASHMAP_JUMP_TOPK", "16"))
272
+ self.jump_bank = None
273
+ size = os.environ.get("FLASHMAP_DRAFT_VOCAB")
274
+ size = int(size if size is not None else getattr(config, "draft_vocab_size", 0))
275
+ self.draft_vocab_size = size if 0 < size < self.vocab_size else 0
276
+ self._vocab_ranking = None # draft_vocab.safetensors' ids
277
+ # Scales the starting noise; checks set 0 for deterministic drafts.
278
+ self.noise_temperature = 1.0
279
+ self.folded_noise = os.environ.get("FLASHMAP_FOLDED_NOISE", "0") == "1"
280
+ # FP8 (e4m3) copies of the large weights, per output channel, with the
281
+ # activations quantized per token on the fly (_quantize_fp8).
282
+ self.fp8 = os.environ.get("FLASHMAP_FP8", "0") == "1"
283
+ self._fp8 = {}
284
+ self._constants = None
285
+ self._fused_write = None # (pool id, kernel arguments)
286
+ # Tree drafting (tree_worker.py): up to FLASHMAP_TREE_N candidate
287
+ # blocks per request from independent noise in one draft pass, merged
288
+ # into a trie cut to the verify width (block_size + 1 with the root); a
289
+ # batch of bs requests drafts clamp(FLASHMAP_TREE_BUDGET // bs, 1, N).
290
+ self.tree_max_n = int(os.environ.get("FLASHMAP_TREE_N", "0"))
291
+ self.tree_budget = int(os.environ.get("FLASHMAP_TREE_BUDGET", "32"))
292
+ self.tree_mode = self.tree_max_n >= 1
293
+ self.tree_width = self.block_size + 1
294
+ self._tree_buffers = None
295
+ self._tree_scratch = None
296
+ self._build_tree = build_tree_abi
297
+ self._kv_fp8 = False # an e4m3 draft KV pool (set with the pool)
298
+ self._draft = self._draft_impl
299
+ self._anchor_kv = self._anchor_kv_impl
300
+ from sglang.srt.distributed import get_tensor_model_parallel_world_size
301
+
302
+ tp_size = _server_setting(get_tensor_model_parallel_world_size, 1)
303
+ if tp_size != 1:
304
+ raise NotImplementedError(
305
+ f"the FlashMap draft runs unsharded: serve with --tp-size 1 (got {tp_size})"
306
+ )
307
+ if os.environ.get("FLASHMAP_COMPILE", "1") == "1":
308
+ mode = os.environ.get("FLASHMAP_COMPILE_MODE") or None
309
+ if mode not in (None, "default", "max-autotune-no-cudagraphs"):
310
+ raise ValueError(
311
+ f"FLASHMAP_COMPILE_MODE={mode!r} would capture CUDA graphs "
312
+ "inside sglang's; use default or max-autotune-no-cudagraphs"
313
+ )
314
+ self._draft = torch.compile(self._draft_impl, dynamic=True, mode=mode)
315
+ self._anchor_kv = torch.compile(self._anchor_kv_impl, dynamic=True)
316
+ # ~50 small index ops per step otherwise, inside the draft graph.
317
+ self._build_tree = torch.compile(build_tree_abi, dynamic=True)
318
+ logger.info(
319
+ "FlashMap draft: %d jump(s) on %s, block_size=%d, draft vocabulary %s",
320
+ self.num_steps,
321
+ self.grid,
322
+ self.block_size,
323
+ self.draft_vocab_size or "full",
324
+ )
325
+
326
+ # --- loading -----------------------------------------------------------
327
+
328
+ def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]) -> None:
329
+ """The folder's weights. sglang reads every ``*.safetensors`` file there,
330
+ so ``draft_vocab.safetensors`` arrives too: its ``ids`` rank every token
331
+ id, most frequent first (``counts`` are their frequencies)."""
332
+ params = {**dict(self.named_parameters()), **dict(self.named_buffers())}
333
+ loaded = set()
334
+ for name, tensor in weights:
335
+ if name in ("ids", "draft_vocab_ids"):
336
+ self._vocab_ranking = tensor
337
+ continue
338
+ if name == "counts":
339
+ continue
340
+ if name not in params:
341
+ raise KeyError(f"unexpected FlashMap weight {name!r}")
342
+ with torch.no_grad():
343
+ params[name].copy_(tensor.to(params[name].dtype))
344
+ loaded.add(name)
345
+ missing = sorted(
346
+ n for n in params if n not in loaded and not n.startswith("markov_head.")
347
+ )
348
+ if missing:
349
+ raise KeyError(f"FlashMap weights missing from the checkpoint: {missing}")
350
+ layers = self.layers
351
+ # Every layer's context keys and values in one matmul (rows [k; v] per
352
+ # layer); each k_proj/v_proj weight becomes a view of its rows.
353
+ self._ctx_kv_weight = torch.cat(
354
+ [
355
+ torch.cat(
356
+ [layer.cross_attn.k_proj.weight, layer.cross_attn.v_proj.weight]
357
+ )
358
+ for layer in layers
359
+ ]
360
+ )
361
+ for i, layer in enumerate(layers):
362
+ rows = self._ctx_kv_weight[2 * self.dim * i : 2 * self.dim * (i + 1)]
363
+ layer.cross_attn.k_proj.weight.data = rows[: self.dim]
364
+ layer.cross_attn.v_proj.weight.data = rows[self.dim :]
365
+ # Likewise each self-attention's queries, keys and values.
366
+ self._self_qkv_weights = []
367
+ for layer in layers:
368
+ attn = layer.self_attn
369
+ qkv = torch.cat(
370
+ [attn.q_proj.weight, attn.k_proj.weight, attn.v_proj.weight]
371
+ )
372
+ attn.q_proj.weight.data = qkv[: self.dim]
373
+ attn.k_proj.weight.data = qkv[self.dim : 2 * self.dim]
374
+ attn.v_proj.weight.data = qkv[2 * self.dim :]
375
+ self._self_qkv_weights.append(qkv)
376
+ # The norms' weights in float32, as the kernels take them.
377
+ self._self_norm_weights = [
378
+ (
379
+ layer.self_attn.q_norm.weight.float(),
380
+ layer.self_attn.k_norm.weight.float(),
381
+ )
382
+ for layer in layers
383
+ ]
384
+ self._cross_q_norm_weights = [
385
+ layer.cross_attn.q_norm.weight.float() for layer in layers
386
+ ]
387
+ self._hidden_norm_weight = self.hidden_norm.weight.float()
388
+
389
+ def attach_shared_modules(self, *, embed_tokens: nn.Module, lm_head: nn.Module):
390
+ """DSPARK hands over the target's embedding and LM head; everything the
391
+ draft step reads from them is precomputed here, before sglang sizes its
392
+ KV pools. A target with tied embeddings (Qwen3-4B) hands over its
393
+ embedding as the LM head; one without a head of its own falls back to
394
+ the embedding too."""
395
+ if self.logits_mup_width_multiplier:
396
+ raise NotImplementedError("FlashMap does not support muP-scaled targets")
397
+ embedding = embed_tokens.weight.detach()
398
+ head = getattr(lm_head, "weight", None)
399
+ head = embedding if head is None else head.detach()
400
+ for name, weight in (("embedding", embedding), ("LM head", head)):
401
+ # Rows past the vocabulary would be tensor-parallel padding.
402
+ if weight.shape != (self.vocab_size, self.target_hidden_size):
403
+ raise ValueError(
404
+ f"the target's {name} is {tuple(weight.shape)}, the draft "
405
+ f"expects ({self.vocab_size}, {self.target_hidden_size}); padded "
406
+ "or sharded vocabularies are not supported"
407
+ )
408
+ self.embedding = embedding
409
+ self.prepare(head)
410
+
411
+ @torch.no_grad()
412
+ def prepare(self, lm_head_weight: torch.Tensor) -> None:
413
+ if self.draft_vocab_size and self._vocab_ranking is None:
414
+ logger.warning(
415
+ "FlashMap: no draft_vocab.safetensors next to the weights; drafting "
416
+ "over the full vocabulary"
417
+ )
418
+ self.draft_vocab_size = 0
419
+ if self.draft_vocab_size:
420
+ self.hot_ids = (
421
+ self._vocab_ranking[: self.draft_vocab_size]
422
+ .to(device=self.embedding.device, dtype=torch.int64)
423
+ .sort()
424
+ .values
425
+ )
426
+ self.folded_head = self._fold_lm_head(lm_head_weight, self.hot_ids)
427
+ self.lm_head = _FoldedHead(*self.folded_head, self.vocab_size)
428
+ self.markov_head.hot_ids = self.hot_ids
429
+ if self.fp8:
430
+ self._quantize_fp8()
431
+ self._constants = self._jump_constants()
432
+ if self.tree_mode:
433
+ if self.num_steps != 1:
434
+ raise NotImplementedError("tree drafting takes one jump")
435
+ self._tree_buffers = self._alloc_tree_buffers(self._graph_max_bs())
436
+ if self.num_steps > 1:
437
+ self.jump_bank = JumpNoiseBank(
438
+ self,
439
+ self.grid,
440
+ size=self.jump_bank_size,
441
+ temperature=self.noise_temperature,
442
+ )
443
+ logger.info(
444
+ "FlashMap: %d jumps from a bank of %d noise draws (%.0f MB), "
445
+ "predictions cut to %d tokens",
446
+ self.num_steps,
447
+ self.jump_bank_size,
448
+ self.jump_bank.nbytes() / 2**20,
449
+ self.jump_topk,
450
+ )
451
+ if torch.cuda.is_available():
452
+ torch.cuda.empty_cache()
453
+
454
+ def _quantize_fp8(self) -> None:
455
+ """FP8 copies of every large GEMM's weight: the DiT's linears, ``x_embed``,
456
+ the folded head, ``fc`` and the context/anchor projections. Norms, the
457
+ AdaLN constants and the noise draw (``z @ L^T``, whose input is the noise
458
+ itself) stay bf16."""
459
+ if torch.cuda.get_device_capability() < (8, 9):
460
+ raise RuntimeError("FLASHMAP_FP8=1 needs FP8 tensor cores (Ada/Hopper)")
461
+ weights = {
462
+ "x_embed": (self.x_embed.weight, self.x_embed.bias),
463
+ "fc": (self.fc.weight, None),
464
+ "ctx_kv": (self._ctx_kv_weight, None),
465
+ "anchor_proj": (self.anchor_proj.weight, self.anchor_proj.bias),
466
+ "head": self.folded_head,
467
+ }
468
+ for i, layer in enumerate(self.layers):
469
+ weights[f"qkv{i}"] = (self._self_qkv_weights[i], None)
470
+ weights[f"self_o{i}"] = (layer.self_attn.o_proj.weight, None)
471
+ weights[f"cross_q{i}"] = (layer.cross_attn.q_proj.weight, None)
472
+ weights[f"cross_o{i}"] = (layer.cross_attn.o_proj.weight, None)
473
+ weights[f"fc1_{i}"] = (layer.mlp.fc1.weight, layer.mlp.fc1.bias)
474
+ weights[f"fc2_{i}"] = (layer.mlp.fc2.weight, layer.mlp.fc2.bias)
475
+ for key, (weight, bias) in weights.items():
476
+ self._fp8[key] = (*quantize_weight_fp8(weight), bias)
477
+
478
+ def _linear(self, x, key: str, weight, bias=None) -> torch.Tensor:
479
+ """``x @ weight^T + bias``, in FP8 with ``FLASHMAP_FP8=1``."""
480
+ if self.fp8:
481
+ return fp8_linear(x, *self._fp8[key])
482
+ return F.linear(x, weight, bias)
483
+
484
+ def head_logits(self, hidden: torch.Tensor) -> torch.Tensor:
485
+ """The draft-vocabulary logits of the DiT's output (the folded head)."""
486
+ return self._linear(hidden, "head", *self.folded_head)
487
+
488
+ def _fold_lm_head(self, lm_head, rows: Optional[torch.Tensor], chunk: int = 8192):
489
+ """The DiT head (dim -> hidden, with bias) and the LM head (hidden ->
490
+ vocab) as one ``(vocab rows, dim)`` projection and bias; there is no
491
+ nonlinearity between them (Qwen3 has no final-logit softcap). Folded
492
+ ``chunk`` rows at a time in float32."""
493
+ count = lm_head.shape[0] if rows is None else rows.numel()
494
+ weight = lm_head.new_empty(count, self.dim)
495
+ bias = lm_head.new_empty(count)
496
+ head_weight, head_bias = self.head.weight.float(), self.head.bias.float()
497
+ for start in range(0, count, chunk):
498
+ if rows is None:
499
+ part = lm_head[start : start + chunk].float()
500
+ else:
501
+ part = lm_head[rows[start : start + chunk]].float()
502
+ weight[start : start + chunk] = (part @ head_weight).to(lm_head.dtype)
503
+ bias[start : start + chunk] = (part @ head_bias).to(lm_head.dtype)
504
+ return weight, bias
505
+
506
+ def _jump_constants(self):
507
+ """Everything a draft needs that depends only on the jump times: per
508
+ jump the AdaLN modulations ``(1 + scale, shift, gate)`` of every norm;
509
+ the embedded starting state's constant (the uniform posterior's
510
+ embedding); and, for one jump, what its noise is drawn with: the
511
+ Cholesky factor of ``E^T E``, or the folded path (``_folded_noise``)."""
512
+ device = self.fc.weight.device
513
+ per_jump = []
514
+ for start, end in zip(self.grid[:-1], self.grid[1:]):
515
+ # (scale, shift, gate) of every norm, from the reference model.
516
+ layers, (scale, shift, _) = self.time_conditioning(start, end)
517
+ modulations = [
518
+ tuple(
519
+ (1 + scale[0, 0], shift[0, 0], gate[0, 0])
520
+ for scale, shift, gate in norms
521
+ )
522
+ for norms in layers
523
+ ]
524
+ per_jump.append((modulations, (1 + scale[0, 0], shift[0, 0])))
525
+ embedding = self.embedding
526
+ posterior = torch.full(
527
+ (1, self.vocab_size), 1.0 / self.vocab_size, device=device
528
+ ).to(embedding.dtype)
529
+ uniform = self.state_norm(posterior @ embedding)[0]
530
+ noise = None
531
+ if self.num_steps == 1 and self.folded_noise:
532
+ noise = self._folded_noise(uniform)
533
+ elif self.num_steps == 1:
534
+ gram = self._embedding_gram()
535
+ noise = torch.linalg.cholesky(gram).T.to(embedding.dtype)
536
+ return per_jump, uniform, noise
537
+
538
+ def _embedding_gram(self) -> torch.Tensor:
539
+ """``E^T E`` in float64 (``E`` the target's embedding), accumulated a
540
+ chunk of rows at a time."""
541
+ hidden = self.target_hidden_size
542
+ gram = torch.zeros(
543
+ hidden, hidden, dtype=torch.float64, device=self.embedding.device
544
+ )
545
+ for start in range(0, self.vocab_size, 8192):
546
+ part = self.embedding[start : start + 8192].double()
547
+ gram += part.T @ part
548
+ return gram
549
+
550
+ def noise_basis(self):
551
+ """``(lam, Q)`` in float64 with ``E^T E = Q diag(lam) Q^T``."""
552
+ lam, basis = torch.linalg.eigh(self._embedding_gram())
553
+ return lam.clamp_min(0), basis
554
+
555
+ def _folded_noise(self, uniform: torch.Tensor):
556
+ """The first layer's input for one jump from pure noise ``x``: with
557
+ ``x @ E = (z * sqrt(lam)) @ Q^T = y``, ``z ~ N(0, I_H)``,
558
+
559
+ x_embed(uniform + gate * noise_norm(y))
560
+ = x_embed(uniform) + gate * r * (z @ M),
561
+ r = rsqrt(sum(lam z^2) / H + eps), M = diag(sqrt(lam)) Q^T diag(w) W_x^T,
562
+
563
+ ``w`` the noise norm's weight. Returns ``(lam / H, M, x_embed(uniform))``."""
564
+ lam, basis = self.noise_basis()
565
+ weight = self.noise_norm.weight.double()
566
+ fold = (lam.sqrt()[:, None] * basis.T) * weight[None, :]
567
+ proj = (fold @ self.x_embed.weight.double().T).to(self.embedding.dtype)
568
+ variance = (lam / self.target_hidden_size).float()
569
+ return variance, proj, self.x_embed(uniform)
570
+
571
+ @staticmethod
572
+ def _graph_max_bs() -> int:
573
+ return _server_setting(
574
+ lambda: get_exec().graph.cuda_graph_config.decode.max_bs, 64
575
+ )
576
+
577
+ def _alloc_tree_buffers(self, max_bs: int):
578
+ """The tree a draft step hands to verification, in EAGLE's encoding
579
+ (build_tree_kernel_efficient): per request its W - 1 non-root nodes'
580
+ tokens and ids ``parent * W + index``, the parent list, and (trie nodes,
581
+ distinct candidates); the draft's CUDA graphs write them in place."""
582
+ width, device = self.tree_width, self._ctx_kv_weight.device
583
+
584
+ def zeros(*shape):
585
+ return torch.zeros(*shape, dtype=torch.int64, device=device)
586
+
587
+ return (
588
+ zeros(max_bs, width - 1),
589
+ zeros(max_bs, width - 1),
590
+ zeros(max_bs, width * (self.block_size - 1) + 1),
591
+ zeros(max_bs, 2),
592
+ )
593
+
594
+ def tree_outputs(self, bsz: int):
595
+ """The tree buffers for a batch of ``bsz``: the persistent ones, which
596
+ captured graphs write, or (an eager batch above them) scratch ones."""
597
+ buffers = self._tree_buffers
598
+ if bsz > buffers[0].shape[0]:
599
+ if torch.cuda.is_current_stream_capturing():
600
+ raise RuntimeError(
601
+ f"capturing a draft graph for {bsz} requests, above the tree buffers"
602
+ )
603
+ if self._tree_scratch is None or self._tree_scratch[0].shape[0] < bsz:
604
+ self._tree_scratch = self._alloc_tree_buffers(bsz)
605
+ buffers = self._tree_scratch
606
+ return tuple(buffer[:bsz] for buffer in buffers)
607
+
608
+ def tree_n_for(self, bsz: int) -> int:
609
+ """Candidates per request in a batch of ``bsz`` (fixed per captured
610
+ batch size, so graphs replay what they captured)."""
611
+ return max(1, min(self.tree_max_n, self.tree_budget // max(bsz, 1)))
612
+
613
+ # Rows of the head's logits held at once while taking the tree's argmaxes.
614
+ _TREE_HEAD_CHUNK = 1024
615
+
616
+ def _draft_tree(self, hidden: torch.Tensor, bsz: int, n_cand: int) -> None:
617
+ """Greedy tokens of every candidate, merged into a trie cut to the
618
+ verify width, written to the tree buffers (inside the draft graph)."""
619
+ rows = hidden.shape[0]
620
+ tokens = torch.empty(rows, dtype=torch.int64, device=hidden.device)
621
+ for start in range(0, rows, self._TREE_HEAD_CHUNK):
622
+ end = min(rows, start + self._TREE_HEAD_CHUNK)
623
+ tokens[start:end] = self.head_logits(hidden[start:end]).argmax(dim=-1)
624
+ if self.hot_ids is not None:
625
+ tokens = self.hot_ids[tokens]
626
+ out_tokens, out_ids, out_parents, out_stats = self.tree_outputs(bsz)
627
+ node_tokens, ids, stats = self._build_tree(
628
+ tokens.view(bsz, n_cand, self.block_size), self.tree_width
629
+ )
630
+ out_tokens.copy_(node_tokens)
631
+ out_ids.copy_(ids)
632
+ out_parents[:, 1 : ids.shape[1] + 1].copy_(ids)
633
+ out_stats.copy_(stats)
634
+
635
+ def noise_gate_at(self, start: float, end: float) -> torch.Tensor:
636
+ """The weight of the state's noise term for a jump from ``start`` to
637
+ ``end``: ``noise_gate * (end - start)``, or ``noise_gate`` alone for a
638
+ ``"constant"`` gate (``modeling_flashmap``'s ``mix_state``)."""
639
+ jump = 1.0 if self.config.noise_gate == "constant" else end - start
640
+ return (self.noise_gate.float() * jump).to(self.noise_gate.dtype)
641
+
642
+ # --- DSPARK protocol ------------------------------------------------------
643
+
644
+ def forward_embed(self, input_ids: torch.Tensor) -> torch.Tensor:
645
+ # Its presence tells DSPARK the draft embeds its own block in-graph, so
646
+ # it skips staging the target's embedding of the block every step; the
647
+ # draft never reads it (the anchor is embedded by _anchor_kv).
648
+ return F.embedding(input_ids, self.embedding)
649
+
650
+ def prune_to_ctx_kv_injection(self) -> None:
651
+ pass
652
+
653
+ def compute_base_logits(self, hidden: torch.Tensor):
654
+ return self.head_logits(hidden), None
655
+
656
+ @torch.no_grad()
657
+ def write_target_hidden_kv(
658
+ self,
659
+ *,
660
+ target_hidden: torch.Tensor,
661
+ pool,
662
+ positions: torch.Tensor,
663
+ cache_loc: torch.Tensor,
664
+ cache_loc_2d: Optional[torch.Tensor] = None,
665
+ commit_lens: Optional[torch.Tensor] = None,
666
+ target_hidden_is_projected: bool = False,
667
+ ) -> None:
668
+ assert not target_hidden_is_projected
669
+ if target_hidden.shape[0] == 0:
670
+ return
671
+ args = self._fused_write_args(pool)
672
+ if cache_loc_2d is not None and commit_lens is not None:
673
+ # Verify windows: only each request's committed columns are written.
674
+ self._write_context(
675
+ target_hidden,
676
+ positions,
677
+ cache_loc_2d.reshape(-1),
678
+ args,
679
+ commit_lens=commit_lens,
680
+ locs_row_width=cache_loc_2d.shape[1],
681
+ )
682
+ return
683
+ # A prefill, a chunk of rows at a time: every layer's keys and values of
684
+ # one row take 6 * 2 * 2048 bf16 (48 KB for Qwen3-8B's map).
685
+ chunk = self._WRITE_CHUNK
686
+ for start in range(0, target_hidden.shape[0], chunk):
687
+ self._write_context(
688
+ target_hidden[start : start + chunk],
689
+ positions[start : start + chunk],
690
+ cache_loc[start : start + chunk],
691
+ args,
692
+ )
693
+
694
+ # Prefill rows projected at once by write_target_hidden_kv.
695
+ _WRITE_CHUNK = 2048
696
+
697
+ def _write_context(self, target_hidden, positions, locs, args, **commit):
698
+ """k-norm, RoPE and the write for every layer in one launch (sglang's
699
+ fused DSpark kernel, which skips uncommitted verify columns)."""
700
+ context = rms_norm(
701
+ self._linear(
702
+ target_hidden.to(self._ctx_kv_weight.dtype), "fc", self.fc.weight
703
+ ),
704
+ self._hidden_norm_weight,
705
+ self.rms_norm_eps,
706
+ )
707
+ self._kv_write(
708
+ self._linear(context, "ctx_kv", self._ctx_kv_weight),
709
+ args,
710
+ positions,
711
+ locs,
712
+ **commit,
713
+ )
714
+
715
+ def _kv_write(self, kv, args, positions, locs, **commit) -> None:
716
+ """Every layer's context keys (k-normed, rotated) and values from the
717
+ projections ``kv``, written at ``locs``: sglang's fused DSpark kernel for
718
+ a bf16 pool, ours for an e4m3 one."""
719
+ meta, k_norm_weights, cos_sin, kv_size = args
720
+ write = kv_norm_rope_write_fp8 if self._kv_fp8 else fused_kv_norm_rope_write
721
+ if not self._kv_fp8:
722
+ commit["is_neox_style"] = False # FlashMap rotates interleaved pairs
723
+ write(
724
+ kv,
725
+ meta,
726
+ k_norm_weights,
727
+ cos_sin,
728
+ positions,
729
+ locs,
730
+ self.num_stages,
731
+ kv_size,
732
+ self.head_dim,
733
+ self.qk_norm_eps,
734
+ **commit,
735
+ )
736
+
737
+ def _fused_write_args(self, pool):
738
+ """``fused_kv_norm_rope_write``'s arguments for ``pool``."""
739
+ if self._fused_write is not None and self._fused_write[0] == id(pool):
740
+ return self._fused_write[1]
741
+ buffers = [
742
+ (pool.get_key_buffer(layer_id), pool.get_value_buffer(layer_id))
743
+ for layer_id in range(self.num_stages)
744
+ ]
745
+ for buf in (buf for pair in buffers for buf in pair):
746
+ if not buf.is_cuda or buf.dtype not in (
747
+ torch.bfloat16,
748
+ torch.float8_e4m3fn,
749
+ ):
750
+ raise ValueError(
751
+ f"the FlashMap draft KV pool must be bf16 or fp8_e4m3 on CUDA, got "
752
+ f"{buf.dtype} (--speculative-draft-kv-cache-dtype)"
753
+ )
754
+ assert buf.shape[1:] == (self.num_heads, self.head_dim), buf.shape
755
+ assert buf.stride(1) == self.head_dim and buf.stride(2) == 1
756
+ self._kv_fp8 = buffers[0][0].dtype == torch.float8_e4m3fn
757
+ device = self._ctx_kv_weight.device
758
+ meta = torch.tensor(
759
+ [
760
+ [k.data_ptr(), v.data_ptr(), k.stride(0), v.stride(0)]
761
+ for k, v in buffers
762
+ ],
763
+ dtype=torch.int64,
764
+ device=device,
765
+ )
766
+ k_norm_weights = torch.stack(
767
+ [layer.cross_attn.k_norm.weight.float() for layer in self.layers]
768
+ )
769
+ # Row p: cos then sin of p times each frequency, for every position the
770
+ # server can reach (the fused kernel does not bound-check): a request's
771
+ # tokens fit its req_to_token row, and a draft reaches block_size past them.
772
+ row = _server_setting(lambda: get_req_to_token_pool().req_to_token.shape[1], 0)
773
+ context_length = _server_setting(lambda: get_model().context_length, 0)
774
+ max_positions = (
775
+ max(
776
+ int(row),
777
+ int(context_length),
778
+ int(getattr(self.config, "max_position_embeddings", 0) or 0),
779
+ )
780
+ + self.block_size
781
+ + 1
782
+ )
783
+ freqs = rope_freqs(
784
+ torch.arange(max_positions, device=device), self.head_dim, self.rope_theta
785
+ )
786
+ cos_sin = torch.cat([freqs[..., 0], freqs[..., 1]], dim=-1).contiguous()
787
+ args = (meta, k_norm_weights, cos_sin, self.num_heads * self.head_dim)
788
+ self._fused_write = (id(pool), args)
789
+ return args
790
+
791
+ @torch.no_grad()
792
+ def forward(
793
+ self,
794
+ input_ids: torch.Tensor,
795
+ positions: torch.Tensor,
796
+ forward_batch,
797
+ input_embeds: Optional[torch.Tensor] = None,
798
+ get_embedding: bool = False,
799
+ pp_proxy_tensors=None,
800
+ ) -> LogitsProcessorOutput:
801
+ """A block for every request; returns the DiT's final hidden states,
802
+ which ``compute_base_logits`` (or the folded sampler) maps to logits.
803
+
804
+ DSPARK asks for ``gamma`` drafts per request, the block size unless
805
+ ``--speculative-dspark-block-size`` verifies fewer (at high concurrency
806
+ the target's verify is compute-bound and the last drafts are rarely
807
+ accepted); the map always drafts its whole block, as it was trained,
808
+ and hands over the first ``gamma``."""
809
+ del input_embeds, get_embedding, pp_proxy_tensors
810
+ bsz, block_size = forward_batch.batch_size, self.block_size
811
+ gamma = positions.numel() // bsz
812
+ assert gamma <= block_size, (
813
+ f"--speculative-dspark-block-size {gamma} exceeds the map's block of "
814
+ f"{block_size} drafts"
815
+ )
816
+ pool = get_token_to_kv_pool()
817
+ anchor_positions = positions.view(bsz, gamma)[:, 0]
818
+ block_positions = anchor_positions[:, None] + torch.arange(
819
+ block_size, device=positions.device
820
+ )
821
+ # The anchor's context entry goes into its own slot, the first of the
822
+ # request's verify window, so each request attends to positions
823
+ # 0..anchor; committing the verified tokens later overwrites the slot.
824
+ self._kv_write(
825
+ self._anchor_kv(input_ids.view(bsz, gamma)[:, 0]),
826
+ self._fused_write_args(pool),
827
+ anchor_positions,
828
+ forward_batch.out_cache_loc.view(bsz, gamma)[:, 0],
829
+ )
830
+ page_table = get_req_to_token_pool().req_to_token[
831
+ forward_batch.req_pool_indices
832
+ ]
833
+ cache_seqlens = (forward_batch.seq_lens + 1).to(torch.int32)
834
+ # Tree drafting puts a request's candidate blocks side by side in the
835
+ # cross-attention's query axis, so its context is read once, not N times.
836
+ n_cand = self.tree_n_for(bsz) if self.tree_mode else 1
837
+ schedule = _cross_attention_schedule(
838
+ (bsz, n_cand * block_size, self.num_heads, self.head_dim),
839
+ cache_seqlens,
840
+ page_table,
841
+ torch.float8_e4m3fn if self._kv_fp8 else torch.bfloat16,
842
+ )
843
+ hidden = self._draft(
844
+ block_positions,
845
+ page_table,
846
+ cache_seqlens,
847
+ schedule,
848
+ [pool.get_key_buffer(i) for i in range(self.num_stages)],
849
+ [pool.get_value_buffer(i) for i in range(self.num_stages)],
850
+ n_cand,
851
+ )
852
+ if self.tree_mode:
853
+ assert gamma == block_size, "tree drafting verifies the whole block"
854
+ self._draft_tree(hidden, bsz, n_cand)
855
+ # DSPARK's proposer expects one block per request: hand it the first
856
+ # candidate's (the tree path reads the trie instead).
857
+ hidden = hidden.view(bsz, n_cand, block_size, -1)[:, 0]
858
+ hidden = hidden.reshape(bsz * block_size, -1)
859
+ if gamma < block_size:
860
+ hidden = hidden.view(bsz, block_size, -1)[:, :gamma].reshape(
861
+ bsz * gamma, -1
862
+ )
863
+ return LogitsProcessorOutput(next_token_logits=None, hidden_states=hidden)
864
+
865
+ def _anchor_kv_impl(self, anchor_ids: torch.Tensor) -> torch.Tensor:
866
+ """Every layer's context key/value row for each anchor token, before
867
+ k-norm and RoPE (the fused write applies those)."""
868
+ anchor = F.embedding(anchor_ids, self.embedding)
869
+ anchor = self._linear(
870
+ anchor, "anchor_proj", self.anchor_proj.weight, self.anchor_proj.bias
871
+ )
872
+ return self._linear(self.anchor_norm(anchor), "ctx_kv", self._ctx_kv_weight)
873
+
874
+ def _draft_impl(
875
+ self,
876
+ block_positions,
877
+ page_table,
878
+ cache_seqlens,
879
+ schedule,
880
+ keys,
881
+ values,
882
+ n_cand: int = 1,
883
+ ) -> torch.Tensor:
884
+ """The block's ``num_steps`` jumps, a pure tensor function so it can be
885
+ compiled; returns the last jump's hidden states ``(bs * K, dim)``.
886
+
887
+ ``n_cand`` > 1 (one jump only, tree drafting) drafts that many blocks
888
+ per request from independent noise, candidate-major per request."""
889
+ _, uniform, noise = self._constants
890
+ requests, block_size = block_positions.shape
891
+ bsz = requests * n_cand
892
+ slots = bsz * block_size
893
+ # The slots sit at anchor + 1 .. anchor + K.
894
+ freqs = rope_freqs(block_positions + 1, self.head_dim, self.rope_theta)
895
+ if n_cand > 1:
896
+ freqs = freqs.repeat_interleave(n_cand, dim=0)
897
+ attend = (freqs, page_table, cache_seqlens, schedule, keys, values, n_cand)
898
+ if self.num_steps == 1 and not self.folded_noise:
899
+ # One jump from t = 0: the noise drawn in the embedding's space.
900
+ z = self.noise_temperature * torch.randn(
901
+ bsz,
902
+ block_size,
903
+ noise.shape[0],
904
+ device=block_positions.device,
905
+ dtype=noise.dtype,
906
+ )
907
+ gate = self.noise_gate_at(self.grid[0], self.grid[1])
908
+ embedded = uniform + gate * self.noise_norm(z @ noise)
909
+ hidden = self._linear(
910
+ embedded, "x_embed", self.x_embed.weight, self.x_embed.bias
911
+ )
912
+ return self._denoise(0, hidden, *attend).reshape(slots, -1)
913
+ if self.num_steps == 1:
914
+ # The same, embedded by the folded path (_folded_noise).
915
+ variance, proj, embedded_uniform = noise
916
+ z = self.noise_temperature * torch.randn(
917
+ bsz,
918
+ block_size,
919
+ proj.shape[0],
920
+ device=block_positions.device,
921
+ dtype=proj.dtype,
922
+ )
923
+ mean_square = (z.float().square() * variance).sum(-1, keepdim=True)
924
+ scale = self.noise_gate_at(self.grid[0], self.grid[1]) * torch.rsqrt(
925
+ mean_square + self.rms_norm_eps
926
+ )
927
+ hidden = embedded_uniform + (scale * (z @ proj).float()).to(proj.dtype)
928
+ return self._denoise(0, hidden, *attend).reshape(slots, -1)
929
+ # Banked jumps (jump_bank.py): no vocabulary-sized state, the
930
+ # predictions between jumps from the draft vocabulary's head.
931
+ assert n_cand == 1, "several candidates per request take one jump"
932
+ bank = self.jump_bank
933
+ rows, seeds = bank.draw(slots, block_positions.device)
934
+ embedded = bank.first_input(self, rows, uniform)
935
+ supports = []
936
+ for step in range(self.num_steps):
937
+ if step:
938
+ embedded = bank.jump_input(self, step, rows, seeds, supports)
939
+ hidden = self._linear(
940
+ embedded.view(bsz, block_size, -1),
941
+ "x_embed",
942
+ self.x_embed.weight,
943
+ self.x_embed.bias,
944
+ )
945
+ hidden = self._denoise(step, hidden, *attend).reshape(slots, -1)
946
+ if step + 1 < self.num_steps:
947
+ logits = self.head_logits(hidden)
948
+ supports.append(top_predictions(logits, self.jump_topk, self.hot_ids))
949
+ return hidden
950
+
951
+ def _denoise(
952
+ self,
953
+ step,
954
+ hidden,
955
+ freqs,
956
+ page_table,
957
+ cache_seqlens,
958
+ schedule,
959
+ keys,
960
+ values,
961
+ n_cand: int = 1,
962
+ ) -> torch.Tensor:
963
+ """One evaluation of the map at jump ``step``'s times on the embedded
964
+ state after ``x_embed``, ``(bs, K, dim)``; returns the final AdaLN's
965
+ output."""
966
+ modulations, (final_scale, final_shift) = self._constants[0][step]
967
+ bsz, block_size, _ = hidden.shape
968
+ shape = (bsz, block_size, self.num_heads, self.head_dim)
969
+ eps = self.layer_norm_eps
970
+ for i, layer in enumerate(self.layers):
971
+ (s_scale, s_shift, s_gate), (c_scale, c_shift, c_gate), mlp = modulations[i]
972
+ m_scale, m_shift, m_gate = mlp
973
+ # Self-attention within each request's block.
974
+ normed = F.layer_norm(hidden, (self.dim,), eps=eps) * s_scale + s_shift
975
+ qkv = self._linear(normed, f"qkv{i}", self._self_qkv_weights[i])
976
+ attended = block_self_attention(
977
+ qkv.view(bsz, block_size, 3, self.num_heads, self.head_dim),
978
+ *self._self_norm_weights[i],
979
+ freqs,
980
+ self.qk_norm_eps,
981
+ )
982
+ attended = attended.reshape(bsz, block_size, self.dim)
983
+ hidden = hidden + s_gate * self._linear(
984
+ attended, f"self_o{i}", layer.self_attn.o_proj.weight
985
+ )
986
+ # Cross-attention onto the paged context.
987
+ normed = F.layer_norm(hidden, (self.dim,), eps=eps) * c_scale + c_shift
988
+ attn = layer.cross_attn
989
+ query = rms_norm_rope(
990
+ self._linear(normed, f"cross_q{i}", attn.q_proj.weight).view(shape),
991
+ self._cross_q_norm_weights[i],
992
+ freqs,
993
+ self.qk_norm_eps,
994
+ )
995
+ cross_attention = (
996
+ _paged_cross_attention_fp8 if self._kv_fp8 else _paged_cross_attention
997
+ )
998
+ # A request's candidates as one query sequence (their rows are
999
+ # adjacent, candidate-major).
1000
+ context = cross_attention(
1001
+ query.reshape(bsz // n_cand, n_cand * block_size, *shape[2:]),
1002
+ keys[i],
1003
+ values[i],
1004
+ cache_seqlens,
1005
+ page_table,
1006
+ schedule,
1007
+ )
1008
+ context = context.reshape(bsz, block_size, self.dim)
1009
+ hidden = hidden + c_gate * self._linear(
1010
+ context, f"cross_o{i}", attn.o_proj.weight
1011
+ )
1012
+ normed = F.layer_norm(hidden, (self.dim,), eps=eps) * m_scale + m_shift
1013
+ mlp = layer.mlp
1014
+ normed = self._linear(normed, f"fc1_{i}", mlp.fc1.weight, mlp.fc1.bias)
1015
+ normed = F.gelu(normed)
1016
+ normed = self._linear(normed, f"fc2_{i}", mlp.fc2.weight, mlp.fc2.bias)
1017
+ hidden = hidden + m_gate * normed
1018
+ return F.layer_norm(hidden, (self.dim,), eps=eps) * final_scale + final_shift
1019
+
1020
+
1021
+ EntryClass = [FlashMapDraftModel]
1022
+
1023
+
1024
+ # --- DSPARK worker hooks, for FlashMap drafts only -----------------------------
1025
+
1026
+ _Worker = dspark_worker_v2.DSparkWorkerV2
1027
+
1028
+
1029
+ def _no_attention_metadata(*args, **kwargs) -> None:
1030
+ return None
1031
+
1032
+
1033
+ def _wrap_init_cuda_graphs(init_cuda_graphs):
1034
+ def wrapped(self, *args, **kwargs):
1035
+ if getattr(self.draft_model, "flashmap", False):
1036
+ # FlashMap attends through its own paged cross-attention and never
1037
+ # reads the draft attention backend: skip the metadata it prepares
1038
+ # every step.
1039
+ backend = self.draft_model_runner.attn_backend
1040
+ assert backend is not self.model_runner.attn_backend
1041
+ for name in (
1042
+ "init_forward_metadata",
1043
+ "init_forward_metadata_capture_cuda_graph",
1044
+ "init_forward_metadata_replay_cuda_graph",
1045
+ ):
1046
+ setattr(backend, name, _no_attention_metadata)
1047
+ return init_cuda_graphs(self, *args, **kwargs)
1048
+
1049
+ return wrapped
1050
+
1051
+
1052
+ def _wrap_build_draft_sampler(build_draft_sampler):
1053
+ def wrapped(self, *args, **kwargs):
1054
+ sampler = build_draft_sampler(self, *args, **kwargs)
1055
+ if sampler is None or not getattr(self.draft_model, "flashmap", False):
1056
+ return sampler
1057
+ max_bs = max(get_exec().graph.cuda_graph_config.decode.bs)
1058
+ sampler = FlashMapDraftSampler(sampler, max_bs=max_bs, device=self.device)
1059
+ logger.info(
1060
+ "FlashMap draft sampler: one pass over the block (folded sampling %s, "
1061
+ "fused kernel %s)",
1062
+ sampler.folded_sampling,
1063
+ sampler.fused,
1064
+ )
1065
+ return sampler
1066
+
1067
+ return wrapped
1068
+
1069
+
1070
+ # Installed once, however often the module is imported.
1071
+ if not getattr(_Worker, "_flashmap_sglang_hooks", False):
1072
+ _Worker.init_cuda_graphs = _wrap_init_cuda_graphs(_Worker.init_cuda_graphs)
1073
+ _Worker._maybe_build_draft_sampler = _wrap_build_draft_sampler(
1074
+ _Worker._maybe_build_draft_sampler
1075
+ )
1076
+ _Worker._flashmap_sglang_hooks = True