👻

vLLMにおける低並列での推論性能向上の試行

に公開
2026/01/07

はじめに

前回の記事ではAMD Ryzen AI Max+ 395でgpt-oss-20bをllama.cppとvLLMで推論させたときの性能と精度を評価した。

https://zenn.dev/anjn/articles/5afd0b2e525c27

その結果、vLLMでは低並列リクエスト時の性能がllama.cppに対して著しく低いことが分かったが、納得がいかなかったのでvLLMで推論性能が低い原因を調べてみた。

PyTorch Profilerによる調査

詳細は割愛するが、PyTorch Profilerを使用してvLLMで1トークンを生成するときのタイムラインを取得した。水色と紫のパターンが24回繰り返しているのは、Transformerを24ブロック実行していることを意味している。

拡大するとmatmul(行列積)カーネルの実行に時間がかかっていることがわかる。

以下はカーネルごとの実行時間を集計してみた図。

最も時間のかかっているカーネルの名前は_matmul_ogs_NNT_bf16xbf16xmxfp4_32x256x128x1_swigluとなっていて、おそらく以下を意味していると考えられる。

  • 入力と出力の型はbf16
  • 重みの型はmxfp4
  • block_m = 32
  • block_n = 256
  • block_k = 128
  • split_k = 1
  • SwiGLU活性化関数を融合

TransformerのFFNでは大きな行列の積を計算するが、それをそのまま計算するのではなくてタイル状に分割して計算する。block_{m,n,k}はそのタイルの大きさを表している。

行列積カーネルの最適化

上記の行列積の計算はTritonにより行われていて、行列の大きさに応じてどのようにタイル分割するかはヒューリスティックな手法で決定されているようだ。試しにブロックサイズを変更するとカーネルの実行時間が大きく変化し、短縮できる可能性があることがわかった。

最適なブロックサイズを求めるために、並列度(conc)とブロックサイズを変えながらカーネル実行時間を測定した。

