meta-something

論文とか研究とか趣味のネタとか

型落ちゲーミングPCからはじめる自宅Kubernetes + GPU環境

この記事は whywaita Advent Calendar 2023 の23日目です。

皆さんは whywaita Advent Calendar とは何かご存じでしょうか?私はよくわからないまま参加しています。whywaitaらしき人物に事情を伺うと 2019年のほうれん草に関する記事 のURLを貼られて、さらによくわからなくなりました。

一方、近年は大規模言語モデルや生成AIの研究開発・それらを利用したサービスが極めて活発になっており、whywaita氏と関係が1mmもないとは辛うじて言い切れない株式会社サイバーエージェント日本語LLMを一般公開する など、国内外で大きな盛り上がりを見せています。

また、現代の一般のITエンジニアの自宅には「世代交代などで今は使われなくなったGPUマシン」が眠っている確率が68.3%程度ある[要出展]と言われており、廃棄するのが勿体ないor面倒くさいという理由で計算資源が放置されているケースが多々あります。

そこで今回はそうした休眠中の計算資源を再利用し、サイバーエージェントの公開する日本語LLMである OpenCalm の動作環境を整備して、 「whywaita advent calendar とは何か?」と聞いてみることにしました。

真面目に向き合う気持ちはないので どんな環境で動かしてもよいのですが、折角なので自宅にある型落ちのゲーミングPCを使って自宅にUbuntu Serverを立てて、その上でKubernetes + GPU環境を構築しましょう。 今時の一般のご家庭には型落ちのゲーミングPCはその辺に無限個くらい転がっているはずですからね。*1

自宅で電源ケーブルを抜かれて小物置きになっていた型落ちのゲーミングPC

1. Ubuntu Server と NVIDIA Driver のインストール

Ubuntu 22.04 インストール

whywaita Advent Calendar の読者でUbuntuのインストール作業を見たい人はあまりいないと思われるので、大幅に割愛します。 下の記事を参考にUbuntuインストール用のUSBメディアを作って Ubuntu Server 22.04 を入れました。

WindowsでUbuntuのインストールUSBメディアを作成する – diagnose-fix.com

マシン名は baccano にしました。理由はちょっと前の週末に平成の超名作アニメ作品 Baccano! を見返したからです。超名作なので超オススメです。*2

NVIDIA Driver のインストール

NVIDIA Driverは当初はaptでインストールしようと思っていましたが、Ubuntuの公式を見るとNVIDIA Driverのインストール手順の記載がありました。どうやら ubuntu-drivers を使って簡単に導入できるようなのでやってみましょう。

https://ubuntu.com/server/docs/nvidia-drivers-installation

root@baccano:~# ubuntu-drivers devices
== /sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0 ==
modalias : pci:v000010DEd00001F08sv000010DEsd000012FDbc03sc00i00
vendor   : NVIDIA Corporation
model    : TU106 [GeForce RTX 2060 Rev. A]
driver   : nvidia-driver-535-server-open - distro non-free
driver   : nvidia-driver-525 - distro non-free
driver   : nvidia-driver-525-server - distro non-free
driver   : nvidia-driver-450-server - distro non-free
driver   : nvidia-driver-535-server - distro non-free
...

落ちゲーミングPCには GeForce RTX2060 が刺さっていたらしいです。全然覚えてませんでした。 とりあえず最新の nvidia-driver-535-server を入れておきましょう。

root@baccano:~# ubuntu-drivers install nvidia:535-server

導入後は忘れずに再起動しておきます。

2. Docker + NVIDIA Container Toolkit のインストール

下書き時点ではひとつひとつ実行コマンド等を書いていたのですが「そんなに細かく書く必要ないな?」と思ってきたので、細かいコマンド実行は省いて巻いていきましょう。

Docker と NVIDIA Container Toolkit は以下のURLから手順通りに入れていきます。

Install Docker Engine on Ubuntu | Docker Docs

Installing the NVIDIA Container Toolkit — NVIDIA Container Toolkit 1.14.3 documentation

導入後に docker run --gpus=allGPUが認識できているか確認します。

