Repository navigation
feat(training): add DSpark draft heads and loss (algorithm core) - #5234
Conversation
|
Added a multi-GPU functional test for the one piece of this PR that cannot be validated on a single process: the loss's
This test needs a 2-GPU node and is not part of the CPU unit lane. I do not have permission to trigger GPU CI on this repo, so it needs a maintainer to run it (or The remaining validation needs the multi-node cluster and is the Phase 1b/2 follow-up from the design (#5233), not part of this PR: the parallel-aware draft backbone (the block-attention module that reads the frozen target's hidden states as context K/V, with TP/PP sharding), the |
Design proposal (RFC) for adding DSpark speculative-decoding draft training to Megatron-Bridge, tracking NVIDIA-NeMo#5230. It describes the DSpark draft (a semi-autoregressive parallel backbone plus Markov and confidence heads), its three-term CE / TV / confidence training objective and acceptance metric, and how it maps onto the existing ModelOpt distillation subsystem pattern (provider hook, custom forward step, loss, thin entry, HF weight mapping). It includes a phased implementation plan and references the NeMo AutoModel reference implementation. Signed-off-by: khazic <khazzz1c@gmail.com>
Requested in review: the Megatron-Bridge design doc does not need a breakdown of the AutoModel implementation; the pointer in the overview is enough. Signed-off-by: khazic <khazzz1c@gmail.com>
The design page carried a four-phase implementation plan and an open-questions list. Neither describes what this PR (or the repo) actually contains: they promise work that is not scheduled and has no owner, so they go stale the moment priorities move. The sequencing and the open decisions belong in the tracking issue (NVIDIA-NeMo#5230), which is where they can be updated. The remaining page is the design itself: the algorithm, where it fits in Megatron-Bridge, and the proposed file surface. The frozen-target supervision section keeps both capture options, since the trade-off between them is design content rather than a schedule. Signed-off-by: khazic <khazzz1c@gmail.com>
c57847e to
63ad70f
Compare
yaoyu-33
left a comment
There was a problem hiding this comment.
Architecture/correctness disposition: changes requested at exact head 63ad70f3.
I independently checked the head against the DSpark paper, DeepSpec, NeMo AutoModel, current Bridge/MCore training contracts, issue #5230, RFC #5233, and the stacked #5241-#5244 changes. The head/teacher-forcing shapes, Markov and RNN recurrence, target detachment, 1 - 0.5 * L1 acceptance label, prefix-product tau + 1, explicit DP-group requirement, and single-call DP gradient algebra are sound. All 16 submitted CPU unit cases passed, and I ran the submitted test with exactly two GPUs: its NCCL gradient-equivalence assertion passed.
The blockers are the inline items below plus the unresolved subsystem ownership in #5233. This PR installs public, trainable modules with no provider/model/forward-step/recipe caller, so the actual causality mask, confidence input [h_k; embed(x_{k-1})], frozen/shared parameter ownership, TP/PP/CP placement, checkpoint schema, and MCore loss return contract cannot be verified here. Those are not separable details: the current per-call normalization is incompatible with uneven microbatches under MCore, and the full-vocabulary FP32 probability path is not viable at reference-scale anchor counts.
For the stack: #5241 should not be folded in as written—the primary objective trains raw weighted L1 and uses one-half only for the acceptance label. The mask/sentinel hardening in #5242/#5244 is valid if this standalone core is retained. #5243's missing-group policy should follow the approved trainer/process-group contract rather than precede it.
I am not issuing /ok to test yet: the GPU test is not wired into an active CI launch script, and the architecture/loss contract needs revision first. Please settle the owner/API boundary in #5233, then either move these modules to that owner or land them together with the smallest real Bridge integration that makes the distributed and checkpoint contracts testable.
| outputs: The draft forward outputs; see :class:`DSparkForwardOutput`. All | ||
| tensors are ``[batch, num_blocks, block_size(, vocab)]``. | ||
| ce_alpha: Cross-entropy weight. | ||
| l1_alpha: Total-variation (``0.5 * L1``) weight. |
There was a problem hiding this comment.
The implementation below accumulates raw L1, not 0.5 * L1, and the implementation is the part that matches the primary algorithm. Paper Eq. 10 defines the training term as sum_k w_k ||p_d - p_t||_1; DeepSpec and NeMo AutoModel do the same. The factor 0.5 appears in Eq. 8 only when forming the acceptance/confidence label. With the published coefficients, changing this to half-L1 (as #5241 proposes) halves the dominant training term and changes the recipe. Please keep the raw-L1 computation, correct this docstring and the module/design text, and add a disjoint-distribution regression expecting l1_loss == 2 and accept_rate == 0 so the two contracts cannot drift again.
| # Divide by the (optionally DP-reduced) denominators; the backward gradient is | ||
| # scaled by world_size so the DP-averaged gradient matches the global loss. | ||
| world_size = dist.get_world_size(dp_group) if dp_group is not None else 1 | ||
| ce_g, l1_g, conf_g = (_reduce_den(d, dp_group) for d in (ce_den, l1_den, conf_den)) |
There was a problem hiding this comment.
This denominator collective only spans DP ranks for one loss invocation; it does not avoid the claimed per-microbatch token-count bias. MCore invokes the loss function once per microbatch. Its 3-tuple path accumulates an unnormalized numerator plus num_tokens and scales gradients once by the global token total; its legacy 2-tuple path averages the already-returned losses across microbatches. Returning a normalized value here therefore computes an average of microbatch means when masks differ. A focused 1-token/3-token reproduction gives 1.1269 here versus the correct global token mean 0.6269; returning a token count later would instead double-normalize. Please define this together with the real MCore forward/loss return contract, return reducible numerator/denominator reporting values, and test uneven masks across both DP ranks and at least two microbatches. The current 2-GPU test validates only the single-call PyTorch-DDP algebra.
| Returns: | ||
| Per-position L1 distance ``[...]`` (twice the total-variation distance). | ||
| """ | ||
| draft_probs = torch.softmax(draft_logits.float(), dim=-1) |
There was a problem hiding this comment.
This materializes two full FP32 [B, N, L, V] probability tensors and then a same-sized subtraction/absolute-value temporary. At a reference-scale shape such as B=1, N=512, L=7, V≈150k, each FP32 tensor is about 2 GiB, so this helper alone needs over 6 GiB of forward temporaries before autograd storage. The tiny-vocabulary unit tests cannot expose that. The current NeMo AutoModel implementation chunks probability tokens (and checkpoints the chunks) for this reason. Please use a bounded chunked accumulation or establish and enforce a much smaller anchor-count contract with memory evidence before exposing this as a training core.
|
|
||
|
|
||
| @pytest.mark.skipif(torch.cuda.device_count() < 2, reason="DSpark loss DP-gradient test needs 2 GPUs") | ||
| def test_dspark_loss_dp_gradient_matches_single_process(): |
There was a problem hiding this comment.
I ran this exact test with two GPUs and it passes, but CI will not: no active H100/GB200 launch script references this file. Functional tests in test_groups/ are selected by the launch scripts, not discovered repo-wide. Also, because this test already calls mp.spawn(..., nprocs=2), it must be launched by a single pytest coordinator with CUDA_VISIBLE_DEVICES=0,1; placing it under an existing two-rank torch.distributed.run launcher would create four workers and collide on devices/ports. Please add a dedicated active launch entry (at most two GPUs) using the prescribed uv run python -m pytest command.
…racts 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>
First increment of DSpark speculative-decoding draft training (NVIDIA-NeMo#5230; design in NVIDIA-NeMo#5233): the architecture-agnostic, pure-torch core. Adds src/megatron/bridge/training/post_training/dspark/: * heads.py: the sequential head (VanillaMarkov / GatedMarkovHead / RNNHead) that injects intra-block token dependency onto the parallel-backbone logits, plus the confidence head. * loss.py: the three-term CE + total-variation + confidence-BCE objective with per-position decay, and the analytical acceptance metrics (accept_rate = 1 - TV, tau = sum cumprod(accept) + 1). The data-parallel denominator reduction is an injectable dp_group, so the same math wires to a Megatron-Core forward step later. CPU unit tests cover the head math and the loss / acceptance math. The parallel-aware draft backbone and the forward-step / provider integration (which need Megatron-Core and multi-GPU validation) are follow-ups. Signed-off-by: khazic <khazzz1c@gmail.com>
The DSpark loss denominator reduction now runs only when a dp_group is passed; dp_group=None is local-only. Previously it fell back to the default process group whenever torch.distributed was initialized, whose members are not the DP replicas under Megatron-Core parallelism (TP/PP/CP), so the caller must pass the data-parallel group explicitly. Add a 2-GPU functional test (tests/functional_tests/test_groups/training/test_dspark_loss_dp.py) that spawns two NCCL ranks and asserts the DP-averaged gradient through a shared head matches the single-process full-batch gradient. This validates the dp_group reduction, which cannot be exercised on a single process, and needs a GPU node to run. Signed-off-by: khazic <khazzz1c@gmail.com>
Return the unnormalized micro-batch numerator plus num_tokens and reducible [numerator, denominator] reporting pairs, instead of dividing by a locally reduced denominator. Megatron-Core calls the loss once per micro-batch, so a locally normalized value accumulates a mean of micro-batch means and biases the objective whenever micro-batches carry different supervised-token counts. The unbiased global mean needs calculate_per_token_loss=True, which assert_per_token_loss_config now enforces at the integration point. Chunk the FP32 probability L1 so the softmax temporaries are bounded by chunk_tokens * vocab rather than the full [batch, num_blocks, block_size, vocab] tensor, recomputing each chunk in the backward. Three such tensors at reference scale are several GiB of forward temporaries before autograd storage. Correct the objective's documentation: 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. The implementation already did this; the docstring and the design page did not. A disjoint-distribution regression now asserts l1_loss == 2 with accept_rate == 0 so the two cannot drift apart again. Rewrite the 2-GPU functional test around the new contract: two ranks, two micro-batches each, uneven supervised-token counts, asserting both that the global-token gradient matches the single-process reference and that the per-micro-batch normalization does not. Add a dedicated L0 launch script so CI actually selects the test, using a single pytest coordinator because the test self-spawns its ranks. Signed-off-by: khazic <khazzz1c@gmail.com>
63ad70f to
c8cf0bc
Compare
|
Thanks @yaoyu-33, this is addressed in c8cf0bc. I checked each item against the sources you cited rather than taking them on faith, and all four held up. Numbers below are from a real 2-GPU run, not estimates. 1. Objective scaling. You are right, and the direction was the docstring's, not the implementation's. AutoModel's 2. Loss return contract. Rewritten to the Megatron-Core 3-tuple: Tracing it through confirmed your point and sharpened it: the 3-tuple alone is not sufficient. At Measured on 2 ranks x 2 micro-batches with uneven supervised-token counts ( A 37% gradient error, so the bias is well clear of noise. The test asserts both directions: the global-token path matches, and the per-micro-batch path must not, so it cannot silently stop discriminating. It also asserts the micro-batch token counts are actually unequal, since with equal counts the two normalizations coincide and the test would prove nothing. 3. Memory. Confirmed and larger than the estimate. Chunking the L1 alone was not enough: Identical values for both terms, and a unit test pins that chunk size changes neither the values nor the gradients. Chunk size matches the AutoModel reference's 128. 4. CI launch. You were right that no active script referenced the file, and right about the four-worker collision. Rather than keep the bespoke launch mode, I dropped On the stack. #5241 is closed. #5242 and #5244 are still valid but were stacked on it, so they carry the bad commit as their base and need rebasing onto this branch; I will do that once this one settles. #5243 is obsolete: On scope. Your point that this installs public modules with no caller stands, and the RFC now settles the owner question that gates it (Megatron-Core native, with the reasoning and the consequences written down). Say the word if you would rather these modules move to Megatron-Core now, or land here together with a minimal forward-step and recipe, and I will restructure accordingly. How this was validated. The GPU runs above loaded |
|
/ok to test f376431 |
|
/ok to test 91fac4a |
…string The docs are built with MyST, where a line of dashes under text is a setext H2. The generated apidocs page for this module therefore started at H2 and tripped "Document headings start at H2, not H1 [myst.header]", which fails the docs build because it treats warnings as errors. Use bold text instead; this was the only RST underline under src/megatron/bridge. Signed-off-by: khazic <khazzz1c@gmail.com>
|
/ok to test 1f2ae35 |
|
/ok to test c2878d0 |
|
This one is approved with all 96 checks green and shows as mergeable, but it carries both Is there specific coverage still missing? Happy to add it if so. Otherwise the |
What
First implementation increment of DSpark speculative-decoding draft training
(feature #5230, design PR #5233): the architecture-agnostic, pure-
torchcore. Opened as a draft because it is not yet wired to a trainer (see Scope).
Adds
src/megatron/bridge/training/post_training/dspark/:heads.py: the DSpark sequential head that injects intra-block tokendependency onto the parallel backbone's logits, as
P(x | x0) = prod_k p_k(x_k | x0, x_1..x_{k-1}). Three variants:VanillaMarkov(low-rank token-to-token additive bias),GatedMarkovHead(gates the previous-token embedding by the backbone hidden), and
RNNHead(carries recurrent state across the block). Plus the
ConfidenceHead.loss.py: the three-term objective, position-weightedexp(-k/gamma):cross-entropy (
0.1) + total-variation0.5*||p_draft - p_target||_1(0.9)1.0); and the analytical acceptance metricsaccept_rate = 1 - TVandtau = sum cumprod(accept_rate) + 1. Thedata-parallel denominator reduction is an injectable
dp_group, so the samemath wires to a Megatron-Core forward step (and its DP group) in a follow-up.
Follows the DSpark paper (arXiv:2607.05147)
and the NeMo AutoModel reference implementation
(
nemo_automodel/components/speculative/dspark/).Scope (why draft)
This PR is deliberately limited to the pieces that are pure tensor math and
CPU-testable, so they can be reviewed on their own. The parts that need
Megatron-Core and multi-GPU validation are intentionally not in this PR:
frozen target's hidden states as context K/V), as an MCore-native or
ModelOpt-injected module;
forward_step_dspark+ provider hook +dspark.*HF weight mappings, and arecipe, following the ModelOpt distillation subsystem.
Tests
tests/unit_tests/training/post_training/test_dspark_heads.pyandtest_dspark_loss.py(CPU): the Markov/RNN/confidence head math and shapes, andthe loss/acceptance math (
accept_rate = 1 - TV,tau = block_size + 1when thedraft matches the target, three-term combine, backward, masking, error paths).
Validation status
ruffclean. The core is puretorch, and its math was verified end-to-endlocally (all head and loss/acceptance assertions pass). It has no Megatron-Core
dependency, so the unit tests run in the CPU lane. The backbone and integration
follow-ups still require multi-GPU validation.