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,303 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
2
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3
|
+
<title>HLG Curve Designer</title>
|
|
4
|
+
<style>
|
|
5
|
+
:root{--text:#1f2937;--text2:#475569;--text3:#94a3b8;--bg:#f7f9fc;--card:#fff;--bd:#e2e8f0;
|
|
6
|
+
--accent:#2563eb;--accent2:#0891b2;--pos:#16a34a;--warn:#d97706;--danger:#dc2626;--grid:#eef2f7;}
|
|
7
|
+
*{box-sizing:border-box}
|
|
8
|
+
body{margin:0;background:var(--bg);color:var(--text);font:13.5px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",ui-sans-serif,system-ui,sans-serif;-webkit-font-smoothing:antialiased}
|
|
9
|
+
.wrap{max-width:1240px;margin:0 auto;padding:18px 20px 70px}
|
|
10
|
+
.head{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:14px}
|
|
11
|
+
h1{font-size:19px;margin:0;letter-spacing:-.01em}
|
|
12
|
+
.head a.back{font-size:12px;color:var(--accent);text-decoration:none}
|
|
13
|
+
.head .desc{color:var(--text2);font-size:13px}
|
|
14
|
+
.engine{margin-left:auto;display:flex;align-items:center;gap:7px;font-size:12px}
|
|
15
|
+
.engine input{font:12px ui-monospace,Menlo,monospace;border:1px solid var(--bd);border-radius:7px;padding:6px 8px;width:230px;background:var(--card)}
|
|
16
|
+
.engine .dot{width:8px;height:8px;border-radius:50%;background:var(--text3)}
|
|
17
|
+
.engine .dot.ok{background:var(--pos)} .engine .dot.bad{background:var(--danger)}
|
|
18
|
+
.cols{display:grid;grid-template-columns:minmax(440px,1fr) 1fr;gap:18px;align-items:start}
|
|
19
|
+
@media(max-width:920px){.cols{grid-template-columns:1fr}}
|
|
20
|
+
.card{background:var(--card);border:1px solid var(--bd);border-radius:14px;padding:14px 16px;box-shadow:0 1px 2px rgba(15,23,42,.04),0 10px 30px -24px rgba(15,23,42,.2)}
|
|
21
|
+
.card h2{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--text2);margin:0 0 10px;display:flex;align-items:center;gap:10px}
|
|
22
|
+
svg.plot{width:100%;height:auto;display:block;touch-action:none;cursor:crosshair}
|
|
23
|
+
.slopes{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin:10px 0 4px}
|
|
24
|
+
.slopes .s{background:var(--grid);border-radius:9px;padding:6px 8px}
|
|
25
|
+
.slopes .s .k{font-size:9px;text-transform:uppercase;letter-spacing:.03em;color:var(--text3)}
|
|
26
|
+
.slopes .s .v{font-size:14px;font-weight:700;font-variant-numeric:tabular-nums}
|
|
27
|
+
.slopes .s .t{font-size:9.5px}
|
|
28
|
+
.copyrow{display:flex;gap:7px;align-items:center;margin-top:12px}
|
|
29
|
+
.copyrow input{flex:1;font:11px ui-monospace,Menlo,monospace;border:1px solid var(--bd);border-radius:7px;padding:7px 8px;background:var(--grid)}
|
|
30
|
+
button.primary{background:var(--accent);color:#fff;border:0;border-radius:9px;padding:9px 16px;font:700 13px inherit;cursor:pointer}
|
|
31
|
+
button.primary:disabled{opacity:.55;cursor:default}
|
|
32
|
+
button.ghost{background:var(--card);border:1px solid var(--bd);border-radius:8px;padding:7px 11px;font:600 12px inherit;color:var(--text2);cursor:pointer}
|
|
33
|
+
.prompt{width:100%;font:13px inherit;border:1px solid var(--bd);border-radius:9px;padding:9px 10px;resize:vertical;min-height:54px;background:var(--card)}
|
|
34
|
+
.pchips{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}
|
|
35
|
+
.pchips button{font:600 11px/1 inherit;padding:6px 9px;border:1px solid var(--bd);background:var(--grid);border-radius:7px;cursor:pointer;color:var(--text2)}
|
|
36
|
+
.genrow{display:flex;gap:10px;align-items:center;margin:10px 0;flex-wrap:wrap}
|
|
37
|
+
.genrow .mini{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--text2)}
|
|
38
|
+
.genrow .mini input{width:56px;font:12px inherit;border:1px solid var(--bd);border-radius:6px;padding:5px 6px}
|
|
39
|
+
.samples{display:flex;flex-direction:column;gap:8px;margin-top:6px}
|
|
40
|
+
.samp{font-size:13.5px;background:var(--grid);border-radius:9px;padding:9px 11px;border-left:3px solid var(--accent);white-space:pre-wrap}
|
|
41
|
+
.samp.junk{border-left-color:var(--danger);background:#fef6f6}
|
|
42
|
+
.samp .tag{font-size:10px;font-weight:700;color:var(--danger);float:right}
|
|
43
|
+
.status{font-size:12px;color:var(--text3);min-height:16px}
|
|
44
|
+
.note{font-size:11.5px;color:var(--text3);margin-top:10px;line-height:1.5}
|
|
45
|
+
.testcard{border:1px solid var(--bd);border-radius:12px;padding:13px 15px;margin:6px 0;background:linear-gradient(180deg,#f4f8ff,#fff)}
|
|
46
|
+
.testcard .opt{font-size:14px;background:var(--card);border:1px solid var(--bd);border-radius:9px;padding:10px 12px;margin:7px 0;cursor:pointer;display:flex;gap:9px;transition:border-color .12s}
|
|
47
|
+
.testcard .opt:hover{border-color:var(--accent2)}
|
|
48
|
+
.testcard .opt .lab{font-weight:800;color:var(--accent2);flex:none}
|
|
49
|
+
.testcard .opt.picked{border-color:var(--accent2);box-shadow:0 0 0 2px rgba(8,145,178,.15)}
|
|
50
|
+
.testcard .opt .reveal{margin-left:auto;font-size:11px;font-weight:700;flex:none;align-self:center}
|
|
51
|
+
.testcard .q{font-size:12.5px;font-weight:600;margin:4px 0 2px}
|
|
52
|
+
.testcard .verdict{font-size:13px;font-weight:700;margin-top:8px;min-height:18px}
|
|
53
|
+
</style></head>
|
|
54
|
+
<body><div class="wrap">
|
|
55
|
+
<div class="head">
|
|
56
|
+
<h1>HLG Curve Designer</h1>
|
|
57
|
+
<a class="back" href="/" id="backLink" style="display:none">← mlx-bun</a>
|
|
58
|
+
<span class="desc">Drag the curve over the signal → generate → read prose → adjust.</span>
|
|
59
|
+
<span class="engine"><span class="dot" id="engdot"></span>engine <input id="engine"></span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="cols">
|
|
62
|
+
<div class="card">
|
|
63
|
+
<h2>Transfer curve <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--text3)">x = p_in, y = p_out (log) · diagonal = temperature 1 · the gray histogram is the real next-token signal</span></h2>
|
|
64
|
+
<svg class="plot" id="plot" viewBox="0 0 460 360"></svg>
|
|
65
|
+
<div class="slopes" id="slopeReadout"></div>
|
|
66
|
+
<div id="baselineHint" style="font-size:11.5px;font-weight:600;margin:4px 0 0"></div>
|
|
67
|
+
<div class="copyrow"><input id="cfgField" readonly><button class="ghost" id="copyBtn">Copy values</button><button class="ghost" id="resetBtn">Reset</button><span id="copyOk" style="color:var(--pos);font-weight:700;font-size:11px"></span></div>
|
|
68
|
+
<div class="note">Five movable control points; drag any of them (including the corners). It always stays a smooth monotone curve. <b>Above the diagonal</b> raises those tokens, <b>below</b> lowers them; drag a point down over the histogram's tail to gate it.</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="card">
|
|
71
|
+
<h2>Generate</h2>
|
|
72
|
+
<textarea class="prompt" id="prompt">Write the first sentence of a short story about a lighthouse keeper who discovers something strange in the fog.</textarea>
|
|
73
|
+
<div class="pchips" id="pchips"></div>
|
|
74
|
+
<div class="genrow">
|
|
75
|
+
<button class="primary" id="genBtn">Generate</button>
|
|
76
|
+
<button class="primary" id="testBtn" style="background:var(--accent2)" title="One sentence from the default recipe, one from your curve — unlabeled. Guess which is which.">Test (blind)</button>
|
|
77
|
+
<button class="ghost" id="sigBtn">↻ load signal</button>
|
|
78
|
+
<button class="ghost" id="copyTextBtn" style="display:none">Copy text</button>
|
|
79
|
+
<span class="mini">N <input id="nSamp" type="number" min="1" max="8" value="4"></span>
|
|
80
|
+
<span class="mini">seed <input id="seed" type="number" value="100"></span>
|
|
81
|
+
<span class="status" id="status"></span>
|
|
82
|
+
</div>
|
|
83
|
+
<div id="testArea"></div>
|
|
84
|
+
<div class="samples" id="samples"></div>
|
|
85
|
+
<div class="note">Samples are a <b>feel-check</b>, not evidence — easy to overfit. The blind two-judge eval stays the verdict.</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<script>
|
|
90
|
+
const ln=Math.log;
|
|
91
|
+
const UMIN=ln(1e-6); // bottom-left / plot floor (matches src/curve-sampler.ts CURVE_UMIN)
|
|
92
|
+
const ENGINE_BASE = location.protocol.startsWith("http") ? location.origin : "http://localhost:8090";
|
|
93
|
+
// ---- curve state: 5 movable control points, sorted ascending in p_in ----
|
|
94
|
+
function identityPoints(){ return [{x_pct:1e-4,y_pct:1e-4},{x_pct:0.1,y_pct:0.1},{x_pct:1,y_pct:1},{x_pct:9,y_pct:9},{x_pct:100,y_pct:100}]; }
|
|
95
|
+
let C = { space:"logprob", points:identityPoints(), monotonic:true };
|
|
96
|
+
let SIGNAL=null; // {bins:[], vocab}
|
|
97
|
+
let LAST=null; // {prompt, curve, samples} of the most recent generation
|
|
98
|
+
const PROMPTS=[
|
|
99
|
+
"Write the first sentence of a short story about a lighthouse keeper who discovers something strange in the fog.",
|
|
100
|
+
"In one sentence, what causes the seasons on Earth?",
|
|
101
|
+
"Give one unusual but genuinely plausible use for a paperclip, in a sentence.",
|
|
102
|
+
"Describe the feeling of a city at 3am in one vivid sentence.",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// ---- math: monotone cubic (PCHIP), IDENTICAL to src/curve-sampler.ts ----
|
|
106
|
+
const Lp=pt=>[ln(Math.max(pt.x_pct,1e-9)/100),ln(Math.max(pt.y_pct,1e-9)/100)];
|
|
107
|
+
function knotArrays(c){ const k=c.points.map(Lp).sort((a,b)=>a[0]-b[0]); return {X:k.map(q=>q[0]),Y:k.map(q=>q[1])}; }
|
|
108
|
+
function secants(c){ const {X,Y}=knotArrays(c),s=[]; for(let i=X.length-1;i>0;i--)s.push((Y[i]-Y[i-1])/(X[i]-X[i-1])); return s; }
|
|
109
|
+
function monotone(c){ return secants(c).every(s=>s>=-1e-12); }
|
|
110
|
+
// identity = every control point on the diagonal ⇒ generate with the model's DEFAULT
|
|
111
|
+
// chat recipe (the real "what you'd get chatting" baseline), not a literal temp-1 curve.
|
|
112
|
+
function isIdentity(c){ return c.points.every(p=>Math.abs(ln(Math.max(p.x_pct,1e-9))-ln(Math.max(p.y_pct,1e-9)))<0.03); }
|
|
113
|
+
function cubicInU(x0,h,y0,y1,m0,m1){ const c0=y0,c1=h*m0,c2=-3*y0-2*h*m0+3*y1-h*m1,c3=2*y0+h*m0-2*y1+h*m1; const s=1/h,s2=s*s,s3=s2*s; let A=c0,B=0,C=0,D=0; B+=c1*s;A+=-c1*s*x0; C+=c2*s2;B+=-2*x0*c2*s2;A+=x0*x0*c2*s2; D+=c3*s3;C+=-3*x0*c3*s3;B+=3*x0*x0*c3*s3;A+=-x0*x0*x0*c3*s3; return {A,B,C,D}; }
|
|
114
|
+
function buildSpline(c){ const {X,Y}=knotArrays(c),n=X.length,D=[];
|
|
115
|
+
for(let i=0;i<n-1;i++)D.push((Y[i+1]-Y[i])/(X[i+1]-X[i]));
|
|
116
|
+
const M=new Array(n); M[0]=D[0]; M[n-1]=D[n-2];
|
|
117
|
+
for(let i=1;i<n-1;i++){ if(D[i-1]*D[i]<=0){M[i]=0;continue;} const h1=X[i]-X[i-1],h2=X[i+1]-X[i],w1=2*h2+h1,w2=h2+2*h1; M[i]=(w1+w2)/(w1/D[i-1]+w2/D[i]); }
|
|
118
|
+
const segs=[]; for(let i=0;i<n-1;i++)segs.push(cubicInU(X[i],X[i+1]-X[i],Y[i],Y[i+1],M[i],M[i+1])); return {X,Y,M,segs}; }
|
|
119
|
+
function evalSpline(sp,u){ const n=sp.X.length;
|
|
120
|
+
if(u<sp.X[0])return sp.Y[0]+Math.max(sp.M[0],1)*(u-sp.X[0]);
|
|
121
|
+
if(u>sp.X[n-1])return sp.Y[n-1]+sp.M[n-1]*(u-sp.X[n-1]);
|
|
122
|
+
let i=sp.segs.length-1; while(i>0&&u<sp.X[i])i--; const s=sp.segs[i]; return s.A+s.B*u+s.C*u*u+s.D*u*u*u; }
|
|
123
|
+
|
|
124
|
+
// ---- plot mapping ----
|
|
125
|
+
const VBW=460,VBH=360,PL=46,PR=14,PT=14,PB=34, FLOOR=1e-4;
|
|
126
|
+
// The data domain [UMIN,0] maps EXACTLY onto the box, so its corners ARE the data
|
|
127
|
+
// extremes: the first control point lives in the bottom-left corner, the last in
|
|
128
|
+
// the top-right. Dragging an endpoint keeps it welded to the box edge — its X is
|
|
129
|
+
// pinned (see clampPoint) and Y slides along that edge, like a tone-curve endpoint.
|
|
130
|
+
const px=u=>PL+(1-u/UMIN)*(VBW-PL-PR), py=u=>PT+(u/UMIN)*(VBH-PT-PB);
|
|
131
|
+
const uX=mx=>UMIN*(1-(mx-PL)/(VBW-PL-PR)), uY=my=>UMIN*((my-PT)/(VBH-PT-PB)), pct=u=>Math.exp(u)*100;
|
|
132
|
+
const TICKS=[100,10,1,0.1,0.01,0.001,0.0001]; // 0.0001% = UMIN = the box floor/left edge, so the corner is labeled
|
|
133
|
+
const PCOL=["#94a3b8","#0891b2","#7c3aed","#2563eb","#94a3b8"]; // endpoint, knee, center, shoulder, endpoint
|
|
134
|
+
|
|
135
|
+
function draw(){
|
|
136
|
+
let s=`<defs><clipPath id="plotclip"><rect x="${PL}" y="${PT}" width="${VBW-PL-PR}" height="${VBH-PT-PB}"/></clipPath></defs>`;
|
|
137
|
+
// signal histogram (behind)
|
|
138
|
+
if(SIGNAL&&SIGNAL.bins){ const b=SIGNAL.bins,NB=b.length,maxc=Math.max(...b,1),hz=(VBH-PT-PB)*0.55,base=VBH-PB;
|
|
139
|
+
for(let i=0;i<NB;i++){ if(!b[i])continue; const x0=px(UMIN*(1-i/NB)),x1=px(UMIN*(1-(i+1)/NB)); const hgt=ln(1+b[i])/ln(1+maxc)*hz;
|
|
140
|
+
s+=`<rect x="${Math.min(x0,x1)}" y="${base-hgt}" width="${Math.abs(x1-x0)+0.4}" height="${hgt}" fill="var(--text3)" opacity="0.26"/>`; } }
|
|
141
|
+
for(const p of TICKS){ const u=ln(p/100);
|
|
142
|
+
s+=`<line x1="${px(u)}" y1="${PT}" x2="${px(u)}" y2="${VBH-PB}" stroke="var(--grid)"/><line x1="${PL}" y1="${py(u)}" x2="${VBW-PR}" y2="${py(u)}" stroke="var(--grid)"/>`;
|
|
143
|
+
s+=`<text x="${px(u)}" y="${VBH-PB+13}" text-anchor="middle" font-size="8.5" fill="var(--text3)">${p}%</text>`;
|
|
144
|
+
s+=`<text x="${PL-9}" y="${py(u)+3}" text-anchor="end" font-size="8.5" fill="var(--text3)">${p}%</text>`; }
|
|
145
|
+
s+=`<text x="${(PL+VBW-PR)/2}" y="${VBH-3}" text-anchor="middle" font-size="10" fill="var(--text2)">p_in (model's true probability)</text>`;
|
|
146
|
+
s+=`<line x1="${px(UMIN)}" y1="${py(UMIN)}" x2="${px(0)}" y2="${py(0)}" stroke="var(--text3)" stroke-width="1.4" stroke-dasharray="5 4"/>`;
|
|
147
|
+
// smooth clipped curve
|
|
148
|
+
const sp=buildSpline(C); let d="";
|
|
149
|
+
for(let i=0;i<=240;i++){ const u=UMIN*(1-i/240); const v=Math.max(Math.min(evalSpline(sp,u),0),UMIN*1.25); d+=(i?"L":"M")+px(u).toFixed(1)+" "+py(v).toFixed(1)+" "; }
|
|
150
|
+
s+=`<g clip-path="url(#plotclip)"><path d="${d}" fill="none" stroke="var(--accent)" stroke-width="2.6"/></g>`;
|
|
151
|
+
// 5 control points
|
|
152
|
+
C.points.forEach((p,i)=>{ const u=Lp(p); const col=PCOL[Math.min(i,PCOL.length-1)];
|
|
153
|
+
s+=`<circle class="kp" data-i="${i}" cx="${px(u[0])}" cy="${py(u[1])}" r="7" fill="${col}" stroke="#fff" stroke-width="2" style="cursor:grab"/>`; });
|
|
154
|
+
document.getElementById("plot").innerHTML=s;
|
|
155
|
+
bindDrag(); drawSlopes(); document.getElementById("cfgField").value=cfgJSON();
|
|
156
|
+
document.getElementById("baselineHint").innerHTML = isIdentity(C)
|
|
157
|
+
? '<span style="color:var(--pos)">● identity</span> — Generate uses the model’s <b>default chat recipe</b> (temp + top-p + top-k): the real baseline.'
|
|
158
|
+
: '<span style="color:var(--accent)">● custom curve</span> — Generate replaces temperature+softmax with your curve.';
|
|
159
|
+
}
|
|
160
|
+
function drawSlopes(){
|
|
161
|
+
const sc=secants(C); const names=["head","upper-mid","lower-mid","shadows"];
|
|
162
|
+
const cell=(k,sl)=>{ const t=sl<0.97?"flatten":sl>1.03?"sharpen":"≈ identity", col=sl<0.97?"var(--pos)":sl>1.03?"var(--danger)":"var(--text2)";
|
|
163
|
+
return `<div class="s"><div class="k">${k}</div><div class="v">${sl.toFixed(2)}</div><div class="t" style="color:${col}">${t}</div></div>`; };
|
|
164
|
+
document.getElementById("slopeReadout").innerHTML=sc.slice(0,4).map((sl,i)=>cell(names[i]||("seg"+i),sl)).join("");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function clampPoint(i){
|
|
168
|
+
const P=C.points, n=P.length, lo=i>0?P[i-1]:null, hi=i<n-1?P[i+1]:null;
|
|
169
|
+
if(i===0) P[i].x_pct=FLOOR; // first point welded to the LEFT edge of the box
|
|
170
|
+
else if(i===n-1) P[i].x_pct=100; // last point welded to the RIGHT edge of the box
|
|
171
|
+
else P[i].x_pct=Math.max(lo.x_pct*1.03, Math.min(hi.x_pct*0.97, P[i].x_pct));
|
|
172
|
+
P[i].y_pct=Math.max(lo?lo.y_pct*1.03:FLOOR, Math.min(hi?hi.y_pct*0.97:100, P[i].y_pct));
|
|
173
|
+
}
|
|
174
|
+
function bindDrag(){
|
|
175
|
+
const svg=document.getElementById("plot");
|
|
176
|
+
svg.querySelectorAll(".kp").forEach(el=>{
|
|
177
|
+
el.addEventListener("pointerdown",ev=>{ ev.preventDefault(); el.setPointerCapture(ev.pointerId);
|
|
178
|
+
clearGeneratedState();
|
|
179
|
+
const i=+el.dataset.i, rect=svg.getBoundingClientRect();
|
|
180
|
+
const move=e=>{ const mx=(e.clientX-rect.left)*(VBW/rect.width), my=(e.clientY-rect.top)*(VBH/rect.height);
|
|
181
|
+
C.points[i].x_pct=Math.min(100,Math.max(FLOOR,pct(uX(mx)))); C.points[i].y_pct=Math.min(100,Math.max(FLOOR,pct(uY(my)))); clampPoint(i); draw(); };
|
|
182
|
+
const up=()=>{ try{el.releasePointerCapture(ev.pointerId)}catch(_){}; window.removeEventListener("pointermove",move); window.removeEventListener("pointerup",up); };
|
|
183
|
+
window.addEventListener("pointermove",move); window.addEventListener("pointerup",up);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function cfgJSON(){ return JSON.stringify({space:"logprob",points:C.points.map(p=>({x_pct:+p.x_pct.toFixed(4),y_pct:+p.y_pct.toFixed(5)})),monotonic:true}); }
|
|
188
|
+
function clearGeneratedState(status=""){
|
|
189
|
+
document.getElementById("samples").innerHTML="";
|
|
190
|
+
document.getElementById("testArea").innerHTML="";
|
|
191
|
+
document.getElementById("copyTextBtn").style.display="none";
|
|
192
|
+
document.getElementById("status").textContent=status;
|
|
193
|
+
LAST=null; TESTST=null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- server calls ----
|
|
197
|
+
const engBase=()=>document.getElementById("engine").value.replace(/\/(generate|signal)\/?$/,"");
|
|
198
|
+
async function probe(){ const dot=document.getElementById("engdot");
|
|
199
|
+
try{ const r=await fetch(engBase()+"/v1"); dot.className="dot "+(r.ok?"ok":"bad"); }catch(_){ dot.className="dot bad"; } }
|
|
200
|
+
async function loadSignal(){
|
|
201
|
+
const st=document.getElementById("status");
|
|
202
|
+
try{ const r=await fetch(engBase()+"/signal",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({prompt:document.getElementById("prompt").value})});
|
|
203
|
+
if(!r.ok)return; const j=await r.json(); SIGNAL=j; draw(); st.textContent="signal loaded ("+(j.vocab||"?")+" tokens)"; setTimeout(()=>{if(st.textContent.startsWith("signal"))st.textContent=""},1800);
|
|
204
|
+
}catch(_){ /* engine not up — no histogram */ }
|
|
205
|
+
}
|
|
206
|
+
async function generate(){
|
|
207
|
+
const btn=document.getElementById("genBtn"), st=document.getElementById("status"), out=document.getElementById("samples");
|
|
208
|
+
clearGeneratedState();
|
|
209
|
+
if(!monotone(C)){ st.textContent="curve not monotone."; return; }
|
|
210
|
+
btn.disabled=true; const baseline=isIdentity(C); st.textContent=baseline?"generating (model default)…":"generating…"; out.innerHTML="";
|
|
211
|
+
const payload={ prompt:document.getElementById("prompt").value,
|
|
212
|
+
n:Math.max(1,Math.min(8,+document.getElementById("nSamp").value||4)), seed:+document.getElementById("seed").value||100, max_tokens:90,
|
|
213
|
+
...(baseline?{default:true}:{curve:JSON.parse(cfgJSON())}) };
|
|
214
|
+
try{
|
|
215
|
+
const r=await fetch(engBase()+"/generate",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(payload)});
|
|
216
|
+
const j=await r.json();
|
|
217
|
+
if(!r.ok){ st.textContent="engine error: "+(j.error||r.status); btn.disabled=false; return; }
|
|
218
|
+
const tag = j.mode==="default"&&j.recipe ? `model default · T=${j.recipe.temperature} top-p=${j.recipe.topP} top-k=${j.recipe.topK}` : "your curve";
|
|
219
|
+
st.textContent=`${tag} · ${j.samples.length} samples · seed ${j.seed}`;
|
|
220
|
+
out.innerHTML=j.samples.map((x,i)=>`<div class="samp${x.junk?" junk":""}">${x.junk?'<span class="tag">JUNK</span>':""}<b>${i+1}.</b> ${esc(x.text)}</div>`).join("");
|
|
221
|
+
LAST={ prompt:payload.prompt, mode:j.mode, recipe:j.recipe, curve:cfgJSON(), samples:j.samples };
|
|
222
|
+
document.getElementById("copyTextBtn").style.display="inline";
|
|
223
|
+
}catch(e){ st.textContent="cannot reach engine — is the server running? "+e.message; }
|
|
224
|
+
btn.disabled=false;
|
|
225
|
+
}
|
|
226
|
+
function esc(s){ return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
|
|
227
|
+
|
|
228
|
+
// ---- blind test: 1 sentence from the default recipe + 1 from the curve, unlabeled, guess which ----
|
|
229
|
+
async function postGen(payload){
|
|
230
|
+
const r=await fetch(engBase()+"/generate",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(payload)});
|
|
231
|
+
const j=await r.json(); if(!r.ok) throw new Error(j.error||r.status); return j;
|
|
232
|
+
}
|
|
233
|
+
let TESTST=null; // {prompt, opts:[A,B], curveSlot, guessed}
|
|
234
|
+
async function runTest(){
|
|
235
|
+
const tb=document.getElementById("testBtn"), st=document.getElementById("status"), area=document.getElementById("testArea");
|
|
236
|
+
if(!monotone(C)){ st.textContent="curve not monotone."; return; }
|
|
237
|
+
if(isIdentity(C)){ st.textContent="shape the curve first — identity IS the default, nothing to compare."; return; }
|
|
238
|
+
tb.disabled=true; st.textContent="testing (default vs curve)…"; area.innerHTML="";
|
|
239
|
+
const prompt=document.getElementById("prompt").value, fld=+document.getElementById("seed").value||0;
|
|
240
|
+
// Fresh INDEPENDENT seeds each run, so repeated Tests give a new pair (not the same
|
|
241
|
+
// two sentences with their A/B position flipped). Combined with the coin flip below
|
|
242
|
+
// this makes the randomness self-evident — no fixed "curve sentence" to track.
|
|
243
|
+
const seedD=fld+Math.floor(Math.random()*1e6), seedC=fld+Math.floor(Math.random()*1e6);
|
|
244
|
+
try{
|
|
245
|
+
const [d,c]=await Promise.all([
|
|
246
|
+
postGen({prompt,default:true,n:1,seed:seedD,max_tokens:60}),
|
|
247
|
+
postGen({prompt,curve:JSON.parse(cfgJSON()),n:1,seed:seedC,max_tokens:60}),
|
|
248
|
+
]);
|
|
249
|
+
const dText=d.samples[0].text, cText=c.samples[0].text, curveFirst=Math.random()<0.5;
|
|
250
|
+
TESTST={ prompt, opts:[ curveFirst?cText:dText, curveFirst?dText:cText ], curveSlot: curveFirst?0:1, guessed:false };
|
|
251
|
+
renderTestCard(); st.textContent="blind pair ready — which is your curve?";
|
|
252
|
+
}catch(e){ st.textContent="test failed: "+e.message; }
|
|
253
|
+
tb.disabled=false;
|
|
254
|
+
}
|
|
255
|
+
function renderTestCard(){
|
|
256
|
+
const area=document.getElementById("testArea"), T=TESTST;
|
|
257
|
+
area.innerHTML=`<div class="testcard">
|
|
258
|
+
<div class="opt" data-s="0"><span class="lab">A</span><span>${esc(T.opts[0])}</span><span class="reveal" id="rv0"></span></div>
|
|
259
|
+
<div class="opt" data-s="1"><span class="lab">B</span><span>${esc(T.opts[1])}</span><span class="reveal" id="rv1"></span></div>
|
|
260
|
+
<div class="q">Which came from <b>your curve</b>? (the other is the model's default recipe)</div>
|
|
261
|
+
<div style="display:flex;gap:8px"><button class="ghost" data-g="0">It's A</button><button class="ghost" data-g="1">It's B</button><button class="ghost" id="copyPairBtn" style="margin-left:auto">Copy pair (blind)</button></div>
|
|
262
|
+
<div class="verdict" id="verdict"></div></div>`;
|
|
263
|
+
area.querySelectorAll("button[data-g]").forEach(b=>b.addEventListener("click",()=>guess(+b.dataset.g)));
|
|
264
|
+
area.querySelector("#copyPairBtn").addEventListener("click",copyPair);
|
|
265
|
+
}
|
|
266
|
+
function guess(slot){
|
|
267
|
+
const T=TESTST; if(!T||T.guessed)return; T.guessed=true;
|
|
268
|
+
document.getElementById("rv"+T.curveSlot).innerHTML='<span style="color:var(--accent2)">← your curve</span>';
|
|
269
|
+
document.getElementById("rv"+(1-T.curveSlot)).innerHTML='<span style="color:var(--text3)">← model default</span>';
|
|
270
|
+
document.querySelectorAll(".testcard .opt")[slot].classList.add("picked");
|
|
271
|
+
document.getElementById("verdict").innerHTML = slot===T.curveSlot
|
|
272
|
+
? '<span style="color:var(--pos)">✓ Correct — you picked your curve.</span>'
|
|
273
|
+
: '<span style="color:var(--danger)">✗ Nope — that was the model default; your curve was '+(T.curveSlot===0?"A":"B")+'.</span>';
|
|
274
|
+
}
|
|
275
|
+
function copyPair(){
|
|
276
|
+
const T=TESTST; if(!T)return;
|
|
277
|
+
const txt="Prompt: "+T.prompt+"\n\nA: "+T.opts[0]+"\nB: "+T.opts[1]+"\n\n(one is a custom log-prob sampling curve, the other the model's default recipe — which is which?)";
|
|
278
|
+
const v=document.getElementById("verdict"), prev=v.innerHTML, done=()=>{v.innerHTML='<span style="color:var(--pos)">pair copied ✓ (blind — paste to a judge)</span>';setTimeout(()=>{v.innerHTML=prev},1700);};
|
|
279
|
+
if(navigator.clipboard)navigator.clipboard.writeText(txt).then(done,done); else { const ta=document.createElement("textarea"); ta.value=txt; document.body.appendChild(ta); ta.select(); try{document.execCommand("copy")}catch(_){}; ta.remove(); done(); }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---- wiring ----
|
|
283
|
+
document.getElementById("engine").value=ENGINE_BASE+"/generate";
|
|
284
|
+
if(location.protocol.startsWith("http")) document.getElementById("backLink").style.display="inline";
|
|
285
|
+
document.getElementById("pchips").innerHTML=PROMPTS.map((p,i)=>`<button data-i="${i}">${p.slice(0,38)}…</button>`).join("");
|
|
286
|
+
document.getElementById("pchips").querySelectorAll("button").forEach(b=>b.addEventListener("click",()=>{document.getElementById("prompt").value=PROMPTS[+b.dataset.i];clearGeneratedState();loadSignal();}));
|
|
287
|
+
document.getElementById("genBtn").addEventListener("click",generate);
|
|
288
|
+
document.getElementById("testBtn").addEventListener("click",runTest);
|
|
289
|
+
document.getElementById("sigBtn").addEventListener("click",loadSignal);
|
|
290
|
+
document.getElementById("copyTextBtn").addEventListener("click",()=>{ if(!LAST)return;
|
|
291
|
+
const sampler = LAST.mode==="default"&&LAST.recipe ? "Sampler: model default recipe (T="+LAST.recipe.temperature+", top-p="+LAST.recipe.topP+", top-k="+LAST.recipe.topK+")" : "Curve: "+LAST.curve;
|
|
292
|
+
const txt="Prompt: "+LAST.prompt+"\n"+sampler+"\n\n"+LAST.samples.map((s,i)=>(i+1)+". "+(s.junk?"[JUNK] ":"")+s.text).join("\n\n");
|
|
293
|
+
const st=document.getElementById("status"), done=()=>{st.textContent="text copied ✓";setTimeout(()=>{if(st.textContent==="text copied ✓")st.textContent=""},1500);};
|
|
294
|
+
if(navigator.clipboard)navigator.clipboard.writeText(txt).then(done,done); else { const ta=document.createElement("textarea"); ta.value=txt; document.body.appendChild(ta); ta.select(); try{document.execCommand("copy")}catch(_){}; ta.remove(); done(); } });
|
|
295
|
+
document.getElementById("resetBtn").addEventListener("click",()=>{C.points=identityPoints();draw();clearGeneratedState();});
|
|
296
|
+
document.getElementById("copyBtn").addEventListener("click",()=>{ const f=document.getElementById("cfgField"); f.select();
|
|
297
|
+
const ok=document.getElementById("copyOk"), done=()=>{ok.textContent="Copied!";setTimeout(()=>ok.textContent="",1500);};
|
|
298
|
+
if(navigator.clipboard)navigator.clipboard.writeText(f.value).then(done,()=>{document.execCommand("copy");done();}); else {document.execCommand("copy");done();} });
|
|
299
|
+
document.getElementById("engine").addEventListener("change",()=>{clearGeneratedState();probe();loadSignal();});
|
|
300
|
+
document.getElementById("prompt").addEventListener("change",()=>{clearGeneratedState();loadSignal();});
|
|
301
|
+
draw(); probe(); loadSignal();
|
|
302
|
+
</script>
|
|
303
|
+
</body></html>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Adapters end-to-end — notes / TODO (2026-06-16)
|
|
2
|
+
|
|
3
|
+
Working notes for wiring trained LoRA adapters through the whole stack: Pi (CLI +
|
|
4
|
+
web), the web Chat page UI, and the server's discovery surface. Captured from the
|
|
5
|
+
2026-06-16 session (serving verified working; pi-extension mechanism confirmed from
|
|
6
|
+
a pi session example).
|
|
7
|
+
|
|
8
|
+
## The mental model — three distinct states
|
|
9
|
+
|
|
10
|
+
Josh's framing, which the whole design should keep separate:
|
|
11
|
+
|
|
12
|
+
1. **Available** — adapters that exist *on disk* and are *compatible* with the
|
|
13
|
+
currently-served model. Not in memory, cost nothing. (Discovery problem.)
|
|
14
|
+
2. **Loaded / mounted** — adapters mounted into the *running server* via
|
|
15
|
+
`AdapterManager`; they cost memory. (`POST`/`DELETE /v1/adapters`.)
|
|
16
|
+
3. **Selected for this request** — the `adapter` field on a given request:
|
|
17
|
+
`"none"` (default) | `"<id>"` | `"a+b"` (stacked). (`resolveSpec`.)
|
|
18
|
+
|
|
19
|
+
You can be available-but-not-loaded, loaded-but-not-selected, etc. The UI and the
|
|
20
|
+
API should let you move an adapter between these states explicitly.
|
|
21
|
+
|
|
22
|
+
## What already exists (server side)
|
|
23
|
+
|
|
24
|
+
- `GET /v1/adapters` → lists **loaded** adapters (`id, path, rank, scale, size_bytes, mounted_layers`). `src/server.ts:1086`
|
|
25
|
+
- `POST /v1/adapters {id, path}` → **load/mount**. `src/server.ts:1094`
|
|
26
|
+
- `DELETE /v1/adapters/:id` → **unload/unmount**. `src/server.ts:1113`
|
|
27
|
+
- Request `adapter` field → `resolveSpec` (`src/lora.ts:231`): `"none"`/empty → `[]`
|
|
28
|
+
(genuine base); unknown id → **throws** (loud, not a silent base fallback);
|
|
29
|
+
`"a+b"` stacks. Default behavior is already "none".
|
|
30
|
+
|
|
31
|
+
So **loaded** and **selected** are done. The gap is **available** (discovery) plus
|
|
32
|
+
the two front-ends (Pi extension, web Chat selector).
|
|
33
|
+
|
|
34
|
+
## TODO
|
|
35
|
+
|
|
36
|
+
### A. Pi CLI/terminal — adapter via extension *(default none)*
|
|
37
|
+
|
|
38
|
+
Mechanism (confirmed from the pi docs in-session): a Pi extension using the
|
|
39
|
+
`before_provider_request` hook to inject the field into the outgoing
|
|
40
|
+
OpenAI-compatible payload. `models.json` can't add arbitrary body fields — the hook
|
|
41
|
+
is the right tool.
|
|
42
|
+
|
|
43
|
+
Create `~/.pi/agent/extensions/mlx-bun-adapter.ts`:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
47
|
+
|
|
48
|
+
export default function (pi: ExtensionAPI) {
|
|
49
|
+
pi.on("before_provider_request", (event) => {
|
|
50
|
+
const payload = event.payload as Record<string, unknown>;
|
|
51
|
+
if (payload.model !== "local") return; // only our local alias
|
|
52
|
+
const adapter = process.env.PI_MLX_ADAPTER; // unset → default none
|
|
53
|
+
if (!adapter) return; // no field → server serves base
|
|
54
|
+
return { ...payload, adapter };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- Defaults to **none** (env unset → hook returns `undefined` → payload unchanged).
|
|
60
|
+
- Selection via `PI_MLX_ADAPTER=chunk pi --provider mlx-bun --model local -p "..."`
|
|
61
|
+
(no native `-p --adapter` flag; unknown CLI flags get rejected). Optional shell
|
|
62
|
+
helper: `pi-chunk() { PI_MLX_ADAPTER=chunk pi --provider mlx-bun --model local -p "$@"; }`.
|
|
63
|
+
- Put it in the global `~/.pi/agent/extensions/` dir so it loads for non-interactive
|
|
64
|
+
`-p` too. `/reload` to pick it up.
|
|
65
|
+
- **Ship this** as part of mlx-bun (a copy in-repo + a one-liner installer), so users
|
|
66
|
+
don't hand-write it. Consider also reading the *selected* adapter from a small
|
|
67
|
+
state file the web UI / a `mlx-bun adapter use <id>` command writes, so CLI and web
|
|
68
|
+
share one selection.
|
|
69
|
+
|
|
70
|
+
### B. Web Chat page — selector + load/unload *(default none)*
|
|
71
|
+
|
|
72
|
+
Path: `src/web/app.html` (UI) ↔ `/ws/chat` (`src/pi-web.ts`) ↔ in-process Pi ↔ model.
|
|
73
|
+
|
|
74
|
+
- **Selector** in `app.html`: a dropdown of *available* adapters with **"none"
|
|
75
|
+
selected by default**. Changing it sets the active selection for subsequent
|
|
76
|
+
messages. Allow multi-select → `"a+b"` (resolveSpec already supports stacking).
|
|
77
|
+
- **Load / unload controls**: buttons that call `POST /v1/adapters` (load) and
|
|
78
|
+
`DELETE /v1/adapters/:id` (unload). Show each adapter's state (available / loaded /
|
|
79
|
+
selected) so the three concepts are visible. An adapter must be **loaded** before
|
|
80
|
+
it can be **selected** (selecting an unloaded id errors) — UI should either
|
|
81
|
+
auto-load on select or gate selection to loaded ones.
|
|
82
|
+
- **Inject via the `before_provider_request` hook — the primary mechanism, identical
|
|
83
|
+
to the CLI (§A).** `pi-web.ts` ALREADY registers an inline extension this way
|
|
84
|
+
(`pi.on("tool_call", …)` wired into `createAgentSession`, pi-web.ts:469/605), so
|
|
85
|
+
`pi.on("before_provider_request", (e) => ({ ...e.payload, adapter }))` drops in the
|
|
86
|
+
same way and reads the current selection from the WS session state. **No custom
|
|
87
|
+
provider plumbing, no new model-call wiring.** The hook is the spine for BOTH UIs.
|
|
88
|
+
- **Selection → hook**: the dropdown just updates the session's selected-adapter value
|
|
89
|
+
that the hook reads — a tiny client→server message (or reuse the `set_thinking`-style
|
|
90
|
+
toggle path), NOT a heavyweight protocol. Default none → hook returns the payload
|
|
91
|
+
unchanged → base model.
|
|
92
|
+
- **Mounting is the only thing the hook can't do**: the injected id must be mounted
|
|
93
|
+
server-side or `resolveSpec` rejects it. Auto-load-on-select handles it — selecting
|
|
94
|
+
fires one `POST /v1/adapters`, then the hook injects the id. Discovery (§C) and a
|
|
95
|
+
richer protocol are optional polish; the dropdown can start from `GET /v1/adapters`.
|
|
96
|
+
|
|
97
|
+
### C. Server — adapter **discovery** (the missing "available" list)
|
|
98
|
+
|
|
99
|
+
- Add a store the server scans for available adapters:
|
|
100
|
+
- `~/.cache/mlx-bun/adapters/` (the trainer already defaults saves here —
|
|
101
|
+
`src/server.ts:1653`) and/or a **sidecar next to the model dir**.
|
|
102
|
+
- New endpoint, e.g. `GET /v1/adapters/available` →
|
|
103
|
+
`[{ id, path, source_repo, rank, scale, compatible }]`, where each entry is read
|
|
104
|
+
from the adapter's `optiq_lora_config.json` (`source_model`, `rank`, `scale`).
|
|
105
|
+
- **Filter by the served model.** Today `source_model` is a *machine/snapshot-specific
|
|
106
|
+
path* (`/Users/joshrossi/.cache/huggingface/.../snapshots/<hash>`), which won't
|
|
107
|
+
match across machines/snapshots. Key compatibility on the **repo id**
|
|
108
|
+
(`mlx-community/MiniCPM5-1B-OptiQ-4bit`) instead — see "repo-id keying" below. The
|
|
109
|
+
server already tracks `repoId` per model; `compatible = adapter.source_repo === served.repoId`.
|
|
110
|
+
|
|
111
|
+
### D. Overnight training from the web UI
|
|
112
|
+
|
|
113
|
+
Goal (Josh): *"start a new training session before bed, wake up to a new adapter."*
|
|
114
|
+
Training must be fully drivable from the web UI — not just a script.
|
|
115
|
+
|
|
116
|
+
- A **training page / panel** in the web UI: pick base model + dataset, set params
|
|
117
|
+
(rank / scale / num_layers / LR / dropout / iters / seq / segmentSize — the
|
|
118
|
+
configurable-params task, mlx-lm defaults), **start**, and leave it running.
|
|
119
|
+
- **Monitor**: live loss / iter / ETA, and survives the browser closing (the run is
|
|
120
|
+
server-side; the page reconnects). `POST /v1/fit` already exists
|
|
121
|
+
(`src/server.ts:1652`) — needs a status/stream endpoint + a UI.
|
|
122
|
+
- **Completion**: the finished adapter lands in the store and shows up as *available*
|
|
123
|
+
(state A in the three-state model), ready to load/select or auto-serve.
|
|
124
|
+
- This closes the loop with the chunk-task roadmap: iterate data → train overnight →
|
|
125
|
+
wake to a scored adapter.
|
|
126
|
+
|
|
127
|
+
### E. `--force-adapter` for the nightly serving pipeline
|
|
128
|
+
|
|
129
|
+
Goal (Josh): spin up a server and say *"use this model AND this adapter"* without
|
|
130
|
+
per-request `adapter` fields — for an unattended nightly pipeline.
|
|
131
|
+
|
|
132
|
+
- New CLI flag, e.g. `mlx-bun serve <model> --force-adapter <id|path>`:
|
|
133
|
+
1. **mount** the adapter at startup (load it), and
|
|
134
|
+
2. **force it active as the default** for every request — overriding the normal
|
|
135
|
+
"selected defaults to none." A request could still override, or `--force-adapter`
|
|
136
|
+
could be a hard pin (decide; pin is simpler for a pipeline).
|
|
137
|
+
- Surface it on the ready card's `Perf`/serving summary so it's visible the server is
|
|
138
|
+
adapter-pinned.
|
|
139
|
+
- Pairs with D: train overnight via the UI → serve the result with `--force-adapter`
|
|
140
|
+
in the nightly pipeline.
|
|
141
|
+
|
|
142
|
+
## Cross-cutting / already-open threads to fold in
|
|
143
|
+
|
|
144
|
+
- **Repo-id keying for adapters** (from this session's discovery check): adapters
|
|
145
|
+
currently record the model only as a fragile absolute path (`source_model` /
|
|
146
|
+
`base_model_name_or_path`). Have the **trainer write the repo id** into the adapter
|
|
147
|
+
config at save time, and match discovery on repo id. Robust across machines and
|
|
148
|
+
snapshots. This is the prerequisite for B's selector and C's `compatible` filter.
|
|
149
|
+
- **mlx-lm-aligned training defaults** (PARTIALLY DONE): `rank / scale /
|
|
150
|
+
learning_rate` (plus `seq / segmentSize / iters / save_checkpoints`) are now
|
|
151
|
+
run-configurable via env on the chunk-finetune script, threaded
|
|
152
|
+
`chunk-finetune.ts` → `FinetuneSubmit` → `cfg`. In particular **`scale` is now
|
|
153
|
+
wired end-to-end** (`SCALE` env → submit `scale` → `cfg.scale` →
|
|
154
|
+
`buildTrainableLora`), and the script defaults to the **mlx-lm-aligned
|
|
155
|
+
`scale 20 / LR 1e-5`** (== effective `scale 1 / LR 2e-4`). STILL PENDING:
|
|
156
|
+
surface `num_layers` on the script (already in the submit contract +
|
|
157
|
+
`TrainConfig`, just no env) and add `dropout` (not in `TrainConfig` yet).
|
|
158
|
+
Note `DEFAULT_TRAIN_CONFIG` (`src/train/trainer.ts`) stays the conservative
|
|
159
|
+
library default (`rank 8 / scale 1 / LR 2e-4`); the *script* overrides to the
|
|
160
|
+
mlx-lm pair. (Older `minicpm5-chunk-segmented` predates this: rank 16 / scale 1
|
|
161
|
+
/ LR 2e-4.) Flow through
|
|
162
|
+
`DEFAULT_TRAIN_CONFIG` (`src/train/trainer.ts`) → `FinetuneSubmit`/`parseConfig`
|
|
163
|
+
(`src/train/job.ts`) → the `"finetune"` subprocess job (`src/server.ts:1678`,
|
|
164
|
+
`submitSubprocess(store, "finetune", …)`). NOTE: `GET /fit` is NOT the training
|
|
165
|
+
submit — it is the memory-fit solver (`src/fit.ts`): it solves weights + KV
|
|
166
|
+
growth + prefill against this machine's usable memory and returns
|
|
167
|
+
`{ fits, max_safe_context, predicted_decode_tps }` + a SKU matrix (used by
|
|
168
|
+
`GET /fit`, serve-time admission, and the model listing).
|
|
169
|
+
- **Adapter storage location**: prefer a dedicated `~/.cache/mlx-bun/adapters/` store
|
|
170
|
+
with repo-id metadata over writing sidecars into the fragile HF snapshot cache.
|
|
171
|
+
|
|
172
|
+
## Inventory (scanned 2026-06-16)
|
|
173
|
+
|
|
174
|
+
5 adapters on this machine, **all local — none in the HF cache** (no LoRA repos
|
|
175
|
+
downloaded, no `HF_HOME`/`HF_HUB_CACHE` override):
|
|
176
|
+
|
|
177
|
+
- **3 production** in `~/.cache/mlx-bun-finetunes/`: `minicpm5-chunk-final`,
|
|
178
|
+
`minicpm5-chunk-segmented`, `minicpm5-chunk-seq8192` — all
|
|
179
|
+
`mlx-community/MiniCPM5-1B-OptiQ-4bit`, rank 16 / scale 1 / all layers / 44.9 MB.
|
|
180
|
+
- **2 test fixtures** in `fixtures/adapters/`: `french`, `upper` (num_layers 4,
|
|
181
|
+
7 MB, **no `source_model` recorded** — model-agnostic).
|
|
182
|
+
|
|
183
|
+
Design implications confirmed by the scan:
|
|
184
|
+
|
|
185
|
+
1. **HF cache is empty of adapters today** but the discovery scanner should still
|
|
186
|
+
cover `~/.cache/huggingface/hub` (future LoRA pulls land there).
|
|
187
|
+
2. **`repo_id` / `source_repo` is absent in every real adapter** — only the
|
|
188
|
+
snapshot *path* is stored. Repo-id keying must come first, plus a one-time
|
|
189
|
+
backfill to stamp the existing three.
|
|
190
|
+
3. **The three MiniCPM5 adapters are indistinguishable by config** (same model /
|
|
191
|
+
rank / scale / layers) — only the dir name differs. Discovery must surface the
|
|
192
|
+
dir name as the id and ideally richer metadata (task, data, date) so the selector
|
|
193
|
+
isn't three identical rows.
|
|
194
|
+
|
|
195
|
+
## Defaults & invariants to lock everywhere
|
|
196
|
+
|
|
197
|
+
- Selected adapter defaults to **none** (Pi extension, web UI, server) — base model.
|
|
198
|
+
- Unknown/typo'd id → **loud error** (already true server-side; surface it in the UI,
|
|
199
|
+
don't silently fall back to base).
|
|
200
|
+
- Stacking (`"a+b"`) is supported end-to-end; UI can expose it as multi-select.
|
|
201
|
+
- An adapter must be **loaded** before it can be **selected**.
|