mlx-bun 0.0.10 → 0.0.12

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 (216) hide show
  1. package/README.md +107 -32
  2. package/THIRD_PARTY_LICENSES.md +10 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/colibri-glm52-port.md +1479 -0
  10. package/docs/design/compat-cli-surface-design.md +4 -3
  11. package/docs/design/decode-speed-program.md +172 -0
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +371 -0
  14. package/docs/design/dspark-speculative-decoding.md +167 -5
  15. package/docs/design/faithful-l1-consolidation.md +310 -0
  16. package/docs/design/generic-model-support.md +8 -5
  17. package/docs/design/grammar-spec-batching-integration.md +301 -0
  18. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  19. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  20. package/docs/design/omlx-adoption-map.md +14 -6
  21. package/docs/design/orpo-training.md +34 -30
  22. package/docs/design/paged-kv-cache.md +214 -0
  23. package/docs/design/parallel-slots.md +82 -49
  24. package/docs/design/parity-tier-dag.md +43 -33
  25. package/docs/design/pre-colibri-stabilization.md +329 -0
  26. package/docs/design/repo-cleanup-plan.md +189 -0
  27. package/docs/design/runtime-isolation.md +19 -3
  28. package/docs/design/segmented-backward-training.md +3 -2
  29. package/docs/design/ssd-kv-cold-tier.md +130 -0
  30. package/docs/design/structured-output.md +484 -0
  31. package/docs/design/the-dreaming-master-plan.md +2 -3
  32. package/docs/design/turboquant-kv.md +235 -0
  33. package/docs/design/unified-engine-frontier-plan.md +774 -0
  34. package/docs/design/web-chat-beat-matrix.md +315 -0
  35. package/docs/design/web-chat-redesign.md +1112 -0
  36. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  37. package/docs/investigations/dspark-handoff.md +182 -16
  38. package/docs/investigations/indexshare-performance-spike.md +357 -0
  39. package/docs/investigations/lab-build-journal.md +2 -2
  40. package/docs/investigations/openwiki-evaluation.md +249 -0
  41. package/docs/planning/IDEAS.md +14 -0
  42. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  43. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  44. package/docs/planning/chat-ui-vision.md +50 -0
  45. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  46. package/docs/planning/release-notes-v0.0.11.md +239 -0
  47. package/docs/planning/release-notes-v0.0.12.md +80 -0
  48. package/docs/reference/cli.md +63 -18
  49. package/docs/reference/distribution.md +37 -6
  50. package/docs/reference/embedding.md +40 -16
  51. package/docs/reference/features-matrix.md +99 -0
  52. package/docs/reference/library-api.md +69 -20
  53. package/docs/reference/memory.md +26 -6
  54. package/docs/reference/models.md +60 -3
  55. package/docs/reference/orpo-quickstart.md +3 -4
  56. package/docs/reference/server-api.md +717 -24
  57. package/docs/reference/server-config.md +265 -129
  58. package/docs/reference/training.md +24 -16
  59. package/package.json +5 -3
  60. package/src/anthropic.ts +67 -44
  61. package/src/audio/conformer.ts +647 -0
  62. package/src/audio/decode.ts +133 -0
  63. package/src/audio/features.ts +202 -0
  64. package/src/audio/transcode.ts +67 -0
  65. package/src/chat-template.ts +155 -10
  66. package/src/cli.ts +483 -113
  67. package/src/config.ts +116 -24
  68. package/src/diffusion/diffusion-generate.ts +14 -8
  69. package/src/download.ts +143 -12
  70. package/src/eval/README.md +7 -6
  71. package/src/eval/ifeval.ts +63 -120
  72. package/src/eval/kl.ts +59 -3
  73. package/src/eval/quality-db.ts +2 -2
  74. package/src/eval/runner.ts +17 -8
  75. package/src/eval/tasks/ifeval.ts +167 -31
  76. package/src/expert-io.ts +391 -0
  77. package/src/expert-offload.ts +15 -2
  78. package/src/expert-residency.ts +1134 -0
  79. package/src/expert-usage.ts +415 -0
  80. package/src/flags.ts +13 -0
  81. package/src/generate.ts +438 -35
  82. package/src/grammar.ts +585 -0
  83. package/src/hf-push.ts +1 -1
  84. package/src/hub-rest.ts +242 -0
  85. package/src/index.ts +14 -2
  86. package/src/jobs/runner.ts +37 -18
  87. package/src/kv-store.ts +586 -93
  88. package/src/lora.ts +85 -14
  89. package/src/md-modules.d.ts +42 -0
  90. package/src/media-fetch.ts +426 -0
  91. package/src/memory/rest.ts +245 -0
  92. package/src/memory/vault.ts +74 -0
  93. package/src/mlx/array.ts +149 -19
  94. package/src/mlx/autograd.ts +53 -16
  95. package/src/mlx/checkpoint.ts +46 -13
  96. package/src/mlx/custom-vjp.ts +83 -35
  97. package/src/mlx/ffi.ts +27 -3
  98. package/src/mlx/metal-kernel.ts +7 -4
  99. package/src/mlx/ops.ts +153 -23
  100. package/src/mlx/turboquant-ops.ts +831 -0
  101. package/src/mlx/turboquant-tables.ts +237 -0
  102. package/src/model/batched-mask.ts +62 -1
  103. package/src/model/batched-quant.ts +206 -0
  104. package/src/model/batched-rotating-quant.ts +346 -0
  105. package/src/model/batched-rotating.ts +46 -1
  106. package/src/model/compiled-decode.ts +4 -15
  107. package/src/model/factory.ts +103 -6
  108. package/src/model/gemma4-base.ts +395 -39
  109. package/src/model/gemma4.ts +122 -33
  110. package/src/model/generated/gemma4-12b.ts +9 -13
  111. package/src/model/generated/gemma4-26b.ts +10 -5
  112. package/src/model/generated/gemma4-e4b.ts +14 -9
  113. package/src/model/glm52-atlas.ts +511 -0
  114. package/src/model/glm52-cache.ts +706 -0
  115. package/src/model/glm52-config.ts +262 -0
  116. package/src/model/glm52-container.ts +305 -0
  117. package/src/model/glm52-coupling.ts +388 -0
  118. package/src/model/glm52-dsa.ts +602 -0
  119. package/src/model/glm52-expert-layout.ts +253 -0
  120. package/src/model/glm52-layout.ts +152 -0
  121. package/src/model/glm52-memory.ts +473 -0
  122. package/src/model/glm52-mla.ts +786 -0
  123. package/src/model/glm52-moe.ts +223 -0
  124. package/src/model/glm52-pilot.ts +537 -0
  125. package/src/model/glm52-reference.ts +445 -0
  126. package/src/model/glm52-residency.ts +557 -0
  127. package/src/model/glm52-resident-weights.ts +135 -0
  128. package/src/model/glm52-streamed-experts.ts +453 -0
  129. package/src/model/glm52-streamed-metal.ts +610 -0
  130. package/src/model/glm52-weights.ts +503 -0
  131. package/src/model/glm52.ts +1153 -0
  132. package/src/model/minicpm5.ts +58 -102
  133. package/src/model/paged-kv.ts +253 -0
  134. package/src/model/qwen3-delta.ts +76 -15
  135. package/src/model/qwen3-moe.ts +404 -0
  136. package/src/model/qwen3.ts +9 -8
  137. package/src/model/qwen3_5.ts +88 -23
  138. package/src/model/support.ts +19 -1
  139. package/src/model/universal/dense.ts +40 -5
  140. package/src/model/universal/rope.ts +36 -21
  141. package/src/native/expert_io.c +523 -0
  142. package/src/native-pack.ts +6 -4
  143. package/src/pi-web.ts +1299 -82
  144. package/src/preflight.ts +27 -8
  145. package/src/prompt-cache.ts +293 -33
  146. package/src/quantize/quantizer.ts +16 -1
  147. package/src/registry.ts +57 -2
  148. package/src/responses.ts +62 -49
  149. package/src/serve/batch-scheduler.ts +1050 -78
  150. package/src/serve/generation-gateway.ts +252 -24
  151. package/src/serve/isolate.ts +417 -0
  152. package/src/serve/lane-registry.ts +44 -0
  153. package/src/serve/session-search.ts +326 -0
  154. package/src/server.ts +1701 -175
  155. package/src/spec/assistant-source.ts +150 -0
  156. package/src/spec/deepspec-source.ts +143 -0
  157. package/src/spec/dflash-source.ts +124 -0
  158. package/src/spec/dspark/ab-stats.ts +136 -0
  159. package/src/spec/dspark/calibration.ts +112 -0
  160. package/src/spec/dspark/deepspec-module.ts +801 -0
  161. package/src/spec/dspark/generate-dflash.ts +72 -20
  162. package/src/spec/dspark/loader.ts +23 -0
  163. package/src/spec/dspark/module-dflash.ts +278 -36
  164. package/src/spec/dspark/quantize-drafter.ts +91 -0
  165. package/src/spec/generate.ts +59 -25
  166. package/src/spec/glm52-mtp-source.ts +392 -0
  167. package/src/spec/ngram-source.ts +132 -0
  168. package/src/spec/serve-loop.ts +617 -0
  169. package/src/spec/source.ts +118 -0
  170. package/src/spec/two-model.ts +171 -0
  171. package/src/ssd-cache.ts +111 -35
  172. package/src/tokenizer.ts +288 -2
  173. package/src/tool-approvals.ts +131 -0
  174. package/src/tool-call.ts +438 -22
  175. package/src/train/flash-cce.ts +2 -2
  176. package/src/train/job.ts +0 -12
  177. package/src/train/lora-params.ts +17 -6
  178. package/src/train/trainer.ts +60 -30
  179. package/src/vision/prompt.ts +217 -48
  180. package/src/web/app.html +1197 -1791
  181. package/src/web/app.js +5694 -0
  182. package/src/web/icon.svg +13 -0
  183. package/src/web/manifest.webmanifest +24 -0
  184. package/src/web/src/adapters-panel.ts +288 -0
  185. package/src/web/src/api.ts +57 -0
  186. package/src/web/src/assistant.ts +485 -0
  187. package/src/web/src/chat.ts +1050 -0
  188. package/src/web/src/composer.ts +959 -0
  189. package/src/web/src/dataset.ts +132 -0
  190. package/src/web/src/finetune.ts +267 -0
  191. package/src/web/src/hub.ts +428 -0
  192. package/src/web/src/main.ts +98 -0
  193. package/src/web/src/markdown.ts +0 -0
  194. package/src/web/src/memory-panel.ts +572 -0
  195. package/src/web/src/model-picker.ts +159 -0
  196. package/src/web/src/palette.ts +346 -0
  197. package/src/web/src/protocol.ts +62 -0
  198. package/src/web/src/quantize.ts +167 -0
  199. package/src/web/src/rag.ts +274 -0
  200. package/src/web/src/sessions.ts +469 -0
  201. package/src/web/src/shell.ts +804 -0
  202. package/src/web/src/status.ts +300 -0
  203. package/src/web/src/ui-catalog.ts +92 -0
  204. package/src/web/sw.js +71 -0
  205. package/src/web/vendor/README.md +61 -0
  206. package/src/web/vendor/hljs-LICENSE +29 -0
  207. package/src/web/vendor/hljs-theme.css +59 -0
  208. package/src/web/vendor/hljs.js +26 -0
  209. package/src/web-tools.ts +54 -2
  210. package/src/weights.ts +82 -19
  211. package/src/model/fused-decode-kernel.ts +0 -312
  212. package/src/model/fused-geglu-kernel.ts +0 -198
  213. package/src/model/fused-mlp-kernel.ts +0 -169
  214. package/src/model/fused-swiglu-kernel.ts +0 -240
  215. package/src/model/steel-linear-kernel.ts +0 -96
  216. package/src/status-page.html +0 -497
