mlx-bun 0.0.10 → 0.0.11
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/README.md +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +363 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1380 -135
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
|
@@ -15,12 +15,22 @@ import * as ops from "../../mlx/ops";
|
|
|
15
15
|
import { Gemma4Model } from "../../model/gemma4";
|
|
16
16
|
import type { DflashDrafter } from "./module-dflash";
|
|
17
17
|
import { KeyStream, processLogits, probsOf, probAtToken, sampleToken, sampleResidual, type DSparkSampleConfig } from "./sample";
|
|
18
|
+
import type { ConfSample } from "./calibration";
|
|
18
19
|
|
|
19
20
|
export interface DflashGenOptions {
|
|
20
21
|
gamma?: number;
|
|
21
22
|
maxTokens?: number;
|
|
22
23
|
eosTokenIds?: number[];
|
|
23
24
|
sample?: DSparkSampleConfig;
|
|
25
|
+
/** Confidence-scheduled draft-length pruning (Alg 1). Defaults to the
|
|
26
|
+
* checkpoint's STS thresholds (drafter.cfg.sts) when calibrated; pass
|
|
27
|
+
* minConf for a uniform manual override; pass thresholds:[] to disable. */
|
|
28
|
+
thresholds?: number[];
|
|
29
|
+
minConf?: number;
|
|
30
|
+
/** Calibration hook (scripts/dspark-calibrate.ts): called once per draft
|
|
31
|
+
* round with that round's per-position (confidence, accepted) samples,
|
|
32
|
+
* AFTER kAccept is known. Not used by ordinary decode. */
|
|
33
|
+
onRound?: (samples: ConfSample[]) => void;
|
|
24
34
|
}
|
|
25
35
|
export interface DflashStats {
|
|
26
36
|
emitted: number; drafted: number; accepted: number; targetCalls: number;
|
|
@@ -32,19 +42,26 @@ export interface DflashResult { tokens: number[]; stats: DflashStats }
|
|
|
32
42
|
* hidden [1,L,H] AND the m-layer context [1,L,m*H] (tapLayers concatenated on
|
|
33
43
|
* the feature axis). */
|
|
34
44
|
function forwardTapped(model: Gemma4Model, ids: MlxArray, caches: import("../../model/gemma4").Cache[], tapLayers: number[]): { finalH: MlxArray; ctxML: MlxArray } {
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
const cap = new Map<number, MlxArray>();
|
|
46
|
+
model.hiddenTap = { layers: new Set(tapLayers), captured: cap };
|
|
47
|
+
let finalH: MlxArray | null = null;
|
|
37
48
|
let ctxML: MlxArray;
|
|
38
49
|
try {
|
|
39
50
|
finalH = model.forwardHidden(ids, caches);
|
|
40
|
-
const cap = model.hiddenTap.captured;
|
|
41
51
|
const perLayer = tapLayers.map((li) => { const a = cap.get(li); if (!a) throw new Error(`layer ${li} not captured`); return a; });
|
|
42
52
|
ctxML = ops.concatAxis(perLayer, 2); // [1,L,m*H]
|
|
43
53
|
for (const [, a] of cap) a.dispose();
|
|
54
|
+
cap.clear(); // consumed — the finally must not double-dispose
|
|
55
|
+
const out = { finalH, ctxML };
|
|
56
|
+
finalH = null; // ownership returned to the caller
|
|
57
|
+
return out;
|
|
44
58
|
} finally {
|
|
59
|
+
// Free partial captures + an orphaned finalH on the throw path (on success
|
|
60
|
+
// both are already gone).
|
|
61
|
+
finalH?.dispose();
|
|
62
|
+
for (const [, a] of cap) a.dispose();
|
|
45
63
|
model.hiddenTap = null;
|
|
46
64
|
}
|
|
47
|
-
return { finalH, ctxML };
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
function sampleFromHidden(model: Gemma4Model, hidden: MlxArray, pos: number, cfg: DSparkSampleConfig | null, keys: KeyStream): number {
|
|
@@ -90,14 +107,22 @@ function verifySampling(model: Gemma4Model, vHidden: MlxArray, draftLogits: MlxA
|
|
|
90
107
|
let verdict: Verdict | null = null;
|
|
91
108
|
for (let k = 0; k < gamma; k++) {
|
|
92
109
|
const x = drafts[k]!;
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
// processLogits does not consume its input — bind each sliced row and
|
|
111
|
+
// dispose it after (an inline slice(...) here leaked one [1,V] per position).
|
|
112
|
+
const tRow = slice(tl, k);
|
|
113
|
+
const ps = processLogits(tRow, cfg); tRow.dispose(); const P = probsOf(ps); ps.dispose();
|
|
114
|
+
const qRow = slice(draftLogits, k);
|
|
115
|
+
const qs = processLogits(qRow, cfg); qRow.dispose(); const Q = probsOf(qs); qs.dispose();
|
|
95
116
|
const pX = probAtToken(P, x), qX = probAtToken(Q, x);
|
|
96
117
|
if (u[k]! < Math.min(1, qX > 0 ? pX / qX : 1)) { P.dispose(); Q.dispose(); continue; }
|
|
97
118
|
const corr = sampleResidual(P, Q, keys.next()); P.dispose(); Q.dispose();
|
|
98
119
|
verdict = { kAccept: k, emit: corr }; break;
|
|
99
120
|
}
|
|
100
|
-
if (!verdict) {
|
|
121
|
+
if (!verdict) {
|
|
122
|
+
const bRow = slice(tl, gamma);
|
|
123
|
+
const bs = processLogits(bRow, cfg); bRow.dispose();
|
|
124
|
+
verdict = { kAccept: gamma, emit: sampleToken(bs, keys.next()) }; bs.dispose();
|
|
125
|
+
}
|
|
101
126
|
tl.dispose();
|
|
102
127
|
return verdict;
|
|
103
128
|
}
|
|
@@ -114,6 +139,12 @@ export function dflashGenerate(model: Gemma4Model, drafter: DflashDrafter, promp
|
|
|
114
139
|
const out: number[] = [];
|
|
115
140
|
let confSum = 0, rounds = 0, acceptLenSum = 0;
|
|
116
141
|
let H_ctx: MlxArray | null = null;
|
|
142
|
+
// Round-local tensors, hoisted so the finally frees them on a mid-round
|
|
143
|
+
// throw (verify, sampling, concat); each is nulled at its normal disposal
|
|
144
|
+
// — the serve-loop discipline (src/spec/serve-loop.ts).
|
|
145
|
+
let vHidden: MlxArray | null = null;
|
|
146
|
+
let vCtxML: MlxArray | null = null;
|
|
147
|
+
let blockLogits: MlxArray | null = null;
|
|
117
148
|
|
|
118
149
|
try {
|
|
119
150
|
// 1. prefill (tapped): seed H_ctx with the prompt's m-layer hiddens.
|
|
@@ -132,31 +163,49 @@ export function dflashGenerate(model: Gemma4Model, drafter: DflashDrafter, promp
|
|
|
132
163
|
out.push(next); stats.emitted++;
|
|
133
164
|
|
|
134
165
|
const tDecode = performance.now();
|
|
166
|
+
// Alg 1 thresholds: explicit option > checkpoint STS calibration > none.
|
|
167
|
+
const thresholds = options.thresholds ?? drafter.cfg.sts?.thresholds;
|
|
135
168
|
outer: while (stats.emitted < maxTokens) {
|
|
136
|
-
// 2a. draft against the current (growing) H_ctx.
|
|
137
|
-
|
|
169
|
+
// 2a. draft against the current (growing) H_ctx. The scheduler may prune
|
|
170
|
+
// the block short (d ≤ γ) — verify then covers exactly d positions.
|
|
171
|
+
const block = drafter.forwardInfer(model, H_ctx!, next, gamma, {
|
|
172
|
+
sample: sampleCfg ?? undefined, keys, thresholds, minConf: options.minConf,
|
|
173
|
+
});
|
|
138
174
|
const drafts = block.tokens;
|
|
139
|
-
|
|
175
|
+
blockLogits = block.draftLogits ?? null;
|
|
176
|
+
const d = drafts.length;
|
|
177
|
+
stats.drafted += d;
|
|
140
178
|
for (const c of block.conf) confSum += c;
|
|
141
179
|
|
|
142
180
|
// 2b. verify (tapped) — get accepted tokens' m-layer hiddens to append.
|
|
143
|
-
const vIds = ops.fromInt32([next, ...drafts], [1,
|
|
144
|
-
const
|
|
181
|
+
const vIds = ops.fromInt32([next, ...drafts], [1, d + 1]);
|
|
182
|
+
const ft = forwardTapped(model, vIds, caches, tapLayers);
|
|
183
|
+
vHidden = ft.finalH;
|
|
184
|
+
vCtxML = ft.ctxML;
|
|
145
185
|
vIds.dispose();
|
|
146
186
|
stats.targetCalls++;
|
|
147
187
|
|
|
148
188
|
const { kAccept, emit } = sampleCfg
|
|
149
|
-
? verifySampling(model, vHidden,
|
|
150
|
-
: verifyGreedy(model, vHidden, drafts,
|
|
151
|
-
|
|
189
|
+
? verifySampling(model, vHidden, blockLogits!, drafts, d, sampleCfg, keys)
|
|
190
|
+
: verifyGreedy(model, vHidden, drafts, d);
|
|
191
|
+
blockLogits?.dispose();
|
|
192
|
+
blockLogits = null;
|
|
152
193
|
stats.accepted += kAccept; rounds++; acceptLenSum += kAccept + 1;
|
|
153
194
|
|
|
195
|
+
// Calibration hook (scripts/dspark-calibrate.ts): report this round's
|
|
196
|
+
// per-position (confidence, accepted) outcomes now that kAccept is known.
|
|
197
|
+
if (options.onRound) {
|
|
198
|
+
const roundSamples: ConfSample[] = [];
|
|
199
|
+
for (let k = 0; k < drafts.length; k++) roundSamples.push({ pos: k, conf: block.conf[k]!, accepted: k < kAccept });
|
|
200
|
+
options.onRound(roundSamples);
|
|
201
|
+
}
|
|
202
|
+
|
|
154
203
|
// 2c. emit accepted drafts
|
|
155
204
|
for (let k = 0; k < kAccept; k++) {
|
|
156
205
|
const isEos = eosTokenIds.includes(drafts[k]!);
|
|
157
206
|
if (!isEos) out.push(drafts[k]!);
|
|
158
207
|
stats.emitted++;
|
|
159
|
-
if (isEos || stats.emitted >= maxTokens)
|
|
208
|
+
if (isEos || stats.emitted >= maxTokens) break outer; // finally frees vHidden/vCtxML
|
|
160
209
|
}
|
|
161
210
|
const emitIsEos = eosTokenIds.includes(emit);
|
|
162
211
|
if (!emitIsEos) out.push(emit);
|
|
@@ -164,21 +213,24 @@ export function dflashGenerate(model: Gemma4Model, drafter: DflashDrafter, promp
|
|
|
164
213
|
|
|
165
214
|
// 2d. append accepted stream's m-layer hiddens (anchor + kAccept drafts =
|
|
166
215
|
// vCtxML[:, 0..kAccept]) to H_ctx; never the rejected tips.
|
|
167
|
-
const add = vCtxML
|
|
216
|
+
const add = vCtxML!.slice([0, 0, 0], [1, kAccept + 1, vCtxML!.shape[2]!]);
|
|
168
217
|
const grown = ops.concatAxis([H_ctx!, add], 1);
|
|
169
|
-
H_ctx!.dispose(); add.dispose(); vCtxML
|
|
218
|
+
H_ctx!.dispose(); add.dispose(); vCtxML!.dispose(); vCtxML = null;
|
|
170
219
|
H_ctx = grown;
|
|
171
220
|
|
|
172
221
|
// 2e. roll back rejected KV tips (bypass trim, past-window safe)
|
|
173
|
-
if (kAccept <
|
|
222
|
+
if (kAccept < d) { const nRej = d - kAccept; for (const c of caches) c.trim(nRej, true); }
|
|
174
223
|
|
|
175
|
-
vHidden
|
|
224
|
+
vHidden!.dispose(); vHidden = null;
|
|
176
225
|
if (emitIsEos || stats.emitted >= maxTokens) break;
|
|
177
226
|
next = emit;
|
|
178
227
|
}
|
|
179
228
|
stats.decodeMs = performance.now() - tDecode;
|
|
180
229
|
} finally {
|
|
181
230
|
H_ctx?.dispose();
|
|
231
|
+
vHidden?.dispose();
|
|
232
|
+
vCtxML?.dispose();
|
|
233
|
+
blockLogits?.dispose();
|
|
182
234
|
for (const c of caches) c.dispose();
|
|
183
235
|
}
|
|
184
236
|
return finalize(stats, out, rounds, acceptLenSum, confSum);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Central DSpark checkpoint loader — the variant dispatch the modules never
|
|
2
|
+
// had (scripts imported their class directly; the serve path needs one entry
|
|
3
|
+
// point). Reads dspark.json and returns the matching drafter:
|
|
4
|
+
// variant "dspark" — the faithful KV-injection module (canonical)
|
|
5
|
+
// variant "dflash" (legacy) — same module, pre-rename stamp
|
|
6
|
+
// variant absent — a v1 single-vector checkpoint (module.ts,
|
|
7
|
+
// superseded research baseline) — refused for
|
|
8
|
+
// serving with a pointer at the v2 trainer.
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { DflashDrafter } from "./module-dflash";
|
|
13
|
+
|
|
14
|
+
export function loadDsparkDrafter(dir: string): DflashDrafter {
|
|
15
|
+
const metaPath = join(dir, "dspark.json");
|
|
16
|
+
if (!existsSync(metaPath)) throw new Error(`no dspark.json in ${dir} — not a DSpark checkpoint`);
|
|
17
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf8")) as { variant?: string };
|
|
18
|
+
if (meta.variant === "dspark" || meta.variant === "dflash") return DflashDrafter.load(dir);
|
|
19
|
+
throw new Error(
|
|
20
|
+
`${dir} is a v1 single-vector DSpark checkpoint (variant=${meta.variant ?? "none"}) — ` +
|
|
21
|
+
`superseded, not serveable; retrain with scripts/dspark-train-dflash.ts`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -23,6 +23,21 @@ import { processLogits, sampleToken, KeyStream, type DSparkSampleConfig } from "
|
|
|
23
23
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
|
|
26
|
+
/** STS calibration (§3.2.1) — per-position confidence thresholds fit on real
|
|
27
|
+
* (confidence, accepted) verify outcomes (src/spec/dspark/calibration.ts,
|
|
28
|
+
* scripts/dspark-calibrate.ts). Consumed by the confidence-scheduled
|
|
29
|
+
* draft-length pruning (Alg 1, single-user form) in forwardInfer. Stored
|
|
30
|
+
* inside DflashConfig so it round-trips through dspark.json for free.
|
|
31
|
+
* ABSENT ⟹ no pruning — an uncalibrated checkpoint drafts exactly as before. */
|
|
32
|
+
export interface StsCalibration {
|
|
33
|
+
/** thresholds[k] = min confidence to KEEP draft position k. length γ. */
|
|
34
|
+
thresholds: number[];
|
|
35
|
+
/** Target acceptance precision the thresholds were fit for (e.g. 0.5). */
|
|
36
|
+
target: number;
|
|
37
|
+
/** Verify outcomes the fit saw (provenance). */
|
|
38
|
+
samples?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
export interface DflashConfig {
|
|
27
42
|
gamma: number;
|
|
28
43
|
dDraft: number;
|
|
@@ -34,6 +49,10 @@ export interface DflashConfig {
|
|
|
34
49
|
/** Target layer indices tapped for H_ctx (Eq 2). nLayers-of-target..sentinel.
|
|
35
50
|
* For e4b (42 layers): index 0..41 = post-layer residual, 42 = post-finalNorm. */
|
|
36
51
|
tapLayers: number[];
|
|
52
|
+
/** Sequential head: "markov" (Eq 5, default) or "rnn" (Eq 6). */
|
|
53
|
+
seqHead?: "markov" | "rnn";
|
|
54
|
+
/** STS calibration (§3.2.1); absent = no confidence pruning. */
|
|
55
|
+
sts?: StsCalibration;
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
export const DEFAULT_DFLASH_CONFIG: DflashConfig = {
|
|
@@ -49,8 +68,30 @@ export const DEFAULT_DFLASH_CONFIG: DflashConfig = {
|
|
|
49
68
|
export interface TargetDims { hiddenSize: number; vocabSize: number; eps: number }
|
|
50
69
|
|
|
51
70
|
export interface DflashTrainOut { draftLogits: MlxArray; conf: MlxArray }
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
/** tokens/conf may be SHORTER than the requested γ when confidence pruning
|
|
72
|
+
* fires (Alg 1); always ≥1. draftLogits covers exactly tokens.length
|
|
73
|
+
* positions — ABSENT when the caller passed collectLogits:false (the loop
|
|
74
|
+
* never materialized/concatenated the per-position logits). */
|
|
75
|
+
export interface DflashDraftBlock { tokens: number[]; conf: number[]; draftLogits?: MlxArray }
|
|
76
|
+
export interface DflashDraftOpts {
|
|
77
|
+
sample?: DSparkSampleConfig;
|
|
78
|
+
keys?: KeyStream;
|
|
79
|
+
/** Confidence-scheduled draft-length pruning (Alg 1, single-user form):
|
|
80
|
+
* position k is DROPPED (and the block truncated there) when its predicted
|
|
81
|
+
* acceptance c_k < thresholds[k] (per-position, STS-calibrated) or < minConf
|
|
82
|
+
* (uniform manual override). Position 0 is always kept (a source must
|
|
83
|
+
* return ≥1 token). Both absent ⟹ fixed-γ drafting (pre-scheduler
|
|
84
|
+
* behavior). Losslessness is invariant — pruning only changes how many
|
|
85
|
+
* positions the target VERIFIES, never what is emitted. */
|
|
86
|
+
thresholds?: number[];
|
|
87
|
+
minConf?: number;
|
|
88
|
+
/** Materialize+concat the per-position draft logits into the returned
|
|
89
|
+
* DflashDraftBlock.draftLogits. Default true (the standalone
|
|
90
|
+
* dflashGenerate sampling-verify path reads it). The serve-loop draft()
|
|
91
|
+
* path runs its own verify lm-head and never reads it — pass false there
|
|
92
|
+
* to skip the dead concat + a host-unrelated GPU alloc every round. */
|
|
93
|
+
collectLogits?: boolean;
|
|
94
|
+
}
|
|
54
95
|
|
|
55
96
|
const CDT = Dtype.float32;
|
|
56
97
|
const NEG = -1e9;
|
|
@@ -126,6 +167,15 @@ export class DflashDrafter {
|
|
|
126
167
|
set("markov.w2", zerosArray([r, V])); // W2=0 → starts as pure parallel DFlash
|
|
127
168
|
set("conf.w", zerosArray([dDraft + r, 1]));
|
|
128
169
|
set("conf.b", zerosArray([1]));
|
|
170
|
+
// rnn.* MUST be initialized last (after every markov/shared param) so the
|
|
171
|
+
// seeded rng stream hands out IDENTICAL draws to a markov-only init up to
|
|
172
|
+
// this point — this is what makes the init-equivalence gate hold (an rnn
|
|
173
|
+
// drafter and a markov drafter, same seed, are bit-identical at t=0).
|
|
174
|
+
if (cfg.seqHead === "rnn") {
|
|
175
|
+
set("rnn.wH", normalArray(rng, [r, r], 0.02));
|
|
176
|
+
set("rnn.bH", zerosArray([r]));
|
|
177
|
+
set("rnn.wO", zerosArray([r, V])); // zero-init → starts as pure DFlash, mirrors markov.w2
|
|
178
|
+
}
|
|
129
179
|
return d;
|
|
130
180
|
}
|
|
131
181
|
|
|
@@ -261,6 +311,61 @@ export class DflashDrafter {
|
|
|
261
311
|
const B = ops.matmul(emb1, this.get("markov.w2")); // [A,γ,V]
|
|
262
312
|
return { B, emb1 };
|
|
263
313
|
}
|
|
314
|
+
|
|
315
|
+
/** RNN sequential head — the ELMAN variant. VERIFIED-DIVERGENT from paper
|
|
316
|
+
* Eq 6 (2026-07-06 audit vs arXiv:2607.05147): the paper's cell is GATED
|
|
317
|
+
* over z_k=[s_{k−1}; W₁[x_{k−1}]; h_k] (backbone hidden included!) with a
|
|
318
|
+
* fused (2r+d)→3r projection, s_k=σ(W_g z)⊙s_{k−1}+(1−σ)⊙tanh(W_c z),
|
|
319
|
+
* B_k=W₂ᵀtanh(W_o z) — DeepSpec's RNNHead is that form. This ungated
|
|
320
|
+
* tanh recurrence over the shared token embedding (no h_k) is kept as a
|
|
321
|
+
* documented VARIANT ([[dont-delete-optionality-on-one-measurement]]):
|
|
322
|
+
* the paper reports the RNN head's gains over Markov as "marginal" and
|
|
323
|
+
* production uses Markov (our default). Add the gated cell as
|
|
324
|
+
* seqHead:"gru" if it ever earns a run; details in the design doc's
|
|
325
|
+
* "DeepSpec ground truth" section. */
|
|
326
|
+
#rnn(prevToks: MlxArray): { B: MlxArray; emb1: MlxArray } {
|
|
327
|
+
const { gamma: G, markovRank: r } = this.cfg;
|
|
328
|
+
const A = prevToks.shape[0]!;
|
|
329
|
+
const emb1 = ops.takeAxis(this.get("markov.w1"), prevToks, 0); // [A,γ,r] (shared embedding, no 2nd table)
|
|
330
|
+
const wH = this.get("rnn.wH"), bH = this.get("rnn.bH"), wO = this.get("rnn.wO");
|
|
331
|
+
let s: MlxArray | null = zerosArray([A, r]); // s_{-1} = 0
|
|
332
|
+
const bSlices: MlxArray[] = [];
|
|
333
|
+
const pending: MlxArray[] = []; // iteration temps, for the catch (forwardInfer pattern)
|
|
334
|
+
try {
|
|
335
|
+
for (let k = 0; k < G; k++) {
|
|
336
|
+
const e1S = emb1.slice([0, k, 0], [A, k + 1, r]); pending.push(e1S);
|
|
337
|
+
const e1k = ops.reshape(e1S, [A, r]); pending.push(e1k);
|
|
338
|
+
e1S.dispose();
|
|
339
|
+
const sh = ops.matmul(s!, wH); pending.push(sh);
|
|
340
|
+
s!.dispose(); s = null;
|
|
341
|
+
let sNext = ops.add(sh, e1k); pending.push(sNext);
|
|
342
|
+
sh.dispose(); e1k.dispose();
|
|
343
|
+
sNext = disposing(sNext, ops.add(sNext, bH)); pending.push(sNext);
|
|
344
|
+
sNext = disposing(sNext, ops.tanh(sNext)); pending.push(sNext);
|
|
345
|
+
s = sNext; // s_k
|
|
346
|
+
const bk = ops.matmul(s, wO); pending.push(bk); // [A,V]
|
|
347
|
+
bSlices.push(ops.reshape(bk, [A, 1, this.dims.vocabSize])); bk.dispose();
|
|
348
|
+
pending.length = 0;
|
|
349
|
+
}
|
|
350
|
+
s!.dispose(); s = null;
|
|
351
|
+
const B = ops.concatAxis(bSlices, 1); // [A,γ,V]
|
|
352
|
+
for (const b of bSlices) b.dispose();
|
|
353
|
+
return { B, emb1 };
|
|
354
|
+
} catch (err) {
|
|
355
|
+
// Mid-recurrence throw: free the carried state, the collected slices,
|
|
356
|
+
// the current iteration's temps, and the emb1 the caller never received
|
|
357
|
+
// (dispose() idempotence makes over-disposal inert).
|
|
358
|
+
for (const a of pending) a.dispose();
|
|
359
|
+
for (const b of bSlices) b.dispose();
|
|
360
|
+
s?.dispose();
|
|
361
|
+
emb1.dispose();
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#seqHead(prevToks: MlxArray): { B: MlxArray; emb1: MlxArray } {
|
|
367
|
+
return this.cfg.seqHead === "rnn" ? this.#rnn(prevToks) : this.#markov(prevToks);
|
|
368
|
+
}
|
|
264
369
|
#confidence(block: MlxArray, emb1: MlxArray): MlxArray {
|
|
265
370
|
const inp = ops.concatAxis([block, emb1], 2);
|
|
266
371
|
let z = ops.matmul(inp, this.get("conf.w")); inp.dispose();
|
|
@@ -275,23 +380,49 @@ export class DflashDrafter {
|
|
|
275
380
|
* hCtx [A,Lctx,m*H], ctxMask [A,Lctx]|null, anchorEmb [A,H], prevToks [A,γ]. */
|
|
276
381
|
forwardTrain(model: Gemma4Model, hCtx: MlxArray, ctxMask: MlxArray | null, anchorEmb: MlxArray, prevToks: MlxArray): DflashTrainOut {
|
|
277
382
|
const A = hCtx.shape[0]!;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
383
|
+
let block: MlxArray | null = null, Uf: MlxArray | null = null;
|
|
384
|
+
let B: MlxArray | null = null, emb1: MlxArray | null = null;
|
|
385
|
+
try {
|
|
386
|
+
block = this.#backbone(hCtx, anchorEmb, ctxMask, A);
|
|
387
|
+
const U = this.#baseLogits(model, block);
|
|
388
|
+
Uf = U.astype(CDT); U.dispose();
|
|
389
|
+
({ B, emb1 } = this.#seqHead(prevToks));
|
|
390
|
+
const draftLogits = ops.add(Uf, B);
|
|
391
|
+
Uf.dispose(); Uf = null; B.dispose(); B = null;
|
|
392
|
+
const conf = this.#confidence(block, emb1);
|
|
393
|
+
block.dispose(); block = null; emb1.dispose(); emb1 = null;
|
|
394
|
+
return { draftLogits, conf };
|
|
395
|
+
} catch (err) {
|
|
396
|
+
// Free whatever a mid-forward throw left live (each slot is nulled the
|
|
397
|
+
// moment its normal disposal ran — no double-free).
|
|
398
|
+
block?.dispose(); Uf?.dispose(); B?.dispose(); emb1?.dispose();
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
286
401
|
}
|
|
287
402
|
|
|
288
403
|
/** Inference: parallel backbone once, then sequential Markov sampling.
|
|
289
|
-
* hCtx [1,Lctx,m*H] (full current context), anchorTok the bonus token.
|
|
404
|
+
* hCtx [1,Lctx,m*H] (full current context), anchorTok the bonus token.
|
|
405
|
+
*
|
|
406
|
+
* Host-sync discipline (docs/investigations/dspark-handoff.md item 3): the
|
|
407
|
+
* greedy path's token recurrence stays ON-DEVICE across positions — the
|
|
408
|
+
* argmax result [1] uint32 feeds directly into the next position's
|
|
409
|
+
* takeAxis, no itemUint32 in the loop. Per-position token arrays are
|
|
410
|
+
* collected and resolved to host numbers with exactly ONE concat +
|
|
411
|
+
* toIntTokens() after the loop. Confidence is the one read the pruning
|
|
412
|
+
* ALGORITHM inherently needs per position (Alg 1 must decide whether to
|
|
413
|
+
* stop NOW) — so it stays per-position whenever thresholds/minConf are
|
|
414
|
+
* actually active; when pruning is inactive there is no such need, so
|
|
415
|
+
* confidence is deferred the same way (collect [1] arrays, one concat +
|
|
416
|
+
* one toFloat32 after the loop). The sampling path already forces a host
|
|
417
|
+
* read every position (sampleToken draws from a device RNG key) — that
|
|
418
|
+
* path is unchanged. */
|
|
290
419
|
forwardInfer(model: Gemma4Model, hCtx: MlxArray, anchorTok: number, gamma: number, opts: DflashDraftOpts = {}): DflashDraftBlock {
|
|
291
420
|
const { dDraft, markovRank: r } = this.cfg;
|
|
292
421
|
const V = this.dims.vocabSize;
|
|
293
422
|
const sample = opts.sample && opts.sample.temperature > 0 ? opts.sample : null;
|
|
294
423
|
const keys = opts.keys ?? new KeyStream(opts.sample?.seed ?? 0);
|
|
424
|
+
const collectLogits = opts.collectLogits ?? true;
|
|
425
|
+
const pruningActive = opts.thresholds !== undefined || opts.minConf !== undefined;
|
|
295
426
|
|
|
296
427
|
const ids = ops.fromInt32([anchorTok], [1, 1]);
|
|
297
428
|
const ae3 = model.embed.encode(ids); ids.dispose();
|
|
@@ -302,30 +433,134 @@ export class DflashDrafter {
|
|
|
302
433
|
const Uf = U.astype(CDT); U.dispose();
|
|
303
434
|
|
|
304
435
|
const w1 = this.get("markov.w1"), w2 = this.get("markov.w2");
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
436
|
+
const isRnn = this.cfg.seqHead === "rnn";
|
|
437
|
+
const wH = isRnn ? this.get("rnn.wH") : null;
|
|
438
|
+
const bH = isRnn ? this.get("rnn.bH") : null;
|
|
439
|
+
const wO = isRnn ? this.get("rnn.wO") : null;
|
|
440
|
+
let s: MlxArray | null = isRnn ? zerosArray([1, r]) : null; // s_{-1} = 0, carried across k
|
|
441
|
+
const conf: number[] = [];
|
|
442
|
+
const perPos: MlxArray[] = []; // per-position [1,1,V] draft logits (collectLogits only)
|
|
443
|
+
const tokArrs: MlxArray[] = []; // per-position [1] device token ids, greedy path only
|
|
444
|
+
const confArrs: MlxArray[] = []; // per-position [1] device confidences, deferred (no pruning) only
|
|
445
|
+
let tokens: number[] = [];
|
|
446
|
+
let prevIdx: MlxArray = ops.fromInt32([anchorTok], [1]); // device token id fed to takeAxis
|
|
447
|
+
let ownPrevIdx = true; // false once prevIdx aliases a tokArrs/argmax entry we must not double-dispose
|
|
448
|
+
// Iteration-local temps register here at creation so the catch can reach
|
|
449
|
+
// them on a mid-flight throw; drained (length=0) at the bottom of each
|
|
450
|
+
// iteration once everything in it was disposed on the normal path.
|
|
451
|
+
// dispose() idempotence makes catch-side re-disposal of the already-freed
|
|
452
|
+
// ones inert.
|
|
453
|
+
const pending: MlxArray[] = [];
|
|
454
|
+
try {
|
|
455
|
+
for (let k = 0; k < gamma; k++) {
|
|
456
|
+
const UkS = Uf.slice([0, k, 0], [1, k + 1, V]); pending.push(UkS);
|
|
457
|
+
const Uk = ops.reshape(UkS, [1, V]); pending.push(Uk);
|
|
458
|
+
UkS.dispose();
|
|
459
|
+
const e1 = ops.takeAxis(w1, prevIdx, 0); pending.push(e1); // [1,r], shared embedding either way
|
|
460
|
+
if (ownPrevIdx) { prevIdx.dispose(); ownPrevIdx = false; } // consumed; prevIdx reassigned below
|
|
461
|
+
let Bk: MlxArray;
|
|
462
|
+
if (isRnn) {
|
|
463
|
+
const sh = ops.matmul(s!, wH!); pending.push(sh);
|
|
464
|
+
s!.dispose(); s = null;
|
|
465
|
+
let sNext = ops.add(sh, e1); pending.push(sNext);
|
|
466
|
+
sh.dispose();
|
|
467
|
+
sNext = disposing(sNext, ops.add(sNext, bH!)); pending.push(sNext);
|
|
468
|
+
sNext = disposing(sNext, ops.tanh(sNext)); pending.push(sNext);
|
|
469
|
+
s = sNext; // s_k, survives to the next iteration (covered by the s slot from here)
|
|
470
|
+
Bk = ops.matmul(s, wO!); pending.push(Bk);
|
|
471
|
+
} else {
|
|
472
|
+
Bk = ops.matmul(e1, w2); pending.push(Bk);
|
|
473
|
+
}
|
|
474
|
+
const logitsK = ops.add(Uk, Bk); pending.push(logitsK);
|
|
475
|
+
Uk.dispose(); Bk.dispose();
|
|
476
|
+
// prevIdx becomes the SINGLE tracked owner of the next token id the
|
|
477
|
+
// moment it exists — no separate local for the catch to miss.
|
|
478
|
+
if (sample) {
|
|
479
|
+
const sc = processLogits(logitsK, sample); pending.push(sc);
|
|
480
|
+
const tok = sampleToken(sc, keys.next()); sc.dispose();
|
|
481
|
+
tokens.push(tok);
|
|
482
|
+
prevIdx = ops.fromInt32([tok], [1]); ownPrevIdx = true;
|
|
483
|
+
} else {
|
|
484
|
+
const am = ops.argmaxAxis(logitsK, -1); // [1] uint32, ON-DEVICE — no itemUint32 here
|
|
485
|
+
tokArrs.push(am);
|
|
486
|
+
prevIdx = am; ownPrevIdx = false; // aliases tokArrs[k] — that collection owns it
|
|
487
|
+
}
|
|
488
|
+
if (collectLogits) perPos.push(ops.reshape(logitsK, [1, 1, V]));
|
|
489
|
+
logitsK.dispose();
|
|
490
|
+
const hkS = block.slice([0, k, 0], [1, k + 1, dDraft]); pending.push(hkS);
|
|
491
|
+
const hk = ops.reshape(hkS, [1, dDraft]); pending.push(hk);
|
|
492
|
+
hkS.dispose();
|
|
493
|
+
const ci = ops.concatAxis([hk, e1], 1); pending.push(ci);
|
|
494
|
+
hk.dispose(); e1.dispose();
|
|
495
|
+
let cz = ops.matmul(ci, this.get("conf.w")); pending.push(cz);
|
|
496
|
+
ci.dispose();
|
|
497
|
+
cz = disposing(cz, ops.add(cz, this.get("conf.b"))); pending.push(cz);
|
|
498
|
+
cz = disposing(cz, ops.sigmoid(cz)); pending.push(cz);
|
|
499
|
+
|
|
500
|
+
// Alg 1 (single-user): drop position k when its predicted acceptance
|
|
501
|
+
// is below the calibrated/manual threshold — the target then
|
|
502
|
+
// verifies a shorter window. Position 0 always survives (sources
|
|
503
|
+
// return ≥1 token). Pruning inherently needs the confidence value
|
|
504
|
+
// THIS position, on the host, to decide whether to stop — no way
|
|
505
|
+
// around that read when active. When inactive, defer it (device
|
|
506
|
+
// array, resolved once after the loop) since nothing this iteration
|
|
507
|
+
// depends on its value.
|
|
508
|
+
const thr = opts.thresholds?.[k] ?? opts.minConf;
|
|
509
|
+
if (pruningActive) {
|
|
510
|
+
const cVal = cz.toFloat32()[0]!; cz.dispose();
|
|
511
|
+
if (thr !== undefined && k > 0 && cVal < thr) {
|
|
512
|
+
// Drop position k SYMMETRICALLY on both paths so tokens/conf/
|
|
513
|
+
// draftLogits stay aligned (the DflashDraftBlock contract —
|
|
514
|
+
// verifySampling slices draftLogits per token). Greedy: pop the
|
|
515
|
+
// device array (prevIdx aliases it — one dispose via the owning
|
|
516
|
+
// collection). Sample: pop the host token + free the standalone
|
|
517
|
+
// fromInt32.
|
|
518
|
+
if (!sample) tokArrs.pop()!.dispose(); // also frees prevIdx (same array)
|
|
519
|
+
else { prevIdx.dispose(); tokens.pop(); }
|
|
520
|
+
ownPrevIdx = false; // dead either way — post-loop/catch must not re-dispose
|
|
521
|
+
if (collectLogits) perPos.pop()!.dispose(); // logits row for the dropped position
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
conf.push(cVal);
|
|
525
|
+
} else {
|
|
526
|
+
confArrs.push(cz);
|
|
527
|
+
}
|
|
528
|
+
pending.length = 0; // everything above was disposed (or handed to a collection)
|
|
529
|
+
}
|
|
530
|
+
pending.length = 0; // early-break path: the broken iteration's temps are all freed too
|
|
531
|
+
if (ownPrevIdx) prevIdx.dispose(); // last iteration's fresh sample-path array, unused past the loop
|
|
532
|
+
s?.dispose(); // final on-device state (loop end or early break — both leave s live)
|
|
533
|
+
Uf.dispose(); block.dispose();
|
|
534
|
+
|
|
535
|
+
if (!sample) {
|
|
536
|
+
const allToks = ops.concatAxis(tokArrs, 0); // [nKept] uint32, ONE host read below
|
|
537
|
+
for (const a of tokArrs) a.dispose();
|
|
538
|
+
tokens = allToks.toIntTokens(); allToks.dispose();
|
|
539
|
+
}
|
|
540
|
+
if (!pruningActive && confArrs.length) {
|
|
541
|
+
const allConf = ops.concatAxis(confArrs, 0); // [nKept,1]
|
|
542
|
+
for (const a of confArrs) a.dispose();
|
|
543
|
+
const flat = allConf.toFloat32(); allConf.dispose();
|
|
544
|
+
for (const v of flat) conf.push(v);
|
|
545
|
+
}
|
|
546
|
+
const draftLogits = collectLogits ? ops.concatAxis(perPos, 1) : undefined;
|
|
547
|
+
if (collectLogits) for (const a of perPos) a.dispose();
|
|
548
|
+
return { tokens, conf, draftLogits };
|
|
549
|
+
} catch (err) {
|
|
550
|
+
// Cleanup on a mid-flight throw — the collections, the carried state,
|
|
551
|
+
// AND the current iteration's registered temps (`pending`). dispose()
|
|
552
|
+
// is idempotent (array.ts #disposed guard) so re-disposing an array the
|
|
553
|
+
// try block already freed (or that aliases one of these collections,
|
|
554
|
+
// e.g. prevIdx === tokArrs[k]) is inert, never a double-free bug.
|
|
555
|
+
for (const a of pending) a.dispose();
|
|
556
|
+
if (ownPrevIdx) prevIdx.dispose();
|
|
557
|
+
for (const a of tokArrs) a.dispose();
|
|
558
|
+
for (const a of confArrs) a.dispose();
|
|
559
|
+
for (const a of perPos) a.dispose();
|
|
560
|
+
s?.dispose();
|
|
561
|
+
Uf.dispose(); block.dispose();
|
|
562
|
+
throw err;
|
|
325
563
|
}
|
|
326
|
-
Uf.dispose(); block.dispose();
|
|
327
|
-
const draftLogits = ops.concatAxis(perPos, 1); for (const a of perPos) a.dispose();
|
|
328
|
-
return { tokens, conf, draftLogits };
|
|
329
564
|
}
|
|
330
565
|
|
|
331
566
|
save(dir: string): void {
|
|
@@ -333,7 +568,9 @@ export class DflashDrafter {
|
|
|
333
568
|
const tensors: NamedTensor[] = this.names.map((name) => ({ name, array: this.get(name) }));
|
|
334
569
|
writeShardedSafetensors(dir, tensors);
|
|
335
570
|
writeFileSync(join(dir, "dspark.json"), JSON.stringify({
|
|
336
|
-
|
|
571
|
+
// "dspark" is the canonical variant (this faithful module IS DSpark);
|
|
572
|
+
// load() accepts the legacy "dflash" stamp from older checkpoints.
|
|
573
|
+
kind: "dspark-drafter", variant: "dspark", version: 1,
|
|
337
574
|
target_id: this.targetId, config: this.cfg, dims: this.dims, param_names: this.names,
|
|
338
575
|
}, null, 2));
|
|
339
576
|
}
|
|
@@ -341,7 +578,8 @@ export class DflashDrafter {
|
|
|
341
578
|
const meta = JSON.parse(readFileSync(join(dir, "dspark.json"), "utf8")) as {
|
|
342
579
|
variant?: string; config: DflashConfig; dims: TargetDims; target_id: string;
|
|
343
580
|
};
|
|
344
|
-
if (meta.variant !== "
|
|
581
|
+
if (meta.variant !== "dspark" && meta.variant !== "dflash")
|
|
582
|
+
throw new Error(`${dir} is not a dspark drafter (variant=${meta.variant ?? "none — a v1 single-vector checkpoint?"})`);
|
|
345
583
|
const d = new DflashDrafter(meta.config, meta.dims, meta.target_id);
|
|
346
584
|
const tensors = loadAdapterTensors(join(dir, "model.safetensors"));
|
|
347
585
|
try {
|
|
@@ -357,5 +595,9 @@ function buildNames(cfg: DflashConfig): string[] {
|
|
|
357
595
|
for (let i = 0; i < cfg.nLayers; i++)
|
|
358
596
|
names.push(`bb.${i}.attn_norm`, `bb.${i}.q`, `bb.${i}.k`, `bb.${i}.v`, `bb.${i}.o`, `bb.${i}.ff_norm`, `bb.${i}.gate`, `bb.${i}.up`, `bb.${i}.down`);
|
|
359
597
|
names.push("W_c", "ctx_norm", "tok_proj", "mask_emb", "block_pos", "out_norm", "out_proj", "markov.w1", "markov.w2", "conf.w", "conf.b");
|
|
598
|
+
// rnn.* MUST be appended LAST — see the matching comment in initFromDims:
|
|
599
|
+
// this ordering keeps the shared/markov params drawing identical seeded rng
|
|
600
|
+
// values whether or not the rnn head is present (init-equivalence gate).
|
|
601
|
+
if (cfg.seqHead === "rnn") names.push("rnn.wH", "rnn.bH", "rnn.wO");
|
|
360
602
|
return names;
|
|
361
603
|
}
|