This is an automated email from the ASF dual-hosted git repository.
400Ping pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/mahout.git
The following commit(s) were added to refs/heads/main by this push:
new 826c3abb5 [MAHOUT-1414][QDP] Expose native cuda_available() and gate
GPU tests on it (#1416)
826c3abb5 is described below
commit 826c3abb57ba1d39930a7a0e8af57f5844f7304e
Author: Ryan Huang <[email protected]>
AuthorDate: Sun Jul 5 21:42:35 2026 +0900
[MAHOUT-1414][QDP] Expose native cuda_available() and gate GPU tests on it
(#1416)
Follow-up to #1321 (addresses #1414). After #1321, `_qdp` can build and
import without the CUDA toolkit (stub runtime), so "extension importable"
no longer implies a usable GPU. The test suite gated GPU tests on
`torch.cuda.is_available()`, a proxy that is wrong on this PR's headline
scenario -- a GPU host with PyTorch but no toolkit, where `_qdp` is a stub
build yet torch still reports a device.
- qdp-core: add `cuda_runtime_available()`, which queries
`cudaGetDeviceCount` (false in a stub build via the existing 999 sentinel
stub, and on hosts with no device). Re-exported from the crate root.
- _qdp: expose it as `_qdp.cuda_available()`.
- qumat_qdp: add `is_cuda_available()`, mirroring
`is_triton_amd_available()`,
as the single Python source of truth.
- testing/conftest: gate the `@pytest.mark.gpu` auto-skip on the native
signal (falling back to torch only if the helper is absent).
- test_fallback: coverage that runs on a stub build too, guarding that
querying availability returns a bool without aborting.
Verified on GPU (tests run and pass) and with CUDA hidden (tests skip);
full Rust suite, clippy --all-features, ruff, and ty all clean.
---
qdp/qdp-core/src/gpu/cuda_ffi.rs | 24 ++++++++++++++++++++++++
qdp/qdp-core/src/lib.rs | 1 +
qdp/qdp-python/qumat_qdp/__init__.py | 22 ++++++++++++++++++++++
qdp/qdp-python/src/lib.rs | 13 +++++++++++++
testing/conftest.py | 13 +++++++++++--
testing/qdp_python/test_fallback.py | 23 +++++++++++++++++++++++
6 files changed, 94 insertions(+), 2 deletions(-)
diff --git a/qdp/qdp-core/src/gpu/cuda_ffi.rs b/qdp/qdp-core/src/gpu/cuda_ffi.rs
index dc8930584..dbbe844db 100644
--- a/qdp/qdp-core/src/gpu/cuda_ffi.rs
+++ b/qdp/qdp-core/src/gpu/cuda_ffi.rs
@@ -69,6 +69,10 @@ unsafe extern "C" {
pub(crate) fn cudaMemGetInfo(free: *mut usize, total: *mut usize) -> i32;
+ /// Number of CUDA-capable devices. Used to detect whether a GPU is
+ /// actually present at runtime (see `cuda_runtime_available`).
+ pub(crate) fn cudaGetDeviceCount(count: *mut i32) -> i32;
+
pub(crate) fn cudaMemcpyAsync(
dst: *mut c_void,
src: *const c_void,
@@ -153,6 +157,10 @@ mod no_cuda_stubs {
QDP_CUDA_UNAVAILABLE
}
+ pub(crate) unsafe fn cudaGetDeviceCount(_count: *mut i32) -> i32 {
+ QDP_CUDA_UNAVAILABLE
+ }
+
pub(crate) unsafe fn cudaMemcpyAsync(
_dst: *mut c_void,
_src: *const c_void,
@@ -216,3 +224,19 @@ mod no_cuda_stubs {
#[cfg(qdp_no_cuda)]
pub(crate) use no_cuda_stubs::*;
+
+/// Returns `true` if a usable CUDA device is present at runtime.
+///
+/// This is deliberately distinct from "the extension is importable": a
+/// no-toolkit (`qdp_no_cuda`) build links CUDA stubs that return a non-zero
+/// sentinel, and a host with the toolkit but no GPU reports zero devices. Both
+/// yield `false` here, so callers (and the Python test suite) can gate GPU
work
+/// on an accurate signal rather than mere importability.
+pub fn cuda_runtime_available() -> bool {
+ let mut count: i32 = 0;
+ // SAFETY: `cudaGetDeviceCount` writes a single `i32` through `count` and
+ // accesses no other memory. In a `qdp_no_cuda` build this resolves to the
+ // stub above, which returns the unavailable sentinel without
dereferencing.
+ let rc = unsafe { cudaGetDeviceCount(&mut count) };
+ rc == CUDA_SUCCESS && count > 0
+}
diff --git a/qdp/qdp-core/src/lib.rs b/qdp/qdp-core/src/lib.rs
index 822fba96e..95259793c 100644
--- a/qdp/qdp-core/src/lib.rs
+++ b/qdp/qdp-core/src/lib.rs
@@ -36,6 +36,7 @@ pub mod types;
mod profiling;
pub use error::{MahoutError, Result, cuda_error_to_string};
+pub use gpu::cuda_ffi::cuda_runtime_available;
pub use gpu::memory::Precision;
pub use reader::{FloatElem, NullHandling, handle_float32_nulls,
handle_float64_nulls};
pub use types::{Dtype, Encoding};
diff --git a/qdp/qdp-python/qumat_qdp/__init__.py
b/qdp/qdp-python/qumat_qdp/__init__.py
index 2b7566141..81ffd6634 100644
--- a/qdp/qdp-python/qumat_qdp/__init__.py
+++ b/qdp/qdp-python/qumat_qdp/__init__.py
@@ -44,6 +44,27 @@ else:
BACKEND = get_backend()
+
+def is_cuda_available() -> bool:
+ """Return whether a usable CUDA device is available to the native engine.
+
+ Unlike the importability of the ``_qdp`` extension -- which only means it
+ was built, possibly against CUDA stubs on a host without the toolkit --
this
+ reflects whether GPU work can actually run: ``False`` for a stub build or a
+ host with no CUDA device, ``True`` only when the native runtime reports at
+ least one device.
+ """
+ if _qdp_mod is None:
+ return False
+ probe = getattr(_qdp_mod, "cuda_available", None)
+ if probe is None:
+ return False
+ try:
+ return bool(probe())
+ except Exception:
+ return False
+
+
from qumat_qdp.api import (
LatencyResult,
QdpBenchmark,
@@ -68,6 +89,7 @@ __all__ = [
"ThroughputResult",
"TritonAmdEngine",
"force_backend",
+ "is_cuda_available",
"is_triton_amd_available",
"run_throughput_pipeline_py",
]
diff --git a/qdp/qdp-python/src/lib.rs b/qdp/qdp-python/src/lib.rs
index 04d772a90..0bfd77189 100644
--- a/qdp/qdp-python/src/lib.rs
+++ b/qdp/qdp-python/src/lib.rs
@@ -68,6 +68,18 @@ fn run_throughput_pipeline_py(
))
}
+/// Returns ``True`` if a usable CUDA device is available to the native engine.
+///
+/// This reflects whether GPU work can actually run -- it is ``False`` for a
+/// stub build (the extension built without the CUDA toolkit) or a host with no
+/// CUDA device, and ``True`` only when the native runtime reports a device.
+/// Importability of this module only means it was built, which a stub build
+/// makes possible without a GPU.
+#[pyfunction]
+fn cuda_available() -> bool {
+ qdp_core::cuda_runtime_available()
+}
+
/// Quantum Data Plane (QDP) Python module
///
/// GPU-accelerated quantum data encoding with DLPack integration.
@@ -78,6 +90,7 @@ fn _qdp(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<QdpEngine>()?;
m.add_class::<QuantumTensor>()?;
+ m.add_function(wrap_pyfunction!(cuda_available, m)?)?;
#[cfg(target_os = "linux")]
m.add_class::<PyQuantumLoader>()?;
#[cfg(target_os = "linux")]
diff --git a/testing/conftest.py b/testing/conftest.py
index 95009cc3c..010b7230a 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -47,9 +47,18 @@ def _gpu_available() -> bool:
toolkit -- it links stub CUDA Runtime symbols (see qdp-core ``build.rs`` /
``cuda_ffi.rs``). So importing ``_qdp`` no longer implies a working GPU,
and ``@pytest.mark.gpu`` tests must additionally check for a real device.
- This mirrors the ``torch.cuda.is_available()`` guard used by the inline
- skips throughout the QDP test modules.
+
+ Prefers the native engine's own signal (``qumat_qdp.is_cuda_available()``),
+ which is correct even for a stub build: a host with a GPU + PyTorch but no
+ CUDA toolkit builds ``_qdp`` against stubs, where
``torch.cuda.is_available()``
+ would wrongly report True. Falls back to torch when the helper is absent.
"""
+ try:
+ from qumat_qdp import is_cuda_available
+
+ return bool(is_cuda_available())
+ except Exception:
+ pass
try:
import torch
diff --git a/testing/qdp_python/test_fallback.py
b/testing/qdp_python/test_fallback.py
index 9b34ff12b..ac9e7739d 100644
--- a/testing/qdp_python/test_fallback.py
+++ b/testing/qdp_python/test_fallback.py
@@ -85,6 +85,29 @@ class TestBackendDetection:
t = get_torch()
assert t is not None # torch is available in test env
+ def test_is_cuda_available_returns_bool(self):
+ """is_cuda_available() returns a plain bool without crashing.
+
+ On a stub build (the extension built without the CUDA toolkit) or a
host
+ with no device it must be False -- and querying it must NOT abort the
+ process, which is the regression this guards (a stub build previously
+ aborted when GPU code ran).
+ """
+ from qumat_qdp import is_cuda_available
+
+ assert isinstance(is_cuda_available(), bool)
+
+ def test_is_cuda_available_matches_extension(self):
+ """The helper mirrors the native ``_qdp.cuda_available()`` signal."""
+ from qumat_qdp import is_cuda_available
+
+ try:
+ import _qdp
+ except ImportError:
+ assert is_cuda_available() is False
+ else:
+ assert is_cuda_available() == bool(_qdp.cuda_available())
+
# ---------------------------------------------------------------------------
# Loader with explicit PyTorch backend