Sonar (Aphrodite Engine) + EXL3 + GDNハイブリッド構成 検証記録
日付: 2026-07-15
環境: cadsv (WSL2 Ubuntu, RTX 5070 ×2 / 12GB×2)
背景
TabbyAPI(exllamav3)でQwopus3.6-27B(EXL3量子化)を運用中、OpenWebUI+Hermesの同時利用でリクエストが直列処理になる問題があった。Continuous Batchingを持つSonar(Aphrodite Engine, vLLMフォーク)への移行を検証した。
発見した2つの実バグと修正
Aphrodite-engine 0.21.0はEXL3量子化に対応しているが、Qwen3.6/Qwen3.5系のハイブリッドMamba/GDNアーキテクチャ + LoRA有効時の組み合わせで2つのバグを踏んだ。いずれもローカルパッチで解決。
バグ1: EXL3のin_proj_qkv層のshard_id不一致
症状: --enable-lora指定時、ValueError: Missing EXL3 tensors for ...in_proj_qkv: suh[0], suh[1], suh[2], ...で起動失敗。
原因: aphrodite/model_executor/models/qwen3_5.pyのload_weights()は、LoRA有効時にin_proj_qkvの重みをタプル(0, 1, 2)という単一キーでロードする:
if self.enable_lora:
stacked_params_mapping.extend([
("in_proj_qkv", "in_proj_qkv", (0, 1, 2)), # ← タプル1個で1回だけ呼ばれる
("in_proj_z", "in_proj_z", 0),
])
しかしaphrodite/model_executor/layers/quantization/exl3.pyの_shard_ids_for_layer()には、LoRA無効時の統合レイヤー名in_proj_qkvz向けの分岐しかなく、LoRA有効時の単体レイヤー名in_proj_qkv向けの分岐が無かった。そのため汎用フォールバックに落ちて[0, 1, 2]という3つの別々の整数キーを期待してしまい、実際にロードされたタプルキー(0, 1, 2)と一致しなかった。
exllamav3自体のアーキテクチャ定義(architecture/qwen3_5.py)を確認したところ、GDN層のqkv投影は常にq+k+vを1つの融合trellis行列として量子化する仕様であり、LoRA有無に関わらず一貫していることを確認した。
修正(1行追加):
# aphrodite/model_executor/layers/quantization/exl3.py
if prefix.endswith("in_proj_qkv"):
return [(0, 1, 2)]
バグ2: LoRAレイヤーのデバイス判定がEXL3を認識しない
症状: ValueError: Unsupported base layer: MergedColumnParallelLinear(...)
原因: aphrodite/lora/layers/utils.pyの_get_lora_device()が、量子化方式ごとに決め打ちの属性名(weight, weight_packed, qweight, w2_weight等)でデバイスを判定していたが、EXL3の属性名(suh/svh/trellis/mcg/mul1)がリストに無かった。EXL3+LoRAの組み合わせがAphrodite全体として一度も配線されていなかったことを意味する。
修正:
elif hasattr(base_layer, "trellis"):
return torch.device("cuda", torch.cuda.current_device())
(EXL3はCUDA専用実装のため、常にcurrent_deviceで問題ない)
環境要因: WSL2でのlibcuda.soリンクエラー
flashinferのJITビルド時に/usr/bin/x86_64-linux-gnu-ld.bfd: cannot find -lcuda。/usr/local/cudaがconda-forge由来のCUDAツールキット(exllamav3 micromamba環境)を指すシンボリックリンクになっており、そちらのdevスタブはtargets/x86_64-linux/lib/stubs/libcuda.soという非標準レイアウトだったため、PyTorchが期待する$CUDA_HOME/lib64/stubs/libcuda.soが見つからなかった。sonar環境専用の独立スタブディレクトリ(/usr/lib/wsl/lib/libcuda.so.1へのシンボリックリンク)を作成し、LIBRARY_PATHに追加することでexl3・既存環境に触れずに解決。
ベンチマーク結果
モデル・設定
- モデル: Qwen3.6-27B-exl3-3.08bpw (4.00bpw版も検証したが3.08bpwの方が余裕があり実用的)
- TP=2, KVキャッシュ FP8, gpu-memory-utilization=0.88
- max-model-len=81920, max-num-seqs=6
- LoRA無し(LoRA込みだとメモリ予算が厳しく別途要検討)
CUDAグラフ有効化の効果
--enforce-eager(CUDAグラフ/torch.compile無効)で最初動かしていたが、速度が出なかったため通常モードに切り替えた。
┌────────┬──────────────────────┬─────────────────────────┐
│ 並列数 │ eagerモード(合計t/s) │ CUDAグラフ有効(合計t/s) │
├────────┼──────────────────────┼─────────────────────────┤
│ 1 │ 11.23 │ 32.62 │
├────────┼──────────────────────┼─────────────────────────┤
│ 2 │ 20.39 │ 60.32 │
├────────┼──────────────────────┼─────────────────────────┤
│ 3 │ 32.28 │ 85.60 │
├────────┼──────────────────────┼─────────────────────────┤
│ 4 │ 44.88 │ 99.81 │
├────────┼──────────────────────┼─────────────────────────┤
│ 5 │ 56.06 │ 125.68 │
├────────┼──────────────────────┼─────────────────────────┤
│ 6 │ 64.81 │ 146.67 │
└────────┴──────────────────────┴─────────────────────────┘
CUDAグラフ有効化で単発速度は約2.9倍(11.2→32.6 t/s)、6並列合計では約2.3倍(64.8→146.7 t/s)に向上。
トレードオフとして、CUDAグラフ用バッファの分だけKVキャッシュプールが減少:
- eagerモード: 143,360トークン (81,920トークン1本あたり concurrency 1.75x)
- CUDAグラフ有効: 111,177トークン (同 concurrency 1.36x)
並列処理の実地検証(Continuous Batching)
1人が約54,000トークン(実測、狙いは80K)のロングコンテキストを使いながら、他5人がランダムな短めのリクエストを同時に投げるテスト:
- 開始直後: running=5, waiting=1(KVブロック空き待ちで1件だけ一時的に待機)
- 数秒後: running=6, waiting=0(全員着席)
- 全リクエストHTTP 200で正常完了(約60秒)
大きいリクエストが小さいリクエストを締め出さない、Continuous Batchingの狙い通りの挙動を確認。
精度検証
基本問題(8問、enable_thinking=false): 8/8正解(算数3問・事実確認2問・指示追従2問・簡単な文章題1問)
難問(6問、enable_thinking=true):
- Needle-in-haystack (18,367トークン中に埋め込んだ秘密コード): ✅ 正確に抽出
- Needle-in-haystack (45,917トークン中に埋め込んだ秘密コード): ✅ 正確に抽出
- 論理パズル(3人の帽子の色): ✅ 正解
- 多段階計算(原価→定価→割引後価格): ✅ 正解(2通りの方法で検算していた)
- コード生成(重複除去関数): ✅ 正しい実装
- 要約(継続的バッチ処理の説明文): ✅ 自然で要点を押さえた要約
46,000トークン先の情報を正確に検索できた点は特筆に値する。ハイブリッドMamba/GDNアーキテクチャ(大半の層がlinear_attention、4層に1層だけfull_attention)でも、EXL3量子化+今回のパッチ込みで長距離検索が機能することの実証になった。
繰り返し(ループ)チェック: n-gram分析で3つの長文回答(734〜800トークン)を検査、繰り返しループは検出されず。max_tokens不足で打ち切られたケースも、単に丁寧な多角的検討の途中で切れただけで、退化的な繰り返しではなかった。
検証方法の反省点
論理パズルの正誤判定で、期待値を自分で間違えて設定した上に(正解は「緑」なのに「青」と誤記)、「期待文字列が回答全文に含まれるか」という単純な部分一致で判定していたため、モデルの思考過程中に別の色への言及があったことで偶然「一致」と誤判定していた。reasoningモデルの出力を自動採点する際は、思考過程を含む全文ではなく最終回答部分を明確に切り出して照合する必要がある。
プリフィルチャンクサイズ(--max-num-batched-tokens)の比較
Continuous Batchingではchunked prefillがデフォルトで有効(enable_chunked_prefill=True)。チャンクサイズによって「公平性」と「総スループット」がトレードオフになることを、1人が約54,000トークンの巨大リクエスト・他5人が短いリクエストという混在ワークロードで実測した。
┌──────────────────┬──────────────────────┬──────────────────────────┐
│ chunk size │ 完了時刻の幅(公平性) │ 全体完了(総スループット) │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 256 │ 約60.9秒(最悪) │ 72.1秒(最遅) │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 512 │ 約0.95秒 │ 59.2秒 │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 2048(デフォルト) │ 約5.48秒 │ 51.1秒 │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 4096 │ 約0.89秒(最良) │ 52.7秒(僅差2位) │
└──────────────────┴──────────────────────┴──────────────────────────┘
256まで小さくすると、公平性・速度の両方が悪化する「行き過ぎ」領域に入る(1ステップあたりのスケジューリングオーバーヘッドが支配的になるためと推測)。4096が本ワークロードでは最もバランスが良く、最終構成に採用した。
同時ユーザー数(--max-num-seqs)の調整
--max-num-seqsを6→10に増やしたところ、プロファイリング時の見積もりメモリが増えKV不足でクラッシュ(max-model-len=81920との両立に必要な予算が足りず、gpu-memory-utilizationを上げてもXwayland使用量の変動で不安定)。max-model-len=81920を維持したままmax-num-seqs=8まで妥協した結果、無事起動・動作を確認。
最終構成での再検証(N=1〜8スケーリング)
--tensor-parallel-size 2 --kv-cache-dtype fp8 --gpu-memory-utilization 0.88
--max-model-len 81920 --max-num-seqs 8 --max-num-batched-tokens 4096
--enable-auto-tool-choice --tool-call-parser qwen3_coder
┌────────┬─────────┬──────────────┐
│ 並列数 │ 合計t/s │ 1人あたりt/s │
├────────┼─────────┼──────────────┤
│ 1 │ 31.92 │ 31.92 │
├────────┼─────────┼──────────────┤
│ 2 │ 60.81 │ 30.41 │
├────────┼─────────┼──────────────┤
│ 3 │ 88.44 │ 29.48 │
├────────┼─────────┼──────────────┤
│ 4 │ 117.24 │ 29.31 │
├────────┼─────────┼──────────────┤
│ 5 │ 134.47 │ 26.89 │
├────────┼─────────┼──────────────┤
│ 6 │ 156.57 │ 26.10 │
├────────┼─────────┼──────────────┤
│ 7 │ 185.75 │ 26.54 │
├────────┼─────────┼──────────────┤
│ 8 │ 194.11 │ 24.26 │
└────────┴─────────┴──────────────┘
max-num-seqs引き上げ後、大小混在テストも再実施。完了時刻の幅が約0.41秒、全体完了49.79秒と、これまでで最良の結果(公平性・速度とも)を記録した。
tool-calling / プロキシの本番反映
- --enable-auto-tool-choice --tool-call-parser qwen3_coderを追加し、実際にfunction callingがfinish_reason: "tool_calls"で正しく動作することを確認。
- 既存のOpenWebUI向けプロキシ(8082=nothink, 8083=think)が、GSQ→OSCAR→Sonarと過去の移行を経てTARGET_MODELが古いパスに固定されたまま放置されており、Sonarへの切り替え後は実際には全リクエストが404していたことが判明。動的モデルID取得(get_model_real())を使うよう修正し復旧。
upstreamへの還元
発見した2つのバグはdphnAI/sonar(Aphrodite-engine)にPRとして提出済み:
- PR #1711 — EXL3 in_proj_qkv shard_id不一致の修正
- PR #1712 — LoRAデバイス判定にEXL3を追加
最終結論
- EXL3 + Qwen3.6ハイブリッドMamba/GDN + Aphrodite-engineの組み合わせは、2箇所のローカルパッチで動作した(upstreamには未報告のバグ、PR提出済み)
- 3.08bpw量子化 + CUDAグラフ有効 + チャンクサイズ/同時ユーザー数のチューニングで、精度を落とさずに実用的な速度を達成: 単発約32 t/s、8並列で合計約194 t/s
- 46Kトークンの長距離検索も正確、精度劣化なし
- tool-calling・OpenWebUI向けプロキシも含めて本番投入可能な状態まで到達
- 課題: LoRA有効時はメモリ予算がさらに厳しくなり、81920トークン級の大きいコンテキストとの両立は要調整。2×12GBという比較的小さいVRAM構成では、フレームワークオーバーヘッド(CUDA context/NCCL/コンパイルバッファ等、vLLM本家由来で約2.2GiB)の存在が効いてくる