🦜

【LLM】CPU環境でLlama-3-ELYZA-JP on gRPCを試してみた

2024/09/24に公開

はじめに

こんにちは。 @yu_yukk_y です!
さて、昨今LLMをベースとしたAI機能をアプリに組み込む会社・開発者が増えています。現状LLMをアプリに組み込む場合は

  • API経由で利用する(GPT-4o, Gemini-1.5-Proなど)
  • GPUが使用可能なサーバーにLLMを組み込んだアプリケーションをデプロイする
  • modalのような格安なGPUコンピューティングリソースサービスを用いる

の3つの手段が考えられますが、1・2はどちらの手段を取ってもランニングコストはそこそこな金額になってしまいますし、3のような独立したクラウドサービスはアプリケーションの要件次第では採用できないこともしばしばあります[1]

CPU環境でLLMが動かせればもっと用途の幅が広がるのに。。。と思っていた矢先、CPU環境でもLLMが動かせるようになるというllama.cppの存在を知りました。
また、このllama.cpplangchainから呼び出すことで、Pythonやlangchainの柔軟な機能と組み合わせてアプリケーションを作れることを知りました。

今回はllama.cppを用いて作成したgRPCサーバーのコードと、作ってみて得られた気づきについて書きます。

前提知識

Llama-3-ELYZA-JPとは

Llama-3-ELYZA-JPとは、東大松尾研究室発のAIカンパニー「ELYZA」が開発した日本語大規模言語モデルのことです。
この記事で使用したモデルLlama-3-ELYZA-JP-8Bは、meta社が開発したモデルMeta-Llama-3-8B-Instrucに対し、日本語における指示追従能力を拡張するための、日本語追加事前学習および事後学習を行ったものです。
この記事では後述するllama.cppで利用するために、このモデルの量子化バージョンであるLlama-3-ELYZA-JP-GGUFを使用しています。

https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF

また、こちらのモデルはMETA LLAMA 3 COMMUNITY LICENSEに準拠しています。使用する前にライセンスの内容を読み込んでから使用してください。

llama.cppとは

llama.cppとは、LLMに量子化技術を施すことで推論の高速化を図るツールです。これを用いることでローカルPCでも現実的な速度でLLMを利用することができます。
こちらについてはサイバーエージェントさんの技術記事にまとまっているため、より詳しくはそちらをご覧ください。
https://developers.cyberagent.co.jp/blog/archives/45308/

langchainとは

LangChain is a framework to build with LLMs by chaining interoperable components. LangGraph is the framework for building controllable agentic workflows.

(deepL訳)LangChainは、相互運用可能なコンポーネントを連結してLLMを構築するフレームワークです。 LangGraphは、制御可能なエージェントのワークフローを構築するためのフレームワークです。

公式にもこのように書かれているように、langchainはLLMモデルを組み合わせたり、ワークフローを作成したり、またRAGのようなLLMをより強力にする手法を実装しやすくしてくれるフレームワークです。
色々な機能が存在するのですが、今回はllama.cppをPythonから呼び出すためだけに使用します[2]

gRPCとは

gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.

(deepL訳)gRPCは、どのような環境でも実行できる最新のオープンソース高性能リモートプロシージャコール(RPC)フレームワークです。 ロードバランシング、トレース、ヘルスチェック、認証をプラグインでサポートし、データセンター内やデータセンター間のサービスを効率的に接続することができます。 また、デバイス、モバイル・アプリケーション、ブラウザをバックエンド・サービスに接続する分散コンピューティングのラストワンマイルにも適用できる。

公式に書かれている通り、gRPCはさまざまな環境で実行可能で高性能なRPCプロトコルを採用したフレームワークです。マイクロサービス間の通信などにしばしば用いられています。
can run any environmentと書かれていますが、実行する際はHTTP/2に対応した環境が必要です。
開発の際は.protoファイルにProtocol Buffersを使ったサービスの定義をかき、それをもとにさまざまなプログラミング言語のソースコードを自動生成して開発します。

こちらについてはさくらインターネットさんの技術記事にまとまっているため、より詳しくはそちらをご覧ください。

https://knowledge.sakura.ad.jp/24059/#gRPC

ソースコード

ソースコードは下記のものになっています。

https://github.com/Yoshino-Yukitaro/elyza-grpc-sample

実行する際はREADMEにしたがって環境を準備してください。

src/guide/guide.py

src/guide/guide.pyで実際にllama.cpp経由でLlama-3-ELYZA-JPを呼び出しています。

src/guide/guide.py
import multiprocessing

from langchain_community.chat_models import ChatLlamaCpp

