ローカルマシンで動いている言語モデルを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という中継サービスを使います。
作業の流れ
ユーザー登録 (github accountなどを使えるので、簡単です)
指示に従って、マシンの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()
動作の様子。
この記事が気に入ったらサポートをしてみませんか?