| // Ceres Solver - A fast non-linear least squares minimizer | |
| // Copyright 2024 Google Inc. All rights reserved. | |
| // http://ceres-solver.org/ | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // * Redistributions of source code must retain the above copyright notice, | |
| // this list of conditions and the following disclaimer. | |
| // * Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // * Neither the name of Google Inc. nor the names of its contributors may be | |
| // used to endorse or promote products derived from this software without | |
| // specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |
| // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| // POSSIBILITY OF SUCH DAMAGE. | |
| // | |
| // Author: markshachkov@gmail.com (Mark Shachkov) | |
| #include "ceres/cuda_sparse_cholesky.h" | |
| #ifndef CERES_NO_CUDSS | |
| #include <cstddef> | |
| #include <iostream> | |
| #include <memory> | |
| #include <string> | |
| #include <type_traits> | |
| #include "Eigen/Core" | |
| #include "absl/log/check.h" | |
| #include "absl/log/log.h" | |
| #include "ceres/compressed_row_sparse_matrix.h" | |
| #include "ceres/cuda_buffer.h" | |
| #include "ceres/linear_solver.h" | |
| #include "cudss.h" | |
| namespace ceres::internal { | |
| // cuDSS 0.8 replaced cudaDataType_t with cudssDataType_t in the matrix | |
| // creation APIs. The enumerators of cudssDataType_t mirror the values of | |
| // their cudaDataType_t counterparts, but the two enum types are not | |
| // implicitly convertible, so pick the right type and enumerators at compile | |
| // time to stay compatible with both versions of the API. | |
| #if CUDSS_VERSION >= 800 | |
| using CuDSSDataType = cudssDataType_t; | |
| constexpr CuDSSDataType kCuDSSR32I = CUDSS_R_32I; | |
| constexpr CuDSSDataType kCuDSSR32F = CUDSS_R_32F; | |
| constexpr CuDSSDataType kCuDSSR64F = CUDSS_R_64F; | |
| #else | |
| using CuDSSDataType = cudaDataType_t; | |
| constexpr CuDSSDataType kCuDSSR32I = CUDA_R_32I; | |
| constexpr CuDSSDataType kCuDSSR32F = CUDA_R_32F; | |
| constexpr CuDSSDataType kCuDSSR64F = CUDA_R_64F; | |
| #endif // CUDSS_VERSION >= 800 | |
| inline std::string cuDSSStatusToString(cudssStatus_t status) { | |
| switch (status) { | |
| case CUDSS_STATUS_SUCCESS: | |
| return "CUDSS_STATUS_SUCCESS"; | |
| case CUDSS_STATUS_NOT_INITIALIZED: | |
| return "CUDSS_STATUS_NOT_INITIALIZED"; | |
| case CUDSS_STATUS_ALLOC_FAILED: | |
| return "CUDSS_STATUS_ALLOC_FAILED"; | |
| case CUDSS_STATUS_INVALID_VALUE: | |
| return "CUDSS_STATUS_INVALID_VALUE"; | |
| case CUDSS_STATUS_NOT_SUPPORTED: | |
| return "CUDSS_STATUS_NOT_SUPPORTED"; | |
| case CUDSS_STATUS_EXECUTION_FAILED: | |
| return "CUDSS_STATUS_EXECUTION_FAILED"; | |
| case CUDSS_STATUS_INTERNAL_ERROR: | |
| return "CUDSS_STATUS_INTERNAL_ERROR"; | |
| #if CUDSS_VERSION >= 800 | |
| case CUDSS_STATUS_IR_FAILED: | |
| return "CUDSS_STATUS_IR_FAILED"; | |
| #endif // CUDSS_VERSION >= 800 | |
| default: | |
| return "unknown cuDSS status: " + std::to_string(status); | |
| } | |
| } | |
| #define CUDSS_STATUS_CHECK(IN) \ | |
| if (cudssStatus_t status = IN; status != CUDSS_STATUS_SUCCESS) { \ | |
| CHECK(false) << "Got error: " << cuDSSStatusToString(status); \ | |
| } | |
| #define CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR(IN, additional_message) \ | |
| if (cudssStatus_t status = IN; status != CUDSS_STATUS_SUCCESS) { \ | |
| *message = std::string(additional_message) + \ | |
| " Got error: " + cuDSSStatusToString(status); \ | |
| return factorize_result_ = LinearSolverTerminationType::FATAL_ERROR; \ | |
| } | |
| #define CUDSS_STATUS_OK_OR_RETURN_CUDSS_STATUS(IN) \ | |
| if (cudssStatus_t status = IN; status != CUDSS_STATUS_SUCCESS) { \ | |
| return status; \ | |
| } | |
| class CERES_NO_EXPORT CuDSSMatrixBase { | |
| public: | |
| CuDSSMatrixBase() = default; | |
| CuDSSMatrixBase(const CuDSSMatrixBase&) = delete; | |
| CuDSSMatrixBase(CuDSSMatrixBase&&) = delete; | |
| CuDSSMatrixBase& operator=(const CuDSSMatrixBase&) = delete; | |
| CuDSSMatrixBase& operator=(CuDSSMatrixBase&&) = delete; | |
| ~CuDSSMatrixBase() { CUDSS_STATUS_CHECK(Free()); } | |
| cudssStatus_t Free() noexcept { | |
| if (matrix_) { | |
| const auto status = cudssMatrixDestroy(matrix_); | |
| matrix_ = nullptr; | |
| return status; | |
| } | |
| return CUDSS_STATUS_SUCCESS; | |
| } | |
| cudssMatrix_t Get() const noexcept { return matrix_; } | |
| protected: | |
| cudssMatrix_t matrix_{nullptr}; | |
| }; | |
| class CERES_NO_EXPORT CuDSSMatrixCSR : public CuDSSMatrixBase { | |
| public: | |
| cudssStatus_t Reset(int64_t num_rows, | |
| int64_t num_cols, | |
| int64_t num_nonzeros, | |
| void* rows_start, | |
| void* rows_end, | |
| void* cols, | |
| void* values, | |
| CuDSSDataType index_type, | |
| CuDSSDataType value_type, | |
| cudssMatrixType_t matrix_type, | |
| cudssMatrixViewType_t matrix_storage_type, | |
| cudssIndexBase_t index_base) { | |
| CUDSS_STATUS_OK_OR_RETURN_CUDSS_STATUS(Free()); | |
| return cudssMatrixCreateCsr(&matrix_, | |
| num_rows, | |
| num_cols, | |
| num_nonzeros, | |
| rows_start, | |
| rows_end, | |
| cols, | |
| values, | |
| #if CUDSS_VERSION >= 800 | |
| // cuDSS 0.8 takes the type of the row offsets | |
| // separately from the type of the column | |
| // indices; Ceres uses the same type for both. | |
| index_type, | |
| #endif // CUDSS_VERSION >= 800 | |
| index_type, | |
| value_type, | |
| matrix_type, | |
| matrix_storage_type, | |
| index_base); | |
| } | |
| }; | |
| class CERES_NO_EXPORT CuDSSMatrixDense : public CuDSSMatrixBase { | |
| public: | |
| cudssStatus_t Reset(int64_t num_rows, | |
| int64_t num_cols, | |
| int64_t leading_dimension_size, | |
| void* values, | |
| CuDSSDataType value_type, | |
| cudssLayout_t layout) { | |
| CUDSS_STATUS_OK_OR_RETURN_CUDSS_STATUS(Free()); | |
| return cudssMatrixCreateDn(&matrix_, | |
| num_rows, | |
| num_cols, | |
| leading_dimension_size, | |
| values, | |
| value_type, | |
| layout); | |
| } | |
| }; | |
| struct CudssContext { | |
| CudssContext(cudssHandle_t cudss_handle) : cudss_handle_(cudss_handle) { | |
| CUDSS_STATUS_CHECK(cudssConfigCreate(&solver_config_)); | |
| CUDSS_STATUS_CHECK(cudssDataCreate(cudss_handle_, &solver_data_)); | |
| } | |
| CudssContext(const CudssContext&) = delete; | |
| CudssContext(CudssContext&&) = delete; | |
| CudssContext& operator=(const CudssContext&) = delete; | |
| CudssContext& operator=(CudssContext&&) = delete; | |
| ~CudssContext() { | |
| CUDSS_STATUS_CHECK(cudssDataDestroy(cudss_handle_, solver_data_)); | |
| CUDSS_STATUS_CHECK(cudssConfigDestroy(solver_config_)); | |
| } | |
| cudssHandle_t cudss_handle_{nullptr}; | |
| cudssConfig_t solver_config_{nullptr}; | |
| cudssData_t solver_data_{nullptr}; | |
| }; | |
| template <typename Scalar> | |
| class CERES_NO_EXPORT CudaSparseCholeskyImpl final : public SparseCholesky { | |
| public: | |
| static_assert(std::is_same_v<Scalar, float> || std::is_same_v<Scalar, double>, | |
| "Scalar type is unsupported by cuDSS"); | |
| static constexpr CuDSSDataType kCuDSSScalar = | |
| std::is_same_v<Scalar, float> ? kCuDSSR32F : kCuDSSR64F; | |
| CudaSparseCholeskyImpl(ContextImpl* context) | |
| : context_(context), | |
| lhs_rows_d_(context_), | |
| lhs_cols_d_(context_), | |
| lhs_values_d_(context_), | |
| rhs_d_(context_), | |
| x_d_(context_) {} | |
| CudaSparseCholeskyImpl(const CudaSparseCholeskyImpl&) = delete; | |
| CudaSparseCholeskyImpl(CudaSparseCholeskyImpl&&) = delete; | |
| CudaSparseCholeskyImpl& operator=(const CudaSparseCholeskyImpl&) = delete; | |
| CudaSparseCholeskyImpl& operator=(CudaSparseCholeskyImpl&&) = delete; | |
| ~CudaSparseCholeskyImpl() = default; | |
| CompressedRowSparseMatrix::StorageType StorageType() const { | |
| return CompressedRowSparseMatrix::StorageType::LOWER_TRIANGULAR; | |
| } | |
| LinearSolverTerminationType Factorize(CompressedRowSparseMatrix* lhs, | |
| std::string* message) { | |
| if (lhs->num_rows() != lhs->num_cols()) { | |
| *message = "lhs matrix must be square"; | |
| return factorize_result_ = LinearSolverTerminationType::FATAL_ERROR; | |
| } | |
| if (lhs->storage_type() != StorageType()) { | |
| *message = "lhs matrix must be lower triangular"; | |
| return factorize_result_ = LinearSolverTerminationType::FATAL_ERROR; | |
| } | |
| // If, after previous attempt to factorize, cudssDataGet(CUDSS_DATA_INFO) | |
| // returned a numerical error, such error will be preserved by cuDSS 0.3.0 | |
| // and returned by cudssDataGet(CUDSS_DATA_INFO) even after correctly | |
| // factorizeable matrix is provided. Such behaviour forces us to reset a | |
| // cudssData_t object (managed by CudssContext) and to loose a result of | |
| // anylyze stage, thus we have to perform analyze one more time. | |
| // TODO: do not re-perform analyze in case of failed factorization numerics | |
| if (analyze_result_ != LinearSolverTerminationType::SUCCESS || | |
| factorize_result_ != LinearSolverTerminationType::SUCCESS) { | |
| analyze_result_ = Analyze(lhs, message); | |
| if (analyze_result_ != LinearSolverTerminationType::SUCCESS) { | |
| return analyze_result_; | |
| } | |
| } | |
| CHECK_NE(cudss_context_.get(), nullptr); | |
| ConvertAndCopyToDevice(lhs->values(), lhs_values_h_.data(), lhs_values_d_); | |
| CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR( | |
| cudssExecute(context_->cudss_handle_, | |
| CUDSS_PHASE_FACTORIZATION, | |
| cudss_context_->solver_config_, | |
| cudss_context_->solver_data_, | |
| cudss_lhs_.Get(), | |
| cudss_x_.Get(), | |
| cudss_rhs_.Get()), | |
| "cudssExecute with CUDSS_PHASE_FACTORIZATION failed"); | |
| int cudss_data_info; | |
| CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR( | |
| GetCudssDataInfo(cudss_data_info), | |
| "cudssDataGet with CUDSS_DATA_INFO failed"); | |
| const auto factorization_status = | |
| static_cast<cudssStatus_t>(cudss_data_info); | |
| if (factorization_status == CUDSS_STATUS_SUCCESS) { | |
| return factorize_result_ = LinearSolverTerminationType::SUCCESS; | |
| } | |
| if (cudss_data_info > 0) { | |
| return factorize_result_ = LinearSolverTerminationType::FAILURE; | |
| } | |
| return factorize_result_ = LinearSolverTerminationType::FATAL_ERROR; | |
| } | |
| LinearSolverTerminationType Solve(const double* rhs, | |
| double* solution, | |
| std::string* message) { | |
| CHECK_NE(cudss_context_.get(), nullptr); | |
| if (factorize_result_ != LinearSolverTerminationType::SUCCESS) { | |
| *message = "Factorize did not complete successfully previously."; | |
| return factorize_result_; | |
| } | |
| ConvertAndCopyToDevice(rhs, rhs_h_.data(), rhs_d_); | |
| CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR( | |
| cudssExecute(context_->cudss_handle_, | |
| CUDSS_PHASE_SOLVE, | |
| cudss_context_->solver_config_, | |
| cudss_context_->solver_data_, | |
| cudss_lhs_.Get(), | |
| cudss_x_.Get(), | |
| cudss_rhs_.Get()), | |
| "cudssExecute with CUDSS_PHASE_SOLVE failed"); | |
| ConvertAndCopyToHost(x_d_, x_h_.data(), solution); | |
| int cudss_data_info; | |
| CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR( | |
| GetCudssDataInfo(cudss_data_info), | |
| "cudssDataGet with CUDSS_DATA_INFO failed"); | |
| const auto solve_status = static_cast<cudssStatus_t>(cudss_data_info); | |
| if (solve_status != CUDSS_STATUS_SUCCESS) { | |
| return LinearSolverTerminationType::FAILURE; | |
| } | |
| return LinearSolverTerminationType::SUCCESS; | |
| } | |
| private: | |
| cudssStatus_t GetCudssDataInfo(int& cudss_data_info) { | |
| CHECK_NE(cudss_context_.get(), nullptr); | |
| std::size_t size_written = 0; | |
| CUDSS_STATUS_OK_OR_RETURN_CUDSS_STATUS( | |
| cudssDataGet(context_->cudss_handle_, | |
| cudss_context_->solver_data_, | |
| CUDSS_DATA_INFO, | |
| &cudss_data_info, | |
| sizeof(cudss_data_info), | |
| &size_written)); | |
| // TODO: enable following check after cudssDataGet will be fixed | |
| // CHECK_EQ(size_written, sizeof(cudss_data_info)); | |
| return CUDSS_STATUS_SUCCESS; | |
| } | |
| LinearSolverTerminationType Analyze(const CompressedRowSparseMatrix* lhs, | |
| std::string* message) { | |
| if (auto status = SetupCudssMatrices(lhs, message); | |
| status != LinearSolverTerminationType::SUCCESS) { | |
| return status; | |
| } | |
| lhs_rows_d_.CopyFromCpu(lhs->rows(), lhs->num_rows() + 1); | |
| lhs_cols_d_.CopyFromCpu(lhs->cols(), lhs->num_nonzeros()); | |
| // Analyze and factorization results are stored in cudssData_t (managed by | |
| // CudssContext). Given that cuDSS 0.3.0 does not reset it's error state in | |
| // case of failed numerics at factorization stage, we have to reset | |
| // cudssData_t and to recompute an analyze stage while trying to factorize a | |
| // rescaled matrix with the same structure. | |
| // TODO: move creation of CudssContext to ctor of CudaSparseCholeskyImpl | |
| cudss_context_ = std::make_unique<CudssContext>(context_->cudss_handle_); | |
| CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR( | |
| cudssExecute(context_->cudss_handle_, | |
| CUDSS_PHASE_ANALYSIS, | |
| cudss_context_->solver_config_, | |
| cudss_context_->solver_data_, | |
| cudss_lhs_.Get(), | |
| cudss_x_.Get(), | |
| cudss_rhs_.Get()), | |
| "cudssExecute with CUDSS_PHASE_ANALYSIS failed"); | |
| return LinearSolverTerminationType::SUCCESS; | |
| } | |
| // Resize buffers and setup cuDSS structs that describe the type and storage | |
| // configuration of linear system operands. | |
| LinearSolverTerminationType SetupCudssMatrices( | |
| const CompressedRowSparseMatrix* lhs, std::string* message) { | |
| const auto num_rows = lhs->num_rows(); | |
| const auto num_nonzeros = lhs->num_nonzeros(); | |
| if constexpr (std::is_same_v<Scalar, float>) { | |
| lhs_values_h_.Reserve(num_nonzeros); | |
| rhs_h_.Reserve(num_rows); | |
| x_h_.Reserve(num_rows); | |
| } | |
| lhs_rows_d_.Reserve(num_rows + 1); | |
| lhs_cols_d_.Reserve(num_nonzeros); | |
| lhs_values_d_.Reserve(num_nonzeros); | |
| rhs_d_.Reserve(num_rows); | |
| x_d_.Reserve(num_rows); | |
| static constexpr auto kFailedToCreateCuDSSMatrix = | |
| "cudssMatrixCreate() call failed"; | |
| CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR(cudss_lhs_.Reset(num_rows, | |
| num_rows, | |
| num_nonzeros, | |
| lhs_rows_d_.data(), | |
| nullptr, | |
| lhs_cols_d_.data(), | |
| lhs_values_d_.data(), | |
| kCuDSSR32I, | |
| kCuDSSScalar, | |
| CUDSS_MTYPE_SPD, | |
| CUDSS_MVIEW_LOWER, | |
| CUDSS_BASE_ZERO), | |
| kFailedToCreateCuDSSMatrix); | |
| CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR( | |
| cudss_rhs_.Reset(num_rows, | |
| 1, | |
| num_rows, | |
| rhs_d_.data(), | |
| kCuDSSScalar, | |
| CUDSS_LAYOUT_COL_MAJOR), | |
| kFailedToCreateCuDSSMatrix); | |
| CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR( | |
| cudss_x_.Reset(num_rows, | |
| 1, | |
| num_rows, | |
| x_d_.data(), | |
| kCuDSSScalar, | |
| CUDSS_LAYOUT_COL_MAJOR), | |
| kFailedToCreateCuDSSMatrix); | |
| return LinearSolverTerminationType::SUCCESS; | |
| } | |
| template <typename S, typename D> | |
| void Convert(const S* source, D* destination, size_t size) { | |
| Eigen::Map<Eigen::Matrix<D, Eigen::Dynamic, 1>>(destination, size) = | |
| Eigen::Map<const Eigen::Matrix<S, Eigen::Dynamic, 1>>(source, size) | |
| .template cast<D>(); | |
| } | |
| void ConvertAndCopyToDevice(const double* source, | |
| Scalar* intermediate, | |
| CudaBuffer<Scalar>& destination) { | |
| const auto size = destination.size(); | |
| if constexpr (std::is_same_v<Scalar, double>) { | |
| destination.CopyFromCpu(source, size); | |
| } else { | |
| Convert(source, intermediate, size); | |
| destination.CopyFromCpu(intermediate, size); | |
| } | |
| } | |
| void ConvertAndCopyToHost(const CudaBuffer<Scalar>& source, | |
| Scalar* intermediate, | |
| double* destination) { | |
| const auto size = source.size(); | |
| if constexpr (std::is_same_v<Scalar, double>) { | |
| source.CopyToCpu(destination, source.size()); | |
| } else { | |
| source.CopyToCpu(intermediate, source.size()); | |
| Convert(intermediate, destination, size); | |
| } | |
| } | |
| ContextImpl* context_{nullptr}; | |
| std::unique_ptr<CudssContext> cudss_context_; | |
| CuDSSMatrixCSR cudss_lhs_; | |
| CuDSSMatrixDense cudss_rhs_; | |
| CuDSSMatrixDense cudss_x_; | |
| CudaPinnedHostBuffer<Scalar> lhs_values_h_; | |
| CudaPinnedHostBuffer<Scalar> rhs_h_; | |
| CudaPinnedHostBuffer<Scalar> x_h_; | |
| CudaBuffer<int> lhs_rows_d_; | |
| CudaBuffer<int> lhs_cols_d_; | |
| CudaBuffer<Scalar> lhs_values_d_; | |
| CudaBuffer<Scalar> rhs_d_; | |
| CudaBuffer<Scalar> x_d_; | |
| LinearSolverTerminationType analyze_result_ = | |
| LinearSolverTerminationType::FATAL_ERROR; | |
| LinearSolverTerminationType factorize_result_ = | |
| LinearSolverTerminationType::FATAL_ERROR; | |
| }; | |
| template <typename Scalar> | |
| std::unique_ptr<SparseCholesky> CudaSparseCholesky<Scalar>::Create( | |
| ContextImpl* context, const OrderingType ordering_type) { | |
| if (ordering_type == OrderingType::NESDIS) { | |
| LOG(FATAL) | |
| << "Congratulations you have found a bug in Ceres Solver. Please " | |
| "report it to the Ceres Solver developers."; | |
| return nullptr; | |
| } | |
| if (context == nullptr || !context->IsCudaInitialized()) { | |
| LOG(FATAL) << "CudaSparseCholesky requires CUDA context to be initialized"; | |
| return nullptr; | |
| } | |
| return std::make_unique<CudaSparseCholeskyImpl<Scalar>>(context); | |
| } | |
| template class CudaSparseCholesky<float>; | |
| template class CudaSparseCholesky<double>; | |
| } // namespace ceres::internal | |
| #undef CUDSS_STATUS_CHECK | |
| #undef CUDSS_STATUS_OK_OR_RETURN_FATAL_ERROR | |
| #undef CUDSS_STATUS_OK_OR_RETURN_CUDSS_STATUS | |
| #endif // CERES_NO_CUDSS |