ml-models / Mathematical foundations / Iterative solvers: conjugate gradient and Krylov methods advanced

Iterative solvers: conjugate gradient and Krylov methods

LU and Cholesky solve a linear system exactly, but they need the whole matrix and n³ work. When the matrix is huge, sparse, or never written down at all, as with the Hessian of a neural network, the tool is an iterative solver that only ever multiplies the matrix by a vector. This page derives conjugate gradient from steepest descent, runs both on a 2 × 2 and a 40 × 40 system, shows what a preconditioner buys, and follows the same ideas into scikit-learn's ridge and Newton-CG solvers and PyTorch's lobpcg and Hessian-vector products.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Matrix-free: only A·v
  5. Steepest descent
  6. Conjugate gradient
  7. Preconditioning
  8. The Krylov family
  9. Where they appear in ML
  10. Worked example
  11. Two steps of CG by hand
  12. A badly scaled 40 × 40 system
  13. Implementation
  14. Tradeoffs
  15. Check yourself
  16. Connections
  17. Read first
  18. Read next
  19. References

Problem

The direct methods factor A once and then solve by substitution. For an n × n dense matrix that costs about n³/3 operations and n² numbers of memory. At n = 10⁵ that is 3 × 10¹⁴ operations and 80 GB in float64, and machine learning meets far bigger systems: the Hessian of a model with 10⁸ parameters has 10¹⁶ entries, so it can never be stored, let alone factored. Yet second-order optimizers, Gaussian processes, ridge regression on sparse text features and spectral methods all need to solve Ax = b or find a few eigenvectors of A.

What these matrices do allow is cheap products. A sparse matrix with a few nonzeros per row multiplies a vector in O(nnz) time. XᵀX + αI multiplies a vector as Xᵀ(Xv) + αv without ever being formed. And automatic differentiation computes a Hessian-vector product Hv for about the cost of two gradients. An iterative solver is an algorithm that needs nothing else.

Intuition

For a symmetric positive-definite A, solving Ax = b is the same as minimizing the quadratic bowl f(x) = ½xᵀAx − bᵀx, because its gradient is Ax − b. The residual r = b − Ax is the negative gradient, so the obvious method is gradient descent with the best step length along r. That is steepest descent, and on a stretched bowl it zig-zags: each exact line search ends where the new gradient is perpendicular to the old one, so it keeps undoing part of its previous progress.

Conjugate gradient (CG) fixes this by choosing each new direction so that minimizing along it never spoils the minimization already done along the earlier ones. The directions are not perpendicular but A-conjugate, piᵀApj = 0: perpendicular after the bowl is stretched back into a round one. In n dimensions there are only n such directions, so CG reaches the exact answer in at most n steps, and usually is close long before.

Steepest descent and conjugate gradient on the same 2 × 2 system Two copies of the elliptical contours of f(x) = ½xᵀAx − bᵀx for A = [[3, 2], [2, 6]] and b = [2, -8], centred on the solution [2, -2]. Both methods start at [-2, -2]. On the left steepest descent zig-zags with each step at a right angle to the previous one and needs 10 steps to get within 0.1% of the answer. On the right conjugate gradient takes the same first step, then a second step in a conjugate direction that lands exactly on the solution. Steepest descent: 10 steps to 0.1% start x* Conjugate gradient: 2 steps, exact start x* contours of f; the long axis follows the eigenvector of λ = 2, the short one λ = 7; κ = 3.5

Both runs computed by steepest_descent and conjugate_gradient in the page's helpers. The first step is identical, since CG's first direction is the residual. After that, steepest descent turns 90° every step, while CG's second direction p₂ satisfies p₁ᵀAp₂ = 9e-16, zero up to rounding.

Mechanics

Matrix-free: only A·v

Every method on this page touches A through one operation, v ↦ Av, plus a handful of vector additions and dot products. Each iteration costs one product and O(n) extra work, and the memory is a few vectors of length n. Libraries make this explicit: SciPy's LinearOperator, which scikit-learn wraps around XᵀX + αI, and a closure over autograd for a Hessian. How fast the method converges depends on A's eigenvalues, measured by the condition number κ = λmax/λmin.

Steepest descent

