Skip to content

[Perf] Streams 1-4#410

Merged
hughperkins merged 208 commits intomainfrom
hp/streams-quadrantsic-4-stream-pool
May 7, 2026
Merged

[Perf] Streams 1-4#410
hughperkins merged 208 commits intomainfrom
hp/streams-quadrantsic-4-stream-pool

Conversation

@hughperkins
Copy link
Copy Markdown
Collaborator

Issue: #

Brief Summary

copilot:summary

Walkthrough

copilot:walkthrough

Introduces qd.create_stream() and qd.create_event() for launching
kernels on separate CUDA streams with event-based synchronization.
The qd_stream kwarg on kernel calls routes the launch to a specific
stream. Non-CUDA backends return no-op handles (0). Routes kernel
launcher memory ops through the active stream.
Mirrors the CUDA stream implementation for HIP: adds stream_ member
to AMDGPUContext, stream_destroy/stream_wait_event/malloc_async/
mem_free_async to HIP driver functions, and AMDGPU branches in all
Program stream/event methods. Converts AMDGPU kernel launcher to use
async memory operations through the active stream. CPU backend
returns 0 handles (no-op).
Introduces stream_parallel() for running top-level for-loop blocks on
separate GPU streams. The AST transformer maps 'with qd.stream_parallel()'
blocks to stream-parallel group IDs, which propagate through IR lowering
and offloading to the CUDA/AMDGPU kernel launchers. Each unique group ID
gets its own stream at launch time. Includes validation that all top-level
kernel statements must be stream_parallel blocks (no mixing), and offline
cache key support.
- Make CUDAContext::stream_ thread_local for thread-safety
- Convert sync memcpy_host_to_device to async on active_stream
- Use weakref in Stream/Event __del__ to safely handle interpreter shutdown
- Add __enter__/__exit__ context manager support for Stream and Event
- Use consistent qd_stream parameter naming in Event.record and Event.wait
- Add handle==0 guard to stream_synchronize
Batch the device_result_buffer free into the stream pipeline before the
sync barrier, matching the CUDA kernel launcher's ordering for
consistency and marginal performance improvement.
Use memcpy_host_to_device_async for external array transfers so they
are properly ordered on the active stream, matching the CUDA launcher.
Lower GPU speedup threshold from 1.5x to 1.3x to reduce flakiness in
CI under contention, and print actual timings for diagnostics.
…ead_local

Mirror the CUDA fixes: guard stream_synchronize against handle==0 to
avoid unintentional default stream sync, and make AMDGPUContext::stream_
thread_local for thread-safety.
…adrantsic-3-stream-parallel

# Conflicts:
#	python/quadrants/lang/stream.py
Prevents stale group IDs from leaking if insert_for is called after a
path that set a non-zero stream_parallel_group_id, matching the reset
pattern of all other ForLoopConfig fields.
Add an error check in begin_stream_parallel() to prevent nesting, which
would produce undefined group ID semantics.
…context safety

Add comments explaining that streams are created/destroyed per launch
(stream pooling as future optimization), and that RuntimeContext sharing
across concurrent streams is safe because kernels only read from it.
Replace per-launch stream_create/stream_destroy with acquire_stream/
release_stream on CUDAContext and AMDGPUContext. Streams are cached in
a pool and reused across invocations, avoiding the driver-level overhead
of stream creation (~5-50us) on every kernel launch in hot loops.
@hughperkins hughperkins changed the base branch from main to hp/streams-quadrantsic-3-stream-parallel March 12, 2026 01:35
Calls a stream_parallel kernel 5 times in a loop to verify that pooled
streams are correctly reused with correct results each iteration.
@hughperkins
Copy link
Copy Markdown
Collaborator Author

Review from Opus 4.6:

What it does: Introduces a stream pool in both CUDAContext and AMDGPUContext so that GPU streams are reused across kernel launches rather than created and destroyed every time.
The kernel launchers now call acquire_stream() / release_stream() instead of stream_create() / stream_destroy().
What's good:

  1. Clear motivation. The previous branch even had a comment saying "a stream pool could reduce overhead for hot loops" — this PR implements exactly that and removes the TODO
    comment. Nice follow-through.
  2. Symmetry. The CUDA and AMDGPU implementations are perfectly mirrored, which is easy to review and maintain.
  3. Thread safety. Both acquire_stream and release_stream are guarded by lock_, which is correct since the context is a singleton shared across threads.
  4. Test coverage. The test runs 5 iterations of a kernel with two stream_parallel blocks, which exercises the pool reuse path.

