Skip to content

Commit 9d31c98

Browse files
authored
docs(training): add DSpark speculative decoding design (#5233)
Signed-off-by: khazic <khazzz1c@gmail.com>
1 parent 1a9d8d5 commit 9d31c98

2 files changed

Lines changed: 282 additions & 0 deletions

File tree

‎docs/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ training/moe-optimization.md
8282
training/peft.md
8383
training/packed-sequences.md
8484
training/multi-token-prediction.md
85+
training/dspark-speculative-decoding.md
8586
training/callbacks.md
8687
```
8788

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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

Comments
 (0)