5.2. Implementing Device Adapters

Viskores comes with several implementations of device adapters so that it may be ported to a variety of platforms. It is also possible to provide new device adapters to support yet more devices, compilers, and libraries. A new device adapter provides a tag, a class to manage arrays in the execution environment, a collection of algorithms that run in the execution environment, and, optionally, a timer.

Most device adapters are associated with some type of device or library, and all source code related directly to that device is placed in a subdirectory of viskores/cont. For example, files associated with CUDA are in viskores/cont/cuda, files associated with the Intel Threading Building Blocks (TBB) are located in viskores/cont/tbb, and files associated with OpenMP are in viskores/cont/openmp. The documentation here assumes that you are adding a device adapter to the Viskores source code and following these file conventions.

For the purposes of discussion in this chapter, we will give a simple example of implementing a device adapter using the std::thread class provided by C++11. We will call our device Cxx11Thread and place it in the directory viskores/cont/cxx11.

By convention, the implementation of device adapters within Viskores is divided among internal headers named DeviceAdapterTag*.h, DeviceAdapterRuntimeDetector*.h, DeviceAdapterMemoryManager*.h, RuntimeDeviceConfiguration*.h, and DeviceAdapterAlgorithm*.h. The DeviceAdapter*.h header that most code includes is a trivial header that simply includes these other headers. For our example std::thread device, we will create the base header at viskores/cont/cxx11/DeviceAdapterCxx11Thread.h. Its contents are the following, with minutiae such as include guards removed.

Example 5.3 Contents of the base header for a device adapter.
#include <viskores/cont/cxx11/internal/DeviceAdapterTagCxx11Thread.h>
#include <viskores/cont/cxx11/internal/DeviceAdapterRuntimeDetectorCxx11Thread.h>
#include <viskores/cont/cxx11/internal/DeviceAdapterMemoryManagerCxx11Thread.h>
#include <viskores/cont/cxx11/internal/RuntimeDeviceConfigurationCxx11Thread.h>
#include <viskores/cont/cxx11/internal/DeviceAdapterAlgorithmCxx11Thread.h>

The reason Viskores breaks up the code for its device adapters this way is that there is an interdependence between the implementation of each device adapter and the mechanism to pick a default device adapter. Breaking up the device adapter code in this way maintains an acyclic dependence among header files.

5.2.1. Tag

The device adapter tag, as described in Section 2.11.1 (Device Adapter Tag), is a simple empty type that is used as a template parameter to identify the device adapter. Every device adapter implementation provides one. The device adapter tag is typically defined in an internal header file with a prefix of DeviceAdapterTag.

The device adapter tag should be created with the VISKORES_VALID_DEVICE_ADAPTER macro. This macro takes an abbreviated name that it appends to DeviceAdapterTag to make the tag structure. It also creates support classes that allow Viskores to introspect the device adapter. The macro also expects a unique integer identifier that is usually stored in a macro prefixed with VISKORES_DEVICE_ADAPTER_. The identifiers for device adapters provided by core Viskores are declared in viskores/cont/internal/DeviceAdapterTag.h.

VISKORES_VALID_DEVICE_ADAPTER(Name, Id)

Creates a tag named viskores::cont::DeviceAdapterTagName and associated MPL structures to use this tag.

Always use this macro (in the base namespace) when creating a device adapter.

If the device adapter is not being compiled, then the header file should create the tag with VISKORES_INVALID_DEVICE_ADAPTER. This is common if the device adapter is not selected by CMake variables or the necessary libraries are not available.

VISKORES_INVALID_DEVICE_ADAPTER(Name, Id)

Marks the tag named viskores::cont::DeviceAdapterTagName and associated structures as invalid to use.

Always use this macro (in the base namespace) when creating a device adapter.

The following example gives the implementation of our custom device adapter, which by convention would be placed in the viskores/cont/cxx11/internal/DeviceAdapterTagCxx11Thread.h header file. This example assumes that a CMake configuration variable named VISKORES_ENABLE_CXX11THREAD, which is not documented here.

Example 5.4 Implementation of a device adapter tag.
 1#include <viskores/cont/DeviceAdapterTag.h>
 2
 3// If this device adapter were to be contributed to Viskores, then this macro
 4// declaration should be moved to DeviceAdapterTag.h and given a unique
 5// number. It also has to be less than VISKORES_MAX_DEVICE_ADAPTER_ID.
 6#define VISKORES_DEVICE_ADAPTER_CXX11_THREAD 6
 7
 8#ifdef VISKORES_ENABLE_CXX11THREAD
 9VISKORES_VALID_DEVICE_ADAPTER(Cxx11Thread, VISKORES_DEVICE_ADAPTER_CXX11_THREAD);
10#else
11VISKORES_INVALID_DEVICE_ADAPTER(Cxx11Thread, VISKORES_DEVICE_ADAPTER_CXX11_THREAD);
12#endif

This new device adapter tag needs to be added to viskores::cont::DeviceAdapterListCommon, which is defined in viskores/cont/DeviceAdapterList.h. Other components of Viskores will use this list to write code for the device. If you do not add the device tag to this list, then the device will not be tried when things are invoked in the execution environment, and directly specifying execution on this device will likely fail.

Example 5.5 Modification of DeviceAdapterListCommon in DeviceAdapterList.h.
using DeviceAdapterListCommon =
  viskores::List<viskores::cont::DeviceAdapterTagCuda,
                 viskores::cont::DeviceAdapterTagTBB,
                 viskores::cont::DeviceAdapterTagOpenMP,
                 viskores::cont::DeviceAdapterTagKokkos,
                 viskores::cont::DeviceAdapterTagCxx11Thread,
                 viskores::cont::DeviceAdapterTagSerial>;

Did You Know?

The order of device adapter tags in viskores::cont::DeviceAdapterListCommon matters. Devices will be tried in the order listed. Thus, the most “preferred” devices should be listed first. In Example 5.5, our new C++11 thread device will be used before the serial device but after the other parallel devices.

It is OK for viskores::cont::DeviceAdapterListCommon to contain device adapter tags for devices that are not being compiled for. These devices will be registered as inactive and skipped.

5.2.2. Runtime Detector

Viskores defines a template named viskores::cont::DeviceAdapterRuntimeDetector that detects whether a given device is available on the current system. DeviceAdapterRuntimeDetector has a single template argument: the device adapter tag.

template<class DeviceAdapterTag>
class DeviceAdapterRuntimeDetector

Class providing a device-specific runtime support detector.

The class provide the actual implementation used by viskores::cont::RuntimeDeviceInformation.

A default implementation is provided but device adapters which require physical hardware or other special runtime requirements should provide one (in conjunction with DeviceAdapterAlgorithm) where appropriate.

Public Functions

bool Exists() const

Returns true if the given device adapter is supported on the current machine.

No default implementation is provided as it could possible cause ODR violations when headers are included in differing order.

All device adapter implementations must create a specialization of viskores::cont::DeviceAdapterRuntimeDetector. They must contain a method named viskores::cont::DeviceAdapterRuntimeDetector::Exists() that returns true or false to indicate whether the device is available on the current runtime system. For our simple C++ threading example, C++ threading is always available, even if only one such processing element exists, so our implementation simply returns true if the device has been compiled. It determines whether the device is available using the flag DeviceAdapterTagCxx11Thread::IsEnabled at Example 5.6, line 12, which is created by either VISKORES_VALID_DEVICE_ADAPTER or VISKORES_INVALID_DEVICE_ADAPTER as demonstrated in Example 5.4.

Example 5.6 Implementation of a DeviceAdapterRuntimeDetector specialization.
 1namespace viskores
 2{
 3namespace cont
 4{
 5
 6template<>
 7class DeviceAdapterRuntimeDetector<viskores::cont::DeviceAdapterTagCxx11Thread>
 8{
 9public:
10  VISKORES_CONT bool Exists() const
11  {
12    return viskores::cont::DeviceAdapterTagCxx11Thread::IsEnabled;
13  }
14};
15
16} // namespace cont
17} // namespace viskores

5.2.3. Memory Manager

Viskores defines a template named viskores::cont::internal::DeviceAdapterMemoryManager that allocates memory on the device and copies datax. DeviceAdapterMemoryManager has a single template argument: the device adapter tag.

template<typename DeviceAdapterTag>
class DeviceAdapterMemoryManager

The device adapter memory manager.

