Skip to content

Commit ac09592

Browse files
committed
docs(training): settle the DSpark owner, objective, and parallel contracts
Select Megatron-Core native ownership and give the reasons. ModelOpt's DSpark support is HF-path only (hf_dspark and modeling_dspark plugins plus DSparkExporter, with megatron_eagle and megatron_medusa but no megatron_dspark), so routing through ModelOpt would put a Model-Optimizer contribution and a pin bump in front of every Bridge milestone. Replace the ModelProviderMixin recommendation with ModelConfig + ModelBuilder. Provider-based configuration is deprecated in current main, so it could not be the stable API this page promises. Correct the objective: the training term is the raw weighted L1 of paper Eq. 10, and the factor 1/2 belongs only to the Eq. 8 acceptance label. Training half the L1 at the published coefficient would halve the dominant gradient. Define the contracts the previous revision left open: replica-not-alias storage for the frozen embedding and LM head, shard-local exact L1 under tensor parallelism, selected-hidden transport under pipeline parallelism, block-aligned anchor sampling under context parallelism, the versioned offline feature-cache schema, the Megatron-Core loss return contract with its calculate_per_token_loss requirement, the measured memory contract for the probability L1, and Bridge as the owner of the HF export schema. Signed-off-by: khazic <khazzz1c@gmail.com>
1 parent a4047be commit ac09592

1 file changed

Lines changed: 170 additions & 56 deletions

File tree

‎docs/training/dspark-speculative-decoding.md‎

Lines changed: 170 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ budget.
1818
A working draft-training reference already exists in NeMo AutoModel at
1919
`nemo_automodel/components/speculative/dspark/`. This document specifies how the
2020
same training recipe maps onto Megatron-Bridge and Megatron-Core. It
21-
intentionally does **not** ship the draft module itself: that is a
22-
Megatron-Core (or ModelOpt) neural module whose parallel/loss integration must be
23-
validated on multi-GPU runs, so it belongs in later, separately reviewed PRs.
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.
2425

2526
## Background: the DSpark draft
2627

@@ -57,11 +58,24 @@ emphasizing earlier positions):
5758

5859
- `L_ce` (weight 0.1): cross-entropy of the draft logits against the target's
5960
teacher-forced next tokens.
60-
- `L_tv` (weight 0.9): total-variation distance `0.5 * ||p_draft - p_target||_1`
61+
- `L_l1` (weight 0.9): the **raw** probability L1 distance `||p_draft - p_target||_1`
6162
to the target's next-token distribution. This is the dominant term and a direct
6263
acceptance proxy.
6364
- `L_conf` (weight 1.0): binary cross-entropy training the confidence head against
64-
the analytical acceptance label `c_k* = 1 - TV(p_draft, p_target)`.
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.
6579

6680
The target's teacher distribution is produced by passing the target's last hidden
6781
state through the (shared, frozen) LM head and is always detached, so no gradient
@@ -70,7 +84,8 @@ flows into the target.
7084
### Acceptance and confidence
7185

7286
Training measures acceptance analytically rather than by running rejection
73-
sampling. Per position, `accept_rate = clamp(1 - TV, 0, 1)`. The expected accepted
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
7489
prefix length of a block is `tau = sum_k cumprod(accept_rate)_k + 1` (a token
7590
survives only if every earlier token in its block is accepted; the `+1` counts the
7691
verified anchor token). `tau` is the training-time speedup proxy. The confidence
@@ -89,73 +104,172 @@ batch plumbing in `training/gpt_step.py`, and per-model HF weight mappings such
89104
ModelOpt **export** path, not in training.
90105

