Repository navigation
cuda: skip redundant stream synchronization - #29796
hthadicherla wants to merge 5 commits into
Conversation
Assisted-by: OpenAI Codex
|
/bot review |
Automated code reviewReview of PR #29796 ( I traced every async work submission and every synchronization site in Verification of the core invariant
No blocking correctness or security issues found. The approach is minimal, reuses the existing Will slow the review (minor, worth addressing) (point 1) (point 2) Nits (point 3) (point 4) K-shift/defrag memory graphs ( Everything else checks out: naming/indentation match surrounding style, ASCII only, concise comments, no API or docs changes needed, and concrete perf data is included in the PR description. AI usage disclosure is filled in per the PR template. This review was generated automatically by pi coding agent using |
| void llama_context::output_synchronize() { | ||
| if (output_pending) { | ||
| synchronize(); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Any reason to introduce output_synchronize() instead of adding the flag logic directly to the existing synchornize()?
There was a problem hiding this comment.
No particular reason against it. I didn't get the idea at the time .
Added the flag logic directly inside synchronize now in the latest commit.
Keep timing and token accounting outside the pending-work guard. Clear the flag after pipeline synchronization and restore the original getter and sampling call sites. Assisted-by: OpenAI Codex
ggerganov
left a comment
There was a problem hiding this comment.
My concern is that this bookkeeping is fragile - we can easily forget to set the output_pending flag in the future if another call to ggml_backend_sched_graph_compute_async is added somewhere in the llama_context.
Maybe we should think about moving this flag down inside the ggml scheduler?
Fair point. I didn't think about there being multiple instances of
Makes sense, I will do this in the next commit |
|
Could you attach screenshots of the nsys reports to show the before and after comparison for this PR? |
Assisted-by: OpenAI Codex
|
Thanks for the suggestion @ruixiang63 . Here are the before/after Nsight Systems captures. On three identical public Qwen3.8-27B + DFlash2 coding requests (both Q4_K_M, C1, draft length 7, 512 output tokens each), recorded cudaStreamSynchronize calls fell from 36,199 to 14,341 (-60.4%). Generated outputs and draft counters match. |
Preserve unconditional synchronization for backends that have not opted in. Restore the SYCL and virtgpu paths and check the default fallback in test-alloc. Assisted-by: OpenAI Codex
|
Updated in latest commit. The flag is now tracked at the shared ggml backend entry points rather than in I also narrowed the optimization to CUDA through an explicit opt-in. SYCL/virtgpu edits are removed, and all other backends retain unconditional synchronization. Other backends can opt in after their maintainers verify that every async submission path invalidates the tracking state. The backend API bump is needed for the internal struct layout change. The fallback regression test and local CPU/CUDA checks pass. I updated the description to include the added tests |
|
cc @aendk |
I missed that. So this does not work. Also the new version of the PR does not work - this is much more complex than I initially thought. For now I am not sure what would be a good way to support this. Btw ~10% gains from this change sound a bit unrealistic. Would be useful to get a second confirmation of this suggested improvement. |
|
The improvement would probably be in windows where these things cost a lot? |
|
@am17an I highly doubt that 10% is likely here. |
|
Quickly ran it through My stance on this is that medium term, we should continue on the path to first improve testing on the scheduler via #27258 and then to rework the scheduler via #25319, as previously planned. This could serve as a bandaid-fix for the mean time, but with the same associated risks as my previous linked PRs (which led to them being reverted). |
|
My opinion is that we should be conservative with changes to the scheduler until #27258 is done. The way I see it our bottleneck is reviewer time so we should prioritize the things that make this easier, i.e. the tests. Once we have that I would look at this PR in detail, compare it against #25319 , and then see how to proceed from there. I think a 10% speedup, while nice, is not large enough to justify doing things in an order that is suboptimal in terms of maintainer time. So I would say not to merge this PR as a band-aid fix. |
|
I agree - let's park it for now. |
Assisted-by: OpenAI Codex
|
Thanks for the feedback everyone. I’ve narrowed this to a CUDA-only change: stream usage is tracked inside the CUDA context, with no scheduler changes or backend API bump. The final diff touches just two CUDA files. It preserves the measured C1 benefit, and local correctness tests pass. It still changes synchronization behavior beneath the scheduler, so I understand the caution and will leave the review timing to you. @ggerganov @aendk , can you PTAL at the PR now ? |
|
Is there any measurable performance benefit from just these changes without the ggml backend scheduler changes? |
|
@JohannesGaessler Yes. I measured the CUDA-only change now in latest commit, without the earlier generic backend changes or any scheduler changes. Sorry, I should have made that explicit in my previous comment. Both arms already use DFlash2 with Qwen3.8-27B (both Q4_K_M, target output Q8_0). On Windows / RTX PRO 6000 Blackwell / CUDA 13.4, at C1 over the full SPEED-Bench coding category (80 conversations / 89 turns per run):
The changed value is the mean of two full runs (139.20 and 138.77 tokens/s); stock is one preserved earlier run. Requests/settings and all 89 generated outputs, usage and generation/draft counters match, with zero failures. This is the measured benefit on this Windows DFlash2 workload, not a claim of the same gain on Linux or ordinary decoding; an independent reproduction would still be useful. |
|
Btw the improvements numbers are for model with DFlash2. The improvement in perf for just the baseline model without DFlash2 are much less than above. Comparision in perf with only Qwen3.8-27B model
|
Imo it's not the responsibility of a backend to track whether its busy or not, and this PR adds technical debt in that regard. If we want to invest in improving spec dec, we should instead focus & invest on enabling a path where spec dec fully resides on a backend. We are doing baby steps in that regard already with rejection sampling. |
|
I think I'm missing some context for this PR. To my understanding, in its current form essentially all it does is cache in the ggml CUDA code whether or not a call to the CUDA API for synchronization is actually necessary. And I guess on Windows the performance of those API calls is terrible so it makes a difference? |
@JohannesGaessler Yes, that’s essentially what the current version does. After a successful synchronization, subsequent calls skip cudaStreamSynchronize until the stream or BLAS handle is acquired again. Acquiring either conservatively marks the stream as potentially having new work.
If we want to invest in improving spec dec, we should instead focus & invest on enabling a path where spec dec fully resides on a backend. We are doing baby steps in that regard already with rejection sampling. @ORippler, I agree that moving speculative decoding onto the backend is a better long-term direction. This overhead also affects ordinary decoding: without speculative decoding, we measured a 16.9% throughput improvement on the same Windows setup. That broadens the benefit, but I understand it doesn’t remove the concern about maintaining additional state in the backend. The tradeoff is whether that measured benefit justifies the bookkeeping while the broader synchronization work progresses. I would like to hear your thoughts on this. |
|
Until we are sure that this is the right solution for this issue, and the solution is robust enough, it makes sense to keep this PR as draft. Parking it for now. |
Measured on the standard greedy prompt (iGPU masked): inject 6.67 ms, draft-noise 13.11 ms per call; with verify taking the remaining ~52 ms of the 72 ms step the three llama_process calls fully account for the wall - each blocks through its own GPU completion. Async-pipelining inject into draft is the -5..6 ms decode lever but needs a streamQuery guard (the naive sync-skip poisoned the draft once before, see ggml-org#29796 notes).




Overview
Repeated synchronization calls can enter the CUDA runtime even when no stream work has been submitted since the previous wait.
Track stream use inside the CUDA context. Acquiring a stream or BLAS handle marks the stream as used; successful synchronization clears that state. A later synchronization returns immediately if the stream has not been acquired again. Graph, copy and event callers use the existing accessors without separate pending-work bookkeeping.
The stream and BLAS handle arrays are private. Borrowed handles must stay within the current backend call and must not be cached across context synchronization.
The optimization applies only to native CUDA with one logical device. Multiple devices, HIP and MUSA retain unconditional synchronization. The change touches two CUDA files: no scheduler changes, generic backend API/ABI changes, or CUDA kernel changes.
Performance
Full SPEED-Bench coding, C1: 80 conversations / 89 turns per run.
Decode throughput increased by 52.55% on this setup. The changed version is the mean of two full runs (139.20 and 138.77 tokens/s); stock is the preserved earlier full run. All outputs and generation/draft counters match. These are local Windows measurements, not a general cross-platform performance claim or a statistical equivalence test.
Setup: Qwen3.8-27B + DFlash2, both Q4_K_M, target output Q8_0 shared with the draft; RTX PRO 6000 Blackwell Server Edition, Windows, CUDA 13.4. Context 40960, F16 KV, Flash Attention, greedy seed 1234, 512-token cap, draft length 7, thinking, prompt caching and target/draft backend sampling enabled.
Separate fresh Nsight Systems captures cover three identical coding requests, 512 output tokens each, after an excluded warmup:
cudaStreamSynchronizecudaGraphLaunchcudaMemcpyAsyncSynchronization calls decreased by 65.8%; outputs and counters match. The throughput table above comes from unprofiled full runs, not the instrumented captures.
Local validation
Requirements