«Мій CPU не може нагодувати GPU»: правда проти міфу

Було корисно?

Ви купили великий GPU. Панель показує 20–40% завантаження. Тренування повільне, інференс ривками, кадри пропадають, а в чаті хтось каже фразу: «Ваш CPU не може нагодувати GPU».

Іноді це правда. Іноді — це міф. А іноді справжній винуватець — сховище, PCIe або невелика синхронізація, про яку ви й не здогадувались. Виправлення залежить від того, який саме «корм» ви маєте на увазі: передача завдань, доставка даних або забезпечення достатньої паралельності роботи.

Що насправді означає «годувати GPU»

«Годувати GPU» — це неточна фраза, яка зводить до одного відчуття три різні канали передачі:

  1. Передача роботи: хост (CPU) запускає ядра, налаштовує командні буфери, планує CUDA Graphs, ставить копії в чергу і синхронізує стріми. Якщо хост не може запускати достатньо швидко, GPU простає між викликами ядра.
  2. Доставка даних: хост читає дані зі сховища, декодує/попередньо обробляє їх і передає через PCIe/NVLink у пам’ять GPU. Якщо це повільно, GPU чекає наступний батч.
  3. Доступність паралельної роботи: навіть якщо передача і доставка ідеальні, GPU потребує достатньо незалежної роботи, щоб заповнити SM. Надто малі батчі, малі ядра або багато послідовних залежностей можуть тримати низьке завантаження без участі CPU.

Тому коли хтось каже «CPU не може нагодувати GPU», вимагайте уточнення: який саме «корм»? Якщо відповіді немає — ви вже знайшли перше вузьке місце: якість діагностики.

Цитата, яку варто пам’ятати (перефразований вислів Дональда Кнута): Передчасна оптимізація — це частий спосіб марно витратити час; спершу виміряйте, потім оптимізуйте те, що має значення.

І так, я буду наполегливим щодо вимірювань. Продакшен-системи не працюють на впевненості.

Правда проти міфу: коли CPU дійсно є вузьким місцем

«Правда» випадки (CPU справді обмежує пропускну здатність GPU)

Це нудно реально:

  • Навантаження від запуску ядер домінує. Ви запускаєте тисячі дрібних ядер на крок. Потік CPU проводить час у викликах драйвера, а GPU чекає на наступний запуск.
  • Однопоточний вхідний пайплайн. Декодування даних, аугментації, токенізація або інженерія фіч працюють на одному ядрі, бо хтось поставив workers=0 «для детермінізму». GPU чекає на наступний батч, наче за повільною касою.
  • Зайва синхронізація. Схожі на cudaDeviceSynchronize() виклики (прямо чи опосередковано) послідовно серіалізують пайплайн. CPU блокується, потім GPU блокується, а ви звинувачуєте CPU у «слабкості».
  • Попередня обробка, що завантажує CPU. Думайте JPEG декодування, відео без апаратного декодеру, парсинг JSON або розпакування. GPU швидкий; ваш CPU досі підкоряється фізиці і прогнозуванню гілок.
  • NUMA і нестача пропускної здатності пам’яті. CPU має «багато ядер», але вони всі тягнуть дані через межу сокетів, бо процес і GPU на різних NUMA вузлах.
  • Накладні витрати драйвера/прошивки і переривання. Особливо в мульти-GPU серверах із великою I/O. CPU не «слабкий», він зайнятий як планувальник I/O і губка для переривань.

«Мем» випадки (CPU не є обмежувачем)

Тут люди витрачають тижні:

  • Низьке завантаження GPU через короткі спалахи роботи. Середнє завантаження приховує мікростани; GPU насправді чекає пам’ять всередині ядра, а не CPU.
  • Ви обмежені PCIe. Копії насичують PCIe. Прокачування CPU не розширить PCIe Gen3 x8 магічно.
  • Брак VRAM. Ви зменшили batch size, щоб вписатися в пам’ять, що знижує арифметичну інтенсивність і робить GPU «менш нагодованим». Це не про CPU; це про робочий набір.
  • Ваші ядра неефективні. Низька occupancy, погана коалізація пам’яті, розгалуження. GPU «працює», але неефективно, а не чекає CPU.
  • Ваше завдання заточене на латентність (наприклад, інференс з малими батчами). Завантаження GPU може ніколи не бути високим, бо робота не має достатньо паралелізму. «100% GPU» — не закон природи.

