Consolidate sampling-transform scoring on one shared chunked core; implement top-k/top-p/min-p replay for generic ce_modes - #74
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.
…ty branch Absorbs the scoring-path consolidation so the train_serve_profile contract and the Qwen3 dense exact-contract fixes are reviewed and GPU-qualified against the refactored shared loss/sampling-transform core. File sets are disjoint (verified: zero-overlap at 8074259 vs acf0046); the semantic touchpoint is ce_mode=bi_fused routing through the consolidated core, which the re-run K3 parity lanes qualify.
|
Note: this branch's four commits (through 8074259) have been merged into #89 (merge commit 5ff4ec4) so the scoring-path consolidation and the Qwen3 parity + train_serve_profile work are reviewed and GPU-qualified together — the four K3 parity lanes were re-run green (K3=0, bitwise FP32) at the merged head, exercising this PR's consolidated bi_fused scoring core via ce_mode=bi_fused. Leaving this PR open for reference; see #89 for the combined evidence. |
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.