Skip to content

feat(cuda): cuda_device_count, and two device probes that still aborted - #297

Merged
AregGevorgyan merged 1 commit into
mainfrom
cuda/3.8-hardware-verification
Aug 12, 2026
Merged

feat(cuda): cuda_device_count, and two device probes that still aborted#297
AregGevorgyan merged 1 commit into
mainfrom
cuda/3.8-hardware-verification

Conversation

@AregGevorgyan

@AregGevorgyan AregGevorgyan commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Verified on 2× RTX 3090 (driver 570.207, CUDA 12.8, LLVM 15.0.7 + NVPTX, rustc 1.97.1 to match CI). Every claim below was checked by running it — none of the 3.8.0 CUDA changes had executed on hardware, because no CI job builds the extension with the feature.

Verification of what 3.8.0 shipped

# Claim Result
1 assert_ran_on_gpu hard gate PASS — 17 tests, no prime falls back
2 GpuBackendReport counters count PASS — see table below
4 memcheck instruments the test binary PASS — proven by positive control
5 capabilities() contract v3 PASSgroebner_cuda/numpy gone
7 gpu_available() catch_unwind PASS — returns true, suite runs

The counters are observably both true and false, so ran_on_gpu() is not permanently one or the other:

device_id on_gpu on_cpu ran_on_gpu() fell_back_to_cpu() first_gpu_error
None 0 16 false true None
Some(0) 16 0 true false None
Some(1) 16 0 true false None
Some(4096) 0 16 false true latched

memcheck is doing real work (item 4), checked rather than assumed. Removing the bounds guard from eliminate_row_kernel turned ERROR SUMMARY: 0 errors into 272 errors, reported as Invalid __global__ read of size 8 bytes at eliminate_row_kernel+0xf0. It analyses kernel instructions by name and offset, not the build tool. Guard restored.

What this changes

cuda_device_count() (item 6), which 3.8.0 deferred for want of a machine that could compile and run it. catch_unwind is load-bearing: cudarc panics rather than returning Err when libcuda.so cannot be dlopen'd, so a naive binding aborts the process on exactly the driverless machines a capability probe exists to report on. Reports 0 for every "no GPU here" shape, never raises. Both directions pinned by tests that run on hardware: every ordinal below the count launches, the ordinal at the count is refused. Added to _FEATURE_EXCLUSIVE_NAMES["cuda"] so the v3 contract test covers it.

Two probes still aborted on driverless machines. The 3.8.0 catch_unwind fix landed on groebner_cuda.rs::gpu_available only. nvptx_gpu.rs::device_available kept the bare CudaContext::new(0), and nvptx_multi_device_both_3090s called CudaContext::device_count() direct — both abort where they mean to skip, on the machines their own doc comments promise to support. Both now go through the guarded path.

A test for the error-fallback branch. first_gpu_error and the Err(e) arm of the reduce_gpu dispatch had never executed anywhere: the existing coverage is the None path, which never touches the driver.

racecheck (item 3) — promoted, with a caveat on the step

Flipped to continue-on-error: false. It is clean on real hardware (0 hazards, twice) and was never the false-positive risk the old comment assumed.

But read the coverage note before trusting a green tick. racecheck detects __shared__ memory hazards, and no kernel in this workspace uses any: ELIMINATE_ROW_PTX declares no .shared, compile_cuda's generated PTX declares none, launches pass shared_mem_bytes: 0. So it cannot fail today, and its green tick is a statement about the absence of shared memory, not the absence of races. It is a gate for the first kernel that introduces __shared__ — exactly when nobody would think to add one. The step is renamed to say so rather than left to be misread, since a ninth gate that passes while inspecting nothing is the thing this release exists to stop.

Gates

  • cargo fmt --all -- --check — clean
  • ruff format python/ tests/ — 167 files unchanged; ruff check all passed
  • cargo clippy --all-targets --features cuda,groebner-cuda -- -D warnings — exit 0
  • cargo test --features cuda,groebner-cuda2083 passed, 0 failed
  • cargo check --workspace (no CUDA) — passes, confirming the cfg-gating does not break default builds
  • pytest -q2709 passed, 57 skipped, 0 failed
  • tests/test_cuda.py + test_agent_contract.py under ALKAHEST_GPU_TESTS=1 — 32 passed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added CUDA device discovery through cuda_device_count() in the Python and core APIs.
    • CUDA availability checks now safely report zero devices when drivers or hardware are unavailable.
    • GPU execution validates device indices and falls back to CPU processing when a requested device is unavailable.
  • Bug Fixes

    • Improved handling of missing or unusable CUDA drivers without raising unexpected errors.
  • Documentation

    • Updated GPU usage guidance with device-count behavior and valid device indices.
  • Tests

    • Expanded coverage for device discovery, fallback behavior, device validation, and CUDA race detection.

