nunspark 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. nunspark-0.1.0/.gitignore +15 -0
  2. nunspark-0.1.0/LICENSE +21 -0
  3. nunspark-0.1.0/PKG-INFO +379 -0
  4. nunspark-0.1.0/README.md +332 -0
  5. nunspark-0.1.0/pyproject.toml +41 -0
  6. nunspark-0.1.0/report.md +250 -0
  7. nunspark-0.1.0/requirment.md +52 -0
  8. nunspark-0.1.0/scripts/analyze_expert_trace.py +351 -0
  9. nunspark-0.1.0/scripts/cache_probe.py +51 -0
  10. nunspark-0.1.0/scripts/io_headroom_probe.py +94 -0
  11. nunspark-0.1.0/scripts/io_warm_bench.py +91 -0
  12. nunspark-0.1.0/scripts/io_warm_probe.py +59 -0
  13. nunspark-0.1.0/scripts/io_zerocopy_probe.py +123 -0
  14. nunspark-0.1.0/scripts/kv_probe.py +99 -0
  15. nunspark-0.1.0/scripts/m1_baseline.py +304 -0
  16. nunspark-0.1.0/scripts/policy_sim.py +73 -0
  17. nunspark-0.1.0/scripts/result.md +558 -0
  18. nunspark-0.1.0/scripts/spec_accept_probe.py +102 -0
  19. nunspark-0.1.0/scripts/spec_relaxed_probe.py +129 -0
  20. nunspark-0.1.0/scripts/train_eagle_drafter.py +317 -0
  21. nunspark-0.1.0/scripts/try_real_model.py +525 -0
  22. nunspark-0.1.0/src/nunspark/__init__.py +3 -0
  23. nunspark-0.1.0/src/nunspark/architectures.py +485 -0
  24. nunspark-0.1.0/src/nunspark/archspec.py +200 -0
  25. nunspark-0.1.0/src/nunspark/assistant.py +180 -0
  26. nunspark-0.1.0/src/nunspark/cli.py +480 -0
  27. nunspark-0.1.0/src/nunspark/eagle_drafter.py +417 -0
  28. nunspark-0.1.0/src/nunspark/engine.py +607 -0
  29. nunspark-0.1.0/src/nunspark/gemma4_assistant.py +410 -0
  30. nunspark-0.1.0/src/nunspark/generate.py +780 -0
  31. nunspark-0.1.0/src/nunspark/generate_optimized.py +197 -0
  32. nunspark-0.1.0/src/nunspark/kv_store.py +253 -0
  33. nunspark-0.1.0/src/nunspark/manifest.py +63 -0
  34. nunspark-0.1.0/src/nunspark/packer.py +271 -0
  35. nunspark-0.1.0/src/nunspark/piece_cache.py +483 -0
  36. nunspark-0.1.0/src/nunspark/piece_store.py +28 -0
  37. nunspark-0.1.0/src/nunspark/prefix_cache.py +114 -0
  38. nunspark-0.1.0/src/nunspark/server.py +494 -0
  39. nunspark-0.1.0/src/nunspark/tree_shape.py +63 -0
  40. nunspark-0.1.0/src/nunspark/tree_spec.py +259 -0
  41. nunspark-0.1.0/src/nunspark/webapp/__init__.py +1 -0
  42. nunspark-0.1.0/src/nunspark/webapp/app.py +108 -0
  43. nunspark-0.1.0/src/nunspark/webapp/engine_pool.py +58 -0
  44. nunspark-0.1.0/src/nunspark/webapp/jobs.py +127 -0
  45. nunspark-0.1.0/src/nunspark/webapp/models.py +73 -0
  46. nunspark-0.1.0/src/nunspark/webapp/runner.py +319 -0
  47. nunspark-0.1.0/src/nunspark/webapp/schemas.py +97 -0
  48. nunspark-0.1.0/src/nunspark/webapp/static/.gitkeep +0 -0
  49. nunspark-0.1.0/src/nunspark/webapp/static/app.js +137 -0
  50. nunspark-0.1.0/src/nunspark/webapp/static/index.html +79 -0
  51. nunspark-0.1.0/src/nunspark/webapp/static/styles.css +27 -0
  52. nunspark-0.1.0/tests/conftest.py +345 -0
  53. nunspark-0.1.0/tests/quant_helpers.py +27 -0
  54. nunspark-0.1.0/tests/test_analyze_expert_trace.py +77 -0
  55. nunspark-0.1.0/tests/test_architectures.py +342 -0
  56. nunspark-0.1.0/tests/test_archspec.py +141 -0
  57. nunspark-0.1.0/tests/test_assistant.py +179 -0
  58. nunspark-0.1.0/tests/test_build_draft_tree.py +82 -0
  59. nunspark-0.1.0/tests/test_cli.py +125 -0
  60. nunspark-0.1.0/tests/test_engine_expert_prefetch.py +212 -0
  61. nunspark-0.1.0/tests/test_engine_expert_trace.py +87 -0
  62. nunspark-0.1.0/tests/test_engine_forward.py +107 -0
  63. nunspark-0.1.0/tests/test_engine_gpt_oss.py +92 -0
  64. nunspark-0.1.0/tests/test_engine_kv.py +77 -0
  65. nunspark-0.1.0/tests/test_engine_moe_selective.py +331 -0
  66. nunspark-0.1.0/tests/test_engine_quant.py +68 -0
  67. nunspark-0.1.0/tests/test_engine_qwen3.py +79 -0
  68. nunspark-0.1.0/tests/test_engine_qwen3_moe.py +121 -0
  69. nunspark-0.1.0/tests/test_engine_tree_forward.py +192 -0
  70. nunspark-0.1.0/tests/test_generate.py +45 -0
  71. nunspark-0.1.0/tests/test_generate_kv.py +105 -0
  72. nunspark-0.1.0/tests/test_generate_quant.py +37 -0
  73. nunspark-0.1.0/tests/test_kv_store.py +215 -0
  74. nunspark-0.1.0/tests/test_kv_store_cachekinds.py +78 -0
  75. nunspark-0.1.0/tests/test_kv_store_truncate.py +33 -0
  76. nunspark-0.1.0/tests/test_manifest.py +44 -0
  77. nunspark-0.1.0/tests/test_memory_bounded.py +68 -0
  78. nunspark-0.1.0/tests/test_packer.py +192 -0
  79. nunspark-0.1.0/tests/test_piece_cache.py +510 -0
  80. nunspark-0.1.0/tests/test_piece_store.py +30 -0
  81. nunspark-0.1.0/tests/test_prefix_cache.py +136 -0
  82. nunspark-0.1.0/tests/test_server.py +376 -0
  83. nunspark-0.1.0/tests/test_smoke.py +12 -0
  84. nunspark-0.1.0/tests/test_speculative.py +158 -0
  85. nunspark-0.1.0/tests/test_speculative_qwen3.py +41 -0
  86. nunspark-0.1.0/tests/test_tree_sampling.py +88 -0
  87. nunspark-0.1.0/tests/test_tree_shape.py +53 -0
  88. nunspark-0.1.0/tests/test_tree_spec.py +115 -0
  89. nunspark-0.1.0/tests/webapp/__init__.py +0 -0
  90. nunspark-0.1.0/tests/webapp/conftest.py +11 -0
  91. nunspark-0.1.0/tests/webapp/test_app.py +78 -0
  92. nunspark-0.1.0/tests/webapp/test_cli_web.py +10 -0
  93. nunspark-0.1.0/tests/webapp/test_engine_pool.py +22 -0
  94. nunspark-0.1.0/tests/webapp/test_jobs.py +82 -0
  95. nunspark-0.1.0/tests/webapp/test_models.py +45 -0
  96. nunspark-0.1.0/tests/webapp/test_runner.py +113 -0
  97. nunspark-0.1.0/tests/webapp/test_schemas.py +31 -0
  98. nunspark-0.1.0/tests/webapp/test_sse.py +35 -0
  99. nunspark-0.1.0/uv.lock +1045 -0
