Skip to main content

kryst/solver/
pcg.rs

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
21/// A synchronous reduction engine for caller-provided communicator wrappers.
22///
23/// `PcgSolver::solve_with_comm` accepts `Comm + Clone` implementations.
24/// The solver delegates its iteration to `CgSolver`, which needs a
25/// `UniverseComm` for its execution policy, so this engine preserves the
26/// caller's communicator for the actual reductions.
27struct 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    /// Monitor the preconditioned residual `sqrt(rᵀz)` (default)
95    Preconditioned,
96    /// Monitor the unpreconditioned residual `||r||₂`
97    Unpreconditioned,
98    /// Monitor the "natural" norm of the preconditioned residual vector
99    /// `||z||₂`, matching PETSc's `-ksp_norm_type natural` semantics.
100    Natural,
101    /// Do not compute or report a residual norm
102    None,
103}
104
105/// Supported PCG algorithmic variants.
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum PcgVariant {
108    /// Classic Hestenes-Stiefel style PCG.
109    Classic,
110    /// Pipelined CG with asynchronous reductions.
111    Pipelined {
112        /// Residual replacement interval (0 disables replacement).
113        replace_every: usize,
114    },
115}
116
117/// Default replacement interval for pipelined CG.
118///
119pub 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    /// Whether the initial guess in `x` should be treated as nonzero.
127    ///
128    /// PETSc zeroes the initial guess by default unless told otherwise via
129    /// `KSPSetInitialGuessNonzero`. We follow the same policy; when this flag is
130    /// `false` and the provided `x` is exactly the zero vector, the solver
131    /// skips the initial matvec and assumes a zero guess.
132    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    /// Optional runtime update of solver tolerances
160    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    /// Enable a more reproducible (but slightly slower) local dot product using
173    /// Kahan summation. When combined with a deterministic MPI reduction this
174    /// yields bitwise-identical results across runs.
175    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    /// Install a monitor that receives the true residual norm `||b - A x||₂`
185    /// at each iteration. This uses the already available residual and is
186    /// intended for debugging.
187    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    /// Indicate whether the supplied initial guess is nonzero.
332    ///
333    /// By default `x` is assumed to be zero, which avoids an extra matvec on
334    /// entry. Calling this with `true` forces the solver to compute the initial
335    /// residual `b - A x` even if `x` happens to be the zero vector.
336    pub fn with_nonzero_guess(mut self, f: bool) -> Self {
337        self.initial_guess_nonzero = f;
338        self
339    }
340
341    /// Set the nonzero initial guess flag after construction.
342    pub fn set_nonzero_guess(&mut self, f: bool) {
343        self.initial_guess_nonzero = f;
344    }
345
346    /// Toggle reproducible local dot products after construction.
347    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    /// Set or clear the true residual monitor after construction.
356    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}