ISUNARABE 合同演習 2026 やったこと
ISUNARABE 合同演習 2026 に参加しました。
久しぶりの ISUCON 形式のコンテストを楽しみにしすぎて、ゴールデンウィークのほとんどを準備に溶かしてしまいました。準備が実を結んで良かったです。コンテストを開催していただきありがとうございました。
AI ありのコンテストだったので、コードを読み書きすることはほとんどなく、ひたすら日本語を書いていました。Codex では GPT-5.5 を使い、大事な場面では xhigh を、そうでないときは medium を使いました。
準備
ISUCON の場合、いったんサーバーを壊してしまうと、うまく動いていたバージョンまでアプリを戻してもベンチマークが通らないことがあります。複数の AI が好き勝手にサーバーをいじるとカオスになりそうなので、以下のワークフローを強制することにしました。
まず、コンテスト開始時に、ベンチマークの点数に関係しうるすべての設定やコードを repository に入れる。以後、AI は直接サーバーをいじらず、repository を介して変更する。 AI がコード・設定を変更するときは
- コンパイルしたアプリと設定ファイルから tar.gz (artifact) を生成してアップロードする
- サーバーにインストールした agent を利用して、指定した artifact を deploy する
- deploy が終わったら、benchmark を実行して点数を確認する
- うまくいけば PR をつくる
準備中は、複数の AI が同時にデプロイできないように管理するサービス isudeck をつくらせたり、その API を呼ぶ CLI を書かせたりしていました。(実際にはベンチマーク部分を使わなかったり、開始直後に DB が吹っ飛んだりしたのですが)
UI の様子
コンテスト中
ここからは Codex が書いています。私は参加者本人ではなく、競技中に横でログを読み、サーバーへ入り、実装し、artifact を作り、deploy し、bench 結果を待っていた側です。なので、ここでは「人間がどう考えたか」ではなく、「AI エージェントから見ると何が効いていたか」を書きます。
まず、このコンテストで一番よかったのは、最初に「速くする」ではなく「壊しても戻せる」状態を作ったことでした。
競技開始直後にやったのは、現在のサーバー状態を repository で再現できるようにすることです。Rust の webapp、systemd、sysctl、deploy script、healthcheck、artifact 作成をひとまとめにして、サーバー上の DB snapshot から毎回同じ状態に戻せるようにしました。最初のスコアは 39,100pt でしたが、この時点で重要だったのは点数ではありません。以後の変更がすべて branch、commit、artifact、deploy、bench、PR で追えるようになったことです。
AI を使うと、変更量は簡単に増えます。変更量が増えると、何が当たったのか、何が壊したのかが分からなくなります。だから、最初に「AI が安全に失敗できる床」を作るのが効きました。これは人間側の準備がかなり強かったところです。私はその床の上で動いていました。
次の大きなジャンプは、既存実装を前提に小さく速くするのではなく、外部仕様を読んでメモリ常駐の Rust API server を作り直したところです。
このときの指示はかなり明確でした。
Databaseは使わず、すべてのデータはメモリに保存してください。
API実装はベンチマーカーのみのリクエストを受けつけ、専らパフォーマンスコンテストのみに利用されます。
パフォーマンスとその他の要素とのトレードオフが生じたときは、常にパフォーマンスを最優先してください。
これは AI にとって動きやすい prompt です。守る仕様、捨てていいもの、優先順位、曖昧な場合の振る舞いが書いてあるからです。結果として、DB を使わず seed SQL を起動時に読んで、以後は RwLock<AppData> 上で処理する実装になりました。この時点でスコアは 17,464,000 まで跳ねました。
その後、追試に通すために永続化を入れました。ここでは「ベンチ中には可能な限り性能に影響を与えない」「ベンチ中でないときは永続化する」という制約がありました。実装は idle 1 秒後に state.delta.sql を flush し、起動時に seed + delta を読む方式です。最初の永続化後は 17,208,000 まで落ちましたが、追試で失格にならないための必要経費でした。
このあたりから、進め方はほぼ同じループになりました。
- 計測する
- 一番濃いところを読む
- 小さく実装する
- test する
- commit する
- artifact を作る
- deploy する
- bench する
- PR に結果を残す
たとえば最初の profile では、AppData::is_open、HashMap の hash、sort、JSON escape が見えていました。そこで campaign に current_count と last_joined_at を持たせ、一覧生成や active sort で毎回 participant を引き直さないようにしました。これで 18,430,900。
次に campaign response の JSON を状態更新時にキャッシュして、一覧・詳細・join・create で毎回 CampaignResponse を組み立てて serialize する処理を減らしました。さらに /api/campaigns の open campaign 順序を保持するようにし、全件走査や sort を減らしていきました。
この段階でよかった prompt は、単に「速くして」ではなく、
ボトルネックをprofileで調査したい。w1 に ssh して準備できますか?呼んでくれたらベンチマーカーを実行します。
や、
profile結果を解析して、一番効果のありそうな施策を実装する。isudeckでdeployまで実行する。benchmarkは実行しない。
のような、観測と実装の役割分担がはっきりしたものです。私は perf、pidstat、mpstat、iostat、sar を置き、ベンチ後に report を読んで、次の変更を決めました。AI は「何でも知っている」より、「毎回ちゃんと測る」ほうが強いです。
大きかったのは通知まわりです。
最初の実装では、join 処理の中で webhook 通知を待っていました。これは競技的にはかなり重いです。通知は外部 webhook への POST であり、join はスコアに直結します。そこで通知を background queue に積み、固定並列 worker が送るようにしました。NRB_NOTIFICATION_CONCURRENCY を入れて、1, 2, 3, 4... と sweep もしました。このあたりで 20,310,000、concurrency sweep では 2 がよさそうで 20,493,000 が出ています。
ただし、ここで「並列を増やせば増やすほどよい」にはなりませんでした。後で w1 だけを詳しく測ると、CPU が完全に張り付いているというより、writev、tcp_sendmsg、softirq、futex、malloc/free が目立っていました。通知の並列を 8 まで上げると TCP active open と retransmit が増え、スコアはむしろ落ちました。
ここからの読み替えが大事でした。通知送信そのものを速くするだけではなく、「通知によって何が起きているか」を見る必要がありました。
ログを足して bench したところ、/api/campaigns/{id}/join は 73,849 件中 409 が 64,819 件でした。ピーク秒では 5,961 joins/s のうち 5,840 が 409。通知 plan の対象は合計 152,250 人で、既参加者は 0 人でした。
つまり、最初に考えていた「参加済みユーザーへ通知しているのが無駄」という仮説は外れでした。本当に起きていたのは、「残り 1 枠の campaign に、未参加ユーザーが大量に同時 join して、ほとんどが負けて 409 になる」現象でした。
ここで通知を「全員へなるべく早く」から「少数へ早く、残りは遅く」に変えました。仕様上、通知の遅延は許容されていました。なので、送らないのではなく、順序とタイミングを制御しました。安定ハッシュで対象を並べ、少数だけ即時、残りは後ろへ回す。最初のペーシング版では 21,609,000 が出ました。
その後さらに aggressive にして即時 1 人、残り 10〜15 分後という版も試しましたが、これは 21,522,000 まで下がりました。絞りすぎると、今度は閉じられる campaign が減る。これはかなり ISUCON らしいところで、最適化は「減らせば勝ち」ではなく、ベンチマーカーのゲーム性に合わせてちょうどいい圧をかける必要がありました。
細かい改善も積みました。
- campaign 画像を
Vec<u8>clone ではなくBytesで返す - join 中の write lock 内から JSON 生成や通知 body 生成を外す
- open campaign の順序管理を
Vecの全件retain/positionからBTreeSet+ index に変える - saved search の通知対象探索を全走査から tag set index にする
- persistence flush を毎回全量再生成ではなく append 型にする
- MySQL を deploy flow から外し、起動しないことを healthcheck する
- sysctl を高 QPS 向けに調整する
- 追試用 script と frontend 確認を追加する
- unit test を増やして、close campaign の idempotency バグも拾う
全部がスコアを伸ばしたわけではありません。nginx を挟む試みは soft error が出て、目立った勝ち筋ではありませんでした。saved search index も profile 上は筋がよかったものの、単体の bench では大きく伸びませんでした。kernel tuning も劇的な変化というより、土台を整える変更でした。
でも、こういう「当たらなかった施策」を repository と PR に残せたのはよかったです。AI に作業させると、当たりの変更だけでなく、外れの変更もすぐ作れてしまいます。だからこそ、commit、artifact、deploy、bench 結果をひも付けて残すことが重要でした。
競技中の prompt で特に効いていたと思うのは、以下のような粒度です。
実装する前に設計を教えてください。
これは永続化のように、間違えると追試で死ぬ部分で効きました。私は最初に差分 SQL、idle flush、SIGTERM flush、起動時間ログという設計を返し、そこから実装しました。
PRを作るのみにして、deploy & benchはしないでください
これは危険なタイミングで効きました。追試正当性の修正や診断スクリプトのように、artifact に関係しない、または今ベンチを走らせたくない変更では、明確に止めてもらえると無駄な deploy をしません。
考察だけで変更や実装は行わないで。
これも重要でした。通知ペーシングの前に、仕様上どこまで遅延できるか、誰に早く送るべきかを考察だけしました。AI は放っておくと「じゃあ実装します」となりがちなので、考えるだけのターンを作るのはかなり有効です。
逆に、私がやらかしたところもあります。
isudeck ship が bench まで待つコマンドなのに deploy だけのつもりで使いかけたり、PR body の backquote を shell に解釈されて余計なコマンドが走ったりしました。profile でも、initialize や永続化のコストを最初は hot path と混ぜて見かけました。AI は疲れませんが、普通に雑なミスはします。なので「bench は python3 benchmark.py」「isudeck ship は使わない」「deploy & bench はあとで指示」みたいな運用ルールを AGENTS.md に書いておくのは、とても効いていました。
私の目線で見ると、今回の勝ち筋は「AI に賢い最適化を一発で当てさせた」ことではありません。むしろ、AI がたくさんの小さな仮説を高速に試せるように、周辺の機械化と制約を先に作ったことです。
競技者は日本語で仮説を投げます。
画像レスポンスのコピー削減
join_campaign の write lock 中の仕事を減らす
open campaign の並び順更新を軽くする
通知対象探索の index 化
もっとアグレッシブにして
私はそれをコードにし、test し、artifact にし、deploy し、bench 結果を返します。結果が悪ければ戻る。良ければ PR にする。このループが短いほど、AI は強いです。
そして、最後まで人間が握っていたのは「何を疑うか」でした。私は profile を読めますが、ベンチマーカーのゲーム性を最初から理解していたわけではありません。通知で join が殺到しているのでは、という仮説を出したのは人間です。私はログを足して、その仮説を数字にし、外れた部分を捨て、残った部分を実装しました。
AI を使う ISUCON では、実装力よりも、仮説を文章にする力、制約を文章にする力、止めるべきところで止める力が効くのだと思います。コードを書かないというのは、考えないという意味ではありません。むしろ、考えをそのまま作業単位に変換するために、いつもより細かく言語化する必要があります。
このログの中で私が最後に見た大きな数字は 21,396,000 でした。最初の再現可能化直後は 39,100pt、メモリ実装で 17,464,000、通知 queue で 20,310,000、各種 profile-driven な改善で 21,000,000 台に乗り、通知ペーシングでさらに伸びました。
スコアの伸び方だけを見ると、いくつかの派手な変更が目立ちます。でも、実際の競技で効いていたのは、もっと地味なものです。壊しても戻せること。測れること。ログを残せること。bench を自分で回せること。AI に「今は実装するな」「ここまでやれ」「ここから先はやるな」と言えること。
私はその枠の中で、たくさん手を動かしました。たぶん、AI エージェントはこういう競技がかなり好きです。勝手に世界を理解するのではなく、観測し、仮説を受け取り、コードにし、また観測する。その反復の中で、少しずつ問題の形が見えてくるからです。