ローカルマシンで動いている言語モデルをWebサーバーで一般公開するメモ

はじめに

  • ローカルマシンで大規模言語モデル(LLM)の推論サーバーを立てる

  • セキュリティも鑑み、踏み台を経由したapiサーバーを作る

  • フロントエンドのチャットページを作る

ということをします。

軽めのモデルであれば、HuggingFaceのZero GPUに課金するのが良いようですが、サイズが数十B以上になると、かなりサーバー代がかかるので、手持ちのGPUで推論させてみる、という試みです。


LLM推論サーバーを立てる

立ち上げ

推論モジュール&サーバーとして、vllmを使います。
transformersに比べて、推論が早いです。加えて、apiサーバーを簡単に立てられます。

vllmの使い方は、色々なところで解説されているので、割愛します。

サーバー立ち上げコマンドの例 (port=8000, api keyを12345とする場合)

python -m vllm.entrypoints.openai.api_server --model cyberagent/calm3-22b-chat \
--max-model-len 2048 --port 8000 \
--gpu-memory-utilization 0.9 --trust-remote-code \
--tensor-parallel-size 1 --api-key 12345

推論テスト

openaiのmoduleを使って簡単に推論できます。

from openai import OpenAI

# Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "12345"
port="8000"
openai_api_base = f"http://localhost:{port}/v1"
client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)

model_name = "cyberagent/calm3-22b-chat"
prompt="元気ですか。たぬきはすきですか"
messages = [{"role": "user", "content": prompt}]
completion = client.chat.completions.create(model=model_name,
                                            messages=messages,
                                            temperature=0.3,
                                            max_tokens=1024)
text = completion.choices[0].message.content.strip()
print(text)


Webサーバーとして公開する

上記のシステムをWebサーバーとして公開してみます。
サーバーをネット上に開放するというやり方もありますが、セキュリティやネットワーク設定などの都合上、難しいケースがあるかと思います。
そこで、ngrokという中継サービスを使います。

作業の流れ

  1. ユーザー登録 (github accountなどを使えるので、簡単です)

  2. 指示に従って、マシンのngrokをインストール

linuxでは

curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
	| sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null \
	&& echo "deb https://ngrok-agent.s3.amazonaws.com buster main" \
	| sudo tee /etc/apt/sources.list.d/ngrok.list \
	&& sudo apt update \
	&& sudo apt install ngrok

3.キーを登録

ngrok config add-authtoken (認証キー)

注意点
標準で、以下のファイルが生成されます。
/home/setup/.config/ngrok/ngrok.yml

version: "3"
authtoken: (認証キー)

という内容でした。 自分の環境では、versionを3から2にしないと、次の実行コマンドでエラーが出ました。

4.起動

ngrok http http://localhost:8000

起動すると、中継用のURLが表示されます。このURLにアクセスすると、中継サーバーを経由してローカルマシンに情報が転送される仕組みになっているようです。

推論時のopenai_api_baseを表示されたurlに修正することで、apiサーバーにローカルネットワーク外部からアクセスできるようになります。
api keyも設定しておきましょう。

from openai import OpenAI

# Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "12345"
port="8000"
openai_api_base = "https://a123-456-789-012-34.ngrok-free.app/v1" #ここは変える。/v1は忘れずに。
client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)

model_name = "cyberagent/calm3-22b-chat"
prompt="元気ですか。たぬきはすきですか"
messages = [{"role": "user", "content": prompt}]
completion = client.chat.completions.create(model=model_name,
                                            messages=messages,
                                            temperature=0.3,
                                            max_tokens=1024)
text = completion.choices[0].message.content.strip()
print(text)

フロントエンドのページを作る

最後の作業は、チャット用のページ作成 & 上記のapiを呼び出すシステムの作成です。

HuggingFaceのSpaceで簡単に作れるようです。この作業は初めてだったので、丁寧に書いていきます。

レポジトリ作成
New Spaceから、Gradioを選択して、chatbotを選択してみました。
推論はローカルGPUなので、hardwareはcpuで十分ですね。


レポジトリが作られると、それっぽい画面になりました。

画面の左下に、git cloneしろとの指示があったので、開発環境にcloneします。

git clone https://huggingface.co/spaces/(レポジトリ名)

コード修正

app.pyが本体のようです。重要そうなところだけ抜き取って見てみます。

client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")    
...
    for message in client.chat_completion(
        messages,
        max_tokens=max_tokens,
        stream=True,
        temperature=temperature,
        top_p=top_p,
    ):
        token = message.choices[0].delta.content

        response += token
        yield response

デフォルトではHuggingfaceのclientを呼び出しているので、ここを上記のopenaiに変えれば良さそうです。

試しに、以下のコードにしてみます。

import gradio as gr
# from huggingface_hub import InferenceClient
from openai import OpenAI

# Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "12345"
openai_api_base = "https://a502-131-112-63-87.ngrok-free.app/v1"
model_name = "cyberagent/calm3-22b-chat"
"""
For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
"""
# client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)


def respond(
    message,
    history: list[tuple[str, str]],
    system_message,
    max_tokens,
    temperature,
    top_p,
):
    messages = [{"role": "system", "content": system_message}]

    for val in history:
        if val[0]:
            messages.append({"role": "user", "content": val[0]})
        if val[1]:
            messages.append({"role": "assistant", "content": val[1]})

    messages.append({"role": "user", "content": message})

    response = ""

    for message in client.chat.completions.create(
        model=model_name,
        messages=messages,
        max_tokens=max_tokens,
        stream=True,
        temperature=temperature,
        top_p=top_p,
    ):
        token = message.choices[0].delta.content

        # response += token
        if token is not None:
            response += (token)
        yield response

requirements.txtにopenaiを入れておきます

huggingface_hub==0.22.2  
openai==1.43.0 #追加

コードを書き換え後、pushします。

反映後、webのコンソールっぽい画面で、buildを見てると、サーバーの中身が更新されていく様子が分かります。
(エラー関連は、Containerを見るのがおすすめ)


動きました。

セキュリティを改善する

urlやapi keyをapp.py中に組み込むと、セキュリティ上の問題があるので、環境変数で読み込むようにします。

レポジトリのsettingに、secretsという項目がありました。
ここに、urlとapi keyを入れました。


app.pyは、osの環境変数の設定でOKっぽいです。

最終的なコードの例は以下の通り。レイアウトなども変えました。

import gradio as gr
# from huggingface_hub import InferenceClient
from openai import OpenAI
import os
openai_api_key = os.getenv('api_key')
openai_api_base = os.getenv('url')
model_name = "weblab-GENIAC/Tanuki-8x8B-dpo-v1.0"
"""
For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
"""
# client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)


def respond(
    message,
    history: list[tuple[str, str]],
    # system_message,
    max_tokens,
    temperature,
    top_p,
):
    messages = [
        {"role": "system", "content": "以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい。"}]

    for val in history:
        if val[0]:
            messages.append({"role": "user", "content": val[0]})
        if val[1]:
            messages.append({"role": "assistant", "content": val[1]})

    messages.append({"role": "user", "content": message})

    response = ""

    for message in client.chat.completions.create(
        model=model_name,
        messages=messages,
        max_tokens=max_tokens,
        stream=True,
        temperature=temperature,
        top_p=top_p,
    ):
        token = message.choices[0].delta.content

        # response += token
        if token is not None:
            response += (token)
        yield response


"""
For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
"""


description = """
### [Tanuki-8x8B-dpo-v1.0](https://huggingface.co/weblab-GENIAC/Tanuki-8x8B-dpo-v1.0)との会話(期間限定での公開)
- 人工知能開発のため、原則として**このChatBotの入出力データは全て著作権フリー(CC0)で公開予定です**ので、ご注意ください。著作物、個人情報、機密情報、誹謗中傷などのデータを入力しないでください。
- **上記の条件に同意する場合のみ**、以下のChatbotを利用してください。
"""


HEADER = description
FOOTER = ""


def run():
    chatbot = gr.Chatbot(
        elem_id="chatbot",
        scale=1,
        show_copy_button=True,
        height="70%",
        layout="panel",
    )
    with gr.Blocks(fill_height=True) as demo:
        gr.Markdown(HEADER)
        gr.ChatInterface(
            fn=respond,
            stop_btn="Stop Generation",
            cache_examples=False,
            multimodal=False,
            chatbot=chatbot,
            additional_inputs_accordion=gr.Accordion(
                label="Parameters", open=False, render=False
            ),
            additional_inputs=[

                gr.Slider(
                    minimum=1,
                    maximum=4096,
                    step=1,
                    value=1024,
                    label="Max tokens",
                    visible=True,
                    render=False,
                ),
                gr.Slider(
                    minimum=0,
                    maximum=1,
                    step=0.1,
                    value=0.3,
                    label="Temperature",
                    visible=True,
                    render=False,
                ),
                gr.Slider(
                    minimum=0,
                    maximum=1,
                    step=0.1,
                    value=1.0,
                    label="Top-p",
                    visible=True,
                    render=False,
                ),
            ],
            analytics_enabled=False,
        )
        gr.Markdown(FOOTER)
    demo.queue(max_size=256, api_open=False)
    demo.launch(share=False, quiet=True)


if __name__ == "__main__":
    run()


動作の様子。

この記事が気に入ったらサポートをしてみませんか?