見出し画像

オープンソースなLLM(calm2-7b)のCPU推論エンドポイントをAzureとllama-cpp-pythonでシュッとつくる

こんにちは、https://twitter.com/iryutab という者です。

はじめに

12/05 追記: この方法は、デプロイが有効である間ずっとインスタンスが起動しっぱなしになってしまいます(基本VMです)。一週間少しで数万円に膨れてしまうので、使っていないときには停止させましょう。節約や、実際のプロダクトへの組み込みの際にはアクセスが有る際だけインスタンスを起動する形のContainer Appなどが適していると思うので、こちらも近々動作させて記事化します。

オープンソース(ローカル)LLMの発展は日々凄まじいですが、いざそれを活用して遊ぼうと思うと、デプロイという課題がついて回ります。
2023年11月現在のLLMでは、エッジデバイスで推論できるくらいの大きさのモデルだと性能が微妙ですし、クラウドを使おうにもGPU付きインスタンスの確保は中々困難です。
しかしColabで動かして感心して終わり、というのも勿体ないので、何とかしてクラウドに自分専用の推論APIを立てたいと思います。

※公開後追記※
この記事の内容は「とりあえず」動かすには確かに楽なのですが、24時間マシンが立ち上がってしまうので、流石にコスト面的に明らかにベターな方法があり、実践してみています。手が空いたらそちらの記事も書きますので是非!


注意

筆者はAzureド初心者です。
OpenAIとMSの騒動をきっかけにAzureに興味を持ちアカウント作成をして、その日のうちにこの記事の内容を実践して動作させたため、諸々の実行手順にムダがあったり、アンチパターンを踏んでいる可能性がかなりあります。
とりあえず動いたよ!」「とりあえずこうすると使えるよ!」程度の情報であること前提でお願いします。

逆に言えば、初めてAzureを触った人がごちゃごちゃ半日いじるだけで動くくらい簡単です。Do try it.

基本のアイディア

  • Azure上のAI関連プロダクトがまとまった「Azure Machine Learning Studio」(以下AzureML) では、モデルのアップロードからデプロイまで一括して簡単に管理できます。

  • AzureML上の「モデル」は、一般にはPyTorchなどの重みをMLFlowという形式に変換して扱いますが、実はオリジナルの推論コードと推論環境用のDockerfileを用意すれば、任意のファイル(!)をアップロードし、他のモデルと同様に管理することができます。

  • そこで、llama.cppで用いられるggufファイルをモデルとしてAzureMLにアップロードし、推論用の簡単なpythonコードをllama-cpp-pythonを使って書き、それを実行するためのミニマルなDockerfileと組み合わせることで、お手軽にggufモデルを動かしてくれるエンドポイントを生やします。

必要なもの

  • Azureアカウント

    • https://signup.azure.com/  から作りましょう。

    • 最初アカウントを作ると、200USDぶんのクレジットがもらえます。

    • この記事では、アカウントの作成・カードの登録などの手順はカバーしません。が、概ね こちらの記事 のとおりです。

  • 動かしたいモデルのgguf変換済みファイル

    • Hugging faceで公開されているものでも、自分でファインチューンしたものでもLoRAアダプトしたモデルでもおkです。

    • ただし、事前にllama-cpp-python使ってシュッと推論できるか確かめておくとより良いと思います。

  • 推論用pythonスクリプト

    • あとで書きましょう。かんたんです。

  • 実行環境用Dockerfile

    • あとで書きましょう。かんたんです。

手順

1. Azure Machine Learning ワークスペースを作成する

Azure Portalから、「Azure Machine Learning」と検索します。

検索結果

クリックするとワークスペース一覧が表示されます。まだ何も無いはずですので、作成します。

ワークスペースを作成する画面。レジストリは複数人で作業するときに便利ですが、一人で作業する場合には、「新しいワークスペース」で構いません。

作成画面では、以下のものが必要となります。

  • サブスクリプション

  • リソースグループ

  • 名前

  • リージョン

  • ストレージアカウント

  • キーコンテナー

  • Application Insights

作成していないものもあると思いますが、この画面から新規作成できるので、気にせず名前を決めていきましょう。

