前回 はGGUFを使ったので、今回はONNX。
llama.cppのセットアップは前回やったので省略。
事前準備
python3 -m venv .venv-onnx
source .venv-onnx/bin/activate
pip install "optimum[onnx]" onnx onnxruntime transformers
現在の onnxruntime にはApple Silicon向けのCoreML Execution Providerも含まれているので安心。
確認しておく。
$ python - <<'EOF'
import onnxruntime
print(onnxruntime.get_available_providers())
EOF
['CoreMLExecutionProvider', 'AzureExecutionProvider', 'CPUExecutionProvider']
うん、大丈夫そう。Azureなんたらはよくわからんが。
Hagging Faceからモデル取得
今回はHuggingFaceTB/SmolLM2-360M-Instruct。
hf download HuggingFaceTB/SmolLM2-360M-Instruct \
--local-dir ./models/SmolLM2-360M-Instruct
ONNXへexport
optimum-cli export onnx \
--model ./models/SmolLM2-360M-Instruct \
--task text-generation-with-past \
./models/SmolLM2-360M-Instruct-onnx
色々みてみる
import onnx
from collections import Counter
model = onnx.load(
"./models/SmolLM2-360M-Instruct-onnx/model.onnx",
load_external_data=False,
)
graph = model.graph
print("graph name:", graph.name)
print("inputs:", len(graph.input))
print("outputs:", len(graph.output))
print("nodes:", len(graph.node))
print("initializers:", len(graph.initializer))
counts = Counter(node.op_type for node in graph.node)
for op, count in counts.most_common():
print(f"{op:30} {count}")$ python graph.py
graph name: main_graph
inputs: 67
outputs: 65
nodes: 7911
initializers: 291
Constant 2891
Unsqueeze 1008
Mul 519
Shape 489
Gather 424
Concat 388
MatMul 290
Cast 271
Reshape 260
Add 228
Transpose 161
Slice 160
Where 130
Div 129
ConstantOfShape 67
Equal 66
Expand 66
Pow 65
ReduceMean 65
Sqrt 65
Neg 64
Softmax 32
IsNaN 32
Sigmoid 32
Range 3
And 2
LessOrEqual 1
Flatten 1
Cos 1
Sin 1
最初の30ノードを見てみる。
import onnx
from collections import Counter
model = onnx.load(
"./models/SmolLM2-360M-Instruct-onnx/model.onnx",
load_external_data=False,
)
graph = model.graph
for node in graph.node[:30]:
print(
f"{node.name:60} "
f"{node.op_type:20} "
f"{list(node.input)} -> {list(node.output)}"
)/model/embed_tokens/Gather Gather ['model.embed_tokens.weight', 'input_ids'] -> ['/model/embed_tokens/Gather_output_0']
/model/Shape Shape ['past_key_values.0.key'] -> ['/model/Shape_output_0']
/model/Constant Constant [] -> ['/model/Constant_output_0']
/model/Gather Gather ['/model/Shape_output_0', '/model/Constant_output_0'] -> ['/model/Gather_output_0']
/model/Shape_1 Shape ['/model/embed_tokens/Gather_output_0'] -> ['/model/Shape_1_output_0']
/model/Constant_1 Constant [] -> ['/model/Constant_1_output_0']
/model/Gather_1 Gather ['/model/Shape_1_output_0', '/model/Constant_1_output_0'] -> ['/model/Gather_1_output_0']
/model/Add Add ['/model/Gather_output_0', '/model/Gather_1_output_0'] -> ['/model/Add_output_0']
/model/Cast Cast ['/model/Gather_output_0'] -> ['/model/Cast_output_0']
/model/Cast_1 Cast ['/model/Add_output_0'] -> ['/model/Cast_1_output_0']
/model/Constant_2 Constant [] -> ['/model/Constant_2_output_0']
/model/Range Range ['/model/Cast_output_0', '/model/Cast_1_output_0', '/model/Constant_2_output_0'] -> ['/model/Range_output_0']
/model/Cast_2 Cast ['attention_mask'] -> ['/model/Cast_2_output_0']
/model/Shape_2 Shape ['/model/Range_output_0'] -> ['/model/Shape_2_output_0']
/model/Constant_3 Constant [] -> ['/model/Constant_3_output_0']
/model/Gather_2 Gather ['/model/Shape_2_output_0', '/model/Constant_3_output_0'] -> ['/model/Gather_2_output_0']
/model/Add_1 Add ['/model/Gather_output_0', '/model/Gather_2_output_0'] -> ['/model/Add_1_output_0']
/model/Shape_3 Shape ['/model/embed_tokens/Gather_output_0'] -> ['/model/Shape_3_output_0']
/model/Constant_4 Constant [] -> ['/model/Constant_4_output_0']
/model/Gather_3 Gather ['/model/Shape_3_output_0', '/model/Constant_4_output_0'] -> ['/model/Gather_3_output_0']
/model/Constant_5 Constant [] -> ['/model/Constant_5_output_0']
/model/Unsqueeze Unsqueeze ['/model/Range_output_0', '/model/Constant_5_output_0'] -> ['/model/Unsqueeze_output_0']
/model/Constant_6 Constant [] -> ['/model/Constant_6_output_0']
/model/Unsqueeze_1 Unsqueeze ['/model/Unsqueeze_output_0', '/model/Constant_6_output_0'] -> ['/model/Unsqueeze_1_output_0']
/model/Constant_7 Constant [] -> ['/model/Constant_7_output_0']
/model/Unsqueeze_2 Unsqueeze ['/model/Unsqueeze_1_output_0', '/model/Constant_7_output_0'] -> ['/model/Unsqueeze_2_output_0']
/model/Cast_3 Cast ['/model/Gather_3_output_0'] -> ['/model/Cast_3_output_0']
/model/Constant_8 Constant [] -> ['/model/Constant_8_output_0']
/model/Constant_9 Constant [] -> ['/model/Constant_9_output_0']
/model/Range_1 Range ['/model/Constant_8_output_0', '/model/Cast_3_output_0', '/model/Constant_9_output_0'] -> ['/model/Range_1_output_0']
MatMul, Add, Reshape, Transpose, Softmax, Gather みたいな演算が並んでいる。 つまり、forward passそのものがグラフとして保存されている、ってことかな。
入力shapeも見てみる。
import onnxruntime as ort
session = ort.InferenceSession(
"./models/SmolLM2-360M-Instruct-onnx/model.onnx",
providers=["CPUExecutionProvider"],
)
for inp in session.get_inputs():
print(inp.name, inp.shape, inp.type)$ python ./input_shapes.py
input_ids ['batch_size', 'sequence_length'] tensor(int64)
attention_mask ['batch_size', 'past_sequence_length + sequence_length'] tensor(int64)
position_ids ['batch_size', 'sequence_length'] tensor(int64)
past_key_values.0.key ['batch_size', 5, 'past_sequence_length', 64] tensor(float)
past_key_values.0.value ['batch_size', 5, 'past_sequence_length', 64] tensor(float)
past_key_values.1.key ['batch_size', 5, 'past_sequence_length', 64] tensor(float)
past_key_values.1.value ['batch_size', 5, 'past_sequence_length', 64] tensor(float)
...
optimum上で実行
import argparse
import time
from transformers import AutoTokenizer
from optimum.onnxruntime import ORTModelForCausalLM
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--provider",
choices=[
"CPUExecutionProvider",
"CoreMLExecutionProvider",
],
default="CPUExecutionProvider",
)
parser.add_argument(
"--model-dir",
default="./models/SmolLM2-360M-Instruct-onnx",
)
parser.add_argument(
"--max-new-tokens",
type=int,
default=100,
)
args = parser.parse_args()
tokenizer = AutoTokenizer.from_pretrained(args.model_dir)
model = ORTModelForCausalLM.from_pretrained(
args.model_dir,
provider=args.provider,
)
messages = [
{
"role": "user",
"content": "Explain the difference between TCP and UDP in a few sentences.",
}
]
prompt = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer(
prompt,
return_tensors="pt",
)
prompt_tokens = inputs["input_ids"].shape[1]
start = time.perf_counter()
outputs = model.generate(
**inputs,
max_new_tokens=args.max_new_tokens,
do_sample=False,
)
elapsed = time.perf_counter() - start
generated_tokens = outputs[0][prompt_tokens:]
generated_count = len(generated_tokens)
print("=== provider ===")
print(args.provider)
print("\n=== generated ===")
print(
tokenizer.decode(
generated_tokens,
skip_special_tokens=True,
)
)
print("\n=== benchmark ===")
print(f"Prompt tokens: {prompt_tokens}")
print(f"Generated tokens: {generated_count}")
print(f"Elapsed: {elapsed:.3f} sec")
print(f"Generation: {generated_count / elapsed:.2f} tokens/sec")
if __name__ == "__main__":
main()CPU
$ python ./run_optimum.py
=== provider ===
CPUExecutionProvider
=== generated ===
TCP (Transmission Control Protocol) is a connection-oriented protocol that ensures reliable data transfer by establishing a connection between two devices before sending data. It provides a reliable and ordered delivery of data, and it is typically used for applications that require high reliability and performance, such as file transfers and web browsing.
UDP (User Datagram Protocol) is a connectionless protocol that provides fast and reliable data transfer. It does not establish a connection before sending data, and it does not guarantee delivery
=== benchmark ===
Prompt tokens: 42
Generated tokens: 100
Elapsed: 2.846 sec
Generation: 35.13 tokens/sec
CoreML
$ python run_optimum.py --provider CoreMLExecutionProvider
2026-09-13 23:37:25.168 Python[44794:41607830] 2026-09-13 23:37:25.168588 [W:onnxruntime:, coreml_execution_provider.cc:137 GetCapability] CoreMLExecutionProvider::GetCapability, number of partitions supported by CoreML: 257 number of nodes in the graph: 3812 number of nodes supported by CoreML: 1680
2026-09-13 23:37:29.481 Python[44794:41607830] 2026-09-13 23:37:29.481260 [W:onnxruntime:, session_state.cc:1397 VerifyEachNodeIsAssignedToAnEp] Some nodes were not assigned to the preferred execution providers which may or may not have an negative impact on performance. e.g. ORT explicitly assigns shape related ops to CPU to improve perf.
2026-09-13 23:37:29.481 Python[44794:41607830] 2026-09-13 23:37:29.481298 [W:onnxruntime:, session_state.cc:1399 VerifyEachNodeIsAssignedToAnEp] Rerunning with verbose output on a non-minimal build will show node assignments.
2026-09-13 23:37:30.487 Python[44794:41607830] 2026-09-13 23:37:30.487626 [E:onnxruntime:, sequential_executor.cc:671 ExecuteKernel] Non-zero status code returned while running 17211290811114605102_CoreML_17211290811114605102_0 node. Name:'CoreMLExecutionProvider_17211290811114605102_CoreML_17211290811114605102_0_0' Status Message: coreml_execution_provider.cc:222 operator() Input (past_key_values.0.key) has a dynamic shape ({-1,5,-1,64}) but the runtime shape ({1,5,0,64}) has zero elements. This is not supported by the CoreML EP.
Traceback (most recent call last):
File "/Users/thara/work/20260913/run_optimum.py", line 88, in <module>
main()
File "/Users/thara/work/20260913/run_optimum.py", line 58, in main
outputs = model.generate(
^^^^^^^^^^^^^^^
File "/Users/thara/work/20260913/.venv-onnx/lib/python3.11/site-packages/torch/utils/_contextlib.py", line 124, in decorate_context
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/thara/work/20260913/.venv-onnx/lib/python3.11/site-packages/transformers/generation/utils.py", line 2566, in generate
result = decoding_method(
^^^^^^^^^^^^^^^^
File "/Users/thara/work/20260913/.venv-onnx/lib/python3.11/site-packages/transformers/generation/utils.py", line 2786, in _sample
outputs = self(**model_inputs, return_dict=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/thara/work/20260913/.venv-onnx/lib/python3.11/site-packages/optimum/onnxruntime/base.py", line 466, in __call__
return self.forward(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/thara/work/20260913/.venv-onnx/lib/python3.11/site-packages/optimum/onnxruntime/modeling_decoder.py", line 412, in forward
onnx_outputs = self.session.run(None, onnx_inputs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/thara/work/20260913/.venv-onnx/lib/python3.11/site-packages/onnxruntime/capi/onnxruntime_inference_collection.py", line 395, in run
return self._sess.run(output_names, input_feed, run_options)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
onnxruntime.capi.onnxruntime_pybind11_state.Fail: [ONNXRuntimeError] : 1 : FAIL : Non-zero status code returned while running 17211290811114605102_CoreML_17211290811114605102_0 node. Name:'CoreMLExecutionProvider_17211290811114605102_CoreML_17211290811114605102_0_0' Status Message: coreml_execution_provider.cc:222 operator() Input (past_key_values.0.key) has a dynamic shape ({-1,5,-1,64}) but the runtime shape ({1,5,0,64}) has zero elements. This is not supported by the CoreML EP.
CPUExecutionProviderは正常動作してるけど、CoreMLExecutionProviderはなんかダメぽ。
Input (past_key_values.0.key) has a dynamic shape ({-1,5,-1,64}) but the runtime shape ({1,5,0,64}) has zero elements. This is not supported by the CoreML EP.
past_key_values.0.key はさっき見たように入力shape。 実行時shapeが{1,5,0,64} になってて、1 * 5 * 0 * 64 = 0 で空tensorになってしまっているのが原因っぽい。 初回のKV cacheが空なのはダメか。
CPUExecutionProviderはOKで、CoreMLExecutionProviderでNG、ということは、 onnxのExecutionProviderは単にハードウェアの差異を吸収するだけでなく、ONNXのグラフをどう実行するかの違いもある、ということがわかる。
CoreML動かしたかったけど、動かせなかった。 ONNX export時にcacheしないようにすればいいんだろうけど、実用的に意味がないのでやる意味なさそう。
まぁこういうこともある。 ONNXを使ってなんかやる時は、EPの実装差異に注意しないといけないかもね。