Оптимизация ML-модели (квантизация) для мобильного устройства
Квантизация — перевод весов модели из float32 в формат с меньшей разрядностью: float16, int8, int4. Модель ResNet-50 весит 98 МБ в FP32. После int8 квантизации — 25 МБ. Скорость инференса на мобильном CPU вырастает в 2–4× за счёт уменьшения объёма данных и использования целочисленных инструкций ARM NEON/SVE.
Но простая квантизация часто роняет точность сильнее, чем хотелось бы. Правильная квантизация — это подбор метода, анализ чувствительных слоёв и верификация деградации.
Типы квантизации и когда что применять
Post-Training Quantization (PTQ) — квантизуем уже обученную модель без дообучения. Два варианта:
- Dynamic quantization — веса в int8, активации вычисляются в float32 в рантайме. Простая, не требует калибровочных данных. Хорошо работает для RNN/Transformer (BERT, LLM). Для CNN даёт меньший прирост.
- Static quantization — и веса, и активации в int8. Требует calibration dataset (100–500 репрезентативных примеров). Быстрее dynamic, но нужна калибровка.
Quantization-Aware Training (QAT) — модель дообучается с «симулированной» квантизацией. Веса адаптируются к пониженной точности. Лучшее качество, но требует доступа к обучающему датасету и GPU-времени.
# PyTorch: static PTQ через torch.quantization
import torch
from torch.quantization import quantize_static, get_default_qconfig
model.eval()
model.qconfig = get_default_qconfig('fbgemm') # x86; для ARM — 'qnnpack'
torch.quantization.prepare(model, inplace=True)
# Calibration: прогоняем calibration dataset
with torch.no_grad():
for batch in calibration_loader:
model(batch)
torch.quantization.convert(model, inplace=True)
# Теперь model содержит квантизованные слои
Для мобильного Android (ARM) — qconfig = 'qnnpack', не 'fbgemm'. Это меняет порядок квантизованных операций под QNNPACK backend, который использует ARM NEON инструкции.
TFLite квантизация: full integer
# Конвертация с full int8 (активации + веса)
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
# Calibration generator — критично для точности static quantization
def representative_dataset():
for sample in calibration_data[:500]:
yield [sample.astype(np.float32)]
converter.representative_dataset = representative_dataset
tflite_model = converter.convert()
Full int8 модель работает на NNAPI и Hexagon DSP — там, где FP16 не поддерживается. На Snapdragon 778G через Hexagon — 5–8× быстрее CPU при правильной INT8 квантизации.
Core ML квантизация на iOS
import coremltools as ct
from coremltools.optimize.coreml import (
OptimizationConfig,
OpLinearQuantizerConfig,
linearly_quantize_weights
)
# Загружаем уже конвертированную Core ML модель
mlmodel = ct.models.MLModel("model_fp32.mlpackage")
# Конфигурация: 8-bit линейная квантизация весов
config = OptimizationConfig(
global_config=OpLinearQuantizerConfig(
mode="linear_symmetric",
dtype=np.int8,
granularity="per_channel" # per_channel точнее per_tensor для CNN
)
)
compressed_model = linearly_quantize_weights(mlmodel, config)
compressed_model.save("model_int8.mlpackage")
per_channel квантизация — отдельный scale factor для каждого выходного канала свёрточного слоя. Значительно точнее per_tensor (один scale на весь слой), но чуть медленнее. Для CNN обычно оправдано.
Анализ чувствительных слоёв
Не все слои одинаково переносят квантизацию. Первый и последний слои сети, а также слои attention в трансформерах — часто самые чувствительные. Инструмент: per-layer sensitivity analysis.
# Проверяем деградацию точности при квантизации каждого слоя по отдельности
from torch.quantization.quantize_fx import prepare_fx, convert_fx
baseline_accuracy = evaluate(float_model, test_loader)
for layer_name in get_all_quantizable_layers(model):
# Квантизуем только этот слой
single_layer_model = quantize_single_layer(model, layer_name)
layer_accuracy = evaluate(single_layer_model, test_loader)
sensitivity = baseline_accuracy - layer_accuracy
print(f"{layer_name}: sensitivity={sensitivity:.4f}")
Слои с высокой чувствительностью оставляем в FP32 — это mixed precision quantization. Остальные переводим в INT8. 5–10% «тяжёлых» слоёв остаются в FP32, модель теряет только 20–30% объёма вместо 75%, но точность сохраняется.
Верификация: что и как проверять
После квантизации обязательно:
-
Точность на тестовом датасете — сравниваем top-1/top-5 accuracy с оригиналом. Допустимая деградация: FP16 — <0.5%, INT8 — <2%. Если больше — переходим к QAT или mixed precision.
-
Числовая погрешность — на одинаковых входах сравниваем выходы float и quantized модели. MSE < 0.01 обычно приемлемо.
-
Скорость на реальных устройствах — не на симуляторе. Xcode Instruments → Core ML Profiler для iOS,
adb shell am instrument+ TFLite Benchmark Tool для Android. -
Краш-тест — разные входы, edge cases (чёрное изображение, очень яркое, нестандартный aspect ratio). INT8 модели иногда overflow на экстремальных входах.
Практический кейс
Модель детекции объектов YOLOv8n в FP32 — 6.3 МБ, 45 мс на iPhone 13. После Core ML INT8 квантизации — 1.8 МБ, 12 мс. mAP упал с 37.3 до 36.1 — в пределах допустимого для большинства задач. На Snapdragon 8 Gen 1 через TFLite INT8 + NNAPI — 8 мс.
Процесс
Аудит исходной модели → выбор метода (PTQ/QAT, INT8/FP16) → калибровка → анализ чувствительных слоёв → mixed precision при необходимости → верификация точности → замеры скорости на целевых устройствах.
Ориентиры по срокам
PTQ для одной модели с верификацией — 1–2 недели. QAT с полным циклом дообучения и тестированием — 3–6 недель в зависимости от размера датасета.







