mirror of
https://github.com/OPM/opm-simulators.git
synced 2026-10-31 01:41:29 -05:00
Added a description of how to port a class to GPU
This commit is contained in:
1 parent
8509d5fa4d
commit
e0263534f3
1 file changed
+580
@@ -0,0 +1,580 @@
|
||||
# Porting a Composite Class to the GPU
|
||||
|
||||
This document describes how to port an existing composite C++ class
|
||||
(i.e. a class that *owns* one or more containers such as `std::vector`)
|
||||
to the GPU using the **`GpuBuffer`** / **`GpuView`** paradigm that is
|
||||
already established in `opm-simulators/opm/simulators/linalg/gpuistl/`.
|
||||
|
||||
The pattern is split across three repositories:
|
||||
|
||||
| Repository | Role |
|
||||
| --------------- | ------------------------------------------------------------------------------------- |
|
||||
| `opm-common` | Header-only physics / material classes that need to run on the GPU. |
|
||||
| `opm-grid` | Header-only grid / utility classes that need to run on the GPU (e.g. `SparseTable`). |
|
||||
| `opm-simulators`| Hosts `GpuBuffer`, `GpuView`, the CUDA/HIP build system, the kernels and **all** tests.|
|
||||
|
||||
> **Important:** Only `opm-simulators` actually compiles GPU code.
|
||||
> `opm-common` and `opm-grid` may only contain *header-only* GPU code,
|
||||
> and any GPU-specific helper (such as `copy_to_gpu` / `make_view`)
|
||||
> **must** be guarded by `#if HAVE_CUDA` (see `SparseTable.hpp` for an
|
||||
> example). The class itself stays GPU-agnostic; the GPU instantiation
|
||||
> happens through a template parameter for the storage container.
|
||||
|
||||
---
|
||||
|
||||
## 1. The two pillars: `GpuBuffer` and `GpuView`
|
||||
|
||||
`opm-simulators/opm/simulators/linalg/gpuistl/`
|
||||
|
||||
* **`GpuBuffer<T>`** — *owning* container. It allocates GPU memory in
|
||||
its constructor and frees it in its destructor (RAII). It is the
|
||||
GPU equivalent of `std::vector<T>` and lives on the **host** (the
|
||||
object itself is a host object, but the bytes it points to are on
|
||||
the device).
|
||||
* **`GpuView<T>`** — *non-owning* view over a contiguous block of GPU
|
||||
memory. It is essentially `{T* ptr; size_t size;}` and is **trivially
|
||||
copyable**, which is what makes it safe to pass *by value* into a
|
||||
`__global__` kernel.
|
||||
|
||||
The relationship is intentionally analogous to
|
||||
`std::vector<T>` ↔ `std::span<T>`.
|
||||
|
||||
### Ownership rules (read this before writing any code)
|
||||
|
||||
1. **`copy_to_gpu` returns an owning object built from `GpuBuffer`s.**
|
||||
The caller (CPU code) **must keep this object alive** for as long as
|
||||
the GPU is allowed to dereference the data. Destroying it frees the
|
||||
GPU memory.
|
||||
2. **`make_view` returns a non-owning object built from `GpuView`s.**
|
||||
The view object is the thing you pass into kernels. It does **not**
|
||||
extend the lifetime of the underlying buffers, so the corresponding
|
||||
buffer-holding object from step 1 must outlive every kernel that
|
||||
uses the view.
|
||||
|
||||
A typical CPU-side flow looks like this:
|
||||
|
||||
```cpp
|
||||
// 1. CPU object, fully populated and finalized.
|
||||
auto cpuParams = makeMyParams();
|
||||
|
||||
// 2. Owning GPU copy — return type is always MyParams<..., GpuBuffer>, no template arg needed.
|
||||
// Hold on to this for the entire lifetime of all kernel launches that need the data.
|
||||
auto gpuBufferParams = Opm::gpuistl::copy_to_gpu(cpuParams);
|
||||
|
||||
// 3. Non-owning view — return type is always MyParams<..., GpuView>, no template arg needed.
|
||||
// Cheap to copy, safe to pass to kernels.
|
||||
auto gpuViewParams = Opm::gpuistl::make_view(gpuBufferParams);
|
||||
|
||||
myKernel<<<grid, block>>>(gpuViewParams, /* … */); // pass by value
|
||||
OPM_GPU_SAFE_CALL(cudaDeviceSynchronize());
|
||||
|
||||
// 4. gpuBufferParams goes out of scope here → device memory is freed.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Decorating the class: `gpuDecorators.hpp`
|
||||
|
||||
`opm-common/opm/common/utility/gpuDecorators.hpp`
|
||||
|
||||
> When creating a GPU-compatible class, **use `OPM_HOST_DEVICE` instead
|
||||
> of `__host__ __device__`**. Include the header:
|
||||
>
|
||||
> ```cpp
|
||||
> #include <opm/common/utility/gpuDecorators.hpp>
|
||||
> ```
|
||||
|
||||
The header is safe to include unconditionally — when the GPU compiler
|
||||
is not active, the macros expand to nothing. The full list of macros it
|
||||
provides is:
|
||||
|
||||
| Macro | Expands to (GPU build) | Expands to (CPU build) | Purpose |
|
||||
| ------------------------------------ | ----------------------------- | ---------------------- | ----------------------------------------------------------------- |
|
||||
| `OPM_HOST_DEVICE` | `__device__ __host__` | *(empty)* | Decorate functions callable from both CPU and a CUDA/HIP kernel. |
|
||||
| `OPM_DEVICE` | `__device__` | *(empty)* | Decorate functions only callable from the device. |
|
||||
| `OPM_HOST` | `__host__` | *(empty)* | Included for completeness; **rarely needed** in practice — most host-only logic is compiled by the host compiler without any decorator. |
|
||||
| `OPM_IS_USING_GPU` | `1` | `0` | Compile-time constant: GPU support enabled (`HAVE_CUDA`) or not. |
|
||||
| `OPM_IS_COMPILING_WITH_GPU_COMPILER` | `1` under `nvcc` / `hipcc` | `0` | True when the current TU is being processed by `nvcc` or `hipcc`. |
|
||||
| `OPM_IS_INSIDE_DEVICE_FUNCTION` | `1` inside a device function | `0` | Useful for `if constexpr`-style specialization within a function. |
|
||||
| `OPM_IS_INSIDE_HOST_FUNCTION` | `0` inside a device function | `1` | The complement of the above. |
|
||||
|
||||
When `HAVE_CUDA` is set the header also pulls in either
|
||||
`<cuda_runtime.h>` or `<hip/hip_runtime.h>` (selected via `USE_HIP`).
|
||||
|
||||
### Exceptions: use `OPM_THROW`
|
||||
|
||||
Always use **`OPM_THROW`** (from `<opm/common/ErrorMacros.hpp>`) for
|
||||
error reporting; do not use raw `throw`. `OPM_THROW` is GPU-aware and
|
||||
behaves correctly in code shared between host and device:
|
||||
|
||||
```cpp
|
||||
#include <opm/common/ErrorMacros.hpp>
|
||||
|
||||
OPM_HOST_DEVICE double safeDiv(double a, double b)
|
||||
{
|
||||
if (b == 0.0) {
|
||||
OPM_THROW(std::runtime_error, "Division by zero");
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
```
|
||||
|
||||
There is a dedicated test (`tests/gpuistl/test_throw_macros_on_gpu.cu`)
|
||||
that validates this behaviour.
|
||||
|
||||
---
|
||||
|
||||
## 3. Templating the class on its storage container
|
||||
|
||||
The trick that makes the same class instantiable on CPU *and* GPU is
|
||||
to template it on the container type using a **template template
|
||||
parameter**, defaulting to `VectorWithDefaultAllocator`.
|
||||
The same class is then instantiated three times:
|
||||
|
||||
| Instantiation | Storage type | Lives on |
|
||||
| ------------------------------------ | ----------------------------- | -------- |
|
||||
| `MyClass<Traits>` | `VectorWithDefaultAllocator<Scalar>` | CPU |
|
||||
| `MyClass<Traits, GpuBuffer>` | `GpuBuffer<Scalar>` (owning) | GPU mem, CPU object |
|
||||
| `MyClass<Traits, GpuView>` | `GpuView<Scalar>` | GPU (passed to kernel) |
|
||||
|
||||
The canonical reference implementation is
|
||||
`opm-common/opm/material/fluidmatrixinteractions/PiecewiseLinearTwoPhaseMaterialParams.hpp`.
|
||||
Other examples worth studying:
|
||||
|
||||
* `opm-common/opm/material/common/UniformTabulated2DFunction.hpp`
|
||||
* `opm-common/opm/material/components/CO2Tables.hpp`
|
||||
* `opm-common/opm/material/fluidsystems/blackoilpvt/BrineCo2Pvt.hpp`
|
||||
* `opm-grid/opm/grid/utility/SparseTable.hpp`
|
||||
|
||||
### Skeleton of a GPU-portable composite class
|
||||
|
||||
```cpp
|
||||
// my_composite.hpp (lives in opm-common or opm-grid; HEADER ONLY)
|
||||
#include <opm/common/ErrorMacros.hpp>
|
||||
#include <opm/common/utility/gpuDecorators.hpp>
|
||||
#include <opm/common/utility/VectorWithDefaultAllocator.hpp>
|
||||
|
||||
// Use template<class> class Storage (template template parameter) so that
|
||||
// GpuBuffer, GpuView, and VectorWithDefaultAllocator can all be passed
|
||||
// without spelling out the scalar type at the call site.
|
||||
template <class Traits, template<class> class Storage = VectorWithDefaultAllocator>
|
||||
class MyComposite;
|
||||
|
||||
// Forward-declare the GPU helpers in their final namespace so that we
|
||||
// can friend them inside the class.
|
||||
// Both are guarded by HAVE_CUDA; they always return GpuBuffer / GpuView
|
||||
// instantiations — the caller never needs to spell out the storage type.
|
||||
namespace Opm::gpuistl {
|
||||
template <class Traits>
|
||||
MyComposite<Traits, GpuBuffer>
|
||||
copy_to_gpu(const MyComposite<Traits>& cpu);
|
||||
|
||||
template <class Traits, template<class> class ContainerT>
|
||||
MyComposite<Traits, GpuView>
|
||||
make_view(MyComposite<Traits, ContainerT>& gpuBuffers);
|
||||
}
|
||||
|
||||
namespace Opm {
|
||||
|
||||
template <class Traits, template<class> class Storage>
|
||||
class MyComposite
|
||||
{
|
||||
using Scalar = typename Traits::Scalar;
|
||||
using VectorT = Storage<Scalar>; // concrete type for this instantiation
|
||||
public:
|
||||
MyComposite() = default;
|
||||
MyComposite(VectorT xs, VectorT ys)
|
||||
: xs_(std::move(xs)), ys_(std::move(ys))
|
||||
{
|
||||
if (xs_.size() != ys_.size()) {
|
||||
OPM_THROW(std::invalid_argument,
|
||||
"xs and ys must have equal size");
|
||||
}
|
||||
}
|
||||
|
||||
OPM_HOST_DEVICE const VectorT& xs() const { return xs_; }
|
||||
OPM_HOST_DEVICE const VectorT& ys() const { return ys_; }
|
||||
|
||||
OPM_HOST_DEVICE Scalar evaluate(Scalar x) const
|
||||
{
|
||||
// ... uses xs_/ys_ via operator[] which is also OPM_HOST_DEVICE
|
||||
}
|
||||
|
||||
private:
|
||||
// Friends must match the forward declarations exactly.
|
||||
template <class T>
|
||||
friend MyComposite<T, GpuBuffer>
|
||||
Opm::gpuistl::copy_to_gpu(const MyComposite<T>& cpu);
|
||||
|
||||
template <class T, template<class> class ContainerT>
|
||||
friend MyComposite<T, GpuView>
|
||||
Opm::gpuistl::make_view(MyComposite<T, ContainerT>& gpuBuffers);
|
||||
|
||||
VectorT xs_;
|
||||
VectorT ys_;
|
||||
};
|
||||
|
||||
} // namespace Opm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Writing `copy_to_gpu`
|
||||
|
||||
`copy_to_gpu` returns an **owning** GPU object. It is the moment when
|
||||
host memory is transferred to the device.
|
||||
|
||||
```cpp
|
||||
// Still in the same header, but inside namespace Opm::gpuistl and
|
||||
// guarded by HAVE_CUDA in repos that do not compile GPU code:
|
||||
#if HAVE_CUDA
|
||||
#include <opm/simulators/linalg/gpuistl/GpuBuffer.hpp>
|
||||
|
||||
namespace Opm::gpuistl {
|
||||
|
||||
template <class Traits>
|
||||
MyComposite<Traits, GpuBuffer>
|
||||
copy_to_gpu(const MyComposite<Traits>& cpu)
|
||||
{
|
||||
using Scalar = typename Traits::Scalar;
|
||||
|
||||
// Defensive checks on the host object first.
|
||||
if (cpu.xs().size() != cpu.ys().size()) {
|
||||
OPM_THROW(std::logic_error,
|
||||
"Inconsistent CPU object passed to copy_to_gpu");
|
||||
}
|
||||
|
||||
// Return type is always MyComposite<Traits, GpuBuffer>.
|
||||
// Each constructor performs a synchronous H→D copy.
|
||||
auto gpuXs = GpuBuffer<Scalar>(cpu.xs());
|
||||
auto gpuYs = GpuBuffer<Scalar>(cpu.ys());
|
||||
|
||||
return MyComposite<Traits, GpuBuffer>(gpuXs, gpuYs);
|
||||
}
|
||||
|
||||
} // namespace Opm::gpuistl
|
||||
#endif // HAVE_CUDA
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
* **Return type:** `copy_to_gpu` always returns
|
||||
`MyClass<OtherTemplates, GpuBuffer>` — the storage type is fixed, not
|
||||
a template argument. **Use `auto` to hold the result** and call with
|
||||
no explicit template argument:
|
||||
```cpp
|
||||
// No template argument needed — return type is always MyClass<Traits, GpuBuffer>
|
||||
auto owned = Opm::gpuistl::copy_to_gpu(cpuObj);
|
||||
```
|
||||
* The returned object **owns** every byte of GPU memory it points to,
|
||||
via the `GpuBuffer` members. Keep it alive on the CPU for as long as
|
||||
any kernel may dereference it.
|
||||
* Use `OPM_THROW` for any precondition violation. Heavyweight checks
|
||||
(e.g. "is the object finalized?") belong here, not in the kernel.
|
||||
|
||||
---
|
||||
|
||||
## 5. Writing `make_view`
|
||||
|
||||
`make_view` returns a **non-owning** object. It exists solely to be
|
||||
passed *by value* into a kernel.
|
||||
|
||||
```cpp
|
||||
#if HAVE_CUDA
|
||||
#include <opm/simulators/linalg/gpuistl/GpuView.hpp>
|
||||
|
||||
namespace Opm::gpuistl {
|
||||
|
||||
template <class Traits, template<class> class ContainerT>
|
||||
MyComposite<Traits, GpuView>
|
||||
make_view(MyComposite<Traits, ContainerT>& gpuBuffers)
|
||||
{
|
||||
using Scalar = typename Traits::Scalar;
|
||||
|
||||
// Return type is always MyComposite<Traits, GpuView>.
|
||||
// make_view(GpuBuffer<T>&) is provided by GpuBuffer.hpp.
|
||||
GpuView<Scalar> xsView = make_view<Scalar>(gpuBuffers.xs_);
|
||||
GpuView<Scalar> ysView = make_view<Scalar>(gpuBuffers.ys_);
|
||||
|
||||
return MyComposite<Traits, GpuView>(xsView, ysView);
|
||||
}
|
||||
|
||||
} // namespace Opm::gpuistl
|
||||
#endif // HAVE_CUDA
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
* **Return type:** `make_view` always returns
|
||||
`MyClass<OtherTemplates, GpuView>` — the storage type is fixed, not
|
||||
a template argument. This type is trivially copyable and safe to pass
|
||||
*by value* to a `__global__` kernel. **Use `auto` to hold the result**
|
||||
and call with no explicit template argument:
|
||||
```cpp
|
||||
// No template argument needed — return type is always MyClass<Traits, GpuView>
|
||||
auto view = Opm::gpuistl::make_view(owned);
|
||||
```
|
||||
* `ViewT` is typically `GpuView`. The `const`-ness of the element type
|
||||
is baked into `GpuView` itself for read-only kernels, which is the
|
||||
strongest signal of that intent.
|
||||
* Because it is non-owning, the original `gpuBuffers` object **must
|
||||
outlive every kernel that uses the view**.
|
||||
* `make_view` accesses private members directly (hence the `friend`
|
||||
declarations in section 3). For primitive scalars there is no work
|
||||
to do other than wrapping pointers; no H↔D transfer happens here.
|
||||
|
||||
---
|
||||
|
||||
## 5b. GPU smart pointers (`gpu_smart_pointer.hpp`)
|
||||
|
||||
`opm-simulators/opm/simulators/linalg/gpuistl/gpu_smart_pointer.hpp`
|
||||
|
||||
When a class member is a *scalar or small struct* (rather than a
|
||||
vector), you may want to store it in GPU memory via a smart pointer
|
||||
rather than wrapping it in `GpuBuffer`. The utilities in
|
||||
`gpu_smart_pointer.hpp` handle this.
|
||||
|
||||
### Allocation and copying
|
||||
|
||||
| Function | Description |
|
||||
| -------- | ----------- |
|
||||
| `make_gpu_shared_ptr<T>()` | Allocates `sizeof(T)` bytes on the GPU; returns `std::shared_ptr<T>` with a `cudaFree` deleter. |
|
||||
| `make_gpu_shared_ptr<T>(value)` | Same, but immediately copies `value` host → device. |
|
||||
| `make_gpu_unique_ptr<T>()` | Same idea, returns `std::unique_ptr<T, ...>`. |
|
||||
| `make_gpu_unique_ptr<T>(value)` | Same, with initial host → device copy. |
|
||||
| `copyFromGPU(ptr)` | Synchronous device → host copy; accepts a raw pointer, `shared_ptr`, or `unique_ptr`. |
|
||||
| `copyToGPU(value, ptr)` | Synchronous host → device copy; accepts the same pointer types. |
|
||||
|
||||
```cpp
|
||||
// Allocate and initialise a single Scalar on the GPU.
|
||||
auto gpuVal = Opm::gpuistl::make_gpu_unique_ptr<Scalar>(hostValue);
|
||||
|
||||
// Later, read it back (involves a synchronization point).
|
||||
Scalar result = Opm::gpuistl::copyFromGPU(gpuVal);
|
||||
```
|
||||
|
||||
### Passing smart-pointer data to kernels: `PointerView`
|
||||
|
||||
`std::shared_ptr` and `std::unique_ptr` are **not** trivially copyable,
|
||||
so they cannot be passed by value to a `__global__` kernel. Use
|
||||
`PointerView<T>` instead — it is a lightweight, trivially copyable
|
||||
wrapper around a raw `T*`:
|
||||
|
||||
```cpp
|
||||
// make_view(smart_ptr) returns PointerView<T> — non-owning and
|
||||
// trivially copyable, safe to pass by value to a kernel.
|
||||
auto view = Opm::gpuistl::make_view(gpuVal); // PointerView<Scalar>
|
||||
|
||||
myKernel<<<grid, block>>>(view, /* … */); // pass by value
|
||||
```
|
||||
|
||||
Inside the kernel `PointerView<T>` behaves exactly like a pointer:
|
||||
|
||||
```cpp
|
||||
__global__
|
||||
void myKernel(Opm::gpuistl::PointerView<Scalar> val, Scalar* out)
|
||||
{
|
||||
*out = *val + 1.0f; // dereference
|
||||
// or: *out = val.get()[0];
|
||||
}
|
||||
```
|
||||
|
||||
The lifetime rule mirrors `GpuView`: the underlying smart pointer
|
||||
**must outlive** every kernel that uses the view.
|
||||
|
||||
### Wrapping an inline value: `ValueAsPointer`
|
||||
|
||||
`ValueAsPointer<T>` wraps a plain `T` so that it can be used wherever
|
||||
a pointer-like interface is expected inside a kernel. This is useful
|
||||
for generic template code that must work with both `PointerView<T>`
|
||||
(pointing at GPU memory) and an inline value (no allocation):
|
||||
|
||||
```cpp
|
||||
Opm::gpuistl::ValueAsPointer<float> vp(3.14f);
|
||||
|
||||
__global__
|
||||
void myKernel(Opm::gpuistl::ValueAsPointer<float> v, float* out)
|
||||
{
|
||||
*out = *v; // dereferences like a pointer; no GPU allocation needed
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Putting it together: launching a kernel
|
||||
|
||||
A complete CPU-side example, mirroring
|
||||
`tests/gpuistl/test_gpu_linear_two_phase_material.cu`:
|
||||
|
||||
```cpp
|
||||
using Scalar = float;
|
||||
|
||||
// 1. Build and finalize the CPU object.
|
||||
auto cpu = Opm::MyComposite<Traits>({0.0f, 0.5f, 1.0f}, {0.0f, 0.9f, 1.0f});
|
||||
|
||||
// 2. Owning GPU copy — no template arg: return type is always MyComposite<Traits, GpuBuffer>.
|
||||
// Keep this alive until all kernels are done.
|
||||
auto gpuOwned = Opm::gpuistl::copy_to_gpu(cpu);
|
||||
|
||||
// 3. Cheap, trivially copyable view — no template arg: return type is always MyComposite<Traits, GpuView>.
|
||||
// Pass by value to the kernel.
|
||||
auto gpuView = Opm::gpuistl::make_view(gpuOwned);
|
||||
|
||||
// 4. Launch.
|
||||
Scalar* dResult = nullptr;
|
||||
OPM_GPU_SAFE_CALL(cudaMalloc(&dResult, sizeof(Scalar)));
|
||||
|
||||
evaluateKernel<<<1, 1>>>(gpuView, /*x=*/0.25f, dResult);
|
||||
OPM_GPU_SAFE_CALL(cudaDeviceSynchronize());
|
||||
|
||||
Scalar hResult = 0;
|
||||
OPM_GPU_SAFE_CALL(cudaMemcpy(&hResult, dResult,
|
||||
sizeof(Scalar), cudaMemcpyDeviceToHost));
|
||||
OPM_GPU_SAFE_CALL(cudaFree(dResult));
|
||||
|
||||
// 5. gpuOwned destroyed here → GPU memory released.
|
||||
```
|
||||
|
||||
The kernel itself simply dereferences the view:
|
||||
|
||||
```cpp
|
||||
using GpuViewComposite = Opm::MyComposite<Traits, Opm::gpuistl::GpuView>;
|
||||
|
||||
__global__
|
||||
void evaluateKernel(GpuViewComposite params, Scalar x, Scalar* out)
|
||||
{
|
||||
*out = params.evaluate(x);
|
||||
}
|
||||
```
|
||||
|
||||
Always wrap CUDA/HIP runtime calls in **`OPM_GPU_SAFE_CALL`** (from
|
||||
`opm/simulators/linalg/gpuistl/detail/gpu_safe_call.hpp`) so that
|
||||
errors are turned into `OPM_THROW`-style exceptions on the host.
|
||||
|
||||
---
|
||||
|
||||
## 7. Adding tests
|
||||
|
||||
Tests live in **`opm-simulators/tests/gpuistl/`** — that is the only
|
||||
place where `.cu` files are actually compiled. The pattern is:
|
||||
|
||||
1. Create `tests/gpuistl/test_my_composite.cu`. Use Boost.Test as in
|
||||
the surrounding files. The header skeleton looks like:
|
||||
|
||||
```cpp
|
||||
#include <config.h>
|
||||
|
||||
#define BOOST_TEST_MODULE TestMyComposite
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <opm/path/to/MyComposite.hpp>
|
||||
#include <opm/simulators/linalg/gpuistl/GpuBuffer.hpp>
|
||||
#include <opm/simulators/linalg/gpuistl/GpuView.hpp>
|
||||
#include <opm/simulators/linalg/gpuistl/detail/gpu_safe_call.hpp>
|
||||
```
|
||||
|
||||
2. Define one or more `__global__` wrapper kernels that take the *view*
|
||||
instantiation of your class and write a result into device memory.
|
||||
3. Inside `BOOST_AUTO_TEST_CASE`, build the CPU object, call
|
||||
`copy_to_gpu`, call `make_view`, launch the kernel, copy the result
|
||||
back, and `BOOST_CHECK` against the CPU reference computed from the
|
||||
exact same inputs. Always compare GPU output to a CPU reference
|
||||
computed from the *same* class — that is what guarantees identical
|
||||
semantics on both backends.
|
||||
4. Register the test in
|
||||
**`opm-simulators/CMakeLists_files.cmake`** by adding a single
|
||||
line next to the existing GPU tests, e.g.:
|
||||
|
||||
```cmake
|
||||
ADD_CUDA_OR_HIP_FILE(TEST_SOURCE_FILES tests test_my_composite.cu)
|
||||
```
|
||||
|
||||
The `ADD_CUDA_OR_HIP_FILE` macro takes care of wiring the file into
|
||||
the CUDA build *and* into the HIP build (see section 8).
|
||||
|
||||
Reference test files to read before writing your own:
|
||||
|
||||
* `tests/gpuistl/test_gpu_linear_two_phase_material.cu` — composite
|
||||
class with multiple `GpuBuffer`s.
|
||||
* `tests/gpuistl/test_GpuSparseTable.cu` — composite class living in
|
||||
`opm-grid`.
|
||||
* `tests/gpuistl/test_GpuBuffer.cu` — exercises the buffer/view
|
||||
primitives themselves.
|
||||
* `tests/gpuistl/test_throw_macros_on_gpu.cu` — shows the expected
|
||||
behaviour of `OPM_THROW` from device code.
|
||||
|
||||
---
|
||||
|
||||
## 8. Relation to hipification
|
||||
|
||||
When the build is configured for AMD GPUs, the CUDA sources are
|
||||
translated to HIP at build time. The mechanism lives **entirely inside
|
||||
`opm-simulators`**:
|
||||
|
||||
* `opm-simulators/CMakeLists.txt` looks for `hipify-perl`,
|
||||
imports it as the `hipify-perl` CMake target and, when
|
||||
`CONVERT_CUDA_TO_HIP` is `ON`, registers the `hipified_headers`
|
||||
target that the GPU library/test executables depend on.
|
||||
* `opm-simulators/CMakeLists_files.cmake` defines the macro
|
||||
`ADD_CUDA_OR_HIP_FILE(LIST DIR FILE)`. When CUDA is found the file
|
||||
is added as-is; when HIP is the active backend the macro emits an
|
||||
`add_custom_command` that runs `bin/hipify_file.sh` to translate
|
||||
`${DIR}/gpuistl/${FILE}` into
|
||||
`${PROJECT_BINARY_DIR}/${DIR}/gpuistl_hip/${FILE/.cu/.hip}` and adds
|
||||
the generated file to the build. Public headers are tracked
|
||||
separately and end up as a `hipified_headers` target dependency.
|
||||
* `opm-simulators/bin/hipify_file.sh` is the actual translation step:
|
||||
it runs `hipify-perl` and post-processes the `#include` paths so
|
||||
they refer to the `gpuistl_hip/` (rather than `gpuistl/`) versions of
|
||||
any sibling files.
|
||||
|
||||
Practical implications when porting a class:
|
||||
|
||||
* **`opm-common` and `opm-grid` headers are *not* hipified.** They are
|
||||
consumed unchanged from the hipified `opm-simulators` translation
|
||||
units. This is exactly why `OPM_HOST_DEVICE`, `OPM_THROW`,
|
||||
`<cuda_runtime.h>` vs `<hip/hip_runtime.h>` and the `HAVE_CUDA`
|
||||
guards are routed through `gpuDecorators.hpp` — those headers must
|
||||
compile cleanly under both backends. Do **not** `#include` runtime
|
||||
headers directly from a header that lives in `opm-common` or
|
||||
`opm-grid`.
|
||||
* If you add a new GPU source file to `opm-simulators`, register it
|
||||
through `ADD_CUDA_OR_HIP_FILE` so that it is picked up automatically
|
||||
by the HIP build. Never list `.cu` files unconditionally.
|
||||
* If you add a new public header that should *not* be hipified (for
|
||||
example because it must remain reachable from non-GPU translation
|
||||
units that are built without HIP), add it to `PUBLIC_HEADER_FILES`
|
||||
directly — see the comment near
|
||||
`opm/simulators/linalg/gpuistl/device_management.hpp` in
|
||||
`CMakeLists_files.cmake` for the precedent.
|
||||
* The `OPM_IS_COMPILING_WITH_GPU_COMPILER` macro is `1` under both
|
||||
`nvcc` and `hipcc`, so it is the right knob if you ever need to
|
||||
branch on "GPU compiler vs host compiler" rather than on
|
||||
"CUDA vs HIP".
|
||||
|
||||
In short: write the class for CUDA in `opm-simulators` (and pure,
|
||||
header-only, decorator-guarded code in `opm-common` / `opm-grid`),
|
||||
register sources through `ADD_CUDA_OR_HIP_FILE`, and the HIP backend
|
||||
falls out automatically.
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Class is templated on its storage container using
|
||||
`template<class> class Storage = VectorWithDefaultAllocator`.
|
||||
- [ ] Every member function callable from a kernel is annotated with
|
||||
`OPM_HOST_DEVICE`.
|
||||
- [ ] All exceptions go through `OPM_THROW`.
|
||||
- [ ] `<opm/common/utility/gpuDecorators.hpp>` is the only GPU-related
|
||||
header included from the class definition itself.
|
||||
- [ ] `copy_to_gpu` and `make_view` live in `namespace Opm::gpuistl`,
|
||||
are friended by the class, and are guarded by `#if HAVE_CUDA`
|
||||
when the class itself is in `opm-common` or `opm-grid`.
|
||||
- [ ] The CPU code that calls `copy_to_gpu` keeps the returned owning
|
||||
object alive until every kernel using the view has completed.
|
||||
- [ ] A new `test_*.cu` exists in `opm-simulators/tests/gpuistl/` and
|
||||
is registered via `ADD_CUDA_OR_HIP_FILE` in
|
||||
`opm-simulators/CMakeLists_files.cmake`.
|
||||
Reference in new issue
Block a user