-
Notifications
You must be signed in to change notification settings - Fork 552
[Speculative Decoding] DFlash2 draft variant (sublayer convolution + candidate selector) #2216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1419d47
6b4ca0d
9975a5b
197dd85
13e8e2f
ba377e7
fe8d5b8
c446f6e
bf36110
fd085f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,18 +31,26 @@ | |||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def _get_rope_theta(config, default=None): | ||||||||||||||||||
| """Get RoPE theta from either legacy or Transformers 5 config fields.""" | ||||||||||||||||||
| rope_theta = getattr(config, "rope_theta", None) | ||||||||||||||||||
| if rope_theta is not None: | ||||||||||||||||||
| return rope_theta | ||||||||||||||||||
|
|
||||||||||||||||||
| """Get RoPE theta from either legacy or Transformers 5 config fields. | ||||||||||||||||||
|
|
||||||||||||||||||
| ``rope_parameters`` is checked FIRST. A config can carry both fields with | ||||||||||||||||||
| different values: Transformers 5 stores the real base under | ||||||||||||||||||
| ``rope_parameters`` while the class default (10000.0 for Qwen3) may still be | ||||||||||||||||||
| visible as a top-level ``rope_theta``. Reading ``rope_theta`` first silently | ||||||||||||||||||
| exports a draft whose RoPE base is 100x off the target's, which breaks | ||||||||||||||||||
| serving because DFlash injects the target's KV into every draft layer. | ||||||||||||||||||
| """ | ||||||||||||||||||
| # Transformers 5 stores this under rope_parameters (and exposes the same | ||||||||||||||||||
| # data through rope_scaling for backwards compatibility). | ||||||||||||||||||
| for attr in ("rope_parameters", "rope_scaling"): | ||||||||||||||||||
| rope_config = getattr(config, attr, None) | ||||||||||||||||||
| if isinstance(rope_config, dict) and rope_config.get("rope_theta") is not None: | ||||||||||||||||||
| return rope_config["rope_theta"] | ||||||||||||||||||
|
|
||||||||||||||||||
| rope_theta = getattr(config, "rope_theta", None) | ||||||||||||||||||
| if rope_theta is not None: | ||||||||||||||||||
| return rope_theta | ||||||||||||||||||
|
|
||||||||||||||||||
| return default | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -533,3 +541,45 @@ def _export_config(self): | |||||||||||||||||
| } | ||||||||||||||||||
| ) | ||||||||||||||||||
| return config | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class DFlash2Exporter(DFlashExporter): | ||||||||||||||||||
| """Draft model exporter for DFlash2 (DFlash backbone + convolutions + selector). | ||||||||||||||||||
|
|
||||||||||||||||||
| Same z-lab-compatible format as DFlash, plus the DFlash2 weights | ||||||||||||||||||
| (``layers.*.attention_conv.*`` / ``layers.*.mlp_conv.*`` / | ||||||||||||||||||
| ``candidate_selector.*``, already captured by the inherited ``dflash_module.`` | ||||||||||||||||||
| stripping) and the config fields the SGLang/vLLM ``DFlash2DraftModel`` loader | ||||||||||||||||||
| needs to rebuild them (``conv_kernel_size``, ``conv_group_size``, | ||||||||||||||||||
| ``selector_rank``, ``selector_top_k``). | ||||||||||||||||||
|
|
||||||||||||||||||
| The architecture name is what selects the DFlash2 serving path: a checkpoint | ||||||||||||||||||
| declaring ``DFlashDraftModel`` loads as a plain DFlash draft and would silently | ||||||||||||||||||
| ignore the convolutions and the selector. | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| def _export_config(self): | ||||||||||||||||||
| """Extend the DFlash config with the DFlash2 architecture fields.""" | ||||||||||||||||||
| config = super()._export_config() | ||||||||||||||||||
| draft_config = self.model.dflash_config | ||||||||||||||||||
|
|
||||||||||||||||||
| config["architectures"] = ["DFlash2DraftModel"] | ||||||||||||||||||
| # Present because HFDFlash2Model.modify validates them at convert time. | ||||||||||||||||||
| config["dflash_config"].update( | ||||||||||||||||||
| { | ||||||||||||||||||
| "projector_type": getattr(draft_config, "projector_type", "dflash2"), | ||||||||||||||||||
| "conv_kernel_size": draft_config.conv_kernel_size, | ||||||||||||||||||
| "conv_group_size": draft_config.conv_group_size, | ||||||||||||||||||
| "selector_rank": draft_config.selector_rank, | ||||||||||||||||||
| "selector_top_k": draft_config.selector_top_k, | ||||||||||||||||||
| # The published DFlash2 checkpoints carry block_size inside | ||||||||||||||||||
| # dflash_config; the DFlash loader reads it from the top level. | ||||||||||||||||||
| # Emit both so either contract resolves to the same value. | ||||||||||||||||||
| "block_size": config["block_size"], | ||||||||||||||||||
| } | ||||||||||||||||||
| ) | ||||||||||||||||||
| # Published DFlash2 checkpoints state causality explicitly rather than | ||||||||||||||||||
| # leaving it to be inferred from layer_types. Only set it when the SWA | ||||||||||||||||||
| # block above has not already written a `causal` entry. | ||||||||||||||||||
| config.setdefault("is_causal", config["dflash_config"].get("causal", False)) | ||||||||||||||||||
|
Comment on lines
+581
to
+584
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] This
The value itself is right (
Suggested change
or, if |
||||||||||||||||||
| return config | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -419,10 +419,21 @@ def modify(self, config): | |
| # overwrite any user value and warn. (rope_scaling is intentionally NOT inherited: | ||
| # DFlash uses standard Qwen3 RotaryEmbedding; the long-context YaRN scaling is | ||
| # added only at export via dflash_export_rope_scaling.) | ||
| # A config can carry BOTH a top-level rope_theta and a rope_parameters dict | ||
| # with different values: Transformers 5 keeps the real base in | ||
| # rope_parameters while the class default (10000.0 for Qwen3) stays visible | ||
| # as rope_theta. rope_parameters wins, otherwise the draft trains against a | ||
| # RoPE base 100x off the target's. | ||
| base_rope_params = getattr(base_config, "rope_parameters", None) | ||
| if not isinstance(base_rope_params, dict): | ||
| base_rope_params = {} | ||
| for attr in ("rope_theta", "rope_type", "rope_interleaved"): | ||
| if not hasattr(base_config, attr): | ||
| if attr in base_rope_params: | ||
| base_val = base_rope_params[attr] | ||
| elif hasattr(base_config, attr): | ||
| base_val = getattr(base_config, attr) | ||
| else: | ||
| continue | ||
|
Comment on lines
+427
to
436
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [IMPORTANT Compatibility] Reading What. The loop now resolves all three of Why it matters. The comment directly above says:
setattr(self.dflash_config, "rope_type", "yarn")
draft_rope_params["rope_type"] = "yarn" # dict the rotary module actually readswhile Suggested fix. Scope the nested lookup to the field the bug is actually about, and leave for attr in ("rope_theta", "rope_type", "rope_interleaved"):
# Only rope_theta is read from the nested dict: rope_type without its
# companion scaling fields (factor, original_max_position_embeddings)
# would put the draft's rotary embedding on a scaling path it has no
# parameters for. Long-context scaling is injected at export instead.
if attr == "rope_theta" and attr in base_rope_params:
base_val = base_rope_params[attr]
elif hasattr(base_config, attr):
base_val = getattr(base_config, attr)
else:
continueIf inheriting the full scaling config for the draft is actually intended, it needs to copy |
||
| base_val = getattr(base_config, attr) | ||
| user_val = getattr(self.dflash_config, attr, None) | ||
| if user_val is not None and user_val != base_val: | ||
| logger.warning( | ||
|
|
@@ -434,6 +445,12 @@ def modify(self, config): | |
| base_val, | ||
| ) | ||
| setattr(self.dflash_config, attr, base_val) | ||
| # Qwen3Config populates rope_parameters at construction, so a later | ||
| # setattr on the flat field alone would leave the dict — which is what | ||
| # the rotary module reads — holding the stale value. | ||
| draft_rope_params = getattr(self.dflash_config, "rope_parameters", None) | ||
| if isinstance(draft_rope_params, dict) and attr in draft_rope_params: | ||
| draft_rope_params[attr] = base_val | ||
|
|
||
| self.dflash_config.head_dim = getattr( | ||
| self.dflash_config, | ||
|
|
@@ -632,7 +649,14 @@ def _build_generate_swa_mask(self, ctx_len, bsz, dtype, device): | |
| return attn_mask | ||
|
|
||
| def _compute_loss( | ||
| self, logits, input_ids, anchor_positions, block_keep_mask, loss_mask, base_logits=None | ||
| self, | ||
| logits, | ||
| input_ids, | ||
| anchor_positions, | ||
| block_keep_mask, | ||
| loss_mask, | ||
| base_logits=None, | ||
| draft_hidden=None, | ||
| ): | ||
| """Compute weighted cross-entropy (or KD) loss and accuracy. | ||
|
|
||
|
|
@@ -643,6 +667,8 @@ def _compute_loss( | |
| block_keep_mask: Valid block mask [B, N]. | ||
| loss_mask: Token-level loss mask [B, seq_len]. | ||
| base_logits: Base model logits for KD loss [B, seq_len, vocab], or None for CE. | ||
| draft_hidden: Draft hidden states [B, N*block_size, H] behind ``logits``. | ||
| Unused here; DFlash2 needs them for its candidate-selector term. | ||
|
|
||
| Returns: | ||
| (loss, accuracy) tuple. | ||
|
|
@@ -921,6 +947,7 @@ def forward( | |
| block_keep_mask, | ||
| loss_mask, | ||
| base_outputs.logits if self.dflash_self_logit_distillation else None, | ||
| draft_hidden=hidden, | ||
| ) | ||
|
|
||
| return ModelOutput( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Reduce this entry to two sentences.
This entry has four sentences. The changelog standard limits each entry to one or two external-user sentences.
As per coding guidelines, each
CHANGELOG.rstentry must use one or two sentences written for external users.Proposed revision
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines