Distributed SVD#
DistributedSvd computes a thin singular value decomposition of a
two-dimensional matrix distributed by rows across MPI ranks. It uses NumPy for
local QR and SVD operations and mpi4py for communication. The global input
matrix is never gathered on any rank.
API#
- class romtools.linalg.DistributedSvd(comm=None)[source]
Bases:
objectNumPy-compatible thin SVD callable for a row-distributed matrix.
Bind an MPI communicator once, then pass this object anywhere a callable compatible with
numpy.linalg.svd()is expected, includingromtools.vector_space.VectorSpaceFromPOD.- Parameters:
comm (MPI_Comm) – Communicator over which matrix rows are distributed. If
None, supported calls delegate to NumPy in serial.
Example
>>> distributed_svd = DistributedSvd(comm) >>> U_local, s, Vh = distributed_svd( ... A_local, full_matrices=False, compute_uv=True, hermitian=False ... )
- __call__(a, full_matrices=True, compute_uv=True, hermitian=False)[source]
Compute a thin SVD collectively over the bound communicator.
- Parameters:
a (np.ndarray) – The calling rank’s local matrix rows.
full_matrices (bool) – Must be
Falsewhencompute_uv=True.compute_uv (bool) – Return singular vectors when
True; otherwise return only singular values.hermitian (bool) – Must be
False.
- Returns:
(U_local, s, Vh)whencompute_uv=True; otherwises.
Result distribution#
For a global matrix \(A \in \mathbb{C}^{m \times n}\), let
\(k = \min(m,n)\). Each rank supplies its local rows A_local and calls
the operation collectively. The result has the following distribution:
U_localhas shape(A_local.shape[0], k)and the same row distribution as the input.shas shape(k,)and is replicated on all ranks.Vhhas shape(k, n)and is replicated on all ranks.
Uneven partitions and ranks with zero local rows are supported. Real and complex numeric dtypes, wide matrices, tall matrices, rank-deficient matrices, and globally empty dimensions are supported. Input arrays and the communicator are not modified.
Supported NumPy options#
The callable has the same argument names and defaults as numpy.linalg.svd.
The following behavior is intentional:
full_matrices=False, compute_uv=True, hermitian=Falsereturns(U_local, s, Vh).compute_uv=False, hermitian=Falsereturns only the replicated singular values. As in NumPy,full_matricesdoes not affect this result.full_matrices=Truewithcompute_uv=TrueraisesNotImplementedErrorbecause constructing the global full left null space is not supported.hermitian=TrueraisesNotImplementedError.Inputs must be two-dimensional, and every rank must use the same column count, dtype, and options. Invalid distributed metadata raises
ValueErrorcollectively.
Algorithm#
The implementation uses a two-level Tall-Skinny QR (TSQR) factorization:
Each rank computes
A_local = Q_local @ R_local.Rank zero gathers only the local
R_localfactors and performs a second reduced QR factorization of their vertical stack.Rank zero computes the SVD of the final reduced factor.
The singular values and right singular vectors are broadcast, while the reduced left transformations are scattered and multiplied by each
Q_local.
This avoids forming \(A^H A\), which would square the condition number.
MPI example#
The following program can be run with mpiexec -n 4 python distributed_pod.py:
import numpy as np
from mpi4py import MPI
from romtools.linalg import DistributedSvd
from romtools.vector_space import VectorSpaceFromPOD
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
# Every rank owns a potentially different number of matrix rows.
local_rows = rank + 2
rng = np.random.default_rng(1000 + rank)
A_local = rng.normal(size=(local_rows, 6))
distributed_svd = DistributedSvd(comm)
U_local, s, Vh = distributed_svd(
A_local,
full_matrices=False,
compute_uv=True,
hermitian=False,
)
# VectorSpaceFromPOD accepts the same communicator-bound callable. With one
# variable, the tensor's spatial axis is distributed across ranks.
local_snapshots = A_local.reshape(1, local_rows, 6)
pod_space = VectorSpaceFromPOD(
local_snapshots,
svdFnc=distributed_svd,
)
local_basis = pod_space.get_basis()