File size: 18,608 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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
"""Stateless Phase 2a native drafter. All windows use absolute positions.

Forward semantics follow the sha-pinned official model.py read by V1. Inputs
are committed target taps or explicitly marked engine main_x fallback rows.
The stages never detach/reuse a conditioning cache across optimizer steps.
"""
from __future__ import annotations

from dataclasses import dataclass
import hashlib
import json
from typing import Any, Mapping, Protocol, cast

import torch
from torch import Tensor, nn
from torch.nn import functional as F
from torch.utils.checkpoint import checkpoint

from ...native.formats import decode_fp8_block
from ...native.official import OfficialShards
from .ops import Dense, PackedLinear, collapse, expand, frozen_linear, grouped_linear, mixes, qdq, rms, rope


class Source(Protocol):
    def get(self, name: str) -> Tensor: ...
    def info(self, name: str) -> dict[str, object]: ...
    def verify(self, name: str) -> None: ...


@dataclass(frozen=True)
class Config:
    dim: int = 5120
    inter: int = 2304
    stages: int = 3
    vocab: int = 129280
    block: int = 5
    noise: int = 128799
    hc: int = 4
    heads: int = 64
    hd: int = 512
    rd: int = 64
    qr: int = 1280
    groups: int = 8
    outr: int = 1024
    experts: int = 128
    topk: int = 3
    markov: int = 256
    window: int = 128
    eps: float = 1e-20
    hc_eps: float = 1e-6
    sinkhorn: int = 20
    theta: float = 10000.0
    route_scale: float = 1.5
    gate_temp: float = 1.0
    limit: float = 10.0


def key(name: str) -> str:
    return name.replace(".", "__")


class Tensors(nn.Module):
    def __init__(self, source: Source, device: torch.device, reference_math: bool = False):
        super().__init__()
        self.source, self.device = source, device
        self.reference_math = reference_math

    def read(self, name: str) -> Tensor:
        self.source.verify(name)
        return self.source.get(name).to(self.device)

    def add(self, name: str, train: bool = False, bf16: bool = False) -> Tensor:
        t = self.read(name).to(torch.bfloat16 if bf16 else torch.float32)
        if self.reference_math and not bf16:
            t = t.cpu()
        if not bool(torch.isfinite(t).all()):
            raise ValueError(f"nonfinite {name}")
        if train:
            self.register_parameter(key(name), nn.Parameter(t))
        else:
            self.register_buffer(key(name), t)
        return t

    def __getitem__(self, name: str) -> Tensor:
        return cast(Tensor, getattr(self, key(name)))

    def dense(self, name: str, rank: int, acc: torch.dtype, quant_input: bool = True) -> Dense:
        w = self.read(name + ".weight")
        if self.source.info(name + ".weight")["dtype"] == "F8_E4M3":
            w = decode_fp8_block(w, self.read(name + ".scale"))
        return Dense(w, rank, quant_input, acc)


