技術約12分で読めます

MiniCPM-V 4.7の35Bモデルを16GBのMac miniで動かしてみた

いけさん目次

MiniCPM-V 4.7の35B-A3Bが、iPhoneで3GBのメモリを使って動くという話を見かけた。
手元のM4・16GBのMac miniでも動くのか気になったので、実際に導入して試してみた。動くなら、以前のLLM比較で使ったコード生成や表の読み取り課題を同じ条件で入力して比べてみる。

モデル一式の容量とMoEのエキスパート分割

OpenBMBの公式READMEによると、iPhone 17 Pro Maxでの実行メモリは3GB、生成速度は1秒あたり13トークン(13 tokens/s)以上とされている。
対象はMiniCPM-V 4.7の35B-A3B版で、別に用意されている1B版は今回の検証には含めていない。

35B-A3Bは、総パラメータが約350億、1トークンの処理で動かすパラメータが約30億のMoE(Mixture of Experts)モデルだ。
MoEは、複数の小さな計算モジュール(エキスパート)の中から、入力に応じて必要なものを選んで計算する仕組みになっている。使われないエキスパートも後から選ばれる可能性があるため、モデル全体のデータはどこかに保持しておく必要がある。

公式アプリの説明を確認すると、端末向けのモデルはメインファイルとエキスパート群に分かれていた。
エキスパートを内蔵SSDに置き、必要な分だけをメモリへ読み込む設計だ。モデル全体の保存形式である通常のGGUFファイルを丸ごとメモリへ載せる構成とは異なる。

配布形式ファイルの大きさ今回の用途
通常のQ4_K_M版21.17GB、画像用ファイルは別使用せず
LES用メインファイル1.66GB使用
LES用エキスパート40ファイル合計13.84GB使用
画像プロジェクタなどのmmproj1.12GB画像の検証で使用
LES用一式16.62GB、マニフェストを含む内蔵SSDへ保存

容量は通常版のファイル一覧と、分割版のファイル一覧のバイト数から計算した。
ここでは1GBを10億バイトとしている。分割版は公式アプリの案内にある tc-mb/MiniCPM-V-Apps-gguf を使った。

LESによるエキスパートの先読み

SSDからエキスパートを逐次読み込むと、転送待ちで生成速度が落ちてしまう。
そこでLES(Lookahead Expert Scheduling)の技術説明では、次の層で使いそうなエキスパートを予測して先読みし、SSDからの転送とGPU計算を重ねている。

予測は読み込む順番を決めるための仕組みで、予測が外れたエキスパートも除外せず、ルーターが選んだものは読み込んで計算する。
今回は有効エキスパート数を絞る設定は使っていない。

公式が公表している速度には、重みを少ないビット数で保持する量子化や、高速化手法のDSparkも関わっている。
最初の比較では混合量子化の分割版とLESを使い、ドラフトモデルによる推論補助のDSparkは無効にして測定した。

M4 Mac miniでの動作環境とビルド

検証した環境は次のとおり。読み込み速度を落とさないよう、モデル一式は内蔵SSDに配置した。

項目今回の環境
マシンMac mini、Apple M4
メモリ16GiB(17,179,869,184バイト)
OSmacOS 26.5.2
推論エンジンOpenBMB/llama.cpp-omni、コミット 0b74404
GPUMetal
モデルMiniCPM-V-4_7-35B-A3B-mix_q3gud_im-les.ggufと対応するexperts
モデルのリビジョン8716e5209f308fd53f197ccd7d50c6fdcc2dc37d
実験スクリプトPython 3.14.4、uvで専用環境を作成

モデルのメインファイル単体では起動できないため、対応する experts/ と画像用の mmproj-model-f16.gguf をまとめてダウンロードした。
ファイル43個の合計は16,623,585,508バイト。モデル一式はGitには含めていない。

推論エンジンは公式リポジトリからビルドした。今回のコミットには通常の llama-server ターゲットが含まれておらず、サーバービルドはエラーになった。
CLIツールの llama-cli はビルドできたため、以降の測定はCLI経由で実施した。エンジン本体のコードは変更していない。

cmake -S llama.cpp-omni -B llama.cpp-omni/build \
  -DCMAKE_BUILD_TYPE=Release -DGGML_METAL=ON \
  -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_APP=OFF -DLLAMA_BUILD_UI=OFF
