diskformer 0.5.3 → 0.6.4
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 +61 -0
- package/README.md +9 -8
- 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/gemma4_moe_ssd.js +2 -2
- 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 +3 -3
- 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 +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/* ternary_bonsai_2_mtp.js — MTP (multi-token prediction) speculative decoding for the vendored Ternary Bonsai 2
|
|
2
|
+
engine (ternary_bonsai_2_27b.js), behind the same specDecodeRunner() seam the DFlash 2 runner uses.
|
|
3
|
+
|
|
4
|
+
The draft head is ProCreations' r3 MTP block for Ternary Bonsai 2 27B (one Qwen3.5 full-attention decoder layer
|
|
5
|
+
+ its own norms, Q8_0, 451 MB), as grafted into CaryPalmer/Ternary-Bonsai-2-27B-262k-GGUF (Apache-2.0). That file
|
|
6
|
+
is the PTQ1_0 trunk LocalMind already loads plus the head block appended at its end, so the app range-reads only
|
|
7
|
+
the head's bytes (and the trunk's 20 KB output_norm) from it: MTP_HEAD below pins the revision, the file size and
|
|
8
|
+
every tensor's offset, so no GGUF header (11 MB of tokenizer) is downloaded.
|
|
9
|
+
|
|
10
|
+
Graph (llama.cpp PrismML `prism` branch, src/models/qwen35.cpp graph_mtp; trunk side `h_nextn`):
|
|
11
|
+
h = output_norm(trunk final hidden at position p-1) (the trunk's normed last-layer output)
|
|
12
|
+
e = token_embd(x_p), Hadamard-inverted to the plain basis (the engine's own embed op + inverse rotation)
|
|
13
|
+
x = eh_proj([enorm(e) ; hnorm(h)])
|
|
14
|
+
x += attn_output(attention(attn_norm(x)) * sigmoid(gate)) (24 q heads x 256, 4 kv heads, q/k RMS norm,
|
|
15
|
+
NEOX RoPE on 64 dims, theta 1e7, own KV cache)
|
|
16
|
+
x += ffn_down(silu(ffn_gate(n)) * ffn_up(n)), n = post_attention_norm(x)
|
|
17
|
+
h' = shared_head_norm(x); draft = argmax(lm_head(h')) (the trunk's lm_head, Hadamard-rotated input)
|
|
18
|
+
Depth N chains the head: the next step takes the draft token and h' (llama.cpp `draft-mtp`). The head's KV cache
|
|
19
|
+
row at position p holds the pair (x_p, h_{p-1}); after each verify the accepted rows are re-run with the trunk's
|
|
20
|
+
own h so the cache matches what llama.cpp keeps.
|
|
21
|
+
|
|
22
|
+
Output is greedy-identical to the engine's own decode: every emitted token is the trunk's argmax from the batched
|
|
23
|
+
verify pass, drafts only decide how many verify rows are accepted per cycle. */
|
|
24
|
+
|
|
25
|
+
const MTP_HEAD = {
|
|
26
|
+
url: 'https://huggingface.co/CaryPalmer/Ternary-Bonsai-2-27B-262k-GGUF/resolve/9f9b92cd8229cb13e473ca3d11055c3f60276508/Ternary-Bonsai-2-27B-PTQ1_0-mtp-procreations.gguf',
|
|
27
|
+
fileSize: 6397970272,
|
|
28
|
+
// [absolute offset, length]: the trunk's output_norm.weight, then the whole blk.64 head block (to end of file)
|
|
29
|
+
ranges: [[289240928, 20480], [5946650464, 451319808]],
|
|
30
|
+
bytes: 20480 + 451319808,
|
|
31
|
+
// name: [absolute offset, ggml type (0 F32, 8 Q8_0), ne0 (K), ne1 (N)]
|
|
32
|
+
tensors: {
|
|
33
|
+
output_norm: [289240928, 0, 5120, 1],
|
|
34
|
+
eh_proj: [5946650464, 8, 10240, 5120],
|
|
35
|
+
attn_norm: [6002356064, 0, 5120, 1],
|
|
36
|
+
ffn_down: [6002376544, 8, 17408, 5120],
|
|
37
|
+
ffn_gate: [6097076064, 8, 5120, 17408],
|
|
38
|
+
ffn_up: [6191775584, 8, 5120, 17408],
|
|
39
|
+
post_attention_norm: [6286475104, 0, 5120, 1],
|
|
40
|
+
attn_k_norm: [6286495584, 0, 256, 1],
|
|
41
|
+
attn_k: [6286496608, 8, 5120, 1024],
|
|
42
|
+
attn_output: [6292067168, 8, 6144, 5120],
|
|
43
|
+
attn_q_norm: [6325490528, 0, 256, 1],
|
|
44
|
+
attn_q: [6325491552, 8, 5120, 12288],
|
|
45
|
+
attn_v: [6392338272, 8, 5120, 1024],
|
|
46
|
+
shared_head_norm: [6397908832, 0, 5120, 1],
|
|
47
|
+
enorm: [6397929312, 0, 5120, 1],
|
|
48
|
+
hnorm: [6397949792, 0, 5120, 1],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const MCFG = { H: 5120, I: 17408, NH: 24, NKV: 4, HD: 256, ROT: 64, THETA: 1e7, EPS: 1e-6, VOCAB: 248320, MAXM: 8 };
|
|
52
|
+
|
|
53
|
+
// ---------------- WGSL ----------------
|
|
54
|
+
// Y[m, n] (+)= sum_k X[m, k] * W[n, k]; W is Q8_0 repacked at load: 32 int8 per block as 8 u32 (two vec4<u32>), the
|
|
55
|
+
// block scales as f16 pairs. 256 threads = 8 output rows x 32 lanes striding over the K blocks; M rows of X per call.
|
|
56
|
+
const WG_Q8 = (M) => /* wgsl */`
|
|
57
|
+
struct P { K: u32, N: u32, nb: u32, acc: u32 };
|
|
58
|
+
@group(0) @binding(0) var<uniform> p: P;
|
|
59
|
+
@group(0) @binding(1) var<storage, read> X: array<vec4<f32>>;
|
|
60
|
+
@group(0) @binding(2) var<storage, read> Wq: array<vec4<u32>>;
|
|
61
|
+
@group(0) @binding(3) var<storage, read> Ws: array<u32>;
|
|
62
|
+
@group(0) @binding(4) var<storage, read_write> Y: array<f32>;
|
|
63
|
+
const M: u32 = ${M}u;
|
|
64
|
+
var<workgroup> red: array<f32, ${256 * M}>;
|
|
65
|
+
fn i8x4(w: u32) -> vec4<f32> { return vec4<f32>(unpack4xI8(w)); }
|
|
66
|
+
@compute @workgroup_size(256)
|
|
67
|
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
68
|
+
let tid = lid.x; let lane = tid & 31u; let n = wg.x * 8u + (tid >> 5u);
|
|
69
|
+
var acc: array<f32, ${M}>;
|
|
70
|
+
for (var m = 0u; m < M; m++) { acc[m] = 0.0; }
|
|
71
|
+
if (n < p.N) {
|
|
72
|
+
let K4 = p.K >> 2u;
|
|
73
|
+
for (var b = lane; b < p.nb; b += 32u) {
|
|
74
|
+
let blk = n * p.nb + b;
|
|
75
|
+
let q0 = Wq[blk * 2u]; let q1 = Wq[blk * 2u + 1u];
|
|
76
|
+
let s = unpack2x16float(Ws[blk >> 1u])[blk & 1u];
|
|
77
|
+
let w0 = i8x4(q0.x); let w1 = i8x4(q0.y); let w2 = i8x4(q0.z); let w3 = i8x4(q0.w);
|
|
78
|
+
let w4 = i8x4(q1.x); let w5 = i8x4(q1.y); let w6 = i8x4(q1.z); let w7 = i8x4(q1.w);
|
|
79
|
+
for (var m = 0u; m < M; m++) {
|
|
80
|
+
let xb = m * K4 + b * 8u;
|
|
81
|
+
let d = dot(w0, X[xb]) + dot(w1, X[xb + 1u]) + dot(w2, X[xb + 2u]) + dot(w3, X[xb + 3u])
|
|
82
|
+
+ dot(w4, X[xb + 4u]) + dot(w5, X[xb + 5u]) + dot(w6, X[xb + 6u]) + dot(w7, X[xb + 7u]);
|
|
83
|
+
acc[m] += s * d;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (var m = 0u; m < M; m++) { red[m * 256u + tid] = acc[m]; }
|
|
88
|
+
workgroupBarrier();
|
|
89
|
+
for (var o = 16u; o > 0u; o >>= 1u) {
|
|
90
|
+
if (lane < o) { for (var m = 0u; m < M; m++) { red[m * 256u + tid] += red[m * 256u + tid + o]; } }
|
|
91
|
+
workgroupBarrier();
|
|
92
|
+
}
|
|
93
|
+
if (lane == 0u && n < p.N) {
|
|
94
|
+
for (var m = 0u; m < M; m++) {
|
|
95
|
+
let v = red[m * 256u + tid];
|
|
96
|
+
if (p.acc == 1u) { Y[m * p.N + n] += v; } else { Y[m * p.N + n] = v; }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}`;
|
|
100
|
+
|
|
101
|
+
// RMS norm with a weight, strided rows: Y[r*os + oo + i] = X[r*is + io + i] * rsqrt(mean(x^2) + eps) * W[i]
|
|
102
|
+
const WG_NORM = /* wgsl */`
|
|
103
|
+
struct P { D: u32, is: u32, io: u32, os: u32, oo: u32, eps: f32, pad0: u32, pad1: u32 };
|
|
104
|
+
@group(0) @binding(0) var<uniform> p: P;
|
|
105
|
+
@group(0) @binding(1) var<storage, read> X: array<f32>;
|
|
106
|
+
@group(0) @binding(2) var<storage, read> W: array<f32>;
|
|
107
|
+
@group(0) @binding(3) var<storage, read_write> Y: array<f32>;
|
|
108
|
+
var<workgroup> red: array<f32, 256>;
|
|
109
|
+
@compute @workgroup_size(256)
|
|
110
|
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
111
|
+
let r = wg.x; let tid = lid.x; let ib = r * p.is + p.io; let ob = r * p.os + p.oo;
|
|
112
|
+
var s = 0.0;
|
|
113
|
+
for (var i = tid; i < p.D; i += 256u) { let v = X[ib + i]; s += v * v; }
|
|
114
|
+
red[tid] = s; workgroupBarrier();
|
|
115
|
+
for (var o = 128u; o > 0u; o >>= 1u) { if (tid < o) { red[tid] += red[tid + o]; } workgroupBarrier(); }
|
|
116
|
+
let inv = inverseSqrt(red[0] / f32(p.D) + p.eps);
|
|
117
|
+
for (var i = tid; i < p.D; i += 256u) { Y[ob + i] = X[ib + i] * inv * W[i]; }
|
|
118
|
+
}`;
|
|
119
|
+
|
|
120
|
+
// Per-head q/k RMS norm + NEOX RoPE (pairs i, i+32 over the first 64 dims, freq theta^(-2i/64), position pos0+m), and
|
|
121
|
+
// the K/V cache write at that position. Workgroup (m, j): j < 24 a q head, 24..27 a k head, 28..31 a v head.
|
|
122
|
+
const WG_QKPREP = /* wgsl */`
|
|
123
|
+
struct P { pos0: u32, pad0: u32, pad1: u32, pad2: u32, theta: f32, eps: f32, pad3: f32, pad4: f32 };
|
|
124
|
+
@group(0) @binding(0) var<uniform> p: P;
|
|
125
|
+
@group(0) @binding(1) var<storage, read> Qf: array<f32>;
|
|
126
|
+
@group(0) @binding(2) var<storage, read> Kt: array<f32>;
|
|
127
|
+
@group(0) @binding(3) var<storage, read> Vt: array<f32>;
|
|
128
|
+
@group(0) @binding(4) var<storage, read> QN: array<f32>;
|
|
129
|
+
@group(0) @binding(5) var<storage, read> KN: array<f32>;
|
|
130
|
+
@group(0) @binding(6) var<storage, read_write> Q: array<f32>;
|
|
131
|
+
@group(0) @binding(7) var<storage, read_write> Kc: array<f32>;
|
|
132
|
+
@group(0) @binding(8) var<storage, read_write> Vc: array<f32>;
|
|
133
|
+
var<workgroup> red: array<f32, 256>;
|
|
134
|
+
var<workgroup> sh: array<f32, 256>;
|
|
135
|
+
@compute @workgroup_size(256)
|
|
136
|
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
137
|
+
let m = wg.x; let j = wg.y; let tid = lid.x; let pos = p.pos0 + m;
|
|
138
|
+
if (j >= 28u) { Vc[pos * 1024u + (j - 28u) * 256u + tid] = Vt[m * 1024u + (j - 28u) * 256u + tid]; return; }
|
|
139
|
+
var x = 0.0; var w = 0.0;
|
|
140
|
+
if (j < 24u) { x = Qf[m * 12288u + j * 512u + tid]; w = QN[tid]; } else { x = Kt[m * 1024u + (j - 24u) * 256u + tid]; w = KN[tid]; }
|
|
141
|
+
red[tid] = x * x; workgroupBarrier();
|
|
142
|
+
for (var o = 128u; o > 0u; o >>= 1u) { if (tid < o) { red[tid] += red[tid + o]; } workgroupBarrier(); }
|
|
143
|
+
let y = x * inverseSqrt(red[0] / 256.0 + p.eps) * w;
|
|
144
|
+
sh[tid] = y; workgroupBarrier();
|
|
145
|
+
var outv = y;
|
|
146
|
+
if (tid < 64u) {
|
|
147
|
+
let i = tid & 31u;
|
|
148
|
+
let ang = f32(pos) * exp(-f32(2u * i) / 64.0 * log(p.theta));
|
|
149
|
+
let c = cos(ang); let s = sin(ang);
|
|
150
|
+
if (tid < 32u) { outv = y * c - sh[tid + 32u] * s; } else { outv = sh[tid - 32u] * s + y * c; }
|
|
151
|
+
}
|
|
152
|
+
if (j < 24u) { Q[(m * 24u + j) * 256u + tid] = outv; } else { Kc[pos * 1024u + (j - 24u) * 256u + tid] = outv; }
|
|
153
|
+
}`;
|
|
154
|
+
|
|
155
|
+
// Causal attention over the head's own KV cache, keys kvLo..pos0+m, online softmax in 256-key tiles; output gated by
|
|
156
|
+
// sigmoid(gate) (the second half of each q head's 512-wide slice). Workgroup (m, h), thread = output dim.
|
|
157
|
+
const WG_ATTN = /* wgsl */`
|
|
158
|
+
struct P { pos0: u32, kvLo: u32, pad0: u32, pad1: u32, scale: f32, pad2: f32, pad3: f32, pad4: f32 };
|
|
159
|
+
@group(0) @binding(0) var<uniform> p: P;
|
|
160
|
+
@group(0) @binding(1) var<storage, read> Q: array<vec4<f32>>;
|
|
161
|
+
@group(0) @binding(2) var<storage, read> Kc: array<vec4<f32>>;
|
|
162
|
+
@group(0) @binding(3) var<storage, read> Vc: array<f32>;
|
|
163
|
+
@group(0) @binding(4) var<storage, read> Qf: array<f32>;
|
|
164
|
+
@group(0) @binding(5) var<storage, read_write> O: array<f32>;
|
|
165
|
+
var<workgroup> q: array<vec4<f32>, 64>;
|
|
166
|
+
var<workgroup> sc: array<f32, 256>;
|
|
167
|
+
var<workgroup> red: array<f32, 256>;
|
|
168
|
+
@compute @workgroup_size(256)
|
|
169
|
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
170
|
+
let m = wg.x; let h = wg.y; let tid = lid.x; let kvh = h / 6u; let qpos = p.pos0 + m;
|
|
171
|
+
if (tid < 64u) { q[tid] = Q[(m * 24u + h) * 64u + tid]; }
|
|
172
|
+
workgroupBarrier();
|
|
173
|
+
var mx = -1e30; var l = 0.0; var acc = 0.0;
|
|
174
|
+
for (var t0 = p.kvLo; t0 <= qpos; t0 += 256u) {
|
|
175
|
+
let jk = t0 + tid;
|
|
176
|
+
var s = -1e30;
|
|
177
|
+
if (jk <= qpos) {
|
|
178
|
+
let kb = (jk * 4u + kvh) * 64u;
|
|
179
|
+
var d = 0.0;
|
|
180
|
+
for (var e = 0u; e < 64u; e++) { d += dot(q[e], Kc[kb + e]); }
|
|
181
|
+
s = d * p.scale;
|
|
182
|
+
}
|
|
183
|
+
red[tid] = s; workgroupBarrier();
|
|
184
|
+
for (var o = 128u; o > 0u; o >>= 1u) { if (tid < o) { red[tid] = max(red[tid], red[tid + o]); } workgroupBarrier(); }
|
|
185
|
+
let nm = max(mx, red[0]); workgroupBarrier();
|
|
186
|
+
let pj = select(0.0, exp(s - nm), jk <= qpos);
|
|
187
|
+
sc[tid] = pj; red[tid] = pj; workgroupBarrier();
|
|
188
|
+
for (var o = 128u; o > 0u; o >>= 1u) { if (tid < o) { red[tid] += red[tid + o]; } workgroupBarrier(); }
|
|
189
|
+
let corr = exp(mx - nm);
|
|
190
|
+
l = l * corr + red[0]; acc = acc * corr;
|
|
191
|
+
let cnt = min(256u, qpos - t0 + 1u);
|
|
192
|
+
for (var jj = 0u; jj < cnt; jj++) { acc += sc[jj] * Vc[((t0 + jj) * 4u + kvh) * 256u + tid]; }
|
|
193
|
+
mx = nm;
|
|
194
|
+
workgroupBarrier();
|
|
195
|
+
}
|
|
196
|
+
let g = Qf[m * 12288u + h * 512u + 256u + tid];
|
|
197
|
+
O[m * 6144u + h * 256u + tid] = acc / l * (1.0 / (1.0 + exp(-g)));
|
|
198
|
+
}`;
|
|
199
|
+
|
|
200
|
+
const WG_SILU_MUL = /* wgsl */`
|
|
201
|
+
struct P { n: u32, pad0: u32, pad1: u32, pad2: u32 };
|
|
202
|
+
@group(0) @binding(0) var<uniform> p: P;
|
|
203
|
+
@group(0) @binding(1) var<storage, read> U: array<f32>;
|
|
204
|
+
@group(0) @binding(2) var<storage, read_write> G: array<f32>;
|
|
205
|
+
@compute @workgroup_size(256)
|
|
206
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
207
|
+
let i = gid.x; if (i >= p.n) { return; }
|
|
208
|
+
let g = G[i]; G[i] = g / (1.0 + exp(-g)) * U[i];
|
|
209
|
+
}`;
|
|
210
|
+
|
|
211
|
+
// ---------------- the head ----------------
|
|
212
|
+
// Weights on the GPU, the KV cache, and one forward pass over M <= 8 rows at consecutive positions.
|
|
213
|
+
class MTPHead {
|
|
214
|
+
constructor(dev, opts = {}) {
|
|
215
|
+
this.dev = dev; this.log = opts.log || (() => {});
|
|
216
|
+
this.stats = { weightBytes: 0, passes: 0, rows: 0 };
|
|
217
|
+
this.buf = {}; this.w = {}; this.uni = []; this.pipes = {};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// source(absOffset, length) -> Promise<Uint8Array>: bytes of the grafted GGUF (or of any file with MTP_HEAD's layout)
|
|
221
|
+
async loadWeights(source, onProgress = () => {}) {
|
|
222
|
+
const { dev } = this; const S = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
|
223
|
+
let done = 0; const total = MTP_HEAD.bytes;
|
|
224
|
+
for (const [name, [off, type, K, N]] of Object.entries(MTP_HEAD.tensors)) {
|
|
225
|
+
if (type === 0) {
|
|
226
|
+
const raw = await source(off, K * 4); if (raw.byteLength !== K * 4) throw new Error(`MTP head: short read for ${name}`);
|
|
227
|
+
const b = dev.createBuffer({ size: K * 4, usage: S, label: 'mtp.' + name }); dev.queue.writeBuffer(b, 0, raw); this.w[name] = b; done += K * 4;
|
|
228
|
+
} else if (type === 8) {
|
|
229
|
+
const nb = K / 32; if (nb % 2) throw new Error(`MTP head: ${name} has an odd block count`);
|
|
230
|
+
const wq = dev.createBuffer({ size: N * nb * 32, usage: S, label: 'mtp.' + name + '.q' });
|
|
231
|
+
const ws = dev.createBuffer({ size: N * nb * 2, usage: S, label: 'mtp.' + name + '.s' });
|
|
232
|
+
const ROWS = 512;
|
|
233
|
+
for (let r0 = 0; r0 < N; r0 += ROWS) {
|
|
234
|
+
const rows = Math.min(ROWS, N - r0), len = rows * nb * 34;
|
|
235
|
+
const raw = await source(off + r0 * nb * 34, len); if (raw.byteLength !== len) throw new Error(`MTP head: short read for ${name}`);
|
|
236
|
+
const q = new Uint8Array(rows * nb * 32), s = new Uint16Array(rows * nb);
|
|
237
|
+
for (let b = 0; b < rows * nb; ++b) { const o = b * 34; s[b] = raw[o] | (raw[o + 1] << 8); q.set(raw.subarray(o + 2, o + 34), b * 32); }
|
|
238
|
+
dev.queue.writeBuffer(wq, r0 * nb * 32, q); dev.queue.writeBuffer(ws, r0 * nb * 2, s);
|
|
239
|
+
done += len; onProgress({ done, total });
|
|
240
|
+
}
|
|
241
|
+
this.w[name] = { q: wq, s: ws, K, N, nb };
|
|
242
|
+
this.stats.weightBytes += N * nb * 34;
|
|
243
|
+
} else throw new Error(`MTP head: unexpected type ${type} for ${name}`);
|
|
244
|
+
}
|
|
245
|
+
onProgress({ done: total, total });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Activation buffers + pipelines; kvCap = cache rows (positions) the head's KV cache holds.
|
|
249
|
+
async init(kvCap) {
|
|
250
|
+
const { dev } = this; const { H, I, MAXM } = MCFG;
|
|
251
|
+
const S = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
|
252
|
+
const mk = (name, floats) => { this.buf[name] = dev.createBuffer({ size: floats * 4, usage: S, label: 'mtp.' + name }); };
|
|
253
|
+
mk('E', MAXM * H); mk('Hn', MAXM * H); mk('Hraw', MAXM * H); mk('Xc', MAXM * 2 * H); mk('X', MAXM * H); mk('A', MAXM * H);
|
|
254
|
+
mk('Qf', MAXM * 12288); mk('Kt', MAXM * 1024); mk('Vt', MAXM * 1024); mk('Q', MAXM * 6144); mk('O', MAXM * 6144);
|
|
255
|
+
mk('F1', MAXM * I); mk('F2', MAXM * I); mk('Hout', MAXM * H); mk('Hlast', H); mk('zero', H);
|
|
256
|
+
const maxBind = dev.limits.maxStorageBufferBindingSize;
|
|
257
|
+
this.kvCap = Math.min(kvCap, Math.floor(maxBind / 4096));
|
|
258
|
+
mk('Kc', this.kvCap * 1024); mk('Vc', this.kvCap * 1024);
|
|
259
|
+
const compile = async (code, label) => {
|
|
260
|
+
const mod = dev.createShaderModule({ code, label });
|
|
261
|
+
const info = await mod.getCompilationInfo(); const errs = info.messages.filter((x) => x.type === 'error');
|
|
262
|
+
if (errs.length) throw new Error(`MTP ${label} WGSL: ${errs.map((x) => x.message).join('; ')}`);
|
|
263
|
+
return dev.createComputePipeline({ layout: 'auto', compute: { module: mod, entryPoint: 'main' }, label });
|
|
264
|
+
};
|
|
265
|
+
for (let M = 1; M <= MAXM; ++M) this.pipes['q8_' + M] = await compile(WG_Q8(M), 'q8_' + M);
|
|
266
|
+
this.pipes.norm = await compile(WG_NORM, 'norm');
|
|
267
|
+
this.pipes.qk = await compile(WG_QKPREP, 'qkprep');
|
|
268
|
+
this.pipes.attn = await compile(WG_ATTN, 'attn');
|
|
269
|
+
this.pipes.silu = await compile(WG_SILU_MUL, 'silu');
|
|
270
|
+
this.uniPool = []; this.uniUsed = 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#uniform(data) {
|
|
274
|
+
if (this.uniUsed >= this.uniPool.length) this.uniPool.push(this.dev.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }));
|
|
275
|
+
const u = this.uniPool[this.uniUsed++]; this.dev.queue.writeBuffer(u, 0, data); return u;
|
|
276
|
+
}
|
|
277
|
+
#bind(pipe, bufs) { return this.dev.createBindGroup({ layout: pipe.getBindGroupLayout(0), entries: bufs.map((b, i) => ({ binding: i, resource: { buffer: b } })) }); }
|
|
278
|
+
#dispatch(pass, pipe, bufs, x, y = 1) { pass.setPipeline(pipe); pass.setBindGroup(0, this.#bind(pipe, bufs)); pass.dispatchWorkgroups(x, y); }
|
|
279
|
+
|
|
280
|
+
#norm(pass, rows, D, X, is, io, W, Y, os, oo) {
|
|
281
|
+
const u = new ArrayBuffer(32); const v = new Uint32Array(u); v.set([D, is, io, os, oo]); new Float32Array(u)[5] = MCFG.EPS;
|
|
282
|
+
this.#dispatch(pass, this.pipes.norm, [this.#uniform(u), X, W, Y], rows);
|
|
283
|
+
}
|
|
284
|
+
#q8(pass, M, wname, X, Y, acc = 0) {
|
|
285
|
+
const t = this.w[wname];
|
|
286
|
+
this.#dispatch(pass, this.pipes['q8_' + M], [this.#uniform(new Uint32Array([t.K, t.N, t.nb, acc])), X, t.q, t.s, Y], Math.ceil(t.N / 8));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Normalize M raw trunk rows (src buffer region) with output_norm into Hn rows [dst, dst+M).
|
|
290
|
+
normTrunkRows(src, srcOffset, M, dst) {
|
|
291
|
+
const { dev, buf } = this; const H = MCFG.H; this.uniUsed = 0;
|
|
292
|
+
const e = dev.createCommandEncoder();
|
|
293
|
+
e.copyBufferToBuffer(src, srcOffset, buf.Hraw, 0, M * H * 4);
|
|
294
|
+
const pass = e.beginComputePass();
|
|
295
|
+
this.#norm(pass, M, H, buf.Hraw, H, 0, this.w.output_norm, buf.Hn, H, dst * H);
|
|
296
|
+
pass.end(); dev.queue.submit([e.finish()]);
|
|
297
|
+
}
|
|
298
|
+
// Copy one normed row into Hn[dst] (from Hlast, Hout[row] or zero).
|
|
299
|
+
setHnRow(dst, srcBuf, srcRow = 0) {
|
|
300
|
+
const e = this.dev.createCommandEncoder(); e.copyBufferToBuffer(srcBuf, srcRow * MCFG.H * 4, this.buf.Hn, dst * MCFG.H * 4, MCFG.H * 4); this.dev.queue.submit([e.finish()]);
|
|
301
|
+
}
|
|
302
|
+
saveLast(srcBuf, srcRow) {
|
|
303
|
+
const e = this.dev.createCommandEncoder(); e.copyBufferToBuffer(srcBuf, srcRow * MCFG.H * 4, this.buf.Hlast, 0, MCFG.H * 4); this.dev.queue.submit([e.finish()]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// One forward over M rows at positions pos0..pos0+M-1. Inputs: embeddings of the M tokens at embed (GPU buffer,
|
|
307
|
+
// row-major [>=M, H]) and Hn[0..M) (normed trunk/head h of each row's previous position). Writes KV rows, leaves
|
|
308
|
+
// shared_head_norm(x) in Hout[0..M).
|
|
309
|
+
forward(embed, embedOffset, M, pos0, kvLo) {
|
|
310
|
+
const { dev, buf, w } = this; const { H, I } = MCFG;
|
|
311
|
+
if (M < 1 || M > MCFG.MAXM) throw new Error(`MTP forward: ${M} rows`);
|
|
312
|
+
if (pos0 + M > this.kvCap) throw new Error(`MTP forward: position ${pos0 + M} beyond the head's KV cache (${this.kvCap})`);
|
|
313
|
+
this.uniUsed = 0;
|
|
314
|
+
const e = dev.createCommandEncoder();
|
|
315
|
+
e.copyBufferToBuffer(embed, embedOffset, buf.E, 0, M * H * 4);
|
|
316
|
+
const pass = e.beginComputePass();
|
|
317
|
+
this.#norm(pass, M, H, buf.E, H, 0, w.enorm, buf.Xc, 2 * H, 0);
|
|
318
|
+
this.#norm(pass, M, H, buf.Hn, H, 0, w.hnorm, buf.Xc, 2 * H, H);
|
|
319
|
+
this.#q8(pass, M, 'eh_proj', buf.Xc, buf.X);
|
|
320
|
+
this.#norm(pass, M, H, buf.X, H, 0, w.attn_norm, buf.A, H, 0);
|
|
321
|
+
this.#q8(pass, M, 'attn_q', buf.A, buf.Qf); this.#q8(pass, M, 'attn_k', buf.A, buf.Kt); this.#q8(pass, M, 'attn_v', buf.A, buf.Vt);
|
|
322
|
+
const qu = new ArrayBuffer(32); new Uint32Array(qu)[0] = pos0; const qf = new Float32Array(qu); qf[4] = MCFG.THETA; qf[5] = MCFG.EPS;
|
|
323
|
+
this.#dispatch(pass, this.pipes.qk, [this.#uniform(qu), buf.Qf, buf.Kt, buf.Vt, w.attn_q_norm, w.attn_k_norm, buf.Q, buf.Kc, buf.Vc], M, 32);
|
|
324
|
+
const au = new ArrayBuffer(32); new Uint32Array(au).set([pos0, kvLo]); new Float32Array(au)[4] = 1 / Math.sqrt(MCFG.HD);
|
|
325
|
+
this.#dispatch(pass, this.pipes.attn, [this.#uniform(au), buf.Q, buf.Kc, buf.Vc, buf.Qf, buf.O], M, 24);
|
|
326
|
+
this.#q8(pass, M, 'attn_output', buf.O, buf.X, 1);
|
|
327
|
+
this.#norm(pass, M, H, buf.X, H, 0, w.post_attention_norm, buf.A, H, 0);
|
|
328
|
+
this.#q8(pass, M, 'ffn_gate', buf.A, buf.F1); this.#q8(pass, M, 'ffn_up', buf.A, buf.F2);
|
|
329
|
+
this.#dispatch(pass, this.pipes.silu, [this.#uniform(new Uint32Array([M * I, 0, 0, 0])), buf.F2, buf.F1], Math.ceil(M * I / 256));
|
|
330
|
+
this.#q8(pass, M, 'ffn_down', buf.F1, buf.X, 1);
|
|
331
|
+
this.#norm(pass, M, H, buf.X, H, 0, w.shared_head_norm, buf.Hout, H, 0);
|
|
332
|
+
pass.end(); dev.queue.submit([e.finish()]);
|
|
333
|
+
this.stats.passes++; this.stats.rows += M;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
dispose() {
|
|
337
|
+
for (const b of Object.values(this.buf)) { try { b.destroy(); } catch (_) {} }
|
|
338
|
+
for (const t of Object.values(this.w)) { try { if (t.q) { t.q.destroy(); t.s.destroy(); } else t.destroy(); } catch (_) {} }
|
|
339
|
+
for (const u of this.uniPool || []) { try { u.destroy(); } catch (_) {} }
|
|
340
|
+
this.buf = {}; this.w = {};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------- the runner ----------------
|
|
345
|
+
const MTP_PREFILL_BLOCKS = [16, 32, 64, 128, 256, 512];
|
|
346
|
+
|
|
347
|
+
class MTPRunner {
|
|
348
|
+
static async create(engine, opts = {}) {
|
|
349
|
+
const r = new MTPRunner(engine, opts);
|
|
350
|
+
await r.#init(opts);
|
|
351
|
+
const cache = engine.generationState?.cache;
|
|
352
|
+
if (cache) await r.#resources(cache);
|
|
353
|
+
return r;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
constructor(engine, opts) {
|
|
357
|
+
this.engine = engine; this.inner = engine.model; this.rt = this.inner.runtime; this.dev = this.rt.host.device; this.cfg = this.inner.config;
|
|
358
|
+
this.I = engine.constructor.__dflashInternals;
|
|
359
|
+
if (!this.I || !this.I.lh || !this.I.ch || !this.I.RewindSession) throw new Error('MTPRunner: engine is missing the dflash internals hook (regenerate ternary_bonsai_2_27b.js)');
|
|
360
|
+
this.depth = opts.depth ?? 2;
|
|
361
|
+
if (!(this.depth >= 1 && this.depth <= MCFG.MAXM - 1)) throw new Error(`MTPRunner: depth must be 1..${MCFG.MAXM - 1}`);
|
|
362
|
+
this.window = opts.window ?? 1024; // prompt rows (the tail of each prefill) fed to the head's KV cache
|
|
363
|
+
// Adaptive depth (up to `depth`): keep a running estimate of how often the draft at each position is accepted
|
|
364
|
+
// (given the earlier ones were), and before each cycle pick the depth with the most expected tokens per unit of
|
|
365
|
+
// time: depth d yields 1 + p1 + p1*p2 + ... tokens for a cycle that costs `cost[d]` plain decode steps (measured
|
|
366
|
+
// in headless Chrome, M4 Pro: a 2-row verify + one head step ~1.8 steps, a 3-row verify + two head steps ~2.15).
|
|
367
|
+
// Depth 0 = plain decode through the engine's own decode graph for `plainRun` tokens, then probe again at depth 1
|
|
368
|
+
// with the estimates pulled halfway back to the 0.8 prior.
|
|
369
|
+
this.adaptive = !!opts.adaptive;
|
|
370
|
+
this.cost = opts.cost ?? [1, 1.8, 2.15, 2.6]; this.alpha = opts.alpha ?? 0.1; this.plainRun = opts.plainRun ?? 32;
|
|
371
|
+
this.smallM = opts.smallM === 'off' ? undefined : { precision: opts.smallM ?? 'f16' };
|
|
372
|
+
this.log = opts.log || (() => {});
|
|
373
|
+
this.H = this.cfg.hidden_size; this.VOC = this.cfg.vocab_size;
|
|
374
|
+
if (this.H !== MCFG.H || this.VOC !== MCFG.VOCAB) throw new Error(`MTPRunner: target H/V ${this.H}/${this.VOC} != head ${MCFG.H}/${MCFG.VOCAB}`);
|
|
375
|
+
this.res = new Map();
|
|
376
|
+
this.stats = { cycles: 0, tokens: 0, accepted: 0, drafted: 0, generations: 0, plainTokens: 0, depthChanges: 0 };
|
|
377
|
+
this.disposed = false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async #init(opts) {
|
|
381
|
+
const { I, inner, H, VOC } = this;
|
|
382
|
+
this.head = new MTPHead(this.dev, { log: this.log });
|
|
383
|
+
await this.head.loadWeights(opts.head, opts.onProgress || (() => {}));
|
|
384
|
+
class Micro extends I.f0 { constructor(emit) { super(inner, null, 8); this._emit = emit; } buildEmission() { return this._emit(); } }
|
|
385
|
+
const micro = async (name, emit) => { const s = new Micro(() => { const S = new I.ba(); const b = I._i(S); const t = emit(S, b); return { graph: S.finish({ name }), weights: b.boundWeights, states: b.states, ...t }; }); await s.build(); return s; };
|
|
386
|
+
// token embedding in the plain basis: LlamaEmbed + inverse Hadamard (what the prefill prologue emits)
|
|
387
|
+
this.embedS = await micro('mtp-embed', (S, b) => {
|
|
388
|
+
const re = b.w; const ids = S.stepInput('input_ids', 'uint32', [8]); const hid = S.scratch('embed.hidden', 'float32', [8, H]); let h;
|
|
389
|
+
if (inner.embedBits && inner.embedScales) h = S.op('com.xenova.LlamaEmbed', { inputT: ids, bitsT: re('embed.bits', inner.embedBits), scalesT: re('embed.scales', inner.embedScales), hiddenT: hid }, { args: { hiddenSize: H, vocabSize: VOC, seqLen: 8, format: inner.embedFormat ?? 'q8_rows' } }).hiddenT;
|
|
390
|
+
else { const ke = I.pi(re, inner)('top'); h = S.op('com.xenova.LlamaEmbed', { inputT: ids, weightsT: ke, hiddenT: hid }, { args: { embedOffset: inner.offsets.top.embed_tokens, hiddenSize: H, vocabSize: VOC, seqLen: 8 } }).hiddenT; }
|
|
391
|
+
h = I.bi(S, inner, re)(h, 'embed_tokens', true); S.output(h, 'embed'); return {};
|
|
392
|
+
});
|
|
393
|
+
const embedT = this.embedS.compiled.tensor('embed'); this.embedBuf = embedT.buffer; this.embedOff = embedT.byteOffset ?? 0;
|
|
394
|
+
this.embedIds = this.embedS.compiled.tensor('input_ids');
|
|
395
|
+
// the trunk's lm_head on one row: Hadamard rotation + Lut2SmallMGemm (M = 1) + GPU argmax
|
|
396
|
+
if (!(inner.lmHeadQ4 && inner.lmHeadQ4Scales && inner.lmHeadLut === 9 && this.cfg.prismHadamard?.weights.includes('output.weight'))) throw new Error('MTPRunner: expected a prism-rotated lut2 lm_head (lmHeadLut 9)');
|
|
397
|
+
const prec = (this.smallM && this.smallM.precision) || 'f16';
|
|
398
|
+
this.headS = await micro('mtp-head', (S, b) => {
|
|
399
|
+
const re = b.w; const x = S.stepInput('hid', 'float32', [1, H]); const rot = I.bi(S, inner, re)(x, 'lm_head'); const lg = S.scratch('logits', 'float32', [1, VOC]);
|
|
400
|
+
S.op('com.xenova.Lut2SmallMGemm', { aT: S.view(rot, 0, 'float32', [1, H], 'head.in'), bitsT: re('head.bits', inner.lmHeadQ4), scalesT: re('head.scales', inner.lmHeadQ4Scales), yT: lg }, { args: { M: 1, inFeatures: H, outFeatures: VOC, blockOffset: 0, outStride: VOC, dstColStart: 0, lut: 9, precision: prec, kSplits: 1 } });
|
|
401
|
+
const am = S.op('ai.onnx.ArgMax', { x: lg }, { attrs: { axis: 1, keepdims: 0 } }).y;
|
|
402
|
+
S.output(am, 'argmax'); return {};
|
|
403
|
+
});
|
|
404
|
+
this.headIn = this.headS.compiled.tensor('hid'); this.headArg = this.headS.compiled.tensor('argmax');
|
|
405
|
+
this.log(`MTPRunner ready: depth ${this.depth}, head ${(this.head.stats.weightBytes / 1e6).toFixed(0)} MB on GPU`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async #resources(cache) {
|
|
409
|
+
let r = this.res.get(cache); if (r) return r;
|
|
410
|
+
const { I, inner, smallM } = this; const LV = this.depth + 1;
|
|
411
|
+
if (!this.head.kvCap) await this.head.init(cache.maxLength + 8);
|
|
412
|
+
const sessions = new Map();
|
|
413
|
+
const session = async (T, opts) => { const key = T + (opts.allRowsHead ? 'v' : 'p'); let s = sessions.get(key); if (s) return s; class S extends I.ch { buildEmission() { return I.lh(this.model, this.cache, this.blockLen, opts); } } s = new S(inner, cache, T); await s.build(); sessions.set(key, s); return s; };
|
|
414
|
+
// one verify session + rewind per block length the loop can use (adaptive: every depth 1..depth)
|
|
415
|
+
const verifyBy = new Map(), rewinds = [];
|
|
416
|
+
for (let L = this.adaptive ? 2 : LV; L <= LV; ++L) {
|
|
417
|
+
const verify = await session(L, { allRowsHead: true, ...(smallM ? { smallM } : {}), teeRecurrence: true, finalHidden: true });
|
|
418
|
+
const rewind = new I.RewindSession(inner, cache, L, verify); await rewind.build(); rewinds.push(rewind);
|
|
419
|
+
verifyBy.set(L, { verify, rewind });
|
|
420
|
+
}
|
|
421
|
+
const prefill = (T) => session(T, { finalHidden: true });
|
|
422
|
+
await prefill(16); await prefill(512);
|
|
423
|
+
if (this.adaptive) await prefill(1); // the last token of a plain stretch: next token + its hidden row
|
|
424
|
+
const slot = await cache.allocateCheckpointSlot();
|
|
425
|
+
// head KV bookkeeping: rows [kvLo, valid) of the head's KV cache are correct; hLastPos = the position whose
|
|
426
|
+
// (token, h) pair Hlast completes (Hlast = normed trunk h at hLastPos - 1)
|
|
427
|
+
r = { sessions, verifyBy, rewinds, verify: verifyBy.get(LV).verify, prefill, slot, kvLo: 0, valid: 0, hLastPos: -1 }; this.res.set(cache, r); return r;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Change the draft depth; per-cache sessions (sized depth + 1) are rebuilt on the next generate.
|
|
431
|
+
setDepth(depth, adaptive = this.adaptive) {
|
|
432
|
+
if (!(depth >= 1 && depth <= MCFG.MAXM - 1)) throw new Error(`MTPRunner: depth must be 1..${MCFG.MAXM - 1}`);
|
|
433
|
+
if (depth === this.depth && !!adaptive === this.adaptive) return;
|
|
434
|
+
for (const cache of [...this.res.keys()]) this.release(cache);
|
|
435
|
+
this.depth = depth; this.adaptive = !!adaptive;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Self-check of the trunk-hidden route: output_norm + the shared lm_head over the last verify run's row 0 must give
|
|
439
|
+
// the verify graph's own token for that row.
|
|
440
|
+
async checkTrunkHidden(cache) {
|
|
441
|
+
const r = this.res.get(cache); if (!r) return null;
|
|
442
|
+
const ht = r.verify.compiled.tensor('mtp.hidden');
|
|
443
|
+
const vt = Array.from(await this.rt.readTensor(r.verify.compiled.tensor('verify_tokens')));
|
|
444
|
+
const got = [];
|
|
445
|
+
for (let j = 0; j < vt.length; ++j) {
|
|
446
|
+
this.head.normTrunkRows(ht.buffer, (ht.byteOffset ?? 0) + j * this.H * 4, 1, 0);
|
|
447
|
+
const e = this.dev.createCommandEncoder(); e.copyBufferToBuffer(this.head.buf.Hn, 0, this.headIn.buffer, this.headIn.byteOffset ?? 0, this.H * 4); this.dev.queue.submit([e.finish()]);
|
|
448
|
+
this.headS.compiled.collector.enqueue(this.headS.steps);
|
|
449
|
+
got.push((await this.rt.readTensor(this.headArg))[0]);
|
|
450
|
+
}
|
|
451
|
+
return { verifyTokens: vt, fromHidden: got, match: got.every((t, j) => t === vt[j]) };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
release(cache) {
|
|
455
|
+
const r = this.res.get(cache); if (!r) return; this.res.delete(cache);
|
|
456
|
+
for (const s of r.sessions.values()) { try { s.dispose(); } catch (_) {} }
|
|
457
|
+
for (const rw of r.rewinds) { try { rw.dispose(); } catch (_) {} }
|
|
458
|
+
try { this.I.r2(r.slot.storage); } catch (_) {}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
dispose() {
|
|
462
|
+
if (this.disposed) return; this.disposed = true;
|
|
463
|
+
for (const cache of [...this.res.keys()]) this.release(cache);
|
|
464
|
+
try { this.embedS.dispose(); } catch (_) {}
|
|
465
|
+
try { this.headS.dispose(); } catch (_) {}
|
|
466
|
+
try { this.head.dispose(); } catch (_) {}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// run ids (1..T) through a session at past_len = cache.seqLength; advances the cache
|
|
470
|
+
async #runBlock(r, s, ids) {
|
|
471
|
+
const T = ids.length; const pos = r.cache.seqLength;
|
|
472
|
+
const next = await s.run(new Uint32Array(ids), pos); r.cache.seqLength = pos + T;
|
|
473
|
+
const ht = s.compiled.tensor('mtp.hidden');
|
|
474
|
+
return { next, pos, hidden: { buffer: ht.buffer, offset: ht.byteOffset ?? 0 }, tokens: () => this.rt.readTensor(s.compiled.tensor('verify_tokens')) };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
#embed(ids) {
|
|
478
|
+
const a = new Uint32Array(8); a.set(ids.slice(0, 8));
|
|
479
|
+
this.rt.host.writeBuffer(this.embedIds.buffer, this.embedIds.byteOffset ?? 0, a); this.embedS.compiled.collector.enqueue(this.embedS.steps);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Head pass over `toks` at positions pos0.. (Hn rows prepared by the caller); with `draft`, returns the argmax
|
|
483
|
+
// of the last row's head output.
|
|
484
|
+
async #pass(r, toks, pos0, draft) {
|
|
485
|
+
this.#embed(toks);
|
|
486
|
+
this.head.forward(this.embedBuf, this.embedOff, toks.length, pos0, r.kvLo);
|
|
487
|
+
r.valid = Math.max(r.valid, pos0 + toks.length);
|
|
488
|
+
if (!draft) return null;
|
|
489
|
+
const e = this.dev.createCommandEncoder(); e.copyBufferToBuffer(this.head.buf.Hout, (toks.length - 1) * this.H * 4, this.headIn.buffer, this.headIn.byteOffset ?? 0, this.H * 4); this.dev.queue.submit([e.finish()]);
|
|
490
|
+
this.headS.compiled.collector.enqueue(this.headS.steps);
|
|
491
|
+
return (await this.rt.readTensor(this.headArg))[0];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// End a pipelined plain stretch: when the decode graph ran more steps than the cache keeps (a step in flight past
|
|
495
|
+
// an EOS or an abandoned yield), restore the checkpoint taken at the stretch start and replay the kept tokens.
|
|
496
|
+
async #unplain(plain, cache) {
|
|
497
|
+
for (const pr of plain.inflight) { try { await pr; } catch (_) {} }
|
|
498
|
+
const keep = cache.seqLength - plain.P0;
|
|
499
|
+
if (plain.submitted > keep) {
|
|
500
|
+
const r = this.res.get(cache);
|
|
501
|
+
r.slot.checkpoint.restore();
|
|
502
|
+
const toks = plain.fed.slice(0, keep);
|
|
503
|
+
if (toks.length) await plain.sess.replayKnownInputs(toks, plain.P0);
|
|
504
|
+
}
|
|
505
|
+
plain.inflight = [];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---- the engine seam ----
|
|
509
|
+
async *generate(tokenIds, cache, generationArgs, beginDecode, eosTokenId) {
|
|
510
|
+
const { I, rt, H, head } = this; const LV = this.depth + 1, D = this.depth;
|
|
511
|
+
if (tokenIds.length === 0) throw new Error('generation requires at least one input token');
|
|
512
|
+
const { maxNewTokens, stopOnEos, onPrefillDone } = I.dc(generationArgs, {});
|
|
513
|
+
const isEos = (t) => stopOnEos && I.fc(t, eosTokenId);
|
|
514
|
+
const r = await this.#resources(cache); r.cache = cache;
|
|
515
|
+
this.stats.generations++;
|
|
516
|
+
// head KV continuity: rows below the cache's current length stay valid only if this cache produced them
|
|
517
|
+
const past = cache.get_seq_length();
|
|
518
|
+
if (past < r.valid) r.valid = past;
|
|
519
|
+
if (r.valid < past) { r.kvLo = past; r.valid = past; } // a gap (prefix imported or decoded without the head)
|
|
520
|
+
if (r.kvLo > past) r.kvLo = past;
|
|
521
|
+
const haveLast = r.hLastPos === past && past > 0;
|
|
522
|
+
// prompt prefill in the engine's block sizes through sessions that also output the final hidden rows; the
|
|
523
|
+
// last `window` rows feed the head's KV cache (pairs (x_p, h_{p-1})).
|
|
524
|
+
const ids = Array.from(tokenIds); const firstKept = Math.max(0, ids.length - this.window);
|
|
525
|
+
if (firstKept > 0) { r.kvLo = past + firstKept; r.valid = r.kvLo; }
|
|
526
|
+
let anchor = null, lastHidden = null, lastT = 0;
|
|
527
|
+
for (let i = 0; i < ids.length;) {
|
|
528
|
+
const remaining = ids.length - i; const T = MTP_PREFILL_BLOCKS.find((b) => b >= remaining) ?? MTP_PREFILL_BLOCKS[MTP_PREFILL_BLOCKS.length - 1];
|
|
529
|
+
const chunk = ids.slice(i, i + T); const run = await this.#runBlock(r, await r.prefill(T), chunk); anchor = run.next;
|
|
530
|
+
// head rows for this chunk: local rows a..b-1 (absolute positions run.pos + a ...), h from the previous row
|
|
531
|
+
for (let a = Math.max(0, firstKept - i); a < chunk.length; a += MCFG.MAXM) {
|
|
532
|
+
const b = Math.min(chunk.length, a + MCFG.MAXM), M = b - a;
|
|
533
|
+
// Hn[0] = h at position run.pos + a - 1: the previous row of this chunk, or the carried-over last row
|
|
534
|
+
if (a > 0) head.normTrunkRows(run.hidden.buffer, run.hidden.offset + (a - 1) * H * 4, M, 0);
|
|
535
|
+
else {
|
|
536
|
+
if (M > 1) head.normTrunkRows(run.hidden.buffer, run.hidden.offset, M - 1, 1);
|
|
537
|
+
if (i === 0 && !haveLast) head.setHnRow(0, head.buf.zero); else head.setHnRow(0, head.buf.Hlast);
|
|
538
|
+
}
|
|
539
|
+
await this.#pass(r, chunk.slice(a, b), run.pos + a, false);
|
|
540
|
+
}
|
|
541
|
+
// carry the chunk's last row: Hlast = normed h at the chunk's last position
|
|
542
|
+
head.normTrunkRows(run.hidden.buffer, run.hidden.offset + (chunk.length - 1) * H * 4, 1, 0);
|
|
543
|
+
head.saveLast(head.buf.Hn, 0);
|
|
544
|
+
lastHidden = run.hidden; lastT = chunk.length;
|
|
545
|
+
i += chunk.length;
|
|
546
|
+
}
|
|
547
|
+
r.hLastPos = cache.get_seq_length();
|
|
548
|
+
await rt.queueIdle();
|
|
549
|
+
onPrefillDone?.({ tokens: tokenIds.length, cache_length: cache.get_seq_length() });
|
|
550
|
+
if (maxNewTokens <= 0 || isEos(anchor)) return;
|
|
551
|
+
yield anchor; let emitted = 1;
|
|
552
|
+
// pending head rows: tokens at positions pendPos.. whose h rows sit in Hn[0..]; the last one is the anchor
|
|
553
|
+
let pendTok = [anchor], pendPos = cache.seqLength;
|
|
554
|
+
head.setHnRow(0, head.buf.Hlast);
|
|
555
|
+
let eosSeen = false;
|
|
556
|
+
let plain = null; // the plain stretch in progress (pipelined decode steps)
|
|
557
|
+
let depth = D; const pAcc = [0.8, 0.8, 0.8, 0.8]; // per-position acceptance estimates (adaptive)
|
|
558
|
+
const pick = () => { let best = 0, bestRate = 1; for (let d = 1; d <= Math.min(D, this.cost.length - 1); ++d) { let t = 1, q = 1; for (let i = 0; i < d; ++i) { q *= pAcc[i]; t += q; } const rate = t / this.cost[d]; if (rate > bestRate) { best = d; bestRate = rate; } } return best; };
|
|
559
|
+
try {
|
|
560
|
+
while (emitted < maxNewTokens && !eosSeen) {
|
|
561
|
+
const P = cache.seqLength;
|
|
562
|
+
if (depth === 0) {
|
|
563
|
+
// plain stretch through the engine's decode graph. It leaves no head KV rows, so write the pending rows
|
|
564
|
+
// below P now and restart the head's attention after the stretch.
|
|
565
|
+
if (pendTok.length > 1) await this.#pass(r, pendTok.slice(0, -1), pendPos, false);
|
|
566
|
+
pendTok = []; pendPos = P;
|
|
567
|
+
const n = Math.min(this.plainRun, maxNewTokens - emitted);
|
|
568
|
+
if (P + n + 1 > cache.maxLength || P + n + 2 > head.kvCap) break;
|
|
569
|
+
// n - 1 steps through the decode graph, two in flight (each chained step reads the previous step's output
|
|
570
|
+
// on the GPU). A step submitted past an EOS (or past an abandoned yield) is undone in #unplain.
|
|
571
|
+
const sess = await beginDecode(cache);
|
|
572
|
+
r.slot.checkpoint.capture();
|
|
573
|
+
plain = { sess, P0: P, fed: [anchor], inflight: [], submitted: 0 };
|
|
574
|
+
const total = n - 1;
|
|
575
|
+
const submit = () => { plain.inflight.push(sess.submitStep(plain.submitted === 0 ? anchor : null, P + plain.submitted)); plain.submitted++; };
|
|
576
|
+
let tok = anchor;
|
|
577
|
+
if (total > 0) submit();
|
|
578
|
+
while (plain.inflight.length) {
|
|
579
|
+
if (plain.submitted < total) submit();
|
|
580
|
+
const next = await plain.inflight.shift(); cache.seqLength += 1;
|
|
581
|
+
if (isEos(next)) { eosSeen = true; break; }
|
|
582
|
+
plain.fed.push(next);
|
|
583
|
+
yield next; emitted++; this.stats.tokens++; this.stats.plainTokens++; tok = next;
|
|
584
|
+
}
|
|
585
|
+
await this.#unplain(plain, cache); plain = null;
|
|
586
|
+
if (eosSeen) break;
|
|
587
|
+
// the stretch's last token goes through a 1-row session that also returns its hidden row (the h the
|
|
588
|
+
// head pairs with the next token)
|
|
589
|
+
const run = await this.#runBlock(r, await r.prefill(1), [tok]); const next = run.next;
|
|
590
|
+
head.normTrunkRows(run.hidden.buffer, run.hidden.offset, 1, 0);
|
|
591
|
+
r.kvLo = run.pos + 1; r.valid = r.kvLo;
|
|
592
|
+
pendTok = [next]; pendPos = run.pos + 1;
|
|
593
|
+
if (isEos(next)) { eosSeen = true; break; }
|
|
594
|
+
yield next; emitted++; this.stats.tokens++; this.stats.plainTokens++;
|
|
595
|
+
anchor = next; depth = 1; this.stats.depthChanges++;
|
|
596
|
+
for (let i = 0; i < pAcc.length; ++i) pAcc[i] = (pAcc[i] + 0.8) / 2; // half-forget: the text may have changed
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const LV = depth + 1; const { verify, rewind } = r.verifyBy.get(LV); r.verify = verify;
|
|
600
|
+
if (P + LV > cache.maxLength || P + LV + 1 > head.kvCap) break;
|
|
601
|
+
// draft: first pass also re-writes the accepted rows' KV with the trunk's own h; later passes chain h'
|
|
602
|
+
const drafts = [];
|
|
603
|
+
drafts.push(await this.#pass(r, pendTok, pendPos, true));
|
|
604
|
+
for (let d = 1; d < depth; ++d) { head.setHnRow(0, head.buf.Hout, d === 1 ? pendTok.length - 1 : 0); drafts.push(await this.#pass(r, [drafts[d - 1]], P + d, true)); }
|
|
605
|
+
this.stats.drafted += depth;
|
|
606
|
+
const block = [anchor, ...drafts];
|
|
607
|
+
r.slot.checkpoint.capture();
|
|
608
|
+
const run = await this.#runBlock(r, verify, block); const vt = Array.from(await run.tokens());
|
|
609
|
+
let k = 0; while (k < depth && vt[k] === drafts[k]) k++;
|
|
610
|
+
if (this.adaptive) {
|
|
611
|
+
// position i was tried only if positions < i were accepted
|
|
612
|
+
for (let i = 0; i < depth && i <= k; ++i) pAcc[i] += this.alpha * ((k > i ? 1 : 0) - pAcc[i]);
|
|
613
|
+
const next = pick(); if (next !== depth) { depth = next; this.stats.depthChanges++; }
|
|
614
|
+
}
|
|
615
|
+
const bonus = vt[k];
|
|
616
|
+
const out = [];
|
|
617
|
+
for (let i = 0; i < k; ++i) { if (isEos(drafts[i])) { eosSeen = true; k = i; break; } out.push(drafts[i]); }
|
|
618
|
+
if (!eosSeen) { if (isEos(bonus)) eosSeen = true; else out.push(bonus); }
|
|
619
|
+
const y = Math.min(out.length, maxNewTokens - emitted);
|
|
620
|
+
const keep = y < out.length ? y : k + 1;
|
|
621
|
+
// next cycle's pending rows: positions P+1..P+keep with the trunk's h at P..P+keep-1 (verify rows 0..keep-1)
|
|
622
|
+
head.normTrunkRows(run.hidden.buffer, run.hidden.offset, keep, 0);
|
|
623
|
+
pendTok = [...block.slice(1, keep), out[keep - 1] ?? bonus]; pendPos = P + 1;
|
|
624
|
+
r.valid = Math.min(r.valid, P + 1); // rows written from drafted h past P are speculative
|
|
625
|
+
if (keep < LV) { r.slot.checkpoint.restore(); rewind.run(keep); cache.seqLength = P + keep; }
|
|
626
|
+
this.stats.cycles++; this.stats.accepted += k;
|
|
627
|
+
for (let i = 0; i < y; ++i) { yield out[i]; emitted++; this.stats.tokens++; }
|
|
628
|
+
anchor = bonus;
|
|
629
|
+
}
|
|
630
|
+
} finally {
|
|
631
|
+
if (plain) await this.#unplain(plain, cache);
|
|
632
|
+
// leave the head's KV correct for every cached position, and Hlast for the next one
|
|
633
|
+
const S = cache.seqLength;
|
|
634
|
+
const n = Math.max(0, Math.min(pendTok.length, S + 1 - pendPos));
|
|
635
|
+
if (n > 1) await this.#pass(r, pendTok.slice(0, n - 1), pendPos, false);
|
|
636
|
+
if (n >= 1) { head.saveLast(head.buf.Hn, n - 1); r.hLastPos = pendPos + n - 1; } else r.hLastPos = -1;
|
|
637
|
+
r.valid = Math.min(Math.max(r.valid, pendPos + Math.max(0, n - 1)), S);
|
|
638
|
+
await rt.queueIdle();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export { MTPRunner, MTPHead, MTP_HEAD, MCFG };
|