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,224 @@
|
|
|
1
|
+
# Server API
|
|
2
|
+
|
|
3
|
+
`mlx-bun serve` (or `bun scripts/serve.ts`) exposes an OpenAI-compatible
|
|
4
|
+
HTTP API on one model. The request's `model` field is ignored; the
|
|
5
|
+
loaded model's id is echoed back. By default generation is serialized
|
|
6
|
+
through a single queue (one GPU, batch = 1); `--batch N` switches the
|
|
7
|
+
server into bf16 continuous batching.
|
|
8
|
+
|
|
9
|
+
For the full set of start flags (`--port`, `--memory-budget`,
|
|
10
|
+
`--prompt-cache`, `--batch`, `--kv-quant`, `--thinking`, sampling
|
|
11
|
+
defaults, and the perf levers) and a compatibility matrix of which
|
|
12
|
+
combinations compose, see [server-config.md](./server-config.md). This
|
|
13
|
+
doc covers the request/response wire format. LoRA adapters are mounted at
|
|
14
|
+
runtime via `POST /v1/adapters` (below), not a start flag.
|
|
15
|
+
|
|
16
|
+
## POST /v1/chat/completions
|
|
17
|
+
|
|
18
|
+
Request body (OpenAI chat schema; unknown fields ignored):
|
|
19
|
+
|
|
20
|
+
```jsonc
|
|
21
|
+
{
|
|
22
|
+
"messages": [ /* role: system | user | assistant | tool */ ],
|
|
23
|
+
"stream": false,
|
|
24
|
+
"max_tokens": 1024, // or max_completion_tokens (wins)
|
|
25
|
+
"temperature": 0.7, // 0 = greedy
|
|
26
|
+
"top_p": 0, "top_k": 0, // 0 = off
|
|
27
|
+
"seed": 1234, // omit for time-derived
|
|
28
|
+
"repetition_penalty": 1.1, // optional
|
|
29
|
+
"stop": "\n\n", // or ["###", "\n\n"] (spec: up to 4)
|
|
30
|
+
"tools": [ /* OpenAI function tools */ ],
|
|
31
|
+
"tool_choice": "auto", // "none" disables tools
|
|
32
|
+
"chat_template_kwargs": { // forwarded to the chat template
|
|
33
|
+
"enable_thinking": false // MiniCPM5: <think> channel on/off
|
|
34
|
+
},
|
|
35
|
+
"adapter": "id" // LoRA: "id", stacked "a+b", or "none"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Sampling defaults follow the model author's `generation_config.json`
|
|
40
|
+
when a field is omitted (optiq serve's gen_config behavior); explicit
|
|
41
|
+
request values always win. MiniCPM5 defaults to the no-think direct
|
|
42
|
+
answer mode unless `chat_template_kwargs.enable_thinking` is `true`.
|
|
43
|
+
|
|
44
|
+
`stop` sequences are matched on **decoded text**, not token ids, so a
|
|
45
|
+
sequence that spans token boundaries still fires. Generation halts at
|
|
46
|
+
the first match; the stop sequence itself is excluded from the content
|
|
47
|
+
and `finish_reason` is `"stop"`.
|
|
48
|
+
|
|
49
|
+
Message `content` is a string or an array of parts:
|
|
50
|
+
`{ "type": "text", "text": ... }` and
|
|
51
|
+
`{ "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }`
|
|
52
|
+
(http/https URLs also accepted; PNG, JPEG, HEIC, AVIF, WebP, TIFF, GIF,
|
|
53
|
+
BMP via native OS codecs; requires a model with the vision sidecar).
|
|
54
|
+
|
|
55
|
+
Non-streaming response:
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"id": "chatcmpl-…", "object": "chat.completion", "created": 1760000000,
|
|
60
|
+
"model": "<loaded model id>",
|
|
61
|
+
"choices": [{
|
|
62
|
+
"index": 0,
|
|
63
|
+
"message": {
|
|
64
|
+
"role": "assistant",
|
|
65
|
+
"content": "…", // "" when only tool calls
|
|
66
|
+
"tool_calls": [{ // present when the model called tools
|
|
67
|
+
"id": "call_…", "type": "function",
|
|
68
|
+
"function": { "name": "…", "arguments": "{…json…}" }
|
|
69
|
+
}]
|
|
70
|
+
},
|
|
71
|
+
"finish_reason": "stop" | "length" | "tool_calls"
|
|
72
|
+
}],
|
|
73
|
+
"usage": {
|
|
74
|
+
"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
75
|
+
"prompt_tokens_details": { "cached_tokens": 0 } // prompt-cache reuse
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Streaming (`"stream": true`) is SSE: `data: <chunk>\n\n` per event,
|
|
81
|
+
terminated by `data: "[DONE]"`. Chunks are `chat.completion.chunk`
|
|
82
|
+
objects whose `choices[0].delta` carries `{role}`, then `{content}`
|
|
83
|
+
increments (multi-byte sequences are held back until decodable; text
|
|
84
|
+
that could begin a `stop` sequence is held back until disambiguated,
|
|
85
|
+
so no part of a stop sequence is ever streamed), then
|
|
86
|
+
for tool calls a final `{tool_calls: [{index, id, type, function}]}`
|
|
87
|
+
delta; the last chunk carries `finish_reason` and `usage`.
|
|
88
|
+
|
|
89
|
+
Tool round-trip: send the assistant message with its `tool_calls` back,
|
|
90
|
+
followed by `{ "role": "tool", "tool_call_id": …, "content": … }`
|
|
91
|
+
messages; multi-turn prompt prefixes reuse the KV prompt cache
|
|
92
|
+
automatically.
|
|
93
|
+
|
|
94
|
+
Tool-call parsing is per model family. Gemma 4 uses its native
|
|
95
|
+
`<|tool_call>`…`<tool_call|>` sentinel tokens. MiniCPM5 emits XML in
|
|
96
|
+
decoded text (`<function name="…"><param name="…">…`, CDATA-wrapped
|
|
97
|
+
values supported); content before the tool markup still streams live,
|
|
98
|
+
only the markup is withheld and converted to `tool_calls`. Argument
|
|
99
|
+
values are decoded against the tool's JSON schema (string-typed params
|
|
100
|
+
stay strings); markup that fails to parse falls back to plain content.
|
|
101
|
+
|
|
102
|
+
### Errors
|
|
103
|
+
|
|
104
|
+
All errors are `{ "error": { "message": …, ... } }`.
|
|
105
|
+
|
|
106
|
+
- `400` — malformed JSON, empty `messages`, unknown adapter id, vision
|
|
107
|
+
request on a model without a sidecar, prompt build failures.
|
|
108
|
+
- `400` with `"type": "memory_admission"`, `"code":
|
|
109
|
+
"context_over_budget"` — `prompt + max_tokens` exceeds the memory
|
|
110
|
+
budget's max safe context (only when serving with `--memory-budget`;
|
|
111
|
+
the GPU OOM this prevents would kill the process, so it is refused
|
|
112
|
+
up front). Lower `max_tokens` or shorten the prompt; the ceiling is
|
|
113
|
+
visible at `/stats`.
|
|
114
|
+
|
|
115
|
+
## POST /v1/messages (Anthropic Messages API)
|
|
116
|
+
|
|
117
|
+
Anthropic-protocol surface over the same engine — on by default, like
|
|
118
|
+
`optiq serve`. Point any Anthropic-SDK tool at the server
|
|
119
|
+
(`ANTHROPIC_BASE_URL=http://localhost:8090`, any `x-api-key`) — Claude
|
|
120
|
+
Code works as a client this way.
|
|
121
|
+
|
|
122
|
+
- `system` (string or text blocks), `messages` with string or
|
|
123
|
+
content-block arrays; `tool_use` / `tool_result` blocks map to the
|
|
124
|
+
native gemma tool-calling path (better than the optiq shim, which
|
|
125
|
+
inlines them as text); `image` blocks (base64 or url source) hit the
|
|
126
|
+
vision path on sidecar models.
|
|
127
|
+
- `tools` (`{name, description, input_schema}`) map to function tools;
|
|
128
|
+
server-tool types (web_search, …) are dropped silently.
|
|
129
|
+
- `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences`,
|
|
130
|
+
`stream` as in the Anthropic spec.
|
|
131
|
+
- Response: `{id: "msg_…", type: "message", content: [{type: "text"} |
|
|
132
|
+
{type: "tool_use"}…], stop_reason, usage: {input_tokens,
|
|
133
|
+
output_tokens, cache_read_input_tokens}}` —
|
|
134
|
+
`cache_read_input_tokens` comes from the prompt cache.
|
|
135
|
+
- Streaming follows the Anthropic event grammar exactly:
|
|
136
|
+
`message_start → content_block_start/delta/stop (text_delta,
|
|
137
|
+
input_json_delta) → message_delta (stop_reason + usage) →
|
|
138
|
+
message_stop`. Errors are `event: error` frames.
|
|
139
|
+
- Errors: `{type: "error", error: {type: "invalid_request_error" |
|
|
140
|
+
"api_error", message}}`.
|
|
141
|
+
|
|
142
|
+
## POST /v1/responses (OpenAI Responses API)
|
|
143
|
+
|
|
144
|
+
Responses-protocol surface (Codex, Cursor, Continue, Cline, and the
|
|
145
|
+
OpenAI SDK speak this now). Oracle: optiq responses shim.
|
|
146
|
+
|
|
147
|
+
- `input` (string or item array: `message`, `function_call`,
|
|
148
|
+
`function_call_output`), `instructions` (merged with any
|
|
149
|
+
system/developer items into one leading system message),
|
|
150
|
+
`max_output_tokens`, `temperature`, `top_p`, `top_k`, flat
|
|
151
|
+
`tools`/`tool_choice` (built-in tool types dropped), `stream`.
|
|
152
|
+
- **`previous_response_id` resumption**: pass a prior response id
|
|
153
|
+
instead of resending the conversation; the server splices the stored
|
|
154
|
+
input + output back in (instructions carry forward when omitted).
|
|
155
|
+
Store is per-process, 1 h TTL, 32 MiB byte-capped LRU — observable
|
|
156
|
+
at `GET /stats` (`response_store`). Unknown/expired id → 404.
|
|
157
|
+
- Response: `{id: "resp_…", object: "response", status: "completed" |
|
|
158
|
+
"incomplete", output: [{type: "message"|"function_call"…}], usage}`.
|
|
159
|
+
- Streaming event chain: `response.created → response.in_progress →
|
|
160
|
+
response.output_item.added → response.content_part.added →
|
|
161
|
+
response.output_text.delta… → response.output_text.done →
|
|
162
|
+
response.content_part.done → response.output_item.done →
|
|
163
|
+
response.completed` (+ `response.function_call_arguments.delta/.done`
|
|
164
|
+
for tool calls).
|
|
165
|
+
|
|
166
|
+
## GET /v1/models
|
|
167
|
+
|
|
168
|
+
`{ "object": "list", "data": [{ "id": "<model id>", "object": "model", … }] }`
|
|
169
|
+
|
|
170
|
+
## GET /stats
|
|
171
|
+
|
|
172
|
+
```jsonc
|
|
173
|
+
{
|
|
174
|
+
"server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
|
|
175
|
+
"prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0 },
|
|
176
|
+
"response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
|
|
177
|
+
"kv_quant": { "mode": "mixed (kv_config.json)" | "uniform-kv8" | "bf16",
|
|
178
|
+
"layers": { "kv4": 8, "bf16": 40 },
|
|
179
|
+
"attention": { "global": 10, "sliding_window": 38 } },
|
|
180
|
+
"admission": {
|
|
181
|
+
"max_safe_context": 0, // tokens; requests above this 400
|
|
182
|
+
"memory_budget_bytes": null, // explicit budget, or null (machine default)
|
|
183
|
+
"usable_bytes": 0,
|
|
184
|
+
"weights_bytes": 0
|
|
185
|
+
},
|
|
186
|
+
"batch": {
|
|
187
|
+
"configured": 1, // the --batch N value
|
|
188
|
+
"batched": false, // batching enabled (N>1)
|
|
189
|
+
"active_rows": 0 // rows currently decoding in the batch
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Adapters (LoRA hot-swap)
|
|
195
|
+
|
|
196
|
+
- `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes, mounted_layers }] }`
|
|
197
|
+
- `POST /v1/adapters` — `{ "id": "...", "path": "/dir" }`; mounts
|
|
198
|
+
through the generation queue (never races a forward pass). 400 on
|
|
199
|
+
shape/compat mismatch — validation is all-or-nothing.
|
|
200
|
+
- `DELETE /v1/adapters/<id>` — unmount; 404 if not mounted.
|
|
201
|
+
|
|
202
|
+
Select per request with the `adapter` body field. Prompt-cache entries
|
|
203
|
+
are namespaced per adapter spec, so switching adapters never reuses
|
|
204
|
+
another adapter's KV.
|
|
205
|
+
|
|
206
|
+
## Client setup: pi
|
|
207
|
+
|
|
208
|
+
`~/.pi/agent/models.json`:
|
|
209
|
+
|
|
210
|
+
```jsonc
|
|
211
|
+
{
|
|
212
|
+
"providers": {
|
|
213
|
+
"mlx-bun": {
|
|
214
|
+
"baseUrl": "http://localhost:8090/v1",
|
|
215
|
+
"api": "openai-completions",
|
|
216
|
+
"apiKey": "sk-anything-nonempty",
|
|
217
|
+
"models": [{ "id": "<model id from /v1/models>" }]
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Any OpenAI SDK works the same way: `baseURL: "http://localhost:8090/v1"`,
|
|
224
|
+
any non-empty `apiKey`.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Server configuration
|
|
2
|
+
|
|
3
|
+
Every start flag for `mlx-bun serve` (and `mlx-bun pi`, which shares the
|
|
4
|
+
same flag set), what each does, its default, and — the part that bites —
|
|
5
|
+
**which combinations compose and which don't**. For the request/response
|
|
6
|
+
wire format (chat/messages/responses bodies, SSE grammar, tool calling,
|
|
7
|
+
adapters admin) see [server-api.md](./server-api.md); this doc is about
|
|
8
|
+
*starting* the server.
|
|
9
|
+
|
|
10
|
+
The flags are parsed in `serverRuntimeFlags()`
|
|
11
|
+
([src/cli.ts](../../src/cli.ts)); the runtime knobs live in
|
|
12
|
+
`ServerOptions` ([src/server.ts](../../src/server.ts)). The two execution
|
|
13
|
+
lanes (serial vs. `--batch N`) and the lane picker are in
|
|
14
|
+
[src/serve/generation-gateway.ts](../../src/serve/generation-gateway.ts);
|
|
15
|
+
the design rationale is in
|
|
16
|
+
[docs/design/parallel-slots.md](../design/parallel-slots.md).
|
|
17
|
+
|
|
18
|
+
## Start flags
|
|
19
|
+
|
|
20
|
+
| Flag | Arg | Default | What it does |
|
|
21
|
+
| --- | --- | --- | --- |
|
|
22
|
+
| `--host` | addr | all interfaces | Interface to bind. `127.0.0.1` = loopback only. |
|
|
23
|
+
| `--port` | n | `8090` | Listen port. A pre-flight probe refuses to start if the port is already serving. |
|
|
24
|
+
| `--memory-budget` | GB | machine RAM × 0.75 | Admission ceiling. Requests whose `prompt + max_tokens` exceed the budget's max safe context are **rejected with 400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM. Also caps the mlx allocator (`mlx_set_memory_limit`) as defense in depth. **Decimal GB (×10⁹).** |
|
|
25
|
+
| `--prompt-cache` | GB | `2` (≈2e9 bytes) | Byte cap for the prompt (prefix-reuse KV) cache. **Binary GiB (×2³⁰)** on input. `0`/unset ⇒ default; there is no flag to disable the cache outright. |
|
|
26
|
+
| `--batch` | n | `1` (serial) | Max concurrent requests batched through the mlx-lm-parity engine. `>1` switches the **whole server** into bf16 continuous batching — a *mode*, not a load fallback. See [Execution modes](#execution-modes-serial-vs---batch-n). Alias: `--decode-concurrency` (mlx_lm.server drop-in). |
|
|
27
|
+
| `--kv-quant` | `config`\|`off`\|`4`\|`8` | `config` serial / `off`(bf16) under `--batch N` | KV-cache quantization. `config` = per-layer `kv_config.json` (optiq parity); `off` = bf16; `4`/`8` = uniform bits (group 64, start 0). Under `--batch N`, an explicit value routes those requests to the **serial** lane (batched is bf16-only). |
|
|
28
|
+
| `--thinking` | `true`\|`false` | model's own (false for CPM) | Server-wide default for the chat template's `enable_thinking` (MiniCPM5/CPM hybrid reasoning). A request's `chat_template_kwargs.enable_thinking` overrides it. |
|
|
29
|
+
| `--temperature` | n ∈ [0,5] | `generation_config.json` | Server-wide sampling default. Per-request `temperature` still wins; the browser chat (sends none) inherits this. |
|
|
30
|
+
| `--top-p` | n ∈ [0,1] | `generation_config.json` | Server-wide top-p default (per-request `top_p` wins). |
|
|
31
|
+
| `--top-k` | n ∈ [0,1e6] | `generation_config.json` | Server-wide top-k default (per-request `top_k` wins). |
|
|
32
|
+
| `--compiled-decode` | on\|off | on | Replay the per-step decode graph in C++ (`MLX_BUN_COMPILED_DECODE`). Bit-exact A/B lever. **Serial lane only** (see note below). |
|
|
33
|
+
| `--perf-kernel` | on\|off | **on** | Fused quantized-KV decode-SDPA Metal kernel (`MLX_BUN_PERF_KERNEL`), the perf side of the compat A/B. Engages on quantized caches at decode. **Serial lane only.** |
|
|
34
|
+
| `--fused-decode` | on\|off | off | Experimental: tile the quantized decode SDPA (`MLX_BUN_FUSED_DECODE`). **Serial lane only.** |
|
|
35
|
+
| `--fused-sdpa` | on\|off | on | Fused SDPA path for quantized prefill/continuation (inverted env `MLX_BUN_NO_FUSED_SDPA`). **Serial lane only.** |
|
|
36
|
+
| `--force-wire` | (bool) | off | Wire weights into memory for the whole generation (`MLX_BUN_FORCE_WIRE`). Near-ceiling models (e.g. 26B) need it. **Serial lane only.** |
|
|
37
|
+
|
|
38
|
+
The five performance levers are A/B knobs whose defaults are the measured
|
|
39
|
+
winners; flip them to compare. They are set as `MLX_BUN_*` env vars
|
|
40
|
+
before the model loads, so they apply to `mlx-bun pi` too. They affect
|
|
41
|
+
the **serial** decode path (`generate()`); the batched scheduler calls
|
|
42
|
+
`model.forwardHidden` directly and so is unaffected by all of them — see
|
|
43
|
+
[Levers that don't reach the batched lane](#levers-that-dont-reach-the-batched-lane).
|
|
44
|
+
|
|
45
|
+
> **Note — `--perf-kernel` default.** The code defaults it **on**
|
|
46
|
+
> (`perfKernelEnabled()` returns true unless `MLX_BUN_PERF_KERNEL=0`, and
|
|
47
|
+
> the ready card / `/stats` report it on). `STATUS.md` still lists a
|
|
48
|
+
> "default flip" as pending; treat that as stale — the engaged default
|
|
49
|
+
> today is on. (If the intent is genuinely off-until-clean-machine-pass,
|
|
50
|
+
> the *code* is what needs changing, not this doc.)
|
|
51
|
+
|
|
52
|
+
## Per-request overrides
|
|
53
|
+
|
|
54
|
+
Most quality knobs can be set per request in the chat body and override
|
|
55
|
+
the server-wide default. Precedence, highest first:
|
|
56
|
+
|
|
57
|
+
1. explicit request field (`temperature`, `top_p`, `top_k`, `seed`,
|
|
58
|
+
`repetition_penalty`, `max_tokens`/`max_completion_tokens`, `stop`,
|
|
59
|
+
`chat_template_kwargs.enable_thinking`, `adapter`, `tools`)
|
|
60
|
+
2. the matching `--temperature`/`--top-p`/`--top-k`/`--thinking` server default
|
|
61
|
+
3. the model's `generation_config.json`
|
|
62
|
+
4. the built-in fallback (temp 0.7, top-p/top-k 0 = off)
|
|
63
|
+
|
|
64
|
+
These ride all three protocol surfaces (`/v1/chat/completions`,
|
|
65
|
+
`/v1/messages`, `/v1/responses`) because each funnels through one
|
|
66
|
+
`handleChat`. Full field list: [server-api.md](./server-api.md).
|
|
67
|
+
|
|
68
|
+
## Environment levers
|
|
69
|
+
|
|
70
|
+
The CLI flags above set these; you can also export them directly (e.g.
|
|
71
|
+
for `bun scripts/serve.ts` or paired A/B harnesses). One has no CLI flag.
|
|
72
|
+
|
|
73
|
+
| Env var | CLI flag | Default | Effect |
|
|
74
|
+
| --- | --- | --- | --- |
|
|
75
|
+
| `MLX_BUN_COMPILED_DECODE` | `--compiled-decode` | on (`!=="0"`) | Compiled decode graph replay. |
|
|
76
|
+
| `MLX_BUN_PERF_KERNEL` | `--perf-kernel` | on (`!=="0"`) | Fused quantized-KV decode kernel (not bit-exact; perf A/B). |
|
|
77
|
+
| `MLX_BUN_FUSED_DECODE` | `--fused-decode` | off (`==="1"`) | Tile quantized decode SDPA. |
|
|
78
|
+
| `MLX_BUN_NO_FUSED_SDPA` | `--fused-sdpa` (inverted) | fused on | `=1` forces the stock unfused SDPA everywhere. |
|
|
79
|
+
| `MLX_BUN_FUSED_GELU` | *(none)* | on (`!=="0"`) | Fused GeGLU MLP kernel. Bit-exact, so it stays on both lanes; env-only opt-out. |
|
|
80
|
+
| `MLX_BUN_FORCE_WIRE` | `--force-wire` | off (`==="1"`) | Wire weights for the generation. |
|
|
81
|
+
|
|
82
|
+
## Execution modes: serial vs. `--batch N`
|
|
83
|
+
|
|
84
|
+
`--batch 1` (default) is the serialized single-queue path: one GPU, one
|
|
85
|
+
generation at a time, prompt-cache prefix reuse, mixed-precision KV by
|
|
86
|
+
default. Untouched and unconditional.
|
|
87
|
+
|
|
88
|
+
`--batch N` (N>1) opts the **whole server** into a continuous-batching
|
|
89
|
+
engine that is **bit-parity with `mlx_lm.server` at `B=N`**. It is a
|
|
90
|
+
*mode switch*, not a load-dependent fallback (results must not depend on
|
|
91
|
+
concurrency).
|
|
92
|
+
|
|
93
|
+
Parity is the guarantee, so the batched lane runs in **compat mode**: it
|
|
94
|
+
exposes **none** of the serial lane's perf flags and runs the plain
|
|
95
|
+
bit-exact forward — the same path mlx-lm runs, never the optional
|
|
96
|
+
parity-breaking kernels. This is the mechanism of the guarantee, not a
|
|
97
|
+
missing feature (see
|
|
98
|
+
[compat mode](#--batch-n-is-compat-mode--perf-flags-dont-apply-by-design)).
|
|
99
|
+
Because mlx-lm's batched path is bf16 (its quantized batching is NYI),
|
|
100
|
+
bf16 continuous batching **is** the drop-in:
|
|
101
|
+
|
|
102
|
+
- **KV quant unset ⇒ bf16** so the batch path engages out of the box
|
|
103
|
+
("Option B"). The serial default stays mixed-precision (optiq parity).
|
|
104
|
+
- **Explicit `--kv-quant config|4|8` ⇒** those requests route to the
|
|
105
|
+
serial lane (batched is bf16-only; a startup warning is printed). With
|
|
106
|
+
an explicit `--kv-quant`, *every* request carries a quant scheme, so
|
|
107
|
+
**nothing batches** — `--batch N --kv-quant config` is effectively
|
|
108
|
+
serial-with-quant. Omit `--kv-quant` to actually batch.
|
|
109
|
+
|
|
110
|
+
### The lane picker (`GenerationGateway.willBatch`)
|
|
111
|
+
|
|
112
|
+
Under `--batch N`, each request is routed per-request. It joins the
|
|
113
|
+
batch only if **all** of these hold; otherwise it drains the batch and
|
|
114
|
+
runs solo (mlx-lm's `_is_batchable` behavior). The two lanes are
|
|
115
|
+
mutually exclusive on the GPU (one `AsyncMutex`), so a serial-fallback
|
|
116
|
+
request never runs alongside a batched step — but batched requests run
|
|
117
|
+
concurrently with each other.
|
|
118
|
+
|
|
119
|
+
| Request property | Batches? |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| vision (image parts) | ❌ serial — needs offset-0 single-seq prefill + bidirectional image mask |
|
|
122
|
+
| LoRA `adapter` (resolves to ≥1) | ❌ serial — `loraState.active` is one per-generation field; per-row adapters unsupported |
|
|
123
|
+
| `repetition_penalty` | ❌ serial — per-row logits processors are a later refinement |
|
|
124
|
+
| explicit `seed` | ❌ serial — reproducibility ⇒ solo (matches mlx-lm) |
|
|
125
|
+
| KV quant active (explicit `--kv-quant`) | ❌ serial — batched is bf16-only in v1 |
|
|
126
|
+
| `temperature` / `top_p` / `top_k` | ✅ batches (each row samples with its own seed) |
|
|
127
|
+
| `stop` sequences | ✅ batches (per-row `StopMatcher` in the onToken closure) |
|
|
128
|
+
| `tools` / `tool_choice` | ✅ batches (per-row tool router; decode-layer parse) |
|
|
129
|
+
| `--thinking` / `enable_thinking` | ✅ batches (template-render concern, lane-independent) |
|
|
130
|
+
| multi-turn / long prompt | ✅ batches, but **no prompt-cache reuse** (`cached_tokens=0`) |
|
|
131
|
+
|
|
132
|
+
Both full-attention (CPM) and sliding-window (Gemma) models batch; the
|
|
133
|
+
scheduler assembles each layer's cache by attention type.
|
|
134
|
+
|
|
135
|
+
## Compatibility matrix
|
|
136
|
+
|
|
137
|
+
How each option behaves in each mode. "serial" = `--batch 1`; "`--batch N`"
|
|
138
|
+
= the batched mode (a given request may still take the serial *lane*
|
|
139
|
+
inside it per the table above).
|
|
140
|
+
|
|
141
|
+
| Option | serial (`--batch 1`) | `--batch N` (N>1) |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| `--kv-quant config`/`4`/`8` | ✅ applied to all requests | ⚠️ applied, but forces **all** requests to the serial lane (no batching) |
|
|
144
|
+
| `--kv-quant off` | ✅ bf16 | ✅ bf16 (same as the implicit batch default) |
|
|
145
|
+
| *(kv-quant unset)* | mixed-precision `config` | **bf16** (Option B) — incl. serial-lane fallback requests |
|
|
146
|
+
| `--memory-budget` | ✅ per-request admission | ✅ per-request admission — but **not aggregate** across rows (see limitations) |
|
|
147
|
+
| `--prompt-cache` | ✅ prefix reuse | ⚠️ bypassed for batched requests (`cached_tokens=0`); serial-lane requests still reuse |
|
|
148
|
+
| `--temperature`/`--top-p`/`--top-k` | ✅ | ✅ (per-row) |
|
|
149
|
+
| `--thinking` | ✅ | ✅ |
|
|
150
|
+
| vision request | ✅ | ✅ via serial lane (in bf16 under Option B) |
|
|
151
|
+
| LoRA `adapter` | ✅ | ✅ via serial lane |
|
|
152
|
+
| `repetition_penalty` | ✅ | ✅ via serial lane |
|
|
153
|
+
| `seed` | ✅ | ✅ via serial lane |
|
|
154
|
+
| `tools` / `stop` | ✅ | ✅ (batches) |
|
|
155
|
+
| `--compiled-decode`/`--perf-kernel`/`--fused-*`/`--force-wire` | ✅ (serial perf tree) | **n/a — compat mode, no perf flags by design** |
|
|
156
|
+
|
|
157
|
+
### `--batch N` is compat mode — perf flags don't apply by design
|
|
158
|
+
|
|
159
|
+
The bit-parity guarantee (mlx-lm B=N) is the *whole point* of `--batch N`,
|
|
160
|
+
and it requires running the plain forward path. So the batched lane
|
|
161
|
+
deliberately exposes **no** perf knobs: the scheduler
|
|
162
|
+
([batch-scheduler.ts](../../src/serve/batch-scheduler.ts)) drives the
|
|
163
|
+
model through `forwardHidden`/`logitsFromHidden` directly (not
|
|
164
|
+
`generate()`), running the same bit-exact kernels mlx-lm runs — never the
|
|
165
|
+
optional, parity-breaking ones. Flagging the batched lane would defeat
|
|
166
|
+
the guarantee, so it's intentionally not wired.
|
|
167
|
+
|
|
168
|
+
- **`--perf-kernel` / `--fused-decode` / `--compiled-decode`** — never
|
|
169
|
+
engage in the batched lane. They diverge from the -O0 reference (or, for
|
|
170
|
+
the quantized-KV kernels, are moot since batched is bf16). They're the
|
|
171
|
+
L3 perf row, a separate KL-gated path — see the validation matrix in
|
|
172
|
+
[parallel-slots.md](../design/parallel-slots.md).
|
|
173
|
+
- **`--force-wire`** — doesn't wire (the scheduler bypasses `generate()`'s
|
|
174
|
+
wired scope). A model that needs wiring for speed (e.g. 26B: 8.6 → 32.3
|
|
175
|
+
tok/s wired) runs at mlx-lm-equivalent unwired speed under `--batch N`;
|
|
176
|
+
in practice such a model has no headroom for B>1 KV anyway.
|
|
177
|
+
- **Always-on bit-exact kernels still run.** Fused GeGLU is bit-exact with
|
|
178
|
+
the spelled-out MLP, so it stays on in both lanes without breaking
|
|
179
|
+
parity — "compat mode" means *no parity-breaking optionality*, not
|
|
180
|
+
-O0.
|
|
181
|
+
|
|
182
|
+
The serial lane is where the perf/optimization tree lives: `--perf-kernel`
|
|
183
|
+
/ `--fused-*` / `--compiled-decode` engage there, and the
|
|
184
|
+
mixed-precision-KV (optiq) default applies there.
|
|
185
|
+
|
|
186
|
+
## Known limitations under `--batch N`
|
|
187
|
+
|
|
188
|
+
These are deliberate v1 scope, not bugs — but they change behavior, so
|
|
189
|
+
know them:
|
|
190
|
+
|
|
191
|
+
1. **Prompt cache bypassed.** Batched requests solo-prefill every row;
|
|
192
|
+
`cached_tokens=0`. Wiring `PromptCache` into the scheduler is a
|
|
193
|
+
follow-up.
|
|
194
|
+
2. **Admission is per-request, not aggregate.** `--memory-budget` checks
|
|
195
|
+
each request against single-sequence max-safe-context, but N
|
|
196
|
+
concurrent rows can collectively exceed the budget (the `B×S_max`
|
|
197
|
+
KV-budget admission is TODO). With a tight budget and several
|
|
198
|
+
large-context requests, the allocator cap is the only backstop — and
|
|
199
|
+
a true GPU OOM here is uncatchable. Size the budget with headroom for
|
|
200
|
+
N rows, or keep N small.
|
|
201
|
+
3. **Short-context only.** Verified pre-ring-wrap (rows < the 1024
|
|
202
|
+
sliding window). Long-context (context > window) batched decode is a
|
|
203
|
+
separate validation.
|
|
204
|
+
4. **bf16 only — by contract.** mlx-lm's batched path *is* bf16, so
|
|
205
|
+
bf16-only batching is exactly what mlx-lm-parity means (not a
|
|
206
|
+
shortcoming). Going further — batched + mixed-precision KV quant — is
|
|
207
|
+
novel territory with no mlx-lm/optiq oracle, so it's a deferred,
|
|
208
|
+
KL-gated extension.
|
|
209
|
+
5. **`extend` join not yet used.** A joining request re-merges the whole
|
|
210
|
+
batch (O(B·S)); mlx-lm's keep-the-running-batch `extend` is a later
|
|
211
|
+
optimization (numerically equivalent).
|
|
212
|
+
|
|
213
|
+
## Observability — `GET /stats`
|
|
214
|
+
|
|
215
|
+
The live config and batch state:
|
|
216
|
+
|
|
217
|
+
```jsonc
|
|
218
|
+
{
|
|
219
|
+
"server": { "owner": "serve", "model": "...", "started_at": 0 },
|
|
220
|
+
"prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 2000000000, "hits": 0, "misses": 0 },
|
|
221
|
+
"response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
|
|
222
|
+
"kv_quant": {
|
|
223
|
+
"mode": "mixed (kv_config.json)" | "uniform-kv8" | "bf16",
|
|
224
|
+
"layers": { "kv4": 8, "bf16": 40 },
|
|
225
|
+
"attention": { "global": 10, "sliding_window": 38 }
|
|
226
|
+
},
|
|
227
|
+
"admission": {
|
|
228
|
+
"max_safe_context": 0, // requests above this 400
|
|
229
|
+
"memory_budget_bytes": null, // explicit budget, or null = machine default
|
|
230
|
+
"usable_bytes": 0, "weights_bytes": 0
|
|
231
|
+
},
|
|
232
|
+
"batch": {
|
|
233
|
+
"configured": 1, // the --batch N value
|
|
234
|
+
"batched": false, // batching enabled (N>1) for this server
|
|
235
|
+
"active_rows": 0 // rows currently decoding in the batch
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`batch.batched` reflects only whether `--batch N` (N>1) is configured;
|
|
241
|
+
with an explicit `--kv-quant` it can read `true` while `active_rows`
|
|
242
|
+
never exceeds 1 (every request routes serial). `active_rows` is the
|
|
243
|
+
honest signal of whether anything is actually batching.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Handoff — segmented backward training (2026-06-16)
|
|
2
|
+
|
|
3
|
+
Current-state report for the segmented-backward LoRA training work. Deep dossier
|
|
4
|
+
(design, proofs, every measurement): [`docs/design/segmented-backward-training.md`](design/segmented-backward-training.md)
|
|
5
|
+
§9 (MiniCPM5) and §10 (e4b). Branch: `segmented-backward-training`,
|
|
6
|
+
PR [#9](https://github.com/joshuarossi/mlx-bun/pull/9).
|
|
7
|
+
|
|
8
|
+
## TL;DR
|
|
9
|
+
|
|
10
|
+
Long-context LoRA SFT that **streams the backward segment-by-segment** so only one
|
|
11
|
+
segment's activations are live at a time — training fits at context lengths where
|
|
12
|
+
the full backward spikes. Two phases, both working:
|
|
13
|
+
|
|
14
|
+
- **MiniCPM5 (Phase A): done, production-ready.** Bit-exact mechanism, no memory
|
|
15
|
+
leak, trains end-to-end. Real 300-iter chunk fine-tune → **chunk-eval 95.10/100**
|
|
16
|
+
(beats the 91.70 non-segmented baseline; pre-tune 11.89).
|
|
17
|
+
- **e4b / gemma-3n (Phase B): built + validated.** Forward bit-exact; grads
|
|
18
|
+
bf16-class; trains **all 42 layers at 8K context** where `mlx_lm.lora` OOMs.
|
|
19
|
+
|
|
20
|
+
**Parity:** our **non-segmented** training is **bit-exact with mlx-lm** at 2k
|
|
21
|
+
(forward, loss, AND gradient — verified by running mlx-lm's own `default_loss` /
|
|
22
|
+
`nn.value_and_grad`). The **segmented** path is **bf16-class (~2%)** off, a
|
|
23
|
+
grouping-controllable bf16 non-associativity (not a logic bug).
|
|
24
|
+
|
|
25
|
+
## What was built
|
|
26
|
+
|
|
27
|
+
| file | what |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `src/train/segmented.ts` | `SegmentedBackward` (MiniCPM5) + `SegmentedBackwardGemma4` (e4b) + `planSegmentsBySize`; the per-segment vjp driver, `detachLeaf` |
|
|
30
|
+
| `src/mlx/autograd.ts` | `Vjp` — `mlx_vjp` binding (the backward primitive; see "leak" below) |
|
|
31
|
+
| `src/mlx/ffi.ts` | `mlx_vjp` FFI decl |
|
|
32
|
+
| `src/mlx/array.ts` | `MlxArray.fromBytesCopy` — graph-free leaf copy (row-major) |
|
|
33
|
+
| `src/model/minicpm5.ts` | `runLayerRange` (forwardLayers delegates) |
|
|
34
|
+
| `src/model/gemma4.ts` | `runLayerRange` + `makeTrainingMasks` + `embedForSegmented` + `reusedDonors` (additive; forwardLayers untouched) |
|
|
35
|
+
| `src/train/trainer.ts` | `TrainConfig.segmentSize`; gated segmented path (MiniCPM5 + Gemma4) |
|
|
36
|
+
| `src/train/loss.ts` | exported `responseOnlyCe`, `maskedCe` |
|
|
37
|
+
|
|
38
|
+
**Enable:** set `segmentSize` (layers per segment) > 0 in the train config, or
|
|
39
|
+
`SEG=n` in `scripts/chunk-finetune.ts`. 0 = the normal full backward.
|
|
40
|
+
|
|
41
|
+
How it works (B=1 SFT): forward saves a DETACHED boundary (graph-free leaf) at each
|
|
42
|
+
segment edge → a loss-head `mlx_vjp` gives `dh` for the last boundary → reverse
|
|
43
|
+
per-segment `mlx_vjp`s thread `dh` and the segment's LoRA params, accumulating
|
|
44
|
+
grads. Only one segment's activations are live. e4b adds two boundary streams: the
|
|
45
|
+
per-layer-input tensor (a pure constant) and the **KV-shared donor K/V** (a second
|
|
46
|
+
boundary stream WITH cotangent accumulation — the intricate part; docs §10).
|
|
47
|
+
|
|
48
|
+
## What was validated (honest numbers)
|
|
49
|
+
|
|
50
|
+
**Correctness**
|
|
51
|
+
- MiniCPM5 segmented grads vs the full value_and_grad: **bit-exact under flash**
|
|
52
|
+
(relNorm 0.0000%). Under `ops.sdpa`, ~6% — a *pre-existing* `ops.sdpa`
|
|
53
|
+
eager-vs-autograd forward divergence, not segmentation.
|
|
54
|
+
- e4b: forward **bit-exact** vs full; single-consumer donor reuse **bit-exact**;
|
|
55
|
+
natural-cut donor-KV grads **~1% (bf16 non-associativity)** — single-consumer
|
|
56
|
+
exact, flat in consumer count, tracks summand count (donor 22's 15 sharers
|
|
57
|
+
≈0.46% vs donor 23's 3 ≈0%), grouping-dependent for both bf16 AND fp32. fp32
|
|
58
|
+
accumulation does NOT help (the term is mlx's within-vjp bf16 sum). **Controllable
|
|
59
|
+
by the segment cut** (coarser sharer grouping → tighter, down to bit-exact at
|
|
60
|
+
1 consumer), not the dtype.
|
|
61
|
+
- **Parity vs mlx-lm (ran their code):** forward/loss — denominator (`M` vs
|
|
62
|
+
`ntoks`) bit-exact across 6 mask spans incl. edges; response-only CE == our
|
|
63
|
+
full-masked CE (a pure memory optimization); loss bit-exact on 4/6 spans, 0.5
|
|
64
|
+
float32-ulp on 2/6 (a logsumexp/reduction f32 effect, ~5e-5%, harmless).
|
|
65
|
+
Gradient — our FULL (non-segmented) `dB` over all 42 q_proj **bit-exact** with
|
|
66
|
+
mlx-lm; segmented `dB` ~2.3% (the bf16 donor-KV effect).
|
|
67
|
+
|
|
68
|
+
**Memory leak (found + fixed).** The first cut used a surrogate-loss
|
|
69
|
+
`value_and_grad`; it leaked ~32 MB/segment/step (mlx-level, not GC/cache/sync
|
|
70
|
+
reclaimable). **Fixed by using `mlx_vjp`** (the natural primitive — no surrogate).
|
|
71
|
+
End-to-end now flat (active constant across a 300-iter run). Repro:
|
|
72
|
+
`scripts/vag-leak-test.ts` (MODE=layers leaks via value_and_grad; MODE=vjp flat).
|
|
73
|
+
|
|
74
|
+
**Memory vs the real reference** (`mlx_lm.lora --grad-checkpoint`, the way a user
|
|
75
|
+
trains — it loads the OptiQ e4b directly). mlx-lm checkpoints **per-layer**; ours
|
|
76
|
+
groups `segmentSize` layers. Matched grouping (both per-layer = segSize 1, all 42
|
|
77
|
+
layers, batch 1):
|
|
78
|
+
|
|
79
|
+
| L | mlx-lm (per-layer) | ours (segSize=1) |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| 2048 | 12.84 GB | 8.76 GB |
|
|
82
|
+
| 4096 | 20.87 GB | 10.93 GB |
|
|
83
|
+
| 8192 | **OOM** (25.7 GB @16 layers) | 15.29 GB |
|
|
84
|
+
|
|
85
|
+
Ours lower at every length AND trains all 42 layers at 8K where mlx-lm OOMs. **Two
|
|
86
|
+
confounds, not isolated:** (1) mlx-lm materializes the full `[1,L,262k]` logits
|
|
87
|
+
(~4 GB @4K) while we use response-only CE; (2) our full backward streaming vs
|
|
88
|
+
mlx-lm's per-layer checkpoint. So the gap is part CE-optimization, part mechanism —
|
|
89
|
+
not purely "segmentation beats checkpointing." (Earlier "reference crashes at 4K /
|
|
90
|
+
~70 GB" was WRONG — it used mlx-bun's *own* ineffective checkpoint as the baseline.)
|
|
91
|
+
|
|
92
|
+
## How to run an adapter
|
|
93
|
+
|
|
94
|
+
The fine-tune saves a hot-swap adapter dir (`adapters.safetensors` +
|
|
95
|
+
`optiq_lora_config.json` + `adapter_config.json`).
|
|
96
|
+
|
|
97
|
+
**One-off generate** (verified working) — `scripts/run-adapter.ts`:
|
|
98
|
+
```sh
|
|
99
|
+
ADAPTER=~/.cache/mlx-bun-finetunes/minicpm5-chunk-segmented \
|
|
100
|
+
PROMPT="Split into chunks and return JSON: ..." \
|
|
101
|
+
MAXTOK=256 bun scripts/run-adapter.ts
|
|
102
|
+
# omit ADAPTER for the base model; MODEL=<dir> to change base; SYSTEM=... TEMP=...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Score it on the chunk task:**
|
|
106
|
+
```sh
|
|
107
|
+
ADAPTER=~/.cache/mlx-bun-finetunes/minicpm5-chunk-segmented bun scripts/chunk-eval.ts
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Serve it** (you run the server; mount via the API):
|
|
111
|
+
```sh
|
|
112
|
+
# 1) start: mlx-bun serve <model-dir>
|
|
113
|
+
# 2) mount: curl -X POST localhost:PORT/v1/adapters -d '{"id":"chunk","path":"<adapter-dir>"}'
|
|
114
|
+
# 3) list: curl localhost:PORT/v1/adapters # unmount: DELETE /v1/adapters/chunk
|
|
115
|
+
```
|
|
116
|
+
The adapter activates per request through `loraState`; in code the pattern is
|
|
117
|
+
`new AdapterManager(model).mount(id, dir)` then `model.loraState.active = [id]`.
|
|
118
|
+
|
|
119
|
+
**Train a new adapter** (segmented):
|
|
120
|
+
```sh
|
|
121
|
+
# MiniCPM5, seq 4096, segments of 4 layers:
|
|
122
|
+
MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0 \
|
|
123
|
+
SEQ=4096 ITERS=300 SEG=4 DATA=<data-dir> ADAPTER=<out-dir> \
|
|
124
|
+
bun scripts/chunk-finetune.ts
|
|
125
|
+
# e4b: add MODEL=<e4b-dir> and use SEG=2 (or 1) for ≥8K to keep peak down.
|
|
126
|
+
```
|
|
127
|
+
Training REQUIRES `MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0` (those fused kernels
|
|
128
|
+
have no vjp). Use `MLX_BUN_TRAIN_ATTN=flash` for exact grads (slower).
|
|
129
|
+
|
|
130
|
+
## What's next / open
|
|
131
|
+
|
|
132
|
+
1. **e4b @8K toward ~10 GB** (currently 15.3 GB segSize=1): the §5 full-attention
|
|
133
|
+
ISOLATION `planSegments` — put each O(L²) full-attn layer (5,11,17,23,29,35,41)
|
|
134
|
+
in its own short segment. Mechanism + tuning knob are proven; this is peak-shaving.
|
|
135
|
+
2. **Isolate the memory confound** (optional): run ours at segSize=1 with full-logits
|
|
136
|
+
CE to separate the response-only-CE saving from the streaming mechanism.
|
|
137
|
+
3. **mlx-bun's own gradient checkpoint is ineffective** (23 GB @2048 vs mlx-lm's
|
|
138
|
+
12.8) — orthogonal bug; segmented sidesteps it.
|
|
139
|
+
4. Scale knobs: `segmentSize` is the peak↔grad-fidelity knob (finer = lower peak,
|
|
140
|
+
more donor-KV bf16 deviation).
|
|
141
|
+
|
|
142
|
+
## Diagnostic scripts (kept)
|
|
143
|
+
|
|
144
|
+
`scripts/segmented-grad-test.ts` (MiniCPM5 grads + peak + leak loop),
|
|
145
|
+
`segmented-grad-test-e4b.ts` (e4b grads + peak; RANGES/SEG_ONLY/SEG_DUMP),
|
|
146
|
+
`parity-vs-mlxlm.ts` + `grad-parity-vs-mlxlm.ts` (mlx-lm parity; Python sides in
|
|
147
|
+
`/tmp/mlxlm-*.py`), `vag-leak-test.ts`, `seg-debug.ts`, `gemma4-runlayerrange-test.ts`,
|
|
148
|
+
`run-adapter.ts`, `chunk-{finetune,eval}.ts`.
|