[cuda] make LambdaRank gradient kernel bit-deterministic when deterministic=true#14
[cuda] make LambdaRank gradient kernel bit-deterministic when deterministic=true#14maxwbuckley wants to merge 2 commits into
Conversation
|
Thanks Max — and Claude Code. Real solution to a real problem: I worked through the slot-decomposition argument and it checks out — the Verification: 50/50 bit-identical run-to-run on the parity sweep, kernels agree to within 1e-8 on the same input. Performance tradeoff (12–376% slowdown depending on Two small things before merge:
Once that's in, this is ready to merge. |
|
Quick rebase nudge — #7 and #8 just landed on master and touched files this PR also modifies ( Ready to merge as soon as the conflict's resolved. |
|
Thank you, Max — and thank you, Claude Code (separately 🙂). Friendly rebase nudge 👉
Could you rebase onto current |
83f6164 to
74183fc
Compare
|
Rebased onto current Please re-review — the branch was rebuilt rather than replayed, so it isn't byte-for-byte what you reviewed before. What changed. The old branch carried 23 commits, ~18 of which have since landed on
(10 files → 4, 23 commits → 2.) The LambdaRank determinism change itself is unchanged. Validation (RTX 5090, CUDA 13.2, built from source): full Rebased and validated with Claude Code; the test run above is a real GPU run, not CI (CI has no GPU, so it never exercises these paths). |
…nistic=true
CUDALambdarankNDCG's gradient kernel scatters per-pair gradient and
hessian contributions into shared_lambdas/shared_hessians via
atomicAdd_block. Different runs interleave those atomics in different
orders, and because floating-point addition is non-associative the
per-slot sums (and hence the trees built from them) are not bit-
identical run to run. The "deterministic" config flag was previously a
no-op on CUDA — only the warning fired.
This change makes deterministic=true actually deliver bit-deterministic
LambdaRank output on CUDA. When the flag is set, CUDALambdarankNDCG
dispatches to two new kernels that:
* Compute sum_lambdas via cub::BlockReduce::Sum (fixed reduction tree
based on thread IDs, deterministic across runs).
* Parallelize over output slots. For slot s with rank r in the
descending sort, the only pairs that touch s are (i, r) for i < r
and (r, j) for j > r; the thread iterates these in (i, r)-then-
(r, j) order — mirroring CPU's flat (i, j) iteration order for
slot s's contributions — and accumulates with a score_t (float)
accumulator. No atomics; the accumulation order is fixed by the
loop, not by the hardware.
Verification on the cpu_cuda_parity sweep:
* 50 / 50 cases bit-identical run-to-run with deterministic=true
(lambdarank_500 and lambdarank_1500 used to differ run-to-run by
up to ~6e-8 max|Δ| via atomicAdd_block ordering).
* Existing CUDA(deterministic=false) and the new CUDA(deterministic=
true) match each other within 1e-8 (FP epsilon).
* CPU vs CUDA divergence is unchanged from baseline (the new kernel
inherits the existing differences from sigmoid table vs direct
exp() and from float vs double precision in CPU's per-slot
accumulator vs CUDA's shared-memory atomic).
Performance impact (100 boosting rounds, gpu_use_dp=true, RTX 5090):
* items=50, groups=10 : 0.98s -> 1.10s (+12%)
* items=200, groups=20 : 1.02s -> 1.41s (+38%)
* items=500, groups=10 : 0.99s -> 2.79s (+182%)
* items=1500, groups=4 : 1.05s -> 4.99s (+376%)
Slowdown grows with items_per_query because Phase B is O(items *
(items + truncation_level)) per query, vs the existing kernel's
O(num_pairs / blockDim) per call. For typical workloads with
truncation_level=30 the deterministic path is acceptable as an
opt-in mode.
Adds two parametrized regression tests in test_dual.py covering both
the BitonicArgSort_1024 (items <= 1024) and BitonicArgSort_2048
(items > 1024) code paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2d7c282)
cpplint's whitespace/indent_namespace flagged the continuation indent; move both initializers onto a single line after the `:` (matches the repo's existing multi-init pattern in serial_tree_learner.cpp). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> (cherry picked from commit 83f6164)
74183fc to
d6a14c5
Compare
|
Verdict: mergeable as-is pending gates. Applies cleanly to current master (the rank objective files were untouched by last night's 27 commits). The key acceptance criterion for us: zero cost when Design review:
Nits:
CI: the regular/Windows failures look inherited from the md5 locks: unaffected (default Gate runs tomorrow: both new determinism tests (50 and 1500 items/query) x3 runs, plus a timing sanity check that 🤖 Review drafted with Claude Code |
Summary
CUDALambdarankNDCG's gradient kernel scatters per-pair gradient/hessian contributions into shared memory viaatomicAdd_block. Different runs interleave those atomics in different orders, and FP add is non-associative, so the per-slot sums (and the trees built from them) are not bit-identical run-to-run. Today thedeterministicconfig flag is a no-op on CUDA — only the runtime warning fires.This PR makes
deterministic=trueactually deliver bit-deterministic LambdaRank training on CUDA. The flag dispatches to two new kernels that:sum_lambdasviacub::BlockReduce::Sum(fixed reduction tree, deterministic across runs).swith rankrin the descending sort, only pairs(i, r)fori < rand(r, j)forj > rtouchs; the thread iterates these in(i, r)-then-(r, j)order — mirroring CPU's flat(i, j)iteration for slots's contributions — and accumulates into ascore_t(float) variable. No atomics; the accumulation order is fixed by the loop, not by the hardware.Verification
Run-to-run determinism on the existing 50-case parity sweep, with
deterministic=True:lambdarank_500,lambdarank_1500at ~6e-8 max|Δ|)The deterministic and existing non-deterministic kernels agree to within 1e-8 on the same input, so existing CPU/CUDA divergence is unchanged.
Performance impact (RTX 5090, 100 rounds, gpu_use_dp=true)
Slowdown grows with
items_per_querybecause Phase B isO(items × (items + truncation_level))per query. Defaulttruncation_level=30keeps the per-slot inner loops small enough that this is acceptable as an opt-in mode.Test plan
test_dual.pycovering both theBitonicArgSort_1024(items ≤ 1024) andBitonicArgSort_2048(1024 < items ≤ 2048) code paths.lambdarank_500/lambdarank_1500: divergence vs CPU unchanged from the existing non-deterministic kernel (within 1e-8).(i, r)/(r, j)slot decomposition matches CPU's flat(i, j)iteration order for slots's contributions.🤖 Generated with Claude Code