@tryhamster/gerbil 1.11.1 → 1.11.3

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.
Files changed (46) hide show
  1. package/dist/cli.mjs +7 -7
  2. package/dist/cli.mjs.map +1 -1
  3. package/dist/frameworks/express.mjs +1 -1
  4. package/dist/frameworks/fastify.mjs +1 -1
  5. package/dist/frameworks/hono.mjs +1 -1
  6. package/dist/frameworks/next.d.mts +2 -2
  7. package/dist/frameworks/next.mjs +1 -1
  8. package/dist/frameworks/trpc.mjs +1 -1
  9. package/dist/gerbil-CTefAwKp.mjs +4 -0
  10. package/dist/{gerbil-BpYemKEH.mjs → gerbil-CYVmU8sQ.mjs} +2 -2
  11. package/dist/{gerbil-BpYemKEH.mjs.map → gerbil-CYVmU8sQ.mjs.map} +1 -1
  12. package/dist/{gerbil-CaAGG2VP.d.mts → gerbil-Cu7YwBBz.d.mts} +2 -2
  13. package/dist/{gerbil-CaAGG2VP.d.mts.map → gerbil-Cu7YwBBz.d.mts.map} +1 -1
  14. package/dist/gpu/hooks.d.mts +1 -1
  15. package/dist/gpu/index.d.mts +2 -2
  16. package/dist/gpu/index.mjs +3 -3
  17. package/dist/{gpu-BCfd_K38.mjs → gpu-EyIH7Qfc.mjs} +20 -16
  18. package/dist/{gpu-BCfd_K38.mjs.map → gpu-EyIH7Qfc.mjs.map} +1 -1
  19. package/dist/index-B3tjyDJI.d.mts.map +1 -1
  20. package/dist/{index-DqJMofyU.d.mts → index-DeKYzJgJ.d.mts} +46 -2
  21. package/dist/index-DeKYzJgJ.d.mts.map +1 -0
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +4 -4
  24. package/dist/integrations/ai-sdk.mjs +1 -1
  25. package/dist/integrations/langchain.mjs +1 -1
  26. package/dist/integrations/llamaindex.mjs +1 -1
  27. package/dist/integrations/mcp.d.mts +2 -2
  28. package/dist/integrations/mcp.mjs +4 -4
  29. package/dist/{mcp-DSpjqWZC.mjs → mcp-CAsD7eCj.mjs} +3 -3
  30. package/dist/{mcp-DSpjqWZC.mjs.map → mcp-CAsD7eCj.mjs.map} +1 -1
  31. package/dist/{moonshine-stt-CgDXoLFd.mjs → moonshine-stt-BXoZaHJE.mjs} +651 -550
  32. package/dist/moonshine-stt-BXoZaHJE.mjs.map +1 -0
  33. package/dist/moonshine-stt-DZVnKgPO.mjs +4 -0
  34. package/dist/{one-liner-BgNDxJSJ.mjs → one-liner-ppgw4jHH.mjs} +2 -2
  35. package/dist/{one-liner-BgNDxJSJ.mjs.map → one-liner-ppgw4jHH.mjs.map} +1 -1
  36. package/dist/{repl-DSK2hDzu.mjs → repl-C0Ew7_Z-.mjs} +3 -3
  37. package/dist/skills/index.d.mts +2 -2
  38. package/dist/skills/index.mjs +3 -3
  39. package/dist/{skills-Cd75ZGew.mjs → skills-CQa1Gshd.mjs} +2 -2
  40. package/dist/{skills-Cd75ZGew.mjs.map → skills-CQa1Gshd.mjs.map} +1 -1
  41. package/dist/tune/index.mjs +1 -1
  42. package/package.json +1 -1
  43. package/dist/gerbil-BY5EW-Jk.mjs +0 -4
  44. package/dist/index-DqJMofyU.d.mts.map +0 -1
  45. package/dist/moonshine-stt-CcVt4Vdd.mjs +0 -4
  46. package/dist/moonshine-stt-CgDXoLFd.mjs.map +0 -1
@@ -8842,6 +8842,381 @@ const KERNEL_REGISTRY = {
8842
8842
  GroupNorm: groupnormSpec
8843
8843
  };
8844
8844
 
