File size: 12,741 Bytes
e1acc15 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | """Exact finite-vocabulary speculative sampling, standard tokenwise verification.
Fractions define probabilities; randrange uses integer rejection internally. Thus
there is no float acceptance threshold or CDF-rounding approximation here. This
is a small-vocabulary oracle, not a practical GPU implementation. Models and
processors are pure functions of the supplied prefix, never mutable KV/grammar
objects. Nothing loads a checkpoint, imports torch, or connects to a service.
"""
from __future__ import annotations
import hashlib
import math
import random
import struct
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from fractions import Fraction
Weight = int | float | Fraction
Prefix = tuple[int, ...]
def _fraction(value: Weight) -> Fraction:
if isinstance(value, bool) or not isinstance(value, (int, float, Fraction)):
raise TypeError("weights must be real int/float/Fraction values")
if isinstance(value, float) and not math.isfinite(value):
raise ValueError("non-finite weight")
result = Fraction(value)
if result < 0:
raise ValueError("negative weight")
return result
def _token(token: int, vocab_size: int) -> None:
if type(token) is not int or not 0 <= token < vocab_size:
raise ValueError(f"global token ID {token!r} outside vocabulary {vocab_size}")
@dataclass(frozen=True, init=False)
class Categorical:
"""Normalized exact distribution; unlisted global IDs have exactly zero mass.
Sparse entries must be unique, including entries with zero weight. A full q
is constructed with dense(); sparse_topk() chooses support BEFORE sampling.
"""
vocab_size: int
ids: tuple[int, ...]
probabilities: tuple[Fraction, ...]
tickets: tuple[int, ...]
def __init__(self, vocab_size: int, ids: Sequence[int], weights: Sequence[Weight]):
if type(vocab_size) is not int or vocab_size < 1:
raise ValueError("vocabulary size must be a positive integer")
if len(ids) != len(weights) or not ids:
raise ValueError("support and weights must have matching nonempty lengths")
for token in ids:
_token(token, vocab_size)
if len(set(ids)) != len(ids):
raise ValueError("duplicate global token ID")
entries = sorted(zip(ids, map(_fraction, weights)))
mass = sum((weight for _, weight in entries), Fraction())
if mass == 0:
raise ValueError("empty effective support (all zero or all masked)")
probs = tuple(weight / mass for _, weight in entries)
denominator = math.lcm(*(p.denominator for p in probs))
tickets = tuple(p.numerator * (denominator // p.denominator) for p in probs)
object.__setattr__(self, "vocab_size", vocab_size)
object.__setattr__(self, "ids", tuple(token for token, _ in entries))
object.__setattr__(self, "probabilities", probs)
object.__setattr__(self, "tickets", tickets)
@classmethod
def dense(cls, weights: Sequence[Weight]) -> Categorical:
return cls(len(weights), tuple(range(len(weights))), weights)
def prob(self, token: int) -> Fraction:
_token(token, self.vocab_size)
# Intentionally simple and exact; production sparse lookup needs a kernel.
return next((p for i, p in zip(self.ids, self.probabilities) if i == token), Fraction())
def sample(self, rng: random.Random) -> int:
draw = rng.randrange(sum(self.tickets))
for token, count in zip(self.ids, self.tickets):
if draw < count:
return token
draw -= count
raise AssertionError("categorical integer sampler exhausted its support")
def masked(self, allowed: Sequence[bool]) -> Categorical:
if len(allowed) != self.vocab_size or any(type(x) is not bool for x in allowed):
raise ValueError("mask must have one boolean per target vocabulary token")
return Categorical(self.vocab_size, self.ids,
tuple(p if allowed[i] else 0 for i, p in zip(self.ids, self.probabilities)))
def sparse_topk(self, k: int) -> Categorical:
if type(k) is not int or not 1 <= k <= self.vocab_size:
raise ValueError("top-k outside vocabulary")
# Global lowest ID breaks ties, independent of TP ownership/order.
selected = sorted(zip(self.ids, self.probabilities), key=lambda item: (-item[1], item[0]))[:k]
return Categorical(self.vocab_size, tuple(i for i, _ in selected), tuple(p for _, p in selected))
def fp32_weights(vocab_size: int, ids: Sequence[int], weights: Sequence[Weight]) -> Categorical:
"""Quantize BEFORE sampling, then normalize the exact FP32 values identically.
This is the semantics oracle for transported FP32 weights. CPU normalization
here is rational, not an assertion of bit-parity with a future GPU softmax.
"""
rounded: list[float] = []
for weight in weights:
_fraction(weight)
try:
rounded.append(struct.unpack("<f", struct.pack("<f", float(weight)))[0])
except (OverflowError, struct.error) as error:
raise ValueError("weight cannot be represented as finite FP32") from error
return Categorical(vocab_size, ids, rounded)
def acceptance(p: Categorical, q: Categorical, token: int) -> Fraction:
if p.vocab_size != q.vocab_size:
raise ValueError("target/proposal vocabulary mismatch")
qx = q.prob(token)
if qx == 0:
raise ValueError("proposal token has zero q mass")
return min(Fraction(1), p.prob(token) / qx)
def residual(p: Categorical, q: Categorical) -> Categorical:
"""(p-q)+ over the FULL target vocabulary, never just transmitted support."""
if p.vocab_size != q.vocab_size:
raise ValueError("target/proposal vocabulary mismatch")
return Categorical.dense(tuple(max(p.prob(i) - q.prob(i), Fraction())
for i in range(p.vocab_size)))
@dataclass(frozen=True)
class Owner:
request_id: str
generation: int
def __post_init__(self) -> None:
if not isinstance(self.request_id, str) or not self.request_id or type(self.generation) is not int or self.generation < 0:
raise ValueError("invalid request/generation owner")
class RandomStreams:
"""Request/generation/round-owned, domain-separated reproducible CPU streams.
Creating streams consumes no process-global RNG. A verifier never uses the
proposal generator as its acceptance or correction generator. Owner binding
and block freshness are checked before draws. Not a GPU Philox replacement.
"""
def __init__(self, seed: int, owner: Owner, round_id: int):
if type(round_id) is not int or round_id < 0:
raise ValueError("round ID must be nonnegative")
self.owner = owner
self.round_id = round_id
self.proposal = self._make(seed, "proposal")
self.acceptance = self._make(seed, "acceptance")
self.correction = self._make(seed, "correction-or-bonus")
def _make(self, seed: int, domain: str) -> random.Random:
# Length-aware repr of a typed tuple prevents ambiguous field concatenation.
key = repr((seed, self.owner.request_id, self.owner.generation, self.round_id, domain)).encode()
return random.Random(int.from_bytes(hashlib.sha256(key).digest(), "big"))
Model = Callable[[Prefix], Categorical]
Processor = Callable[[Prefix, Categorical], Categorical]
def identity_processor(prefix: Prefix, distribution: Categorical) -> Categorical:
return distribution
@dataclass(frozen=True)
class Proposal:
token: int
q: Categorical
@dataclass(frozen=True)
class Block:
owner: Owner
round_id: int
prefix: Prefix
context_id: str
fixed_depth: int
proposals: tuple[Proposal, ...]
stop_ids: frozenset[int]
def propose(model: Model, prefix: Prefix, streams: RandomStreams, *, depth: int,
context_id: str, stop_ids: frozenset[int] = frozenset()) -> Block:
"""Choose depth before any draws; q_i may depend on earlier sampled tokens."""
if type(depth) is not int or not 1 <= depth <= 5:
raise ValueError("reference fixed depth must be in 1..5")
if not context_id:
raise ValueError("processor/model context identity is required")
proposals: list[Proposal] = []
current = prefix
for _ in range(depth):
q = model(current)
for token in (*current, *stop_ids):
_token(token, q.vocab_size)
token = q.sample(streams.proposal)
proposals.append(Proposal(token, q))
current += (token,)
if token in stop_ids:
break
return Block(streams.owner, streams.round_id, prefix, context_id, depth, tuple(proposals), stop_ids)
@dataclass(frozen=True)
class Result:
tokens: tuple[int, ...]
accepted_drafts: int
rejected: bool
stopped: bool
class Verifier:
"""Single request session: commit only accepted prefix + correction/bonus.
Processor callbacks must be pure functions of the *supplied* prefix. A real
engine integration must clone/rollback grammar, penalty, KV and Markov state.
Invalid state raises; no missing-q or target-only fallback is hidden here.
"""
def __init__(self, owner: Owner, prefix: Prefix, context_id: str, target: Model, *,
processor: Processor = identity_processor, stop_ids: frozenset[int] = frozenset()):
self.owner = owner
self.prefix = prefix
self.context_id = context_id
self.target = target
self.processor = processor
self.stop_ids = stop_ids
self.last_round = -1
self.stopped = False
self.failed = False
def verify(self, block: Block, streams: RandomStreams) -> Result:
if self.stopped:
raise ValueError("request has already stopped")
if self.failed:
raise ValueError("failed verification requires explicit session reset")
if block.owner != self.owner or streams.owner != self.owner:
raise ValueError("request/generation RNG or block ownership mismatch")
if block.round_id != streams.round_id or block.round_id <= self.last_round:
raise ValueError("stale/reused round or wrong RNG round")
if block.prefix != self.prefix or block.context_id != self.context_id:
raise ValueError("stale prefix or changed model/processor context")
if block.stop_ids != self.stop_ids:
raise ValueError("stop policy mismatch")
if type(block.fixed_depth) is not int or not 1 <= block.fixed_depth <= 5 or not 1 <= len(block.proposals) <= block.fixed_depth:
raise ValueError("invalid fixed-depth block")
if len(block.proposals) != block.fixed_depth and block.proposals[-1].token not in self.stop_ids:
raise ValueError("depth may shorten only on a terminal token")
vocab = block.proposals[0].q.vocab_size
for proposal in block.proposals:
if proposal.q.vocab_size != vocab or proposal.q.prob(proposal.token) == 0:
raise ValueError("invalid proposal distribution/token")
for token in (*self.prefix, *self.stop_ids):
_token(token, vocab)
# Poison this round before consuming RNG: a failed callback must not allow
# a retry with partially advanced randomness. Recovery is explicit reset.
self.last_round = block.round_id
self.failed = True
current = self.prefix
output: list[int] = []
accepted = 0
rejected = False
for proposal in block.proposals:
p = self.processor(current, self.target(current))
a = acceptance(p, proposal.q, proposal.token)
if streams.acceptance.randrange(a.denominator) < a.numerator:
token = proposal.token
accepted += 1
else:
token = residual(p, proposal.q).sample(streams.correction)
rejected = True
output.append(token)
current += (token,)
if rejected or token in self.stop_ids:
break
if not rejected and output[-1] not in self.stop_ids:
p = self.processor(current, self.target(current))
if p.vocab_size != vocab:
raise ValueError("bonus target vocabulary mismatch")
token = p.sample(streams.correction)
output.append(token)
current += (token,)
self.prefix = current
self.stopped = output[-1] in self.stop_ids
self.failed = False
return Result(tuple(output), accepted, rejected, self.stopped)
|