Развертывание нейросетевых моделей в production-среде — критически важный этап ML-пайплайна. Когда речь заходит о встраивании в C++ приложения (будь то высоконаРазвертывание нейросетевых моделей в production-среде — критически важный этап ML-пайплайна. Когда речь заходит о встраивании в C++ приложения (будь то высокона

Инференс нейросетевых моделей для табличных данных с помощью ONNX Runtime на C++

Развертывание нейросетевых моделей в production-среде — критически важный этап ML-пайплайна. Когда речь заходит о встраивании в C++ приложения (будь то высоконагруженные сервисы, desktop-софт или встраиваемые системы), выбор инструментария сужается. Прямое использование фреймворков вроде PyTorch или TensorFlow часто избыточно и приводит к зависимостям, сложностям сборки и излишнему потреблению памяти.

ONNX Runtime (ORT) — это высокопроизводительный движок для выполнения моделей в формате Open Neural Network Exchange (ONNX). Он предлагает оптимизированные реализации для CPU и GPU, поддержку различных аппаратных ускорителей и, что ключевое, простой C++ API. В этой статье мы разберем, как выполнить инференс модели для табличных данных, используя ONNX Runtime в C++ проекте.

Ссылка для скачивания: Библиотеку можно получить через официальный GitHub (сборка из исходников). Но для простоты часто достаточно забрать предсобранные бинарники из релизов.

Преимущества ONNX Runtime перед альтернативами

Сравнение с TensorRT

NVIDIA TensorRT — мощный фреймворк для инференса, но с ключевыми ограничениями:

  • Жесткая привязка к железу NVIDIA: Не работает на CPU, AMD или других GPU

  • Сложность портирования: Требует компиляции модели под конкретную GPU

  • Переконвертация: Модель из ONNX → TensorRT может требовать дополнительной настройки

ONNX Runtime в этом плане универсален:

  • Кросс-платформенность: Один формат модели работает на CPU, NVIDIA GPU, AMD GPU (через ROCm), Intel (OpenVINO), Arm NPU и других акселераторах

  • Гибкость деплоя: Можете разрабатывать на CPU, а в продакшне переключиться на GPU изменением одной опции

  • Единый пайплайн: Одна модель → один формат → множество устройств

ORT оптимизирован именно для инференса:

  • Минимальный footprint: Библиотека на порядок легче полного фреймворка

  • Статическая компиляция графа: максимальная производительность за счет предварительных оптимизаций

  • Чистый inference-ориентированный API: Все то, что нужно для предсказаний

Ключевые сущности ONNX Runtime C++ API

Перед работой с ONNX Runtime необходимо понять его архитектуру и основные абстракции. Вот детальный разбор ключевых сущностей, которые составляют основу любого инференс-приложения на C++.

1. Ort::Env - глобальное окружение

Что это: Корневой объект, представляющий среду выполнения ONNX Runtime. Это синглтон на уровне процесса, который инициализирует внутренние системы ORT: менеджер памяти, систему логирования, реестр провайдеров.

Создается один раз при старте приложения. Не создавайте несколько Env объектов - это пустая трата ресурсов и может привести к неопределенному поведению.

Конструктор:

// Уровни логирования от наиболее подробного к наименее: // ORT_LOGGING_LEVEL_VERBOSE - для отладки, очень много логов // ORT_LOGGING_LEVEL_INFO - информационные сообщения // ORT_LOGGING_LEVEL_WARNING - предупреждения (рекомендуемый уровень по умолчанию) // ORT_LOGGING_LEVEL_ERROR - только ошибки // ORT_LOGGING_LEVEL_FATAL - только критические ошибки Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "MyApplication"); // Второй параметр - логин-префикс для фильтрации в логах

Важные нюансы:

  • Env должен жить дольше всех сессий, созданных в его контексте

  • В многопоточных приложениях доступ к Env потокобезопасен

2. Ort::SessionOptions - тонкая настройка сессии

Что это: Конфигурационный объект, который определяет как будет выполняться модель. Это самый важный объект для оптимизации производительности.

Основные категории настроек:

Категория

Методы

Влияние на производительность

Параллелизм

SetIntraOpNumThreads(), SetInterOpNumThreads(), SetExecutionMode()

До 300% на многоядерных CPU

Оптимизации

SetGraphOptimizationLevel(), EnableCpuMemArena()

20-50% ускорение

Провайдеры

AppendExecutionProvider_CUDA(), AppendExecutionProvider_OpenVINO()

5-100x на GPU/NPU

Память

DisableMemPattern(), EnableCpuMemArena()

Стабильность vs скорость

Пример продвинутой конфигурации:

Ort::SessionOptions options; // Оптимизация для inference (убирает dropout, объединяет операции) options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED); // Параллелизм: 4 потока для операций, 2 для независимых ветвей графа options.SetIntraOpNumThreads(4); options.SetInterOpNumThreads(2); options.SetExecutionMode(ExecutionMode::ORT_PARALLEL); // Для embedded систем с ограниченной памятью: options.DisableMemPattern(); // Отключаем динамическое выделение памяти options.DisableCpuMemArena(); // Фиксированный размер памяти // Включаем профилирование (замедляет на 5-10%, но дает детальную статистику) options.EnableProfiling("model_execution_profile"); // Документация по всем опциям: // https://onnxruntime.ai/docs/api/c/group___global.html // https://onnxruntime.ai/docs/execution-providers/

3. Ort::Session - загруженная модель

Что это: Объект, представляющий загруженную и оптимизированную модель. При создании сессии:

  1. Загружается ONNX-файл

  2. Проверяется корректность графа

  3. Применяются оптимизации (fusion операций, удаление ненужных узлов)

  4. Выбирается лучший провайдер для каждой операции

  5. Выделяется память под промежуточные тензоры

Конструкторы:

// Из файла Ort::Session session(env, "model.onnx", options); // Из буфера в памяти (удобно для зашитых в бинарник моделей) std::vector<uint8_t> model_data = loadModelData(); Ort::Session session(env, model_data.data(), model_data.size(), options); // С пользовательскими путями (для mobile/embedded) Ort::Session session(env, model_path, options);

Ключевые методы:

// Получить информацию о входах/выходах модели size_t num_inputs = session.GetInputCount(); size_t num_outputs = session.GetOutputCount(); // Получить метаданные входного тензора i Ort::TypeInfo type_info = session.GetInputTypeInfo(i); Ort::TensorTypeAndShapeInfo tensor_info = type_info.GetTensorTypeAndShapeInfo(); ONNXTensorElementDataType type = tensor_info.GetElementType(); // например ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT std::vector<int64_t> shape = tensor_info.GetShape(); // например {1, 6} // Имя входа/выхода по индексу char* input_name = session.GetInputName(i, allocator);

4. Ort::Value - тензор с данными

Что это: Умный контейнер для данных, передаваемых в модель и получаемых из нее. Основные особенности:

  • Автоматическое управление памятью (RAII)

  • Поддержка CPU и GPU памяти через единый интерфейс

  • Информация о форме (shape) и типе данных (dtype)

Создание тензоров:

// 1. Тензор из существующего буфера (без копирования) std::vector<float> input_data(6, 0.0f); Ort::Value tensor = Ort::Value::CreateTensor<float>( memory_info, // Ort::MemoryInfo input_data.data(), // указатель на данные input_data.size(), // общее количество элементов shape.data(), // форма {1, 6} shape.size() // ранг (2) ); // 2. Тензор с собственной памятью (ORT управляет памятью) Ort::Value tensor = Ort::Value::CreateTensor<float>( memory_info, shape.data(), shape.size() ); // затем копируем данные: float* tensor_data = tensor.GetTensorMutableData<float>(); std::copy(input_data.begin(), input_data.end(), tensor_data); // 3. Тензор на GPU (требуется GPU провайдер) Ort::MemoryInfo gpu_mem_info("Cuda", OrtArenaAllocator, 0, OrtMemTypeDefault); Ort::Value gpu_tensor = Ort::Value::CreateTensor<float>(gpu_mem_info, shape.data(), shape.size());

Методы доступа:

// Проверка, что это тензор bool is_tensor = tensor.IsTensor(); // Получить информацию Ort::TensorTypeAndShapeInfo info = tensor.GetTensorTypeAndShapeInfo(); std::vector<int64_t> shape = info.GetShape(); size_t element_count = info.GetElementCount(); // Доступ к данным const float* data = tensor.GetTensorData<float>(); // только чтение float* mutable_data = tensor.GetTensorMutableData<float>(); // для записи // Для GPU: получить указатель на устройство // (требует кастомного аллокатора)

  1. Данные в Ort::Value должны жить дольше вызова Run()

  2. GPU память освобождается автоматически при разрушении Ort::Value

Важные нюансы:

  • Данные в Ort::Value должны жить дольше вызова Run()

  • GPU память освобождается автоматически при разрушении Ort::Value

5. Ort::MemoryInfo - где живут данные

Что это: Дескриптор, описывающий местонахождение и способ выделения памяти. Это критически важная абстракция для гетерогенных вычислений.

Создание:

// CPU память (самый частый случай) Ort::MemoryInfo cpu_mem_info = Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, // Использовать arena (быстрее) OrtMemTypeCPU // Обычная CPU память ); // CPU память без arena (для embedded/real-time) Ort::MemoryInfo cpu_mem_info_no_arena = Ort::MemoryInfo::CreateCpu( OrtDeviceAllocator, // Прямое выделение OrtMemTypeCPU ); // GPU память (если есть CUDA/ROCM провайдер) Ort::MemoryInfo cuda_mem_info = Ort::MemoryInfo::CreateCpu( "Cuda", // Имя провайдера OrtArenaAllocator, 0, // Device ID OrtMemTypeDefault // Память устройства по умолчанию );

Особенности для GPU:

// При использовании CUDA провайдера: // 1. Входные данные могут быть в CPU памяти - ORT сам скопирует // 2. Для zero-copy лучше создавать тензоры сразу в GPU памяти // 3. MemoryInfo для входов и выходов может различаться // Пример: модель работает на GPU, но выход хотим получить на CPU std::vector<const char*> output_names = {"output"}; std::vector<Ort::Value> outputs = session.Run( run_options, input_names.data(), input_tensors.data(), input_tensors.size(), output_names.data(), output_names.size() ); // outputs[0] будет в CPU памяти, даже если вычисления на GPU // ORT автоматически выполнил cudaMemcpy Device→Host

6. Ort::RunOptions - параметры выполнения

Что это: Настройки для конкретного вызова Session::Run. В отличие от SessionOptions, которые настраивают сессию глобально, RunOptions позволяют контролировать отдельный запуск.

Основные сценарии использования:

Ort::RunOptions run_options; // 1. Логирование конкретного запуска run_options.SetRunLogVerbosityLevel(ORT_LOGGING_LEVEL_VERBOSE); run_options.SetRunTag("inference_batch_42"); // Метка для поиска в логах // 2. Обработка прерываний (для interactive приложений) run_options.SetTerminate(); // Асинхронная остановка выполнения // 3. Профилирование только этого запуска run_options.AddConfigEntry("profiling.enable", "1"); // 4. Выбор конкретного провайдера для этого запуска // (если сессия поддерживает несколько) run_options.AddConfigEntry("execution_provider_preference", "CUDA:0;CPU:1");

7. Ort::Allocator - управление памятью

Что это: Низкоуровневый интерфейс для управления памятью. Обычно используется ORT внутренне, но доступен для продвинутых сценариев.

Когда нужен:

  • Кастомные аллокаторы для embedded систем

  • Shared memory между процессами

  • Memory-mapped файлы для больших моделей

Пример пользовательского аллокатора:

class CustomAllocator : public OrtAllocator { public: void* Alloc(size_t size) override { return my_memory_pool.allocate(size); } void Free(void* p) override { my_memory_pool.free(p); } const OrtMemoryInfo* GetInfo() const override { return Ort::MemoryInfo::CreateCpu("Custom", OrtCustomAllocator, 0, OrtMemTypeCPU); } }; // Использование в сессии CustomAllocator custom_allocator; options.AddConfigEntry("session.use_custom_allocator", "1");

Подготовка модели и ее изучение в Netron

Допустим, у нас уже есть готовая модель, например, для прогнозирования на основе 6 признаков, сохраненная как model.onnx. Первый шаг — понять ее сигнатуру: имена входных и выходных узлов, а также форму входных данных.

Для визуализации и изучения архитектуры ONNX моделей существует отличное бесплатное приложение Netron (https://netron.app/). У него также есть свой репозитория (https://github.com/lutzroeder/netron). Просто открываем в нем файл *.onnx и видим граф вычислений. Нас интересуют:

  1. Входной узел (Input): Обычно имеет имя (например, "input") и форму (например, [batch_size, 6]).

  2. Выходной узел (Output): Также имеет имя (например, "output").

Именно по этим именам ORT будет искать тензоры в графе модели. Запишем их в константы в нашем коде.

Практика: Классы для инференса на C++

Рассмотрим реализацию двух классов, которые инкапсулируют работу с ONNX Runtime.

Базовый класс BaseNeuralModel

#pragma once #include <cmath> #include <string> #include <vector> #include <onnxruntime/onnxruntime_cxx_api.h> // Подключаем C++ API ORT class BaseNeuralModel { public: BaseNeuralModel( const Ort::Env & env, Ort::SessionOptions network_session_options, std::string network_path, std::vector<float> offset_data, std::vector<float> scale_data, std::vector<std::int64_t> input_shape) noexcept : network_session_options_(network_session_options), network_path_(network_path), // Создание сессии. // Сессия загружает модель из файла, проводит оптимизации графа // и готовит его к выполнению на выбранном провайдере (CPU/GPU). session_(env, network_path_.c_str(), network_session_options_), offset_data_(offset_data), scale_data_(scale_data), input_shape_(input_shape), input_data_(input_shape_.back(), 0.0F), // Создание входного тензора. // Ort::Value - обертка ORT для тензора. // CreateTensor не копирует данные, а использует переданный указатель (input_data_.data()). // Важно: данные должны жить дольше, чем этот тензор. input_tensor_(Ort::Value::CreateTensor<float>( mem_info_, input_data_.data(), input_data_.size(), input_shape_.data(), input_shape_.size())) {} BaseNeuralModel() = delete; virtual ~BaseNeuralModel() = default; [[nodiscard]] virtual float update(const std::vector<InputParam> & input) noexcept = 0; private: [[nodiscard]] virtual float calcNeural() noexcept = 0; protected: Ort::SessionOptions network_session_options_; std::string network_path_; Ort::Session session_; // Основной объект для выполнения модели // Имена входного и выходного узлов. // Должны точно совпадать с именами, увиденными в Netron. static constexpr auto input_name_onnx_ = "input"; static constexpr auto output_name_onnx_ = "output"; std::vector<float> offset_data_; std::vector<float> scale_data_; std::vector<std::int64_t> input_shape_; std::vector<float> input_data_; // Буфер для входных данных // Информация о памяти. // Указывает ORT, где размещать/искать тензоры (CPU память в данном случае). Ort::MemoryInfo mem_info_ = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeCPU); Ort::RunOptions run_opts_; // Опции для выполнения (можно оставить по умолчанию) Ort::Value input_tensor_; // Тензор, связанный с буфером input_data_ };

Реализация конкретной модели NeuralModel

#pragma once #include "base_neural_model.hpp" class NeuralModel final : public BaseNeuralModel { public: using BaseNeuralModel::BaseNeuralModel; ~NeuralModel() override = default; [[nodiscard]] float update(const std::vector<InputParam> & input) noexcept override { // Предобработка данных. // Чаще всего данные нужно нормализовать. Здесь применяется Scale-Shift. // Вычисления происходят в буфере input_data_, на который ссылается input_tensor_. for (std::size_t i = 0; i != 3; ++i) { input_data_[i] = (input[i].target_acc - offset_data_[i]) * scale_data_[i]; } const auto & input3 = input[3]; input_data_[3] = (input3.target_acc - offset_data_[3]) * scale_data_[3]; input_data_[4] = (input3.curr_vel - offset_data_[4]) * scale_data_[4]; input_data_[5] = (input3.curr_acc - offset_data_[5]) * scale_data_[5]; // Запуск метода инференса. return calcNeural(); } private: [[nodiscard]] float calcNeural() noexcept override { // Вызов Session::Run - точка запуска модели. // Метод принимает имена узлов и соответствующие им тензоры. // Возвращает вектор Ort::Value с результатами. auto output = session_.Run(run_opts_, &input_name_onnx_, &input_tensor_, 1, // 1 входной тензор &output_name_onnx_, 1); // 1 выходной тензор // ПОЛУЧЕНИЕ РЕЗУЛЬТАТА. // Извлекаем сырые данные из выходного тензора. return output.front().GetTensorMutableData<float>()[0]; } };

Настройка сессии: Ort::SessionOptions

Тонкая настройка сессии — залог производительности и предсказуемого потребления памяти. ORT предоставляет богатый набор опций. Вот часть из них:

void configureSession(Ort::SessionOptions & options) { // Уровни оптимизации графа (по нарастанию агрессивности): // ORT_DISABLE_ALL, ORT_ENABLE_BASIC, ORT_ENABLE_EXTENDED, ORT_ENABLE_ALL options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED); // Параллелизм на CPU: options.SetIntraOpNumThreads(4); // Потоки внутри операций (MatMul, Conv) options.SetInterOpNumThreads(2); // Потоки между независимыми операциями options.SetExecutionMode(ExecutionMode::ORT_PARALLEL); // ORT_SEQUENTIAL для детерминизма // Отключение паттернов памяти (важно для real-time): // Без этого ORT может аллоцировать память "умно", но с непредсказуемыми задержками options.DisableMemPattern(); // Использование arena-аллокатора (по умолчанию включено): // Ускоряет выделение памяти за счет пула предварительных аллокаций options.EnableCpuMemArena(); // Или DisableCpuMemArena() для полного контроля // Логирование // Уровни логирования: // ORT_LOGGING_LEVEL_VERBOSE, INFO, WARNING, ERROR, FATAL options.SetLogSeverityLevel(ORT_LOGGING_LEVEL_WARNING); // Идентификатор сессии для логов: options.SetLogId("MyModelSession"); // Профилирование производительности: options.EnableProfiling("profile.json"); // Сохранит timeline выполнения }

Инференс на GPU: что изменится?

Переход с CPU на GPU в ORT требует пересборки библиотеки с поддержкой CUDA (или ROCm), либо использования готовых бинарников с GPU-поддержкой. Изменения в коде минимальны:

  1. Добавление провайдера выполнения: В SessionOptions нужно добавить соответствующий провайдер (например, CUDAExecutionProvider). Делается это через options.AppendExecutionProvider_CUDA(...). Для GPU обычно не задают количество потоков вручную, как для CPU.

  2. Память: Тензоры автоматически будут создаваться в GPU-памяти, если использовать соответствующий MemoryInfo. ORT также поддерживает копирование данных с CPU на GPU "под капотом", если передать CPU тензор в сессии с GPU провайдером.

  3. Асинхронность: GPU-провайдеры часто поддерживают асинхронное выполнение, что позволяет совмещать вычисления на GPU с подготовкой данных на CPU.

Вывод

ONNX Runtime предоставляет элегантный и мощный C++ API для инференса нейросетевых моделей. Он позволяет:

  • Избавиться от зависимостей от тяжелых ML-фреймворков в продакшн-коде.

  • Достичь высокой производительности за счет оптимизированных ядер и гибкой настройки сессии.

  • Унифицировать процесс развертывания моделей из разных фреймворков через единый ONNX формат.

  • Легко переключаться между CPU и GPU выполнениями с минимальными изменениями кода.

Представленный каркас классов можно легко адаптировать под любую табличную модель, изменив логику предобработки в методе update, параметры нормализации и тип тензора Ort::MemoryInfo. Это делает подход идеальным для встраивания ML-моделей в высоконагруженные C++ приложения, где важны контроль над памятью, потоками и детерминированное поведение.

Источник

Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу service@support.mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.

Вам также может быть интересно

Solana: вот где может сформироваться следующее крупное дно

Solana: вот где может сформироваться следующее крупное дно

Solana (SOL) испытывает растущее техническое давление после потери критической зоны поддержки $103, при этом ценовое действие теперь отражает явное изменение краткосрочной рыночной динамики
Поделиться
Ethnews2026/02/01 19:13
Tesla обгоняет Биткоин по рыночной капитализации, меняя глобальную инвестиционную картину

Tesla обгоняет Биткоин по рыночной капитализации, меняя глобальную инвестиционную картину

Tesla превосходит Bitcoin по рыночной капитализации, отмечая исторический момент для мировых рынков Tesla обогнала Bitcoin по общей рыночной капитализации, отмечая
Поделиться
Hokanews2026/02/01 19:36
Перевод кита Ethereum: Ошеломляющее движение ETH на $243 миллиона на Binance вызывает рыночные спекуляции

Перевод кита Ethereum: Ошеломляющее движение ETH на $243 миллиона на Binance вызывает рыночные спекуляции

BitcoinWorld Перевод крупного держателя Ethereum: Ошеломляющее перемещение ETH на сумму 243 миллиона $ в Binance вызывает рыночные спекуляции В ошеломляющем событии, которое привлекло внимание мирового криптовалютного
Поделиться
bitcoinworld2026/02/01 18:55