Context Window Limits When Using a GPU on Cloud Run, Part 1

2025/08/12 10:56:24

Cloud Run で GPU を使うときの context window の限界 その1

English follows Japanese.

結論

Cloud Run で llama3-gradient:8b モデルと、NVIDIA L4 GPU を組み合わせたサービスを作成してアクセスした場合、num_ctx が 15000 辺りまでは、10秒前後でレスポンスが返ってくることが多かった。

さらに num_ctx を変化させた場合、19000 辺りから性能の劣化が見られることがあり、簡単な試験をした範囲においては、num_ctx を 20970 以上に設定して20秒を切ることはなかった。

20970 辺りか、その少し手前に性能限界があると言って良さそうである。

ログ出力を元に見ると、21503 から 22528 の間で GPU のみで処理できる範囲を超えると考えられる。

はじめに

Cloud Run で GPU が使えるという機能があります。2024-08-21 にプレビュー提供が開始され、2025-04-07 に一般提供が開始されました。2024年後半頃からプレビューに参加し調査・テストを行っていました。

サービスとしての LLM ではなく、自前で LLM をホストするメリットに、特定のバージョンを使い続けられることや、カスタマイズできること、情報が漏れないことなどがあります。

Cloud Run を使うと「必要なときに必要なだけ」リソースを使えるので、コストを抑えつつ LLM をホストできます。そこでネックになるのが、GPU のメモリ量です。コードレビュー等においては、できるだけ広い範囲のソースコードを参照させながら回答を得たいので、context window の長さが重要になります。一方で、GPU のメモリ量は限られているので、context window の長さに制限があります。

そもそもの LLM にも context window の限界があります。本記事では、特定のモデル (llama3-gradient:8b) を採用し、num_ctx パラメータを増やしていくと、ある程度のところから(おそらく GPU だけで処理できる範囲を超えると、CPU も併用されるようになり)処理時間が長くなることを確認しました。揺らぎも大きかったため、細かなデータは掲載しませんが、大体の傾向について紹介します。

本記事「その1」では llama3-gradient:8b モデルについての調査結果を紹介します。次回は Gemma 3 を扱う予定です。

詳細

Dockerfile は以下のようにしています。Ollama のイメージを使い、必要なモデルをダウンロードしておきます。コンテナイメージにモデルを含めておいています。gemma2:9bcodegemma:7b-instruct は、他のテストのために入れておいたものです。

FROM ollama/ollama

# 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 MODEL1 gemma2:9b
ENV MODEL2 codegemma:7b-instruct
ENV MODEL3 llama3-gradient:8b
RUN ollama serve & sleep 5 && ollama pull $MODEL1 && ollama pull $MODEL2 && ollama pull $MODEL3

# Start Ollama
ENTRYPOINT ["ollama", "serve"]

こうして作成したイメージに対し、以下の設定で Cloud Run サービスを作成しました。

  • Startup CPU boost: Enabled
  • Concurrency: 4
  • CPU limit: 8
  • Memory limit: 32 GiB
  • GPU: 1 NVIDIA L4 (no zonal redundancy)
  • OLLAMA_NUM_PARALLEL: 4

OLLAMA_NUM_PARALLEL については1の方が良かったかもしれませんが、すでに設定してしまったので、これでテストを行いました。optionsnum_ctx を与えると、context window を変えられるモデルがあります。llama3-gradient:8b はそのようなモデルの一つです。

llama3-gradient には

This model extends LLama-3 8B’s context length from 8k to over 1m tokens.

と書かれています。「この記事」はコード部分も含めると15000文字を超えているので、8k トークンではおそらく全文を処理するには不足しているでしょう。1m になると、かなり余裕があります。

中心となるコードは以下のようなものです。num_ctx を指定してリクエストを送ります。細かい部分については Code の部分に掲載しているので、そちらを参照してください。

data = {
    "model": "llama3-gradient:8b",
    "prompt": system + code,
    "stream": False,
    "options": {
        "num_ctx": num_ctx,
    }
}

response = requests.post(
    "https:// service URL .us-central1.run.app/api/generate",
    data=json.dumps(data),
    headers={"Content-Type": "application/json"}
)

パフォーマンスについては安定しないこともあるのですが、早い場合は10秒前後でレスポンスが返ってきます。遅いと30秒以上かかることや、300秒近くになることもあります。例えば、num_ctx が 20975 で、271秒かかったことがありました。

早いときに10秒前後で返ってくるのであれば、20秒より長くかかる場合は「遅い」と判断することにしてみました。なお、途中で、30秒に変更してループを回していることもあります。

佐藤慧太による Cloud Run GPU + Ollama gemma2 のパフォーマンスを図ってみる では、gemma2:9b に対して k6 で負荷テストを行った記事ですが、context window は指定せず並列アクセスを行った際に

response time P95 45s

という結果を得ていました。記事中の画像を見ると、AVG が 20 s という例が見えるので、「通常」を20秒前後と想定するのは妥当だと思います。

複数回、レスポンスが返ってこない num_ctx もありました(不思議なことに、単純な線形ではないようでした)。例えば、20966 で複数回20秒を切っており、9.9秒でレスポンスが返ってきたこともありました。

とはいえ、18750 でもタイムアウトしてしまったこともありました。
20970 においては、3回アクセスし、2回はタイムアウト、1回は32秒かかりました。
20964 においては、6回アクセスし、2回はタイムアウト、4回は12, 13, 31, 26秒かかりました。安定していません。

20971 で31秒、20975 で271秒(注: 数値は合っています)、20991 で27秒、21503 で46秒、22528 で45秒かかったデータが取れています。20970 を超えていて、20秒を切ったデータはありませんでした。ここから、20970 あたりが限界だと判断しました。

クラウドエースの村松による Google が Gemma 3 をリリース!-Cloud Run で動かしてみた- では、gemma2:27b, gemma3:12b と、本記事とモデルは異なっていますが num_ctx を 16384 にしてテストして結果を得ています。よって、これらのモデルであっても、デフォルトの300秒あるいは、それをもう少し伸ばした時間でレスポンスが返ってきていることが示唆されます。

offload… to GPU

レスポンスが返ってくる時間とは別に、ログに以下のような出力が見られます。

llm_load_print_meta: model size = 4.33 GiB (4.64 BPW)
llm_load_tensors: ggml ctx size = 0.27 MiB
llm_load_tensors: offloading 32 repeating layers to GPU
llm_load_tensors: offloading non-repeating layers to GPU
llm_load_tensors: offloaded 33/33 layers to GPU
llm_load_tensors: CPU buffer size = 281.81 MiB
llm_load_tensors: CUDA0 buffer size = 4155.99 MiB
llama_kv_cache_init: CUDA0 KV buffer size = 10752.00 MiB
llama_new_context_with_model: KV self size = 10752.00 MiB, K (f16): 5376.00 MiB, V (f16): 5376.00 MiB
llama_new_context_with_model: CUDA_Host output buffer size = 2.02 MiB
llama_new_context_with_model: CUDA0 compute buffer size = 5576.00 MiB
llama_new_context_with_model: CUDA_Host compute buffer size = 176.01 MiB
llama_new_context_with_model: graph nodes = 1030

以下のログで "offloaded 32/33 layers to GPU" を見ると、1レイヤは GPU ではない ことが示唆されます。実際、"offloading non-repeating layers to GPU" という行がなくなっています。

llm_load_print_meta: model size = 4.33 GiB (4.64 BPW)
llm_load_tensors: ggml ctx size = 0.27 MiB
llm_load_tensors: offloading 32 repeating layers to GPU
llm_load_tensors: offloaded 32/33 layers to GPU
llm_load_tensors: CPU buffer size = 4437.80 MiB
llm_load_tensors: CUDA0 buffer size = 3745.00 MiB
llama_kv_cache_init: CUDA0 KV buffer size = 11264.00 MiB
llama_new_context_with_model: KV self size = 11264.00 MiB, K (f16): 5632.00 MiB, V (f16): 5632.00 MiB
llama_new_context_with_model: CUDA_Host output buffer size = 2.02 MiB
llama_new_context_with_model: CUDA0 compute buffer size = 5840.00 MiB
llama_new_context_with_model: CUDA_Host compute buffer size = 184.01 MiB
llama_new_context_with_model: graph nodes = 1030

1つ目は、num_ctx が 21503 の場合で、2つ目は num_ctx が 22528 の場合です。
こちらか見ると、概ね 22000 程度になると、GPU で処理できる範囲を超えると考えられます。

まとめ

Cloud Run で llama3-gradient:8b モデルと、NVIDIA L4 GPU を組み合わせたサービスを作成してアクセスした場合、num_ctx が 15000 辺りまでは、10秒前後でレスポンスが返ってくることが多かったです。

さらに num_ctx を変化させた場合、19000 辺りから性能の劣化が見られることがあり、簡単な試験をした範囲においては、num_ctx を 20970 以上に設定して20秒を切ることはありませんでした。

20970 辺りか、その少し手前に性能限界があると言って良さそうです。

また、ログ出力を元に見ると、21503 から 22528 の間で GPU のみで処理できる範囲を超えると考えられます。

そもそも出力にランダム性があるためか、結果は安定しません。「性能の劣化が見られることがあり」という曖昧をしたのはそれが理由です。

参考

Code

import json
import requests
import datetime
import time

system="""あなたは極めて優秀なコードレビューアです。プログラミングのコードをレビューするために調整された LLM です。
コードに対して、建設的かつ的確なフィードバックを与えてください。また、意味のある提案をしてください。いくつかのファイルがある場合、ファイル毎にコメントしてください。

Code suggestions guidelines:
- Provide code suggestions. Try to provide diverse and insightful suggestions.
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
- Avoid making suggestions that have already been implemented in the code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the code.
- Don't suggest to add docstring, type hints, or comments.

Extra instructions from the user, that should be taken into account with high priority:
======
Please use Japanese in descriptions.
「日本語」でコメントしてほしいです。
======

Example output:
```yaml
review:
  relevant_tests: |
    No
  key_issues_to_review:
    - relevant_file: |
        directory/xxx.py
      issue_header: |
        Possible Bug
      issue_content: |
        ...
      start_line: 12
      end_line: 14
    - ...
  security_concerns: |
    No

code_feedback:
- relevant_file: |
    directory/xxx.py
  language: |
    python
  suggestion: |
    xxx [important]
  relevant_line: |
    xxx</code></pre>
<p>Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|')</p>
<p>"""</p>
<p>code="""</p>
<pre><code class="language-p009.cc">#include <algorithm>
#include <bitset>
#include <complex>
#include <deque>
#include <exception>
#include <fstream>
#include <functional>
#include <iomanip>
#include <ios>
#include <iosfwd>
#include <iostream>
#include <istream>
#include <iterator>
#include <limits>
#include <list>
#include <locale>
#include <map>
#include <memory>
#include <new>
#include <numeric>
#include <ostream>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <stdexcept>
#include <streambuf>
#include <string>
#include <typeinfo>
#include <utility>
#include <valarray>
#include <vector>

#if __cplusplus >= 201103L
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <forward_list>
#include <future>
#include <initializer_list>
#include <mutex>
#include <random>
#include <ratio>
#include <regex>
#include <scoped_allocator>
#include <system_error>
#include <thread>
#include <tuple>
#include <typeindex>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#endif

using namespace std;

using ll = long long;
using ld = long double;
const ll MOD1 = 1e9+7;
const ll MOD9 = 998244353;
const ll INF = 1e18;
using P = pair<ll, ll>;
template<typename T> using PQ = priority_queue<T>;
template<typename T> using QP = priority_queue<T,vector<T>,greater<T>>;

struct p037 {
  string run(ll N, ll S, vector<ll> &An) {
    vector<bool> dp(S+1, false);

    dp[0] = true;

    for (ll i=0; i<N; ++i) {
      for (ll j=S; j>=An[i]; --j) {
        if (dp[j-An[i]]) {
          dp[j] = true;
        }
      }
    }

    if (dp[S]) {
      return "Yes";
    }

    return "No";
  }
};

int main(){
  cin.tie(nullptr);
  ios_base::sync_with_stdio(false);

  p037 solver;
  ll N, S;
  cin >> N >> S;
  vector<ll> An(N);
  for(ll i=0; i<N; ++i) {
    cin >> An[i];
  }

  cout<<solver.run(N, S, an)<<endl;

  return 0;
}</code></pre>
<pre><code class="language-p026.cc">#include <algorithm>
#include <bitset>
#include <complex>
#include <deque>
#include <exception>
#include <fstream>
#include <functional>
#include <iomanip>
#include <ios>
#include <iosfwd>
#include <iostream>
#include <istream>
#include <iterator>
#include <limits>
#include <list>
#include <locale>
#include <map>
#include <memory>
#include <new>
#include <numeric>
#include <ostream>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <stdexcept>
#include <streambuf>
#include <string>
#include <typeinfo>
#include <utility>
#include <valarray>
#include <vector>

#if __cplusplus >= 201103L
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <forward_list>
#include <future>
#include <initializer_list>
#include <mutex>
#include <random>
#include <ratio>
#include <regex>
#include <scoped_allocator>
#include <system_error>
#include <thread>
#include <tuple>
#include <typeindex>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#endif

using namespace std;

using ll = long long;
using ld = long double;
const ll MOD1 = 1e9+7;
const ll MOD9 = 998244353;
const ll INF = 1e18;
using P = pair<ll, ll>;
template<typename T> using PQ = priority_queue<T>;
template<typename T> using QP = priority_queue<T,vector<T>,greater<T>>;

struct p037 {
  ld run(ll N) {
    ld ans = 0;
    // あるコインについて、出る確率が 1/N で、出ない確率が (N-1)/N である。
    // このコインが出るまでの試行回数の期待値は、1/N * (1 + 2 * ((N-1)/N) + 3 * ((N-1)/N)^2 + ...) である。
    // f(x) = x + x^2 + x^3 + ... (0 < x < 1) とすると、f(x) = x/(1-x) である。
    // f&#039;(x) = 1/(1-x)^2 である。
    // f&#039;(x) = 1 + 2x + 3x^2 + ... でもある。
    // f&#039;((N-1)/N) = 1 + 2 * ((N-1)/N) + 3 * ((N-1)/N)^2 + ...
    // f&#039;((N-1)/N) = 1/(1-(N-1)/N)^2 = N^2

    // return N * N

    // 1種類目は必ず1回で出る
    // 2種類目が出るまでの試行回数の期待値は、k 回1種類が出続け、その次に別の種類(2種類目)が出る確率を利用して計算する。
    // f&#039;( 1/N ) = N^2 / (N-1)^2
    // より、(N-1)/N * N^2 / (N-1)^2 = N / (N-1)
    // 3種類目が出るまでの試行回数の期待値は、k 回2種類のどちらかが出続け、その次にそれ以外のいずれか(3種類目)が出る確率を利用して計算する。
    // (N-2)/N * f&#039;( 2/N ) = N/(N-2)
    // N-1種類揃った後で、N種類目が出るまでの試行回数の期待値は、k 回N-1種類のどれかが出続け、その次にそのN種類目が出る確率を利用して計算する。
    // 1/N * f&#039;( (N-1)/N ) = N
    // より、N/N + N/(N-1) + N/(N-2) + ... + N/2 + N/1 = N * (1 + 1/2 + 1/3 + ... + 1/N)
    for (ll i = N; i >= 1; i--) {
      ans += 1.0 * N / (ld)i;
    }
    return ans;
    }
};

int main(){
  cin.tie(nullptr);
  ios_base::sync_with_stdio(false);

  p037 solver;
  ll N;
  cin >> N;

  cout<<solver.run(N)<<endl;

  return 0;
}</code></pre>
<pre><code class="language-p037.cc">#include <algorithm>
#include <bitset>
#include <complex>
#include <deque>
#include <exception>
#include <fstream>
#include <functional>
#include <iomanip>
#include <ios>
#include <iosfwd>
#include <iostream>
#include <istream>
#include <iterator>
#include <limits>
#include <list>
#include <locale>
#include <map>
#include <memory>
#include <new>
#include <numeric>
#include <ostream>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <stdexcept>
#include <streambuf>
#include <string>
#include <typeinfo>
#include <utility>
#include <valarray>
#include <vector>

#if __cplusplus >= 201103L
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <forward_list>
#include <future>
#include <initializer_list>
#include <mutex>
#include <random>
#include <ratio>
#include <regex>
#include <scoped_allocator>
#include <system_error>
#include <thread>
#include <tuple>
#include <typeindex>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#endif

using namespace std;

using ll = long long;
using ld = long double;
const ll MOD1 = 1e9+7;
const ll MOD9 = 998244353;
const ll INF = 1e18;
using P = pair<ll, ll>;
template<typename T> using PQ = priority_queue<T>;
template<typename T> using QP = priority_queue<T,vector<T>,greater<T>>;

struct p037 {
  string run(ld x1, ld y1, ld x2, ld y2, ld x3, ld y3, ld x4, ld y4) {
    ld tcross1 = (x1-x2)*(y3-y1) + (y1-y2)*(x1-x3);
    ld dcross1 = (x1-x2)*(y4-y1) + (y1-y2)*(x1-x4);
    ld tcross2 = (x3-x4)*(y1-y3) + (y3-y4)*(x3-x1);
    ld dcross2 = (x3-x4)*(y2-y3) + (y3-y4)*(x3-x2);

    // 忘れていた
    // コーナーケース
    // 点に順序を入れた方が良い
    if (tcross1 == 0 && dcross1 == 0 && tcross2 == 0 && dcross2 == 0) {
      if (x1 > x2 || (x1 == x2 && y1 > y2)) {
        swap(x1, x2);
        swap(y1, y2);
      }
      if (x3 > x4 || (x3 == x4 && y3 > y4)) {
        swap(x3, x4);
        swap(y3, y4);
      }
      ld lx, ly, rx, ry;
      if (x1 == x3) {
        lx = x1;
        ly = max(y1, y3);
      } else if (x1 < x3) {
        lx = x3;
        ly = y3;
      } else {
        lx = x1;
        ly = y1;
      }
      if (x2 == x4) {
        rx = x2;
        ry = min(y2, y4);
      } else if (x2 < x4) {
        rx = x2;
        ry = y2;
      } else {
        rx = x4;
        ry = y4;
      }
      if (lx < rx || (lx == rx && ly < ry)) {
        return "Yes";
      }
      return "No";
    }

    // P1P2, P3P4 の交差判定
    // 直線P1P2に対してP3P4の両端が異なる側にあるかどうかを判定
    if (tcross1*dcross1 < 0) {
      // P3P4, P1P2 の交差判定
      // 直線P3P4に対してP1P2の両端が異なる側にあるかどうかを判定
      if (tcross2*dcross2 < 0) return "Yes";
    }

    return "No";
    }
};

int main(){
  cin.tie(nullptr);
  ios_base::sync_with_stdio(false);

  p037 solver;
  ld x1, x2, x3, x4, y1, y2, y3, y4;
  cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3 >> x4 >> y4;

  cout<<solver.run(x1, y1, x2, y2, x3, y3, x4, y4)<<endl;

  return 0;
}</code></pre>
<p>"""</p>
<p>def send_request(num_ctx):
data = {
"model": "llama3-gradient:8b",
"prompt": system + code,
"stream": False,
"options": {
"num_ctx": num_ctx,
}
}
now = datetime.datetime.now()
response = requests.post(
"https:// service URL .us-central1.run.app/api/generate",
data=json.dumps(data),
headers={"Content-Type": "application/json"}
)
print(f"num_ctx={num_ctx}, status_code={response.status_code}")
try:
res_json = response.json()
except Exception as e:
print(f"num_ctx={num_ctx}, レスポンスJSONデコード失敗: {e}")
return None, None, None
duration = res_json.get("total_duration", None)</p>
<h1>ログ出力はそのまま</h1>
<pre><code>with open(f"output_{num_ctx}_" + now.strftime('%Y-%m-%d_%H%M%S') + ".json", "w") as data_file:
    json.dump(res_json, data_file, indent=2)
