以前から、EVO-X2のメモリ128GBを大きなモデルを動かすのではなく、モデルをできるだけ小さくしてコンテキスト長に全振りしたらどうなるのかが気になっていた。
私の使い方が悪いだけかもしれないけど、「長文から特定の内容を検索する」みたいなときにRAGだとうまく行かなくて、長文をフルコンテキストでモデルに読み込ませる方がうまく行くことが多い。そんな使い方をしているから、すぐにコンテキストオーバーでエラーになったりする。
EVO-X2 × Qwen3.8-Flash-Next 番外編2 で、小さいモデルのQwen3.5-2B UD-Q4_K_XLを使い、コンテキスト長1Mまでの速度とメモリ使用量を計測した。2Mにも挑戦したが、Vulkanのpinned memory allocation警告が繰り返し出て、極端に低速化したので途中で停止した。
今回はその原因を調査して回避し、設定コンテキスト4M(4,194,304 tokens)、実際の入力3,774,852 tokensまで伸ばしてみた。速度とメモリ使用量だけでなく、長い入力の中から簡単な情報を取り出せるかも調べてみた。
結論
- Qwen3.5-2B UD-Q4_K_XLで、コンテキスト長設定4M、実入力3,774,852 トークンの読み込みと回答生成ができた
- 読み込み、生成ができるのと、生成内容が正しいかは別問題で、今回の内容テストではコンテキスト1Mまでは合格、1.5Mから回答形式に異常、2Mと4Mでは不合格となった
- コンテキスト4Mの処理時間は16時間54分。PPは62.05 tok/s、TGは8.27 tok/sまで低下した
- コンテキスト4Mでもメモリには余裕があった。さらに伸ばすのをやめた主な理由は、処理時間・回答品質と、Vulkanの単一バッファ/単一メモリ割当の約2GiBの制約だった
検証環境
| 項目 | 内容 |
|---|---|
| PC | GMKtec NucBox EVO-X2 |
| APU / GPU | AMD Ryzen AI Max+ 395 / Radeon 8060S(gfx1151) |
| RAM | 128GB UMA |
| UMA Frame Buffer | 96GB |
| OS | Windows 11 Pro 25H2、OS Build 26200.9168 |
| モデル |
Unsloth Qwen3.5-2B-GGUF、UD-Q4_K_XL
|
| モデル容量 | llama.cpp表示で1.24GiB、パラメータ数約1.88B |
| モデルの標準コンテキスト | 262,144 tokens |
| 推論 | llama.cpp Vulkan(後述の2GiB guard対策の改造版) |
| Vulkan SDK / Compiler | 1.4.357.0 / LLVM・Clang 20.1.8 |
| 主な設定 | GPUオフロード、Flash Attention有効、MTPなし、-b 2048、temperature 0 |
今回はVulkanでの実験であり、ROCmで同じ4Mの結果が得られるかは検証していない。
YaRNとは
通常、モデルには学習・設計上想定されたコンテキスト長がある。今回のQwen3.5-2Bは262,144 tokensが標準の長さ。これを超えるために使ったのが、YaRN(Yet another RoPE extensioN) というRoPEの拡張方式だ。
RoPE(Rotary Position Embedding)は、Attentionでトークンの位置関係を扱うための仕組み。YaRNはその位置情報の扱いを調整し、元の長さより長い系列を扱えるようにする。今回のテストでは追加学習ではなく、llama-cli.exeの推論時設定で拡張した。
| 設定コンテキスト | 元の262,144 tokensからの拡張倍率 |
|---|---|
| 32K / 262K | 拡張なし |
| 512K | YaRN ×2 |
| 1M | YaRN ×4 |
| 1.5M | YaRN ×6 |
| 2M | YaRN ×8 |
| 4M | YaRN ×16 |
ただし、入力可能な長さを設定できることと、その長さでの回答品質は別問題。位置情報を拡張しただけで、モデルの検索能力や指示追従能力が保証されるわけではない。
速度比較結果
各コンテキスト長の速度計測結果はこうなった。入力は後述の単純検索テストを使った。
以下、Prompt Processing(入力を読み込む処理)をPP、Token Generation(回答を生成する処理)をTGと表記する。速度の単位はどちらもtok/sで、高いほど速い。経過時間は実行開始から終了までの実時間(wall time)。
| 設定ctx | 実入力tokens | KV | -ub |
経過時間 | PP(tok/s) | TG(tok/s) |
|---|---|---|---|---|---|---|
| 32K | 29,488 | q8_0 | 512 | 14秒 | 2,765.94 | 109.87 |
| 262K | 235,925 | q8_0 | 512 | 4分59秒 | 798.83 | 54.39 |
| 262K | 235,925 | q4_0 | 512 | 5分20秒 | 743.98 | 66.60 |
| 512K | 471,860 | q4_0 | 512 | 19分06秒 | 413.15 | 44.35 |
| 1M | 943,681 | q4_0 | 512 | 1時間11分 | 221.67 | 27.56 |
| 1.5M | 1,415,551 | q4_0 | 512 | 2時間37分 | 150.43 | 20.34 |
| 2M | 1,887,421 | q4_0 | 512 | 4時間36分 | 114.16 | 15.87 |
| 4M | 3,774,852 | q4_0 | 256 | 16時間54分 | 62.05 | 8.27 |
モデルが小さいので、32KではPPもTGもQwen3.8-Flash-Nextの計測では見たことがないような高速な数字が出ているが、1Mぐらいから遅くなり、4MではPPは2桁、TGは1桁まで落ちて、約17時間かかった。
これは、長いKVキャッシュを参照する処理が増えることなどによるもので、モデル本体が1.24GiBと小さくても長文処理が軽いわけではない。
なお、途中でKVキャッシュの量子化をq8_0からq4_0に変更し、4Mだけubatchを512から256に変更している(理由は後述)。したがって、この表の速度差を全部「コンテキスト長だけの影響」とは言えない。
夜間にバッチ実行するにしても、所要時間の許容範囲は10時間ぐらいなので、17時間だと現状では実用性はゼロ。仮にここを高速化できても、次の回答品質の問題が残る。
単純検索テストの結果
速度だけ測っても、「長文を読み込んだ後にまともに回答できるのか」が分からない。そこで、長い入力の中に5個の目印を埋め込み、最後にその目印に対応するコードを尋ねる問題を作成した。
具体的には次のような問題で、複雑な推論は要求していない。
入力の序盤: <<< MEMORY TARGET ALPHA >>> / CODE=MEM-422777
入力の終盤: <<< MEMORY TARGET ECHO >>> / CODE=MEM-509065
入力の末尾: <<< LOCAL CONTROL >>> / CODE=CTL-445570
Q1:MEMORY TARGET ALPHA のCODEを答える → Q1=MEM-422777
(中略:Q2〜Q5)
Q6:LOCAL CONTROL のCODEを答える → Q6=CTL-445570
5個の目印は入力本文のおよそ5%、25%、50%、75%、95%の位置に置いた。Q6は質問の直前付近にある情報だけを探せば正解できるlocal controlとする。
Q1〜Q5は「設問番号」と「完全なCODE」が一致していることを正解条件にした。例えば、正しいコードが出力中に登場しても、別のQ番号に紐付いていたら不正解とする。
| 設定ctx(KV) | Q1〜Q5採点結果 | 正しい完全CODEの登場数 | Q6 | 観察されたこと |
|---|---|---|---|---|
| 32K(q8_0) | 5/5 | 5/5 | PASS | 完全正解 |
| 262K(q8_0) | 5/5 | 5/5 | PASS | 完全正解 |
| 262K(q4_0) | 5/5 | 5/5 | PASS | KVをq4_0にしても完全正解 |
| 512K(q4_0) | 5/5 | 5/5 | PASS | YaRN ×2でも完全正解 |
| 1M(q4_0) | 5/5 | 5/5 | PASS | YaRN ×4でも完全正解 |
| 1.5M(q4_0) | 5/5 | 5/5 | FAIL | Q6の数値は合うが接頭辞を誤る |
| 2M(q4_0) | 0/5 | 5/5 | FAIL | コードは出るが設問との対応・形式が崩壊 |
| 4M(q4_0) | 0/5 | 0/5 | FAIL | 桁欠けした同一コードを6問に反復 |
単に正答率が徐々に落ちるのではなく、壊れ方が段階によって違った。
1.5M:検索自体はできているが、分類を間違える
Q1〜Q5はすべて正解。一方、末尾付近のQ6で、正解のCTL-445570がMEM-445570になった。数字は合っているのに、接頭辞だけ違う。
local controlの方が質問と正解が書いてある位置が近いので簡単なのかと思ったけど、そうでもないらしい。
2M:コードを拾っているのに、質問との対応付けが崩れる
回答中に正しい完全CODEが5種類とも存在した。ただしQ1=...のような設問番号がなくなり、順序や形式も崩れたので、採点結果は0/5。Q6も不合格だった。
これは「2Mでは該当箇所をまったく参照できない」とは少し違う。少なくとも正しい情報の断片は出力に現れているが、それを指定どおりに対応付けて回答できていない。
4M:全問に対して、同じ、しかも不完全なコードを繰り返す
4Mでは、Q1の正解MEM-422777から最後の1桁が欠けたMEM-42277を、Q1〜Q6に繰り返し出力した。正しい完全CODEは0種類で、Q6も不合格だった。
Q1=MEM-42277
Q2=MEM-42277
...
Q6=MEM-42277
こちらは2Mより明らかに崩れ方が大きい。ただし、この出力だけから、モデル内部で情報が完全に失われたのか、読み出しや指示追従に失敗したのかまでは分からない。
今回のテストでは、全問合格できるのは1Mまでたった。 ただし、これはQwen3.5-2Bの一般的・絶対的な「実用上限」ではない。モデルの検索能力、入力データの作り方、質問順、YaRN倍率などが影響するからだ。
試しに同じ内容で質問の順番だけ逆順にして試してみると、32Kと512Kでも採点結果が4/5になる場合があった。32Kでも全問正解できない場合があるので、単純に長コンテキスト化だけのせいとは言えない。
メモリ使用量比較結果
| 設定ctx(KV) | Vulkan context割当(MiB) | 推論プロセスのGPUピーク(GiB) | Windows全体のCommitピーク(GiB) | 最小空きRAM(GiB) |
|---|---|---|---|---|
| 32K(q8_0) | 223 | 2.15 | 11.87 | 23.94 |
| 262K(q8_0) | 1,651 | 4.41 | 14.19 | 23.45 |
| 262K(q4_0) | 883 | 3.15 | 12.88 | 23.49 |
| 512K(q4_0) | 1,747 | 4.53 | 14.27 | 23.26 |
| 1M(q4_0) | 3,475 | 7.20 | 17.80 | 22.01 |
| 1.5M(q4_0) | 5,203 | 9.89 | 20.09 | 21.97 |
| 2M(q4_0) | 6,931 | 10.58 | 23.14 | 21.77 |
| 4M(q4_0) | 13,843 | 17.30 | 31.36 | 18.14 |
-
Vulkan context割当:llama.cppのメモリ内訳で
contextに分類された容量。モデル本体やすべての一時バッファの合計ではない -
GPUピーク:自作監視スクリプトで取得した
TargetGPUTotalCommitted_GiB。推論プロセスについて、WindowsのGPU関連カウンターから取得した値 - Commitピーク:Windowsシステム全体のコミット量。GPUピークと足して128GB中の使用量を計算するものではない
- 最小空きRAM:Windows側で観測した空きRAMの最小値。UMA 96GB設定のためWindows CPU側に見える容量が約32GBの構成での値
4Mのとき、Vulkan context割当は13,843MiB(約13.5GiB)、プロセスのGPUピークは17.30GiB、最小空きRAMは18.14GiBだった。少なくとも今回の4M完走時点では、総メモリ容量を使い切ってはいない。
当初の予定では128GBメモリの限界までコンテキスト長を伸ばしてみようと思っていたのだけど、時間がかかりすぎるし、出力品質にも問題があり、Vulkanの単一バッファ2GiB制限の回避方法を考える気がしなくなったので、メモリに余裕はあるけど4Mで終了とした。
コンテキスト1Mを越えたあたりの急激な低速化の原因
前回は コンテキスト長2Mに設定して、1Mあたりで急激に低速化して途中で停止した。
今回も最初に同条件で試したときは同じ現象が再現した。
具体的には、以下の警告が多発して、
ggml_vulkan: Failed to allocate pinned memory (Requested buffer size exceeds device buffer size limit: ErrorOutOfDeviceMemory)
1,048,576→1,067,008の18,432 tokensを処理するだけで約4時間40分かかっており、この区間は約1.1tok/s になった。このまま続けてもいつ終わるか分からないので、いったん停止して原因を調べてみた。
調べてみると、Flash Attention向けのF16 KQ maskが2GiB級になり、graphの分割・フォールバックやpinned-memory警告が発生して、極端な低速化の原因になっているらしい。
Attentionでは、Query(Q)がどのKey(K)を参照できるかを制御するためにKQ maskというマスクを使う。たとえば、未来のトークンを見ないようにする制限やパディングを表す。今回のFlash Attention経路では、このマスクのF16作業領域が、処理中のマイクロバッチサイズ(-ub)と参照するKV長の増大に伴って巨大化していた。
そこで、-ub 1024から-ub 512に下げるとマスクが小さくなり、極端な速度低下は起こらなくなった、と思ったら、その直後に別のエラーで停止した。
ggml_vulkan: Device memory allocation of size 2148532224 failed.
ggml_vulkan: Requested buffer size exceeds device buffer size limit: ErrorOutOfDeviceMemory
ログ上では、入力は約1,048,576 tokensまで進んで、その後でdecode() failedになっていた。
Vulkanの「単一バッファ2GiB制限」とは?
最初の極度な低速化、2回目のErrorOutOfDeviceMemoryの両方とも、2GiBぐらいのメモリ割り当てで引っかかっている。
システム全体ではRAMもVRAMも余裕があるのに、何で引っかかるのかを調べてみたら、Vulkanのメモリ割り当てに制約があった。
Vulkanでは、デバイスごとにmaxBufferSizeやmaxMemoryAllocationSizeなどの制約がある。後者はヒープ全体に空きがあっても、1回のメモリ割当サイズに上限があることを示す。数値は実装・ドライバーによって異なるため、「Vulkanの仕様はすべて2GiBまで」という意味ではないが、EVO-X2の私の環境では2GiBの制約があった。
ErrorOutOfDeviceMemoryの原因を調べてみると、使っていたllama.cppでは量子化したKVキャッシュをF16へ展開する最適化が入っていて、これで、KとVの合計を一度に扱う一時領域が2,148,532,224 bytes(2,049MiB)必要となった。これは2GiBより1MiB大きい。
※KVキャッシュは、過去に処理したトークンのKeyとValueを保存しておく領域。入力が長くなると増大する。
そこで、K+V一時領域の合計が上限を超える場合にはF16展開の最適化を使わず、量子化KVを扱うFlash Attention経路に切り替えるようにllama.cppを改造した。
なんか、こういうときにllama.cppを改造するという発想がサラッと出てくるのは沼の深みにハマっているのではないかと心配になるけど、実際に実装するのはチャッピー様だし、まあいいか。
コンテキストを4Mまで伸ばす
ここまでの -ub 1024 -> 512 と、KVキャッシュのF16最適化回避で、コンテキスト2Mまでは動くようになったけど、4Mまで伸ばそうと思うと、また2GiB制限に引っかかる可能性が高い。
なので、KVキャッシュの量子化をQ8_0からQ4_0に変えて、さらに -ub 256まで減らした。つまり、2GiB制限を越えられるようにしたのではなく、2GbiB制限に引っかからないようにパラメータを調整した。
2Mの最初の途中で極端に低速化したときと、ub変更+llama.cpp改造で正常に動作したとき、4Mで正常に動作したときの16,384トークンごとのPP速度をグラフ化してみた(チャッピー様にグラフ化してもらった)
このグラフは、stderr.logのcached n_tokensと経過時刻から、16,384トークンごとのPP速度を計算して重ねたもの。
赤が低速化して中断した2M、青が修正後に完走した2M、緑が後で完走した4M。約1Mの点線は低速化が始まった付近。赤が急落しているのは手動停止して止まったからではなく、止める前の速度がここまで低下している。
横軸は設定ctxではなく実際に処理済みのトークン数、縦軸は大きな速度差を表示するため対数目盛りにしている。
※4MはKV q4_0・ub256、2MはKV q8_0であり、入力のテスト問題も異なるため、単純な性能比較はできない
まとめ
小さなモデルを使えば、128GBのEVO-X2でも4Mのコンテキストが処理できることは分かった。約377万tokensを16時間54分かけて読み込み、回答生成まで到達した。
一方、単純検索問題でも、1.5Mでは識別子の取り違え、2Mでは対応付けと形式の崩壊、4Mでは不完全なコードの反復という違いが現れた。少なくとも今回の条件で、「メモリに入るから実用的に使える」とは言えない。
今回、実験を終わらせたのはメモリ残量ではなく、所要時間、検索品質、そしてVulkanの単一バッファ2GiB制約への対応負担だった。4Mより先の容量限界については未測定なので、最大何Mまで動くかは不明のまま。
メモリには余裕があったので、もう少し大き目のモデルで試せばもう少し性能が出るかもしれないけど、Vulkanの単一バッファ2GiB制約に引っかかるのは変わらない。
やっぱり無理矢理コンテキストを伸ばすよりは、RAGをもっと工夫して、ちゃんと使えるようにした方が良いのか??
付録
起動オプションの例
# 4Mの主要設定例
& $exe -m $model -f $prompt `
-c 4194304 -ngl 999 -n 128 -t 4 `
-b 2048 -ub 256 -fa 1 `
-ctk q4_0 -ctv q4_0 --temp 0 `
--override-kv qwen35.context_length=int:4194304 `
--rope-scaling yarn --rope-scale 16 --yarn-orig-ctx 262144
ここで-cはモデルに確保するコンテキスト枠、--rope-scale 16は元の262,144に対するYaRNの倍率。--override-kvも併用して、モデル側のコンテキスト長メタデータを拡張している。
単純検索テストの作り方
長い文章と質問からなる入力を用意する際、普通の文章を大量に並べると、質問そのものが難しいのか、長すぎることで失敗したのか分かりにくい。そこで、機械的に生成した背景レコードと、明確な正解コードを持つ目印を使った。
- 設定コンテキストの約90%になるよう背景レコードを大量に生成する
- 全体の約5%、25%、50%、75%、95%に、それぞれ一意な
MEMORY TARGETレコードとCODE=MEM-xxxxxxを配置する - 末尾に
LOCAL CONTROLとCODE=CTL-xxxxxxを置く - 最後の質問Q1〜Q5で5か所の
MEMORY TARGETに対応するCODEを尋ね、Q6でLOCAL CONTROLを尋ねる - temperature 0、通常の質問順、固定seedで実行し、設問IDとCODEの完全一致を採点する
Q6は、遠距離検索をしなくても取得できるはずの情報を探す対照問題。ここを誤ると、長距離検索の成績を解釈しづらくなるのでFAILの場合は全体をINVALIDとして扱う
実は、もう少し難しい問題として、正解と似たダミー候補(distractor)を置いて複数条件で選ばせる案も試した。しかし、32Kの時点でも問題の作り方によって失敗が発生したため、「長さによる変化」の比較には、まず単純なテストを使うことにした。
実行環境
- CPU / APU:Ryzen AI Max+ 395(Windows表示 3.00GHz)
- GPU:AMD Radeon 8060S Graphics(
gfx1151) - RAM:128GB UMA、Frame Buffer 96GB、Windows CPU側に見えるRAM約32GB
- OS:Windows 11 Pro 25H2、Build 26200.9168、Windows Feature Experience Pack 1000.26100.344.0
- Model:
Qwen3.5-2B-UD-Q4_K_XL.gguf、1.24GiB、約1.88B parameters、標準ctx 262,144 - llama.cpp:upstream
bed0a856606ee4a24a164066f73d2379447033f5の改造版 - Backend:Vulkan、SDK 1.4.357.0、LLVM / Clang 20.1.8