Verified on 2x RTX 3090 (driver 570.207, CUDA 12.8, LLVM 15.0.7 + NVPTX,
rustc 1.97.1). Everything below was checked by running it, which is the point:
none of the 3.8.0 CUDA changes had executed on hardware, because no CI job
builds the extension with the feature and the dev box has neither GPU nor LLVM.

The 3.8.0 work holds up. The `assert_ran_on_gpu` hard gate passes — no prime
silently falls back — and the `GpuBackendReport` counters do count: a genuine
run reports 16 reductions on GPU and 0 on CPU on either card, `device_id: None`
reports 0/16, and an absent ordinal reports 0/16 with `first_gpu_error` latched
and the basis still correct. `ran_on_gpu()` is therefore observably true and
observably false, not permanently one or the other.

`cuda_device_count()` is added, which 3.8.0 deliberately deferred for want of a
machine that could compile and run it. `catch_unwind` is load-bearing: `cudarc`
panics rather than returning `Err` when `libcuda.so` cannot be dlopen'd, so a
naive binding aborts the process on exactly the driverless machines a capability
probe exists to report on. It reports 0 for every "no GPU here" shape and never
raises. Both directions are pinned by tests that run: every ordinal below the
count launches, the ordinal at the count is refused.

That same panic still aborted two probes. The 3.8.0 fix landed on
`groebner_cuda.rs::gpu_available` only; `nvptx_gpu.rs::device_available` kept the
bare `CudaContext::new(0)`, and `nvptx_multi_device_both_3090s` called
`CudaContext::device_count()` direct. Both aborted where they meant to skip, on
the machines their own doc comments promise to support. Both now go through the
guarded path.

racecheck is promoted to `continue-on-error: false`, but the honest reason to
read it is on the step, not in this message: it detects `__shared__` hazards and
no kernel here uses shared memory — `ELIMINATE_ROW_PTX` declares none,
`compile_cuda`'s output declares none, launches pass `shared_mem_bytes: 0`. It
cannot fail today. It is a gate for the first kernel that introduces `__shared__`,
which is exactly when nobody would remember to add one. A green tick from it
means "no shared memory", not "no races", and the step now says so.

memcheck, by contrast, is doing real work, and this was checked rather than
assumed: removing the bounds guard from `eliminate_row_kernel` turned
`ERROR SUMMARY: 0 errors` into 272 errors reported as
`Invalid __global__ read of size 8 bytes at eliminate_row_kernel+0xf0`. It is
analysing kernel instructions by name and offset, not the build tool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds safe CUDA device discovery across Rust and Python APIs. It updates exports and GPU documentation, adds fallback and ordinal tests, and makes the CUDA nightly racecheck a required workflow gate.

Changes

CUDA device discovery

Layer / File(s) Summary
Core CUDA device probe
alkahest-core/src/jit/..., alkahest-core/src/lib.rs
Adds cuda_device_count(), returns 0 for unavailable CUDA environments, and exposes the function through crate-level and stable APIs.
Python API and usage contract
alkahest-py/src/lib.rs, python/alkahest/__init__.py, docs/mdbook/src/gpu.md
Exposes cuda_device_count() in CUDA-enabled Python builds and documents device-count and ordinal behavior.
Fallback, ordinal, and CI validation
alkahest-core/tests/..., tests/test_*.py, .github/workflows/cuda_nightly.yml
Tests CPU fallback, valid and invalid device ordinals, public exports, and required CUDA racecheck execution.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: 🟡 Moderate · up to 807c0

The PR adds CUDA device validation, but an invalid device ordinal can still be accepted for empty batches, masking configuration errors and violating the documented behavior. Merge should wait for validation to occur before the empty-batch return or for the contract to be explicitly narrowed.

