singe_cusolver/irs.rs
1use std::{ptr, slice};
2
3use singe_cuda::{data_type::DataTypeLike, memory::DeviceMemory};
4
5use crate::{
6 context::Context,
7 error::{Error, Result},
8 layout::{MatrixMut, MatrixRef},
9 sys, try_ffi,
10 types::{IrsRefinement, PrecisionType},
11 utility::{to_i32, to_u64, to_usize},
12};
13
14#[derive(Debug)]
15pub struct IrsParams {
16 handle: sys::cusolverDnIRSParams_t,
17 main_precision: Option<PrecisionType>,
18 lowest_precision: Option<PrecisionType>,
19}
20
21#[derive(Debug, Default)]
22pub struct IrsInfos {
23 handle: sys::cusolverDnIRSInfos_t,
24 residual_history_requested: bool,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct ResidualHistoryEntry<T> {
29 pub total_iterations: T,
30 pub residual_norm: T,
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub struct ResidualHistory<T> {
35 pub rows: Vec<ResidualHistoryEntry<T>>,
36 pub leading_dimension: usize,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct IrsSolve {
41 pub n: usize,
42 pub right_hand_sides: usize,
43}
44
45impl IrsSolve {
46 pub fn new(n: usize, right_hand_sides: usize) -> Self {
47 Self {
48 n,
49 right_hand_sides,
50 }
51 }
52
53 pub fn workspace_size<T: DataTypeLike>(
54 self,
55 ctx: &Context,
56 params: &mut IrsParams,
57 ) -> Result<usize> {
58 xgesv_buffer_size::<T>(ctx, params, self.n, self.right_hand_sides)
59 }
60
61 pub fn execute<T: DataTypeLike>(
62 self,
63 ctx: &Context,
64 params: &mut IrsParams,
65 infos: &IrsInfos,
66 bindings: IrsSolveBindings<'_, T>,
67 ) -> Result<i32> {
68 xgesv(
69 ctx,
70 params,
71 infos,
72 self.n,
73 self.right_hand_sides,
74 bindings.a,
75 bindings.b,
76 bindings.x,
77 bindings.device_workspace,
78 bindings.dev_info,
79 )
80 }
81}
82
83#[derive(Debug)]
84pub struct IrsSolveBindings<'a, T> {
85 pub a: MatrixMut<'a, T>,
86 pub b: MatrixRef<'a, T>,
87 pub x: MatrixMut<'a, T>,
88 pub device_workspace: &'a mut DeviceMemory<u8>,
89 pub dev_info: &'a mut DeviceMemory<i32>,
90}
91
92// IRS parameter/info handles expose mutation through &mut self and inspection
93// through shared references, so immutable sharing follows the cuSOLVER contract.
94unsafe impl Send for IrsParams {}
95unsafe impl Sync for IrsParams {}
96unsafe impl Send for IrsInfos {}
97unsafe impl Sync for IrsInfos {}
98
99impl IrsParams {
100 /// Creates and initializes the parameter structure for IRS solvers such as
101 /// [`xgesv`] and [`xgels`].
102 ///
103 /// The returned parameter structure can be reused across calls to the same
104 /// IRS solver or to different IRS solvers.
105 ///
106 /// In CUDA 10.2, the behavior was different and a new parameter structure
107 /// was required for each IRS solve call.
108 ///
109 /// You can also reconfigure the parameters between solves, but only after
110 /// the previous IRS call has completed.
111 ///
112 /// # Errors
113 ///
114 /// Returns an error if cuSOLVER cannot allocate the required resources
115 /// or does not return a valid handle.
116 pub fn create() -> Result<Self> {
117 let mut handle = ptr::null_mut();
118 unsafe {
119 try_ffi!(sys::cusolverDnIRSParamsCreate(&raw mut handle))?;
120 }
121 if handle.is_null() {
122 return Err(Error::NullHandle);
123 }
124 let mut params = Self {
125 handle,
126 main_precision: None,
127 lowest_precision: None,
128 };
129 params.set_refinement_solver(IrsRefinement::None)?;
130 Ok(params)
131 }
132
133 /// Sets the refinement solver used by IRS operations such as [`xgesv`] and
134 /// [`xgels`].
135 ///
136 /// Configure the refinement algorithm before the first IRS solve. Newly created [`IrsParams`] do not set one by default.
137 ///
138 /// The supported values are described below.
139 ///
140 /// [`IrsRefinement::NotSet`]: Solver is not set. The IRS solver returns an
141 /// error if this value is used.
142 ///
143 /// [`IrsRefinement::None`]: No refinement solver; the IRS solver performs a factorization followed by a solve without any refinement.
144 /// For example, if the IRS solver was [`xgesv`], this is equivalent to an
145 /// [`xgesv`] solve without refinement, with the factorization carried out in
146 /// the lowest configured precision.
147 /// If both the main and lowest precision are [`PrecisionType::R64F`], the
148 /// solve is effectively performed in `f64`.
149 ///
150 /// [`IrsRefinement::Classical`]: Classical iterative refinement solver.
151 /// Similar to the value used in LAPACK operations.
152 ///
153 /// [`IrsRefinement::Gmres`]: GMRES (Generalized Minimal Residual) based iterative refinement solver.
154 /// Recent studies use GMRES as a refinement solver that can outperform
155 /// classical iterative refinement.
156 /// Recommended setting based on cuSOLVER experimentation.
157 ///
158 /// [`IrsRefinement::ClassicalGmres`]: Classical iterative refinement solver that uses the GMRES (Generalized Minimal Residual) internally to solve the correction equation at each iteration.
159 /// The classical refinement iteration is the outer iteration, and GMRES is
160 /// the inner iteration.
161 /// If the tolerance of the inner GMRES is set very low, for
162 /// example near machine precision, then the outer *classical refinement
163 /// iteration* performs only one iteration and this option behaves like
164 /// [`IrsRefinement::Gmres`].
165 ///
166 /// [`IrsRefinement::GmresGmres`]: GMRES-based iterative refinement solver
167 /// that uses another GMRES solve internally for the preconditioned system.
168 ///
169 /// # Errors
170 ///
171 /// Returns an error if cuSOLVER rejects the parameter structure.
172 pub fn set_refinement_solver(&mut self, refinement: IrsRefinement) -> Result<()> {
173 unsafe {
174 try_ffi!(sys::cusolverDnIRSParamsSetRefinementSolver(
175 self.as_raw(),
176 refinement.into(),
177 ))?;
178 }
179 Ok(())
180 }
181
182 /// Sets the main precision for the Iterative Refinement Solver (IRS).
183 ///
184 /// The main precision is the type of the input and output data.
185 /// Configure both the main and lowest precision before the first IRS solve. Those
186 /// values are not inferred when the parameter structure is created because
187 /// they depend on the input/output data type and the requested solver
188 /// configuration. You can set them independently or together with
189 /// [`IrsParams::set_solver_precisions`].
190 ///
191 /// # Errors
192 ///
193 /// Returns an error if cuSOLVER rejects the parameter structure.
194 pub fn set_main_precision(&mut self, precision: PrecisionType) -> Result<()> {
195 unsafe {
196 try_ffi!(sys::cusolverDnIRSParamsSetSolverMainPrecision(
197 self.as_raw(),
198 precision.into(),
199 ))?;
200 }
201 self.main_precision = Some(precision);
202 Ok(())
203 }
204
205 /// Sets the lowest precision that the IRS solver may use.
206 ///
207 /// The lowest precision is the minimum compute precision used
208 /// during the LU factorization process.
209 ///
210 /// Configure both the main and lowest precision before the first IRS solve. They
211 /// are not inferred when creating the parameter structure because they
212 /// depend on the input and output data types and the requested solver
213 /// configuration.
214 /// Usually the lowest precision defines the speedup that can be achieved.
215 /// The ratio between the performance of the lowest precision and the main
216 /// precision gives an approximate upper bound on the speedup.
217 /// More precisely, it depends on many factors, but for large matrices it is
218 /// often tied to the performance ratio of large GEMM-like kernels.
219 /// For instance, if the input/output precision is real double precision
220 /// [`PrecisionType::R64F`] and the lowest precision is
221 /// [`PrecisionType::R32F`], then a speedup of at most about 2x is expected
222 /// for large problem sizes.
223 /// If the lowest precision is [`PrecisionType::R16F`], expect 3x-4x.
224 /// A reasonable strategy accounts for the number of right-hand sides, the matrix size, and the convergence rate.
225 ///
226 /// # Errors
227 ///
228 /// Returns an error if cuSOLVER rejects the parameter structure.
229 pub fn set_lowest_precision(&mut self, precision: PrecisionType) -> Result<()> {
230 unsafe {
231 try_ffi!(sys::cusolverDnIRSParamsSetSolverLowestPrecision(
232 self.as_raw(),
233 precision.into(),
234 ))?;
235 }
236 self.lowest_precision = Some(precision);
237 Ok(())
238 }
239
240 /// Sets both the main and lowest precision for the Iterative Refinement
241 /// Solver (IRS).
242 ///
243 /// The main precision is the precision of the input and output data.
244 /// The lowest precision is the minimum compute precision used
245 /// during the LU factorization process.
246 ///
247 /// Configure both values before the first IRS solve. They are not inferred when
248 /// creating the parameter structure because they depend on the input and
249 /// output data types and the requested solver configuration.
250 ///
251 /// Convenience wrapper around
252 /// [`IrsParams::set_main_precision`] and
253 /// [`IrsParams::set_lowest_precision`].
254 /// All possible combinations of main/lowest precision are described in the table below.
255 /// Usually the lowest precision defines the speedup that can be achieved.
256 /// The ratio between the performance of the lowest precision and the main
257 /// precision gives an approximate upper bound on the speedup.
258 /// More precisely, it depends on many factors, but for large matrices it is
259 /// often tied to the performance ratio of large GEMM-like kernels.
260 /// For instance, if the input/output precision is real double precision
261 /// [`PrecisionType::R64F`] and the lowest precision is
262 /// [`PrecisionType::R32F`], then a speedup of at most about 2x is expected
263 /// for large problem sizes.
264 /// If the lowest precision is [`PrecisionType::R16F`], expect 3x-4x.
265 /// A reasonable strategy accounts for the number of right-hand sides, the matrix size, and the convergence rate.
266 ///
267 /// **Supported input/output data type and lower precision for the IRS solver**
268 ///
269 /// | **input/output Data Type (for example, main precision)** | **Supported values for the lowest precision** |
270 /// | --- | --- |
271 /// | [`PrecisionType::C64F`] | [`PrecisionType::C64F`], [`PrecisionType::C32F`], [`PrecisionType::C16F`], [`PrecisionType::C16Bf`], [`PrecisionType::CTf32`] |
272 /// | [`PrecisionType::C32F`] | [`PrecisionType::C32F`], [`PrecisionType::C16F`], [`PrecisionType::C16Bf`], [`PrecisionType::CTf32`] |
273 /// | [`PrecisionType::R64F`] | [`PrecisionType::R64F`], [`PrecisionType::R32F`], [`PrecisionType::R16F`], [`PrecisionType::R16Bf`], [`PrecisionType::RTf32`] |
274 /// | [`PrecisionType::R32F`] | [`PrecisionType::R32F`], [`PrecisionType::R16F`], [`PrecisionType::R16Bf`], [`PrecisionType::RTf32`] |
275 ///
276 /// # Errors
277 ///
278 /// Returns an error if cuSOLVER rejects the parameter structure.
279 pub fn set_solver_precisions(
280 &mut self,
281 main_precision: PrecisionType,
282 lowest_precision: PrecisionType,
283 ) -> Result<()> {
284 unsafe {
285 try_ffi!(sys::cusolverDnIRSParamsSetSolverPrecisions(
286 self.as_raw(),
287 main_precision.into(),
288 lowest_precision.into(),
289 ))?;
290 }
291 self.main_precision = Some(main_precision);
292 self.lowest_precision = Some(lowest_precision);
293 Ok(())
294 }
295
296 /// Sets the tolerance for the refinement solver.
297 /// By default it is such that all the RHS satisfy:
298 ///
299 /// `RNRM < SQRT(N)*XNRM*ANRM*EPS*BWDMAX` where
300 ///
301 /// * RNRM is the infinity-norm of the residual
302 /// * XNRM is the infinity-norm of the solution
303 /// * ANRM is the infinity-operator-norm of the matrix A
304 /// * EPS is the machine epsilon for the input/output data type that matches
305 /// LAPACK `xLAMCH('Epsilon')`
306 /// * BWDMAX, the value BWDMAX is fixed to 1.0
307 ///
308 /// Use this to set the tolerance to a lower or higher value.
309 /// The tolerance value is always stored in real double precision,
310 /// regardless of the input and output data type.
311 ///
312 /// # Errors
313 ///
314 /// Returns an error if cuSOLVER rejects the parameter structure.
315 pub fn set_tolerance(&mut self, tolerance: f64) -> Result<()> {
316 unsafe {
317 try_ffi!(sys::cusolverDnIRSParamsSetTol(self.as_raw(), tolerance))?;
318 }
319 Ok(())
320 }
321
322 /// Sets the tolerance for the inner refinement solver when
323 /// the refinement solver consists of two levels, for example
324 /// [`IrsRefinement::ClassicalGmres`] or [`IrsRefinement::GmresGmres`].
325 /// Ignored for one-level refinement solvers such as [`IrsRefinement::Classical`] or [`IrsRefinement::Gmres`].
326 /// The default value is 1e-4.
327 /// This sets the tolerance for the inner solver, such as the inner GMRES.
328 /// For example, if the refinement solver is
329 /// [`IrsRefinement::ClassicalGmres`], setting this tolerance means that the
330 /// inner GMRES solver converges to that tolerance at each outer
331 /// iteration of the classical refinement solver.
332 /// The tolerance value is always stored in real double precision,
333 /// regardless of the input and output data type.
334 ///
335 /// # Errors
336 ///
337 /// Returns an error if cuSOLVER rejects the parameter structure.
338 pub fn set_inner_tolerance(&mut self, tolerance: f64) -> Result<()> {
339 unsafe {
340 try_ffi!(sys::cusolverDnIRSParamsSetTolInner(
341 self.as_raw(),
342 tolerance,
343 ))?;
344 }
345 Ok(())
346 }
347
348 /// Sets the total number of allowed refinement iterations before the solver stops.
349 /// The total is the sum of the outer and inner iterations. Inner iterations are meaningful when a two-level refinement solver is configured.
350 /// The default value is 50.
351 ///
352 /// # Errors
353 ///
354 /// Returns an error if cuSOLVER rejects the parameter structure.
355 pub fn set_max_iterations(&mut self, max_iterations: i32) -> Result<()> {
356 unsafe {
357 try_ffi!(sys::cusolverDnIRSParamsSetMaxIters(
358 self.as_raw(),
359 max_iterations,
360 ))?;
361 }
362 Ok(())
363 }
364
365 /// Sets the maximum number of iterations allowed for the inner refinement solver.
366 /// Ignored for one-level refinement solvers such as [`IrsRefinement::Classical`] or [`IrsRefinement::Gmres`].
367 /// The inner refinement solver stops after reaching either the inner tolerance or `MaxItersInner`.
368 /// The default value is 50.
369 /// Cannot be larger than `MaxIters` because `MaxIters` is the total number of allowed iterations.
370 /// If [`IrsParams::set_max_iterations`] is called after this method, it has priority and overwrites `MaxItersInner` with `min(MaxIters, MaxItersInner)`.
371 ///
372 /// # Errors
373 ///
374 /// Returns an error if `max_iterations` is larger than `MaxIters`, or if
375 /// cuSOLVER rejects the parameter structure.
376 pub fn set_max_inner_iterations(&mut self, max_iterations: i32) -> Result<()> {
377 unsafe {
378 try_ffi!(sys::cusolverDnIRSParamsSetMaxItersInner(
379 self.as_raw(),
380 max_iterations,
381 ))?;
382 }
383 Ok(())
384 }
385
386 /// Returns the current maximum-iteration setting in this parameter structure.
387 /// Current parameter configuration, distinct from [`IrsInfos::max_iterations`], which returns the maximum number of iterations allowed for a particular IRS solver call.
388 /// The parameter structure can be reused across many IRS solver calls.
389 /// The allowed `MaxIters` value can change between calls, while the `Infos` structure contains information about one particular call and cannot be reused for different calls.
390 ///
391 /// # Errors
392 ///
393 /// Returns an error if cuSOLVER rejects the parameter structure.
394 pub fn max_iterations(&self) -> Result<i32> {
395 let mut value = 0;
396 unsafe {
397 try_ffi!(sys::cusolverDnIRSParamsGetMaxIters(
398 self.as_raw(),
399 &raw mut value,
400 ))?;
401 }
402 Ok(value)
403 }
404
405 /// Enables fallback to the main precision if the Iterative Refinement Solver (IRS) fails to converge.
406 /// If the IRS solver fails to converge, it returns a non-convergence code such as `niter < 0`.
407 /// With fallback disabled, it returns the non-convergent solution as-is.
408 /// With fallback enabled, it falls back to the main precision, which is the input/output data precision, and solves the problem again from scratch.
409 /// This fallback is the default behavior.
410 ///
411 /// # Errors
412 ///
413 /// Returns an error if cuSOLVER rejects the parameter structure.
414 pub fn enable_fallback(&mut self) -> Result<()> {
415 unsafe {
416 try_ffi!(sys::cusolverDnIRSParamsEnableFallback(self.as_raw()))?;
417 }
418 Ok(())
419 }
420
421 /// Disables fallback to the main precision if the Iterative Refinement Solver (IRS) fails to converge.
422 /// If the IRS solver fails to converge, it returns a non-convergence code such as `niter < 0`.
423 /// With fallback disabled, the returned solution is whatever the refinement solver reached before returning.
424 /// Disabling fallback does not guarantee that the solution is accurate.
425 /// Re-enable fallback with [`IrsParams::enable_fallback`].
426 ///
427 /// # Errors
428 ///
429 /// Returns an error if cuSOLVER rejects the parameter structure.
430 pub fn disable_fallback(&mut self) -> Result<()> {
431 unsafe {
432 try_ffi!(sys::cusolverDnIRSParamsDisableFallback(self.as_raw()))?;
433 }
434 Ok(())
435 }
436
437 fn ensure_type_precision<T: DataTypeLike>(&mut self) -> Result<()> {
438 let precision = PrecisionType::from_data_type(T::data_type())
439 .ok_or(Error::InvalidPrecisionConfiguration)?;
440 match self.main_precision {
441 Some(existing) if existing != precision => {
442 return Err(Error::InvalidPrecisionConfiguration);
443 }
444 None => self.set_main_precision(precision)?,
445 _ => {}
446 }
447 if self.lowest_precision.is_none() {
448 self.set_lowest_precision(precision)?;
449 }
450 Ok(())
451 }
452
453 pub fn as_raw(&self) -> sys::cusolverDnIRSParams_t {
454 self.handle
455 }
456
457 /// Takes ownership of a raw cuSOLVER IRS params handle.
458 ///
459 /// # Safety
460 ///
461 /// `handle` must be a valid `cusolverDnIRSParams_t` created by cuSOLVER.
462 /// The returned wrapper takes ownership and will destroy it with
463 /// `cusolverDnIRSParamsDestroy`; no other owner may destroy or keep using it.
464 pub unsafe fn from_raw(handle: sys::cusolverDnIRSParams_t) -> Result<Self> {
465 if handle.is_null() {
466 return Err(Error::NullHandle);
467 }
468 Ok(Self {
469 handle,
470 main_precision: None,
471 lowest_precision: None,
472 })
473 }
474
475 /// Releases ownership and returns the raw cuSOLVER IRS params handle.
476 ///
477 /// The caller becomes responsible for destroying the handle.
478 pub fn into_raw(self) -> sys::cusolverDnIRSParams_t {
479 let handle = self.handle;
480 std::mem::forget(self);
481 handle
482 }
483}
484
485impl Drop for IrsParams {
486 fn drop(&mut self) {
487 unsafe {
488 if let Err(err) = try_ffi!(sys::cusolverDnIRSParamsDestroy(self.handle)) {
489 #[cfg(debug_assertions)]
490 eprintln!("failed to destroy cusolver irs params: {err}");
491 }
492 }
493 }
494}
495
496impl IrsInfos {
497 /// Creates and initializes the `Infos` structure that holds refinement information for an Iterative Refinement Solver (IRS) call.
498 /// Such information includes the total number of iterations needed to converge (`Niters`), the number of outer iterations (meaningful when a two-level preconditioner such as [`IrsRefinement::ClassicalGmres`] is used), the maximum number of iterations allowed for that call, and a pointer to the convergence-history residual norm matrix.
499 /// Construct the `Infos` structure before calling an IRS solver.
500 /// The `Infos` structure is valid for only one call to an IRS solver, since it holds information about that solve; each solve requires its own `Infos` structure.
501 ///
502 /// # Errors
503 ///
504 /// Returns an error if cuSOLVER cannot allocate the required resources
505 /// or does not return a valid handle.
506 pub fn create() -> Result<Self> {
507 let mut handle = ptr::null_mut();
508 unsafe {
509 try_ffi!(sys::cusolverDnIRSInfosCreate(&raw mut handle))?;
510 }
511 if handle.is_null() {
512 return Err(Error::NullHandle);
513 }
514 Ok(Self {
515 handle,
516 residual_history_requested: false,
517 })
518 }
519
520 /// Returns the total number of iterations performed by the IRS solver.
521 /// If this value is negative, the IRS solver did not converge. If fallback to full precision was enabled, the solver fell back to a full-precision solution.
522 /// See [`xgesv`] and [`xgels`] for the meaning of negative `niters` values.
523 ///
524 /// # Errors
525 ///
526 /// Returns an error if cuSOLVER rejects the `Infos` structure.
527 pub fn niters(&self) -> Result<i32> {
528 let mut value = 0;
529 unsafe {
530 try_ffi!(sys::cusolverDnIRSInfosGetNiters(
531 self.as_raw(),
532 &raw mut value,
533 ))?;
534 }
535 Ok(value)
536 }
537
538 /// Returns the number of iterations performed by the outer refinement loop of the IRS solver.
539 /// For one-level solvers such as [`IrsRefinement::Classical`] or [`IrsRefinement::Gmres`], this is the same as `Niters`.
540 /// For two-level solvers such as [`IrsRefinement::ClassicalGmres`] or [`IrsRefinement::GmresGmres`], this is the number of outer-loop iterations.
541 /// See [`IrsRefinement`] for refinement mode details.
542 ///
543 /// # Errors
544 ///
545 /// Returns an error if cuSOLVER rejects the `Infos` structure.
546 pub fn outer_niters(&self) -> Result<i32> {
547 let mut value = 0;
548 unsafe {
549 try_ffi!(sys::cusolverDnIRSInfosGetOuterNiters(
550 self.as_raw(),
551 &raw mut value,
552 ))?;
553 }
554 Ok(value)
555 }
556
557 /// Returns the maximum number of iterations allowed for the corresponding IRS solver call.
558 /// Setting used when that call happened, distinct from [`IrsParams::max_iterations`], which returns the current setting in the `params` configuration structure.
559 /// The `params` structure can be reused for many IRS solver calls.
560 /// The allowed `MaxIters` value can change between calls, while this `Infos` structure contains information about one particular call and cannot be reused for different calls.
561 ///
562 /// # Errors
563 ///
564 /// Returns an error if cuSOLVER rejects the `Infos` structure.
565 pub fn max_iterations(&self) -> Result<i32> {
566 let mut value = 0;
567 unsafe {
568 try_ffi!(sys::cusolverDnIRSInfosGetMaxIters(
569 self.as_raw(),
570 &raw mut value,
571 ))?;
572 }
573 Ok(value)
574 }
575
576 /// Asks the IRS solver to store the convergence history
577 /// (residual norms) of the refinement phase so it can later be queried with
578 /// [`IrsInfos::residual_history_f32`] or [`IrsInfos::residual_history_f64`].
579 ///
580 /// # Errors
581 ///
582 /// Returns an error if cuSOLVER rejects the `Infos` structure.
583 pub fn request_residual_history(&mut self) -> Result<()> {
584 unsafe {
585 try_ffi!(sys::cusolverDnIRSInfosRequestResidual(self.as_raw()))?;
586 }
587 self.residual_history_requested = true;
588 Ok(())
589 }
590
591 /// Returns the convergence history stored by the IRS solver when [`IrsInfos::request_residual_history`] was called before solving.
592 /// The residual norm type depends on the input and output precision.
593 /// Double-precision real and complex configurations report `f64` residuals, while single-precision real and complex configurations report `f32` residuals.
594 ///
595 /// The residual history matrix has two columns, even for multiple right-hand sides, and `MaxIters + 1` rows.
596 /// Only the first `OuterNiters + 1` rows contain residual norms; the remaining rows are undefined.
597 /// In the first column, each row `i` contains the total number of iterations performed up to outer iteration `i`.
598 /// In the second column, each row contains the residual norm for that outer iteration.
599 /// Row 0 contains the initial residual before the refinement loop starts, and subsequent rows contain residuals obtained at each outer iteration.
600 /// The history only covers the outer loop.
601 ///
602 /// If the refinement solver was [`IrsRefinement::Classical`] or [`IrsRefinement::Gmres`], then `OuterNiters == Niters`, and there are `Niters + 1` rows of norms corresponding to the `Niters` outer iterations.
603 ///
604 /// If the refinement solver was [`IrsRefinement::ClassicalGmres`] or [`IrsRefinement::GmresGmres`], then `OuterNiters <= Niters` corresponds to the outer iterations performed by the outer refinement loop.
605 /// There are `OuterNiters + 1` residual norms. Row `i` corresponds to outer iteration `i`; the first column contains the total number of outer and inner iterations performed up to that step, and the second column contains the residual norm at that step.
606 ///
607 /// For example, if [`IrsRefinement::ClassicalGmres`] needs 3 outer iterations to converge and 4, 3, and 3 inner iterations at each outer iteration, it performs 10 total iterations.
608 /// Row 0 corresponds to the first residual before the refinement start, so it has 0 in its first column.
609 /// Row 1 corresponds to outer iteration 1 and contains 4 in its first column, row 2 contains 7, and row 3 contains 10.
610 ///
611 /// In summary, let `ldh = MaxIters + 1`, the leading dimension of the residual matrix. Then `residual_history[i]` contains the total number of iterations performed at outer iteration `i`, and `residual_history[i + ldh]` contains the residual norm at that outer iteration.
612 ///
613 /// # Errors
614 ///
615 /// Returns an error if residual history was not requested before solving,
616 /// or if cuSOLVER rejects the `Infos` structure.
617 pub fn residual_history_f32(&self) -> Result<ResidualHistory<f32>> {
618 if !self.residual_history_requested {
619 return Err(Error::InvalidPrecisionConfiguration);
620 }
621 let (leading_dimension, valid_rows) = self.residual_history_layout()?;
622 let mut history = ptr::null_mut();
623 unsafe {
624 try_ffi!(sys::cusolverDnIRSInfosGetResidualHistory(
625 self.as_raw(),
626 &raw mut history,
627 ))?;
628 Ok(copy_residual_history(
629 history.cast::<f32>(),
630 leading_dimension,
631 valid_rows,
632 ))
633 }
634 }
635
636 /// Returns the convergence history stored by the IRS solver when [`IrsInfos::request_residual_history`] was called before solving.
637 /// The residual norm type depends on the input and output precision.
638 /// Double-precision real and complex configurations report `f64` residuals, while single-precision real and complex configurations report `f32` residuals.
639 ///
640 /// The residual history matrix has two columns, even for multiple right-hand sides, and `MaxIters + 1` rows.
641 /// Only the first `OuterNiters + 1` rows contain residual norms; the remaining rows are undefined.
642 /// In the first column, each row `i` contains the total number of iterations performed up to outer iteration `i`.
643 /// In the second column, each row contains the residual norm for that outer iteration.
644 /// Row 0 contains the initial residual before the refinement loop starts, and subsequent rows contain residuals obtained at each outer iteration.
645 /// The history only covers the outer loop.
646 ///
647 /// If the refinement solver was [`IrsRefinement::Classical`] or [`IrsRefinement::Gmres`], then `OuterNiters == Niters`, and there are `Niters + 1` rows of norms corresponding to the `Niters` outer iterations.
648 ///
649 /// If the refinement solver was [`IrsRefinement::ClassicalGmres`] or [`IrsRefinement::GmresGmres`], then `OuterNiters <= Niters` corresponds to the outer iterations performed by the outer refinement loop.
650 /// There are `OuterNiters + 1` residual norms. Row `i` corresponds to outer iteration `i`; the first column contains the total number of outer and inner iterations performed up to that step, and the second column contains the residual norm at that step.
651 ///
652 /// For example, if [`IrsRefinement::ClassicalGmres`] needs 3 outer iterations to converge and 4, 3, and 3 inner iterations at each outer iteration, it performs 10 total iterations.
653 /// Row 0 corresponds to the first residual before the refinement start, so it has 0 in its first column.
654 /// Row 1 corresponds to outer iteration 1 and contains 4 in its first column, row 2 contains 7, and row 3 contains 10.
655 ///
656 /// In summary, let `ldh = MaxIters + 1`, the leading dimension of the residual matrix. Then `residual_history[i]` contains the total number of iterations performed at outer iteration `i`, and `residual_history[i + ldh]` contains the residual norm at that outer iteration.
657 ///
658 /// # Errors
659 ///
660 /// Returns an error if residual history was not requested before solving,
661 /// or if cuSOLVER rejects the `Infos` structure.
662 pub fn residual_history_f64(&self) -> Result<ResidualHistory<f64>> {
663 if !self.residual_history_requested {
664 return Err(Error::InvalidPrecisionConfiguration);
665 }
666 let (leading_dimension, valid_rows) = self.residual_history_layout()?;
667 let mut history = ptr::null_mut();
668 unsafe {
669 try_ffi!(sys::cusolverDnIRSInfosGetResidualHistory(
670 self.as_raw(),
671 &raw mut history,
672 ))?;
673 Ok(copy_residual_history(
674 history.cast::<f64>(),
675 leading_dimension,
676 valid_rows,
677 ))
678 }
679 }
680
681 pub fn as_raw(&self) -> sys::cusolverDnIRSInfos_t {
682 self.handle
683 }
684
685 /// Takes ownership of a raw cuSOLVER IRS infos handle.
686 ///
687 /// # Safety
688 ///
689 /// `handle` must be a valid `cusolverDnIRSInfos_t` created by cuSOLVER.
690 /// The returned wrapper takes ownership and will destroy it with
691 /// `cusolverDnIRSInfosDestroy`; no other owner may destroy or keep using it.
692 pub unsafe fn from_raw(handle: sys::cusolverDnIRSInfos_t) -> Result<Self> {
693 if handle.is_null() {
694 return Err(Error::NullHandle);
695 }
696 Ok(Self {
697 handle,
698 residual_history_requested: false,
699 })
700 }
701
702 /// Releases ownership and returns the raw cuSOLVER IRS infos handle.
703 ///
704 /// The caller becomes responsible for destroying the handle.
705 pub fn into_raw(self) -> sys::cusolverDnIRSInfos_t {
706 let handle = self.handle;
707 std::mem::forget(self);
708 handle
709 }
710
711 fn residual_history_layout(&self) -> Result<(usize, usize)> {
712 let leading_dimension = self
713 .max_iterations()?
714 .checked_add(1)
715 .ok_or(Error::InvalidResidualHistory)
716 .and_then(|value| {
717 usize::try_from(value).map_err(|_| Error::OutOfRange {
718 name: "residual history leading dimension".into(),
719 })
720 })?;
721 let valid_rows = self
722 .outer_niters()?
723 .checked_add(1)
724 .ok_or(Error::InvalidResidualHistory)
725 .and_then(|value| {
726 usize::try_from(value).map_err(|_| Error::OutOfRange {
727 name: "residual history rows".into(),
728 })
729 })?;
730
731 if valid_rows > leading_dimension {
732 return Err(Error::InvalidResidualHistory);
733 }
734
735 Ok((leading_dimension, valid_rows))
736 }
737}
738
739impl Drop for IrsInfos {
740 fn drop(&mut self) {
741 unsafe {
742 if let Err(err) = try_ffi!(sys::cusolverDnIRSInfosDestroy(self.handle)) {
743 #[cfg(debug_assertions)]
744 eprintln!("failed to destroy cusolver irs infos: {err}");
745 }
746 }
747 }
748}
749
750pub fn xgesv_buffer_size<T: DataTypeLike>(
751 ctx: &Context,
752 params: &mut IrsParams,
753 n: usize,
754 nrhs: usize,
755) -> Result<usize> {
756 ctx.bind()?;
757 if n == 0 || nrhs == 0 {
758 return Err(Error::InvalidMatrixShape);
759 }
760 params.ensure_type_precision::<T>()?;
761 let mut workspace_bytes = 0;
762 unsafe {
763 try_ffi!(sys::cusolverDnIRSXgesv_bufferSize(
764 ctx.as_raw(),
765 params.as_raw(),
766 to_i32(n, "n")?,
767 to_i32(nrhs, "nrhs")?,
768 &raw mut workspace_bytes,
769 ))?;
770 }
771 to_usize(workspace_bytes, "workspace size")
772}
773
774/// Provides the same solve as the typed cuSOLVER `gesv` entry
775/// points, but through a generic Rust wrapper that exposes IRS configuration
776/// and reporting more directly.
777/// [`xgesv`] allows additional control of the solver parameters such as setting:
778///
779/// * the main precision (input/output precision) of the solver
780/// * the lowest precision to be used internally by the solver
781/// * the refinement solver type
782/// * the maximum allowed number of iterations in the refinement phase
783/// * the tolerance of the refinement solver
784/// * the fallback to main precision
785/// * and more
786///
787/// through [`IrsParams`] and its helper methods.
788/// Moreover, [`xgesv`] provides additional output information such as the convergence history (for example, residual norms) at each iteration and the number of iterations needed to converge.
789/// [`IrsInfos`] exposes the information reported for a particular solve.
790///
791/// The returned value describes the solving process.
792/// `Ok` indicates that the solve finished successfully. An error indicates that one of the arguments is incorrect, that the parameter or info structures are misconfigured, or that the solve did not finish successfully.
793/// Check `niters` and `dinfo` for additional error details.
794/// Provide the required device workspace through `workspace`.
795/// Query the required byte count with [`xgesv_buffer_size`].
796/// Apply any required configuration through the parameter structure before calling [`xgesv_buffer_size`] so the workspace size matches that configuration.
797///
798/// Tensor Float (TF32), introduced with NVIDIA Ampere architecture GPUs, is the most robust tensor core accelerated compute mode for the iterative refinement solver.
799/// It solves a broad range of HPC problems and can provide up to 4x and 5x
800/// speedups for real and complex systems, respectively.
801/// On Volta and Turing architecture GPUs, half precision tensor core acceleration is recommended.
802/// In cases where the iterative refinement solver fails to converge to the desired accuracy (main precision, input/output data precision), it is recommended to use main precision as internal lowest precision.
803///
804/// The following table provides all possible lowest-precision values corresponding to the input/output data type.
805/// If the lowest precision matches the input/output data type, the main
806/// precision factorization is used.
807///
808/// **Supported input/output data type and lower precision for the IRS solver**
809///
810/// | **input/output Data Type (for example, main precision)** | **Supported values for the lowest precision** |
811/// | --- | --- |
812/// | [`PrecisionType::C64F`] | [`PrecisionType::C64F`], [`PrecisionType::C32F`], [`PrecisionType::C16F`], [`PrecisionType::C16Bf`], [`PrecisionType::CTf32`] |
813/// | [`PrecisionType::C32F`] | [`PrecisionType::C32F`], [`PrecisionType::C16F`], [`PrecisionType::C16Bf`], [`PrecisionType::CTf32`] |
814/// | [`PrecisionType::R64F`] | [`PrecisionType::R64F`], [`PrecisionType::R32F`], [`PrecisionType::R16F`], [`PrecisionType::R16Bf`], [`PrecisionType::RTf32`] |
815/// | [`PrecisionType::R32F`] | [`PrecisionType::R32F`], [`PrecisionType::R16F`], [`PrecisionType::R16Bf`], [`PrecisionType::RTf32`] |
816///
817/// [`xgesv_buffer_size`] returns the required workspace size in bytes for the
818/// current [`IrsParams`] configuration.
819///
820/// # Errors
821///
822/// Returns an error if cuSOLVER rejects the matrix dimensions, leading
823/// dimensions, parameter structure, info structure, or workspace. The workspace
824/// can become invalid if [`xgesv_buffer_size`] is called and then an IRS
825/// configuration value, such as the lowest precision, is changed. cuSOLVER can
826/// also report an error if host memory allocation fails, if the selected IRS
827/// configuration is not supported on the current GPU architecture, if the
828/// library has not been initialized, or if the solve ends with an internal or
829/// numerical failure. Check `niters` and `dinfo` for additional solver details.
830pub fn xgesv<T: DataTypeLike>(
831 ctx: &Context,
832 params: &mut IrsParams,
833 infos: &IrsInfos,
834 n: usize,
835 nrhs: usize,
836 a: MatrixMut<'_, T>,
837 b: MatrixRef<'_, T>,
838 x: MatrixMut<'_, T>,
839 device_workspace: &mut DeviceMemory<u8>,
840 dev_info: &mut DeviceMemory<i32>,
841) -> Result<i32> {
842 ctx.bind()?;
843 validate_matrix(n, n, a.data.len(), a.leading_dimension)?;
844 validate_matrix(n, nrhs, b.data.len(), b.leading_dimension)?;
845 validate_matrix(n, nrhs, x.data.len(), x.leading_dimension)?;
846 require_info_buffer(dev_info)?;
847 let workspace_bytes = xgesv_buffer_size::<T>(ctx, params, n, nrhs)?;
848 require_workspace_bytes(device_workspace.byte_len(), workspace_bytes)?;
849 let mut niters = 0;
850 unsafe {
851 try_ffi!(sys::cusolverDnIRSXgesv(
852 ctx.as_raw(),
853 params.as_raw(),
854 infos.as_raw(),
855 to_i32(n, "n")?,
856 to_i32(nrhs, "nrhs")?,
857 a.data.as_mut_ptr() as _,
858 to_i32(a.leading_dimension, "ldda")?,
859 b.data.as_ptr() as _,
860 to_i32(b.leading_dimension, "lddb")?,
861 x.data.as_mut_ptr() as _,
862 to_i32(x.leading_dimension, "lddx")?,
863 device_workspace.as_mut_ptr() as _,
864 to_u64(workspace_bytes, "lwork_bytes")?,
865 &raw mut niters,
866 dev_info.as_mut_ptr() as _,
867 ))?;
868 }
869 Ok(niters)
870}
871
872pub fn xgels_buffer_size<T: DataTypeLike>(
873 ctx: &Context,
874 params: &mut IrsParams,
875 m: usize,
876 n: usize,
877 nrhs: usize,
878) -> Result<usize> {
879 ctx.bind()?;
880 if m == 0 || n == 0 || nrhs == 0 || n > m {
881 return Err(Error::InvalidMatrixShape);
882 }
883 params.ensure_type_precision::<T>()?;
884 let mut workspace_bytes = 0;
885 unsafe {
886 try_ffi!(sys::cusolverDnIRSXgels_bufferSize(
887 ctx.as_raw(),
888 params.as_raw(),
889 to_i32(m, "m")?,
890 to_i32(n, "n")?,
891 to_i32(nrhs, "nrhs")?,
892 &raw mut workspace_bytes,
893 ))?;
894 }
895 to_usize(workspace_bytes, "workspace size")
896}
897
898/// Provides the same solve as the typed cuSOLVER `gels` entry
899/// points, but through a generic Rust wrapper that exposes IRS configuration
900/// and reporting more directly.
901/// [`xgels`] allows additional control of the solver parameters such as setting:
902///
903/// * the main precision (input/output precision) of the solver,
904/// * the lowest precision to be used internally by the solver,
905/// * the refinement solver type
906/// * the maximum allowed number of iterations in the refinement phase
907/// * the tolerance of the refinement solver
908/// * the fallback to main precision
909/// * and others
910///
911/// through [`IrsParams`] and its helper methods.
912/// Moreover, [`xgels`] provides additional output information such as the convergence history (for example, residual norms) at each iteration and the number of iterations needed to converge.
913/// [`IrsInfos`] exposes the information reported for a particular solve.
914///
915/// The returned value describes the solving process.
916/// `Ok` indicates that the solve finished successfully. An error indicates that one of the arguments is incorrect, that the parameter or info structures are misconfigured, or that the solve did not finish successfully.
917/// Check `niters` and `dinfo` for additional error details.
918/// Provide the required device workspace through `workspace`.
919/// Query the required byte count with [`xgels_buffer_size`].
920/// Apply any required configuration through the parameter structure before calling [`xgels_buffer_size`] so the workspace size matches that configuration.
921///
922/// The following table provides all possible lowest-precision values corresponding to the input/output data type.
923/// If the lowest precision matches the input/output data type, the main
924/// precision factorization is used.
925///
926/// Tensor Float (TF32), introduced with NVIDIA Ampere architecture GPUs, is the most robust tensor core accelerated compute mode for the iterative refinement solver.
927/// It solves a broad range of HPC problems and can provide up to 4x and 5x
928/// speedups for real and complex systems, respectively.
929/// On Volta and Turing architecture GPUs, half precision tensor core acceleration is recommended.
930/// In cases where the iterative refinement solver fails to converge to the desired accuracy (main precision, input/output data precision), it is recommended to use main precision as internal lowest precision.
931///
932/// **Supported input/output data type and lower precision for the IRS solver**
933///
934/// | **input/output Data Type (for example, main precision)** | **Supported values for the lowest precision** |
935/// | --- | --- |
936/// | [`PrecisionType::C64F`] | [`PrecisionType::C64F`], [`PrecisionType::C32F`], [`PrecisionType::C16F`], [`PrecisionType::C16Bf`], [`PrecisionType::CTf32`] |
937/// | [`PrecisionType::C32F`] | [`PrecisionType::C32F`], [`PrecisionType::C16F`], [`PrecisionType::C16Bf`], [`PrecisionType::CTf32`] |
938/// | [`PrecisionType::R64F`] | [`PrecisionType::R64F`], [`PrecisionType::R32F`], [`PrecisionType::R16F`], [`PrecisionType::R16Bf`], [`PrecisionType::RTf32`] |
939/// | [`PrecisionType::R32F`] | [`PrecisionType::R32F`], [`PrecisionType::R16F`], [`PrecisionType::R16Bf`], [`PrecisionType::RTf32`] |
940///
941/// [`xgels_buffer_size`] returns the required workspace size in bytes for the
942/// current [`IrsParams`] configuration.
943///
944/// # Errors
945///
946/// Returns an error if cuSOLVER rejects the matrix dimensions, leading
947/// dimensions, parameter structure, info structure, or workspace. The workspace
948/// can become invalid if [`xgels_buffer_size`] is called and then an IRS
949/// configuration value, such as the lowest precision, is changed. cuSOLVER can
950/// also report an error if host memory allocation fails, if the selected IRS
951/// configuration is not supported on the current GPU architecture, if the
952/// library has not been initialized, or if the solve ends with an internal or
953/// numerical failure. Check `niters` and `dinfo` for additional solver details.
954pub fn xgels<T: DataTypeLike>(
955 ctx: &Context,
956 params: &mut IrsParams,
957 infos: &IrsInfos,
958 m: usize,
959 n: usize,
960 nrhs: usize,
961 a: MatrixMut<'_, T>,
962 b: MatrixRef<'_, T>,
963 x: MatrixMut<'_, T>,
964 device_workspace: &mut DeviceMemory<u8>,
965 dev_info: &mut DeviceMemory<i32>,
966) -> Result<i32> {
967 ctx.bind()?;
968 if n > m {
969 return Err(Error::InvalidMatrixShape);
970 }
971 validate_matrix(m, n, a.data.len(), a.leading_dimension)?;
972 validate_matrix(m, nrhs, b.data.len(), b.leading_dimension)?;
973 validate_matrix(n, nrhs, x.data.len(), x.leading_dimension)?;
974 require_info_buffer(dev_info)?;
975 let workspace_bytes = xgels_buffer_size::<T>(ctx, params, m, n, nrhs)?;
976 require_workspace_bytes(device_workspace.byte_len(), workspace_bytes)?;
977 let mut niters = 0;
978 unsafe {
979 try_ffi!(sys::cusolverDnIRSXgels(
980 ctx.as_raw(),
981 params.as_raw(),
982 infos.as_raw(),
983 to_i32(m, "m")?,
984 to_i32(n, "n")?,
985 to_i32(nrhs, "nrhs")?,
986 a.data.as_mut_ptr() as _,
987 to_i32(a.leading_dimension, "ldda")?,
988 b.data.as_ptr() as _,
989 to_i32(b.leading_dimension, "lddb")?,
990 x.data.as_mut_ptr() as _,
991 to_i32(x.leading_dimension, "lddx")?,
992 device_workspace.as_mut_ptr() as _,
993 to_u64(workspace_bytes, "lwork_bytes")?,
994 &raw mut niters,
995 dev_info.as_mut_ptr() as _,
996 ))?;
997 }
998 Ok(niters)
999}
1000
1001fn require_info_buffer(dev_info: &DeviceMemory<i32>) -> Result<()> {
1002 if dev_info.is_empty() {
1003 return Err(Error::InvalidVectorShape);
1004 }
1005 Ok(())
1006}
1007
1008fn require_workspace_bytes(actual: usize, required: usize) -> Result<()> {
1009 if actual < required {
1010 return Err(Error::InsufficientWorkspaceSize { required, actual });
1011 }
1012 Ok(())
1013}
1014
1015unsafe fn copy_residual_history<T: Copy>(
1016 history: *const T,
1017 leading_dimension: usize,
1018 valid_rows: usize,
1019) -> ResidualHistory<T> {
1020 let history = unsafe { slice::from_raw_parts(history, leading_dimension.saturating_mul(2)) };
1021 let mut rows = Vec::with_capacity(valid_rows);
1022 for row in 0..valid_rows {
1023 rows.push(ResidualHistoryEntry {
1024 total_iterations: history[row],
1025 residual_norm: history[row + leading_dimension],
1026 });
1027 }
1028 ResidualHistory {
1029 rows,
1030 leading_dimension,
1031 }
1032}
1033
1034fn validate_matrix(rows: usize, cols: usize, len: usize, lda: usize) -> Result<()> {
1035 if rows == 0 || cols == 0 {
1036 return Err(Error::InvalidMatrixShape);
1037 }
1038 if lda < rows {
1039 return Err(Error::InvalidLeadingDimension);
1040 }
1041 let required = lda.checked_mul(cols).ok_or(Error::InvalidMatrixShape)?;
1042 if len < required {
1043 return Err(Error::InvalidMatrixShape);
1044 }
1045 Ok(())
1046}
1047
1048#[cfg(all(test, feature = "testing"))]
1049mod tests {
1050 use singe_cuda::memory::DeviceMemory;
1051
1052 use super::*;
1053 use crate::testing::setup_context_if_available;
1054
1055 #[test]
1056 fn test_xgesv_solves_diagonal_system() -> Result<()> {
1057 let Some(ctx) = setup_context_if_available()? else {
1058 return Ok(());
1059 };
1060 let mut params = IrsParams::create()?;
1061 let infos = IrsInfos::create()?;
1062
1063 let mut a = DeviceMemory::from_slice(&[
1064 2.0_f32, 0.0, //
1065 0.0, 4.0,
1066 ])?;
1067 let b = DeviceMemory::from_slice(&[
1068 6.0_f32, //
1069 8.0,
1070 ])?;
1071 let mut x = DeviceMemory::create(2)?;
1072 let workspace_bytes = xgesv_buffer_size::<f32>(&ctx, &mut params, 2, 1)?;
1073 let mut workspace = DeviceMemory::create(workspace_bytes.max(1))?;
1074 let mut dev_info = DeviceMemory::create(1)?;
1075
1076 let _ = xgesv(
1077 &ctx,
1078 &mut params,
1079 &infos,
1080 2,
1081 1,
1082 MatrixMut::new(&mut a, 2),
1083 MatrixRef::new(&b, 2),
1084 MatrixMut::new(&mut x, 2),
1085 &mut workspace,
1086 &mut dev_info,
1087 )?;
1088
1089 assert_eq!(dev_info.copy_to_host_vec()?, vec![0]);
1090 assert_eq!(x.copy_to_host_vec()?, vec![3.0, 2.0]);
1091 Ok(())
1092 }
1093}