Посмотреть как Markdown

Производительность обучения

Скорость шага обучения меняют три рычага: смешанная точность, захват forward и backward сети в графы CUDA и то, что, по данным профилировщика, на самом деле тормозит шаг.

Измерение перед любыми изменениями

Три рычага ниже решают разные проблемы, и если взяться не за тот, ничего не изменится. Какая проблема именно у вас, показывает профилировщик.

Профилирование с продолжением обучения
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt") # Профилирует короткое окно реальных шагов, печатает вердикт, затем# продолжает запуск уже без хуков.model.train(data="my-dataset.yaml", epochs=100, profile=True)
Только измерение, затем остановка
# Ставит no_aug_epochs=0 и запускает ровно столько эпох, чтобы заполнить окно.libreyolo profile run coco128 --weights LibreYOLO9s.pt --size s
Разбор результата
libreyolo profile summary runs/profile/prof/profile.jsonlibreyolo profile phases runs/profile/prof/profile.jsonlibreyolo profile kernels runs/profile/prof/profile.json --top 10

profile=True замеряет окно реальных шагов обучения — по умолчанию пять отбрасываются, затем двадцать измеряются, — печатает отчёт, пишет свои артефакты и продолжает обучение уже без хуков. Выключенный профилировщик ничего не стоит, а при распределённом обучении он игнорируется.

Отчёт заканчивается одним из четырёх вердиктов:

ВердиктЧто означаетРычаги
dataloaderGPU ждёт входные данныебольше workers, cache="ram" или "disk", более лёгкая аугментация, больший батч
host / launchGPU кормят слишком медленно, много мелких ядербольший батч, графы CUDA, меньше синхронизаций с хостом на шаг
computeGPU загружен полностьюAMP или bfloat16 — либо смириться
memory-pressureаллокатор буксует, VRAM на пределеуменьшить батч; цифры загрузки здесь недостоверны

Показатель загрузки — это время работы ядер, делённое на время шага без синхронизации. Окно намеренно разделено надвое: первая половина идёт без дополнительной синхронизации, чтобы вердикт отражал реальное перекрытие, и только вторая половина обрамляет каждую фазу синхронизацией, чтобы отнести время GPU к фазам. Синхронизация каждой фазы даёт воркерам загрузчика данных запас времени и скрывает голодание, поэтому разбивка шага по фазам никогда не используется для выбора вердикта.

В каталог запуска попадают четыре файла: timeline.html, который открывается в браузере сам по себе, profile_trace.json для Perfetto или Nsight, profile_summary.json и profile.json — самодостаточный, его и стоит копировать и скармливать обратно подкомандам libreyolo profile.

Про profile run стоит знать две вещи. Он ставит no_aug_epochs=0, потому что профилировщик замеряет нулевую эпоху, а короткий запуск со значением no_aug_epochs по умолчанию профилировал бы облегчённый загрузчик данных без аугментаций, а не тот, который реально используется при обучении. И --repeat N выводит среднее и стандартное отклонение — это важно, потому что шаг, упирающийся в запуск ядер, достаточно шумный, чтобы один запуск ввёл в заблуждение; он пишет каталоги по попыткам prof_1, prof_2 и так далее плюс сводный profile_repeat.json.

Смешанная точность

amp=True — значение по умолчанию для большинства семейств, и forward выполняется под CUDA autocast. amp_dtype выбирает float16 или bfloat16.

Python
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")model.train(data="my-dataset.yaml", amp=True, amp_dtype="bfloat16")
CLI
libreyolo train model=LibreYOLO9s.pt data=my-dataset.yaml \  amp_dtype=bfloat16

Float16 требует динамического масштабирования функции потерь и получает работающий градиентный скейлер; у bfloat16 диапазон экспоненты шире, поэтому ему это не нужно и его скейлер выключен. Четыре семейства поставляются с amp=False — D-FINE, DEIM, YOLO-NAS и FOMO, — а настройка DEIM по наследованию доходит и до RT-DETRv4. D-FINE называет причину: его декодер ограничивает активации значением 65504 — наибольшим конечным значением float16.

