mlx-bun 0.0.12 → 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 +40 -24
- 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/archive/planning/release-notes-v0.0.13.md +77 -0
- 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 +8 -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 -1
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +86 -19
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/chat-template.ts +34 -1
- package/src/cli.ts +131 -36
- package/src/config.ts +10 -0
- 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 +768 -1367
- 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}/web-ui-pass-plan.md +0 -0
package/src/eval/tasks/mmlu.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// table. MLX_BUN_MMLU_FROZEN=0 falls back to our own sampling of the full 14k set.
|
|
11
11
|
|
|
12
12
|
import { loadJsonl, sampleIndices, type TaskModel } from "../runner";
|
|
13
|
+
import { runtimeValue } from "../../runtime-config";
|
|
13
14
|
|
|
14
15
|
interface MmluRow {
|
|
15
16
|
question: string;
|
|
@@ -85,7 +86,7 @@ export async function evaluateMmlu(
|
|
|
85
86
|
// parity, not sampling. Set MLX_BUN_MMLU_FROZEN=0 (or frozen:false) to fall back to
|
|
86
87
|
// our own stratified sampling of the full 14k test set (a DIFFERENT subset: JS RNG
|
|
87
88
|
// ≠ numpy RandomState, so it does not match optiq's questions even at the same seed).
|
|
88
|
-
const useFrozen = opts.frozen ?? (
|
|
89
|
+
const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_MMLU_FROZEN") !== "0");
|
|
89
90
|
|
|
90
91
|
const testRows = loadJsonl<MmluRow>(useFrozen ? "mmlu_optiq_frozen" : "mmlu_test");
|
|
91
92
|
const devRows = loadJsonl<MmluRow>(useFrozen ? "mmlu_optiq_dev" : "mmlu_dev");
|
package/src/expert-io.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { nativePackDir } from "./native-pack";
|
|
5
5
|
import type { MlxArray } from "./mlx/array";
|
|
6
6
|
import type { MlxHandle } from "./mlx/ffi";
|
|
7
|
+
import { runtimeValue } from "./runtime-config";
|
|
7
8
|
|
|
8
9
|
const { cstring, i32, ptr: pointer, u32, u64 } = FFIType;
|
|
9
10
|
const EBUSY = 16;
|
|
@@ -69,7 +70,8 @@ export interface ExpertIOHintSnapshot {
|
|
|
69
70
|
|
|
70
71
|
function resolveLibrary(explicit?: string): string {
|
|
71
72
|
if (explicit) return explicit;
|
|
72
|
-
|
|
73
|
+
const configured = runtimeValue("MLX_BUN_EXPERT_IO_DYLIB");
|
|
74
|
+
if (configured) return configured;
|
|
73
75
|
const candidates = [
|
|
74
76
|
join(dirname(process.execPath), "libmlx_bun_expert_io.dylib"),
|
|
75
77
|
join(nativePackDir(), "libmlx_bun_expert_io.dylib"),
|
package/src/expert-offload.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { readFileSync } from "node:fs";
|
|
|
16
16
|
import { MmapFile } from "./mmap";
|
|
17
17
|
import { MlxArray, SAFETENSORS_TO_MLX } from "./mlx/array";
|
|
18
18
|
import type { SafetensorsDtype } from "./safetensors";
|
|
19
|
+
import { runtimeValue } from "./runtime-config";
|
|
19
20
|
|
|
20
21
|
export interface OffloadEntry {
|
|
21
22
|
name: string;
|
|
@@ -71,4 +72,5 @@ export function expertOffloadArray(name: string): MlxArray | null {
|
|
|
71
72
|
return MlxArray.fromPointer(mm.pointer(e.offset), e.shape, SAFETENSORS_TO_MLX[e.dtype]);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
const expertOffloadPath = runtimeValue("MLX_BUN_EXPERT_OFFLOAD");
|
|
76
|
+
if (expertOffloadPath) activateExpertOffload(expertOffloadPath);
|
package/src/expert-trace.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { Dtype } from "./mlx/ffi";
|
|
21
21
|
import type { MlxArray } from "./mlx/array";
|
|
22
|
+
import { runtimeValue } from "./runtime-config";
|
|
22
23
|
|
|
23
24
|
let sink: Bun.FileSink | null = null;
|
|
24
25
|
let seq = 0;
|
|
@@ -61,7 +62,8 @@ export function recordRouting(layer: number, indices: MlxArray): void {
|
|
|
61
62
|
if (++written % 2000 === 0) sink.flush();
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
const expertTracePath = runtimeValue("MLX_BUN_EXPERT_TRACE");
|
|
66
|
+
if (expertTracePath) beginExpertTrace(expertTracePath);
|
|
65
67
|
|
|
66
68
|
process.on("exit", () => { try { endExpertTrace(); } catch { /* best-effort */ } });
|
|
67
69
|
process.on("SIGINT", () => { endExpertTrace(); process.exit(0); });
|
package/src/fit.ts
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
import { totalmem } from "node:os";
|
|
12
12
|
import type { ModelConfig } from "./config";
|
|
13
|
+
import type { MemoryPlan } from "./memory-plan";
|
|
14
|
+
import {
|
|
15
|
+
kvBytesAt,
|
|
16
|
+
kvGeometry,
|
|
17
|
+
kvQuantBytesPerElement,
|
|
18
|
+
type KvSchemeOptions,
|
|
19
|
+
} from "./kv-scheme";
|
|
20
|
+
|
|
21
|
+
export { kvBytesAt, kvQuantBytesPerElement } from "./kv-scheme";
|
|
22
|
+
export type FitKvScheme = KvSchemeOptions;
|
|
13
23
|
|
|
14
24
|
/** Decode-efficiency vs theoretical bandwidth ceiling, measured on the
|
|
15
25
|
* reference machine (24.9 tok/s vs 30.3 ceiling @600 ctx). */
|
|
@@ -26,8 +36,6 @@ export const TRANSIENT_PER_TOKEN = 0.55e6;
|
|
|
26
36
|
* recommendedMaxWorkingSetSize is ~75% on consumer SKUs). */
|
|
27
37
|
export const WIRED_FRACTION = 0.75;
|
|
28
38
|
export const DEFAULT_CHUNK = 2048;
|
|
29
|
-
/** KV cache element size (bf16; quantized KV lands in Phase 6). */
|
|
30
|
-
const KV_BYTES = 2;
|
|
31
39
|
|
|
32
40
|
export interface MachineSpec {
|
|
33
41
|
name: string;
|
|
@@ -130,43 +138,9 @@ export function chooseAutoModel<T extends AutoPickCandidate>(
|
|
|
130
138
|
return bySizeDesc.find(fitsCoexistBudget) ?? bySizeDesc.find(fitsFullBudget);
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
export interface FitReport {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
weightsBytes: number;
|
|
137
|
-
kvBytes: number;
|
|
138
|
-
transientBytes: number;
|
|
139
|
-
totalBytes: number;
|
|
140
|
-
usableBytes: number;
|
|
141
|
-
maxSafeContext: number;
|
|
142
|
-
predictedDecodeTps: number;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
interface KvGeometry {
|
|
146
|
-
fullLayers: number;
|
|
147
|
-
slidingLayers: number;
|
|
148
|
-
fullBytesPerToken: number;
|
|
149
|
-
slidingBytesPerToken: number;
|
|
150
|
-
window: number;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function kvGeometry(config: ModelConfig): KvGeometry {
|
|
154
|
-
const t = config.text;
|
|
155
|
-
const slidingLayers = t.layerTypes.filter((l) => l === "sliding_attention").length;
|
|
156
|
-
const fullLayers = t.numHiddenLayers - slidingLayers;
|
|
157
|
-
const kEqVFactor = t.attentionKEqV ? 2 : 2; // k and v stored separately either way
|
|
158
|
-
return {
|
|
159
|
-
fullLayers,
|
|
160
|
-
slidingLayers,
|
|
161
|
-
fullBytesPerToken: fullLayers * kEqVFactor * t.numGlobalKeyValueHeads * t.globalHeadDim * KV_BYTES,
|
|
162
|
-
slidingBytesPerToken: slidingLayers * 2 * t.numKeyValueHeads * t.headDim * KV_BYTES,
|
|
163
|
-
window: t.slidingWindow,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function kvBytesAt(config: ModelConfig, ctx: number): number {
|
|
168
|
-
const g = kvGeometry(config);
|
|
169
|
-
return g.fullBytesPerToken * ctx + g.slidingBytesPerToken * Math.min(ctx, g.window);
|
|
141
|
+
export interface FitReport extends MemoryPlan {
|
|
142
|
+
readonly strategy: "generic-kv";
|
|
143
|
+
readonly predictedDecodeTps: number;
|
|
170
144
|
}
|
|
171
145
|
|
|
172
146
|
export function fit(
|
|
@@ -182,17 +156,20 @@ export function fit(
|
|
|
182
156
|
* replaces the machine-derived usable ceiling (ram × WIRED_FRACTION)
|
|
183
157
|
* outright — the budget IS the usable envelope. */
|
|
184
158
|
usableBytes?: number,
|
|
159
|
+
/** Active KV-quant scheme; a quantized cache holds more context in the
|
|
160
|
+
* same budget, so the solved ceiling (and admission) must bill it. */
|
|
161
|
+
kvScheme?: FitKvScheme,
|
|
185
162
|
): FitReport {
|
|
186
163
|
const usable = usableBytes ?? machine.ramBytes * WIRED_FRACTION;
|
|
187
164
|
const transient = Math.min(chunk, ctx) * TRANSIENT_PER_TOKEN;
|
|
188
|
-
const kv = kvBytesAt(config, ctx);
|
|
165
|
+
const kv = kvBytesAt(config, ctx, kvScheme);
|
|
189
166
|
const total = weightsBytes + kv + transient;
|
|
190
167
|
|
|
191
168
|
// solve max context: weights + kv(ctx) + transient ≤ usable.
|
|
192
169
|
// Below the window both KV terms are linear in ctx; above it the
|
|
193
170
|
// sliding term saturates and only full-attention layers keep growing.
|
|
194
|
-
const g = kvGeometry(config);
|
|
195
|
-
const fixed = weightsBytes + chunk * TRANSIENT_PER_TOKEN;
|
|
171
|
+
const g = kvGeometry(config, kvScheme);
|
|
172
|
+
const fixed = weightsBytes + chunk * TRANSIENT_PER_TOKEN + g.linearStateBytes;
|
|
196
173
|
let maxCtx = 0;
|
|
197
174
|
if (usable > fixed) {
|
|
198
175
|
const budget = usable - fixed;
|
|
@@ -216,11 +193,14 @@ export function fit(
|
|
|
216
193
|
(isMoe ? MOE_DECODE_EFFICIENCY : DECODE_EFFICIENCY);
|
|
217
194
|
|
|
218
195
|
return {
|
|
196
|
+
schemaVersion: 1,
|
|
197
|
+
strategy: "generic-kv",
|
|
219
198
|
fits: total <= usable,
|
|
220
199
|
contextTokens: ctx,
|
|
221
200
|
weightsBytes,
|
|
222
201
|
kvBytes: kv,
|
|
223
202
|
transientBytes: transient,
|
|
203
|
+
reserveBytes: 0,
|
|
224
204
|
totalBytes: total,
|
|
225
205
|
usableBytes: usable,
|
|
226
206
|
maxSafeContext: maxCtx,
|
|
@@ -231,6 +211,7 @@ export function fit(
|
|
|
231
211
|
/** The SKU matrix: which Apple Silicon configs run this model at `ctx`. */
|
|
232
212
|
export function skuMatrix(
|
|
233
213
|
config: ModelConfig, weightsBytes: number, ctx: number, expertsBytes = 0,
|
|
214
|
+
kvScheme?: FitKvScheme,
|
|
234
215
|
): { sku: string; ramGB: number; fits: boolean; maxContext: number; decodeTps: number }[] {
|
|
235
216
|
const rows: ReturnType<typeof skuMatrix> = [];
|
|
236
217
|
for (const sku of APPLE_SKUS) {
|
|
@@ -240,7 +221,7 @@ export function skuMatrix(
|
|
|
240
221
|
ramBytes: ram * 2 ** 30,
|
|
241
222
|
bandwidthGBs: sku.bandwidthGBs,
|
|
242
223
|
};
|
|
243
|
-
const r = fit(config, weightsBytes, ctx, m, DEFAULT_CHUNK, expertsBytes);
|
|
224
|
+
const r = fit(config, weightsBytes, ctx, m, DEFAULT_CHUNK, expertsBytes, undefined, kvScheme);
|
|
244
225
|
rows.push({
|
|
245
226
|
sku: sku.chip,
|
|
246
227
|
ramGB: ram,
|
package/src/flags.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// identically.
|
|
1
|
+
import { runtimeFlag, type RuntimeKey } from "./runtime-config";
|
|
2
|
+
|
|
3
|
+
// Generic on/off resolver for per-fork decode flags. Process environment is
|
|
4
|
+
// captured once by runtime-config; CLI tier aliases install an explicit
|
|
5
|
+
// immutable override before model modules are loaded.
|
|
7
6
|
|
|
8
7
|
/** Resolve an on/off env flag. An explicit "1"/"0" always wins; otherwise the
|
|
9
8
|
* flag takes `defaultOn`. */
|
|
10
|
-
export function flagOn(name:
|
|
11
|
-
|
|
12
|
-
return v === "1" ? true : v === "0" ? false : defaultOn;
|
|
9
|
+
export function flagOn(name: RuntimeKey, defaultOn: boolean): boolean {
|
|
10
|
+
return runtimeFlag(name, defaultOn);
|
|
13
11
|
}
|
package/src/generate.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import { MlxArray, gpuStream } from "./mlx/array";
|
|
10
10
|
import {
|
|
11
|
+
activeMemory,
|
|
11
12
|
clearCache,
|
|
12
13
|
maxRecommendedWorkingSetSize,
|
|
14
|
+
peakMemory,
|
|
13
15
|
setWiredLimit,
|
|
14
16
|
synchronize,
|
|
15
17
|
} from "./mlx/ffi";
|
|
@@ -20,11 +22,17 @@ import { PagedKVCache } from "./model/paged-kv";
|
|
|
20
22
|
import { DiffusionGemmaModel } from "./model/diffusion-gemma";
|
|
21
23
|
import { diffusionGenerate } from "./diffusion/diffusion-generate";
|
|
22
24
|
import type { RuntimeModel } from "./model/factory";
|
|
25
|
+
import type { PromptResponseTrace } from "./serve/prompt-response-trace";
|
|
23
26
|
import type { KvQuantSpec, TurboQuantScheme } from "./config";
|
|
24
27
|
import { flagOn } from "./flags";
|
|
28
|
+
import { runtimeValue } from "./runtime-config";
|
|
25
29
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
disposeStepExtras,
|
|
31
|
+
makeStepSampler,
|
|
32
|
+
stepExtrasArrays,
|
|
33
|
+
type LogitsProcessorOptions,
|
|
34
|
+
type SamplerOptions,
|
|
35
|
+
type StepExtras,
|
|
28
36
|
} from "./sampler";
|
|
29
37
|
import type { GrammarController } from "./grammar";
|
|
30
38
|
|
|
@@ -296,6 +304,11 @@ export interface GenerateStats {
|
|
|
296
304
|
};
|
|
297
305
|
}
|
|
298
306
|
|
|
307
|
+
export interface GenerateDiagnostics {
|
|
308
|
+
trace?: PromptResponseTrace;
|
|
309
|
+
mechanism?: "serial" | "continuous";
|
|
310
|
+
}
|
|
311
|
+
|
|
299
312
|
export interface GeneratedToken {
|
|
300
313
|
token: number;
|
|
301
314
|
index: number;
|
|
@@ -346,7 +359,11 @@ export class Generation implements AsyncIterable<GeneratedToken> {
|
|
|
346
359
|
// Scope semantics match the reference: set → generate → synchronize →
|
|
347
360
|
// restore; nothing stays pinned between generations. Re-entrant: only
|
|
348
361
|
// the outermost wiring scope touches the limit.
|
|
349
|
-
|
|
362
|
+
// macOS 26.6 reports a 24.96 GiB recommended set on this 24 GB machine.
|
|
363
|
+
// The old 0.75 fraction therefore stopped wiring the 13-16 GiB Qwen/GLM
|
|
364
|
+
// models even though they page heavily without an explicit wired limit.
|
|
365
|
+
// Keep smaller 8-9 GiB models unwired while covering the large-model class.
|
|
366
|
+
const WIRE_THRESHOLD = 0.5;
|
|
350
367
|
let wiredScopeDepth = 0;
|
|
351
368
|
let wiredOldLimit = 0;
|
|
352
369
|
|
|
@@ -370,7 +387,7 @@ export function wiredWorkingSetBytes(model: WiredModelMemory): number {
|
|
|
370
387
|
export function modelNeedsWiredLimit(
|
|
371
388
|
model: WiredModelMemory,
|
|
372
389
|
recommendedBytes = maxRecommendedWorkingSetSize(),
|
|
373
|
-
force =
|
|
390
|
+
force = runtimeValue("MLX_BUN_FORCE_WIRE") === "1",
|
|
374
391
|
): boolean {
|
|
375
392
|
return force ||
|
|
376
393
|
wiredWorkingSetBytes(model) > WIRE_THRESHOLD * recommendedBytes;
|
|
@@ -387,10 +404,23 @@ function exitWiredScope(): void {
|
|
|
387
404
|
}
|
|
388
405
|
}
|
|
389
406
|
|
|
407
|
+
/** Hold the process wired limit while one model owns GPU execution. */
|
|
408
|
+
export function acquireModelWiredLimit(model: WiredModelMemory): () => void {
|
|
409
|
+
if (!modelNeedsWiredLimit(model)) return () => {};
|
|
410
|
+
enterWiredScope();
|
|
411
|
+
let released = false;
|
|
412
|
+
return () => {
|
|
413
|
+
if (released) return;
|
|
414
|
+
released = true;
|
|
415
|
+
exitWiredScope();
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
390
419
|
export function generate(
|
|
391
420
|
model: RuntimeModel,
|
|
392
421
|
promptTokens: number[],
|
|
393
422
|
options: GenerateOptions = {},
|
|
423
|
+
diagnostics: GenerateDiagnostics = {},
|
|
394
424
|
): Generation {
|
|
395
425
|
// DiffusionGemma is non-autoregressive: route to the denoising engine instead
|
|
396
426
|
// of the AR decode loop. Same Generation/GenerateStats contract so the CLI and
|
|
@@ -398,7 +428,7 @@ export function generate(
|
|
|
398
428
|
let inner =
|
|
399
429
|
model instanceof DiffusionGemmaModel
|
|
400
430
|
? generateDiffusionInner(model, promptTokens, options)
|
|
401
|
-
: generateInner(model, promptTokens, options);
|
|
431
|
+
: generateInner(model, promptTokens, options, diagnostics);
|
|
402
432
|
if (options.adapters?.length) inner = adapterScoped(model, options.adapters, inner);
|
|
403
433
|
const memoryModel: WiredModelMemory = model;
|
|
404
434
|
if (memoryModel.expertRuntime?.flushUsage)
|
|
@@ -496,12 +526,11 @@ export async function withModelWiredLimit<T>(
|
|
|
496
526
|
model: WiredModelMemory,
|
|
497
527
|
run: () => Promise<T>,
|
|
498
528
|
): Promise<T> {
|
|
499
|
-
|
|
500
|
-
enterWiredScope();
|
|
529
|
+
const release = acquireModelWiredLimit(model);
|
|
501
530
|
try {
|
|
502
531
|
return await run();
|
|
503
532
|
} finally {
|
|
504
|
-
|
|
533
|
+
release();
|
|
505
534
|
}
|
|
506
535
|
}
|
|
507
536
|
|
|
@@ -542,15 +571,13 @@ async function* generateInner(
|
|
|
542
571
|
model: RuntimeModel,
|
|
543
572
|
promptTokens: number[],
|
|
544
573
|
options: GenerateOptions,
|
|
574
|
+
diagnostics: GenerateDiagnostics,
|
|
545
575
|
): AsyncGenerator<GeneratedToken, GenerateStats> {
|
|
546
576
|
const {
|
|
547
577
|
maxTokens = 512,
|
|
548
578
|
eosTokenIds = model.config.eosTokenIds,
|
|
549
579
|
prefillChunkSize = 2048,
|
|
550
580
|
} = options;
|
|
551
|
-
const sampler = makeSampler(options);
|
|
552
|
-
const processors = makeLogitsProcessors(options);
|
|
553
|
-
const needsTokenHistory = processors.length > 0;
|
|
554
581
|
|
|
555
582
|
// logprobs capture (mlx_lm.server semantics — see GenerateOptions.logprobs).
|
|
556
583
|
// Everything below is gated: when neither flag is set, no extra ops, evals,
|
|
@@ -558,8 +585,23 @@ async function* generateInner(
|
|
|
558
585
|
const captureSelLp = options.logprobs === true;
|
|
559
586
|
const captureTopK =
|
|
560
587
|
options.topLogprobs && options.topLogprobs > 0 ? options.topLogprobs : 0;
|
|
561
|
-
const
|
|
588
|
+
const stepSampler = makeStepSampler(options, {
|
|
589
|
+
tokenRepresentation: "device",
|
|
590
|
+
grammarWait: "external",
|
|
591
|
+
historyUpdate: "manual",
|
|
592
|
+
captureSelectedLogprob: captureSelLp,
|
|
593
|
+
captureTopLogprobs: captureTopK,
|
|
594
|
+
});
|
|
595
|
+
const needsTokenHistory = stepSampler.needsHistory;
|
|
562
596
|
|
|
597
|
+
const closePrefill = diagnostics.trace?.begin("prefill.total", {
|
|
598
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
599
|
+
promptTokens: promptTokens.length,
|
|
600
|
+
cachedTokens: options.cache?.[0]?.offset ?? 0,
|
|
601
|
+
});
|
|
602
|
+
const closeBatchSetup = diagnostics.trace?.begin("prefill.batch_setup", {
|
|
603
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
604
|
+
});
|
|
563
605
|
const ownsCache = !options.cache;
|
|
564
606
|
const cache = options.cache ?? model.makeCache();
|
|
565
607
|
const cachedTokens = cache[0]!.offset;
|
|
@@ -572,22 +614,10 @@ async function* generateInner(
|
|
|
572
614
|
// prompt + maxTokens − 1 (step maxTokens−1's forward), so this bound
|
|
573
615
|
// makes pool exhaustion an accounting-bug tripwire, not a request limit.
|
|
574
616
|
maybePageKv(cache, options, promptTokens.length + maxTokens);
|
|
575
|
-
|
|
576
|
-
let history: MlxArray | null = null;
|
|
577
|
-
|
|
617
|
+
closeBatchSetup?.();
|
|
578
618
|
/** Device-side logprob capture for one step, computed from the SAME lp the
|
|
579
619
|
* sampler saw (post-processors, pre-truncation) — read back lazily with the
|
|
580
620
|
* token so decode pipelining is preserved. */
|
|
581
|
-
interface StepExtras {
|
|
582
|
-
sel: MlxArray | null; // [1,1] emitted token's logprob
|
|
583
|
-
topIdx: MlxArray | null; // [1,k] top-k token ids
|
|
584
|
-
topVals: MlxArray | null; // [1,k] their logprobs
|
|
585
|
-
}
|
|
586
|
-
const extrasArrays = (e: StepExtras | null): MlxArray[] =>
|
|
587
|
-
e ? [e.sel, e.topIdx, e.topVals].filter((a): a is MlxArray => a !== null) : [];
|
|
588
|
-
const disposeExtras = (e: StepExtras | null): void => {
|
|
589
|
-
if (e) for (const a of [e.sel, e.topIdx, e.topVals]) a?.dispose();
|
|
590
|
-
};
|
|
591
621
|
/** Read extras back to JS (forces eval — they were async-dispatched with the
|
|
592
622
|
* token) without appending casts behind the next decode step. */
|
|
593
623
|
const readExtras = (e: StepExtras | null): TokenLogprobs | undefined => {
|
|
@@ -601,7 +631,7 @@ async function* generateInner(
|
|
|
601
631
|
(a, b) => b.logprob - a.logprob,
|
|
602
632
|
);
|
|
603
633
|
}
|
|
604
|
-
|
|
634
|
+
disposeStepExtras(e);
|
|
605
635
|
return out;
|
|
606
636
|
};
|
|
607
637
|
|
|
@@ -611,52 +641,11 @@ async function* generateInner(
|
|
|
611
641
|
logits3d: MlxArray,
|
|
612
642
|
step: number,
|
|
613
643
|
): { tok: MlxArray; extras: StepExtras | null } => {
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
for (const p of processors) logits = disposing(logits, p(history, logits));
|
|
617
|
-
// Grammar mask (src/grammar.ts): apply AFTER the standard processors so the
|
|
618
|
-
// grammar has the final say on validity (a -inf'd token stays -inf through
|
|
619
|
-
// log-softmax). Off (no grammar) = zero cost — the branch is unset.
|
|
620
|
-
if (options.grammar) logits = disposing(logits, options.grammar.applyMask(logits));
|
|
621
|
-
const lp = toLogprobs(logits);
|
|
622
|
-
logits.dispose();
|
|
623
|
-
const tok = sampler(lp, step);
|
|
624
|
-
let extras: StepExtras | null = null;
|
|
625
|
-
if (capture) {
|
|
626
|
-
let sel: MlxArray | null = null;
|
|
627
|
-
let topIdx: MlxArray | null = null;
|
|
628
|
-
let topVals: MlxArray | null = null;
|
|
629
|
-
if (captureSelLp) {
|
|
630
|
-
const idx = ops.reshape(tok, [1, 1]);
|
|
631
|
-
sel = ops.takeAlongAxis(lp, idx, -1);
|
|
632
|
-
idx.dispose();
|
|
633
|
-
}
|
|
634
|
-
if (captureTopK > 0) {
|
|
635
|
-
// mlx-lm _format_top_logprobs: argpartition(-logprobs, kth=k-1)[:k]
|
|
636
|
-
const k = Math.min(captureTopK, V);
|
|
637
|
-
const negLp = ops.neg(lp);
|
|
638
|
-
const part = ops.argpartitionAxis(negLp, k - 1, -1);
|
|
639
|
-
const idxView = part.slice([0, 0], [1, k]);
|
|
640
|
-
topIdx = ops.contiguous(idxView); // slices are strided views; raw readback needs contiguous
|
|
641
|
-
topVals = ops.takeAlongAxis(lp, topIdx, -1);
|
|
642
|
-
for (const a of [negLp, part, idxView]) a.dispose();
|
|
643
|
-
}
|
|
644
|
-
extras = { sel, topIdx, topVals };
|
|
645
|
-
}
|
|
646
|
-
lp.dispose();
|
|
647
|
-
return { tok, extras };
|
|
644
|
+
const result = stepSampler.sample(logits3d, step);
|
|
645
|
+
return { tok: result.token, extras: result.extras };
|
|
648
646
|
};
|
|
649
647
|
|
|
650
|
-
const pushHistory = (tok: MlxArray) =>
|
|
651
|
-
if (!needsTokenHistory) return;
|
|
652
|
-
if (!history) {
|
|
653
|
-
history = ops.reshape(tok, [1]);
|
|
654
|
-
} else {
|
|
655
|
-
const prev = history;
|
|
656
|
-
history = ops.concatAxis([prev, tok], 0);
|
|
657
|
-
prev.dispose();
|
|
658
|
-
}
|
|
659
|
-
};
|
|
648
|
+
const pushHistory = (tok: MlxArray): void => stepSampler.commitDevice(tok);
|
|
660
649
|
|
|
661
650
|
// Decode-loop state lives at function scope so the finally can still
|
|
662
651
|
// report stats and dispose in-flight arrays when the consumer
|
|
@@ -673,6 +662,7 @@ async function* generateInner(
|
|
|
673
662
|
let nextExtras: StepExtras | null = null;
|
|
674
663
|
let finished = false;
|
|
675
664
|
let threw = false;
|
|
665
|
+
let closeTokenZero: (() => void) | undefined;
|
|
676
666
|
const makeStats = (): GenerateStats => ({
|
|
677
667
|
promptTokens: promptTokens.length,
|
|
678
668
|
cachedTokens,
|
|
@@ -686,14 +676,19 @@ async function* generateInner(
|
|
|
686
676
|
|
|
687
677
|
try {
|
|
688
678
|
// ---- prefill ----
|
|
679
|
+
const closeInitialKv = diagnostics.trace?.begin("prefill.kv_maintenance", {
|
|
680
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
681
|
+
boundary: "initial",
|
|
682
|
+
});
|
|
689
683
|
maybeQuantizeKv(cache, options);
|
|
684
|
+
closeInitialKv?.();
|
|
690
685
|
const tPrefill = performance.now();
|
|
691
686
|
let h0: MlxArray;
|
|
692
687
|
if (options.promptEmbeddings) {
|
|
693
688
|
if (cachedTokens !== 0)
|
|
694
689
|
throw new Error("promptEmbeddings cannot be combined with a pre-warmed cache");
|
|
695
690
|
if (needsTokenHistory)
|
|
696
|
-
|
|
691
|
+
stepSampler.seedHistory(promptTokens);
|
|
697
692
|
// e2b/e4b need the spliced token ids to build per-layer inputs
|
|
698
693
|
// (multimodal soft-token positions zeroed inside forwardEmbeddings).
|
|
699
694
|
const embedIds = ops.fromInt32(promptTokens, [1, promptTokens.length]);
|
|
@@ -715,6 +710,11 @@ async function* generateInner(
|
|
|
715
710
|
if (snapAt > pos && snapAt < promptTokens.length) {
|
|
716
711
|
while (pos < snapAt) {
|
|
717
712
|
const chunk = promptTokens.slice(pos, Math.min(pos + prefillChunkSize, snapAt));
|
|
713
|
+
const closeChunk = diagnostics.trace?.begin("prefill.chunk", {
|
|
714
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
715
|
+
startToken: pos,
|
|
716
|
+
tokens: chunk.length,
|
|
717
|
+
});
|
|
718
718
|
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
719
719
|
const h = await forwardHiddenForGeneration(model, ids, cache);
|
|
720
720
|
ids.dispose();
|
|
@@ -722,6 +722,7 @@ async function* generateInner(
|
|
|
722
722
|
evalCacheState(cache);
|
|
723
723
|
maybeQuantizeKv(cache, options);
|
|
724
724
|
clearCache();
|
|
725
|
+
closeChunk?.();
|
|
725
726
|
pos += chunk.length;
|
|
726
727
|
// Macrotask yield between chunks (runtime-isolation.md Phase 1):
|
|
727
728
|
// each chunk is a synchronous multi-hundred-ms FFI eval; without
|
|
@@ -749,6 +750,11 @@ async function* generateInner(
|
|
|
749
750
|
? Math.min(prefillChunkSize, promptTokens.length - pos - 1)
|
|
750
751
|
: prefillChunkSize;
|
|
751
752
|
const chunk = promptTokens.slice(pos, pos + n);
|
|
753
|
+
const closeChunk = diagnostics.trace?.begin("prefill.chunk", {
|
|
754
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
755
|
+
startToken: pos,
|
|
756
|
+
tokens: chunk.length,
|
|
757
|
+
});
|
|
752
758
|
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
753
759
|
const h = await forwardHiddenForGeneration(model, ids, cache);
|
|
754
760
|
ids.dispose();
|
|
@@ -762,12 +768,15 @@ async function* generateInner(
|
|
|
762
768
|
// measured, scripts/decode-split.ts; the context-scaling decode
|
|
763
769
|
// gap's main term).
|
|
764
770
|
clearCache();
|
|
771
|
+
closeChunk?.();
|
|
765
772
|
pos += n;
|
|
773
|
+
if (runtimeValue("MLX_BUN_PREFILL_MEM_LOG") === "1")
|
|
774
|
+
console.error(`[prefill-mem] ${pos} active ${(activeMemory() / 2 ** 30).toFixed(2)} peak ${(peakMemory() / 2 ** 30).toFixed(2)}`);
|
|
766
775
|
// Macrotask yield between chunks (runtime-isolation.md Phase 1).
|
|
767
776
|
await new Promise<void>((r) => setImmediate(r));
|
|
768
777
|
}
|
|
769
778
|
if (needsTokenHistory) {
|
|
770
|
-
|
|
779
|
+
stepSampler.seedHistory(promptTokens);
|
|
771
780
|
}
|
|
772
781
|
// Under tailSplit this is exactly [promptTokens[len-1]] — the L=1
|
|
773
782
|
// step-0 forward (uncompiled L=1 is bit-exact with compiled decode,
|
|
@@ -776,12 +785,27 @@ async function* generateInner(
|
|
|
776
785
|
// PromptCache.put alignment) is unchanged: after step 0 the caches
|
|
777
786
|
// cover exactly the prompt, as before.
|
|
778
787
|
const lastChunk = promptTokens.slice(pos);
|
|
788
|
+
const closeChunk = diagnostics.trace?.begin("prefill.chunk", {
|
|
789
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
790
|
+
startToken: pos,
|
|
791
|
+
tokens: lastChunk.length,
|
|
792
|
+
final: true,
|
|
793
|
+
});
|
|
779
794
|
const ids0 = ops.fromInt32(lastChunk, [1, lastChunk.length]);
|
|
780
795
|
h0 = await forwardHiddenForGeneration(model, ids0, cache);
|
|
781
796
|
ids0.dispose();
|
|
797
|
+
closeChunk?.();
|
|
782
798
|
}
|
|
783
799
|
if (options.snapshotAt === undefined || options.snapshotAt >= promptTokens.length)
|
|
784
800
|
options.onPrefillDone?.();
|
|
801
|
+
// The trace's prefill span is the uncached model-forward portion only.
|
|
802
|
+
// Token-0 lm-head, sampling, and readback are a separate additive stage.
|
|
803
|
+
if (diagnostics.trace && runtimeValue("MLX_BUN_P2R_SYNC") === "1")
|
|
804
|
+
synchronize(gpuStream);
|
|
805
|
+
closePrefill?.();
|
|
806
|
+
closeTokenZero = diagnostics.trace?.begin("token_zero.total", {
|
|
807
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
808
|
+
});
|
|
785
809
|
const [, L0, H] = h0.shape as [number, number, number];
|
|
786
810
|
const hLast = h0.slice([0, L0 - 1, 0], [1, L0, H]);
|
|
787
811
|
h0.dispose();
|
|
@@ -799,7 +823,7 @@ async function* generateInner(
|
|
|
799
823
|
// to prompt_time, not decode — replicated so cross-stack decode
|
|
800
824
|
// tok/s measure the same quantity. The boundary cost is real and
|
|
801
825
|
// scales with prompt length; it belongs to "having prefilled".
|
|
802
|
-
ops.asyncEvalAll([pending, ...
|
|
826
|
+
ops.asyncEvalAll([pending, ...stepExtrasArrays(pendingExtras)]);
|
|
803
827
|
|
|
804
828
|
// ---- decode (pipelined) ----
|
|
805
829
|
// Compiled decode (docs/design/optimization_plan.md Phase A): replay the per-step
|
|
@@ -886,13 +910,7 @@ async function* generateInner(
|
|
|
886
910
|
// following iteration (supports() re-checks the grown caches).
|
|
887
911
|
maybeQuantizeKv(cache, options);
|
|
888
912
|
pushHistory(cur);
|
|
889
|
-
|
|
890
|
-
const jt = ops.fromInt32(jumpEmit, [jumpEmit.length]);
|
|
891
|
-
const prev = history!;
|
|
892
|
-
history = ops.concatAxis([prev, jt], 0);
|
|
893
|
-
prev.dispose();
|
|
894
|
-
jt.dispose();
|
|
895
|
-
}
|
|
913
|
+
stepSampler.commitNumbers(jumpEmit);
|
|
896
914
|
const chunk = [grammarTok, ...jumpEmit];
|
|
897
915
|
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
898
916
|
const h = await forwardHiddenForGeneration(model, ids, cache);
|
|
@@ -910,7 +928,7 @@ async function* generateInner(
|
|
|
910
928
|
nextPending = sn.tok;
|
|
911
929
|
nextExtras = sn.extras;
|
|
912
930
|
logits.dispose();
|
|
913
|
-
ops.asyncEvalAll([nextPending, ...
|
|
931
|
+
ops.asyncEvalAll([nextPending, ...stepExtrasArrays(nextExtras)]);
|
|
914
932
|
} else {
|
|
915
933
|
h.dispose(); // burst ends the generation (max_tokens or grammar done)
|
|
916
934
|
}
|
|
@@ -945,7 +963,7 @@ async function* generateInner(
|
|
|
945
963
|
nextPending = sn.tok;
|
|
946
964
|
nextExtras = sn.extras;
|
|
947
965
|
logits.dispose();
|
|
948
|
-
ops.asyncEvalAll([nextPending, ...
|
|
966
|
+
ops.asyncEvalAll([nextPending, ...stepExtrasArrays(nextExtras), ...evalWith]);
|
|
949
967
|
}
|
|
950
968
|
|
|
951
969
|
// sync-read step n's token while n+1 computes
|
|
@@ -957,6 +975,8 @@ async function* generateInner(
|
|
|
957
975
|
// (mlx-lm stream_generate's n==0 clock swap; the first token is
|
|
958
976
|
// "free" on the decode clock there too)
|
|
959
977
|
prefillMs = performance.now() - tPrefill;
|
|
978
|
+
closeTokenZero?.();
|
|
979
|
+
closeTokenZero = undefined;
|
|
960
980
|
tDecode = performance.now();
|
|
961
981
|
}
|
|
962
982
|
cur.dispose();
|
|
@@ -967,10 +987,10 @@ async function* generateInner(
|
|
|
967
987
|
if (nextPending !== null && !jumpEmit) forwarded.push(token);
|
|
968
988
|
|
|
969
989
|
if (eosTokenIds.includes(token)) {
|
|
970
|
-
|
|
990
|
+
disposeStepExtras(curExtras);
|
|
971
991
|
nextPending?.dispose();
|
|
972
992
|
nextPending = null;
|
|
973
|
-
|
|
993
|
+
disposeStepExtras(nextExtras);
|
|
974
994
|
nextExtras = null;
|
|
975
995
|
stop = true;
|
|
976
996
|
} else {
|
|
@@ -1013,8 +1033,8 @@ async function* generateInner(
|
|
|
1013
1033
|
if (!finished) {
|
|
1014
1034
|
pending?.dispose();
|
|
1015
1035
|
nextPending?.dispose();
|
|
1016
|
-
|
|
1017
|
-
|
|
1036
|
+
disposeStepExtras(pendingExtras);
|
|
1037
|
+
disposeStepExtras(nextExtras);
|
|
1018
1038
|
}
|
|
1019
1039
|
if (ownsCache) for (const c of cache) c.dispose();
|
|
1020
1040
|
// The grammar controller owns native WASM state (GrammarMatcher +
|
|
@@ -1022,7 +1042,7 @@ async function* generateInner(
|
|
|
1022
1042
|
// throw). TokenizerInfo itself is process-cached (vocab-structural), so
|
|
1023
1043
|
// only the per-request matcher/compiled are freed here.
|
|
1024
1044
|
options.grammar?.dispose();
|
|
1025
|
-
|
|
1045
|
+
stepSampler.dispose();
|
|
1026
1046
|
if (!finished && !threw) {
|
|
1027
1047
|
// forced early return (consumer break at a yield): still report
|
|
1028
1048
|
// stats — `forwarded` only lists tokens whose KV actually entered
|
|
@@ -1032,8 +1052,3 @@ async function* generateInner(
|
|
|
1032
1052
|
}
|
|
1033
1053
|
}
|
|
1034
1054
|
}
|
|
1035
|
-
|
|
1036
|
-
function disposing(old: MlxArray, next: MlxArray): MlxArray {
|
|
1037
|
-
old.dispose();
|
|
1038
|
-
return next;
|
|
1039
|
-
}
|
package/src/grammar.ts
CHANGED
|
@@ -33,6 +33,7 @@ import xgrammar, {
|
|
|
33
33
|
import { readFileSync } from "node:fs";
|
|
34
34
|
import { MlxArray } from "./mlx/array";
|
|
35
35
|
import * as ops from "./mlx/ops";
|
|
36
|
+
import { runtimeValue } from "./runtime-config";
|
|
36
37
|
import type { LoadedTokenizer } from "./tokenizer";
|
|
37
38
|
|
|
38
39
|
/** The request shape — the union of every structured-output field name across
|
|
@@ -581,5 +582,5 @@ function resolveGrammarRequest(req: GrammarRequest):
|
|
|
581
582
|
* (requests degrade to prompt injection). Matches the project's envelope-gated
|
|
582
583
|
* kill-switch discipline (MLX_BUN_COMPILED_DECODE, MLX_BUN_GRAMMAR_BATCH, …). */
|
|
583
584
|
export function grammarEnabled(): boolean {
|
|
584
|
-
return
|
|
585
|
+
return runtimeValue("MLX_BUN_GRAMMAR") !== "0";
|
|
585
586
|
}
|
package/src/harness-pi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// mlx-bun harness pi — register mlx-bun as a provider in the user's pi
|
|
2
|
-
// install (Phase 16 P1, docs/investigations/pi-builtin-investigation.md).
|
|
2
|
+
// install (Phase 16 P1, docs/archive/investigations/pi-builtin-investigation.md).
|
|
3
3
|
//
|
|
4
4
|
// Mechanism: a generated extension file in ~/.pi/agent/extensions/ that
|
|
5
5
|
// calls pi.registerProvider() with dynamic discovery — it fetches
|