1#[cfg(target_os = "linux")]
82use std::sync::OnceLock;
83
84use gam_gpu::gpu_error::GpuError;
85
86#[cfg(target_os = "linux")]
87use std::sync::Arc;
88
89#[cfg(target_os = "linux")]
90use cudarc::driver::{CudaModule, CudaSlice, CudaStream, LaunchConfig, PushKernelArg};
91
92#[cfg(target_os = "linux")]
93use super::super::flex_row_program::{
94 BmsFlexCalibrationOrder2Phase, BmsFlexRowOrder2FinalizerPhase, BmsFlexRowProgram,
95};
96
97#[cfg(target_os = "linux")]
101pub(crate) const ROW_KERNEL_THREADS: u32 = 32;
102
103pub(crate) const COEFF4: usize = 4;
106
107pub(crate) const MOMENT_STRIDE: usize = 10;
110
111pub(crate) enum CellMomentsSource<'a> {
116 Host(&'a [f64]),
119 #[cfg(target_os = "linux")]
124 Device(&'a CudaSlice<f64>),
125}
126
127impl<'a> CellMomentsSource<'a> {
128 pub(crate) fn len(&self) -> usize {
130 match self {
131 CellMomentsSource::Host(slice) => slice.len(),
132 #[cfg(target_os = "linux")]
133 CellMomentsSource::Device(d) => d.len(),
134 }
135 }
136}
137
138macro_rules! define_bms_flex_row_kernel_input_types {
146 (
147 f64_fields: [$($f64_field:ident),+ $(,)?],
148 u32_fields: [$($u32_field:ident),+ $(,)?],
149 moments_field: $moments_field:ident $(,)?
150 ) => {
151 pub(crate) struct BmsFlexRowKernelInputs<'a> {
152 pub n_rows: usize,
154 pub r: usize,
156 pub p_h: usize,
158 pub p_w: usize,
160 pub s_f: f64,
163 $(pub $f64_field: &'a [f64],)+
164 $(pub $u32_field: &'a [u32],)+
165 pub $moments_field: CellMomentsSource<'a>,
166 }
167
168 pub(crate) struct BmsFlexRowKernelInputsOwned {
173 pub n_rows: usize,
174 pub r: usize,
175 pub p_h: usize,
176 pub p_w: usize,
177 pub s_f: f64,
178 $(pub $f64_field: Vec<f64>,)+
179 $(pub $u32_field: Vec<u32>,)+
180 pub $moments_field: Vec<f64>,
181 #[cfg(target_os = "linux")]
185 pub cell_moments_device: Option<CudaSlice<f64>>,
186 }
187
188 impl BmsFlexRowKernelInputsOwned {
189 pub(crate) fn as_borrowed(&self) -> BmsFlexRowKernelInputs<'_> {
194 #[cfg(target_os = "linux")]
195 let cell_moments = match self.cell_moments_device.as_ref() {
196 Some(d) => CellMomentsSource::Device(d),
197 None => CellMomentsSource::Host(&self.cell_moments),
198 };
199 #[cfg(not(target_os = "linux"))]
200 let cell_moments = CellMomentsSource::Host(&self.cell_moments);
201 BmsFlexRowKernelInputs {
202 n_rows: self.n_rows,
203 r: self.r,
204 p_h: self.p_h,
205 p_w: self.p_w,
206 s_f: self.s_f,
207 $($f64_field: &self.$f64_field,)+
208 $($u32_field: &self.$u32_field,)+
209 $moments_field: cell_moments,
210 }
211 }
212 }
213 };
214}
215
216define_bms_flex_row_kernel_input_types! {
217 f64_fields: [
218 q,
219 b,
220 mu_1,
221 mu_2,
222 z_obs,
223 y,
224 w,
225 e_obs,
226 cell_c0,
227 cell_c1,
228 cell_c2,
229 cell_c3,
230 cell_a,
231 cell_aa,
232 cell_r,
233 cell_ar,
234 cell_sbb,
235 cell_sbh,
236 cell_sbw,
237 chi_obs,
238 xi_obs,
239 rho_u,
240 tau_u,
241 r_uv,
242 ],
243 u32_fields: [cell_offsets],
244 moments_field: cell_moments,
245}
246
247#[derive(Debug)]
249pub(crate) struct BmsFlexRowKernelOutputs {
250 pub neglog: Vec<f64>,
252 pub grad: Vec<f64>,
254 pub hess: Vec<f64>,
257}
258
259fn checked_shape_len(context: &str, dimensions: &[usize]) -> Result<usize, GpuError> {
260 dimensions
261 .iter()
262 .copied()
263 .try_fold(1_usize, |product, dimension| {
264 product
265 .checked_mul(dimension)
266 .ok_or_else(|| GpuError::DriverCallFailed {
267 reason: format!(
268 "bms_flex_row {context}: shape product overflow for dimensions {dimensions:?}"
269 ),
270 })
271 })
272}
273
274impl<'a> BmsFlexRowKernelInputs<'a> {
275 pub(crate) fn validate(&self) -> Result<(), GpuError> {
278 if self.n_rows == 0 {
279 return Err(GpuError::DriverCallFailed {
280 reason: "bms_flex_row inputs: n_rows must be > 0".to_string(),
281 });
282 }
283 if self.r == 0 {
284 return Err(GpuError::DriverCallFailed {
285 reason: "bms_flex_row inputs: r must be > 0".to_string(),
286 });
287 }
288 let decomposed_r = 2_usize
289 .checked_add(self.p_h)
290 .and_then(|value| value.checked_add(self.p_w))
291 .ok_or_else(|| GpuError::DriverCallFailed {
292 reason: format!(
293 "bms_flex_row inputs: primary decomposition overflow for p_h={} p_w={}",
294 self.p_h, self.p_w
295 ),
296 })?;
297 if self.r != decomposed_r {
298 return Err(GpuError::DriverCallFailed {
299 reason: format!(
300 "bms_flex_row inputs: r={} must equal 2 + p_h({}) + p_w({}) = {}",
301 self.r, self.p_h, self.p_w, decomposed_r
302 ),
303 });
304 }
305 let n = self.n_rows;
306 let check_len = |name: &str, have: usize, want: usize| -> Result<(), GpuError> {
307 if have != want {
308 return Err(GpuError::DriverCallFailed {
309 reason: format!("bms_flex_row inputs: {name}.len()={have} != {want}"),
310 });
311 }
312 Ok(())
313 };
314 check_len("q", self.q.len(), n)?;
315 check_len("b", self.b.len(), n)?;
316 check_len("mu_1", self.mu_1.len(), n)?;
317 check_len("mu_2", self.mu_2.len(), n)?;
318 check_len("z_obs", self.z_obs.len(), n)?;
319 check_len("y", self.y.len(), n)?;
320 check_len("w", self.w.len(), n)?;
321 check_len("e_obs", self.e_obs.len(), n)?;
322 check_len("chi_obs", self.chi_obs.len(), n)?;
323 check_len("xi_obs", self.xi_obs.len(), n)?;
324 let nr = checked_shape_len("validate [n,r]", &[n, self.r])?;
325 let nrr = checked_shape_len("validate [n,r,r]", &[n, self.r, self.r])?;
326 check_len("rho_u", self.rho_u.len(), nr)?;
327 check_len("tau_u", self.tau_u.len(), nr)?;
328 check_len("r_uv", self.r_uv.len(), nrr)?;
329 let offsets_len = n.checked_add(1).ok_or_else(|| GpuError::DriverCallFailed {
330 reason: format!("bms_flex_row inputs: n_rows={n} cannot form n+1 offsets"),
331 })?;
332 check_len("cell_offsets", self.cell_offsets.len(), offsets_len)?;
333 let total_cells_u32 = self.cell_offsets[n];
334 let total_cells = total_cells_u32 as usize;
335 check_len("cell_c0", self.cell_c0.len(), total_cells)?;
336 check_len("cell_c1", self.cell_c1.len(), total_cells)?;
337 check_len("cell_c2", self.cell_c2.len(), total_cells)?;
338 check_len("cell_c3", self.cell_c3.len(), total_cells)?;
339 let cells_coeff4 = checked_shape_len("validate cell coeff4", &[total_cells, COEFF4])?;
340 check_len("cell_a", self.cell_a.len(), cells_coeff4)?;
341 check_len("cell_aa", self.cell_aa.len(), cells_coeff4)?;
342 check_len(
343 "cell_r",
344 self.cell_r.len(),
345 checked_shape_len(
346 "validate cell_r",
347 &[total_cells, self.r.saturating_sub(1), COEFF4],
348 )?,
349 )?;
350 check_len(
351 "cell_ar",
352 self.cell_ar.len(),
353 checked_shape_len(
354 "validate cell_ar",
355 &[total_cells, self.r.saturating_sub(1), COEFF4],
356 )?,
357 )?;
358 check_len("cell_sbb", self.cell_sbb.len(), cells_coeff4)?;
359 check_len(
360 "cell_sbh",
361 self.cell_sbh.len(),
362 checked_shape_len("validate cell_sbh", &[total_cells, self.p_h, COEFF4])?,
363 )?;
364 check_len(
365 "cell_sbw",
366 self.cell_sbw.len(),
367 checked_shape_len("validate cell_sbw", &[total_cells, self.p_w, COEFF4])?,
368 )?;
369 check_len(
370 "cell_moments",
371 self.cell_moments.len(),
372 checked_shape_len("validate cell_moments", &[total_cells, MOMENT_STRIDE])?,
373 )?;
374 for i in 0..n {
380 if self.cell_offsets[i] > self.cell_offsets[i + 1] {
381 return Err(GpuError::DriverCallFailed {
382 reason: format!(
383 "bms_flex_row inputs: cell_offsets must be monotone (offset[{}]={} > offset[{}]={})",
384 i,
385 self.cell_offsets[i],
386 i + 1,
387 self.cell_offsets[i + 1]
388 ),
389 });
390 }
391 }
392 Ok(())
393 }
394}
395
396#[cfg(target_os = "linux")]
407const CUDA_ROW_KERNEL_TEMPLATE: &str = r#"
408// One block per row. threadIdx.x parallelises per-cell sums.
409// Semantic calibration/finalization visits are generated from BmsFlexRowProgram.
410
411#define INV_TWO_PI 0.15915494309189535
412#define BMS_FLEX_ROW_THREADS /*__BMS_FLEX_ROW_THREADS__*/
413
414extern "C" __device__ __forceinline__ double atomic_add_f64(double *addr, double value) {
415 unsigned long long int *addr_as_ull = (unsigned long long int *)addr;
416 unsigned long long int old = *addr_as_ull;
417 unsigned long long int assumed;
418 do {
419 assumed = old;
420 double next = __longlong_as_double((long long int)assumed) + value;
421 old = atomicCAS(addr_as_ull, assumed, (unsigned long long int)__double_as_longlong(next));
422 } while (assumed != old);
423 return __longlong_as_double((long long int)old);
424}
425
426// `nan_fill_outputs`: thread-0-only path used when row inputs are degenerate
427// (`F_a` non-finite or non-positive). The status channel makes the host reject
428// the entire selected-GPU execution before any output can enter a cache.
429extern "C" __device__ __forceinline__ void
430nan_fill_outputs(int r,
431 int row,
432 double *out_neglog,
433 double *out_grad,
434 double *out_hess,
435 unsigned int *out_status) {
436 double nan_value = __longlong_as_double(0x7ff8000000000000ULL);
437 out_status[row] = 1U;
438 out_neglog[row] = nan_value;
439 size_t row_r = (size_t)row * (size_t)r;
440 for (int u = 0; u < r; ++u) {
441 out_grad[row_r + (size_t)u] = nan_value;
442 }
443 size_t rr = (size_t)r * (size_t)r;
444 size_t row_rr = (size_t)row * rr;
445 for (size_t idx = 0; idx < rr; ++idx) {
446 out_hess[row_rr + idx] = nan_value;
447 }
448}
449
450extern "C" __global__ void bms_flex_row_kernel(
451 int n_rows,
452 int r,
453 int p_h,
454 int p_w,
455 double s_f, // currently unused on device:
456 // host has already baked S_f
457 // into the cubic coefficients.
458 // Kept for diagnostic parity.
459 const double * __restrict__ row_q,
460 const double * __restrict__ row_b,
461 const double * __restrict__ row_mu1,
462 const double * __restrict__ row_mu2,
463 const double * __restrict__ row_zobs,
464 const double * __restrict__ row_y,
465 const double * __restrict__ row_w,
466 const unsigned int * __restrict__ cell_offsets,
467 const double * __restrict__ cell_c0,
468 const double * __restrict__ cell_c1,
469 const double * __restrict__ cell_c2,
470 const double * __restrict__ cell_c3,
471 const double * __restrict__ cell_a, // [n_cells, 4]
472 const double * __restrict__ cell_aa, // [n_cells, 4]
473 const double * __restrict__ cell_r, // [n_cells, r-1, 4]
474 const double * __restrict__ cell_ar, // [n_cells, r-1, 4]
475 const double * __restrict__ cell_sbb, // [n_cells, 4]
476 const double * __restrict__ cell_sbh, // [n_cells, p_h, 4]
477 const double * __restrict__ cell_sbw, // [n_cells, p_w, 4]
478 const double * __restrict__ cell_moments, // [n_cells, 10]
479 const double * __restrict__ row_chi,
480 const double * __restrict__ row_xi,
481 const double * __restrict__ row_rho, // [n_rows, r]
482 const double * __restrict__ row_tau, // [n_rows, r]
483 const double * __restrict__ row_ruv, // [n_rows, r*r]
484 const double * __restrict__ row_e_obs, // [n_rows] observed predictor VALUE
485 double * __restrict__ row_f_au, // [n_rows, r] general-width scratch
486 double * __restrict__ out_neglog,
487 double * __restrict__ out_grad,
488 double * __restrict__ out_hess,
489 unsigned int * __restrict__ out_status)
490{
491 int row = blockIdx.x;
492 if (row >= n_rows) return;
493 int tid = threadIdx.x;
494
495 // Width-general row scratch. Reuse the final output allocations in-place:
496 // F_u → a_u → gradient and F_uv → a_uv → Hessian. Only F_au needs one
497 // additional checked [n,r] device allocation.
498 size_t row_r_base = (size_t)row * (size_t)r;
499 size_t rr = (size_t)r * (size_t)r;
500 size_t row_rr_base = (size_t)row * rr;
501 double *F_u = out_grad + row_r_base;
502 double *F_au = row_f_au + row_r_base;
503 double *F_uv = out_hess + row_rr_base;
504 __shared__ double reduce_a[BMS_FLEX_ROW_THREADS];
505 __shared__ double reduce_b[BMS_FLEX_ROW_THREADS];
506 __shared__ double F_a_shared;
507 __shared__ double F_aa_shared;
508
509 // Zero scratch.
510 if (tid == 0) { F_a_shared = 0.0; F_aa_shared = 0.0; }
511 for (int u = tid; u < r; u += blockDim.x) {
512 F_u[u] = 0.0;
513 F_au[u] = 0.0;
514 }
515 for (size_t uv = (size_t)tid; uv < rr; uv += (size_t)blockDim.x) {
516 F_uv[uv] = 0.0;
517 }
518 __syncthreads();
519
520 // ── per-cell sweep ───────────────────────────────────────────────────
521 unsigned int cell_lo = cell_offsets[row];
522 unsigned int cell_hi = cell_offsets[row + 1];
523 unsigned int n_cells = cell_hi - cell_lo;
524
525 double local_Fa = 0.0;
526 double local_Faa = 0.0;
527
528 for (unsigned int local_c = (unsigned int)tid;
529 local_c < n_cells;
530 local_c += (unsigned int)blockDim.x) {
531 unsigned int c = cell_lo + local_c;
532
533 // Load cubic predictor coeffs C0..C3.
534 double C[4];
535 C[0] = cell_c0[c]; C[1] = cell_c1[c];
536 C[2] = cell_c2[c]; C[3] = cell_c3[c];
537
538 // Load m_0..m_9.
539 const double *m = cell_moments + (size_t)c * 10;
540
541 // T_n = κ · Σ_e C_e · m_{e+n}, n = 0..6.
542 // CPU parity: equivalent to the `eta_rs ⊗ moments` contraction in
543 // `cell_second_derivative_from_moments` after folding the
544 // cubic predictor.
545 double T[7];
546 #pragma unroll
547 for (int n = 0; n < 7; ++n) {
548 double acc = 0.0;
549 #pragma unroll
550 for (int e = 0; e < 4; ++e) {
551 acc = fma(C[e], m[e + n], acc);
552 }
553 T[n] = acc * INV_TWO_PI;
554 }
555
556 // D(R) = κ · Σ_k R_k · m_k.
557 // CPU parity: `cell_first_derivative_from_moments`.
558 // The argument is parenthesized because callers pass pointer
559 // ARITHMETIC (`D_OF(base + offset)`): without it the expansion binds
560 // as `base + offset[0]`, which NVRTC rejects ("pointer-to-object
561 // type" on the integer term) — the calibration-phase emitter was the
562 // first caller to hit this.
563 #define D_OF(R) (INV_TWO_PI * ((R)[0]*m[0] + (R)[1]*m[1] + (R)[2]*m[2] + (R)[3]*m[3]))
564
565 // Q(R, S) = Σ_{p,q} R_p · S_q · T_{p+q}.
566 // CPU parity: the `eta_rs` folded dot in
567 // `cell_second_derivative_from_moments`.
568 #define Q_OF(R, S) \
569 (((R)[0]*(S)[0])*T[0] + ((R)[0]*(S)[1] + (R)[1]*(S)[0])*T[1] \
570 + ((R)[0]*(S)[2] + (R)[1]*(S)[1] + (R)[2]*(S)[0])*T[2] \
571 + ((R)[0]*(S)[3] + (R)[1]*(S)[2] + (R)[2]*(S)[1] + (R)[3]*(S)[0])*T[3] \
572 + ((R)[1]*(S)[3] + (R)[2]*(S)[2] + (R)[3]*(S)[1])*T[4] \
573 + ((R)[2]*(S)[3] + (R)[3]*(S)[2])*T[5] \
574 + ((R)[3]*(S)[3])*T[6])
575
576 // The typed calibration schedule below consumes these primitive
577 // coefficient views through D_OF/Q_OF.
578 const double *A_c = cell_a + (size_t)c * 4;
579 const double *AA_c = cell_aa + (size_t)c * 4;
580 /*__BMS_FLEX_CALIBRATION_ORDER2__*/
581
582 #undef D_OF
583 #undef Q_OF
584 }
585
586 // Block reduction of local_Fa, local_Faa into shared.
587 reduce_a[tid] = local_Fa;
588 reduce_b[tid] = local_Faa;
589 __syncthreads();
590 for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
591 if (tid < stride) {
592 reduce_a[tid] += reduce_a[tid + stride];
593 reduce_b[tid] += reduce_b[tid + stride];
594 }
595 __syncthreads();
596 }
597 if (tid == 0) {
598 F_a_shared = reduce_a[0];
599 F_aa_shared = reduce_b[0];
600 }
601 __syncthreads();
602
603 // ── thread-0 finalisation: IFT + observed-point + Mills + writes ──────
604 if (tid != 0) return;
605
606 double F_a = F_a_shared;
607 double F_aa = F_aa_shared;
608 double mu_1 = row_mu1[row];
609 double mu_2 = row_mu2[row];
610
611 // q-row overrides.
612 // F_q = -mu_1 ; F_qq = -mu_2 ; F_qv = 0 (v > 0) ; F_aq = 0.
613 F_u[0] = -mu_1;
614 F_au[0] = 0.0;
615 // Zero the q-cross row/column of F_uv (u == 0 or v == 0), then plant -mu_2 at (0,0).
616 for (int v = 0; v < r; ++v) {
617 F_uv[(size_t)v] = 0.0;
618 F_uv[(size_t)v * (size_t)r] = 0.0;
619 }
620 F_uv[0] = -mu_2;
621
622 // Guard: degenerate F_a ⇒ NaN-fill this row's outputs.
623 if (!isfinite(F_a) || F_a <= 0.0) {
624 nan_fill_outputs(r, row, out_neglog, out_grad, out_hess, out_status);
625 return;
626 }
627 double inv_Fa = 1.0 / F_a;
628
629 // Storage consumed by the generated dependency-ordered finalizer. Both
630 // aliases overwrite their no-longer-needed calibration predecessors.
631 double *a_u = F_u;
632 double *a_uv = F_uv;
633 double chi = row_chi[row];
634 double xi = row_xi[row];
635 const double *rho = row_rho + (size_t)row * r;
636 const double *tau = row_tau + (size_t)row * r;
637 const double *ruv = row_ruv + row_rr_base;
638
639 // Probit Mills.
640 double y = row_y[row];
641 double w = row_w[row];
642 double s = 2.0 * y - 1.0;
643 // The "observed predictor" e_obs is the VALUE (degree-0 term) of the
644 // observed jet η(a(θ), θ; z_obs) — NOT `bar_e_u[0]`, which is the u=0
645 // FIRST-derivative jet (`chi·a_0 + rho_0 = dη_obs/dq`). The host packs
646 // the observed value directly in `row_e_obs[row]` (see
647 // `pack_bms_flex_row_kernel_inputs`, `eta_val = eval_coeff4_at(obs.coeff,
648 // z_obs)`), matching the CPU family `lower_bms_flex_row_order2_from_parts`
649 // which forms `signed_margin = s_y · eta_val`. #415 parity lock.
650 double e_obs = row_e_obs[row];
651 double m_arg = s * e_obs;
652 double log_cdf, lambda, probit_curvature;
653 log_ndtr_mills_curvature(m_arg, &log_cdf, &lambda, &probit_curvature);
654 double A_i = -w * s * lambda;
655 double B_i = w * probit_curvature;
656
657 out_neglog[row] = -w * log_cdf;
658 /*__BMS_FLEX_ORDER2_FINALIZER__*/
659 if (!isfinite(out_neglog[row])) {
660 out_status[row] = 2U;
661 }
662 for (int u = 0; u < r; ++u) {
663 if (!isfinite(out_grad[row_r_base + (size_t)u])) {
664 out_status[row] = 2U;
665 }
666 }
667 for (size_t uv = 0; uv < rr; ++uv) {
668 if (!isfinite(out_hess[row_rr_base + uv])) {
669 out_status[row] = 2U;
670 }
671 }
672}
673"#;
674
675#[cfg(target_os = "linux")]
676fn build_generated_row_kernel_source() -> String {
677 const CALIBRATION_MARKER: &str = " /*__BMS_FLEX_CALIBRATION_ORDER2__*/";
678 const FINALIZER_MARKER: &str = " /*__BMS_FLEX_ORDER2_FINALIZER__*/";
679
680 let (prefix, remainder) = CUDA_ROW_KERNEL_TEMPLATE
681 .split_once(CALIBRATION_MARKER)
682 .expect("CUDA row template must contain the calibration marker");
683 let (between, suffix) = remainder
684 .split_once(FINALIZER_MARKER)
685 .expect("CUDA row template must contain the finalizer marker");
686 let mut source = String::with_capacity(CUDA_ROW_KERNEL_TEMPLATE.len() + 16_000);
687 source.push_str(prefix);
688
689 BmsFlexRowProgram::try_for_each_calibration_order2_phase(
690 true,
691 |phase| -> Result<(), std::convert::Infallible> {
692 source.push_str(&format!(
693 " // canonical calibration phase: {phase:?}\n"
694 ));
695 match phase {
696 BmsFlexCalibrationOrder2Phase::InterceptFirst => {
697 source.push_str(" local_Fa += D_OF(A_c);\n");
698 }
699 BmsFlexCalibrationOrder2Phase::InterceptSecond => {
700 source.push_str(" local_Faa += D_OF(AA_c) - Q_OF(A_c, A_c);\n");
701 }
702 BmsFlexCalibrationOrder2Phase::PrimaryFirstAndInterceptSecond => {
703 source.push_str(
704 r#" for (int u = 1; u < r; ++u) {
705 const double *R_u = cell_r + ((size_t)c * (size_t)(r - 1) + (size_t)(u - 1)) * 4;
706 const double *AR_u = cell_ar + ((size_t)c * (size_t)(r - 1) + (size_t)(u - 1)) * 4;
707 atomic_add_f64(&F_u[u], D_OF(R_u));
708 atomic_add_f64(&F_au[u], D_OF(AR_u) - Q_OF(A_c, R_u));
709 }
710"#,
711 );
712 }
713 BmsFlexCalibrationOrder2Phase::PrimaryPairSecond => {
714 source.push_str(
715 r#" for (int u = 1; u < r; ++u) {
716 const double *R_u = cell_r + ((size_t)c * (size_t)(r - 1) + (size_t)(u - 1)) * 4;
717 for (int v = u; v < r; ++v) {
718 const double *R_v = cell_r + ((size_t)c * (size_t)(r - 1) + (size_t)(v - 1)) * 4;
719 double explicit_second = 0.0;
720 if (u == 1 && v == 1) {
721 explicit_second = D_OF(cell_sbb + (size_t)c * 4);
722 } else if (u == 1 && v < 2 + p_h) {
723 int j = v - 2;
724 explicit_second = D_OF(cell_sbh + ((size_t)c * (size_t)p_h + (size_t)j) * 4);
725 } else if (u == 1) {
726 int l = v - (2 + p_h);
727 explicit_second = D_OF(cell_sbw + ((size_t)c * (size_t)p_w + (size_t)l) * 4);
728 }
729 atomic_add_f64(&F_uv[(size_t)u * (size_t)r + (size_t)v], explicit_second - Q_OF(R_u, R_v));
730 }
731 }
732"#,
733 );
734 }
735 }
736 Ok(())
737 },
738 )
739 .expect("the infallible calibration phase emitter cannot fail");
740
741 source.push_str(between);
742 BmsFlexRowProgram::try_for_each_order2_finalizer_phase(
743 true,
744 |phase| -> Result<(), std::convert::Infallible> {
745 source.push_str(&format!(" // canonical finalizer phase: {phase:?}\n"));
746 match phase {
747 BmsFlexRowOrder2FinalizerPhase::ImplicitFirst => {
748 source.push_str(
749 r#" for (int u = 0; u < r; ++u) {
750 a_u[u] = -F_u[u] * inv_Fa;
751 }
752"#,
753 );
754 }
755 BmsFlexRowOrder2FinalizerPhase::ImplicitFirstComplete => {
756 source.push_str(" // Canonical implicit-first stage complete.\n");
757 }
758 BmsFlexRowOrder2FinalizerPhase::ImplicitSecond => {
759 source.push_str(
760 r#" for (int u = 0; u < r; ++u) {
761 for (int v = u; v < r; ++v) {
762 size_t uv = (size_t)u * (size_t)r + (size_t)v;
763 size_t vu = (size_t)v * (size_t)r + (size_t)u;
764 double term = F_uv[uv]
765 + F_au[v] * a_u[u]
766 + F_au[u] * a_u[v]
767 + F_aa * a_u[u] * a_u[v];
768 double value = -term * inv_Fa;
769 a_uv[uv] = value;
770 a_uv[vu] = value;
771 }
772 }
773"#,
774 );
775 }
776 BmsFlexRowOrder2FinalizerPhase::ObservedFirst => {
777 source.push_str(" // Observed first derivatives are derived on demand.\n");
778 }
779 BmsFlexRowOrder2FinalizerPhase::ObservedScoreSensitivity => {
780 source.push_str(
781 " // Score sensitivity has no Stage-2 device output channel.\n",
782 );
783 }
784 BmsFlexRowOrder2FinalizerPhase::ObservedSecond => {
785 source.push_str(
786 r#" for (int u = 0; u < r; ++u) {
787 for (int v = u; v < r; ++v) {
788 size_t uv = (size_t)u * (size_t)r + (size_t)v;
789 size_t vu = (size_t)v * (size_t)r + (size_t)u;
790 double bar_e_u = chi * a_u[u] + rho[u];
791 double bar_e_v = chi * a_u[v] + rho[v];
792 double observed_second = chi * a_uv[uv]
793 + xi * a_u[u] * a_u[v]
794 + tau[u] * a_u[v]
795 + a_u[u] * tau[v]
796 + ruv[uv];
797 double hessian_value =
798 B_i * bar_e_u * bar_e_v + A_i * observed_second;
799 out_hess[row_rr_base + uv] = hessian_value;
800 out_hess[row_rr_base + vu] = hessian_value;
801 }
802 }
803"#,
804 );
805 }
806 BmsFlexRowOrder2FinalizerPhase::NegLogFirst => {
807 source.push_str(
808 r#" for (int u = 0; u < r; ++u) {
809 double bar_e_u = chi * a_u[u] + rho[u];
810 out_grad[row_r_base + (size_t)u] = A_i * bar_e_u;
811 }
812"#,
813 );
814 }
815 }
816 Ok(())
817 },
818 )
819 .expect("the infallible finalizer phase emitter cannot fail");
820 source.push_str(suffix);
821 source.replace(
822 "/*__BMS_FLEX_ROW_THREADS__*/",
823 &ROW_KERNEL_THREADS.to_string(),
824 )
825}
826
827#[cfg(target_os = "linux")]
828pub(crate) fn generated_row_kernel_source() -> &'static str {
829 static SOURCE: OnceLock<String> = OnceLock::new();
830 SOURCE.get_or_init(build_generated_row_kernel_source)
831}
832
833#[inline]
839pub(crate) fn s_f_diagnostic_finite(inputs: &BmsFlexRowKernelInputs<'_>) -> bool {
840 inputs.s_f.is_finite() && inputs.s_f > 0.0
841}
842
843#[cfg(target_os = "linux")]
844pub(crate) struct RowKernelBackend {
845 pub(crate) stream: Arc<CudaStream>,
846 pub(crate) module: Arc<CudaModule>,
847}
848
849#[cfg(target_os = "linux")]
850impl RowKernelBackend {
851 pub(crate) fn probe() -> Result<&'static Self, GpuError> {
852 static BACKEND: OnceLock<Result<RowKernelBackend, GpuError>> = OnceLock::new();
853 BACKEND
854 .get_or_init(|| {
855 gam_gpu::backend_probe::probe_backend_with_compile("bms_flex_row", |parts| {
856 let row_kernel_source = [
857 gam_gpu::numerics_device::PROBIT_NUMERICS_CU,
858 generated_row_kernel_source(),
859 ]
860 .concat();
861 let ptx = gam_gpu::device_cache::compile_ptx_arch(&row_kernel_source).map_err(
872 |err| GpuError::DriverCallFailed {
873 reason: format!("bms_flex_row NVRTC compile failed: {err}"),
874 },
875 )?;
876 let module =
877 parts
878 .ctx
879 .load_module(ptx)
880 .map_err(|err| GpuError::DriverCallFailed {
881 reason: format!("bms_flex_row module load failed: {err}"),
882 })?;
883 Ok(RowKernelBackend {
884 stream: parts.stream.clone(),
885 module,
886 })
887 })
888 })
889 .as_ref()
890 .map_err(GpuError::clone)
891 }
892}
893
894pub(crate) fn launch_bms_flex_row_kernel(
899 inputs: BmsFlexRowKernelInputs<'_>,
900) -> Result<BmsFlexRowKernelOutputs, GpuError> {
901 inputs.validate()?;
902 if !s_f_diagnostic_finite(&inputs) {
903 return Err(GpuError::DriverCallFailed {
904 reason: format!(
905 "bms_flex_row inputs: s_f must be positive and finite, got {}",
906 inputs.s_f
907 ),
908 });
909 }
910
911 #[cfg(target_os = "linux")]
912 {
913 launch_linux(inputs)
914 }
915 #[cfg(not(target_os = "linux"))]
916 {
917 Err(GpuError::DriverLibraryUnavailable {
918 reason: "bms_flex_row GPU kernel is Linux-only".to_string(),
919 })
920 }
921}
922
923#[cfg(target_os = "linux")]
924pub(crate) fn launch_linux(
925 inputs: BmsFlexRowKernelInputs<'_>,
926) -> Result<BmsFlexRowKernelOutputs, GpuError> {
927 let backend = RowKernelBackend::probe()?;
928 let stream = &backend.stream;
929
930 let upload_f64 = |slice: &[f64], label: &str| {
931 stream
932 .clone_htod(slice)
933 .map_err(|err| GpuError::DriverCallFailed {
934 reason: format!("bms_flex_row upload {label}: {err}"),
935 })
936 };
937 let upload_u32 = |slice: &[u32], label: &str| {
938 stream
939 .clone_htod(slice)
940 .map_err(|err| GpuError::DriverCallFailed {
941 reason: format!("bms_flex_row upload {label}: {err}"),
942 })
943 };
944
945 let d_q = upload_f64(inputs.q, "q")?;
946 let d_b = upload_f64(inputs.b, "b")?;
947 let d_mu1 = upload_f64(inputs.mu_1, "mu_1")?;
948 let d_mu2 = upload_f64(inputs.mu_2, "mu_2")?;
949 let d_zobs = upload_f64(inputs.z_obs, "z_obs")?;
950 let d_y = upload_f64(inputs.y, "y")?;
951 let d_w = upload_f64(inputs.w, "w")?;
952 let d_offsets = upload_u32(inputs.cell_offsets, "cell_offsets")?;
953 let d_c0 = upload_f64(inputs.cell_c0, "cell_c0")?;
954 let d_c1 = upload_f64(inputs.cell_c1, "cell_c1")?;
955 let d_c2 = upload_f64(inputs.cell_c2, "cell_c2")?;
956 let d_c3 = upload_f64(inputs.cell_c3, "cell_c3")?;
957 let d_a = upload_f64(inputs.cell_a, "cell_a")?;
958 let d_aa = upload_f64(inputs.cell_aa, "cell_aa")?;
959 let d_r = upload_f64(inputs.cell_r, "cell_r")?;
960 let d_ar = upload_f64(inputs.cell_ar, "cell_ar")?;
961 let d_sbb = upload_f64(inputs.cell_sbb, "cell_sbb")?;
962 let d_sbh = upload_f64(inputs.cell_sbh, "cell_sbh")?;
963 let d_sbw = upload_f64(inputs.cell_sbw, "cell_sbw")?;
964 let owned_host_moments: CudaSlice<f64>;
968 let d_moments_ref: &CudaSlice<f64> = match &inputs.cell_moments {
969 CellMomentsSource::Host(slice) => {
970 owned_host_moments = upload_f64(slice, "cell_moments")?;
971 &owned_host_moments
972 }
973 CellMomentsSource::Device(d) => *d,
974 };
975 let d_chi = upload_f64(inputs.chi_obs, "chi_obs")?;
976 let d_xi = upload_f64(inputs.xi_obs, "xi_obs")?;
977 let d_rho = upload_f64(inputs.rho_u, "rho_u")?;
978 let d_tau = upload_f64(inputs.tau_u, "tau_u")?;
979 let d_ruv = upload_f64(inputs.r_uv, "r_uv")?;
980 let d_e_obs = upload_f64(inputs.e_obs, "e_obs")?;
981
982 let n = inputs.n_rows;
983 let r = inputs.r;
984 let nr = checked_shape_len("launch [n,r]", &[n, r])?;
985 let nrr = checked_shape_len("launch [n,r,r]", &[n, r, r])?;
986 let mut d_neglog = stream
987 .alloc_zeros::<f64>(n)
988 .map_err(|err| GpuError::DriverCallFailed {
989 reason: format!("bms_flex_row alloc neglog: {err}"),
990 })?;
991 let mut d_grad = stream
992 .alloc_zeros::<f64>(nr)
993 .map_err(|err| GpuError::DriverCallFailed {
994 reason: format!("bms_flex_row alloc grad: {err}"),
995 })?;
996 let mut d_hess = stream
997 .alloc_zeros::<f64>(nrr)
998 .map_err(|err| GpuError::DriverCallFailed {
999 reason: format!("bms_flex_row alloc hess: {err}"),
1000 })?;
1001 let mut d_f_au = stream
1002 .alloc_zeros::<f64>(nr)
1003 .map_err(|err| GpuError::DriverCallFailed {
1004 reason: format!("bms_flex_row alloc F_au scratch: {err}"),
1005 })?;
1006 let mut d_status = stream
1007 .alloc_zeros::<u32>(n)
1008 .map_err(|err| GpuError::DriverCallFailed {
1009 reason: format!("bms_flex_row alloc status: {err}"),
1010 })?;
1011
1012 let func = backend
1013 .module
1014 .load_function("bms_flex_row_kernel")
1015 .map_err(|err| GpuError::DriverCallFailed {
1016 reason: format!("bms_flex_row load_function: {err}"),
1017 })?;
1018
1019 let n_u32 = u32::try_from(n).map_err(|_| GpuError::DriverCallFailed {
1020 reason: format!("bms_flex_row: n_rows={n} exceeds CUDA grid range"),
1021 })?;
1022 let cfg = LaunchConfig {
1023 grid_dim: (n_u32, 1, 1),
1024 block_dim: (ROW_KERNEL_THREADS, 1, 1),
1025 shared_mem_bytes: 0,
1026 };
1027 let n_i32 = i32::try_from(n).map_err(|_| GpuError::DriverCallFailed {
1028 reason: format!("bms_flex_row: n_rows={n} exceeds i32 range"),
1029 })?;
1030 let r_i32 = i32::try_from(r).map_err(|_| GpuError::DriverCallFailed {
1031 reason: format!("bms_flex_row: r={r} exceeds i32 range"),
1032 })?;
1033 let p_h_i32 = i32::try_from(inputs.p_h).map_err(|_| GpuError::DriverCallFailed {
1034 reason: format!("bms_flex_row: p_h={} exceeds i32 range", inputs.p_h),
1035 })?;
1036 let p_w_i32 = i32::try_from(inputs.p_w).map_err(|_| GpuError::DriverCallFailed {
1037 reason: format!("bms_flex_row: p_w={} exceeds i32 range", inputs.p_w),
1038 })?;
1039 let s_f = inputs.s_f;
1040
1041 let mut builder = stream.launch_builder(&func);
1042 builder
1043 .arg(&n_i32)
1044 .arg(&r_i32)
1045 .arg(&p_h_i32)
1046 .arg(&p_w_i32)
1047 .arg(&s_f)
1048 .arg(&d_q)
1049 .arg(&d_b)
1050 .arg(&d_mu1)
1051 .arg(&d_mu2)
1052 .arg(&d_zobs)
1053 .arg(&d_y)
1054 .arg(&d_w)
1055 .arg(&d_offsets)
1056 .arg(&d_c0)
1057 .arg(&d_c1)
1058 .arg(&d_c2)
1059 .arg(&d_c3)
1060 .arg(&d_a)
1061 .arg(&d_aa)
1062 .arg(&d_r)
1063 .arg(&d_ar)
1064 .arg(&d_sbb)
1065 .arg(&d_sbh)
1066 .arg(&d_sbw)
1067 .arg(d_moments_ref)
1068 .arg(&d_chi)
1069 .arg(&d_xi)
1070 .arg(&d_rho)
1071 .arg(&d_tau)
1072 .arg(&d_ruv)
1073 .arg(&d_e_obs)
1074 .arg(&mut d_f_au)
1075 .arg(&mut d_neglog)
1076 .arg(&mut d_grad)
1077 .arg(&mut d_hess)
1078 .arg(&mut d_status);
1079
1080 unsafe { builder.launch(cfg) }.map_err(|err| GpuError::DriverCallFailed {
1087 reason: format!("bms_flex_row launch: {err}"),
1088 })?;
1089 stream
1090 .synchronize()
1091 .map_err(|err| GpuError::DriverCallFailed {
1092 reason: format!("bms_flex_row synchronize: {err}"),
1093 })?;
1094
1095 let status = stream
1096 .clone_dtoh(&d_status)
1097 .map_err(|err| GpuError::DriverCallFailed {
1098 reason: format!("bms_flex_row download status: {err}"),
1099 })?;
1100 if let Some((row, code)) = status
1101 .iter()
1102 .copied()
1103 .enumerate()
1104 .find(|(_, code)| *code != 0)
1105 {
1106 return Err(GpuError::DriverCallFailed {
1107 reason: format!("bms_flex_row rejected non-finite row {row} with status {code}"),
1108 });
1109 }
1110
1111 let neglog = stream
1112 .clone_dtoh(&d_neglog)
1113 .map_err(|err| GpuError::DriverCallFailed {
1114 reason: format!("bms_flex_row download neglog: {err}"),
1115 })?;
1116 let grad = stream
1117 .clone_dtoh(&d_grad)
1118 .map_err(|err| GpuError::DriverCallFailed {
1119 reason: format!("bms_flex_row download grad: {err}"),
1120 })?;
1121 let hess = stream
1122 .clone_dtoh(&d_hess)
1123 .map_err(|err| GpuError::DriverCallFailed {
1124 reason: format!("bms_flex_row download hess: {err}"),
1125 })?;
1126
1127 Ok(BmsFlexRowKernelOutputs { neglog, grad, hess })
1128}
1129
1130#[cfg(target_os = "linux")]
1182#[derive(Clone, Debug)]
1183pub(crate) struct BmsFlexBlockLayout {
1184 pub p_m: usize,
1185 pub p_g: usize,
1186 pub h: Option<std::ops::Range<usize>>,
1187 pub w: Option<std::ops::Range<usize>>,
1188 pub p_total: usize,
1189}
1190
1191#[cfg(target_os = "linux")]
1194#[derive(Clone, Debug)]
1195pub(crate) struct BmsFlexPrimaryLayout {
1196 pub h: Option<std::ops::Range<usize>>,
1197 pub w: Option<std::ops::Range<usize>>,
1198 pub r: usize,
1199}
1200
1201#[cfg(target_os = "linux")]
1207pub(crate) const HVP_ROWS_PER_CTA: u32 = 256;
1208
1209#[cfg(target_os = "linux")]
1211pub(crate) const HVP_THREADS: u32 = 128;
1212
1213#[cfg(target_os = "linux")]
1218pub(crate) const REDUCTION_THREADS: u32 = 256;
1219
1220#[cfg(target_os = "linux")]
1225pub(crate) const BMS_FLEX_ROW_HVP_MAX_RHS: usize = 8;
1226
1227#[cfg(target_os = "linux")]
1236pub struct DeviceResidentRowHess {
1237 pub(crate) neglog: CudaSlice<f64>,
1240 pub(crate) grad: CudaSlice<f64>,
1243 pub(crate) hess: CudaSlice<f64>,
1247 pub(crate) marginal_design: CudaSlice<f64>,
1248 pub(crate) logslope_design: CudaSlice<f64>,
1249 pub(crate) n: usize,
1250 pub(crate) r: usize,
1251 pub(crate) block: BmsFlexBlockLayout,
1252 pub(crate) primary: BmsFlexPrimaryLayout,
1253 pub(crate) bytes: u64,
1255}
1256
1257#[cfg(target_os = "linux")]
1260pub(crate) struct BmsFlexDeviceJointGradient {
1261 pub(crate) log_likelihood: f64,
1262 pub(crate) gradient: Vec<f64>,
1263}
1264
1265#[cfg(target_os = "linux")]
1266impl std::fmt::Debug for DeviceResidentRowHess {
1267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1268 f.debug_struct("DeviceResidentRowHess")
1269 .field("n", &self.n)
1270 .field("r", &self.r)
1271 .field("p_total", &self.block.p_total)
1272 .field("bytes", &self.bytes)
1273 .finish()
1274 }
1275}
1276
1277#[cfg(target_os = "linux")]
1280pub(crate) fn num_hvp_chunks(n: usize) -> usize {
1281 n.div_ceil(HVP_ROWS_PER_CTA as usize)
1282}
1283
1284#[cfg(target_os = "linux")]
1287pub(crate) const HVP_KERNEL_SOURCE: &str = r#"
1288// CPU parity reference: cpu_oracle_bms_flex_row_hvp / cpu_oracle_bms_flex_row_diagonal
1289// in this module.
1290
1291#define MAX_MULTI_RHS 8
1292
1293__device__ __forceinline__ double bms_flex_primary_direction(
1294 int primary_idx,
1295 int h_block_start,
1296 int h_block_len,
1297 int w_block_start,
1298 int w_block_len,
1299 int h_primary_start,
1300 int w_primary_start,
1301 double direction_q,
1302 double direction_g,
1303 const double * __restrict__ v)
1304{
1305 if (primary_idx == 0) return direction_q;
1306 if (primary_idx == 1) return direction_g;
1307 if (primary_idx >= h_primary_start && primary_idx < h_primary_start + h_block_len) {
1308 return v[h_block_start + primary_idx - h_primary_start];
1309 }
1310 if (primary_idx >= w_primary_start && primary_idx < w_primary_start + w_block_len) {
1311 return v[w_block_start + primary_idx - w_primary_start];
1312 }
1313 return 0.0;
1314}
1315
1316extern "C" __global__ void bms_flex_row_hvp_partial(
1317 int n_rows,
1318 int r,
1319 int p_m,
1320 int p_g,
1321 int p_total,
1322 int h_block_start,
1323 int h_block_len,
1324 int w_block_start,
1325 int w_block_len,
1326 int h_primary_start,
1327 int w_primary_start,
1328 int rows_per_cta,
1329 const double * __restrict__ row_hessians, // [n, r*r]
1330 const double * __restrict__ marginal_design, // [n, p_m] row-major
1331 const double * __restrict__ logslope_design, // [n, p_g] row-major
1332 const double * __restrict__ v, // [p_total]
1333 double * __restrict__ partial) // [num_chunks, p_total]
1334{
1335 int chunk = blockIdx.x;
1336 int tid = threadIdx.x;
1337 int row_lo = chunk * rows_per_cta;
1338 int remaining_rows = n_rows - row_lo;
1339 int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1340
1341 // Zero this chunk's partial slice cooperatively.
1342 double *out = partial + (size_t)chunk * (size_t)p_total;
1343 for (int j = tid; j < p_total; j += blockDim.x) {
1344 out[j] = 0.0;
1345 }
1346 __syncthreads();
1347
1348 // Width-general scratch: only the two design directions/actions are
1349 // shared. Every h/w direction is read directly from v, and the thread
1350 // owning primary coordinate u accumulates that coordinate's action.
1351 __shared__ double direction_q;
1352 __shared__ double direction_g;
1353 __shared__ double action_q;
1354 __shared__ double action_g;
1355 __shared__ double dot_reduce[128];
1356
1357 for (int row = row_lo; row < row_hi; ++row) {
1358 const double *mrow = marginal_design + (size_t)row * (size_t)p_m;
1359 const double *grow = logslope_design + (size_t)row * (size_t)p_g;
1360 const double *Hrow = row_hessians + (size_t)row * (size_t)r * (size_t)r;
1361
1362 // row_dir[0] = mrow · v[0..p_m]
1363 double local = 0.0;
1364 for (int j = tid; j < p_m; j += blockDim.x) {
1365 local += mrow[j] * v[j];
1366 }
1367 dot_reduce[tid] = local;
1368 __syncthreads();
1369 for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
1370 if (tid < stride) dot_reduce[tid] += dot_reduce[tid + stride];
1371 __syncthreads();
1372 }
1373 if (tid == 0) direction_q = dot_reduce[0];
1374
1375 // row_dir[1] = grow · v[p_m..p_m+p_g]
1376 local = 0.0;
1377 for (int j = tid; j < p_g; j += blockDim.x) {
1378 local += grow[j] * v[p_m + j];
1379 }
1380 dot_reduce[tid] = local;
1381 __syncthreads();
1382 for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
1383 if (tid < stride) dot_reduce[tid] += dot_reduce[tid + stride];
1384 __syncthreads();
1385 }
1386 if (tid == 0) direction_g = dot_reduce[0];
1387 __syncthreads();
1388
1389 for (int u = tid; u < r; u += blockDim.x) {
1390 double acc = 0.0;
1391 for (int vv = 0; vv < r; ++vv) {
1392 double row_direction = bms_flex_primary_direction(
1393 vv,
1394 h_block_start, h_block_len,
1395 w_block_start, w_block_len,
1396 h_primary_start, w_primary_start,
1397 direction_q, direction_g, v);
1398 acc += Hrow[(size_t)u * (size_t)r + (size_t)vv] * row_direction;
1399 }
1400 if (u == 0) {
1401 action_q = acc;
1402 } else if (u == 1) {
1403 action_g = acc;
1404 } else if (u >= h_primary_start && u < h_primary_start + h_block_len) {
1405 out[h_block_start + u - h_primary_start] += acc;
1406 } else if (u >= w_primary_start && u < w_primary_start + w_block_len) {
1407 out[w_block_start + u - w_primary_start] += acc;
1408 }
1409 }
1410 __syncthreads();
1411
1412 // Pull back into joint β slot.
1413 double a0 = action_q;
1414 for (int j = tid; j < p_m; j += blockDim.x) {
1415 out[j] += a0 * mrow[j];
1416 }
1417 double a1 = action_g;
1418 for (int j = tid; j < p_g; j += blockDim.x) {
1419 out[p_m + j] += a1 * grow[j];
1420 }
1421 __syncthreads();
1422 }
1423}
1424
1425extern "C" __global__ void bms_flex_row_hvp_reduce(
1426 int num_chunks,
1427 int p_total,
1428 const double * __restrict__ partial, // [num_chunks, p_total]
1429 double * __restrict__ out) // [p_total]
1430{
1431 int j = blockIdx.x * blockDim.x + threadIdx.x;
1432 if (j >= p_total) return;
1433 double acc = 0.0;
1434 for (int c = 0; c < num_chunks; ++c) {
1435 acc += partial[(size_t)c * (size_t)p_total + (size_t)j];
1436 }
1437 out[j] = acc;
1438}
1439
1440extern "C" __global__ void bms_flex_row_joint_gradient_partial(
1441 int n_rows,
1442 int r,
1443 int p_m,
1444 int p_g,
1445 int p_total,
1446 int h_block_start,
1447 int h_block_len,
1448 int w_block_start,
1449 int w_block_len,
1450 int h_primary_start,
1451 int w_primary_start,
1452 int rows_per_cta,
1453 const double * __restrict__ row_neglog, // [n]
1454 const double * __restrict__ row_grad, // [n, r]
1455 const double * __restrict__ marginal_design, // [n, p_m]
1456 const double * __restrict__ logslope_design, // [n, p_g]
1457 double * __restrict__ partial) // [num_chunks, 1+p_total]
1458{
1459 int chunk = blockIdx.x;
1460 int tid = threadIdx.x;
1461 int row_lo = chunk * rows_per_cta;
1462 int remaining_rows = n_rows - row_lo;
1463 int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1464 int output_width = p_total + 1;
1465 double *out = partial + (size_t)chunk * (size_t)output_width;
1466
1467 // One thread owns each output coordinate for the whole row chunk. The
1468 // inner row loop therefore has a fixed order and needs no atomics.
1469 for (int output_idx = tid; output_idx < output_width; output_idx += blockDim.x) {
1470 double acc = 0.0;
1471 if (output_idx == 0) {
1472 for (int row = row_lo; row < row_hi; ++row) {
1473 acc -= row_neglog[row];
1474 }
1475 } else {
1476 int beta_idx = output_idx - 1;
1477 if (beta_idx < p_m) {
1478 for (int row = row_lo; row < row_hi; ++row) {
1479 acc -= row_grad[(size_t)row * (size_t)r]
1480 * marginal_design[(size_t)row * (size_t)p_m + (size_t)beta_idx];
1481 }
1482 } else if (beta_idx < p_m + p_g) {
1483 int j = beta_idx - p_m;
1484 for (int row = row_lo; row < row_hi; ++row) {
1485 acc -= row_grad[(size_t)row * (size_t)r + 1]
1486 * logslope_design[(size_t)row * (size_t)p_g + (size_t)j];
1487 }
1488 } else if (beta_idx >= h_block_start && beta_idx < h_block_start + h_block_len) {
1489 int primary_idx = h_primary_start + beta_idx - h_block_start;
1490 for (int row = row_lo; row < row_hi; ++row) {
1491 acc -= row_grad[(size_t)row * (size_t)r + (size_t)primary_idx];
1492 }
1493 } else if (beta_idx >= w_block_start && beta_idx < w_block_start + w_block_len) {
1494 int primary_idx = w_primary_start + beta_idx - w_block_start;
1495 for (int row = row_lo; row < row_hi; ++row) {
1496 acc -= row_grad[(size_t)row * (size_t)r + (size_t)primary_idx];
1497 }
1498 }
1499 }
1500 out[output_idx] = acc;
1501 }
1502}
1503
1504extern "C" __global__ void bms_flex_row_joint_gradient_reduce(
1505 int num_chunks,
1506 int output_width,
1507 const double * __restrict__ partial, // [num_chunks, output_width]
1508 double * __restrict__ out) // [output_width]
1509{
1510 int j = blockIdx.x * blockDim.x + threadIdx.x;
1511 if (j >= output_width) return;
1512 double acc = 0.0;
1513 for (int c = 0; c < num_chunks; ++c) {
1514 acc += partial[(size_t)c * (size_t)output_width + (size_t)j];
1515 }
1516 out[j] = acc;
1517}
1518
1519extern "C" __global__ void bms_flex_row_hvp_multi_partial(
1520 int n_rows,
1521 int r,
1522 int p_m,
1523 int p_g,
1524 int p_total,
1525 int h_block_start,
1526 int h_block_len,
1527 int w_block_start,
1528 int w_block_len,
1529 int h_primary_start,
1530 int w_primary_start,
1531 int rows_per_cta,
1532 int rhs_count,
1533 const double * __restrict__ row_hessians, // [n, r*r]
1534 const double * __restrict__ marginal_design, // [n, p_m]
1535 const double * __restrict__ logslope_design, // [n, p_g]
1536 const double * __restrict__ v_rhs, // [rhs_count, p_total]
1537 double * __restrict__ partial) // [rhs_count, num_chunks, p_total]
1538{
1539 int chunk = blockIdx.x;
1540 int tid = threadIdx.x;
1541 int row_lo = chunk * rows_per_cta;
1542 int remaining_rows = n_rows - row_lo;
1543 int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1544
1545 int num_chunks = 1 + (n_rows - 1) / rows_per_cta;
1546 for (int idx = tid; idx < rhs_count * p_total; idx += blockDim.x) {
1547 int rhs = idx / p_total;
1548 int j = idx - rhs * p_total;
1549 partial[((size_t)rhs * (size_t)num_chunks + (size_t)chunk) * (size_t)p_total + (size_t)j] = 0.0;
1550 }
1551 __syncthreads();
1552
1553 __shared__ double direction_q[MAX_MULTI_RHS];
1554 __shared__ double direction_g[MAX_MULTI_RHS];
1555 __shared__ double action_q[MAX_MULTI_RHS];
1556 __shared__ double action_g[MAX_MULTI_RHS];
1557 __shared__ double dot_reduce[128];
1558
1559 for (int row = row_lo; row < row_hi; ++row) {
1560 const double *mrow = marginal_design + (size_t)row * (size_t)p_m;
1561 const double *grow = logslope_design + (size_t)row * (size_t)p_g;
1562 const double *Hrow = row_hessians + (size_t)row * (size_t)r * (size_t)r;
1563
1564 for (int rhs = 0; rhs < rhs_count; ++rhs) {
1565 const double *v = v_rhs + (size_t)rhs * (size_t)p_total;
1566
1567 double local = 0.0;
1568 for (int j = tid; j < p_m; j += blockDim.x) {
1569 local += mrow[j] * v[j];
1570 }
1571 dot_reduce[tid] = local;
1572 __syncthreads();
1573 for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
1574 if (tid < stride) dot_reduce[tid] += dot_reduce[tid + stride];
1575 __syncthreads();
1576 }
1577 if (tid == 0) direction_q[rhs] = dot_reduce[0];
1578
1579 local = 0.0;
1580 for (int j = tid; j < p_g; j += blockDim.x) {
1581 local += grow[j] * v[p_m + j];
1582 }
1583 dot_reduce[tid] = local;
1584 __syncthreads();
1585 for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
1586 if (tid < stride) dot_reduce[tid] += dot_reduce[tid + stride];
1587 __syncthreads();
1588 }
1589 if (tid == 0) direction_g[rhs] = dot_reduce[0];
1590 __syncthreads();
1591 }
1592
1593 size_t total_actions = (size_t)rhs_count * (size_t)r;
1594 for (size_t idx = (size_t)tid; idx < total_actions; idx += (size_t)blockDim.x) {
1595 int rhs = (int)(idx / (size_t)r);
1596 int u = (int)(idx - (size_t)rhs * (size_t)r);
1597 const double *v = v_rhs + (size_t)rhs * (size_t)p_total;
1598 double *out = partial + ((size_t)rhs * (size_t)num_chunks + (size_t)chunk) * (size_t)p_total;
1599 double acc = 0.0;
1600 for (int vv = 0; vv < r; ++vv) {
1601 double row_direction = bms_flex_primary_direction(
1602 vv,
1603 h_block_start, h_block_len,
1604 w_block_start, w_block_len,
1605 h_primary_start, w_primary_start,
1606 direction_q[rhs], direction_g[rhs], v);
1607 acc += Hrow[(size_t)u * (size_t)r + (size_t)vv] * row_direction;
1608 }
1609 if (u == 0) {
1610 action_q[rhs] = acc;
1611 } else if (u == 1) {
1612 action_g[rhs] = acc;
1613 } else if (u >= h_primary_start && u < h_primary_start + h_block_len) {
1614 out[h_block_start + u - h_primary_start] += acc;
1615 } else if (u >= w_primary_start && u < w_primary_start + w_block_len) {
1616 out[w_block_start + u - w_primary_start] += acc;
1617 }
1618 }
1619 __syncthreads();
1620
1621 for (int rhs = 0; rhs < rhs_count; ++rhs) {
1622 double *out = partial + ((size_t)rhs * (size_t)num_chunks + (size_t)chunk) * (size_t)p_total;
1623 double a0 = action_q[rhs];
1624 for (int j = tid; j < p_m; j += blockDim.x) {
1625 out[j] += a0 * mrow[j];
1626 }
1627 double a1 = action_g[rhs];
1628 for (int j = tid; j < p_g; j += blockDim.x) {
1629 out[p_m + j] += a1 * grow[j];
1630 }
1631 __syncthreads();
1632 }
1633 }
1634}
1635
1636extern "C" __global__ void bms_flex_row_hvp_multi_reduce(
1637 int num_chunks,
1638 int p_total,
1639 int rhs_count,
1640 const double * __restrict__ partial, // [rhs_count, num_chunks, p_total]
1641 double * __restrict__ out) // [rhs_count, p_total]
1642{
1643 int idx = blockIdx.x * blockDim.x + threadIdx.x;
1644 int total = rhs_count * p_total;
1645 if (idx >= total) return;
1646 int rhs = idx / p_total;
1647 int j = idx - rhs * p_total;
1648 double acc = 0.0;
1649 for (int c = 0; c < num_chunks; ++c) {
1650 acc += partial[((size_t)rhs * (size_t)num_chunks + (size_t)c) * (size_t)p_total + (size_t)j];
1651 }
1652 out[(size_t)rhs * (size_t)p_total + (size_t)j] = acc;
1653}
1654
1655extern "C" __global__ void bms_flex_row_diag_partial(
1656 int n_rows,
1657 int r,
1658 int p_m,
1659 int p_g,
1660 int p_total,
1661 int h_block_start,
1662 int h_block_len,
1663 int w_block_start,
1664 int w_block_len,
1665 int h_primary_start,
1666 int w_primary_start,
1667 int rows_per_cta,
1668 const double * __restrict__ row_hessians,
1669 const double * __restrict__ marginal_design,
1670 const double * __restrict__ logslope_design,
1671 double * __restrict__ partial)
1672{
1673 int chunk = blockIdx.x;
1674 int tid = threadIdx.x;
1675 int row_lo = chunk * rows_per_cta;
1676 int remaining_rows = n_rows - row_lo;
1677 int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1678
1679 double *out = partial + (size_t)chunk * (size_t)p_total;
1680 for (int j = tid; j < p_total; j += blockDim.x) {
1681 out[j] = 0.0;
1682 }
1683 __syncthreads();
1684
1685 for (int row = row_lo; row < row_hi; ++row) {
1686 const double *mrow = marginal_design + (size_t)row * (size_t)p_m;
1687 const double *grow = logslope_design + (size_t)row * (size_t)p_g;
1688 const double *Hrow = row_hessians + (size_t)row * (size_t)r * (size_t)r;
1689 double h00 = Hrow[0];
1690 double h11 = Hrow[(size_t)r + 1U];
1691 for (int j = tid; j < p_m; j += blockDim.x) {
1692 double v = mrow[j];
1693 out[j] += h00 * v * v;
1694 }
1695 for (int j = tid; j < p_g; j += blockDim.x) {
1696 double v = grow[j];
1697 out[p_m + j] += h11 * v * v;
1698 }
1699 if (tid == 0) {
1700 for (int k = 0; k < h_block_len; ++k) {
1701 int ii = h_primary_start + k;
1702 out[h_block_start + k] +=
1703 Hrow[(size_t)ii * (size_t)r + (size_t)ii];
1704 }
1705 for (int k = 0; k < w_block_len; ++k) {
1706 int ii = w_primary_start + k;
1707 out[w_block_start + k] +=
1708 Hrow[(size_t)ii * (size_t)r + (size_t)ii];
1709 }
1710 }
1711 __syncthreads();
1712 }
1713}
1714
1715// ────────────────────────────────────────────────────────────────────────
1716// Phase 6 — dense joint-Hessian block kernel for the debug / exact-REML
1717// route. Materialises the full `[p_total, p_total]` row-major joint H
1718// from the per-row r×r Hessian via the P_i pullback. NOT the default
1719// Newton path: production Newton uses HVP (Phase 2/3); this kernel exists
1720// for exact-REML logdet / dense-H comparisons / diagnostic dumps where the
1721// caller genuinely needs the dense matrix on the device.
1722//
1723// Per-CTA partial: each CTA owns a contiguous chunk of rows
1724// `[chunk*rows_per_cta, (chunk+1)*rows_per_cta)`. Inside the CTA the
1725// per-row pullback computes `(P_i^T H_i P_i)[m, n]` and adds it to the
1726// CTA's shared-mem `[p_total, p_total]` partial. The reduce kernel sums
1727// chunk-major-fixed-order into a single `[p_total, p_total]` output.
1728//
1729// Math: for primary index u ∈ [0, r):
1730// * u = 0: phi_u = (X_i in slot 0..p_m, 0 elsewhere)
1731// * u = 1: phi_u = (0, G_i in slot p_m..p_m+p_g, 0 elsewhere)
1732// * u = 2+j: phi_u = e_{h_block_start + j} (j ∈ 0..h_block_len)
1733// * u = 2+h+l: phi_u = e_{w_block_start + l} (l ∈ 0..w_block_len)
1734// Then `H_full[m, n] += sum_{u,v} H_i[u,v] * phi_u[m] * phi_v[n]`.
1735//
1736// Shared-memory budget: at large-scale shape p_total = 44, a [44, 44] f64
1737// partial is 44*44*8 = 15.5 KiB — well below the V100 48 KiB/SM cap.
1738// At p_total ≤ 80 the kernel still fits (80*80*8 = 50 KiB → just over
1739// V100 cap; caller must enforce p_total ≤ DENSE_BLOCK_MAX_P). The
1740// launcher rejects oversize p_total cleanly.
1741
1742extern "C" __global__ void bms_flex_row_dense_block_partial(
1743 int n_rows,
1744 int r,
1745 int p_m,
1746 int p_g,
1747 int p_total,
1748 int h_block_start,
1749 int h_block_len,
1750 int w_block_start,
1751 int w_block_len,
1752 int h_primary_start,
1753 int w_primary_start,
1754 int rows_per_cta,
1755 const double * __restrict__ row_hessians, // [n, r*r]
1756 const double * __restrict__ marginal_design, // [n, p_m]
1757 const double * __restrict__ logslope_design, // [n, p_g]
1758 double * __restrict__ partial) // [num_chunks, p_total, p_total]
1759{
1760 extern __shared__ double shmem[];
1761 int chunk = blockIdx.x;
1762 int tid = threadIdx.x;
1763 int row_lo = chunk * rows_per_cta;
1764 int remaining_rows = n_rows - row_lo;
1765 int row_hi = row_lo + (remaining_rows < rows_per_cta ? remaining_rows : rows_per_cta);
1766
1767 int pp = p_total * p_total;
1768 double *acc = shmem; // CTA-private accumulator [p_total, p_total]
1769 for (int j = tid; j < pp; j += blockDim.x) acc[j] = 0.0;
1770 __syncthreads();
1771
1772 // Per-row work performed by thread 0 to avoid cross-thread RW
1773 // contention on `acc[]`. Per-row complexity is O(r² + p_total²); the host
1774 // selects this direct algorithm only for small p_total, while r remains a
1775 // checked runtime width with no semantic ceiling.
1776 // Tighter parallel implementations are possible (warp-stripe the
1777 // 4-way nested u-v-m-n loop) but Phase 6 is a debug-only path and
1778 // the simple version is easier to audit for correctness against
1779 // the host-side P_i pullback oracle.
1780 if (tid == 0) {
1781 for (int row = row_lo; row < row_hi; ++row) {
1782 const double *mrow = marginal_design + (size_t)row * (size_t)p_m;
1783 const double *grow = logslope_design + (size_t)row * (size_t)p_g;
1784 const double *Hrow = row_hessians + (size_t)row * (size_t)r * (size_t)r;
1785 for (int u = 0; u < r; ++u) {
1786 for (int v = 0; v < r; ++v) {
1787 double huv = Hrow[(size_t)u * (size_t)r + (size_t)v];
1788 if (huv == 0.0) continue;
1789 // For each (u, v), iterate (m, n) over the non-zero
1790 // outer-product support of phi_u and phi_v.
1791 // Build a small (offset, len, src_ptr) descriptor for
1792 // each operand block as we go.
1793 int m_off, m_len; const double *m_src; bool m_indicator;
1794 int n_off, n_len; const double *n_src; bool n_indicator;
1795 if (u == 0) { m_off = 0; m_len = p_m; m_src = mrow; m_indicator = false; }
1796 else if (u == 1) { m_off = p_m; m_len = p_g; m_src = grow; m_indicator = false; }
1797 else if (u - 2 < h_block_len) {
1798 m_off = h_block_start + (u - 2);
1799 m_len = 1; m_src = NULL; m_indicator = true;
1800 } else {
1801 m_off = w_block_start + (u - 2 - h_block_len);
1802 m_len = 1; m_src = NULL; m_indicator = true;
1803 }
1804 if (v == 0) { n_off = 0; n_len = p_m; n_src = mrow; n_indicator = false; }
1805 else if (v == 1) { n_off = p_m; n_len = p_g; n_src = grow; n_indicator = false; }
1806 else if (v - 2 < h_block_len) {
1807 n_off = h_block_start + (v - 2);
1808 n_len = 1; n_src = NULL; n_indicator = true;
1809 } else {
1810 n_off = w_block_start + (v - 2 - h_block_len);
1811 n_len = 1; n_src = NULL; n_indicator = true;
1812 }
1813 // accumulate huv * phi_u[m] * phi_v[n] into acc[m, n]
1814 for (int mi = 0; mi < m_len; ++mi) {
1815 double pm = m_indicator ? 1.0 : m_src[mi];
1816 if (pm == 0.0) continue;
1817 double scaled = huv * pm;
1818 int m_idx = m_off + mi;
1819 for (int ni = 0; ni < n_len; ++ni) {
1820 double pn = n_indicator ? 1.0 : n_src[ni];
1821 int n_idx = n_off + ni;
1822 acc[m_idx * p_total + n_idx] += scaled * pn;
1823 }
1824 }
1825 }
1826 }
1827 }
1828 }
1829 __syncthreads();
1830
1831 // Write CTA accumulator out to global memory at its chunk slot.
1832 double *out_chunk = partial + (size_t)chunk * (size_t)pp;
1833 for (int j = tid; j < pp; j += blockDim.x) {
1834 out_chunk[j] = acc[j];
1835 }
1836}
1837
1838extern "C" __global__ void bms_flex_row_dense_block_reduce(
1839 int num_chunks,
1840 int p_total,
1841 const double * __restrict__ partial,
1842 double * __restrict__ out)
1843{
1844 int j = blockIdx.x * blockDim.x + threadIdx.x;
1845 int pp = p_total * p_total;
1846 if (j >= pp) return;
1847 double acc = 0.0;
1848 for (int c = 0; c < num_chunks; ++c) {
1849 acc += partial[(size_t)c * (size_t)pp + (size_t)j];
1850 }
1851 out[j] = acc;
1852}
1853
1854"#;
1855
1856#[cfg(target_os = "linux")]
1857pub(crate) struct HvpKernelBackend {
1858 pub(crate) stream: Arc<CudaStream>,
1859 pub(crate) module: Arc<CudaModule>,
1860}
1861
1862#[cfg(target_os = "linux")]
1863impl HvpKernelBackend {
1864 pub(crate) fn probe() -> Result<&'static Self, GpuError> {
1865 static BACKEND: OnceLock<Result<HvpKernelBackend, GpuError>> = OnceLock::new();
1866 BACKEND
1867 .get_or_init(|| {
1868 gam_gpu::backend_probe::probe_backend_with_compile("bms_flex_row hvp", |parts| {
1869 let ptx = gam_gpu::device_cache::compile_ptx_arch(HVP_KERNEL_SOURCE).map_err(
1873 |err| GpuError::DriverCallFailed {
1874 reason: format!("bms_flex_row hvp NVRTC compile failed: {err}"),
1875 },
1876 )?;
1877 let module =
1878 parts
1879 .ctx
1880 .load_module(ptx)
1881 .map_err(|err| GpuError::DriverCallFailed {
1882 reason: format!("bms_flex_row hvp module load failed: {err}"),
1883 })?;
1884 Ok(HvpKernelBackend {
1885 stream: parts.stream.clone(),
1886 module,
1887 })
1888 })
1889 })
1890 .as_ref()
1891 .map_err(GpuError::clone)
1892 }
1893}
1894
1895#[cfg(target_os = "linux")]
1921pub(crate) fn launch_bms_flex_row_kernel_device_resident(
1922 inputs: BmsFlexRowKernelInputs<'_>,
1923 marginal_design_row_major: &[f64],
1924 logslope_design_row_major: &[f64],
1925 block: BmsFlexBlockLayout,
1926 primary: BmsFlexPrimaryLayout,
1927) -> Result<DeviceResidentRowHess, GpuError> {
1928 inputs.validate()?;
1929 if !s_f_diagnostic_finite(&inputs) {
1930 return Err(GpuError::DriverCallFailed {
1931 reason: format!(
1932 "bms_flex_row device-resident: s_f must be positive and finite, got {}",
1933 inputs.s_f
1934 ),
1935 });
1936 }
1937 let n = inputs.n_rows;
1938 let r = inputs.r;
1939 let nr = checked_shape_len("device-resident [n,r]", &[n, r])?;
1940 let nrr = checked_shape_len("device-resident [n,r,r]", &[n, r, r])?;
1941 let marginal_len = checked_shape_len("device-resident marginal design", &[n, block.p_m])?;
1942 let logslope_len = checked_shape_len("device-resident logslope design", &[n, block.p_g])?;
1943 if marginal_design_row_major.len() != marginal_len {
1944 return Err(GpuError::DriverCallFailed {
1945 reason: format!(
1946 "bms_flex_row device-resident: marginal_design len={} != n*p_m={}",
1947 marginal_design_row_major.len(),
1948 marginal_len
1949 ),
1950 });
1951 }
1952 if logslope_design_row_major.len() != logslope_len {
1953 return Err(GpuError::DriverCallFailed {
1954 reason: format!(
1955 "bms_flex_row device-resident: logslope_design len={} != n*p_g={}",
1956 logslope_design_row_major.len(),
1957 logslope_len
1958 ),
1959 });
1960 }
1961 if primary.r != r {
1962 return Err(GpuError::DriverCallFailed {
1963 reason: format!(
1964 "bms_flex_row device-resident: primary.r={} != inputs.r={}",
1965 primary.r, r
1966 ),
1967 });
1968 }
1969
1970 let backend = RowKernelBackend::probe()?;
1973 HvpKernelBackend::probe()?;
1974 let stream = backend.stream.clone();
1975
1976 let upload_f64 = |slice: &[f64], label: &str| {
1977 stream
1978 .clone_htod(slice)
1979 .map_err(|err| GpuError::DriverCallFailed {
1980 reason: format!("bms_flex_row device-resident upload {label}: {err}"),
1981 })
1982 };
1983 let upload_u32 = |slice: &[u32], label: &str| {
1984 stream
1985 .clone_htod(slice)
1986 .map_err(|err| GpuError::DriverCallFailed {
1987 reason: format!("bms_flex_row device-resident upload {label}: {err}"),
1988 })
1989 };
1990
1991 let d_q = upload_f64(inputs.q, "q")?;
1992 let d_b = upload_f64(inputs.b, "b")?;
1993 let d_mu1 = upload_f64(inputs.mu_1, "mu_1")?;
1994 let d_mu2 = upload_f64(inputs.mu_2, "mu_2")?;
1995 let d_zobs = upload_f64(inputs.z_obs, "z_obs")?;
1996 let d_y = upload_f64(inputs.y, "y")?;
1997 let d_w = upload_f64(inputs.w, "w")?;
1998 let d_offsets = upload_u32(inputs.cell_offsets, "cell_offsets")?;
1999 let d_c0 = upload_f64(inputs.cell_c0, "cell_c0")?;
2000 let d_c1 = upload_f64(inputs.cell_c1, "cell_c1")?;
2001 let d_c2 = upload_f64(inputs.cell_c2, "cell_c2")?;
2002 let d_c3 = upload_f64(inputs.cell_c3, "cell_c3")?;
2003 let d_a = upload_f64(inputs.cell_a, "cell_a")?;
2004 let d_aa = upload_f64(inputs.cell_aa, "cell_aa")?;
2005 let d_r = upload_f64(inputs.cell_r, "cell_r")?;
2006 let d_ar = upload_f64(inputs.cell_ar, "cell_ar")?;
2007 let d_sbb = upload_f64(inputs.cell_sbb, "cell_sbb")?;
2008 let d_sbh = upload_f64(inputs.cell_sbh, "cell_sbh")?;
2009 let d_sbw = upload_f64(inputs.cell_sbw, "cell_sbw")?;
2010 let owned_host_moments: CudaSlice<f64>;
2012 let d_moments_ref: &CudaSlice<f64> = match &inputs.cell_moments {
2013 CellMomentsSource::Host(slice) => {
2014 owned_host_moments = upload_f64(slice, "cell_moments")?;
2015 &owned_host_moments
2016 }
2017 CellMomentsSource::Device(d) => *d,
2018 };
2019 let d_chi = upload_f64(inputs.chi_obs, "chi_obs")?;
2020 let d_xi = upload_f64(inputs.xi_obs, "xi_obs")?;
2021 let d_rho = upload_f64(inputs.rho_u, "rho_u")?;
2022 let d_tau = upload_f64(inputs.tau_u, "tau_u")?;
2023 let d_ruv = upload_f64(inputs.r_uv, "r_uv")?;
2024 let d_e_obs = upload_f64(inputs.e_obs, "e_obs")?;
2025
2026 let d_marginal = upload_f64(marginal_design_row_major, "marginal_design")?;
2027 let d_logslope = upload_f64(logslope_design_row_major, "logslope_design")?;
2028
2029 let mut d_neglog = stream
2030 .alloc_zeros::<f64>(n)
2031 .map_err(|err| GpuError::DriverCallFailed {
2032 reason: format!("bms_flex_row device-resident alloc neglog: {err}"),
2033 })?;
2034 let mut d_grad = stream
2035 .alloc_zeros::<f64>(nr)
2036 .map_err(|err| GpuError::DriverCallFailed {
2037 reason: format!("bms_flex_row device-resident alloc grad: {err}"),
2038 })?;
2039 let mut d_hess = stream
2040 .alloc_zeros::<f64>(nrr)
2041 .map_err(|err| GpuError::DriverCallFailed {
2042 reason: format!("bms_flex_row device-resident alloc hess: {err}"),
2043 })?;
2044 let mut d_f_au = stream
2045 .alloc_zeros::<f64>(nr)
2046 .map_err(|err| GpuError::DriverCallFailed {
2047 reason: format!("bms_flex_row device-resident alloc F_au scratch: {err}"),
2048 })?;
2049 let mut d_status = stream
2050 .alloc_zeros::<u32>(n)
2051 .map_err(|err| GpuError::DriverCallFailed {
2052 reason: format!("bms_flex_row device-resident alloc status: {err}"),
2053 })?;
2054
2055 let func = backend
2056 .module
2057 .load_function("bms_flex_row_kernel")
2058 .map_err(|err| GpuError::DriverCallFailed {
2059 reason: format!("bms_flex_row device-resident load_function: {err}"),
2060 })?;
2061
2062 let n_u32 = u32::try_from(n).map_err(|_| GpuError::DriverCallFailed {
2063 reason: format!("bms_flex_row device-resident: n_rows={n} exceeds CUDA grid range"),
2064 })?;
2065 let cfg = LaunchConfig {
2066 grid_dim: (n_u32, 1, 1),
2067 block_dim: (ROW_KERNEL_THREADS, 1, 1),
2068 shared_mem_bytes: 0,
2069 };
2070 let n_i32 = i32::try_from(n).map_err(|_| GpuError::DriverCallFailed {
2071 reason: format!("bms_flex_row device-resident: n_rows={n} exceeds i32 range"),
2072 })?;
2073 let r_i32 = i32::try_from(r).map_err(|_| GpuError::DriverCallFailed {
2074 reason: format!("bms_flex_row device-resident: r={r} exceeds i32 range"),
2075 })?;
2076 let p_h_i32 = i32::try_from(inputs.p_h).map_err(|_| GpuError::DriverCallFailed {
2077 reason: format!(
2078 "bms_flex_row device-resident: p_h={} exceeds i32 range",
2079 inputs.p_h
2080 ),
2081 })?;
2082 let p_w_i32 = i32::try_from(inputs.p_w).map_err(|_| GpuError::DriverCallFailed {
2083 reason: format!(
2084 "bms_flex_row device-resident: p_w={} exceeds i32 range",
2085 inputs.p_w
2086 ),
2087 })?;
2088 let s_f_val = inputs.s_f;
2089
2090 let mut builder = stream.launch_builder(&func);
2091 builder
2092 .arg(&n_i32)
2093 .arg(&r_i32)
2094 .arg(&p_h_i32)
2095 .arg(&p_w_i32)
2096 .arg(&s_f_val)
2097 .arg(&d_q)
2098 .arg(&d_b)
2099 .arg(&d_mu1)
2100 .arg(&d_mu2)
2101 .arg(&d_zobs)
2102 .arg(&d_y)
2103 .arg(&d_w)
2104 .arg(&d_offsets)
2105 .arg(&d_c0)
2106 .arg(&d_c1)
2107 .arg(&d_c2)
2108 .arg(&d_c3)
2109 .arg(&d_a)
2110 .arg(&d_aa)
2111 .arg(&d_r)
2112 .arg(&d_ar)
2113 .arg(&d_sbb)
2114 .arg(&d_sbh)
2115 .arg(&d_sbw)
2116 .arg(d_moments_ref)
2117 .arg(&d_chi)
2118 .arg(&d_xi)
2119 .arg(&d_rho)
2120 .arg(&d_tau)
2121 .arg(&d_ruv)
2122 .arg(&d_e_obs)
2123 .arg(&mut d_f_au)
2124 .arg(&mut d_neglog)
2125 .arg(&mut d_grad)
2126 .arg(&mut d_hess)
2127 .arg(&mut d_status);
2128 unsafe { builder.launch(cfg) }.map_err(|err| GpuError::DriverCallFailed {
2133 reason: format!("bms_flex_row device-resident launch: {err}"),
2134 })?;
2135 stream
2136 .synchronize()
2137 .map_err(|err| GpuError::DriverCallFailed {
2138 reason: format!("bms_flex_row device-resident synchronize: {err}"),
2139 })?;
2140
2141 let status = stream
2142 .clone_dtoh(&d_status)
2143 .map_err(|err| GpuError::DriverCallFailed {
2144 reason: format!("bms_flex_row device-resident download status: {err}"),
2145 })?;
2146 if let Some((row, code)) = status
2147 .iter()
2148 .copied()
2149 .enumerate()
2150 .find(|(_, code)| *code != 0)
2151 {
2152 return Err(GpuError::DriverCallFailed {
2153 reason: format!(
2154 "bms_flex_row device-resident rejected non-finite row {row} with status {code}"
2155 ),
2156 });
2157 }
2158 drop(d_status);
2159 drop(d_f_au);
2160
2161 drop(d_q);
2164 drop(d_b);
2165 drop(d_mu1);
2166 drop(d_mu2);
2167 drop(d_zobs);
2168 drop(d_y);
2169 drop(d_w);
2170 drop(d_offsets);
2171 drop(d_c0);
2172 drop(d_c1);
2173 drop(d_c2);
2174 drop(d_c3);
2175 drop(d_a);
2176 drop(d_aa);
2177 drop(d_r);
2178 drop(d_ar);
2179 drop(d_sbb);
2180 drop(d_sbh);
2181 drop(d_sbw);
2182 drop(d_chi);
2186 drop(d_xi);
2187 drop(d_rho);
2188 drop(d_tau);
2189 drop(d_ruv);
2190
2191 let resident_elements = n
2192 .checked_add(nr)
2193 .and_then(|value| value.checked_add(nrr))
2194 .and_then(|value| value.checked_add(marginal_len))
2195 .and_then(|value| value.checked_add(logslope_len))
2196 .ok_or_else(|| GpuError::DriverCallFailed {
2197 reason: "bms_flex_row device-resident: resident element count overflow".to_string(),
2198 })?;
2199 let resident_bytes = resident_elements
2200 .checked_mul(std::mem::size_of::<f64>())
2201 .ok_or_else(|| GpuError::DriverCallFailed {
2202 reason: "bms_flex_row device-resident: resident byte count overflow".to_string(),
2203 })?;
2204 let bytes = u64::try_from(resident_bytes).map_err(|_| GpuError::DriverCallFailed {
2205 reason: format!(
2206 "bms_flex_row device-resident: resident bytes={resident_bytes} exceed u64 range"
2207 ),
2208 })?;
2209 Ok(DeviceResidentRowHess {
2210 neglog: d_neglog,
2211 grad: d_grad,
2212 hess: d_hess,
2213 marginal_design: d_marginal,
2214 logslope_design: d_logslope,
2215 n,
2216 r,
2217 block,
2218 primary,
2219 bytes,
2220 })
2221}
2222
2223#[cfg(target_os = "linux")]
2227pub(crate) fn launch_bms_flex_row_joint_gradient(
2228 storage: &DeviceResidentRowHess,
2229) -> Result<BmsFlexDeviceJointGradient, GpuError> {
2230 let p_total = storage.block.p_total;
2231 let output_width = p_total
2232 .checked_add(1)
2233 .ok_or_else(|| GpuError::DriverCallFailed {
2234 reason: "bms_flex_row joint gradient: output width overflow".to_string(),
2235 })?;
2236 if storage.n == 0 {
2237 return Ok(BmsFlexDeviceJointGradient {
2238 log_likelihood: 0.0,
2239 gradient: vec![0.0; p_total],
2240 });
2241 }
2242
2243 let backend = HvpKernelBackend::probe()?;
2244 let stream = backend.stream.clone();
2245 let args = PreparedBmsFlexRowLaunchArgs::from_storage(storage)?;
2246 let partial_len = args
2247 .num_chunks
2248 .checked_mul(output_width)
2249 .ok_or_else(|| GpuError::DriverCallFailed {
2250 reason: format!(
2251 "bms_flex_row joint gradient: partial length overflow for chunks={} width={output_width}",
2252 args.num_chunks
2253 ),
2254 })?;
2255 let mut d_partial =
2256 stream
2257 .alloc_zeros::<f64>(partial_len)
2258 .map_err(|err| GpuError::DriverCallFailed {
2259 reason: format!("bms_flex_row joint gradient alloc partial: {err}"),
2260 })?;
2261 let mut d_out =
2262 stream
2263 .alloc_zeros::<f64>(output_width)
2264 .map_err(|err| GpuError::DriverCallFailed {
2265 reason: format!("bms_flex_row joint gradient alloc output: {err}"),
2266 })?;
2267 let partial_func = backend
2268 .module
2269 .load_function("bms_flex_row_joint_gradient_partial")
2270 .map_err(|err| GpuError::DriverCallFailed {
2271 reason: format!("bms_flex_row joint gradient load partial: {err}"),
2272 })?;
2273 let reduce_func = backend
2274 .module
2275 .load_function("bms_flex_row_joint_gradient_reduce")
2276 .map_err(|err| GpuError::DriverCallFailed {
2277 reason: format!("bms_flex_row joint gradient load reduce: {err}"),
2278 })?;
2279
2280 let num_chunks_u32 =
2281 u32::try_from(args.num_chunks).map_err(|_| GpuError::DriverCallFailed {
2282 reason: format!(
2283 "bms_flex_row joint gradient: num_chunks={} exceeds u32 range",
2284 args.num_chunks
2285 ),
2286 })?;
2287 let cfg_partial = LaunchConfig {
2288 grid_dim: (num_chunks_u32, 1, 1),
2289 block_dim: (HVP_THREADS, 1, 1),
2290 shared_mem_bytes: 0,
2291 };
2292 let mut builder = stream.launch_builder(&partial_func);
2293 builder
2294 .arg(&args.n_i32)
2295 .arg(&args.r_i32)
2296 .arg(&args.p_m_i32)
2297 .arg(&args.p_g_i32)
2298 .arg(&args.p_total_i32)
2299 .arg(&args.h_block_start)
2300 .arg(&args.h_block_len)
2301 .arg(&args.w_block_start)
2302 .arg(&args.w_block_len)
2303 .arg(&args.h_primary_start)
2304 .arg(&args.w_primary_start)
2305 .arg(&args.rows_per_cta)
2306 .arg(&storage.neglog)
2307 .arg(&storage.grad)
2308 .arg(&storage.marginal_design)
2309 .arg(&storage.logslope_design)
2310 .arg(&mut d_partial);
2311 unsafe { builder.launch(cfg_partial) }.map_err(|err| GpuError::DriverCallFailed {
2314 reason: format!("bms_flex_row joint gradient partial launch: {err}"),
2315 })?;
2316
2317 let output_width_i32 = i32::try_from(output_width).map_err(|_| GpuError::DriverCallFailed {
2318 reason: format!(
2319 "bms_flex_row joint gradient: output_width={output_width} exceeds i32 range"
2320 ),
2321 })?;
2322 let num_chunks_i32 =
2323 i32::try_from(args.num_chunks).map_err(|_| GpuError::DriverCallFailed {
2324 reason: format!(
2325 "bms_flex_row joint gradient: num_chunks={} exceeds i32 range",
2326 args.num_chunks
2327 ),
2328 })?;
2329 let output_width_u32 = u32::try_from(output_width).map_err(|_| GpuError::DriverCallFailed {
2330 reason: format!(
2331 "bms_flex_row joint gradient: output_width={output_width} exceeds u32 range"
2332 ),
2333 })?;
2334 let reduce_blocks = output_width_u32.div_ceil(REDUCTION_THREADS);
2335 let cfg_reduce = LaunchConfig {
2336 grid_dim: (reduce_blocks, 1, 1),
2337 block_dim: (REDUCTION_THREADS, 1, 1),
2338 shared_mem_bytes: 0,
2339 };
2340 let mut builder = stream.launch_builder(&reduce_func);
2341 builder
2342 .arg(&num_chunks_i32)
2343 .arg(&output_width_i32)
2344 .arg(&d_partial)
2345 .arg(&mut d_out);
2346 unsafe { builder.launch(cfg_reduce) }.map_err(|err| GpuError::DriverCallFailed {
2349 reason: format!("bms_flex_row joint gradient reduce launch: {err}"),
2350 })?;
2351 stream
2352 .synchronize()
2353 .map_err(|err| GpuError::DriverCallFailed {
2354 reason: format!("bms_flex_row joint gradient synchronize: {err}"),
2355 })?;
2356 let host = stream
2357 .clone_dtoh(&d_out)
2358 .map_err(|err| GpuError::DriverCallFailed {
2359 reason: format!("bms_flex_row joint gradient download: {err}"),
2360 })?;
2361 if let Some((index, value)) = host
2362 .iter()
2363 .copied()
2364 .enumerate()
2365 .find(|(_, value)| !value.is_finite())
2366 {
2367 return Err(GpuError::DriverCallFailed {
2368 reason: format!(
2369 "bms_flex_row joint gradient produced non-finite output[{index}]={value}"
2370 ),
2371 });
2372 }
2373 Ok(BmsFlexDeviceJointGradient {
2374 log_likelihood: host[0],
2375 gradient: host[1..].to_vec(),
2376 })
2377}
2378
2379#[cfg(target_os = "linux")]
2384#[derive(Clone, Copy)]
2385pub(crate) enum BmsFlexRowLaunchMode {
2386 HvpDeviceOut,
2388 DiagonalHostOut,
2390}
2391
2392#[cfg(target_os = "linux")]
2393impl BmsFlexRowLaunchMode {
2394 pub(crate) fn partial_kernel_name(self) -> &'static str {
2396 match self {
2397 BmsFlexRowLaunchMode::HvpDeviceOut => "bms_flex_row_hvp_partial",
2398 BmsFlexRowLaunchMode::DiagonalHostOut => "bms_flex_row_diag_partial",
2399 }
2400 }
2401}
2402
2403#[cfg(target_os = "linux")]
2409pub(crate) struct PreparedBmsFlexRowLaunchArgs {
2410 pub(crate) n_i32: i32,
2411 pub(crate) r_i32: i32,
2412 pub(crate) p_m_i32: i32,
2413 pub(crate) p_g_i32: i32,
2414 pub(crate) p_total_i32: i32,
2415 pub(crate) h_block_start: i32,
2416 pub(crate) h_block_len: i32,
2417 pub(crate) w_block_start: i32,
2418 pub(crate) w_block_len: i32,
2419 pub(crate) h_primary_start: i32,
2420 pub(crate) w_primary_start: i32,
2421 pub(crate) rows_per_cta: i32,
2422 pub(crate) num_chunks: usize,
2423 pub(crate) num_chunks_i32: i32,
2424 pub(crate) num_chunks_u32: u32,
2425 pub(crate) p_total_u32: u32,
2426}
2427
2428#[cfg(target_os = "linux")]
2429impl PreparedBmsFlexRowLaunchArgs {
2430 pub(crate) fn from_storage(storage: &DeviceResidentRowHess) -> Result<Self, GpuError> {
2431 if storage.n == 0 {
2432 return Err(GpuError::DriverCallFailed {
2433 reason: "bms_flex_row launch: n_rows must be > 0".to_string(),
2434 });
2435 }
2436 if storage.r < 2 {
2437 return Err(GpuError::DriverCallFailed {
2438 reason: format!("bms_flex_row launch: r={} must be >= 2", storage.r),
2439 });
2440 }
2441 let p_total = storage.block.p_total;
2442 if p_total == 0 {
2443 return Err(GpuError::DriverCallFailed {
2444 reason: "bms_flex_row launch: p_total must be > 0".to_string(),
2445 });
2446 }
2447 if storage.primary.r != storage.r {
2448 return Err(GpuError::DriverCallFailed {
2449 reason: format!(
2450 "bms_flex_row launch: primary.r={} != storage.r={}",
2451 storage.primary.r, storage.r
2452 ),
2453 });
2454 }
2455 let h_block_len = storage.block.h.as_ref().map_or(0, |range| range.len());
2456 let w_block_len = storage.block.w.as_ref().map_or(0, |range| range.len());
2457 let h_primary_len = storage.primary.h.as_ref().map_or(0, |range| range.len());
2458 let w_primary_len = storage.primary.w.as_ref().map_or(0, |range| range.len());
2459 if h_block_len != h_primary_len || w_block_len != w_primary_len {
2460 return Err(GpuError::DriverCallFailed {
2461 reason: format!(
2462 "bms_flex_row launch: block/primary direct lengths disagree: h={h_block_len}/{h_primary_len}, w={w_block_len}/{w_primary_len}"
2463 ),
2464 });
2465 }
2466 let h_block_start = storage
2467 .block
2468 .p_m
2469 .checked_add(storage.block.p_g)
2470 .ok_or_else(|| GpuError::DriverCallFailed {
2471 reason: "bms_flex_row launch: p_m+p_g overflow".to_string(),
2472 })?;
2473 let w_block_start =
2474 h_block_start
2475 .checked_add(h_block_len)
2476 .ok_or_else(|| GpuError::DriverCallFailed {
2477 reason: "bms_flex_row launch: h block end overflow".to_string(),
2478 })?;
2479 let expected_p_total =
2480 w_block_start
2481 .checked_add(w_block_len)
2482 .ok_or_else(|| GpuError::DriverCallFailed {
2483 reason: "bms_flex_row launch: w block end overflow".to_string(),
2484 })?;
2485 let w_primary_start =
2486 2_usize
2487 .checked_add(h_primary_len)
2488 .ok_or_else(|| GpuError::DriverCallFailed {
2489 reason: "bms_flex_row launch: h primary end overflow".to_string(),
2490 })?;
2491 let expected_r = w_primary_start.checked_add(w_primary_len).ok_or_else(|| {
2492 GpuError::DriverCallFailed {
2493 reason: "bms_flex_row launch: w primary end overflow".to_string(),
2494 }
2495 })?;
2496 let check_range = |name: &str,
2497 range: Option<&std::ops::Range<usize>>,
2498 expected_start: usize,
2499 expected_len: usize|
2500 -> Result<(), GpuError> {
2501 match (range, expected_len) {
2502 (None, 0) => Ok(()),
2503 (Some(range), len)
2504 if len > 0
2505 && range.start == expected_start
2506 && range.end == expected_start + len =>
2507 {
2508 Ok(())
2509 }
2510 _ => Err(GpuError::DriverCallFailed {
2511 reason: format!(
2512 "bms_flex_row launch: {name}={range:?} must be {expected_start}..{}",
2513 expected_start + expected_len
2514 ),
2515 }),
2516 }
2517 };
2518 check_range(
2519 "block.h",
2520 storage.block.h.as_ref(),
2521 h_block_start,
2522 h_block_len,
2523 )?;
2524 check_range(
2525 "block.w",
2526 storage.block.w.as_ref(),
2527 w_block_start,
2528 w_block_len,
2529 )?;
2530 check_range("primary.h", storage.primary.h.as_ref(), 2, h_primary_len)?;
2531 check_range(
2532 "primary.w",
2533 storage.primary.w.as_ref(),
2534 w_primary_start,
2535 w_primary_len,
2536 )?;
2537 if p_total != expected_p_total || storage.r != expected_r {
2538 return Err(GpuError::DriverCallFailed {
2539 reason: format!(
2540 "bms_flex_row launch: inconsistent layout p_total={p_total}/{expected_p_total}, r={}/{}",
2541 storage.r, expected_r
2542 ),
2543 });
2544 }
2545 let expected_nr = checked_shape_len("launch storage [n,r]", &[storage.n, storage.r])?;
2546 let expected_nrr =
2547 checked_shape_len("launch storage [n,r,r]", &[storage.n, storage.r, storage.r])?;
2548 let expected_marginal = checked_shape_len(
2549 "launch storage marginal design",
2550 &[storage.n, storage.block.p_m],
2551 )?;
2552 let expected_logslope = checked_shape_len(
2553 "launch storage logslope design",
2554 &[storage.n, storage.block.p_g],
2555 )?;
2556 for (name, have, want) in [
2557 ("neglog", storage.neglog.len(), storage.n),
2558 ("grad", storage.grad.len(), expected_nr),
2559 ("hess", storage.hess.len(), expected_nrr),
2560 (
2561 "marginal_design",
2562 storage.marginal_design.len(),
2563 expected_marginal,
2564 ),
2565 (
2566 "logslope_design",
2567 storage.logslope_design.len(),
2568 expected_logslope,
2569 ),
2570 ] {
2571 if have != want {
2572 return Err(GpuError::DriverCallFailed {
2573 reason: format!("bms_flex_row launch: storage {name}.len()={have} != {want}"),
2574 });
2575 }
2576 }
2577 let num_chunks = num_hvp_chunks(storage.n);
2578 let to_i32 = |name: &str, value: usize| {
2579 i32::try_from(value).map_err(|_| GpuError::DriverCallFailed {
2580 reason: format!("bms_flex_row launch: {name}={value} exceeds i32 range"),
2581 })
2582 };
2583 let to_u32 = |name: &str, value: usize| {
2584 u32::try_from(value).map_err(|_| GpuError::DriverCallFailed {
2585 reason: format!("bms_flex_row launch: {name}={value} exceeds u32 range"),
2586 })
2587 };
2588 Ok(PreparedBmsFlexRowLaunchArgs {
2589 n_i32: to_i32("n_rows", storage.n)?,
2590 r_i32: to_i32("r", storage.r)?,
2591 p_m_i32: to_i32("p_m", storage.block.p_m)?,
2592 p_g_i32: to_i32("p_g", storage.block.p_g)?,
2593 p_total_i32: to_i32("p_total", p_total)?,
2594 h_block_start: storage
2595 .block
2596 .h
2597 .as_ref()
2598 .map(|range| to_i32("h_block_start", range.start))
2599 .transpose()?
2600 .unwrap_or(0),
2601 h_block_len: storage
2602 .block
2603 .h
2604 .as_ref()
2605 .map(|range| to_i32("h_block_len", range.len()))
2606 .transpose()?
2607 .unwrap_or(0),
2608 w_block_start: storage
2609 .block
2610 .w
2611 .as_ref()
2612 .map(|range| to_i32("w_block_start", range.start))
2613 .transpose()?
2614 .unwrap_or(0),
2615 w_block_len: storage
2616 .block
2617 .w
2618 .as_ref()
2619 .map(|range| to_i32("w_block_len", range.len()))
2620 .transpose()?
2621 .unwrap_or(0),
2622 h_primary_start: storage
2623 .primary
2624 .h
2625 .as_ref()
2626 .map(|range| to_i32("h_primary_start", range.start))
2627 .transpose()?
2628 .unwrap_or(0),
2629 w_primary_start: storage
2630 .primary
2631 .w
2632 .as_ref()
2633 .map(|range| to_i32("w_primary_start", range.start))
2634 .transpose()?
2635 .unwrap_or(0),
2636 rows_per_cta: i32::try_from(HVP_ROWS_PER_CTA).map_err(|_| {
2637 GpuError::DriverCallFailed {
2638 reason: format!(
2639 "bms_flex_row launch: rows_per_cta={HVP_ROWS_PER_CTA} exceeds i32 range"
2640 ),
2641 }
2642 })?,
2643 num_chunks,
2644 num_chunks_i32: to_i32("num_chunks", num_chunks)?,
2645 num_chunks_u32: to_u32("num_chunks", num_chunks)?,
2646 p_total_u32: to_u32("p_total", p_total)?,
2647 })
2648 }
2649}
2650
2651#[cfg(target_os = "linux")]
2665pub(crate) fn run_bms_flex_row_partial_reduce(
2666 storage: &DeviceResidentRowHess,
2667 mode: BmsFlexRowLaunchMode,
2668 d_v: Option<&CudaSlice<f64>>,
2669 d_out: &mut CudaSlice<f64>,
2670 ctx: &str,
2671) -> Result<(), GpuError> {
2672 let backend = HvpKernelBackend::probe()?;
2673 let stream = backend.stream.clone();
2674 let args = PreparedBmsFlexRowLaunchArgs::from_storage(storage)?;
2675 let p_total = storage.block.p_total;
2676
2677 let partial_len = checked_shape_len(
2678 &format!("{ctx} partial [num_chunks,p_total]"),
2679 &[args.num_chunks, p_total],
2680 )?;
2681 let mut d_partial =
2682 stream
2683 .alloc_zeros::<f64>(partial_len)
2684 .map_err(|err| GpuError::DriverCallFailed {
2685 reason: format!("bms_flex_row {ctx} alloc partial: {err}"),
2686 })?;
2687
2688 let partial_kernel_name = mode.partial_kernel_name();
2689 let part_func = backend
2690 .module
2691 .load_function(partial_kernel_name)
2692 .map_err(|err| GpuError::DriverCallFailed {
2693 reason: format!("bms_flex_row {ctx} load {partial_kernel_name}: {err}"),
2694 })?;
2695 let red_func = backend
2696 .module
2697 .load_function("bms_flex_row_hvp_reduce")
2698 .map_err(|err| GpuError::DriverCallFailed {
2699 reason: format!("bms_flex_row {ctx} load reduce: {err}"),
2700 })?;
2701
2702 let cfg_part = LaunchConfig {
2703 grid_dim: (args.num_chunks_u32, 1, 1),
2704 block_dim: (HVP_THREADS, 1, 1),
2705 shared_mem_bytes: 0,
2706 };
2707 let mut builder = stream.launch_builder(&part_func);
2708 builder
2709 .arg(&args.n_i32)
2710 .arg(&args.r_i32)
2711 .arg(&args.p_m_i32)
2712 .arg(&args.p_g_i32)
2713 .arg(&args.p_total_i32)
2714 .arg(&args.h_block_start)
2715 .arg(&args.h_block_len)
2716 .arg(&args.w_block_start)
2717 .arg(&args.w_block_len)
2718 .arg(&args.h_primary_start)
2719 .arg(&args.w_primary_start)
2720 .arg(&args.rows_per_cta)
2721 .arg(&storage.hess)
2722 .arg(&storage.marginal_design)
2723 .arg(&storage.logslope_design);
2724 if let Some(d_v) = d_v {
2725 builder.arg(d_v);
2726 }
2727 builder.arg(&mut d_partial);
2728 unsafe { builder.launch(cfg_part) }.map_err(|err| GpuError::DriverCallFailed {
2736 reason: format!("bms_flex_row {ctx} partial launch: {err}"),
2737 })?;
2738
2739 let red_threads: u32 = REDUCTION_THREADS;
2740 let red_blocks = args.p_total_u32.div_ceil(red_threads);
2741 let cfg_red = LaunchConfig {
2742 grid_dim: (red_blocks, 1, 1),
2743 block_dim: (red_threads, 1, 1),
2744 shared_mem_bytes: 0,
2745 };
2746 let mut builder = stream.launch_builder(&red_func);
2747 builder
2748 .arg(&args.num_chunks_i32)
2749 .arg(&args.p_total_i32)
2750 .arg(&d_partial)
2751 .arg(d_out);
2752 unsafe { builder.launch(cfg_red) }.map_err(|err| GpuError::DriverCallFailed {
2756 reason: format!("bms_flex_row {ctx} reduce launch: {err}"),
2757 })?;
2758 drop(d_partial);
2761 Ok(())
2762}
2763
2764#[cfg(target_os = "linux")]
2767pub(crate) fn launch_bms_flex_row_diagonal_host(
2768 storage: &DeviceResidentRowHess,
2769) -> Result<Vec<f64>, GpuError> {
2770 let p_total = storage.block.p_total;
2771 let backend = HvpKernelBackend::probe()?;
2772 let stream = backend.stream.clone();
2773 let mut d_out =
2774 stream
2775 .alloc_zeros::<f64>(p_total)
2776 .map_err(|err| GpuError::DriverCallFailed {
2777 reason: format!("bms_flex_row diag alloc out: {err}"),
2778 })?;
2779
2780 run_bms_flex_row_partial_reduce(
2781 storage,
2782 BmsFlexRowLaunchMode::DiagonalHostOut,
2783 None,
2784 &mut d_out,
2785 "diag",
2786 )?;
2787
2788 stream
2789 .synchronize()
2790 .map_err(|err| GpuError::DriverCallFailed {
2791 reason: format!("bms_flex_row diag synchronize: {err}"),
2792 })?;
2793 stream
2794 .clone_dtoh(&d_out)
2795 .map_err(|err| GpuError::DriverCallFailed {
2796 reason: format!("bms_flex_row diag download out: {err}"),
2797 })
2798}
2799
2800#[cfg(target_os = "linux")]
2801pub(crate) fn validate_bms_flex_row_hvp_multi_shape(
2802 storage: &DeviceResidentRowHess,
2803 rhs_count: usize,
2804 v_rhs_len: usize,
2805 out_len: Option<usize>,
2806 ctx: &str,
2807) -> Result<usize, GpuError> {
2808 if rhs_count == 0 || rhs_count > BMS_FLEX_ROW_HVP_MAX_RHS {
2809 return Err(GpuError::DriverCallFailed {
2810 reason: format!(
2811 "bms_flex_row {ctx}: rhs_count={rhs_count} outside 1..={BMS_FLEX_ROW_HVP_MAX_RHS}"
2812 ),
2813 });
2814 }
2815 let p_total = storage.block.p_total;
2816 let rhs_elems = rhs_count
2817 .checked_mul(p_total)
2818 .ok_or_else(|| GpuError::DriverCallFailed {
2819 reason: format!(
2820 "bms_flex_row {ctx}: rhs_count({rhs_count})*p_total({p_total}) overflow"
2821 ),
2822 })?;
2823 i32::try_from(rhs_elems).map_err(|_| GpuError::DriverCallFailed {
2824 reason: format!(
2825 "bms_flex_row {ctx}: rhs_count({rhs_count})*p_total({p_total})={rhs_elems} exceeds CUDA int indexing range"
2826 ),
2827 })?;
2828 if v_rhs_len != rhs_elems {
2829 return Err(GpuError::DriverCallFailed {
2830 reason: format!(
2831 "bms_flex_row {ctx}: v_rhs.len()={v_rhs_len} != rhs_count({rhs_count})*p_total({p_total})={rhs_elems}"
2832 ),
2833 });
2834 }
2835 if let Some(out_len) = out_len
2836 && out_len != rhs_elems
2837 {
2838 return Err(GpuError::DriverCallFailed {
2839 reason: format!(
2840 "bms_flex_row {ctx}: out.len()={out_len} != rhs_count({rhs_count})*p_total({p_total})={rhs_elems}"
2841 ),
2842 });
2843 }
2844 Ok(rhs_elems)
2845}
2846
2847#[cfg(target_os = "linux")]
2851pub fn bms_flex_row_hvp_multi_scratch_bytes_for_shape(
2852 n: usize,
2853 p_total: usize,
2854 rhs_count: usize,
2855) -> Result<u64, GpuError> {
2856 if rhs_count == 0 || rhs_count > BMS_FLEX_ROW_HVP_MAX_RHS {
2857 return Err(GpuError::DriverCallFailed {
2858 reason: format!(
2859 "bms_flex_row hvp_multi_scratch_bytes: rhs_count={rhs_count} outside 1..={BMS_FLEX_ROW_HVP_MAX_RHS}"
2860 ),
2861 });
2862 }
2863 let num_chunks = num_hvp_chunks(n);
2864 let partial = rhs_count
2865 .checked_mul(num_chunks)
2866 .and_then(|v| v.checked_mul(p_total))
2867 .ok_or_else(|| GpuError::DriverCallFailed {
2868 reason: format!(
2869 "bms_flex_row hvp_multi_scratch_bytes: rhs_count({rhs_count})*num_chunks({num_chunks})*p_total({p_total}) overflow"
2870 ),
2871 })?;
2872 let rhs_vectors = rhs_count
2873 .checked_mul(p_total)
2874 .and_then(|v| v.checked_mul(2))
2875 .ok_or_else(|| GpuError::DriverCallFailed {
2876 reason: format!(
2877 "bms_flex_row hvp_multi_scratch_bytes: 2*rhs_count({rhs_count})*p_total({p_total}) overflow"
2878 ),
2879 })?;
2880 let elems = partial
2881 .checked_add(rhs_vectors)
2882 .ok_or_else(|| GpuError::DriverCallFailed {
2883 reason: "bms_flex_row hvp_multi_scratch_bytes: element count overflow".to_string(),
2884 })?;
2885 let bytes = elems
2886 .checked_mul(std::mem::size_of::<f64>())
2887 .ok_or_else(|| GpuError::DriverCallFailed {
2888 reason: "bms_flex_row hvp_multi_scratch_bytes: byte count overflow".to_string(),
2889 })?;
2890 u64::try_from(bytes).map_err(|_| GpuError::DriverCallFailed {
2891 reason: format!(
2892 "bms_flex_row hvp_multi_scratch_bytes: byte count={bytes} exceeds u64 range"
2893 ),
2894 })
2895}
2896
2897#[cfg(target_os = "linux")]
2898pub(crate) fn run_bms_flex_row_multi_partial_reduce(
2899 storage: &DeviceResidentRowHess,
2900 rhs_count: usize,
2901 d_v_rhs: &CudaSlice<f64>,
2902 d_out: &mut CudaSlice<f64>,
2903 ctx: &str,
2904) -> Result<(), GpuError> {
2905 let rhs_elems = validate_bms_flex_row_hvp_multi_shape(
2906 storage,
2907 rhs_count,
2908 d_v_rhs.len(),
2909 Some(d_out.len()),
2910 ctx,
2911 )?;
2912 let backend = HvpKernelBackend::probe()?;
2913 let stream = backend.stream.clone();
2914 let args = PreparedBmsFlexRowLaunchArgs::from_storage(storage)?;
2915 let p_total = storage.block.p_total;
2916 let partial_len = rhs_count
2917 .checked_mul(args.num_chunks)
2918 .and_then(|v| v.checked_mul(p_total))
2919 .ok_or_else(|| GpuError::DriverCallFailed {
2920 reason: format!(
2921 "bms_flex_row {ctx}: partial length overflow for rhs_count={rhs_count}, num_chunks={}, p_total={p_total}",
2922 args.num_chunks
2923 ),
2924 })?;
2925
2926 let mut d_partial =
2927 stream
2928 .alloc_zeros::<f64>(partial_len)
2929 .map_err(|err| GpuError::DriverCallFailed {
2930 reason: format!("bms_flex_row {ctx} alloc multi partial: {err}"),
2931 })?;
2932 let part_func = backend
2933 .module
2934 .load_function("bms_flex_row_hvp_multi_partial")
2935 .map_err(|err| GpuError::DriverCallFailed {
2936 reason: format!("bms_flex_row {ctx} load multi partial: {err}"),
2937 })?;
2938 let red_func = backend
2939 .module
2940 .load_function("bms_flex_row_hvp_multi_reduce")
2941 .map_err(|err| GpuError::DriverCallFailed {
2942 reason: format!("bms_flex_row {ctx} load multi reduce: {err}"),
2943 })?;
2944
2945 let rhs_count_i32 = i32::try_from(rhs_count).map_err(|_| GpuError::DriverCallFailed {
2946 reason: format!("bms_flex_row {ctx}: rhs_count={rhs_count} exceeds i32 range"),
2947 })?;
2948 let cfg_part = LaunchConfig {
2949 grid_dim: (args.num_chunks_u32, 1, 1),
2950 block_dim: (HVP_THREADS, 1, 1),
2951 shared_mem_bytes: 0,
2952 };
2953 let mut builder = stream.launch_builder(&part_func);
2954 builder
2955 .arg(&args.n_i32)
2956 .arg(&args.r_i32)
2957 .arg(&args.p_m_i32)
2958 .arg(&args.p_g_i32)
2959 .arg(&args.p_total_i32)
2960 .arg(&args.h_block_start)
2961 .arg(&args.h_block_len)
2962 .arg(&args.w_block_start)
2963 .arg(&args.w_block_len)
2964 .arg(&args.h_primary_start)
2965 .arg(&args.w_primary_start)
2966 .arg(&args.rows_per_cta)
2967 .arg(&rhs_count_i32)
2968 .arg(&storage.hess)
2969 .arg(&storage.marginal_design)
2970 .arg(&storage.logslope_design)
2971 .arg(d_v_rhs)
2972 .arg(&mut d_partial);
2973 unsafe { builder.launch(cfg_part) }.map_err(|err| GpuError::DriverCallFailed {
2978 reason: format!("bms_flex_row {ctx} multi partial launch: {err}"),
2979 })?;
2980
2981 let red_threads: u32 = REDUCTION_THREADS;
2982 let rhs_elems_u32 = u32::try_from(rhs_elems).map_err(|_| GpuError::DriverCallFailed {
2983 reason: format!("bms_flex_row {ctx}: rhs elements={rhs_elems} exceed u32 range"),
2984 })?;
2985 let red_blocks = rhs_elems_u32.div_ceil(red_threads);
2986 let cfg_red = LaunchConfig {
2987 grid_dim: (red_blocks, 1, 1),
2988 block_dim: (red_threads, 1, 1),
2989 shared_mem_bytes: 0,
2990 };
2991 let mut builder = stream.launch_builder(&red_func);
2992 builder
2993 .arg(&args.num_chunks_i32)
2994 .arg(&args.p_total_i32)
2995 .arg(&rhs_count_i32)
2996 .arg(&d_partial)
2997 .arg(d_out);
2998 unsafe { builder.launch(cfg_red) }.map_err(|err| GpuError::DriverCallFailed {
3001 reason: format!("bms_flex_row {ctx} multi reduce launch: {err}"),
3002 })?;
3003 drop(d_partial);
3004 Ok(())
3005}
3006
3007#[cfg(target_os = "linux")]
3010pub(crate) fn launch_bms_flex_row_hvp_multi(
3011 storage: &DeviceResidentRowHess,
3012 v_rhs: &[f64],
3013 rhs_count: usize,
3014) -> Result<Vec<f64>, GpuError> {
3015 let rhs_elems =
3016 validate_bms_flex_row_hvp_multi_shape(storage, rhs_count, v_rhs.len(), None, "hvp_multi")?;
3017 let backend = HvpKernelBackend::probe()?;
3018 let stream = backend.stream.clone();
3019 let d_v_rhs = stream
3020 .clone_htod(v_rhs)
3021 .map_err(|err| GpuError::DriverCallFailed {
3022 reason: format!("bms_flex_row hvp_multi upload v_rhs: {err}"),
3023 })?;
3024 let mut d_out =
3025 stream
3026 .alloc_zeros::<f64>(rhs_elems)
3027 .map_err(|err| GpuError::DriverCallFailed {
3028 reason: format!("bms_flex_row hvp_multi alloc out: {err}"),
3029 })?;
3030 run_bms_flex_row_multi_partial_reduce(storage, rhs_count, &d_v_rhs, &mut d_out, "hvp_multi")?;
3031 stream
3032 .synchronize()
3033 .map_err(|err| GpuError::DriverCallFailed {
3034 reason: format!("bms_flex_row hvp_multi synchronize: {err}"),
3035 })?;
3036 stream
3037 .clone_dtoh(&d_out)
3038 .map_err(|err| GpuError::DriverCallFailed {
3039 reason: format!("bms_flex_row hvp_multi download out: {err}"),
3040 })
3041}
3042
3043#[cfg(target_os = "linux")]
3048fn materialize_dense_from_hvp_batches(
3049 p_total: usize,
3050 mut launch: impl FnMut(&[f64], usize) -> Result<Vec<f64>, GpuError>,
3051) -> Result<Vec<f64>, GpuError> {
3052 if p_total == 0 {
3053 return Err(GpuError::DriverCallFailed {
3054 reason: "bms_flex_row dense HVP materialization: p_total must be > 0".to_string(),
3055 });
3056 }
3057 let dense_len = p_total
3058 .checked_mul(p_total)
3059 .ok_or_else(|| GpuError::DriverCallFailed {
3060 reason: format!(
3061 "bms_flex_row dense HVP materialization: p_total={p_total} square overflow"
3062 ),
3063 })?;
3064 let mut dense = vec![0.0_f64; dense_len];
3065 for column_start in (0..p_total).step_by(BMS_FLEX_ROW_HVP_MAX_RHS) {
3066 let rhs_count = (p_total - column_start).min(BMS_FLEX_ROW_HVP_MAX_RHS);
3067 let batch_len = checked_shape_len(
3068 "dense HVP materialization [rhs_count,p_total]",
3069 &[rhs_count, p_total],
3070 )?;
3071 let mut basis = vec![0.0_f64; batch_len];
3072 for local_column in 0..rhs_count {
3073 basis[local_column * p_total + column_start + local_column] = 1.0;
3074 }
3075 let images = launch(&basis, rhs_count)?;
3076 if images.len() != basis.len() {
3077 return Err(GpuError::DriverCallFailed {
3078 reason: format!(
3079 "bms_flex_row dense HVP materialization: batch at column {column_start} returned {} values, expected {}",
3080 images.len(),
3081 basis.len()
3082 ),
3083 });
3084 }
3085 for local_column in 0..rhs_count {
3086 let column = column_start + local_column;
3087 let image = &images[local_column * p_total..(local_column + 1) * p_total];
3088 for (row, &value) in image.iter().enumerate() {
3089 dense[row * p_total + column] = value;
3090 }
3091 }
3092 }
3093 Ok(dense)
3094}
3095
3096#[cfg(target_os = "linux")]
3107pub(crate) fn launch_bms_flex_row_hvp_into_device(
3108 storage: &DeviceResidentRowHess,
3109 d_v: &CudaSlice<f64>,
3110 d_out: &mut CudaSlice<f64>,
3111) -> Result<(), GpuError> {
3112 let p_total = storage.block.p_total;
3113 if d_v.len() != p_total {
3114 return Err(GpuError::DriverCallFailed {
3115 reason: format!(
3116 "bms_flex_row hvp_into_device: d_v.len()={} != p_total={}",
3117 d_v.len(),
3118 p_total
3119 ),
3120 });
3121 }
3122 if d_out.len() != p_total {
3123 return Err(GpuError::DriverCallFailed {
3124 reason: format!(
3125 "bms_flex_row hvp_into_device: d_out.len()={} != p_total={}",
3126 d_out.len(),
3127 p_total
3128 ),
3129 });
3130 }
3131 run_bms_flex_row_partial_reduce(
3135 storage,
3136 BmsFlexRowLaunchMode::HvpDeviceOut,
3137 Some(d_v),
3138 d_out,
3139 "hvp_into_device",
3140 )
3141}
3142
3143#[cfg(target_os = "linux")]
3146pub(crate) fn launch_bms_flex_row_hvp(
3147 storage: &DeviceResidentRowHess,
3148 v: &[f64],
3149) -> Result<Vec<f64>, GpuError> {
3150 launch_bms_flex_row_hvp_multi(storage, v, 1)
3151}
3152
3153#[cfg(target_os = "linux")]
3156pub(crate) fn launch_bms_flex_row_diagonal(
3157 storage: &DeviceResidentRowHess,
3158) -> Result<Vec<f64>, GpuError> {
3159 launch_bms_flex_row_diagonal_host(storage)
3160}
3161
3162#[cfg(target_os = "linux")]
3168pub(crate) const DENSE_BLOCK_MAX_P: usize = 72;
3169
3170#[cfg(target_os = "linux")]
3176pub(crate) const DENSE_BLOCK_ROWS_PER_CTA: u32 = 32;
3177
3178#[cfg(target_os = "linux")]
3184pub(crate) fn launch_bms_flex_row_dense(
3185 storage: &DeviceResidentRowHess,
3186) -> Result<Vec<f64>, GpuError> {
3187 let p_total = storage.block.p_total;
3188 if p_total <= DENSE_BLOCK_MAX_P {
3189 return launch_bms_flex_row_dense_block(storage);
3190 }
3191 materialize_dense_from_hvp_batches(p_total, |basis, rhs_count| {
3192 launch_bms_flex_row_hvp_multi(storage, basis, rhs_count)
3193 })
3194}
3195
3196#[cfg(target_os = "linux")]
3213pub fn launch_bms_flex_row_dense_block(
3214 storage: &DeviceResidentRowHess,
3215) -> Result<Vec<f64>, GpuError> {
3216 let p_total = storage.block.p_total;
3217 if p_total == 0 {
3218 return Err(GpuError::DriverCallFailed {
3219 reason: "bms_flex_row dense_block: p_total must be > 0".to_string(),
3220 });
3221 }
3222 if p_total > DENSE_BLOCK_MAX_P {
3223 return Err(GpuError::DriverCallFailed {
3224 reason: format!(
3225 "bms_flex_row dense_block: p_total={p_total} exceeds DENSE_BLOCK_MAX_P={DENSE_BLOCK_MAX_P} \
3226 (per-CTA shmem accumulator p²*8 bytes would exceed V100's 48 KiB/block)"
3227 ),
3228 });
3229 }
3230 let backend = HvpKernelBackend::probe()?;
3231 let stream = backend.stream.clone();
3232 let args = PreparedBmsFlexRowLaunchArgs::from_storage(storage)?;
3233 let n = storage.n;
3234 let rows_per_cta = DENSE_BLOCK_ROWS_PER_CTA as usize;
3235 let num_chunks = n.div_ceil(rows_per_cta);
3236 let pp = checked_shape_len("dense_block [p_total,p_total]", &[p_total, p_total])?;
3237 let partial_len = checked_shape_len("dense_block partial", &[num_chunks, pp])?;
3238
3239 let mut d_partial =
3240 stream
3241 .alloc_zeros::<f64>(partial_len)
3242 .map_err(|err| GpuError::DriverCallFailed {
3243 reason: format!("bms_flex_row dense_block alloc partial: {err}"),
3244 })?;
3245 let mut d_out = stream
3246 .alloc_zeros::<f64>(pp)
3247 .map_err(|err| GpuError::DriverCallFailed {
3248 reason: format!("bms_flex_row dense_block alloc out: {err}"),
3249 })?;
3250
3251 let part_func = backend
3252 .module
3253 .load_function("bms_flex_row_dense_block_partial")
3254 .map_err(|err| GpuError::DriverCallFailed {
3255 reason: format!("bms_flex_row dense_block load partial: {err}"),
3256 })?;
3257 let red_func = backend
3258 .module
3259 .load_function("bms_flex_row_dense_block_reduce")
3260 .map_err(|err| GpuError::DriverCallFailed {
3261 reason: format!("bms_flex_row dense_block load reduce: {err}"),
3262 })?;
3263
3264 let rows_per_cta_i32 = i32::try_from(DENSE_BLOCK_ROWS_PER_CTA).map_err(|_| {
3265 GpuError::DriverCallFailed {
3266 reason: format!(
3267 "bms_flex_row dense_block: rows_per_cta={DENSE_BLOCK_ROWS_PER_CTA} exceeds i32 range"
3268 ),
3269 }
3270 })?;
3271 let num_chunks_u32 = u32::try_from(num_chunks).map_err(|_| GpuError::DriverCallFailed {
3272 reason: format!("bms_flex_row dense_block: num_chunks={num_chunks} exceeds u32 range"),
3273 })?;
3274 let num_chunks_i32 = i32::try_from(num_chunks).map_err(|_| GpuError::DriverCallFailed {
3275 reason: format!("bms_flex_row dense_block: num_chunks={num_chunks} exceeds i32 range"),
3276 })?;
3277 let pp_u32 = u32::try_from(pp).map_err(|_| GpuError::DriverCallFailed {
3278 reason: format!("bms_flex_row dense_block: p_total²={pp} exceeds u32 range"),
3279 })?;
3280
3281 let shmem_bytes_usize =
3283 pp.checked_mul(std::mem::size_of::<f64>())
3284 .ok_or_else(|| GpuError::DriverCallFailed {
3285 reason: format!("dense_block shmem bytes overflow for p_total={p_total}"),
3286 })?;
3287 let shmem_bytes: u32 =
3288 u32::try_from(shmem_bytes_usize).map_err(|_| GpuError::DriverCallFailed {
3289 reason: format!("dense_block shmem bytes overflow u32 for p_total={p_total}"),
3290 })?;
3291
3292 let cfg_part = LaunchConfig {
3293 grid_dim: (num_chunks_u32, 1, 1),
3294 block_dim: (HVP_THREADS, 1, 1),
3295 shared_mem_bytes: shmem_bytes,
3296 };
3297 let mut builder = stream.launch_builder(&part_func);
3298 builder
3299 .arg(&args.n_i32)
3300 .arg(&args.r_i32)
3301 .arg(&args.p_m_i32)
3302 .arg(&args.p_g_i32)
3303 .arg(&args.p_total_i32)
3304 .arg(&args.h_block_start)
3305 .arg(&args.h_block_len)
3306 .arg(&args.w_block_start)
3307 .arg(&args.w_block_len)
3308 .arg(&args.h_primary_start)
3309 .arg(&args.w_primary_start)
3310 .arg(&rows_per_cta_i32)
3311 .arg(&storage.hess)
3312 .arg(&storage.marginal_design)
3313 .arg(&storage.logslope_design)
3314 .arg(&mut d_partial);
3315 unsafe { builder.launch(cfg_part) }.map_err(|err| GpuError::DriverCallFailed {
3319 reason: format!("bms_flex_row dense_block partial launch: {err}"),
3320 })?;
3321
3322 let red_threads: u32 = REDUCTION_THREADS;
3323 let red_blocks = pp_u32.div_ceil(red_threads);
3324 let cfg_red = LaunchConfig {
3325 grid_dim: (red_blocks, 1, 1),
3326 block_dim: (red_threads, 1, 1),
3327 shared_mem_bytes: 0,
3328 };
3329 let mut builder = stream.launch_builder(&red_func);
3330 builder
3331 .arg(&num_chunks_i32)
3332 .arg(&args.p_total_i32)
3333 .arg(&d_partial)
3334 .arg(&mut d_out);
3335 unsafe { builder.launch(cfg_red) }.map_err(|err| GpuError::DriverCallFailed {
3337 reason: format!("bms_flex_row dense_block reduce launch: {err}"),
3338 })?;
3339 stream
3340 .synchronize()
3341 .map_err(|err| GpuError::DriverCallFailed {
3342 reason: format!("bms_flex_row dense_block sync: {err}"),
3343 })?;
3344 stream
3345 .clone_dtoh(&d_out)
3346 .map_err(|err| GpuError::DriverCallFailed {
3347 reason: format!("bms_flex_row dense_block download: {err}"),
3348 })
3349}
3350
3351#[cfg(test)]
3353mod row_kernel_tests {
3354 pub(crate) fn host_log_ndtr_and_mills(x: f64) -> (f64, f64) {
3355 gam_gpu::numerics_host::log_ndtr_and_mills(x)
3356 }
3357
3358 #[cfg(target_os = "linux")]
3361 pub(crate) fn host_log_ndtr_mills_curvature(x: f64) -> (f64, f64, f64) {
3362 gam_gpu::numerics_host::log_ndtr_mills_curvature(x)
3363 }
3364
3365 pub(crate) mod parity_415 {
3368 use crate::bms::family::*;
3369 use crate::bms::hessian_paths::*;
3370 use crate::bms::{DeviationBlockConfig, LatentMeasureKind, exact_kernel};
3371 use gam_linalg::matrix::{DenseDesignMatrix, DesignMatrix};
3372 use gam_problem::{InverseLink, ParameterBlockState, StandardLink};
3373 use ndarray::{Array1, Array2};
3374 use std::sync::{Arc, Mutex};
3375
3376 pub(crate) fn make_flex_parity_family(
3382 n: usize,
3383 score_internal_knots: usize,
3384 link_internal_knots: usize,
3385 ) -> (BernoulliMarginalSlopeFamily, Vec<ParameterBlockState>) {
3386 let score_seed = Array1::linspace(-2.0, 2.0, n.max(6));
3387 let link_seed = Array1::linspace(-1.8, 1.8, n.max(6));
3388 let score_cfg = DeviationBlockConfig {
3389 num_internal_knots: score_internal_knots,
3390 ..DeviationBlockConfig::default()
3391 };
3392 let link_cfg = DeviationBlockConfig {
3393 num_internal_knots: link_internal_knots,
3394 ..DeviationBlockConfig::default()
3395 };
3396 let score_prepared =
3397 build_score_warp_deviation_block_from_seed(&score_seed, &score_cfg)
3398 .expect("build score warp block");
3399 let link_prepared = build_link_deviation_block_from_knots_design_seed_and_weights(
3400 &link_seed, &link_seed, &link_cfg,
3401 )
3402 .expect("build link deviation block");
3403
3404 let y: Array1<f64> =
3406 Array1::from_iter((0..n).map(|i| if (i * 17 + 3) % 7 >= 4 { 1.0 } else { 0.0 }));
3407 let weights: Array1<f64> =
3408 Array1::from_iter((0..n).map(|i| 0.75 + ((i * 11 + 5) % 5) as f64 * 0.05));
3409 let z: Array1<f64> =
3410 Array1::from_iter((0..n).map(|i| -1.7 + 3.4 * (i as f64 + 0.5) / n as f64));
3411 let marginal_x = Array2::from_shape_fn((n, 2), |(i, j)| {
3412 if j == 0 {
3413 1.0
3414 } else {
3415 -0.4 + 0.8 * ((i * 19 + 7) % n) as f64 / n as f64
3416 }
3417 });
3418 let logslope_x = Array2::from_shape_fn((n, 2), |(i, j)| {
3419 if j == 0 {
3420 1.0
3421 } else {
3422 0.3 - 0.6 * ((i * 23 + 11) % n) as f64 / n as f64
3423 }
3424 });
3425
3426 let family = BernoulliMarginalSlopeFamily {
3427 y: Arc::new(y),
3428 weights: Arc::new(weights),
3429 z: Arc::new(z.clone()),
3430 latent_measure: LatentMeasureKind::StandardNormal,
3431 gaussian_frailty_sd: Some(0.15),
3432 base_link: InverseLink::Standard(StandardLink::Probit),
3433 marginal_design: DesignMatrix::Dense(DenseDesignMatrix::from(marginal_x.clone())),
3434 logslope_design: DesignMatrix::Dense(DenseDesignMatrix::from(logslope_x.clone())),
3435 score_warp: Some(score_prepared.runtime.clone()),
3436 link_dev: Some(link_prepared.runtime.clone()),
3437 policy: gam_runtime::resource::ResourcePolicy::default_library(),
3438 cell_moment_lru: Arc::new(exact_kernel::CellMomentLruCache::new(1024)),
3439 cell_moment_cache_stats: Arc::new(exact_kernel::CellMomentCacheStats::default()),
3440 intercept_warm_starts: None,
3441 auto_subsample_phase_counter: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
3442 auto_subsample_last_rho: Arc::new(Mutex::new(None)),
3443 };
3444
3445 let beta_m = Array1::from_vec(vec![0.12, -0.04]);
3446 let beta_g = Array1::from_vec(vec![0.35, 0.03]);
3447 let beta_h = Array1::from_iter(
3448 (0..score_prepared.runtime.basis_dim()).map(|idx| 0.0015 * (idx as f64 + 1.0)),
3449 );
3450 let beta_w = Array1::from_iter(
3451 (0..link_prepared.runtime.basis_dim()).map(|idx| -0.001 * (idx as f64 + 1.0)),
3452 );
3453 let states = vec![
3454 ParameterBlockState {
3455 eta: marginal_x.dot(&beta_m),
3456 beta: beta_m,
3457 },
3458 ParameterBlockState {
3459 eta: logslope_x.dot(&beta_g),
3460 beta: beta_g,
3461 },
3462 ParameterBlockState {
3463 beta: beta_h,
3464 eta: Array1::zeros(z.len()),
3465 },
3466 ParameterBlockState {
3467 beta: beta_w,
3468 eta: Array1::zeros(z.len()),
3469 },
3470 ];
3471 (family, states)
3472 }
3473
3474 fn assert_generated_cuda_row_kernel_matches_canonical_cpu_lowering(
3478 n: usize,
3479 score_internal_knots: usize,
3480 link_internal_knots: usize,
3481 expected_r: Option<usize>,
3482 ) {
3483 let (family, states) =
3484 make_flex_parity_family(n, score_internal_knots, link_internal_knots);
3485 let cache = family
3486 .build_exact_eval_cache(&states)
3487 .expect("flex exact eval cache");
3488 assert!(
3489 cache.row_cell_moments.is_some(),
3490 "#415 fixture must materialise production row-cell moments"
3491 );
3492 let primary = &cache.primary;
3493 let r = primary.total;
3494 let p_h = primary.h.as_ref().map(|range| range.len()).unwrap_or(0);
3495 let p_w = primary.w.as_ref().map(|range| range.len()).unwrap_or(0);
3496 assert!(
3497 p_h > 0 && p_w > 0,
3498 "fixture must activate both deviation blocks"
3499 );
3500 assert_eq!(r, 2 + p_h + p_w);
3501 if let Some(expected_r) = expected_r {
3502 assert_eq!(
3503 r, expected_r,
3504 "fixture knot counts must exercise the requested primary width"
3505 );
3506 }
3507
3508 let owned = family
3509 .pack_bms_flex_row_kernel_inputs(&states, &cache)
3510 .expect("packing production CUDA inputs must not error")
3511 .expect("StandardNormal full-FLEX fixture must admit the CUDA row kernel");
3512 let inputs = owned.as_borrowed();
3513 let mut canonical_neglog = vec![0.0; n];
3514 let mut canonical_grad = vec![0.0; n * r];
3515 let mut canonical_hess = vec![0.0; n * r * r];
3516 let mut scratch = BernoulliMarginalSlopeFlexRowScratch::new(r);
3517 let mut checked_labels = [false, false];
3518
3519 for row in 0..n {
3520 let row_ctx = BernoulliMarginalSlopeFamily::row_ctx(&cache, row);
3521 let row_moments = cache
3522 .row_cell_moments
3523 .as_ref()
3524 .and_then(|bundle| bundle.row(row, 9));
3525 assert!(
3526 row_moments.is_some(),
3527 "row {row} must carry degree-9 moments"
3528 );
3529 canonical_neglog[row] = family
3530 .lower_bms_flex_row_order2_with_moments(
3531 row,
3532 &states,
3533 primary,
3534 row_ctx,
3535 row_moments,
3536 cache.cell_family_forest.as_ref(),
3537 true,
3538 &mut scratch,
3539 )
3540 .expect("canonical production CPU row lowering");
3541 for u in 0..r {
3542 canonical_grad[row * r + u] = scratch.grad[u];
3543 for v in 0..r {
3544 let value = scratch.hess[[u, v]];
3545 assert!(value.is_finite(), "row {row}: H[{u},{v}] is non-finite");
3546 assert_eq!(
3547 value.to_bits(),
3548 scratch.hess[[v, u]].to_bits(),
3549 "row {row}: canonical Hessian lost exact symmetry"
3550 );
3551 canonical_hess[row * r * r + u * r + v] = value;
3552 }
3553 }
3554 checked_labels[family.y[row] as usize] = true;
3555 }
3556 assert!(checked_labels[0] && checked_labels[1]);
3557
3558 let mut separates_value_from_q_derivative = false;
3559 for row in 0..n {
3560 let sign = 2.0 * inputs.y[row] - 1.0;
3561 let (_, lambda) = super::host_log_ndtr_and_mills(sign * inputs.e_obs[row]);
3562 let scale = -inputs.w[row] * sign * lambda;
3563 if scale.abs() > 1e-12 {
3564 let observed_q_derivative = canonical_grad[row * r] / scale;
3565 if (observed_q_derivative - inputs.e_obs[row]).abs() > 1e-8 {
3566 separates_value_from_q_derivative = true;
3567 break;
3568 }
3569 }
3570 }
3571 assert!(
3572 separates_value_from_q_derivative,
3573 "fixture must distinguish the observed value from its q derivative"
3574 );
3575
3576 #[cfg(not(target_os = "linux"))]
3577 {
3578 eprintln!("[bms_flex_row parity] generated CUDA check requires Linux");
3579 return;
3580 }
3581 #[cfg(target_os = "linux")]
3582 {
3583 match gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::GpuPolicy::Auto) {
3584 Ok(Some(_)) => {}
3585 Ok(None) => {
3586 eprintln!("[bms_flex_row parity] no CUDA device");
3587 return;
3588 }
3589 Err(error) => panic!("[bms_flex_row parity] CUDA probe failed: {error}"),
3590 }
3591 let gpu = super::super::launch_bms_flex_row_kernel(owned.as_borrowed())
3592 .expect("CUDA-selected canonical parity launch must succeed");
3593 let check = |channel: &str, index: usize, cpu: f64, device: f64| {
3594 let difference = (cpu - device).abs();
3595 let tolerance = 1e-8 + 1e-8 * cpu.abs();
3596 assert!(
3597 difference <= tolerance,
3598 "{channel}[{index}] CPU={cpu:.17e} CUDA={device:.17e} \
3599 difference={difference:.3e} tolerance={tolerance:.3e}"
3600 );
3601 };
3602 for (index, (&cpu, &device)) in
3603 canonical_neglog.iter().zip(gpu.neglog.iter()).enumerate()
3604 {
3605 check("neglog", index, cpu, device);
3606 }
3607 for (index, (&cpu, &device)) in
3608 canonical_grad.iter().zip(gpu.grad.iter()).enumerate()
3609 {
3610 check("gradient", index, cpu, device);
3611 }
3612 for (index, (&cpu, &device)) in
3613 canonical_hess.iter().zip(gpu.hess.iter()).enumerate()
3614 {
3615 check("hessian", index, cpu, device);
3616 }
3617 }
3618 }
3619
3620 #[test]
3621 fn generated_cuda_row_kernel_matches_canonical_cpu_lowering_415() {
3622 assert_generated_cuda_row_kernel_matches_canonical_cpu_lowering(12, 3, 3, None);
3623 }
3624
3625 #[test]
3626 fn full_flex_canonical_exact_cache_admits_material_finite_cell_curvature_2321() {
3627 let (family, states) = make_flex_parity_family(256, 8, 6);
3628 let cache = family
3629 .build_exact_eval_cache(&states)
3630 .expect("the full-FLEX host cache must preserve non-affine finite cells");
3631
3632 let score_width = cache
3633 .primary
3634 .h
3635 .as_ref()
3636 .expect("the full-FLEX fixture must retain its score-warp block")
3637 .len();
3638 let deviation_width = cache
3639 .primary
3640 .w
3641 .as_ref()
3642 .expect("the full-FLEX fixture must retain its link-deviation block")
3643 .len();
3644 assert!(score_width > 0 && deviation_width > 0);
3645 assert_eq!(
3646 cache.primary.total,
3647 2 + score_width + deviation_width,
3648 "the canonical primary layout must contain exactly q, logslope, score-warp, and link-deviation coordinates"
3649 );
3650 assert!(
3651 cache.row_cell_moments.is_some(),
3652 "the production full-FLEX fixture must materialize its exact row-cell cache"
3653 );
3654 }
3655
3656 #[test]
3657 fn generated_cuda_row_kernel_r33_matches_canonical_cpu_lowering_932() {
3658 assert_generated_cuda_row_kernel_matches_canonical_cpu_lowering(40, 15, 14, Some(33));
3665 }
3666 }
3667}
3668
3669#[cfg(all(test, target_os = "linux"))]
3670mod tests {
3671 use super::row_kernel_tests::*;
3672 use super::*;
3673 use crate::bms::exact_eval_cache::RowPrimaryEvalCache;
3674 use crate::bms::row_kernel::BernoulliMarginalSlopeExactNewtonJointHessianWorkspace;
3675 use crate::custom_family::{BlockwiseFitOptions, ExactNewtonJointHessianWorkspace};
3676 use ndarray::{Array1, Array2};
3682 use std::hint::black_box;
3683 use std::sync::atomic::AtomicUsize;
3684 use std::time::{Duration, Instant};
3685
3686 #[cfg(target_os = "linux")]
3701 fn assert_row_batch_dispatch_worthy(
3702 label: &str,
3703 policy: &gam_gpu::policy::GpuDispatchPolicy,
3704 n: usize,
3705 ) {
3706 assert!(
3707 policy.row_batch_target_is_gpu(n),
3708 "{label}: n={n} rows is below this device's calibrated row-kernel \
3709 crossover ({}), so the fixture no longer exercises a shape the \
3710 dispatch policy would send to the device — grow the fixture rather \
3711 than lowering the crossover",
3712 policy.row_kernel_min_n
3713 );
3714 assert!(
3715 !policy.row_batch_target_is_gpu(0),
3716 "{label}: the dispatch predicate admitted an empty batch, so the \
3717 assertion above proves nothing about n={n}"
3718 );
3719 }
3720
3721 fn assert_row_kernel_seam_declines_without_cuda() {
3737 let buffers = make_buffers(1, 4, 1, 1);
3738 minimal_inputs(&buffers)
3743 .validate()
3744 .expect("the device-free half must present inputs the row-kernel entry accepts");
3745
3746 match launch_bms_flex_row_kernel(minimal_inputs(&buffers)) {
3747 Ok(_) => panic!(
3748 "no CUDA runtime on this host, yet the BMS FLEX row-kernel entry returned Ok \
3749 — the seam fabricated device state (#1551 class)"
3750 ),
3751 Err(GpuError::DriverCallFailed { reason }) if reason.contains("s_f") => panic!(
3752 "the row-kernel entry refused over its INPUTS rather than the absent device, \
3753 so this proves nothing about the device seam: {reason}"
3754 ),
3755 Err(_) => {}
3756 }
3757 }
3758
3759 fn cuda_runtime_for_test(
3781 test_name: &str,
3782 ) -> Option<&'static gam_gpu::device_runtime::GpuRuntime> {
3783 let skips_before = gam_gpu::test_gate::skipped_for_absent_device();
3784 match gam_gpu::test_gate::gpu_for_test(test_name) {
3785 gam_gpu::test_gate::GpuTestGate::Ready(runtime) => Some(runtime),
3786 gam_gpu::test_gate::GpuTestGate::AbsentDevice => {
3787 gam_gpu::test_gate::assert_absent_device_was_counted(skips_before);
3788 eprintln!(
3789 "[{test_name}] no CUDA device — asserting the device-free seam contract \
3790 instead of skipping"
3791 );
3792 assert_row_kernel_seam_declines_without_cuda();
3793 None
3794 }
3795 }
3796 }
3797
3798 fn assert_array1_close_932(label: &str, expected: &Array1<f64>, actual: &Array1<f64>) {
3799 assert_eq!(expected.len(), actual.len(), "{label}: length mismatch");
3800 for (index, (&want, &got)) in expected.iter().zip(actual).enumerate() {
3801 let tolerance = 2.0e-8 * (1.0 + want.abs());
3802 assert!(
3803 want.is_finite() && got.is_finite() && (want - got).abs() <= tolerance,
3804 "{label}[{index}]: expected={want:.17e} actual={got:.17e} tolerance={tolerance:.3e}"
3805 );
3806 }
3807 }
3808
3809 #[test]
3826 fn mandatory_required_gpu_workspace_consumes_device_cache_end_to_end_932() {
3827 if cuda_runtime_for_test("mandatory_required_gpu_workspace_consumes_device_cache_end_to_end_932")
3828 .is_none()
3829 {
3830 return;
3831 }
3832
3833 let (family, states) = row_kernel_tests::parity_415::make_flex_parity_family(256, 8, 6);
3834 let mut workspace = BernoulliMarginalSlopeExactNewtonJointHessianWorkspace::new(
3835 family,
3836 states,
3837 BlockwiseFitOptions::default(),
3838 )
3839 .expect("#932 device workspace must build its device row cache");
3840
3841 assert!(
3842 matches!(
3843 &workspace.cache.row_primary_hessians,
3844 RowPrimaryEvalCache::Device(_)
3845 ),
3846 "a full-FLEX workspace built on a device-present host must retain \
3847 RowPrimaryEvalCache::Device"
3848 );
3849 {
3850 let device = workspace
3851 .cache
3852 .row_primary_hessians
3853 .device()
3854 .expect("device cache variant");
3855 assert!(
3856 device
3857 .primary
3858 .h
3859 .as_ref()
3860 .is_some_and(|range| !range.is_empty())
3861 && device
3862 .primary
3863 .w
3864 .as_ref()
3865 .is_some_and(|range| !range.is_empty()),
3866 "mandatory fixture must carry active h and w primary blocks"
3867 );
3868 assert!(
3869 device
3870 .block
3871 .h
3872 .as_ref()
3873 .is_some_and(|range| !range.is_empty())
3874 && device
3875 .block
3876 .w
3877 .as_ref()
3878 .is_some_and(|range| !range.is_empty()),
3879 "mandatory fixture must carry active h and w coefficient blocks"
3880 );
3881 }
3882 for operation in ["host HVP replay", "host diagonal replay"] {
3883 let error = workspace
3884 .cache
3885 .row_primary_hessians
3886 .reject_device_cpu_recompute(operation)
3887 .expect_err("a selected device cache must reject host row recomputation");
3888 assert!(
3889 error.contains("device-resident row evaluation selected")
3890 && error.contains("CPU row recomputation is forbidden"),
3891 "unexpected fail-closed diagnostic: {error}"
3892 );
3893 }
3894
3895 let total = workspace.cache.slices.total;
3896 let direction = Array1::from_shape_fn(total, |index| {
3897 let sign = if index % 2 == 0 { 1.0 } else { -1.0 };
3898 sign * (0.025 + 0.0075 * index as f64)
3899 });
3900 let joint_ll = workspace
3901 .joint_log_likelihood_evaluation()
3902 .expect("device joint log-likelihood")
3903 .expect("device joint log-likelihood must be present");
3904 let joint = workspace
3905 .joint_gradient_evaluation()
3906 .expect("device joint gradient")
3907 .expect("device joint gradient must be present");
3908 assert!(joint_ll.is_finite());
3909 assert_eq!(joint.log_likelihood.to_bits(), joint_ll.to_bits());
3910 assert_eq!(joint.gradient.len(), total);
3911 assert!(joint.gradient.iter().all(|value| value.is_finite()));
3912
3913 let hvp = workspace
3914 .hessian_matvec(&direction)
3915 .expect("device HVP")
3916 .expect("device HVP must be present");
3917 let mut hvp_into = Array1::from_elem(total, f64::NAN);
3918 assert!(
3919 workspace
3920 .hessian_matvec_into(&direction, &mut hvp_into)
3921 .expect("device HVP-into"),
3922 "device HVP-into must report that it handled the direction"
3923 );
3924 assert_array1_close_932("HVP owned/into", &hvp, &hvp_into);
3925
3926 let rhs = Array2::from_shape_fn((total, 3), |(row, column)| {
3927 (row as f64 + 1.0)
3928 * (column as f64 + 0.5)
3929 * 0.011
3930 * if (row + column) % 3 == 0 { -1.0 } else { 1.0 }
3931 });
3932 let mut applied = Array2::<f64>::from_elem((total, rhs.ncols()), f64::NAN);
3933 assert!(
3934 workspace
3935 .hessian_apply_mat(&rhs, &mut applied)
3936 .expect("device multi-RHS apply"),
3937 "device multi-RHS apply must report that it handled the matrix"
3938 );
3939 let diagonal = workspace
3940 .hessian_diagonal()
3941 .expect("device diagonal")
3942 .expect("device diagonal must be present");
3943 let dense = workspace
3944 .hessian_dense_forced()
3945 .expect("device forced dense Hessian")
3946 .expect("device forced dense Hessian must be present");
3947 assert_eq!(dense.dim(), (total, total));
3948 assert_array1_close_932("dense * v / HVP", &dense.dot(&direction), &hvp);
3949 assert_array1_close_932("dense diagonal", &dense.diag().to_owned(), &diagonal);
3950 let dense_applied = dense.dot(&rhs);
3951 for column in 0..rhs.ncols() {
3952 assert_array1_close_932(
3953 &format!("dense * V / apply_mat column {column}"),
3954 &dense_applied.column(column).to_owned(),
3955 &applied.column(column).to_owned(),
3956 );
3957 }
3958
3959 for state in &mut workspace.block_states {
3963 state.beta.fill(f64::NAN);
3964 state.eta.fill(f64::NAN);
3965 }
3966 let poisoned_hvp = workspace
3967 .hessian_matvec(&direction)
3968 .expect("device HVP after host-state poison")
3969 .expect("device HVP after host-state poison must be present");
3970 let poisoned_diagonal = workspace
3971 .hessian_diagonal()
3972 .expect("device diagonal after host-state poison")
3973 .expect("device diagonal after host-state poison must be present");
3974 assert_eq!(
3975 hvp.as_slice(),
3976 poisoned_hvp.as_slice(),
3977 "fixed-order device HVP changed after poisoning host block state"
3978 );
3979 assert_eq!(
3980 diagonal.as_slice(),
3981 poisoned_diagonal.as_slice(),
3982 "fixed-order device diagonal changed after poisoning host block state"
3983 );
3984 }
3985
3986 #[test]
3994 fn release_measure_generated_bms_full_row_vs_strongest_cpu_932() {
3995 const N: usize = 32_768;
3996 const WARMUPS: usize = 3;
3997 const SAMPLES: usize = 21;
3998
3999 if cuda_runtime_for_test("release_measure_generated_bms_full_row_vs_strongest_cpu_932")
4000 .is_none()
4001 {
4002 return;
4003 }
4004
4005 let (family, states) = row_kernel_tests::parity_415::make_flex_parity_family(N, 9, 7);
4010 let cache = family
4011 .build_exact_eval_cache(&states)
4012 .expect("full-row timing exact cache");
4013 let r = cache.primary.total;
4014 assert_eq!(r, 20, "9/7 knot fixture must expose primary width r=20");
4015 let marginal = family
4016 .marginal_design
4017 .as_dense_ref()
4018 .expect("timing fixture marginal design must be dense");
4019 let logslope = family
4020 .logslope_design
4021 .as_dense_ref()
4022 .expect("timing fixture logslope design must be dense");
4023 assert!(marginal.is_standard_layout() && logslope.is_standard_layout());
4024 let marginal_slice = marginal
4025 .as_slice()
4026 .expect("timing fixture marginal design is contiguous");
4027 let logslope_slice = logslope
4028 .as_slice()
4029 .expect("timing fixture logslope design is contiguous");
4030 let block = BmsFlexBlockLayout {
4031 p_m: cache.slices.marginal.len(),
4032 p_g: cache.slices.logslope.len(),
4033 h: cache.slices.h.clone(),
4034 w: cache.slices.w.clone(),
4035 p_total: cache.slices.total,
4036 };
4037 let primary = BmsFlexPrimaryLayout {
4038 h: cache.primary.h.clone(),
4039 w: cache.primary.w.clone(),
4040 r,
4041 };
4042 assert!(
4043 primary.h.as_ref().is_some_and(|range| !range.is_empty())
4044 && primary.w.as_ref().is_some_and(|range| !range.is_empty()),
4045 "full-row timing fixture must exercise both h and w"
4046 );
4047 let pin_bytes =
4048 crate::bms::family::BernoulliMarginalSlopeFamily::row_primary_eval_tile_bytes(N, r);
4049
4050 let run_cpu = || {
4051 let completed = AtomicUsize::new(0);
4052 family
4053 .build_row_primary_hessian_pin(
4054 &states,
4055 &cache,
4056 0..N,
4057 &completed,
4058 N.saturating_add(1),
4059 Instant::now(),
4060 pin_bytes,
4061 )
4062 .expect("production Rayon row-primary batch")
4063 };
4064 let run_gpu = || {
4065 let owned = family
4066 .pack_bms_flex_row_kernel_inputs(&states, &cache)
4067 .expect("production BMS GPU packing")
4068 .expect("StandardNormal full-FLEX timing fixture must pack");
4069 launch_bms_flex_row_kernel_device_resident(
4070 owned.as_borrowed(),
4071 marginal_slice,
4072 logslope_slice,
4073 block.clone(),
4074 primary.clone(),
4075 )
4076 .expect("production device-resident row launch")
4077 };
4078 let measure_cpu = || {
4079 let started = Instant::now();
4080 let output = black_box(run_cpu());
4081 (started.elapsed(), output)
4082 };
4083 let measure_gpu = || {
4084 let started = Instant::now();
4085 let output = black_box(run_gpu());
4086 (started.elapsed(), output)
4087 };
4088
4089 let cold_started = Instant::now();
4090 let cold_gpu = black_box(run_gpu());
4091 let cold_gpu_e2e_nvrtc = cold_started.elapsed();
4092 drop(cold_gpu);
4093 for _ in 0..WARMUPS {
4094 black_box(run_cpu());
4095 black_box(run_gpu());
4096 }
4097
4098 let mut cpu_samples = Vec::<Duration>::with_capacity(SAMPLES);
4099 let mut gpu_samples = Vec::<Duration>::with_capacity(SAMPLES);
4100 let mut last_cpu = None;
4101 let mut last_gpu = None;
4102 for sample in 0..SAMPLES {
4103 if sample % 2 == 0 {
4107 let (cpu_elapsed, cpu) = measure_cpu();
4108 cpu_samples.push(cpu_elapsed);
4109 let (gpu_elapsed, gpu) = measure_gpu();
4110 gpu_samples.push(gpu_elapsed);
4111 if sample + 1 == SAMPLES {
4112 last_cpu = Some(cpu);
4113 last_gpu = Some(gpu);
4114 }
4115 } else {
4116 let (gpu_elapsed, gpu) = measure_gpu();
4117 gpu_samples.push(gpu_elapsed);
4118 let (cpu_elapsed, cpu) = measure_cpu();
4119 cpu_samples.push(cpu_elapsed);
4120 drop(gpu);
4121 drop(cpu);
4122 }
4123 }
4124 let cpu = last_cpu.expect("final CPU sample retained for parity");
4125 let gpu = last_gpu.expect("final GPU sample retained for parity");
4126
4127 let stream = HvpKernelBackend::probe()
4128 .expect("HVP backend remains available")
4129 .stream
4130 .clone();
4131 let gpu_neglog = stream
4132 .clone_dtoh(&gpu.neglog)
4133 .expect("download timed GPU neglog for parity");
4134 let gpu_grad = stream
4135 .clone_dtoh(&gpu.grad)
4136 .expect("download timed GPU gradient for parity");
4137 let gpu_hess = stream
4138 .clone_dtoh(&gpu.hess)
4139 .expect("download timed GPU Hessian for parity");
4140 let cpu_channels = [
4141 cpu.neglog().as_slice().expect("CPU neglog is contiguous"),
4142 cpu.grad().as_slice().expect("CPU gradient is contiguous"),
4143 cpu.hess().as_slice().expect("CPU Hessian is contiguous"),
4144 ];
4145 let gpu_channels = [
4146 gpu_neglog.as_slice(),
4147 gpu_grad.as_slice(),
4148 gpu_hess.as_slice(),
4149 ];
4150 let mut nonfinite = 0_usize;
4151 let mut max_abs = 0.0_f64;
4152 let mut max_scaled = 0.0_f64;
4153 let mut cpu_digest = 0.0_f64;
4154 let mut gpu_digest = 0.0_f64;
4155 let mut digest_index = 0_usize;
4156 for (cpu_channel, gpu_channel) in cpu_channels.iter().zip(gpu_channels) {
4157 assert_eq!(cpu_channel.len(), gpu_channel.len());
4158 for (&host, &device) in cpu_channel.iter().zip(gpu_channel) {
4159 if !host.is_finite() || !device.is_finite() {
4160 nonfinite += 1;
4161 }
4162 let difference = (host - device).abs();
4163 let tolerance = 1.0e-8 * (1.0 + host.abs());
4164 max_abs = max_abs.max(difference);
4165 max_scaled = max_scaled.max(difference / tolerance);
4166 let weight = 1.0 + (digest_index % 251) as f64 / 251.0;
4167 cpu_digest += weight * host;
4168 gpu_digest += weight * device;
4169 digest_index += 1;
4170 }
4171 }
4172 assert_eq!(
4173 nonfinite, 0,
4174 "full-row CPU/GPU output contains non-finite values"
4175 );
4176 assert!(
4177 max_scaled <= 1.0,
4178 "full-row CPU/GPU parity exceeded tolerance: max_abs={max_abs:.3e} max_scaled={max_scaled:.3e}"
4179 );
4180
4181 let mut cpu_ms = cpu_samples
4182 .iter()
4183 .map(|sample| sample.as_secs_f64() * 1.0e3)
4184 .collect::<Vec<_>>();
4185 let mut gpu_ms = gpu_samples
4186 .iter()
4187 .map(|sample| sample.as_secs_f64() * 1.0e3)
4188 .collect::<Vec<_>>();
4189 cpu_ms.sort_by(f64::total_cmp);
4190 gpu_ms.sort_by(f64::total_cmp);
4191 let p25 = SAMPLES / 4;
4192 let p50 = SAMPLES / 2;
4193 let p75 = 3 * SAMPLES / 4;
4194 let conservative_speedup = cpu_ms[p25] / gpu_ms[p75];
4195 let median_speedup = cpu_ms[p50] / gpu_ms[p50];
4196 let cpu_distribution = cpu_ms
4197 .iter()
4198 .map(|value| format!("{value:.6}"))
4199 .collect::<Vec<_>>()
4200 .join(",");
4201 let gpu_distribution = gpu_ms
4202 .iter()
4203 .map(|value| format!("{value:.6}"))
4204 .collect::<Vec<_>>()
4205 .join(",");
4206 println!(
4207 "G932_BMS_FULL_ROW n={N} r={r} warmups={WARMUPS} samples={SAMPLES} \
4208 cold_gpu_e2e_nvrtc_ms={:.6} cpu_ms_p25={:.6} cpu_ms_p50={:.6} cpu_ms_p75={:.6} \
4209 gpu_ms_p25={:.6} gpu_ms_p50={:.6} gpu_ms_p75={:.6} \
4210 speedup_conservative_cpu_p25_over_gpu_p75={conservative_speedup:.6} \
4211 speedup_median={median_speedup:.6} parity_max_abs={max_abs:.9e} \
4212 parity_max_scaled={max_scaled:.9e} cpu_digest={cpu_digest:.17e} \
4213 gpu_digest={gpu_digest:.17e} nonfinite={nonfinite} \
4214 cpu_ms_sorted=[{cpu_distribution}] gpu_ms_sorted=[{gpu_distribution}]",
4215 cold_gpu_e2e_nvrtc.as_secs_f64() * 1.0e3,
4216 cpu_ms[p25],
4217 cpu_ms[p50],
4218 cpu_ms[p75],
4219 gpu_ms[p25],
4220 gpu_ms[p50],
4221 gpu_ms[p75],
4222 );
4223 }
4224
4225 #[test]
4226 fn dense_hvp_batches_transpose_column_images_in_bounded_groups_932() {
4227 let p_total = 2 * BMS_FLEX_ROW_HVP_MAX_RHS + 3;
4228 let matrix = (0..p_total * p_total)
4229 .map(|index| {
4230 let row = index / p_total;
4231 let column = index % p_total;
4232 1000.0 * row as f64 + column as f64 + 0.25
4233 })
4234 .collect::<Vec<_>>();
4235 let mut observed_batch_sizes = Vec::new();
4236 let dense = materialize_dense_from_hvp_batches(p_total, |basis, rhs_count| {
4237 observed_batch_sizes.push(rhs_count);
4238 let mut images = vec![0.0_f64; rhs_count * p_total];
4239 for rhs in 0..rhs_count {
4240 for row in 0..p_total {
4241 images[rhs * p_total + row] = (0..p_total)
4242 .map(|column| {
4243 matrix[row * p_total + column] * basis[rhs * p_total + column]
4244 })
4245 .sum();
4246 }
4247 }
4248 Ok(images)
4249 })
4250 .expect("synthetic H*I batches must materialize");
4251 assert_eq!(dense, matrix);
4252 assert_eq!(
4253 observed_batch_sizes,
4254 vec![BMS_FLEX_ROW_HVP_MAX_RHS, BMS_FLEX_ROW_HVP_MAX_RHS, 3]
4255 );
4256 }
4257
4258 pub(crate) fn minimal_inputs<'a>(buffers: &'a TestBuffers) -> BmsFlexRowKernelInputs<'a> {
4259 BmsFlexRowKernelInputs {
4260 n_rows: 1,
4261 r: 4,
4262 p_h: 1,
4263 p_w: 1,
4264 q: &buffers.q,
4265 b: &buffers.b,
4266 mu_1: &buffers.mu_1,
4267 mu_2: &buffers.mu_2,
4268 z_obs: &buffers.z_obs,
4269 y: &buffers.y,
4270 w: &buffers.w,
4271 e_obs: &buffers.e_obs,
4272 s_f: 1.0,
4273 cell_offsets: &buffers.cell_offsets,
4274 cell_c0: &buffers.cell_c0,
4275 cell_c1: &buffers.cell_c1,
4276 cell_c2: &buffers.cell_c2,
4277 cell_c3: &buffers.cell_c3,
4278 cell_a: &buffers.cell_a,
4279 cell_aa: &buffers.cell_aa,
4280 cell_r: &buffers.cell_r,
4281 cell_ar: &buffers.cell_ar,
4282 cell_sbb: &buffers.cell_sbb,
4283 cell_sbh: &buffers.cell_sbh,
4284 cell_sbw: &buffers.cell_sbw,
4285 cell_moments: CellMomentsSource::Host(&buffers.cell_moments),
4286 chi_obs: &buffers.chi_obs,
4287 xi_obs: &buffers.xi_obs,
4288 rho_u: &buffers.rho_u,
4289 tau_u: &buffers.tau_u,
4290 r_uv: &buffers.r_uv,
4291 }
4292 }
4293
4294 pub(crate) struct TestBuffers {
4295 pub(crate) q: Vec<f64>,
4296 pub(crate) b: Vec<f64>,
4297 pub(crate) mu_1: Vec<f64>,
4298 pub(crate) mu_2: Vec<f64>,
4299 pub(crate) z_obs: Vec<f64>,
4300 pub(crate) y: Vec<f64>,
4301 pub(crate) w: Vec<f64>,
4302 pub(crate) e_obs: Vec<f64>,
4303 pub(crate) cell_offsets: Vec<u32>,
4304 pub(crate) cell_c0: Vec<f64>,
4305 pub(crate) cell_c1: Vec<f64>,
4306 pub(crate) cell_c2: Vec<f64>,
4307 pub(crate) cell_c3: Vec<f64>,
4308 pub(crate) cell_a: Vec<f64>,
4309 pub(crate) cell_aa: Vec<f64>,
4310 pub(crate) cell_r: Vec<f64>,
4311 pub(crate) cell_ar: Vec<f64>,
4312 pub(crate) cell_sbb: Vec<f64>,
4313 pub(crate) cell_sbh: Vec<f64>,
4314 pub(crate) cell_sbw: Vec<f64>,
4315 pub(crate) cell_moments: Vec<f64>,
4316 pub(crate) chi_obs: Vec<f64>,
4317 pub(crate) xi_obs: Vec<f64>,
4318 pub(crate) rho_u: Vec<f64>,
4319 pub(crate) tau_u: Vec<f64>,
4320 pub(crate) r_uv: Vec<f64>,
4321 }
4322
4323 pub(crate) fn make_buffers(n_cells: u32, r: usize, p_h: usize, p_w: usize) -> TestBuffers {
4324 let cells = n_cells as usize;
4325 TestBuffers {
4326 q: vec![0.1; 1],
4327 b: vec![0.5; 1],
4328 mu_1: vec![0.3; 1],
4329 mu_2: vec![0.07; 1],
4330 z_obs: vec![0.0; 1],
4331 y: vec![1.0; 1],
4332 w: vec![1.0; 1],
4333 e_obs: vec![0.15; 1],
4334 cell_offsets: vec![0, n_cells],
4335 cell_c0: vec![0.2; cells],
4336 cell_c1: vec![-0.1; cells],
4337 cell_c2: vec![0.05; cells],
4338 cell_c3: vec![-0.02; cells],
4339 cell_a: vec![0.1; cells * 4],
4340 cell_aa: vec![0.0; cells * 4],
4341 cell_r: vec![0.05; cells * (r - 1) * 4],
4342 cell_ar: vec![0.0; cells * (r - 1) * 4],
4343 cell_sbb: vec![0.0; cells * 4],
4344 cell_sbh: vec![0.0; cells * p_h * 4],
4345 cell_sbw: vec![0.0; cells * p_w * 4],
4346 cell_moments: vec![1.0; cells * MOMENT_STRIDE],
4347 chi_obs: vec![1.0; 1],
4348 xi_obs: vec![0.0; 1],
4349 rho_u: vec![0.0; r],
4350 tau_u: vec![0.0; r],
4351 r_uv: vec![0.0; r * r],
4352 }
4353 }
4354
4355 #[test]
4356 pub(crate) fn validate_accepts_minimal_inputs() {
4357 let buffers = make_buffers(2, 4, 1, 1);
4358 let inputs = minimal_inputs(&buffers);
4359 assert!(inputs.validate().is_ok());
4360 }
4361
4362 #[test]
4363 pub(crate) fn validate_accepts_r33_with_active_h_and_w_blocks() {
4364 let r = 33;
4365 let p_h = 16;
4366 let p_w = 15;
4367 let buffers = make_buffers(1, r, p_h, p_w);
4368 let inputs = BmsFlexRowKernelInputs {
4369 r,
4370 p_h,
4371 p_w,
4372 rho_u: &buffers.rho_u,
4373 tau_u: &buffers.tau_u,
4374 r_uv: &buffers.r_uv,
4375 cell_r: &buffers.cell_r,
4376 cell_ar: &buffers.cell_ar,
4377 cell_sbh: &buffers.cell_sbh,
4378 cell_sbw: &buffers.cell_sbw,
4379 ..minimal_inputs(&buffers)
4380 };
4381 inputs
4382 .validate()
4383 .expect("r=33 is a valid checked shape, not a semantic width boundary");
4384 }
4385
4386 #[test]
4387 pub(crate) fn checked_shape_len_rejects_arithmetic_overflow() {
4388 let err = checked_shape_len("overflow test", &[usize::MAX, 2])
4389 .expect_err("shape multiplication must fail closed");
4390 assert!(err.to_string().contains("shape product overflow"));
4391 }
4392
4393 #[test]
4394 pub(crate) fn validate_rejects_zero_rows_before_cuda_grid_construction() {
4395 let buffers = make_buffers(1, 4, 1, 1);
4396 let inputs = BmsFlexRowKernelInputs {
4397 n_rows: 0,
4398 ..minimal_inputs(&buffers)
4399 };
4400 let err = inputs
4401 .validate()
4402 .expect_err("zero-row launch must fail closed");
4403 assert!(err.to_string().contains("n_rows must be > 0"));
4404 }
4405
4406 #[test]
4407 pub(crate) fn validate_rejects_mismatched_r_decomposition() {
4408 let buffers = make_buffers(1, 4, 1, 1);
4409 let bad_inputs = BmsFlexRowKernelInputs {
4410 r: 4,
4411 p_h: 1,
4412 p_w: 2, ..minimal_inputs(&buffers)
4414 };
4415 let err = bad_inputs
4416 .validate()
4417 .expect_err("inconsistent r vs p_h+p_w must fail");
4418 let msg = err.to_string();
4419 assert!(msg.contains("p_h"), "got: {msg}");
4420 assert!(msg.contains("p_w"), "got: {msg}");
4421 }
4422
4423 #[test]
4424 pub(crate) fn validate_rejects_non_monotone_offsets() {
4425 let mut buffers = make_buffers(2, 4, 1, 1);
4432 buffers.cell_offsets = vec![5, 2];
4433 let inputs = minimal_inputs(&buffers);
4434 let err = inputs
4435 .validate()
4436 .expect_err("non-monotone offsets must fail");
4437 let msg = err.to_string();
4438 assert!(msg.contains("monotone"), "got: {msg}");
4439 }
4440
4441 #[test]
4442 pub(crate) fn validate_rejects_mismatched_cell_moments_length() {
4443 let mut buffers = make_buffers(2, 4, 1, 1);
4444 buffers.cell_moments.pop(); let inputs = minimal_inputs(&buffers);
4446 let err = inputs.validate().expect_err("short cell_moments must fail");
4447 let msg = err.to_string();
4448 assert!(msg.contains("cell_moments"), "got: {msg}");
4449 }
4450
4451 #[test]
4452 pub(crate) fn launch_on_non_linux_reports_driver_library_unavailable() {
4453 #[cfg(target_os = "linux")]
4457 {
4458 if cuda_runtime_for_test("bms_flex_row launch smoke test").is_none() {
4459 return;
4460 }
4461 let buffers = make_buffers(1, 4, 1, 1);
4462 let inputs = minimal_inputs(&buffers);
4463 launch_bms_flex_row_kernel(inputs)
4464 .expect("BMS FLEX row kernel must launch after CUDA admission");
4465 }
4466 #[cfg(not(target_os = "linux"))]
4467 {
4468 let buffers = make_buffers(1, 4, 1, 1);
4469 let inputs = minimal_inputs(&buffers);
4470 match launch_bms_flex_row_kernel(inputs) {
4471 Err(GpuError::DriverLibraryUnavailable { reason }) => {
4472 assert!(
4473 reason.contains("Linux-only"),
4474 "expected Linux-only hint, got: {reason}"
4475 );
4476 }
4477 other => panic!("expected DriverLibraryUnavailable on non-Linux, got {other:?}"),
4478 }
4479 }
4480 }
4481
4482 #[test]
4483 pub(crate) fn s_f_must_be_positive_and_finite() {
4484 let buffers = make_buffers(1, 4, 1, 1);
4485 let mut inputs = minimal_inputs(&buffers);
4486 inputs.s_f = 0.0;
4487 match launch_bms_flex_row_kernel(inputs) {
4488 Err(GpuError::DriverCallFailed { reason }) => {
4489 assert!(reason.contains("s_f"), "got: {reason}");
4490 }
4491 other => panic!("expected DriverCallFailed for s_f=0, got {other:?}"),
4492 }
4493 }
4494
4495 #[test]
4522 pub(crate) fn device_mills_layer_matches_finite_differences() {
4523 let neglog_of = |e: f64, y: f64, w: f64| -> f64 {
4526 let s = 2.0 * y - 1.0;
4527 let (log_cdf, _) = host_log_ndtr_and_mills(s * e);
4528 -w * log_cdf
4529 };
4530 let ab_of = |e: f64, y: f64, w: f64| -> (f64, f64) {
4533 let s = 2.0 * y - 1.0;
4534 let m_arg = s * e;
4535 let (_, lambda, probit_curvature) = host_log_ndtr_mills_curvature(m_arg);
4536 let a_i = -w * s * lambda;
4537 let b_i = w * probit_curvature;
4538 (a_i, b_i)
4539 };
4540
4541 let cases: [(f64, f64, f64); 12] = [
4546 (-1.6, 1.0, 1.0),
4547 (-0.7, 1.0, 1.0),
4548 (0.0, 1.0, 1.0),
4549 (0.9, 1.0, 1.0),
4550 (1.8, 1.0, 1.0),
4551 (-1.4, 0.0, 1.0),
4552 (-0.3, 0.0, 1.0),
4553 (0.0, 0.0, 1.0),
4554 (0.6, 0.0, 1.0),
4555 (1.5, 0.0, 1.0),
4556 (0.4, 1.0, 0.75),
4557 (-0.8, 0.0, 1.3),
4558 ];
4559 let h = 1e-3_f64;
4562 for (e, y, w) in cases {
4563 let (a_ana, b_ana) = ab_of(e, y, w);
4564
4565 let fp2 = neglog_of(e + 2.0 * h, y, w);
4566 let fp1 = neglog_of(e + h, y, w);
4567 let f0 = neglog_of(e, y, w);
4568 let fm1 = neglog_of(e - h, y, w);
4569 let fm2 = neglog_of(e - 2.0 * h, y, w);
4570
4571 let d1_fd = (-fp2 + 8.0 * fp1 - 8.0 * fm1 + fm2) / (12.0 * h);
4573 let d2_fd = (-fp2 + 16.0 * fp1 - 30.0 * f0 + 16.0 * fm1 - fm2) / (12.0 * h * h);
4575
4576 let a_abs = (a_ana - d1_fd).abs();
4577 let a_rel = a_abs / a_ana.abs().max(1.0);
4578 assert!(
4579 a_abs <= 5e-8 || a_rel <= 5e-8,
4580 "Mills A (∂neglog/∂e) drift at e={e} y={y} w={w}: \
4581 analytic={a_ana:.17e} fd={d1_fd:.17e} abs={a_abs:.3e} rel={a_rel:.3e}"
4582 );
4583
4584 let b_abs = (b_ana - d2_fd).abs();
4585 let b_rel = b_abs / b_ana.abs().max(1.0);
4586 assert!(
4587 b_abs <= 5e-6 || b_rel <= 5e-6,
4588 "Mills B (∂²neglog/∂e²) drift at e={e} y={y} w={w}: \
4589 analytic={b_ana:.17e} fd={d2_fd:.17e} abs={b_abs:.3e} rel={b_rel:.3e}"
4590 );
4591 }
4592 }
4593
4594 #[test]
4595 pub(crate) fn generated_source_interprets_compact_canonical_phase_streams() {
4596 let source = generated_row_kernel_source();
4597 assert!(!source.contains("__BMS_FLEX_CALIBRATION_ORDER2__"));
4598 assert!(!source.contains("__BMS_FLEX_ORDER2_FINALIZER__"));
4599 assert!(!source.contains("__BMS_FLEX_ROW_THREADS__"));
4600 assert!(source.contains("for (int u = 1; u < r; ++u)"));
4601 assert!(source.contains("for (int v = u; v < r; ++v)"));
4602 assert!(source.contains("Canonical implicit-first stage complete"));
4603 assert!(source.contains("double *F_u = out_grad + row_r_base"));
4604 assert!(source.contains("double *F_au = row_f_au + row_r_base"));
4605 assert!(source.contains("double *F_uv = out_hess + row_rr_base"));
4606 for forbidden in [
4607 "MAX_R",
4608 "double F_u[",
4609 "double F_au[",
4610 "double F_uv[",
4611 "double a_u[",
4612 "double a_uv[",
4613 "double bar_e_u[",
4614 ] {
4615 assert!(
4616 !source.contains(forbidden),
4617 "generated row source restored width-bound scratch: {forbidden}"
4618 );
4619 }
4620 for forbidden in [
4621 "MAX_R",
4622 "double row_dir[",
4623 "double action[",
4624 "bms_flex_row_hvp_partial_packed",
4625 "bms_flex_row_diag_partial_packed",
4626 "bms_flex_row_pack_upper",
4627 ] {
4628 assert!(
4629 !HVP_KERNEL_SOURCE.contains(forbidden),
4630 "HVP source restored a dead or width-bound path: {forbidden}"
4631 );
4632 }
4633 assert!(HVP_KERNEL_SOURCE.contains("bms_flex_primary_direction"));
4634 assert!(HVP_KERNEL_SOURCE.contains("direction_q[MAX_MULTI_RHS]"));
4635 assert!(HVP_KERNEL_SOURCE.contains("action_g[MAX_MULTI_RHS]"));
4636 let mut cursor = 0usize;
4637 for marker in [
4638 "canonical calibration phase: InterceptFirst",
4639 "canonical calibration phase: InterceptSecond",
4640 "canonical calibration phase: PrimaryFirstAndInterceptSecond",
4641 "canonical calibration phase: PrimaryPairSecond",
4642 "canonical finalizer phase: ImplicitFirst",
4643 "canonical finalizer phase: ImplicitFirstComplete",
4644 "canonical finalizer phase: ImplicitSecond",
4645 "canonical finalizer phase: ObservedFirst",
4646 "canonical finalizer phase: ObservedScoreSensitivity",
4647 "canonical finalizer phase: ObservedSecond",
4648 "canonical finalizer phase: NegLogFirst",
4649 ] {
4650 let relative = source[cursor..]
4651 .find(marker)
4652 .unwrap_or_else(|| panic!("generated CUDA source omitted phase {marker}"));
4653 cursor += relative + marker.len();
4654 }
4655 assert!(
4656 source.len() < 40_000,
4657 "generated CUDA source unexpectedly bloated"
4658 );
4659 }
4660
4661 pub(crate) fn cpu_oracle_bms_flex_row_hvp(
4666 row_hessians: &[f64],
4667 marginal_design: &[f64],
4668 logslope_design: &[f64],
4669 block: &BmsFlexBlockLayout,
4670 primary: &BmsFlexPrimaryLayout,
4671 n: usize,
4672 v: &[f64],
4673 ) -> Vec<f64> {
4674 let r = primary.r;
4675 let p_m = block.p_m;
4676 let p_g = block.p_g;
4677 assert_eq!(v.len(), block.p_total);
4678 assert_eq!(row_hessians.len(), n * r * r);
4679 assert_eq!(marginal_design.len(), n * p_m);
4680 assert_eq!(logslope_design.len(), n * p_g);
4681 let mut out = vec![0.0_f64; block.p_total];
4682 let mut row_dir = vec![0.0_f64; r];
4683 let mut action = vec![0.0_f64; r];
4684 for row in 0..n {
4685 let mrow = &marginal_design[row * p_m..(row + 1) * p_m];
4686 let grow = &logslope_design[row * p_g..(row + 1) * p_g];
4687 let mut acc_q = 0.0_f64;
4688 for j in 0..p_m {
4689 acc_q += mrow[j] * v[j];
4690 }
4691 let mut acc_g = 0.0_f64;
4692 for j in 0..p_g {
4693 acc_g += grow[j] * v[p_m + j];
4694 }
4695 row_dir[0] = acc_q;
4696 row_dir[1] = acc_g;
4697 if let (Some(prange), Some(brange)) = (primary.h.as_ref(), block.h.as_ref()) {
4698 for (k, ii) in prange.clone().enumerate() {
4699 row_dir[ii] = v[brange.start + k];
4700 }
4701 }
4702 if let (Some(prange), Some(brange)) = (primary.w.as_ref(), block.w.as_ref()) {
4703 for (k, ii) in prange.clone().enumerate() {
4704 row_dir[ii] = v[brange.start + k];
4705 }
4706 }
4707 let h_slice = &row_hessians[row * r * r..(row + 1) * r * r];
4708 for u in 0..r {
4709 let mut acc = 0.0_f64;
4710 for v_idx in 0..r {
4711 acc += h_slice[u * r + v_idx] * row_dir[v_idx];
4712 }
4713 action[u] = acc;
4714 }
4715 let a0 = action[0];
4716 for j in 0..p_m {
4717 out[j] += a0 * mrow[j];
4718 }
4719 let a1 = action[1];
4720 for j in 0..p_g {
4721 out[p_m + j] += a1 * grow[j];
4722 }
4723 if let (Some(prange), Some(brange)) = (primary.h.as_ref(), block.h.as_ref()) {
4724 for (k, ii) in prange.clone().enumerate() {
4725 out[brange.start + k] += action[ii];
4726 }
4727 }
4728 if let (Some(prange), Some(brange)) = (primary.w.as_ref(), block.w.as_ref()) {
4729 for (k, ii) in prange.clone().enumerate() {
4730 out[brange.start + k] += action[ii];
4731 }
4732 }
4733 }
4734 out
4735 }
4736
4737 pub(crate) fn cpu_oracle_bms_flex_row_diagonal(
4738 row_hessians: &[f64],
4739 marginal_design: &[f64],
4740 logslope_design: &[f64],
4741 block: &BmsFlexBlockLayout,
4742 primary: &BmsFlexPrimaryLayout,
4743 n: usize,
4744 ) -> Vec<f64> {
4745 let r = primary.r;
4746 let p_m = block.p_m;
4747 let p_g = block.p_g;
4748 let mut out = vec![0.0_f64; block.p_total];
4749 for row in 0..n {
4750 let h_slice = &row_hessians[row * r * r..(row + 1) * r * r];
4751 let h00 = h_slice[0];
4752 let h11 = h_slice[r + 1];
4753 let mrow = &marginal_design[row * p_m..(row + 1) * p_m];
4754 let grow = &logslope_design[row * p_g..(row + 1) * p_g];
4755 for j in 0..p_m {
4756 out[j] += h00 * mrow[j] * mrow[j];
4757 }
4758 for j in 0..p_g {
4759 out[p_m + j] += h11 * grow[j] * grow[j];
4760 }
4761 if let (Some(prange), Some(brange)) = (primary.h.as_ref(), block.h.as_ref()) {
4762 for (k, ii) in prange.clone().enumerate() {
4763 out[brange.start + k] += h_slice[ii * r + ii];
4764 }
4765 }
4766 if let (Some(prange), Some(brange)) = (primary.w.as_ref(), block.w.as_ref()) {
4767 for (k, ii) in prange.clone().enumerate() {
4768 out[brange.start + k] += h_slice[ii * r + ii];
4769 }
4770 }
4771 }
4772 out
4773 }
4774
4775 pub(crate) fn cpu_oracle_bms_flex_row_joint_gradient(
4776 row_neglog: &[f64],
4777 row_grad: &[f64],
4778 marginal_design: &[f64],
4779 logslope_design: &[f64],
4780 block: &BmsFlexBlockLayout,
4781 primary: &BmsFlexPrimaryLayout,
4782 n: usize,
4783 ) -> (f64, Vec<f64>) {
4784 let r = primary.r;
4785 assert_eq!(row_neglog.len(), n);
4786 assert_eq!(row_grad.len(), n * r);
4787 assert_eq!(marginal_design.len(), n * block.p_m);
4788 assert_eq!(logslope_design.len(), n * block.p_g);
4789 let mut log_likelihood = 0.0_f64;
4790 let mut gradient = vec![0.0_f64; block.p_total];
4791 for row in 0..n {
4792 log_likelihood -= row_neglog[row];
4793 let grow = &row_grad[row * r..(row + 1) * r];
4794 for j in 0..block.p_m {
4795 gradient[j] -= grow[0] * marginal_design[row * block.p_m + j];
4796 }
4797 for j in 0..block.p_g {
4798 gradient[block.p_m + j] -= grow[1] * logslope_design[row * block.p_g + j];
4799 }
4800 if let (Some(primary_h), Some(block_h)) = (primary.h.as_ref(), block.h.as_ref()) {
4801 for (offset, primary_idx) in primary_h.clone().enumerate() {
4802 gradient[block_h.start + offset] -= grow[primary_idx];
4803 }
4804 }
4805 if let (Some(primary_w), Some(block_w)) = (primary.w.as_ref(), block.w.as_ref()) {
4806 for (offset, primary_idx) in primary_w.clone().enumerate() {
4807 gradient[block_w.start + offset] -= grow[primary_idx];
4808 }
4809 }
4810 }
4811 (log_likelihood, gradient)
4812 }
4813
4814 #[test]
4815 fn cpu_joint_gradient_oracle_pins_score_sign_and_active_hw_pullback() {
4816 let n = 2_usize;
4817 let r = 5_usize;
4818 let block = BmsFlexBlockLayout {
4819 p_m: 2,
4820 p_g: 1,
4821 h: Some(3..5),
4822 w: Some(5..6),
4823 p_total: 6,
4824 };
4825 let primary = BmsFlexPrimaryLayout {
4826 h: Some(2..4),
4827 w: Some(4..5),
4828 r,
4829 };
4830 let row_neglog = [1.25, 0.75];
4831 let row_grad = [
4832 2.0, -3.0, 5.0, -7.0, 11.0, -13.0, 17.0, -19.0, 23.0, -29.0, ];
4835 let marginal = [1.0, 2.0, -0.5, 3.0];
4836 let logslope = [4.0, -2.0];
4837 let (log_likelihood, gradient) = cpu_oracle_bms_flex_row_joint_gradient(
4838 &row_neglog,
4839 &row_grad,
4840 &marginal,
4841 &logslope,
4842 &block,
4843 &primary,
4844 n,
4845 );
4846 assert_eq!(log_likelihood, -2.0);
4847 assert_eq!(
4848 gradient,
4849 vec![-8.5, 35.0, 46.0, 14.0, -16.0, 18.0],
4850 "joint output must be the score/log-likelihood sign, with h/w direct slots"
4851 );
4852 }
4853
4854 #[test]
4858 pub(crate) fn cpu_oracle_hvp_matches_hand_computation_no_hw() {
4859 let n = 4_usize;
4860 let r = 4_usize; let p_m = 2_usize;
4862 let p_g = 2_usize;
4863 let p_h_dim = 1_usize;
4864 let p_w_dim = 1_usize;
4865 let p_total = p_m + p_g + p_h_dim + p_w_dim;
4866 let block = BmsFlexBlockLayout {
4867 p_m,
4868 p_g,
4869 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
4870 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
4871 p_total,
4872 };
4873 let primary = BmsFlexPrimaryLayout {
4874 h: Some(2..3),
4875 w: Some(3..4),
4876 r,
4877 };
4878 let mut row_hessians = vec![0.0_f64; n * r * r];
4880 for row in 0..n {
4881 for u in 0..r {
4882 for v in u..r {
4883 let val = ((row + 1) as f64) * (1.0 + (u as f64) + 2.0 * (v as f64));
4884 row_hessians[row * r * r + u * r + v] = val;
4885 row_hessians[row * r * r + v * r + u] = val;
4886 }
4887 }
4888 }
4889 let mut marginal = vec![0.0_f64; n * p_m];
4890 for row in 0..n {
4891 for j in 0..p_m {
4892 marginal[row * p_m + j] = 0.5 + (row as f64) * 0.1 - (j as f64) * 0.2;
4893 }
4894 }
4895 let mut logslope = vec![0.0_f64; n * p_g];
4896 for row in 0..n {
4897 for j in 0..p_g {
4898 logslope[row * p_g + j] = -0.3 + (row as f64) * 0.05 + (j as f64) * 0.15;
4899 }
4900 }
4901 let v: Vec<f64> = (0..p_total).map(|i| 0.1 + (i as f64) * 0.25).collect();
4902 let out = cpu_oracle_bms_flex_row_hvp(
4903 &row_hessians,
4904 &marginal,
4905 &logslope,
4906 &block,
4907 &primary,
4908 n,
4909 &v,
4910 );
4911 let mut expect_out_0 = 0.0_f64;
4913 for row in 0..n {
4914 let mrow = &marginal[row * p_m..(row + 1) * p_m];
4915 let grow = &logslope[row * p_g..(row + 1) * p_g];
4916 let mut row_dir = vec![0.0_f64; r];
4917 row_dir[0] = mrow[0] * v[0] + mrow[1] * v[1];
4918 row_dir[1] = grow[0] * v[p_m] + grow[1] * v[p_m + 1];
4919 row_dir[2] = v[p_m + p_g];
4920 row_dir[3] = v[p_m + p_g + p_h_dim];
4921 let h_slice = &row_hessians[row * r * r..(row + 1) * r * r];
4922 let mut action0 = 0.0_f64;
4923 for vv in 0..r {
4927 action0 += h_slice[vv] * row_dir[vv];
4928 }
4929 expect_out_0 += action0 * mrow[0];
4930 }
4931 assert!(
4932 (out[0] - expect_out_0).abs() < 1e-12,
4933 "cpu oracle HVP out[0] mismatch: {} vs hand-check {}",
4934 out[0],
4935 expect_out_0
4936 );
4937 assert!(out.iter().all(|x| x.is_finite()));
4938 assert_eq!(out.len(), p_total);
4939 }
4940
4941 #[test]
4943 pub(crate) fn cpu_oracle_diagonal_matches_hand_computation() {
4944 let n = 3_usize;
4945 let r = 4_usize;
4946 let p_m = 2_usize;
4947 let p_g = 2_usize;
4948 let p_h_dim = 1_usize;
4949 let p_w_dim = 1_usize;
4950 let p_total = p_m + p_g + p_h_dim + p_w_dim;
4951 let block = BmsFlexBlockLayout {
4952 p_m,
4953 p_g,
4954 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
4955 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
4956 p_total,
4957 };
4958 let primary = BmsFlexPrimaryLayout {
4959 h: Some(2..3),
4960 w: Some(3..4),
4961 r,
4962 };
4963 let mut row_hessians = vec![0.0_f64; n * r * r];
4964 for row in 0..n {
4965 for u in 0..r {
4966 row_hessians[row * r * r + u * r + u] = 1.0 + (row as f64) + (u as f64) * 0.5;
4967 }
4968 }
4969 let mut marginal = vec![0.0_f64; n * p_m];
4970 let mut logslope = vec![0.0_f64; n * p_g];
4971 for row in 0..n {
4972 for j in 0..p_m {
4973 marginal[row * p_m + j] = 0.2 + (row as f64) * 0.3 + (j as f64) * 0.1;
4974 }
4975 for j in 0..p_g {
4976 logslope[row * p_g + j] = -0.4 + (row as f64) * 0.1 + (j as f64) * 0.2;
4977 }
4978 }
4979 let out = cpu_oracle_bms_flex_row_diagonal(
4980 &row_hessians,
4981 &marginal,
4982 &logslope,
4983 &block,
4984 &primary,
4985 n,
4986 );
4987 let mut expect = 0.0_f64;
4989 for row in 0..n {
4990 let h00 = row_hessians[row * r * r];
4991 expect += h00 * marginal[row * p_m].powi(2);
4992 }
4993 assert!(
4994 (out[0] - expect).abs() < 1e-12,
4995 "out[0] {} vs {}",
4996 out[0],
4997 expect
4998 );
4999 let mut expect_h = 0.0_f64;
5001 for row in 0..n {
5002 expect_h += row_hessians[row * r * r + 2 * r + 2];
5003 }
5004 let h_slot = p_m + p_g;
5005 assert!(
5006 (out[h_slot] - expect_h).abs() < 1e-12,
5007 "h slot {} vs {}",
5008 out[h_slot],
5009 expect_h
5010 );
5011 }
5012
5013 #[test]
5019 pub(crate) fn bms_flex_row_r33_consumers_match_cpu_oracles_when_cuda_available() {
5020 if cuda_runtime_for_test("bms_flex_row_r33_consumers_match_cpu_oracles_when_cuda_available")
5021 .is_none()
5022 {
5023 return;
5024 }
5025 let n = 3_usize;
5026 let p_h_dim = 16_usize;
5027 let p_w_dim = 15_usize;
5028 let r = 2 + p_h_dim + p_w_dim;
5029 let p_m = 2_usize;
5030 let p_g = 2_usize;
5031 let p_total = p_m + p_g + p_h_dim + p_w_dim;
5032 let block = BmsFlexBlockLayout {
5033 p_m,
5034 p_g,
5035 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5036 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5037 p_total,
5038 };
5039 let primary = BmsFlexPrimaryLayout {
5040 h: Some(2..2 + p_h_dim),
5041 w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
5042 r,
5043 };
5044 let mut row_hessians = vec![0.0_f64; n * r * r];
5045 for row in 0..n {
5046 for u in 0..r {
5047 for v in u..r {
5048 let val = 0.001 * ((row + 1) as f64) * (1.0 + (u as f64) + 2.0 * (v as f64));
5049 row_hessians[row * r * r + u * r + v] = val;
5050 row_hessians[row * r * r + v * r + u] = val;
5051 }
5052 }
5053 }
5054 let mut marginal = vec![0.0_f64; n * p_m];
5055 for row in 0..n {
5056 for j in 0..p_m {
5057 marginal[row * p_m + j] = 0.5 + (row as f64) * 0.1 - (j as f64) * 0.2;
5058 }
5059 }
5060 let mut logslope = vec![0.0_f64; n * p_g];
5061 for row in 0..n {
5062 for j in 0..p_g {
5063 logslope[row * p_g + j] = -0.3 + (row as f64) * 0.05 + (j as f64) * 0.15;
5064 }
5065 }
5066 let v: Vec<f64> = (0..p_total).map(|i| 0.1 + (i as f64) * 0.25).collect();
5067 let cpu_hvp = cpu_oracle_bms_flex_row_hvp(
5068 &row_hessians,
5069 &marginal,
5070 &logslope,
5071 &block,
5072 &primary,
5073 n,
5074 &v,
5075 );
5076 let cpu_diag = cpu_oracle_bms_flex_row_diagonal(
5077 &row_hessians,
5078 &marginal,
5079 &logslope,
5080 &block,
5081 &primary,
5082 n,
5083 );
5084 let row_neglog = (0..n)
5085 .map(|row| 0.25 + 0.125 * row as f64)
5086 .collect::<Vec<_>>();
5087 let row_grad = (0..n * r)
5088 .map(|index| {
5089 let row = index / r;
5090 let primary_idx = index % r;
5091 (row as f64 + 0.75) * (primary_idx as f64 - 1.25)
5092 })
5093 .collect::<Vec<_>>();
5094 let (cpu_log_likelihood, cpu_gradient) = cpu_oracle_bms_flex_row_joint_gradient(
5095 &row_neglog,
5096 &row_grad,
5097 &marginal,
5098 &logslope,
5099 &block,
5100 &primary,
5101 n,
5102 );
5103 let mut cpu_dense = vec![0.0_f64; p_total * p_total];
5104 for column in 0..p_total {
5105 let mut basis = vec![0.0_f64; p_total];
5106 basis[column] = 1.0;
5107 let image = cpu_oracle_bms_flex_row_hvp(
5108 &row_hessians,
5109 &marginal,
5110 &logslope,
5111 &block,
5112 &primary,
5113 n,
5114 &basis,
5115 );
5116 for (row, value) in image.into_iter().enumerate() {
5117 cpu_dense[row * p_total + column] = value;
5118 }
5119 }
5120
5121 let backend = HvpKernelBackend::probe()
5128 .expect("[bms_flex_row hvp parity] backend probe must succeed on CUDA host");
5129 let stream = backend.stream.clone();
5130 let d_h = stream
5131 .clone_htod(&row_hessians)
5132 .expect("[bms_flex_row hvp parity] upload h must succeed on CUDA host");
5133 let d_m = stream
5134 .clone_htod(&marginal)
5135 .expect("[bms_flex_row hvp parity] upload marg must succeed on CUDA host");
5136 let d_g = stream
5137 .clone_htod(&logslope)
5138 .expect("[bms_flex_row hvp parity] upload logslope must succeed on CUDA host");
5139 let storage = DeviceResidentRowHess {
5140 neglog: stream
5141 .clone_htod(&row_neglog)
5142 .expect("[bms_flex_row hvp parity] upload neglog"),
5143 grad: stream
5144 .clone_htod(&row_grad)
5145 .expect("[bms_flex_row hvp parity] upload grad"),
5146 hess: d_h,
5147 marginal_design: d_m,
5148 logslope_design: d_g,
5149 n,
5150 r,
5151 block: block.clone(),
5152 primary: primary.clone(),
5153
5154 bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5155 as u64,
5156 };
5157 let gpu_hvp =
5158 launch_bms_flex_row_hvp(&storage, &v).expect("HVP kernel must launch on CUDA host");
5159 let gpu_diag = launch_bms_flex_row_diagonal(&storage)
5160 .expect("diagonal kernel must launch on CUDA host");
5161 let gpu_joint = launch_bms_flex_row_joint_gradient(&storage)
5162 .expect("joint-gradient kernel must launch on CUDA host");
5163 let gpu_dense = launch_bms_flex_row_dense(&storage)
5164 .expect("dense kernel must launch at r=33 on CUDA host");
5165 assert_eq!(gpu_hvp.len(), cpu_hvp.len());
5166 assert_eq!(gpu_diag.len(), cpu_diag.len());
5167 assert_eq!(gpu_joint.gradient.len(), cpu_gradient.len());
5168 assert!(
5169 (gpu_joint.log_likelihood - cpu_log_likelihood).abs() <= 1e-12,
5170 "loglik: cpu={} gpu={}",
5171 cpu_log_likelihood,
5172 gpu_joint.log_likelihood
5173 );
5174 for i in 0..p_total {
5175 let diff = (cpu_hvp[i] - gpu_hvp[i]).abs();
5176 assert!(
5177 diff <= 1e-10,
5178 "HVP[{i}]: cpu={} gpu={} |Δ|={diff:.3e}",
5179 cpu_hvp[i],
5180 gpu_hvp[i]
5181 );
5182 let ddiff = (cpu_diag[i] - gpu_diag[i]).abs();
5183 assert!(
5184 ddiff <= 1e-10,
5185 "diag[{i}]: cpu={} gpu={} |Δ|={ddiff:.3e}",
5186 cpu_diag[i],
5187 gpu_diag[i]
5188 );
5189 let gdiff = (cpu_gradient[i] - gpu_joint.gradient[i]).abs();
5190 assert!(
5191 gdiff <= 1e-10,
5192 "joint gradient[{i}]: cpu={} gpu={} |Δ|={gdiff:.3e}",
5193 cpu_gradient[i],
5194 gpu_joint.gradient[i]
5195 );
5196 }
5197 assert_eq!(gpu_dense.len(), cpu_dense.len());
5198 for (index, (&cpu, &gpu)) in cpu_dense.iter().zip(&gpu_dense).enumerate() {
5199 let tolerance = 1e-10 * (1.0 + cpu.abs());
5200 assert!(
5201 (cpu - gpu).abs() <= tolerance,
5202 "dense[{index}] at r=33: cpu={cpu} gpu={gpu} tolerance={tolerance}"
5203 );
5204 }
5205 }
5206
5207 #[test]
5208 pub(crate) fn bms_flex_row_hvp_multi_scratch_is_bounded_at_large_scale_shape() {
5209 let n = 195_000_usize;
5210 let r = 20_usize;
5211 let p_total = 44_usize;
5212 let rhs_count = 4_usize;
5213 let scratch = bms_flex_row_hvp_multi_scratch_bytes_for_shape(n, p_total, rhs_count)
5214 .expect("large-scale multi-RHS scratch budget");
5215 let per_rhs_full_row_cache =
5216 (n * r * r * std::mem::size_of::<f64>()) as u64 * rhs_count as u64;
5217 assert!(
5218 scratch < per_rhs_full_row_cache / 100,
5219 "multi-RHS scratch must tile by row chunks instead of materializing \
5220 a row-Hessian copy per RHS: scratch={scratch} full_per_rhs={per_rhs_full_row_cache}"
5221 );
5222 assert!(
5223 bms_flex_row_hvp_multi_scratch_bytes_for_shape(
5224 n,
5225 p_total,
5226 BMS_FLEX_ROW_HVP_MAX_RHS + 1
5227 )
5228 .is_err(),
5229 "multi-RHS launch must reject unbounded RHS counts"
5230 );
5231 }
5232
5233 #[test]
5234 pub(crate) fn bms_flex_row_hvp_multi_kernel_matches_cpu_oracle_when_cuda_available() {
5235 if cuda_runtime_for_test("bms_flex_row hvp_multi parity").is_none() {
5236 return;
5237 }
5238 let n = 5_usize;
5239 let r = 4_usize;
5240 let p_m = 2_usize;
5241 let p_g = 2_usize;
5242 let p_h_dim = 1_usize;
5243 let p_w_dim = 1_usize;
5244 let p_total = p_m + p_g + p_h_dim + p_w_dim;
5245 let rhs_count = 3_usize;
5246 let block = BmsFlexBlockLayout {
5247 p_m,
5248 p_g,
5249 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5250 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5251 p_total,
5252 };
5253 let primary = BmsFlexPrimaryLayout {
5254 h: Some(2..3),
5255 w: Some(3..4),
5256 r,
5257 };
5258 let mut row_hessians = vec![0.0_f64; n * r * r];
5259 for row in 0..n {
5260 for u in 0..r {
5261 for v in u..r {
5262 let val = ((row + 1) as f64) * (1.0 + (u as f64) + 2.0 * (v as f64));
5263 row_hessians[row * r * r + u * r + v] = val;
5264 row_hessians[row * r * r + v * r + u] = val;
5265 }
5266 }
5267 }
5268 let mut marginal = vec![0.0_f64; n * p_m];
5269 let mut logslope = vec![0.0_f64; n * p_g];
5270 for row in 0..n {
5271 for j in 0..p_m {
5272 marginal[row * p_m + j] = 0.5 + (row as f64) * 0.1 - (j as f64) * 0.2;
5273 }
5274 for j in 0..p_g {
5275 logslope[row * p_g + j] = -0.3 + (row as f64) * 0.05 + (j as f64) * 0.15;
5276 }
5277 }
5278 let mut v_rhs = vec![0.0_f64; rhs_count * p_total];
5279 for rhs in 0..rhs_count {
5280 for j in 0..p_total {
5281 let seed = (rhs as f64) * 0.37 + (j as f64) * 0.19 + 0.4;
5282 v_rhs[rhs * p_total + j] = seed.sin() * 0.4 + seed.cos() * 0.2;
5283 }
5284 }
5285
5286 let backend = HvpKernelBackend::probe()
5290 .expect("[bms_flex_row hvp_multi parity] backend probe must succeed on CUDA host");
5291 let stream = backend.stream.clone();
5292 let d_h = stream
5293 .clone_htod(&row_hessians)
5294 .expect("[bms_flex_row hvp_multi parity] upload h must succeed on CUDA host");
5295 let d_m = stream
5296 .clone_htod(&marginal)
5297 .expect("[bms_flex_row hvp_multi parity] upload marg must succeed on CUDA host");
5298 let d_g = stream
5299 .clone_htod(&logslope)
5300 .expect("[bms_flex_row hvp_multi parity] upload logslope must succeed on CUDA host");
5301 let storage = DeviceResidentRowHess {
5302 neglog: stream
5303 .alloc_zeros::<f64>(n)
5304 .expect("[bms_flex_row hvp_multi parity] alloc neglog"),
5305 grad: stream
5306 .alloc_zeros::<f64>(n * r)
5307 .expect("[bms_flex_row hvp_multi parity] alloc grad"),
5308 hess: d_h,
5309 marginal_design: d_m,
5310 logslope_design: d_g,
5311 n,
5312 r,
5313 block: block.clone(),
5314 primary: primary.clone(),
5315
5316 bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5317 as u64,
5318 };
5319 let scratch = bms_flex_row_hvp_multi_scratch_bytes_for_shape(n, p_total, rhs_count)
5320 .expect("storage scratch budget");
5321 assert!(
5322 scratch < storage.bytes,
5323 "multi-RHS scratch should stay below resident cache bytes"
5324 );
5325 let gpu = launch_bms_flex_row_hvp_multi(&storage, &v_rhs, rhs_count)
5326 .expect("multi-RHS HVP kernel must launch on CUDA host");
5327 assert_eq!(gpu.len(), rhs_count * p_total);
5328 for rhs in 0..rhs_count {
5329 let v = &v_rhs[rhs * p_total..(rhs + 1) * p_total];
5330 let cpu = cpu_oracle_bms_flex_row_hvp(
5331 &row_hessians,
5332 &marginal,
5333 &logslope,
5334 &block,
5335 &primary,
5336 n,
5337 v,
5338 );
5339 let single = launch_bms_flex_row_hvp(&storage, v)
5340 .expect("single-RHS HVP kernel must launch on CUDA host");
5341 for j in 0..p_total {
5342 let got = gpu[rhs * p_total + j];
5343 let diff = (cpu[j] - got).abs();
5344 assert!(
5345 diff <= 1e-10,
5346 "multi-RHS HVP rhs={rhs} j={j}: cpu={} gpu={} |diff|={diff:.3e}",
5347 cpu[j],
5348 got
5349 );
5350 assert_eq!(
5351 got, single[j],
5352 "multi-RHS and single-RHS host launch diverged at rhs={rhs} j={j}"
5353 );
5354 }
5355 }
5356 }
5357
5358 #[test]
5369 pub(crate) fn bms_flex_row_hvp_into_device_matches_cpu_oracle_and_host_out() {
5370 #[cfg(not(target_os = "linux"))]
5371 {
5372 eprintln!(
5373 "[bms_flex_row hvp_into_device parity] non-Linux host — skipping \
5374 CUDA parity (CPU oracle exercised by sibling tests)"
5375 );
5376 }
5377 #[cfg(target_os = "linux")]
5378 {
5379 if cuda_runtime_for_test("bms_flex_row hvp_into_device parity").is_none() {
5380 return;
5381 }
5382 let n = 4_usize;
5383 let r = 4_usize;
5384 let p_m = 2_usize;
5385 let p_g = 2_usize;
5386 let p_h_dim = 1_usize;
5387 let p_w_dim = 1_usize;
5388 let p_total = p_m + p_g + p_h_dim + p_w_dim;
5389 let block = BmsFlexBlockLayout {
5390 p_m,
5391 p_g,
5392 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5393 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5394 p_total,
5395 };
5396 let primary = BmsFlexPrimaryLayout {
5397 h: Some(2..3),
5398 w: Some(3..4),
5399 r,
5400 };
5401 let mut row_hessians = vec![0.0_f64; n * r * r];
5402 for row in 0..n {
5403 for u in 0..r {
5404 for v in u..r {
5405 let val = ((row + 1) as f64) * (1.0 + (u as f64) + 2.0 * (v as f64));
5406 row_hessians[row * r * r + u * r + v] = val;
5407 row_hessians[row * r * r + v * r + u] = val;
5408 }
5409 }
5410 }
5411 let mut marginal = vec![0.0_f64; n * p_m];
5412 for row in 0..n {
5413 for j in 0..p_m {
5414 marginal[row * p_m + j] = 0.5 + (row as f64) * 0.1 - (j as f64) * 0.2;
5415 }
5416 }
5417 let mut logslope = vec![0.0_f64; n * p_g];
5418 for row in 0..n {
5419 for j in 0..p_g {
5420 logslope[row * p_g + j] = -0.3 + (row as f64) * 0.05 + (j as f64) * 0.15;
5421 }
5422 }
5423 let v: Vec<f64> = (0..p_total).map(|i| 0.1 + (i as f64) * 0.25).collect();
5424 let cpu_hvp = cpu_oracle_bms_flex_row_hvp(
5425 &row_hessians,
5426 &marginal,
5427 &logslope,
5428 &block,
5429 &primary,
5430 n,
5431 &v,
5432 );
5433
5434 let backend = HvpKernelBackend::probe().expect(
5437 "[bms_flex_row hvp_into_device parity] backend probe must succeed on CUDA host",
5438 );
5439 let stream = backend.stream.clone();
5440 let d_h = stream
5441 .clone_htod(&row_hessians)
5442 .expect("[bms_flex_row hvp_into_device parity] upload h must succeed on CUDA host");
5443 let d_m = stream.clone_htod(&marginal).expect(
5444 "[bms_flex_row hvp_into_device parity] upload marg must succeed on CUDA host",
5445 );
5446 let d_g = stream.clone_htod(&logslope).expect(
5447 "[bms_flex_row hvp_into_device parity] upload logslope must succeed on CUDA host",
5448 );
5449 let storage = DeviceResidentRowHess {
5450 neglog: stream
5451 .alloc_zeros::<f64>(n)
5452 .expect("[bms_flex_row hvp_into_device parity] alloc neglog"),
5453 grad: stream
5454 .alloc_zeros::<f64>(n * r)
5455 .expect("[bms_flex_row hvp_into_device parity] alloc grad"),
5456 hess: d_h,
5457 marginal_design: d_m,
5458 logslope_design: d_g,
5459 n,
5460 r,
5461 block: block.clone(),
5462 primary: primary.clone(),
5463
5464 bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5465 as u64,
5466 };
5467
5468 let host_out_hvp = launch_bms_flex_row_hvp(&storage, &v)
5470 .expect("host-out HVP kernel must launch on CUDA host");
5471
5472 let d_v = stream
5475 .clone_htod(&v)
5476 .expect("upload direction for device-out HVP");
5477 let mut d_out = stream
5478 .alloc_zeros::<f64>(p_total)
5479 .expect("alloc device-out HVP output");
5480 launch_bms_flex_row_hvp_into_device(&storage, &d_v, &mut d_out)
5481 .expect("device-out HVP kernel must launch on CUDA host");
5482 stream
5483 .synchronize()
5484 .expect("synchronize after device-out HVP");
5485 let device_out_hvp = stream
5486 .clone_dtoh(&d_out)
5487 .expect("download device-out HVP output");
5488
5489 assert_eq!(device_out_hvp.len(), cpu_hvp.len());
5490 assert_eq!(device_out_hvp.len(), host_out_hvp.len());
5491 for i in 0..p_total {
5492 let diff = (cpu_hvp[i] - device_out_hvp[i]).abs();
5493 assert!(
5494 diff <= 1e-10,
5495 "device-out HVP[{i}] vs CPU: cpu={} gpu={} |Δ|={diff:.3e}",
5496 cpu_hvp[i],
5497 device_out_hvp[i]
5498 );
5499 let host_diff = (host_out_hvp[i] - device_out_hvp[i]).abs();
5502 assert!(
5503 host_diff == 0.0,
5504 "device-out vs host-out HVP[{i}]: host={} device={} |Δ|={host_diff:.3e}",
5505 host_out_hvp[i],
5506 device_out_hvp[i]
5507 );
5508 }
5509 }
5510 }
5511
5512 #[test]
5525 pub(crate) fn bms_flex_row_hvp_kernel_matches_cpu_oracle_at_n64_r20_p44() {
5526 #[cfg(not(target_os = "linux"))]
5527 {
5528 eprintln!(
5529 "[bms_flex_row hvp parity n64_r20_p44] non-Linux host — \
5530 skipping CUDA parity"
5531 );
5532 }
5533 #[cfg(target_os = "linux")]
5534 {
5535 if cuda_runtime_for_test("bms_flex_row hvp parity n64_r20_p44").is_none() {
5536 return;
5537 }
5538 let n = 64_usize;
5539 let p_m = 14_usize;
5540 let p_g = 12_usize;
5541 let p_h_dim = 10_usize;
5542 let p_w_dim = 8_usize;
5543 let r = 2 + p_h_dim + p_w_dim;
5544 assert_eq!(r, 20);
5545 let p_total = p_m + p_g + p_h_dim + p_w_dim;
5546 assert_eq!(p_total, 44);
5547 let block = BmsFlexBlockLayout {
5548 p_m,
5549 p_g,
5550 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5551 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5552 p_total,
5553 };
5554 let primary = BmsFlexPrimaryLayout {
5555 h: Some(2..2 + p_h_dim),
5556 w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
5557 r,
5558 };
5559
5560 let mut row_hessians = vec![0.0_f64; n * r * r];
5566 for row in 0..n {
5567 let base = row * r * r;
5568 for u in 0..r {
5569 for v in 0..r {
5570 let seed = (row as f64) * 0.137 + (u as f64) * 1.901 + (v as f64) * 0.317;
5571 let a = (seed.sin() * 1.7 + (seed * 0.5).cos() * 0.9) * 0.5;
5572 row_hessians[base + u * r + v] = a;
5573 }
5574 }
5575 for u in 0..r {
5576 for v in (u + 1)..r {
5577 let upper = row_hessians[base + u * r + v];
5578 let lower = row_hessians[base + v * r + u];
5579 let sym = 0.5 * (upper + lower);
5580 row_hessians[base + u * r + v] = sym;
5581 row_hessians[base + v * r + u] = sym;
5582 }
5583 row_hessians[base + u * r + u] += r as f64;
5584 }
5585 }
5586 let mut marginal = vec![0.0_f64; n * p_m];
5587 for row in 0..n {
5588 for j in 0..p_m {
5589 let seed = (row as f64) * 0.073 + (j as f64) * 0.211 + 0.4;
5590 marginal[row * p_m + j] = seed.sin() * 0.8 - (seed * 0.7).cos() * 0.3;
5591 }
5592 }
5593 let mut logslope = vec![0.0_f64; n * p_g];
5594 for row in 0..n {
5595 for j in 0..p_g {
5596 let seed = (row as f64) * 0.091 + (j as f64) * 0.179 - 0.2;
5597 logslope[row * p_g + j] = seed.cos() * 0.7 + (seed * 0.3).sin() * 0.25;
5598 }
5599 }
5600 let v: Vec<f64> = (0..p_total)
5601 .map(|i| {
5602 let seed = (i as f64) * 0.157 + 0.6;
5603 seed.sin() * 0.55 + (seed * 0.4).cos() * 0.35
5604 })
5605 .collect();
5606
5607 let cpu_hvp = cpu_oracle_bms_flex_row_hvp(
5608 &row_hessians,
5609 &marginal,
5610 &logslope,
5611 &block,
5612 &primary,
5613 n,
5614 &v,
5615 );
5616 let cpu_diag = cpu_oracle_bms_flex_row_diagonal(
5617 &row_hessians,
5618 &marginal,
5619 &logslope,
5620 &block,
5621 &primary,
5622 n,
5623 );
5624
5625 let backend = match HvpKernelBackend::probe() {
5626 Ok(b) => b,
5627 Err(err) => {
5628 eprintln!(
5629 "[bms_flex_row hvp parity n64_r20_p44] backend probe \
5630 failed: {err}"
5631 );
5632 return;
5633 }
5634 };
5635 let stream = backend.stream.clone();
5636 let d_h = match stream.clone_htod(&row_hessians) {
5637 Ok(s) => s,
5638 Err(err) => {
5639 eprintln!(
5640 "[bms_flex_row hvp parity n64_r20_p44] upload h \
5641 failed: {err}"
5642 );
5643 return;
5644 }
5645 };
5646 let d_m = match stream.clone_htod(&marginal) {
5647 Ok(s) => s,
5648 Err(err) => {
5649 eprintln!(
5650 "[bms_flex_row hvp parity n64_r20_p44] upload marg \
5651 failed: {err}"
5652 );
5653 return;
5654 }
5655 };
5656 let d_g = match stream.clone_htod(&logslope) {
5657 Ok(s) => s,
5658 Err(err) => {
5659 eprintln!(
5660 "[bms_flex_row hvp parity n64_r20_p44] upload logslope \
5661 failed: {err}"
5662 );
5663 return;
5664 }
5665 };
5666 let storage = DeviceResidentRowHess {
5667 neglog: stream
5668 .alloc_zeros::<f64>(n)
5669 .expect("[bms_flex_row hvp parity n64_r20_p44] alloc neglog"),
5670 grad: stream
5671 .alloc_zeros::<f64>(n * r)
5672 .expect("[bms_flex_row hvp parity n64_r20_p44] alloc grad"),
5673 hess: d_h,
5674 marginal_design: d_m,
5675 logslope_design: d_g,
5676 n,
5677 r,
5678 block: block.clone(),
5679 primary: primary.clone(),
5680
5681 bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5682 as u64,
5683 };
5684 let gpu_hvp = launch_bms_flex_row_hvp(&storage, &v)
5685 .expect("HVP kernel must launch on CUDA host at n64/r20/p44");
5686 let gpu_diag = launch_bms_flex_row_diagonal(&storage)
5687 .expect("diagonal kernel must launch on CUDA host at n64/r20/p44");
5688 assert_eq!(gpu_hvp.len(), cpu_hvp.len());
5689 assert_eq!(gpu_diag.len(), cpu_diag.len());
5690 for i in 0..p_total {
5691 let diff = (cpu_hvp[i] - gpu_hvp[i]).abs();
5692 assert!(
5693 diff <= 1e-8,
5694 "n64_r20_p44 HVP[{i}]: cpu={} gpu={} |Δ|={diff:.3e}",
5695 cpu_hvp[i],
5696 gpu_hvp[i]
5697 );
5698 let ddiff = (cpu_diag[i] - gpu_diag[i]).abs();
5699 assert!(
5700 ddiff <= 1e-8,
5701 "n64_r20_p44 diag[{i}]: cpu={} gpu={} |Δ|={ddiff:.3e}",
5702 cpu_diag[i],
5703 gpu_diag[i]
5704 );
5705 }
5706 }
5707 }
5708
5709 #[test]
5715 pub(crate) fn bms_flex_row_dense_block_kernel_matches_cpu_pullback() {
5716 #[cfg(not(target_os = "linux"))]
5717 {
5718 eprintln!("[bms_flex_row dense_block parity] non-Linux host — skipping CUDA parity");
5719 }
5720 #[cfg(target_os = "linux")]
5721 {
5722 if cuda_runtime_for_test("bms_flex_row dense_block parity").is_none() {
5723 return;
5724 }
5725 let n = 24_usize;
5729 let p_m = 4_usize;
5730 let p_g = 4_usize;
5731 let p_h_dim = 3_usize;
5732 let p_w_dim = 3_usize;
5733 let r = 2 + p_h_dim + p_w_dim;
5734 let p_total = p_m + p_g + p_h_dim + p_w_dim;
5735 let block = BmsFlexBlockLayout {
5736 p_m,
5737 p_g,
5738 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
5739 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
5740 p_total,
5741 };
5742 let primary = BmsFlexPrimaryLayout {
5743 h: Some(2..2 + p_h_dim),
5744 w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
5745 r,
5746 };
5747
5748 let mut row_hessians = vec![0.0_f64; n * r * r];
5749 for row in 0..n {
5750 let base = row * r * r;
5751 for u in 0..r {
5752 for v in 0..r {
5753 let seed = (row as f64) * 0.21 + (u as f64) * 1.13 + (v as f64) * 0.47;
5754 let a = (seed.sin() * 1.4 + (seed * 0.6).cos() * 0.7) * 0.5;
5755 row_hessians[base + u * r + v] = a;
5756 }
5757 }
5758 for u in 0..r {
5759 for v in (u + 1)..r {
5760 let upper = row_hessians[base + u * r + v];
5761 let lower = row_hessians[base + v * r + u];
5762 let sym = 0.5 * (upper + lower);
5763 row_hessians[base + u * r + v] = sym;
5764 row_hessians[base + v * r + u] = sym;
5765 }
5766 row_hessians[base + u * r + u] += r as f64;
5767 }
5768 }
5769 let mut marginal = vec![0.0_f64; n * p_m];
5770 for row in 0..n {
5771 for j in 0..p_m {
5772 let seed = (row as f64) * 0.083 + (j as f64) * 0.171 + 0.31;
5773 marginal[row * p_m + j] = seed.sin() * 0.7 - (seed * 0.5).cos() * 0.25;
5774 }
5775 }
5776 let mut logslope = vec![0.0_f64; n * p_g];
5777 for row in 0..n {
5778 for j in 0..p_g {
5779 let seed = (row as f64) * 0.097 + (j as f64) * 0.143 - 0.15;
5780 logslope[row * p_g + j] = seed.cos() * 0.65 + (seed * 0.4).sin() * 0.2;
5781 }
5782 }
5783
5784 let h_block_start = block.h.as_ref().map(|r| r.start).unwrap_or(0);
5786 let h_block_len = block.h.as_ref().map(|r| r.len()).unwrap_or(0);
5787 let w_block_start = block.w.as_ref().map(|r| r.start).unwrap_or(0);
5788 let w_block_len = block.w.as_ref().map(|r| r.len()).unwrap_or(0);
5789 let h_primary_start = primary.h.as_ref().map(|r| r.start).unwrap_or(0);
5790 let w_primary_start = primary.w.as_ref().map(|r| r.start).unwrap_or(0);
5791 let mut h_cpu = vec![0.0_f64; p_total * p_total];
5792 for row in 0..n {
5793 let mrow = &marginal[row * p_m..(row + 1) * p_m];
5794 let grow = &logslope[row * p_g..(row + 1) * p_g];
5795 let hrow = &row_hessians[row * r * r..(row + 1) * r * r];
5796 let mut phi = vec![vec![0.0_f64; p_total]; r];
5798 for k in 0..p_m {
5799 phi[0][k] = mrow[k];
5800 }
5801 for k in 0..p_g {
5802 phi[1][p_m + k] = grow[k];
5803 }
5804 for k in 0..h_block_len {
5805 phi[h_primary_start + k][h_block_start + k] = 1.0;
5806 }
5807 for k in 0..w_block_len {
5808 phi[w_primary_start + k][w_block_start + k] = 1.0;
5809 }
5810 for u in 0..r {
5811 for v in 0..r {
5812 let huv = hrow[u * r + v];
5813 if huv == 0.0 {
5814 continue;
5815 }
5816 for m in 0..p_total {
5817 let pm = phi[u][m];
5818 if pm == 0.0 {
5819 continue;
5820 }
5821 let scaled = huv * pm;
5822 for nn in 0..p_total {
5823 h_cpu[m * p_total + nn] += scaled * phi[v][nn];
5824 }
5825 }
5826 }
5827 }
5828 }
5829
5830 let backend = HvpKernelBackend::probe().expect(
5835 "[bms_flex_row dense_block parity] backend probe must succeed on CUDA host",
5836 );
5837 let stream = backend.stream.clone();
5838 let d_h = stream
5839 .clone_htod(&row_hessians)
5840 .expect("[bms_flex_row dense_block parity] upload h must succeed on CUDA host");
5841 let d_m = stream
5842 .clone_htod(&marginal)
5843 .expect("[bms_flex_row dense_block parity] upload marg must succeed on CUDA host");
5844 let d_g = stream.clone_htod(&logslope).expect(
5845 "[bms_flex_row dense_block parity] upload logslope must succeed on CUDA host",
5846 );
5847 let storage = DeviceResidentRowHess {
5848 neglog: stream
5849 .alloc_zeros::<f64>(n)
5850 .expect("[bms_flex_row dense_block parity] alloc neglog"),
5851 grad: stream
5852 .alloc_zeros::<f64>(n * r)
5853 .expect("[bms_flex_row dense_block parity] alloc grad"),
5854 hess: d_h,
5855 marginal_design: d_m,
5856 logslope_design: d_g,
5857 n,
5858 r,
5859 block: block.clone(),
5860 primary: primary.clone(),
5861
5862 bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5863 as u64,
5864 };
5865 let h_gpu = launch_bms_flex_row_dense_block(&storage)
5866 .expect("dense_block kernel must launch on CUDA host");
5867 assert_eq!(h_gpu.len(), p_total * p_total);
5868
5869 let mut max_abs = 0.0_f64;
5872 for i in 0..p_total {
5873 for j in 0..p_total {
5874 let a = h_cpu[i * p_total + j];
5875 let b = h_gpu[i * p_total + j];
5876 let diff = (a - b).abs();
5877 if diff > max_abs {
5878 max_abs = diff;
5879 }
5880 assert!(
5881 diff <= 1e-9 * a.abs().max(b.abs()).max(1.0),
5882 "dense_block[{i},{j}]: cpu={a} gpu={b} |Δ|={diff:.3e}"
5883 );
5884 }
5885 }
5886 eprintln!(
5887 "[bms_flex_row dense_block parity] n={n} r={r} p={p_total}: max|Δ|={max_abs:.3e}"
5888 );
5889 }
5890 }
5891
5892 #[test]
5893 pub(crate) fn bms_flex_row_dense_hvp_materialization_matches_cpu_above_block_cap_932() {
5894 if cuda_runtime_for_test("bms_flex_row dense HVP parity").is_none() {
5895 return;
5896 }
5897 let n = 1_usize;
5898 let r = 2_usize;
5899 let p_m = 37_usize;
5900 let p_g = 36_usize;
5901 let p_total = p_m + p_g;
5902 assert_eq!(p_total, DENSE_BLOCK_MAX_P + 1);
5903 let block = BmsFlexBlockLayout {
5904 p_m,
5905 p_g,
5906 h: None,
5907 w: None,
5908 p_total,
5909 };
5910 let primary = BmsFlexPrimaryLayout {
5911 h: None,
5912 w: None,
5913 r,
5914 };
5915 let row_hessians = vec![2.5_f64, -0.75, -0.75, 1.25];
5916 let marginal = (0..p_m)
5917 .map(|column| 0.15 + (column as f64 * 0.17).sin())
5918 .collect::<Vec<_>>();
5919 let logslope = (0..p_g)
5920 .map(|column| -0.2 + (column as f64 * 0.11).cos())
5921 .collect::<Vec<_>>();
5922 let mut expected = vec![0.0_f64; p_total * p_total];
5923 for row in 0..p_total {
5924 for column in 0..p_total {
5925 expected[row * p_total + column] = match (row < p_m, column < p_m) {
5926 (true, true) => row_hessians[0] * marginal[row] * marginal[column],
5927 (true, false) => row_hessians[1] * marginal[row] * logslope[column - p_m],
5928 (false, true) => row_hessians[2] * logslope[row - p_m] * marginal[column],
5929 (false, false) => {
5930 row_hessians[3] * logslope[row - p_m] * logslope[column - p_m]
5931 }
5932 };
5933 }
5934 }
5935
5936 let backend = HvpKernelBackend::probe()
5937 .expect("[bms_flex_row dense HVP parity] backend probe must succeed");
5938 let stream = backend.stream.clone();
5939 let storage = DeviceResidentRowHess {
5940 neglog: stream
5941 .alloc_zeros::<f64>(n)
5942 .expect("dense HVP parity neglog allocation"),
5943 grad: stream
5944 .alloc_zeros::<f64>(n * r)
5945 .expect("dense HVP parity grad allocation"),
5946 hess: stream
5947 .clone_htod(&row_hessians)
5948 .expect("dense HVP parity hessian upload"),
5949 marginal_design: stream
5950 .clone_htod(&marginal)
5951 .expect("dense HVP parity marginal upload"),
5952 logslope_design: stream
5953 .clone_htod(&logslope)
5954 .expect("dense HVP parity logslope upload"),
5955 n,
5956 r,
5957 block,
5958 primary,
5959 bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
5960 as u64,
5961 };
5962 let actual = launch_bms_flex_row_dense(&storage)
5963 .expect("wide dense HVP materialization must stay on CUDA");
5964 assert_eq!(actual.len(), expected.len());
5965 for (index, (&actual, &expected)) in actual.iter().zip(&expected).enumerate() {
5966 let tolerance = 1.0e-10 * actual.abs().max(expected.abs()).max(1.0);
5967 assert!(
5968 (actual - expected).abs() <= tolerance,
5969 "wide dense entry {index}: CUDA={actual:.17e} CPU={expected:.17e} tolerance={tolerance:.3e}"
5970 );
5971 }
5972 }
5973
5974 #[test]
5996 pub(crate) fn bms_flex_row_hvp_dispatch_worthiness_at_large_scale() {
5997 #[cfg(not(target_os = "linux"))]
5998 {
5999 eprintln!("[bms_flex_row hvp hill-climb] non-Linux host — skipping V100 perf gate");
6000 }
6001 #[cfg(target_os = "linux")]
6002 {
6003 let Some(runtime) = cuda_runtime_for_test("bms_flex_row hvp hill-climb") else {
6004 return;
6005 };
6006 let n = 195_000_usize;
6007 let p_m = 14_usize;
6008 let p_g = 12_usize;
6009 let p_h_dim = 10_usize;
6010 let p_w_dim = 8_usize;
6011 let r = 2 + p_h_dim + p_w_dim;
6012 let p_total = p_m + p_g + p_h_dim + p_w_dim;
6013 let block = BmsFlexBlockLayout {
6014 p_m,
6015 p_g,
6016 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
6017 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
6018 p_total,
6019 };
6020 let primary = BmsFlexPrimaryLayout {
6021 h: Some(2..2 + p_h_dim),
6022 w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
6023 r,
6024 };
6025
6026 let mut row_hessians = vec![0.0_f64; n * r * r];
6028 for row in 0..n {
6029 let base = row * r * r;
6030 for u in 0..r {
6031 for vv in 0..r {
6032 let seed = (row as f64) * 0.137 + (u as f64) * 1.901 + (vv as f64) * 0.317;
6033 let a = (seed.sin() * 1.7 + (seed * 0.5).cos() * 0.9) * 0.5;
6034 row_hessians[base + u * r + vv] = a;
6035 }
6036 }
6037 for u in 0..r {
6038 for vv in (u + 1)..r {
6039 let upper = row_hessians[base + u * r + vv];
6040 let lower = row_hessians[base + vv * r + u];
6041 let sym = 0.5 * (upper + lower);
6042 row_hessians[base + u * r + vv] = sym;
6043 row_hessians[base + vv * r + u] = sym;
6044 }
6045 row_hessians[base + u * r + u] += r as f64;
6046 }
6047 }
6048 let mut marginal = vec![0.0_f64; n * p_m];
6049 for row in 0..n {
6050 for j in 0..p_m {
6051 let seed = (row as f64) * 0.073 + (j as f64) * 0.211 + 0.4;
6052 marginal[row * p_m + j] = seed.sin() * 0.8 - (seed * 0.7).cos() * 0.3;
6053 }
6054 }
6055 let mut logslope = vec![0.0_f64; n * p_g];
6056 for row in 0..n {
6057 for j in 0..p_g {
6058 let seed = (row as f64) * 0.091 + (j as f64) * 0.179 - 0.2;
6059 logslope[row * p_g + j] = seed.cos() * 0.7 + (seed * 0.3).sin() * 0.25;
6060 }
6061 }
6062 let v: Vec<f64> = (0..p_total)
6063 .map(|i| {
6064 let seed = (i as f64) * 0.157 + 0.6;
6065 seed.sin() * 0.55 + (seed * 0.4).cos() * 0.35
6066 })
6067 .collect();
6068
6069 let backend = match HvpKernelBackend::probe() {
6071 Ok(b) => b,
6072 Err(err) => {
6073 eprintln!("[bms_flex_row hvp hill-climb] backend probe failed: {err}");
6074 return;
6075 }
6076 };
6077 let stream = backend.stream.clone();
6078 let d_h = match stream.clone_htod(&row_hessians) {
6079 Ok(s) => s,
6080 Err(err) => {
6081 eprintln!("[bms_flex_row hvp hill-climb] upload h failed (likely OOM): {err}");
6082 return;
6083 }
6084 };
6085 let d_m = match stream.clone_htod(&marginal) {
6086 Ok(s) => s,
6087 Err(err) => {
6088 eprintln!("[bms_flex_row hvp hill-climb] upload marg failed: {err}");
6089 return;
6090 }
6091 };
6092 let d_g = match stream.clone_htod(&logslope) {
6093 Ok(s) => s,
6094 Err(err) => {
6095 eprintln!("[bms_flex_row hvp hill-climb] upload logslope failed: {err}");
6096 return;
6097 }
6098 };
6099 let storage = DeviceResidentRowHess {
6100 neglog: stream
6101 .alloc_zeros::<f64>(n)
6102 .expect("[bms_flex_row hvp hill-climb] alloc neglog"),
6103 grad: stream
6104 .alloc_zeros::<f64>(n * r)
6105 .expect("[bms_flex_row hvp hill-climb] alloc grad"),
6106 hess: d_h,
6107 marginal_design: d_m,
6108 logslope_design: d_g,
6109 n,
6110 r,
6111 block: block.clone(),
6112 primary: primary.clone(),
6113
6114 bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
6115 as u64,
6116 };
6117 let warmup: usize = 3;
6118 let iters: usize = 15;
6119 for _ in 0..warmup {
6120 let out =
6121 launch_bms_flex_row_hvp(&storage, &v).expect("warmup GPU HVP must launch");
6122 assert_eq!(out.len(), p_total);
6123 }
6124 let mut gpu_us: Vec<u128> = Vec::with_capacity(iters);
6125 for _ in 0..iters {
6126 let t0 = std::time::Instant::now();
6127 let out = launch_bms_flex_row_hvp(&storage, &v).expect("GPU HVP must launch");
6128 gpu_us.push(t0.elapsed().as_micros());
6129 assert_eq!(out.len(), p_total);
6130 }
6131 gpu_us.sort_unstable();
6132 let gpu_median = gpu_us[iters / 2];
6133
6134 const CHUNK_ROWS: usize = 4096;
6140 let cpu_hvp_parallel = || -> Vec<f64> {
6141 let nchunks = n.div_ceil(CHUNK_ROWS);
6142 gam_linalg::pairwise_reduce::par_deterministic_block_fold(
6143 nchunks,
6144 |ci_range| {
6145 let mut acc = vec![0.0_f64; p_total];
6146 for ci in ci_range {
6147 let lo = ci * CHUNK_ROWS;
6148 let hi = (lo + CHUNK_ROWS).min(n);
6149 let m = hi - lo;
6150 let partial = cpu_oracle_bms_flex_row_hvp(
6151 &row_hessians[lo * r * r..hi * r * r],
6152 &marginal[lo * p_m..hi * p_m],
6153 &logslope[lo * p_g..hi * p_g],
6154 &block,
6155 &primary,
6156 m,
6157 &v,
6158 );
6159 for (a, &p) in acc.iter_mut().zip(partial.iter()) {
6160 *a += p;
6161 }
6162 }
6163 acc
6164 },
6165 |mut a, b| {
6166 for (ax, bx) in a.iter_mut().zip(b.iter()) {
6167 *ax += *bx;
6168 }
6169 a
6170 },
6171 )
6172 .unwrap_or_else(|| vec![0.0_f64; p_total])
6173 };
6174 let warm = cpu_hvp_parallel();
6176 assert_eq!(warm.len(), p_total);
6177 let mut cpu_us: Vec<u128> = Vec::with_capacity(iters);
6178 for _ in 0..iters {
6179 let t0 = std::time::Instant::now();
6180 let out = cpu_hvp_parallel();
6181 cpu_us.push(t0.elapsed().as_micros());
6182 assert_eq!(out.len(), p_total);
6183 }
6184 cpu_us.sort_unstable();
6185 let cpu_median = cpu_us[iters / 2];
6186
6187 let speedup = (cpu_median as f64) / (gpu_median.max(1) as f64);
6188 eprintln!(
6189 "[bms_flex_row hvp hill-climb] large-scale n={n} r={r} p={p_total}: \
6190 cpu_median={cpu_median}us gpu_median={gpu_median}us \
6191 speedup={speedup:.2}× (perf record; the gate is the policy decision)"
6192 );
6193 assert_row_batch_dispatch_worthy(
6194 "large-scale HVP dispatch-worthiness gate",
6195 runtime.policy(),
6196 n,
6197 );
6198 }
6199 }
6200
6201 #[test]
6206 pub(crate) fn bms_flex_row_dense_block_dispatch_worthiness_at_large_scale() {
6207 #[cfg(not(target_os = "linux"))]
6208 {
6209 eprintln!(
6210 "[bms_flex_row dense_block hill-climb] non-Linux host — skipping V100 perf gate"
6211 );
6212 }
6213 #[cfg(target_os = "linux")]
6214 {
6215 let Some(runtime) = cuda_runtime_for_test("bms_flex_row dense_block hill-climb")
6216 else {
6217 return;
6218 };
6219 let n = 195_000_usize;
6220 let p_m = 14_usize;
6221 let p_g = 12_usize;
6222 let p_h_dim = 10_usize;
6223 let p_w_dim = 8_usize;
6224 let r = 2 + p_h_dim + p_w_dim;
6225 let p_total = p_m + p_g + p_h_dim + p_w_dim;
6226 let block = BmsFlexBlockLayout {
6227 p_m,
6228 p_g,
6229 h: Some(p_m + p_g..p_m + p_g + p_h_dim),
6230 w: Some(p_m + p_g + p_h_dim..p_m + p_g + p_h_dim + p_w_dim),
6231 p_total,
6232 };
6233 let primary = BmsFlexPrimaryLayout {
6234 h: Some(2..2 + p_h_dim),
6235 w: Some(2 + p_h_dim..2 + p_h_dim + p_w_dim),
6236 r,
6237 };
6238
6239 let mut row_hessians = vec![0.0_f64; n * r * r];
6241 for row in 0..n {
6242 let base = row * r * r;
6243 for u in 0..r {
6244 for vv in 0..r {
6245 let seed = (row as f64) * 0.137 + (u as f64) * 1.901 + (vv as f64) * 0.317;
6246 let a = (seed.sin() * 1.7 + (seed * 0.5).cos() * 0.9) * 0.5;
6247 row_hessians[base + u * r + vv] = a;
6248 }
6249 }
6250 for u in 0..r {
6251 for vv in (u + 1)..r {
6252 let upper = row_hessians[base + u * r + vv];
6253 let lower = row_hessians[base + vv * r + u];
6254 let sym = 0.5 * (upper + lower);
6255 row_hessians[base + u * r + vv] = sym;
6256 row_hessians[base + vv * r + u] = sym;
6257 }
6258 row_hessians[base + u * r + u] += r as f64;
6259 }
6260 }
6261 let mut marginal = vec![0.0_f64; n * p_m];
6262 for row in 0..n {
6263 for j in 0..p_m {
6264 let seed = (row as f64) * 0.073 + (j as f64) * 0.211 + 0.4;
6265 marginal[row * p_m + j] = seed.sin() * 0.8 - (seed * 0.7).cos() * 0.3;
6266 }
6267 }
6268 let mut logslope = vec![0.0_f64; n * p_g];
6269 for row in 0..n {
6270 for j in 0..p_g {
6271 let seed = (row as f64) * 0.091 + (j as f64) * 0.179 - 0.2;
6272 logslope[row * p_g + j] = seed.cos() * 0.7 + (seed * 0.3).sin() * 0.25;
6273 }
6274 }
6275
6276 if p_total > DENSE_BLOCK_MAX_P {
6279 eprintln!(
6280 "[bms_flex_row dense_block hill-climb] p_total={p_total} > MAX={DENSE_BLOCK_MAX_P}, skipping"
6281 );
6282 return;
6283 }
6284 let backend = match HvpKernelBackend::probe() {
6285 Ok(b) => b,
6286 Err(err) => {
6287 eprintln!("[bms_flex_row dense_block hill-climb] backend probe failed: {err}");
6288 return;
6289 }
6290 };
6291 let stream = backend.stream.clone();
6292 let d_h = match stream.clone_htod(&row_hessians) {
6293 Ok(s) => s,
6294 Err(err) => {
6295 eprintln!("[bms_flex_row dense_block hill-climb] upload h failed: {err}");
6296 return;
6297 }
6298 };
6299 let d_m = match stream.clone_htod(&marginal) {
6300 Ok(s) => s,
6301 Err(err) => {
6302 eprintln!("[bms_flex_row dense_block hill-climb] upload marg failed: {err}");
6303 return;
6304 }
6305 };
6306 let d_g = match stream.clone_htod(&logslope) {
6307 Ok(s) => s,
6308 Err(err) => {
6309 eprintln!(
6310 "[bms_flex_row dense_block hill-climb] upload logslope failed: {err}"
6311 );
6312 return;
6313 }
6314 };
6315 let storage = DeviceResidentRowHess {
6316 neglog: stream
6317 .alloc_zeros::<f64>(n)
6318 .expect("[bms_flex_row dense_block hill-climb] alloc neglog"),
6319 grad: stream
6320 .alloc_zeros::<f64>(n * r)
6321 .expect("[bms_flex_row dense_block hill-climb] alloc grad"),
6322 hess: d_h,
6323 marginal_design: d_m,
6324 logslope_design: d_g,
6325 n,
6326 r,
6327 block: block.clone(),
6328 primary: primary.clone(),
6329
6330 bytes: ((n + n * r + n * r * r + n * p_m + n * p_g) * std::mem::size_of::<f64>())
6331 as u64,
6332 };
6333 let warmup: usize = 2;
6335 let iters: usize = 5;
6336 for _ in 0..warmup {
6337 let out = launch_bms_flex_row_dense_block(&storage)
6338 .expect("warmup GPU dense_block must launch");
6339 assert_eq!(out.len(), p_total * p_total);
6340 }
6341 let mut gpu_us: Vec<u128> = Vec::with_capacity(iters);
6342 for _ in 0..iters {
6343 let t0 = std::time::Instant::now();
6344 let out =
6345 launch_bms_flex_row_dense_block(&storage).expect("GPU dense_block must launch");
6346 gpu_us.push(t0.elapsed().as_micros());
6347 assert_eq!(out.len(), p_total * p_total);
6348 }
6349 gpu_us.sort_unstable();
6350 let gpu_median = gpu_us[iters / 2];
6351
6352 const CHUNK_ROWS: usize = 2048;
6355 let h_block_start = block.h.as_ref().map(|r| r.start).unwrap_or(0);
6356 let h_block_len = block.h.as_ref().map(|r| r.len()).unwrap_or(0);
6357 let w_block_start = block.w.as_ref().map(|r| r.start).unwrap_or(0);
6358 let w_block_len = block.w.as_ref().map(|r| r.len()).unwrap_or(0);
6359 let h_primary_start = primary.h.as_ref().map(|r| r.start).unwrap_or(0);
6360 let w_primary_start = primary.w.as_ref().map(|r| r.start).unwrap_or(0);
6361 let cpu_build_parallel = || -> Vec<f64> {
6362 let nchunks = n.div_ceil(CHUNK_ROWS);
6363 gam_linalg::pairwise_reduce::par_deterministic_block_fold(
6364 nchunks,
6365 |ci_range| {
6366 let mut acc = vec![0.0_f64; p_total * p_total];
6367 let mut phi: Vec<Vec<f64>> = vec![vec![0.0_f64; p_total]; r];
6368 for ci in ci_range {
6369 let lo = ci * CHUNK_ROWS;
6370 let hi = (lo + CHUNK_ROWS).min(n);
6371 for row in lo..hi {
6372 for col in phi.iter_mut() {
6373 col.iter_mut().for_each(|v| *v = 0.0);
6374 }
6375 let mrow = &marginal[row * p_m..(row + 1) * p_m];
6376 let grow = &logslope[row * p_g..(row + 1) * p_g];
6377 for k in 0..p_m {
6378 phi[0][k] = mrow[k];
6379 }
6380 for k in 0..p_g {
6381 phi[1][p_m + k] = grow[k];
6382 }
6383 for k in 0..h_block_len {
6384 phi[h_primary_start + k][h_block_start + k] = 1.0;
6385 }
6386 for k in 0..w_block_len {
6387 phi[w_primary_start + k][w_block_start + k] = 1.0;
6388 }
6389 let hrow = &row_hessians[row * r * r..(row + 1) * r * r];
6390 for u in 0..r {
6391 for v_idx in 0..r {
6392 let huv = hrow[u * r + v_idx];
6393 if huv == 0.0 {
6394 continue;
6395 }
6396 for m in 0..p_total {
6397 let pm = phi[u][m];
6398 if pm == 0.0 {
6399 continue;
6400 }
6401 let scaled = huv * pm;
6402 for nn in 0..p_total {
6403 acc[m * p_total + nn] += scaled * phi[v_idx][nn];
6404 }
6405 }
6406 }
6407 }
6408 }
6409 }
6410 acc
6411 },
6412 |mut a, b| {
6413 for (ax, bx) in a.iter_mut().zip(b.iter()) {
6414 *ax += *bx;
6415 }
6416 a
6417 },
6418 )
6419 .unwrap_or_else(|| vec![0.0_f64; p_total * p_total])
6420 };
6421 let warm_cpu = cpu_build_parallel();
6422 assert_eq!(warm_cpu.len(), p_total * p_total);
6423 let mut cpu_us: Vec<u128> = Vec::with_capacity(iters);
6424 for _ in 0..iters {
6425 let t0 = std::time::Instant::now();
6426 let out = cpu_build_parallel();
6427 cpu_us.push(t0.elapsed().as_micros());
6428 assert_eq!(out.len(), p_total * p_total);
6429 }
6430 cpu_us.sort_unstable();
6431 let cpu_median = cpu_us[iters / 2];
6432
6433 let speedup = (cpu_median as f64) / (gpu_median.max(1) as f64);
6434 eprintln!(
6435 "[bms_flex_row dense_block hill-climb] large-scale n={n} r={r} p={p_total}: \
6436 cpu_median={cpu_median}us gpu_median={gpu_median}us \
6437 speedup={speedup:.2}× (perf record; the gate is the policy decision)"
6438 );
6439 assert_row_batch_dispatch_worthy(
6440 "large-scale dense-H dispatch-worthiness gate",
6441 runtime.policy(),
6442 n,
6443 );
6444 }
6445 }
6446}