//===------- Offload API tests - olLaunchKernel --------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "../common/Fixtures.hpp" #include #include #define KERNEL_TEST(NAME, KERNEL) \ struct olLaunchKernel##NAME##Test : LaunchSingleKernelTestBase { \ void SetUp() override { SetUpKernel(#KERNEL); } \ }; \ OFFLOAD_TESTS_INSTANTIATE_DEVICE_FIXTURE(olLaunchKernel##NAME##Test); KERNEL_TEST(Foo, foo) KERNEL_TEST(NoArgs, noargs) KERNEL_TEST(MultiArgs, multiargs) KERNEL_TEST(Composite, composite) KERNEL_TEST(Byte, byte) KERNEL_TEST(LocalMem, localmem) KERNEL_TEST(LocalMemReduction, localmem_reduction) KERNEL_TEST(LocalMemStatic, localmem_static) KERNEL_TEST(SingleCounterSyncEvent, single_counter) KERNEL_TEST(GlobalCtor, global_ctor) KERNEL_TEST(GlobalDtor, global_dtor) KERNEL_TEST(GridSize, gridsize) struct LaunchMultipleKernelTestBase : LaunchKernelTestBase { void SetUpKernels(const char *program, std::vector kernels) { RETURN_ON_FATAL_FAILURE(SetUpProgram(program)); Kernels.resize(kernels.size()); size_t I = 0; for (auto K : kernels) ASSERT_SUCCESS( olGetSymbol(Program, K, OL_SYMBOL_KIND_KERNEL, &Kernels[I++])); } std::vector Kernels; }; struct ArgsSingleCounter { int32_t InitLoop; int32_t Addend; uint32_t *InitVal; uint32_t *Out; }; #define KERNEL_MULTI_TEST(NAME, PROGRAM, ...) \ struct olLaunchKernel##NAME##Test : LaunchMultipleKernelTestBase { \ void SetUp() override { SetUpKernels(#PROGRAM, {__VA_ARGS__}); } \ }; \ OFFLOAD_TESTS_INSTANTIATE_DEVICE_FIXTURE(olLaunchKernel##NAME##Test); KERNEL_MULTI_TEST(Global, global, "write", "read") TEST_P(olLaunchKernelFooTest, Success) { void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.GroupSize.x * sizeof(uint32_t), &Mem)); void *ArgPtrs[] = {&Mem}; size_t ArgSizes[] = {sizeof(Mem)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); uint32_t *Data = (uint32_t *)Mem; for (uint32_t i = 0; i < 64; i++) { ASSERT_EQ(Data[i], i); } ASSERT_SUCCESS(olMemFree(Mem)); } TEST_P(olLaunchKernelFooTest, SuccessThreaded) { SKIP_KNOWN_FAILURE(LevelZero{"thread-safety issues"}); threadify([&](size_t) { void *DevAlloc, *HstAlloc; size_t Size = LaunchArgs.GroupSize.x * sizeof(uint32_t); ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_DEVICE, Size, &DevAlloc)); ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_HOST, Size, &HstAlloc)); void *ArgPtrs[] = {&DevAlloc}; size_t ArgSizes[] = {sizeof(DevAlloc)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olMemcpy(Queue, HstAlloc, Host, DevAlloc, Device, Size)); ASSERT_SUCCESS(olSyncQueue(Queue)); uint32_t *Data = static_cast(HstAlloc); for (uint32_t i = 0; i < LaunchArgs.GroupSize.x; i++) { ASSERT_EQ(Data[i], i); } ASSERT_SUCCESS(olMemFree(DevAlloc)); ASSERT_SUCCESS(olMemFree(HstAlloc)); }); } TEST_P(olLaunchKernelNoArgsTest, Success) { ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, 0, nullptr, nullptr)); ASSERT_SUCCESS(olSyncQueue(Queue)); } TEST_P(olLaunchKernelMultiArgsTest, Success) { void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.GroupSize.x * sizeof(int), &Mem)); char A = 3; int *B = (int *)Mem; short C = 5; void *ArgPtrs[] = {&A, &B, &C}; size_t ArgSizes[] = {sizeof(A), sizeof(B), sizeof(C)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); int *Data = (int *)Mem; for (uint32_t i = 0; i < LaunchArgs.GroupSize.x; i++) ASSERT_EQ(Data[i], A + C + static_cast(i)); ASSERT_SUCCESS(olMemFree(Mem)); } struct Foo { uint32_t a; uint32_t b; }; TEST_P(olLaunchKernelCompositeTest, Success) { void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.GroupSize.x * sizeof(uint32_t), &Mem)); uint8_t N = 1; Foo F{2, 3}; uint32_t *Out = (uint32_t *)Mem; void *ArgPtrs[] = {&N, &F, &Out}; size_t ArgSizes[] = {sizeof(N), sizeof(F), sizeof(Out)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); uint32_t *Data = (uint32_t *)Mem; for (uint32_t i = 0; i < LaunchArgs.GroupSize.x; i++) ASSERT_EQ(Data[i], N + F.a + F.b + i); ASSERT_SUCCESS(olMemFree(Mem)); } TEST_P(olLaunchKernelFooTest, SuccessSynchronous) { void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.GroupSize.x * sizeof(uint32_t), &Mem)); void *ArgPtrs[] = {&Mem}; size_t ArgSizes[] = {sizeof(Mem)}; ASSERT_SUCCESS(olLaunchKernel(nullptr, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); uint32_t *Data = (uint32_t *)Mem; for (uint32_t i = 0; i < 64; i++) { ASSERT_EQ(Data[i], i); } ASSERT_SUCCESS(olMemFree(Mem)); } TEST_P(olLaunchKernelByteTest, Success) { unsigned char C = 42; void *ArgPtrs[] = {&C}; size_t ArgSizes[] = {sizeof(C)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); } TEST_P(olLaunchKernelLocalMemTest, Success) { SKIP_KNOWN_FAILURE(LevelZero{"unsupported DynSharedMemory"}); LaunchArgs.NumGroups.x = 4; LaunchArgs.DynSharedMemory = 64 * sizeof(uint32_t); void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.GroupSize.x * LaunchArgs.NumGroups.x * sizeof(uint32_t), &Mem)); void *ArgPtrs[] = {&Mem}; size_t ArgSizes[] = {sizeof(Mem)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); uint32_t *Data = (uint32_t *)Mem; for (uint32_t i = 0; i < LaunchArgs.GroupSize.x * LaunchArgs.NumGroups.x; i++) ASSERT_EQ(Data[i], (i % 64) * 2); ASSERT_SUCCESS(olMemFree(Mem)); } TEST_P(olLaunchKernelLocalMemReductionTest, Success) { SKIP_KNOWN_FAILURE(LevelZero{"unsupported DynSharedMemory"}); LaunchArgs.NumGroups.x = 4; LaunchArgs.DynSharedMemory = 64 * sizeof(uint32_t); void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.NumGroups.x * sizeof(uint32_t), &Mem)); void *ArgPtrs[] = {&Mem}; size_t ArgSizes[] = {sizeof(Mem)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); uint32_t *Data = (uint32_t *)Mem; for (uint32_t i = 0; i < LaunchArgs.NumGroups.x; i++) ASSERT_EQ(Data[i], 2 * LaunchArgs.GroupSize.x); ASSERT_SUCCESS(olMemFree(Mem)); } TEST_P(olLaunchKernelLocalMemStaticTest, Success) { SKIP_KNOWN_FAILURE(LevelZero{"unsupported DynSharedMemory"}); LaunchArgs.NumGroups.x = 4; LaunchArgs.DynSharedMemory = 0; void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.NumGroups.x * sizeof(uint32_t), &Mem)); void *ArgPtrs[] = {&Mem}; size_t ArgSizes[] = {sizeof(Mem)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); uint32_t *Data = (uint32_t *)Mem; for (uint32_t i = 0; i < LaunchArgs.NumGroups.x; i++) ASSERT_EQ(Data[i], 2 * LaunchArgs.GroupSize.x); ASSERT_SUCCESS(olMemFree(Mem)); } // The test intends to verify the correctness of the current implementation of // the event synchronisation. TEST_P(olLaunchKernelSingleCounterSyncEventTest, SuccessSyncEvent) { void *InitValuePassed; void *ResNum; size_t Size = sizeof(uint32_t); ASSERT_SUCCESS( olMemAlloc(Device, OL_ALLOC_TYPE_DEVICE, Size, &InitValuePassed)); ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_DEVICE, Size, &ResNum)); uint32_t HostInitVal = 0; ASSERT_SUCCESS( olMemcpy(Queue, InitValuePassed, Device, &HostInitVal, Host, Size)); ASSERT_SUCCESS(olMemcpy(Queue, ResNum, Device, &HostInitVal, Host, Size)); ASSERT_SUCCESS(olSyncQueue(Queue)); // The execution time of the provided kernel should be high enough to ensure // that the read value of the final result is not correct without explicit // synchronization: explicit waiting for the event following the submitted // operation. LoopRange is arbitrarily set with the goal of prolonging the // kernel execution through a high number of loop iterations. In each // iteration, NumberToAdd is added to the final sum, which is stored in // ResNum. int32_t LoopRange = 1000000; int32_t NumberToAdd = 2; ArgsSingleCounter Args{LoopRange, NumberToAdd, (uint32_t *)InitValuePassed, (uint32_t *)ResNum}; void *ArgPtrs[] = {&Args.InitLoop, &Args.Addend, &Args.InitVal, &Args.Out}; size_t ArgSizes[] = {sizeof(Args.InitLoop), sizeof(Args.Addend), sizeof(Args.InitVal), sizeof(Args.Out)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); uint32_t FinalResVal = 0; ASSERT_SUCCESS(olMemcpy(Queue, &FinalResVal, Host, ResNum, Device, Size)); ol_event_handle_t Event = nullptr; ASSERT_SUCCESS(olCreateEvent(Queue, OL_EVENT_FLAGS_NONE, &Event)); ASSERT_SUCCESS(olSyncEvent(Event)); ASSERT_EQ(FinalResVal, NumberToAdd * LoopRange); ASSERT_SUCCESS(olMemFree(InitValuePassed)); ASSERT_SUCCESS(olMemFree(ResNum)); } // The test checks the correctness of the synchronization between queues using // events. Enqueueing the kernel on the queue `Q1` should produce `Result1`, // which is used as an initial value for the queue `Q2`. Therefore, before // executing any future work, `Q2` should wait for the event `Event1`, which is // created after submitting work on `Q1`. The required synchronization is // ensured by `olWaitEvents(Q2, &Event1, 1)`. If `Q2` uses the value passed as // `Result1` before the work on `Q1` has completed, the result of the kernel // enqueued on `Q2` would be incorrect. TEST_P(olLaunchKernelSingleCounterSyncEventTest, SuccessTwoQueues) { void *InitValuePassed; void *ResNum1; void *ResNum2; size_t Size = sizeof(uint32_t); ASSERT_SUCCESS( olMemAlloc(Device, OL_ALLOC_TYPE_DEVICE, Size, &InitValuePassed)); ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_DEVICE, Size, &ResNum1)); ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_DEVICE, Size, &ResNum2)); uint32_t HostInitVal = 0; ASSERT_SUCCESS( olMemcpy(Queue, InitValuePassed, Device, &HostInitVal, Host, Size)); ASSERT_SUCCESS(olMemcpy(Queue, ResNum1, Device, &HostInitVal, Host, Size)); ASSERT_SUCCESS(olMemcpy(Queue, ResNum2, Device, &HostInitVal, Host, Size)); ASSERT_SUCCESS(olSyncQueue(Queue)); ol_queue_handle_t Queue2 = nullptr; ASSERT_SUCCESS(olCreateQueue(Device, &Queue2)); // For the explanation of the reasoning behind particular values assigned to // parameters, see the comment in the Success test from the same test suite int32_t LoopRange = 1000000; int32_t NumberToAdd = 2; ArgsSingleCounter Args{LoopRange, NumberToAdd, (uint32_t *)InitValuePassed, (uint32_t *)ResNum1}; void *ArgPtrs[] = {&Args.InitLoop, &Args.Addend, &Args.InitVal, &Args.Out}; size_t ArgSizes[] = {sizeof(Args.InitLoop), sizeof(Args.Addend), sizeof(Args.InitVal), sizeof(Args.Out)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ol_event_handle_t Event = nullptr; ASSERT_SUCCESS(olCreateEvent(Queue, OL_EVENT_FLAGS_NONE, &Event)); ASSERT_SUCCESS(olWaitEvents(Queue2, &Event, 1)); ArgsSingleCounter Args2{LoopRange, NumberToAdd, (uint32_t *)ResNum1, (uint32_t *)ResNum2}; void *ArgPtrs2[] = {&Args2.InitLoop, &Args2.Addend, &Args2.InitVal, &Args2.Out}; // At the beginning of the kernel, ResNum from the first queue is saved // locally as the initial value. If operations enqueued before Event have not // been completed by the time the kernel is executed using the second queue, // the FinalResVal would be incorrect. ASSERT_SUCCESS(olLaunchKernel(Queue2, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs2), ArgPtrs2, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue2)); uint32_t FinalResVal = 0; ASSERT_SUCCESS(olMemcpy(Queue2, &FinalResVal, Host, ResNum2, Device, Size)); ASSERT_SUCCESS(olSyncQueue(Queue2)); ASSERT_EQ(FinalResVal, 2 * NumberToAdd * LoopRange); ASSERT_SUCCESS(olMemFree(InitValuePassed)); ASSERT_SUCCESS(olMemFree(ResNum1)); ASSERT_SUCCESS(olMemFree(ResNum2)); } TEST_P(olLaunchKernelGlobalTest, Success) { void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.GroupSize.x * sizeof(uint32_t), &Mem)); void *ArgPtrs[] = {&Mem}; size_t ArgSizes[] = {sizeof(Mem)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernels[0], &LaunchArgs, nullptr, 0, nullptr, nullptr)); ASSERT_SUCCESS(olSyncQueue(Queue)); ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernels[1], &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); uint32_t *Data = (uint32_t *)Mem; for (uint32_t i = 0; i < 64; i++) { ASSERT_EQ(Data[i], i * 2); } ASSERT_SUCCESS(olMemFree(Mem)); } TEST_P(olLaunchKernelGlobalTest, InvalidNotAKernel) { ol_symbol_handle_t Global = nullptr; ASSERT_SUCCESS( olGetSymbol(Program, "global", OL_SYMBOL_KIND_GLOBAL_VARIABLE, &Global)); ASSERT_ERROR(OL_ERRC_SYMBOL_KIND, olLaunchKernel(Queue, Device, Global, &LaunchArgs, nullptr, 0, nullptr, nullptr)); } TEST_P(olLaunchKernelGlobalCtorTest, Success) { SKIP_KNOWN_FAILURE(LevelZero{"unsupported feature"}); void *Mem; ASSERT_SUCCESS(olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, LaunchArgs.GroupSize.x * sizeof(uint32_t), &Mem)); void *ArgPtrs[] = {&Mem}; size_t ArgSizes[] = {sizeof(Mem)}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); uint32_t *Data = (uint32_t *)Mem; for (uint32_t i = 0; i < 64; i++) { ASSERT_EQ(Data[i], i + 100); } ASSERT_SUCCESS(olMemFree(Mem)); } TEST_P(olLaunchKernelGlobalDtorTest, Success) { // TODO: We can't inspect the result of a destructor yet, once we // find/implement a way, update this test. For now we just check that nothing // crashes ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, 0, nullptr, nullptr)); ASSERT_SUCCESS(olSyncQueue(Queue)); } TEST_P(olLaunchKernelGridSizeTest, Success) { void *Mem; ASSERT_SUCCESS( olMemAlloc(Device, OL_ALLOC_TYPE_MANAGED, 6 * sizeof(uint32_t), &Mem)); uint32_t *NumBlocks = static_cast(Mem); uint32_t *NumThreads = static_cast(Mem) + 3; void *ArgPtrs[] = {&NumBlocks, &NumThreads}; size_t ArgSizes[] = {sizeof(NumBlocks), sizeof(NumThreads)}; const uint32_t BaseBlocks[3] = {64, 16, 8}; const uint32_t BaseThreads[3] = {32, 4, 2}; for (uint32_t Dim = 1; Dim <= 3; ++Dim) { NumBlocks[0] = NumBlocks[1] = NumBlocks[2] = 0; NumThreads[0] = NumThreads[1] = NumThreads[2] = 0; LaunchArgs.Dimensions = 3; LaunchArgs.NumGroups = {BaseBlocks[0], Dim >= 2 ? BaseBlocks[1] : 1, Dim >= 3 ? BaseBlocks[2] : 1}; LaunchArgs.GroupSize = {BaseThreads[0], Dim >= 2 ? BaseThreads[1] : 1, Dim >= 3 ? BaseThreads[2] : 1}; ASSERT_SUCCESS(olLaunchKernel(Queue, Device, Kernel, &LaunchArgs, nullptr, std::size(ArgPtrs), ArgPtrs, ArgSizes)); ASSERT_SUCCESS(olSyncQueue(Queue)); ASSERT_EQ(NumBlocks[0], LaunchArgs.NumGroups.x); ASSERT_EQ(NumBlocks[1], LaunchArgs.NumGroups.y); ASSERT_EQ(NumBlocks[2], LaunchArgs.NumGroups.z); ASSERT_EQ(NumThreads[0], LaunchArgs.GroupSize.x); ASSERT_EQ(NumThreads[1], LaunchArgs.GroupSize.y); ASSERT_EQ(NumThreads[2], LaunchArgs.GroupSize.z); } ASSERT_SUCCESS(olMemFree(Mem)); }