Fix system hang from DIFR prefetch GPFIFO exhaustion - #1286
Conversation
nvWriteGpEntry() waits for the GPU to consume a GPFIFO entry in a loop whose only exit is nvPushCheckChannelError(), which reports an error only once RM has written 0xFFFF into the channel's error notifier. A channel that stalls without faulting -- one that is simply never serviced -- leaves the notifier clean, so the loop never terminates. Because it is a busy spin with no yield, this hangs the calling kernel thread, and with it the machine. Give the loop a deadline using the same idiom as IdleChannel(), honouring the existing noTimeout opt-out, and yield between polls as the notifier wait already does. On expiry return FALSE, which Kickoff() already handles by leaving putOffset unchanged.
When PrefetchSingleSurface() gives up waiting for the prefetch semaphore it returns FAIL_CE_HW_ERROR, but the GPFIFO entries it already wrote stay queued on a channel the copy engine never drained. Nothing reclaims them, so GET stops advancing and the channel permanently loses one kickoff's worth of its ring. The DIFR channel's pushbuffer is 1024 bytes, giving only 16 GPFIFO entries, so a handful of such faults exhausts it and the next kickoff waits on a ring that can never drain. Reset the channel on that status so a fault costs nothing permanent, and a later prefetch starts with GET and PUT back in sync. Only FAIL_CE_HW_ERROR can leak: the other failure paths return before any kickoff. If the channel cannot be reallocated, remember that and report FAIL_INSUFFICIENT_L2_SIZE from then on, which stops RM requesting further prefetches, rather than leaving a freed channel for the next one to use.
|
Good news, my proposed fix at PR below is working well in my own pc. No more hangs! For those who like to try it, I built this easy script in attached zip file: Simply run these commands: |
|
Independent reproduction on a different GPU generation, compositor and trigger, Configuration
Trigger: display idle, with no suspend anywhere in the pictureYou mention a second-hand report of the non-S3 case in #1205. This is a Four occurrences with an identical signature — 2026-08-06 18:09, 08-12 18:36, StacksHolder, spinning in Waiter — and this is a different entry point from the atomic-modeset path So the deadlock is reachable through plain Two details that support your "stall without faulting" reading:
Re: your first review question (is the yield legal?)
For what it's worth from outside as well, this looks safe in the published
Only the two channels allocated through Re: the timeout value
One data point for whichever you pick. The legitimate worst case on the DIFR and Testing offerI arrived at the same fix for the first hunk independently before finding this
Happy to post it if it would help, and equally happy to run either of your One caution on the sibling loop
|
|
@aritger you're welcome. Glad to hear that the fix is being looked at seriously. @Cartagines thank you for confirming it |
Two related fixes for a deterministic system hang in the DIFR prefetch path. The first stops a stalled channel from taking down the machine; the second stops the channel getting into that state.
Context and the full per-boot data are in #1205.
1.
nvidia-push: bound the wait for a free GPFIFO entrynvWriteGpEntry()waits for a free GPFIFO entry here:No deadline, and the only exit is
nvPushCheckChannelError(), which returns TRUE only once RM has written0xFFFFinto the error notifier. A channel that stalls without faulting — never serviced, no RC event, no notifier write — leaves that check returning FALSE forever. It is also a busy spin with no yield, so the calling kernel thread is lost and the watchdog escalates to a system hang.Every other wait in this file is bounded:
IdleChannel()takes atimeoutMSec, and the notifier wait has short and long timeouts plus annvPushImportYield(). This loop is the exception.The change gives it a deadline using
IdleChannel()'s idiom, honours the existingnoTimeoutopt-out, yields between polls, and returnsFALSEon expiry — a pathKickoff()already handles by leavingputOffsetunchanged. No new helpers or constants.I chose
SHORT_TIMEOUT(3s) overLONG_TIMEOUT(10s) deliberately: the soft-lockup watchdog reports at ~20s, and I wanted this to surface as a logged error rather than a lockup splat. Happy to switch it for consistency with the other waits.2.
nvkms-difr: reset the prefetch channel after a CE faultThis is what makes the channel stall in the first place.
PrefetchSingleSurface()kicks off a copy and waitsDIFR_PREFETCH_WAIT_PERIOD_US(10ms) for the semaphore. On expiry it returnsFAIL_CE_HW_ERROR— but the GPFIFO entries it already wrote stay queued on a channel the CE never drained. Nothing reclaims them, so GET stops advancing and the channel permanently loses one kickoff's worth of ring.That ring is small:
pushBufferSizeInBytes = 1024givesnumGpFifoEntries = 16(nvidia-push-init.c), 2 entries per kickoff, andInitGpFifoExtendedBase()consumes 2 more at alloc on Hopper+ without a matching progress-tracker release. So there is very little headroom, and a handful of faults exhausts it — after which the next kickoff waits on a ring that can never drain, which is fix 1's loop.The change resets the channel on exactly that status, reusing the existing
Free/Allochelpers, so a fault costs nothing permanent and a later prefetch starts with GET and PUT in sync. OnlyFAIL_CE_HW_ERRORcan leak — the other two failure codes return before any kickoff. If reallocation itself fails, that is remembered andFAIL_INSUFFICIENT_L2_SIZEis returned from then on (the code which, per the existing comment, tells RM and PMU to stop requesting prefetches) rather than leaving a freed channel for the next prefetch to dereference.I did not attempt to make DIFR retry within a boot cycle.
subdeviceCtrlCmdLpwrDifrCtrl_IMPLhas no implementation in the open tree, so the "disable until next driver load" decision after a CE fault is yours, not NVKMS's, and I did not want to guess at it. In practice a resume re-enables DIFR anyway, so with this fix the next cycle gets a clean channel and can succeed if the stall was transient.How I hit this
RTX 5070 Ti, Ubuntu 24.04, kernel 6.17.0-1028-oem, driver 595.71.05-open, GNOME Wayland. The machine hard-hangs on exactly the 6th suspend/resume cycle of every boot — 7 boots out of 7, on both
deep(S3) ands2idle, never once surviving a 6th. Always:The fixed count is what pointed at a small ring being consumed one kickoff per resume rather than anything probabilistic.
Worth noting the user-visible consequence of the leak beyond the hang: because a CE fault disables DIFR until the next driver load, and each resume re-enables it for exactly one more doomed attempt, DIFR appears to be silently non-functional on this machine after the first suspend of any boot — while still accumulating the damage that eventually hangs it.
#1205 also has a report of the same deadlock signature triggered by plain display idle with no S3 involved (RTX 4060 Max-Q, 580.173.02), which fits: any repeated prefetch failure fills the ring, and suspend/resume is just a reliable way to produce one.
Testing
Both patches are derived from source analysis and have been compile-tested only. They build clean against 6.17.0-1028-oem with no new warnings. I have not run them on hardware, so I can't claim they resolve the hang — only that fix 1 removes the code path's ability to spin forever, and fix 2 removes the accumulation that leads there.
Two review questions I can't answer from outside:
nvPushImportYield()has precedent innvidia-push.c, but you know better than I do whether everyKickoff()caller is in a context where yielding is legal. Happy to gate or drop the yield and keep only the deadline.DifrPrefetchEventDeferredWork()'s kthread-queue context?nvkms-difr.calready makes RM control calls from timer and kthread callbacks, but full channel reallocation from there is an assumption on my part. If it isn't safe, the reset would need deferring to a worker.Both files are byte-identical between 595.71.05, 595.91.07 and 610.57.04, so nothing released contains a fix. I have a 100% reproducible case in six suspend cycles and am glad to build and test any alternative patch you'd prefer, on either branch.