ツール · Rust · Ratatui · 2026-10-11 更新

dgxtop:偽のゼロを表示しない GPU ターミナルモニターの設計

dgxtop は NVIDIA DGX Spark、または NVIDIA GPU を搭載した Linux ホストの CPU、GPU、メモリ、ディスク、ネットワーク、GPU プロセスをターミナルで監視します。バージョン 2 は「読み取れない値を 0 と表示しない」という 1 つの原則を軸に全面的に書き直しました。このページでは、その原則を支える構成と、それぞれの設計判断に伴うコストを説明します。

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
dgx-spark-overview.webp1909×961
DGX Spark 上の dgxtop 概要画面。CPU とメモリ、使用率 92% の GB10 GPU と RAM のバー、ディスクとネットワークの表、GPU メモリを 69.8 GiB 使用する Python プロセス 1 つを表示
DGX Spark の概要画面。Python のジョブが GB10 を使用率 92% に保っています。CPU とメモリを共有するため、GPU プロセスが使用する RAM はシステムメモリの内側に別の色で描かれます。

書き直した理由

バージョン 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 はデータを変更することも、シグナルを送ることもできません。

構成:6 つの要素、2 つの経路左端の実線がデータ経路、破線が制御経路です。Viewer は Store を読まず、Manager が公開する view model を描画します。

データ経路

  1. Linux + NVIDIA ドライバー/proc · sysfs · NVML
  2. Adapter hostそれぞれ独立したプロセス
    • procfs
    • hwmon
    • thermal
    • powercap
    • NVML
    • network
    • filesystem
  3. Collector正規化 · 重複排除 · 仲裁 · レート
  4. Manager唯一の書き込み主体
  5. Storeスナップショット · 履歴 · 設定 · 監査
  6. Manager投影
  7. Viewer不変の view model · Ratatui

制御経路

  1. Controllerキー · マウス · CLI → 型付きコマンド
  2. Manager検証 · 認可
  3. 設定 · スケジューラー · 操作 adapterシグナルは pidfd 経由
  4. 監査 + 結果シグナル送信前に永続化
  5. Store → Viewer結果を表示
構成要素担当する責務行ってはいけないこと
Adapter1 種類のドライバーまたはカーネルインターフェースと通信し、ネイティブ値を出所、時刻、エラーとともに返す権威値の選択、複数出所の平均、Store への書き込み、描画
Collector単位の正規化、同一由来と確認できた別名の重複排除、仲裁、レートの算出ドライバー内部の呼び出し、SQL の実行、設定変更、シグナル送信
Managerサンプリングの計画、方針と設定の管理、コマンドの認可、データのコミット、view model の生成画面のレイアウト、イベントループ内でブロッキングするドライバー呼び出しを待つこと
Storeスナップショット、履歴、設定、監査ログの保存センサーの選択、コマンド実行
Viewer表示モジュールの配置と、不変の view model の描画サンプリング、Store の読み書き、レート計算、操作の認可
Controllerキー、クリック、フラグを安定した ID を持つ型付きコマンドに変換出所の決定、シグナル送信、描画

10 個の表示モジュールはコンパイル時に登録され、自分の view model と画面領域だけを受け取ります。あるモジュールが panic してもモジュールエラーとして隔離され、サンプリングまで巻き込んで停止させません。

ドライバーが止まっても画面は動く

実行モデル:1 つの親プロセスと常駐 hostここでは NVML host がタイムアウトしています。CPU、メモリ、画面は他の host により更新を続けます。

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. 縮退
  2. サーキット開放
  3. バックオフ 1 s · 2 s · 4 s … 60 s
  4. 試行
  5. ウォームアップ

回収不可 → 隔離 · 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 の値だけを比較します。

仲裁:metric key ごとの 9 段階各候補、採用または除外した理由、経過時間、出所を、コミットする値とともに保存します。
A · hwmon · package 068 °C
B · thermal zone42 °C
  1. 意味を対応付ける
  2. 単位を正規化
  3. 妥当性検証
  4. 鮮度を確認
  5. 同一由来を重複排除
  6. 候補を順位付け
  7. 競合を検出
  8. ヒステリシス · 3 回連勝 · 5 秒
  9. コミットと説明

起こり得る結果

  • 採用 · 68 °C · hwmon
  • 出所の競合 · A 68 °C/B 42 °C
  • 非対応
  • 読み取り失敗
  • 期限切れ
  • 55 °C · 平均にはしない

単一の信頼度スコアにせず、4 つの独立した軸で表す

  • 読み取り状態
  • データ種別
  • 品質
  • 鮮度
合成例。A:hwmon package 0 が 68 °C を報告。B:ある thermal zone が 42 °C を報告。
状況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 つの独立した軸として保ちます。

正確な履歴

履歴:カウンター、bridge、欠測区間分の境界をまたぐ bridge は、その全体を含む時間窓で 1 回だけ数えます。欠測区間は 0 にせず、空白として描画します。
履歴:カウンター、bridge、欠測区間. 分の境界をまたぐ bridge は、その全体を含む時間窓で 1 回だけ数えます。欠測区間は 0 にせず、空白として描画します。12:00–12:0112:01–12:0212:02–12:03欠測区間bridgeΔ カウンター ÷ Δt点はサンプル、各線分は正確な 1 つの区間

レートは 2 つの u64 カウンターの差分を、CLOCK_BOOTTIMEによる実経過時間で割った値です。この時計はサスペンド中も進みます。カウンターのリセット、再起動、出所の変更があれば新しい区間を開始し、負のレートや架空のレートを作りません。

1 分単位の集計では、その区間のどの瞬間にバイトが流れたかは分かりません。そのため総量には時間窓の内側に完全に収まる区間だけを含めます。分の境界をまたぐ区間は正確な bridge として保持し、1 回だけ数え、比例配分はしません。

ゲージ値は時間で重み付けし、出所の鮮度期限までだけ有効とします。欠測区間は欠測のまま残します。履歴はhistory.sqlite3 に 7 日間、最大 1 GiB 保存するため、以前の実行時の記録も表示できます。アーカイブへの書き込みはsynchronous=NORMAL、操作監査は FULLを使います。グラフの最後の 1 秒が失われることは許容しても、シグナル送信の記録が失われることは許容しません。

安全策を備えたプロセス終了

プロセス終了:SIGTERM は高々 1 回どの拒否条件でもシグナルは送りません。送信が始まるまでは操作を取り消せます。
  1. KGPU プロセスを選択
  2. 準備同じ利用者 · root 以外 · PID 1、dgxtop、その host を除く
  3. pidfd_open/proc の識別情報を確認
  4. 確認10 秒間有効な一度限りのトークン · y
  5. intent を監査記録SQLite synchronous=FULL
  6. 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 の目標を別に設けています。

256 MiB のデータ予算(既定値)
区分MiB内容
カタログとメタデータ24名前、tombstone、仲裁の状態
未加工の候補32出所診断用の 60 秒分の証拠
現在の状態16512 系列、256 プロセス、1,024 件の GPU 関連付け
元の解像度の履歴32直近 300 秒
分単位の集計9624 時間の集計、出所の変更、欠測区間
入力(ingress)8稼働中のすべての IPC とデコード用バッファ
表示12view model と整形キャッシュ
履歴クエリ16実行中のクエリが固定するスナップショット
制御と予備20コマンド、アーカイブ journal、ログ、安全用の予備領域
過負荷の状態キューのバイト使用量が 75% に達するか収集が遅くなると圧迫状態に入り、メモリが 90% に達すると抑制状態に入ります。
  1. 正常通常のレート
  2. 圧迫任意の作業を停止 · 2 fps
  3. 抑制クエリを制限 · 周期を延長
  4. 回復中30 秒ごとに 1 段階戻す

過負荷時には、dgxtop は精度を黙って落とすのではなく、縮退状態を明示します。圧迫状態では任意の作業を止め、描画を最大 2 fps にします。抑制状態ではクエリを制限し、サンプリング周期を延ばし、バナーに設定値と実際の値を併記します。回復時は正常な状態が 30 秒続くごとに 1 段階ずつ戻し、失われたサンプルを補間しません。

境界と検証

Workspace:中心に 1 つの契約 crate具体的な実装が交わるのは、組み立ての起点だけです。
dgxtop組み立ての起点 + CLI
  • 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 だけに依存
dgxtop-core型 · 単位 · port · コマンド · バイト予算

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 は不要です。遅いターミナルでも入力をブロックしてはいけません。