@@ -1,138 +1,81 @@
1
- // IFEval — programmatically-verifiable instruction-following scoring (Zhou et
2
- // al. 2023, "Instruction-Following Evaluation for Large Language Models"). Each
3
- // instance pairs a prompt with one or more *verifiable* instructions; a response
4
- // is scored by deterministic checks (no LLM judge), so before/after ORPO
5
- // comparisons are exact and reproducible.
1
+ // Compatibility facade for the canonical IFEval scorer.
6
2
  //
7
- // This implements the high-frequency instruction families (a working subset of
8
- // the ~25 in the reference suite, extensible via INSTRUCTIONS). Strict scoring:
9
- // an instance passes (strict) iff every instruction passes; we also report the
10
- // per-instruction pass rate. The official metrics are prompt-level and
11
- // instruction-level accuracy — both derived from `scoreInstance`.
3
+ // The complete verifier registry and strict/loose aggregation contract live in
4
+ // `tasks/ifeval.ts`, which is also used by `scripts/eval.ts`. Keep this module
5
+ // as the stable import surface used by `scripts/run-ifeval.ts` and older
6
+ // callers; it must not grow a second verifier implementation.
12
7
 
13
- export interface IFEvalInstance {
14
- /** The prompt text (used by instructions that reference it, e.g. "repeat the request"). */
15
- prompt: string;
16
- /** Instruction ids, e.g. "keywords:existence". */
17
- instruction_id_list: string[];
18
- /** Per-instruction kwargs, aligned with instruction_id_list. */
19
- kwargs: Array<Record<string, unknown>>;
20
- }
8
+ import {
9
+ scoreIfevalInstance,
10
+ scoreIfevalPairs,
11
+ type IfevalInstance,
12
+ type IfevalResult,
13
+ } from "./tasks/ifeval";
14
+
15
+ export {
16
+ SUPPORTED_INSTRUCTIONS,
17
+ looseClean,
18
+ scoreIfevalInstance,
19
+ scoreIfevalPairs,
20
+ stripThinking,
21
+ verifyResponse,
22
+ type IfevalCoverage,
23
+ type IfevalInstance,
24
+ type IfevalInstanceScore,
25
+ type IfevalPair,
26
+ type IfevalResult,
27
+ type IfevalVerification,
28
+ } from "./tasks/ifeval";
29
+
30
+ /** Historical spelling retained for callers of this module. */
31
+ export type IFEvalInstance = IfevalInstance;
21
32
 