Every device adapter is expected to define a specialization of DeviceAdapterMemoryManager. This class must be a (perhaps indirect) subclass of DeviceAdapterMemoryManagerBase. All abstract methods must be implemented.

All device adapter implementations must create a specialization of viskores::cont::internal::DeviceAdapterMemoryManager. This specialization must inherit from viskores::cont::internal::DeviceAdapterMemoryManagerBase. The memory manager allocates memory and returns it wrapped in a viskores::cont::internal::BufferInfo object. The superclass provides viskores::cont::internal::DeviceAdapterMemoryManagerBase::ManageArray(), which takes a raw device pointer, captured as a void*, along with metadata and management functions and returns that pointer wrapped in a BufferInfo management object.

class DeviceAdapterMemoryManagerBase

The base class for device adapter memory managers.

Every device adapter is expected to define a specialization of DeviceAdapterMemoryManager, and they are all expected to subclass this base class.

Subclassed by viskores::cont::internal::DeviceAdapterMemoryManager< viskores::cont::DeviceAdapterTagCuda >, viskores::cont::internal::DeviceAdapterMemoryManager< viskores::cont::DeviceAdapterTagKokkos >, viskores::cont::internal::DeviceAdapterMemoryManagerShared

Public Functions

virtual viskores::cont::internal::BufferInfo Allocate(viskores::BufferSizeType size) const = 0

Allocates a buffer of the specified size in bytes and returns a BufferInfo object containing information about it.

void Reallocate(viskores::cont::internal::BufferInfo &buffer, viskores::BufferSizeType newSize) const

Reallocates the provided buffer to a new size.

The passed in BufferInfo should be modified to reflect the changes.

BufferInfo ManageArray(void *memory, void *container, viskores::BufferSizeType size, viskores::cont::internal::BufferInfo::Deleter deleter, viskores::cont::internal::BufferInfo::Reallocater reallocater) const

Manages the provided array. Returns a BufferInfo object that contains the data.

virtual viskores::cont::DeviceAdapterId GetDevice() const = 0

Returns the device that this manager is associated with.

virtual viskores::cont::internal::BufferInfo CopyHostToDevice(const viskores::cont::internal::BufferInfo &src) const = 0

Copies data from the provided host buffer provided onto the device and returns a buffer info object holding the pointer for the device.

virtual void CopyHostToDevice(const viskores::cont::internal::BufferInfo &src, const viskores::cont::internal::BufferInfo &dest) const = 0

Copies data from the provided host buffer into the provided pre-allocated device buffer.

The BufferInfo object for the device was created by a previous call to this object.

virtual viskores::cont::internal::BufferInfo CopyDeviceToHost(const viskores::cont::internal::BufferInfo &src) const = 0

Copies data from the device buffer provided to the host.

The passed in BufferInfo object was created by a previous call to this object.

virtual void CopyDeviceToHost(const viskores::cont::internal::BufferInfo &src, const viskores::cont::internal::BufferInfo &dest) const = 0

Copies data from the device buffer provided into the provided pre-allocated host buffer.

The BufferInfo object for the device was created by a previous call to this object.

virtual viskores::cont::internal::BufferInfo CopyDeviceToDevice(const viskores::cont::internal::BufferInfo &src) const = 0

Deep copies data from one device buffer to another device buffer.

The passed in BufferInfo object was created by a previous call to this object.

virtual void CopyDeviceToDevice(const viskores::cont::internal::BufferInfo &src, const viskores::cont::internal::BufferInfo &dest) const = 0

Deep copies data from one device buffer to another device buffer.

The passed in BufferInfo objects were created by a previous call to this object.

virtual void *AllocateRawPointer(viskores::BufferSizeType size) const

Low-level method to allocate memory on the device.

This method allocates an array of the given number of bytes on the device and returns a void pointer to the array. The preferred method to allocate memory is to use the Allocate method, which returns a BufferInfo that manages its own memory. However, for cases where you are interfacing with code outside of Viskores and need just a raw pointer, this method can be used. The returned memory can be freed with DeleteRawPointer.

virtual void CopyDeviceToDeviceRawPointer(const void *src, void *dest, viskores::BufferSizeType size) const

Low-level method to copy data on the device.

This method copies data from one raw pointer to another. It performs the same function as CopyDeviceToDevice, except that it operates on raw pointers instead of BufferInfo objects. This is a useful low-level mechanism to move data on a device in memory locations created externally to Viskores.

virtual void DeleteRawPointer(void*) const = 0

Low-level method to delete memory on the device.

This method takes a pointer to memory allocated on the device and frees it. The preferred method to delete memory is to use the deallocation routines in BufferInfo objects created with Allocate. But for cases where you only have a raw pointer to the data, this method can be used to manage it. This method should only be used on memory allocated with this DeviceAdaperMemoryManager.

class BufferInfo

Public Types

using Deleter = void(void *container)

A function callback for deleting the memory.

using Reallocater = void(void *&memory, void *&container, viskores::BufferSizeType oldSize, viskores::BufferSizeType newSize)

A function callback for reallocating the memory.

Public Functions

void *GetPointer() const

Returns a pointer to the memory that is allocated.

This pointer may only be referenced on the associated device.

viskores::BufferSizeType GetSize() const

Returns the size of the buffer in bytes.

viskores::cont::DeviceAdapterId GetDevice() const

Returns the device on which this buffer is allocated.

If the buffer is not on a device (i.e. it is on the host), then DeviceAdapterIdUndefined is returned.

BufferInfo(const BufferInfo &src, viskores::cont::DeviceAdapterId device)

Shallow copy buffer from one host/device to another host/device.

Make sure that these two devices share the same memory space. (This is not checked and will cause badness if not correct.)

BufferInfo(viskores::cont::DeviceAdapterId device, void *memory, void *container, viskores::BufferSizeType size, Deleter deleter, Reallocater reallocater)

Creates a BufferInfo with the given memory, some (unknown) container holding that memory, a deletion function, and a reallocation function.

The deleter will be called with the pointer to the container when the buffer is released.

void Reallocate(viskores::BufferSizeType newSize)

Reallocates the buffer to a new size.

TransferredBuffer TransferOwnership()

Transfers ownership of the underlying allocation and Deleter and Reallocater to the caller.

After ownership has been transferred this buffer will be equivalent to one that was passed to Viskores as view only.

This means that the Deleter will do nothing, and the Reallocater will throw an ErrorBadAllocation.

void PreventReallocation()

Replace the buffer’s reallocater with one that throws ErrorBadAllocation on any attempted resize.

Used to lock the underlying pointer in place when the memory has been pinned for external access.

A specialization of viskores::cont::internal::DeviceAdapterMemoryManager must override the following pure virtual methods defined in the viskores::cont::internal::DeviceAdapterMemoryManagerBase superclass.

If the control and execution environments share the same memory space, the execution array manager can, and should, share buffers between the host and “device” and shallow-copy data when possible. Viskores provides viskores::cont::internal::DeviceAdapterMemoryManagerShared, which implements a device memory manager that shares a memory space with the control environment. In this case, the DeviceAdapterMemoryManager specialization needs to override only GetDevice. DeviceAdapterMemoryManagerShared provides all other necessary overrides.

Continuing our example of a device adapter based on C++11’s std::thread class, here is the implementation of DeviceAdapterMemoryManager, which by convention would be placed in the viskores/cont/cxx11/internal/DeviceAdapterMemoryManagerCxx11Thread.h header file. Because this threaded device has the same memory space as the control environment and the same memory management functions, the implementation is simplified by inheriting from viskores::cont::internal::DeviceAdapterMemoryManagerShared, which provides the memory management for a device that shares memory with the host.

Example 5.7 Specialization of DeviceAdapterMemoryManager.
 1#include <viskores/cont/cxx11/internal/DeviceAdapterTagCxx11Thread.h>
 2
 3#include <viskores/cont/internal/DeviceAdapterMemoryManager.h>
 4#include <viskores/cont/internal/DeviceAdapterMemoryManagerShared.h>
 5
 6namespace viskores
 7{
 8namespace cont
 9{
10namespace internal
11{
12
13template<>
14class DeviceAdapterMemoryManager<viskores::cont::DeviceAdapterTagCxx11Thread>
15  : public viskores::cont::internal::DeviceAdapterMemoryManagerShared
16{
17public:
18  VISKORES_CONT viskores::cont::DeviceAdapterId GetDevice() const override
19  {
20    return viskores::cont::DeviceAdapterTagCxx11Thread{};
21  }
22};
23
24}
25}
26} // namespace viskores::cont::internal

