Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
backends/cusolver/svd.hpp
Go to the documentation of this file.
1#pragma once
2
18
19#include <algorithm>
20#include <concepts>
21#include <cstddef>
22#include <limits>
23#include <memory>
24#include <optional>
25#include <type_traits>
26#include <utility>
27
28namespace uni20::linalg
29{
30namespace detail::cusolver_backend
31{
32
33template <class Scalar>
34concept CusolverSvdScalar = std::same_as<Scalar, float> || std::same_as<Scalar, double>;
35
36template <class Mdspec, class Scalar, std::size_t Rank>
37concept RawWritableCudaMdspec =
39 std::same_as<typename std::remove_cvref_t<Mdspec>::element_type, Scalar> &&
40 std::same_as<typename std::remove_cvref_t<Mdspec>::accessor_type, uni20::cuda::CudaPointerAccessor<Scalar>>;
41
42template <class Values, class Left, class Right, class Matrix>
43concept CusolverSvdMdspecs = CusolverSvdScalar<typename std::remove_cvref_t<Matrix>::element_type> &&
44 RawWritableCudaMdspec<Values, typename std::remove_cvref_t<Matrix>::element_type, 1> &&
45 RawWritableCudaMdspec<Left, typename std::remove_cvref_t<Matrix>::element_type, 2> &&
46 RawWritableCudaMdspec<Right, typename std::remove_cvref_t<Matrix>::element_type, 2> &&
47 RawWritableCudaMdspec<Matrix, typename std::remove_cvref_t<Matrix>::element_type, 2>;
48
49template <class Scalar> struct MatrixDescriptor
50{
51 cuda::CudaBufferView<Scalar> data;
52 int rows = 0;
53 int columns = 0;
54 int leading_dimension = 0;
55};
56
57template <CusolverSvdScalar Scalar> struct SvdPlan
58{
59 cuda::CudaBufferView<Scalar> singular_values;
60 MatrixDescriptor<Scalar> left;
61 MatrixDescriptor<Scalar> right_adjoint;
62 MatrixDescriptor<Scalar> matrix;
63 int rank = 0;
66 int device = -1;
67};
68
69template <class Mdspec> [[nodiscard]] auto try_matrix_descriptor(Mdspec& span)
70{
71 using scalar_type = typename std::remove_cvref_t<Mdspec>::element_type;
73 if (!stage || stage->unit_stride_axis != 0 || stage->needs_conjugation)
74 return std::optional<MatrixDescriptor<scalar_type>>{};
75 auto matrix = blas::blas_writable_matrix(*stage);
76 if (!std::in_range<int>(matrix.rows) || !std::in_range<int>(matrix.cols) ||
77 !std::in_range<int>(matrix.leading_dimension))
78 return std::optional<MatrixDescriptor<scalar_type>>{};
79 return std::optional{MatrixDescriptor<scalar_type>{.data = matrix.data,
80 .rows = static_cast<int>(matrix.rows),
81 .columns = static_cast<int>(matrix.cols),
82 .leading_dimension = static_cast<int>(matrix.leading_dimension)}};
83}
84
85template <class Scalar> [[nodiscard]] std::size_t required_elements(MatrixDescriptor<Scalar> const& matrix)
86{
87 if (matrix.rows == 0 || matrix.columns == 0) return 0;
88 auto const rows = static_cast<std::size_t>(matrix.rows);
89 auto const columns = static_cast<std::size_t>(matrix.columns);
90 auto const leading_dimension = static_cast<std::size_t>(matrix.leading_dimension);
91 CHECK(columns - 1 <= (std::numeric_limits<std::size_t>::max() - rows) / leading_dimension);
92 return (columns - 1) * leading_dimension + rows;
93}
94
95template <class View> void require_descriptor_range(View const& view, std::size_t required)
96{
97 auto const offset = view.element_offset();
98 auto const size = view.buffer().size();
99 CHECK(offset <= size && required <= size - offset, offset, required, size);
100}
101
102template <class Pointer> [[nodiscard]] Pointer offset_pointer(Pointer pointer, std::size_t offset)
103{
104 if (offset == 0) return pointer;
105 CHECK(pointer != nullptr, offset);
106 return pointer + offset;
107}
108
109template <CusolverSvdScalar Scalar> int query_workspace(cusolver::ExecutionLease& execution, int rows, int columns)
110{
111 int size = 0;
112 if constexpr (std::same_as<Scalar, float>)
113 cusolver::check(cusolverDnSgesvd_bufferSize(execution.handle().native_handle(), rows, columns, &size),
114 "cusolverDnSgesvd_bufferSize", execution.handle().device());
115 else
116 cusolver::check(cusolverDnDgesvd_bufferSize(execution.handle().native_handle(), rows, columns, &size),
117 "cusolverDnDgesvd_bufferSize", execution.handle().device());
118 CHECK(size >= 0, size);
119 return size;
120}
121
122template <class T> class StreamAllocation {
123 public:
124 StreamAllocation(cuda::Stream const& stream, std::size_t size, bool memory_pools_supported)
125 : stream_(&stream), stream_ordered_(memory_pools_supported)
126 {
127 if (size == 0) return;
128 CHECK(size <= std::numeric_limits<std::size_t>::max() / sizeof(T), size, sizeof(T));
129 cuda::ScopedDevice guard(stream.device());
130 if (stream_ordered_)
131 cuda::check(cudaMallocAsync(reinterpret_cast<void**>(&data_), size * sizeof(T), stream.native_handle()),
132 "cudaMallocAsync cuSOLVER workspace", stream.device());
133 else
134 cuda::check(cudaMalloc(reinterpret_cast<void**>(&data_), size * sizeof(T)), "cudaMalloc cuSOLVER workspace",
135 stream.device());
136 }
137
138 StreamAllocation(StreamAllocation const&) = delete;
139 StreamAllocation& operator=(StreamAllocation const&) = delete;
140 ~StreamAllocation() noexcept
141 {
142 if (data_ == nullptr) return;
143 try
144 {
145 cuda::ScopedDevice guard(stream_->device());
146 auto const status = stream_ordered_ ? cudaFreeAsync(data_, stream_->native_handle()) : cudaFree(data_);
147 if (status != cudaSuccess)
148 PANIC("cuSOLVER workspace cleanup failed", stream_->device(), cudaGetErrorName(status),
149 cudaGetErrorString(status));
150 }
151 catch (...)
152 {
153 PANIC("CUDA device selection failed while freeing cuSOLVER workspace", stream_->device());
154 }
155 }
156
157 [[nodiscard]] T* data() const noexcept { return data_; }
158
159 private:
160 cuda::Stream const* stream_ = nullptr;
161 T* data_ = nullptr;
162 bool stream_ordered_ = false;
163};
164
165template <CusolverSvdScalar Scalar>
166void invoke_gesvd(cusolver::ExecutionLease& execution, SvdPlan<Scalar> const& plan, Scalar* matrix,
167 Scalar* singular_values, Scalar* left, Scalar* right_adjoint, Scalar* workspace, int workspace_size,
168 int* info)
169{
170 signed char const jobu = static_cast<signed char>(plan.left_extent == SvdVectorExtent::Full ? 'A' : 'S');
171 signed char const jobvt = static_cast<signed char>(plan.right_extent == SvdVectorExtent::Full ? 'A' : 'S');
172 auto handle = execution.handle().native_handle();
173 if constexpr (std::same_as<Scalar, float>)
174 {
175 cusolver::check(cusolverDnSgesvd(handle, jobu, jobvt, plan.matrix.rows, plan.matrix.columns, matrix,
176 plan.matrix.leading_dimension, singular_values, left, plan.left.leading_dimension,
177 right_adjoint, plan.right_adjoint.leading_dimension, workspace, workspace_size,
178 nullptr, info),
179 "cusolverDnSgesvd", plan.device);
180 }
181 else
182 {
183 cusolver::check(cusolverDnDgesvd(handle, jobu, jobvt, plan.matrix.rows, plan.matrix.columns, matrix,
184 plan.matrix.leading_dimension, singular_values, left, plan.left.leading_dimension,
185 right_adjoint, plan.right_adjoint.leading_dimension, workspace, workspace_size,
186 nullptr, info),
187 "cusolverDnDgesvd", plan.device);
188 }
189}
190
191template <CusolverSvdScalar Scalar> KernelAttempt execute_svd(SvdPlan<Scalar> const& plan)
192{
193 auto& resources = plan.matrix.data.buffer().resources();
194 auto execution = cusolver::execution_pool(resources).acquire();
195 int const workspace_size = query_workspace<Scalar>(execution, plan.matrix.rows, plan.matrix.columns);
196 bool const memory_pools_supported = resources.device().capabilities().memory_pools_supported;
197 StreamAllocation<Scalar> workspace(execution.stream(), static_cast<std::size_t>(workspace_size),
198 memory_pools_supported);
199 StreamAllocation<int> device_info(execution.stream(), 1, memory_pools_supported);
200
201 auto values_access = plan.singular_values.buffer().write_synchronized_with(execution.stream());
202 auto left_access = plan.left.data.buffer().write_synchronized_with(execution.stream());
203 auto right_access = plan.right_adjoint.data.buffer().write_synchronized_with(execution.stream());
204 auto matrix_access = plan.matrix.data.buffer().write_synchronized_with(execution.stream());
205
206 auto* values = offset_pointer(values_access.data(), plan.singular_values.element_offset());
207 auto* left = offset_pointer(left_access.data(), plan.left.data.element_offset());
208 auto* right = offset_pointer(right_access.data(), plan.right_adjoint.data.element_offset());
209 auto* matrix = offset_pointer(matrix_access.data(), plan.matrix.data.element_offset());
210 invoke_gesvd(execution, plan, matrix, values, left, right, workspace.data(), workspace_size, device_info.data());
211
212 int info = 0;
213 cuda::check(cudaMemcpyAsync(&info, device_info.data(), sizeof(info), cudaMemcpyDeviceToHost,
214 execution.stream().native_handle()),
215 "cudaMemcpyAsync cuSOLVER devInfo", plan.device);
216 execution.stream().synchronize();
217 matrix_access.release_after_synchronization();
218 right_access.release_after_synchronization();
219 left_access.release_after_synchronization();
220 values_access.release_after_synchronization();
221 ERROR_IF(info < 0, "cuSOLVER gesvd reported an invalid parameter", -info);
222 ERROR_IF(info > 0, "cuSOLVER gesvd failed to converge", info);
224}
225
226template <class Values, class Left, class Right, class Matrix>
227 requires CusolverSvdMdspecs<Values, Left, Right, Matrix>
228KernelAttempt try_svd(svd_op const& operation, Values& values, Left& left, Right& right_adjoint, Matrix& matrix)
229{
230 using scalar_type = typename std::remove_cvref_t<Matrix>::element_type;
231 if (operation.overwrite != SvdOverwrite::None) return KernelAttempt::unsupported_instance;
232 if (matrix.extent(0) == 0 || matrix.extent(1) == 0) return KernelAttempt::unsupported_shape;
233 if (matrix.extent(0) < matrix.extent(1)) return KernelAttempt::unsupported_shape;
234
235 auto matrix_descriptor = try_matrix_descriptor(matrix);
236 auto left_descriptor = try_matrix_descriptor(left);
237 auto right_descriptor = try_matrix_descriptor(right_adjoint);
238 if (!matrix_descriptor || !left_descriptor || !right_descriptor) return KernelAttempt::unsupported_layout;
239 if (values.extent(0) > 1 && values.stride(0) != 1) return KernelAttempt::unsupported_layout;
240
241 int const rows = matrix_descriptor->rows;
242 int const columns = matrix_descriptor->columns;
243 int const rank = std::min(rows, columns);
244 int const left_columns = operation.left == SvdVectorExtent::Full ? rows : rank;
245 int const right_rows = operation.right == SvdVectorExtent::Full ? columns : rank;
246 CHECK_EQUAL(values.extent(0), rank);
247 CHECK_EQUAL(left_descriptor->rows, rows);
248 CHECK_EQUAL(left_descriptor->columns, left_columns);
249 CHECK_EQUAL(right_descriptor->rows, right_rows);
250 CHECK_EQUAL(right_descriptor->columns, columns);
251
252 auto values_view = values.data_descriptor();
253 int const device = matrix_descriptor->data.buffer().device().ordinal();
254 if (values_view.buffer().device().ordinal() != device ||
255 left_descriptor->data.buffer().device().ordinal() != device ||
256 right_descriptor->data.buffer().device().ordinal() != device)
258
259 auto const* matrix_buffer = std::addressof(matrix_descriptor->data.buffer());
260 CHECK(matrix_buffer != std::addressof(values_view.buffer()) &&
261 matrix_buffer != std::addressof(left_descriptor->data.buffer()) &&
262 matrix_buffer != std::addressof(right_descriptor->data.buffer()),
263 "cuSOLVER SVD matrix workspace must not alias an output buffer");
264 CHECK(std::addressof(values_view.buffer()) != std::addressof(left_descriptor->data.buffer()) &&
265 std::addressof(values_view.buffer()) != std::addressof(right_descriptor->data.buffer()) &&
266 std::addressof(left_descriptor->data.buffer()) != std::addressof(right_descriptor->data.buffer()),
267 "cuSOLVER SVD output buffers must be distinct");
268
269 require_descriptor_range(values_view, static_cast<std::size_t>(rank));
270 require_descriptor_range(matrix_descriptor->data, required_elements(*matrix_descriptor));
271 require_descriptor_range(left_descriptor->data, required_elements(*left_descriptor));
272 require_descriptor_range(right_descriptor->data, required_elements(*right_descriptor));
273
274 return execute_svd(SvdPlan<scalar_type>{.singular_values = values_view,
275 .left = *left_descriptor,
276 .right_adjoint = *right_descriptor,
277 .matrix = *matrix_descriptor,
278 .rank = rank,
279 .left_extent = operation.left,
280 .right_extent = operation.right,
281 .device = device});
282}
283
284} // namespace detail::cusolver_backend
285
287template <uni20::MutableRankedStridedMdspecLike<1> Values, uni20::MutableRankedStridedMdspecLike<2> Left,
288 uni20::MutableRankedStridedMdspecLike<2> Right, uni20::MutableRankedStridedMdspecLike<2> Matrix>
289consteval auto kernel_accepts_types(CusolverBackend const&, svd_op const&, Values&, Left&, Right&, Matrix&)
290{
291 if constexpr (detail::cusolver_backend::CusolverSvdMdspecs<Values, Left, Right, Matrix>)
292 return kernel_types_maybe;
293 else
294 return kernel_types_no;
295}
296
298template <uni20::MutableRankedStridedMdspecLike<1> Values, uni20::MutableRankedStridedMdspecLike<2> Left,
299 uni20::MutableRankedStridedMdspecLike<2> Right, uni20::MutableRankedStridedMdspecLike<2> Matrix>
300 requires detail::cusolver_backend::CusolverSvdMdspecs<Values, Left, Right, Matrix>
301KernelAttempt try_kernel(CusolverBackend, svd_op const& operation, Values& values, Left& left, Right& right,
302 Matrix& matrix)
303{
304 return detail::cusolver_backend::try_svd(operation, values, left, right, matrix);
305}
306
307} // namespace uni20::linalg
Typed CUDA device buffers and scoped stream access guards.
ExecutionLease acquire()
Definition cusolver/execution.cpp:193
Mutable strided mdspec-like type with a specified static rank.
Definition mdspan/concepts.hpp:524
CUDA-accessible mdspec backed by a CudaBufferView descriptor.
Definition cuda_storage.hpp:118
cuda::ResourceLease< HandleSlot > handle
Definition cublas/execution.cpp:33
CUDA-device-callable accessors for leased Tensor storage.
Structured CUDA runtime failures and checked-call helpers.
CUDA Tensor storage with deferred device-memory data descriptors.
cuSOLVER handle slots and dynamically paired stream execution leases.
Structured cuSOLVER failures and checked-call helpers.
Operation-tag backend dispatch over tensor-view operands.
Lower mdspan matrix views to provider-ready BLAS operands.
int columns(std::FILE *stream)
Return the terminal column count associated with a C stream.
Definition terminal.cpp:493
std::pair< int, int > size(std::FILE *stream)
Return the terminal size associated with a C stream.
Definition terminal.cpp:504
int rows(std::FILE *stream)
Return the terminal row count associated with a C stream.
Definition terminal.cpp:482
void check(cudaError_t status, std::string_view operation, std::optional< int > device, std::source_location where)
Raise when a CUDA runtime operation did not return cudaSuccess.
Definition cuda_error.cpp:44
void check(cusolverStatus_t status, std::string_view operation, int device, std::source_location where)
Raise unless a cuSOLVER call returned CUSOLVER_STATUS_SUCCESS.
Definition cusolver_error.cpp:59
ExecutionPool & execution_pool(cuda::DeviceResources &resources)
Return the lazily constructed cuSOLVER execution pool for one device.
Definition cusolver/execution.cpp:205
void info(checked_format_string< Args... > format, Args &&... args)
Emit an informational display message.
Definition display.hpp:393
stream
Selects the ordinary display stream for a message.
Definition display.hpp:25
void status(presentation::semantic_glyph glyph, checked_format_string< Args... > format, Args &&... args)
Emit a semantic status message through the active display sink.
Definition display.hpp:388
constexpr auto blas_writable_matrix(MdspanMatrixStage< Scalar, Handle > const &stage) -> BlasWritableMatrix< Scalar, Handle >
Return the provider-ready writable operand for a staged mdspan matrix.
Definition mdspan_matrix.hpp:225
auto try_mdspan_matrix_stage(Mdspan const &span) -> std::optional< MdspanMatrixStage< std::remove_cv_t< typename Mdspan::element_type >, detail::span_data_t< Mdspan > > >
Build a matrix staging descriptor when direct provider lowering is possible.
Definition mdspan_matrix.hpp:202
blas_int workspace_size(Scalar const &query)
Convert a provider workspace query result to the configured LAPACK integer type.
Definition linalg/backends/lapack/common.hpp:43
auto values(Tensor &tensor)
Definition packed_matrix.hpp:29
Definition linalg/async.hpp:28
KernelAttempt try_kernel(BlasBackend, gemm_op const &, C &c, S const &alpha, A &a, B &b, S const &beta)
Pack logical host values and execute GEMM through value-preserving provider conversion.
Definition linalg/backends/blas/binary80.hpp:30
KernelAttempt
Result of one runtime kernel backend attempt.
Definition kernel_attempt.hpp:27
constexpr KernelAcceptance< KernelTypeAcceptance::no > kernel_types_no
Report that a backend rejects the argument types.
Definition dispatch_error.hpp:34
SvdVectorExtent
Extent policy for one side of an exact singular value decomposition.
Definition operation_tags.hpp:248
consteval auto kernel_accepts_types(BlasBackend, gemm_op const &, C &, S const &, A &, B &, S const &)
Accept host-readable native binary80 operands, including transforming accessors.
Definition linalg/backends/blas/binary80.hpp:19
constexpr KernelAcceptance< KernelTypeAcceptance::maybe > kernel_types_maybe
Report that runtime operand values determine backend acceptance.
Definition dispatch_error.hpp:37
auto singular_values(BackendSelector selector, async::Async< MatrixTensor > const &matrix, std::optional< Precision > requested={})
Schedule preserving exact singular values through an explicit selector.
Definition async/svd.hpp:416
Backend-independent operation values used with normalized kernel operands.
std::size_t span
Definition presentation.cpp:2017
Accessor for a CUDA mdspan whose device pointer has been leased.
Definition cuda_accessor.hpp:80
Backend value for cuSOLVER dense factorization kernels.
Definition backend_selector.hpp:41
Exact dense singular value decomposition operation tag.
Definition operation_tags.hpp:292
#define CHECK(cond,...)
Definition trace.hpp:299
#define PANIC(...)
Definition trace.hpp:382
#define CHECK_EQUAL(a, b,...)
Definition trace.hpp:309
#define ERROR_IF(cond,...)
Definition trace.hpp:389