Raspberry Piクラスタにおける軽量LLMのKubernetes Pod化と推論

目的:

本稿では、3台のRaspberry Piを用いて高可用性(HA)構成のKubernetesクラスタを構築し、その上で軽量LLMであるPhi-3 Miniの動作検証を行うことを目的とします。初期段階として、単一のRaspberry Pi上でのPhi-3 Miniの基本的な動作確認を行い、その様子を動画で示します。最終的には、構築したKubernetesクラスタ上にPhi-3 MiniをPodとしてデプロイし、分散環境下でのLLM推論の実現を目指します。

まずは、普通にラズパイで動かして見ましょう。 まずはPhi-3 Miniです。動作中の動画を御覧ください。

どうでしょうか。ちょっとゆっくりですがちゃんと動いている感じがします。

Phi-3 Mini とは?

  • Microsoft Researchが開発した、小型で高性能なLLM(小規模大規模言語モデル)
  • パラメータ数:約 3.8B (38億)
  • モデルサイズ:Phi-3 Mini Q4_0(量子化済み)で 約1.8~2.2GB
  • 推論フレームワーク:主に ONNX / GGUF / PyTorch に対応

✅ Raspberry Pi 5 のスペック(16GBモデル)

項目
CPU4-core Cortex-A76 @2.4GHz
RAM最大16GB LPDDR4
GPUVideoCore VII
OSRaspberry Pi OS (Debian)
ストレージUSB SSD推奨(microSDは遅い)

環境の構築手順は以下のブログに整理しています。

以下の条件を満たす必要があります:

✅ 条件まとめ:

条件内容
✅ モデル形式GGUF 形式 + Q4_0 または Q4_K_M の量子化済み
✅ 実行エンジンllama.cpp(CPU)またはllama-cpp-python
✅ 実行環境swap有効 + 物理メモリ16GB + SSD推奨
✅ スレッドn_threads=4〜6(Piのコア数に合わせて調整)
⚠️ 推論速度遅い(2〜5トークン/秒が限界)

llama.cpp のビルド(Raspberry Pi 上)

$ sudo apt update
$ sudo apt install -y build-essential cmake libcurl4-openssl-dev
$ git clone https://github.com/ggerganov/llama.cpp
$ cd llama.cpp
$ mkdir build && cd build
$ cmake ..
$ make -j4
Zsh

モデルの準備

$ mkdir -p ~/models/phi3 && cd ~/models/phi3
$ wget https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf
Zsh

実行

$ cd ~/llama.cpp/
$ ./build/bin/llama-cli -m ~/models/phi3/Phi-3-mini-4k-instruct-q4.gguf -p "日本で2番目に高い山は?" -n 128
Zsh

動きましたか?!実行成功おめでとうございます!しかし…この回答、内容的には完全に間違っていますよね。

❌ モデルの誤答内容の分析

✖ 「富士山の竜嶺(ふじさんのたんげり)」

  • そんな名前の地形は存在しませんw。モデルが架空の用語を生成しています。

✖ 「富士山自体が1,238.6メートル」

  • 富士山は日本で最も高い山であり、標高は 3,776m
  • モデルが「次に高い山」と「最高の傾斜」を混同して誤情報を生成しています。

✅ ちなみに正しい答えは?

  • 日本で2番目に高い山は:北岳(きただけ)
  • 山梨県南アルプス市、標高 3,193m

このような 小型LLM(3.8B) は、まだ「事実知識」の保持が不完全です。

推論において「もっともらしいが間違った文」を作りがちです(ハルシネーションと呼ばれる現象です)。

特に地理・数字・固有名詞は苦手な傾向があります。

日本語入力に英語学習済モデルが部分的対応している状態なので、言語的な混乱も加わっています。

ですが、日本語の処理能力はまずまずなので、RAGで知識を与えた上で、答えてもらえればもしかしたら使えるかも?しれませんね。

🔹 1. サーバー起動

llama-serverllama.cpp に付属している 軽量なLLM APIサーバー で、モデルをバックエンドにして HTTP経由で推論リクエストを送れる便利なツールです。

以下のようにして起動します:

$ ./build/bin/llama-server -m ~/models/phi3/Phi-3-mini-4k-instruct-q4.gguf -c 2048 -n 128 --host 0.0.0.0 --port 8080
Zsh

🧪 Webブラウザからも簡単に試せます

http://<PiのIP>:8080/ を開くと、簡易Web UIが出てきます。

では本題にうつりましょう。Kubernetesで実行したいのでした。

LLMをKubernetesで動かそう

