[Summarin](/)の要約は、Google Cloud Run の上で llama.cpp を動かして処理しています。個人開発でGPUサーバーを常時動かすのは現実的ではないため、アクセスが無いときは止まる構成を選びました。この記事では、その構成で実用的な速度を出すために考えたことを書きます。
なぜCloud Runなのか
最初から Cloud Run ありきだったわけではなく、いくつか比べたうえでの選択でした。
自宅サーバーも考えました。機材の面では一番安く済みます。しかし外部に公開するとなると、ネットワークの構築が大きな課題になります。自宅の回線を外に開けることになるため、セキュリティの面でも現実的ではないと判断しました。
GCE(仮想マシン)も候補でしたが、公開する以上はセキュリティの考慮が必要になる点は同じです。加えて、インスタンスの稼働を細かく制御しにくいのも気になりました。使われていない時間も動かし続けることになり、費用がかさみます。
結果として、比較的容易に始められるという理由で Cloud Run を選びました。
- アクセスが無い間はインスタンスが停止し、費用がかからない
- コンテナをそのまま動かせるので、llama.cppのような仕組みも載せられる
- 公開のためのネットワーク構築を、ほとんど自分で用意しなくてよい
最大の課題はコールドスタート
止まる構成の代償が、最初のリクエストで起動を待たせることです。しかも言語モデルの場合、コンテナが立ち上がっただけでは足りません。モデルをメモリへ展開し終えるまで、推論は始められません。
ここが一番の課題でした。やったことは2つあります。
1. モデルをイメージに焼き込む
起動のたびにモデルをダウンロードしていては、そのぶん待ち時間が伸びます。そこで、ビルドの段階でモデルを用意し、コンテナイメージに含めてしまうことで、起動時のダウンロードをなくしました。
Summarinが使っているのはチューニング済みのモデルです。手元にあるファイルをイメージに入れるだけなので、ビルド時間そのものは増えません。 ただし、リポジトリへpushする時間はモデルを増やすほど長くなります。 ここは避けられないので、モデルを追加するときのコストとして受け入れています。
一時期、チューニングしていない既成のモデルを使うことも検討しました。しかしこの場合はビルド時にモデルのダウンロードが走るため、ビルド時間が伸び、そのうえpushの時間も伸びるという悪循環になります。結局、手元で用意したモデルを焼き込む形に落ち着きました。
2. 複数モデルを並列で展開する
Summarinでは2種類のモデルを選べるようにしています。これを順番に読み込むと、モデルを増やすほど起動が遅くなります。待ち時間がモデルの数だけ積み上がっていく形です。
そこで、起動時にすべてのモデルを並列でメモリへ展開するようにしました。厳密な計測はしていませんが、理屈のうえでは、待ち時間は「全モデルの合計」ではなく「最も時間のかかる1つ」に近づきます。モデルを増やしても起動の遅れが積み上がらないので、選べるモデルを増やしたいという要求と、起動を遅くしたくないという要求を両立できます。
量子化とメモリの決め方
モデルはGGUF形式に変換し、量子化した状態で使っています。計算に使う数値の精度を落とすことで、必要なメモリを大きく減らせます。
量子化の種類は、理屈で決めるのではなく実際に作って比べました。 チューニング済みモデルを複数の量子化方式で書き出し、それぞれの精度を検証したうえで、その時点でもっとも結果の良かったものを選んでいます。モデルを更新するたびに、この比較をやり直しています。
インスタンスに割り当てるメモリ量も、この検証から決めました。 精度を検証している最中のメモリ使用量を見て、そこから必要な量を逆算しています。余裕を持たせすぎれば費用が増え、足りなければ起動に失敗するので、実測から決めるのが確実でした。
スレッド数は自動検出に任せない
llama.cppは既定で物理コア数を自動検出します。ただしコンテナ環境では、検出した値と実際に割り当てられたvCPUがずれることがあります。 そこでSummarinでは、環境変数でスレッド数を明示的に指定できるようにしました。
実際にずれて困ったわけではなく、念のための備えです。コンテナの設定を変えたときに、気づかないまま性能が落ちているという事態を避けたかったためです。
頻度制限をどう入れたか
無料で公開している以上、短時間に集中してアクセスされると費用にも応答速度にも響きます。そこで、同一IPからのリクエストを60秒あたり10回までに制限しています。
この数字に厳密な根拠はなく、決め打ちです。考え方としては次のとおりです。
- 要約には一定の処理時間がかかるため、人が普通に使っているかぎり、この制限には引っかかりません
- 弾きたいのは、機械的に連続でリクエストを送ってくるものだけです
利用者の邪魔をせず、機械的なアクセスだけを止められればよい。その線引きとして、この程度なら問題ないだろうという水準に決めました。
まとめ
この構成で一番の課題は、やはりインスタンスの立ち上がりに時間がかかり、すぐに処理を始められないことでした。それでいて、選べるモデルは複数用意したい。この2つは本来ぶつかり合う要求です。
モデルの展開を並列化したことで、待ち時間を大きく減らしながら複数モデルを提供できるようになりました。この構成を選んだのは正解だったと考えています。
費用の面でも、Cloud Runにしたことでネットワーク構築の手間を省けたうえ、使われていない間はインスタンスが止まるため、開発にかかる費用を抑えられています。個人開発でローカルLLMを公開する方法としては、現時点で現実的な選択肢だと思います。
要約の仕組みそのものについてはなぜローカルLLMで要約サービスを作ったのかに書いています。
