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,188 @@
1
+ # coding=utf-8
2
+ """DSPARK's Markov head and draft sampler, specialized for FlashMap.
3
+
4
+ sglang's DSPARK worker requires a Markov head (a low-rank bias on each draft
5
+ position's logits from the previous token) and samples the block one position
6
+ at a time through it. FlashMap has no such head: its block positions are
7
+ independent given the noise. :class:`IdentityMarkov` stands in for the head
8
+ (rank 1, never loaded, contributes nothing) so DSPARK samples the map's logits
9
+ unchanged and verifies them with its usual rejection sampling.
10
+
11
+ With no bias, the positions are independent, yet DSPARK's graph-folded sampler
12
+ would still walk them one at a time (an exponential-noise fill and two
13
+ online-softmax kernels per position, or an argmax per position for greedy
14
+ batches). :class:`FlashMapDraftSampler` samples the whole block at once inside
15
+ the draft's CUDA graph: the logits written into DSPARK's buffer by one GEMM
16
+ against the folded head, then one ``SampleStepTokens`` pass with fresh
17
+ exponential noise per row and each request's temperature (exactly DSPARK's
18
+ per-position sampling, since the rows are independent; greedy rows take their
19
+ argmax). Drafted indices of a restricted vocabulary map back to token ids in
20
+ the graph too, and the logits rejection sampling reads stay over the full
21
+ vocabulary (-inf outside the draft vocabulary).
22
+
23
+ ``FLASHMAP_FUSED_SAMPLER=1`` (opt-in, measured on Qwen3-8B only) samples with
24
+ one kernel instead: each greedy row's
25
+ argmax, each other row Gumbel-max-sampled at its temperature with in-kernel
26
+ Philox noise, so there is no noise buffer, and the full-vocabulary logits are
27
+ scattered only when DSPARK reads them for a sampling batch, so greedy batches
28
+ never pay for them.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import os
34
+
35
+ import torch
36
+ from sglang.kernels.ops.speculative.dspark.dspark_draft_model import SampleStepTokens
37
+ from sglang.srt.models.dspark import VanillaMarkov
38
+ from sglang.srt.speculative.dspark_components.dspark_draft_sampler import (
39
+ DsparkDraftSampler,
40
+ )
41
+ from sglang.srt.speculative.spec_tp_sync import SpecTpSyncSite
42
+
43
+ from .kernels import sample_rows
44
+
45
+
46
+ class IdentityMarkov(VanillaMarkov):
47
+ """A Markov head that leaves the draft's logits unchanged.
48
+
49
+ With a restricted draft vocabulary (``hot_ids``, set by the draft) its
50
+ eager block sampler also maps drafted indices back to token ids and
51
+ scatters the logits back over the full vocabulary (-inf elsewhere, so
52
+ rejection sampling stays exact)."""
53
+
54
+ def __init__(self, *, vocab_size: int) -> None:
55
+ super().__init__(vocab_size=vocab_size, markov_rank=1)
56
+ for weight in self.parameters():
57
+ weight.requires_grad_(False)
58
+ torch.nn.init.zeros_(weight)
59
+ self.hot_ids = None
60
+
61
+ def apply_step_logits(self, logits, *args, **kwargs):
62
+ return logits
63
+
64
+ def apply_block_logits(self, base_logits, *args, **kwargs):
65
+ return base_logits
66
+
67
+ def sample_block_greedy_fused(self, *args, **kwargs):
68
+ return None # the caller falls back to sample_block
69
+
70
+ def sample_block(
71
+ self,
72
+ base_logits,
73
+ *,
74
+ first_prev_tokens,
75
+ hidden_states,
76
+ sampler,
77
+ collect_corrected: bool = True,
78
+ ):
79
+ del first_prev_tokens, hidden_states
80
+ tokens = torch.stack(
81
+ [sampler(base_logits[:, i], i) for i in range(base_logits.shape[1])], dim=1
82
+ )
83
+ if self.hot_ids is None:
84
+ return tokens, base_logits if collect_corrected else None
85
+ logits = None
86
+ if collect_corrected:
87
+ logits = base_logits.new_full(
88
+ (*base_logits.shape[:2], self.vocab_size), float("-inf")
89
+ )
90
+ logits.index_copy_(2, self.hot_ids, base_logits)
91
+ return self.hot_ids[tokens], logits
92
+
93
+
94
+ class _FullLogits:
95
+ """DSPARK's ``corrected_out`` for a restricted draft vocabulary: slicing
96
+ the first ``rows`` scatters those rows' draft-vocabulary logits into the
97
+ full-vocabulary buffer (-inf elsewhere) and returns them."""
98
+
99
+ def __init__(self, full: torch.Tensor, hot: torch.Tensor, hot_ids: torch.Tensor):
100
+ self.full, self.hot, self.hot_ids = full, hot, hot_ids
101
+ self.full.fill_(float("-inf"))
102
+
103
+ def __getitem__(self, index: slice) -> torch.Tensor:
104
+ assert index.start is None and index.step is None, index
105
+ rows = index.stop
106
+ return self.full[:rows].index_copy_(1, self.hot_ids, self.hot[:rows])
107
+
108
+
109
+ class FlashMapDraftSampler(DsparkDraftSampler):
110
+ """DSPARK's graph-folded draft sampler for a FlashMap draft: the whole block
111
+ in one pass (see the module docstring). Same contract."""
112
+
113
+ # DsparkDraftSampler state this sampler takes over (sglang 0.5.20).
114
+ _INHERITED = (
115
+ "model", "gamma", "sample_from_anchor", "query_token_num", "out",
116
+ "confidence_out", "folded_sampling", "temperatures", "greedy_mask",
117
+ "exp_noise", "corrected_out", "_tp_sync",
118
+ ) # fmt: skip
119
+
120
+ def __init__(self, sampler: DsparkDraftSampler, *, max_bs: int, device):
121
+ missing = [name for name in self._INHERITED if not hasattr(sampler, name)]
122
+ assert not missing, f"DsparkDraftSampler lacks {missing}: unsupported sglang"
123
+ self.__dict__.update(sampler.__dict__)
124
+ assert self.sample_from_anchor and self.confidence_out is None
125
+ self.fused = os.environ.get("FLASHMAP_FUSED_SAMPLER", "0") == "1"
126
+ self.hot_logits = None
127
+ if not self.folded_sampling:
128
+ return
129
+ hot_ids = self.model.hot_ids
130
+ rows, draft_vocab = int(max_bs) * self.gamma, self.model.folded_head[0].shape[0]
131
+ self.exp_noise = None
132
+ if not self.fused:
133
+ # Exponential noise for every row of the block, not one position's.
134
+ self.row_noise = torch.empty(
135
+ (rows, draft_vocab), dtype=torch.float32, device=device
136
+ )
137
+ if hot_ids is not None:
138
+ self.corrected_out.fill_(float("-inf"))
139
+ return
140
+ self.seed = torch.zeros((), dtype=torch.int64, device=device)
141
+ if hot_ids is not None:
142
+ # The draft-vocabulary logits outlive the graph replay until DSPARK
143
+ # reads them (``_FullLogits``).
144
+ self.hot_logits = torch.empty(
145
+ (rows, draft_vocab), dtype=self.corrected_out.dtype, device=device
146
+ )
147
+ self.corrected_out = _FullLogits(
148
+ self.corrected_out, self.hot_logits, hot_ids
149
+ )
150
+
151
+ def __call__(self, hidden_states, input_ids):
152
+ del input_ids
153
+ bs = hidden_states.shape[0] // self.query_token_num
154
+ rows = bs * self.gamma
155
+ hot_ids = self.model.hot_ids
156
+ if not self.folded_sampling:
157
+ logits = self.model.head_logits(hidden_states)
158
+ tokens = self._tp_sync.sync(
159
+ SpecTpSyncSite.DSPARK_GRAPH_GREEDY, logits.argmax(dim=-1)
160
+ )
161
+ else:
162
+ temperatures = self.temperatures[:bs].repeat_interleave(self.gamma)
163
+ greedy = self.greedy_mask[:bs].repeat_interleave(self.gamma)
164
+ if self.fused:
165
+ out = self.hot_logits if hot_ids is not None else self.corrected_out
166
+ logits = out[:rows]
167
+ logits.copy_(self.model.head_logits(hidden_states))
168
+ self.seed.add_(1)
169
+ tokens = sample_rows(logits, temperatures, greedy, self.seed)
170
+ else:
171
+ logits = self.model.head_logits(hidden_states)
172
+ if hot_ids is None:
173
+ self.corrected_out[:rows].copy_(logits)
174
+ else:
175
+ self.corrected_out[:rows].index_copy_(1, hot_ids, logits)
176
+ tokens = SampleStepTokens.execute(
177
+ step_logits=logits,
178
+ temperatures=temperatures,
179
+ greedy_mask=greedy,
180
+ exp_noise=self.row_noise[:rows].exponential_(),
181
+ )
182
+ tokens = self._tp_sync.sync(SpecTpSyncSite.DSPARK_GRAPH_SAMPLE, tokens)
183
+ if hot_ids is not None:
184
+ tokens = hot_ids[tokens]
185
+ self.out[:rows].copy_(tokens.reshape(-1))
186
+
187
+
188
+ __all__ = ["FlashMapDraftSampler", "IdentityMarkov"]
@@ -0,0 +1,78 @@
1
+ # coding=utf-8
2
+ """The verify tree of a multi-candidate FlashMap draft (pure torch, no sglang
3
+ imports)."""
4
+
5
+ import torch
6
+ import torch.nn.functional as F
7
+
8
+
9
+ def build_tree_abi(candidates: torch.Tensor, width: int):
10
+ """The verify tree from N candidate blocks per request, in EAGLE's encoding.
11
+
12
+ ``candidates (bs, N, K)`` are the drafts after each request's anchor (the
13
+ tree's root). Their trie has a node per distinct prefix; a node's vote is
14
+ how many candidates pass through it, a Monte Carlo estimate of the prefix's
15
+ probability under the map that never grows with depth. The ``width - 1``
16
+ nodes with the most votes (ties: shallower, then lower candidate) are kept,
17
+ so every kept node's parent is kept too, and listed parents first (by depth,
18
+ then candidate). A trie with fewer nodes is padded with copies of the first
19
+ node under the root, listed after it: verification takes the first matching
20
+ child, so a copy is never accepted.
21
+
22
+ Returns ``tokens (bs, width - 1)``; ``ids (bs, width - 1)``, node n's id
23
+ ``parent(n) * width + n`` (root = 0), which build_tree_kernel_efficient
24
+ decodes with topk = width and parent_list[:, n] = ids[:, n - 1]; and
25
+ ``stats (bs, 2)``: the trie's node count before the cut and the number of
26
+ distinct candidates.
27
+ All on device, fixed shapes, no host synchronization.
28
+ """
29
+ bs, n, k = candidates.shape
30
+ m = width - 1
31
+ device = candidates.device
32
+ # eq[b, i, j, d]: candidates i and j agree on positions 0..d.
33
+ eq = (
34
+ (candidates.unsqueeze(2) == candidates.unsqueeze(1))
35
+ .to(torch.int32)
36
+ .cumprod(dim=-1)
37
+ )
38
+ votes = eq.sum(dim=2) # (bs, N, K)
39
+ first = eq.argmax(dim=2) # the lowest candidate with the same prefix
40
+ cand = torch.arange(n, device=device).view(1, n, 1)
41
+ depth = torch.arange(k, device=device).view(1, 1, k)
42
+ real = first == cand # one representative slot per trie node
43
+ key = torch.where(
44
+ real, votes * (n * k) + (k - 1 - depth) * n + (n - 1 - cand), -1
45
+ ).view(bs, n * k)
46
+ if n * k < m:
47
+ key = F.pad(key, (0, m - n * k), value=-1)
48
+ # Keys of real nodes are distinct, so a sort ranks exactly as topk would;
49
+ # topk's radix select costs ~15 us per step here against ~4 us for a sort.
50
+ top, slot = key.sort(dim=1, descending=True)
51
+ top, slot = top[:, :m], slot[:, :m]
52
+ pad = top < 0
53
+ slot = slot.clamp(max=n * k - 1)
54
+ i, d = slot // k, slot % k
55
+ order = torch.where(
56
+ pad, n * k + torch.arange(m, device=device).view(1, m), d * n + i
57
+ )
58
+ order, perm = order.sort(dim=1)
59
+ slot, pad, i, d = (
60
+ slot.gather(1, perm),
61
+ pad.gather(1, perm),
62
+ i.gather(1, perm),
63
+ d.gather(1, perm),
64
+ )
65
+ index = torch.arange(1, m + 1, device=device).view(1, m).expand(bs, m)
66
+ # Node index of each kept slot; padding goes to a spare last column.
67
+ node_of = torch.zeros(bs, n * k + 1, dtype=torch.int64, device=device)
68
+ node_of.scatter_(1, torch.where(pad, n * k, slot), index)
69
+ # A node's parent is the representative of its prefix one shorter.
70
+ up = first.view(bs, n * k).gather(1, (slot - 1).clamp(min=0)) * k + (d - 1).clamp(
71
+ min=0
72
+ )
73
+ parent = torch.where((d == 0) | pad, 0, node_of.gather(1, up))
74
+ tokens = candidates.reshape(bs, n * k).gather(1, slot)
75
+ tokens = torch.where(pad, tokens[:, :1], tokens)
76
+ distinct = real[:, :, k - 1].sum(dim=1) # candidates that are their own last node
77
+ stats = torch.stack([real.view(bs, n * k).sum(dim=1), distinct], dim=1)
78
+ return tokens, parent * width + index, stats
@@ -0,0 +1,396 @@
1
+ # coding=utf-8
2
+ """Tree verification of multi-candidate FlashMap drafts inside sglang's DSPARK
3
+ worker.
4
+
5
+ Opt-in with ``FLASHMAP_TREE_N=<N>`` on a FlashMap server, with
6
+ ``SGLANG_DSPARK_FOLDED_PROPOSAL=0`` (the tree path does its own proposal), a
7
+ target attention backend that honours a tree mask (``fa3``, ``flashinfer`` or
8
+ ``triton``) and ``--page-size 1``. Greedy decoding only: the scheduler refuses
9
+ a request that samples (temperature > 0), asks for logprobs or constrains its
10
+ output (JSON schema, regex, EBNF, structural tag) when it arrives, with an error
11
+ response saying why (``tree_refusal``); the server keeps serving.
12
+
13
+ Each decode step, replacing DSPARK's chain step:
14
+
15
+ 1. The draft (one CUDA-graph replay) drafts up to N blocks per request from
16
+ independent noise in one forward, merges them into a trie and keeps the
17
+ block_size nodes the most candidates pass through, in EAGLE's tree encoding
18
+ (``tree.build_tree_abi``). With the root that is DSPARK's own
19
+ verify width, so the target's verify graphs and KV reservation are unchanged.
20
+ 2. ``build_tree_kernel_efficient`` turns it into the tree mask, positions and
21
+ child/sibling links; the target verifies the tree in one forward over the
22
+ request's window ``req_to_token[prefix : prefix + W]``.
23
+ 3. ``verify_tree_greedy`` walks the tree along the target's argmax. Instead of
24
+ moving the accepted nodes' KV, the window's ``req_to_token`` entries are
25
+ reordered so the accepted path comes first (``tree_commit``, one launch);
26
+ the path's hidden states become draft context there.
27
+
28
+ ``FLASHMAP_SPLITKV_VERIFY=1`` (with ``--attention-backend triton``) runs the
29
+ target's verify attention with ``verify_attention.SplitKVVerify``: GQA
30
+ rows packed per KV head, the prefix split across programs, the tree mask only
31
+ on each request's W x W block.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import logging
37
+ import os
38
+
39
+ import torch
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ TREE_ENABLED = int(os.environ.get("FLASHMAP_TREE_N", "0")) >= 1
44
+ SPLITKV_ENABLED = os.environ.get("FLASHMAP_SPLITKV_VERIFY", "0") == "1"
45
+
46
+ if TREE_ENABLED or SPLITKV_ENABLED:
47
+ from . import check_sglang_version
48
+
49
+ check_sglang_version()
50
+
51
+ from sglang.srt.layers.attention import flashattention_backend, triton_backend
52
+ from sglang.srt.lora.layers import unwrap_lora_layer
53
+ from sglang.srt.managers.scheduler import GenerationBatchResult, Scheduler
54
+ from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode
55
+ from sglang.srt.speculative import dflash_utils
56
+ from sglang.srt.speculative.dflash_info import DFlashVerifyInput
57
+ from sglang.srt.speculative.dflash_info_v2 import DFlashDraftInputV2
58
+ from sglang.srt.speculative.dflash_utils import (
59
+ apply_dflash_verify_logits_adjustments,
60
+ resolve_dflash_verify_mask_policy,
61
+ )
62
+ from sglang.srt.speculative.dspark_components import dspark_worker_v2
63
+ from sglang.srt.speculative.dspark_components.dspark_draft import (
64
+ make_next_draft_input,
65
+ )
66
+ from sglang.srt.speculative.dspark_components.dspark_planner import (
67
+ alloc_verify_window,
68
+ )
69
+ from sglang.srt.speculative.eagle_utils import (
70
+ TreeMaskMode,
71
+ build_tree_kernel_efficient,
72
+ verify_tree_greedy_func,
73
+ )
74
+
75
+ from .kernels import tree_commit
76
+ from .verify_attention import SplitKVVerify
77
+
78
+
79
+ def _install_splitkv() -> None:
80
+ """The split-KV verify on the target's Triton backend (sglang routes a
81
+ verify through ``verify_splitkv_fwd`` when ``use_verify_splitkv`` is set,
82
+ upstream on AMD only), installed before the target's graphs are captured."""
83
+ triton_init = triton_backend.TritonAttnBackend.__init__
84
+
85
+ def init(self, model_runner, *args, **kwargs):
86
+ triton_init(self, model_runner, *args, **kwargs)
87
+ if getattr(model_runner, "is_draft_worker", False) or self.use_mla:
88
+ return
89
+ self.use_verify_shared_kv = False
90
+ self.use_verify_splitkv = True
91
+ self._flashmap_splitkv = SplitKVVerify()
92
+ # The tree mask as each request's W x W block (QLEN_ONLY).
93
+ self._flashmap_splitkv.qlen_mask = TREE_ENABLED
94
+ self.verify_splitkv_fwd = torch.compiler.disable(
95
+ self._flashmap_splitkv.__call__
96
+ )
97
+ logger.info("FlashMap split-KV verify attention on the target's Triton backend")
98
+
99
+ triton_backend.TritonAttnBackend.__init__ = init
100
+
101
+
102
+ _CONSTRAINTS = ("json_schema", "regex", "ebnf", "structural_tag")
103
+
104
+
105
+ def tree_refusal(req):
106
+ """Why a tree-mode server refuses ``req`` (an sglang ``Req``), or None.
107
+
108
+ The tree verify walks the target's argmax, so it serves greedy requests
109
+ only; a request that samples, asks for logprobs or constrains its output is
110
+ refused when it arrives rather than failing the batch it would join."""
111
+ params = req.sampling_params
112
+ asks = []
113
+ if params.top_k > 1: # sglang's greedy is top_k 1 (temperature 0)
114
+ asks.append("sampling (temperature > 0)")
115
+ if req.return_logprob:
116
+ asks.append("logprobs")
117
+ if any(getattr(params, name, None) for name in _CONSTRAINTS):
118
+ asks.append("constrained decoding")
119
+ if not asks:
120
+ return None
121
+ return (
122
+ "this server drafts FlashMap trees (FLASHMAP_TREE_N), which serve greedy "
123
+ f"requests (temperature 0) only; the request asks for {' and '.join(asks)}. "
124
+ "Send it with temperature 0, or serve without FLASHMAP_TREE_N."
125
+ )
126
+
127
+
128
+ def _install_tree() -> None:
129
+ # Capture the target's verify graphs with a custom-mask buffer on the
130
+ # backends that read one (DSPARK otherwise captures them mask-free).
131
+ dflash_utils._DFLASH_VERIFY_SKIP_CUSTOM_MASK_BACKENDS = (
132
+ dflash_utils._DFLASH_VERIFY_SKIP_CUSTOM_MASK_BACKENDS
133
+ - {"FlashInferAttnBackend", "TritonAttnBackend"}
134
+ )
135
+ fa3_init = flashattention_backend.FlashAttentionBackend.__init__
136
+
137
+ def fa3_tree_init(self, model_runner, *args, **kwargs):
138
+ """FA3 verifies a tree, as for EAGLE with top-k > 1, by cascade
139
+ attention; every such branch keys on ``topk > 1``, which DSPARK pins to
140
+ 1, so any top-k above 1 selects it on the target."""
141
+ fa3_init(self, model_runner, *args, **kwargs)
142
+ if not model_runner.is_draft_worker and self.topk <= 1:
143
+ self.topk = 2
144
+
145
+ flashattention_backend.FlashAttentionBackend.__init__ = fa3_tree_init
146
+ forward_decode = dspark_worker_v2.DSparkWorkerV2._forward_decode
147
+
148
+ def _forward_decode(self, batch, on_publish, grammar_barrier=None):
149
+ if not getattr(self.draft_model, "tree_mode", False) or (
150
+ batch.forward_mode.is_idle()
151
+ ):
152
+ return forward_decode(self, batch, on_publish, grammar_barrier)
153
+ return _forward_decode_tree(self, batch, on_publish)
154
+
155
+ dspark_worker_v2.DSparkWorkerV2._forward_decode = _forward_decode
156
+
157
+ # Refuse what the tree verify cannot serve when it arrives: every request
158
+ # (new, or out of the grammar queue) enters the waiting queue here. An
159
+ # aborted request is still run once (a one-token prefill, and with overlap
160
+ # scheduling possibly one decode step) before its error is returned, so it
161
+ # is also made greedy (aborting already drops its grammar and logprobs):
162
+ # every batch it joins stays one the tree step serves.
163
+ add_request_to_queue = Scheduler._add_request_to_queue
164
+
165
+ def _add_request_to_queue(self, req, is_retracted: bool = False):
166
+ if not is_retracted and not req.finished():
167
+ refusal = tree_refusal(req)
168
+ if refusal is not None:
169
+ req.set_finish_with_abort(refusal)
170
+ req.sampling_params.top_k = 1
171
+ return add_request_to_queue(self, req, is_retracted)
172
+
173
+ Scheduler._add_request_to_queue = _add_request_to_queue
174
+
175
+
176
+ def _tree_state(self):
177
+ """Per-worker constants of the tree path, checked once."""
178
+ state = getattr(self, "_flashmap_tree", None)
179
+ if state is not None:
180
+ return state
181
+ backend, reads_mask = resolve_dflash_verify_mask_policy(
182
+ self.model_runner.attn_backend
183
+ )
184
+ attn = self.model_runner.attn_backend
185
+ while getattr(attn, "full_attn_backend", None) is not None:
186
+ attn = attn.full_attn_backend
187
+ if not (reads_mask or (backend == "FlashAttentionBackend" and attn.topk > 1)):
188
+ raise RuntimeError(
189
+ "FlashMap tree verify needs a target attention backend that reads a "
190
+ f"tree mask (fa3, flashinfer or triton), got {backend}"
191
+ )
192
+ if self._draft_sampler is not None:
193
+ raise RuntimeError(
194
+ "FlashMap tree verify does its own proposal: set "
195
+ "SGLANG_DSPARK_FOLDED_PROPOSAL=0"
196
+ )
197
+ width = int(self.draft_model.tree_width)
198
+ assert width == self.verify_num_draft_tokens == self.gamma + 1, (
199
+ width,
200
+ self.verify_num_draft_tokens,
201
+ )
202
+ # Reordering a window's req_to_token entries keeps each slot's KV where it
203
+ # is only if every token is its own page.
204
+ if self.model_runner.page_size != 1:
205
+ raise RuntimeError("FlashMap tree verify needs --page-size 1")
206
+ splitkv = getattr(attn, "_flashmap_splitkv", None)
207
+ state = {
208
+ "width": width,
209
+ "mask_mode": (
210
+ TreeMaskMode.QLEN_ONLY
211
+ if getattr(splitkv, "qlen_mask", False)
212
+ else TreeMaskMode.FULL_MASK
213
+ ),
214
+ "offsets": torch.arange(width, device=self.device, dtype=torch.int64),
215
+ "req_to_token": self.model_runner.req_to_token_pool.req_to_token,
216
+ }
217
+ self._flashmap_tree = state
218
+ logger.info(
219
+ "FlashMap tree verify: up to %d candidates per request (budget %d per "
220
+ "step), %d verify tokens, target backend %s",
221
+ self.draft_model.tree_max_n,
222
+ self.draft_model.tree_budget,
223
+ width,
224
+ backend,
225
+ )
226
+ return state
227
+
228
+
229
+ def _forward_decode_tree(self, batch, on_publish):
230
+ if batch.spec_info is None:
231
+ batch.spec_info = DFlashDraftInputV2.create_idle_input(device=self.device)
232
+ draft_input = batch.spec_info
233
+ sampling_info = batch.sampling_info
234
+ if (
235
+ batch.has_grammar
236
+ or batch.return_logprob
237
+ or not (sampling_info is None or sampling_info.is_all_greedy)
238
+ ):
239
+ # The scheduler refuses such requests on arrival (tree_refusal), so
240
+ # this is unreachable. Falling back to DSPARK's chain step is not safe:
241
+ # tried with greedy logprob requests, it broke their outputs on a
242
+ # tree-mode server.
243
+ raise NotImplementedError(
244
+ "FlashMap tree verify (FLASHMAP_TREE_N) serves greedy requests "
245
+ "(temperature 0) without grammar or logprobs only"
246
+ )
247
+ state = _tree_state(self)
248
+ width, depth = state["width"], self.gamma
249
+
250
+ batch.seq_lens.record_stream(torch.get_device_module(self.device).current_stream())
251
+ bs, device = len(batch.seq_lens), self.device
252
+ prefix_lens = batch.seq_lens
253
+ target_model = self.target_worker.model_runner.model
254
+ seq_lens_cpu = (
255
+ batch.seq_lens_cpu
256
+ if batch.seq_lens_cpu is not None
257
+ else draft_input.nxt_kv_lens_cpu
258
+ )
259
+ window = alloc_verify_window(
260
+ batch=batch,
261
+ bs=bs,
262
+ device=device,
263
+ verify_num_draft_tokens=width,
264
+ block_pos_offsets=state["offsets"],
265
+ model_runner=self.model_runner,
266
+ )
267
+
268
+ # 1. Draft: one graph replay fills the draft's tree buffers.
269
+ with self._draft_context():
270
+ self._proposer._run_forward(
271
+ batch=batch,
272
+ draft_input=draft_input,
273
+ verify_window=window,
274
+ bs=bs,
275
+ device=device,
276
+ embed_module=unwrap_lora_layer(target_model.get_input_embeddings()),
277
+ draft_sampler=None,
278
+ sampling_info=sampling_info,
279
+ )
280
+ node_tokens, node_ids, parent_list, _ = self.draft_model.tree_outputs(bs)
281
+
282
+ # 2. The tree's mask, positions and links; verify it.
283
+ (
284
+ tree_mask,
285
+ positions,
286
+ retrieve_index,
287
+ retrieve_next_token,
288
+ retrieve_next_sibling,
289
+ verify_ids,
290
+ ) = build_tree_kernel_efficient(
291
+ draft_input.bonus_tokens.view(-1),
292
+ parent_list,
293
+ node_ids,
294
+ node_tokens,
295
+ prefix_lens,
296
+ int(seq_lens_cpu.sum()) + bs * width,
297
+ topk=width,
298
+ spec_steps=depth,
299
+ num_verify_tokens=width,
300
+ tree_mask_mode=state["mask_mode"],
301
+ )
302
+ verify_input = DFlashVerifyInput(
303
+ draft_token=verify_ids,
304
+ positions=positions,
305
+ draft_token_num=width,
306
+ custom_mask=tree_mask,
307
+ capture_hidden_mode=CaptureHiddenMode.FULL,
308
+ live_seq_lens_cpu=batch.seq_lens_cpu,
309
+ )
310
+ batch.out_cache_loc = window.verify_cache_loc
311
+ executor = self._verify_executor
312
+ seq_lens_cpu_backup, seq_lens_sum_backup = batch.seq_lens_cpu, batch.seq_lens_sum
313
+ if not executor._verify_backend_self_adds_seq_lens():
314
+ if seq_lens_cpu_backup is not None:
315
+ batch.seq_lens_cpu = seq_lens_cpu_backup + width
316
+ batch.seq_lens_sum = int(batch.seq_lens_cpu.sum())
317
+ elif draft_input.nxt_kv_lens_cpu is not None:
318
+ batch.seq_lens_cpu = draft_input.nxt_kv_lens_cpu
319
+ batch.seq_lens_sum = int(draft_input.nxt_kv_lens_sum)
320
+ verified = executor._forward_prepared_verify(
321
+ batch=batch,
322
+ verify_input=verify_input,
323
+ seq_lens_cpu_backup=seq_lens_cpu_backup,
324
+ seq_lens_sum_backup=seq_lens_sum_backup,
325
+ )
326
+ logits_output = verified.logits_output
327
+ logits = logits_output.next_token_logits
328
+ if sampling_info is not None:
329
+ apply_dflash_verify_logits_adjustments(
330
+ next_token_logits=logits, sampling_info=sampling_info, draft_token_num=width
331
+ )
332
+
333
+ # 3. Accept the longest path the target agrees with.
334
+ predict = torch.zeros(bs * width, dtype=torch.int32, device=device)
335
+ accept_index = torch.full((bs, depth + 1), -1, dtype=torch.int32, device=device)
336
+ num_correct = torch.empty((bs,), dtype=torch.int32, device=device)
337
+ verify_tree_greedy_func(
338
+ predicts=predict,
339
+ accept_index=accept_index,
340
+ accept_token_num=num_correct,
341
+ candidates=verify_ids.view(bs, width),
342
+ retrieve_index=retrieve_index,
343
+ retrieve_next_token=retrieve_next_token,
344
+ retrieve_next_sibling=retrieve_next_sibling,
345
+ target_predict=logits.argmax(dim=-1).view(bs, width),
346
+ )
347
+ # 4. Commit without moving KV: reorder each request's window so the
348
+ # accepted path (root first) is its first commit_lens slots, where its KV
349
+ # already is; the path's hidden states become draft context there (rows off
350
+ # the path go to the pool's dummy slot 0).
351
+ out_tokens, bonus, commit_lens, new_seq_lens, inject_locs = tree_commit(
352
+ accept_index,
353
+ num_correct,
354
+ window.verify_cache_loc_2d,
355
+ predict,
356
+ batch.req_pool_indices,
357
+ prefix_lens,
358
+ state["req_to_token"],
359
+ )
360
+ # sglang's scheduler assumes a forward only reads req_to_token: its
361
+ # write-after-read barrier must wait for this write too.
362
+ reordered = torch.get_device_module(self.device).Event()
363
+ reordered.record()
364
+ self.model_runner.shared_read_done_event = reordered
365
+ if on_publish is not None:
366
+ on_publish(new_seq_lens)
367
+ executor.kv_injector.inject_target_hidden(
368
+ target_hidden=logits_output.hidden_states,
369
+ cache_loc=inject_locs,
370
+ positions=positions,
371
+ )
372
+ logits_output.hidden_states = None
373
+ return GenerationBatchResult(
374
+ logits_output=logits_output,
375
+ next_token_ids=out_tokens.reshape(-1),
376
+ accept_lens=commit_lens,
377
+ block_accept_lens=commit_lens,
378
+ cap_lens=None,
379
+ can_run_cuda_graph=verified.can_run_cuda_graph,
380
+ next_draft_input=make_next_draft_input(
381
+ bonus_tokens=bonus, new_seq_lens=new_seq_lens
382
+ ),
383
+ speculative_num_draft_tokens=int(self.verify_num_draft_tokens),
384
+ new_seq_lens=new_seq_lens,
385
+ )
386
+
387
+
388
+ # Installed once, however often the module is imported.
389
+ if SPLITKV_ENABLED and not getattr(
390
+ triton_backend, "_flashmap_sglang_splitkv", False
391
+ ):
392
+ _install_splitkv()
393
+ triton_backend._flashmap_sglang_splitkv = True
394
+ if TREE_ENABLED and not getattr(dspark_worker_v2, "_flashmap_sglang_tree", False):
395
+ _install_tree()
396
+ dspark_worker_v2._flashmap_sglang_tree = True