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/config.ts ADDED
@@ -0,0 +1,229 @@
1
+ // Model config + quantization metadata reader for the load path.
2
+ // Parses HF config.json (Gemma 4 unified layout: text_config nested),
3
+ // the OptiQ per-layer quantization map, and kv_config.json when present.
4
+
5
+ export interface RopeParams {
6
+ ropeTheta: number;
7
+ ropeType: string;
8
+ partialRotaryFactor: number;
9
+ /** ProportionalRoPE frequency multiplier (rope_utils: factor, default 1). */
10
+ factor: number;
11
+ }
12
+
13
+ export interface TextConfig {
14
+ hiddenSize: number;
15
+ numHiddenLayers: number;
16
+ numAttentionHeads: number;
17
+ numKeyValueHeads: number;
18
+ headDim: number;
19
+ /** Global (full-attention) layers can use different KV geometry. */
20
+ numGlobalKeyValueHeads: number;
21
+ globalHeadDim: number;
22
+ attentionKEqV: boolean;
23
+ intermediateSize: number;
24
+ hiddenActivation: string;
25
+ rmsNormEps: number;
26
+ vocabSize: number;
27
+ maxPositionEmbeddings: number;
28
+ slidingWindow: number;
29
+ /** "sliding_attention" | "full_attention" per layer. */
30
+ layerTypes: string[];
31
+ /** e2b/e4b: per-layer input embedding width (0 = disabled). */
32
+ hiddenSizePerLayerInput: number;
33
+ vocabSizePerLayerInput: number;
34
+ /** Trailing layers that share KV with earlier same-type layers. */
35
+ numKvSharedLayers: number;
36
+ /** 26B-A4B MoE block: dense MLP + routed experts per layer. */
37
+ enableMoeBlock: boolean;
38
+ numExperts: number;
39
+ topKExperts: number;
40
+ moeIntermediateSize: number;
41
+ /** Qwen3.5 hybrid (gated-DeltaNet) geometry; 0/false for other families. */
42
+ linearNumValueHeads: number;
43
+ linearNumKeyHeads: number;
44
+ linearKeyHeadDim: number;
45
+ linearValueHeadDim: number;
46
+ linearConvKernelDim: number;
47
+ /** Every `fullAttentionInterval`-th layer is full-attention (Qwen3.5). */
48
+ fullAttentionInterval: number;
49
+ /** Qwen3.5 full-attention output gate: q_proj emits 2× and gates the output. */
50
+ attnOutputGate: boolean;
51
+ /** Rotary fraction of head_dim (Qwen3.5 full attention: 0.25 → 64 of 256). */
52
+ partialRotaryFactor: number;
53
+ ropeParameters: Record<string, RopeParams>;
54
+ finalLogitSoftcapping: number | null;
55
+ tieWordEmbeddings: boolean;
56
+ bosTokenId: number;
57
+ eosTokenId: number | number[];
58
+ }
59
+
60
+ export interface QuantSpec {
61
+ bits: number;
62
+ groupSize: number;
63
+ mode: string;
64
+ }
65
+
66
+ export interface QuantizationConfig {
67
+ default: QuantSpec;
68
+ /** Per-module overrides keyed by module path (e.g. "language_model.model.embed_tokens"). */
69
+ perLayer: Map<string, QuantSpec>;
70
+ }
71
+
72
+ export interface KvQuantSpec {
73
+ layerIdx: number;
74
+ bits: number;
75
+ groupSize: number;
76
+ }
77
+
78
+ export interface ModelConfig {
79
+ modelDir: string;
80
+ modelType: string;
81
+ architectures: string[];
82
+ dtype: string;
83
+ text: TextConfig;
84
+ quantization: QuantizationConfig | null;
85
+ kvQuant: KvQuantSpec[] | null;
86
+ hasVisionSidecar: boolean;
87
+ eosTokenIds: number[];
88
+ raw: Record<string, unknown>;
89
+ }
90
+
91
+ function parseRope(raw: Record<string, any>): Record<string, RopeParams> {
92
+ const out: Record<string, RopeParams> = {};
93
+ for (const [kind, p] of Object.entries(raw ?? {})) {
94
+ out[kind] = {
95
+ ropeTheta: p.rope_theta,
96
+ ropeType: p.rope_type ?? "default",
97
+ partialRotaryFactor: p.partial_rotary_factor ?? 1.0,
98
+ factor: p.factor ?? 1.0,
99
+ };
100
+ }
101
+ return out;
102
+ }
103
+
104
+ function parseQuantization(raw: Record<string, any> | undefined): QuantizationConfig | null {
105
+ if (!raw) return null;
106
+ const def: QuantSpec = {
107
+ bits: raw.bits,
108
+ groupSize: raw.group_size,
109
+ mode: raw.mode ?? "affine",
110
+ };
111
+ const perLayer = new Map<string, QuantSpec>();
112
+ for (const [key, value] of Object.entries(raw)) {
113
+ if (key === "bits" || key === "group_size" || key === "mode") continue;
114
+ if (typeof value === "object" && value !== null) {
115
+ perLayer.set(key, {
116
+ bits: (value as any).bits ?? def.bits,
117
+ groupSize: (value as any).group_size ?? def.groupSize,
118
+ mode: (value as any).mode ?? def.mode,
119
+ });
120
+ } else if (value === false) {
121
+ // mlx convention: `"layer": false` means not quantized
122
+ perLayer.set(key, { bits: 0, groupSize: 0, mode: "none" });
123
+ }
124
+ }
125
+ return { default: def, perLayer };
126
+ }
127
+
128
+ /** Quant spec for a module path, or null if the module is unquantized. */
129
+ export function quantFor(q: QuantizationConfig | null, modulePath: string): QuantSpec | null {
130
+ if (!q) return null;
131
+ const spec = q.perLayer.get(modulePath) ?? q.default;
132
+ return spec.mode === "none" ? null : spec;
133
+ }
134
+
135
+ export async function loadModelConfig(modelDir: string): Promise<ModelConfig> {
136
+ const raw = (await Bun.file(`${modelDir}/config.json`).json()) as Record<string, any>;
137
+ // Gemma 4 unified nests the LM config; plain text models keep it at top level.
138
+ const t = (raw.text_config ?? raw) as Record<string, any>;
139
+ const modelType = raw.model_type;
140
+ const isLlama = modelType === "llama";
141
+ const isQwen35 = typeof modelType === "string" && modelType.startsWith("qwen3_5");
142
+ // Qwen3.5 rope_parameters is a flat dict ({type, rope_theta, mrope_section,
143
+ // partial_rotary_factor}), not the gemma per-attention-type map — and
144
+ // type "default" means plain partial nn.RoPE (mrope_section ignored for text).
145
+ const qwenRope = (t.rope_parameters ?? {}) as Record<string, any>;
146
+
147
+ const text: TextConfig = {
148
+ hiddenSize: t.hidden_size,
149
+ numHiddenLayers: t.num_hidden_layers,
150
+ numAttentionHeads: t.num_attention_heads,
151
+ numKeyValueHeads: t.num_key_value_heads,
152
+ headDim: t.head_dim ?? Math.floor(t.hidden_size / t.num_attention_heads),
153
+ numGlobalKeyValueHeads: t.num_global_key_value_heads ?? t.num_key_value_heads,
154
+ globalHeadDim: t.global_head_dim ?? t.head_dim ?? Math.floor(t.hidden_size / t.num_attention_heads),
155
+ attentionKEqV: t.attention_k_eq_v ?? false,
156
+ intermediateSize: t.intermediate_size,
157
+ hiddenActivation: t.hidden_activation ?? t.hidden_act ?? "gelu_pytorch_tanh",
158
+ rmsNormEps: t.rms_norm_eps,
159
+ vocabSize: t.vocab_size,
160
+ maxPositionEmbeddings: t.max_position_embeddings,
161
+ slidingWindow: t.sliding_window ?? 0,
162
+ layerTypes: t.layer_types ?? (isLlama ? Array(t.num_hidden_layers).fill("full_attention") : []),
163
+ hiddenSizePerLayerInput: t.hidden_size_per_layer_input ?? 0,
164
+ vocabSizePerLayerInput: t.vocab_size_per_layer_input ?? t.vocab_size,
165
+ numKvSharedLayers: t.num_kv_shared_layers ?? 0,
166
+ enableMoeBlock: t.enable_moe_block ?? false,
167
+ numExperts: t.num_experts ?? 0,
168
+ topKExperts: t.top_k_experts ?? 0,
169
+ moeIntermediateSize: t.moe_intermediate_size ?? 0,
170
+ linearNumValueHeads: t.linear_num_value_heads ?? 0,
171
+ linearNumKeyHeads: t.linear_num_key_heads ?? 0,
172
+ linearKeyHeadDim: t.linear_key_head_dim ?? 0,
173
+ linearValueHeadDim: t.linear_value_head_dim ?? 0,
174
+ linearConvKernelDim: t.linear_conv_kernel_dim ?? 0,
175
+ fullAttentionInterval: t.full_attention_interval ?? 0,
176
+ attnOutputGate: t.attn_output_gate ?? false,
177
+ partialRotaryFactor:
178
+ qwenRope.partial_rotary_factor ?? t.partial_rotary_factor ?? 1.0,
179
+ ropeParameters: isQwen35
180
+ ? {
181
+ full_attention: {
182
+ ropeTheta: qwenRope.rope_theta ?? t.rope_theta ?? 10000,
183
+ ropeType: "default",
184
+ partialRotaryFactor: qwenRope.partial_rotary_factor ?? 1.0,
185
+ factor: 1.0,
186
+ },
187
+ }
188
+ : t.rope_parameters
189
+ ? parseRope(t.rope_parameters)
190
+ : isLlama
191
+ ? {
192
+ full_attention: {
193
+ ropeTheta: t.rope_theta ?? 10000,
194
+ ropeType: "default",
195
+ partialRotaryFactor: 1.0,
196
+ factor: 1.0,
197
+ },
198
+ }
199
+ : {},
200
+ finalLogitSoftcapping: t.final_logit_softcapping ?? null,
201
+ tieWordEmbeddings: t.tie_word_embeddings ?? raw.tie_word_embeddings ?? false,
202
+ bosTokenId: t.bos_token_id ?? raw.bos_token_id,
203
+ eosTokenId: t.eos_token_id ?? raw.eos_token_id,
204
+ };
205
+
206
+ let kvQuant: KvQuantSpec[] | null = null;
207
+ if (await Bun.file(`${modelDir}/kv_config.json`).exists()) {
208
+ const kv = (await Bun.file(`${modelDir}/kv_config.json`).json()) as any[];
209
+ kvQuant = kv.map((e) => ({
210
+ layerIdx: e.layer_idx,
211
+ bits: e.bits,
212
+ groupSize: e.group_size,
213
+ }));
214
+ }
215
+
216
+ const eos = raw.eos_token_id ?? text.eosTokenId;
217
+ return {
218
+ modelDir,
219
+ modelType,
220
+ architectures: raw.architectures ?? [],
221
+ dtype: raw.dtype ?? "bfloat16",
222
+ text,
223
+ quantization: parseQuantization(raw.quantization ?? raw.quantization_config),
224
+ kvQuant,
225
+ hasVisionSidecar: await Bun.file(`${modelDir}/optiq_vision.safetensors`).exists(),
226
+ eosTokenIds: Array.isArray(eos) ? eos : [eos],
227
+ raw,
228
+ };
229
+ }
@@ -0,0 +1,122 @@
1
+ // curve-sampler.ts — the v2 log-prob transfer-curve sampler. Replaces the whole
2
+ // temperature+softmax stage with a drawn MONOTONE CUBIC curve in log-probability
3
+ // space:
4
+ //
5
+ // p_out_i = exp(curve(log p_in_i)) / Σ_j exp(curve(log p_in_j))
6
+ //
7
+ // `lp` reaching the sampler is already log p_in (toLogprobs = logits − logsumexp),
8
+ // so the curve maps it directly and an identity curve reproduces temperature 1.
9
+ // The curve is a Fritsch–Carlson monotone cubic Hermite (PCHIP) through N movable
10
+ // control points in nat log-prob space (DaVinci-curves style — the endpoints start
11
+ // at the corners but are draggable too). Smooth, never overshoots, monotone. A
12
+ // control point dragged below the diagonal suppresses those tokens; the gate is
13
+ // emergent from the curve shape, not a separate threshold.
14
+ //
15
+ // Reading the curve: at a given p_in, ABOVE the diagonal raises those tokens,
16
+ // BELOW lowers them (renorm makes the literal number an intent); the SLOPE across
17
+ // a region sets how evenly the tokens inside it spread (flat<1 even, steep>1 separated).
18
+
19
+ import { MlxArray } from "./mlx/array";
20
+ import * as ops from "./mlx/ops";
21
+
22
+ export interface CurvePoint { x_pct: number; y_pct: number; }
23
+ export interface CurveParams {
24
+ space?: "logprob";
25
+ points: CurvePoint[]; // ≥2 control points, all movable; first/last are the endpoints
26
+ monotonic?: boolean;
27
+ }
28
+
29
+ /** Bottom-left default anchor / plot floor (p = 1e-6). */
30
+ export const CURVE_UMIN = Math.log(1e-6);
31
+ const L = (pt: CurvePoint): [number, number] => [Math.log(Math.max(pt.x_pct, 1e-9) / 100), Math.log(Math.max(pt.y_pct, 1e-9) / 100)];
32
+
33
+ /** Control points in nat log-prob space, sorted ascending in x. */
34
+ function knotArrays(p: CurveParams): { X: number[]; Y: number[] } {
35
+ const k = p.points.map(L).sort((a, b) => a[0] - b[0]);
36
+ return { X: k.map((q) => q[0]), Y: k.map((q) => q[1]) };
37
+ }
38
+
39
+ /** Consecutive-point secants, top→bottom (region slopes; what the editor readout shows). */
40
+ export function curveSecants(p: CurveParams): number[] {
41
+ const { X, Y } = knotArrays(p);
42
+ const s: number[] = [];
43
+ for (let i = X.length - 1; i > 0; i--) s.push((Y[i]! - Y[i - 1]!) / (X[i]! - X[i - 1]!));
44
+ return s;
45
+ }
46
+ /** True iff every consecutive-point secant is ≥ 0 (monotone non-decreasing). */
47
+ export function isMonotone(p: CurveParams): boolean {
48
+ return p.points.length >= 2 && curveSecants(p).every((s) => s >= -1e-12);
49
+ }
50
+
51
+ interface Seg { A: number; B: number; C: number; D: number } // cubic in u: A+Bu+Cu²+Du³
52
+ export interface Spline { X: number[]; Y: number[]; M: number[]; segs: Seg[] }
53
+
54
+ function cubicInU(x0: number, h: number, y0: number, y1: number, m0: number, m1: number): Seg {
55
+ const c0 = y0, c1 = h * m0;
56
+ const c2 = -3 * y0 - 2 * h * m0 + 3 * y1 - h * m1;
57
+ const c3 = 2 * y0 + h * m0 - 2 * y1 + h * m1;
58
+ const s = 1 / h, s2 = s * s, s3 = s2 * s;
59
+ let A = c0, B = 0, C = 0, D = 0;
60
+ B += c1 * s; A += -c1 * s * x0;
61
+ C += c2 * s2; B += -2 * x0 * c2 * s2; A += x0 * x0 * c2 * s2;
62
+ D += c3 * s3; C += -3 * x0 * c3 * s3; B += 3 * x0 * x0 * c3 * s3; A += -x0 * x0 * x0 * c3 * s3;
63
+ return { A, B, C, D };
64
+ }
65
+
66
+ /** Monotone cubic (Fritsch–Carlson) over the N control points. */
67
+ export function buildSpline(p: CurveParams): Spline {
68
+ const { X, Y } = knotArrays(p);
69
+ const n = X.length;
70
+ const Dsec: number[] = [];
71
+ for (let i = 0; i < n - 1; i++) Dsec.push((Y[i + 1]! - Y[i]!) / (X[i + 1]! - X[i]!));
72
+ const M = new Array<number>(n);
73
+ M[0] = Dsec[0]!; M[n - 1] = Dsec[n - 2]!;
74
+ for (let i = 1; i < n - 1; i++) {
75
+ if (Dsec[i - 1]! * Dsec[i]! <= 0) { M[i] = 0; continue; }
76
+ const h1 = X[i]! - X[i - 1]!, h2 = X[i + 1]! - X[i]!;
77
+ const w1 = 2 * h2 + h1, w2 = h2 + 2 * h1;
78
+ M[i] = (w1 + w2) / (w1 / Dsec[i - 1]! + w2 / Dsec[i]!);
79
+ }
80
+ const segs: Seg[] = [];
81
+ for (let i = 0; i < n - 1; i++) segs.push(cubicInU(X[i]!, X[i + 1]! - X[i]!, Y[i]!, Y[i + 1]!, M[i]!, M[i + 1]!));
82
+ return { X, Y, M, segs };
83
+ }
84
+
85
+ /** Scalar eval (editor preview / tests). Extrapolates linearly past both endpoints. */
86
+ export function evalSpline(sp: Spline, u: number): number {
87
+ const n = sp.X.length;
88
+ if (u < sp.X[0]!) return sp.Y[0]! + Math.max(sp.M[0]!, 1) * (u - sp.X[0]!); // deep tail: slope ≥1, stays gated
89
+ if (u > sp.X[n - 1]!) return sp.Y[n - 1]! + sp.M[n - 1]! * (u - sp.X[n - 1]!);
90
+ let i = sp.segs.length - 1;
91
+ while (i > 0 && u < sp.X[i]!) i--;
92
+ const s = sp.segs[i]!;
93
+ return s.A + s.B * u + s.C * u * u + s.D * u * u * u;
94
+ }
95
+ export function curveAt(p: CurveParams, u: number): number { return evalSpline(buildSpline(p), u); }
96
+
97
+ /** Apply the monotone-cubic transfer to normalized logprobs `lp` [.., V] (on-device). */
98
+ export function applyCurve(lp: MlxArray, p: CurveParams): MlxArray {
99
+ const sp = buildSpline(p);
100
+ const n = sp.X.length;
101
+ const tmp: MlxArray[] = [];
102
+ const k = <T extends MlxArray>(a: T): T => { tmp.push(a); return a; };
103
+ const C = (v: number): MlxArray => k(ops.scalarLike(v, lp));
104
+ const lp2 = k(ops.mul(lp, lp));
105
+ const lp3 = k(ops.mul(lp2, lp));
106
+ const seg = (s: Seg): MlxArray => k(ops.add(
107
+ k(ops.add(C(s.A), k(ops.mulScalar(lp, s.B)))),
108
+ k(ops.add(k(ops.mulScalar(lp2, s.C)), k(ops.mulScalar(lp3, s.D)))),
109
+ ));
110
+ // below the bottom point: slope ≥ 1 (deep tail stays gated)
111
+ const lowSlope = Math.max(sp.M[0]!, 1);
112
+ let out: MlxArray = k(ops.add(C(sp.Y[0]! - lowSlope * sp.X[0]!), k(ops.mulScalar(lp, lowSlope))));
113
+ for (let i = 0; i < sp.segs.length; i++) {
114
+ const next = ops.where(k(ops.greaterEqual(lp, C(sp.X[i]!))), seg(sp.segs[i]!), out);
115
+ tmp.push(next); out = next;
116
+ }
117
+ // above the top point: linear extrapolation with the top tangent (if it was dragged left of 100%)
118
+ const aboveExt = k(ops.add(C(sp.Y[n - 1]! - sp.M[n - 1]! * sp.X[n - 1]!), k(ops.mulScalar(lp, sp.M[n - 1]!))));
119
+ const result = ops.where(k(ops.greaterEqual(lp, C(sp.X[n - 1]!))), aboveExt, out);
120
+ for (const a of tmp) a.dispose();
121
+ return result;
122
+ }