Жарт #1: GPU не «голодний», він вибагливий — якщо подати по одній грінці, він дивитиметься на вас, ніби проблема у вас.

Цікаві факти й трохи історії (бо це пояснює сучасні болі)

  • Факт 1: Ранні моделі програмування GPU (до CUDA) були по суті прихованими графічними API; «годувати GPU» буквально означало тримати графічний конвеєр заповненим трикутниками. Сьогоднішні обчислювальні ядра успадкували той самий менталітет пропускної здатності.
  • Факт 2: Модель запуску CUDA зробила простим ставити ядра в чергу, але ранні практики заохочували багато дрібних ядер; сучасні поради часто радять ф’южн і CUDA Graphs, щоб зменшити накладні витрати запуску.
  • Факт 3: PCIe покращувався поступово, але не в тому ж темпі, що FLOPS GPU. Ця прогалина — чому передачі host→device досі частий вузький момент навіть на «монстр» серверах.
  • Факт 4: NUMA став болючим, коли двосокетні сервери стали домінувати в датацентрах; афінітет GPU і «найближчий CPU» важливі, бо затримка пам’яті через сокети — не дрібниця.
  • Факт 5: Pinned (page-locked) пам’ять швидша для DMA-передач, але забагато pinned пам’яті шкодить ОС та іншим процесам, зменшуючи гнучкість pageable RAM.
  • Факт 6: NVLink існує переважно тому, що PCIe не був достатнім для мульти-GPU робочих навантажень; але він не вирішує CPU-сторонню попередню обробку, накладні витрати запуску ядер чи інгест даних зі сховища.
  • Факт 7: Лічильники «GPU utilization» спочатку будувалися для графіки і довготривалих ядер. Інтерпретувати їх для ML-тренувань із сумішшю копій/обчислень може вводити в оману без таймлайна.
  • Факт 8: Підйом дата-орієнтованого ML зробив вхідні пайплайни (декодування, аугментації, токенізація) проблемою першого класу; ваше «тренування» часто поводиться як ETL-робота з прикріпленим GPU.

Чотири типи вузьких місць, які ви плутаєте

1) Вузьке місце на подачі з CPU (залежне від запусків)

Симптоми: GPU показує прогалини між ядрами, багато коротких ядер, потік CPU застряг у системному часі або викликах драйвера, час кроку масштабується з «кількістю запусків», а не з розміром батча.

Типові виправлення: зливайте ядра, збільшуйте batch size, використовуйте CUDA Graphs, зменшуйте накладні витрати Python, уникайте викликів на пристрій для кожного зразка, зменшуйте синхронізацію, застосовуйте persistent kernels де доречно.

2) Вузьке місце попередньої обробки на CPU (декод/аугмент/токенізація)

Симптоми: ядра CPU насичені в user-просторі, читання з диска/мережі виглядають нормально, GPU чекає на вхід, збільшення числа workers допомагає до появи contention.

Типові виправлення: паралелізуйте попередню обробку, векторизуйте, кешуйте декодовані дані, переносіть трансформації на GPU, використовуйте швидші кодеки, зменшуйте вартість аугментацій, застосовуйте більші батчі для амортизації накладних витрат.

3) Вузьке місце I/O та сховища (ваш «сервер GPU» насправді клієнт сховища)

Симптоми: високий iowait, довгі затримки читання, непостійна пропускна здатність, шумне завантаження GPU, продуктивність покращується, коли датасет локально на NVMe або в кеші.

Типові виправлення: локальний кеш, prefetch, більші послідовні читання, кращі формати файлів, уникайте дрібних випадкових читань, переконайтесь, що файлові системи та мережа не обмежують.

4) Вузьке місце на стороні GPU (він зайнятий, але не так, як треба)

Симптоми: завантаження GPU може бути високе або низьке, але профіль показує memory stalls, низьку occupancy, простаючі tensor cores або неефективні ядра. CPU в основному простий.

Типові виправлення: оптимізація ядер, кращі бібліотеки, mixed precision, зміни макету даних, більші злиті операції, виправлення некоалізованого доступу до пам’яті, переконайтесь, що ви використовуєте правильний бекенд.

