MoE をテンソルコアで計算する
64 トークン以上の MoE の行列積について、低ビットの重みを共有メモリ上で FP16 に展開し、Volta のテンソルコアで計算するカーネルを書いた。第 1 版は展開のコストが重く、既存の MMQ より遅かった。モデル全体の prefill で、第 2 版は MMQ 比 +6% 前後(192K 設定・250 W)、第 3 版は第 2 版比 +9% 前後。精度は CPU の計算結果と突き合わせて確認している。
Tesla V100 ×2 · llama.cpp
NVLink も P2P もない Tesla V100 ×2 のサーバで、Qwen3.8-Flash-Next(180B MoE)を 256K コンテキストで普段使いしている。llama.cpp に手を入れて、prefill を約 585 t/s から 2,361 t/s まで上げた。そのときの構成、数字、うまくいかなかったことのメモ。
改造前の約 585 t/s から 約 4.0 倍
boost 250 W:最大 2,591.5 t/s(depth 128K)
GPU 間は PCIe だけで、片側は x4。層分割+パイプライン並列しか選べないので、2 枚の処理の重なりを切らさないことが速度をほぼ決めた。
MoE をテンソルコアで回す自作カーネルと、同期・メモリ予約まわりの修正を重ねて、prefill は改造前の約 4 倍。
実際の作業での decode 100 t/s には届いていない。MTP の受理率と、decode 側のカーネルが次の課題。
GPU は Tesla V100-PCIE-32GB と Tesla PG500-216(中身は V100 PCIe 32GB)の 2 枚。NVLink はなく、GGML_CUDA_P2P を有効にすると出力が壊れたので P2P も使っていない。さらに V100 はチップセット側のスロットで、電気的には x4 になっている。
80 GiB のモデルは tensor-split 26,24 で層ごとに 2 枚へ分け、パイプライン並列で動かしている。層の受け渡しは毎回ホスト経由になる。
PLE(per_layer_token_embd)は約 27 GiB あり、VRAM には載せずホスト RAM に mmap している。中身は 2-gram・3-gram のハッシュ表 16 本(各約 2,000 万行)で、1 トークンごとに散らばった 16 行を読む。RAM 30 GiB に対してこれなので、メモリにはほとんど余裕がない(07 参照)。
| GPU | Tesla V100-PCIE-32GB + Tesla PG500-216(各 32 GB) |
|---|---|
| 接続 | PG500:PCIe Gen3 x16(CPU 直結)/ V100:PCIe Gen3 x4(チップセット経由) |
| CPU・RAM | Ryzen 7 5700X3D / DDR4-3000 30 GiB |
| ストレージ | モデル用に NVMe 500 GB を追加。ロードが 144–156 s から 41–45 s に |
| モデル | Qwen3.8-Flash-Next GGUF 80.0 GiB(MTP 込み)experts UD-IQ3_XXS / dense Q8_0 / KV q8_0 |
| ソフト | llama.cpp 0.5.0 + 有志の Qwen3.8 対応パッチ + 自前の修正(qwen38-v100-patches)CUDA 12.9.86・sm_70・Ubuntu 26.04.1 |
| サーバ設定 | 192K(196,608)/ 256K(262,144・常用)/ 320K(はみ出し用・YaRN 1.25・MTP なし)前段のルーターがリクエスト長で振り分け |
| 電力 | 常用は PL 170 W(prefill 中 170 W・それ以外 150 W に自動で切り替え)。数字は特に断りがなければ 170 W、03 章と V6 boost だけ 250 W |
出発点の V0 は、配布元のパッチに上流でマージ済みの PLE 先読み(#29599)を移植し、192K 設定・ubatch 512 にした状態で約 585 t/s。パッチを当てただけの 256K 設定では 362〜419 t/s で、配布元のモデルカード(DGX-1 の V100-SXM2 ×2・層分割で 710 t/s)の 5〜6 割だった。
V1 以降は構成を切り替えるたびに、256K 設定で合言葉入りの長い入力(05 の needle 検査)を流して prefill を測っている。V2 以降は 74,627 トークン、V1 だけ 50,139 トークン。電力は常用の PL 170 W。
V0 は参考値で、192K 設定・ubatch 512・入力 32K / 64K・PL 170 W・MTP ありで測った 578〜590 t/s。V6 boost は PL 250 W で depth 別に測ったときの最大値(depth 128K・3 回平均・03 参照)。増減は一つ前の構成との比較で、検査の条件が違う V0→V1・V1→V2 は概算。* V6 だけは、モデルを NVMe に移したあと同日に V5 を測り直した値(2,127.2 t/s)との比較。
V3 の +63% はカーネル単体の効果ではない
V3 ではカーネルの差し替えと同時に、同期まわりの修正と ubatch の拡大も入れている。カーネル以外の条件を揃えてモデル全体の prefill で比べると、第 2 版は既存の MMQ 比で +6% 前後(192K 設定・250 W。256K では +2%)、第 3 版は第 2 版比で +9% 前後だった。
V6 を depth ごとに測った結果(256K 設定・各 3 回の平均)。この章と 02 の V6 boost だけ PL 250 W(boost)で、常用の 170 W で測った他の章の数字とは直接比べられない。
prefill は depth 32K〜190K で 2,309〜2,592 t/s、ピークは 128K。depth 250K でも 2,181 t/s 出る。decode は depth にあまり影響されず 121〜128 t/s。電力・温度による減速は一度もなかった。
depth 0 は 1,978 トークンの新規プロンプト。32K 以降は、前の段に 3.2 万〜6.7 万トークンを足したときの prefill。
| depth | 0 | 32K | 64K | 128K | 190K | 250K |
|---|---|---|---|---|---|---|
| decode | 123.1 | 122.2 | 128.0 | 125.8 | 124.3 | 121.2 |
合成テキストと実際の作業で、decode が大きく違う
設計・レビュー・質問回答の 3 種のプロンプト(temperature 0.7・top_p 0.9)では、それぞれ 99.1 / 78.5 / 81.3 t/s。MTP の受理率が合成テキストの 94〜98% から 45〜66% に落ちるのが主な理由で、decode を伸ばすならここになる。
起動オプションと環境変数で変えられるものは一通り試した。
nsys で見ると、ubatch をある程度以上大きくしても CPU 側のカーネル起動が詰まるだけで速くならなかった。設定だけで詰められる範囲はわりと早く尽きた。
修正はすべて環境変数で切り替えられる。
64 トークン以上の MoE の行列積について、低ビットの重みを共有メモリ上で FP16 に展開し、Volta のテンソルコアで計算するカーネルを書いた。第 1 版は展開のコストが重く、既存の MMQ より遅かった。モデル全体の prefill で、第 2 版は MMQ 比 +6% 前後(192K 設定・250 W)、第 3 版は第 2 版比 +9% 前後。精度は CPU の計算結果と突き合わせて確認している。
計算バッファの取り直し、グラフの作り直し、入力転送のどれかで 2 枚が同期すると、重なりが途切れる。メモリ予約を先に済ませる、QSA の chunk サイズを固定する、予約用グラフを小さくする、KV の参照範囲の丸め方を変える、入力転送の同期を外す、といった修正で同期点を減らした。計算の内容は変えていない。
prefill 中の MTP の追いつきは KV の書き込みだけにして、次の ubatch の後ろへ回した。追いつき幅は 128 トークン。ドラフト側の出力語彙は頻出 65,536 語に絞っている(検証は全語彙なので出力は変わらない)。
新しい世代の GPU 前提で書かれている処理を、Volta で動くように置き換えたもの。
| 案 | 結果 | 判定 |
|---|---|---|
| CPU 経由で 2 枚同時に decode | 実測で効果なし | 不採用 |
| ubatch 2048 / 4096 | 2048 は 1536 比 −3.6〜−10.7%。4096 は出力が壊れた | 不採用 |
| batch 61,440 | 短い入力は +4.1%、64K で −4.1%、128K で −2.8% | 不採用 |
| QSA ブロック 256/512・4 件まとめ | 単体では +44%、モデル全体では −5〜−7% | 不採用 |
| Q8 経路の mmvq 高速化 | 出力は完全一致したが、目標の平均 +2% に届かず | 不採用 |
| MTP ドラフトの余分な前処理を削る | 1 ステップあたり 0.4% しか変わらず、再ビルドで起動が 8 分延びる | 不採用 |
--moe-expert-cache | Q4 版での試験で decode が 30 → 21 → 19 t/s。CPU に回る expert が増え、キャッシュヒットで取り返せない | 不採用 |
別の推論エンジンの Strata も試している。
Strata v0.1.40.4 と V6 へ同じプローブを当てて測った。条件は PL 170 W、思考なし、greedy。
prefill(t/s)ロード直後の 1 回目・目盛 0–2,400
decode(t/s)greedy・2 回平均・目盛 0–125・破線 = 100 t/s
prefill は 1 回ずつ。V6 の 2 回目は 2,161.0 / 2,296.9 / 2,370.7 t/s で、26.7K だけ 1 回目が低い。decode は 2 回の平均どうしの差。
同じ測り方なら、V6 が prefill で 10〜14%、decode で 1〜5% 速い。Strata も 0.1.39 から 0.1.40.4 で decode が平均 6.6% 伸びている(prefill はほぼ同じ)。
思考なし・greedy の短い質問なら V6 は日本語 108.9、コード 119.7 t/s で 100 を超える(英語は 98.4)。ただ、実際の作業に近いプロンプトでは 78〜99 t/s のまま(03・250 W)。エンジンを替えても同じ測り方で V6 を上回るものはなかった。
実装と測定には AI コーディングエージェントを使ってほぼ無人で回した。
needle 検査
2,600 行のログ風テキストの途中 1 行に合言葉を埋め込み、全体を 74,627 トークンにして答えさせる。
長文の中の 1 点を拾えるかの確認と、毎回同じ入力での速度計測を兼ねている。02 のグラフの V2〜V6 はこの検査での値(V1 は 50,139 トークン版)。
V6 に切り替えてからの約 12 時間、Qwen Code 0.25.0(VS Code 拡張)から普段どおり使ったときのログを集計した。
VRAM は 256K 設定で PG500 側が 31,400 MiB まで埋まる(残り約 1.3 GiB)。
それまで使っていた Qwen3.8-27B(dense・Q6)と、同じ課題セットで比べた。
正答数に意味のある差は出なかった(一般課題 p=0.69)。課題が易しすぎて両方とも上限近くに張り付いている。一方、修正 1 件にかかる時間は 6.7 倍違ったので、普段使いは Flash-Next に切り替えた。
無人で回していると、同じ失敗に気づかないまま何度も繰り返しやすい。起きたものは、運用ルールを足すよりスクリプト側で防ぐようにした。
| # | 起きたこと | 対策 |
|---|---|---|
| 1 | 改行コードで計画が誤作動計画ファイルに CRLF が混じり、チェックを通っていない設定で走った | キューへの投入をスクリプト経由に限定(形式チェック・ハッシュ照合・アトミックな配置) |
| 2 | 空の応答のまま 25 分空回り測定結果が妥当かを確認せずに受け取っていた | マルチターンの模擬測定に、応答の中身を確かめるターンを追加。おかしければ無効として扱う |
| 3 | エージェントが作った検査で 5 回停止レビューせずにそのまま実行した | 受け入れ前の確認手順を決め、実機での dry-run を必須にした |
| 4 | 死活監視がモデルをアンロードするルーターが叩く /slots でモデルがメモリから退避されていた | サーバが空くのを待ってから操作するようにした |
| 5 | PLE と RAM の取り合いbatch 16,384 で direct reclaim が 21 倍に増えた | 当時は batch 8,192 に戻した(V3 以降は ubatch 1,536 / batch 15,360) |
| 6 | 早期 EOS を疑った件短い応答で止まる退行を疑い、302 回ずつ再実行して比較(p=0.088) | V6 の退行ではなく temperature 1 のサンプリングの性質と判断。min_p 0 を採用 |
MoE の計算は GPU 時間の 30〜40% 程度。残りの多くは同期とメモリ予約、カーネルの積み方の整理で縮んだ。
ただし第 1 版は既存の MMQ に負けた。第 2 版で上回り、第 3 版で差を広げた。
MTP の追いつきなどが原因で、ここを数字で押さえられたことが V5 の修正につながった。
空の応答を成功扱いにしない、レビューしていない検査を走らせない。モデルよりも測る側で事故が起きた。
| 構成ごとの prefill | 256K 設定・合言葉入りの長文(V2〜V6 は 74,627、V1 は 50,139 トークン)・常用の PL 170 W。切替直後の検査での値 |
|---|---|
| depth 別 | V6・256K 設定・PL 250 W・各 3 回平均・2 GPU の層分割で計測 |
| Strata 比較 | PL 170 W・思考なし・greedy。prefill は 1 回、decode は 2 回平均 |
| 実使用 | V6・普段の電力設定・Qwen Code からのリクエストをサーバログで集計 |
| 解析 | nsys / ncu |
llama.cpp への修正は GitHub の qwen38-v100-patches で公開している。配布元の v3 パッチの上に当てる差分で、V6 は版 pcie-20261006。