cmake --build llama.cpp-omni/build --target llama-cli -j 4

疎通確認として「日本の首都はどこですか。都市名だけを日本語で答えてください。」と入力したところ、「東京」と返ってきた。
起動から終了までの実測は約6.6秒、プロセスの物理メモリ使用量を示す最大RSSは約2.46GBだった。CLI上の生成速度表示は10.0 tokens/sだったが、短文出力のため速度の評価対象には含めていない。

過去の比較と同じ3つの課題で検証する

以前のLLM比較で使ったプロンプトと採点コードを引き継いだ。解釈が分かれやすい設問Dはあらかじめ入力から除外している。

課題求める処理採点
Pythonの区間統合重なる区間を統合し、端点が接するだけの区間は分ける。型や不正値も検証空入力、巨大整数、非数(NaN)など24ケース
イベント集計IDごとの最新バージョンを使い、同一バージョンなら後着を優先して支払済だけを通貨別に合計正解JSONとの一致
資料と主張の照合架空資料から、入力と出力、限定提供と一般公開、価格条件を区別する3問の正誤

3つの課題を1回のリクエストにまとめ、同じプロンプトを各モデルへ3回ずつ入力した。
生成されたPythonコードは、ネットワークやファイル操作を含まないことを確かめてから実行している。モデル自身にWeb検索やコード実行はさせていない。

思考モード(thinking)は最終回答の前に推論トークンを生成する設定で、最初の比較ではMiniCPM-V・Qwen・DeepSeekで無効(off)にした。

モデル呼び出し方推論・生成設定
MiniCPM-V 4.7 35B-A3Bローカルのllama-clithinking off、temperature 0.2、seed 43・44・45
Qwen3.8 MaxModelScope(Qwen-Ambassador名義)enable_thinking=false、temperature 0.2
GPT-6.1 SolCodex CLI 0.162.1medium
Claude Opus 5.5Claude Code 2.1.285medium
DeepSeek-V4.1-FlashModelScopethinking.type=disabled、temperature 0.2

MiniCPM-Vはコンテキスト8,192、出力上限3,072トークンに設定した。QwenとDeepSeekも出力上限は3,072としている。
CLIツールのSolとOpusは推論量を medium に指定し、温度や出力上限はツール既定のまま実行した。

MiniCPM-Vのエキスパート読み込み設定は --moe-slot-bank 640、LLAMA_MOE_DECODE_WINDOW=16、先読み数8で固定した。
バッチサイズとマイクロバッチはいずれも128。試行ごとにプロセスを再起動して測定している。

テキスト課題の結果

この環境での3回のテストでは、イベント集計(各回3/3)と資料照合(各回9/9)は全モデルが全問正解した。
コード生成課題で差がつき、MiniCPM-VとQwen3.8 Maxが3回とも23/24、残る3モデルが3回とも24/24となった。

モデルコード通過数(各24ケース)回答までの時間(平均 ± 標準偏差)
MiniCPM-V 4.7 35B-A3B23・23・2393.24 ± 2.17秒
Qwen3.8 Max23・23・237.76 ± 0.18秒
GPT-6.1 Sol24・24・2419.21 ± 2.90秒
Claude Opus 5.524・24・2418.82 ± 0.74秒
DeepSeek-V4.1-Flash24・24・244.13 ± 0.38秒

標準偏差は3回の標本標準偏差。回答までの時間はCLIの起動、モデル読み込み、APIの通信往復を含む。
MiniCPM-VはMac miniローカル、残る4モデルはクラウド側APIでの推論となるため、ネットワーク通信時間を含んだ実測値となっている。

巨大整数の浮動小数点変換で生じたOverflowError

MiniCPM-VとQwen3.8 Maxは、区間の端点に 10**400 を与えたケースで失敗した。
どちらも整数に対して math.isnan() や math.isinf() を呼び出しており、手元のPython 3.14.4で OverflowError が発生した。

MiniCPM-Vが生成したコードでは、型の確認を行ったあと、整数と小数を分けずに判定していた。

if math.isnan(start) or math.isnan(end):
    raise ValueError("NaN values are not allowed")
if math.isinf(start) or math.isinf(end):
    raise ValueError("Infinite values are not allowed")

