drowzeys/keys-Mac-oMLX-0.7.0.dev2-DeepSeek-V4.1-Flash-oQ3e-2b27-CED-MTP
keys-Mac-oMLX-0.7.0.dev2-DeepSeek-V4.1-Flash-oQ3e-2b27-CED-MTP
DeepSeek-V4.1-Flash (763B) running on a single 256 GB Mac Studio M3 Ultra.
The published `Jundot/DeepSeek-V4.1-Flash-oQ3e-mtp` does not fit this machine: even with Engram SSD-offload, 239.04 GiB of weights stay resident against 238.42 GiB of usable RAM. This checkpoint requantizes 27 of 40 MoE layers' routed experts from affine 3-bit to affine 2-bit at group_size 64, shedding 42.7 GiB and landing at 197.19 GB resident โ with real working headroom.
๐ฆ Recipe, patches, benchmarks and serving notes: [GitHub](https://github.com/drowzeys/keys-Mac-oMLX-0.7.0.dev2-DeepSeek-V4.1-Flash-oQ3e-2b27-CED-MTP)
โ ๏ธ Requirements โ read before serving
1. Raise the kernel wired limit. oMLX caps prefill at 90% of `iogpu.wired_limit_mb`. The stock 249036 on a 256 GB Mac puts that cap at 218.4 GB โ below this model โ so the server refuses every prefill (surfacing to clients as a misleading "Context length exceeded") or dies with a silent SIGKILL.
sudo sysctl iogpu.wired_limit_mb=253952 # does not persist across reboot2. Launch through `omlx.cli serve`, never `python -m omlx.server`. Only the CLI enables prefix reuse. The bare module silently logs oMLX cache disabled and no env var fixes it. For agent workloads that resend a large system prompt this is the difference between ~99 s and ~7 s per call.
python -m omlx.cli serve --model-dir ~/.omlx/models --host 0.0.0.0 --port 11601 \
--paged-ssd-cache-dir ~/kvcache --paged-ssd-cache-max-size 16GB \
--initial-cache-blocks 8 --memory-guard safe--initial-cache-blocks 8 matters: the default 256 reserves too much up front and SIGKILLs the load.
Model settings (~/.omlx/model_settings.json):
{ "mtp_enabled": true, "mtp_num_draft_tokens": 3,
"deepseek_v41_engram_ssd_offload": true,
"deepseek_v41_expert_ssd_offload": false,
"deepseek_v41_ced_prefill_enabled": true }Long context: 404,805 tokens verified
KV storage was never the constraint. Only 4 layers (kv_source_layer_ids) produce shared compressed KV โ about 2.0 KB/token, so even 1M context is ~2.15 GB, and each layer's local attention is a fixed 128-token window. The real constraint is the prefill working set (~505 KB/token): with too little headroom oMLX shrinks the prefill chunk to a 32-token floor and long prompts collapse. Quantizing 27 layers frees ~46 GB of headroom, so the chunk stays at 2048:
Prefill gets faster at longer context (chunk amortization), then holds ~800โ840 tok/s. 500K+ is reachable; budget ~8.4 min of prefill for 400K.
Performance
Mac15,14 / M3 Ultra / 256 GB / macOS 26.6.2. Single stream, 256 max_tokens, temp 0:
Concurrency (aggregate tok/s) โ saturates ~19โ23 because decode is not batched, so aggregate tracks the single-stream rate:
All numbers at the shipped mtp_num_draft_tokens: 3. MTP acceptance 75.4% code / 59.1% prose.
Use mtp_num_draft_tokens: 3
More 2-bit layers desynchronize the target from the (untouched) DSpark drafter, so acceptance falls. The fix is shorter drafts โ a long draft from a weak drafter just wastes verification. Measured on prose at temp 0.3:
End to end: prose 23.35 โ 25.06 tok/s (+7.3%), acceptance 55.2 โ 59.1%, code unaffected (29.64 โ 29.78). No per-task tradeoff, so k=3 is the shipped default.
โ ๏ธ Acceptance also rises with temperature (54.3% at temp 0 โ 63.8% at temp 1.0) โ rejection sampling accepts more from a flatter target โ but throughput does not follow, so it is not a lever.
Quality notes
Layer selection is structural, not numerical. Per-layer weight-cosine damage from 3bโ2b is flat across all 40 layers (spread 0.0037), so it cannot guide the choice โ and trusting it caused a real regression: an earlier attempt included layers 28 and 32, members of index_source_layer_ids, which produce the index keys for sparse attention. MTP acceptance collapsed 84% โ 58.5%. Excluding them restored it.
Never quantized below 3-bit: kv_source_layer_ids, index_source_layer_ids, candidate_source_layer_id, engram_layer_ids, dspark_target_layer_ids, layer 0 and layer nโ1. DSpark MTP weights are untouched โ a degraded drafter is what sank an earlier 2.4-bit V4 checkpoint on code despite being 1.8ร smaller.
Measured MTP acceptance on this build: ~81โ85% on code, ~64% on prose (prose drafts worse; that is task-intrinsic).
Credits
Almost everything here is other people's work. This checkpoint changes 14 of 40 MoE layers and adds a set of measurements. The model, the quantization that made it tractable, the runtime and the kernels all come from the people below. Thank you.
DeepSeek-AI โ the model
**DeepSeek-V4.1-Flash**, the 763B MoE underneath all of this: CSA2 sparse attention, the Engram n-gram tables, the DSpark MTP drafter, sqrtsoftplus routing.
Jundot โ the runtime and the checkpoint
Two separate debts, either of which alone would have made this impossible.
- **oMLX** (Apache-2.0) โ the whole serving stack: the
deepseek_v41implementation, Engram SSD offload, the custom Metal kernels, the paged KV cache, the oQ quantizer, DSpark MTP integration. Specifically #3574 (V4.1 Flash + MTP + Engram offload), #3607 (CED prefill โ +45% prefill, โ31% TTFT), #2595 (MoE expert offload) and #3571. - **`Jundot/DeepSeek-V4.1-Flash-oQ3e-mtp`** โ the calibrated mixed-precision checkpoint this is derived from. Their oQ pipeline (sensitivity measurement, imatrix calibration, mixed-bit allocator) did the hard quantization work. 231.1 GiB of this repo's 309 GB is byte-identical to theirs. Their oQ4e card also supplied the residency analysis that framed the problem.
Apple / ml-explore
MLX (MIT), mlx-lm, mlx-vlm, the Metal toolchain, and the M3 Ultra's unified memory โ without which a 763B model on one desktop is not a conversation anyone gets to have.
Also in the chain
mlx-embeddings (Prince Canuma), nanobind (Wenzel Jakob), and Hugging Face โ transformers, tokenizers, huggingface_hub and the Hub. HF's xet deduplication meant publishing 309 GB cost ~9.5 GB of new data rather than re-uploading 231 GiB of Jundot's bytes.
Full attribution, including an honest "what is actually ours" accounting: CREDITS.md.
License
Model weights derive from Jundot/DeepSeek-V4.1-Flash-oQ3e-mtp and the underlying DeepSeek-V4.1-Flash; both carry their own licenses, which apply here unchanged. The serving stack is oMLX (Apache-2.0). Tooling in the companion GitHub repo is Apache-2.0. Any bugs here are ours, not upstream's.
