実験日: 2026-08-24
環境: RTX 5070 Ti / WSL2 / PyTorch 2.13.0+cu130 / ONNX 1.22.0 / ONNX Runtime 1.29.0
PyTorchで学習したmodelを別のruntimeやedge deviceへ持ち出すとき、ONNXはよく使われます。しかしexport commandが成功しただけでは、出力が同じか、dynamic batchが動くか、速くなるかは分かりません。
export成功は確認の入口です。1万枚の出力が同じかは、別に検証します。
FashionMNIST CNNをこのPCで3 epoch学習し、opset 18のONNXへexportしました。10,000枚すべてでlogitを比較すると最大絶対差は5.72×10^-6、top-1 predictionの変化は0枚、accuracyは両方86.50%でした。
CPU推論はbatch 1でONNX Runtimeがわずかに遅く、batch 32で2.59倍、batch 256で3.66倍速くなりました。「ONNXにすれば必ず速い」ではなく、batchとruntime最適化を含めて測る必要があります。
- 1万枚で出力が一致した
- ONNXはweight fileだけではない
- 学習modelを固定する
- dynamic batch付きでexport
- legacy exporterのwarningを無視しない
- checkerを通してからruntimeへ渡す
- ONNX Runtime CPU sessionを作る
- 10,000枚すべてのlogitを比較
- accuracyも再計算する
- benchmarkはwarmup後100回
- batch 1ではPyTorchがわずかに速い
- batchを増やすとONNX Runtimeが速かった
- dynamic batchが動くこともtestになっている
- model deploymentで追加確認すること
- 再現手順
- 結論
- 関連記事
1万枚で出力が一致した
| 確認項目 | 結果 |
|---|---|
| test画像 | 10,000枚 |
| 最大logit絶対差 | 5.72e-6 |
| 平均logit絶対差 | 5.07e-7 |
| top-1一致率 | 100% |
| prediction変化 | 0枚 |
| PyTorch accuracy | 86.50% |
| ONNX Runtime accuracy | 86.50% |
| batch | PyTorch CPU中央値 | ONNX Runtime CPU中央値 | ORT倍率 |
|---|---|---|---|
| 1 | 0.0799 ms | 0.0851 ms | 0.94× |
| 32 | 0.4791 ms | 0.1848 ms | 2.59× |
| 256 | 3.2021 ms | 0.8744 ms | 3.66× |

