リングを p4 へ移す

エンジンと呼び出し元のあいだで起きた 7 つの契約の食い違い。どれも違うかたちで壊れ、そのうちエラーらしく見えたのは 1 つだけだった。

p4 エンジンの新しいリリースをマージしたら、リングが提供をやめた。クラッシュではない——ローダーは成功を報告し、エージェントは ready を報告し、そして何も起きなかった。その沈黙から遡るのに 1 日かかり、我々の呼び出し元とエンジンが乖離していた箇所が 7 つ出てきた。書き留めるに値するのはその数ではない。7 つのうち 6 つがエラーを出さなかったことだ。

失敗その 1

存在しないノード宛のイベントは、拒否されず転送される

我々のローダーは LOAD コマンドを、これから作ろうとしているノード宛に送っていた。しかしノードは LOAD が作るまで存在せず、未知のノードを名指すイベントに対するブローカーの規則は、拒否ではなく外向きに転送することだった。コマンドはエージェントを離れて行き先を探し、どこにも見つからず、捨てられた。ログ行は出ない。ブローカーの視点からは何も間違っていなかったからだ。

修正は、LOAD をエンジンのバックエンド非依存なライフサイクル封筒に包み、アダプタ自身のコマンドを不透明な本体として、*エージェント*宛に送ることだった。後から見れば当たり前で、外からはまったく見えない。

失敗その 2

別の数字と等しくなければならない数字

モデルは load generation のもとでロードされ、各ノードは node generation とともに登録される。我々はこの 2 つを独立したものとして扱っていた——片方はタイムスタンプ、もう片方は 1——それで何もかも動いていた。リングはロードされた。リクエストにも正しく答えた。そのあと先頭ノードが死んだ。

アダプタのリリース会計における、その検査。

let Endpoint::Node { generation, .. } = &event.envelope.source;
if *generation != receipt.load_generation {
    return Err("release owner census generation differs from source");
}

終了したリクエストを締めるレシートは load generation を運び、それを送るノードは自分のものを運ぶ。両者が異なればノードは停止される。つまりこのバグのかたちはこうだ:ロードは成功する、最初のリクエストも成功する、先頭が死ぬ、以後のセッションはすべて半ロードのままハングする。負荷下でのクラッシュにそっくりで、不一致にはまるで見えない。我々のローダーはいまや、2 つの数字が食い違うプランを、何かがロードされる前に拒否する。

失敗その 3

決して満たされえない上限

アダプタは、ステージが返しうる結果の最大値を、ステージサーバーが起動時に報告する値と完全一致で比較する。その値はモデルをロードしなければ知りようがない——だから当て推量でロードしたところ、失敗が 4 ステージ分の本当の数字を一度に教えてくれた:

1 回の実行、4 つの答え。

step37-s0: profile=33554432, READY=34419218444
step37-s1: profile=33554432, READY=34419218444
step37-s2: profile=33554432, READY=34419218444
step37-s3: profile=33554432, READY=59136012

34 GB。エージェントの保持ストアは 256 MiB なので、このリングは決して受理されえなかった。ステージサーバーから導出を読み出すと理由がわかった:この上限は n_batch × n_ubatch に比例し、我々はこの検査より前の設定から 2048 というバッチ幅を引き継いでいた。本番レイアウトが使う幅である 128 行なら、上限は 138 MB で収まる。いまは覚えていた定数を持ち回る代わりに、同じ式からプランの中で導出している。そして末尾ステージの予測値は、別のデプロイが記録していた数字とぴたり一致した——20 分かかるロードに踏み出す前に得ておきたい種類の一致だ。

残りの 4 つ

手短に

  • 2 ホストのリングにおけるループバックアドレス。ステージは次のステージのエージェントが広告しているアドレスへダイヤルする。127.0.0.1 を広告すればホスト A は自分自身へダイヤルする。このリングの記録された設定は最初からずっとループバックで、ホストをまたいで動くことは決してありえなかった。
  • ジャーナルは必須。エージェントの運用ジャーナルなしにモデルはロードされない。別のエラーを追っている最中にこれを切ってしまい、症状を悪化させた——しかも前進のように見えるかたちで。
  • デバイス名はバックエンド固有。リファレンスのプランビルダーは CUDA を対象とし --device CUDA0 を出す。HIP ビルドは自分のデバイスを ROCm0 と名づける。そのプランはモデル全体をロードし、*そのあとで*デバイスが見つからず失敗する。
  • ネイティブサーバーはリリースの一部。新しいツリーからビルドしたエージェントは、インストール済みのステージサーバーが報告しない capability を要求する。これもフルモデルロードのあとで発覚した。
そこから得たもの

沈黙こそが高くつく失敗モード

どれも修正は安く、発見は高くついた。パターンは一貫している:高くついたのは、正しく見えるシステムが何もしなかった場合か、一度だけ何かをした場合だった。我々が足したガードはすべて同じかたちをしている——間違いがまだ読み取れる場所で、早く拒否する。ローダーは最初のコマンドを送る*前に* load generation をディスクへ書く。さもなければ回復不能だからだ。generation の不一致は、最初のリクエストのあとで発見するのではなく拒否する。結果の上限は覚えるのではなく導出する。

リングは提供中だ。 2 台のマシンにまたがる 4 ステージ、常駐する重み 113 GiB、最初のトークンまでコールドで 1.4 秒・ウォームで約 0.3 秒、そしてノードごとの貢献が初めて精算台帳まで流れている。