$ python sweep_tiles.py 
conc=1 bm=16 bn=32 bk=128 sk=1  median=10.101 ms  samples=[10.100704000000007]
conc=1 bm=16 bn=64 bk=128 sk=1  median=11.037 ms  samples=[11.036509000000002]
conc=1 bm=16 bn=128 bk=128 sk=1  median=16.656 ms  samples=[16.65572499999999]
conc=1 bm=32 bn=32 bk=128 sk=1  median=11.430 ms  samples=[11.430138999999992]
conc=1 bm=32 bn=64 bk=128 sk=1  median=24.284 ms  samples=[24.283716000000002]
conc=1 bm=32 bn=128 bk=128 sk=1  median=28.853 ms  samples=[28.853489000000042]
BEST 1 {'bm': 16, 'bn': 32, 'bk': 128, 'sk': 1, 'median_ms': 10.100704000000007}
conc=2 bm=16 bn=32 bk=128 sk=1  median=15.147 ms  samples=[15.147296000000022]
conc=2 bm=16 bn=64 bk=128 sk=1  median=17.775 ms  samples=[17.775126000000007]
conc=2 bm=16 bn=128 bk=128 sk=1  median=17.025 ms  samples=[17.024792000000016]
conc=2 bm=32 bn=32 bk=128 sk=1  median=11.497 ms  samples=[11.496612999999986]
conc=2 bm=32 bn=64 bk=128 sk=1  median=37.420 ms  samples=[37.42023899999997]
conc=2 bm=32 bn=128 bk=128 sk=1  median=29.012 ms  samples=[29.011639999999993]
BEST 2 {'bm': 32, 'bn': 32, 'bk': 128, 'sk': 1, 'median_ms': 11.496612999999986}
conc=3 bm=16 bn=32 bk=128 sk=1  median=15.170 ms  samples=[15.170093999999994]
conc=3 bm=16 bn=64 bk=128 sk=1  median=17.639 ms  samples=[17.638684999999995]
conc=3 bm=16 bn=128 bk=128 sk=1  median=24.430 ms  samples=[24.430322000000032]
conc=3 bm=32 bn=32 bk=128 sk=1  median=11.533 ms  samples=[11.532536999999977]
conc=3 bm=32 bn=64 bk=128 sk=1  median=37.682 ms  samples=[37.68203199999997]
conc=3 bm=32 bn=128 bk=128 sk=1  median=46.550 ms  samples=[46.54976800000002]
BEST 3 {'bm': 32, 'bn': 32, 'bk': 128, 'sk': 1, 'median_ms': 11.532536999999977}
conc=4 bm=16 bn=32 bk=128 sk=1  median=15.256 ms  samples=[15.256412]
conc=4 bm=16 bn=64 bk=128 sk=1  median=17.777 ms  samples=[17.77655900000001]
conc=4 bm=16 bn=128 bk=128 sk=1  median=24.520 ms  samples=[24.520478999999973]
conc=4 bm=32 bn=32 bk=128 sk=1  median=11.505 ms  samples=[11.505040000000015]
conc=4 bm=32 bn=64 bk=128 sk=1  median=37.579 ms  samples=[37.578684999999986]
conc=4 bm=32 bn=128 bk=128 sk=1  median=46.486 ms  samples=[46.486446]
BEST 4 {'bm': 32, 'bn': 32, 'bk': 128, 'sk': 1, 'median_ms': 11.505040000000015}
conc=5 bm=16 bn=32 bk=128 sk=1  median=80.182 ms  samples=[80.18241600000006]
conc=5 bm=16 bn=64 bk=128 sk=1  median=94.947 ms  samples=[94.94713600000009]
conc=5 bm=16 bn=128 bk=128 sk=1  median=15.922 ms  samples=[15.922151999999999]
conc=5 bm=32 bn=32 bk=128 sk=1  median=43.256 ms  samples=[43.25561900000002]
conc=5 bm=32 bn=64 bk=128 sk=1  median=23.605 ms  samples=[23.605318999999987]
conc=5 bm=32 bn=128 bk=128 sk=1  median=19.858 ms  samples=[19.85752999999998]
BEST 5 {'bm': 16, 'bn': 128, 'bk': 128, 'sk': 1, 'median_ms': 15.922151999999999}
conc=6 bm=16 bn=32 bk=128 sk=1  median=80.110 ms  samples=[80.10993799999994]
conc=6 bm=16 bn=64 bk=128 sk=1  median=95.799 ms  samples=[95.79853300000006]
conc=6 bm=16 bn=128 bk=128 sk=1  median=15.987 ms  samples=[15.987370999999992]
conc=6 bm=32 bn=32 bk=128 sk=1  median=43.207 ms  samples=[43.20698200000002]
conc=6 bm=32 bn=64 bk=128 sk=1  median=23.584 ms  samples=[23.583591999999996]
conc=6 bm=32 bn=128 bk=128 sk=1  median=19.790 ms  samples=[19.789950999999984]
BEST 6 {'bm': 16, 'bn': 128, 'bk': 128, 'sk': 1, 'median_ms': 15.987370999999992}
conc=7 bm=16 bn=32 bk=128 sk=1  median=80.658 ms  samples=[80.657629]
conc=7 bm=16 bn=64 bk=128 sk=1  median=96.565 ms  samples=[96.56531200000012]
conc=7 bm=16 bn=128 bk=128 sk=1  median=15.989 ms  samples=[15.989277000000014]
conc=7 bm=32 bn=32 bk=128 sk=1  median=43.456 ms  samples=[43.45628800000001]
conc=7 bm=32 bn=64 bk=128 sk=1  median=23.845 ms  samples=[23.84533799999998]
conc=7 bm=32 bn=128 bk=128 sk=1  median=20.238 ms  samples=[20.23824]
BEST 7 {'bm': 16, 'bn': 128, 'bk': 128, 'sk': 1, 'median_ms': 15.989277000000014}
conc=8 bm=16 bn=32 bk=128 sk=1  median=81.026 ms  samples=[81.02575999999995]
conc=8 bm=16 bn=64 bk=128 sk=1  median=95.400 ms  samples=[95.39969900000006]
conc=8 bm=16 bn=128 bk=128 sk=1  median=16.175 ms  samples=[16.175329999999978]
conc=8 bm=32 bn=32 bk=128 sk=1  median=43.418 ms  samples=[43.41767900000001]
conc=8 bm=32 bn=64 bk=128 sk=1  median=23.697 ms  samples=[23.697203000000005]
conc=8 bm=32 bn=128 bk=128 sk=1  median=19.975 ms  samples=[19.975342000000005]
BEST 8 {'bm': 16, 'bn': 128, 'bk': 128, 'sk': 1, 'median_ms': 16.175329999999978}

行列積のブロックサイズは以下の関数で決定されている。

https://github.com/triton-lang/triton/blob/v3.5.0/python/triton_kernels/triton_kernels/matmul_ogs_details/opt_flags.py#L33

測定した結果からパラメータを決定し、関数の入り口に次のコードを追加することで、並列数に応じてブロックサイズを強制するようにした。

    constraints = dict(constraints)
    if m <= 16:
        constraints["block_m"] = 32
        constraints["block_n"] = 32
        constraints["block_k"] = 128
        constraints["split_k"] = 1
    else:
        constraints["block_m"] = 16
        constraints["block_n"] = 128
        constraints["block_k"] = 128
        constraints["split_k"] = 1

