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,158 @@
1
+ // Safetensors reader: mmap the file (via libc — see mmap.ts for why not
2
+ // Bun.mmap), parse the JSON header, expose tensors as raw pointers into
3
+ // the mapping. No tensor bytes are ever copied; pages materialize only
4
+ // when something (the GPU) touches them.
5
+ //
6
+ // Format: [u64 LE header_len][header_len bytes of JSON][data...]
7
+ // Header: { "<name>": { dtype, shape, data_offsets: [begin, end] }, ... }
8
+ // with optional "__metadata__"; offsets are relative to the data section.
9
+
10
+ import { MmapFile } from "./mmap";
11
+
12
+ export type SafetensorsDtype =
13
+ | "BOOL" | "U8" | "I8" | "U16" | "I16" | "U32" | "I32" | "U64" | "I64"
14
+ | "F16" | "BF16" | "F32" | "F64";
15
+
16
+ export const DTYPE_SIZE: Record<SafetensorsDtype, number> = {
17
+ BOOL: 1, U8: 1, I8: 1, U16: 2, I16: 2, U32: 4, I32: 4, U64: 8, I64: 8,
18
+ F16: 2, BF16: 2, F32: 4, F64: 8,
19
+ };
20
+
21
+ export interface TensorInfo {
22
+ name: string;
23
+ dtype: SafetensorsDtype;
24
+ shape: number[];
25
+ /** Byte range relative to the start of the data section. */
26
+ begin: number;
27
+ end: number;
28
+ }
29
+
30
+ export class SafetensorsFile {
31
+ readonly path: string;
32
+ readonly metadata: Record<string, string>;
33
+ readonly tensors: Map<string, TensorInfo>;
34
+ readonly mmap: MmapFile;
35
+ readonly #dataStart: number;
36
+
37
+ private constructor(
38
+ path: string,
39
+ mmap: MmapFile,
40
+ tensors: Map<string, TensorInfo>,
41
+ metadata: Record<string, string>,
42
+ dataStart: number,
43
+ ) {
44
+ this.path = path;
45
+ this.mmap = mmap;
46
+ this.tensors = tensors;
47
+ this.metadata = metadata;
48
+ this.#dataStart = dataStart;
49
+ }
50
+
51
+ static open(path: string): SafetensorsFile {
52
+ const mmap = MmapFile.open(path);
53
+ if (mmap.size < 8) throw new Error(`${path}: too small for safetensors`);
54
+
55
+ const lenBytes = mmap.view(0, 8);
56
+ const headerLen = Number(
57
+ new DataView(lenBytes.buffer, lenBytes.byteOffset, 8).getBigUint64(0, true),
58
+ );
59
+ const dataStart = 8 + headerLen;
60
+ if (dataStart > mmap.size)
61
+ throw new Error(`${path}: header length ${headerLen} exceeds file size`);
62
+
63
+ const headerJson = new TextDecoder().decode(mmap.view(8, headerLen));
64
+ const header = JSON.parse(headerJson) as Record<
65
+ string,
66
+ { dtype: SafetensorsDtype; shape: number[]; data_offsets: [number, number] }
67
+ >;
68
+
69
+ const tensors = new Map<string, TensorInfo>();
70
+ let metadata: Record<string, string> = {};
71
+ for (const [name, entry] of Object.entries(header)) {
72
+ if (name === "__metadata__") {
73
+ metadata = entry as unknown as Record<string, string>;
74
+ continue;
75
+ }
76
+ const { dtype, shape, data_offsets: [begin, end] } = entry;
77
+ if (!(dtype in DTYPE_SIZE))
78
+ throw new Error(`${path}: tensor ${name} has unsupported dtype ${dtype}`);
79
+ const expected = shape.reduce((a, b) => a * b, 1) * DTYPE_SIZE[dtype];
80
+ if (end - begin !== expected)
81
+ throw new Error(
82
+ `${path}: tensor ${name} byte range ${end - begin} != shape ${JSON.stringify(shape)} × ${dtype}`,
83
+ );
84
+ if (dataStart + end > mmap.size)
85
+ throw new Error(`${path}: tensor ${name} extends past end of file`);
86
+ tensors.set(name, { name, dtype, shape, begin, end });
87
+ }
88
+ return new SafetensorsFile(path, mmap, tensors, metadata, dataStart);
89
+ }
90
+
91
+ #info(name: string): TensorInfo {
92
+ const t = this.tensors.get(name);
93
+ if (!t) throw new Error(`${this.path}: no tensor named ${name}`);
94
+ return t;
95
+ }
96
+
97
+ /** Raw pointer to the tensor's first byte (for zero-copy FFI handoff). */
98
+ pointer(name: string): number {
99
+ return this.mmap.pointer(this.#dataStart + this.#info(name).begin);
100
+ }
101
+
102
+ /** JS view of a tensor's bytes (small tensors / tests only). */
103
+ view(name: string): Uint8Array {
104
+ const t = this.#info(name);
105
+ return this.mmap.view(this.#dataStart + t.begin, t.end - t.begin);
106
+ }
107
+ }
108
+
109
+ /** A sharded model directory: index json + N safetensors files. */
110
+ export class ShardedSafetensors {
111
+ readonly files = new Map<string, SafetensorsFile>();
112
+ readonly tensorToFile = new Map<string, SafetensorsFile>();
113
+
114
+ static async open(modelDir: string): Promise<ShardedSafetensors> {
115
+ const self = new ShardedSafetensors();
116
+ const indexPath = `${modelDir}/model.safetensors.index.json`;
117
+ if (await Bun.file(indexPath).exists()) {
118
+ const index = (await Bun.file(indexPath).json()) as {
119
+ weight_map: Record<string, string>;
120
+ };
121
+ for (const [tensor, file] of Object.entries(index.weight_map)) {
122
+ let sf = self.files.get(file);
123
+ if (!sf) {
124
+ sf = SafetensorsFile.open(`${modelDir}/${file}`);
125
+ self.files.set(file, sf);
126
+ }
127
+ self.tensorToFile.set(tensor, sf);
128
+ }
129
+ } else {
130
+ const sf = SafetensorsFile.open(`${modelDir}/model.safetensors`);
131
+ self.files.set("model.safetensors", sf);
132
+ for (const name of sf.tensors.keys()) self.tensorToFile.set(name, sf);
133
+ }
134
+ return self;
135
+ }
136
+
137
+ get tensorNames(): string[] {
138
+ return [...this.tensorToFile.keys()].sort();
139
+ }
140
+
141
+ #file(name: string): SafetensorsFile {
142
+ const sf = this.tensorToFile.get(name);
143
+ if (!sf) throw new Error(`no tensor named ${name}`);
144
+ return sf;
145
+ }
146
+
147
+ info(name: string): TensorInfo {
148
+ return this.#file(name).tensors.get(name)!;
149
+ }
150
+
151
+ pointer(name: string): number {
152
+ return this.#file(name).pointer(name);
153
+ }
154
+
155
+ view(name: string): Uint8Array {
156
+ return this.#file(name).view(name);
157
+ }
158
+ }
package/src/sampler.ts ADDED
@@ -0,0 +1,517 @@
1
+ // Sampling — port of mlx-lm's sample_utils.make_sampler /
2
+ // make_logits_processors (temperature, top-p, top-k, repetition penalty).
3
+ // All filtering happens on-device; only the chosen token id crosses to JS.
4
+ // Seeded: each step derives a fresh key from (seed, step) so runs are
5
+ // reproducible without sharing global RNG state.
6
+
7
+ import { Dtype } from "./mlx/ffi";
8
+ import { MlxArray } from "./mlx/array";
9
+ import * as ops from "./mlx/ops";
10
+ import { applyCurve, type CurveParams } from "./curve-sampler";
11
+
12
+ /** Resolved HLG sampling config (the user-facing knobs). The mid gain is NOT
13
+ * here — it folds from temperature (m = 1/T) when the sampler is built. */
14
+ export interface HlgConfig {
15
+ enabled: boolean;
16
+ width: number;
17
+ shoulder: number;
18
+ toe: number;
19
+ pivot?: "top" | "entropy" | "median";
20
+ pivotOffset: number;
21
+ /** Use the LITERAL HLG OETF (gamma below 1/12, log above) instead of the
22
+ * parametric toe/mid/shoulder curve. Ignores gain/width/shoulder/toe/pivot. */
23
+ oetf?: boolean;
24
+ /** Use the LITERAL HLG EOTF (inverse OETF → OOTF gamma) — the decode/display
25
+ * direction (suppresses the tail). Ignores the parametric knobs. */
26
+ eotf?: boolean;
27
+ /** EOTF only: nominal peak display luminance L_W (cd/m²) → OOTF gamma.
28
+ * Default 1000 (γ = 1.2). Lower = flatter (more diverse), higher = sharper. */
29
+ lw?: number;
30
+ /** Use the FULL HLG chain: OETF signal shape → OOTF scaling (applyHlgPipeline).
31
+ * γ from `lw`. Ignores the parametric knobs. */
32
+ pipeline?: boolean;
33
+ /** Pipeline only: α = max brightness — the top logit the OOTF scales everything
34
+ * under ("the most confidence we want"). Default 5. For the shaper, reused as
35
+ * `out_scale` (default 12). */
36
+ maxBrightness?: number;
37
+ /** Use the user-specified HLGShaper (windowed anchor → piecewise OETF with a
38
+ * cubic suppress-toe → OOTF → ×out_scale). `lw`→L_W, `maxBrightness`→out_scale. */
39
+ shaper?: boolean;
40
+ /** Shaper only: W = nats of headroom below the top logit that span the curve
41
+ * (`x = clamp((ℓ−ℓmax)/W + 1, 0, 1)`). Default 10. */
42
+ window?: number;
43
+ /** Shaper only: s_m — mid sharpness (how hard above-median tokens are elevated). */
44
+ sM?: number;
45
+ /** Shaper only: A — shoulder compression (how hard top-end confidence is tamed). */
46
+ shoulderA?: number;
47
+ /** Shaper only: target top-to-reference logit gap (out_scale auto-derives from this
48
+ * so it's decoupled from W). Default 15. */
49
+ targetGap?: number;
50
+ /** Shaper only: reference token nats below the top for the auto out_scale. Default 4. */
51
+ refGap?: number;
52
+ /** Shaper only: curve geometry (the pivots / toe / mid power). Default to the
53
+ * HLGShaper constants (xM 0.55, yM 0.5, xFloor 0.2, yFloor 0.18, p 2.0) when unset. */
54
+ xM?: number; yM?: number; xFloor?: number; yFloor?: number; p?: number;
55
+ /** Optional explicit mid gain m. Default: folds from temperature (m = 1/T),
56
+ * so temperature stays the contrast knob. An explicit value decouples mid
57
+ * contrast from temperature — needed to probe mid-boost while holding the
58
+ * model's recommended temperature fixed. */
59
+ gain?: number;
60
+ }
61
+
62
+ export interface SamplerOptions {
63
+ temperature?: number;
64
+ topP?: number;
65
+ topK?: number;
66
+ seed?: number;
67
+ /** HLG tone-curve sampling. When enabled, replaces temperature's flat slope
68
+ * with the piecewise curve (temperature becomes the mid gain). Off/undefined
69
+ * ⇒ the plain temperature path, unchanged. docs/design/hlg-sampling.md. */
70
+ hlg?: HlgConfig;
71
+ /** v2 log-prob transfer-curve sampler. When set, the drawn monotone curve
72
+ * REPLACES temperature+softmax entirely (stochastic, seeded) — see
73
+ * src/curve-sampler.ts. Identity curve ≡ temperature 1. */
74
+ curve?: CurveParams;
75
+ }
76
+
77
+ export interface LogitsProcessorOptions {
78
+ repetitionPenalty?: number;
79
+ repetitionContextSize?: number;
80
+ }
81
+
82
+ /** logprobs [1, V] → sampled token array (uint32, shape [1]). */
83
+ export type Sampler = (logprobs: MlxArray, step: number) => MlxArray;
84
+
85
+ /** (deviceTokens [n] | null, logits [1, V]) → logits [1, V]. */
86
+ export type LogitsProcessor = (tokens: MlxArray | null, logits: MlxArray) => MlxArray;
87
+
88
+ const GOLDEN = 0x9e3779b97f4a7c15n;
89
+
90
+ function stepKey(seed: number, step: number): MlxArray {
91
+ const mixed = (BigInt(seed) ^ ((BigInt(step) + 1n) * GOLDEN)) & 0xffffffffffffffffn;
92
+ return ops.randomKey(mixed);
93
+ }
94
+
95
+ function negInfLike(a: MlxArray): MlxArray {
96
+ return ops.scalarLike(-Infinity, a);
97
+ }
98
+
99
+ /** apply_top_p: keep the smallest set of tokens whose cumulative
100
+ * probability exceeds top_p; others → -inf. */
101
+ function applyTopP(lp: MlxArray, topP: number): MlxArray {
102
+ const probs = ops.exp(lp);
103
+ const sortedIdx = ops.argsortAxis(lp, -1);
104
+ const sortedProbs = ops.takeAlongAxis(probs, sortedIdx, -1);
105
+ const cum = ops.cumsum(sortedProbs, -1);
106
+ // scatter arange back through sortedIdx to invert the permutation
107
+ const V = lp.shape[lp.shape.length - 1]!;
108
+ const zerosIdx = ops.zeros(sortedIdx.shape, sortedIdx.dtype);
109
+ const ar = ops.arange(0, V, 1, sortedIdx.dtype);
110
+ const arB = ops.reshape(ar, sortedIdx.shape);
111
+ const inverse = ops.putAlongAxis(zerosIdx, sortedIdx, arB, -1);
112
+ const cumOrig = ops.takeAlongAxis(cum, inverse, -1);
113
+ const threshold = ops.scalarLike(1 - topP, cumOrig);
114
+ const keep = ops.less(threshold, cumOrig); // cum > 1 - topP
115
+ const ninf = negInfLike(lp);
116
+ const out = ops.where(keep, lp, ninf);
117
+ for (const a of [probs, sortedIdx, sortedProbs, cum, zerosIdx, ar, arB, inverse, cumOrig, threshold, keep, ninf])
118
+ a.dispose();
119
+ return out;
120
+ }
121
+
122
+ /** apply_top_k: all but the k highest logprobs → -inf. */
123
+ function applyTopK(lp: MlxArray, topK: number): MlxArray {
124
+ const V = lp.shape[lp.shape.length - 1]!;
125
+ const negLp = ops.neg(lp);
126
+ const part = ops.argpartitionAxis(negLp, topK - 1, -1);
127
+ const maskIdx = part.slice([0, topK], [1, V]);
128
+ const ninf = negInfLike(lp);
129
+ const out = ops.putAlongAxis(lp, maskIdx, ninf, -1);
130
+ for (const a of [negLp, part, maskIdx, ninf]) a.dispose();
131
+ return out;
132
+ }
133
+
134
+ /** HLG sampling parameters — a piecewise tone curve on the logprobs.
135
+ * See docs/design/hlg-sampling.md. Knobs are in nats (the logprob unit). */
136
+ export interface HlgParams {
137
+ /** m — mid-region slope. Folds temperature: m = 1/temperature. */
138
+ gain: number;
139
+ /** w — half-width of the linear mid region. Infinity ⇒ no rolloff (pure temperature). */
140
+ width: number;
141
+ /** β_h — highlight rolloff scale. ≤ 0 ⇒ no shoulder (highlights stay linear). */
142
+ shoulder: number;
143
+ /** β_t — shadow rolloff scale. ≤ 0 ⇒ no toe (shadows stay linear). */
144
+ toe: number;
145
+ /** pivot (middle grey) placement — changes where the curve centers, hence its
146
+ * shape. "top": μ = max(ℓ) − offset. "entropy": μ = Σp·ℓ = −H (center of mass
147
+ * in tonal space). "median": μ = the logprob at the 50% cumulative-mass
148
+ * boundary (auto-levels). */
149
+ pivot?: "top" | "entropy" | "median";
150
+ /** shift applied to the pivot base: μ = base − pivotOffset. */
151
+ pivotOffset?: number;
152
+ }
153
+
154
+ const HLG_LOG_EPS = 1e-9;
155
+
156
+ /** Pivot (middle grey) μ_base for the tone curve, in logprob units — the same
157
+ * curve, centered three different ways. Operates on the full logprob vector. */
158
+ function hlgPivotBase(
159
+ lp: MlxArray,
160
+ mode: "top" | "entropy" | "median",
161
+ k: <T extends MlxArray>(a: T) => T,
162
+ ): MlxArray {
163
+ if (mode === "entropy") {
164
+ // center of mass in tonal space: μ = Σ p·ℓ = −H (p = exp(ℓ); ℓ are logprobs)
165
+ const probs = k(ops.exp(lp));
166
+ return k(ops.sumAxis(k(ops.mul(probs, lp)), -1, true));
167
+ }
168
+ if (mode === "median") {
169
+ // logprob at the 50% cumulative-mass boundary (auto-levels / histogram median)
170
+ const probs = k(ops.exp(lp));
171
+ const idxDesc = k(ops.argsortAxis(k(ops.neg(lp)), -1)); // ascending of −ℓ = ℓ descending
172
+ const sortedProbs = k(ops.takeAlongAxis(probs, idxDesc, -1));
173
+ const sortedLp = k(ops.takeAlongAxis(lp, idxDesc, -1));
174
+ const cum = k(ops.cumsum(sortedProbs, -1));
175
+ const keep = k(ops.greaterEqual(cum, k(ops.scalarLike(0.5, cum))));
176
+ const masked = k(ops.where(keep, sortedLp, k(ops.scalarLike(-Infinity, sortedLp))));
177
+ return k(ops.maxAxis(masked, -1, true)); // first crossover = max logprob of the ≥0.5 suffix
178
+ }
179
+ return k(ops.maxAxis(lp, -1, true)); // "top": the peak
180
+ }
181
+
182
+ /** Remap logprobs through the centered curve g(z), z = ℓ − μ:
183
+ * toe (z < −w): −m·w − m·β_t·ln(1 + (−z − w)/β_t) gentle shadow rolloff
184
+ * mid (|z| ≤ w): m·z contrast gain
185
+ * shoulder(z > w): m·w + m·β_h·ln(1 + ( z − w)/β_h) highlight rolloff
186
+ * Monotone in ℓ ⇒ ranking-preserving (argmax unchanged). With width = ∞ (or
187
+ * β_h, β_t ≤ 0) it returns mulScalar(lp, m) — temperature T = 1/m — bit-exact
188
+ * by construction (the same op the temperature path uses). `-inf` (masked)
189
+ * tokens map to `-inf`. */
190
+ export function applyHlg(lp: MlxArray, p: HlgParams): MlxArray {
191
+ const m = p.gain;
192
+ const hasShoulder = Number.isFinite(p.width) && p.shoulder > 0;
193
+ const hasToe = Number.isFinite(p.width) && p.toe > 0;
194
+ // No-rolloff regime ≡ temperature: identical op to the temperature sampler.
195
+ if (!hasShoulder && !hasToe) return ops.mulScalar(lp, m);
196
+
197
+ const w = p.width;
198
+ const off = p.pivotOffset ?? 0;
199
+ const tmp: MlxArray[] = [];
200
+ const k = <T extends MlxArray>(a: T): T => {
201
+ tmp.push(a);
202
+ return a;
203
+ };
204
+
205
+ // pivot μ = base(mode) − off ; centered coordinate z = ℓ − μ
206
+ const base = hlgPivotBase(lp, p.pivot ?? "top", k);
207
+ const mu = k(ops.sub(base, k(ops.scalarLike(off, base))));
208
+ const z = k(ops.sub(lp, mu));
209
+ const wC = k(ops.scalarLike(w, lp));
210
+ let g: MlxArray = k(ops.mulScalar(z, m)); // mid: m·z
211
+
212
+ if (hasShoulder) {
213
+ const bh = p.shoulder;
214
+ const inner = k(ops.add(k(ops.scalarLike(1, lp)), k(ops.mulScalar(k(ops.sub(z, wC)), 1 / bh))));
215
+ const clamped = k(ops.maximum(inner, k(ops.scalarLike(HLG_LOG_EPS, lp))));
216
+ const shoulder = k(ops.add(k(ops.scalarLike(m * w, lp)), k(ops.mulScalar(k(ops.log(clamped)), m * bh))));
217
+ g = k(ops.where(k(ops.greaterEqual(z, wC)), shoulder, g));
218
+ }
219
+ if (hasToe) {
220
+ const bt = p.toe;
221
+ const negz = k(ops.neg(z));
222
+ const inner = k(ops.add(k(ops.scalarLike(1, lp)), k(ops.mulScalar(k(ops.sub(negz, wC)), 1 / bt))));
223
+ const clamped = k(ops.maximum(inner, k(ops.scalarLike(HLG_LOG_EPS, lp))));
224
+ const toe = k(ops.sub(k(ops.scalarLike(-m * w, lp)), k(ops.mulScalar(k(ops.log(clamped)), m * bt))));
225
+ g = k(ops.where(k(ops.less(z, k(ops.scalarLike(-w, lp)))), toe, g));
226
+ }
227
+
228
+ for (const a of tmp) if (a !== g) a.dispose();
229
+ return g;
230
+ }
231
+
232
+ // Literal HLG OETF (ARIB STD-B67 / ITU-R BT.2100) — the actual transfer
233
+ // function, exact constants. Input L = p/p_max ∈ (0,1] (exposure-normalized: the
234
+ // top token is reference white). Output V ∈ [0,1]:
235
+ // V = √3 · √L for 0 ≤ L ≤ 1/12 (gamma/sqrt: shadows → 0 softly)
236
+ // V = a·ln(12L − b) + c for 1/12 < L ≤ 1 (log: highlights compressed)
237
+ // We sample ∝ V by feeding log(V) to the categorical draw. Monotone in ℓ ⇒
238
+ // ranking-preserving. No knobs — this is HLG itself.
239
+ const HLG_A = 0.17883277;
240
+ const HLG_B = 0.28466892; // 1 − 4a
241
+ const HLG_C = 0.55991073; // 0.5 − a·ln(4a)
242
+ const HLG_SQRT3 = Math.sqrt(3);
243
+ const HLG_KNEE = 1 / 12;
244
+
245
+ export function applyHlgOetf(lp: MlxArray): MlxArray {
246
+ const tmp: MlxArray[] = [];
247
+ const k = <T extends MlxArray>(a: T): T => {
248
+ tmp.push(a);
249
+ return a;
250
+ };
251
+
252
+ // L = p / p_max = exp(ℓ − ℓmax) ∈ (0, 1]
253
+ const lmax = k(ops.maxAxis(lp, -1, true));
254
+ const L = k(ops.exp(k(ops.sub(lp, lmax))));
255
+
256
+ // lower (L ≤ 1/12): V = √3 · √L
257
+ const lo = k(ops.mulScalar(k(ops.sqrt(L)), HLG_SQRT3));
258
+ // upper (L > 1/12): V = a·ln(12L − b) + c (clamp the arg > 0 for the unused lane)
259
+ const arg = k(ops.maximum(k(ops.sub(k(ops.mulScalar(L, 12)), k(ops.scalarLike(HLG_B, L)))), k(ops.scalarLike(HLG_LOG_EPS, L))));
260
+ const hi = k(ops.add(k(ops.mulScalar(k(ops.log(arg)), HLG_A)), k(ops.scalarLike(HLG_C, L))));
261
+ const V = k(ops.where(k(ops.greaterEqual(L, k(ops.scalarLike(HLG_KNEE, L)))), hi, lo));
262
+
263
+ // sample ∝ V → logits = log(V)
264
+ const out = ops.log(k(ops.maximum(V, k(ops.scalarLike(HLG_LOG_EPS, V)))));
265
+ for (const a of tmp) a.dispose();
266
+ return out;
267
+ }
268
+
269
+ /** OOTF system gamma from nominal peak display luminance L_W (cd/m²).
270
+ * γ = 1.2 + 0.42·log10(L_W/1000); γ = 1.2 exactly at L_W = 1000. */
271
+ export function hlgGammaForLw(lw: number): number {
272
+ return 1.2 + 0.42 * Math.log10(lw / 1000);
273
+ }
274
+
275
+ // Literal HLG EOTF (signal → display linear) = inverse OETF then OOTF — the
276
+ // DECODE direction. Treat p/p_max as the signal E'. The inverse OETF squares the
277
+ // lower half (E = E'²/3) so the tail is SUPPRESSED (the opposite of the OETF,
278
+ // which expands it). The OOTF on a single-"pixel" (grayscale) distribution has
279
+ // no colour channels for luminance to couple, so it reduces exactly to a gamma
280
+ // power law E^γ — i.e. temperature. Sample ∝ E^γ ⇒ logits = γ·log(E).
281
+ export function applyHlgEotf(lp: MlxArray, gamma: number): MlxArray {
282
+ const tmp: MlxArray[] = [];
283
+ const k = <T extends MlxArray>(a: T): T => {
284
+ tmp.push(a);
285
+ return a;
286
+ };
287
+
288
+ const d = k(ops.sub(lp, k(ops.maxAxis(lp, -1, true)))); // ℓ − ℓmax ≤ 0
289
+ const Ep = k(ops.exp(d)); // signal E' = p/p_max ∈ (0, 1]
290
+ // log E from the inverse OETF, computed stably per branch (the deep tail
291
+ // underflows if E is formed directly, so the lower branch stays in log-space):
292
+ // E' ≤ 1/2: E = E'²/3 → log E = 2·(ℓ−ℓmax) − ln3 (exact)
293
+ // E' > 1/2: E = (exp((E'−c)/a)+b)/12 → log E = log(that) (well-conditioned)
294
+ const logElo = k(ops.sub(k(ops.mulScalar(d, 2)), k(ops.scalarLike(Math.log(3), d))));
295
+ const hiArg = k(ops.exp(k(ops.mulScalar(k(ops.sub(Ep, k(ops.scalarLike(HLG_C, Ep)))), 1 / HLG_A))));
296
+ const hiE = k(ops.mulScalar(k(ops.add(hiArg, k(ops.scalarLike(HLG_B, Ep)))), 1 / 12));
297
+ const logEhi = k(ops.log(hiE));
298
+ const logE = k(ops.where(k(ops.greaterEqual(Ep, k(ops.scalarLike(0.5, Ep)))), logEhi, logElo));
299
+ // OOTF (grayscale) → gamma: sample ∝ E^γ ⇒ logits = γ·log(E)
300
+ const out = ops.mulScalar(logE, gamma);
301
+ for (const a of tmp) a.dispose();
302
+ return out;
303
+ }
304
+
305
+ // The FULL HLG chain: OETF signal shape, THEN OOTF scaling.
306
+ // 1. E = p/p_max ∈ (0,1] (top token = the max-brightness reference)
307
+ // 2. E' = √(3E) for E ≤ 1/12 (gamma/√ toe below the knee)
308
+ // = a·ln(12E−b)+c for E > 1/12 (log highlights above the knee)
309
+ // 3. R_d = α · E'^γ (OOTF: α = max brightness, scales everything
310
+ // under it; γ = 1.2+0.42·log10(L_W/1000))
311
+ // R_d are the final logits → categorical. α > 0 ⇒ monotone (ranking preserved).
312
+ export function applyHlgPipeline(lp: MlxArray, alpha: number, gamma: number): MlxArray {
313
+ const tmp: MlxArray[] = [];
314
+ const k = <T extends MlxArray>(a: T): T => {
315
+ tmp.push(a);
316
+ return a;
317
+ };
318
+
319
+ // 1. E = p/p_max ∈ (0,1]
320
+ const E = k(ops.exp(k(ops.sub(lp, k(ops.maxAxis(lp, -1, true))))));
321
+ // 2. OETF: √(3E) below the knee, a·ln(12E−b)+c above
322
+ const lo = k(ops.sqrt(k(ops.mulScalar(E, 3))));
323
+ const arg = k(ops.maximum(k(ops.sub(k(ops.mulScalar(E, 12)), k(ops.scalarLike(HLG_B, E)))), k(ops.scalarLike(HLG_LOG_EPS, E))));
324
+ const hi = k(ops.add(k(ops.mulScalar(k(ops.log(arg)), HLG_A)), k(ops.scalarLike(HLG_C, E))));
325
+ const Eprime = k(ops.where(k(ops.greaterEqual(E, k(ops.scalarLike(HLG_KNEE, E)))), hi, lo));
326
+ // 3. OOTF: R_d = α · E'^γ = α · exp(γ·log E')
327
+ const EpGamma = k(ops.exp(k(ops.mulScalar(k(ops.log(k(ops.maximum(Eprime, k(ops.scalarLike(HLG_LOG_EPS, Eprime)))))), gamma))));
328
+ const out = ops.mulScalar(EpGamma, alpha);
329
+ for (const a of tmp) a.dispose();
330
+ return out;
331
+ }
332
+
333
+ // ── User-specified HLGShaper (windowed-anchor input) ────────────────────────
334
+ // logit → windowed-anchor signal x∈[0,1] (top W nats span the curve; min-max
335
+ // over the 262k vocab dumped every candidate into the shoulder, so we anchor at
336
+ // the top instead) → piecewise OETF (log shoulder above x_m, power mid, CUBIC
337
+ // toe that suppresses to 0) → OOTF (normalize to peak, ^γ) → ×out_scale →
338
+ // softmax. Geometry constants derived once from the default knobs, exactly as
339
+ // HLGShaper.__init__ does (cubic toe coeffs from a 2×2 solve).
340
+ export interface HlgShaperParams {
341
+ lw?: number; // L_W (cd/m²) → OOTF γ
342
+ outScale?: number; // explicit override; default auto-derived from targetGap/refGap
343
+ targetGap?: number; // desired top-1-to-reference final-logit gap (sets sharpness, W-independent)
344
+ refGap?: number; // reference token sits this many nats below the top
345
+ window?: number; // W: nats below the top spanning the curve
346
+ xM?: number; yM?: number; xFloor?: number; yFloor?: number; p?: number;
347
+ sM?: number; // mid sharpness — how hard above-median tokens are elevated
348
+ A?: number; // shoulder compression — how hard confidence is tamed
349
+ }
350
+
351
+ export function applyHlgShaper(lp: MlxArray, opts: HlgShaperParams = {}): MlxArray {
352
+ const lw = opts.lw ?? 1200, window = opts.window ?? 10;
353
+ const xM = opts.xM ?? 0.55, yM = opts.yM ?? 0.5, xFloor = opts.xFloor ?? 0.2, yFloor = opts.yFloor ?? 0.18;
354
+ const p = opts.p ?? 2.0, sM = opts.sM ?? 0.7, A = opts.A ?? 0.35;
355
+ const targetGap = opts.targetGap ?? 15, refGap = opts.refGap ?? 4;
356
+ // geometry derived per call (cheap scalar math), exactly as HLGShaper.__init__
357
+ const dxf = xM - xFloor;
358
+ const kc = (yM - sM * dxf - yFloor) / Math.pow(dxf, p); // mid reaches y_floor
359
+ const sFloor = sM + kc * p * Math.pow(dxf, p - 1); // mid slope at the toe join
360
+ const det = -Math.pow(xFloor, 4); // solve [[xf³,xf²],[3xf²,2xf]]·[e,f]=[y_floor,s_floor]
361
+ const ec = (yFloor * (2 * xFloor) - xFloor ** 2 * sFloor) / det;
362
+ const fc = (xFloor ** 3 * sFloor - yFloor * (3 * xFloor ** 2)) / det;
363
+ const yPeak = yM + A * Math.log(1 + (sM / A) * (1 - xM));
364
+ const gamma = 1.2 + 0.42 * Math.log10(Math.max(lw, 1e-3) / 1000);
365
+
366
+ // out_scale AUTO-derived so the top-1-to-reference final-logit gap = targetGap,
367
+ // INDEPENDENT of W — decouples the two knobs so W is a clean 1-D sweep. The
368
+ // reference token sits refGap nats below the top; its post-shape value is the
369
+ // scalar curve evaluated in JS (no device cost). Calibrated so W=5 ⇒ os ≈ 18.
370
+ const oetfS = (xx: number): number => {
371
+ let yy: number;
372
+ if (xx >= xM) yy = yM + A * Math.log(Math.max(1 + (sM / A) * (xx - xM), 1e-9));
373
+ else if (xx >= xFloor) yy = yM - sM * (xM - xx) - kc * Math.pow(Math.max(xM - xx, 0), p);
374
+ else yy = ec * xx ** 3 + fc * xx ** 2;
375
+ return Math.max(yy, 0);
376
+ };
377
+ const shapeS = (xx: number): number => Math.pow(Math.max(oetfS(xx) / yPeak, 1e-9), gamma);
378
+ const xRef = Math.max(0, Math.min(1, 1 - refGap / window));
379
+ const outScale = opts.outScale ?? targetGap / Math.max(1e-6, 1 - shapeS(xRef));
380
+
381
+ const tmp: MlxArray[] = [];
382
+ const k = <T extends MlxArray>(a: T): T => {
383
+ tmp.push(a);
384
+ return a;
385
+ };
386
+ const C = (v: number, like: MlxArray): MlxArray => k(ops.scalarLike(v, like));
387
+
388
+ // x = clamp((ℓ − ℓmax)/W + 1, 0, 1) — windowed anchor at the top
389
+ const lmax = k(ops.maxAxis(lp, -1, true));
390
+ const x = k(ops.clip(k(ops.add(k(ops.mulScalar(k(ops.sub(lp, lmax)), 1 / window)), C(1, lp))), C(0, lp), C(1, lp)));
391
+
392
+ // OETF: up (x≥x_m) log shoulder, mid power, toe (x<x_floor) cubic suppress
393
+ const upArg = k(ops.maximum(k(ops.add(C(1, x), k(ops.mulScalar(k(ops.sub(x, C(xM, x))), sM / A)))), C(1e-9, x)));
394
+ const up = k(ops.add(C(yM, x), k(ops.mulScalar(k(ops.log(upArg)), A))));
395
+ const dm = k(ops.sub(C(xM, x), x));
396
+ const dmPow = k(ops.exp(k(ops.mulScalar(k(ops.log(k(ops.maximum(dm, C(1e-9, x))))), p))));
397
+ const mid = k(ops.sub(k(ops.sub(C(yM, x), k(ops.mulScalar(dm, sM)))), k(ops.mulScalar(dmPow, kc))));
398
+ const x2 = k(ops.square(x));
399
+ const x3 = k(ops.mul(x, x2));
400
+ const toe = k(ops.add(k(ops.mulScalar(x3, ec)), k(ops.mulScalar(x2, fc))));
401
+ const upMid = k(ops.where(k(ops.greaterEqual(x, C(xM, x))), up, mid));
402
+ const y = k(ops.maximum(k(ops.where(k(ops.greaterEqual(x, C(xFloor, x))), upMid, toe)), C(0, x)));
403
+
404
+ // OOTF: r = (max(y/y_peak, 1e-9))^γ ; then softmax(r · out_scale)
405
+ const yn = k(ops.maximum(k(ops.mulScalar(y, 1 / yPeak)), C(1e-9, y)));
406
+ const r = k(ops.exp(k(ops.mulScalar(k(ops.log(yn)), gamma))));
407
+ const out = ops.mulScalar(r, outScale);
408
+ for (const a of tmp) a.dispose();
409
+ return out;
410
+ }
411
+
412
+ export function makeSampler(opts: SamplerOptions = {}): Sampler {
413
+ const { temperature = 0, topP = 0, topK = 0, seed = 0, hlg, curve } = opts;
414
+
415
+ // v2 curve sampler: the drawn log-prob transfer curve REPLACES temperature +
416
+ // softmax. Stochastic and seeded by design (the curve IS the sampling shape),
417
+ // so it overrides the greedy default — the designer wants variety across N.
418
+ if (curve) {
419
+ return (lp, step) => {
420
+ const shaped = applyCurve(lp, curve);
421
+ const tok = ops.randomCategorical(shaped, stepKey(seed, step));
422
+ shaped.dispose();
423
+ return tok;
424
+ };
425
+ }
426
+
427
+ // Greedy: HLG is a no-op (a monotone curve cannot move the argmax), so the
428
+ // greedy path is untouched whether or not HLG is enabled.
429
+ if (temperature === 0)
430
+ return (lp) => ops.argmaxAxis(lp, -1);
431
+
432
+ // HLG is a REPLACEMENT sampler: when enabled, the tone curve IS the whole
433
+ // post-logits step — its toe does the tail control that top_p/top_k would, so
434
+ // they are NOT applied. Otherwise we do exactly what we used to: top_p, top_k,
435
+ // then temperature scaling. (Temperature still folds into the mid gain m = 1/T
436
+ // unless the config sets an explicit gain.)
437
+ if (hlg?.enabled === true) {
438
+ const useOetf = hlg.oetf === true;
439
+ const useEotf = hlg.eotf === true;
440
+ const usePipeline = hlg.pipeline === true;
441
+ const eotfGamma = hlgGammaForLw(hlg.lw ?? 1000);
442
+ const alpha = hlg.maxBrightness ?? 5;
443
+ const params: HlgParams = {
444
+ gain: hlg.gain ?? 1 / temperature,
445
+ width: hlg.width,
446
+ shoulder: hlg.shoulder,
447
+ toe: hlg.toe,
448
+ pivot: hlg.pivot,
449
+ pivotOffset: hlg.pivotOffset,
450
+ };
451
+ return (lp, step) => {
452
+ const scaled = hlg.shaper === true
453
+ ? applyHlgShaper(lp, { lw: hlg.lw, outScale: hlg.maxBrightness, window: hlg.window, sM: hlg.sM, A: hlg.shoulderA, targetGap: hlg.targetGap, refGap: hlg.refGap, xM: hlg.xM, yM: hlg.yM, xFloor: hlg.xFloor, yFloor: hlg.yFloor, p: hlg.p })
454
+ : usePipeline ? applyHlgPipeline(lp, alpha, eotfGamma)
455
+ : useEotf ? applyHlgEotf(lp, eotfGamma) : useOetf ? applyHlgOetf(lp) : applyHlg(lp, params);
456
+ const key = stepKey(seed, step);
457
+ const tok = ops.randomCategorical(scaled, key);
458
+ scaled.dispose();
459
+ key.dispose();
460
+ return tok;
461
+ };
462
+ }
463
+
464
+ return (lp, step) => {
465
+ let cur = lp;
466
+ const owned: MlxArray[] = [];
467
+ if (topP > 0 && topP < 1) { cur = applyTopP(cur, topP); owned.push(cur); }
468
+ if (topK > 0) { cur = applyTopK(cur, topK); owned.push(cur); }
469
+ const scaled = ops.mulScalar(cur, 1 / temperature);
470
+ const key = stepKey(seed, step);
471
+ const tok = ops.randomCategorical(scaled, key);
472
+ scaled.dispose();
473
+ key.dispose();
474
+ for (const a of owned) a.dispose();
475
+ return tok;
476
+ };
477
+ }
478
+
479
+ export function makeLogitsProcessors(opts: LogitsProcessorOptions = {}): LogitsProcessor[] {
480
+ const out: LogitsProcessor[] = [];
481
+ const { repetitionPenalty, repetitionContextSize = 20 } = opts;
482
+
483
+ if (repetitionPenalty && repetitionPenalty !== 0) {
484
+ if (repetitionPenalty < 0)
485
+ throw new Error("repetitionPenalty must be non-negative");
486
+ out.push((tokens, logits) => {
487
+ if (!tokens) return logits;
488
+ const n = tokens.shape[0]!;
489
+ if (n === 0) return logits;
490
+ const start = Math.max(0, n - repetitionContextSize);
491
+ const recent = tokens.slice([start], [n]);
492
+ const idx = ops.reshape(recent, [1, n - start]);
493
+ const selected = ops.takeAlongAxis(logits, idx, -1);
494
+ const zero = ops.scalarLike(0, selected);
495
+ const isNeg = ops.less(selected, zero);
496
+ const timesP = ops.mulScalar(selected, repetitionPenalty);
497
+ const pen = ops.scalarLike(repetitionPenalty, selected);
498
+ const overP = ops.div(selected, pen);
499
+ const penalized = ops.where(isNeg, timesP, overP);
500
+ const updated = ops.putAlongAxis(logits, idx, penalized, -1);
501
+ for (const a of [recent, idx, selected, zero, isNeg, timesP, pen, overP, penalized])
502
+ a.dispose();
503
+ return updated;
504
+ });
505
+ }
506
+ return out;
507
+ }
508
+
509
+ /** logits [1, V] → logprobs [1, V] (logits - logsumexp). */
510
+ export function toLogprobs(logits: MlxArray): MlxArray {
511
+ const lse = ops.logsumexpAxis(logits, -1, true);
512
+ const out = ops.sub(logits, lse);
513
+ lse.dispose();
514
+ return out;
515
+ }
516
+
517
+ export { Dtype };