Skip to content

Commit 2ef3077

Browse files
committed
[Perf] CUDA + AMDGPU launchers: pin stream to null at launcher scope; drop redundant pre/post-kernel stream_synchronize
1 parent 0a27b89 commit 2ef3077

6 files changed

Lines changed: 77 additions & 53 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#pragma once
2+
3+
// RAII guard that pins `CUDAContext::stream_` to the legacy default stream (nullptr) for the duration of the scope,
4+
// then restores the prior `stream_` on destruction. Pinning makes the consumer + producer streams match unconditionally
5+
// so the cross-stream-visibility break that CUDA's stream-ordered allocator (`cuMemAllocAsync` pool) shows on
6+
// pre-Ampere hardware (Turing T4 faults at `cuLaunchKernel` with `illegal-address`) is closed by construction. The
7+
// destructor does NOT call `stream_synchronize(nullptr)`: the helpers using this guard always issue a synchronous DtoH
8+
// inside the scope which already drains the null stream, and the main launcher's kernel-only fast path (no host-side
9+
// sync DtoH) deliberately stays asynchronous so kernel launches do not pay a forced per-launch host-GPU round-trip.
10+
// Restoring the prior stream while null-stream work is still in flight is safe: subsequent operations on the prior
11+
// stream that depend on the null-stream work will surface the standard cross-stream visibility constraints anyway, and
12+
// the next launch through this guard re-pins to null and serialises with prior null-stream work via legacy default
13+
// stream semantics. Used at the entry of every per-launch CUDA host path that issues HtoD or kernel work that needs
14+
// the same-stream invariant: `KernelLauncher::launch_llvm_kernel`, the adstack helpers
15+
// `dispatch_max_reducers_for_tasks` / `publish_adstack_metadata` / `publish_per_task_bound_count_device`. AMDGPU does
16+
// not need an equivalent because `AMDGPUContext::launch` passes `nullptr` for the stream parameter unconditionally;
17+
// HIP kernels always run on the null stream regardless of any context-level stream setting.
18+
19+
#include "quadrants/platform/cuda/detect_cuda.h"
20+
21+
#if defined(QD_WITH_CUDA)
22+
#include "quadrants/rhi/cuda/cuda_context.h"
23+
#include "quadrants/rhi/cuda/cuda_driver.h"
24+
#endif
25+
26+
namespace quadrants::lang {
27+
28+
#if defined(QD_WITH_CUDA)
29+
struct CudaDefaultStreamPinGuard {
30+
bool engaged{false};
31+
void *prev_stream{nullptr};
32+
explicit CudaDefaultStreamPinGuard(bool engage) : engaged(engage) {
33+
if (engaged) {
34+
prev_stream = CUDAContext::get_instance().get_stream();
35+
CUDAContext::get_instance().set_stream(nullptr);
36+
}
37+
}
38+
~CudaDefaultStreamPinGuard() {
39+
if (engaged) {
40+
CUDAContext::get_instance().set_stream(prev_stream);
41+
}
42+
}
43+
};
44+
#endif
45+
46+
} // namespace quadrants::lang

