@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.
- package/dist/cli.mjs +7 -7
- package/dist/cli.mjs.map +1 -1
- package/dist/frameworks/express.mjs +1 -1
- package/dist/frameworks/fastify.mjs +1 -1
- package/dist/frameworks/hono.mjs +1 -1
- package/dist/frameworks/next.d.mts +2 -2
- package/dist/frameworks/next.mjs +1 -1
- package/dist/frameworks/trpc.mjs +1 -1
- package/dist/gerbil-CTefAwKp.mjs +4 -0
- package/dist/{gerbil-BpYemKEH.mjs → gerbil-CYVmU8sQ.mjs} +2 -2
- package/dist/{gerbil-BpYemKEH.mjs.map → gerbil-CYVmU8sQ.mjs.map} +1 -1
- package/dist/{gerbil-CaAGG2VP.d.mts → gerbil-Cu7YwBBz.d.mts} +2 -2
- package/dist/{gerbil-CaAGG2VP.d.mts.map → gerbil-Cu7YwBBz.d.mts.map} +1 -1
- package/dist/gpu/hooks.d.mts +1 -1
- package/dist/gpu/index.d.mts +2 -2
- package/dist/gpu/index.mjs +3 -3
- package/dist/{gpu-BCfd_K38.mjs → gpu-EyIH7Qfc.mjs} +20 -16
- package/dist/{gpu-BCfd_K38.mjs.map → gpu-EyIH7Qfc.mjs.map} +1 -1
- package/dist/index-B3tjyDJI.d.mts.map +1 -1
- package/dist/{index-DqJMofyU.d.mts → index-DeKYzJgJ.d.mts} +46 -2
- package/dist/index-DeKYzJgJ.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +4 -4
- package/dist/integrations/ai-sdk.mjs +1 -1
- package/dist/integrations/langchain.mjs +1 -1
- package/dist/integrations/llamaindex.mjs +1 -1
- package/dist/integrations/mcp.d.mts +2 -2
- package/dist/integrations/mcp.mjs +4 -4
- package/dist/{mcp-DSpjqWZC.mjs → mcp-CAsD7eCj.mjs} +3 -3
- package/dist/{mcp-DSpjqWZC.mjs.map → mcp-CAsD7eCj.mjs.map} +1 -1
- package/dist/{moonshine-stt-CgDXoLFd.mjs → moonshine-stt-BXoZaHJE.mjs} +651 -550
- package/dist/moonshine-stt-BXoZaHJE.mjs.map +1 -0
- package/dist/moonshine-stt-DZVnKgPO.mjs +4 -0
- package/dist/{one-liner-BgNDxJSJ.mjs → one-liner-ppgw4jHH.mjs} +2 -2
- package/dist/{one-liner-BgNDxJSJ.mjs.map → one-liner-ppgw4jHH.mjs.map} +1 -1
- package/dist/{repl-DSK2hDzu.mjs → repl-C0Ew7_Z-.mjs} +3 -3
- package/dist/skills/index.d.mts +2 -2
- package/dist/skills/index.mjs +3 -3
- package/dist/{skills-Cd75ZGew.mjs → skills-CQa1Gshd.mjs} +2 -2
- package/dist/{skills-Cd75ZGew.mjs.map → skills-CQa1Gshd.mjs.map} +1 -1
- package/dist/tune/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/gerbil-BY5EW-Jk.mjs +0 -4
- package/dist/index-DqJMofyU.d.mts.map +0 -1
- package/dist/moonshine-stt-CcVt4Vdd.mjs +0 -4
- 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.
|
|
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
|
-
|
|
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/
|
|
11479
|
+
//#region src/gpu/gptq-adapter.ts
|
|
11306
11480
|
/**
|
|
11307
|
-
*
|
|
11481
|
+
* Repack GPTQ tensors into Gerbil's flat INT4 format.
|
|
11308
11482
|
*
|
|
11309
|
-
*
|
|
11310
|
-
*
|
|
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
|
|
11313
|
-
|
|
11314
|
-
const
|
|
11315
|
-
const
|
|
11316
|
-
|
|
11317
|
-
const
|
|
11318
|
-
const
|
|
11319
|
-
const
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
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
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
metadata
|
|
11519
|
+
packed,
|
|
11520
|
+
scales,
|
|
11521
|
+
zeros
|
|
11343
11522
|
};
|
|
11344
11523
|
}
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
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
|
-
*
|
|
11528
|
+
* IndexedDB-backed blob cache for model weights.
|
|
11366
11529
|
*
|
|
11367
|
-
*
|
|
11368
|
-
*
|
|
11369
|
-
*
|
|
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
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
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
|
-
*
|
|
11394
|
-
*
|
|
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
|
-
|
|
11397
|
-
|
|
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
|
-
*
|
|
11401
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
11414
|
-
|
|
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
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
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
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
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
|
-
/**
|
|
11464
|
-
function
|
|
11465
|
-
const
|
|
11466
|
-
|
|
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
|
-
*
|
|
11475
|
-
*
|
|
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
|
|
11478
|
-
const
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
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
|
-
|
|
11495
|
-
return {
|
|
11496
|
-
config,
|
|
11497
|
-
tensors,
|
|
11498
|
-
label: spec
|
|
11499
|
-
};
|
|
11680
|
+
return buf;
|
|
11500
11681
|
}
|
|
11501
11682
|
/**
|
|
11502
|
-
*
|
|
11503
|
-
*
|
|
11504
|
-
*
|
|
11505
|
-
*
|
|
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
|
|
11513
|
-
const
|
|
11514
|
-
|
|
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
|
-
/**
|
|
11519
|
-
|
|
11520
|
-
|
|
11521
|
-
|
|
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
|
-
*
|
|
11531
|
-
*
|
|
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
|
|
11534
|
-
const
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
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
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
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
|
|
11718
|
+
return true;
|
|
11569
11719
|
}
|
|
11570
|
-
/**
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
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 {
|
|
14840
|
-
//# sourceMappingURL=moonshine-stt-
|
|
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
|