共有の設定 — スライス・MIG・クォータで分けておく
目標
1枚を複数人で使えるようにする3つの方式を、設定とオブジェクトで扱います。スライス数の分だけ枠が増えることをノードのstatusで確認し、MIGノードはリソース名そのものが違うことをスケジューリングで確認し、チームに割り当てた分をクォータで固定するところまで行います。
なぜ重要なのか
GPUは、買ったあとに、使用率が1日平均で1桁なのに、待ち行列はいつも埋まっています。Pod1つがカード1枚を丸ごと確保するからです。ここから「1枚を複数人で使えるようにしてほしい」という要求が出てきて、答えは3つあるのに、名前が似ているので、よく混同されます。
最も重要な区別は、何を分けるのかです。タイムスライシングのreplicasは、GPUを分割しろという意味ではなく、同じデバイスを複数回登録しろという意味です。枠は増えますが、VRAMは分割されていない1つの塊なので、1つのPodが多く確保すると、残りがメモリ不足に遭います。MPSは、カーネルを同時に実行させてスループットを上げますが、分離は依然として弱いです。MIGだけがハードウェアで分けるので、メモリもエラーも分離されます。そして、MIGノードは、リソース名そのものがプロファイル名に変わるため、古いマニフェストがそのノードをずっと使えないことが起きます。
環境
このPodには、GPUもデバイスプラグインもありません。代わりに、kwokが起動した本物のコントロールプレーンがあり、ノードのstatusに数字を書くと、本物のスケジューラーがそれを見て判定します。クォータも本物のapiserverが検査します。作業ディレクトリは/root/gpushareで、その下のk8s/・bin/・out/を使います。
ステップ
gpu-share・team-aネームスペースと、設定を2セット入れたConfigMapを作成します。- ノードのラベルで設定を選び、アドバタイズ量をそれぞれ書きます。
lab-node-2をMIG mixedノードにします。- リソース名だけが違うPodを2つ起動して、行き先が分かれることを確認します。
team-aにクォータを設定し、超過する要求が拒否されることをout/quota-denied.txtに残します。bin/check-request.shで、スライスを複数要求するリクエストを事前に弾きます。- ConfigMapに
mpsを追加し、out/isolation.txtに分離の表を書きます。 - 3つのワークロードを、それぞれに合うノードに配置して、
out/placement.txtに書きます。
参考
- ステップ2のアドバタイズ量は、推測せずにステップ1の
replicasを掛けて入れてください。採点ツールがConfigMapを読んで、同じ掛け算をもう一度行います。 capacityとallocatableを一緒に書く必要があります。片方だけだと、スケジューラーが使える空きが生まれません。- ステップ3で、MIGノードに
nvidia.com/gpuを一緒に書かないでください。mixed戦略の核心は、そのノードがプロファイル名でだけアドバタイズすることにあります。 - ステップ8のノード名は、
kubectl get pod -o wideで確認して書いてください。
デバイスプラグインの設定に名前を付けて、2セット入れる
gpu-shareとteam-aネームスペースを作成し、/root/gpushare/k8s/device-plugin-configs.yamlで、gpu-shareにdevice-plugin-configsというConfigMapを作成してください。キーはdefaultとsharedの2つで、sharedには、sharing.timeSlicingの下にfailRequestsGreaterThanOne: trueと、resources[0]がnvidia.com/gpu・replicas: 4で入ります。defaultにはsharingを置かないでください。
デバイスプラグインは、設定を1つのConfigMapに複数セット入れておいて、ノードごとに選んで使わせることができます。そうすれば、同じクラスターの中で、あるノードは丸ごと、あるノードは分け合って使えます。failRequestsGreaterThanOneは、スライスを2つ以上ほしいという要求を拒否するスイッチです。スライスを2つ受け取っても、同じデバイスを2回予約しているだけで、性能もメモリも2倍にならないからです。ConfigMapの値は複数行の文字列なので、|-を使います。
ノードのラベルで、どの設定を使うかを選ぶ
lab-node-0にnvidia.com/device-plugin.config=defaultを、lab-node-1に=sharedを付けてください。そのあと、2台のノードがアドバタイズするnvidia.com/gpuをstatusに書きます。2台とも物理GPUは2枚として、lab-node-0は2、lab-node-1は2にスライス数を掛けた値です。
デバイスプラグインは、ノードのnvidia.com/device-plugin.configラベルを見て、ConfigMapのどのキーを使うかを選びます。そのため、同じクラスターの中で、ノードごとに異なる共有ポリシーを置けます。アドバタイズ量は、推測せずにステップ1のreplicasをそのまま掛けて入れてください。capacityとallocatableの両方に書く必要があります。ここで増えるのは枠の数だけで、VRAMは依然として1つの塊です。
MIG mixedノードは、リソース名そのものが異なる
lab-node-2にnvidia.com/mig.strategy=mixedとnvidia.com/gpu.productラベルを付け、statusにnvidia.com/mig-1g.10gbを7としてアドバタイズしてください。このノードには、nvidia.com/gpuを書かないでください。
MIGは、ハードウェアでカードを分割します。mixed戦略では、デバイスプラグインがプロファイルごとに異なる名前でリソースをアドバタイズするので、Podが要求するリソース名も、nvidia.com/gpuではなく、nvidia.com/mig-1g.10gbのようなプロファイル名になります。そのため、このノードはnvidia.com/gpuをまったくアドバタイズしません。A100 40GBを1g.10gbで分割すると、7つのスライスができます。
リソース名が違うと、互いの枠には行けない
/root/gpushare/k8s/mig-job.yamlにPodを2つ書いて、gpu-shareに適用してください。mig-jobはnvidia.com/mig-1g.10gbを1個、plain-gpuはnvidia.com/gpuを1個要求します。どちらもノードの条件は指定しないでください。
条件を指定しなくても、リソース名だけで行き先が分かれます。mig-jobは、その名前をアドバタイズするノードにだけ行け、plain-gpuは、MIGノードには行けません。同じ物理カードを使っていても、スケジューラーにとってはまったく別のリソースです。そのため、MIGノードを導入するときに古いマニフェストをそのままにしておくと、Podがそのノードをずっと使えないことが起きます。
チームに割り当てた分を、クォータで固定する
/root/gpushare/k8s/gpu-quota.yamlで、team-aにgpu-quotaというResourceQuotaを作成してください。requests.nvidia.com/gpuを2に制限します。そのあと、GPUを1枚ずつ使うPodを2つteam-aに起動して上限を埋め、さらに1枚を要求する/root/gpushare/k8s/team-over.yamlを適用して、拒否メッセージを/root/gpushare/out/quota-denied.txtに保存してください。
拡張リソースも、ネームスペースのクォータで制限できます。キー名がrequests.<자원이름>の形である点だけが違います(プレースホルダーはリソース名です)。Pod2つは、nvidia.com/device-plugin.config: sharedのノードセレクターでスライスのノードに置くと、枠が十分にあります。上限を埋めたあとの要求は、スケジューラーではなくアドミッションの段階で拒否されるので、Podはそもそも作成されません。メッセージは標準エラー出力に出るので、2>&1で受けてください。
スライスを複数要求するリクエストを、デプロイ前に弾く
/root/gpushare/bin/check-request.sh <파드매니페스트>を作成してください(プレースホルダーはPodのマニフェストです)。コンテナがnvidia.com/gpuを1より大きく要求していたら、OVER=<컨테이너이름>=<개수>を1行ずつ出力して、終了コード1で終わります(プレースホルダーはコンテナ名と個数です)。そうでなければOKを出力して、0で終わります。GPUをまったく要求しないPodも、通過になる必要があります。
failRequestsGreaterThanOneは、デバイスプラグインが実行時に行う判定です。同じ判定をデプロイ前に行っておけば、ユーザーがリソースを誤解したまま計画を立てるのを防げます。複数のドキュメントが入ったファイルもあるので、yaml.safe_load_allで読み、limitsとrequestsの両方を見てください。採点ツールは、このスクリプトを3種類のマニフェストで実際に実行します。
3つ目の方式を追加して、分離の表を作成する
ConfigMapのdevice-plugin-configsにmpsキーを追加してください。sharing.mps.resources[0]がnvidia.com/gpu・replicas: 4で、timeSlicingは一緒に置きません。そして/root/gpushare/out/isolation.txtに、5行で分離の表を書いてください。TIMESLICING_MEMORY_ISOLATION、MPS_MEMORY_ISOLATION、MIG_MEMORY_ISOLATION、TIMESLICING_FAULT_ISOLATION、MIG_FAULT_ISOLATIONです。
MPSは、複数のプロセスのカーネルを1つのコンテキストにまとめて、本当に同時に実行させます。プロセスごとにメモリの上限をかけることもできますが、それは協調的な制限に近く、1つのプロセスが落ちるときに、他のプロセスまで影響を受けることがあります。5行の値は、yes・no・partialのどれかです。値が完全な分離ならyes、なければno、上限はかけられるが強制的な分離ではなければpartialです。
性質が異なる3つのワークロードを、それぞれに合うノードに配置する
/root/gpushare/k8s/placement.yamlで、gpu-shareにPodを3つ作成してください。prod-inferenceはMIGノードでプロファイルのリソースを、notebookはスライスのノードでnvidia.com/gpuを、batch-trainは分割しないノードでnvidia.com/gpuを、それぞれ1個ずつ使います。ノードはラベルで選んでください。そのあと、実際にどこに配置されたかを、/root/gpushare/out/placement.txtに<파드이름>=<노드이름>の3行で書きます(プレースホルダーはPod名とノード名です)。
基準線はこうです。分離が必要ならMIG、利用率が目的ならタイムスライシングです。本番の推論は、隣のPodのメモリ障害に巻き込まれてはいけないので、ハードウェアパーティションのほうへ送り、実験用のノートブックは、使用率が低いのでスライスのノードへ送ります。バッチ学習は、カードを丸ごと使うほうが速いので、分割しないノードへ送ります。ノードの条件は、ステップ2とステップ3で付けたラベルをそのまま使えばよいです。ファイルのノード名は、推測せずにkubectl get pod -o wideで確認して書いてください。