Оптимизация инференса LLM с помощью ONNX Runtime
ONNX Runtime (ORT) — универсальный инференс-движок от Microsoft, работающий на CPU, GPU и специализированных ускорителях (DirectML, TensorRT EP, ROCm). Оптимальный выбор для сред без NVIDIA GPU или при необходимости кросс-платформенного деплоя.
Когда ONNX Runtime — правильный выбор
- CPU inference: для малых и средних моделей (≤ 7B) без GPU. ORT значительно быстрее HF transformers на CPU через AVX512 оптимизации
- Edge и embedded: ARM устройства, Azure IoT, Windows on ARM
- Mixed cloud: деплой на разных облачных провайдерах с разными GPU
- Compliance: сред без NVIDIA GPU (AMD, Intel GPU, CPU-only)
- Маленькие энкодер-модели: BERT, RoBERTa для классификации и NER — ORT даёт 2–4x ускорение
Конвертация модели в ONNX
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from optimum.exporters.onnx import main_export
# Экспорт через Optimum (рекомендуется)
main_export(
model_name_or_path="cardiffnlp/twitter-roberta-base-sentiment",
output="./onnx_model/",
task="text-classification",
opset=17,
device="cuda", # экспорт с GPU для лучшей оптимизации
fp16=True # половинная точность
)
# Альтернатива: torch.onnx.export для кастомных моделей
import torch
model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-Instruct-v0.3")
dummy_input = {
"input_ids": torch.ones(1, 128, dtype=torch.long),
"attention_mask": torch.ones(1, 128, dtype=torch.long),
}
torch.onnx.export(
model,
dummy_input,
"model.onnx",
opset_version=17,
input_names=["input_ids", "attention_mask"],
output_names=["logits"],
dynamic_axes={
"input_ids": {0: "batch", 1: "seq_len"},
"attention_mask": {0: "batch", 1: "seq_len"},
}
)
INT8 Quantization для CPU
from onnxruntime.quantization import quantize_dynamic, QuantType
# Dynamic quantization — самый простой способ, не требует calibration data
quantize_dynamic(
model_input="model.onnx",
model_output="model_int8.onnx",
weight_type=QuantType.QInt8,
per_channel=True,
reduce_range=True # для старых Intel CPU
)
Результат на Intel Xeon: BERT-base inference 8ms → 3ms при <1% деградации accuracy на GLUE.
Static INT8 с calibration
from onnxruntime.quantization import quantize_static, CalibrationDataReader
class SentimentCalibrationDataReader(CalibrationDataReader):
def __init__(self, calibration_texts: list[str], tokenizer):
self.tokenizer = tokenizer
self.data = iter(calibration_texts)
def get_next(self) -> dict | None:
text = next(self.data, None)
if text is None:
return None
inputs = self.tokenizer(text, return_tensors="np", padding="max_length",
max_length=128, truncation=True)
return dict(inputs)
calibration_reader = SentimentCalibrationDataReader(
calibration_texts=load_calibration_data(), # 100-500 representativesamples
tokenizer=tokenizer
)
quantize_static(
model_input="model.onnx",
model_output="model_int8_static.onnx",
calibration_data_reader=calibration_reader,
quant_format=QuantFormat.QDQ,
per_channel=True
)
Оптимизированный инференс
import onnxruntime as ort
import numpy as np
# Конфигурация сессии
session_options = ort.SessionOptions()
session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
session_options.intra_op_num_threads = 8 # параллелизм внутри оператора
session_options.inter_op_num_threads = 2 # параллелизм между операторами
session_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
# Провайдеры: порядок важен (используется первый доступный)
providers = [
("CUDAExecutionProvider", {
"device_id": 0,
"arena_extend_strategy": "kNextPowerOfTwo",
"gpu_mem_limit": 4 * 1024 ** 3, # 4 GB
"cudnn_conv_algo_search": "EXHAUSTIVE",
}),
"CPUExecutionProvider"
]
session = ort.InferenceSession(
"model_int8.onnx",
sess_options=session_options,
providers=providers
)
def predict_batch(texts: list[str]) -> list[dict]:
inputs = tokenizer(
texts, padding=True, truncation=True,
max_length=128, return_tensors="np"
)
outputs = session.run(None, dict(inputs))
logits = outputs[0]
probs = softmax(logits, axis=1)
return [
{"label": LABELS[np.argmax(p)], "score": float(np.max(p))}
for p in probs
]
ONNX Runtime для LLM: onnxruntime-genai
Для генеративных моделей Microsoft разработал специализированный пакет:
import onnxruntime_genai as og
# Загрузка предоптимизированной модели (phi-3, llama, mistral)
model = og.Model("./phi3-mini-onnx/")
tokenizer = og.Tokenizer(model)
params = og.GeneratorParams(model)
params.set_search_options(max_length=200, temperature=0.7)
params.input_ids = tokenizer.encode("Explain ONNX Runtime")
generator = og.Generator(model, params)
while not generator.is_done():
generator.compute_logits()
generator.generate_next_token()
token = generator.get_next_tokens()[0]
print(tokenizer.decode([token]), end="", flush=True)
Производительность на CPU
На Intel Xeon Platinum 8375C (32 cores), Phi-3-mini (3.8B), batch=8:
| Конфигурация | Throughput |
|---|---|
| HF transformers (FP32) | 12 tok/s |
| ORT (FP32) | 28 tok/s |
| ORT (INT8 dynamic) | 67 tok/s |
| ORT (INT4) | 124 tok/s |