Швидкий план діагностики (перший/другий/третій)

Перший: встановіть, чи GPU чекає, чи працює

  • Перевірте завантаження GPU, енергоспоживання, частоти, використання пам’яті і—критично—чи завантаження є сплесковим.
  • Подивіться, чи активні двигуни копіювання проти обчислень (H2D/D2H проти активності SM).
  • Якщо GPU дійсно багато простоює, він чекає на щось вище по ланцюжку (подача з CPU, попередня обробка, I/O, синхронізація).

Другий: відокремте передачу завдань від попередньої обробки

  • Якщо однопоточний CPU гарячий і багато системного часу: підозрюйте накладні витрати запуску або синхронізацію.
  • Якщо багато ядер CPU гарячі в user-часі: підозрюйте попередню обробку або розпакування/декодування/токенізацію.
  • Якщо CPU в основному простий, а GPU недозавантажений: підозрюйте неефективність на стороні GPU або малий робочий об’єм.

Третій: перевірте шлях транспорту (PCIe/NUMA) і шлях сховища

  • Підтвердьте ширину/швидкість PCIe. «x16» — це не відчуття; це погоджений стан лінку.
  • Перевірте NUMA-локальність: CPU, пам’ять і GPU мають бути співпадаючими коли можливо.
  • Перевірте затримки сховища і розміри читань. Випадкові 4KB читання з мережевого файлового сховища змусять H100 страждати.

Жарт #2: Купувати швидший CPU, щоб виправити PCIe-вузьке місце — це як наймати швидшого касира, тому що вантажівка застрягла в заторі.

Практичні завдання: команди, виходи та рішення

Це кроки «перестаньте сперечатись, почніть перевіряти». Кожне завдання включає команду, приклад виходу, що це означає і яке рішення прийняти.

Завдання 1: Перевірте живе завантаження GPU, частоти та енергоспоживання

cr0x@server:~$ nvidia-smi dmon -s pucvmt
# gpu    pwr gtemp mtemp  sm   mem   enc   dec   mclk   pclk
# Idx      W     C     C   %     %     %     %    MHz    MHz
    0     92    64     -  28     18     0     0   5001   1410
    0     88    63     -  31     20     0     0   5001   1410
    0     55    60     -   4      6     0     0   5001    705

Що це означає: SM% коливається від ~30% до ~4% із скиданням частот — це вказує на спалахи роботи або стани очікування. Падіння енергоспоживання/частот часто означає, що GPU достатньо простоює, щоб знизити частоту.

Рішення: Якщо спалахи корелюють із межами батчів, шукайте проблему вгору по ланцюжку (вхід, синхронізація). Якщо SM% стабільний але низький — дивіться на ефективність ядер або розмір батча.

Завдання 2: Подивіться використання GPU по процесах (чи ви взагалі дивитесь на потрібну задачу?)

cr0x@server:~$ nvidia-smi pmon -s um
# gpu        pid  type    sm   mem   enc   dec   command
    0      27431     C     9    12     0     0   python
    0      29902     G     0     1     0     0   Xorg

Що це означає: Ваш Python-процес ледь використовує SM. Пам’ять виділена, обчислення — ні.

Рішення: Профілюйте вхід і синхронізацію. Не припускайте, що «виділена VRAM» означає «GPU працює».

Завдання 3: Перевірте швидкість і ширину PCIe лінку

cr0x@server:~$ nvidia-smi -q | sed -n '/PCI/,/Replay/p'
    PCI
        Bus                             : 00000000:81:00.0
        Link Width                      : 8x
        Link Speed                      : 8.0 GT/s
        Replay Counter                  : 0

Що це означає: Це PCIe Gen3 x8. Якщо ви очікували Gen4 x16 — ви вже знайшли клас вузького місця.

Рішення: Виправте налаштування BIOS, розміщення у слоті, riser або невідповідність платформи перед тим, як переписувати код.

Завдання 4: Підтвердіть погоджений стан PCIe через lspci

cr0x@server:~$ sudo lspci -s 81:00.0 -vv | egrep -i 'LnkCap|LnkSta'
LnkCap: Port #0, Speed 16GT/s, Width x16
LnkSta: Speed 8GT/s (downgraded), Width x8 (downgraded)

