ツール · Rust · Ratatui · 2026-10-11 更新
dgxtop:偽のゼロを表示しない GPU ターミナルモニターの設計
dgxtop は NVIDIA DGX Spark、または NVIDIA GPU を搭載した Linux ホストの CPU、GPU、メモリ、ディスク、ネットワーク、GPU プロセスをターミナルで監視します。バージョン 2 は「読み取れない値を 0 と表示しない」という 1 つの原則を軸に全面的に書き直しました。このページでは、その原則を支える構成と、それぞれの設計判断に伴うコストを説明します。
- repository
- github.com/DennySORA/dgxtop(外部サイト)
- stack
- Rust 1.92 · Ratatui · crossterm · NVML · SQLite
- platforms
- Linux x86_64/aarch64(glibc 2.34 以降)· DGX Spark(GB10)と RTX 5080 + RTX 3060 搭載ワークステーションでテスト済み
- license
- Apache-2.0
- status
- v2(2026-10-11 リリース)
- updated
書き直した理由
バージョン 1 は UI ループ内でハードウェアをサンプリングしていたため、ドライバー呼び出しが遅くなると画面全体が固まりました。データ、履歴、UI 状態を 1 つの共有構造体に入れ、thermal zone の名前と走査順で CPU 温度を選び、読み取れない値を 0 と表示していました。v2 の仕様ではこうした問題を 10 項目挙げ、それぞれに設計上の回答を用意しています。
| v1 の問題 | v2 の対応 |
|---|---|
| PID だけでプロセスを識別 | ProcessKey(ホスト、起動、PID namespace、PID、開始時刻)、事前に保持する pidfd、2 段階の確認 |
| 欠損値を 0 に変換 | 読み取り状態、データ種別、品質、鮮度を別々の軸に分離。欠損値は理由付きの null |
| 統合メモリの意味が混在 | システム RAM、GPU framebuffer、プロセスごとの割り当てを区別 |
| 使用率から帯域幅を推定 | 測定値、導出値、推定値を区別。バイトカウンターがなければスループットを出さない |
| 更新間隔と累計量が測定ではなく仮定 | スケジューラーが実際の間隔を確認。実経過時間とカウンターの差分を使用 |
| 履歴の時間窓がずれる | 起動、カバー範囲、欠測区間の情報を持つ時間バケット |
| 複数 GPU 上の同じ PID を重複計上 | ProcessKey ごとにホストの CPU とメモリを 1 回だけサンプリングし、各 GPU に関連付ける |
| インストーラーがダウンロード内容を検証しない | チェックサムとアーカイブ構造を検証し、ロールバック用に以前の実行ファイルを保持 |
| サンプリングが UI をブロック | 制御、サンプリング、描画を分離。ドライバーを個別のプロセスに隔離 |
| ビルドを再現できない | Cargo.lock をコミットし、ツールチェーンを固定してリリースの検証条件を設定 |
実装に先立って、40 項目の要件、15 の作業パッケージ、40 の受け入れケースを仕様にまとめました。その後、固定した core の契約に従い、8 crate の Rust workspace として実装しています。Rust は約 157,000 行、テストは 1,373 件です。
表示できるもの
概要、GPU、履歴、設定の 4 つのタブがあります。以下は実際にテストした 2 台のスクリーンショットです。
6 つの構成要素、2 つの経路
数値はデータ経路を通り、利用者の意図は制御経路を通ります。Manager が唯一の書き込み主体です。Store の書き込み権限とプロセス操作の port を持つのは Manager だけなので、Viewer と Controller はデータを変更することも、シグナルを送ることもできません。
データ経路
- Linux + NVIDIA ドライバー/proc · sysfs · NVML
- Adapter hostそれぞれ独立したプロセス
- procfs
- hwmon
- thermal
- powercap
- NVML
- network
- filesystem
- Collector正規化 · 重複排除 · 仲裁 · レート
- Manager唯一の書き込み主体
- Storeスナップショット · 履歴 · 設定 · 監査
- Manager投影
- Viewer不変の view model · Ratatui
制御経路
- Controllerキー · マウス · CLI → 型付きコマンド
- Manager検証 · 認可
- 設定 · スケジューラー · 操作 adapterシグナルは pidfd 経由
- 監査 + 結果シグナル送信前に永続化
- Store → Viewer結果を表示
| 構成要素 | 担当する責務 | 行ってはいけないこと |
|---|---|---|
| Adapter | 1 種類のドライバーまたはカーネルインターフェースと通信し、ネイティブ値を出所、時刻、エラーとともに返す | 権威値の選択、複数出所の平均、Store への書き込み、描画 |
| Collector | 単位の正規化、同一由来と確認できた別名の重複排除、仲裁、レートの算出 | ドライバー内部の呼び出し、SQL の実行、設定変更、シグナル送信 |
| Manager | サンプリングの計画、方針と設定の管理、コマンドの認可、データのコミット、view model の生成 | 画面のレイアウト、イベントループ内でブロッキングするドライバー呼び出しを待つこと |
| Store | スナップショット、履歴、設定、監査ログの保存 | センサーの選択、コマンド実行 |
| Viewer | 表示モジュールの配置と、不変の view model の描画 | サンプリング、Store の読み書き、レート計算、操作の認可 |
| Controller | キー、クリック、フラグを安定した ID を持つ型付きコマンドに変換 | 出所の決定、シグナル送信、描画 |
10 個の表示モジュールはコンパイル時に登録され、自分の view model と画面領域だけを受け取ります。あるモジュールが panic してもモジュールエラーとして隔離され、サンプリングまで巻き込んで停止させません。
ドライバーが止まっても画面は動く
dgxtop 親プロセス
- ターミナル · 入力と描画、最大 4 fps
- Manager · 制御処理の時間枠 2 ms
- supervisor · パイプと回収
- collector worker × 2
- hot store · 単一ライター
- 履歴クエリ × 2
- 設定 · 監査 · アーカイブ · ログ
- プロセス操作 worker
adapter host · それぞれ独立したプロセス
- procfs最新
- hwmon最新
- thermal最新
- powercap最新
- NVMLタイムアウト
- network最新
- filesystem最新
↔ 匿名パイプ · 長さ付き JSON · 1 フレーム ≤ 1 MiB
失敗が続く host
- 縮退
- サーキット開放
- バックオフ 1 s · 2 s · 4 s … 60 s
- 試行
- ウォームアップ
回収不可 → 隔離 · host は最大 8 個
タイムアウトは結果が遅いことを示すだけで、呼び出しを取り消しません。ベンダーのライブラリ内で停止したスレッドを安全に止めることはできないため、各 live adapter は専用の常駐子プロセスで動きます。同じ実行ファイルを adapter host モードで起動し、長さを先頭に付けた最大 1 MiB の JSON フレームを匿名パイプで親プロセスと交換します。
期限を超えた host は縮退状態になります。3 回連続で失敗するとサーキットブレーカーが開き、再試行は 1、2、4…60 秒の間隔でバックオフします。host の交換は回収(reap)が完了してから行い、回収できない host は隔離します。同時に存在する数は最大 8 個です。
親プロセスは固定数のスレッドを使い、async runtime は使いません。ネットワーク I/O がなく、spawn_blockingを使っても、取り消せない同じ呼び出しが future の背後に隠れるだけだからです。管理対象の全データで 256 MiB の予算を共有し、各キューには件数とバイト数の両方の上限を設けます。有界 channel が数えるのはメッセージ数で、バイト数ではありません。
平均せず、出所を説明する
Linux は同じセンサーを複数のインターフェースで公開することがあります。ラベルだけでは何を測るかも確定せず、temp1 が CPU とは限りません。dgxtop はまず、ホスト、起動、エンティティ、指標、範囲で構成する metric key により読み取り値の意味を定め、同じ key の値だけを比較します。
- 意味を対応付ける
- 単位を正規化
- 妥当性検証
- 鮮度を確認
- 同一由来を重複排除
- 候補を順位付け
- 競合を検出
- ヒステリシス · 3 回連勝 · 5 秒
- コミットと説明
起こり得る結果
- 採用 · 68 °C · hwmon
- 出所の競合 · A 68 °C/B 42 °C
- 非対応
- 読み取り失敗
- 期限切れ
55 °C· 平均にはしない
単一の信頼度スコアにせず、4 つの独立した軸で表す
- 読み取り状態
- データ種別
- 品質
- 鮮度
| 状況 | dgxtop の処理 | 画面の表示 |
|---|---|---|
| B が SoC の zone | 異なる指標なので比較しない | CPU package 68 °C、SoC 42 °C |
| B が A と同一であると確認済みの別名 | 同じセンサーへの 2 つの入口として 1 つに数える | 1 つの値と、不一致に関する注記 |
| A と B が独立し、同等で、条件もそろった測定 | 26 °C の差が許容範囲を超えるため競合とし、値は null | 出所の競合と両方の値を表示。55 °C にはしない |
| B が fault を報告 | B を除外して A を採用 | A の 68 °C を表示し、B の故障を明記 |
別の出所への切り替えには、3 回連続での優位と最低 5 秒の経過を必要とし、値がセンサー間を行き来するのを防ぎます。読み取り状態、データ種別、品質、鮮度は単一の信頼度スコアにまとめず、4 つの独立した軸として保ちます。
正確な履歴
レートは 2 つの u64 カウンターの差分を、CLOCK_BOOTTIMEによる実経過時間で割った値です。この時計はサスペンド中も進みます。カウンターのリセット、再起動、出所の変更があれば新しい区間を開始し、負のレートや架空のレートを作りません。
1 分単位の集計では、その区間のどの瞬間にバイトが流れたかは分かりません。そのため総量には時間窓の内側に完全に収まる区間だけを含めます。分の境界をまたぐ区間は正確な bridge として保持し、1 回だけ数え、比例配分はしません。
ゲージ値は時間で重み付けし、出所の鮮度期限までだけ有効とします。欠測区間は欠測のまま残します。履歴はhistory.sqlite3 に 7 日間、最大 1 GiB 保存するため、以前の実行時の記録も表示できます。アーカイブへの書き込みはsynchronous=NORMAL、操作監査は FULLを使います。グラフの最後の 1 秒が失われることは許容しても、シグナル送信の記録が失われることは許容しません。
安全策を備えたプロセス終了
- KGPU プロセスを選択
- 準備同じ利用者 · root 以外 · PID 1、dgxtop、その host を除く
- pidfd_open/proc の識別情報を確認
- 確認10 秒間有効な一度限りのトークン · y
- intent を監査記録SQLite synchronous=FULL
- pidfd_send_signalSIGTERM
シグナル送信済み
- 終了を観察済み
- 実行継続中
intent 記録後にクラッシュ → 不明、再送しない
SIGKILL は既定で無効(allow_force)
監視ツールにとって、誤ったプロセスを終了することは最悪の失敗です。そのため終了操作はProcessKey(ホスト、起動、PID namespace、PID、開始時刻)を対象に、監査付きの 2 段階で行います。行番号や PID だけを対象にはしません。
準備段階で利用者を確認し、root セッション、PID 1、dgxtop 自身とその host を拒否します。pidfd を開いてから /procで識別情報を再確認します。確認には 10 秒間有効な一度限りのトークンを使います。シグナル送信前にsynchronous=FULLで intent を書き込み、準備段階から保持している pidfd 経由で送信するため、再利用された PID を誤って終了させません。
結果はシグナル送信済みと終了を観察済みを分けて示します。intent の永続化後、結果の記録前にクラッシュした場合は不明と記録し、再送しません。データベースのコミットとシステムコールに共通のトランザクションはないため、保証できるのは「高々 1 回」です。SIGKILL は actions.allow_forceを設定しない限り無効で、--read-only はすべての操作を無効にします。
資源予算と過負荷
dgxtop が管理するすべての割り当ては、用途別に区分した共通の 256 MiB の予算から差し引きます。これは割り当てを許可する上限で、メモリの実測値ではありません。アロケーターのオーバーヘッド、スレッドのスタック、NVIDIA ライブラリは別途加わるため、仕様ではプロセスツリー全体に 384 MiB の目標を別に設けています。
| 区分 | MiB | 内容 |
|---|---|---|
| カタログとメタデータ | 24 | 名前、tombstone、仲裁の状態 |
| 未加工の候補 | 32 | 出所診断用の 60 秒分の証拠 |
| 現在の状態 | 16 | 512 系列、256 プロセス、1,024 件の GPU 関連付け |
| 元の解像度の履歴 | 32 | 直近 300 秒 |
| 分単位の集計 | 96 | 24 時間の集計、出所の変更、欠測区間 |
| 入力(ingress) | 8 | 稼働中のすべての IPC とデコード用バッファ |
| 表示 | 12 | view model と整形キャッシュ |
| 履歴クエリ | 16 | 実行中のクエリが固定するスナップショット |
| 制御と予備 | 20 | コマンド、アーカイブ journal、ログ、安全用の予備領域 |
- 正常通常のレート
- 圧迫任意の作業を停止 · 2 fps
- 抑制クエリを制限 · 周期を延長
- 回復中30 秒ごとに 1 段階戻す
過負荷時には、dgxtop は精度を黙って落とすのではなく、縮退状態を明示します。圧迫状態では任意の作業を止め、描画を最大 2 fps にします。抑制状態ではクエリを制限し、サンプリング周期を延ばし、バナーに設定値と実際の値を併記します。回復時は正常な状態が 30 秒続くごとに 1 段階ずつ戻し、失われたサンプルを補間しません。
境界と検証
- dgxtop-runtimehost · IPC · worker→ dgxtop-core だけに依存
- dgxtop-adaptersprocfs · NVML · pidfd→ dgxtop-core だけに依存
- dgxtop-collectors仲裁 · レート→ dgxtop-core だけに依存
- dgxtop-store履歴 · SQLite→ dgxtop-core だけに依存
- dgxtop-managerユースケース · セッション→ dgxtop-core だけに依存
- dgxtop-uiRatatui · keymap→ dgxtop-core だけに依存
CI で検査 · check_boundaries.py
workspace は 8 crate で構成されます。各実装 crate が依存するのはdgxtop-core だけで、すべてを参照できるのは組み立ての起点だけです。CI スクリプトが cargo metadata を読み、実装 crate が別の実装 crate に依存するとビルドを失敗させます。core 自体は NVML、libc、SQLite、UI に依存しません。
フォーマット、lint、1,373 件のテスト(aarch64 では 1,372 件)、依存関係の境界、実機テストは、RTX 5080 と RTX 3060 を搭載する Debian 13 ワークステーションと DGX Spark の両方で合格しています。受け入れ報告は 40 ケースを追跡しており、14 件が合格、24 件が未実行(多くは明示的な起動を要するエンドツーエンドテスト)、2 件が 30 分のベンチマーク 3 回と 72 時間の連続稼働テストを待っています。これらがそろうまでは、どのプラットフォームも受け入れ検証済みとはせず、性能値も目標値のままです。
設計上のトレードオフ
- adapter ごとにスレッドではなくプロセスを使う。常駐メモリと IPC のコストは増えますが、ドライバーが停止しても他の処理を固めません。
- async runtime ではなく、固定スレッドと crossbeam channel を使う。導入する理由となるネットワーク I/O がないためです。
- プラグインではなく、コンパイル時に登録する。adapter と表示モジュールのために不安定な Rust ABI を使う必要がなく、新たな攻撃面も増やしません。
- 平均を取らず、競合を表示する。画面は多少整わなくても、どのセンサーも報告していない数値は出しません。
- 再試行せず、シグナルは高々 1 回にする。結果が不明なら、不明と表示します。
- 変化したときだけ、最大 4 fps で再描画する。監視に 60 fps は不要です。遅いターミナルでも入力をブロックしてはいけません。







