はじめに
本記事のFastFlowLMコンテンな作成により、以前構築したollama-rocmのコンテナがcpuにフォールバックするようになってしまいました。ollama公式コンテナ(rocmなし)からVulkanサポートで使ってください。
色々試行錯誤した結果をまとめたものなので、不備があるかもしれません。ご了承ください。
カーネルバージョンアップの都合でその他のコンテナに影響があるかもしれません。必要に応じて再構築などをしてください。
Ryzen AI Max+ 395には50TOPSのNPUが搭載されていますが、なかなか自由に使えない状態でした。最近Ubuntu 24.04上でFastFlowLMを使ってNPUを動かす記事が出てきましたので、FastFlowLMのコンテナを作ってみることにしました。
この記事ではlemonade-serverは扱いません。
もう一つ、OSディストリビューションが違いますが、重要な情報を提供していただいた下記の記事に感謝します。
FastFlowLMについてはこちらをご覧ください。
NPUへアクセスするkernel driverはproprietaryとのことでライセンスにはご注意ください。年間1000万ドル(15億円)以上の売上ある組織が利用する場合はライセンス契約が必要です。
前提の環境
以下の環境で構築しました。それ以外の環境ではテストしていませんので、ご注意ください。
- GMKTek EVO-X2 / 128GB (AMD Ryzen AI Max+ 395 / 128GB)
- Ubuntu Server 24.04LTS (headless)
- Docker CE 29.3.1
ホスト側の準備
素のUbuntu 24.04では色々足りないため、いくつかのパッケージのインストールと設定をします。
カーネルバージョンアップとブートオプションの調整
linuxカーネル6.17とそのヘッダファイルをインストールして再起動します。
sudo apt update
sudo apt install --install-recommends linux-image-generic-6.17 linux-headers-generic-6.17
sudo reboot
その後、ブートオプションの調整をします。amd_iommuはonでないとNPUを認識しないため、調整します。エディタは使い勝手の良いものを選んでください。
sudo vi /etc/default/grub
/etc/default/grubの中のGRUB_CMDLINE_LINUX_DEFAULTの行を以下のようにエディタで修正し保存します。
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash amd_iommu=on iommu=pt"
保存できたら、更新して再起動します。
sudo update-grub
sudo reboot
NPU関連パッケージのインストールと設定
NPUに関連するパッケージをインストールします。
sudo add-apt-repository ppa:amd-team/xrt
sudo apt update
sudo apt install -y libxrt2 libxrt-npu2 libxrt-dev libxrt-utils libxrt-utils-npu amdxdna-dkms
グループrenderに自分を登録して、再起動します。
sudo usermod -aG render "$USER"
sudo reboot
/dev/accel/accel0ができていればNPUが利用可能な状態になっています。以下の作業で適切なバージョンとなっていることを確認します。
uname -r
ls -l /dev/accel/accel0
modinfo amdxdna | grep -E '^filename:'
実行結果が以下のとおりなら良好です。カーネルのバージョンが上がっていて、accel0が見え、そのカーネルバージョンのdkms以下にamdxdna.ko.zstがあれば良いです。
$ uname -r
6.17.0-20-generic
$ ls -l /dev/accel/accel0
crw-rw---- 1 root render 261, 0 Apr 5 00:16 /dev/accel/accel0
$ modinfo amdxdna | grep -E '^(filename|version):'
filename: /lib/modules/6.17.0-20-generic/updates/dkms/amdxdna.ko.zst
コンテナの作成
ディレクトリ構成の調整
コンテナを置く場所を ~/Containers/fastflowlmとします。ここは皆さんの都合の良いように変更してください。このディレクトリ下にデータ等を永続的に保存するディレクトリを作成します。
mkdir -p ~/Containers/fastflowlm
cd ~/Containers/fastflowlm
mkdir -p data models
コンテナ作成用ファイルの作成
以下のDockerfileをカレントディレクトリに置きます。
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
SHELL ["/bin/bash", "-lc"]
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl wget gnupg software-properties-common \
lsb-release git build-essential cmake ninja-build pkg-config \
python3 python3-pip pciutils usbutils libglib2.0-0 libstdc++6 \
libjson-glib-dev libcurl4-openssl-dev uuid-dev rapidjson-dev \
pybind11-dev ocl-icd-opencl-dev opencl-headers \
libboost-all-dev libfftw3-dev libavformat-dev libavcodec-dev \
libavutil-dev libswscale-dev libswresample-dev libdrm-dev \
&& rm -rf /var/lib/apt/lists/*
RUN add-apt-repository -y ppa:amd-team/xrt \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
libxrt2 libxrt-npu2 libxrt-dev \
&& rm -rf /var/lib/apt/lists/*
ENV RUSTUP_HOME=/opt/rustup
ENV CARGO_HOME=/opt/cargo
ENV PATH=/opt/cargo/bin:${PATH}
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --profile minimal --default-toolchain stable \
&& rustc --version \
&& cargo --version
WORKDIR /opt
RUN git clone --recursive https://github.com/FastFlowLM/FastFlowLM.git \
&& cd /opt/FastFlowLM \
&& git submodule update --init --recursive \
&& cmake -S src --preset linux-default \
&& ninja -C src/build -j1 -v \
&& cmake --install src/build
ENV LD_LIBRARY_PATH=/opt/fastflowlm/lib:/opt/xilinx/xrt/lib
ENV FLM_CONFIG_PATH=/opt/fastflowlm/share/flm/model_list.json
ENV FLM_MODEL_PATH=/models
ENV HF_HOME=/data/huggingface
ENV HUGGINGFACE_HUB_CACHE=/data/huggingface/hub
ENV XDG_CACHE_HOME=/data/.cache
RUN mkdir -p /workspace /data/huggingface /data/.cache /models
WORKDIR /workspace
EXPOSE 52625
CMD bash -lc '\
echo "=== kernel ==="; uname -r; \
echo "=== accel ==="; ls -l /dev/accel || true; \
echo "=== flm validate ==="; /opt/fastflowlm/bin/flm validate || true; \
echo "=== starting flm serve ==="; \
exec /opt/fastflowlm/bin/flm serve --host 0.0.0.0 --port 52625 \
'
次にdocker-compose.ymlをカレントディレクトリに置きます。
services:
fastflowlm:
build:
context: .
dockerfile: Dockerfile
container_name: fastflowlm
restart: unless-stopped
ports:
- "52625:52625"
environment:
LD_LIBRARY_PATH: "/opt/fastflowlm/lib:/opt/xilinx/xrt/lib"
FLM_CONFIG_PATH: "/opt/fastflowlm/share/flm/model_list.json"
FLM_MODEL_PATH: "/models"
HF_HOME: "/data/huggingface"
HUGGINGFACE_HUB_CACHE: "/data/huggingface/hub"
XDG_CACHE_HOME: "/data/.cache"
volumes:
- ./data:/data
- ./models:/models
devices:
- /dev/accel:/dev/accel
ulimits:
memlock:
soft: -1
hard: -1
cap_add:
- IPC_LOCK
- SYS_NICE
security_opt:
- seccomp:unconfined
コンテナ構築
ビルドして起動します。
docker compose build --no-cache
docker compose up -d
docker compose logs -f
最後のログ確認で以下のように表示されればOKです。
$ docker compose logs -f
fastflowlm | === kernel ===
fastflowlm | 6.17.0-20-generic
fastflowlm | === accel ===
fastflowlm | total 0
fastflowlm | crw-rw---- 1 root 993 261, 0 Apr 5 05:26 accel0
fastflowlm | === flm validate ===
fastflowlm | [FLM] Using custom model list path: /opt/fastflowlm/share/flm/model_list.json
fastflowlm | [Linux] Kernel: 6.17.0-20-generic
fastflowlm | [Linux] NPU: /dev/accel/accel0 with 8 columns
fastflowlm | [Linux] NPU FW Version: 1.1.2.65
fastflowlm | [Linux] amdxdna version: 0.6
fastflowlm | [Linux] Memlock Limit: infinity
fastflowlm | === starting flm serve ===
fastflowlm | [FLM] Using custom model list path: /opt/fastflowlm/share/flm/model_list.json
fastflowlm | [FLM] Using user-specified port: 52625
fastflowlm | [FLM] Starting server on port 52625...
fastflowlm | [FLM] WebServer started on port 52625 with 10 I/O threads
fastflowlm | [FLM] Press Ctrl+C to stop.
動作確認
ホスト側からcurlでいくつか試してみましょう。
最初はモデルリストから。初期状態で32個のモデルが利用可能で、deepseek-r1:8b, gemma3:4b, gpt-oss:20b, qwen3.5:9bなどが含まれています。これらのモデルは切り替えながら利用可能という点でollamaと似ていますが、初めて利用する時にモデルのダウンロードを始めるので、その時の最初の応答が得られるまでにはそれなりに時間がかかります。
curl -s http://127.0.0.1:52625/v1/models | jq
結果は以下の通り。
{
"data": [
{
"created": 1775367291,
"id": "deepseek-r1:8b",
"object": "model",
"owned_by": "FastFlowLM"
},
(snip)
{
"created": 1775367291,
"id": "gemma3:4b",
"object": "model",
"owned_by": "FastFlowLM"
},
{
"created": 1775367291,
"id": "gpt-oss:20b",
"object": "model",
"owned_by": "FastFlowLM"
},
(snip)
{
"created": 1775367291,
"id": "qwen3.5:9b",
"object": "model",
"owned_by": "FastFlowLM"
},
{
"created": 1775367291,
"id": "qwen3vl-it:4b",
"object": "model",
"owned_by": "FastFlowLM"
},
{
"created": 1775367291,
"id": "translategemma:4b",
"object": "model",
"owned_by": "FastFlowLM"
}
],
"object": "list"
}
もう一つ。Open WebUIが投げそうなパラメータで試します。
$ curl -s http://localhost:52625/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-oss:20b",
"messages": [
{"role": "user", "content": "1+1は?"}
],
"temperature": 0.7,
"top_p": 0.9,
"max_tokens": 64,
"stream": false
}' | jq .
結果は以下の通り。
{
"id": "fastflowlm-chat-completion",
"object": "chat.completion",
"created": 1775368079,
"model": "gpt-oss:20b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"reasoning_content": "User asked \"1+1は?\". That is Japanese: \"What is 1+1?\" So answer: 2. But maybe they want explanation? The user didn't specify context. Just answer.\n\nWe should respond in Japanese or English? They wrote question in Japanese, so likely expect answer in Japanese",
"content": ""
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 72,
"completion_tokens": 64,
"total_tokens": 136,
"load_duration": 252.474884096,
"prefill_duration_ttft": 3.821402624,
"decoding_duration": 3.244395,
"prefill_speed_tps": 18.84124942705854,
"decoding_speed_tps": 19.726328021094844
},
"service_tier": "default"
}
Open WebUIからも適切に設定すれば以下のように使えます。
ベンチマーク
fastflowlmのコンテナとollama(vulkan)コンテナのそれぞれで同じプロンプトをcurlから送信したときの結果は以下のとおりです。
| モデル/計算ユニット | prefill (token/s) | decoding (token/s) |
|---|---|---|
| gpt-oss:20b/NPU | 19.93 | 19.53 |
| gpt-oss:20b/iGPU | 862.32 | 45.47 |
| qwen3.5:9b/NPU | 16.59 | 7.83 |
| qwen3.5:9b/iGPU | 208.83 | 31.43 |
