Производительность обучения
Скорость шага обучения меняют три рычага: смешанная точность, захват 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 slibreyolo profile summary runs/profile/prof/profile.jsonlibreyolo profile phases runs/profile/prof/profile.jsonlibreyolo profile kernels runs/profile/prof/profile.json --top 10profile=True замеряет окно реальных шагов обучения — по умолчанию пять
отбрасываются, затем двадцать измеряются, — печатает отчёт, пишет свои артефакты
и продолжает обучение уже без хуков. Выключенный профилировщик ничего не стоит, а
при распределённом обучении он игнорируется.
Отчёт заканчивается одним из четырёх вердиктов:
| Вердикт | Что означает | Рычаги |
|---|---|---|
dataloader | GPU ждёт входные данные | больше workers, cache="ram" или "disk", более лёгкая аугментация, больший батч |
host / launch | GPU кормят слишком медленно, много мелких ядер | больший батч, графы CUDA, меньше синхронизаций с хостом на шаг |
compute | GPU загружен полностью | 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.
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")model.train(data="my-dataset.yaml", amp=True, amp_dtype="bfloat16")libreyolo train model=LibreYOLO9s.pt data=my-dataset.yaml \ amp_dtype=bfloat16Float16 требует динамического масштабирования функции потерь и получает
работающий градиентный скейлер; у 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,
убирая накладные расходы на запуск ядер на каждом шаге.
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")model.train(data="my-dataset.yaml", epochs=100, cuda_graph=True)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 | С графом | Ускорение |
|---|---|---|---|---|---|
| FOMO | s | 16 | 7.0 мс | 1.9 мс | 3.63x |
| MobileNetV4 | s | 16 | 14.5 мс | 5.3 мс | 2.74x |
| EfficientNetV2 | b0 | 16 | 29.0 мс | 11.9 мс | 2.44x |
| YOLOv9 | t | 8 | 93.6 мс | 47.0 мс | 1.99x |
| NAFNet | s | 8 | 132.5 мс | 105.5 мс | 1.26x |
| PicoDet | s | 8 | 145.0 мс | 118.7 мс | 1.22x |
| D-FINE | n | 4 | 185.3 мс | 159.2 мс | 1.16x |
| RF-DETR | n | 4 | 276.3 мс | 239.8 мс | 1.15x |
| YOLOX | t | 8 | 102.2 мс | 90.5 мс | 1.13x |
| RTMDet | t | 8 | 149.7 мс | 136.2 мс | 1.10x |
| YOLOv7 | b | 4 | 102.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.
Где захват применяется
| Задача | Семейства |
|---|---|
| detect | yolo9, yolo9_p2, yolo9_e2e, yolox, yolo7, yolonas, picodet, rtmdet, rfdetr, dfine, deim, deimv2, rtdetr, rtdetrv2, rtdetrv4, ec |
| classify | resnet, convnext, mobilenetv4, efficientnetv2 |
| semantic | segformer, lingbotvision |
| point | fomo |
| restore | nafnet |
Всё остальное откатывается в 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 ГБ с графом. Если из-за этого запуск выходит за лимит, уменьшите батч или не включайте флаг.
Смежные страницы
- Гиперпараметры — про
batch,nbs,cacheиworkers. - Обучение на нескольких GPU, где недоступны и графы CUDA, и профилировщик.
- Графы CUDA — про общую матрицу поддержки для инференса и обучения, разделения по швам и контракт по числам.