feat: add DFlash2 drafter training backend - #62
Conversation
DFlash2 (https://inco.ai/blog/dflash2/) keeps DFlash's block-diffusion drafting and target-context backbone and adds two modules on top: - a two-tap dynamic depthwise convolution wrapped around every attention and MLP sublayer, which counteracts the accuracy decay DFlash shows toward the end of a block; - a candidate selector that keeps selector_top_k candidates per block position and traces one coherent path with a low-rank bilinear score over adjacent candidates: S_t(a,b) = U_t(b) + <A(a)*H(h_t), B(b)>. Implemented as a DFlash variant, mirroring how Domino and DSpark extend the same backbone: DFlash2DraftModel subclasses DFlashDraftModel and DFlash2TrainerBackend subclasses DFlashTrainerBackend, so the block-drafter plumbing, anchor sampling, packing and FSDP2 wrapping are reused rather than duplicated. Defaults match the released z-lab/Qwen3.8-27B-DFlash2 checkpoint (block_size 8, conv 2-tap / group 16, selector rank 256 / top-k 16). Two deltas over the upstream inference implementation are load-bearing here: - The convolution is causal along the sequence axis. Upstream only ever drafts a single block, but training packs n_blocks blocks into one flat draft sequence, so the conv is applied block-locally; otherwise tap 1 at a block's first position would read the last position of the previous, unrelated block. - Upstream allocates base_kernel with torch.empty and relies on from_pretrained to fill it. Training from scratch needs a real init, so the conv starts as an identity passthrough (tap 0 = 1, later taps = 0, zeroed projection): a freshly built DFlash2 is numerically identical to DFlash and learns the correction. The selector's training objective teacher-forces the predecessor to the ground truth, so it scores all positions in one shot instead of the sequential trace inference uses; it is a cross-entropy over the drafter's own top-k restricted to rows whose ground truth survived that top-k. It requires loss_mode=full_vocab and fails loud otherwise, since a restricted vocabulary would make the candidate ids meaningless. To avoid copying DFlashTrainingModel.forward wholesale (as Domino does), DFlashTrainingModel gains a documented _auxiliary_loss extension point that returns None for plain DFlash, leaving every existing backend's behaviour and numerics unchanged. Signed-off-by: khazic <khazzz1c@gmail.com>
Mirrors tests/special_standalone/domino_gpu_smoke.py: drives the real training path on a real target, reporting the DFlash CE signals plus the DFlash2-specific selector_loss / selector_acc / selector_coverage. Signed-off-by: khazic <khazzz1c@gmail.com>
The smoke hard-cast the module to bf16 (copied from the Domino smoke), which makes the shared DFlash forward assign a bf16 cross_entropy result into its fp32 loss_per_token buffer. The real training path keeps fp32 parameters and runs the forward under torch.amp.autocast(bfloat16), which leaves cross_entropy in fp32. Match that instead; the Domino smoke only survives the hard cast because its own forward explicitly floats the logits. Signed-off-by: khazic <khazzz1c@gmail.com>
The production forward stacks both: FSDP MixedPrecision(param_dtype=bf16) makes the parameters bf16, and the surrounding autocast keeps cross_entropy in fp32. Emulating only one of the two breaks in a different place each time (bf16 params alone scatter a bf16 CE result into the fp32 loss_per_token buffer; autocast alone leaves the RMSNorm weights fp32, so attention q/k become fp32 while v stays bf16). Signed-off-by: khazic <khazzz1c@gmail.com>
test_factory_lists_every_supported_algorithm asserts the exact supported set, so it has to learn about DFLASH2. Also parametrize the hidden-states layout test over it: DFlash2 consumes the same DFlash aux context layers, and tagging it eagle3_aux_plus_last would make DFlash preprocessing fail closed. Signed-off-by: khazic <khazzz1c@gmail.com>
- Pin the conv block size to the trainer's block size. They came from two independent sources (checkpoint config vs dflash2_block_size), and when the training value is a multiple of the config value the block-multiple guard still passes while each conv block spans several anchor blocks, so the causal tap reads across an anchor boundary silently. Resolve once in build_model. - Gate dflash2_num_target_layers on the running algorithm. It sat after domino_num_target_layers in a flat priority list, and speco_base.yaml defines the Domino key unconditionally, so it could never be reached; a DFLASH2 run with a non-default value would build a drafter expecting N context layers while the rollout collected 5. - Compute the selector loss over all active rows, zero-weighting the ones whose ground truth missed the drafter's top-k, instead of slicing them out. Slicing made the autograd graph coverage-dependent, so on a cold-started drafter the selector parameters could receive gradients on some ranks and not others and desync the FSDP2/DDP reduction. - Log the selector diagnostics: they were computed and returned but absent from _record_dflash_training_metrics' scalar_keys, so the only signals showing whether the selector learns were dropped outside the standalone smoke. - Wire DFLASH2 into both SGLang gates: it now requests the DFlash aux hidden states like the rest of the family, and rejects DFLASH2 as an engine-level algorithm with a Domino-style message instead of forwarding the raw string to ServerArgs. - Drop the dead _normalize_dflash_config override (num_context_layers defaults to 5, never None, so the guard never fired). - Tests: importorskip transformers in the four tests that reach it through DFlashConfig (they errored instead of skipping without it), fix the smoke path in the module docstring, and add a regression test for the block-size drift. Signed-off-by: khazic <khazzz1c@gmail.com>
selector_acc on its own is not evidence that the selector learned anything. S_t(a, b) starts from the drafter's own logit U_t(b), so once the backbone ranks the ground truth first by itself the selector scores perfectly whether or not the bilinear term contributes. Report the unary-only ranking accuracy on the same scored rows as the control, so the lift between them isolates the selector's actual contribution. Signed-off-by: khazic <khazzz1c@gmail.com>
check_license.py covers tests/ as well as verl_speco/, so the new contract test file failed the pre-commit job. Every sibling test file already carries it. Signed-off-by: khazic <khazzz1c@gmail.com>
The selector is an auxiliary head that re-ranks candidates the drafter already produced, but its cross-entropy was reaching the backbone through two paths: torch.topk passes gradient through the selected logits, and the selector's hidden projection read the backbone state directly. So the selector objective was reshaping the drafter itself rather than only training the re-ranker. That is wrong on its own terms, and it also invalidates any DFlash/DFlash2 comparison: the two backbones would be trained under different effective objectives instead of differing only by architecture, so a measured delta could not be attributed to the architecture change. Detach both inputs and pin the invariant with a test. Signed-off-by: khazic <khazzz1c@gmail.com>
The block drafters report per-position accuracies, but those are marginals: each position is counted independently. A speculative verifier stops at the first mismatch, so what governs the achievable speedup is the length of the correct prefix, which the marginals cannot be combined into. Add _block_acceptance_counts() and call it from the DFlash, Domino and DSpark forwards, so every block drafter emits the metric the base trainer already had prefix-generic plumbing for. The helper takes the drafted positions only and leaves the slicing to each caller, because the layouts differ: DFlash keeps an unscored anchor at column 0, while Domino and DSpark build shifted labels where every column is a real prediction and slicing would both drop a token and stop a wrong first token from truncating the block. DFlash intersects the scoring mask with the reweighted mask, since correctness is only recorded where the reweighted mask is positive; without that, a position zeroed by loss decay or front_position_weight would read as a mismatch and truncate every block. Reported as mean_acceptance_length under the same convention as the rollout-side drafter/spec_decode/mean_acceptance_length, whose leading 1.0 is the token the target emits itself at each verification step, so the two are directly comparable. The raw sum and block count are published alongside it. Signed-off-by: khazic <khazzz1c@gmail.com>
7a06c0b to
6e97e04
Compare
| super().__init__() | ||
| self.rank = int(config.selector_rank) | ||
| self.top_k = int(config.selector_top_k) | ||
| self.predecessor_codebook = nn.Embedding(config.vocab_size, self.rank) |
There was a problem hiding this comment.
官方脚本from pretrain时有个key mapping,这里是否也需要处理下
There was a problem hiding this comment.
Confirmed. The released checkpoint stores both selector codebooks as bare Parameters (candidate_selector.predecessor_codebook); this overlay holds them in nn.Embedding, so they arrived under names the model does not have and were dropped with only a debug log. Of its 81 tensors those two were the only mismatch. Fixed in 3492314: a _CHECKPOINT_KEY_ALIASES table on the base backend, plus a _validate_normalized_state hook that rejects a partial DFlash2 module set. Against the real key list, 23 of 23 module keys now match with none stray.
| self.selector_loss_weight = float(selector_loss_weight) | ||
|
|
||
| @classmethod | ||
| def from_dflash2_pretrained(cls, model_path: str): |
There was a problem hiding this comment.
dflash config的默认rope_theta与dflash2是否一致,dflash2好像默认是读取rope_parameters
There was a problem hiding this comment.
You are right. The released config has no top-level rope_theta, only rope_parameters.rope_theta = 1e7, so DFlashConfig fell back to its 10000.0 default. The cold-start path had the same gap on the target side. Fixed in dcf452e: resolve_rope_theta takes both spellings and is used by DFlashConfig, modeling_dflash and all four DFlash-family fallback configs. A scaled rope_type now warns instead of being discarded silently (27c680e).
| "EAGLE2", | ||
| "EAGLE3", | ||
| "DFLASH", | ||
| "DFLASH2", |
There was a problem hiding this comment.
co-train模式下,sgl与vllm是否支持dflash2类别?
There was a problem hiding this comment.
Neither serves DFLASH2 as an engine method: it is a DFlash variant, like Domino. SGLang already failed loud, vLLM fell through to a generic "unsupported" string, and the advice both give (serve the trained checkpoint as DFLASH) was blocked on vLLM by an architectures allowlist. Fixed in 2ee2c99. DFlash2 is training-side only here, so online co-train is out of scope; the PR description now says so.
transformers 5 moved the RoPE base out of a top-level rope_theta and into a rope_parameters dict. The released DFlash-family drafter checkpoints and modern target configs carry only the nested spelling, so DFlashConfig fell back to its 10000.0 default: three orders of magnitude below the 1e7 those models were trained at, silently, on both the checkpoint path and the cold-start path that reads the base off the target config. Resolve both spellings in one helper and use it in DFlashConfig plus every DFlash-family fallback config, so a DFlash baseline and a DFlash2 arm cannot end up on different RoPE bases. Signed-off-by: khazic <khazzz1c@gmail.com>
…g them The released z-lab DFlash2 checkpoint stores the two selector codebooks as bare nn.Parameter tensors, while this overlay holds them in nn.Embedding modules, whose state dict spells the same tensor with a trailing .weight. Every other DFlash2 parameter name matches upstream exactly, so nothing failed: the base loader drops keys the model does not have with only a debug log, and its required-key gate covers the DFlash backbone alone, which left both codebooks at their random init while training carried on. Rename them on load, and fail loud when a checkpoint carries some DFlash2 modules under unrecognized names, so a future upstream rename cannot degrade into a silent cold start. A checkpoint carrying none of them is still accepted: warm-starting DFlash2 from a plain DFlash backbone stays a legitimate flow. Signed-off-by: khazic <khazzz1c@gmail.com>
DFLASH2 is not an engine-level algorithm, so SGLang failed loud with guidance while vLLM fell through to a bare 'Unsupported speculative_algorithm' string. Worse, the guidance both should give (serve a trained DFlash2 checkpoint as DFLASH) was unfollowable on vLLM, whose DFlash drafter validator accepted only architectures=['DFlashDraftModel'] and so rejected DFlash2DraftModel. Add the DFLASH2 branch to the vLLM method resolver with the same message, accept the DFlash2 architecture on the DFlash serve path, and classify it in the old-logprob aux-layer lookup so a serve-only run resolves the DFlash layout. Drop DFLASH2 from the SGLang aux-hidden set: that path needs the same drafter.enable the ServerArgs override rejects DFLASH2 under, so it was unreachable. Signed-off-by: khazic <khazzz1c@gmail.com>
…idation hook onto the base backend /simplify pass over the three fixes. The DFlash2 rename was a second, differently shaped pass wrapped around the base normalizer, and the completeness check was a full _load_draft_checkpoint override whose default-fill was copied verbatim from the base. Both become declarative: a _CHECKPOINT_KEY_ALIASES table applied by the base normalizer, and a _validate_normalized_state hook the base loader calls after its own backbone gate. Also promote the base loader's dropped-key report to a warning when keys were unexpected or shape-mismatched (missing keys stay at debug, the embedding is loaded separately), so the silent drop this fix works around is visible for DFlash / DSpark / Domino too. Signed-off-by: khazic <khazzz1c@gmail.com>
…oPE scaling /code-review follow-up. resolve_rope_theta reads the base out of rope_parameters and discards the rest, but DFlashRotaryEmbedding takes only a base, so a target using yarn / linear / llama3 scaling gets a draft whose rotary phase diverges past the original context length. That was already true before this branch, and it is invisible for exactly the reason a defaulted base is. Warn once per distinct rope_type rather than per decoder layer. Applying the scaling itself is a larger change than this PR should carry. Signed-off-by: khazic <khazzz1c@gmail.com>
The CPU unit-test job runs without transformers and relies on every test that imports verl_speco.models skipping itself. The three rope-resolver tests import it transitively through verl_speco.models.dflash, so they errored there while passing locally. Signed-off-by: khazic <khazzz1c@gmail.com>
Closes #61.
Summary
Adds DFlash2 drafter training support. DFlash2 (writeup, checkpoint z-lab/Qwen3.8-27B-DFlash2, code z-lab/dflash) keeps DFlash's block-diffusion drafting and target-context backbone and adds two modules on top, so this is implemented as a DFlash variant exactly the way Domino (#16, #17) and DSpark extend the same backbone:
DFlash2DraftModelsubclassesDFlashDraftModelandDFlash2TrainerBackendsubclassesDFlashTrainerBackend, reusing the block-drafter plumbing, anchor sampling, packing and FSDP2 wrapping rather than duplicating them.Defaults match the released checkpoint:
block_size=8, two-tap conv withconv_group_size=16,selector_rank=256,selector_top_k=16.What DFlash2 adds
Two-tap dynamic depthwise convolution wrapped around every attention and MLP sublayer, targeting the accuracy decay DFlash shows toward the end of a block:
with a static per-channel kernel plus a content-adaptive correction shared by every
conv_group_sizechannels.prepareruns before the sublayer and also emits the kernelfinishapplies after it, so one projection feeds both taps.Candidate selector that keeps the drafter's top-k candidates at each block position and traces one coherent path through them:
U_t(b)is the drafter's own logit for candidateb,A/Bare the predecessor/successor codebooks andHprojects the backbone hidden state.Two deltas over the upstream inference implementation
Both are load-bearing for training and are the only places this diverges from z-lab/dflash:
The convolution is applied block-locally. It is causal along the sequence axis. Upstream only ever drafts a single block, but training packs
n_blocksblocks into one flat draft sequence, so a naive port would let tap 1 at a block's first position read the last position of the previous, unrelated block.GroupedDynamicCausalConvreshapes to[bsz * n_blocks, block_size, hidden]first and raises if the draft length is not a block multiple. Covered bytest_conv_does_not_leak_across_block_boundaries.The convolution starts as an identity passthrough. Upstream allocates
base_kernelwithtorch.emptyand relies onfrom_pretrainedto fill it, which is fine for inference from a released checkpoint but produces uninitialized memory when training from scratch. Here tap 0 is initialized to 1, later taps to 0, and the kernel projection to zero, so a freshly built DFlash2 is numerically identical to DFlash and learns the correction from there. Covered bytest_conv_is_identity_at_init.Selector training objective
At inference the selector walks the block sequentially, feeding each choice in as the next predecessor. Training teacher-forces the predecessor to the ground-truth previous token, so all positions score in one shot. The loss is a cross-entropy over the drafter's own top-k, restricted to rows whose ground truth survived that top-k (elsewhere there is no correct choice to make), added to the DFlash CE with weight
dflash2_selector_loss_weight.test_selector_pair_scores_match_the_sequential_selectorpins the two paths together: replaying the sequential trace's own predecessors through the vectorized scorer reproduces the same path.Because the selector scores real vocabulary ids, it requires
loss_mode=full_vocaband fails loud otherwise; a restricted or sampled vocabulary would make the candidate ids meaningless.The selector's inputs are detached. It is an auxiliary head that re-ranks what the drafter already produced, so its objective must not reshape the drafter itself.
torch.topkpasses gradient through the selected logits andhidden_projectionwould otherwise push gradient back through the backbone, which would both change the drafter and make the ablation below meaningless.test_selector_loss_does_not_backprop_into_the_backbonepins this.Acceptance length metric
The block drafters reported per-position accuracies, but those are marginals: each position is counted independently. A verifier stops at the first mismatch, so what governs the achievable speedup is the length of the correct prefix, and the marginals cannot be combined into it.
_block_acceptance_counts()is added to the shared block-drafter forward and called from the DFlash, Domino and DSpark forwards, so every block drafter emits it (Domino and DSpark carry their own copies of that forward, so they needed the call explicitly; the base trainer's aggregation was already prefix-generic for them). It is reported asmean_acceptance_lengthunder the same convention as the existing rollout-sidedrafter/spec_decode/mean_acceptance_length, whose leading1.0is the token the target emits itself at each verification step, so the training metric predicts the served one rather than sitting a token below it on the same dashboard. The raw sum and block count are published alongside it so the ratio can be inspected.The helper takes the drafted positions only and leaves the slicing to each caller, because the layouts genuinely differ: DFlash builds labels at
arange(block_size)and masks column 0 off as the anchor, while Domino and DSpark build them atarange(block_size) + 1, so their column 0 is a real prediction. Slicing inside the helper would drop that token and, worse, stop a wrong first token from truncating the block, which is the exact failure the metric exists to catch.test_acceptance_counts_do_not_assume_an_anchor_columnpins this.DFlash additionally intersects the scoring mask with the reweighted mask, because correctness is only recorded where the reweighted mask is positive. Without that, a position zeroed by
loss_decay_gammaorfront_position_weightwould read as a mismatch rather than as unscored and would truncate every block's prefix, pinning the metric at1.0while the per-position accuracies still looked healthy.Two limits worth stating. The metric is computed from the drafter's own argmax, so for DFlash2 it does not see the candidate selector that would re-rank tokens at serve time. And a block truncated by a supervision boundary counts fully in the denominator while its numerator is capped by the supervised length, which biases the training-side number below the rollout-side one in a data-dependent way. It is a training-time signal for comparing drafters, not a prediction of served speedup.
Note on the shared DFlash file
Rather than copying
DFlashTrainingModel.forwardwholesale (as Domino does, ~200 lines),DFlashTrainingModelgains a documented_auxiliary_lossextension point that returnsNonefor plain DFlash. The two conv hooks added toDFlashDecoderLayerare likewiseNonefor every non-DFlash2 drafter, mirroring how upstream's own layer carries them. Both changes leave DFlash / Domino / DSpark / JetSpec behaviour and numerics untouched, which the regression run below confirms.Review follow-ups
Three issues were raised in review. All three are real, and all three are confirmed against the released
z-lab/Qwen3.8-27B-DFlash2checkpoint rather than argued from the code alone.The RoPE base was read only from
rope_theta. transformers 5 moved it into arope_parametersdict, and the released config carries only the nested spelling, soDFlashConfigfell back to its10000.0default where the checkpoint was trained at1e7. Nothing failed, becausemodeling_dflashreadsconfig.rope_thetaand cannot tell a defaulted base from a real one. The cold-start path had the same gap on the other side, reading the base off the target config, so a target that nests it would have produced the same silent mismatch with no checkpoint involved.resolve_rope_thetanow accepts both spellings, a top-level value still wins, and it is used byDFlashConfig,modeling_dflashand all four DFlash-family fallback configs, so a DFlash baseline and a DFlash2 arm cannot end up on different bases.The resolver reads the base and discards the rest of
rope_parameters, andDFlashRotaryEmbeddingtakes only a base, so a target using yarn / linear / llama3 scaling still gets a draft whose rotary phase diverges past the original context length. That predates this branch and applying the scaling is a larger change than this PR should carry, so the resolver warns once per distinctrope_typerather than discarding it silently.Two selector weights were dropped on load. Upstream stores the codebooks as bare
nn.Parametertensors (candidate_selector.predecessor_codebook), while this overlay holds them innn.Embeddingmodules, whose state dict spells the same tensor...codebook.weight. Of the checkpoint's 81 tensors those two were the only mismatch, and nothing said so: the loader drops unrecognized keys with a debug log, and its required-key gate covers the DFlash backbone only. Both codebooks stayed at their random init while training carried on.DFlashTrainerBackendgains a_CHECKPOINT_KEY_ALIASEStable applied by the shared normalizer, DFlash2 fills in those two entries, and a new_validate_normalized_statehook rejects a checkpoint carrying only part of the DFlash2 modules under recognized names, so a future upstream rename cannot degrade into a silent cold start. A checkpoint carrying none of them is still accepted, since warm-starting DFlash2 from a plain DFlash backbone is a legitimate flow and the DFlash2 modules cold-start as an identity passthrough by design. The dropped-key report is promoted from debug to warning when keys were unexpected or shape-mismatched, which gives DFlash / DSpark / Domino the same visibility.Checked against the real checkpoint's key list: 23 of 23 DFlash2 module keys match after normalization with none stray, and without the rename exactly those two codebooks are dropped.
The two engines disagreed about
DFLASH2. SGLang failed loud with guidance while vLLM fell through to a bare "Unsupported speculative_algorithm". Worse, the guidance both should give (serve a trained DFlash2 checkpoint asDFLASH) was unfollowable on vLLM, whose DFlash drafter validator accepted onlyarchitectures=['DFlashDraftModel']and so rejectedDFlash2DraftModel. vLLM now carries the sameDFLASH2branch and message, accepts the DFlash2 architecture on the DFlash serve path, and_is_dflash_configclassifies it so a serve-only run resolves the DFlash aux layout.DFLASH2is dropped from the SGLang aux-hidden set: that path needs the samedrafter.enablethe ServerArgs override rejectsDFLASH2under, so it was unreachable.DominoDraftModelis deliberately left out of the servable set. DFlash2's serve-as-DFlash route is the documented upstream recipe for the released checkpoint, while Domino's GRU correction head has no established DFlash runtime, so failing early stays the safer answer there.The ablation below is unaffected by the RoPE fix. All three arms build their config through the same
_build_fallback_config, so they resolved the same base before and after; the fix moves absolute numbers only for a target whose config nests the base underrope_parameters.Testing
Contract tests (
tests/integration/test_dflash2_backend_contract.py): the conv and selector modules, the block-locality invariant, identity-at-init, selector vectorized-vs-sequential agreement, the selector's contribution to the total loss, the detach boundary, thefull_vocabguard, factory/aux-layer registration, config routing including the nesteddflash_configblock upstream checkpoints use, and the two acceptance-length semantics tests. The review follow-ups add the RoPE-base resolution on both the checkpoint and the cold-start path, the upstream codebook rename against the released key spelling, the partial-module guard and the plain-DFlash warm start it must not reject, and theDFLASH2routing on both engines.tests/integration/test_vllm_runtime_contract.pygains the DFlash2 case for the DFlash drafter validator. A scaledrope_typeis pinned to warn rather than pass silently.Regression: the full
tests/integration/suite on this branch versus the merge-base.The same 9 failures on both, name for name, all pre-existing and unrelated to this change (
test_drafter_runtime_control_contract,test_dspark_trainer_backend::test_dspark_checkpoint_preserves_source_config_and_vllm_weight_names,test_verl_npu_vllm_compat). This run matters more than usual because the acceptance-length call was added to the Domino and DSpark forwards as well, and neither picked up a new failure.Real-data ablation
Trained on a real multi-turn conversation dataset whose assistant turns were regenerated with the target model itself, so the drafter is fitting the distribution it will actually have to predict. 768 training conversations, a disjoint held-out set, assistant tokens only, sequences truncated to 384 tokens, target-side context hidden states pre-computed once and shared byte-for-byte by every arm. Every arm is cold-started, uses the same seed, and runs the same 4000 optimizer steps; the only variables are the algorithm and the selector loss weight.
DFLASHDFLASH2DFLASH2:0Held-out results at step 4000:
DFLASHDFLASH2DFLASH2:0DFLASH2:0vsDFLASHDFLASH2vsDFLASH2:0DFLASH2vsDFLASHPer-position accuracy, block positions 1 to 7:
DFLASHDFLASH2:0The convolution accounts for the gain, and its shape matches what it is designed to fix: the improvement grows from +7.3% at position 1 to a 20 to 25% plateau from position 3 onward, which is the end-of-block decay the writeup targets.
The selector objective does not contribute positively here. The conv-only arm is ahead of full DFlash2 on both metrics at every evaluation point, and the selector's held-out lift over the drafter's own unary ranking ends at -0.019.
Two things that reading should not be stretched into:
clip_grad_norm_computes one global norm over all parameters. The selector's gradients raise that norm, shrink the clipping coefficient, and so shrink the backbone's effective step. Single-sample steps make this worse. That is a plausible mechanism for the -0.5pp, and it is a property of the training setup rather than of the selector design.Full training log, all three arms
train_lossis a single-step instantaneous value at the evaluation point, not a running mean, and it is not comparable across arms: the DFlash2 arm's total loss carries an extra selector cross-entropy term that neither other arm has. Only the held-out numbers are comparable.What this ablation does not establish
DFLASHlacks both modules, soDFLASH2:0vsDFLASHmixes the convolution's design with 87M extra parameters. OnlyDFLASH2vsDFLASH2:0is a clean contrast, and there the counts are identical.GPU training smoke
tests/special_standalone/dflash2_gpu_smoke.pydrives the real training path end to end on a real target with a cold-started draft. It replays one fixed batch, so its terminalacc 1.00is memorization and carries no generalization signal; it exists to prove the path trains and that gradients reach both the convolutions and the selector, and the ablation above is what speaks to draft quality.The smoke emulates the production dtype setup deliberately: FSDP
MixedPrecision(param_dtype=bf16)gives the forward bf16 parameters, and the surroundingtorch.amp.autocastkeepscross_entropyin fp32. Emulating only one of the two fails, in a different place each time.Out of scope
Inference-side tree drafting and serving, same as for DFlash / Domino / JetSpec in this repo. This PR is training-side only, and without tree drafting the selector cannot be evaluated at the job it was designed for.
Online co-training is out of scope for the same reason Domino's is:
DFLASH2is not an engine-level algorithm, so both engines reject the string and point atDFLASH. Train the drafter offline, then serve the checkpoint asDFLASHto use it as a frozen rollout drafter.