SPB Git

spb/zyquo-mlx Public MIT

The local MLX foundry for your Mac — run, fine-tune, quantize, and ship models. Nothing leaves your machine.

Swift 93.4% Python 3.8% Makefile 2.2% Shell 0.5%

phase0: complete research docs — MLX-RESEARCH, TRAINING-RESEARCH, MODELS (43 live-verified repos), BUILD (Metal toolchain resolved empirically)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simon-pierre boucher committed 11 days ago (Jul 30, 2026) parent 051b9b5

Showing 5 changed files with +1,249 and −26

added docs/BUILD.md +208 −0
@@ -0,0 +1,208 @@
1 +<!--
2 + BUILD.md
3 + Zyquo MLX
4 +
5 + Author: Simon-Pierre Boucher
6 + Mail: contact@spboucher.ai
7 +-->
8 +
9 +# Zyquo MLX — Build Recipe (No Xcode IDE)
10 +
11 +> Phase 0 deliverable. The Metal-toolchain question was **resolved empirically
12 +> on this machine on 2026-07-30** and cross-checked against upstream sources.
13 +> This document is the contract for the Phase 1 `Makefile`.
14 +
15 +---
16 +
17 +## 1. The Metal Toolchain Question — Resolved
18 +
19 +### 1.1 Empirical test (this Mac)
20 +
21 +Host: Apple M5 Max, 48 GB, macOS 27.0, Swift 6.4
22 +(swiftlang-6.4.0.25.4), `xcode-select -p`
23 +`/Library/Developer/CommandLineTools`, no Xcode.app installed anywhere.
24 +
25 +Test: fresh SPM package depending on `mlx-swift` (resolved 0.31.6),
26 +`swift build`. Result:
27 +
28 +- All ~500 C++/Swift compile steps of `Cmlx`/`MLX`/`MLXNN` **succeed** with
29 + CLT only.
30 +- The build **fails** at
31 + `CompileMetalFile …/Source/Cmlx/mlx-generated/metal/steel/attn/kernels/steel_attention.metal`
32 + with `error: unable to spawn process 'metal' (No such file or directory)`.
33 +- The CLT ships no `metal` binary (verified: nothing metal-related in
34 + `/Library/Developer/CommandLineTools/usr/bin`).
35 +
36 +**Conclusion: CLT-only builds are impossible today.** Only the `.metal`
37 +kernel → `default.metallib` step needs the Metal toolchain.
38 +
39 +### 1.2 Why (verified in mlx-swift `Package.swift` at main)
40 +
41 +`Cmlx` uses a hybrid kernel strategy: many kernels are runtime-JIT'd (checked
42 +-in generated `.cpp` sources with embedded kernel strings, compiled at runtime
43 +via Metal's `newLibrary(source:)`), **but** a set of AOT `.metal` files
44 +(arg_reduce, conv, gemv, layer_norm, steel attention/gemm, …) must be compiled
45 +at build time into the resource bundle `mlx-swift_Cmlx.bundle/default.metallib`
46 +(`SWIFTPM_BUNDLE="mlx-swift_Cmlx"`, `METAL_PATH="default.metallib"`).
47 +No prebuilt-metallib or binary xcframework distribution exists (upstream work
48 +in flight: mlx-swift PR #430 "Build SwiftPM default Metal library resource",
49 +mlx#3597 `set_metallib_path`, mlx-swift#416 `GPU.setMetallibPath` — none
50 +landed; re-check at each dependency bump).
51 +
52 +### 1.3 Getting the Metal compiler in the Xcode 26+ era
53 +
54 +- The Metal compiler is a separate ~700 MB downloadable component since
55 + Xcode 26: `xcodebuild -downloadComponent metalToolchain`
56 + (verify with `xcodebuild -showComponent metalToolchain`; CI export/import
57 + flags exist: `-exportPath` / `-importComponent … -importPath`).
58 +- `xcodebuild` **refuses to run under CLT** ("requires Xcode" — reproduced
59 + locally). No standalone Metal Toolchain download exists on
60 + developer.apple.com (*none found publicly; UNVERIFIED whether one hides
61 + behind login*).
62 +- **→ Full Xcode.app is required.** Per the charter (0.A.6), the rule adapts:
63 + **install Xcode strictly as a toolchain; drive everything via command-line
64 + `swift build`/`xcodebuild`; never open the IDE; never hand-author an
65 + `.xcodeproj`.** All automation lives in the `Makefile`.
66 +
67 +---
68 +
69 +## 2. One-Time Machine Setup
70 +
71 +```bash
72 +# 1. Install Xcode.app (toolchain only — the IDE is never opened).
73 +# Mac App Store (mas), or download from developer.apple.com/download.
74 +# 2. Point the toolchain at it:
75 +sudo xcode-select -s /Applications/Xcode.app
76 +sudo xcodebuild -license accept
77 +# 3. Install the Metal toolchain component (Xcode 26+):
78 +xcodebuild -downloadComponent metalToolchain
79 +xcodebuild -showComponent metalToolchain # verify "installed"
80 +# 4. Sanity check:
81 +xcrun -f metal # must resolve inside Xcode/Metal toolchain, not fail
82 +```
83 +
84 +Per-command alternative to the global `xcode-select` (keeps CLT default):
85 +`DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer <cmd>` — the
86 +Makefile uses this form so the machine's default toolchain is untouched.
87 +
88 +Other host requirements (already present on this Mac): `uv` (Python venv
89 +bootstrap), `rsvg-convert` + `iconutil` (icon pipeline, Phase 5),
90 +`notarytool` + `stapler` (ship with CLT, Phase 8).
91 +
92 +---
93 +
94 +## 3. Building the App
95 +
96 +### 3.1 Build commands
97 +
98 +Primary (officially documented for mlx-swift — README: "Although SwiftPM
99 +(command line) cannot build the Metal shaders, **xcodebuild can** and it can
100 +be used to do command line builds"):
101 +
102 +```bash
103 +DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
104 +xcodebuild build \
105 + -scheme ZyquoMLX \
106 + -destination 'platform=OS X,arch=arm64' \
107 + -configuration Release \
108 + -derivedDataPath .build/xcode \
109 + -skipMacroValidation
110 +```
111 +
112 +(SPM package schemes are auto-generated from `Package.swift` — no
113 +`.xcodeproj` is ever created by hand.)
114 +
115 +**Also try at Phase 1:** plain `swift build -c release` with
116 +`DEVELOPER_DIR` pointing at Xcode. Local evidence shows Swift 6.4's SwiftPM
117 +(new swift-build backend) issues `CompileMetalFile` steps itself, so with the
118 +Metal toolchain installed it may now build the metallib end-to-end
119 +(*UNVERIFIED until the toolchain is installed — mlx-swift's README statement
120 +predates Swift 6.4*). If it works, prefer `swift build` (simpler paths);
121 +keep `xcodebuild` as the documented fallback. Record the outcome here.
122 +
123 +### 3.2 Assembling `Zyquo MLX.app` (Makefile)
124 +
125 +```
126 +Zyquo MLX.app/
127 +└── Contents/
128 + ├── Info.plist # CFBundleDisplayName "Zyquo MLX", com.zyquo.mlx,
129 + │ # LSMinimumSystemVersion 14.0, arm64 priority,
130 + │ # NSHighResolutionCapable, developer-tools category
131 + ├── MacOS/ZyquoMLX # release binary
132 + └── Resources/
133 + ├── AppIcon.icns
134 + ├── mlx-swift_Cmlx.bundle/ # REQUIRED — contains default.metallib
135 + │ # (missing bundle = GPU ops crash at runtime;
136 + │ # mlx-swift issue #345)
137 + └── <other SPM resource bundles> # mlx-swift-lm / swift-transformers
138 + # bundles if produced by the build
139 +```
140 +
141 +The Makefile copies **every** `*.bundle` from the build products directory
142 +into `Contents/Resources/` — this is the classic mlx-swift packaging pitfall.
143 +Existence proof that this recipe signs/notarizes/ships: sfomuseum/Docent
144 +(discussed with mlx-swift maintainers in issue #345).
145 +
146 +### 3.3 Makefile targets (contract for Phase 1)
147 +
148 +| Target | Does |
149 +|---|---|
150 +| `make build` | debug build (xcodebuild or swift build per §3.1 outcome) |
151 +| `make app` | release build + assemble `Zyquo MLX.app` + ad-hoc sign |
152 +| `make dev` | `make app` + launch from terminal (proper activation) |
153 +| `make icon` | SVG → PNGs (`rsvg-convert`) → `.iconset``iconutil -c icns` |
154 +| `make release` | Phase 8: hardened-runtime Developer ID sign (nested-first) + notarize + staple |
155 +| `make clean` | remove `.build/` + `dist/` |
156 +
157 +### 3.4 Python environment (runtime, not build-time)
158 +
159 +The Python side is **not** part of the app build — it is provisioned at first
160 +run into `~/Library/Application Support/ZyquoMLX/py/` (outside the signed
161 +bundle, per the notarization fallback already anticipated in the charter):
162 +
163 +```bash
164 +uv python install 3.12
165 +uv venv "$APP_SUPPORT/py/venv" --python 3.12
166 +uv pip install --python "$APP_SUPPORT/py/venv" \
167 + mlx-lm==0.31.3 mlx-vlm==0.6.8 mlx-whisper==0.4.3 mlx-audio==0.4.6
168 +```
169 +
170 +Pinned via a checked-in requirements lock (`uv pip compile`). Python 3.12
171 +chosen deliberately: mlx wheels span cp310–cp314, but 3.12 is the safest
172 +ecosystem-wide floor; wheels are `macosx_14_0_arm64`+ — consistent with the
173 +app's Apple-Silicon-only gate. Repair = delete venv + re-provision (uv
174 +supports `--offline` with a local wheel cache). Helper scripts live in
175 +`PyBridge/scripts/` and are versioned with the app, never fetched remotely.
176 +
177 +---
178 +
179 +## 4. Signing & Notarization Posture (informs Phase 8; credentials from zyquo-term)
180 +
181 +- **Runtime Metal JIT is not process-JIT**: MLX compiles kernels at runtime
182 + through the Metal framework (`newLibrary(source:)`) — GPU shader
183 + compilation, not writable-executable process memory. No reports of MLX apps
184 + needing `com.apple.security.cs.allow-jit` or
185 + `allow-unsigned-executable-memory` (none in ml-explore trackers; Apple's
186 + LoRATrainingExample entitlements are sandbox + network.client + files
187 + read-only + iOS memory-limit only).
188 + **→ Start with Hardened Runtime and NO extra entitlements; add only what a
189 + failing notarization proves necessary, and document why here.**
190 +- The `mlx-swift_Cmlx.bundle` contains no Mach-O code (metallib is data) —
191 + the classic failure is *omitting* the bundle, not signing it. Sign nested
192 + frameworks/dylibs first (if any), then the app, then notarize
193 + (`ditto -c -k --keepParent``notarytool submit --wait``stapler staple`).
194 +- Python lives in Application Support (unsigned territory) → the bundled-
195 + Python notarization minefield (unsigned `.so` files) is avoided entirely.
196 +- No App Sandbox (local ML workbench needing broad file/compute access),
197 + per the charter.
198 +
199 +---
200 +
201 +## 5. Open Items to Re-Verify During Phase 1
202 +
203 +1. Does plain `swift build` produce `default.metallib` with the Metal
204 + toolchain installed (Swift 6.4 path)? → record result in §3.1.
205 +2. Exact set of resource bundles emitted by mlx-swift-lm 3.31.4 /
206 + swift-transformers 1.3.x → finalize §3.2 copy list.
207 +3. mlx-swift PR #430 / #416 status at dependency-bump time (a prebuilt
208 + metallib path would remove the Xcode requirement — re-test CLT-only then).
added docs/MLX-RESEARCH.md +388 −0
@@ -0,0 +1,388 @@
1 +<!--
2 + MLX-RESEARCH.md
3 + Zyquo MLX
4 +
5 + Author: Simon-Pierre Boucher
6 + Mail: contact@spboucher.ai
7 +-->
8 +
9 +# Zyquo MLX — MLX Framework Research (Ground Truth 2026-07-30)
10 +
11 +> Phase 0.A research document. Every version number and API name below was
12 +> verified against live sources (GitHub releases API, raw source files at
13 +> `main`, PyPI JSON, official docs) on 2026-07-30, plus an empirical build test
14 +> on this machine. Items not verifiable against a primary source are marked
15 +> *UNVERIFIED*.
16 +
17 +**Version snapshot (live-verified):**
18 +
19 +| Component | Version | Date | Source |
20 +|---|---|---|---|
21 +| `mlx` (core, PyPI/GitHub) | **0.32.0** | 2026-07-07 | github.com/ml-explore/mlx |
22 +| `mlx-swift` | **0.31.6** | 2026-07-02 | github.com/ml-explore/mlx-swift |
23 +| `mlx-swift-lm` | **3.31.4** | 2026-06-30 | github.com/ml-explore/mlx-swift-lm |
24 +| `mlx-lm` (PyPI) | **0.31.3** | 2026-04-22 | pypi.org/project/mlx-lm |
25 +| `swift-transformers` | **1.3.3** | 2026-05-16 | github.com/huggingface/swift-transformers |
26 +| `swift-huggingface` (HubClient) | **0.9.0** | — | github.com/huggingface/swift-huggingface |
27 +| `mlx-vlm` (PyPI) | **0.6.8** | 2026-07-27 | github.com/Blaizzy/mlx-vlm |
28 +| `mlx-whisper` (PyPI) | **0.4.3** | 2025-08-29 | ml-explore/mlx-examples/whisper |
29 +| `mlx-audio` (PyPI) | **0.4.6** | 2026-07-25 | github.com/Blaizzy/mlx-audio |
30 +
31 +---
32 +
33 +## 1. What MLX Is — Core Primitives
34 +
35 +- **An array framework for ML on Apple Silicon** from Apple ML research:
36 + NumPy-like `mlx.core`, NN modules `mlx.nn`, `mlx.optimizers`. C++ core with a
37 + Metal backend (a CUDA/Linux backend also exists now — visible in mlx-swift's
38 + `Package.swift`, which conditionally compiles `mlx/backend/cuda` on Linux).
39 +- **Lazy evaluation & `eval()`**: operations record a compute graph; nothing
40 + executes until `mx.eval(...)` or an implicit eval (printing, `.item()`,
41 + NumPy conversion, `mx.save`). Laziness enables transformations (`grad`,
42 + `vmap`, `compile`), dead-output elimination, and loading weights without
43 + materializing intermediates. Guidance: eval at outer-loop boundaries (after
44 + an optimizer step); graphs of "tens to thousands of ops" per eval are the
45 + sweet spot. (ml-explore.github.io/mlx — lazy evaluation guide, fetched.)
46 +- **Unified memory**: arrays live in memory shared by CPU and GPU. There is no
47 + data placement — only *operation* placement via `stream:`/`device:`
48 + (`mx.gpu` / `mx.cpu`). The scheduler manages cross-stream dependencies
49 + automatically. (Unified-memory guide, fetched.)
50 +- **Autodiff**: `mx.grad` / `mx.value_and_grad`, composable with `mx.vmap` and
51 + `mx.compile`. Module-aware: `nn.value_and_grad(model, loss_fn)`; Swift:
52 + `MLXNN.valueAndGrad(model:_:)` (3 public overloads, verified in source).
53 +- **Dispatch to Metal**: the core dispatches lazily-built graphs to
54 + precompiled + runtime-JIT Metal kernels (`backend/metal/kernels`,
55 + `jit_kernels.cpp`, `default.metallib`); quantized matmul and
56 + scaled-dot-product-attention are dedicated kernels.
57 +
58 +---
59 +
60 +## 2. Swift vs. Python — The Honest Capability Split (2026-07-30)
61 +
62 +**Major reorg (critical):** `MLXLLM`, `MLXVLM`, `MLXLMCommon`, `MLXEmbedders`
63 +moved out of `mlx-swift-examples` into **`ml-explore/mlx-swift-lm`** (3.x).
64 +3.x decoupled downloads/tokenizers into protocols (`Downloader`, `Tokenizer`,
65 +`TokenizerLoader`) with concrete impls supplied by the **`MLXHuggingFace`
66 +macros** (`#hubDownloader()`, `#huggingfaceTokenizerLoader()`,
67 +`#huggingFaceLoadModelContainer(...)`) backed by `swift-huggingface` +
68 +`swift-transformers`. `mlx-swift-examples` (renamed package `mlx-libraries`)
69 +now only ships `StableDiffusion` and `MLXMNIST` + example apps.
70 +
71 +| Capability | Native Swift today | Python | Zyquo MLX strategy |
72 +|---|---|---|---|
73 +| LLM inference (streaming, KV cache, chat) | ✅ MLXLLM/MLXLMCommon (`ChatSession`, `ModelContainer`, speculative decoding, KV-cache quantization) | mlx-lm | **Swift** |
74 +| VLM inference | ✅ MLXVLM (smaller zoo; no omni/audio) | mlx-vlm (broader) | **Swift**; Py optional for exotic/omni |
75 +| Embeddings | ✅ MLXEmbedders | mlx-embeddings (thin, v0.1.0) | **Swift** |
76 +| Speech (STT/TTS) | ❌ no official Swift package | ✅ mlx-whisper / mlx-audio | **Python bridge** |
77 +| Image generation | ✅ `StableDiffusion` (SD 2.1 / SDXL-turbo) in mlx-swift-examples; FLUX ❌ | ✅ mlx-examples flux + SD scripts | Swift for SD; **Python bridge for FLUX** |
78 +| LoRA/DoRA training | ✅ `LoRATrain` (MLXLLM) — real but text-only datasets, no grad-checkpoint/accum, no numbered checkpoints | ✅ mlx_lm.lora (chat/completions datasets, mask_prompt, full/dora, grad checkpoint/accum, checkpoints, schedules) | **Python bridge primary** (see TRAINING-RESEARCH.md §8.3) |
79 +| QLoRA (quantized base) | ✅ `QLoRALinear` | ✅ implicit when base is quantized | Both |
80 +| Adapter interchange | ✅ `LoRAContainer.from(directory:)` — documented mlx-lm-compatible (`adapter_config.json` + `adapters.safetensors`) | native | Interoperable both ways |
81 +| Full fine-tuning | ❌ no packaged pipeline (primitives exist) | ✅ `--fine-tune-type full` | **Python bridge** |
82 +| Quantization + HF→MLX conversion | ✅ **new native Swift**: `LLMModelFactory.convert(from:to:options:)` (safetensors input only, LLMs; affine/mxfp4) | ✅ mlx_lm.convert (+ AWQ/DWQ/GPTQ/dynamic, mixed recipes, `.bin`, GGUF export, VLM convert) | Swift for standard LLM convert/quant; **Python for advanced** |
83 +| Fuse adapter → disk | ✅ in-memory fuse; packaged fuse-to-disk one-liner UNVERIFIED | ✅ mlx_lm.fuse (+ `--dequantize`, GGUF) | **Python safest**; Swift feasible |
84 +| Evaluation (ppl / lm-eval) | partial (`LoRATrain.evaluate` loss) | ✅ mlx_lm.evaluate (lm-eval), mlx_lm.perplexity, `--test` | **Python bridge** |
85 +| OpenAI-compatible server | ❌ | ✅ mlx_lm.server / mlx_vlm.server | out of scope v1 |
86 +
87 +---
88 +
89 +## 3. Model Types & the Current MLX Zoo
90 +
91 +### 3.1 Swift-native architectures (verified from factory registries at main)
92 +
93 +- **LLM** (`LLMTypeRegistry`, keyed by `config.json` `model_type`): llama,
94 + mistral, mixtral, mistral3, phi, phi3, phimoe, gemma, gemma2,
95 + gemma3/gemma3_text, gemma3n, gemma4/gemma4_unified/gemma4_text, qwen2, qwen3,
96 + qwen3_moe, qwen3_next, qwen3_5, qwen3_5_moe, qwen3_5_text, smollm3,
97 + deepseek_v2, deepseek_v3, glm4, glm4_moe, glm4_moe_lite, gpt_oss, granite,
98 + granitemoehybrid, cohere, starcoder2, openelm, internlm2, minicpm, mimo,
99 + minimax, falcon_h1, bitnet, ernie4_5, lfm2, lfm2_moe, exaone4, olmoe, olmo2,
100 + olmo3, bailing_moe, nemotron_h, jamba, mamba2, apertus, and more
101 + (Qwen2.5 = `qwen2`; DeepSeek-R1 distills use their base arch).
102 +- **VLM** (`VLMModelFactory`): paligemma, qwen2_vl, qwen2_5_vl, qwen3_vl,
103 + qwen3_5, qwen3_5_moe, idefics3, gemma3, gemma4, gemma4_unified, smolvlm,
104 + fastvlm (llava_qwen2), pixtral, mistral3, lfm2_vl, glm_ocr.
105 +- **Embedders** (`EmbedderTypeRegistry`): bert, roberta, xlm-roberta,
106 + distilbert, nomic_bert, qwen3 (Qwen3-Embedding), lfm2, gemma3/gemma3n
107 + (EmbeddingGemma). Curated `EmbedderRegistry` includes bge family, MiniLM,
108 + nomic-embed-text v1/v1.5, e5, mixedbread, Qwen3-Embedding.
109 +- **Image gen**: `StableDiffusion` product (SD 2.1, SDXL-turbo).
110 +- **Bonus (new since 2025, all verified in mlx-swift-lm):** native conversion +
111 + quantization (`ModelConversion.swift`), LoRA/DoRA adapters + fusing,
112 + LoRA training, speculative decoding + MTP drafters, KV-cache quantization
113 + (incl. "TurboQuant"), guided generation (`MLXGuidedGeneration`, JSON
114 + Schema/EBNF via vendored xgrammar), tool calling with per-family parsers,
115 + prompt-cache persistence, `MLXFoundationModels` bridge (macOS 27 SDK).
116 +
117 +### 3.2 Python-first packages
118 +
119 +| Package | What | Interface |
120 +|---|---|---|
121 +| mlx-lm 0.31.3 | LLM inference/serve/convert/quant/train | CLIs: `mlx_lm.generate/chat/convert/fuse/lora/server/evaluate/manage/benchmark/cache_prompt/perplexity/upload`; quant CLIs `mlx_lm.awq/dwq/gptq/dynamic_quant`. Python: `load()`, `generate()`, `stream_generate()` (yields `GenerationResponse{text, token, prompt_tps, generation_tps, peak_memory, finish_reason}`), `make_sampler(...)` |
122 +| mlx-vlm 0.6.8 | VLM + omni (audio/video) inference & LoRA | `mlx_vlm.generate/chat/convert/server`; huge zoo (Qwen-VL/3.5, gemma 3/4, DeepSeek-OCR, Moondream, MiniCPM-V/o, Pixtral, LLaVA, Idefics…) |
123 +| mlx-whisper 0.4.3 | STT | `mlx_whisper audio.mp3 [-f fmt --model repo]`; API `mlx_whisper.transcribe()`; needs ffmpeg |
124 +| mlx-audio 0.4.6 | TTS/STT/STS | `mlx_audio.tts.generate`, `mlx_audio.stt.generate`, `mlx_audio.convert`, `mlx_audio.server` |
125 +| mlx-examples (repo) | SD + FLUX image gen (script-based), FLUX LoRA (dreambooth.py) | `stable_diffusion/txt2image.py`, `flux/txt2image.py --model schnell\|dev` |
126 +
127 +---
128 +
129 +## 4. Inference API Specifics (Swift-first, exact names from source)
130 +
131 +### 4.1 Loading (mlx-swift-lm 3.x)
132 +
133 +```swift
134 +// Free functions in MLXLMCommon (ModelFactory.swift):
135 +loadModelContainer(from: any Downloader, using: any TokenizerLoader,
136 + configuration: ModelConfiguration,
137 + progressHandler: @Sendable (Progress) -> Void) async throws -> ModelContainer
138 +loadModelContainer(from: any Downloader, using: any TokenizerLoader,
139 + id: String, revision: String = "main", ) async throws -> ModelContainer
140 +loadModelContainer(from directory: URL, using: any TokenizerLoader)
141 + async throws -> ModelContainer // pure-local — ideal for our library
142 +// Factory-scoped: LLMModelFactory.shared.loadContainer(...) / VLMModelFactory.shared...
143 +// Macros (MLXHuggingFace): #huggingFaceLoadModelContainer(configuration:), #hubDownloader(), #huggingfaceTokenizerLoader()
144 +```
145 +
146 +`ModelContainer` is a `Sendable final class` wrapping `ModelContext`
147 +(model + tokenizer + processor) with `perform { }` / `update { }` access;
148 +exposes `configuration`, `tokenizer`, `processor`, `modelDirectory`,
149 +`prepare(input:) -> LMInput`, `generate(input:parameters:) ->
150 +AsyncStream<Generation>`, `encode/decode`, `applyChatTemplate(messages:)`.
151 +
152 +High-level chat: `ChatSession(model)``respond(to:)` /
153 +`streamResponse(to:)` / `streamDetails(...)`, `clear()`, `saveCache(to:)`;
154 +supports instructions, tools, speculative decoding.
155 +
156 +### 4.2 Streaming generation (core path)
157 +
158 +```swift
159 +generate(input: LMInput, cache: [KVCache]? = nil, parameters: GenerateParameters,
160 + context: ModelContext, ) throws -> AsyncStream<Generation>
161 +
162 +enum Generation { case chunk(String); case info(GenerateCompletionInfo); case toolCall(ToolCall) }
163 +struct GenerateCompletionInfo {
164 + promptTokenCount: Int; generationTokenCount: Int
165 + promptTime: TimeInterval // → TTFT
166 + generateTime: TimeInterval // → tok/s
167 + stopReason: GenerateStopReason // .stop / .length / .cancelled
168 + // + speculative-decoding stats
169 +}
170 +```
171 +
172 +Callback-based `generate` overloads are **deprecated** in favor of
173 +`AsyncStream`. Cancellation = cancel the Task / terminate the stream
174 +(`generateTask` returns `(AsyncStream, Task)`). Token-level:
175 +`generateTokens(...) -> AsyncStream<TokenGeneration>`. Per-run stats for the
176 +Playground (tok/s, TTFT) come from `GenerateCompletionInfo`.
177 +
178 +### 4.3 GenerateParameters (verified fields)
179 +
180 +`prefillStepSize`, `maxTokens`, `maxKVSize` (→ `RotatingKVCache`),
181 +`kvBits` + `kvGroupSize` (default 64) + `quantizedKVStart`,
182 +`kvScheme` ("affine4"/"affine8"/"turbo8v3"…), `temperature`, `topP`, `topK`,
183 +`minP`, `seed: UInt64?` (nil = entropy; global RNG via `MLXRandom.seed`),
184 +`repetitionPenalty` (+ context size).
185 +
186 +### 4.4 Tokenizers & chat templates
187 +
188 +`MLXLMCommon.Tokenizer` protocol: `encode(text:addSpecialTokens:)`,
189 +`decode(tokenIds:skipSpecialTokens:)`, `bosToken/eosToken`,
190 +`applyChatTemplate(messages:tools:additionalContext:) throws -> [Int]`.
191 +Concrete impl from swift-transformers via the MLXHuggingFace macro adapter.
192 +Messages: `Chat.Message` (`.system/.user/.assistant/.tool`, with
193 +`images:/videos:/audios:`) → `UserInput``processor.prepare(input:)`
194 +`LMInput`. VLM: `UserInput(chat: [.user("Describe", images: [.url(fileURL)])],
195 +processing: .init(resize: …))`.
196 +
197 +### 4.5 KV cache
198 +
199 +`KVCache` protocol; `KVCacheSimple`, `RotatingKVCache`, `QuantizedKVCache`,
200 +`ChunkedKVCache`, `MambaCache`, `CacheList`; persistence via
201 +`savePromptCache(...)` / `loadPromptCache(...)`.
202 +
203 +### 4.6 Memory management (RENAMED — important)
204 +
205 +`MLX.GPU.set(cacheLimit:)` & friends are **deprecated**; current API is the
206 +**`MLX.Memory`** enum (`Source/MLX/Memory.swift`):
207 +
208 +```swift
209 +Memory.cacheLimit = 2 * 1024 * 1024 // deprecated alias: GPU.set(cacheLimit:)
210 +Memory.memoryLimit = // default 1.5× device recommended working set
211 +Memory.activeMemory; Memory.cacheMemory; Memory.peakMemory
212 +let snap = Memory.snapshot() // Snapshot{activeMemory, cacheMemory, peakMemory}, .delta(other)
213 +Memory.clearCache() // free all cached buffers
214 +GPU.deviceInfo() // architecture, maxBufferSize, maxRecommendedWorkingSetSize, memorySize
215 +GPU.resetPeakMemory()
216 +```
217 +
218 +**Unload pattern (drives Playground/Engine):** release all `ModelContainer`
219 +references → `Memory.clearCache()` → verify with `Memory.snapshot()` deltas.
220 +Doc comment (verified): small cache limits (~2 MB) often perform as well as
221 +unconstrained — expose in Settings › Compute.
222 +
223 +### 4.7 Embeddings (MLXEmbedders 3.x)
224 +
225 +```swift
226 +let container = try await EmbedderModelFactory.shared.loadContainer(
227 + from: #hubDownloader(), using: #huggingfaceTokenizerLoader(),
228 + configuration: EmbedderRegistry.nomic_text_v1_5)
229 +let vectors: [[Float]] = await container.perform { model, tokenizer, pooling in
230 + let ids = MLXArray(tokenizer.encode(text: text, addSpecialTokens: true))[.newAxis, 0...]
231 + let out = model(ids, positionIds: nil, tokenTypeIds: nil, attentionMask: nil)
232 + return pooling(out, normalize: true).asArray(Float.self)
233 + // Pooling.Strategy: .mean/.cls/.first/.last/.max/.none
234 +}
235 +```
236 +
237 +### 4.8 Training primitives in Swift
238 +
239 +`MLXNN.valueAndGrad`; `MLXOptimizers`: SGD, RMSprop, AdaGrad, AdaDelta, Adam,
240 +AdamW, Adamax, Lion, Adafactor, Muon, MultiOptimizer; schedulers:
241 +`exponentialDecay`, `stepDecay`, `cosineDecay`, `linearSchedule`,
242 +`joinSchedules`. `MLX.QuantizationMode`: `.affine`, `.mxfp4`.
243 +Details of `LoRATrain` in `docs/TRAINING-RESEARCH.md` §8.
244 +
245 +---
246 +
247 +## 5. Quantization & Conversion
248 +
249 +### 5.1 MLX core quantization (verified docs)
250 +
251 +`mx.quantize(w, group_size=None, bits=None, mode='affine', *, global_scale=None)`
252 +**affine**: group sizes 32/64/128, bits **2,3,4 (default),5,6,8**,
253 +scale+bias per group. **mxfp4/mxfp8**: E8M0 scales (group 32); **nvfp4**:
254 +E4M3 scale (group 16). **No NF4 mode exists.**
255 +
256 +### 5.2 `mlx_lm.convert` (verified argparse at main)
257 +
258 +```
259 +mlx_lm.convert --hf-path <repo-or-dir> [--mlx-path mlx_model]
260 + [-q] [--q-bits N] [--q-group-size G]
261 + [--q-mode affine|mxfp4|nvfp4|mxfp8]
262 + [--quant-predicate mixed_2_6|mixed_3_4|mixed_3_6|mixed_4_6]
263 + [--dtype float16|bfloat16|float32] [-d/--dequantize] [--upload-repo] [--trust-remote-code]
264 +```
265 +
266 +Mode defaults: affine → (group 64, 4 bits); mxfp4 → (32, 4); nvfp4 → (16, 4);
267 +mxfp8 → (32, 8). Mixed recipes are llama.cpp-Q4_K_M-style (affine only).
268 +Advanced (beyond round-to-nearest): **`mlx_lm.awq`, `mlx_lm.dwq`,
269 +`mlx_lm.gptq`, `mlx_lm.dynamic_quant`** — first-class CLIs.
270 +`convert()` also exists as a Python API. Swift-native equivalent:
271 +
272 +```swift
273 +let result = try await LLMModelFactory.shared.convert(
274 + from: directory, to: outputDirectory,
275 + options: .init(bits: 4, groupSize: 64, mode: .affine),
276 + progressHandler: { p in /* ModelConversionProgress{stage, fractionCompleted, message} */ })
277 +// safetensors input only; PyTorch .bin explicitly out of scope; LLMs only
278 +```
279 +
280 +### 5.3 On-disk MLX model format (verified `save_model` in utils.py)
281 +
282 +- `config.json` with top-level `"quantization": {"group_size", "bits", "mode"}`
283 + (+ per-layer overrides for mixed/partial quantization).
284 +- Weights: `model.safetensors` or sharded `model-0000X-of-0000N.safetensors`
285 + (**max 5 GB/shard**), each with safetensors metadata `{"format": "mlx"}`.
286 +- `model.safetensors.index.json` with `metadata.total_size`,
287 + `metadata.total_parameters`, `weight_map`.
288 +- Tokenizer files (`tokenizer.save_pretrained`), copied `*.py` +
289 + `generation_config.json`, and an mlx-tagged `README.md` model card.
290 +
291 +`ModelStore` validates a local model dir by checking: `config.json`
292 +(`model_type`, optional `quantization`), weight file(s) + index consistency,
293 +tokenizer files present.
294 +
295 +---
296 +
297 +## 6. Build & Execution Strategy (resolved — details in `docs/BUILD.md`)
298 +
299 +**Build (empirically tested on this Mac, 2026-07-30):** `swift build` with CLT
300 +only compiles all C++/Swift then **fails** at `CompileMetalFile
301 +steel_attention.metal` — `unable to spawn process 'metal'`. The CLT ships no
302 +Metal compiler; no prebuilt-metallib distribution of mlx-swift exists yet
303 +(PR #430 / mlx#3597 / mlx-swift#416 in flight, not landed). The Metal Toolchain
304 +is an Xcode 26+ downloadable component (`xcodebuild -downloadComponent
305 +metalToolchain`) and cannot be installed with CLT alone.
306 +**→ Adapted rule per charter 0.A.6: install full Xcode.app strictly as a
307 +toolchain, drive everything via command-line `xcodebuild`/`swift build`
308 +from the Makefile. No IDE. No hand-authored `.xcodeproj`.**
309 +The app bundle must ship the **`mlx-swift_Cmlx.bundle`** resource bundle
310 +(contains `default.metallib`); existence proof of a signed/notarized
311 +xcodebuild-built mlx-swift app: sfomuseum/Docent (mlx-swift issue #345).
312 +
313 +**Execution model (the 0.A.6 decision):**
314 +
315 +1. **Native Swift (in-process)** — LLM/VLM/embeddings inference, Stable
316 + Diffusion image gen, standard HF→MLX conversion + affine/mxfp4 quantization
317 + of safetensors LLMs, adapter load/unload/fuse in memory.
318 +2. **Embedded Python venv (out-of-process via `PythonRunner`)** — fine-tuning
319 + (LoRA/QLoRA/DoRA/full via `mlx_lm.lora` API with a custom
320 + `TrainingCallback` → JSON-lines protocol), adapter fuse-to-disk + GGUF
321 + export, advanced quantization (AWQ/DWQ/GPTQ/mixed), `.bin` conversions,
322 + Whisper STT, mlx-audio TTS, FLUX image gen, evaluation
323 + (`--test` ppl, `mlx_lm.evaluate`).
324 + - Venv: **Python 3.12 pinned** (mlx wheels are cp310–cp314,
325 + macOS ≥ 14 arm64-only — aligns with the Apple-Silicon gate), provisioned
326 + with **uv** into `~/Library/Application Support/ZyquoMLX/py/` on first
327 + run (never inside the signed bundle; never touching system Python).
328 + Pins: `mlx-lm==0.31.3`, `mlx-vlm==0.6.8`, `mlx-whisper==0.4.3`,
329 + `mlx-audio==0.4.6` (+ lockfile via `uv pip compile`); uv supports
330 + `--offline` + local wheel cache for repair-offline behavior.
331 + - Progress protocol: our own JSON lines on stdout emitted by pinned helper
332 + scripts (`PyBridge/scripts/`) — **never scrape mlx-lm's stdout**
333 + (format changed between 0.31.3 and main; see TRAINING-RESEARCH.md §5).
334 +
335 +---
336 +
337 +## 7. Memory & Performance
338 +
339 +- **Inference RAM ≈ weights + KV cache + overhead.** Weights: 4-bit affine
340 + gs64 ≈ 0.5625 B/param (verified against real repo sizes, see MODELS.md);
341 + 8-bit ≈ 1.07; bf16 ≈ 2.0. KV cache (fp16):
342 + `2 × layers × kvHeads × headDim × seqLen × 2 bytes` — e.g. Qwen3-8B
343 + (36L, 8 KV heads, 128 dim) at 8k ctx ≈ 1.2 GB; quantizable via
344 + `kvBits`/`kvScheme`. (Formula standard practice; per-model numbers
345 + *UNVERIFIED* against one official doc — `MemoryAdvisor` computes from
346 + config.json fields and calibrates in Phase 7.)
347 +- **`Memory.memoryLimit` defaults to 1.5× `maxRecommendedWorkingSetSize`**
348 + (verified doc comment); macOS caps GPU-visible memory ≈ 66–75% of unified
349 + RAM — query `GPU.deviceInfo().maxRecommendedWorkingSetSize` at runtime for
350 + exact gating rather than hardcoding.
351 +- **Training memory**: LoRA ≈ 2–4× the inference footprint of the same base
352 + (optimizer state + activations); QLoRA on a 4-bit base is the 16–32 GB
353 + path; full FT ≈ 8 B/param + activations. Full tables in
354 + TRAINING-RESEARCH.md §6 and MODELS.md §3.
355 +- **Throughput** (third-party 2026 benchmarks, *indicative, UNVERIFIED
356 + first-party*): M4 Max ≈ 62 tok/s on 8B-4bit, ≈ 38 tok/s on 14B-4bit;
357 + M4 Air 16 GB ≈ 25–35 tok/s on 9B-4bit; generation is
358 + memory-bandwidth-bound. First-party per-run stats come from
359 + `GenerateCompletionInfo` / `GenerationResponse`.
360 +- **This Mac (dev machine): M5 Max, 48 GB** → comfortable up to ~35B 4-bit
361 + inference, QLoRA ≤ 32B, LoRA ≤ 14B, full FT ≤ ~3–4B.
362 +
363 +---
364 +
365 +## 8. Dependency Decision (feeds Phase 1 `Package.swift`)
366 +
367 +```swift
368 +.package(url: "https://github.com/ml-explore/mlx-swift", .upToNextMinor(from: "0.31.6")),
369 +.package(url: "https://github.com/ml-explore/mlx-swift-lm", .upToNextMajor(from: "3.31.4")),
370 +.package(url: "https://github.com/huggingface/swift-huggingface", from: "0.9.0"),
371 +.package(url: "https://github.com/huggingface/swift-transformers", from: "1.3.0"),
372 +// StableDiffusion (image gen) from mlx-swift-examples ("mlx-libraries") — add in Phase 6 if SD ships in v1
373 +```
374 +
375 +Platform floor: macOS 14 (mlx-swift Package.swift) — Zyquo MLX targets
376 +**macOS 14+, arm64 only**. Note: mlx-swift `main` uses
377 +`swift-tools-version: 6.3 (experimentalCGen)` — depend on **released tags
378 +only**, never branch `main`.
379 +
380 +### Primary sources
381 +
382 +github.com/ml-explore/{mlx, mlx-swift, mlx-swift-lm, mlx-swift-examples,
383 +mlx-lm, mlx-examples} (releases API + raw files at main) ·
384 +ml-explore.github.io/mlx (lazy eval, unified memory, quantize docs) ·
385 +pypi.org/pypi/{mlx, mlx-lm, mlx-vlm, mlx-whisper, mlx-audio}/json ·
386 +github.com/huggingface/{swift-transformers, swift-huggingface} ·
387 +github.com/Blaizzy/{mlx-vlm, mlx-audio} · mlx-swift PR #430, issues #345/#349 ·
388 +local empirical build test (this Mac, 2026-07-30).
added docs/MODELS.md +229 −0
@@ -0,0 +1,229 @@
1 +<!--
2 + MODELS.md
3 + Zyquo MLX
4 +
5 + Author: Simon-Pierre Boucher
6 + Mail: contact@spboucher.ai
7 +-->
8 +
9 +# Zyquo MLX — Hub Integration & Curated Model Catalog
10 +
11 +> Phase 0.C research document. Every catalog row was **live-verified against the
12 +> Hugging Face Hub API on 2026-07-30** (HTTP 200, sizes summed from real tree
13 +> listings). Anything computed rather than measured is marked *estimated*.
14 +
15 +---
16 +
17 +## 1. Hugging Face Hub HTTP API
18 +
19 +### 1.1 Search — `GET https://huggingface.co/api/models`
20 +
21 +Verified working parameter set:
22 +
23 +```
24 +?author=mlx-community&pipeline_tag=text-generation&filter=mlx
25 +&search=Qwen3&sort=downloads&direction=-1&limit=100&full=true&config=true
26 +```
27 +
28 +| Param | Behavior (verified) |
29 +|---|---|
30 +| `search` | substring match on repo name |
31 +| `author` | org/user scope (`mlx-community`, `lmstudio-community`, …) |
32 +| `filter` | tag filter; `filter=mlx` matches the MLX library tag across all orgs |
33 +| `pipeline_tag` | `text-generation`, `image-text-to-text` (VLM), `feature-extraction` (embeddings), `automatic-speech-recognition`, `image-to-image` |
34 +| `sort` + `direction=-1` | `downloads`, `likes`, `lastModified`, `trendingScore` (default) |
35 +| `limit` | up to **1000** per page; pagination via `Link: <…cursor=…>; rel="next"` response header (cursor-based) |
36 +| `full=true` | adds `siblings` (filenames only — **no sizes**) + `sha` |
37 +| `config=true` | adds `config` incl. `quantization_config` (bits/group_size), `architectures`, `model_type` |
38 +
39 +List-item fields: `id, author, downloads (30-day rolling), likes, tags, gated,
40 +lastModified, createdAt, pipeline_tag, library_name, private`.
41 +`safetensors` parameter counts are **not** in search results — only on the
42 +per-model endpoint.
43 +
44 +**Rate limits (anonymous, from live response headers):**
45 +`ratelimit-policy: "fixed window";"api";q=500;w=300` → **500 API requests per
46 +5 minutes**; `ratelimit: "api";r=…;t=…` gives remaining/reset. Design
47 +`HubService` to batch (one 1000-item search call) and cache.
48 +
49 +### 1.2 Model info & file listing
50 +
51 +- `GET /api/models/{repo_id}``sha` (commit, for revision pinning),
52 + `siblings`, `usedStorage`, `safetensors: {parameters: {BF16: n, U32: n}, total}`,
53 + `gated` (`false` | `"manual"`), `cardData`.
54 +- ⚠️ **Quantized-parameter quirk (verified):** MLX packs 4-bit weights as U32,
55 + so `safetensors.total` undercounts ~8×. Real params ≈ `BF16 + U32 × 8`
56 + (Qwen3-4B-4bit: 125.9M BF16 + 502.8M U32 → ≈ 4.1B ✓). `ModelStore` must use
57 + this formula or parse the repo name.
58 +- `GET /api/models/{repo_id}/tree/main?recursive=true` → per-file
59 + `{type, path, size, lfs: {oid, size, pointerSize}}`.
60 + - **Not recursive by default** — always pass `recursive=true` (FLUX repos
61 + keep components in subdirectories).
62 + - `size` is always the true byte size; `lfs.oid` is the file's SHA-256 —
63 + use it as the integrity check after download.
64 + - Revision pinning: `/tree/{commit_sha}?recursive=true`. 1000 entries per
65 + page, `Link rel="next"` beyond.
66 +
67 +### 1.3 Download / resolve (drives `DownloadManager`)
68 +
69 +- `GET https://huggingface.co/{repo}/resolve/{revision}/{filename}`**302**
70 + to a signed, expiring CDN URL (Xet bridge, `us.aws.cdn.hf.co/xet-bridge-us/…`).
71 + Small files (e.g. `config.json`) → **307** to `/api/resolve-cache/…`.
72 +- `HEAD` on the resolve URL (before redirect) yields: `x-linked-size` (exact
73 + size), `x-linked-etag` (content SHA-256), `x-repo-commit` (resolved commit —
74 + **pin all subsequent files of a download to it** for a consistent snapshot),
75 + `accept-ranges: bytes`.
76 +- **Resume (verified):** `Range: bytes={downloaded}-` on a *fresh* resolve
77 + request (CDN URLs expire — never persist them) → HTTP 206 with
78 + `content-range`. This is the resumable-download contract.
79 +- **Auth / gated repos:** `Authorization: Bearer hf_…` works on both `/api/*`
80 + and `/resolve/*`. Gated repos report `gated: "manual"` and return 401
81 + anonymously (verified with `meta-llama/Llama-3.1-8B-Instruct`). All
82 + `mlx-community` catalog repos verified `gated: false`.
83 +
84 +### 1.4 MLX ecosystem conventions (census of top-1000 mlx-community repos)
85 +
86 +- Tags: library tag `mlx`, region tags `4-bit`/`8-bit`,
87 + `base_model:quantized:{original-repo}`.
88 +- Name-suffix census (top-1000 by downloads): `-4bit` ×400, `-8bit` ×176,
89 + `-bf16` ×104, `-mlx`/`-MLX` ×99, `-6bit` ×70, `-qat` ×50, `mxfp4` ×33,
90 + `-5bit` ×24, `-3bit` ×22, `-DWQ` ×18, `-fp16` ×16, whisper-style `-q4/-q8` ×17.
91 +- `config.json` `"quantization"`: `{bits, group_size, mode}` — 2026 repos say
92 + `"affine"`; `"mxfp4"` (gpt-oss, group_size 32) and mixed recipes
93 + (`MXFP4-Q8`, `DQ4plus-q8`) also occur. `-DWQ` = distilled-weight quantization
94 + (same size, better quality).
95 +- Ecosystem state mid-2026 (from live download charts): current generations
96 + are **Qwen3.5 / Qwen3.6, gemma-4, GLM-4.7/5.2, Kimi-K2.5/K2.6, DeepSeek-V4,
97 + Mistral-Small-4, FLUX.2-Klein, Qwen3-TTS/ASR, parakeet v3**. No Llama-4 in
98 + mlx-community. Many new text models carry `pipeline_tag: image-text-to-text`
99 + (natively multimodal).
100 +
101 +---
102 +
103 +## 2. Featured Catalog (live-verified 2026-07-30)
104 +
105 +Disk = exact sum of weight files from the live tree listing.
106 +Min RAM = weights + ~20% overhead + OS headroom, rounded to a Mac tier
107 +(*estimated — calibrate in Phase 7*). Downloads = 30-day count at verification.
108 +
109 +### 2.1 Text LLMs — small (0.5–3B)
110 +
111 +| Repo | Params | Quant | Disk | Min RAM | DLs |
112 +|---|---|---|---|---|---|
113 +| `mlx-community/Qwen3-0.6B-4bit` | 0.6B | 4-bit gs64 | 0.34 GB | 8 GB | 49.9k |
114 +| `mlx-community/Llama-3.2-1B-Instruct-4bit` | 1B | 4-bit gs64 | 0.70 GB | 8 GB | 48.3k |
115 +| `mlx-community/gemma-3-1b-it-qat-4bit` | 1B | 4-bit gs64 QAT | 0.73 GB | 8 GB | 33.2k |
116 +| `mlx-community/Qwen3-1.7B-4bit` | 1.7B | 4-bit gs64 | 0.97 GB | 8 GB | 14.1k |
117 +| `mlx-community/SmolLM3-3B-4bit` | 3B | 4-bit gs64 | 1.73 GB | 8 GB | 2.0k |
118 +| `mlx-community/Llama-3.2-3B-Instruct-4bit` | 3B | 4-bit gs64 | 1.81 GB | 8 GB | 21.1k |
119 +
120 +### 2.2 Text LLMs — mid (4–9B)
121 +
122 +| Repo | Params | Quant | Disk | Min RAM | DLs |
123 +|---|---|---|---|---|---|
124 +| `mlx-community/Qwen3-4B-Instruct-2507-4bit` | 4B | 4-bit gs64 | 2.26 GB | 8 GB | 41.6k |
125 +| `mlx-community/Mistral-7B-Instruct-v0.3-4bit` | 7B | 4-bit gs64 | 4.08 GB | 16 GB | 12.1k |
126 +| `mlx-community/Qwen2.5-Coder-7B-Instruct-4bit` | 7B | 4-bit gs64 | 4.28 GB | 16 GB | 30.7k |
127 +| `mlx-community/Llama-3.1-8B-Instruct-4bit` | 8B | 4-bit gs64 | 4.52 GB | 16 GB | 27.1k |
128 +| `mlx-community/Qwen3-8B-4bit` | 8B | 4-bit gs64 | 4.61 GB | 16 GB | 32.3k |
129 +| `mlx-community/gemma-4-e4b-it-4bit` | ~8B (eff. 4B) | 4-bit gs64 affine | 5.15 GB | 16 GB | 61.6k |
130 +| `mlx-community/Qwen3.5-9B-4bit` | 9B | 4-bit gs64 affine | 5.95 GB | 16 GB | 22.8k |
131 +
132 +### 2.3 Text LLMs — large (14–35B, incl. MoE)
133 +
134 +| Repo | Params | Quant | Disk | Min RAM | DLs |
135 +|---|---|---|---|---|---|
136 +| `mlx-community/Qwen3-14B-4bit` | 14B | 4-bit gs64 | 8.31 GB | 16 GB | 40.3k |
137 +| `mlx-community/DeepSeek-R1-Distill-Qwen-14B-4bit` | 14B | 4-bit gs64 | 8.31 GB | 16 GB | 41.8k |
138 +| `mlx-community/gpt-oss-20b-MXFP4-Q8` | 20.9B MoE | MXFP4 gs32 + Q8 | 12.08 GB | 24 GB | 355.3k |
139 +| `mlx-community/Mistral-Small-3.1-24B-Instruct-2503-4bit` | 24B | 4-bit gs64 | 14.09 GB | 24 GB | 27.5k |
140 +| `mlx-community/Devstral-Small-2-24B-Instruct-2512-4bit` | 24B | 4-bit gs64 affine | 15.10 GB | 24 GB | 114.0k |
141 +| `mlx-community/Qwen3.6-27B-4bit` | 27B | 4-bit gs64 affine | 16.05 GB | 32 GB | 50.7k |
142 +| `mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit` | 30B-A3B MoE | 4-bit gs64 | 17.18 GB | 32 GB | 83.8k |
143 +| `mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit` | 30B-A3B MoE | 4-bit gs64 | 17.18 GB | 32 GB | 7.4k |
144 +| `mlx-community/gemma-4-31b-it-4bit` | 31B | 4-bit gs64 affine | 18.41 GB | 32 GB | 47.5k |
145 +| `mlx-community/Qwen3-32B-4bit` | 32B | 4-bit gs64 | 18.43 GB | 32 GB | 2.1k |
146 +| `mlx-community/DeepSeek-R1-Distill-Qwen-32B-4bit` | 32B | 4-bit gs64 | 18.43 GB | 32 GB | 8.2k |
147 +| `mlx-community/Qwen3.6-35B-A3B-4bit` | 35B-A3B MoE | 4-bit gs64 affine | 20.40 GB | 32 GB | 62.0k |
148 +
149 +MoE note: RAM is set by **total** params (all experts resident); speed by
150 +active params. `Qwen3-30B-A3B` needs a 32 GB Mac but generates at ~3B speed —
151 +the ideal recommendation for 32 GB machines.
152 +
153 +### 2.4 Vision-language models
154 +
155 +| Repo | Params | Quant | Disk | Min RAM | DLs |
156 +|---|---|---|---|---|---|
157 +| `mlx-community/Qwen3-VL-4B-Instruct-4bit` | 4B | 4-bit gs64 affine | 3.09 GB | 16 GB | 17.5k |
158 +| `mlx-community/SmolVLM2-2.2B-Instruct-mlx` | 2.2B | bf16 | 4.49 GB | 16 GB | 365 |
159 +| `mlx-community/Qwen2.5-VL-7B-Instruct-4bit` | 7B | 4-bit gs64 | 5.64 GB | 16 GB | 3.6k |
160 +| `mlx-community/Qwen3-VL-8B-Instruct-4bit` | 8B | 4-bit gs64 affine | 5.76 GB | 16 GB | 2.3k |
161 +| `mlx-community/pixtral-12b-4bit` | 12B | 4-bit gs64 | 7.14 GB | 16 GB | 276 |
162 +| `mlx-community/gemma-3-12b-it-qat-4bit` | 12B | 4-bit gs64 QAT | 8.03 GB | 24 GB | 31.8k |
163 +
164 +### 2.5 Embeddings (MLX format)
165 +
166 +| Repo | Params | Quant | Disk | Min RAM | DLs |
167 +|---|---|---|---|---|---|
168 +| `mlx-community/all-MiniLM-L6-v2-4bit` | 22M | 4-bit gs64 | 0.01 GB | 8 GB | 1.7k |
169 +| `mlx-community/nomicai-modernbert-embed-base-bf16` | 149M | bf16 | 0.30 GB | 8 GB | 5.2k |
170 +| `mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ` | 0.6B | 4-bit DWQ | 0.34 GB | 8 GB | 15.3k |
171 +| `mlx-community/bge-m3-mlx-fp16` | 568M | fp16 | 1.14 GB | 8 GB | 11.2k |
172 +| `mlx-community/Qwen3-Embedding-4B-4bit-DWQ` | 4B | 4-bit DWQ | 2.26 GB | 8 GB | 2.4k |
173 +
174 +### 2.6 Speech
175 +
176 +| Repo | Params | Quant | Disk | Min RAM | DLs |
177 +|---|---|---|---|---|---|
178 +| `mlx-community/Qwen3-ASR-0.6B-8bit` | 0.6B | 8-bit gs64 affine | 1.01 GB | 8 GB | 75.3k |
179 +| `mlx-community/whisper-large-v3-turbo` | 0.8B | fp16 | 1.61 GB | 8 GB | 77.8k |
180 +| `mlx-community/parakeet-tdt-0.6b-v3` | 0.6B | bf16 | 2.51 GB | 8 GB | 1.33M |
181 +| `mlx-community/whisper-large-v3-mlx` | 1.5B | fp16 | 3.08 GB | 8 GB | 26.6k |
182 +
183 +### 2.7 Image generation
184 +
185 +| Repo | Type | Quant | Disk | Min RAM | DLs |
186 +|---|---|---|---|---|---|
187 +| `mlx-community/FLUX.2-Klein-4B-4bit` | FLUX.2 Klein 4B | 4-bit (components in subdirs) | 4.61 GB | 16 GB | 575 |
188 +| `argmaxinc/mlx-FLUX.1-schnell-4bit-quantized` | FLUX.1-schnell (DiffusionKit) | 4-bit | 7.03 GB | 16 GB | 7.3k |
189 +| `dhairyashil/FLUX.1-schnell-mflux-4bit` | FLUX.1-schnell (mflux) | 4-bit | 9.61 GB | 24 GB | 606 |
190 +
191 +No stable-diffusion MLX repos exist in `mlx-community` (live search returned
192 +zero). The mflux ecosystem lives under `mflux-community/` — pick per
193 +mflux-version compatibility at implementation time.
194 +
195 +**Verification total: 43 repos checked, 0 failures, all `gated: false`.**
196 +
197 +---
198 +
199 +## 3. RAM Guidance Table (powers `MemoryAdvisor` badges)
200 +
201 +*Estimated — arithmetic + community guidance; calibrate on real runs in Phase 7.*
202 +
203 +Basis: 4-bit gs64 affine = 4 + 32/64 bits/weight = **4.5 bits ≈ 0.5625 B/param**
204 +(confirmed by catalog: Qwen3-14B → 0.561 B/p, Qwen3-32B → 0.562 B/p);
205 +bf16 = 2 B/param. macOS GPU working-set ceiling ≈ 70–75% of unified RAM; keep
206 +model + KV cache + activations ≤ ~(RAM − 5 GB). KV cache ≈ 0.5–2 GB at 8k ctx
207 +for 7–32B GQA models. LoRA (bf16 base) ≈ 2 B/param + activations + adapter
208 +optimizer state. QLoRA (4-bit frozen base) ≈ 0.5625 B/param + activations
209 +(~2–5 GB at batch 1–4, seq 1–2k, grad checkpointing on).
210 +
211 +| Mac RAM | 4-bit inference (realistic max) | LoRA FT (bf16 base) | QLoRA FT (4-bit base) |
212 +|---|---|---|---|
213 +| 8 GB | ≤3B comfortable; 4B tight | ≤0.6B | ≤1.7B |
214 +| 16 GB | 7–9B comfortable; 14B ok w/ modest ctx | ≤3B | ≤7–8B |
215 +| 24 GB | 14B comfortable; 20B MoE & 24B ok | ≤7B (tight) | ≤14B |
216 +| 32 GB | 27–32B dense & 30B/35B-A3B MoE | ≤8–9B | 14B comfortable; 24B tight |
217 +| 48 GB | 32–35B comfortable + long ctx; ~50B-class | ≤14B | ≤32B |
218 +| 64 GB | 70B 4-bit (~39.4 GB; `Llama-3.3-70B-Instruct-4bit` verified to exist) | ≤24B | 32B comfortable |
219 +| 96 GB | 70B comfortable; ~100–120B MoE | ≤32B | ≤70B (tight) |
220 +| 128 GB | 120B-class MoE (e.g. `Mistral-Small-4-119B-2603-4bit`, exists); 70B 8-bit | ≤35B dense | 70B comfortable; ~120B MoE possible |
221 +
222 +**This Mac (dev machine): 48 GB** → 4-bit inference up to ~35B comfortable;
223 +LoRA ≤14B; QLoRA ≤32B.
224 +
225 +Caveats for `MemoryAdvisor`:
226 +1. Min-RAM columns are computed, not measured — Phase 7 calibrates them.
227 +2. `downloads` is a 30-day rolling count (freshness signal, not lifetime).
228 +3. For quantized repos derive real params as `BF16 + U32 × 8` from the
229 + `safetensors` field, or parse the name suffix.
modified docs/PLAN.md +40 −26
@@ -13,28 +13,37 @@ Strict phase order 0 → 8. One phase at a time. Each phase ends with a checkpoi
13 13
14 14 ---
15 15
16 ## Phase 0 — Mandatory Intensive Web Research (in progress)
17
18 - [ ] 0.A `docs/MLX-RESEARCH.md` — MLX framework deep dive
19 - [ ] Core primitives: arrays, lazy eval / `eval()`, unified memory, streams/devices, autodiff
20 - [ ] Swift vs. Python honest capability split (current, verified against live repos)
21 - [ ] Model types & current MLX model zoo (LLM / VLM / embeddings / speech / image-gen)
22 - [ ] Inference API specifics (Swift-first: loading, tokenizers, streaming, params, KV cache, memory release)
23 - [ ] Quantization & conversion (tools, exact commands, on-disk format)
24 - [ ] Build & execution strategy resolved (Metal toolchain question TESTED locally)
25 - [ ] Memory & performance (RAM estimation for inference vs. training)
26 - [ ] 0.B `docs/TRAINING-RESEARCH.md` — fine-tuning on MLX
27 - [ ] LoRA / QLoRA exact workflow, hyperparameters, adapter save/fuse
28 - [ ] Full fine-tuning feasibility & memory cost
29 - [ ] Dataset formats (chat / prompt-completion / text), split, templating, validation
30 - [ ] Training observability: metrics, checkpoint cadence, resume
31 - [ ] Evaluation: held-out loss/perplexity, base vs. tuned comparison
32 - [ ] 0.C `docs/MODELS.md` — Hub integration + curated catalog
33 - [ ] Hugging Face Hub HTTP API (search, info, tree, resolve, LFS, token)
34 - [ ] Featured catalog across types/sizes, live-verified `mlx-community` repo IDs
35 - [ ] RAM table (8–128 GB) for inference AND LoRA fine-tuning
36 - [ ] `docs/BUILD.md` — no-Xcode-IDE build recipe incl. Metal (tested on this Mac)
37 - [ ] Phase checkpoint: docs complete, traceable, committed
16 +## Phase 0 — Mandatory Intensive Web Research ✅ (completed 2026-07-30)
17 +
18 +- [x] 0.A `docs/MLX-RESEARCH.md` — MLX framework deep dive
19 + - [x] Core primitives: arrays, lazy eval / `eval()`, unified memory, streams/devices, autodiff
20 + - [x] Swift vs. Python honest capability split (current, verified against live repos)
21 + - [x] Model types & current MLX model zoo (LLM / VLM / embeddings / speech / image-gen)
22 + - [x] Inference API specifics (Swift-first: loading, tokenizers, streaming, params, KV cache, memory release)
23 + - [x] Quantization & conversion (tools, exact commands, on-disk format)
24 + - [x] Build & execution strategy resolved (Metal toolchain question TESTED locally)
25 + - [x] Memory & performance (RAM estimation for inference vs. training)
26 +- [x] 0.B `docs/TRAINING-RESEARCH.md` — fine-tuning on MLX
27 + - [x] LoRA / QLoRA exact workflow, hyperparameters, adapter save/fuse
28 + - [x] Full fine-tuning feasibility & memory cost
29 + - [x] Dataset formats (chat / prompt-completion / text), split, templating, validation
30 + - [x] Training observability: metrics, checkpoint cadence, resume
31 + - [x] Evaluation: held-out loss/perplexity, base vs. tuned comparison
32 +- [x] 0.C `docs/MODELS.md` — Hub integration + curated catalog
33 + - [x] Hugging Face Hub HTTP API (search, info, tree, resolve, LFS, token)
34 + - [x] Featured catalog across types/sizes, live-verified `mlx-community` repo IDs (43 repos, 0 failures)
35 + - [x] RAM table (8–128 GB) for inference AND LoRA fine-tuning
36 +- [x] `docs/BUILD.md` — no-Xcode-IDE build recipe incl. Metal (tested on this Mac)
37 +- [x] Phase checkpoint: docs complete, traceable, committed
38 +
39 +**Phase 0 summary:** Verified the mid-2026 MLX ecosystem live (mlx 0.32.0,
40 +mlx-swift 0.31.6, mlx-swift-lm 3.31.4, mlx-lm 0.31.3): LM libraries moved to
41 +`ml-explore/mlx-swift-lm` 3.x, which now natively covers LLM/VLM/embeddings
42 +inference, LoRA/QLoRA training, and safetensors conversion/quantization; the
43 +Python bridge is required only for rich-dataset training, full FT, speech,
44 +FLUX, advanced quant, and evaluation. Locally proved CLT-only builds fail at
45 +Metal kernel compilation → strategy: full Xcode as toolchain, CLI-only builds,
46 +`mlx-swift_Cmlx.bundle` shipped in the app. Catalog: 43 models live-verified.
38 47
39 48 ### Local ground truth (recorded 2026-07-30)
40 49
@@ -42,10 +51,15 @@ Strict phase order 0 → 8. One phase at a time. Each phase ends with a checkpoi
42 51 - Swift 6.4 (swiftlang-6.4.0.25.4), **Command Line Tools only** at
43 52 `/Library/Developer/CommandLineTools``xcodebuild` unavailable, **no `metal`
44 53 compiler in CLT**
45 - Evidence from a prior controlled test (`/private/tmp/mlx-buildtest`,
46 mlx-swift 0.31.6): the mlx-swift build invokes
47 `metal -c -target air64-apple-macos14.0 … steel_attention.metal` — i.e. Metal
48 kernel compilation happens at build time and needs a real Metal toolchain
54 +- **TESTED 2026-07-30 (clean run, `/tmp/zyquo-mlx-buildtest`, mlx-swift 0.31.x):**
55 + `swift build` with CLT only compiles all of Cmlx C++/Swift, then fails at
56 + `CompileMetalFile steel_attention.metal` with
57 + `error: unable to spawn process 'metal' (No such file or directory)`.
58 + → CLT-only build is IMPOSSIBLE; a real Metal toolchain (full Xcode, CLI-only
59 + usage) is required. Per charter 0.A.6 the rule adapts to: command-line
60 + `swift build`/`xcodebuild` only, no Xcode IDE, no hand-authored `.xcodeproj`.
61 +- No Xcode.app present anywhere on this machine yet → Phase 1 must provision the
62 + Xcode toolchain (download strategy documented in `docs/BUILD.md`)
49 63 - Python 3.14.4 (Homebrew) + `uv` available; `rsvg-convert` + `iconutil` present
50 64 for the Phase 5 icon pipeline
51 65
added docs/TRAINING-RESEARCH.md +384 −0
@@ -0,0 +1,384 @@
1 +<!--
2 + TRAINING-RESEARCH.md
3 + Zyquo MLX
4 +
5 + Author: Simon-Pierre Boucher
6 + Mail: contact@spboucher.ai
7 +-->
8 +
9 +# Zyquo MLX — Fine-Tuning Research (MLX, Ground Truth 2026-07-30)
10 +
11 +> Phase 0.B research document. Verified against live sources: `ml-explore/mlx-lm`
12 +> main branch (version `0.31.3`, latest PyPI release 0.31.3, 2026-04-22) and
13 +> `ml-explore/mlx-swift-lm` main. All code excerpts were fetched raw from GitHub
14 +> on 2026-07-30. Items not verifiable against a primary source are marked
15 +> *UNVERIFIED*.
16 +
17 +---
18 +
19 +## 1. LoRA / QLoRA Workflow (mlx-lm, Python)
20 +
21 +### 1.1 Entry points
22 +
23 +- CLI: `mlx_lm.lora …` or `python -m mlx_lm lora …`
24 + (`python -m mlx_lm.lora` prints a deprecation notice but works).
25 +- Training extras: `pip install "mlx-lm[train]"`.
26 +- Minimal run: `mlx_lm.lora --model <hf-repo-or-local-dir> --train --data <dir> --iters 600`.
27 +- YAML config: `mlx_lm.lora -c config.yaml`; **CLI flags override config values**,
28 + config overrides `CONFIG_DEFAULTS`.
29 +- Python API (what `PythonRunner` scripts will use):
30 + `mlx_lm.tuner.trainer.{TrainingArgs, train, evaluate}`,
31 + `mlx_lm.tuner.utils.{linear_to_lora_layers, load_adapters, build_schedule}`,
32 + `mlx_lm.tuner.datasets.{load_dataset, CacheDataset}`;
33 + `mlx_lm.lora.run(args, training_callback)` mirrors the CLI programmatically.
34 +
35 +Source: `mlx_lm/lora.py` (main).
36 +
37 +### 1.2 Complete hyperparameter table (exact names + defaults, `CONFIG_DEFAULTS`)
38 +
39 +| CLI flag | Config key | Default | Notes |
40 +|---|---|---|---|
41 +| `--model` | `model` | `"Qwen/Qwen3-0.6b"` | HF repo or local converted dir |
42 +| `--train` | `train` | `False` | |
43 +| `--data` | `data` | `"mlx-community/WikiSQL"` | dir with `{train,valid,test}.jsonl` or HF dataset |
44 +| `--fine-tune-type` | `fine_tune_type` | `"lora"` | `lora` \| `dora` \| `full` |
45 +| `--optimizer` | `optimizer` | `"adam"` | `adam`, `adamw`, `muon`, `sgd`, `adafactor` |
46 +| *(config only)* | `optimizer_config` | `{}` per optimizer | e.g. `adamw: {betas:[0.9,0.98], eps:1e-6, weight_decay:0.05}` |
47 +| `--mask-prompt` | `mask_prompt` | `False` | loss on completion only; chat & completions formats only |
48 +| `--num-layers` | `num_layers` | `16` | `-1` = all layers; LoRA applies to the **last** N layers |
49 +| `--batch-size` | `batch_size` | `4` | |
50 +| `--iters` | `iters` | `1000` | |
51 +| `--val-batches` | `val_batches` | `25` | `-1` = full valid set |
52 +| `--learning-rate` | `learning_rate` | `1e-5` | |
53 +| `--steps-per-report` | `steps_per_report` | `10` | |
54 +| `--steps-per-eval` | `steps_per_eval` | `200` | also evals at iter 1 and final iter |
55 +| `--grad-accumulation-steps` | `grad_accumulation_steps` | `1` | averaged accumulation |
56 +| `--resume-adapter-file` | `resume_adapter_file` | `None` | see §5.3 — warm start only |
57 +| `--adapter-path` | `adapter_path` | `"adapters"` | |
58 +| `--save-every` | `save_every` | `100` | checkpoint cadence |
59 +| `--test` | `test` | `False` | eval on `test.jsonl` |
60 +| `--test-batches` | `test_batches` | `500` | `-1` = full test set |
61 +| `--max-seq-length` | `max_seq_length` | `2048` | longer sequences truncated with warning |
62 +| `--grad-checkpoint` | `grad_checkpoint` | `False` | per-layer `mx.checkpoint` |
63 +| `--clear-cache-threshold` | `clear_cache_threshold` | `0` | e.g. `4GB`; calls `mx.clear_cache()` above it |
64 +| `--report-to` | `report_to` | `None` | `wandb`, `swanlab`, or both |
65 +| `--seed` | `seed` | `0` | `mx.random.seed` + `np.random.seed` |
66 +| `--trust-remote-code` | `trust_remote_code` | `False` | |
67 +| *(config only)* | `lora_parameters` | `{rank: 8, dropout: 0.0, scale: 20.0}` | plus optional `keys: […]`; **not settable via CLI** |
68 +| *(config only)* | `lr_schedule` | `None` | `{name, warmup, warmup_init, arguments}` |
69 +| *(config only)* | `hf_dataset` | — | see §4.3 |
70 +
71 +**`lora_parameters` details:** `rank` (8), `scale` (20.0 — MLX exposes a single
72 +`scale` factor, *not* alpha/rank), `dropout` (0.0), `keys` (target modules;
73 +example yaml uses `["self_attn.q_proj", "self_attn.v_proj"]`). If `keys` is
74 +omitted, `linear_to_lora_layers` adapts **all** Linear/QuantizedLinear/
75 +SwitchLinear/Embedding modules in each targeted layer.
76 +→ Zyquo's hyperparameter form maps: rank, scale, dropout, keys, num-layers,
77 +LR, batch size, iters, seed, max-seq-length, grad-checkpoint, optimizer,
78 +mask-prompt, save-every, steps-per-eval — and must generate a **YAML config**
79 +for `lora_parameters`/`lr_schedule` since those have no CLI flags.
80 +
81 +**`lr_schedule`** (`build_schedule`): `name` = any `mlx.optimizers.schedulers`
82 +function (e.g. `cosine_decay`), `arguments` positional (first = initial LR),
83 +optional linear `warmup` steps with `warmup_init`.
84 +Example: `{name: cosine_decay, warmup: 100, warmup_init: 1e-7, arguments: [1e-5, 1000, 1e-7]}`.
85 +
86 +### 1.3 QLoRA
87 +
88 +No flag: **if `--model` points to a quantized model, training is QLoRA**
89 +(LORA.md). Quantized checkpoints load as `nn.QuantizedLinear`;
90 +`linear_to_lora_layers` wraps them keeping the frozen base quantized. Create a
91 +quantized base with `mlx_lm.convert --hf-path <repo> -q` or use any
92 +`mlx-community/*-4bit` repo.
93 +
94 +### 1.4 DoRA and full
95 +
96 +- **DoRA**: `--fine-tune-type dora` (`tuner/dora.py`: `DoRALinear`,
97 + `DoRAEmbedding`). MoE `SwitchLinear` layers raise "doesn't support DoRA yet".
98 +- **Full**: `--fine-tune-type full` unfreezes the last `num_layers` layers
99 + (`-1` = true full fine-tune); `lora_parameters` becomes `None`.
100 +
101 +---
102 +
103 +## 2. Adapters & Fusing
104 +
105 +### 2.1 Contents of `--adapter-path` (verified)
106 +
107 +- `adapter_config.json` — the **entire resolved training config** written at
108 + start (`save_config(vars(args))`); `load_adapters()` reads `fine_tune_type`,
109 + `num_layers`, `lora_parameters` from it to rebuild the LoRA layers.
110 +- `adapters.safetensors` — latest weights, overwritten every `save_every`
111 + iterations and at the final iteration.
112 +- Numbered checkpoints `{it:07d}_adapters.safetensors`
113 + (e.g. `0000100_adapters.safetensors`) — one per cadence, 7-digit zero-padded.
114 +- Contents = `tree_flatten(model.trainable_parameters())` — for full
115 + fine-tuning this contains all unfrozen weights (same naming).
116 +
117 +### 2.2 `mlx_lm.fuse` (verified argparse)
118 +
119 +| Flag | Default | Notes |
120 +|---|---|---|
121 +| `--model` | `"mlx_model"` | base model path/repo |
122 +| `--save-path` | `"fused_model"` | |
123 +| `--adapter-path` | `"adapters"` | |
124 +| `--upload-repo` | `None` | push to HF Hub |
125 +| `--dequantize` | off | de-quantize a QLoRA base to fp16 while fusing; strips `quantization` from config |
126 +| `--export-gguf` | off | `llama`/`mixtral`/`mistral` model types only, fp16 |
127 +| `--gguf-path` | `"ggml-model-f16.gguf"` | written inside save-path |
128 +
129 +Mechanics: loads model + adapters, calls `.fuse(dequantize:)` on every capable
130 +module, saves a complete standalone MLX model dir (config.json, sharded
131 +safetensors + index, tokenizer files) — directly loadable afterwards. Fusing
132 +into a **quantized** base without `--dequantize` re-quantizes fused weights
133 +(small quality loss vs. keeping the adapter separate) — surface this trade-off
134 +in the Convert UI. (LORA.md mentions `--hf-path` for fuse but it is absent from
135 +current argparse — docs stale; UNVERIFIED if intentional.)
136 +
137 +---
138 +
139 +## 3. Full Fine-Tuning Feasibility
140 +
141 +- Memory with Adam/AdamW: weights + grads + 2 moments, all bf16 ≈
142 + **8 bytes/param ≈ 4× bf16 weights**, plus activations
143 + (∝ batch × seq; `--grad-checkpoint` cuts these substantially).
144 +- Realistic tiers (*engineering estimates from the 8 B/param rule — UNVERIFIED,
145 + no published Apple table exists*):
146 + - 32 GB: full FT ≤ ~1.5–3B (7B with small `--num-layers` + grad-checkpoint + batch 1)
147 + - 48 GB (**this Mac**): ~3–4B full, 7B partial
148 + - 64 GB: ~7B full (tight; SGD/Adafactor shrink optimizer state)
149 + - 128 GB: ~13–15B full
150 +- Community consensus: full FT > 7B belongs on other hardware — use LoRA/QLoRA.
151 +- Trainer machinery (checkpoints, metrics, resume file) is identical to LoRA;
152 + the "adapter" file simply holds all trainable weights.
153 +
154 +---
155 +
156 +## 4. Dataset Formats (truth from `mlx_lm/tuner/datasets.py`)
157 +
158 +### 4.1 Directory layout (`--data <dir>`)
159 +
160 +`train.jsonl` (required for `--train`), `valid.jsonl` (**optional** — absent →
161 +"Warning: Validation set not found or empty. Training will proceed without
162 +validation."), `test.jsonl` (required for `--test`). One JSON object per line.
163 +
164 +### 4.2 Auto-detected formats (detection order, first sample)
165 +
166 +1. **completions** — both `prompt` and `completion` keys → `CompletionsDataset`.
167 + Keys renamable via `prompt_feature`/`completion_feature`. Internally
168 + converted to a 2-message chat and run through
169 + `tokenizer.apply_chat_template`**completions data IS chat-templated**,
170 + incl. optional per-row `tools`.
171 +2. **chat**`messages` key (renamable via `chat_feature`) → `ChatDataset`,
172 + tokenized via `apply_chat_template(messages, tools=d.get("tools"))`.
173 + A `tools` key (OpenAI function-calling schema) is passed to the template.
174 +3. **text**`text` key (renamable via `text_feature`) → raw encode, EOS
175 + appended if missing. `mask_prompt` raises `ValueError` for text datasets.
176 +
177 +### 4.3 HF datasets
178 +
179 +- `--data <hf-dataset-id>` directly if pre-formatted (`train`/`valid`/`test`
180 + splits; needs `pip install datasets`).
181 +- Config `hf_dataset:` (dict or **list** — concatenated): `path`,
182 + `train_split` (default `"train[:80%]"`), `valid_split` (default
183 + `"train[-10%:]"`), `test_split`, feature-name overrides, `config:` dict
184 + forwarded to `datasets.load_dataset`.
185 +
186 +### 4.4 `mask_prompt` (exact behavior)
187 +
188 +Each sample is `(tokens, offset)`. With `mask_prompt`, offset = token length of
189 +the template applied to all-but-last message (`add_generation_prompt=True` for
190 +completions; for chat, set when last role is `assistant`). Loss mask:
191 +`steps >= offset AND steps <= length` — only completion tokens count. Without
192 +it, offset = 0.
193 +
194 +### 4.5 Batching / truncation (`trainer.py iterate_batches`)
195 +
196 +- Samples sorted by token length, batched, batch order shuffled per epoch;
197 + dataset must have ≥ `batch_size` examples or `ValueError`.
198 +- Padding to `1 + 32·ceil(maxlen/32)` capped at `max_seq_length`; over-long
199 + sequences truncated with: `[WARNING] Some sequences are longer than
200 + {max_seq_length} tokens. The longest sentence {n} will be truncated to
201 + {max_seq_length}. Consider pre-splitting your data to save memory.`
202 +- Tokenization lazy + memoized (`CacheDataset`).
203 +- **No auto train/valid split for local JSONL** — Zyquo's `DatasetService`
204 + must produce the split files itself (this is a feature we own).
205 +
206 +---
207 +
208 +## 5. Observability
209 +
210 +### 5.1 ⚠️ Two stdout regimes exist right now — do not scrape stdout
211 +
212 +- **PyPI ≤ 0.31.3** (plain prints, flush=True):
213 + ```
214 + Iter {it}: Val loss {val_loss:.3f}, Val took {val_time:.3f}s
215 + Iter {it}: Train loss {t:.3f}, Learning Rate {lr:.3e}, It/sec {i:.3f}, Tokens/sec {tk:.3f}, Trained Tokens {n}, Peak mem {p:.3f} GB
216 + Iter {it}: Saved adapter weights to {adapter_file} and {checkpoint}.
217 + Saved final weights to {adapter_file}.
218 + ```
219 +- **main (post-0.31.3)**: `rich`-based `TrainUI` (`mlx_lm/cli_ui.py`) — ANSI
220 + panels, progress bar, columnar rows; **learning rate and peak memory no
221 + longer printed** (still in the callback dict).
222 +
223 +**Decision for Zyquo:** never parse trainer stdout. Drive training via a
224 +pinned-version Python helper that registers a custom `TrainingCallback` and
225 +emits a **JSON-lines protocol** on stdout (our own, stable).
226 +
227 +### 5.2 The stable channel: `TrainingCallback` (`tuner/callbacks.py`)
228 +
229 +- `on_train_loss_report({iteration, train_loss, learning_rate,
230 + iterations_per_second, tokens_per_second, trained_tokens, peak_memory})`
231 + (peak_memory in GB via `mx.get_peak_memory()/1e9`)
232 +- `on_val_loss_report({iteration, val_loss, val_time})`
233 +- Built-ins: `--report-to wandb|swanlab`. No built-in JSON stdout mode — we
234 + write our own driver script.
235 +
236 +Other startup lines (main): `Loading pretrained model`, `Loading datasets`,
237 +`Training`, `Trainable parameters: {pct:.3f}% ({M}M/{T}M)`; `--test` prints
238 +`Test loss {:.3f}, Test ppl {:.3f}.`
239 +
240 +### 5.3 Resume semantics (`--resume-adapter-file`)
241 +
242 +`model.load_weights(path, strict=False)` **only** — does NOT restore optimizer
243 +state, LR-schedule position, iteration counter, or RNG. Training restarts at
244 +iter 1 as a **warm start**. Prints `Loading fine-tuned weights from {path}`.
245 +→ Zyquo's `RunStore` must persist its own notion of completed iterations and
246 +present resume honestly (remaining-iters warm start), or drive the Python API
247 +directly to keep optimizer state within one process (pause = in-process, not
248 +cross-process).
249 +
250 +---
251 +
252 +## 6. Memory & Speed (LoRA/QLoRA)
253 +
254 +### 6.1 Official guidance (LORA.md "Memory Issues")
255 +
256 +QLoRA (quantized base); reduce `--batch-size` (4→2→1);
257 +`--grad-accumulation-steps` for effective batch; reduce `--num-layers`
258 +(16→8→4); pre-split long examples; `--grad-checkpoint` ("more helpful for
259 +larger batch sizes or sequence lengths with smaller or quantized models").
260 +Reference datum: Mistral-7B, batch 1, num-layers 4, M1 Max 32 GB →
261 +**~250 tokens/sec** training.
262 +
263 +### 6.2 Community data (*indicative, UNVERIFIED individually*)
264 +
265 +- QLoRA 7–8B ≈ 7 GB peak (fits 16 GB); LoRA fp16 8B ≈ 14 GB; 14B QLoRA ≈ 12 GB /
266 + LoRA ≈ 24 GB; 70B QLoRA ≈ 45 GB (64 GB+, comfortable at 96–128).
267 +- QLoRA tiers: 8 GB→≤3B, 16 GB→7–8B, 24 GB→8–14B, 32 GB→14B, 48 GB→32B,
268 + 64 GB+→32–70B. LoRA fp16 ≈ one tier down.
269 +- Training throughput: M1 Max ~250 tok/s (matches official), M3 Max ~320,
270 + M4 Max ~380, M2 Ultra ~475. `--grad-checkpoint` ≈ ~30% slower for large
271 + activation savings.
272 +- Peak-memory drivers in order: base weights (dominant), activations
273 + (∝ batch × seq² attention + hidden), trainable-layer count (LoRA adapter
274 + optimizer state is tiny — rank-8 on 7B ≈ 10–20 MB).
275 +
276 +These feed `MemoryAdvisor`'s training-side verdicts; calibrate in Phase 7.
277 +
278 +---
279 +
280 +## 7. Evaluation
281 +
282 +- **Held-out loss/perplexity**: `mlx_lm.lora --model <m> --adapter-path <a>
283 + --data <dir> --test [--test-batches N]` → `Test loss {:.3f}, Test ppl {:.3f}.`
284 + (ppl = exp(loss)). Pass `--adapter-path ""` to test the **base** — that plus
285 + a run with the adapter is the canonical base-vs-tuned scorecard. Python:
286 + `tuner.trainer.evaluate(model, dataset, batch_size, num_batches,
287 + max_seq_length)` → avg loss.
288 +- **`mlx_lm.evaluate`** = lm-evaluation-harness integration
289 + (`lm_eval.simple_evaluate`). Flags: `--model` (req), `--tasks` (req),
290 + `--output-dir`, `--batch-size` (16), `--num-shots`, `--max-tokens`,
291 + `--limit`, `--seed` (123), `--fewshot-as-multiturn`, `--apply-chat-template`,
292 + `--chat-template-args`, `--temp`/`--top-p`/`--top-k`. **No `--adapter-path`**
293 + — harness eval of an adapter requires fusing first (or API loading).
294 +- **Qualitative**: `mlx_lm.generate --model <m> --adapter-path <a> --prompt …`
295 + runs the adapted model directly, no fuse needed → powers the Playground
296 + base-vs-tuned compare.
297 +
298 +---
299 +
300 +## 8. Swift-Native Training — State Today
301 +
302 +**Repo shift (important):** `MLXLMCommon`, `MLXLLM`, `MLXVLM`, `MLXEmbedders`
303 +moved from `mlx-swift-examples` into **`ml-explore/mlx-swift-lm`** ("all
304 +updates … in the other repository"). `mlx-swift-examples` now hosts only
305 +apps/tools + `MLXMNIST`/`StableDiffusion`, depending on mlx-swift ≥ 0.31.4 and
306 +swift-transformers ≥ 1.3.0. **Zyquo MLX must depend on `mlx-swift-lm`.**
307 +
308 +### 8.1 What exists natively in Swift (verified in mlx-swift-lm main)
309 +
310 +- **`LoRATrain`** (`Libraries/MLXLLM/LoraTrain.swift`): `Parameters`
311 + (batchSize=4, iterations=1000, stepsPerReport=10, stepsPerEval=100,
312 + validationBatches=10 [0=all], saveEvery=100, adapterURL);
313 + `train(model:train:validate:optimizer:loss:tokenizer:parameters:progress:)`
314 + with `Progress` enum — `.train(iteration, trainingLoss,
315 + iterationsPerSecond, tokensPerSecond)`, `.validation(…)`, `.save(…)` — and
316 + `ProgressDisposition .stop/.more`**built-in cooperative cancellation**;
317 + `evaluate(…) -> Float`; `saveLoRAWeights(model:url:)`.
318 +- **Adapters** (`Libraries/MLXLMCommon/Adapters/LoRA/`): `LoRAConfiguration`
319 + (Codable; CodingKeys `num_layers`/`fine_tune_type`/`lora_parameters`
320 + **documented as compatible with mlx-lm's `adapter_config.json`**);
321 + `LoRAContainer.from(directory:)` loads `adapter_config.json` +
322 + `adapters.safetensors`; `.load(into:)`, `.fuse(with:)`, `.unload(from:)`.
323 + Layers: `LoRALinear`, **`QLoRALinear`** (→ **QLoRA training on quantized
324 + bases works natively in Swift**; `fused()` re-quantizes), `DoRALinear`,
325 + `QDoRALinear`; plus `PEFTAdapter.swift` (HF-PEFT format) and an adapter
326 + factory registry.
327 +- **CLI reference**: `mlx-swift-examples/Tools/llm-tool/LoraCommands.swift`
328 + train/fuse/test/eval subcommands (`--adapter`, `--layers`, `--resume`,
329 + `--data`, `--learning-rate`, `--batch-size`, `--iterations`,
330 + `--steps-per-report`, `--steps-per-eval`, `--validation-batches`,
331 + `--save-every`; fuse: `--de-quantize`).
332 +- **GUI example**: `LoRATrainingExample` — QLoRA on
333 + `mlx-community/Mistral-7B-v0.1-hf-4bit-mlx`, ~4 GB model memory / ~6 GB
334 + physical RAM.
335 +
336 +### 8.2 Swift gaps vs Python (verified)
337 +
338 +- Dataset loader: only `{"text": …}` jsonl / plain `.txt` — no chat-template/
339 + messages/completions/`mask_prompt` handling (would require applying the chat
340 + template ourselves via swift-transformers).
341 +- No gradient checkpointing, no grad accumulation, no `lr_schedule` yaml (but
342 + MLXOptimizers schedules can be attached to Adam), no dropout in LoRA params
343 + (rank/scale/keys only), no LR/peak-mem in progress payload, no numbered
344 + checkpoints (single adapterURL overwritten), no wandb, **no full fine-tuning**.
345 +- Adapter interchange: Swift uses `lora_a`/`lora_b` keys + mlx-lm-style
346 + `adapter_config.json` — **round-trip with Python adapters supported by
347 + design** (`LoRAContainer` doc).
348 +
349 +### 8.3 Execution-strategy conclusion for `TrainingService`
350 +
351 +Python (`mlx_lm.lora` via `PythonRunner` + custom `TrainingCallback` JSON
352 +protocol) is the primary training backend: chat/completions datasets,
353 +`mask_prompt`, grad-checkpoint, DoRA, full FT, numbered checkpoints, LR
354 +schedules — all Python-only today. Swift-native QLoRA (`LoRATrain` +
355 +`QLoRALinear`) is real but dataset- and feature-limited; keep it as a
356 +possible fast path later, not the Phase 3 core. Adapters interchange both ways.
357 +
358 +---
359 +
360 +## Key Implications for Zyquo MLX
361 +
362 +1. **Never parse trainer stdout** — format changed between 0.31.3 and main.
363 + Pin mlx-lm and drive `mlx_lm.lora.run(args, callback)` from our own helper
364 + script emitting JSON lines from the stable callback dicts.
365 +2. **Resume ≠ true resume** upstream. `RunStore` persists iteration counts and
366 + presents warm-start resume honestly; in-process pause/resume can be added by
367 + owning the training loop via the Python API.
368 +3. QLoRA works on quantized bases in both stacks; DoRA is Python-only for MoE.
369 +4. Depend on `ml-explore/mlx-swift-lm` (not mlx-swift-examples) for Swift LM
370 + libraries.
371 +5. The hyperparameter form must serialize a YAML config (some keys are
372 + config-only), and `DatasetService` owns train/valid splitting (mlx-lm does
373 + not auto-split local JSONL).
374 +
375 +### Primary sources
376 +
377 +`mlx-lm`: lora.py · tuner/trainer.py · tuner/datasets.py · tuner/utils.py ·
378 +tuner/callbacks.py · cli_ui.py · fuse.py · evaluate.py · LORA.md ·
379 +examples/lora_config.yaml (main + v0.31.3 tags).
380 +`mlx-swift-lm`: Libraries/MLXLLM/LoraTrain.swift ·
381 +Libraries/MLXLMCommon/Adapters/LoRA/*.
382 +`mlx-swift-examples`: README · Tools/llm-tool/LoraCommands.swift ·
383 +Applications/LoRATrainingExample. Community memory/speed figures:
384 +insiderllm.com (*UNVERIFIED*).
385