@@ -0,0 +1,15 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .DS_Store
9
+ .codegraph
10
+ .nunspark_*/
11
+ .claude
12
+ .ruff_cache
13
+ docs/*
14
+ packed
15
+ scripts/results
nunspark-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sharma SK
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,379 @@
1
+ Metadata-Version: 2.4
2
+ Name: nunspark
3
+ Version: 0.1.0
4
+ Summary: Run LLMs bigger than your Mac's RAM: disk-streamed weights, MoE expert streaming, and deep-K speculative decoding on Apple Silicon
5
+ Author: Sharma
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Sharma SK
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Keywords: apple-silicon,inference,llm,mixture-of-experts,mlx,speculative-decoding,streaming
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Operating System :: MacOS
32
+ Classifier: Programming Language :: Python :: 3.11
33
+ Classifier: Programming Language :: Python :: 3.12
34
+ Classifier: Programming Language :: Python :: 3.13
35
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
36
+ Requires-Python: >=3.11
37
+ Requires-Dist: mlx-lm>=0.31.0
38
+ Requires-Dist: mlx>=0.31.0
39
+ Requires-Dist: numpy>=1.26
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=8.0; extra == 'dev'
42
+ Provides-Extra: web
43
+ Requires-Dist: fastapi>=0.110; extra == 'web'
44
+ Requires-Dist: python-multipart>=0.0.9; extra == 'web'
45
+ Requires-Dist: uvicorn>=0.29; extra == 'web'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # NunSpark
49
+
50
+ **Run LLMs that don't fit in your Mac's RAM — at usable speeds.**
51
+
52
+ Your Mac has 16 GB of unified memory. The model you want needs 16–40 GB of weights. NunSpark
53
+ runs it anyway: it packs the model into per-layer (and, for MoE models, per-*expert*) pieces
54
+ on disk and streams exactly the weights each token needs through a small resident budget —
55
+ then claws the speed back with three levers that only make sense in the disk-bound regime:
56
+
57
+ - **MoE expert streaming** — a Qwen3-30B-A3B token fires only 8 of 128 experts per layer.
58
+ NunSpark loads *only those*, caches them expert-aware, and prefetches the ones the next
59
+ verify pass will probably fire. Measured on a 16 GB M4: **0.5 → 2.1 tok/s**, reading
60
+ ~100 MB/token instead of ~1 GB.
61
+ - **Deep-K speculative decoding** — on disk-bound hardware the draft model's compute is free,
62
+ so speculation can go far deeper (K=16–24) than GPU serving ever would. One weight sweep
63
+ buys up to 7 accepted tokens. Lossless: output is bit-identical to running the target alone.
64
+ - **Byte-budgeted streaming engine** — every forward pass verified bit-identical to full-load
65
+ mlx-lm, fp16 and 4-bit, across 24 registered architectures.
66
+
67
+ **Measured on a 16 GB M4 MacBook Pro** (16–40 GB models that can't fully load, all lossless):
68
+
69
+ | Model (4-bit) | Size on disk | tok/s | How |
70
+ |---|---|---|---|
71
+ | Qwen3-30B-A3B (MoE) | 16 GB | **1.5–2.1** | expert streaming + expert-aware cache |
72
+ | Qwen3-30B-A3B (MoE) | 16 GB | **up to 1.54 speculative** | + deep-K spec (M≈7 on reasoning prompts) |
73
+ | Qwen2.5-32B (dense) | 18 GB | **0.9–1.1** | streaming + deep-K spec |
74
+ | Llama-3.3-70B (dense) | 40 GB | **~0.9** | streaming + deep-K spec |
75
+
76
+ For scale: naive streaming (or `llama.cpp` mmap-thrashing) gives ~0.1–0.2 tok/s on the same
77
+ hardware for the 70B, and ~0.5 tok/s for the 30B MoE. Nothing here is a quality tradeoff —
78
+ "lossless" means the streamed model produces exactly the tokens the full-RAM model would.
79
+
80
+ **Try it in three commands** (needs an Apple Silicon Mac, `uv`, and ~35 GB of free disk —
81
+ the HF download and the packed copy each take ~16 GB; the download cache can be deleted
82
+ after packing):
83
+
84
+ ```bash
85
+ uv venv --python 3.11 && uv sync
86
+ uv run nunspark pack mlx-community/Qwen3-30B-A3B-4bit ./packed/qwen3-30b
87
+ uv run nunspark generate ./packed/qwen3-30b --budget 8GB --max-tokens 200 --metrics \
88
+ --prompt "Explain how a B-tree stays balanced."
89
+ ```
90
+
91
+ That's a 30-billion-parameter model generating on a machine that cannot hold it in memory.
92
+ Add `--draft-model Qwen/Qwen3-0.6B --num-draft-tokens 24` for the speculative mode.
93
+
94
+ > This is an experimental research project, not a production inference server. Read
95
+ > [requirment.md](requirment.md) for the honest story of what worked and what didn't,
96
+ > [report.md](report.md) for the dense-model benchmarks, and
97
+ > [docs/plan4-m3-gate-summary.md](docs/plan4-m3-gate-summary.md) for the MoE numbers above.
98
+
99
+ ---
100
+
101
+ ## Why this exists
102
+
103
+ The obvious approach — split a model into pieces and load only the weights you need — was
104
+ tried first in its plain form and it *works*, but it's not competitive: naive weight-streaming
105
+ gives roughly the same throughput as `llama.cpp`'s `mmap` (~0.1–0.2 tok/s for a 70B model on a
106
+ 16 GB M4). Streaming alone is not a moat. Two findings changed that.
107
+
108
+ ### Finding 1: the disk-bound regime rewards *deep* speculation
109
+
110
+ Speculative decoding's usual cost (extra compute for the draft model) is hidden behind the
111
+ dominant cost of reading weights off disk. That means you can push speculation much deeper
112
+ (larger K) than anyone tuning for GPU-bound serving would ever try, and each additional
113
+ accepted draft token is nearly free — it saves a full weight-read pass instead of costing
114
+ extra latency. Measured acceptance multipliers rose from ~2.5× to ~5× as K grew, while naive
115
+ tok/s fell — opposite slopes, which is the signal that this regime rewards deep speculation
116
+ differently than GPU serving does. And it's lossless: the target model verifies every draft
117
+ token, so output is identical to running the target alone. See [requirment.md](requirment.md)
118
+ for the full analysis and caveats (vocab lock-in between draft/target, etc). The dense-model
119
+ benchmarks in [report.md](report.md) confirmed it, and added a second lesson:
120
+ **draft–target agreement, not resident cache size, is the primary determinant of throughput.**
121
+
122
+ ### Finding 2: MoE models are the natural fit for streaming
123
+
124
+ A dense model makes you read *every* weight for *every* token — streaming can only amortize
125
+ that. A mixture-of-experts model already routes each token through a small slice of itself:
126
+ Qwen3-30B-A3B fires 8 of 128 experts per layer, ~1 GB of expert weights per token out of a
127
+ 16 GB model. NunSpark packs each expert as its own piece and exploits that sparsity end to end:
128
+
129
+ - **Load only fired experts.** The router runs first; only the 8 winners per layer are read.
130
+ This holds in the speculative tree-verify path too, which loads the fired *union* (measured
131
+ 51–65% of experts at K=24) instead of all 128.
132
+ - **Cache experts on their own terms.** Dense layers scan cyclically (MRU is right); experts
133
+ reuse sparsely (LRU is right). A two-region cache with per-layer cores pinned took the
134
+ expert hit rate from 57–64% to 89–92% at the same 8 GB budget — that alone was 0.5 → 1.3–2.1
135
+ tok/s.
136
+ - **Prefetch the next verify pass's experts.** Consecutive verify passes fire 74–80%
137
+ overlapping expert sets. Predicted experts stream in a low-priority I/O tier into a staging
138
+ buffer that can never evict the live working set. Speculative decoding on top: up to
139
+ **1.54 tok/s lossless** on reasoning prompts (2.4× the no-prefetch control).
140
+
141
+ Every one of those policies was chosen by measurement — the failed variants and their numbers
142
+ are written up in [docs/](docs/) gate summaries.
143
+
144
+ ## Design principles
145
+
146
+ - **Maximize capability, not raw speed.** The goal is running the biggest model your disk can
147
+ hold, not the fastest possible tok/s. Seconds-per-token is an acceptable outcome; the metric
148
+ that matters is "does it run at all."
149
+ - **Built on stock MLX / mlx-lm**, not architecture-specific hacks. NunSpark reuses mlx-lm's
150
+ layer math and registry wherever possible so new model families need minimal glue.
151
+ - **Minimize target-weight reads.** Every mechanism (prefetch, caching, speculative decoding,
152
+ prefix cache) exists to reduce how many bytes of the *target* model get pulled off disk per
153
+ generated token — that's the bottleneck everything else is fighting.
154
+
155
+ ## What's implemented
156
+
157
+ - **Packer** (`nunspark pack`) — converts an mlx-lm-compatible model (local dir or HF repo) into
158
+ per-layer safetensors pieces plus a JSON manifest. Supports 4-bit quantized weights
159
+ (embedding + lm_head triplets included).
160
+ - **Streaming engine** (`StreamingEngine`) — loads pieces into a byte-budgeted cache
161
+ (`PieceCache`) with a background prefetch pool that overlaps disk I/O with compute.
162
+ Forward pass is verified bit-identical to full-load mlx-lm, fp16 and 4-bit alike.
163
+ - **MoE expert streaming** — for MoE models (Qwen3-MoE, gpt-oss), the packer splits each layer
164
+ into a core piece (attention/norms/router) plus one piece per expert; the engine loads only
165
+ the experts the router fires. Two-region cache (MRU for the dense cyclic scan, LRU for
166
+ sparse expert reuse, cores pinned), fired-union selective loading in tree verify, and
167
+ temporal expert prefetch at verify-pass granularity into an eviction-safe staging buffer.
168
+ All bit-identical to loading every expert.
169
+ - **Speculative decoding** — the core reason this is usable:
170
+ - Standard draft-model speculative decoding (`--draft-model`, any mlx_lm-format model that
171
+ shares a tokenizer with the target).
172
+ - EAGLE-style feature-level speculation (`--eagle-drafter`) — no full draft model needed.
173
+ - Tree-based speculative decoding for exploring multiple candidate continuations per step.
174
+ - Gemma3/4 MTP (multi-token-prediction) assistant drafting.
175
+ - Configurable draft depth (`--num-draft-tokens`) and acceptance policy
176
+ (`--accept-top-k`: `1` = lossless, `>1` = "fast mode" with controlled deviation).
177
+ - **KV-cache management** — RAM-resident KV cache with optional 4/8-bit quantization
178
+ (`--kv-bits`), and a server-side single-slot prefix cache that reuses the previous request's
179
+ KV state and prefills only the new suffix (`--no-prefix-cache` to disable).
180
+ - **OpenAI-compatible server** (`nunspark serve`) — serves a packed model behind a v1-style API.
181
+ - **Local web UI** (`nunspark web`) — a FastAPI app for long, document-driven **batch**
182
+ generation (not interactive chat — this engine is seconds-per-token). Upload files, submit a
183
+ batch, get one generation job per file streamed to disk with live progress over SSE.
184
+ - **Architecture support** — 24 decoder-only model types are registered
185
+ (`src/nunspark/architectures.py`), including Llama, Mistral, Phi-3, Qwen2, Qwen3 (dense +
186
+ MoE via selective expert streaming), Gemma3/Gemma4 (including heterogeneous attention),
187
+ GLM/GLM-4, OLMo-2, InternLM3, gpt-oss, and more. The current list is queryable in code via
188
+ `nunspark.architectures.supported_model_types()`; `pack` will tell you if a model's
189
+ `model_type` isn't supported. Multimodal models are not supported (text decoders only).
190
+
191
+ Not built / deferred: TurboQuant 2–4 bit KV (blocked on upstream MLX SDPA support).
192
+
193
+ ## Requirements
194
+
195
+ - **Apple Silicon Mac** (this is an MLX / unified-memory project — it does not run on
196
+ non-Apple hardware).
197
+ - **Python 3.11+**. The system Python on macOS is commonly 3.9, which is too old — create the
198
+ venv with an explicit version:
199
+
200
+ ```bash
201
+ uv venv --python 3.11
202
+ ```
203
+
204
+ - [`uv`](https://github.com/astral-sh/uv) for dependency management (recommended). A plain
205
+ `uv venv` with no `--python` flag may pick up the system 3.9 interpreter and fail — always
206
+ pass `--python 3.11`.
207
+
208
+ ## Installation
209
+
210
+ ```bash
211
+ git clone <this-repo>
212
+ cd Nunspark
213
+ uv venv --python 3.11
214
+ uv sync
215
+ ```
216
+
217
+ For the web UI, install the optional `web` extra:
218
+
219
+ ```bash
220
+ uv sync --extra web
221
+ ```
222
+
223
+ > **Gotcha:** the web dependencies (fastapi, uvicorn, python-multipart) are optional. If you
224
+ > run a plain `uv run nunspark web ...` after only `uv sync`, `uv run` re-syncs the environment
225
+ > and **drops the extra**, causing `ModuleNotFoundError`. Always pass `--extra web` on the `uv
226
+ > run` invocation itself (or install the extra into a stable, non-`uv`-managed venv):
227
+ >
228
+ > ```bash
229
+ > uv run --extra web nunspark web --packed-root ./models --port 8000
230
+ > ```
231
+
232
+ ## Quickstart
233
+
234
+ ### 1. Pack a model
235
+
236
+ ```bash
237
+ uv run nunspark pack mlx-community/SmolLM2-360M-Instruct-bf16 ./packed/smollm2
238
+ ```
239
+
240
+ `model_dir` accepts either a local path or a Hugging Face repo id. Output is a directory of
241
+ per-layer safetensors files plus `manifest.json`.
242
+
243
+ ### 2. Generate
244
+
245
+ ```bash
246
+ uv run nunspark generate ./packed/smollm2 \
247
+ --prompt "Explain disk-streaming inference in two sentences." \
248
+ --max-tokens 128 \
249
+ --budget 1GB \
250
+ --metrics
251
+ ```
252
+
253
+ Key flags:
254
+
255
+ | Flag | Meaning |
256
+ |------|---------|
257
+ | `--budget` | Resident weight budget (e.g. `512MB`, `4GB`). Lower = more disk reads, less RAM. |
258
+ | `--kv-budget` | Resident KV-cache budget (default: unbounded). |
259
+ | `--kv-bits {4,8}` | Quantize the KV cache (default fp16). |
260
+ | `--io-threads` / `--warm-window` | Parallel page-cache warming (experimental; measured net-neutral or negative in most configurations)). |
261
+ | `--draft-model <path>` | Enable speculative decoding against a smaller draft model (must share a tokenizer with the target). |
262
+ | `--eagle-drafter <path>` | Use a trained EAGLE feature-level drafter instead of a full draft model. |
263
+ | `--num-draft-tokens` | Draft tokens proposed per speculative sweep (default 16 — the "deep-K" lever described above). |
264
+ | `--accept-top-k` | `1` = lossless speculative decoding; `>1` = fast mode (bounded deviation from the target distribution). |
265
+ | `--metrics` | Print tok/s, peak memory, cache hit/miss, and (if speculative) acceptance-multiplier stats after generation. |
266
+
267
+ ### The headline use case: a 30B MoE model on a 16 GB Mac
268
+
269
+ This is the configuration behind the numbers at the top — a 16 GB model on a 16 GB machine,
270
+ streaming only the experts each token actually fires:
271
+
272
+ ```bash
273
+ # 1. Pack the oversized target once. For MoE models this splits every layer into a
274
+ # core piece + one piece per expert (~6200 files for Qwen3-30B-A3B).
275
+ uv run nunspark pack mlx-community/Qwen3-30B-A3B-4bit ./packed/qwen3-30b
276
+
277
+ # 2. Stream it. Greedy decode alone reaches 1.5–2.1 tok/s at an 8 GB budget:
278
+ uv run nunspark generate ./packed/qwen3-30b \
279
+ --prompt "Explain how a B-tree stays balanced." \
280
+ --budget 8GB --max-tokens 256 --metrics
281
+
282
+ # 3. Add deep-K speculation. The draft (Qwen3-0.6B) shares Qwen3's tokenizer, so its
283
+ # proposals are valid target tokens; one 30B verify sweep then buys up to ~7 tokens.
284
+ uv run nunspark generate ./packed/qwen3-30b \
285
+ --prompt "Think step by step: a train leaves at 9:14 travelling 83 km/h ..." \
286
+ --draft-model Qwen/Qwen3-0.6B \
287
+ --num-draft-tokens 24 \
288
+ --accept-top-k 1 \
289
+ --budget 8GB --max-tokens 256 --metrics
290
+ ```
291
+
292
+ With `--metrics` you'll see the acceptance multiplier M and the effective tok/s. Which mode
293
+ wins depends on the prompt: speculation shines where the draft agrees with the target
294
+ (reasoning chains, prose — measured M up to 7.4 and 1.54 tok/s), while terse low-agreement
295
+ prompts can be faster in plain greedy (2.1 tok/s on prose, 1.5 on code). `--accept-top-k 1`
296
+ keeps it lossless; raise it for "fast mode" if you'll accept bounded deviation. The same two
297
+ commands work for dense models (Qwen2.5-32B, Llama-3.3-70B) — speculation is the main lever
298
+ there, since every token reads the full layer stack.
299
+
300
+ For an end-to-end script that also downloads/converts the model, verifies streamed output
301
+ against a full-load run, and can A/B linear vs tree speculation, see
302
+ [scripts/try_real_model.py](scripts/try_real_model.py):
303
+
304
+ ```bash
305
+ uv run python scripts/try_real_model.py --model mlx-community/Qwen3-30B-A3B-4bit --repack \
306
+ --draft Qwen/Qwen3-0.6B --ab-spec --temp 0.0 --budget 4GB
307
+ ```
308
+
309
+ ### 3. Serve an OpenAI-compatible API
310
+
311
+ ```bash
312
+ uv run nunspark serve ./packed/smollm2 --port 8080 --draft-model <path-to-draft>
313
+ ```
314
+
315
+ Accepts the same budget/KV/speculative flags as `generate`, plus `--model-name` and
316
+ `--no-prefix-cache` (disables reusing the previous request's KV state across calls).
317
+
318
+ ### 4. Web UI (batch generation)
319
+
320
+ ```bash
321
+ uv run --extra web nunspark web --packed-root ./packed --port 8000
322
+ ```
323
+
324
+ Open `http://127.0.0.1:8000`, pick or pack a model, upload files, submit a batch. Each file
325
+ becomes one generation job; output streams to `<output_dir>/<name>.out.txt` with a
326
+ `.meta.json` sidecar, and progress is pushed live over SSE. This is built for long
327
+ document-batch jobs, not interactive chat — the engine runs seconds-per-token.
328
+
329
+ ## Development
330
+
331
+ ```bash
332
+ uv venv --python 3.11 # not a bare `uv venv` — the system Python is too old
333
+ uv sync --extra dev
334
+ uv run pytest -q
335
+ ```
336
+
337
+ Notes:
338
+ - Always invoke tests as plain `uv run pytest ...`. Do **not** pass `--active` — if your shell
339
+ has `VIRTUAL_ENV` set to something else (e.g. a system Python framework), `--active` forces
340
+ that broken environment; plain `uv run` correctly ignores it and uses the project's `.venv`.
341
+ - Watch out for `... | tail` masking a failing exit code in chained commands — check the pytest
342
+ summary line explicitly rather than trusting `$?` after a pipe.
343
+ - `scripts/` contains standalone probes used during development (`io_warm_probe.py`,
344
+ `spec_accept_probe.py`, `kv_probe.py`, `try_real_model.py`, `train_eagle_drafter.py`, etc.) —
345
+ useful references for benchmarking a real model end-to-end, not part of the package API.
346
+
347
+ ## Project layout
348
+
349
+ ```
350
+ src/nunspark/
351
+ packer.py, manifest.py, piece_store.py, piece_cache.py # model → pieces, and piece I/O/caching
352
+ engine.py, archspec.py, architectures.py, tree_shape.py # per-layer streaming forward pass, arch registry
353
+ generate.py, generate_optimized.py # generation loops (plain + speculative)
354
+ eagle_drafter.py, assistant.py, gemma4_assistant.py # speculative drafters
355
+ kv_store.py, prefix_cache.py, tree_spec.py # KV cache management
356
+ server.py # OpenAI-compatible API server
357
+ webapp/ # FastAPI batch-generation UI
358
+ cli.py # `nunspark` entry point (pack/generate/serve/web)
359
+ scripts/ # standalone benchmarking/probe scripts
360
+ tests/ # pytest suite, mirrors src/nunspark modules
361
+ ```
362
+
363
+ ## Known limitations
364
+
365
+ - **Vocab lock-in**: draft and target models must share a tokenizer, which narrows which
366
+ (draft, target) pairs are usable for speculative decoding — fine for major model families,
367
+ awkward for exotic ones.
368
+ - **gpt-oss** opts out of quantized KV: its attention-sink mechanism is rejected by quantized
369
+ SDPA, and the engine fails fast at startup rather than silently falling back.
370
+ - I/O warming (`--io-threads`/`--warm-window`) was investigated in depth and found to be net-
371
+ neutral-to-negative versus plain `mmap` in most tested configurations — it's exposed as a
372
+ flag for experimentation, not recommended by default.
373
+ - This is a single-stream engine: the server and web UI both process one generation at a time
374
+ (no concurrent-request batching) since the whole point is disk-bound streaming, not
375
+ throughput-oriented serving.
376
+
377
+ ## License
378
+
379
+ MIT — see [LICENSE](LICENSE).