spec: support speculators-format checkpoints for DSpark - #26275
Conversation
083c33f to
77e9a31
Compare
Yes, I’ve tested this checkpoint, but it didn’t perform very well. Its acceptance rate was roughly the same as dflash’s. |
77e9a31 to
e107846
Compare
| } | ||
| mask_token_id = llama_vocab_mask(llama_model_get_vocab(model_dft)); | ||
|
|
||
| if (is_dspark) { |
There was a problem hiding this comment.
same here. Feel it is not necessary. Would be better to keep the code change minimal and simple.
|
What is the difference between this PR and #25549? Which one should we review first? |
|
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. |
|
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. |
|
can you rebase this PR? @wjinxu Now I have some bandwidth to review the details more closely. |
f806441 to
72f87af
Compare
Done. Thanks very much. |
| if "aux_hidden_state_layer_ids" in self.hparams: | ||
| self.hparams.setdefault("dflash_config", { | ||
| "mask_token_id": self.hparams.get("mask_token_id"), | ||
| "target_layer_ids": [i - 1 for i in self.hparams["aux_hidden_state_layer_ids"]], |
There was a problem hiding this comment.
Can you confirm it is i-1 instead of i or i+1?
There was a problem hiding this comment.
Confirmed: the base class adds +1 when writing, so -1 passes the aux ids through unchanged, matching EAGLE3.
| ggml_tensor * w1_prev = ggml_get_rows(ctx0, w1, prev); // [R, n_blocks] | ||
| ggml_tensor * bias = ggml_mul_mat(ctx0, w2, w1_prev); // [n_vocab, n_blocks] | ||
| ggml_tensor * bias = ggml_mul_mat(ctx0, w2, w1_prev); // [n_vocab_draft, n_blocks] | ||
| if (model.d2t) { |
There was a problem hiding this comment.
I saw 1+Nd2t mapping for DSpark. Can you confirm this is correct?
There was a problem hiding this comment.
Intentional: logits must be in target-vocab space (sampler + argmax feeding markov_w1, which is keyed by target ids); the per-position w2 bias is draft-vocab-only, so it's zero-scattered onto the -inf base. Greedy output byte-identical, acceptance unchanged.
| cur = build_lora_mm(output, cur, output_s); | ||
|
|
||
| // reduced-draft-vocab exports: scatter the draft logits to the target vocabulary via d2t | ||
| if (model.d2t) { |
There was a problem hiding this comment.
The first d2t mapping is here. And the next N mapping is in build_dspark_markov_head.
|
Once the questions in comments are resolved, could you take another look? @CISC |
|
Sorry — a previous bug in the sampling decision caused errors when I used qwen3.8-27B-dspark. That is fixed now, and the acceptance rate looks normal. I am currently running SpeedBench ( Sorry again for the noise. |
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
- 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
Co-authored-by: desovo7 <942845546@qq.com> Assisted-by: Claude Fable
Co-authored-by: desovo7 <942845546@qq.com> Assisted-by: Claude Fable
6012af5 to
d11902e
Compare
Co-authored-by: desovo7 <942845546@qq.com> Assisted-by: Claude Fable
* origin/master: (86 commits) ui: enforce alphabetical enum member ordering (ggml-org#27272) ui: Refactor Built-In Tools naming (Server/Browser) (ggml-org#27271) ci: more optimizations (ggml-org#26983) doc: document MCP stdio servers and CORS defaults in the server README [no release] [no ci] (ggml-org#26847) server: save processed mtmd chunks as placeholder (ggml-org#27278) mtmd: use sha256 for input hashing (ggml-org#27274) vocab : support integer tokenizer scores (ggml-org#27260) mtmd : skip thumbnail for non-tiled LFM2 images (ggml-org#27246) cuda : skip UMA override for HIP builds (ggml-org#27083) vendor: move hash to vendor (ggml-org#27262) ci : push release tag explicitly in release.yml (ggml-org#27261) ui: move get_datetime tool to frontend (ggml-org#27255) ci : reduce builds in build-xcframework.sh (ggml-org#27252) model: support speculators-format checkpoints for DSpark (ggml-org#26275) ui: add browser get_info tool (ggml-org#27251) ci : restore release.yml check during make-release.yml (ggml-org#27247) mtmd: harden preprocessor_granite (ggml-org#27235) ci : allow make-release to target a specific commit (ggml-org#27234) ci : make release workflows use a deploy key (ggml-org#27229) convert: add @ModelBase.example (ggml-org#27208) ...
) * dspark: support speculators-format checkpoints (SpecForge exports) 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 * dspark: fold the speculators draft class into DSparkModel 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 * dspark: address review comments - 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 * dspark: fold the sample_from_anchor read into the block_size block * dspark: fix flake8 continuation indent * clean up * dspark: key the sample_from_anchor default off the export format Co-authored-by: desovo7 <942845546@qq.com> Assisted-by: Claude Fable * dspark: drop t2d in filter_tensors Co-authored-by: desovo7 <942845546@qq.com> Assisted-by: Claude Fable * dspark: map model.lm_head instead of bypassing the dflash prefix Co-authored-by: desovo7 <942845546@qq.com> Assisted-by: Claude Fable --------- Co-authored-by: desovo7 <942845546@qq.com> Co-authored-by: ruixiang63 <wangruixiang07@outlook.com>
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_anchorto declare the block layout:trueselects anchor-first, matching dense DeepSpec where every slot predicts a token;falseselects DFlash-style1+Ninfilling, where the anchor slot contains the bonus token and1 + n_maxslots produce n_max draft tokens. A missing field defaults to false, since legacy exports only use the1+Nlayout.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-dsparkRedHatAI/gemma-4-31B-it-speculator.dsparkqwen3.8-27B-dspark
Additional information
Testing and feedback are welcome.
Requirements