1use ndarray::Array1;
11
12use crate::gpu_kernels::arrow_schur::{
13 ArrowSchurGpuFailure, solve_arrow_newton_step, solve_arrow_newton_step_dense_reference,
14};
15use gam_problem::ExecutionPath;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum InnerSolveMode {
43 DeviceResident,
44 DeviceReupload,
45 CpuReference,
46 CpuArrow,
47}
48
49impl InnerSolveMode {
50 #[inline]
55 const fn execution_path(self) -> ExecutionPath {
56 match self {
57 Self::DeviceResident => ExecutionPath::GpuResidentFull,
58 Self::DeviceReupload => ExecutionPath::GpuReupload,
59 Self::CpuReference | Self::CpuArrow => ExecutionPath::Cpu,
60 }
61 }
62
63 #[inline]
71 const fn operator_on_device(self) -> bool {
72 match self {
73 Self::DeviceResident | Self::DeviceReupload => true,
74 Self::CpuReference | Self::CpuArrow => false,
75 }
76 }
77}
78use crate::arrow_schur::{ArrowSchurError, ArrowSchurSystem};
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub struct DeviceResidentArrowShape {
88 pub n: usize,
89 pub p: usize,
90 pub basis_cols: usize,
91 pub d: usize,
92}
93
94impl DeviceResidentArrowShape {
95 #[inline]
96 pub const fn qwen_non_gating() -> Self {
97 Self {
98 n: 2_000,
99 p: 2_048,
100 basis_cols: 8,
101 d: 2,
102 }
103 }
104
105 #[inline]
109 pub const fn color_arm() -> Self {
110 Self {
111 n: 180,
112 p: 5_120,
113 basis_cols: 9,
114 d: 2,
115 }
116 }
117
118 #[inline]
119 pub const fn target_len(self) -> usize {
120 self.n * self.p
121 }
122
123 #[inline]
124 pub const fn basis_len(self) -> usize {
125 self.n * self.basis_cols
126 }
127
128 #[inline]
129 pub const fn row_hessian_len(self) -> usize {
130 self.n * self.d * self.d
131 }
132
133 #[inline]
134 pub const fn row_cross_len(self) -> usize {
135 self.n * self.d * self.p
136 }
137
138 #[inline]
139 pub const fn row_gradient_len(self) -> usize {
140 self.n * self.d
141 }
142
143 #[inline]
144 pub const fn border_hessian_len(self) -> usize {
145 self.p * self.p
146 }
147}
148
149#[derive(Clone, Debug)]
156pub struct DeviceResidentArrowSlabs {
157 pub row_hessian_slabs: Vec<f64>,
158 pub row_cross_slabs: Vec<f64>,
159 pub row_gradient_slabs: Vec<f64>,
160 pub border_hessian: Vec<f64>,
161 pub border_gradient: Vec<f64>,
162}
163
164#[derive(Clone, Debug)]
166pub struct DeviceResidentArrowStep {
167 pub delta_t: Array1<f64>,
168 pub delta_beta: Array1<f64>,
169 pub objective: f64,
170 pub gradient_norm: f64,
171 pub log_det_hessian: f64,
172 pub execution_path: ExecutionPath,
173}
174
175#[derive(Debug, Clone)]
176pub enum DeviceResidentArrowError {
177 Shape { reason: String },
178 Unavailable { reason: String },
179 Solve { reason: String },
180}
181
182impl std::fmt::Display for DeviceResidentArrowError {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 match self {
185 Self::Shape { reason } | Self::Unavailable { reason } | Self::Solve { reason } => {
186 f.write_str(reason)
187 }
188 }
189 }
190}
191
192impl std::error::Error for DeviceResidentArrowError {}
193
194#[cfg(target_os = "linux")]
195pub struct DeviceResidentArrowBuffers {
196 pub stream: std::sync::Arc<cudarc::driver::CudaStream>,
197 pub target_x_dev: cudarc::driver::CudaSlice<f64>,
198 pub basis_values_dev: cudarc::driver::CudaSlice<f64>,
199 pub gate_activations_dev: cudarc::driver::CudaSlice<f64>,
200 pub row_hessian_dev: cudarc::driver::CudaSlice<f64>,
201 pub row_cross_dev: cudarc::driver::CudaSlice<f64>,
202 pub row_gradient_dev: cudarc::driver::CudaSlice<f64>,
203 pub border_hessian_dev: cudarc::driver::CudaSlice<f64>,
204 pub border_gradient_dev: cudarc::driver::CudaSlice<f64>,
205 pub bytes: usize,
206 blas: cudarc::cublas::CudaBlas,
211 operator_scratch: std::sync::Mutex<DeviceOperatorScratch>,
217}
218
219#[cfg(target_os = "linux")]
225struct DeviceOperatorScratch {
226 t_dev: cudarc::driver::CudaSlice<f64>,
227 beta_dev: cudarc::driver::CudaSlice<f64>,
228 cross_t_dev: cudarc::driver::CudaSlice<f64>,
229 cross_beta_dev: cudarc::driver::CudaSlice<f64>,
230 border_beta_dev: cudarc::driver::CudaSlice<f64>,
231}
232
233#[derive(Clone, Debug)]
260struct ArrowOperatorApply {
261 cross_t: Vec<f64>,
262 cross_beta: Vec<f64>,
263 border_beta: Vec<f64>,
264}
265
266pub struct DeviceResidentArrowWorkspace {
268 shape: DeviceResidentArrowShape,
269 target_x: Vec<f64>,
270 basis_values: Vec<f64>,
271 gate_activations: Vec<f64>,
272 slabs: DeviceResidentArrowSlabs,
273 #[cfg(target_os = "linux")]
274 device: Option<DeviceResidentArrowBuffers>,
275}
276
277impl DeviceResidentArrowWorkspace {
278 pub fn new(
279 shape: DeviceResidentArrowShape,
280 target_x: Vec<f64>,
281 basis_values: Vec<f64>,
282 gate_activations: Vec<f64>,
283 slabs: DeviceResidentArrowSlabs,
284 ) -> Result<Self, DeviceResidentArrowError> {
285 validate_shape(shape, &target_x, &basis_values, &gate_activations, &slabs)?;
286 #[cfg(target_os = "linux")]
287 let device =
288 upload_resident_buffers(shape, &target_x, &basis_values, &gate_activations, &slabs);
289 Ok(Self {
290 shape,
291 target_x,
292 basis_values,
293 gate_activations,
294 slabs,
295 #[cfg(target_os = "linux")]
296 device,
297 })
298 }
299
300 #[inline]
301 pub const fn shape(&self) -> DeviceResidentArrowShape {
302 self.shape
303 }
304
305 #[must_use]
306 pub fn device_resident(&self) -> bool {
307 #[cfg(target_os = "linux")]
308 {
309 self.device.is_some()
310 }
311 #[cfg(not(target_os = "linux"))]
312 {
313 false
314 }
315 }
316
317 #[must_use]
318 pub fn resident_device_bytes(&self) -> usize {
319 #[cfg(target_os = "linux")]
320 {
321 self.device.as_ref().map_or(0, |device| device.bytes)
322 }
323 #[cfg(not(target_os = "linux"))]
324 {
325 0
326 }
327 }
328
329 #[must_use]
334 fn context_id(&self) -> usize {
335 usize::from(self.device_resident())
336 }
337
338 #[must_use]
341 fn frame_upload_bytes(&self) -> usize {
342 [
343 self.slabs.row_hessian_slabs.len(),
344 self.slabs.row_cross_slabs.len(),
345 self.slabs.row_gradient_slabs.len(),
346 self.slabs.border_hessian.len(),
347 self.slabs.border_gradient.len(),
348 ]
349 .into_iter()
350 .sum::<usize>()
351 * std::mem::size_of::<f64>()
352 }
353
354 #[must_use]
355 pub fn host_shadow_bytes(&self) -> usize {
356 [
357 self.target_x.len(),
358 self.basis_values.len(),
359 self.gate_activations.len(),
360 self.slabs.row_hessian_slabs.len(),
361 self.slabs.row_cross_slabs.len(),
362 self.slabs.row_gradient_slabs.len(),
363 self.slabs.border_hessian.len(),
364 self.slabs.border_gradient.len(),
365 ]
366 .into_iter()
367 .sum::<usize>()
368 * std::mem::size_of::<f64>()
369 }
370
371 pub fn one_inner_iteration(
374 &self,
375 ridge_t: f64,
376 ridge_beta: f64,
377 ) -> Result<DeviceResidentArrowStep, DeviceResidentArrowError> {
378 if !self.device_resident() {
379 return Err(DeviceResidentArrowError::Unavailable {
380 reason: "SAE resident inner iteration unavailable: CUDA runtime did not admit the qwen-scale row-block workload".to_string(),
381 });
382 }
383 let sys = self.to_arrow_system();
384 let frame = crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
385 &sys, ridge_t, ridge_beta,
386 )
387 .map_err(map_gpu_error)?;
388 let g_t: Vec<f64> = sys
389 .rows
390 .iter()
391 .flat_map(|row| row.gt.iter().copied())
392 .collect();
393 let g_beta: Vec<f64> = sys.gb.iter().copied().collect();
394 frame
400 .solve_gradient(&g_t, &g_beta)
401 .map(|solution| self.finish_step(solution, ExecutionPath::GpuResidentLinearization))
402 .map_err(map_gpu_error)
403 }
404
405 pub fn cpu_reference_step(
408 &self,
409 ridge_t: f64,
410 ridge_beta: f64,
411 ) -> Result<DeviceResidentArrowStep, DeviceResidentArrowError> {
412 let sys = self.to_arrow_system();
413 solve_arrow_newton_step_dense_reference(&sys, ridge_t, ridge_beta)
414 .map(|solution| self.finish_step(solution, ExecutionPath::Cpu))
415 .map_err(|reason| DeviceResidentArrowError::Solve { reason })
416 }
417
418 pub fn inner_iteration_for_production(
443 &self,
444 mode: gam_gpu::GpuPolicy,
445 ridge_t: f64,
446 ridge_beta: f64,
447 ) -> Result<DeviceResidentArrowStep, DeviceResidentArrowError> {
448 match mode {
449 gam_gpu::GpuPolicy::Off => {
450 note_resident_engagement(false, "GpuPolicy::Off — CPU reference step");
451 self.cpu_reference_step(ridge_t, ridge_beta)
452 }
453 gam_gpu::GpuPolicy::Required => {
454 if !self.device_resident() {
455 return Err(DeviceResidentArrowError::Unavailable {
456 reason: format!(
457 "SAE resident inner step GpuPolicy::Required: workspace is not \
458 device-resident (the CUDA runtime did not admit shape n={} p={} d={} \
459 at break-even); refusing to run on the CPU",
460 self.shape.n, self.shape.p, self.shape.d
461 ),
462 });
463 }
464 note_resident_engagement(true, "GpuPolicy::Required — resident device step");
465 self.one_inner_iteration(ridge_t, ridge_beta)
466 }
467 gam_gpu::GpuPolicy::Auto => {
468 if !self.device_resident() {
469 note_resident_engagement(
470 false,
471 "GpuPolicy::Auto — workspace not device-resident; CPU reference step",
472 );
473 return self.cpu_reference_step(ridge_t, ridge_beta);
474 }
475 match self.one_inner_iteration(ridge_t, ridge_beta) {
476 Ok(step) => {
477 note_resident_engagement(true, "GpuPolicy::Auto — resident device step");
478 Ok(step)
479 }
480 Err(err) => {
481 note_resident_engagement(
482 false,
483 &format!(
484 "GpuPolicy::Auto — device solve fault, CPU reference fallback: {err}"
485 ),
486 );
487 self.cpu_reference_step(ridge_t, ridge_beta)
488 }
489 }
490 }
491 }
492 }
493
494 pub fn to_arrow_system(&self) -> ArrowSchurSystem {
495 let shape = self.shape;
496 let mut sys = ArrowSchurSystem::new(shape.n, shape.d, shape.p);
497 for i in 0..shape.n {
498 let h_base = i * shape.d * shape.d;
499 let b_base = i * shape.d * shape.p;
500 let g_base = i * shape.d;
501 for r in 0..shape.d {
502 for c in 0..shape.d {
503 sys.rows[i].htt[[r, c]] =
504 self.slabs.row_hessian_slabs[h_base + r * shape.d + c];
505 }
506 sys.rows[i].gt[r] = self.slabs.row_gradient_slabs[g_base + r];
507 for c in 0..shape.p {
508 sys.rows[i].htbeta[[r, c]] =
509 self.slabs.row_cross_slabs[b_base + r * shape.p + c];
510 }
511 }
512 }
513 for r in 0..shape.p {
514 sys.gb[r] = self.slabs.border_gradient[r];
515 for c in 0..shape.p {
516 sys.hbb[[r, c]] = self.slabs.border_hessian[r * shape.p + c];
517 }
518 }
519 sys.refresh_row_hessian_fingerprint();
520 sys
521 }
522
523 fn finish_step(
524 &self,
525 solution: crate::gpu_kernels::arrow_schur::ArrowSchurGpuSolution,
526 execution_path: ExecutionPath,
527 ) -> DeviceResidentArrowStep {
528 DeviceResidentArrowStep {
529 delta_t: solution.delta_t,
530 delta_beta: solution.delta_beta,
531 objective: 0.5 * squared_norm(&self.target_x),
532 gradient_norm: self.gradient_norm(),
533 log_det_hessian: solution.log_det_hessian,
534 execution_path,
535 }
536 }
537
538 fn gradient_norm(&self) -> f64 {
539 let row = squared_norm(&self.slabs.row_gradient_slabs);
540 let border = squared_norm(&self.slabs.border_gradient);
541 (row + border).sqrt()
542 }
543
544 pub fn device_fit(
573 &self,
574 opts: &DeviceResidentInnerOptions,
575 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
576 if !self.device_resident() {
577 return Err(DeviceResidentArrowError::Unavailable {
578 reason: "SAE resident inner loop unavailable: CUDA runtime did not admit the qwen-scale row-block workload".to_string(),
579 });
580 }
581 self.run_inner_loop(opts, InnerSolveMode::DeviceResident)
582 }
583
584 pub fn device_reupload_fit(
592 &self,
593 opts: &DeviceResidentInnerOptions,
594 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
595 if !self.device_resident() {
596 return Err(DeviceResidentArrowError::Unavailable {
597 reason: "SAE re-uploading inner loop unavailable: CUDA runtime did not admit the row-block workload".to_string(),
598 });
599 }
600 self.run_inner_loop(opts, InnerSolveMode::DeviceReupload)
601 }
602
603 pub fn cpu_reference_fit(
613 &self,
614 opts: &DeviceResidentInnerOptions,
615 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
616 self.run_inner_loop(opts, InnerSolveMode::CpuReference)
617 }
618
619 pub fn cpu_arrow_fit(
630 &self,
631 opts: &DeviceResidentInnerOptions,
632 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
633 self.run_inner_loop(opts, InnerSolveMode::CpuArrow)
634 }
635
636 fn run_inner_loop(
637 &self,
638 opts: &DeviceResidentInnerOptions,
639 mode: InnerSolveMode,
640 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
641 let execution_path = mode.execution_path();
642 let n = self.shape.n;
643 let d = self.shape.d;
644 let p = self.shape.p;
645 let t_len = n * d;
646
647 let mut t = vec![0.0_f64; t_len];
651 let mut beta = vec![0.0_f64; p];
652
653 let base = self.to_arrow_system();
654 let half_target_energy = 0.5 * squared_norm(&self.target_x);
655 let mut residual = self.to_arrow_system();
658 let mut accounting = ResidencyAccounting::default();
659
660 let mut ridge_t = opts.initial_ridge_t.max(0.0);
661 let mut ridge_beta = opts.initial_ridge_beta.max(0.0);
662 let mut resident_frame: Option<(
671 f64,
672 f64,
673 crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle,
674 )> = None;
675 let mut current_apply = accounting.apply(self, mode, &t, &beta);
676 let mut current_objective =
677 self.objective_from_apply(&base, half_target_energy, ¤t_apply, &t, &beta);
678 let mut accepted_iters = 0_usize;
679 let mut total_iters = 0_usize;
680 let mut converged = false;
681 let mut last_step = DeviceResidentArrowStep {
682 delta_t: Array1::zeros(t_len),
683 delta_beta: Array1::zeros(p),
684 objective: current_objective,
685 gradient_norm: 0.0,
686 log_det_hessian: 0.0,
687 execution_path,
688 };
689
690 while total_iters < opts.max_iterations {
691 self.residual_into(&mut residual, &base, ¤t_apply, &t);
694 let g_norm = arrow_system_gradient_norm(&residual);
695 let scale = 1.0 + iterate_norm(&t, &beta);
696 if g_norm / scale < opts.convergence_tolerance {
697 converged = true;
698 break;
699 }
700
701 let solve_start = std::time::Instant::now();
702 let solution = match mode {
703 InnerSolveMode::DeviceResident => {
704 let frame_matches = resident_frame
709 .as_ref()
710 .is_some_and(|(rt, rb, _)| *rt == ridge_t && *rb == ridge_beta);
711 let mut frame_build_error: Option<DeviceResidentArrowError> = None;
712 if !frame_matches {
713 resident_frame = None;
714 match crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
715 &residual, ridge_t, ridge_beta,
716 ) {
717 Ok(frame) => {
718 gam_gpu::profile::telemetry_record_handle_creation(
724 self.context_id(),
725 );
726 gam_gpu::profile::telemetry_record_factorization();
727 gam_gpu::profile::telemetry_record_h2d(self.frame_upload_bytes());
728 resident_frame = Some((ridge_t, ridge_beta, frame));
729 }
730 Err(err) => frame_build_error = Some(map_gpu_error(err)),
731 }
732 }
733 match resident_frame.as_ref() {
734 Some((_, _, frame)) => {
735 let mut g_t = Vec::with_capacity(n * d);
738 for row in &residual.rows {
739 for &v in row.gt.iter() {
740 g_t.push(v);
741 }
742 }
743 let g_beta: Vec<f64> = residual.gb.iter().copied().collect();
744 let grad_bytes =
748 (g_t.len() + g_beta.len()) * std::mem::size_of::<f64>();
749 gam_gpu::profile::telemetry_record_h2d(grad_bytes);
750 gam_gpu::profile::telemetry_record_kernel_launch();
751 gam_gpu::profile::telemetry_record_d2h(
752 (n * d + p) * std::mem::size_of::<f64>(),
753 );
754 frame.solve_gradient(&g_t, &g_beta).map_err(map_gpu_error)
755 }
756 None => Err(frame_build_error.unwrap_or_else(|| {
757 DeviceResidentArrowError::Solve {
758 reason: "SAE resident frame build declined".to_string(),
759 }
760 })),
761 }
762 }
763 InnerSolveMode::DeviceReupload => {
764 gam_gpu::profile::telemetry_record_handle_creation(self.context_id());
770 gam_gpu::profile::telemetry_record_factorization();
771 gam_gpu::profile::telemetry_record_h2d(self.frame_upload_bytes());
772 gam_gpu::profile::telemetry_record_kernel_launch();
773 gam_gpu::profile::telemetry_record_d2h(
774 (n * d + p) * std::mem::size_of::<f64>(),
775 );
776 solve_arrow_newton_step(&residual, ridge_t, ridge_beta).map_err(map_gpu_error)
777 }
778 InnerSolveMode::CpuReference => {
779 solve_arrow_newton_step_dense_reference(&residual, ridge_t, ridge_beta)
780 .map_err(|reason| DeviceResidentArrowError::Solve { reason })
781 }
782 InnerSolveMode::CpuArrow => {
783 cpu_arrow_newton_step(&residual, ridge_t, ridge_beta)
784 }
785 };
786
787 accounting.solve_seconds += solve_start.elapsed().as_secs_f64();
788 let solution = match solution {
789 Ok(sol) => sol,
790 Err(DeviceResidentArrowError::Solve { .. })
791 | Err(DeviceResidentArrowError::Unavailable { .. }) => {
792 ridge_t = grow_ridge(ridge_t, opts.lm_grow);
796 ridge_beta = grow_ridge(ridge_beta, opts.lm_grow);
797 if ridge_t > opts.max_ridge || ridge_beta > opts.max_ridge {
798 return Err(DeviceResidentArrowError::Solve {
799 reason: format!(
800 "SAE resident inner loop: LM ridge exceeded max ({:e}) at iter {total_iters}",
801 opts.max_ridge
802 ),
803 });
804 }
805 total_iters += 1;
806 continue;
807 }
808 Err(other) => return Err(other),
809 };
810
811 let predicted_reduction = crate::arrow_schur::arrow_bare_quadratic_model_reduction(
814 &residual,
815 solution.delta_t.view(),
816 solution.delta_beta.view(),
817 ridge_t,
818 ridge_beta,
819 )
820 .map_err(|err| DeviceResidentArrowError::Solve {
821 reason: format!("SAE resident inner loop predicted-reduction failed: {err}"),
822 })?;
823
824 let mut trial_t = t.clone();
826 let mut trial_beta = beta.clone();
827 for (slot, dv) in trial_t.iter_mut().zip(solution.delta_t.iter()) {
828 *slot += *dv;
829 }
830 for (slot, dv) in trial_beta.iter_mut().zip(solution.delta_beta.iter()) {
831 *slot += *dv;
832 }
833 let trial_apply = accounting.apply(self, mode, &trial_t, &trial_beta);
834 let trial_objective = self.objective_from_apply(
835 &base,
836 half_target_energy,
837 &trial_apply,
838 &trial_t,
839 &trial_beta,
840 );
841
842 let objective_scale = current_objective.abs();
854 let noise_floor = objective_scale * 1e-14;
855 let actual_reduction = current_objective - trial_objective;
856 let rho = if predicted_reduction > noise_floor {
857 actual_reduction / predicted_reduction
858 } else if actual_reduction >= -noise_floor {
859 1.0
860 } else {
861 -1.0
862 };
863
864 if rho > 0.0 && trial_objective.is_finite() {
865 t = trial_t;
866 beta = trial_beta;
867 current_apply = trial_apply;
868 current_objective = trial_objective;
869 ridge_t = (ridge_t * opts.lm_shrink).max(0.0);
870 ridge_beta = (ridge_beta * opts.lm_shrink).max(0.0);
871 last_step = DeviceResidentArrowStep {
872 delta_t: solution.delta_t,
873 delta_beta: solution.delta_beta,
874 objective: current_objective,
875 gradient_norm: g_norm,
876 log_det_hessian: solution.log_det_hessian,
877 execution_path,
878 };
879 accepted_iters += 1;
880 total_iters += 1;
881 } else {
882 ridge_t = grow_ridge(ridge_t, opts.lm_grow);
883 ridge_beta = grow_ridge(ridge_beta, opts.lm_grow);
884 if ridge_t > opts.max_ridge || ridge_beta > opts.max_ridge {
885 return Err(DeviceResidentArrowError::Solve {
886 reason: format!(
887 "SAE resident inner loop: LM rejected step until ridge exceeded max ({:e}) at iter {total_iters} (rho={rho:.3e})",
888 opts.max_ridge
889 ),
890 });
891 }
892 total_iters += 1;
893 }
894 }
895
896 Ok(DeviceResidentInnerOutcome {
897 t: Array1::from_vec(t),
898 beta: Array1::from_vec(beta),
899 objective: current_objective,
900 gradient_norm: last_step.gradient_norm,
901 log_det_hessian: last_step.log_det_hessian,
902 iterations: total_iters,
903 accepted_iterations: accepted_iters,
904 converged,
905 execution_path,
906 residency: accounting.finish(),
907 })
908 }
909
910 pub fn device_fit_outer_sequence(
946 &self,
947 base_gradient_overrides: &[(Vec<f64>, Vec<f64>)],
948 opts: &DeviceResidentInnerOptions,
949 ) -> Result<OuterSequenceOutcome, DeviceResidentArrowError> {
950 if !self.device_resident() {
951 return Err(DeviceResidentArrowError::Unavailable {
952 reason: "SAE outer-sequence residency unavailable: CUDA runtime did not admit the row-block workload".to_string(),
953 });
954 }
955 self.run_outer_sequence(
956 base_gradient_overrides,
957 opts,
958 InnerSolveMode::DeviceResident,
959 )
960 }
961
962 pub fn cpu_reference_outer_sequence(
967 &self,
968 base_gradient_overrides: &[(Vec<f64>, Vec<f64>)],
969 opts: &DeviceResidentInnerOptions,
970 ) -> Result<OuterSequenceOutcome, DeviceResidentArrowError> {
971 self.run_outer_sequence(base_gradient_overrides, opts, InnerSolveMode::CpuReference)
972 }
973
974 fn run_outer_sequence(
975 &self,
976 base_gradient_overrides: &[(Vec<f64>, Vec<f64>)],
977 opts: &DeviceResidentInnerOptions,
978 mode: InnerSolveMode,
979 ) -> Result<OuterSequenceOutcome, DeviceResidentArrowError> {
980 let n = self.shape.n;
981 let d = self.shape.d;
982 let p = self.shape.p;
983 let t_len = n * d;
984 let half_target_energy = 0.5 * squared_norm(&self.target_x);
985
986 let mut shared = SharedFrameState::default();
993 let mut outcomes = Vec::with_capacity(base_gradient_overrides.len());
994
995 for (g_t_override, g_beta_override) in base_gradient_overrides {
996 if g_t_override.len() != t_len || g_beta_override.len() != p {
997 return Err(DeviceResidentArrowError::Shape {
998 reason: format!(
999 "outer-sequence gradient shape mismatch: g_t={} (want {t_len}), g_beta={} (want {p})",
1000 g_t_override.len(),
1001 g_beta_override.len()
1002 ),
1003 });
1004 }
1005 let mut base = self.to_arrow_system();
1008 for (i, row) in base.rows.iter_mut().enumerate() {
1009 for r in 0..d {
1010 row.gt[r] = g_t_override[i * d + r];
1011 }
1012 }
1013 for (j, gb) in base.gb.iter_mut().enumerate() {
1014 *gb = g_beta_override[j];
1015 }
1016 base.refresh_row_hessian_fingerprint();
1017
1018 let outcome = self.run_one_outer(&base, half_target_energy, opts, mode, &mut shared)?;
1019 outcomes.push(outcome);
1020 }
1021
1022 Ok(OuterSequenceOutcome {
1023 outers: outcomes,
1024 frame_builds: shared.frame_builds,
1025 })
1026 }
1027
1028 fn run_one_outer(
1035 &self,
1036 base: &ArrowSchurSystem,
1037 half_target_energy: f64,
1038 opts: &DeviceResidentInnerOptions,
1039 mode: InnerSolveMode,
1040 shared: &mut SharedFrameState,
1041 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError> {
1042 let execution_path = mode.execution_path();
1043 let n = self.shape.n;
1044 let d = self.shape.d;
1045 let p = self.shape.p;
1046 let t_len = n * d;
1047
1048 let mut t = vec![0.0_f64; t_len];
1049 let mut beta = vec![0.0_f64; p];
1050 let mut ridge_t = opts.initial_ridge_t.max(0.0);
1051 let mut ridge_beta = opts.initial_ridge_beta.max(0.0);
1052 let mut residual = self.to_arrow_system();
1053 let mut accounting = ResidencyAccounting::default();
1054 let mut current_apply = accounting.apply(self, mode, &t, &beta);
1055 let mut current_objective =
1056 self.objective_from_apply(base, half_target_energy, ¤t_apply, &t, &beta);
1057 let mut accepted_iters = 0_usize;
1058 let mut total_iters = 0_usize;
1059 let mut converged = false;
1060 let mut last_gradient_norm = 0.0_f64;
1061 let mut last_log_det = 0.0_f64;
1062
1063 while total_iters < opts.max_iterations {
1064 self.residual_into(&mut residual, base, ¤t_apply, &t);
1065 let g_norm = arrow_system_gradient_norm(&residual);
1066 let scale = 1.0 + iterate_norm(&t, &beta);
1067 if g_norm / scale < opts.convergence_tolerance {
1068 converged = true;
1069 break;
1070 }
1071
1072 let solve_start = std::time::Instant::now();
1073 let solution = match mode {
1074 InnerSolveMode::DeviceResident => {
1075 let frame_matches = shared
1076 .frame
1077 .as_ref()
1078 .is_some_and(|(rt, rb, _)| *rt == ridge_t && *rb == ridge_beta);
1079 let mut frame_build_error: Option<DeviceResidentArrowError> = None;
1080 if !frame_matches {
1081 shared.frame = None;
1082 match crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
1083 &residual, ridge_t, ridge_beta,
1084 ) {
1085 Ok(frame) => {
1086 shared.frame_builds += 1;
1087 gam_gpu::profile::telemetry_record_handle_creation(
1088 self.context_id(),
1089 );
1090 gam_gpu::profile::telemetry_record_factorization();
1091 gam_gpu::profile::telemetry_record_h2d(self.frame_upload_bytes());
1092 shared.frame = Some((ridge_t, ridge_beta, frame));
1093 }
1094 Err(err) => frame_build_error = Some(map_gpu_error(err)),
1095 }
1096 }
1097 match shared.frame.as_ref() {
1098 Some((_, _, frame)) => {
1099 let mut g_t = Vec::with_capacity(n * d);
1100 for row in &residual.rows {
1101 for &v in row.gt.iter() {
1102 g_t.push(v);
1103 }
1104 }
1105 let g_beta: Vec<f64> = residual.gb.iter().copied().collect();
1106 let grad_bytes =
1107 (g_t.len() + g_beta.len()) * std::mem::size_of::<f64>();
1108 gam_gpu::profile::telemetry_record_h2d(grad_bytes);
1109 gam_gpu::profile::telemetry_record_kernel_launch();
1110 gam_gpu::profile::telemetry_record_d2h(
1111 (n * d + p) * std::mem::size_of::<f64>(),
1112 );
1113 frame.solve_gradient(&g_t, &g_beta).map_err(map_gpu_error)
1114 }
1115 None => Err(frame_build_error.unwrap_or_else(|| {
1116 DeviceResidentArrowError::Solve {
1117 reason: "SAE resident frame build declined".to_string(),
1118 }
1119 })),
1120 }
1121 }
1122 InnerSolveMode::DeviceReupload => {
1123 solve_arrow_newton_step(&residual, ridge_t, ridge_beta).map_err(map_gpu_error)
1124 }
1125 InnerSolveMode::CpuReference => {
1126 solve_arrow_newton_step_dense_reference(&residual, ridge_t, ridge_beta)
1127 .map_err(|reason| DeviceResidentArrowError::Solve { reason })
1128 }
1129 InnerSolveMode::CpuArrow => {
1130 cpu_arrow_newton_step(&residual, ridge_t, ridge_beta)
1131 }
1132 };
1133
1134 accounting.solve_seconds += solve_start.elapsed().as_secs_f64();
1135 let solution = match solution {
1136 Ok(sol) => sol,
1137 Err(DeviceResidentArrowError::Solve { .. })
1138 | Err(DeviceResidentArrowError::Unavailable { .. }) => {
1139 ridge_t = grow_ridge(ridge_t, opts.lm_grow);
1140 ridge_beta = grow_ridge(ridge_beta, opts.lm_grow);
1141 if ridge_t > opts.max_ridge || ridge_beta > opts.max_ridge {
1142 return Err(DeviceResidentArrowError::Solve {
1143 reason: format!(
1144 "SAE outer-sequence inner loop: LM ridge exceeded max ({:e}) at iter {total_iters}",
1145 opts.max_ridge
1146 ),
1147 });
1148 }
1149 total_iters += 1;
1150 continue;
1151 }
1152 Err(other) => return Err(other),
1153 };
1154
1155 let predicted_reduction = crate::arrow_schur::arrow_bare_quadratic_model_reduction(
1156 &residual,
1157 solution.delta_t.view(),
1158 solution.delta_beta.view(),
1159 ridge_t,
1160 ridge_beta,
1161 )
1162 .map_err(|err| DeviceResidentArrowError::Solve {
1163 reason: format!("SAE outer-sequence predicted-reduction failed: {err}"),
1164 })?;
1165
1166 let mut trial_t = t.clone();
1167 let mut trial_beta = beta.clone();
1168 for (slot, dv) in trial_t.iter_mut().zip(solution.delta_t.iter()) {
1169 *slot += *dv;
1170 }
1171 for (slot, dv) in trial_beta.iter_mut().zip(solution.delta_beta.iter()) {
1172 *slot += *dv;
1173 }
1174 let trial_apply = accounting.apply(self, mode, &trial_t, &trial_beta);
1175 let trial_objective = self.objective_from_apply(
1176 base,
1177 half_target_energy,
1178 &trial_apply,
1179 &trial_t,
1180 &trial_beta,
1181 );
1182
1183 let objective_scale = current_objective.abs();
1184 let noise_floor = objective_scale * 1e-14;
1185 let actual_reduction = current_objective - trial_objective;
1186 let rho = if predicted_reduction > noise_floor {
1187 actual_reduction / predicted_reduction
1188 } else if actual_reduction >= -noise_floor {
1189 1.0
1190 } else {
1191 -1.0
1192 };
1193
1194 if rho > 0.0 && trial_objective.is_finite() {
1195 t = trial_t;
1196 beta = trial_beta;
1197 current_apply = trial_apply;
1198 current_objective = trial_objective;
1199 ridge_t = (ridge_t * opts.lm_shrink).max(0.0);
1200 ridge_beta = (ridge_beta * opts.lm_shrink).max(0.0);
1201 last_gradient_norm = g_norm;
1202 last_log_det = solution.log_det_hessian;
1203 accepted_iters += 1;
1204 total_iters += 1;
1205 } else {
1206 ridge_t = grow_ridge(ridge_t, opts.lm_grow);
1207 ridge_beta = grow_ridge(ridge_beta, opts.lm_grow);
1208 if ridge_t > opts.max_ridge || ridge_beta > opts.max_ridge {
1209 return Err(DeviceResidentArrowError::Solve {
1210 reason: format!(
1211 "SAE outer-sequence inner loop: LM rejected step until ridge exceeded max ({:e}) at iter {total_iters} (rho={rho:.3e})",
1212 opts.max_ridge
1213 ),
1214 });
1215 }
1216 total_iters += 1;
1217 }
1218 }
1219
1220 Ok(DeviceResidentInnerOutcome {
1221 t: Array1::from_vec(t),
1222 beta: Array1::from_vec(beta),
1223 objective: current_objective,
1224 gradient_norm: last_gradient_norm,
1225 log_det_hessian: last_log_det,
1226 iterations: total_iters,
1227 accepted_iterations: accepted_iters,
1228 converged,
1229 execution_path,
1230 residency: accounting.finish(),
1231 })
1232 }
1233
1234 fn apply_operator(
1248 &self,
1249 on_device: bool,
1250 t: &[f64],
1251 beta: &[f64],
1252 ) -> (ArrowOperatorApply, OperatorApplyCost) {
1253 #[cfg(target_os = "linux")]
1254 {
1255 if on_device && self.device.is_some() {
1256 match self.apply_operator_device(t, beta) {
1257 Some(apply) => {
1258 let moved = (t.len() + beta.len()) * std::mem::size_of::<f64>();
1259 let returned = (t.len() + 2 * beta.len()) * std::mem::size_of::<f64>();
1260 gam_gpu::profile::telemetry_record_h2d(moved);
1261 gam_gpu::profile::telemetry_record_kernel_launch();
1262 gam_gpu::profile::telemetry_record_d2h(returned);
1263 return (
1264 apply,
1265 OperatorApplyCost {
1266 on_device: true,
1267 host_to_device_bytes: moved,
1268 device_to_host_bytes: returned,
1269 },
1270 );
1271 }
1272 None => note_resident_engagement(
1273 false,
1274 "resident operator apply faulted on device; host contraction fallback",
1275 ),
1276 }
1277 }
1278 }
1279 if on_device {
1288 note_resident_engagement(
1289 false,
1290 "resident operator apply requested the device arm but the workspace is not \
1291 device-resident; host contraction",
1292 );
1293 }
1294 (
1295 self.apply_operator_host(t, beta),
1296 OperatorApplyCost::default(),
1297 )
1298 }
1299
1300 fn apply_operator_host(&self, t: &[f64], beta: &[f64]) -> ArrowOperatorApply {
1314 let n = self.shape.n;
1315 let d = self.shape.d;
1316 let p = self.shape.p;
1317 let cross = self.slabs.row_cross_slabs.as_slice();
1318 let border = self.slabs.border_hessian.as_slice();
1319
1320 let chunk_rows = n.div_ceil(OPERATOR_MAX_ROW_CHUNKS).max(OPERATOR_MIN_ROW_CHUNK);
1323 let parallel = n >= OPERATOR_PARALLEL_ROW_MIN && rayon::current_thread_index().is_none();
1324
1325 let mut cross_t = vec![0.0_f64; n * d];
1326 let mut cross_beta = vec![0.0_f64; p];
1327 let row_chunk = |rows: std::ops::Range<usize>,
1328 cross_t_chunk: &mut [f64],
1329 cross_beta_partial: &mut [f64]| {
1330 for (local, i) in rows.enumerate() {
1331 for r in 0..d {
1332 let base = (i * d + r) * p;
1333 let slab = &cross[base..base + p];
1334 let mut acc = 0.0_f64;
1335 for (j, &value) in slab.iter().enumerate() {
1336 acc += value * beta[j];
1337 }
1338 cross_t_chunk[local * d + r] = acc;
1339 let weight = t[i * d + r];
1340 for (j, &value) in slab.iter().enumerate() {
1341 cross_beta_partial[j] += value * weight;
1342 }
1343 }
1344 }
1345 };
1346
1347 if parallel {
1348 use rayon::prelude::*;
1349 let partials: Vec<Vec<f64>> = cross_t
1350 .par_chunks_mut(chunk_rows * d)
1351 .enumerate()
1352 .map(|(chunk_idx, cross_t_chunk)| {
1353 let start = chunk_idx * chunk_rows;
1354 let end = (start + chunk_rows).min(n);
1355 let mut partial = vec![0.0_f64; p];
1356 row_chunk(start..end, cross_t_chunk, &mut partial);
1357 partial
1358 })
1359 .collect();
1360 for partial in &partials {
1363 for (slot, &value) in cross_beta.iter_mut().zip(partial.iter()) {
1364 *slot += value;
1365 }
1366 }
1367 } else {
1368 let mut chunk_start = 0_usize;
1369 while chunk_start < n {
1370 let chunk_end = (chunk_start + chunk_rows).min(n);
1371 let mut partial = vec![0.0_f64; p];
1372 let slice = &mut cross_t[chunk_start * d..chunk_end * d];
1373 row_chunk(chunk_start..chunk_end, slice, &mut partial);
1374 for (slot, value) in cross_beta.iter_mut().zip(partial) {
1375 *slot += value;
1376 }
1377 chunk_start = chunk_end;
1378 }
1379 }
1380
1381 let border_row = |r: usize| -> f64 {
1382 let row = &border[r * p..(r + 1) * p];
1383 let mut acc = 0.0_f64;
1384 for (c, &value) in row.iter().enumerate() {
1385 acc += value * beta[c];
1386 }
1387 acc
1388 };
1389 let border_beta: Vec<f64> = if parallel {
1390 use rayon::prelude::*;
1391 (0..p).into_par_iter().map(border_row).collect()
1392 } else {
1393 (0..p).map(border_row).collect()
1394 };
1395
1396 ArrowOperatorApply {
1397 cross_t,
1398 cross_beta,
1399 border_beta,
1400 }
1401 }
1402
1403 #[cfg(target_os = "linux")]
1408 fn apply_operator_device(&self, t: &[f64], beta: &[f64]) -> Option<ArrowOperatorApply> {
1409 use cudarc::cublas::sys::cublasOperation_t;
1410 use cudarc::cublas::{Gemv, GemvConfig};
1411
1412 let device = self.device.as_ref()?;
1413 let rows = self.shape.n.checked_mul(self.shape.d)?;
1414 let p = self.shape.p;
1415 let rows_i = i32::try_from(rows).ok()?;
1416 let p_i = i32::try_from(p).ok()?;
1417 let mut guard = device.operator_scratch.lock().ok()?;
1418 let scratch = &mut *guard;
1419 device.stream.memcpy_htod(t, &mut scratch.t_dev).ok()?;
1420 device
1421 .stream
1422 .memcpy_htod(beta, &mut scratch.beta_dev)
1423 .ok()?;
1424
1425 let cross_t_cfg = GemvConfig::<f64> {
1430 trans: cublasOperation_t::CUBLAS_OP_T,
1431 m: p_i,
1432 n: rows_i,
1433 alpha: 1.0,
1434 lda: p_i,
1435 incx: 1,
1436 beta: 0.0,
1437 incy: 1,
1438 };
1439 unsafe {
1444 device.blas.gemv(
1445 cross_t_cfg,
1446 &device.row_cross_dev,
1447 &scratch.beta_dev,
1448 &mut scratch.cross_t_dev,
1449 )
1450 }
1451 .ok()?;
1452
1453 let cross_beta_cfg = GemvConfig::<f64> {
1454 trans: cublasOperation_t::CUBLAS_OP_N,
1455 m: p_i,
1456 n: rows_i,
1457 alpha: 1.0,
1458 lda: p_i,
1459 incx: 1,
1460 beta: 0.0,
1461 incy: 1,
1462 };
1463 unsafe {
1466 device.blas.gemv(
1467 cross_beta_cfg,
1468 &device.row_cross_dev,
1469 &scratch.t_dev,
1470 &mut scratch.cross_beta_dev,
1471 )
1472 }
1473 .ok()?;
1474
1475 let border_cfg = GemvConfig::<f64> {
1478 trans: cublasOperation_t::CUBLAS_OP_T,
1479 m: p_i,
1480 n: p_i,
1481 alpha: 1.0,
1482 lda: p_i,
1483 incx: 1,
1484 beta: 0.0,
1485 incy: 1,
1486 };
1487 unsafe {
1490 device.blas.gemv(
1491 border_cfg,
1492 &device.border_hessian_dev,
1493 &scratch.beta_dev,
1494 &mut scratch.border_beta_dev,
1495 )
1496 }
1497 .ok()?;
1498
1499 let cross_t = device.stream.clone_dtoh(&scratch.cross_t_dev).ok()?;
1500 let cross_beta = device.stream.clone_dtoh(&scratch.cross_beta_dev).ok()?;
1501 let border_beta = device.stream.clone_dtoh(&scratch.border_beta_dev).ok()?;
1502 Some(ArrowOperatorApply {
1503 cross_t,
1504 cross_beta,
1505 border_beta,
1506 })
1507 }
1508
1509 fn objective_from_apply(
1514 &self,
1515 base: &ArrowSchurSystem,
1516 half_target_energy: f64,
1517 apply: &ArrowOperatorApply,
1518 t: &[f64],
1519 beta: &[f64],
1520 ) -> f64 {
1521 let n = self.shape.n;
1522 let d = self.shape.d;
1523 let p = self.shape.p;
1524 let mut quad = 0.0_f64;
1526 let mut lin = 0.0_f64;
1527 for i in 0..n {
1528 let t_base = i * d;
1529 for r in 0..d {
1530 let mut htt_t = 0.0_f64;
1531 for c in 0..d {
1532 htt_t += base.rows[i].htt[[r, c]] * t[t_base + c];
1533 }
1534 quad += t[t_base + r] * (htt_t + 2.0 * apply.cross_t[t_base + r]);
1535 lin += base.rows[i].gt[r] * t[t_base + r];
1536 }
1537 }
1538 for r in 0..p {
1539 quad += beta[r] * apply.border_beta[r];
1540 lin += base.gb[r] * beta[r];
1541 }
1542 half_target_energy + 0.5 * quad - lin
1543 }
1544
1545 fn residual_into(
1557 &self,
1558 sys: &mut ArrowSchurSystem,
1559 base: &ArrowSchurSystem,
1560 apply: &ArrowOperatorApply,
1561 t: &[f64],
1562 ) {
1563 let n = self.shape.n;
1564 let d = self.shape.d;
1565 let p = self.shape.p;
1566 for i in 0..n {
1567 let t_base = i * d;
1568 for r in 0..d {
1569 let mut hz = 0.0_f64;
1570 for c in 0..d {
1571 hz += base.rows[i].htt[[r, c]] * t[t_base + c];
1572 }
1573 hz += apply.cross_t[t_base + r];
1574 sys.rows[i].gt[r] = hz - base.rows[i].gt[r];
1575 }
1576 }
1577 for r in 0..p {
1578 sys.gb[r] = apply.border_beta[r] + apply.cross_beta[r] - base.gb[r];
1579 }
1580 }
1581}
1582
1583const OPERATOR_MAX_ROW_CHUNKS: usize = 256;
1588
1589const OPERATOR_MIN_ROW_CHUNK: usize = 64;
1592
1593const OPERATOR_PARALLEL_ROW_MIN: usize = 256;
1596
1597#[derive(Clone, Copy, Debug, Default)]
1599struct OperatorApplyCost {
1600 on_device: bool,
1601 host_to_device_bytes: usize,
1602 device_to_host_bytes: usize,
1603}
1604
1605#[derive(Default)]
1614struct ResidencyAccounting {
1615 operator_applies: usize,
1616 operator_device_applies: usize,
1617 operator_seconds: f64,
1618 solve_seconds: f64,
1619 host_to_device_bytes: usize,
1620 device_to_host_bytes: usize,
1621}
1622
1623impl ResidencyAccounting {
1624 fn apply(
1625 &mut self,
1626 workspace: &DeviceResidentArrowWorkspace,
1627 mode: InnerSolveMode,
1628 t: &[f64],
1629 beta: &[f64],
1630 ) -> ArrowOperatorApply {
1631 let start = std::time::Instant::now();
1632 let (apply, cost) = workspace.apply_operator(mode.operator_on_device(), t, beta);
1633 self.operator_seconds += start.elapsed().as_secs_f64();
1634 self.operator_applies += 1;
1635 if cost.on_device {
1636 self.operator_device_applies += 1;
1637 }
1638 self.host_to_device_bytes += cost.host_to_device_bytes;
1639 self.device_to_host_bytes += cost.device_to_host_bytes;
1640 apply
1641 }
1642
1643 fn finish(self) -> ResidencyReport {
1644 ResidencyReport {
1645 operator_applies: self.operator_applies,
1646 operator_device_applies: self.operator_device_applies,
1647 operator_seconds: self.operator_seconds,
1648 solve_seconds: self.solve_seconds,
1649 operator_host_to_device_bytes: self.host_to_device_bytes,
1650 operator_device_to_host_bytes: self.device_to_host_bytes,
1651 }
1652 }
1653}
1654
1655#[derive(Clone, Copy, Debug, Default, PartialEq)]
1662pub struct ResidencyReport {
1663 pub operator_applies: usize,
1665 pub operator_device_applies: usize,
1667 pub operator_seconds: f64,
1669 pub solve_seconds: f64,
1671 pub operator_host_to_device_bytes: usize,
1673 pub operator_device_to_host_bytes: usize,
1675}
1676
1677impl ResidencyReport {
1678 #[must_use]
1681 pub fn operator_device_fraction(&self) -> f64 {
1682 if self.operator_applies == 0 {
1683 return 0.0;
1684 }
1685 self.operator_device_applies as f64 / self.operator_applies as f64
1686 }
1687
1688 #[must_use]
1692 pub fn transfer_bytes(&self) -> usize {
1693 self.operator_host_to_device_bytes + self.operator_device_to_host_bytes
1694 }
1695}
1696
1697#[derive(Clone, Copy, Debug)]
1701pub struct DeviceResidentInnerOptions {
1702 pub max_iterations: usize,
1703 pub convergence_tolerance: f64,
1704 pub initial_ridge_t: f64,
1705 pub initial_ridge_beta: f64,
1706 pub lm_grow: f64,
1707 pub lm_shrink: f64,
1708 pub max_ridge: f64,
1709}
1710
1711impl Default for DeviceResidentInnerOptions {
1712 fn default() -> Self {
1713 Self {
1714 max_iterations: 16,
1715 convergence_tolerance: 1e-9,
1716 initial_ridge_t: 0.0,
1717 initial_ridge_beta: 0.0,
1718 lm_grow: 4.0,
1719 lm_shrink: 0.5,
1720 max_ridge: 1e9,
1721 }
1722 }
1723}
1724
1725#[derive(Clone, Debug)]
1727pub struct DeviceResidentInnerOutcome {
1728 pub t: Array1<f64>,
1729 pub beta: Array1<f64>,
1730 pub objective: f64,
1731 pub gradient_norm: f64,
1732 pub log_det_hessian: f64,
1733 pub iterations: usize,
1734 pub accepted_iterations: usize,
1735 pub converged: bool,
1736 pub execution_path: ExecutionPath,
1737 pub residency: ResidencyReport,
1739}
1740
1741#[derive(Clone, Debug)]
1752pub struct OuterSequenceOutcome {
1753 pub outers: Vec<DeviceResidentInnerOutcome>,
1754 pub frame_builds: usize,
1755}
1756
1757#[derive(Default)]
1762struct SharedFrameState {
1763 frame: Option<(
1764 f64,
1765 f64,
1766 crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle,
1767 )>,
1768 frame_builds: usize,
1769}
1770
1771fn note_resident_engagement(engaged: bool, detail: &str) {
1778 use std::sync::Once;
1779 static ENGAGED_ONCE: Once = Once::new();
1780 static DECLINED_ONCE: Once = Once::new();
1781 let once = if engaged {
1782 &ENGAGED_ONCE
1783 } else {
1784 &DECLINED_ONCE
1785 };
1786 once.call_once(|| {
1787 let verdict = if engaged {
1788 "device ENGAGED"
1789 } else {
1790 "device DECLINED - CPU reference"
1791 };
1792 log::warn!("[gam-solve sae_resident inner step] {verdict}: {detail}");
1793 });
1794}
1795
1796fn cpu_arrow_newton_step(
1804 sys: &ArrowSchurSystem,
1805 ridge_t: f64,
1806 ridge_beta: f64,
1807) -> Result<crate::gpu_kernels::arrow_schur::ArrowSchurGpuSolution, DeviceResidentArrowError> {
1808 let mut options = crate::arrow_schur::ArrowSolveOptions::direct();
1809 options.gpu_policy = gam_gpu::GpuPolicy::Off;
1810 let (delta_t, delta_beta, _diagnostics) = sys
1811 .solve_with_options(ridge_t, ridge_beta, &options)
1812 .map_err(|err| DeviceResidentArrowError::Solve {
1813 reason: format!("SAE CPU arrow baseline step failed: {err}"),
1814 })?;
1815 Ok(crate::gpu_kernels::arrow_schur::ArrowSchurGpuSolution {
1816 delta_t,
1817 delta_beta,
1818 log_det_hessian: f64::NAN,
1819 })
1820}
1821
1822fn grow_ridge(current: f64, grow: f64) -> f64 {
1823 if current == 0.0 { 1e-6 } else { current * grow }
1824}
1825
1826fn arrow_system_gradient_norm(sys: &ArrowSchurSystem) -> f64 {
1827 let mut acc = 0.0_f64;
1828 for row in &sys.rows {
1829 for &v in row.gt.iter() {
1830 acc += v * v;
1831 }
1832 }
1833 for &v in sys.gb.iter() {
1834 acc += v * v;
1835 }
1836 acc.sqrt()
1837}
1838
1839fn iterate_norm(t: &[f64], beta: &[f64]) -> f64 {
1840 (squared_norm(t) + squared_norm(beta)).sqrt()
1841}
1842
1843fn validate_shape(
1844 shape: DeviceResidentArrowShape,
1845 target_x: &[f64],
1846 basis_values: &[f64],
1847 gate_activations: &[f64],
1848 slabs: &DeviceResidentArrowSlabs,
1849) -> Result<(), DeviceResidentArrowError> {
1850 let checks = [
1851 ("target_x", target_x.len(), shape.target_len()),
1852 ("basis_values", basis_values.len(), shape.basis_len()),
1853 (
1854 "gate_activations",
1855 gate_activations.len(),
1856 shape.basis_len(),
1857 ),
1858 (
1859 "row_hessian_slabs",
1860 slabs.row_hessian_slabs.len(),
1861 shape.row_hessian_len(),
1862 ),
1863 (
1864 "row_cross_slabs",
1865 slabs.row_cross_slabs.len(),
1866 shape.row_cross_len(),
1867 ),
1868 (
1869 "row_gradient_slabs",
1870 slabs.row_gradient_slabs.len(),
1871 shape.row_gradient_len(),
1872 ),
1873 (
1874 "border_hessian",
1875 slabs.border_hessian.len(),
1876 shape.border_hessian_len(),
1877 ),
1878 ("border_gradient", slabs.border_gradient.len(), shape.p),
1879 ];
1880 for (label, got, want) in checks {
1881 if got != want {
1882 return Err(DeviceResidentArrowError::Shape {
1883 reason: format!(
1884 "SAE resident workspace shape mismatch for {label}: got {got}, expected {want}"
1885 ),
1886 });
1887 }
1888 }
1889 if shape.n == 0 || shape.p == 0 || shape.d == 0 || shape.basis_cols == 0 {
1890 return Err(DeviceResidentArrowError::Shape {
1891 reason: "SAE resident workspace requires nonzero n, p, basis_cols, and d".to_string(),
1892 });
1893 }
1894 Ok(())
1895}
1896
1897#[cfg(target_os = "linux")]
1898fn upload_resident_buffers(
1899 shape: DeviceResidentArrowShape,
1900 target_x: &[f64],
1901 basis_values: &[f64],
1902 gate_activations: &[f64],
1903 slabs: &DeviceResidentArrowSlabs,
1904) -> Option<DeviceResidentArrowBuffers> {
1905 use gam_gpu::linalg_dispatch::{DispatchOp, route_through_gpu};
1906
1907 let runtime = route_through_gpu(DispatchOp::SmallDenseBatchedPotrf {
1920 p: shape.d,
1921 batch: shape.n,
1922 })
1923 .or_else(|| {
1924 route_through_gpu(DispatchOp::Gemm {
1925 m: shape.p,
1926 n: shape.p,
1927 k: shape.n * shape.d,
1928 })
1929 })?;
1930 let ctx = gam_gpu::device_runtime::cuda_context_for(runtime.device.ordinal)?;
1931 let stream = ctx.new_stream().ok()?;
1932 let target_x_dev = stream.clone_htod(target_x).ok()?;
1933 let basis_values_dev = stream.clone_htod(basis_values).ok()?;
1934 let gate_activations_dev = stream.clone_htod(gate_activations).ok()?;
1935 let row_hessian_dev = stream.clone_htod(&slabs.row_hessian_slabs).ok()?;
1936 let row_cross_dev = stream.clone_htod(&slabs.row_cross_slabs).ok()?;
1937 let row_gradient_dev = stream.clone_htod(&slabs.row_gradient_slabs).ok()?;
1938 let border_hessian_dev = stream.clone_htod(&slabs.border_hessian).ok()?;
1939 let border_gradient_dev = stream.clone_htod(&slabs.border_gradient).ok()?;
1940 let blas = cudarc::cublas::CudaBlas::new(stream.clone()).ok()?;
1943 let rows_times_d = shape.n.checked_mul(shape.d)?;
1944 let operator_scratch = std::sync::Mutex::new(DeviceOperatorScratch {
1945 t_dev: stream.alloc_zeros::<f64>(rows_times_d).ok()?,
1946 beta_dev: stream.alloc_zeros::<f64>(shape.p).ok()?,
1947 cross_t_dev: stream.alloc_zeros::<f64>(rows_times_d).ok()?,
1948 cross_beta_dev: stream.alloc_zeros::<f64>(shape.p).ok()?,
1949 border_beta_dev: stream.alloc_zeros::<f64>(shape.p).ok()?,
1950 });
1951 let bytes = [
1952 target_x.len(),
1953 basis_values.len(),
1954 gate_activations.len(),
1955 slabs.row_hessian_slabs.len(),
1956 slabs.row_cross_slabs.len(),
1957 slabs.row_gradient_slabs.len(),
1958 slabs.border_hessian.len(),
1959 slabs.border_gradient.len(),
1960 ]
1961 .into_iter()
1962 .sum::<usize>()
1963 * std::mem::size_of::<f64>();
1964 Some(DeviceResidentArrowBuffers {
1965 stream,
1966 target_x_dev,
1967 basis_values_dev,
1968 gate_activations_dev,
1969 row_hessian_dev,
1970 row_cross_dev,
1971 row_gradient_dev,
1972 border_hessian_dev,
1973 border_gradient_dev,
1974 bytes,
1975 blas,
1976 operator_scratch,
1977 })
1978}
1979
1980fn map_gpu_error(err: ArrowSchurGpuFailure) -> DeviceResidentArrowError {
1981 match err {
1982 ArrowSchurGpuFailure::Unavailable => DeviceResidentArrowError::Unavailable {
1983 reason: "SAE resident inner iteration unavailable after GPU admission".to_string(),
1984 },
1985 ArrowSchurGpuFailure::RidgeBumpRequired { row, bump } => DeviceResidentArrowError::Solve {
1986 reason: format!("SAE resident inner iteration row {row} requires ridge bump {bump:e}"),
1987 },
1988 ArrowSchurGpuFailure::SchurFactorFailed { reason } => {
1989 DeviceResidentArrowError::Solve { reason }
1990 }
1991 ArrowSchurGpuFailure::GpuRequiresDenseSystem {
1992 had_hbb_matvec,
1993 had_htbeta_matvec,
1994 } => DeviceResidentArrowError::Solve {
1995 reason: format!(
1996 "SAE resident inner iteration requires dense slabs; hbb_matvec={had_hbb_matvec} htbeta_matvec={had_htbeta_matvec}"
1997 ),
1998 },
1999 }
2000}
2001
2002fn squared_norm(values: &[f64]) -> f64 {
2003 values.iter().map(|v| v * v).sum()
2004}
2005
2006impl From<ArrowSchurError> for DeviceResidentArrowError {
2007 fn from(err: ArrowSchurError) -> Self {
2008 Self::Solve {
2009 reason: err.to_string(),
2010 }
2011 }
2012}
2013
2014pub fn qwen_non_gating_fixture() -> Result<DeviceResidentArrowWorkspace, DeviceResidentArrowError> {
2016 qwen_non_gating_fixture_seeded(0x1017_0003_D3A1_5EED)
2017}
2018
2019pub fn qwen_non_gating_fixture_seeded(
2023 seed: u64,
2024) -> Result<DeviceResidentArrowWorkspace, DeviceResidentArrowError> {
2025 fixture_for_shape_seeded(DeviceResidentArrowShape::qwen_non_gating(), seed)
2026}
2027
2028pub fn color_arm_fixture() -> Result<DeviceResidentArrowWorkspace, DeviceResidentArrowError> {
2033 fixture_for_shape_seeded(DeviceResidentArrowShape::color_arm(), 0x1017_C010_2A12_5EED)
2034}
2035
2036fn fixture_for_shape_seeded(
2041 shape: DeviceResidentArrowShape,
2042 seed: u64,
2043) -> Result<DeviceResidentArrowWorkspace, DeviceResidentArrowError> {
2044 if shape.d == 0 {
2045 return Err(DeviceResidentArrowError::Shape {
2046 reason: "fixture_for_shape_seeded requires d >= 1".to_string(),
2047 });
2048 }
2049 let d = shape.d;
2050 let mut rng = SplitMix64::new(seed);
2051 let mut target_x = vec![0.0_f64; shape.target_len()];
2052 for i in 0..shape.n {
2053 for j in 0..shape.p {
2054 let phase = ((i % 97) as f64) * 0.013 + ((j % 131) as f64) * 0.007;
2055 target_x[i * shape.p + j] = 0.02 * phase.sin() + 0.001 * rng.sample_signed();
2056 }
2057 }
2058 let mut basis_values = vec![0.0_f64; shape.basis_len()];
2059 let mut gate_activations = vec![1.0_f64; shape.basis_len()];
2060 for i in 0..shape.n {
2061 for a in 0..shape.basis_cols {
2062 let phase = ((i + 1) as f64) * ((a + 1) as f64) * 0.003;
2063 basis_values[i * shape.basis_cols + a] = phase.cos();
2064 gate_activations[i * shape.basis_cols + a] = 1.0;
2065 }
2066 }
2067 let mut row_hessian_slabs = vec![0.0_f64; shape.row_hessian_len()];
2068 let mut row_cross_slabs = vec![0.0_f64; shape.row_cross_len()];
2069 let mut row_gradient_slabs = vec![0.0_f64; shape.row_gradient_len()];
2070 for i in 0..shape.n {
2071 let mut basis_sum = 0.0_f64;
2072 for a in 0..shape.basis_cols {
2073 basis_sum +=
2074 basis_values[i * shape.basis_cols + a] * gate_activations[i * shape.basis_cols + a];
2075 }
2076 let h_base = i * d * d;
2079 for r in 0..d {
2080 for c in 0..d {
2081 let v = if r == c {
2082 3.0 + 0.01 * basis_sum.abs() + 0.1 * (r as f64)
2083 } else {
2084 0.02 * (basis_sum + (r + c) as f64).sin() / (d as f64)
2085 };
2086 row_hessian_slabs[h_base + r * d + c] = v;
2087 }
2088 }
2089 for r in 0..d {
2091 for c in 0..r {
2092 let avg = 0.5
2093 * (row_hessian_slabs[h_base + r * d + c]
2094 + row_hessian_slabs[h_base + c * d + r]);
2095 row_hessian_slabs[h_base + r * d + c] = avg;
2096 row_hessian_slabs[h_base + c * d + r] = avg;
2097 }
2098 }
2099 let b_base = i * d * shape.p;
2101 let g_base = i * d;
2102 for r in 0..d {
2103 for j in 0..shape.p {
2104 let feature = ((j % 257) as f64) * 0.011;
2105 row_cross_slabs[b_base + r * shape.p + j] =
2106 1.0e-4 * (basis_sum + r as f64).sin() * feature.cos();
2107 }
2108 row_gradient_slabs[g_base + r] = 0.01 * (basis_sum + r as f64).sin();
2109 }
2110 }
2111 let mut border_hessian = vec![0.0_f64; shape.border_hessian_len()];
2112 for r in 0..shape.p {
2113 border_hessian[r * shape.p + r] = 4.0;
2114 if r + 1 < shape.p {
2115 border_hessian[r * shape.p + r + 1] = 0.01;
2116 border_hessian[(r + 1) * shape.p + r] = 0.01;
2117 }
2118 }
2119 let mut border_gradient = vec![0.0_f64; shape.p];
2120 for j in 0..shape.p {
2121 border_gradient[j] = 0.001 * ((j % 193) as f64 * 0.017).sin();
2122 }
2123 DeviceResidentArrowWorkspace::new(
2124 shape,
2125 target_x,
2126 basis_values,
2127 gate_activations,
2128 DeviceResidentArrowSlabs {
2129 row_hessian_slabs,
2130 row_cross_slabs,
2131 row_gradient_slabs,
2132 border_hessian,
2133 border_gradient,
2134 },
2135 )
2136}
2137
2138pub struct MultiplexedFit {
2140 pub outcome: DeviceResidentInnerOutcome,
2141}
2142
2143pub fn run_resident_fits_multiplexed(
2173 workspaces: Vec<DeviceResidentArrowWorkspace>,
2174 opts: DeviceResidentInnerOptions,
2175) -> Result<Vec<Result<MultiplexedFit, DeviceResidentArrowError>>, String> {
2176 run_resident_fits_multiplexed_with(workspaces, opts, |workspace, opts| {
2177 workspace.device_fit(opts)
2178 })
2179}
2180
2181fn run_resident_fits_multiplexed_with<Run>(
2185 workspaces: Vec<DeviceResidentArrowWorkspace>,
2186 opts: DeviceResidentInnerOptions,
2187 run_one: Run,
2188) -> Result<Vec<Result<MultiplexedFit, DeviceResidentArrowError>>, String>
2189where
2190 Run: Fn(
2191 &DeviceResidentArrowWorkspace,
2192 &DeviceResidentInnerOptions,
2193 ) -> Result<DeviceResidentInnerOutcome, DeviceResidentArrowError>
2194 + Sync,
2195{
2196 let rows = crate::topology_selector::run_topology_race_parallel(
2197 workspaces,
2198 move |workspace: DeviceResidentArrowWorkspace| {
2199 run_one(&workspace, &opts).map(|outcome| MultiplexedFit { outcome })
2200 },
2201 )?;
2202 Ok(rows.into_iter().map(|row| row.result).collect())
2203}
2204
2205pub fn run_resident_fits_sequential(
2210 workspaces: &[DeviceResidentArrowWorkspace],
2211 opts: &DeviceResidentInnerOptions,
2212) -> Vec<Result<MultiplexedFit, DeviceResidentArrowError>> {
2213 workspaces
2214 .iter()
2215 .map(|workspace| {
2216 workspace
2217 .device_fit(opts)
2218 .map(|outcome| MultiplexedFit { outcome })
2219 })
2220 .collect()
2221}
2222
2223#[derive(Clone, Copy, Debug)]
2242pub struct SweepVariant {
2243 pub dim: DeviceResidentArrowShape,
2245 pub seed: u64,
2247}
2248
2249#[derive(Clone, Copy, Debug)]
2251pub struct SweepThroughput {
2252 pub fits: usize,
2253 pub succeeded: usize,
2254 pub wall_seconds: f64,
2255 pub fits_per_second: f64,
2257}
2258
2259pub fn build_sweep_workspaces(
2264 variants: &[SweepVariant],
2265) -> Result<Vec<DeviceResidentArrowWorkspace>, DeviceResidentArrowError> {
2266 variants
2267 .iter()
2268 .map(|v| fixture_for_shape_seeded(v.dim, v.seed))
2269 .collect()
2270}
2271
2272pub fn run_variant_sweep_multiplexed(
2277 variants: &[SweepVariant],
2278 opts: DeviceResidentInnerOptions,
2279) -> Result<
2280 (
2281 Vec<Result<MultiplexedFit, DeviceResidentArrowError>>,
2282 SweepThroughput,
2283 ),
2284 String,
2285> {
2286 let workspaces = build_sweep_workspaces(variants).map_err(|e| e.to_string())?;
2287 run_battery_sweep_multiplexed(workspaces, opts)
2288}
2289
2290pub fn run_battery_sweep_multiplexed(
2301 workspaces: Vec<DeviceResidentArrowWorkspace>,
2302 opts: DeviceResidentInnerOptions,
2303) -> Result<
2304 (
2305 Vec<Result<MultiplexedFit, DeviceResidentArrowError>>,
2306 SweepThroughput,
2307 ),
2308 String,
2309> {
2310 let fits = workspaces.len();
2311 let start = std::time::Instant::now();
2312 let results = run_resident_fits_multiplexed(workspaces, opts)?;
2313 let wall_seconds = start.elapsed().as_secs_f64();
2314 let succeeded = results.iter().filter(|r| r.is_ok()).count();
2315 let throughput = SweepThroughput {
2316 fits,
2317 succeeded,
2318 wall_seconds,
2319 fits_per_second: (fits as f64) / wall_seconds.max(1e-9),
2320 };
2321 Ok((results, throughput))
2322}
2323
2324#[must_use]
2331pub fn color_arm_variant_matrix() -> Vec<SweepVariant> {
2332 let topologies = ["euclidean", "circle", "torus", "sphere"];
2333 let mut variants = Vec::with_capacity(4 * topologies.len() * 2);
2334 for k in 1..=4u64 {
2335 for (t_idx, _topology) in topologies.iter().enumerate() {
2336 for &(d, basis_cols, basis_tag) in &[(2usize, 8usize, 0u64), (1usize, 2usize, 1u64)] {
2338 let mut dim = DeviceResidentArrowShape::color_arm();
2339 dim.d = d;
2340 dim.basis_cols = basis_cols;
2341 let seed = 0x1017_C010_0000_0000 ^ (k << 16) ^ ((t_idx as u64) << 8) ^ basis_tag;
2342 variants.push(SweepVariant { dim, seed });
2343 }
2344 }
2345 }
2346 variants
2347}
2348
2349pub fn assert_sweep_parity_vs_sequential(
2356 variants: &[SweepVariant],
2357 opts: &DeviceResidentInnerOptions,
2358 multiplexed: &[Result<MultiplexedFit, DeviceResidentArrowError>],
2359) -> Result<SweepThroughput, String> {
2360 let workspaces = build_sweep_workspaces(variants).map_err(|e| e.to_string())?;
2361 let start = std::time::Instant::now();
2362 let sequential = run_resident_fits_sequential(&workspaces, opts);
2363 let wall_seconds = start.elapsed().as_secs_f64();
2364 if sequential.len() != multiplexed.len() {
2365 return Err(format!(
2366 "sweep parity: length mismatch seq={} mux={}",
2367 sequential.len(),
2368 multiplexed.len()
2369 ));
2370 }
2371 for (idx, (seq, mux)) in sequential.iter().zip(multiplexed.iter()).enumerate() {
2372 match (seq, mux) {
2373 (Ok(s), Ok(m)) => {
2374 if s.outcome.t.as_slice() != m.outcome.t.as_slice()
2375 || s.outcome.beta.as_slice() != m.outcome.beta.as_slice()
2376 || s.outcome.objective.to_bits() != m.outcome.objective.to_bits()
2377 {
2378 return Err(format!(
2379 "sweep parity: fit {idx} multiplexed result differs from sequential"
2380 ));
2381 }
2382 }
2383 (Err(_), Err(_)) => {}
2384 _ => {
2385 return Err(format!(
2386 "sweep parity: fit {idx} success/failure disagrees seq-vs-mux"
2387 ));
2388 }
2389 }
2390 }
2391 let fits = variants.len();
2392 let succeeded = sequential.iter().filter(|r| r.is_ok()).count();
2393 Ok(SweepThroughput {
2394 fits,
2395 succeeded,
2396 wall_seconds,
2397 fits_per_second: (fits as f64) / wall_seconds.max(1e-9),
2398 })
2399}
2400
2401struct SplitMix64 {
2402 state: u64,
2403}
2404
2405impl SplitMix64 {
2406 const fn new(seed: u64) -> Self {
2407 Self { state: seed }
2408 }
2409
2410 fn next_u64(&mut self) -> u64 {
2411 gam_linalg::utils::splitmix64(&mut self.state)
2412 }
2413
2414 fn sample_signed(&mut self) -> f64 {
2415 let unit = (self.next_u64() >> 11) as f64 / ((1_u64 << 53) as f64);
2416 2.0 * unit - 1.0
2417 }
2418}
2419
2420#[cfg(test)]
2421mod tests {
2422 use super::*;
2423 use ndarray::Array2;
2424
2425 fn small_fixture(seed: u64) -> DeviceResidentArrowWorkspace {
2429 let shape = DeviceResidentArrowShape {
2437 n: 8,
2438 p: 4,
2439 basis_cols: 2,
2440 d: 2,
2441 };
2442 let mut rng = SplitMix64::new(seed);
2443 let target_x = vec![0.0_f64; shape.target_len()];
2444 let basis_values = vec![0.5_f64; shape.basis_len()];
2445 let gate_activations = vec![1.0_f64; shape.basis_len()];
2446
2447 let mut row_hessian_slabs = vec![0.0_f64; shape.row_hessian_len()];
2448 let mut row_cross_slabs = vec![0.0_f64; shape.row_cross_len()];
2449 let mut row_gradient_slabs = vec![0.0_f64; shape.row_gradient_len()];
2450 for i in 0..shape.n {
2451 let h = i * shape.d * shape.d;
2452 row_hessian_slabs[h] = 5.0 + 0.1 * rng.sample_signed();
2453 row_hessian_slabs[h + 1] = 0.05 * rng.sample_signed();
2454 row_hessian_slabs[h + 2] = row_hessian_slabs[h + 1];
2455 row_hessian_slabs[h + 3] = 4.0 + 0.1 * rng.sample_signed();
2456 let b = i * shape.d * shape.p;
2457 for j in 0..shape.p {
2458 row_cross_slabs[b + j] = 0.01 * rng.sample_signed();
2459 row_cross_slabs[b + shape.p + j] = 0.01 * rng.sample_signed();
2460 }
2461 let g = i * shape.d;
2462 row_gradient_slabs[g] = rng.sample_signed();
2463 row_gradient_slabs[g + 1] = rng.sample_signed();
2464 }
2465 let mut border_hessian = vec![0.0_f64; shape.border_hessian_len()];
2466 for r in 0..shape.p {
2467 border_hessian[r * shape.p + r] = 6.0 + 0.1 * rng.sample_signed();
2468 }
2469 let border_gradient: Vec<f64> = (0..shape.p).map(|_| rng.sample_signed()).collect();
2470
2471 DeviceResidentArrowWorkspace::new(
2472 shape,
2473 target_x,
2474 basis_values,
2475 gate_activations,
2476 DeviceResidentArrowSlabs {
2477 row_hessian_slabs,
2478 row_cross_slabs,
2479 row_gradient_slabs,
2480 border_hessian,
2481 border_gradient,
2482 },
2483 )
2484 .expect("small resident fixture must validate")
2485 }
2486
2487 fn dense_hz(
2490 ws: &DeviceResidentArrowWorkspace,
2491 sys: &ArrowSchurSystem,
2492 ) -> (Array2<f64>, Array1<f64>) {
2493 let shape = ws.shape;
2494 let total = shape.n * shape.d + shape.p;
2495 let mut h = Array2::<f64>::zeros((total, total));
2496 let mut g0 = Array1::<f64>::zeros(total);
2497 for i in 0..shape.n {
2498 let base = i * shape.d;
2499 for r in 0..shape.d {
2500 for c in 0..shape.d {
2501 h[[base + r, base + c]] = sys.rows[i].htt[[r, c]];
2502 }
2503 for c in 0..shape.p {
2504 let v = sys.rows[i].htbeta[[r, c]];
2505 h[[base + r, shape.n * shape.d + c]] = v;
2506 h[[shape.n * shape.d + c, base + r]] = v;
2507 }
2508 g0[base + r] = sys.rows[i].gt[r];
2509 }
2510 }
2511 for r in 0..shape.p {
2512 for c in 0..shape.p {
2513 h[[shape.n * shape.d + r, shape.n * shape.d + c]] = sys.hbb[[r, c]];
2514 }
2515 g0[shape.n * shape.d + r] = sys.gb[r];
2516 }
2517 (h, g0)
2518 }
2519
2520 #[test]
2525 fn operator_apply_matches_dense_hessian_product() {
2526 let ws = small_fixture(0x2393_0A01);
2527 let sys = ws.to_arrow_system();
2528 let (h, _g0) = dense_hz(&ws, &sys);
2529 let shape = ws.shape;
2530 let t_len = shape.n * shape.d;
2531 let mut rng = SplitMix64::new(0x2393_0A02);
2532 let t: Vec<f64> = (0..t_len).map(|_| rng.sample_signed()).collect();
2533 let beta: Vec<f64> = (0..shape.p).map(|_| rng.sample_signed()).collect();
2534
2535 let apply = ws.apply_operator_host(&t, &beta);
2536
2537 let mut max_err = 0.0_f64;
2539 for row in 0..t_len {
2540 let mut expect = 0.0_f64;
2541 for (col, &b) in beta.iter().enumerate() {
2542 expect += h[[row, t_len + col]] * b;
2543 }
2544 max_err = max_err.max((expect - apply.cross_t[row]).abs());
2545 }
2546 for col in 0..shape.p {
2548 let mut cross = 0.0_f64;
2549 for (row, &tv) in t.iter().enumerate() {
2550 cross += h[[t_len + col, row]] * tv;
2551 }
2552 max_err = max_err.max((cross - apply.cross_beta[col]).abs());
2553 let mut border = 0.0_f64;
2554 for (c, &b) in beta.iter().enumerate() {
2555 border += h[[t_len + col, t_len + c]] * b;
2556 }
2557 max_err = max_err.max((border - apply.border_beta[col]).abs());
2558 }
2559 assert!(
2560 max_err < 1e-12,
2561 "fused operator apply disagrees with the dense H·z blocks: max_abs_err={max_err:e}"
2562 );
2563 }
2564
2565 #[test]
2570 fn residual_and_objective_from_apply_match_dense_quadratic() {
2571 let ws = small_fixture(0x2393_0B01);
2572 let base = ws.to_arrow_system();
2573 let (h, g0) = dense_hz(&ws, &base);
2574 let shape = ws.shape;
2575 let t_len = shape.n * shape.d;
2576 let total = t_len + shape.p;
2577 let mut rng = SplitMix64::new(0x2393_0B02);
2578 let t: Vec<f64> = (0..t_len).map(|_| rng.sample_signed()).collect();
2579 let beta: Vec<f64> = (0..shape.p).map(|_| rng.sample_signed()).collect();
2580 let z: Vec<f64> = t.iter().chain(beta.iter()).copied().collect();
2581
2582 let apply = ws.apply_operator_host(&t, &beta);
2583 let mut residual = ws.to_arrow_system();
2584 ws.residual_into(&mut residual, &base, &apply, &t);
2585
2586 let mut max_err = 0.0_f64;
2587 for row in 0..total {
2588 let mut hz = 0.0_f64;
2589 for (col, &value) in z.iter().enumerate() {
2590 hz += h[[row, col]] * value;
2591 }
2592 let want = hz - g0[row];
2593 let got = if row < t_len {
2594 residual.rows[row / shape.d].gt[row % shape.d]
2595 } else {
2596 residual.gb[row - t_len]
2597 };
2598 max_err = max_err.max((want - got).abs());
2599 }
2600 assert!(
2601 max_err < 1e-12,
2602 "residual_into disagrees with dense H z − g₀: max_abs_err={max_err:e}"
2603 );
2604
2605 let half_target_energy = 0.5 * squared_norm(&ws.target_x);
2606 let mut quad = 0.0_f64;
2607 let mut lin = 0.0_f64;
2608 for (row, &zr) in z.iter().enumerate() {
2609 let mut hz = 0.0_f64;
2610 for (col, &zc) in z.iter().enumerate() {
2611 hz += h[[row, col]] * zc;
2612 }
2613 quad += zr * hz;
2614 lin += g0[row] * zr;
2615 }
2616 let want = half_target_energy + 0.5 * quad - lin;
2617 let got = ws.objective_from_apply(&base, half_target_energy, &apply, &t, &beta);
2618 let rel = (want - got).abs() / want.abs().max(1.0);
2619 assert!(
2620 rel < 1e-12,
2621 "objective_from_apply disagrees with the dense quadratic: want={want:e} got={got:e} rel={rel:e}"
2622 );
2623 }
2624
2625 #[test]
2629 fn cpu_arrow_baseline_matches_dense_reference() {
2630 let ws = small_fixture(0x2393_0C01);
2631 let opts = DeviceResidentInnerOptions::default();
2632 let dense = ws
2633 .cpu_reference_fit(&opts)
2634 .expect("dense reference fit must succeed on the PD fixture");
2635 let arrow = ws
2636 .cpu_arrow_fit(&opts)
2637 .expect("structured CPU baseline must succeed on the PD fixture");
2638 assert_eq!(arrow.execution_path, ExecutionPath::Cpu);
2639 assert_eq!(arrow.residency.operator_device_applies, 0);
2640 assert!(arrow.residency.operator_applies > 0);
2641 let scale = dense
2642 .t
2643 .iter()
2644 .chain(dense.beta.iter())
2645 .fold(1.0_f64, |m, &v| m.max(v.abs()));
2646 let mut max_rel = 0.0_f64;
2647 for (a, b) in dense.t.iter().zip(arrow.t.iter()) {
2648 max_rel = max_rel.max((a - b).abs() / scale);
2649 }
2650 for (a, b) in dense.beta.iter().zip(arrow.beta.iter()) {
2651 max_rel = max_rel.max((a - b).abs() / scale);
2652 }
2653 assert!(
2654 max_rel < 1e-9,
2655 "structured CPU baseline solves a different problem than the dense oracle: max_rel={max_rel:e}"
2656 );
2657 }
2658
2659 #[test]
2665 fn host_operator_apply_is_bit_identical_across_thread_counts() {
2666 let shape = DeviceResidentArrowShape {
2667 n: 600,
2668 p: 12,
2669 basis_cols: 2,
2670 d: 2,
2671 };
2672 let ws = fixture_for_shape_seeded(shape, 0x2393_0D01)
2673 .expect("sweep-shaped fixture must validate");
2674 let t_len = shape.n * shape.d;
2675 let mut rng = SplitMix64::new(0x2393_0D02);
2676 let t: Vec<f64> = (0..t_len).map(|_| rng.sample_signed()).collect();
2677 let beta: Vec<f64> = (0..shape.p).map(|_| rng.sample_signed()).collect();
2678
2679 let outside = ws.apply_operator_host(&t, &beta);
2681 assert!(
2682 shape.n >= OPERATOR_PARALLEL_ROW_MIN,
2683 "fixture must be large enough to take the parallel arm"
2684 );
2685 let (inside, ()) = rayon::join(|| ws.apply_operator_host(&t, &beta), || ());
2689 assert_eq!(outside.cross_t, inside.cross_t);
2690 assert_eq!(outside.cross_beta, inside.cross_beta);
2691 assert_eq!(outside.border_beta, inside.border_beta);
2692 }
2693
2694 #[test]
2695 fn cpu_inner_loop_reaches_quadratic_minimiser() {
2696 let ws = small_fixture(0xABCD_0001);
2697 let opts = DeviceResidentInnerOptions::default();
2698 let outcome = ws.cpu_reference_fit(&opts).expect("cpu fit");
2699 assert!(
2700 outcome.converged,
2701 "inner loop must converge on a PD quadratic"
2702 );
2703
2704 let base = ws.to_arrow_system();
2706 let (h, g0) = dense_hz(&ws, &base);
2707 let total = ws.shape.n * ws.shape.d + ws.shape.p;
2708 let mut z = Array1::<f64>::zeros(total);
2709 for r in 0..ws.shape.n * ws.shape.d {
2710 z[r] = outcome.t[r];
2711 }
2712 for c in 0..ws.shape.p {
2713 z[ws.shape.n * ws.shape.d + c] = outcome.beta[c];
2714 }
2715 let hz = h.dot(&z);
2716 let mut max_resid = 0.0_f64;
2717 for r in 0..total {
2718 max_resid = max_resid.max((hz[r] - g0[r]).abs());
2719 }
2720 assert!(
2721 max_resid < 1e-9,
2722 "inner loop fixed point must solve H z = g0; residual {max_resid:e}"
2723 );
2724 }
2725
2726 #[test]
2727 fn cpu_multiplex_matches_sequential_bit_identical() {
2728 let seeds = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
2729 let opts = DeviceResidentInnerOptions::default();
2730
2731 let seq_workspaces: Vec<_> = seeds.iter().map(|&s| small_fixture(s)).collect();
2732 let sequential: Vec<_> = seq_workspaces
2733 .iter()
2734 .map(|ws| ws.cpu_reference_fit(&opts).expect("seq cpu fit"))
2735 .collect();
2736
2737 let mux_workspaces: Vec<_> = seeds.iter().map(|&s| small_fixture(s)).collect();
2738 let multiplexed = run_resident_fits_multiplexed_with(mux_workspaces, opts, |ws, opts| {
2739 ws.cpu_reference_fit(opts)
2740 })
2741 .expect("multiplexed cpu fits");
2742
2743 assert_eq!(sequential.len(), multiplexed.len());
2744 for (seq, mux) in sequential.iter().zip(multiplexed.iter()) {
2745 let mux = mux.as_ref().expect("mux fit ok");
2746 assert_eq!(seq.t.as_slice(), mux.outcome.t.as_slice());
2749 assert_eq!(seq.beta.as_slice(), mux.outcome.beta.as_slice());
2750 assert_eq!(seq.objective.to_bits(), mux.outcome.objective.to_bits());
2751 }
2752 }
2753
2754 #[test]
2763 fn device_resident_fit_matches_cpu_reference() {
2764 let ws = small_fixture(0x5AE_1017);
2765 let opts = DeviceResidentInnerOptions::default();
2766
2767 let cpu = ws.cpu_reference_fit(&opts).expect("cpu reference fit");
2769 assert!(cpu.converged, "cpu reference must converge on PD quadratic");
2770
2771 let base = ws.to_arrow_system();
2772
2773 println!(
2774 "DIAG_RESIDENT device_resident={} shape=({},{},{})",
2775 ws.device_resident(),
2776 ws.shape.n,
2777 ws.shape.d,
2778 ws.shape.p
2779 );
2780 if ws.device_resident() {
2781 let dev = ws.device_fit(&opts).expect("device resident fit");
2783 assert_eq!(
2784 dev.execution_path,
2785 ExecutionPath::GpuResidentFull,
2786 "device_fit must report the full device-resident execution path"
2787 );
2788 assert!(dev.converged, "device resident loop must converge");
2789
2790 let t_scale = cpu.t.iter().fold(1.0_f64, |m, &v| m.max(v.abs()));
2796 let b_scale = cpu.beta.iter().fold(1.0_f64, |m, &v| m.max(v.abs()));
2797 let mut max_rel = 0.0_f64;
2798 for (a, b) in dev.t.iter().zip(cpu.t.iter()) {
2799 max_rel = max_rel.max((a - b).abs() / t_scale);
2800 }
2801 for (a, b) in dev.beta.iter().zip(cpu.beta.iter()) {
2802 max_rel = max_rel.max((a - b).abs() / b_scale);
2803 }
2804 assert!(
2805 max_rel < 1e-9,
2806 "resident device fit must match CPU reference (rel {max_rel:e})"
2807 );
2808
2809 let one = ws
2815 .one_inner_iteration(opts.initial_ridge_t, opts.initial_ridge_beta)
2816 .expect("resident one_inner_iteration");
2817 assert_eq!(
2818 one.execution_path,
2819 ExecutionPath::GpuResidentLinearization,
2820 "one_inner_iteration must report resident single-linearization residency"
2821 );
2822
2823 match crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
2831 &base,
2832 opts.initial_ridge_t,
2833 opts.initial_ridge_beta,
2834 ) {
2835 Err(err) => panic!("resident frame must build on CUDA host: {err:?}"),
2836 Ok(frame) => {
2837 let g_t: Vec<f64> = base
2838 .rows
2839 .iter()
2840 .flat_map(|r| r.gt.iter().copied())
2841 .collect();
2842 let g_beta: Vec<f64> = base.gb.iter().copied().collect();
2843 let resident_sol = frame
2844 .solve_gradient(&g_t, &g_beta)
2845 .expect("resident single-gradient solve");
2846 let full =
2847 crate::gpu_kernels::arrow_schur::solve_arrow_newton_step_dense_reference(
2848 &base,
2849 opts.initial_ridge_t,
2850 opts.initial_ridge_beta,
2851 )
2852 .expect("dense reference single solve");
2853 let mut max_step_rel = 0.0_f64;
2854 let step_scale = full
2855 .delta_t
2856 .iter()
2857 .chain(full.delta_beta.iter())
2858 .fold(1.0_f64, |m, &v| m.max(v.abs()));
2859 for (a, b) in resident_sol.delta_t.iter().zip(full.delta_t.iter()) {
2860 max_step_rel = max_step_rel.max((a - b).abs() / step_scale);
2861 }
2862 for (a, b) in resident_sol.delta_beta.iter().zip(full.delta_beta.iter()) {
2863 max_step_rel = max_step_rel.max((a - b).abs() / step_scale);
2864 }
2865 assert!(
2866 max_step_rel < 1e-9,
2867 "resident solve_gradient must match full dense reference step \
2868 (rel {max_step_rel:e})"
2869 );
2870 }
2871 }
2872
2873 let reup = ws
2876 .device_reupload_fit(&opts)
2877 .expect("device re-uploading fit");
2878 assert_eq!(
2879 reup.execution_path,
2880 ExecutionPath::GpuReupload,
2881 "device_reupload_fit must report the re-uploading device path"
2882 );
2883 assert!(reup.converged, "re-uploading loop must converge");
2884 let mut max_reup_rel = 0.0_f64;
2885 for (a, b) in reup.t.iter().zip(cpu.t.iter()) {
2886 max_reup_rel = max_reup_rel.max((a - b).abs() / t_scale);
2887 }
2888 for (a, b) in reup.beta.iter().zip(cpu.beta.iter()) {
2889 max_reup_rel = max_reup_rel.max((a - b).abs() / b_scale);
2890 }
2891 assert!(
2892 max_reup_rel < 1e-9,
2893 "re-uploading GPU fit must match CPU reference (rel {max_reup_rel:e})"
2894 );
2895 } else {
2896 assert!(
2904 gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::GpuPolicy::Auto)
2905 .unwrap_or_else(|error| {
2906 panic!("GPU probe fault in resident SAE engagement test: {error}")
2907 })
2908 .is_none(),
2909 "device_resident() is false on a host WITH a CUDA runtime present, \
2910 despite a floor-clearing fixture (batch=8): the resident device \
2911 buffers failed to bind — a real device fault, not a CPU-only skip."
2912 );
2913 let dev = ws.device_fit(&opts);
2915 assert!(
2916 matches!(dev, Err(DeviceResidentArrowError::Unavailable { .. })),
2917 "device_fit must report Unavailable on a CPU-only host, got {dev:?}"
2918 );
2919 let reup = ws.device_reupload_fit(&opts);
2920 assert!(
2921 matches!(reup, Err(DeviceResidentArrowError::Unavailable { .. })),
2922 "device_reupload_fit must report Unavailable on a CPU-only host, got {reup:?}"
2923 );
2924 let frame = crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(
2925 &base,
2926 opts.initial_ridge_t,
2927 opts.initial_ridge_beta,
2928 );
2929 assert!(
2930 frame.is_err(),
2931 "resident frame construction must decline on a CPU-only host"
2932 );
2933 }
2934 }
2935
2936 #[test]
2954 fn resident_inner_solve_matches_production_arrow_core() {
2955 use crate::arrow_schur::{ArrowSolveOptions, solve_arrow_newton_step_core};
2956
2957 let ws = small_fixture(0x1017_F17);
2958 let opts = DeviceResidentInnerOptions::default();
2959
2960 let resident = ws.cpu_reference_fit(&opts).expect("resident cpu fit");
2962 assert!(
2963 resident.converged,
2964 "resident reference must converge on the PD quadratic"
2965 );
2966
2967 let sys = ws.to_arrow_system();
2971 let (delta_t, delta_beta, _diag) = solve_arrow_newton_step_core(
2972 &sys,
2973 opts.initial_ridge_t,
2974 opts.initial_ridge_beta,
2975 &ArrowSolveOptions::direct(),
2976 )
2977 .expect("production arrow-core solve");
2978
2979 let t_scale = resident.t.iter().fold(1.0_f64, |m, &v| m.max(v.abs()));
2984 let b_scale = resident.beta.iter().fold(1.0_f64, |m, &v| m.max(v.abs()));
2985 let mut max_rel_t = 0.0_f64;
2991 let mut worst_t: Option<(usize, f64, f64)> = None;
2992 for (i, (prod, res)) in delta_t.iter().zip(resident.t.iter()).enumerate() {
2993 let rel = (prod + res).abs() / t_scale;
2994 if rel > max_rel_t {
2995 max_rel_t = rel;
2996 worst_t = Some((i, *prod, *res));
2997 }
2998 }
2999 let mut max_rel_b = 0.0_f64;
3000 let mut worst_b: Option<(usize, f64, f64)> = None;
3001 for (i, (prod, res)) in delta_beta.iter().zip(resident.beta.iter()).enumerate() {
3002 let rel = (prod + res).abs() / b_scale;
3003 if rel > max_rel_b {
3004 max_rel_b = rel;
3005 worst_b = Some((i, *prod, *res));
3006 }
3007 }
3008 let max_rel = max_rel_t.max(max_rel_b);
3009 assert!(
3010 max_rel < 1e-9,
3011 "production arrow-core Newton step must be −(resident converged fit) on \
3012 the same quadratic; wiring the device seam into the SAE inner loop must \
3013 not change the system being solved. rel_t={max_rel_t:e} (worst {worst_t:?}: \
3014 Δt+t* must be 0), rel_beta={max_rel_b:e} (worst {worst_b:?}: Δβ+β* must \
3015 be 0). A t-only gap implicates the per-row factor / row-gradient \
3016 assembly; a β-only gap the border Schur path."
3017 );
3018 }
3019
3020 #[test]
3028 fn outer_sequence_reuses_frame_and_matches_independent() {
3029 let ws = super::color_arm_fixture().expect("color_arm fixture");
3030 let opts = DeviceResidentInnerOptions::default();
3031 let n = ws.shape.n;
3032 let d = ws.shape.d;
3033 let p = ws.shape.p;
3034
3035 let outers: Vec<(Vec<f64>, Vec<f64>)> = (0..3)
3039 .map(|s| {
3040 let g_t: Vec<f64> = (0..n * d)
3041 .map(|i| 0.01 * (((i + 3 * s) as f64) * 0.002).sin())
3042 .collect();
3043 let g_beta: Vec<f64> = (0..p)
3044 .map(|j| 0.001 * (((j + 11 * s) as f64) * 0.0009).cos())
3045 .collect();
3046 (g_t, g_beta)
3047 })
3048 .collect();
3049
3050 let independent = ws
3053 .cpu_reference_outer_sequence(&outers, &opts)
3054 .expect("cpu reference outer sequence");
3055 assert_eq!(independent.outers.len(), outers.len());
3056
3057 if ws.device_resident() {
3058 let shared = ws
3060 .device_fit_outer_sequence(&outers, &opts)
3061 .expect("device outer sequence");
3062 assert_eq!(
3063 shared.frame_builds,
3064 1,
3065 "across-outer residency must build the resident frame exactly once \
3066 for an unchanged operator (got {} builds over {} outers) — a count \
3067 > 1 means the frame was needlessly re-factored per outer",
3068 shared.frame_builds,
3069 outers.len()
3070 );
3071 for (idx, (sh, ind)) in shared
3074 .outers
3075 .iter()
3076 .zip(independent.outers.iter())
3077 .enumerate()
3078 {
3079 let scale = ind
3080 .t
3081 .iter()
3082 .chain(ind.beta.iter())
3083 .fold(1.0_f64, |m, &v| m.max(v.abs()));
3084 let mut max_rel = 0.0_f64;
3085 for (a, b) in sh.t.iter().zip(ind.t.iter()) {
3086 max_rel = max_rel.max((a - b).abs() / scale);
3087 }
3088 for (a, b) in sh.beta.iter().zip(ind.beta.iter()) {
3089 max_rel = max_rel.max((a - b).abs() / scale);
3090 }
3091 assert!(
3092 max_rel < 1e-9,
3093 "outer {idx}: across-outer-shared frame must match independent fit \
3094 (rel {max_rel:e})"
3095 );
3096 }
3097 println!(
3098 "[#1017 outer-seq color_arm] outers={} frame_builds={} (across-outer factor \
3099 amortized) parity<1e-9 OK",
3100 outers.len(),
3101 shared.frame_builds
3102 );
3103 } else {
3104 println!(
3105 "[#1017 outer-seq color_arm] no CUDA device — across-outer residency skipped; \
3106 run on the GPU node to assert frame_builds==1 + device parity"
3107 );
3108 }
3109 }
3110
3111 #[test]
3132 fn gpu_residency_per_solve_bench() {
3133 use std::time::Instant;
3134 const N_SOLVES: usize = 24;
3135 for (label, ws) in [
3136 ("color_arm", super::color_arm_fixture()),
3137 ("qwen_non_gating", super::qwen_non_gating_fixture()),
3138 ] {
3139 let ws = ws.expect("bench fixture must validate");
3140 let base = ws.to_arrow_system();
3141 let n = ws.shape.n;
3146 let d = ws.shape.d;
3147 let p = ws.shape.p;
3148 let gradients: Vec<(Vec<f64>, Vec<f64>)> = (0..N_SOLVES)
3149 .map(|s| {
3150 let g_t: Vec<f64> =
3151 (0..n * d).map(|i| ((i + s) as f64 * 0.001).sin()).collect();
3152 let g_beta: Vec<f64> = (0..p)
3153 .map(|j| ((j + 7 * s) as f64 * 0.0007).cos())
3154 .collect();
3155 (g_t, g_beta)
3156 })
3157 .collect();
3158
3159 if !ws.device_resident() {
3160 println!(
3161 "[#1017 per-solve {label}] no CUDA device — {N_SOLVES} solves skipped; \
3162 run on the GPU node for the across-iteration residency speedup"
3163 );
3164 continue;
3165 }
3166
3167 let t_build = Instant::now();
3171 match crate::gpu_kernels::arrow_schur::ResidentArrowFrameHandle::new(&base, 0.0, 0.0) {
3176 Err(err) => panic!("resident frame must build on CUDA host: {err:?}"),
3177 Ok(frame) => {
3178 let frame_build_ms = t_build.elapsed().as_secs_f64() * 1e3;
3179
3180 frame
3187 .solve_gradient(&gradients[0].0, &gradients[0].1)
3188 .expect("resident warm-up solve");
3189 {
3190 let mut sys = ws.to_arrow_system();
3191 for (i, row) in sys.rows.iter_mut().enumerate() {
3192 for r in 0..d {
3193 row.gt[r] = gradients[0].0[i * d + r];
3194 }
3195 }
3196 for (j, gb) in sys.gb.iter_mut().enumerate() {
3197 *gb = gradients[0].1[j];
3198 }
3199 sys.refresh_row_hessian_fingerprint();
3200 crate::gpu_kernels::arrow_schur::solve_arrow_newton_step(&sys, 0.0, 0.0)
3201 .expect("reupload warm-up solve");
3202 }
3203
3204 let t_res = Instant::now();
3209 let mut resident_steps = Vec::with_capacity(N_SOLVES);
3210 for (g_t, g_beta) in &gradients {
3211 resident_steps.push(
3212 frame
3213 .solve_gradient(g_t, g_beta)
3214 .expect("resident solve_gradient"),
3215 );
3216 }
3217 let resident_ms = t_res.elapsed().as_secs_f64() * 1e3;
3218
3219 let t_reup = Instant::now();
3221 let mut reupload_steps = Vec::with_capacity(N_SOLVES);
3222 for (g_t, g_beta) in &gradients {
3223 let mut sys = ws.to_arrow_system();
3224 for (i, row) in sys.rows.iter_mut().enumerate() {
3225 for r in 0..d {
3226 row.gt[r] = g_t[i * d + r];
3227 }
3228 }
3229 for (j, gb) in sys.gb.iter_mut().enumerate() {
3230 *gb = g_beta[j];
3231 }
3232 sys.refresh_row_hessian_fingerprint();
3233 reupload_steps.push(
3234 crate::gpu_kernels::arrow_schur::solve_arrow_newton_step(
3235 &sys, 0.0, 0.0,
3236 )
3237 .expect("reupload solve_arrow_newton_step"),
3238 );
3239 }
3240 let reupload_ms = t_reup.elapsed().as_secs_f64() * 1e3;
3241
3242 let mut max_rel = 0.0_f64;
3245 for (rs, us) in resident_steps.iter().zip(reupload_steps.iter()) {
3246 let scale = us
3247 .delta_t
3248 .iter()
3249 .chain(us.delta_beta.iter())
3250 .fold(1.0_f64, |m, &v| m.max(v.abs()));
3251 for (a, b) in rs.delta_t.iter().zip(us.delta_t.iter()) {
3252 max_rel = max_rel.max((a - b).abs() / scale);
3253 }
3254 for (a, b) in rs.delta_beta.iter().zip(us.delta_beta.iter()) {
3255 max_rel = max_rel.max((a - b).abs() / scale);
3256 }
3257 }
3258
3259 let resident_per_solve = resident_ms / N_SOLVES as f64;
3260 let reupload_per_solve = reupload_ms / N_SOLVES as f64;
3261 let residency_speedup = reupload_ms / resident_ms.max(1e-9);
3262 println!(
3263 "[#1017 per-solve {label}] N={N_SOLVES} frame_build={frame_build_ms:.2}ms \
3264 resident={resident_ms:.2}ms ({resident_per_solve:.3}ms/solve, \
3265 grad-upload + warm factors) reupload={reupload_ms:.2}ms \
3266 ({reupload_per_solve:.3}ms/solve, N factors + N D/B uploads) \
3267 residency_speedup={residency_speedup:.2}x parity_rel={max_rel:e}"
3268 );
3269 assert!(
3270 max_rel < 1e-9,
3271 "{label}: resident per-solve steps must match reupload (rel {max_rel:e})"
3272 );
3273
3274 let min_speedup = if label == "color_arm" { 1.5 } else { 1.0 };
3290 assert!(
3291 residency_speedup > min_speedup,
3292 "{label}: across-iteration residency must beat per-solve re-upload \
3293 (residency_speedup={residency_speedup:.3}x, required >{min_speedup}x; \
3294 resident {resident_per_solve:.3}ms/solve vs reupload \
3295 {reupload_per_solve:.3}ms/solve over N={N_SOLVES} solves) — the resident \
3296 frame either silently re-uploaded D/B or the dispatch dropped the \
3297 amortized factor path"
3298 );
3299 }
3300 }
3301 }
3302 }
3303
3304 fn battery_variant_matrix() -> Vec<super::SweepVariant> {
3309 let mut variants = Vec::new();
3310 for k in 1..=4u64 {
3313 for basis_cols in [4usize, 8, 12] {
3314 let mut dim = DeviceResidentArrowShape::color_arm();
3315 dim.basis_cols = basis_cols;
3316 variants.push(super::SweepVariant {
3317 dim,
3318 seed: 0x1017_0040_0000_0000 ^ (k << 8) ^ (basis_cols as u64),
3319 });
3320 }
3321 }
3322 variants
3323 }
3324
3325 fn battery_variant_matrix_cpu_gate() -> Vec<super::SweepVariant> {
3335 battery_variant_matrix()
3336 .into_iter()
3337 .map(|mut variant| {
3338 variant.dim.p = 256;
3339 variant
3340 })
3341 .collect()
3342 }
3343
3344 #[test]
3348 fn variant_sweep_multiplex_matches_sequential() {
3349 let variants = battery_variant_matrix_cpu_gate();
3350 let opts = DeviceResidentInnerOptions::default();
3351
3352 let workspaces =
3355 super::build_sweep_workspaces(&variants).expect("sweep workspaces must build");
3356 let multiplexed =
3357 super::run_resident_fits_multiplexed_with(workspaces, opts, |ws, opts| {
3358 ws.cpu_reference_fit(opts)
3359 })
3360 .expect("multiplexed cpu sweep");
3361
3362 let seq_workspaces =
3363 super::build_sweep_workspaces(&variants).expect("sweep workspaces must build");
3364 let sequential: Vec<_> = seq_workspaces
3365 .iter()
3366 .map(|ws| ws.cpu_reference_fit(&opts))
3367 .collect();
3368
3369 assert_eq!(multiplexed.len(), sequential.len());
3370 for (idx, (mux, seq)) in multiplexed.iter().zip(sequential.iter()).enumerate() {
3371 let mux = &mux.as_ref().unwrap().outcome;
3372 let seq = seq.as_ref().unwrap();
3373 assert_eq!(
3374 mux.t.as_slice(),
3375 seq.t.as_slice(),
3376 "variant {idx}: multiplexed t differs from sequential"
3377 );
3378 assert_eq!(
3379 mux.beta.as_slice(),
3380 seq.beta.as_slice(),
3381 "variant {idx}: multiplexed beta differs from sequential"
3382 );
3383 assert_eq!(
3384 mux.objective.to_bits(),
3385 seq.objective.to_bits(),
3386 "variant {idx}: multiplexed objective differs from sequential"
3387 );
3388 }
3389 }
3390
3391 #[test]
3397 fn gpu_multiplex_throughput_bench() {
3398 let variants = battery_variant_matrix();
3399 let opts = DeviceResidentInnerOptions::default();
3400
3401 let probe = super::build_sweep_workspaces(&variants).expect("sweep workspaces");
3402 let any_device = probe.iter().any(|w| w.device_resident());
3403 if !any_device {
3404 println!(
3405 "[#1017 mux-bench] no CUDA device — {} variants (K1..4 x 3 basis) \
3406 skipped; run on the GPU node for cross-fit throughput",
3407 variants.len()
3408 );
3409 return;
3410 }
3411
3412 let (results, mux_tp) =
3413 super::run_variant_sweep_multiplexed(&variants, opts).expect("multiplexed sweep");
3414 let seq_tp = super::assert_sweep_parity_vs_sequential(&variants, &opts, &results)
3415 .expect("sweep parity vs sequential must hold");
3416 println!(
3417 "[#1017 mux-bench] fits={} succeeded={} multiplexed={:.3}s ({:.1} fits/s) \
3418 sequential={:.3}s ({:.1} fits/s) cross-fit-speedup={:.2}x",
3419 mux_tp.fits,
3420 mux_tp.succeeded,
3421 mux_tp.wall_seconds,
3422 mux_tp.fits_per_second,
3423 seq_tp.wall_seconds,
3424 seq_tp.fits_per_second,
3425 mux_tp.fits_per_second / seq_tp.fits_per_second.max(1e-9),
3426 );
3427 assert_eq!(
3428 mux_tp.succeeded, mux_tp.fits,
3429 "all battery variants must fit successfully on device"
3430 );
3431 }
3432}