TT Lab
はじめる
学ぶ 学習パス コース

GPU Operatorとタイムスライシング

ランタイムの関門 — 検証されない文字列と、ロードされたものの照合

TT Labで続きを見る

目標

ランタイムのゲートの両側を一緒に扱います。クラスター側では、RuntimeClassが何を選んで、何を検証しないのかを確認し、ノード側では、設定が静かに無視される2つのケースを作ってみます。最後に、その2つを照合するチェッカーを自分で作ります。

なぜ重要なのか

Podがノードに割り当てられたのに、コンテナの作成で止まって失敗するなら、それはランタイム側です。RuntimeClassのhandlerはただの文字列であり、apiserverはそれを検証しません。ノードごとにcontainerdの設定が異なる可能性があり、検証する主体がそもそもないからです。そのため、タイプミスは適用もされ、スケジュールもされ、そのノードがコンテナを作る瞬間にだけ表面化します。

ノード側には、静かな失敗がさらに2つあります。importsのグロブがファイル名とずれていると、containerdは何も言わずに通り過ぎ、設定バージョンが3に上がると、プラグイン名そのものが変わるため、古い名前で書いた設定が丸ごと無視されます。どちらの場合も、ファイルには、望んだ内容がそのまま書かれています。そのため、点検はファイルではなく、読み込まれた設定を見る必要があり、その判定をクラスターのRuntimeClassの一覧と照らし合わせて、初めて役に立ちます。このラボの最後の出力物が、そのチェッカーです。

環境

このPodには、本物のcontainerdがありません。そのため、設定はTOMLパーサーで扱い、チェッカーはdumpを引数で受け取れるように作って、採点ツールが2種類のdumpで実際に実行します。クラスター側は、kwokが起動した本物のapiserverとスケジューラーなので、RuntimeClassの登録もオーバーヘッドの計算も本物です。作業ディレクトリは/root/gpurtで、その下のk8s/・etc/・bin/・fixtures/・out/を使います。

ステップ

  1. gpu-rtネームスペースと、RuntimeClassを2つ作成します。
  2. タイプミスしたhandlerでもPodがスケジュールされることを確認して、out/unvalidated.txtに書きます。
  3. cpu 1のノードを2台作成し、RuntimeClassのオーバーヘッドで結果が分かれることを確認します。
  4. ドロップインを捕まえられないimportsのグロブを作成し、out/imports.txtに書きます。
  5. 同じ設定を、バージョン3の形式でetc/config-v3.tomlに書き直します。
  6. bin/rc-crosscheck.shで、クラスターとdumpを照合するチェッカーを作成します。
  7. 既定のランタイムを変えるドロップインを作成し、out/blast.txtに爆発半径を書きます。
  8. 一部だけが読み込まれたdumpを模して、チェッカーを実行し、out/crosscheck.txtに保存します。

参考

ハンドラーを選ぶオブジェクトを作成する

gpu-rtネームスペースを作成し、/root/gpurt/k8s/runtimeclasses.yamlにRuntimeClassを2つ書いて適用してください。名前とhandlerは、それぞれnvidia、nvidia-cdiです。

RuntimeClassはクラスタースコープのオブジェクトで、handlerの値は、ノードのcontainerd設定にあるruntimes.<이름>と一致している必要があります(プレースホルダーはハンドラー名です)。kubeletはPodを作るとき、この文字列をCRIリクエストに載せて送り、containerdは自分の設定で同じ名前を探します。apiVersionはnode.k8s.io/v1です。

タイプミスしたハンドラーもapiserverを通過する

/root/gpurt/k8s/runtimeclass-typo.yamlに、handlerをわざと間違えて書いたnvidia-typoというRuntimeClassと、それを使うprobe-typoというPodを一緒に書いて、gpu-rtに適用してください。確認した事実を、/root/gpurt/out/unvalidated.txtに3行で書きます。API_VALIDATES_HANDLER、FAILS_AT、VISIBLE_TOです。

apiserverはhandlerの文字列を検証しません。ノードごとにcontainerdの設定が異なる可能性があり、検証する主体がそもそもないからです。そのため、タイプミスは適用もされ、スケジュールもされます。ずれが表面化する場所は、そのノードがコンテナを作る瞬間で、それを知っているのはノードだけです。3行の値は、noと、どの段階で失敗するのか、誰がそれを知っているのかを、それぞれ1単語で書いてください。

RuntimeClassのオーバーヘッドがスケジューリングを変える

/root/gpurt/k8s/node-tight.yamlで、cpu 1のkwokノードを2台(gpu-tight-a、gpu-tight-b)作成してください。そのあと/root/gpurt/k8s/overhead.yamlに、overhead.podFixedがcpu 250m・memory 128Miのnvidia-overheadというRuntimeClassと、cpuを900mずつ要求するPodを2つ書いて適用してください。fitsはaノードにRuntimeClassなしで、overはbノードにそのRuntimeClassを付けて配置します。

RuntimeClassのoverheadは、そのランタイムがPod1つごとに追加で消費するリソースです。apiserverがPodのspec.overheadにその値を埋め込み、スケジューラーは、コンテナの要求にそれを加えて空きを計算します。そのため、同じ900mの要求でも、オーバーヘッドが付くと1150mになり、cpu 1のノードには入れません。2台のノードを別々に置くのは、1台に置くと、先に入ったPodが空きを使ってしまい、比較がぼやけるからです。kwokのノードは、kwok.x-k8s.io/node: fakeアノテーションがあって初めて管理されます。