quadrants/runtime/amdgpu/kernel_launcher.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,14 @@ void KernelLauncher::launch_llvm_kernel(Handle handle, LaunchContextBuilder &ctx
258258
}
259259
}
260260
}
261-
if (transfers.size() > 0) {
262-
AMDGPUDriver::get_instance().stream_synchronize(nullptr);
263-
}
261+
// No explicit `stream_synchronize(nullptr)` needed here. `AMDGPUContext::launch` passes `nullptr` as the stream
262+
// parameter to `hipModuleLaunchKernel` unconditionally (see `rhi/amdgpu/amdgpu_context.cpp::launch`), so kernels
263+
// always run on the null stream regardless of any context-level stream setting; with the HtoD copies above being
264+
// sync `hipMemcpyHtoD` (host-blocking) on the same null stream, ordering is preserved without an explicit barrier
265+
// even on hardware where stream-ordered allocator pools have stricter cross-stream visibility rules. Mirrors the
266+
// CUDA launcher cleanup; CUDA needs a `CudaDefaultStreamPinGuard` to enforce same-stream invariant because
267+
// `CUDAContext::launch` reads `CUDAContext::stream_` (mutable, may be non-null), while HIP's launch path is
268+
// unconditionally null-stream.
264269
char *host_result_buffer = (char *)ctx.get_context().result_buffer;
265270
if (ctx.result_buffer_size > 0) {
266271
// Malloc_Async and Free_Async are available after ROCm 5.4

quadrants/runtime/cuda/kernel_launcher.cpp

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "quadrants/runtime/cuda/kernel_launcher.h"
22
#include "quadrants/runtime/cuda/cuda_utils.h"
33
#include "quadrants/rhi/cuda/cuda_context.h"
4+
#include "quadrants/rhi/cuda/cuda_stream_pin.h"
45
#include "quadrants/runtime/llvm/llvm_runtime_executor.h"
56
#include "quadrants/program/adstack_size_expr_eval.h"
67
#include "quadrants/program/program.h"
@@ -208,6 +209,18 @@ void KernelLauncher::launch_llvm_kernel(Handle handle, LaunchContextBuilder &ctx
208209

209210
CUDAContext::get_instance().make_current();
210211

212+
// Pin `CUDAContext::stream_` to the legacy default stream (nullptr) for the duration of the launch. The HtoD copies
213+
// below are sync `cuMemcpyHtoD` (legacy null stream), the kernel dispatches via `cuda_module->launch` go through
214+
// `CUDAContext::stream_` (now nullptr), and the post-kernel async + sync DtoH calls also land on the null stream.
215+
// With every operation pinned to one stream, the cross-stream-visibility break that pre-Ampere shows for
216+
// `cuMemAllocAsync`-pool buffers (Turing T4 faults at `cuLaunchKernel` with `illegal-address` when consumer +
217+
// producer streams differ) is eliminated unconditionally, and the explicit `stream_synchronize(nullptr)` barriers
218+
// between phases below collapse to no-ops paid for by the surrounding sync DtoH calls. Same RAII pattern as the
219+
// Same RAII pattern as the `CudaDefaultStreamPinGuard` defined in `rhi/cuda/cuda_stream_pin.h`; engaging it here at
220+
// launcher scope covers the HtoD/kernel/DtoH chain that the per-helper guards inside the adstack subsystem do not
221+
// span.
222+
CudaDefaultStreamPinGuard cuda_pin(/*engage=*/true);
223+
211224
// |transfers| is only used for external arrays whose data is originally on
212225
// host. They are first transferred onto device and that device pointer is
213226
// stored in |device_ptrs| below. |transfers| saves its original pointer so
@@ -309,9 +322,10 @@ void KernelLauncher::launch_llvm_kernel(Handle handle, LaunchContextBuilder &ctx
309322
}
310323
}
311324
}
312-
if (transfers.size() > 0) {
313-
CUDADriver::get_instance().stream_synchronize(nullptr);
314-
}
325+
// No explicit `stream_synchronize(nullptr)` needed here. The HtoD copies above are sync `cuMemcpyHtoD` (host-blocking
326+
// by definition); with the launcher's `CudaDefaultStreamPinGuard` ensuring the subsequent kernel dispatch lands on
327+
// the same null stream, ordering between HtoD and kernel is preserved without an explicit barrier even on pre-Ampere
328+
// pool buffers - the cross-stream visibility break only fires when producer + consumer streams differ.
315329
char *host_result_buffer = (char *)ctx.get_context().result_buffer;
316330
if (ctx.result_buffer_size > 0) {
317331
ctx.get_context().result_buffer = (uint64 *)device_result_buffer;
@@ -380,9 +394,11 @@ void KernelLauncher::launch_llvm_kernel(Handle handle, LaunchContextBuilder &ctx
380394
CUDADriver::get_instance().memcpy_device_to_host_async(host_result_buffer, device_result_buffer,
381395
ctx.result_buffer_size, nullptr);
382396
}
383-
// copy data back to host
397+
// copy data back to host. With the launcher-scope `CudaDefaultStreamPinGuard` the kernel ran on the null stream, so
398+
// sync `cuMemcpyDtoH` (host-blocking, on null stream) sees the kernel's writes without an explicit cross-stream
399+
// barrier. The first sync DtoH below also drains the prior async `memcpy_device_to_host_async(host_result_buffer,
400+
// ...)` queued on the same null stream, so an explicit `stream_synchronize(nullptr)` here would be redundant.
384401
if (transfers.size() > 0) {
385-
CUDADriver::get_instance().stream_synchronize(nullptr);
386402
for (auto itr = transfers.begin(); itr != transfers.end(); itr++) {
387403
auto &idx = itr->first;
388404
CUDADriver::get_instance().memcpy_device_to_host(itr->second.first, (void *)device_ptrs[idx],

quadrants/runtime/llvm/adstack_lazy_claim/bound_eval.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
#include "quadrants/rhi/amdgpu/amdgpu_context.h"
2828
#include "quadrants/rhi/amdgpu/amdgpu_driver.h"
2929

30-
#include "quadrants/runtime/llvm/adstack_lazy_claim/cuda_stream_pin.h"
30+
#include "quadrants/rhi/cuda/cuda_stream_pin.h"
3131

3232
namespace quadrants::lang {
3333

quadrants/runtime/llvm/adstack_lazy_claim/cuda_stream_pin.h

Lines changed: 0 additions & 43 deletions
This file was deleted.

quadrants/runtime/llvm/adstack_lazy_claim/metadata_publish.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
#include "quadrants/rhi/amdgpu/amdgpu_context.h"
3232
#endif
3333

34-
#include "quadrants/runtime/llvm/adstack_lazy_claim/cuda_stream_pin.h"
34+
#include "quadrants/rhi/cuda/cuda_stream_pin.h"
3535

3636
namespace quadrants::lang {
3737

0 commit comments

Comments
 (0)