5.1. Try Execute
Most operations in Viskores do not require specifying on which device to run.
For example, when using viskores::cont::Invoker to execute a worklet,
you do not need to specify a device; it chooses a device for you.
Internally, viskores::cont::Invoker has a mechanism to automatically
select a device, try it, and fall back to other devices if the first one fails.
We saw this at work in the implementation of filters in
Section 3.3.5 (Invoking a Worklet).
Furthermore, Section 2.11.4 (Specifying Devices) describes how users
can control which devices get used.
viskores::cont::Invoker internally uses
viskores::cont::TryExecute() to choose a device.
viskores::cont::TryExecute() can also be used in other situations where
a specific device needs to be chosen.
It provides a simple, generic mechanism to run an algorithm that requires a
device adapter without directly specifying one.
-
template<typename Functor, typename ...Args>
bool viskores::cont::TryExecute(Functor &&functor, Args&&... args) Try to execute a functor on a set of devices until one succeeds.
This function takes a functor and optionally a set of devices to compile support. It then tries to run the functor for each device (in the order given in the list) until the execution succeeds.
The TryExecute is also able to perfectly forward arbitrary arguments onto the functor. These arguments must be placed after the optional device adapter list and will passed to the functor in the same order as listed.
The functor must implement the function call operator (
operator()) with a return type ofbooland that istrueif the execution succeeds,falseif it fails. If an exception is thrown from the functor, then the execution is assumed to have failed. The functor call operator must also take at least one argument being the requiredDeviceAdapterTagto use.struct TryCallExample { template<typename DeviceList> bool operator()(DeviceList tags, int) const { return true; } }; // Executing without a deviceId, or device list viskores::cont::TryExecute(TryCallExample(), int{42}); // Executing with a device list using DeviceList = viskores::List<viskores::cont::DeviceAdapterTagSerial>; viskores::cont::TryExecute(TryCallExample(), DeviceList(), int{42});
This function returns
trueif the functor succeeded on a device,falseotherwise.If no device list is specified, then
VISKORES_DEFAULT_DEVICE_ADAPTER_LISTis used.
To demonstrate the operation of viskores::cont::TryExecute(), consider
an operation to find the average value of an array.
Doing so with a given device adapter is a straightforward use of the reduction
operator.
1template<typename T, typename Storage, typename Device>
2VISKORES_CONT T ArrayAverage(const viskores::cont::ArrayHandle<T, Storage>& array,
3 Device device)
4{
5 // Specialize a timer for this specific device.
6 viskores::cont::Timer timer;
7 timer.Reset(device);
8
9 // Call reduce on this specific device.
10 timer.Start();
11 T sum = viskores::cont::Algorithm::Reduce(device, array, T(0));
12 timer.Stop();
13
14 std::cout << "Elapsed reduction time: " << timer.GetElapsedTime() << std::endl;
15
16 return sum / T(array.GetNumberOfValues());
17}
The function in Example 5.1 requires a device adapter.
(This is a somewhat contrived example as viskores::cont::Algorithm::Reduce()
will internally select a device if not provided one, but this is a way to have
viskores::cont::Timer only synchronize the device being run on.)
We want to make an alternate version of this function that does not need a
specific device adapter but rather finds one to use.
To do this, we first make a functor as described in the
viskores::cont::TryExecute() API documentation.
It takes a device adapter tag as an argument, calls the version of the function
shown in Example 5.1, and returns true when the operation
succeeds.
We then create a new version of the array average function that does not need a
specific device adapter tag and calls viskores::cont::TryExecute() with
the aforementioned functor.
1namespace detail
2{
3
4struct ArrayAverageFunctor
5{
6 template<typename Device, typename T, typename Storage>
7 VISKORES_CONT bool operator()(Device device,
8 const viskores::cont::ArrayHandle<T, Storage>& inArray,
9 T& outValue) const
10 {
11 // Call the version of ArrayAverage that takes a DeviceAdapter.
12 outValue = ArrayAverage(inArray, device);
13
14 return true;
15 }
16};
17
18} // namespace detail
19
20template<typename T, typename Storage>
21VISKORES_CONT T ArrayAverage(const viskores::cont::ArrayHandle<T, Storage>& array)
22{
23 T outValue;
24
25 bool foundAverage =
26 viskores::cont::TryExecute(detail::ArrayAverageFunctor{}, array, outValue);
27
28 if (!foundAverage)
29 {
30 throw viskores::cont::ErrorExecution("Could not compute array average.");
31 }
32
33 return outValue;
34}