上からリソースグループの名前とワークスペースの名前を適当に入力すると、ストレージアカウント以下の名前は勝手に入力してくれますので、そのまま作成してしまいます。

適当に入力した例
デプロイ中…
完了!

作成ボタンを押すとデプロイが開始され、暫く待つとワークスペースが作成されます。「リソースに移動」を押し…

スタジオが起動できるように

「スタジオを起動」して、AzureML Studioに移動しましょう。

2. Modelの作成

AzureML Studioを起動すると、このような画面になるはずです。

AzureML Studioホーム画面

左側メニューより「Models」を選択し、ローカルファイルからモデルをアップロードしましょう。

Models管理画面
From local filesを選択

これで開くモーダル上で、「Model Type」で「Unspecified Type」を選択し、「Browse」から予めダウンロードしておいた.ggufファイルを選択します。

モデルの選択

今回は前述の通り、CyberAgentさんのcalm2-7b-chatモデルをmmngaさんが.gguf変換してHugging Faceで公開されているものを利用しています。

「Next」を押下すると、名前や説明などの入力を求められるので適当に入力します。

適当に入力。バージョンは空白にしておけば1からインクリメントします

「Next」を押すとレビュー画面が開くので、確認して「Register」しましょう。

レビュー画面。ggufモデルでも簡単にアップロードできるのが良いですね

「Register」ボタンを押してからファイルのアップロードが始まるので、それなりに時間がかかります。

完了するとModels一覧にアップロードしたモデルが見えるはずです。

やったね!

3. Environmentの作成

Environmentは、推論用のコードを実行するための実行環境で、主にDockerfileで構成します。

まず、AzureML Studio 左手メニューより「Environments」を選択します。

Environmentsを選択

「Custom environments」タブから、「Create」を選択します。

Custom environments → Create

適当な名前を入力し、「Create a new docker context」を選択します。

Create a new docker context

すると、サラッとインラインのDockerfileエディタが表示されます。

ここでDockerfileを書く

基本的には、
1. mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04 イメージをベースに構築すること
2. azureml-mlflow がインストールされていること

がEnvironmentsとしての最低条件のようなので、ここにllama-cpp-pythonを動かすためのコードを追加していきます。

私の場合、最低限のコードとして以下を使用しました:

FROM mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04

# llama-cpp-pythonとazuleml-mlflow, azureml-inference-server-httpをインストール
RUN pip install llama-cpp-python azureml-mlflow azureml-inference-server-http

# ワークディレクトリを設定
WORKDIR /usr/src/app

llama-cpp-pythonパッケージのインストールはもちろんなのですが、
azureml-inference-server-http パッケージも (何故かテンプレには含まれていないものの) 必要らしく、これの存在を知らずに実行したところこのパッケージが不足している旨のエラーが発生したので追加しています。

上記のコードをコピペ

コードを書いたので、「Next」を押します。

タグ追加画面。今回は空白で構いません。
最終レビュー画面。

この内容で問題ないので、Createしてしまいます。しばらく待つと作成が完了します。

作成完了。imageのビルドまで勝手にやってくれます

4. score.py (推論用スクリプト) の用意

これで実行環境と推論するモデルが用意されましたが、AzureMLが想定しているMLFlow互換のモデルではないため、オリジナルの推論コードが必要です。

シュッと作ってしまいましょう。まず、Azure公式のExampleを見てみます。
(何故かGithub上のファイルへのリンクを埋め込めないので、生リンク)
https://github.com/Azure/azureml-examples/blob/main/cli/endpoints/online/model-1/onlinescoring/score.py

Exampleのコメントだけ日本語翻訳したものを置いておきます。

import os
import logging
import json
import numpy
import joblib


def init():
    """
    この関数は、コンテナが初期化/起動されたとき、通常はデプロイメントの作成/更新後に呼び出されます。
    メモリ内でモデルをキャッシュするなど、初期化操作を行うロジックをここに記述できます
    """
    global model
    # AZUREML_MODEL_DIRはデプロイ時に作成される環境変数です。
    # モデルフォルダへのパス(./azureml-models/$MODEL_NAME/$VERSION)です
    # モデルのフォルダ名がある場合は、その名前を提供してください
    model_path = os.path.join(
        os.getenv("AZUREML_MODEL_DIR"), "model/sklearn_regression_model.pkl"
    )
    # sklearnモデルに戻すためにモデルファイルを逆シリアライズします
    model = joblib.load(model_path)
    logging.info("Init complete")


