目的:
本稿では、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モデル)
項目 | 値 |
---|---|
CPU | 4-core Cortex-A76 @2.4GHz |
RAM | 最大16GB LPDDR4 |
GPU | VideoCore VII |
OS | Raspberry 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動きましたか?!実行成功おめでとうございます!しかし…この回答、内容的には完全に間違っていますよね。
<|user|> 日本で2番目に高い山は?<|assistant|> 日本で2番目に高い山は富士山の竜嶺(ふじさんのたんげり)です。これは、日本国内で最高傾斜があり、3,776メートルの高さです。次に高い山としては、富士山自体が1,238.6メートルで高いことが知られていますが、富士山の竜嶺は山の高
❌ モデルの誤答内容の分析
✖ 「富士山の竜嶺(ふじさんのたんげり)」
- そんな名前の地形は存在しませんw。モデルが架空の用語を生成しています。
✖ 「富士山自体が1,238.6メートル」
- 富士山は日本で最も高い山であり、標高は 3,776m。
- モデルが「次に高い山」と「最高の傾斜」を混同して誤情報を生成しています。
✅ ちなみに正しい答えは?
- 日本で2番目に高い山は:北岳(きただけ)
- 山梨県南アルプス市、標高 3,193m
このような 小型LLM(3.8B) は、まだ「事実知識」の保持が不完全です。
推論において「もっともらしいが間違った文」を作りがちです(ハルシネーションと呼ばれる現象です)。
特に地理・数字・固有名詞は苦手な傾向があります。
日本語入力に英語学習済モデルが部分的対応している状態なので、言語的な混乱も加わっています。
ですが、日本語の処理能力はまずまずなので、RAGで知識を与えた上で、答えてもらえればもしかしたら使えるかも?しれませんね。
🔹 1. サーバー起動
llama-server
は llama.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で制御(必要に応じて) |
作業手順
- Dockerfileを作る。
- Dockerビルドを行いイメージを用意する。
- イメージを直接各ノードに配布
- PVCを作成し、モデルを配置する
- DeploymentマニュフェストでKubernetesに配置を行う。
- 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
ZshDocker 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
Zsh127.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
Zshdomain.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 $
ZshPVC作成
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 Mode | Longhorn対応 | 備考 |
---|---|---|
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 $
ZshNFSサーバで見てみると、フォルダが作られていることが確認できます。

作成されたフォルダの中に、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
YAMLRaspberry Pi クラスタのどのノードのIPでも、http://<IP>:30080
で llama-server
にアクセスできます(iptablesルールなどにより転送される)。
これで、Phi-3 MiniをRaspberry Pi 5上のKubernetesのPODで実行することができました。