前回と同様に推論性能を測定した。

Conc. RPS Avg Lat.(s) P99 Lat.(s) Gen. toks/s Avg TTFT(s) P99 TTFT(s) Avg TPOT(s) P99 TPOT(s) Success Rate
1 0.06 15.851 15.851 32.30 0.116 0.116 0.031 0.031 100.0%
2 0.10 19.188 19.188 53.37 0.177 0.177 0.037 0.037 100.0%
3 0.12 25.627 25.640 59.91 0.275 0.336 0.050 0.050 100.0%
4 0.16 24.882 24.895 82.26 0.326 0.385 0.048 0.048 100.0%
5 0.20 25.403 25.418 100.71 0.389 0.448 0.049 0.049 100.0%
6 0.22 26.923 26.934 113.95 0.396 0.458 0.052 0.052 100.0%
7 0.25 27.975 28.005 127.98 0.487 0.564 0.054 0.054 100.0%
8 0.29 27.591 27.617 148.31 0.551 0.626 0.053 0.054 100.0%
16 0.46 34.785 34.824 235.23 1.517 1.643 0.065 0.068 100.0%
32 0.82 39.085 39.120 418.71 1.965 2.064 0.073 0.076 100.0%

トークン生成速度をグラフ化すると以下のようになる(緑が最適化後)。

行列積のパラメータを変更することですべての並列数においてトークン生成性能が向上した。TTFTも改善し、RPSが大きく向上している。

まとめ

低並列のときvLLMの推論性能が低かったのはTritonによる行列積のパラメータが最適でなかったためであることがわかった。パラメータを調整することで2~4倍くらい性能が向上した。

llama.cppと比べると並列数が1のときのみ大きく劣っている。今回最適化したmatmul以外にも時間がかかっているカーネルがあるのでそちらも改善の余地があるかもしれない。

バックアップ(コード修正に問題がありうまく最適化できなかったときの結果)

測定した結果からパラメータを決め、簡易的に次のようなコードを追加した。

    if tokens_per_expt <= 16:
        block_m, block_n, block_k, split_k = 32, 32, 128, 1
    elif tokens_per_expt <= 32:
        block_m, block_n, block_k, split_k = 16, 128, 128, 1    

最適化の結果

前回と同様に推論性能を測定した。

Conc. RPS Avg Lat.(s) P99 Lat.(s) Gen. toks/s Avg TTFT(s) P99 TTFT(s) Avg TPOT(s) P99 TPOT(s) Success Rate
1 0.06 15.963 15.963 32.07 0.218 0.218 0.031 0.031 100.0%
2 0.10 19.529 19.529 52.43 0.177 0.177 0.038 0.038 100.0%
3 0.12 26.022 26.035 59.00 0.545 0.686 0.050 0.050 100.0%
4 0.16 24.700 24.701 82.91 0.584 0.584 0.047 0.047 100.0%
5 0.08 58.950 58.966 43.41 0.730 0.847 0.114 0.115 100.0%
6 0.10 62.247 62.283 49.32 0.815 0.941 0.120 0.121 100.0%
7 0.10 71.727 71.762 49.94 0.911 1.032 0.139 0.140 100.0%
8 0.13 63.062 63.125 64.88 0.882 1.149 0.122 0.122 100.0%
16 0.21 74.561 74.605 109.81 3.317 3.554 0.139 0.145 100.0%
32 0.33 97.719 97.785 167.48 3.381 4.189 0.185 0.189 100.0%

グラフ化すると以下のようになる(緑が最適化後)。

並列数が4以下の場合の推論性能が大きく向上した。それ以外の並列数ではなぜか性能が下がってしまったので、もう一度きれいな状態からやり直してみるつもり。

Tritonのmatmul以外にも時間がかかっているカーネルがあるのでそちらも最適化の余地があるかもしれない。

Tritonについて気がついたこと

vLLM v0.13.0ではTriton v3.5.0のtriton_kernelsを使用している。Triton v3.5.1でmatmulのブロックサイズの決定ロジックに変更が入っているので、Tritonのバージョンを変えるだけでまたパフォーマンスが変わるかもしれない。これも試してみるつもり。 → 追記:この変更はNvidia GPUのみが対象、かつv3.5.1で入った変更ではなさそう。

https://github.com/triton-lang/triton/commit/0a2e3a391cbb9e13d29bf12a2a0005e358102d74

Discussion