mlx-bun 0.0.10 → 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 +58 -35
- 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 +14 -6
- 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 +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -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/dspark-handoff.md +182 -16
- 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.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 +250 -121
- package/docs/reference/training.md +14 -15
- 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 +356 -105
- 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 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- 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 +106 -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 +153 -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 +363 -39
- 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 +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- 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 +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- 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 +1380 -135
- 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 +107 -35
- 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 +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- 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
|
}
|
|
@@ -138,7 +190,7 @@ export class MlxArray {
|
|
|
138
190
|
return Number(C.mlx_array_ndim(this.handle));
|
|
139
191
|
}
|
|
140
192
|
|
|
141
|
-
// 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
|
|
142
194
|
// toArrayBuffer readbacks below (shape/rawBytes/toFloat32) are safe.
|
|
143
195
|
// The hazard is reading a *pre-existing* typed array after an FFI call
|
|
144
196
|
// wrote through its pointer — the JIT forwards a stale value from before
|
|
@@ -183,6 +235,18 @@ export class MlxArray {
|
|
|
183
235
|
|
|
184
236
|
/** Raw bytes of the evaluated array (copy). bf16/f16/f32 only. */
|
|
185
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 {
|
|
186
250
|
this.eval();
|
|
187
251
|
const dt = this.dtype;
|
|
188
252
|
const p =
|
|
@@ -190,9 +254,32 @@ export class MlxArray {
|
|
|
190
254
|
: dt === Dtype.float16 ? C.mlx_array_data_float16(this.handle)
|
|
191
255
|
: dt === Dtype.bfloat16 ? C.mlx_array_data_bfloat16(this.handle)
|
|
192
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)
|
|
193
259
|
: null;
|
|
194
|
-
if (p === null) throw new Error(`
|
|
195
|
-
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));
|
|
196
283
|
}
|
|
197
284
|
|
|
198
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));
|
package/src/mlx/ops.ts
CHANGED
|
@@ -43,10 +43,39 @@ export function sigmoid(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
|
43
43
|
return new MlxArray(outArray("sigmoid", (o) => C.mlx_sigmoid(o, a.handle, s)));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
46
|
+
/** f32 → bf16 truncation with round-to-nearest-even, returning the 2
|
|
47
|
+
* little-endian bytes of the bf16 value. Matches mlx's
|
|
48
|
+
* `static_cast<bfloat16_t>(float)` bit-for-bit so a host-built bf16 scalar is
|
|
49
|
+
* numerically identical to `f32_scalar.astype(bf16)` — just without the GPU
|
|
50
|
+
* Copy (astype) kernel. */
|
|
51
|
+
function bf16Bytes(value: number): Uint8Array {
|
|
52
|
+
const fb = new Float32Array([value]);
|
|
53
|
+
const bits = new Uint32Array(fb.buffer)[0]!;
|
|
54
|
+
let hi16: number;
|
|
55
|
+
if (((bits >>> 23) & 0xff) === 0xff) {
|
|
56
|
+
// inf / nan: keep the high 16 bits, and keep a NaN a (quiet) NaN.
|
|
57
|
+
hi16 = (bits >>> 16) & 0xffff;
|
|
58
|
+
if ((bits & 0x7fffff) !== 0) hi16 |= 0x40;
|
|
59
|
+
} else {
|
|
60
|
+
const roundingBias = 0x7fff + ((bits >>> 16) & 1); // round to nearest even
|
|
61
|
+
hi16 = ((bits + roundingBias) >>> 16) & 0xffff;
|
|
62
|
+
}
|
|
63
|
+
return new Uint8Array([hi16 & 0xff, (hi16 >>> 8) & 0xff]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Scalar constant with the same dtype as `like` (python weak-scalar semantics).
|
|
67
|
+
* Built HOST-SIDE at the target dtype — mlx's own scalar→array path constructs
|
|
68
|
+
* a bf16/f32 constant directly, so `scalar * bf16_array` is a single multiply.
|
|
69
|
+
* Going through an f32 leaf + GPU `.astype(bf16)` (the old path) emits a
|
|
70
|
+
* separate Copy kernel (`v_copy…`) that the reference never dispatches. */
|
|
47
71
|
export function scalarLike(value: number, like: MlxArray): MlxArray {
|
|
72
|
+
if (like.dtype === Dtype.float32)
|
|
73
|
+
return MlxArray.fromFloat32(new Float32Array([value]), []);
|
|
74
|
+
if (like.dtype === Dtype.bfloat16)
|
|
75
|
+
return MlxArray.fromBytesCopy(bf16Bytes(value), [], Dtype.bfloat16);
|
|
76
|
+
// f16 / other float dtypes: keep the astype path (rare here; no bit-exact JS
|
|
77
|
+
// converter written for them yet).
|
|
48
78
|
const f = MlxArray.fromFloat32(new Float32Array([value]), []);
|
|
49
|
-
if (like.dtype === Dtype.float32) return f;
|
|
50
79
|
const cast = f.astype(like.dtype);
|
|
51
80
|
f.dispose();
|
|
52
81
|
return cast;
|
|
@@ -147,6 +176,25 @@ export function ropeDynamic(
|
|
|
147
176
|
);
|
|
148
177
|
}
|
|
149
178
|
|
|
179
|
+
/** Array-offset fast::rope with every knob exposed (traditional + scale) —
|
|
180
|
+
* the dynamic twin of `ropeScaled`, for the universal rope factory's
|
|
181
|
+
* batched-decode path (per-row [B] offsets under left-padding). Same
|
|
182
|
+
* kernel as `ropeDynamic`; static/dynamic bit-exactness is asserted in
|
|
183
|
+
* tests/compile.test.ts. */
|
|
184
|
+
export function ropeScaledDynamic(
|
|
185
|
+
x: MlxArray, dims: number, traditional: boolean, base: number | null,
|
|
186
|
+
scale: number, offset: MlxArray, freqs: MlxArray | null, s: S = gpuStream,
|
|
187
|
+
): MlxArray {
|
|
188
|
+
return new MlxArray(
|
|
189
|
+
outArray("fast_rope_dynamic", (o) =>
|
|
190
|
+
C.mlx_fast_rope_dynamic(
|
|
191
|
+
o, x.handle, dims, traditional, optFloat(base), scale, offset.handle,
|
|
192
|
+
freqs?.handle ?? 0n, s,
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
150
198
|
export function sdpa(
|
|
151
199
|
q: MlxArray, k: MlxArray, v: MlxArray, scale: number,
|
|
152
200
|
maskMode: "" | "causal" | "array", maskArr: MlxArray | null = null,
|
|
@@ -292,21 +340,35 @@ export function pow(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
|
292
340
|
return new MlxArray(outArray("power", (o) => C.mlx_power(o, a.handle, b.handle, s)));
|
|
293
341
|
}
|
|
294
342
|
|
|
295
|
-
/** nn.gelu_approx: 0.5 x (1 + tanh(√(2/π) (x + 0.044715 x³))) — composed
|
|
296
|
-
* exactly as mlx's python source: x**3 is mx.power
|
|
297
|
-
* round differently in bf16),
|
|
343
|
+
/** nn.gelu_approx: 0.5 * x * (1 + tanh(√(2/π) * (x + 0.044715 * x³))) — composed
|
|
344
|
+
* exactly as mlx's python source, INCLUDING operand order: x**3 is mx.power
|
|
345
|
+
* (NOT x·x·x — they round differently in bf16), and every scalar·array product
|
|
346
|
+
* puts the SCALAR FIRST (`0.5 * x`, not `x * 0.5`). Multiply is commutative so
|
|
347
|
+
* this is bit-identical, but it makes the compiled geglu graph's operand string
|
|
348
|
+
* (`Multiply(C, V)`) match the oracle's fused kernel exactly. Scalars promote
|
|
349
|
+
* weakly to x's dtype. */
|
|
298
350
|
export function geluApprox(x: MlxArray, s: S = gpuStream): MlxArray {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
351
|
+
// EXACT copy of mlx's graph for
|
|
352
|
+
// 0.5 * x * (1 + tanh(√(2/π) * (x + 0.044715 * x**3)))
|
|
353
|
+
// as dumped via mx.export_to_dot. mlx's `__rmul__`/`__radd__` canonicalize
|
|
354
|
+
// every `scalar OP array` to `array OP scalar`, so every commutative product/
|
|
355
|
+
// sum below is ARRAY-FIRST, constant-second (NOT scalar-first). Build order
|
|
356
|
+
// follows Python left-to-right (`0.5 * x` first). Same libmlx compiles this to
|
|
357
|
+
// the identical fused kernel.
|
|
358
|
+
const cHalf = scalarLike(0.5, x);
|
|
359
|
+
const halfx = mul(x, cHalf, s); // Multiply(x, 0.5)
|
|
360
|
+
const cThree = scalarLike(3, x);
|
|
361
|
+
const x3 = pow(x, cThree, s); // Power(x, 3)
|
|
362
|
+
const cCoef = scalarLike(0.044715, x);
|
|
363
|
+
const cx3 = mul(x3, cCoef, s); // Multiply(x**3, 0.044715)
|
|
364
|
+
const inner = add(x, cx3, s); // Add(x, cx3)
|
|
365
|
+
const cSqrt = scalarLike(Math.sqrt(2 / Math.PI), x);
|
|
366
|
+
const scaled = mul(inner, cSqrt, s); // Multiply(inner, √(2/π))
|
|
304
367
|
const t = tanh(scaled, s);
|
|
305
|
-
const
|
|
306
|
-
const t1 = add(
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
for (const a of [three, x3, cx3, inner, scaled, t, one, t1, halfx]) a.dispose();
|
|
368
|
+
const cOne = scalarLike(1, x);
|
|
369
|
+
const t1 = add(t, cOne, s); // Add(tanh, 1)
|
|
370
|
+
const out = mul(halfx, t1, s); // Multiply(halfx, 1+tanh)
|
|
371
|
+
for (const a of [cHalf, halfx, cThree, x3, cCoef, cx3, inner, cSqrt, scaled, t, cOne, t1]) a.dispose();
|
|
310
372
|
return out;
|
|
311
373
|
}
|
|
312
374
|
|
|
@@ -402,7 +464,7 @@ export function where(cond: MlxArray, x: MlxArray, y: MlxArray, s: S = gpuStream
|
|
|
402
464
|
|
|
403
465
|
// arange is built host-side and uploaded. The original mlx_arange binding
|
|
404
466
|
// broke once the calling path got JIT-optimized; a standalone repro
|
|
405
|
-
// (repro/bun-ffi-f64/) traced the root cause to a Bun bug: after DFG
|
|
467
|
+
// (lab/repro/bun-ffi-f64/) traced the root cause to a Bun bug: after DFG
|
|
406
468
|
// tier-up, typed-array reads following a bun:ffi call return stale values
|
|
407
469
|
// (the JIT eliminates the load across the native call). Not f64 marshaling
|
|
408
470
|
// — args reach C intact. See PLAN.md Phase 4 findings. Large constant
|
|
@@ -418,13 +480,18 @@ export function arange(start: number, stop: number, step: number, dtype: Dtype,
|
|
|
418
480
|
const cached = arangeCache.get(key);
|
|
419
481
|
if (cached) return cached.slice([0], [n], s);
|
|
420
482
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
arr.
|
|
427
|
-
|
|
483
|
+
let arr: MlxArray;
|
|
484
|
+
if (dtype === Dtype.int32) {
|
|
485
|
+
// host-built int32 leaf (the Bun-f64-FFI-safe path — see note above).
|
|
486
|
+
const data = new Int32Array(n);
|
|
487
|
+
for (let i = 0; i < n; i++) data[i] = start + i * step;
|
|
488
|
+
arr = MlxArray.fromInt32(data, [n]);
|
|
489
|
+
} else {
|
|
490
|
+
// Native mlx Arange in the target dtype — matches mlx's `Arange` op exactly.
|
|
491
|
+
// The old `fromInt32 → .astype(dtype)` emitted a phantom int32→float Copy
|
|
492
|
+
// (an AsType the oracle never dispatches — e.g. the proportional-RoPE freq
|
|
493
|
+
// exponents). Integer start/step passed as doubles are exact.
|
|
494
|
+
arr = new MlxArray(outArray("arange", (o) => C.mlx_arange(o, start, stop, step, dtype, s)));
|
|
428
495
|
}
|
|
429
496
|
if (n >= ARANGE_CACHE_MIN) {
|
|
430
497
|
arangeCache.set(key, arr);
|
|
@@ -811,6 +878,26 @@ export function conv1d(
|
|
|
811
878
|
);
|
|
812
879
|
}
|
|
813
880
|
|
|
881
|
+
/** nn.Conv2d: input [B, H, W, C_in] (NHWC), weight [C_out, kH, kW, C_in/groups]
|
|
882
|
+
* → [B, H', W', C_out]. mlx weight layout — the oracle transposes PyTorch's
|
|
883
|
+
* [C_out, C_in, kH, kW] at load. For the gemma-4 audio SSCP subsampler.
|
|
884
|
+
* dilation_1 and groups ride in one packed u64 — see the mlx_conv2d hazard
|
|
885
|
+
* note in ffi.ts (bun:ffi stack-arg ABI bug). */
|
|
886
|
+
export function conv2d(
|
|
887
|
+
input: MlxArray, weight: MlxArray, stride: [number, number] = [1, 1],
|
|
888
|
+
padding: [number, number] = [0, 0], dilation: [number, number] = [1, 1],
|
|
889
|
+
groups = 1, s: S = gpuStream,
|
|
890
|
+
): MlxArray {
|
|
891
|
+
const dilation1AndGroups =
|
|
892
|
+
(BigInt(dilation[1] >>> 0) & 0xffffffffn) | (BigInt(groups >>> 0) << 32n);
|
|
893
|
+
return new MlxArray(
|
|
894
|
+
outArray("conv2d", (o) =>
|
|
895
|
+
C.mlx_conv2d(o, input.handle, weight.handle, stride[0], stride[1],
|
|
896
|
+
padding[0], padding[1], dilation[0], dilation1AndGroups, s),
|
|
897
|
+
),
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
814
901
|
/** mx.split(a, indices, axis): split at the given boundary offsets along `axis`
|
|
815
902
|
* into N+1 contiguous slices (mirrors numpy/mlx split-by-indices). */
|
|
816
903
|
export function split(a: MlxArray, indices: number[], axis: number): MlxArray[] {
|
|
@@ -844,3 +931,46 @@ export function silu(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
|
844
931
|
sig.dispose();
|
|
845
932
|
return r;
|
|
846
933
|
}
|
|
934
|
+
|
|
935
|
+
/** fast::hadamard_transform over the last axis. `scale` defaults to
|
|
936
|
+
* 1/sqrt(dim) (mlx's own default when unset) — pass it explicitly to
|
|
937
|
+
* match a reference that always states its normalization, per
|
|
938
|
+
* turboquant-ops.ts's fwht(). */
|
|
939
|
+
export function hadamardTransform(x: MlxArray, scale: number | null = null, s: S = gpuStream): MlxArray {
|
|
940
|
+
return new MlxArray(
|
|
941
|
+
outArray("hadamard_transform", (o) =>
|
|
942
|
+
C.mlx_hadamard_transform(o, x.handle, optFloat(scale), s),
|
|
943
|
+
),
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
export function minAxis(a: MlxArray, axis: number, keepdims: boolean, s: S = gpuStream): MlxArray {
|
|
948
|
+
return new MlxArray(
|
|
949
|
+
outArray("min_axis", (o) => C.mlx_min_axis(o, a.handle, axis, keepdims, s)),
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** mx.round(x, decimals=0) — round-half-to-even at the ULP grid mlx uses. */
|
|
954
|
+
export function round(a: MlxArray, s: S = gpuStream): MlxArray {
|
|
955
|
+
return new MlxArray(outArray("round", (o) => C.mlx_round(o, a.handle, 0, s)));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export function greater(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
959
|
+
return new MlxArray(outArray("greater", (o) => C.mlx_greater(o, a.handle, b.handle, s)));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function bitwiseAnd(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
963
|
+
return new MlxArray(outArray("bitwise_and", (o) => C.mlx_bitwise_and(o, a.handle, b.handle, s)));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function bitwiseOr(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
967
|
+
return new MlxArray(outArray("bitwise_or", (o) => C.mlx_bitwise_or(o, a.handle, b.handle, s)));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function leftShift(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
971
|
+
return new MlxArray(outArray("left_shift", (o) => C.mlx_left_shift(o, a.handle, b.handle, s)));
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export function rightShift(a: MlxArray, b: MlxArray, s: S = gpuStream): MlxArray {
|
|
975
|
+
return new MlxArray(outArray("right_shift", (o) => C.mlx_right_shift(o, a.handle, b.handle, s)));
|
|
976
|
+
}
|