Skip to content

dspark: support speculators-format checkpoints - #26275

Open
wjinxu wants to merge 4 commits into
ggml-org:masterfrom
wjinxu:dspark-speculators
Open

dspark: support speculators-format checkpoints#26275
wjinxu wants to merge 4 commits into
ggml-org:masterfrom
wjinxu:dspark-speculators

Conversation

@wjinxu

@wjinxu wjinxu commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Overview

Follow-up to #25173. This adds support for DSpark drafts exported in the speculators format (SpecForge / RedHat), which vLLM gained in vllm-project/vllm#47093.

Format differences

These checkpoints differ from the dense DeepSpec checkpoints in same ways:

The speculators config uses sample_from_anchor to declare the block layout: true selects anchor-first, matching dense DeepSpec where every slot predicts a token; false selects DFlash-style 1+N infilling, where the anchor slot contains the bonus token and 1 + n_max slots produce n_max draft tokens. A missing field defaults to false, since legacy exports only use the 1+N layout.

A checkpoint may use a pruned draft output vocabulary (draft_vocab_size < vocab_size) together with a draft-to-target (d2t) remapping table.

Verification

makora-ai/gemma4-26b-a4b-dspark

category       base_avg_pred_t/s  spec_avg_pred_t/s  decode_speedup  base_avg_latency  spec_avg_latency  latency_speedup  accept_rate
  -------------  -----------------  -----------------  --------------  ----------------  ----------------  ---------------  -----------
  coding         82.59              145.53             1.76x           32.130s           18.790s           1.71x            0.5777
  humanities     83.68              89.31              1.07x           16.266s           17.955s           0.91x            0.2816
  math           83.87              96.88              1.16x           13.976s           14.484s           0.96x            0.3016
  qa             84.36              89.16              1.06x           10.334s           9.125s            1.13x            0.3173
  rag            83.29              104.72             1.26x           11.497s           10.121s           1.14x            0.3347
  reasoning      83.83              88.92              1.06x           15.457s           17.113s           0.90x            0.2790
  stem           83.95              89.88              1.07x           13.436s           16.299s           0.82x            0.2823
  writing        81.62              81.47              1.00x           32.122s           31.942s           1.01x            0.2379
  multilingual   84.05              91.50              1.09x           11.673s           10.677s           1.09x            0.3052
  summarization  84.51              92.09              1.09x           6.422s            5.352s            1.20x            0.2873
  roleplay       83.12              83.53              1.00x           33.010s           32.700s           1.01x            0.2678
  overall        83.53              95.73              1.15x           17.848s           16.778s           1.06x            0.3079

RedHatAI/gemma-4-31B-it-speculator.dspark

Comparison: baseline=baseline.json speculative=dspark.json
category       base_avg_pred_t/s  spec_avg_pred_t/s  decode_speedup  base_avg_latency  spec_avg_latency  latency_speedup  accept_rate
-------------  -----------------  -----------------  --------------  ----------------  ----------------  ---------------  -----------
coding         14.56              45.15              3.10x           121.646s          40.026s           3.04x            0.4527     
humanities     14.69              29.65              2.02x           56.045s           24.973s           2.24x            0.2536     
math           14.71              33.19              2.26x           44.351s           18.708s           2.37x            0.2829     
qa             14.78              30.67              2.08x           26.889s           13.033s           2.06x            0.2420     
rag            14.60              36.29              2.49x           46.995s           21.810s           2.15x            0.2871     
reasoning      14.71              29.17              1.98x           43.860s           20.688s           2.12x            0.2279     
stem           14.71              29.23              1.99x           39.184s           17.862s           2.19x            0.2294     
writing        14.40              31.40              2.18x           152.613s          74.782s           2.04x            0.2514     
multilingual   14.65              31.12              2.12x           69.500s           32.204s           2.16x            0.2646     
summarization  14.73              28.12              1.91x           33.126s           17.515s           1.89x            0.2150     
roleplay       14.60              29.71              2.04x           128.897s          66.507s           1.94x            0.2397     
overall        14.65              32.15              2.19x           69.373s           31.646s           2.19x            0.2720    

