Qwen3.5-0.8B DSpark drafter (v0.3, experimental)
1
41 commits
7 linked in READMEs
updated Jul 31, 2026
Card status (2026-07-25, corrected): all numbers below are measured. The matched-vs-generic A/B has landed (see its section) and the vLLM smoke test passed (see How to run). 2026-07-31: vLLM acceptance fixed and verified at llama.cpp parity (0.155 vs 0.158 per drafted token). The earlier vLLM shortfall was a
config.jsonrope bug in this repo (partial_rotary_factorinherited from the target), not a runtime difference; the shipped config now sets it to 1.0 explicitly. See the vLLM and cross-stack sections. Correction: an earlier version of this card described a "quant tax" and claimed a bf16-trained head loses most of its acceptance on aggressive quants. Follow-up work refuted that: acceptance tracks whether the target is still coherent, not the precision gap between head and target. The affected sections are rewritten below, with the evidence.
A DSpark speculative-decoding draft head for Qwen/Qwen3.5-0.8B, trained from scratch with the DeepSpec toolkit via our fork branch satindergrewal/DeepSpec:qwen35-lane (qwen3.5-family target support, online-training mode merged from PR #23 by Ofir408, warm-start hook). As far as we know this is the first published DSpark head for a vanilla Qwen3.5 model (see DeepSpec issue #22, an open request for exactly these heads).
Status: v0.3 released. Use head_q08_v03.gguf. v0.3 doubles the self-distilled
corpus (27,576 samples, 2.1x v0.2) with the recipe otherwise pinned, including the batch
config the v0.2 caveat below promised to pin (global batch 128, matching v0.2). Acceptance
improves monotonically in every measured cell; see the v0.3 section. v0.1 and v0.2 remain in
the repo for reproducibility.
Previous status: v0.2 released, head_q08_v02.gguf. v0.2 keeps v0.1's recipe, size, and sample
count and trains on far richer self-distilled data: full-length target responses instead of
truncated ones, roughly 9x the supervised tokens. That bought +30-49% relative acceptance (see
the v0.2 section below). v0.1 remains in the repo for reproducibility, and the measurement
sections below (economics ladder, target-quant ladder, per-dataset eval) were measured on v0.1
unless labelled otherwise; the relative structure of those ladders is head-version-independent.
v0.3 changes exactly one variable against v0.2: corpus size. Same generation pipeline (responses regenerated by the target itself), same 10 epochs, same global batch 128 (the batch confound v0.2 flagged is pinned this time), same everything else. 27,576 kept samples against 13,118.
Gate: identical to the v0.2 gate. llama.cpp --spec-type draft-dspark --spec-draft-n-max 7,
greedy, 200-token completions, two fixed prompts (P1 code, P2 counting), bf16 and Q8_0
targets, hash over reasoning_content plus content (both fields; content alone is often
empty on this reasoning model).
| head | samples | bf16 P1 | bf16 P2 | Q8 P1 | Q8 P2 | mean len (P1) |
|---|---|---|---|---|---|---|
| v0.1 | 12.2k | 0.116 | 0.152 | 0.116 | 0.129 | 1.80 |
| v0.2 | 13.1k | 0.173 | 0.194 | 0.173 | 0.185 | 2.19 |
| v0.3 | 27.6k | 0.259 | 0.211 | 0.253 | 0.197 | 2.80 |

| samples (k) | P1 acceptance |
|---|---|
| 12.2 | 0.116 |
| 13.1 | 0.173 |
| 27.6 | 0.259 |

Training: 2,150 steps (5 epochs at 27.5k samples), final loss about 1.06 from 2.8. The whole train-export-gate sequence ran unattended: a watcher keyed on the trainer PID exiting plus a size-stable final checkpoint fired the export and the three-head gate with no human in the loop.
The D1 claim this card previously held back is now measured at scale: quantizing the MTP drafter block of a 744B-class MoE (GLM-5.2, blk.78 experts to q4_K) reclaimed 2.32 GiB of VRAM with byte-identical outputs and bit-identical acceptance rates across three independent runs. Verification makes drafter-side precision a pure throughput knob, from 0.8B heads to 744B internal drafters.
Correction (2026-07-28): the first version of this section claimed v0.1 trained on generic dataset responses and v0.2 on target-regenerated ones, framing this as a distribution-matching experiment. That was wrong: both versions trained on self-distilled data (responses regenerated by the target itself, same public prompt pool, same temperature). The real difference, found by auditing the training files: v0.1's regeneration capped responses at ~110 tokens on average (426 chars), while v0.2's ran to full length (~3,500 chars avg). v0.2 saw ~9x the supervised tokens, almost entirely from response length. The acceptance gains are real; the correct attribution is supervision volume (and long-form responses), not distribution matching.
Controls that held: identical architecture, block size, cold-start, epoch count (10 passes), and prompt source; near-identical sample count (13.1K vs 12.0K). Variables that differed: per-response length (~9x supervised tokens), a fresh prompt draw, and the global batch (documented below). Every training response was regenerated by the bf16 target itself (temperature 0.7, thinking disabled) instead of coming from the generic open-perfectblend pairs.
| probe (bf16 target, Metal, same session) | v0.1 | v0.2 | relative |
|---|---|---|---|
| code (binary search) acceptance | 0.1159 (88/759) | 0.1725 (108/626) | +49% |
| counting acceptance | 0.1518 (102/672) | 0.1976 (115/582) | +30% |
| tok/s under speculation | 73.9 / 84.1 | 89.3 / 96.8 | +21% / +15% |


(Training-loss curves are epoch-normalized because the step counts differ, and the two losses are computed on different data, so the gap between curves is not itself a quality claim; the acceptance table above is the comparison that counts. Final losses: 1.64 vs 1.26.)
Notes for anyone reproducing:
llama-server (bf16, 16 parallel slots) using
the fork's generate_train_data.py --resume; features captured with the GGUF capture pipeline
documented in the training guide linked below.
DSpark runtime support is in PR #25173 plus this model family's support on our branch. Until both are in mainline, build the branch:
git clone --branch dspark-qwen35 https://github.com/satindergrewal/llama.cpp
cd llama.cpp
cmake -B build -DGGML_CUDA=ON # Metal is the default on Apple Silicon
cmake --build build -j --target llama-server
Get the head and a target:
hf download satgeze/Qwen3.5-0.8B-DSpark Qwen3.5-0.8B-DSpark.gguf --local-dir .
hf download unsloth/Qwen3.5-0.8B-GGUF Qwen3.5-0.8B-BF16.gguf --local-dir .
Serve with speculative decoding:
./build/bin/llama-server \
-m Qwen3.5-0.8B-BF16.gguf \
-md Qwen3.5-0.8B-DSpark.gguf \
--spec-type draft-dspark --spec-draft-n-max 7 \
-c 4096 -ngl 99 -fa on --jinja --port 8080
--spec-draft-n-max 7 matters: llama.cpp defaults to drafting 3 tokens, but this head was
trained for a block of 7, so the default discards part of the (already marginal) speedup. Set it to
the head's block size, printed at startup as block_size=.
Any OpenAI-compatible client works against http://localhost:8080/v1; the server log prints
draft acceptance per request. Note the economics section below before deploying this pair:
on fast hardware this 0.8B target outruns its own drafter.
vLLM has DSpark support in mainline since
#46995 (merged 2026-07-01, DeepSpec-format
heads) and #47093 (merged 2026-07-02,
speculators-format checkpoints). Requires vLLM >= 0.25 (0.24.0 predates the merges and
rejects "method": "dspark"). This repo's safetensors are DeepSpec-format with
architectures: ["Qwen3DSparkModel"], the exact key vLLM's registry maps for DSpark drafters.
Verified 2026-07-31 on vLLM 0.25.1 with this head (greedy, 200-token code completion): 0.155 acceptance per drafted token, position-0 acceptance 0.55, matching the llama.cpp numbers on the same probe. The mrope/rope code path is byte-identical between 0.25.1 and 0.26.x, so this carries over.
Config note (2026-07-31 fix): an earlier revision of this repo's config.json inherited
the target's rope geometry (partial_rotary_factor: 0.25 plus mrope keys) while the head was
trained with plain full-dimension RoPE; vLLM rotated only a quarter of each head and
acceptance collapsed (position 0 ~0.26, later positions ~0). The shipped config now sets
partial_rotary_factor: 1.0 explicitly; deleting the key is not enough, because the
qwen3_5_text config class re-injects the 0.25 family default at parse time. If you
downloaded config.json before 2026-07-31, re-download it.
vllm serve Qwen/Qwen3.5-0.8B \
--speculative-config '{"method": "dspark", "model": "satgeze/Qwen3.5-0.8B-DSpark", "num_speculative_tokens": 7}'
Setup notes from our smoke test (RTX Pro 6000 Blackwell, sm_120a): flashinfer's device
detection can fail in the engine subprocess and raise a misleading "requires sm75 or higher";
fix with FLASHINFER_CUDA_ARCH_LIST="12.0a". First run JIT-compiles kernels (needs ninja and
nvcc on PATH). With a 248K-vocab model, default warmup can OOM even on 96GB; lower
--gpu-memory-utilization / --max-num-seqs if it does.
DSpark support is on SGLang's roadmap: watch the tracking issue sgl-project/sglang#30344 for the landing PRs and the up-to-date run method. We will add the command here once it ships; everything on this card runs today with the llama.cpp commands above.
| metric | value |
|---|---|
| draft acceptance (code prompt) | 0.116 |
| draft acceptance (counting) | 0.152 |
| draft acceptance (no-think prompt) | 0.158 |
| target baseline decode | ~500 t/s |
| with DSpark | 198-250 t/s (net slowdown) |
| metric | value |
|---|---|
| draft acceptance (code prompt) | 0.1159 (88/759, identical accepted count to CUDA) |
| draft acceptance (counting) | 0.1518 |
| target baseline decode | 146-147 t/s |
| with DSpark | 74-84 t/s (net slowdown) |
Acceptance on Metal matches CUDA to the third decimal on byte-identical GGUFs: the drafter's behavior is backend-independent. Only the economics change per platform.

