Skip to content

Commit c2c6c2b

Browse files
committed
keep incomplete events in the in-order order manager
_InOrderManager now builds on _SequentialOrderManager, so events added from other queues are kept and non-events raise right away. submitted_events adds the queue's last event to them.
1 parent 2fe0782 commit c2c6c2b

5 files changed

Lines changed: 90 additions & 107 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
* Added `add_event` and `add_cleanup_event` methods and `cleanup_events` and `num_cleanup_events` properties to the order manager, for tracking the events that gate the release of objects used by offloaded tasks [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
1212
* Added `SyclQueue_DeferRelease` C-API function, which defers a release callback until work submitted to a queue completes, and through which `dpctl::utils::keep_args_alive` keeps its arguments alive [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
1313
* Added `dpctl::utils::defer_release` to `dpctl4pybind11.hpp`, which calls a release callback once the given events have completed [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
14-
1514
* Added `dpctl.SyclQueue.get_last_event` and `dpctl.SyclQueue.is_get_last_event_available`, and the `DPCTLQueue_GetLastEvent` and `DPCTLQueue_GetLastEvent_Available` C-API functions, wrapping the DPC++ `sycl_ext_oneapi_in_order_queue_events` extension [gh-2299](https://github.com/IntelPython/dpctl/pull/2299)
1615

1716
### Changed
1817
* Object lifetime during offload is now managed by a list of releases deferred until the events that gate them have completed, rather than by a `host_task`. `dpctl` keeps one thread for looking over the list and running the releases that have come due, started by the first release deferred and again in the child of a `fork`, which takes the GIL to do so and lets a release call into the interpreter and free USM allocations [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
1918
* `import_dpctl()` now returns 0 on success and -1 with a Python exception set on failure, and `dpctl4pybind11.hpp` raises that exception rather than failing on the first C-API call [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
2019
* The event returned by `dpctl.SyclQueue.keep_args_alive`, `dpctl::utils::keep_args_alive` and `dpctl.SyclQueue._submit_keep_args_alive` is now for an empty kernel that gates the deferred release rather than for a `host_task` that performs it, so its completion means that the objects are no longer in use rather than that they have been released [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)
21-
22-
* `dpctl.utils.SequentialOrderManager` keeps only the most recently added events for in-order queues, and `submitted_events` returns the queue's last event when `sycl_ext_oneapi_in_order_queue_events` is available [gh-2299](https://github.com/IntelPython/dpctl/pull/2299)
20+
* `dpctl.utils.SequentialOrderManager[q].submitted_events` includes the queue's last event for in-order queues when `sycl_ext_oneapi_in_order_queue_events` is available, and `wait()` waits for all work on an in-order queue [gh-2299](https://github.com/IntelPython/dpctl/pull/2299)
2321

2422
### Deprecated
2523
* Deprecated `dpctl.SyclQueue._submit_keep_args_alive` in favor of `dpctl.SyclQueue.keep_args_alive`, which it now calls and which behaves identically [gh-2359](https://github.com/IntelPython/dpctl/pull/2359)

‎docs/doc_sources/api_reference/dpctl/utils.rst‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@
3131
best-effort: references that thread is already dropping are left to it, so
3232
they may still be held when ``wait`` returns.
3333

34-
For an in-order queue, the queue orders the tasks, and the order manager
35-
keeps only the most recently added events. ``submitted_events`` is the
36-
event of the last command submitted to the queue if
37-
:meth:`dpctl.SyclQueue.is_get_last_event_available` is ``True``, and the
38-
most recently added computation events otherwise. Only in the first case
39-
does it cover tasks submitted to the queue without being added to the
40-
order manager, e.g. by other threads. ``wait`` waits on the queue.
34+
For an in-order queue, if
35+
:meth:`dpctl.SyclQueue.is_get_last_event_available` is ``True``,
36+
``submitted_events`` also has the event of the last command submitted to
37+
the queue. That covers tasks submitted to the queue without being added
38+
to the order manager, e.g. by other threads, and may be the event of an
39+
empty kernel submitted by :meth:`dpctl.SyclQueue.keep_args_alive`.
40+
``wait``, ``clear`` and thread exit wait for all work on the queue,
41+
including work submitted by other threads.
4142

4243
.. deprecated:: 0.24.0
4344
``add_event_pair``, ``host_task_events`` and ``num_host_task_events``

‎dpctl/tests/test_sycl_queue.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ def test_queue_get_last_event():
180180
q = dpctl.SyclQueue(property="in_order")
181181
except dpctl.SyclQueueCreationError:
182182
pytest.skip("Queue could not be created for default-selected device")
183-
assert q.get_last_event() is None
184183
buf = dpctl.memory.MemoryUSMDevice(1024, queue=q)
185184
ev = q.memset_async(buf, 1, 1024)
186185
last_ev = q.get_last_event()

‎dpctl/tests/test_utils.py‎

Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -522,49 +522,49 @@ def use_last_event(request, monkeypatch):
522522
return request.param
523523

524524

525-
def test_in_order_order_manager(use_last_event):
525+
def test_in_order_order_manager():
526526
q = _in_order_queue()
527527
_som = dpctl.utils.SequentialOrderManager
528528
_mngr = _som[q]
529529
assert _som[q] is _mngr
530-
assert _mngr.num_cleanup_events == 0
531-
assert _mngr.num_submitted_events == 0
530+
assert isinstance(_mngr, _order_manager._InOrderManager)
532531
assert isinstance(_mngr.submitted_events, list)
533532
assert isinstance(_mngr.cleanup_events, list)
534533
_mngr.add_event(dpctl.SyclEvent())
535534
_mngr.add_event([dpctl.SyclEvent(), dpctl.SyclEvent()])
536535
_mngr.add_cleanup_event(dpctl.SyclEvent())
537536
_mngr.add_cleanup_event([dpctl.SyclEvent(), dpctl.SyclEvent()])
538-
cl_ev, comp_ev = dpctl.SyclEvent(), dpctl.SyclEvent()
539-
_mngr.add_event([comp_ev])
540-
_mngr.add_cleanup_event(cl_ev)
541-
# only the most recently added events are kept
542-
assert _mngr.cleanup_events == [cl_ev]
543-
assert _mngr.num_cleanup_events == 1
544-
# nothing was submitted to the queue, so both modes return added events
545-
assert _mngr.submitted_events == [comp_ev]
546-
assert _mngr.num_submitted_events == 1
537+
# complete events are not kept
538+
assert _mngr.num_cleanup_events == 0
547539
_mngr.wait()
548540
cpy = _mngr.__copy__()
549-
assert cpy.cleanup_events == [cl_ev]
550-
assert cpy.submitted_events == [comp_ev]
541+
assert isinstance(cpy, _order_manager._InOrderManager)
542+
assert cpy.num_cleanup_events == 0
551543
_som.clear()
552544
del cpy
553545

554546

547+
@pytest.mark.parametrize("in_order", [True, False])
548+
def test_order_manager_add_non_event(in_order):
549+
q = _in_order_queue() if in_order else dpctl.SyclQueue()
550+
_mngr = dpctl.utils.SequentialOrderManager[q]
551+
with pytest.raises(TypeError):
552+
_mngr.add_event(None)
553+
with pytest.raises(TypeError):
554+
_mngr.add_cleanup_event([None])
555+
dpctl.utils.SequentialOrderManager.clear()
556+
557+
555558
def test_in_order_order_manager_host_task_api():
556559
q = _in_order_queue()
557560
_som = dpctl.utils.SequentialOrderManager
558561
_mngr = _som[q]
559-
ht_ev, comp_ev = dpctl.SyclEvent(), dpctl.SyclEvent()
560562
with warnings.catch_warnings():
561563
warnings.simplefilter("error")
562564
_mngr.add_event_pair(dpctl.SyclEvent(), dpctl.SyclEvent())
563565
_mngr.add_event_pair([dpctl.SyclEvent()], dpctl.SyclEvent())
564-
_mngr.add_event_pair(ht_ev, [comp_ev])
565-
assert _mngr.host_task_events == [ht_ev]
566-
assert _mngr.num_host_task_events == 1
567-
assert _mngr.cleanup_events == [ht_ev]
566+
assert _mngr.host_task_events == []
567+
assert _mngr.num_host_task_events == 0
568568
_som.clear()
569569

570570

@@ -589,44 +589,62 @@ def test_in_order_order_manager_submitted_events(use_last_event):
589589
q = _in_order_queue()
590590
_mngr = dpctl.utils.SequentialOrderManager[q]
591591
buf = dpctl.memory.MemoryUSMDevice(1024, queue=q)
592-
comp_ev = q.memset_async(buf, 1, 1024)
593-
ht_ev = q.keep_args_alive((buf,), [comp_ev])
594-
_mngr.add_event(comp_ev)
595-
_mngr.add_cleanup_event(ht_ev)
596-
if use_last_event:
597-
# the keep-alive task is the last command submitted to the queue
598-
assert len(_mngr.submitted_events) == 1
599-
assert _mngr.submitted_events[0] is not comp_ev
600-
else:
601-
assert _mngr.submitted_events == [comp_ev]
602592
# work which is not added to the order manager
603-
q.memset_async(buf, 2, 1024)
593+
q.memset_async(buf, 1, 1024)
604594
if use_last_event:
605-
assert len(_mngr.submitted_events) == 1
595+
assert _mngr.num_submitted_events == 1
606596
else:
607-
assert _mngr.submitted_events == [comp_ev]
597+
assert _mngr.num_submitted_events == 0
598+
dpctl.utils.SequentialOrderManager.clear()
599+
600+
601+
def _fill_in_steps(q, buf, n_fills, mngr=None):
602+
nbytes = buf.nbytes
603+
ev = None
604+
for v in range(1, n_fills + 1):
605+
ev = q.memset_async(buf, v, nbytes, dEvents=[ev] if ev else None)
606+
if mngr is not None:
607+
mngr.add_event(ev)
608+
mngr.add_cleanup_event(q.keep_args_alive((buf,), [ev]))
609+
return ev
610+
611+
612+
def test_in_order_order_manager_other_queue_depends():
613+
if not dpctl.SyclQueue.is_get_last_event_available():
614+
pytest.skip("sycl_ext_oneapi_in_order_queue_events not available")
615+
q_in = _in_order_queue()
616+
q_other = dpctl.SyclQueue(q_in.sycl_context, q_in.sycl_device)
617+
_mngr = dpctl.utils.SequentialOrderManager[q_in]
618+
n_fills = 8
619+
buf = dpctl.memory.MemoryUSMDevice(64 * 1024 * 1024, queue=q_in)
620+
host = np.empty(buf.nbytes, dtype=np.uint8)
621+
for _ in range(5):
622+
q_in.memset(buf, 0, buf.nbytes)
623+
_fill_in_steps(q_in, buf, n_fills - 1, _mngr)
624+
# the last fill is not added to the order manager
625+
q_in.memset_async(buf, n_fills, buf.nbytes)
626+
q_other.memcpy_async(
627+
host, buf, buf.nbytes, dEvents=_mngr.submitted_events
628+
).wait()
629+
assert np.all(host == n_fills)
608630
dpctl.utils.SequentialOrderManager.clear()
609631

610632

611-
def test_in_order_order_manager_other_queue_depends(use_last_event):
633+
def test_in_order_order_manager_other_queue_event(use_last_event):
612634
q_in = _in_order_queue()
613635
q_other = dpctl.SyclQueue(q_in.sycl_context, q_in.sycl_device)
614636
_mngr = dpctl.utils.SequentialOrderManager[q_in]
615-
nbytes, n_fills = 32 * 1024 * 1024, 8
616-
buf = dpctl.memory.MemoryUSMDevice(nbytes, queue=q_in)
617-
host = np.empty(nbytes, dtype=np.uint8)
618-
for _ in range(3):
619-
q_in.memset(buf, 0, nbytes)
620-
for v in range(1, n_fills + 1):
621-
comp_ev = q_in.memset_async(buf, v, nbytes)
622-
ht_ev = q_in.keep_args_alive((buf,), [comp_ev])
623-
_mngr.add_event(comp_ev)
624-
_mngr.add_cleanup_event(ht_ev)
625-
# work on another queue must wait for work done on the in-order queue
626-
deps = _mngr.submitted_events
627-
assert deps
628-
q_other.memcpy_async(host, buf, nbytes, dEvents=deps).wait()
637+
n_fills = 8
638+
buf = dpctl.memory.MemoryUSMDevice(64 * 1024 * 1024, queue=q_in)
639+
host = np.empty(buf.nbytes, dtype=np.uint8)
640+
for _ in range(5):
641+
q_in.memset(buf, 0, buf.nbytes)
642+
_mngr.add_event(_fill_in_steps(q_other, buf, n_fills))
643+
q_in.memcpy_async(
644+
host, buf, buf.nbytes, dEvents=_mngr.submitted_events
645+
).wait()
629646
assert np.all(host == n_fills)
647+
q_other.wait()
630648
dpctl.utils.SequentialOrderManager.clear()
631649

632650

‎dpctl/utils/_order_manager.py‎

Lines changed: 16 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -146,84 +146,51 @@ def __copy__(self):
146146
return res
147147

148148

149-
def _as_event_list(ev):
150-
if isinstance(ev, (list, tuple)):
151-
return list(ev)
152-
return [ev]
153-
154-
155149
_use_last_event = SyclQueue.is_get_last_event_available()
156150

157151

158-
class _InOrderManager:
152+
class _InOrderManager(_SequentialOrderManager):
159153
"""
160-
Order manager for an in-order queue. The queue orders its tasks, so only
161-
the most recently added events are kept.
154+
Order manager for an in-order queue.
162155
163-
``submitted_events`` is the event of the last command submitted to the
164-
queue if :meth:`dpctl.SyclQueue.get_last_event` is available, and the
165-
most recently added computation events otherwise. ``wait`` waits on the
166-
queue.
156+
Holds the added events which are not complete, as
157+
:class:`_SequentialOrderManager` does. ``submitted_events`` also has the
158+
event of the last command submitted to the queue if
159+
:meth:`dpctl.SyclQueue.get_last_event` is available. ``wait`` waits on
160+
the queue.
167161
"""
168162

169163
def __init__(self, q: SyclQueue):
164+
super().__init__()
170165
self._queue = q
171-
self._cleanup_evs = []
172-
self._comp_evs = []
173166

174167
def __del__(self):
175168
if sys.is_finalizing():
176169
return
177170
self._queue.wait()
178-
179-
def add_event_pair(self, host_task_ev, comp_ev):
180-
self._cleanup_evs = _as_event_list(host_task_ev)
181-
self._comp_evs = _as_event_list(comp_ev)
182-
183-
def add_event(self, comp_ev):
184-
self._comp_evs = _as_event_list(comp_ev)
185-
186-
def add_cleanup_event(self, cleanup_ev):
187-
self._cleanup_evs = _as_event_list(cleanup_ev)
188-
189-
@property
190-
def num_host_task_events(self):
191-
return self.num_cleanup_events
192-
193-
@property
194-
def num_cleanup_events(self):
195-
return len(self._cleanup_evs)
171+
super().__del__()
196172

197173
@property
198174
def num_submitted_events(self):
199175
return len(self.submitted_events)
200176

201-
@property
202-
def host_task_events(self):
203-
return self.cleanup_events
204-
205-
@property
206-
def cleanup_events(self):
207-
return list(self._cleanup_evs)
208-
209177
@property
210178
def submitted_events(self):
179+
evs = self._state.get_submitted_events()
211180
if _use_last_event:
212181
ev = self._queue.get_last_event()
213182
if ev is not None:
214-
return [ev]
215-
return list(self._comp_evs)
183+
evs.append(ev)
184+
return evs
216185

217186
def wait(self):
218187
self._queue.wait()
219-
# see _SequentialOrderManager.wait
220-
if not sys.is_finalizing():
221-
_drain_deferred_releases()
188+
return super().wait()
222189

223190
def __copy__(self):
224-
res = _InOrderManager(self._queue)
225-
res._cleanup_evs = list(self._cleanup_evs)
226-
res._comp_evs = list(self._comp_evs)
191+
res = _InOrderManager.__new__(_InOrderManager)
192+
res._state = _OrderManager(self._state)
193+
res._queue = self._queue
227194
return res
228195

229196

0 commit comments

Comments
 (0)