Type-erased resource wrappers#
With the property design depicted in cuda::get_property, a library has flexibility in checking constraints and querying custom properties. However, there is also a cost in providing function templates for a potentially wide range of inputs. Depending on the number of different memory resources, both compile time and binary size might increase considerably.
The type-erased wrappers let you coalesce such APIs into a single function. Both resource_ref and any_resource
preserve property constraints while erasing the concrete resource type.
Wrapper |
Ownership |
Typical use |
|---|---|---|
|
Non-owning |
Accept references/pointers without extending lifetime |
|
Owning |
Store a resource with the object that uses it |
Common usage#
Both wrappers provide a non-templated API surface for allocation and property queries. Choose which wrapper to use based on ownership, then the usage patterns are the same.
resource_ref is constructible from any non-const reference or pointer to a memory resource that satisfies
cuda::mr::resource. any_resource is constructible from a resource object and takes ownership of it.
Properties may be passed to both wrappers just as with cuda::mr::resource_with.
void* do_allocate(cuda::mr::resource_ref<> resource, cuda::stream_ref stream, std::size_t size, std::size_t align) {
return resource.allocate(stream, size, align);
}
void* do_allocate_owned(cuda::mr::any_resource<> resource, cuda::stream_ref stream, std::size_t size, std::size_t align) {
return resource.allocate(stream, size, align);
}
my_memory_resource resource;
my_memory_resource* pointer_to_resource = &resource;
void* from_reference = do_allocate(resource, stream, 1337, 256);
void* from_ptr = do_allocate(pointer_to_resource, stream, 1337, 256);
void* from_owned = do_allocate_owned(cuda::mr::any_resource<>{resource}, stream, 1337, 256);
resource_ref#
cuda::mr::resource_ref is the non-owning, type-erased wrapper. Prefer it when the caller controls the resource
lifetime.
struct required_alignment {
using value_type = std::size_t;
};
void* do_allocate_with_alignment(cuda::mr::resource_ref<required_alignment> resource, cuda::stream_ref stream, std::size_t size) {
return resource.allocate(stream, size, get_property(resource, required_alignment{}));
}
However, the type erasure comes with the cost that arbitrary properties cannot be queried from either wrapper:
struct required_alignment {
using value_type = std::size_t;
};
void* buggy_allocate_with_alignment(cuda::mr::resource_ref<> resource, cuda::stream_ref stream, std::size_t size) {
if constexpr (cuda::has_property<decltype(resource), required_alignment>) { // BUG: This will always be false
return resource.allocate(stream, size, get_property(resource, required_alignment{}));
} else {
return resource.allocate(stream, size, my_default_alignment);
}
}
So, choose wisely. If your library has a well-defined set of fixed properties that you expect to always be available,
then cuda::mr::{synchronous_}resource_ref is an amazing tool to improve compile times and binary size. If you need a
flexible interface then constraining a template argument through cuda::mr::{synchronous_}resource_with is the proper solution.
any_resource#
cuda::mr::any_resource is the owning counterpart. It is especially suited for containers that must ensure the
resource outlives the container.
cuda::mr::make_any_resource<Resource, Properties...>(args...) constructs an any_resource<Properties...> that
owns a newly constructed Resource. Resource must satisfy cuda::mr::resource_with<Resource, Properties...>.
#include <cuda/devices>
#include <cuda/memory_resource>
#include <cuda/stream>
void use_any_resource(cuda::stream_ref stream) {
// Wrap a device memory resource
cuda::mr::any_resource<cuda::mr::device_accessible> resource{
cuda::device_default_memory_pool(cuda::devices[0])
};
// Allocate memory
void* ptr = resource.allocate(stream, 1024, 16);
// Use memory...
// Deallocate
resource.deallocate(stream, ptr, 1024, 16);
}
Synchronous variants#
The synchronous wrappers mirror the same ownership split: synchronous_resource_ref is non-owning, while
any_synchronous_resource owns the resource instance.
cuda::mr::any_synchronous_resource#
cuda::mr::any_synchronous_resource is a type-erased wrapper for resources that satisfy
cuda::mr::synchronous_resource. It can be constructed from an any_resource, but only exposes synchronous
allocation/deallocation.
cuda::mr::make_any_synchronous_resource<Resource, Properties...>(args...) constructs an
any_synchronous_resource<Properties...> that owns a newly constructed Resource. Resource must satisfy
cuda::mr::synchronous_resource_with<Resource, Properties...>.
#include <cuda/memory_resource>
void use_sync_resource() {
cuda::mr::any_synchronous_resource<cuda::mr::host_accessible> resource{
cuda::mr::legacy_pinned_memory_resource{}
};
// Synchronous allocation
void* ptr = resource.allocate_sync(1024, 16);
// Use memory...
// Synchronous deallocation
resource.deallocate_sync(ptr, 1024, 16);
}
resource_cast#
cuda::mr::resource_cast<T>(&resource) returns a pointer to the concrete resource stored in a type-erased wrapper
when the stored resource has type T. It returns nullptr when the stored type does not match.
This is similar to pointer-form dynamic_cast<T*> in that callers must check for nullptr before using the
result. Unlike dynamic_cast, resource_cast matches the exact type stored in the wrapper.
resource_cast works with any_resource, any_synchronous_resource, resource_ref, and
synchronous_resource_ref.
#include <cuda/devices>
#include <cuda/memory_resource>
#include <cuda/stream>
void use_resource_cast(cuda::stream_ref stream) {
cuda::mr::any_resource<cuda::mr::device_accessible> resource{
cuda::device_default_memory_pool(cuda::devices[0])
};
if (auto* pool = cuda::mr::resource_cast<cuda::device_memory_pool_ref>(&resource)) {
pool->trim_to(1024 * 1024);
void* ptr = pool->allocate(stream, 1024, 16);
pool->deallocate(stream, ptr, 1024, 16);
}
}
try_get_property#
Type-erased wrapper conversions can discard properties from the wrapper’s type. try_get_property(resource, property)
can query a property that was present before such a conversion, even when it is not listed in the resulting wrapper’s
property set. For stateless properties, it returns bool. For stateful properties, it returns
cuda::std::optional<Property::value_type>.
#include <cuda/memory_resource>
bool is_device_accessible(cuda::mr::any_synchronous_resource<> resource) {
return try_get_property(resource, cuda::mr::device_accessible{});
}
dynamic_resource_cast#
cuda::mr::dynamic_resource_cast<Properties...>(resource) casts a type-erased resource wrapper to the same wrapper
kind with a different property set. It checks the stored resource at run time and throws if the stored resource does
not support the requested properties.
#include <cuda/memory_resource>
cuda::mr::any_resource<cuda::mr::device_accessible>
require_device_accessible(cuda::mr::any_resource<> resource) {
return cuda::mr::dynamic_resource_cast<cuda::mr::device_accessible>(cuda::std::move(resource));
}
cuda::mr::synchronous_resource_ref#
cuda::mr::synchronous_resource_ref provides the same type-erased reference behavior as resource_ref, but only
exposes synchronous allocation and deallocation.
#include <cuda/memory_resource>
// indicates that the resource must be host-accessible
void use_sync_ref(
cuda::mr::synchronous_resource_ref<cuda::mr::host_accessible> resource
) {
// Synchronous allocation
void* ptr = resource.allocate_sync(1024, 16);
// Use memory...
// Synchronous deallocation
resource.deallocate_sync(ptr, 1024, 16);
}