Семантика аргументов, включая то, что происходит при запросе bfloat16 на железе без поддержки bfloat16, описана в разделе Гиперпараметры.

Графы CUDA

cuda_graph=True захватывает обучающие forward и backward сети в граф CUDA, убирая накладные расходы на запуск ядер на каждом шаге.

Python
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")model.train(data="my-dataset.yaml", epochs=100, cuda_graph=True)
CLI
libreyolo train model=LibreYOLO9s.pt data=my-dataset.yaml \  epochs=100 cuda_graph=true

Флаг безопасно передавать всегда. Семейство, задача или конфигурация, которые захватить нельзя, пишут одну строку в лог и обучаются в eager-режиме без изменений.

Захватывается только сеть. Функция потерь остаётся в eager-режиме намеренно: функции потерь детекции выбирают элементы по булевым маскам, запускают венгерское сопоставление и ветвятся по результатам назначения — ничего из этого граф записать не может. Шаг оптимизатора, обрезка градиентов, обновление EMA и расписание скорости обучения тоже остаются в eager-режиме.

Поэтому выигрыш ограничен тем, какую долю шага занимает сеть, а доля эта очень разная. Замеры на RTX 5070 Ti при 640 px и батче 8: у YOLOv9-t на сеть приходится 84 процента шага, у YOLOv7-b — 44 процента, у YOLOX-t — 31 процент, у RTMDet-t — 26 процентов. Последние два большую часть шага проводят внутри своих алгоритмов назначения меток, поэтому захват сети помогает им меньше всего.

Что это даёт

Условия для всех чисел ниже: RTX 5070 Ti, Windows, AMP, по одному процессу на ветку эксперимента из общего сохранённого состояния, воспроизведение одного реального батча, чтобы загрузчик данных не участвовал, самый быстрый из 24 шагов после прогрева. Детекция при 640 px, классификация при 224 px. Размер батча указан построчно.

СемействоРазмерБатчEagerС графомУскорение
FOMOs167.0 мс1.9 мс3.63x
MobileNetV4s1614.5 мс5.3 мс2.74x
EfficientNetV2b01629.0 мс11.9 мс2.44x
YOLOv9t893.6 мс47.0 мс1.99x
NAFNets8132.5 мс105.5 мс1.26x
PicoDets8145.0 мс118.7 мс1.22x
D-FINEn4185.3 мс159.2 мс1.16x
RF-DETRn4276.3 мс239.8 мс1.15x
YOLOXt8102.2 мс90.5 мс1.13x
RTMDett8149.7 мс136.2 мс1.10x
YOLOv7b4102.5 мс98.0 мс1.05x

Эти числа изолируют шаг на GPU. Полное дообучение платит ещё и за загрузчик данных, и за валидацию. YOLOv9-t на детекционном датасете из 406 изображений, 20 эпох, батч 8, 640 px, 4 воркера загрузчика данных, на той же машине: в eager — 428.4 с реального времени, с графом — 367.7 с, выигрыш 1.16x, при mAP50-95 0.6394 в обеих ветках.

На эти числа влияют три вещи. Маленькие батчи упираются в запуск ядер, большие — в вычисления, поэтому RT-DETR-r18 выигрывает 1.19x при батче 2 и 1.04x при батче 8. Накладные расходы на запуск максимальны на Windows, а на Linux выигрыш составляет примерно от трети до половины табличного. И запуск, упирающийся в загрузчик данных, не меняет реального времени вообще — поэтому профилировщик идёт первым.

При amp=False захват включается точно так же, но fp32-ядра выполняются дольше, поэтому шаг меньше упирается в запуск и большинство семейств выигрывает меньше. На том же железе у MobileNetV4-s при батче 16 ускорение меняется с 2.74x под AMP до 3.61x на fp32, у YOLOv9-t при батче 8 — с 1.99x до 1.69x, а у RT-DETR-r18 при батче 4 — с 1.12x до 0.99x.

