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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- 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
|
+
}
|