Properties

Modern C++ programs use the type system to verify statically known properties of the code. It is undesirable to use run-time assertions to verify whether a function that accesses device memory is passed a memory resource that provides device-accessible allocations. A property system is provided for this purpose.

To tell the compiler that memory provided by my_memory_resource is device accessible, use the cuda::mr::device_accessible tag type with a free function get_property to declare that the resource provides device-accessible memory.

struct my_memory_resource {
    friend constexpr void get_property(const my_memory_resource&, cuda::mr::device_accessible) noexcept {}
};

A library can constrain interfaces with cuda::has_property to require that a passed memory resource provides the right kind of memory (See here for a minimal Compiler Explorer example).

template<class MemoryResource>
    requires cuda::has_property<MemoryResource, cuda::mr::device_accessible>
void function_that_dispatches_to_device(MemoryResource& resource);

If C++20 is not available, the function can instead be constrained via SFINAE (See here for a minimal Compiler Explorer example).

template<class MemoryResource, class = cuda::std::enable_if_t<cuda::has_property<MemoryResource, cuda::mr::device_accessible>>>
void function_that_dispatches_to_device(MemoryResource& resource);

For now, libcu++ provides various commonly used properties:

  • cuda::mr::device_accessible and cuda::mr::host_accessible indicate whether memory allocated using a memory resource is accessible from host or device respectively.

More properties may be added as the library and the hardware capabilities evolve. However, a user library is free to define custom properties.

Note that currently the libcu++ provided properties are stateless. However, properties can also provide stateful information that is retrieved via the get_property free function. In order to communicate the desired type of the carried state, a stateful property must define the value_type alias. A library can constrain interfaces that require a stateful property with cuda::has_property_with as shown in the example below (See here for a minimal Compiler Explorer example).

struct required_alignment{
    using value_type = std::size_t;
};
struct my_memory_resource {
    friend constexpr std::size_t get_property(const my_memory_resource& resource, required_alignment) noexcept { return resource.required_alignment; }

    std::size_t required_alignment;
};
static_assert(cuda::has_property_with<my_memory_resource, required_alignment, std::size_t>);

template<class MemoryResource>
void* allocate_check_alignment(MemoryResource& resource, std::size_t size) {
    if constexpr(cuda::has_property_with<MemoryResource, required_alignment, std::size_t>) {
        return resource.allocate(size, get_property(resource, required_alignment{}));
    } else {
        // Use default alignment
        return resource.allocate(size, 42);
    }
}

In generic code it is often desireable to propagate properties from a base type to a derived type, without knowing the base type at all. This common use case is covered by cuda::forward_property, a simple CRTP template.

template<class MemoryResource>
class logging_resource : cuda::forward_property<logging_resource<MemoryResource>, MemoryResource> {
    MemoryResource base;
public:
    void* allocate(std::size_t size, std::size_t alignment) {
        std::cout << "allocating\n";
        return base.allocate(size, alignment);
    }
    void deallocate(void* ptr, std::size_t size, std::size_t alignment) {
        std::cout << "deallocating\n";
        return base.deallocate(ptr, size, alignment);
    }
}