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
@@ -0,0 +1,180 @@
1
+ // Calibration data loading for sensitivity analysis.
2
+ //
3
+ // Port of optiq/calibration/datasets.py `load_llm_calibration` /
4
+ // `_load_optiq_calibration`: load a multi-domain JSONL mix, tokenize, build one
5
+ // long token stream, and chunk it into fixed-length sequences. The
6
+ // `calibrationFn` contract is the TS analogue of OptIQ's
7
+ // `list[(args_tuple, kwargs_dict)]`: here, just a list of `number[]` token-id
8
+ // prompts (each a (1, seq_len) row the model forwards).
9
+ //
10
+ // Two deviations from the Python, both forced by our pure-JS tokenizer surface
11
+ // (src/tokenizer.ts exposes plain `encode` only, no `apply_chat_template`):
12
+ // - `messages` samples use the role-prefixed dumb-concatenation fallback —
13
+ // exactly OptIQ's `no chat template` branch.
14
+ // - `add_special_tokens=False` is the OptIQ encode flag; our `encode` already
15
+ // omits special tokens for raw substrings.
16
+ //
17
+ // Source mix: the bundled `optiq.jsonl` if locatable on disk (the oracle venv's
18
+ // optiq/calibration/data/optiq.jsonl), else a small built-in fallback corpus so
19
+ // the path works with no external data.
20
+
21
+ import { existsSync } from "node:fs";
22
+ import type { LoadedTokenizer } from "../tokenizer";
23
+
24
+ /** One JSONL calibration sample (subset of the OptIQ schema we can consume). */
25
+ interface CalibrationSample {
26
+ domain?: string;
27
+ text?: string;
28
+ messages?: Array<{ role: string; content: string }>;
29
+ tools?: unknown;
30
+ }
31
+
32
+ /** Candidate on-disk locations for the bundled optiq.jsonl mix. */
33
+ function bundledMixCandidates(): string[] {
34
+ const home = process.env.HOME ?? "";
35
+ const rel = "lib/python3.14/site-packages/optiq/calibration/data/optiq.jsonl";
36
+ return [
37
+ process.env.MLX_BUN_CALIBRATION_JSONL,
38
+ `${home}/Code/mlx-lm-example/.venv/${rel}`,
39
+ `${home}/Code/mlx-lm/.venv/${rel}`,
40
+ ].filter((p): p is string => !!p);
41
+ }
42
+
43
+ /** A tiny built-in fallback corpus (used only when no JSONL mix is found).
44
+ * Multi-domain prose so groups have meaningful activation diversity. */
45
+ const FALLBACK_CORPUS: string[] = [
46
+ "The history of computing stretches back to mechanical calculators and the abacus. Charles Babbage designed the Analytical Engine, a general-purpose mechanical computer, in the nineteenth century. Ada Lovelace wrote what is often considered the first algorithm intended for such a machine. The theoretical foundations were later laid by Alan Turing, whose model of computation defined the limits of what machines can compute.",
47
+ "In physics, the conservation of energy states that the total energy of an isolated system remains constant over time. Energy can neither be created nor destroyed; rather, it transforms from one form to another. Kinetic energy becomes potential energy as a ball rises, and back again as it falls. Thermodynamics extends these ideas to heat, work, and entropy in large systems.",
48
+ "Functions in programming are reusable blocks of code that perform a specific task. They take inputs, called arguments, and may return a value. Pure functions have no side effects and always return the same output for the same input, which makes them easier to test and reason about. Recursion is a technique where a function calls itself to solve smaller subproblems.",
49
+ "The water cycle describes the continuous movement of water on, above, and below the surface of the Earth. Water evaporates from oceans and lakes, condenses into clouds, and precipitates as rain or snow. Rivers carry water back to the sea, and groundwater slowly percolates through soil and rock. Plants release water vapor through transpiration, contributing to atmospheric moisture.",
50
+ "Economic markets coordinate the production and exchange of goods through prices. When demand for a product rises and supply stays fixed, prices tend to increase, signaling producers to make more. Competition drives firms to lower costs and innovate. Externalities, such as pollution, are costs not reflected in market prices and often require regulation to address.",
51
+ "A neural network is composed of layers of interconnected nodes, each applying a weighted sum followed by a nonlinear activation. During training, the network adjusts its weights to minimize a loss function via gradient descent. Backpropagation efficiently computes gradients by applying the chain rule layer by layer. Deeper networks can represent more complex functions but are harder to train.",
52
+ ];
53
+
54
+ /**
55
+ * Build a calibration callable from a tokenizer.
56
+ *
57
+ * @returns a zero-arg function producing `number[][]` — N token-id rows, each
58
+ * of length `seqLen`, ready to forward through a model.
59
+ */
60
+ export function loadLlmCalibration(
61
+ tokenizer: LoadedTokenizer,
62
+ opts: {
63
+ nSamples?: number;
64
+ seqLen?: number;
65
+ seed?: number;
66
+ mix?: string;
67
+ } = {},
68
+ ): () => number[][] {
69
+ const nSamples = opts.nSamples ?? 32;
70
+ const seqLen = opts.seqLen ?? 1024;
71
+ const seed = opts.seed ?? 42;
72
+ const mix = opts.mix ?? "optiq";
73
+
74
+ const samples = loadSamples(mix);
75
+
76
+ // Concatenate every sample's tokens into one long stream.
77
+ const allTokens: number[] = [];
78
+ for (const s of samples) {
79
+ let text: string | undefined;
80
+ if (s.messages) {
81
+ // No chat template available in our JS tokenizer — role-prefixed
82
+ // concatenation (OptIQ's no-chat-template fallback branch).
83
+ text = s.messages.map((m) => `[${m.role}]: ${m.content}`).join("\n\n");
84
+ } else if (s.text !== undefined) {
85
+ text = s.text;
86
+ } else {
87
+ continue;
88
+ }
89
+ const toks = tokenizer.encode(text);
90
+ for (const t of toks) allTokens.push(t);
91
+ }
92
+
93
+ if (allTokens.length < seqLen) {
94
+ throw new Error(
95
+ `Calibration mix produced only ${allTokens.length} tokens (< seqLen=${seqLen}). ` +
96
+ `Try a smaller seqLen or a larger mix.`,
97
+ );
98
+ }
99
+
100
+ // Chunk start offsets, shuffled deterministically (seeded), like OptIQ.
101
+ const starts: number[] = [];
102
+ for (let s = 0; s < allTokens.length - seqLen; s += seqLen) starts.push(s);
103
+ shuffleInPlace(starts, seed);
104
+
105
+ const chunks: number[][] = [];
106
+ for (const start of starts.slice(0, nSamples)) {
107
+ chunks.push(allTokens.slice(start, start + seqLen));
108
+ }
109
+
110
+ if (chunks.length === 0) {
111
+ throw new Error("Could not create any calibration sequences.");
112
+ }
113
+
114
+ return () => chunks;
115
+ }
116
+
117
+ /** Load raw samples from the mix spec ("optiq" → bundled, else a JSONL path,
118
+ * else the built-in fallback corpus). */
119
+ function loadSamples(mix: string): CalibrationSample[] {
120
+ let jsonlPath: string | null = null;
121
+ if (mix === "optiq") {
122
+ jsonlPath = bundledMixCandidates().find((p) => existsSync(p)) ?? null;
123
+ } else if (existsSync(mix)) {
124
+ jsonlPath = mix;
125
+ } else {
126
+ throw new Error(
127
+ `calibration mix not found at ${mix}. Use mix='optiq' (default) or a ` +
128
+ `path to a JSONL file.`,
129
+ );
130
+ }
131
+
132
+ if (jsonlPath) return parseJsonl(jsonlPath);
133
+
134
+ // Built-in fallback: synthesize one prose sample per corpus entry.
135
+ return FALLBACK_CORPUS.map((text) => ({ domain: "prose", text }));
136
+ }
137
+
138
+ /** Parse a JSONL file into CalibrationSample[] (one object per non-blank line). */
139
+ function parseJsonl(path: string): CalibrationSample[] {
140
+ // Synchronous read so the caller can stay synchronous; the file is small.
141
+ const text = readFileSyncUtf8(path);
142
+ const out: CalibrationSample[] = [];
143
+ for (const line of text.split("\n")) {
144
+ const trimmed = line.trim();
145
+ if (trimmed) out.push(JSON.parse(trimmed) as CalibrationSample);
146
+ }
147
+ return out;
148
+ }
149
+
150
+ function readFileSyncUtf8(path: string): string {
151
+ // Bun's readFileSync via node:fs is available in this runtime.
152
+ // (Kept local so the rest of the module is import-light.)
153
+ const { readFileSync } = require("node:fs") as typeof import("node:fs");
154
+ return readFileSync(path, "utf8");
155
+ }
156
+
157
+ /** Deterministic Fisher–Yates shuffle (mulberry32 PRNG seeded by `seed`).
158
+ * NOTE: this does NOT reproduce numpy's RandomState shuffle order — it only
159
+ * needs to be deterministic for our own reproducibility; the exact sample
160
+ * ordering does not affect the allocator's 1:1 parity (that test feeds
161
+ * synthetic SensitivityResults directly). */
162
+ function shuffleInPlace<T>(arr: T[], seed: number): void {
163
+ const rand = mulberry32(seed >>> 0);
164
+ for (let i = arr.length - 1; i > 0; i--) {
165
+ const j = Math.floor(rand() * (i + 1));
166
+ const tmp = arr[i]!;
167
+ arr[i] = arr[j]!;
168
+ arr[j] = tmp;
169
+ }
170
+ }
171
+
172
+ function mulberry32(a: number): () => number {
173
+ return () => {
174
+ a |= 0;
175
+ a = (a + 0x6d2b79f5) | 0;
176
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
177
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
178
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
179
+ };
180
+ }
@@ -0,0 +1,132 @@
1
+ // Config + sidecar writer for a quantized model directory.
2
+ //
3
+ // Produces the `config.json` block the loader (src/config.ts → parseQuantization)
4
+ // reads back: a top-level `quantization` object AND a mirror `quantization_config`
5
+ // (HF compatibility — config.ts reads `raw.quantization ?? raw.quantization_config`).
6
+ // The block carries default {group_size, bits, mode} plus per-module overrides
7
+ // keyed by module path (e.g. "model.embed_tokens") — exactly the OptiQ layout.
8
+ //
9
+ // Aux files (tokenizer, chat template, generation config, …) are copied through
10
+ // verbatim so the output directory is a complete, loadable model snapshot.
11
+
12
+ import { existsSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ /** A per-module quantization override. `false` means "left unquantized". */
16
+ export type PerLayerEntry = { bits: number; groupSize: number } | false;
17
+
18
+ /** The default quantization definition for the model. */
19
+ export interface QuantDef {
20
+ bits: number;
21
+ groupSize: number;
22
+ mode?: string;
23
+ }
24
+
25
+ /** The serialized quantization block, ready to drop into config.json. */
26
+ export interface QuantizationBlock {
27
+ group_size: number;
28
+ bits: number;
29
+ mode: string;
30
+ // plus per-module entries: { [modulePath]: {bits, group_size} | false }
31
+ [modulePath: string]: number | string | { bits: number; group_size: number } | false;
32
+ }
33
+
34
+ /**
35
+ * Build the `quantization` config object: a `{group_size, bits, mode}` default
36
+ * plus one entry per module in `perLayer`. A module mapped to `false` is
37
+ * serialized as `false` (mlx's "not quantized" convention); otherwise it
38
+ * becomes `{bits, group_size}`.
39
+ */
40
+ export function buildQuantizationBlock(
41
+ def: QuantDef,
42
+ perLayer: Map<string, PerLayerEntry>,
43
+ ): QuantizationBlock {
44
+ const block: QuantizationBlock = {
45
+ group_size: def.groupSize,
46
+ bits: def.bits,
47
+ mode: def.mode ?? "affine",
48
+ };
49
+ for (const [path, entry] of perLayer) {
50
+ block[path] = entry === false ? false : { bits: entry.bits, group_size: entry.groupSize };
51
+ }
52
+ return block;
53
+ }
54
+
55
+ /** Aux files copied through verbatim from the source dir when present. */
56
+ const AUX_FILES = [
57
+ "tokenizer.json",
58
+ "tokenizer_config.json",
59
+ "tokenizer.model",
60
+ "spiece.model",
61
+ "chat_template.jinja",
62
+ "generation_config.json",
63
+ "special_tokens_map.json",
64
+ "added_tokens.json",
65
+ "vocab.json",
66
+ "merges.txt",
67
+ "README.md",
68
+ "kv_config.json",
69
+ ];
70
+
71
+ /** Metadata for the OptiQ-style sidecar describing the achieved quantization. */
72
+ export interface OptiqMetadata {
73
+ method: string;
74
+ base_model: string;
75
+ bits: number;
76
+ group_size: number;
77
+ achieved_bpw: number;
78
+ per_layer_count: number;
79
+ }
80
+
81
+ /**
82
+ * Write `config.json` (deep copy of `srcConfigRaw` with both `quantization`
83
+ * and `quantization_config` set to `block`) into `outDir`, copy through any
84
+ * present aux files, and write `optiq_metadata.json` describing the run.
85
+ *
86
+ * `*.model` tokenizer files beyond the known names are also swept in so SPM /
87
+ * tiktoken sidecars survive the round-trip.
88
+ */
89
+ export async function writeQuantizedConfig(
90
+ srcConfigRaw: Record<string, unknown>,
91
+ outDir: string,
92
+ block: QuantizationBlock,
93
+ opts: { srcDir?: string; optiq?: OptiqMetadata } = {},
94
+ ): Promise<void> {
95
+ // Deep copy so we never mutate the caller's config object.
96
+ const config = JSON.parse(JSON.stringify(srcConfigRaw)) as Record<string, unknown>;
97
+ config.quantization = block;
98
+ config.quantization_config = block;
99
+ await Bun.write(join(outDir, "config.json"), JSON.stringify(config, null, 2));
100
+
101
+ if (opts.srcDir) await copyAuxFiles(opts.srcDir, outDir);
102
+
103
+ if (opts.optiq) {
104
+ await Bun.write(
105
+ join(outDir, "optiq_metadata.json"),
106
+ JSON.stringify(opts.optiq, null, 2),
107
+ );
108
+ }
109
+ }
110
+
111
+ /** Copy known aux files (and any `*.model`) from src → out when present. */
112
+ async function copyAuxFiles(srcDir: string, outDir: string): Promise<void> {
113
+ const seen = new Set<string>();
114
+ for (const f of AUX_FILES) {
115
+ const src = join(srcDir, f);
116
+ if (existsSync(src)) {
117
+ await Bun.write(join(outDir, f), Bun.file(src));
118
+ seen.add(f);
119
+ }
120
+ }
121
+ // Sweep any additional *.model tokenizer sidecars not in the known list.
122
+ try {
123
+ const { readdirSync } = await import("node:fs");
124
+ for (const f of readdirSync(srcDir)) {
125
+ if (f.endsWith(".model") && !seen.has(f)) {
126
+ await Bun.write(join(outDir, f), Bun.file(join(srcDir, f)));
127
+ }
128
+ }
129
+ } catch {
130
+ // src dir unreadable — aux copy is best-effort.
131
+ }
132
+ }
@@ -0,0 +1,57 @@
1
+ // Public API for native model quantization. The server (and the job system)
2
+ // import from here: quantize a model directory, write sharded safetensors,
3
+ // build the config block, and resolve/inspect models for the wizard UI.
4
+
5
+ export { quantizeModelDir, isQuantizable } from "./quantizer";
6
+ export type {
7
+ QuantizeOptions,
8
+ QuantizeResult,
9
+ ProgressEvent,
10
+ } from "./quantizer";
11
+
12
+ export {
13
+ writeShardedSafetensors,
14
+ DEFAULT_SHARD_BYTES,
15
+ } from "./safetensors-writer";
16
+ export type {
17
+ NamedTensor,
18
+ ShardInfo,
19
+ SafetensorsIndex,
20
+ WriteResult,
21
+ WriteOpts,
22
+ } from "./safetensors-writer";
23
+
24
+ export {
25
+ buildQuantizationBlock,
26
+ writeQuantizedConfig,
27
+ } from "./config-writer";
28
+ export type {
29
+ QuantDef,
30
+ PerLayerEntry,
31
+ QuantizationBlock,
32
+ OptiqMetadata,
33
+ } from "./config-writer";
34
+
35
+ // Mixed-precision (OptiQ sensitivity + knapsack) surface.
36
+ export {
37
+ optimizeMixedPrecision,
38
+ computeBpw,
39
+ klReduction,
40
+ UpgradeHeap,
41
+ } from "./allocator";
42
+ export type {
43
+ LayerQuantConfig,
44
+ OptimizationResult,
45
+ OptimizeMixedPrecisionOptions,
46
+ UpgradeEntry,
47
+ } from "./allocator";
48
+
49
+ export { analyzeSensitivityExact, klFromRef } from "./sensitivity";
50
+ export type {
51
+ SensitivityResult,
52
+ AnalyzeSensitivityOptions,
53
+ } from "./sensitivity";
54
+
55
+ export { loadLlmCalibration } from "./calibration";
56
+
57
+ export { quantizeRunner, inspectModel } from "./job";
@@ -0,0 +1,136 @@
1
+ // The quantize JobRunner: wires a submitted quantize job to quantizeModelDir().
2
+ // Speaks the shared src/jobs/types.ts contract. The submit config carries the
3
+ // source model (a registry query, a model_id, or an explicit src_dir), the
4
+ // output dir, and the target bits / group size.
5
+ //
6
+ // Registers itself with the jobs runner registry on import so both the
7
+ // in-process submit path and the subprocess job-entry resolver (which
8
+ // dynamic-imports this module via KIND_MODULES) find the "quantize" runner.
9
+
10
+ import { statSync } from "node:fs";
11
+ import type { Emit, JobRunner } from "../jobs/types";
12
+ import { registerRunner } from "../jobs/runner";
13
+ import { Registry } from "../registry";
14
+ import { loadModelConfig } from "../config";
15
+ import { quantizeModelDir, type QuantizeOptions } from "./quantizer";
16
+
17
+ /** Resolve the source model directory from a job config. Accepts an explicit
18
+ * filesystem path (src_dir) or a registry query / model id (model_id). */
19
+ function resolveSrcDir(config: Record<string, unknown>): string {
20
+ const srcDir = config.src_dir as string | undefined;
21
+ if (srcDir) return srcDir;
22
+ const modelId = config.model_id as string | undefined;
23
+ if (!modelId) throw new Error("quantize job: missing src_dir or model_id");
24
+ // A path-looking model_id is taken literally; otherwise resolve via registry.
25
+ if (modelId.includes("/") && existsSyncSafe(modelId)) return modelId;
26
+ const reg = new Registry();
27
+ try {
28
+ return reg.resolve(modelId).path;
29
+ } finally {
30
+ reg.close();
31
+ }
32
+ }
33
+
34
+ function existsSyncSafe(p: string): boolean {
35
+ try {
36
+ statSync(p);
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ export const quantizeRunner: JobRunner = async (emit: Emit, config) => {
44
+ const outDir = String(config.out_dir ?? "");
45
+ if (!outDir) throw new Error("quantize job: missing out_dir");
46
+
47
+ const bits = Number(config.bits ?? 4) as 4 | 8;
48
+ const groupSize = Number(config.group_size ?? 64) as 32 | 64;
49
+ if (bits !== 4 && bits !== 8) throw new Error(`quantize job: bits must be 4 or 8 (got ${bits})`);
50
+ if (groupSize !== 32 && groupSize !== 64)
51
+ throw new Error(`quantize job: group_size must be 32 or 64 (got ${groupSize})`);
52
+
53
+ const srcDir = resolveSrcDir(config);
54
+
55
+ emit({
56
+ type: "stage",
57
+ stage: "starting",
58
+ progress: 0.01,
59
+ message: `Quantizing ${srcDir} → ${bits}-bit (g${groupSize})`,
60
+ });
61
+
62
+ const opts: QuantizeOptions = { bits, groupSize, mode: String(config.mode ?? "affine") };
63
+
64
+ const r = await quantizeModelDir(srcDir, outDir, opts, (e) =>
65
+ emit({ type: "stage", stage: e.stage, progress: e.progress, message: e.message }),
66
+ );
67
+
68
+ emit({
69
+ type: "stage",
70
+ stage: "done",
71
+ progress: 1,
72
+ message: `Quantized ${r.nQuantized} modules (${r.achievedBpw.toFixed(2)} bpw)`,
73
+ output_dir: r.outDir,
74
+ });
75
+
76
+ return { outputPath: r.outDir };
77
+ };
78
+
79
+ /** Lightweight model inspection for the `/api/quantize/inspect` route: report
80
+ * whether a model is quantizable and its on-disk size, from the registry +
81
+ * config (never touches tensor bytes). */
82
+ export async function inspectModel(model_id: string): Promise<{
83
+ ok: boolean;
84
+ model_id: string;
85
+ arch: string | null;
86
+ support: boolean;
87
+ size_gb: number;
88
+ error?: string;
89
+ }> {
90
+ try {
91
+ let path = model_id;
92
+ let arch: string | null = null;
93
+ let sizeBytes = 0;
94
+
95
+ if (!(model_id.includes("/") && existsSyncSafe(model_id))) {
96
+ const reg = new Registry();
97
+ try {
98
+ const rec = reg.resolve(model_id);
99
+ path = rec.path;
100
+ arch = rec.modelType;
101
+ sizeBytes = rec.sizeBytes;
102
+ } finally {
103
+ reg.close();
104
+ }
105
+ }
106
+
107
+ const config = await loadModelConfig(path);
108
+ arch = arch ?? config.modelType ?? (config.architectures[0] ?? null);
109
+ // Supported = a text architecture this quantizer can walk. v1 quantizes
110
+ // any model whose weights are plain 2D Linear/embedding tensors; we treat
111
+ // every loadable text config as supported and let eligibility filter
112
+ // per-tensor at quantize time.
113
+ const support = config.text.numHiddenLayers > 0;
114
+
115
+ return {
116
+ ok: true,
117
+ model_id,
118
+ arch,
119
+ support,
120
+ size_gb: sizeBytes / (1 << 30),
121
+ };
122
+ } catch (e) {
123
+ return {
124
+ ok: false,
125
+ model_id,
126
+ arch: null,
127
+ support: false,
128
+ size_gb: 0,
129
+ error: e instanceof Error ? e.message : String(e),
130
+ };
131
+ }
132
+ }
133
+
134
+ // Self-register on import: `kind: "quantize"` resolves both for in-process
135
+ // submit and for the subprocess job-entry (KIND_MODULES → ../quantize/job.ts).
136
+ registerRunner("quantize", quantizeRunner);