Где захват применяется

ЗадачаСемейства
detectyolo9, yolo9_p2, yolo9_e2e, yolox, yolo7, yolonas, picodet, rtmdet, rfdetr, dfine, deim, deimv2, rtdetr, rtdetrv2, rtdetrv4, ec
classifyresnet, convnext, mobilenetv4, efficientnetv2
semanticsegformer, lingbotvision
pointfomo
restorenafnet

Всё остальное откатывается в eager-режим с одной строкой в логе: другие задачи на тех же семействах, семейства не из списка, распределённые запуски и запуски с дистилляцией. Сбой захвата во время работы тоже переводит остаток запуска в eager-режим, а не прерывает его.

У детекторов с энкодером и декодером — D-FINE, DEIM, DEIMv2, RT-DETR v1, v2 и v4, а также EC — захватываются только бэкбон и энкодер. Их декодер читает эталонную разметку (ground truth), чтобы построить запросы контрастивного шумоподавления, а число этих запросов следует за наибольшим количеством эталонных объектов в батче, поэтому число токенов меняется от батча к батчу.

Формы

Граф действителен ровно для той формы входа, с которой он был захвачен. Обучающий цикл считает формы батчей и захватывает граф, когда форма повторилась три раза. Батчи любой другой формы выполняются в eager-режиме: многомасштабные батчи и последний неполный батч эпохи.

Это ловушка для семейств DETR, которые по умолчанию меняют размер каждого батча. При multi_scale=True короткий запуск может так и не увидеть ни одну форму достаточно часто, чтобы вообще что-то захватить. Передавайте multi_scale=False, когда важно именно ускорение.

YOLOX посреди запуска меняет то, что считает захваченная область: он включает свою ветку L1-регрессии, когда на no_aug_epochs отключается мозаика. Обучающий цикл в этот момент сбрасывает захват и захватывает заново, когда новая форма установится.

Числовое поведение и память

Большинство семейств под AMP побитово воспроизводят свою траекторию функции потерь из eager. FOMO и LingBot-Vision отличаются в последнем бите float32 из-за другого порядка суммирования. Детекторы с деформируемым вниманием — D-FINE, DEIM, DEIMv2, RT-DETR, RF-DETR и EC — не воспроизводят и собственные eager-прогоны, потому что этот backward накапливает результат атомарными операциями, а свёртки TF32 выбирают порядок редукции на каждом запуске; прогон с графом остаётся внутри этого разброса. RTMDet отличается примерно на 3e-4 в относительном выражении на двух градиентах из 139, потому что он переиспользует свёртки головы на всех уровнях пирамиды, и два пути backward суммируют три вклада в разном порядке. У SegFormer внутри захваченной области есть стохастическая глубина, поэтому воспроизведённый граф тянет собственный поток случайных чисел и статистически эквивалентен eager, а не идентичен ему; менеджер сообщает об этом один раз в момент захвата.

При amp=False побитового совпадения на этом железе не даёт ничто — ни с захватом, ни без него. Два одинаковых eager-прогона YOLOv9-t с одним и тем же зерном расходятся на 36 процентов в относительном выражении за 20 шагов, а YOLOX-t — на 2.6 процента, потому что cuDNN выбирает недетерминированный алгоритм градиента по весам для некоторых форм fp32-свёрток.

Захваченный граф закрепляет статические буферы входа, выхода и рабочей области, поэтому пиковый VRAM растёт примерно на один дополнительный набор активаций. По семействам выше пиковое выделение памяти сдвинулось в пределах от -5 до +19 процентов. Относительная цена максимальна у маленьких классификационных моделей, у которых активации и так невелики: у ResNet-18 при 224 px и батче 16 пик вырос с 0.48 ГБ в eager до 0.57 ГБ с графом. Если из-за этого запуск выходит за лимит, уменьшите батч или не включайте флаг.

Смежные страницы

Проверено с LibreYOLO v1.5.0.