Additional information

Testing and feedback are welcome.

Requirements

@github-actions github-actions Bot added documentation Improvements or additions to documentation model Model specific conversion labels Jul 29, 2026
@wjinxu
wjinxu force-pushed the dspark-speculators branch from 083c33f to 77e9a31 Compare July 29, 2026 13:19
@wjinxu
wjinxu marked this pull request as ready for review July 30, 2026 06:18
@wjinxu
wjinxu requested review from a team and CISC as code owners July 30, 2026 06:18
@ruixiang63
ruixiang63 self-requested a review July 30, 2026 10:35
@ruixiang63 ruixiang63 self-assigned this Jul 31, 2026
@wjinxu

wjinxu commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Will it support this DSpark drafter?

Yes, I’ve tested this checkpoint, but it didn’t perform very well. Its acceptance rate was roughly the same as dflash’s.

@wjinxu
wjinxu force-pushed the dspark-speculators branch from 77e9a31 to e107846 Compare August 3, 2026 09:07
Comment thread common/speculative.cpp Outdated
Comment thread common/speculative.cpp Outdated
}
mask_token_id = llama_vocab_mask(llama_model_get_vocab(model_dft));

if (is_dspark) {

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.

same here. Feel it is not necessary. Would be better to keep the code change minimal and simple.

Copy link
Copy Markdown
Contributor 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 Outdated
Comment thread common/speculative.cpp Outdated
Comment thread common/speculative.cpp Outdated
Comment thread conversion/qwen.py Outdated
Comment thread gguf-py/gguf/constants.py Outdated
Comment thread gguf-py/gguf/gguf_writer.py Outdated
Comment thread src/models/dflash.cpp Outdated
@ruixiang63

Copy link
Copy Markdown
Member

What is the difference between this PR and #25549? Which one should we review first?

@wjinxu

wjinxu commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

I completely understand what you mean. How about we handle this one first? Since this isn't about adding a new backbone but adapting to another training framework, I think it should be relatively simpler.

@wjinxu

wjinxu commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

As for how to adapt the backbone going forward — Qwen 3.8 may be released in a few days — I think waiting to see how the community chooses could be a good approach. If the community goes with a separate backbone, we can adjust accordingly in time.

@ruixiang63

ruixiang63 commented Aug 11, 2026

Copy link
Copy Markdown
Member

can you rebase this PR? @wjinxu Now I have some bandwidth to review the details more closely.

wjinxu and others added 2 commits August 12, 2026 00:47
Speculators-format DSpark drafts (e.g. SpecForge exports for the
Gemma-4-26B-A4B target) differ from the dense DeepSpec checkpoints in
three ways:

- the config nests the backbone hparams under transformer_layer_config
  and gives the extract layers as aux_hidden_state_layer_ids
- the block is the DFlash 1+N fill-in layout: the anchor slot is a bonus
  token, not a prediction slot. Written as dflash.bonus_anchor; such
  drafts build the block and read the mask positions exactly like
  DFlash (n_max drafts from a 1+n_max block), only the Markov/confidence
  sampling comes from DSpark
- the draft output vocab may be reduced (draft_vocab_size < vocab_size)
  with a d2t remap table. The converter expands lm_head/markov_w2 back
  to the full vocab and synthesizes an lm_head bias of -1e9 on the rows
  the draft cannot produce, so the runtime needs no d2t remapping. Such
  drafts ship their own (now optional) token_embd/output tensors instead
  of sharing the target's

Verified against gemma4-26b-a4b-dspark: greedy outputs are byte-identical
with and without the draft; acceptance 0.46, mean draft len 3.7 (n_max 6).

Co-authored-by: desovo7 <942845546@qq.com>
Assisted-by: Claude Fable 5
One class now covers every DSpark variant. What used to pick the class is
a single flag, because the arch name turns out to be the only thing that
separates the two families: SpecForge also exports a flat schema that
carries no speculators_* fields yet still uses the 1+N bonus-anchor block,
so keying on those fields would silently mis-read its drafts.

Also rename i0 to i_first_pred in the draft read loop and the Markov head,
and give the head a real bonus_anchor bool instead of testing i0 > 0.

Converting the Qwen3-8B DeepSpec draft and both gemma-4 speculators drafts
produces byte-identical GGUFs. The one behaviour change is that the
markov_head_type check now also covers the DeepSpec checkpoints, which
previously skipped it.

Co-authored-by: desovo7 <942845546@qq.com>
Assisted-by: Claude Opus 5
@wjinxu
wjinxu force-pushed the dspark-speculators branch from f806441 to 72f87af Compare August 11, 2026 16:48
@wjinxu

wjinxu commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

can you rebase this PR? @wjinxu Now I have some bandwidth to review the details more closely.

Done. Thanks very much.

Comment thread common/speculative.cpp Outdated
const bool is_dspark;

// dspark speculators exports: DFlash 1+N block, the anchor is not a prediction slot
bool bonus_anchor = false;

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.

Suggested change
bool bonus_anchor = false;
bool sample_from_anchor = false;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread conversion/qwen.py Outdated
return TextModel.filter_tensors(item)
return super().filter_tensors(item)

def _expand_reduced_vocab(self, data_torch: Tensor, name: str) -> Iterable[tuple[str, Tensor]]:

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.

Can you check how Eagle3 uses d2t and make this consistent with the existing implementation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread conversion/qwen.py
yield from super().modify_tensors(bias, "lm_head.bias", None)

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if name == "d2t":

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.

same here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread conversion/qwen.py Outdated
hparams = ModelBase.load_hparams(dir_model, False)

# only the arch name separates the SpecForge drafts from the DeepSpec ones
self._is_specforge = hparams["architectures"][0] in ("DSparkDraftModel", "DSparkSpeculator")

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 don’t think we need _is_specforge here. The Eagle3 conversion function can handle checkpoints trained with SpecForge as well as many other trained Eagle3 variants. I’d prefer to avoid adding framework-specific training logic or checks here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed _is_specforge to _sample_from_anchor, determined by which architecture is in use. However, I think deciding whether to sample based on the architecture is necessary behavior.

Comment thread conversion/qwen.py Outdated
name, gen = item
if name.endswith(("embed_tokens.weight", "lm_head.weight")):
name = item[0]
if name == "t2d": # training-only target->draft mask

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.

as far as I know we don't even need to mention t2d here, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread gguf-py/gguf/constants.py Outdated
TARGET_LAYERS = "{arch}.target_layers"
TARGET_HIDDEN_SIZE = "{arch}.target_hidden_size"
BLOCK_SIZE = "{arch}.block_size"
BONUS_ANCHOR = "{arch}.bonus_anchor"

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.

Suggested change
BONUS_ANCHOR = "{arch}.bonus_anchor"
SAMPLE_FROM_ANCHOR = "{arch}.sample_from_anchor"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread src/models/dflash.cpp Outdated
GGML_ASSERT(block_size > 0);

// bonus anchor (SpecForge exports): slot 0 holds a bonus token, not a prediction slot
const auto it_ba = model.gguf_kv.find("dflash.bonus_anchor");

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.

Please use the new name and improve it_ba naming here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Renamed it_ba to it_sample_from_anchor.

wjinxu and others added 2 commits August 12, 2026 02:03
- rename bonus_anchor to sample_from_anchor (GGUF key and code), matching
  the checkpoint config field; absent key still means anchor-first
- rework the reduced draft vocab to match EAGLE3: d2t is written as I64
  absolute target ids and the logits are scattered at runtime, instead of
  expanding lm_head/markov_w2 and synthesizing an output bias at conversion
- move the t2d skip to modify_tensors, like EAGLE3
- drop _is_specforge: the arch name only picks the sample_from_anchor
  default, embed/lm_head sharing is decided by the draft vocab size
- deduplicate the tok_embd create_tensor left behind by the rebase

Verified with the RedHat gemma-4-31b speculator draft: greedy output is
byte-identical with and without the draft; acceptance 0.26 (n_max 7).

Co-authored-by: desovo7 <942845546@qq.com>
Assisted-by: Claude Fable 5
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants