Error Handling

This guide was created for versions: v0.3.1 - Latest

Before diving into more complicated examples, we will cover error handling. That way we can make sure that we don't ignore any errors later on.

In general, there are two kinds of errors in SYCL. These are synchronous and asynchronous. Synchronous errors are classical C++ exceptions, thrown whenever the user calls a function with wrong arguments. These can be caught with a try..catch block.

Asynchronous errors, on the other hand, are those that describe faults in asynchronously executed code, for example inside a command group or a kernel. Since they can occur in a different stackframe, asynchronous error cannot be propagated up the stack. By default, they are considered lost. The way in which we can retrieve them is by providing an error handler function.

Error handling 〉 ≡

#include <CL/sycl.hpp>
#include <iostream>

namespace sycl = cl::sycl;

int main(int, char**) {
  <<Create exception handler>>

  <<Execute with wrong parameters>>

  return 0;
}

Create exception handler 〉 ≡

auto exception_handler = [] (sycl::exception_list exceptions) {
  for (std::exception_ptr const& e : exceptions) {
    try {
      std::rethrow_exception(e);
    } catch(sycl::exception const& e) {
      std::cout << "Caught asynchronous SYCL exception:\n"
		<< e.what() << std::endl;
    }
  }
};

The handler is a function object that accepts an exception_list [link] parameter. The parameter is an iterable list of std::exception_ptr objects. In our simple handler, we rethrow the pointer (there is no way to read from it directly), catch it, and output the exception description.

Execute with wrong parameters 〉 ≡

sycl::queue queue(sycl::default_selector{}, exception_handler);

queue.submit([&] (sycl::handler& cgh) {
  auto range = sycl::nd_range<1>(sycl::range<1>(1), sycl::range<1>(10));
  cgh.parallel_for<class invalid_kernel>(range, [=] (sycl::nd_item<1>) {});
});

try {
  queue.wait_and_throw();
} catch (sycl::exception const& e) {
  std::cout << "Caught synchronous SYCL exception:\n"
	    << e.what() << std::endl;
}

Then, we setup a default queue and supply it with an invalid kernel. The reason why this code is erroneous is unimportant for now (it has to do with work-group sizes). Finally, we call queue::wait_and_throw [link]. This function blocks and waits for all enqueued tasks to finish. Then, it sends all asynchronous exceptions to our handler. Additionally, it is possible, but very unlikely, for it to directly throw a synchronous exception. For completeness, we also catch these.

You might be wondering why the exception handler has to take an exception list rather than a single exception as the argument. A single wait_and_throw request might report multiple exceptions, so it is convenient to group them into a list object. Each request has a single list corresponding to it, so the user is not burdened with manually checking which errors are in which group.

The output we get back on the machine building this guide is:

 SYCL Exception: Error: [ComputeCpp:RT0301] Work-group size is invalid (Local size exceeds the global work group size)
Caught asynchronous SYCL exception:
Error: [ComputeCpp:RT0301] Work-group size is invalid (Local size exceeds the global work group size)