Skip to content

Commit badd8c1

Browse files
authored
[SYCL][Graph] Support sycl_ext_oneapi_raw_kernel_arg (#15252)
Support using the `raw_kernel_arg` objects defined by [sycl_ext_oneapi_raw_kernel_arg](https://github.com/intel/llvm/blob/sycl/sycl/doc/extensions/experimental/sycl_ext_oneapi_raw_kernel_arg.asciidoc) in SYCL-Graphs. The majority of changes in this PR are for supporting the [dynamic_parameter](https://github.com/intel/llvm/blob/sycl/sycl/doc/extensions/experimental/sycl_ext_oneapi_graph.asciidoc#742-dynamic-parameters) feature of SYCL graph. In particular: * The #14335 implementation of ` `sycl_oneapi_raw_kernel_arg` doesn't use the PIMPL implementation pattern. We therefore add the `dynamic_parameter` impl class as a friend so that, it can access the underlying members. * To prevent circular include dependencies we can't include the public facing `raw_kernel_arg.hpp` header in the public facing `graph.hpp` header to using a raw_kernel_arg specific code path for `dynamic_parameter` update. Instead we overlead the `updateValue(const void *NewValue, size_t Size);` method with `void updateValue(const raw_kernel_arg *NewRawValue, size_t Size);` as the `raw_kernel_arg` pointer can be forward declared and the compiler will pick the correct code path although the `Size` argument is unused.
1 parent d6a851e commit badd8c1

File tree

14 files changed

+307
-2
lines changed

14 files changed

+307
-2
lines changed

sycl/doc/extensions/experimental/sycl_ext_oneapi_graph.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ compile time using a template parameter. This underlying type can be an
501501
accessor, a pointer to a USM allocation, scalar passed by value, or a raw byte
502502
representation of the argument. The raw byte representation is intended to
503503
enable updating arguments set using
504-
link:../proposed/sycl_ext_oneapi_raw_kernel_arg.asciidoc[sycl_ext_oneapi_raw_kernel_arg].
504+
link:../experimental/sycl_ext_oneapi_raw_kernel_arg.asciidoc[sycl_ext_oneapi_raw_kernel_arg].
505505

506506
Dynamic parameters are registered with nodes in a modifiable graph, with each
507507
registration associating one or more node arguments to the dynamic parameter

sycl/include/sycl/ext/oneapi/experimental/graph.hpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ enum class graph_state {
3939
executable, ///< In executable state, the graph is ready to execute.
4040
};
4141

42-
// Forward declare Graph class
42+
// Forward declare ext::oneapi::experimental classes
4343
template <graph_state State> class command_graph;
44+
class raw_kernel_arg;
4445

4546
namespace detail {
4647
// List of sycl features and extensions which are not supported by graphs. Used
@@ -441,6 +442,11 @@ class __SYCL_EXPORT dynamic_parameter_base {
441442
protected:
442443
void updateValue(const void *NewValue, size_t Size);
443444

445+
// Update a sycl_ext_oneapi_raw_kernel_arg parameter. Size parameter is
446+
// ignored as it represents sizeof(raw_kernel_arg), which doesn't represent
447+
// the number of underlying bytes.
448+
void updateValue(const raw_kernel_arg *NewRawValue, size_t Size);
449+
444450
void updateAccessor(const sycl::detail::AccessorBaseHost *Acc);
445451
std::shared_ptr<dynamic_parameter_impl> impl;
446452

sycl/include/sycl/ext/oneapi/experimental/raw_kernel_arg.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class raw_kernel_arg {
2727
size_t MArgSize;
2828

2929
friend class sycl::handler;
30+
// For sycl_ext_oneapi_graph integration
31+
friend class detail::dynamic_parameter_impl;
3032
};
3133

3234
} // namespace ext::oneapi::experimental

sycl/source/detail/graph_impl.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,11 @@ void dynamic_parameter_base::updateValue(const void *NewValue, size_t Size) {
17141714
impl->updateValue(NewValue, Size);
17151715
}
17161716

1717+
void dynamic_parameter_base::updateValue(const raw_kernel_arg *NewRawValue,
1718+
size_t Size) {
1719+
impl->updateValue(NewRawValue, Size);
1720+
}
1721+
17171722
void dynamic_parameter_base::updateAccessor(
17181723
const sycl::detail::AccessorBaseHost *Acc) {
17191724
impl->updateAccessor(Acc);

sycl/source/detail/graph_impl.hpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <sycl/detail/cg_types.hpp>
1212
#include <sycl/detail/os_util.hpp>
1313
#include <sycl/ext/oneapi/experimental/graph.hpp>
14+
#include <sycl/ext/oneapi/experimental/raw_kernel_arg.hpp>
1415
#include <sycl/handler.hpp>
1516

1617
#include <detail/accessor_impl.hpp>
@@ -1500,6 +1501,17 @@ class dynamic_parameter_impl {
15001501
std::memcpy(MValueStorage.data(), Data, ParamSize);
15011502
}
15021503

1504+
/// sycl_ext_oneapi_raw_kernel_arg constructor
1505+
/// Parameter size is taken from member of raw_kernel_arg object.
1506+
dynamic_parameter_impl(std::shared_ptr<graph_impl> GraphImpl, size_t,
1507+
raw_kernel_arg *Data)
1508+
: MGraph(GraphImpl) {
1509+
size_t RawArgSize = Data->MArgSize;
1510+
const void *RawArgData = Data->MArgData;
1511+
MValueStorage.reserve(RawArgSize);
1512+
std::memcpy(MValueStorage.data(), RawArgData, RawArgSize);
1513+
}
1514+
15031515
/// Register a node with this dynamic parameter
15041516
/// @param NodeImpl The node to be registered
15051517
/// @param ArgIndex The arg index for the kernel arg associated with this
@@ -1511,6 +1523,25 @@ class dynamic_parameter_impl {
15111523
/// Get a pointer to the internal value of this dynamic parameter
15121524
void *getValue() { return MValueStorage.data(); }
15131525

1526+
/// Update sycl_ext_oneapi_raw_kernel_arg parameter
1527+
/// @param NewRawValue Pointer to a raw_kernel_arg object.
1528+
/// @param Size Parameter is ignored.
1529+
void updateValue(const raw_kernel_arg *NewRawValue, size_t Size) {
1530+
// Number of bytes is taken from member of raw_kernel_arg object rather
1531+
// than using the size parameter which represents sizeof(raw_kernel_arg).
1532+
std::ignore = Size;
1533+
size_t RawArgSize = NewRawValue->MArgSize;
1534+
const void *RawArgData = NewRawValue->MArgData;
1535+
1536+
for (auto &[NodeWeak, ArgIndex] : MNodes) {
1537+
auto NodeShared = NodeWeak.lock();
1538+
if (NodeShared) {
1539+
NodeShared->updateArgValue(ArgIndex, RawArgData, RawArgSize);
1540+
}
1541+
}
1542+
std::memcpy(MValueStorage.data(), RawArgData, RawArgSize);
1543+
}
1544+
15141545
/// Update the internal value of this dynamic parameter as well as the value
15151546
/// of this parameter in all registered nodes.
15161547
/// @param NewValue Pointer to the new value
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// RUN: %{build} -o %t.out
2+
// RUN: %{run} %t.out
3+
// Extra run to check for leaks in Level Zero using UR_L0_LEAKS_DEBUG
4+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=0 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
5+
// Extra run to check for immediate-command-list in Level Zero
6+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
7+
8+
// REQUIRES: ocloc && level_zero
9+
10+
#define GRAPH_E2E_EXPLICIT
11+
12+
#include "../Inputs/raw_kernel_arg.cpp"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Tests using a raw_kernel_arg with 32-bit sized scalars.
2+
3+
#include "../graph_common.hpp"
4+
5+
auto constexpr CLSource = R"===(
6+
__kernel void RawArgKernel(int scalar, __global int *out) {
7+
size_t id = get_global_id(0);
8+
out[id] = id + scalar;
9+
}
10+
)===";
11+
12+
int main() {
13+
queue Queue{};
14+
15+
auto SourceKB =
16+
sycl::ext::oneapi::experimental::create_kernel_bundle_from_source(
17+
Queue.get_context(),
18+
sycl::ext::oneapi::experimental::source_language::opencl, CLSource);
19+
auto ExecKB = sycl::ext::oneapi::experimental::build(SourceKB);
20+
21+
exp_ext::command_graph Graph{Queue};
22+
23+
int32_t *Ptr = malloc_device<int32_t>(Size, Queue);
24+
Queue.memset(Ptr, 0, Size * sizeof(int32_t)).wait();
25+
26+
int32_t Scalar = 42;
27+
exp_ext::raw_kernel_arg RawScalar(&Scalar, sizeof(int32_t));
28+
29+
auto KernelNode = add_node(Graph, Queue, [&](handler &cgh) {
30+
cgh.set_arg(0, RawScalar);
31+
cgh.set_arg(1, Ptr);
32+
cgh.parallel_for(sycl::range<1>{Size},
33+
ExecKB.ext_oneapi_get_kernel("RawArgKernel"));
34+
});
35+
36+
auto ExecGraph = Graph.finalize();
37+
38+
// Ptr should be filled with values based on Scalar
39+
Queue.ext_oneapi_graph(ExecGraph).wait();
40+
41+
std::vector<int32_t> HostData(Size);
42+
Queue.copy(Ptr, HostData.data(), Size).wait();
43+
44+
for (size_t i = 0; i < Size; i++) {
45+
assert(HostData[i] == (i + Scalar));
46+
}
47+
48+
return 0;
49+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Tests whole graph update with raw argument extensions
2+
3+
#include "../graph_common.hpp"
4+
5+
void SubmitKernelNode(
6+
exp_ext::command_graph<exp_ext::graph_state::modifiable> Graph, queue Queue,
7+
int32_t *Ptr, exp_ext::raw_kernel_arg &RawArg) {
8+
9+
auto constexpr CLSource = R"===(
10+
__kernel void RawArgKernel(int scalar, __global int *out) {
11+
size_t id = get_global_id(0);
12+
out[id] = id + scalar;
13+
}
14+
)===";
15+
16+
auto SourceKB =
17+
sycl::ext::oneapi::experimental::create_kernel_bundle_from_source(
18+
Queue.get_context(),
19+
sycl::ext::oneapi::experimental::source_language::opencl, CLSource);
20+
auto ExecKB = sycl::ext::oneapi::experimental::build(SourceKB);
21+
22+
add_node(Graph, Queue, [&](handler &cgh) {
23+
cgh.set_arg(0, RawArg);
24+
cgh.set_arg(1, Ptr);
25+
cgh.parallel_for(sycl::range<1>{Size},
26+
ExecKB.ext_oneapi_get_kernel("RawArgKernel"));
27+
});
28+
}
29+
30+
int main() {
31+
queue Queue{};
32+
33+
exp_ext::command_graph GraphA{Queue};
34+
35+
const size_t N = 1024;
36+
int32_t *PtrA = malloc_device<int32_t>(N, Queue);
37+
Queue.memset(PtrA, 0, N * sizeof(int32_t)).wait();
38+
39+
int32_t ScalarA = 42;
40+
sycl::ext::oneapi::experimental::raw_kernel_arg RawScalarA(&ScalarA,
41+
sizeof(int32_t));
42+
43+
SubmitKernelNode(GraphA, Queue, PtrA, RawScalarA);
44+
auto ExecGraphA = GraphA.finalize(exp_ext::property::graph::updatable{});
45+
46+
// PtrA should be filled with values based on ScalarA
47+
Queue.ext_oneapi_graph(ExecGraphA).wait();
48+
49+
std::vector<int32_t> HostDataA(N);
50+
Queue.copy(PtrA, HostDataA.data(), N).wait();
51+
for (size_t i = 0; i < N; i++) {
52+
assert(HostDataA[i] == (i + ScalarA));
53+
}
54+
55+
exp_ext::command_graph GraphB{Queue.get_context(), Queue.get_device()};
56+
57+
int32_t *PtrB = malloc_device<int32_t>(N, Queue);
58+
Queue.memset(PtrB, 0, N * sizeof(int32_t)).wait();
59+
60+
int32_t ScalarB = 0xA;
61+
sycl::ext::oneapi::experimental::raw_kernel_arg RawScalarB(&ScalarB,
62+
sizeof(int32_t));
63+
64+
// Swap ScalarB and PtrB to be the new inputs/outputs
65+
SubmitKernelNode(GraphB, Queue, PtrB, RawScalarB);
66+
ExecGraphA.update(GraphB);
67+
Queue.ext_oneapi_graph(ExecGraphA).wait();
68+
69+
std::vector<int32_t> HostDataB(N);
70+
Queue.copy(PtrA, HostDataA.data(), N);
71+
Queue.copy(PtrB, HostDataB.data(), N);
72+
Queue.wait();
73+
for (size_t i = 0; i < N; i++) {
74+
assert(HostDataA[i] == (i + ScalarA));
75+
assert(HostDataB[i] == (i + ScalarB));
76+
}
77+
return 0;
78+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// RUN: %{build} -o %t.out
2+
// RUN: %{run} %t.out
3+
// Extra run to check for leaks in Level Zero using UR_L0_LEAKS_DEBUG
4+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=0 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
5+
// Extra run to check for immediate-command-list in Level Zero
6+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
7+
8+
// REQUIRES: ocloc && level_zero
9+
10+
#define GRAPH_E2E_RECORD_REPLAY
11+
12+
#include "../Inputs/raw_kernel_arg.cpp"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// RUN: %{build} -o %t.out
2+
// RUN: %{run} %t.out
3+
// Extra run to check for leaks in Level Zero using UR_L0_LEAKS_DEBUG
4+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=0 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
5+
// Extra run to check for immediate-command-list in Level Zero
6+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
7+
8+
// REQUIRES: ocloc && level_zero
9+
10+
#define GRAPH_E2E_EXPLICIT
11+
12+
#include "../../Inputs/whole_update_raw_arg.cpp"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// RUN: %{build} -o %t.out
2+
// RUN: %{run} %t.out
3+
// Extra run to check for leaks in Level Zero using UR_L0_LEAKS_DEBUG
4+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=0 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
5+
// Extra run to check for immediate-command-list in Level Zero
6+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
7+
8+
// REQUIRES: ocloc && level_zero
9+
10+
#define GRAPH_E2E_RECORD_REPLAY
11+
12+
#include "../../Inputs/whole_update_raw_arg.cpp"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// RUN: %{build} -o %t.out
2+
// RUN: %{run} %t.out
3+
// Extra run to check for leaks in Level Zero using UR_L0_LEAKS_DEBUG
4+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=0 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
5+
// Extra run to check for immediate-command-list in Level Zero
6+
// RUN: %if level_zero %{env SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1 %{l0_leak_check} %{run} %t.out 2>&1 | FileCheck %s --implicit-check-not=LEAK %}
7+
8+
// REQUIRES: ocloc && level_zero
9+
10+
// Tests updating a raw_kernel_arg with 32-bit sized scalars.
11+
12+
#include "../graph_common.hpp"
13+
14+
auto constexpr CLSource = R"===(
15+
__kernel void RawArgKernel(int scalar, __global int *out) {
16+
size_t id = get_global_id(0);
17+
out[id] = id + scalar;
18+
}
19+
)===";
20+
21+
int main() {
22+
queue Queue{};
23+
24+
auto SourceKB =
25+
sycl::ext::oneapi::experimental::create_kernel_bundle_from_source(
26+
Queue.get_context(),
27+
sycl::ext::oneapi::experimental::source_language::opencl, CLSource);
28+
auto ExecKB = sycl::ext::oneapi::experimental::build(SourceKB);
29+
30+
exp_ext::command_graph Graph{Queue};
31+
32+
const size_t N = 1024;
33+
int32_t *PtrA = malloc_device<int32_t>(N, Queue);
34+
int32_t *PtrB = malloc_device<int32_t>(N, Queue);
35+
Queue.memset(PtrA, 0, N * sizeof(int32_t));
36+
Queue.memset(PtrB, 0, N * sizeof(int32_t));
37+
Queue.wait();
38+
39+
int32_t ScalarA = 42;
40+
exp_ext::raw_kernel_arg RawScalarA(&ScalarA, sizeof(int32_t));
41+
42+
int32_t ScalarB = 0xA;
43+
exp_ext::raw_kernel_arg RawScalarB(&ScalarB, sizeof(int32_t));
44+
45+
exp_ext::dynamic_parameter PtrParam(Graph, PtrA);
46+
exp_ext::dynamic_parameter ScalarParam(Graph, RawScalarA);
47+
48+
auto KernelNode = Graph.add([&](handler &cgh) {
49+
cgh.set_arg(0, ScalarParam);
50+
cgh.set_arg(1, PtrParam);
51+
cgh.parallel_for(sycl::range<1>{Size},
52+
ExecKB.ext_oneapi_get_kernel("RawArgKernel"));
53+
});
54+
55+
auto ExecGraph = Graph.finalize(exp_ext::property::graph::updatable{});
56+
57+
// PtrA should be filled with values based on ScalarA
58+
Queue.ext_oneapi_graph(ExecGraph).wait();
59+
60+
std::vector<int> HostDataA(N);
61+
std::vector<int> HostDataB(N);
62+
63+
Queue.copy(PtrA, HostDataA.data(), N);
64+
Queue.copy(PtrB, HostDataB.data(), N);
65+
Queue.wait();
66+
for (size_t i = 0; i < N; i++) {
67+
assert(HostDataA[i] == (i + ScalarA));
68+
assert(HostDataB[i] == 0);
69+
}
70+
71+
// Swap ScalarB and PtrB to be the new inputs/outputs
72+
PtrParam.update(PtrB);
73+
ScalarParam.update(RawScalarB);
74+
ExecGraph.update(KernelNode);
75+
Queue.ext_oneapi_graph(ExecGraph).wait();
76+
77+
Queue.copy(PtrA, HostDataA.data(), N).wait();
78+
Queue.copy(PtrB, HostDataB.data(), N).wait();
79+
for (size_t i = 0; i < N; i++) {
80+
assert(HostDataA[i] == (i + ScalarA));
81+
assert(HostDataB[i] == (i + ScalarB));
82+
}
83+
return 0;
84+
}

sycl/test/abi/sycl_symbols_linux.dump

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3062,6 +3062,7 @@ _ZN4sycl3_V13ext6oneapi12experimental6detail14image_mem_implC2ERKNS3_16image_des
30623062
_ZN4sycl3_V13ext6oneapi12experimental6detail14image_mem_implD1Ev
30633063
_ZN4sycl3_V13ext6oneapi12experimental6detail14image_mem_implD2Ev
30643064
_ZN4sycl3_V13ext6oneapi12experimental6detail17build_from_sourceERNS0_13kernel_bundleILNS0_12bundle_stateE3EEERKSt6vectorINS0_6deviceESaISA_EERKS9_INS0_6detail11string_viewESaISG_EEPNSF_6stringESK_
3065+
_ZN4sycl3_V13ext6oneapi12experimental6detail22dynamic_parameter_base11updateValueEPKNS3_14raw_kernel_argEm
30653066
_ZN4sycl3_V13ext6oneapi12experimental6detail22dynamic_parameter_base11updateValueEPKvm
30663067
_ZN4sycl3_V13ext6oneapi12experimental6detail22dynamic_parameter_base14updateAccessorEPKNS0_6detail16AccessorBaseHostE
30673068
_ZN4sycl3_V13ext6oneapi12experimental6detail22dynamic_parameter_baseC1ENS3_13command_graphILNS3_11graph_stateE0EEEmPKv

sycl/test/abi/sycl_symbols_windows.dump

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4265,6 +4265,7 @@
42654265
?update@executable_command_graph@detail@experimental@oneapi@ext@_V1@sycl@@QEAAXAEBVnode@34567@@Z
42664266
?updateAccessor@dynamic_parameter_base@detail@experimental@oneapi@ext@_V1@sycl@@IEAAXPEBVAccessorBaseHost@267@@Z
42674267
?updateValue@dynamic_parameter_base@detail@experimental@oneapi@ext@_V1@sycl@@IEAAXPEBX_K@Z
4268+
?updateValue@dynamic_parameter_base@detail@experimental@oneapi@ext@_V1@sycl@@IEAAXPEBVraw_kernel_arg@34567@_K@Z
42684269
?use_kernel_bundle@handler@_V1@sycl@@QEAAXAEBV?$kernel_bundle@$01@23@@Z
42694270
?verifyDeviceHasProgressGuarantee@handler@_V1@sycl@@AEAAXW4forward_progress_guarantee@experimental@oneapi@ext@23@W4execution_scope@56723@1@Z
42704271
?verifyUsedKernelBundle@handler@_V1@sycl@@AEAAXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z

0 commit comments

Comments
 (0)