mirror of
https://github.com/XuehaiPan/nvitop.git
synced 2026-10-23 08:07:34 -06:00
[PR #223] [MERGED] fix(api/libnvml): drain in-flight NVML queries before atexit shutdown #222
Labels
No labels
api
bug
bug
cli / tui
dependencies
documentation
documentation
documentation
duplicate
enhancement
exporter
invalid
pull-request
pynvml
question
question
upstream
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
github-starred/nvitop#222
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/XuehaiPan/nvitop/pull/223
Author: @XuehaiPan
Created: 7/9/2026
Status: ✅ Merged
Merged: 7/10/2026
Merged by: @XuehaiPan
Base:
main← Head:fix/nvml-shutdown-drain📝 Commits (8)
77b78eachore(pre-commit): update pre-commit hooks6888d7efix(api/libnvml): drain in-flight NVML queries before atexit shutdownd611b1ffix(api/libnvml): avoid re-initializing NVML during atexit shutdown051d0cafix(api/libnvml): guard atexit nvmlShutdown() and register the hook once6189dabfix(api/libnvml): cover lazy initialization in the shutdown drain06b5accfix(api/libnvml): reset the shutdown-drain state after os.fork()b0821c6docs(api/libnvml): correct nvmlQuery Raises and the __shutting_down comment5ade547docs(changelog): record the atexit NVML shutdown-drain fix📊 Changes
3 files changed (+162 additions, -44 deletions)
View changed files
📝
.pre-commit-config.yaml(+2 -2)📝
CHANGELOG.md(+1 -1)📝
nvitop/api/libnvml.py(+159 -41)📄 Description
Issue Type
Runtime Environment
3.14.6(reporter) /3.12.3(verification host)580.126.20nvitopversion or commit:fix/nvml-shutdown-drain@b0821c6python-ml-pyversion:13.595.45Description
Pressing q to quit the TUI intermittently crashed with a
SIGSEGVinsidelibnvidia-ml. TheatexitnvmlShutdown()could free NVML internal state while a background thread was still inside alibnvidia-mlcall — a use-after-free. This affects any concurrent NVML user ofnvitop.api, including the TUI snapshot daemons and the reproducer in #222 (which spawns query threads with no TUI involved).The fix makes the implicit
atexitshutdown safe rather than dropping it, by draining in-flight queries before the shutdown. All changes are confined tonvitop/api/libnvml.pyand to nvitop's own additions on top ofpynvml— the re-exportedpynvmlAPI is left untouched.Drain in-flight queries at
atexit:nvmlQuerybails on an unlocked__shutting_downfast path, then reserves an in-flight slot — incrementing__active_queriesunder athreading.Condition— before any NVML-touching work (including lazynvmlInit), and re-checks__shutting_downafter initialization. The NVML call itself runs outside the lock, so concurrent NVML access is never serialized; the slot is released and the condition notified in afinally._atexit_shutdownsets the one-way__shutting_downlatch, then waits on the condition for__active_queriesto reach zero before callingnvmlShutdown(). Once the latch is set no new query starts, so the counter only decreases and the wait wakes on the last decrement (event-driven, not polling). The wait is bounded (120 s) and skipsnvmlShutdown()if queries do not drain in time — the OS reclaims NVML resources at process exit anyway.Reset the drain state across
os.fork():os.fork()copies the module globals but leaves only the calling thread alive in the child, so the child can inherit a non-zero__active_queries(a query owned by a thread that no longer exists) or an already-held internal lock — making the inheritedatexithook block for its full timeout, or deadlock._reset_after_fork, registered viaos.register_at_fork(after_in_child=...), rebuilds the lock/condition and zeroes the drain state in the child (guarded withhasattr(os, 'register_at_fork')since it is unavailable on Windows).The re-exported
pynvmlAPI is unchanged:nvmlShutdown()keeps its original signature, and an explicitnvmlShutdown()— or a rawlibnvml.nvml*call — racing in-flight queries remains the caller's responsibility, since the drain only tracks calls made throughnvmlQuery.The
chore(pre-commit)commit bumps theruffandmypypre-commit hooks.Motivation and Context
Fixes #222.
Root cause: the
atexitnvmlShutdown()races with in-flight NVML calls from background threads still running at interpreter exit; the shutdown frees NVML internal state under a concurrent call. Draining in-flightnvmlQuerycalls before the shutdown closes the race for any concurrent caller — including the no-TUI reproducer in the issue, which a TUI-level fix could not help.Testing
Verified on a Linux host with 4× NVIDIA H100 80 GB GPUs (Python 3.12.3) using the
nvml_race.pyreproducer from #222, run 25 times per build:nvml_race.py× 25main(no fix)SIGSEGV(rc=139)fix/nvml-shutdown-drainmainthe reproducer segfaults on ~60% of runs; on this branch it never crashes across 25 runs.maindoes not coredump.os.fork()reset was verified GPU-free with a realfork(): a child that inherits a non-zero in-flight counter is reset to zero by the registered hook, so itsatexitdrain returns immediately instead of blocking for the timeout.nvmlQueryrefuses new NVML calls once__shutting_downis set, and_atexit_shutdownwaits for an in-flight query before callingnvmlShutdown().pre-commit(ruff, ruff-format, codespell, mypy, pylint) passes. No automated tests are included (the repository has no test harness yet).🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.