Skip to content

Commit e5507ea

Browse files
committed
Cover resource accounting, saturated scheduling and cross-loop reuse
Four cases that the contract implies but the suite did not exercise yet: * a failure after the native event set exists (device_register_events rejects) frees it exactly once instead of leaking it or freeing it twice; * with the worker bound reached, slices are serialised, the queued slice does not reach the driver out of turn, and the queue wait stays inside the caller's budget; * the same event set is reusable across consecutive event loops, while a second loop waiting on it concurrently is rejected by the lease instead of racing the driver for the same event set; * type and range errors come from the annotated signatures (sync and async), including that an async argument error only surfaces when awaited. The suite is 27 cases now. Signed-off-by: 0z5a <Dezhen.lu@student.uni-tuebingen.de>
1 parent 8c7936d commit e5507ea

1 file changed

Lines changed: 104 additions & 1 deletion

File tree

cuda_core/tests/system/test_system_events_async.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
from cuda.bindings import nvml
2222
from cuda.core import system
23-
from cuda.core.system._async_events import _SLICE_MS, EventSetWaiting
23+
from cuda.core.system import _async_events
24+
from cuda.core.system._async_events import _MAX_WORKERS, _SLICE_MS, EventSetWaiting, _Dispatcher
2425
from cuda.core.system.typing import EventType, SystemEventType
2526

2627
TIMEOUT = nvml.TimeoutError(nvml.Return.ERROR_TIMEOUT)
@@ -437,3 +438,105 @@ async def main():
437438
await task
438439

439440
asyncio.run(main())
441+
442+
443+
# ============================================================================
444+
# Resource accounting, scheduling and cross-loop reuse
445+
# ============================================================================
446+
447+
448+
def test_registration_failure_frees_the_event_set_once(monkeypatch):
449+
"""N16: a failure after the native set exists must not leak or double free."""
450+
gc.collect() # drain sets owned by earlier tests before the fake allocator
451+
created, freed = [], []
452+
453+
def create():
454+
created.append(0x5150)
455+
return 0x5150
456+
457+
def register(*args):
458+
raise nvml.InvalidArgumentError(nvml.Return.ERROR_INVALID_ARGUMENT)
459+
460+
monkeypatch.setattr(nvml, "event_set_create", create)
461+
monkeypatch.setattr(nvml, "event_set_free", freed.append)
462+
monkeypatch.setattr(nvml, "device_register_events", register)
463+
464+
with pytest.raises(nvml.InvalidArgumentError):
465+
system.Device(index=0).register_events([EventType.CLOCK])
466+
gc.collect()
467+
assert created == [0x5150]
468+
assert freed.count(0x5150) == 1, f"our event set was freed {freed.count(0x5150)} times"
469+
470+
471+
def test_saturated_dispatcher_serialises_slices(monkeypatch):
472+
"""N21: one worker serves one slice at a time, and queueing is inside the budget."""
473+
dispatcher = _Dispatcher(max_workers=1)
474+
monkeypatch.setattr(_async_events, "_dispatcher", dispatcher)
475+
476+
holding = FakeWait(hold=True, deliver_at=0)
477+
queued = FakeWait(deliver_at=0)
478+
first, second = EventSetWaiting(), EventSetWaiting()
479+
480+
async def main():
481+
held = asyncio.create_task(first.wait_async(holding, 0))
482+
await spin_until(lambda: len(holding.calls) == 1)
483+
waiting = asyncio.create_task(second.wait_async(queued, 400))
484+
await asyncio.sleep(0.05)
485+
assert queued.calls == [], "a queued slice must not reach the driver out of turn"
486+
holding.release.set()
487+
await held
488+
return await waiting
489+
490+
started = time.monotonic()
491+
assert asyncio.run(main()) is EVENT
492+
elapsed_ms = (time.monotonic() - started) * 1000
493+
assert holding.peak_in_flight == 1, "the bound must hold"
494+
assert queued.peak_in_flight == 1
495+
assert elapsed_ms < 400 + 4 * _SLICE_MS, f"queueing must stay inside the budget ({elapsed_ms:.0f} ms)"
496+
497+
498+
def test_event_set_is_reusable_across_event_loops():
499+
"""N22: serial reuse across loops is fine; a concurrent loop is rejected."""
500+
state = EventSetWaiting()
501+
fake = FakeWait(deliver_at=0)
502+
assert asyncio.run(state.wait_async(fake, 100)) is EVENT
503+
assert asyncio.run(state.wait_async(fake, 100)) is EVENT, "a fresh loop must not see a stale future"
504+
assert state.is_waiting is False
505+
506+
holding = FakeWait(hold=True, deliver_at=0)
507+
outcome = {}
508+
509+
def other_loop():
510+
async def run():
511+
outcome["result"] = await state.wait_async(holding, 0)
512+
513+
asyncio.run(run())
514+
515+
thread = threading.Thread(target=other_loop, name="other-loop")
516+
thread.start()
517+
try:
518+
while not holding.calls:
519+
time.sleep(0.001)
520+
with pytest.raises(RuntimeError, match="already in flight"):
521+
asyncio.run(state.wait_async(fake, 10))
522+
finally:
523+
holding.release.set()
524+
thread.join()
525+
assert outcome["result"] is EVENT
526+
527+
528+
def test_parameter_bounds_match_the_signatures():
529+
"""N23: the type and range errors come from the annotated signatures."""
530+
events = system.Device(index=0).register_events([EventType.CLOCK])
531+
with pytest.raises(TypeError):
532+
events.wait(timeout_ms="soon")
533+
with pytest.raises(TypeError):
534+
asyncio.run(events.wait_async(timeout_ms="soon")) # async def converts at await time
535+
with pytest.raises(ValueError, match="timeout_ms"):
536+
asyncio.run(events.wait_async(timeout_ms=-1))
537+
with pytest.raises(OverflowError):
538+
events.wait(timeout_ms=-1)
539+
540+
541+
def test_dispatcher_bound_is_finite():
542+
assert 0 < _MAX_WORKERS <= 64

0 commit comments

Comments
 (0)