diskformer 0.5.4 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +6 -5
- package/engines/NOTICE +3 -2
- package/engines/flashmap_drafter.js +697 -0
- package/engines/flint_moe_ssd.js +487 -0
- package/engines/gemma-4-e2b.js +5677 -0
- package/engines/hy_v3_moe_ssd.js +198 -0
- package/engines/iq_quants.js +511 -0
- package/engines/lfm2_5.js +5942 -0
- package/engines/lfm2_moe_ssd.js +708 -0
- package/engines/ling_moe_ssd.js +903 -0
- package/engines/ple-opfs.js +108 -0
- package/engines/qwen35_dense.js +836 -0
- package/engines/qwen3_moe_ssd.js +2 -0
- package/engines/ternary_bonsai_2_27b.js +14941 -0
- package/engines/ternary_bonsai_2_dflash.js +936 -0
- package/engines/ternary_bonsai_2_mtp.js +643 -0
- package/ggml/dist/BUILD.md +1 -1
- package/ggml/dist/df-chat.wasm +0 -0
- package/ggml/llama.js +4 -3
- package/ggml/worker.mjs +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* hy_v3_moe_ssd.js — Tencent Hy-Memory-30B-A3B (GGUF arch `hy_v3`) with its routed experts on disk: rung 2f of the
|
|
2
|
+
* SSD-streaming ladder. Hy-Memory is a long-term-memory writer (it extracts and updates memory entries; arXiv
|
|
3
|
+
* 2610.22196); as an engine it is a plain GQA transformer with a DeepSeek-style router, so it reuses the LFM2.5 rung
|
|
4
|
+
* (lfm2_moe_ssd.js: sigmoid router + selection bias, dense leading layers run inside MoE layer 0's trunk pass,
|
|
5
|
+
* Q4_0 / Q6_K kernels) with every layer an attention layer.
|
|
6
|
+
*
|
|
7
|
+
* The math follows llama.cpp src/models/hy-v3.cpp. 48 layers, each:
|
|
8
|
+
* x += attn(rms_norm(x) · attn_norm): Q/K/V (32 / 4 / 4 heads × 128; Q is 4096 wide, wider than the 2048 hidden),
|
|
9
|
+
* per-head weighted RMS norm on Q and K, NeoX RoPE (base 11158840, all 128 dims), softmax(QKᵀ/√128) V, output
|
|
10
|
+
* projection 4096 → 2048.
|
|
11
|
+
* x += ffn(rms_norm(x) · ffn_norm): a dense SwiGLU (6912) on layer 0; on layers 1–47 a MoE of 128 experts, top 8 by
|
|
12
|
+
* sigmoid(router) + exp_probs_b (the bias only picks), weights = the picked sigmoids over their sum × 2.826
|
|
13
|
+
* (expert_weights_scale), SwiGLU experts (1536), plus one always-on shared SwiGLU expert (1536, no gate).
|
|
14
|
+
* Embedding: Q8_0 rows (no scale); head: rms_norm(x) · output_norm, then output.weight (Q8_0, untied).
|
|
15
|
+
*
|
|
16
|
+
* File: the community Q8_0 GGUF (rapatsky/Hy-Memory-30B-A3B-GGUF): every matrix, the experts and both vocab tables
|
|
17
|
+
* Q8_0; norms, router and router bias F32. The LFM2.5 rung's graph runs on the Q8_0 kernels of the Qwen rungs.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Qwen3MoeSsd, BpeTokenizer } from './qwen3_moe_ssd.js';
|
|
21
|
+
import { QWEN35_BATCH_KERNELS } from './qwen35_moe_ssd.js';
|
|
22
|
+
import { Gemma4MoeSsd } from './gemma4_moe_ssd.js';
|
|
23
|
+
import { Lfm2MoeSsd, planLayoutLfm2Moe, planUnitsLfm2Moe } from './lfm2_moe_ssd.js';
|
|
24
|
+
|
|
25
|
+
export function configFromGgufHyV3(kv) {
|
|
26
|
+
const a = kv['general.architecture'];
|
|
27
|
+
if (a !== 'hy_v3') throw new Error(`expected an hy_v3 GGUF, got ${a}`);
|
|
28
|
+
const g = (k) => kv[`${a}.${k}`];
|
|
29
|
+
// llama.cpp defaults hy_v3 to the sigmoid router when the key is absent (hy-v3.cpp load_arch_hparams).
|
|
30
|
+
if ((g('expert_gating_func') ?? 2) !== 2) throw new Error(`hy_v3 expert_gating_func ${g('expert_gating_func')}: this engine implements sigmoid (2)`);
|
|
31
|
+
if (g('expert_weights_norm') === false) throw new Error('hy_v3 without expert_weights_norm: not implemented');
|
|
32
|
+
if (g('nextn_predict_layers')) throw new Error('hy_v3 GGUF with MTP layers: convert with --no-mtp');
|
|
33
|
+
const modelLayers = g('block_count'), lead = g('leading_dense_block_count') ?? 1, layers = modelLayers - lead;
|
|
34
|
+
const H = g('embedding_length'), heads = g('attention.head_count'), kvH = g('attention.head_count_kv');
|
|
35
|
+
const hd = g('attention.key_length') || H / heads, theta = g('rope.freq_base');
|
|
36
|
+
if (g('rope.dimension_count') && g('rope.dimension_count') !== hd) throw new Error('hy_v3 partial RoPE: not implemented');
|
|
37
|
+
return {
|
|
38
|
+
arch: a, modelLayers, lead, layers,
|
|
39
|
+
attn: Array(modelLayers).fill(true),
|
|
40
|
+
hidden: H, denseFf: g('feed_forward_length'), heads, kvHeadsAttn: kvH, attnHeadDim: hd,
|
|
41
|
+
experts: g('expert_count'), topK: g('expert_used_count'), expertFf: g('expert_feed_forward_length'),
|
|
42
|
+
shexpFf: g('expert_shared_feed_forward_length'), expScale: g('expert_weights_scale') ?? 1,
|
|
43
|
+
eps: g('attention.layer_norm_rms_epsilon'), contextLength: g('context_length'),
|
|
44
|
+
vocab: kv['tokenizer.ggml.tokens'].length,
|
|
45
|
+
bos: kv['tokenizer.ggml.bos_token_id'], eos: kv['tokenizer.ggml.eos_token_id'],
|
|
46
|
+
swa: Array(layers).fill(false), headDim: Array(layers).fill(hd), ropeTheta: Array(layers).fill(theta), softcap: 0,
|
|
47
|
+
kvHeads: Array(layers).fill(kvH),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The LFM2.5 store shape. The GGUF has no leading_dense_block_count key, so the layout checks the default (1) against
|
|
52
|
+
// the tensors: dense layers have ffn_gate.weight, MoE layers ffn_gate_inp.weight.
|
|
53
|
+
export function planLayoutHyV3(gguf) {
|
|
54
|
+
const cfg = configFromGgufHyV3(gguf.kv), names = new Set(gguf.tensors.map((t) => t.name));
|
|
55
|
+
let lead = 0;
|
|
56
|
+
while (names.has(`blk.${lead}.ffn_gate.weight`)) lead++;
|
|
57
|
+
if (lead !== cfg.lead || !names.has(`blk.${lead}.ffn_gate_inp.weight`)) throw new Error(`hy_v3: ${lead} dense leading layers, expected ${cfg.lead}`);
|
|
58
|
+
return planLayoutLfm2Moe(gguf, cfg);
|
|
59
|
+
}
|
|
60
|
+
export const HYV3_INGEST_PLAN = { layout: planLayoutHyV3, units: planUnitsLfm2Moe };
|
|
61
|
+
|
|
62
|
+
// The community Q8_0 GGUF (rapatsky; its card: converted from Tencent's release with the expert width corrected to 768).
|
|
63
|
+
// Q8_0 chosen by Chirag 2026-10-17 over the 4-bit files for quality (our Q4_0 measured KLD 0.094 vs bf16).
|
|
64
|
+
export const HY_MEMORY_30B_A3B = {
|
|
65
|
+
repo: 'rapatsky/Hy-Memory-30B-A3B-GGUF',
|
|
66
|
+
file: 'Hy-Memory-30B-A3B-Q8_0.gguf',
|
|
67
|
+
revision: '8674f68e9f60b9ec237c37130244ecee6baf3364',
|
|
68
|
+
sha256: 'b5375575b238d1f03eace82a092b2e3c95e31ad336337f72a20d985f208821bf',
|
|
69
|
+
size: 31985729056,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ── Tokenizer: byte-level BPE; tokenizer.json's pre-tokenizer is a Sequence of three Isolated splits (llama.cpp
|
|
73
|
+
// LLAMA_VOCAB_PRE_TYPE_HY_V4 has the same three regexes): digit runs of up to 3, CJK / kana runs, then the word split.
|
|
74
|
+
// Each split keeps both the matches and the text between them, and the next split runs over every piece.
|
|
75
|
+
const HY_SPLITS = [
|
|
76
|
+
/\p{N}{1,3}/gu,
|
|
77
|
+
/[一-龥-ゟ゠-ヿ]+/gu,
|
|
78
|
+
/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~][A-Za-z]+|[^\r\n\p{L}\p{P}\p{S}]?[\p{L}\p{M}]+| ?[\p{P}\p{S}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+/gu,
|
|
79
|
+
];
|
|
80
|
+
export function isolate(pieces, re) {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const s of pieces) {
|
|
83
|
+
let last = 0;
|
|
84
|
+
for (const m of s.matchAll(re)) {
|
|
85
|
+
if (m.index > last) out.push(s.slice(last, m.index));
|
|
86
|
+
if (m[0]) out.push(m[0]);
|
|
87
|
+
last = m.index + m[0].length;
|
|
88
|
+
}
|
|
89
|
+
if (last < s.length) out.push(s.slice(last));
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
export class HyV3Tokenizer extends BpeTokenizer {
|
|
94
|
+
constructor(kv) {
|
|
95
|
+
super(kv);
|
|
96
|
+
// preRe is matched with matchAll over each special-free piece; this one yields the three-split pieces.
|
|
97
|
+
this.preRe = { [Symbol.matchAll]: (s) => HY_SPLITS.reduce(isolate, [s]).map((p) => [p])[Symbol.iterator]() };
|
|
98
|
+
}
|
|
99
|
+
// The vocab lacks some single bytes (\r = "č"). tokenizers' BPE (no unk token) drops such a character before merging,
|
|
100
|
+
// so "\r\n\r\n" is "ĊĊ"; do the same.
|
|
101
|
+
bpe(word) {
|
|
102
|
+
try { return super.bpe(word); } catch {
|
|
103
|
+
let parts = Array.from(word).filter((ch) => this.ids.has(ch));
|
|
104
|
+
while (parts.length > 1) {
|
|
105
|
+
let best = -1, bestRank = Infinity;
|
|
106
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
107
|
+
const r = this.ranks.get(parts[i] + ' ' + parts[i + 1]);
|
|
108
|
+
if (r !== undefined && r < bestRank) { bestRank = r; best = i; }
|
|
109
|
+
}
|
|
110
|
+
if (best < 0) break;
|
|
111
|
+
parts = [...parts.slice(0, best), parts[best] + parts[best + 1], ...parts.slice(best + 2)];
|
|
112
|
+
}
|
|
113
|
+
return parts.map((p) => this.ids.get(p)).filter((id) => id !== undefined);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The chat template (tokenizer chat_template.jinja), text turns, no tools: BOS, the system text raw, the reasoning
|
|
119
|
+
// mode marker, then <|hy_User|>… and <|hy_Assistant|><think></think>…<eos> per turn. Thinking off is the template's
|
|
120
|
+
// default `no_think` (the generation prompt closes an empty think block); thinking on uses `low` (it opens one).
|
|
121
|
+
export function hyChatPrompt(messages, { enableThinking = false } = {}) {
|
|
122
|
+
const effort = enableThinking ? 'low' : 'no_think';
|
|
123
|
+
let sys = '';
|
|
124
|
+
for (const m of messages) if (m.role === 'system') sys += String(m.content ?? '');
|
|
125
|
+
let s = '<|hy_begin▁of▁sentence|>' + sys + '<|reasoning_mode|>reasoning_effort:' + effort;
|
|
126
|
+
const turns = messages.filter((m) => m.role === 'user' || m.role === 'assistant');
|
|
127
|
+
turns.forEach((m, i) => {
|
|
128
|
+
let c = String(m.content ?? '');
|
|
129
|
+
if (m.role === 'user') { s += '<|hy_User|>' + c; return; }
|
|
130
|
+
// Earlier replies lose their reasoning: the model's own </think> and LocalMind's <|think|> … <|/think|> markers.
|
|
131
|
+
c = c.replace(/<\|think\|>[\s\S]*?<\|\/think\|>/g, '');
|
|
132
|
+
if (c.includes('</think>')) c = c.split('</think>').pop();
|
|
133
|
+
s += '<|hy_Assistant|><think></think>' + c.trim() + (i < turns.length - 1 ? '<eos:6124c78e>' : '');
|
|
134
|
+
});
|
|
135
|
+
if (turns.length && turns[turns.length - 1].role === 'assistant') return s;
|
|
136
|
+
return s + '<|hy_Assistant|><think>' + (enableThinking ? '' : '</think>');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Engine ──────────────────────────────────────────────────────────────────
|
|
140
|
+
// Expert prefetch off by default. Measured 2026-10-17 on the Q8_0 file (M4 Pro, internal disk, ~400-token prompt, 256
|
|
141
|
+
// tokens warm): 4 GB pool (856 slots) → prefill 21.0 tok/s and decode 4.76 with prefetch off, 17.7 / 4.69 at lookahead 1,
|
|
142
|
+
// 15.1 / 4.42 at 2; 8 GB pool (1,365 slots, the binding cap) at lookahead 1 → 16.1 / 4.99. A memory write is mostly
|
|
143
|
+
// prompt reading, so prefill decides (plan/2026-10-16-hymemory-harness/h-bench-q8.js, Seagate 2026-10-17-hymemory-q8-gate).
|
|
144
|
+
export class HyV3MoeSsd extends Lfm2MoeSsd {
|
|
145
|
+
static configFrom(kv) { return configFromGgufHyV3(kv); }
|
|
146
|
+
static get ingestPlan() { return HYV3_INGEST_PLAN; }
|
|
147
|
+
static tokenizerFrom(kv) { return new HyV3Tokenizer(kv); }
|
|
148
|
+
static load(modelId, opts = {}) { return Qwen3MoeSsd.load.call(this, modelId, { ...opts, source: opts.source || HY_MEMORY_30B_A3B }); }
|
|
149
|
+
|
|
150
|
+
constructor(device, manifest, gguf, opts) {
|
|
151
|
+
super(device, manifest, gguf, opts);
|
|
152
|
+
this.prefetch = opts.prefetch ?? false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
chatPrompt(messages, opts) { return hyChatPrompt(messages, opts); }
|
|
156
|
+
get stopTokenIds() {
|
|
157
|
+
const t = this.tokenizer.ids;
|
|
158
|
+
return [this.cfg.eos, t.get('<eos:6124c78e>'), t.get('<|hy_end▁of▁sentence|>'), t.get('<|hy_EOT|>')].filter((x) => x !== undefined);
|
|
159
|
+
}
|
|
160
|
+
get thinkOpenTokenId() { return this.tokenizer.ids.get('<think>') ?? null; }
|
|
161
|
+
get thinkCloseTokenId() { return this.tokenizer.ids.get('</think>') ?? null; }
|
|
162
|
+
get thinkInPrompt() { return true; }
|
|
163
|
+
get alwaysThinks() { return false; }
|
|
164
|
+
get thinkPreamble() { return ''; }
|
|
165
|
+
|
|
166
|
+
// Q8_0 throughout (the community Q8_0 GGUF): matrices and experts through the Q8_0 kernels, the token table and the
|
|
167
|
+
// untied head as plain Q8_0 planes (no Q6_K split).
|
|
168
|
+
get kq() { return { mm: 'matmulQ8', mmT: 'matmulQ8T', ex: 'expertQ8', exB: 'expertQ8B' }; }
|
|
169
|
+
get embedKernel() { return 'embedQ8'; }
|
|
170
|
+
get embedKernelB() { return 'embedQ8B'; }
|
|
171
|
+
get lmKernel() { return 'matmulQ8'; }
|
|
172
|
+
get kernels() {
|
|
173
|
+
return (this._hyKernels ||= {
|
|
174
|
+
...super.kernels,
|
|
175
|
+
...(this.batchPrefill ? Object.fromEntries(['matmulQ8T', 'expertQ8B', 'embedQ8B'].map((k) => [k, QWEN35_BATCH_KERNELS[k]])) : {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async uploadDense(onProgress) {
|
|
179
|
+
const dt = this.manifest.dense.tensors;
|
|
180
|
+
for (const [name, d] of Object.entries(dt)) if (d.type !== 'f32' && d.type !== 'q8') throw new Error(`${name}: ${d.type} in a Q8_0 hy_v3 file`);
|
|
181
|
+
await Qwen3MoeSsd.prototype.uploadDense.call(this, onProgress);
|
|
182
|
+
this.ropeFreqs = null;
|
|
183
|
+
}
|
|
184
|
+
initBuffers() {
|
|
185
|
+
super.initBuffers();
|
|
186
|
+
const W = this.w, H = this.cfg.hidden;
|
|
187
|
+
this.g.embed = this.bind('embedQ8', [W['token_embd.weight'].q, W['token_embd.weight'].s, this.a.x, this.tok, this.uniform([H])]);
|
|
188
|
+
this.g.lm = this.bind('matmulQ8', [this.a.xn, W['output.weight'].q, W['output.weight'].s, this.a.logits, this.uniform([this.cfg.vocab, H])]);
|
|
189
|
+
}
|
|
190
|
+
initBatch() {
|
|
191
|
+
super.initBatch();
|
|
192
|
+
const W = this.w;
|
|
193
|
+
this.gB.embed = this.bind('embedQ8B', [W['token_embd.weight'].q, W['token_embd.weight'].s, this.b.ids, this.b.x, this.uniform([this.cfg.hidden]), this.qb]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// No recurrent state: thinking follows the request (LFM2.5 forces it on).
|
|
197
|
+
async *generateTurn(messages, opts = {}) { yield* Gemma4MoeSsd.prototype.generateTurn.call(this, messages, opts); }
|
|
198
|
+
}
|