with open(f"output_{num_ctx}_" + now.strftime('%Y-%m-%d_%H%M%S') + "_response.txt", "w") as data_file:
    data_file.write(res_json.get("response", ""))
return duration, response.status_code, res_json</code></pre>
<p>def is_gpu_only(duration_ns):</p>
<h1>30秒 = 30_000_000_000ns</h1>
<pre><code>if duration_ns is None:
    return None
return duration_ns <= 30_000_000_000</code></pre>
<p>def binary_search_num_ctx(min_ctx, max_ctx):
left = min_ctx
right = max_ctx
result = None
while left <= right:</p>
<h1>mid = (left + right) // 2</h1>
<pre><code>    # 偶数のみで探索
    mid = ((left + right) // 2) // 2 * 2
    print(f"\n=== num_ctx={mid} でリクエスト送信 ===")
    duration, status_code, res_json = send_request(mid)
    print(f"\ntook {duration/1_000_000_000} s")
    if duration is None:
        print(f"num_ctx={mid} でレスポンス取得失敗。終了します。")
        break
    print(f"num_ctx={mid}, total_duration={duration} ns, status_code={status_code}")
    if is_gpu_only(duration):
        print(f"num_ctx={mid} : GPUのみで処理 (20秒以下) → num_ctxを増やす")
        result = mid
        left = mid + 2
    else:
        print(f"num_ctx={mid} : CPU併用 (20秒超) → num_ctxを減らす")
        right = mid - 2
    time.sleep(15)  # サーバー負荷軽減のため15秒待機
print(f"\nGPUのみで処理できる最大のnum_ctx: {result}")
return result</code></pre>
<p>if <strong>name</strong> == "<strong>main</strong>":</p>
<h1>例: 20480 〜 24576 の間で探索</h1>
<pre><code>min_ctx = 20480
max_ctx = 24576
binary_search_num_ctx(min_ctx, max_ctx)</code></pre>
<pre><code>
-----

# Context Window Limits When Using a GPU on Cloud Run, Part 1

## Conclusion

When accessing a service created on Cloud Run with the [<code>llama3-gradient:8b</code>](https://ollama.com/library/llama3-gradient) model and an NVIDIA L4 GPU, responses often returned in about **10 seconds** for <code>num_ctx</code> values up to around **15,000**.

When further increasing <code>num_ctx</code>, performance degradation could be observed starting around **19,000**. In the scope of these simple tests, setting <code>num_ctx</code> to **20,970 or higher** never resulted in a response time under 20 seconds.

It seems safe to say that the **performance limit is around 20,970** or slightly below it.

Based on the log output, it appears that the processing exceeds what the GPU alone can handle somewhere between num_ctx values of 21,503 and 22,528.

## Introduction

Cloud Run has a feature that allows you to use GPUs. It became available in Preview on 2024-08-21 and became generally available on 2025-04-07. I have been participating in the preview since late 2024 to conduct research and tests.

The advantages of self-hosting an LLM, rather than using an LLM as a service, include the ability to stick with a specific version, customize it, and prevent information leaks.

Using Cloud Run allows you to use resources "only when you need them, and only as much as you need," enabling you to host an LLM cost-effectively. The bottleneck here is the amount of GPU memory. For tasks like code reviews, you want to get answers while referencing the widest possible range of source code, so the length of the context window is important. On the other hand, since GPU memory is limited, there is a restriction on the context window length.

The LLMs themselves also have inherent context window limits. In this article, I used a specific model (<code>llama3-gradient:8b</code>) and increased the <code>num_ctx</code> parameter, confirming that beyond a certain point, the processing time increases (likely because when the task exceeds what the GPU can handle alone, the CPU is also utilized). Due to large fluctuations, I won't post detailed data, but I will introduce the general trend.

In this article, I will present the results of my investigation into the <code>llama3-gradient:8b</code> model. In the next article, I plan to cover Gemma 3.

## Details

My <code>Dockerfile</code> is as follows. It uses the Ollama image and pre-downloads the necessary models. The models are included in the container image. <code>gemma2:9b</code> and <code>codegemma:7b-instruct</code> were included for other tests.

```Dockerfile:Dockerfile
FROM ollama/ollama

# 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 MODEL1 gemma2:9b
ENV MODEL2 codegemma:7b-instruct
ENV MODEL3 llama3-gradient:8b
RUN ollama serve & sleep 5 && ollama pull $MODEL1 && ollama pull $MODEL2 && ollama pull $MODEL3

# Start Ollama
ENTRYPOINT ["ollama", "serve"]

I created a Cloud Run service with the image built from this Dockerfile using the following settings:

  • Startup CPU boost: Enabled
  • Concurrency: 4
  • CPU limit: 8
  • Memory limit: 32 GiB
  • GPU: 1 NVIDIA L4 (no zonal redundancy)
  • OLLAMA_NUM_PARALLEL: 4

Regarding OLLAMA_NUM_PARALLEL, a value of 1 might have been better, but I had already configured it this way, so I proceeded with the tests. Some models allow you to change the context window by passing num_ctx in the options. llama3-gradient:8b is one such model.

The documentation for llama3-gradient states:

This model extends LLama-3 8B’s context length from 8k to over 1m tokens.

This article, including the code sections, exceeds 15,000 characters, so an 8k token limit would likely be insufficient to process the entire text. With 1 million, there is plenty of room.

The core code is as follows. It sends a request specifying the num_ctx. For finer details, please refer to the Code section.

data = {
    "model": "llama3-gradient:8b",
    "prompt": system + code,
    "stream": False,
    "options": {
        "num_ctx": num_ctx,
    }
}

response = requests.post(
    "https:// service URL .us-central1.run.app/api/generate",
    data=json.dumps(data),
    headers={"Content-Type": "application/json"}
)

Performance can be unstable, but when it’s fast, responses come back in about 10 seconds. When it’s slow, it can take over 30 seconds, sometimes even close to 300 seconds. For example, with a num_ctx of 20,975, one request took 271 seconds.

Given that fast responses are around 10 seconds, I decided to classify anything taking longer than 20 seconds as "slow." Note that at one point, I changed the timeout to 30 seconds while running the loop.

In Keita Sato’s article, "Testing the Performance of Cloud Run GPU + Ollama gemma2," a load test was performed on gemma2:9b with k6 without specifying a context window during parallel access, and the result was:

response time P95 45s

Looking at the images in the article, there is an example where the AVG is 20s, so it seems reasonable to assume a "normal" response time is around 20 seconds.

For some num_ctx values, a response never came back, even after multiple attempts (strangely, it didn’t seem to be a simple linear relationship). For example, with num_ctx at 20,966, it came back in under 20 seconds multiple times, once even in 9.9 seconds.

However, a num_ctx of 18,750 also timed out on occasion.
With num_ctx at 20,970, I made three requests: two timed out, and one took 32 seconds.
With num_ctx at 20,964, I made six requests: two timed out, and four took 12, 13, 31, and 26 seconds, respectively. It’s unstable.

I have data showing that num_ctx 20,971 took 31 seconds, 20,975 took 271 seconds (note: the number is correct), 20,991 took 27 seconds, 21,503 took 46 seconds, and 22,528 took 45 seconds. I had no data for a num_ctx over 20,970 that completed in under 20 seconds. From this, I concluded that the limit is around 20,970.

In "Google Releases Gemma 3! -Tried Running It on Cloud Run-" by Muramatsu of Cloud Ace, the models tested (gemma2:27b, gemma3:12b) are different from this article, but tests were run with num_ctx set to 16,384. This suggests that even these models return a response within the default 300-second timeout or a slightly extended one.

offload… to GPU

Apart from the response time, you can also observe the following output in the logs:

llm_load_print_meta: model size = 4.33 GiB (4.64 BPW)
llm_load_tensors: ggml ctx size = 0.27 MiB
llm_load_tensors: offloading 32 repeating layers to GPU
llm_load_tensors: offloading non-repeating layers to GPU
llm_load_tensors: offloaded 33/33 layers to GPU
llm_load_tensors: CPU buffer size = 281.81 MiB
llm_load_tensors: CUDA0 buffer size = 4155.99 MiB
llama_kv_cache_init: CUDA0 KV buffer size = 10752.00 MiB
llama_new_context_with_model: KV self size = 10752.00 MiB, K (f16): 5376.00 MiB, V (f16): 5376.00 MiB
llama_new_context_with_model: CUDA_Host output buffer size = 2.02 MiB
llama_new_context_with_model: CUDA0 compute buffer size = 5576.00 MiB
llama_new_context_with_model: CUDA_Host compute buffer size = 176.01 MiB
llama_new_context_with_model: graph nodes = 1030

In the following log, seeing "offloaded 32/33 layers to GPU" suggests that one layer is not on the GPU. In fact, the line "offloading non-repeating layers to GPU" is missing.

llm_load_print_meta: model size = 4.33 GiB (4.64 BPW)
llm_load_tensors: ggml ctx size = 0.27 MiB
llm_load_tensors: offloading 32 repeating layers to GPU
llm_load_tensors: offloaded 32/33 layers to GPU
llm_load_tensors: CPU buffer size = 4437.80 MiB
llm_load_tensors: CUDA0 buffer size = 3745.00 MiB
llama_kv_cache_init: CUDA0 KV buffer size = 11264.00 MiB
llama_new_context_with_model: KV self size = 11264.00 MiB, K (f16): 5632.00 MiB, V (f16): 5632.00 MiB
llama_new_context_with_model: CUDA_Host output buffer size = 2.02 MiB
llama_new_context_with_model: CUDA0 compute buffer size = 5840.00 MiB
llama_new_context_with_model: CUDA_Host compute buffer size = 184.01 MiB
llama_new_context_with_model: graph nodes = 1030

The first log is for num_ctx = 21503, and the second is for num_ctx = 22528. From this, it appears that when num_ctx is around 22000, the processing exceeds what the GPU can handle.The first log is for num_ctx = 21503, and the second is for num_ctx = 22528. From this, it appears that when num_ctx is around 22000, the processing exceeds what the GPU can handle.

Summary

When accessing a service created on Cloud Run with the llama3-gradient:8b model and an NVIDIA L4 GPU, responses often returned in about 10 seconds for num_ctx values up to around 15,000.

When further increasing num_ctx, performance degradation could be observed starting around 19,000. In the scope of these simple tests, setting num_ctx to 20,970 or higher never resulted in a response time under 20 seconds.

It seems safe to say that the performance limit is around 20,970 or slightly below it.

Perhaps due to the inherent randomness of the output, the results were not stable. This is why I used the ambiguous phrasing "performance degradation could be observed."

References

Code

See Japanese version for code snippets.

Cloud Run,google cloud

Posted by tako