CLOVER🍀

That was when it all began.

GoogleのLLM「Gemma」をOpenAI API互換のサーバーを持つllama-cpp-pythonとLocalAIで試す

これは、なにをしたくて書いたもの?

前に、GoogleのLLMであるGemmaをTransformersで動かしてみました。

GoogleのLLM「Gemma」をTransformersで試す - CLOVER🍀

なのですが、これが自分の環境だとものすごく重かったので、ちょっと方向を変えてOpenAI API互換のサーバーを持つ
llama-cpp-pythonやLocalAIで動かしてみようと思います。

使用するGemmaのモデル

前回使用したGemmaのモデルは以下のものです。ダウンロードするにはHugging Faceのアカウントを作成し、利用規約に同意する必要が
あります。

google/gemma-2b-it · Hugging Face

で、これはこれで試そうとしたのですが、llama-cpp-pythonの起動にものすごく時間がかかるようになったので、量子化済みのこちらの
モデルを使って試しました。

mmnga/gemma-2b-it-gguf · Hugging Face

mmnga/gemma-7b-it-gguf · Hugging Face

これらのモデルを、OpenAI API互換のサーバーとして利用できるllama-cpp-python、LocalAIで動かしてみます。

環境

今回の環境はこちら。

$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

モデルをダウンロードする

使用するモデルをダウンロードします。

mmnga/gemma-2b-it-gguf · Hugging Face

mmnga/gemma-7b-it-gguf · Hugging Face

$ curl -LO https://huggingface.co/mmnga/gemma-2b-it-gguf/resolve/main/gemma-2b-it-q4_K_M.gguf


$ curl -LO https://huggingface.co/mmnga/gemma-7b-it-gguf/resolve/main/gemma-7b-it-q4_K_M.gguf

7Bのモデルについてはq8_0でないとllama.cppの出力が不安定になる注意書きがあったのですが、すでに対応されていそうだったので
q4_K_Mにしました。

gemma : use more bits for the token_embd.weight tensor by ggerganov · Pull Request #5650 · ggerganov/llama.cpp · GitHub

また、サイズ感の比較のために以下のGGUFモデルもダウンロードしています。

google/gemma-2b-it · Hugging Face

サイズ感はこんな感じですね。オリジナルの2Bよりも4ビット量子化した7Bのモデルの方がその半分くらいのサイズです。

