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.
Files changed (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. 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
+ }