📅 Квітень 2026⏱ ≈ 13 хв читання🎯 Просунутий·Останнє оновлення: 28 травня 2026 р.
Паралельні обчислення на GPU — SIMT, розгалуження варпів і модель Roofline
Сучасний дискретний GPU виконує десятки тисяч потоків одночасно. NVIDIA
H100 має 16 896 ядер CUDA та сягає пікового значення ~60 TFLOPS FP32.
Щоб досягти навіть 50% пікової продуктивності, потрібно розуміти модель
виконання — SIMT, варпи, ієрархію пам’яті та заповненість — і те, як
уникати вбивць продуктивності: розгалуження варпів, конфліктів банків
пам’яті та затримки глобальної пам’яті.
CPU SIMD (одна інструкція, багато даних): одна інструкція оперує над
ВЕКТОРОМ із 4/8/16 елементів даних одночасно. Наприклад, AVX-512:
одна інструкція додає 16 чисел float водночас. Програміст явно
векторизує; кожен елемент адресується незалежно. Розгалуження гілок:
обробляється маскуванням деяких смуг. GPU SIMT (одна інструкція,
багато потоків): одна інструкція оперує над 32 ПОТОКАМИ одночасно
(варп). Кожен потік має ВЛАСНІ регістри, лічильник команд та стек.
Потоки видаються програмісту незалежними. Ключова відмінність від
SIMD: SIMD — програміст явно керує шириною вектора. SIMT — апаратура
автоматично векторизує 32 потоки зі спільним PC. SIMT дозволяє: -
кожен потік може розгалужуватися незалежно (але див. розгалуження
варпів нижче) - кожен потік має власний файл регістрів (явне
маскування не потрібне) - потоки в межах варпа можуть добровільно
синхронізуватися (__syncthreads)
2. Варпи, блоки потоків та сітки
Ієрархія потоків (термінологія CUDA): Grid (усі блоки потоків для
одного запуску ядра) └── Block (до 1024 потоків, спільна пам’ять,
синхронізація через __syncthreads) └── Warp (32 потоки — одиниця
планування) └── Thread (незалежний файл регістрів + PC) Розгалуження
варпів: коли потоки в межах варпа беруть РІЗНІ гілки (if/else): варп
виконує ОБИДВА шляхи з маскуванням неактивних смуг. По суті
серіалізує: 32 потоки виконують шлях A, потім шлях B. Штраф
продуктивності: до 2× уповільнення при розгалуженні 50/50. Приклад —
уникайте цього: if (threadIdx.x % 2 == 0) { doA(); } else { doB(); } →
половина кожного варпа розгалужується. Краще: реструктуруйте так, щоб
блоки потоків були цілком A або цілком B. Ключові числа (NVIDIA
Ampere/Hopper): потоків на варп: 32 Макс. потоків на блок: 1024 Макс.
варпів на SM: 48–64 Розмір варпа дорівнює 32 від часів G80 (2006);
навряд чи зміниться.
Амортизація затримки планування варпів: затримка
глобальної пам’яті становить ~300–500 циклів. GPU це терплять,
перемикаючись на інший готовий варп, поки перший варп чекає на
пам’ять. Це приховування затримки працює лише за наявності
достатньої кількості інших готових варпів — звідси й важливість
заповненості.
3. Ієрархія пам’яті GPU
Рівень
Розмір
Затримка
Область видимості
Пропускна здатність
Регістри
~64K на SM
~1 цикл
Приватні для потоку
еквівалент ~120+ TFLOPS
Спільна пам’ять / L1
~48–228 КБ/SM
~20–40 циклів
Спільна для блоку
~ТБ/с сумарно
Кеш L2
~50 МБ (H100)
~200 циклів
На весь GPU
~7 ТБ/с
Глобальна (HBM)
~16–80 ГБ
~300–500 циклів
На весь GPU, видима хосту
3,35 ТБ/с (H100 SXM)
Конфлікти банків спільної пам’яті
Спільна пам’ять поділена на 32 БАНКИ (по одному на потік варпа).
Послідовні 4-байтові слова відображаються на послідовні банки: bank =
(addr/4) % 32. Доступ без конфліктів: кожен потік варпа влучає в інший
банк → 1 цикл. N-кратний конфлікт банку: N потоків звертаються до
банку B → N серіалізованих звернень. Поширена помилка: float
tile[32][32]; // Потік i звертається до tile[i][j]: крок по стовпцях =
1 (bank = 0,1,...31) ✓ // Потік i звертається до tile[j][i]: доступ по
рядках, усі потоки влучають у банк j % 32 // → 32-кратний конфлікт
банку! → у 32 рази повільніше Виправлення: додайте відступ: float
tile[32][33]; — зсуває кожен рядок на 1 слово, розподіляючи звернення
між різними банками.
4. Модель Roofline
Модель Roofline (Williams, Waterman, Patterson, 2009)
— це простий візуальний інструмент, що допомагає зрозуміти, чи ядро
обмежене обчисленнями, чи пам’яттю. Два «дахи» визначають стелю
продуктивності:
Арифметична інтенсивність (AI) = FLOPs ÷ байти трафіку пам’яті
Рівняння Roofline: Attainable_GFLOPs = min(Peak_GFLOPs, AI ×
Peak_Bandwidth_GB/s) Приклад: NVIDIA H100 SXM Пікова продуктивність
FP32: ~60 TFLOPS = 60 000 GFLOPS Пропускна здатність HBM: ~3 350 GB/s
AI у точці зламу: 60 000 / 3 350 ≈ 17,9 FLOP/байт Ядро з AI < 17,9
обмежене пам’яттю (обмежене пропускною здатністю). Ядро з AI > 17,9
обмежене обчисленнями (обмежене FLOP). Поширені арифметичні
інтенсивності ядер: копіювання вектора (y = x): AI = 0,08 (2 байти, ~0
FLOP) → глибоко обмежене пам’яттю DAXPY (y = αx + y): AI = 0,125 →
обмежене пам’яттю Розріджена матриця на вектор: AI ~ 0,25–2,0 →
обмежене пам’яттю Множення щільних матриць: AI ~ n/2 (зростає з n) →
обмежене обчисленнями для великих n Стратегія оптимізації: Обмежене
пам’яттю → зменшити байти (напівточність, тайлінг, злиття, кешування)
Обмежене обчисленнями → зменшити FLOP (кращі алгоритми) або додати
паралелізм
5. Заповненість та обмежувальні ресурси
Заповненість = активні варпи на SM / максимум варпів на SM Ресурси, що
обмежують заповненість (на SM, обирайте конфігурацію блоку для
максимізації): 1. Спільна пам’ять: виділення більшої спільної пам’яті
на блок → менше блоків вміщається. 2. Регістри: більше регістрів на
потік → менше варпів вміщається. 3. Макс. блоків: апаратне обмеження
на одночасні блоки на SM. Приклад: SM може вмістити макс. 48 варпів.
Блок із 256 потоків = 8 варпів. Якщо кожен блок використовує 16 КБ
спільної пам’яті, а SM має 64 КБ: → макс. 4 блоки = 32 варпи →
заповненість = 32/48 = 67%. Якщо блок — 128 потоків (4 варпи), лише 8
КБ спільної: → 8 блоків = 32 варпи → заповненість = 67% (та сама,
обмежена інакше). Зменшіть до 4 КБ → 16 блоків = 48 варпів → 100%
заповненість. Регістри: якщо ядро використовує 64 регістри/потік, а SM
має 64K регістрів: 64K / 64 = 1024 потоки = 32 варпи → заповненість
обмежена. Важливо: 100% заповненість не гарантує пікової
продуктивності! Обмежене обчисленнями ядро може працювати швидко при
50% заповненості, якщо тримає всі функціональні блоки зайнятими.
Використовуйте NVIDIA Nsight Compute для профілювання. Формула
калькулятора заповненості NVIDIA CUDA: active_blocks = min(
floor(max_shared / shared_per_block), floor(max_registers /
(reg_per_thread × block_size)), max_blocks_per_sm ) active_warps =
active_blocks × ceil(block_size / 32)
6. Обчислювальні шейдери WebGPU
WebGPU (доступний у Chrome 113+, Firefox Nightly) надає обчислювальні
шейдери GPU у браузері за допомогою WGSL (WebGPU Shading Language):
// Обчислювальний шейдер WGSL — паралельна префіксна сума (один прохід)@group(0) @binding(0)var<storage, read> input : array<f32>;
@group(0) @binding(1)var<storage, read_write> output : array<f32>;
@group(0) @binding(2)var<uniform> params : struct { n: u32 };
var<workgroup> shared_data : array<f32, 256>;
@compute @workgroup_size(256, 1, 1)fnmain(
@builtin(global_invocation_id) gid : vec3<u32>,
@builtin(local_invocation_id) lid : vec3<u32>
) {
let i = gid.x;
let l = lid.x;
// Завантаження у спільну пам’ять робочої групи
shared_data[l] = select(0.0f, input[i], i < params.n);
workgroupBarrier();
// Паралельне сканування Hillis-Steele (log₂ 256 = 8 ітерацій)for (var offset = 1u; offset < 256u; offset <<= 1u) {
if (l >= offset) {
shared_data[l] += shared_data[l - offset];
}
workgroupBarrier();
}
if (i < params.n) { output[i] = shared_data[l]; }
}
Запускайте цей шейдер через
passEncoder.dispatchWorkgroups(Math.ceil(n/256), 1, 1).
Кожна робоча група обробляє 256 елементів за ~8 розділених бар’єрами
проходів, причому спільна пам’ять усуває звернення до глобальної
пам’яті між проходами.
7. Ключові патерни оптимізації
Об’єднаний доступ до глобальної пам’яті: потоки
варпа мають звертатися до послідовних адрес пам’яті (крок 1).
Необ’єднаний доступ фрагментується на кілька транзакцій. За змоги
забезпечуйте розкладку «структура масивів» (SoA) замість «масив
структур» (AoS).
Огрублення потоків: призначайте кілька елементів на
потік, щоб зменшити накладні витрати на планування та поліпшити
паралелізм на рівні інструкцій у межах потоку. Корисно, коли
заповненість уже висока.
Злиття ядер: об’єднуйте кілька обмежених пам’яттю
ядер в одне, щоб уникнути повторних читань/записів глобальної пам’яті
між ядрами. Може підвищити арифметичну інтенсивність до
обчислювального даху.
Тайлінг для множення матриць: завантажуйте тайли з
глобальної пам’яті у спільну, обчислюйте часткові суми, накопичуйте в
регістрах. Перетворює O(N³) глобальних байтів на O(N³/T) з розміром
тайла T, різко підвищуючи арифметичну інтенсивність.
Примітиви рівня варпа: використовуйте інструкції
перемішування (__shfl_xor_sync) для обміну в межах
варпа без спільної пам’яті — нульова затримка, без конфліктів банків.
Асинхронні копіювання (Ampere+):memcpy_async перекриває завантаження даних з
обчисленнями за допомогою програмного конвеєра (упереджене
завантаження двох тайлів по черзі).
Змішана точність: FP16 має 2× пропускну здатність,
тензорні ядра BF16/TF32 дають 4–8× пропускної здатності для множення
матриць. Використовуйте накопичення FP16/BF16 там, де точність
дозволяє.
CPU проти GPU: топовий CPU (32 ядра, AVX-512) дає ~3
TFLOPS FP32. Дискретний GPU (H100) дає ~60 TFLOPS. Але пропускна
здатність реалізується лише для паралельних навантажень з достатньою
арифметичною інтенсивністю — послідовний код із залежностями працює
значно швидше на CPU.