前回、AI PCのNPUについて調べて、MacのNeural Engineで実際に測ってみたとき、eightはこう書きました。「806MBを超えるとANEのコンパイルが失敗する。ここに崖がある」と。
あのときは自作の小さなモデルを少しずつ大きくして測っていました。今回は実物のLLM——Qwen2.5の0.5B・1.5B・3Bを本当にCore MLに変換して、同じ崖にぶつかるのかを確かめます。結論から言うと、ぶつかりませんでした。そして前回の結論は間違っていました😳
・★前回の訂正:943MBのLLMは、演算の99.7%がANEで動いた
・犯人捜しの過程:層数・埋め込み表・head_dim・入力長を1つずつ切り分ける
・★正体は2つ:「約2GBのサイズの壁」と「特定の形だけ落ちる落とし穴」
・越え方:
bisect_model で2GB未満に割れば数GB級も載る・実測:1.5B・3BのANE/GPU比較と、分割による1.8倍speedup
・正しさの検証:PyTorchを基準にした精度と、実際の日本語生成
※実測環境は Apple M4 / メモリ32GB / macOS 26.5.2 / coremltools 9.0 / transformers 5.10.2、精度はfloat16です。ANE(Apple Neural Engine)の挙動はOSとcoremltoolsのバージョンに強く依存します。以下の数字は2026年7月時点・この構成での結果としてお読みください。
前回、eightは間違ったことを書きました
p953では、自作の小さなモデルのサイズを変えながらANEに載るかを見て、806MBのところで ANECCompile() FAILED が出たことから「サイズの崖がある」と結論づけました。数字がきれいに出ていたので、eightはそれを素直に信じてしまったわけです。
ただ、あれは自作のモデルでした。実物のLLMは層の形も構成もまったく違います。本物で確かめないと分からない——そこで今回は、実際に使われているモデルをそのまま持ってきました。
| モデル | パラメータ数 | 層 | hidden | head_dim | intermediate |
|---|---|---|---|---|---|
| Qwen2.5-0.5B-Instruct | 0.49B | 24 | 896 | 64 | 4864 |
| Qwen2.5-1.5B-Instruct | 1.54B | 28 | 1536 | 128 | 8960 |
| Qwen2.5-3B-Instruct | 3.09B | 36 | 2048 | 128 | 11008 |
表の項目は、あとの犯人捜しでそのまま容疑者になるので、先に意味を書いておきます。ここはLLMの中身の「形」を表す数字だと思ってください。
| 項目 | 何を表しているか |
|---|---|
| パラメータ数 | モデルが持っている数値(重み)の総数。「0.5B」は5億個という意味で、モデルの規模=賢さとファイルサイズの目安になります。今回はこれが大きくなるほどファイルも大きくなり、ANEの壁に近づいていきます |
| 層 | 同じ構造のブロックを何段積み重ねているか。1段ごとに文章の理解を深めていくイメージです。段数を減らせばモデルは小さくなりますが、1段あたりの形は変わりません——この性質を使って「大きさが原因か、形が原因か」を切り分けます |
| hidden | 1つの単語(トークン)を何個の数値で表しているか。いわば情報を流す管の太さで、太いほど多くのニュアンスを運べます。0.5Bは896本、1.5Bは1536本 |
| head_dim | 「どの単語に注目するか」を考える仕組み(アテンション)は、複数の担当者が別々の観点で同時に見る作りになっています。その担当者1人あたりの持ち幅がhead_dimです。0.5Bは64、1.5B以降は128と倍になっており、目立つ違いなので最初に疑いました |
| intermediate | 各層の中で、情報をいったん大きく広げてから元の太さに戻す部分の広げ幅。hidden 1536に対して8960まで広げます。層の中で一番大きな計算になる場所で、結果的にここが今回の犯人でした |
そして、この「同じ系列でサイズだけ違う」3つという選び方が、犯人捜しにとても効いてきます。設計思想が揃っているので、差が出たら「どの数字が違うせいか」を絞り込みやすいのです。逆に別々の会社のモデルを並べていたら、違いが多すぎて原因を特定できませんでした。
まず変換する:思ったより素直に通らない
普段ローカルLLMを動かすときはGGUF形式をllama.cppに渡すだけですが、Core MLに変換するには元のPyTorchの重みが要ります。GGUFやMLXの4bit版は独自形式に固めたあとなので、変換元には使えません。
やることは「PyTorchのモデルを1本道に固めて(トレース)、Core MLの形式に翻訳する」だけ……のはずでした。実際にはここで2回つまずきます。
つまずき1:翻訳表にない演算
coremltoolsはPyTorchの演算を1つずつCore MLの演算に翻訳しています。表にない演算が出てくると、そこで止まります。
NotImplementedError: PyTorch convert function for op 'diff' not implemented.
調べると、transformers 5.x が「複数の文をひとつのバッチに詰める」ときの区切り検出で torch.diff を使っていました。今回は1文ずつ流すので結果には関係ないのですが、トレースには残ってしまいます。
対処は2つあります。1つは翻訳のしかたを自分で書いて登録する方法。無い演算は足せる、というのは知っておくと便利です。
from coremltools.converters.mil import Builder as mb
from coremltools.converters.mil.frontend.torch.torch_op_registry \
import register_torch_op
@register_torch_op
def diff(context, node):
# torch.diff(x) = 隣り合う要素の差 = x[1:] - x[:-1]
inputs = _get_inputs(context, node, min_expected=1)
x = inputs[0]
...
later = _slice_along(full, axis, 1, None, rank) # x[1:]
earlier = _slice_along(full, axis, 0, -1, rank) # x[:-1]
context.add(mb.sub(x=later, y=earlier, name=node.name))
ところがこれを足すと、次は new_ones が出てきます。1つずつ追いかけるとキリがありません😅
つまずき2:マスク生成ごと迂回する
そこで発想を変えました。これらの演算はすべて「どこを見てよいか」を決めるマスクを作る処理から出てきています。ならばマスクを自分で作って渡してしまえば、その処理ごと飛ばせるはずです。
transformersは、渡されたマスクがすでに4次元なら「準備済み」とみなしてそのまま使い、生成処理を飛ばします。この性質を使います。
# float16(最小 -65504)に収まる範囲で十分に小さい値
NEG_MASK = -30000.0
def forward(self, input_ids):
seq = input_ids.shape[1]
dtype = self.m.dtype
# 未来のトークンを見せない三角マスク(0 は見てよい、大きな負の数は見せない)
mask = torch.full((seq, seq), NEG_MASK, dtype=dtype)
mask = torch.triu(mask, diagonal=1)
mask = mask[None, None, :, :] # (1, 1, seq, seq) = 4次元
out = self.m(input_ids=input_ids, attention_mask=mask,
use_cache=False)
return out.logits
⚠ ここで大事なのが NEG_MASK の値です。「見せない」印には普通 -∞ に近い値を使いますが、torch.finfo(float32).min(約-3.4×1038)を使うとfloat16に収まらず溢れます。変換時に RuntimeWarning: overflow encountered in cast が出ていたら、これを疑ってください。有限の -30000.0 で十分です(exp(-30000)は0になるので、意味は変わりません)。
これで変換が通るようになりました。あとはfloat16を指定して、ANEを使う設定で変換します。前回確かめた通り、float32のままではANEはそもそも動き出しません。
mlmodel = ct.convert(
traced,
inputs=[ct.TensorType(name="input_ids",
shape=(1, seq), dtype=np.int32)],
# ★float16でないとANEは動かない
compute_precision=ct.precision.FLOAT16,
compute_units=ct.ComputeUnit.CPU_AND_NE, # ★ANEを使う指定
minimum_deployment_target=ct.target.macOS15,
convert_to="mlprogram",
)
「本当にANEで動いた」をどう確かめるか
ここが今回いちばん大事な準備です。「速いから多分ANEが効いている」では根拠になりません。Core MLは指定した装置に載せられなければ黙ってCPUやGPUに落とすので、速度だけ見ていると勘違いします。
そこで MLComputePlan を使いました。これは1つ1つの演算がどの装置に割り当てられたかを教えてくれる仕組みです。
from coremltools.models.compute_plan import MLComputePlan
from coremltools.models.utils import compile_model
# ★ .mlpackage をそのまま渡すとプロセスごと落ちる。先にコンパイルする
compiled = compile_model(pkg_path)
plan = MLComputePlan.load_from_path(
path=compiled, compute_units=ct.ComputeUnit.CPU_AND_NE)
for func in plan.model_structure.program.functions.values():
for op in func.block.operations:
usage = plan.get_compute_device_usage_for_mlprogram_operation(op)
dev = type(usage.preferred_compute_device).__name__
# → ANE / GPU / CPU のいずれか
これで「全1355演算のうち何個がANEに行ったか」を数えられます。以降の数字はすべてこの方法で取っています。
1本目の結果:943MBが99.7%ANEに載った
まず0.5Bを、入力長128で変換しました。出来上がったのは943MB。前回言っていた「806MBの崖」の、すぐ上です。落ちるはずでした。
結果:演算1355個のうち1351個(99.7%)がANE。CPUに残ったのはわずか4個。推論は26.1msで、GPU(58.4ms)やCPU(58.0ms)の約2.2倍速い。
崖はありませんでした。806MBを大きく超えた943MBのモデルが、ほぼ丸ごとANEで動いています。前回の結論は、ここで崩れました。
| Qwen2.5-0.5B(943MB・入力長128) | 推論時間 | ロード時間 | 演算の割り当て |
|---|---|---|---|
| ANE指定 | 26.1ms | 6.44s | ANE 1351 / CPU 4 |
| GPU指定 | 58.4ms | 2.30s | GPU 1355 |
| CPU指定 | 58.0ms | 2.37s | CPU 1355 |
ここでロード時間にも注目してください。ANEは速い代わりに読み込みに約2.8倍かかっています。ANE向けにコンパイルする手間がここに乗ります。これは後半でもずっとついて回る性質です。
ところが1.5Bは、0%だった
気を良くして1.5Bへ。変換すると2945MB、いよいよ「数GB級」です。
結果:ANEに載った演算は0個。全1579演算がCPUに落ちました。推論は167.3msで、0.5Bより遅いのは当然としても、ANEがまったく使われていません。
ログには前回と同じあの文字列が出ていました。
E5RT encountered an STL exception. msg = MILCompilerForANE error: failed to compile ANE model using ANEF. Error=_ANECompiler : ANECCompile() FAILED.
943MBは載って、2945MBは載らない。やはりサイズなのでは? ——素直に考えればそうです。でも前回それで間違えたので、今度はちゃんと切り分けることにしました。
犯人捜し①:大きさではなかった
最初に試したのは「小さくすれば載るのか」です。coremltoolsには bisect_model(あとで主役になります)という、モデルを2つに割る関数があります。これで1.5Bを割って、かけらごとにANEに載るかを見ました。
| 1.5Bを割ったかけら | サイズ | ANEに載った割合 |
|---|---|---|
| 2分割 その1 | 1490MB | 0% |
| 4分割 その3 | 509MB | 0% |
509MBまで小さくしても0%のまま。943MBが載ったのに、509MBが載らない。この時点で「サイズが原因」という説は崩れます。
次に、1.5Bの層を1つだけにして試しました。層を減らしても1層あたりの形は変わらないので、「大きさが原因か、形が原因か」を分けられます。
| 条件(入力長128) | サイズ | ANE |
|---|---|---|
| 1.5B・1層だけ・埋め込み表なし | 89.4MB | 0% |
| 0.5B・全24層(対照) | 942.8MB | 100% |
89MBが落ちて、943MBが載る。これで決まりです。大きさは犯人ではありません。ついでに「入口の巨大な埋め込み表が悪いのでは」という説も、表を外しても0%だったので消えました。
残るのは層の形だけです。
犯人捜し②:形を1つずつ振ってみる
ここからは、1層だけの小さなモデルを乱数の重みで作り、設定を1つずつ変えて試します。中身の精度は関係なく「載るか載らないか」だけを見るので、これで十分です。
0.5Bと1.5Bの目立つ違いは head_dim(64 対 128) と intermediate_size(4864 対 8960)。まずhead_dimを疑いました。
head_dimを32・64・72・80・96・112・128・256と振った結果 → 全部100%ANE。犯人ではありませんでした。仮説が外れたので次へ。
では intermediate_size(層の中でいったん広げる幅)はどうか。ここで様子がおかしくなります。
| intermediate_size | サイズ | ANE |
|---|---|---|
| 4864 〜 7680 | 48〜73MB | 100% |
| 7936 〜 16384 | 75〜149MB | 0% |
| 17408 〜 20000 | 158〜181MB | 91% |
単調ではありません。大きくしていくと途中で落ちて、さらに大きくするとまた載る。「上限」ならこうはならないので、これはコンパイラが処理のしかたを切り替えている境目だと考えられます。
そして、いちばん驚いたのがここです。intermediate_sizeを1.5Bと同じ8960に固定して、入力長だけを変えてみました。
| 入力長(seq) | 32 | 64 | 128 | 256 | 512 |
|---|---|---|---|---|---|
| ANEに載った割合 | 100% | 100% | 0% | 100% | 100% |
まったく同じモデルなのに、入力長128のときだけ落ちる。サイズは同じ(84MB)。前後の32・64・256・512では問題なく載ります。
つまり、eightが最初に1.5Bで見た「0%」は、たまたま入力長128を選んだせいだった可能性が出てきました。前回の「806MBの崖」も、おそらく同じ種類の偶然です。サイズを変えたつもりで、実は形の組み合わせを踏んでいたのだと思います😅
正体:壁は2つあった
では入力長を64にすれば1.5Bも載るのか——試すと、やはり0%でした。1層なら載るのに、28層まとめると落ちる。ここでもう一度、層数を増やしながら測り直しました(入力長64・埋め込み表なし)。
| 層数 | 1 | 8 | 14 | 20 | 22 | 24 | 28 |
|---|---|---|---|---|---|---|---|
| サイズ(MB) | 89 | 714 | 1250 | 1786 | 1964 | 2143 | 2500 |
| ANE | 100% | 100% | 100% | 100% | 100% | 0% | 0% |
1964MBは載って、2143MBは載らない。今度はきれいに単調です。境目はほぼ2GB。これは本物の「サイズの壁」でした。
整理すると、こういうことになります。
越え方:Appleが用意した「割る」道具
サイズの壁が2GBなら、2GB未満に割ればいい。そのための関数がcoremltoolsに用意されています。bisect_model です。重みがだいたい半分ずつになるところでモデルを2つに切ります。
from coremltools.models.utils import bisect_model # 2945MB のモデルを2つに割る bisect_model(pkg_path, out_dir, check_output_correctness=False)
「そんな道具が標準で入っている」こと自体が、大きいモデルがANEに載らないのはよくある話だという何よりの証拠だと思います。
入力長を64(落とし穴を避けた値)にして、1.5Bを割った結果がこちらです。
| Qwen2.5-1.5B(元 2945MB) | サイズ | ANEに載った割合 |
|---|---|---|
| 分割前(そのまま) | 2945MB | 0% |
| 分割後 かけら1 | 1490MB | 99% |
| 分割後 かけら2 | 1900MB | 100% |
載りました。2.9GBのモデルが、2つに割るだけでほぼ丸ごとNeural Engineで動くようになります。
つなぎ方も簡単で、前半の出力をそのまま後半の入力に渡すだけです。bisect_model が入出力の名前を合わせてくれているので、辞書を引き継いでいけば動きます。
feed = {"input_ids": input_ids}
for m, spec in zip(models, specs):
need = {i.name for i in spec.description.input}
out = m.predict({k: v for k, v in feed.items() if k in need})
feed.update(out) # 前半の出力を後半の入力に渡す
logits = out["logits"]
3B(5.9GB)はどうか
ここまで来たら、もっと大きいものも試したくなります。3Bを変換すると5887MB。2GBの壁に対して3倍近いので、1回割っただけでは足りないはずです。
| Qwen2.5-3B(元 5887MB) | サイズ | ANE |
|---|---|---|
| 1回割り かけら1 | 2946MB | 0% |
| 1回割り かけら2 | 3535MB | 0% |
| 2回割り かけら1 | 1476MB | 97% |
| 2回割り かけら2 | 1470MB | 100% |
| 2回割り かけら3 | 1176MB | 100% |
| 2回割り かけら4 | 2358MB | 100% |
4分割で、5.9GBのモデルがANEに載りました。
ただし正直に書いておくと、いちばん下のかけらは2358MBあるのに100%載っています。さきほど「壁は約2GB」と書いたばかりなのに、それを超えているわけです。ですから「2GBがきっちりした閾値」とは言えません。同じ形のモデルを厚くしていったときの境目が1964〜2143MBだった、というのが正確なところで、モデルの構成が変われば境目も動きます。結局のところ、試してみるしかないというのが実感です。
速さ:ANEはGPUより速いのか
載っただけでは意味がないので、速度を測ります。入力長64、5回試して最速値です。
| Qwen2.5-1.5B(2分割・入力長64) | 推論時間 | ロード時間 |
|---|---|---|
| 分割後・ANE | 53.1ms | 13.23s |
| 分割後・GPU | 86.1ms | 8.07s |
| 分割前(ANE指定だがCPUに落ちる) | 97.7ms | 11.33s |
割っただけで 97.7ms → 53.1ms。1.8倍速くなりました。GPUと比べても1.6倍速い。モデルの中身は1バイトも変えていません。「ANEに載る形にした」だけです。
一方でロードは13.2秒とGPU(8.1秒)の1.6倍かかります。ANEは起動が遅く、走り出すと速い。常駐して何度も推論するアプリには向きますが、起動してすぐ1回だけ使うような用途では、この初期費用が効いてきます。
答えは合っているのか
速くなっても、答えが変わっていたら意味がありません。ここは念入りに確かめました。
基準は変換していない元のPyTorchモデル(float32)。それに対して、分割後のモデルをGPUとANEで動かした結果を比べます。見るのは「数値の差」と、実用上より大事な「結局どのトークンを選ぶか」の一致率です。
| PyTorch(正解)との比較 | 最大の差 | 平均の差 | 選ぶトークンの一致 |
|---|---|---|---|
| 分割後・GPU | 0.631 | 0.0088 | 100% |
| 分割後・ANE | 2.805 | 0.169 | 96.15% |
ここから2つのことが言えます。
①分割そのものは答えを変えません。分割後をGPUで動かすと一致率100%。bisect_model は計算のグラフを切っているだけで、内容には手を付けていないからです。
②ANEはわずかに答えがずれます。一致率96.15%、平均の差0.169。これはANEのfloat16の計算精度による差で、ほとんどの場面では同じ答えになるが、僅差のときに選択が変わることがあるという程度です。翻訳や要約なら実用上ほぼ問題になりませんが、毎回まったく同じ出力が要る用途では注意が必要です。
実際に喋らせてみる
最後に、数字ではなく実際の動作で確かめます。0.5BのモデルをANEで動かし、1トークンずつ選んで文章を作らせました。
質問:日本の首都はどこですか?短く答えてください。 --- 生成結果 --- 日本の首都は東京都です。 ---------------- 8トークン生成 / 0.24s = 33.4 tok/s
ちゃんと日本語で答えました😊 Neural Engineだけで、実物のLLMが動いています。
ただしこの 33.4 tok/s という数字は割り引いて見てください。今回のモデルはKVキャッシュを持たない固定長なので、1トークン増えるたびに128トークンぶん全部を計算し直しています。p951でllama.cppが出した20.6 tok/sと単純に比べられる数字ではありません(あちらはキャッシュありの素直な生成)。実用的な速度を出すには、KVキャッシュを状態として持つ作り(ct.StateType)に直す必要があります。今回はそこまで踏み込まず、「載るのか」に集中しました。
つまずいたところ
この検証で引っかかった、記事本編に入らなかったものを並べておきます。同じことをする方の役に立てば幸いです。
| 症状 | 原因と対処 |
|---|---|
MLComputePlan でプロセスごと落ちる | 受け取るのは .mlpackage ではなくコンパイル済みの .mlmodelc。compile_model() を通してから渡す |
変換時に overflow encountered in cast | マスクの -∞ がfloat16に収まっていない。-30000.0 のような有限値にする |
| 1.5BをCPU指定で動かすと出力が全部NaN | GPU・ANEでは正常、0.5Bでは3装置とも正常。float16での桁あふれとみられる。このため1.5BのCPU計測値は比較から外した |
| 3条件を1プロセスで比較すると強制終了 | 数GBのモデルを同時に抱えるとメモリが足りない。プロセスを分けて結果をファイルに保存してから突き合わせる |
| ★ディスクが突然なくなる | Core MLのコンパイルキャッシュ ~/Library/Caches/python3/com.apple.e5rt.e5bundlecache が113GBまで肥大していた。変換を繰り返す検証では都度消す |
最後のキャッシュには本当にやられました。変換するたびにコンパイル結果が積み上がっていて、気づいたときには空き容量が1GBを切り、コンパイルに失敗した結果がNaNとして出てくるという紛らわしい症状まで出ていました😅 一度「Core MLのバグでは」と疑ってしまったので、おかしな結果が出たらまずディスクを見るのは大事な教訓です。
【追記】では、NPUを使っているアプリは実際にあるのか
ここまで「載せられるか」ばかり書いてきましたが、公開後に「そもそもNPUを使うアプリは世の中にあるのか。日常や仕事で得はあるのか」という疑問をいただきました。せっかくなので調べたことを追記します(2026年7月時点)。
結論:あります。ただし「気づかずに使っている」ものが中心
NPU専用に作られたアプリというより、既存アプリの一部の機能だけがNPUに回されている——というのが実際の姿でした。PC Watchの実機検証などで、使用が確認されているものを並べます。
| アプリ・機能 | NPUで何をしているか | 実測の様子 |
|---|---|---|
| Windows スタジオエフェクト | 背景ぼかし・視線補正 | 背景ぼかしのみでNPU使用率6%、3効果併用で20% |
| フォトの Image Creator | 画像生成 | 512×512を1秒あまり。NPUをほぼフル活用 |
| Audacity(OpenVINO) | 音声の文字起こし | 10秒の音声を「ほぼ一瞬」で処理 |
| CapCut | 動画の背景削除 | 30秒の動画を15秒(NPUとGPUを併用) |
| Luminar Neo | ポートレートのボケ | NPU使用率40%程度 |
| GIMP(OpenVINO) | Stable Diffusionで画像生成 | 1枚約8秒(NPUとGPUを併用) |
ほかにNortonの詐欺対策(音声・動画のディープフェイク判定、フィッシング検出)、djay Proのボーカル・楽器の分離、Photoshopの被写体選択やノイズ除去なども、NPUで動くようになっています。
Macでは、whisper.cppがCore ML経由でANEを使うとCPUのみ比で3倍以上速くなります。音声対話の遅延を削った回で使ったのがまさにこれで、今回わかった「ANEが得意なサイズ感」にちょうど収まっているのがポイントです。Final Cut Proの文字起こし検索やシーン検索も、Neural Engine前提の機能として載っています。
メリットは「速さ」ではなく「電力」と「他が空くこと」
ここが誤解されやすいところで、NPUの利点は速度が主役ではありません。
① 電力効率。同じAI処理を、GPUなら30〜40W必要なところNPUは5〜10Wで済みます。AI機能を多用する場面でバッテリーが15〜20%長持ちするという報告もあります。
② CPUとGPUが空く。Web会議で背景ぼかしをかけたまま資料を作る、といった場面で効きます。ぼかしをNPUに逃がせるので、本業の処理が重くなりません。
③ ずっと動かしていられる。電力が小さいので常時稼働に向きます。字幕の自動生成やノイズ除去は、この性質があって初めて成立します。
今回の実測でも、条件が合えばANEはGPUの2.2倍速く動きました(0.5Bで26.1ms対58.4ms)。ただし読み込みは逆に2.8倍かかっています。「起動は遅く、走り出すと速く、そして省電力」——この性格が、そのまま得意な用途を決めているのだと思います。
正直に書いておきたい注意点
「NPU対応」とうたっていても、実際には使われないことが多いのが実情です。NPUはメーカーごとに中身が違い、それぞれ専用の対応が要るためです。実際、同じLuminar Neoでも、Copilot+ PCではNPUを使うのに、別のCPUを積んだPCでは使われなかったという検証結果が出ています。
そして大きなAI(ローカルLLM)は、やはりNPUの苦手分野です。前回調べた通り主流のツールはNPUを使っておらず、今回こうして実際に載せてはみたものの、2GB未満に割る・特定の形を避ける・載ったことを確認するという手間が要りました。普段づかいの道具にはまだ遠い、というのが率直な感想です。
さらに、Microsoft自身がCopilot+の機能をNPU専用から外してGPUでも動かす方向でテストしているという報道もあり、「NPUでなければできない」という前提そのものが動きつつあります。
で、買うときはどう見ればいいのか
Web会議が多い・外出先での作業が多い・文字起こしをよく使う——ここに当てはまるなら、NPUの恩恵は実感できます。バッテリーが持ち、ファンが回らず、他の作業が重くならない、という形で効いてきます。
逆にローカルLLMを動かしたい、画像生成を本格的にやりたいのであれば、今はまだGPUとVRAMを見るのが正解です。TOPSの数字の大きさは、この用途ではあてになりません。
つまりNPUは、買う理由にするというより、選んだPCに載っていれば裏で静かに得をしている——そういう付き合い方が、今の実態に合っているように思います😊
【この追記の主な参考】PC Watch「Intel NPUってタダの飾り?」/窓の杜「NPU対応アプリなのにNPUが使えない?」/Microsoft Learn「Copilot+ PC 開発者ガイド」/Windows Central/whisper.cpp 公式README/Tom's Hardware(Copilot+機能のGPU対応テスト報道)。いずれも2026年7月時点で確認。この分野は動きが速く、対応状況は短期間で変わります。
まとめ:ANEに載せる5か条
今回分かったことを、5つにまとめます。
① float16にする。float32のままではANEは動き出しません(前回の結論はここは正しかった)。
② 1つのモデルは2GB未満に。超えるなら bisect_model で割る。割っても答えは変わりません。
③ 入力長や層の幅は、値を変えて試す。特定の組み合わせだけ落ちることがあります。128がだめでも64や256なら載る、ということが実際に起きます。
④ 速度ではなく MLComputePlan で確認する。載らなかったときCore MLは黙ってCPUに落とすので、速度だけ見ていると気づけません。
⑤ ロード時間を勘定に入れる。ANEは走り出すと速いぶん、読み込みはGPUの1.5〜2倍かかります。
そして、いちばんの収穫は前回の自分の結論が間違っていたと分かったことでした。「806MBの崖」は、実際には性質の違う2つの問題が重なって、たまたま1つの崖に見えていただけです。数字がきれいに出ていると、つい信じてしまうのだなと反省しました。
結論としては——数GB級のLLMは、Neural Engineでちゃんと動きます。ただし「そのまま渡せば動く」ものではなく、2GB未満に割って、形の落とし穴を避けて、載ったことを確認する、という手順が要ります。llama.cppにGGUFを渡すだけの手軽さとは、まだかなり距離があるというのが正直なところです。
それでは、今回はここまで。最後までありがとうございました😊