はじめに
前回、自宅のRTX 4070にSlurmを入れて、GPUジョブのキューイングを体験するという記事を書きました。HPC文脈のジョブスケジューラで、GPU1枚に複数ジョブを投げて順番待ちさせる、という内容です。
今回はその続きとして、同じマシンに k3s(軽量Kubernetes)を入れて、GPUをPodに割り当てるところまでをやります。
同じ「GPU 1枚をどう配るか」という課題に対して、HPC(Slurm)とコンテナオーケストレーション(Kubernetes)では考え方がまるで違います。両方を同じ実機で動かしてみると、その違いが具体的に見えてきました。
この記事でできるようになること
- k3s に NVIDIA GPU をリソースとして認識させる(
nvidia.com/gpu: 1) - GPUを要求するPodを起動して、コンテナ内から
nvidia-smiを通す - 公式手順どおりにやっても device plugin が起動しないという罠と、その原因究明
- GPU1枚に2つのPodを投げて、Slurm のキューイングと比べる
とくに3つ目が本記事の肝です。NVIDIA device plugin を Helm で入れたのに DaemonSet が DESIRED 0 のまま動かない、という状況に遭遇しました。同じところで詰まる人は少なくないはずです。
動作環境
| 項目 | 内容 |
|---|---|
| OS | Ubuntu 24.04.4 LTS |
| GPU | NVIDIA GeForce RTX 4070 (VRAM 12GB) |
| CPU | Intel Core i5-12400 |
| メモリ | 約31GB |
| ドライバ | 595.84 / CUDA 13.2 |
| k3s | v1.36.4+k3s1 |
| NVIDIA device plugin | v0.20.0 |
前提として、Docker + NVIDIA Container Toolkit は導入済みです。こちらは別記事にまとめています。この記事の手順は、その環境の上に乗せる形になります。
1. k3s のインストール
k3s は Rancher(SUSE)が開発する軽量Kubernetesです。バイナリ1つで動き、インストールはワンライナーで済みます。
curl -sfL https://get.k3s.io | sh -
1分ほどで完了します。確認します。
sudo k3s kubectl get node
NAME STATUS ROLES AGE VERSION
ubuntu-host Ready control-plane 4m56s v1.36.4+k3s1
Ready になっていれば成功です。
💡 インストール直後に
kubectl get pods -Aを叩いてNo resources foundと出ることがありますが、これは起動が完了する前に見ただけです。少し待てば各Podが立ち上がります。
sudo なしで kubectl を使う
毎回 sudo k3s kubectl と打つのは面倒なので、kubeconfig を自分のホームに配置します。
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
chmod 600 ~/.kube/config
echo 'export KUBECONFIG=~/.kube/config' >> ~/.bashrc
export KUBECONFIG=~/.kube/config
以降は kubectl だけで使えます。
同梱されるアドオンについて
k3s はデフォルトで Traefik(Ingressコントローラ)と ServiceLB(ロードバランサ)を同梱します。GPU検証だけなら使いませんが、動いていても実害はありません。気になる場合は次のように無効化してインストールできます。
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --disable=servicelb" sh -
2. containerd への NVIDIA ランタイム登録 ― 実は不要だった
ここが最初の関門になる、と身構えていた部分です。
k3s は Docker ではなく、独自の containerd を使います。 先に設定した NVIDIA Container Toolkit は Docker 向けの設定だったので、k3s の containerd 側にも別途 NVIDIA ランタイムを登録する必要がある、というのが当初の想定でした。
ネット上の記事を見ると、/var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl というテンプレートファイルを手書きする手順がよく出てきます。
ところが、現在の k3s では、この作業は不要でした。
公式ドキュメントにこう書かれています。
K3s will automatically detect alternative container runtimes if they are present when K3s starts.
k3s は起動時に PATH 上の代替ランタイムを自動検出します。対応しているランタイムには nvidia が含まれています。
確認手順
まず nvidia-container-runtime がPATH上にあるか確認します。
which nvidia-container-runtime
/usr/bin/nvidia-container-runtime
NVIDIA Container Toolkit を入れた時点で、ここに入っています。あとは k3s を再起動するだけです。
sudo systemctl restart k3s
containerd の設定ファイルを見ると、NVIDIA ランタイムが自動で追記されています。
sudo grep nvidia /var/lib/rancher/k3s/agent/etc/containerd/config.toml
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.'nvidia']
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.'nvidia'.options]
BinaryName = "/usr/bin/nvidia-container-runtime"
手書きのテンプレートは一切作っていません。 k3s が勝手にやってくれました。
RuntimeClass も組み込み済み
Kubernetes で代替ランタイムを使うには RuntimeClass というリソースが必要ですが、これも k3s には最初から入っています。
kubectl get runtimeclass
NAME HANDLER AGE
crun crun 8d
lunatic lunatic 8d
nvidia nvidia 8d
nvidia-experimental nvidia-experimental 8d
slight slight 8d
spin spin 8d
wasmedge wasmedge 8d
wasmer wasmer 8d
wasmtime wasmtime 8d
wws wws 8d
nvidia が最初から存在します。AGE 8d は k3s をインストールした日時です。つまり自分で YAML を書いて作る必要はありません。
⚠️ ネット上には
config.toml.tmplを手書きする記事が多く残っていますが、k3s のバージョンによっては不要です。まずwhich nvidia-container-runtimeとgrep nvidia config.tomlで確認してから、必要な場合だけ手を動かすのが良いと思います。
3. NVIDIA device plugin の導入 ― ここで詰まった
containerd がランタイムを認識しても、それだけでは Kubernetes は GPU をリソースとして扱えません。NVIDIA device plugin を DaemonSet としてデプロイして、「このノードには GPU が1枚ある」とkubeletに登録させる必要があります。
Helm で入れます。k3s には Helm が同梱されていないので、先に入れます。
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
そして device plugin をインストールします。ここで runtimeClassName=nvidia の指定が必須です。k3s のデフォルトランタイムは runc のままなので、これを指定しないと plugin 自身が GPU を見られません。
kubectl create namespace nvidia-device-plugin
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update
helm install nvdp nvdp/nvidia-device-plugin \
--namespace nvidia-device-plugin \
--set runtimeClassName=nvidia
NAME: nvdp
LAST DEPLOYED: ...
NAMESPACE: nvidia-device-plugin
STATUS: deployed
REVISION: 1
deployed と出ました。成功したように見えます。しかし。
kubectl get pods -n nvidia-device-plugin
No resources found in nvidia-device-plugin namespace.
Podが1つも起動していません。
原因究明
まず DaemonSet の状態を見ます。
kubectl get daemonset -n nvidia-device-plugin
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR
nvdp-nvidia-device-plugin 0 0 0 0 0 <none>
nvdp-nvidia-device-plugin-mps-control-daemon 0 0 0 0 0 nvidia.com/mps.capable=true
DaemonSet は存在します。しかし DESIRED が 0 です。「このDaemonSetを配置すべきノードが1つも無い」とKubernetesが判断している状態です。
NODE SELECTOR は <none> なので、セレクタが原因ではありません。となると affinity(ノード選択条件) が怪しいので、中身を見ます。
kubectl get daemonset nvdp-nvidia-device-plugin -n nvidia-device-plugin -o yaml | grep -A 30 "affinity:"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: feature.node.kubernetes.io/pci-10de.present
operator: In
values:
- "true"
- matchExpressions:
- key: feature.node.kubernetes.io/cpu-model.vendor_id
operator: In
values:
- NVIDIA
- matchExpressions:
- key: nvidia.com/gpu.present
operator: In
values:
- "true"
原因が判明しました。
DaemonSet は次の3つのラベルのいずれかを持つノードを探しています。
| ラベル | 誰が付けるか |
|---|---|
feature.node.kubernetes.io/pci-10de.present=true |
NFD(Node Feature Discovery) |
feature.node.kubernetes.io/cpu-model.vendor_id=NVIDIA |
NFD |
nvidia.com/gpu.present=true |
手動でも付けられる |
上2つは NFD(Node Feature Discovery) というコンポーネントが自動で付けるものです。NFDはノードのハードウェア構成を検出してラベルを付ける仕組みで、10de は NVIDIA の PCI ベンダーIDです。
私は NFD を入れていませんでした。ノードのラベルを確認します。
kubectl get node ubuntu-host --show-labels
LABELS
beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=k3s,
beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,
kubernetes.io/hostname=ubuntu-host,kubernetes.io/os=linux,
node-role.kubernetes.io/control-plane=true,node.kubernetes.io/instance-type=k3s
3つのラベルがどれも付いていません。だから条件に合うノードが存在せず、DESIRED 0 になっていたわけです。
対処:ノードにラベルを付ける
NFD を追加で導入する手もありますが、単一ノードの検証環境なら3つ目のラベルを手で付けるのが素直です。「このノードにはGPUがある」と明示的に宣言する形になります。
kubectl label node ubuntu-host nvidia.com/gpu.present=true
node/ubuntu-host labeled
ラベルを付けた瞬間に DaemonSet コントローラが検知します。
kubectl get pods -n nvidia-device-plugin
NAME READY STATUS RESTARTS AGE
nvdp-nvidia-device-plugin-z7mjh 0/1 ContainerCreating 0 0s
Podがスケジュールされました。
4. GPU がリソースとして認識されたか確認
device plugin のログを見ます。
kubectl logs -n nvidia-device-plugin -l app.kubernetes.io/name=nvidia-device-plugin --tail=20
I0917 22:06:19.678628 1 plugin-manager.go:101] Detected platform: nvml
I0917 22:06:19.678647 1 plugin-manager.go:50] Using device discovery strategy: nvml
I0917 22:06:19.686670 1 server.go:198] Starting GRPC server for 'nvidia.com/gpu'
I0917 22:06:19.687126 1 server.go:142] Starting to serve 'nvidia.com/gpu' on /var/lib/kubelet/device-plugins/nvidia-gpu.sock
I0917 22:06:19.688314 1 server.go:149] Registered device plugin for 'nvidia.com/gpu' with Kubelet
Detected platform: nvml で GPU を検出し、Registered device plugin for 'nvidia.com/gpu' with Kubelet で kubelet への登録まで完了しています。
ノードの情報を見ます。
kubectl describe node ubuntu-host | grep -i -A2 -B2 "nvidia.com/gpu"
Capacity:
memory: 32087032Ki
nvidia.com/gpu: 1
pods: 110
Allocatable:
memory: 32087032Ki
nvidia.com/gpu: 1
pods: 110
Capacity と Allocatable の両方に nvidia.com/gpu: 1 が出ました。 GPU が Kubernetes のリソースとして正式に認識された状態です。
CPUやメモリと同じ列に GPU が並んでいるのがポイントです。Kubernetes から見ると、GPU は「割り当て可能なリソースの一種」として抽象化されています。
5. GPU Pod を起動する
いよいよ実際にGPUを要求するPodを動かします。
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: gpu-test
spec:
restartPolicy: Never
runtimeClassName: nvidia
containers:
- name: cuda
image: nvidia/cuda:13.0.0-base-ubuntu24.04
command: ["nvidia-smi"]
resources:
limits:
nvidia.com/gpu: 1
EOF
ポイントは2つです。
-
runtimeClassName: nvidia… これを忘れるとGPUが見えません。k3sのデフォルトランタイムはruncのままなので、Pod側で明示的にNVIDIAランタイムを要求します -
limits: nvidia.com/gpu: 1… GPU 1枚を要求します。Slurm の--gres=gpu:1に相当する部分です
状態を見守ります。
kubectl get pod gpu-test -w
NAME READY STATUS RESTARTS AGE
gpu-test 0/1 ContainerCreating 0 0s
gpu-test 1/1 Running 0 17s
gpu-test 0/1 Completed 0 17s
Completed になりました。nvidia-smi を実行して正常終了した、という意味です。Ctrl+C で監視を抜けて、ログを見ます。
kubectl logs gpu-test
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 595.84 Driver Version: 595.84 CUDA Version: 13.2 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 4070 Off | 00000000:01:00.0 On | N/A |
| 0% 41C P8 8W / 200W | 1555MiB / 12282MiB | 0% Default |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+
Pod の中から RTX 4070 が見えました。 これで完了です。
💡
No running processes foundと出ますが正常です。コンテナは独立したPID名前空間で動くため、ホスト側のプロセスは見えません。VRAM使用量(1555MiB)にはホスト側の使用分が反映されています。
後片付けします。
kubectl delete pod gpu-test
Slurm と Kubernetes、同じGPUを配る2つの考え方
ここからが本題です。同じマシンで両方を動かしてみて見えた違いを整理します。
リソース要求の書き方
Slurm はコマンドラインオプションで完結します。
srun --gres=gpu:1 nvidia-smi
Kubernetes は YAML でマニフェストを書きます。
resources:
limits:
nvidia.com/gpu: 1
Slurm は1行、Kubernetes は Pod 定義全体で十数行。記述量は明らかに Slurm の方が少ないです。ただし Kubernetes 側は、イメージ・ネットワーク・ストレージ・再起動ポリシーまで同じ場所で宣言できるので、単純な比較はできません。
空きがないときの振る舞い
ここが一番わかりやすい違いでした。
Slurm では、GPU 1枚に3ジョブを投入すると、1つが RUNNING、残りが PENDING (Resources) になります。前のジョブが終わると自動で次が走り、ログのタイムスタンプを見ると1秒のギャップで直列に実行されていました。キューに並べて順番に流す、という発想です。
Kubernetes でも同じことを試しました。nvidia.com/gpu: 1 を要求するPodを2つ同時に作ります。
for i in 1 2; do
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: gpu-queue-$i
spec:
restartPolicy: Never
runtimeClassName: nvidia
containers:
- name: cuda
image: nvidia/cuda:13.0.0-base-ubuntu24.04
command: ["sh", "-c", "nvidia-smi && sleep 60"]
resources:
limits:
nvidia.com/gpu: 1
EOF
done
kubectl get pods
NAME READY STATUS RESTARTS AGE
gpu-queue-1 1/1 Running 0 8s
gpu-queue-2 0/1 Pending 0 8s
2つ目が Pending になりました。理由を見ます。
kubectl describe pod gpu-queue-2 | tail -6
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 8s default-scheduler 0/1 nodes are available: 1 Insufficient nvidia.com/gpu. no new claims to deallocate, preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.
0/1 nodes are available: 1 Insufficient nvidia.com/gpu。この表現が Kubernetes の発想をよく表しています。「順番待ちしている」ではなく、**「配置できるノードが0個」**という言い方をします。さらに No preemption victims found(追い出せるPodも無い)とまで報告してくれます。
結果は似ていますが、モデルが違います。
| Slurm | Kubernetes | |
|---|---|---|
| 単位 | ジョブ(コマンド実行) | Pod(コンテナ) |
| 待ち状態 | PENDING (Resources) |
Pending / Insufficient |
| 発想 | キューに並べて順番に流す | 空きノードに配置する |
| 主戦場 | HPC・大規模学習 | サービング・マイクロサービス |
設計思想の違い
Slurm は時間軸で並べる仕組みです。バッチジョブが前提で、「いつ終わるか分からないが、いずれ順番が来る」という世界観。大規模な学習ジョブを何日も回すHPC用途に向いています。
Kubernetes は空間軸で配置する仕組みです。「どのノードに置くか」を解く問題として扱い、常時稼働するサービスをどう分散させるかが得意。推論サーバーのような、止まらないワークロードに向いています。
どちらが優れているという話ではなく、解こうとしている問題が違うのだと実感しました。実際、大規模AIクラスタでは両方を併用する構成(Slinky など)も出てきています。
まとめ
k3s 上で GPU を Pod に割り当てられるようになりました。
詰まりどころの整理
| 想定していた作業 | 実際 |
|---|---|
containerd の config.toml.tmpl を手書き |
不要 — k3s が nvidia-container-runtime を自動検出 |
RuntimeClass を YAML で作成 |
不要 — k3s に10種類が組み込み済み |
| (想定外) | ノードに nvidia.com/gpu.present=true ラベルが必要 |
一番ハマったのは3つ目です。Helm でのインストールは deployed と表示されて成功したように見えるのに、Podが1つも起動しない。kubectl get daemonset で DESIRED 0 を見て、-o yaml で affinity の中身を読んで、ようやく原因に辿り着きました。
NFD(Node Feature Discovery)を導入している環境なら自動でラベルが付くため、この問題は起きません。逆に言うと、単一ノードで手軽に試そうとすると必ず踏むということです。
確認用チェックリスト
同じ構成で試す方は、この順で確認すると詰まりにくいはずです。
-
which nvidia-container-runtimeでパスが出るか -
sudo grep nvidia /var/lib/rancher/k3s/agent/etc/containerd/config.tomlで設定が入っているか -
kubectl get runtimeclassにnvidiaがあるか -
kubectl get daemonset -n nvidia-device-pluginでDESIREDが 1 か(0ならラベル不足) -
kubectl describe node <ノード名> | grep nvidia.com/gpuで1が出るか -
Pod に
runtimeClassName: nvidiaを書いたか