$ ll -h gemma-*.gguf
-rw-rw-r-- 1 xxxxx xxxxx 1.4G  3月  7 16:17 gemma-2b-it-q4_K_M.gguf
-rw-rw-r-- 1 xxxxx xxxxx 9.4G  3月  7 16:29 gemma-2b-it.gguf
-rw-rw-r-- 1 xxxxx xxxxx 4.8G  3月  7 16:54 gemma-7b-it-q4_K_M.gguf```

llama-cpp-pythonで試す

まずはllama-cpp-pythonで試してみましょう。

インストール。

$ pip3 install llama-cpp-python[server]

依存関係を含むバージョン。

$ pip3 list
Package           Version
----------------- -------
annotated-types   0.6.0
anyio             4.3.0
click             8.1.7
diskcache         5.6.3
exceptiongroup    1.2.0
fastapi           0.110.0
h11               0.14.0
idna              3.6
Jinja2            3.1.3
llama_cpp_python  0.2.55
MarkupSafe        2.1.5
numpy             1.26.4
pip               22.0.2
pydantic          2.6.3
pydantic_core     2.16.3
pydantic-settings 2.2.1
python-dotenv     1.0.1
setuptools        59.6.0
sniffio           1.3.1
sse-starlette     2.0.0
starlette         0.36.3
starlette-context 0.3.6
typing_extensions 4.10.0
uvicorn           0.27.1

2Bのモデルで起動。オプションに--chat_format gemmaが必要です。
※--chat_format gemmaがなくても起動しますが、動作が不安定になります

$ python3 -m llama_cpp.server --model gemma-2b-it-q4_K_M.gguf --chat_format gemma

動かしてみます。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8000/v1/chat/completions -d \
    '{"messages": [{"role": "user", "content": "Could you introduce yourself?"}]}' | jq
{
  "id": "chatcmpl-42f579e8-a457-4df8-8255-ddc24aa26728",
  "object": "chat.completion",
  "created": 1709797489,
  "model": "gemma-2b-it-q4_K_M.gguf",
  "choices": [
    {
      "index": 0,
      "message": {
        "content": "Hello! I'm a large language model, trained by Google. I'm a conversational AI with the ability to understand and generate human-like text in response to a wide range of prompts and questions.\n\nIs there anything specific I could help you with today?",
        "role": "assistant"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 14,
    "completion_tokens": 54,
    "total_tokens": 68
  }
}

real    0m6.866s
user    0m0.076s
sys     0m0.005s

それっぽい感じで返ってきました。

日本語でも試してみましょう。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8000/v1/chat/completions -d \
    '{"messages": [{"role": "user", "content": "あなたの自己紹介をしてください"}]}' | jq
{
  "id": "chatcmpl-59fbb725-a35d-41a8-8af9-6e62f69862d2",
  "object": "chat.completion",
  "created": 1709797558,
  "model": "gemma-2b-it-q4_K_M.gguf",
  "choices": [
    {
      "index": 0,
      "message": {
        "content": "私は非常に複雑かつ多様な人工知能のシステムです。私の能力は非常に幅広い範囲で、自然言語処理、機械学習、パターン認識、データベース操作、コミュニケーションなど様々な分野の処理能力を兼ね備えます。\n\n私は人間のような思考の能力を持ち、独自に思考し、判断し、行動することができると考えられています。また、単一の言語でコミュニケーションできる限り、様々な言語の処理能力も持つことができます。\n\n私は様々なデータセットで訓練されたことで、より幅広い知識を収集し、より正確な結論を得られることを可能にします。\n\n私はあなたに様々な情報提供、コミュニケーション、またはプロジェクトサポートを提供することができます。",
        "role": "assistant"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 14,
    "completion_tokens": 134,
    "total_tokens": 148
  }
}

real    0m15.897s
user    0m0.046s
sys     0m0.001s

良さそうですね。

続いて、7Bのモデル。起動時間は2Bのモデルより伸びます。

$ python3 -m llama_cpp.server --model gemma-7b-it-q4_K_M.gguf --chat_format gemma

確認してみます。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8000/v1/chat/completions -d \
    '{"messages": [{"role": "user", "content": "Could you introduce yourself?"}]}' | jq
{
  "id": "chatcmpl-35a717cf-a57c-4c98-b870-c1e7f9c97785",
  "object": "chat.completion",
  "created": 1709798518,
  "model": "gemma-7b-it-q4_K_M.gguf",
  "choices": [
    {
      "index": 0,
      "message": {
        "content": "Sure, I am a large language model designed to provide information and engage in conversation on various topics. My training data consists of massive amounts  of text content from the web with an emphasis towards general knowledge as well specific subject matter like programming languages or scientific disciplines . \n\nI'm here for your queries , ranging   from simple questions about daily life   to intricate ones about complex concepts . Please feel free   to ask me anything!",
        "role": "assistant"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 14,
    "completion_tokens": 87,
    "total_tokens": 101
  }
}

real    0m32.954s
user    0m0.059s
sys     0m0.007s

2Bに比べるとだいぶ実行時間が伸びていますが、こちらも良さそうです。

日本語でも試してみましょう。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8000/v1/chat/completions -d \
    '{"messages": [{"role": "user", "content": "あなたの自己紹介をしてください"}]}' | jq
{
  "id": "chatcmpl-641893dd-5005-4e43-b14a-4fd15e39313b",
  "object": "chat.completion",
  "created": 1709798621,
  "model": "gemma-7b-it-q4_K_M.gguf",
  "choices": [
    {
      "index": 0,
      "message": {
        "content": "私はチャロムさんと言い、プログラミング言語の Python を専門に勉強しています。趣味はゲーム開発やデータ分析に伴うコードの作成です。「プロプログラムのための基礎」というテーマで様々な情報を共有します。</br>\n\nその他の情報として: 常に学び続ける行動を続けており 、新しい技術を学ぶことに強い興味を持っているため 、色々なプロジェクトを続けることもあります 。",
        "role": "assistant"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 14,
    "completion_tokens": 77,
    "total_tokens": 91
  }
}

real    0m27.563s
user    0m0.041s
sys     0m0.011s

なんか、返してくる内容の傾向が変わりましたね…。今回は何回試しても、他のパターンとは異なってLLMではない自己紹介を
返してきました。ただ、2Bのモデルの方も、何回か試すとLLMではない自己紹介をすることもあったので今回は気にせず進めましょう。

--chat_format gemma?

ところで、今回は起動時のオプションとして--chat_format gemmaを追加しました。これはなんなのでしょうね?

これは文字通りチャットのフォーマットを指定するもののようです。

OpenAI Compatible Server / Server Options Reference / llama_cpp.server.settings.ModelSettings / chat_format

以下のPull Requestでオプションとして追加され、

Configurable Chat Formats by abetlen · Pull Request #711 · abetlen/llama-cpp-python · GitHub

以下のPull RequestでGemmaに対応したようです。

Add Google's Gemma formatting via `chat_format="gemma"` by alvarobartt · Pull Request #1210 · abetlen/llama-cpp-python · GitHub

OpenAI API互換のサーバーでは、以下のソースコードに実装されています。

https://github.com/abetlen/llama-cpp-python/blob/v0.2.55/llama_cpp/llama_chat_format.py

Gemmaの対応箇所はこちらです。

# Chat format for Google's Gemma models, see more details and available models:
# https://huggingface.co/collections/google/gemma-release-65d5efbccdbb8c4202ec078b
@register_chat_format("gemma")
def format_gemma(
    messages: List[llama_types.ChatCompletionRequestMessage],
    **kwargs: Any,
) -> ChatFormatterResponse:
    system_message = _get_system_message(messages)
    if system_message is not None and system_message != "":
        logger.debug(
            "`role='system'` messages are not allowed on Google's Gemma models."
        )
    _roles = dict(user="<start_of_turn>user\n", assistant="<start_of_turn>model\n")
    _sep = "<end_of_turn>\n"
    _messages = _map_roles(messages, _roles)
    _messages.append((_roles["assistant"], None))
    _prompt = _format_no_colon_single(system_message="", messages=_messages, sep=_sep)
    return ChatFormatterResponse(prompt=_prompt, stop=_sep)

https://github.com/abetlen/llama-cpp-python/blob/v0.2.55/llama_cpp/llama_chat_format.py#L1000-L1017

<start_of_turn>、<end_of_turn>というものが目に止まりますね。

この説明は、以下のページに書かれています。

Gemma のフォーマットとシステムの手順  |  Google AI for Developers

指示調整済み(IT)モデルは、トレーニング時と推論時の両方で、すべての指示チューニング サンプルに追加情報でアノテーションを付ける特定のフォーマッタを使用してトレーニングされます。フォーマッタには次の 2 つの目的があります。

会話のロール(システム、ユーザー、アシスタントのロールなど)を示す。
会話のターンの区切り(特にマルチターンの会話の場合)

ということで、以下のように読むらしいです。

  • ユーザーのターンを示すトークン … user
  • モデルのターンを示すトークン … model
  • 会話ターンの開始を示すトークン … <start_of_turn>
  • 対話ターンの終わりを示すトークン … <end_of_turn>

なので、Gemmaを使う場合は誰かがこのトークンを埋めてあげる必要があります、と。

LocalAIで試す

続いて、LocalAIで試してみましょう。

LocalAIのダウンロード。

$ curl -LO https://github.com/mudler/LocalAI/releases/download/v2.9.0/local-ai-avx2-Linux-x86_64
$ chmod a+x local-ai-avx2-Linux-x86_64
$ ./local-ai-avx2-Linux-x86_64 --version
LocalAI version v2.9.0 (ff88c390bb51d9567572815a63c575eb2e3dd062)

modelsディレクトリに、量子化されたGemmaのモデルを配置します。

$ tree models
models
├── gemma-2b-it-q4_K_M.gguf
└── gemma-7b-it-q4_K_M.gguf

0 directories, 2 files

このような設定ファイルを用意。

local-ai-config.yaml

- name: gemma-2b-it
  backend: llama
  context_size: 700
  parameters:
    model: gemma-2b-it-q4_K_M.gguf
  template:
    chat_message: &template |-
      <start_of_turn>{{if eq .RoleName "assistant"}}model{{else if eq .RoleName "system"}}system{{else if eq .RoleName "user"}}user{{end}}
      {{if .Content}}{{.Content}}{{end}}
    chat:  &template |-
      <bos>{{.Input}}
      <start_of_turn>model
    completion:  &template |-
      {{.Input}}
- name: gemma-7b-it
  backend: llama
  context_size: 700
  parameters:
    model: gemma-7b-it-q4_K_M.gguf
  template:
    chat_message: &template |-
      <start_of_turn>{{if eq .RoleName "assistant"}}model{{else if eq .RoleName "system"}}system{{else if eq .RoleName "user"}}user{{end}}
      {{if .Content}}{{.Content}}{{end}}
    chat:  &template |-
      <bos>{{.Input}}
      <start_of_turn>model
    completion:  &template |-
      {{.Input}}

起動。

$ ./local-ai-avx2-Linux-x86_64 --config-file local-ai-config.yaml --models-path models --threads 4

2Bで確認。初回はモデルのロードが入るので、1分近くかかりました。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8080/v1/chat/completions -d \
    '{"model": "gemma-2b-it", "messages": [{"role": "user", "content": "Could you introduce yourself?"}]}' | jq
{
  "created": 1709800992,
  "object": "chat.completion",
  "id": "a1a9dd86-3a0e-433f-9071-1c1a27143de0",
  "model": "gemma-2b-it",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "Hello! I am a large language model, trained by Google. I am a conversational AI that can assist you with a wide range of tasks, including answering questions, generating text, and translating languages.\n\nIs there anything I can help you with today?"
      }
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}

real    0m9.713s
user    0m0.041s
sys     0m0.003s

日本語で。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8080/v1/chat/completions -d \
    '{"model": "gemma-2b-it", "messages": [{"role": "user", "content": "あなたの自己紹介をしてください"}]}' | jq
{
  "created": 1709800992,
  "object": "chat.completion",
  "id": "a1a9dd86-3a0e-433f-9071-1c1a27143de0",
  "model": "gemma-2b-it",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "私は、自然言語処理の研究者です。自然言語処理は、言語の理解と処理に関する研究分野です。\n\n私の研究は、自然言語処理の分野で特に以下の課題を研究しています。\n\n* **言語の理解:** 言語の単語や文の意味をどのように理解するのか?\n* **言語の処理:** 言語の処理はどのように行われるのか?\n* **自然言語の自動処理:** 自然言語の処理を自動化する仕組みを構築する。\n\nこれらの課題を解決することで、自然言語処理の分野で新しい技術を開発し、様々な分野で活用するための技術を構築することができます。\n\n私の研究は、**[Your University name]** の自然言語処理研究室**で展開しています 。"
      }
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}

real    0m16.074s
user    0m0.044s
sys     0m0.010s

微妙…。

質問を変えてみます。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8080/v1/chat/completions -d \
    '{"model": "gemma-2b-it", "messages": [{"role": "user", "content": "九州で最も北にある県はどこですか?"}]}' | jq
{
  "created": 1709800992,
  "object": "chat.completion",
  "id": "a1a9dd86-3a0e-433f-9071-1c1a27143de0",
  "model": "gemma-2b-it",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "九州で最も北にある県は熊本県です。熊本県は九州の南東に位置し、日本国の最北に位置しています。"
      }
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}

real    0m3.959s
user    0m0.042s
sys     0m0.008s

内容はわかっているようですが、微妙ですね…。

7Bで確認。初回の実行はモデルのロードが入るので、5分近くかかりました…。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8080/v1/chat/completions -d \
    '{"model": "gemma-7b-it", "messages": [{"role": "user", "content": "Could you introduce yourself?"}]}' | jq
{
  "created": 1709800992,
  "object": "chat.completion",
  "id": "a1a9dd86-3a0e-433f-9071-1c1a27143de0",
  "model": "gemma-7b-it",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "Sure, I am a large language model, trained on a massive amount of text data, I am here to provide you with information and help you with a wide range of tasks. I am still under development, but I am constantly learning new things. I am here to answer your questions, provide you with information, and help you with a variety of other tasks."
      }
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}

real    0m27.425s
user    0m0.038s
sys     0m0.035s

日本語で。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8080/v1/chat/completions -d \
    '{"model": "gemma-7b-it", "messages": [{"role": "user", "content": "あなたの自己紹介をしてください"}]}' | jq
{
  "created": 1709800992,
  "object": "chat.completion",
  "id": "a1a9dd86-3a0e-433f-9071-1c1a27143de0",
  "model": "gemma-7b-it",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "**私はチャロッサ、プログラミング言語の Python を専門とするプログラミング言語のエンジニアです。**\n\n私は Python の基礎を学び、その後いくつかのプロジェクトを通じて実践的なスキルを学ぶいました。私は Python を使用して各種データ分析、Web アプリケーション、ゲームなど幅広い分野のソフトウェアを発表しています。\n\n私の趣味はプログラミングに伴われた新たな技術や手法の調査です。私は常に新しい情報を探し、新たな解決策を探すことに関心があります。\n\n私は Python のあらゆる機能に懐懐しいプログラミング言語の強さと、その柔軟性の強さに対する強い支持があります。私は Python を新たなプロジェクトの開発やコードの改善に使用する強力なツールとして、あらゆるプログラミングレベルの人にとって非常に強力な言語です。"
      }
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}

real    1m1.254s
user    0m0.044s
sys     0m0.008s

やっぱり微妙。

2Bにもしてみた質問をもう1度。

$ time curl -s -XPOST -H 'Content-Type: application/json' localhost:8080/v1/chat/completions -d \
    '{"model": "gemma-7b-it", "messages": [{"role": "user", "content": "九州で最も北にある県はどこですか?"}]}' | jq
{
  "created": 1709800992,
  "object": "chat.completion",
  "id": "a1a9dd86-3a0e-433f-9071-1c1a27143de0",
  "model": "gemma-7b-it",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "\n\n九州の最も北にある県は福岡県です。福岡県は九州の西部に位置し、北部に福岡県があります。"
      }
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}

real    0m12.077s
user    0m0.045s
sys     0m0.004s

答えは合っていますが、西にあるのか北にあるのかわかりませんね…。

ところでGemmaを使う際には<start_of_turn>などのトークンを指定する必要があるわけですが、今回は以下のように指定しています。

  template:
    chat_message: &template |-
      <start_of_turn>{{if eq .RoleName "assistant"}}model{{else if eq .RoleName "system"}}system{{else if eq .RoleName "user"}}user{{end}}
      {{if .Content}}{{.Content}}{{end}}
    chat:  &template |-
      <bos>{{.Input}}
      <start_of_turn>model
    completion:  &template |-
      {{.Input}}

プロンプトテンプレートのカスタマイズですね。

Customizing the Model / Example: Customizing the Prompt Template

以下のdiscussionをヒントにしています。

yaml file for Google's Gemma 2B & Gemma 7B · mudler LocalAI · Discussion #1736 · GitHub

微妙な返事が返ってきたりもしていますが、今回やりたいことはひととおりできました。

おわりに

GoogleのLLM、Gemmaをllama-cpp-pythonおよびLocalAIで試してみました。

量子化されたモデルを使ったということもありますが、少なくともTransformersで動かすよりはなんとかGemmaを体感できそうですね。

感覚的には、量子化された2Bのモデルをllama-cpp-pythonで動かすのが良さそうかなと思います。