KaMPIng-v2 Design Guide¶
This document describes the design patterns and layering in KaMPIng-v2. It’s meant as a reference for implementing new features and understanding the existing code without reading every header.
Architecture Overview¶
KaMPIng-v2 uses a layered architecture with a clear separation of concerns:
┌─────────────────────────────────────────────────┐
│ v2 Wrappers (kamping/v2/) │
│ - infer() protocol (deferred sizing, metadata) │
│ - Result types (struct binding support) │
│ - Ownership semantics (ref_view vs owning) │
└──────────────┬──────────────────────────────────┘
↓ delegates to
┌──────────────────────────────────────────────────┐
│ Core Layer (mpi-core/include/mpi/) │
│ - Buffer protocol (concepts, accessors) │
│ - Bare MPI wrappers (one call each) │
│ - Handle dispatch (traits-based) │
│ - No view machinery, no resizing │
└──────────────────────────────────────────────────┘
↓ uses
┌──────────────────────────────────────────────────┐
│ View Adapters (kamping/v2/views/) │
│ - Composable via | operator │
│ - Lazy metadata attachment │
│ - Ownership forwarding │
└──────────────────────────────────────────────────┘
The key principle: core layer is views-free. All view machinery, ownership forwarding, and resize logic lives in v2 and the views namespace. This keeps the MPI wrappers simple and reusable.
Core Concepts¶
1. Buffer Protocol¶
All MPI operations work with buffers satisfying one of these concepts:
// Single-buffer operations (send, recv, allgather, broadcast, etc.)
concept send_buffer {
count(t) → std::integral // element count
ptr(t) → convertible to (void const*) // read-only pointer
type(t) → MPI_Datatype // element type
};
concept recv_buffer {
count(t) → std::integral
ptr(t) → convertible to void* // read-write pointer
type(t) → MPI_Datatype
};
// data_buffer requires count + ptr (any pointer) + type
// send_buffer and recv_buffer both extend data_buffer,
// distinguished by ptr() constness (void const* vs void*)
concept data_buffer = /* count + ptr + type */;
// Variadic operations (allgatherv, alltoallv, etc.)
concept send_buffer_v = send_buffer + {
counts(t) → std::ranges::contiguous_range<int const> // per-rank counts
displs(t) → std::ranges::contiguous_range<int const> // per-rank displacements
};
concept recv_buffer_v = recv_buffer + {
counts(t) → std::ranges::contiguous_range<int> // mutable for infer()
displs(t) → std::ranges::contiguous_range<int const>
};
Accessor dispatch follows a 3-tier priority:
mpi::experimental::buffer_traits<T>specialization — non-intrusive, for types you don’t ownMember functions —
t.mpi_count(),t.mpi_ptr(),t.mpi_type(),t.mpi_counts(),t.mpi_displs()Fallback —
std::ranges::size(),std::ranges::data(), builtin type deduction
To adapt a third-party type:
template <>
struct mpi::experimental::buffer_traits<MyBuffer> {
static std::ptrdiff_t count(MyBuffer const& b) { return b.size; }
static void* ptr(MyBuffer& b) { return b.data; } // recv
static void const* ptr(MyBuffer const& b) { return b.data; } // send
static MPI_Datatype type(MyBuffer const& b) { return MPI_INT; }
};
2. Handle Dispatch¶
All MPI handles (MPI_Comm, MPI_Request, MPI_Datatype, MPI_Status, MPI_Message) are extracted via mpi::experimental::handle(x) and mpi::experimental::handle_ptr(x), with the same 3-tier dispatch:
handle_traits<T>specializationMember functions —
t.mpi_handle(),t.mpi_handle_ptr()Builtin passthrough — raw
MPI_Comm,MPI_Request, etc. pass through unchanged
Example (non-intrusive wrapper):
class MyComm {
MPI_Comm comm_;
public:
MPI_Comm get() const { return comm_; }
};
template <>
struct mpi::experimental::handle_traits<MyComm> {
static MPI_Comm handle(MyComm const& c) { return c.get(); }
};
// Now you can pass MyComm directly to mpi::experimental::allgather()
mpi::experimental::allgather(sbuf, rbuf, MyComm{comm});
Handle ranges¶
Functions that take an array of handles (e.g. the requests of waitall) obtain it via mpi::experimental::handle_range(x), with the same 3-tier dispatch:
handle_range_traits<T>specialization —static … handle_range(T& t)Member function —
t.mpi_handle_range()Contiguous range passthrough — an lvalue
std::vector<MPI_Request>,std::array, C array orstd::spanis returned by reference
The result must be a writable contiguous range of exactly the handle type that refers to the caller’s storage: an lvalue reference or a borrowed view such as std::span, never a container returned by value. MPI writes back into it (completed requests become MPI_REQUEST_NULL, statuses are filled in), so writes into a copy would be lost. convertible_to_mpi_handle_range<T, Handle> checks all of this. A handle range need not be sized; arguments that need a count (like a request array) additionally require a sized range.
The member tier exists for types whose iteration differs from their storage. For example, a vector of requests that iterates as request_views (with wait()/test()) but stores raw MPI_Requests:
class MyRequests {
std::vector<MPI_Request> requests_;
public:
std::span<MPI_Request> mpi_handle_range() noexcept { return requests_; }
// begin()/end() may iterate as views instead of raw handles
};
Arguments and defaults¶
The two layers handle optional arguments differently:
Core (
mpi::experimental) mirrors the MPI signature exactly. There are no default arguments, except a trailing output or hint argument that MPI lets you ignore (status = MPI_STATUS_IGNORE,info = MPI_INFO_NULL).v2 (
kamping::v2) always requires the communicator, and it is always the last argument, followed only by an optionalstatusoutput. Optional arguments in front of it are left out by passing fewer arguments, never by default arguments beforecomm:v2::send(buf, dest, comm); v2::send(buf, dest, tag, comm); v2::recv(buf, comm); v2::recv(buf, source, comm); v2::recv(buf, source, tag, comm); v2::bcast(buf, comm); v2::bcast(buf, root, comm); v2::reduce(s, r, comm); v2::reduce(s, r, op, comm); v2::reduce(s, r, op, root, comm);
A left-out argument takes the obvious value: send tag
DEFAULT_SEND_TAG(0), receive fromMPI_ANY_SOURCEwithMPI_ANY_TAG, root 0, operationstd::plus<>. A caller-supplied request for a non-blocking operation is passed first. Rooted collectives also have a non-root shorthand without the root-only buffer (gather(s, [root,] comm),scatter(r, [root,] comm),scatterv(r, [root,] comm),reduce(s, [op, [root,]] comm)), exceptgatherv, whose non-root receive buffer depends on whether root’s counts are deferred.
Why not default arguments? On MPICH, MPI_Comm, MPI_Request, MPI_Op and the other handles are int, so a
communicator, a request, an operation, a rank and a tag cannot be told apart by type. Overloads that differ only in
which of them they expect at a position would be ambiguous or, worse, silently take a communicator for a tag. The
rules above keep every overload set unambiguous: forms differ in their number of arguments, or in an argument kind
that is never an int (a buffer, a request in first position, a status pointer). The same reason excludes a
reduce(s, r, root, comm) form: a root cannot be told apart from an MPI_Op.
Adding a New Collective¶
Here’s a step-by-step walkthrough using allgather as an example.
Step 1: Implement the Core Wrapper¶
File: mpi-core/include/mpi/collectives/allgather.hpp
namespace mpi::experimental {
template <send_buffer SBuf, recv_buffer RBuf, convertible_to_mpi_handle<MPI_Comm> Comm>
void allgather(SBuf&& sbuf, RBuf&& rbuf, Comm const& comm) {
int comm_size = 0;
MPI_Comm_size(mpi::experimental::handle(comm), &comm_size);
KAMPING_V2_ASSERT(
static_cast<int>(mpi::experimental::count(rbuf)) % comm_size == 0,
"recv buffer size must be divisible by comm size"
);
int err = MPI_Allgather(
mpi::experimental::ptr(sbuf),
static_cast<int>(mpi::experimental::count(sbuf)),
mpi::experimental::type(sbuf),
mpi::experimental::ptr(rbuf),
static_cast<int>(mpi::experimental::count(rbuf)) / comm_size,
mpi::experimental::type(rbuf),
mpi::experimental::handle(comm)
);
if (err != MPI_SUCCESS) {
throw mpi_error(err);
}
}
} // namespace mpi::experimental
Key points:
Template over buffer concepts (
send_buffer,recv_buffer), not specific typesExtract metadata via the
count(),ptr(),type()free functions and handles viahandle(), always called qualified (mpi::experimental::count(x)), so argument-dependent lookup cannot pick up a user’s function of the same nameMirror the MPI signature: no default arguments, except a trailing
status = MPI_STATUS_IGNORE(orinfo)Use
KAMPING_V2_ASSERTfor precondition validationThrow
mpi_erroron MPI failureNo inference, no resizing, no ownership tricks — pure MPI wrapper
Step 2: Define the Infer Overload¶
File: kamping-v2/include/kamping/v2/infer.hpp
Add an operation tag to kamping::v2::comm_op (kamping-v2/include/kamping/v2/comm_op.hpp) and a default
infer() overload for it:
namespace kamping::v2 {
namespace comm_op {
struct allgather {};
} // namespace comm_op
// Default infer overload for allgather
template <mpi::experimental::send_buffer SBuf, mpi::experimental::recv_buffer RBuf>
void infer(comm_op::allgather, SBuf const& sbuf, RBuf& rbuf, MPI_Comm comm) {
// If recv is a deferred buffer (has set_recv_count), resize it
if constexpr (kamping::v2::deferred_recv_buf<RBuf>) {
int comm_size = mpi::experimental::comm_view{comm}.size();
rbuf.set_recv_count(comm_size * static_cast<std::ptrdiff_t>(mpi::experimental::count(sbuf)));
}
}
} // namespace kamping::v2
Why a separate infer? It discovers metadata before the MPI call:
For deferred buffers: calculate required size from comm_size and send count
For variadic ops: negotiate counts/displacements via collective calls
For recv: probe network to discover incoming message size
The infer is called by the v2 wrapper before delegating to the core layer.
Step 3: Implement the v2 Wrapper¶
Implement the full form with all arguments, then the shortened forms (see Arguments and defaults) as thin overloads that forward to it.
File: kamping-v2/include/kamping/v2/collectives/allgather.hpp
namespace kamping::v2 {
template <
mpi::experimental::send_buffer SBuf,
mpi::experimental::recv_buffer RBuf,
mpi::experimental::convertible_to_mpi_handle<MPI_Comm> Comm>
auto allgather(SBuf&& sbuf, RBuf&& rbuf, Comm const& comm) -> result<SBuf, RBuf> {
// Construct result, preserving ownership (lvalue → reference, rvalue → owned)
result<SBuf, RBuf> res{std::forward<SBuf>(sbuf), std::forward<RBuf>(rbuf)};
// Run infer to discover/resize metadata before MPI call
infer(comm_op::allgather{}, res.send, res.recv, mpi::experimental::handle(comm));
// Delegate to core layer
mpi::experimental::allgather(res.send, res.recv, comm);
return res;
}
} // namespace kamping::v2
Pattern:
Construct
result<SBuf, RBuf>wrapper (ownership preservation handled automatically)Call
infer(comm_op::allgather{}, ...)to discover/resize metadataDelegate to
mpi::experimental::allgather()Return result (supports structured bindings:
auto [s, r] = allgather(...))
Step 4: Tests¶
Create kamping-v2/tests/allgather_test.cpp and register it in kamping-v2/tests/CMakeLists.txt as test_v2_allgather (add_executable, target_link_libraries with kamping_v2 kamping_v2_warnings KaTestrophe::main, and katestrophe_add_mpi_test(test_v2_allgather CORES 1 2 4)). Cover:
Single-rank send/recv
Multi-rank with uniform sizes
Multi-rank with non-uniform send sizes (validate recv sizing)
Structured binding usage
Deferred buffers (with
views::resize)
Working with Views¶
The View Interface Pattern¶
All v2 views derive from view_interface<Derived> and implement a base() method:
template <typename Base>
struct resize_view : public view_interface<resize_view<Base>> {
Base base_; // The wrapped range
auto base() { return base_; }
auto base() const { return base_; }
};
view_interface is a CRTP mixin that:
Forwards
begin(),end()tobase()Forwards MPI accessors (
mpi_count(),mpi_ptr(),mpi_type()) viampi::experimental::*()dispatchConditionally provides
mpi_resize_for_receive()if base supportsresize
This means a view automatically satisfies recv_buffer if its base does, even if the view itself doesn’t define these methods.
Composing Views with |¶
// Each view is an adaptor_closure that supports piping
std::vector<int> data = {...};
// Composed view: resize on receive, override element type
auto view = data | kamping::v2::views::resize | kamping::v2::views::with_type(MPI_INT);
// Lazy: metadata is computed only when MPI operation queries it
// resize is forwarded through the adaptor chain via mpi_resize_for_receive()
Implementation sketch:
namespace kamping::v2::views {
struct resize_adaptor : adaptor_closure<resize_adaptor> {
template <std::ranges::range R>
auto operator()(R&& r) const {
return resize_view<all_t<R>>{all(std::forward<R>(r))};
}
};
inline constexpr resize_adaptor resize;
} // namespace kamping::v2::views
The adaptor_closure base provides operator| overloads that call operator().
Variadic Operations (counts/displacements)¶
Variadic collectives (allgatherv, alltoallv, etc.) require send_buffer_v / recv_buffer_v:
template <send_buffer_v SBuf, recv_buffer_v RBuf, ...>
void allgatherv(SBuf&& sbuf, RBuf&& rbuf, MPI_Comm comm) {
// Metadata: per-rank counts and displacements
auto counts_s = counts(sbuf); // std::span<int const>
auto displs_s = displs(sbuf); // std::span<int const>
auto counts_r = counts(rbuf); // std::span<int> (may be mutable for infer)
auto displs_r = displs(rbuf); // std::span<int const>
MPI_Allgatherv(
ptr(sbuf), counts_s.data(), displs_s.data(), type(sbuf),
ptr(rbuf), counts_r.data(), displs_r.data(), type(rbuf),
comm
);
}
View support:
views::with_counts(range)— attaches per-rank countsviews::with_displs(range)— attaches per-rank displacementsviews::auto_counts()— deferred counts;infer()fills in viaset_comm_size+commit_counts()views::auto_displs([container])— computes displs from counts via exclusive_scan
Reduction Operations (make_op)¶
Reduce-like collectives (reduce, allreduce, scan, exscan) take an operation argument alongside their buffers. Operations follow the same core-layer, traits-dispatched design as buffers and handles — everything lives in mpi-core/include/mpi/ops.hpp (mpi::experimental), with the kamping::v2 layer only re-exporting the user-facing names.
Operation dispatch (as_mpi_op)¶
File: mpi-core/include/mpi/ops.hpp
Every reduce-like wrapper resolves its operation argument to a raw MPI_Op via mpi::experimental::as_mpi_op(op, sbuf, rbuf), with a 3-tier priority mirroring the buffer/handle accessors:
op_traits<Op, SBuf, RBuf>specialization — non-intrusive, for op types you don’t ownBuiltin
MPI_Ophandle — anything convertible toMPI_Opviahandle()passes through (rawMPI_SUM, and the scoped op handles below)Builtin range op — maps a functor like
std::plus<>on the buffer’s element type to a predefined constant (MPI_SUM, …) viakamping::types::mpi_operation_traits
The valid_op<Op, SBuf, RBuf> concept is simply “as_mpi_op(op, sbuf, rbuf) is well-formed”; reduce-like wrappers constrain their Op template parameter on it and pass op by const& throughout (so a move-only op handle is never copied).
make_op — constructing user-defined operations¶
For operations that aren’t builtin, mpi::experimental::make_op(...) (re-exported as kamping::v2::make_op) creates an RAII MPI_Op from the kamping-types dependency. It is a thin factory — the element-wise reduction logic lives in kamping::types::ScopedFunctorOp / ScopedCallbackOp, not here — that does a single MPI_Op_create (“one MPI call, no inference”), which is why it belongs in the core layer next to as_mpi_op rather than in the bindings layer.
Two overloads:
// (a) default-constructible binary functor / captureless lambda → ScopedFunctorOp.
// Element type T is explicit; the functor must be callable as T(T const&, T const&).
auto op = kamping::v2::make_op<int>([](int a, int b) { return a + b; });
// (b) raw MPI callback void(void*, void*, int*, MPI_Datatype*) → ScopedCallbackOp.
// No element type needed — the callback is already type-erased.
auto op = kamping::v2::make_op(&my_mpi_callback);
kamping::v2::reduce(data, recv, op, /*root=*/0, comm);
Commutativity is an optional trailing argument (kamping::v2::commutative / non_commutative, re-exported from kamping::ops), defaulting to non_commutative — the correctness-safe choice, since MPI never reorders a non-commutative op. Opt in for performance:
auto op = kamping::v2::make_op<int>(std::plus<int>{}, kamping::v2::commutative);
Overload (a) is constrained on is_default_constructible_v<Op> && is_invocable_r_v<T, Op, T const&, T const&>, so a bad functor is a clean overload-resolution failure instead of a hard error inside the dependency. Stateful (capturing) lambdas are intentionally unsupported — they are neither default-constructible nor convertible to a raw callback; wrap the state in a functor, or build the callback yourself and use overload (b).
Making the result flow through collectives¶
make_op returns ScopedFunctorOp / ScopedCallbackOp, which expose their MPI_Op via .get() but have no mpi_handle() member. Two handle_traits specializations (also in mpi-core/include/mpi/ops.hpp) bridge that gap:
template <bool Commutative, typename T, typename Op>
struct mpi::experimental::handle_traits<kamping::types::ScopedFunctorOp<Commutative, T, Op>> {
static MPI_Op handle(kamping::types::ScopedFunctorOp<Commutative, T, Op> const& op) noexcept {
return op.get();
}
};
// ... and the analogous ScopedCallbackOp specialization.
This makes any scoped op satisfy convertible_to_mpi_handle<MPI_Op>, so as_mpi_op tier 2 picks it up automatically — the op flows into every reduce-like collective with no per-collective wiring, however it was constructed.
Adding a custom operation type non-intrusively¶
To adapt an op type you don’t own (e.g. one that already knows its own MPI_Op), specialize op_traits — the tier-1 hook:
template <>
struct mpi::experimental::op_traits<MyOp, std::vector<int>, std::vector<int>> {
static MPI_Op op(MyOp const& o, std::vector<int> const&, std::vector<int> const&) {
return o.to_mpi_op();
}
};
Non-Blocking Operations¶
Non-blocking MPI calls return iresult<Buf> (or iresult<SBuf, RBuf> for sendrecv):
// Core layer: the caller supplies the request (last, as in MPI)
template <send_buffer SBuf, rank Dest, tag Tag, convertible_to_mpi_handle<MPI_Comm> Comm,
convertible_to_mpi_handle_ptr<MPI_Request> Request>
void isend(SBuf&& sbuf, Dest dest, Tag tag, Comm const& comm, Request&& request);
// v2 layer: the iresult owns the request and, for rvalues, the buffer (on the heap, so it survives a move)
template <send_buffer SBuf, rank Dest, tag Tag, convertible_to_mpi_handle<MPI_Comm> Comm>
auto isend(SBuf&& sbuf, Dest dest, Tag tag, Comm const& comm) -> iresult<SBuf> {
iresult<SBuf> res{std::forward<SBuf>(sbuf)};
mpi::experimental::isend(res.view(), dest, tag, comm, res.mpi_handle_ptr());
return res;
}
The v2 layer also offers each operation with a caller-supplied request, passed as the first argument (see Arguments and defaults); those overloads return the buffer instead of an iresult.
iresult<Buf> properties:
Move-only (buffer pointer captured by MPI stays stable)
.wait()— blocks until completion, returns buffer (owned buffers moved out; borrowed buffers return reference).test()— non-blocking poll[[nodiscard]]; destroying it with an outstanding request prints a diagnostic and callsstd::terminate()(no implicitMPI_Wait): the library never completes a request implicitly, completion must be explicit
Deferred Buffers¶
Deferred buffers are used when sizes aren’t known until communication time (e.g., MPI_Recv without a prior MPI_Probe).
concept deferred_recv_buf<T> = recv_buffer<T> && requires(T& t) {
t.set_recv_count(std::ptrdiff_t(0)); // Called by infer()
};
Example: views::resize
template <std::ranges::range Base>
struct resize_view : view_interface<resize_view<Base>> {
Base base_;
void mpi_resize_for_receive(std::ptrdiff_t n) {
// Call resize(n) on the underlying container if it has one
if constexpr (std::ranges::sized_range<Base> && requires { base_.resize(n); }) {
base_.resize(n);
}
}
};
Resize contract. resize_for_receive(n) and every mpi_resize_for_receive(n) keep the values of the first
min(old size, n) elements; new elements are unspecified (custom protocols may leave them uninitialized). Like a
receive, which only writes the range it has data for, a resize only touches what it has to. So a buffer that already
holds received data can be grown to receive more behind it: call set_recv_count again on a resize_view.
When infer(comm_op::recv{}, buf | views::resize, ...) is called:
Detects that the view is deferred
Probes the network via
MPI_Mprobeto determine sizeCalls
mpi_resize_for_receive(n)to resize the bufferStores the matched message in
infer()so the actual recv uses it
Extensibility¶
Custom Buffer Types¶
Implement the buffer protocol and optionally set_recv_count():
struct MyBuffer {
void* data;
std::ptrdiff_t size;
MPI_Datatype dtype;
void resize(std::ptrdiff_t n) { size = n; /* ... */ }
void set_recv_count(std::ptrdiff_t n) { resize(n); }
// Optional: provide intrusive accessors
std::ptrdiff_t mpi_count() const { return size; }
void* mpi_ptr() { return data; }
MPI_Datatype mpi_type() const { return dtype; }
};
// Or use buffer_traits for non-intrusive customization
template <>
struct mpi::experimental::buffer_traits<MyBuffer> { /* ... */ };
Custom MPI Handles¶
Implement mpi_handle() or specialize handle_traits:
class CommWrapper {
MPI_Comm comm_;
public:
MPI_Comm mpi_handle() const { return comm_; }
};
// Or non-intrusively:
template <>
struct mpi::experimental::handle_traits<CommWrapper> {
static MPI_Comm handle(CommWrapper const& c) { return c.get_comm(); }
};
Custom Infer Overloads¶
Add ADL-discovered infer() overloads for custom types or operations:
namespace my_namespace {
struct CustomOp {};
template <typename SBuf, typename RBuf>
void infer(CustomOp, SBuf& sbuf, RBuf& rbuf, MPI_Comm comm) {
// Custom size discovery logic
// Resize rbuf if deferred
}
}
// Called automatically before MPI operation when using a compatible buffer
Testing Patterns¶
Test structure:
#include <gtest/gtest.h>
#include <kamping/v2/collectives/allgather.hpp>
#include <mpi.h>
class AllgatherTest : public ::testing::Test {
protected:
MPI_Comm comm = MPI_COMM_WORLD;
int rank, size;
void SetUp() override {
MPI_Comm_rank(comm, &rank);
MPI_Comm_size(comm, &size);
}
};
TEST_F(AllgatherTest, UniformSendSizes) {
std::vector<int> send_data(10);
std::vector<int> recv_data(10 * size);
auto [s, r] = kamping::v2::allgather(send_data, recv_data, comm);
EXPECT_EQ(r.size(), 10 * size);
// Verify contents...
}
TEST_F(AllgatherTest, DeferredRecvBuffer) {
std::vector<int> send_data = {rank};
std::vector<int> recv_data; // Deferred size
auto [s, r] = kamping::v2::allgather(
send_data,
recv_data | kamping::v2::views::resize,
comm
);
EXPECT_EQ(r.size(), size); // Should be resized by infer()
}
Run with multiple rank counts:
ctest --test-dir build -R test_v2_allgather # Runs with 1, 2, 4, 8 ranks
Quick Reference: Where to Add Things¶
Feature |
Files |
Key Classes/Functions |
|---|---|---|
New collective |
|
|
New view adaptor |
|
Derive from |
Custom buffer |
User code |
Specialize |
Custom handle |
User code |
Specialize |
Reduction operation |
|
|
Custom operation type |
User code |
Specialize |
Non-blocking variant |
Same files as blocking |
Return |
Ecosystem Adapters¶
Adapters live under ecosystem/ and connect KaMPIng to external C++ libraries. Each adapter is
an independent CMake component that depends only on kamping::v2 and the target library.
SYCL adapter (ecosystem/sycl/)¶
Exposes SYCL buffer accessors as MPI buffers via two complementary types.
Compiler requirement: Intel oneAPI DPC++ only.
The adapter relies on sycl::host_task + sycl::interop_handle (standard SYCL 2020) to extract
raw device pointers. AdaptiveCpp does not implement this feature, so the adapter cannot be used
with AdaptiveCpp.
Two access paths:
Accessor type |
How to get a pointer |
Adaptor usage |
|---|---|---|
|
|
|
|
|
|
sycl_view<Base> wraps either accessor type:
For device accessors the constructor takes
(Base, interop_handle const&)and stores a pointer to the handle.mpi_ptr()callsget_native_memfor the active SYCL backend (Level Zero, CUDA, HIP).For host accessors the constructor takes
(Base)andmpi_ptr()callsget_pointer(). Theih_member is eliminated at zero cost via[[no_unique_address]]+std::monostate.mpi_count()andmpi_type()propagate from the accessor throughview_interface.
views::sycl is a single adaptor object (sycl_fn) that covers both paths:
// Device accessor — partial application binds ih, returns a pipeable closure:
acc | views::sycl(ih)
// Equivalent full call:
views::sycl(acc, ih)
// Host accessor — zero-arg closure, no handle needed:
hacc | views::sycl
sycl_accessor_traits<T> is the internal trait struct that provides mode and is_host for both
sycl::accessor and sycl::host_accessor. Concepts sycl_device_accessor / sycl_host_accessor
are built on top of it. Path selection in mpi_ptr() is if constexpr on these concepts.
SYCL USM pointers (sycl::malloc_device, sycl::malloc_shared) are plain C++ pointers.
Wrap them in std::span or mpi_span — no sycl_view adaptor is needed.
supports_matched_probe and enable_borrowed_buffer are propagated from Base, so opt-out
specializations on the accessor type carry through automatically.
Example: ecosystem/sycl/examples/sycl_send_recv_example.cpp
Rank 0 fills 0..9 on device, sends via device-aware MPI (
acc | views::sycl(ih)).Rank 1 receives into device memory, squares each element on device.
Rank 1 sends back via host accessor (
hacc | views::sycl).Rank 0 receives via host accessor and prints the result.
See Also¶
CLAUDE.md— project structure, build commands, code conventions