Sequence Diagram(s)

sequenceDiagram
  participant PythonCaller
  participant PythonExtension
  participant RustAPI
  participant CUDADriver
  PythonCaller->>PythonExtension: call cuda_device_count()
  PythonExtension->>RustAPI: call cuda_device_count()
  RustAPI->>CUDADriver: initialize CUDA and query devices
  CUDADriver-->>RustAPI: count or initialization failure
  RustAPI-->>PythonExtension: count or 0
  PythonExtension-->>PythonCaller: return integer count
Loading

Possibly related PRs

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the new cuda_device_count API and the guarded CUDA probes, which are the main changes in the pull request.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cuda/3.8-hardware-verification

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@alkahest-core/src/jit/nvptx.rs`:
- Around line 90-115: Update CudaCompiledFn::call_batch_on to validate
device_ordinal against cuda_device_count() before returning early for an empty
output, so out-of-range ordinals never succeed. Preserve the existing
empty-batch success behavior for valid ordinals and keep cuda_device_count’s
documented boundary consistent.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a4d1401-7ade-423f-a2ba-41b55a88d467

📥 Commits

Reviewing files that changed from the base of the PR and between a7b854d and 807c0a2.

📒 Files selected for processing (11)
  • .github/workflows/cuda_nightly.yml
  • alkahest-core/src/jit/mod.rs
  • alkahest-core/src/jit/nvptx.rs
  • alkahest-core/src/lib.rs
  • alkahest-core/tests/groebner_cuda.rs
  • alkahest-core/tests/nvptx_gpu.rs
  • alkahest-py/src/lib.rs
  • docs/mdbook/src/gpu.md
  • python/alkahest/__init__.py
  • tests/test_agent_contract.py
  • tests/test_cuda.py

Comment on lines +90 to +115
/// Number of CUDA devices visible to this process, or `0` when none are.
///
/// Answers "which ordinals may I pass to [`CudaCompiledFn::call_batch_on`]?"
/// without the caller having to probe by launching and catching
/// `E-CUDA-003`, which is what `docs/mdbook/src/gpu.md` had to recommend
/// while no verified implementation existed.
///
/// Returns `0` rather than an error for every "no GPU here" shape — no
/// driver, no device, driver too old. The distinction a caller acts on is
/// "can I use a GPU", and each of those answers it identically; an ordinal
/// is valid iff it is `< cuda_device_count()`.
///
/// `catch_unwind` is load-bearing for the same reason it is in
/// `groebner_cuda.rs::gpu_available`: `cudarc` *panics* rather than
/// returning `Err` when `libcuda.so` cannot be dlopen'd at all, which is the
/// state of any machine with no driver installed. A capability probe that
/// aborts the process on the exact configuration it exists to report on
/// would be worse than useless.
#[cfg(feature = "cuda")]
pub fn cuda_device_count() -> usize {
std::panic::catch_unwind(|| {
cudarc::driver::CudaContext::device_count()
.map(|n| n.max(0) as usize)
.unwrap_or(0)
})
.unwrap_or(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the ordinal boundary true for empty batches.

Line 100 states that an ordinal is valid only when it is below cuda_device_count(). However, CudaCompiledFn::call_batch_on returns Ok(()) for an empty output before it loads or validates device_ordinal. An out-of-range ordinal can therefore succeed for an empty batch.

Validate the ordinal before the empty-batch return, or restrict the documented contract to nonempty launches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alkahest-core/src/jit/nvptx.rs` around lines 90 - 115, Update
CudaCompiledFn::call_batch_on to validate device_ordinal against
cuda_device_count() before returning early for an empty output, so out-of-range
ordinals never succeed. Preserve the existing empty-batch success behavior for
valid ordinals and keep cuda_device_count’s documented boundary consistent.

@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 35 untouched benchmarks
⏩ 49 skipped benchmarks1


Comparing cuda/3.8-hardware-verification (807c0a2) with main (a7b854d)

Open in CodSpeed

Footnotes

  1. 49 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@AregGevorgyan
AregGevorgyan merged commit eb5db8a into main Aug 12, 2026
15 checks passed
@AregGevorgyan
AregGevorgyan deleted the cuda/3.8-hardware-verification branch August 12, 2026 21:54
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.

1 participant