feat(dspark): support a pruned draft vocabulary for the DFlash family - #764
feat(dspark): support a pruned draft vocabulary for the DFlash family#764fg11991 wants to merge 2 commits into
Conversation
EAGLE3 can train a draft over a subset of the target vocabulary; the DFlash family could not, and its contract rejected the attempt. On a 27B target the DSpark Markov head over a full vocabulary is the dominant memory term, and most of that vocabulary never appears as a supervised target in a domain corpus. Set draft_vocab_size below vocab_size in the draft config and the draft predicts over the pruned vocabulary; leave it out and nothing changes. - DraftVocabMappingMixin holds the t2d/d2t contract now shared by EAGLE3 and the DFlash family: registration, installation, validation, and the load-state-dict path. Buffers are registered only when the run actually prunes, so a full-vocabulary checkpoint keeps its exact state dict, and the installed state is derived from the persisted buffers rather than a flag, so a reloaded pruned checkpoint is not reported as uninstalled. - Installing a mapping over a different one of the same size is rejected: identical shapes with different contents silently repoint every draft id at another token, which no shape check can catch. - Objective: target ids are mapped into the draft id space, out-of-vocab labels leave both the numerator and the denominator of the cross entropy (so alpha_ce keeps its meaning instead of scaling with how much of the batch the vocabulary happens to cover), and acceptance uses the target's true retained probability mass rather than a distribution renormalized over the kept tokens, which otherwise reports acceptance well above what the pruned draft can deliver. - Two new ratio metrics make the ceiling visible while training: draft_vocab_coverage (share of supervised tokens the vocabulary can propose) and teacher_kept_mass (the teacher belief that survives). - The unpruned path is unchanged, deliberately down to the arithmetic: the single-denominator expression is kept so an existing run's loss curve does not move by a few ulps, and the second all-reduce is emitted only when the run prunes, so the collective sequence of existing runs is untouched. - keeps_vocab_buffers_when_unpruned lets validation tell the two families apart. A draft that registers the buffers unconditionally can absorb a redundant mapping; one that registers them only when pruning cannot, so that config is rejected at planning time instead of failing later inside the model. Where the draft config cannot be resolved at all, _prunes_vocabulary reports None and each rule falls back to what its algorithm needs by default, so config validation still works without the draft config on disk. Tested on CPU with the real DSpark draft and online model: t2d/d2t round trips, label id spaces, coverage, the analytic acceptance counterexample, checkpoint reload and mapping-conflict rejection, and byte-for-byte equality of the unpruned loss with the pre-change expression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Hi. I acknowledge this feature, but does it really deliver a substantial performance boost? |
|
Thanks for taking a look. The win isn't in the kernel — it's in what the freed memory buys you at training time, plus a small but consistent decode-side gain. Setup. Target: Qwen3.6-27B (
Why that table is the point. On the DFlash family the Markov head over the full vocabulary is the dominant memory term. Cutting it to ~1/4 of the vocabulary frees enough memory to double both Quality cost. At the same number of epochs, the pruned draft's acceptance rate on general-domain datasets is 1–2% below the full-vocabulary draft. Serving. After adapting this to vLLM, both DSpark and MTP at 7 draft steps, 4 concurrent requests: on some domains the pruned draft is faster than the full-vocabulary one by under 1ms, and it is faster than MTP on every domain we measured, by 1–2ms. (We also tried MTP at 3 steps — it was slower than at 7.) |
Motivation
EAGLE3 can train a draft over a subset of the target vocabulary. The DFlash family cannot — its contract rejects
model.vocab_mapping_pathoutright — even though the same argument applies more strongly there: on a 27B target, DSpark's Markov head over a full vocabulary is the dominant memory term, and most of that vocabulary never appears as a supervised target in a domain corpus.This adds pruning to the DFlash family. Set
draft_vocab_sizebelowvocab_sizein the draft config and the draft predicts over the pruned vocabulary; leave it out and nothing changes.What's in it
DraftVocabMappingMixinnow holds thet2d/d2tcontract shared by EAGLE3 and the DFlash family: registration, installation, validation, and theload_state_dictpath. Buffers are registered only when the run actually prunes, so a full-vocabulary checkpoint keeps its exact state dict, and installed state is derived from the persisted buffers rather than a flag — otherwise a reloaded pruned checkpoint reports itself as uninstalled.alpha_cekeeps its meaning instead of scaling with how much of the batch the vocabulary happens to cover. Acceptance uses the target's true retained probability mass rather than a distribution renormalized over the kept tokens — the renormalized form reports acceptance well above what the pruned draft can deliver.draft_vocab_coverage(share of supervised tokens the vocabulary can propose) andteacher_kept_mass(teacher belief that survives pruning).keeps_vocab_buffers_when_unprunedlets validation tell the two families apart. A draft that registers the buffers unconditionally can absorb a redundant mapping; one that registers them only when pruning cannot, so that config is rejected at planning time instead of failing later inside the model. When the draft config cannot be resolved at all,_prunes_vocabularyreportsNoneand each rule falls back to what its algorithm needs by default — config validation keeps working without the draft config on disk.Testing
tests/test_utils/test_dspark_vocab_mapping.pyruns the real DSpark draft and online model on CPU with a tiny random config, because what breaks silently here — which factor of the Markov head follows which vocabulary, which id space the labels live in — is invisible to shape-only checks. It coverst2d/d2tround trips, label id spaces, coverage, an analytic acceptance counterexample, checkpoint reload, mapping-conflict rejection, and byte-for-byte equality of the unpruned loss with the pre-change expression.Full suite run on CPU (no CUDA available on the dev box): no test fails that does not already fail on
mainat the same commit.