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,252 @@
1
+ // HumanEval — code-generation pass@1. Port of optiq/eval/humaneval.py:
2
+ // raw function-completion (or instruct code-block when a chat template
3
+ // exists), greedy decode, stop-sequence truncation, then execute the
4
+ // assembled program (prompt + completion + test + check(entry)) in a
5
+ // macOS sandbox-exec jail (falls back to a plain subprocess) against the
6
+ // oracle venv python. A problem passes iff the program exits 0 within the
7
+ // timeout.
8
+ //
9
+ // SECURITY: generated code is UNTRUSTED model output. It is NEVER eval'd
10
+ // in-process — each candidate runs as a separate, sandboxed python
11
+ // subprocess with a hard wall-clock timeout and a memory cap, in a fresh
12
+ // temp dir that is removed afterwards.
13
+
14
+ import { spawnSync } from "node:child_process";
15
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
19
+
20
+ // Oracle venv python — the same interpreter the Python reference uses.
21
+ const ORACLE_PYTHON = "/Users/joshrossi/Code/mlx-lm/.venv/bin/python";
22
+
23
+ interface HumanevalRow {
24
+ task_id: string;
25
+ prompt: string;
26
+ canonical_solution: string;
27
+ test: string;
28
+ entry_point: string;
29
+ }
30
+
31
+ export interface HumanevalResult {
32
+ nPass: number;
33
+ nTotal: number;
34
+ accuracy: number; // pass@1, 0..1
35
+ }
36
+
37
+ // Stop sequences — mirror humaneval.py's _STOP_SEQS exactly.
38
+ const STOP_SEQS = ["\nclass ", "\ndef ", "\n#", "\nif __name__", "\nprint(", "\n```"];
39
+
40
+ /** Last fenced ```python|py? ... ``` block, or null (port of _extract_code_block). */
41
+ function extractCodeBlock(text: string): string | null {
42
+ const re = /```(?:python|py)?\n([\s\S]*?)\n```/g;
43
+ let last: string | null = null;
44
+ for (const m of text.matchAll(re)) last = m[1]!;
45
+ return last;
46
+ }
47
+
48
+ /** Escape a string for use as a literal inside a RegExp (re.escape). */
49
+ function reEscape(s: string): string {
50
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
51
+ }
52
+
53
+ /**
54
+ * Strip the `def entry_point(...):` header + docstring, keep just the body
55
+ * (port of _strip_function_def). Falls through if no such def is found.
56
+ */
57
+ function stripFunctionDef(body: string, entryPoint: string): string {
58
+ const pat = new RegExp(`def\\s+${reEscape(entryPoint)}\\s*\\([^)]*\\)[^:]*:\\s*\\n`);
59
+ const m = body.match(pat);
60
+ if (!m || m.index === undefined) return body;
61
+ const after = body.slice(m.index + m[0].length);
62
+ // Skip leading blank lines + a possible docstring.
63
+ let rest = after.replace(/^\n+/, "");
64
+ const doc = rest.match(/^\s*(["']{3})([\s\S]*?)\1\s*\n/);
65
+ if (doc) rest = rest.slice(doc[0].length);
66
+ return rest;
67
+ }
68
+
69
+ /**
70
+ * Reduce the model response to a function body suitable for concatenating
71
+ * onto the HumanEval prompt (port of _truncate_completion).
72
+ */
73
+ export function truncateCompletion(text: string, entryPoint = ""): string {
74
+ let out = text;
75
+ if (out.includes("</think>")) out = out.split("</think>").slice(1).join("</think>");
76
+ if (out.includes("<channel|>")) out = out.split("<channel|>").slice(1).join("<channel|>");
77
+
78
+ // Prefer the last fenced code block (conversational models).
79
+ const block = extractCodeBlock(out);
80
+ if (block !== null) {
81
+ return entryPoint ? stripFunctionDef(block, entryPoint) : block;
82
+ }
83
+
84
+ // Legacy / completion-mode path: strip a leading fence, then cut at the
85
+ // first stop sequence.
86
+ out = out.replace(/^\s+/, "");
87
+ if (out.startsWith("```python\n")) out = out.slice("```python\n".length);
88
+ else if (out.startsWith("```\n")) out = out.slice("```\n".length);
89
+
90
+ let earliest = out.length;
91
+ for (const stop of STOP_SEQS) {
92
+ const i = out.indexOf(stop);
93
+ if (i !== -1 && i < earliest) earliest = i;
94
+ }
95
+ return out.slice(0, earliest);
96
+ }
97
+
98
+ /** Concatenate prompt + completion + test driver (port of _build_program). */
99
+ function buildProgram(prompt: string, completion: string, test: string, entryPoint: string): string {
100
+ return prompt + completion + "\n\n" + test + "\n\n" + `check(${entryPoint})\n`;
101
+ }
102
+
103
+ // sandbox-exec SBPL profile (port of _SBPL_PROFILE): deny network, deny
104
+ // most filesystem writes except the temp dir; reads stay open so python
105
+ // can find its stdlib.
106
+ const SBPL_PROFILE = (tmp: string) => `(version 1)
107
+ (deny default)
108
+ (allow process-fork)
109
+ (allow process-exec)
110
+ (allow file-read*)
111
+ (allow file-write* (subpath "${tmp}"))
112
+ (allow file-write-data
113
+ (literal "/dev/null")
114
+ (literal "/dev/dtracehelper"))
115
+ (allow sysctl-read)
116
+ (allow mach-lookup)
117
+ (allow ipc-posix-shm)
118
+ `;
119
+
120
+ // rlimit + no-socket preamble prepended to the script (port of the
121
+ // sandbox-exec preamble). Caps address space / data segment and disables
122
+ // raw socket creation so a buggy program can't exhaust RAM or hit the net.
123
+ const preamble = (memoryMb: number) => `import resource
124
+ try:
125
+ resource.setrlimit(resource.RLIMIT_AS, (${memoryMb} * 1024 * 1024,) * 2)
126
+ except (ValueError, OSError):
127
+ pass
128
+ try:
129
+ resource.setrlimit(resource.RLIMIT_DATA, (${memoryMb} * 1024 * 1024,) * 2)
130
+ except (ValueError, OSError):
131
+ pass
132
+ import socket as _s
133
+ _orig_socket = _s.socket
134
+ def _no_socket(*a, **k):
135
+ raise OSError("network access disabled in sandbox")
136
+ _s.socket = _no_socket
137
+ `;
138
+
139
+ const HAS_SANDBOX_EXEC = (() => {
140
+ try {
141
+ return spawnSync("which", ["sandbox-exec"]).status === 0;
142
+ } catch {
143
+ return false;
144
+ }
145
+ })();
146
+
147
+ interface RunResult { ok: boolean; timedOut: boolean }
148
+
149
+ /**
150
+ * Run `program` as a python script in a fresh temp dir, sandboxed if
151
+ * sandbox-exec is available (else a plain rlimit-capped subprocess), with
152
+ * a hard wall-clock timeout. Returns whether it exited 0 (pass). The code
153
+ * is NEVER eval'd in-process — always a separate subprocess.
154
+ */
155
+ function runProgram(program: string, timeoutSec: number, memoryMb: number): RunResult {
156
+ const sbRoot = mkdtempSync(join(tmpdir(), "mlxbun_he_"));
157
+ try {
158
+ const scriptPath = join(sbRoot, "script.py");
159
+ writeFileSync(scriptPath, preamble(memoryMb) + "\n" + program);
160
+
161
+ const env = {
162
+ PATH: "/usr/bin:/bin",
163
+ PYTHONIOENCODING: "utf-8",
164
+ HOME: sbRoot,
165
+ TMPDIR: sbRoot,
166
+ MPLBACKEND: "Agg",
167
+ MPLCONFIGDIR: sbRoot,
168
+ XDG_CACHE_HOME: sbRoot,
169
+ XDG_CONFIG_HOME: sbRoot,
170
+ };
171
+
172
+ let argv: string[];
173
+ if (HAS_SANDBOX_EXEC) {
174
+ const profilePath = join(sbRoot, "policy.sb");
175
+ writeFileSync(profilePath, SBPL_PROFILE(sbRoot));
176
+ argv = ["sandbox-exec", "-f", profilePath, ORACLE_PYTHON, "-I", scriptPath];
177
+ } else {
178
+ argv = [ORACLE_PYTHON, "-I", scriptPath];
179
+ }
180
+
181
+ const res = spawnSync(argv[0]!, argv.slice(1), {
182
+ cwd: sbRoot,
183
+ env,
184
+ timeout: timeoutSec * 1000, // hard wall-clock kill
185
+ killSignal: "SIGKILL",
186
+ stdio: ["ignore", "ignore", "ignore"],
187
+ });
188
+
189
+ // spawnSync sets signal === "SIGKILL" (and status === null) when the
190
+ // timeout fires; ETIMEDOUT shows up in res.error on some platforms.
191
+ const timedOut =
192
+ res.signal === "SIGKILL" ||
193
+ (res.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
194
+ const ok = !timedOut && res.status === 0;
195
+ return { ok, timedOut };
196
+ } finally {
197
+ rmSync(sbRoot, { recursive: true, force: true });
198
+ }
199
+ }
200
+
201
+ export async function evaluateHumaneval(
202
+ tm: TaskModel,
203
+ opts: {
204
+ nSamples?: number;
205
+ maxTokens?: number;
206
+ seed?: number;
207
+ timeoutSec?: number;
208
+ memoryMb?: number;
209
+ } = {},
210
+ ): Promise<HumanevalResult> {
211
+ const maxTokens = opts.maxTokens ?? 512;
212
+ const timeoutSec = opts.timeoutSec ?? 10;
213
+ const memoryMb = opts.memoryMb ?? 512;
214
+ const rows = loadJsonl<HumanevalRow>("humaneval");
215
+ const idx = sampleIndices(rows.length, opts.nSamples ?? rows.length, opts.seed ?? 42);
216
+
217
+ // When a chat template exists, ask the model (instruct-style) for the
218
+ // complete function inside a ```python block — humaneval.py's use_chat
219
+ // path. Otherwise feed the raw signature for completion.
220
+ const useChat = tm.template !== null;
221
+
222
+ let nPass = 0;
223
+ for (let k = 0; k < idx.length; k++) {
224
+ const ex = rows[idx[k]!]!;
225
+
226
+ let body: string;
227
+ if (useChat) {
228
+ body =
229
+ "Complete the following Python function. Output the " +
230
+ "complete function (header, docstring, body) inside a " +
231
+ "single ```python ... ``` code block. No commentary " +
232
+ "outside the code block.\n\n" +
233
+ "```python\n" + ex.prompt + "```";
234
+ } else {
235
+ body = ex.prompt;
236
+ }
237
+
238
+ const raw = await generateText(tm, body, { maxTokens, useChat });
239
+ const completion = truncateCompletion(raw, ex.entry_point);
240
+ const program = buildProgram(ex.prompt, completion, ex.test, ex.entry_point);
241
+
242
+ const { ok } = runProgram(program, timeoutSec, memoryMb);
243
+ if (ok) nPass++;
244
+
245
+ if ((k + 1) % 5 === 0 || k + 1 === idx.length)
246
+ process.stderr.write(
247
+ `\r humaneval ${k + 1}/${idx.length} pass@1=${((nPass / (k + 1)) * 100).toFixed(1)}%`,
248
+ );
249
+ }
250
+ process.stderr.write("\n");
251
+ return { nPass, nTotal: idx.length, accuracy: idx.length ? nPass / idx.length : 0 };
252
+ }
@@ -0,0 +1,354 @@
1
+ // IFEval — instruction-following evaluation. Port of optiq/eval/ifeval.py.
2
+ //
3
+ // Google's IFEval probes whether a model follows verifiable, mechanically-
4
+ // checkable instructions ("respond in 3 bullet points", "include the keyword
5
+ // 'banana'", "respond in JSON"). Each example carries one or more constraint
6
+ // instructions; we generate a response and run the corresponding verifier.
7
+ //
8
+ // We report prompt-level pass rates (matches the official methodology):
9
+ // * strict — fraction of examples where ALL of the prompt's instructions
10
+ // pass on the raw response.
11
+ // * loose — same but with response-cleaning preprocessing (strip leading
12
+ // "Sure, here is…" boilerplate and outer code fences).
13
+ // The primary metric (`accuracy`) is prompt-level strict, headlined by
14
+ // ifeval.py's IFEvalResult.__str__.
15
+ //
16
+ // Faithful port note: several verifiers below read kwarg keys that the dataset
17
+ // rows never populate (e.g. _check_capital_words_count / _check_letter_frequency
18
+ // read `relation`, while the data supplies `capital_relation` / `let_relation`).
19
+ // This mirrors ifeval.py exactly — those reads fall through to the "at least"
20
+ // default in both implementations, so behavior is identical.
21
+
22
+ import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
23
+
24
+ interface IfevalRow {
25
+ key: number;
26
+ prompt: string;
27
+ instruction_id_list: string[];
28
+ kwargs: Record<string, unknown>[];
29
+ }
30
+
31
+ export interface IfevalResult {
32
+ nTotal: number;
33
+ strictAcc: number; // 0..1 — prompt-level strict pass rate
34
+ looseAcc: number; // 0..1 — prompt-level loose pass rate
35
+ accuracy: number; // 0..1 — primary metric == strictAcc (ifeval.py headline)
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // kwarg accessors — Python uses kw.get(name, default); replicate with the same
40
+ // default semantics. Keys are read with the EXACT names ifeval.py uses.
41
+ // ---------------------------------------------------------------------------
42
+ type Kw = Record<string, unknown>;
43
+
44
+ function kwStr(kw: Kw, name: string, def = ""): string {
45
+ const v = kw[name];
46
+ return v === undefined || v === null ? def : String(v);
47
+ }
48
+ function kwInt(kw: Kw, name: string, def = 0): number {
49
+ const v = kw[name];
50
+ if (v === undefined || v === null) return def;
51
+ const n = typeof v === "number" ? v : parseInt(String(v), 10);
52
+ return Number.isFinite(n) ? Math.trunc(n) : def;
53
+ }
54
+ function kwArr(kw: Kw, name: string): string[] {
55
+ const v = kw[name];
56
+ return Array.isArray(v) ? (v as unknown[]).map((x) => String(x)) : [];
57
+ }
58
+ function reEscape(s: string): string {
59
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Constraint verifiers — each takes (response, kwargs) and returns boolean.
64
+ // One-to-one with the `_check_*` functions in ifeval.py.
65
+ // ---------------------------------------------------------------------------
66
+
67
+ // \b\w+\b — JS \w is ASCII-ish like Python's default re (no re.UNICODE here).
68
+ function checkLengthWords(response: string, kw: Kw): boolean {
69
+ const n = (response.match(/\b\w+\b/g) ?? []).length;
70
+ const rel = kwStr(kw, "relation", "at least");
71
+ const target = kwInt(kw, "num_words", 0);
72
+ return rel.includes("least") ? n >= target : n <= target;
73
+ }
74
+
75
+ function checkLengthSentences(response: string, kw: Kw): boolean {
76
+ // Python: re.split(r"[.!?]+\s+", response.strip())
77
+ const n = response.trim().split(/[.!?]+\s+/).length;
78
+ const rel = kwStr(kw, "relation", "at least");
79
+ const target = kwInt(kw, "num_sentences", 0);
80
+ return rel.includes("least") ? n >= target : n <= target;
81
+ }
82
+
83
+ function checkLengthParagraphs(response: string, kw: Kw): boolean {
84
+ const n = response.split("\n\n").filter((p) => p.trim()).length;
85
+ const rel = kwStr(kw, "relation", "at least");
86
+ const target = kwInt(kw, "num_paragraphs", 0);
87
+ return rel.includes("least") ? n >= target : n <= target;
88
+ }
89
+
90
+ function checkKeywordsExistence(response: string, kw: Kw): boolean {
91
+ const keywords = kwArr(kw, "keywords");
92
+ const text = response.toLowerCase();
93
+ return keywords.every((k) => text.includes(k.toLowerCase()));
94
+ }
95
+
96
+ function checkKeywordsForbidden(response: string, kw: Kw): boolean {
97
+ const forbidden = kwArr(kw, "forbidden_words");
98
+ const text = response.toLowerCase();
99
+ return forbidden.every((w) => !text.includes(w.toLowerCase()));
100
+ }
101
+
102
+ function checkKeywordFrequency(response: string, kw: Kw): boolean {
103
+ const kwWord = kwStr(kw, "keyword", "").toLowerCase();
104
+ const rel = kwStr(kw, "relation", "at least");
105
+ const target = kwInt(kw, "frequency", 0);
106
+ const n = (response.toLowerCase().match(new RegExp(`\\b${reEscape(kwWord)}\\b`, "g")) ?? []).length;
107
+ return rel.includes("least") ? n >= target : n <= target;
108
+ }
109
+
110
+ function checkLetterFrequency(response: string, kw: Kw): boolean {
111
+ const letter = kwStr(kw, "letter", "").toLowerCase();
112
+ const rel = kwStr(kw, "relation", "at least");
113
+ // Python: int(kw.get("let_frequency", kw.get("frequency", 0)))
114
+ const target = kw["let_frequency"] !== undefined && kw["let_frequency"] !== null
115
+ ? kwInt(kw, "let_frequency", 0)
116
+ : kwInt(kw, "frequency", 0);
117
+ // Python: response.lower().count(letter) — count of non-overlapping occurrences.
118
+ const n = letter.length ? response.toLowerCase().split(letter).length - 1 : 0;
119
+ return rel.includes("least") ? n >= target : n <= target;
120
+ }
121
+
122
+ function checkCapitalWordsCount(response: string, kw: Kw): boolean {
123
+ const words = response.match(/\b\w+\b/g) ?? [];
124
+ // Python: w.isupper() and len(w) > 1 — str.isupper() requires ≥1 cased char,
125
+ // all cased chars uppercase. Approximate: has a letter, no lowercase letter.
126
+ const n = words.filter((w) => w.length > 1 && /[A-Za-z]/.test(w) && w === w.toUpperCase()).length;
127
+ const rel = kwStr(kw, "relation", "at least");
128
+ // Python: int(kw.get("capital_frequency", kw.get("frequency", 0)))
129
+ const target = kw["capital_frequency"] !== undefined && kw["capital_frequency"] !== null
130
+ ? kwInt(kw, "capital_frequency", 0)
131
+ : kwInt(kw, "frequency", 0);
132
+ return rel.includes("least") ? n >= target : n <= target;
133
+ }
134
+
135
+ function checkChangeCaseCapital(response: string, _kw: Kw): boolean {
136
+ return response.toUpperCase() === response;
137
+ }
138
+
139
+ function checkChangeCaseLowercase(response: string, _kw: Kw): boolean {
140
+ return response.toLowerCase() === response;
141
+ }
142
+
143
+ function checkResponseLanguage(response: string, kw: Kw): boolean {
144
+ // Coarse: ASCII-ness for English; pass for others (no language detector).
145
+ const target = kwStr(kw, "language", "en");
146
+ if (target === "en") {
147
+ return [...response.slice(0, 200)].every((c) => c.charCodeAt(0) < 128);
148
+ }
149
+ return true;
150
+ }
151
+
152
+ function checkPunctuationNoComma(response: string, _kw: Kw): boolean {
153
+ return !response.includes(",");
154
+ }
155
+
156
+ function checkStartendQuotation(response: string, _kw: Kw): boolean {
157
+ const s = response.trim();
158
+ return s.startsWith('"') && s.endsWith('"');
159
+ }
160
+
161
+ function checkStartendEndPhrase(response: string, kw: Kw): boolean {
162
+ // Python: kw.get("end_phrase","").strip().rstrip(".!?")
163
+ const end = kwStr(kw, "end_phrase", "").trim().replace(/[.!?]+$/, "");
164
+ const tail = response.trim().replace(/[.!?]+$/, "").toLowerCase();
165
+ return tail.endsWith(end.toLowerCase());
166
+ }
167
+
168
+ function checkFormatNumberBullets(response: string, kw: Kw): boolean {
169
+ // Python: re.findall(r"^\s*[\*\-+]\s+|^\s*\d+\.\s+", response, re.MULTILINE)
170
+ const bullets = response.match(/^[ \t]*[*\-+][ \t]+|^[ \t]*\d+\.[ \t]+/gm) ?? [];
171
+ const target = kwInt(kw, "num_bullets", 0);
172
+ return bullets.length === target;
173
+ }
174
+
175
+ function checkFormatNumberHighlighted(response: string, kw: Kw): boolean {
176
+ const n = (response.match(/\*[^*]+\*/g) ?? []).length;
177
+ const target = kwInt(kw, "num_highlights", 0);
178
+ return n >= target;
179
+ }
180
+
181
+ function checkFormatTitle(response: string, _kw: Kw): boolean {
182
+ return /<<[^>]+>>/.test(response);
183
+ }
184
+
185
+ function checkFormatConstrainedResponse(response: string, _kw: Kw): boolean {
186
+ const valid = new Set(["My answer is yes.", "My answer is no.", "My answer is maybe."]);
187
+ return valid.has(response.trim());
188
+ }
189
+
190
+ function checkFormatJson(response: string, _kw: Kw): boolean {
191
+ let s = response.trim();
192
+ if (s.startsWith("```")) {
193
+ s = s.replace(/^```\w*\n?/, "");
194
+ s = s.replace(/\n?```$/, "");
195
+ }
196
+ try {
197
+ JSON.parse(s);
198
+ return true;
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ function checkFormatMultipleSections(response: string, kw: Kw): boolean {
205
+ const marker = kwStr(kw, "section_spliter", "Section").trim();
206
+ const target = kwInt(kw, "num_sections", 0);
207
+ const n = (response.match(new RegExp(`${reEscape(marker)}\\s*\\d+`, "gi")) ?? []).length;
208
+ return n >= target;
209
+ }
210
+
211
+ function checkCombinationTwoResponses(response: string, _kw: Kw): boolean {
212
+ return response.includes("******");
213
+ }
214
+
215
+ function checkCombinationRepeatPrompt(_response: string, _kw: Kw): boolean {
216
+ return true; // ifeval.py: we don't have the prompt here; assume pass.
217
+ }
218
+
219
+ const alwaysPass = (): boolean => true;
220
+
221
+ // Map IFEval instruction IDs → verifier. One-to-one with ifeval.py `_VERIFIERS`.
222
+ type Verifier = (response: string, kwargs: Kw) => boolean;
223
+ const VERIFIERS = new Map<string, Verifier>([
224
+ ["length_constraints:number_words", checkLengthWords],
225
+ ["length_constraints:number_sentences", checkLengthSentences],
226
+ ["length_constraints:number_paragraphs", checkLengthParagraphs],
227
+ ["length_constraints:nth_paragraph_first_word", alwaysPass],
228
+ ["keywords:existence", checkKeywordsExistence],
229
+ ["keywords:frequency", checkKeywordFrequency],
230
+ ["keywords:forbidden_words", checkKeywordsForbidden],
231
+ ["keywords:letter_frequency", checkLetterFrequency],
232
+ ["language:response_language", checkResponseLanguage],
233
+ ["change_case:english_capital", checkChangeCaseCapital],
234
+ ["change_case:english_lowercase", checkChangeCaseLowercase],
235
+ ["change_case:capital_word_frequency", checkCapitalWordsCount],
236
+ ["punctuation:no_comma", checkPunctuationNoComma],
237
+ ["startend:quotation", checkStartendQuotation],
238
+ ["startend:end_checker", checkStartendEndPhrase],
239
+ ["detectable_format:number_bullet_lists", checkFormatNumberBullets],
240
+ ["detectable_format:number_highlighted_sections", checkFormatNumberHighlighted],
241
+ ["detectable_format:title", checkFormatTitle],
242
+ ["detectable_format:constrained_response", checkFormatConstrainedResponse],
243
+ ["detectable_format:json_format", checkFormatJson],
244
+ ["detectable_format:multiple_sections", checkFormatMultipleSections],
245
+ ["combination:two_responses", checkCombinationTwoResponses],
246
+ ["combination:repeat_prompt", checkCombinationRepeatPrompt],
247
+ ["detectable_content:number_placeholders", alwaysPass],
248
+ ["detectable_content:postscript", alwaysPass],
249
+ ]);
250
+
251
+ /**
252
+ * Verify one response against its instruction list.
253
+ * Returns { pass, unhandled }. An instruction_id with no registered verifier is
254
+ * counted as a FAIL (NOT silently skipped) and recorded in `unhandled`. This is
255
+ * stricter than ifeval.py (which treats unhandled as pass); per the mlx-bun
256
+ * harness rule, anything we cannot port must fail rather than bias high.
257
+ */
258
+ function verifyResponse(
259
+ response: string,
260
+ instructionIds: string[],
261
+ kwargsList: Kw[],
262
+ ): { pass: boolean; unhandled: string[] } {
263
+ const unhandled: string[] = [];
264
+ let pass = true;
265
+ for (let i = 0; i < instructionIds.length; i++) {
266
+ const iid = instructionIds[i]!;
267
+ const kw = kwargsList[i] ?? {};
268
+ const verifier = VERIFIERS.get(iid);
269
+ if (verifier === undefined) {
270
+ unhandled.push(iid);
271
+ pass = false; // count unported instruction as a failure
272
+ continue;
273
+ }
274
+ try {
275
+ if (!verifier(response, kw)) pass = false;
276
+ } catch {
277
+ pass = false;
278
+ }
279
+ }
280
+ return { pass, unhandled };
281
+ }
282
+
283
+ /** Loose-mode preprocessing — mirrors ifeval.py `_loose_clean`. */
284
+ function looseClean(response: string): string {
285
+ let s = response.trim();
286
+ // Python: re.sub(r"^(Sure|Here|Of course)[,!.]\s*[^\n]*\n+", "", s, count=1)
287
+ s = s.replace(/^(Sure|Here|Of course)[,!.]\s*[^\n]*\n+/, "");
288
+ if (s.startsWith("```")) {
289
+ s = s.replace(/^```\w*\n?/, "");
290
+ s = s.replace(/\n?```$/, "");
291
+ }
292
+ return s.trim();
293
+ }
294
+
295
+ /** Strip a leading thinking block, matching ifeval.py's split on </think>. */
296
+ function stripThinking(response: string): string {
297
+ const idx = response.indexOf("</think>");
298
+ return idx === -1 ? response : response.slice(idx + "</think>".length);
299
+ }
300
+
301
+ export async function evaluateIfeval(
302
+ tm: TaskModel,
303
+ opts: { nSamples?: number; maxTokens?: number; seed?: number } = {},
304
+ ): Promise<IfevalResult> {
305
+ // ifeval.py: max_tokens default 512.
306
+ const maxTokens = opts.maxTokens ?? 512;
307
+ const seed = opts.seed ?? 42;
308
+
309
+ const rows = loadJsonl<IfevalRow>("ifeval");
310
+ const nSamples = opts.nSamples ?? rows.length;
311
+ const idx = nSamples < rows.length
312
+ ? sampleIndices(rows.length, nSamples, seed)
313
+ : Array.from({ length: rows.length }, (_, i) => i);
314
+
315
+ let nStrict = 0;
316
+ let nLoose = 0;
317
+ const unhandledCounts = new Map<string, number>();
318
+
319
+ for (let k = 0; k < idx.length; k++) {
320
+ const item = rows[idx[k]!]!;
321
+ const iids = item.instruction_id_list ?? [];
322
+ const kwList = item.kwargs ?? iids.map(() => ({}));
323
+
324
+ const raw = await generateText(tm, item.prompt, { maxTokens, useChat: true });
325
+ const response = stripThinking(raw);
326
+
327
+ const strict = verifyResponse(response, iids, kwList);
328
+ const loose = verifyResponse(looseClean(response), iids, kwList);
329
+ for (const u of strict.unhandled) unhandledCounts.set(u, (unhandledCounts.get(u) ?? 0) + 1);
330
+
331
+ if (strict.pass) nStrict++;
332
+ if (loose.pass) nLoose++;
333
+
334
+ if ((k + 1) % 10 === 0 || k + 1 === idx.length)
335
+ process.stderr.write(
336
+ `\r ifeval ${k + 1}/${idx.length} strict=${((nStrict / (k + 1)) * 100).toFixed(1)}% loose=${((nLoose / (k + 1)) * 100).toFixed(1)}%`,
337
+ );
338
+ }
339
+ process.stderr.write("\n");
340
+
341
+ if (unhandledCounts.size) {
342
+ const top = [...unhandledCounts.entries()].sort((a, b) => b[1] - a[1]);
343
+ process.stderr.write(
344
+ " ifeval unported instruction ids (counted as FAIL): " +
345
+ top.map(([k, v]) => `${k}(${v})`).join(", ") +
346
+ "\n",
347
+ );
348
+ }
349
+
350
+ const nTotal = idx.length;
351
+ const strictAcc = nTotal ? nStrict / nTotal : 0;
352
+ const looseAcc = nTotal ? nLoose / nTotal : 0;
353
+ return { nTotal, strictAcc, looseAcc, accuracy: strictAcc };
354
+ }