diskformer 0.5.4 → 0.6.6
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.
- package/CHANGELOG.md +64 -0
- package/README.md +6 -5
- package/engines/NOTICE +3 -2
- package/engines/flashmap_drafter.js +697 -0
- package/engines/flint_moe_ssd.js +487 -0
- package/engines/gemma-4-e2b.js +5677 -0
- package/engines/hy_v3_moe_ssd.js +198 -0
- package/engines/iq_quants.js +511 -0
- package/engines/lfm2_5.js +5942 -0
- package/engines/lfm2_moe_ssd.js +708 -0
- package/engines/ling_moe_ssd.js +903 -0
- package/engines/ple-opfs.js +108 -0
- package/engines/qwen35_dense.js +836 -0
- package/engines/qwen3_moe_ssd.js +2 -0
- package/engines/ternary_bonsai_2_27b.js +14941 -0
- package/engines/ternary_bonsai_2_dflash.js +936 -0
- package/engines/ternary_bonsai_2_mtp.js +643 -0
- package/ggml/dist/BUILD.md +2 -2
- package/ggml/dist/df-chat.mjs +1 -1
- package/ggml/dist/df-chat.wasm +0 -0
- package/ggml/llama.js +4 -3
- package/ggml/worker.mjs +33 -15
- package/package.json +1 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/* flashmap_drafter.js — FlashMap speculative decoding (Shen et al., arXiv:2610.24365) for Qwen3Dense: the flow-map
|
|
2
|
+
* drafter j-shen/FlashMap-Qwen3-4B (475M parameters, bf16, Apache-2.0) in WGSL, and the speculative generate loop.
|
|
3
|
+
* Output is the target's own greedy decoding; the drafter only proposes.
|
|
4
|
+
*
|
|
5
|
+
* The math follows the drafter's modeling_flashmap.py (rev 4e8d6a3). The drafter drafts the 16 tokens after the last
|
|
6
|
+
* verified token (the anchor) in one network evaluation, a jump of a flow map from noise (s = 0) to tokens (t = 1):
|
|
7
|
+
* context: the target's hidden states at layers 1, 9, 17, 25, 33 (concatenated, 12800) of every verified position →
|
|
8
|
+
* fc (12800 → 2048) → RMSNorm, then each DiT layer's cross-attention keys (k_proj, per-head RMSNorm, RoPE) and
|
|
9
|
+
* values (v_proj), cached; the anchor's slot gets anchor_norm(anchor_proj(E[anchor])) the same way.
|
|
10
|
+
* input: zero noise, so every slot starts from state_norm(uniform_embeds) (uniform_embeds = the mean row of the target's
|
|
11
|
+
* token table E); x_embed (2560 → 2048). The noise term noise_norm(z·Lᵀ) of a random draft is zero at z = 0, which
|
|
12
|
+
* also removes the Cholesky factor of EᵀE (zero noise matched random noise on acceptance: plan/2026-10-22-flashmap).
|
|
13
|
+
* 6 DiT layers, each: x += gate·self_attn(LN(x)·(1+scale)+shift) over the 16 slots (no mask); x += gate·cross_attn(…)
|
|
14
|
+
* over the cached context; x += gate·MLP(…) (fc1, exact GELU, fc2). scale/shift/gate per norm come from the time
|
|
15
|
+
* conditioning of the jump (0 → 1), constant, computed once here in JS. Attention: 16 heads × 128, RMSNorm on q and
|
|
16
|
+
* k, RoPE on interleaved channel pairs (2i, 2i+1), base 10000, softmax(qkᵀ/√128).
|
|
17
|
+
* output: final LN·(1+scale)+shift, head (2048 → 2560), then the target's lm_head (here: the tied Q8_0 token table
|
|
18
|
+
* through Qwen3Dense's GEMM) and an argmax per slot.
|
|
19
|
+
* Weights stay bf16 on the GPU (the safetensors bytes as they are) and are decoded in the kernels; activations are f32.
|
|
20
|
+
*
|
|
21
|
+
* const target = await Qwen3Dense.load(null, { captureLayers: FLASHMAP_QWEN3_4B_LAYERS, … });
|
|
22
|
+
* const drafter = await FlashMapDrafter.load(target, { url });
|
|
23
|
+
* for await (const step of specGenerate(target, drafter, promptIds, { maxNewTokens })) …
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { STORAGE, COPY_SRC, COPY_DST, MAP_READ, UNIFORM } from './qwen3_moe_ssd.js';
|
|
27
|
+
import { OpfsWriter, OpfsReaderPool, opfsFileSize, readOpfsText, writeOpfsText } from '../src/opfs-reader.js';
|
|
28
|
+
import { MM_TILE, MM_SG_TOKENS, MVT_ROWS, gpuRowBytes } from './iq_quants.js';
|
|
29
|
+
|
|
30
|
+
export const FLASHMAP_QWEN3_4B = {
|
|
31
|
+
repo: 'j-shen/FlashMap-Qwen3-4B',
|
|
32
|
+
file: 'model.safetensors',
|
|
33
|
+
revision: '4e8d6a39a84ad108c5b60cb59072eb3f968c98cd',
|
|
34
|
+
size: 949897866,
|
|
35
|
+
};
|
|
36
|
+
export const FLASHMAP_QWEN3_4B_LAYERS = [1, 9, 17, 25, 33];
|
|
37
|
+
const CFG = { dim: 2048, heads: 16, hd: 128, layers: 6, ff: 8192, block: 16, targetDim: 2560, cond: 256, timeDim: 256,
|
|
38
|
+
timeScale: 1000, theta: 10000, lnEps: 1e-6, rmsEps: 1e-6, qkEps: 1e-5 };
|
|
39
|
+
|
|
40
|
+
// ── WGSL ────────────────────────────────────────────────────────────────────
|
|
41
|
+
const BF = 'fn bf(w: u32, hi: bool) -> f32 { return bitcast<f32>(select(w << 16u, w & 0xffff0000u, hi)); }';
|
|
42
|
+
const KS = {
|
|
43
|
+
// y[t·M + m] = Σ_k x[t·N + k]·W[m][k] (+ b[m]); W bf16 [M][N]. 16 rows × 16 tokens per workgroup, 32-wide k tiles
|
|
44
|
+
// through shared memory. Dispatch (ceil(M / 16), ceil(T / 16)).
|
|
45
|
+
gemmBf16: `
|
|
46
|
+
struct P { M: u32, N: u32, T: u32, bias: u32 }
|
|
47
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
48
|
+
@group(0) @binding(1) var<storage, read> w: array<u32>;
|
|
49
|
+
@group(0) @binding(2) var<storage, read> b: array<u32>;
|
|
50
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
51
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
52
|
+
${BF}
|
|
53
|
+
var<workgroup> ws: array<f32, 512>;
|
|
54
|
+
var<workgroup> xs: array<f32, 512>;
|
|
55
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
56
|
+
let m0 = wg.x * 16u; let t0 = wg.y * 16u; let r = l.x / 16u; let c = l.x % 16u;
|
|
57
|
+
var acc = 0.0;
|
|
58
|
+
for (var k0 = 0u; k0 < p.N; k0 += 32u) {
|
|
59
|
+
for (var i = l.x; i < 512u; i += 256u) {
|
|
60
|
+
let rr = i / 32u; let kk = i % 32u; let m = m0 + rr; let t = t0 + rr; let k = k0 + kk;
|
|
61
|
+
var wv = 0.0;
|
|
62
|
+
if (m < p.M && k < p.N) { let e = m * p.N + k; wv = bf(w[e >> 1u], (e & 1u) == 1u); }
|
|
63
|
+
ws[i] = wv;
|
|
64
|
+
var xv = 0.0;
|
|
65
|
+
if (t < p.T && k < p.N) { xv = x[t * p.N + k]; }
|
|
66
|
+
xs[i] = xv;
|
|
67
|
+
}
|
|
68
|
+
workgroupBarrier();
|
|
69
|
+
for (var kk = 0u; kk < 32u; kk++) { acc += ws[r * 32u + kk] * xs[c * 32u + kk]; }
|
|
70
|
+
workgroupBarrier();
|
|
71
|
+
}
|
|
72
|
+
let m = m0 + r; let t = t0 + c;
|
|
73
|
+
if (m < p.M && t < p.T) {
|
|
74
|
+
var v = acc;
|
|
75
|
+
if (p.bias == 1u) { v += bf(b[m >> 1u], (m & 1u) == 1u); }
|
|
76
|
+
y[t * p.M + m] = v;
|
|
77
|
+
}
|
|
78
|
+
}`,
|
|
79
|
+
// The same product for a few tokens (T ≤ TT: the draft's 16 slots, a verify's accepted rows, the anchor): one row per
|
|
80
|
+
// lane, 256 rows per workgroup, each weight read once; per 256-wide k block the T activation rows go through shared
|
|
81
|
+
// memory (broadcast reads). The token loop has a constant trip count so the sums stay in registers. N must be a
|
|
82
|
+
// multiple of 256. Dispatch (ceil(M / 256)).
|
|
83
|
+
gemvT17: gemvT(17),
|
|
84
|
+
gemvT1: gemvT(1),
|
|
85
|
+
// y = x · (w · rsqrt(mean(x²) + eps)) per row of D; one workgroup per row.
|
|
86
|
+
rmsRows: `
|
|
87
|
+
struct P { D: u32, eps: f32 }
|
|
88
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
89
|
+
@group(0) @binding(1) var<storage, read> w: array<u32>;
|
|
90
|
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
|
91
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
92
|
+
${BF}
|
|
93
|
+
var<workgroup> red: array<f32, 256>;
|
|
94
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
95
|
+
let base = wg.x * p.D; let t = l.x;
|
|
96
|
+
var a = 0.0;
|
|
97
|
+
for (var i = t; i < p.D; i += 256u) { let v = x[base + i]; a += v * v; }
|
|
98
|
+
red[t] = a; workgroupBarrier();
|
|
99
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
100
|
+
let r = 1.0 / sqrt(red[0] / f32(p.D) + p.eps);
|
|
101
|
+
for (var i = t; i < p.D; i += 256u) { y[base + i] = x[base + i] * (bf(w[i >> 1u], (i & 1u) == 1u) * r); }
|
|
102
|
+
}`,
|
|
103
|
+
// y = LayerNorm(x) (no affine) · (1 + scale) + shift per row of D; scale/shift at md[off], md[off + D] (`mod` is a
|
|
104
|
+
// reserved word in WGSL).
|
|
105
|
+
lnMod: `
|
|
106
|
+
struct P { D: u32, off: u32, eps: f32, pad: u32 }
|
|
107
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
108
|
+
@group(0) @binding(1) var<storage, read> md: array<f32>;
|
|
109
|
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
|
110
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
111
|
+
var<workgroup> red: array<f32, 256>;
|
|
112
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
113
|
+
let base = wg.x * p.D; let t = l.x;
|
|
114
|
+
var a = 0.0;
|
|
115
|
+
for (var i = t; i < p.D; i += 256u) { a += x[base + i]; }
|
|
116
|
+
red[t] = a; workgroupBarrier();
|
|
117
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
118
|
+
let mu = red[0] / f32(p.D); workgroupBarrier();
|
|
119
|
+
var q = 0.0;
|
|
120
|
+
for (var i = t; i < p.D; i += 256u) { let d = x[base + i] - mu; q += d * d; }
|
|
121
|
+
red[t] = q; workgroupBarrier();
|
|
122
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
123
|
+
let r = 1.0 / sqrt(red[0] / f32(p.D) + p.eps);
|
|
124
|
+
for (var i = t; i < p.D; i += 256u) { y[base + i] = (x[base + i] - mu) * r * (1.0 + md[p.off + i]) + md[p.off + p.D + i]; }
|
|
125
|
+
}`,
|
|
126
|
+
// In place, per (head wg.x, row wg.y): RMSNorm over hd with weight w, then RoPE on interleaved pairs (2i, 2i+1) with
|
|
127
|
+
// the row's [cos hd/2 | sin hd/2] in rope.
|
|
128
|
+
headNormRope: `
|
|
129
|
+
struct P { hd: u32, row: u32, eps: f32, pad: u32 }
|
|
130
|
+
@group(0) @binding(0) var<storage, read_write> x: array<f32>;
|
|
131
|
+
@group(0) @binding(1) var<storage, read> w: array<u32>;
|
|
132
|
+
@group(0) @binding(2) var<storage, read> rope: array<f32>;
|
|
133
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
134
|
+
${BF}
|
|
135
|
+
var<workgroup> red: array<f32, 64>;
|
|
136
|
+
@compute @workgroup_size(64) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
137
|
+
let base = wg.y * p.row + wg.x * p.hd; let t = l.x; let half = p.hd / 2u; let rb = wg.y * p.hd;
|
|
138
|
+
var a = 0.0;
|
|
139
|
+
for (var i = t; i < p.hd; i += 64u) { let v = x[base + i]; a += v * v; }
|
|
140
|
+
red[t] = a; workgroupBarrier();
|
|
141
|
+
for (var s = 32u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
142
|
+
let r = 1.0 / sqrt(red[0] / f32(p.hd) + p.eps);
|
|
143
|
+
workgroupBarrier();
|
|
144
|
+
for (var i = t; i < half; i += 64u) {
|
|
145
|
+
let a0 = x[base + 2u * i] * (bf(w[i], false) * r);
|
|
146
|
+
let a1 = x[base + 2u * i + 1u] * (bf(w[i], true) * r);
|
|
147
|
+
let c = rope[rb + i]; let sn = rope[rb + half + i];
|
|
148
|
+
x[base + 2u * i] = a0 * c - a1 * sn;
|
|
149
|
+
x[base + 2u * i + 1u] = a1 * c + a0 * sn;
|
|
150
|
+
}
|
|
151
|
+
}`,
|
|
152
|
+
// Attention without a mask: query row wg.y, head wg.x, over keys [0, L) of k/v (f32 rows of D); scores in shared
|
|
153
|
+
// memory (L ≤ 4096).
|
|
154
|
+
attnF32: attn('f32', ''),
|
|
155
|
+
// The same over the f16 context cache.
|
|
156
|
+
attnF16: attn('f16', 'enable f16;'),
|
|
157
|
+
// cache[(start + t)·D + i] = f16(x[t·D + i]) for both k and v. Dispatch (ceil(D / 256), T).
|
|
158
|
+
kvWrite: `enable f16;
|
|
159
|
+
struct P { D: u32, start: u32, T: u32, pad: u32 }
|
|
160
|
+
@group(0) @binding(0) var<storage, read> k: array<f32>;
|
|
161
|
+
@group(0) @binding(1) var<storage, read> v: array<f32>;
|
|
162
|
+
@group(0) @binding(2) var<storage, read_write> kc: array<f16>;
|
|
163
|
+
@group(0) @binding(3) var<storage, read_write> vc: array<f16>;
|
|
164
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
165
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
166
|
+
let i = g.x; let t = g.y; if (i >= p.D || t >= p.T) { return; }
|
|
167
|
+
kc[(p.start + t) * p.D + i] = f16(k[t * p.D + i]); vc[(p.start + t) * p.D + i] = f16(v[t * p.D + i]);
|
|
168
|
+
}`,
|
|
169
|
+
// x = x · Φ(x) (exact GELU; erf from Abramowitz–Stegun 7.1.26, |error| ≤ 1.5e-7).
|
|
170
|
+
gelu: `
|
|
171
|
+
struct P { n: u32 }
|
|
172
|
+
@group(0) @binding(0) var<storage, read_write> x: array<f32>;
|
|
173
|
+
@group(0) @binding(1) var<uniform> p: P;
|
|
174
|
+
fn erf_(z: f32) -> f32 {
|
|
175
|
+
let s = sign(z); let a = abs(z); let t = 1.0 / (1.0 + 0.3275911 * a);
|
|
176
|
+
let y = 1.0 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * exp(-a * a);
|
|
177
|
+
return s * y;
|
|
178
|
+
}
|
|
179
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>) {
|
|
180
|
+
let i = g.y * ng.x * 256u + g.x; if (i >= p.n) { return; }
|
|
181
|
+
let v = x[i]; x[i] = 0.5 * v * (1.0 + erf_(v * 0.70710678118654752));
|
|
182
|
+
}`,
|
|
183
|
+
// x[t·D + i] += md[off + i] · y[t·D + i] (the gated residual). Dispatch (ceil(D / 256), T).
|
|
184
|
+
gateAdd: `
|
|
185
|
+
struct P { D: u32, off: u32, T: u32, pad: u32 }
|
|
186
|
+
@group(0) @binding(0) var<storage, read_write> x: array<f32>;
|
|
187
|
+
@group(0) @binding(1) var<storage, read> y: array<f32>;
|
|
188
|
+
@group(0) @binding(2) var<storage, read> md: array<f32>;
|
|
189
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
190
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
191
|
+
let i = g.x; let t = g.y; if (i >= p.D || t >= p.T) { return; }
|
|
192
|
+
x[t * p.D + i] += md[p.off + i] * y[t * p.D + i];
|
|
193
|
+
}`,
|
|
194
|
+
// x[t·D + i] = r[i] for t < T (every slot starts from the same input at zero noise).
|
|
195
|
+
broadcast: `
|
|
196
|
+
struct P { D: u32, T: u32 }
|
|
197
|
+
@group(0) @binding(0) var<storage, read> r: array<f32>;
|
|
198
|
+
@group(0) @binding(1) var<storage, read_write> x: array<f32>;
|
|
199
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
200
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
201
|
+
let i = g.x; let t = g.y; if (i >= p.D || t >= p.T) { return; }
|
|
202
|
+
x[t * p.D + i] = r[i];
|
|
203
|
+
}`,
|
|
204
|
+
// The mean row of the target's Q8_0 token table in Qwen3Dense's padded layout (36-byte blocks: f16 d, 2 pad bytes,
|
|
205
|
+
// 32 int8): one thread per column; uniform_embeds = (1/V)·1ᵀE.
|
|
206
|
+
colMeanQ8: `
|
|
207
|
+
struct P { V: u32, H: u32, rowWords: u32, pad: u32 }
|
|
208
|
+
@group(0) @binding(0) var<storage, read> e: array<u32>;
|
|
209
|
+
@group(0) @binding(1) var<storage, read_write> y: array<f32>;
|
|
210
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
211
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
212
|
+
let i = g.x; if (i >= p.H) { return; }
|
|
213
|
+
let blk = i / 32u; let j = i % 32u; var acc = 0.0;
|
|
214
|
+
for (var r = 0u; r < p.V; r++) {
|
|
215
|
+
let w0 = r * p.rowWords + blk * 9u;
|
|
216
|
+
let d = unpack2x16float(e[w0]).x;
|
|
217
|
+
let q = (e[w0 + 1u + j / 4u] >> ((j % 4u) * 8u)) & 0xffu;
|
|
218
|
+
acc += d * f32(select(i32(q), i32(q) - 256, q >= 128u));
|
|
219
|
+
}
|
|
220
|
+
y[i] = acc / f32(p.V);
|
|
221
|
+
}`,
|
|
222
|
+
};
|
|
223
|
+
function gemvT(TT) {
|
|
224
|
+
return `
|
|
225
|
+
struct P { M: u32, N: u32, T: u32, bias: u32 }
|
|
226
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
227
|
+
@group(0) @binding(1) var<storage, read> w: array<u32>;
|
|
228
|
+
@group(0) @binding(2) var<storage, read> b: array<u32>;
|
|
229
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
230
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
231
|
+
${BF}
|
|
232
|
+
const TT = ${TT}u;
|
|
233
|
+
var<workgroup> xs: array<vec4<f32>, ${TT * 64}>;
|
|
234
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
235
|
+
let t = l.x; let row = wg.x * 256u + t; let rr = min(row, p.M - 1u); let T = min(p.T, TT); let rowW = p.N / 2u;
|
|
236
|
+
var acc: array<f32, ${TT}>;
|
|
237
|
+
for (var kb = 0u; kb < p.N; kb += 256u) {
|
|
238
|
+
workgroupBarrier();
|
|
239
|
+
for (var i = t; i < TT * 64u; i += 256u) {
|
|
240
|
+
let tk = i / 64u; let o = tk * p.N + kb + (i % 64u) * 4u;
|
|
241
|
+
xs[i] = select(vec4<f32>(0.0), vec4<f32>(x[o], x[o + 1u], x[o + 2u], x[o + 3u]), tk < T);
|
|
242
|
+
}
|
|
243
|
+
workgroupBarrier();
|
|
244
|
+
let base = rr * rowW + kb / 2u;
|
|
245
|
+
for (var j = 0u; j < 32u; j++) {
|
|
246
|
+
let u0 = w[base + 4u * j]; let u1 = w[base + 4u * j + 1u]; let u2 = w[base + 4u * j + 2u]; let u3 = w[base + 4u * j + 3u];
|
|
247
|
+
let w0 = vec4<f32>(bf(u0, false), bf(u0, true), bf(u1, false), bf(u1, true));
|
|
248
|
+
let w1 = vec4<f32>(bf(u2, false), bf(u2, true), bf(u3, false), bf(u3, true));
|
|
249
|
+
for (var tk = 0u; tk < TT; tk++) { acc[tk] += dot(w0, xs[tk * 64u + 2u * j]) + dot(w1, xs[tk * 64u + 2u * j + 1u]); }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (row < p.M) {
|
|
253
|
+
var bv = 0.0;
|
|
254
|
+
if (p.bias == 1u) { bv = bf(b[row >> 1u], (row & 1u) == 1u); }
|
|
255
|
+
for (var tk = 0u; tk < TT; tk++) { if (tk < T) { y[tk * p.M + row] = acc[tk] + bv; } }
|
|
256
|
+
}
|
|
257
|
+
}`;
|
|
258
|
+
}
|
|
259
|
+
function attn(kt, en) {
|
|
260
|
+
return `${en}
|
|
261
|
+
struct P { heads: u32, hd: u32, L: u32, scale: f32 }
|
|
262
|
+
@group(0) @binding(0) var<storage, read> q: array<f32>;
|
|
263
|
+
@group(0) @binding(1) var<storage, read> k: array<${kt}>;
|
|
264
|
+
@group(0) @binding(2) var<storage, read> v: array<${kt}>;
|
|
265
|
+
@group(0) @binding(3) var<storage, read_write> o: array<f32>;
|
|
266
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
267
|
+
var<workgroup> sc: array<f32, 4096>;
|
|
268
|
+
var<workgroup> red: array<f32, 256>;
|
|
269
|
+
var<workgroup> qs: array<f32, 128>;
|
|
270
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
271
|
+
let h = wg.x; let row = wg.y; let t = l.x; let D = p.heads * p.hd; let qb = row * D + h * p.hd;
|
|
272
|
+
if (t < p.hd) { qs[t] = q[qb + t]; }
|
|
273
|
+
workgroupBarrier();
|
|
274
|
+
var mx = -3.4e38;
|
|
275
|
+
for (var j = t; j < p.L; j += 256u) {
|
|
276
|
+
var s = 0.0; let kb = j * D + h * p.hd;
|
|
277
|
+
for (var d = 0u; d < p.hd; d++) { s += qs[d] * f32(k[kb + d]); }
|
|
278
|
+
s = s * p.scale; sc[j] = s; mx = max(mx, s);
|
|
279
|
+
}
|
|
280
|
+
red[t] = mx; workgroupBarrier();
|
|
281
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] = max(red[t], red[t + s]); } workgroupBarrier(); }
|
|
282
|
+
let m = red[0]; workgroupBarrier();
|
|
283
|
+
var sum = 0.0;
|
|
284
|
+
for (var j = t; j < p.L; j += 256u) { let e = exp(sc[j] - m); sc[j] = e; sum += e; }
|
|
285
|
+
red[t] = sum; workgroupBarrier();
|
|
286
|
+
for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
|
|
287
|
+
let inv = 1.0 / red[0];
|
|
288
|
+
if (t < p.hd) {
|
|
289
|
+
var acc = 0.0;
|
|
290
|
+
for (var j = 0u; j < p.L; j++) { acc += sc[j] * f32(v[j * D + h * p.hd + t]); }
|
|
291
|
+
o[qb + t] = acc * inv;
|
|
292
|
+
}
|
|
293
|
+
}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── safetensors ─────────────────────────────────────────────────────────────
|
|
297
|
+
async function readSafetensors(url, fetchFn) {
|
|
298
|
+
const r0 = await fetchFn(url, { headers: { Range: 'bytes=0-7' } });
|
|
299
|
+
if (!r0.ok && r0.status !== 206) throw new Error(`drafter: HTTP ${r0.status}`);
|
|
300
|
+
const n = Number(new DataView(await r0.arrayBuffer()).getBigUint64(0, true));
|
|
301
|
+
const r1 = await fetchFn(url, { headers: { Range: `bytes=8-${8 + n - 1}` } });
|
|
302
|
+
const header = JSON.parse(new TextDecoder().decode(await r1.arrayBuffer()));
|
|
303
|
+
delete header.__metadata__;
|
|
304
|
+
for (const t of Object.values(header)) if (t.dtype !== 'BF16') throw new Error(`drafter: ${t.dtype} tensor, expected BF16`);
|
|
305
|
+
return { header, dataStart: 8 + n };
|
|
306
|
+
}
|
|
307
|
+
// The drafter file in OPFS (localmind-ssd/flashmap-<repo>/model.safetensors): streamed in once (a partial copy resumes
|
|
308
|
+
// from its size), then read from there. A marker file (<file>.ok, the pinned revision) is written only after a full,
|
|
309
|
+
// size-checked copy; without it the copy is fetched again from byte 0. Returns a fetch-like function serving Range
|
|
310
|
+
// requests from the copy.
|
|
311
|
+
async function opfsCopy(url, fetchFn, src, onProgress) {
|
|
312
|
+
const path = `localmind-ssd/flashmap-${src.repo.split('/').pop().toLowerCase()}/${src.file}`, ok = `${path}.ok`;
|
|
313
|
+
let have = (await opfsFileSize(path)) || 0;
|
|
314
|
+
const done = have === src.size && (await readOpfsText(ok).catch(() => null)) === src.revision;
|
|
315
|
+
if (have >= src.size && !done) have = 0;
|
|
316
|
+
if (!done) {
|
|
317
|
+
if (have > src.size) have = 0;
|
|
318
|
+
const w = await OpfsWriter.open(path, { truncate: have === 0 });
|
|
319
|
+
const r = await fetchFn(url, have ? { headers: { Range: `bytes=${have}-` } } : {});
|
|
320
|
+
if (!r.ok && r.status !== 206) throw new Error(`drafter download: HTTP ${r.status}`);
|
|
321
|
+
if (have && r.status !== 206) have = 0; // the server ignored the range: start over
|
|
322
|
+
const reader = r.body.getReader();
|
|
323
|
+
let at = have;
|
|
324
|
+
for (;;) {
|
|
325
|
+
const { done, value } = await reader.read();
|
|
326
|
+
if (done) break;
|
|
327
|
+
const n = value.byteLength; // write() transfers the buffer, so take the length first
|
|
328
|
+
await w.write(value, at); at += n;
|
|
329
|
+
onProgress({ status: 'drafter-download', loaded: at, total: src.size });
|
|
330
|
+
}
|
|
331
|
+
await w.close();
|
|
332
|
+
if (at !== src.size) throw new Error(`drafter download stopped at ${at} of ${src.size} bytes; it resumes on the next load`);
|
|
333
|
+
await writeOpfsText(ok, src.revision);
|
|
334
|
+
}
|
|
335
|
+
const pool = await OpfsReaderPool.open(path, { workers: 2 });
|
|
336
|
+
const read = async (u, init = {}) => {
|
|
337
|
+
const m = /bytes=(\d+)-(\d+)/.exec((init.headers && init.headers.Range) || '');
|
|
338
|
+
const start = m ? Number(m[1]) : 0, end = m ? Number(m[2]) : src.size - 1;
|
|
339
|
+
const got = await pool.read(start, end - start + 1);
|
|
340
|
+
if (got.got !== end - start + 1) throw new Error(`drafter: short OPFS read at ${start}`);
|
|
341
|
+
return { ok: true, status: 206, arrayBuffer: async () => got.buf.slice(0, got.got) };
|
|
342
|
+
};
|
|
343
|
+
read.close = () => pool.close();
|
|
344
|
+
return read;
|
|
345
|
+
}
|
|
346
|
+
const bf16ToF32 = (u16) => { const out = new Float32Array(u16.length), u = new Uint32Array(out.buffer); for (let i = 0; i < u16.length; i++) u[i] = u16[i] << 16; return out; };
|
|
347
|
+
|
|
348
|
+
// The time conditioning of the jump s → t (FlashMapDraftModel.time_conditioning) in f32: per DiT layer (self, cross,
|
|
349
|
+
// MLP) and the final norm, each [scale D | shift D | gate D]. Small linears, computed once on the CPU.
|
|
350
|
+
function timeConditioning(cpu, s, t) {
|
|
351
|
+
const lin = (name, x) => {
|
|
352
|
+
const w = cpu[`${name}.weight`], b = cpu[`${name}.bias`], [M, N] = w.shape, y = new Float32Array(M);
|
|
353
|
+
for (let m = 0; m < M; m++) { let a = b ? b.data[m] : 0; for (let k = 0; k < N; k++) a += w.data[m * N + k] * x[k]; y[m] = Math.fround(a); }
|
|
354
|
+
return y;
|
|
355
|
+
};
|
|
356
|
+
const silu = (x) => x.map((v) => v / (1 + Math.exp(-v)));
|
|
357
|
+
const embed = (prefix, time) => {
|
|
358
|
+
const half = CFG.timeDim / 2, e = new Float32Array(CFG.timeDim);
|
|
359
|
+
for (let i = 0; i < half; i++) {
|
|
360
|
+
const f = Math.fround(Math.exp(-Math.log(10000) * i / half)), a = Math.fround(Math.fround(time * CFG.timeScale) * f);
|
|
361
|
+
e[i] = Math.cos(a); e[half + i] = Math.sin(a);
|
|
362
|
+
}
|
|
363
|
+
return lin(`${prefix}.linear_2`, silu(lin(`${prefix}.linear_1`, e)));
|
|
364
|
+
};
|
|
365
|
+
const ts = embed('time_embed', s), tj = embed('jump_embed', t), cond = silu(ts.map((v, i) => v + tj[i]));
|
|
366
|
+
const out = new Float32Array((CFG.layers * 3 + 1) * 3 * CFG.dim);
|
|
367
|
+
let o = 0;
|
|
368
|
+
for (let l = 0; l < CFG.layers; l++) for (const n of ['self_attn_adaln', 'cross_attn_adaln', 'mlp_adaln']) { out.set(lin(`layers.${l}.${n}.linear`, cond), o); o += 3 * CFG.dim; }
|
|
369
|
+
out.set(lin('final_adaln.linear', cond), o);
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
// RoPE tables for rows at positions pos0 … pos0 + n − 1: [cos hd/2 | sin hd/2] per row (rope_tables: f32 angles).
|
|
373
|
+
function ropeRows(pos0, n) {
|
|
374
|
+
const half = CFG.hd / 2, r = new Float32Array(n * CFG.hd);
|
|
375
|
+
const inv = Array.from({ length: half }, (_, i) => Math.fround(1 / Math.fround(Math.pow(CFG.theta, Math.fround(2 * i / CFG.hd)))));
|
|
376
|
+
for (let t = 0; t < n; t++) for (let i = 0; i < half; i++) { const a = Math.fround((pos0 + t) * inv[i]); r[t * CFG.hd + i] = Math.cos(a); r[t * CFG.hd + half + i] = Math.sin(a); }
|
|
377
|
+
return r;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── drafter ─────────────────────────────────────────────────────────────────
|
|
381
|
+
export class FlashMapDrafter {
|
|
382
|
+
// target: a loaded Qwen3Dense with opts.captureLayers = FLASHMAP_QWEN3_4B_LAYERS (it shares the device, the token
|
|
383
|
+
// table, the lm_head GEMM and the feature rows). opts.url: the safetensors file (default: Hugging Face, pinned).
|
|
384
|
+
// The file is copied into OPFS once (opts.cache === false reads `url` directly, for a local serve).
|
|
385
|
+
static async load(target, opts = {}) {
|
|
386
|
+
const src = opts.source || FLASHMAP_QWEN3_4B, onProgress = opts.onProgress || (() => {});
|
|
387
|
+
const url = opts.url || `https://huggingface.co/${src.repo}/resolve/${src.revision}/${src.file}`;
|
|
388
|
+
const net = opts.fetch || ((u, i) => fetch(u, i));
|
|
389
|
+
const fetchFn = opts.cache === false ? net : await opfsCopy(url, net, src, onProgress);
|
|
390
|
+
try {
|
|
391
|
+
const { header, dataStart } = await readSafetensors(url, fetchFn);
|
|
392
|
+
const d = new FlashMapDrafter(target, opts);
|
|
393
|
+
await d.upload(url, fetchFn, header, dataStart, onProgress);
|
|
394
|
+
d.init();
|
|
395
|
+
return d;
|
|
396
|
+
} finally { if (fetchFn.close) await fetchFn.close(); }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
constructor(target, opts) {
|
|
400
|
+
if (!target.capture || target.featWidth !== FLASHMAP_QWEN3_4B_LAYERS.length * CFG.targetDim) throw new Error('FlashMap: load the target with captureLayers = FLASHMAP_QWEN3_4B_LAYERS');
|
|
401
|
+
this.t = target; this.device = target.device; this.opts = opts;
|
|
402
|
+
this.maxCtx = Math.min(target.maxCtx + CFG.block + 1, 4096);
|
|
403
|
+
this.pipelines = {}; this.gpuBytes = 0;
|
|
404
|
+
}
|
|
405
|
+
buffer(size, usage) { const b = this.device.createBuffer({ size: Math.max(16, Math.ceil(size / 4) * 4), usage }); this.gpuBytes += b.size; return b; }
|
|
406
|
+
uniform(words) {
|
|
407
|
+
const ab = new ArrayBuffer(Math.max(16, Math.ceil(words.length / 4) * 16)), u = new Uint32Array(ab), f = new Float32Array(ab);
|
|
408
|
+
words.forEach((w, i) => { if (typeof w === 'object') f[i] = w.f; else u[i] = w; });
|
|
409
|
+
const b = this.device.createBuffer({ size: ab.byteLength, usage: UNIFORM | COPY_DST }); this.device.queue.writeBuffer(b, 0, ab); return b;
|
|
410
|
+
}
|
|
411
|
+
pipeline(name) {
|
|
412
|
+
if (!this.pipelines[name]) {
|
|
413
|
+
const module = this.device.createShaderModule({ code: KS[name], label: `flashmap ${name}` });
|
|
414
|
+
this.pipelines[name] = this.device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' }, label: `flashmap ${name}` });
|
|
415
|
+
}
|
|
416
|
+
return this.pipelines[name];
|
|
417
|
+
}
|
|
418
|
+
bind(name, res) {
|
|
419
|
+
return this.device.createBindGroup({ layout: this.pipeline(name).getBindGroupLayout(0), entries: res.map((r, i) => ({ binding: i, resource: r instanceof GPUBuffer ? { buffer: r } : r })) });
|
|
420
|
+
}
|
|
421
|
+
run(pass, name, group, x, y = 1) { pass.setPipeline(this.pipeline(name)); pass.setBindGroup(0, group); pass.dispatchWorkgroups(x, y); }
|
|
422
|
+
|
|
423
|
+
// Every tensor to its own GPU buffer, bf16 bytes as they are; the time-conditioning linears and the noise gate also to
|
|
424
|
+
// the CPU. Reads the data section in ranges of up to 64 MB.
|
|
425
|
+
async upload(url, fetchFn, header, dataStart, onProgress) {
|
|
426
|
+
const names = Object.keys(header).sort((a, b) => header[a].data_offsets[0] - header[b].data_offsets[0]);
|
|
427
|
+
const cpuWanted = (n) => /^(time_embed|jump_embed)\.|adaln\.linear\.|^noise_gate$/.test(n);
|
|
428
|
+
this.w = {}; this.cpu = {};
|
|
429
|
+
const total = header[names[names.length - 1]].data_offsets[1];
|
|
430
|
+
const CH = 64 << 20;
|
|
431
|
+
let i = 0, done = 0;
|
|
432
|
+
while (i < names.length) {
|
|
433
|
+
const start = header[names[i]].data_offsets[0];
|
|
434
|
+
let j = i, end = start;
|
|
435
|
+
while (j < names.length && (header[names[j]].data_offsets[1] - start <= CH || j === i)) { end = header[names[j]].data_offsets[1]; j++; }
|
|
436
|
+
const r = await fetchFn(url, { headers: { Range: `bytes=${dataStart + start}-${dataStart + end - 1}` } });
|
|
437
|
+
if (!r.ok && r.status !== 206) throw new Error(`drafter: HTTP ${r.status} reading weights`);
|
|
438
|
+
const buf = new Uint8Array(await r.arrayBuffer());
|
|
439
|
+
if (buf.byteLength !== end - start) throw new Error(`drafter: short read ${buf.byteLength} of ${end - start}`);
|
|
440
|
+
for (let k = i; k < j; k++) {
|
|
441
|
+
const n = names[k], [a, b] = header[n].data_offsets, bytes = buf.subarray(a - start, b - start);
|
|
442
|
+
const padded = new Uint8Array(Math.ceil(bytes.byteLength / 4) * 4); padded.set(bytes);
|
|
443
|
+
const g = this.buffer(padded.byteLength, STORAGE | COPY_DST);
|
|
444
|
+
this.device.queue.writeBuffer(g, 0, padded);
|
|
445
|
+
this.w[n] = g;
|
|
446
|
+
if (cpuWanted(n)) this.cpu[n] = { shape: header[n].shape, data: bf16ToF32(new Uint16Array(padded.buffer, 0, bytes.byteLength / 2)) };
|
|
447
|
+
}
|
|
448
|
+
done = end; onProgress({ status: 'drafter', loaded: done, total });
|
|
449
|
+
i = j;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
init() {
|
|
454
|
+
const D = CFG.dim, H = CFG.targetDim, K = CFG.block, F = CFG.ff, t = this.t, dev = this.device;
|
|
455
|
+
for (const k of Object.keys(KS)) this.pipeline(k); // compile now, not in the first rounds
|
|
456
|
+
const maxRows = Math.max(K, t.chunkTokens), A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST);
|
|
457
|
+
this.mod = this.buffer((CFG.layers * 3 + 1) * 3 * D * 4, STORAGE | COPY_DST | COPY_SRC);
|
|
458
|
+
dev.queue.writeBuffer(this.mod, 0, timeConditioning(this.cpu, 0, 1));
|
|
459
|
+
this.zero = this.buffer(16, STORAGE);
|
|
460
|
+
this.kc = []; this.vc = [];
|
|
461
|
+
for (let l = 0; l < CFG.layers; l++) { this.kc.push(this.buffer(this.maxCtx * D * 2, STORAGE)); this.vc.push(this.buffer(this.maxCtx * D * 2, STORAGE)); }
|
|
462
|
+
const a = this.a = {
|
|
463
|
+
x: A(K * D), h: A(K * D), q: A(K * D), k: A(K * D), v: A(K * D), o: A(K * D), y: A(K * D), u: A(K * F), out: A(K * H),
|
|
464
|
+
logits: A(K * t.cfg.vocab), am: this.buffer(K * 4, STORAGE | COPY_SRC), rbAm: this.buffer(K * 4, MAP_READ | COPY_DST),
|
|
465
|
+
ctx: A(maxRows * D), ctxN: A(maxRows * D), ck: A(maxRows * D), cv: A(maxRows * D), ropeC: A(maxRows * CFG.hd), ropeS: A(K * CFG.hd), ropeA: A(CFG.hd),
|
|
466
|
+
anchorId: this.buffer(16, STORAGE | COPY_DST), anchorE: A(H), anchorC0: A(D), anchorC: A(D), anchorK: A(D), anchorV: A(D),
|
|
467
|
+
uniform: A(H), inRow: A(H), h0: A(D), qbK: this.buffer(16, UNIFORM | COPY_DST), qbA: this.buffer(16, UNIFORM | COPY_DST),
|
|
468
|
+
};
|
|
469
|
+
dev.queue.writeBuffer(a.qbK, 0, new Uint32Array([K, 0, 0, 0]));
|
|
470
|
+
dev.queue.writeBuffer(a.qbA, 0, new Uint32Array([1, 0, 0, 0]));
|
|
471
|
+
const W = this.w, lw = (l, n) => W[`layers.${l}.${n}`];
|
|
472
|
+
// GEMM ops: { group, M, T }, the y grid is ceil(T / 16).
|
|
473
|
+
const gemm = (x, name, y, M, N, T, bias = true) => {
|
|
474
|
+
const res = [x, W[`${name}.weight`], bias && W[`${name}.bias`] ? W[`${name}.bias`] : this.zero, y, this.uniform([M, N, T, bias && W[`${name}.bias`] ? 1 : 0])];
|
|
475
|
+
return { res, M, N, T };
|
|
476
|
+
};
|
|
477
|
+
this.gemm = gemm;
|
|
478
|
+
const modOff = (n, part) => n * 3 * D + part * D; // part 0 scale (lnMod reads scale, shift), 2 gate
|
|
479
|
+
const u = {
|
|
480
|
+
rmsD: this.uniform([D, { f: CFG.rmsEps }]), rmsH: this.uniform([H, { f: CFG.rmsEps }]),
|
|
481
|
+
qkK: this.uniform([CFG.hd, D, { f: CFG.qkEps }, 0]), self: this.uniform([CFG.heads, CFG.hd, K, { f: Math.fround(1 / Math.sqrt(CFG.hd)) }]),
|
|
482
|
+
gelu: this.uniform([K * F]), bK: this.uniform([D, K]),
|
|
483
|
+
};
|
|
484
|
+
this.u = u;
|
|
485
|
+
// The target's pieces: its token table (anchor embedding, uniform_embeds) and its lm_head GEMM over 16 slots.
|
|
486
|
+
const emb = t.w['token_embd.weight'].raw, et = t.types['token_embd.weight'], rowBytes = gpuRowBytes(et, H);
|
|
487
|
+
this.g = {
|
|
488
|
+
anchorE: t.bindR(`embedB_${et}`, [emb, t.grid, a.anchorId, a.anchorE, t.uniform([H, rowBytes]), a.qbA]),
|
|
489
|
+
anchorP: gemm(a.anchorE, 'anchor_proj', a.anchorC0, D, H, 1),
|
|
490
|
+
anchorN: this.bind('rmsRows', [a.anchorC0, W['anchor_norm.weight'], a.anchorC, u.rmsD]),
|
|
491
|
+
bcast: this.bind('broadcast', [a.h0, a.x, u.bK]),
|
|
492
|
+
// The 16 slots through the target's head on its verify-block matvec (one read of the table), else its GEMM.
|
|
493
|
+
lm: t.kernels[`mvT_${et}`]
|
|
494
|
+
? { k: `mvT_${et}`, g: t.bindR(`mvT_${et}`, [a.out, emb, t.grid, a.logits, t.uniform([t.cfg.vocab, H, rowBytes, 0]), a.qbK]), wg: Math.ceil(t.cfg.vocab / MVT_ROWS), small: true }
|
|
495
|
+
: { k: `mm_${et}`, g: t.bindR(`mm_${et}`, [a.out, emb, t.grid, a.logits, t.uniform([t.cfg.vocab, H, rowBytes, 0]), a.qbK]), wg: Math.ceil(t.cfg.vocab / MM_TILE) },
|
|
496
|
+
am: t.bind('argmaxRows', [a.logits, a.am, t.uniform([t.cfg.vocab])]),
|
|
497
|
+
final: this.bind('lnMod', [a.x, this.mod, a.h, this.uniform([D, modOff(CFG.layers * 3, 0), { f: CFG.lnEps }, 0])]),
|
|
498
|
+
head: gemm(a.h, 'head', a.out, H, D, K),
|
|
499
|
+
};
|
|
500
|
+
this.layers = [];
|
|
501
|
+
for (let l = 0; l < CFG.layers; l++) {
|
|
502
|
+
const ln = (n) => this.bind('lnMod', [a.x, this.mod, a.h, this.uniform([D, modOff(3 * l + n, 0), { f: CFG.lnEps }, 0])]);
|
|
503
|
+
const ga = (n) => this.bind('gateAdd', [a.x, a.y, this.mod, this.uniform([D, modOff(3 * l + n, 2), K, 0])]);
|
|
504
|
+
const p = (n) => `layers.${l}.${n}`;
|
|
505
|
+
this.layers.push({
|
|
506
|
+
lnS: ln(0), sq: gemm(a.h, p('self_attn.q_proj'), a.q, D, D, K, false), sk: gemm(a.h, p('self_attn.k_proj'), a.k, D, D, K, false), sv: gemm(a.h, p('self_attn.v_proj'), a.v, D, D, K, false),
|
|
507
|
+
sqn: this.bind('headNormRope', [a.q, lw(l, 'self_attn.q_norm.weight'), a.ropeS, u.qkK]), skn: this.bind('headNormRope', [a.k, lw(l, 'self_attn.k_norm.weight'), a.ropeS, u.qkK]),
|
|
508
|
+
sat: this.bind('attnF32', [a.q, a.k, a.v, a.o, u.self]), so: gemm(a.o, p('self_attn.o_proj'), a.y, D, D, K, false), gaS: ga(0),
|
|
509
|
+
lnC: ln(1), cq: gemm(a.h, p('cross_attn.q_proj'), a.q, D, D, K, false), cqn: this.bind('headNormRope', [a.q, lw(l, 'cross_attn.q_norm.weight'), a.ropeS, u.qkK]),
|
|
510
|
+
co: gemm(a.o, p('cross_attn.o_proj'), a.y, D, D, K, false), gaC: ga(1),
|
|
511
|
+
lnM: ln(2), f1: gemm(a.h, p('mlp.fc1'), a.u, F, D, K), gl: this.bind('gelu', [a.u, u.gelu]), f2: gemm(a.u, p('mlp.fc2'), a.y, D, F, K), gaM: ga(2),
|
|
512
|
+
// The anchor's context slot.
|
|
513
|
+
ak: gemm(a.anchorC, p('cross_attn.k_proj'), a.anchorK, D, D, 1, false), av: gemm(a.anchorC, p('cross_attn.v_proj'), a.anchorV, D, D, 1, false),
|
|
514
|
+
akn: this.bind('headNormRope', [a.anchorK, lw(l, 'cross_attn.k_norm.weight'), a.ropeA, u.qkK]),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
// Constant input of every slot at zero noise: x_embed(state_norm(uniform_embeds)).
|
|
518
|
+
const enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
|
|
519
|
+
const rowWords = rowBytes / 4;
|
|
520
|
+
this.run(pass, 'colMeanQ8', this.bind('colMeanQ8', [emb, a.uniform, this.uniform([t.cfg.vocab, H, rowWords, 0])]), Math.ceil(H / 256));
|
|
521
|
+
this.run(pass, 'rmsRows', this.bind('rmsRows', [a.uniform, W['state_norm.weight'], a.inRow, u.rmsH]), 1);
|
|
522
|
+
this.runGemm(pass, gemm(a.inRow, 'x_embed', a.h0, D, H, 1));
|
|
523
|
+
pass.end(); dev.queue.submit([enc.finish()]);
|
|
524
|
+
this.length = 0;
|
|
525
|
+
}
|
|
526
|
+
// A GEMM op on the kernel its token count suits: gemvT1 / gemvT17 (one read of the weights) for up to 17 tokens, the
|
|
527
|
+
// tiled GEMM for longer runs (a prompt's context rows). Bind groups are made on first use and kept on the op.
|
|
528
|
+
runGemm(pass, op) {
|
|
529
|
+
const T = op.T, k = T <= 1 ? 'gemvT1' : T <= 17 && op.N % 256 === 0 ? 'gemvT17' : 'gemmBf16';
|
|
530
|
+
const g = (op.groups ||= {})[k] ||= this.bind(k, op.res);
|
|
531
|
+
pass.setPipeline(this.pipeline(k)); pass.setBindGroup(0, g);
|
|
532
|
+
if (k === 'gemmBf16') pass.dispatchWorkgroups(Math.ceil(op.M / 16), Math.ceil(T / 16)); else pass.dispatchWorkgroups(Math.ceil(op.M / 256));
|
|
533
|
+
}
|
|
534
|
+
reset() { this.length = 0; }
|
|
535
|
+
|
|
536
|
+
// Append the target's feature rows 0 … n − 1 (its b.feat after a chunk) as context positions start … start + n − 1.
|
|
537
|
+
extendContext(n, start) {
|
|
538
|
+
if (start + n + CFG.block + 1 > this.maxCtx) throw new Error('FlashMap: context full');
|
|
539
|
+
const D = CFG.dim, a = this.a, t = this.t, dev = this.device;
|
|
540
|
+
dev.queue.writeBuffer(a.ropeC, 0, ropeRows(start, n));
|
|
541
|
+
const enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
|
|
542
|
+
this.runGemm(pass, this.gemm(t.b.feat, 'fc', a.ctx, D, t.featWidth, n, false));
|
|
543
|
+
this.run(pass, 'rmsRows', this.bind('rmsRows', [a.ctx, this.w['hidden_norm.weight'], a.ctxN, this.u.rmsD]), n);
|
|
544
|
+
for (let l = 0; l < CFG.layers; l++) {
|
|
545
|
+
const p = (x) => `layers.${l}.cross_attn.${x}`;
|
|
546
|
+
this.runGemm(pass, this.gemm(a.ctxN, p('k_proj'), a.ck, D, D, n, false));
|
|
547
|
+
this.runGemm(pass, this.gemm(a.ctxN, p('v_proj'), a.cv, D, D, n, false));
|
|
548
|
+
this.run(pass, 'headNormRope', this.bind('headNormRope', [a.ck, this.w[p('k_norm.weight')], a.ropeC, this.u.qkK]), CFG.heads, n);
|
|
549
|
+
this.run(pass, 'kvWrite', this.bind('kvWrite', [a.ck, a.cv, this.kc[l], this.vc[l], this.uniform([D, start, n, 0])]), Math.ceil(D / 256), n);
|
|
550
|
+
}
|
|
551
|
+
pass.end(); dev.queue.submit([enc.finish()]);
|
|
552
|
+
this.length = start + n;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// The 16 tokens after `anchor`, which sits at context position `pos` (= the number of verified tokens before it).
|
|
556
|
+
async draft(anchor, pos) {
|
|
557
|
+
const D = CFG.dim, K = CFG.block, a = this.a, t = this.t, dev = this.device, g = this.g;
|
|
558
|
+
dev.queue.writeBuffer(a.anchorId, 0, new Uint32Array([anchor]));
|
|
559
|
+
dev.queue.writeBuffer(a.ropeA, 0, ropeRows(pos, 1));
|
|
560
|
+
dev.queue.writeBuffer(a.ropeS, 0, ropeRows(pos + 1, K));
|
|
561
|
+
const L = pos + 1, cross = this.uniform([CFG.heads, CFG.hd, L, { f: Math.fround(1 / Math.sqrt(CFG.hd)) }]);
|
|
562
|
+
const enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
|
|
563
|
+
// The anchor's context slot at pos.
|
|
564
|
+
t.dispatch(pass, `embedB_${t.types['token_embd.weight']}`, g.anchorE, Math.ceil(CFG.targetDim / 8 / 256), 1); // the target's kernel
|
|
565
|
+
this.runGemm(pass, g.anchorP); this.run(pass, 'rmsRows', g.anchorN, 1);
|
|
566
|
+
for (let l = 0; l < CFG.layers; l++) {
|
|
567
|
+
const y = this.layers[l];
|
|
568
|
+
this.runGemm(pass, y.ak); this.runGemm(pass, y.av);
|
|
569
|
+
this.run(pass, 'headNormRope', y.akn, CFG.heads, 1);
|
|
570
|
+
this.run(pass, 'kvWrite', this.bind('kvWrite', [a.anchorK, a.anchorV, this.kc[l], this.vc[l], this.uniform([D, pos, 1, 0])]), Math.ceil(D / 256), 1);
|
|
571
|
+
}
|
|
572
|
+
// The jump 0 → 1 from zero noise.
|
|
573
|
+
this.run(pass, 'broadcast', g.bcast, Math.ceil(D / 256), K);
|
|
574
|
+
for (let l = 0; l < CFG.layers; l++) {
|
|
575
|
+
const y = this.layers[l];
|
|
576
|
+
this.run(pass, 'lnMod', y.lnS, K);
|
|
577
|
+
this.runGemm(pass, y.sq); this.runGemm(pass, y.sk); this.runGemm(pass, y.sv);
|
|
578
|
+
this.run(pass, 'headNormRope', y.sqn, CFG.heads, K); this.run(pass, 'headNormRope', y.skn, CFG.heads, K);
|
|
579
|
+
this.run(pass, 'attnF32', y.sat, CFG.heads, K);
|
|
580
|
+
this.runGemm(pass, y.so); this.run(pass, 'gateAdd', y.gaS, Math.ceil(D / 256), K);
|
|
581
|
+
this.run(pass, 'lnMod', y.lnC, K);
|
|
582
|
+
this.runGemm(pass, y.cq); this.run(pass, 'headNormRope', y.cqn, CFG.heads, K);
|
|
583
|
+
this.run(pass, 'attnF16', this.bind('attnF16', [a.q, this.kc[l], this.vc[l], a.o, cross]), CFG.heads, K);
|
|
584
|
+
this.runGemm(pass, y.co); this.run(pass, 'gateAdd', y.gaC, Math.ceil(D / 256), K);
|
|
585
|
+
this.run(pass, 'lnMod', y.lnM, K);
|
|
586
|
+
this.runGemm(pass, y.f1); this.run(pass, 'gelu', y.gl, Math.ceil(K * CFG.ff / 256));
|
|
587
|
+
this.runGemm(pass, y.f2); this.run(pass, 'gateAdd', y.gaM, Math.ceil(D / 256), K);
|
|
588
|
+
}
|
|
589
|
+
this.run(pass, 'lnMod', g.final, K);
|
|
590
|
+
this.runGemm(pass, g.head);
|
|
591
|
+
if (g.lm.small) t.dispatch(pass, g.lm.k, g.lm.g, g.lm.wg);
|
|
592
|
+
else t.dispatch(pass, g.lm.k, g.lm.g, g.lm.wg, Math.ceil(K / (t.matrixKernels ? MM_SG_TOKENS : MM_TILE)));
|
|
593
|
+
t.dispatch(pass, 'argmaxRows', g.am, K);
|
|
594
|
+
pass.end();
|
|
595
|
+
enc.copyBufferToBuffer(a.am, 0, a.rbAm, 0, K * 4);
|
|
596
|
+
dev.queue.submit([enc.finish()]);
|
|
597
|
+
await a.rbAm.mapAsync(1);
|
|
598
|
+
const out = Array.from(new Uint32Array(a.rbAm.getMappedRange().slice(0)));
|
|
599
|
+
a.rbAm.unmap();
|
|
600
|
+
return out;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Debug: the 16 slots' DiT output (before the lm_head) of the last draft.
|
|
604
|
+
async readOut() {
|
|
605
|
+
const n = CFG.block * CFG.targetDim * 4, rb = this.device.createBuffer({ size: n, usage: MAP_READ | COPY_DST });
|
|
606
|
+
const enc = this.device.createCommandEncoder(); enc.copyBufferToBuffer(this.a.out, 0, rb, 0, n); this.device.queue.submit([enc.finish()]);
|
|
607
|
+
await rb.mapAsync(1); const v = new Float32Array(rb.getMappedRange().slice(0)); rb.unmap(); rb.destroy(); return v;
|
|
608
|
+
}
|
|
609
|
+
// Debug: the first n values, and counts of NaN and zero, of named working buffers (this.a) or of this.mod.
|
|
610
|
+
async peek(names, n = 6) {
|
|
611
|
+
const out = {};
|
|
612
|
+
for (const name of names) {
|
|
613
|
+
const src = name === 'mod' ? this.mod : this.a[name], size = Math.min(src.size, 1 << 20);
|
|
614
|
+
const rb = this.device.createBuffer({ size, usage: MAP_READ | COPY_DST });
|
|
615
|
+
const enc = this.device.createCommandEncoder(); enc.copyBufferToBuffer(src, 0, rb, 0, size); this.device.queue.submit([enc.finish()]);
|
|
616
|
+
await rb.mapAsync(1); const v = new Float32Array(rb.getMappedRange().slice(0)); rb.unmap(); rb.destroy();
|
|
617
|
+
let nan = 0, zero = 0; for (const x of v) { if (Number.isNaN(x)) nan++; else if (x === 0) zero++; }
|
|
618
|
+
out[name] = { head: Array.from(v.slice(0, n), (x) => +x.toPrecision(5)), nan, zero, of: v.length };
|
|
619
|
+
}
|
|
620
|
+
return out;
|
|
621
|
+
}
|
|
622
|
+
destroy() { for (const b of Object.values(this.w || {})) b.destroy(); }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Lossless speculative greedy decoding: prompt `ids` → yields { tokens, accepted, drafted } per round (tokens: the new
|
|
626
|
+
// ones), or { tokens, plain: true } per plain step. The target must be a Qwen3Dense loaded with captureLayers; the
|
|
627
|
+
// drafter one from FlashMapDrafter.load.
|
|
628
|
+
// verify: how many of the 16 drafts a round checks. 'adaptive' (default) checks 7 (an 8-token verify block, about half
|
|
629
|
+
// the cost of 17) while recent rounds accept fewer than 5, else all 16; a number fixes it.
|
|
630
|
+
// fallback (default on): when rounds yield fewer tokens than plain steps would in the same time (a slow average under
|
|
631
|
+
// 85% of round time over plain-step time, both measured as it runs), decode plainly for probeEvery tokens, with the steps still feeding the
|
|
632
|
+
// drafter's context, then try a round again. Chat text drafts poorly; code and maths draft well.
|
|
633
|
+
export async function* specGenerate(target, drafter, ids, { maxNewTokens = 256, stopTokenIds = target.stopTokenIds, rounds, verify = 'adaptive', fallback = true, probeEvery = 16 } = {}) {
|
|
634
|
+
const stops = new Set(stopTokenIds || []);
|
|
635
|
+
let ema = 4, emaTokens = 5, roundMs = null, stepMs = 30, plainLeft = 0, featRows = 0, featStart = 0, catchUp = false, nRounds = 0;
|
|
636
|
+
target.reset(); drafter.reset();
|
|
637
|
+
let next = null;
|
|
638
|
+
for (let i = 0; i < ids.length; i += target.chunkTokens) {
|
|
639
|
+
const chunk = ids.slice(i, i + target.chunkTokens), last = i + target.chunkTokens >= ids.length;
|
|
640
|
+
next = await target.prefillChunk(chunk, last ? 'argmax' : 'none');
|
|
641
|
+
drafter.extendContext(chunk.length, i);
|
|
642
|
+
}
|
|
643
|
+
let produced = 1;
|
|
644
|
+
yield { tokens: [next], accepted: 0, drafted: 0, prefill: true };
|
|
645
|
+
if (stops.has(next)) return;
|
|
646
|
+
while (produced < maxNewTokens) {
|
|
647
|
+
// Near the drafter's context limit: plain steps to the end (no more drafting).
|
|
648
|
+
if (target.position + 2 * CFG.block + 2 > drafter.maxCtx) {
|
|
649
|
+
if (target.position >= target.maxCtx) return;
|
|
650
|
+
const tok = await target.step(next, 'argmax');
|
|
651
|
+
produced++; next = tok;
|
|
652
|
+
yield { tokens: [tok], plain: true };
|
|
653
|
+
if (stops.has(tok)) return;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (plainLeft > 0) {
|
|
657
|
+
// A plain step on `next`, its features into the next free feature row.
|
|
658
|
+
if (featRows === 0) featStart = target.position;
|
|
659
|
+
const t0 = performance.now();
|
|
660
|
+
target.stepFeatRow = featRows;
|
|
661
|
+
const tok = await target.step(next, 'argmax');
|
|
662
|
+
target.stepFeatRow = null;
|
|
663
|
+
stepMs = 0.7 * stepMs + 0.3 * (performance.now() - t0);
|
|
664
|
+
featRows++; plainLeft--; produced++; next = tok;
|
|
665
|
+
if (plainLeft === 0 || featRows >= target.chunkTokens || stops.has(tok) || produced >= maxNewTokens) { drafter.extendContext(featRows, featStart); featRows = 0; catchUp = true; }
|
|
666
|
+
yield { tokens: [tok], plain: true };
|
|
667
|
+
if (stops.has(tok)) return;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const pos = target.position, anchor = next, K = verify === 'adaptive' ? (ema >= 5 ? CFG.block : 7) : Math.min(CFG.block, verify);
|
|
671
|
+
const t0 = performance.now();
|
|
672
|
+
const drafts = (await drafter.draft(anchor, pos)).slice(0, K);
|
|
673
|
+
const t1 = performance.now();
|
|
674
|
+
const rows = await target.prefillChunk([anchor, ...drafts], 'rows');
|
|
675
|
+
const t2 = performance.now();
|
|
676
|
+
let acc = 0;
|
|
677
|
+
while (acc < K && drafts[acc] === rows[acc]) acc++;
|
|
678
|
+
let fresh = [...drafts.slice(0, acc), rows[acc]];
|
|
679
|
+
const hit = fresh.findIndex((x) => stops.has(x));
|
|
680
|
+
if (hit >= 0) fresh = fresh.slice(0, hit + 1);
|
|
681
|
+
ema = 0.6 * ema + 0.4 * acc;
|
|
682
|
+
emaTokens = 0.75 * emaTokens + 0.25 * (acc + 1); nRounds++;
|
|
683
|
+
// A round right after a plain stretch also waits for that stretch's context; keep it out of the round time.
|
|
684
|
+
if (!catchUp) roundMs = roundMs == null ? t2 - t0 : 0.6 * roundMs + 0.4 * (t2 - t0);
|
|
685
|
+
catchUp = false;
|
|
686
|
+
drafter.extendContext(acc + 1, pos);
|
|
687
|
+
target.rewind(pos + acc + 1);
|
|
688
|
+
next = fresh[fresh.length - 1];
|
|
689
|
+
produced += fresh.length;
|
|
690
|
+
yield { tokens: fresh, accepted: acc, drafted: K, draftMs: t1 - t0, verifyMs: t2 - t1 };
|
|
691
|
+
if (hit >= 0 || (rounds && --rounds <= 0)) return;
|
|
692
|
+
// Fall back when rounds lose (tokens per round under 85% of break-even, on a slow average over at least 4 rounds),
|
|
693
|
+
// so chat text drops to plain steps while code and maths near break-even keep drafting; after each plain stretch one
|
|
694
|
+
// round probes again on the same average.
|
|
695
|
+
if (fallback && nRounds >= 4 && roundMs != null && emaTokens * stepMs < 0.85 * roundMs) plainLeft = probeEvery;
|
|
696
|
+
}
|
|
697
|
+
}
|