8845
+ //#endregion
8846
+ //#region src/gpu/safetensors.ts
8847
+ /**
8848
+ * Parse safetensors header from an ArrayBuffer.
8849
+ *
8850
+ * Can be called with just the header bytes (for streaming — parse header first,
8851
+ * then fetch tensor data by offset) or with the entire file.
8852
+ */
8853
+ function parseSafetensorsHeader(buffer) {
8854
+ if (buffer.byteLength < 8) throw new Error(`Buffer too small to contain safetensors header length (need 8 bytes, got ${buffer.byteLength}).`);
8855
+ const view = new DataView(buffer);
8856
+ const headerLength = Number(view.getBigUint64(0, true));
8857
+ if (headerLength > buffer.byteLength - 8) throw new Error(`Safetensors header length (${headerLength}) exceeds buffer size (${buffer.byteLength - 8}). Pass at least the first ${headerLength + 8} bytes.`);
8858
+ const headerBytes = new Uint8Array(buffer, 8, headerLength);
8859
+ const headerStr = new TextDecoder().decode(headerBytes);
8860
+ const header = JSON.parse(headerStr);
8861
+ const dataStart = 8 + headerLength;
8862
+ const entries = [];
8863
+ let metadata = null;
8864
+ for (const [name, info] of Object.entries(header)) {
8865
+ if (name === "__metadata__") {
8866
+ metadata = info;
8867
+ continue;
8868
+ }
8869
+ const { dtype, shape, data_offsets } = info;
8870
+ entries.push({
8871
+ name,
8872
+ dtype,
8873
+ shape,
8874
+ dataOffset: data_offsets[0],
8875
+ dataLength: data_offsets[1] - data_offsets[0]
8876
+ });
8877
+ }
8878
+ entries.sort((a, b) => a.dataOffset - b.dataOffset);
8879
+ return {
8880
+ headerLength,
8881
+ dataStart,
8882
+ entries,
8883
+ metadata
8884
+ };
8885
+ }
8886
+ /** Byte alignment required for each dtype. */
8887
+ function dtypeAlignment(dtype) {
8888
+ switch (dtype) {
8889
+ case "F64":
8890
+ case "I64":
8891
+ case "U64": return 8;
8892
+ case "F32":
8893
+ case "I32":
8894
+ case "U32": return 4;
8895
+ case "F16":
8896
+ case "BF16":
8897
+ case "I16":
8898
+ case "U16": return 2;
8899
+ case "I8":
8900
+ case "U8":
8901
+ case "BOOL": return 1;
8902
+ default: return 1;
8903
+ }
8904
+ }
8905
+ /**
8906
+ * Create a typed array for the given dtype.
8907
+ *
8908
+ * If `offset` is aligned to the element size, returns a zero-copy view
8909
+ * into `buffer`. Otherwise, copies the relevant slice into a new
8910
+ * properly-aligned ArrayBuffer.
8911
+ */
8912
+ function makeTypedView(buffer, offset, byteLength, dtype) {
8913
+ const aligned = offset % dtypeAlignment(dtype) === 0;
8914
+ const src = aligned ? buffer : buffer.slice(offset, offset + byteLength);
8915
+ const base = aligned ? offset : 0;
8916
+ switch (dtype) {
8917
+ case "F32": return new Float32Array(src, base, byteLength / 4);
8918
+ case "F16": return new Uint16Array(src, base, byteLength / 2);
8919
+ case "BF16": return new Uint16Array(src, base, byteLength / 2);
8920
+ case "I32": return new Int32Array(src, base, byteLength / 4);
8921
+ case "U32": return new Uint32Array(src, base, byteLength / 4);
8922
+ case "I8": return new Int8Array(src, base, byteLength);
8923
+ case "U8": return new Uint8Array(src, base, byteLength);
8924
+ case "I16": return new Int16Array(src, base, byteLength / 2);
8925
+ case "U16": return new Uint16Array(src, base, byteLength / 2);
8926
+ case "F64": return new Float64Array(src, base, byteLength / 8);
8927
+ case "I64": return new BigInt64Array(src, base, byteLength / 8);
8928
+ case "U64": return new BigUint64Array(src, base, byteLength / 8);
8929
+ case "BOOL": return new Uint8Array(src, base, byteLength);
8930
+ default: throw new Error(`Unsupported safetensors dtype: ${dtype}`);
8931
+ }
8932
+ }
8933
+ /**
8934
+ * Get a typed array view for a tensor entry from a complete safetensors buffer.
8935
+ * Zero-copy when the byte offset is properly aligned; copies otherwise.
8936
+ */
8937
+ function getTensorData(buffer, file, entry) {
8938
+ return makeTypedView(buffer, file.dataStart + entry.dataOffset, entry.dataLength, entry.dtype);
8939
+ }
8940
+ /**
8941
+ * Convert BF16 (bfloat16) data to F32.
8942
+ * BF16 is the upper 16 bits of an IEEE 754 float32, so conversion is a simple left-shift.
8943
+ */
8944
+ function bf16ToF32(bf16Bytes) {
8945
+ const bf16 = new Uint16Array(bf16Bytes.buffer, bf16Bytes.byteOffset, bf16Bytes.byteLength / 2);
8946
+ const f32 = new Float32Array(bf16.length);
8947
+ const u32 = new Uint32Array(f32.buffer);
8948
+ for (let i = 0; i < bf16.length; i++) u32[i] = bf16[i] << 16;
8949
+ return f32;
8950
+ }
8951
+ /**
8952
+ * Convert F16 (IEEE 754 half-precision) data to F32.
8953
+ */
8954
+ function f16ToF32(f16View) {
8955
+ const u16 = f16View instanceof Uint16Array ? f16View : new Uint16Array(f16View.buffer, f16View.byteOffset, f16View.byteLength / 2);
8956
+ const f32 = new Float32Array(u16.length);
8957
+ for (let i = 0; i < u16.length; i++) {
8958
+ const h = u16[i];
8959
+ const sign = h >> 15 & 1;
8960
+ const exp = h >> 10 & 31;
8961
+ const frac = h & 1023;
8962
+ if (exp === 0) f32[i] = (sign ? -1 : 1) * 2 ** -14 * (frac / 1024);
8963
+ else if (exp === 31) f32[i] = frac === 0 ? sign ? -Infinity : Infinity : NaN;
8964
+ else f32[i] = (sign ? -1 : 1) * 2 ** (exp - 15) * (1 + frac / 1024);
8965
+ }
8966
+ return f32;
8967
+ }
8968
+
8969
+ //#endregion
8970
+ //#region src/gpu/lora.ts
8971
+ function resolveAdapterBase(spec, revision) {
8972
+ if (spec.startsWith("file:")) return {
8973
+ base: spec.slice(5),
8974
+ local: true
8975
+ };
8976
+ if (spec.startsWith("http://") || spec.startsWith("https://")) return {
8977
+ base: spec,
8978
+ local: false
8979
+ };
8980
+ return {
8981
+ base: `https://huggingface.co/${spec.startsWith("hf:") ? spec.slice(3) : spec}/resolve/${revision}`,
8982
+ local: false
8983
+ };
8984
+ }
8985
+ async function readLocalFile(dir, name) {
8986
+ try {
8987
+ const fs = await import("node:fs");
8988
+ const p = (await import("node:path")).join(dir, name);
8989
+ if (!fs.existsSync(p)) return null;
8990
+ const buf = fs.readFileSync(p);
8991
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
8992
+ } catch {
8993
+ return null;
8994
+ }
8995
+ }
8996
+ async function fetchFile(base, local, name, hfToken) {
8997
+ if (local) return readLocalFile(base, name);
8998
+ const headers = {};
8999
+ if (hfToken) headers.Authorization = `Bearer ${hfToken}`;
9000
+ const res = await fetch(`${base}/${name}`, { headers });
9001
+ if (!res.ok) return null;
9002
+ return res.arrayBuffer();
9003
+ }
9004
+ /** Convert a safetensors tensor slice to f32 (adapters are tiny; eager is fine). */
9005
+ function sliceToF32(buf, dataStart, entry) {
9006
+ const start = dataStart + entry.dataOffset;
9007
+ switch (entry.dtype) {
9008
+ case "F32": return new Float32Array(buf.slice(start, start + entry.dataLength));
9009
+ case "BF16": return bf16ToF32(new Uint8Array(buf, start, entry.dataLength));
9010
+ case "F16": return f16ToF32(new Uint8Array(buf, start, entry.dataLength));
9011
+ default: throw new Error(`LoRA: unsupported adapter tensor dtype ${entry.dtype}`);
9012
+ }
9013
+ }
9014
+ /**
9015
+ * Download + parse an adapter (`adapter_config.json` + `adapter_model.safetensors`).
9016
+ * Returns null when the source has no adapter (so callers can no-op cleanly).
9017
+ */
9018
+ async function fetchAdapter(spec, opts = {}) {
9019
+ const { base, local } = resolveAdapterBase(spec, opts.revision ?? "main");
9020
+ opts.onProgress?.(`Loading adapter ${spec}…`);
9021
+ const configBuf = await fetchFile(base, local, "adapter_config.json", opts.hfToken);
9022
+ if (!configBuf) throw new Error(`LoRA: no adapter_config.json at ${spec}. A flavor needs a PEFT adapter (adapter_config.json + adapter_model.safetensors).`);
9023
+ const config = JSON.parse(new TextDecoder().decode(configBuf));
9024
+ const weightsBuf = await fetchFile(base, local, "adapter_model.safetensors", opts.hfToken);
9025
+ if (!weightsBuf) throw new Error(`LoRA: no adapter_model.safetensors at ${spec} (only .safetensors adapters are supported, not legacy .bin).`);
9026
+ const file = parseSafetensorsHeader(weightsBuf);
9027
+ const tensors = /* @__PURE__ */ new Map();
9028
+ for (const entry of file.entries) {
9029
+ if (!entry.name.includes("lora_A") && !entry.name.includes("lora_B")) continue;
9030
+ tensors.set(entry.name, {
9031
+ data: sliceToF32(weightsBuf, file.dataStart, entry),
9032
+ shape: entry.shape
9033
+ });
9034
+ }
9035
+ if (tensors.size === 0) throw new Error(`LoRA: adapter at ${spec} has no lora_A/lora_B tensors.`);
9036
+ return {
9037
+ config,
9038
+ tensors,
9039
+ label: spec
9040
+ };
9041
+ }
9042
+ /**
9043
+ * Strip a PEFT tensor key down to its module path and canonicalize it to the
9044
+ * base weight key the engine uses.
9045
+ *
9046
+ * PEFT names a target like
9047
+ * base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight
9048
+ * We drop the PEFT wrapper prefix + the lora_A/B suffix to get the module path
9049
+ * (…layers.0.self_attn.q_proj), append ".weight", and run the SAME key mapper
9050
+ * the base load used — so Qwen ("model." strip), Gemma, and LFM2 (out_proj→
9051
+ * o_proj) all canonicalize identically to their base tensors.
9052
+ */
9053
+ function canonicalWeightKey(peftKey, keyMapper) {
9054
+ const marker = peftKey.includes(".lora_A") ? ".lora_A" : ".lora_B";
9055
+ let stem = peftKey.slice(0, peftKey.indexOf(marker));
9056
+ stem = stem.replace(/^base_model\.model\./, "").replace(/^base_model\./, "");
9057
+ return keyMapper(`${stem.startsWith("model.") ? stem : `model.${stem}`}.weight`);
9058
+ }
9059
+ /** Longest matching suffix in a {moduleName: value} pattern map. */
9060
+ function patternLookup(patterns, peftStem) {
9061
+ if (!patterns) return void 0;
9062
+ let best;
9063
+ let bestLen = -1;
9064
+ for (const [name, val] of Object.entries(patterns)) if (peftStem.includes(name) && name.length > bestLen) {
9065
+ best = val;
9066
+ bestLen = name.length;
9067
+ }
9068
+ return best;
9069
+ }
9070
+ /**
9071
+ * Pair lora_A/lora_B tensors and resolve each to a canonical `LoRADelta`.
9072
+ * `keyMapper` must be the same mapper the base model load used.
9073
+ */
9074
+ function buildLoRADeltas(adapter, keyMapper) {
9075
+ const { config, tensors } = adapter;
9076
+ const stems = /* @__PURE__ */ new Set();
9077
+ for (const name of tensors.keys()) {
9078
+ const marker = name.includes(".lora_A") ? ".lora_A" : ".lora_B";
9079
+ stems.add(name.slice(0, name.indexOf(marker)));
9080
+ }
9081
+ const deltas = [];
9082
+ for (const stem of stems) {
9083
+ const A = tensors.get(`${stem}.lora_A.weight`) ?? tensors.get(`${stem}.lora_A`);
9084
+ const B = tensors.get(`${stem}.lora_B.weight`) ?? tensors.get(`${stem}.lora_B`);
9085
+ if (!A || !B) continue;
9086
+ const key = canonicalWeightKey(`${stem}.lora_A.weight`, keyMapper);
9087
+ if (!key) continue;
9088
+ const r = A.shape[0];
9089
+ const inFeatures = A.shape[1];
9090
+ const outFeatures = B.shape[0];
9091
+ if (B.shape[1] !== r) {
9092
+ console.warn(`[gerbil] LoRA: rank mismatch on ${stem} (A r=${r}, B r=${B.shape[1]}); skipping.`);
9093
+ continue;
9094
+ }
9095
+ const alpha = patternLookup(config.alpha_pattern, stem) ?? config.lora_alpha;
9096
+ const effR = patternLookup(config.rank_pattern, stem) ?? r;
9097
+ const scaling = config.use_rslora ? alpha / Math.sqrt(effR) : alpha / effR;
9098
+ deltas.push({
9099
+ key,
9100
+ A: A.data,
9101
+ B: B.data,
9102
+ r,
9103
+ inFeatures,
9104
+ outFeatures,
9105
+ scaling,
9106
+ fanInFanOut: config.fan_in_fan_out === true
9107
+ });
9108
+ }
9109
+ return deltas;
9110
+ }
9111
+ /**
9112
+ * Split a fused query+gate LoRA delta into two deltas (query, gate).
9113
+ *
9114
+ * Qwen3.5-style gated attention fuses the query projection and the attention
9115
+ * output-gate into ONE checkpoint tensor: `q_proj.weight` is
9116
+ * `[2·num_heads·head_dim, hidden]`, per-head interleaved — for each head the
9117
+ * first `head_dim` rows are query, the next `head_dim` rows are the gate. The
9118
+ * model loader deinterleaves this into two runtime linears (`q_proj` and
9119
+ * `attn_gate`), each `[num_heads·head_dim, hidden]`.
9120
+ *
9121
+ * A PEFT adapter trained on the FUSED checkpoint carries a single `q_proj`
9122
+ * `lora_B` of `[2·num_heads·head_dim, r]`, whose out-dim is exactly 2× either
9123
+ * split linear — so it matches neither at runtime. Because the base weight is
9124
+ * deinterleaved by a pure per-head row gather, the same gather applied to
9125
+ * `lora_B`'s rows yields the query and gate factors; `lora_A` (the input side)
9126
+ * is shared unchanged. This is mathematically identical to merging the fused
9127
+ * delta before the loader's split.
9128
+ */
9129
+ function splitFusedQGateDelta(d, qKey, gateKey, headDim) {
9130
+ const half = d.outFeatures / 2;
9131
+ const numHeads = half / headDim;
9132
+ const { r } = d;
9133
+ const rowsPerHead = headDim * r;
9134
+ const headBlock = 2 * rowsPerHead;
9135
+ const Bq = new Float32Array(half * r);
9136
+ const Bg = new Float32Array(half * r);
9137
+ for (let h = 0; h < numHeads; h++) {
9138
+ const srcQ = h * headBlock;
9139
+ const srcG = srcQ + rowsPerHead;
9140
+ const dst = h * rowsPerHead;
9141
+ Bq.set(d.B.subarray(srcQ, srcQ + rowsPerHead), dst);
9142
+ Bg.set(d.B.subarray(srcG, srcG + rowsPerHead), dst);
9143
+ }
9144
+ const shared = {
9145
+ A: d.A,
9146
+ r: d.r,
9147
+ inFeatures: d.inFeatures,
9148
+ outFeatures: half,
9149
+ scaling: d.scaling,
9150
+ fanInFanOut: d.fanInFanOut
9151
+ };
9152
+ return [{
9153
+ ...shared,
9154
+ key: qKey,
9155
+ B: Bq
9156
+ }, {
9157
+ ...shared,
9158
+ key: gateKey,
9159
+ B: Bg
9160
+ }];
9161
+ }
9162
+ /**
9163
+ * Fold every resolved delta into the store's base weights in place
9164
+ * (W += scaling · B·A). Runs after weights are loaded and before architecture
9165
+ * transforms/quantization. Returns the count of tensors merged (for logging).
9166
+ *
9167
+ * Skips — loudly — any target whose base tensor is missing, shape-mismatched, or
9168
+ * not a float pack (a pre-quantized MLX/GPTQ base can't be merged this way).
9169
+ */
9170
+ async function applyLoRAToStore(store, deltas, onProgress) {
9171
+ let merged = 0;
9172
+ let skipped = 0;
9173
+ for (const d of deltas) {
9174
+ const base = await store.get(d.key);
9175
+ if (!base) {
9176
+ skipped++;
9177
+ continue;
9178
+ }
9179
+ if (!(base.data instanceof Float32Array)) {
9180
+ console.warn(`[gerbil] LoRA: base tensor ${d.key} is not float (pre-quantized base?); skipping merge.`);
9181
+ skipped++;
9182
+ continue;
9183
+ }
9184
+ const [outF, inF] = base.shape;
9185
+ const rows = d.fanInFanOut ? inF : outF;
9186
+ const cols = d.fanInFanOut ? outF : inF;
9187
+ if (rows !== d.outFeatures || cols !== d.inFeatures) {
9188
+ console.warn(`[gerbil] LoRA: shape mismatch on ${d.key} (base ${base.shape}, delta ${d.outFeatures}×${d.inFeatures}); skipping.`);
9189
+ skipped++;
9190
+ continue;
9191
+ }
9192
+ const W = new Float32Array(base.data);
9193
+ const { A, B, r, scaling, fanInFanOut } = d;
9194
+ for (let o = 0; o < d.outFeatures; o++) {
9195
+ const bRow = o * r;
9196
+ for (let k = 0; k < r; k++) {
9197
+ const bScaled = scaling * B[bRow + k];
9198
+ if (bScaled === 0) continue;
9199
+ const aRow = k * d.inFeatures;
9200
+ if (fanInFanOut) for (let i = 0; i < d.inFeatures; i++) W[i * outF + o] += bScaled * A[aRow + i];
9201
+ else {
9202
+ const wRow = o * inF;
9203
+ for (let i = 0; i < d.inFeatures; i++) W[wRow + i] += bScaled * A[aRow + i];
9204
+ }
9205
+ }
9206
+ }
9207
+ await store.set(d.key, {
9208
+ data: W,
9209
+ shape: base.shape
9210
+ });
9211
+ merged++;
9212
+ }
9213
+ onProgress?.(`Adapter merged into ${merged} tensors${skipped ? ` (${skipped} skipped)` : ""}.`);
9214
+ return {
9215
+ merged,
9216
+ skipped
9217
+ };
9218
+ }
9219
+
8845
9220
  //#endregion