def run(raw_data):
    """
    この関数は、エンドポイントの呼び出し毎に実際のスコアリング/予測を行うために呼び出されます。
    例では、json入力からデータを抽出し、scikit-learnモデルのpredict()メソッドを呼び出し、
    結果を返します。
    """
    logging.info("model 1: リクエストを受信しました")
    data = json.loads(raw_data)["data"]
    data = numpy.array(data)
    result = model.predict(data)
    logging.info("Request processed")
    return result.tolist()

というわけでこれをベースに、llama-cpp-pythonを用いた簡単な推論コードを書きます。

import os
import json
import logging
from llama_cpp import Llama

def init():
    global llm
    try:
        model_path = os.path.join(
            os.getenv("AZUREML_MODEL_DIR"), "cyberagent-calm2-7b-chat-q4_K_M.gguf"
        )
        llm = Llama(model_path=model_path, n_ctx=32768)
        logging.info("Model loaded.")
    except Exception as e:
        logging.error(f"Failed to load model. Work termination. {e}") 
        raise e


def run(data) -> dict:
    data = json.loads(data) if isinstance(data, str) else data
    prompt = data.get("prompt")
    repeat_penalty = data.get("repeat_penalty")
    temperature = data.get("temperature")

    if not prompt or repeat_penalty is None or temperature is None:
        raise ValueError("Data is missing required fields: 'prompt', 'repeat_penalty', 'temperature'")

    answer = llm(prompt, max_tokens=25000, echo=True, repeat_penalty=repeat_penalty, temperature=temperature)
    logging.info("Inference done.")
    return answer

このファイルをscore.py (名前はお好みで) として保存しておきます。

5. Endpointの作成

AzureMLの左手メニューから、「Endpoints」を選択します。

Endpoints

「Create」を押してEndpointの作成に進みます。

Createしましょう

Model選択画面になるので、先程アップロードしたggufモデルを選択し、「Select」します。

.先程のものを選択

諸々設定が出てきますが、適宜名前だけ設定して、あとは触らなくて問題ありません。

名前だけ好きに設定しましょう。

さっき選んだのですが、またModel選択画面です。何もせずNext。

Next!

デプロイに関する諸々の設定です。とりあえず動かすだけであれば何もいじらずに進んで問題ありません。

Next!

次が重要です。ここで「scoring script for inferencing」に先程用意した推論用のスクリプト、そして「Select environment」でこちらも先程用意した実行環境を選択します。

推論用スクリプトと実行環境を選択

次に、計算資源を選択します。今回の例で使用しているEast US 2リージョンでは、GPUインスタンスはQuotaがないのでそもそも選択できません (他リージョンでも同じかも?)

今回推論する7b, 4bit量子化モデルではStandard_E4s_v3程度で十分かと思いますが、高速にしたい場合やモデルサイズによって適宜何種類か試してみると良いかと思います。

Instance countも、とりあえず自分用に推論するぶんには1台で十分かと思います。

計算資源を選択

次に、このエンドポイントへのトラフィックのうち、どれくらいの割合をこのデプロイメントに割り当てるかを選択します。が、このエンドポイントには今設定している一つのデプロイしか無いので、必然的に100%になります。そのままNext。

アロケーション

最後にレビューして、Nextでデプロイが開始されます。

最終確認


エンドポイント作成のジョブ送信が完了し、エンドポイント一覧に表示されました

この一連の操作は、内部的に「エンドポイント」と「デプロイメント」の2つを連続で生成してくれます。そのため、エンドポイント一覧に出てきてもデプロイが完了するまでにはそれなりな時間がかかります。

大体10分程度かかるので、紅茶かコーヒーでも淹れてきましょう。

「エンドポイント」の作成が走っている状態のステータス
「デプロイメント」の作成が走っている状態のステータス


完了!

推論させてみる

とりあえずGUIで動作確認

動作確認は、AzureML Studio上からGUIで簡単に可能です。「Test」タブに移動して、データを入力、Testしてみましょう。

