|
| 1 | +# DSpark Speculative Decoding (Draft Training) Design |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This page is a design proposal (RFC) for adding **DSpark** speculative-decoding |
| 6 | +draft training to Megatron-Bridge. It tracks |
| 7 | +[issue #5230](https://github.com/NVIDIA-NeMo/Megatron-Bridge/issues/5230). |
| 8 | + |
| 9 | +DSpark ([arXiv:2607.05147](https://arxiv.org/abs/2607.05147)) is a |
| 10 | +speculative-decoding method that trains a lightweight **draft** against a frozen |
| 11 | +target. The draft proposes a block of tokens; the target verifies them. DSpark's |
| 12 | +draft is *semi-autoregressive*: a parallel backbone predicts a whole block in one |
| 13 | +pass, and a small sequential head restores the intra-block token dependency that a |
| 14 | +purely parallel drafter loses (the "suffix decay" problem). A confidence head |
| 15 | +predicts per-position acceptance so the serving engine can schedule its verify |
| 16 | +budget. |
| 17 | + |
| 18 | +A working draft-training reference already exists in NeMo AutoModel at |
| 19 | +`nemo_automodel/components/speculative/dspark/`. This document specifies how the |
| 20 | +same training recipe maps onto Megatron-Bridge and Megatron-Core. It |
| 21 | +intentionally does **not** ship the draft module itself: that is a Megatron-Core |
| 22 | +neural module (see [Decision: Megatron-Core native](#decision-megatron-core-native)) |
| 23 | +whose parallel/loss integration must be validated on multi-GPU runs, so it belongs |
| 24 | +in later, separately reviewed PRs. |
| 25 | + |
| 26 | +## Background: the DSpark draft |
| 27 | + |
| 28 | +### Semi-autoregressive draft |
| 29 | + |
| 30 | +For a block of `block_size` positions (`gamma`, default 7 offline / 5 in |
| 31 | +production), the draft factorizes the block distribution as |
| 32 | +`P(x | x0) = prod_k p_k(x_k | x0, x_1..x_{k-1})`: |
| 33 | + |
| 34 | +- **Parallel backbone.** A small transformer predicts every position of the block |
| 35 | + in one forward pass. Crucially, the backbone does not re-encode the prompt: it |
| 36 | + attends to the frozen target's hidden states as the attention **context K/V**, |
| 37 | + with the block's own "noise" slots as the queries. Each block query attends to |
| 38 | + the context strictly before its anchor and bidirectionally within its own block. |
| 39 | + This is the block-parallel (DFlash-style) half of the draft. |
| 40 | +- **Sequential head.** A lightweight module adds intra-block dependency on top of |
| 41 | + the parallel logits. DSpark provides a **Markov head** (a low-rank, |
| 42 | + token-conditioned additive logit bias, rank 256) and an **RNN head** (a |
| 43 | + GRU-like recurrence carrying prefix state across the block). At training time |
| 44 | + the head is teacher-forced with the previous block tokens; only at inference is |
| 45 | + it run serially. |
| 46 | +- **Confidence head.** A small projection `c_k = sigmoid(w^T [h_k ; embed(x_{k-1})])` |
| 47 | + predicts each position's acceptance probability. |
| 48 | + |
| 49 | +The draft **shares and freezes the target's token embedding and LM head**; there |
| 50 | +is no separate (compressed) draft vocabulary or `d2t`/`t2d` remapping. The |
| 51 | +target's selected decoder-layer hidden states are fused by a learned linear |
| 52 | +projection (`fc`) into the draft hidden size and used as the attention context. |
| 53 | + |
| 54 | +### Training objective |
| 55 | + |
| 56 | +The draft is trained with three position-weighted terms (weights `exp(-k/gamma)`, |
| 57 | +emphasizing earlier positions): |
| 58 | + |
| 59 | +- `L_ce` (weight 0.1): cross-entropy of the draft logits against the target's |
| 60 | + teacher-forced next tokens. |
| 61 | +- `L_l1` (weight 0.9): the **raw** probability L1 distance `||p_draft - p_target||_1` |
| 62 | + to the target's next-token distribution. This is the dominant term and a direct |
| 63 | + acceptance proxy. |
| 64 | +- `L_conf` (weight 1.0): binary cross-entropy training the confidence head against |
| 65 | + the analytical acceptance label `c_k* = 1 - 0.5 * ||p_draft - p_target||_1`. |
| 66 | + |
| 67 | +The factor `1/2` belongs to the acceptance label only. Paper |
| 68 | +[Eq. 10](https://arxiv.org/html/2607.05147#S3.E10) minimizes the weighted **raw** |
| 69 | +L1 distance at coefficient 0.9; the `1/2` that converts L1 into a total-variation |
| 70 | +distance appears in [Eq. 8](https://arxiv.org/html/2607.05147#S3.E8), where the |
| 71 | +analytical acceptance/confidence target is formed. Both the |
| 72 | +[DeepSpec](https://github.com/deepseek-ai/DeepSpec/blob/main/deepspec/modeling/dspark/loss.py) |
| 73 | +and [NeMo AutoModel](https://github.com/NVIDIA-NeMo/Automodel/blob/main/nemo_automodel/components/speculative/dspark/loss.py) |
| 74 | +implementations make the same distinction. Training `0.5 * L1` instead would halve |
| 75 | +the dominant term's gradient while leaving the published coefficient in place, so |
| 76 | +the two must not be conflated. A regression test asserts the pair together |
| 77 | +(disjoint distributions give `l1_loss == 2` and `accept_rate == 0`) so the |
| 78 | +implementation and this page cannot drift apart. |
| 79 | + |
| 80 | +The target's teacher distribution is produced by passing the target's last hidden |
| 81 | +state through the (shared, frozen) LM head and is always detached, so no gradient |
| 82 | +flows into the target. |
| 83 | + |
| 84 | +### Acceptance and confidence |
| 85 | + |
| 86 | +Training measures acceptance analytically rather than by running rejection |
| 87 | +sampling. Per position, `accept_rate = clamp(1 - 0.5 * ||p_draft - p_target||_1, 0, 1)` |
| 88 | +(this is where the `1/2` of Eq. 8 lives). The expected accepted |
| 89 | +prefix length of a block is `tau = sum_k cumprod(accept_rate)_k + 1` (a token |
| 90 | +survives only if every earlier token in its block is accepted; the `+1` counts the |
| 91 | +verified anchor token). `tau` is the training-time speedup proxy. The confidence |
| 92 | +head regresses to `accept_rate`; the paper applies a post-hoc Sequential |
| 93 | +Temperature Scaling calibration for serving. |
| 94 | + |
| 95 | +## Where DSpark fits in Megatron-Bridge |
| 96 | + |
| 97 | +### Current state |
| 98 | + |
| 99 | +Megatron-Bridge has no standalone speculative or draft-training subsystem today. |
| 100 | +The only draft-adjacent training path is **Multi-Token Prediction (MTP)**, which |
| 101 | +is a native Megatron-Core module that Bridge integrates thinly (provider flags, |
| 102 | +batch plumbing in `training/gpt_step.py`, and per-model HF weight mappings such as |
| 103 | +`mtp.*` in `models/qwen/qwen3_bridge.py`). EAGLE and Medusa appear only in the |
| 104 | +ModelOpt **export** path, not in training. |
| 105 | + |
| 106 | +DSpark's draft (semi-autoregressive block plus Markov and confidence heads) is a |
| 107 | +new neural module, so the first thing this RFC has to settle is which repository |
| 108 | +owns it end to end. The two candidates lead to genuinely different contracts, and |
| 109 | +picking one is a prerequisite for every section below. |
| 110 | + |
| 111 | +1. **Megatron-Core native** (as MTP does in |
| 112 | + `megatron/core/transformer/multi_token_prediction.py`): Megatron-Core owns the |
| 113 | + parallel module, the loss folding, and the sharded checkpoint; Bridge owns |
| 114 | + configuration and the HF mapping. Requires a Megatron-LM change and a |
| 115 | + Bridge-defined HF export schema. |
| 116 | +2. **ModelOpt-injected** (as EAGLE/Medusa are, via `modelopt.torch.speculative`): |
| 117 | + ModelOpt owns the transformation, module, loss, and export; Bridge only |
| 118 | + configures it. Requires nothing new in Megatron-LM, but ModelOpt has to grow a |
| 119 | + Megatron-side DSpark plugin first. |
| 120 | + |
| 121 | +### Decision: Megatron-Core native |
| 122 | + |
| 123 | +**This RFC selects option 1.** ModelOpt's DSpark support is currently |
| 124 | +HF-path-only: `modelopt/torch/speculative/plugins/` ships `hf_dspark.py` and |
| 125 | +`modeling_dspark.py`, and `modelopt/torch/export/plugins/hf_spec_export.py` ships |
| 126 | +`DSparkExporter`, but the Megatron-side plugins are `megatron_eagle.py` and |
| 127 | +`megatron_medusa.py` only. There is no `megatron_dspark.py`. Choosing option 2 |
| 128 | +would therefore mean landing a Megatron-native DSpark in Model-Optimizer first and |
| 129 | +then waiting on Bridge's ModelOpt pin (currently `nvidia-modelopt==0.46.0rc1`) to |
| 130 | +pick it up, which puts a cross-repo dependency in front of every Bridge-side |
| 131 | +milestone. Option 1 keeps the critical path inside Megatron-LM and Bridge, where |
| 132 | +MTP already demonstrates the pattern end to end. |
| 133 | + |
| 134 | +Consequences of the decision, which the rest of this document then specifies: |
| 135 | + |
| 136 | +- The parallel draft module, its loss folding, and its sharded checkpoint are a |
| 137 | + **Megatron-Core** contribution, not a Bridge one. |
| 138 | +- Bridge owns the **HF export schema** for trained drafts, because ModelOpt's |
| 139 | + `DSparkExporter` covers its own HF-path artifacts and not an MCore-native |
| 140 | + checkpoint. |
| 141 | +- The architecture-agnostic pieces that depend only on `torch` (the sequential and |
| 142 | + confidence heads, and the CE / L1 / confidence objective) can be developed and |
| 143 | + unit-tested in Bridge, and move to Megatron-Core when the parallel module lands |
| 144 | + there. They are not a substitute for that module. |
| 145 | + |
| 146 | +## Proposed design |
| 147 | + |
| 148 | +The construction path is `ModelConfig` + `ModelBuilder`, **not** a |
| 149 | +`ModelProviderMixin` subclass: `ModelProviderMixin`-based configuration is |
| 150 | +deprecated in current Bridge main (`models/model_provider.py` raises a |
| 151 | +`DeprecationWarning` directing callers to `ModelConfig` + `ModelBuilder`), so a new |
| 152 | +provider subclass could not be the stable API this RFC promises. The DSpark config |
| 153 | +subclasses `GPTModelConfig` and names a `DSparkModelBuilder`, mirroring how |
| 154 | +`MambaModelConfig` names `MambaModelBuilder`. |
| 155 | + |
| 156 | +| Piece | Location (proposed) | Mirrors | |
| 157 | +|---|---|---| |
| 158 | +| Draft config + builder | `models/dspark/dspark_builder.py` (`DSparkModelConfig`, `DSparkModelBuilder`) | `models/mamba/mamba_builder.py` (`MambaModelConfig` naming `MambaModelBuilder`) | |
| 159 | +| Parallel draft module | Megatron-Core `megatron/core/transformer/` | `multi_token_prediction.py` | |
| 160 | +| Custom forward + loss dispatch | `training/gpt_step.py` (`forward_step_dspark`) | `forward_step_modelopt` | |
| 161 | +| DSpark loss | `training/post_training/dspark/` | `post_training/distillation.py` (the three-term CE/L1/confidence loss and the `tau` acceptance metric) | |
| 162 | +| Thin entry point | `training/dspark.py` | `training/distill.py` | |
| 163 | +| HF to/from Megatron weight mapping for `dspark.*` params | `models/<family>/<name>_bridge.py` mapping registry | the `mtp.*` mappings in `qwen3_bridge.py` | |
| 164 | +| Recipe + example | `recipes/<family>/…` + `examples/dspark/` | the distillation recipe/example | |
| 165 | +| Docs | this page | `multi-token-prediction.md` | |
| 166 | + |
| 167 | +### Loss return contract |
| 168 | + |
| 169 | +`dspark_loss` follows the Megatron-Core 3-tuple contract used by |
| 170 | +`training/losses.py`: it returns the **unnormalized** micro-batch numerator, this |
| 171 | +micro-batch's `num_tokens`, and a report mapping each metric to a reducible |
| 172 | +`[numerator, denominator]` pair. Normalization happens once, against the globally |
| 173 | +reduced token count. |
| 174 | + |
| 175 | +This requires `calculate_per_token_loss=True` on the model config, and DSpark |
| 176 | +training must fail fast when it is unset. With the default `False`, Megatron-Core |
| 177 | +divides each micro-batch by its own `num_tokens` before averaging over |
| 178 | +micro-batches, which trains a mean of micro-batch means. That is not a rounding |
| 179 | +detail: with two ranks running two micro-batches each at uneven supervised-token |
| 180 | +counts, the per-micro-batch normalization produces a gradient **37% away** from |
| 181 | +the full-batch reference, while the global-token normalization matches it to |
| 182 | +`1.8e-07`. |
| 183 | + |
| 184 | +Reporting pairs exist for the same reason. A logged ratio must be formed as |
| 185 | +`sum(numerator) / sum(denominator)` over the log window and the data-parallel |
| 186 | +group; averaging per-micro-batch ratios reintroduces the same bias in the metrics. |
| 187 | + |
| 188 | +### Memory contract for the full-vocabulary terms |
| 189 | + |
| 190 | +Both the cross-entropy and the L1 term need the draft's normalized distribution |
| 191 | +over the whole vocabulary. At reference scale (`num_blocks=512`, `block_size=7`, |
| 192 | +`vocab≈150k`) one FP32 distribution tensor is 2.03 GiB, and computing the terms |
| 193 | +separately keeps several live at once: the L1 needs the draft probabilities, the |
| 194 | +target probabilities, and their difference, while the cross-entropy saves its own |
| 195 | +`log_softmax` for the backward. Measured on an 80 GB card, an unchunked pass costs |
| 196 | +**10.15 GiB** of forward-plus-backward overhead above its inputs. |
| 197 | + |
| 198 | +The implementation therefore derives both terms from a single FP32 `log_softmax`, |
| 199 | +chunks the flattened rows, and recomputes each chunk in the backward. That brings |
| 200 | +the overhead to **2.06 GiB** (4.9x) at identical values for both terms. Any owner |
| 201 | +of this module inherits the same constraint: either chunk, or state and enforce a |
| 202 | +much smaller anchor-count contract with measurements. Chunking only the L1 is not |
| 203 | +enough, because the cross-entropy's saved `log_softmax` is a full-size tensor of |
| 204 | +the same order. |
| 205 | + |
| 206 | +### Frozen weights: replicas, not aliases |
| 207 | + |
| 208 | +The draft does **not** physically alias the target's parameters. The AutoModel |
| 209 | +reference creates draft-owned `embed_tokens` and `lm_head`, copies the target |
| 210 | +weights into them, freezes the replicas, and controls their trainability |
| 211 | +separately. This RFC adopts the same storage model, because it is what makes the |
| 212 | +remaining contracts well-defined: |
| 213 | + |
| 214 | +- **Pipeline placement.** Replicas let the draft's embedding and LM head sit on |
| 215 | + whatever pipeline stage hosts the draft, instead of forcing the draft to share a |
| 216 | + stage with the target's first and last stages. |
| 217 | +- **Checkpointing.** The frozen replicas are ordinary sharded-checkpoint entries. |
| 218 | + They are saved (not reconstructed) so a resumed run is bit-identical without |
| 219 | + needing the target checkpoint present. |
| 220 | +- **Trainability.** "Frozen" is a `requires_grad=False` property of draft-owned |
| 221 | + parameters, not an aliasing side effect, so unfreezing the LM head later is a |
| 222 | + config change rather than a structural one. |
| 223 | + |
| 224 | +### Frozen-target supervision |
| 225 | + |
| 226 | +The draft trains against features captured from the frozen target: the selected |
| 227 | +decoder-layer hidden states (fused by `fc` into the draft's context K/V) and the |
| 228 | +final hidden state (which drives the teacher distribution through the shared LM |
| 229 | +head). Two capture modes are supported, with different schemas: |
| 230 | + |
| 231 | +- **Colocated capture** during the same forward (analogous to how MTP consumes |
| 232 | + extra token IDs), keeping the target and draft in one model. No cache format; |
| 233 | + costs target memory. |
| 234 | +- **Offline precomputation** of target features, streamed to a draft-only training |
| 235 | + job (as AutoModel's `precompute_dspark.py` does). Sidesteps holding the target |
| 236 | + in memory, and requires a versioned cache schema recording the target checkpoint |
| 237 | + id, the selected layer indices, the hidden dtype, the anchor sampling seed, and |
| 238 | + the block size, so a cache can never be silently paired with a different target. |
| 239 | + |
| 240 | +Both modes must respect tensor, pipeline, and context parallelism: |
| 241 | + |
| 242 | +- **Tensor parallel.** The teacher and draft logits are vocab-sharded, so the L1 |
| 243 | + distance must be computed shard-locally and summed across the TP group rather |
| 244 | + than gathered: `||p_d - p_t||_1` is the sum over TP shards of the local absolute |
| 245 | + difference, with the softmax denominators reduced across the group first. The |
| 246 | + exactness of this term is what the acceptance label depends on, so an |
| 247 | + approximate (top-k, or gathered in lower precision) path is not acceptable here. |
| 248 | +- **Pipeline parallel.** The selected decoder-layer hidden states and the final |
| 249 | + hidden state must be transported to the draft's stage. When the draft shares the |
| 250 | + last stage, this is a local read; otherwise the selected hiddens are additional |
| 251 | + pipeline payload, and their dtype and layout are part of the module contract. |
| 252 | +- **Context parallel.** Anchor blocks must not straddle CP shard boundaries. |
| 253 | + Anchors are sampled per CP shard from positions whose whole block is local, and |
| 254 | + the loss mask zeroes any block that would cross a boundary, so no cross-shard |
| 255 | + attention or gather is needed for the draft. |
| 256 | + |
| 257 | +### Checkpoint and export ownership |
| 258 | + |
| 259 | +- **Training checkpoint.** MCore-native sharded checkpoint, owned by the |
| 260 | + Megatron-Core module, covering the draft backbone, `fc`, `markov_head`, |
| 261 | + `confidence_head`, and the frozen embedding and LM-head replicas. |
| 262 | +- **HF export.** Owned by Bridge. ModelOpt's `DSparkExporter` targets ModelOpt's |
| 263 | + own HF-path artifacts, so an MCore-native checkpoint needs its own HF schema |
| 264 | + before `AutoBridge` can round-trip it. The bridge mapping registry gains |
| 265 | + `dspark.*` entries (QKV and gated-MLP splits for the draft backbone, plain |
| 266 | + linears for `fc` and the heads); the frozen replicas are exported as ordinary |
| 267 | + weights so a served draft is self-contained. The `mtp.*` mappings in |
| 268 | + `qwen3_bridge.py` are the closest existing template. |
| 269 | + |
| 270 | +These mappings are specified only once the storage model above is fixed; listing |
| 271 | +them earlier would have been guesswork about whether the shared tensors are stored |
| 272 | +or reconstructed. |
| 273 | + |
| 274 | +## References |
| 275 | + |
| 276 | +- DSpark paper: [arXiv:2607.05147](https://arxiv.org/abs/2607.05147), |
| 277 | + *Confidence-Scheduled Speculative Decoding with Semi-Autoregressive Generation*. |
| 278 | +- Draft-training reference implementation: NeMo AutoModel, |
| 279 | + [`nemo_automodel/components/speculative/dspark/`](https://github.com/NVIDIA-NeMo/Automodel/tree/main/nemo_automodel/components/speculative/dspark). |
| 280 | +- Tracking issue: [#5230](https://github.com/NVIDIA-NeMo/Megatron-Bridge/issues/5230). |
| 281 | +- Related in-repo training: [Multi-Token Prediction](multi-token-prediction.md). |
0 commit comments