Concerns / suggestions:

  1. Unbounded pool growth. Streams are never destroyed — release_stream just pushes back into the vector forever. If a spike causes many concurrent streams to be allocated,
    they'll remain in the pool indefinitely. Consider adding a high-water mark or trimming idle streams, or at minimum destroying them in the context destructor.
  2. No cleanup in destructor. The stream_pool_ vector will leak GPU streams when the context is destroyed. You should iterate and destroy all pooled streams in the destructor.
  3. Lock contention. Every acquire_stream / release_stream takes the global lock_ mutex. In the hot loop case this PR is optimizing for, the acquire and release happen close
    together per launch. This is probably fine for now, but if you ever acquire/release streams from many threads concurrently, a lock-free pool or thread-local free-lists would
    be better. Not needed now, just worth noting.
  4. std::vector as a stack is fine but pop_back() + push_back() means LIFO reuse, which is actually good — recently used streams are more likely to be warm in driver caches.
  5. Test could be stronger. The test validates correctness but doesn't actually verify that streams are being reused (e.g., that no new streams are created on the 2nd+
    iteration). A unit test that directly calls acquire_stream / release_stream and checks pool size would give more confidence the pooling logic itself works. Though I
    understand that may be harder to do from Python.

Overall, it's a solid, focused change. The main actionable item is making sure pooled streams get destroyed when the context shuts down.

@hughperkins
Copy link
Copy Markdown
Collaborator Author

Response from the original Opus that wrote this PR:

  1. Unbounded pool growth -- In practice this is bounded by the number of unique stream_parallel group IDs per kernel, which is typically 2-4. The pool won't grow beyond that
    peak. Not a real concern.
  2. No cleanup in destructor -- Technically valid, but both CUDAContext and AMDGPUContext are singletons allocated with new and intentionally never deleted (the CUDA destructor
    is even commented out). The OS reclaims GPU resources on process exit. That said, adding cleanup is trivially cheap and good hygiene -- I could add it.
  3. Lock contention -- Reviewer already acknowledges it's fine for now. Agreed.
  4. LIFO reuse -- Just an observation, and a positive one.
  5. Stronger test -- Fair point but acquire_stream/release_stream are C++ methods not exposed via pybind, so testing pool internals from Python isn't practical. The correctness
    test (5 iterations with correct results) is the right level of testing from this side.

I think I might get some opinions from a fresh Opus.

@hughperkins
Copy link
Copy Markdown
Collaborator Author

The reviewer Opus said the following when I asked "what are your thougths on concern 1?":

Honestly, on reflection it's probably overstated. The number of streams acquired per launch is determined by the number of distinct stream_parallel groups in a kernel — that's
a compile-time property, typically small (single digits). The pool will quickly stabilize at the high-water mark and stay there. There's no mechanism that would cause a
"spike" of stream allocation that then subsides, since the acquire/release pattern is synchronous within each launch (acquire all, launch, sync, release all).
GPU streams are also lightweight when idle — they're essentially just driver-side command queues, not large resource allocations.
So the "unbounded growth" framing is misleading. The real actionable concern is just concern 2 — destructor cleanup. You should still destroy the pooled streams when the
context shuts down to avoid leaking driver handles. But worrying about pool size management or high-water trimming is over-engineering for this use case.

@hughperkins
Copy link
Copy Markdown
Collaborator Author

So both opuses concur that the only concern is the destructor, concern 2.

@hughperkins hughperkins marked this pull request as ready for review March 12, 2026 01:51
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

PR change report (8a7cdd795)

Code lines (excluding blank lines, comment-only lines, and Python multi-line strings).

