vllm-openai dockerを使って、GPU nodeにLLM Webサービスを構築する
この記事について
こんにちは、東京大学鈴村研究室で、インフラエンジニアとしてお手伝いさせていただいています、福田と申します。
これまで、クラウド基盤mdxの上でKubernetes環境を構築し、サーバレスWebアプリケーションを開発するための手順や、分散学習を行うための手順について説明してきました。
今回のこの記事では、vllmというLLMエンジンのOpenAI Docker imageを用いて、GPUのworker nodeが存在するKubernetes上に、簡単にLLM Webサービスを構築するための手順について説明します。
以前、以下の記事でGPU worker nodeでLLM Webサービスを構築する方法を説明しました。
ただ、vllm-openai dockerを使えば、もっと簡単で軽量なLLM Webサービスを構築できることが分かり、今回のこの記事ではその手順について記載していこうと思います。前提条件
- mdx仮想マシン上で、Kubernetesクラスタが構築されていること
- GPU worker nodeが存在すること
- KNativeやMetalLBのインストールが済んでいること
デプロイしたいモデルを決める
まず、Hugging Faceでデプロイしたいモデルを探して決めます。
vllmに対応しているモデルは、添付画像の赤枠にvllmのタグが付いていたり、Use this modelのボタンを押すと、vllmの例が出てきたりします。
今回の記事では、gpt-ossの20 billionのモデルを用いて進めたいと思います。
deployment.yamlの作成
まず、Hugging Faceのモデルのページの赤枠部分をコピーして、モデル名をコピーします。
次に、以下のような、deployment.yamlを作成します。
その際、コピーしたモデル名を--model
の引数に指定します。
また、--max-model-len
は、推論時に扱える最大トークン長(シーケンス長)ですので、リソースに応じて適宜、適切な値を指定します。
私の環境では、mdxのGPUインスタンスであれば、32000のトークン数でも動きました。
--api-key
の引数は、APIにアクセスするアクセストークンとなりますので、第三者から推測されにくいものを用います。
また、metadata.nameの値が、WebサービスとしてデプロイしたときのURLのサブドメインとなりますので、例えば、モデル名が類推しやすい名前を使うと良いです。
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: gpt-oss-20b # これがWebサービスのサブドメインとなる
namespace: default
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: "1" # 最低でも1Podは起動させる
spec:
containers:
- image: vllm/vllm-openai:latest
args:
- "--model"
- "openai/gpt-oss-20b" # ここでコピーしたモデル名を指定する
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
- "--max-model-len"
- "50000" # 推論時に扱える最大トークン長。
- "--download-dir"
- "/large"
- "--api-key"
- "XXXXXXXXXXXXXX" # APIキーは第三者から推測されにくいものを用いる
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- mountPath: /large # Pod内のマウント先を指定する
name: pvc-large-volume
volumes:
- name: pvc-large-volume
persistentVolumeClaim:
claimName: hostpath-pvc-large
踏み台サーバへのログイン
いつもの通り、mdx踏み台サーバに接続します。
PCにインストールされたLENSからもアクセスできるように、kube masterへのssh portfowardを有効にします。
eval `ssh-agent`
ssh-add ~/.ssh/(ssh公開秘密鍵のファイル名)
ssh -L 6443:(master nodeのPrivate IPアドレス):6443 -A mdxuser@(踏み台サーバのGlobal IPアドレス)
Kubernetes Clusterの選択
Lensを開いて、対象のKubernetes Clusterを選択します。
ターミナルを開きます
deployment.yamlを使ったデプロイ
Lensのターミナルで、以下のコマンドを実行し、先ほど作成した、deployment.yamlをKubernetes Clusterにデプロイします。
kubectl apply -f (deployment.yamlへのpath)
Lens上で、サービスが立ち上がるのを確認します。
以下のコマンドを実行し、サービスにアクセスするためのURLを確認します。
kubectl get ksvc
以下ような結果が得られます。
NAME URL LATESTCREATED LATESTREADY READY REASON
pt-oss-20b https://gpt-oss-20b.default.example.com gpt-oss-20b-00001 gpt-oss-20b-00001 True
クライアントからのアクセス
今回デプロイしたLLM Webサービスは OpenAIのAPIに準拠していますので、PythonのOpenAIクライアントをアクセス元のPCなどにインストールします。
pip install openai
uvなどのパッケージマネージャーを使用している場合は、以下のコマンドインストールしてください。
uv add openai
次に、アクセスするためのスクリプトを作成します。
ここで、api_keyには、KNativeのWebサービス定義のyamlに定義した、api_keyの値と一致させます。
またアクセスするためのURLとしては、kubectl get ksvc
で得られたURLの末尾に、/v1
を付与したものを使用するようにします。
if __name__ == '__main__':
from openai import OpenAI
client = OpenAI(
base_url="https://gpt-oss-20b.default.example.com/v1", # URLの末尾には/v1を付与する
api_key="xxxxxxxxxxxxxx", # KNativeのapi_keyの値と一致させる。
)
completion = client.chat.completions.create(
model="openai/gpt-oss-20b", # KNativeの--modelの引数に指定した値を設定する
temperature=0.1,
messages=[
{"role": "user", "content": "フーリエ変換について教えてください"}
],
stream=True,
)
for chunk in completion:
content = chunk.choices[0].delta.content
if content:
print(chunk.choices[0].delta.content, flush=True, end="")
すると以下のような結果が得られ、正しくDeepSeekがデプロイされていることを確認できました。
## フーリエ変換(Fourier Transform)とは?
フーリエ変換は「時間(または空間)領域で表現された信号」を「周波数領域で表現された信号」に変換する数学的手法です。
簡単に言えば、ある波形がどんな周波数成分(音の高さ、色の波長など)で構成されているかを解析するための「窓関数」や「スペクトル」を得る方法です。
---
### 1. 基本的な定義
#### 連続フーリエ変換(Continuous Fourier Transform, CFT)
- 時間領域(または空間領域)の信号 \(x(t)\) を周波数領域の信号 \(X(f)\) に変換します。
\[
X(f) = \int_{-\infty}^{\infty} x(t)\, e^{-j2\pi ft}\, dt
\]
- 逆変換(Inverse Fourier Transform, IFT)
\[
x(t) = \int_{-\infty}^{\infty} X(f)\, e^{j2\pi ft}\, df
\]
#### 離散フーリエ変換(Discrete Fourier Transform, DFT)
- 時間領域の離散データ列 \(x[n]\)(\(n=0,\dots,N-1\))を周波数領域の離散データ列 \(X[k]\) に変換します。
東京大学鈴村研究室について
Discussion