mlx-bun 0.0.9 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +255 -0
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +98 -0
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +18 -2
- package/docs/design/ssd-kv-cold-tier.md +288 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/decode-roofline-lookagain.md +92 -6
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +251 -119
- package/docs/reference/training.md +20 -16
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +402 -107
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -16
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +618 -122
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +127 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +179 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +391 -34
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +123 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +99 -25
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +338 -32
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +61 -3
- package/src/serve/batch-scheduler.ts +1001 -76
- package/src/serve/generation-gateway.ts +292 -30
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1460 -105
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +285 -0
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +29 -26
- package/src/train/job.ts +0 -2
- package/src/train/loss.ts +53 -11
- package/src/train/prefix-shared.ts +30 -7
- package/src/train/segmented.ts +150 -46
- package/src/train/trainer.ts +22 -15
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
package/src/memory/vault.ts
CHANGED
|
@@ -218,6 +218,17 @@ export async function readArticle(root: string, article: string): Promise<{ path
|
|
|
218
218
|
return { path, content: await readFile(path, "utf8") };
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/** Vault-relative path (`articles/<stem>.md` or `Reference/<stem>.md`) for an
|
|
222
|
+
* existing article/reference doc — the safe argv piece for git plumbing
|
|
223
|
+
* (articleHistory/articleDiff), resolved via the same name normalization/
|
|
224
|
+
* existence check as readArticle so a caller can't smuggle a path-traversal
|
|
225
|
+
* or shell-metacharacter name through. Throws if the doc doesn't exist. */
|
|
226
|
+
export async function resolveArticleRelPath(root: string, article: string): Promise<string> {
|
|
227
|
+
const doc = normalizeMemoryDocId(article);
|
|
228
|
+
await resolveArticlePath(root, article); // existence check, reuses the same normalization
|
|
229
|
+
return doc.kind === "reference" ? join("Reference", `${doc.stem}.md`) : join("articles", `${doc.stem}.md`);
|
|
230
|
+
}
|
|
231
|
+
|
|
221
232
|
/** Filename stems for every `*.md` under articles/, sorted alphabetically. */
|
|
222
233
|
export async function listArticles(root: string): Promise<string[]> {
|
|
223
234
|
let names: string[];
|
|
@@ -1013,6 +1024,69 @@ function runGit(args: string[], cwd: string): Promise<boolean> {
|
|
|
1013
1024
|
});
|
|
1014
1025
|
}
|
|
1015
1026
|
|
|
1027
|
+
// ---- read-only git plumbing (per-article history/diff) ----------------
|
|
1028
|
+
//
|
|
1029
|
+
// Used by the memory REST surface (GET /api/memory/history|diff) to render
|
|
1030
|
+
// "watch it self-heal" — the article's commit history and a specific
|
|
1031
|
+
// commit's diff. Read-only plumbing only (log/show), args always an
|
|
1032
|
+
// explicit argv array via Bun.spawn (never a shell string), and callers
|
|
1033
|
+
// MUST validate `rev`/`name` before calling these (see server.ts) — this
|
|
1034
|
+
// layer trusts its `relPath`/`rev` inputs are already safe.
|
|
1035
|
+
|
|
1036
|
+
export interface ArticleHistoryEntry {
|
|
1037
|
+
hash: string;
|
|
1038
|
+
date: string;
|
|
1039
|
+
subject: string;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** `git log` for one file, newest first, argv-only (never a shell string).
|
|
1043
|
+
* Returns [] when the vault isn't a git repo, the file has no history, or
|
|
1044
|
+
* git is unavailable. `relPath` must already be a repo-relative path (no
|
|
1045
|
+
* `..`, not absolute) — callers validate before calling. */
|
|
1046
|
+
export async function articleHistory(root: string, relPath: string, limit = 50): Promise<ArticleHistoryEntry[]> {
|
|
1047
|
+
if (!(await pathExists(join(root, ".git")))) return [];
|
|
1048
|
+
try {
|
|
1049
|
+
const proc = Bun.spawn(
|
|
1050
|
+
["git", "log", `--max-count=${limit}`, "--date=short", "--pretty=format:%H%x1f%ad%x1f%s%x1e", "--", relPath],
|
|
1051
|
+
{ cwd: root, stdout: "pipe", stderr: "ignore" },
|
|
1052
|
+
);
|
|
1053
|
+
const out = await new Response(proc.stdout).text();
|
|
1054
|
+
if ((await proc.exited) !== 0) return [];
|
|
1055
|
+
return out
|
|
1056
|
+
.split("\x1e")
|
|
1057
|
+
.map((rec) => rec.trim())
|
|
1058
|
+
.filter((rec) => rec.length > 0)
|
|
1059
|
+
.map((rec) => {
|
|
1060
|
+
const [hash = "", date = "", subject = ""] = rec.split("\x1f");
|
|
1061
|
+
return { hash, date, subject };
|
|
1062
|
+
})
|
|
1063
|
+
.filter((e) => e.hash.length > 0);
|
|
1064
|
+
} catch {
|
|
1065
|
+
return [];
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** `git show <rev> -- <relPath>` — the diff for one commit touching one file.
|
|
1070
|
+
* `rev` must already be validated as a hex commit hash by the caller (never
|
|
1071
|
+
* a ref expression like `HEAD~1` or `main^`, which could be abused as an
|
|
1072
|
+
* argument-injection vector); `relPath` must already be repo-relative.
|
|
1073
|
+
* Returns null when the vault isn't a git repo or the commit/diff can't be
|
|
1074
|
+
* produced. */
|
|
1075
|
+
export async function articleDiff(root: string, rev: string, relPath: string): Promise<string | null> {
|
|
1076
|
+
if (!(await pathExists(join(root, ".git")))) return null;
|
|
1077
|
+
try {
|
|
1078
|
+
const proc = Bun.spawn(
|
|
1079
|
+
["git", "show", rev, "--pretty=format:", "--", relPath],
|
|
1080
|
+
{ cwd: root, stdout: "pipe", stderr: "ignore" },
|
|
1081
|
+
);
|
|
1082
|
+
const out = await new Response(proc.stdout).text();
|
|
1083
|
+
if ((await proc.exited) !== 0) return null;
|
|
1084
|
+
return out;
|
|
1085
|
+
} catch {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1016
1090
|
export interface SetupResult {
|
|
1017
1091
|
root: string;
|
|
1018
1092
|
created: string[];
|
package/src/mlx/array.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// MlxArray: ownership wrapper around an mlx_array handle.
|
|
2
2
|
// Explicit .dispose() is the contract; a FinalizationRegistry backstop
|
|
3
|
-
// frees leaked handles on GC (verified in spikes/phase0-memory.ts).
|
|
3
|
+
// frees leaked handles on GC (verified in lab/spikes/phase0-memory.ts).
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { dlopen, ptr, toArrayBuffer } from "bun:ffi";
|
|
6
6
|
import { C, Dtype, DTYPE_NAMES, type MlxHandle, optInt, outArray, takeMlxError } from "./ffi";
|
|
7
7
|
import type { SafetensorsDtype } from "../safetensors";
|
|
8
8
|
|
|
@@ -24,17 +24,39 @@ export const SAFETENSORS_TO_MLX: Record<SafetensorsDtype, Dtype> = {
|
|
|
24
24
|
|
|
25
25
|
// --- zero-copy pinning ---------------------------------------------------
|
|
26
26
|
// Buffers handed to mlx via new_data_managed_payload must outlive the mlx
|
|
27
|
-
// array.
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// array. fromView pins the JS view in this map (a GC root, nothing more);
|
|
28
|
+
// release is EXPLICIT and JS-thread-only via unpinHostBuffer() — never an
|
|
29
|
+
// mlx dtor. See the hazard note below.
|
|
30
30
|
|
|
31
31
|
const pinned = new Map<number, Uint8Array>();
|
|
32
32
|
let nextPinId = 1;
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
34
|
+
// HAZARD (root cause of the 2026-07-06 restart-restore hang): mlx releases
|
|
35
|
+
// buffer Data wherever the LAST shared_ptr drops — often the Metal
|
|
36
|
+
// COMPLETION thread (gpu::eval retains buffers until the command buffer
|
|
37
|
+
// finishes, i.e. past dispose()). A bun:ffi JSCallback dtor invoked from
|
|
38
|
+
// that thread deadlocks when the JS thread is inside a blocking FFI eval
|
|
39
|
+
// (completion waits on JS, JS waits on completion) and SIGTRAPs mid-GC.
|
|
40
|
+
// So NO array here ever gets a JS dtor: fromPointer AND fromView both hand
|
|
41
|
+
// mlx the native no-op dtor below (payload 0), and host-buffer lifetime is
|
|
42
|
+
// the CALLER's contract, released only from the JS thread (unpinHostBuffer)
|
|
43
|
+
// or never (process-lifetime mmaps: expert-offload).
|
|
44
|
+
|
|
45
|
+
// Native no-op dtor: payload is always 0, and libc free(NULL) is defined to
|
|
46
|
+
// do nothing — a dtor mlx can safely call from ANY thread. The pointed-to
|
|
47
|
+
// memory's lifetime is the CALLER's contract (weight mmaps live for the
|
|
48
|
+
// process; restored KV is COPIED at load since 2026-07-07 — see kv-store.ts
|
|
49
|
+
// loadKvCache — so no restore mapping needs pinning anymore). The address
|
|
50
|
+
// comes via dlsym because
|
|
51
|
+
// bun:ffi's symbol .ptr returns the pointer bit-cast to float64
|
|
52
|
+
// (lab/repro/bun-ffi-f64) — passing that back truncates to NULL.
|
|
53
|
+
const libcDlsym = dlopen("/usr/lib/libSystem.B.dylib", {
|
|
54
|
+
dlsym: { args: ["u64", "ptr"], returns: "u64" },
|
|
55
|
+
});
|
|
56
|
+
const FREE_NAME = Buffer.from("free\0");
|
|
57
|
+
const RTLD_DEFAULT = 0xfffffffffffffffen;
|
|
58
|
+
const freeFnAddr = Number(libcDlsym.symbols.dlsym(RTLD_DEFAULT, ptr(FREE_NAME)));
|
|
59
|
+
if (!freeFnAddr) throw new Error("dlsym(free) failed — zero-copy arrays need a native no-op dtor");
|
|
38
60
|
|
|
39
61
|
export function pinnedBufferCount(): number {
|
|
40
62
|
return pinned.size;
|
|
@@ -50,31 +72,61 @@ export class MlxArray {
|
|
|
50
72
|
#handle: MlxHandle;
|
|
51
73
|
#disposed = false;
|
|
52
74
|
#token = {};
|
|
75
|
+
#pinId = 0; // fromView only: key into the pinned-view map (0 = no pin)
|
|
53
76
|
|
|
54
77
|
constructor(handle: MlxHandle) {
|
|
55
78
|
this.#handle = handle;
|
|
56
79
|
registry.register(this, handle, this.#token);
|
|
57
80
|
}
|
|
58
81
|
|
|
59
|
-
/** Zero-copy: wrap an existing buffer
|
|
60
|
-
*
|
|
82
|
+
/** Zero-copy: wrap an existing JS buffer. The view is pinned (GC-rooted)
|
|
83
|
+
* in a process-side map; mlx gets the NATIVE no-op dtor (free(NULL)) —
|
|
84
|
+
* never a JS callback, so the last release may happen on the Metal
|
|
85
|
+
* completion thread without deadlock (see hazard note above). Buffer
|
|
86
|
+
* lifetime is therefore the CALLER's contract:
|
|
87
|
+
* - mmap-backed views (toArrayBuffer aliases): the mapping owns the
|
|
88
|
+
* memory; prefer fromPointer, which skips the pin entirely.
|
|
89
|
+
* - JS-heap buffers: the pin keeps the buffer alive; call
|
|
90
|
+
* unpinHostBuffer() only after a JS-side sync point proves mlx is
|
|
91
|
+
* done with it (everything depending on the array evaluated AND the
|
|
92
|
+
* array disposed), or never (process-lifetime, e.g. short scripts). */
|
|
61
93
|
static fromView(view: Uint8Array, shape: number[], dtype: Dtype): MlxArray {
|
|
62
94
|
const sb = shapeBuf(shape);
|
|
63
95
|
const id = nextPinId++;
|
|
64
96
|
pinned.set(id, view);
|
|
65
97
|
const handle = C.mlx_array_new_data_managed_payload(
|
|
66
|
-
ptr(view), ptr(sb), shape.length, dtype,
|
|
98
|
+
ptr(view), ptr(sb), shape.length, dtype, 0, freeFnAddr,
|
|
67
99
|
);
|
|
68
|
-
|
|
100
|
+
const arr = new MlxArray(handle);
|
|
101
|
+
arr.#pinId = id;
|
|
102
|
+
return arr;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Release the GC root fromView placed on this array's host buffer.
|
|
106
|
+
* JS-thread-only by construction (it's a plain method) — this is the
|
|
107
|
+
* replacement for the deadlocking JSCallback unpin dtor. Idempotent;
|
|
108
|
+
* no-op for non-fromView arrays. Caller contract: only call once mlx can
|
|
109
|
+
* no longer touch the buffer (see fromView doc), or when the memory is
|
|
110
|
+
* owned elsewhere (an mmap kept mapped past every use). */
|
|
111
|
+
unpinHostBuffer(): void {
|
|
112
|
+
if (this.#pinId !== 0) {
|
|
113
|
+
pinned.delete(this.#pinId);
|
|
114
|
+
this.#pinId = 0;
|
|
115
|
+
}
|
|
69
116
|
}
|
|
70
117
|
|
|
71
118
|
/** Zero-copy: wrap a raw pointer (e.g. into an mmap'd weight shard).
|
|
72
|
-
* The caller guarantees the memory outlives
|
|
73
|
-
*
|
|
119
|
+
* The caller guarantees the memory outlives EVERY mlx
|
|
120
|
+
* reference to the array — including GPU command buffers that retain the
|
|
121
|
+
* buffer past dispose() — i.e. mmaps backing these arrays stay mapped for
|
|
122
|
+
* the process. The dtor is free(NULL) (payload 0): native + no-op, so
|
|
123
|
+
* mlx may run it from the Metal completion thread without touching the
|
|
124
|
+
* JS runtime (the old JSCallback dtor deadlocked serving / SIGTRAPed
|
|
125
|
+
* under GC when that thread held the last reference — 2026-07-06). */
|
|
74
126
|
static fromPointer(dataPtr: number, shape: number[], dtype: Dtype): MlxArray {
|
|
75
127
|
const sb = shapeBuf(shape);
|
|
76
128
|
const handle = C.mlx_array_new_data_managed_payload(
|
|
77
|
-
dataPtr, ptr(sb), shape.length, dtype, 0,
|
|
129
|
+
dataPtr, ptr(sb), shape.length, dtype, 0, freeFnAddr,
|
|
78
130
|
);
|
|
79
131
|
return new MlxArray(handle);
|
|
80
132
|
}
|
|
@@ -108,6 +160,27 @@ export class MlxArray {
|
|
|
108
160
|
);
|
|
109
161
|
}
|
|
110
162
|
|
|
163
|
+
/** Graph-free LEAF with this array's exact bytes — ONE copy, straight from
|
|
164
|
+
* the (evaluated) mlx buffer into a fresh mlx-owned array, no JS-heap
|
|
165
|
+
* intermediate (`fromBytesCopy(rawBytes())` is TWO copies: device→JS heap
|
|
166
|
+
* →new leaf). mlx_array_new_data copies synchronously, so the source only
|
|
167
|
+
* has to outlive this call. The buffer is read LINEARLY — the caller must
|
|
168
|
+
* pass a row-major contiguous array (ops.contiguous first for views), same
|
|
169
|
+
* contract as rawBytes. Segmented-backward boundary detach hot path. */
|
|
170
|
+
detachCopy(): MlxArray {
|
|
171
|
+
this.eval();
|
|
172
|
+
const dt = this.dtype;
|
|
173
|
+
const p =
|
|
174
|
+
dt === Dtype.float32 ? C.mlx_array_data_float32(this.handle)
|
|
175
|
+
: dt === Dtype.float16 ? C.mlx_array_data_float16(this.handle)
|
|
176
|
+
: dt === Dtype.bfloat16 ? C.mlx_array_data_bfloat16(this.handle)
|
|
177
|
+
: dt === Dtype.uint32 ? C.mlx_array_data_uint32(this.handle)
|
|
178
|
+
: null;
|
|
179
|
+
if (p === null) throw new Error(`detachCopy: unsupported dtype ${this.dtypeName}`);
|
|
180
|
+
const sb = shapeBuf(this.shape);
|
|
181
|
+
return new MlxArray(C.mlx_array_new_data(p as never, ptr(sb), this.ndim, dt));
|
|
182
|
+
}
|
|
183
|
+
|
|
111
184
|
get handle(): MlxHandle {
|
|
112
185
|
if (this.#disposed) throw new Error("MlxArray used after dispose");
|
|
113
186
|
return this.#handle;
|
|
@@ -117,7 +190,7 @@ export class MlxArray {
|
|
|
117
190
|
return Number(C.mlx_array_ndim(this.handle));
|
|
118
191
|
}
|
|
119
192
|
|
|
120
|
-
// NOTE on the DFG stale-read bug (repro/bun-ffi-f64/ISSUE.md): the
|
|
193
|
+
// NOTE on the DFG stale-read bug (lab/repro/bun-ffi-f64/ISSUE.md): the
|
|
121
194
|
// toArrayBuffer readbacks below (shape/rawBytes/toFloat32) are safe.
|
|
122
195
|
// The hazard is reading a *pre-existing* typed array after an FFI call
|
|
123
196
|
// wrote through its pointer — the JIT forwards a stale value from before
|
|
@@ -162,6 +235,18 @@ export class MlxArray {
|
|
|
162
235
|
|
|
163
236
|
/** Raw bytes of the evaluated array (copy). bf16/f16/f32 only. */
|
|
164
237
|
rawBytes(): Uint8Array {
|
|
238
|
+
return this.rawBytesView().slice();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** ZERO-COPY view of the evaluated array's bytes — aliases the mlx
|
|
242
|
+
* buffer directly, no JS-heap copy. Valid only while THIS array is
|
|
243
|
+
* alive and unmutated: do not retain past dispose(). Built for
|
|
244
|
+
* bulk-write paths (kv-store's streamed writer) where per-tensor
|
|
245
|
+
* rawBytes() copies pile up in the JS heap faster than GC collects
|
|
246
|
+
* them — the residual A7 whole-entry RSS spike (2026-07-07). The
|
|
247
|
+
* array must be contiguous (ops.contiguous first for views), same
|
|
248
|
+
* contract as rawBytes. */
|
|
249
|
+
rawBytesView(): Uint8Array {
|
|
165
250
|
this.eval();
|
|
166
251
|
const dt = this.dtype;
|
|
167
252
|
const p =
|
|
@@ -169,9 +254,32 @@ export class MlxArray {
|
|
|
169
254
|
: dt === Dtype.float16 ? C.mlx_array_data_float16(this.handle)
|
|
170
255
|
: dt === Dtype.bfloat16 ? C.mlx_array_data_bfloat16(this.handle)
|
|
171
256
|
: dt === Dtype.uint32 ? C.mlx_array_data_uint32(this.handle)
|
|
257
|
+
: dt === Dtype.uint8 ? C.mlx_array_data_uint8(this.handle)
|
|
258
|
+
: dt === Dtype.int8 ? C.mlx_array_data_int8(this.handle)
|
|
172
259
|
: null;
|
|
173
|
-
if (p === null) throw new Error(`
|
|
174
|
-
return new Uint8Array(toArrayBuffer(p!, 0, this.nbytes))
|
|
260
|
+
if (p === null) throw new Error(`rawBytesView: unsupported dtype ${this.dtypeName}`);
|
|
261
|
+
return new Uint8Array(toArrayBuffer(p!, 0, this.nbytes));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Read back an INTEGER array without enqueueing a cast kernel: eval +
|
|
265
|
+
* direct buffer read for uint32/int32. A cast (`astype`) is a NEW op that
|
|
266
|
+
* queues BEHIND everything already dispatched on the GPU stream — on the
|
|
267
|
+
* batched decode hot path, reading the pipeline register via toFloat32
|
|
268
|
+
* stalled the "overlapped" token read for a FULL step of GPU work every
|
|
269
|
+
* token (the core of the Phase-0 B=1 gap; unified-engine plan Phase 2).
|
|
270
|
+
* Non-integer dtypes fall back to the rounded float readback. */
|
|
271
|
+
toIntTokens(): number[] {
|
|
272
|
+
if (this.dtype === Dtype.uint32) {
|
|
273
|
+
this.eval();
|
|
274
|
+
const p = C.mlx_array_data_uint32(this.handle);
|
|
275
|
+
return [...new Uint32Array(toArrayBuffer(p!, 0, this.size * 4))];
|
|
276
|
+
}
|
|
277
|
+
if (this.dtype === Dtype.int32) {
|
|
278
|
+
this.eval();
|
|
279
|
+
const p = C.mlx_array_data_int32(this.handle);
|
|
280
|
+
return [...new Int32Array(toArrayBuffer(p!, 0, this.size * 4))];
|
|
281
|
+
}
|
|
282
|
+
return [...this.toFloat32()].map((x) => Math.round(x));
|
|
175
283
|
}
|
|
176
284
|
|
|
177
285
|
/** Read back as float32 (casts on GPU if needed, then copies out). */
|
package/src/mlx/autograd.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Native autograd via mlx-c value_and_grad over a JS loss closure.
|
|
2
2
|
//
|
|
3
|
-
// Encapsulates the exact FFI lifecycle proven in spikes/phase-train-vag.ts:
|
|
3
|
+
// Encapsulates the exact FFI lifecycle proven in lab/spikes/phase-train-vag.ts:
|
|
4
4
|
// JS loss closure (JSCallback) → mlx_closure_new_func_payload →
|
|
5
5
|
// mlx_value_and_grad → mlx_closure_value_and_grad_apply → read grads.
|
|
6
6
|
//
|
|
@@ -45,7 +45,7 @@ export class ValueAndGrad {
|
|
|
45
45
|
// Read the primals out of the input vector_array. mlx_vector_array_get
|
|
46
46
|
// returns a fresh (ref-counted) handle, so disposing the wrapper
|
|
47
47
|
// afterward is correct — it drops our reference, not mlx's graph
|
|
48
|
-
// reference (proven in spikes/phase-train-vag.ts).
|
|
48
|
+
// reference (proven in lab/spikes/phase-train-vag.ts).
|
|
49
49
|
const n = Number(C.mlx_vector_array_size(inVec));
|
|
50
50
|
const primals: MlxArray[] = [];
|
|
51
51
|
for (let i = 0; i < n; i++) {
|
package/src/mlx/ffi.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { existsSync } from "node:fs";
|
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { nativePackDir } from "../native-pack";
|
|
17
17
|
|
|
18
|
-
const { ptr: P, i32, u64, f32, cstring } = FFIType;
|
|
18
|
+
const { ptr: P, i32, u64, f32, f64, cstring } = FFIType;
|
|
19
19
|
|
|
20
20
|
/** libmlxc resolution, in order: explicit env override → next to the
|
|
21
21
|
* executable (the `bun build --compile` sidecar layout, where
|
|
@@ -57,6 +57,9 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
57
57
|
mlx_array_data_float16: { args: [u64], returns: P },
|
|
58
58
|
mlx_array_data_bfloat16: { args: [u64], returns: P },
|
|
59
59
|
mlx_array_data_uint32: { args: [u64], returns: P },
|
|
60
|
+
mlx_array_data_int32: { args: [u64], returns: P },
|
|
61
|
+
mlx_array_data_uint8: { args: [u64], returns: P },
|
|
62
|
+
mlx_array_data_int8: { args: [u64], returns: P },
|
|
60
63
|
mlx_array_item_float32: { args: [P, u64], returns: i32 },
|
|
61
64
|
// streams
|
|
62
65
|
mlx_default_gpu_stream_new: { args: [], returns: u64 },
|
|
@@ -142,6 +145,7 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
142
145
|
mlx_negative: { args: [P, u64, u64], returns: i32 },
|
|
143
146
|
mlx_argsort_axis: { args: [P, u64, i32, u64], returns: i32 },
|
|
144
147
|
mlx_argpartition_axis: { args: [P, u64, i32, i32, u64], returns: i32 },
|
|
148
|
+
mlx_arange: { args: [P, f64, f64, f64, i32, u64], returns: i32 },
|
|
145
149
|
mlx_take_along_axis: { args: [P, u64, u64, i32, u64], returns: i32 },
|
|
146
150
|
mlx_put_along_axis: { args: [P, u64, u64, u64, i32, u64], returns: i32 },
|
|
147
151
|
mlx_cumsum: { args: [P, u64, i32, FFIType.bool, FFIType.bool, u64], returns: i32 },
|
|
@@ -169,7 +173,17 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
169
173
|
// (res, input, weight, stride, padding, dilation, groups, stream) — depthwise
|
|
170
174
|
// causal conv1d for Qwen3.5 gated-DeltaNet (groups == channels, padding 0).
|
|
171
175
|
mlx_conv1d: { args: [P, u64, u64, i32, i32, i32, i32, u64], returns: i32 },
|
|
172
|
-
//
|
|
176
|
+
// C signature: (res, input, weight, stride_0, stride_1, padding_0,
|
|
177
|
+
// padding_1, dilation_0, dilation_1, groups, stream) — NHWC conv2d for the
|
|
178
|
+
// gemma-4 audio SSCP subsampler.
|
|
179
|
+
// HAZARD (lab/repro/bun-ffi-stack-args): Apple arm64 packs sub-8-byte STACK
|
|
180
|
+
// args at natural size (dilation_1@sp+0, groups@sp+4, stream@sp+8) but
|
|
181
|
+
// bun:ffi 1.3.14 writes one 8-byte slot per arg — the callee reads shifted
|
|
182
|
+
// garbage and mlx segfaults on a bogus stream. Workaround: declare the two
|
|
183
|
+
// stack ints as ONE u64 (dilation_1 | groups<<32) so Bun's slot layout
|
|
184
|
+
// byte-matches the Apple ABI. ops.conv2d does the packing.
|
|
185
|
+
mlx_conv2d: { args: [P, u64, u64, i32, i32, i32, i32, i32, u64, u64], returns: i32 },
|
|
186
|
+
// --- training: autograd (value_and_grad) — proven in lab/spikes/phase-train-vag.ts.
|
|
173
187
|
// mlx_value_and_grad(res: mlx_closure_value_and_grad*, fun: mlx_closure,
|
|
174
188
|
// argnums: int*, n) → builds a value+grad closure differentiating the
|
|
175
189
|
// selected inputs of `fun` (a mlx_closure_new_func_payload over a JS loss).
|
|
@@ -260,6 +274,16 @@ export const C = dlopen(LIBMLXC_PATH, {
|
|
|
260
274
|
// GPU trace capture (Phase E step 2: size the prize before the kernel)
|
|
261
275
|
mlx_metal_start_capture: { args: [cstring], returns: i32 },
|
|
262
276
|
mlx_metal_stop_capture: { args: [], returns: i32 },
|
|
277
|
+
// TurboQuant KV codec (Phase 13): FWHT rotation + per-group affine/Lloyd-Max
|
|
278
|
+
// (res, a, opt scale, stream) — 1/sqrt(dim) normalization when scale is null.
|
|
279
|
+
mlx_hadamard_transform: { args: [P, u64, u64, u64], returns: i32 },
|
|
280
|
+
mlx_min_axis: { args: [P, u64, i32, FFIType.bool, u64], returns: i32 },
|
|
281
|
+
mlx_round: { args: [P, u64, i32, u64], returns: i32 },
|
|
282
|
+
mlx_greater: { args: [P, u64, u64, u64], returns: i32 },
|
|
283
|
+
mlx_bitwise_and: { args: [P, u64, u64, u64], returns: i32 },
|
|
284
|
+
mlx_bitwise_or: { args: [P, u64, u64, u64], returns: i32 },
|
|
285
|
+
mlx_left_shift: { args: [P, u64, u64, u64], returns: i32 },
|
|
286
|
+
mlx_right_shift: { args: [P, u64, u64, u64], returns: i32 },
|
|
263
287
|
}).symbols;
|
|
264
288
|
|
|
265
289
|
export type MlxHandle = bigint;
|
|
@@ -329,7 +353,7 @@ export const NULL_HANDLE = 0n;
|
|
|
329
353
|
// RULE: never read a typed array that a bun:ffi call wrote through a
|
|
330
354
|
// pointer — once the calling function is DFG-compiled, the JIT eliminates
|
|
331
355
|
// the load across the native call and `buf[i]` returns stale values
|
|
332
|
-
// (repro/bun-ffi-f64/ISSUE.md; PLAN.md Phase 4 findings). Out-param slots
|
|
356
|
+
// (lab/repro/bun-ffi-f64/ISSUE.md; PLAN.md Phase 4 findings). Out-param slots
|
|
333
357
|
// must be read back with bun:ffi `read.*` (verified safe). Initializing the
|
|
334
358
|
// slot via the typed-array *constructor* is fine: that store happens in
|
|
335
359
|
// host code the JIT can't elide.
|
package/src/mlx/metal-kernel.ts
CHANGED
|
@@ -89,15 +89,18 @@ export class MetalKernel {
|
|
|
89
89
|
C.mlx_fast_metal_kernel_config_set_thread_group(cfg, call.threadGroup[0], call.threadGroup[1], call.threadGroup[2]);
|
|
90
90
|
if (call.initValue !== undefined)
|
|
91
91
|
C.mlx_fast_metal_kernel_config_set_init_value(cfg, call.initValue);
|
|
92
|
-
|
|
92
|
+
// Dtypes-first, then ints, to MATCH mlx's own specialization-name order
|
|
93
|
+
// (mlx-lm's gated_delta_step passes [("InT",…),("StT",…),("Dk",…),…]).
|
|
94
|
+
// Order affects only the generated name/cache-key, not binding (named args).
|
|
95
|
+
for (const [k, v] of Object.entries(call.templateDtypes ?? {})) {
|
|
93
96
|
const kb = Buffer.from(k + "\0", "utf8");
|
|
94
97
|
keepAlive.push(kb);
|
|
95
|
-
C.
|
|
98
|
+
C.mlx_fast_metal_kernel_config_add_template_arg_dtype(cfg, ptr(kb), v);
|
|
96
99
|
}
|
|
97
|
-
for (const [k, v] of Object.entries(call.
|
|
100
|
+
for (const [k, v] of Object.entries(call.templateInts ?? {})) {
|
|
98
101
|
const kb = Buffer.from(k + "\0", "utf8");
|
|
99
102
|
keepAlive.push(kb);
|
|
100
|
-
C.
|
|
103
|
+
C.mlx_fast_metal_kernel_config_add_template_arg_int(cfg, ptr(kb), v);
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
const handles = new BigUint64Array(inputs.map((a) => a.handle));
|