| target rung | baseline t/s | with DSpark t/s | net |
|---|---|---|---|
| CUDA Pro 6000, bf16 | ~500 | 198-250 | 0.45x |
| Metal bf16 | 142-146 | 74-84 | 0.55x |
| Metal Q8_0 | 185-187 | 74-86 | 0.43x |
| Metal Q4_K_M | 213-217 | 65-83 | 0.34x |
| Metal Q2_K | ~219 | 46-69 | 0.26x |
| Metal IQ1_S (imatrix) | 208-223 | 44-50 | 0.22x |
| output len | coding spec / base | writing spec / base | ratio |
|---|---|---|---|
| 256 | 79.1 / 138.3 t/s | 77.2 / 138.1 t/s | 0.57x / 0.56x |
| 1024 | 80.6 / 140.2 t/s | 51.3 / 140.4 t/s | 0.57x / 0.37x |
Aggregate draft acceptance across the diverse SPEED-Bench prompts: 0.126-0.139, consistent with the greedy probes. The verdict below holds on varied real prompts, not just narrow probes.
A 0.8B target is structurally a poor speculation host: the smaller and faster the target gets, the worse speculation pays, because draft+verify overhead is roughly constant while the baseline accelerates. Break-even for this pair on Metal sits around 0.47 acceptance, which no 12K-sample head reaches. Speculation value starts around 9B-class targets (we measure 1.62x overall / 2.19x coding on a 9B with a fully-trained head, and 2.54-2.67x on a 27B: satgeze/Qwen3.6-27B-DSpark).
Acceptance says how fast the drafter makes the target go. It says nothing about whether the target's long-form output is usable. Measured separately with an independent harness (8 prompts, greedy, 1500 tokens each, real cycle detection, prompt set v1, M3 Max Metal):
| target rung | loop rate (greedy, 1500 tok) |
|---|---|
| bf16 (16 bpw) | 7/7 (100%) |
| Q8_0 (8.5 bpw) | 6/7 (86%) |
| Q4_K_M (4.8 bpw) | 7/8 (88%) |
| Q2_K (2.6 bpw) | 8/8 (100%) |
| IQ1_S (1.6 bpw) | 6/6 (100%) |
This is flat, and it is flat at the ceiling. The model loops on essentially every long greedy generation even at full bf16 precision, so here looping is a property of the 0.8B model under long greedy decoding, not a quantization effect. For contrast, the same harness on a 27B target finds zero loops at every rung down to 2.9 bpw, and only breaks below 2 bpw. Model scale, not bit-width, dominates this failure mode.
Mitigation, measured on the same rungs: enabling the DRY sampler (temperature 0.7) cuts the bf16 loop rate from 100% to 38%, but leaves Q4_K_M at 88%. So DRY helps and is worth enabling, but it does not make this target reliable for long free-form generation. Use this head for what the rest of the card describes (short completions, pipeline validation, quantization studies), not for long-form or agentic runs.
(Loop rates are only comparable within the same prompt set, token budget and decoding settings,
and were all measured on Metal. Do not compare them against rates measured on another backend.
Two further scoping notes: these are raw /completion probes, deliberately worst-case; chat-template
serving with a repetition sampler behaves measurably better, so state which mode you measured. And
if you measure a reasoning model, count reasoning tokens: a loop can live entirely inside the think
field while visible content stays short, and a metric that reads only content scores a looping
model as clean.)
Same head (trained vs the bf16 target), same probes, only the target quantization changed. All rungs quantized from the same bf16 GGUF; the IQ1_S rung uses an imatrix computed on this model's own regeneration data (600 samples), measured on M3 Max Metal:
| target | code probe acc | counting probe acc |
|---|---|---|
| bf16 (16 bpw) | 0.1159 | 0.1518 |
| Q8_0 (8.5 bpw) | 0.1159 | 0.1570 |
| Q4_K_M (4.8 bpw) | 0.1281 | 0.2010 |
| Q2_K (2.6 bpw, no imatrix) | 0.0472 | 0.1358 |
| IQ1_S (1.6 bpw, imatrix) | 0.0493 | 0.0256 |

