mlx-bun 0.0.4 → 0.0.6
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 +175 -93
- package/docs/dag/training-inference-map.html +575 -0
- package/docs/design/adapters-end-to-end.md +50 -46
- package/docs/design/compat-cli-surface-design.md +10 -8
- package/docs/design/docs-reorg-plan.md +14 -1
- package/docs/design/hlg-sampling.md +12 -2
- package/docs/design/memory-system.md +193 -295
- package/docs/design/optimization_plan.md +4 -2
- package/docs/design/orpo-dynamic-lambda.md +179 -0
- package/docs/design/orpo-future-enhancements.md +340 -0
- package/docs/design/orpo-training.md +1212 -0
- package/docs/design/parallel-slots.md +34 -26
- package/docs/design/parity-tier-dag.md +143 -0
- package/docs/design/runtime-isolation.md +1 -1
- package/docs/design/segmented-backward-training.md +58 -39
- package/docs/design/tauri-desktop-app.md +127 -0
- package/docs/design/w4a16-compute-precision-spike.md +232 -0
- package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
- package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
- package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
- package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
- package/docs/investigations/starter-model-port-handoff.md +32 -13
- package/docs/investigations/steel-flash-cce-handoff.md +489 -0
- package/docs/investigations/trainer-validation-experiment.md +173 -0
- package/docs/planning/IDEAS.md +29 -0
- package/docs/planning/PRODUCT_ROADMAP.md +147 -27
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/journal.md +161 -0
- package/docs/reference/distribution.md +61 -5
- package/docs/reference/library-api.md +29 -14
- package/docs/reference/memory.md +77 -0
- package/docs/reference/orpo-quickstart.md +130 -0
- package/docs/reference/server-api.md +105 -7
- package/docs/reference/server-config.md +12 -5
- package/docs/reference/training.md +296 -0
- package/package.json +1 -1
- package/src/cli.ts +651 -18
- package/src/eval/ifeval.ts +138 -0
- package/src/eval/runner.ts +76 -6
- package/src/eval/tasks/bfcl.ts +49 -15
- package/src/eval/tasks/gsm8k.ts +21 -9
- package/src/eval/tasks/hashhop.ts +33 -4
- package/src/eval/tasks/humaneval.ts +30 -8
- package/src/eval/tasks/ifeval.ts +52 -18
- package/src/eval/tasks/mmlu.ts +85 -74
- package/src/eval/ultrafeedback.ts +60 -0
- package/src/jobs/runner.ts +6 -0
- package/src/lora.ts +12 -7
- package/src/memory/chunk.ts +25 -0
- package/src/memory/cluster.ts +24 -0
- package/src/memory/db.ts +114 -0
- package/src/memory/ingest.ts +22 -0
- package/src/memory/pipeline.ts +63 -0
- package/src/memory/prompts.ts +23 -0
- package/src/memory/schedule.ts +163 -0
- package/src/memory/synthesize.ts +29 -0
- package/src/memory/tools.ts +399 -0
- package/src/memory/vault.ts +694 -0
- package/src/memory/wikify.ts +23 -0
- package/src/mlx/metal-kernel.ts +31 -4
- package/src/model/flash-attention.ts +37 -19
- package/src/model/fused-geglu-kernel.ts +111 -0
- package/src/model/gemma4-base.ts +39 -1
- package/src/model/gemma4.ts +196 -21
- package/src/model/minicpm5.ts +44 -3
- package/src/pi-provider.ts +29 -13
- package/src/pi-session.ts +81 -0
- package/src/pi-terminal.ts +13 -24
- package/src/pi-web.ts +246 -166
- package/src/quantize/job.ts +17 -2
- package/src/quantize/quantizer.ts +26 -2
- package/src/server.ts +216 -47
- package/src/tokenizer.ts +6 -2
- package/src/train/dataset.ts +29 -6
- package/src/train/flash-cce.ts +1175 -0
- package/src/train/job.ts +64 -3
- package/src/train/lora-params.ts +53 -4
- package/src/train/loss.ts +947 -0
- package/src/train/optimizer.ts +10 -3
- package/src/train/prefix-shared.ts +435 -0
- package/src/train/rank.ts +36 -0
- package/src/train/segmented.ts +1148 -3
- package/src/train/steel-qmm-header.ts +1461 -0
- package/src/train/trainer.ts +570 -54
- package/src/train/watch.ts +542 -0
- package/src/web/app.html +285 -19
- package/src/web/skills/memory/SKILL.md +97 -0
- package/src/web/skills/web-research/SKILL.md +3 -1
- package/src/web/skills.ts +46 -8
- /package/{docs → src/assets}/curve-designer.html +0 -0
package/src/model/gemma4.ts
CHANGED
|
@@ -51,22 +51,48 @@ import {
|
|
|
51
51
|
type SharedKv,
|
|
52
52
|
type LoraWeights,
|
|
53
53
|
} from "./gemma4-base";
|
|
54
|
-
import { fusedGeglu, fusedGeluEnabled, fusedGeluSupported } from "./fused-geglu-kernel";
|
|
54
|
+
import { fusedGeglu, fusedGegluDifferentiable, fusedGeluEnabled, fusedGeluSupported, fusedGeluTraining } from "./fused-geglu-kernel";
|
|
55
55
|
import { Checkpoint } from "../mlx/checkpoint";
|
|
56
56
|
import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
|
|
57
57
|
|
|
58
58
|
/** Optional gradient-checkpointing context for the training forward. When set,
|
|
59
59
|
* forwardLayers wraps each layer in a Checkpoint so its interior activations
|
|
60
60
|
* are recomputed in the backward pass. `byLayer` maps a layer index to that
|
|
61
|
-
* layer's trainable LoRA weights
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* layer's trainable LoRA weights, partitioned into the attention sub-block
|
|
62
|
+
* (q/k/v/o_proj) and the MLP sub-block (gate/up/down_proj + per-layer gate/
|
|
63
|
+
* projection) — re-swapped inside the closure so they are explicit checkpoint
|
|
64
|
+
* inputs (required because the autograd primals are disposed before the
|
|
65
|
+
* recompute). When `splitMlp` is set, the layer is wrapped in TWO checkpoints
|
|
66
|
+
* (attn sub-block + MLP sub-block) with the post-attn residual `hMid` as the
|
|
67
|
+
* boundary, so the backward recompute holds `max(attn,MLP)+hMid` instead of the
|
|
68
|
+
* whole layer (the intra-layer MLP split, lever 4). `keepAlive` collects the
|
|
64
69
|
* Checkpoint objects; the trainer disposes them after value_and_grad. */
|
|
70
|
+
export interface LayerLoras {
|
|
71
|
+
attn: LoraWeights[];
|
|
72
|
+
mlp: LoraWeights[];
|
|
73
|
+
}
|
|
65
74
|
export interface GradCheckpointCtx {
|
|
66
|
-
byLayer: Map<number,
|
|
75
|
+
byLayer: Map<number, LayerLoras>;
|
|
76
|
+
splitMlp: boolean;
|
|
67
77
|
keepAlive: Checkpoint[];
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
// Shared prompt-prefix plan (lever 7, e4b — docs/design/orpo-training.md). When
|
|
81
|
+
// set, Attention ropes the concatenated [prompt(P); chosenResp(Rc); rejectedResp(Rr)]
|
|
82
|
+
// sequence BLOCK-WISE — prompt at offset 0, EACH response reset to logical offset
|
|
83
|
+
// P — instead of the uniform cache.offset, so ONE forward over the concat is
|
|
84
|
+
// bit-exact with the two separate [prompt;resp] forwards (each response sees the
|
|
85
|
+
// prompt's RoPE positions, not shifted-by-the-other-response). The matching
|
|
86
|
+
// block-sparse + LOGICAL-window attention mask rides in via the prefix cache's
|
|
87
|
+
// makeMask (src/train/prefix-shared.ts). Set around the single forward
|
|
88
|
+
// (single-threaded), cleared after; null → the normal uniform-offset rope (every
|
|
89
|
+
// other forward is untouched).
|
|
90
|
+
export interface GemmaPrefixPlan { P: number; Rc: number; Rr: number }
|
|
91
|
+
let _gemmaPrefixPlan: GemmaPrefixPlan | null = null;
|
|
92
|
+
export function setGemmaPrefixPlan(p: GemmaPrefixPlan | null): void {
|
|
93
|
+
_gemmaPrefixPlan = p;
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
class Attention {
|
|
71
97
|
readonly isSliding: boolean;
|
|
72
98
|
readonly windowSize: number;
|
|
@@ -161,6 +187,32 @@ class Attention {
|
|
|
161
187
|
: ops.ropeDynamic(x, this.headDim, this.ropeBase, offset, this.ropeFreqs);
|
|
162
188
|
}
|
|
163
189
|
|
|
190
|
+
/** Block-wise RoPE for the prefix-shared concat [prompt; chosen; rejected] along
|
|
191
|
+
* the sequence axis (axis 2 of [B,H,T,D]): prompt at offset 0, EACH response
|
|
192
|
+
* reset to logical offset P. RoPE is per-token, so roping each contiguous block
|
|
193
|
+
* at its scalar offset and concatenating == roping with per-token position-ids
|
|
194
|
+
* [0..P-1, P..P+Rc-1, P..P+Rr-1]. Uses this.rope per block so the layer's
|
|
195
|
+
* proportional/ropeFreqs + sliding-vs-full geometry applies identically to the
|
|
196
|
+
* two-forward path. Caller disposes the input. */
|
|
197
|
+
ropeBlocks(x: MlxArray, plan: GemmaPrefixPlan): MlxArray {
|
|
198
|
+
const { P, Rc, Rr } = plan;
|
|
199
|
+
const [B, H, , D] = x.shape as [number, number, number, number];
|
|
200
|
+
const blocks = [
|
|
201
|
+
{ start: 0, len: P, off: 0 },
|
|
202
|
+
{ start: P, len: Rc, off: P },
|
|
203
|
+
{ start: P + Rc, len: Rr, off: P },
|
|
204
|
+
].filter((b) => b.len > 0);
|
|
205
|
+
const parts = blocks.map((b) => {
|
|
206
|
+
const sl = x.slice([0, 0, b.start, 0], [B, H, b.start + b.len, D]);
|
|
207
|
+
const r = this.rope(sl, b.off);
|
|
208
|
+
sl.dispose();
|
|
209
|
+
return r;
|
|
210
|
+
});
|
|
211
|
+
const out = ops.concatAxis(parts, 2);
|
|
212
|
+
for (const p of parts) p.dispose();
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
164
216
|
/** Returns the attention output plus the fetched KV (for KV-shared
|
|
165
217
|
* sharer layers and the speculative drafter). The SharedKv arrays are
|
|
166
218
|
* owned by the caller (forwardLayers) and disposed after the pass. */
|
|
@@ -203,7 +255,9 @@ class Attention {
|
|
|
203
255
|
const kNormed = this.kNorm!.forward(k);
|
|
204
256
|
const kT = ops.transposeAxes(kNormed, [0, 2, 1, 3]);
|
|
205
257
|
kNormed.dispose();
|
|
206
|
-
const kRoped =
|
|
258
|
+
const kRoped = _gemmaPrefixPlan
|
|
259
|
+
? this.ropeBlocks(kT, _gemmaPrefixPlan)
|
|
260
|
+
: this.rope(kT, offsetArr ?? offset);
|
|
207
261
|
kT.dispose();
|
|
208
262
|
|
|
209
263
|
const vNormed = this.vNorm!.forward(v);
|
|
@@ -230,7 +284,9 @@ class Attention {
|
|
|
230
284
|
}
|
|
231
285
|
|
|
232
286
|
q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
|
|
233
|
-
q = disposing(q,
|
|
287
|
+
q = disposing(q, _gemmaPrefixPlan
|
|
288
|
+
? this.ropeBlocks(q, _gemmaPrefixPlan)
|
|
289
|
+
: this.rope(q, shared.offsetArr ?? shared.offset));
|
|
234
290
|
|
|
235
291
|
let attn: MlxArray;
|
|
236
292
|
const ta = getTrainingAttn();
|
|
@@ -273,9 +329,12 @@ class MLP {
|
|
|
273
329
|
const u = this.up.forward(x);
|
|
274
330
|
// Fused GeGLU perf kernel (opt-in, not in a compiled trace): one pass
|
|
275
331
|
// instead of ~9 element-wise kernels. Off → the bit-exact spelled path.
|
|
332
|
+
// In training mode the differentiable wrapper (kernel forward + hand-derived
|
|
333
|
+
// vjp) is used so autograd can flow AND the backward recomputes the gelu
|
|
334
|
+
// from the primal instead of retaining the spelled-out intermediates.
|
|
276
335
|
let h: MlxArray;
|
|
277
336
|
if (fusedGeluEnabled() && !isCompiledTrace() && fusedGeluSupported(g)) {
|
|
278
|
-
h = fusedGeglu(g, u);
|
|
337
|
+
h = fusedGeluTraining() ? fusedGegluDifferentiable(g, u) : fusedGeglu(g, u);
|
|
279
338
|
g.dispose();
|
|
280
339
|
u.dispose();
|
|
281
340
|
} else {
|
|
@@ -500,6 +559,23 @@ export class DecoderLayer {
|
|
|
500
559
|
forward(
|
|
501
560
|
x: MlxArray, mask: Mask, cache: Cache | null,
|
|
502
561
|
sharedIn: SharedKv | null, perLayerInput: MlxArray | null,
|
|
562
|
+
): { h: MlxArray; shared: SharedKv } {
|
|
563
|
+
// Composed from the two sub-blocks (the intra-layer min-cut is the post-attn
|
|
564
|
+
// residual `hMid` [1,T,hidden]). Behaviour is identical to the spelled-out
|
|
565
|
+
// body; the split lets the checkpoint path rematerialize attn and MLP
|
|
566
|
+
// separately (peak attn+MLP → max(attn,MLP)+hMid). `forwardMlp` borrows hMid.
|
|
567
|
+
const { h: hMid, shared } = this.forwardAttn(x, mask, cache, sharedIn);
|
|
568
|
+
const h = this.forwardMlp(hMid, perLayerInput);
|
|
569
|
+
hMid.dispose();
|
|
570
|
+
return { h, shared };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Attention sub-block: inputNorm → attn → postAttnNorm → residual add.
|
|
574
|
+
* Returns the post-attn residual stream `h` [1,T,hidden] (the intra-layer
|
|
575
|
+
* boundary) and the attention's fetched/produced K/V (`shared`). `x` is
|
|
576
|
+
* borrowed (never disposed). The e4b donor-KV threading lives entirely here. */
|
|
577
|
+
forwardAttn(
|
|
578
|
+
x: MlxArray, mask: Mask, cache: Cache | null, sharedIn: SharedKv | null,
|
|
503
579
|
): { h: MlxArray; shared: SharedKv } {
|
|
504
580
|
let h = this.inputNorm.forward(x);
|
|
505
581
|
const { out, shared } = this.attn.forward(h, mask, cache, sharedIn);
|
|
@@ -507,18 +583,24 @@ export class DecoderLayer {
|
|
|
507
583
|
h = out;
|
|
508
584
|
h = disposing(h, this.postAttnNorm.forward(h));
|
|
509
585
|
h = disposing(h, ops.add(x, h));
|
|
586
|
+
return { h, shared };
|
|
587
|
+
}
|
|
510
588
|
|
|
511
|
-
|
|
589
|
+
/** MLP sub-block: FFN (dense or MoE) + postFfNorm + residual add, then the
|
|
590
|
+
* e4b per-layer-input gate and layer scalar. `hMid` (the attn sub-block's
|
|
591
|
+
* output) is BORROWED — the caller owns/disposes it (it is the checkpoint
|
|
592
|
+
* boundary leaf), so this never disposes it. */
|
|
593
|
+
forwardMlp(hMid: MlxArray, perLayerInput: MlxArray | null): MlxArray {
|
|
512
594
|
let f: MlxArray;
|
|
513
595
|
if (this.router && this.experts) {
|
|
514
|
-
// MoE: dense MLP and routed experts as parallel branches off
|
|
596
|
+
// MoE: dense MLP and routed experts as parallel branches off hMid
|
|
515
597
|
// (reference DecoderLayer, enable_moe path)
|
|
516
|
-
let h1 = this.preFfNorm.forward(
|
|
598
|
+
let h1 = this.preFfNorm.forward(hMid);
|
|
517
599
|
h1 = disposing(h1, this.mlp.forward(h1));
|
|
518
600
|
h1 = disposing(h1, this.postFfNorm1!.forward(h1));
|
|
519
601
|
|
|
520
|
-
const { indices, weights: topKWeights } = this.router.forward(
|
|
521
|
-
let h2 = this.preFfNorm2!.forward(
|
|
602
|
+
const { indices, weights: topKWeights } = this.router.forward(hMid);
|
|
603
|
+
let h2 = this.preFfNorm2!.forward(hMid);
|
|
522
604
|
h2 = disposing(h2, this.experts.forward(h2, indices, topKWeights));
|
|
523
605
|
h2 = disposing(h2, this.postFfNorm2!.forward(h2));
|
|
524
606
|
indices.dispose();
|
|
@@ -528,20 +610,25 @@ export class DecoderLayer {
|
|
|
528
610
|
h1.dispose();
|
|
529
611
|
h2.dispose();
|
|
530
612
|
} else {
|
|
531
|
-
f = this.preFfNorm.forward(
|
|
613
|
+
f = this.preFfNorm.forward(hMid);
|
|
532
614
|
f = disposing(f, this.mlp.forward(f));
|
|
533
615
|
}
|
|
534
616
|
f = disposing(f, this.postFfNorm.forward(f));
|
|
535
|
-
h = ops.add(
|
|
536
|
-
residual.dispose();
|
|
617
|
+
let h = ops.add(hMid, f); // hMid borrowed — NOT disposed here
|
|
537
618
|
f.dispose();
|
|
538
619
|
|
|
539
|
-
// per-layer input gating (reference gemma4_text DecoderLayer)
|
|
620
|
+
// per-layer input gating (reference gemma4_text DecoderLayer). gelu(gate)·pli
|
|
621
|
+
// is the second GeGLU site — fused (differentiable) on the training path, the
|
|
622
|
+
// bit-exact spelled-out gelu+mul on the inference path (unchanged).
|
|
540
623
|
if (this.perLayerGate && this.perLayerProjection && this.postPerLayerNorm && perLayerInput) {
|
|
541
624
|
const res2 = h;
|
|
542
625
|
let gate = this.perLayerGate.forward(h);
|
|
543
|
-
|
|
544
|
-
|
|
626
|
+
if (fusedGeluTraining() && fusedGeluEnabled() && !isCompiledTrace() && fusedGeluSupported(gate)) {
|
|
627
|
+
gate = disposing(gate, fusedGegluDifferentiable(gate, perLayerInput));
|
|
628
|
+
} else {
|
|
629
|
+
gate = disposing(gate, ops.geluApprox(gate));
|
|
630
|
+
gate = disposing(gate, ops.mul(gate, perLayerInput));
|
|
631
|
+
}
|
|
545
632
|
gate = disposing(gate, this.perLayerProjection.forward(gate));
|
|
546
633
|
gate = disposing(gate, this.postPerLayerNorm.forward(gate));
|
|
547
634
|
h = ops.add(res2, gate);
|
|
@@ -550,7 +637,7 @@ export class DecoderLayer {
|
|
|
550
637
|
}
|
|
551
638
|
|
|
552
639
|
if (this.layerScalar) h = disposing(h, ops.mul(h, this.layerScalar));
|
|
553
|
-
return
|
|
640
|
+
return h;
|
|
554
641
|
}
|
|
555
642
|
}
|
|
556
643
|
|
|
@@ -927,7 +1014,19 @@ export class Gemma4Model {
|
|
|
927
1014
|
layerCache: Cache | null, sharedIn: SharedKv | null, pls: MlxArray | null,
|
|
928
1015
|
): { h: MlxArray; shared: SharedKv | null } {
|
|
929
1016
|
const ctx = this.gradCkpt!;
|
|
930
|
-
const
|
|
1017
|
+
const ll = ctx.byLayer.get(i) ?? { attn: [], mlp: [] };
|
|
1018
|
+
return ctx.splitMlp
|
|
1019
|
+
? this.runSplitCheckpointedLayer(layer, h, mask, layerCache, sharedIn, pls, ll, ctx)
|
|
1020
|
+
: this.runSingleCheckpointedLayer(layer, h, mask, layerCache, sharedIn, pls, ll, ctx);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** One Checkpoint over the whole layer (the original path). */
|
|
1024
|
+
private runSingleCheckpointedLayer(
|
|
1025
|
+
layer: DecoderLayer, h: MlxArray, mask: Mask,
|
|
1026
|
+
layerCache: Cache | null, sharedIn: SharedKv | null, pls: MlxArray | null,
|
|
1027
|
+
ll: LayerLoras, ctx: GradCheckpointCtx,
|
|
1028
|
+
): { h: MlxArray; shared: SharedKv | null } {
|
|
1029
|
+
const lws = [...ll.attn, ...ll.mlp];
|
|
931
1030
|
const isSharer = layerCache === null;
|
|
932
1031
|
const sIn = isSharer && sharedIn && sharedIn.kind === "plain" ? sharedIn : null;
|
|
933
1032
|
const hasMaskArr = mask.arr !== null;
|
|
@@ -970,6 +1069,82 @@ export class Gemma4Model {
|
|
|
970
1069
|
return { h: outs[0]!, shared };
|
|
971
1070
|
}
|
|
972
1071
|
|
|
1072
|
+
/** Intra-layer MLP split (lever 4): TWO checkpoints — the attention sub-block
|
|
1073
|
+
* (forwardAttn: inputNorm → attn → residual add, threading the donor K/V) and
|
|
1074
|
+
* the MLP sub-block (forwardMlp: FFN + per-layer gate) — with the post-attn
|
|
1075
|
+
* residual `hMid` as the boundary. The backward recompute holds only one
|
|
1076
|
+
* sub-block's activations at a time, dropping per-layer peak from `attn+MLP`
|
|
1077
|
+
* to `max(attn,MLP)+hMid`. Attn LoRA / pls / donor-KV go to the attn ck; MLP
|
|
1078
|
+
* LoRA to the MLP ck. */
|
|
1079
|
+
private runSplitCheckpointedLayer(
|
|
1080
|
+
layer: DecoderLayer, h: MlxArray, mask: Mask,
|
|
1081
|
+
layerCache: Cache | null, sharedIn: SharedKv | null, pls: MlxArray | null,
|
|
1082
|
+
ll: LayerLoras, ctx: GradCheckpointCtx,
|
|
1083
|
+
): { h: MlxArray; shared: SharedKv | null } {
|
|
1084
|
+
const isSharer = layerCache === null;
|
|
1085
|
+
const sIn = isSharer && sharedIn && sharedIn.kind === "plain" ? sharedIn : null;
|
|
1086
|
+
const hasMaskArr = mask.arr !== null;
|
|
1087
|
+
const hasPls = pls !== null;
|
|
1088
|
+
const maskMode = mask.mode;
|
|
1089
|
+
const attnLws = ll.attn;
|
|
1090
|
+
const mlpLws = ll.mlp;
|
|
1091
|
+
|
|
1092
|
+
// --- attn sub-block checkpoint: [h, maskArr?, sharedInK?, sharedInV?, attnLora*]
|
|
1093
|
+
const attnInputs: MlxArray[] = [h];
|
|
1094
|
+
if (hasMaskArr) attnInputs.push(mask.arr!);
|
|
1095
|
+
if (sIn) attnInputs.push(sIn.keys, sIn.values);
|
|
1096
|
+
for (const lw of attnLws) attnInputs.push(lw.a, lw.b);
|
|
1097
|
+
|
|
1098
|
+
const attnCk = new Checkpoint((ins) => {
|
|
1099
|
+
let k = 0;
|
|
1100
|
+
const hIn = ins[k++]!;
|
|
1101
|
+
const mArr = hasMaskArr ? ins[k++]! : null;
|
|
1102
|
+
const sharedInRebuilt: SharedKv | null = sIn
|
|
1103
|
+
? { kind: "plain", keys: ins[k++]!, values: ins[k++]!, offset: 0 }
|
|
1104
|
+
: null;
|
|
1105
|
+
const saved = attnLws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
|
|
1106
|
+
for (const lw of attnLws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
|
|
1107
|
+
try {
|
|
1108
|
+
const out = layer.forwardAttn(hIn, { mode: maskMode, arr: mArr }, layerCache, sharedInRebuilt);
|
|
1109
|
+
if (!isSharer && out.shared && out.shared.kind === "plain")
|
|
1110
|
+
return [out.h, out.shared.keys, out.shared.values];
|
|
1111
|
+
return [out.h];
|
|
1112
|
+
} finally {
|
|
1113
|
+
attnLws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
ctx.keepAlive.push(attnCk);
|
|
1117
|
+
const attnOuts = attnCk.apply(attnInputs);
|
|
1118
|
+
const hMid = attnOuts[0]!;
|
|
1119
|
+
const shared: SharedKv | null =
|
|
1120
|
+
!isSharer && attnOuts.length === 3
|
|
1121
|
+
? { kind: "plain", keys: attnOuts[1]!, values: attnOuts[2]!, offset: 0 }
|
|
1122
|
+
: null;
|
|
1123
|
+
|
|
1124
|
+
// --- MLP sub-block checkpoint: [hMid, pls?, mlpLora*] -> [hOut]
|
|
1125
|
+
const mlpInputs: MlxArray[] = [hMid];
|
|
1126
|
+
if (hasPls) mlpInputs.push(pls!);
|
|
1127
|
+
for (const lw of mlpLws) mlpInputs.push(lw.a, lw.b);
|
|
1128
|
+
|
|
1129
|
+
const mlpCk = new Checkpoint((ins) => {
|
|
1130
|
+
let k = 0;
|
|
1131
|
+
const hMidIn = ins[k++]!;
|
|
1132
|
+
const plsIn = hasPls ? ins[k++]! : null;
|
|
1133
|
+
const saved = mlpLws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
|
|
1134
|
+
for (const lw of mlpLws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
|
|
1135
|
+
try {
|
|
1136
|
+
return [layer.forwardMlp(hMidIn, plsIn)];
|
|
1137
|
+
} finally {
|
|
1138
|
+
mlpLws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
ctx.keepAlive.push(mlpCk);
|
|
1142
|
+
const mlpOuts = mlpCk.apply(mlpInputs);
|
|
1143
|
+
hMid.dispose(); // boundary leaf consumed by the MLP ck (mlx retains it in-graph)
|
|
1144
|
+
|
|
1145
|
+
return { h: mlpOuts[0]!, shared };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
973
1148
|
/** hidden [1, L, hidden] → softcapped logits [1, L, vocab]. */
|
|
974
1149
|
logitsFromHidden(h: MlxArray): MlxArray {
|
|
975
1150
|
let logits = this.embed.asLinear(h);
|
package/src/model/minicpm5.ts
CHANGED
|
@@ -21,6 +21,45 @@ import {
|
|
|
21
21
|
} from "./gemma4-base";
|
|
22
22
|
import { flashAttention, getTrainingAttn, flashSupported } from "./flash-attention";
|
|
23
23
|
|
|
24
|
+
// Shared prompt-prefix plan (lever 7). When set, the attention ropes the
|
|
25
|
+
// concatenated [prompt(P); chosenResp(Rc); rejectedResp(Rr)] sequence
|
|
26
|
+
// BLOCK-WISE — prompt at offset 0, EACH response reset to offset P — instead of
|
|
27
|
+
// the uniform cache.offset. This is what lets one forward over the concat be
|
|
28
|
+
// bit-exact with the two separate [prompt;resp] forwards (each response sees the
|
|
29
|
+
// prompt's RoPE positions, not shifted-by-the-other-response). Set by
|
|
30
|
+
// orpoLossPrefixShared around the forward (single-threaded), cleared after; null
|
|
31
|
+
// → the normal uniform-offset rope (every other forward is untouched). The
|
|
32
|
+
// matching block-sparse attention mask rides in via PrefixSharedCache.makeMask.
|
|
33
|
+
export interface PrefixPlan { P: number; Rc: number; Rr: number }
|
|
34
|
+
let _prefixPlan: PrefixPlan | null = null;
|
|
35
|
+
export function setMiniCpmPrefixPlan(p: PrefixPlan | null): void {
|
|
36
|
+
_prefixPlan = p;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Block-wise RoPE for the prefix-shared concat [prompt; chosen; rejected] along
|
|
40
|
+
* the sequence axis (axis 2 of [B,H,T,D]): prompt rotated at offset 0, each
|
|
41
|
+
* response at offset P (reset). RoPE is per-token, so roping each contiguous
|
|
42
|
+
* block at its scalar offset and concatenating == roping with per-token
|
|
43
|
+
* position-ids [0..P-1, P..P+Rc-1, P..P+Rr-1]. Caller disposes the input. */
|
|
44
|
+
function ropeBlocks(x: MlxArray, dims: number, base: number, plan: PrefixPlan): MlxArray {
|
|
45
|
+
const { P, Rc, Rr } = plan;
|
|
46
|
+
const [B, H, , D] = x.shape as [number, number, number, number];
|
|
47
|
+
const blocks: { start: number; len: number; off: number }[] = [
|
|
48
|
+
{ start: 0, len: P, off: 0 },
|
|
49
|
+
{ start: P, len: Rc, off: P },
|
|
50
|
+
{ start: P + Rc, len: Rr, off: P },
|
|
51
|
+
].filter((b) => b.len > 0);
|
|
52
|
+
const parts = blocks.map((b) => {
|
|
53
|
+
const sl = x.slice([0, 0, b.start, 0], [B, H, b.start + b.len, D]);
|
|
54
|
+
const r = ops.rope(sl, dims, base, b.off, null);
|
|
55
|
+
sl.dispose();
|
|
56
|
+
return r;
|
|
57
|
+
});
|
|
58
|
+
const out = ops.concatAxis(parts, 2);
|
|
59
|
+
for (const p of parts) p.dispose();
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
24
63
|
class LlamaAttention {
|
|
25
64
|
readonly qProj: QuantizedLinear;
|
|
26
65
|
readonly kProj: QuantizedLinear;
|
|
@@ -65,9 +104,11 @@ class LlamaAttention {
|
|
|
65
104
|
// runs before updateAndFetch, so K and Q share the pre-write offset.
|
|
66
105
|
const offsetArr = cache.ropeOffsetArr;
|
|
67
106
|
const ropeStep = (x: MlxArray): MlxArray =>
|
|
68
|
-
|
|
69
|
-
?
|
|
70
|
-
:
|
|
107
|
+
_prefixPlan
|
|
108
|
+
? ropeBlocks(x, this.headDim, this.ropeBase, _prefixPlan)
|
|
109
|
+
: offsetArr
|
|
110
|
+
? ops.ropeDynamic(x, this.headDim, this.ropeBase, offsetArr, null)
|
|
111
|
+
: ops.rope(x, this.headDim, this.ropeBase, cache.offset, null);
|
|
71
112
|
q = disposing(q, ropeStep(q));
|
|
72
113
|
k = disposing(k, ropeStep(k));
|
|
73
114
|
let attn: MlxArray;
|
package/src/pi-provider.ts
CHANGED
|
@@ -40,6 +40,13 @@ export interface PiProviderOptions {
|
|
|
40
40
|
* reasoning and sends the on/off switch the server maps to enable_thinking.
|
|
41
41
|
* Default false. */
|
|
42
42
|
reasoning?: boolean;
|
|
43
|
+
/** The loaded model can accept images (server's `ctx.vision != null`).
|
|
44
|
+
* This MUST match the real model: pi-ai serializes user content as OpenAI
|
|
45
|
+
* multimodal content-parts (`[{type:"text",…}]`) for image-capable models
|
|
46
|
+
* and as a plain string otherwise. A text-only chat template renders nothing
|
|
47
|
+
* for a parts array, so declaring a non-vision model as image-capable makes
|
|
48
|
+
* the user's turn vanish ("I don't see any message"). Default false. */
|
|
49
|
+
vision?: boolean;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export interface PiProviderWiring {
|
|
@@ -69,19 +76,28 @@ export function buildPiProvider(baseUrl: string, opts: PiProviderOptions = {}):
|
|
|
69
76
|
name: opts.name ?? "mlx-bun (local)",
|
|
70
77
|
api: PI_API,
|
|
71
78
|
reasoning: opts.reasoning ?? false,
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
// Compat flags that make pi emit the wire format our local server's
|
|
80
|
+
// chat templates expect (docs/custom-provider.md "Model Definition
|
|
81
|
+
// Reference"):
|
|
82
|
+
// - supportsDeveloperRole:false → pi sends the system prompt as the
|
|
83
|
+
// `system` role, not `developer` (our templates only know
|
|
84
|
+
// system/user/assistant/tool).
|
|
85
|
+
// - thinkingFormat:"qwen-chat-template" (reasoning models only) → pi
|
|
86
|
+
// sends chat_template_kwargs.enable_thinking, the boolean our
|
|
87
|
+
// Qwen3.5/MiniCPM5 templates gate on. pi computes the boolean from
|
|
88
|
+
// the selected thinking level (off → false), so no thinkingLevelMap.
|
|
89
|
+
compat: {
|
|
90
|
+
supportsDeveloperRole: false,
|
|
91
|
+
...(opts.reasoning ? { thinkingFormat: "qwen-chat-template" as const } : {}),
|
|
92
|
+
},
|
|
93
|
+
// input MUST match the loaded model. pi-ai serializes user content as a
|
|
94
|
+
// plain string for text-only models and as OpenAI content-parts
|
|
95
|
+
// (`[{type:"text",…}]`) once a model declares image input — and a
|
|
96
|
+
// text-only chat template renders NOTHING for a parts array, dropping
|
|
97
|
+
// the user's turn ("I don't see any message"). So declare image input
|
|
98
|
+
// only for an actual vision model; the web `ready` frame's `vision`
|
|
99
|
+
// flag is the same signal that drives the UI's image affordance.
|
|
100
|
+
input: opts.vision ? ["text", "image"] : ["text"],
|
|
85
101
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
86
102
|
contextWindow: opts.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
87
103
|
maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Shared pi front-door assembly for mlx-bun's embedded web + terminal agents.
|
|
2
|
+
//
|
|
3
|
+
// This is the single place that decides which custom tools and bundled skills
|
|
4
|
+
// are visible to pi. Keeping that policy here prevents the browser and TUI from
|
|
5
|
+
// drifting — especially for personal memory, whose prompt surface must be
|
|
6
|
+
// relevant-but-not-dominant.
|
|
7
|
+
|
|
8
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { createWebTools, WEB_TOOL_NAMES } from "./web-tools";
|
|
10
|
+
import { materializeBundledSkillPaths } from "./web/skills";
|
|
11
|
+
import {
|
|
12
|
+
createMemoryTools,
|
|
13
|
+
MEMORY_TOOL_NAMES,
|
|
14
|
+
memoryIndexHint,
|
|
15
|
+
isMemoryEnabled,
|
|
16
|
+
createReferenceTools,
|
|
17
|
+
REFERENCE_TOOL_NAMES,
|
|
18
|
+
} from "./memory/tools";
|
|
19
|
+
|
|
20
|
+
/** Built-in pi coding tools plus mlx-bun's non-mutating web tools. */
|
|
21
|
+
export const PI_BASE_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls", ...WEB_TOOL_NAMES] as const;
|
|
22
|
+
|
|
23
|
+
export interface PiAgentSurfaceOptions {
|
|
24
|
+
/** Force memory on/off for tests. Default: auto-detect vault existence. */
|
|
25
|
+
memory?: "auto" | "on" | "off";
|
|
26
|
+
/** Include web_search/web_fetch/weather. Default true. */
|
|
27
|
+
webTools?: boolean;
|
|
28
|
+
/** Include read/bash/edit/write/grep/find/ls. Default true. */
|
|
29
|
+
codingTools?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PiAgentSurface {
|
|
33
|
+
memoryEnabled: boolean;
|
|
34
|
+
/** Tool allowlist to pass to createAgentSession/createAgentSessionFromServices. */
|
|
35
|
+
tools: string[];
|
|
36
|
+
/** Custom tools to pass to pi. Built-ins are selected by name in `tools`. */
|
|
37
|
+
customTools: ToolDefinition[];
|
|
38
|
+
/** Skill paths for DefaultResourceLoader.additionalSkillPaths. */
|
|
39
|
+
skillPaths: string[];
|
|
40
|
+
/** Short prompt addendum. Empty unless memory is enabled. */
|
|
41
|
+
memoryHint: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function resolveMemoryEnabled(mode: PiAgentSurfaceOptions["memory"]): Promise<boolean> {
|
|
45
|
+
if (mode === "on") return true;
|
|
46
|
+
if (mode === "off") return false;
|
|
47
|
+
return isMemoryEnabled();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the shared tool + skill surface for both pi front doors.
|
|
52
|
+
*
|
|
53
|
+
* Memory policy: if the user enabled memory (vault exists), memory is available.
|
|
54
|
+
* It is still a scoped retrieval capability, not a default first step for every
|
|
55
|
+
* prompt. If memory is absent, no memory skill/tool schemas are exposed at all.
|
|
56
|
+
*/
|
|
57
|
+
export async function buildPiAgentSurface(opts: PiAgentSurfaceOptions = {}): Promise<PiAgentSurface> {
|
|
58
|
+
const memoryEnabled = await resolveMemoryEnabled(opts.memory ?? "auto");
|
|
59
|
+
const includeWeb = opts.webTools ?? true;
|
|
60
|
+
const includeCoding = opts.codingTools ?? true;
|
|
61
|
+
|
|
62
|
+
const tools: string[] = [];
|
|
63
|
+
if (includeCoding) tools.push("read", "bash", "edit", "write", "grep", "find", "ls");
|
|
64
|
+
const customTools: ToolDefinition[] = [];
|
|
65
|
+
if (includeWeb) {
|
|
66
|
+
tools.push(...WEB_TOOL_NAMES);
|
|
67
|
+
customTools.push(...createWebTools());
|
|
68
|
+
}
|
|
69
|
+
if (memoryEnabled) {
|
|
70
|
+
tools.push(...MEMORY_TOOL_NAMES, ...REFERENCE_TOOL_NAMES);
|
|
71
|
+
customTools.push(...createMemoryTools(), ...createReferenceTools());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
memoryEnabled,
|
|
76
|
+
tools,
|
|
77
|
+
customTools,
|
|
78
|
+
skillPaths: materializeBundledSkillPaths({ includeMemory: memoryEnabled }),
|
|
79
|
+
memoryHint: memoryEnabled ? await memoryIndexHint() : "",
|
|
80
|
+
};
|
|
81
|
+
}
|
package/src/pi-terminal.ts
CHANGED
|
@@ -35,8 +35,7 @@ import {
|
|
|
35
35
|
runRpcMode,
|
|
36
36
|
type CreateAgentSessionRuntimeFactory,
|
|
37
37
|
} from "@earendil-works/pi-coding-agent";
|
|
38
|
-
import {
|
|
39
|
-
import { materializeBundledSkills } from "./web/skills";
|
|
38
|
+
import { buildPiAgentSurface } from "./pi-session";
|
|
40
39
|
import { buildPiProvider, DEFAULT_CONTEXT_WINDOW } from "./pi-provider";
|
|
41
40
|
|
|
42
41
|
/** How the embedded agent runs. Mirrors pi's CLI appMode. */
|
|
@@ -49,8 +48,6 @@ export type PiTerminalMode = "interactive" | "print" | "rpc";
|
|
|
49
48
|
* sees it. Mutating tools (bash/edit/write) surface pi's own TUI approval
|
|
50
49
|
* prompt at call time — we don't add a gate.
|
|
51
50
|
*/
|
|
52
|
-
const ALL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", ...WEB_TOOL_NAMES];
|
|
53
|
-
|
|
54
51
|
/**
|
|
55
52
|
* System prompt for the embedded terminal coding agent.
|
|
56
53
|
*
|
|
@@ -70,20 +67,11 @@ export function buildTerminalSystemPrompt(modelLabel?: string): string {
|
|
|
70
67
|
const model = modelLabel ? `the local model \`${modelLabel}\`` : "a local model";
|
|
71
68
|
return `You are mlx-bun's terminal coding agent — a capable, hands-on pair programmer working alongside the user in their terminal. You run on ${model} entirely on the user's own Apple-silicon Mac via mlx-bun, so the whole session stays local and private; nothing leaves the machine except the explicit web tools below.
|
|
72
69
|
|
|
73
|
-
You operate inside the user's current project directory
|
|
74
|
-
|
|
75
|
-
Tools used freely (no confirmation needed):
|
|
76
|
-
- read — open and read any file
|
|
77
|
-
- ls, find, grep — list directories and search the tree by name or content
|
|
78
|
-
- web_search — search the web for current, real-world information (docs, APIs, errors, anything that changes or that you don't already know)
|
|
79
|
-
- web_fetch — fetch a URL and read the page or data behind it (a natural follow-up to web_search, or when the user gives you a link)
|
|
80
|
-
- weather — current conditions and a short forecast for any place
|
|
70
|
+
You operate inside the user's current project directory. Available capabilities when relevant: read-only file inspection (read, ls, find, grep), web search/fetch (web_search, web_fetch), weather, and user-approved local actions (bash, edit, write).
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
- bash — run shell commands (build, test, git, scripts)
|
|
84
|
-
- edit, write — modify existing files or create new ones
|
|
72
|
+
Use tools in service of helping the user, not as a performance. For ordinary social conversation, answer naturally yourself. For real-world facts, current events, schedules, releases, local information, recommendations, restaurants, movies, prices, travel, docs, or anything likely to have changed, look it up with web_search/web_fetch or weather instead of relying on memory or guessing. For coding tasks, inspect the files, investigate before you answer, and follow through to a working result. When you need to change code or run something, propose the concrete edit or command and let the approval prompt handle consent — don't ask permission in prose first.
|
|
85
73
|
|
|
86
|
-
|
|
74
|
+
Environment metadata such as current date, working directory, model name, and available tools may be provided for context; do not report or summarize that metadata unless the user asks for it or it is directly relevant. Do not narrate tool policies or internal workflows.
|
|
87
75
|
|
|
88
76
|
Be honest about what you did and what you found, including mistakes, dead ends, and anything you couldn't do. Keep responses tight and skimmable: format with Markdown, show file paths and commands plainly, and cite links when you used the web.`;
|
|
89
77
|
}
|
|
@@ -121,6 +109,7 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
|
|
|
121
109
|
const cwd = process.cwd();
|
|
122
110
|
// Own session/settings dir, isolated from any standalone pi install.
|
|
123
111
|
const agentDir = join(homedir(), ".mlx-bun", "pi");
|
|
112
|
+
const sessionDir = join(agentDir, "sessions", `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`);
|
|
124
113
|
|
|
125
114
|
const { authStorage, modelRegistry, model } = buildPiProvider(opts.baseUrl, {
|
|
126
115
|
contextWindow: opts.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
@@ -128,10 +117,10 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
|
|
|
128
117
|
reasoning: opts.reasoning ?? false,
|
|
129
118
|
});
|
|
130
119
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
const
|
|
134
|
-
const systemPrompt = buildTerminalSystemPrompt(opts.modelLabel);
|
|
120
|
+
// Shared pi tool/skill surface. Memory is included only when the user has
|
|
121
|
+
// enabled a vault; if present it remains scoped to user-specific continuity.
|
|
122
|
+
const surface = await buildPiAgentSurface();
|
|
123
|
+
const systemPrompt = buildTerminalSystemPrompt(opts.modelLabel) + surface.memoryHint;
|
|
135
124
|
|
|
136
125
|
// The runtime factory: same shape as pi's own CLI (dist/main.js) and the
|
|
137
126
|
// shipped examples/sdk/13-session-runtime.ts. The same factory is reused by
|
|
@@ -154,7 +143,7 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
|
|
|
154
143
|
// AGENTS.md / CLAUDE.md context files (unlike the web chat).
|
|
155
144
|
noContextFiles: false,
|
|
156
145
|
// Our curated skills on top (noSkills above only disables discovery).
|
|
157
|
-
additionalSkillPaths:
|
|
146
|
+
additionalSkillPaths: surface.skillPaths,
|
|
158
147
|
// Replace pi's default coding-agent prompt with our mlx-bun persona.
|
|
159
148
|
systemPrompt,
|
|
160
149
|
},
|
|
@@ -165,15 +154,15 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
|
|
|
165
154
|
sessionManager,
|
|
166
155
|
sessionStartEvent,
|
|
167
156
|
model,
|
|
168
|
-
tools:
|
|
169
|
-
customTools:
|
|
157
|
+
tools: surface.tools,
|
|
158
|
+
customTools: surface.customTools,
|
|
170
159
|
})),
|
|
171
160
|
services,
|
|
172
161
|
diagnostics: services.diagnostics,
|
|
173
162
|
};
|
|
174
163
|
};
|
|
175
164
|
|
|
176
|
-
const sessionManager = SessionManager.create(cwd);
|
|
165
|
+
const sessionManager = SessionManager.create(cwd, sessionDir);
|
|
177
166
|
const runtime = await createAgentSessionRuntime(createRuntime, { cwd, agentDir, sessionManager });
|
|
178
167
|
|
|
179
168
|
try {
|