Skip to content

Commit 91ca883

Browse files
committed
Merge branch 'hp/streams-quadrantsic-2-amdgpu-cpu' into hp/streams-quadrantsic-3-stream-parallel
2 parents 007b050 + 2238969 commit 91ca883

3 files changed

Lines changed: 148 additions & 1 deletion

File tree

docs/source/user_guide/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ tile16
5454
:titlesonly:
5555
5656
graph
57+
streams
5758
perf_dispatch
5859
```
5960

docs/source/user_guide/streams.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Streams
2+
3+
Streams allow concurrent execution of GPU operations. By default, all Quadrants kernels launch on the default
4+
stream, which serializes everything. By creating explicit streams, you can run independent kernels concurrently
5+
and control synchronization with events.
6+
7+
## Supported platforms
8+
9+
| Backend | Streams | Events | Notes |
10+
|---------|---------|--------|-------|
11+
| CUDA | Yes | Yes | Full concurrent execution |
12+
| AMDGPU | Yes | Yes | Full concurrent execution (requires ROCm >= 5.4) |
13+
| CPU | No-op | No-op | `qd_stream` is silently ignored, kernels run serially |
14+
| Metal | No-op | No-op | `qd_stream` is silently ignored, kernels run serially |
15+
| Vulkan | No-op | No-op | `qd_stream` is silently ignored, kernels run serially |
16+
17+
On backends without native stream support, `create_stream()` and `create_event()` return objects with handle
18+
`0`. All stream/event operations become no-ops and kernels run serially. Code written with streams is portable across all backends in the sense that it will run without modifications, but serially.
19+
20+
## Creating and using streams
21+
22+
```python
23+
import quadrants as qd
24+
25+
qd.init(arch=qd.cuda)
26+
27+
N = 1024
28+
a = qd.field(qd.f32, shape=(N,))
29+
b = qd.field(qd.f32, shape=(N,))
30+
31+
@qd.kernel
32+
def fill_a():
33+
for i in range(N):
34+
a[i] = 1.0
35+
36+
@qd.kernel
37+
def fill_b():
38+
for i in range(N):
39+
b[i] = 2.0
40+
41+
s1 = qd.create_stream()
42+
s2 = qd.create_stream()
43+
44+
fill_a(qd_stream=s1)
45+
fill_b(qd_stream=s2)
46+
47+
s1.synchronize()
48+
s2.synchronize()
49+
50+
s1.destroy()
51+
s2.destroy()
52+
```
53+
54+
Pass `qd_stream=` to any kernel call to launch it on that stream. Kernels on different streams may execute
55+
concurrently. Call `synchronize()` to block until all work on a stream completes.
56+
57+
## Events
58+
59+
Events let you express dependencies between streams without full synchronization.
60+
61+
```python
62+
s1 = qd.create_stream()
63+
s2 = qd.create_stream()
64+
65+
@qd.kernel
66+
def produce():
67+
for i in range(N):
68+
a[i] = 10.0
69+
70+
@qd.kernel
71+
def consume():
72+
for i in range(N):
73+
b[i] = a[i]
74+
75+
produce(qd_stream=s1)
76+
77+
e = qd.create_event()
78+
e.record(s1) # record when s1 finishes produce()
79+
e.wait(qd_stream=s2) # s2 waits for that event before proceeding
80+
81+
consume(qd_stream=s2) # safe to read a[] — produce() is guaranteed complete
82+
s2.synchronize()
83+
84+
e.destroy()
85+
s1.destroy()
86+
s2.destroy()
87+
```
88+
89+
`e.record(stream)` captures the point in `stream`'s execution. `e.wait(qd_stream=stream)` makes `stream` wait
90+
until the recorded point is reached. If `qd_stream` is omitted, the default stream waits.
91+
92+
## Context managers
93+
94+
Streams and events support `with` blocks for automatic cleanup:
95+
96+
```python
97+
with qd.create_stream() as s:
98+
fill_a(qd_stream=s)
99+
s.synchronize()
100+
# s.destroy() called automatically
101+
```
102+
103+
## PyTorch interop (CUDA)
104+
105+
When mixing Quadrants kernels with PyTorch operations on CUDA, both frameworks must use the same stream to
106+
avoid race conditions. Without explicit stream management, Quadrants and PyTorch may launch work on different
107+
streams with no ordering guarantees, leading to intermittent data corruption.
108+
109+
### Running Quadrants kernels on PyTorch's stream
110+
111+
```python
112+
import torch
113+
from quadrants.lang.stream import Stream
114+
115+
torch_stream_ptr = torch.cuda.current_stream().cuda_stream
116+
stream = Stream(torch_stream_ptr)
117+
118+
physics_kernel(qd_stream=stream)
119+
observations = compute_obs_tensor() # PyTorch op on the same stream
120+
apply_actions_kernel(qd_stream=stream)
121+
```
122+
123+
Wrap PyTorch's raw `CUstream` pointer in a Quadrants `Stream` object. Do **not** call `destroy()` on this
124+
wrapper — PyTorch owns the underlying stream.
125+
126+
### Running PyTorch operations on a Quadrants stream
127+
128+
```python
129+
qd_stream = qd.create_stream()
130+
torch_stream = torch.cuda.ExternalStream(qd_stream.handle)
131+
132+
with torch.cuda.stream(torch_stream):
133+
physics_kernel(qd_stream=qd_stream)
134+
observations = compute_obs_tensor()
135+
apply_actions_kernel(qd_stream=qd_stream)
136+
137+
qd_stream.destroy()
138+
```
139+
140+
`Stream.handle` is the raw `CUstream` pointer, which `torch.cuda.ExternalStream` accepts directly.
141+
142+
## Limitations
143+
144+
- **Not compatible with graphs.** Do not pass `qd_stream` to a kernel decorated with `graph=True`.
145+
- **No automatic synchronization.** You are responsible for inserting events or `synchronize()` calls when one
146+
stream's output is another stream's input.

tests/python/test_perf_dispatch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def my_func1_impl_a_shape0_ge_2(
109109
assert len(speed_checker._trial_count_by_dispatch_impl_by_geometry_hash[geometry]) == 2
110110

111111

112-
@test_utils.test()
112+
@test_utils.test(exclude=[qd.vulkan])
113113
def test_perf_dispatch_python() -> None:
114114
WARMUP = 3
115115

0 commit comments

Comments
 (0)