class Stage(nn.Module):
    wo_a_base: Tensor

    def __init__(self, c: Config, index: int, source: Source, device: torch.device, rank: int, acc: torch.dtype,
                 reference_math: bool = False):
        super().__init__()
        self.c, self.index, self.acc = c, index, acc
        self.prefix = p = f"mtp.{index}."
        self.fast_training = False
        self.t = t = Tensors(source, device, reference_math)
        self.dense = nn.ModuleDict()
        for name in ("attn.wq_a", "attn.wq_b", "attn.wkv", "attn.wo_b",
                     "ffn.shared_experts.w1", "ffn.shared_experts.w2", "ffn.shared_experts.w3"):
            self.dense[key(name)] = t.dense(p + name, rank, acc)
        # wo_a is grouped, so each output group has its own low-rank update.
        full = t.dense(p + "attn.wo_a", 0, acc, False).weight
        self.register_buffer("wo_a_base", (full.cpu() if reference_math else full).reshape(c.groups, c.outr, -1))
        self.wo_a = nn.ModuleList(Dense(w, rank, False, acc) for w in full.chunk(c.groups, 0))
        for name in ("attn_norm", "ffn_norm", "attn.q_norm", "attn.kv_norm"):
            t.add(p + name + ".weight", train=True)
        for name in ("hc_attn_fn", "hc_attn_scale", "hc_attn_base", "hc_ffn_fn", "hc_ffn_scale", "hc_ffn_base",
                     "attn.attn_sink", "ffn.gate.weight", "ffn.gate.bias"):
            t.add(p + name)
        self.experts = nn.ModuleList()
        for e in range(c.experts):
            ex = nn.ModuleDict()
            for name in ("w1", "w2", "w3"):
                ep = f"{p}ffn.experts.{e}.{name}"
                ex[name] = PackedLinear(t.read(ep + ".weight"), t.read(ep + ".scale"), acc)
            self.experts.append(ex)

    def weight(self, name: str) -> Tensor:
        return self.t[self.prefix + name]

    def norm(self, x: Tensor, name: str) -> Tensor:
        return rms(x, self.weight(name + ".weight"), self.c.eps)

    def linear(self, name: str, x: Tensor) -> Tensor:
        return cast(Tensor, self.dense[key(name)](x))

    def kv(self, x: Tensor, positions: Tensor) -> Tensor:
        z = self.norm(self.linear("attn.wkv", x), "attn.kv_norm")
        return qdq(rope(z, positions, self.c.rd, self.c.theta), self.fast_training)

    def attention(self, x: Tensor, mx: Tensor, positions: Tensor, mask: Tensor, P: Tensor
                  ) -> tuple[Tensor, Tensor]:
        c = self.c
        kv_ctx = self.kv(mx, positions)
        # Match the reference's ring-slot order (important for summation bands).
        slots = positions.remainder(c.window).argsort(-1)
        ctx = kv_ctx.gather(1, slots.unsqueeze(-1).expand_as(kv_ctx))
        valid_ctx = mask.gather(1, slots)
        query_pos = P[:, None] + 1 + torch.arange(c.block, device=x.device)
        q = self.linear("attn.wq_b", self.norm(self.linear("attn.wq_a", x), "attn.q_norm"))
        q = rope(q.reshape(*q.shape[:-1], c.heads, c.hd), query_pos, c.rd, c.theta)
        kv = torch.cat((ctx, self.kv(x, query_pos)), 1)
        valid = torch.cat((valid_ctx, torch.ones(x.shape[:2], device=x.device, dtype=torch.bool)), 1)
        scores = torch.einsum("bshd,btd->bsht", q.to(self.acc), kv.to(self.acc)) * c.hd ** -.5
        scores = scores.masked_fill(~valid[:, None, None, :], -torch.inf)
        sink = self.weight("attn.attn_sink").to(self.acc)[None, None, :, None].expand(*scores.shape[:-1], 1)
        prob = torch.cat((scores, sink), -1).softmax(-1)[..., :-1]
        o = torch.einsum("bsht,btd->bshd", prob, kv.to(self.acc)).to(x.dtype)
        o = rope(o, query_pos, c.rd, c.theta, inverse=True).reshape(x.shape[0], c.block, c.groups, -1)
        o = qdq(o, self.fast_training)
        projected = grouped_linear(o, self.wo_a_base)
        updates = [F.linear(F.linear(o[:, :, g].to(cast(Dense, layer).a.device), cast(Dense, layer).a), cast(Dense, layer).b).to(o.device)
                   for g, layer in enumerate(self.wo_a) if cast(Dense, layer).rank]
        if updates:
            projected = projected + torch.stack(updates, -2)
        return self.linear("attn.wo_b", projected.flatten(-2)), kv_ctx

    def expert(self, x: Tensor, e: nn.ModuleDict, w: Tensor | None = None) -> Tensor:
        gate, up = e["w1"](x).float(), e["w3"](x).float()
        h = F.silu(gate.clamp(max=self.c.limit)) * up.clamp(-self.c.limit, self.c.limit)
        if w is not None:
            h = h * w
        return cast(Tensor, e["w2"](h.to(x.dtype)))

    def moe(self, x: Tensor) -> tuple[Tensor, Tensor, Tensor]:
        c = self.c
        shape = x.shape
        x = x.reshape(-1, c.dim)
        # The official router uses F.linear FP32 even in the FP64 oracle mode.
        if self.t.reference_math:
            raw = torch.cat([F.linear(x[b:b+c.block].float(), self.weight("ffn.gate.weight"))
                             for b in range(0, x.shape[0], c.block)], 0)
        else:
            raw = F.linear(x.float(), self.weight("ffn.gate.weight"))
        score = F.softplus(raw / c.gate_temp).sqrt()
        biased = score + self.weight("ffn.gate.bias")
        idx = biased.topk(c.topk, -1).indices
        w = score.gather(1, idx)
        w = w / (w.sum(-1, keepdim=True) + 1e-20) * c.route_scale
        y = torch.zeros_like(x, dtype=torch.float32)
        if self.fast_training:
            y = self.fast_moe(x, idx, w)
        else:
            for eid in sorted(set(idx.detach().flatten().tolist())):
                rows, col = torch.where(idx == eid)
                # Per selected expert only; no full-bank dequantization in either pass.
                out = self.expert(x[rows], self.experts[eid], w[rows, col, None])
                y = y.index_add(0, rows, out.float())
        shared = nn.ModuleDict({n: self.dense[key("ffn.shared_experts." + n)] for n in ("w1", "w2", "w3")})
        y = y + self.expert(x, shared)
        return y.reshape(shape).to(x.dtype), idx.reshape(*shape[:-1], c.topk), biased

    def fast_moe(self, x: Tensor, idx: Tensor, w: Tensor) -> Tensor:
        from .fast_gpu import ExpertBank
        bank = cast(ExpertBank, self.expert_bank)
        order = idx.flatten().argsort(stable=True)
        counts = torch.bincount(idx.flatten(), minlength=self.c.experts)
        offsets = F.pad(counts.cumsum(0), (1, 0))
        tiles = ((counts + 15) // 16).cumsum(0)
        rows = order // self.c.topk
        xq = qdq(x, True)[rows]
        gate = bank.product("w1", xq, offsets, tiles)
        up = bank.product("w3", xq, offsets, tiles)
        h = F.silu(gate.clamp(max=self.c.limit)) * up.clamp(-self.c.limit, self.c.limit)
        h = qdq(h * w.flatten()[order, None], True)
        out = bank.product("w2", h, offsets, tiles)
        # Ascending expert order, as in the reference. CUDA index_add accumulates
        # only top-k terms; agreement is measured, never presumed bit-exact.
        return torch.zeros_like(x).index_add(0, rows, out)

    def hc_mix(self, x: Tensor, kind: str) -> tuple[Tensor, Tensor, Tensor]:
        c = self.c
        if self.t.reference_math and x.shape[0] > 1:
            parts = [self.hc_mix(x[b:b+1], kind) for b in range(x.shape[0])]
            return (torch.cat([p[0] for p in parts], 0), torch.cat([p[1] for p in parts], 0), torch.cat([p[2] for p in parts], 0))
        return mixes(x, self.weight(f"hc_{kind}_fn"), self.weight(f"hc_{kind}_scale"),
                     self.weight(f"hc_{kind}_base"), c.hc, c.sinkhorn, c.eps, c.hc_eps, self.acc)

    def forward(self, h: Tensor, pre: Tensor, mx: Tensor, positions: Tensor, mask: Tensor, P: Tensor,
                trace: dict[str, object] | None = None) -> tuple[Tensor, Tensor]:
        original_h, original_pre = h, pre
        if trace is None and bool((P == 0).all()):
            # Official DSparkBlock(start_pos=0) seeds context only; no block
            # transformation occurs. Stateless callers need no mutable seed.
            return h, pre
        apre, apost, acomb = self.hc_mix(h, "attn")
        aout, kv = self.attention(self.norm(collapse(h, pre), "attn_norm"), mx, positions, mask, P)
        h = expand(aout, h, apost, acomb)
        fpre, fpost, fcomb = self.hc_mix(h, "ffn")
        fin = self.norm(collapse(h, apre), "ffn_norm")
        fout, idx, biased = self.moe(fin)
        h = expand(fout, h, fpost, fcomb)
        h = torch.where((P == 0)[:, None, None, None], original_h, h)
        fpre = torch.where((P == 0)[:, None, None], original_pre, fpre)
        if trace is not None:
            trace.update(attn_out=aout.detach(), ffn_in=fin.detach(), ffn_out=fout.detach(), h_out=h.detach(),
                         kv=kv.detach(), route={"idx": idx.detach(), "biased": biased.detach()})
        return h, fpre


class Drafter(nn.Module):
    def __init__(self, source: Source, config: Config = Config(), device: str = "cpu", rank: int = 32,
                 acc: torch.dtype = torch.float32, checkpoint_stages: bool = True, reference_math: bool = False):
        super().__init__()
        torch.backends.cuda.matmul.allow_tf32 = False
        self.c, self.acc, self.checkpoint_stages = config, acc, checkpoint_stages
        self.reference_math = reference_math
        self.fast_training = False
        self.activation_device = "cpu" if reference_math else device
        identity = source.manifest if isinstance(source, OfficialShards) else getattr(source, "fingerprint", None)
        if identity is None:
            raise ValueError("tensor source must provide a stable frozen-weight identity")
        self.frozen_identity = hashlib.sha256(json.dumps(identity, sort_keys=True).encode()).hexdigest()
        self.t = t = Tensors(source, torch.device(device), reference_math)
        self.main_proj = t.dense("mtp.0.main_proj", 0, acc)
        t.add("mtp.0.main_norm.weight", train=True)
        t.add("embed.weight", bf16=True)
        t.add("head.weight", bf16=True)
        last = f"mtp.{config.stages-1}."
        for name in ("norm.weight", "markov_head.embed.weight", "markov_head.head.weight", "confidence_head.proj.weight"):
            t.add(last + name, train=True)
        self.stages = nn.ModuleList(Stage(config, i, source, t.device, rank, acc, reference_math) for i in range(config.stages))

    def enable_fast_training(self) -> None:
        if self.reference_math or self.t.device.type != "cuda":
            raise ValueError("fast training requires a separate all-GPU model")
        if self.fast_training:
            return
        from .fast_gpu import ExpertBank
        torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction = False
        for module_stage in self.stages:
            stage = cast(Stage, module_stage)
            stage.expert_bank = ExpertBank(stage.experts)
            stage.fast_training = True
        for module in self.modules():
            if isinstance(module, Dense):
                module.fast_training = True
        self.fast_training = True

    def last(self, name: str) -> Tensor:
        return self.t[f"mtp.{self.c.stages-1}." + name]

    def conditioning(self, taps: Tensor, engine_mx: Tensor, has_taps: Tensor,
                     main_projection: Tensor | None = None) -> Tensor:
        if main_projection is not None:
            raise ValueError("projection cache refused: detached output does not prove frozen weights AND frozen inputs; use raw taps")
        # FP64 quantized GEMM sums retain the same per-row QDQ inputs when batched.
        flat = taps.reshape(-1, taps.shape[-1])
        selected = has_taps.flatten().nonzero().flatten()
        mx = engine_mx.float().reshape(-1, self.c.dim)
        if selected.numel():
            projected = self.main_proj(flat[selected].float())
            native = rms(projected, self.t["mtp.0.main_norm.weight"], self.c.eps)
            mx = mx.index_copy(0, selected, native)
        return mx.reshape(*taps.shape[:-1], self.c.dim)

    def hidden(self, taps: Tensor, engine_mx: Tensor, has_taps: Tensor, positions: Tensor, mask: Tensor,
               anchor: Tensor, P: Tensor, trace: dict[str, Any] | None = None,
               main_projection: Tensor | None = None) -> tuple[Tensor, Tensor]:
        c = self.c
        mx = self.conditioning(taps, engine_mx, has_taps, main_projection)
        ids = torch.full((anchor.numel(), c.block), c.noise, dtype=torch.long, device=anchor.device)
        ids[:, 0] = anchor
        h = F.embedding(ids.to(self.t["embed.weight"].device), self.t["embed.weight"]).to(self.activation_device).float().unsqueeze(-2).repeat(1, 1, c.hc, 1)
        pre = torch.zeros(*h.shape[:-1], device=h.device, dtype=torch.float32)
        pre[..., 0] = 1
        for i, stage in enumerate(self.stages):
            if self.checkpoint_stages and self.training and torch.is_grad_enabled() and trace is None:
                h, pre = checkpoint(stage, h, pre, mx, positions, mask, P, use_reentrant=False)
            else:
                st: dict[str, object] = {}
                h, pre = stage(h, pre, mx, positions, mask, P, st if trace is not None else None)
                if trace is not None:
                    trace[f"s{i}"] = st
        head_x = collapse(h, pre)
        hidden = rms(head_x, self.last("norm.weight"), c.eps)
        if trace is not None:
            trace.update(head_x=head_x.detach(), head_hidden=hidden.detach(), main_x=mx.detach())
        return hidden, head_x

    def hidden_from(self, inp: Mapping[str, Tensor], trace: dict[str, Any] | None = None) -> tuple[Tensor, Tensor]:
        return self.hidden(inp["taps"], inp["engine_mx"], inp["has_taps"], inp["positions"], inp["mask"],
                           inp["anchor"], inp["P"], trace, inp.get("main_projection"))

    def logit_chunk(self, hidden: Tensor, previous: Tensor, start: int, end: int) -> Tensor:
        if self.fast_training:
            from .ops import fast_head
            base = fast_head(hidden.float(), self.t["head.weight"][start:end])
        else:
            base = frozen_linear(hidden.float(), self.t["head.weight"][start:end], acc=torch.float32)
        emb = F.embedding(previous, self.last("markov_head.embed.weight"))
        return base + F.linear(emb.float(), self.last("markov_head.head.weight")[start:end])

    def confidence(self, head_x: Tensor, previous: Tensor) -> Tensor:
        emb = F.embedding(previous, self.last("markov_head.embed.weight"))
        return F.linear(torch.cat((head_x, emb), -1).float(), self.last("confidence_head.proj.weight")).squeeze(-1)

    @torch.no_grad()
    def greedy(self, hidden: Tensor, head_x: Tensor, anchor: Tensor, chunk: int = 4096) -> tuple[Tensor, Tensor]:
        prev = anchor
        drafts, conf = [], []
        for pos in range(self.c.block):
            best = torch.full_like(prev, -torch.inf, dtype=torch.float32)
            pick = torch.zeros_like(prev)
            for lo in range(0, self.c.vocab, chunk):
                logits = self.logit_chunk(hidden[:, pos], prev, lo, min(lo + chunk, self.c.vocab))
                val, idx = logits.max(-1)
                better = val > best  # equal ties retain the lower vocabulary id
                pick = torch.where(better, idx + lo, pick)
                best = torch.maximum(best, val)
            conf.append(self.confidence(head_x[:, pos], prev).sigmoid())
            drafts.append(pick)
            prev = pick
        return torch.stack(drafts, -1), torch.stack(conf, -1)


def load(root: str, device: str = "cpu", rank: int = 32, oracle: bool = False, reference_math: bool = False,
         native_manifest: str | None = None) -> Drafter:
    source = OfficialShards(root) if native_manifest is None else OfficialShards(root, native_manifest)
    return Drafter(source, device=device, rank=rank,
                   acc=torch.float64 if oracle or reference_math else torch.float32, reference_math=reference_math)