importsのグロブがずれると、静かに通り過ぎる

/root/gpurt/etc/config.tomlに、障害が起きたノードのメイン設定を作成してください。version = 2、CRIプラグインの下にdefault_runtime_name = "runc"とruntimes.runc、そしてドロップインを捕まえられないimportsのグロブを置きます。ドロップインは、/root/gpurt/etc/conf.d/99-nvidia.tomlにruntimes.nvidiaとして作成してください。確認結果を/root/gpurt/out/imports.txtに4行で書きます。

グロブが何にもマッチしないことは、エラーではなく正常な結果なので、containerdはログも残しません。そのため、拡張子を.confと書いておいて、ファイルは.tomlで置くと、その設定は永遠に読み込まれません。imports.txtの4行はGLOB、MATCHED、DROPIN_EXISTS、LOADED_NVIDIAで、GLOBは設定に書いたグロブをそのまま、MATCHEDは、そのグロブが実際に捕まえるファイル数を書きます。確認は、ls <글롭>で行います(プレースホルダーはグロブです)。

containerd 2.xの設定バージョン3で書き直す

/root/gpurt/etc/config-v3.tomlに、同じ内容をバージョン3の形式で書いてください。version = 3で、プラグイン名はio.containerd.cri.v1.runtimeです。default_runtime_nameはrunc、runtimesにはruncとnvidiaの両方がある必要があり、nvidiaにはoptions.BinaryNameを置きます。importsのグロブは、今回はドロップインを実際に捕まえる必要があります。

containerd 2.xは設定バージョン3を使い、そのときプラグイン名そのものが変わります。バージョンだけを上げて、古い名前(io.containerd.grpc.v1.cri)をそのままにしておくと、その設定は丸ごと無視されますが、エラーは出ません。前のステップと同じ種類の、静かな失敗です。書き終えたあとは、目で見るのではなく、パーサーで一度読んで確認してください。python3 -c "import tomllib,sys;print(tomllib.load(open(sys.argv[1],'rb')))" <파일>で確認できます(プレースホルダーはファイルのパスです)。

クラスターが要求するハンドラーと、読み込まれたものを照合する

/root/gpurt/bin/rc-crosscheck.shを作成してください。引数でdumpファイルを受け取ればそれを、なければcontainerd config dumpの結果を読みます。クラスターのRuntimeClassが要求するhandlerのうち、dumpにないものごとにMISSING=<핸들러>を1行ずつ出力して、終了コード1で終わります(プレースホルダーはハンドラー名です)。1つもなければOKを出力して、0で終わります。

核心は、判定の根拠がファイルではなく、読み込まれた設定であることです。/etc/containerd/config.tomlを読む点検は、この障害を原理的に見つけられません。ファイルには望んだ内容がそのまま書かれていて、読み込まれた結果だけが違うからです。dumpのランタイム名は、バージョンによってio.containerd.grpc.v1.criの下にもio.containerd.cri.v1.runtimeの下にもありうるので、両方を見る必要があります。クラスター側はkubectl get runtimeclass -o jsonpath='{range .items[*]}{.handler}{"\n"}{end}'で取り出し、TOMLのパースはpython3のtomllibで行います。採点ツールが、このスクリプトを2種類のdumpで実際に実行します。

既定のランタイムを変えると、爆発半径が変わる

/root/gpurt/etc/conf.d/50-default.tomlに、CRIプラグインのdefault_runtime_nameをnvidiaにするドロップインを作成してください。その結果を/root/gpurt/out/blast.txtに4行で書きます。DEFAULT_RUNTIME、AFFECTED、NEEDS_RUNTIMECLASS、BLAST_RADIUSです。

toolkitが、既定のランタイムを丸ごとnvidiaに書き換えておく構成が一般的です。PodごとにruntimeClassNameを書かなくてよいので便利ですが、GPUを使わないPodまでそのランタイムを通ることになります。その設定が壊れると、GPU Podだけでなく、そのノードのすべてのPodが起動できなくなります。4行の値は、それぞれランタイム名、影響を受けるPodの範囲、PodごとにRuntimeClassが必要かどうか、そして障害が及ぶ範囲を、1単語で書いてください。

作成したチェッカーを、実際のdumpにかけてみる

/root/gpurt/fixtures/dump-node1.tomlに、一部のハンドラーだけが読み込まれたノードのdumpを模して書いてください。そのあと、ステップ6のスクリプトをそのファイルにかけて、出力を/root/gpurt/out/crosscheck.txtに保存してください。

このfixtureは、「ファイルにはすべて書かれているのに、読み込まれたものは一部だけ」というノードを真似るものです。runcは必ず入っている必要があり、クラスターのRuntimeClassが要求するハンドラーのうち、一部は欠けている必要があります。すべて入っていると、ずれがなくなり、このステップの意味が生きません。採点ツールは、このfixtureとクラスターを自分で照合して、どのハンドラーが欠けているかを計算したうえで、あなたの出力と突き合わせます。