The bf16-trained head is effectively free down to ~4-bit targets. Below that, this 0.8B target
itself stops being a coherent model and acceptance falls with it. The Q2_K row here is
uncalibrated: on our 27B ladder, adding an own-distribution imatrix at the same bit-width moved
acceptance from 0.149 to 0.251, and a calibrated 1.6-bpw IQ1_S 27B target held Q8-level
acceptance (0.293) with the same bf16-trained-head recipe. So what collapses at the bottom of
this table is the target, not the head. The same law, measured on the official
deepseek-ai/dspark_qwen3_4b_block7 head in the same runtime, for reference:
| target precision (4B head, reference) | draft acceptance |
|---|---|
| Q8_0 (near-lossless) | 0.365 |
| Q2_K | 0.220 |
| IQ1_S (~1.5-bit, imatrix) | 0.074 |
Practical consequences:
We trained two additional heads with identical config/seed/steps, differing ONLY in where the training features came from: hidden states captured from the bf16 GGUF vs from the Q8_0 GGUF, both captured with our llama.cpp capture tool (this model is the testbed; caches validated at cosine >= 0.9998 against the HF pipeline). Acceptance matrix (code probe / counting probe):
| head trained on | vs bf16 target | vs Q8_0 target |
|---|---|---|
| bf16-captured features | 0.126 / 0.152 | 0.123 / 0.124 |
| Q8-captured features | 0.116 / 0.150 | 0.116 / 0.134 |
Two results:
One trajectory-matched comparison, everything pinned that can be pinned: the same 8 prompts
(code, math, prose), temperature 0, max_tokens 256, sequential single requests, speculative
depth 7 on both stacks, bf16 target weights on both sides (HF safetensors in vLLM, the GGUF
conversion of the same weights in llama.cpp), and this same step-450 head (safetensors in
vLLM's --speculative-config '{"method": "dspark", ...}', the GGUF conversion via -md in
llama.cpp). Acceptance computed from each stack's own raw counters (llama.cpp per-request
draft acceptance log line; vLLM /metrics spec-decode counters).
| stack | accepted / drafted | acceptance per drafted token | accepted per verify step |
|---|---|---|---|
llama.cpp (--spec-type draft-dspark, RTX Pro 6000) | 1,064 / 6,734 | 15.8% | ~1.11 (mean accepted len 1.66-3.23 across prompts) |
vLLM 0.25.1 (method: dspark, same GPU) | 472 / 10,990 | 4.3% | 0.30 (per-position accepts 356/100/12/2/2/0/0) |
Same weights, same proposals available, roughly 3.7x more drafted tokens survive verification in llama.cpp, on either denominator.
2026-07-31 correction: the 3.7x gap was a config bug in this repo, not a runtime-semantics
difference. The drafter is trained with plain full-dimension RoPE, but the exported
config.json inherited the target's rope geometry (partial_rotary_factor: 0.25 and mrope
keys). vLLM honored those and rotated only 64 of each head's 256 rotary dims, degrading
attention exactly as the vLLM row shows (position 0 partly alive, later positions near
zero). llama.cpp's GGUF path never carried the bad rope keys, which is why it measured full
acceptance all along. With partial_rotary_factor: 1.0 set explicitly, vLLM measures 0.155
per drafted token on the same greedy code probe vs llama.cpp's 0.158: parity. The 4.3% row
is kept above as the honest record of what shipped broken, and the "runtime-semantics"
framing this section carried before is retracted: the runtimes agreed once the config told
the truth.
--spec-type draft-dspark is required. Without it the
server loads the draft model and then silently serves without speculation; the only
visible differences are a missing draft acceptance line in the per-request timings and
faster-than-expected raw decode.| dataset | acceptance | dataset | acceptance | |
|---|---|---|---|---|
| gsm8k | 0.291 | livecodebench | 0.164 | |
| math500 | 0.278 | mt-bench | 0.129 | |
| aime25 | 0.239 | alpaca | 0.114 | |
| humaneval | 0.179 | arena-hard-v2 | 0.104 | |
| mbpp | 0.176 |
Confidence head calibration is notably good even at this data scale: AUC 0.94, ECE 0.018 (mt-bench).
The training signal is self-distillation: the 12K prompts (mlabonne/open-perfectblend) were
answered by the target model itself (served with llama.cpp, thinking disabled, temperature 0.7),
so the drafter learns the exact distribution it will draft for at inference. Hidden states were
captured at layers [1, 6, 11, 16, 21] of 24 with DeepSpec's prepare_target_cache
(11,577 / 11,722 samples valid), then the drafter (block size 7, 5 layers, markov_rank 256,
confidence head) trained for 5 epochs / 450 steps at global batch 128 on a single 96GB GPU,
loss 2.86 to 1.63. Cold start: unlike our
27B head (warm-started from a z-lab DFlash
checkpoint, mean accepted length 5.4), this head was initialized from scratch, and at 12K samples
that is the dominant limitation: mean accepted length ~1.8-2.05, exactly where the community
scaling curve predicts a cold-start 12K head to land.
Two honest findings worth more than the head itself:
--spec-type draft-dspark --spec-draft-n-max 7.Files: head_q08_v02.gguf (v0.2 drafter, recommended, block-7, ready for llama.cpp -md),
model_v02.safetensors + config_v02.json (v0.2 checkpoint, step 1020),
model.safetensors (v0.1 drafter, bf16), Qwen3.5-0.8B-DSpark.gguf (converted, ready for llama.cpp
-md), training config, this card.
DeepSeek (DeepSpec toolkit and DSpark method), wjinxu (llama.cpp DSpark PR #25173), ruixiang63 (DFlash/EAGLE3 foundation in llama.cpp), Ofir408 (DeepSpec online-training mode PR #23), the community heads whose published numbers anchor the scaling curve (dbirks, fal, stanleyphoong, deepseek-ai official), Qwen team (target model), mlabonne (open-perfectblend prompts).
41 commits
Qwen3.5-0.8B DSpark drafter (v0.3, experimental)
1
41 commits
7 linked in READMEs
updated Jul 31, 2026
Card status (2026-07-25, corrected): all numbers below are measured. The matched-vs-generic A/B has landed (see its section) and the vLLM smoke test passed (see How to run). 2026-07-31: vLLM acceptance fixed and verified at llama.cpp parity (0.155 vs 0.158 per drafted token). The earlier vLLM shortfall was a
config.jsonrope bug in this repo (partial_rotary_factorinherited from the target), not a runtime difference; the shipped config now sets it to 1.0 explicitly. See the vLLM and cross-stack sections. Correction: an earlier version of this card described a "quant tax" and claimed a bf16-trained head loses most of its acceptance on aggressive quants. Follow-up work refuted that: acceptance tracks whether the target is still coherent, not the precision gap between head and target. The affected sections are rewritten below, with the evidence.
A DSpark speculative-decoding draft head for Qwen/Qwen3.5-0.8B, trained from scratch with the DeepSpec toolkit via our fork branch satindergrewal/DeepSpec:qwen35-lane (qwen3.5-family target support, online-training mode merged from PR #23 by Ofir408, warm-start hook). As far as we know this is the first published DSpark head for a vanilla Qwen3.5 model (see DeepSpec issue #22, an open request for exactly these heads).
Status: v0.3 released. Use head_q08_v03.gguf. v0.3 doubles the self-distilled
corpus (27,576 samples, 2.1x v0.2) with the recipe otherwise pinned, including the batch
config the v0.2 caveat below promised to pin (global batch 128, matching v0.2). Acceptance
improves monotonically in every measured cell; see the v0.3 section. v0.1 and v0.2 remain in
the repo for reproducibility.
Previous status: v0.2 released, head_q08_v02.gguf. v0.2 keeps v0.1's recipe, size, and sample
count and trains on far richer self-distilled data: full-length target responses instead of
truncated ones, roughly 9x the supervised tokens. That bought +30-49% relative acceptance (see
the v0.2 section below). v0.1 remains in the repo for reproducibility, and the measurement
sections below (economics ladder, target-quant ladder, per-dataset eval) were measured on v0.1
unless labelled otherwise; the relative structure of those ladders is head-version-independent.
v0.3 changes exactly one variable against v0.2: corpus size. Same generation pipeline (responses regenerated by the target itself), same 10 epochs, same global batch 128 (the batch confound v0.2 flagged is pinned this time), same everything else. 27,576 kept samples against 13,118.
Gate: identical to the v0.2 gate. llama.cpp --spec-type draft-dspark --spec-draft-n-max 7,
greedy, 200-token completions, two fixed prompts (P1 code, P2 counting), bf16 and Q8_0
targets, hash over reasoning_content plus content (both fields; content alone is often
empty on this reasoning model).
| head | samples | bf16 P1 | bf16 P2 | Q8 P1 | Q8 P2 | mean len (P1) |
|---|---|---|---|---|---|---|
| v0.1 | 12.2k | 0.116 | 0.152 | 0.116 | 0.129 | 1.80 |
| v0.2 | 13.1k | 0.173 | 0.194 | 0.173 | 0.185 | 2.19 |
| v0.3 | 27.6k | 0.259 | 0.211 | 0.253 | 0.197 | 2.80 |

| samples (k) | P1 acceptance |
|---|---|
| 12.2 | 0.116 |
| 13.1 | 0.173 |
| 27.6 | 0.259 |

Training: 2,150 steps (5 epochs at 27.5k samples), final loss about 1.06 from 2.8. The whole train-export-gate sequence ran unattended: a watcher keyed on the trainer PID exiting plus a size-stable final checkpoint fired the export and the three-head gate with no human in the loop.
The D1 claim this card previously held back is now measured at scale: quantizing the MTP drafter block of a 744B-class MoE (GLM-5.2, blk.78 experts to q4_K) reclaimed 2.32 GiB of VRAM with byte-identical outputs and bit-identical acceptance rates across three independent runs. Verification makes drafter-side precision a pure throughput knob, from 0.8B heads to 744B internal drafters.
Correction (2026-07-28): the first version of this section claimed v0.1 trained on generic dataset responses and v0.2 on target-regenerated ones, framing this as a distribution-matching experiment. That was wrong: both versions trained on self-distilled data (responses regenerated by the target itself, same public prompt pool, same temperature). The real difference, found by auditing the training files: v0.1's regeneration capped responses at ~110 tokens on average (426 chars), while v0.2's ran to full length (~3,500 chars avg). v0.2 saw ~9x the supervised tokens, almost entirely from response length. The acceptance gains are real; the correct attribution is supervision volume (and long-form responses), not distribution matching.
Controls that held: identical architecture, block size, cold-start, epoch count (10 passes), and prompt source; near-identical sample count (13.1K vs 12.0K). Variables that differed: per-response length (~9x supervised tokens), a fresh prompt draw, and the global batch (documented below). Every training response was regenerated by the bf16 target itself (temperature 0.7, thinking disabled) instead of coming from the generic open-perfectblend pairs.
| probe (bf16 target, Metal, same session) | v0.1 | v0.2 | relative |
|---|---|---|---|
| code (binary search) acceptance | 0.1159 (88/759) | 0.1725 (108/626) | +49% |
| counting acceptance | 0.1518 (102/672) | 0.1976 (115/582) | +30% |
| tok/s under speculation | 73.9 / 84.1 | 89.3 / 96.8 | +21% / +15% |


(Training-loss curves are epoch-normalized because the step counts differ, and the two losses are computed on different data, so the gap between curves is not itself a quality claim; the acceptance table above is the comparison that counts. Final losses: 1.64 vs 1.26.)
Notes for anyone reproducing:
llama-server (bf16, 16 parallel slots) using
the fork's generate_train_data.py --resume; features captured with the GGUF capture pipeline
documented in the training guide linked below.
DSpark runtime support is in PR #25173 plus this model family's support on our branch. Until both are in mainline, build the branch:
git clone --branch dspark-qwen35 https://github.com/satindergrewal/llama.cpp
cd llama.cpp
cmake -B build -DGGML_CUDA=ON # Metal is the default on Apple Silicon
cmake --build build -j --target llama-server
Get the head and a target:
hf download satgeze/Qwen3.5-0.8B-DSpark Qwen3.5-0.8B-DSpark.gguf --local-dir .
hf download unsloth/Qwen3.5-0.8B-GGUF Qwen3.5-0.8B-BF16.gguf --local-dir .
Serve with speculative decoding:
./build/bin/llama-server \
-m Qwen3.5-0.8B-BF16.gguf \
-md Qwen3.5-0.8B-DSpark.gguf \
--spec-type draft-dspark --spec-draft-n-max 7 \
-c 4096 -ngl 99 -fa on --jinja --port 8080
--spec-draft-n-max 7 matters: llama.cpp defaults to drafting 3 tokens, but this head was
trained for a block of 7, so the default discards part of the (already marginal) speedup. Set it to
the head's block size, printed at startup as block_size=.
Any OpenAI-compatible client works against http://localhost:8080/v1; the server log prints
draft acceptance per request. Note the economics section below before deploying this pair:
on fast hardware this 0.8B target outruns its own drafter.
vLLM has DSpark support in mainline since
#46995 (merged 2026-07-01, DeepSpec-format
heads) and #47093 (merged 2026-07-02,
speculators-format checkpoints). Requires vLLM >= 0.25 (0.24.0 predates the merges and
rejects "method": "dspark"). This repo's safetensors are DeepSpec-format with
architectures: ["Qwen3DSparkModel"], the exact key vLLM's registry maps for DSpark drafters.
Verified 2026-07-31 on vLLM 0.25.1 with this head (greedy, 200-token code completion): 0.155 acceptance per drafted token, position-0 acceptance 0.55, matching the llama.cpp numbers on the same probe. The mrope/rope code path is byte-identical between 0.25.1 and 0.26.x, so this carries over.
Config note (2026-07-31 fix): an earlier revision of this repo's config.json inherited
the target's rope geometry (partial_rotary_factor: 0.25 plus mrope keys) while the head was
trained with plain full-dimension RoPE; vLLM rotated only a quarter of each head and
acceptance collapsed (position 0 ~0.26, later positions ~0). The shipped config now sets
partial_rotary_factor: 1.0 explicitly; deleting the key is not enough, because the
qwen3_5_text config class re-injects the 0.25 family default at parse time. If you
downloaded config.json before 2026-07-31, re-download it.
vllm serve Qwen/Qwen3.5-0.8B \
--speculative-config '{"method": "dspark", "model": "satgeze/Qwen3.5-0.8B-DSpark", "num_speculative_tokens": 7}'
Setup notes from our smoke test (RTX Pro 6000 Blackwell, sm_120a): flashinfer's device
detection can fail in the engine subprocess and raise a misleading "requires sm75 or higher";
fix with FLASHINFER_CUDA_ARCH_LIST="12.0a". First run JIT-compiles kernels (needs ninja and
nvcc on PATH). With a 248K-vocab model, default warmup can OOM even on 96GB; lower
--gpu-memory-utilization / --max-num-seqs if it does.
DSpark support is on SGLang's roadmap: watch the tracking issue sgl-project/sglang#30344 for the landing PRs and the up-to-date run method. We will add the command here once it ships; everything on this card runs today with the llama.cpp commands above.
| metric | value |
|---|---|
| draft acceptance (code prompt) | 0.116 |
| draft acceptance (counting) | 0.152 |
| draft acceptance (no-think prompt) | 0.158 |
| target baseline decode | ~500 t/s |
| with DSpark | 198-250 t/s (net slowdown) |
| metric | value |
|---|---|
| draft acceptance (code prompt) | 0.1159 (88/759, identical accepted count to CUDA) |
| draft acceptance (counting) | 0.1518 |
| target baseline decode | 146-147 t/s |
| with DSpark | 74-84 t/s (net slowdown) |
Acceptance on Metal matches CUDA to the third decimal on byte-identical GGUFs: the drafter's behavior is backend-independent. Only the economics change per platform.

