tk3.biz
ブログ一覧に戻る Back to Blog

使っていない画像機能を外したら、メモリは1GiB減り、速度は変わらなかった Dropping an Unused Image Feature Saved 1 GiB of Memory and Didn't Change Speed

はじめに

プロンプトキャッシュの回で、サーバの起動ログにこの行を見つけました。

srv    load_model: cache_reuse is not supported by multimodal, it will be disabled

使っている Qwen3.6-35B-A3B は画像も扱えるモデルで、 -hf で指定すると画像用の mmproj(約 0.9 GB のファイル)が自動で読み込まれます。

ところが、llm コマンドも dev-orchestra のレビューも、テキストしか使っていません。 使わない機能のために、毎回ファイルを読んでメモリを使っていたことになります。

外したら何が変わるのか。候補は 3 つありました。

  • メモリが減る(ほぼ確実)
  • 起動が速くなる(読むファイルが 1 つ減る)
  • 読み込み・生成が速くなる(llama-bench と実サーバの速度差、約 2 割の原因かもしれない)

3 つ目が当たれば、毎日の llm がそのまま速くなります。

チェックリストから設計する

前回、測定で 19 回間違えた経験をチェックリストにまとめました。 今回はそれを最初から当てはめています。

チェック項目今回の対応
設定が本当に有効か起動ログに mmproj の読み込み行が出ていないことを確かめ、想定と違えば止まる
比べ方あり → なし → あり → なし → あり と交互に 5 回。最初と最後の「あり」が揃うかでずれを見る
本番と同じ条件serve.ps1 と同じ引数(-c 65536、MTP あり)。違いは --no-mmproj だけ
割り込みllama 以外が使った CPU・GPU を 10 秒ごとに記録
安全装置スリープ抑止が効かなければ止まる。サーバが起動しなければ止まる
生データ生成した文章の冒頭を記録し、両条件でまともな出力かを見る

1 つ目は --cache-reuse の件の反省です。 「入れた設定が効いているはず」で 4 日間間違えていたので、効いていなければ計測自体を止めるようにしました。

測ったのは、起動時間、メモリ、32K トークンの読み込み時間、生成速度(256 トークン × 3 回)です。

1 回目の結果

#mmproj起動メモリ(起動後)生成(平均)32K の読み込み
1あり9.7 秒25.11 GiB18.00 t/s140.4 秒
2なし8.3 秒24.12 GiB19.13 t/s133.6 秒
3あり9.4 秒25.21 GiB18.42 t/s144.9 秒
4なし9.6 秒24.12 GiB17.25 t/s134.9 秒
5あり10.1 秒25.21 GiB19.22 t/s125.5 秒

メモリは、はっきり 1 GiB 減った

「あり」は 25.11〜25.21 GiB、「なし」は 2 回とも 24.12 GiB。差は約 1.0 GiB です。 5 回の中で値がほとんど動かず、両者の範囲は重なりません。これは言い切れます。

速度は、何も言えなかった

一方で速度は、表を見ても方向が定まりません。 生成は「なし」の 1 回目が速く、2 回目が遅い。読み込みは「なし」が真ん中あたりに来ています。

決め手は、最初と最後の「あり」が揃っていないことでした。

#1(あり)#5(あり)ずれ
32K の読み込み140.4 秒125.5 秒−11%
生成18.00 t/s19.22 t/s+7%

同じ条件なのに、計測の始めと終わりで 1 割動いています。 「あり」どうしのばらつき(読み込み 125〜145 秒)の中に、「なし」の 134 秒がすっぽり収まっています。

条件の差より、計測中のずれのほうが大きい。 この状態で「速くなった」とも「変わらない」とも言えません。 挟み込みで測ったおかげで、それが分かったとも言えます。 1 回ずつの比較だったら、#4 と #5 を並べて「mmproj ありのほうが速い」と書いていたかもしれません。

監視に引っかかったもの

監視ログを見ると、16:14〜16:20 に llama 以外の CPU が 13〜22% まで上がっていました。 上位は python3.13 と StreamDeck です。python3.13 は計測スクリプトではありません。

調べると、この PC の Python は Microsoft Store 版の 3.13 で、プロセス名が python3.13 と見えます。 そして同じ時間帯に、別の Claude のセッションが dev-orchestra で作業していました。 dev-orchestra はこの Python で動くツールです。裏で動いていたのは、それだと見ています (別セッションのログまでは確かめていないので、状況からの判断です)。

この区間は #3 の終盤と #4 にあたります。#4 の生成(17.25 t/s)が低いのは、その影響かもしれません。 ただ、重なっていない #1 と #5 のあいだでも 1 割ずれているので、ずれの原因がこれだけとは言えません。

裏を止めて、測り直した

チェックリストの「割り込み」に**「別の Claude のセッションが動いていないか確かめる」**を足し、測り直しました。

測り始める前に確かめると、dev-orchestra のセッションの最終操作が 40 秒前でした。 まさに使われていたわけです。そのセッションを止めてもらい、python のプロセスが無いことを確かめてから始めました。

回数も増やしています。あり → なし を交互に 7 回(あり 4 回、なし 3 回)。 こうすると「なし」の各回を、前後の「あり」の平均と比べられます。 計測中にゆっくりずれていっても、前後で挟めば打ち消せます。

mmprojありとなしの比較を1回目と2回目で並べたもの。メモリはどちらの回も約1 GiB離れて重ならない。生成速度と32Kの読み込み時間は、1回目は同じ「あり」どうしで1割ばらついたが、2回目は2%前後に収まり、その中で「なし」と「あり」が重なった

#mmproj起動メモリ(起動後)生成(平均)32K の読み込み
1あり25.0 秒 ※25.06 GiB18.91 t/s133.2 秒
2なし8.3 秒23.97 GiB19.39 t/s136.6 秒
3あり8.0 秒25.06 GiB18.87 t/s136.0 秒
4なし7.1 秒23.97 GiB18.77 t/s134.7 秒
5あり8.1 秒25.06 GiB18.91 t/s136.8 秒
6なし7.3 秒23.97 GiB18.85 t/s136.9 秒
7あり7.8 秒25.06 GiB18.86 t/s136.3 秒

※ #1 は、直前まで動いていたサーバを止めた直後の起動です。

監視では、llama 以外の CPU は中央値 7.1% で、1 回目のような連続した山はありませんでした。

ずれが、1 割から 2% 前後まで縮みました。