🎯 設計要件(確認)

観点内容
モデル選定Llama 2 7B Q4 または Phi-3 Mini Q4(軽量)
推論エンジンllama.cpp または llama-cpp-python(Python連携)
起動方式Docker化 → Kubernetesデプロイ(GPU非前提構成)
API提供FastAPI or Flask によりREST化し、他サービスと連携
メモリ要件2〜4GB想定(Q4量子化モデルでRasPi 5に適合)
Pod配置他コンポーネントと干渉しないようにnodeSelector / affinityで制御(必要に応じて)

作業手順

  1. Dockerfileを作る。
  2. Dockerビルドを行いイメージを用意する。
  3. イメージを直接各ノードに配布
  4. PVCを作成し、モデルを配置する
  5. DeploymentマニュフェストでKubernetesに配置を行う。
  6. Serviceを設定し、アクセスできるようにする。

なお、Docker Build したローカルのイメージを Kubernetes クラスタで使う方法には、大きく3つの選択肢があります。

方法1:DockerHubなどのパブリックレジストリにPush(最も一般的)

方法2:ローカルレジストリ(軽量HTTPレジストリ)を構築

方法3:イメージを直接各ノードに配布する(スモール構成におすすめ)

今回は試験用ラズパイクラスタですから直接 docker load 配布したいと思います。

Raspberry Pi 5 に Docker をインストールする手順

まずはDockerをインストールしましょう。

# 事前準備
$ sudo apt update
$ sudo apt install -y ca-certificates curl gnupg
# Docker公式GPGキーを追加
$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# DockerのAPTリポジトリを追加
$ echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Dockerエンジンをインストール
$ sudo apt update
$ sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# ユーザーを docker グループに追加(sudoなしで使いたい場合)
$ sudo usermod -aG docker $USER
# このあと 一度ログアウトして再ログイン してください(グループ変更を反映させるため)もしくは次のコマンドでセッションを更新
$ newgrp docker
# バージョン確認
$ docker --version
Docker version 28.1.1, build 4eba377
Zsh

Docker Buildを行う。

Raspberry Pi 上で llama-server 用の Docker イメージをビルドするには、以下のように Dockerfile のあるディレクトリで docker build コマンドを実行します。

$ docker build -t llama-server:0.1.0 .
Zsh

📦 ビルド完了後の確認

$ docker images
REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
llama-server   0.1.0     3b1f7b166060   24 seconds ago   833MB
Zsh

準備が整えば、次は イメージを配布する方法です。

ローカルレジストリを作成する

--restart=always を付けた Docker コンテナは、Raspberry Pi の再起動後にも自動で起動します

以下のように docker run すると、イメージがホストの /opt/registry に保存されて永続化されます

# registryの永続化ボリュームを指定する場合
docker run -d -p 5000:5000 --restart=always --name registry \
  -v /opt/registry:/var/lib/registry \
  registry:2
Zsh
  • -p 5000:5000:ホストの 5000 番ポートをレジストリに割り当て
  • --restart=always:再起動時に自動起動
  • -v /opt/registry:/var/lib/registry:イメージの保存先を永続化
  • registry:2:Docker Registry のバージョン2を使用
sudo nano /etc/docker/daemon.json
# /etc/docker/daemon.json に以下を追加:なければファイル作成
{
  "insecure-registries": ["localhost:5000"]
}

# デーモン再起動
sudo systemctl restart docker
Zsh

これでlocalhost:5000からの接続についてはHTTPでも許可されるようになります。ただこれだと開発用途だけとなり不便なので、次の手順でHTTPS構成にします。

まずはAnsible用のラズパイにIPアドレスを固定します(今まではDHCPだったのですが、この先サーバ用として使うので固定します)

$ sudo nmcli con mod "Wired connection 1" \
  ipv4.addresses 192.168.0.161/24 \
  ipv4.gateway 192.168.0.1 \
  ipv4.dns "192.168.0.1 8.8.8.8" \
  ipv4.method manual

# 設定を反映(再起動または接続の再アクティベート):

$ sudo nmcli con down "Wired connection 1" && sudo nmcli con up "Wired connection 1"

$ sudo nano /etc/hosts
Zsh
127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

127.0.1.1       raspi-ctrl
192.168.0.161   raspi-ctrl

192.168.0.101   raspi-cp01
192.168.0.102   raspi-cp02
192.168.0.103   raspi-cp03

ホスト名も用途を表すように変更しておきましょう。

$ sudo hostnamectl set-hostname raspi-ctrl
Zsh

