1use crate::algebra::parallel::{dot_conj_local_with_mode, sum_abs2_local_with_mode};
2#[allow(unused_imports)]
3use crate::algebra::prelude::*;
4use crate::config::options::CgVariant;
5use crate::context::ksp_context::Workspace;
6use crate::error::KError;
7use crate::matrix::op::LinOp;
8use crate::ops::wrap::{as_s_op, as_s_pc_mut};
9use crate::parallel::{Comm, NoComm, ReduceHandle, ReductionEngine, UniverseComm};
10use crate::preconditioner::{PcSide, Preconditioner};
11use crate::reduction::{ReductionOptions, ReproMode};
12use crate::solver::LinearSolver;
13use crate::solver::MonitorCallback;
14use crate::solver::cg::CgSolver;
15use crate::utils::convergence::{Convergence, ReductionModel, SolveStats};
16use crate::utils::reduction::{ReductOptions, record_reduction};
17use std::any::Any;
18use std::fmt;
19use std::sync::Arc;
20
21struct CallerCommReductionEngine<C> {
28 comm: C,
29 mode: ReproMode,
30}
31
32impl<C> CallerCommReductionEngine<C> {
33 fn new(comm: C, mode: ReproMode) -> Self {
34 Self { comm, mode }
35 }
36}
37
38impl<C> fmt::Debug for CallerCommReductionEngine<C> {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 f.debug_struct("CallerCommReductionEngine")
41 .finish_non_exhaustive()
42 }
43}
44
45impl<C: Comm> ReductionEngine for CallerCommReductionEngine<C> {
46 fn supports_async(&self) -> bool {
47 false
48 }
49
50 fn allreduce_sum_r(&self, x: R) -> R {
51 self.comm.allreduce_sum(x)
52 }
53
54 fn allreduce_sum_s(&self, x: S) -> S {
55 self.comm.allreduce_sum_scalar(x)
56 }
57
58 fn norm2_s(&self, x: &[S]) -> R {
59 self.allreduce_sum_r(sum_abs2_local_with_mode(x, self.mode))
60 .max(R::zero())
61 .sqrt()
62 }
63
64 fn dot_s(&self, x: &[S], y: &[S]) -> S {
65 self.allreduce_sum_s(dot_conj_local_with_mode(x, y, self.mode))
66 }
67
68 fn sum_vec_r(&self, mut values: Vec<R>) -> Vec<R> {
69 record_reduction(values.len());
70 self.comm.allreduce_sum_slice(&mut values);
71 values
72 }
73
74 fn iallreduce_sum_r(&self, x: R) -> ReduceHandle<R> {
75 record_reduction(1);
76 ReduceHandle::ready(self.allreduce_sum_r(x))
77 }
78
79 fn iallreduce_sum_s(&self, x: S) -> ReduceHandle<S> {
80 #[cfg(feature = "complex")]
81 record_reduction(2);
82 #[cfg(not(feature = "complex"))]
83 record_reduction(1);
84 ReduceHandle::ready(self.allreduce_sum_s(x))
85 }
86
87 fn iallreduce_sum_vec_r(&self, values: Vec<R>) -> ReduceHandle<Vec<R>> {
88 ReduceHandle::ready(self.sum_vec_r(values))
89 }
90}
91
92#[derive(Debug, Clone, Copy)]
93pub enum CgNormType {
94 Preconditioned,
96 Unpreconditioned,
98 Natural,
101 None,
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum PcgVariant {
108 Classic,
110 Pipelined {
112 replace_every: usize,
114 },
115}
116
117pub const PCG_PIPELINED_DEFAULT_REPLACE_EVERY: usize = 50;
120
121pub struct PcgSolver {
122 pub(crate) conv: Convergence,
123 norm_type: CgNormType,
124 reduction: ReductionOptions,
125 true_residual_monitor: Option<Box<MonitorCallback<f64>>>,
126 initial_guess_nonzero: bool,
133 variant: PcgVariant,
134 async_reduction: ReductOptions,
135 async_enabled: bool,
136 async_min_n: usize,
137}
138
139impl PcgSolver {
140 pub fn new(rtol: f64, maxits: usize) -> Self {
141 Self {
142 conv: Convergence {
143 rtol,
144 atol: 1e-50,
145 dtol: 1e5,
146 max_iters: maxits,
147 },
148 norm_type: CgNormType::Preconditioned,
149 reduction: ReductionOptions::default(),
150 true_residual_monitor: None,
151 initial_guess_nonzero: false,
152 variant: PcgVariant::Classic,
153 async_reduction: ReductOptions::default(),
154 async_enabled: true,
155 async_min_n: 10_000,
156 }
157 }
158
159 pub fn set_tolerances(&mut self, rtol: f64, atol: f64, dtol: f64, maxits: usize) {
161 self.conv.rtol = rtol;
162 self.conv.atol = atol;
163 self.conv.dtol = dtol;
164 self.conv.max_iters = maxits;
165 }
166
167 pub fn with_norm(mut self, norm_type: CgNormType) -> Self {
168 self.norm_type = norm_type;
169 self
170 }
171
172 pub fn with_reproducible_dot(mut self, f: bool) -> Self {
176 self.reduction.mode = if f {
177 ReproMode::Deterministic
178 } else {
179 ReproMode::Fast
180 };
181 self
182 }
183
184 pub fn with_true_residual_monitor(mut self, m: Box<MonitorCallback<f64>>) -> Self {
188 self.true_residual_monitor = Some(m);
189 self
190 }
191
192 #[must_use = "with_variant returns an updated solver; assign it before continuing"]
193 pub fn with_variant(mut self, variant: PcgVariant) -> Self {
194 self.variant = variant;
195 self
196 }
197
198 pub fn set_variant(&mut self, variant: PcgVariant) {
199 self.variant = variant;
200 }
201
202 pub fn variant(&self) -> PcgVariant {
203 self.variant
204 }
205
206 pub fn pipelined_residual_refresh_every(&self) -> Option<usize> {
207 match self.variant {
208 PcgVariant::Pipelined { replace_every } if replace_every > 0 => Some(replace_every),
209 _ => None,
210 }
211 }
212
213 pub fn set_async_reduction_options(&mut self, opt: ReductOptions) {
214 self.async_reduction = opt;
215 }
216
217 pub fn set_async_enabled(&mut self, enabled: bool) {
218 self.async_enabled = enabled;
219 }
220
221 pub fn async_enabled(&self) -> bool {
222 self.async_enabled
223 }
224
225 pub fn set_async_min_n(&mut self, n: usize) {
226 self.async_min_n = n;
227 }
228
229 pub fn async_min_n(&self) -> usize {
230 self.async_min_n
231 }
232
233 fn reduction_model(&self) -> ReductionModel {
234 match self.variant {
235 PcgVariant::Classic => ReductionModel {
236 variant: "pcg-classic",
237 startup: 2,
238 per_iteration: 2.0,
239 tail: 0,
240 },
241 PcgVariant::Pipelined { .. } => ReductionModel {
242 variant: "pcg-pipelined",
243 startup: 1,
244 per_iteration: 1.0,
245 tail: 0,
246 },
247 }
248 }
249
250 fn async_options(&self) -> ReductOptions {
251 let mut opt = self.async_reduction.clone();
252 opt.mode = self.reduction.mode;
253 opt
254 }
255
256 fn configured_cg(&self) -> CgSolver {
257 let mut cg = CgSolver::new(self.conv.rtol, self.conv.max_iters);
258 cg.conv.atol = self.conv.atol;
259 cg.conv.dtol = self.conv.dtol;
260 cg.set_nonzero_guess(self.initial_guess_nonzero);
261 cg.set_async_enabled(self.async_enabled);
262 cg.set_async_min_n(self.async_min_n);
263 cg.set_variant(match self.variant {
264 PcgVariant::Classic => CgVariant::Classic,
265 PcgVariant::Pipelined { .. } => CgVariant::Pipelined,
266 });
267 if let PcgVariant::Pipelined { replace_every } = self.variant {
268 cg.set_pipelined_residual_refresh_every((replace_every > 0).then_some(replace_every));
269 }
270 cg.set_norm(match self.norm_type {
271 CgNormType::Preconditioned => crate::solver::cg::CgNormType::Preconditioned,
272 CgNormType::Unpreconditioned => crate::solver::cg::CgNormType::Unpreconditioned,
273 CgNormType::Natural => crate::solver::cg::CgNormType::Natural,
274 CgNormType::None => crate::solver::cg::CgNormType::None,
275 });
276 cg
277 }
278
279 #[allow(clippy::too_many_arguments)]
280 fn solve_k_via_cg<A>(
281 &mut self,
282 a: &A,
283 pc: Option<&dyn crate::ops::kpc::KPreconditioner<Scalar = S>>,
284 b: &[S],
285 x: &mut [S],
286 pc_side: PcSide,
287 comm: &UniverseComm,
288 monitors: Option<&[Box<MonitorCallback<R>>]>,
289 work: Option<&mut Workspace>,
290 reduction_engine: Option<Arc<dyn ReductionEngine>>,
291 ) -> Result<SolveStats<R>, KError>
292 where
293 A: crate::ops::klinop::KLinOp<Scalar = S> + ?Sized,
294 {
295 let mut owned_workspace;
296 let work = match work {
297 Some(work) => work,
298 None => {
299 owned_workspace = Workspace::new(b.len());
300 &mut owned_workspace
301 }
302 };
303
304 let saved_reduction = work.reduction_options().clone();
305 let saved_engine = work.reduction_engine().cloned();
306 let reduction = self.async_options();
307 work.set_reduction_options(reduction.clone());
308 work.set_reduction_engine(
309 reduction_engine.unwrap_or_else(|| comm.reduction_engine(&reduction)),
310 );
311
312 let mut cg = self.configured_cg();
313 cg.set_true_residual_monitor(self.true_residual_monitor.take());
314 cg.setup_workspace(work);
315
316 let result = cg
317 .solve_with_comm(a, pc, b, x, pc_side, comm, monitors, Some(work))
318 .map(|stats| stats.with_reduction_model(self.reduction_model()));
319
320 self.true_residual_monitor = cg.take_true_residual_monitor();
321 work.set_reduction_options(saved_reduction);
322 if let Some(engine) = saved_engine {
323 work.set_reduction_engine(engine);
324 } else {
325 work.clear_reduction_engine();
326 }
327
328 result
329 }
330
331 pub fn with_nonzero_guess(mut self, f: bool) -> Self {
337 self.initial_guess_nonzero = f;
338 self
339 }
340
341 pub fn set_nonzero_guess(&mut self, f: bool) {
343 self.initial_guess_nonzero = f;
344 }
345
346 pub fn set_reproducible_dot(&mut self, f: bool) {
348 self.reduction.mode = if f {
349 ReproMode::Deterministic
350 } else {
351 ReproMode::Fast
352 };
353 }
354
355 pub fn set_true_residual_monitor(&mut self, m: Option<Box<MonitorCallback<f64>>>) {
357 self.true_residual_monitor = m;
358 }
359
360 #[allow(clippy::too_many_arguments)]
361 pub fn solve_k<A>(
362 &mut self,
363 a: &A,
364 pc: Option<&dyn crate::ops::kpc::KPreconditioner<Scalar = S>>,
365 b: &[S],
366 x: &mut [S],
367 pc_side: PcSide,
368 comm: &UniverseComm,
369 monitors: Option<&[Box<MonitorCallback<R>>]>,
370 work: Option<&mut Workspace>,
371 ) -> Result<SolveStats<R>, KError>
372 where
373 A: crate::ops::klinop::KLinOp<Scalar = S> + ?Sized,
374 {
375 self.solve_k_via_cg(a, pc, b, x, pc_side, comm, monitors, work, None)
376 }
377
378 #[allow(clippy::too_many_arguments)]
379 pub fn solve_with_comm<C: Comm + Clone>(
380 &mut self,
381 a: &dyn LinOp<S = f64>,
382 pc: Option<&mut dyn Preconditioner>,
383 b: &[f64],
384 x: &mut [f64],
385 pc_side: PcSide,
386 comm: &C,
387 monitors: Option<&[Box<MonitorCallback<f64>>]>,
388 work: Option<&mut Workspace>,
389 ) -> Result<SolveStats<f64>, KError> {
390 let universe = (comm as &dyn Any)
391 .downcast_ref::<UniverseComm>()
392 .cloned()
393 .or_else(|| {
394 (comm as &dyn Any)
395 .downcast_ref::<NoComm>()
396 .map(|_| UniverseComm::NoComm(NoComm))
397 })
398 .or_else(|| {
399 #[cfg(feature = "rayon")]
400 {
401 (comm as &dyn Any)
402 .downcast_ref::<crate::parallel::RayonComm>()
403 .map(|c| UniverseComm::Rayon(c.clone()))
404 }
405 #[cfg(not(feature = "rayon"))]
406 {
407 None
408 }
409 })
410 .or_else(|| {
411 #[cfg(feature = "mpi")]
412 {
413 (comm as &dyn Any)
414 .downcast_ref::<crate::parallel::MpiComm>()
415 .map(|c| UniverseComm::Mpi(std::sync::Arc::new(c.dup())))
416 }
417 #[cfg(not(feature = "mpi"))]
418 {
419 None
420 }
421 });
422 let caller_reduction_engine = universe.is_none().then(|| {
423 Arc::new(CallerCommReductionEngine::new(
424 comm.clone(),
425 self.async_options().effective_mode(),
426 )) as Arc<dyn ReductionEngine>
427 });
428 let universe = universe.unwrap_or_else(|| comm.split(0, comm.rank() as i32));
429 self.solve_impl(
430 a,
431 pc,
432 b,
433 x,
434 pc_side,
435 &universe,
436 monitors,
437 work,
438 caller_reduction_engine,
439 )
440 }
441
442 #[allow(clippy::too_many_arguments)]
443 fn solve_impl(
444 &mut self,
445 a: &dyn LinOp<S = f64>,
446 pc: Option<&mut dyn Preconditioner>,
447 b: &[f64],
448 x: &mut [f64],
449 pc_side: PcSide,
450 comm: &UniverseComm,
451 monitors: Option<&[Box<MonitorCallback<f64>>]>,
452 work: Option<&mut Workspace>,
453 reduction_engine: Option<Arc<dyn ReductionEngine>>,
454 ) -> Result<SolveStats<f64>, KError> {
455 let op = as_s_op(a);
456 let pc_wrapper = pc.map(as_s_pc_mut);
457 let pc_ref = pc_wrapper
458 .as_ref()
459 .map(|pc| pc as &dyn crate::ops::kpc::KPreconditioner<Scalar = S>);
460
461 let mut owned_workspace;
462 let work = match work {
463 Some(work) => Some(work),
464 None => {
465 owned_workspace = Workspace::new(b.len());
466 Some(&mut owned_workspace)
467 }
468 };
469
470 #[cfg(not(feature = "complex"))]
471 {
472 let b_s: &[S] = unsafe { &*(b as *const [f64] as *const [S]) };
473 let x_s: &mut [S] = unsafe { &mut *(x as *mut [f64] as *mut [S]) };
474 self.solve_k_via_cg(
475 &op,
476 pc_ref,
477 b_s,
478 x_s,
479 pc_side,
480 comm,
481 monitors,
482 work,
483 reduction_engine,
484 )
485 }
486
487 #[cfg(feature = "complex")]
488 {
489 let b_s: Vec<S> = b.iter().copied().map(S::from_real).collect();
490 let mut x_s: Vec<S> = x.iter().copied().map(S::from_real).collect();
491 let result = self.solve_k_via_cg(
492 &op,
493 pc_ref,
494 &b_s,
495 &mut x_s,
496 pc_side,
497 comm,
498 monitors,
499 work,
500 reduction_engine,
501 );
502 if result.is_ok() {
503 for (dst, src) in x.iter_mut().zip(x_s.iter()) {
504 *dst = src.real();
505 }
506 }
507 result
508 }
509 }
510}
511
512impl LinearSolver for PcgSolver {
513 type Error = KError;
514
515 fn as_any_mut(&mut self) -> &mut dyn Any {
516 self
517 }
518
519 fn setup_workspace(&mut self, work: &mut Workspace) {
520 if work.q.len() < 2 {
521 work.q.resize(2, Vec::new());
522 }
523 }
524
525 fn solve(
526 &mut self,
527 a: &dyn LinOp<S = f64>,
528 pc: Option<&mut dyn Preconditioner>,
529 b: &[f64],
530 x: &mut [f64],
531 pc_side: PcSide,
532 comm: &UniverseComm,
533 monitors: Option<&[Box<MonitorCallback<f64>>]>,
534 work: Option<&mut Workspace>,
535 ) -> Result<SolveStats<f64>, Self::Error> {
536 self.solve_impl(a, pc, b, x, pc_side, comm, monitors, work, None)
537 }
538}