Repository navigation
Conversation
init_persistent_kernel() creates a new worker/scheduler stream pair on every call and never destroys the previous pair. HIP backs streams with a small pool of hardware queues (GPU_MAX_HW_QUEUES, default 4). Once the leaked streams use it up, the new scheduler stream can land on the same hardware queue as the new worker stream. Dispatches on one queue run serially, so the scheduler kernel waits behind the persistent worker kernel, which waits for the scheduler: the next launch never returns. Destroy the previous pair before creating the new one, and clear the handles in finalize_persistent_kernel() so an init after a finalize does not destroy them a second time. Reproduced on MI300X (gfx942), ROCm 7.2.4 with init -> launch -> init -> launch: hangs at the default queue count, passes with GPU_MAX_HW_QUEUES=8, passes with this change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A second call to
init_persistent_kernel()in the same process makes the nextlaunch_persistent_kernel()hang forever.compile()already expects repeated inits for the online modes ("We will init for multiple times so the output directory should be permanent",python/mirage/mpk/persistent_kernel.py). Callinginit_funcagain to changemax_seq_lengthor reset event timing also hits the hang.During the hang the worker kernel is running and the scheduler kernel never starts. The first launch prints 8
[SCHED_XCD]lines; the launch after the second init prints none. A runtime state dump taken during the same hang in a larger graph showed:worker_xcd_ready_count= 296)[1, 0, ...], which is onlyprepare_kernel's seed eventnext_request_idstill 0The workers spin waiting for tasks that no scheduler dispatches.
Root cause
init_persistent_kernel()callscudaStreamCreateWithFlags()(hipStreamCreateWithFlags) forworker_streamandscheduler_streamevery time. It never destroys the pair from the previous init; onlyfinalize_persistent_kernel()destroys streams.HIP backs streams with a small pool of hardware queues (
GPU_MAX_HW_QUEUES, default 4). Once the leaked streams use up the pool, the new scheduler stream can land on the same hardware queue as the new worker stream. Dispatches on one queue run serially. The scheduler kernel therefore waits behind the persistent worker kernel, and the worker kernel waits for the scheduler. This is the same serialization deadlock the split-mode comment ininit_persistent_kernel()describes forrocprofv3 --pmc.Raising the pool size with
GPU_MAX_HW_QUEUES=8makes the same sequence pass, which is consistent with this explanation.Fix
init_persistent_kernel(): if a stream pair already exists, destroy it before creating the new pair.global_runtime_configis a namespace-scope static, so the handles are null on the first init.finalize_persistent_kernel(): clear both handles after destroying them, so an init after a finalize (same launcher.so) does not destroy them twice.The ownership model stays the same (init creates, finalize destroys). Launch behaviour on a single init is unchanged.
This change does not address the events and device buffers that
init_persistent_kernel()also re-creates on each call. They leak memory but do not deadlock; I kept this PR to the hang.Reproduction
The Qwen3 demo calls init once, so it cannot hit this. On MI300X its default path (
USE_CK_FMHA=1) also needs gfx950-only builtins. The reproduction below is instead a standalone script using only upstream code:embed_layer -> rmsnorm_layer -> argmax_partial_layer -> argmax_reduce_layer, one decode iteration per launchcompile()(init Adding support for gpt oss120b model on mi355 #1) -> launch 1 ->init_func(init added GPT-OSS 120B (bs=1) MegaKernel on mi355 #2) -> launch 2--no-reinitskips init added GPT-OSS 120B (bs=1) MegaKernel on mi355 #2output_tokens, so a passing launch really ran the kernelstimeoutis needed becauselaunch_funcblocks incudaStreamSynchronizewhile holding the GIL.reinit_repro.py
Commands (MI300X, so
AMDGPU_TARGETS=gfx942;deps/cloned at json v3.12.0, cutlass v4.7.1, composable_kernel ac18460782fadcd24aa321394c63dc284c90593b):Before, default environment (the 8
[SCHED_XCD]lines of launch 1 omitted; none appear afterlaunch 2: start):Before,
GPU_MAX_HW_QUEUES=8:After, default environment:
Environment:
amd_mi350at 51dce4fTesting
GPU_MAX_HW_QUEUES=8persistent_kernel.cuhis compiled byhipccatcompile()time, so every run above removedpermanent_output_dirfirst. Nopip installrebuild is involved.git clang-format --diff).