mlx-bun 0.0.8 → 0.0.10
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-perf-path.md +232 -0
- 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/omlx-adoption-map.md +90 -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/segmented-backward-training.md +15 -0
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/ssd-kv-cold-tier.md +158 -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 +348 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +171 -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.10.md +57 -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 +43 -9
- package/docs/reference/training.md +31 -12
- 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 +1077 -90
- 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 +149 -18
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/kv-store.ts +240 -98
- 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/array.ts +21 -0
- package/src/mlx/ffi.ts +3 -0
- package/src/mlx/ops.ts +72 -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 +96 -7
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3-delta.ts +48 -1
- package/src/model/qwen3.ts +276 -0
- package/src/model/qwen3_5.ts +11 -2
- 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/prompt-cache.ts +55 -9
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +219 -8
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +307 -83
- package/src/serve/generation-gateway.ts +156 -29
- package/src/server.ts +746 -73
- 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/ssd-cache.ts +213 -0
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/flash-cce.ts +81 -23
- 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 +341 -40
- package/src/train/prefix-shared.ts +62 -11
- package/src/train/segmented.ts +215 -55
- package/src/train/trainer.ts +112 -38
- package/src/web/skills/memory/SKILL.md +40 -17
package/src/spec/drafter.ts
CHANGED
|
@@ -4,14 +4,23 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Q-only 4-layer transformer: K/V come from the TARGET's last sliding-
|
|
6
6
|
// and full-attention donor caches at call time; pre/post projections
|
|
7
|
-
// bridge target hidden space (
|
|
8
|
-
// the centroid-clustered head (2048 centroids, top-32 × 128 tokens).
|
|
7
|
+
// bridge target hidden space (backbone) <-> drafter space.
|
|
9
8
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
9
|
+
// Two output-head variants ship across the Gemma-4 drafter family — we
|
|
10
|
+
// pick by TENSOR PRESENCE, not config (the larger artifacts declare
|
|
11
|
+
// num_centroids in config.json but don't ship the centroid tensors —
|
|
12
|
+
// the field is vestigial):
|
|
13
|
+
// - CENTROID head (small E2B/E4B drafters, `gemma4_assistant`):
|
|
14
|
+
// 2048 centroids, top-32 × 128 tokens via a precomputed ordering.
|
|
15
|
+
// - TIED-EMBEDDING head (larger 12B/26B `gemma4_unified_assistant`):
|
|
16
|
+
// no centroids, no separate lm_head — logits = embed_tokens · h over
|
|
17
|
+
// the full vocab (the dense form the centroid head approximates).
|
|
18
|
+
//
|
|
19
|
+
// Implementation deviation (centroid path, argmax-equivalent): the
|
|
20
|
+
// reference scatters candidate scores into full-vocab logits initialised
|
|
21
|
+
// to -1e30 and argmaxes; we argmax over the 4096 candidate scores and map
|
|
22
|
+
// through candidate ids directly — same winner, no 262k materialisation.
|
|
23
|
+
// The drafter's numeric details only influence acceptance RATE, never
|
|
15
24
|
// output correctness (the target's verify decides every token).
|
|
16
25
|
|
|
17
26
|
import type { ModelConfig } from "../config";
|
|
@@ -56,10 +65,11 @@ export class GemmaAssistantDrafter {
|
|
|
56
65
|
readonly vocabPerCentroid: number;
|
|
57
66
|
#w: Weights;
|
|
58
67
|
#blocks: Block[] = [];
|
|
68
|
+
readonly useCentroids: boolean;
|
|
59
69
|
#prePT: MlxArray; // pre_projection transposed [in, out]
|
|
60
70
|
#postPT: MlxArray;
|
|
61
|
-
#centroidsT: MlxArray; // [hidden, numCentroids]
|
|
62
|
-
#ordering: MlxArray; // [numCentroids, vocabPerCentroid] int32
|
|
71
|
+
#centroidsT: MlxArray | null = null; // [hidden, numCentroids]
|
|
72
|
+
#ordering: MlxArray | null = null; // [numCentroids, vocabPerCentroid] int32
|
|
63
73
|
#embed: MlxArray; // drafter token embeddings [vocab, hidden]
|
|
64
74
|
#norm: MlxArray;
|
|
65
75
|
|
|
@@ -84,14 +94,20 @@ export class GemmaAssistantDrafter {
|
|
|
84
94
|
};
|
|
85
95
|
this.#prePT = transposed("pre_projection.weight");
|
|
86
96
|
this.#postPT = transposed("post_projection.weight");
|
|
87
|
-
this.#centroidsT = transposed("masked_embedding.centroids.weight");
|
|
88
97
|
this.#embed = T("model.embed_tokens.weight");
|
|
89
98
|
this.#norm = T("model.norm.weight");
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this
|
|
94
|
-
|
|
99
|
+
// Head variant: only the small drafters ship the centroid tensors.
|
|
100
|
+
// The 12B/26B artifacts ship a tied embed_tokens head instead (their
|
|
101
|
+
// config still declares num_centroids — ignore it, trust the tensors).
|
|
102
|
+
this.useCentroids = w.has("masked_embedding.centroids.weight");
|
|
103
|
+
if (this.useCentroids) {
|
|
104
|
+
this.#centroidsT = transposed("masked_embedding.centroids.weight");
|
|
105
|
+
const ord64 = T("masked_embedding.token_ordering");
|
|
106
|
+
const ord32 = ord64.astype(Dtype.int32);
|
|
107
|
+
this.#ordering = ops.reshape(ord32, [this.numCentroids, this.vocabPerCentroid]);
|
|
108
|
+
this.#ordering.eval();
|
|
109
|
+
ord32.dispose();
|
|
110
|
+
}
|
|
95
111
|
|
|
96
112
|
const ropeP = t.rope_parameters ?? {};
|
|
97
113
|
for (let i = 0; i < t.num_hidden_layers; i++) {
|
|
@@ -215,41 +231,57 @@ export class GemmaAssistantDrafter {
|
|
|
215
231
|
|
|
216
232
|
h = disposing(h, this.#rms(h, this.#norm));
|
|
217
233
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
234
|
+
let token: number;
|
|
235
|
+
if (this.useCentroids) {
|
|
236
|
+
// centroid decode → drafted token (argmax-equivalent shortcut)
|
|
237
|
+
const cScores = ops.matmul(h, this.#centroidsT!); // (1,1,2048)
|
|
238
|
+
const flat = ops.reshape(cScores, [1, this.numCentroids]);
|
|
239
|
+
cScores.dispose();
|
|
240
|
+
const neg = ops.neg(flat);
|
|
241
|
+
flat.dispose();
|
|
242
|
+
const part = ops.argpartitionAxis(neg, this.topK - 1, -1);
|
|
243
|
+
neg.dispose();
|
|
244
|
+
const topIdx = part.slice([0, 0], [1, this.topK]); // (1, topK)
|
|
245
|
+
part.dispose();
|
|
246
|
+
const topFlat = ops.reshape(topIdx, [this.topK]);
|
|
247
|
+
topIdx.dispose();
|
|
248
|
+
const candIds = ops.takeAxis(this.#ordering!, topFlat, 0); // (topK, vpc) int32
|
|
249
|
+
topFlat.dispose();
|
|
250
|
+
const candFlat = ops.reshape(candIds, [this.topK * this.vocabPerCentroid]);
|
|
251
|
+
candIds.dispose();
|
|
252
|
+
const candEmb = ops.takeAxis(this.#embed, candFlat, 0); // (topK*vpc, hidden)
|
|
253
|
+
const hVec = ops.reshape(h, [this.hidden, 1]);
|
|
254
|
+
const scores = ops.matmul(candEmb, hVec); // (topK*vpc, 1)
|
|
255
|
+
candEmb.dispose();
|
|
256
|
+
hVec.dispose();
|
|
257
|
+
const scoresFlat = ops.reshape(scores, [1, this.topK * this.vocabPerCentroid]);
|
|
258
|
+
scores.dispose();
|
|
259
|
+
const am = ops.argmaxAxis(scoresFlat, -1);
|
|
260
|
+
scoresFlat.dispose();
|
|
261
|
+
const winner = ops.itemUint32(am);
|
|
262
|
+
am.dispose();
|
|
263
|
+
const tokArr = candFlat.slice([winner], [winner + 1]);
|
|
264
|
+
candFlat.dispose();
|
|
265
|
+
const tokU32 = tokArr.astype(Dtype.uint32);
|
|
266
|
+
tokArr.dispose();
|
|
267
|
+
token = ops.itemUint32(tokU32);
|
|
268
|
+
tokU32.dispose();
|
|
269
|
+
} else {
|
|
270
|
+
// tied-embedding head: token = argmax_v (embed[v] · h) over the
|
|
271
|
+
// full vocab — the dense form the centroid head approximates.
|
|
272
|
+
const V = this.#embed.shape[0]!;
|
|
273
|
+
const hVec = ops.reshape(h, [this.hidden, 1]);
|
|
274
|
+
const scores = ops.matmul(this.#embed, hVec); // (vocab, 1)
|
|
275
|
+
hVec.dispose();
|
|
276
|
+
const scoresFlat = ops.reshape(scores, [1, V]);
|
|
277
|
+
scores.dispose();
|
|
278
|
+
const am = ops.argmaxAxis(scoresFlat, -1);
|
|
279
|
+
scoresFlat.dispose();
|
|
280
|
+
token = ops.itemUint32(am);
|
|
281
|
+
am.dispose();
|
|
282
|
+
}
|
|
251
283
|
|
|
252
|
-
const nextHidden = ops.matmul(h, this.#postPT); // (1,1,
|
|
284
|
+
const nextHidden = ops.matmul(h, this.#postPT); // (1,1,backbone)
|
|
253
285
|
h.dispose();
|
|
254
286
|
return { token, nextHidden };
|
|
255
287
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Data path for faithful DFlash (multi-layer H_ctx). Parallel to data.ts.
|
|
2
|
+
//
|
|
3
|
+
// Shard stores, per packed sequence, the target's tapped hiddens at m layers
|
|
4
|
+
// concatenated on the feature axis: `hidden_ml` [Ltot, m*H] bf16 (the m layers
|
|
5
|
+
// in tapLayers order, the LAST being the post-finalNorm hidden used for p^t),
|
|
6
|
+
// plus `ids` [Ltot], `seq_lens` [nSeq], `resp_starts` [nSeq].
|
|
7
|
+
//
|
|
8
|
+
// A training anchor at response position t attends the FULL prefix context
|
|
9
|
+
// [0..t] (paper Eq 2/3), so sampleBatch slices a per-anchor variable-length
|
|
10
|
+
// prefix and LEFT-PADS the batch to a fixed maxCtx with a key-padding mask.
|
|
11
|
+
|
|
12
|
+
import { MlxArray } from "../../mlx/array";
|
|
13
|
+
import { Dtype } from "../../mlx/ffi";
|
|
14
|
+
import * as ops from "../../mlx/ops";
|
|
15
|
+
import { loadAdapterTensors } from "../../lora";
|
|
16
|
+
import { writeShardedSafetensors, type NamedTensor } from "../../quantize/safetensors-writer";
|
|
17
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
/** One re-forwarded sequence: ids, the response-region start, and the m-layer
|
|
21
|
+
* hiddens over the FULL sequence (row-major [L, m*H] bf16 bytes). */
|
|
22
|
+
export interface DflashRecord {
|
|
23
|
+
ids: number[];
|
|
24
|
+
respStart: number;
|
|
25
|
+
hiddenMlBf16: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DflashShardMeta {
|
|
29
|
+
nSeq: number; nTokens: number; hiddenSize: number; m: number; tapLayers: number[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writeDflashShard(outDir: string, shardIdx: number, records: DflashRecord[], hiddenSize: number, m: number, tapLayers: number[]): DflashShardMeta {
|
|
33
|
+
const dir = join(outDir, `shard_${String(shardIdx).padStart(5, "0")}`);
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
const mH = m * hiddenSize;
|
|
36
|
+
const seqLens: number[] = [], respStarts: number[] = [], allIds: number[] = [], parts: Uint8Array[] = [];
|
|
37
|
+
for (const r of records) {
|
|
38
|
+
seqLens.push(r.ids.length); respStarts.push(r.respStart);
|
|
39
|
+
for (const id of r.ids) allIds.push(id);
|
|
40
|
+
parts.push(r.hiddenMlBf16);
|
|
41
|
+
}
|
|
42
|
+
const nTokens = allIds.length;
|
|
43
|
+
let total = 0; for (const p of parts) total += p.length;
|
|
44
|
+
const bytes = new Uint8Array(total); let off = 0; for (const p of parts) { bytes.set(p, off); off += p.length; }
|
|
45
|
+
const hidden = MlxArray.fromBytesCopy(bytes, [nTokens, mH], Dtype.bfloat16);
|
|
46
|
+
const ids = MlxArray.fromInt32(new Int32Array(allIds), [nTokens]);
|
|
47
|
+
const lens = MlxArray.fromInt32(new Int32Array(seqLens), [seqLens.length]);
|
|
48
|
+
const rs = MlxArray.fromInt32(new Int32Array(respStarts), [respStarts.length]);
|
|
49
|
+
const tensors: NamedTensor[] = [
|
|
50
|
+
{ name: "hidden_ml", array: hidden }, { name: "ids", array: ids },
|
|
51
|
+
{ name: "seq_lens", array: lens }, { name: "resp_starts", array: rs },
|
|
52
|
+
];
|
|
53
|
+
writeShardedSafetensors(dir, tensors);
|
|
54
|
+
for (const t of tensors) t.array.dispose();
|
|
55
|
+
const meta: DflashShardMeta = { nSeq: records.length, nTokens, hiddenSize, m, tapLayers };
|
|
56
|
+
writeFileSync(join(dir, "shard.json"), JSON.stringify(meta, null, 2));
|
|
57
|
+
return meta;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class DflashShard {
|
|
61
|
+
readonly hiddenMl: MlxArray; // [Ltot, m*H] bf16
|
|
62
|
+
readonly ids: Int32Array;
|
|
63
|
+
readonly seqStart: number[]; readonly seqLen: number[]; readonly respStart: number[];
|
|
64
|
+
readonly hiddenSize: number; readonly m: number;
|
|
65
|
+
constructor(hiddenMl: MlxArray, ids: Int32Array, seqLens: Int32Array, respStarts: Int32Array, hiddenSize: number, m: number) {
|
|
66
|
+
this.hiddenMl = hiddenMl; this.ids = ids; this.hiddenSize = hiddenSize; this.m = m;
|
|
67
|
+
this.seqStart = []; this.seqLen = []; this.respStart = [];
|
|
68
|
+
let off = 0;
|
|
69
|
+
for (let i = 0; i < seqLens.length; i++) { this.seqStart.push(off); this.seqLen.push(seqLens[i]!); this.respStart.push(respStarts[i]!); off += seqLens[i]!; }
|
|
70
|
+
}
|
|
71
|
+
static load(dir: string): DflashShard {
|
|
72
|
+
const meta = JSON.parse(readFileSync(join(dir, "shard.json"), "utf8")) as DflashShardMeta;
|
|
73
|
+
const t = loadAdapterTensors(join(dir, "model.safetensors"));
|
|
74
|
+
try {
|
|
75
|
+
const hidden = t.get("hidden_ml"), idsA = t.get("ids"), lensA = t.get("seq_lens"), rsA = t.get("resp_starts");
|
|
76
|
+
if (!hidden || !idsA || !lensA || !rsA) throw new Error(`shard ${dir} missing tensors`);
|
|
77
|
+
const ids = Int32Array.from(idsA.toFloat32(), (v) => Math.round(v));
|
|
78
|
+
const lens = Int32Array.from(lensA.toFloat32(), (v) => Math.round(v));
|
|
79
|
+
const rs = Int32Array.from(rsA.toFloat32(), (v) => Math.round(v));
|
|
80
|
+
idsA.dispose(); lensA.dispose(); rsA.dispose();
|
|
81
|
+
return new DflashShard(hidden, ids, lens, rs, meta.hiddenSize, meta.m);
|
|
82
|
+
} catch (e) { for (const [, a] of t) a.dispose(); throw e; }
|
|
83
|
+
}
|
|
84
|
+
dispose(): void { this.hiddenMl.dispose(); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function listDflashShards(root: string): string[] {
|
|
88
|
+
if (!existsSync(root)) return [];
|
|
89
|
+
return readdirSync(root).filter((n) => n.startsWith("shard_")).sort().map((n) => join(root, n));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DflashBatch {
|
|
93
|
+
hCtx: MlxArray; // [A, maxCtx, m*H] bf16, LEFT-padded
|
|
94
|
+
ctxMask: MlxArray; // [A, maxCtx] f32 (1=real, 0=pad)
|
|
95
|
+
targetHidden: MlxArray; // [A, γ, H] bf16 (final-layer hiddens at block positions → p^t)
|
|
96
|
+
anchorToks: number[];
|
|
97
|
+
blockToks: number[][];
|
|
98
|
+
size: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Sample A anchors and gather them into a faithful DFlash batch. Each anchor at
|
|
103
|
+
* response position t contributes its full prefix context [0..t] (capped at
|
|
104
|
+
* maxCtx, keeping the most recent), left-padded. The final tapped layer (last
|
|
105
|
+
* in tapLayers = post-finalNorm) supplies the block's p^t hiddens.
|
|
106
|
+
*/
|
|
107
|
+
export function sampleDflashBatch(shard: DflashShard, A: number, gamma: number, maxCtx: number, rng: () => number): DflashBatch | null {
|
|
108
|
+
const H = shard.hiddenSize, m = shard.m, mH = m * H;
|
|
109
|
+
const ctxIdx: number[] = []; // A*maxCtx global indices (dummy 0 for pads)
|
|
110
|
+
const maskRows: number[] = []; // A*maxCtx
|
|
111
|
+
const blockIdx: number[] = []; // A*γ global indices
|
|
112
|
+
const anchorToks: number[] = [];
|
|
113
|
+
const blockToks: number[][] = [];
|
|
114
|
+
|
|
115
|
+
let got = 0, guard = 0;
|
|
116
|
+
while (got < A && guard < A * 64) {
|
|
117
|
+
guard++;
|
|
118
|
+
const s = Math.floor(rng() * shard.seqLen.length);
|
|
119
|
+
const len = shard.seqLen[s]!, rStart = shard.respStart[s]!, start = shard.seqStart[s]!;
|
|
120
|
+
// anchor local t in [rStart, len-γ-1]; need γ tokens after and t in response.
|
|
121
|
+
const lo = rStart, hi = len - gamma - 1;
|
|
122
|
+
if (hi < lo) continue;
|
|
123
|
+
const t = lo + Math.floor(rng() * (hi - lo + 1));
|
|
124
|
+
const g = start + t;
|
|
125
|
+
// prefix context [0..t], capped to the most-recent maxCtx positions.
|
|
126
|
+
const La = Math.min(t + 1, maxCtx);
|
|
127
|
+
const ctxStart = g - La + 1;
|
|
128
|
+
const pad = maxCtx - La;
|
|
129
|
+
for (let j = 0; j < pad; j++) { ctxIdx.push(0); maskRows.push(0); }
|
|
130
|
+
for (let j = 0; j < La; j++) { ctxIdx.push(ctxStart + j); maskRows.push(1); }
|
|
131
|
+
const bt: number[] = [];
|
|
132
|
+
for (let k = 1; k <= gamma; k++) bt.push(shard.ids[g + k]!); // CE/markov targets x_{t+1..t+γ}
|
|
133
|
+
// p^t hiddens: block position k predicts x_{t+1+k}, whose target dist is
|
|
134
|
+
// softmax(LM_head(h_{t+k})) — the hidden that PREDICTS it. So gather
|
|
135
|
+
// positions [t..t+γ-1] = [g..g+γ-1], NOT [g+1..g+γ] (off-by-one fix).
|
|
136
|
+
for (let k = 0; k < gamma; k++) blockIdx.push(g + k);
|
|
137
|
+
anchorToks.push(shard.ids[g]!); blockToks.push(bt); got++;
|
|
138
|
+
}
|
|
139
|
+
if (got === 0) return null;
|
|
140
|
+
|
|
141
|
+
const cIdx = MlxArray.fromInt32(new Int32Array(ctxIdx), [got * maxCtx]);
|
|
142
|
+
const gathered = ops.takeAxis(shard.hiddenMl, cIdx, 0); // [A*maxCtx, m*H]
|
|
143
|
+
cIdx.dispose();
|
|
144
|
+
const hCtx = ops.reshape(gathered, [got, maxCtx, mH]); gathered.dispose();
|
|
145
|
+
const ctxMask = MlxArray.fromFloat32(new Float32Array(maskRows), [got, maxCtx]);
|
|
146
|
+
|
|
147
|
+
const bIdx = MlxArray.fromInt32(new Int32Array(blockIdx), [got * gamma]);
|
|
148
|
+
const bGathered = ops.takeAxis(shard.hiddenMl, bIdx, 0); // [A*γ, m*H]
|
|
149
|
+
bIdx.dispose();
|
|
150
|
+
// final tapped layer = last H columns → the post-finalNorm hidden for p^t
|
|
151
|
+
const finalSlice = bGathered.slice([0, (m - 1) * H], [got * gamma, m * H]); // [A*γ, H]
|
|
152
|
+
bGathered.dispose();
|
|
153
|
+
const targetHidden = ops.reshape(finalSlice, [got, gamma, H]); finalSlice.dispose();
|
|
154
|
+
|
|
155
|
+
return { hCtx, ctxMask, targetHidden, anchorToks, blockToks, size: got };
|
|
156
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// DSpark training data — the regen target (paper §3.3 + §5.1 "hidden state
|
|
2
|
+
// communication"). Training the drafter requires, per block position, the
|
|
3
|
+
// frozen target's TRUE distribution p^t and the tapped hidden state. Rather
|
|
4
|
+
// than store full-vocab logits (V≈262k → enormous), we store only the target's
|
|
5
|
+
// per-position final hidden states (the tap, H≈2816) plus the token ids, and
|
|
6
|
+
// recompute p^t on the fly during training via the SHARED frozen LM head —
|
|
7
|
+
// O(d) storage instead of O(V). (paper §5.1.)
|
|
8
|
+
//
|
|
9
|
+
// On-disk layout: a directory of shards, each `shard_NNNNN/model.safetensors`
|
|
10
|
+
// holding three tensors over a pack of sequences:
|
|
11
|
+
// - "hidden" [Ltot, H] bf16 — concatenated per-position final hiddens
|
|
12
|
+
// - "ids" [Ltot] int32 — concatenated token ids
|
|
13
|
+
// - "seq_lens" [nSeq] int32 — per-sequence lengths (to split Ltot)
|
|
14
|
+
// plus a sidecar `shard.json` with counts. The regen script (Josh runs it on
|
|
15
|
+
// GPU) writes these; the trainer reads them and samples anchor blocks.
|
|
16
|
+
//
|
|
17
|
+
// An anchor at sequence position t yields a training example:
|
|
18
|
+
// hCtx = hidden[t] (the injected context, paper H_ctx)
|
|
19
|
+
// anchorTok = ids[t] (the bonus token x0)
|
|
20
|
+
// blockToks = ids[t+1 .. t+γ] (ground truth x*_1..x*_γ)
|
|
21
|
+
// targetHidden= hidden[t+1 .. t+γ] (→ p^t_k via the frozen head at train)
|
|
22
|
+
|
|
23
|
+
import { MlxArray } from "../../mlx/array";
|
|
24
|
+
import { Dtype } from "../../mlx/ffi";
|
|
25
|
+
import * as ops from "../../mlx/ops";
|
|
26
|
+
import { loadAdapterTensors } from "../../lora";
|
|
27
|
+
import { writeShardedSafetensors, type NamedTensor } from "../../quantize/safetensors-writer";
|
|
28
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
|
|
31
|
+
/** One re-forwarded sequence: ids + per-position final hidden (bf16 bytes). */
|
|
32
|
+
export interface DSparkRecord {
|
|
33
|
+
ids: number[];
|
|
34
|
+
/** Row-major [L, H] bf16 bytes (from MlxArray.rawBytes of the tap). */
|
|
35
|
+
hiddenBf16: Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DSparkShardMeta {
|
|
39
|
+
nSeq: number;
|
|
40
|
+
nTokens: number;
|
|
41
|
+
hiddenSize: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Write a shard of re-forwarded sequences to `dir/shard_NNNNN/`. */
|
|
45
|
+
export function writeDSparkShard(
|
|
46
|
+
outDir: string,
|
|
47
|
+
shardIdx: number,
|
|
48
|
+
records: DSparkRecord[],
|
|
49
|
+
hiddenSize: number,
|
|
50
|
+
): DSparkShardMeta {
|
|
51
|
+
const dir = join(outDir, `shard_${String(shardIdx).padStart(5, "0")}`);
|
|
52
|
+
mkdirSync(dir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
const seqLens: number[] = [];
|
|
55
|
+
const allIds: number[] = [];
|
|
56
|
+
const hiddenParts: Uint8Array[] = [];
|
|
57
|
+
for (const rec of records) {
|
|
58
|
+
seqLens.push(rec.ids.length);
|
|
59
|
+
for (const id of rec.ids) allIds.push(id);
|
|
60
|
+
hiddenParts.push(rec.hiddenBf16);
|
|
61
|
+
}
|
|
62
|
+
const nTokens = allIds.length;
|
|
63
|
+
|
|
64
|
+
// concat hidden bytes
|
|
65
|
+
let total = 0;
|
|
66
|
+
for (const p of hiddenParts) total += p.length;
|
|
67
|
+
const hiddenBytes = new Uint8Array(total);
|
|
68
|
+
let off = 0;
|
|
69
|
+
for (const p of hiddenParts) { hiddenBytes.set(p, off); off += p.length; }
|
|
70
|
+
|
|
71
|
+
const hidden = MlxArray.fromBytesCopy(hiddenBytes, [nTokens, hiddenSize], Dtype.bfloat16);
|
|
72
|
+
const ids = MlxArray.fromInt32(new Int32Array(allIds), [nTokens]);
|
|
73
|
+
const lens = MlxArray.fromInt32(new Int32Array(seqLens), [seqLens.length]);
|
|
74
|
+
const tensors: NamedTensor[] = [
|
|
75
|
+
{ name: "hidden", array: hidden },
|
|
76
|
+
{ name: "ids", array: ids },
|
|
77
|
+
{ name: "seq_lens", array: lens },
|
|
78
|
+
];
|
|
79
|
+
writeShardedSafetensors(dir, tensors);
|
|
80
|
+
hidden.dispose(); ids.dispose(); lens.dispose();
|
|
81
|
+
|
|
82
|
+
const meta: DSparkShardMeta = { nSeq: records.length, nTokens, hiddenSize };
|
|
83
|
+
writeFileSync(join(dir, "shard.json"), JSON.stringify(meta, null, 2));
|
|
84
|
+
return meta;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A loaded shard, ready for anchor sampling. Owns `hidden`; call dispose(). */
|
|
88
|
+
export class DSparkShard {
|
|
89
|
+
readonly hidden: MlxArray; // [Ltot, H] bf16
|
|
90
|
+
readonly ids: Int32Array; // [Ltot]
|
|
91
|
+
readonly seqStart: number[]; // per-seq start offset into Ltot
|
|
92
|
+
readonly seqLen: number[]; // per-seq length
|
|
93
|
+
readonly hiddenSize: number;
|
|
94
|
+
|
|
95
|
+
constructor(hidden: MlxArray, ids: Int32Array, seqLens: Int32Array, hiddenSize: number) {
|
|
96
|
+
this.hidden = hidden;
|
|
97
|
+
this.ids = ids;
|
|
98
|
+
this.hiddenSize = hiddenSize;
|
|
99
|
+
this.seqStart = [];
|
|
100
|
+
this.seqLen = [];
|
|
101
|
+
let off = 0;
|
|
102
|
+
for (const l of seqLens) {
|
|
103
|
+
this.seqStart.push(off);
|
|
104
|
+
this.seqLen.push(l);
|
|
105
|
+
off += l;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static load(dir: string): DSparkShard {
|
|
110
|
+
const metaPath = join(dir, "shard.json");
|
|
111
|
+
if (!existsSync(metaPath)) throw new Error(`no shard.json in ${dir}`);
|
|
112
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf8")) as DSparkShardMeta;
|
|
113
|
+
const t = loadAdapterTensors(join(dir, "model.safetensors"));
|
|
114
|
+
try {
|
|
115
|
+
const hidden = t.get("hidden");
|
|
116
|
+
const idsArr = t.get("ids");
|
|
117
|
+
const lensArr = t.get("seq_lens");
|
|
118
|
+
if (!hidden || !idsArr || !lensArr) throw new Error(`shard ${dir} missing tensors`);
|
|
119
|
+
// int32 ids round-trip exactly through f32 (vocab < 2^24).
|
|
120
|
+
const ids = Int32Array.from(idsArr.toFloat32(), (v) => Math.round(v));
|
|
121
|
+
const lens = Int32Array.from(lensArr.toFloat32(), (v) => Math.round(v));
|
|
122
|
+
idsArr.dispose(); lensArr.dispose();
|
|
123
|
+
return new DSparkShard(hidden, ids, lens, meta.hiddenSize);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
for (const [, arr] of t) arr.dispose();
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
dispose(): void {
|
|
131
|
+
this.hidden.dispose();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** List shard directories under a data root, sorted. */
|
|
136
|
+
export function listShards(root: string): string[] {
|
|
137
|
+
if (!existsSync(root)) return [];
|
|
138
|
+
return readdirSync(root)
|
|
139
|
+
.filter((n) => n.startsWith("shard_"))
|
|
140
|
+
.sort()
|
|
141
|
+
.map((n) => join(root, n));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface DSparkBatch {
|
|
145
|
+
/** [A, H] bf16 — tapped target hidden at each anchor. */
|
|
146
|
+
hCtx: MlxArray;
|
|
147
|
+
/** [A, γ, H] bf16 — target final hiddens at the γ block positions. */
|
|
148
|
+
targetHidden: MlxArray;
|
|
149
|
+
/** anchor token x0 per row. */
|
|
150
|
+
anchorToks: number[];
|
|
151
|
+
/** ground-truth block tokens x*_1..x*_γ per row [A][γ]. */
|
|
152
|
+
blockToks: number[][];
|
|
153
|
+
/** number of anchors A. */
|
|
154
|
+
size: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Sample A anchors from a loaded shard and gather them into a training batch.
|
|
159
|
+
* A valid anchor at global index g (within a sequence) needs γ continuation
|
|
160
|
+
* tokens in the SAME sequence. `rng` returns [0,1).
|
|
161
|
+
*/
|
|
162
|
+
export function sampleBatch(shard: DSparkShard, A: number, gamma: number, rng: () => number): DSparkBatch | null {
|
|
163
|
+
const anchorGlobal: number[] = [];
|
|
164
|
+
const anchorToks: number[] = [];
|
|
165
|
+
const blockToks: number[][] = [];
|
|
166
|
+
const blockGlobal: number[] = []; // flattened γ indices per anchor
|
|
167
|
+
|
|
168
|
+
let guard = 0;
|
|
169
|
+
while (anchorGlobal.length < A && guard < A * 64) {
|
|
170
|
+
guard++;
|
|
171
|
+
const s = Math.floor(rng() * shard.seqLen.length);
|
|
172
|
+
const len = shard.seqLen[s]!;
|
|
173
|
+
if (len < gamma + 1) continue;
|
|
174
|
+
const start = shard.seqStart[s]!;
|
|
175
|
+
// local anchor position t in [0, len-γ-1]
|
|
176
|
+
const t = Math.floor(rng() * (len - gamma));
|
|
177
|
+
const g = start + t;
|
|
178
|
+
anchorGlobal.push(g);
|
|
179
|
+
anchorToks.push(shard.ids[g]!);
|
|
180
|
+
const bt: number[] = [];
|
|
181
|
+
for (let k = 1; k <= gamma; k++) {
|
|
182
|
+
bt.push(shard.ids[g + k]!);
|
|
183
|
+
blockGlobal.push(g + k);
|
|
184
|
+
}
|
|
185
|
+
blockToks.push(bt);
|
|
186
|
+
}
|
|
187
|
+
if (anchorGlobal.length === 0) return null;
|
|
188
|
+
const got = anchorGlobal.length;
|
|
189
|
+
|
|
190
|
+
const anchorIdx = MlxArray.fromInt32(new Int32Array(anchorGlobal), [got]);
|
|
191
|
+
const hCtx = ops.takeAxis(shard.hidden, anchorIdx, 0); // [A,H]
|
|
192
|
+
anchorIdx.dispose();
|
|
193
|
+
|
|
194
|
+
const blockIdx = MlxArray.fromInt32(new Int32Array(blockGlobal), [got * gamma]);
|
|
195
|
+
const tgtFlat = ops.takeAxis(shard.hidden, blockIdx, 0); // [A*γ, H]
|
|
196
|
+
blockIdx.dispose();
|
|
197
|
+
const targetHidden = ops.reshape(tgtFlat, [got, gamma, shard.hiddenSize]);
|
|
198
|
+
tgtFlat.dispose();
|
|
199
|
+
|
|
200
|
+
return { hCtx, targetHidden, anchorToks, blockToks, size: got };
|
|
201
|
+
}
|