| target rung | baseline t/s | with DSpark t/s | net |
|---|---|---|---|
| CUDA Pro 6000, bf16 | ~500 | 198-250 | 0.45x |
| Metal bf16 | 142-146 | 74-84 | 0.55x |
| Metal Q8_0 | 185-187 | 74-86 | 0.43x |
| Metal Q4_K_M | 213-217 | 65-83 | 0.34x |
| Metal Q2_K | ~219 | 46-69 | 0.26x |
| Metal IQ1_S (imatrix) | 208-223 | 44-50 | 0.22x |
| output len | coding spec / base | writing spec / base | ratio |
|---|---|---|---|
| 256 | 79.1 / 138.3 t/s | 77.2 / 138.1 t/s | 0.57x / 0.56x |
| 1024 | 80.6 / 140.2 t/s | 51.3 / 140.4 t/s | 0.57x / 0.37x |
Aggregate draft acceptance across the diverse SPEED-Bench prompts: 0.126-0.139, consistent with the greedy probes. The verdict below holds on varied real prompts, not just narrow probes.
A 0.8B target is structurally a poor speculation host: the smaller and faster the target gets, the worse speculation pays, because draft+verify overhead is roughly constant while the baseline accelerates. Break-even for this pair on Metal sits around 0.47 acceptance, which no 12K-sample head reaches. Speculation value starts around 9B-class targets (we measure 1.62x overall / 2.19x coding on a 9B with a fully-trained head, and 2.54-2.67x on a 27B: satgeze/Qwen3.6-27B-DSpark).
Acceptance says how fast the drafter makes the target go. It says nothing about whether the target's long-form output is usable. Measured separately with an independent harness (8 prompts, greedy, 1500 tokens each, real cycle detection, prompt set v1, M3 Max Metal):
| target rung | loop rate (greedy, 1500 tok) |
|---|---|
| bf16 (16 bpw) | 7/7 (100%) |
| Q8_0 (8.5 bpw) | 6/7 (86%) |
| Q4_K_M (4.8 bpw) | 7/8 (88%) |
| Q2_K (2.6 bpw) | 8/8 (100%) |
| IQ1_S (1.6 bpw) | 6/6 (100%) |
This is flat, and it is flat at the ceiling. The model loops on essentially every long greedy generation even at full bf16 precision, so here looping is a property of the 0.8B model under long greedy decoding, not a quantization effect. For contrast, the same harness on a 27B target finds zero loops at every rung down to 2.9 bpw, and only breaks below 2 bpw. Model scale, not bit-width, dominates this failure mode.
Mitigation, measured on the same rungs: enabling the DRY sampler (temperature 0.7) cuts the bf16 loop rate from 100% to 38%, but leaves Q4_K_M at 88%. So DRY helps and is worth enabling, but it does not make this target reliable for long free-form generation. Use this head for what the rest of the card describes (short completions, pipeline validation, quantization studies), not for long-form or agentic runs.
(Loop rates are only comparable within the same prompt set, token budget and decoding settings,
and were all measured on Metal. Do not compare them against rates measured on another backend.
Two further scoping notes: these are raw /completion probes, deliberately worst-case; chat-template
serving with a repetition sampler behaves measurably better, so state which mode you measured. And
if you measure a reasoning model, count reasoning tokens: a loop can live entirely inside the think
field while visible content stays short, and a metric that reads only content scores a looping
model as clean.)
Same head (trained vs the bf16 target), same probes, only the target quantization changed. All rungs quantized from the same bf16 GGUF; the IQ1_S rung uses an imatrix computed on this model's own regeneration data (600 samples), measured on M3 Max Metal:
| target | code probe acc | counting probe acc |
|---|---|---|
| bf16 (16 bpw) | 0.1159 | 0.1518 |
| Q8_0 (8.5 bpw) | 0.1159 | 0.1570 |
| Q4_K_M (4.8 bpw) | 0.1281 | 0.2010 |
| Q2_K (2.6 bpw, no imatrix) | 0.0472 | 0.1358 |
| IQ1_S (1.6 bpw, imatrix) | 0.0493 | 0.0256 |