Що це означає: Карта може працювати на Gen4 x16, але зараз знижена. Це може статися через проводку слота, BIOS або проблемний riser.

Рішення: Розглядайте це як апаратну/платформну проблему. Жодна кількість «додаткових workers» не виправить знижений лінк.

Завдання 5: Перевірте топологію GPU і NUMA афінітет

cr0x@server:~$ nvidia-smi topo -m
        GPU0    CPU Affinity    NUMA Affinity
GPU0     X      0-15           0

Що це означає: GPU0 найближчий до ядер CPU 0–15 на NUMA вузлі 0.

Рішення: Прив’язуйте процес і алокації пам’яті до тих ядер/вузла, якщо ви виконуєте важку попередню обробку або H2D передачі.

Завдання 6: Перевірте насичення CPU і чергу запуску

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (server)  01/10/2026  _x86_64_  (32 CPU)

12:40:20 PM  CPU   %usr %nice %sys %iowait %irq %soft %idle
12:40:21 PM  all   210.0 0.00 35.0  0.50    0.0  1.2   753.3
12:40:21 PM    7    98.0 0.00  2.0  0.00    0.0  0.0     0.0
12:40:21 PM    8     4.0 0.00 60.0  0.00    0.0  0.0    36.0

Що це означає: Одне ядро (CPU 7) завантажене у user-часі, інші — прості. CPU 8 має високий system-час (робота драйвера/ядра). Це пахне однопотоковим вузьким місцем (GIL у Python, потік запуску або синхронізація).

Рішення: Оптимізуйте шлях на хості: зменшіть накладні витрати Python на крок, використайте batching, CUDA Graphs, уникайте частих синхронізацій.

Завдання 7: Відловіть підказки i/o wait і латентності сховища

cr0x@server:~$ iostat -xz 1 3
avg-cpu:  %user %nice %system %iowait  %steal %idle
          18.2   0.0    6.1     22.9     0.0  52.8

Device            r/s   rkB/s  rrqm/s  %util  await
nvme0n1          85.0  4200.0    0.0   78.0   9.8

Що це означає: iowait високий, а NVMe await ~10ms під навантаженням. Для вхідного пайплайна з багатьма дрібними читаннями це болісно.

Рішення: Збільшіть розмір читань, використайте prefetch, пакуйте дані в більші шарди, кешуйте локально або перемістіть датасет зі спільного сховища.

Завдання 8: Підтвердіть шаблон доступу до датасету (дрібні випадкові читання проти стрімінгу)

cr0x@server:~$ sudo strace -f -e trace=openat,read -p 27431 -s 80 -tt 2>&1 | head -n 8
12:41:10.102334 openat(AT_FDCWD, "/data/ds/img_000812.jpg", O_RDONLY) = 57
12:41:10.102801 read(57, "\377\330\377\340\0\20JFIF\0\1\1\0\0\1\0\1\0\0", 4096) = 4096
12:41:10.103122 read(57, "...", 4096) = 4096
12:41:10.103444 openat(AT_FDCWD, "/data/ds/img_000813.jpg", O_RDONLY) = 58

Що це означає: Багато дрібних 4KB читань серед великої кількості файлів. Класичний випадок «на мому ноуті виглядає нормально», який ламається в масштабі.

Рішення: Консолідуйте файли (tar/shards), використовуйте послідовні читання, сприяйте дружнім до page cache паттернам і робіть prefetch батчів.

Завдання 9: Перевірте масштабування частоти CPU (тихий вбивця пропускної здатності)

cr0x@server:~$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
powersave

Що це означає: CPU дозволено агресивно знижувати частоту. Для вибіркової попередньої обробки це може збільшити хвостову латентність і «голодувати» GPU між батчами.

Рішення: На виділених нодах для тренувань використовуйте governor performance або налаштування, що відповідають платформі, а потім знову виміряйте.

Завдання 10: Виявіть NUMA-помилку розміщення (процес «далеко» від GPU)

cr0x@server:~$ numactl --show
policy: default
preferred node: current
physcpubind: 16 17 18 19 20 21 22 23
membind: 1

Що це означає: Ваш процес прив’язаний до NUMA вузла 1, а раніше топологія показувала, що GPU0 віддає перевагу NUMA вузлу 0. Це крос-сокетний трафік для кожного DMA-буфера і результатів попередньої обробки.

Рішення: Переприв’яжіть до GPU-локального NUMA вузла (або перемістіть задачу на GPU, прикріплений до вузла 1). Це часто дає двозначне відсоткове покращення.

Завдання 11: Перевірте тротлінг і теплові обмеження

cr0x@server:~$ nvidia-smi -q | sed -n '/Clocks/,/Applications Clocks/p'
    Clocks
        Graphics                        : 705 MHz
        SM                              : 705 MHz
        Memory                          : 5001 MHz
    Applications Clocks
        Graphics                        : 1410 MHz
        Memory                          : 5001 MHz

Що це означає: Поточна SM-частота значно нижча за application clock. Якщо це триває під навантаженням, можлива енергетична/теплова тротлінг або просто проста.

Рішення: Якщо завантаження високе, а частоти низькі — перевірте ліміти потужності, охолодження і потік повітря в шасі. Якщо завантаження низьке — це скоріше проста і нормальне зниження частот.

Завдання 12: Виявіть патерн з великою кількістю запусків ядер (багато дрібної роботи на GPU)

cr0x@server:~$ sudo perf top -p 27431 -g --stdio
Samples: 3K of event 'cycles'
  18.40%  libcuda.so.1        [.] cuLaunchKernel
  11.22%  libc.so.6           [.] memcpy
   9.87%  libpthread.so.0     [.] pthread_mutex_lock
   6.31%  python3.10          [.] _PyEval_EvalFrameDefault

Що це означає: Процес витрачає багато циклів на запуск ядер і виконання Python-кадрів. Це — накладні витрати подачі.

Рішення: Зливайте операції, використайте компільовані графи, зменшіть Python-локальні виклики на операцію і розгляньте більші ядра/батчі.

Завдання 13: Перевірте вплив мережевого файлового сховища (якщо датасет віддалений)

cr0x@server:~$ nfsstat -c
Client rpc stats:
calls      retrans    authrefrsh
248391     1203       0

Client nfs v4:
ops         count
read        182744
open        50322
getattr     411802

Що це означає: Є повторні запити, і обсяг getattr/open величезний. Метадані плюс віддалені читання можуть цілком «голодувати» GPU.

Рішення: Сташуйте на локальний NVMe, зменшіть кількість файлів, підвищте кеш клієнта там, де безпечно, або змініть формат датасету.

Завдання 14: Підтвердьте сигнали тиску huge pages / pinned memory (шлях пам’яті хоста)

cr0x@server:~$ grep -E 'MemAvailable|Dirty|Writeback' /proc/meminfo
MemAvailable:   1842332 kB
Dirty:           482912 kB
Writeback:        12984 kB

Що це означає: Низький MemAvailable і високий Dirty вказують на тиск пам’яті і writeback. Це може уповільнити попередню обробку і створити шумні латентності, навіть якщо GPU «в порядку».

Рішення: Зменшіть використання pinned memory, уникайте надмірного кешування в додатку, або забезпечте більше RAM / ізолюйте шумних сусідів.

Три корпоративні міні-історії (як команди реально помиляються)

Міні-історія 1: Інцидент через неправильне припущення

У них був новий GPU-кластер. Усі раділи, і чеклист запуску мав звичні пункти: версії драйверів, CUDA runtime, перевірки здоров’я і швидкий тренувальний прогін. Завантаження GPU було низьким, тому висновок з’явився швидко: «Ці CPU занадто малі. Ми на них зекономили.»

Команда ескалувала. Закупівля потрапила на інженерну нараду. Хтось запропонував міняти SKU ноди. Після тижня дискусій SRE поставив одне набридливе питання: «А який PCIe лінк ми насправді погодили?»

Виявилось, що ноди були підключені через riser-конфігурацію, яка мовчки погодила PCIe з меншою шириною і поколінням, ніж очікувалось. GPUs були в порядку. CPU були в порядку. Шлях між ними — ні. H2D передачі були обмежені, і цикл тренування зупинявся на кожному копіюванні батчів.

