mlx-bun 0.0.7 → 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/diffusion-gemma-port.md +602 -0
- 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 +54 -3
- package/src/diffusion/diffusion-generate.ts +436 -0
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +19 -7
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +196 -19
- 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 +11 -0
- package/src/mlx/ops.ts +113 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/diffusion-gemma.ts +1026 -0
- package/src/model/factory.ts +35 -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 +45 -7
- 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 +90 -18
- package/src/server.ts +643 -50
- 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/diffusion-lora.ts +216 -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/vision/diffusion-vision.ts +620 -0
- package/src/web/skills/memory/SKILL.md +40 -17
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
// DSpark draft module — a trainable, semi-autoregressive speculative drafter
|
|
2
|
+
// for frozen Gemma-4 e4b. Port of the architecture in DeepSeek's DSpark paper
|
|
3
|
+
// ("Confidence-Scheduled Speculative Decoding with Semi-Autoregressive
|
|
4
|
+
// Generation"), instantiated against this repo's e4b spine.
|
|
5
|
+
//
|
|
6
|
+
// Three sub-parts (paper §3.1–3.2), all reading e4b's tapped hidden state:
|
|
7
|
+
// 1. DFlash backbone — a lightweight bidirectional transformer over a
|
|
8
|
+
// γ-token block (anchor + γ−1 learned mask tokens) plus one injected
|
|
9
|
+
// context token (the tapped target hidden, H_ctx). One forward pass
|
|
10
|
+
// predicts all γ draft positions in parallel. (paper Eq 2–3, m=1
|
|
11
|
+
// single-vector context for v1.)
|
|
12
|
+
// 2. Markov head — low-rank first-order transition bias added to the base
|
|
13
|
+
// logits, B(x_{k-1},·) = W1[x_{k-1}]·W2, r=256. W2 INIT 0 so the module
|
|
14
|
+
// starts as the pure-parallel DFlash baseline and τ climbs as the head
|
|
15
|
+
// learns intra-block dependency. (paper Eq 5)
|
|
16
|
+
// 3. Confidence head — c_k = σ(wᵀ[h_k; W1[x_{k-1}]]) ∈ (0,1), per-position
|
|
17
|
+
// predicted acceptance probability. (paper Eq 7)
|
|
18
|
+
//
|
|
19
|
+
// It SHARES e4b's frozen embedding + LM head (logits = the same head the
|
|
20
|
+
// target verifies with, so draft tokens live in the same vocab/logit space).
|
|
21
|
+
// Only the parts here are trainable; the target stays frozen.
|
|
22
|
+
//
|
|
23
|
+
// Tap point (v1, by design decision): the final post-norm hidden from
|
|
24
|
+
// model.forwardHidden — the m=1 degenerate of DFlash's multi-layer H_ctx,
|
|
25
|
+
// and exactly what the existing GemmaAssistantDrafter consumes. Zero changes
|
|
26
|
+
// to the parity-gated model files. Richer context injection (intermediate
|
|
27
|
+
// post-PLE residual, multi-layer concat, or borrowing the target's prefix
|
|
28
|
+
// K/V à la GemmaAssistantDrafter) is the main τ lever after v1.
|
|
29
|
+
|
|
30
|
+
import { MlxArray } from "../../mlx/array";
|
|
31
|
+
import { Dtype } from "../../mlx/ffi";
|
|
32
|
+
import * as ops from "../../mlx/ops";
|
|
33
|
+
import type { Gemma4Model } from "../../model/gemma4";
|
|
34
|
+
import { loadAdapterTensors } from "../../lora";
|
|
35
|
+
import { writeShardedSafetensors, type NamedTensor } from "../../quantize/safetensors-writer";
|
|
36
|
+
import { processLogits, sampleToken, KeyStream, type DSparkSampleConfig } from "./sample";
|
|
37
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
38
|
+
import { join } from "node:path";
|
|
39
|
+
|
|
40
|
+
export interface DSparkConfig {
|
|
41
|
+
/** Max draft-block size γ (paper default 5). */
|
|
42
|
+
gamma: number;
|
|
43
|
+
/** Backbone hidden width. */
|
|
44
|
+
dDraft: number;
|
|
45
|
+
/** Backbone depth (paper default 5; 2 already beats 5-layer DFlash). */
|
|
46
|
+
nLayers: number;
|
|
47
|
+
/** Backbone attention heads (dDraft must be divisible). */
|
|
48
|
+
nHeads: number;
|
|
49
|
+
/** Markov low-rank dim r (paper default 256). */
|
|
50
|
+
markovRank: number;
|
|
51
|
+
/** FFN expansion multiple (dInter = ffMult·dDraft). */
|
|
52
|
+
ffMult: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const DEFAULT_DSPARK_CONFIG: DSparkConfig = {
|
|
56
|
+
gamma: 5,
|
|
57
|
+
dDraft: 1024,
|
|
58
|
+
nLayers: 2,
|
|
59
|
+
nHeads: 8,
|
|
60
|
+
markovRank: 256,
|
|
61
|
+
ffMult: 2,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Dims read from the live target model — never hardcoded. */
|
|
65
|
+
export interface TargetDims {
|
|
66
|
+
hiddenSize: number;
|
|
67
|
+
vocabSize: number;
|
|
68
|
+
eps: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Output of the parallel training forward over a batch of A anchors. */
|
|
72
|
+
export interface DSparkTrainOut {
|
|
73
|
+
/** Draft logits U_k + B_k, pre-softmax. [A, γ, V] (float32). */
|
|
74
|
+
draftLogits: MlxArray;
|
|
75
|
+
/** Per-position predicted acceptance probability c_k ∈ (0,1). [A, γ]. */
|
|
76
|
+
conf: MlxArray;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Output of one inference draft-block generation. */
|
|
80
|
+
export interface DSparkDraftBlock {
|
|
81
|
+
/** Drafted token ids d_1..d_γ. */
|
|
82
|
+
tokens: number[];
|
|
83
|
+
/** Per-position confidence c_1..c_γ. */
|
|
84
|
+
conf: number[];
|
|
85
|
+
/** Raw per-position draft logits U_k+B_k [1, γ, V] (given the sampled prefix).
|
|
86
|
+
* The verifier reprocesses these with the same sampling config to recover the
|
|
87
|
+
* draft distribution q_k for the accept/residual rule. Caller disposes. */
|
|
88
|
+
draftLogits: MlxArray;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Options for one draft-block generation. */
|
|
92
|
+
export interface DSparkDraftOpts {
|
|
93
|
+
/** Sampling config; omit (or temperature 0) ⇒ greedy argmax. */
|
|
94
|
+
sample?: DSparkSampleConfig;
|
|
95
|
+
/** Shared key stream (so draft + verify draws are reproducible together). */
|
|
96
|
+
keys?: KeyStream;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const CDT = Dtype.float32; // compute & param dtype for the small draft module
|
|
100
|
+
|
|
101
|
+
function disposing(old: MlxArray, next: MlxArray): MlxArray {
|
|
102
|
+
old.dispose();
|
|
103
|
+
return next;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- seeded init --------------------------------------------------------
|
|
107
|
+
// xorshift32 + Box–Muller. Deterministic given a seed so checkpoints and
|
|
108
|
+
// smoke tests reproduce. (Date/Math.random are unavailable in some harness
|
|
109
|
+
// contexts; this is self-contained anyway.)
|
|
110
|
+
function makeRng(seed: number): () => number {
|
|
111
|
+
let s = seed >>> 0 || 0x9e3779b9;
|
|
112
|
+
return () => {
|
|
113
|
+
s ^= s << 13; s >>>= 0;
|
|
114
|
+
s ^= s >> 17;
|
|
115
|
+
s ^= s << 5; s >>>= 0;
|
|
116
|
+
return s / 0x100000000;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalArray(rng: () => number, shape: number[], std: number): MlxArray {
|
|
121
|
+
const n = shape.reduce((a, b) => a * b, 1);
|
|
122
|
+
const out = new Float32Array(n);
|
|
123
|
+
for (let i = 0; i < n; i += 2) {
|
|
124
|
+
const u1 = Math.max(rng(), 1e-12);
|
|
125
|
+
const u2 = rng();
|
|
126
|
+
const r = Math.sqrt(-2 * Math.log(u1));
|
|
127
|
+
out[i] = r * Math.cos(2 * Math.PI * u2) * std;
|
|
128
|
+
if (i + 1 < n) out[i + 1] = r * Math.sin(2 * Math.PI * u2) * std;
|
|
129
|
+
}
|
|
130
|
+
return MlxArray.fromFloat32(out, shape);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function zerosArray(shape: number[]): MlxArray {
|
|
134
|
+
const n = shape.reduce((a, b) => a * b, 1);
|
|
135
|
+
return MlxArray.fromFloat32(new Float32Array(n), shape);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function constArray(shape: number[], value: number): MlxArray {
|
|
139
|
+
const n = shape.reduce((a, b) => a * b, 1);
|
|
140
|
+
return MlxArray.fromFloat32(new Float32Array(n).fill(value), shape);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class DSparkDrafter {
|
|
144
|
+
readonly cfg: DSparkConfig;
|
|
145
|
+
readonly dims: TargetDims;
|
|
146
|
+
/** Stamped onto the checkpoint so the registry can key it to its target. */
|
|
147
|
+
readonly targetId: string;
|
|
148
|
+
/** Trainable params keyed by stable name. */
|
|
149
|
+
#p = new Map<string, MlxArray>();
|
|
150
|
+
/** Canonical flat order — the only source of truth for param ordering. */
|
|
151
|
+
readonly names: string[];
|
|
152
|
+
|
|
153
|
+
private constructor(cfg: DSparkConfig, dims: TargetDims, targetId: string) {
|
|
154
|
+
this.cfg = cfg;
|
|
155
|
+
this.dims = dims;
|
|
156
|
+
this.targetId = targetId;
|
|
157
|
+
if (cfg.dDraft % cfg.nHeads !== 0)
|
|
158
|
+
throw new Error(`dDraft ${cfg.dDraft} not divisible by nHeads ${cfg.nHeads}`);
|
|
159
|
+
this.names = buildNames(cfg);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static dimsOf(model: Gemma4Model): TargetDims {
|
|
163
|
+
const t = model.config.text;
|
|
164
|
+
return { hiddenSize: t.hiddenSize, vocabSize: t.vocabSize, eps: t.rmsNormEps };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Fresh, randomly initialized module sized to `model`. W2=0 (pure DFlash
|
|
168
|
+
* at init), confidence head zero (c≈0.5 at init). */
|
|
169
|
+
static init(model: Gemma4Model, cfg: DSparkConfig, targetId: string, seed = 0): DSparkDrafter {
|
|
170
|
+
return DSparkDrafter.initFromDims(DSparkDrafter.dimsOf(model), cfg, targetId, seed);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** As `init` but sized from explicit dims (no model needed — used by tests
|
|
174
|
+
* and the offline checkpoint path). */
|
|
175
|
+
static initFromDims(dims: TargetDims, cfg: DSparkConfig, targetId: string, seed = 0): DSparkDrafter {
|
|
176
|
+
const d = new DSparkDrafter(cfg, dims, targetId);
|
|
177
|
+
const rng = makeRng(seed);
|
|
178
|
+
const { dDraft, nLayers, markovRank: r, ffMult } = cfg;
|
|
179
|
+
const dInter = ffMult * dDraft;
|
|
180
|
+
const { hiddenSize: H, vocabSize: V } = d.dims;
|
|
181
|
+
const set = (n: string, a: MlxArray) => d.#p.set(n, a.eval());
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < nLayers; i++) {
|
|
184
|
+
set(`bb.${i}.attn_norm`, constArray([dDraft], 1.0));
|
|
185
|
+
for (const w of ["q", "k", "v", "o"]) set(`bb.${i}.${w}`, normalArray(rng, [dDraft, dDraft], 0.02));
|
|
186
|
+
set(`bb.${i}.ff_norm`, constArray([dDraft], 1.0));
|
|
187
|
+
set(`bb.${i}.gate`, normalArray(rng, [dDraft, dInter], 0.02));
|
|
188
|
+
set(`bb.${i}.up`, normalArray(rng, [dDraft, dInter], 0.02));
|
|
189
|
+
set(`bb.${i}.down`, normalArray(rng, [dInter, dDraft], 0.02));
|
|
190
|
+
}
|
|
191
|
+
set("ctx_proj", normalArray(rng, [H, dDraft], 0.02));
|
|
192
|
+
set("tok_proj", normalArray(rng, [H, dDraft], 0.02));
|
|
193
|
+
set("mask_emb", normalArray(rng, [cfg.gamma - 1, dDraft], 0.02));
|
|
194
|
+
set("pos_emb", normalArray(rng, [cfg.gamma + 1, dDraft], 0.02));
|
|
195
|
+
set("out_norm", constArray([dDraft], 1.0));
|
|
196
|
+
set("out_proj", normalArray(rng, [dDraft, H], 0.02));
|
|
197
|
+
set("markov.w1", normalArray(rng, [V, r], 0.02));
|
|
198
|
+
set("markov.w2", zerosArray([r, V])); // ← starts as pure DFlash
|
|
199
|
+
set("conf.w", zerosArray([dDraft + r, 1])); // ← c≈0.5 at init
|
|
200
|
+
set("conf.b", zerosArray([1]));
|
|
201
|
+
return d;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get(name: string): MlxArray {
|
|
205
|
+
const a = this.#p.get(name);
|
|
206
|
+
if (!a) throw new Error(`DSpark param not found: ${name}`);
|
|
207
|
+
return a;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Params in canonical flat order — the leaves ValueAndGrad differentiates. */
|
|
211
|
+
flatParams(): MlxArray[] {
|
|
212
|
+
return this.names.map((n) => this.get(n));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Run `fn` with `primals` temporarily installed as the params (then restore
|
|
216
|
+
* the originals). Mirrors the LoRA swapPrimals discipline — lets a
|
|
217
|
+
* ValueAndGrad closure differentiate w.r.t. the flat param list while the
|
|
218
|
+
* forward reads params by name. */
|
|
219
|
+
useParams<T>(primals: MlxArray[], fn: () => T): T {
|
|
220
|
+
if (primals.length !== this.names.length)
|
|
221
|
+
throw new Error(`useParams: ${primals.length} primals vs ${this.names.length} params`);
|
|
222
|
+
const saved = this.names.map((n) => this.get(n));
|
|
223
|
+
this.names.forEach((n, i) => this.#p.set(n, primals[i]!));
|
|
224
|
+
try {
|
|
225
|
+
return fn();
|
|
226
|
+
} finally {
|
|
227
|
+
this.names.forEach((n, i) => this.#p.set(n, saved[i]!));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Install the optimizer's updated leaf as the i-th param. Does NOT dispose
|
|
232
|
+
* the previous one — AdamW.step already freed it. Use as AdamW's write
|
|
233
|
+
* callback: `new AdamW(d.flatParams(), opts, (i, p) => d.installParam(i, p))`. */
|
|
234
|
+
installParam(i: number, p: MlxArray): void {
|
|
235
|
+
this.#p.set(this.names[i]!, p);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- backbone -----------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/** One pre-norm bidirectional transformer layer over [A, S, dDraft]. */
|
|
241
|
+
#layer(i: number, x: MlxArray, A: number, S: number): MlxArray {
|
|
242
|
+
const { dDraft, nHeads } = this.cfg;
|
|
243
|
+
const hd = dDraft / nHeads;
|
|
244
|
+
const eps = this.dims.eps;
|
|
245
|
+
|
|
246
|
+
// attention (full / bidirectional — no mask)
|
|
247
|
+
const residual = x;
|
|
248
|
+
let n = ops.rmsNorm(x, this.get(`bb.${i}.attn_norm`), eps);
|
|
249
|
+
const proj = (w: string) => {
|
|
250
|
+
const m = ops.matmul(n, this.get(`bb.${i}.${w}`)); // [A,S,dDraft]
|
|
251
|
+
const r = ops.reshape(m, [A, S, nHeads, hd]);
|
|
252
|
+
m.dispose();
|
|
253
|
+
const t = ops.transposeAxes(r, [0, 2, 1, 3]); // [A,nHeads,S,hd]
|
|
254
|
+
r.dispose();
|
|
255
|
+
return t;
|
|
256
|
+
};
|
|
257
|
+
const q = proj("q");
|
|
258
|
+
const k = proj("k");
|
|
259
|
+
const v = proj("v");
|
|
260
|
+
n.dispose();
|
|
261
|
+
let attn = ops.sdpa(q, k, v, Math.pow(hd, -0.5), "", null);
|
|
262
|
+
q.dispose(); k.dispose(); v.dispose();
|
|
263
|
+
attn = disposing(attn, ops.transposeAxes(attn, [0, 2, 1, 3])); // [A,S,nHeads,hd]
|
|
264
|
+
attn = disposing(attn, ops.reshape(attn, [A, S, dDraft]));
|
|
265
|
+
attn = disposing(attn, ops.matmul(attn, this.get(`bb.${i}.o`)));
|
|
266
|
+
x = ops.add(residual, attn);
|
|
267
|
+
attn.dispose();
|
|
268
|
+
|
|
269
|
+
// gated-GELU FFN
|
|
270
|
+
const res2 = x;
|
|
271
|
+
n = ops.rmsNorm(x, this.get(`bb.${i}.ff_norm`), eps);
|
|
272
|
+
const g = ops.matmul(n, this.get(`bb.${i}.gate`));
|
|
273
|
+
const u = ops.matmul(n, this.get(`bb.${i}.up`));
|
|
274
|
+
n.dispose();
|
|
275
|
+
const act = ops.geluApprox(g);
|
|
276
|
+
g.dispose();
|
|
277
|
+
let mlp = ops.mul(act, u);
|
|
278
|
+
act.dispose(); u.dispose();
|
|
279
|
+
mlp = disposing(mlp, ops.matmul(mlp, this.get(`bb.${i}.down`)));
|
|
280
|
+
const out = ops.add(res2, mlp);
|
|
281
|
+
res2.dispose(); mlp.dispose();
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Build the [A, γ+1, dDraft] input sequence: [ctxToken, anchor, mask₁..].
|
|
286
|
+
* hCtx [A,H], anchorEmb [A,H] (raw embedding, no scale). */
|
|
287
|
+
#buildSeq(hCtx: MlxArray, anchorEmb: MlxArray, A: number): MlxArray {
|
|
288
|
+
const { gamma, dDraft } = this.cfg;
|
|
289
|
+
const posEmb = this.get("pos_emb"); // [γ+1, dDraft]
|
|
290
|
+
|
|
291
|
+
// backbone math is float32; cast the (possibly bf16) target tensors in.
|
|
292
|
+
const hCtxF = hCtx.dtype === CDT ? hCtx : hCtx.astype(CDT);
|
|
293
|
+
const anchorF = anchorEmb.dtype === CDT ? anchorEmb : anchorEmb.astype(CDT);
|
|
294
|
+
|
|
295
|
+
// ctx token = ctx_proj(hCtx) + pos[0]
|
|
296
|
+
let ctx = ops.matmul(hCtxF, this.get("ctx_proj")); // [A,dDraft]
|
|
297
|
+
if (hCtxF !== hCtx) hCtxF.dispose();
|
|
298
|
+
const pos0 = posEmb.slice([0, 0], [1, dDraft]);
|
|
299
|
+
ctx = disposing(ctx, ops.add(ctx, pos0));
|
|
300
|
+
pos0.dispose();
|
|
301
|
+
ctx = disposing(ctx, ops.reshape(ctx, [A, 1, dDraft]));
|
|
302
|
+
|
|
303
|
+
// anchor token (block position 0) = tok_proj(anchorEmb) + pos[1]
|
|
304
|
+
let anchor = ops.matmul(anchorF, this.get("tok_proj")); // [A,dDraft]
|
|
305
|
+
if (anchorF !== anchorEmb) anchorF.dispose();
|
|
306
|
+
const pos1 = posEmb.slice([1, 0], [2, dDraft]);
|
|
307
|
+
anchor = disposing(anchor, ops.add(anchor, pos1));
|
|
308
|
+
pos1.dispose();
|
|
309
|
+
anchor = disposing(anchor, ops.reshape(anchor, [A, 1, dDraft]));
|
|
310
|
+
|
|
311
|
+
const parts = [ctx, anchor];
|
|
312
|
+
if (gamma > 1) {
|
|
313
|
+
// mask tokens (block positions 1..γ-1) = mask_emb[j-1] + pos[j+1], built
|
|
314
|
+
// once at batch 1 then broadcast to A in a single add.
|
|
315
|
+
const maskEmb = this.get("mask_emb"); // [γ-1, dDraft]
|
|
316
|
+
const posMask = posEmb.slice([2, 0], [gamma + 1, dDraft]); // pos[2..γ] → [γ-1,dDraft]
|
|
317
|
+
let masks = ops.add(maskEmb, posMask); // [γ-1, dDraft]
|
|
318
|
+
posMask.dispose();
|
|
319
|
+
masks = disposing(masks, ops.reshape(masks, [1, gamma - 1, dDraft]));
|
|
320
|
+
const zerosA = MlxArray.fromFloat32(new Float32Array(A * dDraft), [A, 1, dDraft]);
|
|
321
|
+
const masksA = ops.add(masks, zerosA); // broadcast → [A, γ-1, dDraft]
|
|
322
|
+
masks.dispose(); zerosA.dispose();
|
|
323
|
+
parts.push(masksA);
|
|
324
|
+
}
|
|
325
|
+
const seq = ops.concatAxis(parts, 1); // [A, γ+1, dDraft]
|
|
326
|
+
for (const p of parts) p.dispose();
|
|
327
|
+
return seq;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Run the backbone and return the per-block-position hidden h_1..h_γ
|
|
331
|
+
* [A, γ, dDraft]. */
|
|
332
|
+
#backbone(hCtx: MlxArray, anchorEmb: MlxArray, A: number): MlxArray {
|
|
333
|
+
const { gamma, dDraft } = this.cfg;
|
|
334
|
+
const S = gamma + 1;
|
|
335
|
+
let h = this.#buildSeq(hCtx, anchorEmb, A);
|
|
336
|
+
for (let i = 0; i < this.cfg.nLayers; i++) h = disposing(h, this.#layer(i, h, A, S));
|
|
337
|
+
h = disposing(h, ops.rmsNorm(h, this.get("out_norm"), this.dims.eps));
|
|
338
|
+
// drop the ctx position (index 0); keep the γ block outputs
|
|
339
|
+
const block = h.slice([0, 1, 0], [A, S, dDraft]); // [A,γ,dDraft]
|
|
340
|
+
h.dispose();
|
|
341
|
+
return block;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Base logits U from the SHARED frozen LM head over the backbone block
|
|
345
|
+
* hidden. [A,γ,V] (model dtype). */
|
|
346
|
+
#baseLogits(model: Gemma4Model, block: MlxArray): MlxArray {
|
|
347
|
+
const hOut = ops.matmul(block, this.get("out_proj")); // [A,γ,H] (f32)
|
|
348
|
+
const hBf = hOut.astype(model.embed.scales.dtype); // head's activation dtype
|
|
349
|
+
hOut.dispose();
|
|
350
|
+
const logits = model.logitsFromHidden(hBf); // shared head (+softcap) [A,γ,V]
|
|
351
|
+
hBf.dispose();
|
|
352
|
+
return logits;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Markov transition bias B [A,γ,V] from teacher/prev tokens [A,γ] and the
|
|
356
|
+
* W1-embedding emb1 [A,γ,r] (also returned for the confidence head). */
|
|
357
|
+
#markov(prevToks: MlxArray): { B: MlxArray; emb1: MlxArray } {
|
|
358
|
+
const w1 = this.get("markov.w1"); // [V,r]
|
|
359
|
+
const w2 = this.get("markov.w2"); // [r,V]
|
|
360
|
+
const emb1 = ops.takeAxis(w1, prevToks, 0); // [A,γ,r]
|
|
361
|
+
const B = ops.matmul(emb1, w2); // [A,γ,V]
|
|
362
|
+
return { B, emb1 };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Confidence c_k = σ(wᵀ[h_k; emb1_k]). block [A,γ,dDraft], emb1 [A,γ,r]. */
|
|
366
|
+
#confidence(block: MlxArray, emb1: MlxArray): MlxArray {
|
|
367
|
+
const inp = ops.concatAxis([block, emb1], 2); // [A,γ,dDraft+r]
|
|
368
|
+
let z = ops.matmul(inp, this.get("conf.w")); // [A,γ,1]
|
|
369
|
+
inp.dispose();
|
|
370
|
+
z = disposing(z, ops.add(z, this.get("conf.b")));
|
|
371
|
+
z = disposing(z, ops.sigmoid(z));
|
|
372
|
+
const A = z.shape[0]!, G = z.shape[1]!;
|
|
373
|
+
const c = ops.reshape(z, [A, G]); // [A,γ]
|
|
374
|
+
z.dispose();
|
|
375
|
+
return c;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Parallel TRAINING forward over A anchors (teacher-forced Markov).
|
|
380
|
+
* - hCtx [A, H] target final hidden at each anchor (the tap, const)
|
|
381
|
+
* - anchorEmb [A, H] raw embedding of the anchor token x0 (const)
|
|
382
|
+
* - prevToks [A, γ] int32: [x0, x*_1, …, x*_{γ-1}] (teacher tokens)
|
|
383
|
+
* Returns draftLogits = U + B [A,γ,V] (float32) and conf [A,γ].
|
|
384
|
+
*/
|
|
385
|
+
forwardTrain(model: Gemma4Model, hCtx: MlxArray, anchorEmb: MlxArray, prevToks: MlxArray): DSparkTrainOut {
|
|
386
|
+
const A = hCtx.shape[0]!;
|
|
387
|
+
const block = this.#backbone(hCtx, anchorEmb, A); // [A,γ,dDraft]
|
|
388
|
+
const U = this.#baseLogits(model, block); // [A,γ,V]
|
|
389
|
+
const Uf = U.astype(CDT); U.dispose();
|
|
390
|
+
const { B, emb1 } = this.#markov(prevToks);
|
|
391
|
+
let draftLogits = ops.add(Uf, B); // [A,γ,V] f32
|
|
392
|
+
Uf.dispose(); B.dispose();
|
|
393
|
+
const conf = this.#confidence(block, emb1);
|
|
394
|
+
block.dispose(); emb1.dispose();
|
|
395
|
+
return { draftLogits, conf };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* INFERENCE: generate one γ-block by the semi-autoregressive rule — the
|
|
400
|
+
* backbone runs ONCE (parallel U_1..U_γ), then the Markov head walks the block
|
|
401
|
+
* left-to-right (each position conditions on the previously emitted draft
|
|
402
|
+
* token). Greedy argmax when no sampling config (or temperature 0); otherwise
|
|
403
|
+
* samples each token from the SAME processed distribution the verifier
|
|
404
|
+
* reconstructs (so q is consistent). hCtx [1,H], anchorTok the bonus token.
|
|
405
|
+
* Returns the raw per-position logits so the verifier can recover q.
|
|
406
|
+
*/
|
|
407
|
+
forwardInfer(model: Gemma4Model, hCtx: MlxArray, anchorTok: number, gamma: number, opts: DSparkDraftOpts = {}): DSparkDraftBlock {
|
|
408
|
+
const { dDraft, markovRank: r } = this.cfg;
|
|
409
|
+
const V = this.dims.vocabSize;
|
|
410
|
+
const A = 1;
|
|
411
|
+
const sample = opts.sample && opts.sample.temperature > 0 ? opts.sample : null;
|
|
412
|
+
const keys = opts.keys ?? new KeyStream(opts.sample?.seed ?? 0);
|
|
413
|
+
|
|
414
|
+
// anchor embedding (raw)
|
|
415
|
+
const ids = ops.fromInt32([anchorTok], [1, 1]);
|
|
416
|
+
const anchorEmb3 = model.embed.encode(ids); // [1,1,H]
|
|
417
|
+
ids.dispose();
|
|
418
|
+
const anchorEmb = ops.reshape(anchorEmb3, [1, this.dims.hiddenSize]);
|
|
419
|
+
anchorEmb3.dispose();
|
|
420
|
+
|
|
421
|
+
const block = this.#backbone(hCtx, anchorEmb, A); // [1,γ,dDraft]
|
|
422
|
+
anchorEmb.dispose();
|
|
423
|
+
const U = this.#baseLogits(model, block); // [1,γ,V]
|
|
424
|
+
const Uf = U.astype(CDT); U.dispose();
|
|
425
|
+
|
|
426
|
+
const w1 = this.get("markov.w1"); // [V,r]
|
|
427
|
+
const w2 = this.get("markov.w2"); // [r,V]
|
|
428
|
+
const tokens: number[] = [];
|
|
429
|
+
const conf: number[] = [];
|
|
430
|
+
const perPosLogits: MlxArray[] = []; // each [1,1,V], stacked into draftLogits
|
|
431
|
+
let prevTok = anchorTok;
|
|
432
|
+
for (let k = 0; k < gamma; k++) {
|
|
433
|
+
// U_k [1,V]
|
|
434
|
+
const Uk = Uf.slice([0, k, 0], [1, k + 1, V]);
|
|
435
|
+
const UkFlat = ops.reshape(Uk, [1, V]);
|
|
436
|
+
Uk.dispose();
|
|
437
|
+
// B_k = W1[prevTok] · W2 → [1,V]
|
|
438
|
+
const prevIdx = ops.fromInt32([prevTok], [1]);
|
|
439
|
+
const e1 = ops.takeAxis(w1, prevIdx, 0); // [1,r]
|
|
440
|
+
prevIdx.dispose();
|
|
441
|
+
const Bk = ops.matmul(e1, w2); // [1,V]
|
|
442
|
+
const logitsK = ops.add(UkFlat, Bk); // raw U_k + B_k [1,V]
|
|
443
|
+
UkFlat.dispose(); Bk.dispose();
|
|
444
|
+
|
|
445
|
+
let tok: number;
|
|
446
|
+
if (sample) {
|
|
447
|
+
const scaled = processLogits(logitsK, sample); // top-p/top-k/temp
|
|
448
|
+
tok = sampleToken(scaled, keys.next());
|
|
449
|
+
scaled.dispose();
|
|
450
|
+
} else {
|
|
451
|
+
const am = ops.argmaxAxis(logitsK, -1);
|
|
452
|
+
tok = ops.itemUint32(am);
|
|
453
|
+
am.dispose();
|
|
454
|
+
}
|
|
455
|
+
perPosLogits.push(ops.reshape(logitsK, [1, 1, V]));
|
|
456
|
+
logitsK.dispose();
|
|
457
|
+
|
|
458
|
+
// confidence c_k = σ(conf.w·[h_k; e1] + b)
|
|
459
|
+
const hk = block.slice([0, k, 0], [1, k + 1, dDraft]);
|
|
460
|
+
const hkFlat = ops.reshape(hk, [1, dDraft]);
|
|
461
|
+
hk.dispose();
|
|
462
|
+
const ci = ops.concatAxis([hkFlat, e1], 1); // [1,dDraft+r]
|
|
463
|
+
hkFlat.dispose(); e1.dispose();
|
|
464
|
+
let cz = ops.matmul(ci, this.get("conf.w"));
|
|
465
|
+
ci.dispose();
|
|
466
|
+
cz = disposing(cz, ops.add(cz, this.get("conf.b")));
|
|
467
|
+
cz = disposing(cz, ops.sigmoid(cz));
|
|
468
|
+
const cv = cz.toFloat32()[0]!;
|
|
469
|
+
cz.dispose();
|
|
470
|
+
tokens.push(tok);
|
|
471
|
+
conf.push(cv);
|
|
472
|
+
prevTok = tok;
|
|
473
|
+
}
|
|
474
|
+
Uf.dispose();
|
|
475
|
+
block.dispose();
|
|
476
|
+
const draftLogits = ops.concatAxis(perPosLogits, 1); // [1,γ,V]
|
|
477
|
+
for (const a of perPosLogits) a.dispose();
|
|
478
|
+
return { tokens, conf, draftLogits };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- persistence --------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
/** Save params + a sidecar dspark.json (config + target identity). */
|
|
484
|
+
save(dir: string): void {
|
|
485
|
+
mkdirSync(dir, { recursive: true });
|
|
486
|
+
const tensors: NamedTensor[] = this.names.map((name) => ({ name, array: this.get(name) }));
|
|
487
|
+
writeShardedSafetensors(dir, tensors);
|
|
488
|
+
const meta = {
|
|
489
|
+
kind: "dspark-drafter",
|
|
490
|
+
version: 1,
|
|
491
|
+
target_id: this.targetId,
|
|
492
|
+
config: this.cfg,
|
|
493
|
+
dims: this.dims,
|
|
494
|
+
param_names: this.names,
|
|
495
|
+
};
|
|
496
|
+
writeFileSync(join(dir, "dspark.json"), JSON.stringify(meta, null, 2));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Load a saved module. Validates only that the tensor set matches the
|
|
500
|
+
* declared names (structural staleness lives in the registry association,
|
|
501
|
+
* not a runtime check). */
|
|
502
|
+
static load(dir: string): DSparkDrafter {
|
|
503
|
+
const metaPath = join(dir, "dspark.json");
|
|
504
|
+
if (!existsSync(metaPath)) throw new Error(`no dspark.json in ${dir}`);
|
|
505
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf8")) as {
|
|
506
|
+
target_id: string; config: DSparkConfig; dims: TargetDims; param_names: string[];
|
|
507
|
+
};
|
|
508
|
+
const d = new DSparkDrafter(meta.config, meta.dims, meta.target_id);
|
|
509
|
+
const tensors = loadAdapterTensors(join(dir, "model.safetensors"));
|
|
510
|
+
try {
|
|
511
|
+
for (const name of d.names) {
|
|
512
|
+
const a = tensors.get(name);
|
|
513
|
+
if (!a) throw new Error(`checkpoint missing tensor: ${name}`);
|
|
514
|
+
d.#p.set(name, a);
|
|
515
|
+
}
|
|
516
|
+
} finally {
|
|
517
|
+
// dispose anything not adopted
|
|
518
|
+
for (const [name, arr] of tensors) if (!d.names.includes(name)) arr.dispose();
|
|
519
|
+
}
|
|
520
|
+
return d;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
dispose(): void {
|
|
524
|
+
for (const a of this.#p.values()) a.dispose();
|
|
525
|
+
this.#p.clear();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Canonical flat param-name order (single source of truth). */
|
|
530
|
+
function buildNames(cfg: DSparkConfig): string[] {
|
|
531
|
+
const names: string[] = [];
|
|
532
|
+
for (let i = 0; i < cfg.nLayers; i++) {
|
|
533
|
+
names.push(`bb.${i}.attn_norm`, `bb.${i}.q`, `bb.${i}.k`, `bb.${i}.v`, `bb.${i}.o`,
|
|
534
|
+
`bb.${i}.ff_norm`, `bb.${i}.gate`, `bb.${i}.up`, `bb.${i}.down`);
|
|
535
|
+
}
|
|
536
|
+
names.push("ctx_proj", "tok_proj", "mask_emb", "pos_emb", "out_norm", "out_proj",
|
|
537
|
+
"markov.w1", "markov.w2", "conf.w", "conf.b");
|
|
538
|
+
return names;
|
|
539
|
+
}
|
|
540
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Sampling support for DSpark speculative decoding at temperature > 0.
|
|
2
|
+
//
|
|
3
|
+
// Lossless speculative SAMPLING (Leviathan/Chen): a drafted token x sampled
|
|
4
|
+
// from draft dist q is accepted w.p. min(1, p(x)/q(x)) where p is the target
|
|
5
|
+
// dist; on rejection x is resampled from the residual norm(relu(p − q)); if the
|
|
6
|
+
// whole block is accepted a bonus token is sampled from p. The emitted stream is
|
|
7
|
+
// then distributed EXACTLY as if the target sampled token-by-token from p —
|
|
8
|
+
// independent of q (q only moves the acceptance rate τ). This is the
|
|
9
|
+
// temperature analog of the greedy longest-prefix gate.
|
|
10
|
+
//
|
|
11
|
+
// `p` and `q` must be the SAME processing applied to target/draft logits — so we
|
|
12
|
+
// reuse the server sampler's exact top-p/top-k/temperature path (sampler.ts).
|
|
13
|
+
// HLG/curve samplers are out of scope here; the caller rejects them.
|
|
14
|
+
|
|
15
|
+
import { MlxArray } from "../../mlx/array";
|
|
16
|
+
import { Dtype } from "../../mlx/ffi";
|
|
17
|
+
import * as ops from "../../mlx/ops";
|
|
18
|
+
import { applyTopP, applyTopK, toLogprobs } from "../../sampler";
|
|
19
|
+
|
|
20
|
+
export interface DSparkSampleConfig {
|
|
21
|
+
/** temperature > 0 enables sampling; 0 ⇒ greedy (handled by the caller). */
|
|
22
|
+
temperature: number;
|
|
23
|
+
topP?: number;
|
|
24
|
+
topK?: number;
|
|
25
|
+
seed?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const GOLDEN = 0x9e3779b97f4a7c15n;
|
|
29
|
+
const U64 = 0xffffffffffffffffn;
|
|
30
|
+
|
|
31
|
+
/** Seeded stream of mlx random keys — reproducible draws without sharing global
|
|
32
|
+
* RNG state (same derivation as sampler.ts stepKey). */
|
|
33
|
+
export class KeyStream {
|
|
34
|
+
#seed: bigint;
|
|
35
|
+
#ctr = 0;
|
|
36
|
+
constructor(seed = 0) { this.#seed = BigInt(seed >>> 0); }
|
|
37
|
+
next(): MlxArray {
|
|
38
|
+
const mixed = (this.#seed ^ ((BigInt(this.#ctr++) + 1n) * GOLDEN)) & U64;
|
|
39
|
+
return ops.randomKey(mixed);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Processed categorical logits for one position: logits [1,V] → masked +
|
|
45
|
+
* temperature-scaled logits [1,V] (the input to randomCategorical;
|
|
46
|
+
* softmax(·) is the sampling distribution). Mirrors makeSampler's top-p/top-k/
|
|
47
|
+
* temperature path exactly. temperature must be > 0.
|
|
48
|
+
*/
|
|
49
|
+
export function processLogits(logits: MlxArray, cfg: DSparkSampleConfig): MlxArray {
|
|
50
|
+
if (cfg.temperature <= 0) throw new Error("processLogits requires temperature > 0");
|
|
51
|
+
const lp = toLogprobs(logits); // [1,V]
|
|
52
|
+
let cur = lp;
|
|
53
|
+
const owned: MlxArray[] = [lp];
|
|
54
|
+
if (cfg.topP && cfg.topP > 0 && cfg.topP < 1) { cur = applyTopP(cur, cfg.topP); owned.push(cur); }
|
|
55
|
+
if (cfg.topK && cfg.topK > 0) { cur = applyTopK(cur, cfg.topK); owned.push(cur); }
|
|
56
|
+
const scaled = ops.mulScalar(cur, 1 / cfg.temperature);
|
|
57
|
+
for (const a of owned) a.dispose();
|
|
58
|
+
return scaled;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** softmax of processed logits → probability vector [1,V]. */
|
|
62
|
+
export function probsOf(scaled: MlxArray): MlxArray {
|
|
63
|
+
return ops.softmaxAxis(scaled, 1, true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Sample one token id from processed logits [1,V] (∝ softmax). */
|
|
67
|
+
export function sampleToken(scaled: MlxArray, key: MlxArray): number {
|
|
68
|
+
const t = ops.randomCategorical(scaled, key); // [1] uint32
|
|
69
|
+
const id = ops.itemUint32(t);
|
|
70
|
+
t.dispose();
|
|
71
|
+
return id;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Probability mass a [1,V] distribution assigns to token id (host scalar). */
|
|
75
|
+
export function probAtToken(probs: MlxArray, id: number): number {
|
|
76
|
+
const sl = probs.slice([0, id], [1, id + 1]); // [1,1]
|
|
77
|
+
const v = sl.toFloat32()[0]!;
|
|
78
|
+
sl.dispose();
|
|
79
|
+
return v;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Sample one token from the residual norm(relu(p − q)) over [1,V]. Falls back
|
|
83
|
+
* to sampling from p if the residual is degenerate (mass ≈ 0). */
|
|
84
|
+
export function sampleResidual(p: MlxArray, q: MlxArray, key: MlxArray): number {
|
|
85
|
+
const diff = ops.sub(p, q);
|
|
86
|
+
const zero = MlxArray.fromFloat32(new Float32Array([0]), []);
|
|
87
|
+
const relu = ops.maximum(diff, zero);
|
|
88
|
+
diff.dispose(); zero.dispose();
|
|
89
|
+
const mass = ops.sumAxis(relu, 1, true); // [1,1]
|
|
90
|
+
const massV = mass.toFloat32()[0]!;
|
|
91
|
+
if (!(massV > 1e-12)) {
|
|
92
|
+
// degenerate (p ⪯ q everywhere) — sample from p directly.
|
|
93
|
+
relu.dispose(); mass.dispose();
|
|
94
|
+
const lp = ops.log(p);
|
|
95
|
+
const id = sampleToken(lp, key);
|
|
96
|
+
lp.dispose();
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
const resid = ops.div(relu, mass); // normalized [1,V]
|
|
100
|
+
relu.dispose(); mass.dispose();
|
|
101
|
+
// randomCategorical samples ∝ exp(logits) → feed log(resid).
|
|
102
|
+
const eps = MlxArray.fromFloat32(new Float32Array([1e-30]), []);
|
|
103
|
+
const safe = ops.add(resid, eps);
|
|
104
|
+
resid.dispose(); eps.dispose();
|
|
105
|
+
const logits = ops.log(safe);
|
|
106
|
+
safe.dispose();
|
|
107
|
+
const id = sampleToken(logits, key);
|
|
108
|
+
logits.dispose();
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { Dtype };
|