第8回「EVO-X2(Ryzen AI Max+ 395 / 128GB)+ Qwen3.8-Flash-Nextをいじっていたら、禁断のllama.cpp改造に手を出していた話」の続き。
前回は休暇の数日前にバタバタとllama.cpp改造に着手してしまい、そのまま約2週間の休暇に入って完全放置。
戻ってきたら本家llama.cppもHalogenも色々新しくなっているし、Strataというなんかすごそうな推論エンジンも出ている。Qwen3.8-Flash-Nextを高速に動かしたいだけなら、自前で何とかするよりllama.cppや他の推論エンジンの更新を待った方が良いような気がするけど、llama.cppの改造自体が楽しくなっているところなので、もう少し遊んでみることにした。
休暇前の改造は、LaurentZuijdwijkのStrix Halo専用forkをベースにしていたけど、本家(upstream)のllama.cppがどんどん新しくなるので、ベースが古いままだと最新の更新を取り込み続けるのも大変そう、ということで、全体の構成から見直すことにした。
以降、Prompt Processing(入力を読み込む処理)をPP、Token Generation(出力生成)をTGと表記する。どちらも単位はtok/sで、高いほど速い。
結論
本家(upstream)のllama.cppをベースにして、改造項目ごとに小さいパッチを作成して追加していく方式に変更した。upstreamに大きな更新が入ったときは、ベースを差し替えて必要なパッチだけ再適用できるようにする。
今回のベースは、
- upstream:
ggml-org/llama.cpp - build:
b11247 - commit:
0bc845d356f437d5ce4fe975c36428f7522829cb
今までVulkan版のビルドしかしたことがなかったけど、比較のためにROCm版も動かしたいので、Windows上でROCm 10.0 / TheRockを使った自前ビルドも行った。
そして、ベースのupstream llama.cppに、第8回の一番最初にやった「PLEテーブルを16分割してGPUに載せる」まで移植した状態で、Vulkan / ROCmを64k、128k、256kで比較するとこうなった。
| Backend | Context | PP | TG |
|---|---|---|---|
| Vulkan | 64k | 267.90 | 17.20 |
| ROCm | 64k | 357.93 | 14.86 |
| Vulkan | 128k | 159.56 | 12.00 |
| ROCm | 128k | 259.41 | 9.91 |
| Vulkan | 256k | 103.75 | 7.30 |
| ROCm | 256k | 167.35 | 5.74 |
第6回のunsloth版llama.cpp(b10798-mix-659e406)の時点では、私の環境ではPP、TGともにVulkan版の方が速かった。
今回は逆に、PPはROCm、TGはVulkanが速い。
64kではROCmのPPがVulkanより約33.6%高く、TGはVulkanが約15.7%高い。128k、256kになると差はさらに大きい。
ROCmビルドでハマったり、計測用スクリプトを整備したりしていたら意外と時間がかかったので、「PLEテーブルを16分割してGPUに載せる」という部分の移植まで。
高速化にはまだ着手できていない。
ソース、Windowsビルド、検証スクリプトなどはここで公開している。
main ブランチはStrix Halo専用forkベースのr2安定版でいったん凍結し、今回からの作業は r3/upstream-first ブランチで行っている。
検証環境
速度に関係するところだけ先に。
| 項目 | 内容 |
|---|---|
| PC | GMKtec NucBox EVO-X2 |
| APU | AMD Ryzen AI MAX+ 395 / Radeon 8060S |
| RAM | 128GB UMA |
| UMA Frame Buffer | 96GB |
| OS | Windows 11 Pro 25H2 |
| モデル | Unsloth Qwen3.8-Flash-Next UD-IQ3_XXS |
| モデル配置 | PLE16変換版(COMMON-001以降) |
| KV cache | f16 |
| ubatch | 1024 |
| MTP | OFF |
| Vulkan compiler | LLVM / Clang 20.1.8 |
| ROCm | 10.0.0 / TheRock |
| ROCm compiler | AMD Clang 23.0.0 |
| GPU target | gfx1151 |
| 主な入力 | 61,789 / 126,253 / 255,181 tokens |
詳細は記事末尾にまとめる。
改造の方針を変更、だんだん沼の深みにハマっていく
当初の予定では、休暇明けには、休暇前の第8回の記事投稿後に試したMTPや他の高速化の中から、効果があったものだけをピックアップして統合するところから始める予定だった。
のだけど、チャッピー様にupstreamのllama.cpp、Halogen、その他の関連情報を調べてもらったら、休暇中に色々状況が変わっていた。
MTPや元々改造候補にしていた項目が既にupstreamに取り込まれていたり、Halogenがさらに速くなっていたり、Strataという新しいエンジンが出ていたり。
たぶんHalogenかStrataが、うちのEVO-X2 Windowsでそのまま動かせる状況だったら、llama.cppの改造は放り出して、そっちを動かしてみるのを先にやっていた気がするけれど、そうではなかったので、もうしばらくllama.cppの改造を続けてみることにした。
前回のLaurentZuijdwijkのStrix Halo専用forkベースの改造版に、さらに改造を加えていくと、upstream llama.cppに良さそうな修正が入るたびに個別に取り込みが必要になる。
しかも、upstream側で同じような処理が実装されたときに、古いforkへ入れた自前実装と二重管理になる可能性もある。
将来Qwen4など別のモデル世代へ移るときに最初からやり直しになるのも嫌なので、初回の手間はかかるけど、
upstream llama.cppそのものを「何も足していない基準」にして、必要な差分だけ小さいpatchとして積む
という方式に変更した。
今回のベースはこちら。
upstream: ggml-org/llama.cpp
build: b11247
commit: 0bc845d356f437d5ce4fe975c36428f7522829cb
title: vulkan : reuse descriptor sets when bindings are constant (#29280)
この時点では、
- r2のQSA unionなし
- PLE16 loaderなし
- Unsloth MTP互換なし
- MTP-QSA prototypeなし
という、意図的に何も足していない状態。
ここから必要な変更だけ COMMON-xxx / VULKAN-xxx / ROCM-xxx という管理IDをつけて追加していくことにした。
速度計測とビルドに関するスクリプトを整備
今までも速度計測用、VRAM使用量等のリソース計測用のPowerShellスクリプトを使っていたのだけど、1計測ごとにリソース計測と速度計測を別々のPowerShellウインドウで手動起動、出力ログファイル名も一部手入力、という状況だった。
なので、
ファイル名には64kと書いてあるのに実際の起動条件は128k
みたいな間違いも起きる。
そこで、r3では計測系を先に整理した。
現在のスクリプトは、
以下に置いてある。
大まかな構成は、
tools/evox2/
├─ build/
│ ├─ Build-Vulkan.ps1
│ └─ Build-ROCm.ps1
├─ benchmark/
│ ├─ Measure-LlamaCli.ps1
│ ├─ Measure-LlamaBench.ps1
│ ├─ Monitor-LlamaProcess.ps1
│ └─ Invoke-BenchmarkMatrix.ps1
├─ lib/
├─ model/
└─ experiments/
という感じ。
Measure-LlamaCli.ps1 では、実際に使う llama-cli.exe から、
- backend / device
- llama.cpp build番号、commit、compiler
- executable hash
- contextなどの実行条件
- PP / TG / token数
- MTP acceptance
- resource log
などを自動記録するようにした。
1回の計測ごとに、
conditions.json
result.json
summary.csv
stdout.log
stderr.log
resources.csv
のようなファイルをまとめて保存する。
さらに Invoke-BenchmarkMatrix.ps1 で寝ている間や留守中に複数コンテキスト長や修正パッチON/OFFパターンをまとめて回せるようにした。
ついでにビルドについてもPowerShellスクリプト化し、使用したsource commit、compiler、backendなどの情報をmanifestとして残すようにした。
ここまでやっておけば、後でAIエージェントで完全自動化したくなったときにも楽かもしれない。
VulkanとROCmの速度計測結果
まずはベースのupstream llama.cppに改造を加えない状態でビルドして、baselineの速度を計測することにした。速度計測はいつもの日本語長文要約タスクで行っている。
Vulkanはいつも通り
Vulkan版については第7回で初ビルドし、第8回の改造版も問題なくビルドできていたので、そのままPowerShellスクリプト化してサクッとビルドできた。
ソースはベースのllama.cppのまま、PLE16変換前のオリジナルのUnsloth UD-IQ3_XXSモデル、コンテキスト長64k / MTP OFFでは、
Vulkan
PP 265.10 tok/s
TG 16.75 tok/s
だった。
ROCmは初の自前ビルド
比較のため、ROCm版もビルドしてみようと思った。
第6回のunsloth版llama.cpp(b10798-mix-659e406)の時点ではPP、TGともにVulkan版の方が速かったのと、前回のベースにしたStrix Halo専用forkはVulkan専用だったので、自分でROCm版をビルドしたことはなかった。
いろいろハマった話は後述するとして、最終的にWindows上で、
ROCm 10.0.0 / TheRock
AMD Clang 23.0.0
GPU target: gfx1151
という構成で動作した。
Vulkan版と同じソース、モデル、コンテキスト長で、
ROCm
PP 359.84 tok/s
TG 14.50 tok/s
だった。
| Backend | PP | TG |
|---|---|---|
| Vulkan | 265.10 | 16.75 |
| ROCm | 359.84 | 14.50 |
同じWindows、同じEVO-X2、同じllama.cpp sourceでも、PPはROCmがかなり速い。一方、TGはVulkanの方が速い。
プレビルド版のb11243 ROCmバイナリで測ったときは、
PP 358.42 tok/s
TG 14.42 tok/s
だったので、自前b11247 ROCmの 359.84 / 14.50 はかなり近い。
build番号が違うので厳密な性能比較ではないけど、少なくとも「自前ビルドしたら何か変な構成になって極端な数字が出た」という感じではなさそう。
PLE16テーブル対応
まずは第8回の一番最初にやった「PLEテーブルを16分割してGPUに載せる」という部分を移植した。
これは前回も高速化そのものにはあまり関係なかったけど、前回の改造後の計測は基本的にUnsloth UD-IQ3_XXSモデルのPLE16変換版とAgentionAI版を使っている。
これから改造するもの(githubのr3ブランチ)を、前回(guthubのr2ブランチ)と比較しやすくするためにも、最初に必要だった。
なお、PLE16は16bit化ではない。
元の巨大なPLE n-gram tensorを16個のhead単位へ分割した形式で、モデルの変換は前回と同じくLaurentZuijdwijkのStrix Halo専用forkに入っていた、
gguf-py\gguf\scripts\gguf_split_ple_heads.py
を使用している。
今回は、このsplitされたtensorを読み込めるloaderを移植した。
この変更には、GitHubのドキュメント上で COMMON-001 という管理IDをつけた。
commitは、
902e9f4a762cc0fe89d613868c35e4861610081e
qwen4exp: support split PLE n-gram tensors
実装としては、
ple_ngram_embd.N.weight
というsplit tensorを検出し、16個のheadを読み込み、各headのlocal row indexへ変換してgatherした後、qwen4exp側のgraphへ戻す。
※AgentionAI版モデルを読み込むためにはROCmFP4対応という別の改造も必要になるため、今回の計測はUnsloth UD-IQ3_XXSモデルのPLE16変換版のみで行った。
PLE16テーブル対応実装後
| Context | Backend | PLE16実装前 PP | PLE16実装後 PP | PP差 | PLE16実装前 TG | PLE16実装後 TG | TG差 |
|---|---|---|---|---|---|---|---|
| 64k | Vulkan | 265.10 | 267.90 | +1.1% | 16.75 | 17.20 | +2.7% |
| 64k | ROCm | 359.84 | 357.93 | -0.5% | 14.50 | 14.86 | +2.5% |
| 128k | Vulkan | 未計測) | 159.56 | — | (未計測) | 12.00 | — |
| 128k | ROCm | (未計測) | 259.41 | — | (未計測) | 9.91 | — |
| 256k | Vulkan | (未計測) | 103.75 | — | (未計測) | 7.30 | — |
| 256k | ROCm | (未計測) | 167.35 | — | (未計測) | 5.74 | — |
PLE16を入れても、基本傾向は変わらなかった。TGの差は誤差範囲かどうか微妙なところだけど、今は深入りしないことにする。
- PPはROCmが速い
- TGはVulkanが速い
- TGはcontextが長くなるほどかなり落ちる
- 特にROCmのTG低下が大きい
前回計測値と比べる
ここで前回計測値と比べてみる。
前回(r2)はStrix Halo専用fork + upstream選択統合 + QSA unionまで入った状態。一方、今(r3)はまだその高速化を移植していなくて、COMMON-001(Ple16テーブル対応)のみ移植済み。
| Context | r2 Vulkan PP | r3 Vulkan PP | r3 ROCm PP |
|---|---|---|---|
| 64k | 274.67 | 267.90 | 357.93 |
| 128k | 222.77 | 159.56 | 259.41 |
| 256k | 177.01 | 103.75 | 167.35 |
r3 VulkanはQSA unionをまだ移植していないので、長文になるほどr2との差が大きい。
ROCmは何も高速化を入れていないのに64k、128kではr2 Vulkanを上回った。256kでもr2の177.01 tok/sに対して167.35 tok/sまで来ている。
PPだけ見ると、
もう自前で改造しなくてもROCmでかなり速いのでは?
という気もするけど、改造を加えたらさらに速くなるかもしれない。
TGの方は前回(r2)と今回(r3)を比較するとこうなった。
| Context | r2 Vulkan TG | r3 Vulkan TG | r3 ROCm TG |
|---|---|---|---|
| 64k | 23.55 | 17.20 | 14.86 |
| 128k | 21.65※ | 12.00 | 9.91 |
| 256k | 18.84 | 7.30 | 5.74 |
※128k TGは第8回の記事本文には掲載していない。手元に残っていた計測値2回分(21.72 / 21.57 tok/s)から平均した値。
TGが妙に遅くなっている。
前回行ったQSA unionは主にPP向けで、TGはほぼ変わっていなかったので、QSA union移植前だからということではなさそう。
元の専用fork側にTG向けの工夫があるのか、upstream側の実装変化で長文decode時に余計な処理が増えたのか。
これは次回詳しく調べることにした。
ROCmビルド手順の詳細
今回、意外と時間がかかったのがWindows版ROCmの自前ビルド。
最終的に動いた構成は、
Windows 11
ROCm 10.0.0 / TheRock
AMD Clang 23.0.0
Visual Studio 2022 Build Tools
MSVC 14.44.35207
CMake 4.3.1
Ninja 1.13.2
GPU target: gfx1151
だった。
最初は古いHIP環境で失敗
最初に、元の環境を全く変えずにビルドを試したら、configure時に
clang++: error: cannot find ROCm device library
で停止。これは、HIP SDK 7.2を使っているためだった。
プレビルド版はROCm 10.0と書いてあるし、7.2を修正して粘ってハマるのも嫌だったので、ROCm 10.0 / TheRockへ移行した。
ROCm 10.0はインストーラをダウンロードしてポチっと実行というわけにはいかなくて、こちらの手順に従って、pythonのvenvで仮想環境を作ってpip installする必要があった。(詳細は「TheRock環境」の項で説明する)
ROCm 10.0でも最初はMSVCで失敗
ROCm 10.0に変えればすぐ通るかと思ったら、今度はVisual Studio 2026 / MSVC 14.51環境で最初のHIPソース acc.cu をコンパイルしたところ、
isgreater
isless
isunordered
など、HIP側の数学関数とMSVCの <cmath> が衝突して停止した。
対策はVisual Studio 2026 Build Toolとは別に、新しくVisual Studio 2022 Build Toolsを追加して、MSVC 14.44を使うことだった。
Visual Studio 2022は旧バージョンなので、ダウンロードするにはここからマイクロソフトアカウントでログインする必要があった。
TheRock環境
ROCm 10.0 / TheRockのインストールコマンドは、最終的に
python -m venv C:\TheRock\build\.venv
& C:\TheRock\build\.venv\Scripts\Activate.ps1
python -m pip install `
--index-url https://stable.repo.amd.com/rocm/whl-next/ `
"rocm[libraries,devel,device-gfx1151]==10.0.0"
rocm-sdk init
rocm-sdk test
という構成にした。
device-gfx1151 が重要。
最初は、
rocm[libraries,devel]
だけを入れていて、llama.cpp自体のbuildは成功した。
ところが、llama-cliで推論しようとすると、
rocBLAS error: Cannot read .../rocblas/library/TensileLibrary.dat
No such file or directory for GPU arch : gfx1151
で停止した。
つまり、ビルドに必要なROCm一式と、gfx1151で実際にrocBLASを動かすためのdevice packageは別だった。
device-gfx1151 を追加すると正常に動いた。
ビルドコマンド
ビルド時は、Visual Studio 2022の「x64 Native Tools Command Prompt」から
powershell -ep bypass
でpowershellを起動した後、ROCm 10.0 / TheRockをインストールした仮想環境を有効にするため、毎回
C:\TheRock\build\.venv\Scripts\Activate.ps1
を実行する必要がある。その後congfigure
cmake -S . -B .\build-rocm-b11247 -G "Ninja Multi-Config" `
"-DCMAKE_PREFIX_PATH=$RocmPath" `
-DGGML_BACKEND_DL=ON `
-DGGML_NATIVE=OFF `
-DGGML_HIP=ON `
"-DCMAKE_C_COMPILER=$RocmPath\lib\llvm\bin\clang.exe" `
"-DCMAKE_CXX_COMPILER=$RocmPath\lib\llvm\bin\clang++.exe" `
"-DCMAKE_C_FLAGS=-Wno-error=incompatible-pointer-types" `
"-DHIP_PATH=$RocmPath" `
-DGPU_TARGETS=gfx1151 `
-DLLAMA_BUILD_BORINGSSL=ON `
-DLLAMA_BUILD_EXAMPLES=OFF `
-DLLAMA_BUILD_TESTS=ON `
-DLLAMA_BUILD_TOOLS=ON `
-DLLAMA_BUILD_SERVER=ON `
-DLLAMA_BUILD_UI=OFF `
-DGGML_RPC=ON
その後 ggml-hip だけビルドして、
cmake --build .\build-rocm-b11247 `
--config Release `
--parallel 4 `
--target ggml-hip
うまく行ったら、llama-cli等の残りのツールもビルド
cmake --build .\build-rocm-b11247 `
--config Release `
--parallel 4 `
--target llama-cli llama-server llama-bench test-backend-ops
WindowsではROCm DLLの置き場所でもハマった
buildが通ったので終わりかと思ったら、
ROCm0: AMD Radeon(TM) 8060S Graphics (0 MiB, 0 MiB free)
となった。GPU名は見えているのにVRAMが0 MiB。
原因は、PATHにTheRock側を入れていてもSystem32側のAMD HIP runtimeが先に拾われる場合があることだった。
最終的にROCm 10.0側の、
amdhip64_7.dll
rocm_kpack.dll
amd_comgr.dll
を llama-cli.exe と同じディレクトリへコピーしたら
Available devices:
ROCm0: AMD Radeon(TM) 8060S Graphics (110456 MiB, 110301 MiB free)
と正常に認識した。
backend test
最後に、
& "$Bin\test-backend-ops.exe" test -b ROCm0 -o FLASH_ATTN_EXT
を実行。
結果は、
3982/3982 tests passed
Backend ROCm0: OK
だった。
ビルド時に大量のwarningは出るけど、今回の目的は「upstream b11247をなるべくそのままの状態でbaselineにする」ことなので、warningを消すためのsource修正やcompiler flag追加は行わなかった。
最終的には実モデルの llama-cli も正常に動作し、64kの長文入力まで完走。
これで、VulkanとROCmを同じsourceから自前ビルドして比較できる土台ができた。
まとめ、今後の予定
今回は環境を整えただけで、高速化の改造は何も進んでいない。
それどころか、前回効果があったQSA unionもまだ移植できていないし、TGに関してはかなり遅くなっている。
なんか後退しているようにも見えるけど、ある程度長く続けるならこういう整理は必要なんだと思う。
もし休暇がなくて前回の勢いのまま突き進んでいたら、今ごろ泥沼にはまっていたかもしれない。
次回はTGが遅くなった原因を調べて修正するところから始める予定。
休暇前、第9回の記事投稿後にもMTPを入れてみたり、他の高速化を試して却下したり、色々あったのだけど、それは後日別の記事にまとめることにする。
付録
長文要約テストのコマンド例
.\llama-cli.exe `
-m "(モデルのパス)" `
-c (コンテキスト長) `
-ngl 999 `
-ncmoe 0 `
-t 4 `
-tb 4 `
-b 2048 `
-ub 1024 `
-fa 1 `
-ctk f16 `
-ctv f16 `
--temp 0.2 `
--top-k 20 `
--top-p 0.8 `
--min-p 0.05 `
--jinja `
--single-turn `
--reasoning off `
-f 入力ファイルのパス `
-n 1024 `
-lv 4
テストデータについて
今回も前回までと同じ長文入力を使用した。
実入力token数は、
| context | 実入力 |
|---|---|
| 64k | 61,789 tokens |
| 128k | 126,253 tokens |
| 256k | 255,181 tokens |
256kでは要求context 262,144の約97.3%まで入力している。
前回までの記事と同じものなので詳細は省略する。
詳細な検証環境
Hardware
- Device: GMKtec NucBox EVO-X2
- CPU / APU: AMD Ryzen AI MAX+ 395 with Radeon 8060S
- CPU clock displayed by Windows: 3.00 GHz
- Installed memory: 128 GB
- UMA Frame Buffer: 96 GB
- Windows CPU側に見えるRAM: 約32 GB
- GPU: AMD Radeon 8060S Graphics
- GPU arch:
gfx1151
OS
- OS: Windows 11 Pro
- Version: 25H2
- OS Build: 26200.9168
- Installed: 2025-10-24
- Windows Feature Experience Pack: 1000.26100.344.0
ベースのllama.cpp
- Upstream repository:
ggml-org/llama.cpp - Build:
b11247 - Commit:
0bc845d356f437d5ce4fe975c36428f7522829cb - Commit title:
vulkan : reuse descriptor sets when bindings are constant (#29280) - r3 branch:
r3/upstream-first
Vulkan:
- Compiler: LLVM / Clang 20.1.8
- Vulkan SDK: 1.4.357.0
ROCm:
- ROCm: 10.0.0 / TheRock
- Compiler: AMD Clang 23.0.0
- Host toolchain: Visual Studio 2022 Build Tools / MSVC 14.44
- GPU target:
gfx1151
COMMON-001
902e9f4a762cc0fe89d613868c35e4861610081e
qwen4exp: support split PLE n-gram tensors
使用したモデル
- Repository:
unsloth/Qwen3.8-Flash-Next-GGUF - URL: https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF
- Quantization:
UD-IQ3_XXS - File size reported by llama.cpp: 76.32 GiB
- BPW: 3.71
- Model params: 176.94B
上記モデルを、LaurentZuijdwijkのStrix Halo専用fork内の、
gguf-py\gguf\scripts\gguf_split_ple_heads.py
でPLE16形式へ変換したモデルを使用した。