8846
9221
  //#region src/gpu/executor.ts
8847
9222
  const MAP_MODE_READ$1 = 1;
@@ -8951,6 +9326,17 @@ var Executor = class Executor {
8951
9326
  this.pleSource = src;
8952
9327
  }
8953
9328
  /**
9329
+ * True when this model streams a CPU-resident Per-Layer-Embedding table
9330
+ * (Gemma 4): each decode step must gather+upload the PLE rows for the ACTUAL
9331
+ * input token id on the CPU before the forward runs. The GPU-chained pipelined
9332
+ * decode path and forwardArgmax both bypass CPU token knowledge, so they leave
9333
+ * `ple_embed_out` stale and must NOT be used for these models — only the
9334
+ * per-step forward() path (which calls streamPleRows) is correct. See generate().
9335
+ */
9336
+ hasPleSource() {
9337
+ return this.pleSource !== null;
9338
+ }
9339
+ /**
8954
9340
  * Materialize a cache-backed PLE table into heap exactly once. Deferred to the
8955
9341
  * first forward (after GPU upload) so the ~1.17 GB table is not co-resident
8956
9342
  * with the upload's transient allocations during the load-time memory peak.
@@ -9167,23 +9553,62 @@ var Executor = class Executor {
9167
9553
  * the base weight buffers and bind groups are never touched or re-uploaded.
9168
9554
  * Pass an empty list (or call {@link clearRuntimeLoRA}) to drop the overlay.
9169
9555
  */
