Consolidate sampling-transform scoring on one shared chunked core; implement top-k/top-p/min-p replay for generic ce_modes - #74
Draft
qywu wants to merge 4 commits into
Draft
Conversation
Addresses the scoring-path half of #71 (and #60): - Promote xorl.ops.exact_sampling_transforms to the documented backend-independent replay contract: the module docstring now pins the tie-break rule, the inclusive top-p crossing, min-p against the original row maximum, and the top_p == 1.0 fp32-overshoot identity, and states explicitly that stock SGLang/FlashInfer filters make no such guarantees. - Deduplicate the per-row temperature validation/normalization (5 copies) and the selected-logprob reference-VJP trio (2 copies) into that module; the GLM-5.2 and DSV4 exact heads now import them. - Add xorl.ops.loss.sampling_transform_ce: one chunked projection -> temperature -> support -> selected-logprob + VJP core with an injected logits policy and optional vocabulary-parallel body TP. - Implement top-k/top-p/min-p and per-row temperature replay for every generic ce_mode (eager, compiled, quack_linear, fused_quack), with and without ordinary body TP, honoring lm_head_fp32. The five scattered NotImplementedError sites in per_token_ce are gone; the one remaining rejection (FP8 lm_head module) raises a single actionable message. - Route the bi_fused temperature/filter paths through the shared core with the batch-invariant kernels injected, deleting the duplicated chunk loops and the filtered backward; the no-transform fast path is untouched. - Drop the dead legacy-call shims from the exact-head autograd functions. A replayed token outside its row's current support scores -inf logprob (+inf CE) with zero gradient; transform-carrying requests are never silently scored against unfiltered support.
Broly Security ScanNote ✅ Clean scan Note Re-scan this PR anytime with
|
… stale caller - Restore the single-pass vocabulary-chunked backward for unfiltered (temperature-only) calls in the shared core: each weight-gradient slice is written exactly once, full-row logits never rematerialize, and under TP no gather is needed in backward. This keeps the pre-refactor backward cost for the common rollout-temperature bi_fused configuration. - Accumulate the filtered branch's weight gradient with addmm(out=) instead of allocating a full [V, H] FP32 temporary per row chunk. - Add validate_sampling_transform_rows to the program module: one all-or-none + row-alignment/dtype/device check consumed by the core and both bi_fused entries; over-length metadata now raises instead of silently scoring tokens against the wrong row's filter (regression test added). - Clamp _plain_selected_score to <= 0, matching every sibling selected-logprob implementation's one-ulp handling. - Fix the stale 7-arg _Glm52ExactDistributedTP16LmHeadFunction.apply call in the distributed collectives test that the legacy-shim removal missed. - Use normalize_temperature_rows in the per_token_ce routing block and document the scalar (pre-GEMM) vs per-row-tensor (post-GEMM) temperature convention split.
Second consolidation tranche for #71: the two exact heads ran the same program skeleton in near-duplicate. New xorl/models/transformers/exact_lm_head_shared.py owns the skeleton; each family keeps only its contract-bearing pieces (base projection kernel, temperature dtype boundary, native selected-score kernel, surrogate formulation, geometry validation). - ONE autograd boundary (ExactLmHeadFunction) replaces the three per-family Functions. Row ownership is an injected ExactHeadRowPlan: replicated (GLM's local head), equal rank-order blocks (GLM TP16), or ragged padded blocks (DSV4 TP8). Row-gather closures stay family-side so their monkeypatch points and collectives semantics are unchanged. - ONE filtered surrogate-VJP chunk loop and ONE unfiltered reference-grad plumbing, parameterized by exact-score/reference-logits closures. - Shared LoRA A/B SGEMM choreography (exact_lora_local_logits) and single-adapter LoRABatchInfo builder; families keep their pinned base GEMM. - Shared rank-order collectives (vocab gather with dtype/world parameters, equal and ragged row gathers, row counts, fp32 sum) and one TP-group geometry checker with per-family program names, preserving the pinned error substrings. - Both filtered forwards now score through score_with_sampling_transforms instead of hand-rolled support/identity/partitioned sequences. - gather_vocab_shards moves from the loss module to xorl.utils.dist_utils (review placement finding on #74). Net -575 lines. Forward value paths are byte-preserved: the same kernels run in the same order with the same dtype boundaries; only the plumbing moved.
Review findings on the dedup commit: - The shared forward's FP32 selected-logprob assertion was lifted from the GLM-only functions and rejected DSV4's value program, which is BF16 end-to-end (BF16 vocabulary gather, BF16 temperature store, batch-invariant BF16 log_softmax) — the DSV4 exact head raised on its first real forward, masked in CI by FP32-returning fake components. The expected dtype is now part of the family contract (component.logprob_dtype: FP32 for GLM-5.2, BF16 for DSV4), the fakes pin the real BF16 contract, and an undeclared-dtype mismatch test covers the gate. - The shared backward revalidates the TP group before its first collective, restoring the deterministic contract error the removed per-family backwards produced when the group was rebound or destroyed between forward and backward. - require_equal_nonzero_row_count reuses rank_order_row_counts instead of re-implementing the count exchange. - ExactHeadRowPlan documents the no-tensor-capture contract for closures stored on the autograd context.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the scoring-path half of #71 (points 1, 2, 3, 5 of the proposed direction) and closes the #60 gap: a generic (non-exact-family) model can now train on rollouts sampled with
top_k/top_p/min_pand per-row temperature, getting logprobs on the filtered support, under ordinary body TP.Refined plan (updating the issue's direction)
The issue's five points remain the plan; this PR deliberately takes the scoring path and leaves point 4 (capability negotiation across the 78
_exact_contract_familybranch sites — PP byte contract, weight sync, checkpointing) to a follow-up PR series. Two inputs for that follow-up surfaced while inventorying the branch surface:_qwen3_dense_exact_contractnever becomes an_exact_contract_familyvalue, so under PP an exact dense Qwen3 model does not get_pp_exact_boundary_contract(only DSV4 is explicitly re-added inengage_pp_byte_contract), even though it is pinned tobi_fusedand BI trunk linears._glm52_exact_tp16_lm_head,_dsv4_exact_tp8_lm_head), not the family — this PR leans into that: once the generic path is capability-complete for transform replay, the loss layer no longer needs lane membership at all.What this PR does
1. One documented sampling-transform program (issue point 1).
xorl/ops/exact_sampling_transforms.pyis now declared the XoRL-wide replay contract for every lane, not an exact-lane-only artifact. The docstring pins: temperature-first at the lane's dtype boundary, stable token-ID tie-break, inclusive top-p crossing, min-p against the original row maximum, andtop_p == 1.0as the exact identity despite fp32 cumulative overshoot (the question raised on #60; already tested attests/ops/test_exact_sampling_transforms.py::test_identity_top_p_one_is_full_support_despite_fp32_cumulative_overshoot). It also states explicitly that stock SGLang/FlashInfer filters do not guarantee these rules, so exact replay requires a sampler that implements this program.2. One shared chunked scoring core (issue point 2). New
xorl/ops/loss/sampling_transform_ce.py: chunked projection → temperature → pinned support program → selected logprob, with a recompute closed-form VJP, saving only per-row scalars (never a[tokens, vocab]activation). The genuinely lane-specific piece — how chunk logits are computed — is an injected policy:lm_head_fp32);bi_fusedinjects the batch-invariant kernels (head_v2_full_logits_with_lse/bi_lm_head_full_logits+exact_temperature_scale_fp32_logits), so its forward values keep matching serving bytes; its duplicated chunk loops and filtered backward are deleted (bi_fused_lm_head.py−362/+94 net).Ordinary vocabulary-parallel body TP is handled inside the core: chunk-local logits are gathered in rank order, hidden gradients are summed across vocab ranks, weight-shard gradients stay local.
3. Transforms implemented on the generic modes (issue point 3).
per_token_ce.py:274/:337(NotImplementedError: top-k/top-p/min-p replay is supported only by exact LM-head modes) and the three per-row-temperature rejections are gone. All ofeager/compiled/quack_linear/fused_quacknow score transform-carrying requests through the shared core, with and without body TP.4. One loud rejection in one place (issue point 5). The single remaining unsupported combination — transforms through an FP8
lm_headmodule — raises one actionable message (SAMPLING_TRANSFORM_LM_HEAD_MODULE_ERROR) naming the fix (lm_head_fp32: true). A replayed token outside its row's current support scores-inflogprob (+infCE) with zero gradient — no path silently scores against unfiltered support.Acceptance criteria status
tests/distributed/test_vocab_parallel_ce.py::check_sampling_transform_replay— CE and both grads bit-match the dense reference at TP2)bi_fusedconsumes it directly, GLM-5.2/DSV4 consume the shared program + helpers (their kernel-exact QLoRA VJPs remain theirs by design)+infCE, zero grad, tested)_exact_contract_family— follow-up (point 4)Not in this PR (follow-ups)
_qwen3_dense_exact_contractPP hole above.model_runner._pp_sampling_transform_kwargskeeps its loud error; the per-token metadata queue that temperature uses needs the same treatment for the filter fields)._BiFusedVocabParallelPerTokenCE) keeps its own row-owner broadcast structure; it now shares the gather + scoring helpers but not the core loop.Self-review pass (second commit)
An adversarial review of the diff surfaced and the second commit fixes: a stale 7-arg autograd call in
tests/distributed/test_glm52_exact_lm_head_qlora_collectives.pymissed by the legacy-shim removal; a validation gap where over-length transform metadata was silently truncated (nowvalidate_sampling_transform_rows, shared by the core and bothbi_fusedentries, with a regression test); a backward perf regression for the common temperature-onlybi_fusedconfiguration (restored as a single-pass vocabulary-chunked backward in the core — write-once weight-gradient slices, no full-row logits, no TP gather in backward);addmm(out=)accumulation in the filtered backward instead of a per-chunk[V, H]fp32 temporary; the missing<= 0clamp in_plain_selected_score; and the scalar (pre-GEMM) vs per-row-tensor (post-GEMM) temperature convention split is now documented.Known perf headroom deliberately left for later: the filtered TP path gathers full-vocab logits per row chunk in forward and backward and re-sorts on every rank (this path previously did not exist at all — correctness first), and
gather_vocab_shardscould move toxorl.utils.dist_utilsalongside unifying the glm5/dsv4 private rank-order gathers.Third commit: exact-head deduplication (formerly #75, folded in)
The GLM-5.2 and DSV4 exact heads ran the same program skeleton in near-duplicate. New
xorl/models/transformers/exact_lm_head_shared.pyowns the skeleton — oneExactLmHeadFunctionautograd boundary (row ownership injected as anExactHeadRowPlan: replicated / equal TP16 blocks / ragged TP8 blocks), one filtered + one unfiltered surrogate-VJP plumbing loop, the serving LoRA A/B SGEMM choreography, the rank-order collectives, and one TP-group geometry checker. Each family keeps only its contract-bearing pieces: base projection kernel (head_v2FP32 vsF.linearBF16), temperature dtype boundary, native selected-score kernel, surrogate VJP formulation, and geometry validation. Forward value paths are byte-preserved — the same kernels run in the same order with the same dtype boundaries.gather_vocab_shardsmoved toxorl.utils.dist_utils. Net −575 lines (glm5 head 1298→847, dsv4 head 836→612, +369 shared).Fourth commit: review fixes on the dedup
An adversarial review of the dedup commit found (and the fourth commit fixes) a real bug it introduced: the shared autograd boundary inherited GLM's FP32 selected-logprob assertion, while DSV4's pinned value program is BF16 end-to-end — the DSV4 exact head raised on its first real forward, masked in CI by FP32-returning fake components. The expected dtype is now part of the family contract (
component.logprob_dtype), the DSV4 fakes pin the real BF16 contract, and a mismatch test covers the gate. Also fixed: the shared backward revalidates the TP group before its first collective (deterministic contract error instead of an NCCL hang on a rebound/destroyed group),require_equal_nonzero_row_countreusesrank_order_row_counts, andExactHeadRowPlandocuments its no-tensor-capture contract. Noted, not changed: DSV4's collapsed legacy error strings ("DSV4 exact lm-head TP8 rank/order mismatch") are replaced by the checker's more specific per-condition messages; the pad-to-max row-gather choreography still has siblings invocab_parallel_reverse_kl/opd_loss/model_runner(follow-up); the ~7 per-row gathers per scored token predate this PR and can now be batched in one place.Testing
tests/ops/loss/test_sampling_transform_ce.py(12 tests, CPU): filtered-support parity with the dense program reference across all four generic modes, gradient parity with full autograd, out-of-support ⇒+inf/zero-grad, per-row temperature vs scaled CE,lm_head_fp32convention, identity metadata collapses to the untouched fast path (bit-equal), FP8-module rejection.tests/distributed/test_vocab_parallel_ce.pygains the body-TP transform-replay check (2×H100: CE/grad_h/grad_w max err 0.0 vs dense reference).tests/ops/test_exact_sampling_transforms.py(14),tests/ops/loss/+ exact-head + wiring tests (77 total), GPUtests/ops/test_bi_fused_lm_head.py(12),tests/distributed/test_bi_fused_lm_head_tp.py,test_fused_linear_logprob_tp.py, and the model-runner/trainer transform-relay tests (66).References are against
main@ e12b12b.