|
4 | 4 | #include "quadrants/runtime/cuda/cuda_utils.h" |
5 | 5 | #include "quadrants/rhi/cuda/cuda_context.h" |
6 | 6 | #include "quadrants/rhi/cuda/cuda_driver.h" |
| 7 | +#include "quadrants/rhi/cuda/cuda_stream_pin.h" |
7 | 8 | #include "quadrants/runtime/llvm/llvm_runtime_executor.h" |
8 | 9 | #include "quadrants/program/adstack_size_expr_eval.h" |
9 | 10 | #include "quadrants/program/program.h" |
@@ -285,6 +286,23 @@ void KernelLauncher::launch_llvm_kernel(Handle handle, LaunchContextBuilder &ctx |
285 | 286 |
|
286 | 287 | auto *active_stream = CUDAContext::get_instance().get_stream(); |
287 | 288 |
|
| 289 | + // Default-stream fast path: every HtoD / DtoH / kernel-dispatch in this launcher already routes through |
| 290 | + // `active_stream`, so when entry `stream_ == nullptr` AND every offloaded task launches on the same `active_stream` |
| 291 | + // (i.e. `stream_parallel_group_id == 0`), the entire chain serialises on the legacy default stream. The |
| 292 | + // `stream_synchronize` barriers between phases collapse to no-ops the surrounding sync DtoH (host-blocking on |
| 293 | + // pageable host memory) already drains, and the `CudaDefaultStreamPinGuard` re-pins `CUDAContext::stream_` to |
| 294 | + // nullptr defensively across the launch in case an inner helper temporarily swaps it. Outside the fast path - |
| 295 | + // user-supplied stream OR any task on `stream_parallel_group_id != 0` (per-group acquired streams differ from |
| 296 | + // `active_stream`) - the cross-stream barriers below are load-bearing for HtoD / kernel / DtoH visibility and the |
| 297 | + // pin guard would silently override the user-requested stream at the kernel-launch site, so the guard stays |
| 298 | + // disengaged and main's sync semantics remain untouched. Closes the cross-stream-visibility break that |
| 299 | + // `cuMemAllocAsync`-pool buffers show on pre-Ampere hardware (Turing T4 faults at `cuLaunchKernel` with |
| 300 | + // `illegal-address` when consumer + producer streams differ) on the default-stream path. |
| 301 | + const bool all_sgid_zero = std::all_of(offloaded_tasks.begin(), offloaded_tasks.end(), |
| 302 | + [](const OffloadedTask &t) { return t.stream_parallel_group_id == 0; }); |
| 303 | + const bool default_stream_path = (active_stream == nullptr) && all_sgid_zero; |
| 304 | + CudaDefaultStreamPinGuard cuda_pin(/*engage=*/default_stream_path); |
| 305 | + |
288 | 306 | char *device_result_buffer{nullptr}; |
289 | 307 | // Launcher-global persistent `result_buffer`. See `kernel_launcher.h` for why this one is shared across handles |
290 | 308 | // (kernel writes + synchronous host readback before any other reader runs). `arg_buffer` and `runtime_context` |
@@ -370,7 +388,11 @@ void KernelLauncher::launch_llvm_kernel(Handle handle, LaunchContextBuilder &ctx |
370 | 388 | } |
371 | 389 | } |
372 | 390 | } |
373 | | - if (transfers.size() > 0) { |
| 391 | + // On the default-stream fast path the post-HtoD `stream_synchronize` is redundant: HtoD goes on the null stream and |
| 392 | + // the subsequent `cuda_module->launch` reads `CUDAContext::stream_` (pinned to nullptr) so the kernel dispatch |
| 393 | + // serialises with the HtoD by null-stream ordering. Outside the fast path the barrier remains load-bearing because |
| 394 | + // HtoD on `active_stream` is async and per-group launches read it from a different stream. |
| 395 | + if (transfers.size() > 0 && !default_stream_path) { |
374 | 396 | CUDADriver::get_instance().stream_synchronize(active_stream); |
375 | 397 | } |
376 | 398 | char *host_result_buffer = (char *)ctx.get_context().result_buffer; |
@@ -459,19 +481,32 @@ void KernelLauncher::launch_llvm_kernel(Handle handle, LaunchContextBuilder &ctx |
459 | 481 | CUDADriver::get_instance().memcpy_device_to_host_async(host_result_buffer, device_result_buffer, |
460 | 482 | ctx.result_buffer_size, active_stream); |
461 | 483 | } |
462 | | - // copy data back to host |
| 484 | + // Copy data back to host. On the default-stream fast path the kernel ran on the null stream, so a sync |
| 485 | + // `cuMemcpyDtoH` (host-blocking, on null stream) sees the kernel's writes without an explicit cross-stream barrier |
| 486 | + // and host-drains the prior async `memcpy_device_to_host_async(host_result_buffer, ...)` queued on the same null |
| 487 | + // stream - the explicit `stream_synchronize(nullptr)` calls collapse to no-ops. Outside the fast path the barriers |
| 488 | + // remain load-bearing for cross-stream visibility (per-group kernel writes vs `active_stream` DtoH). |
463 | 489 | if (transfers.size() > 0) { |
464 | | - CUDADriver::get_instance().stream_synchronize(active_stream); |
| 490 | + if (!default_stream_path) { |
| 491 | + CUDADriver::get_instance().stream_synchronize(active_stream); |
| 492 | + } |
465 | 493 | for (auto itr = transfers.begin(); itr != transfers.end(); itr++) { |
466 | 494 | auto &idx = itr->first; |
467 | | - CUDADriver::get_instance().memcpy_device_to_host_async(itr->second.first, (void *)device_ptrs[idx], |
468 | | - ctx.array_runtime_sizes[idx.arg_id], active_stream); |
| 495 | + if (default_stream_path) { |
| 496 | + CUDADriver::get_instance().memcpy_device_to_host(itr->second.first, (void *)device_ptrs[idx], |
| 497 | + ctx.array_runtime_sizes[idx.arg_id]); |
| 498 | + } else { |
| 499 | + CUDADriver::get_instance().memcpy_device_to_host_async(itr->second.first, (void *)device_ptrs[idx], |
| 500 | + ctx.array_runtime_sizes[idx.arg_id], active_stream); |
| 501 | + } |
| 502 | + } |
| 503 | + if (!default_stream_path) { |
| 504 | + CUDADriver::get_instance().stream_synchronize(active_stream); |
469 | 505 | } |
470 | | - CUDADriver::get_instance().stream_synchronize(active_stream); |
471 | 506 | for (auto itr = transfers.begin(); itr != transfers.end(); itr++) { |
472 | 507 | executor->deallocate_memory_on_device(itr->second.second); |
473 | 508 | } |
474 | | - } else if (ctx.result_buffer_size > 0) { |
| 509 | + } else if (ctx.result_buffer_size > 0 && !default_stream_path) { |
475 | 510 | CUDADriver::get_instance().stream_synchronize(active_stream); |
476 | 511 | } |
477 | 512 | // Free per-call ephemeral buffers (explicit-stream path). The free is stream-ordered: it won't execute until all |
|
0 commit comments