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,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"]