0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで学習したCNNモデルをONNX Runtime (C++) で推論してみた

0
Posted at

はじめに

前回の記事では、
ONNX RuntimeをPythonで使い、推論時間0.13msという結果を得た。
PyTorch (0.20ms) やlibtorch (0.23ms) より速く、推論特化の強みが出た。

今回は同じmodel.onnxを使い、C++のONNX Runtime APIで推論してみる。
Pythonのオーバーヘッドが完全になくなったとき、速度はどう変わるか。


全体の流れ

[PyTorch環境]              [ONNX Runtime C++環境]
学習 → model.onnx保存 →   読み込み → 推論
                           ※PyTorchもPythonも不要!

前回と同じmodel.onnxをそのまま使い回せる。
これがONNXの強みだ。


環境構築

.devcontainer/devcontainer.json:

{
  "name": "ONNX Runtime C++",
  "image": "mcr.microsoft.com/devcontainers/cpp:ubuntu-24.04",
  "postCreateCommand": "bash .devcontainer/setup.sh",
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-vscode.cpptools",
        "ms-vscode.cmake-tools"
      ]
    }
  }
}

.devcontainer/setup.sh:

#!/bin/bash
# ONNX Runtime C++版をダウンロード・展開する

set -e

ORT_VERSION="1.18.0"
ORT_URL="https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/onnxruntime-linux-x64-${ORT_VERSION}.tgz"
DEST="$HOME/onnxruntime"

if [ ! -d "$DEST" ]; then
    echo "ONNX Runtimeをダウンロード中..."
    wget -q "$ORT_URL" -O /tmp/onnxruntime.tgz
    tar -xzf /tmp/onnxruntime.tgz -C "$HOME/"
    # mkdirせずに直接mvする(パスがずれないように)
    mv "$HOME/onnxruntime-linux-x64-${ORT_VERSION}" "$DEST"
    rm /tmp/onnxruntime.tgz
    echo "ONNX Runtimeのセットアップ完了: $DEST"
else
    echo "ONNX Runtimeは既にセットアップ済みです: $DEST"
fi

ビルド設定

CMakeLists.txt:

cmake_minimum_required(VERSION 3.18)
project(onnx_inference)

set(CMAKE_CXX_STANDARD 17)

# ONNX Runtimeのパス
set(ORT_ROOT "$ENV{HOME}/onnxruntime")

# インクルードパスとライブラリの設定
include_directories(${ORT_ROOT}/include)
link_directories(${ORT_ROOT}/lib)

add_executable(inference inference.cpp)
target_link_libraries(inference onnxruntime)

推論コード

inference.cpp:

#include <onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>
#include <chrono>
#include <random>

int main() {
    // ONNX Runtimeの初期化
    Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "onnx_inference");
    Ort::SessionOptions session_options;
    session_options.SetIntraOpNumThreads(1);

    // モデルの読み込み
    Ort::Session session(env, "model.onnx", session_options);
    std::cout << "モデルを読み込みました: model.onnx" << std::endl;

    // 入出力の名前を取得
    Ort::AllocatorWithDefaultOptions allocator;
    auto input_name_ptr  = session.GetInputNameAllocated(0, allocator);
    auto output_name_ptr = session.GetOutputNameAllocated(0, allocator);
    const char* input_name  = input_name_ptr.get();
    const char* output_name = output_name_ptr.get();

    // ダミー入力を準備(MNISTと同じサイズ: 1x1x28x28)
    std::vector<int64_t> input_shape = {1, 1, 28, 28};
    size_t input_size = 1 * 1 * 28 * 28;

    std::vector<float> input_data(input_size);
    std::mt19937 rng(42);
    std::normal_distribution<float> dist(0.0f, 1.0f);
    for (auto& v : input_data) v = dist(rng);

    auto memory_info = Ort::MemoryInfo::CreateCpu(
        OrtArenaAllocator, OrtMemTypeDefault
    );
    Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
        memory_info,
        input_data.data(), input_size,
        input_shape.data(), input_shape.size()
    );

    std::vector<const char*> input_names  = {input_name};
    std::vector<const char*> output_names = {output_name};

    // ウォームアップ(前回と条件を揃える)
    session.Run(
        Ort::RunOptions{nullptr},
        input_names.data(), &input_tensor, 1,
        output_names.data(), 1
    );
    std::cout << "ウォームアップ完了" << std::endl;

    // 推論時間を計測(100回平均)
    const int N = 100;
    auto start = std::chrono::high_resolution_clock::now();

    std::vector<Ort::Value> output_tensors;
    for (int i = 0; i < N; i++) {
        output_tensors = session.Run(
            Ort::RunOptions{nullptr},
            input_names.data(), &input_tensor, 1,
            output_names.data(), 1
        );
    }

    auto end = std::chrono::high_resolution_clock::now();
    double elapsed_ms =
        std::chrono::duration<double, std::milli>(end - start).count() / N;

    // 結果の表示
    float* output_data = output_tensors[0].GetTensorMutableData<float>();
    int predicted = std::max_element(output_data, output_data + 10) - output_data;

    std::cout << "予測クラス: " << predicted << std::endl;
    std::cout << "推論時間(" << N << "回平均): " << elapsed_ms << " ms" << std::endl;

    return 0;
}

ビルドと実行:

mkdir build && cd build
cmake ..
make
./inference

実行結果・4者比較

実行環境:Ryzen 5 7530U(CPUのみ)、ウォームアップあり、100回平均

実装 推論時間(100回平均) Pythonとの比較
Python (PyTorch) 0.20 ms 基準
C++ (libtorch) 0.23 ms 1.15倍遅い
ONNX Runtime (Python) 0.13 ms 1.54倍速い
ONNX Runtime (C++) 0.04 ms 5倍速い!

圧倒的な差が出た。


なぜここまで速いのか

同じmodel.onnxを使っているのに、PythonとC++でこれほど差が出る理由は何か。

Pythonオーバーヘッドの排除
Python版はループのたびにインタープリタを介する。
C++版はそのオーバーヘッドが完全にゼロだ。

ONNX Runtimeのグラフ最適化
ONNX Runtimeは推論に特化した最適化を自動で行う。
C++環境ではその最適化が余すことなく効く。

メモリ管理の効率化
PythonはGCの影響を受けるが、C++では直接制御できる。
100回ループでのメモリ割り当て・解放のコストが大きく違う。


まとめ

4回の比較を振り返ると:

実装 速度 主なユースケース
Python (PyTorch) 0.20 ms 学習・開発・検証
C++ (libtorch) 0.23 ms Pythonが使えない環境
ONNX Runtime (Python) 0.13 ms 軽量・クロス言語デプロイ
ONNX Runtime (C++) 0.04 ms 速度重視の本番環境

速度だけを追うならONNX Runtime (C++) が圧倒的だ。
ただし開発・検証のしやすさを考えるとPython (PyTorch) が現実的な選択肢になる。

「何のために推論するか」でツールを選ぶのが正解だと思う。


おわりに・次回予告

次回はONNX Runtime (C#) での推論を試す予定。
同じmodel.onnxがC#でも使えるか、速度はどう変わるか確認していく。
NuGetで簡単にインストールできる点も見どころだ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?