Графи CUDA
Граф CUDA записує одне виконання фіксованої послідовності ядер і повторно запускає його як одну операцію. LibreYOLO захоплює інференс для 39 перевірених сімейств і навчання для 24, завжди окремо для сімейства, лише після перевірки побітової відповідності й без непомітного резервного шляху.
- Сімейства інференсу
- 39
- Сімейства навчання
- 24
- Прапорець інференсу
predict(cuda_graph=True)- Прапорець навчання
train(cuda_graph=True)
Що захоплюється
Граф записує фіксовану послідовність ядер і адреси пам'яті, які вони читають і записують. Він не записує значення, форми чи потік керування. Повторне виконання відбувається одним запуском замість сотень, тому найбільший виграш дають малі мережі з малими батчами, де етап переважно витрачається на запуск, а не на обчислення.
Дві точки входу захоплюють різні обсяги роботи.
| Усередині графа | Негайне виконання | |
|---|---|---|
| Інференс | Прямий прохід мережі, model._forward(x) | Попереднє оброблення, NMS, усе постоброблення |
| Навчання | Прямий і зворотний проходи мережі | Функція втрат, крок оптимізатора, обрізання градієнтів, EMA, розклад швидкості навчання |
Ні NMS, ні функція втрат виявлення не придатні для захоплення. Обидві операції вибирають дані за булевими масками, виконують угорське зіставлення або призначувач і розгалужуються залежно від результату, а саме цього граф записати не може. Винесення їх за межі графа робить захоплення безпечним і не є обмеженням, яке потрібно обходити.
from libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibreYOLO9t.pt") # True захоплює граф під час першого використання кожної форми вхідних даних.# "auto" чекає повторення форми, перш ніж сплатити вартість захоплення.result = model(SAMPLE_IMAGE, cuda_graph=True)from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9t.pt")model.train(data="my-dataset.yaml", epochs=100, cuda_graph=True)libreyolo train model=LibreYOLO9t.pt data=my-dataset.yaml \ epochs=100 --cuda-graphПід час передбачення cuda_graph приймає три значення. Типовим є False.
True захоплює граф під час першої появи кожної форми вхідних даних. "auto"
чекає повторення форми, тому одноразова робота й робота зі змінними формами не
сплачують за захоплення, яке не буде використано повторно.
capture_graph(imgsz=None, batch=1, dtype=None) переносить витрати за межі
першого запиту, graph_info() повідомляє про захоплені графи й кількість
повторних виконань, а release_graphs() звільняє їх.
Під час навчання прапорець є простим булевим значенням, --cuda-graph у CLI.
Пов'язані засоби керування описано в розділах
продуктивність передбачення і
продуктивність навчання.
Підтримка інференсу
Підтримка визначається окремо для кожного сімейства через змінну класу
SUPPORTS_CUDA_GRAPH. Сімейство отримує позначку лише після захоплення та
побітово ідентичного повторного виконання для двох пробних вхідних даних із
різних розподілів. Спільна матриця відповідності охоплює 39 сімейств у дев'яти завданнях.
| Завдання | Сімейства |
|---|---|
| detect | yolo1, yolo2, yolo3, yolo4, yolo9, yolo9_p2, yolo9_e2e, yolox, yolo7, yolonas, picodet, rtmdet, dfine, deim, deimv2, rtdetr, rtdetrv2, rtdetrv4, rfdetr, ec |
| segment | dfine, rtmdet, rfdetr, ec |
| pose | ec, yolonas, rfdetr |
| point | fomo |
| classify | resnet, convnext, mobilenetv4, efficientnetv2, clip, dinov2, siglip2 |
| semantic | eomt, dinov2, segformer, pidnet, lingbotvision |
| depth | depth_anything, depth_anything3, zipdepth |
| restore | nafnet, realesrgan, swinir |
| matte | birefnet |
Кілька сімейств наведено в кількох завданнях, тому в матриці більше рядків, ніж окремих сімейств. Ще три сімейства виконують захоплення через власні шляхи коду з окремими спеціалізованими тестами, а не через спільну матрицю, тому не входять до 39: PP-OCR, SAM і SenseNova.
Перевірка побітова, а не приблизна. Попередня версія протоколу оцінювала відповідність за відносною величиною й помилково понизила три справні сімейства, YOLOX, EfficientNetV2 і YOLOv7, у яких різниця між негайним виконанням та графом становила близько 1e-7, хоча на важливій пробі вони були побітово ідентичними.
Підтримка навчання
У цьому випуску захоплення навчання розширено з двох сімейств до 24 у п'яти завданнях.
| Завдання | Сімейства |
|---|---|
| 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 |
Усе інше навчається в негайному режимі: інші завдання тих самих сімейств,
сімейства поза списком, розподілені запуски та запуски дистиляції. Захоплення
також пропускається, доки форма залишається новою, оскільки шлях навчання чекає,
щоб форма вхідних даних повторилася тричі. Через це multi_scale=True може
ніколи не захопити граф.
Дві різні відповіді для непідтримуваного сімейства
Шлях інференсу спричиняє помилку. Виклик predict(cuda_graph=True) для
сімейства, яке не заявило підтримку, спричиняє NotImplementedError із назвою
сімейства, а не виконується негайно, створюючи хибне враження прискорення.
Причина полягає в тому, що неправильне захоплення не завершується явною помилкою:
повторне виконання прямого проходу з операцією, яку не можна захопити, непомітно
повертає неправильні числа. Тому підтримка має бути явною властивістю кожного
сімейства, а не спробою з резервним шляхом.
Шлях навчання записує повідомлення в журнал. train(cuda_graph=True) завжди
безпечно передавати, а сімейство, завдання чи конфігурація, які не можна
захопити, записують один рядок і без змін навчаються в негайному режимі.
Захоплення, що не вдалося посеред запуску, також переводить решту запуску в
негайний режим замість переривання. Ця асиметрія навмисна: виклик передбачення
можна виправити на місці, а запуск навчання не повинен завершуватися на шостій
годині через необов'язкову оптимізацію.
Поділ за межами
Деякі сімейства не можна захопити цілком, оскільки один етап справді виконує операцію, яку граф не може записати. Замість вилучення сімейства захоплення розділяється за перевіреною межею: придатна частина повторюється, решта виконується негайно, а об'єднаний вивід збігається з повністю негайним виконанням.
| Сімейство | Захоплено | Негайне виконання та причина |
|---|---|---|
| Depth Anything 3 | Мережа | Етап неба, який виконує видиму хосту роботу після прямого проходу |
| BiRefNet | Кодувальник, forward_enc | Декодер, чий deform_conv2d під час повторного захопленого виконання дає інший результат |
| PP-OCR | Етап виявлення, forward_det | Розпізнавання, оскільки ширина фрагментів різниться для кожного рядка |
| SAM | Кодувальник зображення | Шлях підказки, який виконується багато разів на одне кодування |
| SenseNova | Візуальна башта | Авторегресивне генерування з кешем KV, що зростає на кожному кроці |
| Детектори кодувальник-декодер | Бекбон і кодувальник | Декодер і угорський критерій |
Поділ BiRefNet варто перечитати: неправильна поведінка deform_conv2d під час
захоплення відтворюється на окремому виклику поза будь-якою моделлю. Заміну на
еквівалент у чистому PyTorch відхилено, оскільки вона також змінила б
передбачення в негайному режимі, а саме ці числа є контрактом.
Випадок кодувальника-декодера охоплює D-FINE, DEIM, DEIMv2, RT-DETR, RT-DETRv2, RT-DETRv4 та EC. Їхній декодер будує контрастно-зашумлювальні запити з еталонної розмітки, а кількість таких запитів походить із найбільшої кількості еталонних об'єктів у батчі, тому кількість токенів декодера змінюється між батчами. Саме цього граф не може витримати. Для цих сімейств бекбон разом із кодувальником становить приблизно від п'ятої до четвертої частини кроку, тому вони розташовані внизу таблиці прискорення.
PP-OCR захоплює по одному графу для кожної форми вхідних даних виявлення в межах ліміту кешу засобу виконання й повертає негайний результат, коли область захоплення не активна.
Числові результати
Більшість сімейств побітово ідентичні, а для решти причину названо прямо. На нульовому кроці навчання функція втрат побітово ідентична для всіх 24 сімейств, і жоден буфер BatchNorm не відрізняється; категорії визначає порівняння градієнтів.
| Клас | Сімейства | Значення |
|---|---|---|
| Точний | Більшість із 24 | Кожен градієнт побітово ідентичний |
| 1 ULP | fomo, lingbotvision | Останній біт float32, близько 1e-7 відносно, через інший порядок підсумовування |
| Шум негайного режиму | Лінія DETR | Різниця графа з негайним режимом не перевищує різницю між двома негайними запусками |
| Округлення рухомої коми | rtmdet | 137 із 139 градієнтів побітово ідентичні, два відрізняються приблизно на 3e-4 |
| Власний потік RNG | segformer | Стохастична глибина розташована всередині захопленої області |
Клас шуму негайного режиму потрібно тлумачити правильно. Для цих сімейств два
негайні запуски з однаковим seed уже не збігаються, тому побітова ідентичність
не є вимогою, яку не виконав запуск із графом; її не виконує жоден запуск. Це
ширше проявляється за amp=False, де виміряна відносна недетермінованість
3.2e-7 у градієнті ваги fp32 накопичується: два негайні запуски YOLOv9-t з
однаковим seed розходяться на 36 відсотків за 20 кроків, а вимкнення TF32 цього не виправляє.
Закріплена пам'ять
Захоплення виконується з capture_error_mode="thread_local". У типовому режимі
PyTorch "global" потік закріпленої пам'яті DataLoader, що готує наступний
батч, викликає cudaHostAlloc. Це одночасно робить поточне захоплення недійсним
і пошкоджує ним потік, тому запуск завершується під час отримання наступного
батча з помилкою всередині потоку закріпленої пам'яті. Таке поєднання двічі
спостерігалося в реальних навчальних кампаніях до встановлення причини.
Локальний для потоку режим обмежує лише потік захоплення. Потік закріплення
ніколи не торкається потоку захоплення, тому жодна його операція не має входити
до графа. Під час навчання додатково тимчасово підставляється підклас
torch.cuda.CUDAGraph, який примусово задає цей режим, оскільки
make_graphed_callables не надає для нього аргументу. Операція виконується під
блокуванням, щоб два одночасні захоплення не залишили підстановку встановленою.
Практична користь
Вимірювання виконано на RTX 5070 Ti з AMP, по одному процесу на варіант, із повторним виконанням одного справжнього батча без участі завантажувача даних. Взято найшвидший із 24 кроків після прогрівання. Виявлення працює з 640 px, класифікація з 224 px.
| Сімейство | Батч | Прискорення |
|---|---|---|
| FOMO s | 16 | 3.63x |
| MobileNetV4 s | 16 | 2.74x |
| EfficientNetV2 b0 | 16 | 2.44x |
| YOLOv9-t | 8 | 1.99x |
| YOLOv9 e2e | 8 | 1.76x |
| YOLOv9 p2 | 8 | 1.49x |
| Усе інше | різниться | 1.04x до 1.26x |
Виграш усього запуску менший, оскільки граф не може прискорити завантажувач даних або валідацію. Донавчання YOLOv9-t протягом 20 епох на 406 зображеннях скоротилося з 428.4 с до 367.7 с, що відповідає наскрізному прискоренню 1.16x, з однаковим mAP50-95 0.6394 в обох варіантах і однаковими втратами за епохами.
Межу встановлює частка кроку, зайнята мережею. На тому самому обладнанні за 640 px і батча 8 вона становить 84 відсотки для YOLOv9-t, але лише 26 відсотків для RTMDet-t, який витрачає більшість кроку в призначувачі міток. Накладні витрати запуску найвищі у Windows, тому виграш у Linux становить приблизно від третини до половини значень таблиці, а запуск, обмежений завантажувачем даних, взагалі не змінює час за годинником. Пікове використання пам'яті змінюється від зменшення на 5 відсотків до збільшення на 19 відсотків.
Застереження
Граф записує адреси, а не значення, тому будь-яка операція, що переміщує
параметри, видаляє його. Зміна пристрою через predict(device=...), квантування
та деквантування роблять усі захоплені графи недійсними.
Розмір батча важливіший за сімейство: RT-DETR-r18 дає виграш 1.19x із батчем 2 і 1.04x із батчем 8, оскільки великий батч обмежений обчисленнями й має менше накладних витрат запуску, які можна усунути.
Набір тестів відповідності інференсу запускався без установленого додаткового
пакета kernels, тому безпека захоплення з активними скомпільованими ядрами Hub
не охоплена. Установіть LIBREYOLO_HUB_KERNELS=0, щоб усунути їх під час
ізоляції проблеми захоплення. Дивіться ядра.