Після виправлення розміщення слотів і налаштувань BIOS завантаження підскочило без жодного рядка коду. Постмортем не був про PCIe; він був про припущення: вони сприйняли «CPU не може нагодувати GPU» як пояснення, а не гіпотезу.

Міні-історія 2: Оптимізація, що дала протилежний ефект

Група інженерів даних хотіла «краще нагодувати GPU», тому вони агресивно збільшили кількість workers у dataloader і включили pinned memory скрізь. Пропускна здатність покращилась на тихому тестовому вузлі. Вони розгорнули це в спільний флот тренувань.

За день тренувальні задачі почали випадково падати. Деякі працювали швидко. Інші зависали. Деякі були вбиті OOM killer, хоча пам’ять GPU залишалась стабільною. On-call мав важкий час, бо графіки виглядали як «рандомні інфраструктурні флейки».

Корінна причина була буденною: pinned memory плюс багато worker-ів створили значний тиск на хост-RAM і алокатор сторінок. На нодах із іншими colocated сервісами і потребою файлового кешу «оптимізація» перетворилась на конкуренцію за пам’ять і хвильові латентності. Workers також підвищили навантаження на мережеве файлове сховище дрібними конкурентними читаннями, піднявши хвостову латентність для всіх.

Виправлення не полягало у відмові від паралелізму; його треба було правильно розмірити. Вони обмежили workers на ноду, сташували датасети локально для високопродуктивних задач і використовували pinned memory тільки там, де H2D був критичним. Годування GPU не означає голодувати ОС.

Міні-історія 3: Нудна, але правильна практика, що врятувала

Інша команда вела мульти-арендну платформу інференсу. У них було суворе правило: кожен інцидент продуктивності починається з відтворюваного захоплення метрик хоста, метрик GPU і короткого трасування профілю. Без винятків, без «мені здається».

Одної п’ятниці спрацювали тривоги латентності. Завантаження GPU було низьким, і простий висновок — «CPU насичений». Але їхній ранбук змусив швидко перевірити CPU run queue, частоти GPU, стан PCIe і латентність сховища для кешу моделей. CPU не був насичений. GPU не чекав на запуски. PCIe був здоровий.

Трасування показало, що процес inference блокується на читаннях файлів для шард моделей після розгортання. Невелика зміна перемістила директорію кешу моделей з локального NVMe на мережевий монт. Ніхто не думав, що це важливо, бо «модель вміщається в RAM», але не завжди вона лишається у page cache при високому навантаженні.

Вони відкотили шлях кешу, навмисно прогріли кеш, і інцидент закрився без героїчного дебагу. Практика, що їх врятувала, була не про складний профайлер. Вона була про чекліст, який забирав у них гонитву за неправильним сценарієм вузького місця.

Типові помилки: симптом → корінь → виправлення

1) Симптом: Низьке завантаження GPU, висока VRAM

Корінь: Ви виділили тензори на GPU, але обчислення малі або заблоковані синхронізацією/очікуванням на вхід.

Виправлення: Профілюйте прогалини; збільшіть batch size; приберіть per-step sync; перевірте пропускну здатність dataloader; знайдіть «гарячі» місця попередньої обробки на CPU.

2) Симптом: Завантаження сплескове, час кроку непостійний

Корінь: Джитер вхідного пайплайна (латентність сховища, віддалена файлова система, паузи GC, масштабування частоти CPU).

Виправлення: Сташуйте дані локально; шардуйте файли; використовуйте prefetch; ставте governor CPU в performance; обмежте worker-ів, щоб уникнути трешу.

3) Симптом: Одне CPU-ядро завантажене, інші прості, GPU не зайнятий

Корінь: Однопотоковий запуск або накладні витрати Python; GIL; забагато дрібних ядер; часті виклики драйвера.

Виправлення: Зливайте операції; використовуйте CUDA Graphs; батчуйте роботу; виносьте логіку з гарячого циклу Python; зменшуйте per-sample device виклики.

4) Симптом: Двигуни копіювання зайняті, SM низький

Корінь: Ботлнек передач (PCIe насичений, копії pageable memory, дрібні передачі, неправильний стан лінку).

Виправлення: Перевірте Gen/width; використовуйте pinned memory обережно; збільшіть batch size; накладайте копії на обчислення; зменшіть chatter хост↔пристрій.

