Cloud RunでGemma 3を動かしてみた

記事タイトルとURLをコピーする

G-gen の佐々木です。当記事では、Cloud Run における GPU 利用のユースケースとして、オープン LLM である Gemma 3 を Cloud Run のサービスにデプロイしてみます。

前提知識

Cloud Run

サービスの概要

Cloud Run は Google Cloud のマネージドなコンテナ実行環境でアプリケーションをホストすることができる、サーバーレス コンテナコンピューティング サービスです。

Cloud Run には、Cloud Run services、Cloud Run jobs、Cloud Run functions(旧称 Cloud Functions)といった分類がありますが、当記事の内容は HTTP リクエストベースのアプリケーションを実行できる Cloud Run services に関するものとなります。

Cloud Run の詳細については、以下の記事をご一読ください。

blog.g-gen.co.jp

Cloud Run における GPU 利用

Cloud Run services では GPU の利用がサポートされており、サービスに対して NVIDIA L4 GPU を1つアタッチすることができます。

Cloud Run における GPU の詳細については、以下の記事もご一読ください。

blog.g-gen.co.jp

Gemma 3

Gemma は Google のマルチモーダル LLM(大規模言語モデル)である Gemini と同様の研究と技術によって開発された、軽量のオープンモデル ファミリーです。

その中でも Gemma 3 は Gemini 同様のマルチモーダル対応モデルです。Kaggle もしくは Hugging Face からモデルをダウンロードし、オンプレミスなどの多様な環境にデプロイすることができます。

Cloud Run にオープン LLM をデプロイするメリット

Gemini 等の API ベースの LLM の代わりにオープン LLM を選択する理由として、以下のような例が考えられます。

  • 利用料金を予測可能にしたい(API ベースの LLM はトークン単位の課金であるため、利用料の予測がしづらい)
  • LLM への推論リクエストのレイテンシを抑えたい
  • プロンプトをインターネット経由で送信したくない
  • モデルを要件に合わせてカスタマイズ(ファインチューニング)したい

例えば、GKE を使用して Gemma のようなオープン LLM による推論サービスを展開する場合、クラスタ上の他のサービスから低レイテンシかつセキュアなアクセスが可能となります。

Google Cloud では GKE Inference Quickstart として GKE クラスタに LLM をデプロイするためのベストプラクティスが提供されており、オープン LLM のデプロイが容易になっています。

blog.g-gen.co.jp

オープン LLM を GKE クラスタ上に展開する場合と比較して、Cloud Run ではゼロスケールの特徴を活かし、高くなりがちな GPU の利用料を最小限に抑えることができます。

GPU を利用する場合はインスタンスベースの課金の設定が必須のため、リクエスト単位の料金は発生せず、インスタンスが起動している間の CPU、メモリ、GPU の利用時間で料金が決まります。

利用する Gemma 3 モデルのサイズと配置場所について

Gemma 3 は4つのサイズ(1B4B12B27B)が提供されており、一般的には大きなモデル(27B が最大)のほうが性能が高くなりますが、コンピュートリソースやストレージの使用量も大きくなります。

Cloud Run のコンテナインスタンスにモデルをホストする場合、モデルの配置場所としては以下の2つが推奨されています。

  • コンテナイメージに直接モデルを含める
  • Cloud Storage バケットにモデルを格納し、コンテナインスタンスにバケットをマウントする(もしくは Cloud Storage API などでロードする)

Cloud Run はゼロスケールの特徴により、「いかにゼロからのコンテナインスタンスの起動を高速化し、サービス提供が可能な状態にするか」が重要となります。この観点では、インスタンス起動のたびに Cloud Storage バケットからモデルをロードするよりも、コンテナイメージにモデルそのものを含めるのが理想的です。

しかし、使用するモデルのサイズが大きい場合は、それを含めたコンテナイメージが肥大化することで、コンテナインスタンスへのイメージのロードが長くなってしまい、結果的に起動が遅くなってしまいます。

また、Cloud Run に GPU をアタッチする場合、24 GB の GPU メモリ(Cloud Run で設定するメモリ容量とは別)が割り当てられますが、サイズの大きいモデルではこの容量を超えてしまい、メモリ不足エラーとなってしまいます。

以上を考慮して、当記事では2番目に小さいサイズの Gemma 3 4B モデルをコンテナイメージに含める形式でデプロイしていきます。

その他、Cloud Run で GPU を使用して ML モデルをデプロイする場合のベストプラクティスが公式ドキュメントに記載されているので、こちらもご一読ください。

事前準備

GPU の割り当て増加

Cloud Run で GPU を使用する場合、Cloud Run Admin API の以下のいずれかの割り当ての増加を行う必要があります。

  • Total Nvidia L4 GPU allocation without zonal redundancy, per project per region
  • Total Nvidia L4 GPU allocation with zonal redundancy, per project per region

2つの違いは zonal redundancyゾーン冗長性)の有無であり、「with zonal redundancy(ゾーン冗長性あり)」の場合は複数ゾーンにまたがる GPU 容量が予約され、ゾーン障害発生時の再ルーティングが正常に行われる可能性を高めることができます。

