TrueNAS SCALE(LinuxベースのアプライアンスOS)上で、AMD Ryzen APU内蔵のXDNA2 NPU(Strix / Strix Halo等)を認識させ、Dockerコンテナ上の FastFlowLM (FLM) / Lemonade Server からNPU推論を実行させるまでの構築記録とベンチマーク結果です。
6.12系カーネルにおける SVA (Shared Virtual Addressing) API不適合によるフリーズエラー(ret -95)を「IOVA強制モード(force_iova=1)」で打破するノウハウ や、実際にGemmaモデルを走らせて 8kコンテキスト時に1,000 tok/s超のPrefill(プロンプト処理)速度 を確認した結果などをまとめています。
なお、記事にまとめる中で対応した事象と記述順序が前後している箇所があります。ご了承くださいませ
1. 動作環境・システム構成
| 項目 | 詳細・バージョン情報 |
|---|---|
| ホストOS | TrueNAS SCALE (Kernel: 6.12.91-production+truenas) |
| CPU / APU | AMD Ryzen AI 5 340 ES (Strix) |
| NPU | AMD Strix Neural Processing Unit (PCI ID: 1022:17f0, rev 20) |
| ストレージ構成 | OCuLink ➔ LSI 9211-8i HBA ➔ 5 x 8TB HDDs (RAIDZ1) |
| 推論基盤 | FastFlowLM (FLM) / Lemonade Server (Dockerコンテナ) |
| 目標成果物 | amdxdna.ko モジュールロード ➔ /dev/accel/accel0 生成 ➔ FLMでNPU推論 |
2. カーネルモジュール(amdxdna.ko)のビルド方針
アプライアンスOSであるTrueNAS SCALEのローカル環境上で直接ビルド環境(DKMSや開発ツールチェーン)を整えるのはシステム保護の観点から推奨されません。
そのため、別体のDebian VM環境にて外付けビルドを行う手法を採りました。
- カーネルヘッダーの抽出: TrueNAS上の稼働カーネル(6.12.91-production+truenas)からヘッダー情報を抽出し、VM側へ移動し、下記xdna-driverをビルドする際のカーネルヘッダとします。(※カーネルバージョンのアップグレード毎に再構築が必要となる、非常に高度かつ自己責任を伴う工程です)
- ドライバソースコード: ビルドに利用するドライバソースコードはAMD公式のGitHub https://github.com/amd/xdna-driver から取得しました
- カーネルヘッダ・ビルドオプションの細かな適合: GCC 14でビルドを行うとxdna-driverのコードや動作しているTrueNASから抽出したヘッダ情報にて、GCC由来の厳密な型チェックやヘッダバージョンが推奨より低いことから発生するシンボル名エラー等が多発します。それらを乗り越えるべく、CFLAG補正やシンボル名や引数の数を合わせるためのCマクロをヘッダに定義するなどを繰り返してコンパイルを通し、amdxdna.ko を生成します。
3. 構築およびロード手順
ステップ0: GRUBでのAMD IOMMU有効化(事前準備)
TrueNASのGRUBカーネルオプションでは intelのIOMMUは有効になっていますが、AMDのIOMMUは有効化されていません。幸いにしてカーネルドライバそのものは存在するため、GRUBのオプションを書き換えるだけで AMD IOMMUを有効にできます。NPUがPCIeデバイスとして正しくメモリ空間を扱えるように、AMD IOMMUが有効でなければ以下を追記してTrueNASホストを再起動しておきます。
# /etc/default/grub.d/truenas.cfg の GRUB_CMDLINE_LINUX_DEFAULT 行へ追記
# 例: GRUB_CMDLINE_LINUX_DEFAULT="... amd_iommu=on iommu=pt"
# GRUB設定の反映と再起動
sudo update-grub
sudo reboot
ステップ1: ZFSルートの保護解除と成果物配置
TrueNASはアプライアンスOSのため、ルートデータセットはZFSレベルでRead-Only保護されています。
そのため、カーネルオブジェクト等配置のため一時的にRead-Only保護を解除する必要があります。
WSL2等からTrueNASの /tmp/npu_install/ へビルド済みモジュールおよびファームウェアを一括転送し、ZFSレベルのRead-Only保護を一時解除してファイルを配置します。
# TrueNAS側:ZFSルートデータセットのロック一時解除とRW再マウント
sudo zfs list -H -o name | grep '/ROOT/' | xargs -I {} sudo zfs set readonly=off {}
sudo mount -o remount,rw /
# ファームウェアとモジュールの配置
sudo mkdir -p /lib/firmware/amdnpu
sudo cp -r /tmp/npu_install/amdnpu/* /lib/firmware/amdnpu/
sudo mkdir -p /lib/modules/$(uname -r)/kernel/drivers/accel/amdxdna
sudo cp /tmp/npu_install/amdxdna.ko /lib/modules/$(uname -r)/kernel/drivers/accel/amdxdna/
sudo depmod -a
# FastFlowLM要求バージョン(1.1.2.64等)へシンボリックリンク更新
cd /lib/firmware/amdnpu/17f0_10
sudo ln -sf npu.sbin.1.1.2.64 npu.sbin
ステップ2: 【最重要】SVA無効化とIOVA強制モードでのドライバロード
TrueNAS SCALEの6.12カーネルでは、最新NPUドライバが要求するSVA (Shared Virtual Addressing) の処理で *ERROR* amdxdna_sva_init: SVA bind device failed, ret -95 となりNPUがフリーズする問題が発生します。
※ この件は /dev ノードが生成された時点では判別できず、実際にFLMで推論を走らせた際の dmesg で初めてSVAバインド失敗が発覚しました……
これを回避するため、モジュールロード時に force_iova=1 パラメータを付与して標準IOVAモードで動作させます。
# 古いモジュールをアンロード
sudo rmmod amdxdna
# 魔法の引数 force_iova=1 を与えてロード
sudo modprobe amdxdna force_iova=1
# dmesgで初期化ログを確認
sudo dmesg | tail -n 20 | grep -iE "amdxdna|accel"
【期待される出力ログ例】
[drm] amdxdna_iommu_init: Enabled force_iova mode.
[drm] Load firmware amdnpu/17f0_10/npu.sbin
[drm] Initialized amdxdna_accel_driver 0.16.0 for 0000:c7:00.1 on minor 0
4. 動作確認(/dev/accel/accel0 導通テスト)
/dev/accel/accel0 が生成されていることを確認し、Pythonの記述でデバイスノードが正常にオープンできるかテストします。
# デバイスノードの確認
ls -al /dev/accel/accel0
# crw-rw---- 1 root render 261, 0 Aug 4 21:56 /dev/accel/accel0
# アクセステスト
sudo python3 -c \
"import os; fd = os.open('/dev/accel/accel0', os.O_RDWR); print('大勝利! fd:', fd)"
【期待される出力結果】
大勝利! fd: 3
5. FastFlowLM (FLM) 実行時のノウハウ(カーネルバージョンチェックの偽装)
/dev/accel/accel0 が生えた後、Dockerコンテナ上で FastFlowLM (FLM) や Lemonade Server を起動して NPU を使用する際、「TrueNASのカスタムカーネル文字列」 や 「未対応カーネルバージョン」 であるとしてランタイム側で弾かれる罠があります。
ハマりポイントと対策: uname -a の偽装 (uname-shim)
FLMのソースコード等では、以下のように uname システムコールを発行してカーネルバージョン(major > 6 または major == 6 && minor >= 17 等)の互換性チェックを行っています。
// FLMソースコードにおけるカーネルチェック処理の例
struct utsname u_name;
uname(&u_name);
int major, minor;
sscanf(u_name.release, "%d.%d", &major, &minor);
bool kernel_ok = (major > 6) || (major == 6 && minor >= 17);
TrueNASの 6.12.91-production+truenas のようなバージョンではチェックに抵触し Kernel version incompatible with this version of FLM で終了してしまいます。
そのため、コンテナ内で LD_PRELOAD を用いた uname 関数のフックライブラリ(libuname_shim.so)を挟み込み、システムコールに対して公式サポート風のカーネル文字列(例: 6.18.0 等)を応答させることで判定をバイパスさせました。
Dockerコンテナ起動コマンド例
docker run -d \
--name lemonade-npu-server \
--device=/dev/accel/accel0 \
--group-add render \
-v /opt/lemonade:/opt/lemonade \
-e LD_PRELOAD=/usr/local/lib/libuname_shim.so \
lemonade-npu-image
わたしが作成したDockerfile
# 1. 移管された最新の公式イメージをベースにする!
FROM ghcr.io/lemonade-sdk/lemonade-server:latest
# 2. パッケージをインストールするために、一時的に root 権限にチェンジ!
USER root
# 3. NPU 駆動に必須な libdrm2、libfftw3 に加え、偽装用の gcc を追加!
RUN apt-get update && apt-get install -y \
libdrm2 \
libfftw3-single3 \
gcc \
&& rm -rf /var/lib/apt/lists/*
# 4. FastFlowLM の展開 & 闇の魔術(カーネル偽装)の錬成!
ARG FLM_VERSION=0.9.46
RUN mkdir -p /opt/fastflowlm && \
cd /tmp && \
curl -LO https://github.com/ROCm/FastFlowLM/releases/download/v${FLM_VERSION}/fastflowlm_${FLM_VERSION}_linux.tar.gz && \
tar -xzf fastflowlm_${FLM_VERSION}_linux.tar.gz -C /opt/fastflowlm && \
rm fastflowlm_${FLM_VERSION}_linux.tar.gz && \
printf '#include <sys/utsname.h>\n#include <string.h>\nint uname(struct utsname *buf) {\n strcpy(buf->sysname, "Linux");\n strcpy(buf->nodename, "truenas");\n strcpy(buf->release, "6.17.0-fake");\n strcpy(buf->version, "#1 SMP");\n strcpy(buf->machine, "x86_64");\n return 0;\n}\n' > /tmp/fake_uname.c && \
gcc -shared -fPIC -o /opt/fastflowlm/fake_uname.so /tmp/fake_uname.c && \
rm /tmp/fake_uname.c && \
chmod 755 /opt/fastflowlm/fake_uname.so && \
ln -s /opt/fastflowlm/flm /usr/local/bin/flm && \
chown -R lemonade:lemonade /opt/fastflowlm
# 5. 偽装ライブラリをシステム全体で強制ロード!
ENV LD_PRELOAD="/opt/fastflowlm/fake_uname.so"
ENV PATH="/opt/fastflowlm:${PATH}"
ENV LD_LIBRARY_PATH="/opt/fastflowlm:/opt/fastflowlm/lib:/opt/fastflowlm/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
6. Dockerコンテナ(FastFlowLM)でのNPU推論ベンチマーク
無事にランタイムのチェックを突破後、flm bench で性能測定を行いました。
① Gemma 4-E2B (gemma4-it:e2b)
docker exec -u root -it lemonade-npu-server flm bench gemma4-it:e2b
| Context Length | TTFT (s) | Prefill Speed (tok/s) | Decoding Speed (tok/s) |
|---|---|---|---|
| 1k | 1.592 | 614.96 | 21.77 |
| 2k | 2.362 | 824.99 | 20.98 |
| 4k | 4.084 | 951.70 | 19.42 |
| 8k | 7.677 | 1011.13 | 17.17 |
| 16k | 16.537 | 938.03 | 13.82 |
| 32k | 41.596 | 745.49 | 9.96 |
8kコンテキスト時のPrefill(プロンプト処理)速度は 1,000 tok/s 超え を達成。
② Gemma 4-E4B (gemma4-it:e4b)
docker exec -u root -it lemonade-npu-server flm bench gemma4-it:e4b
| Context Length | TTFT (s) | Prefill Speed (tok/s) | Decoding Speed (tok/s) |
|---|---|---|---|
| 1k | 2.466 | 396.95 | 12.17 |
| 2k | 3.873 | 503.01 | 11.86 |
| 4k | 6.822 | 569.53 | 11.30 |
| 8k | 12.702 | 610.92 | 10.31 |
| 16k | 26.124 | 593.60 | 8.78 |
| 32k | 61.068 | 507.69 | 6.78 |
7. 運用上の注意点(TrueNAS SCALEアップデート時のリスク)
TrueNAS SCALEはアプライアンスOSであるため、システムアップデート(OSメジャー/マイナーバージョンアップ)を実行すると、以下のリスクが生じます。
- 手動配備ファイルの消滅: /lib/modules/ や /lib/firmware/ 配下に配置した amdxdna.ko やファームウェアがアップデート処理で上書き・消去される可能性があります。
- カーネルシンボル不一致: アップデート後の新カーネル(例: 6.18系等)では、旧環境でビルドした amdxdna.ko がシンボル不一致によりロードできなくなります。
【対策】
- OSアップデート前にビルド用のDebian VM環境(およびビルド手順)を維持しておくこと。
- アップデート後の新カーネルヘッダーを抽出し、ヘッダー参照の補正を行った上で amdxdna.ko を再ビルド・再配備する運用計画を立てておくことが推奨されます。
8. まとめ
TrueNAS SCALE というアプライアンスストレージOS環境において、以下のステップを踏むことで完全にNPUを実稼働させることができました。
- カーネルモジュールの外付けビルド: ヘッダー整合とコンパイラオプションの調整
- カーネル制約の回避: force_iova=1 による SVA 依存エラーの回避
- ランタイムチェックの回避: LD_PRELOAD(uname-shim)によるカーネルバージョン判定突破
- コンテナパススルー: /dev/accel/accel0 のマウントと FastFlowLM による高速推論
NASとしての本業(ZFSアレイの運用)に影響を与えることなく、空きリソースのNPUを活用して1,000 tok/s規模の超高速Prefill環境を同居させることが可能となりました。
この記事、当然ながら公式での方法ではありませんし、試行錯誤の上に成り立っているものとなります。また、TrueNASのバージョンが上がれば .ko ファイルは作り直しになりますし、次バージョンで xdnaモジュールを有効化してくるかもしれません。 At your own risk でお願いしますねっ。