5) Симптом: Частоти GPU низькі під навантаженням

Корінь: Ліміт потужності, тепловий тротлінг або GPU фактично простяє достатньо, щоб знизити частоту.

Виправлення: Перевірте ліміти потужності, охолодження, повітряний потік; якщо простоює — йдіть вгору по ланцюжку і знайдіть чекання.

6) Симптом: Продуктивність погіршилась після «більше workers»

Корінь: Конкуренція (шторм метаданих файлової системи, тиск RAM, накладні переключення контексту, треш кешу).

Виправлення: Правильно підійміть кількість worker-ів; використовуйте більші шарди; кешуйте; зменшуйте трансформації; вимірюйте end-to-end пропускну здатність, а не лише швидкість лоадера.

7) Симптом: Два «ідентичні» сервери мають величезну різницю

Корінь: Різний PCIe negotiation, NUMA розміщення, налаштування BIOS енергоспоживання, фонові демони або відмінності шляху сховища.

Виправлення: Порівняйте PCIe LnkSta, governor CPU, NUMA прив’язки та опції монтування. Стандартизуйте образ ноди та профіль BIOS.

8) Симптом: GPU недовантажений тільки при малих batch size

Корінь: Навантаження не має паралельізму; накладні витрати запуску і латентності пам’яті домінують при малих батчах.

Виправлення: Збільшіть batch size, використайте batching/queuing або прийміть низьке завантаження як компроміс латентності. Не ганяйтеся за 100% завантаження для п99 SLA.

Чеклісти / покроковий план

Чекліст A: Доведіть або спростуйте «CPU не може нагодувати GPU» за 20 хвилин

  1. Спостерігайте за поведінкою GPU: завантаження, частоти, енергія, пам’ять, сплесковість.
  2. Перевірте стан PCIe: погоджене покоління/ширина, помилки, топологія.
  3. Перевірте «форму» CPU: одне ядро заповнене vs багато ядер vs проста; тиск в черзі виконання.
  4. Перевірте iowait і шлях датасету: локальний vs мережевий; випадкові читання vs послідовні.
  5. Підтвердіть NUMA-локальність: афінітет CPU процесу та прив’язки пам’яті vs прикріплення GPU.
  6. Зробіть короткий профіль: визначте, чи час витрачається на запуск ядер, memcpy, декодування чи очікування.

Чекліст B: Якщо це CPU submission (залежне від запусків)

  1. Зменшіть кількість ядер: зливайте операції, скоротіть Python-цикли.
  2. Збільшуйте роботу на запуск: більші батчі, більші плитки, менше мікро-ядир.
  3. Приберіть точки синхронізації: уникайте примусових синхронізацій у гарячому шляху.
  4. Розгляньте CUDA Graphs або компільований шлях виконання, якщо фреймворк це підтримує.
  5. Перевірте з тим же датасетом і фіксованим seed, щоб не ганятися за шумом.

Чекліст C: Якщо це CPU preprocessing (декод/аугмент/токенізація)

  1. Виміряйте час на етапи (читання, декод, трансформ, батч, копія).
  2. Паралелізуйте обережно: більше workers аж доки не з’явиться contention, потім зупиніться.
  3. Віддавайте перевагу векторизованим операціям і бібліотекам, що використовують SIMD.
  4. Кешуйте дорогі трансформації, коли вони відтворювані.
  5. Переміщайте трансформації на GPU, коли це зменшує витрати CPU більше, ніж додає часу GPU.

Чекліст D: Якщо це I/O

  1. Сташуйте датасети локально для високопропускних запусків.
  2. Упакуйте багато дрібних файлів у шарди; уникайте per-sample open.
  3. Prefetch і читайте послідовно; збільшіть розміри запитів.
  4. Слідкуйте за операціями метаданих на мережевих файлових системах.
  5. Переконайтесь, що сховище не поділене та не насичене іншими задачами.

Чекліст E: Якщо це неефективність на GPU

  1. Використайте timeline profiler, щоб побачити стаги (пам’ять vs обчислення vs синхронізація).
  2. Переконайтесь, що ви використовуєте потрібні ядра (tensor cores, оптимізовані бібліотеки).
  3. Підлаштуйте batch size і точність, щоб підвищити арифметичну інтенсивність.
  4. Виправте макет і патерни доступу до пам’яті; уникайте дрібних ядер.
  5. Перестаньте звинувачувати CPU, коли GPU працює погано.