5.2.4. Runtime Device Configuration

Viskores defines a template named viskores::cont::internal::RuntimeDeviceConfiguration that makes it possible to initialize runtime configuration parameters of the underlying devices. viskores::cont::internal::RuntimeDeviceConfiguration has a single template argument: the device adapter tag.

template<typename DeviceAdapterTag>
class RuntimeDeviceConfiguration

All device adapter implementations must create a specialization of viskores::cont::internal::RuntimeDeviceConfiguration. This specialization must inherit from viskores::cont::internal::RuntimeDeviceConfigurationBase. viskores::cont::internal::RuntimeDeviceConfiguration provides various Set* and Get* methods for setting and accessing device-specific runtime parameters. The superclass provides viskores::cont::internal::RuntimeDeviceConfigurationBase::Initialize(), which takes a viskores::cont::internal::RuntimeDeviceConfigurationOptions argument used to set device parameters when Viskores is initialized.

class RuntimeDeviceConfigurationBase

Superclass for all RuntimeDeviceConfiguration classes.

Every device adapter must provide a specialization of RuntimeDeviceConfiguration, and every specialization must inherit from this class.

Subclassed by viskores::cont::internal::RuntimeDeviceConfiguration< viskores::cont::DeviceAdapterTagCuda >, viskores::cont::internal::RuntimeDeviceConfiguration< viskores::cont::DeviceAdapterTagKokkos >, viskores::cont::internal::RuntimeDeviceConfiguration< viskores::cont::DeviceAdapterTagOpenMP >, viskores::cont::internal::RuntimeDeviceConfiguration< viskores::cont::DeviceAdapterTagSerial >, viskores::cont::internal::RuntimeDeviceConfiguration< viskores::cont::DeviceAdapterTagTBB >

Public Functions

virtual viskores::cont::DeviceAdapterId GetDevice() const = 0

Returns a viskores::cont::DeviceAdapterId for the device that the runtime configuration oversees.

void Initialize(const RuntimeDeviceConfigurationOptions &configOptions)

Calls the various Set* methods in this class with the provided set of config options which can either be manually provided or automatically initialized from command line arguments and environment variables via viskores::cont::Initialize.

Each Set* method is called only if the corresponding viskores option is set, and a warning is logged based on the value of the RuntimeDeviceConfigReturnCode returned via the Set* method.

virtual RuntimeDeviceConfigReturnCode SetThreads(const viskores::Id &value)

Attempts to set the number of threads to use for this device.

Returns INVALID_FOR_DEVICE if the overridden device does not support setting this configuration.

virtual RuntimeDeviceConfigReturnCode SetDeviceInstance(const viskores::Id &value)

Attempts to set the device instance to use.

On systems that support multiple devices, the device to use in the current system process can be selected. Returns INVALID_FOR_DEVICE if the overridden device does not support setting this configuration.

virtual RuntimeDeviceConfigReturnCode GetThreads(viskores::Id &value) const

Attempts to get the number of threads to use for this device.

Returns INVALID_FOR_DEVICE if the overridden device does not support this parameter.

virtual RuntimeDeviceConfigReturnCode GetDeviceInstance(viskores::Id &value) const

Attempts to get the device instance to use.

On systems that support multiple devices, the device to use in the current system process can be selected. Returns INVALID_FOR_DEVICE if the overridden device does not support this parameter.

virtual RuntimeDeviceConfigReturnCode GetMaxThreads(viskores::Id &value) const

Provides the maximum value that can be used in SetThreads.

Returns INVALID_FOR_DEVICE if the overridden device does not support this parameter.

virtual RuntimeDeviceConfigReturnCode GetMaxDevices(viskores::Id &value) const

Provides the maximum value that can be used in SetDeviceInstance.

Returns INVALID_FOR_DEVICE if the overridden device does not support this parameter.

enum class viskores::cont::internal::RuntimeDeviceConfigReturnCode

Values:

enumerator SUCCESS
enumerator OUT_OF_BOUNDS
enumerator INVALID_FOR_DEVICE
enumerator INVALID_VALUE
enumerator NOT_APPLIED
class RuntimeDeviceConfigurationOptions

Provides a default set of RuntimeDeviceOptions that viskores currently supports setting.

Each option provided in this class should have a corresponding Set* method in the RuntimeDeviceConfiguration.

Public Functions

RuntimeDeviceConfigurationOptions(std::vector<option::Descriptor> &usage)

Calls the default constructor and additionally pushes back additional command line options to the provided usage vector for integration with the viskores option parser.

RuntimeDeviceConfigurationOptions(int &argc, char *argv[])

Allows the caller to initialize these runtime config arguments directly from command line arguments.

void Initialize(const option::Option *options)

Calls Initialize for each of this class’s current configuration options and marks the options as initialized.

bool IsInitialized() const

Returns whether this device has been initialized.

Public Members

RuntimeDeviceOption ViskoresNumThreads

Holds the number of threads this device is configured to use.

RuntimeDeviceOption ViskoresDeviceInstance

Holds the device instance being used by Viskores.

class RuntimeDeviceOption

Public Functions

RuntimeDeviceOption(const viskores::Id &index, const std::string &envName)

Constructs a RuntimeDeviceOption, sets the Source to NOT_SET.

Parameters:
  • index – Location of this command line argument in an option::Option array

  • envName – The environment variable name of this option

void Initialize(const option::Option *options)

Initializes this option’s value from the environment and then the provided options array in that order.

The options array is expected to be filled in using the viskores::cont::internal::option::OptionIndex with the usage vector defined in viskores::cont::Initialize.

void SetOptionFromEnvironment()

Sets the Value to the environment variable of the constructed EnvName.

void SetOptionFromOptionsArray(const option::Option *options)

Grabs and sets the option value using the constructed Index.

void SetOption(const viskores::Id &value)

Directly set the value for this option.

Specializations of viskores::cont::internal::RuntimeDeviceConfiguration must override viskores::cont::internal::RuntimeDeviceConfigurationBase::GetDevice(), which returns a viskores::cont::DeviceAdapterId for the device that the runtime configuration oversees. Specializations are not required to override the other methods defined in viskores::cont::internal::RuntimeDeviceConfigurationBase. These methods should be overridden only if suitable device-specific runtime parameters can be set or queried.

Continuing our example of a device adapter based on C++11’s std::thread class, here is the implementation of RuntimeDeviceConfiguration, which by convention would be placed in the viskores/cont/cxx11/internal/RuntimeDeviceConfigurationCxx11Thread.h header file.

Example 5.8 Specialization of RuntimeDeviceConfiguration.
 1#include <viskores/cont/cxx11/internal/DeviceAdapterTagCxx11Thread.h>
 2
 3#include <viskores/cont/internal/RuntimeDeviceConfiguration.h>
 4
 5#include <thread>
 6
 7namespace viskores
 8{
 9namespace cont
10{
11namespace internal
12{
13
14template<>
15class RuntimeDeviceConfiguration<viskores::cont::DeviceAdapterTagCxx11Thread>
16  : public viskores::cont::internal::RuntimeDeviceConfigurationBase
17{
18public:
19  VISKORES_CONT RuntimeDeviceConfiguration<viskores::cont::DeviceAdapterTagCxx11Thread>()
20    : NumThreads(std::thread::hardware_concurrency())
21  {
22  }
23
24  VISKORES_CONT viskores::cont::DeviceAdapterId GetDevice() const override
25  {
26    return viskores::cont::DeviceAdapterTagCxx11Thread{};
27  }
28
29  VISKORES_CONT viskores::cont::internal::RuntimeDeviceConfigReturnCode GetThreads(
30    viskores::Id& value) const override
31  {
32    value = this->NumThreads;
33    return viskores::cont::internal::RuntimeDeviceConfigReturnCode::SUCCESS;
34  }
35
36  VISKORES_CONT viskores::cont::internal::RuntimeDeviceConfigReturnCode SetThreads(
37    const viskores::Id& value) override
38  {
39    if ((value <= 0) ||
40        (value > static_cast<viskores::Id>(std::thread::hardware_concurrency())))
41    {
42      this->NumThreads = std::thread::hardware_concurrency();
43    }
44    else
45    {
46      this->NumThreads = value;
47    }
48    return viskores::cont::internal::RuntimeDeviceConfigReturnCode::SUCCESS;
49  }
50
51  VISKORES_CONT viskores::cont::internal::RuntimeDeviceConfigReturnCode GetMaxThreads(
52    viskores::Id& value) const override
53  {
54    value = std::thread::hardware_concurrency();
55    return viskores::cont::internal::RuntimeDeviceConfigReturnCode::SUCCESS;
56  }
57
58private:
59  viskores::Id NumThreads;
60};
61
62}
63}
64} // namespace viskores::cont::internal

Common Errors

viskores::cont::Initialize() automatically initializes the viskores::cont::internal::RuntimeDeviceConfiguration for every available device using parsed Viskores command-line arguments. These device runtime configurations are statically managed through viskores::cont::RuntimeDeviceInformation, which ensures that exactly one initialized instance of each viskores::cont::internal::RuntimeDeviceConfiguration is available for each device. This guarantees that runtime device configuration classes cannot be initialized more than once, but it can lead to device initialization inconsistencies when code attempts to access a configuration before calling viskores::cont::Initialize().

When creating a new viskores::cont::internal::RuntimeDeviceConfiguration, it is important to add an include for the new viskores::cont::DeviceAdapterRuntimeDetector header to viskores::cont::RuntimeDeviceInformation so that the new device is compiled correctly. Additionally, accessing a RuntimeDeviceConfiguration through viskores::cont::RuntimeDeviceInformation::GetRuntimeConfiguration() inside viskores::cont::DeviceAdapterRuntimeDetector::Exists() initializes the underlying device incorrectly because Viskores performs device-existence checks while parsing command-line arguments.

class RuntimeDeviceInformation

A class that can be used to determine if a given device adapter is supported on the current machine at runtime.

This is very important for device adapters where a physical hardware requirements such as a GPU or a Accelerator Card is needed for support to exist.

Public Functions

DeviceAdapterNameType GetName(DeviceAdapterId id) const

Returns the name corresponding to the device adapter id.

If id is not recognized, InvalidDeviceId is returned. Queries for a name are all case-insensitive.

DeviceAdapterId GetId(DeviceAdapterNameType name) const

Returns the id corresponding to the device adapter name.

If name is not recognized, DeviceAdapterTagUndefined is returned.

bool Exists(DeviceAdapterId id) const

Returns true if the given device adapter is supported on the current machine.

viskores::cont::internal::DeviceAdapterMemoryManagerBase &GetMemoryManager(DeviceAdapterId id) const

Returns a reference to a DeviceAdapterMemoryManager that will work with the given device.

This method will throw an exception if the device id is not a real device (for example DeviceAdapterTagAny). If the device in question is not valid, a DeviceAdapterMemoryManager will be returned, but attempting to call any of the methods will result in a runtime exception.

viskores::cont::internal::RuntimeDeviceConfigurationBase &GetRuntimeConfiguration(DeviceAdapterId id, const viskores::cont::internal::RuntimeDeviceConfigurationOptions &configOptions, int &argc, char *argv[] = nullptr) const

Returns a reference to a RuntimeDeviceConfiguration that will work with the given device.

If the device in question is not valid, a placeholder InvalidRuntimeDeviceConfiguration will be returned. Attempting to call any of the methods of this object will result in a runtime exception. The fully loaded version of this method is automatically called at the end of vkmt::cont::Initialize which performs automated setup of all runtime devices using parsed viskores arguments.

params: id - The specific device to retrieve the RuntimeDeviceConfiguration options for configOptions - Viskores provided options that should be included when initializing a given RuntimeDeviceConfiguration argc - The number of command line arguments to parse when Initializing a given RuntimeDeviceConfiguration argv - The extra command line arguments to parse when Initializing a given RuntimeDeviceConfiguration. This argument is mainlued used in conjunction with Kokkos config arg parsing to include specific &#8212;kokkos command line flags and environment variables.

5.2.5. Algorithms

A device adapter implementation must also provide a specialization of viskores::cont::DeviceAdapterAlgorithm, which provides the underlying implementation of the algorithms described in Chapter 4.15 (Device Algorithms). The implementation for the device adapter algorithms is typically placed in a header file with a prefix of viskores::cont::DeviceAdapterAlgorithm.

template<class DeviceAdapterTag>
struct DeviceAdapterAlgorithm

Struct containing device adapter algorithms.

This struct, templated on the device adapter tag, comprises static methods that implement the algorithms provided by the device adapter. The default struct is not implemented. Device adapter implementations must specialize the template.

Unnamed Group

static void Fill(viskores::cont::BitField &bits, bool value, viskores::Id numBits)

Fill the BitField with a specific pattern of bits.

For boolean values, all bits are set to 1 if value is true, or 0 if value is false. For word masks, the word type must be an unsigned integral type, which will be stamped across the BitField. If numBits is provided, the BitField is resized appropriately.

static void Fill(viskores::cont::BitField &bits, bool value)

Fill the BitField with a specific pattern of bits.

For boolean values, all bits are set to 1 if value is true, or 0 if value is false. For word masks, the word type must be an unsigned integral type, which will be stamped across the BitField. If numBits is provided, the BitField is resized appropriately.

template<typename WordType>
static void Fill(viskores::cont::BitField &bits, WordType word, viskores::Id numBits)

Fill the BitField with a specific pattern of bits.

For boolean values, all bits are set to 1 if value is true, or 0 if value is false. For word masks, the word type must be an unsigned integral type, which will be stamped across the BitField. If numBits is provided, the BitField is resized appropriately.

template<typename WordType>
static void Fill(viskores::cont::BitField &bits, WordType word)

Fill the BitField with a specific pattern of bits.

For boolean values, all bits are set to 1 if value is true, or 0 if value is false. For word masks, the word type must be an unsigned integral type, which will be stamped across the BitField. If numBits is provided, the BitField is resized appropriately.

Unnamed Group

template<typename T, typename S>
static void Fill(viskores::cont::ArrayHandle<T, S> &array, const T &value)

Fill array with value.

If numValues is specified, the array will be resized.

template<typename T, typename S>
static void Fill(viskores::cont::ArrayHandle<T, S> &array, const T &value, const viskores::Id numValues)

Fill array with value.

If numValues is specified, the array will be resized.

Public Static Functions

template<typename IndicesStorage>
static viskores::Id BitFieldToUnorderedSet(const viskores::cont::BitField &bits, viskores::cont::ArrayHandle<Id, IndicesStorage> &indices)

Create a unique, unsorted list of indices denoting which bits are set in a bitfield.

Returns the total number of set bits.

template<typename T, typename U, class CIn, class COut>
static void Copy(const viskores::cont::ArrayHandle<T, CIn> &input, viskores::cont::ArrayHandle<U, COut> &output)

Copy the contents of one ArrayHandle to another.

Copies the contents of input to output. The array output will be allocated to the same size of input. If output has already been allocated we will reallocate and clear any current values.

template<typename T, typename U, class CIn, class CStencil, class COut>
static void CopyIf(const viskores::cont::ArrayHandle<T, CIn> &input, const viskores::cont::ArrayHandle<U, CStencil> &stencil, viskores::cont::ArrayHandle<T, COut> &output)

Conditionally copy elements in the input array to the output array.

Calls the parallel primitive function of stream compaction on the input to remove unwanted elements. The result of the stream compaction is placed in output. The values in stencil are used to determine which input values are placed into output, with all stencil values not equal to the default constructor being considered valid. The size of output will be modified after this call as we can’t know the number of elements that will be removed by the stream compaction algorithm.

template<typename T, typename U, class CIn, class CStencil, class COut, class UnaryPredicate>
static void CopyIf(const viskores::cont::ArrayHandle<T, CIn> &input, const viskores::cont::ArrayHandle<U, CStencil> &stencil, viskores::cont::ArrayHandle<T, COut> &output, UnaryPredicate unary_predicate)

Conditionally copy elements in the input array to the output array.

Calls the parallel primitive function of stream compaction on the input to remove unwanted elements. The result of the stream compaction is placed in output. The values in stencil are passed to the unary comparison object which is used to determine which /c input values are placed into output. The size of output will be modified after this call as we can’t know the number of elements that will be removed by the stream compaction algorithm.

template<typename T, typename U, class CIn, class COut>
static bool CopySubRange(const viskores::cont::ArrayHandle<T, CIn> &input, viskores::Id inputStartIndex, viskores::Id numberOfElementsToCopy, viskores::cont::ArrayHandle<U, COut> &output, viskores::Id outputIndex = 0)

Copy the contents of a section of one ArrayHandle to another.

Copies the a range of elements of input to output. The number of elements is determined by numberOfElementsToCopy, and initial start position is determined by inputStartIndex. You can control where in the destination the copy should occur by specifying the outputIndex

If inputStartIndex + numberOfElementsToCopy is greater than the length of input we will only copy until we reach the end of the input array

If the outputIndex + numberOfElementsToCopy is greater than the length of output we will reallocate the output array so it can fit the number of elements we desire.

Requirements:

  • If input and output share memory, the input and output ranges must not overlap.

static viskores::Id CountSetBits(const viskores::cont::BitField &bits)

Returns the total number of “1” bits in BitField.

template<typename T, class CIn, class CVal, class COut>
static void LowerBounds(const viskores::cont::ArrayHandle<T, CIn> &input, const viskores::cont::ArrayHandle<T, CVal> &values, viskores::cont::ArrayHandle<viskores::Id, COut> &output)

Output is the first index in input for each item in values that wouldn’t alter the ordering of input.

LowerBounds is a vectorized search. From each value in values it finds the first place the item can be inserted in the ordered input array and stores the index in output.

Requirements:

  • input must already be sorted

template<typename T, class CIn, class CVal, class COut, class BinaryCompare>
static void LowerBounds(const viskores::cont::ArrayHandle<T, CIn> &input, const viskores::cont::ArrayHandle<T, CVal> &values, viskores::cont::ArrayHandle<viskores::Id, COut> &output, BinaryCompare binary_compare)

Output is the first index in input for each item in values that wouldn’t alter the ordering of input.

LowerBounds is a vectorized search. From each value in values it finds the first place the item can be inserted in the ordered input array and stores the index in output. Uses the custom comparison functor to determine the correct location for each item.

Requirements:

  • input must already be sorted

template<class CIn, class COut>
static void LowerBounds(const viskores::cont::ArrayHandle<viskores::Id, CIn> &input, viskores::cont::ArrayHandle<viskores::Id, COut> &values_output)

A special version of LowerBounds that does an in place operation.

This version of lower bounds performs an in place operation where each value in the values_output array is replaced by the index in input where it occurs. Because this is an in place operation, the type of the arrays is limited to viskores::Id.

template<typename T, typename U, class CIn>
static U Reduce(const viskores::cont::ArrayHandle<T, CIn> &input, U initialValue)

Compute a accumulated sum operation on the input ArrayHandle.

Computes an accumulated sum on the input ArrayHandle, returning the total sum. Reduce is similar to the stl accumulate sum function, exception that Reduce doesn’t do a serial summation. This means that if you have defined a custom plus operator for T it must be commutative, or you will get inconsistent results.

Returns:

The total sum.

template<typename T, typename U, class CIn, class BinaryFunctor>
static U Reduce(const viskores::cont::ArrayHandle<T, CIn> &input, U initialValue, BinaryFunctor binary_functor)

Compute a accumulated sum operation on the input ArrayHandle.

Computes an accumulated sum (or any user binary operation) on the input ArrayHandle, returning the total sum. Reduce is similar to the stl accumulate sum function, exception that Reduce doesn’t do a serial summation. This means that if you have defined a custom plus operator for T it must be commutative, or you will get inconsistent results.

Returns:

The total sum.

template<typename T, typename U, class CKeyIn, class CValIn, class CKeyOut, class CValOut, class BinaryFunctor>
static void ReduceByKey(const viskores::cont::ArrayHandle<T, CKeyIn> &keys, const viskores::cont::ArrayHandle<U, CValIn> &values, viskores::cont::ArrayHandle<T, CKeyOut> &keys_output, viskores::cont::ArrayHandle<U, CValOut> &values_output, BinaryFunctor binary_functor)

Compute a accumulated sum operation on the input key value pairs.

Computes a segmented accumulated sum (or any user binary operation) on the keys and values ArrayHandle(s). Each segmented accumulated sum is run on consecutive equal keys with the binary operation applied to all values inside that range. Once finished a single key and value is created for each segment.

template<typename T, class CIn, class COut>
static T ScanInclusive(const viskores::cont::ArrayHandle<T, CIn> &input, viskores::cont::ArrayHandle<T, COut> &output)

Compute an inclusive prefix sum operation on the input ArrayHandle.

Computes an inclusive prefix sum operation on the input ArrayHandle, storing the results in the output ArrayHandle. InclusiveScan is similar to the stl partial sum function, exception that InclusiveScan doesn’t do a serial summation. This means that if you have defined a custom plus operator for T it must be associative, or you will get inconsistent results. When the input and output ArrayHandles are the same ArrayHandle the operation will be done inplace.

Returns:

The total sum.

template<typename T, class CIn, class COut, class BinaryFunctor>
static T ScanInclusive(const viskores::cont::ArrayHandle<T, CIn> &input, viskores::cont::ArrayHandle<T, COut> &output, BinaryFunctor binary_functor)

Compute an inclusive prefix sum operation on the input ArrayHandle.

Computes an inclusive prefix sum operation on the input ArrayHandle, storing the results in the output ArrayHandle. InclusiveScan is similar to the stl partial sum function, exception that InclusiveScan doesn’t do a serial summation. This means that if you have defined a custom plus operator for T it must be associative, or you will get inconsistent results. When the input and output ArrayHandles are the same ArrayHandle the operation will be done inplace.

Returns:

The total sum.

template<typename T, typename U, typename KIn, typename VIn, typename VOut, typename BinaryFunctor>
static void ScanInclusiveByKey(const viskores::cont::ArrayHandle<T, KIn> &keys, const viskores::cont::ArrayHandle<U, VIn> &values, viskores::cont::ArrayHandle<U, VOut> &values_output, BinaryFunctor binary_functor)

Compute a segmented inclusive prefix sum operation on the input key value pairs.

Computes a segmented inclusive prefix sum (or any user binary operation) on the keys and values ArrayHandle(s). Each segmented inclusive prefix sum is run on consecutive equal keys with the binary operation applied to all values inside that range. Once finished the result is stored in values_output ArrayHandle.

template<typename T, typename U, typename KIn, typename VIn, typename VOut>
static void ScanInclusiveByKey(const viskores::cont::ArrayHandle<T, KIn> &keys, const viskores::cont::ArrayHandle<U, VIn> &values, viskores::cont::ArrayHandle<U, VOut> &values_output)

Compute a segmented inclusive prefix sum operation on the input key value pairs.

Computes a segmented inclusive prefix sum on the keys and values ArrayHandle(s). Each segmented inclusive prefix sum is run on consecutive equal keys with the binary operation viskores::Add applied to all values inside that range. Once finished the result is stored in values_output ArrayHandle.

template<typename T, class CIn, class COut>
static T ScanExclusive(const viskores::cont::ArrayHandle<T, CIn> &input, viskores::cont::ArrayHandle<T, COut> &output)

Compute an exclusive prefix sum operation on the input ArrayHandle.

Computes an exclusive prefix sum operation on the input ArrayHandle, storing the results in the output ArrayHandle. ExclusiveScan is similar to the stl partial sum function, exception that ExclusiveScan doesn’t do a serial summation. This means that if you have defined a custom plus operator for T it must be associative, or you will get inconsistent results. When the input and output ArrayHandles are the same ArrayHandle the operation will be done inplace.

Returns:

The total sum.

template<typename T, class CIn, class COut, class BinaryFunctor>
static T ScanExclusive(const viskores::cont::ArrayHandle<T, CIn> &input, viskores::cont::ArrayHandle<T, COut> &output, BinaryFunctor binaryFunctor, const T &initialValue)

Compute an exclusive prefix sum operation on the input ArrayHandle.

Computes an exclusive prefix sum operation on the input ArrayHandle, storing the results in the output ArrayHandle. ExclusiveScan is similar to the stl partial sum function, exception that ExclusiveScan doesn’t do a serial summation. This means that if you have defined a custom plus operator for T it must be associative, or you will get inconsistent results. When the input and output ArrayHandles are the same ArrayHandle the operation will be done inplace.

Returns:

The total sum.

template<typename T, typename U, typename KIn, typename VIn, typename VOut, class BinaryFunctor>
static void ScanExclusiveByKey(const viskores::cont::ArrayHandle<T, KIn> &keys, const viskores::cont::ArrayHandle<U, VIn> &values, viskores::cont::ArrayHandle<U, VOut> &output, const U &initialValue, BinaryFunctor binaryFunctor)

Compute a segmented exclusive prefix sum operation on the input key value pairs.

Computes a segmented exclusive prefix sum (or any user binary operation) on the keys and values ArrayHandle(s). Each segmented exclusive prefix sum is run on consecutive equal keys with the binary operation applied to all values inside that range. Once finished the result is stored in values_output ArrayHandle.

template<typename T, typename U, class KIn, typename VIn, typename VOut>
static void ScanExclusiveByKey(const viskores::cont::ArrayHandle<T, KIn> &keys, const viskores::cont::ArrayHandle<U, VIn> &values, viskores::cont::ArrayHandle<U, VOut> &output)

Compute a segmented exclusive prefix sum operation on the input key value pairs.

Computes a segmented inclusive prefix sum on the keys and values ArrayHandle(s). Each segmented inclusive prefix sum is run on consecutive equal keys with the binary operation viskores::Add applied to all values inside that range. Once finished the result is stored in values_output ArrayHandle.

template<typename T, class CIn, class COut>
static void ScanExtended(const viskores::cont::ArrayHandle<T, CIn> &input, viskores::cont::ArrayHandle<T, COut> &output)

Compute an extended prefix sum operation on the input ArrayHandle.

Computes an extended prefix sum operation on the input ArrayHandle, storing the results in the output ArrayHandle. The output array is one element longer than the input array. This produces an output array that contains both an inclusive scan (in elements [1, size]) and an exclusive scan (in elements [0, size-1]). As such, the first element of the output array always has the initial value and the last element of the output array always has the total sum. By using ArrayHandleView, arrays containing both inclusive and exclusive scans can be generated from an extended scan with minimal memory usage.

This algorithm may also be more efficient than ScanInclusive and ScanExclusive on some devices, since it may be able to avoid copying the total sum to the control environment to return.

ScanExtended is similar to the stl partial sum function, exception that ScanExtended doesn’t do a serial summation. This means that if you have defined a custom plus operator for T it must be associative, or you will get inconsistent results.

This overload of ScanExtended uses viskores::Add for the binary functor, and uses zero for the initial value of the scan operation.

template<typename T, class CIn, class COut, class BinaryFunctor>
static void ScanExtended(const viskores::cont::ArrayHandle<T, CIn> &input, viskores::cont::ArrayHandle<T, COut> &output, BinaryFunctor binaryFunctor, const T &initialValue)

Compute an extended prefix sum operation on the input ArrayHandle.

Computes an extended prefix sum operation on the input ArrayHandle, storing the results in the output ArrayHandle. The output array is one element longer than the input array. This produces an output array that contains both an inclusive scan (in elements [1, size]) and an exclusive scan (in elements [0, size-1]). As such, the first element of the output array always has the initial value and the last element of the output array always has the total sum. By using ArrayHandleView, arrays containing both inclusive and exclusive scans can be generated from an extended scan with minimal memory usage.

This algorithm may also be more efficient than ScanInclusive and ScanExclusive on some devices, since it may be able to avoid copying the total sum to the control environment to return.

ScanExtended is similar to the stl partial sum function, exception that ScanExtended doesn’t do a serial summation. This means that if you have defined a custom plus operator for T it must be associative, or you will get inconsistent results.

template<class Functor>
static void Schedule(Functor functor, viskores::Id numInstances)

Schedule many instances of a function to run on concurrent threads.

Calls the functor on several threads. This is the function used in the control environment to spawn activity in the execution environment. functor is a function-like object that can be invoked with the calling specification functor(viskores::Id index). It also has a method called from the control environment to establish the error reporting buffer with the calling specification functor.SetErrorMessageBuffer(const viskores::exec::internal::ErrorMessageBuffer &errorMessage). This object can be stored in the functor’s state such that if RaiseError is called on it in the execution environment, an ErrorExecution will be thrown from Schedule.

The argument of the invoked functor uniquely identifies the thread or instance of the invocation. There should be one invocation for each index in the range [0, numInstances].

template<class Functor, class IndiceType>
static void Schedule(Functor functor, viskores::Id3 rangeMax)

Schedule many instances of a function to run on concurrent threads.

Calls the functor on several threads. This is the function used in the control environment to spawn activity in the execution environment. functor is a function-like object that can be invoked with the calling specification functor(viskores::Id3 index) or functor(viskores::Id index). It also has a method called from the control environment to establish the error reporting buffer with the calling specification functor.SetErrorMessageBuffer(const viskores::exec::internal::ErrorMessageBuffer &errorMessage). This object can be stored in the functor’s state such that if RaiseError is called on it in the execution environment, an ErrorExecution will be thrown from Schedule.

The argument of the invoked functor uniquely identifies the thread or instance of the invocation. It is at the device adapter’s discretion whether to schedule on 1D or 3D indices, so the functor should have an operator() overload for each index type. If 3D indices are used, there is one invocation for every i, j, k value between [0, 0, 0] and rangeMax. If 1D indices are used, this Schedule behaves as if Schedule(functor, rangeMax[0]*rangeMax[1]*rangeMax[2]) were called.

template<typename T, class Storage>
static void Sort(viskores::cont::ArrayHandle<T, Storage> &values)

Unstable ascending sort of input array.

Sorts the contents of values so that they in ascending value. Doesn’t guarantee stability

template<typename T, class Storage, class BinaryCompare>
static void Sort(viskores::cont::ArrayHandle<T, Storage> &values, BinaryCompare binary_compare)

Unstable ascending sort of input array.

Sorts the contents of values so that they in ascending value based on the custom compare functor.

BinaryCompare should be a strict weak ordering comparison operator

template<typename T, typename U, class StorageT, class StorageU>
static void SortByKey(viskores::cont::ArrayHandle<T, StorageT> &keys, viskores::cont::ArrayHandle<U, StorageU> &values)

Unstable ascending sort of keys and values.

Sorts the contents of keys and values so that they in ascending value based on the values of keys.

template<typename T, typename U, class StorageT, class StorageU, class BinaryCompare>
static void SortByKey(viskores::cont::ArrayHandle<T, StorageT> &keys, viskores::cont::ArrayHandle<U, StorageU> &values, BinaryCompare binary_compare)

Unstable ascending sort of keys and values.

Sorts the contents of keys and values so that they in ascending value based on the custom compare functor.

BinaryCompare should be a strict weak ordering comparison operator

static void Synchronize()

Completes any asynchronous operations running on the device.

Waits for any asynchronous operations running on the device to complete.

template<typename T, typename U, typename V, typename StorageT, typename StorageU, typename StorageV, typename BinaryFunctor>
static void Transform(const viskores::cont::ArrayHandle<T, StorageT> &input1, const viskores::cont::ArrayHandle<U, StorageU> &input2, viskores::cont::ArrayHandle<V, StorageV> &output, BinaryFunctor binaryFunctor)

Apply a given binary operation function element-wise to input arrays.

Apply the give binary operation to pairs of elements from the two input array input1 and input2. The number of elements in the input arrays do not have to be the same, in this case, only the smaller of the two numbers of elements will be applied. Outputs of the binary operation is stored in output.

template<typename T, class Storage>
static void Unique(viskores::cont::ArrayHandle<T, Storage> &values)

Reduce an array to only the unique values it contains.

Removes all duplicate values in values that are adjacent to each other. Which means you should sort the input array unless you want duplicate values that aren’t adjacent. Note the values array size might be modified by this operation.

template<typename T, class Storage, class BinaryCompare>
static void Unique(viskores::cont::ArrayHandle<T, Storage> &values, BinaryCompare binary_compare)

Reduce an array to only the unique values it contains.

Removes all duplicate values in values that are adjacent to each other. Which means you should sort the input array unless you want duplicate values that aren’t adjacent. Note the values array size might be modified by this operation.

Uses the custom binary predicate Comparison to determine if something is unique. The predicate must return true if the two items are the same.

template<typename T, class CIn, class CVal, class COut>
static void UpperBounds(const viskores::cont::ArrayHandle<T, CIn> &input, const viskores::cont::ArrayHandle<T, CVal> &values, viskores::cont::ArrayHandle<viskores::Id, COut> &output)

Output is the last index in input for each item in values that wouldn’t alter the ordering of input.

UpperBounds is a vectorized search. From each value in values it finds the last place the item can be inserted in the ordered input array and stores the index in output.

Requirements:

  • input must already be sorted

template<typename T, class CIn, class CVal, class COut, class BinaryCompare>
static void UpperBounds(const viskores::cont::ArrayHandle<T, CIn> &input, const viskores::cont::ArrayHandle<T, CVal> &values, viskores::cont::ArrayHandle<viskores::Id, COut> &output, BinaryCompare binary_compare)

Output is the last index in input for each item in values that wouldn’t alter the ordering of input.

LowerBounds is a vectorized search. From each value in values it finds the last place the item can be inserted in the ordered input array and stores the index in output. Uses the custom comparison functor to determine the correct location for each item.

Requirements:

  • input must already be sorted

template<class CIn, class COut>
static void UpperBounds(const viskores::cont::ArrayHandle<viskores::Id, CIn> &input, viskores::cont::ArrayHandle<viskores::Id, COut> &values_output)

A special version of UpperBounds that does an in place operation.

This version of lower bounds performs an in place operation where each value in the values_output array is replaced by the last index in input where it occurs. Because this is an in place operation, the type of the arrays is limited to viskores::Id.

Although there are many methods in viskores::cont::DeviceAdapterAlgorithm, it is seldom necessary to implement them all. Instead, Viskores provides viskores::cont::internal::DeviceAdapterAlgorithmGeneral, which supplies generic implementations for most of the required algorithms. By deriving the specialization of viskores::cont::DeviceAdapterAlgorithm from viskores::cont::internal::DeviceAdapterAlgorithmGeneral, only viskores::cont::DeviceAdapterAlgorithm::Schedule() and viskores::cont::DeviceAdapterAlgorithm::Synchronize() need to be implemented. All other algorithms can be derived from those.

That said, not all algorithms implemented in viskores::cont::internal::DeviceAdapterAlgorithmGeneral are optimized for every type of device. Thus, it is worthwhile to provide algorithms optimized for the specific device when possible. In particular, it is best to provide specializations for the sort, scan, and reduce algorithms.

It is standard practice to implement a specialization of viskores::cont::DeviceAdapterAlgorithm by having it inherit from viskores::cont::internal::DeviceAdapterAlgorithmGeneral and specializing those methods that are optimized for a particular system. viskores::cont::internal::DeviceAdapterAlgorithmGeneral is a templated class that takes the derived algorithm and device adapter tag as template parameters. For example, a device adapter algorithm structure named DeviceAdapterAlgorithm<DeviceAdapterTagFoo> subclasses DeviceAdapterAlgorithmGeneral<DeviceAdapterAlgorithm<DeviceAdapterTagFoo>, DeviceAdapterTagFoo>.

template<class DerivedAlgorithm, class DeviceAdapterTag>
struct DeviceAdapterAlgorithmGeneral

General implementations of device adapter algorithms.

This struct provides algorithms that implement “general” device adapter algorithms. If a device adapter provides implementations for Schedule, and Synchronize, the rest of the algorithms can be implemented by calling these functions.

It should be noted that we recommend that you also implement Sort, ScanInclusive, and ScanExclusive for improved performance.

An easy way to implement the DeviceAdapterAlgorithm specialization is to subclass this and override the implementation of methods as necessary. As an example, the code would look something like this.

template<>
struct DeviceAdapterAlgorithm<DeviceAdapterTagFoo>
   : DeviceAdapterAlgorithmGeneral<DeviceAdapterAlgorithm<DeviceAdapterTagFoo>,
                                   DeviceAdapterTagFoo>
{
  template<typename Hints, typename Functor>
  VISKORES_CONT static void Schedule(Hints, Functor functor, viskores::Id numInstances)
  {
    ...
  }

  template<typename Functor>
  VISKORES_CONT static void Schedule(Functor&& functor, viskores::Id numInstances)
  {
    Schedule(viskores::cont::internal::HintList<>{}, functor, numInstances);
  }

  template<typename Hints, typename Functor>
  VISKORES_CONT static void Schedule(Hints, Functor functor, viskores::Id3 maxRange)
  {
    ...
  }

  template<typename Functor>
  VISKORES_CONT static void Schedule(Functor&& functor, viskores::Id3 maxRange)
  {
    Schedule(viskores::cont::internal::HintList<>{}, functor, numInstances);
  }

  VISKORES_CONT static void Synchronize()
  {
    ...
  }
};

You might note that DeviceAdapterAlgorithmGeneral has two template parameters that are redundant. Although the first parameter, the class for the actual DeviceAdapterAlgorithm class containing Schedule, and Synchronize is the same as DeviceAdapterAlgorithm<DeviceAdapterTag>, it is made a separate template parameter to avoid a recursive dependence between DeviceAdapterAlgorithmGeneral.h and DeviceAdapterAlgorithm.h

Did You Know?

The convention of having a base class be templated on the derived class’s type is known as the Curiously Recurring Template Pattern (CRTP). In the case of viskores::cont::internal::DeviceAdapterAlgorithmGeneral, Viskores uses this CRTP behavior to allow the general implementation of these algorithms to run viskores::cont::DeviceAdapterAlgorithm::Schedule() and other specialized algorithms in the subclass.

One point to note when implementing the viskores::cont::DeviceAdapterAlgorithm::Schedule() methods is to make sure that errors signaled in the execution environment are handled correctly. As described in Chapter 4.5 (Worklet Error Handling), errors are signaled in the execution environment by calling RaiseError on a functor or worklet object. This is handled internally by viskores::exec::internal::ErrorMessageBuffer. viskores::exec::internal::ErrorMessageBuffer holds a small string buffer, which must be provided by the device adapter’s viskores::cont::DeviceAdapterAlgorithm::Schedule() method.

Before viskores::cont::DeviceAdapterAlgorithm::Schedule() executes the functor it is given, it should allocate a small string array in the execution environment, initialize it to the empty string, encapsulate the array in an viskores::exec::internal::ErrorMessageBuffer object, and set this buffer object in the functor. When execution completes, viskores::cont::DeviceAdapterAlgorithm::Schedule() should check whether an error exists in this buffer and throw viskores::cont::ErrorExecution if an error has been reported.

Common Errors

Exceptions are generally not supposed to be thrown in the execution environment, but it can happen on devices that support them. Nevertheless, few thread schedulers work well when an exception is thrown in them. Thus, when implementing adapters for devices that support exceptions, it is good practice to catch them within the thread and report them through viskores::exec::internal::ErrorMessageBuffer.

The following example is a minimal implementation of device adapter algorithms using C++11’s std::thread class. No attempt at optimization has been made, although many optimizations are possible. By convention, this code would be placed in the viskores/cont/cxx11/internal/DeviceAdapterAlgorithmCxx11Thread.h header file.

Example 5.9 Minimal specialization of viskores::cont::DeviceAdapterAlgorithm.
  1#include <viskores/cont/cxx11/internal/DeviceAdapterTagCxx11Thread.h>
  2
  3#include <viskores/cont/DeviceAdapterAlgorithm.h>
  4#include <viskores/cont/ErrorExecution.h>
  5#include <viskores/cont/internal/DeviceAdapterAlgorithmGeneral.h>
  6
  7#include <thread>
  8
  9namespace viskores
 10{
 11namespace cont
 12{
 13
 14template<>
 15struct DeviceAdapterAlgorithm<viskores::cont::DeviceAdapterTagCxx11Thread>
 16  : viskores::cont::internal::DeviceAdapterAlgorithmGeneral<
 17      DeviceAdapterAlgorithm<viskores::cont::DeviceAdapterTagCxx11Thread>,
 18      viskores::cont::DeviceAdapterTagCxx11Thread>
 19{
 20private:
 21  template<typename FunctorType>
 22  struct ScheduleKernel1D
 23  {
 24    VISKORES_CONT ScheduleKernel1D(const FunctorType& functor)
 25      : Functor(functor)
 26    {
 27    }
 28
 29    void operator()() const
 30    {
 31      try
 32      {
 33        for (viskores::Id threadId = this->BeginId; threadId < this->EndId; threadId++)
 34        {
 35          this->Functor(threadId);
 36          // If an error is raised, abort execution.
 37          if (this->ErrorMessage.IsErrorRaised())
 38          {
 39            return;
 40          }
 41        }
 42      }
 43      catch (const viskores::cont::Error& error)
 44      {
 45        this->ErrorMessage.RaiseError(error.GetMessage().c_str());
 46      }
 47      catch (const std::exception& error)
 48      {
 49        this->ErrorMessage.RaiseError(error.what());
 50      }
 51      catch (...)
 52      {
 53        this->ErrorMessage.RaiseError("Unknown exception raised.");
 54      }
 55    }
 56
 57    FunctorType Functor;
 58    viskores::exec::internal::ErrorMessageBuffer ErrorMessage;
 59    viskores::Id BeginId;
 60    viskores::Id EndId;
 61  };
 62
 63  template<typename FunctorType>
 64  struct ScheduleKernel3D
 65  {
 66    VISKORES_CONT ScheduleKernel3D(const FunctorType& functor, viskores::Id3 maxRange)
 67      : Functor(functor)
 68      , MaxRange(maxRange)
 69    {
 70    }
 71
 72    void operator()() const
 73    {
 74      viskores::Id3 threadId3D(this->BeginId % this->MaxRange[0],
 75                               (this->BeginId / this->MaxRange[0]) % this->MaxRange[1],
 76                               this->BeginId / (this->MaxRange[0] * this->MaxRange[1]));
 77
 78      try
 79      {
 80        for (viskores::Id threadId = this->BeginId; threadId < this->EndId; threadId++)
 81        {
 82          this->Functor(threadId3D);
 83          // If an error is raised, abort execution.
 84          if (this->ErrorMessage.IsErrorRaised())
 85          {
 86            return;
 87          }
 88
 89          threadId3D[0]++;
 90          if (threadId3D[0] >= MaxRange[0])
 91          {
 92            threadId3D[0] = 0;
 93            threadId3D[1]++;
 94            if (threadId3D[1] >= MaxRange[1])
 95            {
 96              threadId3D[1] = 0;
 97              threadId3D[2]++;
 98            }
 99          }
100        }
101      }
102      catch (const viskores::cont::Error& error)
103      {
104        this->ErrorMessage.RaiseError(error.GetMessage().c_str());
105      }
106      catch (const std::exception& error)
107      {
108        this->ErrorMessage.RaiseError(error.what());
109      }
110      catch (...)
111      {
112        this->ErrorMessage.RaiseError("Unknown exception raised.");
113      }
114    }
115
116    FunctorType Functor;
117    viskores::exec::internal::ErrorMessageBuffer ErrorMessage;
118    viskores::Id BeginId;
119    viskores::Id EndId;
120    viskores::Id3 MaxRange;
121  };
122
123  template<typename KernelType>
124  VISKORES_CONT static void DoSchedule(KernelType kernel, viskores::Id numInstances)
125  {
126    if (numInstances < 1)
127    {
128      return;
129    }
130
131    const viskores::Id MESSAGE_SIZE = 1024;
132    char errorString[MESSAGE_SIZE];
133    errorString[0] = '\0';
134    viskores::exec::internal::ErrorMessageBuffer errorMessage(errorString, MESSAGE_SIZE);
135    kernel.Functor.SetErrorMessageBuffer(errorMessage);
136    kernel.ErrorMessage = errorMessage;
137
138    viskores::Id numThreads;
139
140    auto config = internal::RuntimeDeviceConfiguration<
141      viskores::cont::DeviceAdapterTagCxx11Thread>();
142    config.SetThreads(numInstances);
143    config.GetThreads(numThreads);
144    viskores::Id numInstancesPerThread = (numInstances + numThreads - 1) / numThreads;
145
146    std::thread* threadPool = new std::thread[numThreads];
147    viskores::Id beginId = 0;
148    for (viskores::Id threadIndex = 0; threadIndex < numThreads; threadIndex++)
149    {
150      viskores::Id endId = std::min(beginId + numInstancesPerThread, numInstances);
151      KernelType threadKernel = kernel;
152      threadKernel.BeginId = beginId;
153      threadKernel.EndId = endId;
154      std::thread newThread(threadKernel);
155      threadPool[threadIndex].swap(newThread);
156      beginId = endId;
157    }
158
159    for (viskores::Id threadIndex = 0; threadIndex < numThreads; threadIndex++)
160    {
161      threadPool[threadIndex].join();
162    }
163
164    delete[] threadPool;
165
166    if (errorMessage.IsErrorRaised())
167    {
168      throw viskores::cont::ErrorExecution(errorString);
169    }
170  }
171
172public:
173  template<typename FunctorType>
174  VISKORES_CONT static void Schedule(FunctorType functor, viskores::Id numInstances)
175  {
176    DoSchedule(ScheduleKernel1D<FunctorType>(functor), numInstances);
177  }
178
179  template<typename FunctorType>
180  VISKORES_CONT static void Schedule(FunctorType functor, viskores::Id3 maxRange)
181  {
182    viskores::Id numInstances = maxRange[0] * maxRange[1] * maxRange[2];
183    DoSchedule(ScheduleKernel3D<FunctorType>(functor, maxRange), numInstances);
184  }
185
186  VISKORES_CONT
187  static void Synchronize()
188  {
189    // Nothing to do. This device schedules all of its operations using a
190    // split/join paradigm. This means that the if the control threaad is
191    // calling this method, then nothing should be running in the execution
192    // environment.
193  }
194};
195
196} // namespace cont
197} // namespace viskores

5.2.6. Timer Implementation

The Viskores timer, described in Chapter 2.12 (Timers), delegates to an internal class named viskores::cont::DeviceAdapterTimerImplementation. The interface for this class is the same as that for viskores::cont::Timer. A default implementation of this templated class uses the system timer and the viskores::cont::DeviceAdapterAlgorithm::Synchronize() method in the device adapter algorithms.

However, some devices might provide alternate or better methods for implementing timers. For example, the TBB and CUDA libraries come with high-resolution timers that have better accuracy than standard system timers. Thus, the device adapter can optionally provide a specialization of viskores::cont::DeviceAdapterTimerImplementation, which is typically placed in the same header file as the device adapter algorithms.

template<class DeviceAdapterTag>
class DeviceAdapterTimerImplementation

Class providing a device-specific timer.

The class provide the actual implementation used by viskores::cont::Timer. A default implementation is provided but device adapters should provide one (in conjunction with DeviceAdapterAlgorithm) where appropriate. The interface for this class is exactly the same as viskores::cont::Timer.

Public Functions

inline DeviceAdapterTimerImplementation()

When a timer is constructed, all threads are synchronized and the current time is marked so that GetElapsedTime returns the number of seconds elapsed since the construction.

inline void Reset()

Resets the timer.

All further calls to GetElapsedTime will report the number of seconds elapsed since the call to this. This method synchronizes all asynchronous operations.

inline viskores::Float64 GetElapsedTime() const

Returns the elapsed time in seconds between the construction of this class or the last call to Reset and the time this function is called.

The time returned is measured in wall time. GetElapsedTime may be called any number of times to get the progressive time. This method synchronizes all asynchronous operations.

struct TimeStamp

Continuing our example of a custom device adapter using C++11’s std::thread class, we could use the default timer and it would work fine. But C++11 also comes with a std::chrono package that contains portable time functions. The following code demonstrates creating a custom timer for our device adapter using this package. By convention, viskores::cont::DeviceAdapterTimerImplementation is placed in the same header file as viskores::cont::DeviceAdapterAlgorithm.

 1#include <chrono>
 2
 3namespace viskores
 4{
 5namespace cont
 6{
 7
 8template<>
 9class DeviceAdapterTimerImplementation<viskores::cont::DeviceAdapterTagCxx11Thread>
10{
11public:
12  VISKORES_CONT
13  DeviceAdapterTimerImplementation() { this->Reset(); }
14
15  VISKORES_CONT
16  void Reset()
17  {
18    viskores::cont::DeviceAdapterAlgorithm<
19      viskores::cont::DeviceAdapterTagCxx11Thread>::Synchronize();
20    this->StartTime = std::chrono::high_resolution_clock::now();
21  }
22
23  VISKORES_CONT
24  viskores::Float64 GetElapsedTime()
25  {
26    viskores::cont::DeviceAdapterAlgorithm<
27      viskores::cont::DeviceAdapterTagCxx11Thread>::Synchronize();
28    std::chrono::high_resolution_clock::time_point endTime =
29      std::chrono::high_resolution_clock::now();
30
31    std::chrono::high_resolution_clock::duration elapsedTicks =
32      endTime - this->StartTime;
33
34    std::chrono::duration<viskores::Float64> elapsedSeconds(elapsedTicks);
35
36    return elapsedSeconds.count();
37  }
38
39private:
40  std::chrono::high_resolution_clock::time_point StartTime;
41};
42
43} // namespace cont
44} // namespace viskores