今回の推論コードではプロンプト、Repeat Penalty、Temperatureだけを受け取るようにしているので、それをjsonで与えます。推論コードを変えればカスタマイズできます。

実行してみると…

まどマギプロンプトはもう定番ですね

ちゃんと動いています。

PythonでAPIコールしてみる

GUI上で試すだけでも仕方がないので、Pythonから呼び出してみましょう。

実は、デプロイに成功すると勝手にサンプルの推論コードを作ってくれます。「Consume」タブを開きます。

ご丁寧なことにサンプルコード

ここで推論用のREST API EndpointとAPIキー、サンプルコードが閲覧できます。

ただしこのままのコードだとデータのフォーマットが含まれていなかったり、レスポンスの生データを返してしまうため、コメントの日本語訳とともに少々変更したバージョンを作成しました。

import urllib.request
import json
import os
import ssl

def allowSelfSignedHttps(allowed):
    # クライアント側でサーバー証明書の検証をバイパス
    if allowed and not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None):
        ssl._create_default_https_context = ssl._create_unverified_context

allowSelfSignedHttps(True) # 自己署名証明書を使用するスコアリングサービスで必要です。

# リクエストデータはこちら
# 下記の例はJSON形式を想定していますが、エンドポイントが期待する形式に応じて更新される可能性があります。
# 詳細はこちらを参照してください:
# https://docs.microsoft.com/azure/machine-learning/how-to-deploy-advanced-entry-script
data = {
    "prompt": "USER: こんにちは、お元気ですか? \n ASSISTANT:",
    "repeat_penalty": 1.3,
    "temperature": 0.9
}

body = str.encode(json.dumps(data))

# エンドポイントのREST API URLをここに入れてください
url = ''
# エンドポイントに対するプライマリ/セカンダリキーをここに入れてください
api_key = ''
if not api_key:
    raise Exception("A key should be provided to invoke the endpoint")

# azureml-model-deployment ヘッダーはリクエストを特定のデプロイメントに強制的に送るために使用されます。
# エンドポイントのトラフィックルールを遵守するためには、このヘッダーを削除してください
headers = {'Content-Type':'application/json', 'Authorization':('Bearer '+ api_key), 'azureml-model-deployment': 'calm2-7b-chat-gguf-1' }

req = urllib.request.Request(url, body, headers)

try:
    response = urllib.request.urlopen(req)

    result = response.read()
    # バイト文字列をデコードしてJSONオブジェクトに変換
    result_json = json.loads(result.decode('utf-8'))
    # 'choices'の中の'text'フィールドを取得
    text = result_json['choices'][0]['text']

    print(text)
except urllib.error.HTTPError as error:
    print("The request failed with status code: " + str(error.code))

    # ヘッダーを出力します - リクエストIDとタイムスタンプが含まれており、障害のデバッグに役立ちます
    print(error.info())
    print(error.read().decode("utf8", 'ignore'))

実際にローカルファイルに保存し、実行してみましょう。

中々クセが強い

内容はともかく、無事に生成されました。

今後できること

さて、とりあえず動きましたが、企業として実運用したり、また個人メイカーのレベルでの利用でも、もう少し改善できる幅があります。

  • APIキーよりも高度な認証手段

    • 公開するプロダクトでAPIを叩く場合にはあったほうが安心です

  • スケーリング幅の確保

    • トラフィック増大時には必要ですね

  • llama-cpp-pythonを使わず、llama.cppをビルドしたバイナリで直接推論

    • 多分高速化できます

  • その他高速化・最適化、コスト対策

    • Azure初心者なのでここは本当に何もわからないのですが、多分かなり改善幅あると思います…

個人でものづくりに利用する範囲として楽しく使えるレベルのデプロイは実現できましたが、もしこの辺りご存じの方いらっしゃいましたら、是非実践記事を公開いただけたらうれしいです。

私はとりあえず、これをスーパーカワイイロボットことAIスタックチャンに組み込んで行こうと思います。

部品が届かないのでまだ机の片隅に静かに佇んでいます。

最後までありがとうございました。何かご指摘点などあれば、 https://twitter.com/iryutab までお願い致します。

おわり

いいなと思ったら応援しよう!

この記事が参加している募集