-
Notifications
You must be signed in to change notification settings - Fork 848
[SYCL] [DOC] Prepare design-document for assert feature #3461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
2911ea7
b69a1cd
15ea88e
ca08fec
1f8d9a9
2ee590c
77699a2
001a573
32b6479
b8637c2
b0cd85f
8c03648
121c945
13b40fd
a4b4884
c06db5f
823124a
a99368b
78d7fcb
6882e95
32663e0
2b84a83
423107b
7611511
a31b808
257054a
3f50173
c1326aa
5095b1a
5078fcc
4dc7b1f
9bcac02
7ec3ac8
8cbfde7
cc085f5
8835bf8
8835756
ecb8659
07debdb
995e4d8
b57ac48
d2f13ff
6281bc5
a5461f3
32a32f4
641d071
dc058a9
16fd8f0
fbca768
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,269 @@ | ||
| # Assert feature | ||
|
|
||
| **IMPORTANT**: This document is a draft. | ||
|
|
||
| Using the standard C++ `assert` API ("assertions") is an important debugging | ||
| technique widely used by developers. This document describes the design of | ||
| supporting assertions within SYCL device code. | ||
| The basic approach we chose is delivering device-side assertions as host-side | ||
| asynchronous exceptions, which allows further extensibility, such as better | ||
| error handling or potential recovery. | ||
|
|
||
| As usual, device-side assertions can be disabled by defining `NDEBUG` macro at | ||
| compile time. | ||
|
|
||
| ## Use-case example | ||
|
|
||
| ``` | ||
| using namespace sycl; | ||
| auto ErrorHandler = [] (exception_list Exs) { | ||
| for (exception_ptr const& E : Exs) { | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| try { | ||
| std::rethrow_exception(E); | ||
| } | ||
| catch (event_error const& Ex) { | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| std::cout << “Exception - ” << Ex.what(); // assertion failed | ||
| std::abort(); | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| }; | ||
|
|
||
| void user_func(item<2> Item) { | ||
| assert((Item[0] % 2) && “Nil”); | ||
| } | ||
|
|
||
| int main() { | ||
| queue Q(ErrorHandler); | ||
| q.submit([&] (handler& CGH) { | ||
| CGH.parallel_for<class TheKernel>(range<2>{N, M}, [=](item<2> It) { | ||
| do_smth(); | ||
| user_func(It); | ||
| do_smth_else(); | ||
| }); | ||
| }); | ||
| Q.wait_and_throw(); | ||
| std::cout << “One shouldn’t see this message.“; | ||
| return 0; | ||
| } | ||
| ``` | ||
|
|
||
| In this use-case every work-item with even X dimension will trigger assertion | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| failure. Assertion failure should be reported via asynchronous exceptions. If | ||
| asynchronous exception handler is set the failure is reported with | ||
| `sycl::event_error` exception. Otherwise, SYCL Runtime should trigger abort. | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| Even though multiple failures of the same or different assertions can happen in | ||
| multiple workitems, implementation is required to deliver only one. The | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| assertion failure message is printed to `stderr` by SYCL Runtime. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it happen always or only without
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it should always print the assertion message because:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, even if user set an
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it was weird when I first read this spec also. But then I tried the following test: The results: Despite the fact that I catch the SIGABRT and exit without printing anything, I still get a message printed to stderr. Therefore, it seems like the behavior defined in this spec is consistent with the way
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's kind of obvious due to the fact that In device-code, |
||
|
|
||
| When multiple kernels are enqueued and more than one fail at assertion, at least | ||
| single assertion should be reported. | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| ## User requirements | ||
|
|
||
| From user's point of view there are the following requirements: | ||
|
|
||
| | # | Title | Description | Importance | | ||
| | - | ----- | ----------- | ---------- | | ||
| | 1 | Handle assertion failure | Signal about assertion failure via SYCL asynchronous exception | Must have | | ||
| | 2 | Print assert message | Assert function should print message to stderr at host | Must have | | ||
| | 3 | Stop under debugger | When debugger is attached, break at assertion point | Highly desired | | ||
| | 4 | Reliability | Assert failure should be reported regardless of kernel deadlock | Highly desired | | ||
|
|
||
|
s-kanaev marked this conversation as resolved.
|
||
| Implementations without enough capabilities to implement fourth requirement are | ||
| allowed to realize the fallback approach described below, which does not | ||
| guarantee assertion failure delivery to host, but is still useful in many | ||
| practical cases. | ||
|
|
||
| ## Contents of `sycl::event_error` | ||
|
|
||
| Interface of `sycl::event_error` should look like: | ||
| ``` | ||
| class event_error : public runtime_error { | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| public: | ||
| event_error() = default; | ||
|
|
||
| event_error(const char *Msg, cl_int Err) | ||
| : event_error(string_class(Msg), Err) {} | ||
|
|
||
| event_error(const string_class &Msg, cl_int Err) : runtime_error(Msg, Err) {} | ||
| }; | ||
| ``` | ||
|
|
||
| Regardless of whether asynchronous exception handler is set or not, there's an | ||
| action to be performed by SYCL Runtime. To achieve this, information about | ||
| assert failure should be propagated from device-side to SYCL Runtime. This | ||
| should be performed via calls to `piEventGetInfo`. This Plugin Interface call | ||
| "lowers" to `clGetEventInfo` for OpenCL backend and `zeEventQueryStatus` for | ||
| Level-Zero backend. | ||
|
|
||
|
|
||
| ## Terms | ||
|
|
||
| - Device-side Runtime - runtime library supplied by the Native Device Compiler | ||
| and running on the device. | ||
| - Native Device Compiler - compiler which generates device-native binary image | ||
| based on input SPIR-V image. | ||
| - Low-level Runtime - the backend/runtime behind DPCPP Runtime attached via the | ||
| Plugin Interface. | ||
| - Accessor metadata - parts of accessor representation at device-side: pointer, | ||
| ranges, offset. | ||
|
|
||
|
|
||
| ## How it works? | ||
|
|
||
| `assert(expr)` macro ends up in call to `__devicelib_assert_fail`. This function | ||
| is part of [Device library extension](extensions/C-CXX-StandardLibrary/DeviceLibExtensions.rst#cl_intel_devicelib_cassert). | ||
|
|
||
| Implementation of this function is supplied by Native Device Compiler for | ||
| safe approach or by DPCPP Compiler for fallback one. | ||
|
|
||
| Due to lack of support of online linking in Level-Zero, the application is | ||
| linked against fallback implementation of `__devicelib_assert_fail`. Hence, | ||
| Native Device Compilers should prefer their implementation instead of the one | ||
| provided in incoming SPIR-V/LLVM IR binary. | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| ### Safe approach | ||
|
|
||
| This is the preferred approach and implementations should use it when possible. | ||
| It guarantees assertion failure notification delivery to the host regardless of | ||
| kernel behavior which hit the assertion. | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
|
|
||
| The Native Device Compiler is responsible for providing implementation of | ||
| `__devicelib_assert_fail` which completely hides details of communication | ||
| between the device code and the Low-Level Runtime from the SYCL device compiler | ||
| and runtime. The Low-Level Runtime is responsible for: | ||
| - detecting if assert failure took place; | ||
| - flushing assert message to `stderr` on host. | ||
|
s-kanaev marked this conversation as resolved.
|
||
|
|
||
| When detected, Low-level Runtime reports assert failure to DPCPP Runtime | ||
| via events objects. | ||
|
|
||
| Refer to [OpenCL](extensions/Assert/opencl.md) and [Level-Zero](extensions/Assert/level-zero.md) | ||
| extensions. | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| ### Fallback approach | ||
|
|
||
| If Device-side Runtime doesn't support `__devicelib_assert_fail` then a buffer | ||
| based approach comes in place. The approach doesn't require any support from | ||
| Device-side Runtime and Native Device Compiler. Neither it does from Low-level | ||
| Runtime. | ||
|
|
||
| Within this approach, a dedicated assert buffer is allocated and implicit kernel | ||
| argument is introduced. The argument is an accessor with `discard_read_write` | ||
| or `discard_write` access mode. Accessor metadata is stored to program scope | ||
|
s-kanaev marked this conversation as resolved.
Outdated
s-kanaev marked this conversation as resolved.
Outdated
|
||
| variable. This allows to refer to the accessor without modifying each and every | ||
| user's function. Fallback implementation of `__devicelib_assert_fail` restores | ||
| accessor metadata from program scope variable and writes assert information to | ||
| the assert buffer. Atomic operations are used in order to not overwrite existing | ||
| information. | ||
|
|
||
| DPCPP Runtime checks contents of the assert buffer for assert failure flag after | ||
| kernel finishes. | ||
|
|
||
| Both storing of accessor metadata and writing assert failure is performed with | ||
| help of built-ins. Implementations of these builtins are substituted by | ||
| frontend. | ||
|
|
||
| #### Built-ins operation | ||
|
|
||
| Accessor is a pointer augmented with offset and two ranges (access range and | ||
| memory range). | ||
|
|
||
| There are two built-ins provided by frontend: | ||
| * `__store_acc()` - to store accessor metadata into program-scope variable. | ||
| * `__store_assert_failure()` - to store flag about assert failure in a buffer | ||
| using the metadata stored in program-scope variable. | ||
|
|
||
| The accessor should be stored to program scope variable in global address space | ||
| using atomic operations. Motivation for using atomic operations: the program may | ||
| contain several kernels and some of them could be running simultaneously on a | ||
| single device. | ||
|
|
||
| The `__store_assert_failure()` built-in atomically sets a flag in a buffer. The | ||
| buffer is accessed using accessor metadata from program-scope variable. This | ||
| built-in return a boolean value which is `true` if the flag is set by this call | ||
| to `__store_assert_failure()` and `false` if the flag was already set. | ||
| Motivation for using atomic operation is the same as with `__store_acc()` | ||
| builtin. | ||
|
|
||
| The following pseudo-code snippets shows how these built-ins are used. | ||
| First of all, assume the following code as user's one: | ||
| ``` | ||
| void user_func(int X) { | ||
| assert(X && “X is nil”); | ||
| } | ||
|
|
||
| int main() { | ||
| queue Q(...); | ||
| Q.submit([&] (handler& CGH) { | ||
| CGH.single_task([=] () { | ||
| do_smth(); | ||
| user_func(0); | ||
| do_smth_else(); | ||
| }); | ||
| }); | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| The following LLVM IR pseudo code will be generated for the user's code: | ||
| ``` | ||
| @AssertBufferPtr = global void* null | ||
| @AssertBufferAccessRange = ... | ||
| @AssertBufferMemoryRange = ... | ||
| @AssertBufferOffset = ... | ||
|
|
||
| /// user's code | ||
| void user_func(int X) { | ||
| if (!(X && “X is nil")) { | ||
| __assert_fail(...); | ||
| } | ||
| } | ||
|
|
||
| users_kernel(...) { | ||
| do_smth() | ||
| user_func(0); | ||
| do_smth_else(); | ||
| } | ||
|
|
||
| /// a wrapped user's kernel | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| kernel(AssertBufferAccessor, OtherArguments...) { | ||
| __store_acc(AssertBufferAccessor); | ||
| users_kernel(OtherArguments...); | ||
| } | ||
|
|
||
| /// __assert_fail belongs to Linux version of devicelib | ||
| void __assert_fail(...) { | ||
| ... | ||
| __devicelib_assert_fail(...); | ||
| } | ||
|
|
||
| void __devicelib_assert_fail(Expr, File, Line, GlobalID, LocalID) { | ||
| ... | ||
| if (__store_assert_info()) | ||
| printf("Assertion `%s' failed in %s at line %i. GlobalID: %i, LocalID: %i", | ||
| Expr, File, Line, GlobalID, LocalID); | ||
|
s-kanaev marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /// The following are built-ins provided by frontend | ||
| void __store_acc(accessor) { | ||
| %1 = accessor.getPtr(); | ||
| store void * %1, void * @AssertBufferPtr | ||
| } | ||
|
|
||
| bool __store_assert_info(...) { | ||
| AssertBAcc = __fetch_acc(); | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
| // fill in data in AsBAcc | ||
| volatile int *Ptr = (volatile int *)AssertBAcc.getPtr(); | ||
| bool Expected = false; | ||
| bool Desired = true; | ||
|
|
||
| return atomic_cas(Ptr, Expected, Desired, SequentialConsistentMemoryOrder); | ||
| // or it could be: | ||
| // return !atomic_exchange(Ptr, Desired, SequentialConsistentMemoryOrder); | ||
| } | ||
| ``` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Overview | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
|
|
||
| This extension enables detection of assert failure of kernel. | ||
|
|
||
| # New enum value | ||
|
|
||
| `ze_result_t` enumeration should be augmented with `ZE_RESULT_ASSERT_FAILED` | ||
| enum element. This enum value indicated a detected assert failure at | ||
| device-side. | ||
|
|
||
| # Changed API | ||
|
|
||
| ``` | ||
| ze_event_handle_t Event; // describes an event of kernel been submitted previously | ||
| ze_result Result = zeEventQueryStatus(Event); | ||
| ``` | ||
|
|
||
| If kernel failed an assertion `zeEventQueryStatus` should return | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don;t think this is possible to achieve in asynchronous / non-blocking way in L0. We dont have any communication between kernel and event - so we can;t signal events with "assert happened" information. if we use global / program wide assert buffer - each kernel will be using the same assert happened flag - we do not have fine grain control to determine which kernel - and which connected event fired the assert. Fences could be used - allowing to synchronize at cmdQueue level and not kernel - any kernel causing assert executed in cmd Queue can then make fence synchronize to return error:https://spec.oneapi.com/level-zero/latest/core/PROG.html#fences
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it still possible in OpenCL?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you, please, provide more details about using fences? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fences are decribed in L0 spec - they are similar to events, but directly connected to command queues: https://spec.oneapi.com/level-zero/latest/core/PROG.html#fences In OpenCL the submission model is different - each enqueue is independent - single kernel is submitted ( queued) at a time. L0 operates on command lists that may contain multiple kernels - once cmd list is submitted to HW - we can;t control when a kernel in whole sequence is started completed. OpenCL handles kernels with printf in a blocking way - enqueueNDRangeKErnel with printf makes this a blocking call - so we have fine control when specific kernel is completed - we can do the same for assert() message - output event will be created when the kernel has already finished. I L0 this is not possible - as we would have to synchronize whoel command list. |
||
| `ZE_RESULT_ASSERT_FAILED`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # Overview | ||
|
|
||
| This extension enables detection of assert failure of kernel. | ||
|
s-kanaev marked this conversation as resolved.
Outdated
|
||
|
|
||
| # New error code | ||
|
|
||
| `CL_ASSERT_FAILURE` is added to indicate a detected assert failure at | ||
| device-side. | ||
|
|
||
| # Changed API | ||
|
|
||
| ``` | ||
| cl_event Event; // describes an event of kernel been submitted previously | ||
| cl_int Result; | ||
| size_t ResultSize; | ||
|
|
||
| clGetEventInfo(Event, CL_EVENT_COMMAND_EXECUTION_STATUS, sizeof(Result), &Result, &ResultSize); | ||
| ``` | ||
|
|
||
| If kernel failed an assertion `clGetEventInfo` should put `CL_ASSERT_FAILURE` | ||
| in `Result`. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.