FAQ

1) Чи завжди низьке завантаження GPU — це погано?

Ні. Для інференсу з вимогою низької латентності з малими батчами низьке завантаження може бути очікуваним. Оптимізуйте під p95/p99 латентність, а не під красивий графік завантаження.

2) Який швидкий знак того, що вузьке місце — dataloader?

Завантаження GPU падає на межах батчів, CPU-ядра сплескують у user-часі, а пропускна здатність покращується при кешуванні локально або збільшенні workers (доки не виникне contention). Також: ривкові часи кроку.

3) Як відрізнити bottleneck PCIe від bottleneck CPU?

Якщо двигуни копіювання зайняті, а SM низький — підозрюйте PCIe/передачі. Підтвердіть погоджений стан лінку. Якщо CPU гарячий у викликах драйвера і бачите багато дрібних ядер — підозрюйте накладні витрати передачі завдань.

4) Чому «більше workers у dataloader» іноді гальмує?

Тому що конкуренція породжує contention: шторм метаданих на файловій системі, треш кешу, тиск пам’яті (особливо з pinned memory) і накладні переключення контексту. Пропускна здатність має максимум; знайдіть його.

5) Чи завжди pinned memory допомагає?

Вона допомагає DMA-передачам, але не безкоштовно. Забагато pinned memory знижує гнучкість ОС і може викликати нестабільність у мультиарендному середовищі. Використовуйте її там, де H2D дійсно критичний.

6) Чи може CPU «годувати» GPU на одному потоці?

Іноді. Для великих ядер і важкого обчислення один хост-потік може вистачати. Для робіт з багатьма дрібними ядрами, численними запусками або per-sample викликами один потік стає пляшковим горлом.

7) Чому два «однакові» вузли поводяться по-різному?

Бо вони не ідентичні в деталях: стан PCIe, NUMA-розміщення, налаштування BIOS енергоспоживання, фонові I/O або шляхи сховища можуть відрізнятись. Спершу виміряйте саме це.

8) Яке найпоширеніше непорозуміння за фразою «CPU не може нагодувати GPU»?

Люди вважають «GPU utilization» одним єдиним істинним метриком. Це середнє по складному таймлайну. Потрібно знати, чи GPU простяє, копіює, застряє через пам’ять або просто виконує короткі спалахи.

9) Що краще апгрейдити спочатку — CPU чи GPU, якщо тренування повільне?

Якщо ви не виміряли — ні того, ні іншого. Якщо профайли показують, що домінує попередня обробка на хості або накладні витрати запуску — допоможе CPU (або зміни в софті). Якщо домінують GPU-ядра і ви compute-bound — GPU допоможе. Якщо вузьке місце I/O — купуйте пропускну здатність сховища і кращі формати даних.

Подальші кроки (робіть це, а не абстрактні ідеї)

Якщо ви візьмете одну операційну настанову від мемної фрази «CPU не може нагодувати GPU», нехай це буде таке: фраза — не діагноз. Це підказка інструментувати пайплайн.

  1. Пройдіть швидкий план діагностики і класифікуйте вузьке місце: подача, попередня обробка, I/O або неефективність GPU.
  2. Підтвердіть фізичну правду: стан PCIe, NUMA афінітет, частоти і тротлінг. Виправте платформу перед правками коду.
  3. Виберіть один метрик, що відображає цінність для користувача (samples/sec, p99 latency, cost per batch) і оптимізуйте під нього. Не під красивий графік завантаження.
  4. Робіть зміни, що можна відкотити і виміряти: одна змінна за раз, той самий зріз датасету і відтворюваний прогін.
  5. Напишіть ранбук, якого вам бракує. Майбутній ви буде втомленішим і менш розумним.

Годування GPU — це системна проблема. CPU — лише один із офіціантів.

← Попередня
Обмеження швидкості в Postfix: запобігайте зловживанням без блокування реальних користувачів
Наступна →
Ubuntu 24.04: коли GRO/LRO/TSO оффлоуди ламають систему — як протестувати і безпечно відключити

Залишити коментар