root@baccano:~# docker run  --gpus=all  --rm -it nvcr.io/nvidia/cuda:12.3.1-runtime-ubuntu22.04 nvidia-smi
...
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.129.03             Driver Version: 535.129.03   CUDA Version: 12.3     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  NVIDIA GeForce RTX 2060        Off | 00000000:01:00.0 Off |                  N/A |
| 20%   46C    P0              23W / 160W |      0MiB /  6144MiB |      0%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+
...

ちゃんとDockerのコンテナからもGPU認識ができていそうです。

3. GPU + Kubernetes (Minikube) 環境の構築

Minikube

Kubernetes環境の作成の仕方は色々ありますが、今回は別にKubernetesで真面目にクラスタを組んだりすることが目的ではないので簡単な方法を選びます。 一般に、Kubernetes環境を簡単に試すだけであれば Minikubemicrok8s を使うのが楽です。今回はMinikubeを使います。

複数ノードを扱ってクラスタを組むことを見越した場合は microk8s や他の方法を取るべきですが、今回は型落ちゲーミングPC1台の予定なのでその時のことはその時の自分が考えます。

minikube.sigs.k8s.io

とりあえずさっと手順通りインストールしてから起動してみます。デフォルトではdockerのdriverが使われるはずです。

$ minikube start
😄  minikube v1.32.0 on Ubuntu 22.04
✨  Automatically selected the docker driver. Other choices: none, ssh
📌  Using Docker driver with root privileges
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
💾  Downloading Kubernetes v1.28.3 preload ...
    > gcr.io/k8s-minikube/kicbase...:  453.90 MiB / 453.90 MiB  100.00% 29.61 M
    > preloaded-images-k8s-v18-v1...:  403.35 MiB / 403.35 MiB  100.00% 19.19 M
🔥  Creating docker container (CPUs=2, Memory=3900MB) ...
🐳  Preparing Kubernetes v1.28.3 on Docker 24.0.7 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying Kubernetes components...
🌟  Enabled addons: storage-provisioner, default-storageclass
💡  kubectl not found. If you need it, try: 'minikube kubectl -- get pods -A'
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

CPUとMemoryが心許なさそうですが、Minikubeの起動には成功していそうです。 Minikubeの環境で動いているPodとNodeを確認してみましょう。

$ minikube kubectl -- get pod -A
NAMESPACE     NAME                               READY   STATUS    RESTARTS      AGE
kube-system   coredns-5dd5756b68-p887f           1/1     Running   0             98s
kube-system   etcd-minikube                      1/1     Running   0             111s
kube-system   kube-apiserver-minikube            1/1     Running   0             111s
kube-system   kube-controller-manager-minikube   1/1     Running   0             111s
kube-system   kube-proxy-t7fm8                   1/1     Running   0             99s
kube-system   kube-scheduler-minikube            1/1     Running   0             114s
kube-system   storage-provisioner                1/1     Running   1 (68s ago)   111s

$ minikube kubectl -- get node -o wide
NAME       STATUS   ROLES           AGE    VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
minikube   Ready    control-plane   2m5s   v1.28.3   192.168.49.2   <none>        Ubuntu 22.04.3 LTS   5.15.0-91-generic   docker://24.0.7

なるほど、こんな感じなんですね(はじめてMinikubeをまともに触った顔)

Minikube + GPU

公式ページを見てみたところ、Minikube で GPUを認識させる方法は利用しているdriverによって異なるようです。

今回はデフォルトの docker をdriverとして利用しています。

minikube.sigs.k8s.io

上記のドキュメントを軽く見た感じだと、おそらくこんな感じでしょうか?

  • docker の場合は Docker + NVIDIA Container Toolkit の機能をそのまま使う
  • none (bare-metal) の場合は NVIDIA Container Toolkit + NVIDIA Device Plugin (ドキュメントが更新されていない気がしますが)
  • kvm の場合は PCIパススルー + NVIDIA Container Toolkit + NVIDIA Device Plugin

今回のように driver が docker の場合はMinikube起動時の引数の指定だけで実現できそうなので、やってみましょう。ついでにCPUとMemoryの割り当ても増やします。

ちなみに minikube 環境を一度削除 (delete)まで しないと minikube start に渡す引数の設定が反映されないので注意です。

