Skip to content

qwen35: DSpark speculative decoding (Qwen3.8-27B drafters) - #625

Draft
davide221 wants to merge 2 commits into
mainfrom
qwen38-dspark
Draft

qwen35: DSpark speculative decoding (Qwen3.8-27B drafters)#625
davide221 wants to merge 2 commits into
mainfrom
qwen38-dspark

Conversation

@davide221

@davide221 davide221 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Adds DSpark drafter support to the qwen35 family spec-decode loop, targeting the published Qwen3.8-27B DSpark checkpoints (DFlash backbone + low-rank Markov bigram head + confidence head, e.g. RadixArk/Qwen3.8-27B-DSpark).

What's in here

Spec loop (qwen35_backend.cpp)

  • When the drafter GGUF ships DSpark heads, the chain draft goes through the markov-corrected greedy chain (fused single-graph variant, non-fused fallback) instead of plain argmax projection. DDTree candidate top-k also gets the markov bias.
  • Env gates, all default-on: DFLASH_QWEN35_DSPARK, DFLASH_QWEN35_FUSED_DSPARK, DFLASH_QWEN35_DSPARK_TREE.
  • Target capture layers now follow the drafter GGUF's dflash.target_layer_ids instead of the evenly-spaced derivation. The Qwen3.8 drafter is trained on layers 4/16/28/40/52; capturing the derived 1/16/31/46/61 silently destroys acceptance.

Draft loader (draft_gguf_loader.cpp)

  • dflash.mask_token_id from the drafter GGUF wins over the family default (this drafter trains with MASK 248077; the default was 248070 — another silent acceptance killer).
  • Optional YaRN rope-scaling keys (rope.scaling.*) parsed into DraftWeights.

Draft graph (draft_graph.cpp)

  • All rope calls go through one helper that honors the drafter's YaRN config; previously plain NEOX rope was hardcoded.

Converter (convert_dflash_to_gguf.py)

  • Single-file DSpark releases (markov/confidence heads inline in model.safetensors, no dflash_aux_heads.pt sidecar) now convert directly.
  • Reads transformers>=5 nested rope_parameters (theta + YaRN) and dflash_config.mask_token_id; emits YaRN scaling metadata.

Fused head path: Qwen35DFlashTarget exposes lm_head_tensor() (the fused chain needs the target lm_head; layer-split target falls back gracefully).

Validation (R9700 / gfx1201, Qwen3.8-27B Q4_K_M, greedy, 300-token gens)

code prose mixed
AR baseline 22.9 ~22 ~22
DSpark spec decode 38.4 23.0 30.2

Serving config for the numbers: q4-mix requantized drafter, --fa-window 2048, q8_0 KV, DFLASH_SINGLE_CHAIN_CHECKPOINT_F32=1 DFLASH_FAST_ROLLBACK_THRESHOLD=1. Acceptance length matches the checkpoint's published SGLang reference (~3.4 tokens/step incl. bonus).

Known limitations / follow-ups

  • Confidence-gate adaptive block length is not wired (per-request step buffers are sized by q_len); the chain runs with the gate off.
  • YaRN on the drafter measured slightly worse acceptance than plain rope at short context on this checkpoint, so the canonical drafter GGUF is converted without it; the plumbing is data-driven either way.
  • DDTree with markov candidates loses to chain verify on prose with this drafter; chain remains the default.
  • Verify/AR forward-cost optimization (AMD-native requant formats) is separate follow-up work.

Review in cubic

Wire the DSpark drafter heads (low-rank Markov bigram correction +
confidence head) into the qwen35 spec-decode loop, so Qwen3.8-27B DSpark
drafters (e.g. RadixArk/Qwen3.8-27B-DSpark) run with full head support:

- spec loop: markov-corrected greedy chain (fused single-graph variant
  with non-fused fallback) replaces plain argmax projection when the
  drafter ships DSpark heads; DDTree candidate top-k gets the markov
  bias too. Env-gated: DFLASH_QWEN35_DSPARK, DFLASH_QWEN35_FUSED_DSPARK,
  DFLASH_QWEN35_DSPARK_TREE (all default on).
- target capture layers now follow the drafter GGUF's
  dflash.target_layer_ids instead of the evenly-spaced derivation; the
  Qwen3.8 drafter is trained on layers 4/16/28/40/52, not 1/16/31/46/61.
- draft loader: dflash.mask_token_id from the drafter GGUF wins over the
  family default (Qwen3.8 drafter uses 248077, default was 248070), and
  optional YaRN rope scaling keys are parsed into DraftWeights.
- draft graph: rope calls honor the drafter's YaRN config (previously
  hardcoded plain NEOX rope).
- Qwen35DFlashTarget exposes lm_head for the fused head path.
- convert_dflash_to_gguf.py: handle single-file DSpark releases (markov/
  confidence heads inline in model.safetensors), transformers>=5 nested
  rope_parameters and dflash_config.mask_token_id, and emit YaRN scaling
  metadata.

The confidence-gate adaptive block length is not wired yet (q_len sizes
the per-request step buffers); the chain runs with the gate off.
Verify/accept now run over v_len (the drafted chain's actual length)
instead of the buffer-sizing q_len, so the DSpark confidence gate's
adaptive block truncation is structurally supported. The gate itself
stays off by default (DFLASH_QWEN35_DSPARK_CONFIDENCE_THRESHOLD=0):
with the RadixArk Qwen3.8 drafter, any threshold in 0.1-0.5 truncates
to the same short chain regardless of value, so the confidence scores
coming out of the shared head path look mis-scaled and need a separate
investigation before the gate can help. threshold=0 is bench-verified
regression-free.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant