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.
#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.
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.
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.
-
bool Exists() const
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.
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 ofDeviceAdapterMemoryManagerBase. 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
BufferInfoshould 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
BufferInfoobject 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
BufferInfoobject 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
BufferInfoobject 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
BufferInfoobject 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
BufferInfoobject 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
BufferInfoobjects 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
Allocatemethod, which returns aBufferInfothat 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 withDeleteRawPointer.
-
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 ofBufferInfoobjects. 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
BufferInfoobjects created withAllocate. 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 thisDeviceAdaperMemoryManager.
-
virtual viskores::cont::internal::BufferInfo Allocate(viskores::BufferSizeType size) const = 0
-
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
viewonly.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
ErrorBadAllocationon any attempted resize.Used to lock the underlying pointer in place when the memory has been pinned for external access.
-
using Deleter = void(void *container)
A specialization of viskores::cont::internal::DeviceAdapterMemoryManager must override the following pure virtual methods defined in the viskores::cont::internal::DeviceAdapterMemoryManagerBase superclass.
viskores::cont::internal::DeviceAdapterMemoryManagerBase::GetDevice()
viskores::cont::internal::DeviceAdapterMemoryManagerBase::Allocate()
viskores::cont::internal::DeviceAdapterMemoryManagerBase::CopyHostToDevice()(two overloads)
viskores::cont::internal::DeviceAdapterMemoryManagerBase::CopyDeviceToHost()(two overloads)
viskores::cont::internal::DeviceAdapterMemoryManagerBase::CopyDeviceToDevice()(two overloads)
viskores::cont::internal::DeviceAdapterMemoryManagerBase::DeleteRawPointer()
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.
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
RuntimeDeviceConfigurationclasses.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::DeviceAdapterIdfor 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 theRuntimeDeviceConfigReturnCodereturned via theSet*method.
-
virtual RuntimeDeviceConfigReturnCode SetThreads(const viskores::Id &value)
Attempts to set the number of threads to use for this device.
Returns
INVALID_FOR_DEVICEif 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_DEVICEif 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_DEVICEif 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_DEVICEif 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_DEVICEif 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_DEVICEif the overridden device does not support this parameter.
-
virtual viskores::cont::DeviceAdapterId GetDevice() const = 0
-
enum class viskores::cont::internal::RuntimeDeviceConfigReturnCode
Values:
-
enumerator SUCCESS
-
enumerator OUT_OF_BOUNDS
-
enumerator INVALID_FOR_DEVICE
-
enumerator INVALID_VALUE
-
enumerator NOT_APPLIED
-
enumerator SUCCESS
-
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.
-
RuntimeDeviceConfigurationOptions(std::vector<option::Descriptor> &usage)
-
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.
-
RuntimeDeviceOption(const viskores::Id &index, const std::string &envName)
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.
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,
InvalidDeviceIdis 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
DeviceAdapterMemoryManagerthat 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, aDeviceAdapterMemoryManagerwill 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
RuntimeDeviceConfigurationthat will work with the given device.If the device in question is not valid, a placeholder
InvalidRuntimeDeviceConfigurationwill 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 ofvkmt::cont::Initializewhich 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 —kokkos command line flags and environment variables.
-
DeviceAdapterNameType GetName(DeviceAdapterId id) const
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.
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
inputtooutput. The arrayoutputwill be allocated to the same size ofinput. 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
inputto remove unwanted elements. The result of the stream compaction is placed inoutput. The values instencilare used to determine whichinputvalues are placed intooutput, with all stencil values not equal to the default constructor being considered valid. The size ofoutputwill 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
inputto remove unwanted elements. The result of the stream compaction is placed inoutput. The values instencilare passed to the unary comparison object which is used to determine which /c input values are placed intooutput. The size ofoutputwill 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
inputtooutput. The number of elements is determined bynumberOfElementsToCopy, and initial start position is determined byinputStartIndex. You can control where in the destination the copy should occur by specifying theoutputIndexIf inputStartIndex + numberOfElementsToCopy is greater than the length of
inputwe will only copy until we reach the end of the input arrayIf the
outputIndex+ numberOfElementsToCopy is greater than the length ofoutputwe will reallocate the output array so it can fit the number of elements we desire.- Requirements:
If
inputandoutputshare 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
valuesit finds the first place the item can be inserted in the orderedinputarray and stores the index inoutput.- Requirements:
inputmust 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
valuesit finds the first place the item can be inserted in the orderedinputarray and stores the index inoutput. Uses the custom comparison functor to determine the correct location for each item.- Requirements:
inputmust 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_outputarray is replaced by the index ininputwhere 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
inputArrayHandle, 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
inputArrayHandle, 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
keysandvaluesArrayHandle(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
inputArrayHandle, storing the results in theoutputArrayHandle. 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
inputArrayHandle, storing the results in theoutputArrayHandle. 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
keysandvaluesArrayHandle(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 invalues_outputArrayHandle.
-
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
keysandvaluesArrayHandle(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 invalues_outputArrayHandle.
-
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
inputArrayHandle, storing the results in theoutputArrayHandle. 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
inputArrayHandle, storing the results in theoutputArrayHandle. 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
keysandvaluesArrayHandle(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 invalues_outputArrayHandle.
-
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
keysandvaluesArrayHandle(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 invalues_outputArrayHandle.
-
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
inputArrayHandle, storing the results in theoutputArrayHandle. 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
inputArrayHandle, storing the results in theoutputArrayHandle. 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
functoron several threads. This is the function used in the control environment to spawn activity in the execution environment.functoris a function-like object that can be invoked with the calling specificationfunctor(viskores::Id index). It also has a method called from the control environment to establish the error reporting buffer with the calling specificationfunctor.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
functoron several threads. This is the function used in the control environment to spawn activity in the execution environment.functoris a function-like object that can be invoked with the calling specificationfunctor(viskores::Id3 index)orfunctor(viskores::Id index). It also has a method called from the control environment to establish the error reporting buffer with the calling specificationfunctor.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 ifSchedule(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
valuesso 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
valuesso 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
keysandvaluesso 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
keysandvaluesso 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
input1andinput2. 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 inoutput.
-
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
valuesthat 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
valuesthat 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
valuesit finds the last place the item can be inserted in the orderedinputarray and stores the index inoutput.- Requirements:
inputmust 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
valuesit finds the last place the item can be inserted in the orderedinputarray and stores the index inoutput. Uses the custom comparison functor to determine the correct location for each item.- Requirements:
inputmust 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_outputarray is replaced by the last index ininputwhere it occurs. Because this is an in place operation, the type of the arrays is limited to viskores::Id.
-
static void Fill(viskores::cont::BitField &bits, bool value, viskores::Id numBits)
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.
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
-
inline DeviceAdapterTimerImplementation()
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