生成32K の読み込み
最初と最後の「あり」(#1 と #7)−0.2%+2.3%
#2(なし)と前後の「あり」+2.6%+1.5%
#4(なし)と前後の「あり」−0.6%−1.2%
#6(なし)と前後の「あり」−0.2%+0.3%

「なし」と前後の「あり」の差は、向きが回ごとにばらばらで、大きさは 3% 以内です。 mmproj を外しても速度は変わらない、と言ってよさそうです。 正確には、差があったとしても 3% 未満です。

メモリは今回も、7 回とも同じ値でした(あり 25.06 GiB、なし 23.97 GiB、差 1.09 GiB)。

1 回目の 1 割のずれが、すべて dev-orchestra のせいだったのかは分かりません。 重なっていない #1 と #5 のあいだでもずれていたからです。 ただ、裏を静かにしただけで、同じスクリプトのばらつきが 5 分の 1 になったのは確かです。

出力は一字一句同じ

2 回の計測の計 12 回とも、生成した文章は冒頭から同じでした。

ローカルLLMをiGPU(統合グラフィックス)で動作させる最大の利点は、専用メモリ(VRAM)を必要とせず、…

mmproj は画像を読むための部品なので、テキストだけなら影響しないのは予想どおりです。 ただ、予想どおりかどうかも確かめておくのが、今回のやり方です。

期待していた 2 つは外れた

--cache-reuse は、外しても使えませんでした。

srv    load_model: cache_reuse is not supported by this context, it will be disabled

理由が「マルチモーダルだから」から「このコンテキストでは非対応」に変わるだけです。 このモデルは SSM とのハイブリッド構成で、KV をずらして使い回す仕組みとは両立しません。 前回確かめたとおりでした。

llama-bench と実サーバの速度差も、mmproj では説明できませんでした。 読み込みが 2 割速くなれば当たりでしたが、測り直した結果は 3% 未満の差です。原因は未解明のままです。

既定を変えた

既定は mmproj なしにしました。

ありなし
メモリ約 25.1 GiB約 24.0 GiB
起動約 8 秒約 7〜8 秒
テキストの出力—同じ
画像の入力使える使えない

失うのは画像の入力だけで、今は使っていません。 93.6 GiB 積んだ機体で 1 GiB は小さな差です。それでも、使わない機能のためにメモリを使い続ける理由もありません。

serve.ps1 には -Vision を足し、画像が必要なときだけ読み込めるようにしました。

# 普段(llm の自動起動もこちら)
pwsh -File scripts\serve.ps1            # --no-mmproj が付く

# 画像を使うとき
llm -Stop
pwsh -File scripts\serve.ps1 -Vision    # mmproj を読み込む

変更後に llm から自動起動させ、--no-mmproj 付きで立ち上がって応答が返ること、 メモリが 24.27 GiB になっていることを確認しています。

まとめ

  • テキストしか使わないのに、画像用の mmproj を毎回読み込んでいた
  • 外すとメモリが約 1 GiB 減る。2 回の計測の 12 回ともほぼ同じ値で、言い切れる
  • 1 回目は同じ条件の最初と最後が 1 割ずれ、速度について何も言えなかった
  • 裏で別の Claude のセッションが dev-orchestra を動かしていた。止めて測り直すと、ずれは 2% 前後に縮んだ
  • 測り直した結果、速度の差は 3% 未満。mmproj を外しても速くも遅くもならない
  • --cache-reuse は外しても使えず、llama-bench との速度差の原因でもなかった
  • 既定は mmproj なしに変更。画像を使うときは -Vision

速くなる話を期待して測り始めましたが、速さは変わらず、確かに得たのは 1 GiB のメモリでした。

1 回目で「分からない」と書けたのも、2 回目で「変わらない」と言えるようになったのも、 挟み込みで測って、ずれの大きさを見ていたからです。 そして、ずれを 5 分の 1 にしたのは、測り方ではなく裏を静かにしたことでした。

Introduction

In the prompt cache post, I spotted this line in the server’s startup log.

srv    load_model: cache_reuse is not supported by multimodal, it will be disabled

The model I use, Qwen3.6-35B-A3B, can also handle images. When you specify it with -hf, the image mmproj (a file of about 0.9 GB) is loaded automatically.

But neither the llm command nor the dev-orchestra reviews use anything but text. So every time, I was reading a file and spending memory on a feature I never use.

What changes if I drop it? I had three candidates.

  • Memory goes down (almost certain)
  • Startup gets faster (one less file to read)
  • Loading and generation get faster (it might be the cause of the roughly 20% speed gap between llama-bench and the real server)

If the third one hit, my daily llm would get faster as it is.

Designing from the Checklist

In my previous post, I turned 19 measurement mistakes into a checklist. This time I applied it from the start.

Checklist itemWhat I did this time
Is the setting really in effect?Check that the mmproj loading line does not appear in the startup log, and stop if it differs from what I expect
How to compareAlternate with → without → with → without → with, 5 runs. Use whether the first and last “with” runs match to see drift
Same conditions as productionSame arguments as serve.ps1 (-c 65536, MTP on). The only difference is --no-mmproj
InterferenceRecord CPU and GPU used by anything other than llama every 10 seconds
Safety stopsStop if sleep prevention doesn’t work. Stop if the server doesn’t start
Raw dataRecord the beginning of the generated text, and check both conditions produce sane output

The first item comes from the lesson of the --cache-reuse episode. I was wrong for four days because I assumed “the setting I added must be working”, so now if it isn’t in effect, the measurement itself stops.

I measured startup time, memory, time to load 32K tokens, and generation speed (256 tokens × 3 runs).

Results of the First Run

#mmprojStartupMemory (after startup)Generation (avg)Loading 32K
1with9.7 s25.11 GiB18.00 t/s140.4 s
2without8.3 s24.12 GiB19.13 t/s133.6 s
3with9.4 s25.21 GiB18.42 t/s144.9 s
4without9.6 s24.12 GiB17.25 t/s134.9 s
5with10.1 s25.21 GiB19.22 t/s125.5 s

Memory Clearly Dropped by 1 GiB

“With” was 25.11–25.21 GiB, and “without” was 24.12 GiB both times. The difference is about 1.0 GiB. The values barely moved across the 5 runs, and the two ranges don’t overlap. This I can state with confidence.

On Speed, I Couldn’t Say Anything

Speed, on the other hand, shows no clear direction in the table. For generation, the first “without” run is fast and the second is slow. For loading, “without” lands somewhere in the middle.

The deciding factor was that the first and last “with” runs didn’t match.

#1 (with)#5 (with)Drift
Loading 32K140.4 s125.5 s−11%
Generation18.00 t/s19.22 t/s+7%

Same conditions, yet the numbers moved by 10% between the start and end of the measurement. The “without” value of 134 seconds sits right inside the spread among the “with” runs (loading 125–145 seconds).

The drift during measurement was larger than the difference between conditions. In this state, I can say neither “it got faster” nor “it didn’t change”. You could also say that interleaving the runs is what showed me this. If I had compared one run each, I might have put #4 and #5 side by side and written “it’s faster with mmproj”.

What the Monitoring Caught

Looking at the monitoring log, CPU use by things other than llama rose to 13–22% between 16:14 and 16:20. The top processes were python3.13 and StreamDeck. python3.13 is not the measurement script.

When I looked into it, the Python on this PC is the Microsoft Store version 3.13, and its process name shows up as python3.13. And during the same period, another Claude session was working with dev-orchestra. dev-orchestra is a tool that runs on this Python. I believe that’s what was running in the background (I didn’t check the other session’s logs, so this is a judgment from the circumstances).

That window covers the end of #3 and #4. The low generation speed in #4 (17.25 t/s) might be due to it. But #1 and #5, which don’t overlap that window, also drift by 10%, so I can’t say this was the only cause of the drift.

Stopping the Background Work and Measuring Again

I added “check that no other Claude session is running” to the “Interference” item of the checklist, and measured again.

When I checked before starting, the last activity in the dev-orchestra session was 40 seconds ago. It really was in use. I had that session stopped, confirmed there were no python processes, and then started.

I also increased the number of runs: alternating with → without 7 times (4 with, 3 without). That way I can compare each “without” run with the average of the “with” runs before and after it. Even if things drift slowly during measurement, bracketing them before and after cancels it out.

Comparison of with and without mmproj in the first and second runs. Memory is about 1 GiB apart in both runs and never overlaps. For generation speed and 32K loading time, the first run varied by 10% even between identical "with" runs, while the second run stayed within about 2%, and "without" overlapped "with" inside that range

#mmprojStartupMemory (after startup)Generation (avg)Loading 32K
1with25.0 s *25.06 GiB18.91 t/s133.2 s
2without8.3 s23.97 GiB19.39 t/s136.6 s
3with8.0 s25.06 GiB18.87 t/s136.0 s
4without7.1 s23.97 GiB18.77 t/s134.7 s
5with8.1 s25.06 GiB18.91 t/s136.8 s
6without7.3 s23.97 GiB18.85 t/s136.9 s
7with7.8 s25.06 GiB18.86 t/s136.3 s

* #1 was started right after stopping the server that had been running until just before.

In the monitoring, CPU use by things other than llama had a median of 7.1%, with no sustained peak like in the first run.

The drift shrank from 10% to around 2%.

GenerationLoading 32K
First and last “with” (#1 and #7)−0.2%+2.3%
#2 (without) vs. the “with” runs around it+2.6%+1.5%
#4 (without) vs. the “with” runs around it−0.6%−1.2%
#6 (without) vs. the “with” runs around it−0.2%+0.3%

The difference between “without” and the “with” runs around it changes direction from run to run and stays within 3%. I think it’s fair to say dropping mmproj doesn’t change speed. More precisely, if there is a difference, it’s under 3%.

Memory was again the same in all 7 runs (with: 25.06 GiB, without: 23.97 GiB, a difference of 1.09 GiB).

I don’t know whether the 10% drift in the first run was entirely due to dev-orchestra, because it drifted between #1 and #5 too, which didn’t overlap that window. But it’s certain that just quieting the background cut the spread of the same script to one fifth.

The Output Is Identical, Word for Word

In all 12 runs across the two measurements, the generated text was the same from the start.

ローカルLLMをiGPU(統合グラフィックス)で動作させる最大の利点は、専用メモリ(VRAM)を必要とせず、…

mmproj is the part that reads images, so as expected it has no effect on text only. Still, checking whether things really are as expected is how I did it this time.

The Two Things I Hoped For Didn’t Pan Out

--cache-reuse still couldn’t be used after dropping it.

srv    load_model: cache_reuse is not supported by this context, it will be disabled

The reason just changes from “because it’s multimodal” to “not supported in this context”. This model is a hybrid with SSM, and it can’t coexist with a mechanism that shifts and reuses the KV. It was just as I confirmed last time.

The speed gap between llama-bench and the real server couldn’t be explained by mmproj either. If loading had become 20% faster, that would have been the answer, but the remeasured difference is under 3%. The cause remains unknown.

I Changed the Default

I made “without mmproj” the default.

WithWithout
Memoryabout 25.1 GiBabout 24.0 GiB
Startupabout 8 sabout 7–8 s
Text output—Same
Image inputAvailableNot available

The only thing I lose is image input, which I don’t use now. On a machine with 93.6 GiB, 1 GiB is a small difference. Still, there’s no reason to keep spending memory on a feature I don’t use.

I added -Vision to serve.ps1, so images are loaded only when I need them.

# 普段(llm の自動起動もこちら)
pwsh -File scripts\serve.ps1            # --no-mmproj が付く

# 画像を使うとき
llm -Stop
pwsh -File scripts\serve.ps1 -Vision    # mmproj を読み込む

After the change, I had llm start the server automatically and confirmed that it came up with --no-mmproj, returned a response, and that memory was at 24.27 GiB.

Summary

  • I only use text, yet the image mmproj was loaded every time
  • Dropping it reduces memory by about 1 GiB. All 12 runs across the two measurements showed nearly the same value, so I can state this with confidence
  • In the first run, the first and last runs of the same condition drifted by 10%, and I couldn’t say anything about speed
  • In the background, another Claude session was running dev-orchestra. After stopping it and measuring again, the drift shrank to around 2%
  • The remeasured speed difference is under 3%. Dropping mmproj makes things neither faster nor slower
  • --cache-reuse still couldn’t be used after dropping it, and mmproj wasn’t the cause of the speed gap with llama-bench either
  • I changed the default to no mmproj. When I need images, I use -Vision

I started measuring hoping for a speedup, but speed didn’t change, and what I actually gained was 1 GiB of memory.

That I could write “I don’t know” after the first run, and say “it doesn’t change” after the second, is because I measured with interleaved runs and watched the size of the drift. And what cut the drift to one fifth wasn’t the measurement method but quieting the background.

関連記事 Related Posts

128K の文書の奥まで、ちゃんと読めているのか Can It Really Read All the Way into a 128K Document?

ローカルLLMに128Kトークンの文書を読ませ、20か所に仕込んだ事実を拾えるか測りました。深さによる取りこぼしは無く、文書に無いことは「記載なし」と答えました。唯一の誤りは、先頭2文字が同じ別の番号との取り違えで、片方の名前を変えると消えました。 I had a local LLM read a 128K-token document and measured whether it could pick up facts planted in 20 places. Nothing was missed because of depth, and for things not in the document it answered "not stated". The only mistake was mixing up a different ID that shared its first two characters, and it went away when I renamed one of them.

128K の文書は、読ませ直さずに使い回せるのか Can a 128K Document Be Reused Without Reading It Again?

ローカルLLMに128Kトークンの文書を読ませると20分以上かかります。2回目以降を速くできるか、プロンプトキャッシュを6通りの使い方で測りました。質問を変えるだけなら数秒で済みますが、ディスクに保存して再起動すると全部読み直しになります。理由はモデルのハイブリッド構成にありました。以前の記事の誤りも見つかりました。 Having a local LLM read a 128K-token document takes over 20 minutes. To see whether later requests can be faster, I measured the prompt cache in 6 scenarios. Changing just the question takes seconds, but saving to disk and restarting means re-reading everything. The reason lies in the model's hybrid architecture. I also found a mistake in an earlier post.

コンテキストを 128K まで伸ばしたら、壁はメモリではなく待ち時間だった I Stretched the Context to 128K, and the Wall Was Waiting Time, Not Memory

ローカルLLMのコンテキストを32Kから128Kまで伸ばして測りました。予想していたメモリ不足は起きず、理由はモデルの構造にありました。代わりに見えた壁はプロンプトの取り込み時間で、128Kを読ませるだけで21〜24分かかります。途中で自分の自動起動に計測を汚された話も含みます。 I stretched my local LLM's context from 32K to 128K and measured it. The memory shortage I expected never happened, and the reason lay in the model's architecture. The wall I hit instead was prompt ingestion time: just reading 128K takes 21–24 minutes. It also covers how my own auto-start polluted the measurements along the way.