Spaces:
Running
Running
| // QVAC disk-streaming GGUF ingestion β pure JS, no wasm in the weight path. | |
| // | |
| // The wasm path holds the whole GGUF in wasm32 linear memory (~4 GB ceiling), so | |
| // it can't ingest a 7B/14B model. This module reads a large GGUF straight off disk | |
| // (HTTP Range against the served file) and converts each tensor to the engine's | |
| // block format ON DEMAND, so RAM never holds more than one tensor at a time. | |
| // | |
| // It is a faithful port of qvac-gguf::dequantize_raw + qvac-layer::quant_blocks + | |
| // model_specs, so the bytes it produces are what the GPU engine already consumes. | |
| // The tokenizer stays in wasm: we feed wasm only the GGUF HEADER (the first | |
| // data_offset bytes), which is all qvac_load_gpu needs for the BPE + manifest. | |
| import { makeIQ } from "./forge/gguf-forge-iq-dequant.mjs"; | |
| // ββ f16 β f32 (matches qvac_gguf::f16_to_f32) ββ | |
| export function f16ToF32(h) { | |
| const sign = (h >> 15) & 1, exp = (h >> 10) & 0x1f, mant = h & 0x3ff; | |
| let val; | |
| if (exp === 0) val = mant * Math.pow(2, -24); | |
| else if (exp === 0x1f) return mant ? NaN : (sign ? -Infinity : Infinity); | |
| else val = (1 + mant / 1024) * Math.pow(2, exp - 15); | |
| return sign ? -val : val; | |
| } | |
| const QK = 32, QK_K = 256; | |
| export const GGML = { | |
| F32: 0, F16: 1, Q4_0: 2, Q4_1: 3, Q8_0: 8, Q2_K: 10, Q3_K: 11, Q4_K: 12, Q5_K: 13, Q6_K: 14, | |
| IQ2_XXS: 16, IQ2_XS: 17, IQ3_XXS: 18, IQ1_S: 19, IQ4_NL: 20, IQ3_S: 21, IQ2_S: 22, IQ4_XS: 23, IQ1_M: 29, | |
| TQ2_0: 35, | |
| }; | |
| // IQ-quant byte layouts: [block elements, block bytes]. (ggml-common.h:485-563) | |
| const IQ_BLOCK = { | |
| 16: [QK_K, 66], 17: [QK_K, 74], 18: [QK_K, 98], 19: [QK_K, 50], 20: [32, 18], | |
| 21: [QK_K, 110], 22: [QK_K, 82], 23: [QK_K, 136], 29: [QK_K, 56], | |
| }; | |
| // BitNet ternary TQ2_0 (ggml-common.h:273): qs[64] + f16 d = 66 B / 256. Runtime | |
| // dequant is float64 (the Tier-A oracle gguf-forge-dequant.mjs is the bit-exact ref). | |
| const TQ_BLOCK = { 35: [QK_K, 66] }; | |
| function dequantTq2_0Rt(raw, elements) { | |
| const out = new Float32Array(elements); | |
| const nb = elements / QK_K; | |
| const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); | |
| let o = 0, base = 0; | |
| for (let i = 0; i < nb; ++i) { | |
| const d = f16ToF32(dv.getUint16(base + 64, true)); | |
| for (let j = 0; j < 64; j += 32) | |
| for (let l = 0; l < 4; ++l) | |
| for (let m = 0; m < 32; ++m) out[o++] = (((raw[base + j + m] >> (l * 2)) & 3) - 1) * d; | |
| base += 66; | |
| } | |
| return out; | |
| } | |
| const TQ_RT = { 35: dequantTq2_0Rt }; | |
| // Runtime IQ dequant (float64; the Tier-A oracle is the bit-exact reference). | |
| const _iq = makeIQ((x) => x, f16ToF32); | |
| const IQ_RT = { | |
| 16: _iq.dequantIQ2XXS, 17: _iq.dequantIQ2XS, 18: _iq.dequantIQ3XXS, 19: _iq.dequantIQ1S, | |
| 20: _iq.dequantIQ4NL, 21: _iq.dequantIQ3S, 22: _iq.dequantIQ2S, 23: _iq.dequantIQ4XS, 29: _iq.dequantIQ1M, | |
| }; | |
| // On-disk byte length of `elements` of a ggml type (matches type_byte_len). | |
| export function typeByteLen(t, elements) { | |
| switch (t) { | |
| case GGML.F32: return elements * 4; | |
| case GGML.F16: return elements * 2; | |
| case GGML.Q8_0: return (elements / QK) * (2 + QK); | |
| case GGML.Q4_0: return (elements / QK) * (2 + QK / 2); | |
| case GGML.Q4_1: return (elements / QK) * (2 + 2 + QK / 2); | |
| case GGML.Q2_K: return (elements / QK_K) * 84; | |
| case GGML.Q3_K: return (elements / QK_K) * 110; | |
| case GGML.Q4_K: return (elements / QK_K) * 144; | |
| case GGML.Q5_K: return (elements / QK_K) * 176; | |
| case GGML.Q6_K: return (elements / QK_K) * 210; | |
| default: | |
| if (t in IQ_BLOCK) { const [be, bb] = IQ_BLOCK[t]; return (elements / be) * bb; } | |
| if (t in TQ_BLOCK) { const [be, bb] = TQ_BLOCK[t]; return (elements / be) * bb; } | |
| throw new Error("unsupported ggml type " + t); | |
| } | |
| } | |
| // (block elements, block bytes) for the range reader. | |
| function blockShape(t) { | |
| switch (t) { | |
| case GGML.F32: return [1, 4]; | |
| case GGML.F16: return [1, 2]; | |
| case GGML.Q8_0: return [QK, 2 + QK]; | |
| case GGML.Q4_0: return [QK, 2 + QK / 2]; | |
| case GGML.Q4_1: return [QK, 2 + 2 + QK / 2]; | |
| case GGML.Q2_K: return [QK_K, 84]; | |
| case GGML.Q3_K: return [QK_K, 110]; | |
| case GGML.Q4_K: return [QK_K, 144]; | |
| case GGML.Q5_K: return [QK_K, 176]; | |
| case GGML.Q6_K: return [QK_K, 210]; | |
| default: | |
| if (t in IQ_BLOCK) return IQ_BLOCK[t]; | |
| if (t in TQ_BLOCK) return TQ_BLOCK[t]; | |
| throw new Error("unsupported ggml type " + t); | |
| } | |
| } | |
| // Dequantize raw tensor bytes β Float32Array (port of dequantize_raw). | |
| export function dequantizeRaw(t, raw, elements) { | |
| const out = new Float32Array(elements); | |
| const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); | |
| let o = 0; | |
| if (t === GGML.F32) { | |
| for (let i = 0; i < elements; i++) out[i] = dv.getFloat32(i * 4, true); | |
| } else if (t === GGML.F16) { | |
| for (let i = 0; i < elements; i++) out[i] = f16ToF32(dv.getUint16(i * 2, true)); | |
| } else if (t === GGML.Q8_0) { | |
| const bb = 2 + QK; | |
| for (let p = 0; p + bb <= raw.byteLength; p += bb) { | |
| const d = f16ToF32(dv.getUint16(p, true)); | |
| for (let j = 0; j < QK; j++) out[o++] = d * (raw[p + 2 + j] << 24 >> 24); | |
| } | |
| } else if (t === GGML.Q4_0) { | |
| const bb = 2 + QK / 2; | |
| for (let p = 0; p + bb <= raw.byteLength; p += bb) { | |
| const d = f16ToF32(dv.getUint16(p, true)); | |
| const qs = p + 2; | |
| for (let j = 0; j < QK / 2; j++) out[o + j] = d * ((raw[qs + j] & 0x0f) - 8); // low nibble β j | |
| for (let j = 0; j < QK / 2; j++) out[o + QK / 2 + j] = d * ((raw[qs + j] >> 4) - 8); // high β j+16 | |
| o += QK; | |
| } | |
| } else if (t === GGML.Q4_1) { | |
| const bb = 2 + 2 + QK / 2; | |
| for (let p = 0; p + bb <= raw.byteLength; p += bb) { | |
| const d = f16ToF32(dv.getUint16(p, true)), m = f16ToF32(dv.getUint16(p + 2, true)); | |
| const qs = p + 4; | |
| for (let j = 0; j < QK / 2; j++) out[o + j] = (raw[qs + j] & 0x0f) * d + m; | |
| for (let j = 0; j < QK / 2; j++) out[o + QK / 2 + j] = (raw[qs + j] >> 4) * d + m; | |
| o += QK; | |
| } | |
| } else if (t === GGML.Q4_K) { | |
| // Q4_K: 256-element super-block of 144 B = d(f16) + dmin(f16) + 12 packed 6-bit scales/mins + 128 nibble qs. | |
| const bb = 144; | |
| for (let p = 0; p + bb <= raw.byteLength; p += bb) { | |
| const d = f16ToF32(dv.getUint16(p, true)), dmin = f16ToF32(dv.getUint16(p + 2, true)); | |
| const sc = p + 4, qs = p + 16; | |
| const sm = (j) => j < 4 ? [raw[sc + j] & 63, raw[sc + j + 4] & 63] | |
| : [(raw[sc + j + 4] & 0xF) | ((raw[sc + j - 4] >> 6) << 4), (raw[sc + j + 4] >> 4) | ((raw[sc + j] >> 6) << 4)]; | |
| let q = 0, is = 0; | |
| for (let j = 0; j < QK_K; j += 64) { | |
| const [s1, m1] = sm(is), [s2, m2] = sm(is + 1); | |
| for (let l = 0; l < 32; l++) out[o + l] = d * s1 * (raw[qs + q + l] & 0xF) - dmin * m1; | |
| for (let l = 0; l < 32; l++) out[o + 32 + l] = d * s2 * (raw[qs + q + l] >> 4) - dmin * m2; | |
| o += 64; q += 32; is += 2; | |
| } | |
| } | |
| } else if (t === GGML.Q6_K) { | |
| const bb = 210; | |
| for (let p = 0; p + bb <= raw.byteLength; p += bb) { | |
| const ql = p, qh = p + 128, sc = p + 192, d = f16ToF32(dv.getUint16(p + 208, true)); | |
| for (let n = 0; n < 2; n++) { | |
| const qlo = ql + n * 64, qho = qh + n * 32, sco = sc + n * 8, yo = o + n * 128; | |
| for (let l = 0; l < 32; l++) { | |
| const is = (l / 16) | 0; | |
| const q1 = ((raw[qlo + l] & 0x0f) | (((raw[qho + l] >> 0) & 3) << 4)) - 32; | |
| const q2 = ((raw[qlo + l + 32] & 0x0f) | (((raw[qho + l] >> 2) & 3) << 4)) - 32; | |
| const q3 = ((raw[qlo + l] >> 4) | (((raw[qho + l] >> 4) & 3) << 4)) - 32; | |
| const q4 = ((raw[qlo + l + 32] >> 4) | (((raw[qho + l] >> 6) & 3) << 4)) - 32; | |
| out[yo + l] = d * (raw[sco + is] << 24 >> 24) * q1; | |
| out[yo + l + 32] = d * (raw[sco + is + 2] << 24 >> 24) * q2; | |
| out[yo + l + 64] = d * (raw[sco + is + 4] << 24 >> 24) * q3; | |
| out[yo + l + 96] = d * (raw[sco + is + 6] << 24 >> 24) * q4; | |
| } | |
| } | |
| o += QK_K; | |
| } | |
| } else if (t === GGML.Q2_K) { | |
| // 84 B: scales[16] qs[64] d(f16) dmin(f16). y = d*(sc&0xF)*q2 - dmin*(sc>>4). | |
| const bb = 84; | |
| for (let p = 0; p + bb <= raw.byteLength; p += bb) { | |
| const sc = p, qs = p + 16, d = f16ToF32(dv.getUint16(p + 80, true)), dmin = f16ToF32(dv.getUint16(p + 82, true)); | |
| let is = 0; | |
| for (let n = 0; n < QK_K; n += 128) { | |
| const q = qs + (n >> 7) * 32; | |
| for (let shift = 0; shift < 8; shift += 2) { | |
| let s = raw[sc + is++]; | |
| for (let l = 0; l < 16; l++) out[o++] = d * (s & 0xf) * ((raw[q + l] >> shift) & 3) - dmin * (s >> 4); | |
| s = raw[sc + is++]; | |
| for (let l = 0; l < 16; l++) out[o++] = d * (s & 0xf) * ((raw[q + l + 16] >> shift) & 3) - dmin * (s >> 4); | |
| } | |
| } | |
| } | |
| } else if (t === GGML.Q3_K) { | |
| // 110 B: hmask[32] qs[64] scales[12] d(f16). 6-bit signed scales via kmask unpack. | |
| const bb = 110, km1 = 0x03030303, km2 = 0x0f0f0f0f; | |
| const aux = new Uint32Array(4), sb = new Int8Array(aux.buffer); | |
| for (let p = 0; p + bb <= raw.byteLength; p += bb) { | |
| const hm = p, qs = p + 32, sco = p + 96, d = f16ToF32(dv.getUint16(p + 108, true)); | |
| aux[0] = dv.getUint32(sco, true); aux[1] = dv.getUint32(sco + 4, true); aux[2] = dv.getUint32(sco + 8, true); | |
| const tmp = aux[2]; | |
| aux[2] = ((aux[0] >>> 4) & km2) | (((tmp >>> 4) & km1) << 4); | |
| aux[3] = ((aux[1] >>> 4) & km2) | (((tmp >>> 6) & km1) << 4); | |
| aux[0] = (aux[0] & km2) | (((tmp >>> 0) & km1) << 4); | |
| aux[1] = (aux[1] & km2) | (((tmp >>> 2) & km1) << 4); | |
| let is = 0, m = 1; | |
| for (let n = 0; n < QK_K; n += 128) { | |
| const q = qs + (n >> 7) * 32; | |
| for (let shift = 0; shift < 8; shift += 2) { | |
| let dl = d * (sb[is++] - 32); | |
| for (let l = 0; l < 16; l++) out[o++] = dl * (((raw[q + l] >> shift) & 3) - ((raw[hm + l] & m) ? 0 : 4)); | |
| dl = d * (sb[is++] - 32); | |
| for (let l = 0; l < 16; l++) out[o++] = dl * (((raw[q + l + 16] >> shift) & 3) - ((raw[hm + l + 16] & m) ? 0 : 4)); | |
| m <<= 1; | |
| } | |
| } | |
| } | |
| } else if (t === GGML.Q5_K) { | |
| // 176 B: d(f16) dmin(f16) scales[12] qh[32] ql[128]. Q4_K nibbles + 5th bit from qh. | |
| const bb = 176; | |
| const smk4 = (sc, j) => j < 4 ? [raw[sc + j] & 63, raw[sc + j + 4] & 63] | |
| : [(raw[sc + j + 4] & 0xF) | ((raw[sc + j - 4] >> 6) << 4), (raw[sc + j + 4] >> 4) | ((raw[sc + j] >> 6) << 4)]; | |
| for (let p = 0; p + bb <= raw.byteLength; p += bb) { | |
| const d = f16ToF32(dv.getUint16(p, true)), dmin = f16ToF32(dv.getUint16(p + 2, true)); | |
| const sc = p + 4, qh = p + 16; let ql = p + 48, is = 0, u1 = 1, u2 = 2; | |
| for (let j = 0; j < QK_K; j += 64) { | |
| const [s1, m1] = smk4(sc, is), [s2, m2] = smk4(sc, is + 1); | |
| const d1 = d * s1, mm1 = dmin * m1, d2 = d * s2, mm2 = dmin * m2; | |
| for (let l = 0; l < 32; l++) out[o++] = d1 * ((raw[ql + l] & 0xF) + ((raw[qh + l] & u1) ? 16 : 0)) - mm1; | |
| for (let l = 0; l < 32; l++) out[o++] = d2 * ((raw[ql + l] >> 4) + ((raw[qh + l] & u2) ? 16 : 0)) - mm2; | |
| ql += 32; is += 2; u1 <<= 2; u2 <<= 2; | |
| } | |
| } | |
| } else if (t in IQ_RT) { | |
| return IQ_RT[t](raw, elements); // IQ-quants (float64 runtime; oracle is the bit-exact ref) | |
| } else if (t in TQ_RT) { | |
| return TQ_RT[t](raw, elements); // BitNet TQ2_0 (float64 runtime; oracle is the bit-exact ref) | |
| } else throw new Error("unsupported ggml type " + t); | |
| return out; | |
| } | |
| // Re-quantize a [n,k] f32 tensor into the engine's per-32-block format | |
| // (port of qvac-layer::quant_blocks). bits=4 β (nibble-8)*scale, scale=amax/7, | |
| // sequential nibble packing; bits=8 β int8, scale=amax/127. Returns {q,s}. | |
| // Arithmetic is done in f32 (Math.fround) with round-half-away-from-zero to match | |
| // Rust's f32 `round()` byte-for-byte, so the frames are bit-identical to wasm's. | |
| const fr = Math.fround; | |
| const rnd = (x) => x >= 0 ? Math.floor(x + 0.5) : Math.ceil(x - 0.5); // half away from zero (Rust f32::round) | |
| export function quantBlocks(f, n, k, bits) { | |
| const nb = k / 32; | |
| const s = new Float32Array(n * nb); | |
| let si = 0; | |
| if (bits === 4) { | |
| const q = new Uint8Array(n * k / 2); | |
| for (let row = 0; row < n; row++) { | |
| const base = row * k; | |
| for (let b = 0; b < nb; b++) { | |
| const bo = base + b * 32; | |
| let amax = 0; for (let j = 0; j < 32; j++) { const a = Math.abs(f[bo + j]); if (a > amax) amax = a; } | |
| amax = Math.max(amax, 1e-9); const scale = fr(amax / 7); s[si++] = scale; | |
| for (let j = 0; j < 32; j++) { | |
| let qv = rnd(fr(f[bo + j] / scale)); qv = qv < -8 ? -8 : qv > 7 ? 7 : qv; qv = (qv + 8) & 0xf; | |
| const g = bo + j; | |
| if ((g & 1) === 0) q[g >> 1] |= qv; else q[g >> 1] |= qv << 4; | |
| } | |
| } | |
| } | |
| return { q, s }; | |
| } else { | |
| const q = new Uint8Array(n * k); | |
| for (let row = 0; row < n; row++) { | |
| const base = row * k; | |
| for (let b = 0; b < nb; b++) { | |
| const bo = base + b * 32; | |
| let amax = 0; for (let j = 0; j < 32; j++) { const a = Math.abs(f[bo + j]); if (a > amax) amax = a; } | |
| amax = Math.max(amax, 1e-9); const scale = fr(amax / 127); s[si++] = scale; | |
| for (let j = 0; j < 32; j++) { let qv = rnd(fr(f[bo + j] / scale)); qv = qv < -127 ? -127 : qv > 127 ? 127 : qv; q[bo + j] = qv & 0xff; } | |
| } | |
| } | |
| return { q, s }; | |
| } | |
| } | |
| // FAST PATH: GGUF Q4_0 β engine Q4 with NO dequant/requant β a pure relayout. | |
| // GGUF Q4_0 already stores (nibble-8)*d, exactly the engine's convention, so the | |
| // nibble value maps straight across; we only reorder the interleaved (j, j+16) | |
| // nibbles into the engine's sequential packing and widen the f16 scale to f32. | |
| // This is both faster (integer-only) and bit-exact to the GGUF (no requant loss). | |
| export function relayoutQ4(raw, n, k) { | |
| const nb = k / 32; | |
| const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); | |
| const q = new Uint8Array(n * k / 2), s = new Float32Array(n * nb); | |
| let bo = 0, si = 0; | |
| for (let row = 0; row < n; row++) { | |
| const rowBase = row * k; | |
| for (let b = 0; b < nb; b++) { | |
| s[si++] = f16ToF32(dv.getUint16(bo, true)); | |
| const qs = bo + 2, blkBase = rowBase + b * 32; | |
| for (let w = 0; w < 32; w++) { | |
| const nib = w < 16 ? (raw[qs + w] & 0x0f) : (raw[qs + (w - 16)] >> 4); | |
| const g = blkBase + w; | |
| if ((g & 1) === 0) q[g >> 1] |= nib; else q[g >> 1] |= nib << 4; | |
| } | |
| bo += 18; | |
| } | |
| } | |
| return { q, s }; | |
| } | |
| // FAST PATH: GGUF Q8_0 β engine Q8 β copy the i8 quants, widen f16 scale to f32. | |
| export function relayoutQ8(raw, n, k) { | |
| const nb = k / 32; | |
| const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); | |
| const q = new Uint8Array(n * k), s = new Float32Array(n * nb); | |
| let bo = 0, si = 0, qi = 0; | |
| for (let row = 0; row < n; row++) { | |
| for (let b = 0; b < nb; b++) { | |
| s[si++] = f16ToF32(dv.getUint16(bo, true)); | |
| for (let w = 0; w < 32; w++) q[qi++] = raw[bo + 2 + w]; | |
| bo += 34; | |
| } | |
| } | |
| return { q, s }; | |
| } | |
| // ββ GGUF header parser (just enough: tensor directory + data offset) ββ | |
| class Cur { | |
| constructor(buf) { this.b = buf; this.dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); this.p = 0; this.v1 = false; } | |
| need(n) { if (this.p + n > this.b.byteLength) throw new RangeError("short"); } | |
| u8() { this.need(1); return this.b[this.p++]; } | |
| u16() { this.need(2); const v = this.dv.getUint16(this.p, true); this.p += 2; return v; } | |
| u32() { this.need(4); const v = this.dv.getUint32(this.p, true); this.p += 4; return v; } | |
| u64() { this.need(8); const lo = this.dv.getUint32(this.p, true), hi = this.dv.getUint32(this.p + 4, true); this.p += 8; return hi * 4294967296 + lo; } | |
| lenField() { return this.v1 ? this.u32() : this.u64(); } | |
| skipStr() { const n = this.lenField(); this.need(n); this.p += n; } | |
| skipValue(ty) { | |
| switch (ty) { | |
| case 0: case 1: case 7: this.p += 1; break; | |
| case 2: case 3: this.p += 2; break; | |
| case 4: case 5: case 6: this.p += 4; break; | |
| case 10: case 11: case 12: this.p += 8; break; | |
| case 8: this.skipStr(); break; | |
| case 9: { const ety = this.u32(); const cnt = this.lenField(); for (let i = 0; i < cnt; i++) this.skipValue(ety); break; } | |
| default: throw new Error("bad meta type " + ty); | |
| } | |
| } | |
| // Read a SCALAR metadata value (numbers/bool/string); arrays are skipped (return undefined). | |
| readScalar(ty) { | |
| switch (ty) { | |
| case 0: return this.u8(); | |
| case 1: { const v = this.u8(); return v << 24 >> 24; } | |
| case 2: return this.u16(); | |
| case 3: { const v = this.u16(); return v << 16 >> 16; } | |
| case 4: return this.u32(); | |
| case 5: { const v = this.u32(); return v | 0; } | |
| case 6: { this.need(4); const v = this.dv.getFloat32(this.p, true); this.p += 4; return v; } | |
| case 7: return this.u8() !== 0; | |
| case 8: { const n = this.lenField(); const s = new TextDecoder().decode(this.b.subarray(this.p, this.p + n)); this.p += n; return s; } | |
| case 10: return this.u64(); | |
| case 11: return this.u64(); | |
| case 12: { this.need(8); const v = this.dv.getFloat64(this.p, true); this.p += 8; return v; } | |
| case 9: { const ety = this.u32(); const cnt = this.lenField(); for (let i = 0; i < cnt; i++) this.skipValue(ety); return undefined; } | |
| default: throw new Error("bad meta type " + ty); | |
| } | |
| } | |
| } | |
| // Parse a GGUF header buffer β { version, dataOffset, tensors:[{name,dims,ggmlType,offset}] }. | |
| // `buf` must contain at least up to the (aligned) end of the tensor-info table. | |
| export function parseGgufHeader(buf) { | |
| const c = new Cur(buf); | |
| if (c.u32() !== 0x46554747) throw new Error("not GGUF"); | |
| const version = c.u32(); | |
| if (version !== 1 && version !== 2 && version !== 3) throw new Error("GGUF version " + version); | |
| c.v1 = version === 1; | |
| const tensorCount = c.lenField(); | |
| const metaCount = c.lenField(); | |
| let alignment = 32; | |
| const meta = {}; | |
| for (let i = 0; i < metaCount; i++) { | |
| const keyLen = c.lenField(); const key = new TextDecoder().decode(buf.subarray(c.p, c.p + keyLen)); c.p += keyLen; | |
| const ty = c.u32(); | |
| const v = c.readScalar(ty); // arrays β undefined (skipped) | |
| if (v !== undefined) meta[key] = v; | |
| if (key === "general.alignment" && typeof v === "number") alignment = v; | |
| } | |
| const tensors = []; | |
| for (let i = 0; i < tensorCount; i++) { | |
| const nl = c.lenField(); const name = new TextDecoder().decode(buf.subarray(c.p, c.p + nl)); c.p += nl; | |
| const nd = c.u32(); const dims = []; for (let j = 0; j < nd; j++) dims.push(c.lenField()); | |
| const ggmlType = c.u32(); const offset = c.u64(); | |
| tensors.push({ name, dims, ggmlType, offset }); | |
| } | |
| alignment = Math.max(1, alignment); | |
| const dataOffset = Math.ceil(c.p / alignment) * alignment; | |
| return { version, dataOffset, tensors, meta }; | |
| } | |
| // Build the engine manifest (dims + tensor list with N,K,blk) from a parsed GGUF | |
| // header β a pure-JS port of qvac-layer::model_specs + gpu_export_manifest, so | |
| // conversion/streaming needs no wasm. `tensors` is the header's tensor directory. | |
| export function buildManifest(meta, tensors, bits) { | |
| const arch = meta["general.architecture"] || "llama"; | |
| const mu = (k) => { const v = meta[`${arch}.${k}`]; return typeof v === "number" ? Math.round(v) : undefined; }; | |
| const tset = new Set(tensors.map((t) => t.name)); | |
| const tbyname = {}; for (const t of tensors) tbyname[t.name] = t; | |
| const d = mu("embedding_length") || 0; | |
| const n_layers = mu("block_count") || 0; | |
| const n_heads = mu("attention.head_count") || 0; | |
| const n_kv_heads = mu("attention.head_count_kv") || n_heads; | |
| // For MoE, the experts use expert_feed_forward_length (e.g. Qwen3-30B-A3B: 768), | |
| // which differs from the (unused) dense feed_forward_length (6144). OLMoE's two | |
| // values happen to be equal. `ff` everywhere downstream means the EXPERT ff for MoE. | |
| const ff = (mu("expert_feed_forward_length") || mu("feed_forward_length")) || 0; | |
| const hd = mu("attention.key_length") || (n_heads ? Math.floor(d / n_heads) : 0); | |
| const kv_dim = n_kv_heads * hd; | |
| const rope_base = (typeof meta[`${arch}.rope.freq_base`] === "number") ? meta[`${arch}.rope.freq_base`] : 10000; | |
| const attn_bias = tset.has("blk.0.attn_q.bias"); | |
| const qk_norm = tset.has("blk.0.attn_q_norm.weight"); | |
| const qk_norm_dim = qk_norm ? (tbyname["blk.0.attn_q_norm.weight"].dims[0] | 0) : 0; // hd (per-head, Qwen3) or d (full, OLMoE) | |
| const n_experts = mu("expert_count") || 0; // >0 β MoE | |
| const n_used = mu("expert_used_count") || 0; | |
| const moe = n_experts > 0; | |
| const bitnet = /^bitnet/.test(arch); // BitNet b1.58: sub-norms before wo/w_down + ReLUΒ² gated FFN | |
| const tied = !tset.has("output.weight"); | |
| const vocab = d > 0 && tbyname["token_embd.weight"] ? Math.floor(tbyname["token_embd.weight"].dims.reduce((a, b) => a * b, 1) / d) : 0; | |
| const blk = (name, N, K) => ({ name, N, K, blk: true }); | |
| const nrm = (name, K) => ({ name, N: 1, K, blk: false }); | |
| const t = []; | |
| t.push(blk("embed", vocab, d)); | |
| t.push(nrm("final_norm", d)); | |
| t.push(blk("lm_head", vocab, d)); | |
| for (let i = 0; i < n_layers; i++) { | |
| t.push(nrm(`l${i}.attn_norm`, d)); | |
| t.push(blk(`l${i}.wq`, n_heads * hd, d)); | |
| t.push(blk(`l${i}.wk`, kv_dim, d)); | |
| t.push(blk(`l${i}.wv`, kv_dim, d)); | |
| if (attn_bias) { t.push(nrm(`l${i}.bq`, n_heads * hd)); t.push(nrm(`l${i}.bk`, kv_dim)); t.push(nrm(`l${i}.bv`, kv_dim)); } | |
| if (qk_norm) { t.push(nrm(`l${i}.q_norm`, qk_norm_dim)); t.push(nrm(`l${i}.k_norm`, qk_norm_dim)); } | |
| if (bitnet) t.push(nrm(`l${i}.attn_sub_norm`, n_heads * hd)); | |
| t.push(blk(`l${i}.wo`, d, n_heads * hd)); | |
| t.push(nrm(`l${i}.ffn_norm`, d)); | |
| if (bitnet) t.push(nrm(`l${i}.ffn_sub_norm`, ff)); | |
| if (moe) { | |
| t.push(nrm(`l${i}.router`, n_experts * d)); // ffn_gate_inp [n_experts, d] f32 (CPU top-k) | |
| // experts are NOT enumerated here (n_layersΒ·n_expertsΒ·3 is huge); the engine | |
| // generates `l{i}.e{e}.{gate,up,down}` names for the top-k it actually needs. | |
| } else { | |
| t.push(blk(`l${i}.w_gate`, ff, d)); | |
| t.push(blk(`l${i}.w_up`, ff, d)); | |
| t.push(blk(`l${i}.w_down`, d, ff)); | |
| } | |
| } | |
| const out = { d, n_heads, n_kv_heads, ff, vocab, n_layers, hd, bits, rope_base, attn_bias, qk_norm, qk_norm_dim, tied, tensors: t }; | |
| if (moe) out.moe = { n_experts, n_used }; | |
| if (bitnet) { out.sub_norm = true; out.ffn_act = "relu2"; } | |
| return out; | |
| } | |
| // engine tensor name β GGUF tensor name (mirror of model_specs). | |
| export function ggufNameFor(name, hasOutputWeight) { | |
| if (name === "embed") return "token_embd.weight"; | |
| if (name === "final_norm") return "output_norm.weight"; | |
| if (name === "lm_head") return hasOutputWeight ? "output.weight" : "token_embd.weight"; | |
| const m = name.match(/^l(\d+)\.(.+)$/); if (!m) return null; | |
| const i = m[1], r = m[2], p = `blk.${i}.`; | |
| const map = { | |
| "attn_norm": "attn_norm.weight", "ffn_norm": "ffn_norm.weight", | |
| "attn_sub_norm": "attn_sub_norm.weight", "ffn_sub_norm": "ffn_sub_norm.weight", | |
| "wq": "attn_q.weight", "wk": "attn_k.weight", "wv": "attn_v.weight", "wo": "attn_output.weight", | |
| "bq": "attn_q.bias", "bk": "attn_k.bias", "bv": "attn_v.bias", | |
| "q_norm": "attn_q_norm.weight", "k_norm": "attn_k_norm.weight", | |
| "w_gate": "ffn_gate.weight", "w_up": "ffn_up.weight", "w_down": "ffn_down.weight", | |
| }; | |
| return map[r] ? p + map[r] : null; | |
| } | |
| // Read the GGUF header from `url` (HTTP Range), growing the read until the tensor | |
| // table fits. Returns { dataOffset, tensors, headerBytes } (headerBytes = the first | |
| // dataOffset bytes, to hand to wasm qvac_load_gpu for the tokenizer + manifest). | |
| export async function readHeader(url, readRange, initial = 48 * 1024 * 1024) { | |
| let n = initial, parsed = null, buf = null; | |
| for (let tries = 0; tries < 6; tries++) { | |
| buf = await readRange(url, 0, n); | |
| try { parsed = parseGgufHeader(buf); break; } catch (e) { if (e instanceof RangeError || /short/.test(String(e))) { n *= 2; continue; } throw e; } | |
| } | |
| if (!parsed) throw new Error("could not parse GGUF header"); | |
| const headerBytes = buf.length >= parsed.dataOffset ? buf.subarray(0, parsed.dataOffset) : await readRange(url, 0, parsed.dataOffset); | |
| return { dataOffset: parsed.dataOffset, tensors: parsed.tensors, headerBytes }; | |
| } | |
| // Build the per-tensor fetcher the GPU engine consumes. `manifest` is the wasm | |
| // manifest (dims + tensors with N,K,blk). Returns fetchTensor(name) β Uint8Array, | |
| // byte-identical to what qvac_gpu_tensor would return β but sourced from disk. | |
| export function makeDiskFetcher({ url, readRange, dataOffset, tensors, manifest, bits }) { | |
| const tdir = {}; for (const t of tensors) tdir[t.name] = t; | |
| const hasOut = !!tdir["output.weight"]; | |
| const mByName = {}; for (const t of manifest.tensors) mByName[t.name] = t; | |
| const ROW_CHUNK = 8192; | |
| const ffM = manifest.ff, dM = manifest.d; | |
| // Resolve an engine tensor name β { info, N, K, blk, eltOffset }. Handles MoE | |
| // expert slices `l{i}.e{e}.{gate,up,down}` (the e-th slab of the 3-D ffn_*_exps | |
| // tensor) and the router `l{i}.router` (ffn_gate_inp, f32) in addition to the | |
| // plain tensors in the manifest. | |
| const resolve = (name) => { | |
| let m = name.match(/^l(\d+)\.e(\d+)\.(gate|up|down)$/); | |
| if (m) { | |
| const i = m[1], e = +m[2], role = m[3]; | |
| const info = tdir[`blk.${i}.ffn_${role}_exps.weight`]; if (!info) return null; | |
| const N = role === "down" ? dM : ffM, K = role === "down" ? ffM : dM; | |
| return { info, N, K, blk: true, eltOffset: e * N * K }; | |
| } | |
| m = name.match(/^l(\d+)\.router$/); | |
| if (m) { const info = tdir[`blk.${m[1]}.ffn_gate_inp.weight`]; return info ? { info, N: 1, K: (manifest.moe.n_experts * dM), blk: false, eltOffset: 0 } : null; } | |
| const spec = mByName[name]; const gname = ggufNameFor(name, hasOut); const info = gname && tdir[gname]; | |
| return (spec && info) ? { info, N: spec.N, K: spec.K, blk: spec.blk, eltOffset: 0 } : null; | |
| }; | |
| return async function fetchTensor(name) { | |
| const r = resolve(name); | |
| if (!r) return new Uint8Array(0); | |
| const { info, N, K, blk, eltOffset } = r; | |
| const [bElems, bBytes] = blockShape(info.ggmlType); | |
| const tBase = dataOffset + info.offset + (eltOffset / bElems) * bBytes; // slab offset for experts | |
| if (!blk) { // norm / bias / router β [f32] | |
| const elems = K; | |
| const raw = await readRange(url, tBase, typeByteLen(info.ggmlType, elems)); | |
| const f = dequantizeRaw(info.ggmlType, raw, elems); | |
| return new Uint8Array(f.buffer, f.byteOffset, f.byteLength); | |
| } | |
| // block weight β [all q bytes][all f32 scales], chunked by rows so a 152k-vocab | |
| // tensor never materialises whole as f32 (mirror of quant_tensor_chunked). | |
| const [blkElems, blkBytes] = blockShape(info.ggmlType); | |
| const qBytesTotal = bits === 4 ? (N * K) / 2 : N * K; | |
| const scaleCount = N * (K / 32); | |
| const out = new Uint8Array(qBytesTotal + scaleCount * 4); | |
| const scales = new Float32Array(out.buffer, qBytesTotal, scaleCount); | |
| // Fast relayout when the source quant matches the engine's width (the common | |
| // case: a Q4_0 model β 4-bit engine). No f32 ever materialises β also lets the | |
| // big embed/lm_head be done in one pass. | |
| const fast = (info.ggmlType === GGML.Q4_0 && bits === 4) || (info.ggmlType === GGML.Q8_0 && bits === 8); | |
| if (fast) { | |
| const raw = await readRange(url, tBase, typeByteLen(info.ggmlType, N * K)); | |
| const { q, s } = info.ggmlType === GGML.Q4_0 ? relayoutQ4(raw, N, K) : relayoutQ8(raw, N, K); | |
| out.set(q, 0); scales.set(s, 0); | |
| return out; | |
| } | |
| let qPos = 0, sPos = 0; | |
| for (let r = 0; r < N; r += ROW_CHUNK) { | |
| const nr = Math.min(ROW_CHUNK, N - r); | |
| const startElem = r * K, countElem = nr * K; | |
| const byteStart = (startElem / blkElems) * blkBytes, byteLen = (countElem / blkElems) * blkBytes; | |
| const raw = await readRange(url, tBase + byteStart, byteLen); | |
| const f = dequantizeRaw(info.ggmlType, raw, countElem); | |
| const { q, s } = quantBlocks(f, nr, K, bits); | |
| out.set(q, qPos); qPos += q.length; | |
| scales.set(s, sPos); sPos += s.length; | |
| } | |
| return out; | |
| }; | |
| } | |
| // HTTP Range reader against a same-origin URL. | |
| export function rangeReader() { | |
| return async (url, start, len) => { | |
| const r = await fetch(url, { headers: { Range: `bytes=${start}-${start + len - 1}` } }); | |
| if (!r.ok && r.status !== 206) throw new Error(`range ${start}+${len}: HTTP ${r.status}`); | |
| return new Uint8Array(await r.arrayBuffer()); | |
| }; | |
| } | |