Qwen3.5-35B-A3Bでctx-sizeを4096→65536にしたらVRAM 800MB増で速度も変わらなかった
前回の記事でQwen3.5-35B-A3Bをllama-server + Vulkanで動かすところまでやった。Q6_Kで53 t/s出ていて実用的な速度だったが、動作確認が目的だったのでctx-sizeはデフォルトの4096のままにしていた。
このモデルをTailscale経由でVPSからAPIとして叩く運用をしている。会話ログが丸ごと送られてくるので、ctx-size 4096では会話の途中でコンテキストが溢れて応答がおかしくなる。かといってコンテキスト長を伸ばすとKVキャッシュがVRAMを食って速度が落ちる——というのが一般的なTransformerモデルの話だが、Qwen3.5-35B-A3Bはそうではなかった。
環境
| 項目 | スペック |
|---|---|
| PC | GMKtec EVO-X2 |
| CPU | AMD Ryzen AI Max+ 395(Zen 5 / 16C 32T) |
| GPU | Radeon 8060S(RDNA 4 / gfx1151) |
| メモリ | 64GB統合メモリ(BIOS: VRAM 32GB / システム 32GB) |
| llama.cpp | b8183 Vulkan + --no-mmap |
| モデル | Qwen3.5-35B-A3B Q6_K(26.55 GiB) |
--no-mmap は前回の記事で検証した通り、統合メモリAPUでのmmap二重マッピング問題を回避するために必須。
Qwen3.5-35B-A3BのアーキテクチャがKVキャッシュに有利な理由
通常のTransformerモデルは全層がAttention機構を持ち、各層がKVキャッシュを消費する。コンテキスト長に比例してKVキャッシュが膨らみ、VRAMを圧迫する。
Qwen3.5-35B-A3Bは違う。SSM(State Space Model、Mamba系)とAttentionのハイブリッドアーキテクチャで、40層中30層がSSM、残り10層だけがフルAttentionだ。full_attention_interval = 4 なので4層に1回だけAttentionが入る構造になっている。
graph TD
A["Layer 1-3: SSM"] --> B["Layer 4: Attention"]
B --> C["Layer 5-7: SSM"]
C --> D["Layer 8: Attention"]
D --> E["..."]
E --> F["Layer 37-39: SSM"]
F --> G["Layer 40: Attention"]
style B fill:#f96,stroke:#333
style D fill:#f96,stroke:#333
style G fill:#f96,stroke:#333
KVキャッシュはAttention層だけが消費する
SSM層はrecurrent stateを持つが、これはコンテキスト長に依存しない固定サイズ(251 MiB)のバッファだ。コンテキストが4Kでも256Kでも、SSM層のメモリ消費は変わらない。
KVキャッシュを消費するのは10層のAttention層だけ。通常の40層全Attentionモデルと比較すると、KVキャッシュのメモリ消費は単純計算で1/4になる。
| バッファ | サイズ | ctx-size依存 |
|---|---|---|
| KVキャッシュ(10 Attention層) | ctx-sizeに比例 | あり |
| RSバッファ(30 SSM層のrecurrent state) | 251 MiB固定 | なし |
RSバッファとは
RSバッファ(Recurrent State Buffer)はSSM層の内部状態を保持するメモリ領域だ。Attention層が入力トークン全体のKey/Valueを保持するのに対し、SSM層は固定サイズの状態ベクトルに過去の情報を圧縮して保持する。
SSMは入力シーケンスを1トークンずつ処理しながら状態ベクトルを更新していく。状態ベクトルのサイズはモデル設計時に決まる(Qwen3.5-35B-A3Bの場合、d_state=128、d_inner=4096)。コンテキストが長くなっても状態ベクトルのサイズは変わらず、古い情報は新しい情報で上書きされていく。これがKVキャッシュと根本的に異なる点で、SSMが長コンテキストで有利な理由の一つだ。
RSバッファのサイズは n_parallel(同時処理スロット数)に依存する。スロットが増えれば各スロット分の状態ベクトルが必要だが、ctx-sizeをいくら増やしても変わらない。
ベンチマーク
4 parallel slotsでctx-sizeを変えて計測した。
| ctx-size | KVキャッシュ | VRAM合計 | 生成速度 |
|---|---|---|---|
| 4,096 | 80 MiB | 27.6 GB | 53.7 t/s |
| 32,768 | 640 MiB | 28.2 GB | 49.9 t/s |
| 65,536(q8_0 KV) | ~870 MiB(推定) | ~28.9 GB | 53.6 t/s |
ctx-sizeを4Kから32Kに8倍にしても、KVキャッシュ増加はわずか560 MiB。速度低下も53.7→49.9 t/sと軽微だ。通常のTransformerモデルでこんな増やし方をしたら、KVキャッシュだけで数GB増えて速度も大幅に落ちる。
65Kではq8_0 KV量子化を使って870 MiB程度に抑えつつ、53.6 t/sと4K時とほぼ同等の速度を維持した。
メモリ上限の見積もり
BIOS設定が32GB/32GBでの実測値。
| 区分 | 状況 |
|---|---|
| システムRAM | 約13GB使用中(Parsec + Tailscale + OS)→ 残り19GB |
| VRAM | モデル + ctx + compute で約29GB / 32GB → 残り約3GB |
Vulkan0のドライバ報告値は free: 46,522 MiB と出るが、これは統合メモリの物理的な空き全体を反映しており、BIOS設定によるVRAM/システムの論理的な境界は考慮されていない。実効的な空きは上記の通りもっと少ない。
KVキャッシュの見積もり
f16(デフォルト)、4 parallel slotsでのKVキャッシュサイズ見積もり。
| ctx-size | KVキャッシュ(f16) | KVキャッシュ(q8_0) |
|---|---|---|
| 32,768 | 640 MiB | ~320 MiB |
| 65,536 | 1,280 MiB | ~640 MiB |
| 131,072 | 2,560 MiB | ~1,280 MiB |
| 262,144 | 5,120 MiB | ~2,560 MiB |
モデルの n_ctx_train = 262,144 なので、モデル自体は256Kトークンまで対応している。131Kでも2.5 GB(f16)しかKVキャッシュを使わない。VRAM的には262Kでも余裕がある計算だ。
10層しかAttentionがないSSM+Attentionハイブリッドの恩恵がここに効いている。通常の40層フルAttentionモデルなら、262Kで20GB以上のKVキャッシュが必要になる。
KVキャッシュ量子化
llama-serverにはKVキャッシュのKey/Valueそれぞれを量子化するオプションがある。
--cache-type-k q8_0 # Keyをq8_0に量子化
--cache-type-v q8_0 # Valueをq8_0に量子化
q8_0はfp16の半分のサイズになる。ctx=65536でq8_0 KVを使用した結果。
| 指標 | f16 KV(ctx=32K) | q8_0 KV(ctx=65K) |
|---|---|---|
| KVキャッシュ | 640 MiB | ~870 MiB |
| 生成速度 | 49.9 t/s | 53.6 t/s |
コンテキスト長を倍にしたのにKVキャッシュの増加は230 MiB程度。速度はむしろ上がっている。q8_0量子化による品質低下は体感できなかった。
KVキャッシュ量子化の品質影響はモデルのアーキテクチャとタスクに依存する。一般に、長文の要約や数学的推論のようなKVキャッシュの精度に敏感なタスクでは影響が出やすい。会話応答のような用途ではq8_0で実用上の問題はない。
TurboQuantとの関連
TurboQuantの記事で詳しく書いたが、Google Research発のTurboQuant(ICLR 2026)はKVキャッシュを3-4ビットに圧縮する技術だ。q8_0よりさらに半分以下のサイズになる。
バックエンド別の実装状況
| バックエンド | 状況 |
|---|---|
| Metal | 動作中(tq3/tq4) |
| CUDA | 動作中(q8_0の98.8%の速度) |
| CPU | 動作中(速度ペナルティゼロ) |
| Vulkan | 初期プロトタイプ段階 |
| upstream llama.cpp | 未マージ |
Radeon 8060S(Vulkan)ではTurboQuantはまだ使えない。Vulkan実装が初期段階で、llama.cpp本体にもマージされていない。llama.cpp Discussion #20969で開発が進行中だ。
QJL vs WHTの検証(Discussion #20969)
同じDiscussion #20969で、3つの独立した実装がTurboQuantのアルゴリズムを再現検証している。scos-lab(Python実装、8モデル検証)、Arclabs001(YATQ、PyTorch実装)、TheTom(turboquant_plus、Metal実装)がそれぞれ「QJL(Quantized Johnson-Lindenstrauss)は実用で逆効果、MSE-only + WHT(Walsh-Hadamard Transform)が最良」と報告した。scos-labのGPT-2(head_dim=64)での3ビット量子化検証では、MSE-onlyのPerplexity悪化が+7.6%なのに対し、論文通りのQJL付きだと+300%に跳ね上がった。QJLはバイアスを除去する代わりに分散を増大させ、softmaxがその分散を増幅するのが原因だ。
ただしこの結論は3月30日に修正されている。Arclabs001とAmesianXが、初期実装者のランダム直交行列(QR分解)をWHT(Walsh-Hadamard Transform)に置き換え、QJLのSRHTと独立な符号パターンを組み合わせると結果が逆転することを示した。Qwen3.5(head_dim=256)でWHT+QJLのPPLは6.54(f16ベースライン6.39の+2.3%)に対し、QJLなしのMSE-onlyだと7.54(+18%)。一方head_dim=128ではQJLを入れると+65%悪化し、初期報告通り逆効果になる。
結論はhead_dimに依存する。256以上ならWHT+QJL、128以下ならMSE-only + WHTが最良。TurboQuantの論文がWHTベースの手法を採用しているのは正しい方向で、初期実装者がランダム直交行列を使っていたのがそもそもの性能低下の原因だった。
この環境にとっての意味
現状q8_0 KVでctx=65Kが53.6 t/sで動いており、実用上の不足はない。TurboQuant(3-4bit KV)が使えるようになればKVキャッシュメモリがさらに半分以下になり、131Kや262Kのコンテキスト長でもVRAMに余裕が生まれる。
ただし、Qwen3.5-35B-A3BのKVキャッシュはそもそも小さい(10 Attention層分しかない)ので、TurboQuantの恩恵は通常のフルAttentionモデルほど劇的ではない。262Kでもf16 KVで5.1 GBにしかならないため、q8_0で2.5 GBに収められれば当面は十分だ。
運用構成
最終的にctx-size 65536、q8_0 KV量子化で運用している。
llama-server.exe \
-m "Qwen3.5-35B-A3B-Q6_K.gguf" \
--port 8080 \
--ctx-size 65536 \
--cache-type-k q8_0 \
--cache-type-v q8_0 \
--reasoning-budget 0 \
--n-gpu-layers 99 \
--no-mmap
VRAM 29GB程度で収まり、53 t/sを維持。会話ログ付きのAPI呼び出しでもコンテキストが溢れなくなった。