Квантизация
Квантизация в LibreYOLO работает целиком в PyTorch: model.quantize() подменяет модули Conv2d и Linear модели квантизованными эквивалентами и калибрует их. Результат сохраняет обычный контракт predict, val, train и save, поэтому квантизованная модель оценивается теми же валидаторами, что и float-модель.
- Вызов
model.quantize(recipe="int8", calib="coco128.yaml")- Команда
libreyolo quantize --model M.pt --recipe int8 --calib coco128.yaml- Дополнительно
- Не нужно. Квантизация работает в PyTorch.
- Семейства
- yolo9, rfdetr, birefnet, feynobg
- Рецепты
fp16, bf16, fp8, int8, w4a16, w4a8, nvfp4, mxfp4, int2- Артефакты развёртывания
export(format="pt") — упакованный чекпойнт, export(format="onnx") — QDQ-граф INT8
Установка
Квантизации не нужен extra. Подмена модулей, проход калибровки и симулируемая
арифметика работают в PyTorch, так что pip install libreyolo — это всё, что
требуется. Артефактам развёртывания нужно то, что нужно их собственному формату:
для пути через ONNX это libreyolo[onnx].
Квантизация
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt") # Подмена структуры плюс калибровка. calib — небольшой НЕРАЗМЕЧЕННЫЙ набор# изображений, он читается только вперёд, чтобы вывести диапазоны активаций и масштабы.qmodel = model.quantize(recipe="int8", calib="coco128.yaml", samples=128) print(qmodel.quant_info())qmodel.val(data="coco8.yaml") # те же валидаторы, что и для float-моделиqmodel.save("LibreYOLO9s-int8.pt") # чекпойнт несёт quant-манифестlibreyolo quantize --model LibreYOLO9s.pt --recipe int8 --calib coco128.yamlmodel.quantize( recipe="int8", calib="coco128.yaml", # путь к data.yaml или встроенное имя; None пропускает калибровку samples=128, # максимум изображений для калибровки batch=8, # размер батча калибровки algorithm="auto", # auto и minmax — одно и то же; альтернатива — percentile keep_high_precision=None, # None использует политику семейства verbose=True,)quantize() преобразует загруженную модель на месте и возвращает её. Градиенты
не задействованы: подмена ставит квантизованные модули, а проход калибровки идёт
только вперёд.
Получившийся чекпойнт — обычный чекпойнт LibreYOLO с приложенным манифестом
quant, поэтому он загружается обратно с нетронутыми структурой и масштабами:
from libreyolo import LibreYOLO # quant-манифест восстанавливает квантизованную структуру и масштабы# ещё до загрузки весов.qmodel = LibreYOLO("LibreYOLO9s-int8.pt")print(qmodel.quant_info())Чекпойнты, которые тренер пишет во время QAT-прогона, тоже несут манифест, а
значит best.pt из такого прогона сам является квантизованным чекпойнтом.
Рецепты
Поддерживаются четыре семейства: yolo9, rfdetr, birefnet и feynobg.
| Рецепт | Что делает | Семейства | Калибровка |
|---|---|---|---|
fp16 | Приведение к половинной точности с контрактом входа и выхода в float32. Только инференс. | все четыре | не нужна |
bf16 | Приведение к bfloat16, который сохраняет диапазон экспоненты float32. Решение на случай, когда fp16 переполняется на модели в стиле DETR. Только инференс. | все четыре | не нужна |
fp8 | Веса и активации E4M3 на Conv2d и Linear: поканальные масштабы весов, откалиброванные потензорные масштабы активаций. | все четыре | нужна |
int8 | W8A8 на Conv2d и Linear: поканальные симметричные веса, потензорные аффинные активации. | все четыре | нужна, либо calib=None только для весов |
w4a16 | Сгруппированные симметричные веса INT4, группа 128 вдоль in_features, float-активации, на Linear. | rfdetr, birefnet, feynobg | не требуется |
w4a8 | Сгруппированные веса INT4 плюс откалиброванные активации INT8, на Linear. | rfdetr, birefnet, feynobg | нужна |
nvfp4 | W4A4 NVFP4 на Linear: элементы E2M1, блоки по 16 элементов, масштабы блоков в FP8 E4M3, масштаб тензора в FP32. Динамическое масштабирование активаций. | rfdetr, birefnet, feynobg | не требуется |
mxfp4 | OCP MXFP4 на Linear: элементы E2M1, блоки по 32 элемента, масштабы блоков E8M0 — степени двойки. Динамическое масштабирование активаций. | rfdetr, birefnet, feynobg | не требуется |
int2 | Только для исследований: сгруппированные 2-битные веса, группа 64, плюс активации INT8, на Linear. Одна лишь посттренировочная квантизация непригодна, поэтому нужен QAT или QAD. | rfdetr | нужна |
Рецепты ниже 8 бит нацелены на nn.Linear и намеренно отклоняются для yolo9:
на нынешнем железе это ускорение работает только для GEMM, поэтому свёртки
остаются в более высокой точности. Для YOLO9 берутся int8 или fp8. int2
отклоняется для birefnet и feynobg, потому что эти семейства работают только
на инференс, а значит восстановления через QAT, на которое опирается рецепт, там
нет.
Умолчания каждого семейства оставляют первый слой и головы во float, а свёртка
DFL в YOLO9 не квантизуется никогда: это фиксированный оператор интегрального
математического ожидания. Переопределяйте через keep_high_precision=("head.",),
если есть причина.
Калибровочные данные — не обучающие данные
calib= принимает несколько сотен изображений, не читает меток и делает только
прямой проход, чтобы оценить диапазоны активаций. data= в train() и val() —
это размеченный датасет для градиентов и метрик. Это разные аргументы с разными
задачами, и по умолчанию calib равен coco128.yaml.
algorithm="minmax" сохраняет абсолютные экстремумы, встреченные по
калибровочным батчам, и именно его выбирает "auto". "percentile" берёт
среднее из 0.1 и 99.9 перцентилей по каждому батчу; измерения показали, что он
обрушивает точность семейства DETR, потому что выбросы в активациях
трансформера несут нагрузку. Чувствительность маленьких моделей к INT8 на самом
деле лечится калибровкой на достаточном числе батчей: с умолчанием coco128
YOLO9-t укладывается примерно в один пункт mAP от своего float-результата.
Выбранный алгоритм записывается в манифест чекпойнта.
Восстановление точности
from libreyolo import LibreYOLO qmodel = LibreYOLO("LibreYOLO9s-int8.pt") # Это дообучение, а не запуск с нуля: берите скорости обучения для дообучения.qmodel.train(data="coco8.yaml", epochs=5, lr0=1e-4)qmodel.train( data="coco8.yaml", epochs=5, lr0=1e-4, distill_model="LibreYOLO9m.pt",)libreyolo train --model LibreYOLO9s-int8.pt --data coco8.yaml --epochs 5 --lr0 1e-4Квантизованные модули хранят мастер-веса fp32 и применяют фейковую квантизацию со straight-through estimator, поэтому градиенты доходят до мастер-весов, а существующие тренеры работают без изменений: EMA, AMP, возобновление с чекпойнта и аргументы дистилляции сочетаются друг с другом.
QAT — это дообучение уже обученной модели. Берите скорости обучения для
дообучения, а не умолчания для обучения с нуля, иначе короткий прогон уничтожит
предобученные веса независимо от квантизации. Доступность QAD следует за
поддержкой дистилляции в семействе, а это сегодня yolo9 и rfdetr.
Модели, квантизованные в fp16 и bf16, работают только на инференс, и тренер
отклоняет их, указывая на amp=True.
Экспорт
from libreyolo import LibreYOLO qmodel = LibreYOLO("LibreYOLO9s-int8.pt") # Записывает LibreYOLO9s-int8-final.pt: упакованные низкобитные веса и масштабы,# мастер-веса fp32 выброшены, неквантизованный остаток приведён к fp16.qmodel.export(format="pt") # remainder="fp32" сохраняет неквантизованные тензоры точными.qmodel.export(format="pt", remainder="fp32")from libreyolo import LibreYOLO qmodel = LibreYOLO("LibreYOLO9s-int8.pt") # Пары QuantizeLinear/DequantizeLinear прямо в графе, несущие собственные# масштабы модели — откалиброванные или обученные в QAT.qmodel.export(format="onnx")libreyolo export --model LibreYOLO9s-int8.pt --format onnxformat="pt" кристаллизует модель. Упакованные низкобитные веса и масштабы
заменяют мастер-веса, а неквантизованный остаток приводится к fp16, если не
передан remainder="fp32". Инвариант упаковки в том, что распаковка бит в бит
воспроизводит симуляцию на том устройстве, где вы финализировали модель,
поэтому финализированный файл даёт ровно тот результат, который вы проверили на
валидации. Измерено: YOLO9-s int8 уменьшается с 29.5 МБ до 9.6 МБ, RF-DETR-n
nvfp4 — со 122 МБ до 26 МБ. Загрузка такого файла даёт готовую к инференсу
модель, а вызов train() на ней автоматически восстанавливает мастер-веса из
упакованных.
format="onnx" применим к моделям int8 и выдаёт QDQ-граф с собственными
масштабами модели — откалиброванными или обученными в QAT, — которые ONNX
Runtime и TensorRT исполняют настоящими INT8-ядрами. Это другой путь, не тот,
что export(format="onnx", int8=True) на float-модели, где
ONNX Runtime выводит масштабы сам.
Рецептам-приведениям квантизованный экспортёр не нужен вовсе:
from libreyolo import LibreYOLO qmodel = LibreYOLO("LibreYOLO9s-int8.pt")qmodel.dequantize() # Теперь подходит любой float-экспортёр, с любой поддерживаемой им точностью.qmodel.export(format="tensorrt", half=True)Ограничения
Квантизованная арифметика исполняется в симуляции: это фейковая квантизация,
которая считается в островках float32 даже под AMP. Симуляция численно
достоверна, поэтому результат val() на любом устройстве — настоящее
утверждение о квантизованной арифметике. Но не утверждение о скорости.
Два исключения исполняются нативно. fp16 и bf16 — обычные приведения.
Финализированные модули fp8 считают свой GEMM прямо на упакованных весах E4M3
через torch._scaled_mm на железе класса Ada, Hopper и Blackwell, с теми же
откалиброванными масштабами активаций, что и в симуляции; LIBREYOLO_KERNELS=off
возвращает в точности симулируемый путь везде.
Покрытие развёртывания уже, чем список рецептов. Пригодная к развёртыванию форма
ONNX здесь есть только у int8; fp8 и линейные рецепты ниже 8 бит
исполняются в PyTorch и кристаллизуются через format="pt". Запрос экспорта в
ONNX для них вызывает ошибку с этой же подсказкой — как и запрос любого формата,
кроме ONNX, для модели int8: движки для конечных сред выполнения собирайте из
QDQ-графа.
Экспорт модели int8, активации которой ни разу не калибровались, пишет
предупреждение и даёт граф только с квантизацией весов.