Repository navigation
fix(vk): a shader preparation the GPU driver stops answering in goes on or ends, and says why (#2107) - #2123
nullifyshot wants to merge 3 commits into
Conversation
…on or ends, and says why (NotRexed#2107) Builds on Radeon and GeForce RTX 40 cards stopped for good while preparing shaders, the Builder's details showing "vertex format NVN 0xe -> vk 17". The driver had faulted inside a pipeline compile; the fault guard caught it and left the driver holding a lock of its own, so every other worker waited on it for ever. - predict_run (new at build time since NotRexed#2032) builds its first pipeline alone, as precompile_run does for NVIDIA's fault at many first pipelines at once, and goes on without the pipelines the driver never finishes once nothing has finished for 30 s (RX_VK_PREDICT_STALL_S). Its count moves per pipeline. - The --prepare-shaders run has a watchdog for every part: after 30 s with no other line it logs what it is working on, or what it is waiting on (the GPU driver, or the disk) and since when, and any driver fault it caught. When nothing has moved for 5 minutes (RX_VK_PREPARE_STALL_S) it logs why, leaves hookshot_prepare_skipped so the game's starts don't stop in the same place, and ends with exit status 4 (TerminateProcess, not exit(), which would unload the stuck driver). The pipeline cache's save is reported, never stopped. - make_game.py says "the GPU driver stopped responding" for exit status 4. - tests/test_prepare_stall.py compiles predict_run and the watchdog from the source with a stand-in driver: a stuck pipeline, a slow one, a fault, the save. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t one vendor or series Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101
left a comment
There was a problem hiding this comment.
Thanks for picking this up. The status lines alone are a real improvement for #2107: players can't tell "slow" from "stuck" right now. Moving the count per pipeline and building the first pipeline alone in predict_run both look right to me. Keeping the stuck workers' state on the heap and detaching them follows precompile_run's pattern and is safe as written: they only touch Pre, never the stack. Exit status 4 and its make_game message line up, and RUN_ALL_ARGS is declared.
Some things I'd like sorted before this goes in:
-
CI is red on Windows because of the new test.
test_prepare_stall.pyfails in part 2/2. clang++ targeting MSVC turnsgetenv(andfopenin the watchdog piece too) into-Wdeprecated-declarationserrors under-Werror. Add-D_CRT_SECURE_NO_WARNINGSon Windows, as the other compiled tests do. This is the PR's own test, not one of main's known failures. -
The save step can now hang forever (inline). Part 3 is never stopped, because "every pipeline is made". That's no longer true once
predict_run(orprecompile_run) has moved on and left a worker insidevkCreateGraphicsPipelines. If that worker holds the driver's lock or the cache's lock,vkGetPipelineCacheDatainpipeline_cache_save(true)waits on it. Worse, the watchdog then logs "still writing ... this is the disk" every 30 s. That output keeps make_game'sPREPARE_SILENCEfrom ever firing, so the 15-minute backstop this PR replaces is gone exactly in the #2107 case. The same goes for the plainreturn 0→exit()after@@done, with a detached thread still in the driver (the unload hazard the description itself cites forexit()). Some ideas: let part 3 stop too when a part moved on (or a fault was counted), or after a longer cap. Stop the status lines after N minutes so the silence timeout still applies. And end the run with_Exit/TerminateProcessafter the save when workers were left behind. -
30 s for
predict_runmay catch slow drivers that still work (inline).precompile_run's guard only fires once everything has been handed out (next >= total), so it only ever drops the last stragglers. This one fires whenever nothing finishes for 30 s, wherever the run is, and gives up on everything left (tens of thousands of pipelines, which are then built in play). It also runs at the game's own start (the start screen and console paths), where there's no watchdog. In #2107, oniryuuko describes a run that was "working, just very very slow", with the progress sometimes stopping "for up to 30 seconds as it hangs on a particular pipeline". That's right at this limit, and right after the first pipeline, which the others now wait for. I'd only fire early oncedriver_faults()has counted something (the case the log shows). Otherwise use the tail-only rule, or a much longer limit, and leave the real hang to the watchdog in--prepare-shaders. -
Overlap with #1823 (approved, open). Both change the
__except/sigsetjmpblocks ofguarded_graphics/guarded_compute,test_linux_driver_fault_guard.pyandtest_precompile_compute_guard.py, so whichever lands second needs a rebase. Two semantic points as well. #1823 makes the graphics guard return-77like compute and adds*pl = VK_NULL_HANDLE, and its gate serializes a program's builds on AMD so that "a build behind a faulted one" doesn't hang (issue 996). That sounds a lot like the fault-then-stuck theory here, and the #2107 reporter is on an RX 580. Please say how the two fit together, e.g. whether the fault counter should also count #1823'skDriverFaultpath and whether its quarantine would already have kept this run from hanging.
Smaller:
- Nit: a worker that's been abandoned keeps building the rest of its current program's list.
abandonedis only checked at the top of the loop, so those pipelines can run alongsideprecompile_runand the save. A check inside the per-pipeline loop would stop it. - Nit:
prepare_movedupdates part/done/total as three separate atomics, so the watchdog can read a mixed snapshot. That's harmless for a log line, but worth knowing if it ever decides anything. - The description's root cause comes from one log, and nobody has reproduced it on an affected card yet ("Not tested"). Other reports in #2107 read like slowness rather than a hang. That's fine for a diagnostic change, but the 30 s default (point 3) makes it change behaviour too.
The watchdog/diagnostic part is close. Points 1–3 are what I'd want fixed first.
| Sleep(100); | ||
| const auto now = std::chrono::steady_clock::now(); | ||
| if (done.load() != last_done) last_done = done.load(), last_change = now; | ||
| else if (stall_s > 0 && now - last_change > std::chrono::seconds(stall_s)) { |
There was a problem hiding this comment.
Unlike precompile_run's guard (next.load() >= total &&), this fires anywhere in the run and gives up on all that's left, here and at the game's own start (no watchdog there). With the first pipeline built alone, a single 30 s compile (which #2107 reports describe on slow-but-working runs) is enough. I'd fire early only once driver_faults() > 0, and otherwise keep the tail-only rule or a much longer limit.
There was a problem hiding this comment.
Done in c69b5a6. It goes on from anywhere in the run only if driver_faults() rose during this run. Otherwise it uses the tail-only rule, as precompile_run does (next >= keys.size()). test_prepare_stall.py checks that a hang early in the run with no fault is waited for.
| prepare_moved(2, 0, 0); | ||
| bar.reach(2); | ||
| precompile_run(progress); | ||
| prepare_moved(3, 0, 0); // (the pipeline cache written out: the watchdog reports it, never stops it) |
There was a problem hiding this comment.
"Every pipeline is made" isn't true when predict/precompile moved on and left a worker inside vkCreate*Pipelines. vkGetPipelineCacheData on the same cache can then wait on it forever. Because part 3 keeps logging "still writing", make_game's 15 min PREPARE_SILENCE never fires either. I think the save needs a cap, or a stop, when workers were left behind.
There was a problem hiding this comment.
Agreed. predict_run and precompile_run now call prepare_left_workers_note() when they go on. When that's set, or a fault was counted, the watchdog stops the save the same way it stops the other parts.
| std::_Exit(4); | ||
| } | ||
| if (now - last_log_ms.load(std::memory_order_relaxed) < quiet_s * 1000) continue; | ||
| if (part >= 3) |
There was a problem hiding this comment.
These lines reset make_game's silence timer, so if the save (or the exit() after @@done) hangs, the build waits forever. Maybe stop emitting after a few minutes in part 3, or end the run here too when driver_faults() or a moved-on part says the driver may be stuck.
There was a problem hiding this comment.
Both are handled in c69b5a6. In a clean run, the save's status lines stop after 300 s, so PREPARE_SILENCE applies again. In a risky run (workers left, or a fault), the save is stopped at 300 s, and a run that gets through ends with TerminateProcess (prepare_end), not exit().
| src = Path(tmp) / (name + ".cpp") | ||
| exe = Path(tmp) / (name + (".exe" if os.name == "nt" else "")) | ||
| src.write_text(code, encoding="utf-8") | ||
| cmd = [cxx, "-std=c++20", "-O1", "-Wall", "-Wextra", "-Werror", "-Wno-unused-function", str(src), "-o", str(exe)] |
There was a problem hiding this comment.
This is what fails on windows-2022 (part 2/2): getenv is deprecated under MSVC's CRT, and with -Werror that's an error (fopen in the watchdog piece would hit the same). Add -D_CRT_SECURE_NO_WARNINGS when os.name == "nt".
There was a problem hiding this comment.
Fixed: -D_CRT_SECURE_NO_WARNINGS is added to the compile command, as in the other compiled checks.
…; the save stopped when workers were left - tests/test_prepare_stall.py: -D_CRT_SECURE_NO_WARNINGS, as the other compiled checks (CI's MSVC-target clang calls getenv and fopen deprecated, an error under -Werror). - predict_run goes on anywhere in the run only once the driver faulted in it; otherwise only once every program was handed out, as precompile_run's guard. A slow driver that still works (up to 30 s on one pipeline, NotRexed#2107), here or at the game's own start, keeps its pipelines; a real hang before then is the watchdog's. - predict_run and precompile_run say when they left workers in the driver (prepare_left_workers). Then, or after a driver fault, the --prepare-shaders watchdog stops the pipeline cache's save as it does the parts, and a run that gets through ends with TerminateProcess (prepare_end), not exit(). Otherwise the save is reported, and its lines end after 300 s so make_game.py's 15 minutes with no line still apply. - An abandoned predict_run worker builds no more of its program's list. - The watchdog reads the part and its count under one lock. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the careful review, hckrman101. All of points 1–3 and both nits are in c69b5a6.
Nits:
On the evidence: you're right that the root cause comes from one log, and that other reports read as slowness. With point 3 changed, the only behaviour change without a counted fault is the watchdog's 5-minute stop in Locally: |
|
Closing in favour of #2195. #2186 landed on main in the meantime. It has the preparation's pipeline steps log their progress every 10 s, which covers the "is it slow or stuck?" half of this PR and conflicted with it. #2195 is this work rebased onto #2186, with two changes on top:
Everything else carries over unchanged, including the changes from hckrman101's review here:
A new PR keeps this one's review and history readable as they were, instead of force-pushing over them. |
…on or ends, and says why (#2107) (#2195) Since v2.1.0, some builds stop for good while preparing shaders (#2107). #2186 made the slow-but-working case visible: the pipeline steps now log their progress every 10 s. A run where the GPU driver itself stops answering still waits forever, though. This makes that run go on, or end with a reason, so the game's first start doesn't hang in the same place. This replaces #2123. It's the same work, rebased onto #2186, with hckrman101's review there addressed, and with the status lines #2186 now covers taken out. ### What happens One player's log (an RX 580) shows a likely cause: ``` vk: the driver faulted compiling a graphics pipeline (exception 0xc0000005): that one is left out vk: graphics pipeline for 6ee656c480d6beea recovered with optimization disabled ``` - `guarded_graphics` catches the driver's crash by jumping out of the compile, and that leaves the compile unfinished inside the driver. #1823's process dump of issue 996 shows what follows on AMD: later builds of that program wait inside the driver forever. - The step it stops in is `predict_run`, which runs at build time since #2032. Unlike `precompile_run`, it had no stall guard. - It also started all its workers' first pipelines at once. `precompile_run` already avoids that, because of a known NVIDIA fault (621.14, RTX 4070 Ti Super). - Most reports in #2107 were slow rather than stuck, which #2186 now shows. This PR changes nothing for those (no fault, still moving) beyond the lines below. ### Changes **`predict_run`** (`gpu/vk/vk_draw.cpp`) - Builds its first pipeline alone, as `precompile_run` does. - If no pipeline finishes for 30 s (`RX_VK_PREDICT_STALL_S`, 0 waits for all), it goes on and leaves the stuck workers in the driver. Their state lives on the heap, as in `precompile_run`. This happens: - **anywhere in the run**, only after a driver fault was caught in it (`driver_faults()`); - **otherwise only once every program has been handed out**, as `precompile_run`'s guard does. So only the last stragglers are given up, never the bulk. A slow driver that still works keeps its pipelines, at the game's own start too. A real hang before then is the watchdog's to end. - An abandoned worker builds no more of its program's list. - The count moves per pipeline, not per program, so a slow program doesn't look stuck. #2186's progress line reads the same count. - `predict_run` and `precompile_run` record when they leave workers in the driver (`prepare_left_workers()`). **A watchdog for the whole `--prepare-shaders` run** (`gpu/vk/vk_main.cpp`), covering every part on every GPU: - **What it's waiting on.** Every 30 s that nothing has moved, it logs the part, its count, and what it's waiting on: "the GPU driver or the disk", "the GPU driver, which faulted (above)", or "the GPU driver, which still has pipelines it did not finish". It logs this even while #2186's 10-second lines keep the log busy, because those repeat the same count during a hang. A driver fault it caught gets a line of its own as soon as it happens. - **The shader programs' count.** That step logs no count of its own, so the watchdog writes one after 30 s of a quiet log. It writes nothing of its own while the pipeline steps are moving, since #2186's lines cover them. - **Stops when nothing moves for 5 minutes** (`RX_VK_PREPARE_STALL_S`, 0 never stops). It then: - logs `STOPPED: ... the GPU driver stopped responding` - leaves `hookshot_prepare_skipped`, so the game's own starts don't hang in the same place (what wasn't prepared is built when first drawn) - ends with exit status 4 through `TerminateProcess`, not `exit()`, which would unload a driver that can still hold workers The 5-minute limit is long on purpose: reading the caches and deciding the predictions report no count for a while on a slow disk or CPU. - **The pipeline cache's save, the last step:** - **If workers were left in the driver, or a fault was caught:** the save is stopped the same way, since `vkGetPipelineCacheData` can wait on them. A run that gets through it ends at once with `TerminateProcess` (after `@@done shaders`). - **Otherwise:** the save is only reported, and its lines stop after 300 s, so make_game's `PREPARE_SILENCE` (15 minutes with no line) still ends a save that never finishes. - The part, its count and the time are read under one lock. **Elsewhere** - `make_game.py` maps exit status 4 to "Left to the game's first start: the GPU driver stopped responding". Before, players waited out `PREPARE_SILENCE`. #2155 then cleans up the leftover process, if any, before an update. - `driver_faults()`: the fault guards count what they catch. These are the same four guard blocks #1823 changes, so whichever lands second needs a mechanical rebase. After #1823, the count would follow its `kDriverFault` path. ### Tested - `tests/test_prepare_stall.py` (new) compiles `predict_run` and the watchdog from the source with a stand-in driver. All 14 cases pass: - **`predict_run`:** - every pipeline built, with the first one alone - the last pipelines never finish: the run goes on - a slow driver that still finishes pipelines, with each program longer than the limit: no stop - a fault, then nothing finishes early in the run: the run goes on - the same with no fault: waited for - **The watchdog:** - stuck, after a fault, and while the shader programs move - stuck and moving while other lines come every 200 ms, as #2186's do: it still says what it's waiting on and stops, and says nothing of its own while things move - a long clean save: reported, then silent, never stopped - a save with workers left in the driver: stopped - a run ending through `prepare_end` - in each case, the lines it logs, the exit status and the skip file - **`make_game.py`:** its message for exit status 4 - `vk_main.cpp` and `vk_draw.cpp` compile with the Windows toolchain's flags (llvm-mingw, `-fms-extensions`). - Updated fixtures for the new calls: `test_prepare_cache_save`, `test_precompile_compute_guard`, `test_linux_driver_fault_guard`. - `python -I -B tests/run_all.py` on Windows: 341 passed, 3 failed. `test_recorder_sync`, `test_fpx_sticky` and `test_precompile_compute_guard` fail the same way on an untouched copy of main on this PC (the last needs MSVC's libraries). The Linux-only checks, `test_linux_driver_fault_guard` among them, are left to CI. - **Not tested:** a real hang on one of the affected cards. I couldn't reproduce it here (RTX 5080). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: breakthetargets <81792782+breakthetargets@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Since v2.1.0, builds can stop for good while preparing shaders (#2107). The reports come from players on a range of graphics cards, AMD and NVIDIA alike, so this isn't one vendor's or one series' bug. The Builder's details freeze on
vertex format NVN 0xe -> vk 17and nothing says why. This makes the preparation go on, or end with a reason, and keeps the log saying what it's doing.What happens
One player's log (an RX 580) shows a likely cause:
guarded_graphicscatches the driver's crash by jumping out of the compile, and that leaves the compile unfinished inside the driver. fix(vk): keep programs the driver faulted on out of the driver (#996) #1823's process dump of issue 996 shows what follows on AMD: later builds of that program wait inside the driver for ever.predict_run, which runs at build time since fix(vk): build every predicted pipeline before the game (the prediction found nothing on fresh builds; no 90 s budget; one progress bar) #2032 (before that, new builds predicted 0 pipelines). Unlikeprecompile_run, it had no stall guard.precompile_runalready avoids that, because of a known NVIDIA fault (621.14, RTX 4070 Ti Super).Changes
predict_run(gpu/vk/vk_draw.cpp)precompile_rundoes.RX_VK_PREDICT_STALL_S, 0 waits for all), it goes on and leaves the stuck workers in the driver, with their state on the heap as inprecompile_run. This happens:driver_faults());precompile_run's guard does, so only the last stragglers are given up, never the bulk. A slow driver that still works keeps its pipelines, at the game's own start too. A real hang before then is the watchdog's to end.predict_runandprecompile_runrecord when they leave workers in the driver (prepare_left_workers()).A watchdog for the whole
--prepare-shadersrun (gpu/vk/vk_main.cpp), covering every part on every GPU:Live status in the log. After 30 s with no other line, it writes what it's working on with its count, or, if the count hasn't moved, what it's waiting on and for how long. That's "the GPU driver or the disk", "the GPU driver, which faulted (above)", or "the GPU driver, which still has pipelines it did not finish". A driver fault it caught gets a line of its own as soon as it happens.
Stops when nothing moves for 5 minutes (
RX_VK_PREPARE_STALL_S, 0 never stops). It then:STOPPED: ... the GPU driver stopped respondinghookshot_prepare_skipped, so the game's own starts don't hang in the same place (what wasn't prepared is built when first drawn)TerminateProcess, notexit(), which would unload the stuck driver and could wait on itGoing on to the next part wouldn't help once the driver is stuck, because every part needs it. The 5-minute limit is long on purpose: reading the caches and deciding the predictions report no count for a while on a slow disk or CPU.
The pipeline cache's save, the last step:
vkGetPipelineCacheDatacan wait on them. A run that gets through ends at once withTerminateProcess(after@@done shaders), notexit().PREPARE_SILENCE(15 minutes with no line) still ends a save that never finishes.The part, its count and the time are read under one lock.
Elsewhere
make_game.pymaps exit status 4 to "Left to the game's first start: the GPU driver stopped responding". Before, players waited forPREPARE_SILENCE, 15 minutes, and the game's first start could then stop in the same place.driver_faults(): the fault guards count what they catch. These are the same four guard blocks fix(vk): keep programs the driver faulted on out of the driver (#996) #1823 changes, so whichever lands second needs a mechanical rebase. After fix(vk): keep programs the driver faulted on out of the driver (#996) #1823, the count would follow itskDriverFaultpath.Tested
tests/test_prepare_stall.py(new) compilespredict_runand the watchdog from the source with a stand-in driver. All 12 cases pass:predict_run:prepare_endmake_game.py: its message for exit status 4vk_main.cppandvk_draw.cppcompile with the Windows toolchain's flags (llvm-mingw,-fms-extensions), and the new functions link between them.test_prepare_cache_save,test_precompile_compute_guard,test_linux_driver_fault_guard.python -I -B tests/run_all.pyon Windows: 341 passed, 3 failed.test_recorder_sync,test_fpx_stickyandtest_precompile_compute_guardfail the same way on an untouched copy of main on this PC (the last needs MSVC's libraries). The Linux-only checks,test_linux_driver_fault_guardamong them, are left to CI.🤖 Generated with Claude Code