🎉

第6回 クラウド基盤mdx + Kubernetesでホスト側のディレクトリをマウントする方法

2025/03/25に公開

この記事について

こんにちは、東京大学鈴村研究室で、インフラエンジニアとしてお手伝いさせていただいています、福田と申します。

https://sites.google.com/view/toyolab/鈴村研究室概要

これまで、クラウド基盤mdxの上に、Kubernetes環境を構築し、KnativeというサーバレスWebアプリケーションの上で、LLMのWebアプリケーションなどをデプロイする手順を説明してきました。

https://zenn.dev/suzumura_lab/articles/627b5063d6884d

ただ、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

より詳しい手順は以下を参照ください。

https://zenn.dev/suzumura_lab/articles/c1de704e627003#ansible-playbookでのkubernetesクラスタの構築と設定

もしそれでも上手く行かない場合は、以下のmdx公式手順に従って、手動でLustreストレージをマウントしてください。

https://docs.mdx.jp/ja/#ubuntu20.04%2C ubuntu22.04 仮想マシンテンプレートの場合

ストレージがマウントされていることを確認したら、以下のコマンドで、ディレクトリの権限設定を変更します。

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

東京大学鈴村研究室について

https://sites.google.com/view/toyolab/鈴村研究室概要

Discussion