Skip to content

Commit b3fbc39

Browse files
committed
[Doc] Add stream_parallel() section to streams user guide
1 parent 91ca883 commit b3fbc39

1 file changed

Lines changed: 57 additions & 27 deletions

File tree

docs/source/user_guide/streams.md

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
11
# Streams
22

33
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.
4+
stream, which serializes everything. With streams, you can run multiple top-level for loops in parallel.
65

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.
6+
## Stream parallelism
197

20-
## Creating and using streams
8+
Inside a `@qd.kernel`, each `with qd.stream_parallel():` block runs on its own GPU stream. The runtime
9+
creates temporary streams, launches the for loops, and synchronizes automatically before the next
10+
non-parallel statement.
2111

2212
```python
2313
import quadrants as qd
@@ -27,17 +17,56 @@ qd.init(arch=qd.cuda)
2717
N = 1024
2818
a = qd.field(qd.f32, shape=(N,))
2919
b = qd.field(qd.f32, shape=(N,))
20+
c = qd.field(qd.f32, shape=(N,))
3021

3122
@qd.kernel
32-
def fill_a():
33-
for i in range(N):
34-
a[i] = 1.0
23+
def compute_ab():
24+
with qd.stream_parallel():
25+
for i in range(N):
26+
a[i] = compute_a(i)
27+
with qd.stream_parallel():
28+
for j in range(N):
29+
b[j] = compute_b(j)
3530

3631
@qd.kernel
37-
def fill_b():
32+
def combine():
3833
for i in range(N):
39-
b[i] = 2.0
34+
c[i] = a[i] + b[i]
35+
36+
compute_ab() # the two stream_parallel blocks run concurrently
37+
combine() # runs after compute_ab() returns — a[] and b[] are ready
38+
```
39+
40+
Consecutive `with qd.stream_parallel():` blocks run concurrently. Multiple for loops within a single block
41+
share a stream and run serially on it. All streams are synchronized before the kernel returns.
42+
43+
### Restrictions
44+
45+
- All top-level statements in a kernel must be either all `stream_parallel` blocks or all regular statements.
46+
Mixing the two at the top level is a compile-time error.
47+
- Nesting `stream_parallel` blocks is not supported.
4048

49+
## Supported platforms
50+
51+
| Backend | Streams | Events |
52+
|---------|---------|--------|
53+
| CUDA | Yes | Yes |
54+
| AMDGPU | Yes | Yes |
55+
| CPU | No-op | No-op |
56+
| Metal | No-op | No-op |
57+
| Vulkan | No-op | No-op |
58+
59+
On backends without native stream support, stream operations are no-ops and for loops run serially. Code using
60+
streams is portable across all backends — it will run without modifications, but serially.
61+
62+
## Explicit streams
63+
64+
For cases that require manual control — such as launching separate kernels on different streams or
65+
interoperating with PyTorch — you can create and manage streams directly.
66+
67+
### Creating and using streams
68+
69+
```python
4170
s1 = qd.create_stream()
4271
s2 = qd.create_stream()
4372

@@ -54,7 +83,7 @@ s2.destroy()
5483
Pass `qd_stream=` to any kernel call to launch it on that stream. Kernels on different streams may execute
5584
concurrently. Call `synchronize()` to block until all work on a stream completes.
5685

57-
## Events
86+
### Events
5887

5988
Events let you express dependencies between streams without full synchronization.
6089

@@ -89,7 +118,7 @@ s2.destroy()
89118
`e.record(stream)` captures the point in `stream`'s execution. `e.wait(qd_stream=stream)` makes `stream` wait
90119
until the recorded point is reached. If `qd_stream` is omitted, the default stream waits.
91120

92-
## Context managers
121+
### Context managers
93122

94123
Streams and events support `with` blocks for automatic cleanup:
95124

@@ -100,13 +129,13 @@ with qd.create_stream() as s:
100129
# s.destroy() called automatically
101130
```
102131

103-
## PyTorch interop (CUDA)
132+
### PyTorch interop (CUDA)
104133

105134
When mixing Quadrants kernels with PyTorch operations on CUDA, both frameworks must use the same stream to
106135
avoid race conditions. Without explicit stream management, Quadrants and PyTorch may launch work on different
107136
streams with no ordering guarantees, leading to intermittent data corruption.
108137

109-
### Running Quadrants kernels on PyTorch's stream
138+
#### Running Quadrants kernels on PyTorch's stream
110139

111140
```python
112141
import torch
@@ -123,7 +152,7 @@ apply_actions_kernel(qd_stream=stream)
123152
Wrap PyTorch's raw `CUstream` pointer in a Quadrants `Stream` object. Do **not** call `destroy()` on this
124153
wrapper — PyTorch owns the underlying stream.
125154

126-
### Running PyTorch operations on a Quadrants stream
155+
#### Running PyTorch operations on a Quadrants stream
127156

128157
```python
129158
qd_stream = qd.create_stream()
@@ -142,5 +171,6 @@ qd_stream.destroy()
142171
## Limitations
143172

144173
- **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.
174+
- **No automatic synchronization with explicit streams.** When using explicit streams, you are responsible for
175+
inserting events or `synchronize()` calls when one stream's output is another stream's input.
176+
`stream_parallel` handles this automatically.

0 commit comments

Comments
 (0)