@tryhamster/gerbil 1.11.1 → 1.11.2
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-BpYemKEH.mjs → gerbil-CfNSSpgW.mjs} +2 -2
- package/dist/{gerbil-BpYemKEH.mjs.map → gerbil-CfNSSpgW.mjs.map} +1 -1
- package/dist/gerbil-Cq20PKtz.mjs +4 -0
- package/dist/{gerbil-CaAGG2VP.d.mts → gerbil-Cs8L0iVm.d.mts} +2 -2
- package/dist/{gerbil-CaAGG2VP.d.mts.map → gerbil-Cs8L0iVm.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-BQ8I-VDx.mjs} +2 -2
- package/dist/{gpu-BCfd_K38.mjs.map → gpu-BQ8I-VDx.mjs.map} +1 -1
- package/dist/index-B3tjyDJI.d.mts.map +1 -1
- package/dist/{index-DqJMofyU.d.mts → index-t_hY-XWW.d.mts} +37 -2
- package/dist/index-t_hY-XWW.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-BITYRhF6.mjs} +3 -3
- package/dist/{mcp-DSpjqWZC.mjs.map → mcp-BITYRhF6.mjs.map} +1 -1
- package/dist/{moonshine-stt-CgDXoLFd.mjs → moonshine-stt-BI-5UmD_.mjs} +640 -550
- package/dist/moonshine-stt-BI-5UmD_.mjs.map +1 -0
- package/dist/moonshine-stt-CZ00EdnB.mjs +4 -0
- package/dist/{one-liner-BgNDxJSJ.mjs → one-liner-BLz8g6Gz.mjs} +2 -2
- package/dist/{one-liner-BgNDxJSJ.mjs.map → one-liner-BLz8g6Gz.mjs.map} +1 -1
- package/dist/{repl-DSK2hDzu.mjs → repl-CXCSbfES.mjs} +3 -3
- package/dist/skills/index.d.mts +2 -2
- package/dist/skills/index.mjs +3 -3
- package/dist/{skills-Cd75ZGew.mjs → skills-BPQb-p5b.mjs} +2 -2
- package/dist/{skills-Cd75ZGew.mjs.map → skills-BPQb-p5b.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;
|
|
@@ -9167,23 +9542,62 @@ var Executor = class Executor {
|
|
|
9167
9542
|
* the base weight buffers and bind groups are never touched or re-uploaded.
|
|
9168
9543
|
* Pass an empty list (or call {@link clearRuntimeLoRA}) to drop the overlay.
|
|
9169
9544
|
*/
|
|
9545
|
+
/** Find the base linear (MatMulInt4 / MatMul) node whose weight is `key`. */
|
|
9546
|
+
findBaseLinear(key) {
|
|
9547
|
+
return this.graph.nodes.find((n) => {
|
|
9548
|
+
if (n.opType !== "MatMulInt4" && n.opType !== "MatMul") return false;
|
|
9549
|
+
const w = n.inputs[1];
|
|
9550
|
+
if (!w) return false;
|
|
9551
|
+
return (w.endsWith(".q") ? w.slice(0, -2) : w) === key;
|
|
9552
|
+
});
|
|
9553
|
+
}
|
|
9554
|
+
/**
|
|
9555
|
+
* Expand any fused query+gate delta into separate `q_proj` + `attn_gate`
|
|
9556
|
+
* deltas so it can splice onto a deinterleaved runtime graph.
|
|
9557
|
+
*
|
|
9558
|
+
* Qwen3.5-style gated attention ships `q_proj.weight` fused as
|
|
9559
|
+
* `[2·num_heads·head_dim, hidden]` (per-head: query rows then gate rows); the
|
|
9560
|
+
* model loader splits it into two runtime linears (`q_proj`, `attn_gate`). An
|
|
9561
|
+
* adapter trained on the fused checkpoint carries a single `q_proj` delta whose
|
|
9562
|
+
* out-dim is 2× either split node, so it matches neither. When BOTH split
|
|
9563
|
+
* linears exist and the delta is exactly their combined width, split it via the
|
|
9564
|
+
* same per-head row deinterleave the loader uses; otherwise the delta passes
|
|
9565
|
+
* through untouched (e.g. a base that never split the projection).
|
|
9566
|
+
*/
|
|
9567
|
+
expandFusedQGateDeltas(deltas) {
|
|
9568
|
+
const out = [];
|
|
9569
|
+
for (const d of deltas) {
|
|
9570
|
+
const m = /^(layers\.\d+\.self_attn)\.q_proj\.weight$/.exec(d.key);
|
|
9571
|
+
const gateKey = m ? `${m[1]}.attn_gate.weight` : null;
|
|
9572
|
+
const qNode = m ? this.findBaseLinear(d.key) : void 0;
|
|
9573
|
+
const gateNode = gateKey ? this.findBaseLinear(gateKey) : void 0;
|
|
9574
|
+
if (!(m && gateKey && qNode && gateNode)) {
|
|
9575
|
+
out.push(d);
|
|
9576
|
+
continue;
|
|
9577
|
+
}
|
|
9578
|
+
const nQ = qNode.attributes.N;
|
|
9579
|
+
const nGate = gateNode.attributes.N;
|
|
9580
|
+
const headDim = this.graph.config.head_dim;
|
|
9581
|
+
if (nQ !== nGate || d.outFeatures !== nQ + nGate || !headDim || nQ % headDim !== 0) {
|
|
9582
|
+
out.push(d);
|
|
9583
|
+
continue;
|
|
9584
|
+
}
|
|
9585
|
+
out.push(...splitFusedQGateDelta(d, d.key, gateKey, headDim));
|
|
9586
|
+
}
|
|
9587
|
+
return out;
|
|
9588
|
+
}
|
|
9170
9589
|
applyRuntimeLoRA(deltas) {
|
|
9171
9590
|
this.clearRuntimeLoRA();
|
|
9172
9591
|
const overlay = /* @__PURE__ */ new Map();
|
|
9173
9592
|
let applied = 0;
|
|
9174
9593
|
let skipped = 0;
|
|
9175
|
-
for (const d of deltas) {
|
|
9594
|
+
for (const d of this.expandFusedQGateDeltas(deltas)) {
|
|
9176
9595
|
if (d.fanInFanOut) {
|
|
9177
9596
|
console.warn(`[executor] runtime LoRA: fan_in_fan_out target ${d.key} unsupported; skipping.`);
|
|
9178
9597
|
skipped++;
|
|
9179
9598
|
continue;
|
|
9180
9599
|
}
|
|
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
|
-
});
|
|
9600
|
+
const node = this.findBaseLinear(d.key);
|
|
9187
9601
|
if (!node) {
|
|
9188
9602
|
skipped++;
|
|
9189
9603
|
continue;
|
|
@@ -11043,586 +11457,262 @@ var Executor = class Executor {
|
|
|
11043
11457
|
}
|
|
11044
11458
|
return entries;
|
|
11045
11459
|
}
|
|
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;
|
|
11460
|
+
/** Lazily allocate a scratch storage buffer at least `minBytes` large. */
|
|
11461
|
+
getBindingScratchBuffer(minBytes) {
|
|
11462
|
+
if (!this.bindingScratchBuffer || this.bindingScratchBuffer.size < minBytes) this.bindingScratchBuffer = createStorageBuffer(this.ctx, "binding_scratch", Math.max(minBytes, 256));
|
|
11463
|
+
return this.bindingScratchBuffer;
|
|
11292
11464
|
}
|
|
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
|
-
}
|
|
11465
|
+
};
|
|
11303
11466
|
|
|
11304
11467
|
//#endregion
|
|
11305
|
-
//#region src/gpu/
|
|
11468
|
+
//#region src/gpu/gptq-adapter.ts
|
|
11306
11469
|
/**
|
|
11307
|
-
*
|
|
11470
|
+
* Repack GPTQ tensors into Gerbil's flat INT4 format.
|
|
11308
11471
|
*
|
|
11309
|
-
*
|
|
11310
|
-
*
|
|
11472
|
+
* This is pure data reshuffling — no quantization math. The 4-bit values,
|
|
11473
|
+
* scales, and zero points are preserved exactly.
|
|
11311
11474
|
*/
|
|
11312
|
-
function
|
|
11313
|
-
|
|
11314
|
-
const
|
|
11315
|
-
const
|
|
11316
|
-
|
|
11317
|
-
const
|
|
11318
|
-
const
|
|
11319
|
-
const
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11475
|
+
function repackGPTQ(gptq) {
|
|
11476
|
+
const { qweight, scales: gptqScales, qzeros, K, N, groupSize } = gptq;
|
|
11477
|
+
const totalElements = N * K;
|
|
11478
|
+
const groupsPerCol = Math.ceil(K / groupSize);
|
|
11479
|
+
const totalGroups = N * groupsPerCol;
|
|
11480
|
+
const packed = new Uint32Array(Math.ceil(totalElements / 8));
|
|
11481
|
+
const scales = new Float32Array(totalGroups);
|
|
11482
|
+
const zeros = new Float32Array(totalGroups);
|
|
11483
|
+
for (let k = 0; k < K; k++) {
|
|
11484
|
+
const gptqRow = k >>> 3;
|
|
11485
|
+
const gptqNibblePos = k & 7;
|
|
11486
|
+
const gptqRowBase = gptqRow * N;
|
|
11487
|
+
for (let col = 0; col < N; col++) {
|
|
11488
|
+
const nibble = qweight[gptqRowBase + col] >>> gptqNibblePos * 4 & 15;
|
|
11489
|
+
const flatIdx = col * K + k;
|
|
11490
|
+
const packedIdx = flatIdx >>> 3;
|
|
11491
|
+
const nibblePos = flatIdx & 7;
|
|
11492
|
+
packed[packedIdx] |= nibble << nibblePos * 4;
|
|
11493
|
+
}
|
|
11494
|
+
}
|
|
11495
|
+
for (let g = 0; g < groupsPerCol; g++) {
|
|
11496
|
+
const srcRow = g * N;
|
|
11497
|
+
for (let col = 0; col < N; col++) scales[col * groupsPerCol + g] = gptqScales[srcRow + col];
|
|
11498
|
+
}
|
|
11499
|
+
const zerosNDiv8 = N >>> 3;
|
|
11500
|
+
for (let g = 0; g < groupsPerCol; g++) {
|
|
11501
|
+
const srcRow = g * zerosNDiv8;
|
|
11502
|
+
for (let col = 0; col < N; col++) {
|
|
11503
|
+
const zeroVal = qzeros[srcRow + (col >>> 3)] >>> (col & 7) * 4 & 15;
|
|
11504
|
+
zeros[col * groupsPerCol + g] = zeroVal + 1;
|
|
11327
11505
|
}
|
|
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
11506
|
}
|
|
11337
|
-
entries.sort((a, b) => a.dataOffset - b.dataOffset);
|
|
11338
11507
|
return {
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
metadata
|
|
11508
|
+
packed,
|
|
11509
|
+
scales,
|
|
11510
|
+
zeros
|
|
11343
11511
|
};
|
|
11344
11512
|
}
|
|
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
|
-
}
|
|
11513
|
+
|
|
11514
|
+
//#endregion
|
|
11515
|
+
//#region src/gpu/idb-cache.ts
|
|
11364
11516
|
/**
|
|
11365
|
-
*
|
|
11517
|
+
* IndexedDB-backed blob cache for model weights.
|
|
11366
11518
|
*
|
|
11367
|
-
*
|
|
11368
|
-
*
|
|
11369
|
-
*
|
|
11519
|
+
* Why IndexedDB and not the Cache API / OPFS we hand-rolled before:
|
|
11520
|
+
* - The Cache API ignores URL fragments in `match()`, which silently collapsed
|
|
11521
|
+
* our per-tensor `${url}#t:start-len` keys onto one entry → wrong bytes → the
|
|
11522
|
+
* `RRRR` garbage and `wt.data` crashes. IDB keys on the exact string.
|
|
11523
|
+
* - OPFS via Worker (`createSyncAccessHandle`) is finicky on iOS Safari (OOM /
|
|
11524
|
+
* transient failures) and added a lot of moving parts.
|
|
11525
|
+
* IndexedDB is the boring, proven large-binary store every browser supports. It
|
|
11526
|
+
* keys on arbitrary strings (no fragment footgun), stores ArrayBuffers directly,
|
|
11527
|
+
* and is durable (subject to the same per-origin quota as Cache/OPFS — install to
|
|
11528
|
+
* Home Screen lifts that ceiling; IDB doesn't change it, it just makes caching
|
|
11529
|
+
* actually CORRECT).
|
|
11530
|
+
*
|
|
11531
|
+
* Inert on Node (no indexedDB) — every export resolves to a miss/no-op so the
|
|
11532
|
+
* heap load path is unchanged off the browser.
|
|
11370
11533
|
*/
|
|
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
|
-
}
|
|
11534
|
+
const DB_NAME = "gerbil-weights-v1";
|
|
11535
|
+
const STORE = "tensors";
|
|
11536
|
+
function idbAvailable() {
|
|
11537
|
+
return typeof indexedDB !== "undefined";
|
|
11391
11538
|
}
|
|
11392
11539
|
/**
|
|
11393
|
-
*
|
|
11394
|
-
*
|
|
11540
|
+
* Ask the browser to mark this origin's storage as persistent (exempt from
|
|
11541
|
+
* eviction). On iOS Safari a plain tab's storage is evicted under pressure unless
|
|
11542
|
+
* persistence is granted (effectively only inside an installed PWA), which is the
|
|
11543
|
+
* primary reason a 404 MB model re-downloads on some reloads. We request it
|
|
11544
|
+
* up-front on the FIRST cache touch (read OR write) — not only on the first write
|
|
11545
|
+
* — so a warm-cache reload re-asserts the request before the browser has a chance
|
|
11546
|
+
* to evict mid-session. Best-effort and memoized: a single, idempotent call per
|
|
11547
|
+
* page that never throws and never blocks the load. Returns the resulting
|
|
11548
|
+
* persisted state (false when unsupported / not granted).
|
|
11395
11549
|
*/
|
|
11396
|
-
|
|
11397
|
-
|
|
11550
|
+
let _persistRequested$1 = null;
|
|
11551
|
+
function idbRequestPersistence() {
|
|
11552
|
+
if (_persistRequested$1) return _persistRequested$1;
|
|
11553
|
+
_persistRequested$1 = (async () => {
|
|
11554
|
+
try {
|
|
11555
|
+
if (typeof navigator === "undefined") return false;
|
|
11556
|
+
const storage = navigator?.storage;
|
|
11557
|
+
if (!storage) return false;
|
|
11558
|
+
if (await storage.persisted?.() === true) return true;
|
|
11559
|
+
return await storage.persist?.() ?? false;
|
|
11560
|
+
} catch {
|
|
11561
|
+
return false;
|
|
11562
|
+
}
|
|
11563
|
+
})();
|
|
11564
|
+
return _persistRequested$1;
|
|
11398
11565
|
}
|
|
11566
|
+
let _dbPromise = null;
|
|
11399
11567
|
/**
|
|
11400
|
-
*
|
|
11401
|
-
*
|
|
11568
|
+
* Per-op watchdog. iOS Safari can silently WEDGE an IndexedDB transaction after
|
|
11569
|
+
* a large (~400 MB) write session or under quota pressure — it fires NONE of
|
|
11570
|
+
* success/error/abort, so an awaiting caller hangs forever. That presents as the
|
|
11571
|
+
* load freezing at "reading model.safetensors header" (the first storage op
|
|
11572
|
+
* after that label) and is the #1 cause of the on-device load hang. Every op
|
|
11573
|
+
* races this timeout and degrades to its fallback (read → miss → network; write
|
|
11574
|
+
* → false → heap) instead of hanging.
|
|
11402
11575
|
*/
|
|
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
|
-
}
|
|
11576
|
+
const IDB_OP_TIMEOUT_MS = 12e3;
|
|
11410
11577
|
/**
|
|
11411
|
-
*
|
|
11578
|
+
* Drop the memoized connection (and close it) so the NEXT op re-opens a fresh
|
|
11579
|
+
* one. A wedged/poisoned connection otherwise persists for the page's lifetime —
|
|
11580
|
+
* an in-page retry (no reload) reuses it and hangs again. Closing also aborts
|
|
11581
|
+
* the wedged transaction.
|
|
11412
11582
|
*/
|
|
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) {
|
|
11583
|
+
function poisonConnection(db) {
|
|
11584
|
+
_dbPromise = null;
|
|
11445
11585
|
try {
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11586
|
+
db?.close();
|
|
11587
|
+
} catch {}
|
|
11588
|
+
}
|
|
11589
|
+
function openDB() {
|
|
11590
|
+
if (_dbPromise) return _dbPromise;
|
|
11591
|
+
_dbPromise = new Promise((resolve) => {
|
|
11592
|
+
if (!idbAvailable()) {
|
|
11593
|
+
resolve(null);
|
|
11594
|
+
return;
|
|
11595
|
+
}
|
|
11596
|
+
idbRequestPersistence();
|
|
11597
|
+
try {
|
|
11598
|
+
const req = indexedDB.open(DB_NAME, 1);
|
|
11599
|
+
req.onupgradeneeded = () => {
|
|
11600
|
+
const db = req.result;
|
|
11601
|
+
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
|
|
11602
|
+
};
|
|
11603
|
+
req.onsuccess = () => {
|
|
11604
|
+
const db = req.result;
|
|
11605
|
+
db.onversionchange = () => poisonConnection(db);
|
|
11606
|
+
db.onclose = () => {
|
|
11607
|
+
_dbPromise = null;
|
|
11608
|
+
};
|
|
11609
|
+
resolve(db);
|
|
11610
|
+
};
|
|
11611
|
+
req.onerror = () => resolve(null);
|
|
11612
|
+
req.onblocked = () => resolve(null);
|
|
11613
|
+
} catch {
|
|
11614
|
+
resolve(null);
|
|
11615
|
+
}
|
|
11616
|
+
});
|
|
11617
|
+
return _dbPromise;
|
|
11454
11618
|
}
|
|
11455
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
11619
|
+
/** A single transaction operation, promisified. Resolves the fallback on any
|
|
11620
|
+
* error OR if the op wedges past IDB_OP_TIMEOUT_MS (and then poisons the
|
|
11621
|
+
* connection so the next op re-opens fresh). */
|
|
11622
|
+
function tx(mode, run, fallback) {
|
|
11623
|
+
return openDB().then((db) => new Promise((resolve) => {
|
|
11624
|
+
if (!db) {
|
|
11625
|
+
resolve(fallback);
|
|
11626
|
+
return;
|
|
11627
|
+
}
|
|
11628
|
+
let settled = false;
|
|
11629
|
+
const finish = (value, poisoned) => {
|
|
11630
|
+
if (settled) return;
|
|
11631
|
+
settled = true;
|
|
11632
|
+
clearTimeout(timer);
|
|
11633
|
+
if (poisoned) poisonConnection(db);
|
|
11634
|
+
resolve(value);
|
|
11635
|
+
};
|
|
11636
|
+
const timer = setTimeout(() => finish(fallback, true), IDB_OP_TIMEOUT_MS);
|
|
11637
|
+
try {
|
|
11638
|
+
const t = db.transaction(STORE, mode);
|
|
11639
|
+
const req = run(t.objectStore(STORE));
|
|
11640
|
+
req.onsuccess = () => finish(req.result ?? fallback, false);
|
|
11641
|
+
req.onerror = () => finish(fallback, true);
|
|
11642
|
+
t.onabort = () => finish(fallback, true);
|
|
11643
|
+
} catch {
|
|
11644
|
+
finish(fallback, true);
|
|
11645
|
+
}
|
|
11646
|
+
}));
|
|
11462
11647
|
}
|
|
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
|
-
}
|
|
11648
|
+
/** Read a key's bytes, or null on miss / no IDB / any error. */
|
|
11649
|
+
async function idbGet(key) {
|
|
11650
|
+
const v = await tx("readonly", (s) => s.get(key), null);
|
|
11651
|
+
return v instanceof ArrayBuffer ? v : null;
|
|
11472
11652
|
}
|
|
11473
11653
|
/**
|
|
11474
|
-
*
|
|
11475
|
-
*
|
|
11654
|
+
* Size-verified read. Returns the bytes ONLY if the cached entry is at least
|
|
11655
|
+
* `minBytes` long; otherwise treats it as a corrupt/partial entry: deletes the
|
|
11656
|
+
* poisoned key (so a future load re-downloads it instead of trusting it again)
|
|
11657
|
+
* and returns null. This is the integrity gate that turns a half-written or
|
|
11658
|
+
* partially-evicted tensor into a clean cache-miss rather than wrong bytes (or a
|
|
11659
|
+
* later "No GPU buffer for tensor …" crash when get() returns undefined). Pass
|
|
11660
|
+
* minBytes <= 0 to skip the size check (behaves like idbGet).
|
|
11476
11661
|
*/
|
|
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
|
-
});
|
|
11662
|
+
async function idbGetVerified(key, minBytes) {
|
|
11663
|
+
const buf = await idbGet(key);
|
|
11664
|
+
if (!buf) return null;
|
|
11665
|
+
if (minBytes > 0 && buf.byteLength < minBytes) {
|
|
11666
|
+
await idbDelete(key);
|
|
11667
|
+
return null;
|
|
11493
11668
|
}
|
|
11494
|
-
|
|
11495
|
-
return {
|
|
11496
|
-
config,
|
|
11497
|
-
tensors,
|
|
11498
|
-
label: spec
|
|
11499
|
-
};
|
|
11669
|
+
return buf;
|
|
11500
11670
|
}
|
|
11501
11671
|
/**
|
|
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.
|
|
11672
|
+
* All cached keys, as a Set, in ONE transaction. Probing presence per-tensor
|
|
11673
|
+
* (hundreds of parallel getKey transactions) storms Safari's IndexedDB and hangs
|
|
11674
|
+
* the load; getAllKeys() does it in a single transaction so callers can check
|
|
11675
|
+
* membership in memory. Empty set on miss / no IDB / error.
|
|
11511
11676
|
*/
|
|
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`);
|
|
11677
|
+
async function idbAllKeys() {
|
|
11678
|
+
const keys = await tx("readonly", (s) => s.getAllKeys(), []);
|
|
11679
|
+
return new Set((keys ?? []).map((k) => String(k)));
|
|
11517
11680
|
}
|
|
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;
|
|
11681
|
+
/** Write a key's bytes. Returns true on success, false on quota/error (caller
|
|
11682
|
+
* falls back to network — never throws). */
|
|
11683
|
+
async function idbPut(key, buf) {
|
|
11684
|
+
return await tx("readwrite", (s) => s.put(buf, key), null) !== null;
|
|
11528
11685
|
}
|
|
11529
11686
|
/**
|
|
11530
|
-
*
|
|
11531
|
-
*
|
|
11687
|
+
* Atomic, verified write. Writes the bytes, then reads back the stored length and
|
|
11688
|
+
* confirms it matches what we wrote. iOS Safari can let a put() transaction
|
|
11689
|
+
* "succeed" yet store a truncated value under quota pressure (or wedge the
|
|
11690
|
+
* transaction, which the watchdog turns into a fallback) — leaving a present-but-
|
|
11691
|
+
* short entry that the size-blind presence probe would later trust. On any
|
|
11692
|
+
* mismatch we DELETE the key so it is never read back as a valid (wrong-size)
|
|
11693
|
+
* tensor, and return false so the caller keeps the network bytes. Returns true
|
|
11694
|
+
* only when the full-length value is provably persisted.
|
|
11532
11695
|
*/
|
|
11533
|
-
function
|
|
11534
|
-
const
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
stems.add(name.slice(0, name.indexOf(marker)));
|
|
11696
|
+
async function idbPutVerified(key, buf) {
|
|
11697
|
+
const expected = buf.byteLength;
|
|
11698
|
+
if (!await idbPut(key, buf)) {
|
|
11699
|
+
await idbDelete(key);
|
|
11700
|
+
return false;
|
|
11539
11701
|
}
|
|
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
|
-
});
|
|
11702
|
+
const back = await idbGet(key);
|
|
11703
|
+
if (!back || back.byteLength !== expected) {
|
|
11704
|
+
await idbDelete(key);
|
|
11705
|
+
return false;
|
|
11567
11706
|
}
|
|
11568
|
-
return
|
|
11707
|
+
return true;
|
|
11569
11708
|
}
|
|
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
|
-
};
|
|
11709
|
+
/** Delete a key (best-effort). */
|
|
11710
|
+
async function idbDelete(key) {
|
|
11711
|
+
await tx("readwrite", (s) => s.delete(key), null);
|
|
11712
|
+
}
|
|
11713
|
+
/** True when IndexedDB is usable in this environment (browser, not Node). */
|
|
11714
|
+
function idbCacheAvailable() {
|
|
11715
|
+
return idbAvailable();
|
|
11626
11716
|
}
|
|
11627
11717
|
|
|
11628
11718
|
//#endregion
|
|
@@ -14836,5 +14926,5 @@ function selectGraphWeights(graph, weights) {
|
|
|
14836
14926
|
}
|
|
14837
14927
|
|
|
14838
14928
|
//#endregion
|
|
14839
|
-
export {
|
|
14840
|
-
//# sourceMappingURL=moonshine-stt-
|
|
14929
|
+
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 };
|
|
14930
|
+
//# sourceMappingURL=moonshine-stt-BI-5UmD_.mjs.map
|