Repository navigation
Keep the gA gradient on the GPU in the timestep backward - #56
Merged
Merged
Conversation
Add a thread-local counter in src/sparse/mod.rs. `primitive_to_vec` increments it. `host_reads_on_this_thread()` reads it. It costs one `Cell` increment per host copy and changes no result. Add Part E to tests/cuda_backward_parity.rs: - E1 `cusparse_backward_keeps_ga_on_the_device` counts the host copies in one timestep backward with `sparse_solver: cuda`. It expects 0. A control run with the host solver must show copies, so the counter is not blind. E1 fails on the current code: 2 copies per timestep op. - E2 `cusparse_ga_matches_cpu_on_a_confluence` compares the gradients of CUDA + cuSPARSE with the CPU backend on a network with two confluences. Some rows there have 2 off-diagonal entries, so `row_for_nnz` repeats. E2 passes before and after the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`timestep_backward_core` copied `gb_rhs` and `x_sol` to the host, looped over nnz to build gA, and uploaded the result. With `sparse_solver: cuda` that was 2 device-to-host copies, 1 upload and a stream sync per timestep op, in every backward (all timestep op variants share this function). Call `dispatch::grada_primitive` instead. `CsrSolveOp::backward` already uses it. On CUDA it gathers, multiplies and negates on the device. On the CPU it runs the same host loop as before, in the same order, so CPU results do not change. E1 (`cusparse_backward_keeps_ga_on_the_device`) now passes: 0 host copies in the backward. It made 2 before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Swapping the row and col indices in `cusparse_grada` fails E2 and `cusparse_solver_path_carries_the_same_gradients`. Indexing `gradb` by `min(row, col + 1)` equals `row` on a chain, and only E2 fails. Both edits were reverted. No code change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename `host_reads_on_this_thread` to `host_reads_on_this_thread_for_test`, as the other `#[doc(hidden)]` test hooks in ddrs (`content_key_for_test`, `run_smoke_for_test`). Its doc now says that it sees only `primitive_to_vec`, not other host reads such as `Tensor::into_data`. The E1 doc now says why that is enough: on the timestep backward path, `primitive_to_vec` is the only host-read helper. The cuSPARSE solve, SpMV and assemble kernels read no tensor to the host. No code behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… build need The timestep backward keeps gA on the device with sparse_solver: cuda. host_reads_on_this_thread_for_test counts the host copies (tests/cuda_backward_parity.rs Part E). On a host with CUDA 13.3, cudarc 0.19.7 needs CUDARC_CUDA_VERSION=13020 at build time. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
taddyb
marked this pull request as ready for review
October 18, 2026 17:44
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.
Summary
On CUDA, the backward of every timestep op copied
gb_rhsandx_solto the host intimestep_backward_core, looped over nnz to build gA, and uploaded the result.It now calls
dispatch::grada_primitive, asCsrSolveOp::backwardalready does.With
sparse_solver: cuda, gA is gathered, multiplied and negated on the device.Why
With
sparse_solver: cuda, each timestep op in the backward did 2 device-to-host copies,1 upload and a stream sync for gA. All timestep op variants share this function
(
TimestepOp, gamma, release, leakance). No issue or PR covered it.CPU path
The CPU branch of
grada_primitiveis the same host loop:-gradb_host[r] * x_host[c], in thesame order, with the same 2 host reads. CPU results do not change. Checked three ways:
It covered a chain and a confluence network,
enforce_positivityfalse and true, and all 4parents (16 gradient vectors). The output on master (65211a9) and on this branch is identical
byte for byte.
compare_ddr_sandboxprints ABSOLUTE MATCH, with the same numbers as master(max abs diff 1.525879e-4 m3/s, max rel diff 2.412143e-6).
cargo test --features fixtures: 1017 passed, 2 failed, 26 ignored over 121 test binaries.The lib unit tests (490),
mmc,sparse_gradcheck,sp8_gradcheck,leakance_gradcheck,leakance_off_parity,leakance_reference_match,reservoir_override,zeta_accumandddr_sandbox_matchall pass.juniata_acceptance(release) passes 3 of 3.The 2 failures also fail on master with the same numbers (see "Known failures on master").
Tests
Part E of
tests/cuda_backward_parity.rs(needs--features cuda):src/sparse/mod.rs.primitive_to_vecincrements it.host_reads_on_this_thread_for_test()reads it. It is a#[doc(hidden)]test hook, likethe other
*_for_testfunctions. It costs oneCellincrement per host copy.primitive_to_vecis the only host-read helper on the timestep backward path.cusparse_backward_keeps_ga_on_the_device: 0 host copies in the backward withsparse_solver: cuda. On master it fails with 2 copies. A control run withsparse_solver: cpuon the same device shows 4 copies, so the counter sees the backward.cusparse_ga_matches_cpu_on_a_confluence: a network with two confluences (rows with 2off-diagonal entries, so
row_for_nnzrepeats). Gradients of all 4 parents, CUDA + cuSPARSEagainst the CPU backend, with and without
enforce_positivity. Worst relative difference 3.2e-7.E2 passes on master too. It pins the new device gather.
Mutation checks, done by hand and reverted: swapping the row and col indices in
cusparse_gradafails E2 andcusparse_solver_path_carries_the_same_gradients. Indexinggradbbymin(row, col + 1)(equal torowon a chain) fails only E2.Local CUDA results (RTX 4080 SUPER, CUDA 13.3 driver, built with
CUDARC_CUDA_VERSION=13020):cuda_backward_parity: 17 of 18 pass. This includes Parts A and B, E1, E2 andcusparse_solver_path_carries_the_same_gradients. The cuSPARSE and host-solve gradients arestill bit-identical on the chain fixture (worst relative difference 0 for all 4 parents).
The worst cross-backend difference in Part B is 2.784e-7, the value in the test comments.
sparse_cusparse_v54 of 4 (1 more is ignored, see below),sparse_cusparse_v83 of 3(ignored tests, run with
--include-ignored) anddevice_selection1 of 1 pass.sp10_k1_bitmatchandsp10_k23_bitmatchhave only ignored tests; they were not run.Known failures on master (not touched here)
These fail on master (65211a9) in the same way. Each was run on a separate master build.
cuda_backward_parity::enforce_positivity_can_never_reach_the_cuda_graph_path(Part D).It expects the
enforce_positivity/ddr_matcherror text, but theuse_cuda_graphscheckfires first. CI does not build with
--features cuda, so it went unseen.training_step_layer_b::layer_b_step3_mc_forward_matches_ddr(MC forward max abs diff 1.01e-1)and
training_step_layer_c::layer_c_step2_gradients_match_ddr(grad_output_weight 2.03e-4 overthe 1e-4 tolerance). Same numbers on master.
sparse_cusparse_v5::end_to_end_smoke_cuda_trainandsparse_cusparse_v6::v6_cuda_is_faster_than_cpu_on_smoke_train(both ignored). They run thetrainbinary with/tmp/*.yamlfiles that do not exist.Timing
One timestep op,
sparse_solver: cuda, release build, RTX 4080 SUPER. Wide shallow networks(n/10 parallel lanes). Median time of
backward()plus reading the gradient, 10 runs after3 warm-up runs, two rounds each. The machine was shared, so treat the numbers as rough.
The timing test is not in this PR.
Not done
cusparse_gradastill uploadsrow_for_nnzandcolon each call. That is cheap next to thehost round trip it replaces. Follow-up: keep these index tensors in
CudaPatternCache.CI covers only the CPU part. The CUDA results above are local.
🤖 Generated with Claude Code