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,78 @@
|
|
|
1
|
+
"""Serve FlashMap drafts in sglang (``pip install "flashmap[sglang]"``).
|
|
2
|
+
|
|
3
|
+
sglang's DSPARK speculative worker drafts with :class:`.model.FlashMapDraftModel`,
|
|
4
|
+
which serves a FlashMap release folder as it is:
|
|
5
|
+
|
|
6
|
+
python -m sglang.launch_server --model-path Qwen/Qwen3-8B \\
|
|
7
|
+
--speculative-algorithm DSPARK \\
|
|
8
|
+
--speculative-draft-model-path j-shen/FlashMap-Qwen3-8B
|
|
9
|
+
|
|
10
|
+
Installing the package registers :func:`register` as an sglang plugin (entry point
|
|
11
|
+
group ``sglang.srt.plugins``), which sglang runs at startup in every server
|
|
12
|
+
process. It registers the ``flashmap`` config with transformers and the model
|
|
13
|
+
with sglang's model registry, by naming this package in
|
|
14
|
+
``SGLANG_EXTERNAL_MODEL_PACKAGE`` (sglang then imports every module here and
|
|
15
|
+
registers its ``EntryClass``). Setting ``SGLANG_EXTERNAL_MODEL_PACKAGE=flashmap.sglang``
|
|
16
|
+
by hand does the same where sglang's plugins do not run.
|
|
17
|
+
|
|
18
|
+
The model relies on internals of one sglang release (``SUPPORTED_SGLANG``). On
|
|
19
|
+
another release the plugin leaves sglang untouched and logs why;
|
|
20
|
+
``FLASHMAP_SGLANG_ANY_VERSION=1`` tries anyway.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
SUPPORTED_SGLANG = "0.5.20"
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _sglang_version() -> str:
|
|
33
|
+
from importlib.metadata import version
|
|
34
|
+
|
|
35
|
+
return version("sglang")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_sglang_version() -> None:
|
|
39
|
+
"""Raise unless the installed sglang is the release this package targets
|
|
40
|
+
(``FLASHMAP_SGLANG_ANY_VERSION=1`` skips the check)."""
|
|
41
|
+
found = _sglang_version()
|
|
42
|
+
if found == SUPPORTED_SGLANG or os.environ.get("FLASHMAP_SGLANG_ANY_VERSION") == "1":
|
|
43
|
+
return
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
f"flashmap.sglang needs sglang=={SUPPORTED_SGLANG} (it uses internals of its "
|
|
46
|
+
f"DSPARK worker), found {found}: pip install sglang=={SUPPORTED_SGLANG}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register() -> None:
|
|
51
|
+
"""The sglang plugin: makes FlashMap release folders servable as draft models.
|
|
52
|
+
|
|
53
|
+
Idempotent. Loading it imported ``flashmap``, which registers the
|
|
54
|
+
``flashmap`` config with transformers (so sglang parses a release folder's
|
|
55
|
+
config.json without ``--trust-remote-code``). It names this package in
|
|
56
|
+
``SGLANG_EXTERNAL_MODEL_PACKAGE``, which the server's processes inherit and
|
|
57
|
+
sglang's model registry reads when it is first imported; a registry
|
|
58
|
+
imported already, or one told to load another package, gets the model
|
|
59
|
+
here."""
|
|
60
|
+
try:
|
|
61
|
+
check_sglang_version()
|
|
62
|
+
except RuntimeError as error:
|
|
63
|
+
logger.warning("FlashMap drafts are not available: %s", error)
|
|
64
|
+
return
|
|
65
|
+
current = os.environ.get("SGLANG_EXTERNAL_MODEL_PACKAGE")
|
|
66
|
+
if not current:
|
|
67
|
+
os.environ["SGLANG_EXTERNAL_MODEL_PACKAGE"] = __name__
|
|
68
|
+
if (current and current != __name__) or "sglang.srt.models.registry" in sys.modules:
|
|
69
|
+
from sglang.srt.models.registry import ModelRegistry
|
|
70
|
+
|
|
71
|
+
ModelRegistry.register(__name__, overwrite=True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# sglang imports this package when SGLANG_EXTERNAL_MODEL_PACKAGE names it; a
|
|
75
|
+
# version mismatch then stops the server with the reason (sglang would otherwise
|
|
76
|
+
# log and skip the model module, and fail later on an unsupported architecture).
|
|
77
|
+
if os.environ.get("SGLANG_EXTERNAL_MODEL_PACKAGE") == __name__:
|
|
78
|
+
check_sglang_version()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Multi-jump FlashMap drafting without vocabulary-sized matmuls.
|
|
3
|
+
|
|
4
|
+
A flow map's jump ``n >= 1`` starts from the vocabulary-sized state
|
|
5
|
+
|
|
6
|
+
x_n = alpha_n z + sum_m beta_nm delta_m, z ~ N(0, T^2 I_V),
|
|
7
|
+
|
|
8
|
+
(``z`` the draft's starting noise at temperature ``T``, ``delta_m`` the earlier
|
|
9
|
+
jumps' predictions, ``alpha_n = 1 - t_n`` and ``beta_nm`` from the jump update
|
|
10
|
+
``x <- ((1 - t) x + (t - s) delta) / (1 - s)``) and embeds it as
|
|
11
|
+
|
|
12
|
+
state_norm(softmax(e_n x_n) @ E) + gate * noise_norm(x_n @ E),
|
|
13
|
+
e_n = t_n / (1 - t_n)^2.
|
|
14
|
+
|
|
15
|
+
Done literally that is two ``(V, H)`` matmuls per jump, plus a third for the
|
|
16
|
+
first jump's noise and a full-vocabulary head for each ``delta_m``. Off the
|
|
17
|
+
predictions' support ``S`` (their top-k tokens) the posterior's logits are
|
|
18
|
+
``a_n z_v`` with ``a_n = t_n / (1 - t_n)``; on it ``a_n z_v + c_v`` with ``c_v =
|
|
19
|
+
e_n sum_m beta_nm delta_m,v >= 0``. So, exactly,
|
|
20
|
+
|
|
21
|
+
softmax(e_n x_n) @ E = (W + sum_{v in S} r_v E_v) / (Z + sum_{v in S} r_v),
|
|
22
|
+
W = sum_v e^{a_n z_v} E_v, Z = sum_v e^{a_n z_v}, r_v = e^{a_n z_v} (e^{c_v} - 1),
|
|
23
|
+
x_n @ E = alpha_n z @ E + sum_m beta_nm delta_m @ E.
|
|
24
|
+
|
|
25
|
+
Everything over the whole vocabulary depends on ``z`` alone: ``z @ E`` and, per
|
|
26
|
+
jump, ``W / Z`` and ``log Z``. :class:`JumpNoiseBank` precomputes them for ``M``
|
|
27
|
+
draws of ``z``; a draft takes a bank row per slot and then reads only the
|
|
28
|
+
embedding rows of the support. The approximations:
|
|
29
|
+
|
|
30
|
+
1. ``delta_m`` comes from the draft-vocabulary head, cut to its top-k;
|
|
31
|
+
2. on ``S``, ``z_v`` is drawn afresh (a hash of the slot's seed and the token,
|
|
32
|
+
so a token keeps its value from jump to jump) instead of read from the
|
|
33
|
+
row, whose sums keep their own term for ``v`` (one of ``V`` terms);
|
|
34
|
+
3. the bank is finite: a slot's noise is one of ``M`` i.i.d. draws.
|
|
35
|
+
|
|
36
|
+
Banked top-16 2-jump drafts match exact 2-jump drafts in accepted length on
|
|
37
|
+
every domain we evaluated.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import math
|
|
43
|
+
from typing import Optional
|
|
44
|
+
|
|
45
|
+
import torch
|
|
46
|
+
|
|
47
|
+
_MASK32 = 0xFFFFFFFF
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _hash32(x: torch.Tensor) -> torch.Tensor:
|
|
51
|
+
"""An integer hash of int64 values in [0, 2^32) (two multiply-xorshift
|
|
52
|
+
rounds; every product stays below 2^59, so int64 arithmetic is exact)."""
|
|
53
|
+
x = x ^ (x >> 16)
|
|
54
|
+
x = (x * 0x45D9F3B) & _MASK32
|
|
55
|
+
x = x ^ (x >> 16)
|
|
56
|
+
x = (x * 0x45D9F3B) & _MASK32
|
|
57
|
+
return x ^ (x >> 16)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def token_normals(seeds: torch.Tensor, ids: torch.Tensor) -> torch.Tensor:
|
|
61
|
+
"""A standard normal per (slot seed, token id): ``seeds (R,)`` and ``ids (R,
|
|
62
|
+
L)`` int64 -> float32 ``(R, L)``, a function of the pair only (Box-Muller on
|
|
63
|
+
two hashed uniforms)."""
|
|
64
|
+
base = _hash32((ids & _MASK32) ^ 0x9E3779B9)
|
|
65
|
+
h1 = _hash32(base ^ (seeds.unsqueeze(-1) & _MASK32))
|
|
66
|
+
h2 = _hash32(h1 ^ 0x68E31DA4)
|
|
67
|
+
u1 = (h1.float() + 1.0) / 4294967297.0
|
|
68
|
+
u2 = h2.float() / 4294967296.0
|
|
69
|
+
return torch.sqrt(-2.0 * torch.log(u1)) * torch.cos(2.0 * math.pi * u2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def jump_coefficients(grid):
|
|
73
|
+
"""Per jump ``n``: ``(alpha_n, [beta_n1, ..., beta_nn])``, the state's
|
|
74
|
+
coefficients of ``z`` and of each earlier prediction."""
|
|
75
|
+
alpha, betas, out = 1.0, [], [(1.0, [])]
|
|
76
|
+
for n in range(len(grid) - 2):
|
|
77
|
+
start, end = float(grid[n]), float(grid[n + 1])
|
|
78
|
+
keep = (1.0 - end) / (1.0 - start)
|
|
79
|
+
alpha = alpha * keep
|
|
80
|
+
betas = [b * keep for b in betas] + [(end - start) / (1.0 - start)]
|
|
81
|
+
out.append((alpha, list(betas)))
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def top_predictions(
|
|
86
|
+
logits: torch.Tensor, topk: int, hot_ids: Optional[torch.Tensor] = None
|
|
87
|
+
):
|
|
88
|
+
"""A jump's prediction over ``logits``' vocabulary (all tokens, or the sorted
|
|
89
|
+
``hot_ids`` the logits cover), cut to its ``topk``: ``(token ids, probs)``."""
|
|
90
|
+
probs, index = logits.float().softmax(dim=-1).topk(topk, dim=-1)
|
|
91
|
+
return (index if hot_ids is None else hot_ids[index]), probs
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class JumpNoiseBank:
|
|
95
|
+
"""``size`` draws of a multi-jump draft's starting noise with what its jumps
|
|
96
|
+
need from them over the whole vocabulary (see the module docstring).
|
|
97
|
+
|
|
98
|
+
``draft`` is the sglang :class:`FlashMapDraftModel`; its ``embedding`` (the
|
|
99
|
+
target's token embedding), ``state_norm``/``noise_norm`` and
|
|
100
|
+
``noise_gate_at`` are read."""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
draft,
|
|
105
|
+
grid,
|
|
106
|
+
*,
|
|
107
|
+
size: int = 8192,
|
|
108
|
+
temperature: float = 1.0,
|
|
109
|
+
seed: int = 0,
|
|
110
|
+
chunk: int = 128,
|
|
111
|
+
):
|
|
112
|
+
embedding = draft.embedding
|
|
113
|
+
device, dtype = embedding.device, embedding.dtype
|
|
114
|
+
steps = len(grid) - 1
|
|
115
|
+
self.grid = [float(t) for t in grid]
|
|
116
|
+
self.size, self.temperature, self.steps = int(size), float(temperature), steps
|
|
117
|
+
self.coefficients = jump_coefficients(self.grid)
|
|
118
|
+
vocab, hidden = embedding.shape
|
|
119
|
+
times = self.grid[1:steps]
|
|
120
|
+
self.evidence = [t / (1.0 - t) ** 2 for t in times] # e_n
|
|
121
|
+
# a_n = e_n alpha_n (= t_n / (1 - t_n)); z carries the temperature.
|
|
122
|
+
self.slopes = [
|
|
123
|
+
e * self.coefficients[n + 1][0] for n, e in enumerate(self.evidence)
|
|
124
|
+
]
|
|
125
|
+
self.noise_embed = torch.empty(self.size, hidden, dtype=dtype, device=device)
|
|
126
|
+
self.mean_embed = torch.empty(
|
|
127
|
+
steps - 1, self.size, hidden, dtype=dtype, device=device
|
|
128
|
+
)
|
|
129
|
+
self.log_mass = torch.empty(
|
|
130
|
+
steps - 1, self.size, dtype=torch.float32, device=device
|
|
131
|
+
)
|
|
132
|
+
generator = torch.Generator(device=device).manual_seed(int(seed))
|
|
133
|
+
with torch.no_grad():
|
|
134
|
+
for start in range(0, self.size, chunk):
|
|
135
|
+
rows = min(chunk, self.size - start)
|
|
136
|
+
z = self.temperature * torch.randn(
|
|
137
|
+
rows, vocab, generator=generator, device=device
|
|
138
|
+
)
|
|
139
|
+
self.noise_embed[start : start + rows] = z.to(dtype) @ embedding
|
|
140
|
+
for n, slope in enumerate(self.slopes):
|
|
141
|
+
logits = slope * z
|
|
142
|
+
self.log_mass[n, start : start + rows] = logits.logsumexp(-1)
|
|
143
|
+
self.mean_embed[n, start : start + rows] = (
|
|
144
|
+
logits.softmax(dim=-1).to(dtype) @ embedding
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def nbytes(self) -> int:
|
|
148
|
+
return sum(
|
|
149
|
+
t.numel() * t.element_size()
|
|
150
|
+
for t in (self.noise_embed, self.mean_embed, self.log_mass)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def draw(self, slots: int, device):
|
|
154
|
+
"""A bank row and a seed per slot."""
|
|
155
|
+
rows = torch.randint(self.size, (slots,), device=device)
|
|
156
|
+
seeds = torch.randint(2**31 - 1, (slots,), device=device)
|
|
157
|
+
return rows, seeds
|
|
158
|
+
|
|
159
|
+
def first_input(self, draft, rows: torch.Tensor, uniform: torch.Tensor):
|
|
160
|
+
"""The embedded starting state of slots ``rows`` at ``t = 0``: the
|
|
161
|
+
uniform posterior's embedding ``uniform (H,)`` plus the gated noise."""
|
|
162
|
+
gate = draft.noise_gate_at(self.grid[0], self.grid[1])
|
|
163
|
+
out = uniform.expand(rows.numel(), -1)
|
|
164
|
+
return out + gate * draft.noise_norm(self.noise_embed[rows])
|
|
165
|
+
|
|
166
|
+
def jump_input(self, draft, n: int, rows, seeds, supports) -> torch.Tensor:
|
|
167
|
+
"""The embedded state of jump ``n >= 1`` for slots ``rows (R,)``;
|
|
168
|
+
``supports``: per earlier prediction ``m``, its top tokens ``(ids (R, k),
|
|
169
|
+
probs (R, k))``. Returns ``(R, H)`` in the embedding's dtype."""
|
|
170
|
+
embedding = draft.embedding
|
|
171
|
+
alpha, betas = self.coefficients[n]
|
|
172
|
+
ids = torch.cat([ids for ids, _ in supports], dim=1) # (R, L)
|
|
173
|
+
# Each slot's contribution to the state x_n at its token: beta_nm delta_m.
|
|
174
|
+
linear = torch.cat(
|
|
175
|
+
[beta * probs.float() for beta, (_, probs) in zip(betas, supports)], dim=1
|
|
176
|
+
)
|
|
177
|
+
same = ids.unsqueeze(-1) == ids.unsqueeze(-2) # (R, L, L)
|
|
178
|
+
# c_v sums every prediction's weight on v; a token listed twice counts once.
|
|
179
|
+
shift = self.evidence[n - 1] * (same.float() @ linear.unsqueeze(-1)).squeeze(-1)
|
|
180
|
+
width = ids.shape[1]
|
|
181
|
+
earlier = torch.ones(width, width, dtype=torch.bool, device=ids.device).tril(-1)
|
|
182
|
+
first = ~(same & earlier).any(dim=-1)
|
|
183
|
+
z = self.temperature * token_normals(seeds, ids)
|
|
184
|
+
log_mass = self.log_mass[n - 1][rows].unsqueeze(-1)
|
|
185
|
+
# r_v / Z
|
|
186
|
+
weight = (
|
|
187
|
+
torch.exp(self.slopes[n - 1] * z - log_mass) * torch.expm1(shift) * first
|
|
188
|
+
)
|
|
189
|
+
# Both sums over the support's embedding rows in one batched matmul
|
|
190
|
+
# (bf16 operands, float32 accumulation): (R, 2, L) @ (R, L, H).
|
|
191
|
+
sums = torch.bmm(
|
|
192
|
+
torch.stack([weight, linear], dim=1).to(embedding.dtype), embedding[ids]
|
|
193
|
+
).float()
|
|
194
|
+
posterior = (self.mean_embed[n - 1][rows].float() + sums[:, 0]) / (
|
|
195
|
+
1.0 + weight.sum(dim=1, keepdim=True)
|
|
196
|
+
)
|
|
197
|
+
out = draft.state_norm(posterior.to(embedding.dtype))
|
|
198
|
+
gate = draft.noise_gate_at(self.grid[n], self.grid[n + 1])
|
|
199
|
+
noise = alpha * self.noise_embed[rows].float() + sums[:, 1]
|
|
200
|
+
return out + gate * draft.noise_norm(noise.to(embedding.dtype))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
__all__ = ["JumpNoiseBank", "jump_coefficients", "token_normals", "top_predictions"]
|