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/mlx/array.ts
CHANGED
|
@@ -108,6 +108,27 @@ export class MlxArray {
|
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Graph-free LEAF with this array's exact bytes — ONE copy, straight from
|
|
112
|
+
* the (evaluated) mlx buffer into a fresh mlx-owned array, no JS-heap
|
|
113
|
+
* intermediate (`fromBytesCopy(rawBytes())` is TWO copies: device→JS heap
|
|
114
|
+
* →new leaf). mlx_array_new_data copies synchronously, so the source only
|
|
115
|
+
* has to outlive this call. The buffer is read LINEARLY — the caller must
|
|
116
|
+
* pass a row-major contiguous array (ops.contiguous first for views), same
|
|
117
|
+
* contract as rawBytes. Segmented-backward boundary detach hot path. */
|
|
118
|
+
detachCopy(): MlxArray {
|
|
119
|
+
this.eval();
|
|
120
|
+
const dt = this.dtype;
|
|
121
|
+
const p =
|
|
122
|
+
dt === Dtype.float32 ? C.mlx_array_data_float32(this.handle)
|
|
123
|
+
: dt === Dtype.float16 ? C.mlx_array_data_float16(this.handle)
|
|
124
|
+
: dt === Dtype.bfloat16 ? C.mlx_array_data_bfloat16(this.handle)
|
|
125
|
+
: dt === Dtype.uint32 ? C.mlx_array_data_uint32(this.handle)
|
|
126
|
+
: null;
|
|
127
|
+
if (p === null) throw new Error(`detachCopy: unsupported dtype ${this.dtypeName}`);
|
|
128
|
+
const sb = shapeBuf(this.shape);
|
|
129
|
+
return new MlxArray(C.mlx_array_new_data(p as never, ptr(sb), this.ndim, dt));
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
get handle(): MlxHandle {
|
|
112
133
|
if (this.#disposed) throw new Error("MlxArray used after dispose");
|
|
113
134
|
return this.#handle;
|
package/src/mlx/ffi.ts
CHANGED
|
@@ -119,6 +119,9 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
119
119
|
mlx_divide: { args: [P, u64, u64, u64], returns: i32 },
|
|
120
120
|
mlx_tanh: { args: [P, u64, u64], returns: i32 },
|
|
121
121
|
mlx_sigmoid: { args: [P, u64, u64], returns: i32 },
|
|
122
|
+
mlx_erf: { args: [P, u64, u64], returns: i32 },
|
|
123
|
+
// (res, c, a, b, alpha, beta, stream) — alpha·(a@b) + beta·c (nn.Linear bias path)
|
|
124
|
+
mlx_addmm: { args: [P, u64, u64, u64, f32, f32, u64], returns: i32 },
|
|
122
125
|
mlx_power: { args: [P, u64, u64, u64], returns: i32 },
|
|
123
126
|
mlx_take_axis: { args: [P, u64, u64, i32, u64], returns: i32 },
|
|
124
127
|
mlx_reshape: { args: [P, u64, P, u64, u64], returns: i32 },
|
package/src/mlx/ops.ts
CHANGED
|
@@ -111,6 +111,23 @@ export function rope(
|
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** fast::rope with every knob exposed (traditional + scale), for the
|
|
115
|
+
* universal rope factory (rope_utils.initialize_rope port). The plain
|
|
116
|
+
* `rope` above stays byte-identical for existing call sites. */
|
|
117
|
+
export function ropeScaled(
|
|
118
|
+
x: MlxArray, dims: number, traditional: boolean, base: number | null,
|
|
119
|
+
scale: number, offset: number, freqs: MlxArray | null, s: S = gpuStream,
|
|
120
|
+
): MlxArray {
|
|
121
|
+
return new MlxArray(
|
|
122
|
+
outArray("fast_rope", (o) =>
|
|
123
|
+
C.mlx_fast_rope(
|
|
124
|
+
o, x.handle, dims, traditional, optFloat(base), scale, offset,
|
|
125
|
+
freqs?.handle ?? 0n, s,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
/** fast::rope with the position offset as an ARRAY (int32 scalar) instead
|
|
115
132
|
* of a baked int — required inside compiled decode graphs, where the
|
|
116
133
|
* offset changes every step but the graph must not. Same kernel as
|
|
@@ -155,6 +172,32 @@ export function quantizedMatmul(
|
|
|
155
172
|
x: MlxArray, w: MlxArray, scales: MlxArray, biases: MlxArray | null,
|
|
156
173
|
spec: QuantSpec, transpose = true, s: S = gpuStream,
|
|
157
174
|
): MlxArray {
|
|
175
|
+
// UPSTREAM MLX BUG WORKAROUND (found 2026-07-02, reproduced on stock mlx
|
|
176
|
+
// 0.31.2): quantized_matmul with transpose=false returns WRONG VALUES when
|
|
177
|
+
// the row count is 2 or 3 (relnorm ~0.3-1.1 vs the dequantized matmul);
|
|
178
|
+
// M=1 and M>=4 are correct, transpose=true is correct at every M. Pad the
|
|
179
|
+
// row axis to 4 with zeros and slice the result back. NOTE this wrapper
|
|
180
|
+
// cannot protect mlx's INTERNAL vjp of a transpose=true qmm (C++ calls the
|
|
181
|
+
// primitive directly) — training heads pad the span through the head
|
|
182
|
+
// instead (loss.ts logitsFromHiddenPadM).
|
|
183
|
+
const rowAxis = x.shape.length - 2;
|
|
184
|
+
const rows = x.shape[rowAxis]!;
|
|
185
|
+
if (!transpose && (rows === 2 || rows === 3)) {
|
|
186
|
+
const padShape = [...x.shape];
|
|
187
|
+
padShape[rowAxis] = 4 - rows;
|
|
188
|
+
const pad = zeros(padShape, x.dtype, s);
|
|
189
|
+
const xPad = concatAxis([x, pad], rowAxis, s);
|
|
190
|
+
pad.dispose();
|
|
191
|
+
const yPad = quantizedMatmul(xPad, w, scales, biases, spec, transpose, s);
|
|
192
|
+
xPad.dispose();
|
|
193
|
+
const start = MlxArray.fromInt32(new Int32Array([0]), [1]);
|
|
194
|
+
const outShape = [...yPad.shape];
|
|
195
|
+
outShape[rowAxis] = rows;
|
|
196
|
+
const y = sliceDynamic(yPad, start, [rowAxis], outShape, s);
|
|
197
|
+
start.dispose();
|
|
198
|
+
yPad.dispose();
|
|
199
|
+
return y;
|
|
200
|
+
}
|
|
158
201
|
return new MlxArray(
|
|
159
202
|
outArray("quantized_matmul", (o) =>
|
|
160
203
|
C.mlx_quantized_matmul(
|
|
@@ -267,6 +310,35 @@ export function geluApprox(x: MlxArray, s: S = gpuStream): MlxArray {
|
|
|
267
310
|
return out;
|
|
268
311
|
}
|
|
269
312
|
|
|
313
|
+
export function erf(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
314
|
+
return new MlxArray(outArray("erf", (o) => C.mlx_erf(o, a.handle, s)));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** nn.gelu (PRECISE, erf-based): x · (1 + erf(x/√2)) / 2 — composed exactly
|
|
318
|
+
* as mlx's python source (weak scalars promote to x's dtype). Used by the
|
|
319
|
+
* gemma-1 gated MLP and starcoder2's plain MLP; distinct from geluApprox
|
|
320
|
+
* (gelu_pytorch_tanh). */
|
|
321
|
+
export function geluPrecise(x: MlxArray, s: S = gpuStream): MlxArray {
|
|
322
|
+
const sqrt2 = scalarLike(Math.sqrt(2), x);
|
|
323
|
+
const xs = div(x, sqrt2, s); // x / math.sqrt(2) — division, not mul-by-reciprocal
|
|
324
|
+
const e = erf(xs, s);
|
|
325
|
+
const one = scalarLike(1, x);
|
|
326
|
+
const e1 = add(one, e, s);
|
|
327
|
+
const xe = mul(x, e1, s);
|
|
328
|
+
const two = scalarLike(2, x);
|
|
329
|
+
const out = div(xe, two, s); // (...) / 2
|
|
330
|
+
for (const a of [sqrt2, xs, e, one, e1, xe, two]) a.dispose();
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** mx.addmm(c, a, b): a@b + c fused (α=β=1) — nn.Linear's bias path
|
|
335
|
+
* (`mx.addmm(bias, x, weight.T)`). */
|
|
336
|
+
export function addmm(c: MlxArray, a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
337
|
+
return new MlxArray(
|
|
338
|
+
outArray("addmm", (o) => C.mlx_addmm(o, c.handle, a.handle, b.handle, 1.0, 1.0, s)),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
270
342
|
export function exp(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
271
343
|
return new MlxArray(outArray("exp", (o) => C.mlx_exp(o, a.handle, s)));
|
|
272
344
|
}
|
|
@@ -590,6 +590,29 @@ export class CompiledDecode {
|
|
|
590
590
|
const evalWith: MlxArray[] = [];
|
|
591
591
|
let carried = cur; // seg 0 consumes the token array; later, hidden state
|
|
592
592
|
let carriedOwned = false; // cur belongs to the caller
|
|
593
|
+
// Mid-step failure safety (a later segment's closure.apply can throw at
|
|
594
|
+
// first trace AFTER earlier layers already advanced): the step must be
|
|
595
|
+
// TRANSACTIONAL, or the caller's uncompiled re-forward of the same token
|
|
596
|
+
// double-writes the committed layers (+1 offset skew forever, silently
|
|
597
|
+
// corrupted attention). Two mechanisms, chosen by what each phase allows:
|
|
598
|
+
// - ring segments: adoptDecodeStep is a pure host-side handle swap with
|
|
599
|
+
// no reader between here and step end (each cache is read only by its
|
|
600
|
+
// own layer, inside the closure that produced the update), so the
|
|
601
|
+
// adopts are STAGED and committed only once every segment and js layer
|
|
602
|
+
// has succeeded. On failure the staged buffers are disposed and the
|
|
603
|
+
// ring caches are exactly at their pre-step state (prepareDecodeStep's
|
|
604
|
+
// growth/trim/rotate bookkeeping is benign for the uncompiled path —
|
|
605
|
+
// the same invariant #stepWhole and generate.ts's fallback rely on).
|
|
606
|
+
// - js (concat) layers: layer.forward commits via the cache's real
|
|
607
|
+
// updateAndFetch mid-loop, so on failure the commit is ROLLED BACK by
|
|
608
|
+
// reverting offset (and ring index) — trim(1) semantics. The written
|
|
609
|
+
// KV row beyond the reverted offset is dead data the re-forward
|
|
610
|
+
// overwrites at the same position (updateAndFetch writes at offset/
|
|
611
|
+
// ringIdx), so post-recovery state equals a single clean write. This
|
|
612
|
+
// holds for the post-wrap quantized-under-perf-kernel case too: the
|
|
613
|
+
// in-place S=1 write slot is re-derived from the reverted ringIdx.
|
|
614
|
+
const stagedAdopts: { c: AnyCache; bufs: MlxArray[] }[] = [];
|
|
615
|
+
const committedJs: { c: AnyCache; offsetBefore: number }[] = [];
|
|
593
616
|
try {
|
|
594
617
|
for (let s = 0; s < segs.length; s++) {
|
|
595
618
|
const { from, to } = segs[s]!;
|
|
@@ -636,17 +659,14 @@ export class CompiledDecode {
|
|
|
636
659
|
carried = outs[0]!;
|
|
637
660
|
carriedOwned = true;
|
|
638
661
|
// ring updates are ancestors of the carried hidden state (the
|
|
639
|
-
// in-graph fetch reads the updated buffer): no eval roots
|
|
662
|
+
// in-graph fetch reads the updated buffer): no eval roots. The
|
|
663
|
+
// host-side adoption is deferred to the end of the step (see above).
|
|
640
664
|
let oi = 1;
|
|
641
665
|
for (let i = from; i < to; i++) {
|
|
642
666
|
const c = anyCaches[i]!;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
} else {
|
|
647
|
-
(c as RotatingKVCache).adoptDecodeStep(outs[oi]!, outs[oi + 1]!);
|
|
648
|
-
oi += 2;
|
|
649
|
-
}
|
|
667
|
+
const width = c instanceof RotatingQuantizedKVCache ? 6 : 2;
|
|
668
|
+
stagedAdopts.push({ c, bufs: outs.slice(oi, oi + width) });
|
|
669
|
+
oi += width;
|
|
650
670
|
}
|
|
651
671
|
}
|
|
652
672
|
if (s < jsLayers.length) {
|
|
@@ -655,6 +675,7 @@ export class CompiledDecode {
|
|
|
655
675
|
const c = anyCaches[li]!;
|
|
656
676
|
const window = layer.layerType === "sliding_attention" ? this.model.windowSize : null;
|
|
657
677
|
const mask = c.makeMask(1, window); // N=1 → mode ""
|
|
678
|
+
committedJs.push({ c, offsetBefore: c.offset });
|
|
658
679
|
const { h: next, shared } = layer.forward(carried, mask, c, null, null);
|
|
659
680
|
if (carriedOwned) carried.dispose();
|
|
660
681
|
carried = next;
|
|
@@ -663,6 +684,20 @@ export class CompiledDecode {
|
|
|
663
684
|
mask.arr?.dispose();
|
|
664
685
|
}
|
|
665
686
|
}
|
|
687
|
+
// every segment and js layer succeeded — commit the staged ring adopts
|
|
688
|
+
for (const { c, bufs } of stagedAdopts) {
|
|
689
|
+
if (c instanceof RotatingQuantizedKVCache) c.adoptDecodeStep(bufs);
|
|
690
|
+
else (c as RotatingKVCache).adoptDecodeStep(bufs[0]!, bufs[1]!);
|
|
691
|
+
}
|
|
692
|
+
} catch (e) {
|
|
693
|
+
// unwind: free staged (never-adopted) ring buffers and the owned hidden
|
|
694
|
+
// state, and revert the js layers' committed writes, so the caller's
|
|
695
|
+
// uncompiled re-forward of this token starts from the pre-step state.
|
|
696
|
+
for (const { bufs } of stagedAdopts) for (const b of bufs) b.dispose();
|
|
697
|
+
for (const { c, offsetBefore } of committedJs)
|
|
698
|
+
if (c.offset > offsetBefore) c.trim(c.offset - offsetBefore);
|
|
699
|
+
if (carriedOwned) carried.dispose();
|
|
700
|
+
throw e;
|
|
666
701
|
} finally {
|
|
667
702
|
for (const t of stepTemps) t.dispose();
|
|
668
703
|
}
|
package/src/model/factory.ts
CHANGED
|
@@ -11,12 +11,21 @@ import { configFingerprint } from "./fingerprint";
|
|
|
11
11
|
import { GENERATED } from "./generated";
|
|
12
12
|
import { MiniCPM5Model } from "./minicpm5";
|
|
13
13
|
import { Qwen35Model } from "./qwen3_5";
|
|
14
|
+
import { Qwen3Model } from "./qwen3";
|
|
14
15
|
import { DiffusionGemmaModel } from "./diffusion-gemma";
|
|
15
|
-
import {
|
|
16
|
+
import { UniversalDenseModel } from "./universal/dense";
|
|
17
|
+
import { genericArgsFor, GENERIC_MODEL_TYPES, remapModelType } from "./universal/archs";
|
|
18
|
+
import { isDiffusionGemmaConfig, isMiniCPM5Config, isQwen35Config, isQwen3Config } from "./support";
|
|
16
19
|
|
|
17
|
-
export type RuntimeModel =
|
|
20
|
+
export type RuntimeModel =
|
|
21
|
+
| Gemma4Model | MiniCPM5Model | Qwen35Model | Qwen3Model | DiffusionGemmaModel
|
|
22
|
+
| UniversalDenseModel;
|
|
18
23
|
|
|
24
|
+
/** Dispatch ladder (docs/design/generic-model-support.md §3.3):
|
|
25
|
+
* dedicated → generated/monolith (gemma4*) → generic (Tier-0 universal)
|
|
26
|
+
* → reject with a helpful error. Generic never shadows a dedicated port. */
|
|
19
27
|
export function createModel(weights: Weights, config: ModelConfig): RuntimeModel {
|
|
28
|
+
// 1. Dedicated classes.
|
|
20
29
|
// DiffusionGemma is non-autoregressive — generate() detects it and routes to
|
|
21
30
|
// the denoising engine instead of the AR decode loop. It exposes the shared
|
|
22
31
|
// RuntimeModel surface (config/weightsBytes/loraState/makeCache) with the
|
|
@@ -24,10 +33,22 @@ export function createModel(weights: Weights, config: ModelConfig): RuntimeModel
|
|
|
24
33
|
if (isDiffusionGemmaConfig(config)) return new DiffusionGemmaModel(weights, config);
|
|
25
34
|
if (isMiniCPM5Config(config)) return new MiniCPM5Model(weights, config);
|
|
26
35
|
if (isQwen35Config(config)) return new Qwen35Model(weights, config);
|
|
27
|
-
if (config
|
|
28
|
-
|
|
29
|
-
if (config.modelType.startsWith("
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
if (isQwen3Config(config)) return new Qwen3Model(weights, config);
|
|
37
|
+
// 2. gemma4*: fingerprint-matched generated specialization, else monolith.
|
|
38
|
+
if (config.modelType.startsWith("gemma4")) {
|
|
39
|
+
const cls = GENERATED.get(configFingerprint(config)) ?? Gemma4Model;
|
|
40
|
+
return new cls(weights, config);
|
|
41
|
+
}
|
|
42
|
+
// 3. Generic Tier-0 fallback: the universal-dense descriptor table
|
|
43
|
+
// (replaces the old hard "llama = MiniCPM5 only" / unknown-type throws).
|
|
44
|
+
const generic = genericArgsFor(config);
|
|
45
|
+
if (generic) return new UniversalDenseModel(weights, config, generic);
|
|
46
|
+
// 4. Reject, naming the arch (post-remap) and the declared surface.
|
|
47
|
+
const arch = remapModelType(config.modelType);
|
|
48
|
+
throw new Error(
|
|
49
|
+
`unsupported model_type "${config.modelType}"` +
|
|
50
|
+
(arch !== config.modelType ? ` (mlx-lm remaps it to "${arch}")` : "") +
|
|
51
|
+
` — targeted: gemma4*, diffusion_gemma, qwen3_5, qwen3, MiniCPM5;` +
|
|
52
|
+
` generic (Tier-0): ${[...GENERIC_MODEL_TYPES].sort().join(", ")}`,
|
|
53
|
+
);
|
|
33
54
|
}
|
|
@@ -649,8 +649,10 @@ export function flashSupported(q: MlxArray): boolean {
|
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
/** Materialized differentiable attention (matmul → mask → softmax → matmul),
|
|
652
|
-
* GQA-aware.
|
|
653
|
-
*
|
|
652
|
+
* GQA-aware. A correct-vjp fallback for shapes/masks the flash kernel doesn't
|
|
653
|
+
* take. (ops.sdpa's dK vjp is CORRECT — FD-verified, the training default;
|
|
654
|
+
* the old "broken dK" note here predated the flash-kernel root-cause: the
|
|
655
|
+
* bugs were flash's own dK transpose + dQ barrier, both fixed.) O(L²) —
|
|
654
656
|
* pair with gradient checkpointing. */
|
|
655
657
|
export function manualSdpa(q: MlxArray, k: MlxArray, v: MlxArray, scale: number, mask: Mask): MlxArray {
|
|
656
658
|
const [B, Hq, T, Dh] = q.shape as [number, number, number, number];
|
|
@@ -26,13 +26,20 @@ import { Dtype } from "../mlx/ffi";
|
|
|
26
26
|
import { MetalKernel } from "../mlx/metal-kernel";
|
|
27
27
|
import * as ops from "../mlx/ops";
|
|
28
28
|
|
|
29
|
-
/** Perf-mode lever: DEFAULT ON (2026-06-11 cleared-machine pass, ~1.02x @8k)
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
/** Perf-mode lever: DEFAULT ON (2026-06-11 cleared-machine pass, ~1.02x @8k)
|
|
30
|
+
* — but an L3/explicit-only lever, OFF in the bare --l2 tier. This is an
|
|
31
|
+
* mlx-bun ORIGINAL kernel, NOT a port of optiq's fused decode, and the
|
|
32
|
+
* quantized-KV parity goldens do NOT track it: the optiq-golden decode
|
|
33
|
+
* composition is stock UNFUSED L=1 (scripts/regen-kvq-goldens.ts — the fused
|
|
34
|
+
* N-tiled SDPA covers L>1 prefill only, where this kernel never dispatches).
|
|
35
|
+
* It is NOT bit-exact vs the -O0 reference (online softmax, tier-b); its gate
|
|
36
|
+
* is ENVELOPE quality — ≥56/64 teacher-forced argmax agreement vs mlx-bun's
|
|
37
|
+
* own frozen compat trajectory (tests/perf-kernel-oracle.test.ts,
|
|
38
|
+
* scripts/freeze-perf-oracle.ts) plus bounded divergence vs the unfused port
|
|
39
|
+
* (tests/fused-decode-kernel.test.ts). MLX_BUN_PERF_KERNEL=0 selects the
|
|
40
|
+
* compat path we bench against the python libs. (B/C-style bit-exact
|
|
41
|
+
* optimizations stay on either way — this lever only toggles the
|
|
42
|
+
* parity-breaking decode kernel.) */
|
|
36
43
|
export function perfKernelEnabled(): boolean {
|
|
37
44
|
return process.env.MLX_BUN_PERF_KERNEL !== "0";
|
|
38
45
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The WHOLE MLP in ONE dispatch — down = (silu(x·Wgᵀ)·(x·Wuᵀ)) · Wdownᵀ — with no
|
|
2
|
+
// intermediate `hidden` ever exposed to MLX. MLX runs this one kernel; there is no
|
|
3
|
+
// boundary between our kernels for its planner to alias (the corruption only lives
|
|
4
|
+
// at those boundaries). Built from the steel building blocks in steel-qmm-header.ts,
|
|
5
|
+
// structured like the flash-CCE backward: each threadgroup owns one (M-block,I-block),
|
|
6
|
+
// computes silu·up for that I-block ONCE (no recompute), then atomic-accumulates its
|
|
7
|
+
// contribution to every down output — exactly how flash-CCE BWD atomic-adds dh.
|
|
8
|
+
//
|
|
9
|
+
// grid = (TGN, I/BI, M/BM); each tg: i-block i0 = tid.y*BI, m-block m0 = tid.z*BM
|
|
10
|
+
// phase A: gate/up [BM,BI] = x[BM,H] @ {Wg,Wu}[i0,H]ᵀ (reduce H) → silu·up = Hs[BM,BI]
|
|
11
|
+
// phase B: for each Ho-block no: D[BM,BN] = Hs @ Wdown[no,i0]ᵀ; atomic-add D → y[M,Ho]
|
|
12
|
+
//
|
|
13
|
+
// Per-weight quant constants (gate/up/down) are baked literals; H/I/Ho are templateInts;
|
|
14
|
+
// M from x_shape. Output is f32 (atomic accumulation); the caller casts to the model
|
|
15
|
+
// dtype at the MLP boundary. L3 (atomic-add ordering + bf16-rounded silu); gated by
|
|
16
|
+
// teacher-forced KL like the rest of the controlled path.
|
|
17
|
+
|
|
18
|
+
import { MlxArray } from "../mlx/array";
|
|
19
|
+
import { Dtype } from "../mlx/ffi";
|
|
20
|
+
import { MetalKernel } from "../mlx/metal-kernel";
|
|
21
|
+
import { STEEL_QMM_HEADER } from "../train/steel-qmm-header";
|
|
22
|
+
import * as ops from "../mlx/ops";
|
|
23
|
+
import type { QuantSpec } from "../mlx/ops";
|
|
24
|
+
|
|
25
|
+
const BM = 32, BN = 32, BK = 32, WM = 2, WN = 2;
|
|
26
|
+
const TGN = WM * WN * 32; // 128 threads / threadgroup
|
|
27
|
+
|
|
28
|
+
const SOURCE = String.raw`
|
|
29
|
+
const uint M=x_shape[0];
|
|
30
|
+
constexpr int I=I_T, H=H_T, Ho=HO_T;
|
|
31
|
+
constexpr int gsG=GS_G,btsG=BITS_G, gsU=GS_U,btsU=BITS_U, gsD=GS_D,btsD=BITS_D;
|
|
32
|
+
constexpr int pfG=get_pack_factor<btsG,8>(), bppG=get_bytes_per_pack<btsG>();
|
|
33
|
+
constexpr int pfU=get_pack_factor<btsU,8>(), bppU=get_bytes_per_pack<btsU>();
|
|
34
|
+
constexpr int pfD=get_pack_factor<btsD,8>(), bppD=get_bytes_per_pack<btsD>();
|
|
35
|
+
constexpr int BKp=BK_T+16/sizeof(T);
|
|
36
|
+
threadgroup T Xs[BM_T*BKp];
|
|
37
|
+
threadgroup T Wgs[BN_T*BKp];
|
|
38
|
+
threadgroup T Wus[BN_T*BKp];
|
|
39
|
+
threadgroup T Lg[BM_T*BN_T];
|
|
40
|
+
threadgroup T Lu[BM_T*BN_T];
|
|
41
|
+
threadgroup T Hs[BM_T*BKp]; // silu·up for this I-block, padded leading dim BKp
|
|
42
|
+
threadgroup T Wds[BN_T*BKp]; // a down-weight [BN, BI] tile
|
|
43
|
+
using gu_mma=mlx::steel::BlockMMA<T,T,BM_T,BN_T,BK_T,WM_T,WN_T,false,true,BKp,BKp>;
|
|
44
|
+
using lx_t =mlx::steel::BlockLoader<T,BM_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE>;
|
|
45
|
+
using lwg_t =QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gsG,btsG>;
|
|
46
|
+
using lwu_t =QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gsU,btsU>;
|
|
47
|
+
using d_mma =mlx::steel::BlockMMA<T,T,BM_T,BN_T,BK_T,WM_T,WN_T,false,true,BKp,BKp>;
|
|
48
|
+
using lwd_t =QuantizedBlockLoader<T,BN_T,BK_T,BKp,1,WM_T*WN_T*SIMD_SIZE,gsD,btsD>;
|
|
49
|
+
const uint sg=simdgroup_index_in_threadgroup, sl=thread_index_in_simdgroup;
|
|
50
|
+
const uint lid=thread_position_in_threadgroup.x;
|
|
51
|
+
const int Hh=(int)H;
|
|
52
|
+
const int KwG=Hh*bppG/pfG, KgG=Hh/gsG;
|
|
53
|
+
const int KwU=Hh*bppU/pfU, KgU=Hh/gsU;
|
|
54
|
+
const int KwD=I*bppD/pfD, KgD=I/gsD; // down weight rows are length I
|
|
55
|
+
const int m0=(int)threadgroup_position_in_grid.z*BM_T;
|
|
56
|
+
const int i0=(int)threadgroup_position_in_grid.y*BN_T;
|
|
57
|
+
const short num_els=(short)min(BM_T,(int)M-m0);
|
|
58
|
+
|
|
59
|
+
// ---- Phase A: hidden[BM,BI] = silu(x@Wg[i0]ᵀ)·(x@Wu[i0]ᵀ), reduce over H ----
|
|
60
|
+
gu_mma mg(sg,sl), mu(sg,sl);
|
|
61
|
+
lx_t lx (x + (int64_t)m0*Hh, Hh, Xs, sg, sl);
|
|
62
|
+
lwg_t lwg((const device uint8_t*)wg + (int64_t)i0*KwG, sg_w + (int64_t)i0*KgG, bg_w + (int64_t)i0*KgG, Hh, Wgs, sg, sl);
|
|
63
|
+
lwu_t lwu((const device uint8_t*)wu + (int64_t)i0*KwU, su_w + (int64_t)i0*KgU, bu_w + (int64_t)i0*KgU, Hh, Wus, sg, sl);
|
|
64
|
+
for (int k=0;k<Hh;k+=BK_T){
|
|
65
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
66
|
+
if (num_els<BM_T) lx.load_safe(short2(BK_T,num_els)); else lx.load_unsafe();
|
|
67
|
+
lwg.load_unsafe(); lwu.load_unsafe();
|
|
68
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
69
|
+
mg.mma(Xs,Wgs); mu.mma(Xs,Wus);
|
|
70
|
+
lx.next(); lwg.next(); lwu.next();
|
|
71
|
+
}
|
|
72
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
73
|
+
mg.Ctile.template store<T,WM_T,WN_T,BN_T,1>(Lg + mg.sm*BN_T + mg.sn);
|
|
74
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
75
|
+
mu.Ctile.template store<T,WM_T,WN_T,BN_T,1>(Lu + mu.sm*BN_T + mu.sn);
|
|
76
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
77
|
+
// silu·up into Hs (padded BKp leading dim, so the phase-B BlockMMA can read it as A)
|
|
78
|
+
for (uint idx=lid; idx<(uint)(BM_T*BN_T); idx+=TGN_T){
|
|
79
|
+
int r=(int)idx/BN_T, c=(int)idx%BN_T;
|
|
80
|
+
T gT=Lg[idx], uT=Lu[idx];
|
|
81
|
+
T sigT=(T)(1.0f/(1.0f+metal::precise::exp(-(float)gT)));
|
|
82
|
+
T siluT=(T)((float)gT*(float)sigT);
|
|
83
|
+
Hs[r*BKp + c] = (T)((float)siluT*(float)uT);
|
|
84
|
+
}
|
|
85
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
86
|
+
|
|
87
|
+
// ---- Phase B: for each Ho-block, D[BM,BN] = Hs @ Wdown[no,i0]ᵀ; atomic-add → y ----
|
|
88
|
+
for (int no=0; no<Ho; no+=BN_T){
|
|
89
|
+
lwd_t lwd((const device uint8_t*)wd + (int64_t)no*KwD + (int64_t)i0*bppD/pfD,
|
|
90
|
+
sd_w + (int64_t)no*KgD + i0/gsD, bd_w + (int64_t)no*KgD + i0/gsD, I, Wds, sg, sl);
|
|
91
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
92
|
+
lwd.load_unsafe(); // one BI-wide tile (BI==BK, single step)
|
|
93
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
94
|
+
d_mma D(sg,sl);
|
|
95
|
+
D.mma(Hs, Wds); // D[BM,BN] = Hs[BM,BI] @ Wds[BN,BI]ᵀ
|
|
96
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
97
|
+
D.Ctile.template store<T,WM_T,WN_T,BN_T,1>(Lg + D.sm*BN_T + D.sn); // reuse Lg to stage D
|
|
98
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
99
|
+
for (uint idx=lid; idx<(uint)(BM_T*BN_T); idx+=TGN_T){
|
|
100
|
+
int r=(int)idx/BN_T, c=(int)idx%BN_T;
|
|
101
|
+
if (m0+r<(int)M){
|
|
102
|
+
const int64_t off=(int64_t)(m0+r)*(int64_t)Ho + (no+c);
|
|
103
|
+
// Absorb the residual: add h exactly once (only the i0==0 threadgroups,
|
|
104
|
+
// which cover every (m,ho) once) so the kernel outputs h + mlp(x) and
|
|
105
|
+
// nothing is left live across it for MLX to plan a buffer around.
|
|
106
|
+
float add = (float)Lg[idx];
|
|
107
|
+
if (i0==0) add += (float)h[off];
|
|
108
|
+
atomic_fetch_add_explicit(&y[off], add, memory_order_relaxed);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const _kernels = new Map<string, MetalKernel>();
|
|
116
|
+
function getKernel(
|
|
117
|
+
gsG: number, btsG: number, gsU: number, btsU: number, gsD: number, btsD: number,
|
|
118
|
+
): MetalKernel {
|
|
119
|
+
const key = `${gsG}:${btsG}:${gsU}:${btsU}:${gsD}:${btsD}`;
|
|
120
|
+
let k = _kernels.get(key);
|
|
121
|
+
if (!k) {
|
|
122
|
+
k = new MetalKernel({
|
|
123
|
+
name: "mlx_bun_fused_mlp",
|
|
124
|
+
inputNames: ["x", "wg", "sg_w", "bg_w", "wu", "su_w", "bu_w", "wd", "sd_w", "bd_w", "h"],
|
|
125
|
+
outputNames: ["y"],
|
|
126
|
+
source: SOURCE,
|
|
127
|
+
header:
|
|
128
|
+
`${STEEL_QMM_HEADER}\n` +
|
|
129
|
+
`#define GS_G ${gsG}\n#define BITS_G ${btsG}\n#define GS_U ${gsU}\n#define BITS_U ${btsU}\n` +
|
|
130
|
+
`#define GS_D ${gsD}\n#define BITS_D ${btsD}\n` +
|
|
131
|
+
`#define BM_T ${BM}\n#define BN_T ${BN}\n#define BK_T ${BK}\n` +
|
|
132
|
+
`#define WM_T ${WM}\n#define WN_T ${WN}\n#define TGN_T ${TGN}\n`,
|
|
133
|
+
ensureRowContiguous: true,
|
|
134
|
+
atomicOutputs: true,
|
|
135
|
+
});
|
|
136
|
+
_kernels.set(key, k);
|
|
137
|
+
}
|
|
138
|
+
return k;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The whole MLP, fused, as one dispatch. x is [M, H] (bf16/f16). Returns the MLP
|
|
142
|
+
* output [M, Ho] in x.dtype (cast from the f32 atomic accumulator). gate/up have
|
|
143
|
+
* shape [I, H*bits/32]; down has [Ho, I*bits/32]. Caller owns the result. */
|
|
144
|
+
export function fusedMlp(
|
|
145
|
+
x: MlxArray,
|
|
146
|
+
wg: MlxArray, sg: MlxArray, bg: MlxArray, gateSpec: QuantSpec,
|
|
147
|
+
wu: MlxArray, su: MlxArray, bu: MlxArray, upSpec: QuantSpec,
|
|
148
|
+
wd: MlxArray, sd: MlxArray, bd: MlxArray, downSpec: QuantSpec,
|
|
149
|
+
h: MlxArray, // the residual (layer input) — absorbed: output = h + mlp(x)
|
|
150
|
+
): MlxArray {
|
|
151
|
+
const M = x.shape[0]!, I = wg.shape[0]!, H = x.shape[1]!, Ho = wd.shape[0]!;
|
|
152
|
+
const [yF] = getKernel(
|
|
153
|
+
gateSpec.groupSize, gateSpec.bits, upSpec.groupSize, upSpec.bits, downSpec.groupSize, downSpec.bits,
|
|
154
|
+
).apply([x, wg, sg, bg, wu, su, bu, wd, sd, bd, h], {
|
|
155
|
+
// f32 atomic accumulator, zero-initialised; grid tiles (M, I) — every I-block
|
|
156
|
+
// contributes to the output via atomic-add.
|
|
157
|
+
outputShapeFn: (ins) => [{ shape: [ins[0]!.shape[0]!, Ho], dtype: Dtype.float32 }],
|
|
158
|
+
grid: (ins) => [TGN, Math.ceil(I / BN), Math.ceil(ins[0]!.shape[0]! / BM)],
|
|
159
|
+
threadGroup: [TGN, 1, 1],
|
|
160
|
+
templateInts: { I_T: I, H_T: H, HO_T: Ho },
|
|
161
|
+
templateDtypes: { T: x.dtype },
|
|
162
|
+
initValue: 0,
|
|
163
|
+
});
|
|
164
|
+
void M;
|
|
165
|
+
if (yF!.dtype === x.dtype) return yF!;
|
|
166
|
+
const y = yF!.astype(x.dtype);
|
|
167
|
+
yF!.dispose();
|
|
168
|
+
return y;
|
|
169
|
+
}
|