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
|
@@ -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"]
|
flashmap/sglang/tree.py
ADDED
|
@@ -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
|