mlx-bun 0.0.13 → 0.2.0
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 +31 -21
- package/bin/mlx-bun.mjs +1 -1
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/colibri-glm52-port.md +1 -1
- package/docs/design/compat-cli-surface-design.md +1 -1
- package/docs/design/decode-speed-program.md +1 -1
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +1 -1
- package/docs/design/dspark-speculative-decoding.md +53 -3
- package/docs/design/grammar-spec-batching-integration.md +1 -1
- package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
- package/docs/design/orpo-dynamic-lambda.md +1 -1
- package/docs/design/orpo-future-enhancements.md +1 -1
- package/docs/design/orpo-training.md +2 -2
- package/docs/design/parity-tier-dag.md +1 -1
- package/docs/design/repo-cleanup-plan.md +46 -4
- package/docs/design/segmented-backward-training.md +2 -2
- package/docs/design/ssd-kv-cold-tier.md +127 -0
- package/docs/design/turboquant-weights.md +467 -0
- package/docs/design/w4a16-compute-precision-spike.md +1 -1
- package/docs/design/web-chat-redesign.md +5 -5
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/curve-sampler-research-plan.md +4 -4
- package/docs/planning/memory-docs-and-dag-plan.md +1 -1
- package/docs/planning/release-notes-v0.2.0.md +49 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/cli.md +15 -4
- package/docs/reference/distribution.md +1 -0
- package/docs/reference/features-matrix.md +3 -2
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +69 -14
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/cli.ts +131 -36
- package/src/eval/kl.ts +27 -20
- package/src/eval/runner.ts +23 -4
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/expert-trace.ts +3 -1
- package/src/fit.ts +24 -43
- package/src/flags.ts +7 -9
- package/src/generate.ts +107 -92
- package/src/grammar.ts +2 -1
- package/src/harness-pi.ts +1 -1
- package/src/index.ts +20 -0
- package/src/jobs/job-entry.ts +3 -2
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +370 -205
- package/src/media-fetch.ts +19 -3
- package/src/memory/model.ts +2 -1
- package/src/memory/vault.ts +2 -1
- package/src/memory-plan.ts +18 -0
- package/src/mlx/ffi.ts +8 -11
- package/src/mlx/ops.ts +31 -6
- package/src/model/batched-mask.ts +1 -1
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +43 -97
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +56 -105
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +3 -2
- package/src/model/factory.ts +44 -49
- package/src/model/gemma4-base.ts +163 -10
- package/src/model/generated/gemma4-12b.ts +50 -146
- package/src/model/generated/gemma4-26b.ts +32 -92
- package/src/model/generated/gemma4-e4b.ts +26 -74
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/minicpm5.ts +2 -1
- package/src/model/profile.ts +433 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5.ts +217 -25
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +13 -6
- package/src/pi-web.ts +6 -5
- package/src/prompt-cache.ts +2 -2
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +6 -0
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +102 -2
- package/src/runtime-config.ts +67 -0
- package/src/sampler.ts +272 -0
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +200 -87
- package/src/serve/completion-executor.ts +346 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +205 -0
- package/src/serve/generation-gateway.ts +116 -109
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +338 -0
- package/src/serve/model-admin-routes.ts +153 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +158 -0
- package/src/serve/static-routes.ts +101 -0
- package/src/server.ts +704 -1400
- package/src/spec/dflash-source.ts +2 -1
- package/src/spec/dspark/module-dflash.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +8 -0
- package/src/spec/qwen-mtp-source.ts +431 -0
- package/src/spec/serve-loop.ts +36 -68
- package/src/ssd-cache.ts +17 -14
- package/src/ssd-durability.ts +195 -0
- package/src/train/flash-cce.ts +12 -11
- package/src/train/forward.ts +32 -1
- package/src/train/loss.ts +2 -1
- package/src/train/segmented.ts +4 -3
- package/src/train/trainer.ts +3 -2
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +47 -1
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +513 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.js +10 -3
- package/src/web/src/chat.ts +12 -5
- package/src/weights.ts +81 -26
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
package/src/model/qwen3_5.ts
CHANGED
|
@@ -30,6 +30,10 @@ import {
|
|
|
30
30
|
type Mask,
|
|
31
31
|
} from "./gemma4-base";
|
|
32
32
|
import { gatedDeltaUpdate, SSMCache } from "./qwen3-delta";
|
|
33
|
+
import {
|
|
34
|
+
activeMrope, applyInterleavedRope, buildMropePositions, mropeInvFreq,
|
|
35
|
+
setActiveMrope, type MropeRequestState,
|
|
36
|
+
} from "./qwen3-mrope";
|
|
33
37
|
|
|
34
38
|
const PREFIX = "language_model";
|
|
35
39
|
|
|
@@ -146,30 +150,58 @@ export class GatedDeltaNet {
|
|
|
146
150
|
const [B, S] = x.shape as [number, number, number];
|
|
147
151
|
const convDim = this.keyDim * 2 + this.valueDim;
|
|
148
152
|
const nKeep = this.convKernel - 1;
|
|
153
|
+
const prof = (globalThis as Record<string, unknown>).__deltaProf as
|
|
154
|
+
Record<string, number>
|
|
155
|
+
| undefined;
|
|
156
|
+
|
|
157
|
+
// Armed speculative verify round (serve loop, serial lane): this forward
|
|
158
|
+
// must be rewindable. Snapshot = hand the REPLACED state slots to the
|
|
159
|
+
// round instead of disposing (free — arrays are immutable); record the
|
|
160
|
+
// position-local kernel inputs (qkv/a/b) and install the prefix replay.
|
|
161
|
+
const spec = cache.specRound;
|
|
162
|
+
if (spec) {
|
|
163
|
+
if (!spec.armed)
|
|
164
|
+
throw new Error("GatedDeltaNet: spec round already recorded this round");
|
|
165
|
+
spec.armed = false;
|
|
166
|
+
spec.S = S;
|
|
167
|
+
spec.replay = (c, keep) => this.#replaySpecPrefix(c, keep);
|
|
168
|
+
}
|
|
149
169
|
|
|
170
|
+
const t0 = prof ? performance.now() : 0;
|
|
150
171
|
const qkv = this.inProjQkv.forward(x); // [B,S,convDim]
|
|
151
172
|
let z = this.inProjZ.forward(x);
|
|
152
173
|
z = disposing(z, ops.reshape(z, [B, S, this.numVHeads, this.headVDim]));
|
|
153
174
|
const b = this.inProjB.forward(x); // [B,S,numVHeads]
|
|
154
|
-
const a = this.inProjA.forward(x);
|
|
175
|
+
const a = this.inProjA.forward(x);
|
|
176
|
+
if (prof) { ops.evalAll([qkv, z, b, a]); prof.proj = (prof.proj ?? 0) + performance.now() - t0; }
|
|
177
|
+
const tc = prof ? performance.now() : 0; // [B,S,numVHeads]
|
|
155
178
|
|
|
156
179
|
// Causal depthwise conv with the conv-state prefix (B=1: no ssm mask).
|
|
157
180
|
const convState =
|
|
158
181
|
cache.conv ?? ops.zeros([B, nKeep, convDim], x.dtype);
|
|
159
182
|
const convInput = ops.concatAxis([convState, qkv], 1); // [B,S+nKeep,convDim]
|
|
160
183
|
if (!cache.conv) convState.dispose();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
if (spec) spec.qkv = qkv;
|
|
185
|
+
else qkv.dispose();
|
|
186
|
+
// New conv state = last nKeep rows. MUST be a TRUE copy (mx.copy):
|
|
187
|
+
// this tail slice is already row-contiguous at B=1, so ops.contiguous
|
|
188
|
+
// is a no-op VIEW sharing convInput's buffer — caching it pinned the
|
|
189
|
+
// whole [B, S+nKeep, convDim] chunk buffer (~42 MB/layer/chunk, 48
|
|
190
|
+
// layers = the measured 2.0 GB-per-2048-chunk prefill leak; 46 GB
|
|
191
|
+
// active at 32k, async-GPU-OOM on 24 GB — M4 2026-08-20).
|
|
192
|
+
const newConvView = convInput.slice([0, S, 0], [B, S + nKeep, convDim]);
|
|
193
|
+
const newConv = ops.copyOf(newConvView);
|
|
194
|
+
newConvView.dispose();
|
|
195
|
+
if (spec) spec.prevConv = cache.conv;
|
|
196
|
+
else cache.conv?.dispose();
|
|
167
197
|
cache.conv = newConv;
|
|
168
198
|
|
|
169
199
|
const conv = ops.conv1d(convInput, this.convWeight, 1, 0, 1, convDim);
|
|
170
200
|
convInput.dispose();
|
|
171
201
|
const convOut = compiledSilu(conv); // nn.silu (mx.compile), matches mlx-lm
|
|
172
202
|
conv.dispose();
|
|
203
|
+
if (prof) { ops.evalAll([convOut]); prof.conv = (prof.conv ?? 0) + performance.now() - tc; }
|
|
204
|
+
const tn = prof ? performance.now() : 0;
|
|
173
205
|
|
|
174
206
|
const [qFlat, kFlat, vFlat] = ops.split(
|
|
175
207
|
convOut, [this.keyDim, 2 * this.keyDim], -1,
|
|
@@ -187,16 +219,26 @@ export class GatedDeltaNet {
|
|
|
187
219
|
q = disposing(q, ops.mulScalar(q, invScale * invScale));
|
|
188
220
|
k = disposing(k, ops.rmsNorm(k, null, 1e-6));
|
|
189
221
|
k = disposing(k, ops.mulScalar(k, invScale));
|
|
222
|
+
if (prof) { ops.evalAll([q, k]); prof.norms = (prof.norms ?? 0) + performance.now() - tn; }
|
|
223
|
+
const tk = prof ? performance.now() : 0;
|
|
190
224
|
|
|
191
225
|
const [out, newState] = gatedDeltaUpdate(
|
|
192
226
|
q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent,
|
|
193
227
|
);
|
|
228
|
+
if (prof) { ops.evalAll([out]); prof.kernel = (prof.kernel ?? 0) + performance.now() - tk; }
|
|
229
|
+
const to = prof ? performance.now() : 0;
|
|
194
230
|
q.dispose();
|
|
195
231
|
k.dispose();
|
|
196
232
|
v.dispose();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
233
|
+
if (spec) {
|
|
234
|
+
spec.a = a;
|
|
235
|
+
spec.b = b;
|
|
236
|
+
spec.prevRecurrent = cache.recurrent;
|
|
237
|
+
} else {
|
|
238
|
+
a.dispose();
|
|
239
|
+
b.dispose();
|
|
240
|
+
cache.recurrent?.dispose();
|
|
241
|
+
}
|
|
200
242
|
cache.recurrent = newState;
|
|
201
243
|
cache.advance(S);
|
|
202
244
|
|
|
@@ -208,9 +250,80 @@ export class GatedDeltaNet {
|
|
|
208
250
|
gated.dispose();
|
|
209
251
|
const result = this.outProj.forward(merged);
|
|
210
252
|
merged.dispose();
|
|
253
|
+
if (prof) { ops.evalAll([result]); prof.out = (prof.out ?? 0) + performance.now() - to; }
|
|
211
254
|
return result;
|
|
212
255
|
}
|
|
213
256
|
|
|
257
|
+
/** Speculative rollback replay: with the pre-round snapshot RESTORED onto
|
|
258
|
+
* `cache`, re-advance the first `keep` window tokens from the recorded
|
|
259
|
+
* position-local inputs. Every op mirrors forward() on the same values —
|
|
260
|
+
* conv windows, silu, per-position norms, and the kernel's serial prefix
|
|
261
|
+
* are all independent of the rejected tail — so the resulting conv +
|
|
262
|
+
* recurrent state is BIT-EXACTLY what a forward over only the accepted
|
|
263
|
+
* prefix would have produced. Output projections (z / out_proj) are state-
|
|
264
|
+
* free and skipped: only the states matter here. */
|
|
265
|
+
#replaySpecPrefix(cache: SSMCache, keep: number): void {
|
|
266
|
+
const r = cache.specRound;
|
|
267
|
+
if (!r || !r.qkv || !r.a || !r.b) throw new Error("GatedDeltaNet replay without recorded round inputs");
|
|
268
|
+
const B = r.qkv.shape[0]!;
|
|
269
|
+
const convDim = this.keyDim * 2 + this.valueDim;
|
|
270
|
+
const nKeep = this.convKernel - 1;
|
|
271
|
+
|
|
272
|
+
const qkvPfxView = r.qkv.slice([0, 0, 0], [B, keep, convDim]);
|
|
273
|
+
const qkvPfx = ops.contiguous(qkvPfxView);
|
|
274
|
+
qkvPfxView.dispose();
|
|
275
|
+
const convState = cache.conv ?? ops.zeros([B, nKeep, convDim], qkvPfx.dtype);
|
|
276
|
+
const convInput = ops.concatAxis([convState, qkvPfx], 1);
|
|
277
|
+
if (!cache.conv) convState.dispose();
|
|
278
|
+
qkvPfx.dispose();
|
|
279
|
+
// TRUE copy, same reason as forward(): the contiguous tail slice would
|
|
280
|
+
// otherwise be a view pinning the whole replay convInput buffer.
|
|
281
|
+
const rConvView = convInput.slice([0, keep, 0], [B, keep + nKeep, convDim]);
|
|
282
|
+
const newConv = ops.copyOf(rConvView);
|
|
283
|
+
rConvView.dispose();
|
|
284
|
+
cache.conv?.dispose();
|
|
285
|
+
cache.conv = newConv;
|
|
286
|
+
|
|
287
|
+
const conv = ops.conv1d(convInput, this.convWeight, 1, 0, 1, convDim);
|
|
288
|
+
convInput.dispose();
|
|
289
|
+
const convOut = compiledSilu(conv);
|
|
290
|
+
conv.dispose();
|
|
291
|
+
const [qFlat, kFlat, vFlat] = ops.split(
|
|
292
|
+
convOut, [this.keyDim, 2 * this.keyDim], -1,
|
|
293
|
+
) as [MlxArray, MlxArray, MlxArray];
|
|
294
|
+
convOut.dispose();
|
|
295
|
+
let q = ops.reshape(qFlat, [B, keep, this.numKHeads, this.headKDim]);
|
|
296
|
+
qFlat.dispose();
|
|
297
|
+
let k = ops.reshape(kFlat, [B, keep, this.numKHeads, this.headKDim]);
|
|
298
|
+
kFlat.dispose();
|
|
299
|
+
const v = disposing(vFlat, ops.reshape(vFlat, [B, keep, this.numVHeads, this.headVDim]));
|
|
300
|
+
const invScale = Math.pow(this.headKDim, -0.5);
|
|
301
|
+
q = disposing(q, ops.rmsNorm(q, null, 1e-6));
|
|
302
|
+
q = disposing(q, ops.mulScalar(q, invScale * invScale));
|
|
303
|
+
k = disposing(k, ops.rmsNorm(k, null, 1e-6));
|
|
304
|
+
k = disposing(k, ops.mulScalar(k, invScale));
|
|
305
|
+
|
|
306
|
+
const aPfxView = r.a.slice([0, 0, 0], [B, keep, this.numVHeads]);
|
|
307
|
+
const aPfx = ops.contiguous(aPfxView);
|
|
308
|
+
aPfxView.dispose();
|
|
309
|
+
const bPfxView = r.b.slice([0, 0, 0], [B, keep, this.numVHeads]);
|
|
310
|
+
const bPfx = ops.contiguous(bPfxView);
|
|
311
|
+
bPfxView.dispose();
|
|
312
|
+
|
|
313
|
+
const [y, newState] = gatedDeltaUpdate(
|
|
314
|
+
q, k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent,
|
|
315
|
+
);
|
|
316
|
+
q.dispose();
|
|
317
|
+
k.dispose();
|
|
318
|
+
v.dispose();
|
|
319
|
+
aPfx.dispose();
|
|
320
|
+
bPfx.dispose();
|
|
321
|
+
y.dispose(); // only the state advance matters on the rollback path
|
|
322
|
+
cache.recurrent?.dispose();
|
|
323
|
+
cache.recurrent = newState;
|
|
324
|
+
cache.advance(keep);
|
|
325
|
+
}
|
|
326
|
+
|
|
214
327
|
private rmsNormGated(hidden: MlxArray, gate: MlxArray): MlxArray {
|
|
215
328
|
const xn = ops.rmsNorm(hidden, this.normWeight, this.eps); // bf16
|
|
216
329
|
// Oracle: _precise_swiglu(hidden, gate, xn) — ALWAYS one @mx.compile kernel
|
|
@@ -279,13 +392,23 @@ export class Qwen3Attention {
|
|
|
279
392
|
// position as ropeOffsetArr (rows have different prompt lengths); the
|
|
280
393
|
// dynamic-offset kernel is the same fast::rope, bit-exact vs the static
|
|
281
394
|
// form (tests/compile.test.ts). Serial lane: scalar offset, unchanged.
|
|
395
|
+
// Vision requests (serial lane only) install activeMrope for the current
|
|
396
|
+
// forward: 3D interleaved positions via the manual apply — the reference
|
|
397
|
+
// never uses the fused fast-rope kernel when position_ids are supplied,
|
|
398
|
+
// so this IS the oracle's own arithmetic. Text-only stays on ops.rope.
|
|
399
|
+
const mr = activeMrope;
|
|
282
400
|
const offArr = (cache as { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
401
|
+
if (mr) {
|
|
402
|
+
q = disposing(q, applyInterleavedRope(q, mr));
|
|
403
|
+
k = disposing(k, applyInterleavedRope(k, mr));
|
|
404
|
+
} else {
|
|
405
|
+
q = disposing(q, offArr
|
|
406
|
+
? ops.ropeDynamic(q, this.ropeDims, this.ropeBase, offArr, null)
|
|
407
|
+
: ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
|
|
408
|
+
k = disposing(k, offArr
|
|
409
|
+
? ops.ropeDynamic(k, this.ropeDims, this.ropeBase, offArr, null)
|
|
410
|
+
: ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
|
|
411
|
+
}
|
|
289
412
|
|
|
290
413
|
let attn: MlxArray;
|
|
291
414
|
if (cache instanceof QuantizedKVCache) {
|
|
@@ -448,8 +571,42 @@ export class Qwen35Model {
|
|
|
448
571
|
return this.forwardLayers(h, cache);
|
|
449
572
|
}
|
|
450
573
|
|
|
451
|
-
|
|
452
|
-
|
|
574
|
+
/** Active vision mRoPE request state (serial lane; set by the generation
|
|
575
|
+
* gateway around a vision request's run, null for text-only — which keeps
|
|
576
|
+
* the bit-exact fast-rope path). While set, forwardLayers installs the
|
|
577
|
+
* per-forward interleaved cos/sin consumed by every full-attn layer. */
|
|
578
|
+
mrope: MropeRequestState | null = null;
|
|
579
|
+
#mropeInvFreq: MlxArray | null = null;
|
|
580
|
+
|
|
581
|
+
/** Vision prefill: spliced input embeddings [1, L, H] (image features
|
|
582
|
+
* overwriting image-token rows — the caller builds them). Caller keeps
|
|
583
|
+
* ownership of `embeds`. bidir/ids/multimodal are the Gemma-shaped extras
|
|
584
|
+
* and are unused: Qwen3.5 vision attends fully causally. */
|
|
585
|
+
forwardEmbeddings(
|
|
586
|
+
embeds: MlxArray, cache: Cache[], _bidir: MlxArray | null,
|
|
587
|
+
_ids: MlxArray | null = null, _multimodal: MlxArray | null = null,
|
|
588
|
+
): MlxArray {
|
|
589
|
+
const h = ops.contiguous(embeds); // fresh handle — the layer loop consumes it
|
|
590
|
+
return this.forwardLayers(h, cache);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Layer-output tap (same contract as Gemma4Model.hiddenTap): the serve
|
|
594
|
+
* loop's forwardMaybeTap sets it around prefill/verify forwards so
|
|
595
|
+
* KV-borrowing draft sources (native Qwen MTP) can read PRE-final-norm
|
|
596
|
+
* hiddens — layer 63's output stream, what mlx-vlm captures with
|
|
597
|
+
* skip_final_norm=True. No-op (and graph-identical) when null. */
|
|
598
|
+
hiddenTap: { layers: Set<number>; pos?: number; captured: Map<number, MlxArray> } | null = null;
|
|
599
|
+
|
|
600
|
+
/** Store layer `i`'s residual stream ([1,L,H]) if the tap requests it.
|
|
601
|
+
* Copies so the loop's h.dispose() can't free the capture. */
|
|
602
|
+
protected captureLayer(i: number, h: MlxArray): void {
|
|
603
|
+
const tap = this.hiddenTap;
|
|
604
|
+
if (!tap || !tap.layers.has(i)) return;
|
|
605
|
+
const H = h.shape[2]!;
|
|
606
|
+
const src = tap.pos !== undefined ? h.slice([0, tap.pos, 0], [1, tap.pos + 1, H]) : h;
|
|
607
|
+
const copy = ops.contiguous(src);
|
|
608
|
+
if (src !== h) src.dispose();
|
|
609
|
+
tap.captured.set(i, copy);
|
|
453
610
|
}
|
|
454
611
|
|
|
455
612
|
protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
|
|
@@ -457,14 +614,49 @@ export class Qwen35Model {
|
|
|
457
614
|
// One full-attention mask shared by all full layers (same offset); linear
|
|
458
615
|
// layers see no ssm mask at B=1.
|
|
459
616
|
const faMask = cache[this.faIdx]!.makeMask(L, null);
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
617
|
+
// Vision requests: one interleaved-mRoPE cos/sin table per forward,
|
|
618
|
+
// shared by all 12 full-attention layers (positions are layer-invariant).
|
|
619
|
+
let mropeFwd: ReturnType<typeof buildMropePositions> | null = null;
|
|
620
|
+
if (this.mrope) {
|
|
621
|
+
const t = this.config.text;
|
|
622
|
+
const ropeDims = Math.trunc(t.headDim * t.partialRotaryFactor);
|
|
623
|
+
const base = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
|
|
624
|
+
this.#mropeInvFreq ??= mropeInvFreq(ropeDims, base);
|
|
625
|
+
mropeFwd = buildMropePositions(
|
|
626
|
+
this.mrope, cache[this.faIdx]!.offset, L, this.#mropeInvFreq, ropeDims,
|
|
627
|
+
);
|
|
628
|
+
setActiveMrope(mropeFwd);
|
|
629
|
+
}
|
|
630
|
+
let h: MlxArray | null = h0;
|
|
631
|
+
const prof = (globalThis as Record<string, unknown>).__deltaProf as
|
|
632
|
+
Record<string, number>
|
|
633
|
+
| undefined;
|
|
634
|
+
try {
|
|
635
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
636
|
+
const tl = prof ? performance.now() : 0;
|
|
637
|
+
const next = this.layers[i]!.forward(h, faMask, cache[i]!);
|
|
638
|
+
h.dispose();
|
|
639
|
+
h = next;
|
|
640
|
+
if (prof) {
|
|
641
|
+
ops.evalAll([h]);
|
|
642
|
+
const key = this.layers[i]!.isLinear ? "layerLin" : "layerFull";
|
|
643
|
+
prof[key] = (prof[key] ?? 0) + performance.now() - tl;
|
|
644
|
+
}
|
|
645
|
+
this.captureLayer(i, h); // native-MTP pre-final-norm tap (no-op unless set)
|
|
646
|
+
}
|
|
647
|
+
const out = disposing(h, this.finalNorm.forward(h));
|
|
648
|
+
h = null; // consumed — the finally must not double-free
|
|
649
|
+
return out;
|
|
650
|
+
} finally {
|
|
651
|
+
if (mropeFwd) {
|
|
652
|
+
setActiveMrope(null);
|
|
653
|
+
mropeFwd.posIds.dispose();
|
|
654
|
+
}
|
|
655
|
+
// A mid-loop layer throw must not strand the mask array or the
|
|
656
|
+
// in-flight [1,L,H] residual (2026-08-18 review).
|
|
657
|
+
faMask.arr?.dispose();
|
|
658
|
+
h?.dispose();
|
|
465
659
|
}
|
|
466
|
-
faMask.arr?.dispose();
|
|
467
|
-
return disposing(h, this.finalNorm.forward(h));
|
|
468
660
|
}
|
|
469
661
|
|
|
470
662
|
logitsFromHidden(h: MlxArray): MlxArray {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// AVFoundation frame-extraction sidecar probe (PLAN 14w) — the video
|
|
2
|
+
// equivalent of the audio path's afconvert doctrine: macOS-native codecs,
|
|
3
|
+
// zero vendored dependencies.
|
|
4
|
+
//
|
|
5
|
+
// frame-extract <video> <outDir> [fps=2] [maxFrames=768]
|
|
6
|
+
// → writes frame-%04d.png (RGB) + prints "N <count> W <w> H <h>"
|
|
7
|
+
// frame-extract --make-fixture <out.mov>
|
|
8
|
+
// → writes the deterministic 6-frame 320x240 gradient test clip
|
|
9
|
+
//
|
|
10
|
+
// Sampling mirrors mlx-vlm's do_sample_frames: timestamps k/fps for
|
|
11
|
+
// k = 0..min(ceil(duration*fps), maxFrames)-1, precise tolerance.
|
|
12
|
+
|
|
13
|
+
import AVFoundation
|
|
14
|
+
import Foundation
|
|
15
|
+
import ImageIO
|
|
16
|
+
import UniformTypeIdentifiers
|
|
17
|
+
|
|
18
|
+
func die(_ msg: String) -> Never {
|
|
19
|
+
FileHandle.standardError.write((msg + "\n").data(using: .utf8)!)
|
|
20
|
+
exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func makeFixture(_ path: String) {
|
|
24
|
+
let url = URL(fileURLWithPath: path)
|
|
25
|
+
try? FileManager.default.removeItem(at: url)
|
|
26
|
+
guard let writer = try? AVAssetWriter(outputURL: url, fileType: .mov) else {
|
|
27
|
+
die("AVAssetWriter init failed")
|
|
28
|
+
}
|
|
29
|
+
let W = 320, H = 240, FRAMES = 6
|
|
30
|
+
let settings: [String: Any] = [
|
|
31
|
+
AVVideoCodecKey: AVVideoCodecType.h264,
|
|
32
|
+
AVVideoWidthKey: W,
|
|
33
|
+
AVVideoHeightKey: H,
|
|
34
|
+
]
|
|
35
|
+
let input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
|
|
36
|
+
let adaptor = AVAssetWriterInputPixelBufferAdaptor(
|
|
37
|
+
assetWriterInput: input,
|
|
38
|
+
sourcePixelBufferAttributes: [
|
|
39
|
+
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB,
|
|
40
|
+
kCVPixelBufferWidthKey as String: W,
|
|
41
|
+
kCVPixelBufferHeightKey as String: H,
|
|
42
|
+
])
|
|
43
|
+
writer.add(input)
|
|
44
|
+
writer.startWriting()
|
|
45
|
+
writer.startSession(atSourceTime: .zero)
|
|
46
|
+
for f in 0..<FRAMES {
|
|
47
|
+
while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 0.01) }
|
|
48
|
+
var pb: CVPixelBuffer?
|
|
49
|
+
CVPixelBufferCreate(nil, W, H, kCVPixelFormatType_32ARGB, nil, &pb)
|
|
50
|
+
guard let pixelBuffer = pb else { die("CVPixelBufferCreate failed") }
|
|
51
|
+
CVPixelBufferLockBaseAddress(pixelBuffer, [])
|
|
52
|
+
let base = CVPixelBufferGetBaseAddress(pixelBuffer)!
|
|
53
|
+
let bpr = CVPixelBufferGetBytesPerRow(pixelBuffer)
|
|
54
|
+
let p = base.assumingMemoryBound(to: UInt8.self)
|
|
55
|
+
for y in 0..<H {
|
|
56
|
+
for x in 0..<W {
|
|
57
|
+
let o = y * bpr + x * 4
|
|
58
|
+
p[o] = 255
|
|
59
|
+
p[o + 1] = UInt8((x * 255 / W + f * 20) % 256)
|
|
60
|
+
p[o + 2] = UInt8((y * 255 / H + f * 10) % 256)
|
|
61
|
+
p[o + 3] = UInt8(((x + y) * 255 / (W + H) + f * 30) % 256)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
|
|
65
|
+
adaptor.append(pixelBuffer, withPresentationTime: CMTime(value: CMTimeValue(f), timescale: 2))
|
|
66
|
+
}
|
|
67
|
+
input.markAsFinished()
|
|
68
|
+
let sem = DispatchSemaphore(value: 0)
|
|
69
|
+
writer.finishWriting { sem.signal() }
|
|
70
|
+
sem.wait()
|
|
71
|
+
guard writer.status == .completed else { die("finishWriting status \(writer.status.rawValue)") }
|
|
72
|
+
print("fixture \(path)")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let args = CommandLine.arguments
|
|
76
|
+
if args.count >= 3 && args[1] == "--make-fixture" {
|
|
77
|
+
makeFixture(args[2])
|
|
78
|
+
exit(0)
|
|
79
|
+
}
|
|
80
|
+
guard args.count >= 3 else { die("usage: frame-extract <video> <outDir> [fps] [maxFrames]") }
|
|
81
|
+
let videoPath = args[1]
|
|
82
|
+
let outDir = args[2]
|
|
83
|
+
let fps = args.count > 3 ? Double(args[3]) ?? 2.0 : 2.0
|
|
84
|
+
let maxFrames = args.count > 4 ? Int(args[4]) ?? 768 : 768
|
|
85
|
+
|
|
86
|
+
let asset = AVURLAsset(url: URL(fileURLWithPath: videoPath))
|
|
87
|
+
let sem = DispatchSemaphore(value: 0)
|
|
88
|
+
var duration = CMTime.zero
|
|
89
|
+
var loadError: Error?
|
|
90
|
+
Task {
|
|
91
|
+
do { duration = try await asset.load(.duration) } catch { loadError = error }
|
|
92
|
+
sem.signal()
|
|
93
|
+
}
|
|
94
|
+
sem.wait()
|
|
95
|
+
if let e = loadError { die("asset load failed: \(e.localizedDescription)") }
|
|
96
|
+
// Fragmented/malformed containers can load with an indefinite duration —
|
|
97
|
+
// CMTimeGetSeconds yields NaN and Int(NaN) is a Swift runtime TRAP. Fall
|
|
98
|
+
// back to the frame cap; the extraction loop below already stops at the
|
|
99
|
+
// first undecodable timestamp.
|
|
100
|
+
let seconds = CMTimeGetSeconds(duration)
|
|
101
|
+
let count = seconds.isFinite
|
|
102
|
+
? max(1, min(Int((seconds * fps).rounded(.up)), maxFrames))
|
|
103
|
+
: maxFrames
|
|
104
|
+
|
|
105
|
+
let gen = AVAssetImageGenerator(asset: asset)
|
|
106
|
+
gen.appliesPreferredTrackTransform = true
|
|
107
|
+
gen.requestedTimeToleranceBefore = .zero
|
|
108
|
+
gen.requestedTimeToleranceAfter = .zero
|
|
109
|
+
// Bound DECODED memory at the source: the language pipeline's video pixel
|
|
110
|
+
// budget always downscales frames far below 1024px anyway (max_pixels
|
|
111
|
+
// 32·32·768 across t·h·w), so capping the longest edge here loses nothing
|
|
112
|
+
// it would have kept while keeping a 4K clip from materializing ~24 MB per
|
|
113
|
+
// frame (768 frames ≈ 17.8 GiB) before preprocessing.
|
|
114
|
+
gen.maximumSize = CGSize(width: 1024, height: 1024)
|
|
115
|
+
|
|
116
|
+
try? FileManager.default.createDirectory(
|
|
117
|
+
atPath: outDir, withIntermediateDirectories: true)
|
|
118
|
+
var written = 0
|
|
119
|
+
var lastW = 0, lastH = 0
|
|
120
|
+
for k in 0..<count {
|
|
121
|
+
let t = CMTime(seconds: Double(k) / fps, preferredTimescale: 600)
|
|
122
|
+
let img: CGImage
|
|
123
|
+
do {
|
|
124
|
+
img = try gen.copyCGImage(at: t, actualTime: nil)
|
|
125
|
+
} catch {
|
|
126
|
+
// Past-end sampling on short clips: stop at the last decodable frame.
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
lastW = img.width
|
|
130
|
+
lastH = img.height
|
|
131
|
+
let out = URL(fileURLWithPath: String(format: "%@/frame-%04d.png", outDir, k))
|
|
132
|
+
guard let dest = CGImageDestinationCreateWithURL(
|
|
133
|
+
out as CFURL, UTType.png.identifier as CFString, 1, nil)
|
|
134
|
+
else { die("png destination failed") }
|
|
135
|
+
CGImageDestinationAddImage(dest, img, nil)
|
|
136
|
+
guard CGImageDestinationFinalize(dest) else { die("png write failed") }
|
|
137
|
+
written += 1
|
|
138
|
+
}
|
|
139
|
+
guard written > 0 else { die("no frames decodable from \(videoPath)") }
|
|
140
|
+
print("N \(written) W \(lastW) H \(lastH)")
|
package/src/native-pack.ts
CHANGED
|
@@ -15,18 +15,24 @@ import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
|
15
15
|
import { homedir, tmpdir } from "node:os";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { downloadOne } from "./download";
|
|
18
|
+
import { runtimeValue } from "./runtime-config";
|
|
18
19
|
|
|
19
|
-
export const NATIVE_PACK_VERSION = "0.
|
|
20
|
+
export const NATIVE_PACK_VERSION = "0.3.0";
|
|
20
21
|
export const NATIVE_PACK_FILES = [
|
|
21
22
|
"libmlxc.dylib", "libmlx.dylib", "libjaccl.dylib", "mlx.metallib",
|
|
22
|
-
"libmlx_bun_expert_io.dylib",
|
|
23
|
+
"libmlx_bun_expert_io.dylib", "mlx-bun-frame-extract",
|
|
23
24
|
] as const;
|
|
24
25
|
|
|
26
|
+
// RELEASE-TIME BAKE: scripts/build-native-pack.sh prints the sha256/size for
|
|
27
|
+
// the freshly built tarball — publish `native-v0.3.0` with both assets and
|
|
28
|
+
// paste the values here BEFORE tagging the package release (the download
|
|
29
|
+
// 404s until the GitHub release exists; dev trees resolve via homebrew/env
|
|
30
|
+
// and never hit this path).
|
|
25
31
|
const SHA256: Record<string, string> = {
|
|
26
|
-
arm64: "
|
|
32
|
+
arm64: "b485fed858971ea6f072f349a0234c0e2d5030d3ef2ddfa942c8975eca2d3b2e",
|
|
27
33
|
};
|
|
28
34
|
const SIZE: Record<string, number> = {
|
|
29
|
-
arm64:
|
|
35
|
+
arm64: 52299265,
|
|
30
36
|
};
|
|
31
37
|
|
|
32
38
|
export function nativePackName(arch = process.arch): string {
|
|
@@ -37,7 +43,7 @@ export function nativePackName(arch = process.arch): string {
|
|
|
37
43
|
* the repo is private the download needs a token (GITHUB_TOKEN or
|
|
38
44
|
* `gh auth token`); once public it works anonymously. */
|
|
39
45
|
export function nativePackUrl(arch = process.arch): string {
|
|
40
|
-
return
|
|
46
|
+
return runtimeValue("MLX_BUN_NATIVE_PACK_URL") ??
|
|
41
47
|
`https://github.com/joshuarossi/mlx-bun/releases/download/native-v${NATIVE_PACK_VERSION}/${nativePackName(arch)}`;
|
|
42
48
|
}
|
|
43
49
|
|
|
@@ -48,7 +54,8 @@ export function nativePackDir(arch = process.arch): string {
|
|
|
48
54
|
/** Where the native runtime currently resolves from, or null if absent
|
|
49
55
|
* everywhere (= a fresh machine that needs the pack). */
|
|
50
56
|
export function nativeRuntimeDir(): string | null {
|
|
51
|
-
|
|
57
|
+
const explicit = runtimeValue("MLX_BUN_LIBMLXC");
|
|
58
|
+
if (explicit) return dirname(explicit);
|
|
52
59
|
const candidates = [
|
|
53
60
|
dirname(process.execPath),
|
|
54
61
|
nativePackDir(),
|
package/src/pi-web.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { createHash } from "node:crypto";
|
|
|
27
27
|
import { mkdirSync, rmSync } from "node:fs";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
import { join, resolve } from "node:path";
|
|
30
|
+
import { runtimeValue } from "./runtime-config";
|
|
30
31
|
import type { ServerWebSocket, WebSocketHandler } from "bun";
|
|
31
32
|
import {
|
|
32
33
|
createAgentSessionFromServices,
|
|
@@ -1394,7 +1395,7 @@ class PiWebSession {
|
|
|
1394
1395
|
(d) => d.state === "active" && d.repoId !== this.opts.modelId,
|
|
1395
1396
|
)?.repoId ?? null,
|
|
1396
1397
|
}, { hasTools: WELCOME_TOOLS.length > 0 }) + surface.memoryHint;
|
|
1397
|
-
if (
|
|
1398
|
+
if (runtimeValue("MLX_BUN_PI_DEBUG")) {
|
|
1398
1399
|
console.error(`[pi-web] prompt ${WEB_CHAT_PROMPT_VERSION} sha=${webChatPromptFingerprint(webPrompt)} memory=${surface.memoryEnabled ? "on" : "off"} codingTools=${this.codingToolsActive ? "on" : "off"}`);
|
|
1399
1400
|
}
|
|
1400
1401
|
const services = await createAgentSessionServices({
|
|
@@ -1826,7 +1827,7 @@ class PiWebSession {
|
|
|
1826
1827
|
|
|
1827
1828
|
pi.on("tool_call", (event: ToolCallEvent) => {
|
|
1828
1829
|
const block = decideBeforeToolCall(this.loopHygiene, event.toolName, event.input);
|
|
1829
|
-
if (block &&
|
|
1830
|
+
if (block && runtimeValue("MLX_BUN_PI_DEBUG")) {
|
|
1830
1831
|
console.error(`[pi-web] loop-hygiene blocked ${event.toolName}: ${block.reason.split("\n")[0]}`);
|
|
1831
1832
|
}
|
|
1832
1833
|
return block ? { block: true, reason: block.reason } : undefined;
|
|
@@ -1852,7 +1853,7 @@ class PiWebSession {
|
|
|
1852
1853
|
private installApprovalGate(pi: ExtensionAPI): void {
|
|
1853
1854
|
pi.on("tool_call", async (event: ToolCallEvent) => {
|
|
1854
1855
|
const tool = event.toolName;
|
|
1855
|
-
if (
|
|
1856
|
+
if (runtimeValue("MLX_BUN_PI_DEBUG")) {
|
|
1856
1857
|
console.error(`[pi-web] tool_call ${tool} args=${JSON.stringify(event.input)}`);
|
|
1857
1858
|
}
|
|
1858
1859
|
|
|
@@ -1945,7 +1946,7 @@ class PiWebSession {
|
|
|
1945
1946
|
}
|
|
1946
1947
|
|
|
1947
1948
|
private onSessionEvent(event: AgentSessionEvent): void {
|
|
1948
|
-
if (
|
|
1949
|
+
if (runtimeValue("MLX_BUN_PI_DEBUG")) {
|
|
1949
1950
|
const extra = event.type === "message_update"
|
|
1950
1951
|
? `/${(event as { assistantMessageEvent?: { type?: string } }).assistantMessageEvent?.type}` : "";
|
|
1951
1952
|
console.error(`[pi-event] ${event.type}${extra}`);
|
|
@@ -2025,7 +2026,7 @@ class PiWebSession {
|
|
|
2025
2026
|
// isStreaming atomically inside prompt(), so this read can't strand a
|
|
2026
2027
|
// message at the turn boundary.
|
|
2027
2028
|
case "prompt": {
|
|
2028
|
-
if (
|
|
2029
|
+
if (runtimeValue("MLX_BUN_PI_DEBUG")) {
|
|
2029
2030
|
console.error(`[pi-web] prompt text=${JSON.stringify(msg.text.slice(0, 500))} images=${msg.images?.length ?? 0} streaming=${session.isStreaming} adapter=${this.selectedAdapter ?? "base"}`);
|
|
2030
2031
|
}
|
|
2031
2032
|
const images = toPiImages(msg.images);
|
package/src/prompt-cache.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// reachable); onPut lets the server schedule its debounced write-behind
|
|
25
25
|
// snapshot for both lanes.
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import type { Cache } from "./model/gemma4";
|
|
28
28
|
import { cloneKvCaches } from "./kv-store";
|
|
29
29
|
|
|
30
30
|
/** Reference-counted release: wraps an entry's `retain` (e.g. an mmap
|
|
@@ -112,7 +112,7 @@ export function cacheBytes(caches: Cache[]): number {
|
|
|
112
112
|
// views per call (kv-store.ts snapshotCache/cloneKvCaches contract) —
|
|
113
113
|
// those must be disposed here or they leak (see generate.ts
|
|
114
114
|
// evalCacheState for the same hazard on the prefill path).
|
|
115
|
-
if (c
|
|
115
|
+
if (c.stateNeedsDispose) for (const a of state) a.dispose();
|
|
116
116
|
}
|
|
117
117
|
return total;
|
|
118
118
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
rmdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
/** Build a complete directory beside its destination, then publish it with a
|
|
14
|
+
* single same-filesystem rename. Failed writers leave no partial destination. */
|
|
15
|
+
export async function writeAtomicDirectory<T>(
|
|
16
|
+
outDir: string,
|
|
17
|
+
write: (stagingDir: string) => Promise<T>,
|
|
18
|
+
): Promise<T> {
|
|
19
|
+
const destination = resolve(outDir);
|
|
20
|
+
const replaceEmptyDestination = existsSync(destination);
|
|
21
|
+
if (
|
|
22
|
+
replaceEmptyDestination &&
|
|
23
|
+
(!statSync(destination).isDirectory() || readdirSync(destination).length !== 0)
|
|
24
|
+
) throw new Error(`output directory already exists: ${outDir}`);
|
|
25
|
+
|
|
26
|
+
const parent = dirname(destination);
|
|
27
|
+
mkdirSync(parent, { recursive: true });
|
|
28
|
+
const staging = mkdtempSync(join(parent, `.${basename(destination)}.tmp-`));
|
|
29
|
+
try {
|
|
30
|
+
const result = await write(staging);
|
|
31
|
+
if (replaceEmptyDestination) rmdirSync(destination);
|
|
32
|
+
renameSync(staging, destination);
|
|
33
|
+
return result;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
rmSync(staging, { recursive: true, force: true });
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { existsSync } from "node:fs";
|
|
22
22
|
import type { LoadedTokenizer } from "../tokenizer";
|
|
23
|
+
import { runtimeValue } from "../runtime-config";
|
|
23
24
|
|
|
24
25
|
/** One JSONL calibration sample (subset of the OptIQ schema we can consume). */
|
|
25
26
|
interface CalibrationSample {
|
|
@@ -34,7 +35,7 @@ function bundledMixCandidates(): string[] {
|
|
|
34
35
|
const home = process.env.HOME ?? "";
|
|
35
36
|
const rel = "lib/python3.14/site-packages/optiq/calibration/data/optiq.jsonl";
|
|
36
37
|
return [
|
|
37
|
-
|
|
38
|
+
runtimeValue("MLX_BUN_CALIBRATION_JSONL"),
|
|
38
39
|
`${home}/Code/mlx-lm-example/.venv/${rel}`,
|
|
39
40
|
`${home}/Code/mlx-lm/.venv/${rel}`,
|
|
40
41
|
].filter((p): p is string => !!p);
|