ONNXはweight fileだけではない
PyTorchのstate_dictはparameter名とtensorを保存します。model classのPython codeは別に必要です。
torch.save(model.state_dict(), "fashion_cnn_state_dict.pt")
ONNX fileは、operator graph、initializer tensor、input/output definitionなどを共通形式で表します。Pythonのmodel classをそのまま配布しなくても、ONNXを理解するruntimeで実行できます。
今回のfile sizeです。
PyTorch state_dict 427,551 bytes
ONNX graph 425,562 bytes
ほぼ同じなのは大部分が同じFP32 weightだからです。ただしdeployment sizeではありません。PyTorchやONNX Runtime本体、CUDA provider、Python、OS libraryを含めていません。
学習modelを固定する
architectureはこれまでのFashionMNIST記事と同じ小型CNNです。
Conv2d 1→16, 3×3 + ReLU + MaxPool
Conv2d 16→32, 3×3 + ReLU + MaxPool
Flatten
Linear 1568→64 + ReLU
Linear 64→10
seed 42、Adam、learning rate 1e-3、batch 256でCUDA学習しました。
epoch 1 2.241 s loss 0.7327
epoch 2 1.644 s loss 0.4251
epoch 3 1.634 s loss 0.3742
学習後にCPUへ移し、eval()を呼びます。dropoutやbatch normalizationがあるmodelでは、train/eval modeを間違えるとexport前後の比較が崩れます。
model = model.cpu().eval()
dynamic batch付きでexport
dummy inputはshape (1, 1, 28, 28)です。
example = torch.zeros(1, 1, 28, 28)
torch.onnx.export(
model,
example,
"fashion_cnn_dynamic.onnx",
input_names=["image"],
output_names=["logits"],
dynamic_axes={
"image": {0: "batch"},
"logits": {0: "batch"},
},
opset_version=18,
dynamo=False,
)
batch axisをdynamicにしないと、exampleと同じbatch 1専用graphになる場合があります。今回は1、32、256の全batchを同じONNX fileへ入力できました。
opset_versionはONNX operator specificationのversionです。大きければ常に良いわけではなく、exporterとtarget runtimeの対応範囲を合わせます。
legacy exporterのwarningを無視しない
PyTorch 2.13は次のwarningを出しました。
You are using the legacy TorchScript-based ONNX export.
Starting in PyTorch 2.9, the new torch.export-based ONNX exporter
has become the default.
今回はdynamo=Falseを明示し、simple CNNを既知のlegacy pathでexportしました。記事の再現性のため、warningを消していません。
新規projectではPyTorchのONNX export documentationを確認し、dynamo=Trueの新exporterもtestすべきです。export routeが違えば対応operatorやdynamic shapeの扱いも変わります。
checkerを通してからruntimeへ渡す
fileが書けた後、ONNX checkerを実行しました。
graph = onnx.load(path)
onnx.checker.check_model(graph)
今回のgraphはIR version 8、node数10で、checkerを通過しました。
checkerはgraph schemaやtypeの整合性を検査します。しかしPyTorchと数値的に同じ出力を保証しません。そのため次に実dataでparity testします。
ONNX Runtime CPU sessionを作る
session = ort.InferenceSession(
"fashion_cnn_dynamic.onnx",
providers=["CPUExecutionProvider"],
)
今回はCPU同士を比較しました。PyTorch CUDAとONNX Runtime CPUを比べると、format差とdevice差が混ざるからです。
input名imageとoutput名logitsはexport時に固定しました。
ort_logits = session.run(
["logits"],
{"image": batch.numpy()},
)[0]
ONNX RuntimeはNumPy arrayを受け取ります。dtypeはFP32、shapeはNCHWです。
10,000枚すべてのlogitを比較
1~2 sampleの目視ではなく、test set全体を512枚ずつ実行しました。
diff = np.abs(pytorch_logits - onnx_logits)
max_abs = diff.max()
mean_abs = diff.mean()
agreement = np.mean(
pytorch_logits.argmax(1) == onnx_logits.argmax(1)
)
結果です。
max absolute difference 0.000005722
mean absolute difference 0.000000507
top-1 agreement 1.0000
changed predictions 0 / 10,000
floating-point演算はoperator fusion、計算順序、library実装で丸め差が出ます。logitのbitwise equalityを要求せず、誤差の大きさとtask-level predictionの両方を確認しました。
今回は全predictionが一致しましたが、class境界で2つのlogitが極めて近いsampleなら、数µの差でargmaxが変わる可能性があります。だから最大差だけでなくtop-1 agreementも必要です。
accuracyも再計算する
PyTorch CPU accuracy 86.50%
ONNX Runtime CPU accuracy 86.50%
top-1が10,000枚すべて一致するため、accuracyも一致します。重要なのは、accuracy 86.50%だけでは変換品質を判定できないことです。
例えば100枚のpredictionが別classへ入れ替わっても、正解→誤答と誤答→正解が同数なら全体accuracyは同じです。parityにはsample-level agreementを使います。
benchmarkはwarmup後100回
各batchで同じinputを使い、20回warmupした後100回測定しました。
for _ in range(20):
inference()
for _ in range(100):
started = time.perf_counter()
inference()
times.append(time.perf_counter() - started)
中央値、平均、p95をJSONへ保存しています。記事の表は外れ値に比較的強いmedianです。
DataLoader、file read、前処理、postprocessは含みません。すでにmemory上にあるtensor/NumPy arrayのforward latencyです。
batch 1ではPyTorchがわずかに速い
PyTorch CPU 0.0799 ms
ORT CPU 0.0851 ms
ONNX Runtimeは0.94倍、つまり約6%遅い結果です。差は約0.005 msで非常に小さく、OS schedulingや測定noiseの影響を受けます。
低latency requestでは、graph optimizationの利益よりruntime call overheadが支配することがあります。「ONNXへ変換したから速い」とは限りません。
batchを増やすとONNX Runtimeが速かった
batch 32 PyTorch 0.4791 ms ORT 0.1848 ms 2.59×
batch 256 PyTorch 3.2021 ms ORT 0.8744 ms 3.66×
batch 256のmedian throughput換算です。
PyTorch CPU 約79,947 images/s
ONNX Runtime CPU 約292,756 images/s
同じCPUでもONNX Runtimeのgraph optimizationやoperator implementationがこのCNNとbatch sizeで効いた可能性があります。CPU thread設定は明示固定していないため、libraryのdefault thread poolも差へ含まれます。
productionで比較するときはthread数、process数、power setting、NUMA、input copyまで固定します。
dynamic batchが動くこともtestになっている
export exampleはbatch 1でしたが、ONNX Runtimeへbatch 32と256を渡せました。これはdynamic axis指定がruntimeで機能した実証です。
shape flexibilityはmodel parityとは別のcontractです。可変解像度や可変sequence lengthが必要なら、それぞれのaxisをdynamicにして複数shapeでtestします。
model deploymentで追加確認すること
今回のsimple CNNは成功しましたが、実modelでは次が問題になります。
- unsupported operator
- custom autograd function
- Python control flow
- dynamic shapeの不完全な伝播
- preprocessing差
- NCHW/NHWCの取り違え
- FP16/INT8変換後のaccuracy低下
- providerごとのoperator fallback
export、checker、runtime load、parity、performanceを別stageにすると、失敗場所を切り分けられます。
PyTorch eval model
↓ export
ONNX file
↓ checker
valid graph
↓ runtime load
executable session
↓ full test parity
numerical confidence
↓ workload benchmark
deployment decision
再現手順
source /opt/ai-lab/venv/bin/activate
pip install onnx onnxruntime
cd /mnt/c/Users/user/Documents/Codex/2026-08-24/ko/work/ai_lab
python -u experiment_pytorch_onnx.py
python plot_pytorch_onnx.py
artifactです。
work/ai_lab/experiment_pytorch_onnx.py
work/ai_lab/models/fashion_cnn_state_dict.pt
work/ai_lab/models/fashion_cnn_dynamic.onnx
work/ai_lab/results/pytorch_onnx.json
work/figures/pytorch-onnx-fashionmnist.png
raw JSONにはenvironment、training、10,000枚parity、batch別100回timing、file size、graph metadataを保存しました。
結論
このFashionMNIST CNNはPyTorchからONNXへ変換しても、10,000枚のtop-1 predictionがすべて一致しました。最大logit差は5.72e-6で、task accuracyも86.50%を維持しています。
速度はbatch依存でした。batch 1はONNX Runtimeが少し遅く、batch 256では3.66倍速くなりました。format名ではなく、実際のrequest sizeでbenchmarkする必要があります。
同じtrained weightをINT8とINT4へ丸めるsimulationを行うと、model sizeの理論値だけでなく、logit誤差・prediction変化・accuracy低下まで評価できます。
