mlx-bun 0.0.4
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/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- package/src/weights.ts +91 -0
package/src/mlx/ffi.ts
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// Raw bun:ffi bindings to libmlxc.
|
|
2
|
+
//
|
|
3
|
+
// Calling convention (verified in Phase 0, see PLAN.md):
|
|
4
|
+
// - Every mlx-c handle is a one-pointer struct `{ void* ctx }` — passed and
|
|
5
|
+
// returned by value in a single register on arm64, so FFIType.u64 works.
|
|
6
|
+
// - Out-params (`mlx_array* res`) are a pointer to that slot: allocate a
|
|
7
|
+
// BigUint64Array(1), pass ptr(), reread after the call via read.u64 —
|
|
8
|
+
// NOT slot[0] (DFG stale-read bug, see rule above outArray).
|
|
9
|
+
// - Ops return int status; 0 = success.
|
|
10
|
+
// - mlx_optional_int is `{ int32 value; bool has_value }` = 8 bytes, by
|
|
11
|
+
// value in one register: value in bits 0..31, has_value at bit 32.
|
|
12
|
+
|
|
13
|
+
import { dlopen, FFIType, ptr, read } from "bun:ffi";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { nativePackDir } from "../native-pack";
|
|
17
|
+
|
|
18
|
+
const { ptr: P, i32, u64, f32, cstring } = FFIType;
|
|
19
|
+
|
|
20
|
+
/** libmlxc resolution, in order: explicit env override → next to the
|
|
21
|
+
* executable (the `bun build --compile` sidecar layout, where
|
|
22
|
+
* libmlxc.dylib + libmlx.dylib + mlx.metallib ship beside the binary —
|
|
23
|
+
* see docs/reference/embedding.md) → the downloaded native-pack cache
|
|
24
|
+
* (src/native-pack.ts; populated on first run by the CLI) → homebrew
|
|
25
|
+
* (arm64, then Intel prefix). Keep in sync with nativeRuntimeDir(). */
|
|
26
|
+
function resolveLibmlxc(): string {
|
|
27
|
+
const env = process.env.MLX_BUN_LIBMLXC;
|
|
28
|
+
if (env) return env;
|
|
29
|
+
const beside = join(dirname(process.execPath), "libmlxc.dylib");
|
|
30
|
+
if (existsSync(beside)) return beside;
|
|
31
|
+
const cached = join(nativePackDir(), "libmlxc.dylib");
|
|
32
|
+
if (existsSync(cached)) return cached;
|
|
33
|
+
for (const p of ["/opt/homebrew/lib/libmlxc.dylib", "/usr/local/lib/libmlxc.dylib"])
|
|
34
|
+
if (existsSync(p)) return p;
|
|
35
|
+
// let dlopen produce the canonical error message for the default path
|
|
36
|
+
return "/opt/homebrew/lib/libmlxc.dylib";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const LIBMLXC_PATH = resolveLibmlxc();
|
|
40
|
+
|
|
41
|
+
export const C = dlopen(LIBMLXC_PATH, {
|
|
42
|
+
// array lifecycle
|
|
43
|
+
mlx_array_new: { args: [], returns: u64 },
|
|
44
|
+
mlx_array_new_data: { args: [P, P, i32, i32], returns: u64 },
|
|
45
|
+
mlx_array_new_data_managed_payload: { args: [P, P, i32, i32, P, P], returns: u64 },
|
|
46
|
+
mlx_array_free: { args: [u64], returns: i32 },
|
|
47
|
+
mlx_array_eval: { args: [u64], returns: i32 },
|
|
48
|
+
// array metadata
|
|
49
|
+
mlx_array_ndim: { args: [u64], returns: u64 },
|
|
50
|
+
mlx_array_shape: { args: [u64], returns: P },
|
|
51
|
+
mlx_array_dim: { args: [u64, i32], returns: i32 },
|
|
52
|
+
mlx_array_dtype: { args: [u64], returns: i32 },
|
|
53
|
+
mlx_array_size: { args: [u64], returns: u64 },
|
|
54
|
+
mlx_array_nbytes: { args: [u64], returns: u64 },
|
|
55
|
+
// data access (forces eval/copy to host-visible memory)
|
|
56
|
+
mlx_array_data_float32: { args: [u64], returns: P },
|
|
57
|
+
mlx_array_data_float16: { args: [u64], returns: P },
|
|
58
|
+
mlx_array_data_bfloat16: { args: [u64], returns: P },
|
|
59
|
+
mlx_array_data_uint32: { args: [u64], returns: P },
|
|
60
|
+
mlx_array_item_float32: { args: [P, u64], returns: i32 },
|
|
61
|
+
// streams
|
|
62
|
+
mlx_default_gpu_stream_new: { args: [], returns: u64 },
|
|
63
|
+
mlx_default_cpu_stream_new: { args: [], returns: u64 },
|
|
64
|
+
mlx_stream_free: { args: [u64], returns: i32 },
|
|
65
|
+
// eval control (both take mlx_vector_array)
|
|
66
|
+
mlx_eval: { args: [u64], returns: i32 },
|
|
67
|
+
mlx_async_eval: { args: [u64], returns: i32 },
|
|
68
|
+
// memory introspection / limits
|
|
69
|
+
mlx_get_active_memory: { args: [P], returns: i32 },
|
|
70
|
+
mlx_get_peak_memory: { args: [P], returns: i32 },
|
|
71
|
+
mlx_reset_peak_memory: { args: [], returns: i32 },
|
|
72
|
+
// mlx_device_info is a one-pointer struct like every other handle
|
|
73
|
+
mlx_device_info_new: { args: [], returns: u64 },
|
|
74
|
+
mlx_device_info_get: { args: [P, u64], returns: i32 },
|
|
75
|
+
mlx_device_info_free: { args: [u64], returns: i32 },
|
|
76
|
+
mlx_device_info_get_size: { args: [P, u64, cstring], returns: i32 },
|
|
77
|
+
mlx_get_default_device: { args: [P], returns: i32 },
|
|
78
|
+
mlx_device_free: { args: [u64], returns: i32 },
|
|
79
|
+
mlx_synchronize: { args: [u64], returns: i32 },
|
|
80
|
+
mlx_get_cache_memory: { args: [P], returns: i32 },
|
|
81
|
+
mlx_clear_cache: { args: [], returns: i32 },
|
|
82
|
+
mlx_set_memory_limit: { args: [P, u64], returns: i32 },
|
|
83
|
+
mlx_set_wired_limit: { args: [P, u64], returns: i32 },
|
|
84
|
+
// io: native lazy safetensors loader (Load primitive is CPU-only —
|
|
85
|
+
// always pass the CPU stream; GPU ops consume the unified buffers)
|
|
86
|
+
mlx_load_safetensors: { args: [P, P, cstring, u64], returns: i32 },
|
|
87
|
+
mlx_map_string_to_array_new: { args: [], returns: u64 },
|
|
88
|
+
mlx_map_string_to_array_free: { args: [u64], returns: i32 },
|
|
89
|
+
mlx_map_string_to_array_get: { args: [P, u64, cstring], returns: i32 },
|
|
90
|
+
mlx_map_string_to_string_new: { args: [], returns: u64 },
|
|
91
|
+
mlx_map_string_to_string_free: { args: [u64], returns: i32 },
|
|
92
|
+
// fast fused ops
|
|
93
|
+
// (res, x, weight may-be-null, eps, stream)
|
|
94
|
+
mlx_fast_rms_norm: { args: [P, u64, u64, f32, u64], returns: i32 },
|
|
95
|
+
// (res, x, weight may-be-null, bias may-be-null, eps, stream)
|
|
96
|
+
mlx_fast_layer_norm: { args: [P, u64, u64, u64, f32, u64], returns: i32 },
|
|
97
|
+
mlx_matmul: { args: [P, u64, u64, u64], returns: i32 },
|
|
98
|
+
mlx_contiguous: { args: [P, u64, FFIType.bool, u64], returns: i32 },
|
|
99
|
+
// (res: mlx_vector_array*, w, opt group_size, opt bits, mode, global_scale, stream)
|
|
100
|
+
mlx_quantize: { args: [P, u64, u64, u64, cstring, u64, u64], returns: i32 },
|
|
101
|
+
mlx_vector_array_get: { args: [P, u64, u64], returns: i32 },
|
|
102
|
+
mlx_vector_array_new: { args: [], returns: u64 },
|
|
103
|
+
mlx_softmax_axis: { args: [P, u64, i32, FFIType.bool, u64], returns: i32 },
|
|
104
|
+
mlx_max_axis: { args: [P, u64, i32, FFIType.bool, u64], returns: i32 },
|
|
105
|
+
mlx_sum_axis: { args: [P, u64, i32, FFIType.bool, u64], returns: i32 },
|
|
106
|
+
mlx_maximum: { args: [P, u64, u64, u64], returns: i32 },
|
|
107
|
+
mlx_expand_dims: { args: [P, u64, i32, u64], returns: i32 },
|
|
108
|
+
mlx_logical_or: { args: [P, u64, u64, u64], returns: i32 },
|
|
109
|
+
// (res, x, dims, traditional, opt base, scale, offset, freqs may-be-null, stream)
|
|
110
|
+
mlx_fast_rope: { args: [P, u64, i32, FFIType.bool, u64, f32, i32, u64, u64], returns: i32 },
|
|
111
|
+
// (res, q, k, v, scale, mask_mode, mask may-be-null, sinks may-be-null, stream)
|
|
112
|
+
mlx_fast_scaled_dot_product_attention: {
|
|
113
|
+
args: [P, u64, u64, u64, f32, cstring, u64, u64, u64], returns: i32,
|
|
114
|
+
},
|
|
115
|
+
// ops (grown as phases need them)
|
|
116
|
+
mlx_add: { args: [P, u64, u64, u64], returns: i32 },
|
|
117
|
+
mlx_subtract: { args: [P, u64, u64, u64], returns: i32 },
|
|
118
|
+
mlx_multiply: { args: [P, u64, u64, u64], returns: i32 },
|
|
119
|
+
mlx_divide: { args: [P, u64, u64, u64], returns: i32 },
|
|
120
|
+
mlx_tanh: { args: [P, u64, u64], returns: i32 },
|
|
121
|
+
mlx_sigmoid: { args: [P, u64, u64], returns: i32 },
|
|
122
|
+
mlx_power: { args: [P, u64, u64, u64], returns: i32 },
|
|
123
|
+
mlx_take_axis: { args: [P, u64, u64, i32, u64], returns: i32 },
|
|
124
|
+
mlx_reshape: { args: [P, u64, P, u64, u64], returns: i32 },
|
|
125
|
+
mlx_transpose_axes: { args: [P, u64, P, u64, u64], returns: i32 },
|
|
126
|
+
mlx_argmax_axis: { args: [P, u64, i32, FFIType.bool, u64], returns: i32 },
|
|
127
|
+
mlx_concatenate_axis: { args: [P, u64, i32, u64], returns: i32 },
|
|
128
|
+
mlx_vector_array_new_data: { args: [P, u64], returns: u64 },
|
|
129
|
+
mlx_vector_array_free: { args: [u64], returns: i32 },
|
|
130
|
+
mlx_array_item_uint32: { args: [P, u64], returns: i32 },
|
|
131
|
+
mlx_array_item_int32: { args: [P, u64], returns: i32 },
|
|
132
|
+
mlx_slice: { args: [P, u64, P, u64, P, u64, P, u64, u64], returns: i32 },
|
|
133
|
+
// (res, src, update, start*, n, stop*, n, strides*, n, stream)
|
|
134
|
+
mlx_slice_update: { args: [P, u64, u64, P, u64, P, u64, P, u64, u64], returns: i32 },
|
|
135
|
+
mlx_zeros: { args: [P, P, u64, i32, u64], returns: i32 },
|
|
136
|
+
mlx_exp: { args: [P, u64, u64], returns: i32 },
|
|
137
|
+
mlx_cos: { args: [P, u64, u64], returns: i32 },
|
|
138
|
+
mlx_sin: { args: [P, u64, u64], returns: i32 },
|
|
139
|
+
mlx_negative: { args: [P, u64, u64], returns: i32 },
|
|
140
|
+
mlx_argsort_axis: { args: [P, u64, i32, u64], returns: i32 },
|
|
141
|
+
mlx_argpartition_axis: { args: [P, u64, i32, i32, u64], returns: i32 },
|
|
142
|
+
mlx_take_along_axis: { args: [P, u64, u64, i32, u64], returns: i32 },
|
|
143
|
+
mlx_put_along_axis: { args: [P, u64, u64, u64, i32, u64], returns: i32 },
|
|
144
|
+
mlx_cumsum: { args: [P, u64, i32, FFIType.bool, FFIType.bool, u64], returns: i32 },
|
|
145
|
+
mlx_where: { args: [P, u64, u64, u64, u64], returns: i32 },
|
|
146
|
+
mlx_logsumexp_axis: { args: [P, u64, i32, FFIType.bool, u64], returns: i32 },
|
|
147
|
+
mlx_greater_equal: { args: [P, u64, u64, u64], returns: i32 },
|
|
148
|
+
mlx_less: { args: [P, u64, u64, u64], returns: i32 },
|
|
149
|
+
mlx_logical_and: { args: [P, u64, u64, u64], returns: i32 },
|
|
150
|
+
mlx_random_key: { args: [P, u64], returns: i32 },
|
|
151
|
+
mlx_random_categorical: { args: [P, u64, i32, u64, u64], returns: i32 },
|
|
152
|
+
mlx_astype: { args: [P, u64, i32, u64], returns: i32 },
|
|
153
|
+
// (res, w, scales, biases, opt group_size, opt bits, mode, global_scale, opt dtype, stream)
|
|
154
|
+
mlx_dequantize: { args: [P, u64, u64, u64, u64, u64, cstring, u64, u64, u64], returns: i32 },
|
|
155
|
+
mlx_quantized_matmul: { args: [P, u64, u64, u64, u64, FFIType.bool, u64, u64, cstring, u64], returns: i32 },
|
|
156
|
+
// (res, x, w, scales, biases?, lhs_indices?, rhs_indices?, transpose,
|
|
157
|
+
// opt group_size, opt bits, mode, sorted_indices, stream)
|
|
158
|
+
mlx_gather_qmm: { args: [P, u64, u64, u64, u64, u64, u64, FFIType.bool, u64, u64, cstring, FFIType.bool, u64], returns: i32 },
|
|
159
|
+
mlx_floor_divide: { args: [P, u64, u64, u64], returns: i32 },
|
|
160
|
+
// (res, input, weight, stride, padding, dilation, groups, stream) — depthwise
|
|
161
|
+
// causal conv1d for Qwen3.5 gated-DeltaNet (groups == channels, padding 0).
|
|
162
|
+
mlx_conv1d: { args: [P, u64, u64, i32, i32, i32, i32, u64], returns: i32 },
|
|
163
|
+
// --- training: autograd (value_and_grad) — proven in spikes/phase-train-vag.ts.
|
|
164
|
+
// mlx_value_and_grad(res: mlx_closure_value_and_grad*, fun: mlx_closure,
|
|
165
|
+
// argnums: int*, n) → builds a value+grad closure differentiating the
|
|
166
|
+
// selected inputs of `fun` (a mlx_closure_new_func_payload over a JS loss).
|
|
167
|
+
mlx_value_and_grad: { args: [P, u64, P, u64], returns: i32 },
|
|
168
|
+
mlx_closure_value_and_grad_new: { args: [], returns: u64 },
|
|
169
|
+
// (value: vector_array*, grads: vector_array*, cls, input: vector_array)
|
|
170
|
+
mlx_closure_value_and_grad_apply: { args: [P, P, u64, u64], returns: i32 },
|
|
171
|
+
mlx_closure_value_and_grad_free: { args: [u64], returns: i32 },
|
|
172
|
+
// mlx_vjp(outputs: vector_array*, vjps: vector_array*, fun: mlx_closure,
|
|
173
|
+
// primals: vector_array, cotangents: vector_array) — reverse-mode AD given an
|
|
174
|
+
// explicit cotangent per output. The natural segmented-backward primitive:
|
|
175
|
+
// fun = segment forward, cotangent = dh, vjps = [dh_in, ...dLoRA]. No
|
|
176
|
+
// surrogate scalar, no value_and_grad closure (see src/train/segmented.ts).
|
|
177
|
+
mlx_vjp: { args: [P, P, u64, u64, u64], returns: i32 },
|
|
178
|
+
// --- training: random init (LoRA A ~ normal; B stays zeros)
|
|
179
|
+
// (res, shape*, n, dtype, loc, scale, key may-be-null, stream)
|
|
180
|
+
mlx_random_normal: { args: [P, P, u64, i32, f32, f32, u64, u64], returns: i32 },
|
|
181
|
+
// (res, low, high, shape*, n, dtype, key may-be-null, stream) — low/high are
|
|
182
|
+
// scalar mlx_arrays. Used for mlx-lm-faithful LoRA A init (uniform).
|
|
183
|
+
mlx_random_uniform: { args: [P, u64, u64, P, u64, i32, u64, u64], returns: i32 },
|
|
184
|
+
mlx_random_split_num: { args: [P, u64, i32, u64], returns: i32 },
|
|
185
|
+
// --- training: loss/optimizer elementwise + reductions
|
|
186
|
+
mlx_stop_gradient: { args: [P, u64, u64], returns: i32 },
|
|
187
|
+
mlx_log: { args: [P, u64, u64], returns: i32 },
|
|
188
|
+
mlx_logaddexp: { args: [P, u64, u64, u64], returns: i32 },
|
|
189
|
+
mlx_mean: { args: [P, u64, FFIType.bool, u64], returns: i32 },
|
|
190
|
+
mlx_mean_axis: { args: [P, u64, i32, FFIType.bool, u64], returns: i32 },
|
|
191
|
+
mlx_sqrt: { args: [P, u64, u64], returns: i32 },
|
|
192
|
+
mlx_rsqrt: { args: [P, u64, u64], returns: i32 },
|
|
193
|
+
mlx_square: { args: [P, u64, u64], returns: i32 },
|
|
194
|
+
mlx_abs: { args: [P, u64, u64], returns: i32 },
|
|
195
|
+
// (res, a, a_min may-be-null, a_max may-be-null, stream)
|
|
196
|
+
mlx_clip: { args: [P, u64, u64, u64, u64], returns: i32 },
|
|
197
|
+
// --- quantization: native safetensors writer (io.h) + map inserts (map.h)
|
|
198
|
+
mlx_save_safetensors: { args: [cstring, u64, u64], returns: i32 },
|
|
199
|
+
mlx_map_string_to_array_insert: { args: [u64, cstring, u64], returns: i32 },
|
|
200
|
+
mlx_map_string_to_string_insert: { args: [u64, cstring, cstring], returns: i32 },
|
|
201
|
+
// compile via closures (Phase A compiled decode — docs/design/optimization_plan.md).
|
|
202
|
+
// mlx_closure is the usual one-pointer struct; the func-payload variant
|
|
203
|
+
// carries an id we use to route the trace callback back to JS.
|
|
204
|
+
mlx_closure_new: { args: [], returns: u64 },
|
|
205
|
+
mlx_closure_new_func_payload: { args: [P, P, P], returns: u64 },
|
|
206
|
+
mlx_closure_free: { args: [u64], returns: i32 },
|
|
207
|
+
mlx_closure_apply: { args: [P, u64, u64], returns: i32 },
|
|
208
|
+
// gradient checkpointing (transforms.h): wrap a closure so its forward
|
|
209
|
+
// activations are dropped and recomputed during the enclosing backward.
|
|
210
|
+
// mlx_checkpoint(res: mlx_closure*, fun: mlx_closure) → checkpointed closure.
|
|
211
|
+
mlx_checkpoint: { args: [P, u64], returns: i32 },
|
|
212
|
+
// custom_vjp (transforms.h): attach a hand-written backward to a forward whose
|
|
213
|
+
// primitives have no usable vjp (e.g. a fused metal_kernel). Needed for the L2
|
|
214
|
+
// flash-attention training op. mlx_custom_vjp(res: mlx_closure*, fun: mlx_closure,
|
|
215
|
+
// fun_vjp: mlx_closure_custom). The custom-vjp closure's callback is
|
|
216
|
+
// (res*, primals, cotangents, outputs, payload) → status.
|
|
217
|
+
mlx_custom_vjp: { args: [P, u64, u64], returns: i32 },
|
|
218
|
+
mlx_closure_custom_new_func_payload: { args: [P, P, P], returns: u64 },
|
|
219
|
+
mlx_closure_custom_free: { args: [u64], returns: i32 },
|
|
220
|
+
mlx_compile: { args: [P, u64, FFIType.bool], returns: i32 },
|
|
221
|
+
mlx_set_compile_mode: { args: [i32], returns: i32 },
|
|
222
|
+
mlx_vector_array_size: { args: [u64], returns: u64 },
|
|
223
|
+
mlx_vector_array_set_data: { args: [P, P, u64], returns: i32 },
|
|
224
|
+
// dynamic-start variants: per-decode-step offsets enter the graph as
|
|
225
|
+
// array VALUES instead of baked ints, so one compiled graph serves
|
|
226
|
+
// every step (no per-step retrace).
|
|
227
|
+
// (res, x, dims, traditional, opt base, scale, offset ARRAY, freqs may-be-null, stream)
|
|
228
|
+
mlx_fast_rope_dynamic: { args: [P, u64, i32, FFIType.bool, u64, f32, u64, u64, u64], returns: i32 },
|
|
229
|
+
// (res, src, update, start ARRAY, axes*, axes_num, stream)
|
|
230
|
+
mlx_slice_update_dynamic: { args: [P, u64, u64, u64, P, u64, u64], returns: i32 },
|
|
231
|
+
// (res, a, start ARRAY, axes*, axes_num, slice_size*, n, stream)
|
|
232
|
+
mlx_slice_dynamic: { args: [P, u64, u64, P, u64, P, u64, u64], returns: i32 },
|
|
233
|
+
// custom Metal kernels (Phase E — mx.fast.metal_kernel from Bun)
|
|
234
|
+
mlx_vector_string_new_data: { args: [P, u64], returns: u64 },
|
|
235
|
+
mlx_vector_string_free: { args: [u64], returns: i32 },
|
|
236
|
+
// (name, input_names, output_names, source, header, ensure_row_contiguous, atomic_outputs)
|
|
237
|
+
mlx_fast_metal_kernel_new: { args: [cstring, u64, u64, cstring, cstring, FFIType.bool, FFIType.bool], returns: u64 },
|
|
238
|
+
mlx_fast_metal_kernel_free: { args: [u64], returns: FFIType.void },
|
|
239
|
+
// (outputs: mlx_vector_array*, kernel, inputs, config, stream)
|
|
240
|
+
mlx_fast_metal_kernel_apply: { args: [P, u64, u64, u64, u64], returns: i32 },
|
|
241
|
+
mlx_fast_metal_kernel_config_new: { args: [], returns: u64 },
|
|
242
|
+
mlx_fast_metal_kernel_config_free: { args: [u64], returns: FFIType.void },
|
|
243
|
+
mlx_fast_metal_kernel_config_add_output_arg: { args: [u64, P, u64, i32], returns: i32 },
|
|
244
|
+
mlx_fast_metal_kernel_config_set_grid: { args: [u64, i32, i32, i32], returns: i32 },
|
|
245
|
+
mlx_fast_metal_kernel_config_set_thread_group: { args: [u64, i32, i32, i32], returns: i32 },
|
|
246
|
+
mlx_fast_metal_kernel_config_set_init_value: { args: [u64, f32], returns: i32 },
|
|
247
|
+
mlx_fast_metal_kernel_config_add_template_arg_int: { args: [u64, cstring, i32], returns: i32 },
|
|
248
|
+
mlx_fast_metal_kernel_config_add_template_arg_dtype: { args: [u64, cstring, i32], returns: i32 },
|
|
249
|
+
// GPU trace capture (Phase E step 2: size the prize before the kernel)
|
|
250
|
+
mlx_metal_start_capture: { args: [cstring], returns: i32 },
|
|
251
|
+
mlx_metal_stop_capture: { args: [], returns: i32 },
|
|
252
|
+
}).symbols;
|
|
253
|
+
|
|
254
|
+
export type MlxHandle = bigint;
|
|
255
|
+
|
|
256
|
+
// --- error handling -------------------------------------------------------
|
|
257
|
+
// mlx-c's default error handler prints and aborts the process. Install a
|
|
258
|
+
// recording handler instead: ops then return non-zero status and outArray
|
|
259
|
+
// throws a JS Error carrying the mlx message + JS stack. mlx-c invokes the
|
|
260
|
+
// handler on the calling thread (exceptions are caught in the C wrappers),
|
|
261
|
+
// so a non-threadsafe JSCallback is safe here.
|
|
262
|
+
import { CString, JSCallback } from "bun:ffi";
|
|
263
|
+
|
|
264
|
+
let lastMlxError: string | null = null;
|
|
265
|
+
|
|
266
|
+
const errorHandler = new JSCallback(
|
|
267
|
+
(msgPtr: number) => {
|
|
268
|
+
lastMlxError = msgPtr ? new CString(msgPtr as never).toString() : "unknown mlx error";
|
|
269
|
+
},
|
|
270
|
+
{ args: ["ptr", "ptr"], returns: "void" },
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const errLib = dlopen(LIBMLXC_PATH, {
|
|
274
|
+
mlx_set_error_handler: { args: [P, P, P], returns: FFIType.void },
|
|
275
|
+
}).symbols;
|
|
276
|
+
errLib.mlx_set_error_handler(errorHandler.ptr, null, null);
|
|
277
|
+
|
|
278
|
+
/** Consume the last recorded mlx error message (if any). */
|
|
279
|
+
export function takeMlxError(): string | null {
|
|
280
|
+
const e = lastMlxError;
|
|
281
|
+
lastMlxError = null;
|
|
282
|
+
return e;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** mlx_dtype enum values (array.h). */
|
|
286
|
+
export const enum Dtype {
|
|
287
|
+
bool = 0, uint8 = 1, uint16 = 2, uint32 = 3, uint64 = 4,
|
|
288
|
+
int8 = 5, int16 = 6, int32 = 7, int64 = 8,
|
|
289
|
+
float16 = 9, float32 = 10, float64 = 11, bfloat16 = 12, complex64 = 13,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const DTYPE_NAMES: Record<number, string> = {
|
|
293
|
+
0: "bool", 1: "uint8", 2: "uint16", 3: "uint32", 4: "uint64",
|
|
294
|
+
5: "int8", 6: "int16", 7: "int32", 8: "int64",
|
|
295
|
+
9: "float16", 10: "float32", 11: "float64", 12: "bfloat16", 13: "complex64",
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/** Pack mlx_optional_int for by-value passing. */
|
|
299
|
+
export function optInt(value: number | null): bigint {
|
|
300
|
+
if (value === null) return 0n;
|
|
301
|
+
return (BigInt(value >>> 0) & 0xffffffffn) | (1n << 32n);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Pack mlx_optional_dtype (same layout: enum + has_value flag). */
|
|
305
|
+
export const optDtype = optInt;
|
|
306
|
+
|
|
307
|
+
/** Pack mlx_optional_float `{float value; bool has_value}` for by-value passing. */
|
|
308
|
+
const f32Bits = new DataView(new ArrayBuffer(4));
|
|
309
|
+
export function optFloat(value: number | null): bigint {
|
|
310
|
+
if (value === null) return 0n;
|
|
311
|
+
f32Bits.setFloat32(0, value, true);
|
|
312
|
+
return BigInt(f32Bits.getUint32(0, true)) | (1n << 32n);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Null handle for optional mlx_array parameters. */
|
|
316
|
+
export const NULL_HANDLE = 0n;
|
|
317
|
+
|
|
318
|
+
// RULE: never read a typed array that a bun:ffi call wrote through a
|
|
319
|
+
// pointer — once the calling function is DFG-compiled, the JIT eliminates
|
|
320
|
+
// the load across the native call and `buf[i]` returns stale values
|
|
321
|
+
// (repro/bun-ffi-f64/ISSUE.md; PLAN.md Phase 4 findings). Out-param slots
|
|
322
|
+
// must be read back with bun:ffi `read.*` (verified safe). Initializing the
|
|
323
|
+
// slot via the typed-array *constructor* is fine: that store happens in
|
|
324
|
+
// host code the JIT can't elide.
|
|
325
|
+
|
|
326
|
+
/** Run an op that writes its result through an mlx_array* out-param. */
|
|
327
|
+
export function outArray(op: string, call: (slotPtr: number) => number): MlxHandle {
|
|
328
|
+
const slot = new BigUint64Array([C.mlx_array_new()]);
|
|
329
|
+
const slotPtr = ptr(slot);
|
|
330
|
+
const status = call(slotPtr);
|
|
331
|
+
const handle = read.u64(slotPtr, 0);
|
|
332
|
+
if (status !== 0) {
|
|
333
|
+
C.mlx_array_free(handle);
|
|
334
|
+
throw new Error(`${op} failed: ${takeMlxError() ?? "(no mlx message)"}`);
|
|
335
|
+
}
|
|
336
|
+
return handle;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function activeMemory(): number {
|
|
340
|
+
const out = new BigUint64Array(1);
|
|
341
|
+
const p = ptr(out);
|
|
342
|
+
C.mlx_get_active_memory(p);
|
|
343
|
+
return Number(read.u64(p, 0));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function cacheMemory(): number {
|
|
347
|
+
const out = new BigUint64Array(1);
|
|
348
|
+
const p = ptr(out);
|
|
349
|
+
C.mlx_get_cache_memory(p);
|
|
350
|
+
return Number(read.u64(p, 0));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function peakMemory(): number {
|
|
354
|
+
const out = new BigUint64Array(1);
|
|
355
|
+
const p = ptr(out);
|
|
356
|
+
C.mlx_get_peak_memory(p);
|
|
357
|
+
return Number(read.u64(p, 0));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** mx.reset_peak_memory — zero the peak counter (e.g. after model load,
|
|
361
|
+
* so a subsequent peakMemory() reads generation-only peak). */
|
|
362
|
+
export function resetPeakMemory(): void {
|
|
363
|
+
C.mlx_reset_peak_memory();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** mx.clear_cache — release the allocator's cached buffers back to the
|
|
367
|
+
* OS. mlx-lm calls this after every prefill chunk (and every 256 decode
|
|
368
|
+
* steps); without it the first decode step after a long prefill pays a
|
|
369
|
+
* one-shot allocator-reclaim stall (~800 ms after an 8k prefill —
|
|
370
|
+
* measured, scripts/decode-split.ts; the root cause of the
|
|
371
|
+
* context-scaling decode gap). */
|
|
372
|
+
export function clearCache(): void {
|
|
373
|
+
C.mlx_clear_cache();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Metal's recommended max working-set size for the default device. */
|
|
377
|
+
export function maxRecommendedWorkingSetSize(): number {
|
|
378
|
+
const devSlot = new BigUint64Array(1);
|
|
379
|
+
const devPtr = ptr(devSlot);
|
|
380
|
+
if (C.mlx_get_default_device(devPtr) !== 0)
|
|
381
|
+
throw new Error(`mlx_get_default_device failed: ${takeMlxError() ?? ""}`);
|
|
382
|
+
const dev = read.u64(devPtr, 0);
|
|
383
|
+
const infoSlot = new BigUint64Array([C.mlx_device_info_new()]);
|
|
384
|
+
const infoPtr = ptr(infoSlot);
|
|
385
|
+
try {
|
|
386
|
+
if (C.mlx_device_info_get(infoPtr, dev) !== 0)
|
|
387
|
+
throw new Error(`mlx_device_info_get failed: ${takeMlxError() ?? ""}`);
|
|
388
|
+
const info = read.u64(infoPtr, 0);
|
|
389
|
+
const out = new BigUint64Array(1);
|
|
390
|
+
const outPtr = ptr(out);
|
|
391
|
+
const key = Buffer.from("max_recommended_working_set_size\0", "utf8");
|
|
392
|
+
if (C.mlx_device_info_get_size(outPtr, info, ptr(key)) !== 0)
|
|
393
|
+
throw new Error("max_recommended_working_set_size not in device info");
|
|
394
|
+
return Number(read.u64(outPtr, 0));
|
|
395
|
+
} finally {
|
|
396
|
+
C.mlx_device_info_free(read.u64(infoPtr, 0));
|
|
397
|
+
C.mlx_device_free(dev);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** mx.set_wired_limit — returns the previous limit. Models close to the
|
|
402
|
+
* working-set ceiling decode ~4x slower without this (Metal evicts and
|
|
403
|
+
* re-faults weight buffers every token; mlx-lm's wired_limit context
|
|
404
|
+
* is the reference behavior). MUST be scoped (set → generate →
|
|
405
|
+
* synchronize → restore), never process-permanent: a permanent limit
|
|
406
|
+
* pins buffers across idle periods and OOM-kills multi-model processes
|
|
407
|
+
* (the test suite) via uncatchable async GPU errors. */
|
|
408
|
+
export function setWiredLimit(bytes: number): number {
|
|
409
|
+
const out = new BigUint64Array(1);
|
|
410
|
+
const p = ptr(out);
|
|
411
|
+
if (C.mlx_set_wired_limit(p, BigInt(Math.floor(bytes))) !== 0)
|
|
412
|
+
throw new Error(`mlx_set_wired_limit failed: ${takeMlxError() ?? ""}`);
|
|
413
|
+
return Number(read.u64(p, 0));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** mx.set_memory_limit — returns the previous limit. At the limit the
|
|
417
|
+
* allocator reclaims/waits instead of ballooning past it. Defense in
|
|
418
|
+
* depth under admission control ONLY: it does NOT make Metal
|
|
419
|
+
* command-buffer OOM catchable (Phase 6 finding — that throw comes
|
|
420
|
+
* from a completion handler and is std::terminate). */
|
|
421
|
+
export function setMemoryLimit(bytes: number): number {
|
|
422
|
+
const out = new BigUint64Array(1);
|
|
423
|
+
const p = ptr(out);
|
|
424
|
+
if (C.mlx_set_memory_limit(p, BigInt(Math.floor(bytes))) !== 0)
|
|
425
|
+
throw new Error(`mlx_set_memory_limit failed: ${takeMlxError() ?? ""}`);
|
|
426
|
+
return Number(read.u64(p, 0));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** mx.synchronize(stream) — wired limit must not change mid-async-eval. */
|
|
430
|
+
export function synchronize(stream: MlxHandle): void {
|
|
431
|
+
if (C.mlx_synchronize(stream) !== 0)
|
|
432
|
+
throw new Error(`mlx_synchronize failed: ${takeMlxError() ?? ""}`);
|
|
433
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// mx.fast.metal_kernel from Bun (docs/design/optimization_plan.md Phase E): the
|
|
2
|
+
// intended MLX extension point for custom Metal kernels — no fork. The
|
|
3
|
+
// kernel body is Metal Shading Language; mlx generates the signature
|
|
4
|
+
// from input/output names (inputs by name, outputs by name, plus
|
|
5
|
+
// `*_shape`/`*_strides`/`*_ndim` helpers when referenced). Dispatched
|
|
6
|
+
// like any lazy op: apply() builds graph nodes, eval runs the kernel.
|
|
7
|
+
|
|
8
|
+
import { ptr, read } from "bun:ffi";
|
|
9
|
+
import { C, Dtype, type MlxHandle, takeMlxError } from "./ffi";
|
|
10
|
+
import { MlxArray, gpuStream } from "./array";
|
|
11
|
+
|
|
12
|
+
function vectorString(strings: string[]): { vec: MlxHandle; keepAlive: Buffer[] } {
|
|
13
|
+
const bufs = strings.map((s) => Buffer.from(s + "\0", "utf8"));
|
|
14
|
+
const ptrs = new BigUint64Array(bufs.map((b) => BigInt(ptr(b))));
|
|
15
|
+
const vec = C.mlx_vector_string_new_data(ptr(ptrs), BigInt(bufs.length));
|
|
16
|
+
return { vec, keepAlive: bufs };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MetalKernelSpec {
|
|
20
|
+
name: string;
|
|
21
|
+
inputNames: string[];
|
|
22
|
+
outputNames: string[];
|
|
23
|
+
/** Kernel BODY (not a full function) in MSL. */
|
|
24
|
+
source: string;
|
|
25
|
+
/** Optional includes/defines prepended outside the kernel. */
|
|
26
|
+
header?: string;
|
|
27
|
+
ensureRowContiguous?: boolean;
|
|
28
|
+
atomicOutputs?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MetalKernelCall {
|
|
32
|
+
outputs: { shape: number[]; dtype: Dtype }[];
|
|
33
|
+
grid: [number, number, number];
|
|
34
|
+
threadGroup: [number, number, number];
|
|
35
|
+
templateInts?: Record<string, number>;
|
|
36
|
+
templateDtypes?: Record<string, Dtype>;
|
|
37
|
+
initValue?: number;
|
|
38
|
+
stream?: MlxHandle;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class MetalKernel {
|
|
42
|
+
#kernel: MlxHandle;
|
|
43
|
+
#disposed = false;
|
|
44
|
+
|
|
45
|
+
constructor(spec: MetalKernelSpec) {
|
|
46
|
+
const inNames = vectorString(spec.inputNames);
|
|
47
|
+
const outNames = vectorString(spec.outputNames);
|
|
48
|
+
const name = Buffer.from(spec.name + "\0", "utf8");
|
|
49
|
+
const source = Buffer.from(spec.source + "\0", "utf8");
|
|
50
|
+
const header = Buffer.from((spec.header ?? "") + "\0", "utf8");
|
|
51
|
+
this.#kernel = C.mlx_fast_metal_kernel_new(
|
|
52
|
+
ptr(name), inNames.vec, outNames.vec, ptr(source), ptr(header),
|
|
53
|
+
spec.ensureRowContiguous ?? true, spec.atomicOutputs ?? false,
|
|
54
|
+
);
|
|
55
|
+
C.mlx_vector_string_free(inNames.vec);
|
|
56
|
+
C.mlx_vector_string_free(outNames.vec);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
apply(inputs: MlxArray[], call: MetalKernelCall): MlxArray[] {
|
|
60
|
+
if (this.#disposed) throw new Error("MetalKernel used after dispose");
|
|
61
|
+
const cfg = C.mlx_fast_metal_kernel_config_new();
|
|
62
|
+
try {
|
|
63
|
+
for (const o of call.outputs) {
|
|
64
|
+
const shape = new Int32Array(o.shape);
|
|
65
|
+
if (C.mlx_fast_metal_kernel_config_add_output_arg(cfg, ptr(shape), BigInt(o.shape.length), o.dtype) !== 0)
|
|
66
|
+
throw new Error(`metal_kernel add_output_arg failed: ${takeMlxError() ?? ""}`);
|
|
67
|
+
}
|
|
68
|
+
C.mlx_fast_metal_kernel_config_set_grid(cfg, call.grid[0], call.grid[1], call.grid[2]);
|
|
69
|
+
C.mlx_fast_metal_kernel_config_set_thread_group(cfg, call.threadGroup[0], call.threadGroup[1], call.threadGroup[2]);
|
|
70
|
+
if (call.initValue !== undefined)
|
|
71
|
+
C.mlx_fast_metal_kernel_config_set_init_value(cfg, call.initValue);
|
|
72
|
+
for (const [k, v] of Object.entries(call.templateInts ?? {})) {
|
|
73
|
+
const kb = Buffer.from(k + "\0", "utf8");
|
|
74
|
+
C.mlx_fast_metal_kernel_config_add_template_arg_int(cfg, ptr(kb), v);
|
|
75
|
+
}
|
|
76
|
+
for (const [k, v] of Object.entries(call.templateDtypes ?? {})) {
|
|
77
|
+
const kb = Buffer.from(k + "\0", "utf8");
|
|
78
|
+
C.mlx_fast_metal_kernel_config_add_template_arg_dtype(cfg, ptr(kb), v);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const handles = new BigUint64Array(inputs.map((a) => a.handle));
|
|
82
|
+
const inVec = C.mlx_vector_array_new_data(ptr(handles), BigInt(inputs.length));
|
|
83
|
+
const slot = new BigUint64Array([C.mlx_vector_array_new()]);
|
|
84
|
+
const slotPtr = ptr(slot);
|
|
85
|
+
const status = C.mlx_fast_metal_kernel_apply(slotPtr, this.#kernel, inVec, cfg, call.stream ?? gpuStream);
|
|
86
|
+
const outVec = read.u64(slotPtr, 0);
|
|
87
|
+
C.mlx_vector_array_free(inVec);
|
|
88
|
+
if (status !== 0) {
|
|
89
|
+
C.mlx_vector_array_free(outVec);
|
|
90
|
+
throw new Error(`metal_kernel apply failed: ${takeMlxError() ?? "(no message)"}`);
|
|
91
|
+
}
|
|
92
|
+
const n = Number(C.mlx_vector_array_size(outVec));
|
|
93
|
+
const outs: MlxArray[] = [];
|
|
94
|
+
const aSlot = new BigUint64Array(1);
|
|
95
|
+
const aPtr = ptr(aSlot);
|
|
96
|
+
for (let i = 0; i < n; i++) {
|
|
97
|
+
aSlot[0] = C.mlx_array_new();
|
|
98
|
+
if (C.mlx_vector_array_get(aPtr, outVec, BigInt(i)) !== 0) {
|
|
99
|
+
C.mlx_vector_array_free(outVec);
|
|
100
|
+
throw new Error(`metal_kernel output get(${i}) failed: ${takeMlxError() ?? ""}`);
|
|
101
|
+
}
|
|
102
|
+
outs.push(new MlxArray(read.u64(aPtr, 0)));
|
|
103
|
+
}
|
|
104
|
+
C.mlx_vector_array_free(outVec);
|
|
105
|
+
return outs;
|
|
106
|
+
} finally {
|
|
107
|
+
C.mlx_fast_metal_kernel_config_free(cfg);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dispose(): void {
|
|
112
|
+
if (this.#disposed) return;
|
|
113
|
+
this.#disposed = true;
|
|
114
|
+
C.mlx_fast_metal_kernel_free(this.#kernel);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** mlx_metal_start/stop_capture — wrap a single decode step to inspect
|
|
119
|
+
* its command buffer in Xcode (Phase E step 2: size the prize). Needs
|
|
120
|
+
* MTL_CAPTURE_ENABLED=1 in the environment. */
|
|
121
|
+
export function metalCapture(path: string, fn: () => void): void {
|
|
122
|
+
const p = Buffer.from(path + "\0", "utf8");
|
|
123
|
+
if (C.mlx_metal_start_capture(ptr(p)) !== 0)
|
|
124
|
+
throw new Error(`metal_start_capture failed: ${takeMlxError() ?? ""}`);
|
|
125
|
+
try {
|
|
126
|
+
fn();
|
|
127
|
+
} finally {
|
|
128
|
+
C.mlx_metal_stop_capture();
|
|
129
|
+
}
|
|
130
|
+
}
|