Skip to content

cubecl-cuda: optimizer.step time climbs monotonically when a network is freshly allocated each training iteration #1406

Description

@hypercat

Summary

On the cuda backend, if a training loop reallocates a network + its Adam optimizer on every outer iteration, Optimizer::step gets progressively slower across iterations, and the slowdown spills over to .step() calls on other, persistent networks in the same process. The same code on wgpu (DX12 / Vulkan) is flat.

Environment

  • cubecl 0.10, Burn 0.21
  • GPU: NVIDIA RTX PRO 6000 Blackwell Server Edition (sm_120)
  • CUDA 12.8 toolkit + matching driver (RunPod pytorch:2.4.0-py3.11-cuda12.8 image)
  • rustc 1.92, --release
  • Backend: burn::backend::Autodiff<burn_cuda::Cuda<f32>>

Reproducer

Minimal example code for reproduction: https://github.com/hypercat/cubecl-cuda-reinit-repro

Two modes over the same persistent network + Adam optimizer + inner loop:

  • --mode flat: only the persistent network is trained across iters.
  • --mode climb: same persistent network + inner loop, PLUS a second network + fresh Adam optimizer freshly allocated inside each outer iter, trained for the same inner loop, then dropped.

Each stdout line: iter=<N> st=<X>ms, where st is the total wall-clock of the persistent network's Optimizer::step calls in iter N. The persistent network's state and op sequence are byte-identical between the two modes. Only the ephemeral net's per-iter alloc/dealloc differs.

cargo build --release
./target/release/cubecl-cuda-reinit-repro --mode flat  --iters 500  # baseline
./target/release/cubecl-cuda-reinit-repro --mode climb --iters 500  # trigger

Symptom

On flat, st stays roughly constant across iters.

On climb, st on the persistent network climbs monotonically across iters. The network's own state doesn't change (it just keeps training on the same shapes); only the process is being asked to alloc+drop a second network + Adam optimizer per outer iter.

Numbers from three Adam-trained networks per outer iter, 50 Optimizer::step calls per network per iter, over 1500 iters, one of the three reallocated per iter (st per network):

Network A st:  1173 -> 1411 ms  (+20.2%)
Network B st:   722 -> 1067 ms  (+47.8%)
Network C st:  1380 -> 1842 ms  (+33.5%)

GradientsParams::from_grads stayed at 1-3 ms flat throughout, so the growth is entirely inside Optimizer::step. fwd, bwd, prep also flat. Same workload on wgpu (DX12): flat end to end.

Isolation

Bisected by dropping each contributor one at a time (500-iter runs each):

Config Persistent net st climb
Baseline: 3 networks trained per iter, one reallocated per iter +37.6%
Only the persistent net trained (drop the other two loops) -7.5% flat
Persistent + second identical net, neither reallocated -0.0% flat
Persistent + reallocated-per-iter net +41.3%
Same, but the reallocated net does half the inner steps +87.1% (larger %)
Persistent + second net, keep reallocation flag OFF -3.1% flat

Halving the reallocated net's inner-step count increases the climb percentage, which means the accumulator is fixed per outer iter, not per Adam step. It scales with iter count, not with total Adam-step count. The only thing that flips the whole system from flat to climbing (or back) is whether or not the second network + its Adam optimizer are freshly allocated inside the outer loop.

Ruled out

  • Autotune cache growth: CUBECL_AUTOTUNE_LEVEL=minimal shows autotune events collapse to near zero after the first 100 iters, but st keeps climbing past that.
  • Autograd graph accumulation: GradientsParams::from_grads flat at 1-3 ms throughout, so the graph is being detached correctly on .backward().
  • Adam step-counter arithmetic: a network trained continuously for 50000-150000 Adam steps without any reallocation stays flat.
  • Fixed-shape workloads with no reallocation: 1 to 3 persistent networks + Adam, exhaustively varied (data content per step, mask ops, bootstrap-target-style extra forwards, etc.) all stay flat until a reallocation is added.

Hypothesis

Something in the cuda backend accumulates state across allocations of network params + Adam moment tensors that never fully returns when those tensors drop. Since wgpu doesn't share the code path, it doesn't reproduce. Reasonable suspects: the cuda memory pool's free-list / per-size buckets, or per-tensor caches keyed by handle.

Workaround for users

Keep networks alive across iterations (avoid Network::with_layers(...) + AdamConfig::new().init() inside your training loop). For loops that need "reset the optimizer between iterations" semantically, zero the Adam moment tensors in place rather than dropping and reallocating.

Happy to run additional probes on the same pod if that would help narrow it further from your side.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions