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 +14 -0
- flashmap/configuration_flashmap.py +105 -0
- flashmap/modeling_flashmap.py +556 -0
- flashmap/sglang/__init__.py +78 -0
- flashmap/sglang/jump_bank.py +203 -0
- flashmap/sglang/kernels.py +457 -0
- flashmap/sglang/model.py +1076 -0
- flashmap/sglang/sampler.py +188 -0
- flashmap/sglang/tree.py +78 -0
- flashmap/sglang/tree_worker.py +396 -0
- flashmap/sglang/verify_attention.py +474 -0
- flashmap/vllm/__init__.py +268 -0
- flashmap/vllm/fold.py +128 -0
- flashmap/vllm/kernels.py +443 -0
- flashmap/vllm/model.py +881 -0
- flashmap/vllm/speculator.py +235 -0
- flashmap-0.1.0.dist-info/METADATA +152 -0
- flashmap-0.1.0.dist-info/RECORD +22 -0
- flashmap-0.1.0.dist-info/WHEEL +5 -0
- flashmap-0.1.0.dist-info/entry_points.txt +5 -0
- flashmap-0.1.0.dist-info/licenses/LICENSE +21 -0
- flashmap-0.1.0.dist-info/top_level.txt +1 -0
flashmap/sglang/model.py
ADDED
|
@@ -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
|