Pythonのmathモジュールの isnan や isinf は浮動小数点数(float)を前提とする関数だ。
この環境では、非常に大きな整数を渡した段階でfloatへの暗黙変換が上限を超えて失敗した。入力された整数自体はPythonの任意精度整数として正常な値だった。

Sol、Opus、DeepSeekの出力では、小数の場合だけ有限性を確認していた。
Solの初回生成コードでは、小数のときだけ有限性を調べる形になっていた。

if isinstance(endpoint, float) and not math.isfinite(endpoint):
    raise ValueError('endpoints must be finite')

画像の表も読ませる

MiniCPM-Vは画像を入力できるので、日本語の表を画像にして渡した。

日本語の表画像を入力して検証する

MiniCPM-Vは画像を入力できるため、日本語の注文表を画像にして入力した。
入力画像は1,000×650ピクセルの同じPNG画像で、ブログ掲載用のみWebPに変換している。

5件の注文を載せた日本語の表。ID、状態、個数、単価の4列があり、支払済の行だけを集計する

表の全行をJSONに転記させた上で、支払済の合計金額、支払済の中で金額が最大のID、画像に記載されていない日付の3点を質問した。日付がない場合は null と出力するよう指示している。元の表の内容は次の5行だ。

注文ID状態個数単価
A-17支払済31,200
B-04保留23,400
C-23支払済7450
D-81キャンセル52,100
E-95支払済4875

正解は、支払済の合計が10,250円(3×1,200 + 7×450 + 4×875)、支払済で最大金額のIDがA-17(3,600円)、日付は存在しないため null となる。

画像側の比較には、ModelScopeのQwen3.5-35B-A3Bと、画像を扱えるCodex CLIのGPT-6.1 Solを使用した。各モデルへ同じ質問文を3回ずつ入力している。

この入力では、3モデルとも全3回で表の5行を完全に正しく転記し、存在しない日付にもすべて null と回答した。
差がついたのは、その後の合計金額と最大IDの判定だった。

モデル合計の正答最大金額のID回答までの時間(平均 ± 標準偏差)
MiniCPM-V 4.7 35B-A3B0/33/368.09 ± 1.21秒
Qwen3.5-35B-A3B0/32/37.57 ± 0.80秒
GPT-6.1 Sol3/33/312.78 ± 1.36秒

MiniCPM-Vは3回とも合計を7,700円と誤答した。
Qwen3.5は順に8,500円、9,200円、8,300円とばらつき、2回目は最大IDもC-23と誤答した。Solは3回とも10,250円、最大IDはA-17で全問正解だった。

MiniCPM-VとQwenが生成したJSONには、元の個数と単価が正しく出力されていた。
文字の読み取り自体は成功しており、数値を抽出した後の合算計算で失敗している。抽出したJSONをPythonなどの通常の計算コードへ渡せば、この計算ミスは発生しない。

入力形式とthinkingの切り替えによる比較

計算ミスの要因を切り分けるため、同じ5行のデータをテキストとJSONでも入力した。
それぞれ思考モード(thinking)の有効・無効を切り替え、MiniCPM-Vへ同じシード値の組み合わせで3回ずつ入力した。

Qwen3.5-35B-A3Bについても、元の画像入力で思考有効時の3回を追加測定した。各条件とも表の全行転記は3/3で正解している。

モデル入力形式thinking合計の正答回答までの時間(平均 ± 標準偏差)
MiniCPM-V画像off0/368.09 ± 1.21秒
MiniCPM-V画像on1/3102.14 ± 66.96秒
MiniCPM-Vテキストoff0/346.82 ± 3.65秒
MiniCPM-Vテキストon3/3142.53 ± 18.51秒
MiniCPM-VJSONoff0/347.79 ± 3.90秒
MiniCPM-VJSONon3/3119.37 ± 17.18秒
Qwen3.5-35B-A3B画像off0/37.57 ± 0.80秒
Qwen3.5-35B-A3B画像on3/310.44 ± 0.87秒

MiniCPM-Vは、テキストやJSONで入力しても thinking off では3回とも合計を誤答した。
テキストでは8,000円・7,700円・7,700円、JSONでは3回とも8,900円となり、画像認識の有無にかかわらず計算部分でつまずいている。

