nvmath-python 1.0.0__cp314-cp314t-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nvmath/__init__.pxd +0 -0
- nvmath/__init__.py +45 -0
- nvmath/_internal/__init__.py +0 -0
- nvmath/_internal/attribute_ifc_factory.py +330 -0
- nvmath/_internal/layout.py +70 -0
- nvmath/_internal/templates.py +130 -0
- nvmath/_internal/threadsafe.py +106 -0
- nvmath/_internal/utils.py +43 -0
- nvmath/_internal/workspace.py +490 -0
- nvmath/_utils.py +147 -0
- nvmath/bindings/__init__.py +60 -0
- nvmath/bindings/_internal/__init__.pxd +0 -0
- nvmath/bindings/_internal/__init__.py +0 -0
- nvmath/bindings/_internal/common_types.pxd +31 -0
- nvmath/bindings/_internal/cublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublas.pxd +530 -0
- nvmath/bindings/_internal/cublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublasLt.pxd +59 -0
- nvmath/bindings/_internal/cublasMp.pxd +52 -0
- nvmath/bindings/_internal/cudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cudss.pxd +54 -0
- nvmath/bindings/_internal/cufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cufft.pxd +70 -0
- nvmath/bindings/_internal/cufftMp.pxd +77 -0
- nvmath/bindings/_internal/curand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/curand.pxd +42 -0
- nvmath/bindings/_internal/cusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolver.pxd +15 -0
- nvmath/bindings/_internal/cusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverDn.pxd +406 -0
- nvmath/bindings/_internal/cusolverMp.pxd +71 -0
- nvmath/bindings/_internal/cusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverSp.pxd +75 -0
- nvmath/bindings/_internal/cusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparse.pxd +471 -0
- nvmath/bindings/_internal/cusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparseLt.pxd +48 -0
- nvmath/bindings/_internal/cutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cutensor.pxd +58 -0
- nvmath/bindings/_internal/mathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/mathdx.pxd +116 -0
- nvmath/bindings/_internal/nvshmem.pxd +29 -0
- nvmath/bindings/_internal/utils.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/utils.pxd +174 -0
- nvmath/bindings/_internal/utils.pyi +10 -0
- nvmath/bindings/cublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cublas.pxd +558 -0
- nvmath/bindings/cublas.pyi +812 -0
- nvmath/bindings/cublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cublasLt.pxd +109 -0
- nvmath/bindings/cublasLt.pyi +1461 -0
- nvmath/bindings/cublasMp.pxd +85 -0
- nvmath/bindings/cublasMp.pyi +267 -0
- nvmath/bindings/cudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cudss.pxd +98 -0
- nvmath/bindings/cudss.pyi +443 -0
- nvmath/bindings/cufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cufft.pxd +118 -0
- nvmath/bindings/cufft.pyi +301 -0
- nvmath/bindings/cufftMp.pxd +124 -0
- nvmath/bindings/cufftMp.pyi +326 -0
- nvmath/bindings/curand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/curand.pxd +71 -0
- nvmath/bindings/curand.pyi +189 -0
- nvmath/bindings/cusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolver.pxd +62 -0
- nvmath/bindings/cusolver.pyi +320 -0
- nvmath/bindings/cusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverDn.pxd +430 -0
- nvmath/bindings/cusolverDn.pyi +422 -0
- nvmath/bindings/cusolverMp.pxd +98 -0
- nvmath/bindings/cusolverMp.pyi +114 -0
- nvmath/bindings/cusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverSp.pxd +95 -0
- nvmath/bindings/cusolverSp.pyi +70 -0
- nvmath/bindings/cusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusparse.pxd +546 -0
- nvmath/bindings/cusparse.pyi +1017 -0
- nvmath/bindings/cusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusparseLt.pxd +99 -0
- nvmath/bindings/cusparseLt.pyi +252 -0
- nvmath/bindings/cutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cutensor.pxd +98 -0
- nvmath/bindings/cutensor.pyi +324 -0
- nvmath/bindings/cycublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycublas.pxd +664 -0
- nvmath/bindings/cycublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycublasLt.pxd +1045 -0
- nvmath/bindings/cycublasMp.pxd +171 -0
- nvmath/bindings/cycudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycudss.pxd +277 -0
- nvmath/bindings/cycufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycufft.pxd +333 -0
- nvmath/bindings/cycufftMp.pxd +342 -0
- nvmath/bindings/cycurand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycurand.pxd +141 -0
- nvmath/bindings/cycusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolver.pxd +137 -0
- nvmath/bindings/cycusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverDn.pxd +443 -0
- nvmath/bindings/cycusolverMp.pxd +107 -0
- nvmath/bindings/cycusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverSp.pxd +93 -0
- nvmath/bindings/cycusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparse.pxd +679 -0
- nvmath/bindings/cycusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparseLt.pxd +135 -0
- nvmath/bindings/cycutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycutensor.pxd +189 -0
- nvmath/bindings/cymathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cymathdx.pxd +552 -0
- nvmath/bindings/cynvshmem.pxd +118 -0
- nvmath/bindings/mathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/mathdx.pxd +182 -0
- nvmath/bindings/mathdx.pyi +1562 -0
- nvmath/bindings/nvpl/__init__.pxd +0 -0
- nvmath/bindings/nvpl/__init__.py +13 -0
- nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
- nvmath/bindings/nvpl/_internal/__init__.py +0 -0
- nvmath/bindings/nvpl/_internal/blas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/_internal/blas.pxd +237 -0
- nvmath/bindings/nvpl/_internal/fft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
- nvmath/bindings/nvpl/blas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/blas.pxd +131 -0
- nvmath/bindings/nvpl/blas.pyi +168 -0
- nvmath/bindings/nvpl/cyblas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/cyblas.pxd +280 -0
- nvmath/bindings/nvpl/cyfft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/cyfft.pxd +93 -0
- nvmath/bindings/nvpl/fft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/fft.pxd +100 -0
- nvmath/bindings/nvpl/fft.pyi +168 -0
- nvmath/bindings/nvshmem.pxd +54 -0
- nvmath/bindings/nvshmem.pyi +191 -0
- nvmath/device/__init__.py +38 -0
- nvmath/device/_deprecated.py +33 -0
- nvmath/device/common.py +315 -0
- nvmath/device/common_backend.py +131 -0
- nvmath/device/common_cuda.py +201 -0
- nvmath/device/common_numba.py +300 -0
- nvmath/device/common_numba_cuda_mlir.py +202 -0
- nvmath/device/common_opaque_tensor.py +201 -0
- nvmath/device/cublasdx.py +1606 -0
- nvmath/device/cublasdx_backend.py +860 -0
- nvmath/device/cublasdx_numba.py +1534 -0
- nvmath/device/cublasdx_numba_cuda_mlir.py +208 -0
- nvmath/device/cufftdx.py +373 -0
- nvmath/device/cufftdx_backend.py +220 -0
- nvmath/device/cufftdx_numba.py +140 -0
- nvmath/device/cufftdx_numba_cuda_mlir.py +79 -0
- nvmath/device/curand_kernel.py +9147 -0
- nvmath/device/cusolverdx.py +2708 -0
- nvmath/device/cusolverdx_backend.py +440 -0
- nvmath/device/cusolverdx_numba.py +567 -0
- nvmath/device/cusolverdx_numba_cuda_mlir.py +604 -0
- nvmath/device/cusolverdx_overload_backend.py +1029 -0
- nvmath/device/llvm_array.py +29 -0
- nvmath/device/random.py +441 -0
- nvmath/device/random_helpers.py +23 -0
- nvmath/device/random_states.py +187 -0
- nvmath/device/types.py +138 -0
- nvmath/device/vector_types_numba.py +259 -0
- nvmath/distributed/__init__.py +200 -0
- nvmath/distributed/_internal/__init__.py +0 -0
- nvmath/distributed/_internal/nccl.py +86 -0
- nvmath/distributed/_internal/nvshmem.py +307 -0
- nvmath/distributed/_internal/symmetric_memory.py +35 -0
- nvmath/distributed/_internal/tensor_ifc.py +70 -0
- nvmath/distributed/_internal/tensor_ifc_cupy.py +68 -0
- nvmath/distributed/_internal/tensor_ifc_host_device.py +172 -0
- nvmath/distributed/_internal/tensor_ifc_numpy.py +46 -0
- nvmath/distributed/_internal/tensor_ifc_torch.py +162 -0
- nvmath/distributed/_internal/tensor_wrapper.py +81 -0
- nvmath/distributed/_utils.py +167 -0
- nvmath/distributed/distribution/__init__.py +30 -0
- nvmath/distributed/distribution/_configuration.py +39 -0
- nvmath/distributed/distribution/distributions.py +1024 -0
- nvmath/distributed/distribution/redistribute.py +1284 -0
- nvmath/distributed/fft/__init__.py +7 -0
- nvmath/distributed/fft/_configuration.py +82 -0
- nvmath/distributed/fft/fft.py +2742 -0
- nvmath/distributed/linalg/__init__.py +22 -0
- nvmath/distributed/linalg/_internal/__init__.py +3 -0
- nvmath/distributed/linalg/_internal/epilog_protocol.py +586 -0
- nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
- nvmath/distributed/linalg/advanced/__init__.py +8 -0
- nvmath/distributed/linalg/advanced/_configuration.py +171 -0
- nvmath/distributed/linalg/advanced/matmulmod.py +3573 -0
- nvmath/distributed/linalg/generic/__init__.py +8 -0
- nvmath/distributed/linalg/generic/_caching.py +66 -0
- nvmath/distributed/linalg/generic/_configuration.py +61 -0
- nvmath/distributed/linalg/generic/_factorization.py +172 -0
- nvmath/distributed/linalg/generic/_initialization.py +966 -0
- nvmath/distributed/linalg/generic/_problem_spec.py +511 -0
- nvmath/distributed/linalg/generic/solvermod.py +1368 -0
- nvmath/distributed/process_group.py +408 -0
- nvmath/fft/__init__.py +7 -0
- nvmath/fft/_configuration.py +189 -0
- nvmath/fft/_exec_utils.py +82 -0
- nvmath/fft/_helpers.py +237 -0
- nvmath/fft/fft.py +3122 -0
- nvmath/internal/__init__.pxd +3 -0
- nvmath/internal/__init__.py +10 -0
- nvmath/internal/_bindings.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/_bindings.pxd +18 -0
- nvmath/internal/_device_utils.py +45 -0
- nvmath/internal/_layout/__init__.pxd +3 -0
- nvmath/internal/_layout/__init__.py +7 -0
- nvmath/internal/_layout/_layout.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/_layout/_layout.pxd +1303 -0
- nvmath/internal/_layout/_layout.pyi +1145 -0
- nvmath/internal/enum_utils.py +142 -0
- nvmath/internal/formatters.py +87 -0
- nvmath/internal/mem_limit.py +51 -0
- nvmath/internal/memory.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/memory.pxd +13 -0
- nvmath/internal/memory.pyi +50 -0
- nvmath/internal/ndbuffer/__init__.pxd +3 -0
- nvmath/internal/ndbuffer/__init__.py +9 -0
- nvmath/internal/ndbuffer/_copy_kernel.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_copy_kernel.pxd +10 -0
- nvmath/internal/ndbuffer/_jit.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_jit.pxd +7 -0
- nvmath/internal/ndbuffer/_ndbuffer.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_ndbuffer.pxd +40 -0
- nvmath/internal/ndbuffer/_ndbuffer.pyi +463 -0
- nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +242 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
- nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
- nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
- nvmath/internal/package_ifc.py +168 -0
- nvmath/internal/package_ifc_cuda.py +57 -0
- nvmath/internal/package_ifc_cupy.py +67 -0
- nvmath/internal/package_ifc_torch.py +69 -0
- nvmath/internal/package_wrapper.py +14 -0
- nvmath/internal/tensor_ifc.py +179 -0
- nvmath/internal/tensor_ifc_cupy.py +234 -0
- nvmath/internal/tensor_ifc_ndbuffer.py +147 -0
- nvmath/internal/tensor_ifc_numpy.py +184 -0
- nvmath/internal/tensor_ifc_torch.py +178 -0
- nvmath/internal/tensor_wrapper.py +160 -0
- nvmath/internal/typemaps.py +113 -0
- nvmath/internal/utils.py +805 -0
- nvmath/linalg/__init__.py +56 -0
- nvmath/linalg/_internal/__init__.py +3 -0
- nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
- nvmath/linalg/_internal/algo_config_ifc.py +43 -0
- nvmath/linalg/_internal/batch.py +234 -0
- nvmath/linalg/_internal/enum_to_tuples.py +64 -0
- nvmath/linalg/_internal/epilog_protocol.py +766 -0
- nvmath/linalg/_internal/layout.py +624 -0
- nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
- nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
- nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
- nvmath/linalg/_internal/solver_utils.py +432 -0
- nvmath/linalg/_internal/typemaps.py +144 -0
- nvmath/linalg/_internal/utils.py +157 -0
- nvmath/linalg/advanced/__init__.py +8 -0
- nvmath/linalg/advanced/_algorithmmod.py +170 -0
- nvmath/linalg/advanced/_configuration.py +351 -0
- nvmath/linalg/advanced/helpers/__init__.py +5 -0
- nvmath/linalg/advanced/helpers/matmul.py +1316 -0
- nvmath/linalg/advanced/matmulmod.py +3734 -0
- nvmath/linalg/generic/__init__.py +53 -0
- nvmath/linalg/generic/_configuration/__init__.py +39 -0
- nvmath/linalg/generic/_configuration/layout.py +263 -0
- nvmath/linalg/generic/_configuration/match.py +734 -0
- nvmath/linalg/generic/_configuration/qualifiers.py +493 -0
- nvmath/linalg/generic/_configuration/solver_configuration.py +59 -0
- nvmath/linalg/generic/_configuration/wrap.py +217 -0
- nvmath/linalg/generic/_dtype.py +15 -0
- nvmath/linalg/generic/matmulmod.py +2094 -0
- nvmath/linalg/generic/solvermod.py +1301 -0
- nvmath/memory.py +279 -0
- nvmath/sparse/__init__.py +38 -0
- nvmath/sparse/_internal/__init__.py +21 -0
- nvmath/sparse/_internal/common_utils.py +147 -0
- nvmath/sparse/_internal/cudss_config_ifc.py +702 -0
- nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
- nvmath/sparse/_internal/cudss_utils.py +506 -0
- nvmath/sparse/_internal/cusparse_utils.py +382 -0
- nvmath/sparse/_internal/sparse_bsc_ifc.py +303 -0
- nvmath/sparse/_internal/sparse_bsr_ifc.py +305 -0
- nvmath/sparse/_internal/sparse_coo_ifc.py +256 -0
- nvmath/sparse/_internal/sparse_csc_ifc.py +268 -0
- nvmath/sparse/_internal/sparse_csr_ifc.py +288 -0
- nvmath/sparse/_internal/sparse_dia_ifc.py +242 -0
- nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
- nvmath/sparse/_internal/sparse_tensor_ifc.py +133 -0
- nvmath/sparse/_internal/sparse_ust_ifc.py +141 -0
- nvmath/sparse/_internal/utils.py +56 -0
- nvmath/sparse/advanced/__init__.py +7 -0
- nvmath/sparse/advanced/_configuration.py +227 -0
- nvmath/sparse/advanced/direct_solver.py +2069 -0
- nvmath/sparse/generic/__init__.py +7 -0
- nvmath/sparse/generic/_configuration.py +129 -0
- nvmath/sparse/generic/_helpers.py +137 -0
- nvmath/sparse/generic/_thunks.py +21 -0
- nvmath/sparse/generic/matmulmod.py +2353 -0
- nvmath/sparse/ust/__init__.py +7 -0
- nvmath/sparse/ust/_converters.py +422 -0
- nvmath/sparse/ust/_cpp.py +28 -0
- nvmath/sparse/ust/_drawer.py +565 -0
- nvmath/sparse/ust/_emitter.py +1033 -0
- nvmath/sparse/ust/_jit.py +188 -0
- nvmath/sparse/ust/_kernel.py +282 -0
- nvmath/sparse/ust/_utils.py +149 -0
- nvmath/sparse/ust/interfaces/__init__.py +0 -0
- nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
- nvmath/sparse/ust/tensor.py +1016 -0
- nvmath/sparse/ust/tensor_format.py +957 -0
- nvmath/tensor/__init__.py +6 -0
- nvmath/tensor/_configuration.py +120 -0
- nvmath/tensor/_internal/__init__.py +3 -0
- nvmath/tensor/_internal/cutensor_config_ifc.py +279 -0
- nvmath/tensor/_internal/cutensor_utils.py +230 -0
- nvmath/tensor/_internal/data.py +43 -0
- nvmath/tensor/_internal/einsum_parser.py +444 -0
- nvmath/tensor/_internal/typemaps.py +96 -0
- nvmath/tensor/contract.py +1900 -0
- nvmath_python-1.0.0.dist-info/METADATA +134 -0
- nvmath_python-1.0.0.dist-info/RECORD +332 -0
- nvmath_python-1.0.0.dist-info/WHEEL +5 -0
- nvmath_python-1.0.0.dist-info/licenses/LICENSE +177 -0
- nvmath_python-1.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__all__ = ["DirectSolver", "InvalidDirectSolverState", "direct_solver"]
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from nvmath._internal.workspace import Workspace
|
|
14
|
+
from nvmath.bindings import (
|
|
15
|
+
cublas, # type: ignore[attr-defined]
|
|
16
|
+
cusolverMp, # type: ignore[attr-defined]
|
|
17
|
+
)
|
|
18
|
+
from nvmath.distributed._internal import tensor_wrapper
|
|
19
|
+
from nvmath.distributed.distribution import Distribution
|
|
20
|
+
from nvmath.internal import utils
|
|
21
|
+
from nvmath.internal.tensor_ifc import AnyTensor, TensorHolder
|
|
22
|
+
from nvmath.linalg._internal.solver_utils import compute_f_strides
|
|
23
|
+
|
|
24
|
+
from ._configuration import DirectSolverOptions
|
|
25
|
+
from ._factorization import _FactorizationState
|
|
26
|
+
from ._initialization import _resolve_runtime_context, _ResolvedProblemSpec, _RuntimeContext, initialize_direct_solver
|
|
27
|
+
|
|
28
|
+
# cuSOLVERMp workflow -> code map
|
|
29
|
+
# (https://docs.nvidia.com/cuda/cusolvermp/getting_started/index.html#workflow)
|
|
30
|
+
#
|
|
31
|
+
# 1. NCCL initialization
|
|
32
|
+
# _resolve_runtime_context() (_initialization.py) -- verifies the NCCL
|
|
33
|
+
# comm (the comm itself is created externally by
|
|
34
|
+
# nvmath.distributed.initialize); called from __init__.
|
|
35
|
+
# 2. cusolverMpCreate (library handle)
|
|
36
|
+
# initialize_direct_solver() (_initialization.py) -- via get_handle()
|
|
37
|
+
# (cached) or a user-supplied options.handle.
|
|
38
|
+
# 3. cusolverMpCreateDeviceGrid
|
|
39
|
+
# _create_device_grids() (_initialization.py).
|
|
40
|
+
# 4. cusolverMpCreateMatrixDesc
|
|
41
|
+
# _create_descriptors() (_initialization.py).
|
|
42
|
+
# 5. query host/device buffer sizes
|
|
43
|
+
# method: plan() -- getrf_buffer_size / getrs_buffer_size.
|
|
44
|
+
# 6. allocate host/device workspace
|
|
45
|
+
# method: factorize() / solve() -- via Workspace.allocate_perhaps.
|
|
46
|
+
# The Workspace objects are created during construction via
|
|
47
|
+
# _create_factorization_state_and_workspaces() (_initialization.py)
|
|
48
|
+
# and sized in plan().
|
|
49
|
+
# 7. execute the routine
|
|
50
|
+
# method: factorize() (cusolverMpGetrf) / solve() (cusolverMpGetrs).
|
|
51
|
+
# 8. synchronize the local stream
|
|
52
|
+
# method: factorize() (sync_and_check_factorize_info) / solve()
|
|
53
|
+
# (stream sync before returning the result).
|
|
54
|
+
# 9. deallocate host/device workspace
|
|
55
|
+
# method: factorize() / solve() (Workspace context-manager exit) and
|
|
56
|
+
# free() -> _release_internal_resources().
|
|
57
|
+
# teardown: cusolverMp{DestroyMatrixDesc, DestroyGrid, Destroy}
|
|
58
|
+
# method: free() -> _release_internal_resources() (the handle
|
|
59
|
+
# is owned by the handle cache, so the instance never destroys it).
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Shared doc fragments for the distributed dense direct linear solver public API.
|
|
63
|
+
GENERIC_DIRECT_SOLVER_DOCUMENTATION: dict = utils.COMMON_SHARED_DOC_MAP.copy()
|
|
64
|
+
GENERIC_DIRECT_SOLVER_DOCUMENTATION.update(
|
|
65
|
+
{
|
|
66
|
+
"a": """\
|
|
67
|
+
A distributed tensor representing the left-hand side operand :math:`a` of the linear system
|
|
68
|
+
:math:`a @ x = b` (see `Semantics`_). The currently
|
|
69
|
+
supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and
|
|
70
|
+
:class:`torch.Tensor`.""".replace("\n", " "),
|
|
71
|
+
#
|
|
72
|
+
"b": """\
|
|
73
|
+
A distributed tensor representing the right-hand side :math:`b` of the linear system
|
|
74
|
+
:math:`a @ x = b` (see `Semantics`_). The currently
|
|
75
|
+
supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and
|
|
76
|
+
:class:`torch.Tensor`.""".replace("\n", " "),
|
|
77
|
+
#
|
|
78
|
+
"distributions": """\
|
|
79
|
+
A sequence ``[distribution_for_a, distribution_for_b]`` specifying how :math:`a` and
|
|
80
|
+
:math:`b` are partitioned across processes. See `Semantics`_ and `Requirements`_ for the
|
|
81
|
+
supported distribution types and the constraints they must satisfy (in particular, the
|
|
82
|
+
row block size of :math:`b` must match that of :math:`a`).""".replace("\n", " "),
|
|
83
|
+
#
|
|
84
|
+
"options": """\
|
|
85
|
+
Specify options for the direct solver as a :class:`DirectSolverOptions` object.
|
|
86
|
+
Alternatively, a `dict` containing the parameters for the ``DirectSolverOptions``
|
|
87
|
+
constructor can also be provided. If not specified, the value will be set to the
|
|
88
|
+
default-constructed ``DirectSolverOptions`` object.""".replace("\n", " "),
|
|
89
|
+
#
|
|
90
|
+
"result": """\
|
|
91
|
+
A distributed tensor representing the solution :math:`x` of the linear system
|
|
92
|
+
:math:`a @ x = b`
|
|
93
|
+
(see `Semantics`_). It has the same distribution,
|
|
94
|
+
local shape, device, and package as the right-hand side ``b``. When the
|
|
95
|
+
:attr:`~nvmath.distributed.linalg.DirectSolverOptions.inplace_b` attribute of
|
|
96
|
+
:class:`DirectSolverOptions` is ``True`` (the default), :math:`x` is written into
|
|
97
|
+
``b`` in place and the returned object is ``b`` itself.""".replace("\n", " "),
|
|
98
|
+
#
|
|
99
|
+
"semantics": """\
|
|
100
|
+
.. _Semantics:
|
|
101
|
+
|
|
102
|
+
The distributed direct solver solves the dense linear system :math:`a @ x = b` for
|
|
103
|
+
:math:`x`, where :math:`a` is a square ``(n, n)`` matrix and :math:`b` has shape
|
|
104
|
+
``(n, nrhs)`` or ``(n,)``, where ``nrhs`` is the number of RHS vectors.
|
|
105
|
+
It performs an LU factorization with partial
|
|
106
|
+
pivoting (:meth:`DirectSolver.factorize`) followed by triangular solves
|
|
107
|
+
(:meth:`DirectSolver.solve`).
|
|
108
|
+
The computation always runs on the GPU; CPU operands are copied to the device
|
|
109
|
+
and back automatically.
|
|
110
|
+
|
|
111
|
+
On each process, ``a`` and ``b`` hold only the portions of the global
|
|
112
|
+
matrices :math:`a` and :math:`b` that are assigned to that process. The
|
|
113
|
+
corresponding entries of ``distributions`` describe how :math:`a` and
|
|
114
|
+
:math:`b` are partitioned across processes.
|
|
115
|
+
|
|
116
|
+
The solution :math:`x` has the same local shape, memory space, and package
|
|
117
|
+
as ``b`` and follows ``b``'s distribution. By default,
|
|
118
|
+
:meth:`DirectSolver.factorize` overwrites ``a`` with its LU factors and
|
|
119
|
+
:meth:`DirectSolver.solve` overwrites ``b`` with :math:`x` and returns
|
|
120
|
+
``b``; this in-place behavior is controlled by the
|
|
121
|
+
:attr:`~nvmath.distributed.linalg.DirectSolverOptions.inplace_a` and
|
|
122
|
+
:attr:`~nvmath.distributed.linalg.DirectSolverOptions.inplace_b` attributes of
|
|
123
|
+
:class:`DirectSolverOptions` (both ``True`` by default). If :math:`a` is singular,
|
|
124
|
+
:meth:`DirectSolver.factorize` raises.
|
|
125
|
+
""".strip(),
|
|
126
|
+
#
|
|
127
|
+
"requirements": """\
|
|
128
|
+
.. _Requirements:
|
|
129
|
+
|
|
130
|
+
**Operands.**
|
|
131
|
+
|
|
132
|
+
- The global number of columns of ``a`` must match the global number of rows of
|
|
133
|
+
``b`` (both equal :math:`n`, the order of the square system).
|
|
134
|
+
- ``a`` and ``b`` must belong to the same package and have the same dtype, one of
|
|
135
|
+
``float32``, ``float64``, ``complex64``, or ``complex128``.
|
|
136
|
+
- ``a`` and ``b`` must reside in the same memory space on each process, and
|
|
137
|
+
GPU operands must be on the device bound to the distributed runtime on
|
|
138
|
+
that process.
|
|
139
|
+
- The local arrays must be column-major; a padded leading dimension
|
|
140
|
+
(a column-major slice of a larger buffer) is allowed.
|
|
141
|
+
|
|
142
|
+
**Distributions.**
|
|
143
|
+
|
|
144
|
+
``distributions`` must contain exactly two entries,
|
|
145
|
+
``[distribution_for_a, distribution_for_b]``, respectively describing how ``a``
|
|
146
|
+
and ``b`` are partitioned across processes.
|
|
147
|
+
|
|
148
|
+
The simplest, but not most performant, choice is to give
|
|
149
|
+
both ``a`` and ``b`` the same row-wise
|
|
150
|
+
:class:`~nvmath.distributed.distribution.Slab` distribution (``Slab.X``),
|
|
151
|
+
so that each process owns an equal, contiguous block of rows of the
|
|
152
|
+
global operands. The only requirement here is that the global number of
|
|
153
|
+
rows be divisible by the number of processes (a ``Slab`` with unequal
|
|
154
|
+
partition sizes is not supported, because it cannot be expressed as a
|
|
155
|
+
block-cyclic distribution); ``a`` and ``b`` are then partitioned
|
|
156
|
+
consistently and all the other requirements below are satisfied
|
|
157
|
+
automatically, so none of them need to be considered.
|
|
158
|
+
|
|
159
|
+
In general, each distribution entry may be a
|
|
160
|
+
:class:`~nvmath.distributed.distribution.Slab` (with uniform partition sizes),
|
|
161
|
+
:class:`~nvmath.distributed.distribution.BlockNonCyclic`, or
|
|
162
|
+
:class:`~nvmath.distributed.distribution.BlockCyclic` distribution,
|
|
163
|
+
and ``a`` and ``b`` may use different distributions.
|
|
164
|
+
This flexibility comes with stricter requirements:
|
|
165
|
+
|
|
166
|
+
- The process grid must place its first process at ``(0, 0)`` and
|
|
167
|
+
use a ``COL_MAJOR`` or ``ROW_MAJOR`` layout.
|
|
168
|
+
- The row block size of ``b`` must equal that of ``a``, while column block sizes
|
|
169
|
+
are independent.
|
|
170
|
+
- A 1-D ``b`` requires ``b``'s distribution to be 1-D (row-only): a 2-D distribution
|
|
171
|
+
is rejected, because the single column cannot be split across process columns.
|
|
172
|
+
- For a 2-D ``b``, ``b``'s distribution may be either 1-D (row-only) or
|
|
173
|
+
2-D, partitioning both rows and columns.
|
|
174
|
+
""".strip(),
|
|
175
|
+
#
|
|
176
|
+
# Re-generate these two with ``version_added=None``: the ``COMMON_SHARED_DOC_MAP``
|
|
177
|
+
# defaults stamp ".. versionadded:: 0.9.0", which does not apply to the (newer)
|
|
178
|
+
# distributed direct solver.
|
|
179
|
+
"release_operands": utils._release_operand_docstring(True, execution_methods=("factorize", "solve")),
|
|
180
|
+
"reset_operands_unchecked": utils._reset_operand_unchecked_docstring(True),
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class InvalidDirectSolverState(Exception):
|
|
186
|
+
"""Raised when a public method is called after :meth:`DirectSolver.free`."""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@utils.docstring_decorator(GENERIC_DIRECT_SOLVER_DOCUMENTATION, skip_missing=False)
|
|
190
|
+
class DirectSolver:
|
|
191
|
+
"""
|
|
192
|
+
Create a stateful object that encapsulates the specified distributed direct solver
|
|
193
|
+
computation for the dense linear system :math:`a @ x = b` and the required resources to
|
|
194
|
+
perform it.
|
|
195
|
+
|
|
196
|
+
A stateful object can be used to amortize the cost of preparation (planning and
|
|
197
|
+
factorization) across multiple solves with different right-hand sides, or with updated
|
|
198
|
+
operands of the same problem specification (see :meth:`reset_operands`).
|
|
199
|
+
The function-form API :func:`direct_solver` is a convenient alternative for single
|
|
200
|
+
use (the user needs to perform just one solve, for example), in which case there
|
|
201
|
+
is no possibility of amortizing preparatory costs.
|
|
202
|
+
|
|
203
|
+
Using the stateful object typically involves the following steps:
|
|
204
|
+
|
|
205
|
+
1. **Problem Specification**: Initialize the object with the operands, their
|
|
206
|
+
distributions, and options.
|
|
207
|
+
2. **Preparation**: Use :meth:`plan` to query and size the solver workspaces for this
|
|
208
|
+
specific problem.
|
|
209
|
+
3. **Execution**: :meth:`factorize` the matrix and :meth:`solve` the system.
|
|
210
|
+
4. **Resource Management**: Ensure all resources are released either by explicitly
|
|
211
|
+
calling :meth:`free` or by managing the stateful object within a context manager.
|
|
212
|
+
|
|
213
|
+
Detailed information on what is happening in the various phases described above can be
|
|
214
|
+
obtained by passing in a :class:`logging.Logger` object to :class:`DirectSolverOptions`
|
|
215
|
+
or by setting the appropriate options in the root logger object, which is used by
|
|
216
|
+
default:
|
|
217
|
+
|
|
218
|
+
>>> import logging
|
|
219
|
+
>>> logging.basicConfig(
|
|
220
|
+
... level=logging.INFO,
|
|
221
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
222
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
223
|
+
... )
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
a: {a}
|
|
227
|
+
|
|
228
|
+
b: {b}
|
|
229
|
+
|
|
230
|
+
distributions: {distributions}
|
|
231
|
+
|
|
232
|
+
options: {options}
|
|
233
|
+
|
|
234
|
+
stream: {stream}
|
|
235
|
+
|
|
236
|
+
Semantics:
|
|
237
|
+
{semantics}
|
|
238
|
+
|
|
239
|
+
Requirements:
|
|
240
|
+
{requirements}
|
|
241
|
+
|
|
242
|
+
.. seealso::
|
|
243
|
+
:meth:`plan`, :meth:`factorize`, :meth:`solve`, :meth:`reset_operands`,
|
|
244
|
+
:meth:`reset_operands_unchecked`, :meth:`release_operands`, :meth:`free`,
|
|
245
|
+
:class:`DirectSolverOptions`, :func:`direct_solver`.
|
|
246
|
+
|
|
247
|
+
Examples:
|
|
248
|
+
|
|
249
|
+
>>> import numpy as np
|
|
250
|
+
>>> import nvmath.distributed
|
|
251
|
+
>>> from nvmath.distributed.distribution import Slab
|
|
252
|
+
>>> from nvmath.distributed.linalg import DirectSolver
|
|
253
|
+
|
|
254
|
+
Get the process group used to initialize nvmath.distributed (for information on
|
|
255
|
+
initializing ``nvmath.distributed``, you can refer to the documentation or to the
|
|
256
|
+
direct solver examples in `nvmath/examples/distributed/linalg/generic/direct_solver
|
|
257
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/
|
|
258
|
+
generic/direct_solver>`_):
|
|
259
|
+
|
|
260
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
261
|
+
|
|
262
|
+
Get my process rank and the total number of processes:
|
|
263
|
+
|
|
264
|
+
>>> rank = process_group.rank
|
|
265
|
+
>>> nranks = process_group.nranks
|
|
266
|
+
|
|
267
|
+
We will solve the dense square system :math:`a @ x = b`, where :math:`a` has shape
|
|
268
|
+
``(n, n)`` and :math:`b` has shape ``(n, nrhs)``. Both operands use a
|
|
269
|
+
:class:`~nvmath.distributed.distribution.Slab` distribution, so each process owns a
|
|
270
|
+
contiguous block of rows of the global matrices.
|
|
271
|
+
|
|
272
|
+
.. note::
|
|
273
|
+
The :class:`~nvmath.distributed.distribution.Slab` distribution is used here
|
|
274
|
+
for convenience of exposition and is not necessarily the most performant
|
|
275
|
+
choice. For :math:`a`, a
|
|
276
|
+
:class:`~nvmath.distributed.distribution.BlockCyclic` distribution is
|
|
277
|
+
generally preferred, as it improves load balancing across processes.
|
|
278
|
+
|
|
279
|
+
Create the local row slabs on the CPU (cuSOLVERMp requires column-major):
|
|
280
|
+
|
|
281
|
+
>>> n, nrhs = 256, 8
|
|
282
|
+
>>> local_n = n // nranks # assume n is divisible by the process count
|
|
283
|
+
>>> rng = np.random.default_rng(rank)
|
|
284
|
+
>>> a = rng.random((local_n, n)).astype(np.float64, order="F")
|
|
285
|
+
>>> b = rng.random((local_n, nrhs)).astype(np.float64, order="F")
|
|
286
|
+
|
|
287
|
+
Make the global :math:`a` diagonally dominant so the system is well-conditioned.
|
|
288
|
+
The diagonal entry ``a[i, i]`` of this rank's row band is at local position
|
|
289
|
+
``[k, rank * local_n + k]``:
|
|
290
|
+
|
|
291
|
+
>>> idx = np.arange(local_n)
|
|
292
|
+
>>> a[idx, rank * local_n + idx] += n
|
|
293
|
+
|
|
294
|
+
Create a DirectSolver object encapsulating the problem specification above:
|
|
295
|
+
|
|
296
|
+
>>> distributions = [Slab.X, Slab.X]
|
|
297
|
+
>>> solver = DirectSolver(a, b, distributions=distributions)
|
|
298
|
+
|
|
299
|
+
Options can be provided above to control the behavior of the operation using the
|
|
300
|
+
`options` argument (see :class:`DirectSolverOptions`).
|
|
301
|
+
|
|
302
|
+
Next, plan the operation:
|
|
303
|
+
|
|
304
|
+
>>> solver.plan()
|
|
305
|
+
|
|
306
|
+
Factorize the matrix, then solve the system.
|
|
307
|
+
By default, the solution :math:`x` overwrites ``b`` and is returned:
|
|
308
|
+
|
|
309
|
+
>>> solver.factorize()
|
|
310
|
+
>>> x = solver.solve()
|
|
311
|
+
|
|
312
|
+
Finally, free the object's resources. To avoid having to explicitly make this call,
|
|
313
|
+
it's recommended to use the DirectSolver object as a context manager as shown below,
|
|
314
|
+
if possible.
|
|
315
|
+
|
|
316
|
+
>>> solver.free()
|
|
317
|
+
|
|
318
|
+
A benefit of the stateful API is that an expensive factorization can
|
|
319
|
+
be reused across multiple right-hand sides: factorize once, then solve
|
|
320
|
+
repeatedly, resetting only ``b`` between solves.
|
|
321
|
+
|
|
322
|
+
Further examples can be found in the
|
|
323
|
+
`nvmath/examples/distributed/linalg/generic/direct_solver
|
|
324
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/
|
|
325
|
+
generic/direct_solver>`_
|
|
326
|
+
directory.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
__slots__ = (
|
|
330
|
+
# Immutable, cross-rank-consistent derived spec
|
|
331
|
+
# see _ResolvedProblemSpec in _initialization.py
|
|
332
|
+
"_spec",
|
|
333
|
+
# Runtime / distributed context.
|
|
334
|
+
"_runtime",
|
|
335
|
+
# Lifecycle flags.
|
|
336
|
+
"_valid_state",
|
|
337
|
+
"_solver_planned",
|
|
338
|
+
"_solver_factorized",
|
|
339
|
+
"_operands_released",
|
|
340
|
+
# cuSOLVERMp resources (flat here for now, but can
|
|
341
|
+
# be refactored into a separate object in the future).
|
|
342
|
+
"_cusolvermp_handle",
|
|
343
|
+
"_cusolvermp_version",
|
|
344
|
+
"_cusolvermp_grid_a",
|
|
345
|
+
"_cusolvermp_grid_b",
|
|
346
|
+
"_desc_a",
|
|
347
|
+
"_desc_b",
|
|
348
|
+
"_factorization_state",
|
|
349
|
+
"_device_workspace",
|
|
350
|
+
"_host_workspace",
|
|
351
|
+
"_last_compute_event",
|
|
352
|
+
"_local_ld_a",
|
|
353
|
+
"_local_ld_b",
|
|
354
|
+
# Operands' state (flat here for now, but can
|
|
355
|
+
# be refactored into a separate object in the future).
|
|
356
|
+
"_lhs_compute",
|
|
357
|
+
"_rhs_compute",
|
|
358
|
+
"_lhs_user",
|
|
359
|
+
"_rhs_user",
|
|
360
|
+
"_lhs_synced",
|
|
361
|
+
"_rhs_synced",
|
|
362
|
+
# Misc.
|
|
363
|
+
"_options",
|
|
364
|
+
"_logger",
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Type annotations for attributes assigned by the construction free
|
|
368
|
+
# functions in ``_initialization.py``. mypy can't infer their types from a
|
|
369
|
+
# call it can't see into, so we declare them here. (Attributes assigned
|
|
370
|
+
# in-class by ``_init_defaults`` / ``__init__`` are inferred and omitted.)
|
|
371
|
+
# The derived problem facts now live behind ``_spec``; only the runtime,
|
|
372
|
+
# resource, and operand-state attributes set by free functions remain.
|
|
373
|
+
_spec: _ResolvedProblemSpec
|
|
374
|
+
_runtime: _RuntimeContext
|
|
375
|
+
# Active operand buffers handed to getrf/getrs as ``data_ptr``, bound in
|
|
376
|
+
# ``_resolve_active_operand_buffers`` from the wrapped user operands held in
|
|
377
|
+
# ``_lhs_user`` / ``_rhs_user``: a device mirror when a mirror is needed, or
|
|
378
|
+
# the user's GPU tensor itself on the cuda + inplace path. They don't exist
|
|
379
|
+
# before that step. So ``_lhs_compute`` always refers to a real compute-space
|
|
380
|
+
# buffer -- never a transient CPU operand.
|
|
381
|
+
_lhs_compute: TensorHolder
|
|
382
|
+
_rhs_compute: TensorHolder
|
|
383
|
+
_local_ld_a: int
|
|
384
|
+
_local_ld_b: int
|
|
385
|
+
|
|
386
|
+
def _init_defaults(self) -> None:
|
|
387
|
+
"""
|
|
388
|
+
Pre-initialize every slot that ``_release_internal_resources`` guards
|
|
389
|
+
with ``if X is not None``.
|
|
390
|
+
|
|
391
|
+
``__init__``'s rollback path calls ``self._release_internal_resources``
|
|
392
|
+
on any partial-construction failure, which then walks the resource
|
|
393
|
+
slots below. The ``None`` defaults here are what makes that walk safe:
|
|
394
|
+
without them the rollback itself would raise ``AttributeError`` due
|
|
395
|
+
to ``__slots__``.
|
|
396
|
+
|
|
397
|
+
Lifecycle flags also live here so the rollback path can read them
|
|
398
|
+
(``_valid_state`` is set to ``True`` only at the end of ``__init__``,
|
|
399
|
+
which is what makes ``free()`` idempotent across double-frees while
|
|
400
|
+
still letting the rollback bypass the guard via the private helper).
|
|
401
|
+
|
|
402
|
+
Note: this is placed *before* __init__ because mypy sees it first;
|
|
403
|
+
subsequent assignments in ``__init__`` / workflow methods narrow each
|
|
404
|
+
union to a concrete value.
|
|
405
|
+
"""
|
|
406
|
+
# Lifecycle flags. _valid_state stays False until __init__ completes.
|
|
407
|
+
self._valid_state: bool = False
|
|
408
|
+
self._solver_planned: bool = False
|
|
409
|
+
self._solver_factorized: bool = False
|
|
410
|
+
self._operands_released: bool = False
|
|
411
|
+
|
|
412
|
+
self._cusolvermp_handle: int | None = None
|
|
413
|
+
# cuSOLVERMp library version set during __init__
|
|
414
|
+
self._cusolvermp_version: int | None = None
|
|
415
|
+
self._cusolvermp_grid_a: int | None = None
|
|
416
|
+
self._cusolvermp_grid_b: int | None = None
|
|
417
|
+
self._desc_a: int | None = None
|
|
418
|
+
self._desc_b: int | None = None
|
|
419
|
+
|
|
420
|
+
# Persistent device state (pivot vector + per-call info flags).
|
|
421
|
+
self._factorization_state: _FactorizationState | None = None
|
|
422
|
+
|
|
423
|
+
self._device_workspace: Workspace | None = None
|
|
424
|
+
self._host_workspace: Workspace | None = None
|
|
425
|
+
|
|
426
|
+
self._last_compute_event = None
|
|
427
|
+
|
|
428
|
+
# User-side wrappers. ``_collective_problem_spec`` binds the wrapped user
|
|
429
|
+
# operands here during construction; ``_resolve_active_operand_buffers``
|
|
430
|
+
# then either keeps them (mirror case, where ``_lhs_compute`` /
|
|
431
|
+
# ``_rhs_compute`` point at a mirror and the user wrapper is held here for
|
|
432
|
+
# copy-back / ``release_operands``) or, for an operand that needs no
|
|
433
|
+
# mirror (cuda + inplace), moves the wrapper into ``_lhs_compute`` /
|
|
434
|
+
# ``_rhs_compute`` and resets the slot to None. So in steady state these
|
|
435
|
+
# are non-None iff ``_spec.need_lhs_mirror`` / ``_spec.need_rhs_mirror``.
|
|
436
|
+
# The None default also keeps the partial-init rollback safe.
|
|
437
|
+
self._lhs_user: TensorHolder | None = None
|
|
438
|
+
self._rhs_user: TensorHolder | None = None
|
|
439
|
+
|
|
440
|
+
# Whether the rhs compute mirror currently holds a faithful copy of the
|
|
441
|
+
# user's b (i.e. no refresh needed before getrs). True after the initial
|
|
442
|
+
# staging copy and after reset_operands(b=...); set False once getrs
|
|
443
|
+
# overwrites the mirror with x. Mirrors the non-distributed solver's
|
|
444
|
+
# ``rhs_synced`` and lets solve() skip the H2D/D2D copy when the mirror
|
|
445
|
+
# is already in sync (e.g. the first solve after construction/reset).
|
|
446
|
+
self._rhs_synced: bool = True
|
|
447
|
+
|
|
448
|
+
# Whether the lhs compute mirror currently holds a faithful copy of the
|
|
449
|
+
# user's A (i.e. no refresh needed before getrf). True after the initial
|
|
450
|
+
# staging copy and after reset_operands(a=...); set False once getrf
|
|
451
|
+
# overwrites the mirror with the LU factors. Symmetric with
|
|
452
|
+
# ``_rhs_synced``: lets factorize() skip the H2D/D2D copy when the mirror
|
|
453
|
+
# is already in sync, and makes a repeated factorize() re-read the
|
|
454
|
+
# pristine A rather than factorizing the previous LU.
|
|
455
|
+
self._lhs_synced: bool = True
|
|
456
|
+
|
|
457
|
+
def __init__(
|
|
458
|
+
self,
|
|
459
|
+
a: AnyTensor,
|
|
460
|
+
b: AnyTensor,
|
|
461
|
+
/,
|
|
462
|
+
*,
|
|
463
|
+
distributions: Sequence[Distribution],
|
|
464
|
+
options: DirectSolverOptions | dict[str, Any] | None = None,
|
|
465
|
+
stream: utils.AnyStream | int | None = None,
|
|
466
|
+
) -> None:
|
|
467
|
+
# __init__ covers cuSOLVERMp workflow steps 1-4 (runtime/handle/grids/
|
|
468
|
+
# descriptors) and creates the workspaces without allocating their
|
|
469
|
+
# memory. The step implementations live in _initialization.py; see the
|
|
470
|
+
# "cuSOLVERMp workflow -> code map" comment at module scope for how
|
|
471
|
+
# every step maps onto this class and that module.
|
|
472
|
+
|
|
473
|
+
# Default-initialize every slot so the rollback cleanup can work safely.
|
|
474
|
+
self._init_defaults()
|
|
475
|
+
|
|
476
|
+
self._options: DirectSolverOptions = utils.check_or_create_options( # type: ignore[assignment]
|
|
477
|
+
DirectSolverOptions, options, "Distributed direct solver options"
|
|
478
|
+
)
|
|
479
|
+
self._logger = self._options.logger if self._options.logger is not None else logging.getLogger()
|
|
480
|
+
self._logger.info("= SPECIFICATION PHASE =")
|
|
481
|
+
|
|
482
|
+
# ``options.blocking`` is accepted, but it is currently ignored:
|
|
483
|
+
# factorize()/solve() always block.
|
|
484
|
+
# This is the correct behavior anyway when blocking is True; when blocking
|
|
485
|
+
# is "auto" (the default), always blocking is still a valid choice and
|
|
486
|
+
# leaves room for forward compatibility once we implement non-blocking
|
|
487
|
+
# execution for GPU operands.
|
|
488
|
+
# TODO: use options.blocking and enable asynchronous execution.
|
|
489
|
+
|
|
490
|
+
# Resolve the runtime context (verify the NCCL communicator and capture
|
|
491
|
+
# rank/device). Kept OUTSIDE the try below because the rollback reads
|
|
492
|
+
# _runtime.device_id.
|
|
493
|
+
self._runtime = _resolve_runtime_context()
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
# Run the cuSOLVERMp construction workflow.
|
|
497
|
+
initialize_direct_solver(self, a, b, distributions=distributions, stream=stream)
|
|
498
|
+
except BaseException:
|
|
499
|
+
# Construction failed partway through; release whatever was already
|
|
500
|
+
# built. We call _release_internal_resources() directly rather than
|
|
501
|
+
# free() because _valid_state is still False, so free() would
|
|
502
|
+
# short-circuit to a no-op. _release_internal_resources() tolerates
|
|
503
|
+
# partial init: each resource is guarded by an is-not-None check.
|
|
504
|
+
try:
|
|
505
|
+
self._release_internal_resources()
|
|
506
|
+
except Exception:
|
|
507
|
+
self._logger.exception("DirectSolver: error during partial-init rollback")
|
|
508
|
+
raise
|
|
509
|
+
|
|
510
|
+
self._valid_state = True
|
|
511
|
+
self._logger.info("The DirectSolver instance has been created.")
|
|
512
|
+
|
|
513
|
+
def _create_compute_mirror(self, src: TensorHolder, stream_holder) -> TensorHolder:
|
|
514
|
+
# Allocate a fresh execution-space mirror for ``src`` and copy its
|
|
515
|
+
# data in. Two implementations behind a single signature:
|
|
516
|
+
|
|
517
|
+
# * Cross-space (cpu operand, cuda execution): ``TensorHolder.to``
|
|
518
|
+
# handles the allocation and the H2D copy in one call.
|
|
519
|
+
# * Same-space (cuda operand, cuda execution): we need a fresh
|
|
520
|
+
# same-device buffer with the same shape/dtype. Cannot reuse
|
|
521
|
+
# ``.to(self._runtime.device_id)`` here because
|
|
522
|
+
# ``CudaTensor.to(same_device)`` is a no-op that returns ``self``
|
|
523
|
+
# (see ``nvmath/internal/tensor_ifc_numpy.py``) -- it would
|
|
524
|
+
# alias the user's tensor instead of cloning it. Allocate via
|
|
525
|
+
# ``TensorHolder.empty`` and then ``copy_`` D2D.
|
|
526
|
+
|
|
527
|
+
if self._spec.operands_memory_space == self._spec.execution_space:
|
|
528
|
+
# Lay the mirror out densely in column-major order (LLD == local
|
|
529
|
+
# rows) rather than mirroring ``src.strides``. The source may be a
|
|
530
|
+
# slice of a larger buffer with a *padded* leading dimension
|
|
531
|
+
# (strides[1] > shape[0]); such strides are not dense, and
|
|
532
|
+
# ``TensorHolder.empty`` sizes its allocation for a dense layout
|
|
533
|
+
# only -- handing it padded strides yields an undersized buffer
|
|
534
|
+
# that cuSOLVERMp then overruns. cuSOLVERMp accepts any LLD >= the
|
|
535
|
+
# local row count, so a tight mirror is both valid and sufficient;
|
|
536
|
+
# ``copy_`` is a strided copy, so it reads the padded source
|
|
537
|
+
# correctly regardless.
|
|
538
|
+
dst = src.__class__.empty(
|
|
539
|
+
shape=src.shape,
|
|
540
|
+
device_id=self._runtime.device_id,
|
|
541
|
+
dtype=src.dtype,
|
|
542
|
+
strides=compute_f_strides(tuple(src.shape)),
|
|
543
|
+
stream_holder=stream_holder,
|
|
544
|
+
)
|
|
545
|
+
dst.copy_(src, stream_holder=stream_holder)
|
|
546
|
+
return dst
|
|
547
|
+
return src.to(self._runtime.device_id, stream_holder)
|
|
548
|
+
|
|
549
|
+
def _check_valid_solver(self, *args, **kwargs):
|
|
550
|
+
if not self._valid_state:
|
|
551
|
+
raise InvalidDirectSolverState("The DirectSolver object cannot be used after resources are freed")
|
|
552
|
+
|
|
553
|
+
def _check_planned(self, *args, **kwargs):
|
|
554
|
+
what = kwargs["what"]
|
|
555
|
+
if not self._solver_planned:
|
|
556
|
+
raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
|
|
557
|
+
|
|
558
|
+
def _check_factorized(self, *args, **kwargs):
|
|
559
|
+
what = kwargs["what"]
|
|
560
|
+
if not self._solver_factorized:
|
|
561
|
+
raise RuntimeError(f"{what} cannot be performed before factorize() has been called.")
|
|
562
|
+
|
|
563
|
+
def _check_valid_operands(self, *args, **kwargs):
|
|
564
|
+
what = kwargs["what"]
|
|
565
|
+
if self._operands_released:
|
|
566
|
+
raise RuntimeError(
|
|
567
|
+
f"{what} cannot be performed after the operands have been released. "
|
|
568
|
+
f"Use reset_operands() to provide new operands before performing the {what.lower()}."
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
@utils.precondition(_check_valid_solver)
|
|
572
|
+
@utils.precondition(_check_valid_operands, "Planning")
|
|
573
|
+
def plan(self, *, stream: utils.AnyStream | int | None = None) -> None:
|
|
574
|
+
"""
|
|
575
|
+
Plan the distributed direct solver. This queries cuSOLVERMp for the host and device
|
|
576
|
+
workspace sizes needed by the factorization and the solve, and sizes the solver's
|
|
577
|
+
internal workspaces accordingly. Planning is idempotent, so calling it again after a
|
|
578
|
+
successful plan is a no-op.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
stream: {stream}
|
|
582
|
+
|
|
583
|
+
.. seealso::
|
|
584
|
+
:meth:`factorize`, :meth:`solve`.
|
|
585
|
+
"""
|
|
586
|
+
# plan() is idempotent: re-entry after a successful plan is a
|
|
587
|
+
# logged no-op so callers can stack plan() inside a setup helper
|
|
588
|
+
# without tracking whether it has run before.
|
|
589
|
+
if self._solver_planned:
|
|
590
|
+
self._logger.info("Skipping planning since it has already been performed in a previous call.")
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
self._logger.info("= PLANNING PHASE =")
|
|
594
|
+
|
|
595
|
+
stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
|
|
596
|
+
self._logger.info(f"The specified stream for DirectSolver.plan is {stream_holder.obj}.")
|
|
597
|
+
|
|
598
|
+
# Device pointers to this rank's local tile of A, b, and the pivot
|
|
599
|
+
a_data_ptr = self._lhs_compute.data_ptr
|
|
600
|
+
b_data_ptr = self._rhs_compute.data_ptr
|
|
601
|
+
ipiv_ptr = self._factorization_state.ipiv_device_ptr # type: ignore[union-attr]
|
|
602
|
+
|
|
603
|
+
with utils.device_ctx(self._runtime.device_id):
|
|
604
|
+
# *_buffer_size queries launch no kernels, so the stream is unused
|
|
605
|
+
# here; set for consistency with factorize()/solve().
|
|
606
|
+
cusolverMp.set_stream(self._cusolvermp_handle, stream_holder.ptr)
|
|
607
|
+
|
|
608
|
+
# https://docs.nvidia.com/cuda/cusolvermp/usage/functions.html#cusolvermpgetrf-buffersize
|
|
609
|
+
getrf_device_bytes, getrf_host_bytes = cusolverMp.getrf_buffer_size(
|
|
610
|
+
self._cusolvermp_handle,
|
|
611
|
+
self._spec.global_m,
|
|
612
|
+
self._spec.global_n,
|
|
613
|
+
a_data_ptr,
|
|
614
|
+
self._spec.ia,
|
|
615
|
+
self._spec.ja,
|
|
616
|
+
self._desc_a,
|
|
617
|
+
ipiv_ptr,
|
|
618
|
+
self._spec.solver_cuda_dtype,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# trans=N: cuSOLVERMp's getrs only supports non-transposed solves
|
|
622
|
+
# against the LU factorization produced by getrf (in-place on A).
|
|
623
|
+
# https://docs.nvidia.com/cuda/cusolvermp/usage/functions.html#cusolvermpgetrs-buffersize
|
|
624
|
+
getrs_device_bytes, getrs_host_bytes = cusolverMp.getrs_buffer_size(
|
|
625
|
+
self._cusolvermp_handle,
|
|
626
|
+
cublas.Operation.N,
|
|
627
|
+
self._spec.global_m,
|
|
628
|
+
self._spec.nrhs,
|
|
629
|
+
a_data_ptr,
|
|
630
|
+
self._spec.ia,
|
|
631
|
+
self._spec.ja,
|
|
632
|
+
self._desc_a,
|
|
633
|
+
ipiv_ptr,
|
|
634
|
+
b_data_ptr,
|
|
635
|
+
self._spec.ib,
|
|
636
|
+
self._spec.jb,
|
|
637
|
+
self._desc_b,
|
|
638
|
+
self._spec.solver_cuda_dtype,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
device_bytes = max(getrf_device_bytes, getrs_device_bytes)
|
|
642
|
+
host_bytes = max(getrf_host_bytes, getrs_host_bytes)
|
|
643
|
+
assert self._device_workspace is not None and self._host_workspace is not None, (
|
|
644
|
+
"Workspace objects must be created before plan()."
|
|
645
|
+
)
|
|
646
|
+
self._device_workspace.set_size(device_bytes)
|
|
647
|
+
self._host_workspace.set_size(host_bytes)
|
|
648
|
+
|
|
649
|
+
self._logger.info(
|
|
650
|
+
"Planned cuSOLVERMp workspaces: device=%d B, host=%d B "
|
|
651
|
+
"(getrf needed device=%d/host=%d, getrs needed device=%d/host=%d).",
|
|
652
|
+
device_bytes,
|
|
653
|
+
host_bytes,
|
|
654
|
+
getrf_device_bytes,
|
|
655
|
+
getrf_host_bytes,
|
|
656
|
+
getrs_device_bytes,
|
|
657
|
+
getrs_host_bytes,
|
|
658
|
+
)
|
|
659
|
+
self._solver_planned = True
|
|
660
|
+
|
|
661
|
+
@utils.precondition(_check_valid_solver)
|
|
662
|
+
@utils.precondition(_check_valid_operands, "Factorization")
|
|
663
|
+
@utils.precondition(_check_planned, "Factorization")
|
|
664
|
+
def factorize(self, *, stream: utils.AnyStream | int | None = None) -> None:
|
|
665
|
+
"""
|
|
666
|
+
Factorize the matrix :math:`a` (LU factorization with partial pivoting).
|
|
667
|
+
|
|
668
|
+
Each call factorizes the left-hand side ``a`` currently held by the solver.
|
|
669
|
+
To change the left-hand side values between calls, call :meth:`reset_operands`
|
|
670
|
+
(or :meth:`reset_operands_unchecked`) with the new ``a``. Alternatively, when
|
|
671
|
+
``inplace_a=True`` and ``a`` is accessible from the execution space, users
|
|
672
|
+
may directly modify ``a`` in place, for example with ``a[:] = a_new``.
|
|
673
|
+
In that case, users are responsible for ensuring that ``a`` contains the intended
|
|
674
|
+
left-hand side values before each call, since :meth:`factorize` overwrites
|
|
675
|
+
that operand with its LU factors.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
stream: {stream}
|
|
679
|
+
|
|
680
|
+
.. seealso::
|
|
681
|
+
:meth:`plan`, :meth:`solve`, :meth:`reset_operands`.
|
|
682
|
+
"""
|
|
683
|
+
|
|
684
|
+
self._logger.info("= FACTORIZATION PHASE =")
|
|
685
|
+
|
|
686
|
+
stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
|
|
687
|
+
self._logger.info(f"The specified stream for DirectSolver.factorize is {stream_holder.obj}.")
|
|
688
|
+
|
|
689
|
+
# Refresh the LHS compute mirror from the user's current A when it is no
|
|
690
|
+
# longer in sync: getrf is destructive on the mirror, so once a prior
|
|
691
|
+
# factorize has overwritten it with the LU we must re-read A before
|
|
692
|
+
# factorizing again (this is also what makes an in-place edit to a mirrored
|
|
693
|
+
# A visible to the next factorize() without an intervening reset_operands,
|
|
694
|
+
# and keeps factorize(); factorize() re-factorizing the original A rather
|
|
695
|
+
# than the previous LU). ``_lhs_synced`` lets us skip the copy when the
|
|
696
|
+
# mirror is already current (e.g. the first factorize after
|
|
697
|
+
# construction/reset). When there is no lhs mirror (gpu + inplace_a=True)
|
|
698
|
+
# the user's A IS the live buffer and needs no refresh; need_lhs_mirror is
|
|
699
|
+
# False there (and _lhs_user is None), so this block is skipped.
|
|
700
|
+
if self._spec.need_lhs_mirror and not self._lhs_synced:
|
|
701
|
+
assert self._lhs_user is not None, "lhs mirror expected but no user handle is held"
|
|
702
|
+
if self._lhs_compute.tensor is None:
|
|
703
|
+
self._lhs_compute = self._create_compute_mirror(self._lhs_user, stream_holder)
|
|
704
|
+
else:
|
|
705
|
+
self._lhs_compute.copy_(self._lhs_user, stream_holder=stream_holder)
|
|
706
|
+
self._lhs_synced = True
|
|
707
|
+
|
|
708
|
+
# narrow for mypy
|
|
709
|
+
factorization_state = self._factorization_state
|
|
710
|
+
device_workspace, host_workspace = self._device_workspace, self._host_workspace
|
|
711
|
+
assert factorization_state is not None
|
|
712
|
+
assert device_workspace is not None and host_workspace is not None
|
|
713
|
+
|
|
714
|
+
# ``options.blocking`` is intentionally ignored here: getrf is
|
|
715
|
+
# launched non-blocking and captures _last_compute_event for
|
|
716
|
+
# stream ordering, then sync_and_check_factorize_info() below does
|
|
717
|
+
# an unconditional blocking sync. factorize() therefore always
|
|
718
|
+
# blocks regardless of options.blocking (see __init__ for context).
|
|
719
|
+
# The non-blocking launch + event capture is kept for forward
|
|
720
|
+
# compatibility: once we support non-blocking execution, honoring
|
|
721
|
+
# options.blocking here becomes almost a matter of skipping that final sync.
|
|
722
|
+
with (
|
|
723
|
+
device_workspace.allocate_perhaps(
|
|
724
|
+
stream_holder,
|
|
725
|
+
get_last_event=lambda: self._last_compute_event,
|
|
726
|
+
) as device_wsp,
|
|
727
|
+
host_workspace.allocate_perhaps(
|
|
728
|
+
stream_holder,
|
|
729
|
+
get_last_event=lambda: self._last_compute_event,
|
|
730
|
+
) as host_wsp,
|
|
731
|
+
utils.cuda_call_ctx(stream_holder, blocking=False, timing=False) as (
|
|
732
|
+
self._last_compute_event,
|
|
733
|
+
_,
|
|
734
|
+
),
|
|
735
|
+
):
|
|
736
|
+
cusolverMp.set_stream(self._cusolvermp_handle, stream_holder.ptr)
|
|
737
|
+
|
|
738
|
+
cusolverMp.getrf(
|
|
739
|
+
self._cusolvermp_handle,
|
|
740
|
+
self._spec.global_m,
|
|
741
|
+
self._spec.global_n,
|
|
742
|
+
self._lhs_compute.data_ptr,
|
|
743
|
+
self._spec.ia,
|
|
744
|
+
self._spec.ja,
|
|
745
|
+
self._desc_a,
|
|
746
|
+
factorization_state.ipiv_device_ptr,
|
|
747
|
+
self._spec.solver_cuda_dtype,
|
|
748
|
+
device_wsp.raw_ptr,
|
|
749
|
+
device_wsp.size,
|
|
750
|
+
host_wsp.raw_ptr,
|
|
751
|
+
host_wsp.size,
|
|
752
|
+
factorization_state.info_getrf_device_ptr,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
# getrf overwrote the compute buffer with the LU factors. Under a mirror
|
|
756
|
+
# the buffer no longer matches the user's A, so mark it stale: the next
|
|
757
|
+
# factorize() will refresh from _lhs_user first (matching solve()'s rhs
|
|
758
|
+
# handling at the getrs site). With no mirror (gpu + inplace_a=True) the
|
|
759
|
+
# user's A IS the buffer and freshness is user-managed, so it stays synced.
|
|
760
|
+
self._lhs_synced = not self._spec.need_lhs_mirror
|
|
761
|
+
|
|
762
|
+
# Unconditional, blocking sync + cross-rank info check; raises on
|
|
763
|
+
# singular / illegal-argument.
|
|
764
|
+
factorization_state.sync_and_check_factorize_info(
|
|
765
|
+
stream_holder,
|
|
766
|
+
nccl_comm=self._runtime.distributed_ctx.nccl_comm,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# inplace_a on a CPU operand: getrf ran on the GPU mirror, so copy the LU
|
|
770
|
+
# back into the user's buffer. Done after the info check above so a
|
|
771
|
+
# failed (e.g. singular) factorization leaves the user's A intact rather
|
|
772
|
+
# than overwriting it with a partial LU.
|
|
773
|
+
if self._spec.need_lhs_mirror and self._options.inplace_a:
|
|
774
|
+
assert self._lhs_user is not None
|
|
775
|
+
self._lhs_user.copy_(self._lhs_compute, stream_holder=stream_holder)
|
|
776
|
+
|
|
777
|
+
self._solver_factorized = True
|
|
778
|
+
self._logger.info("Factorization has been completed on rank %d.", self._runtime.rank)
|
|
779
|
+
|
|
780
|
+
@utils.precondition(_check_valid_solver)
|
|
781
|
+
@utils.precondition(_check_valid_operands, "Solver Execution")
|
|
782
|
+
@utils.precondition(_check_planned, "Solver Execution")
|
|
783
|
+
@utils.precondition(_check_factorized, "Solver Execution")
|
|
784
|
+
def solve(self, *, stream: utils.AnyStream | int | None = None) -> Any:
|
|
785
|
+
"""
|
|
786
|
+
Solve the factorized system :math:`a @ x = b` for :math:`x` using the factors
|
|
787
|
+
from the most recent :meth:`factorize` and the current right-hand side ``b``.
|
|
788
|
+
|
|
789
|
+
Each call uses the right-hand side ``b`` currently held by the solver.
|
|
790
|
+
To change the right-hand side values between calls, call :meth:`reset_operands`
|
|
791
|
+
(or :meth:`reset_operands_unchecked`) with the new ``b``. Alternatively, when
|
|
792
|
+
``inplace_b=True`` and ``b`` is accessible from the execution space, users
|
|
793
|
+
may directly modify ``b`` in place, for example with ``b[:] = b_new``.
|
|
794
|
+
In that case, users are responsible for ensuring that ``b`` contains the
|
|
795
|
+
intended right-hand side values before each call, since :meth:`solve`
|
|
796
|
+
overwrites that operand with the solution.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
stream: {stream}
|
|
800
|
+
|
|
801
|
+
Returns:
|
|
802
|
+
{result}
|
|
803
|
+
|
|
804
|
+
.. seealso::
|
|
805
|
+
:meth:`plan`, :meth:`factorize`, :meth:`reset_operands`.
|
|
806
|
+
"""
|
|
807
|
+
self._logger.info("= SOLVER EXECUTION PHASE =")
|
|
808
|
+
|
|
809
|
+
stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
|
|
810
|
+
self._logger.info(f"The specified stream for DirectSolver.solve is {stream_holder.obj}.")
|
|
811
|
+
|
|
812
|
+
# Refresh the RHS compute mirror from the user's current b when it is no
|
|
813
|
+
# longer in sync: getrs is destructive on the mirror, so once a prior
|
|
814
|
+
# solve has dirtied it we must
|
|
815
|
+
# re-read b before solving again (this is also what makes an in-place
|
|
816
|
+
# edit to b visible to the next solve without an intervening
|
|
817
|
+
# reset_operands). ``_rhs_synced`` lets us skip the copy when the mirror
|
|
818
|
+
# is already current (e.g. the first solve after construction/reset).
|
|
819
|
+
# Two sub-cases when a refresh is due:
|
|
820
|
+
# * tensor is None: the previous solve was cuda + inplace_b=False and
|
|
821
|
+
# handed the mirror buffer (holding x) to the caller, so allocate a
|
|
822
|
+
# fresh mirror from b. (release_operands() also nulls the slot, but
|
|
823
|
+
# _check_valid_operands has already rejected that path before here.)
|
|
824
|
+
# * tensor is live: copy b into the existing mirror (H2D for a cpu
|
|
825
|
+
# operand, D2D for a gpu operand).
|
|
826
|
+
# When there is no rhs mirror (gpu + inplace_b=True) the user's b IS the
|
|
827
|
+
# live buffer and needs no refresh; need_rhs_mirror is False there (and
|
|
828
|
+
# _rhs_user is None), so this block is skipped.
|
|
829
|
+
if self._spec.need_rhs_mirror and not self._rhs_synced:
|
|
830
|
+
assert self._rhs_user is not None, "rhs mirror expected but no user handle is held"
|
|
831
|
+
if self._rhs_compute.tensor is None:
|
|
832
|
+
self._rhs_compute = self._create_compute_mirror(self._rhs_user, stream_holder)
|
|
833
|
+
else:
|
|
834
|
+
self._rhs_compute.copy_(self._rhs_user, stream_holder=stream_holder)
|
|
835
|
+
self._rhs_synced = True
|
|
836
|
+
|
|
837
|
+
# narrow for mypy
|
|
838
|
+
factorization_state = self._factorization_state
|
|
839
|
+
device_workspace, host_workspace = self._device_workspace, self._host_workspace
|
|
840
|
+
assert factorization_state is not None
|
|
841
|
+
assert device_workspace is not None and host_workspace is not None
|
|
842
|
+
|
|
843
|
+
# ``blocking=False`` is intentional: solve() still always blocks now,
|
|
844
|
+
# but the sync happens in the return-path dispatch below (see that
|
|
845
|
+
# comment for the per-branch sync behavior). Blocking here too would
|
|
846
|
+
# just force a redundant sync.
|
|
847
|
+
with (
|
|
848
|
+
device_workspace.allocate_perhaps(
|
|
849
|
+
stream_holder,
|
|
850
|
+
get_last_event=lambda: self._last_compute_event,
|
|
851
|
+
) as device_wsp,
|
|
852
|
+
host_workspace.allocate_perhaps(
|
|
853
|
+
stream_holder,
|
|
854
|
+
get_last_event=lambda: self._last_compute_event,
|
|
855
|
+
) as host_wsp,
|
|
856
|
+
utils.cuda_call_ctx(stream_holder, blocking=False, timing=False) as (
|
|
857
|
+
self._last_compute_event,
|
|
858
|
+
_,
|
|
859
|
+
),
|
|
860
|
+
):
|
|
861
|
+
cusolverMp.set_stream(self._cusolvermp_handle, stream_holder.ptr)
|
|
862
|
+
|
|
863
|
+
# cuSOLVERMp 0.8.0 does not internally zero
|
|
864
|
+
# ``d_info`` at getrs entry: a successful call leaves
|
|
865
|
+
# the buffer holding whatever it had on entry, while
|
|
866
|
+
# only invalid-argument paths actually write a
|
|
867
|
+
# negative info. Zero it ourselves so the post-call
|
|
868
|
+
# value is deterministic (any future reader of getrs
|
|
869
|
+
# info sees 0 on success). Newer versions are correct.
|
|
870
|
+
if self._cusolvermp_version == 800:
|
|
871
|
+
factorization_state.reset_info_getrs_device(stream_holder)
|
|
872
|
+
|
|
873
|
+
cusolverMp.getrs(
|
|
874
|
+
self._cusolvermp_handle,
|
|
875
|
+
# trans=N: getrs only supports non-transposed
|
|
876
|
+
cublas.Operation.N,
|
|
877
|
+
self._spec.global_n,
|
|
878
|
+
self._spec.nrhs,
|
|
879
|
+
self._lhs_compute.data_ptr,
|
|
880
|
+
self._spec.ia,
|
|
881
|
+
self._spec.ja,
|
|
882
|
+
self._desc_a,
|
|
883
|
+
factorization_state.ipiv_device_ptr,
|
|
884
|
+
self._rhs_compute.data_ptr,
|
|
885
|
+
self._spec.ib,
|
|
886
|
+
self._spec.jb,
|
|
887
|
+
self._desc_b,
|
|
888
|
+
self._spec.solver_cuda_dtype,
|
|
889
|
+
device_wsp.raw_ptr,
|
|
890
|
+
device_wsp.size,
|
|
891
|
+
host_wsp.raw_ptr,
|
|
892
|
+
host_wsp.size,
|
|
893
|
+
factorization_state.info_getrs_device_ptr,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
# getrs info is intentionally not checked: cuSOLVERMp's getrs only writes
|
|
897
|
+
# info on an invalid argument (info < 0), which would indicate a bug
|
|
898
|
+
# in the parameters we pass -- not a user error -- and would almost
|
|
899
|
+
# certainly have been caught by getrf, whose arguments are mostly the same.
|
|
900
|
+
# There is no info > 0 (singular) case for a triangular solve.
|
|
901
|
+
|
|
902
|
+
# getrs just overwrote the rhs mirror with x (or, in the no-mirror
|
|
903
|
+
# gpu + inplace_b=True case, the user's b in place). Mark the mirror
|
|
904
|
+
# dirty so the next solve re-reads b first. The no-mirror case never
|
|
905
|
+
# refreshes (guarded on need_rhs_mirror), so leaving it "synced" there
|
|
906
|
+
# is harmless.
|
|
907
|
+
self._rhs_synced = not self._spec.need_rhs_mirror
|
|
908
|
+
|
|
909
|
+
# Return-path dispatch, outer branch on operand memory
|
|
910
|
+
# space, inner branch on ``inplace_b``. The cpu arms always
|
|
911
|
+
# stream-sync (D->H via ``ndbuffer.copy_into``); the cuda
|
|
912
|
+
# arms leave the stream pending and sync at the end of their
|
|
913
|
+
# branch (solve() always blocks).
|
|
914
|
+
if self._spec.operands_memory_space == "cpu":
|
|
915
|
+
# cpu: getrs wrote x into the solver's GPU mirror, so a
|
|
916
|
+
# D->H copy is unavoidable on the way out. Both
|
|
917
|
+
# sub-branches go through ``ndbuffer.copy_into``'s D->H
|
|
918
|
+
# path, which stream-syncs internally -- the result is
|
|
919
|
+
# host-visible by the time the call returns.
|
|
920
|
+
assert self._rhs_user is not None
|
|
921
|
+
if self._options.inplace_b:
|
|
922
|
+
# Copy x back into the user's CPU b.
|
|
923
|
+
self._rhs_user.copy_(self._rhs_compute, stream_holder=stream_holder)
|
|
924
|
+
out = self._rhs_user.tensor
|
|
925
|
+
else:
|
|
926
|
+
# D2H into a fresh CPU tensor; keep the mirror as
|
|
927
|
+
# solver-owned scratch for the next solve.
|
|
928
|
+
out = self._rhs_compute.to("cpu", stream_holder).tensor
|
|
929
|
+
else:
|
|
930
|
+
# cuda: x already lives on the GPU; no D->H involved, so both
|
|
931
|
+
# sub-branches leave stream work pending and we sync explicitly
|
|
932
|
+
# (solve() always blocks). The cpu branch above needs no such
|
|
933
|
+
# sync -- its D->H copy already blocks the host.
|
|
934
|
+
if self._options.inplace_b:
|
|
935
|
+
# No mirror was allocated -- ``_rhs_compute`` is
|
|
936
|
+
# the user's b, getrs wrote x into it in place.
|
|
937
|
+
# Hand it back as-is.
|
|
938
|
+
out = self._rhs_compute.tensor
|
|
939
|
+
else:
|
|
940
|
+
# Hand the mirror buffer (now holding x) directly to the
|
|
941
|
+
# caller, then null the slot so the solver no longer references it.
|
|
942
|
+
# A subsequent solve()/reset_operands reallocates a fresh mirror.
|
|
943
|
+
out = self._rhs_compute.tensor
|
|
944
|
+
self._rhs_compute.tensor = None
|
|
945
|
+
stream_holder.obj.sync()
|
|
946
|
+
|
|
947
|
+
self._logger.info("Solver execution has been completed on rank %d.", self._runtime.rank)
|
|
948
|
+
|
|
949
|
+
return out
|
|
950
|
+
|
|
951
|
+
@utils.precondition(_check_valid_solver)
|
|
952
|
+
def release_operands(self) -> None:
|
|
953
|
+
"""
|
|
954
|
+
{release_operands}
|
|
955
|
+
"""
|
|
956
|
+
if self._operands_released:
|
|
957
|
+
self._logger.info("Operands have already been released; nothing to do.")
|
|
958
|
+
return
|
|
959
|
+
|
|
960
|
+
# Drop the active-buffer references. Under no-mirror operands
|
|
961
|
+
# ``_lhs_compute`` / ``_rhs_compute`` alias the user's GPU
|
|
962
|
+
# tensor; under any "has mirror" case they point at the internal
|
|
963
|
+
# mirror. The slots themselves are guaranteed non-None here:
|
|
964
|
+
# ``_resolve_active_operand_buffers`` sets them in ``__init__``, and
|
|
965
|
+
# ``_check_valid_solver`` gates entry to this method on
|
|
966
|
+
# ``_valid_state == True`` (set only at end-of-``__init__``).
|
|
967
|
+
self._lhs_compute.tensor = None
|
|
968
|
+
self._rhs_compute.tensor = None
|
|
969
|
+
|
|
970
|
+
# Per-operand user-handle cleanup. ``_lhs_user`` / ``_rhs_user``
|
|
971
|
+
# hold the user's tensor wrapper for the mirror case; null its
|
|
972
|
+
# ``.tensor``. Each handle is non-None whenever the matching
|
|
973
|
+
# ``_need_*_mirror`` is True (set when the mirror is allocated and
|
|
974
|
+
# refreshed on reset_operands), so the asserts below always hold.
|
|
975
|
+
if self._spec.need_lhs_mirror:
|
|
976
|
+
assert self._lhs_user is not None
|
|
977
|
+
self._lhs_user.tensor = None
|
|
978
|
+
if self._spec.need_rhs_mirror:
|
|
979
|
+
assert self._rhs_user is not None
|
|
980
|
+
self._rhs_user.tensor = None
|
|
981
|
+
|
|
982
|
+
self._operands_released = True
|
|
983
|
+
self._logger.info("User-provided operands have been released.")
|
|
984
|
+
|
|
985
|
+
def _reset_operands_impl(
|
|
986
|
+
self,
|
|
987
|
+
*,
|
|
988
|
+
a_wrapped: TensorHolder | None,
|
|
989
|
+
b_wrapped: TensorHolder | None,
|
|
990
|
+
stream: utils.AnyStream | int | None,
|
|
991
|
+
) -> None:
|
|
992
|
+
if not self._spec.need_lhs_mirror and not self._spec.need_rhs_mirror:
|
|
993
|
+
# no mirror on either operand (cuda operands with
|
|
994
|
+
# ``inplace_a=True`` and ``inplace_b=True``)
|
|
995
|
+
if a_wrapped is not None:
|
|
996
|
+
self._lhs_compute.tensor = a_wrapped.tensor
|
|
997
|
+
if b_wrapped is not None:
|
|
998
|
+
self._rhs_compute.tensor = b_wrapped.tensor
|
|
999
|
+
|
|
1000
|
+
elif self._spec.operands_memory_space == self._spec.execution_space:
|
|
1001
|
+
# At least one side has a mirror, but both live on the
|
|
1002
|
+
# execution device -- no H2D is needed
|
|
1003
|
+
self._reset_operands_same_space(a_wrapped=a_wrapped, b_wrapped=b_wrapped, stream=stream)
|
|
1004
|
+
|
|
1005
|
+
else:
|
|
1006
|
+
# Cross-space (e.g. cpu operands, cuda execution): both
|
|
1007
|
+
# sides always have mirrors, the stream is always required
|
|
1008
|
+
# for the H2D copy.
|
|
1009
|
+
self._reset_operands_cross_space(a_wrapped=a_wrapped, b_wrapped=b_wrapped, stream=stream)
|
|
1010
|
+
|
|
1011
|
+
if a_wrapped is not None:
|
|
1012
|
+
# New A means the cached LU is stale; force factorize() to
|
|
1013
|
+
# run before the next solve().
|
|
1014
|
+
self._solver_factorized = False
|
|
1015
|
+
|
|
1016
|
+
# The mirror (or the live buffer, no-mirror case) now holds the
|
|
1017
|
+
# freshly pushed A, so the next factorize can skip the refresh copy.
|
|
1018
|
+
self._lhs_synced = True
|
|
1019
|
+
|
|
1020
|
+
if b_wrapped is not None:
|
|
1021
|
+
# The mirror (or the live buffer, no-mirror case) now holds the
|
|
1022
|
+
# freshly pushed b, so the next solve can skip the refresh copy.
|
|
1023
|
+
self._rhs_synced = True
|
|
1024
|
+
|
|
1025
|
+
self._operands_released = False
|
|
1026
|
+
|
|
1027
|
+
def _reset_operands_same_space(
|
|
1028
|
+
self,
|
|
1029
|
+
*,
|
|
1030
|
+
a_wrapped: TensorHolder | None,
|
|
1031
|
+
b_wrapped: TensorHolder | None,
|
|
1032
|
+
stream: utils.AnyStream | int | None,
|
|
1033
|
+
) -> None:
|
|
1034
|
+
# In this case, a side either has a same-device mirror (D2D copy needed)
|
|
1035
|
+
# or is pure-aliased. The stream is therefore
|
|
1036
|
+
# acquired lazily on the first mirror side we encounter.
|
|
1037
|
+
stream_holder = None
|
|
1038
|
+
for src, need_mirror, slot, user_slot_name in (
|
|
1039
|
+
(a_wrapped, self._spec.need_lhs_mirror, self._lhs_compute, "_lhs_user"),
|
|
1040
|
+
(b_wrapped, self._spec.need_rhs_mirror, self._rhs_compute, "_rhs_user"),
|
|
1041
|
+
):
|
|
1042
|
+
if src is None:
|
|
1043
|
+
continue
|
|
1044
|
+
if not need_mirror:
|
|
1045
|
+
# cuda + inplace=True: alias the user's GPU tensor.
|
|
1046
|
+
slot.tensor = src.tensor
|
|
1047
|
+
continue
|
|
1048
|
+
if stream_holder is None:
|
|
1049
|
+
stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
|
|
1050
|
+
if self._operands_released or slot.tensor is None:
|
|
1051
|
+
# No live mirror to copy into: either release_operands()
|
|
1052
|
+
# freed it, or a previous solve handed it to the user
|
|
1053
|
+
# (cuda + inplace_b=False). Allocate a fresh same-device
|
|
1054
|
+
# buffer and copy in.
|
|
1055
|
+
slot.tensor = self._create_compute_mirror(src, stream_holder).tensor
|
|
1056
|
+
else:
|
|
1057
|
+
slot.copy_(src, stream_holder=stream_holder)
|
|
1058
|
+
setattr(self, user_slot_name, src)
|
|
1059
|
+
|
|
1060
|
+
def _reset_operands_cross_space(
|
|
1061
|
+
self,
|
|
1062
|
+
*,
|
|
1063
|
+
a_wrapped: TensorHolder | None,
|
|
1064
|
+
b_wrapped: TensorHolder | None,
|
|
1065
|
+
stream: utils.AnyStream | int | None,
|
|
1066
|
+
) -> None:
|
|
1067
|
+
# Both sides always have a mirror (the user's bytes have to be
|
|
1068
|
+
# staged across address spaces), and the stream is unconditionally
|
|
1069
|
+
# needed for the H2D copy.
|
|
1070
|
+
|
|
1071
|
+
stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
|
|
1072
|
+
needs_fresh_mirror = self._operands_released
|
|
1073
|
+
if a_wrapped is not None:
|
|
1074
|
+
if needs_fresh_mirror:
|
|
1075
|
+
# release_operands() freed the previous mirror; allocate
|
|
1076
|
+
# a fresh execution-space buffer and H2D into it.
|
|
1077
|
+
self._lhs_compute = self._create_compute_mirror(a_wrapped, stream_holder)
|
|
1078
|
+
else:
|
|
1079
|
+
self._lhs_compute.copy_(a_wrapped, stream_holder=stream_holder)
|
|
1080
|
+
self._lhs_user = a_wrapped
|
|
1081
|
+
if b_wrapped is not None:
|
|
1082
|
+
if needs_fresh_mirror:
|
|
1083
|
+
self._rhs_compute = self._create_compute_mirror(b_wrapped, stream_holder)
|
|
1084
|
+
else:
|
|
1085
|
+
self._rhs_compute.copy_(b_wrapped, stream_holder=stream_holder)
|
|
1086
|
+
self._rhs_user = b_wrapped
|
|
1087
|
+
|
|
1088
|
+
def reset_operands_unchecked(
|
|
1089
|
+
self,
|
|
1090
|
+
*,
|
|
1091
|
+
a: Any = None,
|
|
1092
|
+
b: Any = None,
|
|
1093
|
+
stream: utils.AnyStream | int | None = None,
|
|
1094
|
+
) -> None:
|
|
1095
|
+
"""
|
|
1096
|
+
{reset_operands_unchecked}
|
|
1097
|
+
"""
|
|
1098
|
+
self._reset_operands_impl(
|
|
1099
|
+
a_wrapped=tensor_wrapper.wrap_operand(a) if a is not None else None,
|
|
1100
|
+
b_wrapped=tensor_wrapper.wrap_operand(b) if b is not None else None,
|
|
1101
|
+
stream=stream,
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
@utils.precondition(_check_valid_solver)
|
|
1105
|
+
def reset_operands(
|
|
1106
|
+
self,
|
|
1107
|
+
*,
|
|
1108
|
+
a: Any = None,
|
|
1109
|
+
b: Any = None,
|
|
1110
|
+
stream: utils.AnyStream | int | None = None,
|
|
1111
|
+
) -> None:
|
|
1112
|
+
"""
|
|
1113
|
+
Reset one or both operands held by this :class:`DirectSolver` instance.
|
|
1114
|
+
|
|
1115
|
+
Args:
|
|
1116
|
+
a: New left-hand side, or ``None`` to keep the previous left-hand side.
|
|
1117
|
+
|
|
1118
|
+
b: New right-hand side, or ``None`` to keep the previous right-hand side.
|
|
1119
|
+
|
|
1120
|
+
stream: {stream}
|
|
1121
|
+
|
|
1122
|
+
Semantics:
|
|
1123
|
+
- At least one operand is required (all of them after
|
|
1124
|
+
:meth:`release_operands`), otherwise a :class:`ValueError` is raised.
|
|
1125
|
+
|
|
1126
|
+
- This method will perform various checks on the new operands to make sure:
|
|
1127
|
+
|
|
1128
|
+
- The local shapes, strides, and datatypes match those of the old ones.
|
|
1129
|
+
- The packages that the operands belong to match those of the old ones.
|
|
1130
|
+
- The device must match that of the old ones.
|
|
1131
|
+
|
|
1132
|
+
- The distribution of each operand is fixed at construction and cannot be
|
|
1133
|
+
changed by :meth:`reset_operands`; the new local shapes and strides need
|
|
1134
|
+
to be consistent with that distribution.
|
|
1135
|
+
|
|
1136
|
+
- Resetting ``a`` invalidates the current factorization, so :meth:`factorize`
|
|
1137
|
+
must be called again before the next :meth:`solve`.
|
|
1138
|
+
|
|
1139
|
+
.. seealso::
|
|
1140
|
+
:meth:`reset_operands_unchecked`, :meth:`release_operands`.
|
|
1141
|
+
"""
|
|
1142
|
+
# if the operands have been released, both 'a' and 'b' must be provided
|
|
1143
|
+
if self._operands_released:
|
|
1144
|
+
if a is None or b is None:
|
|
1145
|
+
raise ValueError("After release_operands(), both 'a' and 'b' must be provided to reset_operands().")
|
|
1146
|
+
elif a is None and b is None:
|
|
1147
|
+
# All arguments are None: there is nothing to update, so reject the call.
|
|
1148
|
+
raise ValueError("reset_operands() requires at least one operand to be provided.")
|
|
1149
|
+
|
|
1150
|
+
a_wrapped = tensor_wrapper.wrap_operand(a) if a is not None else None
|
|
1151
|
+
b_wrapped = tensor_wrapper.wrap_operand(b) if b is not None else None
|
|
1152
|
+
|
|
1153
|
+
if a_wrapped is not None:
|
|
1154
|
+
self._spec.lhs_reset_invariants.check(a_wrapped)
|
|
1155
|
+
if b_wrapped is not None:
|
|
1156
|
+
self._spec.rhs_reset_invariants.check(b_wrapped)
|
|
1157
|
+
|
|
1158
|
+
self._reset_operands_impl(a_wrapped=a_wrapped, b_wrapped=b_wrapped, stream=stream)
|
|
1159
|
+
|
|
1160
|
+
if a_wrapped is not None:
|
|
1161
|
+
self._logger.info("User-provided lhs/a has been reset on rank %d.", self._runtime.rank)
|
|
1162
|
+
if b_wrapped is not None:
|
|
1163
|
+
self._logger.info("User-provided rhs/b has been reset on rank %d.", self._runtime.rank)
|
|
1164
|
+
|
|
1165
|
+
def _release_internal_resources(self) -> None:
|
|
1166
|
+
"""
|
|
1167
|
+
(private) Resource cleanup. Safe on partial init and on
|
|
1168
|
+
repeat calls; the user-facing :meth:`free` wraps this with the
|
|
1169
|
+
``_valid_state`` short-circuit.
|
|
1170
|
+
|
|
1171
|
+
NCCL communicator teardown is intentionally NOT done
|
|
1172
|
+
here because the comm is owned by ``nvmath.distributed`` and
|
|
1173
|
+
outlives individual solver instances.
|
|
1174
|
+
"""
|
|
1175
|
+
if self._device_workspace is not None:
|
|
1176
|
+
self._device_workspace.release(self._last_compute_event)
|
|
1177
|
+
self._device_workspace = None
|
|
1178
|
+
if self._host_workspace is not None:
|
|
1179
|
+
self._host_workspace.release(self._last_compute_event)
|
|
1180
|
+
self._host_workspace = None
|
|
1181
|
+
|
|
1182
|
+
# Release the persistent factorization buffers, ordering their
|
|
1183
|
+
# stream-ordered free after the last compute event (mirrors the
|
|
1184
|
+
# workspace release above) so teardown stays safe even if execution
|
|
1185
|
+
# ever goes non-blocking. Done before nulling _last_compute_event.
|
|
1186
|
+
if self._factorization_state is not None:
|
|
1187
|
+
self._factorization_state.free(self._last_compute_event)
|
|
1188
|
+
self._factorization_state = None
|
|
1189
|
+
|
|
1190
|
+
self._last_compute_event = None
|
|
1191
|
+
|
|
1192
|
+
# Matrix descriptors then grids then handles. ``_runtime`` is set before
|
|
1193
|
+
# the construction try, so it's present on any rollback; getattr keeps
|
|
1194
|
+
# this defensive if cleanup ever runs before it was assigned.
|
|
1195
|
+
runtime = getattr(self, "_runtime", None)
|
|
1196
|
+
if runtime is not None:
|
|
1197
|
+
with utils.device_ctx(runtime.device_id):
|
|
1198
|
+
if self._desc_b is not None:
|
|
1199
|
+
cusolverMp.destroy_matrix_desc(self._desc_b)
|
|
1200
|
+
self._desc_b = None
|
|
1201
|
+
if self._desc_a is not None:
|
|
1202
|
+
cusolverMp.destroy_matrix_desc(self._desc_a)
|
|
1203
|
+
self._desc_a = None
|
|
1204
|
+
if self._cusolvermp_grid_b is not None:
|
|
1205
|
+
cusolverMp.destroy_grid(self._cusolvermp_grid_b)
|
|
1206
|
+
self._cusolvermp_grid_b = None
|
|
1207
|
+
if self._cusolvermp_grid_a is not None:
|
|
1208
|
+
cusolverMp.destroy_grid(self._cusolvermp_grid_a)
|
|
1209
|
+
self._cusolvermp_grid_a = None
|
|
1210
|
+
# Handle is either user-supplied (user-owned) or cached
|
|
1211
|
+
# (owned by ``_caching`` and cleared in ``finalize()``).
|
|
1212
|
+
# The instance never destroys it.
|
|
1213
|
+
self._cusolvermp_handle = None
|
|
1214
|
+
|
|
1215
|
+
# Drop refs to every remaining slot. Skip ``_logger`` so the
|
|
1216
|
+
# "released" log line in free() still works; skip the lifecycle
|
|
1217
|
+
# flags (_valid_state and the _check_* gates: _solver_planned,
|
|
1218
|
+
# _solver_factorized, _operands_released) so post-free
|
|
1219
|
+
# preconditions still see the right values.
|
|
1220
|
+
_keep = {
|
|
1221
|
+
"_logger",
|
|
1222
|
+
"_valid_state",
|
|
1223
|
+
"_solver_planned",
|
|
1224
|
+
"_solver_factorized",
|
|
1225
|
+
"_operands_released",
|
|
1226
|
+
}
|
|
1227
|
+
for attr in self.__slots__:
|
|
1228
|
+
if attr not in _keep:
|
|
1229
|
+
setattr(self, attr, None)
|
|
1230
|
+
|
|
1231
|
+
def free(self) -> None:
|
|
1232
|
+
"""
|
|
1233
|
+
Free the :class:`DirectSolver` resources.
|
|
1234
|
+
|
|
1235
|
+
It is recommended that the :class:`DirectSolver` object be used within a context
|
|
1236
|
+
manager, but if that is not possible this method must be called explicitly to ensure
|
|
1237
|
+
the solver's resources are properly cleaned up.
|
|
1238
|
+
"""
|
|
1239
|
+
# Idempotent
|
|
1240
|
+
if not self._valid_state:
|
|
1241
|
+
return
|
|
1242
|
+
|
|
1243
|
+
try:
|
|
1244
|
+
self._release_internal_resources()
|
|
1245
|
+
except Exception:
|
|
1246
|
+
self._logger.critical(
|
|
1247
|
+
"Internal error: only part of the DirectSolver object's resources have been released.",
|
|
1248
|
+
)
|
|
1249
|
+
raise
|
|
1250
|
+
finally:
|
|
1251
|
+
self._valid_state = False
|
|
1252
|
+
|
|
1253
|
+
self._logger.info("The DirectSolver object's resources have been released.")
|
|
1254
|
+
|
|
1255
|
+
def __enter__(self) -> DirectSolver:
|
|
1256
|
+
"""Enter the context manager, returning this :class:`DirectSolver` instance."""
|
|
1257
|
+
return self
|
|
1258
|
+
|
|
1259
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
1260
|
+
"""Exit the context manager, releasing the solver's resources via :meth:`free`."""
|
|
1261
|
+
self.free()
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
@utils.docstring_decorator(GENERIC_DIRECT_SOLVER_DOCUMENTATION, skip_missing=False)
|
|
1265
|
+
def direct_solver(
|
|
1266
|
+
a: AnyTensor,
|
|
1267
|
+
b: AnyTensor,
|
|
1268
|
+
/,
|
|
1269
|
+
*,
|
|
1270
|
+
distributions: Sequence[Distribution],
|
|
1271
|
+
options: DirectSolverOptions | dict[str, Any] | None = None,
|
|
1272
|
+
stream: utils.AnyStream | int | None = None,
|
|
1273
|
+
) -> Any:
|
|
1274
|
+
"""
|
|
1275
|
+
Solve the distributed dense linear system :math:`a @ x = b` for :math:`x`. This
|
|
1276
|
+
function-form API is a convenience wrapper around the stateful :class:`DirectSolver`
|
|
1277
|
+
object and is meant for *single* use (the user needs to perform just one solve, for
|
|
1278
|
+
example).
|
|
1279
|
+
|
|
1280
|
+
Args:
|
|
1281
|
+
a: {a}
|
|
1282
|
+
|
|
1283
|
+
b: {b}
|
|
1284
|
+
|
|
1285
|
+
distributions: {distributions}
|
|
1286
|
+
|
|
1287
|
+
options: {options}
|
|
1288
|
+
|
|
1289
|
+
stream: {stream}
|
|
1290
|
+
|
|
1291
|
+
Returns:
|
|
1292
|
+
{result}
|
|
1293
|
+
|
|
1294
|
+
Semantics:
|
|
1295
|
+
{semantics}
|
|
1296
|
+
|
|
1297
|
+
Requirements:
|
|
1298
|
+
{requirements}
|
|
1299
|
+
|
|
1300
|
+
.. seealso::
|
|
1301
|
+
:class:`DirectSolver`, :class:`DirectSolverOptions`.
|
|
1302
|
+
|
|
1303
|
+
Examples:
|
|
1304
|
+
|
|
1305
|
+
>>> import numpy as np
|
|
1306
|
+
>>> import nvmath.distributed
|
|
1307
|
+
>>> from nvmath.distributed.distribution import Slab
|
|
1308
|
+
>>> from nvmath.distributed.linalg import direct_solver
|
|
1309
|
+
|
|
1310
|
+
Get the process group used to initialize nvmath.distributed (for information on
|
|
1311
|
+
initializing ``nvmath.distributed``, you can refer to the documentation or to the
|
|
1312
|
+
direct solver examples in `nvmath/examples/distributed/linalg/generic/direct_solver
|
|
1313
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/
|
|
1314
|
+
generic/direct_solver>`_):
|
|
1315
|
+
|
|
1316
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
1317
|
+
|
|
1318
|
+
Get my process rank and the total number of processes:
|
|
1319
|
+
|
|
1320
|
+
>>> rank = process_group.rank
|
|
1321
|
+
>>> nranks = process_group.nranks
|
|
1322
|
+
|
|
1323
|
+
We will solve the dense square system :math:`a @ x = b`, where :math:`a` has shape
|
|
1324
|
+
``(n, n)`` and :math:`b` has shape ``(n, nrhs)``. Both operands use a
|
|
1325
|
+
:class:`~nvmath.distributed.distribution.Slab` distribution, so each process owns a
|
|
1326
|
+
contiguous block of rows of the global matrices (chosen here for convenience of
|
|
1327
|
+
exposition; a :class:`~nvmath.distributed.distribution.BlockCyclic` distribution is
|
|
1328
|
+
generally preferred for :math:`a`, as it improves load balancing across processes).
|
|
1329
|
+
|
|
1330
|
+
Create the local row slabs on the CPU (cuSOLVERMp requires column-major), and make
|
|
1331
|
+
the global :math:`a` diagonally dominant so the system is well-conditioned. The
|
|
1332
|
+
diagonal entry ``a[i, i]`` of this rank's row band is at local position
|
|
1333
|
+
``[k, rank * local_n + k]``:
|
|
1334
|
+
|
|
1335
|
+
>>> n, nrhs = 256, 8
|
|
1336
|
+
>>> local_n = n // nranks # assume n is divisible by the process count
|
|
1337
|
+
>>> rng = np.random.default_rng(rank)
|
|
1338
|
+
>>> a = rng.random((local_n, n)).astype(np.float64, order="F")
|
|
1339
|
+
>>> b = rng.random((local_n, nrhs)).astype(np.float64, order="F")
|
|
1340
|
+
>>> idx = np.arange(local_n)
|
|
1341
|
+
>>> a[idx, rank * local_n + idx] += n
|
|
1342
|
+
|
|
1343
|
+
Solve the system with a single call to :func:`direct_solver`. By default the
|
|
1344
|
+
solution :math:`x` overwrites ``b`` in place and is returned, so ``x`` is a NumPy
|
|
1345
|
+
ndarray with the same distribution and local shape as ``b``:
|
|
1346
|
+
|
|
1347
|
+
>>> distributions = [Slab.X, Slab.X]
|
|
1348
|
+
>>> x = direct_solver(a, b, distributions=distributions)
|
|
1349
|
+
>>> assert x is b
|
|
1350
|
+
|
|
1351
|
+
Options can be provided to control the behavior of the operation using the
|
|
1352
|
+
``options`` argument (see :class:`DirectSolverOptions`).
|
|
1353
|
+
|
|
1354
|
+
Notes:
|
|
1355
|
+
- This function is a convenience wrapper around :class:`DirectSolver` and is
|
|
1356
|
+
specifically meant for *single* use. To amortize the cost of the factorization
|
|
1357
|
+
across multiple right-hand sides, use the stateful :class:`DirectSolver` API.
|
|
1358
|
+
|
|
1359
|
+
Further examples can be found in the
|
|
1360
|
+
`nvmath/examples/distributed/linalg/generic/direct_solver
|
|
1361
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/
|
|
1362
|
+
generic/direct_solver>`_
|
|
1363
|
+
directory.
|
|
1364
|
+
"""
|
|
1365
|
+
with DirectSolver(a, b, distributions=distributions, options=options, stream=stream) as solver:
|
|
1366
|
+
solver.plan(stream=stream)
|
|
1367
|
+
solver.factorize(stream=stream)
|
|
1368
|
+
return solver.solve(stream=stream)
|