これで、ラズパイのIPアドレスとマシン名がようやく決まりました。

192.168.0.101   raspi-cp01  # K8s コントロールプレーン&ワーカー
192.168.0.102 raspi-cp02 # K8s コントロールプレーン&ワーカ
192.168.0.103 raspi-cp03 # K8s コントロールプレーン&ワーカー
192.168.0.161 raspi-ctrl # Ansible, dockerレジストリ

構成手順(HTTPS + 自己署名証明書)

OpenSSLの設定ファイルを作成

# raspi-ctrl.cnf
[req]
default_bits       = 4096
prompt             = no
default_md         = sha256
distinguished_name = dn
x509_extensions    = v3_req

[dn]
CN = raspi-ctrl.local

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = raspi-ctrl.local
Zsh

証明書と秘密鍵を生成

$ mkdir -p ~/certs && cd ~/certs 
$ openssl req -x509 -nodes -days 365 \
  -newkey rsa:4096 \
  -keyout raspi-ctrl.key \
  -out raspi-ctrl.crt \
  -config raspi-ctrl.cnf
  
# 証明書ファイルの読み取り権限を修正
$ chmod 644 ~/certs/domain.key
Zsh
  • domain.crt: 公開証明書
  • domain.key: 秘密鍵
  • /CN=raspi-ctrl.local ← Piのホスト名またはLAN内FQDNに置き換えてください

# 一旦元のレジストリは削除
$ docker stop registry
registry
$ docker rm registry
registry

# Registry の起動(HTTPS化)
docker run -d -p 443:5000 --restart=always --name registry \
  -v /opt/registry:/var/lib/registry \
  -v /etc/ssl/private/raspi-ctrl.crt:/certs/domain.crt \
  -v /etc/ssl/private/raspi-ctrl.key:/certs/domain.key \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  registry:2
Zsh

-p 443:5000 にしておけば通常のHTTPSとしてアクセス可能です!

/etc/containerd/config.toml を開き、
[plugins.”io.containerd.grpc.v1.cri”.registry.mirrors]
の次に2行追加する

[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."raspi-ctrl.local"]
    endpoint = ["https://raspi-ctrl.local"]
Zsh

再起動

sudo systemctl restart containerd
Zsh

レジストリにDockerイメージをpushします

# タグ付け
$ docker tag llama-server:0.1.0 raspi-ctrl.local/llama-server:0.1.0
# レジストリにPush
$ docker push raspi-ctrl.local/llama-server:0.1.0
The push refers to repository [raspi-ctrl.local/llama-server]
150643f5166c: Pushed 
ef16af6c922e: Pushed 
5f70bf18a086: Pushed 
401146b66e2c: Pushed 
60e6f28b09b9: Pushed 
fec0f5f70886: Pushed 
66a307619b8c: Pushed 
0.1.0: digest: sha256:4767519047ea7e9714cec1d055f60a8cb3ae2033d8ab3a606ec46155dee602ee size: 1786
emboss@raspi-ctrl:~/certs $ 
Zsh

PVC作成

Presistent Volume Claimを作成

PVC(永続的な保存領域の要求) とは、Kubernetesアプリケーションが「ストレージをこれだけ欲しい!」と要求するリクエストのこと。 

用語の対応関係:

用語意味
PV(Persistent Volume)管理者が用意する「実体のストレージ」
PVC(Persistent Volume Claim)開発者が「これだけ容量が欲しい」と出すリクエスト
Podアプリケーション本体(データベースなど)

まずはLLMの実行にllmというネームスペースを作っておき、すべてのリソースをllmに参加させておきます。

# llm-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: llm
Zsh

適用します。

$ kubectl apply -f llm-ns.yaml
# 確認
$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   30h
kube-flannel      Active   30h
kube-node-lease   Active   30h
kube-public       Active   30h
kube-system       Active   30h
llm               Active   36s
longhorn-system   Active   24h
Zsh

デフォルトで llm ネームスペースを使いたい場合は context を変更します。

$ kubectl config set-context --current --namespace=llm
Zsh

次に、PVCを作成します。

当初、LonghornでPVCを作りました。しかしいつまで経ってもボリュームがBoundにならず、Pendingのままでした。理由は、ReadOnlyManyを設定してしまっていたこと。Longhorn のストレージクラスは、通常 ReadWriteOnce (RWO) にしか対応していません。

Longhorn の仕様としては以下の通りです:

Access ModeLonghorn対応備考
ReadWriteOnce単一ノードから読み書き可能
ReadOnlyMany未サポート
ReadWriteMany✅(制限あり)RWXはCSI-NFS等の別構成が必要

📘 AccessModes とは?

PVC(PersistentVolumeClaim)やPV(PersistentVolume)で指定し、ボリュームへのアクセス制御を行います。

主に以下の3つのモードがあります:

モード名説明複数Podからの利用ノード制約
ReadWriteOnce (RWO)1つのPodから読み書き可能❌ 複数Pod不可(同時)同一ノード
ReadOnlyMany (ROX)複数Podから読み込み専用で使用可✅ 読み取り専用で複数Pod OK複数ノードOK
ReadWriteMany (RWX)複数Podから読み書き可能✅ フル共有可能複数ノードOK

🔍 各モードの詳細

1. ReadWriteOnce (RWO)

  • 単一Podのみがボリュームをマウント可能(読み書き)
  • 最も一般的
  • Longhornのデフォルトモード

よくある用途:

  • DB(MySQL/Postgresなど)
  • 単一プロセスが排他制御する必要のあるアプリ

2. ReadOnlyMany (ROX)

  • 複数Podから同時に読み取りのみ可能
  • 読み取り専用でマウント
  • Longhornや他のブロック型ストレージではサポートされないことが多い

よくある用途:

  • モデルファイルの共有(LLM、TensorFlowモデルなど)
  • 静的データ配信(設定ファイルや辞書)

3. ReadWriteMany (RWX)

  • 複数のPodから読み書き可能
  • NFSやCSI driverが必要
  • Longhornは一部構成で対応可能(CSI-NFSなど)

よくある用途:

  • 同期不要なログ書き出し
  • 共有キャッシュ領域
  • データ変換バッチジョブ間のファイル共有

さて、というわけで「複数のPODでLLMモデルを共有するのにLonghornは使えない」ということが理解できました。LonghornはKubernetesのPodが再起動しても、データを失わずに保つためのストレージを、クラスタ上で簡単に構築・管理できるソリューションです。POD毎の領域を安全に管理できるわけです。複数PODでのデータ共有はできないので、POD間でのデータ共有にはNFSが必要ということですね。

NFSで永続化データ領域を確保する

まずは、NFS上にLLMを配置する共有領域を作成しましょう。

# phi3-model-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: phi3-model-pvc
  namespace: llm
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 3Gi
  storageClassName: nfs-client
Zsh

$ kubectl apply -f phi3-model-pvc.yaml 
persistentvolumeclaim/phi3-model-pvc created

# 確認
$ kubectl get PersistentVolumeClaim
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
phi3-model-pvc   Bound    pvc-04e910a0-b856-4226-9741-1842c50805a2   3Gi        RWX            nfs-client     <unset>                 6s
emboss@raspi-ctrl:~/dev/edgeaiagent/ai/llama-server/k8s $ 
Zsh

NFSサーバで見てみると、フォルダが作られていることが確認できます。

作成されたフォルダの中に、LLMを配置しておきましょう。

$ cp ~/models/phi3/Phi-3-mini-4k-instruct-q4.gguf /nfs-share/llm-phi3-model-pvc-pvc-04e910a0-b856-4226-9741-1842c50805a2/

$ ls -l /nfs-share/llm-phi3-model-pvc-pvc-04e910a0-b856-4226-9741-1842c50805a2/
total 2337144
-rw-r--r-- 1 emboss emboss 2393231072 May  2 14:00 Phi-3-mini-4k-instruct-q4.gguf
Zsh

デプロイマニュフェストは以下のようになります。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: llama-server
  namespace: llm
spec:
  replicas: 1
  selector:
    matchLabels:
      app: llama-server
  template:
    metadata:
      labels:
        app: llama-server
    spec:
      containers:
        - name: llama-server
          image: raspi-ctrl.local/llama-server:0.1.0
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: model-volume
              mountPath: /models
      volumes:
        - name: model-volume
          persistentVolumeClaim:
            claimName: phi3-model-pvc
YAML

次のService の定義は、Kubernetes クラスタ内の llama-server アプリケーション(Pod)に外部からアクセスできるようにするための設定です。

apiVersion: v1
kind: Service
metadata:
  name: llama-service
  namespace: llm
spec:
  type: NodePort
  selector:
    app: llama-server
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 30080
YAML

Raspberry Pi クラスタのどのノードのIPでも、http://<IP>:30080llama-server にアクセスできます(iptablesルールなどにより転送される)。

これで、Phi-3 MiniをRaspberry Pi 5上のKubernetesのPODで実行することができました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

上部へスクロール