22
33
  export interface InstanceResult {
23
- /** One pass/fail per instruction (aligned with instruction_id_list). */
34
+ /** Unknown instructions are true here per the canonical prompt contract. */
24
35
  perInstruction: boolean[];
25
- /** Strict: all instructions passed. */
26
36
  followedAll: boolean;
37
+ /** Consult this field rather than interpreting unknown IDs as verified. */
38
+ unhandled: string[];
27
39
  }
28
40
 
29
- type Checker = (response: string, kw: Record<string, unknown>, inst: IFEvalInstance) => boolean;
30
-
31
- const words = (s: string): string[] => s.trim().split(/\s+/).filter(Boolean);
32
- const sentences = (s: string): string[] =>
33
- s.split(/(?<=[.!?])\s+/).map((x) => x.trim()).filter(Boolean);
34
- const paragraphs = (s: string): string[] =>
35
- s.split(/\n\s*\n/).map((x) => x.trim()).filter(Boolean);
36
- const norm = (s: string): string => s.toLowerCase();
37
-
38
- const cmp = (n: number, rel: string | undefined, target: number): boolean => {
39
- switch (rel) {
40
- case "at least": return n >= target;
41
- case "at most": return n <= target;
42
- case "less than": return n < target;
43
- case "exactly": return n === target;
44
- default: return n >= target; // IFEval default for "around"/unspecified is lenient ≥
45
- }
46
- };
47
-
48
- /** The verifiable-instruction registry. Keyed by IFEval instruction_id. */
49
- export const INSTRUCTIONS: Record<string, Checker> = {
50
- // ---- keywords ----
51
- "keywords:existence": (r, kw) =>
52
- (kw.keywords as string[]).every((k) => norm(r).includes(norm(k))),
53
- "keywords:frequency": (r, kw) => {
54
- const k = norm(kw.keyword as string);
55
- const count = norm(r).split(k).length - 1;
56
- return cmp(count, kw.relation as string, kw.frequency as number);
57
- },
58
- "keywords:forbidden_words": (r, kw) =>
59
- !(kw.forbidden_words as string[]).some((k) => norm(r).includes(norm(k))),
60
-
61
- // ---- length constraints ----
62
- "length_constraints:number_words": (r, kw) =>
63
- cmp(words(r).length, kw.relation as string, kw.num_words as number),
64
- "length_constraints:number_sentences": (r, kw) =>
65
- cmp(sentences(r).length, kw.relation as string, kw.num_sentences as number),
66
- "length_constraints:number_paragraphs": (r, kw) =>
67
- cmp(paragraphs(r).length, kw.relation as string, kw.num_paragraphs as number),
68
-
69
- // ---- detectable format ----
70
- "detectable_format:number_bullet_lists": (r, kw) => {
71
- const bullets = r.split("\n").filter((l) => /^\s*[*\-]\s+/.test(l)).length;
72
- return cmp(bullets, kw.relation as string, kw.num_bullets as number);
73
- },
74
- "detectable_format:number_highlighted_sections": (r, kw) => {
75
- const hi = (r.match(/\*[^*\n]+\*/g) ?? []).length;
76
- return cmp(hi, kw.relation as string, kw.num_highlights as number);
77
- },
78
- "detectable_format:title": (r) => /<<[^>\n]+>>/.test(r),
79
- "detectable_format:json_format": (r) => {
80
- const t = r.trim().replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
81
- try { JSON.parse(t); return true; } catch { return false; }
82
- },
83
-
84
- // ---- case ----
85
- "change_case:english_lowercase": (r) => r === r.toLowerCase(),
86
- "change_case:english_capital": (r) => r === r.toUpperCase(),
87
-
88
- // ---- start / end ----
89
- "startend:end_checker": (r, kw) =>
90
- norm(r.trimEnd()).endsWith(norm((kw.end_phrase as string).trim())),
91
- "startend:quotation": (r) => {
92
- const t = r.trim();
93
- return t.startsWith('"') && t.endsWith('"') && t.length >= 2;
94
- },
95
-
96
- // ---- punctuation ----
97
- "punctuation:no_comma": (r) => !r.includes(","),
98
- };
99
-
100
- /** Score one IFEval instance (strict). Unknown instruction ids count as failed
101
- * (so coverage gaps never inflate the score) — filter the dataset to the
102
- * supported ids, or extend INSTRUCTIONS, to avoid that. */
103
- export function scoreInstance(instance: IFEvalInstance, response: string): InstanceResult {
104
- const perInstruction = instance.instruction_id_list.map((id, i) => {
105
- const check = INSTRUCTIONS[id];
106
- if (!check) return false;
107
- try { return check(response, instance.kwargs[i] ?? {}, instance); } catch { return false; }
108
- });
109
- return { perInstruction, followedAll: perInstruction.every(Boolean) };
41
+ /**
42
+ * Historical strict-only helper, now delegated to the canonical scorer.
43
+ * New code should prefer `scoreIfevalInstance` for both strict and loose modes.
44
+ */
45
+ export function scoreInstance(
46
+ instance: IFEvalInstance,
47
+ response: string,
48
+ ): InstanceResult {
49
+ const { strict } = scoreIfevalInstance(instance, response);
50
+ return {
51
+ perInstruction: strict.instructionPasses.map((result) => result ?? true),
52
+ followedAll: strict.pass,
53
+ unhandled: strict.unhandled,
54
+ };
110
55
  }
111
56
 
112
- export interface IFEvalReport {
57
+ export interface IFEvalReport extends IfevalResult {
58
+ /** Historical alias for nTotal. */
113
59
  n: number;
114
- /** Fraction of instances where ALL instructions were followed. */
60
+ /** Historical alias for strictAcc (prompt-level strict). */
115
61
  promptAccuracy: number;
116
- /** Fraction of individual instructions followed. */
62
+ /** Historical alias for strictInstructionAcc. */
117
63
  instructionAccuracy: number;
118
64
  }
119
65
 
120
- /** Aggregate the official prompt-level + instruction-level accuracies over a set
121
- * of (instance, response) pairs. */
122
- export function aggregate(pairs: Array<{ instance: IFEvalInstance; response: string }>): IFEvalReport {
123
- let prompts = 0, instrFollowed = 0, instrTotal = 0;
124
- for (const { instance, response } of pairs) {
125
- const r = scoreInstance(instance, response);
126
- if (r.followedAll) prompts++;
127
- instrFollowed += r.perInstruction.filter(Boolean).length;
128
- instrTotal += r.perInstruction.length;
129
- }
66
+ /**
67
+ * Historical aggregation helper with unambiguous canonical fields plus aliases.
68
+ * Unknown instructions do not fail prompt accuracy, are excluded from
69
+ * instruction accuracy, and remain visible in `coverage`.
70
+ */
71
+ export function aggregate(
72
+ pairs: Array<{ instance: IFEvalInstance; response: string }>,
73
+ ): IFEvalReport {
74
+ const result = scoreIfevalPairs(pairs);
130
75
  return {
131
- n: pairs.length,
132
- promptAccuracy: pairs.length ? prompts / pairs.length : 0,
133
- instructionAccuracy: instrTotal ? instrFollowed / instrTotal : 0,
76
+ ...result,
77
+ n: result.nTotal,
78
+ promptAccuracy: result.strictAcc,
79
+ instructionAccuracy: result.strictInstructionAcc,
134
80
  };
135
81
  }
136
-
137
- /** Instruction ids this implementation supports (filter datasets to these). */
138
- export const SUPPORTED_INSTRUCTIONS = new Set(Object.keys(INSTRUCTIONS));
package/src/eval/kl.ts CHANGED
@@ -26,7 +26,7 @@ import { Dtype } from "../mlx/ffi";
26
26
  import * as ops from "../mlx/ops";
27
27
  import { Registry } from "../registry";
28
28
  import { Weights } from "../weights";
29
- import { loadModelConfig, type ModelConfig } from "../config";
29
+ import { loadModelConfig, type ModelConfig, type TurboQuantScheme } from "../config";
30
30
  import { createModel, type RuntimeModel } from "../model/factory";
31
31
  import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
32
32
  import { maybeQuantizeKv } from "../generate";
@@ -223,10 +223,14 @@ function klScalar(p: Float32Array, q: Float32Array): number {
223
223
  }
224
224
 
225
225
  /** Prefill (bf16) → quantize → teacher-forced decode; return per-step logit
226
- * distributions for the last `tokens.length - prefillLen` positions. */
226
+ * distributions for the last `tokens.length - prefillLen` positions.
227
+ * `kvOverride` replaces the default kvConfig-from-model-config scheme when
228
+ * given — the seam evaluateKlKvArm uses to run a TurboQuant (or bf16-null)
229
+ * arm instead of the env-flag self-flip. */
227
230
  function decodeArm(
228
231
  model: RuntimeModel, config: ModelConfig, tokens: number[], prefillLen: number,
229
232
  flag: string, value: string,
233
+ kvOverride?: Parameters<typeof maybeQuantizeKv>[1],
230
234
  ): Float32Array[] {
231
235
  const prev = process.env[flag];
232
236
  process.env[flag] = value;
@@ -237,7 +241,10 @@ function decodeArm(
237
241
  lp.dispose();
238
242
  // quantize the populated caches (mixed 4/8 per kv_config) — exactly
239
243
  // what generate() does between prefill and decode.
240
- maybeQuantizeKv(cache, { kvConfig: config.kvQuant ?? undefined, quantizedKvStart: 0 });
244
+ maybeQuantizeKv(
245
+ cache,
246
+ kvOverride ?? { kvConfig: config.kvQuant ?? undefined, quantizedKvStart: 0 },
247
+ );
241
248
  const out: Float32Array[] = [];
242
249
  for (let i = prefillLen; i < tokens.length; i++) {
243
250
  const logits = model.forward([tokens[i]!], cache); // [1,1,V] quantized decode
@@ -292,3 +299,52 @@ export async function evaluateKlServingDecode(opts: {
292
299
  refLabel: `serving-decode self:${opts.flag}=${opts.refValue}→${opts.candValue} (${decodeSteps} steps)`,
293
300
  };
294
301
  }
302
+
303
+ /** KV-scheme A/B on the teacher-forced serving-decode path: bf16 baseline
304
+ * (no kvOverride) vs an explicit KV scheme (e.g. TurboQuant), same model /
305
+ * same prompts / same weight load — the quality-vs-bpw curve gate
306
+ * (scripts/eval-turboquant-curve.ts). Unlike evaluateKlServingDecode (env-flag
307
+ * self-flip), the two arms differ by an actual GenerateOptions kv scheme
308
+ * object, so this is the seam for schemes that aren't env-lever-shaped. */
309
+ export async function evaluateKlKvArm(opts: {
310
+ candidate: string;
311
+ /** Undefined ⇒ bf16 (no quantization). */
312
+ candidateScheme?: { turboQuant?: TurboQuantScheme; kvBits?: number; kvConfig?: ModelConfig["kvQuant"] };
313
+ prompts: string[];
314
+ nPrompts?: number;
315
+ seqLen?: number;
316
+ decodeSteps?: number;
317
+ }): Promise<KLResult> {
318
+ const t0 = Date.now();
319
+ const n = opts.nPrompts ?? 64;
320
+ const seqLen = opts.seqLen ?? 256;
321
+ const decodeSteps = opts.decodeSteps ?? 32;
322
+ const { model, tokenizer, config } = await loadRunnable(opts.candidate);
323
+ const tokenized = prepPrompts(opts.prompts, tokenizer, seqLen, n);
324
+ const noopFlag = "MLX_BUN_EVAL_KV_ARM_NOOP"; // decodeArm always flips a flag; this one is inert
325
+ const refKv = {}; // bf16: maybeQuantizeKv no-ops with no kvBits/kvConfig/turboQuant
326
+ const candKv = opts.candidateScheme
327
+ ? { ...opts.candidateScheme, quantizedKvStart: 0, kvConfig: opts.candidateScheme.kvConfig ?? undefined }
328
+ : {};
329
+
330
+ const perPrompt: Float32Array[] = [];
331
+ for (const tokens of tokenized) {
332
+ if (tokens.length < 2) continue;
333
+ const prefillLen = Math.max(1, tokens.length - decodeSteps);
334
+ const refSteps = decodeArm(model, config, tokens, prefillLen, noopFlag, "0", refKv);
335
+ const candSteps = decodeArm(model, config, tokens, prefillLen, noopFlag, "0", candKv);
336
+ const m = Math.min(refSteps.length, candSteps.length);
337
+ const kls = new Float32Array(m);
338
+ for (let s = 0; s < m; s++) kls[s] = klScalar(refSteps[s]!, candSteps[s]!);
339
+ perPrompt.push(kls);
340
+ }
341
+
342
+ const agg = aggregate(perPrompt);
343
+ return {
344
+ nPrompts: perPrompt.length, seqLen, meanKl: agg.mean, medianKl: agg.median, p95Kl: agg.p95,
345
+ elapsedSec: (Date.now() - t0) / 1000,
346
+ refLabel: opts.candidateScheme?.turboQuant
347
+ ? `bf16 vs turbo k${opts.candidateScheme.turboQuant.kBits}v${opts.candidateScheme.turboQuant.vBits} (${decodeSteps} steps)`
348
+ : `bf16 vs kv-scheme (${decodeSteps} steps)`,
349
+ };
350
+ }
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // One row = one (model, config) measured on one task (or the aggregate
7
7
  // "capability" / "kl" rows). `config_json` records the perf levers that
8
- // were active (compile / fused-decode / perf-kernel) so a row is
8
+ // were active (compile / fused-sdpa / kv-quant) so a row is
9
9
  // self-describing: which arm of the head-to-head produced this number.
10
10
 
11
11
  import { Database } from "bun:sqlite";
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS quality_runs (
19
19
  model_path TEXT NOT NULL,
20
20
  commit_sha TEXT,
21
21
  task TEXT NOT NULL, -- 'kl' | 'gsm8k' | 'mmlu' | ... | 'capability'
22
- config_json TEXT NOT NULL, -- {compile,fusedDecode,perfKernel,...} active levers
22
+ config_json TEXT NOT NULL, -- {compile,fusedSdpa,kvQuant,...} active levers
23
23
  n_samples INTEGER NOT NULL,
24
24
  -- task accuracy (percent 0-100); null for kl-only rows
25
25
  pct REAL,
@@ -9,8 +9,12 @@ import { generate, type GenerateOptions } from "../generate";
9
9
  import * as ops from "../mlx/ops";
10
10
  import { ChatTemplate } from "../chat-template";
11
11
  import { loadModelConfig, type ModelConfig } from "../config";
12
- import { Weights } from "../weights";
13
- import { createModel, type RuntimeModel } from "../model/factory";
12
+ import {
13
+ openModel,
14
+ type Glm52RuntimeOpenOptions,
15
+ type RuntimeModel,
16
+ } from "../model/factory";
17
+ import { isGlm52Config } from "../model/support";
14
18
  import { DiffusionGemmaModel } from "../model/diffusion-gemma";
15
19
  import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
16
20
  import { resolveModelDir } from "./kl";
@@ -65,11 +69,16 @@ export interface TaskModel {
65
69
  activeAdapters?: string[];
66
70
  }
67
71
 
68
- export async function loadTaskModel(query: string, adapterDir?: string): Promise<TaskModel> {
72
+ export async function loadTaskModel(
73
+ query: string,
74
+ adapterDir?: string,
75
+ runtimeOptions: Glm52RuntimeOpenOptions = {},
76
+ ): Promise<TaskModel> {
69
77
  const dir = resolveModelDir(query);
70
78
  const config = await loadModelConfig(dir);
71
- const weights = await Weights.open(dir);
72
- const model = createModel(weights, config);
79
+ if (adapterDir && isGlm52Config(config))
80
+ throw new Error("GLM-5.2 does not support LoRA adapters");
81
+ const model = await openModel(dir, runtimeOptions);
73
82
  const activeAdapters: string[] = [];
74
83
  if (adapterDir) {
75
84
  // Mount a trained LoRA adapter so the eval measures base+adapter (e.g. the
@@ -99,11 +108,11 @@ export interface GenOpts {
99
108
  * (its capability numbers are non-thinking; e.g. MiniCPM5 IFEval 64.7 is
100
109
  * non-thinking). Set MLX_BUN_EVAL_THINK=1 or pass true to evaluate thinking mode. */
101
110
  enableThinking?: boolean;
102
- /** Explicit KV-quant scheme (cli `generate --l2/--l3/--kv-quant`, resolved with
111
+ /** Explicit KV-quant scheme (cli `generate --l2/--kv-quant`, resolved with
103
112
  * serve's semantics). Overrides the MLX_BUN_EVAL_KV_QUANT env default; an empty
104
113
  * object forces bf16. When it names quantized KV, generation runs the product
105
114
  * generate() path (same tokens as `serve` with the same scheme). */
106
- kvScheme?: Pick<GenerateOptions, "kvBits" | "kvConfig" | "quantizedKvStart">;
115
+ kvScheme?: Pick<GenerateOptions, "kvBits" | "kvConfig" | "quantizedKvStart" | "turboQuant">;
107
116
  }
108
117
 
109
118
  /** Bit-exact greedy decode: raw `model.forward` + argmax loop, which matches mlx-lm
@@ -159,7 +168,7 @@ export async function generateText(tm: TaskModel, body: string, opts: GenOpts =
159
168
  (process.env.MLX_BUN_EVAL_KV_QUANT === "1" && tm.config.kvQuant?.length)
160
169
  ? { kvConfig: tm.config.kvQuant, quantizedKvStart: 0 } : undefined;
161
170
  const kvScheme = opts.kvScheme ?? envKv;
162
- const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length));
171
+ const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant));
163
172
 
164
173
  // Parity default — greedy + full-precision KV + no sampler arm: decode bit-exactly
165
174
  // via the raw forward (matches mlx-lm token-for-token). Sampler arms / kv-quant fall
@@ -5,13 +5,14 @@
5
5
  // 'banana'", "respond in JSON"). Each example carries one or more constraint
6
6
  // instructions; we generate a response and run the corresponding verifier.
7
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__.
8
+ // We report strict and loose metrics at both official granularities:
9
+ // * prompt — fraction of examples where ALL instructions pass;
10
+ // * instruction — fraction of supported individual checks that pass.
11
+ // Loose mode first strips leading "Sure, here is…" boilerplate and outer code
12
+ // fences. The primary metric (`accuracy`) remains prompt-level strict, as
13
+ // headlined by ifeval.py's IFEvalResult.__str__. Unknown instruction IDs retain
14
+ // optiq's prompt-pass behavior but are excluded from instruction accuracy and
15
+ // reported through the coverage object.
15
16
  //
16
17
  // Faithful port note: several verifiers below read kwarg keys that the dataset
17
18
  // rows never populate (e.g. _check_capital_words_count / _check_letter_frequency
@@ -21,18 +22,56 @@
21
22
 
22
23
  import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
23
24
 
24
- interface IfevalRow {
25
- key: number;
25
+ export interface IfevalInstance {
26
26
  prompt: string;
27
27
  instruction_id_list: string[];
28
28
  kwargs: Record<string, unknown>[];
29
29
  }
30
30
 
31
+ interface IfevalRow extends IfevalInstance {
32
+ key: number;
33
+ }
34
+
35
+ export interface IfevalCoverage {
36
+ /** Prompts for which every instruction ID has a registered verifier. */
37
+ fullySupportedPrompts: number;
38
+ promptCoverage: number;
39
+ /** Registered instruction checks; unknown IDs are excluded from accuracy. */
40
+ supportedInstructions: number;
41
+ totalInstructions: number;
42
+ instructionCoverage: number;
43
+ unhandledInstructionCounts: Record<string, number>;
44
+ }
45
+
31
46
  export interface IfevalResult {
32
47
  nTotal: number;
33
48
  strictAcc: number; // 0..1 — prompt-level strict pass rate
34
49
  looseAcc: number; // 0..1 — prompt-level loose pass rate
35
50
  accuracy: number; // 0..1 — primary metric == strictAcc (ifeval.py headline)
51
+ strictInstructionAcc: number;
52
+ looseInstructionAcc: number;
53
+ coverage: IfevalCoverage;
54
+ }
55
+
56
+ export interface IfevalVerification {
57
+ /**
58
+ * One result per requested instruction. null means the instruction ID is
59
+ * unhandled and therefore excluded from instruction-level accuracy.
60
+ */
61
+ instructionPasses: Array<boolean | null>;
62
+ /** OptiQ parity: unhandled IDs do not make the prompt fail. */
63
+ pass: boolean;
64
+ unhandled: string[];
65
+ }
66
+
67
+ export interface IfevalInstanceScore {
68
+ strict: IfevalVerification;
69
+ loose: IfevalVerification;
70
+ }
71
+
72
+ export interface IfevalPair {
73
+ instance: IfevalInstance;
74
+ response: string;
36
75
  }
37
76
 
38
77
  // ---------------------------------------------------------------------------
@@ -278,20 +317,24 @@ const VERIFIERS = new Map<string, Verifier>([
278
317
  ["detectable_content:postscript", alwaysPass],
279
318
  ]);
280
319
 
320
+ export const SUPPORTED_INSTRUCTIONS = new Set(VERIFIERS.keys());
321
+
281
322
  /**
282
323
  * Verify one response against its instruction list.
283
- * Returns { pass, unhandled }. An instruction_id with no registered verifier is
284
- * treated as a PASS (does not affect `pass`) and only recorded in `unhandled` for
285
- * visibility — matching ifeval.py's `continue` policy EXACTLY, so our strict_acc
286
- * reproduces optiq's. (Our VERIFIERS map mirrors his `_VERIFIERS` 1:1, so unhandled
287
- * is empty for the standard IFEval set anyway; this only matters for parity safety.)
324
+ * `instructionPasses` is aligned with the requested IDs and uses null for
325
+ * unhandled IDs. An ID with no registered verifier does not affect prompt
326
+ * `pass` and is recorded in `unhandled` — matching ifeval.py's `continue`
327
+ * policy exactly, so strict prompt accuracy reproduces OptiQ's. The canonical
328
+ * aggregate excludes those nulls from instruction accuracy and reports them
329
+ * through coverage.
288
330
  */
289
331
  export function verifyResponse(
290
332
  response: string,
291
333
  instructionIds: string[],
292
334
  kwargsList: Kw[],
293
- ): { pass: boolean; unhandled: string[] } {
335
+ ): IfevalVerification {
294
336
  const unhandled: string[] = [];
337
+ const instructionPasses: Array<boolean | null> = [];
295
338
  let pass = true;
296
339
  for (let i = 0; i < instructionIds.length; i++) {
297
340
  const iid = instructionIds[i]!;
@@ -299,15 +342,19 @@ export function verifyResponse(
299
342
  const verifier = VERIFIERS.get(iid);
300
343
  if (verifier === undefined) {
301
344
  unhandled.push(iid);
345
+ instructionPasses.push(null);
302
346
  continue; // ifeval.py: treat an unhandled instruction as PASS (don't touch `pass`)
303
347
  }
304
348
  try {
305
- if (!verifier(response, kw)) pass = false;
349
+ const followed = verifier(response, kw);
350
+ instructionPasses.push(followed);
351
+ if (!followed) pass = false;
306
352
  } catch {
353
+ instructionPasses.push(false);
307
354
  pass = false;
308
355
  }
309
356
  }
310
- return { pass, unhandled };
357
+ return { instructionPasses, pass, unhandled };
311
358
  }
312
359
 
313
360
  /** Loose-mode preprocessing — mirrors ifeval.py `_loose_clean`. */
@@ -328,6 +375,92 @@ export function stripThinking(response: string): string {
328
375
  return idx === -1 ? response : response.slice(idx + "</think>".length);
329
376
  }
330
377
 
378
+ /** Score one generated response with the canonical strict + loose contract. */
379
+ export function scoreIfevalInstance(
380
+ instance: IfevalInstance,
381
+ rawResponse: string,
382
+ ): IfevalInstanceScore {
383
+ const response = stripThinking(rawResponse);
384
+ return {
385
+ strict: verifyResponse(
386
+ response,
387
+ instance.instruction_id_list,
388
+ instance.kwargs,
389
+ ),
390
+ loose: verifyResponse(
391
+ looseClean(response),
392
+ instance.instruction_id_list,
393
+ instance.kwargs,
394
+ ),
395
+ };
396
+ }
397
+
398
+ /**
399
+ * Canonical IFEval aggregation.
400
+ *
401
+ * Prompt accuracy follows the OptiQ reference behavior: an unknown instruction
402
+ * is recorded but does not fail its prompt. Instruction accuracy has a stricter
403
+ * denominator contract: unknown instructions are excluded and their coverage
404
+ * is reported explicitly, so an incomplete verifier registry cannot silently
405
+ * inflate that metric.
406
+ */
407
+ export function scoreIfevalPairs(pairs: IfevalPair[]): IfevalResult {
408
+ let strictPrompts = 0;
409
+ let loosePrompts = 0;
410
+ let fullySupportedPrompts = 0;
411
+ let strictInstructions = 0;
412
+ let looseInstructions = 0;
413
+ let supportedInstructions = 0;
414
+ let totalInstructions = 0;
415
+ const unhandledCounts = new Map<string, number>();
416
+
417
+ for (const { instance, response } of pairs) {
418
+ const score = scoreIfevalInstance(instance, response);
419
+ if (score.strict.pass) strictPrompts++;
420
+ if (score.loose.pass) loosePrompts++;
421
+ if (score.strict.unhandled.length === 0) fullySupportedPrompts++;
422
+ totalInstructions += instance.instruction_id_list.length;
423
+
424
+ for (let i = 0; i < score.strict.instructionPasses.length; i++) {
425
+ const strict = score.strict.instructionPasses[i];
426
+ if (strict === null) continue;
427
+ supportedInstructions++;
428
+ if (strict) strictInstructions++;
429
+ if (score.loose.instructionPasses[i]) looseInstructions++;
430
+ }
431
+ for (const id of score.strict.unhandled)
432
+ unhandledCounts.set(id, (unhandledCounts.get(id) ?? 0) + 1);
433
+ }
434
+
435
+ const nTotal = pairs.length;
436
+ const strictAcc = nTotal ? strictPrompts / nTotal : 0;
437
+ const looseAcc = nTotal ? loosePrompts / nTotal : 0;
438
+ return {
439
+ nTotal,
440
+ strictAcc,
441
+ looseAcc,
442
+ accuracy: strictAcc,
443
+ strictInstructionAcc: supportedInstructions
444
+ ? strictInstructions / supportedInstructions
445
+ : 0,
446
+ looseInstructionAcc: supportedInstructions
447
+ ? looseInstructions / supportedInstructions
448
+ : 0,
449
+ coverage: {
450
+ fullySupportedPrompts,
451
+ promptCoverage: nTotal ? fullySupportedPrompts / nTotal : 0,
452
+ supportedInstructions,
453
+ totalInstructions,
454
+ instructionCoverage: totalInstructions
455
+ ? supportedInstructions / totalInstructions
456
+ : 0,
457
+ unhandledInstructionCounts: Object.fromEntries(
458
+ [...unhandledCounts.entries()].sort(([a], [b]) => a.localeCompare(b)),
459
+ ),
460
+ },
461
+ };
462
+ }
463
+
331
464
  export async function evaluateIfeval(
332
465
  tm: TaskModel,
333
466
  opts: { nSamples?: number; maxTokens?: number; seed?: number; frozen?: boolean } = {},
@@ -348,7 +481,7 @@ export async function evaluateIfeval(
348
481
 
349
482
  let nStrict = 0;
350
483
  let nLoose = 0;
351
- const unhandledCounts = new Map<string, number>();
484
+ const pairs: IfevalPair[] = [];
352
485
 
353
486
  for (let k = 0; k < idx.length; k++) {
354
487
  const item = rows[idx[k]!]!;
@@ -356,14 +489,16 @@ export async function evaluateIfeval(
356
489
  const kwList = item.kwargs ?? iids.map(() => ({}));
357
490
 
358
491
  const raw = await generateText(tm, item.prompt, { maxTokens, useChat: true });
359
- const response = stripThinking(raw);
360
-
361
- const strict = verifyResponse(response, iids, kwList);
362
- const loose = verifyResponse(looseClean(response), iids, kwList);
363
- for (const u of strict.unhandled) unhandledCounts.set(u, (unhandledCounts.get(u) ?? 0) + 1);
492
+ const instance: IfevalInstance = {
493
+ prompt: item.prompt,
494
+ instruction_id_list: iids,
495
+ kwargs: kwList,
496
+ };
497
+ const score = scoreIfevalInstance(instance, raw);
498
+ pairs.push({ instance, response: raw });
364
499
 
365
- if (strict.pass) nStrict++;
366
- if (loose.pass) nLoose++;
500
+ if (score.strict.pass) nStrict++;
501
+ if (score.loose.pass) nLoose++;
367
502
 
368
503
  if ((k + 1) % 10 === 0 || k + 1 === idx.length)
369
504
  process.stderr.write(
@@ -372,8 +507,12 @@ export async function evaluateIfeval(
372
507
  }
373
508
  process.stderr.write("\n");
374
509
 
375
- if (unhandledCounts.size) {
376
- const top = [...unhandledCounts.entries()].sort((a, b) => b[1] - a[1]);
510
+ const result = scoreIfevalPairs(pairs);
511
+ const unhandledCounts = Object.entries(
512
+ result.coverage.unhandledInstructionCounts,
513
+ );
514
+ if (unhandledCounts.length) {
515
+ const top = unhandledCounts.sort((a, b) => b[1] - a[1]);
377
516
  process.stderr.write(
378
517
  " ifeval unported instruction ids (treated as PASS, per optiq): " +
379
518
  top.map(([k, v]) => `${k}(${v})`).join(", ") +
@@ -381,8 +520,5 @@ export async function evaluateIfeval(
381
520
  );
382
521
  }
383
522
 
384
- const nTotal = idx.length;
385
- const strictAcc = nTotal ? nStrict / nTotal : 0;
386
- const looseAcc = nTotal ? nLoose / nTotal : 0;
387
- return { nTotal, strictAcc, looseAcc, accuracy: strictAcc };
523
+ return result;
388
524
  }