GPU · Архітектура комп’ютерів · Паралельні обчислення
📅 Квітень 2026 ⏱ ≈ 13 хв читання 🎯 Просунутий · Останнє оновлення: 28 травня 2026 р.

Паралельні обчислення на GPU — SIMT, розгалуження варпів і модель Roofline

Сучасний дискретний GPU виконує десятки тисяч потоків одночасно. NVIDIA H100 має 16 896 ядер CUDA та сягає пікового значення ~60 TFLOPS FP32. Щоб досягти навіть 50% пікової продуктивності, потрібно розуміти модель виконання — SIMT, варпи, ієрархію пам’яті та заповненість — і те, як уникати вбивць продуктивності: розгалуження варпів, конфліктів банків пам’яті та затримки глобальної пам’яті.

1. Виконання SIMD проти 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)
fn main(
  @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. Ключові патерни оптимізації

CPU проти GPU: топовий CPU (32 ядра, AVX-512) дає ~3 TFLOPS FP32. Дискретний GPU (H100) дає ~60 TFLOPS. Але пропускна здатність реалізується лише для паралельних навантажень з достатньою арифметичною інтенсивністю — послідовний код із залежностями працює значно швидше на CPU.
⚙️ Дослідити алгоритми →