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>
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 | ||