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 | 使用 |
| 画像プロジェクタなどのmmproj | 1.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バイト) |
| OS | macOS 26.5.2 |
| 推論エンジン | OpenBMB/llama.cpp-omni、コミット 0b74404 |
| GPU | Metal |
| モデル | 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-cli | thinking off、temperature 0.2、seed 43・44・45 |
| Qwen3.8 Max | ModelScope(Qwen-Ambassador名義) | enable_thinking=false、temperature 0.2 |
| GPT-6.1 Sol | Codex CLI 0.162.1 | medium |
| Claude Opus 5.5 | Claude Code 2.1.285 | medium |
| DeepSeek-V4.1-Flash | ModelScope | thinking.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-A3B | 23・23・23 | 93.24 ± 2.17秒 |
| Qwen3.8 Max | 23・23・23 | 7.76 ± 0.18秒 |
| GPT-6.1 Sol | 24・24・24 | 19.21 ± 2.90秒 |
| Claude Opus 5.5 | 24・24・24 | 18.82 ± 0.74秒 |
| DeepSeek-V4.1-Flash | 24・24・24 | 4.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に変換している。

表の全行をJSONに転記させた上で、支払済の合計金額、支払済の中で金額が最大のID、画像に記載されていない日付の3点を質問した。日付がない場合は null と出力するよう指示している。元の表の内容は次の5行だ。
| 注文ID | 状態 | 個数 | 単価 |
|---|---|---|---|
| A-17 | 支払済 | 3 | 1,200 |
| B-04 | 保留 | 2 | 3,400 |
| C-23 | 支払済 | 7 | 450 |
| D-81 | キャンセル | 5 | 2,100 |
| E-95 | 支払済 | 4 | 875 |
正解は、支払済の合計が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-A3B | 0/3 | 3/3 | 68.09 ± 1.21秒 |
| Qwen3.5-35B-A3B | 0/3 | 2/3 | 7.57 ± 0.80秒 |
| GPT-6.1 Sol | 3/3 | 3/3 | 12.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 | 画像 | off | 0/3 | 68.09 ± 1.21秒 |
| MiniCPM-V | 画像 | on | 1/3 | 102.14 ± 66.96秒 |
| MiniCPM-V | テキスト | off | 0/3 | 46.82 ± 3.65秒 |
| MiniCPM-V | テキスト | on | 3/3 | 142.53 ± 18.51秒 |
| MiniCPM-V | JSON | off | 0/3 | 47.79 ± 3.90秒 |
| MiniCPM-V | JSON | on | 3/3 | 119.37 ± 17.18秒 |
| Qwen3.5-35B-A3B | 画像 | off | 0/3 | 7.57 ± 0.80秒 |
| Qwen3.5-35B-A3B | 画像 | on | 3/3 | 10.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-V | off | 4/10 | 8/10 | 61.95 ± 0.76秒 |
| MiniCPM-V | on | 3/10 | 8/10 | 73.95 ± 33.50秒 |
| Qwen3.5-35B-A3B | off | 2/10 | 8/10 | 6.96 ± 3.30秒 |
| Qwen3.5-35B-A3B | on | 10/10 | 10/10 | 10.36 ± 2.02秒 |
| GPT-6.1 Sol | medium | 10/10 | 10/10 | 10.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.53GB | 7.6〜8.0 tokens/s |
| 日本語の表画像1枚 | 4.60〜4.61GB | 5.8〜6.0 tokens/s |
16GBのMac miniにおいて、画像入力を含めてメモリ不足を起こさずに動作した。
なお、公式が謳うiPhoneでの実行メモリ3GBという数値はiOS向けの設定値であり、macOS上で動作させた今回の最大RSSとはメモリ管理の前提が異なる。
DSpark有効時の生成速度とメモリ
DSparkは、小さなドラフトモデルで推論候補を先回りして生成し、本体モデルでまとめて検証する高速化手法だ。
公式のDSparkモデルを取得し、自作スクリプトでエンジンが読めるF16形式(16ビット浮動小数点数、ファイルサイズ約789MB)へ変換した。
当初は画像入力でDSparkを試したが、Metalのメモリ上限やドラフト側の位置情報不整合エラーが発生したため、テキスト入力に絞って有効・無効を3回ずつ比較した。
| 項目 | 両条件でそろえた設定 |
|---|---|
| 入力 | 元の5行の表をテキスト形式で入力 |
| thinking・temperature | off・0 |
| コンテキスト・出力上限 | 8,192・1,024トークン |
| エキスパートのキャッシュ幅・先読み | 32・8 |
| 読み込み用スロット | 640 |
| DSparkの候補数 | 有効時は最大3トークン |
| DSpark | 生成速度(平均 ± 標準偏差) | 回答までの時間(平均 ± 標準偏差) | 最大RSSの範囲 |
|---|---|---|---|
| 無効 | 7.36 ± 0.23 tokens/s | 45.17 ± 1.35秒 | 4.40〜4.46GB |
| 有効 | 8.05 ± 0.14 tokens/s | 43.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も記録済みだ。