mlx-bun 0.0.8 → 0.0.9
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 +112 -20
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +262 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +40 -9
- package/docs/reference/training.md +25 -11
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1029 -86
- package/src/config.ts +4 -1
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +15 -6
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +141 -18
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/ffi.ts +3 -0
- package/src/mlx/ops.ts +46 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/factory.ts +29 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +62 -6
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3.ts +276 -0
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +38 -8
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +215 -7
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +272 -80
- package/src/serve/generation-gateway.ts +86 -18
- package/src/server.ts +612 -49
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/flash-cce.ts +70 -15
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +289 -30
- package/src/train/prefix-shared.ts +33 -5
- package/src/train/segmented.ts +67 -11
- package/src/train/trainer.ts +92 -28
- package/src/web/skills/memory/SKILL.md +40 -17
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Faithful DFlash decode loop — the draft attends to a GROWING multi-layer
|
|
2
|
+
// context H_ctx (the target's tapped hiddens over the accepted stream), per
|
|
3
|
+
// paper §3.1 / Eq 3 + §5.1 runtime. Parallel to generate.ts (v1 single-vector).
|
|
4
|
+
//
|
|
5
|
+
// Each verify pass is TAPPED (captures the m layers at the verified positions);
|
|
6
|
+
// after accepting kAccept drafts, the anchor + accepted tokens' m-layer hiddens
|
|
7
|
+
// are appended to H_ctx (rejected tips are never added — mirrors the KV trim).
|
|
8
|
+
// Losslessness is unchanged: greedy = longest-prefix match; temp>0 = min(1,p/q)
|
|
9
|
+
// + residual resample. H_ctx only affects WHICH tokens get drafted (τ), never
|
|
10
|
+
// what is emitted.
|
|
11
|
+
|
|
12
|
+
import { MlxArray } from "../../mlx/array";
|
|
13
|
+
import { Dtype } from "../../mlx/ffi";
|
|
14
|
+
import * as ops from "../../mlx/ops";
|
|
15
|
+
import { Gemma4Model } from "../../model/gemma4";
|
|
16
|
+
import type { DflashDrafter } from "./module-dflash";
|
|
17
|
+
import { KeyStream, processLogits, probsOf, probAtToken, sampleToken, sampleResidual, type DSparkSampleConfig } from "./sample";
|
|
18
|
+
|
|
19
|
+
export interface DflashGenOptions {
|
|
20
|
+
gamma?: number;
|
|
21
|
+
maxTokens?: number;
|
|
22
|
+
eosTokenIds?: number[];
|
|
23
|
+
sample?: DSparkSampleConfig;
|
|
24
|
+
}
|
|
25
|
+
export interface DflashStats {
|
|
26
|
+
emitted: number; drafted: number; accepted: number; targetCalls: number;
|
|
27
|
+
decodeMs: number; prefillMs: number; acceptanceRate: number; meanAcceptLen: number; meanConf: number; decodeTps: number;
|
|
28
|
+
}
|
|
29
|
+
export interface DflashResult { tokens: number[]; stats: DflashStats }
|
|
30
|
+
|
|
31
|
+
/** Tapped forward: run the target over `ids`, returning the final post-norm
|
|
32
|
+
* hidden [1,L,H] AND the m-layer context [1,L,m*H] (tapLayers concatenated on
|
|
33
|
+
* the feature axis). */
|
|
34
|
+
function forwardTapped(model: Gemma4Model, ids: MlxArray, caches: import("../../model/gemma4").Cache[], tapLayers: number[]): { finalH: MlxArray; ctxML: MlxArray } {
|
|
35
|
+
model.hiddenTap = { layers: new Set(tapLayers), captured: new Map() };
|
|
36
|
+
let finalH: MlxArray;
|
|
37
|
+
let ctxML: MlxArray;
|
|
38
|
+
try {
|
|
39
|
+
finalH = model.forwardHidden(ids, caches);
|
|
40
|
+
const cap = model.hiddenTap.captured;
|
|
41
|
+
const perLayer = tapLayers.map((li) => { const a = cap.get(li); if (!a) throw new Error(`layer ${li} not captured`); return a; });
|
|
42
|
+
ctxML = ops.concatAxis(perLayer, 2); // [1,L,m*H]
|
|
43
|
+
for (const [, a] of cap) a.dispose();
|
|
44
|
+
} finally {
|
|
45
|
+
model.hiddenTap = null;
|
|
46
|
+
}
|
|
47
|
+
return { finalH, ctxML };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sampleFromHidden(model: Gemma4Model, hidden: MlxArray, pos: number, cfg: DSparkSampleConfig | null, keys: KeyStream): number {
|
|
51
|
+
const H = hidden.shape[2]!;
|
|
52
|
+
const hSl = hidden.slice([0, pos, 0], [1, pos + 1, H]);
|
|
53
|
+
const logits = model.logitsFromHidden(hSl); hSl.dispose();
|
|
54
|
+
const V = logits.shape[2]!;
|
|
55
|
+
const flat = ops.reshape(logits, [1, V]); logits.dispose();
|
|
56
|
+
let tok: number;
|
|
57
|
+
if (cfg) { const sc = processLogits(flat, cfg); tok = sampleToken(sc, keys.next()); sc.dispose(); }
|
|
58
|
+
else { const am = ops.argmaxAxis(flat, -1); tok = ops.itemUint32(am); am.dispose(); }
|
|
59
|
+
flat.dispose();
|
|
60
|
+
return tok;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function picksBatched(model: Gemma4Model, hidden: MlxArray, count: number): number[] {
|
|
64
|
+
const logits = model.logitsFromHidden(hidden);
|
|
65
|
+
const V = logits.shape[2]!;
|
|
66
|
+
const out: number[] = [];
|
|
67
|
+
for (let k = 0; k < count; k++) {
|
|
68
|
+
const sl = logits.slice([0, k, 0], [1, k + 1, V]);
|
|
69
|
+
const flat = ops.reshape(sl, [1, V]); sl.dispose();
|
|
70
|
+
const am = ops.argmaxAxis(flat, -1); flat.dispose();
|
|
71
|
+
out.push(ops.itemUint32(am)); am.dispose();
|
|
72
|
+
}
|
|
73
|
+
logits.dispose();
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface Verdict { kAccept: number; emit: number }
|
|
78
|
+
|
|
79
|
+
function verifyGreedy(model: Gemma4Model, vHidden: MlxArray, drafts: number[], gamma: number): Verdict {
|
|
80
|
+
const gt = picksBatched(model, vHidden, gamma + 1);
|
|
81
|
+
let k = 0; while (k < gamma && drafts[k] === gt[k]) k++;
|
|
82
|
+
return { kAccept: k, emit: gt[k]! };
|
|
83
|
+
}
|
|
84
|
+
function verifySampling(model: Gemma4Model, vHidden: MlxArray, draftLogits: MlxArray, drafts: number[], gamma: number, cfg: DSparkSampleConfig, keys: KeyStream): Verdict {
|
|
85
|
+
const V = draftLogits.shape[2]!;
|
|
86
|
+
const tl = model.logitsFromHidden(vHidden);
|
|
87
|
+
const us = ops.randomUniform([gamma], Dtype.float32, 0, 1, keys.next());
|
|
88
|
+
const u = us.toFloat32(); us.dispose();
|
|
89
|
+
const slice = (a: MlxArray, i: number) => { const s = a.slice([0, i, 0], [1, i + 1, V]); const f = ops.reshape(s, [1, V]); s.dispose(); return f; };
|
|
90
|
+
let verdict: Verdict | null = null;
|
|
91
|
+
for (let k = 0; k < gamma; k++) {
|
|
92
|
+
const x = drafts[k]!;
|
|
93
|
+
const ps = processLogits(slice(tl, k), cfg); const P = probsOf(ps); ps.dispose();
|
|
94
|
+
const qs = processLogits(slice(draftLogits, k), cfg); const Q = probsOf(qs); qs.dispose();
|
|
95
|
+
const pX = probAtToken(P, x), qX = probAtToken(Q, x);
|
|
96
|
+
if (u[k]! < Math.min(1, qX > 0 ? pX / qX : 1)) { P.dispose(); Q.dispose(); continue; }
|
|
97
|
+
const corr = sampleResidual(P, Q, keys.next()); P.dispose(); Q.dispose();
|
|
98
|
+
verdict = { kAccept: k, emit: corr }; break;
|
|
99
|
+
}
|
|
100
|
+
if (!verdict) { const bs = processLogits(slice(tl, gamma), cfg); verdict = { kAccept: gamma, emit: sampleToken(bs, keys.next()) }; bs.dispose(); }
|
|
101
|
+
tl.dispose();
|
|
102
|
+
return verdict;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function dflashGenerate(model: Gemma4Model, drafter: DflashDrafter, promptTokens: number[], options: DflashGenOptions = {}): DflashResult {
|
|
106
|
+
const gamma = options.gamma ?? drafter.cfg.gamma;
|
|
107
|
+
const { maxTokens = 256, eosTokenIds = model.config.eosTokenIds } = options;
|
|
108
|
+
const sampleCfg = options.sample && options.sample.temperature > 0 ? options.sample : null;
|
|
109
|
+
const keys = new KeyStream(options.sample?.seed ?? 0);
|
|
110
|
+
const tapLayers = drafter.cfg.tapLayers;
|
|
111
|
+
|
|
112
|
+
const caches = model.makeCache();
|
|
113
|
+
const stats: DflashStats = { emitted: 0, drafted: 0, accepted: 0, targetCalls: 0, decodeMs: 0, prefillMs: 0, acceptanceRate: 0, meanAcceptLen: 0, meanConf: 0, decodeTps: 0 };
|
|
114
|
+
const out: number[] = [];
|
|
115
|
+
let confSum = 0, rounds = 0, acceptLenSum = 0;
|
|
116
|
+
let H_ctx: MlxArray | null = null;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// 1. prefill (tapped): seed H_ctx with the prompt's m-layer hiddens.
|
|
120
|
+
const t0 = performance.now();
|
|
121
|
+
const ids = ops.fromInt32(promptTokens, [1, promptTokens.length]);
|
|
122
|
+
const { finalH, ctxML } = forwardTapped(model, ids, caches, tapLayers);
|
|
123
|
+
ids.dispose();
|
|
124
|
+
stats.targetCalls++;
|
|
125
|
+
H_ctx = ctxML; // [1, Lp, m*H]
|
|
126
|
+
const Lp = finalH.shape[1]!;
|
|
127
|
+
let next = sampleFromHidden(model, finalH, Lp - 1, sampleCfg, keys);
|
|
128
|
+
finalH.dispose();
|
|
129
|
+
stats.prefillMs = performance.now() - t0;
|
|
130
|
+
|
|
131
|
+
if (eosTokenIds.includes(next)) return finalize(stats, out, rounds, acceptLenSum, confSum);
|
|
132
|
+
out.push(next); stats.emitted++;
|
|
133
|
+
|
|
134
|
+
const tDecode = performance.now();
|
|
135
|
+
outer: while (stats.emitted < maxTokens) {
|
|
136
|
+
// 2a. draft against the current (growing) H_ctx.
|
|
137
|
+
const block = drafter.forwardInfer(model, H_ctx!, next, gamma, { sample: sampleCfg ?? undefined, keys });
|
|
138
|
+
const drafts = block.tokens;
|
|
139
|
+
stats.drafted += gamma;
|
|
140
|
+
for (const c of block.conf) confSum += c;
|
|
141
|
+
|
|
142
|
+
// 2b. verify (tapped) — get accepted tokens' m-layer hiddens to append.
|
|
143
|
+
const vIds = ops.fromInt32([next, ...drafts], [1, gamma + 1]);
|
|
144
|
+
const { finalH: vHidden, ctxML: vCtxML } = forwardTapped(model, vIds, caches, tapLayers);
|
|
145
|
+
vIds.dispose();
|
|
146
|
+
stats.targetCalls++;
|
|
147
|
+
|
|
148
|
+
const { kAccept, emit } = sampleCfg
|
|
149
|
+
? verifySampling(model, vHidden, block.draftLogits, drafts, gamma, sampleCfg, keys)
|
|
150
|
+
: verifyGreedy(model, vHidden, drafts, gamma);
|
|
151
|
+
block.draftLogits.dispose();
|
|
152
|
+
stats.accepted += kAccept; rounds++; acceptLenSum += kAccept + 1;
|
|
153
|
+
|
|
154
|
+
// 2c. emit accepted drafts
|
|
155
|
+
for (let k = 0; k < kAccept; k++) {
|
|
156
|
+
const isEos = eosTokenIds.includes(drafts[k]!);
|
|
157
|
+
if (!isEos) out.push(drafts[k]!);
|
|
158
|
+
stats.emitted++;
|
|
159
|
+
if (isEos || stats.emitted >= maxTokens) { vHidden.dispose(); vCtxML.dispose(); break outer; }
|
|
160
|
+
}
|
|
161
|
+
const emitIsEos = eosTokenIds.includes(emit);
|
|
162
|
+
if (!emitIsEos) out.push(emit);
|
|
163
|
+
stats.emitted++;
|
|
164
|
+
|
|
165
|
+
// 2d. append accepted stream's m-layer hiddens (anchor + kAccept drafts =
|
|
166
|
+
// vCtxML[:, 0..kAccept]) to H_ctx; never the rejected tips.
|
|
167
|
+
const add = vCtxML.slice([0, 0, 0], [1, kAccept + 1, vCtxML.shape[2]!]);
|
|
168
|
+
const grown = ops.concatAxis([H_ctx!, add], 1);
|
|
169
|
+
H_ctx!.dispose(); add.dispose(); vCtxML.dispose();
|
|
170
|
+
H_ctx = grown;
|
|
171
|
+
|
|
172
|
+
// 2e. roll back rejected KV tips (bypass trim, past-window safe)
|
|
173
|
+
if (kAccept < gamma) { const n = gamma - kAccept; for (const c of caches) c.trim(n, true); }
|
|
174
|
+
|
|
175
|
+
vHidden.dispose();
|
|
176
|
+
if (emitIsEos || stats.emitted >= maxTokens) break;
|
|
177
|
+
next = emit;
|
|
178
|
+
}
|
|
179
|
+
stats.decodeMs = performance.now() - tDecode;
|
|
180
|
+
} finally {
|
|
181
|
+
H_ctx?.dispose();
|
|
182
|
+
for (const c of caches) c.dispose();
|
|
183
|
+
}
|
|
184
|
+
return finalize(stats, out, rounds, acceptLenSum, confSum);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function finalize(stats: DflashStats, out: number[], rounds: number, acceptLenSum: number, confSum: number): DflashResult {
|
|
188
|
+
stats.acceptanceRate = stats.drafted ? stats.accepted / stats.drafted : 0;
|
|
189
|
+
stats.meanAcceptLen = rounds ? acceptLenSum / rounds : 0;
|
|
190
|
+
stats.meanConf = stats.drafted ? confSum / stats.drafted : 0;
|
|
191
|
+
stats.decodeTps = stats.decodeMs ? ((stats.emitted - 1) / stats.decodeMs) * 1000 : 0;
|
|
192
|
+
return { tokens: out, stats };
|
|
193
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// DSpark speculative decode loop — a fork of src/spec/generate.ts
|
|
2
|
+
// (specGenerate). Same γ-draft / verify-in-one-pass / accept-longest-prefix /
|
|
3
|
+
// rollback spine, but the drafter is the trainable DSpark module: one PARALLEL
|
|
4
|
+
// backbone pass + a cheap SEQUENTIAL Markov head produce the γ-block, instead
|
|
5
|
+
// of γ sequential KV-borrowing drafter calls. DSpark needs only the target's
|
|
6
|
+
// final hidden as context (the tap), so the donor-K/V plumbing is gone.
|
|
7
|
+
//
|
|
8
|
+
// Two lossless verify modes — both leave the OUTPUT DISTRIBUTION exactly equal
|
|
9
|
+
// to vanilla e4b decode; the drafter only moves τ (speed), never WHAT is
|
|
10
|
+
// emitted:
|
|
11
|
+
// - GREEDY (temperature 0): accept a draft iff it equals the target's argmax
|
|
12
|
+
// (longest-prefix). Bit-identical to vanilla greedy e4b.
|
|
13
|
+
// - SAMPLING (temperature > 0): the speculative-sampling rule — accept x_k
|
|
14
|
+
// w.p. min(1, p_k(x_k)/q_k(x_k)); on rejection resample from the residual
|
|
15
|
+
// norm(relu(p−q)) and stop; if the whole block is accepted, sample a bonus
|
|
16
|
+
// from p_{γ+1}. p and q use the SAME top-p/top-k/temperature processing
|
|
17
|
+
// (sample.ts), so the emitted stream is distributed exactly as if e4b
|
|
18
|
+
// sampled token-by-token at that temperature — independent of draft quality.
|
|
19
|
+
//
|
|
20
|
+
// Verify the corresponding gate before chasing τ (scripts/dspark-ab.ts): greedy
|
|
21
|
+
// output must match model.generate; the sampling rule's correctness is the
|
|
22
|
+
// statistical "emit distribution == target" check in scripts/dspark-smoke.ts.
|
|
23
|
+
// If a gate fails, the accept/reject or KV rollback is wrong — fix that first.
|
|
24
|
+
|
|
25
|
+
import { MlxArray } from "../../mlx/array";
|
|
26
|
+
import { Dtype } from "../../mlx/ffi";
|
|
27
|
+
import * as ops from "../../mlx/ops";
|
|
28
|
+
import { Gemma4Model } from "../../model/gemma4";
|
|
29
|
+
import type { DSparkDrafter } from "./module";
|
|
30
|
+
import { KeyStream, processLogits, probsOf, probAtToken, sampleToken, sampleResidual, type DSparkSampleConfig } from "./sample";
|
|
31
|
+
|
|
32
|
+
export interface DSparkOptions {
|
|
33
|
+
gamma?: number;
|
|
34
|
+
maxTokens?: number;
|
|
35
|
+
eosTokenIds?: number[];
|
|
36
|
+
/** Sampling config; omit (or temperature 0) ⇒ greedy. */
|
|
37
|
+
sample?: DSparkSampleConfig;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DSparkStats {
|
|
41
|
+
emitted: number;
|
|
42
|
+
drafted: number;
|
|
43
|
+
accepted: number;
|
|
44
|
+
targetCalls: number;
|
|
45
|
+
decodeMs: number;
|
|
46
|
+
prefillMs: number;
|
|
47
|
+
acceptanceRate: number;
|
|
48
|
+
/** Mean accepted length per verify round (τ), incl. the bonus/correction token. */
|
|
49
|
+
meanAcceptLen: number;
|
|
50
|
+
/** Mean predicted confidence over drafted tokens (diagnostic). */
|
|
51
|
+
meanConf: number;
|
|
52
|
+
decodeTps: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DSparkResult {
|
|
56
|
+
tokens: number[];
|
|
57
|
+
stats: DSparkStats;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Greedy pick from one hidden position [1,1,H]. */
|
|
61
|
+
function pickFromHidden(model: Gemma4Model, hidden: MlxArray, pos: number): number {
|
|
62
|
+
const H = hidden.shape[2]!;
|
|
63
|
+
const hSl = hidden.slice([0, pos, 0], [1, pos + 1, H]);
|
|
64
|
+
const logits = model.logitsFromHidden(hSl);
|
|
65
|
+
hSl.dispose();
|
|
66
|
+
const V = logits.shape[2]!;
|
|
67
|
+
const flat = ops.reshape(logits, [1, V]);
|
|
68
|
+
logits.dispose();
|
|
69
|
+
const am = ops.argmaxAxis(flat, -1);
|
|
70
|
+
flat.dispose();
|
|
71
|
+
const t = ops.itemUint32(am);
|
|
72
|
+
am.dispose();
|
|
73
|
+
return t;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Batched verify: one lm-head over the γ+1 window, argmax per position. */
|
|
77
|
+
function picksBatched(model: Gemma4Model, hidden: MlxArray, count: number): number[] {
|
|
78
|
+
const logits = model.logitsFromHidden(hidden); // [1, L, V]
|
|
79
|
+
const V = logits.shape[2]!;
|
|
80
|
+
const out: number[] = [];
|
|
81
|
+
for (let k = 0; k < count; k++) {
|
|
82
|
+
const sl = logits.slice([0, k, 0], [1, k + 1, V]);
|
|
83
|
+
const flat = ops.reshape(sl, [1, V]);
|
|
84
|
+
sl.dispose();
|
|
85
|
+
const am = ops.argmaxAxis(flat, -1);
|
|
86
|
+
flat.dispose();
|
|
87
|
+
out.push(ops.itemUint32(am));
|
|
88
|
+
am.dispose();
|
|
89
|
+
}
|
|
90
|
+
logits.dispose();
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Result of one verify round: how many leading drafts were accepted, and the
|
|
95
|
+
* correction/bonus token to emit after them. */
|
|
96
|
+
interface RoundVerdict { kAccept: number; emit: number }
|
|
97
|
+
|
|
98
|
+
/** GREEDY verify: longest prefix where draft == target argmax. */
|
|
99
|
+
function verifyGreedy(model: Gemma4Model, vHidden: MlxArray, drafts: number[], gamma: number): RoundVerdict {
|
|
100
|
+
const gt = picksBatched(model, vHidden, gamma + 1);
|
|
101
|
+
let kAccept = 0;
|
|
102
|
+
while (kAccept < gamma && drafts[kAccept] === gt[kAccept]) kAccept++;
|
|
103
|
+
return { kAccept, emit: gt[kAccept]! };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** SAMPLING verify (speculative sampling): accept x_k w.p. min(1, p/q); on the
|
|
107
|
+
* first rejection resample from norm(relu(p−q)); if all accepted sample a bonus
|
|
108
|
+
* from p_{γ+1}. p,q processed identically (top-p/top-k/temperature). */
|
|
109
|
+
function verifySampling(
|
|
110
|
+
model: Gemma4Model, vHidden: MlxArray, draftLogits: MlxArray, drafts: number[],
|
|
111
|
+
gamma: number, cfg: DSparkSampleConfig, keys: KeyStream,
|
|
112
|
+
): RoundVerdict {
|
|
113
|
+
const V = draftLogits.shape[2]!;
|
|
114
|
+
const targetLogits = model.logitsFromHidden(vHidden); // [1,γ+1,V]
|
|
115
|
+
// γ uniforms for the accept tests, drawn together.
|
|
116
|
+
const uArr = ops.randomUniform([gamma], Dtype.float32, 0, 1, keys.next());
|
|
117
|
+
const us = uArr.toFloat32();
|
|
118
|
+
uArr.dispose();
|
|
119
|
+
|
|
120
|
+
let verdict: RoundVerdict | null = null;
|
|
121
|
+
for (let k = 0; k < gamma; k++) {
|
|
122
|
+
const x = drafts[k]!;
|
|
123
|
+
const tScaled = processLogits(sliceTmp(targetLogits, k, V), cfg);
|
|
124
|
+
const P = probsOf(tScaled);
|
|
125
|
+
const dScaled = processLogits(sliceTmp(draftLogits, k, V), cfg);
|
|
126
|
+
const Q = probsOf(dScaled);
|
|
127
|
+
tScaled.dispose(); dScaled.dispose();
|
|
128
|
+
const pX = probAtToken(P, x);
|
|
129
|
+
const qX = probAtToken(Q, x);
|
|
130
|
+
const accept = us[k]! < Math.min(1, qX > 0 ? pX / qX : 1);
|
|
131
|
+
if (accept) {
|
|
132
|
+
P.dispose(); Q.dispose();
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// reject at k → resample correction from the residual, stop.
|
|
136
|
+
const corrected = sampleResidual(P, Q, keys.next());
|
|
137
|
+
P.dispose(); Q.dispose();
|
|
138
|
+
verdict = { kAccept: k, emit: corrected };
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (!verdict) {
|
|
142
|
+
// all γ accepted → bonus from p_{γ+1}
|
|
143
|
+
const bScaled = processLogits(sliceTmp(targetLogits, gamma, V), cfg);
|
|
144
|
+
const bonus = sampleToken(bScaled, keys.next());
|
|
145
|
+
bScaled.dispose();
|
|
146
|
+
verdict = { kAccept: gamma, emit: bonus };
|
|
147
|
+
}
|
|
148
|
+
targetLogits.dispose();
|
|
149
|
+
return verdict;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** [1,L,V] → [1,V] at position k (a fresh owned array). */
|
|
153
|
+
function sliceTmp(logits: MlxArray, k: number, V: number): MlxArray {
|
|
154
|
+
const sl = logits.slice([0, k, 0], [1, k + 1, V]);
|
|
155
|
+
const flat = ops.reshape(sl, [1, V]);
|
|
156
|
+
sl.dispose();
|
|
157
|
+
return flat;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function dsparkGenerate(
|
|
161
|
+
model: Gemma4Model,
|
|
162
|
+
drafter: DSparkDrafter,
|
|
163
|
+
promptTokens: number[],
|
|
164
|
+
options: DSparkOptions = {},
|
|
165
|
+
): DSparkResult {
|
|
166
|
+
const gamma = options.gamma ?? drafter.cfg.gamma;
|
|
167
|
+
const { maxTokens = 256, eosTokenIds = model.config.eosTokenIds } = options;
|
|
168
|
+
if (gamma < 1) throw new Error("gamma must be >= 1");
|
|
169
|
+
if (gamma > drafter.cfg.gamma)
|
|
170
|
+
throw new Error(`gamma ${gamma} exceeds drafter max ${drafter.cfg.gamma}`);
|
|
171
|
+
const sampleCfg = options.sample && options.sample.temperature > 0 ? options.sample : null;
|
|
172
|
+
const keys = new KeyStream(options.sample?.seed ?? 0);
|
|
173
|
+
|
|
174
|
+
const caches = model.makeCache();
|
|
175
|
+
const stats: DSparkStats = {
|
|
176
|
+
emitted: 0, drafted: 0, accepted: 0, targetCalls: 0,
|
|
177
|
+
decodeMs: 0, prefillMs: 0, acceptanceRate: 0,
|
|
178
|
+
meanAcceptLen: 0, meanConf: 0, decodeTps: 0,
|
|
179
|
+
};
|
|
180
|
+
const out: number[] = [];
|
|
181
|
+
let confSum = 0;
|
|
182
|
+
let rounds = 0;
|
|
183
|
+
let acceptLenSum = 0;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// 1. prefill → first token. For sampling, the first token must also be
|
|
187
|
+
// sampled from the target dist (not argmax) to stay lossless.
|
|
188
|
+
const t0 = performance.now();
|
|
189
|
+
const ids = ops.fromInt32(promptTokens, [1, promptTokens.length]);
|
|
190
|
+
const hidden = model.forwardHidden(ids, caches);
|
|
191
|
+
ids.dispose();
|
|
192
|
+
stats.targetCalls++;
|
|
193
|
+
const H = hidden.shape[2]!;
|
|
194
|
+
const Lp = hidden.shape[1]!;
|
|
195
|
+
let next: number;
|
|
196
|
+
if (sampleCfg) {
|
|
197
|
+
const hSl = hidden.slice([0, Lp - 1, 0], [1, Lp, H]);
|
|
198
|
+
const logits = model.logitsFromHidden(hSl);
|
|
199
|
+
hSl.dispose();
|
|
200
|
+
const flat = ops.reshape(logits, [1, logits.shape[2]!]);
|
|
201
|
+
logits.dispose();
|
|
202
|
+
const scaled = processLogits(flat, sampleCfg);
|
|
203
|
+
flat.dispose();
|
|
204
|
+
next = sampleToken(scaled, keys.next());
|
|
205
|
+
scaled.dispose();
|
|
206
|
+
} else {
|
|
207
|
+
next = pickFromHidden(model, hidden, Lp - 1);
|
|
208
|
+
}
|
|
209
|
+
let lastHidden = hidden.slice([0, Lp - 1, 0], [1, Lp, H]); // [1,1,H]
|
|
210
|
+
hidden.dispose();
|
|
211
|
+
stats.prefillMs = performance.now() - t0;
|
|
212
|
+
|
|
213
|
+
const nextIsEos = eosTokenIds.includes(next);
|
|
214
|
+
if (!nextIsEos) out.push(next);
|
|
215
|
+
stats.emitted++;
|
|
216
|
+
if (nextIsEos) { lastHidden.dispose(); return finalize(stats, out, rounds, acceptLenSum, confSum); }
|
|
217
|
+
|
|
218
|
+
// 2. outer loop
|
|
219
|
+
const tDecode = performance.now();
|
|
220
|
+
outer: while (stats.emitted < maxTokens) {
|
|
221
|
+
// 2a. draft the γ-block: one backbone pass + sequential Markov head
|
|
222
|
+
const hCtx = ops.reshape(lastHidden, [1, H]); // [1,H]
|
|
223
|
+
const block = drafter.forwardInfer(model, hCtx, next, gamma, { sample: sampleCfg ?? undefined, keys });
|
|
224
|
+
hCtx.dispose();
|
|
225
|
+
const drafts = block.tokens;
|
|
226
|
+
stats.drafted += gamma;
|
|
227
|
+
for (const c of block.conf) confSum += c;
|
|
228
|
+
|
|
229
|
+
// 2b. verify all γ drafts (+ the pending token) in one forward
|
|
230
|
+
const vIds = ops.fromInt32([next, ...drafts], [1, gamma + 1]);
|
|
231
|
+
const vHidden = model.forwardHidden(vIds, caches);
|
|
232
|
+
vIds.dispose();
|
|
233
|
+
stats.targetCalls++;
|
|
234
|
+
|
|
235
|
+
// 2c. accept (greedy longest-prefix OR speculative sampling)
|
|
236
|
+
const { kAccept, emit } = sampleCfg
|
|
237
|
+
? verifySampling(model, vHidden, block.draftLogits, drafts, gamma, sampleCfg, keys)
|
|
238
|
+
: verifyGreedy(model, vHidden, drafts, gamma);
|
|
239
|
+
block.draftLogits.dispose();
|
|
240
|
+
stats.accepted += kAccept;
|
|
241
|
+
rounds++;
|
|
242
|
+
acceptLenSum += kAccept + 1; // accepted drafts + bonus/correction token
|
|
243
|
+
|
|
244
|
+
// 2d. emit accepted drafts
|
|
245
|
+
for (let k = 0; k < kAccept; k++) {
|
|
246
|
+
const isEos = eosTokenIds.includes(drafts[k]!);
|
|
247
|
+
if (!isEos) out.push(drafts[k]!);
|
|
248
|
+
stats.emitted++;
|
|
249
|
+
if (isEos || stats.emitted >= maxTokens) { vHidden.dispose(); break outer; }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 2e. emit the correction (or bonus when all accepted)
|
|
253
|
+
const emitIsEos = eosTokenIds.includes(emit);
|
|
254
|
+
if (!emitIsEos) out.push(emit);
|
|
255
|
+
stats.emitted++;
|
|
256
|
+
|
|
257
|
+
// 2f. roll back rejected entries. bypass=true: we always trim the last
|
|
258
|
+
// n = gamma - kAccept (≤ γ) tips immediately after the verify concat
|
|
259
|
+
// write, which is the one case where trimming past the sliding window is
|
|
260
|
+
// sound (the newest tokens are intact at the buffer tail). See
|
|
261
|
+
// RotatingKVCache.trim.
|
|
262
|
+
if (kAccept < gamma) {
|
|
263
|
+
const n = gamma - kAccept;
|
|
264
|
+
for (const c of caches) c.trim(n, true);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (emitIsEos || stats.emitted >= maxTokens) { vHidden.dispose(); break; }
|
|
268
|
+
|
|
269
|
+
// 2g. chain state
|
|
270
|
+
next = emit;
|
|
271
|
+
lastHidden.dispose();
|
|
272
|
+
lastHidden = vHidden.slice([0, kAccept, 0], [1, kAccept + 1, H]);
|
|
273
|
+
vHidden.dispose();
|
|
274
|
+
}
|
|
275
|
+
stats.decodeMs = performance.now() - tDecode;
|
|
276
|
+
lastHidden.dispose();
|
|
277
|
+
} finally {
|
|
278
|
+
for (const c of caches) c.dispose();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return finalize(stats, out, rounds, acceptLenSum, confSum);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function finalize(stats: DSparkStats, out: number[], rounds: number, acceptLenSum: number, confSum: number): DSparkResult {
|
|
285
|
+
stats.acceptanceRate = stats.drafted ? stats.accepted / stats.drafted : 0;
|
|
286
|
+
stats.meanAcceptLen = rounds ? acceptLenSum / rounds : 0;
|
|
287
|
+
stats.meanConf = stats.drafted ? confSum / stats.drafted : 0;
|
|
288
|
+
stats.decodeTps = stats.decodeMs ? ((stats.emitted - 1) / stats.decodeMs) * 1000 : 0;
|
|
289
|
+
return { tokens: out, stats };
|
|
290
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// DSpark training objective (paper §3.3, Eq 9–12).
|
|
2
|
+
//
|
|
3
|
+
// L = α_ce·L_ce + α_tv·L_tv + α_conf·L_conf (defaults 0.1 / 0.9 / 1.0)
|
|
4
|
+
//
|
|
5
|
+
// all three position-weighted by w_k = exp(−(k−1)/γ) (emphasizes earlier
|
|
6
|
+
// block positions, which contribute most to prefix-based accepted length):
|
|
7
|
+
//
|
|
8
|
+
// L_ce = −Σ_k w_k · log p^d_k(x*_k) (Eq 9)
|
|
9
|
+
// L_tv = Σ_k w_k · ‖p^d_k − p^t_k‖₁ (Eq 10)
|
|
10
|
+
// L_conf = −Σ_k w_k · [ c*_k·log c_k + (1−c*_k)·log(1−c_k) ] (Eq 11)
|
|
11
|
+
//
|
|
12
|
+
// where the analytic acceptance label c*_k = 1 − ½‖p^d_k − p^t_k‖₁ (Eq 8) is
|
|
13
|
+
// STOP-GRADIENT'd — it supervises the confidence head without leaking BCE
|
|
14
|
+
// gradient back into the draft distribution (L_tv already drives p^d→p^t).
|
|
15
|
+
//
|
|
16
|
+
// p^t is the FROZEN target's true distribution at each block position; the
|
|
17
|
+
// caller computes targetLogits = model.logitsFromHidden(targetHidden) (same
|
|
18
|
+
// head DSpark drafts with) and passes it as a constant. M = A·γ is small
|
|
19
|
+
// (γ≈5–7), so plain MLX materialization is fine; a fused TV kernel (the
|
|
20
|
+
// flash-cce vocab-blocked pattern) is only worth porting if measurement
|
|
21
|
+
// shows this term bottlenecks — it does not at these sizes.
|
|
22
|
+
|
|
23
|
+
import { MlxArray } from "../../mlx/array";
|
|
24
|
+
import * as ops from "../../mlx/ops";
|
|
25
|
+
import type { DSparkTrainOut } from "./module";
|
|
26
|
+
|
|
27
|
+
export interface LossWeights {
|
|
28
|
+
ce: number;
|
|
29
|
+
tv: number;
|
|
30
|
+
conf: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_LOSS_WEIGHTS: LossWeights = { ce: 0.1, tv: 0.9, conf: 1.0 };
|
|
34
|
+
|
|
35
|
+
/** Position weights w_k = exp(−(k−1)/γ) as a [1, γ] float32 row. */
|
|
36
|
+
export function positionWeights(gamma: number): MlxArray {
|
|
37
|
+
const w = new Float32Array(gamma);
|
|
38
|
+
for (let j = 0; j < gamma; j++) w[j] = Math.exp(-j / gamma);
|
|
39
|
+
return MlxArray.fromFloat32(w, [1, gamma]).eval();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** mean_A( Σ_k w_k · perPos[A,k] ) → scalar. */
|
|
43
|
+
function weightedReduce(perPos: MlxArray, w: MlxArray): MlxArray {
|
|
44
|
+
const A = perPos.shape[0]!;
|
|
45
|
+
const weighted = ops.mul(perPos, w); // [A,γ] · [1,γ] broadcast
|
|
46
|
+
const overK = ops.sumAxis(weighted, 1, false); // [A]
|
|
47
|
+
weighted.dispose();
|
|
48
|
+
const summed = ops.sumAxis(overK, 0, false); // scalar
|
|
49
|
+
overK.dispose();
|
|
50
|
+
const out = ops.mulScalar(summed, 1 / A);
|
|
51
|
+
summed.dispose();
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Per-position L1 distance ‖p^d − p^t‖₁ → [A, γ]. Caller owns the result. */
|
|
56
|
+
function l1PerPos(pd: MlxArray, pt: MlxArray): MlxArray {
|
|
57
|
+
const diff = ops.sub(pd, pt);
|
|
58
|
+
const ad = ops.abs(diff);
|
|
59
|
+
diff.dispose();
|
|
60
|
+
const l1 = ops.sumAxis(ad, 2, false); // [A,γ]
|
|
61
|
+
ad.dispose();
|
|
62
|
+
return l1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Full DSpark loss. Returns the scalar to differentiate plus the three term
|
|
67
|
+
* scalars (for logging — they share the graph; read them with .toFloat32()
|
|
68
|
+
* AFTER the backward, or eval them alongside the value).
|
|
69
|
+
*
|
|
70
|
+
* - out.draftLogits [A,γ,V] (U+B, pre-softmax, float32)
|
|
71
|
+
* - out.conf [A,γ] (sigmoid output ∈ (0,1))
|
|
72
|
+
* - targetLogits [A,γ,V] (frozen target logits; constant)
|
|
73
|
+
* - xStar [A,γ] int32 ground-truth next tokens
|
|
74
|
+
*/
|
|
75
|
+
export function dsparkLoss(
|
|
76
|
+
out: DSparkTrainOut,
|
|
77
|
+
targetLogits: MlxArray,
|
|
78
|
+
xStar: MlxArray,
|
|
79
|
+
gamma: number,
|
|
80
|
+
w: MlxArray,
|
|
81
|
+
weights: LossWeights = DEFAULT_LOSS_WEIGHTS,
|
|
82
|
+
): { loss: MlxArray; ce: MlxArray; tv: MlxArray; conf: MlxArray } {
|
|
83
|
+
const dl = out.draftLogits; // [A,γ,V]
|
|
84
|
+
const A = dl.shape[0]!;
|
|
85
|
+
|
|
86
|
+
const pd = ops.softmaxAxis(dl, 2, true); // [A,γ,V]
|
|
87
|
+
const ptRaw = ops.softmaxAxis(targetLogits, 2, true);
|
|
88
|
+
const pt = ops.stopGradient(ptRaw); // target is constant
|
|
89
|
+
ptRaw.dispose();
|
|
90
|
+
|
|
91
|
+
// --- L_ce: −log p^d(x*) ---
|
|
92
|
+
const lse = ops.logsumexpAxis(dl, 2, true); // [A,γ,1]
|
|
93
|
+
const logp = ops.sub(dl, lse); // [A,γ,V]
|
|
94
|
+
lse.dispose();
|
|
95
|
+
const xsIdx = ops.expandDims(xStar, 2); // [A,γ,1] int32
|
|
96
|
+
const logpStar = ops.takeAlongAxis(logp, xsIdx, 2); // [A,γ,1]
|
|
97
|
+
logp.dispose(); xsIdx.dispose();
|
|
98
|
+
const cePos0 = ops.reshape(logpStar, [A, gamma]); // [A,γ]
|
|
99
|
+
logpStar.dispose();
|
|
100
|
+
const cePos = ops.neg(cePos0); // −log p^d(x*)
|
|
101
|
+
cePos0.dispose();
|
|
102
|
+
const ceTerm = weightedReduce(cePos, w);
|
|
103
|
+
cePos.dispose();
|
|
104
|
+
|
|
105
|
+
// --- L1 / TV / c* (share the L1) ---
|
|
106
|
+
const l1 = l1PerPos(pd, pt); // [A,γ]
|
|
107
|
+
pd.dispose(); pt.dispose();
|
|
108
|
+
const tvTerm = weightedReduce(l1, w);
|
|
109
|
+
|
|
110
|
+
// c*_k = 1 − ½·l1, stop-gradient (supervision label)
|
|
111
|
+
const half = ops.mulScalar(l1, 0.5);
|
|
112
|
+
const cStarRaw = rsubScalar(half, 1.0); // 1 − ½l1
|
|
113
|
+
half.dispose();
|
|
114
|
+
const cStar = ops.stopGradient(cStarRaw);
|
|
115
|
+
cStarRaw.dispose();
|
|
116
|
+
l1.dispose();
|
|
117
|
+
|
|
118
|
+
// --- L_conf: BCE(c, c*) ---
|
|
119
|
+
const eps = 1e-6;
|
|
120
|
+
const c = clampUnit(out.conf, eps);
|
|
121
|
+
const omc = rsubScalar(c, 1.0); // 1 − c
|
|
122
|
+
const logc = ops.log(c);
|
|
123
|
+
const log1mc = ops.log(omc);
|
|
124
|
+
c.dispose(); omc.dispose();
|
|
125
|
+
const t1 = ops.mul(cStar, logc); // c*·log c
|
|
126
|
+
const omcStar = rsubScalar(cStar, 1.0); // 1 − c*
|
|
127
|
+
cStar.dispose(); logc.dispose();
|
|
128
|
+
const t2 = ops.mul(omcStar, log1mc); // (1−c*)·log(1−c)
|
|
129
|
+
omcStar.dispose(); log1mc.dispose();
|
|
130
|
+
const bcePos0 = ops.add(t1, t2);
|
|
131
|
+
t1.dispose(); t2.dispose();
|
|
132
|
+
const bcePos = ops.neg(bcePos0);
|
|
133
|
+
bcePos0.dispose();
|
|
134
|
+
const confTerm = weightedReduce(bcePos, w);
|
|
135
|
+
bcePos.dispose();
|
|
136
|
+
|
|
137
|
+
// --- weighted sum ---
|
|
138
|
+
const a = ops.mulScalar(ceTerm, weights.ce);
|
|
139
|
+
const b = ops.mulScalar(tvTerm, weights.tv);
|
|
140
|
+
const cc = ops.mulScalar(confTerm, weights.conf);
|
|
141
|
+
const ab = ops.add(a, b); a.dispose(); b.dispose();
|
|
142
|
+
const loss = ops.add(ab, cc); ab.dispose(); cc.dispose();
|
|
143
|
+
return { loss, ce: ceTerm, tv: tvTerm, conf: confTerm };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Held-out monitoring (NO gradient): analytic per-position acceptance
|
|
148
|
+
* a_k = 1 − ½‖p^d_k − p^t_k‖₁ and the expected accepted length
|
|
149
|
+
* τ = 1 + Σ_{j=1}^{γ} Π_{i≤j} a_i (the +1 is the target bonus token). This is
|
|
150
|
+
* the cheap proxy to early-stop on — τ is the number that matters, not loss.
|
|
151
|
+
* Returns means over the batch.
|
|
152
|
+
*/
|
|
153
|
+
export function analyticAcceptance(
|
|
154
|
+
out: DSparkTrainOut,
|
|
155
|
+
targetLogits: MlxArray,
|
|
156
|
+
): { perPos: number[]; tau: number } {
|
|
157
|
+
const dl = out.draftLogits;
|
|
158
|
+
const A = dl.shape[0]!;
|
|
159
|
+
const gamma = dl.shape[1]!;
|
|
160
|
+
const pd = ops.softmaxAxis(dl, 2, true);
|
|
161
|
+
const pt = ops.softmaxAxis(targetLogits, 2, true);
|
|
162
|
+
const l1 = l1PerPos(pd, pt); // [A,γ]
|
|
163
|
+
pd.dispose(); pt.dispose();
|
|
164
|
+
const half = ops.mulScalar(l1, 0.5);
|
|
165
|
+
l1.dispose();
|
|
166
|
+
const aMat = rsubScalar(half, 1.0); // [A,γ] = a_k per anchor
|
|
167
|
+
half.dispose();
|
|
168
|
+
const flat = aMat.toFloat32(); // [A*γ]
|
|
169
|
+
aMat.dispose();
|
|
170
|
+
|
|
171
|
+
const perPos: number[] = new Array(gamma).fill(0);
|
|
172
|
+
let tauSum = 0;
|
|
173
|
+
for (let row = 0; row < A; row++) {
|
|
174
|
+
let cum = 1;
|
|
175
|
+
let tau = 1; // bonus token
|
|
176
|
+
for (let j = 0; j < gamma; j++) {
|
|
177
|
+
const a = flat[row * gamma + j]!;
|
|
178
|
+
perPos[j]! += a;
|
|
179
|
+
cum *= a;
|
|
180
|
+
tau += cum;
|
|
181
|
+
}
|
|
182
|
+
tauSum += tau;
|
|
183
|
+
}
|
|
184
|
+
for (let j = 0; j < gamma; j++) perPos[j]! /= A;
|
|
185
|
+
return { perPos, tau: tauSum / A };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- small scalar helpers (no direct rsub/clamp binding) ---------------
|
|
189
|
+
|
|
190
|
+
/** value − a, elementwise (value + (−a)). */
|
|
191
|
+
function rsubScalar(a: MlxArray, value: number): MlxArray {
|
|
192
|
+
const neg = ops.neg(a);
|
|
193
|
+
const res = addScalarF32(neg, value);
|
|
194
|
+
neg.dispose();
|
|
195
|
+
return res;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** a + value (float32). */
|
|
199
|
+
function addScalarF32(a: MlxArray, value: number): MlxArray {
|
|
200
|
+
const s = MlxArray.fromFloat32(new Float32Array([value]), [1]);
|
|
201
|
+
const out = ops.add(a, s);
|
|
202
|
+
s.dispose();
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** clamp x into [eps, 1−eps]. */
|
|
207
|
+
function clampUnit(x: MlxArray, eps: number): MlxArray {
|
|
208
|
+
const lo = MlxArray.fromFloat32(new Float32Array([eps]), [1]);
|
|
209
|
+
const hi = MlxArray.fromFloat32(new Float32Array([1 - eps]), [1]);
|
|
210
|
+
const out = ops.clip(x, lo, hi);
|
|
211
|
+
lo.dispose(); hi.dispose();
|
|
212
|
+
return out;
|
|
213
|
+
}
|