91106
DSpark's draft (semi-autoregressive block plus Markov and confidence heads) is a
92-
new neural module. It therefore has to live in one of two places:
93-
94-
1. **A Megatron-Core native module** (as MTP does in
95-
`megatron/core/transformer/multi_token_prediction.py`), with tensor- and
96-
pipeline-parallel-aware layers and loss folding. This is a Megatron-LM change,
97-
not a Bridge change.
98-
2. **A ModelOpt-injected module** (as EAGLE/Medusa are, via
99-
`modelopt.torch.speculative`), in which case Bridge only needs a provider hook,
100-
a custom forward step, and export mappings.
101-
102-
### Blueprint: the ModelOpt distillation subsystem
103-
104-
Bridge already contains a self-contained, bolt-on training subsystem that DSpark
105-
should mirror: **ModelOpt knowledge distillation**. It slots a new training mode
106-
in without touching the core training loop, via four pieces:
107-
108-
- `models/distillation_provider.py`: a provider that dynamically subclasses the
109-
base provider and registers a pre-wrap hook to mutate the already-built,
110-
weight-loaded model (there, it attaches the teacher).
111-
- `training/post_training/distillation.py`: the custom loss (`loss_func_kd`).
112-
- `training/gpt_step.py`: `forward_step_modelopt`, which selects that loss when the
113-
model is a distillation model.
114-
- `training/distill.py`: a thin entry point, `distill(config) = pretrain(config,
115-
forward_step_modelopt)`.
116-
117-
Plus an `examples/distillation/` recipe and a `docs/training/` page. This
118-
provider-hook plus custom-forward-step plus custom-loss plus thin-entry pattern is
119-
the established, low-friction way to add a training subsystem here, and it is the
120-
recommended shape for DSpark.
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.
121145

122146
## Proposed design
123147

124-
Following the distillation blueprint, the Bridge-side surface is:
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`.
125155

126156
| Piece | Location (proposed) | Mirrors |
127157
|---|---|---|
128-
| Draft provider hook | `models/dspark_provider.py` | `distillation_provider.py` (dynamic subclass + pre-wrap hook that attaches the frozen target's feature capture and the draft heads) |
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` |
129160
| Custom forward + loss dispatch | `training/gpt_step.py` (`forward_step_dspark`) | `forward_step_modelopt` |
130-
| DSpark loss | `training/post_training/dspark.py` | `post_training/distillation.py` (the three-term CE/TV/confidence loss and the `tau` acceptance metric) |
161+
| DSpark loss | `training/post_training/dspark/` | `post_training/distillation.py` (the three-term CE/L1/confidence loss and the `tau` acceptance metric) |
131162
| Thin entry point | `training/dspark.py` | `training/distill.py` |
132163
| HF to/from Megatron weight mapping for `dspark.*` params | `models/<family>/<name>_bridge.py` mapping registry | the `mtp.*` mappings in `qwen3_bridge.py` |
133-
| Export flag | ModelOpt `export_extra_modules` path | the existing EAGLE/Medusa/MTP export |
134164
| Recipe + example | `recipes/<family>/…` + `examples/dspark/` | the distillation recipe/example |
135165
| Docs | this page | `multi-token-prediction.md` |
136166

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+
137224
### Frozen-target supervision
138225

139226
The draft trains against features captured from the frozen target: the selected
140227
decoder-layer hidden states (fused by `fc` into the draft's context K/V) and the
141228
final hidden state (which drives the teacher distribution through the shared LM
142-
head). In Megatron-Core, this capture must respect tensor, pipeline, and context
143-
parallelism. Two options carry different trade-offs:
144-
145-
- Colocated capture during the same forward (analogous to how MTP consumes extra
146-
token IDs), keeping the target and draft in one model.
147-
- Offline precomputation of target features, streamed to a draft-only training
148-
job (as AutoModel's `precompute_dspark.py` does), which sidesteps holding the
149-
target in memory but adds a cache format.
150-
151-
### HF to/from Megatron conversion
152-
153-
Because the draft reuses the target's embedding and LM head and adds `fc`,
154-
`markov_head`, `confidence_head`, and the draft decoder layers, the bridge mapping
155-
registry must gain `dspark.*` entries (QKV and gated-MLP splits for the draft
156-
backbone, plain linears for `fc` and the heads), so trained drafts round-trip
157-
through `AutoBridge` for serving. The `mtp.*` mappings are the closest existing
158-
template.
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.
159273

160274
## References
161275

0 commit comments

Comments
 (0)