gam_models/bms/gpu/
flex.rs1use 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
16pub 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
33pub 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#[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#[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#[must_use]
76pub struct BmsFlexGpuBackend {
77 #[cfg(target_os = "linux")]
78 pub(crate) inner: gam_gpu::backend_probe::CudaBackendContext,
79}
80
81impl BmsFlexGpuBackend {
82 pub const fn compiled() -> bool {
87 cfg!(target_os = "linux")
88 }
89
90 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 backend.compile_probe_module()?;
122 Ok(backend)
123 }
124
125 #[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 #[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 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 #[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 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#[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 #[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 #[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 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}