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,836 @@
|
|
|
1
|
+
/* qwen35_dense.js — dense Qwen3.5-family models (GGUF arch `qwen35`, e.g. Qwen3.8-27B) from an imatrix
|
|
2
|
+
* IQ-mix GGUF, in a browser tab. First user: Underdog Saluki 27B 1.0 (ConwayResearch, Apache-2.0), one 7.9 GB
|
|
3
|
+
* GGUF mixing eleven llama.cpp block types.
|
|
4
|
+
* Also plain dense Qwen3 (GGUF arch `qwen3`, Qwen3Dense below; first user Qwen3-4B Q8_0, the FlashMap target): every
|
|
5
|
+
* layer full attention with an ungated Q, q/k RMSNorm and NeoX RoPE over the whole head, `ffn_norm` before the MLP,
|
|
6
|
+
* and the output head tied to the token table (llama.cpp src/models/qwen3.cpp).
|
|
7
|
+
*
|
|
8
|
+
* The trunk is the one qwen35_moe_ssd.js runs (llama.cpp's qwen35 graph): 3 of every 4 layers are Gated DeltaNet,
|
|
9
|
+
* every 4th is gated full attention with q/k RMSNorm and partial NeoX RoPE; this module reuses those kernels and
|
|
10
|
+
* replaces the routed experts with the dense SwiGLU MLP. Every weight stays in (nearly) its GGUF block layout on the
|
|
11
|
+
* GPU and is dequantized inside the matvec (iq_quants.js), so the products are llama.cpp's up to summation order.
|
|
12
|
+
*
|
|
13
|
+
* Storage: the GGUF is copied once into OPFS (localmind-ssd/<file>/dense.bin, every tensor byte for byte) by the
|
|
14
|
+
* shared ingest, which resumes an interrupted download and accepts the same file picked from disk; every later
|
|
15
|
+
* load reads it from there into GPU buffers (~7.9 GB of GPU memory for Saluki, plus the KV cache).
|
|
16
|
+
*
|
|
17
|
+
* const m = await Qwen35Dense.load(null, { maxCtx: 16384, onProgress });
|
|
18
|
+
* for await (const { text } of m.generate(messages, { maxNewTokens, enableThinking })) …
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Qwen3MoeSsd, STORAGE, COPY_SRC, COPY_DST, MAP_READ, UNIFORM } from './qwen3_moe_ssd.js';
|
|
22
|
+
import { QWEN35_KERNELS, QWEN35_BATCH_KERNELS } from './qwen35_moe_ssd.js';
|
|
23
|
+
import { FlashMapDrafter, FLASHMAP_QWEN3_4B_LAYERS, specGenerate } from './flashmap_drafter.js';
|
|
24
|
+
import { QTYPE_BY_ID, rowBytes, gpuRowBytes, PADDED, MV_ROWS, MM_TILE, MM_SG_TOKENS, MVT_TOKENS, MVT_ROWS, gridData, repackKernel, mvKernel, mmKernel, mmSgKernel, mvTKernel, embedKernel, embedBKernel } from './iq_quants.js';
|
|
25
|
+
|
|
26
|
+
export const UNDERDOG_SALUKI_27B = {
|
|
27
|
+
repo: 'ConwayResearch/Underdog-Saluki-27B-1.0',
|
|
28
|
+
file: 'Underdog-Saluki-27B-1.0-IQ2-mix.gguf',
|
|
29
|
+
revision: '1336c0b5d74dfe6ad7f793577092f5effd9f97c6',
|
|
30
|
+
sha256: '4a673518b11b1c4445f9b9a9d3c40356f5ba39f6a6260dcb25f3fbfd475d9efb',
|
|
31
|
+
size: 7898369152,
|
|
32
|
+
};
|
|
33
|
+
const F32 = 0;
|
|
34
|
+
const NO_SSM = { dConv: 0, dInner: 0, dState: 0, vHeads: 0, kHeads: 0 }; // qwen3: no DeltaNet layers
|
|
35
|
+
|
|
36
|
+
export function configFromGguf(kv) {
|
|
37
|
+
const a = kv['general.architecture'];
|
|
38
|
+
if (a !== 'qwen35' && a !== 'qwen3') throw new Error(`Qwen35Dense needs a qwen35 or qwen3 GGUF, got ${a}`);
|
|
39
|
+
const g = (k) => kv[`${a}.${k}`], plain = a === 'qwen3';
|
|
40
|
+
return {
|
|
41
|
+
arch: a,
|
|
42
|
+
layers: g('block_count') - (g('nextn_predict_layers') || 0), hidden: g('embedding_length'), ff: g('feed_forward_length'),
|
|
43
|
+
heads: g('attention.head_count'), kvHeads: g('attention.head_count_kv'), headDim: g('attention.key_length'),
|
|
44
|
+
ropeDims: g('rope.dimension_count') || g('attention.key_length'), ropeTheta: g('rope.freq_base'), eps: g('attention.layer_norm_rms_epsilon'),
|
|
45
|
+
attnInterval: plain ? 1 : g('full_attention_interval') || 4, contextLength: g('context_length'),
|
|
46
|
+
// qwen35 attention has a sigmoid gate in the Q projection; qwen3's does not. The MLP's norm is named differently.
|
|
47
|
+
gatedQ: !plain, ffnNorm: plain ? 'ffn_norm.weight' : 'post_attention_norm.weight',
|
|
48
|
+
ssm: plain ? null : { dConv: g('ssm.conv_kernel'), dInner: g('ssm.inner_size'), dState: g('ssm.state_size'), vHeads: g('ssm.time_step_rank'), kHeads: g('ssm.group_count') },
|
|
49
|
+
vocab: kv['tokenizer.ggml.tokens'].length,
|
|
50
|
+
bos: kv['tokenizer.ggml.bos_token_id'], eos: kv['tokenizer.ggml.eos_token_id'],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// OPFS layout: every tensor copied as-is into dense.bin (256-byte aligned); no experts file content.
|
|
55
|
+
function planLayout(gguf) {
|
|
56
|
+
const config = configFromGguf(gguf.kv);
|
|
57
|
+
const dense = {};
|
|
58
|
+
let off = 0;
|
|
59
|
+
for (const t of gguf.tensors) {
|
|
60
|
+
if (/^blk\.\d+\.nextn\./.test(t.name)) continue;
|
|
61
|
+
const n = t.dims.reduce((a, b) => a * b, 1);
|
|
62
|
+
let bytes;
|
|
63
|
+
if (t.type === F32) bytes = n * 4;
|
|
64
|
+
else if (QTYPE_BY_ID[t.type]) bytes = rowBytes(QTYPE_BY_ID[t.type], t.dims[0]) * (n / t.dims[0]);
|
|
65
|
+
else throw new Error(`${t.name}: GGML type ${t.type} is not supported by Qwen35Dense`);
|
|
66
|
+
dense[t.name] = { type: 'raw', ggml: t.type, dims: t.dims, raw: { off, bytes } };
|
|
67
|
+
off += Math.ceil(bytes / 256) * 256;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
config,
|
|
71
|
+
experts: { file: 'experts.bin', record: 0, parts: {}, layers: config.layers, perLayer: 0, bytes: 0 },
|
|
72
|
+
dense: { file: 'dense.bin', bytes: off, tensors: dense },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Raw copies in pieces of at most 8 MiB.
|
|
76
|
+
function planUnits(gguf, layout) {
|
|
77
|
+
const units = [], PIECE = 8 << 20;
|
|
78
|
+
for (const t of gguf.tensors) {
|
|
79
|
+
const d = layout.dense.tensors[t.name];
|
|
80
|
+
if (!d) continue;
|
|
81
|
+
const src = gguf.dataStart + t.offset;
|
|
82
|
+
for (let o = 0; o < d.raw.bytes; o += PIECE) units.push({ src: src + o, len: Math.min(PIECE, d.raw.bytes - o), file: 'dense', raw: d.raw.off + o });
|
|
83
|
+
}
|
|
84
|
+
units.sort((a, b) => a.src - b.src);
|
|
85
|
+
return units;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The dense MLP's activation: act = SiLU(gate)·up, gate and up in one buffer [gate F | up F].
|
|
89
|
+
const DENSE_KERNELS = {
|
|
90
|
+
siluMul: `
|
|
91
|
+
struct P { F: u32 }
|
|
92
|
+
@group(0) @binding(0) var<storage, read> gu: array<f32>;
|
|
93
|
+
@group(0) @binding(1) var<storage, read_write> act: array<f32>;
|
|
94
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
95
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gi: vec3<u32>) {
|
|
96
|
+
let i = gi.x; if (i >= p.F) { return; }
|
|
97
|
+
let g = gu[i]; act[i] = (g / (1.0 + exp(-g))) * gu[p.F + i];
|
|
98
|
+
}`,
|
|
99
|
+
// Speculative decoding support (opts.captureLayers): a layer's T output rows into columns [off, off + H) of the
|
|
100
|
+
// feature rows (width W) a drafter reads. Dispatch (ceil(H / 256), T).
|
|
101
|
+
copyCols: `
|
|
102
|
+
struct P { H: u32, W: u32, off: u32, pad: u32 }
|
|
103
|
+
struct Q { T: u32, pos0: u32, S: u32, pad: u32 }
|
|
104
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
105
|
+
@group(0) @binding(1) var<storage, read_write> f: array<f32>;
|
|
106
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
107
|
+
@group(0) @binding(3) var<uniform> qd: Q;
|
|
108
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
109
|
+
let i = g.x; let t = g.y; if (i >= p.H || t >= qd.T) { return; }
|
|
110
|
+
f[t * p.W + p.off + i] = x[t * p.H + i];
|
|
111
|
+
}`,
|
|
112
|
+
// The one-token step's version: row r.row of the feature rows (r is a uniform the step writes). Dispatch ceil(H / 256).
|
|
113
|
+
copyRow: `
|
|
114
|
+
struct P { H: u32, W: u32, off: u32, pad: u32 }
|
|
115
|
+
struct R { row: u32, a: u32, b: u32, c: u32 }
|
|
116
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
117
|
+
@group(0) @binding(1) var<storage, read_write> f: array<f32>;
|
|
118
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
119
|
+
@group(0) @binding(3) var<uniform> r: R;
|
|
120
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
121
|
+
let i = g.x; if (i >= p.H) { return; }
|
|
122
|
+
f[r.row * p.W + p.off + i] = x[i];
|
|
123
|
+
}`,
|
|
124
|
+
// The argmax of each of T rows of n logits (lowest index on ties). One workgroup per row.
|
|
125
|
+
argmaxRows: `
|
|
126
|
+
struct P { n: u32 }
|
|
127
|
+
@group(0) @binding(0) var<storage, read> l: array<f32>;
|
|
128
|
+
@group(0) @binding(1) var<storage, read_write> out: array<u32>;
|
|
129
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
130
|
+
var<workgroup> bv: array<f32, 256>;
|
|
131
|
+
var<workgroup> bi: array<u32, 256>;
|
|
132
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
133
|
+
let r = wg.x; let t = lid.x; let base = r * p.n;
|
|
134
|
+
var v = -3.4e38; var ix = 0u;
|
|
135
|
+
for (var i = t; i < p.n; i += 256u) { let x = l[base + i]; if (x > v) { v = x; ix = i; } }
|
|
136
|
+
bv[t] = v; bi[t] = ix; workgroupBarrier();
|
|
137
|
+
for (var s = 128u; s > 0u; s >>= 1u) {
|
|
138
|
+
if (t < s) { let o = bv[t + s]; if (o > bv[t] || (o == bv[t] && bi[t + s] < bi[t])) { bv[t] = o; bi[t] = bi[t + s]; } }
|
|
139
|
+
workgroupBarrier();
|
|
140
|
+
}
|
|
141
|
+
if (t == 0u) { out[r] = bi[0]; }
|
|
142
|
+
}`,
|
|
143
|
+
};
|
|
144
|
+
// Rows of logits a verify chunk can return at once (want = 'rows').
|
|
145
|
+
const ROWS_MAX = 32;
|
|
146
|
+
|
|
147
|
+
// Prefill kernels that replace qwen35_moe_ssd.js's batched DeltaNet step and attention (same bindings, same math):
|
|
148
|
+
// gdnStepR — the gated delta rule over T tokens with each head's 128×128 state in registers (256 threads, 64
|
|
149
|
+
// values each; thread pairs split a row) instead of read and written in memory every token.
|
|
150
|
+
// flashAttnB — causal GQA attention with an online softmax: a workgroup takes one KV head and 4 query tokens (that
|
|
151
|
+
// head's heads/kvHeads query heads each), walks the keys in tiles of 8 through shared memory, and never
|
|
152
|
+
// materializes the heads × T × S score matrix (so the chunk size no longer depends on the context).
|
|
153
|
+
const PREFILL_KERNELS = {
|
|
154
|
+
gdnStepR: `
|
|
155
|
+
struct P { kH: u32, dS: u32, keyDim: u32, convDim: u32, vH: u32, pad: u32, eps: f32, scale: f32 }
|
|
156
|
+
struct Q { T: u32, pos0: u32, S: u32, pad: u32 }
|
|
157
|
+
@group(0) @binding(0) var<storage, read> cv: array<f32>;
|
|
158
|
+
@group(0) @binding(1) var<storage, read> bb: array<f32>;
|
|
159
|
+
@group(0) @binding(2) var<storage, read> al: array<f32>;
|
|
160
|
+
@group(0) @binding(3) var<storage, read> dt: array<f32>;
|
|
161
|
+
@group(0) @binding(4) var<storage, read> aa: array<f32>;
|
|
162
|
+
@group(0) @binding(5) var<storage, read_write> M: array<f32>;
|
|
163
|
+
@group(0) @binding(6) var<storage, read_write> o: array<f32>;
|
|
164
|
+
@group(0) @binding(7) var<uniform> p: P;
|
|
165
|
+
@group(0) @binding(8) var<uniform> qd: Q;
|
|
166
|
+
var<workgroup> kq: array<f32, 128>;
|
|
167
|
+
var<workgroup> qq: array<f32, 128>;
|
|
168
|
+
var<workgroup> rq: array<f32, 128>;
|
|
169
|
+
var<workgroup> rk: array<f32, 128>;
|
|
170
|
+
var<workgroup> part: array<f32, 256>;
|
|
171
|
+
var<workgroup> part2: array<f32, 256>;
|
|
172
|
+
fn sigm(x: f32) -> f32 { return 1.0 / (1.0 + exp(-x)); }
|
|
173
|
+
fn log1p_(u: f32) -> f32 { let y = 1.0 + u; return log(y) - ((y - 1.0) - u) / y; }
|
|
174
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
175
|
+
let h = wg.x; let t = l.x; let j = t >> 1u; let i0 = (t & 1u) * 64u; let kh = h % p.kH;
|
|
176
|
+
let n = 128.0; let inv = 1.0 / sqrt(n);
|
|
177
|
+
let row = (h * 128u + j) * 128u + i0;
|
|
178
|
+
var st: array<f32, 64>;
|
|
179
|
+
for (var i = 0u; i < 64u; i++) { st[i] = M[row + i]; }
|
|
180
|
+
let dth = dt[h]; let aah = aa[h];
|
|
181
|
+
for (var tk = 0u; tk < qd.T; tk++) {
|
|
182
|
+
let cb = tk * p.convDim;
|
|
183
|
+
var qv = 0.0; var kv = 0.0;
|
|
184
|
+
if (t < 128u) { qv = cv[cb + kh * 128u + t]; kv = cv[cb + p.keyDim + kh * 128u + t]; rq[t] = qv * qv; rk[t] = kv * kv; }
|
|
185
|
+
workgroupBarrier();
|
|
186
|
+
for (var s = 64u; s > 0u; s >>= 1u) { if (t < s) { rq[t] += rq[t + s]; rk[t] += rk[t + s]; } workgroupBarrier(); }
|
|
187
|
+
if (t < 128u) {
|
|
188
|
+
qq[t] = (qv * (1.0 / sqrt(rq[0] / n + p.eps / n))) * inv;
|
|
189
|
+
kq[t] = (kv * (1.0 / sqrt(rk[0] / n + p.eps / n))) * inv;
|
|
190
|
+
}
|
|
191
|
+
workgroupBarrier();
|
|
192
|
+
let vv = cv[cb + 2u * p.keyDim + h * 128u + j];
|
|
193
|
+
let beta = sigm(bb[tk * p.vH + h]);
|
|
194
|
+
let x = al[tk * p.vH + h] + dth;
|
|
195
|
+
let sp = select(log1p_(exp(x)), x, x > 20.0);
|
|
196
|
+
let decay = exp(sp * aah);
|
|
197
|
+
var sk = 0.0;
|
|
198
|
+
for (var i = 0u; i < 64u; i++) { let m = st[i] * decay; st[i] = m; sk += m * kq[i0 + i]; }
|
|
199
|
+
part[t] = sk; workgroupBarrier();
|
|
200
|
+
let d = (vv - (part[t & ~1u] + part[t | 1u])) * beta;
|
|
201
|
+
var acc = 0.0;
|
|
202
|
+
for (var i = 0u; i < 64u; i++) { let m = st[i] + kq[i0 + i] * d; st[i] = m; acc += m * qq[i0 + i]; }
|
|
203
|
+
part2[t] = acc; workgroupBarrier();
|
|
204
|
+
if ((t & 1u) == 0u) { o[tk * p.vH * 128u + h * 128u + j] = (part2[t] + part2[t + 1u]) * p.scale; }
|
|
205
|
+
}
|
|
206
|
+
for (var i = 0u; i < 64u; i++) { M[row + i] = st[i]; }
|
|
207
|
+
}`,
|
|
208
|
+
flashAttnB: `enable f16;
|
|
209
|
+
struct P { heads: u32, kvHeads: u32, hd: u32, scale: f32 }
|
|
210
|
+
struct Q { T: u32, pos0: u32, S: u32, pad: u32 }
|
|
211
|
+
@group(0) @binding(0) var<storage, read> q: array<f32>;
|
|
212
|
+
@group(0) @binding(1) var<storage, read> kc: array<f16>;
|
|
213
|
+
@group(0) @binding(2) var<storage, read> vc: array<f16>;
|
|
214
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
215
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
216
|
+
@group(0) @binding(5) var<uniform> qd: Q;
|
|
217
|
+
const HD = 256u; const TQ = 4u; const KT = 8u; const G = 6u; const R = 24u; // R = G × TQ query rows
|
|
218
|
+
var<workgroup> qs: array<f32, 6144>; // [row][d], row = tq · G + hh
|
|
219
|
+
var<workgroup> kvs: array<f16, 2048>; // a K or V tile, [key][d]
|
|
220
|
+
var<workgroup> sc: array<f32, 192>; // [row][key] scores, then probabilities
|
|
221
|
+
var<workgroup> mrow: array<f32, 24>;
|
|
222
|
+
var<workgroup> lrow: array<f32, 24>;
|
|
223
|
+
var<workgroup> alpha: array<f32, 24>;
|
|
224
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
225
|
+
let g = wg.x; let tq0 = wg.y * TQ; let t = l.x; let QN = p.heads * HD; let kvn = p.kvHeads * HD;
|
|
226
|
+
for (var e = t; e < R * HD; e += 256u) {
|
|
227
|
+
let r = e / HD; let tok = tq0 + r / G;
|
|
228
|
+
qs[e] = select(0.0, q[min(tok, qd.T - 1u) * QN + (g * G + r % G) * HD + e % HD] * p.scale, tok < qd.T);
|
|
229
|
+
}
|
|
230
|
+
if (t < R) { mrow[t] = -1e30; lrow[t] = 0.0; }
|
|
231
|
+
var o: array<f32, 24>;
|
|
232
|
+
let last = qd.pos0 + min(tq0 + TQ - 1u, qd.T - 1u);
|
|
233
|
+
for (var k0 = 0u; k0 <= last; k0 += KT) {
|
|
234
|
+
workgroupBarrier();
|
|
235
|
+
for (var e = t; e < KT * HD; e += 256u) { kvs[e] = kc[min(k0 + e / HD, last) * kvn + g * HD + e % HD]; }
|
|
236
|
+
workgroupBarrier();
|
|
237
|
+
if (t < R * KT) {
|
|
238
|
+
let r = t / KT; let k = t % KT; let tok = tq0 + r / G;
|
|
239
|
+
var s = 0.0;
|
|
240
|
+
for (var d = 0u; d < HD; d++) { s += qs[r * HD + d] * f32(kvs[k * HD + d]); }
|
|
241
|
+
sc[t] = select(-1e30, s, k0 + k <= qd.pos0 + tok && tok < qd.T);
|
|
242
|
+
}
|
|
243
|
+
workgroupBarrier();
|
|
244
|
+
if (t < R) {
|
|
245
|
+
var mx = mrow[t];
|
|
246
|
+
for (var k = 0u; k < KT; k++) { mx = max(mx, sc[t * KT + k]); }
|
|
247
|
+
let a = exp(mrow[t] - mx); var sum = 0.0;
|
|
248
|
+
for (var k = 0u; k < KT; k++) { let e = exp(sc[t * KT + k] - mx); sc[t * KT + k] = e; sum += e; }
|
|
249
|
+
lrow[t] = lrow[t] * a + sum; mrow[t] = mx; alpha[t] = a;
|
|
250
|
+
}
|
|
251
|
+
for (var e = t; e < KT * HD; e += 256u) { kvs[e] = vc[min(k0 + e / HD, last) * kvn + g * HD + e % HD]; }
|
|
252
|
+
workgroupBarrier();
|
|
253
|
+
for (var r = 0u; r < R; r++) {
|
|
254
|
+
var acc = o[r] * alpha[r];
|
|
255
|
+
for (var k = 0u; k < KT; k++) { acc += sc[r * KT + k] * f32(kvs[k * HD + t]); }
|
|
256
|
+
o[r] = acc;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
workgroupBarrier();
|
|
260
|
+
for (var r = 0u; r < R; r++) {
|
|
261
|
+
let tok = tq0 + r / G;
|
|
262
|
+
if (tok < qd.T) { y[tok * QN + (g * G + r % G) * HD + t] = o[r] / lrow[r]; }
|
|
263
|
+
}
|
|
264
|
+
}`,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export class Qwen35Dense extends Qwen3MoeSsd {
|
|
268
|
+
static configFrom(kv) { return configFromGguf(kv); }
|
|
269
|
+
static get ingestPlan() { return { layout: planLayout, units: planUnits }; }
|
|
270
|
+
|
|
271
|
+
// Without a url, the pinned Hugging Face file; with one (a local serve), the OPFS store is named after the file.
|
|
272
|
+
static async load(modelId = null, opts = {}) {
|
|
273
|
+
const source = opts.source || (opts.url ? { repo: null, file: opts.url.split('/').pop(), revision: null } : UNDERDOG_SALUKI_27B);
|
|
274
|
+
return super.load(modelId, { ...opts, source });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
constructor(device, manifest, gguf, opts) {
|
|
278
|
+
super(device, manifest, gguf, opts);
|
|
279
|
+
const c = this.cfg;
|
|
280
|
+
if (c.ssm && c.ssm.dConv !== 4) throw new Error(`gdnConv assumes a 4-tap conv, got ${c.ssm.dConv}`);
|
|
281
|
+
if (c.ssm && (c.ssm.dState !== 128 || c.ssm.dInner !== c.ssm.vHeads * c.ssm.dState)) throw new Error('gdnStep assumes 128-wide heads');
|
|
282
|
+
this.recurrent = Array.from({ length: c.layers }, (_, l) => (l + 1) % c.attnInterval !== 0);
|
|
283
|
+
this.types = {};
|
|
284
|
+
for (const [name, d] of Object.entries(manifest.dense.tensors)) this.types[name] = d.ggml === F32 ? null : QTYPE_BY_ID[d.ggml];
|
|
285
|
+
if (!this.types['output.weight']) this.types['output.weight'] = this.types['token_embd.weight']; // tied head (qwen3)
|
|
286
|
+
this.prefetch = false;
|
|
287
|
+
// Prompt chunks of up to 256 tokens. Without flash attention the batched scores take heads × chunk × maxCtx
|
|
288
|
+
// floats; then the chunk shrinks to keep them near 96 MB.
|
|
289
|
+
this.batchPrefill = opts.batchPrefill ?? true;
|
|
290
|
+
const fit = this.flashAttention ? Infinity : Math.floor((96 << 20) / (c.heads * this.maxCtx * 4));
|
|
291
|
+
this.chunkTokens = Math.max(16, Math.min(Math.floor(opts.prefillChunk || 256), fit === Infinity ? 256 : fit - (fit % 16) || 16));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// The kernels this model runs: the base engine's norm/attention/argmax, the qwen35 trunk (one token and
|
|
295
|
+
// batched), and a matvec, a tiled GEMM and an embedding gather per block type present in the GGUF.
|
|
296
|
+
get kernels() {
|
|
297
|
+
if (!this._kernels) {
|
|
298
|
+
const base = super.kernels, pick = (o, names) => Object.fromEntries(names.map((n) => [n, o[n]]));
|
|
299
|
+
const k = {
|
|
300
|
+
...pick(base, ['rmsnorm', 'kvStore', 'attnScore', 'softmax', 'attnOut', 'addInPlace', 'argmax']),
|
|
301
|
+
...pick(QWEN35_KERNELS, ['qkNormRopeP', 'sigmoidMul', 'gdnConv', 'gdnStep', 'gdnNormGate']),
|
|
302
|
+
...pick(QWEN35_BATCH_KERNELS, ['rmsnormB', 'gdnConvB', 'gdnNormGateB', 'qkNormRopePB', 'kvStoreB', 'attnScoreB',
|
|
303
|
+
'softmaxB', 'attnOutB', 'sigmoidMulB', 'addInPlaceB', 'siluMul2B']),
|
|
304
|
+
...DENSE_KERNELS,
|
|
305
|
+
gdnStepR: PREFILL_KERNELS.gdnStepR,
|
|
306
|
+
...(this.flashAttention ? { flashAttnB: PREFILL_KERNELS.flashAttnB } : {}),
|
|
307
|
+
};
|
|
308
|
+
const embedType = this.types['token_embd.weight'];
|
|
309
|
+
for (const t of new Set(Object.values(this.types).filter(Boolean))) {
|
|
310
|
+
k[`mv_${t}`] = mvKernel(t); k[`mm_${t}`] = this.matrixKernels ? mmSgKernel(t) : mmKernel(t);
|
|
311
|
+
if (this.opts.captureLayers) { k[`mvT_${t}`] = mvTKernel(t); k[`mvT8_${t}`] = mvTKernel(t, 8); } // verify blocks
|
|
312
|
+
if (PADDED.has(t)) k[`repack_${t}`] = repackKernel(t);
|
|
313
|
+
}
|
|
314
|
+
k[`embed_${embedType}`] = embedKernel(embedType);
|
|
315
|
+
k[`embedB_${embedType}`] = embedBKernel(embedType);
|
|
316
|
+
this._kernels = k;
|
|
317
|
+
}
|
|
318
|
+
return this._kernels;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Prefill GEMMs on subgroup matrices (f16 in, f32 accumulate) when the device has them with 32-wide subgroups and
|
|
322
|
+
// an f16 8×8×8 configuration; otherwise the f32 tiled GEMM. opts.subgroupMatrix === false forces the f32 path.
|
|
323
|
+
// Apple reports only f16→f16 and f32→f32 configurations, yet Dawn compiles f16 operands into an f32 result and
|
|
324
|
+
// computes it right (scripts/test-iq-kernels.mjs; the vendored Bonsai 2 engine relies on the same pairing).
|
|
325
|
+
get matrixKernels() {
|
|
326
|
+
const i = this.device.adapterInfo || this.opts.adapterInfo || {};
|
|
327
|
+
const cfgs = i.subgroupMatrixConfigs || [];
|
|
328
|
+
return this.opts.subgroupMatrix !== false && this.device.features.has('chromium-experimental-subgroup-matrix')
|
|
329
|
+
&& i.subgroupMinSize === 32 && i.subgroupMaxSize === 32
|
|
330
|
+
&& cfgs.some((c) => c.componentType === 'f16' && c.M === 8 && c.N === 8 && c.K === 8);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// flashAttnB is written for 6 query heads per KV head of 256 dims and needs 29.7 KB of workgroup memory.
|
|
334
|
+
get flashAttention() {
|
|
335
|
+
const c = this.cfg;
|
|
336
|
+
return this.opts.flashAttention !== false && c.heads === 6 * c.kvHeads && c.headDim === 256
|
|
337
|
+
&& this.device.limits.maxComputeWorkgroupStorageSize >= 29728;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async init(onProgress) {
|
|
341
|
+
const dev = this.device;
|
|
342
|
+
dev.pushErrorScope('validation');
|
|
343
|
+
for (const k of Object.keys(this.kernels)) this.pipeline(k);
|
|
344
|
+
const err = await dev.popErrorScope();
|
|
345
|
+
if (err) throw new Error(`WGSL: ${err.message}`);
|
|
346
|
+
await this.uploadDense(onProgress);
|
|
347
|
+
await this.repackPadded();
|
|
348
|
+
const g = gridData();
|
|
349
|
+
this.grid = this.buffer(g.byteLength, STORAGE | COPY_DST, 'dense');
|
|
350
|
+
dev.queue.writeBuffer(this.grid, 0, g);
|
|
351
|
+
this.initBuffers();
|
|
352
|
+
await dev.queue.onSubmittedWorkDone();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Blocks of 4k+2 bytes arrive in GGUF layout; give each two bytes of padding after its scale (iq_quants.js PADDED)
|
|
356
|
+
// on the GPU, then free the GGUF-layout buffer. A few tensors at a time, so the extra memory stays small.
|
|
357
|
+
async repackPadded() {
|
|
358
|
+
const dev = this.device;
|
|
359
|
+
let pending = [];
|
|
360
|
+
const flush = async () => { await dev.queue.onSubmittedWorkDone(); for (const b of pending) { this.gpuBytes.dense -= b.size; b.destroy(); } pending = []; };
|
|
361
|
+
for (const [name, t] of Object.entries(this.types)) {
|
|
362
|
+
if (!t || !PADDED.has(t) || !this.manifest.dense.tensors[name]) continue; // the tied head's alias has no tensor
|
|
363
|
+
const dims = this.manifest.dense.tensors[name].dims, rows = dims.reduce((a, b) => a * b, 1) / dims[0];
|
|
364
|
+
const words = gpuRowBytes(t, dims[0]) * rows / 4, src = this.w[name].raw;
|
|
365
|
+
const dst = this.buffer(words * 4, STORAGE | COPY_DST, 'dense');
|
|
366
|
+
const enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
|
|
367
|
+
this.dispatch(pass, `repack_${t}`, this.bind(`repack_${t}`, [src, dst, this.uniform([words])]), Math.ceil(words / 256));
|
|
368
|
+
pass.end(); dev.queue.submit([enc.finish()]);
|
|
369
|
+
this.w[name].raw = dst;
|
|
370
|
+
pending.push(src);
|
|
371
|
+
if (pending.length >= 8) await flush();
|
|
372
|
+
}
|
|
373
|
+
await flush();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Like bind(), but a resource may be { buffer, offset, size }.
|
|
377
|
+
bindR(name, resources) {
|
|
378
|
+
return this.device.createBindGroup({
|
|
379
|
+
layout: this.pipeline(name).getBindGroupLayout(0),
|
|
380
|
+
entries: resources.map((r, i) => ({ binding: i, resource: r instanceof GPUBuffer ? { buffer: r } : r })),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
kvBytes() {
|
|
385
|
+
const c = this.cfg, s = c.ssm || NO_SSM, convDim = 2 * s.kHeads * s.dState + s.dInner;
|
|
386
|
+
return this.recurrent.reduce((sum, r) => sum + (r ? 3 * convDim * 4 + s.vHeads * s.dState * s.dState * 4 : 2 * 2 * this.maxCtx * c.kvHeads * c.headDim), 0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// A quantized weight's matvec: { name, group, wg } for dispatch. y may be { buffer, offset, size }.
|
|
390
|
+
mv(name, x, y, M, N) {
|
|
391
|
+
const t = this.types[name], w = this.w[name];
|
|
392
|
+
if (!t || !w) throw new Error(`missing quantized tensor ${name}`);
|
|
393
|
+
const u = this.uniform([M, N, gpuRowBytes(t, N), 0]);
|
|
394
|
+
return { k: `mv_${t}`, g: this.bindR(`mv_${t}`, [x, w.raw, this.grid, y, u]), wg: Math.ceil(M / MV_ROWS) };
|
|
395
|
+
}
|
|
396
|
+
mm(name, x, y, M, N) {
|
|
397
|
+
const t = this.types[name], w = this.w[name];
|
|
398
|
+
const u = this.uniform([M, N, gpuRowBytes(t, N), 0]);
|
|
399
|
+
const op = { k: `mm_${t}`, g: this.bindR(`mm_${t}`, [x, w.raw, this.grid, y, u, this.qb]), wg: Math.ceil(M / MM_TILE) };
|
|
400
|
+
// Speculative verify blocks (T ≤ MVT_TOKENS) read each weight once instead: the same bindings, the matvec kernel.
|
|
401
|
+
if (this.kernels[`mvT_${t}`] && N % 256 === 0) {
|
|
402
|
+
const v = (k) => ({ k, g: this.bindR(k, [x, w.raw, this.grid, y, u, this.qb]), wg: Math.ceil(M / MVT_ROWS) });
|
|
403
|
+
op.small = v(`mvT_${t}`); op.small8 = v(`mvT8_${t}`);
|
|
404
|
+
}
|
|
405
|
+
return op;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
initBuffers() {
|
|
409
|
+
const c = this.cfg, s = c.ssm || NO_SSM;
|
|
410
|
+
if (!this.w['output.weight']) this.w['output.weight'] = this.w['token_embd.weight']; // tied head (qwen3)
|
|
411
|
+
const H = c.hidden, hd = c.headDim, QN = c.heads * hd, kvn = c.kvHeads * hd, F = c.ff;
|
|
412
|
+
const keyDim = s.kHeads * s.dState, convDim = 2 * keyDim + s.dInner;
|
|
413
|
+
if ((F * 4) % 256) throw new Error('gate/up halves must be 256-byte aligned');
|
|
414
|
+
this.kc = []; this.vc = []; this.conv = []; this.ssm = [];
|
|
415
|
+
for (let l = 0; l < c.layers; l++) {
|
|
416
|
+
if (this.recurrent[l]) {
|
|
417
|
+
this.conv[l] = this.buffer(3 * convDim * 4, STORAGE | COPY_DST, 'state');
|
|
418
|
+
this.ssm[l] = this.buffer(s.vHeads * s.dState * s.dState * 4, STORAGE | COPY_DST, 'state');
|
|
419
|
+
} else {
|
|
420
|
+
this.kc[l] = this.buffer(this.maxCtx * kvn * 2, STORAGE, 'kv');
|
|
421
|
+
this.vc[l] = this.buffer(this.maxCtx * kvn * 2, STORAGE, 'kv');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST);
|
|
425
|
+
const a = this.a = {
|
|
426
|
+
x: A(H), xn: A(H), qg: A(2 * QN), q: A(QN), gate: A(QN), k: A(kvn), kr: A(kvn), v: A(kvn), att: A(QN), o: A(H),
|
|
427
|
+
sc: A(c.heads * this.maxCtx), mixed: A(convDim), cv: A(convDim), z: A(s.dInner), bb: A(s.vHeads), al: A(s.vHeads),
|
|
428
|
+
go: A(s.dInner), gn: A(s.dInner), dummy: A(4), gu: A(2 * F), act: A(F), logits: A(c.vocab), am: A(4), rope: A(c.ropeDims),
|
|
429
|
+
};
|
|
430
|
+
this.tok = this.buffer(16, UNIFORM | COPY_DST);
|
|
431
|
+
this.rbLogits = this.buffer(c.vocab * 4, MAP_READ | COPY_DST);
|
|
432
|
+
this.rbArg = this.buffer(16, MAP_READ | COPY_DST);
|
|
433
|
+
this.qb = this.buffer(16, UNIFORM | COPY_DST, 'batch');
|
|
434
|
+
|
|
435
|
+
const U = (w) => this.uniform(w), eps = { f: c.eps };
|
|
436
|
+
const u = {
|
|
437
|
+
nH: U([H]), rmsH: U([H, eps]),
|
|
438
|
+
ropeQ: U([hd, c.gatedQ ? 2 * hd : hd, c.ropeDims, c.gatedQ ? 1 : 0, eps]), ropeK: U([hd, hd, c.ropeDims, 0, eps]), kvn: U([kvn]),
|
|
439
|
+
att: U([c.heads, c.kvHeads, hd, { f: 1 / Math.sqrt(hd) }]), attOut: U([c.heads, c.kvHeads, hd, 0]), gate: U([QN]),
|
|
440
|
+
conv: U([convDim]), step: U([s.kHeads, s.dState, keyDim, 0, eps, { f: 1 / Math.sqrt(s.dState) }]), ng: U([s.dState, eps]),
|
|
441
|
+
silu: U([F]), am: U([c.vocab]),
|
|
442
|
+
};
|
|
443
|
+
const W = this.w, raw = (name) => { const t = W[name]; if (!t || !t.raw || this.types[name]) throw new Error(`missing F32 tensor ${name}`); return t.raw; };
|
|
444
|
+
const half = (off) => ({ buffer: a.gu, offset: off * F * 4, size: F * 4 });
|
|
445
|
+
const et = this.types['token_embd.weight'];
|
|
446
|
+
this.g = {
|
|
447
|
+
embed: this.bind(`embed_${et}`, [W['token_embd.weight'].raw, this.grid, a.x, this.tok, U([H, gpuRowBytes(et, H)])]),
|
|
448
|
+
rmsOut: this.bind('rmsnorm', [a.x, raw('output_norm.weight'), a.xn, u.rmsH]),
|
|
449
|
+
lm: this.mv('output.weight', a.xn, a.logits, c.vocab, H),
|
|
450
|
+
am: this.bind('argmax', [a.logits, a.am, u.am]),
|
|
451
|
+
};
|
|
452
|
+
this.layers = [];
|
|
453
|
+
for (let l = 0; l < c.layers; l++) {
|
|
454
|
+
const n = (t) => `blk.${l}.${t}`;
|
|
455
|
+
const g = { recur: this.recurrent[l], rmsA: this.bind('rmsnorm', [a.x, raw(n('attn_norm.weight')), a.xn, u.rmsH]) };
|
|
456
|
+
if (g.recur) {
|
|
457
|
+
Object.assign(g, {
|
|
458
|
+
qkv: this.mv(n('attn_qkv.weight'), a.xn, a.mixed, convDim, H),
|
|
459
|
+
z: this.mv(n('attn_gate.weight'), a.xn, a.z, s.dInner, H),
|
|
460
|
+
beta: this.mv(n('ssm_beta.weight'), a.xn, a.bb, s.vHeads, H),
|
|
461
|
+
alpha: this.mv(n('ssm_alpha.weight'), a.xn, a.al, s.vHeads, H),
|
|
462
|
+
conv: this.bind('gdnConv', [a.mixed, raw(n('ssm_conv1d.weight')), this.conv[l], a.cv, u.conv]),
|
|
463
|
+
step: this.bind('gdnStep', [a.cv, a.bb, a.al, raw(n('ssm_dt.bias')), raw(n('ssm_a')), this.ssm[l], a.go, u.step]),
|
|
464
|
+
norm: this.bind('gdnNormGate', [a.go, raw(n('ssm_norm.weight')), a.z, a.gn, u.ng]),
|
|
465
|
+
out: this.mv(n('ssm_out.weight'), a.gn, a.o, H, s.dInner),
|
|
466
|
+
});
|
|
467
|
+
} else {
|
|
468
|
+
Object.assign(g, {
|
|
469
|
+
q: this.mv(n('attn_q.weight'), a.xn, a.qg, c.gatedQ ? 2 * QN : QN, H),
|
|
470
|
+
k: this.mv(n('attn_k.weight'), a.xn, a.k, kvn, H),
|
|
471
|
+
v: this.mv(n('attn_v.weight'), a.xn, a.v, kvn, H),
|
|
472
|
+
ropeQ: this.bind('qkNormRopeP', [a.qg, raw(n('attn_q_norm.weight')), a.rope, a.q, a.gate, u.ropeQ]),
|
|
473
|
+
ropeK: this.bind('qkNormRopeP', [a.k, raw(n('attn_k_norm.weight')), a.rope, a.kr, a.dummy, u.ropeK]),
|
|
474
|
+
kv: this.bind('kvStore', [a.kr, a.v, this.kc[l], this.vc[l], this.tok, u.kvn]),
|
|
475
|
+
score: this.bind('attnScore', [a.q, this.kc[l], a.sc, this.tok, u.att]),
|
|
476
|
+
soft: this.bind('softmax', [a.sc, this.tok]),
|
|
477
|
+
attOut: this.bind('attnOut', [a.sc, this.vc[l], a.att, this.tok, u.attOut]),
|
|
478
|
+
gate: c.gatedQ ? this.bind('sigmoidMul', [a.att, a.gate, u.gate]) : null,
|
|
479
|
+
o: this.mv(n('attn_output.weight'), a.att, a.o, H, QN),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
Object.assign(g, {
|
|
483
|
+
addO: this.bind('addInPlace', [a.x, a.o, u.nH]),
|
|
484
|
+
rmsF: this.bind('rmsnorm', [a.x, raw(n(c.ffnNorm)), a.xn, u.rmsH]),
|
|
485
|
+
up: this.mv(n('ffn_up.weight'), a.xn, half(1), F, H),
|
|
486
|
+
gt: this.mv(n('ffn_gate.weight'), a.xn, half(0), F, H),
|
|
487
|
+
silu: this.bind('siluMul', [a.gu, a.act, u.silu]),
|
|
488
|
+
down: this.mv(n('ffn_down.weight'), a.act, a.o, H, F),
|
|
489
|
+
addD: this.bind('addInPlace', [a.x, a.o, u.nH]),
|
|
490
|
+
});
|
|
491
|
+
this.layers.push(g);
|
|
492
|
+
}
|
|
493
|
+
if (this.batchPrefill) this.initBatch();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── one token ─────────────────────────────────────────────────────────────
|
|
497
|
+
run(pass, op) { this.dispatch(pass, op.k, op.g, op.wg); }
|
|
498
|
+
encodeLayer(pass, l, seqLen) {
|
|
499
|
+
const c = this.cfg, s = c.ssm || NO_SSM, g = this.layers[l], d = (nm, gr, x) => this.dispatch(pass, nm, gr, x);
|
|
500
|
+
const H = c.hidden, QN = c.heads * c.headDim, kvn = c.kvHeads * c.headDim, convDim = 2 * s.kHeads * s.dState + s.dInner;
|
|
501
|
+
d('rmsnorm', g.rmsA, 1);
|
|
502
|
+
if (g.recur) {
|
|
503
|
+
this.run(pass, g.qkv); this.run(pass, g.z); this.run(pass, g.beta); this.run(pass, g.alpha);
|
|
504
|
+
d('gdnConv', g.conv, Math.ceil(convDim / 256));
|
|
505
|
+
d('gdnStep', g.step, s.vHeads);
|
|
506
|
+
d('gdnNormGate', g.norm, s.vHeads);
|
|
507
|
+
this.run(pass, g.out);
|
|
508
|
+
} else {
|
|
509
|
+
this.run(pass, g.q); this.run(pass, g.k); this.run(pass, g.v);
|
|
510
|
+
d('qkNormRopeP', g.ropeQ, c.heads); d('qkNormRopeP', g.ropeK, c.kvHeads);
|
|
511
|
+
d('kvStore', g.kv, Math.ceil(kvn / 256));
|
|
512
|
+
d('attnScore', g.score, Math.ceil(c.heads * seqLen / 256));
|
|
513
|
+
d('softmax', g.soft, c.heads);
|
|
514
|
+
d('attnOut', g.attOut, Math.ceil(QN / 256));
|
|
515
|
+
if (g.gate) d('sigmoidMul', g.gate, Math.ceil(QN / 256));
|
|
516
|
+
this.run(pass, g.o);
|
|
517
|
+
}
|
|
518
|
+
d('addInPlace', g.addO, Math.ceil(H / 256));
|
|
519
|
+
d('rmsnorm', g.rmsF, 1);
|
|
520
|
+
this.run(pass, g.gt); this.run(pass, g.up);
|
|
521
|
+
d('siluMul', g.silu, Math.ceil(c.ff / 256));
|
|
522
|
+
this.run(pass, g.down);
|
|
523
|
+
d('addInPlace', g.addD, Math.ceil(H / 256));
|
|
524
|
+
const c1 = this.stepFeatRow != null && this.capture1 && this.capture1.get(l);
|
|
525
|
+
if (c1) d('copyRow', c1, Math.ceil(H / 256));
|
|
526
|
+
}
|
|
527
|
+
encodeHead(pass, want) {
|
|
528
|
+
this.dispatch(pass, 'rmsnorm', this.g.rmsOut, 1);
|
|
529
|
+
this.run(pass, this.g.lm);
|
|
530
|
+
if (want === 'argmax') this.dispatch(pass, 'argmax', this.g.am, 1);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// One token through every layer at this.position, in one submit. want: 'none' | 'argmax' | 'logits'.
|
|
534
|
+
async step(token, want = 'argmax') {
|
|
535
|
+
const c = this.cfg, dev = this.device;
|
|
536
|
+
if (this.position >= this.maxCtx) throw this.contextFull();
|
|
537
|
+
const tStart = performance.now(), pos = this.position;
|
|
538
|
+
this.writeTokenUniforms(token, pos);
|
|
539
|
+
if (this.stepFeatRow != null && this.featRowU) this.device.queue.writeBuffer(this.featRowU, 0, new Uint32Array([this.stepFeatRow, 0, 0, 0]));
|
|
540
|
+
const enc = dev.createCommandEncoder(), pass = enc.beginComputePass();
|
|
541
|
+
this.dispatch(pass, `embed_${this.types['token_embd.weight']}`, this.g.embed, Math.ceil(c.hidden / 8 / 256));
|
|
542
|
+
for (let l = 0; l < c.layers; l++) this.encodeLayer(pass, l, pos + 1);
|
|
543
|
+
if (want !== 'none') this.encodeHead(pass, want);
|
|
544
|
+
pass.end();
|
|
545
|
+
if (want === 'argmax') enc.copyBufferToBuffer(this.a.am, 0, this.rbArg, 0, 4);
|
|
546
|
+
if (want === 'logits') enc.copyBufferToBuffer(this.a.logits, 0, this.rbLogits, 0, c.vocab * 4);
|
|
547
|
+
dev.queue.submit([enc.finish()]);
|
|
548
|
+
this.counters.encodeMs += performance.now() - tStart;
|
|
549
|
+
this.position++;
|
|
550
|
+
this.cached.push(token);
|
|
551
|
+
const result = await this.readResult(want);
|
|
552
|
+
this.counters.tokens++;
|
|
553
|
+
this.counters.wallMs += performance.now() - tStart;
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
async readResult(want) {
|
|
557
|
+
const tw = performance.now();
|
|
558
|
+
let result = null;
|
|
559
|
+
if (want === 'argmax') {
|
|
560
|
+
await this.rbArg.mapAsync(1, 0, 4);
|
|
561
|
+
result = new Uint32Array(this.rbArg.getMappedRange(0, 4))[0];
|
|
562
|
+
this.rbArg.unmap();
|
|
563
|
+
} else if (want === 'logits') {
|
|
564
|
+
await this.rbLogits.mapAsync(1);
|
|
565
|
+
result = new Float32Array(this.rbLogits.getMappedRange().slice(0));
|
|
566
|
+
this.rbLogits.unmap();
|
|
567
|
+
} else {
|
|
568
|
+
await this.device.queue.onSubmittedWorkDone();
|
|
569
|
+
}
|
|
570
|
+
this.counters.gpuWaitMs += performance.now() - tw;
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ── batched prefill: chunks of T prompt tokens, token-major ───────────────
|
|
575
|
+
initBatch() {
|
|
576
|
+
const c = this.cfg, s = c.ssm || NO_SSM, B = this.chunkTokens, a = this.a;
|
|
577
|
+
const H = c.hidden, hd = c.headDim, QN = c.heads * hd, kvn = c.kvHeads * hd, F = c.ff;
|
|
578
|
+
const keyDim = s.kHeads * s.dState, convDim = 2 * keyDim + s.dInner;
|
|
579
|
+
const A = (n) => this.buffer(n * 4, STORAGE | COPY_SRC | COPY_DST, 'batch');
|
|
580
|
+
const b = this.b = {
|
|
581
|
+
ids: A(B), x: A(B * H), xn: A(B * H), qg: A(B * 2 * QN), q: A(B * QN), gate: A(B * QN), k: A(B * kvn), kr: A(B * kvn), v: A(B * kvn),
|
|
582
|
+
att: A(B * QN), o: A(B * H), sc: this.flashAttention ? A(4) : A(c.heads * B * this.maxCtx), mixed: A(B * convDim), cv: A(B * convDim), z: A(B * s.dInner),
|
|
583
|
+
bb: A(B * s.vHeads), al: A(B * s.vHeads), go: A(B * s.dInner), gn: A(B * s.dInner),
|
|
584
|
+
g: A(B * F), u: A(B * F), act: A(B * F), rope: A(B * c.ropeDims), dummy: A(4),
|
|
585
|
+
};
|
|
586
|
+
const U = (w) => this.uniform(w), eps = { f: c.eps }, qb = this.qb, W = this.w;
|
|
587
|
+
const u = {
|
|
588
|
+
nH: U([H]), rmsH: U([H, eps]),
|
|
589
|
+
ropeQ: U(c.gatedQ ? [hd, 2 * hd, 2 * QN, QN, c.ropeDims, 1, eps] : [hd, hd, QN, QN, c.ropeDims, 0, eps]), ropeK: U([hd, hd, kvn, kvn, c.ropeDims, 0, eps]), kvn: U([kvn]),
|
|
590
|
+
att: U([c.heads, c.kvHeads, hd, { f: 1 / Math.sqrt(hd) }]), attOut: U([c.heads, c.kvHeads, hd, 0]), gate: U([QN]),
|
|
591
|
+
conv: U([convDim]), step: U([s.kHeads, s.dState, keyDim, convDim, s.vHeads, 0, eps, { f: 1 / Math.sqrt(s.dState) }]),
|
|
592
|
+
ng: U([s.dState, s.dInner, eps]), ff: U([F]),
|
|
593
|
+
};
|
|
594
|
+
const raw = (name) => W[name].raw, et = this.types['token_embd.weight'];
|
|
595
|
+
this.gB = { embed: this.bind(`embedB_${et}`, [W['token_embd.weight'].raw, this.grid, b.ids, b.x, U([H, gpuRowBytes(et, H)]), qb]) };
|
|
596
|
+
// Speculative decoding: the chosen layers' output rows, side by side (B × layers·H), for a drafter; and a head over
|
|
597
|
+
// up to ROWS_MAX rows with one argmax per row, for verifying a drafted block in one pass.
|
|
598
|
+
const cap = this.opts.captureLayers || [];
|
|
599
|
+
if (cap.length) {
|
|
600
|
+
this.featWidth = cap.length * H;
|
|
601
|
+
b.feat = A(B * this.featWidth);
|
|
602
|
+
this.capture = new Map(cap.map((L, j) => [L, this.bind('copyCols', [b.x, b.feat, U([H, this.featWidth, j * H, 0]), qb])]));
|
|
603
|
+
// One-token steps with this.stepFeatRow set write their features to that row (a drafter's plain stretch).
|
|
604
|
+
this.featRowU = this.buffer(16, UNIFORM | COPY_DST, 'batch');
|
|
605
|
+
this.capture1 = new Map(cap.map((L, j) => [L, this.bind('copyRow', [a.x, b.feat, U([H, this.featWidth, j * H, 0]), this.featRowU])]));
|
|
606
|
+
b.xnR = A(ROWS_MAX * H); b.logitsR = A(ROWS_MAX * c.vocab);
|
|
607
|
+
b.amR = this.buffer(ROWS_MAX * 4, STORAGE | COPY_SRC, 'batch');
|
|
608
|
+
this.rbRows = this.buffer(ROWS_MAX * 4, MAP_READ | COPY_DST, 'batch');
|
|
609
|
+
this.gB.rmsOutR = this.bind('rmsnormB', [b.x, raw('output_norm.weight'), b.xnR, u.rmsH]);
|
|
610
|
+
this.gB.lmR = this.mm('output.weight', b.xnR, b.logitsR, c.vocab, H);
|
|
611
|
+
this.gB.amR = this.bind('argmaxRows', [b.logitsR, b.amR, U([c.vocab])]);
|
|
612
|
+
}
|
|
613
|
+
for (let l = 0; l < c.layers; l++) {
|
|
614
|
+
const n = (t) => `blk.${l}.${t}`, g = { rmsA: this.bind('rmsnormB', [b.x, raw(n('attn_norm.weight')), b.xn, u.rmsH]) };
|
|
615
|
+
if (this.recurrent[l]) {
|
|
616
|
+
Object.assign(g, {
|
|
617
|
+
qkv: this.mm(n('attn_qkv.weight'), b.xn, b.mixed, convDim, H), z: this.mm(n('attn_gate.weight'), b.xn, b.z, s.dInner, H),
|
|
618
|
+
beta: this.mm(n('ssm_beta.weight'), b.xn, b.bb, s.vHeads, H), alpha: this.mm(n('ssm_alpha.weight'), b.xn, b.al, s.vHeads, H),
|
|
619
|
+
conv: this.bind('gdnConvB', [b.mixed, raw(n('ssm_conv1d.weight')), this.conv[l], b.cv, u.conv, qb]),
|
|
620
|
+
step: this.bind('gdnStepR', [b.cv, b.bb, b.al, raw(n('ssm_dt.bias')), raw(n('ssm_a')), this.ssm[l], b.go, u.step, qb]),
|
|
621
|
+
norm: this.bind('gdnNormGateB', [b.go, raw(n('ssm_norm.weight')), b.z, b.gn, u.ng]),
|
|
622
|
+
out: this.mm(n('ssm_out.weight'), b.gn, b.o, H, s.dInner),
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
Object.assign(g, {
|
|
626
|
+
q: this.mm(n('attn_q.weight'), b.xn, b.qg, c.gatedQ ? 2 * QN : QN, H), k: this.mm(n('attn_k.weight'), b.xn, b.k, kvn, H), v: this.mm(n('attn_v.weight'), b.xn, b.v, kvn, H),
|
|
627
|
+
ropeQ: this.bind('qkNormRopePB', [b.qg, raw(n('attn_q_norm.weight')), b.rope, b.q, b.gate, u.ropeQ]),
|
|
628
|
+
ropeK: this.bind('qkNormRopePB', [b.k, raw(n('attn_k_norm.weight')), b.rope, b.kr, b.dummy, u.ropeK]),
|
|
629
|
+
kv: this.bind('kvStoreB', [b.kr, b.v, this.kc[l], this.vc[l], u.kvn, qb]),
|
|
630
|
+
...(this.flashAttention ? { flash: this.bind('flashAttnB', [b.q, this.kc[l], this.vc[l], b.att, u.att, qb]) } : {
|
|
631
|
+
score: this.bind('attnScoreB', [b.q, this.kc[l], b.sc, u.att, qb]),
|
|
632
|
+
soft: this.bind('softmaxB', [b.sc, qb]),
|
|
633
|
+
attOut: this.bind('attnOutB', [b.sc, this.vc[l], b.att, u.attOut, qb]),
|
|
634
|
+
}),
|
|
635
|
+
gate: c.gatedQ ? this.bind('sigmoidMulB', [b.att, b.gate, u.gate, qb]) : null,
|
|
636
|
+
o: this.mm(n('attn_output.weight'), b.att, b.o, H, QN),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
Object.assign(g, {
|
|
640
|
+
addO: this.bind('addInPlaceB', [b.x, b.o, u.nH, qb]),
|
|
641
|
+
rmsF: this.bind('rmsnormB', [b.x, raw(n(c.ffnNorm)), b.xn, u.rmsH]),
|
|
642
|
+
gt: this.mm(n('ffn_gate.weight'), b.xn, b.g, F, H), up: this.mm(n('ffn_up.weight'), b.xn, b.u, F, H),
|
|
643
|
+
silu: this.bind('siluMul2B', [b.g, b.u, b.act, u.ff, qb]),
|
|
644
|
+
down: this.mm(n('ffn_down.weight'), b.act, b.o, H, F),
|
|
645
|
+
addD: this.bind('addInPlaceB', [b.x, b.o, u.nH, qb]),
|
|
646
|
+
});
|
|
647
|
+
this.layers[l].B = g;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
encodeLayerB(pass, l, T, S) {
|
|
652
|
+
const c = this.cfg, s = c.ssm || NO_SSM, g = this.layers[l].B, d = (nm, gr, x, y) => this.dispatch(pass, nm, gr, x, y);
|
|
653
|
+
const H = c.hidden, QN = c.heads * c.headDim, kvn = c.kvHeads * c.headDim, convDim = 2 * s.kHeads * s.dState + s.dInner;
|
|
654
|
+
const tt = Math.ceil(T / (this.matrixKernels ? MM_SG_TOKENS : MM_TILE));
|
|
655
|
+
const mm = (op) => {
|
|
656
|
+
const sm = op.small && (T <= 8 ? op.small8 : T <= MVT_TOKENS ? op.small : null);
|
|
657
|
+
if (sm) this.dispatch(pass, sm.k, sm.g, sm.wg); else this.dispatch(pass, op.k, op.g, op.wg, tt);
|
|
658
|
+
};
|
|
659
|
+
d('rmsnormB', g.rmsA, T);
|
|
660
|
+
if (this.recurrent[l]) {
|
|
661
|
+
mm(g.qkv); mm(g.z); mm(g.beta); mm(g.alpha);
|
|
662
|
+
d('gdnConvB', g.conv, Math.ceil(convDim / 256));
|
|
663
|
+
d('gdnStepR', g.step, s.vHeads);
|
|
664
|
+
d('gdnNormGateB', g.norm, s.vHeads, T);
|
|
665
|
+
mm(g.out);
|
|
666
|
+
} else {
|
|
667
|
+
mm(g.q); mm(g.k); mm(g.v);
|
|
668
|
+
d('qkNormRopePB', g.ropeQ, c.heads, T); d('qkNormRopePB', g.ropeK, c.kvHeads, T);
|
|
669
|
+
d('kvStoreB', g.kv, Math.ceil(kvn / 256), T);
|
|
670
|
+
if (g.flash) d('flashAttnB', g.flash, c.kvHeads, Math.ceil(T / 4));
|
|
671
|
+
else { d('attnScoreB', g.score, Math.ceil(c.heads * T * S / 256)); d('softmaxB', g.soft, c.heads * T); d('attnOutB', g.attOut, Math.ceil(QN / 256), T); }
|
|
672
|
+
if (g.gate) d('sigmoidMulB', g.gate, Math.ceil(QN / 256), T);
|
|
673
|
+
mm(g.o);
|
|
674
|
+
}
|
|
675
|
+
d('addInPlaceB', g.addO, Math.ceil(H / 256), T);
|
|
676
|
+
d('rmsnormB', g.rmsF, T);
|
|
677
|
+
mm(g.gt); mm(g.up);
|
|
678
|
+
d('siluMul2B', g.silu, Math.ceil(c.ff / 256), T);
|
|
679
|
+
mm(g.down);
|
|
680
|
+
d('addInPlaceB', g.addD, Math.ceil(H / 256), T);
|
|
681
|
+
const cap = this.capture && this.capture.get(l);
|
|
682
|
+
if (cap) d('copyCols', cap, Math.ceil(H / 256), T);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// T = ids.length tokens at this.position through every layer in one submit; want applies to the last token, except
|
|
686
|
+
// 'rows' (opts.captureLayers set, T ≤ ROWS_MAX): the argmax of every row, a Uint32Array of T.
|
|
687
|
+
async prefillChunk(ids, want = 'none') {
|
|
688
|
+
const c = this.cfg, dev = this.device, H = c.hidden, T = ids.length, pos0 = this.position, S = pos0 + T;
|
|
689
|
+
if (T > this.chunkTokens) throw new Error(`chunk of ${T} > ${this.chunkTokens}`);
|
|
690
|
+
if (S > this.maxCtx) throw this.contextFull();
|
|
691
|
+
const tStart = performance.now();
|
|
692
|
+
dev.queue.writeBuffer(this.b.ids, 0, Uint32Array.from(ids));
|
|
693
|
+
dev.queue.writeBuffer(this.qb, 0, new Uint32Array([T, pos0, S, 0]));
|
|
694
|
+
const nRot = c.ropeDims, half = nRot / 2, r = new Float32Array(T * nRot), stepR = Math.fround(Math.pow(c.ropeTheta, -2 / nRot));
|
|
695
|
+
for (let t = 0; t < T; t++) {
|
|
696
|
+
let th = Math.fround(pos0 + t);
|
|
697
|
+
for (let i = 0; i < half; i++) { r[t * nRot + i] = Math.cos(th); r[t * nRot + half + i] = Math.sin(th); th = Math.fround(th * stepR); }
|
|
698
|
+
}
|
|
699
|
+
dev.queue.writeBuffer(this.b.rope, 0, r);
|
|
700
|
+
const enc = dev.createCommandEncoder();
|
|
701
|
+
let pass = enc.beginComputePass();
|
|
702
|
+
this.dispatch(pass, `embedB_${this.types['token_embd.weight']}`, this.gB.embed, Math.ceil(H / 8 / 256), T);
|
|
703
|
+
for (let l = 0; l < c.layers; l++) this.encodeLayerB(pass, l, T, S);
|
|
704
|
+
if (want === 'rows') {
|
|
705
|
+
if (!this.capture || T > ROWS_MAX) throw new Error(`rows: needs captureLayers and at most ${ROWS_MAX} tokens`);
|
|
706
|
+
const tt = Math.ceil(T / (this.matrixKernels ? MM_SG_TOKENS : MM_TILE)), lm = this.gB.lmR;
|
|
707
|
+
this.dispatch(pass, 'rmsnormB', this.gB.rmsOutR, T);
|
|
708
|
+
const sm = lm.small && (T <= 8 ? lm.small8 : T <= MVT_TOKENS ? lm.small : null);
|
|
709
|
+
if (sm) this.dispatch(pass, sm.k, sm.g, sm.wg); else this.dispatch(pass, lm.k, lm.g, lm.wg, tt);
|
|
710
|
+
this.dispatch(pass, 'argmaxRows', this.gB.amR, T);
|
|
711
|
+
}
|
|
712
|
+
pass.end();
|
|
713
|
+
enc.copyBufferToBuffer(this.b.x, (T - 1) * H * 4, this.a.x, 0, H * 4);
|
|
714
|
+
if (want === 'rows') enc.copyBufferToBuffer(this.b.amR, 0, this.rbRows, 0, T * 4);
|
|
715
|
+
else if (want !== 'none') {
|
|
716
|
+
pass = enc.beginComputePass();
|
|
717
|
+
this.encodeHead(pass, want);
|
|
718
|
+
pass.end();
|
|
719
|
+
if (want === 'argmax') enc.copyBufferToBuffer(this.a.am, 0, this.rbArg, 0, 4);
|
|
720
|
+
if (want === 'logits') enc.copyBufferToBuffer(this.a.logits, 0, this.rbLogits, 0, c.vocab * 4);
|
|
721
|
+
}
|
|
722
|
+
dev.queue.submit([enc.finish()]);
|
|
723
|
+
this.position += T;
|
|
724
|
+
this.cached.push(...ids);
|
|
725
|
+
let result;
|
|
726
|
+
if (want === 'rows') {
|
|
727
|
+
const tw = performance.now();
|
|
728
|
+
await this.rbRows.mapAsync(1, 0, T * 4);
|
|
729
|
+
result = new Uint32Array(this.rbRows.getMappedRange(0, T * 4).slice(0));
|
|
730
|
+
this.rbRows.unmap();
|
|
731
|
+
this.counters.gpuWaitMs += performance.now() - tw;
|
|
732
|
+
} else result = await this.readResult(want);
|
|
733
|
+
this.counters.tokens += T;
|
|
734
|
+
this.counters.wallMs += performance.now() - tStart;
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Back to `pos` tokens (a verify pass that kept only part of its block). Attention layers only: their KV past pos is
|
|
739
|
+
// overwritten by the next tokens; DeltaNet state cannot rewind.
|
|
740
|
+
rewind(pos) {
|
|
741
|
+
if (this.recurrent.some(Boolean)) throw new Error('rewind: this model has recurrent layers');
|
|
742
|
+
this.position = pos; this.cached.length = pos;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// The recurrent state cannot rewind, so any change before the cached end restarts from token 0.
|
|
746
|
+
async prefill(ids, want = 'argmax') {
|
|
747
|
+
let common = 0;
|
|
748
|
+
while (common < ids.length - 1 && common < this.cached.length && this.cached[common] === ids[common]) common++;
|
|
749
|
+
if (common < this.cached.length) this.reset();
|
|
750
|
+
const rest = ids.slice(this.cached.length);
|
|
751
|
+
if (!this.batchPrefill || rest.length < 2) {
|
|
752
|
+
let r = null;
|
|
753
|
+
for (let i = 0; i < rest.length; i++) r = await this.step(rest[i], i === rest.length - 1 ? want : 'none');
|
|
754
|
+
return r;
|
|
755
|
+
}
|
|
756
|
+
let r = null;
|
|
757
|
+
for (let i = 0; i < rest.length; i += this.chunkTokens) {
|
|
758
|
+
r = await this.prefillChunk(rest.slice(i, i + this.chunkTokens), i + this.chunkTokens >= rest.length ? want : 'none');
|
|
759
|
+
}
|
|
760
|
+
return r;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
reset() {
|
|
764
|
+
super.reset();
|
|
765
|
+
if (!this.conv) return;
|
|
766
|
+
const enc = this.device.createCommandEncoder();
|
|
767
|
+
for (let l = 0; l < this.cfg.layers; l++) if (this.recurrent[l]) { enc.clearBuffer(this.conv[l]); enc.clearBuffer(this.ssm[l]); }
|
|
768
|
+
this.device.queue.submit([enc.finish()]);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// RoPE angles as ggml's CPU rope cache forms them for text positions (qwen35_moe_ssd.js has the derivation).
|
|
772
|
+
writeTokenUniforms(token, pos) {
|
|
773
|
+
const c = this.cfg, nRot = c.ropeDims, half = nRot / 2;
|
|
774
|
+
this.device.queue.writeBuffer(this.tok, 0, new Uint32Array([token, pos, pos + 1, 0]));
|
|
775
|
+
const r = new Float32Array(nRot), step = Math.fround(Math.pow(c.ropeTheta, -2 / nRot));
|
|
776
|
+
let th = Math.fround(pos);
|
|
777
|
+
for (let i = 0; i < half; i++) { r[i] = Math.cos(th); r[half + i] = Math.sin(th); th = Math.fround(th * step); }
|
|
778
|
+
this.device.queue.writeBuffer(this.a.rope, 0, r);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Qwen3.5-family template: the generation prompt opens the reasoning block when thinking is on.
|
|
782
|
+
chatPrompt(messages, { enableThinking = true } = {}) {
|
|
783
|
+
let p = '';
|
|
784
|
+
for (const m of messages) p += `<|im_start|>${m.role}\n${m.content}<|im_end|>\n`;
|
|
785
|
+
return p + '<|im_start|>assistant\n' + (enableThinking ? '<think>\n' : '<think>\n\n</think>\n\n');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
resetCounters() { this.counters = { tokens: 0, gpuWaitMs: 0, encodeMs: 0, wallMs: 0 }; }
|
|
789
|
+
stats() {
|
|
790
|
+
const c = this.counters, n = Math.max(1, c.tokens);
|
|
791
|
+
return { tokens: c.tokens, tokPerSec: c.tokens / (c.wallMs / 1000), msPerToken: c.wallMs / n, gpuWaitMsPerToken: c.gpuWaitMs / n, gpuBytes: { ...this.gpuBytes } };
|
|
792
|
+
}
|
|
793
|
+
async dispose() { await this.retire(); try { this.device.destroy(); } catch (_) {} }
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Plain dense Qwen3 (arch qwen3) on the same engine. Without a url it loads Qwen's own Q8_0 GGUF of Qwen3-4B, the
|
|
797
|
+
// target of the FlashMap drafter j-shen/FlashMap-Qwen3-4B (Q8_0 chosen by Chirag 2026-10-22 as closest to the bf16
|
|
798
|
+
// model the drafter was trained on).
|
|
799
|
+
export const QWEN3_4B_Q8 = {
|
|
800
|
+
repo: 'Qwen/Qwen3-4B-GGUF',
|
|
801
|
+
file: 'Qwen3-4B-Q8_0.gguf',
|
|
802
|
+
revision: 'bc640142c66e1fdd12af0bd68f40445458f3869b',
|
|
803
|
+
sha256: '8c2f07f26af9747e41988551106f149b03eb9b5cb6df636027b6bf6278473300',
|
|
804
|
+
size: 4280404704,
|
|
805
|
+
};
|
|
806
|
+
// With FlashMap (default; opts.flashmap === false loads the target alone): the drafter j-shen/FlashMap-Qwen3-4B
|
|
807
|
+
// (0.95 GB, copied into OPFS once) and speculative greedy decoding in generateTurn (flashmap_drafter.js specGenerate:
|
|
808
|
+
// the same tokens as plain greedy, faster on code and maths, plain steps where drafting does not pay).
|
|
809
|
+
export class Qwen3Dense extends Qwen35Dense {
|
|
810
|
+
static async load(modelId = null, opts = {}) {
|
|
811
|
+
const source = opts.source || (opts.url ? { repo: null, file: opts.url.split('/').pop(), revision: null } : QWEN3_4B_Q8);
|
|
812
|
+
const flashmap = opts.flashmap !== false;
|
|
813
|
+
const m = await super.load(modelId, { ...opts, source, ...(flashmap ? { captureLayers: FLASHMAP_QWEN3_4B_LAYERS } : {}) });
|
|
814
|
+
if (flashmap && !opts.captureLayers) m.drafter = await FlashMapDrafter.load(m, { fetch: opts.fetch, onProgress: opts.onProgress || (() => {}), url: opts.drafterUrl, cache: opts.drafterCache });
|
|
815
|
+
return m;
|
|
816
|
+
}
|
|
817
|
+
async *generateTurn(messages, opts = {}) {
|
|
818
|
+
if (!this.drafter) { yield* super.generateTurn(messages, opts); return; }
|
|
819
|
+
const { maxNewTokens = 512, signal, enableThinking = true } = opts;
|
|
820
|
+
const ids = this.tokenizer.encode(this.chatPrompt(messages, { enableThinking }));
|
|
821
|
+
const stops = new Set(this.stopTokenIds), out = [];
|
|
822
|
+
for await (const r of specGenerate(this, this.drafter, ids, { maxNewTokens })) {
|
|
823
|
+
for (const tok of r.tokens) {
|
|
824
|
+
if (stops.has(tok)) return;
|
|
825
|
+
out.push(tok);
|
|
826
|
+
yield { text: this.tokenizer.decode(out), token: tok, tokens: out.length };
|
|
827
|
+
if (out.length >= maxNewTokens) return;
|
|
828
|
+
}
|
|
829
|
+
if (signal && signal.aborted) return;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async dispose() { if (this.drafter) this.drafter.destroy(); await super.dispose(); }
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// For kernel tests (scripts/test-qwen35-dense-kernels.mjs).
|
|
836
|
+
export { PREFILL_KERNELS as QWEN35_DENSE_PREFILL_KERNELS };
|