From xk with residual rk = b − Axk, step along rk by the α that minimizes f on that line: αk = rkᵀrk / rkᵀArk. Then xk+1 = xk + αkrk and rk+1 = rk − αkArk, reusing the one product. The exact line search makes consecutive residuals orthogonal, which is the zig-zag. The error, measured in the A-norm ‖e‖A = √(eᵀAe), shrinks by at least a factor (κ − 1)/(κ + 1) per step. At κ = 100 that is 0.98: about 115 steps per factor of 10.

Conjugate gradient

CG (Hestenes and Stiefel, 1952) keeps the residual but builds each search direction from it and the previous direction:

  1. r₀ = b − Ax₀, p₀ = r₀.
  2. αk = rkᵀrk / pkᵀApk; xk+1 = xk + αkpk; rk+1 = rk − αkApk.
  3. βk = rk+1ᵀrk+1 / rkᵀrk; pk+1 = rk+1 + βkpk.

That short recurrence is enough to make every new direction A-conjugate to all earlier ones, not just the last, because A is symmetric. Two consequences follow. After k steps, xk is the point that minimizes ‖x − x*‖A over everything reachable from x₀ in the Krylov subspace span{r₀, Ar₀, A²r₀, …, Ak−1r₀}, the best answer any method could build from k products. And in exact arithmetic CG terminates in at most n steps, or in as many steps as A has distinct eigenvalues. The general error bound is

‖xk − x*‖A ≤ 2 ((√κ − 1)/(√κ + 1))k ‖x₀ − x*‖A,

so the number of steps grows with √κ instead of κ: at κ = 100 the factor is 9/11 ≈ 0.82, about 12 steps per factor of 10. In floating point the conjugacy slowly decays and CG can need more than n steps, but it still converges, and in practice it is stopped long before n.

Preconditioning

