Skip to content

Commit 6bd5f9c

Browse files
authored
[SYCL] Support negative filters for ONEAPI_DEVICE_SELECTOR (#7309)
This PR aims to add support for negative filters for the ONEAPI_DEVICE_SELECTOR variable to provide the user with a more flexible way of specifying which devices should and should not be available for usage. For example, ONEAPI_DEVICE_SELECTOR='opencl:*;**!opencl:gpu**' considers all opencl backend devices except for those that are of the gpu type.
1 parent 7fee8af commit 6bd5f9c

File tree

4 files changed

+107
-7
lines changed

4 files changed

+107
-7
lines changed

sycl/doc/EnvironmentVariables.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ With no environment variables set to say otherwise, all platforms and devices pr
3434
The syntax of this environment variable follows this BNF grammar:
3535
```
3636
ONEAPI_DEVICE_SELECTOR = <selector-string>
37-
<selector-string> ::= <term>[;<term>...]
37+
<selector-string> ::= { <accept-filters> | <discard-filters> | <accept-filters>;<discard-filters> }
38+
<accept-filters> ::= <accept-filter>[;<accept-filter>...]
39+
<discard-filters> ::= <discard-filter>[;<discard-filter>...]
40+
<accept-filter> ::= <term>
41+
<discard-filter> ::= !<term>
3842
<term> ::= <backend>:<devices>
3943
<backend> ::= { * | level_zero | opencl | cuda | hip | esimd_emulator } // case insensitive
4044
<devices> ::= <device>[,<device>...]
@@ -52,6 +56,13 @@ The device indices are zero-based and are unique only within a backend. Therefor
5256

5357
Additionally, if a sub-device is chosen (via numeric index or wildcard), then an additional layer of partitioning can be specified. In other words, a sub-sub-device can be selected. Like sub-devices, this is done with a period ( `.` ) and a sub-sub-device specifier which is a wildcard symbol ( `*` ) or a numeric index. Example `ONEAPI_DEVICE_SELECTOR=level_zero:0.*.*` would partition device 0 into sub-devices and then partition each of those into sub-sub-devices. The range of grandchild sub-sub-devices would be the final devices available to the app, neither device 0, nor its child partitions would be in that list.
5458

59+
Lastly, a filter in the grammar can be thought of as a term in conjuction with an action that is taken on all devices that are selected by the term. The action can be an accept action or a discard action. Based on the action, a filter can be an accept filter or a discard filter.
60+
The string `<term>` represents an accept filter and the string `!<term>` represents a discard filter. The underlying term is the same but they perform different actions on the matching devices list.
61+
For example, `!opencl:*` discards all devices of the opencl backend from the list of available devices. The discarding filters, if there are any, must all appear at the end of the selector string.
62+
When one or more filters accept a device and one or more filters discard the device, the latter have priority and the device is ultimately not made available to the user. This allows the user to provide selector strings such as `*:gpu;!cuda:*` that accepts all gpu devices except those with a CUDA backend.
63+
Furthermore, if the value of this environment variable only has discarding filters, an accepting filter that matches all devices, but not sub-devices and sub-sub-devices, will be implicitly included in the
64+
environment variable to allow the user to specify only the list of devices that must not be made available. Therefore, `!*:cpu` will accept all devices except those that are of the cpu type and `opencl:*;!*:cpu`
65+
will accept all devices of the opencl backend exept those that are of the opencl backend and of the cpu type. It is legal to have a rejection filter even if it specifies devices have already been omitted by previous filters in the selection string. Doing so has no effect; the rejected devices are still omitted.
5566

5667
The following examples further illustrate the usage of this environment variable:
5768

@@ -66,13 +77,14 @@ The following examples further illustrate the usage of this environment variable
6677
| `ONEAPI_DEVICE_SELECTOR=opencl:0.*` | All the sub-devices from the OpenCL device with index 0 are exposed as SYCL root devices. No other devices are available. |
6778
| `ONEAPI_DEVICE_SELECTOR=opencl:0.2` | The third sub-device (2 in zero-based counting) of the OpenCL device with index 0 will be the sole device available. |
6879
| `ONEAPI_DEVICE_SELECTOR=level_zero:*,*.*` | Exposes Level Zero devices to the application in two different ways. Each device (aka "card") is exposed as a SYCL root device and each sub-device is also exposed as a SYCL root device.|
69-
80+
| `ONEAPI_DEVICE_SELECTOR="opencl:*;!opencl:0"` | All OpenCL devices except for the device with index 0 are available. |
81+
| `ONEAPI_DEVICE_SELECTOR="!*:cpu"` | All devices except for CPU devices are available. |
7082

7183
Notes:
7284
- The backend argument is always required. An error will be thrown if it is absent.
7385
- Additionally, the backend MUST be followed by colon ( `:` ) and at least one device specifier of some sort, else an error is thrown.
7486
- For sub-devices and sub-sub-devices, the parent device must support partitioning (`info::partition_property::partition_by_affinity_domain` and `info::partition_affinity_domain::next_partitionable`. See the SYCL 2020 specification for a precise definition.) For Intel GPUs, the sub-device and sub-sub-device syntax can be used to expose tiles or CCSs to the SYCL application as root devices. The exact mapping between sub-device, sub-sub-device, tiles, and CCSs is specific to the hardware.
75-
- The semi-colon character ( `;` ) is treated specially by many shells, so you may need to enclose the string in quotes if the selection string contains this character.
87+
- The semi-colon character ( `;` ) and the exclamation mark character ( `!` ) are treated specially by many shells, so you may need to enclose the string in quotes if the selection string contains these characters.
7688

7789

7890

sycl/include/sycl/detail/device_filter.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ std::ostream &operator<<(std::ostream &os, std::optional<T> const &opt) {
2929
}
3030

3131
// the ONEAPI_DEVICE_SELECTOR string gets broken down into these targets
32-
// will will match devices.
32+
// will will match devices. If the target is negative, such as !opencl:*
33+
// then matching devices will not be made available to the user.
3334
struct ods_target {
3435
public:
3536
std::optional<backend> Backend;
@@ -44,6 +45,8 @@ struct ods_target {
4445
bool HasSubSubDeviceWildCard = false; // two levels of sub-devices.
4546
std::optional<unsigned> SubSubDeviceNum;
4647

48+
bool IsNegativeTarget = false; // used to represent negative filters.
49+
4750
ods_target(backend be) { Backend = be; };
4851
ods_target(){};
4952
friend std::ostream &operator<<(std::ostream &Out, const ods_target &Target);

sycl/source/detail/device_filter.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ Parse_ONEAPI_DEVICE_SELECTOR(const std::string &envStr) {
175175
}
176176

177177
std::vector<std::string_view> Entries = tokenize(envStr, ";");
178+
unsigned int negative_filters = 0;
178179
// Each entry: "level_zero:gpu" or "opencl:0.0,0.1" or "opencl:*" but NOT just
179180
// "opencl".
180181
for (const auto Entry : Entries) {
@@ -190,6 +191,21 @@ Parse_ONEAPI_DEVICE_SELECTOR(const std::string &envStr) {
190191
std::vector<std::string_view> Targets = tokenize(Pair[1], ",");
191192
for (auto TargetStr : Targets) {
192193
ods_target DeviceTarget(be);
194+
if (Entry[0] == '!') { // negative filter
195+
DeviceTarget.IsNegativeTarget = true;
196+
++negative_filters;
197+
} else { // positive filter
198+
// no need to set IsNegativeTarget=false because it is so by default.
199+
// ensure that no negative filter has been seen because all
200+
// negative filters must come after all positive filters
201+
if (negative_filters > 0) {
202+
std::stringstream ss;
203+
ss << "All negative(discarding) filters must appear after all "
204+
"positive(accepting) filters!";
205+
throw sycl::exception(sycl::make_error_code(errc::invalid),
206+
ss.str());
207+
}
208+
}
193209
Parse_ODS_Device(DeviceTarget, TargetStr);
194210
Result.push_back(DeviceTarget);
195211
}
@@ -201,6 +217,20 @@ Parse_ONEAPI_DEVICE_SELECTOR(const std::string &envStr) {
201217
}
202218
}
203219

220+
// This if statement handles the special case when the filter list
221+
// contains at least one negative filter but no positive filters.
222+
// This means that no devices will be available at all and so its as if
223+
// the filter list was empty because the negative filters do not have any
224+
// any effect. Hoewever, it is desirable to be able to set the
225+
// ONEAPI_DEVICE_SELECTOR=!*:gpu to consider all devices except gpu
226+
// devices so that we must implicitly add an acceptall target to the
227+
// list of targets to make this work. So the result will be as if
228+
// the filter string had the *:* string in it.
229+
if (!Result.empty() && negative_filters == Result.size()) {
230+
ods_target acceptAll{backend::all};
231+
acceptAll.DeviceType = info::device_type::all;
232+
Result.push_back(acceptAll);
233+
}
204234
return Result;
205235
}
206236

sycl/source/detail/platform_impl.cpp

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,18 +149,48 @@ std::vector<platform> platform_impl::get_platforms() {
149149
// ONEAPI_DEVICE_SELECTOR This function matches devices in the order of backend,
150150
// device_type, and device_num. The device_filter and ods_target structs pun for
151151
// each other, as do device_filter_list and ods_target_list.
152+
// Since ONEAPI_DEVICE_SELECTOR admits negative filters, we use type traits
153+
// to distinguish the case where we are working with ONEAPI_DEVICE_SELECTOR
154+
// in the places where the functionality diverges between these two
155+
// environment variables.
152156
template <typename ListT, typename FilterT>
153157
static int filterDeviceFilter(std::vector<RT::PiDevice> &PiDevices,
154158
RT::PiPlatform Platform, ListT *FilterList) {
155159

160+
constexpr bool is_ods_target = std::is_same_v<FilterT, ods_target>;
161+
// There are some differences in implementation between SYCL_DEVICE_FILTER
162+
// and ONEAPI_DEVICE_SELECTOR so we use if constexpr to select the
163+
// appropriate execution path if we are dealing with the latter variable.
164+
165+
if constexpr (is_ods_target) {
166+
167+
// Since we are working with ods_target filters ,which can be negative,
168+
// we sort the filters so that all the negative filters appear before
169+
// all the positive filters. This enables us to have the full list of
170+
// blacklisted devices by the time we get to the positive filters
171+
// so that if a positive filter matches a blacklisted device we do
172+
// not add it to the list of available devices.
173+
std::sort(FilterList->get().begin(), FilterList->get().end(),
174+
[](const ods_target &filter1, const ods_target &filter2) {
175+
if (filter2.IsNegativeTarget)
176+
return false;
177+
return true;
178+
});
179+
}
180+
181+
// this map keeps track of devices discarded by negative filters, it is only
182+
// used in the ONEAPI_DEVICE_SELECTOR implemenation. It cannot be placed
183+
// in the if statement above because it will then be out of scope in the rest
184+
// of the function
185+
std::map<RT::PiDevice *, bool> Blacklist;
186+
156187
std::vector<plugin> &Plugins = RT::initialize();
157188
auto It =
158189
std::find_if(Plugins.begin(), Plugins.end(), [Platform](plugin &Plugin) {
159190
return Plugin.containsPiPlatform(Platform);
160191
});
161192
if (It == Plugins.end())
162193
return -1;
163-
164194
plugin &Plugin = *It;
165195
backend Backend = Plugin.getBackend();
166196
int InsertIDx = 0;
@@ -188,12 +218,37 @@ static int filterDeviceFilter(std::vector<RT::PiDevice> &PiDevices,
188218
if (FilterDevType == info::device_type::all) {
189219
// Last, match the device_num entry
190220
if (!Filter.DeviceNum || DeviceNum == Filter.DeviceNum.value()) {
191-
PiDevices[InsertIDx++] = Device;
221+
if constexpr (is_ods_target) { // dealing with ODS filters
222+
if (!Blacklist[&Device]) { // ensure it is not blacklisted
223+
if (!Filter.IsNegativeTarget) { // is filter positive?
224+
PiDevices[InsertIDx++] = Device;
225+
} else {
226+
// Filter is negative and the device matches the filter so
227+
// blacklist the device.
228+
Blacklist[&Device] = true;
229+
}
230+
}
231+
} else { // dealing with SYCL_DEVICE_FILTER
232+
PiDevices[InsertIDx++] = Device;
233+
}
192234
break;
193235
}
236+
194237
} else if (FilterDevType == DeviceType) {
195238
if (!Filter.DeviceNum || DeviceNum == Filter.DeviceNum.value()) {
196-
PiDevices[InsertIDx++] = Device;
239+
if constexpr (is_ods_target) {
240+
if (!Blacklist[&Device]) {
241+
if (!Filter.IsNegativeTarget) {
242+
PiDevices[InsertIDx++] = Device;
243+
} else {
244+
// Filter is negative and the device matches the filter so
245+
// blacklist the device.
246+
Blacklist[&Device] = true;
247+
}
248+
}
249+
} else {
250+
PiDevices[InsertIDx++] = Device;
251+
}
197252
break;
198253
}
199254
}

0 commit comments

Comments
 (0)