9556
+ /** Find the base linear (MatMulInt4 / MatMul) node whose weight is `key`. */
9557
+ findBaseLinear(key) {
9558
+ return this.graph.nodes.find((n) => {
9559
+ if (n.opType !== "MatMulInt4" && n.opType !== "MatMul") return false;
9560
+ const w = n.inputs[1];
9561
+ if (!w) return false;
9562
+ return (w.endsWith(".q") ? w.slice(0, -2) : w) === key;
9563
+ });
9564
+ }
9565
+ /**
9566
+ * Expand any fused query+gate delta into separate `q_proj` + `attn_gate`
9567
+ * deltas so it can splice onto a deinterleaved runtime graph.
9568
+ *
9569
+ * Qwen3.5-style gated attention ships `q_proj.weight` fused as
9570
+ * `[2·num_heads·head_dim, hidden]` (per-head: query rows then gate rows); the
9571
+ * model loader splits it into two runtime linears (`q_proj`, `attn_gate`). An
9572
+ * adapter trained on the fused checkpoint carries a single `q_proj` delta whose
9573
+ * out-dim is 2× either split node, so it matches neither. When BOTH split
9574
+ * linears exist and the delta is exactly their combined width, split it via the
9575
+ * same per-head row deinterleave the loader uses; otherwise the delta passes
9576
+ * through untouched (e.g. a base that never split the projection).
9577
+ */
9578
+ expandFusedQGateDeltas(deltas) {
9579
+ const out = [];
9580
+ for (const d of deltas) {
9581
+ const m = /^(layers\.\d+\.self_attn)\.q_proj\.weight$/.exec(d.key);
9582
+ const gateKey = m ? `${m[1]}.attn_gate.weight` : null;
9583
+ const qNode = m ? this.findBaseLinear(d.key) : void 0;
9584
+ const gateNode = gateKey ? this.findBaseLinear(gateKey) : void 0;
9585
+ if (!(m && gateKey && qNode && gateNode)) {
9586
+ out.push(d);
9587
+ continue;
9588
+ }
9589
+ const nQ = qNode.attributes.N;
9590
+ const nGate = gateNode.attributes.N;
9591
+ const headDim = this.graph.config.head_dim;
9592
+ if (nQ !== nGate || d.outFeatures !== nQ + nGate || !headDim || nQ % headDim !== 0) {
9593
+ out.push(d);
9594
+ continue;
9595
+ }
9596
+ out.push(...splitFusedQGateDelta(d, d.key, gateKey, headDim));
9597
+ }
9598
+ return out;
9599
+ }
9170
9600
  applyRuntimeLoRA(deltas) {
9171
9601
  this.clearRuntimeLoRA();
9172
9602
  const overlay = /* @__PURE__ */ new Map();
9173
9603
  let applied = 0;
9174
9604
  let skipped = 0;
9175
- for (const d of deltas) {
9605
+ for (const d of this.expandFusedQGateDeltas(deltas)) {
9176
9606
  if (d.fanInFanOut) {
9177
9607
  console.warn(`[executor] runtime LoRA: fan_in_fan_out target ${d.key} unsupported; skipping.`);
9178
9608
  skipped++;
9179
9609
  continue;
9180
9610
  }
9181
- const node = this.graph.nodes.find((n) => {
9182
- if (n.opType !== "MatMulInt4" && n.opType !== "MatMul") return false;
9183
- const w = n.inputs[1];
9184
- if (!w) return false;
9185
- return (w.endsWith(".q") ? w.slice(0, -2) : w) === d.key;
9186
- });
9611
+ const node = this.findBaseLinear(d.key);
9187
9612
  if (!node) {
9188
9613
  skipped++;
9189
9614
  continue;
@@ -11043,586 +11468,262 @@ var Executor = class Executor {
11043
11468
  }
11044
11469
  return entries;
11045
11470
  }
11046
- /** Lazily allocate a scratch storage buffer at least `minBytes` large. */
11047
- getBindingScratchBuffer(minBytes) {
11048
- if (!this.bindingScratchBuffer || this.bindingScratchBuffer.size < minBytes) this.bindingScratchBuffer = createStorageBuffer(this.ctx, "binding_scratch", Math.max(minBytes, 256));
11049
- return this.bindingScratchBuffer;
11050
- }
11051
- };
11052
-
11053
- //#endregion
11054
- //#region src/gpu/gptq-adapter.ts
11055
- /**
11056
- * Repack GPTQ tensors into Gerbil's flat INT4 format.
11057
- *
11058
- * This is pure data reshuffling — no quantization math. The 4-bit values,
11059
- * scales, and zero points are preserved exactly.
11060
- */
11061
- function repackGPTQ(gptq) {
11062
- const { qweight, scales: gptqScales, qzeros, K, N, groupSize } = gptq;
11063
- const totalElements = N * K;
11064
- const groupsPerCol = Math.ceil(K / groupSize);
11065
- const totalGroups = N * groupsPerCol;
11066
- const packed = new Uint32Array(Math.ceil(totalElements / 8));
11067
- const scales = new Float32Array(totalGroups);
11068
- const zeros = new Float32Array(totalGroups);
11069
- for (let k = 0; k < K; k++) {
11070
- const gptqRow = k >>> 3;
11071
- const gptqNibblePos = k & 7;
11072
- const gptqRowBase = gptqRow * N;
11073
- for (let col = 0; col < N; col++) {
11074
- const nibble = qweight[gptqRowBase + col] >>> gptqNibblePos * 4 & 15;
11075
- const flatIdx = col * K + k;
11076
- const packedIdx = flatIdx >>> 3;
11077
- const nibblePos = flatIdx & 7;
11078
- packed[packedIdx] |= nibble << nibblePos * 4;
11079
- }
11080
- }
11081
- for (let g = 0; g < groupsPerCol; g++) {
11082
- const srcRow = g * N;
11083
- for (let col = 0; col < N; col++) scales[col * groupsPerCol + g] = gptqScales[srcRow + col];
11084
- }
11085
- const zerosNDiv8 = N >>> 3;
11086
- for (let g = 0; g < groupsPerCol; g++) {
11087
- const srcRow = g * zerosNDiv8;
11088
- for (let col = 0; col < N; col++) {
11089
- const zeroVal = qzeros[srcRow + (col >>> 3)] >>> (col & 7) * 4 & 15;
11090
- zeros[col * groupsPerCol + g] = zeroVal + 1;
11091
- }
11092
- }
11093
- return {
11094
- packed,
11095
- scales,
11096
- zeros
11097
- };
11098
- }
11099
-
11100
- //#endregion
11101
- //#region src/gpu/idb-cache.ts
11102
- /**
11103
- * IndexedDB-backed blob cache for model weights.
11104
- *
11105
- * Why IndexedDB and not the Cache API / OPFS we hand-rolled before:
11106
- * - The Cache API ignores URL fragments in `match()`, which silently collapsed
11107
- * our per-tensor `${url}#t:start-len` keys onto one entry → wrong bytes → the
11108
- * `RRRR` garbage and `wt.data` crashes. IDB keys on the exact string.
11109
- * - OPFS via Worker (`createSyncAccessHandle`) is finicky on iOS Safari (OOM /
11110
- * transient failures) and added a lot of moving parts.
11111
- * IndexedDB is the boring, proven large-binary store every browser supports. It
11112
- * keys on arbitrary strings (no fragment footgun), stores ArrayBuffers directly,
11113
- * and is durable (subject to the same per-origin quota as Cache/OPFS — install to
11114
- * Home Screen lifts that ceiling; IDB doesn't change it, it just makes caching
11115
- * actually CORRECT).
11116
- *
11117
- * Inert on Node (no indexedDB) — every export resolves to a miss/no-op so the
11118
- * heap load path is unchanged off the browser.
11119
- */
11120
- const DB_NAME = "gerbil-weights-v1";
11121
- const STORE = "tensors";
11122
- function idbAvailable() {
11123
- return typeof indexedDB !== "undefined";
11124
- }
11125
- /**
11126
- * Ask the browser to mark this origin's storage as persistent (exempt from
11127
- * eviction). On iOS Safari a plain tab's storage is evicted under pressure unless
11128
- * persistence is granted (effectively only inside an installed PWA), which is the
11129
- * primary reason a 404 MB model re-downloads on some reloads. We request it
11130
- * up-front on the FIRST cache touch (read OR write) — not only on the first write
11131
- * — so a warm-cache reload re-asserts the request before the browser has a chance
11132
- * to evict mid-session. Best-effort and memoized: a single, idempotent call per
11133
- * page that never throws and never blocks the load. Returns the resulting
11134
- * persisted state (false when unsupported / not granted).
11135
- */
11136
- let _persistRequested$1 = null;
11137
- function idbRequestPersistence() {
11138
- if (_persistRequested$1) return _persistRequested$1;
11139
- _persistRequested$1 = (async () => {
11140
- try {
11141
- if (typeof navigator === "undefined") return false;
11142
- const storage = navigator?.storage;
11143
- if (!storage) return false;
11144
- if (await storage.persisted?.() === true) return true;
11145
- return await storage.persist?.() ?? false;
11146
- } catch {
11147
- return false;
11148
- }
11149
- })();
11150
- return _persistRequested$1;
11151
- }
11152
- let _dbPromise = null;
11153
- /**
11154
- * Per-op watchdog. iOS Safari can silently WEDGE an IndexedDB transaction after
11155
- * a large (~400 MB) write session or under quota pressure — it fires NONE of
11156
- * success/error/abort, so an awaiting caller hangs forever. That presents as the
11157
- * load freezing at "reading model.safetensors header" (the first storage op
11158
- * after that label) and is the #1 cause of the on-device load hang. Every op
11159
- * races this timeout and degrades to its fallback (read → miss → network; write
11160
- * → false → heap) instead of hanging.
11161
- */
11162
- const IDB_OP_TIMEOUT_MS = 12e3;
11163
- /**
11164
- * Drop the memoized connection (and close it) so the NEXT op re-opens a fresh
11165
- * one. A wedged/poisoned connection otherwise persists for the page's lifetime —
11166
- * an in-page retry (no reload) reuses it and hangs again. Closing also aborts
11167
- * the wedged transaction.
11168
- */
11169
- function poisonConnection(db) {
11170
- _dbPromise = null;
11171
- try {
11172
- db?.close();
11173
- } catch {}
11174
- }
11175
- function openDB() {
11176
- if (_dbPromise) return _dbPromise;
11177
- _dbPromise = new Promise((resolve) => {
11178
- if (!idbAvailable()) {
11179
- resolve(null);
11180
- return;
11181
- }
11182
- idbRequestPersistence();
11183
- try {
11184
- const req = indexedDB.open(DB_NAME, 1);
11185
- req.onupgradeneeded = () => {
11186
- const db = req.result;
11187
- if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
11188
- };
11189
- req.onsuccess = () => {
11190
- const db = req.result;
11191
- db.onversionchange = () => poisonConnection(db);
11192
- db.onclose = () => {
11193
- _dbPromise = null;
11194
- };
11195
- resolve(db);
11196
- };
11197
- req.onerror = () => resolve(null);
11198
- req.onblocked = () => resolve(null);
11199
- } catch {
11200
- resolve(null);
11201
- }
11202
- });
11203
- return _dbPromise;
11204
- }
11205
- /** A single transaction operation, promisified. Resolves the fallback on any
11206
- * error OR if the op wedges past IDB_OP_TIMEOUT_MS (and then poisons the
11207
- * connection so the next op re-opens fresh). */
11208
- function tx(mode, run, fallback) {
11209
- return openDB().then((db) => new Promise((resolve) => {
11210
- if (!db) {
11211
- resolve(fallback);
11212
- return;
11213
- }
11214
- let settled = false;
11215
- const finish = (value, poisoned) => {
11216
- if (settled) return;
11217
- settled = true;
11218
- clearTimeout(timer);
11219
- if (poisoned) poisonConnection(db);
11220
- resolve(value);
11221
- };
11222
- const timer = setTimeout(() => finish(fallback, true), IDB_OP_TIMEOUT_MS);
11223
- try {
11224
- const t = db.transaction(STORE, mode);
11225
- const req = run(t.objectStore(STORE));
11226
- req.onsuccess = () => finish(req.result ?? fallback, false);
11227
- req.onerror = () => finish(fallback, true);
11228
- t.onabort = () => finish(fallback, true);
11229
- } catch {
11230
- finish(fallback, true);
11231
- }
11232
- }));
11233
- }
11234
- /** Read a key's bytes, or null on miss / no IDB / any error. */
11235
- async function idbGet(key) {
11236
- const v = await tx("readonly", (s) => s.get(key), null);
11237
- return v instanceof ArrayBuffer ? v : null;
11238
- }
11239
- /**
11240
- * Size-verified read. Returns the bytes ONLY if the cached entry is at least
11241
- * `minBytes` long; otherwise treats it as a corrupt/partial entry: deletes the
11242
- * poisoned key (so a future load re-downloads it instead of trusting it again)
11243
- * and returns null. This is the integrity gate that turns a half-written or
11244
- * partially-evicted tensor into a clean cache-miss rather than wrong bytes (or a
11245
- * later "No GPU buffer for tensor …" crash when get() returns undefined). Pass
11246
- * minBytes <= 0 to skip the size check (behaves like idbGet).
11247
- */
11248
- async function idbGetVerified(key, minBytes) {
11249
- const buf = await idbGet(key);
11250
- if (!buf) return null;
11251
- if (minBytes > 0 && buf.byteLength < minBytes) {
11252
- await idbDelete(key);
11253
- return null;
11254
- }
11255
- return buf;
11256
- }
11257
- /**
11258
- * All cached keys, as a Set, in ONE transaction. Probing presence per-tensor
11259
- * (hundreds of parallel getKey transactions) storms Safari's IndexedDB and hangs
11260
- * the load; getAllKeys() does it in a single transaction so callers can check
11261
- * membership in memory. Empty set on miss / no IDB / error.
11262
- */
11263
- async function idbAllKeys() {
11264
- const keys = await tx("readonly", (s) => s.getAllKeys(), []);
11265
- return new Set((keys ?? []).map((k) => String(k)));
11266
- }
11267
- /** Write a key's bytes. Returns true on success, false on quota/error (caller
11268
- * falls back to network — never throws). */
11269
- async function idbPut(key, buf) {
11270
- return await tx("readwrite", (s) => s.put(buf, key), null) !== null;
11271
- }
11272
- /**
11273
- * Atomic, verified write. Writes the bytes, then reads back the stored length and
11274
- * confirms it matches what we wrote. iOS Safari can let a put() transaction
11275
- * "succeed" yet store a truncated value under quota pressure (or wedge the
11276
- * transaction, which the watchdog turns into a fallback) — leaving a present-but-
11277
- * short entry that the size-blind presence probe would later trust. On any
11278
- * mismatch we DELETE the key so it is never read back as a valid (wrong-size)
11279
- * tensor, and return false so the caller keeps the network bytes. Returns true
11280
- * only when the full-length value is provably persisted.
11281
- */
11282
- async function idbPutVerified(key, buf) {
11283
- const expected = buf.byteLength;
11284
- if (!await idbPut(key, buf)) {
11285
- await idbDelete(key);
11286
- return false;
11287
- }
11288
- const back = await idbGet(key);
11289
- if (!back || back.byteLength !== expected) {
11290
- await idbDelete(key);
11291
- return false;
11471
+ /** Lazily allocate a scratch storage buffer at least `minBytes` large. */
11472
+ getBindingScratchBuffer(minBytes) {
11473
+ if (!this.bindingScratchBuffer || this.bindingScratchBuffer.size < minBytes) this.bindingScratchBuffer = createStorageBuffer(this.ctx, "binding_scratch", Math.max(minBytes, 256));
11474
+ return this.bindingScratchBuffer;
11292
11475
  }
11293
- return true;
11294
- }
11295
- /** Delete a key (best-effort). */
11296
- async function idbDelete(key) {
11297
- await tx("readwrite", (s) => s.delete(key), null);
11298
- }
11299
- /** True when IndexedDB is usable in this environment (browser, not Node). */
11300
- function idbCacheAvailable() {
11301
- return idbAvailable();
11302
- }
11476
+ };
11303
11477
 
11304
11478
  //#endregion
11305
- //#region src/gpu/safetensors.ts
11479
+ //#region src/gpu/gptq-adapter.ts
11306
11480
  /**
11307
- * Parse safetensors header from an ArrayBuffer.
11481
+ * Repack GPTQ tensors into Gerbil's flat INT4 format.
11308
11482
  *
11309
- * Can be called with just the header bytes (for streaming parse header first,
11310
- * then fetch tensor data by offset) or with the entire file.
11483
+ * This is pure data reshuffling no quantization math. The 4-bit values,
11484
+ * scales, and zero points are preserved exactly.
11311
11485
  */
11312
- function parseSafetensorsHeader(buffer) {
11313
- if (buffer.byteLength < 8) throw new Error(`Buffer too small to contain safetensors header length (need 8 bytes, got ${buffer.byteLength}).`);
11314
- const view = new DataView(buffer);
11315
- const headerLength = Number(view.getBigUint64(0, true));
11316
- if (headerLength > buffer.byteLength - 8) throw new Error(`Safetensors header length (${headerLength}) exceeds buffer size (${buffer.byteLength - 8}). Pass at least the first ${headerLength + 8} bytes.`);
11317
- const headerBytes = new Uint8Array(buffer, 8, headerLength);
11318
- const headerStr = new TextDecoder().decode(headerBytes);
11319
- const header = JSON.parse(headerStr);
11320
- const dataStart = 8 + headerLength;
11321
- const entries = [];
11322
- let metadata = null;
11323
- for (const [name, info] of Object.entries(header)) {
11324
- if (name === "__metadata__") {
11325
- metadata = info;
11326
- continue;
11486
+ function repackGPTQ(gptq) {
11487
+ const { qweight, scales: gptqScales, qzeros, K, N, groupSize } = gptq;
11488
+ const totalElements = N * K;
11489
+ const groupsPerCol = Math.ceil(K / groupSize);
11490
+ const totalGroups = N * groupsPerCol;
11491
+ const packed = new Uint32Array(Math.ceil(totalElements / 8));
11492
+ const scales = new Float32Array(totalGroups);
11493
+ const zeros = new Float32Array(totalGroups);
11494
+ for (let k = 0; k < K; k++) {
11495
+ const gptqRow = k >>> 3;
11496
+ const gptqNibblePos = k & 7;
11497
+ const gptqRowBase = gptqRow * N;
11498
+ for (let col = 0; col < N; col++) {
11499
+ const nibble = qweight[gptqRowBase + col] >>> gptqNibblePos * 4 & 15;
11500
+ const flatIdx = col * K + k;
11501
+ const packedIdx = flatIdx >>> 3;
11502
+ const nibblePos = flatIdx & 7;
11503
+ packed[packedIdx] |= nibble << nibblePos * 4;
11504
+ }
11505
+ }
11506
+ for (let g = 0; g < groupsPerCol; g++) {
11507
+ const srcRow = g * N;
11508
+ for (let col = 0; col < N; col++) scales[col * groupsPerCol + g] = gptqScales[srcRow + col];
11509
+ }
11510
+ const zerosNDiv8 = N >>> 3;
11511
+ for (let g = 0; g < groupsPerCol; g++) {
11512
+ const srcRow = g * zerosNDiv8;
11513
+ for (let col = 0; col < N; col++) {
11514
+ const zeroVal = qzeros[srcRow + (col >>> 3)] >>> (col & 7) * 4 & 15;
11515
+ zeros[col * groupsPerCol + g] = zeroVal + 1;
11327
11516
  }
11328
- const { dtype, shape, data_offsets } = info;
11329
- entries.push({
11330
- name,
11331
- dtype,
11332
- shape,
11333
- dataOffset: data_offsets[0],
11334
- dataLength: data_offsets[1] - data_offsets[0]
11335
- });
11336
11517
  }
11337
- entries.sort((a, b) => a.dataOffset - b.dataOffset);
11338
11518
  return {
11339
- headerLength,
11340
- dataStart,
11341
- entries,
11342
- metadata
11519
+ packed,
11520
+ scales,
11521
+ zeros
11343
11522
  };
11344
11523
  }
11345
- /** Byte alignment required for each dtype. */
11346
- function dtypeAlignment(dtype) {
11347
- switch (dtype) {
11348
- case "F64":
11349
- case "I64":
11350
- case "U64": return 8;
11351
- case "F32":
11352
- case "I32":
11353
- case "U32": return 4;
11354
- case "F16":
11355
- case "BF16":
11356
- case "I16":
11357
- case "U16": return 2;
11358
- case "I8":
11359
- case "U8":
11360
- case "BOOL": return 1;
11361
- default: return 1;
11362
- }
11363
- }
11524
+
11525
+ //#endregion
11526
+ //#region src/gpu/idb-cache.ts
11364
11527
  /**
11365
- * Create a typed array for the given dtype.
11528
+ * IndexedDB-backed blob cache for model weights.
11366
11529
  *
11367
- * If `offset` is aligned to the element size, returns a zero-copy view
11368
- * into `buffer`. Otherwise, copies the relevant slice into a new
11369
- * properly-aligned ArrayBuffer.
11530
+ * Why IndexedDB and not the Cache API / OPFS we hand-rolled before:
11531
+ * - The Cache API ignores URL fragments in `match()`, which silently collapsed
11532
+ * our per-tensor `${url}#t:start-len` keys onto one entry → wrong bytes → the
11533
+ * `RRRR` garbage and `wt.data` crashes. IDB keys on the exact string.
11534
+ * - OPFS via Worker (`createSyncAccessHandle`) is finicky on iOS Safari (OOM /
11535
+ * transient failures) and added a lot of moving parts.
11536
+ * IndexedDB is the boring, proven large-binary store every browser supports. It
11537
+ * keys on arbitrary strings (no fragment footgun), stores ArrayBuffers directly,
11538
+ * and is durable (subject to the same per-origin quota as Cache/OPFS — install to
11539
+ * Home Screen lifts that ceiling; IDB doesn't change it, it just makes caching
11540
+ * actually CORRECT).
11541
+ *
11542
+ * Inert on Node (no indexedDB) — every export resolves to a miss/no-op so the
11543
+ * heap load path is unchanged off the browser.
11370
11544
  */
11371
- function makeTypedView(buffer, offset, byteLength, dtype) {
11372
- const aligned = offset % dtypeAlignment(dtype) === 0;
11373
- const src = aligned ? buffer : buffer.slice(offset, offset + byteLength);
11374
- const base = aligned ? offset : 0;
11375
- switch (dtype) {
11376
- case "F32": return new Float32Array(src, base, byteLength / 4);
11377
- case "F16": return new Uint16Array(src, base, byteLength / 2);
11378
- case "BF16": return new Uint16Array(src, base, byteLength / 2);
11379
- case "I32": return new Int32Array(src, base, byteLength / 4);
11380
- case "U32": return new Uint32Array(src, base, byteLength / 4);
11381
- case "I8": return new Int8Array(src, base, byteLength);
11382
- case "U8": return new Uint8Array(src, base, byteLength);
11383
- case "I16": return new Int16Array(src, base, byteLength / 2);
11384
- case "U16": return new Uint16Array(src, base, byteLength / 2);
11385
- case "F64": return new Float64Array(src, base, byteLength / 8);
11386
- case "I64": return new BigInt64Array(src, base, byteLength / 8);
11387
- case "U64": return new BigUint64Array(src, base, byteLength / 8);
11388
- case "BOOL": return new Uint8Array(src, base, byteLength);
11389
- default: throw new Error(`Unsupported safetensors dtype: ${dtype}`);
11390
- }
11545
+ const DB_NAME = "gerbil-weights-v1";
11546
+ const STORE = "tensors";
11547
+ function idbAvailable() {
11548
+ return typeof indexedDB !== "undefined";
11391
11549
  }
11392
11550
  /**
11393
- * Get a typed array view for a tensor entry from a complete safetensors buffer.
11394
- * Zero-copy when the byte offset is properly aligned; copies otherwise.
11551
+ * Ask the browser to mark this origin's storage as persistent (exempt from
11552
+ * eviction). On iOS Safari a plain tab's storage is evicted under pressure unless
11553
+ * persistence is granted (effectively only inside an installed PWA), which is the
11554
+ * primary reason a 404 MB model re-downloads on some reloads. We request it
11555
+ * up-front on the FIRST cache touch (read OR write) — not only on the first write
11556
+ * — so a warm-cache reload re-asserts the request before the browser has a chance
11557
+ * to evict mid-session. Best-effort and memoized: a single, idempotent call per
11558
+ * page that never throws and never blocks the load. Returns the resulting
11559
+ * persisted state (false when unsupported / not granted).
11395
11560
  */
11396
- function getTensorData(buffer, file, entry) {
11397
- return makeTypedView(buffer, file.dataStart + entry.dataOffset, entry.dataLength, entry.dtype);
11561
+ let _persistRequested$1 = null;
11562
+ function idbRequestPersistence() {
11563
+ if (_persistRequested$1) return _persistRequested$1;
11564
+ _persistRequested$1 = (async () => {
11565
+ try {
11566
+ if (typeof navigator === "undefined") return false;
11567
+ const storage = navigator?.storage;
11568
+ if (!storage) return false;
11569
+ if (await storage.persisted?.() === true) return true;
11570
+ return await storage.persist?.() ?? false;
11571
+ } catch {
11572
+ return false;
11573
+ }
11574
+ })();
11575
+ return _persistRequested$1;
11398
11576
  }
11577
+ let _dbPromise = null;
11399
11578
  /**
11400
- * Convert BF16 (bfloat16) data to F32.
11401
- * BF16 is the upper 16 bits of an IEEE 754 float32, so conversion is a simple left-shift.
11579
+ * Per-op watchdog. iOS Safari can silently WEDGE an IndexedDB transaction after
11580
+ * a large (~400 MB) write session or under quota pressure it fires NONE of
11581
+ * success/error/abort, so an awaiting caller hangs forever. That presents as the
11582
+ * load freezing at "reading model.safetensors header" (the first storage op
11583
+ * after that label) and is the #1 cause of the on-device load hang. Every op
11584
+ * races this timeout and degrades to its fallback (read → miss → network; write
11585
+ * → false → heap) instead of hanging.
11402
11586
  */
11403
- function bf16ToF32(bf16Bytes) {
11404
- const bf16 = new Uint16Array(bf16Bytes.buffer, bf16Bytes.byteOffset, bf16Bytes.byteLength / 2);
11405
- const f32 = new Float32Array(bf16.length);
11406
- const u32 = new Uint32Array(f32.buffer);
11407
- for (let i = 0; i < bf16.length; i++) u32[i] = bf16[i] << 16;
11408
- return f32;
11409
- }
11587
+ const IDB_OP_TIMEOUT_MS = 12e3;
11410
11588
  /**
11411
- * Convert F16 (IEEE 754 half-precision) data to F32.
11589
+ * Drop the memoized connection (and close it) so the NEXT op re-opens a fresh
11590
+ * one. A wedged/poisoned connection otherwise persists for the page's lifetime —
11591
+ * an in-page retry (no reload) reuses it and hangs again. Closing also aborts
11592
+ * the wedged transaction.
11412
11593
  */
11413
- function f16ToF32(f16View) {
11414
- const u16 = f16View instanceof Uint16Array ? f16View : new Uint16Array(f16View.buffer, f16View.byteOffset, f16View.byteLength / 2);
11415
- const f32 = new Float32Array(u16.length);
11416
- for (let i = 0; i < u16.length; i++) {
11417
- const h = u16[i];
11418
- const sign = h >> 15 & 1;
11419
- const exp = h >> 10 & 31;
11420
- const frac = h & 1023;
11421
- if (exp === 0) f32[i] = (sign ? -1 : 1) * 2 ** -14 * (frac / 1024);
11422
- else if (exp === 31) f32[i] = frac === 0 ? sign ? -Infinity : Infinity : NaN;
11423
- else f32[i] = (sign ? -1 : 1) * 2 ** (exp - 15) * (1 + frac / 1024);
11424
- }
11425
- return f32;
11426
- }
11427
-
11428
- //#endregion
11429
- //#region src/gpu/lora.ts
11430
- function resolveAdapterBase(spec, revision) {
11431
- if (spec.startsWith("file:")) return {
11432
- base: spec.slice(5),
11433
- local: true
11434
- };
11435
- if (spec.startsWith("http://") || spec.startsWith("https://")) return {
11436
- base: spec,
11437
- local: false
11438
- };
11439
- return {
11440
- base: `https://huggingface.co/${spec.startsWith("hf:") ? spec.slice(3) : spec}/resolve/${revision}`,
11441
- local: false
11442
- };
11443
- }
11444
- async function readLocalFile(dir, name) {
11594
+ function poisonConnection(db) {
11595
+ _dbPromise = null;
11445
11596
  try {
11446
- const fs = await import("node:fs");
11447
- const p = (await import("node:path")).join(dir, name);
11448
- if (!fs.existsSync(p)) return null;
11449
- const buf = fs.readFileSync(p);
11450
- return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
11451
- } catch {
11452
- return null;
11453
- }
11597
+ db?.close();
11598
+ } catch {}
11599
+ }
11600
+ function openDB() {
11601
+ if (_dbPromise) return _dbPromise;
11602
+ _dbPromise = new Promise((resolve) => {
11603
+ if (!idbAvailable()) {
11604
+ resolve(null);
11605
+ return;
11606
+ }
11607
+ idbRequestPersistence();
11608
+ try {
11609
+ const req = indexedDB.open(DB_NAME, 1);
11610
+ req.onupgradeneeded = () => {
11611
+ const db = req.result;
11612
+ if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
11613
+ };
11614
+ req.onsuccess = () => {
11615
+ const db = req.result;
11616
+ db.onversionchange = () => poisonConnection(db);
11617
+ db.onclose = () => {
11618
+ _dbPromise = null;
11619
+ };
11620
+ resolve(db);
11621
+ };
11622
+ req.onerror = () => resolve(null);
11623
+ req.onblocked = () => resolve(null);
11624
+ } catch {
11625
+ resolve(null);
11626
+ }
11627
+ });
11628
+ return _dbPromise;
11454
11629
  }
11455
- async function fetchFile(base, local, name, hfToken) {
11456
- if (local) return readLocalFile(base, name);
11457
- const headers = {};
11458
- if (hfToken) headers.Authorization = `Bearer ${hfToken}`;
11459
- const res = await fetch(`${base}/${name}`, { headers });
11460
- if (!res.ok) return null;
11461
- return res.arrayBuffer();
11630
+ /** A single transaction operation, promisified. Resolves the fallback on any
11631
+ * error OR if the op wedges past IDB_OP_TIMEOUT_MS (and then poisons the
11632
+ * connection so the next op re-opens fresh). */
11633
+ function tx(mode, run, fallback) {
11634
+ return openDB().then((db) => new Promise((resolve) => {
11635
+ if (!db) {
11636
+ resolve(fallback);
11637
+ return;
11638
+ }
11639
+ let settled = false;
11640
+ const finish = (value, poisoned) => {
11641
+ if (settled) return;
11642
+ settled = true;
11643
+ clearTimeout(timer);
11644
+ if (poisoned) poisonConnection(db);
11645
+ resolve(value);
11646
+ };
11647
+ const timer = setTimeout(() => finish(fallback, true), IDB_OP_TIMEOUT_MS);
11648
+ try {
11649
+ const t = db.transaction(STORE, mode);
11650
+ const req = run(t.objectStore(STORE));
11651
+ req.onsuccess = () => finish(req.result ?? fallback, false);
11652
+ req.onerror = () => finish(fallback, true);
11653
+ t.onabort = () => finish(fallback, true);
11654
+ } catch {
11655
+ finish(fallback, true);
11656
+ }
11657
+ }));
11462
11658
  }
11463
- /** Convert a safetensors tensor slice to f32 (adapters are tiny; eager is fine). */
11464
- function sliceToF32(buf, dataStart, entry) {
11465
- const start = dataStart + entry.dataOffset;
11466
- switch (entry.dtype) {
11467
- case "F32": return new Float32Array(buf.slice(start, start + entry.dataLength));
11468
- case "BF16": return bf16ToF32(new Uint8Array(buf, start, entry.dataLength));
11469
- case "F16": return f16ToF32(new Uint8Array(buf, start, entry.dataLength));
11470
- default: throw new Error(`LoRA: unsupported adapter tensor dtype ${entry.dtype}`);
11471
- }
11659
+ /** Read a key's bytes, or null on miss / no IDB / any error. */
11660
+ async function idbGet(key) {
11661
+ const v = await tx("readonly", (s) => s.get(key), null);
11662
+ return v instanceof ArrayBuffer ? v : null;
11472
11663
  }
11473
11664
  /**
11474
- * Download + parse an adapter (`adapter_config.json` + `adapter_model.safetensors`).
11475
- * Returns null when the source has no adapter (so callers can no-op cleanly).
11665
+ * Size-verified read. Returns the bytes ONLY if the cached entry is at least
11666
+ * `minBytes` long; otherwise treats it as a corrupt/partial entry: deletes the
11667
+ * poisoned key (so a future load re-downloads it instead of trusting it again)
11668
+ * and returns null. This is the integrity gate that turns a half-written or
11669
+ * partially-evicted tensor into a clean cache-miss rather than wrong bytes (or a
11670
+ * later "No GPU buffer for tensor …" crash when get() returns undefined). Pass
11671
+ * minBytes <= 0 to skip the size check (behaves like idbGet).
11476
11672
  */
11477
- async function fetchAdapter(spec, opts = {}) {
11478
- const { base, local } = resolveAdapterBase(spec, opts.revision ?? "main");
11479
- opts.onProgress?.(`Loading adapter ${spec}…`);
11480
- const configBuf = await fetchFile(base, local, "adapter_config.json", opts.hfToken);
11481
- if (!configBuf) throw new Error(`LoRA: no adapter_config.json at ${spec}. A flavor needs a PEFT adapter (adapter_config.json + adapter_model.safetensors).`);
11482
- const config = JSON.parse(new TextDecoder().decode(configBuf));
11483
- const weightsBuf = await fetchFile(base, local, "adapter_model.safetensors", opts.hfToken);
11484
- if (!weightsBuf) throw new Error(`LoRA: no adapter_model.safetensors at ${spec} (only .safetensors adapters are supported, not legacy .bin).`);
11485
- const file = parseSafetensorsHeader(weightsBuf);
11486
- const tensors = /* @__PURE__ */ new Map();
11487
- for (const entry of file.entries) {
11488
- if (!entry.name.includes("lora_A") && !entry.name.includes("lora_B")) continue;
11489
- tensors.set(entry.name, {
11490
- data: sliceToF32(weightsBuf, file.dataStart, entry),
11491
- shape: entry.shape
11492
- });
11673
+ async function idbGetVerified(key, minBytes) {
11674
+ const buf = await idbGet(key);
11675
+ if (!buf) return null;
11676
+ if (minBytes > 0 && buf.byteLength < minBytes) {
11677
+ await idbDelete(key);
11678
+ return null;
11493
11679
  }
11494
- if (tensors.size === 0) throw new Error(`LoRA: adapter at ${spec} has no lora_A/lora_B tensors.`);
11495
- return {
11496
- config,
11497
- tensors,
11498
- label: spec
11499
- };
11680
+ return buf;
11500
11681
  }
11501
11682
  /**
11502
- * Strip a PEFT tensor key down to its module path and canonicalize it to the
11503
- * base weight key the engine uses.
11504
- *
11505
- * PEFT names a target like
11506
- * base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight
11507
- * We drop the PEFT wrapper prefix + the lora_A/B suffix to get the module path
11508
- * (…layers.0.self_attn.q_proj), append ".weight", and run the SAME key mapper
11509
- * the base load used — so Qwen ("model." strip), Gemma, and LFM2 (out_proj→
11510
- * o_proj) all canonicalize identically to their base tensors.
11683
+ * All cached keys, as a Set, in ONE transaction. Probing presence per-tensor
11684
+ * (hundreds of parallel getKey transactions) storms Safari's IndexedDB and hangs
11685
+ * the load; getAllKeys() does it in a single transaction so callers can check
11686
+ * membership in memory. Empty set on miss / no IDB / error.
11511
11687
  */
11512
- function canonicalWeightKey(peftKey, keyMapper) {
11513
- const marker = peftKey.includes(".lora_A") ? ".lora_A" : ".lora_B";
11514
- let stem = peftKey.slice(0, peftKey.indexOf(marker));
11515
- stem = stem.replace(/^base_model\.model\./, "").replace(/^base_model\./, "");
11516
- return keyMapper(`${stem.startsWith("model.") ? stem : `model.${stem}`}.weight`);
11688
+ async function idbAllKeys() {
11689
+ const keys = await tx("readonly", (s) => s.getAllKeys(), []);
11690
+ return new Set((keys ?? []).map((k) => String(k)));
11517
11691
  }
11518
- /** Longest matching suffix in a {moduleName: value} pattern map. */
11519
- function patternLookup(patterns, peftStem) {
11520
- if (!patterns) return void 0;
11521
- let best;
11522
- let bestLen = -1;
11523
- for (const [name, val] of Object.entries(patterns)) if (peftStem.includes(name) && name.length > bestLen) {
11524
- best = val;
11525
- bestLen = name.length;
11526
- }
11527
- return best;
11692
+ /** Write a key's bytes. Returns true on success, false on quota/error (caller
11693
+ * falls back to network — never throws). */
11694
+ async function idbPut(key, buf) {
11695
+ return await tx("readwrite", (s) => s.put(buf, key), null) !== null;
11528
11696
  }
11529
11697
  /**
11530
- * Pair lora_A/lora_B tensors and resolve each to a canonical `LoRADelta`.
11531
- * `keyMapper` must be the same mapper the base model load used.
11698
+ * Atomic, verified write. Writes the bytes, then reads back the stored length and
11699
+ * confirms it matches what we wrote. iOS Safari can let a put() transaction
11700
+ * "succeed" yet store a truncated value under quota pressure (or wedge the
11701
+ * transaction, which the watchdog turns into a fallback) — leaving a present-but-
11702
+ * short entry that the size-blind presence probe would later trust. On any
11703
+ * mismatch we DELETE the key so it is never read back as a valid (wrong-size)
11704
+ * tensor, and return false so the caller keeps the network bytes. Returns true
11705
+ * only when the full-length value is provably persisted.
11532
11706
  */
11533
- function buildLoRADeltas(adapter, keyMapper) {
11534
- const { config, tensors } = adapter;
11535
- const stems = /* @__PURE__ */ new Set();
11536
- for (const name of tensors.keys()) {
11537
- const marker = name.includes(".lora_A") ? ".lora_A" : ".lora_B";
11538
- stems.add(name.slice(0, name.indexOf(marker)));
11707
+ async function idbPutVerified(key, buf) {
11708
+ const expected = buf.byteLength;
11709
+ if (!await idbPut(key, buf)) {
11710
+ await idbDelete(key);
11711
+ return false;
11539
11712
  }
11540
- const deltas = [];
11541
- for (const stem of stems) {
11542
- const A = tensors.get(`${stem}.lora_A.weight`) ?? tensors.get(`${stem}.lora_A`);
11543
- const B = tensors.get(`${stem}.lora_B.weight`) ?? tensors.get(`${stem}.lora_B`);
11544
- if (!A || !B) continue;
11545
- const key = canonicalWeightKey(`${stem}.lora_A.weight`, keyMapper);
11546
- if (!key) continue;
11547
- const r = A.shape[0];
11548
- const inFeatures = A.shape[1];
11549
- const outFeatures = B.shape[0];
11550
- if (B.shape[1] !== r) {
11551
- console.warn(`[gerbil] LoRA: rank mismatch on ${stem} (A r=${r}, B r=${B.shape[1]}); skipping.`);
11552
- continue;
11553
- }
11554
- const alpha = patternLookup(config.alpha_pattern, stem) ?? config.lora_alpha;
11555
- const effR = patternLookup(config.rank_pattern, stem) ?? r;
11556
- const scaling = config.use_rslora ? alpha / Math.sqrt(effR) : alpha / effR;
11557
- deltas.push({
11558
- key,
11559
- A: A.data,
11560
- B: B.data,
11561
- r,
11562
- inFeatures,
11563
- outFeatures,
11564
- scaling,
11565
- fanInFanOut: config.fan_in_fan_out === true
11566
- });
11713
+ const back = await idbGet(key);
11714
+ if (!back || back.byteLength !== expected) {
11715
+ await idbDelete(key);
11716
+ return false;
11567
11717
  }
11568
- return deltas;
11718
+ return true;
11569
11719
  }
11570
- /**
11571
- * Fold every resolved delta into the store's base weights in place
11572
- * (W += scaling · B·A). Runs after weights are loaded and before architecture
11573
- * transforms/quantization. Returns the count of tensors merged (for logging).
11574
- *
11575
- * Skips — loudly — any target whose base tensor is missing, shape-mismatched, or
11576
- * not a float pack (a pre-quantized MLX/GPTQ base can't be merged this way).
11577
- */
11578
- async function applyLoRAToStore(store, deltas, onProgress) {
11579
- let merged = 0;
11580
- let skipped = 0;
11581
- for (const d of deltas) {
11582
- const base = await store.get(d.key);
11583
- if (!base) {
11584
- skipped++;
11585
- continue;
11586
- }
11587
- if (!(base.data instanceof Float32Array)) {
11588
- console.warn(`[gerbil] LoRA: base tensor ${d.key} is not float (pre-quantized base?); skipping merge.`);
11589
- skipped++;
11590
- continue;
11591
- }
11592
- const [outF, inF] = base.shape;
11593
- const rows = d.fanInFanOut ? inF : outF;
11594
- const cols = d.fanInFanOut ? outF : inF;
11595
- if (rows !== d.outFeatures || cols !== d.inFeatures) {
11596
- console.warn(`[gerbil] LoRA: shape mismatch on ${d.key} (base ${base.shape}, delta ${d.outFeatures}×${d.inFeatures}); skipping.`);
11597
- skipped++;
11598
- continue;
11599
- }
11600
- const W = new Float32Array(base.data);
11601
- const { A, B, r, scaling, fanInFanOut } = d;
11602
- for (let o = 0; o < d.outFeatures; o++) {
11603
- const bRow = o * r;
11604
- for (let k = 0; k < r; k++) {
11605
- const bScaled = scaling * B[bRow + k];
11606
- if (bScaled === 0) continue;
11607
- const aRow = k * d.inFeatures;
11608
- if (fanInFanOut) for (let i = 0; i < d.inFeatures; i++) W[i * outF + o] += bScaled * A[aRow + i];
11609
- else {
11610
- const wRow = o * inF;
11611
- for (let i = 0; i < d.inFeatures; i++) W[wRow + i] += bScaled * A[aRow + i];
11612
- }
11613
- }
11614
- }
11615
- await store.set(d.key, {
11616
- data: W,
11617
- shape: base.shape
11618
- });
11619
- merged++;
11620
- }
11621
- onProgress?.(`Adapter merged into ${merged} tensors${skipped ? ` (${skipped} skipped)` : ""}.`);
11622
- return {
11623
- merged,
11624
- skipped
11625
- };
11720
+ /** Delete a key (best-effort). */
11721
+ async function idbDelete(key) {
11722
+ await tx("readwrite", (s) => s.delete(key), null);
11723
+ }
11724
+ /** True when IndexedDB is usable in this environment (browser, not Node). */
11725
+ function idbCacheAvailable() {
11726
+ return idbAvailable();
11626
11727
  }
11627
11728
 
11628
11729
  //#endregion
@@ -14836,5 +14937,5 @@ function selectGraphWeights(graph, weights) {
14836
14937
  }
14837
14938
 
14838
14939
  //#endregion
14839
- export { destroyBuffers as C, verifyGPU as E, createUniformBuffer as S, initGPU as T, KERNEL_REGISTRY as _, loadModel as a, createBindGroup as b, loadParlerTTS as c, remapPrunedToken as d, Tokenizer as f, Executor as g, fetchAdapter as h, loadKaniTTS as i, quantizeBackboneInt4 as l, buildLoRADeltas as m, MoonshineEncoderExecutor as n, loadMoonshine as o, applyLoRAToStore as p, createKeyMapperForArch as r, loadOuteTTS as s, MoonshineSTT as t, quantizeKaniBackbone as u, MATMUL_BIAS_F16C_SPEC as v, getOrCreatePipeline as w, createStorageBuffer as x, clearPipelineCache as y };
14840
- //# sourceMappingURL=moonshine-stt-CgDXoLFd.mjs.map
14940
+ export { createUniformBuffer as C, verifyGPU as D, initGPU as E, createStorageBuffer as S, getOrCreatePipeline as T, splitFusedQGateDelta as _, loadModel as a, clearPipelineCache as b, loadParlerTTS as c, remapPrunedToken as d, Tokenizer as f, fetchAdapter as g, buildLoRADeltas as h, loadKaniTTS as i, quantizeBackboneInt4 as l, applyLoRAToStore as m, MoonshineEncoderExecutor as n, loadMoonshine as o, Executor as p, createKeyMapperForArch as r, loadOuteTTS as s, MoonshineSTT as t, quantizeKaniBackbone as u, KERNEL_REGISTRY as v, destroyBuffers as w, createBindGroup as x, MATMUL_BIAS_F16C_SPEC as y };
14941
+ //# sourceMappingURL=moonshine-stt-BXoZaHJE.mjs.map