The bf16-trained head is effectively free down to ~4-bit targets. Below that, this 0.8B target
itself stops being a coherent model and acceptance falls with it. The Q2_K row here is
uncalibrated: on our 27B ladder, adding an own-distribution imatrix at the same bit-width moved
acceptance from 0.149 to 0.251, and a calibrated 1.6-bpw IQ1_S 27B target held Q8-level
acceptance (0.293) with the same bf16-trained-head recipe. So what collapses at the bottom of
this table is the target, not the head. The same law, measured on the official
deepseek-ai/dspark_qwen3_4b_block7 head in the same runtime, for reference:
| target precision (4B head, reference) | draft acceptance |
|---|---|
| Q8_0 (near-lossless) | 0.365 |
| Q2_K | 0.220 |
| IQ1_S (~1.5-bit, imatrix) | 0.074 |
Practical consequences:
We trained two additional heads with identical config/seed/steps, differing ONLY in where the training features came from: hidden states captured from the bf16 GGUF vs from the Q8_0 GGUF, both captured with our llama.cpp capture tool (this model is the testbed; caches validated at cosine >= 0.9998 against the HF pipeline). Acceptance matrix (code probe / counting probe):
| head trained on | vs bf16 target | vs Q8_0 target |
|---|---|---|
| bf16-captured features | 0.126 / 0.152 | 0.123 / 0.124 |
| Q8-captured features | 0.116 / 0.150 | 0.116 / 0.134 |
Two results:
One trajectory-matched comparison, everything pinned that can be pinned: the same 8 prompts
(code, math, prose), temperature 0, max_tokens 256, sequential single requests, speculative
depth 7 on both stacks, bf16 target weights on both sides (HF safetensors in vLLM, the GGUF
conversion of the same weights in llama.cpp), and this same step-450 head (safetensors in
vLLM's --speculative-config '{"method": "dspark", ...}', the GGUF conversion via -md in
llama.cpp). Acceptance computed from each stack's own raw counters (llama.cpp per-request
draft acceptance log line; vLLM /metrics spec-decode counters).
| stack | accepted / drafted | acceptance per drafted token | accepted per verify step |
|---|---|---|---|
llama.cpp (--spec-type draft-dspark, RTX Pro 6000) | 1,064 / 6,734 | 15.8% | ~1.11 (mean accepted len 1.66-3.23 across prompts) |
vLLM 0.25.1 (method: dspark, same GPU) | 472 / 10,990 | 4.3% | 0.30 (per-position accepts 356/100/12/2/2/0/0) |
Same weights, same proposals available, roughly 3.7x more drafted tokens survive verification in llama.cpp, on either denominator.
2026-07-31 correction: the 3.7x gap was a config bug in this repo, not a runtime-semantics
difference. The drafter is trained with plain full-dimension RoPE, but the exported
config.json inherited the target's rope geometry (partial_rotary_factor: 0.25 and mrope
keys). vLLM honored those and rotated only 64 of each head's 256 rotary dims, degrading
attention exactly as the vLLM row shows (position 0 partly alive, later positions near
zero). llama.cpp's GGUF path never carried the bad rope keys, which is why it measured full
acceptance all along. With partial_rotary_factor: 1.0 set explicitly, vLLM measures 0.155
per drafted token on the same greedy code probe vs llama.cpp's 0.158: parity. The 4.3% row
is kept above as the honest record of what shipped broken, and the "runtime-semantics"
framing this section carried before is retracted: the runtimes agreed once the config told
the truth.
--spec-type draft-dspark is required. Without it the
server loads the draft model and then silently serves without speculation; the only
visible differences are a missing draft acceptance line in the per-request timings and
faster-than-expected raw decode.| dataset | acceptance | dataset | acceptance | |
|---|---|---|---|---|
| gsm8k | 0.291 | livecodebench | 0.164 | |
| math500 | 0.278 | mt-bench | 0.129 | |
| aime25 | 0.239 | alpaca | 0.114 | |
| humaneval | 0.179 | arena-hard-v2 | 0.104 | |
| mbpp | 0.176 |
Confidence head calibration is notably good even at this data scale: AUC 0.94, ECE 0.018 (mt-bench).
The training signal is self-distillation: the 12K prompts (mlabonne/open-perfectblend) were
answered by the target model itself (served with llama.cpp, thinking disabled, temperature 0.7),
so the drafter learns the exact distribution it will draft for at inference. Hidden states were
captured at layers [1, 6, 11, 16, 21] of 24 with DeepSpec's prepare_target_cache
(11,577 / 11,722 samples valid), then the drafter (block size 7, 5 layers, markov_rank 256,
confidence head) trained for 5 epochs / 450 steps at global batch 128 on a single 96GB GPU,
loss 2.86 to 1.63. Cold start: unlike our
27B head (warm-started from a z-lab DFlash
checkpoint, mean accepted length 5.4), this head was initialized from scratch, and at 12K samples
that is the dominant limitation: mean accepted length ~1.8-2.05, exactly where the community
scaling curve predicts a cold-start 12K head to land.
Two honest findings worth more than the head itself:
--spec-type draft-dspark --spec-draft-n-max 7.Files: head_q08_v02.gguf (v0.2 drafter, recommended, block-7, ready for llama.cpp -md),
model_v02.safetensors + config_v02.json (v0.2 checkpoint, step 1020),
model.safetensors (v0.1 drafter, bf16), Qwen3.5-0.8B-DSpark.gguf (converted, ready for llama.cpp
-md), training config, this card.
DeepSeek (DeepSpec toolkit and DSpark method), wjinxu (llama.cpp DSpark PR #25173), ruixiang63 (DFlash/EAGLE3 foundation in llama.cpp), Ofir408 (DeepSpec online-training mode PR #23), the community heads whose published numbers anchor the scaling curve (dbirks, fal, stanleyphoong, deepseek-ai official), Qwen team (target model), mlabonne (open-perfectblend prompts).
41 commits