llm = ChatLlamaCpp(
    temperature=0.1,
    model_path="./src/guide/Llama-3-ELYZA-JP-8B-q4_k_m.gguf",
    n_threads=multiprocessing.cpu_count() - 1,
    repeat_penalty=1.5,
    verbose=True,
)


def think_response_msg(request_msg):
    messages = [
        (
            "system",
            "あなたは国語の先生です。送られてきた文章に対して、適切な返答をしてください。なお、返事は日本語でお願いします。",
        ),
        ("human", request_msg),
    ]

    return llm.invoke(messages).content

langchain(ver 0.3)ではlangchain_community.chat_models経由でllama.cppの機能を呼び出すことができます。
ChatLlamaCppメソッドには多くの設定項目がありますが、調査のためここでは最低限しか設定していません。また、GPUを利用しないために一部の設定をあえて省いています。
設定次第でGPU & CPUを両方使用可能にし、よりパフォーマンスを高めることもできます。実行環境に余裕がある際は検討してみてください。
なお、全ての設定値はlangchainのドキュメントから確認できます。

https://python.langchain.com/api_reference/community/chat_models/langchain_community.chat_models.llamacpp.ChatLlamaCpp.html

think_response_msg関数ではLLMに「あなたは国語の先生です。送られてきた文章に対して、適切な返答をしてください。なお、返事は日本語でお願いします。」という役割を与えて、ユーザーから送られてきたメッセージ(request_msg)に対する返事を生成してもらっています。

src/proto/guide.proto

ここではgRPCの定義をProtocol Bufferを用いて記述しています。

src/proto/guide.proto
syntax = "proto3";

option java_multiple_files = true;
option java_package = "dev.yukky-sandbox.yukky-sandbox-guide-server.guide";
option java_outer_classname = "GuideProto";
option objc_class_prefix = "GID";

package guide;

// ゆっきーの砂場の案内をするAI機能の定義
service Guide {
  // ユーザーからのメッセージを受けてレスポンス内容を考える
  rpc ThinkResponseMsg (UserMsg) returns (GuideMsg) {}
}

// ユーザーからのメッセージ
message UserMsg {
  string msg = 1;
}

// AIのメッセージ
message GuideMsg {
  string msg = 1;
}

今回はmsgというキーでユーザーからのテキストを受け取り、msgというキーでAIからのメッセージをクライアントに返すように定義しています。
また、通信のやり取りはシンプルなUnary RPCを採用しています。

src/server/servicer.py

servicer.pyでは実際に上記の関数を呼び出すメソッドと、gRPCに渡すServicerクラスを作成しています。

src/server/servicer.py
from proto import guide_pb2
from proto import guide_pb2_grpc
from guide import guide


class GuideServicer(guide_pb2_grpc.GuideServicer):
    def ThinkResponseMsg(self, request, context):
        response_msg = guide.think_response_msg(request.msg)
        return guide_pb2.GuideMsg(msg="Hello, {}".format(response_msg))

実際に動かしてみて

まず、返答内容については「まずまず?」といった印象を受けました。日本語モデルと言いつつもベースがllamaなこともあり、指定しないと英語で返ってきます。
こちらは自分の用意したプロンプトの問題もありそうですし、調整次第では改善が可能かもしれません。
ローカル環境で動作させられるため、いくら調整しようがレート制限にかからないですし、お金もかからないのが最高ですね!!

また、LLMをCPUのみの環境で動かせるようになったことには感動しました。
(画像はメモリ16GBのApple M2 Proでの実行結果)
実行時の処理時間。合計で235835.46ms / 91 tokens

従来重いモデルを動かす際は、処理が1日では終わらなかったり、そもそもPCからすごい音がしだしたり。。なんてもことが多かったように思いますが、LLMのような重量なモデルを数分で動かせるようになったのは本当にすごいことです。
一方で、今回はCPUのみを搭載したサーバー環境で動作させて、実際のアプリケーションとして利用できないか?といった観点での検証を行っていたのですが、CPUのみの環境でアプリケーションとして動作させるにはまだスピードが追いついてなさそうな印象を受けました。実運用を考える際はまだGPUとセットで考える必要がありそうです。

脚注
  1. 自分の場合だと「LLMサーバーのエンドポイントは外部に公開したくない」という思いがあるため、運用前提のアプリケーションでは現状は選択肢から除外しています。しかし、modalの場合はbatch serverとしても運用できるため、そういったエンドポイントを閉じた用途での利用は今後考えていくかもしれません。 ↩︎

  2. llama.cppをPythonから試すだけならlangchainは不要です。本当は今回のコードを使って作りたかったものがあったためlangchainを用いていたのですが、自分の環境では求めていたものが実現できそうになかったため諦めてそのまま公開することにしました。 ↩︎

Discussion