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 of bool and that is true if the execution succeeds, false if 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 required DeviceAdapterTag to 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 true if the functor succeeded on a device, false otherwise.

If no device list is specified, then VISKORES_DEFAULT_DEVICE_ADAPTER_LIST is 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.

Example 5.1 A function to find the average value of an array in parallel.
 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}