File LoC Added Removed
tests/python/test_streams.py 413 +413
quadrants/program/program_stream.cpp 151 +151
python/quadrants/lang/stream.py 111 +111
quadrants/runtime/cuda/kernel_launcher.cpp 322 +83 -27
quadrants/runtime/amdgpu/kernel_launcher.cpp 306 +70 -18
python/quadrants/lang/ast/ast_transformers/function_def_transformer.py 435 +68 -2
python/quadrants/lang/ast/ast_transformer.py 1272 +28 -3
python/quadrants/lang/kernel.py 562 +24 -3
quadrants/rhi/amdgpu/amdgpu_context.h 111 +24
python/quadrants/lang/ast/symbol_resolver.py 48 +23
quadrants/program/program_stream.h 21 +21
quadrants/ir/statements.h 1307 +20 -3
quadrants/rhi/cuda/cuda_context.h 115 +18 -1
quadrants/python/export_stream.cpp 17 +17
quadrants/ir/frontend_ir.h 806 +12
quadrants/codegen/llvm/llvm_compiled_data.h 96 +9 -3
quadrants/rhi/amdgpu/amdgpu_context.cpp 166 +7 -2
quadrants/rhi/amdgpu/amdgpu_profiler.cpp 181 +6 -4
quadrants/python/export_lang.cpp 1048 +6 -3
quadrants/rhi/cuda/cuda_context.cpp 128 +6 -1
quadrants/ir/frontend_ir.cpp 1405 +6
quadrants/rhi/amdgpu/amdgpu_device.cpp 138 +5 -3
quadrants/program/program.h 222 +5
tests/python/test_api.py 460 +5
tests/python/test_cache.py 210 +4 -4
quadrants/runtime/llvm/llvm_runtime_executor.cpp 600 +4 -2
quadrants/rhi/amdgpu/amdgpu_driver_functions.inc.h 58 +4 -1
quadrants/python/export.h 24 +4
quadrants/ir/statements.cpp 392 +3
quadrants/rhi/cuda/cuda_driver_functions.inc.h 69 +3
quadrants/transforms/lower_ast.cpp 423 +3
python/quadrants/lang/__init__.py 51 +2
quadrants/transforms/offload.cpp 602 +2
tests/python/test_perf_dispatch.py 418 +1 -1
quadrants/analysis/gen_offline_cache_key.cpp 562 +1
quadrants/codegen/amdgpu/codegen_amdgpu.cpp 429 +1
quadrants/codegen/cuda/codegen_cuda.cpp 628 +1
quadrants/program/program.cpp 403 +1
python/quadrants/lang/runtime_ops.py 4

Total: 39 file(s) changed, +1172 -81 code lines.

Full per-function report

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

Coverage Report (8a7cdd795)

File Coverage Missing
🟢 python/quadrants/lang/__init__.py 100%
🔴 python/quadrants/lang/ast/ast_transformer.py 71% 1534,1537,1539,1541,1543
🔴 python/quadrants/lang/ast/ast_transformers/function_def_transformer.py 72% 471,474,478-480,483-487,510,516-518
🔴 python/quadrants/lang/ast/symbol_resolver.py 9% 66-67,69-70,72-79,81-84,86-89
🔴 python/quadrants/lang/kernel.py 67% 573,581,586,599,668,673
🔴 python/quadrants/lang/stream.py 63% 30,36,45-51,59-62,66-72,100,105,128-134,142-145,149-155,185
🟢 tests/python/test_cache.py 100%
🟢 tests/python/test_perf_dispatch.py 100%
🟢 tests/python/test_streams.py 88% 14-18,23-27,253,309-310,312-314,316-320,325-326,328-330,332-335,340-341,343-346,348-351,465,489-493

Diff coverage: 79% · Overall: 79% · 624 lines, 132 missing

Full annotated report

@hughperkins
Copy link
Copy Markdown
Collaborator Author

hughperkins commented May 7, 2026

Genesis unit test results:

Screenshot 2026-05-07 at 08 53 35

…c-4-stream-pool

# Conflicts:
#	quadrants/codegen/llvm/llvm_compiled_data.h
#	quadrants/program/program.h
@hughperkins
Copy link
Copy Markdown
Collaborator Author

Genesis benchmark results:

20260507-0427-streams4

Comment thread docs/source/user_guide/streams.md Outdated
Comment thread docs/source/user_guide/streams.md Outdated
Comment thread docs/source/user_guide/streams.md Outdated
Comment thread docs/source/user_guide/streams.md Outdated
Comment thread docs/source/user_guide/streams.md Outdated
Comment thread docs/source/user_guide/streams.md Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Rename fill_a/fill_b to some_func1/some_func2 in explicit stream
examples. Remove redundant synchronize() from context manager example
since destroy() already waits for in-flight work.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

context_synchronize/device_synchronize waits on all streams, which
contradicts the documented behavior that qd.sync() only waits on the
default stream. stream_parallel already synchronizes its pooled streams
before returning, so a global barrier is unnecessary.
Address review comment: make explicit that qd_stream is a special
keyword argument handled by the @qd.kernel decorator, not something
the user declares in the kernel signature.
@duburcqa
Copy link
Copy Markdown
Contributor

duburcqa commented May 7, 2026

ok to merge

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Cover all 5 error paths in ASTTransformer.build_With: multiple context
managers, with-as syntax, non-call expression, non-stream_parallel
context manager, and stream_parallel inside @qd.func.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

@hughperkins hughperkins merged commit 8aad4eb into main May 7, 2026
43 of 45 checks passed
@hughperkins hughperkins deleted the hp/streams-quadrantsic-4-stream-pool branch May 7, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants