DeepSeek's DSpark and z-lab's DFlash speculative decoding — native on Apple Silicon via MLX.
Lossless drafters (same output, just faster) for Gemma-4, Qwen3, Ornith-1.0, Qwen3.6-27B, and Bonsai-27B targets —
plus any matched DSpark / DFlash checkpoint. Run them at the CLI, from Python, or serve an OpenAI-compatible API to LM Studio / any local tool.
mlx-dspark runs two EAGLE-family speculative-decoding drafters natively on Apple Silicon: DeepSeek's DSpark (semi-autoregressive, from the DeepSpec codebase, used to accelerate DeepSeek-V4) and z-lab's DFlash (block diffusion). Both are lossless — the target verifies every token, so output is identical to normal decoding — and run under one verify loop, so you can serve them, script them, or benchmark them head-to-head.
What this is not: DeepSeek-V4 inference. The targets are consumer-size models (Gemma-4, Qwen3, PrismML's ternary Bonsai-27B) with published DSpark drafters — so this runs the real drafter method on a Mac, but the model producing tokens is Gemma / Qwen / Bonsai, not V4. V4 Flash/Pro (MoE, batched serving) is DSpark's own headline use case.
Supported models
Every row auto-resolves its drafter from --model (any quant of the target matches). Measured warm on
an M4 Pro; full tables, baselines, and method in Results at a glance:
| target | best measured speedup | speed |
|---|---|---|
| Ornith-1.0-9B (8-bit) | 2.44× math · 2.17× code · 2.11× chat | ~61 tok/s |
| Gemma-4 12B (8-bit) | 2.11× code · 1.77× chat | ~36 tok/s |
| Qwen3-14B (8-bit) | 1.92× code | ~30 tok/s |
| Qwen3-8B (8-bit) | 1.90× code | ~54 tok/s |
| Qwen3-4B (8-bit) | 1.64× code | ~84 tok/s |
| Qwen3.6-27B (4-bit)** | 1.78× math · 1.42× code | ~22 tok/s |
| Ternary-Bonsai-27B (2-bit) | 1.15× code (--max-draft auto) |
~29 tok/s |
** Qwen3.6-27B works and is lossless, but it's not a speed pick yet: the only drafter published for it so far is a community checkpoint with modest acceptance — a better-qualified drafter would lift this row. See Results at a glance for the caveats.
Target precision: the quants shown are each model's measured best — ratios are non-monotone in bits and peak at 8-bit on current MLX (full Ornith sweep: 4-bit 1.38× · 8-bit 2.17× · bf16 1.54× on code; bf16 loses in both ratio and absolute speed because MLX's unquantized matmul pays a ~2× cost cliff at verify width 2). Details in Results at a glance.
Copy-heavy code editing goes further: when the model re-emits or refactors code already in its
context (the daily agent/assistant workload), match-scaled lookup drafts reach 4.5× on Gemma-12B
(75 tok/s) and 3.6× on Ornith-9B (93 tok/s). Any model not listed still gets drafter-free
lookup speculation via --mode auto.
Install
pip install mlx-dspark # or: uv pip install mlx-dspark
Apple Silicon + Python ≥ 3.10; installs mlx ≥ 0.32.0 automatically (0.32's quantized-matmul kernels are what current speedup numbers are measured on). Model weights download from the Hugging Face cache on first use (none bundled). No server framework is pulled in — the API server is built on the standard library.
Known upstream incompatibilities (both handled): mlx-vlm 0.6.5 moved an internal rope-utils module, which crashed
import mlx_dsparkon fresh installs of mlx-dspark ≤ 0.4.2 — fixed in 0.4.3 (both module layouts supported), so upgrade mlx-dspark rather than pinning mlx-vlm. Separately, mlx-vlm 0.6.4 × transformers ≥ 5.12 breaks loading the gemma4 target with a misleadingOSError: Can't load video processor …(#4, upstream Blaizzy/mlx-vlm#1578 — fixed in mlx-vlm 0.6.5). mlx-dspark ≥ 0.3.2 shims that one at load time, so any mlx-vlm ≥ 0.6.3 works; the shim self-retires on fixed releases, andmlx-dspark doctorreports when it is active.
Quickstart
You name the target model (--model, an HF repo or local path, exactly like mlx-lm); the matching
drafter is resolved automatically for known targets (see Models), or pass --drafter.
Serve an OpenAI-compatible API
mlx-dspark serve --model mlx-community/Qwen3-8B-8bit # → http://127.0.0.1:8080/v1
# --max-batch 4 continuous batching: up to 4 concurrent requests share each forward
# (~2.5× aggregate; a finished request returns immediately, its slot
# admits the next one mid-flight)
# --kv-bits 8 quantized KV cache (long-context bandwidth saver)
# --mode auto|dspark|dflash|lookup|baseline · --no-thinking · --api-key KEY
--mode auto picks the best available speculation for any target (a known DSpark drafter → else
DFlash → else drafter-free n-gram lookup), so any repo serves with some speedup and no extra flags.
Then point any OpenAI client at it — the speculative speedup is transparent:
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="not-needed")
print(client.chat.completions.create(
model="Qwen3-8B-8bit",
messages=[{"role": "user", "content": "Explain rainbows briefly."}],
).choices[0].message.content)
For LM Studio / other tools: set the OpenAI base URL to http://127.0.0.1:8080/v1.
The server speaks the OpenAI API: POST /v1/chat/completions (streaming and non-streaming,
multi-turn), POST /v1/completions, GET /v1/models, GET /health, GET /metrics. It supports
temperature, top_p, top_k, max_tokens, stop, seed, presence_penalty / frequency_penalty,
logprobs / top_logprobs, tool calling (tools / tool_calls), and a per-request thinking toggle
(enable_thinking). Each response carries an x_mlx_dspark block (accept length + tok/s) so the
spec-decode gain is visible. Continuous batching (--max-batch N) serves concurrent requests in one
batched forward for ~2.5× aggregate throughput (see Concurrent throughput);
prefix caching (on by default) reuses the conversation prefix so multi-turn chat doesn't re-prefill
each turn (~13× faster follow-up turns on a long shared context — see Prefix caching).
One-shot generation (CLI)
# downloads the drafter + instruct target on first run
mlx-dspark generate --model mlx-community/Qwen3-4B-8bit --prompt "Explain how rainbows form."
# baseline (plain target) vs dspark — same output, faster (record each, stack for a demo)
mlx-dspark generate --model mlx-community/Qwen3-4B-8bit --mode baseline --prompt "..." --max-new-tokens 400
mlx-dspark generate --model mlx-community/Qwen3-4B-8bit --mode dspark --prompt "..." --max-new-tokens 400
# z-lab DFlash drafter (--max-draft 0 = full 16-block, its native operating point)
mlx-dspark generate --model mlx-community/gemma-4-12B-it-8bit --mode dflash --max-draft 0 --prompt "Write a binary search."
# sampled (not greedy) — lossless w.r.t. the target at temperature T (dspark and dflash)
mlx-dspark generate --model mlx-community/Qwen3-4B-8bit --prompt "Write a short poem." --temperature 1.0 --top-p 0.95 --seed 0
python -m mlx_dspark … works too, and the old flat --prompt … form still maps to generate.
Python
from mlx_dspark import load_pair, speculative_generate
target, tok, drafter, cfg = load_pair("mlx-community/Qwen3-8B-8bit") # drafter auto-resolved
res = speculative_generate(target, tok, drafter, "Explain how rainbows form.")
print(res.text, res.mean_accept_len, res.tokens_per_sec)
from mlx_dspark import load_dflash_pair, dflash_generate # z-lab DFlash instead
target, tok, drafter, cfg = load_dflash_pair("mlx-community/gemma-4-12B-it-8bit")
res = dflash_generate(target, tok, drafter, "Write a binary search in Python.") # max_draft_tokens=None = full block
print(res.text, res.mean_accept_len, res.tokens_per_sec)
Models
Pass any target repo/path to --model; the matched drafter auto-resolves for the targets below
(quantization-agnostic — a -4bit / -8bit / -bf16 of the same model resolves the same drafter). For
anything else, add --drafter <repo>. Run mlx-dspark models to print this table.
target (--model) |
DSpark drafter (--mode dspark) |
DFlash drafter (--mode dflash) |
peak RAM |
|---|---|---|---|
mlx-community/Qwen3-4B-8bit |
deepseek-ai/dspark_qwen3_4b_block7 |
z-lab/Qwen3-4B-DFlash-b16 |
~8 GB |
mlx-community/Qwen3-8B-8bit |
deepseek-ai/dspark_qwen3_8b_block7 |
z-lab/Qwen3-8B-DFlash-b16 |
~11 GB |
mlx-community/gemma-4-12B-it-8bit |
deepseek-ai/dspark_gemma4_12b_block7 |
z-lab/gemma4-12B-it-DFlash |
~15 GB |
prism-ml/Ternary-Bonsai-27B-mlx-2bit |
Rahim/Ternary-Bonsai-27B-dspark |
— | ~12 GB |
mlx-community/Qwen3.6-27B-4bit |
Avesed/Qwen3.6-27B-DSpark (community) |
— | ~20 GB |
mlx-community/Ornith-1.0-9B-8bit |
stanleyphoong/Ornith-1.0-9B-DSpark (community) |
— | ~13 GB |
Peak RAM is measured on an M4 Pro (8-bit target + 4-bit drafter + KV cache); add headroom for macOS.
Rows marked (community) use drafters published by the community, not by DeepSeek — quality varies more
than with the official checkpoints, and it shows up directly as acceptance length (= your speedup).
The Ornith drafter is the strong case: rigorously qualified by its author (17/17 gates, 95% of the
DSpark paper's reference acceptance) and it produces the best chat speedups in this table. The
Qwen3.6-27B drafter is solid but accepts below DeepSeek's official drafters (~2.1 vs ~2.6–2.8 at
cap 2 on code — hence the smaller speedup), is English-centric (Chinese accepts poorly, per its own
card), and was trained against a W4A16 quant — so pair it with the 4-bit target: that's its
matched precision, and we measured the 8-bit target lowering its acceptance (any Qwen3.6-27B-*
quant still resolves the drafter if you want to try; see the results footnote).
A 4-bit target (--model …-it-4bit) roughly halves the target's share (fits smaller Macs). Use the
matched instruct target the drafter was trained against — a base model drops acceptance sharply. The
legacy --family qwen3|gemma4 flags still work but are deprecated in favor of --model.
--drafter lets you run any other matched z-lab / DeepSpec checkpoint with no code change — e.g.
Qwen3-14B (DSpark-only; z-lab published no 14B DFlash; ~18 GB peak; benchmarked below):
mlx-dspark generate --model mlx-community/Qwen3-14B-8bit \
--drafter deepseek-ai/dspark_qwen3_14b_block7 --prompt "Explain how rainbows form."
Bring your own drafter — what runs and what doesn't
New DSpark/DFlash drafters keep landing on HF in three different packagings; here is the honest compatibility contract (loaders refuse incompatible checkpoints with an error naming the reason, never a silent mis-load):
| checkpoint style | example | status |
|---|---|---|
| DeepSpec-native standalone drafter (qwen3/gemma4 backbone, any size/quant) | deepseek-ai/dspark_qwen3_14b_block7 |
✅ runs via --drafter (4B/8B/14B/gemma-12B measured on an M4 Pro; larger sizes should run — reports welcome) |
| z-lab DFlash adapter for a qwen3/gemma4-family target | z-lab/Qwen3-8B-DFlash-b16 |
✅ runs via --mode dflash --drafter |
| PrismML dspark GGUF (Bonsai-27B) | prism-ml/Ternary-Bonsai-27B-gguf → *-dspark-bf16.gguf |
✅ pre-converted repacks auto-resolve (Rahim/*-dspark); any future GGUF-only drop runs via --drafter gguf:<repo>/<file>.gguf (converted locally, once) |
| vLLM "speculators" format | RedHatAI/GLM-5.2-speculator.dspark |
❌ different config schema — not yet (issue?) |
| Full model with embedded drafter | deepseek-ai/DeepSeek-V4-Pro-DSpark (893 GB, MLA+MoE) |
❌ different architecture & packaging — out of scope for consumer Macs |
| DFlash+Markov community hybrids | Hikari07jp/DSpark-Gemma-4-31B-draft |
❌ hybrid head — not yet |
Targets: any dense mlx-lm text model routes automatically; a one-time load probe verifies the
hidden-state tap reproduces the model's own forward and fails loudly if the family needs bespoke
support (drafter-free --mode lookup / --mode auto still work with any target). If you run a
pair we haven't measured, mlx-dspark benchmark --json produces a device-stamped result we can fold
into the table — please share it.
PrismML Bonsai 27B (ternary / 1-bit Qwen3.6-27B)
Bonsai 27B is PrismML's 1.7-bit ternary (and 1-bit) rebuild of Qwen3.6-27B — a full 27B-class reasoning model in ~8 GB. It ships with a DSpark drafter that PrismML publishes GGUF-only and, per their own docs, accelerates their CUDA path but "not Macs yet". mlx-dspark runs it on a Mac:
mlx-dspark generate --model prism-ml/Ternary-Bonsai-27B-mlx-2bit \
--max-draft auto --prompt "Implement binary search in Python."
# first run: downloads the target (8.5 GB) + the matched drafter (6.8 GB bf16 safetensors —
# our 1:1 repack of PrismML's GGUF-only drafter, quantized to 4-bit at load)
Measured on an M4 Pro 48 GB (greedy, warm): baseline ~25.5 tok/s; ~1.15× on code/structured content (acceptance ~2.9/round at cap 2), ~break-even on open chat. Output is lossless — byte-identical to plain greedy decoding. Bonsai's backbone is hybrid linear attention (48 of 64 layers carry recurrent state, which can't be rolled back like a KV cache), so mlx-dspark records each verify round's recurrence inputs and, on a partial accept, rebuilds the state at the exact accept point (bit-exact, a few ms) — rejected drafts cost about as little as a dense KV trim. As far as we know this is the first working speculative decoding for this model family on Apple Silicon.
Two honest caveats. The ceiling is the 2-bit quantization itself: extra verify rows on a
2-bit model are compute-bound (they cost the same as on a 4-bit model, measured), while its
plain step is very fast — so chat-level acceptance hovers at break-even instead of the
1.6–2.1× the 8-bit presets reach. --max-draft auto stays the recommended setting: it
picks the cap from this machine's measured curves + live acceptance and can still park
speculation (plain pipelined steps + probe rounds) on content where it would lose. And it
requires mlx ≥ 0.32.0 (older mlx lacks the multi-row 2-bit matmul path that makes
verification affordable at all). Prefix caching and batching remain dense-target-only.
Baseline/--mode lookup also work for any other qwen3_5 (Qwen3.5/3.6-family) checkpoint.
The 1-bit Bonsai-27B-mlx-1bit pack runs on stock MLX as of mlx-vlm 0.6.5 (which
ships a Python-hosted 1-bit kernel; stock mx.quantize still has no 1-bit mode) — but
speculative decoding measures a net loss on it: that kernel re-reads the full weight
stream once per verified token, so verify cost is linear in draft length, and dspark lands at
0.71–0.77× baseline at every cap (M4 Pro, healthy acceptance, losslessness intact). mlx-dspark
therefore keeps the pack unintegrated — plain generation via mlx-vlm ≥ 0.6.5 is the right tool
for it (~35 tok/s on an M4 Pro vs ~25 for the ternary), and load_target refuses it with a
pointer saying so. The ternary 2-bit variant remains the speculative-decoding operating point.
How it works
- DSpark — a parallel backbone (5 layers) consumes the target's hidden states (EAGLE3-style) and proposes a 7-token block at once; a rank-256 Markov head adds a cheap previous-token correction that kills "suffix decay"; a confidence head scores each position (optional adaptive block length).
- DFlash (
--mode dflash) — a block-diffusion drafter that denoises a whole 16-token block in one parallel pass and reuses the target's own embed/lm-head. Different trade-offs (see below). - The target verifies every token, so output is greedy-correct by construction (identical to plain
decoding up to floating-point tie-breaking).
--temperature > 0switches to lossless speculative sampling — an exact sample from the target at temperature T (with--top-p/--top-k).
The drafter loads 1:1 from the HF checkpoint and is 4-bit quantized by default (cheap to run each round; quantization doesn't change acceptance — that's set by the drafter↔target match).
Which target & drafter should I use?
Short answer on current mlx (≥ 0.32): DSpark, everywhere (--mode auto picks it for you). Measured on
an M4 Pro, warm (code prompt unless marked chat):
| target | DSpark (--mode dspark, cap 2) |
DFlash (--mode dflash --max-draft 0) |
pick |
|---|---|---|---|
| Gemma-4 12B | 2.11× code, 1.77× chat | 1.63× code, ~0.7× chat | DSpark |
| Qwen3-8B | 1.90× | 0.86× full block (1.47× at cap 2) | DSpark |
| Qwen3-4B | 1.64× | modest | DSpark |
| Ternary-Bonsai-27B | 1.15× code (--max-draft auto) |
— | DSpark (auto) |
| Qwen3.6-27B (4-bit, hybrid) | 1.42× code · 1.78× math · 1.27× chat | — | DSpark (auto) |
| Ornith-1.0-9B (8-bit, hybrid) | 2.17× code · 2.44× math · 2.11× chat (cap 3) | — | DSpark (auto) |
This is a version-dependent verdict worth knowing about: on mlx 0.31, verify cost rose steeply with the
number of tokens verified, which made DFlash's full 16-block the winner on Gemma-12B code/math (~2.1× vs
DSpark's ~1.9× then). mlx 0.32's quantized-matmul kernels made narrow multi-row verify disproportionately
cheaper, and DSpark's short block now wins across the board here. If your mlx/hardware differs,
--max-draft auto re-measures the curves on your machine, and mlx-dspark benchmark settles it empirically.
For target precision: 8-bit is the sweet spot (best acceptance + quality); 4-bit gives the highest absolute throughput and fits smaller Macs but a smaller speedup ratio; bf16 is slower on M-series (verify dominates). The drafter stays 4-bit either way. Full numbers and the reasoning are in Benchmarks & deep dive.
Results at a glance
DSpark vs plain greedy decoding of the same model, at its cap=2 optimum (M4 Pro 48 GB, warm,
code prompt, 8-bit instruct target, 4-bit drafter, mlx 0.32.0 — whose quantized-matmul kernels
lifted every row well past the mlx-0.31 numbers this README previously carried):
| target | accept len | baseline | mlx-dspark | speedup |
|---|---|---|---|---|
| Gemma-4 12B | ~2.75 | 17.0 tok/s | 35.9 tok/s | 2.11× |
| Qwen3-14B | ~2.50 | 15.5 tok/s | 29.7 tok/s | 1.92× |
| Qwen3-8B | ~2.58 | 28.7 tok/s | 54.4 tok/s | 1.90× |
| Qwen3-4B | ~2.33 | 51.3 tok/s | 84.1 tok/s | 1.64× |
| Ternary-Bonsai-27B (2-bit, hybrid) | ~2.88 | 25.5 tok/s | 29.4 tok/s | 1.15× (v0.4.2 exact rollback) |
| Qwen3.6-27B (4-bit, hybrid)² | ~2.12 | 15.2 tok/s | 21.6 tok/s | 1.42× (1.78× math at cap 3) |
| Ornith-1.0-9B (8-bit, hybrid)² | ~3.19 | 27.9 tok/s | 60.9 tok/s | 2.17× code (2.44× math, 2.11× chat, cap 3) |
² Community-drafter rows. Qwen3.6-27B runs a 4-bit target (its drafter's matched
precision — trained against a W4A16 quant); code at cap 2 shown, --max-draft auto settles at
cap 3; unlike 2-bit Bonsai, chat stays positive (1.27×). An 8-bit target was measured and is
NOT recommended here: acceptance drops (the drafter is 4-bit-native), and while --max-draft auto still reaches ~2.1× against the slower 8-bit baseline, the 4-bit target is faster in
absolute tok/s everywhere. Rule of thumb: match the target's precision to what the drafter
was trained against — Ornith's drafter (bf16-qualified) wants 8-bit, Avesed's (W4A16) wants 4-bit. Ornith-1.0-9B (an agentic-coding
qwen3_5 hybrid, drafter qualified against the bf16 verifier) runs the 8-bit house
sweet spot — the first target here with chat above 2× — and its acceptance is so high on code
(p≈0.96/position) that auto-cap drives the cap to the full block of 7. The 4-bit Ornith
target trades the ratio for absolute speed: ~1.4–1.55× but 60–76 tok/s (baseline 49.3) —
pick 4-bit for peak tok/s, 8-bit for quality and the headline ratio; the same drafter
auto-resolves for both. And don't bother with a bf16 target for speculation: we swept it
(Ornith bf16: 1.54× code at cap 3, 22.9 tok/s) — the ratio is non-monotone in bits and
peaks at 8-bit, because MLX's unquantized matmul pays a ~2× cost cliff at verify width 2
where the quantized kernels stay flat. bf16 is slower than 8-bit in both ratio and
absolute speed here.
Baselines are this harness's pipelined greedy loop, which measures at parity with mlx_lm.generate
(the Qwen3-4B baseline is the same 51–52 tok/s either way). All paths produce identical output to
plain decoding — they're just faster. Chat content accepts less than code everywhere; on the 2-bit
Bonsai target chat lands ~break-even (its verify rows are compute-bound), and --max-draft auto
adapts or parks where speculation would lose (see the Bonsai section). Why a Mac can't go much
higher and the cost model are below.
The table is fresh-generation content. On copy-heavy editing — the model re-emitting or refactoring code already in its context — match-scaled lookup drafts (0.5.0, on by default) go well past it: Gemma-12B file re-emission 3.03× → 4.51× (75 tok/s), rename-refactor 4.33×; Ornith-9B rename-refactor 2.79× → 3.57× (93 tok/s), re-emission 2.45× — outputs still bit-identical, chat and fresh code unchanged. See the hybrid-drafting bullet in Flags that matter for how it works. The deep-dive's multi-prompt DSpark-vs-DFlash tables are mlx-0.31.2-era and are kept as the last full sweep — 0.32 shifted that balance toward DSpark (spot-checked; see that section's note).
Concurrent throughput
--max-batch N runs up to N concurrently-queued requests through one batched target forward, so
they share a single weight-read per step — the regime where speculative decoding really shines. For a
local agent swarm (a few agents hitting the server at once) this is a large aggregate win, and single
requests are unaffected: a lone request — or one using penalties / logprobs / temperature > 0 dspark —
takes the serial path, so per-request latency never regresses.
Batching is continuous (dspark): a request is delivered the moment it finishes — it never waits
for the batch's slowest member — and its freed slot admits the next queued or newly-arriving request
mid-flight (measured: a short request joining two long ones returned at 2.3 s while they ran to 8.4 s).
With --max-draft auto, the cap is also calibrated per batch width: at B=4 the measured verify
curve flattens past the qmm knee (the paper's cheap-verify regime), so longer draft blocks pay again
(+5% aggregate at B=4 from the auto-picked cap on an M4 Pro).
Qwen3-4B-8bit, M4 Pro, 4 concurrent requests, mlx-0.31.2-era sweep (aggregate tokens/s vs the greedy baseline run serially; the batched-vs-serial ratios are the durable part — absolute levels are higher on 0.32):
| serving | aggregate tok/s | vs serialized baseline |
|---|---|---|
| greedy baseline (one at a time) | 52 | 1.00× |
batched baseline (--mode baseline --max-batch 4) |
128 | 2.48× |
batched dspark (--mode dspark --max-batch 4) |
130 | 2.51× (1.73× over serialized dspark) |
Both the target verify and the DSpark drafter are batched. Output stays greedy-correct per request; a batched quantized target is not bit-identical to single-sequence decoding (the quantized matmul takes a different numeric path at batch width — the same qmv→qmm knee as the cost model below — flipping ~0.5% of near-tie tokens), which is inherent to any batched quantized server, not spec-specific. Dense mlx-lm targets (Qwen3 / Llama / Mistral-class) only; Gemma-4 (mlx-vlm) transparently falls back to serialized.
Prefix caching
The server keeps the target KV cache (and, for DSpark, the drafter context) from the previous turn and reuses the shared conversation prefix instead of re-prefilling it. On a ~750-token shared context this makes follow-up turns ~13× faster (measured: 87 ms vs 1132 ms). It's lossless to the same standard as the rest of the project (a warm turn differs from a cold one only at logit-margin≈0 ties) and invalidates itself on any error so it can't desync.
On by default for --mode dspark / baseline on dense targets (Qwen3); disabled for DFlash. For
Gemma-4 (sliding-window / rotating KV cache) reuse is exact only until the window first wraps, so entries
are reused while under the window and refused once any layer wraps — multi-turn chat under the window skips
re-prefilling like Qwen does. Flags: --no-prefix-cache, --prefix-cache-slots N (LRU slots so a chat and
an agent don't evict each other, default 2), and --prefix-cache-dir DIR + --prefix-cache-max-ram-mb N
for the optional SSD spill tier on very long contexts.
Benchmarks & deep dive
Everything below is for readers who want the numbers and the why. The sections above are enough to use it.
Reproduce the sweep on your own Mac with mlx-dspark benchmark --model <repo> (warm, device-stamped, --json).
The Apple-Silicon speedup ceiling
Speculative decoding amortizes a memory-bound single-token decode across the K tokens verified in one
forward. On a datacenter GPU that arbitrage is huge (parallel verify is nearly free, so speedup ≈ acceptance
length). On an M-series chip it's weaker — verify cost grows with the number of tokens verified
(multi-token verify leaves the quantized matmul's cheap few-rows path). The cost model is
tok/s ≈ A / (drafter + overhead + slope·C) for accept length A and draft cap C; the slope is a
property of (quantization × mlx version × chip), which is why --max-draft auto measures it on your
machine instead of trusting a constant. On mlx 0.31.2 we measured ≈ +14 ms/token for Gemma-4 12B (a
~2.2× ceiling even with a perfect drafter); mlx 0.32's kernels flattened the curve enough that Gemma-4
now measures 2.11× at cap 2 — past what the old curve allowed. The binding limiter remains acceptance
length (set by the drafter↔target match) — not drafter quantization (4-bit / 8-bit / bf16 give
identical acceptance; 4-bit is simply fastest).
Long context
The speculative speedup holds with context depth — measured flat at ~1.6× out to 12k+ tokens on Qwen3-4B (M4 Pro, mlx 0.31.2; absolute levels are higher on 0.32 — the flatness is the point). (Before v0.3.1 the drafter tiled its GQA/MQA KV cache redundantly every round, which scaled with depth and made speculation go net-negative past a few thousand tokens on cheap-verify targets; that's fixed — the fix is bit-for-bit identical output.) On expensive-verify targets (Gemma-12B) speculation actually gains slightly with depth, since the target slows faster than the cheap drafter.
Two things do still grow with a longer prompt, for every decoder (baseline, mlx-lm, this) — not the
speculative speedup: time-to-first-token (reading an L-token prompt is inherent work) and per-token
decode (attention reads a longer KV cache). Soften both with prefix caching (reuse the conversation prefix
across turns, on by default) and --kv-bits 8 (quantized KV cache — the long-context bandwidth lever).
DSpark vs DFlash (head-to-head)
Three drafters from the same DeepSpec lineage, all EAGLE-family (a tiny drafter that consumes the target's hidden states): EAGLE3 is autoregressive (high quality, draft latency grows with block size); DFlash drafts a whole block in one pass (fast, but later positions collide — "suffix decay"); DSpark = DFlash's parallel backbone + a rank-256 Markov head that reinjects token-to-token dependency, fixing suffix decay for ~0.6 ms/round. This is the first MLX port of DSpark; it also runs z-lab's original DFlash (block diffusion, Chen et al., arXiv:2602.06036, MIT) through the same lossless loop.
mlx-version note: the two multi-prompt tables below are the last full sweep, measured on mlx 0.31.2. On mlx 0.32 the balance shifted toward DSpark — narrow multi-row verify got disproportionately cheaper, so on the same code prompt Gemma-12B now measures DSpark cap-2 at 2.11× vs DFlash full-16 at 1.63×, and the 12B "DFlash wins code/math" pick no longer holds on this M4 Pro (the 8B "full block is a net loss" verdict still does: 0.86×). The per-domain acceptance numbers below are mlx-independent and remain the useful part; re-run
mlx-dspark benchmarkfor current throughput on your setup.
Gemma-4 12B (it-8bit, M4 Pro, warm, greedy, 4 prompts/domain — accept / tok·s; greedy ≈ 17.3 tok/s):
| method | chat | code | math |
|---|---|---|---|
| DSpark (cap 2) | 2.45 / 28.5 | 2.78 / 32.8 | 2.86 / 32.4 |
| DFlash (cap 2) | 2.15 / 24.2 | 2.76 / 31.3 | 2.71 / 29.6 |
| DFlash (full 16) | 2.68 / 16.9 | 5.95 / 36.6 | 6.20 / 36.3 |
They're complementary, matching the paper's framing: DFlash's block-16 wins structured content on both axes (accept ~6.0 on code/math vs DSpark's block-7 ceiling ~2.8; ~2.1× throughput) because high acceptance amortizes the wide verify; DSpark's Markov head wins open chat (2.45 / 1.65×; DFlash's block never fills on unpredictable text — full-16 chat is a slight net loss).
But the winner flips on a smaller, cheap-verify target. Qwen3-8B-8bit (warm, greedy, 3 prompts/domain; greedy ≈ 28.8):
| method | chat | code | math |
|---|---|---|---|
| DSpark (cap 2) | 2.38 / 45.7 | 2.55 / 48.8 | 2.40 / 46.1 |
| DFlash (cap 2) | 1.99 / 33.8 | 2.22 / 37.0 | 2.11 / 35.7 |
| DFlash (full 16) | 2.19 / 21.1 | 2.94 / 27.6 | 2.66 / 25.5 |
Here DSpark wins everywhere (~1.6×) and DFlash's block advantage evaporates — full-16 is a net loss
(~0.9×) because the cheap verify makes the wide block cost more than it returns, and accept never climbs
(~2.9 on code vs 5.95 on the 12B). Cross-checked against z-lab's own optimized runner
dflash-mlx on the identical target+drafter: its baseline matches
ours (29.3 tok/s) and its DFlash is also a net loss / wash at 8B (0.92× code full-block, ~1.08× adaptive) —
even with its hand-written Metal verify kernels. So this is DFlash at this model scale on Apple Silicon,
not an artifact of our verify loop.
Per the paper (accept length, full block, temp=1.0), DSpark beats DeepSpec's DFlash by +16–18% and EAGLE3 by +27–31%; our greedy exact-match numbers are lower than the paper's temp=1.0 speculative-sampling numbers because greedy is the strictest possible accept rule (not a bug).
Target precision
Since verify dominates, target precision is a speed/quality knob (mlx-0.31.2-era sweep — the 8-bit column is higher on 0.32, see Results at a glance; the qualitative trade-off is unchanged):
| target | 8-bit (default) | 4-bit |
|---|---|---|
| Gemma-4 12B | greedy 17.5 → spec 30 tok/s (1.73×) | greedy 30.6 → spec 34–38 tok/s (1.1–1.25×) |
| Qwen3-4B | greedy 49.8 → spec 73 tok/s (1.45×) | greedy 82 → spec 96–103 tok/s (1.17–1.26×) |
8-bit for the biggest spec benefit + best quality; 4-bit for max absolute throughput or small RAM
(--model …-it-4bit). The drafter stays 4-bit; a bf16 target is not a win (verify roughly doubles).
Tuning
- DSpark —
--max-draft 2is the measured optimum for the dense presets (default): verify cost grows per token and the marginal draft token rarely survives.--confidence-threshold 0.6truncates the block adaptively via the confidence head instead. For Bonsai-27B use--max-draft auto(see its section). --max-draft auto— measures this machine + model's verify/drafter cost curves once (a few seconds, cached on disk) and picks the cap per round from the curves + live acceptance and observed round times, so it tracks the hardware and the mlx version instead of a hard-codedcap=2. It can also park speculation entirely (plain pipelined steps + periodic probe rounds) on content where speculation would lose — the safety net that makes it the recommended setting for Bonsai. Lossless — the cap only sets how many drafts get verified.- Hybrid n-gram drafting (dspark, on by default) — when the current suffix already occurred earlier in the
context (quoting, code edits, repeats), that free continuation is verified instead of running the drafter
that round, so copy-heavy spans commit several tokens per round. Composes losslessly;
--no-lookup-draftsturns it off.--mode lookupruns the same n-gram speculation with no drafter at all, for any target. Match-scaled long drafts (--lookup-long-draft, default 32): a copy run whose context matches ≥8 tokens deep earns drafts up to ~2× the matched length — verify width 16–32 is a measured plateau on M-series (~2.5× the cost of one step), so verbatim spans commit ~20–30 tokens per forward. Measured (8-bit, M4 Pro, outputs bit-identical): gemma-12B file re-emission 3.0×→4.5× (75 tok/s), Ornith-9B rename-edit 2.8×→3.6× (93 tok/s); chat unchanged. An acceptance gate parks the scaling on insertion-heavy edits (measured neutral there). - DFlash —
--max-draft 0(full 16-block) is its native point and reaches ~6 accepted tokens on code/math; on current mlx that still measures below DSpark cap-2 on this M4 Pro (see the pick table), so treat DFlash as the head-to-head benchmark option rather than the speed pick. Short caps on open chat; the full block never fills there. - Sampling —
--temperature > 0(+--top-p/--top-k) is lossless w.r.t. the target at temperature T (the paper's §2.1 method). On M-series it's ≈ greedy speed (the extra acceptance lives in a tail a short cap never reaches) — it's a sampled-output feature, not a speed lever.
License
MIT — see LICENSE. An independent MLX port of the inference path of DeepSeek's DSpark drafter;
the z-lab DFlash drafter classes are vendored (MIT) with attribution in NOTICE. No model weights
are bundled.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mlx_dspark-0.5.0.tar.gz.
File metadata
- Download URL: mlx_dspark-0.5.0.tar.gz
- Upload date:
- Size: 7.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fda5309ac2ad0046fb8c7e298c0b0850a64c767264b2f4730c3d7c2086913fea
|
|
| MD5 |
0c9dbaf71bbd2820492654b59e25eeed
|
|
| BLAKE2b-256 |
3b80673fead18d3e5d7f2397b616a0f19dfb0810c43599c693041d302dbc743c
|
File details
Details for the file mlx_dspark-0.5.0-py3-none-any.whl.
File metadata
- Download URL: mlx_dspark-0.5.0-py3-none-any.whl
- Upload date:
- Size: 119.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd7ac267970a1107a90e9ec6629a7621b163438e4f96b7f19d779651c76f3582
|
|
| MD5 |
057e423de53f084ebce7cedc71807a42
|
|
| BLAKE2b-256 |
8433f196d6949c6967d23e00fe5176d67e567e1dc87cc735861b11c12da92249
|