Add AMGX preconditioner support with GPU types and introduce backend agnostic AmgxInterface

This commit is contained in:
jakobtorben committed 2025-08-28 09:03:23 +02:00
1 parent 88baa80ad7
commit 2077903929
9 files changed
+1241 -183

No files matched your search

+2
View File
@@ -433,6 +433,7 @@ macro (tests_hook)
endif()
endif()
if(AMGX_FOUND)
list(APPEND tests_SOURCES tests/gpuistl/test_AmgxInterface.cpp)
list(APPEND tests_SOURCES tests/test_AmgxPreconditioner.cpp)
endif()
endmacro (tests_hook)
@@ -790,6 +791,7 @@ if(HYPRE_FOUND)
endif()
if(AMGX_FOUND)
set_tests_properties(AmgxInterface PROPERTIES LABELS gpu_cuda)
set_tests_properties(AmgxPreconditioner PROPERTIES LABELS gpu_cuda)
endif()
+2 -1
View File
@@ -336,6 +336,7 @@ if (HAVE_CUDA)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg detail/CuSparseResource.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg detail/CuSparseResource_impl.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg detail/safe_conversion.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg detail/gpu_type_detection.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg detail/cublas_wrapper.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg detail/cusparse_wrapper.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg detail/cusparse_constants.hpp)
@@ -358,8 +359,8 @@ if (HAVE_CUDA)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg PreconditionerCPUMatrixToGPUMatrix.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg ISTLSolverGPUISTL.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg detail/FlexibleSolverWrapper.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg AmgxInterface.hpp)
if(MPI_FOUND)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg GpuOwnerOverlapCopy.hpp)
ADD_CUDA_OR_HIP_FILE(PUBLIC_HEADER_FILES opm/simulators/linalg GpuSender.hpp)
+75 -130
View File
@@ -25,15 +25,17 @@
#include <opm/common/TimingMacros.hpp>
#include <opm/simulators/linalg/PreconditionerWithUpdate.hpp>
#include <opm/simulators/linalg/PropertyTree.hpp>
#include <opm/simulators/linalg/gpuistl/AmgxInterface.hpp>
#include <dune/common/fmatrix.hh>
#include <dune/istl/bcrsmatrix.hh>
#include <amgx_c.h>
#include <vector>
namespace Amgx
{
namespace Amgx {
using AmgxInterface = Opm::gpuistl::AmgxInterface;
/**
* @brief Configuration structure for AMGX parameters.
@@ -54,7 +56,8 @@ struct AmgxConfig {
double strength_threshold = 0.5;
int max_iters = 1;
explicit AmgxConfig(const Opm::PropertyTree& prm) {
explicit AmgxConfig(const Opm::PropertyTree& prm)
{
determinism_flag = prm.get<int>("determinism_flag", determinism_flag);
print_grid_stats = prm.get<int>("print_grid_stats", print_grid_stats);
print_solve_stats = prm.get<int>("print_solve_stats", print_solve_stats);
@@ -91,14 +94,16 @@ struct AmgxConfig {
*
* This class provides an interface to the AMG preconditioner from the AMGX library.
* It is designed to work with matrices, update vectors, and defect vectors specified
* by the template parameters.
* by the template parameters. The AmgxInterface class provides a unified interface
* to AMGX's functionality, allowing for easy switching between CPU and GPU input
* data types.
*
* @tparam M The matrix type the preconditioner is for.
* @tparam X The type of the update vector.
* @tparam Y The type of the defect vector.
* @tparam M The matrix type
* @tparam X The vector type for the solution
* @tparam Y The vector type for the right-hand side
*/
template<class M, class X, class Y>
class AmgxPreconditioner : public Dune::PreconditionerWithUpdate<X,Y>
template <class M, class X, class Y>
class AmgxPreconditioner : public Dune::PreconditionerWithUpdate<X, Y>
{
public:
//! \brief The matrix type the preconditioner is for
@@ -115,88 +120,59 @@ public:
static constexpr int block_size = 1;
/**
* @brief Constructor for the AmgxPreconditioner class.
* @brief Constructor for AmgxPreconditioner
*
* Initializes the preconditioner with the given matrix and property tree.
* Initializes the AMGX preconditioner with the given matrix and property tree.
*
* @param A The matrix for which the preconditioner is constructed.
* @param prm The property tree containing configuration parameters.
* @param A The matrix for which to construct the preconditioner
* @param prm The property tree containing configuration parameters
*/
AmgxPreconditioner(const M& A, const Opm::PropertyTree prm)
: A_(A)
, N_(A.N())
, nnz_(A.nonzeroes())
, setup_frequency_(prm.get<int>("setup_frequency", 30))
, update_counter_(0)
{
OPM_TIMEBLOCK(prec_construct);
// Create configuration
AmgxConfig config(prm);
AMGX_SAFE_CALL(AMGX_config_create(&cfg_, config.toString().c_str()));
AMGX_SAFE_CALL(AMGX_resources_create_simple(&rsrc_, cfg_));
cfg_ = AmgxInterface::createConfig(config.toString());
rsrc_ = AmgxInterface::createResources(cfg_);
// Setup frequency is set in the property tree
setup_frequency_ = prm.get<int>("setup_frequency", 30);
// Determine appropriate AMGX mode based on matrix and vector types
amgx_mode_ = AmgxInterface::determineAmgxMode<matrix_field_type, vector_field_type>();
// Select appropriate AMGX mode based on matrix and vector scalar types
AMGX_Mode amgx_mode;
if constexpr (std::is_same_v<matrix_field_type, double> && std::is_same_v<vector_field_type, double>) {
amgx_mode = AMGX_mode_dDDI;
} else if constexpr (std::is_same_v<matrix_field_type, float> && std::is_same_v<vector_field_type, double>) {
amgx_mode = AMGX_mode_dDFI;
} else if constexpr (std::is_same_v<matrix_field_type, float> && std::is_same_v<vector_field_type, float>) {
amgx_mode = AMGX_mode_dFFI;
} else {
OPM_THROW(std::runtime_error, "Unsupported combination of matrix and vector types in AmgxPreconditioner");
}
// Create solver, matrix, and vector handles, given the mode
solver_ = AmgxInterface::createSolver(rsrc_, amgx_mode_, cfg_);
A_amgx_ = AmgxInterface::createMatrix(rsrc_, amgx_mode_);
x_amgx_ = AmgxInterface::createVector(rsrc_, amgx_mode_);
b_amgx_ = AmgxInterface::createVector(rsrc_, amgx_mode_);
// Create solver and matrix/vector handles with selected mode
AMGX_SAFE_CALL(AMGX_solver_create(&solver_, rsrc_, amgx_mode, cfg_));
AMGX_SAFE_CALL(AMGX_matrix_create(&A_amgx_, rsrc_, amgx_mode));
AMGX_SAFE_CALL(AMGX_vector_create(&x_amgx_, rsrc_, amgx_mode));
AMGX_SAFE_CALL(AMGX_vector_create(&b_amgx_, rsrc_, amgx_mode));
// Initialize matrix structure and values
AmgxInterface::initializeMatrix(A_, A_amgx_);
// Setup matrix structure
std::vector<int> row_ptrs(N_ + 1);
std::vector<int> col_indices(nnz_);
setupSparsityPattern(row_ptrs, col_indices);
// Initialize vectors with proper dimensions
const int N = A_.N();
AmgxInterface::initializeVector(N, block_size, x_amgx_);
AmgxInterface::initializeVector(N, block_size, b_amgx_);
// initialize matrix with values
const matrix_field_type* values = &(A_[0][0][0][0]);
AMGX_SAFE_CALL(AMGX_pin_memory(const_cast<matrix_field_type*>(values), sizeof(matrix_field_type) * nnz_ * block_size * block_size));
AMGX_SAFE_CALL(AMGX_matrix_upload_all(A_amgx_, N_, nnz_, block_size, block_size,
row_ptrs.data(), col_indices.data(),
values, nullptr));
// Perform initial update
update();
}
/**
* @brief Destructor for the AmgxPreconditioner class.
* @brief Destructor for AmgxPreconditioner
*
* Cleans up resources allocated by the preconditioner.
*/
~AmgxPreconditioner()
{
const matrix_field_type* values = &(A_[0][0][0][0]);
AMGX_SAFE_CALL(AMGX_unpin_memory(const_cast<matrix_field_type*>(values)));
if (solver_) {
AMGX_SAFE_CALL(AMGX_solver_destroy(solver_));
}
if (x_amgx_) {
AMGX_SAFE_CALL(AMGX_vector_destroy(x_amgx_));
}
if (b_amgx_) {
AMGX_SAFE_CALL(AMGX_vector_destroy(b_amgx_));
}
if (A_amgx_) {
AMGX_SAFE_CALL(AMGX_matrix_destroy(A_amgx_));
}
// Destroying resources and config crashes when reinitializing
//if (rsrc_) {
// AMGX_SAFE_CALL(AMGX_resources_destroy(rsrc_));
//}
//if (cfg_) {
// AMGX_SAFE_CALL(AMGX_config_destroy(cfg_));
//}
AmgxInterface::destroySolver(solver_);
AmgxInterface::destroyVector(x_amgx_);
AmgxInterface::destroyVector(b_amgx_);
AmgxInterface::destroyMatrix(A_amgx_, A_);
AmgxInterface::destroyResources(rsrc_);
AmgxInterface::destroyConfig(cfg_);
}
/**
@@ -207,14 +183,16 @@ public:
* @param v The update vector.
* @param d The defect vector.
*/
void pre(X& /*v*/, Y& /*d*/) override {
void pre(X& /*v*/, Y& /*d*/) override
{
}
/**
* @brief Applies the preconditioner to a vector.
*
* Performs one AMG cycle to solve the system.
* Involves uploading vectors to AMGX, applying the preconditioner, and downloading the result.
* Involves uploading vectors to AMGX, applying the preconditioner,
* and transferring the result back to the vector.
*
* @param v The update vector.
* @param d The defect vector.
@@ -223,15 +201,15 @@ public:
{
OPM_TIMEBLOCK(prec_apply);
// Upload vectors to AMGX
AMGX_SAFE_CALL(AMGX_vector_upload(x_amgx_, N_, block_size, &v[0][0]));
AMGX_SAFE_CALL(AMGX_vector_upload(b_amgx_, N_, block_size, &d[0][0]));
// Transfer vectors to AMGX
AmgxInterface::transferVectorToAmgx(v, x_amgx_);
AmgxInterface::transferVectorToAmgx(d, b_amgx_);
// Apply preconditioner
AMGX_SAFE_CALL(AMGX_solver_solve(solver_, b_amgx_, x_amgx_));
// Download result
AMGX_SAFE_CALL(AMGX_vector_download(x_amgx_, &v[0][0]));
// Transfer result back
AmgxInterface::transferVectorFromAmgx(x_amgx_, v);
}
/**
@@ -241,7 +219,8 @@ public:
*
* @param v The update vector.
*/
void post(X& /*v*/) override {
void post(X& /*v*/) override
{
}
/**
@@ -252,7 +231,10 @@ public:
void update() override
{
OPM_TIMEBLOCK(prec_update);
copyMatrixToAmgx();
AmgxInterface::updateMatrixValues(A_, A_amgx_);
// Setup or resetup the solver based on update counter
if (update_counter_ == 0) {
AMGX_SAFE_CALL(AMGX_solver_setup(solver_, A_amgx_));
} else {
@@ -268,7 +250,7 @@ public:
/**
* @brief Returns the solver category.
*
* @return The solver category, which is sequential.
* @return The solver category (sequential).
*/
Dune::SolverCategory::Category category() const override
{
@@ -278,69 +260,32 @@ public:
/**
* @brief Checks if the preconditioner has a perfect update.
*
* @return True, indicating that the preconditioner can be perfectly updated.
* @return True, as the preconditioner can be perfectly updated.
*/
bool hasPerfectUpdate() const override
{
// The AMG hierarchy of the Amgx preconditioner can depend on the values of the matrix, so it must be recreated
// when the matrix values change, at given frequency. Since this is handled internally, we return true.
// when the matrix values change, at a given frequency. Since this is handled internally, we return true.
return true;
}
private:
/**
* @brief Sets up the sparsity pattern for the AMGX matrix.
*
* This method initializes the row pointers and column indices for the AMGX matrix.
*
* @param row_ptrs The row pointers for the AMGX matrix.
* @param col_indices The column indices for the AMGX matrix.
*/
void setupSparsityPattern(std::vector<int>& row_ptrs, std::vector<int>& col_indices)
{
int pos = 0;
row_ptrs[0] = 0;
for (auto row = A_.begin(); row != A_.end(); ++row) {
for (auto col = row->begin(); col != row->end(); ++col) {
col_indices[pos++] = col.index();
}
row_ptrs[row.index() + 1] = pos;
}
}
// Reference to the input matrix
const M& A_;
/**
* @brief Copies the matrix values to the AMGX matrix.
*
* This method updates the AMGX matrix with the current matrix values.
* The method assumes that the sparsity structure is the same and that
* the values are stored in a contiguous array.
*/
void copyMatrixToAmgx()
{
// Get direct pointer to matrix values
const matrix_field_type* values = &(A_[0][0][0][0]);
// Indexing explanation:
// A_[0] - First row of the matrix
// [0] - First block in that row
// [0] - First row within the 1x1 block
// [0] - First column within the 1x1 block
// update matrix with new values, assuming the sparsity structure is the same
AMGX_SAFE_CALL(AMGX_matrix_replace_coefficients(A_amgx_, N_, nnz_, values, nullptr));
}
// AMGX handles
AMGX_config_handle cfg_ = nullptr;
AMGX_resources_handle rsrc_ = nullptr;
AMGX_solver_handle solver_ = nullptr;
AMGX_matrix_handle A_amgx_ = nullptr;
AMGX_vector_handle x_amgx_ = nullptr;
AMGX_vector_handle b_amgx_ = nullptr;
AMGX_Mode amgx_mode_;
const M& A_; //!< The matrix for which the preconditioner is constructed.
const int N_; //!< Number of rows in the matrix.
const int nnz_; //!< Number of non-zero elements in the matrix.
// Internal variables to control AMGX setup and reuse frequency
int setup_frequency_ = -1; //!< Frequency of updating the AMG hierarchy
int update_counter_ = 0; //!< Counter for setup updates.
AMGX_config_handle cfg_ = nullptr; //!< The AMGX configuration handle.
AMGX_resources_handle rsrc_ = nullptr; //!< The AMGX resources handle.
AMGX_solver_handle solver_ = nullptr; //!< The AMGX solver handle.
AMGX_matrix_handle A_amgx_ = nullptr; //!< The AMGX matrix handle.
AMGX_vector_handle x_amgx_ = nullptr; //!< The AMGX solution vector handle.
AMGX_vector_handle b_amgx_ = nullptr; //!< The AMGX right-hand side vector handle.
// Frequency of updating the AMG hierarchy
int setup_frequency_;
// Counter for setup updates
int update_counter_;
};
} // namespace Amgx
@@ -89,8 +89,28 @@ struct StandardPreconditioners<Operator,
return std::make_shared<GPUDILU>(op.getmat(), cpuMatrix, split_matrix, tune_gpu_kernels, mixed_precision_scheme, reorder);
});
});
// Only add AMG preconditioners to the factory if the operator
// is an actual matrix operator.
if constexpr (std::is_same_v<O, Dune::MatrixAdapter<M, V, V>>) {
#if HAVE_AMGX
// Only add AMGX for scalar matrices
F::addCreator("amgx", [](const O& op, const P& prm, const std::function<V()>&, std::size_t) {
// Only create AMGX preconditioner for scalar matrices
if (op.getmat().blockSize() == 1) {
auto prm_copy = prm;
prm_copy.put("setup_frequency", Opm::Parameters::Get<Opm::Parameters::CprReuseInterval>());
return std::make_shared<Amgx::AmgxPreconditioner<M, V, V>>(op.getmat(), prm_copy);
} else {
OPM_THROW(std::logic_error, "AMGX preconditioner only works with scalar matrices (block size 1)");
}
});
#endif
}
}
private:
/**
@@ -0,0 +1,631 @@
/*
Copyright 2025 Equinor ASA
This file is part of the Open Porous Media project (OPM).
OPM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OPM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OPM. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef OPM_AMGX_INTERFACE_HPP
#define OPM_AMGX_INTERFACE_HPP
#include <opm/common/ErrorMacros.hpp>
#include <opm/simulators/linalg/gpuistl/GpuSparseMatrix.hpp>
#include <opm/simulators/linalg/gpuistl/GpuVector.hpp>
#include <opm/simulators/linalg/gpuistl/detail/gpu_safe_call.hpp>
#include <opm/simulators/linalg/gpuistl/detail/gpu_type_detection.hpp>
#include <amgx_c.h>
#include <cuda_runtime.h>
#include <fmt/core.h>
#include <string>
#include <vector>
namespace Opm::gpuistl
{
/**
* @brief Exception class for AMGX errors
*/
class AmgxError : public std::runtime_error
{
public:
explicit AmgxError(const std::string& msg)
: std::runtime_error(msg)
{
}
};
/**
* @brief Get a descriptive error message for an AMGX error code
*
* @param err The AMGX error code
* @param expression The AMGX expression that caused the error
* @param file The source file where the error occurred
* @param function The function where the error occurred
* @param line The line number where the error occurred
* @return std::string The formatted error message
*/
inline std::string
getAmgxErrorMessage(
AMGX_RC err, const std::string& expression, const std::string& file, const std::string& function, int line)
{
char amgx_err_msg[4096];
AMGX_get_error_string(err, amgx_err_msg, sizeof(amgx_err_msg));
return fmt::format(
"AMGX error in expression: {}\nError code: {}\nError message: {}\nLocation: {}:{} in function {}",
expression,
static_cast<int>(err),
amgx_err_msg,
file,
line,
function);
}
/**
* @brief Safe call wrapper for AMGX functions
*
* Checks the return code from AMGX functions and throws an AmgxError if an error occurred.
*
* @param rc The AMGX return code to check
* @param expression The expression being evaluated (for error reporting)
* @param file The source file (typically __FILE__)
* @param function The function name (typically __func__)
* @param line The line number (typically __LINE__)
* @throws AmgxError if the return code indicates an error
*/
inline void
amgxSafeCall(AMGX_RC rc, const std::string& expression, const std::string& file, const std::string& function, int line)
{
if (rc != AMGX_RC_OK) {
throw AmgxError(getAmgxErrorMessage(rc, expression, file, function, line));
}
}
/**
* @brief Macro to wrap AMGX function calls with error checking
*
* Example usage:
* @code
* OPM_AMGX_SAFE_CALL(AMGX_initialize());
* @endcode
*/
#define OPM_AMGX_SAFE_CALL(expr) ::Opm::gpuistl::amgxSafeCall((expr), #expr, __FILE__, __func__, __LINE__)
/**
* @brief Unified interface for AMGX operations with both CPU and GPU data structures
*
* This class provides utilities for working with AMGX resources and transferring
* data between CPU/GPU data structures and AMGX handles. It handles type detection
* and automatically chooses the most efficient transfer method:
* - For GPU types: Device-to-device transfers
* - For CPU types: Host-to-device transfers with memory pinning
*
* @note Error handling: All methods throw AmgxError exceptions when AMGX operations fail.
*/
class AmgxInterface
{
public:
/**
* @brief Initialize the AMGX library
*
* This should be called once at the start of the program before using any AMGX functionality.
* @throws AmgxError if initialization fails
*/
static void initialize()
{
OPM_AMGX_SAFE_CALL(AMGX_initialize());
}
/**
* @brief Finalize the AMGX library
*
* This should be called once at the end of the program to release AMGX resources.
* @throws AmgxError if finalization fails
*/
static void finalize()
{
OPM_AMGX_SAFE_CALL(AMGX_finalize());
}
/**
* @brief Create an AMGX config handle from a configuration string
*
* @param config_string Configuration string for AMGX
* @return AMGX_config_handle The created config handle
* @throws AmgxError if config creation fails
*/
static AMGX_config_handle createConfig(const std::string& config_string)
{
AMGX_config_handle config;
OPM_AMGX_SAFE_CALL(AMGX_config_create(&config, config_string.c_str()));
return config;
}
/**
* @brief Create AMGX resources from a config
*
* @param config The AMGX config handle
* @return AMGX_resources_handle The created resources handle
* @throws AmgxError if resource creation fails
*/
static AMGX_resources_handle createResources(AMGX_config_handle config)
{
AMGX_resources_handle resources;
OPM_AMGX_SAFE_CALL(AMGX_resources_create_simple(&resources, config));
return resources;
}
/**
* @brief Create an AMGX solver
*
* @param resources AMGX resources handle
* @param mode AMGX mode (precision configuration)
* @param config AMGX config handle
* @return AMGX_solver_handle The created solver handle
* @throws AmgxError if solver creation fails
*/
static AMGX_solver_handle createSolver(AMGX_resources_handle resources, AMGX_Mode mode, AMGX_config_handle config)
{
AMGX_solver_handle solver;
OPM_AMGX_SAFE_CALL(AMGX_solver_create(&solver, resources, mode, config));
return solver;
}
/**
* @brief Create an AMGX matrix
*
* @param resources AMGX resources handle
* @param mode AMGX mode (precision configuration)
* @return AMGX_matrix_handle The created matrix handle
* @throws AmgxError if matrix creation fails
*/
static AMGX_matrix_handle createMatrix(AMGX_resources_handle resources, AMGX_Mode mode)
{
AMGX_matrix_handle matrix;
OPM_AMGX_SAFE_CALL(AMGX_matrix_create(&matrix, resources, mode));
return matrix;
}
/**
* @brief Create an AMGX vector
*
* @param resources AMGX resources handle
* @param mode AMGX mode (precision configuration)
* @return AMGX_vector_handle The created vector handle
* @throws AmgxError if vector creation fails
*/
static AMGX_vector_handle createVector(AMGX_resources_handle resources, AMGX_Mode mode)
{
AMGX_vector_handle vector;
OPM_AMGX_SAFE_CALL(AMGX_vector_create(&vector, resources, mode));
return vector;
}
/**
* @brief Destroy an AMGX config handle
*
* @param config The config handle to destroy
* @throws AmgxError if config destruction fails
*/
static void destroyConfig(AMGX_config_handle config)
{
if (config) {
OPM_AMGX_SAFE_CALL(AMGX_config_destroy(config));
}
}
/**
* @brief Destroy an AMGX resources handle
*
* @param resources The resources handle to destroy
* @throws AmgxError if resource destruction fails
*
* @note There is a known issue where destroying resources and then
* reinitializing AMGX can cause crashes in some versions of the library.
* If you encounter this issue, consider keeping the resources alive for
* the entire program lifetime.
*/
static void destroyResources(AMGX_resources_handle resources)
{
if (resources) {
OPM_AMGX_SAFE_CALL(AMGX_resources_destroy(resources));
}
}
/**
* @brief Destroy an AMGX solver handle
*
* @param solver The solver handle to destroy
* @throws AmgxError if solver destruction fails
*/
static void destroySolver(AMGX_solver_handle solver)
{
if (solver) {
OPM_AMGX_SAFE_CALL(AMGX_solver_destroy(solver));
}
}
/**
* @brief Destroy an AMGX matrix handle
*
* @param matrix The matrix handle to destroy
* @throws AmgxError if matrix destruction fails
*/
template <typename MatrixType>
static void destroyMatrix(AMGX_matrix_handle amgx_matrix, const MatrixType& matrix)
{
if constexpr (!is_gpu_type<MatrixType>::value) {
// Unpin memory for CPU matrices
using T = typename MatrixType::field_type;
const T* values = &(matrix[0][0][0][0]);
OPM_AMGX_SAFE_CALL(AMGX_unpin_memory(const_cast<T*>(values)));
}
if (amgx_matrix) {
OPM_AMGX_SAFE_CALL(AMGX_matrix_destroy(amgx_matrix));
}
}
/**
* @brief Destroy an AMGX vector handle
*
* @param vector The vector handle to destroy
* @throws AmgxError if vector destruction fails
*/
static void destroyVector(AMGX_vector_handle vector)
{
if (vector) {
OPM_AMGX_SAFE_CALL(AMGX_vector_destroy(vector));
}
}
/**
* @brief Update an AMGX vector from a GpuVector (device-to-device transfer)
*
* Updates the AMGX vector with the contents of the GpuVector using direct device memory access.
* The AMGX vector must already be created with appropriate resources and mode.
*
* @param gpu_vec The source GpuVector
* @param amgx_vec The AMGX vector to update
* @throws AmgxError if the transfer fails or sizes mismatch
*/
template <typename T>
static void updateAmgxFromGpuVector(const GpuVector<T>& gpu_vec, AMGX_vector_handle amgx_vec)
{
// Get vector size from AMGX to verify compatibility
int n, block_dim;
OPM_AMGX_SAFE_CALL(AMGX_vector_get_size(amgx_vec, &n, &block_dim));
if (n > 0 && static_cast<size_t>(n * block_dim) != gpu_vec.dim()) {
throw AmgxError(fmt::format("Vector size mismatch in updateAmgxFromGpuVector: "
"AMGX vector size {} vs. GpuVector size {}",
n * block_dim,
gpu_vec.dim()));
}
// Get raw device pointer directly from GpuVector
const T* device_ptr = gpu_vec.data();
// Update AMGX vector directly with the device pointer
OPM_AMGX_SAFE_CALL(AMGX_vector_upload(amgx_vec, n, block_dim, const_cast<T*>(device_ptr)));
}
/**
* @brief Update a GpuVector from an AMGX vector (device-to-device transfer)
*
* Updates the GpuVector with the contents of the AMGX vector using direct device memory access.
*
* @param amgx_vec The source AMGX vector
* @param gpu_vec The GpuVector to update
* @throws AmgxError if the transfer fails or sizes mismatch
*/
template <typename T>
static void updateGpuVectorFromAmgx(AMGX_vector_handle amgx_vec, GpuVector<T>& gpu_vec)
{
// Get vector size from AMGX to verify compatibility
int n, block_dim;
OPM_AMGX_SAFE_CALL(AMGX_vector_get_size(amgx_vec, &n, &block_dim));
if (static_cast<size_t>(n * block_dim) != gpu_vec.dim()) {
throw AmgxError(fmt::format("Vector size mismatch in updateGpuVectorFromAmgx: "
"AMGX vector size {} vs. GpuVector size {}",
n * block_dim,
gpu_vec.dim()));
}
// Get destination device pointer from GpuVector
T* dst_device_ptr = gpu_vec.data();
// Download data directly from AMGX vector to GpuVector's device memory
OPM_AMGX_SAFE_CALL(AMGX_vector_download(amgx_vec, dst_device_ptr));
}
/**
* @brief Transfer vector to AMGX from any vector type (CPU or GPU)
*
* Selects the transfer method based on the vector type.
*
* @param vec Source vector (typically BlockVector (CPU) or GpuVector (GPU))
* @param amgx_vec Destination AMGX vector
* @throws AmgxError if the transfer fails
*/
template <typename VectorType>
static void transferVectorToAmgx(const VectorType& vec, AMGX_vector_handle amgx_vec)
{
if constexpr (is_gpu_type<VectorType>::value) {
// GPU implementation - device-to-device transfer
updateAmgxFromGpuVector(vec, amgx_vec);
} else {
// CPU implementation - host-to-device transfer
// For BlockVector, get dimensions
const int N = vec.size();
const int block_size = 1; // Assumes scalar vectors for CPU case
// Upload vector to AMGX
OPM_AMGX_SAFE_CALL(AMGX_vector_upload(amgx_vec, N, block_size, &vec[0][0]));
}
}
/**
* @brief Transfer vector from AMGX to any vector type (CPU or GPU)
*
* Selects the transfer method based on the vector type.
*
* @param amgx_vec Source AMGX vector
* @param vec Destination vector (typically BlockVector (CPU) or GpuVector (GPU))
* @throws AmgxError if the transfer fails
*/
template <typename VectorType>
static void transferVectorFromAmgx(AMGX_vector_handle amgx_vec, VectorType& vec)
{
if constexpr (is_gpu_type<VectorType>::value) {
// GPU implementation - device-to-device transfer
updateGpuVectorFromAmgx(amgx_vec, vec);
} else {
// CPU implementation - device-to-host transfer
OPM_AMGX_SAFE_CALL(AMGX_vector_download(amgx_vec, &vec[0][0]));
}
}
/**
* @brief Update an AMGX matrix from a GpuSparseMatrix (device-to-device transfer)
*
* Uploads the entire matrix structure and values from GpuSparseMatrix to AMGX.
*
* @param gpuSparseMatrix The source GpuSparseMatrix
* @param amgxMatrix The AMGX matrix to update
* @throws AmgxError if the transfer fails
*/
template <typename T>
static void updateAmgxMatrixFromGpuSparseMatrix(const GpuSparseMatrix<T>& gpuSparseMatrix,
AMGX_matrix_handle amgxMatrix)
{
// Get matrix dimensions and sparsity information
auto n = detail::to_int(gpuSparseMatrix.N());
auto nnz = detail::to_int(gpuSparseMatrix.nonzeroes());
auto block_size = detail::to_int(gpuSparseMatrix.blockSize());
// Get device pointers directly from GpuSparseMatrix
const T* values = gpuSparseMatrix.getNonZeroValues().data();
const int* row_ptrs = gpuSparseMatrix.getRowIndices().data();
const int* col_indices = gpuSparseMatrix.getColumnIndices().data();
// Update AMGX matrix with the device pointers
OPM_AMGX_SAFE_CALL(
AMGX_matrix_upload_all(amgxMatrix, n, nnz, block_size, block_size, row_ptrs, col_indices, values, nullptr));
}
/**
* @brief Update only the coefficient values of an AMGX matrix from a GpuSparseMatrix
*
* Updates just the coefficient values of an AMGX matrix without changing its sparsity pattern.
* This is more efficient when the matrix structure remains the same.
*
* @param gpuSparseMatrix The source GpuSparseMatrix with updated values
* @param amgxMatrix The AMGX matrix to update
* @throws AmgxError if the update fails
*/
template <typename T>
static void updateAmgxMatrixCoefficientsFromGpuSparseMatrix(const GpuSparseMatrix<T>& gpuSparseMatrix,
AMGX_matrix_handle amgxMatrix)
{
// Get matrix dimensions and sparsity information
auto n = detail::to_int(gpuSparseMatrix.N());
auto nnz = detail::to_int(gpuSparseMatrix.nonzeroes());
// Get device pointer to values directly from GpuSparseMatrix
const T* values = gpuSparseMatrix.getNonZeroValues().data();
// Update coefficients in AMGX matrix with the device pointer
OPM_AMGX_SAFE_CALL(AMGX_matrix_replace_coefficients(amgxMatrix, n, nnz, values, nullptr));
}
/**
* @brief Update a GpuSparseMatrix from an AMGX matrix (device-to-device transfer)
*
* Downloads the matrix data from AMGX and updates the GpuSparseMatrix with the values.
* The sparsity pattern is assumed to be identical, so only values are updated.
*
* @note This function is intended primarily for testing purposes and should not be used in performance-critical code paths,
* as it may involve unnecessary device-to-device transfers and temporary allocations.
*
* @param amgxMatrix The source AMGX matrix
* @param gpuSparseMatrix The GpuSparseMatrix to update
* @throws AmgxError if the transfer fails
*/
template <typename T>
static void updateGpuSparseMatrixFromAmgxMatrix(AMGX_matrix_handle amgxMatrix, GpuSparseMatrix<T>& gpuSparseMatrix)
{
// Get matrix dimensions from AMGX
int n, nnz, block_sizex, block_sizey;
OPM_AMGX_SAFE_CALL(AMGX_matrix_get_size(amgxMatrix, &n, &block_sizex, &block_sizey));
OPM_AMGX_SAFE_CALL(AMGX_matrix_get_nnz(amgxMatrix, &nnz));
// Allocate temporary device memory for row pointers and column indices (required by AMGX API)
int* temp_row_ptrs;
int* temp_col_indices;
OPM_GPU_SAFE_CALL(cudaMalloc(&temp_row_ptrs, (n + 1) * sizeof(int)));
OPM_GPU_SAFE_CALL(cudaMalloc(&temp_col_indices, nnz * sizeof(int)));
// Get device pointer to values in the GpuSparseMatrix
T* gpu_values = gpuSparseMatrix.getNonZeroValues().data();
// AMGX requires a valid pointer for diagonal data (even if unused)
void* diag_data_ptr = nullptr;
// Download matrix values directly from AMGX to GpuSparseMatrix
OPM_AMGX_SAFE_CALL(AMGX_matrix_download_all(amgxMatrix,
temp_row_ptrs,
temp_col_indices,
gpu_values,
&diag_data_ptr));
// Clean up temporary device memory
OPM_GPU_SAFE_CALL(cudaFree(temp_row_ptrs));
OPM_GPU_SAFE_CALL(cudaFree(temp_col_indices));
}
/**
* @brief Initialize an AMGX matrix from any matrix type (CPU or GPU)
*
* Selects the transfer method based on the matrix type.
*
* @param matrix Source matrix (typically BCRSMatrix (CPU) or GpuSparseMatrix (GPU))
* @param amgx_matrix Destination AMGX matrix
* @throws AmgxError if initialization fails
*/
template <typename MatrixType>
static void initializeMatrix(const MatrixType& matrix, AMGX_matrix_handle amgx_matrix)
{
if constexpr (is_gpu_type<MatrixType>::value) {
// GPU implementation - device-to-device transfer
updateAmgxMatrixFromGpuSparseMatrix(matrix, amgx_matrix);
} else {
// CPU implementation - host-to-device transfer
auto N = detail::to_int(matrix.N());
auto nnz = detail::to_int(matrix.nonzeroes());
const int block_size = 1; // Assumes scalar matrices for CPU case
// Setup sparsity pattern
std::vector<int> row_ptrs(N + 1);
std::vector<int> col_indices(nnz);
int pos = 0;
row_ptrs[0] = 0;
for (auto row = matrix.begin(); row != matrix.end(); ++row) {
for (auto col = row->begin(); col != row->end(); ++col) {
col_indices[pos++] = col.index();
}
row_ptrs[row.index() + 1] = pos;
}
// Get values pointer
using T = typename MatrixType::field_type;
const T* values = &(matrix[0][0][0][0]);
// Indexing explanation:
// matrix[0] - First row of the matrix
// [0] - First block in that row
// [0] - First row within the 1x1 block
// [0] - First column within the 1x1 block
// Pin memory for CPU-to-GPU transfers
OPM_AMGX_SAFE_CALL(AMGX_pin_memory(const_cast<T*>(values), sizeof(T) * nnz * block_size * block_size));
// Upload to AMGX
OPM_AMGX_SAFE_CALL(AMGX_matrix_upload_all(amgx_matrix,
N,
nnz,
block_size,
block_size,
row_ptrs.data(),
col_indices.data(),
const_cast<T*>(values),
nullptr));
}
}
/**
* @brief Initialize an AMGX vector with zeros
*
* Creates a zero-initialized vector of the appropriate size.
*
* @param N Size of the vector
* @param block_size Block size (typically 1 for scalar vectors)
* @param amgx_vector The AMGX vector to initialize
* @throws AmgxError if initialization fails
*/
static void initializeVector(int N, int block_size, AMGX_vector_handle amgx_vector)
{
OPM_AMGX_SAFE_CALL(AMGX_vector_set_zero(amgx_vector, N, block_size));
}
/**
* @brief Update matrix values in AMGX
*
* Selects the update method based on the matrix type.
* Updates the coefficients of an AMGX matrix, without changing its sparsity pattern.
*
* @param matrix Source matrix with updated values
* @param amgx_matrix AMGX matrix to update
* @throws AmgxError if the update fails
*/
template <typename MatrixType>
static void updateMatrixValues(const MatrixType& matrix, AMGX_matrix_handle amgx_matrix)
{
if constexpr (is_gpu_type<MatrixType>::value) {
// GPU implementation - device-to-device transfer
updateAmgxMatrixCoefficientsFromGpuSparseMatrix(matrix, amgx_matrix);
} else {
// CPU implementation - host-to-device transfer
using T = typename MatrixType::field_type;
const T* values = &(matrix[0][0][0][0]);
OPM_AMGX_SAFE_CALL(AMGX_matrix_replace_coefficients(
amgx_matrix, matrix.N(), matrix.nonzeroes(), const_cast<T*>(values), nullptr));
}
}
/**
* @brief Determine the appropriate AMGX mode based on matrix and vector field types
*
* @tparam MatrixFieldType The field type of the matrix
* @tparam VectorFieldType The field type of the vector
* @return AMGX_Mode The appropriate AMGX mode
* @throws std::runtime_error if the type combination is not supported
*/
template <typename MatrixFieldType, typename VectorFieldType>
static AMGX_Mode determineAmgxMode()
{
if constexpr (std::is_same_v<MatrixFieldType, double> && std::is_same_v<VectorFieldType, double>) {
return AMGX_mode_dDDI;
} else if constexpr (std::is_same_v<MatrixFieldType, float> && std::is_same_v<VectorFieldType, double>) {
return AMGX_mode_dDFI;
} else if constexpr (std::is_same_v<MatrixFieldType, float> && std::is_same_v<VectorFieldType, float>) {
return AMGX_mode_dFFI;
} else {
OPM_THROW(std::runtime_error,
"Unsupported combination of matrix and vector types for AMGX: "
+ std::string(typeid(MatrixFieldType).name()) + " and "
+ std::string(typeid(VectorFieldType).name()));
}
}
};
} // namespace Opm::gpuistl
#endif // OPM_AMGX_INTERFACE_HPP
@@ -0,0 +1,64 @@
/*
Copyright 2025 Equinor ASA
This file is part of the Open Porous Media project (OPM).
OPM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OPM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OPM. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef OPM_GPUISTL_GPU_TYPE_DETECTION_HPP
#define OPM_GPUISTL_GPU_TYPE_DETECTION_HPP
#include <type_traits>
namespace Opm::gpuistl
{
// Forward declarations
template <typename T>
class GpuVector;
template <typename T>
class GpuSparseMatrix;
template <typename T>
class GpuSparseMatrixGeneric;
/**
* @brief Type trait to detect if a type is a GPU type
*/
template <typename T>
struct is_gpu_type : std::false_type {
};
// Specializations for known GPU types
template <typename T>
struct is_gpu_type<GpuVector<T>> : std::true_type {
};
template <typename T>
struct is_gpu_type<GpuSparseMatrix<T>> : std::true_type {
};
template <typename T>
struct is_gpu_type<GpuSparseMatrixGeneric<T>> : std::true_type {
};
/**
* @brief Helper variable template for easier usage
*/
template <typename T>
inline constexpr bool is_gpu_type_v = is_gpu_type<T>::value;
} // namespace Opm::gpuistl
#endif // OPM_GPUISTL_GPU_TYPE_DETECTION_HPP
+155 -38
View File
@@ -31,13 +31,18 @@
#include <opm/simulators/linalg/AmgxPreconditioner.hpp>
#include <opm/simulators/linalg/PropertyTree.hpp>
#include <opm/simulators/linalg/gpuistl/GpuSparseMatrix.hpp>
#include <opm/simulators/linalg/gpuistl/GpuVector.hpp>
#include <opm/simulators/linalg/gpuistl/detail/gpu_type_detection.hpp>
template<class Matrix>
void setupLaplace2d(int N, Matrix& mat)
template <class Matrix>
void
setupLaplace2d(int N, Matrix& mat)
{
const int nonZeroes = N*N * 5; // max 5 entries per row (diagonal + 4 neighbors)
const int nonZeroes = N * N * 5; // max 5 entries per row (diagonal + 4 neighbors)
mat.setBuildMode(Matrix::row_wise);
mat.setSize(N*N, N*N, nonZeroes);
mat.setSize(N * N, N * N, nonZeroes);
// Set up sparsity pattern
for (auto row = mat.createbegin(); row != mat.createend(); ++row) {
@@ -45,15 +50,15 @@ void setupLaplace2d(int N, Matrix& mat)
int x = i % N;
int y = i / N;
row.insert(i); // diagonal
if (x > 0) // left neighbor
row.insert(i-1);
if (x < N-1) // right neighbor
row.insert(i+1);
if (y > 0) // upper neighbor
row.insert(i-N);
if (y < N-1) // lower neighbor
row.insert(i+N);
row.insert(i); // diagonal
if (x > 0) // left neighbor
row.insert(i - 1);
if (x < N - 1) // right neighbor
row.insert(i + 1);
if (y > 0) // upper neighbor
row.insert(i - N);
if (y < N - 1) // lower neighbor
row.insert(i + N);
}
// Fill the matrix with values
@@ -66,48 +71,63 @@ void setupLaplace2d(int N, Matrix& mat)
(*row)[i] = 4.0;
// Set off-diagonal entries
if (x > 0) // left neighbor
(*row)[i-1] = -1.0;
if (x < N-1) // right neighbor
(*row)[i+1] = -1.0;
if (y > 0) // upper neighbor
(*row)[i-N] = -1.0;
if (y < N-1) // lower neighbor
(*row)[i+N] = -1.0;
if (x > 0) // left neighbor
(*row)[i - 1] = -1.0;
if (x < N - 1) // right neighbor
(*row)[i + 1] = -1.0;
if (y > 0) // upper neighbor
(*row)[i - N] = -1.0;
if (y < N - 1) // lower neighbor
(*row)[i + N] = -1.0;
}
}
template<typename MatrixScalar, typename VectorScalar>
void testAmgxPreconditioner()
template <typename MatrixType>
MatrixType
createMatrix(int N)
{
if constexpr (!Opm::gpuistl::is_gpu_type_v<MatrixType>) {
MatrixType matrix;
setupLaplace2d(N, matrix);
return matrix;
} else {
// For GPU matrix, create CPU matrix first then convert
using ScalarType = typename MatrixType::field_type;
using CpuMatrix = Dune::BCRSMatrix<Dune::FieldMatrix<ScalarType, 1, 1>>;
CpuMatrix cpu_matrix;
setupLaplace2d(N, cpu_matrix);
return MatrixType::fromMatrix(cpu_matrix);
}
}
template <typename MatrixType, typename VectorType>
void
testAmgxPreconditionerGeneric()
{
constexpr int N = 100; // 100x100 grid
using Matrix = Dune::BCRSMatrix<Dune::FieldMatrix<MatrixScalar, 1, 1>>;
using Vector = Dune::BlockVector<Dune::FieldVector<VectorScalar, 1>>;
// Create matrix
Matrix matrix;
setupLaplace2d(N, matrix);
auto matrix = createMatrix<MatrixType>(N);
// Create vectors
Vector x(N*N), b(N*N);
x = 100.0; // Initial guess
b = 0.0; // RHS
VectorType x(N * N), b(N * N);
x = 0.0;
b = 1.0;
// Create operator
using Operator = Dune::MatrixAdapter<Matrix, Vector, Vector>;
Operator op(matrix);
// Set up AMGX parameters
Opm::PropertyTree prm;
using Operator = Dune::MatrixAdapter<MatrixType, VectorType, VectorType>;
auto op = std::make_shared<Operator>(matrix);
// Create preconditioner
auto prec = std::make_shared<Amgx::AmgxPreconditioner<Matrix, Vector, Vector>>(matrix, prm);
Opm::PropertyTree prm;
auto prec = std::make_shared<Amgx::AmgxPreconditioner<MatrixType, VectorType, VectorType>>(matrix, prm);
// Create solver
double reduction = 1e-8;
int maxit = 300;
int verbosity = 0;
Dune::LoopSolver<Vector> solver(op, *prec, reduction, maxit, verbosity);
Dune::LoopSolver<VectorType> solver(*op, *prec, reduction, maxit, verbosity);
// Solve
Dune::InverseOperatorResult res;
@@ -115,7 +135,104 @@ void testAmgxPreconditioner()
// Check convergence
BOOST_CHECK(res.converged);
BOOST_CHECK_LT(res.reduction, 1e-8);
BOOST_CHECK_LT(res.reduction, reduction);
std::vector<typename VectorType::field_type> x_values;
if constexpr (Opm::gpuistl::is_gpu_type_v<VectorType>) {
x_values = x.asStdVector();
} else {
x_values.assign(&x[0][0], &x[0][0] + x.dim());
}
// Validate solution - check that solution has expected spatial distribution
double center_value = x_values[N / 2 + (N / 2) * N];
double near_center = x_values[(N / 2 + 1) + (N / 2) * N];
double far_from_center = x_values[0]; // Corner
BOOST_CHECK_GT(center_value, 0.0);
BOOST_CHECK_LT(near_center, center_value);
BOOST_CHECK_LT(far_from_center, near_center);
}
template <typename MatrixType, typename VectorType>
void
testAmgxPreconditionerUpdateGeneric()
{
constexpr int N = 20;
// Create matrix
auto matrix = createMatrix<MatrixType>(N);
// Create vectors
VectorType x(N * N), b(N * N);
x = 0.0;
b = 1.0;
// Create preconditioner and apply it with initial values
Opm::PropertyTree prm;
auto prec = std::make_shared<Amgx::AmgxPreconditioner<MatrixType, VectorType, VectorType>>(matrix, prm);
prec->apply(x, b);
// Update matrix with new values and update the preconditioner
if constexpr (!Opm::gpuistl::is_gpu_type_v<MatrixType>) {
matrix *= 2.0;
} else {
matrix.getNonZeroValues() *= 2.0;
}
prec->update();
// Apply again after update but with same initial guess
VectorType x2(N * N);
x2 = 0.0;
prec->apply(x2, b);
std::vector<typename VectorType::field_type> x_values, x2_values;
if constexpr (Opm::gpuistl::is_gpu_type_v<VectorType>) {
x_values = x.asStdVector();
x2_values = x2.asStdVector();
} else {
x_values.assign(&x[0][0], &x[0][0] + x.dim());
x2_values.assign(&x2[0][0], &x2[0][0] + x2.dim());
}
// The vectors should be different after applying preconditioner with updated matrix
bool all_vectors_different = true;
for (size_t i = 0; i < x_values.size(); ++i) {
if (std::abs(x_values[i] - x2_values[i]) <= 1e-10) {
all_vectors_different = false;
break;
}
}
BOOST_CHECK(all_vectors_different);
}
template <typename Scalar, bool GpuInput = false>
void
testAmgxPreconditioner()
{
if constexpr (GpuInput) {
using Matrix = Opm::gpuistl::GpuSparseMatrix<Scalar>;
using Vector = Opm::gpuistl::GpuVector<Scalar>;
testAmgxPreconditionerGeneric<Matrix, Vector>();
} else {
using Matrix = Dune::BCRSMatrix<Dune::FieldMatrix<Scalar, 1, 1>>;
using Vector = Dune::BlockVector<Dune::FieldVector<Scalar, 1>>;
testAmgxPreconditionerGeneric<Matrix, Vector>();
}
}
template <typename Scalar, bool GpuInput = false>
void
testAmgxPreconditionerUpdate()
{
if constexpr (GpuInput) {
using Matrix = Opm::gpuistl::GpuSparseMatrix<Scalar>;
using Vector = Opm::gpuistl::GpuVector<Scalar>;
testAmgxPreconditionerUpdateGeneric<Matrix, Vector>();
} else {
using Matrix = Dune::BCRSMatrix<Dune::FieldMatrix<Scalar, 1, 1>>;
using Vector = Dune::BlockVector<Dune::FieldVector<Scalar, 1>>;
testAmgxPreconditionerUpdateGeneric<Matrix, Vector>();
}
}
#endif // TEST_AMGXPRECONDITIONER_HELPER_HPP
+252
View File
@@ -0,0 +1,252 @@
/*
Copyright 2025 Equinor ASA
This file is part of the Open Porous Media project (OPM).
OPM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OPM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OPM. If not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#define BOOST_TEST_MODULE TestAmgxInterface
#define BOOST_TEST_NO_MAIN
#include <boost/test/unit_test.hpp>
#include <opm/simulators/linalg/gpuistl/AmgxInterface.hpp>
#include <opm/simulators/linalg/gpuistl/GpuSparseMatrix.hpp>
#include <opm/simulators/linalg/gpuistl/GpuVector.hpp>
#include <dune/common/fmatrix.hh>
#include <dune/istl/bcrsmatrix.hh>
#include <cstddef>
#include <random>
#include <vector>
using namespace Opm::gpuistl;
// RAII class for managing AMGX resources during tests
class AmgxResourceGuard
{
public:
AmgxResourceGuard()
{
AmgxInterface::initialize();
cfg_ = AmgxInterface::createConfig("");
rsrc_ = AmgxInterface::createResources(cfg_);
}
~AmgxResourceGuard()
{
AmgxInterface::destroyResources(rsrc_);
AmgxInterface::destroyConfig(cfg_);
AmgxInterface::finalize();
}
AMGX_config_handle getConfig() const
{
return cfg_;
}
AMGX_resources_handle getResources() const
{
return rsrc_;
}
private:
AMGX_config_handle cfg_;
AMGX_resources_handle rsrc_;
};
// Test vector transfer functionality
BOOST_AUTO_TEST_CASE(TestVectorTransfer)
{
// Setup AMGX resources
AmgxResourceGuard resources;
// Create test data
const int n = 100;
std::vector<double> host_data(n);
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<double> dist(-100.0, 100.0);
// Initialize with random data
for (size_t i = 0; i < n; ++i) {
host_data[i] = dist(gen);
}
// Create GPU vector with test data
GpuVector<double> gpu_vec(host_data);
// Create AMGX vector
AMGX_vector_handle amgx_vec = AmgxInterface::createVector(resources.getResources(), AMGX_mode_dDDI);
// Set dimensions for the AMGX vector (needed before first transfer)
OPM_AMGX_SAFE_CALL(AMGX_vector_set_zero(amgx_vec, n, 1));
// Test transfer GPU -> AMGX
AmgxInterface::updateAmgxFromGpuVector(gpu_vec, amgx_vec);
// Create new GPU vector to receive AMGX data
GpuVector<double> result_vec(n);
// Test transfer AMGX -> GPU
AmgxInterface::updateGpuVectorFromAmgx(amgx_vec, result_vec);
// Verify data integrity
std::vector<double> result_data = result_vec.asStdVector();
BOOST_CHECK_EQUAL(result_data.size(), host_data.size());
for (size_t i = 0; i < n; ++i) {
BOOST_CHECK_CLOSE(result_data[i], host_data[i], 1e-10);
}
// Clean up
AmgxInterface::destroyVector(amgx_vec);
}
// Test matrix transfer functionality
BOOST_AUTO_TEST_CASE(TestMatrixTransfer)
{
// Setup AMGX resources
AmgxResourceGuard resources;
// Create a simple tridiagonal matrix
const int N = 10;
const int blockSize = 1;
using FieldMatrix = Dune::FieldMatrix<double, blockSize, blockSize>;
using BCRSMatrix = Dune::BCRSMatrix<FieldMatrix>;
BCRSMatrix dune_matrix(N, N, BCRSMatrix::row_wise);
// Set up matrix structure (tridiagonal)
for (auto row = dune_matrix.createbegin(); row != dune_matrix.createend(); ++row) {
const int i = row.index();
if (i > 0) {
row.insert(i - 1); // Lower diagonal
}
row.insert(i); // Diagonal
if (i < N - 1) {
row.insert(i + 1); // Upper diagonal
}
}
// Fill with values
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<double> dist(-10.0, 10.0);
for (int i = 0; i < N; ++i) {
dune_matrix[i][i] = 10.0 + std::abs(dist(gen)); // Diagonally dominant
if (i > 0) {
dune_matrix[i][i - 1] = dist(gen);
}
if (i < N - 1) {
dune_matrix[i][i + 1] = dist(gen);
}
}
// Convert to GpuSparseMatrix
auto gpu_matrix = GpuSparseMatrix<double>::fromMatrix(dune_matrix);
// Create AMGX matrix and upload data
AMGX_matrix_handle amgx_matrix = AmgxInterface::createMatrix(resources.getResources(), AMGX_mode_dDDI);
AmgxInterface::updateAmgxMatrixFromGpuSparseMatrix(gpu_matrix, amgx_matrix);
// Verify matrix dimensions after upload
int n, block_dimx, block_dimy;
OPM_AMGX_SAFE_CALL(AMGX_matrix_get_size(amgx_matrix, &n, &block_dimx, &block_dimy));
BOOST_CHECK_EQUAL(n, N);
BOOST_CHECK_EQUAL(block_dimx, blockSize);
BOOST_CHECK_EQUAL(block_dimy, blockSize);
// Test round-trip: upload to AMGX, then download back
auto gpu_matrix_copy = GpuSparseMatrix<double>::fromMatrix(dune_matrix);
AmgxInterface::updateGpuSparseMatrixFromAmgxMatrix(amgx_matrix, gpu_matrix_copy);
// Verify round-trip preserves original values
const auto original_values = gpu_matrix.getNonZeroValues().asStdVector();
const auto downloaded_values = gpu_matrix_copy.getNonZeroValues().asStdVector();
BOOST_CHECK_EQUAL(downloaded_values.size(), original_values.size());
for (std::size_t i = 0; i < original_values.size(); ++i) {
BOOST_CHECK_CLOSE(downloaded_values[i], original_values[i], 1e-10);
}
// Test coefficient update: modify GPU matrix and update AMGX coefficients only
gpu_matrix.getNonZeroValues() *= 2.0;
AmgxInterface::updateAmgxMatrixCoefficientsFromGpuSparseMatrix(gpu_matrix, amgx_matrix);
// Verify coefficient update worked
AmgxInterface::updateGpuSparseMatrixFromAmgxMatrix(amgx_matrix, gpu_matrix_copy);
const auto updated_values = gpu_matrix_copy.getNonZeroValues().asStdVector();
for (std::size_t i = 0; i < original_values.size(); ++i) {
BOOST_CHECK_CLOSE(updated_values[i], 2.0 * original_values[i], 1e-10);
}
// Clean up
AmgxInterface::destroyMatrix(amgx_matrix, gpu_matrix);
}
// Test templated utility functions
BOOST_AUTO_TEST_CASE(TestTemplatedUtilities)
{
// Test is_gpu_type trait
BOOST_CHECK(is_gpu_type<GpuVector<double>>::value);
BOOST_CHECK(is_gpu_type<GpuSparseMatrix<double>>::value);
BOOST_CHECK(!is_gpu_type<std::vector<double>>::value);
// Test AMGX mode determination
AMGX_Mode mode_dd = AmgxInterface::determineAmgxMode<double, double>();
BOOST_CHECK_EQUAL(mode_dd, AMGX_mode_dDDI);
AMGX_Mode mode_ff = AmgxInterface::determineAmgxMode<float, float>();
BOOST_CHECK_EQUAL(mode_ff, AMGX_mode_dFFI);
}
// Test error handling
BOOST_AUTO_TEST_CASE(TestErrorHandling)
{
// Setup AMGX resources
AmgxResourceGuard resources;
// Create vectors of different sizes
GpuVector<double> vec1(10);
GpuVector<double> vec2(5);
// Create AMGX vector
AMGX_vector_handle amgx_vec
= AmgxInterface::createVector(resources.getResources(), AmgxInterface::determineAmgxMode<double, double>());
// Set dimensions for the AMGX vector
OPM_AMGX_SAFE_CALL(AMGX_vector_set_zero(amgx_vec, 10, 1));
// This should work fine
AmgxInterface::updateAmgxFromGpuVector(vec1, amgx_vec);
// This should throw an exception due to size mismatch
BOOST_CHECK_THROW(AmgxInterface::updateGpuVectorFromAmgx(amgx_vec, vec2), AmgxError);
// Clean up
AmgxInterface::destroyVector(amgx_vec);
}
// Custom main function to properly initialize/finalize AMGX
int
main(int argc, char** argv)
{
return boost::unit_test::unit_test_main(init_unit_test, argc, argv);
}
+40 -14
View File
@@ -29,30 +29,56 @@
#include "AmgxPreconditionerTestHelper.hpp"
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerMatDoubleVecDouble)
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerCpuInputFloat)
{
testAmgxPreconditioner<double, double>();
testAmgxPreconditioner<float, false>();
}
// This test is disabled because it crashes the program with the following error:
// "Mixed precision modes not currently supported for CUDA 10.1 or later."
//BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerMatFloatVecDouble)
//{
// testAmgxPreconditioner<float, double>();
//}
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerMatFloatVecFloat)
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerCpuInputDouble)
{
testAmgxPreconditioner<float, float>();
testAmgxPreconditioner<double, false>();
}
bool init_unit_test_func()
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerUpdateCpuInputFloat)
{
testAmgxPreconditionerUpdate<float, false>();
}
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerUpdateCpuInputDouble)
{
testAmgxPreconditionerUpdate<double, false>();
}
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerGpuInputFloat)
{
testAmgxPreconditioner<float, true>();
}
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerGpuInputDouble)
{
testAmgxPreconditioner<double, true>();
}
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerUpdateGpuInputFloat)
{
testAmgxPreconditionerUpdate<float, true>();
}
BOOST_AUTO_TEST_CASE(TestAmgxPreconditionerUpdateGpuInputDouble)
{
testAmgxPreconditionerUpdate<double, true>();
}
bool
init_unit_test_func()
{
return true;
}
int main(int argc, char** argv)
int
main(int argc, char** argv)
{
AMGX_SAFE_CALL(AMGX_initialize());