NVIDIAのGPU device-pluginは、KubernetesでGPUスケジューリングを可能にする基盤コンポーネントです。しかし、このプラグインが欠落していたり、設定が誤っていたりしてもPodの起動自体は失敗しません。その代わり、静かなリソース競合が発生し、大規模言語モデル(DeepSeek-R1など)の推論サービスで、高負荷時のOOM(メモリ不足)、メモリリーク、CUDA_ERROR_INVALID_HANDLEといった「謎のクラッシュ」を引き起こす根本原因となります。
典型的な障害現象の分類
- GPUメモリの越境共有: 複数のPodが同一GPUのメモリ空間を共有し、CUDAコンテキストの競合を引き起こします。
- デバイスノードの権限喪失:
/dev/nvidia0の権限がroot:rootで一般ユーザーからの読み取りが許可されていない場合、コンテナ内でnvml.Init()が失敗します。 - トポロジー認識の喪失: NUMAノードとGPUのPCIバスが一致せず、30%以上の帯域幅低下を招きます。
- MIGスライスの未宣言: Podが
nvidia.com/gpu:1を要求しているにもかかわらず、実際にはGPU全体を占有し、MIGによる分離ポリシーが迂回されます。
クイック検証スクリプト
# device-pluginの動作とデバイス登録状況を確認
kubectl get nodes -o wide
kubectl describe node | grep -A 10 "nvidia.com/gpu"
# ノード上でデバイスノードの存在と権限を確認
ls -l /dev/nvidia*
# 正しい出力例: crw-rw-rw- 1 root root ... /dev/nvidia0
主要な設定項目比較表
| 設定項目 | 正しい値 | よくある誤り | 結果 |
|---|---|---|---|
| resourceName | nvidia.com/gpu | nvidia-gpu | K8sスケジューラがGPUリクエストを無視 |
| pluginArgs.deviceListStrategy | envvar | none | MIGインスタンスの検出が不可能 |
修正手順
- 公式device-plugin(v0.15.0以降)をデプロイ:
kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.15.0/nvidia-device-plugin.yml - DaemonSetの準備完了を確認:
kubectl get ds -n kube-system nvidia-device-plugin-daemonset - DeepSeek Podに
runtimeClassName: nvidiaを追加し、resources.limits."nvidia.com/gpu": "1"を明示的に宣言します。
DeepSeek Kubernetesアーキテクチャの核とGPUスケジューリング原理
Kubernetes Device PluginメカニズムとNVIDIA GPUスタックの連携モデル
Kubernetes Device Pluginは、GPUやFPGAといった専用ハードウェアのリソーススケジューリングを拡張するための重要なインターフェースです。NVIDIA GPUスタックは、nvidia-device-pluginを通じてkubeletとの標準的な接続を実現します。
Device Pluginの登録フロー
プラグインは起動後、kubeletのUnixソケットに登録を要求します。
client, _ := pluginapi.NewRegistrationClient("unix:///var/lib/kubelet/device-plugins/kubelet.sock")
req := &pluginapi.RegisterRequest{
Version: pluginapi.Version,
Endpoint: "device-plugin.sock",
ResourceName: "nvidia.com/gpu",
Options: &pluginapi.DevicePluginOptions{PreStartRequired: true},
}
client.Register(context.Background(), req)
この登録により、リソース名nvidia.com/gpuが宣言され、ドライバの準備を確実にするための起動前フックが有効になります。Endpointは、kubeletが後続のListAndWatchを呼び出すためのプラグインのローカルリスニングアドレスを指します。
リソース発見と報告の構造
| フィールド | 説明 |
|---|---|
| ID | 一意のデバイス識別子(例: NVIDIA0000:00:1B.0) |
| Health | リアルタイムのヘルスステータス(Healthy/Unhealthy) |
DeepSeek推論ワークロードの特性に基づくGPUメモリ/演算能力/メモリ帯域幅の要求分析
メモリ帯域幅が主要なボトルネックに
DeepSeek-V2のFP16バッチ推論では、KV Cacheの読み書き頻度がシーケンス長に応じて二次関数的に増加します。以下は、典型的なattentionカーネルのメモリアクセスパターンを示しています。
__global__ void fused_kv_read(float* k_cache, float* v_cache,
int* seq_pos, int layer_id, int head_dim) {
int tid = blockIdx.x * blockDim.x + threadIdx.x;
// トークンごとに2×head_dim×(seq_len)個のパラメータをロード → 帯域幅に敏感
float k_val = k_cache[tid * head_dim + seq_pos[tid]];
}
このカーネルでは、seq_posの非連続なインデックスによりL2キャッシュのヒット率が42%未満となり、実測ではA100-80GBの帯域幅使用率がピークで93%に達します。
演算能力とメモリ要件の分離
| ワークロード段階 | FP16 TFLOPS使用率 | メモリ帯域幅使用率 | メモリ容量への負荷 |
|---|---|---|---|
| Embeddingルックアップ | 8% | 12% | 高(32GB+) |
| Attention計算 | 65% | 78% | 中 |
| FFN順伝播 | 27% | 10% | 低 |
device-plugin欠落によるGPUトポロジー認識の断絶とNUMA親和性喪失の実証検証
現象の再現と診断コマンド
# ノードのGPUデバイストポロジーを確認(device-pluginがない場合は空)
kubectl describe node worker-gpu | grep -A 5 "Allocatable.*nvidia.com/gpu"
# 出力: nvidia.com/gpu: 0 —— デバイスが未登録
このコマンドは、device-pluginが動作していない場合、KubernetesがGPUリソースを認識できず、スケジューラがPCIe/NVLinkトポロジーや関連するNUMAノード情報を完全に無視することを示しています。
NUMA親和性喪失の検証
| シナリオ | GPUの可視性 | CPUがバインドされたNUMAノード | 実測帯域幅(GB/s) |
|---|---|---|---|
| device-plugin正常 | NUMA 0上に4 GPU | taskset -c 0-7 | 28.4 |
| device-plugin欠落 | 報告されたGPUは0 | NUMA 1にランダムスケジュール | 9.1 |
主要なログエビデンス
- Kubeletログに
Starting device plugin managerの起動記録がない。 - NVIDIA device-pluginコンテナが
CrashLoopBackOff状態で、/var/lib/kubelet/device-plugins/kubelet.sockに接続できない。
kubectl describe nodeとnvidia-smi -qに基づくGPUリソースの宣言-割り当て-使用の3状態一貫性検証方法
3状態のマッピング関係
KubernetesにおけるGPUリソースには、3つの重要なビューが存在します。
- 宣言状態(Allocatable):
kubectl describe nodeが出力するnvidia.com/gpuの割り当て可能な数。 - 割り当て状態(Allocated):
kubectl get pods -o wideから逆引きした、GPUにバインドされたPodの数。 - 使用状態(Utilized):
nvidia-smi -q -d MEMORY,UTILIZATIONから取得する実際のメモリ/演算リソースの使用率。
一貫性検証スクリプトの断片
# ノードの宣言GPU数を取得
kubectl describe node $NODE | grep "nvidia.com/gpu" | awk '{print $2}'
# 実際のGPUデバイス数と合計メモリを取得
nvidia-smi -L | wc -l
nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | awk '{sum+=$1} END {print sum}'
このスクリプトは、Kubernetesスケジューリング層の宣言値と、基盤となるドライバが認識する物理デバイス数をそれぞれ抽出します。両者が一致しない場合、device-pluginの登録に異常があるか、GPUが正しく認識されていないことを示します。
状態比較表
| 観点 | kubectl describe node | nvidia-smi -q |
|---|---|---|
| デバイス総数 | allocatable: 8 | GPU 0–7 (8 devices) |
| 合計メモリ | — | 8 × 24576 MB |
Helm Chartにおけるdevice-pluginの注入ポイントとinitContainerライフサイクルフックの誤設定が発生しやすいシナリオの再現
典型的な誤設定パターン
Helm Chartがdevice-pluginをサイドカーとして注入しつつ、initContainersでその準備状態に誤って依存すると、Podの起動がブロックされるリスクが高まります。
- initContainerがdevice-pluginサイドカーより先に起動し、
ls /dev/driが失敗する。 values.yamlでdevicePlugin.enabledとinitContainer.waitDeviceReadyの連動ロジックが制約されていない。
誤った設定例
# values.yaml(危険な設定)
devicePlugin:
enabled: true
initContainers:
- name: gpu-check
image: nvidia/cuda:11.8-base
command: ["sh", "-c", "nvidia-smi -L && sleep 10"]
このinitContainerにはデバイス準備完了プローブがなく、restartPolicy: Alwaysも設定されていないため、device-pluginの準備が遅れると永久に失敗します。
検証マトリックス
| 設定項目 | 安全な値 | リスクのある値 |
|---|---|---|
initContainers[].livenessProbe | 有効、パスは/healthz | 欠落、または/dev/nvidia0を指す |
podSecurityContext.fsGroup | 1001(pluginと一致) | 0(権限拒否) |
4種類のGPUリソース分離障害の根本原因モデル化と特定手順
障害1: 複数Podによる同一GPUメモリ共有が引き起こすOOM Killer誤動作とcgroup v2のmemory.high超過の追跡
問題の現象
複数のPodがNVIDIA Device Pluginを介して1枚のGPU(例: A100 40GB)を共有する場合、cgroup v2のmemory.highがGPUメモリ分離用に設定されていないため、カーネルのOOM Killerがホストメモリの負荷を誤認します。
主要な設定検証
# GPU Podのcgroupパスにあるmemory.highを確認
cat /sys/fs/cgroup/kubepods/pod*/crio-*.scope/memory.high
# 出力: 9223372036854771712(LLONG_MAX、制限なし)
この値は、メモリ上限が明示的に制限されていないことを示しています。cgroup v2はmemory.maxの最終的な保護のみに依存することになり、OOM Killerはmemory.highのソフトリミットを判断基準とするため、誤動作を引き起こします。
修正戦略の比較
| 方法 | 有効な階層 | OOM誤動作の回避 |
|---|---|---|
memory.high=8Giを設定 | Pod cgroup | はい(OOMより前にメモリ回収をトリガー) |
memory.max=16Giのみ設定 | Pod cgroup | いいえ(OOM Killerが依然として積極的に動作する可能性あり) |
障害2: CUDAコンテキストリークによるGPUハンドル枯渇のstrace+nvtopを用いた合同診断実践
現象の特定
トレーニングタスクを繰り返し起動・停止した後、nvidia-smiではGPUメモリが解放されていないように見えませんが、nvtopで「Contexts」の数が継続的に増加(>100)しており、新しいプロセスがCUDAデバイスを割り当てられない状態を観測できます。
主要なシステムコールの動的追跡
strace -p $(pgrep -f "python train.py") -e trace=ioctl,open,close -f 2>&1 | grep -E "(cuda|NVIDIA|drm)"
このコマンドは、対象プロセスによるNVIDIAドライバデバイスノード(例: /dev/nvidiactl)へのioctl呼び出しをキャプチャします。大量のioctl(..., DRM_IOCTL_NVIDIA_GET_CTX_INFO)が成功しているにもかかわらず、対応するclose()がない場合、コンテキストが作成された後に破棄されていないことを示唆します。
コンテキストライフサイクル比較表
| 操作 | 典型的なioctl | 明示的なclose()が必要か |
|---|---|---|
| CUDAコンテキストの初期化 | ioctl(fd, NV_ESC_ALLOC_CONTEXT) | はい |
| CUDAコンテキストの破棄 | ioctl(fd, NV_ESC_FREE_CONTEXT) | いいえ(close(fd)に依存) |
障害3: TensorRT-LLM推論エンジンにおけるPCIe帯域幅競合によるタイムアウトサーキットブレーカーのログパターン認識
典型的なログの特徴
マルチGPU推論シナリオでPCIeリンクが継続的に飽和状態になると、TensorRT-LLMは以下のようなサーキットブレーカーログを頻繁に出力します。
[E] 2024-06-15 14:22:37.892 [TRT-LLM] engine_timeout: wait for GPU event timeout (2000 ms), likely PCIe stall due to bandwidth contention
このログは、イベント同期の待機がタイムアウトしたことを示しており、主な原因はGPU計算のボトルネックではなく、Host→DeviceまたはDevice→Hostのデータ転送が妨げられていることです。
主要な診断観点
- nvlink-pcieクロストポロジー: GPU間の通信がPCIe Root Complexをまたいでいないか確認します。
- nccl_traceログ:
collフェーズでwait_send/wait_recvの長いレイテンシがないか確認します。
帯域幅競合のしきい値リファレンス
| PCIeバージョン | 片方向帯域幅(GB/s) | サーキットブレーカーが作動する持続使用率のしきい値 |
|---|---|---|
| PCIe 4.0 x16 | 31.5 | >82% |
| PCIe 5.0 x16 | 63.0 | >90% |
DeepSeek-K8s本番環境向けGPUオーケストレーション強化ソリューション導入ガイド
Extended Resource + Device Plugin + RuntimeClass に基づく3段階GPU分離戦略の設定
主要コンポーネントの連携関係
Extended Resourceはクラスタレベルでのリソース抽象化を提供し、Device Pluginはデバイス検出と割り当てを実現し、RuntimeClassはランタイム制約をバインドします。これら3つが連携して、リソース宣言→デバイス管理→コンテナスケジューリングのクローズドループを形成します。
主要な設定例
# /etc/kubernetes/device-plugins/nvidia-gpu-plugin.yaml
apiVersion: k8s.io/v1
kind: RuntimeClass
metadata:
name: nvidia-isolated
handler: nvidia-container-runtime
overhead:
podFixed:
nvidia.com/gpu: "1"
このRuntimeClassはGPUのオーバーヘッドを明示的に宣言し、kube-schedulerによるextended resource-awareスケジューリングをトリガーします。handlerはカスタムコンテナランタイムを指定し、cgroup v2におけるGPUデバイスノードとMIGインスタンスの正確なマウントを保証します。
スケジューリング能力の比較
| 戦略階層 | 分離粒度 | 動的調整のサポート |
|---|---|---|
| Extended Resource | ノードレベルの合計 | いいえ(kubeletの再起動が必要) |
| Device Plugin | MIGインスタンス / vGPU | はい(ホットプラグ検出) |
| RuntimeClass | Podレベルのバインディング | はい(Pod作成時に決定) |
kubernetes-device-plugin v0.14+ のTopology ManagerポリシーをDeepSeekのマルチカード分散推論トポロジーに適応させる
Topology Managerポリシー選択の根拠
DeepSeek-V2/Largeモデルは、8カードA100 NVLinkトポロジーにおいて、PCIe階層の親和性を厳密にバインドする必要があります。single-numa-nodeポリシーは、すべてのGPUと対応するCPU、メモリが同一のNUMAノードに配置されることを保証し、ノード間の帯域幅ボトルネックを回避します。
デバイスプラグインの設定例
# device-plugin-config.yaml
topologyManagerPolicy: "single-numa-node"
topologyManagerScope: "container"
deviceListAllocation: true
この設定は、v0.14+で追加されたトポロジー認識割り当て機能を有効にし、Kubeletの--topology-manager-policy=single-numa-nodeと連携して、コンテナレベルのトポロジーアライメントを強制します。
GPU親和性検証表
| ノード | GPUインデックス | NUMAノード | NVLinkドメイン |
|---|---|---|---|
| node-01 | 0,1,2,3 | 0 | Domain-A |
| node-01 | 4,5,6,7 | 1 | Domain-B |
自社開発GPU QoS OperatorによるGPUメモリクォータのハード制限とメモリ断片化率の動的アラート実現
主要な制御ループ設計
func (r *GPUQoSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var pod corev1.Pod
if err := r.Get(ctx, req.NamespacedName, &pod); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
enforceMemoryQuota(&pod) // nvidia.com/gpu-memory-quotaアノテーションを強制的に注入
checkAndAlertFragmentation(&pod)
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
このReconcilerは30秒ごとにPodをスキャンし、nvidia.com/gpu-memory-limitアノテーションに基づいてハードクォータを計算し、cgroup v2のmemory.maxへの書き込みをトリガーします。断片化率のアラートしきい値はデフォルトで65%に設定され、CRDによるグローバル設定が可能です。
メモリ断片化率の評価戦略
nvidia-smi --query-compute-apps=used_memory --format=csv,noheader,nounitsを使用してアクティブなプロセスのメモリ使用量を収集します。/sys/fs/cgroup/nvidia/.../memory.statのpgpgin/pgpgoutを組み合わせて断片化エントロピーを推定します。
アラートしきい値設定表
| パラメータ | デフォルト値 | 説明 |
|---|---|---|
| fragmentationThreshold | 65% | Prometheusアラートをトリガーするメモリ断片化率の下限 |
| quotaEnforcementMode | "strict" | "strict"はcgroupのハード制限を有効にし、"soft"はイベントを記録するのみ |
CI/CDパイプラインに組み込むdevice-pluginヘルスチェック用eBPF検証スクリプト(libbpfgoベース)
eBPFヘルスチェックの主要ロジック
// デバイスヘルスプローブeBPFプログラムをロードして実行
obj := &HealthProbeObjects{}
if err := LoadHealthProbeObjects(obj, &LoadHealthProbeOptions{
LogLevel: 2,
}); err != nil {
log.Fatal("failed to load eBPF objects: ", err)
}
// ユーザー空間プローブをトリガーし、デバイスの準備状態を読み取る
status, err := obj.HealthMap.LookupUint32(0)
このスクリプトは、libbpfgoを介してプリコンパイルされたhealth_probe.oをロードし、HealthMap(BPF_MAP_TYPE_ARRAY)を使用してシングルキーのデバイス準備完了フラグを保存します。LookupUint32(0)が非ゼロ値を返す場合、device-pluginが管理するアクセラレータがカーネル空間のヘルスチェックを通過したことを示します。
CI/CD統合戦略
- Kubernetesノードの事前チェック段階で実行され、
node-feature-discoveryラベルの注入に依存します。 - 失敗した場合は、イメージ公開プロセスを自動的にブロックし、Prometheusメトリクス
device_plugin_health_check_failed_totalを報告します。
検証結果マッピング表
| リターンコード | 意味 | CIの動作 |
|---|---|---|
| 1 | GPU/NPUデバイスが準備完了、DMAパスが正常 | デプロイを続行 |
| 0 | eBPFプローブがタイムアウト、またはドライバが応答なし | パイプラインを中止 |