第6回 クラウド基盤mdx + Kubernetesでホスト側のディレクトリをマウントする方法
この記事について
こんにちは、東京大学鈴村研究室で、インフラエンジニアとしてお手伝いさせていただいています、福田と申します。
これまで、クラウド基盤mdxの上に、Kubernetes環境を構築し、KnativeというサーバレスWebアプリケーションの上で、LLMのWebアプリケーションなどをデプロイする手順を説明してきました。
ただ、LLMの学習済みパラメータファイルは非常に大きいため、仮想マシンのディスク容量を直ぐに食い潰してしまいます。
今回のこの記事では、mdxの仮想マシンから利用可能なLustreの高速ストレージ/fast
や、大容量ストレージ/large
をKubernetes側にマウントして使用する方法を解説します。
こうすることで、巨大なLLMの学習済みパラメータファイルによって仮想マシンのディスク容量を消費することがなくなりますし、パラメータファイルがキャッシュされるようになるので、一度デプロイしたモデルについては起動が速くなる効果が期待できます。
実施手順
Lustreストレージの確認
まず、いつもの通りに、ssh-agentを有効にして踏み台サーバにログインします。
eval `ssh-agent`
ssh-add ~/.ssh/mdx_access_key
ssh -A mdxuser@(踏み台サーバのIPアドレス)
次に、踏み台サーバから、Kubernetesのworker nodeにログインします。
ssh kube-gpu1
worker nodeでLustreの/fast
や/large
がマウントされていることを確認します。
マウントされていない場合、ansibleでの設定が上手く行っていない可能性があるので、踏み台サーバで、以下のコマンドを再実行してみてください。
ansible-playbook -i inventory.ini site.yaml --tags k8s_worker --ask-vault-pass
より詳しい手順は以下を参照ください。
もしそれでも上手く行かない場合は、以下のmdx公式手順に従って、手動でLustreストレージをマウントしてください。
ストレージがマウントされていることを確認したら、以下のコマンドで、ディレクトリの権限設定を変更します。
sudo chmod -R 777 /fast
sudo chmod -R 777 /large
Persistent Volumeの設定
次に、踏み台サーバに戻り、上記のLustreのディレクトリをKubernetesのPersistent Volumeとして使用できるようにします。
そのため、以下のyamlファイルを作成します。
# 先にPersistentVolumeを定義する
apiVersion: v1
kind: PersistentVolume
metadata:
name: hostpath-pv-large
spec:
capacity:
storage: 300Gi # Kubernetesにマウントしたいストレージの合計容量を指定する
accessModes:
- ReadWriteMany # 複数のPodが読み書き可能なモード
persistentVolumeReclaimPolicy: Retain
storageClassName: exa-large # PVCの名前と一致させる
hostPath:
path: /large # ホストマシン上のディレクトリを指定する
いつもの通り、上記のファイルを Kubernetes環境にデプロイします。
kubectl apply -f (Persistent Volumeのyamlファイルのpath)
Lensを確認し、Persistent Volumeが正しくデプロイされていることを確認します。
Persistent Volume Claimの設定
次に、Persistent Volume Claimを定義していきます。
Persistent Volume ClaimはPersistent Volumeとして定義したVolume領域全体に対して、使用したい容量を定義するものになります。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hostpath-pvc-large
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 250Gi # Persistent Volumeの容量の中で、このClainとして使用したい容量を指定する。 Persistent Volumeで定義したサイズを超えるとエラーになる
storageClassName: exa-large # Persistent VolumeのstorageClassNameと一致させる
いつもの通り、上記のファイルをKubernetes環境にデプロイします。
kubectl apply -f (Persistent Volume Claimのyamlファイルのpath)
Lensを確認し、Persistent Volume Claimが正しくデプロイされていることを確認します。
LLM Webアプリケーションの作成
次に、LLM Webアプリケーションのコードの作成を行って行きます。
ベースとなるコードは前回の記事で作成していますので、それを修正していきます。
以下のコードの日本語コメントがある部分を追加しています。
やっている事としては、DOWNLOAD_DIR
という環境変数が設定されていたら、それをLLMクラスのコンストラクタの引数として渡し、重みファイルの保存先を指示しています。
from fastapi import FastAPI, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field
from vllm import LLM, SamplingParams
from huggingface_hub import login
import os
if os.getenv("HF_TOKEN"):
login(token=os.getenv("HF_TOKEN"))
app = FastAPI()
model_name = os.environ["MODEL_NAME"]
# この部分を追加
if os.getenv("DOWNLOAD_DIR"):
# DOWNLOAD_DIRが環境変数として設定していたら、それを重みファイルの保存先として設定する
llm = LLM(model=model_name, tensor_parallel_size=1, download_dir=os.getenv("DOWNLOAD_DIR"))
else:
llm = LLM(model=model_name, tensor_parallel_size=1)
class TextRequest(BaseModel):
prompt: str
temperature: float = Field(default=0.1, ge=0, le=1)
max_tokens: int = Field(default=4000, ge=1)
@app.post("/llm")
async def summarize_text(request: TextRequest, credentials: HTTPAuthorizationCredentials = Security(HTTPBearer())):
auth_token = os.environ["AUTH_TOKEN"]
if credentials.credentials != auth_token:
raise HTTPException(status_code=404, detail="Not Found")
sampling_params = SamplingParams(temperature=request.temperature, max_tokens=request.max_tokens, top_p=0.9)
output = llm.generate([request.prompt], sampling_params)
result = output[0].outputs[0].text.strip()
return {"result": result}
修正が完了したら、必要に応じて、docker compose upで動作確認をします。
その際、compose.yamlの環境変数として、DOWNLOAD_DIR
を指定することを忘れないようにしてください。
services:
app:
build: .
ports:
- "8000:8000"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [ gpu ]
environment:
AUTH_TOKEN: password
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,utility
MODEL_NAME: "llm-jp/llm-jp-3-7.2b-instruct3"
DOWNLOAD_DIR: "/app/model_cache" # この部分を定義する
動作的に問題なさそうであれば、以下のコマンドでdocker build & pushします。
docker build . -t (作成したレポジトリ名):latest
docker tag fast_api_llm:latest (Docker Hubのアカウント名)/(作成したレポジトリ名):latest
docker push (Docker Hubのアカウント名)/(作成したレポジトリ名):latest
KnativeへのWebアプリケーションのデプロイ
次に、作成したDocker Imageを使って、KnativeへのWebアプリケーションのデプロイを行います。
そのため、以下のようなyamlファイルを定義します。
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: llm-jp-3-150m-instruct3
namespace: default
spec:
template:
spec:
containers:
- image: (Docker Hubのアカウント名)/(作成したレポジトリ名):latest
resources:
limits:
nvidia.com/gpu: 1
env:
- name: NVIDIA_VISIBLE_DEVICES
value: all
- name: NVIDIA_DRIVER_CAPABILITIES
value: compute,utility
- name: AUTH_TOKEN
value: 6Ryyj0RxIskQpLFw # 推測されにくいランダムな文字列を認証トークンとして設定
- name: MODEL_NAME
value: "llm-jp/llm-jp-3-150m-instruct3"
- name: DOWNLOAD_DIR
value: "/large" # volumeMounts.mountPathのpathと一致させる
ports:
- containerPort: 8000
volumeMounts:
- mountPath: /large # Pod内のマウント先を指定する
name: pvc-large-volume # volumes.nameで定義した名前と一致させる
imagePullSecrets:
- name: regcred
volumes:
- name: pvc-large-volume # 任意の名前を定義して良い
persistentVolumeClaim:
claimName: hostpath-pvc-large # 定義したPersistent Volume Claimの名前を指定する
前回の記事との差分は、ホスト側のVolumeをマウントするための定義が追加されている点です。
yamlファイルを保存し、いつものkubectl apply
の手順でデプロイを行います。
kubectl apply -f (作成したyamlファイルのpath)
Lensで対象のDeploymentを選択し、logのアイコンをクリックすると、起動ログを確認できます。
最後に、正しくホスト側のディレクトリにモデルが保存されているかを確認します。
以下のコマンドで、GPUサーバにログインします。
ssh kube-gpu2 or kube-gpu1
/large
領域のディレクトリを参照し、重みファイルが正しく保存されていることを確認します。
実行コマンド
ls -l /large/
得られる結果
total 16
-rw-rw-rw- 1 root root 0 Mar 13 17:50 637d21043d8912ea898b17abbd63ac52fbf1c210baab0072d17c3385528287c3llm-jp-llm-jp-3-7.2b-instruct3.lock
-rw-rw-rw- 1 root root 0 Mar 10 11:45 8d0d11b5fcc6e09c27e6df709b633e5147fa939bb1ad5df177c6ca8a0ddb2da6llm-jp-llm-jp-3-13b-instruct.lock
drwxr-xr-x 5 root root 4096 Mar 10 09:48 models--llm-jp--llm-jp-3-13b-instruct
drwxr-xr-x 5 root root 4096 Mar 10 09:21 models--llm-jp--llm-jp-3-7.2b-instruct3
東京大学鈴村研究室について
Discussion