RAG | MISO https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com 未来を創造するITのミソ Tue, 15 Jul 2025 05:18:58 +0000 ja hourly 1 https://wordpress.org/?v=6.7.2 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/wp-content/uploads/2017/09/tdi_300-300-300x280.png RAG | MISO https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com 32 32 ローカルSLM/閉じた環境で動作する生成AIを検証してみた (②性能指標とログの取り方編) https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/local-slm-2 Tue, 15 Jul 2025 05:18:58 +0000 https://alb-owned-https-576747877.ap-northeast-1.elb.amazonaws.com/?p=20600 はじめに ご無沙汰しております。生成AIを「社内や現場のローカルで使いたい」そんな思いからスタートした、このローカル生成AI・SLM検証シリーズ。 第一回では、OllamaとOpen WebUIを使って、閉じた環境で動作…

The post ローカルSLM/閉じた環境で動作する生成AIを検証してみた (②性能指標とログの取り方編) first appeared on MISO.]]>
はじめに

ご無沙汰しております。生成AIを「社内や現場のローカルで使いたい」そんな思いからスタートした、このローカル生成AI・SLM検証シリーズ。
第一回では、OllamaとOpen WebUIを使って、閉じた環境で動作する生成AI(ローカルSLM)を構築し、その手順や構成イメージを紹介しました。
「クラウドに出せない」「ネット接続できない」――そんな制約下でも生成AIを活用できることに、興味を持っていただいた方も多かったようです。もしまだ第一回の記事をご覧になっていない方は、こちらのローカルSLM/閉じた環境で動作する生成AIを検証してみた (①環境構築編)も合わせてご覧ください。

そして今回は、その次の ”現実的な課題” に踏み込みます。「実際に動かしたこのローカルSLM、果たしてどこまで“実用レベル”なのか?」それを、数値で見える化する方法と、実際に取れたデータを交えてご紹介します。

もちろん、前回の記事を読んでいなくても大丈夫です。
「オフライン環境でも、ここまでできるのか!」と感じていただける内容になっていますので、ぜひ最後までお付き合いください。
 
【記事の要約】
読了までの時間 : 12~15分
得られるメリット : ローカルSLMにおける性能指標の可視化やログの取得方法がわかる

 

SLMの“実力”をどう測るか?

生成AIといえば、「文章の自然さ」や「賢さ」に目がいきがちですが、ローカルでの業務活用を考えた場合、それだけでは不十分です。
むしろ、重要になるのは性能指標(ベンチマーク)です。

たとえば──

  • 処理が遅すぎてストレスになる
  • GPUのメモリを食いすぎて、他のアプリが動かなくなる
  • 非GPU環境では使い物にならない

こういった“現場あるある”を避けるためにも、定量的な指標でモデルを評価することが欠かせません。

実務で重視したい 5つの指標

私が今回意識したのは、次の5つです。

指標 意味と実務での重要性
tokens/sec
どれだけ速く文章を生成できるか
(1秒あたりに生成されるトークン数、スループット設計の基礎)
初期応答時間 ユーザーが待たされる体感時間
VRAM使用量 実行できるPCやサーバーの条件把握に直結
CPU使用率 非GPU環境やエッジデバイスの負荷確認
出力自然性 実用性や読みやすさの評価基準

 
これらは、2024年に公開された「Small Language Models: Survey, Measurements, and Insights」という論文でも、SLMの評価軸としてしっかり整理されています
なお、今回はその中でも、比較的取得が容易だった「tokens/sec(生成速度)」を中心に測定を行いました。他の指標については、今後、取得方法の工夫を進め、順次可視化していく予定なので、ご理解いただければ幸いです。

 

前提条件

今回の検証は、以下のPC環境で実施しています。前回の記事と同じ構成です。

OS Windows 11 Home (24H2)
CPU AMD Ryzen AI9 HX370
メモリ 32GB
ディスク 1TB
GPU/VRAM NVIDIA GeForce RTX 4060 / 8GB

 
なお、今回の検証内容や結果は、この構成に基づくものです。環境が異なる場合は、性能傾向が変わる可能性がある点にご注意ください。

 

ローカル環境での測定の壁と工夫

「じゃあ、その数値、どうやって取るの?」これが意外とハマりどころでした。
第一回で構築したOllamaとOpen WebUIの構成だと、
  • tokens/sec(1秒あたりに生成されるトークン数)
  • 初期応答時間

といった細かなデータは、素直に取れません。
また、WebUI側のOpen WebUIを直接いじって表示させることも考えましたが、アップデート対応が大変なので現実的ではないと判断しました。(Open WebUIのリリース状況を見てもわかるように、ほぼ毎週、時には週に数回アップデートがあります。そのたびに中身を直接修正するのは、現実的ではありませんよね…)

リレー処理を導入して“外から覗く”

そこで考えたのが、OllamaとOpen WebUIの間に、リレー処理をかます方法です。
具体的には、以下のイメージです。
[Open WebUI] ⇄ [リレー処理(自作プロキシ)] ⇄ [Ollama]

このプロキシで、リレー処理の中でリクエストとレスポンスを覗き見ることで、

  • プロンプト送信時刻
  • 最初の応答トークンの戻り時刻
  • トータルの生成時間
  • tokens/secの算出結果(1秒あたりに生成されるトークン数)

といったログ情報を出力する仕組みを考えてみました。

実際のコード

リレー処理で使用する「自作プロキシ」処理ですが、今回は Python を使ったシンプルなFlaskアプリケーションで実装してみました。
以下が「自作プロキシ」のコード( proxy_server.py)になります。
from flask import Flask, request, Response
import requests, os, json, time

app = Flask(__name__)
OLLAMA_API = 'http://ollama:11434'
LOG_PATH = '/logs/bench.log'

@app.route('/api/generate', methods=['POST'])
def proxy_generate():
    start_time = time.time()
    resp = requests.post(f"{OLLAMA_API}/api/generate", json=request.get_json())
    duration = time.time() - start_time
    data = resp.json()

    eval_count = data.get("eval_count")
    eval_duration_ns = data.get("eval_duration")

    if eval_count and eval_duration_ns:
        token_sec = eval_count / (eval_duration_ns / 1_000_000_000)
        log_entry = {
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
            "token_per_sec": token_sec
        }
        os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
        with open(LOG_PATH, "a") as f:
            f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")

    return Response(resp.content, status=resp.status_code, content_type=resp.headers.get('Content-Type'))

ポイントは、Ollamaへのリクエストを中継しつつ、必要な情報だけログに記録する部分です。
また、このプロキシ処理内で使用しているライブラリを取得する requirements.txt も用意する必要があるのでお忘れなく!( requirements.txt は上記コードと同じ場所に配置してください)
flask
requests

実行環境のフォルダ構成

「記事は見たけど、自分の環境で動かせないと意味がない」そんな方のために、今回私が検証したフォルダ構成の全体イメージを載せておきます。(フォルダ構成内のコメント先頭に (*) があるフォルダは、新規で作成する必要があります)

./ollama
  ├ compose.yaml
  ├ ollama                  # ollama用のdockerマウントフォルダ
  │    └ :
  ├ open-webui              # open webui用のdockerマウントフォルダ
  │    └ :
  ├ proxy_server            # (*) 自作プロキシ用のフォルダ
  │    ├ proxy_server.py
  │    └ requirements.txt
  └ logs                    # (*) 自作プロキシが出力するログ用フォルダ
        └ bench.log         # ログファイル

この構成であれば、必要なファイルを同じ階層にまとめておけるので、環境再現や構成管理が非常に楽になります。ぜひ参考にしてみてください。

Dockerイメージ化と実行手順

次に、上記で作成した「自作プロキシ」を含めてリレーさせるように、以下のように compose.yaml を用意します。
(自作プロキシが上記「実際のコード」で作成した proxy_server.py となります。また、既存の構成がない場合でも、この内容で新規作成すれば問題ありません)
services:
  ollama:
    image: ollama/ollama
    ports:
      - "11434:11434"
    volumes:
      - ./ollama:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]
              driver: nvidia
              count: all

  proxy:
    image: python:3.10-slim
    working_dir: /app
    volumes:
      - ./proxy_server:/app
      - ./logs:/logs
    command: ["sh", "-c", "pip install -r requirements.txt && exec python proxy_server.py"]
    ports:
      - "8001:8001"
    depends_on:
      - ollama

  open-webui:
    image: ghcr.io/open-webui/open-webui:latest
    ports:
      - "8080:8080"
    environment:
      API_URL: http://proxy:8001
      ENABLE_OLLAMA_API: "True"
      OLLAMA_BASE_URL: http://proxy:8001

ポイントは、Open WebUIのリクエストをプロキシ(proxyコンテナ)経由にすることです。
この構成なら、Open WebUIのバージョンアップがあって内部仕様が変わっても、プロキシで外側からリクエストを見ているため、ログの取得に影響しにくい構成になります。

 

実際に取れたログとその考察

リレー処理を挟んだ結果、以下のようなログが取れました。(ログは抜粋して表示してます)

{"timestamp": "2025-06-28 12:24:22", "model": "gemma3:27b", "total_duration_ns": 94632160664, "load_duration_ns": 19994323564, "prompt_eval_count": 1031, "prompt_eval_duration_ns": 60875012677, "eval_count": 58, "eval_duration_ns": 13735608879, "token_per_sec": 4.222601306642811}
{"timestamp": "2025-06-28 12:30:58", "model": "gemma3:27b", "total_duration_ns": 394963073595, "load_duration_ns": 53579880, "prompt_eval_count": 3546, "prompt_eval_duration_ns": 195046011140, "eval_count": 760, "eval_duration_ns": 199816400248, "token_per_sec": 3.8034916005729964}
{"timestamp": "2025-06-28 13:59:10", "model": "qwen2.5vl:7b", "total_duration_ns": 759561136, "load_duration_ns": 13410971, "prompt_eval_count": 667, "prompt_eval_duration_ns": 408703073, "eval_count": 13, "eval_duration_ns": 334954117, "token_per_sec": 38.81128590516772}
{"timestamp": "2025-06-28 13:59:10", "model": "qwen2.5vl:7b", "total_duration_ns": 875906258, "load_duration_ns": 11146979, "prompt_eval_count": 558, "prompt_eval_duration_ns": 335325142, "eval_count": 20, "eval_duration_ns": 526305136, "token_per_sec": 38.00076919636977}
{"timestamp": "2025-06-28 13:59:37", "model": "qwen2.5vl:7b", "total_duration_ns": 3153000322, "load_duration_ns": 11314367, "prompt_eval_count": 721, "prompt_eval_duration_ns": 754606991, "eval_count": 72, "eval_duration_ns": 2383351049, "token_per_sec": 30.20956565765231}
{"timestamp": "2025-06-28 14:05:37", "model": "gemma3:12b", "total_duration_ns": 5428418296, "load_duration_ns": 22034384, "prompt_eval_count": 448, "prompt_eval_duration_ns": 4024403009, "eval_count": 21, "eval_duration_ns": 1380531832, "token_per_sec": 15.21152900152758}
{"timestamp": "2025-06-28 14:05:42", "model": "gemma3:12b", "total_duration_ns": 4395797252, "load_duration_ns": 24517951, "prompt_eval_count": 337, "prompt_eval_duration_ns": 2879844775, "eval_count": 23, "eval_duration_ns": 1490088937, "token_per_sec": 15.435320287865476}
{"timestamp": "2025-06-28 14:06:01", "model": "gemma3:12b", "total_duration_ns": 8595806105, "load_duration_ns": 30188016, "prompt_eval_count": 497, "prompt_eval_duration_ns": 4741797711, "eval_count": 51, "eval_duration_ns": 3822096375, "token_per_sec": 13.343462591259227}
{"timestamp": "2025-06-28 16:06:21", "model": "gemma3n:e4b", "total_duration_ns": 23945618955, "load_duration_ns": 13359735942, "prompt_eval_count": 16, "prompt_eval_duration_ns": 800558935, "eval_count": 200, "eval_duration_ns": 9782352502, "token_per_sec": 20.444979871570773}
{"timestamp": "2025-06-28 16:06:25", "model": "gemma3n:e4b", "total_duration_ns": 3821787614, "load_duration_ns": 62551020, "prompt_eval_count": 477, "prompt_eval_duration_ns": 593423184, "eval_count": 67, "eval_duration_ns": 3164968738, "token_per_sec": 21.169245432211913}
{"timestamp": "2025-06-28 16:06:27", "model": "gemma3n:e4b", "total_duration_ns": 1725808520, "load_duration_ns": 49401411, "prompt_eval_count": 556, "prompt_eval_duration_ns": 768251988, "eval_count": 20, "eval_duration_ns": 907228651, "token_per_sec": 22.045159153599084}
{"timestamp": "2025-06-28 16:06:30", "model": "gemma3n:e4b", "total_duration_ns": 2859492266, "load_duration_ns": 43687267, "prompt_eval_count": 432, "prompt_eval_duration_ns": 542335379, "eval_count": 46, "eval_duration_ns": 2272627867, "token_per_sec": 20.24088530636679}