thinking on にすると、テキストとJSONはどちらも3/3で正解した。
一方、画像入力の thinking on は1/3の正答にとどまり、残る2回は8,900円だった。この誤答の2回は、思考ログ(<think>タグ内)にJSONを出力したまま処理が終了していた。

Qwen3.5の画像入力は、thinking on に切り替えると3/3で正解した。

数字を変えた10問での追加検証

問題固有の偏りかを確かめるため、個数・単価・状態・行順を変えた10種類の表画像を用意した。
レイアウトは共通で全問5行、支払済は1〜4行の範囲とした。あらかじめ計算した正解を用意し、各モデルへ画像を1問ずつ入力した。

全モデルとも10問すべてで全行の転記(10/10)と存在しない日付の null 判定(10/10)に成功した。

モデル推論設定合計の正答最大金額のID回答までの時間(平均 ± 標準偏差)
MiniCPM-Voff4/108/1061.95 ± 0.76秒
MiniCPM-Von3/108/1073.95 ± 33.50秒
Qwen3.5-35B-A3Boff2/108/106.96 ± 3.30秒
Qwen3.5-35B-A3Bon10/1010/1010.36 ± 2.02秒
GPT-6.1 Solmedium10/1010/1010.32 ± 1.63秒

MiniCPM-Vの thinking on は、10問中9問で思考ログ内にJSONを出力したまま終了した。思考の終了タグと最終回答が両方出力されたのは1問のみで、その回は合計も正解していた。

今回の全50回答について、出力されたJSONの数値を通常のPythonコードで集計し直したところ、50回すべてで正しい合計金額が得られた。
表画像から数値を読み取るOCR処理と、その数値を使った四則演算を分離することで、回答の安定性が大きく向上した。

追加検証で使った画像・正解データ・全回答ログは、LiltingChannelLaboの additional/ に保存している。

各条件でのメモリ使用量(最大RSS)

MiniCPM-Vの測定において、/usr/bin/time -l が記録した最大RSS(物理メモリ使用量)とCLI上の生成速度をまとめた。

MiniCPM-Vの条件最大RSSの範囲CLI表示の生成速度
テキスト3問、画像なし3.52〜3.53GB7.6〜8.0 tokens/s
日本語の表画像1枚4.60〜4.61GB5.8〜6.0 tokens/s

16GBのMac miniにおいて、画像入力を含めてメモリ不足を起こさずに動作した。
なお、公式が謳うiPhoneでの実行メモリ3GBという数値はiOS向けの設定値であり、macOS上で動作させた今回の最大RSSとはメモリ管理の前提が異なる。

DSpark有効時の生成速度とメモリ

DSparkは、小さなドラフトモデルで推論候補を先回りして生成し、本体モデルでまとめて検証する高速化手法だ。
公式のDSparkモデルを取得し、自作スクリプトでエンジンが読めるF16形式(16ビット浮動小数点数、ファイルサイズ約789MB)へ変換した。

当初は画像入力でDSparkを試したが、Metalのメモリ上限やドラフト側の位置情報不整合エラーが発生したため、テキスト入力に絞って有効・無効を3回ずつ比較した。

項目両条件でそろえた設定
入力元の5行の表をテキスト形式で入力
thinking・temperatureoff・0
コンテキスト・出力上限8,192・1,024トークン
エキスパートのキャッシュ幅・先読み32・8
読み込み用スロット640
DSparkの候補数有効時は最大3トークン
DSpark生成速度(平均 ± 標準偏差)回答までの時間(平均 ± 標準偏差)最大RSSの範囲
無効7.36 ± 0.23 tokens/s45.17 ± 1.35秒4.40〜4.46GB
有効8.05 ± 0.14 tokens/s43.24 ± 1.05秒5.52〜5.53GB

テキスト1問の測定では、生成速度が平均約9.5%向上し、全体の待ち時間は約4.3%短縮された。
一方で最大RSSは約1.1GB増加している。ドラフトモデルが生成した候補は、各回216トークン中201トークンが本体に採用されていた。

実験スクリプト、プロンプト、生成コード、全回答データは、LiltingChannelLaboの 2026/10/14/minicpm-v-4-7-mac-mini/ に保存している。重みのリビジョン、エンジンのコミット、入力画像のSHA-256も記録済みだ。