# 停止と削除
$ minikube stop
$ minikube delete
$ minikube start --cpus 6 --memory 8g --driver docker --container-runtime docker --gpus all 
😄  minikube v1.32.0 on Ubuntu 22.04
✨  Using the docker driver based on user configuration
📌  Using Docker driver with root privileges
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🔥  Creating docker container (CPUs=6, Memory=8192MB) ...
🐳  Preparing Kubernetes v1.28.3 on Docker 24.0.7 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
🔎  Verifying Kubernetes components...
    ▪ Using image nvcr.io/nvidia/k8s-device-plugin:v0.14.2
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟  Enabled addons: storage-provisioner, nvidia-device-plugin, default-storageclass
💡  kubectl not found. If you need it, try: 'minikube kubectl -- get pods -A'

CPU, Memory, GPUの引数で指定した設定がちゃんと反映されていそうな雰囲気を感じます。 このあとCUDAイメージのPodを作成して、Podの中のコンテナで nvidia-smi が実行できることを確認しました。

4. OpenCalm のモデルを動かしてみる

PyTorchイメージのデプロイ

さて、ようやくKubernetes環境ができました。次はOpenCalmのモデルを動かすコンテナ環境を整えましょう。 と言っても難しいことはなく、NVIDIAのPytorchイメージのコンテナをKubernetesのPodで作成して、必要なPyPIパッケージを入れるだけです。

本来のKubernetesの流儀では継続的にデプロイできるようにマニフェストを作成していくべきですが、アドカレにそんな真面目に向き合うのもどうかと思うのでコマンドでデプロイしていきましょう。

alias kubectl="minikube kubectl --"

# NVIDIAのPytorchイメージのPodを作成
kubectl run --image=nvcr.io/nvidia/pytorch:23.11-py3 pytorch --command -- sleep INF

# Podのコンテナにbashで接続
kubectl exec -it -n default pytorch -- bash

# コンテナの中で JupyterLab を8888ポートで起動
jupyter lab --allow-root --ip=* --port=8888 --no-browser --NotebookApp.token='' --NotebookApp.allow_origin='*' --notebook-dir=/

# 別のターミナルでPodの8888番をポートフォワード
kubectl port-forward pod/pytorch 8888:8888

ついでにVSCodeSSH機能でGPUマシンの8888ポートをポートフォワードして、手元のメインPCのブラウザから別PCのKubernetes環境上で動いているPodのJupyterLabまで繋げるようにします。

Pytrochイメージで起動したJupyterLab

OpenCalmの実行

ここまでくればあともう少し。次はOpenCalmの実行テストです。 まずは一番小さいモデルから動作確認をしていきましょう。

huggingface.co

pipで実行に必要な依存関係を入れていきます。

$ pip install transformers
$ pip install accelerate

あとはHuggingFaceのページにあるPythonコードを実行すれば動いてくれるはずです。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("cyberagent/open-calm-small", device_map="auto", torch_dtype=torch.float16)
tokenizer = AutoTokenizer.from_pretrained("cyberagent/open-calm-small")

inputs = tokenizer("Whywaitaによって私たちの暮らしは", return_tensors="pt").to(model.device)
with torch.no_grad():
    tokens = model.generate(
        **inputs,
        max_new_tokens=64,
        do_sample=True,
        temperature=0.7,
        top_p=0.9,
        repetition_penalty=1.05,
        pad_token_id=tokenizer.pad_token_id,
    )
    
output = tokenizer.decode(tokens[0], skip_special_tokens=True)
print(output)

サンプルをちょっと変更して「Whywaitaによって私たちの暮らしは」の続きの文章を生成してもらいましょう。

root@pytorch:/workspace/calm# python3 open-calm-small.py 
pytorch_model.bin:  72%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌                                                          | 273M/381M [00:09<00:02, 44.8MB/s]
pytorch_model.bin:  77%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉                                               | 294M/381M [00:09<00:02, 41.6MB/s]
pytorch_model.bin: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 381M/381M [00:11<00:00, 31.9MB/s]
generation_config.json: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 116/116 [00:00<00:00, 314kB/s]
tokenizer_config.json: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 326/326 [00:00<00:00, 900kB/s]
tokenizer.json: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 3.23M/3.23M [00:01<00:00, 3.21MB/s]
special_tokens_map.json: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 129/129 [00:00<00:00, 368kB/s]
...

次の文章が生成されました。

Whywaitaによって私たちの暮らしは、日々少しずつ変わっていきます。 そして、その変化が私たちの生活にどのような影響を与えるのかを考えるとき、私たちは何を拠りどころとして生活をしているのかを考えないといけません。 私たちは、自分がどのように感じているのか、どういう生き方をしているのか、何を大切にしているのかをまずは自分の内側に問いかけてみてください。 そうすると、自分のあり方が...

どうやら Whywaita とは自己の在り様に影響を与える哲学的な存在のようですね。 やや長かったこの記事も遂に核心に近付いてきた感じがします。

5. OpenCalm に whywaita Advent Calendar について聞く

遂に OpenCalm に whywaita Advent Calendarについて問う刻(とき)がやってきました。

whywaita Advent Calendar とはいったい何なのか、永らく謎に包まれていた存在の秘密が遂に明かされます。

ちなみにGPUのメモリ消費量的には余裕があったので、もう少しパラメータ数の大きい Open-calm-1B (1.4B) にモデルを変更しました。 ( open-calm-3b は流石にそのままだとメモリに乗り切らなくてダメでした)

なお、下の記事によると open-calm-3b のモデルのメモリ消費量は 6.5GB くらいだそうです。GeForce RTX 2060 のVRAMは6GBなので流石に厳しい。

CyberAgentの日本語言語モデルを試してみる - きしだのHatena

ということで先程と同じようにして open-calm-1b のモデルに「whywaita Advent Calendar とは、」の続きの文章を生成してもらいます。

root@pytorch:/workspace/calm# python3 open-calm-1b.py
config.json: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 610/610 [00:00<00:00, 1.59MB/s]
pytorch_model.bin: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2.94G/2.94G [00:34<00:00, 86.0MB/s]
generation_config.json: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 116/116 [00:00<00:00, 312kB/s]
tokenizer_config.json: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 323/323 [00:00<00:00, 887kB/s]
tokenizer.json: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 3.23M/3.23M [00:00<00:00, 4.69MB/s]
special_tokens_map.json: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 129/129 [00:00<00:00, 351kB/s]
...

次の文章が生成されました。

whywaita Advent Calendar とは、Kotlin で書かれた Learn オープンソースプログラミング言語 Koin のカレンダーアプリだ。 このカレンダーは Google Apps Script から起動され(そして当然のように AWS API Gateway と Route53 が使われている)、ユーザは CLI を介してブラウザからスケジュールやリマインダーを

もう一回やってみましょう

whywaita Advent Calendar とは、JavaScript の開発者向けのカレンダーです。このアドベントカレンダーでは各社の JavaScript エンジニアが交代でブログ記事を書き(今年は私が担当しました)、カレンダーに沿って1日ずつ紹介していくという趣向になっています。(なお去年 「PHPer-naoのHTML5とかWordPressな日々」というタイトルでした) それでは

ということで、whywaita Advent Calenderとは、Kotolin で書かれたカレンダーアプリ または JavaScript 関連の開発者向けカレンダー らしいです!

いかがでしたか?私は何もわかりませんでした。しかし、今回の記事の執筆を通して「気軽に遊べる自宅Kubernetes + GPU環境を構築する」という本当の目的は無事に果たせたので私は満足しています。

それでは、皆様よい年末年始と、よい whywaita Advent Calenderを!

完走した感想

  • Ubuntuをインストールするときに有線キーボードがなくて近所のヤマダ電機まで買いに行きました
  • UbuntuをインストールするときにLANケーブルの長さが足りなくて近所のヤマダ電機まで買いに行きました(2回目)
  • 当初想定していたよりも長い記事になってしまって少し反省しています
  • Minikubeは単に自宅Kubernetes + GPU環境のために選択したけれど、今回の目的ならDocker + GPU環境だけで十分だと思う

*1:聡明な読者の皆様方は既にお気付きの通り、この話は自宅にある型落ちゲーミングPCの再活用が本来の目的で、その他はすべてあと付けの適当な理由である。

*2:ただしバッカーノ!の第1話は「実質2週目の第1話」と比喩されるように初見では何もかもがわからないので、初めて見る方は第1話は適当に見流して、2話から本腰入れて視聴するのを推奨します。そしてすべてを見終わったあとに第1話を見て「そうだったのか…!」となってください。