If κ is the problem, change the system. With a matrix M ≈ A that is cheap to invert, CG on M−1A converges at the rate set by κ(M−1A). The algorithm changes in one line: each new residual is passed through z = M−1r before it is used to build the direction. The simplest choice is Jacobi, M = diag(A), which fixes badly scaled variables for free. Stronger ones are incomplete Cholesky (Cholesky that keeps A's sparsity pattern), multigrid, and in optimization the diagonal or Kronecker-factored curvature estimates that Adam, Shampoo and K-FAC maintain. A good preconditioner is the main lever: it can turn thousands of iterations into tens.

The Krylov family

CG is one member of a family that all search the same Krylov subspace and differ in what matrix they accept and what they minimize:

  • MINRES minimizes ‖r‖ for symmetric matrices that are indefinite, where CG's bowl has no bottom. GMRES does the same for any square matrix, at the price of storing every direction.
  • CGLS and LSQR solve least squares min ‖Ax − b‖ as CG on AᵀA without forming it or squaring the condition number in the arithmetic; LSQR's damp argument adds a ridge penalty.
  • Lanczos runs the same three-term recurrence to build an orthonormal basis in which A is tridiagonal; the eigenvalues of that small tridiagonal matrix converge to A's extreme eigenvalues within a few dozen products. It is CG's twin: CG's α and β are the entries of that tridiagonal matrix.
  • LOBPCG (Knyazev, 2001) finds a block of extreme eigenpairs by minimizing the Rayleigh quotient xᵀAx/xᵀx over the current vectors, their residuals and the previous vectors, accepts a preconditioner, and is what torch.lobpcg implements.

Where they appear in ML

  • Ridge regression. scikit-learn's Ridge(solver="sparse_cg") wraps the product v ↦ Xᵀ(Xv) + αv in a LinearOperator and hands it to SciPy's CG (_solve_sparse_cg); with more features than samples it switches to the n_samples × n_samples kernel form XXᵀ + αI. solver="lsqr" calls LSQR with damp = √α (_solve_lsqr). Both keep a sparse X sparse.
  • Newton-CG. LogisticRegression(solver="newton-cg") computes each Newton step by solving H s = −g approximately with an inner CG over Hessian-vector products (_cg), stopping when the residual's 1-norm drops below min(0.5, √‖g‖₁)·‖g‖₁ and bailing out on zero or negative curvature, then line-searches along s (_newton_cg). This is truncated Newton: loose solves far from the optimum, tighter ones near it.
  • Hessian-free optimization. The same idea for neural networks (Martens, 2010): Hessian- or Gauss-Newton-vector products from autograd, a damped CG inner loop, no Hessian ever stored. PyTorch exposes the product as autograd.functional.hvp and its transpose vhp. Influence functions and implicit differentiation solve Hx = v the same way.
  • Gaussian processes. Exact GP inference needs (K + σ²I)⁻¹y and log det. GPyTorch replaces the Cholesky with batched, preconditioned CG plus Lanczos for the log-determinant, which turns O(n³) into products a GPU does well (Gardner et al., 2018).
  • Spectral methods. Spectral clustering, Laplacian eigenmaps and the top Hessian eigenvalues used to study sharpness all need only a few extreme eigenpairs: torch.lobpcg computes the k largest or smallest of a dense or sparse symmetric matrix.
ComponentShape/ParamsMeaningSource
_cg fhess_p callable, fgrad [n], maxiter, tol CG on H x = −g using only Hessian-vector products; stops on sum |r| ≤ tol or curvature ≤ 16·eps·‖p‖² (capped CG), falling back to the steepest-descent step if the first direction has negative curvature. optimize.py
_newton_cg grad_hess, func, grad, x0, tol=1e-4, maxiter=100, maxinner=200 Truncated Newton: an inner _cg solve with tolerance min(0.5, √‖g‖₁)·‖g‖₁, then a Wolfe line search. optimize.py
_solve_sparse_cg X [n_samples, n_features], alpha, tol=1e-4 Ridge by CG on (XᵀX + αI)w = Xᵀy, or on the kernel form when n_features > n_samples; X is never densified. _ridge.py
_solve_lsqr X, alpha, damp = √α, atol = btol = tol Ridge as damped least squares with SciPy's LSQR; returns the iteration counts too. _ridge.py
torch.lobpcg A [*, m, m], k, B, X [*, m, n], iK (preconditioner), largest=True, method="ortho" k extreme eigenpairs of a symmetric (generalized) eigenproblem; needs m ≥ 3n; default tol = √(float eps), niter = 1000. _lobpcg.py
autograd.functional.hvp func, inputs, v → (func(inputs), H·v) Hessian-vector product by double backward, without forming H; the docstring notes vhp is faster and equal for twice-differentiable functions. functional.py
Relative residual against iterations on the 40 × 40 system A log-scale plot of ‖r‖/‖r₀‖ over the first 60 iterations for the 40 × 40 example with κ ≈ 178. Steepest descent falls slowly and is still far from 10⁻⁸ at iteration 60; it needs 1,460 steps. Conjugate gradient reaches 10⁻⁸ in 30 steps, and Jacobi-preconditioned CG, with κ ≈ 5.0, in 20. 1e1 1e-1 1e-3 1e-5 1e-7 1e-9 0 10 20 30 40 50 60 iteration (one matrix-vector product each) steepest descent: 1,460 steps CG: 30 steps Jacobi-preconditioned CG: 20 steps

The dashed line is the 10⁻⁸ target. CG's residual is not monotone, since CG minimizes the error in the A-norm, not ‖r‖; it can rise for a few steps and still be making progress.

Worked example

Two steps of CG by hand

Solve Ax = b with A = [[3, 2], [2, 6]], b = [2, -8], from x₀ = [-2, -2]. A's eigenvalues are 2 and 7, so κ = 3.5.

  1. r₀ = b − Ax₀ = [2, -8] − [−10, −16] = [12, 8], and p₀ = r₀.
  2. Ap₀ = [52, 72], r₀ᵀr₀ = 208 and p₀ᵀAp₀ = 12·52 + 8·72 = 1,200, so α₀ = 208/1,200 = 0.1733, giving x₁ = [0.08, -0.6133]. Steepest descent takes exactly this step too.
  3. r₁ = r₀ − α₀Ap₀ = [12 − 9.013, 8 − 12.48] ≈ [2.987, −4.48], so β₀ = r₁ᵀr₁ / r₀ᵀr₀ ≈ 28.99/208 ≈ 0.139 and p₁ = r₁ + β₀p₀ ≈ [4.659, −3.365].
  4. α₁ = r₁ᵀr₁ / p₁ᵀAp₁ ≈ 28.99/70.35 = 0.4121 gives x₂ = [2, -2]: the exact solution, after two products with A, as CG promises for a 2 × 2 system.

Steepest descent from the same start needs 10 steps to bring ‖r‖ below 0.1% of ‖r₀‖. Its rate bound here is (κ − 1)/(κ + 1) = 0.5556 per step.

A badly scaled 40 × 40 system

Take the tridiagonal matrix T with 3 on the diagonal and −1 beside it, and rescale its rows and columns by s = 1, 8, 4, 11, 7, 3, 10, 6, … (sᵢ = 1 + (7i mod 11)), so Aij = siTijsj. This is what unnormalized features do to XᵀX. Solve Ax = 1 from x₀ = 0 to a relative residual of 10⁻⁸:

Methodκ that governs itBound on stepsActual steps
Steepest descent177.71,6371,460
Conjugate gradient177.7128 (and ≤ 40 in exact arithmetic)30
CG with Jacobi, M = diag(A)4.962020
LU, for comparison——one factorization, ≈ 40³/3 ≈ 21,000 multiply-adds

Steepest descent lands close to its κ bound. CG beats its √κ bound by a factor of four, because the bound assumes the worst spread of eigenvalues and this matrix's are clustered. Jacobi scaling undoes the si exactly, leaving D−½AD−½ with κ ≈ 4.96, essentially T's own, and CG then needs 20 products of a matrix with three nonzeros per row. The preconditioned answer agrees with LU's to 4e-10.

Implementation

def conjugate_gradient(matvec, b, x, tol=1e-8, max_steps=1000, inv_diag=None):
    dot = lambda u, v: sum(p * q for p, q in zip(u, v))
    r = [bi - ai for bi, ai in zip(b, matvec(x))]
    z = r if inv_diag is None else [m * ri for m, ri in zip(inv_diag, r)]
    p, rz, r0 = list(z), dot(r, z), dot(r, r) ** 0.5
    for _ in range(max_steps):
        if dot(r, r) ** 0.5 <= tol * r0:
            break
        ap = matvec(p)                      # the only use of A
        alpha = rz / dot(p, ap)
        x = [xi + alpha * pi for xi, pi in zip(x, p)]
        r = [ri - alpha * api for ri, api in zip(r, ap)]
        z = r if inv_diag is None else [m * ri for m, ri in zip(inv_diag, r)]
        rz, rz_old = dot(r, z), rz
        p = [zi + (rz / rz_old) * pi for zi, pi in zip(z, p)]
    return x

A = [[3., 2.], [2., 6.]]
conjugate_gradient(lambda v: [sum(a * x for a, x in zip(row, v)) for row in A], [2., -8.], [-2., -2.])
# [2.0, -2.0] after two steps

Matrix-free with PyTorch: a Newton step on a loss, using Hessian-vector products from autograd.

import torch
from torch.autograd.functional import hvp

def loss(w):
    return torch.nn.functional.softplus(-y * (X @ w)).mean() + 1e-3 * w.square().sum()

w = torch.zeros(X.shape[1], dtype=torch.float64)
g = torch.func.grad(loss)(w)
step = conjugate_gradient(lambda v: hvp(loss, w, torch.tensor(v, dtype=w.dtype))[1].tolist(),
                          (-g).tolist(), [0.0] * len(w), tol=1e-4)

# extreme eigenpairs of a large symmetric matrix, dense or sparse
E, V = torch.lobpcg(S, k=4, largest=True)    # S: [m, m] with m >= 3 * 4

# the same solvers in scikit-learn
from sklearn.linear_model import LogisticRegression, Ridge
Ridge(alpha=1.0, solver="sparse_cg").fit(X_sparse, y)    # CG on X^T X + alpha I, never formed
LogisticRegression(solver="newton-cg").fit(X, y)         # inner CG on Hessian-vector products

Tradeoffs

  • Direct or iterative. Below a few thousand unknowns with a dense matrix, a Cholesky or LU factorization is faster, exact and reusable for many right-hand sides. Iterative methods win when the matrix is sparse, structured or implicit, when an approximate answer is enough, or when memory rules out n².
  • Convergence depends on the spectrum. CG is fast when κ is small or the eigenvalues are clustered, and slow otherwise; there is no fixed cost. Budget for a preconditioner before tuning anything else.
  • Symmetric positive-definite only. CG can fail silently on an indefinite or non-symmetric A: pᵀAp ≤ 0 breaks its step. Use MINRES or GMRES there, and in optimization treat negative curvature as a signal, as scikit-learn's capped CG does.
  • Floating point. Conjugacy decays with rounding, so CG may need more than n steps and Lanczos produces spurious copies of converged eigenvalues unless it re-orthogonalizes. Low precision makes both worse; these solvers usually run in float32 or float64.
  • Stopping rules. The residual is not the error: a small ‖r‖ can hide a large error when κ is large. Inside an outer optimizer, solve loosely early and tightly late (truncated Newton), since accuracy beyond the outer step's needs is wasted.

Check yourself

A 10⁶ × 10⁶ sparse SPD matrix has 5 nonzeros per row and κ = 10⁴. Roughly how much work is CG to 10⁻⁶?

The √κ bound gives about ½·100·ln(2·10⁶) ≈ 730 iterations, each one product of 5·10⁶ multiply-adds plus a few vector operations, so a few times 10⁹ operations in all. A dense Cholesky would be 3·10¹⁷ and would not fit in memory.

Why does CG solve any 2 × 2 SPD system in at most two steps, while steepest descent may need many?

CG's two directions are A-conjugate, so minimizing along the second never undoes the first: after two steps it has minimized f over the whole plane. Steepest descent only ever minimizes along one direction at a time, and on a stretched bowl each step partly undoes the last.

Newton-CG on a non-convex loss hits pᵀHp < 0 inside its inner loop. What should it do?

Stop the inner CG: the quadratic model has no minimum along p. Return the step built so far, or, at the first iteration, the steepest-descent direction, which is what scikit-learn's _cg does. Trust-region variants instead move along p to the boundary.

A design matrix's columns are on very different scales. Which preconditioner is free, and what does it correspond to in data terms?

Jacobi, dividing by the diagonal of XᵀX, which is each column's squared norm. It is the solver-side version of standardizing the features.

Connections

Read first

Read next

No page builds on this one yet.

References

  • Hestenes and Stiefel. Methods of Conjugate Gradients for Solving Linear Systems. Journal of Research of the National Bureau of Standards 49(6), 409–436, 1952.
  • Shewchuk. An Introduction to the Conjugate Gradient Method Without the Agonizing Pain. Carnegie Mellon University technical report CMU-CS-94-125, 1994. The 2 × 2 example above is its running example.
  • Saad. Iterative Methods for Sparse Linear Systems, 2nd ed. SIAM, 2003: chapters 6 (Krylov subspaces, CG, GMRES) and 9–10 (preconditioning).
  • Paige and Saunders. LSQR: An Algorithm for Sparse Linear Equations and Sparse Least Squares. ACM Transactions on Mathematical Software 8(1), 43–71, 1982.
  • Knyazev. Toward the Optimal Preconditioned Eigensolver: Locally Optimal Block Preconditioned Conjugate Gradient Method. SIAM Journal on Scientific Computing 23(2), 517–541, 2001.
  • Pearlmutter. Fast Exact Multiplication by the Hessian. Neural Computation 6(1), 147–160, 1994.
  • Martens. Deep Learning via Hessian-Free Optimization. ICML, 2010.
  • Gardner, Pleiss, Bindel, Weinberger and Wilson. GPyTorch: Blackbox Matrix-Matrix Gaussian Process Inference with GPU Acceleration. NeurIPS, 2018.
  • Royer, O'Neill and Wright. A Newton-CG Algorithm with Complexity Guarantees for Smooth Unconstrained Optimization. 2018 (arXiv 1803.02924), the capped CG cited in scikit-learn's _cg.
  • scikit-learn 1.9.1: _cg, _newton_cg, _solve_sparse_cg, _solve_lsqr.
  • PyTorch v2.15.0: torch.lobpcg, _lobpcg, autograd.functional.hvp, vhp.