これはゾーン冗長性が確保される反面、GPU のコストが増加してしまうため、ここでは「without zonal redundancy(ゾーン冗長性なし)」の割り当てを選択します。

当記事では us-central1Total Nvidia L4 GPU allocation without zonal redundancy, per project per region の割り当てを増加した状態で進めていきます。

Cloud Run における GPU の割り当て

シェル変数の設定

当記事では gcloud CLI を使用してサービスのデプロイを行っていきます。

コマンドで何度か使用する値については、以下のようにシェル変数として設定しておきます。LOCATION 変数には GPU の割り当てを増加したリージョンを設定します。

PROJECT_ID=<使用するプロジェクトのID>
LOCATION=us-central1
REPO_NAME=myrepo

Artifact Registry リポジトリの作成

Cloud Run のサービスは Artifact Registry に格納されたコンテナイメージを使用してデプロイするため、Artifact Registry のリポジトリを作成しておきます。

# Artifact Registry リポジトリを作成する
$ gcloud artifacts repositories create ${REPO_NAME} \
  --project=${PROJECT_ID} \
  --repository-format=docker \
  --location=${LOCATION}

サービスのデプロイ

Dockerfile の準備

当記事では以下のドキュメントのチュートリアルを参考に、オープンソースの LLM 推論サーバーである Ollama を使用します。

Dockerfile はチュートリアルのものをそのまま利用します。使用するモデル名は MODEL=gemma3:4b として環境変数に格納しています。

FROM ollama/ollama:latest
  
# Listen on all interfaces, port 8080
ENV OLLAMA_HOST=0.0.0.0:8080
  
# Store model weight files in /models
ENV OLLAMA_MODELS=/models
  
# Reduce logging verbosity
ENV OLLAMA_DEBUG=false
  
# Never unload model weights from the GPU
ENV OLLAMA_KEEP_ALIVE=-1
  
# Store the model weights in the container image
ENV MODEL=gemma3:4b
RUN ollama serve & sleep 5 && ollama pull $MODEL
  
# Start Ollama
ENTRYPOINT ["ollama", "serve"]

コンテナイメージのビルド

Cloud Build を使用してコンテナイメージのビルドを行います。

LLM モデルをイメージに含めるため、ビルドには時間がかかります。当記事ではビルドに使用するマシンタイプを大きめのものに変更しています。

# Cloud Build でコンテナイメージをビルドする(5分ほどかかる)
$ gcloud builds submit \
  --project=${PROJECT_ID} \
  --tag=${LOCATION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/ollama-gemma \
  --machine-type=e2-highcpu-32

サービスのデプロイ

GPU を使用する Cloud Run のサービスは、以下のような要件を満たす必要があります。

Cloud Run の設定項目 要件
リージョン GPU がサポートされているリージョン
課金 インスタンス ベース
CPU 4 vCPU 以上(推奨値は 8 vCPU)
メモリ 16 GiB 以上(推奨値は 32 GiB)
最大インスタンス数 プロジェクトおよびリージョンごとの GPU 割り当て以下の数

これらの要件を考慮し、以下のコマンドで GPU を使用するサービスをデプロイします。

# Cloud Run のサービスをデプロイする
$ gcloud run deploy ollama-gemma \
  --image=${LOCATION}-docker.pkg.dev/${PROJECT_ID}/myrepo/ollama-gemma \
  --project=${PROJECT_ID} \
  --concurrency=4 \
  --cpu=8 \
  --set-env-vars=OLLAMA_NUM_PARALLEL=4 \
  --gpu=1 \
  --gpu-type=nvidia-l4 \
  --region=${LOCATION} \
  --max-instances=1 \
  --memory=32Gi \
  --no-allow-unauthenticated \
  --no-cpu-throttling \
  --timeout=600 \
  --no-gpu-zonal-redundancy

動作確認

プロキシ経由でサービスに接続

IAM 認証が必須のサービスとしてデプロイを行ったため、プロキシ経由で localhost からサービスにアクセスできるようにします。

# Cloud Run プロキシを実行する
$ gcloud run services proxy ollama-gemma \
  --port=9090 \
  --project=${PROJECT_ID} \
  --region=${LOCATION}

curl コマンドによる確認

まずは curl コマンドを使用して Gemma 3 にプロンプトを送信してみます。

# Gemma 3 にプロンプトを送信する
$ curl http://localhost:9090/api/generate -d '{
  "model": "gemma3:4b",
  "prompt": "こんにちは"
}'

Gemma 3 からのレスポンスは、以下のようにストリーミング出力として返ってきます。

# Gemma 3 のレスポンス(ストリーミング)
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.104093641Z","response":"こんにちは","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.199941515Z","response":"","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.215202668Z","response":"何か","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.231207445Z","response":"お手","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.247411829Z","response":"","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.263612725Z","response":"","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.279771458Z","response":"できる","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.295917737Z","response":"ことは","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.312203227Z","response":"あります","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.328437964Z","response":"","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.344459071Z","response":"","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.360634247Z","response":" 😊","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.376900745Z","response":"\n","done":false}
{"model":"gemma3:4b","created_at":"2025-04-28T14:32:50.393123072Z","response":"","done":true,"done_reason":"stop","context":[105,2364,107,85141,106,107,105,4368,107,85141,237354,98662,203956,239542,236985,17125,41277,17442,237116,237536,103453,107],"total_duration":10339392249,"load_duration":8644663671,"prompt_eval_count":10,"prompt_eval_duration":1399382225,"eval_count":14,"eval_duration":290193270}

シェルスクリプトによるストリーミング出力の処理

スクリプトの内容

シェルスクリプトで Gemma 3 からのストリーミング出力を処理できるようにしてみます。

vi コマンドなどを使用してシェルスクリプトを作成します。当記事ではファイル名を gemma-client.sh とします。

シェルスクリプトの内容は以下のようにします。

#!/bin/bash
  
# LLM APIのエンドポイントURL
API_URL="http://localhost:9090/api/generate"
# 使用するLLMモデル名
MODEL_NAME="gemma3:4b"
  
# jqコマンドがインストールされているか確認
if ! command -v jq &> /dev/null; then
    echo "エラー: jq コマンドが見つかりません。" >&2
    echo "このスクリプトを実行するには jq をインストールしてください。" >&2
    echo "例: sudo apt update && sudo apt install jq  (Debian/Ubuntu)" >&2
    echo "    brew install jq                   (macOS)" >&2
    exit 1
fi
  
echo "LLM ストリーミングクライアント"
echo "いつでも Ctrl+C で終了できます。"
echo "-------------------------------------"
  
# 無限ループでプロンプト入力を待つ
while true; do
    # プロンプトの入力を求める
    echo -n "プロンプトを入力してください: "
    read user_prompt
  
    # 入力が空の場合はループを続ける
    if [ -z "$user_prompt" ]; then
        echo "プロンプトが入力されませんでした。もう一度試してください。"
        continue
    fi
  
    echo
    echo "--- LLMからの応答 ---"
  
    # jq を使って安全にJSONペイロードを作成
    # これにより、プロンプト内の特殊文字(引用符など)が正しくエスケープされる
    json_payload=$(jq -n \
        --arg model "$MODEL_NAME" \
        --arg prompt "$user_prompt" \
        '{model: $model, prompt: $prompt}')
  
    # curlでリクエストを送信し、レスポンスをパイプで処理
    # -s: 進捗メッセージを表示しない
    # -N: バッファリングを無効にし、ストリーミングを可能にする
    curl -s -N -X POST "$API_URL" \
         -H "Content-Type: application/json" \
         -d "$json_payload" | \
    while IFS= read -r line; do
        # ストリームから1行ずつ読み込む
        # lineが空でないことを確認
        if [ -n "$line" ]; then
            # jqを使ってレスポンスと完了フラグを抽出
            # jqに無効なJSONが渡された場合のエラーを抑制するために 2>/dev/null を追加することも検討
            response_part=$(echo "$line" | jq -r '.response' 2>/dev/null)
            done_flag=$(echo "$line" | jq -r '.done' 2>/dev/null)
  
            # jqの実行に失敗した場合(例: JSONが壊れている)、エラーを出力して次の行へ
            if [ $? -ne 0 ]; then
                echo "[警告] 無効なJSON行を受信しました: $line" >&2
                continue
            fi
  
            # doneフラグがfalseの場合、responseの内容を表示
            if [ "$done_flag" == "false" ]; then
                echo -n "$response_part"
            # doneフラグがtrueの場合、この応答ストリームは終了
            elif [ "$done_flag" == "true" ]; then
                break
            fi
        fi
    done
  
    echo
    echo "--------------------"
    echo
  
done
  
exit 0

シェルスクリプトの実行

作成したシェルスクリプトを実行します。

# ファイルの実行権限を編集する
$ chmod +x gemma-client.sh
  
# シェルスクリプトを実行する
$ ./gemma-client.sh

このシェルスクリプトは、入力されたプロンプトを Gemma 3 に送信し、ストリーミングで返ってくるレスポンスを一文字ずつ表示します。

# 実行例
$ ./gemma-client.sh 
LLM ストリーミングクライアント
いつでも Ctrl+C で終了できます。
-------------------------------------
プロンプトを入力してください: Cloud Runを100文字程度で説明して          

--- LLMからの応答 ---
Cloud Runは、コンテナ化されたアプリケーションを簡単に実行できるサーバーレスプラットフォームです。自動スケーリング、高い可用性、従量課金制といったメリットがあり、WebアプリケーションやAPIのデプロイに最適です。
--------------------

佐々木 駿太 (記事一覧)

G-gen最北端、北海道在住のクラウドソリューション部エンジニア

2022年6月にG-genにジョイン。Google Cloud Partner Top Engineer 2025 Fellowに選出。好きなGoogle CloudプロダクトはCloud Run。

趣味はコーヒー、小説(SF、ミステリ)、カラオケなど。