https://preview.redd.it/nqok3dch7utg1.jpg?width=4096&format=pjpg&auto=webp&s=d5c1d3f5e5c1d8c0ba986726d2bda08212175fec
みなさんこんにちは。私は Strix Halo miniPC(Minisforum MS-S1 Max)を持っています。OCuLink 経由でそこに RTX 5070 Ti の eGPU を追加し、llama.cpp 上で両者がどのように連携するかについていくつかテストを行いました。そこで得られた所見を共有したいと思います。
所見の要約(TL;DR):
- Vulkan の汎用性: これは非常に効率的な API で、AMD APU + NVIDIA GPU のように異なるベンダーのチップを安定して組み合わせられます。ネイティブの CUDA または ROCm と比べた性能低下は最小で、せいぜい 5〜10% です。
- OCuLink の役割: この接続の帯域幅は、トークン生成(tg)やプロンプト処理(pp)をボトルネックにしません。転送されるデータはごくわずかです。実際のレイテンシは、遅い APU を待っている間に GPU がアイドル状態になることによって生じています。
- Amdahl の法則とテンソル分割: llama.cpp ではデバイスがレイヤーを厳密に順次処理します(リレーレースのように)。そのため、遅いメモリに一部の計算をオフロードすると、全体の速度は非線形で双曲線的に低下します。この順次実行における全体的な性能劣化こそが、Amdahl の法則が説明するものです。
まず、各 GPU についてネイティブのバックエンドを使った標準的な llama-bench の結果です:
~/llama.cpp/build-rocm/bin/llama-bench -m ~/llama-2-7b.Q4_0.gguf -ngl 99 -fa 1 -p 512,2048,8192
ggml_cuda_init: 1 つの ROCm デバイスを検出しました(総 VRAM: 126976 MiB): Device 0: Radeon 8060S Graphics, gfx1151 (0x1151), VMM: なし, Wave Size: 32, VRAM: 126976 MiB
| model | size | params | backend | ngl | fa | test | t/s |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | ROCm | 99 | 1 | pp512 | 1493.28 ± 30.20 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | ROCm | 99 | 1 | pp2048 | 1350.47 ± 40.94 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | ROCm | 99 | 1 | pp8192 | 958.19 ± 1.85 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | ROCm | 99 | 1 | tg128 | 50.16 ± 0.07 |
~/llama.cpp/build-cuda/bin/llama-bench -m ~/llama-2-7b.Q4_0.gguf -ngl 99 -fa 1 -p 512,2048,8192
ggml_cuda_init: 1 つの CUDA デバイスを検出しました(総 VRAM: 15841 MiB): Device 0: NVIDIA GeForce RTX 5070 Ti、計算能力 12.0、VMM: はい、VRAM: 15841 MiB
| model | size | params | backend | ngl | fa | test | t/s |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | CUDA | 99 | 1 | pp512 | 8476.95 ± 206.73 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | CUDA | 99 | 1 | pp2048 | 8081.18 ± 27.82 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | CUDA | 99 | 1 | pp8192 | 6266.69 ± 6.90 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | CUDA | 99 | 1 | tg128 | 179.20 ± 0.13 |
次に、Vulkan を使った各 GPU のテストです:
GGML_VK_VISIBLE_DEVICES=0 ~/llama.cpp/build-vulkan/bin/llama-bench -m ~/llama-2-7b.Q4_0.gguf -ngl 99 -fa 1 -p 512,2048,8192
ggml_vulkan: 1 つの Vulkan デバイスを検出しました: ggml_vulkan: 0 = NVIDIA GeForce RTX 5070 Ti (NVIDIA) | uma: 0 | fp16: 1 | bf16: 1 | warp size: 32 | shared memory: 49152 | int dot: 1 | matrix cores: NV_coopmat2
| model | size | params | backend | ngl | fa | test | t/s |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | pp512 | 7466.51 ± 17.68 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | pp2048 | 7216.51 ± 1.77 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | pp8192 | 6319.98 ± 7.82 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | tg128 | 167.77 ± 1.56 |
GGML_VK_VISIBLE_DEVICES=1 ~/llama.cpp/build-vulkan/bin/llama-bench -m ~/llama-2-7b.Q4_0.gguf -ngl 99 -fa 1 -p 512,2048,8192
ggml_vulkan: 1 つの Vulkan デバイスを検出しました: ggml_vulkan: 0 = Radeon 8060S Graphics (RADV STRIX_HALO) (radv) | uma: 1 | fp16: 1 | bf16: 0 | warp size: 64 | shared memory: 65536 | int dot: 1 | matrix cores: KHR_coopmat
| model | size | params | backend | ngl | fa | test | t/s |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | pp512 | 1327.76 ± 17.68 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | pp2048 | 1252.70 ± 5.86 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | pp8192 | 960.10 ± 2.37 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | tg128 | 52.29 ± 0.15 |
そして最も興味深い部分は、Vulkanによるテンソル分割で、2つのGPUが一緒に動作するところをテストしたことです。モデルの重みは、NVIDIA RTX 5070 Ti のVRAMと、AMD Radeon 8060S のUMAに、以下の割合で分配されました:100%/0%、90%/10%、80%/20%、70%/30%、60%/40%、50%/50%、40%/60%、30%/70%、20%/80%、10%/90%、0%/100%。
GGML_VK_VISIBLE_DEVICES=0,1 ~/llama.cpp/build-vulkan/bin/llama-bench -m ~/llama-2-7b.Q4_0.gguf -ngl 99 -fa 1 -dev vulkan0/vulkan1 -ts 10/0,9/1,8/2,7/3,6/4,5/5,4/6,3/7,2/8,1/9,0/10 -n 128 -p 512 -r 10
ggml_vulkan: 2つのVulkanデバイスを見つけました:ggml_vulkan: 0 = NVIDIA GeForce RTX 5070 Ti (NVIDIA) | uma: 0 | fp16: 1 | bf16: 1 | warp size: 32 | shared memory: 49152 | int dot: 1 | matrix cores: NV_coopmat2 ggml_vulkan: 1 = Radeon 8060S Graphics (RADV STRIX_HALO) (radv) | uma: 1 | fp16: 1 | bf16: 0 | warp size: 64 | shared memory: 65536 | int dot: 1 | matrix cores: KHR_coopmat
| model | size | params | backend | ngl | fa | dev | ts | test | t/s |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 10.00 | pp512 | 7461.22 ± 6.37 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 10.00 | tg128 | 168.91 ± 0.43 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 9.00/1.00 | pp512 | 5790.85 ± 52.68 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 9.00/1.00 | tg128 | 130.22 ± 0.40 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 8.00/2.00 | pp512 | 4230.90 ± 28.90 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 8.00/2.00 | tg128 | 112.66 ± 0.23 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 7.00/3.00 | pp512 | 3356.88 ± 27.64 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 7.00/3.00 | tg128 | 99.83 ± 0.20 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 6.00/4.00 | pp512 | 2658.89 ± 13.26 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 6.00/4.00 | tg128 | 85.67 ± 2.50 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 5.00/5.00 | pp512 | 2185.28 ± 16.92 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 5.00/5.00 | tg128 | 76.73 ± 1.13 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 4.00/6.00 | pp512 | 1946.46 ± 19.60 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 4.00/6.00 | tg128 | 62.84 ± 0.15 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 3.00/7.00 | pp512 | 1644.25 ± 29.88 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 3.00/7.00 | tg128 | 58.38 ± 0.31 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 2.00/8.00 | pp512 | 1458.99 ± 19.70 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 2.00/8.00 | tg128 | 55.70 ± 0.49 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 1.00/9.00 | pp512 | 1304.67 ± 45.80 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 1.00/9.00 | tg128 | 54.16 ± 1.07 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 0.00/10.00 | pp512 | 1194.55 ± 5.25 |
| llama 7B Q4_0 | 3.56 GiB | 6.74 B | Vulkan | 99 | 1 | Vulkan0/1 | 0.00/10.00 | tg128 | 52.62 ± 0.72 |
分割レイヤーでトークン生成を行う際、全体のtgおよびpp速度の低下はアムダールの法則に従います。少しの割合でもレイヤーを帯域の低いメモリに移すとボトルネックが生まれ、その結果、全体速度(t/s)が非線形に低下します。グラフにすると、古典的な双曲線の形になります。
https://preview.redd.it/8frnjhri7utg1.jpg?width=1600&format=pjpg&auto=webp&s=2577562f66d60ba572670cea11bad2da588c6256
式: P(s) = 100 / [1 + s(k - 1)]
ここで:
- P(s) = システム全体の速度(最大eGPU速度に対する割合、%)。
- s = モデルのうち遅いAPU RAMにオフロードされる割合(0から1。0はすべてVRAM、1はすべてRAM)。
- k = メモリ帯域幅ギャップの比率。最大速度を最小速度で割って計算します(k = V_max / V_min)。
ご覧のとおり、全体のtgおよびpp速度は各ノードのtgとppにだけ依存します。OCuLinkは全体速度にまったく影響しません。
詳細な結論 & 技術的分析:
ベンチマークデータとLLMのアーキテクチャ上の特徴に基づいて、なぜこれらの結果が出るのかをより深く分解します。
1. Vulkanは、ベンダーをまたぐ推論のための究極のAPI
歴史的に、1つのパイプライン内で計算タスクのためにAMDとNVIDIAのチップを混ぜることは、ドライバの悪夢でした。しかし、llama.cppのVulkanバックエンドは状況を完全に変えます。
- 根拠: Vulkanはハードウェア層を抽象化し、まったく異なるアーキテクチャ間で行列乗算の数学を標準化します(APU上のRDNA 3.5と、RTX 5070 Ti上のAda/Blackwellアーキテクチャ)。
- 結果: 個別のVRAMとシステムUMAメモリのシームレスで安定したプーリングを可能にします。CUDAやROCmのような高度に最適化されたネイティブバックエンドと比べた性能ペナルティは、実質的に無視できるほど小さい(約5〜10%のみ)。生の速度のごくわずかな割合はAPI変換レイヤーに奪われますが、クラッシュすることなく、異なるハードウェアのエコシステムにまたがってより大きいモデルを収められるという大きな利点を得られます。
2. OCuLink神話: PCIe 4.0 x4はLLMのボトルネックではない
eGPUコミュニティには、OCuLinkの限られた帯域(約7.8 GB/sまたは64 Gbps)がAI性能を鈍らせるという広く知られたステレオタイプがあります。しかし、LLM推論においてこれはまったくの誤りです。OCuLinkの帯域は、アクティブな生成の間にわずか1%しか使われていません。通信によるペナルティが実質ゼロに近い理由を示す計算は以下です:
- トークン生成(デコードフェーズ): Transformerアーキテクチャのおかげで、GPUはニューラルネットワーク全体を行ったり来たりで送信しません。モデルを2つのデバイスに分割すると、1度に1トークン分の隠れ状態(アクティベーション)の小さなテンソルだけが渡されます。7B、さらには70Bモデルでも、このペイロードはおおよそ数十キロバイトです。7.8 GB/sの接続でキロバイトを送るのにかかるのは、マイクロ秒の「ほんの一部」です。
- コンテキスト処理(プリフェーズ): 1万トークン以上の巨大なプロンプトを消化するときでさえ、llama.cppはデータをチャンク(通常512トークンずつ)にして処理します。512トークンのチャンクは、PCIeバスをまたいで転送されるデータが数メガバイト程度に収まります。OCuLinkで8MBを動かすのにかかる時間は約1ミリ秒です。一方で、GPUがそのチャンクを実際に計算するのには数十ミリ秒〜数百ミリ秒かかります。
- 真のボトルネック: システム速度は、デバイス間のPCIe接続ではなく、各ノード単体のメモリ帯域によって完全に決まります(RTX 5070 Tiが約900 GB/s、APUが約200 GB/s)。OCuLinkの細いバスが実際にあなたに不利になるのは、SSD/RAMからeGPUへモデルの重みを初期ロードするとき(1ではなく3〜4秒かかる)や、巨大な勾配配列を絶えず移動させる必要があるフルのファインチューニングの間だけです。
3. アムダールの法則と、「リレーレース」パイプラインの詰まり
バッチサイズ1(マイクロバッチなしの標準ローカル推論)で、複数デバイスにまたがるTensor Splittingを使うと、llama.cppは厳密に逐次的なパイプラインを実行します。
- 根拠: レイヤー2は、レイヤー1が完了しない限り計算できません。モデルの80%を稲妻のように速いRTX 5070 Tiに、残り20%を遅いAMD APUに置いても、それらは同時に動きません。RTXは自分のレイヤーを瞬時に処理し、アクティベーションの小さなテンソルをOCuLink経由で渡したあと、眠りに入ります(パイプラインスタール)。メモリ帯域に制約のあるAPUが、レイヤーの20%分を擦り切れるように処理し終えるのを待っている間、RTXは完全にアイドル状態のままです。
- 結果: あなたは計算能力を追加しているのではなく、リレーレースに遅い走者を追加しているのです。速いGPUは待たされることを強いられるため、遅いシステムメモリにレイヤーをオフロードすることによる性能ペナルティは非線形になります。データが示すとおり、それはアムダールの法則に支配された古典的な双曲線として見事にグラフ化できます。ワークロードのわずか10〜20%を遅いノードに移すだけで、トータルの「1秒あたりのトークン数」が不釣り合いに大きく落ちます。
システム構成:
- ベース: Minisforum MS-S1 Max(Strix Halo APU、AMD Radeon 8060S iGPU、RDNA 3.5アーキテクチャ)。クワイエット電源モード。
- RAM: 128GB LPDDR5X-8000(iGPUメモリ帯域は実測で約210 GB/s、理論値は256 GB/s)。
- OS: CachyOS(Linux 6.19.11-1-cachyos)に最新のMesaドライバ(RADV)。GRUBパラメータで起動:
GRUB_CMDLINE_LINUX="... iommu=pt amdgpu.gttsize=126976 ttm.pages_limit=32505856"
eGPUセットアップ:
- GPU: NVIDIA RTX 5070 Ti
- Minisforum MS-S1 MaxでOCuLinkポートを得るために、OCuLink SFF8611/8612アダプタにPCIe 4.0 x4を追加しました。
- ドック: 安いF9G-BK7のeGPUドックを購入しました。PSUは1STPLAYER NGDP Gold 850Wです。
- すべてが最初から問題なく動作し、互換性の問題はゼロでした。
submitted by