Skip to main content

gam_models/bms/gpu/
flex.rs

1//! Bernoulli marginal-slope FLEX GPU policy and backend probe.
2
3use std::sync::OnceLock;
4
5use gam_gpu::gpu_error::GpuError;
6#[cfg(target_os = "linux")]
7use gam_gpu::gpu_error::GpuResultExt;
8use gam_gpu::{GpuDecision, GpuKernel, decide};
9
10#[cfg(target_os = "linux")]
11use std::sync::Arc;
12
13#[cfg(target_os = "linux")]
14use cudarc::driver::CudaModule;
15
16/// Decide whether the GPU row-primary Hessian path is eligible for this
17/// fit's `(n, r)`. Always-`use_gpu=false` for `r == 0` (no flex jets to
18/// process) and below the runtime row-kernel threshold.
19pub fn row_primary_hessian_decision(n: usize, r: usize) -> Result<GpuDecision, GpuError> {
20    let large_enough = if r == 0 {
21        false
22    } else {
23        gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::global_policy())?
24            .map(|runtime| n >= runtime.policy().row_kernel_min_n)
25            .unwrap_or(false)
26    };
27    decide(
28        GpuKernel::MarginalSlopeRows,
29        gam_gpu::GpuEligibility::from_flags(BmsFlexGpuBackend::compiled(), large_enough),
30    )
31}
32
33/// Same as [`row_primary_hessian_decision`] but turns
34/// `gpu=required`-without-support into an `Err` string at the call site.
35pub fn require_row_primary_hessian_supported(n: usize, r: usize) -> Result<GpuDecision, String> {
36    let decision = row_primary_hessian_decision(n, r).map_err(String::from)?;
37    decision.clone().log();
38    decision.require_supported()?;
39    Ok(decision)
40}
41
42/// Preserve the selected-GPU execution contract for every downstream
43/// consumer. Once policy has produced device-resident BMS FLEX state, a CUDA
44/// failure is an execution error; callers must not reinterpret it as permission
45/// to run a different CPU algorithm.
46// Its production callers compile under `cfg(target_os = "linux")` (the CUDA
47// path); off-Linux the lib target has no caller and `-D dead-code` rejects it,
48// the break that has been failing the macOS and Windows wheel jobs. Gate to the
49// platforms that own the callers rather than suppressing the lint; the fixtures
50// that exercise it are gated to Linux alongside it.
51#[cfg(target_os = "linux")]
52pub(crate) fn require_selected_gpu_result<T>(
53    operation: &str,
54    result: Result<T, GpuError>,
55) -> Result<T, String> {
56    result.map_err(|error| format!("BMS FLEX selected GPU {operation} failed: {error}"))
57}
58
59/// The PTX source compiled and loaded at first use of the BMS flex GPU
60/// backend. The probe kernel exercises the full NVRTC → cuModuleLoadData
61/// → cuModuleGetFunction → cuLaunchKernel path so the scaffolding catches
62/// host-side issues (PTX cache, arena alloc, stream sync) before the real
63/// row kernel is dispatched by the row-primary cache builder.
64#[cfg(target_os = "linux")]
65pub(crate) const PROBE_KERNEL_SOURCE: &str = r#"
66extern "C" __global__ void bms_flex_probe() {
67    // Intentionally empty. This kernel exists only so the scaffolding can
68    // verify NVRTC compile + module load + launch + synchronize on the
69    // selected device. The real row math lives in the bms_flex_row module.
70}
71"#;
72
73/// Process-wide BMS-flex GPU backend. Lazy-initialised on first call to
74/// [`BmsFlexGpuBackend::probe`].
75#[must_use]
76pub struct BmsFlexGpuBackend {
77    #[cfg(target_os = "linux")]
78    pub(crate) inner: gam_gpu::backend_probe::CudaBackendContext,
79}
80
81impl BmsFlexGpuBackend {
82    /// Returns `true` if the BMS flex GPU backend is compiled into this
83    /// build (Linux + cudarc). On non-Linux builds returns `false` so the
84    /// policy gate reports `cpu-gpu-backend-not-compiled` like the rest
85    /// of the GPU layer.
86    pub const fn compiled() -> bool {
87        cfg!(target_os = "linux")
88    }
89
90    /// Lazily initialise the process-wide BMS flex backend. On the first
91    /// successful call this creates a CUDA context on the runtime's
92    /// selected device, opens a stream, and NVRTC-compiles the probe
93    /// kernel. Subsequent calls return the cached handle.
94    pub fn probe() -> Result<&'static Self, GpuError> {
95        static BACKEND: OnceLock<Result<BmsFlexGpuBackend, GpuError>> = OnceLock::new();
96        BACKEND
97            .get_or_init(|| {
98                #[cfg(target_os = "linux")]
99                {
100                    Self::probe_linux()
101                }
102                #[cfg(not(target_os = "linux"))]
103                {
104                    Err(GpuError::DriverLibraryUnavailable {
105                        reason: "bms_flex GPU backend is Linux-only".to_string(),
106                    })
107                }
108            })
109            .as_ref()
110            .map_err(GpuError::clone)
111    }
112
113    #[cfg(target_os = "linux")]
114    pub(crate) fn probe_linux() -> Result<Self, GpuError> {
115        let parts = gam_gpu::backend_probe::probe_cuda_backend("bms_flex")?;
116        let backend = BmsFlexGpuBackend {
117            inner: gam_gpu::backend_probe::CudaBackendContext::from_parts(parts),
118        };
119        // Eagerly compile the probe kernel so any NVRTC failure surfaces
120        // here, not at first dispatch.
121        backend.compile_probe_module()?;
122        Ok(backend)
123    }
124
125    /// NVRTC-compile (or fetch from cache) the probe module.
126    #[cfg(target_os = "linux")]
127    pub(crate) fn compile_probe_module(&self) -> Result<&Arc<CudaModule>, GpuError> {
128        self.inner
129            .module
130            .get_or_compile(&self.inner.ctx, "bms_flex", PROBE_KERNEL_SOURCE)
131    }
132
133    /// Launch the probe kernel and synchronize. Used by tests and by the
134    /// dispatcher's policy gate to verify the full host-orchestration
135    /// path before the real row kernel is dispatched.
136    #[cfg(target_os = "linux")]
137    pub fn launch_probe(&self) -> Result<(), GpuError> {
138        use cudarc::driver::LaunchConfig;
139        let module = self.compile_probe_module()?;
140        let func = module
141            .load_function("bms_flex_probe")
142            .gpu_ctx("bms_flex probe load_function")?;
143        let cfg = LaunchConfig {
144            grid_dim: (1, 1, 1),
145            block_dim: (1, 1, 1),
146            shared_mem_bytes: 0,
147        };
148        let mut builder = self.inner.stream.launch_builder(&func);
149        // SAFETY: probe kernel takes no arguments and does no memory
150        // access, so launch parameters and lack of args are trivially
151        // valid for any device.
152        unsafe { builder.launch(cfg) }.gpu_ctx("bms_flex probe launch")?;
153        self.inner
154            .stream
155            .synchronize()
156            .gpu_ctx("bms_flex probe synchronize")?;
157        Ok(())
158    }
159
160    #[cfg(not(target_os = "linux"))]
161    pub fn launch_probe(&self) -> Result<(), GpuError> {
162        Err(GpuError::DriverLibraryUnavailable {
163            reason: "bms_flex GPU backend is Linux-only".to_string(),
164        })
165    }
166
167    /// Round-trip the arena: allocate a slab, immediately release it.
168    /// Used by the device-side smoke test to verify the arena code path
169    /// is exercised; production milestones will hold slabs across the
170    /// whole row sweep instead.
171    #[cfg(target_os = "linux")]
172    pub fn arena_round_trip(&self, elements: usize) -> Result<usize, GpuError> {
173        let mut guard = self
174            .inner
175            .arena
176            .lock()
177            .gpu_ctx("bms_flex arena mutex poisoned")?;
178        let (bucket, slab) = guard.alloc(&self.inner.stream, elements, "bms_flex")?;
179        guard.release(bucket, slab);
180        Ok(bucket)
181    }
182
183    /// Return a short string describing the backend state, for logs.
184    pub fn describe(&self) -> String {
185        #[cfg(target_os = "linux")]
186        {
187            return format!(
188                "bms_flex backend: device={:?} module_loaded={}",
189                self.inner.ctx.name().ok(),
190                self.inner.module.get().is_some()
191            );
192        }
193        #[cfg(not(target_os = "linux"))]
194        {
195            "bms_flex backend: unavailable (not Linux)".to_string()
196        }
197    }
198}
199
200// ────────────────────────────────────────────────────────────────────────
201// Tests. Run via `cargo test -p gam bms_flex_gpu -- --nocapture`.
202// ────────────────────────────────────────────────────────────────────────
203
204#[cfg(test)]
205mod bms_flex_gpu_tests {
206    use super::*;
207
208    #[test]
209    pub(crate) fn bms_flex_gpu_policy_decision_is_explicit() {
210        let decision = row_primary_hessian_decision(50_000, 4)
211            .expect("GPU policy resolution must be lossless");
212        assert_eq!(decision.kernel, GpuKernel::MarginalSlopeRows);
213    }
214
215    // Exercises the Linux-only selected-GPU contract helper, so it is gated with
216    // it; stacked attributes read as AND.
217    #[cfg(target_os = "linux")]
218    #[test]
219    pub(crate) fn selected_gpu_errors_propagate_without_algorithm_substitution_932() {
220        let error = require_selected_gpu_result::<()>(
221            "sentinel operation",
222            Err(GpuError::DriverCallFailed {
223                reason: "sentinel device fault".to_string(),
224            }),
225        )
226        .expect_err("a selected CUDA failure must propagate");
227        assert!(error.contains("selected GPU sentinel operation failed"));
228        assert!(error.contains("sentinel device fault"));
229    }
230
231    /// V100-only: probe the backend end-to-end (CUDA context create, NVRTC
232    /// compile, module load, launch, sync). Skipped on hosts without a
233    /// usable device so the test still passes on the CI/mac builders.
234    #[test]
235    pub(crate) fn bms_flex_gpu_context_initialises_when_device_present() {
236        let runtime = match gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::GpuPolicy::Auto)
237        {
238            Ok(Some(runtime)) => runtime,
239            Ok(None) => {
240                eprintln!("[bms_flex_gpu test] no CUDA device — skipping device-side init smoketest");
241                return;
242            }
243            Err(error) => panic!("[bms_flex_gpu test] CUDA probe failed: {error}"),
244        };
245        eprintln!(
246            "[bms_flex_gpu test] runtime selected device ordinal={}",
247            runtime.selected_device().ordinal
248        );
249        let backend = BmsFlexGpuBackend::probe().unwrap_or_else(|err| {
250            panic!("BmsFlexGpuBackend::probe failed on a host that reports a CUDA runtime: {err}")
251        });
252        eprintln!("[bms_flex_gpu test] {}", backend.describe());
253        backend
254            .launch_probe()
255            .expect("probe kernel must launch+sync on a host with a usable device");
256        #[cfg(target_os = "linux")]
257        {
258            let bucket = backend
259                .arena_round_trip(1024)
260                .expect("arena round-trip must succeed on a host with a usable device");
261            assert!(bucket >= 1024, "bucket must be >= requested elements");
262            // Second round-trip at the same size should hit the cache.
263            let bucket2 = backend
264                .arena_round_trip(1024)
265                .expect("arena round-trip must succeed on a host with a usable device");
266            assert_eq!(bucket, bucket2, "bucket size must be stable for same input");
267        }
268    }
269}