今回検証したのは、以下の4つのローカルSLMモデルです。それぞれの特徴と、実際に取得できた平均トークン生成速度(tokens/sec)は次の通りでした。
モデル名 パラメータ規模 特徴・備考 平均 tokens/sec
qwen2.5vl:7b 7B 軽量・高速モデル 35.7
gemma3n:e4b 12B相当(最新) gemma3系の最新世代、最適化モデル 20.5
gemma3:12b 12B 標準的な中量モデル、バランス型 14.7
gemma3:27b 27B 高精度寄りの大型モデル、低速 4.0
※ 平均値は小数第2位まで、四捨五入
 
考察まとめ
この検証結果から、以下のことが見えてきました。
 
qwen2.5vl:7b
群を抜いて高速で、トークン生成速度は 35〜38 tokens/sec 程度を記録した。
軽量モデルならではのメリットを最大限に活かせるため、ローカル環境でのチャットボットや簡易生成タスクには最適といえる。
gemma3:12b
トークン生成速度は約 13〜15 tokens/sec で、中量モデルとしては標準的な結果であった。
qwen2.5vl:7b と比べると速度面では劣るが、より安定した出力品質や汎用性の高さが期待でき、処理時間と品質のバランスを重視する場面では有力な選択肢となる。
gemma3n:e4b 最適化世代という特徴通り、同じ12Bクラスでも約20〜22 tokens/sec と、旧世代の gemma3:12b より明確に高速化されている。
速度と品質のバランスが取れており、現実的な業務用途で最も扱いやすい中量モデルの一つといえる。
gemma3:27b トークン生成速度は約 3.8〜4.2 tokens/sec と、他モデルと比べて圧倒的に低速である。
その代わり、出力品質や精度重視の大型モデルであり、リアルタイム性を必要としない高品質生成タスク(文書要約、テキスト生成など)では十分に価値を発揮する。ローカル環境で使う場合は、運用用途を見極めた上で導入を検討する必要があるといえる。
 
また、今回出力した内容以外にも、リレー処理上のログの出力内容を修正することで、出力させる情報のカスタマイズが可能です。
 
このように、
  • モデルごとの推論時間
  • トークン生成速度(tokens/sec、1秒あたりに生成されるトークン数)
といった客観的な数値をもとに、ローカルSLMの実用性や適用範囲を見極める判断材料を得ることができました。
 
 

まとめと次の展開

今回の検証から得られたポイントは以下の通りです。

 ✅ ローカルSLMの実力を、数値で“見える化”できた
 ✅ プロダクト側を改造せず、ログを外から自由に加工できる構成が組めた
 ✅ 実用性や限界を、感覚ではなくデータで判断できるようになった

次回の展開

次は、さらに実業務に寄せた以下のテーマにも踏み込みたいと考えています。

  • ローカルSLMとRAGの実務レベル活用
  • ビジョンモデルやオフィス文書対応の検証
  • 特に、社内で多用されるExcelやPowerPoint、PDFの要約・活用性の評価

生成AIは“魔法”ではなく、“現実的なツール”です。だからこそ、我々としては、こうした地道な検証を積み重ねて、現場にフィットする使い方を今後も模索していこうと思います。

それでは、また次回もお楽しみに!

The post ローカルSLM/閉じた環境で動作する生成AIを検証してみた (②性能指標とログの取り方編) first appeared on MISO.]]>