Skip to content

fix: GPU training silently scores every sampled trajectory 0 — CPU sampling Generator vs CUDA multinomial - #421

Merged
James-CUDA merged 1 commit into
James-CUDA:mainfrom
kai392:fix/critical-issue-sampling-rng-device
Jul 23, 2026
Merged

fix: GPU training silently scores every sampled trajectory 0 — CPU sampling Generator vs CUDA multinomial#421
James-CUDA merged 1 commit into
James-CUDA:mainfrom
kai392:fix/critical-issue-sampling-rng-device

Conversation

@kai392

@kai392 kai392 commented Jul 22, 2026

Copy link
Copy Markdown

Root cause

optim.sampling.trajectory_sampling_rng (added in #171 to make --seed reproduce a run) builds a CPU torch.Generator. optim.fitness.evaluate_candidate passes it into head.select(..., sample=True), where torch.multinomial draws from softmax probabilities living on the head's devicecuda:0 per configs/trinity.yaml and CoordinatorPolicy.build's default. PyTorch requires multinomial's input and generator to share a device, so on any GPU run the first sampled turn of every trajectory raises:

RuntimeError: Expected a 'cuda' device type for generator but found 'cpu'

evaluate_candidate's asyncio.gather(..., return_exceptions=True) then swallows the error and scores the trajectory 0.0 (by design, for transient API failures), printing only [warn] N/N trajectories failed. Net effect: every candidate in every generation gets fitness 0, sep-CMA-ES optimizes a flat all-zero landscape, and the run 'completes successfully', writing a meaningless best_theta.npy. train.py always passes sample=True, run_seed=args.seed (default 0), so every real GPU training run since #171 is affected. The CPU-only CI can never see it — CPU probs + CPU generator match.

Fix

LinearHead.select now draws on the generator's device: the two prob vectors ((n_models,) / (n_roles,) — a few floats) are moved to rng.device before multinomial. rng=None behavior is unchanged (samples on the probs' own device).

Bonus: the seeded draw becomes device-independent — the same --seed picks the same (agent, role) sequence whether the head runs on CPU or CUDA, strengthening #171's reproducibility goal.

Impact / risk

  • Un-breaks GPU training end-to-end; no behavior change for CPU runs (.to() is a no-op on matching devices) and none for eval (sample=False path untouched).
  • Regression tests simulate the device mismatch without a GPU (meta-device generator stand-in + multinomial spy): they fail on pre-fix code, pass post-fix. The rng=None no-move contract is pinned too.

Note on CI: current main fails collection repo-wide (unresolved merge-conflict markers in src/trinity/analysis/__init__.py, issue #404 / PRs #402, #405). This PR's checks will go green once that lands; the fix itself is ruff-clean and its test file passes (21/21) locally.

🤖 Generated with Claude Code

@kai392 kai392 changed the title fix: resolve GPU-training crash where every sampled trajectory scored… fix: GPU training silently scores every sampled trajectory 0 — CPU sampling Generator vs CUDA multinomial Jul 22, 2026
… 0 (CPU rng vs CUDA probs)

trajectory_sampling_rng builds a CPU torch.Generator, but the head - and so
the softmax probabilities select() feeds torch.multinomial - lives on the
training device (configs/trinity.yaml: cuda:0). torch.multinomial requires
its input and its generator to share a device, so on any GPU run the first
sampled turn of every trajectory raised

    RuntimeError: Expected a 'cuda' device type for generator but found 'cpu'

evaluate_candidate's gather(return_exceptions=True) swallowed the error and
scored every trajectory 0.0, so sep-CMA-ES silently optimized a flat all-zero
fitness landscape while the run 'completed successfully'. The CPU-only test
suite could never see it (CPU probs + CPU generator match).

Fix: draw on the generator's device. select() now moves the two tiny prob
vectors onto rng.device before multinomial. This also makes the seeded draw
device-independent: the same --seed picks the same (agent, role) whether the
head runs on CPU or CUDA. rng=None keeps sampling on the probs' own device.

Regression tests simulate the device mismatch without a GPU via a
meta-device generator stand-in plus a multinomial spy, and pin the rng=None
no-move behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@James-CUDA
James-CUDA force-pushed the fix/critical-issue-sampling-rng-device branch from c2d3208 to 1d1787d Compare July 23, 2026 11:32
@James-CUDA
James-CUDA merged commit ec79538 into James-CUDA:main Jul 23, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants