Skip to content

spec: add DSpark speculative decoding#25173

Open
wjinxu wants to merge 7 commits into
ggml-org:masterfrom
wjinxu:dspark-upstream
Open

spec: add DSpark speculative decoding#25173
wjinxu wants to merge 7 commits into
ggml-org:masterfrom
wjinxu:dspark-upstream

Conversation

@wjinxu

@wjinxu wjinxu commented Jun 30, 2026

Copy link
Copy Markdown

This PR adds DSpark speculative decoding, layered on the merged DFlash drafter. DSpark (DeepSeek + PKU, 2026 — "Confidence-Scheduled Speculative Decoding with Semi-Autoregressive Generation", the DeepSpec repo) is DFlash plus a small semi-autoregressive Markov head: where DFlash takes an independent argmax at each block position (every position marginalizes over all possible predecessors, so acceptance decays along the block), DSpark adds a low-rank, previous-token-conditioned logit bias and samples the block left-to-right, so each draft conditions on the one actually sampled before it. This lifts accepted length at near-zero extra draft cost.

DSpark reuses the entire DFlash machinery unchanged — the encoder/decoder graph, target-layer feature extraction (llama_set_embeddings_layer_inp / _nextn), KV-cache injection, and the verify/accept path. The only additions are:

  • a new draft architecture dspark (llama_model_dspark : llama_model_dflash) that reuses the DFlash graph and additionally loads the Markov head (markov_w1, markov_w2) and an optional confidence head; it shares the target's token-embeddings / lm_head (same as DFlash);
  • a new speculative type draft-dspark (common_speculative_impl_draft_dspark : common_speculative_impl_draft_dflash) that reuses process() (extraction + injection) and overrides only draft(): the block is anchor-first (position 0 already predicts the first draft token) and sampled with the Markov bias bias(prev) = markov_w2 · markov_w1[prev], computed on-device (llama_dspark_markov_bias);
  • a Qwen3DSparkModel converter.

Greedy decoding is lossless: the Markov bias only changes which tokens are proposed; every draft is still verified against the target, so the output is identical to non-speculative greedy.

The confidence head is converted/loaded but not used at inference in this PR (phase 1); the draft-quality win from the Markov head is self-contained and is what the numbers below measure.

How to run

Complete example from scratch (Qwen3-8B). Drafts for other sizes are on the same org: deepseek-ai/dspark_qwen3_{4b,8b,14b}_block7.

1. Get the models — target + its DSpark draft:

huggingface-cli download Qwen/Qwen3-8B --local-dir Qwen3-8B
huggingface-cli download deepseek-ai/dspark_qwen3_8b_block7 --local-dir dspark_qwen3_8b

2. Convert to GGUF — the draft ships no tokenizer and reuses the target's, so pass --target-model-dir:

python convert_hf_to_gguf.py Qwen3-8B --outtype bf16 --outfile Qwen3-8B.gguf
python convert_hf_to_gguf.py dspark_qwen3_8b --outtype bf16 \
    --target-model-dir Qwen3-8B --outfile Qwen3-8B-DSpark.gguf

You may quantize the target (e.g. llama-quantize Qwen3-8B.gguf Qwen3-8B-Q4_K_M.gguf Q4_K_M); keep the draft bf16 — it's tiny, and acceptance is unaffected by target quant.

3. Build with CUDA:

cmake -B build -DGGML_CUDA=ON && cmake --build build --config Release -j

4. Run — the only DSpark-specific flags are -md <draft> and --spec-type draft-dspark
(--spec-draft-n-max = draft tokens per step; the released checkpoints use block size 7):

./build/bin/llama-server -m Qwen3-8B.gguf -md Qwen3-8B-DSpark.gguf \
    --spec-type draft-dspark --spec-draft-n-max 7 \
    --temp 0 --top-k 1 -np 1 -c 4096 -ngl 99 -fa on --jinja

5. Send a request (the server logs draft acceptance = ... per request):

curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{
  "messages": [{"role": "user", "content": "Explain the Pythagorean theorem."}],
  "temperature": 0, "max_tokens": 256
}'

llama-cli works the same way (-m ... -md ... --spec-type draft-dspark). Note: draft-dspark needs the target's hidden states (KV-cache injection), so use llama-server / llama-cli — the speculative-simple example does not drive that path.

Performance

SpeedBench (llama.cpp's own tools/server/bench/speed-bench)

Qwen3-8B (bf16), matched --spec-draft-n-max 7, qualitative split (11 categories), greedy. Baseline is the same server with no draft model. DSpark reaches 1.88× overall decode speedup vs baseline (DFlash is 1.55×), and beats the merged DFlash on every one of the 11 categories (overall 1.21×).

DSpark vs baseline:

category       base_avg_pred_t/s  spec_avg_pred_t/s  decode_speedup  base_avg_latency  spec_avg_latency  latency_speedup  accept_rate
-------------  -----------------  -----------------  --------------  ----------------  ----------------  ---------------  -----------
coding         58.16              123.57             2.12x           11.172s           5.458s            2.05x            0.3219
humanities     58.22              99.06              1.70x           9.573s            5.646s            1.70x            0.2340
math           58.21              109.86             1.89x           10.313s           5.409s            1.91x            0.2840
qa             58.23              107.32             1.84x           8.313s            4.486s            1.85x            0.2659
rag            57.91              123.54             2.13x           9.521s            4.639s            2.05x            0.3264
reasoning      58.21              99.29              1.71x           9.570s            5.622s            1.70x            0.2347
stem           58.19              98.92              1.70x           8.827s            5.205s            1.70x            0.2332
writing        57.82              111.32             1.93x           9.765s            5.282s            1.85x            0.2807
multilingual   58.18              121.96             2.10x           8.691s            4.250s            2.05x            0.3187
summarization  58.36              102.74             1.76x           5.309s            3.001s            1.77x            0.2530
roleplay       58.20              102.56             1.76x           14.139s           8.274s            1.71x            0.2454
overall        58.15              109.10             1.88x           9.563s            5.207s            1.84x            0.2698

DSpark vs the merged DFlash (same --spec-draft-n-max 7):

category       dflash_avg_pred_t/s  dspark_avg_pred_t/s  decode_speedup  dflash_avg_latency  dspark_avg_latency  latency_speedup  accept_rate
-------------  -------------------  -------------------  --------------  ------------------  ------------------  ---------------  -----------
coding         106.00               123.57               1.17x           6.343s              5.458s              1.16x            0.3219
humanities     83.61                99.06                1.18x           6.674s              5.646s              1.18x            0.2340
math           90.48                109.86               1.21x           6.529s              5.409s              1.21x            0.2840
qa             85.20                107.32               1.26x           5.650s              4.486s              1.26x            0.2659
rag            98.61                123.54               1.25x           5.733s              4.639s              1.24x            0.3264
reasoning      83.51                99.29                1.19x           6.681s              5.622s              1.19x            0.2347
stem           83.60                98.92                1.18x           6.154s              5.205s              1.18x            0.2332
writing        90.28                111.32               1.23x           6.443s              5.282s              1.22x            0.2807
multilingual   102.94               121.96               1.18x           5.016s              4.250s              1.18x            0.3187
summarization  85.85                102.74               1.20x           3.606s              3.001s              1.20x            0.2530
roleplay       79.96                102.56               1.28x           10.451s             8.274s              1.26x            0.2454
overall        90.00                109.10               1.21x           6.298s              5.207s              1.21x            0.2698

Hardware: RTX 4090. Target Qwen/Qwen3-8B (bf16), draft deepseek-ai/dspark_qwen3_8b_block7 (bf16). Greedy (--temp 0 --top-k 1), no-thinking, --spec-draft-n-max 7. Baseline = same llama-server with no draft model. DFlash is the merged drafter (z-lab/Qwen3-8B-DFlash, b16), run at the same matched draft size for an apples-to-apples comparison. Per-domain aggregate over the listed prompt counts.

Losslessness

Greedy decoding is lossless by construction (the draft is verified against the target). Output is coherent and matches non-speculative greedy.

Qwen3-4B, target bf16

DSpark vs baseline (DFlash was not benchmarked at 4B — no nested-schema 4B DFlash draft available):

Domain Baseline t/s DSpark t/s (accept) DSpark
GSM8K (40) 103.1 354.0 (75.3%) 3.43×
MATH500 (30) 102.9 341.3 (71.7%) 3.32×
HumanEval (30) 103.9 340.0 (72.9%) 3.27×
MBPP (30) 103.6 281.4 (57.2%) 2.72×
MT-Bench (30) 102.8 190.4 (31.7%) 1.85×
geomean 2.85×

Qwen3-8B, target bf16

Domain Baseline t/s DFlash t/s (accept) DSpark t/s (accept) DFlash DSpark
GSM8K (40) 58.5 182.4 (53.7%) 237.3 (78.9%) 3.12× 4.06×
MATH500 (30) 58.5 195.7 (59.2%) 223.2 (72.8%) 3.35× 3.82×
HumanEval (30) 59.1 238.8 (77.2%) 241.4 (81.7%) 4.04× 4.08×
MBPP (30) 59.6 177.3 (53.3%) 193.1 (63.7%) 2.98× 3.24×
MT-Bench (30) 58.6 93.5 (19.7%) 120.4 (31.3%) 1.60× 2.05×
geomean 2.89× 3.35×

Qwen3-8B, target Q8_0

Domain Baseline t/s DFlash t/s (accept) DSpark t/s (accept) DFlash DSpark
GSM8K (40) 100.6 246.4 (53.2%) 322.9 (77.8%) 2.45× 3.21×
MATH500 (30) 100.5 266.2 (59.2%) 305.7 (72.2%) 2.65× 3.04×
HumanEval (30) 101.3 319.5 (76.5%) 327.9 (81.4%) 3.15× 3.24×
MBPP (30) 102.2 242.4 (54.3%) 268.0 (64.3%) 2.37× 2.62×
MT-Bench (30) 100.7 126.7 (19.3%) 167.8 (31.4%) 1.26× 1.67×
geomean 2.28× 2.68×

Qwen3-8B, target Q4_K_M

Domain Baseline t/s DFlash t/s (accept) DSpark t/s (accept) DFlash DSpark
GSM8K (40) 155.4 259.0 (52.9%) 340.7 (77.4%) 1.67× 2.19×
MATH500 (30) 155.2 284.9 (60.4%) 326.1 (73.9%) 1.84× 2.10×
HumanEval (30) 156.5 314.3 (71.1%) 332.0 (78.4%) 2.01× 2.12×
MBPP (30) 157.5 257.5 (55.5%) 281.3 (66.0%) 1.63× 1.79×
MT-Bench (30) 155.5 135.2 (19.7%) 174.4 (30.6%) 0.87× 1.12×
geomean 1.54× 1.81×

DSpark beats the merged DFlash on every domain (higher accept rate and higher throughput), for a ~1.16× geomean speedup over DFlash. The gains are largest on reasoning (GSM8K +25pp accept, 1.30× over DFlash) and open chat (MT-Bench, 1.29×); on code (HumanEval) the two are close as both already accept ~80%.

Future work

  • Confidence head (phase 2): wire the confidence-scheduled prefix pruning, with the paper's Sequential Temperature Scaling calibration. The big serving win in the paper comes from the batched scheduler, which is a separate, larger change.
  • Markov-bias graph reuse: the bias is computed as a tiny per-step ggml graph on the draft context's scheduler; building it once per block and re-running with new inputs would cut overhead. A fused bias+argmax kernel is a further option but would add a backend-specific op (the current path is pure ggml, no new operator).

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes, use Claude to help discuss and design the DSpark architecture, ask clarifying questions, and assist with writing tests. Everything remains under my control, and I reviewed every single line of AI-generated code.

@github-actions github-actions Bot added model Model specific conversion labels Jun 30, 2026
@ggml-gh-bot

This comment was marked as resolved.

@wjinxu wjinxu force-pushed the dspark-upstream branch from f3b83cd to d74ff77 Compare June 30, 2026 14:16
@github-actions github-actions Bot added the testing Everything test related label Jun 30, 2026
@wjinxu wjinxu force-pushed the dspark-upstream branch from d74ff77 to 37f2513 Compare June 30, 2026 14:39
@wjinxu wjinxu marked this pull request as ready for review June 30, 2026 17:00
@wjinxu wjinxu requested review from a team, CISC, JohannesGaessler and ggerganov as code owners June 30, 2026 17:00
@wjinxu

wjinxu commented Jun 30, 2026

Copy link
Copy Markdown
Author

Hi @CISC @ggerganov , this adds DSpark speculative decoding on top of the merged DFlash drafter (#22105). It's a small change — a new dspark draft arch and draft-dspark spec type that reuse DFlash's graph, feature extraction, KV-cache injection and verify path unchanged; the only new logic is the semi-autoregressive Markov head in draft(). Greedy stays lossless.

I benchmarked it against the merged DFlash using DeepSeek's released Qwen3 DSpark drafts. On Qwen3-8B at bf16 / Q8_0 / Q4_K_M, DSpark beats DFlash on every domain (e.g. GSM8K bf16 4.06× vs 3.12×; full per-domain tables in the PR description).

I believe it's ready for review and I'm happy to walk through any part of it.

@ruixiang63

ruixiang63 commented Jun 30, 2026

Copy link
Copy Markdown
Member

Can you run SpeedBench to do the full comparison between DFlash and DSpark with the same --spec-draft-n-max? https://github.com/ggml-org/llama.cpp/tree/master/tools/server/bench/speed-bench

@CISC CISC requested a review from ruixiang63 June 30, 2026 17:47
Comment thread conversion/qwen.py Outdated
Comment thread conversion/qwen.py Outdated
Comment thread conversion/qwen.py Outdated
@wjinxu

wjinxu commented Jun 30, 2026

Copy link
Copy Markdown
Author

@ruixiang63 I've run the SpeedBench test set as you suggested, and updated the results in the PR description. DSpark does outperform DFlash across the board.

@nipeone

nipeone commented Jul 1, 2026

Copy link
Copy Markdown

could you give some examples how to use?

@wjinxu

wjinxu commented Jul 1, 2026

Copy link
Copy Markdown
Author

could you give some examples how to use?

Good point — I've updated the PR description with a more detailed, copy-pasteable end-to-end example (download → convert → build → run → curl). Let me know if anything's unclear.

Comment thread src/llama-model.cpp
@wjinxu wjinxu force-pushed the dspark-upstream branch from d8b38f2 to 47f3442 Compare July 1, 2026 05:17
@am17an

am17an commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

DSV4 support was merged in #24162, ideally this PR should cover that model as well and try to replicate a similar speedup

@wjinxu

wjinxu commented Jul 1, 2026

Copy link
Copy Markdown
Author

DSV4 support was merged in #24162, ideally this PR should cover that model as well and try to replicate a similar speedup

Thanks! DeepSeek hasn't open-sourced the DSpark weights for DeepSeek-V4 though — only the Qwen3 and Gemma4 drafts are released. So this PR covers Qwen3 for now, and I'll add Gemma4 as a small follow-up.

@am17an

am17an commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

I think they're a part of the spec decoding module https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-DSpark i.e not distributed separately

@wjinxu

wjinxu commented Jul 1, 2026

Copy link
Copy Markdown
Author

I think they're a part of the spec decoding module https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-DSpark i.e not distributed separately

Sorry, and thanks for the heads-up. For this PR I'd like to keep the scope a bit narrower for now - land the Qwen3 DSpark path first and get it solid, then add Gemma4 and DSV4 as follow-ups. Does that sound ok?

@am17an

am17an commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Okay, will try to review. From a cursory look it does not look like adding llama_dspark* is the right thing to do. Honestly don't have a good feeling about the PR, too much AI code.

@wjinxu

wjinxu commented Jul 1, 2026

Copy link
Copy Markdown
Author

Okay, will try to review. From a cursory look it does not look like adding llama_dspark* is the right thing to do. Honestly don't have a good feeling about the PR, too much AI code.

I agree that llama_dspark_* shouldn't be part of the API. The issue is that the Markov bias computation (W2 @ W1[prev]) needs the draft weights and has to run on the backend - I measured it, and doing it on the host is too slow. But the DSpark drafter lives in common/, which can't reach the draft weights, so once the API is removed there's no way to trigger that computation from common/.

@wjinxu

wjinxu commented Jul 2, 2026

Copy link
Copy Markdown
Author

@am17an Thanks for the feedback — you were right about the API. I've reworked the PR:

All llama_dspark_* public APIs are gone. The Markov head is now applied inside the dspark decoder graph(chained per block position, vectorized across blocks), so llama.h / llama-ext.h are untouched and common/speculative.cpp only overrides draft() on top of the DFlash impl, same pattern as before.

I've reviewed and can explain every line of the code myself, and re-verified.

I'd really appreciate another look when you have time.

@wjinxu wjinxu marked this pull request as ready for review July 2, 2026 07:34
Comment thread common/speculative.cpp Outdated
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 4, 2026
@wjinxu

wjinxu commented Jul 4, 2026

Copy link
Copy Markdown
Author

@ruixiang63 Could you take another look at this PR when you have time? Thanks!

@AndrewDBN

Copy link
Copy Markdown

I spent this morning testing wjinxu's DSpark speculative decoding implementation (many thanks for his hard work) and I can confirm that it indeed works as wjinxu described. See the attached writeup. A few remarks:

  1. The DSpark draft model file is definitely not "tiny": for Qwen3-8B, the draft model GGUF file size is 2.3GB, half the size of the Q4_K_M GGUF model file itself!
  2. In my very limited testing I can only confirm a speedup of around 20% versus no speculative decoding.
  3. The main problem I encountered is that Qwen3-8B, despite being only a year old or so, is actually outdated.
  4. On the other hand, I would be extremely interested in testing Gemma4-12B with DSpark speculative decoding, because these days it is my "goto" LLM to run locally on my personal Linux workstation. With MTP speculative decoding I am getting up to 240 tok/s generation with Gemma4-12B.

@wjinxu : please, could you add the code for Gemma4 support? I would be more than willing to test it on my Linux workstation. Thank you!

Attached file is a long description of the testing session:
Testing wjinxu DSpark.txt

@am17an am17an left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I forgot about this PR. It looks okay, but it should have some TODOs in code about how incomplete this implementation is. From what I understand it just adds the autoregressive layer over DFlash which helps in the keeping the generated tokens conditioned on the previous generated tokens. cc @ruixiang63 @ggerganov

@wjinxu

wjinxu commented Jul 8, 2026

Copy link
Copy Markdown
Author

I spent this morning testing wjinxu's DSpark speculative decoding implementation (many thanks for his hard work) and I can confirm that it indeed works as wjinxu described. See the attached writeup. A few remarks:今天早上我测试了 wjinxu 的 DSpark 推测解码实现(非常感谢他的辛勤工作),可以确认它确实如 wjinxu 所描述的那样有效。详见附上的报告。几点说明:

  1. The DSpark draft model file is definitely not "tiny": for Qwen3-8B, the draft model GGUF file size is 2.3GB, half the size of the Q4_K_M GGUF model file itself!DSpark 草稿模型文件绝对算不上“小巧”:对于 Qwen3-8B,草稿模型的 GGUF 文件大小为 2.3GB,是 Q4_K_M GGUF 模型文件本身的一半!
  2. In my very limited testing I can only confirm a speedup of around 20% versus no speculative decoding.在我非常有限的测试中,只能确认相比无推测解码,速度提升了约 20%。
  3. The main problem I encountered is that Qwen3-8B, despite being only a year old or so, is actually outdated.我遇到的主要问题是,Qwen3-8B 虽然才发布一年左右,但实际上已经过时了。
  4. On the other hand, I would be extremely interested in testing Gemma4-12B with DSpark speculative decoding, because these days it is my "goto" LLM to run locally on my personal Linux workstation. With MTP speculative decoding I am getting up to 240 tok/s generation with Gemma4-12B.另一方面,我非常有兴趣测试 Gemma4-12B 配合 DSpark 推测解码的效果,因为目前它是我在个人 Linux 工作站上本地运行的“首选”LLM。使用 MTP 推测解码,Gemma4-12B 的生成速度能达到 240 tok/s。

@wjinxu : please, could you add the code for Gemma4 support? I would be more than willing to test it on my Linux workstation. Thank you!:请问,能否添加对 Gemma4 的支持代码?我非常愿意在我的 Linux 工作站上进行测试。谢谢!

Attached file is a long description of the testing session:附件是测试过程的详细描述: Testing wjinxu DSpark.txt测试 wjinxu DSpark.txt

Thanks for the thorough testing and the detailed writeup — much appreciated! Glad to hear DSpark works as described on your setup.
Sure — I'll submit a draft PR adding Gemma4 support in the next couple of days. I'd be happy to have you test it on your workstation once it's up.

@wjinxu

wjinxu commented Jul 8, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look! You're right — the current implementation only adds the autoregressive layer on top of DFlash, so the drafted tokens stay conditioned on previously generated ones, but the rest is still incomplete. I'll add TODO comments in the code to make the missing pieces explicit.

- confidence head is loaded but not used yet
- confidence-scheduled prefix pruning is not implemented
- the in-graph Markov chain is greedy-only
- only Qwen3 backbones are supported for now (also noted in docs)
@wjinxu

wjinxu commented Jul 8, 2026

Copy link
Copy Markdown
Author

Sorry I forgot about this PR. It looks okay, but it should have some TODOs in code about how incomplete this implementation is. From what I understand it just adds the autoregressive layer over DFlash which helps in the keeping the generated tokens conditioned on the previous generated tokens. cc @ruixiang63 @ggerganov

I've added TODOs marking the incomplete parts, see 1829020. The list:

  • confidence head is loaded but not used yet
  • confidence-scheduled prefix pruning is not implemented
  • the in-graph Markov chain is greedy-only (sampling params don't affect the conditioning path)
  • only Qwen3 backbones are supported for now

I plan to tackle backbone support (Gemma4) first: with the autoregressive Markov head, DSpark already outperforms DFlash at essentially the same draft cost, so the current state is a net win as-is. The confidence-scheduled pruning would come after that.

@ggerganov ggerganov self-assigned this Jul 8, 2026
@ggerganov

Copy link
Copy Markdown
Member

It looks okay, but it should have some TODOs in code about how incomplete this implementation is. From what I understand it just adds the autoregressive layer over DFlash which helps in the keeping the generated tokens conditioned on the previous generated tokens.

@am17an Which parts are missing - I haven't looked at DSpark yet?

@am17an

am17an commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

DSpark also changes the number of verification tokens based on another thing called the "Confidence Head". From the image you can see the verifier drops verifying the last token (C4) based on some confidence thresholds + server load.

image

@ruixiang63

ruixiang63 commented Jul 8, 2026

Copy link
Copy Markdown
Member

Thanks for raising the PR, and sorry for the delay in reviewing it.

I have a few suggestions:

  • Since the current DSpark implementation is based on DFlash and only adds the Markov head for semi-autoregressive decoding based on output logits. I think it should be possible to simplify the code by adding markov head on top of the existing DFlash implementation. This would include the current GGUF model conversion, model loading, and possibly the speculative decoding (the anchor in DSpark paper is exactly id_last in DFlash, and difference is DFlash samples draft tokens from position 1 but DSpark starts from position 0) path as well. Similar to what we added d2t mapping for eagle3, i.e. we don't need a standalone DSpark arch, DSpark can be considered a variant of DFlash IMO.

  • The current DSpark implementation only supports Qwen3, which may not be very useful since Qwen3 is a bit outdated. It would be great to add support for newer models. Since DSpark is based on DFlash, I assume adding support for Gemma 4 models should be relatively straightforward, right?

  • The current implementation is not complete. The missing part is also one of DSpark’s key selling points (i.e. the Confidence Head to truncate draft tokens, let's ignore Hardware-Aware Prefix Scheduler atm), because it can help reduce target model verification time which is quite important. Without that support, I would not consider this PR to be full DSpark support. Personally, I would prefer this PR to support all of these pieces rather than many TODOs since these seems not a very large change, similar to what vLLM did. ([Spec Decode] DSpark vllm-project/vllm#46995)

  • Dspark also supports RNN head in their paper, but I didn't find that in this PR, right? Okay the default setting is Markov head.

Thanks for your work on this. Let’s explore how we can improve it further and keep things moving forward.

@ggerganov ggerganov removed their assignment Jul 8, 2026
Comment thread src/models/models.h Outdated

struct llama_model_dspark : public llama_model_dflash {
llama_model_dspark(const struct llama_model_params & params) : llama_model_dflash(params) {}
// extend the DFlash hparams/tensors with the block size and the Markov / confidence heads

@ruixiang63 ruixiang63 Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Judging from the comment, DFlash already has block size, why we need to extend with block_size here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed — DSpark no longer has its own block size handling.

Comment thread conversion/qwen.py
Comment on lines +678 to +725
@ModelBase.register("Qwen3DSparkModel")
class DSparkModel(Qwen3Model):
# DSpark = DFlash backbone + a semi-autoregressive Markov head (+ optional confidence head).
# The DeepSpec checkpoint stores its config flat (block_size / target_layer_ids / mask_token_id /
# markov_rank at top level). embed_tokens / lm_head are byte-identical to the target, so they are
# NOT emitted here -- the DSpark decoder shares the target's via ctx_other (same as DFlash).
model_arch = gguf.MODEL_ARCH.DSPARK

def set_vocab(self):
if self.target_model_dir is None:
raise ValueError(
"DSpark draft model requires --target-model-dir to be specified. "
"Please provide the path to the target model directory containing the tokenizer."
)
logger.info(f"DSpark: Using tokenizer from target model: {self.target_model_dir}")
original_dir = self.dir_model
self.dir_model = self.target_model_dir
super().set_vocab()
self.dir_model = original_dir

mask_token_id = self.hparams.get("mask_token_id")
if mask_token_id is not None:
self.gguf_writer.add_mask_token_id(mask_token_id)

def set_gguf_parameters(self):
super().set_gguf_parameters()

block_size = self.hparams.get("block_size", 7)
self.gguf_writer.add_block_size(block_size)

# flat DeepSpec schema; mirror DFlash's +1 extract-layer convention
target_layer_ids = self.hparams.get("target_layer_ids", [])
if target_layer_ids:
extract_layer_ids = [i + 1 for i in target_layer_ids]
self.gguf_writer.add_target_layers(extract_layer_ids)

markov_rank = self.hparams.get("markov_rank", 0)
self.gguf_writer.add_markov_rank(markov_rank)

@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, gen = item
# embed_tokens / lm_head are byte-identical to the target and shared at runtime -- drop them
if name.endswith(("embed_tokens.weight", "lm_head.weight")):
return None
if not name.startswith("model."):
name = "model." + name
return super().filter_tensors((name, gen))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This convert code is 95% similar to DFlash. Can we merge it into DFlash to minimize the scope of changes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread common/speculative.cpp
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH, n_seq)
common_speculative_impl_draft_dflash(const common_params_speculative & params, uint32_t n_seq,
common_speculative_type type = COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH)
: common_speculative_impl(type, n_seq)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to change here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the dspark impl inherits from the dflash impl, and type is a const member — it can only be set via the constructor, so it has to be passed through here.

Comment thread common/speculative.cpp
// TODO: implement confidence-scheduled prefix pruning (the "scheduled" part of DSpark):
// use the confidence head to truncate the drafted block at the first low-confidence
// position instead of always keeping all n_draft tokens
for (int32_t i = 0; i < nb; ++i) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is the only different part from DFlash in current implementation, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Comment thread src/models/dspark.cpp Outdated
Comment on lines +51 to +74
class llm_graph_input_dspark_anchor : public llm_graph_input_i {
public:
llm_graph_input_dspark_anchor(uint32_t block_size) : block_size(block_size) {}
virtual ~llm_graph_input_dspark_anchor() = default;

void set_input(const llama_ubatch * ubatch) override {
GGML_ASSERT(ubatch->token);
const int64_t n_blocks = anchors->ne[0];
std::vector<int32_t> buf(n_blocks);
for (int64_t j = 0; j < n_blocks; ++j) {
buf[j] = ubatch->token[j*block_size];
}
ggml_backend_tensor_set(anchors, buf.data(), 0, n_blocks*sizeof(int32_t));
}

bool can_reuse(const llm_graph_params & params) override {
return params.ubatch.token && anchors &&
anchors->ne[0]*(int64_t) block_size == (int64_t) params.ubatch.n_tokens;
}

ggml_tensor * anchors = nullptr; // I32 [n_blocks]

const uint32_t block_size;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this logic should be same as DFlash? I feel it is not the correct way to do it here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, Reworked it to take a strided view of that input instead.

Comment thread src/llama-arch.cpp Outdated
Comment thread src/llama-arch.h Outdated
Comment thread src/llama-hparams.h Outdated
@wjinxu

wjinxu commented Jul 8, 2026

Copy link
Copy Markdown
Author

Thanks for raising the PR, and sorry for the delay in reviewing it.

I have a few suggestions:

  • Since the current DSpark implementation is based on DFlash and only adds the Markov head for semi-autoregressive decoding based on output logits. I think it should be possible to simplify the code by adding markov head on top of the existing DFlash implementation. This would include the current GGUF model conversion, model loading, and possibly the speculative decoding (the anchor in DSpark paper is exactly id_last in DFlash, and difference is DFlash samples draft tokens from position 1 but DSpark starts from position 0) path as well. Similar to what we added d2t mapping for eagle3.
  • The current DSpark implementation only supports Qwen3, which may not be very useful since Qwen3 is a bit outdated. It would be good to add support for newer models. Since DSpark is based on DFlash, I assume adding support for Gemma 4 models should be relatively straightforward, right?
  • The current implementation is not complete. The missing part is also one of DSpark’s key selling points (i.e. the Confidence Head to truncate draft tokens, let's ignore Hardware-Aware Prefix Scheduler atm), because it can help reduce target model verification time which is quite important. Without that support, I would not consider this PR to be full DSpark support. Personally, I would prefer this PR to support all of these pieces, similar to what vLLM did. ([Spec Decode] DSpark vllm-project/vllm#46995)
  • Dspark also supports RNN head in their paper, but I didn't find that in this PR, right? Okay the default setting is Markov head.

Thanks for your work on this. Let’s explore how we can improve it further and keep things moving forward.


  1. OK, I will try.

  2. Not as straightforward as it looks, unfortunately. The released Gemma4 DSpark checkpoint uses a Gemma4 backbone, but DFlash in llama.cpp is Qwen3-only.I'd leave it for a follow-up PR.

  3. The vLLM PR you linked actually lists confidence-based scheduling as out-of-scope too.[Spec Decode] DSpark vllm-project/vllm#46995


Address review: drop LLM_ARCH_DSPARK and the dspark.block_size /
markov_rank GGUF keys. A DSpark draft now converts to a DFlash GGUF;
the Markov head tensors are detected by presence (like eagle3 d2t),
block_size is read from the existing dflash.block_size key, and the
block anchors are taken as a strided view of the decoder's token
input instead of a separate graph input.
@ruixiang63

Copy link
Copy Markdown
Member

Not as straightforward as it looks, unfortunately. The released Gemma4 DSpark checkpoint uses a Gemma4 backbone, but DFlash in llama.cpp is Qwen3-only.I'd leave it for a follow-up PR.

I see. Then let's focus on Qwen3 now.

The vLLM PR you linked actually lists confidence-based scheduling as out-of-scope too.vllm-project/vllm#46995

I think you’ve already loaded MODEL_TENSOR.DSPARK_CONF_PROJ for the confidence projection, and we don’t need to make it hardware-aware as described in the paper.
My understanding is that we only need to add a confidence projection head based on the output of the Markov head, right? Would that be difficult to support in this PR?

I checked the new updates, and it looks better. Thanks for the quick turnaround!

I’m still not sure whether we need a separate DSpark speculative decoding class, or whether we should integrate it directly into the current DFlash speculative decoding implementation, conditioned on the presence of the Markov head and confidence head.
If we integrate it into DFlash, the benefit would be that we can reduce redundant code and keep the diff cleaner.
In my opinion, DSpark is a variant of DFlash with additional heads, but this needs to be confirmed with @ggerganov.

@wjinxu

wjinxu commented Jul 9, 2026

Copy link
Copy Markdown
Author

I think you’ve already loaded MODEL_TENSOR.DSPARK_CONF_PROJ for the confidence projection, and we don’t need to make it hardware-aware as described in the paper.
My understanding is that we only need to add a confidence projection head based on the output of the Markov head, right? Would that be difficult to support in this PR?

Agreed. I'll add a static flag for this.

If we integrate it into DFlash, the benefit would be that we can reduce redundant code and keep the diff cleaner.

While looking into this I found that the presence of the Markov tensors cannot be used to tell DSpark and DFlash drafts apart. DeepSeek's released DFlash checkpoints (e.g. deepseek-ai/dflash_qwen3_8b_block7) are trained with the DSpark architecture (Qwen3DSparkModel) but with markov_rank = 0, so they carry no Markov tensors — yet they still use DSpark's anchor-first block layout (drafts are read from position 0 of the block, a full block_size per step), which differs from z-lab's DFlash drafts (read from position 1, block_size - 1 per step).

The DSpark confidence head predicts per-position acceptance of the
drafted block. --spec-draft-conf-min truncates the block at the first
position below the threshold (default 0 = disabled).
@wjinxu wjinxu requested review from a team and ngxson as code owners July 9, 2026 13:17
@ruixiang63

Copy link
Copy Markdown
Member

DeepSeek's released DFlash checkpoints (e.g. deepseek-ai/dflash_qwen3_8b_block7) are trained with the DSpark architecture (Qwen3DSparkModel) but with markov_rank = 0, so they carry no Markov tensors — yet they still use DSpark's anchor-first block layout (drafts are read from position 0 of the block, a full block_size per step), which differs from z-lab's DFlash drafts (read from position 1, block_size - 1 per step).

I understand, but this is DFlash rather than DSpark, right? I think the scope of this PR is to add DSpark support. I don’t see much value in adding DeepSeek’s DFlash model at this moment, especially since the DFlash models are based on the outdated Qwen3 models.
I’m glad you pointed this out, but let’s not consider it for now.
The important thing is to align the DSpark implementation, simplify the code, and minimize the diff changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conversion documentation Improvements or additions to documentation model Model specific testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: DSpark confidence-scheduled verification & semi-autoregressive drafting

8 participants