FinCodeCow · Обзор AI-инфраструктуры

GPU, CUDA и occupancy: от SIMT до roofline

Обзор шестой главы книги Chris Fregly «AI Systems Performance Engineering» (O'Reilly, 2026) — с изложением содержания, визуализациями, проверкой прогнозов и систематизацией ключевых положений
Об источнике

Настоящий материал представляет собой обзор шестой главы книги Chris Fregly, «AI Systems Performance Engineering: Optimizing Model Training and Inference Workloads with GPUs, CUDA, and PyTorch», O'Reilly Media, 2026 (Copyright © 2026 Flux Capacitor, LLC).

Прямые цитаты из книги выделены отдельными блоками с пометкой «Цитата из книги». Остальной текст — систематизированное изложение содержания главы, сопоставление с актуальными данными об архитектуре Blackwell, проверка прогнозов по открытым источникам, а также дополнительные замечания, выходящие за пределы текста главы. Цитаты приводятся в ограниченном объёме в целях обзора и критического анализа.

Шестая глава книги Fregly посвящена переходу от высокоуровневых представлений об AI-системах к конкретике исполнения на GPU. В ней разбирается модель SIMT, иерархия потоков и памяти, способы настройки запуска ядер, концепция occupancy, модель roofline и инструменты профилирования. Эта глава — фундамент для всех последующих глав, посвящённых оптимизации CUDA-ядер, распределённому обучению и inference. Ниже последовательно изложены ключевые положения главы, приведены визуализации, примеры кода и уточнения по актуальным данным об архитектуре Blackwell.

• • •

IУстройство потокового мультипроцессора

Цитата из книги · Глава 6

«Каждый GPU состоит из множества SM, которые примерно аналогичны ядрам CPU, но оптимизированы под параллелизм. Каждый SM на Blackwell может отслеживать до 64 варпов (групп по 32 потока)».

«Каждый GPU Blackwell включает множество SM — подобных ядрам CPU, но оптимизированных под пропускную способность. На современных GPU каждый SM отслеживает до 64 варпов (2 048 потоков) одновременно. GPU Blackwell располагают 64K 32-битных регистров на SM (256 КБ суммарно) и объединёнными 256 КБ кэша L1 и разделяемой памяти на SM. До 228 КБ (227 КБ полезных) из этой SRAM могут быть сконфигурированы как управляемая пользователем разделяемая память на SM».

Изложение содержания

Потоковый мультипроцессор (SM) — основная вычислительная единица GPU. В отличие от CPU-ядра, оптимизированного под одну последовательную задачу, SM устроен так, чтобы одновременно держать в работе сотни потоков. На Blackwell каждый SM хранит до 64 активных варпов (групп по 32 потока), что даёт 2048 резидентных потоков.

Регистровый файл — 64K 32-битных регистров (256 КБ), из которых одному потоку доступно не более 255. Унифицированный блок SRAM объёмом 256 КБ на SM делится между кэшем L1 и разделяемой памятью, причём до 228 КБ (227 КБ полезных) можно выделить под разделяемую память. Это ключевое архитектурное решение: разработчик может динамически выбирать, что важнее для конкретного ядра — кэш для глобальных данных или программно управляемая разделяемая память для обмена между потоками блока.

Цитата из книги · Глава 6

«Внутри SM Blackwell несколько планировщиков варпов выдают инструкции доступным конвейерам; четыре независимых планировщика варпов позволяют до четырём варпам выдавать инструкции доступным конвейерам каждый такт. Более того, каждый планировщик поддерживает dual-issue — возможность выдать две независимые инструкции (например, одну арифметическую и одну операцию с памятью) за один варп».

Изложение содержания

SM Blackwell разделён на четыре независимых планировщика варпов. Каждый такт каждый планировщик выбирает один готовый варп и может выдать две инструкции: одну арифметическую (INT32, FP32 или Tensor Core) и одну операцию с памятью (загрузка или сохранение). Теоретический пик — четыре варпа × две инструкции = восемь выдач за такт. Это возможно, только если двойная выдача (dual-issue) исходит от одного и того же варпа — комбинировать инструкции из разных варпов в одной выдаче нельзя.

Блок специальных функций (SFU) обрабатывает трансцендентные операции — синус, косинус, обратный квадратный корень и т. д. У SFU собственный конвейер, отдельный от INT32/FP32 и от загрузки/сохранения. Благодаря этому медленные трансцендентные вычисления не блокируют основные вычислительные и памятьные конвейеры, а параллелизм на уровне инструкций возрастает.

Важное уточнение для современного поколения: на Blackwell FP32 и INT32 объединены в единые CUDA-ядра, поэтому в одном такте на одном ядре выполняется либо FP32-, либо INT32-инструкция. Mixed INT32/FP32-потоки больше не выигрывают от dual-issue на одном ядре — вместо этого они используют параллелизм варпов и планировщиков. Этот нюанс меняет стратегию оптимизации ILP: вместо чередования INT и FP в одном потоке выгоднее держать в полёте две независимые FP-инструкции.

Схема 1. Планировщики варпов

Четыре независимых планировщика, по два конвейера на каждый

4 schedulers / dual issue
Описание: Четыре планировщика выбирают по одному готовому варпу за такт и могут выдать до двух инструкций (арифметика + память) из одного варпа. Итог — до 8 выдач за такт на SM. На Blackwell FP32 и INT32 объединены в единый конвейер, поэтому mixed-потоки больше не выигрывают от dual-issue на одном ядре — нужен ILP из двух независимых операций одного типа.
Параметр планировщикаЗначение на Blackwell
Количество планировщиков4
Варпов за такт (максимум)4 (по одному на планировщик)
Арифметических инструкций за такт4
Операций с памятью за такт4
Всего выдач за такт (с dual-issue)до 8
Максимум регистров на поток255
Максимум потоков на блок1024
Максимум варпов на SM64
Итоги раздела I
Ключевое положение главы

SM — throughput-ориентированный процессор, разделённый на четыре независимых планировщика. Каждый такт возможна выдача до двух инструкций из одного варпа. Пик — восемь выдач за такт при полной загрузке.

Ограничение, отмеченное в главе

Регистров на поток — до 255, потоков на блок — до 1024, варпов на SM — 64. Эти числа определяют, сколько работы поместится на один SM и как будет выглядеть occupancy.

Дополнение по архитектуре Blackwell

На Blackwell FP32 и INT32 объединены в единые CUDA-ядра. Mixed-потоки следует оптимизировать через параллелизм варпов, а не через чередование типов инструкций в одном потоке.

• • •

IIПотоки, варпы, блоки и сетки

Цитата из книги · Глава 6

«CUDA структурирует параллельную работу в трёхуровневую иерархию — потоки, блоки потоков (они же cooperative thread arrays [CTA]) и сетки — чтобы уравновесить программируемость и массовую пропускную способность. На нижнем уровне каждый поток выполняет код вашего ядра. Вы группируете потоки в блоки потоков размером до 1 024 потоков на современных GPU. Блоки потоков образуют сетку при запуске ядра».

«Блоки потоков подразделяются на варпы по 32 потока, которые выполняются синхронно в модели SIMT с использованием планировщика варпов».

Изложение содержания

CUDA организует параллельную работу в три уровня: поток → блок потоков (CTA) → сетка. Блок содержит до 1024 потоков; блоки образуют сетку, запускаемую одним вызовом ядра. Внутри блока потоки делят низколатентную разделяемую память и синхронизируются барьером __syncthreads(). Блоки выполняются независимо и в произвольном порядке — планировщик GPU сам распределяет их по SM.

Каждый блок делится на варпы по 32 потока, которые выполняются синхронно в модели SIMT: все потоки варпа проходят один и тот же путь управления. Если часть потоков уходит в ветвь if, а другая — в else, варп выполняет обе ветви последовательно, замаскировав неактивные линии. Это называется расхождением варпов. Разные варпы могут идти по разным ветвям без потери производительности — расхождение затрагивает только потоки внутри одного варпа.

Цитата из книги · Глава 6

«Традиционно потоки из разных блоков потоков не могли взаимодействовать друг с другом напрямую. Однако современные архитектуры GPU и версии CUDA поддерживают кластеры блоков потоков. Кластеры блоков потоков — это группы блоков потоков, которые могут обмениваться данными друг с другом между SM».

Изложение содержания

Кластеры блоков — относительно новая возможность, появившаяся в Hopper и развитая в Blackwell. Внутри кластера потоки из разных блоков могут обращаться к разделяемой памяти друг друга через распределённую разделяемую память (DSMEM). Это позволяет строить более крупные вычислительные конструкции, чем один блок, и синхронизировать их через барьеры уровня кластера, не прибегая к глобальной памяти.

Практический смысл: для задач вроде матричных умножений с большими тайлами можно использовать кластер из двух или четырёх блоков, чтобы эффективно увеличить размер tile и повысить арифметическую интенсивность без выхода за пределы одного SM.

Схема 2. Потоки, блоки, сетки

Три уровня иерархии параллелизма CUDA

block (CTA) grid
Описание: Сетка (grid) содержит блоки, блок (CTA) содержит потоки. Блоки независимы, потоки внутри блока синхронизируются барьером.

Схема 3. Варпы

32 потока, движущихся в lockstep

warp = 32 threads, lockstep
Описание: Варп — 32 потока, выполняющихся синхронно. Все потоки варпа проходят одну инструкцию за такт (SIMT).

Схема 4. Расхождение варпов

Ветвление внутри варпа сериализует исполнение

divergence / uniformity
Описание: Если потоки одного варпа идут в разные ветви if/else, варп выполняет обе последовательно. Разные варпы расходятся без штрафа.

Схема 5. Распределённая разделяемая память

DSMEM: блоки кластера видят SMEM друг друга

DSMEM SMEM block 0 SMEM block 1
Описание: Кластеры блоков позволяют обращаться к разделяемой памяти соседних блоков через DSMEM и использовать барьеры уровня кластера.
Итоги раздела II
Ключевое положение главы

Три уровня иерархии — поток, блок, сетка. Внутри блока потоки делят shared memory и синхронизируются через __syncthreads(). Блоки независимы. Варп — 32 потока, выполняющих одни инструкции синхронно.

Расхождение варпов

Если потоки одного варпа идут по разным ветвям if/else, варп выполняет обе ветви последовательно, маскируя неактивные линии. Расхождение затрагивает только потоки внутри одного варпа.

Дополнение: кластеры блоков

Начиная с Hopper, блоки могут объединяться в кластеры и обращаться к shared memory друг друга через DSMEM. Это позволяет увеличить эффективный размер tile без выхода за пределы одного SM.

• • •

IIIНастройка параметров запуска ядра

Цитата из книги · Глава 6

«Когда вы вызываете ядро из кода CPU (хоста), вы используете синтаксис <<< >>> "шеврон" для указания того, сколько потоков должно выполняться — и как они организованы — с помощью двух параметров конфигурации: blocksPerGrid для числа блоков потоков и threadsPerBlock для числа потоков внутри каждого блока».

«Критически важный аспект производительности GPU — выбор размера блока потоков, согласованного с аппаратным размером варпа в 32 потока. Как следствие, обычно выбирают размеры блоков потоков, кратные 32».

Изложение содержания

Вызов ядра в CUDA выглядит как kernel<<<blocksPerGrid, threadsPerBlock>>>(...). Выбор этих двух параметров напрямую влияет на occupancy — долю аппаратных слотов потоков, занятых активными варпами.

Размер блока должен быть кратен 32 (размеру варпа). Блок из 256 потоков — восемь полных варпов — это универсальная отправная точка. Такой размер даёт несколько преимуществ: нет недозаполненных варпов, хорошая occupancy (до восьми блоков на SM), баланс между ресурсами и параллелизмом.

Для blocksPerGrid используется формула (N + threadsPerBlock - 1) / threadsPerBlock, которая округляет вверх и гарантирует, что все элементы массива будут покрыты. Обязательна проверка границ if (idx < N): без неё «лишние» потоки последнего блока попытаются обратиться к памяти за пределами массива и вызовут cudaErrorIllegalAddress.

Схема 6. Поток программирования CUDA

Host запускает kernel, GPU его исполняет

CPU GPU kernel
Описание: Хост (CPU) запускает ядро в асинхронном режиме. Устройство (GPU) исполняет его, хост продолжает работу.

Схема 7. Иерархия потоков

Host → launch → GPU grid

CPU host GPU grid
Описание: Хост инициирует запуск, GPU разворачивает грид блоков. Каждый блок распределяется планировщиком на свободный SM.

Размер блока: почему 256?

Размер блока должен быть кратен 32 (размеру варпа). Блок из 256 потоков — восемь полных варпов — это универсальная отправная точка. Причины:

  • Нет недозаполненных варпов. 256 делится на 32 без остатка, каждый варп содержит ровно 32 активных потока.
  • Хорошая occupancy. При 256 потоках на блок на SM помещается до 8 блоков (2048 ÷ 256), что полностью насыщает лимит резидентных потоков.
  • Баланс ресурсов. 256 — достаточно мало, чтобы не упереться в лимит 1024 потока на блок, и достаточно велико, чтобы обеспечить много варпов для скрытия задержек.
__global__ void doubleKernel(float* data, int N) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        data[idx] *= 2.0f;
    }
}

int main() {
    const int N = 1'000'000;
    float *h_data, *d_data;
    cudaMallocHost(&h_data, N * sizeof(float));
    for (int i = 0; i < N; ++i) h_data[i] = 1.0f;
    cudaMalloc(&d_data, N * sizeof(float));
    cudaMemcpy(d_data, h_data, N * sizeof(float),
               cudaMemcpyHostToDevice);
    const int threadsPerBlock = 256;
    const int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
    doubleKernel<<>>(d_data, N);
    cudaDeviceSynchronize();
    cudaFree(d_data);
    cudaFreeHost(h_data);
    return 0;
}
Осторожно

Проверка границ if (idx < N) критически важна: без неё «лишние» потоки последнего блока попытаются обратиться к памяти за пределами массива и вызовут cudaErrorIllegalAddress. CUDA не генерирует исключения внутри ядер — ошибка устанавливается в глобальный флаг и проявляется при следующем вызове синхронизации на хосте.

Для двумерных данных используется двумерная сетка блоков. Например, обработка изображения 1024×1024 с блоком 16×16 (256 потоков):

__global__ void double2DKernel(float* image, int width, int height) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    if (x < width && y < height) {
        int idx = y * width + x;
        image[idx] *= 2.0f;
    }
}

int main() {
    const int width = 1024;
    const int height = 1024;
    const int N = width * height;
    float *h_image, *d_image;
    cudaMallocHost(&h_image, N * sizeof(float));
    cudaMalloc(&d_image, N * sizeof(float));
    dim3 threadsPerBlock(16, 16);
    dim3 blocksPerGrid((width + 15) / 16, (height + 15) / 16);
    double2DKernel<<>>(d_image, width, height);
    cudaDeviceSynchronize();
    cudaFree(d_image);
    cudaFreeHost(h_image);
    return 0;
}
Итоги раздела III
Ключевое положение главы

Размер блока должен быть кратен 32. 256 потоков — восьмиварповый блок — универсальная отправная точка. Формула (N + threadsPerBlock - 1) / threadsPerBlock округляет вверх и покрывает все элементы.

Ограничение

Обязательна проверка границ if (idx < N). Без неё лишние потоки последнего блока вызовут ошибку доступа к памяти.

Дополнение: 2D- и 3D-ядра

Для изображений и объёмных данных используются 2D- и 3D-сетки через dim3. Это позволяет естественным образом отображать многомерные данные на потоки GPU.

• • •

IVИерархия памяти: от регистров до HBM

Цитата из книги · Глава 6

«GPU предоставляет многоуровневую иерархию памяти и помогает балансировать ёмкость и скорость. Иерархия включает регистры, разделяемую память, кэши, глобальную память и специализированную TMEM на GPU Blackwell и последующих. TMEM — это выделенная внутрикристальная память объёмом ~256 КБ на SM, используемая инструкциями Tensor Core 5-го поколения на Blackwell».

Изложение содержания

Уровни памяти GPU отличаются по ёмкости, задержке и пропускной способности на несколько порядков. Понимание этой лестницы — ключ к написанию быстрых ядер.

На вершине — регистры: однотактовые, но крайне ограниченные (до 255 на поток). Чуть ниже — разделяемая память и L1-кэш: 20–30 тактов, до 228 КБ на SM. Ещё ниже — L2-кэш: 126 МБ, общий для всего GPU, ~200 тактов. На дне — глобальная память HBM3e: сотни и тысячи тактов, ~8 ТБ/с. Чем дальше от вычислительного ядра, тем больше ёмкость и тем выше задержка.

Отдельно стоит выделить Tensor Memory (TMEM) — специализированную память на 256 КБ на SM, которая используется инструкциями Tensor Core нового поколения. TMEM не адресуется обычными указателями; обмен идёт через инструкции tcgen05. Это ещё один уровень иерархии, который появился с приходом Tensor Cores и Transformer Engine.

Схема 8. Иерархия памяти

Пирамида: от регистров к HBM

regs → L1/SMEM → L2 → HBM → CPU
Описание: Чем ближе уровень — тем меньше ёмкость и задержка, но выше пропускная способность на SM. Дальние уровни — общие для всего GPU.

Схема 9. SMEM и TMEM вокруг Tensor Core

Разделяемая память и тензорная память на одном SM

SMEM TMEM Tensor Cores C = A × B
Описание: Tensor Cores получают операнды из SMEM и TMEM, пишут аккумулятор в TMEM. TMEM не адресуется обычными указателями — обмен идёт через инструкции tcgen05.

Схема 10. Разделяемая память блока

Программно управляемый кэш на SM

__shared__
Описание: Разделяемая память — низколатентный (20–30 тактов) программно управляемый буфер внутри блока. Живёт только пока блок активен.

Схема 11. Локальная память и spill

Когда регистров не хватает — данные уходят в DRAM

DRAM / local spill
Описание: Register spill — вытеснение «лишних» регистров в локальную память (область в DRAM). Обращение стоит сотни тактов и убивает производительность.

Схема 12. HBM и GPU

Глобальная память — основной канал данных

GPU HBM / global memory
Описание: HBM3e обеспечивает до ~8 ТБ/с, но задержка в сотни тактов делает её самым медленным звеном в цепочке.
УровеньОбластьЁмкость (Blackwell)ЗадержкаПропускная способность
РегистрыНа поток, на SM64K × 32 бит (макс. 255 на поток)1 тактДесятки ТБ/с на SM
Разделяемая память / L1На SM228 КБ (227 полезных)20–30 тактовТБ/с на SM
TMEMНа SM256 КБ~10 тактовТБ/с к Tensor Core
Кэш константНа SM~8 КБ (для 64 КБ __constant__)~1 такт (broadcast)Как у регистра
L2На весь GPU126 МБ~200 тактовДесятки ТБ/с
Глобальная (HBM3e)На устройство180 ГБ (B200) / ~288 ГБ (B300)Сотни–тысячи тактов~8 ТБ/с
Итоги раздела IV
Ключевое положение главы

Иерархия памяти GPU — регистры → shared/L1 → L2 → HBM. Для максимальной производительности данные должны оставаться как можно выше в этой иерархии.

Практическое следствие

Register spill — одна из главных причин падения производительности. Переполнение регистрового файла вытесняет данные в локальную память, что эквивалентно обращению к DRAM. Профилировать и устранять spill необходимо в первую очередь.

• • •

VUnified Memory

Цитата из книги · Глава 6

«Unified Memory (также известная как CUDA Managed Memory) даёт вам единое когерентное адресное пространство, охватывающее и CPU, и GPU, так что вам больше не нужно жонглировать отдельными буферами хоста и устройства или явно вызывать cudaMemcpy».

«Хотя работа с Unified Memory очень удобна для разработчика, она может вызывать нежелательные миграции страниц по требованию между CPU и GPU. Это порождает скрытые задержки и простои исполнения».

Изложение содержания

CUDA Managed Memory (Unified Memory) создаёт единое адресное пространство для CPU и GPU. Вы выделяете память через cudaMallocManaged() — и обе стороны видят один указатель. Страницы мигрируют по требованию: если GPU обращается к данным, находящимся в ОЗУ CPU, происходит страничная ошибка и данные переносятся через NVLink-C2C (на Grace Blackwell) или PCIe.

Удобство обманчиво. Автоматические миграции по требованию могут приводить к скрытым задержкам: если поток GPU обращается к странице, которая физически лежит в памяти CPU, ядро останавливается и ждёт переноса. На системах с PCIe это может быть на порядки медленнее, чем обращение к HBM. На Grace Blackwell с NVLink-C2C переносы быстрее, но всё равно не бесплатны.

Решение — явное управление размещением: cudaMemPrefetchAsync для упреждающей миграции страниц на GPU и cudaMemAdvise для подсказок драйверу о том, где данные будут использоваться. Это превращает миграции из синхронных страничных ошибок в асинхронные операции, которые можно перекрыть с вычислениями.

Схема 13. Unified Memory

Страницы мигрируют между CPU и GPU по требованию

CPU GPU
Описание: Один указатель, одна аллокация. Страничные ошибки переносят данные между ОЗУ и HBM через NVLink-C2C или PCIe.

Схема 14. Предзагрузка

cudaMemPrefetchAsync — упреждающая миграция

NVLink-C2C cudaMemPrefetchAsync()
Описание: Предзагрузка страниц на GPU заранее — до запуска ядра. Устраняет синхронные миграции по страничным ошибкам.

Схема 15. Подсказки драйверу

cudaMemAdvise: preferred location, ReadMostly

preferred location / ReadMostly
Описание: cudaMemAdvise подсказывает драйверу, где выгоднее держать страницы и как их использовать. Снижает количество миграций.
cudaMemPrefetchAsync(ptr, size, gpuId, stream);
cudaMemAdvise(ptr, size, cudaMemAdviseSetPreferredLocation, gpuId);
cudaMemAdvise(ptr, size, cudaMemAdviseSetReadMostly, gpuId);
cudaStreamAttachMemAsync(stream, ptr, 0, cudaMemAttachSingle);
Итоги раздела V
Ключевое положение главы

Unified Memory — удобно, но требует явного управления размещением. Автоматические миграции по страничным ошибкам могут вносить скрытые задержки.

Практическое следствие

Используйте cudaMemPrefetchAsync для упреждающей миграции и cudaMemAdvise для подсказок драйверу. Это превращает синхронные миграции в асинхронные, которые можно перекрыть с вычислениями.

• • •

VIOccupancy и скрытие задержек

Цитата из книги · Глава 6

«GPU поддерживают производительность за счёт одновременного выполнения множества варпов, так что когда один варп останавливается в ожидании данных, может выполняться другой. Эта способность быстро переключаться между варпами позволяет GPU скрывать задержку памяти. Как мы описывали ранее, доля ёмкости SM, фактически занятая активными варпами, называется occupancy».

«Если occupancy низкая (всего несколько активных варпов), SM может простаивать, пока один варп ждёт памяти. Это приводит к плохой загрузке SM».

Изложение содержания

Occupancy — отношение активных варпов на SM к теоретическому максимуму (64 варпа, 2048 потоков на Blackwell). Высокая occupancy позволяет планировщику моментально переключаться на готовый варп, когда текущий остановился на ожидании памяти. Это и есть скрытие задержки.

Но occupancy — не самоцель. Если ядро ограничено пропускной способностью памяти (memory-bound), доведение occupancy до 100% не поможет — узкое место в шине HBM, а не в количестве варпов. Более информативные метрики — eligible warps per cycle и warp stall reasons: сколько варпов реально готовы к исполнению и на чём останавливаются остальные.

Пример из главы показывает разницу между последовательной и параллельной реализацией сложения векторов. Последовательная версия использует один поток и загружает GPU на 1,5%. Параллельная распределяет работу и загружает GPU на 95%. Это наглядная иллюстрация того, что GPU — throughput-процессор и для его загрузки нужна массовая параллельность.

Схема 16. Таймлайн: последовательно vs параллельно

Параллельная версия делает то же за меньшее время

sequential / parallel
Описание: Последовательная версия (оранжевая) выполняет всю работу одним потоком. Параллельная (циан) распределяет нагрузку и заканчивает раньше.

Схема 17. Векторизованное сложение

A + B = C — по одному потоку на элемент

A + B = C (parallel)
Описание: Каждый поток обрабатывает один элемент — векторизация заменяет цикл на параллельный запуск.
МетрикаaddSequentialaddParallel
Время ядра (мс)48,212,17
Загрузка GPU1,5%95%
Occupancy1,3%38,7%
Эффективность варпов3,1%100%

Для управления occupancy на уровне компилятора используются аннотации __launch_bounds__. Они подсказывают компилятору, с каким максимальным числом потоков на блок будет запускаться ядро и сколько блоков минимум должно помещаться на SM. Компилятор ограничивает регистры на поток, чтобы эти условия выполнились. Runtime-версия — CUDA Occupancy API:

__global__ __launch_bounds__(256, 16)
void myKernel(...) { /* ... */ }

int minGrid = 0, bestBlock = 0;
cudaOccupancyMaxPotentialBlockSize(&minGrid, &bestBlock,
                                   myKernel,
                                   0,
                                   0);
Итоги раздела VI
Ключевое положение главы

Occupancy — средство для скрытия задержки, а не цель. Высокая occupancy помогает, только если узкое место — задержка, а не пропускная способность памяти.

Метрики, которые важнее occupancy

eligible warps per cycle и warp stall reasons показывают, сколько варпов реально готовы к исполнению и на чём останавливаются остальные. Именно на них следует ориентироваться при тюнинге.

Практическое следствие

Для управления occupancy используйте __launch_bounds__ и CUDA Occupancy API. Помните, что увеличение occupancy за счёт регистров может привести к spill и падению производительности.

• • •

VIIКоалесцирование и конфликты банков

Цитата из книги · Глава 6

«Обращения к глобальной памяти быстрее всего, когда потоки варпа обращаются к непрерывным адресам памяти, которые аппаратура может объединить в меньшее число более крупных транзакций. Если потоки обращаются к разбросанным или невыровненным адресам, устройство не может объединить запросы в минимальное число транзакций кэш-линий, которые на современных GPU представляют собой 128-байтовые линии, состоящие из четырёх 32-байтовых секторов».

Изложение содержания

Когда 32 потока варпа одновременно читают глобальную память, GPU пытается объединить (коалесцировать) эти обращения в минимальное количество транзакций по 128 байт. Если потоки обращаются к последовательным 32-битным элементам, одна транзакция обслуживает весь варп. Если потоки разбросаны по памяти — каждый элемент требует отдельной транзакции.

Аналогичная проблема возникает в разделяемой памяти, но там она называется конфликтами банков. Разделяемая память состоит из 32 банков по 4 байта. Если несколько потоков варпа обращаются к одному банку, доступы сериализуются. Классический случай — доступ по столбцам к матрице, хранящейся по строкам. Решение — добавить padding: один лишний элемент на строку. Для матрицы 32×32, хранящейся как smem[32][33], обращения по столбцам не создают конфликтов.

int idx = blockIdx.x * blockDim.x + threadIdx.x;
float val = data[idx];

float val = data[threadIdx.x * 1024];

__shared__ float smem[32][33];
float val = smem[threadIdx.x][threadIdx.y];
Практика

При доступе к разделяемой памяти по столбцам матрицы, хранящейся по строкам, добавьте padding — один лишний элемент на строку (int stride = width + 1). Это сдвигает банки и устраняет конфликты. Для матрицы 32×32, хранящейся как smem[32][33], обращения по столбцам не создают конфликтов.

Итоги раздела VII
Ключевое положение главы

Коалесцирование — основа пропускной способности глобальной памяти. Последовательный доступ 32 потоков к 128 смежным байтам — одна транзакция. Разбросанный доступ — 32 транзакции.

Конфликты банков

Конфликты банков убивают пропускную способность shared memory. Padding на один элемент — простейший приём их устранения.

Практическое следствие

Проектируйте структуры данных и индексацию так, чтобы 32 потока варпа обращались к смежным адресам. Это правило важнее любых микрооптимизаций.

• • •

VIIIМодель roofline: compute-bound или memory-bound?

Цитата из книги · Глава 6

«Модель roofline — полезная визуализация, отображающая два наложенных аппаратурой потолка производительности: одну горизонтальную линию на уровне пиковой скорости операций с плавающей точкой процессора и одну наклонную линию, задаваемую пиковой пропускной способностью памяти. Вместе они образуют огибающую "roofline", которая показывает, ограничено ли данное ядро вычислениями (compute bound) или перемещением данных (memory bound)».

Изложение содержания

Roofline — график, показывающий, что ограничивает производительность ядра: вычисления или память. По горизонтальной оси — арифметическая интенсивность (FLOP на байт трафика памяти), по вертикальной — достигнутая производительность (FLOP/с).

Две линии формируют «крышу»: горизонтальная — пик вычислений, наклонная — пиковая пропускная способность памяти. Точка пересечения — точка перегиба (ridge point).

Важное уточнение: положение точки перегиба зависит от пути исполнения. Для FP32 на CUDA-ядрах на Blackwell пик составляет ~80 TFLOP/с, а пропускная способность HBM3e — ~8 ТБ/с, что даёт точку перегиба около 10 FLOP/байт. Однако для Tensor Core-путей картина принципиально иная: FP16 dense на B200 обеспечивает ~2,25 PFLOPS, что сдвигает точку перегиба на порядок-два вправо — к сотням FLOP/байт. Для FP8 она ещё выше. Это означает, что одно и то же ядро на одном и том же чипе может быть memory-bound на CUDA-ядрах и compute-bound на Tensor Cores — без каких-либо изменений в алгоритме.

Пример из главы: ядро сложения векторов читает два float (8 байт), складывает (1 FLOP), записывает один float (4 байта). Трафик: 12 байт. Арифметическая интенсивность: 1/12 ≈ 0,083 FLOP/байт. Это в 120 раз левее точки перегиба для FP32 — ядро жёстко ограничено памятью. Чтобы сдвинуть его вправо, нужно либо уменьшить байты (перейти на FP16), либо увеличить вычисления на байт.

Схема 18. Roofline

Наклонная — память, горизонтальная — вычисления

arithmetic intensity →
Описание: Две линии формируют «крышу»: наклонная — пиковая пропускная способность памяти, горизонтальная — пик вычислений. Точка перегиба зависит от пути исполнения: ~10 FLOP/байт для FP32 на CUDA-ядрах, на порядок-два больше для Tensor Core (FP16/FP8).

Трансформеры находятся в разных зонах roofline в зависимости от фазы:

  • Prefill (обработка длинного промпта) — вычислительно тяжёлая фаза с высокой арифметической интенсивностью. Работает на Tensor Cores, где точка перегиба сдвинута вправо на порядки, поэтому фаза compute-bound.
  • Decode (генерация токенов по одному) — каждый шаг требует загрузки всех весов для одного токена. Арифметическая интенсивность падает. Memory-bound.
Итоги раздела VIII
Ключевое положение главы

Roofline определяет стратегию: левее точки перегиба — оптимизируйте память, правее — вычисления.

Оговорка о точности

Точка перегиба зависит от пути исполнения. Для FP32 на CUDA-ядрах Blackwell это ~10 FLOP/байт; для Tensor Core-путей (FP16/FP8/FP4) — на порядок-два выше. Одно и то же ядро может быть memory-bound на CUDA-ядрах и compute-bound на Tensor Cores.

Трансформеры

Prefill — compute-bound, decode — memory-bound. Это объясняет, почему для LLM-инференса нужны разные стратегии оптимизации на разных фазах.

• • •

IXПрофилирование и Compute Sanitizer

Цитата из книги · Глава 6

«Поскольку приложения CUDA могут порождать тысячи потоков на одно ядро, традиционная отладка может не поймать тонкие ошибки памяти и состояния гонки. NVIDIA Compute Sanitizer — набор для проверки функциональной корректности, входящий в CUDA Toolkit, — решает эти задачи, инструментируя код во время выполнения, чтобы находить ошибки на ранних этапах разработки».

Изложение содержания

Сначала корректность, потом скорость. Compute Sanitizer проверяет память, гонки, инициализацию и синхронизацию. Nsight Systems показывает временную шкалу, Nsight Compute — счётчики одного ядра.

Nsight Systems даёт картину целиком: где GPU простаивает, где происходят синхронизации, перекрываются ли копирования с вычислениями. Nsight Compute показывает детали конкретного ядра: occupancy, memory throughput, warp stall reasons. Compute Sanitizer содержит четыре инструмента: memcheck (ошибки памяти), racecheck (гонки), initcheck (неинициализированная память), synccheck (синхронизация).

Интеграция Compute Sanitizer в CI с ключом --error-exitcode позволяет ловить регрессии корректности автоматически. Это особенно ценно при работе с кастомными CUDA-ядрами, где традиционные тесты не всегда покрывают все граничные случаи.

Схема 19. Синхронно vs асинхронно

Асинхронный запуск возвращает управление сразу

synchronous / asynchronous
Описание: Синхронный запуск (янтарный) блокирует хост до завершения ядра. Асинхронный (циан) ставит задачу в очередь и возвращает управление немедленно.
ИнструментНазначениеЧто проверяет
Nsight Systems (nsys)Временная шкала CPU и GPUПростои GPU, барьеры, перекрытие копирований с вычислениями, NVTX-диапазоны
Nsight Compute (ncu)Счётчики одного ядраOccupancy, memory throughput, warp stall reasons, bank conflicts, register spills
Compute Sanitizer memcheckОшибки памятиВыход за границы, misaligned access, утечки device-памяти
Compute Sanitizer racecheckГонки в shared memoryWrite-after-write, write-after-read, read-after-write
Compute Sanitizer initcheckНеинициализированная памятьЧтение неинициализированных глобальных данных
Compute Sanitizer synccheckСинхронизацияНекорректное использование барьеров, deadlocks
Итоги раздела IX
Ключевое положение главы

Сначала корректность, потом скорость. Compute Sanitizer проверяет память, гонки, инициализацию и синхронизацию. Nsight Systems показывает временную шкалу, Nsight Compute — счётчики одного ядра.

Практическое следствие

Интегрируйте Compute Sanitizer в CI с ключом --error-exitcode, чтобы автоматически ловить регрессии корректности. Используйте NVTX-маркеры для привязки проблем к конкретным участкам кода.

Дополнение

Порядок работы: сначала Nsight Systems — найти узкое место в общей картине. Затем Nsight Compute — разобраться в деталях конкретного ядра. И только потом — оптимизировать.

• • •

XПроверка прогнозов и актуальные данные

Книга Fregly писалась в 2025 году. Ниже представлено сопоставление положений главы 6 с открытыми данными о современном состоянии архитектуры NVIDIA и практикой профилирования.

Подтверждено Прогноз 1: SM-архитектура с четырьмя планировщиками сохранится

«Четыре независимых планировщика варпов позволяют до четырём варпам выдавать инструкции доступным конвейерам каждый такт».

Подтверждено. Архитектура SM с четырьмя независимыми планировщиками остаётся основой NVIDIA GPU начиная с Volta и до Blackwell. На Blackwell количество планировщиков и логика dual-issue сохранились, но FP32 и INT32 были объединены в единые CUDA-ядра, что меняет стратегию ILP для mixed-потоков.

Подтверждено Прогноз 2: occupancy останется центральной метрикой, но не единственной

«Высокая occupancy — достаточно варпов, чтобы покрыть задержку, — максимизирует пропускную способность и минимизирует простои».

Подтверждено. Occupancy остаётся важной метрикой, но современные профилировщики и практика показывают, что eligible warps per cycle и warp stall reasons дают более полную картину. На Blackwell с его большим регистровым файлом и L2-кэшем 126 МБ многие ядра достигают пиковой производительности при умеренной occupancy.

Подтверждено Прогноз 3: roofline останется ключевой моделью для выбора стратегии оптимизации

«Цель — сдвинуть рабочую точку ядра вправо по roofline, чтобы повысить его арифметическую интенсивность и приблизиться к состоянию compute bound».

Подтверждено. Roofline-анализ встроен в Nsight Compute и активно используется при оптимизации LLM-ядер. Точка перегиба для Blackwell сместилась из-за роста FLOPS и пропускной способности HBM, но принцип остался тем же: сначала определите, что ограничивает ядро — память или вычисления. Важно помнить, что точка перегиба зависит от пути исполнения: FP32 на CUDA-ядрах даёт ~10 FLOP/байт, Tensor Core-пути (FP16/FP8/FP4) — на порядок-два выше.

Дополнение FP32 и INT32 на Blackwell

В главе упоминается dual-issue с одной арифметической и одной памятью-инструкцией, но не уточняется, что на Blackwell FP32 и INT32 объединены в единые CUDA-ядра. Это означает, что mixed INT32/FP32-потоки больше не выигрывают от dual-issue на одном ядре. Вместо этого следует использовать ILP из двух независимых FP32- или INT32-инструкций, чтобы заполнить конвейер.

Дополнение Tensor Memory (TMEM) и точка перегиба Tensor Core-путей

В главе упоминается TMEM как «dedicated ~256 KB per-SM on-chip memory». Это соответствует действительности: на Blackwell TMEM используется инструкциями Tensor Core нового поколения (tcgen05), и это отдельный уровень иерархии памяти, который не адресуется обычными указателями. Для программиста TMEM управляется библиотеками и компилятором, но знание о его существовании помогает понять, почему Tensor Core-ядра могут работать быстрее, чем ожидается по одной лишь пропускной способности HBM. Отдельно стоит учитывать, что точка перегиба roofline для Tensor Core-путей на порядок-два выше, чем для CUDA-ядер: FP16 dense ~2,25 PFLOPS на B200 даёт точку перегиба порядка сотен FLOP/байт, для FP8 — ещё выше. Это делает большинство matmul-ядер compute-bound на Tensor Cores даже при интенсивности, которая для CUDA-ядер была бы memory-bound.

Подтверждено Прогноз 4: инструменты профилирования останутся критически важными

«Используйте NVIDIA Nsight Systems/Compute или PyTorch profiler для измерения этого отношения. Стремитесь улучшить это отношение, поскольку goodput фокусируется на эффективной, полезной загрузке GPU».

Подтверждено. Nsight Systems и Nsight Compute остаются стандартом де-факто для профилирования CUDA-ядер. PyTorch profiler (на базе Kineto) интегрируется с ними, а MLPerf использует их для воспроизводимых измерений. Compute Sanitizer встроен в CI-пайплайны крупных AI-компаний.

• • •

XIКлючевые положения

  1. Кратность 32 — закон. Размер блока должен быть кратен размеру варпа. Некратные размеры создают недозаполненные варпы, которые занимают слоты планировщика без пользы.
  2. 256 — хорошая отправная точка. Восемь блоков по 256 потоков заполняют SM до 2048 резидентных потоков (64 варпа). Это универсальный стартовый размер для большинства ядер.
  3. Коалесцирование — основа пропускной способности. Последовательный доступ 32 потоков к 128 смежным байтам — одна транзакция. Разбросанный доступ — 32 транзакции.
  4. Конфликты банков убивают пропускную способность SMEM. Padding на один элемент — простейший приём их устранения. Для матрицы 32×32, хранящейся как smem[32][33], обращения по столбцам не создают конфликтов.
  5. Асинхронные аллокаторы — для длительных циклов. cudaMallocAsync и cudaFreeAsync избавляют от глобальной синхронизации, которая убивает перекрытие вычислений и копирований.
  6. Unified Memory требует предварительной выборки. cudaMemPrefetchAsync и cudaMemAdvise — обязательны на системах без NVLink-C2C. Даже на Grace Blackwell предпочтительно явное управление размещением.
  7. Roofline определяет стратегию. Левее точки перегиба — оптимизируйте память. Правее — вычисления. Положение точки перегиба зависит от пути исполнения: ~10 FLOP/байт для FP32 на CUDA-ядрах, на порядок-два выше для Tensor Core (FP16/FP8/FP4).
  8. Occupancy ≠ производительность. Достаточно варпов для скрытия задержки — остальное может быть контрпродуктивно. Более информативные метрики — eligible warps per cycle и warp stall reasons.
  9. Сначала корректность, потом скорость. Compute Sanitizer перед Nsight. Интеграция в CI с ключом --error-exitcode ловит регрессии корректности автоматически.
  10. Двойная выдача (dual-issue) на Blackwell изменилась. FP32 и INT32 объединены в единые CUDA-ядра, поэтому mixed-потоки больше не выигрывают от dual-issue на одном ядре. Вместо этого используйте ILP из двух независимых FP32- или INT32-инструкций.
  11. TMEM — новый уровень иерархии. Tensor Memory на 256 КБ на SM используется инструкциями Tensor Core нового поколения. Она не адресуется обычными указателями, но знание о ней помогает понять производительность Tensor Core-ядер.
Итог обзора

Глава 6 книги Fregly — систематический разбор аппаратной основы исполнения GPU-ядер. Ключевая идея: GPU — иерархическая машина, от однотактовых регистров до тысячетактной HBM. Каждое ядро живёт на этой лестнице, и его производительность определяется тем, насколько эффективно оно использует ближние уровни и избегает дальних. Occupancy, коалесцирование, конфликты банков, roofline — это не академические концепции, а инструменты, которые при правильном применении дают ускорения в десятки раз. Дополнение по архитектуре Blackwell (объединение FP32 и INT32, TMEM, NV-HBI) и уточнение о зависимости точки перегиба roofline от пути исполнения уточняют эти принципы, но не отменяют их.

Источники: Chris Fregly, «AI Systems Performance Engineering» (O'Reilly, 2026), Глава 6 — цитаты приводятся в ограниченном объёме в целях обзора и критического анализа. Документация NVIDIA CUDA C++ Programming Guide, CUDA Toolkit, Nsight Systems, Nsight Compute, Compute Sanitizer; спецификации NVIDIA Blackwell B200/GB200; MLPerf Training/Inference v5.0.

FinCodeCow · AI Infrastructure Review

GPU, CUDA and Occupancy: From SIMT to Roofline

A review of Chapter 6 of Chris Fregly's «AI Systems Performance Engineering» (O'Reilly, 2026) — with exposition of the chapter's content, visualizations, forecast verification, and systematization of key points
About the source

This material is a review of Chapter 6 of Chris Fregly, «AI Systems Performance Engineering: Optimizing Model Training and Inference Workloads with GPUs, CUDA, and PyTorch», O'Reilly Media, 2026 (Copyright © 2026 Flux Capacitor, LLC).

Direct quotes from the book are marked with separate blocks labeled «Quote from the book». The rest of the text is a systematized exposition of the chapter's content, a comparison with current data about Blackwell architecture, verification of forecasts against open sources, and additional observations that go beyond the chapter's text. Quotes are provided in limited volume for review and critical analysis purposes.

Chapter 6 of Fregly's book is dedicated to the transition from high-level views of AI systems to the concrete execution model on the GPU. It examines the SIMT model, the thread and memory hierarchy, launch configuration, occupancy, the roofline model, and profiling tools. This chapter is the foundation for all subsequent chapters on CUDA kernel optimization, distributed training, and inference. The key points of the chapter are presented below, along with visualizations, code examples, and clarifications based on current data about Blackwell architecture.

• • •

IInside the Streaming Multiprocessor

Quote from the book · Chapter 6

«Each GPU comprises many SMs, which are roughly analogous to CPU cores but streamlined for parallelism. Each SM can track up to 64 warps (32-thread groups) on Blackwell».

«Each Blackwell GPU includes many SMs—similar to CPU cores but optimized for throughput. On modern GPUs, each SM tracks up to 64 warps (2,048 threads) concurrently. Blackwell GPUs feature 64K 32-bit registers per SM (256 KB total) and a combined 256 KB L1 cache/shared memory per SM. Up to 228 KB (227 KB usable) of that SRAM can be configured as user-managed shared memory per SM».

Exposition of content

The Streaming Multiprocessor (SM) is the main compute unit of the GPU. Unlike a CPU core optimized for one serial task, an SM is built to keep hundreds of threads in flight simultaneously. On Blackwell, each SM holds up to 64 active warps (groups of 32 threads), totalling 2048 resident threads.

The register file is 64K 32-bit registers (256 KB), with at most 255 available per thread. The unified 256 KB SRAM block per SM is shared between L1 cache and shared memory, with up to 228 KB (227 KB usable) allocatable to shared memory. This is a key architectural decision: the developer can dynamically choose what matters more for a specific kernel — cache for global data or software-managed shared memory for thread-block communication.

Quote from the book · Chapter 6

«Within a Blackwell SM, multiple warp schedulers issue instructions to the available pipelines; four independent warp schedulers allow up to four warps to issue instructions to the available pipelines on every cycle. Furthermore, each scheduler supports dual-issue capable of issuing two independent instructions (e.g., one arithmetic and one memory operation) per warp».

Exposition of content

An SM on Blackwell is split into four independent warp schedulers. Each cycle, each scheduler picks one ready warp and can issue two instructions: one arithmetic (INT32, FP32 or Tensor Core) and one memory operation (load or store). The theoretical peak is therefore four warps × two instructions = eight issues per cycle. This is only possible if the dual-issue comes from the same warp — you cannot combine instructions from different warps in one issue.

The Special Function Unit (SFU) handles transcendental operations — sine, cosine, reciprocal square root, etc. The SFU has its own pipeline, separate from INT32/FP32 and from load/store. This keeps slow transcendental math from blocking the main compute and memory pipelines and increases instruction-level parallelism.

An important clarification for the current generation: on Blackwell FP32 and INT32 are merged into unified CUDA cores, so in a given cycle a core executes either an FP32 or an INT32 instruction. Mixed INT32/FP32 streams no longer benefit from dual-issue on a single core — instead, they use warp and scheduler parallelism. This nuance changes ILP optimization strategy: instead of interleaving INT and FP in one thread, it is better to keep two independent FP instructions in flight.

Diagram 1. Warp Schedulers

Four independent schedulers, two pipelines each

4 schedulers / dual issue
Description: Four schedulers pick one ready warp per cycle and can issue up to two instructions (arithmetic + memory) from that warp. Result: up to 8 issues per cycle per SM. On Blackwell, FP32 and INT32 are merged into a unified pipeline, so mixed streams no longer benefit from dual-issue on a single core — ILP from two independent operations of the same type is needed.
Scheduler parameterValue on Blackwell
Number of schedulers4
Warps per cycle (max)4 (one per scheduler)
Arithmetic instructions per cycle4
Memory ops per cycle4
Total issues per cycle (with dual-issue)up to 8
Max registers per thread255
Max threads per block1024
Max warps per SM64
Section I Summary
Key point of the chapter

The SM is a throughput-oriented processor split into four independent schedulers. Each cycle can issue up to two instructions from one warp. Peak — eight issues per cycle at full load.

Limitation noted in the chapter

Registers per thread — up to 255, threads per block — up to 1024, warps per SM — 64. These numbers determine how much work fits on one SM and what occupancy looks like.

Addition on Blackwell architecture

On Blackwell, FP32 and INT32 are merged into unified CUDA cores. Mixed streams should be optimized through warp parallelism rather than interleaving instruction types in one thread.

• • •

IIThreads, Warps, Blocks and Grids

Quote from the book · Chapter 6

«CUDA structures parallel work into a three-level hierarchy—threads, thread blocks (aka cooperative thread arrays [CTAs]), and grids—to balance programmability with massive throughput. At the lowest level, each thread executes your kernel code. You group threads into thread blocks of up to 1,024 threads each on modern GPUs. Thread blocks form a grid when you launch the kernel».

«Thread blocks are subdivided into warps of 32 threads that execute in lockstep under the SIMT model using a warp scheduler».

Exposition of content

CUDA organizes parallel work in three levels: thread → thread block (CTA) → grid. A block contains up to 1024 threads; blocks form a grid launched by a single kernel call. Inside a block, threads share low-latency shared memory and synchronize via the __syncthreads() barrier. Blocks execute independently and in arbitrary order — the GPU scheduler distributes them across SMs.

Each block is divided into warps of 32 threads that execute synchronously in the SIMT model: all threads in a warp follow the same control path. If some threads take the if branch and others the else, the warp executes both branches sequentially, masking inactive lanes. This is called warp divergence. Different warps can take different branches without penalty — divergence only affects threads within one warp.

Quote from the book · Chapter 6

«Traditionally, threads from different thread blocks could not work with one another directly. However, modern GPU architectures and CUDA versions support thread block clusters. Thread block clusters are groups of thread blocks that can communicate with one another across SMs».

Exposition of content

Thread block clusters are a relatively new capability introduced with Hopper and developed further in Blackwell. Within a cluster, threads from different blocks can access each other's shared memory via distributed shared memory (DSMEM). This allows building larger compute structures than a single block and synchronizing them via cluster-level barriers without resorting to global memory.

Practical meaning: for tasks like matrix multiplication with large tiles, a cluster of two or four blocks can be used to effectively increase tile size and raise arithmetic intensity without leaving the boundaries of one SM.

Diagram 2. Threads, Blocks, Grids

Three levels of CUDA parallelism

block (CTA) grid
Description: A grid contains blocks, a block (CTA) contains threads. Blocks are independent, threads inside a block synchronize at barriers.

Diagram 3. Warps

32 threads moving in lockstep

warp = 32 threads, lockstep
Description: A warp is 32 threads that execute in lockstep. All threads in a warp go through the same instruction per cycle (SIMT).

Diagram 4. Warp Divergence

Branching inside a warp serializes execution

divergence / uniformity
Description: If threads of one warp take different if/else branches, the warp executes both sequentially. Different warps diverge with no penalty.

Diagram 5. Distributed Shared Memory

DSMEM: blocks in a cluster see each other's SMEM

DSMEM SMEM block 0 SMEM block 1
Description: Thread block clusters allow accessing neighbor blocks' shared memory through DSMEM and using cluster-level barriers.
Section II Summary
Key point of the chapter

Three levels of hierarchy — thread, block, grid. Inside a block, threads share shared memory and synchronize via __syncthreads(). Blocks are independent. A warp is 32 threads executing the same instructions in sync.

Warp divergence

If threads of one warp take different if/else branches, the warp executes both sequentially, masking inactive lanes. Divergence only affects threads within one warp.

Addition: thread block clusters

Starting with Hopper, blocks can form clusters and access each other's shared memory via DSMEM. This allows increasing the effective tile size without leaving the boundaries of one SM.

• • •

IIILaunch Configuration

Quote from the book · Chapter 6

«When you invoke a kernel from the CPU (host) code, you use the <<< >>> "chevron" syntax to specify how many threads should run—and how they're organized—using two configuration parameters: blocksPerGrid for the number of thread blocks and threadsPerBlock for the number of threads within each block».

«A critical aspect of GPU performance is choosing a thread block size that aligns with the hardware's 32-thread warp size. As such, you typically pick thread block sizes that are exact multiples of 32».

Exposition of content

A CUDA kernel launch looks like kernel<<<blocksPerGrid, threadsPerBlock>>>(...). The choice of these two parameters directly affects occupancy — the fraction of hardware thread slots occupied by active warps.

Block size should be a multiple of 32 (warp size). A block of 256 threads — eight full warps — is a universal starting point. This size offers several advantages: no partial warps, good occupancy (up to eight blocks per SM), and a balance between resources and parallelism.

For blocksPerGrid, the formula (N + threadsPerBlock - 1) / threadsPerBlock is used, which rounds up and ensures all array elements are covered. A bounds check if (idx < N) is mandatory: without it, "extra" threads of the last block will try to access memory beyond the array and trigger cudaErrorIllegalAddress.

Diagram 6. CUDA Programming Flow

Host launches the kernel, GPU executes it

CPU GPU kernel
Description: The host (CPU) launches the kernel asynchronously. The device (GPU) executes it while the host continues its work.

Diagram 7. Thread Hierarchy

Host → launch → GPU grid

CPU host GPU grid
Description: The host initiates the launch, the GPU unfolds the grid of blocks. Each block is assigned to a free SM by the scheduler.

Block size: why 256?

Block size should be a multiple of 32 (warp size). A block of 256 threads — eight full warps — is a universal starting point. Reasons:

  • No partial warps. 256 divides by 32 with no remainder — every warp contains exactly 32 active threads.
  • Good occupancy. At 256 threads per block, up to 8 blocks fit per SM (2048 ÷ 256), fully saturating the resident-thread limit.
  • Resource balance. 256 is small enough not to hit the 1024 threads-per-block limit, and large enough to provide many warps for latency hiding.
__global__ void doubleKernel(float* data, int N) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        data[idx] *= 2.0f;
    }
}

int main() {
    const int N = 1'000'000;
    float *h_data, *d_data;
    cudaMallocHost(&h_data, N * sizeof(float));
    for (int i = 0; i < N; ++i) h_data[i] = 1.0f;
    cudaMalloc(&d_data, N * sizeof(float));
    cudaMemcpy(d_data, h_data, N * sizeof(float),
               cudaMemcpyHostToDevice);
    const int threadsPerBlock = 256;
    const int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
    doubleKernel<<>>(d_data, N);
    cudaDeviceSynchronize();
    cudaFree(d_data);
    cudaFreeHost(h_data);
    return 0;
}
Caution

The bounds check if (idx < N) is critical: without it, "extra" threads of the last block will try to access memory beyond the array and trigger cudaErrorIllegalAddress. CUDA does not throw exceptions inside kernels — the error is set in a global flag and surfaces at the next host-side synchronization call.

For two-dimensional data, a two-dimensional grid of blocks is used. For example, processing a 1024×1024 image with a 16×16 block (256 threads):

__global__ void double2DKernel(float* image, int width, int height) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    if (x < width && y < height) {
        int idx = y * width + x;
        image[idx] *= 2.0f;
    }
}

int main() {
    const int width = 1024;
    const int height = 1024;
    const int N = width * height;
    float *h_image, *d_image;
    cudaMallocHost(&h_image, N * sizeof(float));
    cudaMalloc(&d_image, N * sizeof(float));
    dim3 threadsPerBlock(16, 16);
    dim3 blocksPerGrid((width + 15) / 16, (height + 15) / 16);
    double2DKernel<<>>(d_image, width, height);
    cudaDeviceSynchronize();
    cudaFree(d_image);
    cudaFreeHost(h_image);
    return 0;
}
Section III Summary
Key point of the chapter

Block size must be a multiple of 32. 256 threads — an eight-warp block — is a universal starting point. The formula (N + threadsPerBlock - 1) / threadsPerBlock rounds up and covers all elements.

Limitation

A bounds check if (idx < N) is mandatory. Without it, extra threads of the last block will trigger a memory access error.

Addition: 2D and 3D kernels

For images and volumetric data, 2D and 3D grids are used via dim3. This naturally maps multidimensional data onto GPU threads.

• • •

IVMemory Hierarchy: From Registers to HBM

Quote from the book · Chapter 6

«The GPU provides a multilevel memory hierarchy and helps balance capacity and speed. The hierarchy includes registers, shared memory, caches, global memory, and a specialized TMEM on Blackwell GPUs and beyond. TMEM is a dedicated ~256 KB per-SM on-chip memory used by Blackwell's 5th-generation Tensor Core instructions».

Exposition of content

GPU memory levels differ in capacity, latency and bandwidth by orders of magnitude. Understanding this ladder is the key to writing fast kernels.

At the top are registers: single-cycle, but extremely limited (up to 255 per thread). Slightly below — shared memory and L1 cache: 20–30 cycles, up to 228 KB per SM. Further down — L2 cache: 126 MB, shared across the entire GPU, ~200 cycles. At the bottom — global HBM3e memory: hundreds and thousands of cycles, ~8 TB/s. The further from the compute core, the greater the capacity and the higher the latency.

Separately, Tensor Memory (TMEM) should be noted — a dedicated 256 KB per-SM memory used by next-generation Tensor Core instructions. TMEM is not addressed by ordinary pointers; exchange goes through tcgen05 instructions. This is another level of the hierarchy that appeared with Tensor Cores and Transformer Engine.

Diagram 8. Memory Hierarchy

A pyramid: from registers to HBM

regs → L1/SMEM → L2 → HBM → CPU
Description: The closer the level — the smaller the capacity and latency, but the higher the bandwidth per SM. Distant levels are shared across the entire GPU.

Diagram 9. SMEM and TMEM Around Tensor Cores

Shared memory and tensor memory on one SM

SMEM TMEM Tensor Cores C = A × B
Description: Tensor Cores take operands from SMEM and TMEM, write the accumulator to TMEM. TMEM is not addressed by ordinary pointers — exchange goes through tcgen05 instructions.

Diagram 10. Block Shared Memory

A software-managed cache on the SM

__shared__
Description: Shared memory is a low-latency (20–30 cycles) software-managed buffer inside a block. It lives only while the block is active.

Diagram 11. Local Memory and Spill

When registers run out — data goes to DRAM

DRAM / local spill
Description: Register spill — eviction of "extra" registers into local memory (a region in DRAM). Access costs hundreds of cycles and kills performance.

Diagram 12. HBM and the GPU

Global memory — the main data channel

GPU HBM / global memory
Description: HBM3e delivers up to ~8 TB/s, but latency of hundreds of cycles makes it the slowest link in the chain.
LevelScopeCapacity (Blackwell)LatencyBandwidth
RegistersPer thread, on SM64K × 32-bit (max 255 per thread)1 cycleTens of TB/s per SM
Shared memory / L1Per SM228 KB (227 usable)20–30 cyclesTB/s per SM
TMEMPer SM256 KB~10 cyclesTB/s to Tensor Cores
Constant cachePer SM~8 KB (for 64 KB __constant__)~1 cycle (broadcast)Register-like
L2GPU-wide126 MB~200 cyclesTens of TB/s
Global (HBM3e)Device-wide180 GB (B200) / ~288 GB (B300)Hundreds–thousands of cycles~8 TB/s
Section IV Summary
Key point of the chapter

The GPU memory hierarchy is registers → shared/L1 → L2 → HBM. For maximum performance, data should stay as high in this hierarchy as possible.

Practical consequence

Register spill is one of the main causes of performance degradation. Overflowing the register file forces data into local memory, equivalent to a DRAM access. Spills must be profiled and eliminated first.

• • •

VUnified Memory

Quote from the book · Chapter 6

«Unified Memory (also known as CUDA Managed Memory) gives you a single, coherent address space that spans both CPU and GPU, so you no longer have to juggle separate host and device buffers or issue explicit cudaMemcpy calls».

«While accessing Unified Memory is super developer-friendly, it can cause unwanted on-demand page migrations between the CPU and the GPU. This will introduce hidden latency and execution stalls».

Exposition of content

CUDA Managed Memory (Unified Memory) creates a single address space for CPU and GPU. You allocate via cudaMallocManaged() — and both sides see one pointer. Pages migrate on demand: if the GPU accesses data sitting in CPU RAM, a page fault occurs and data is transferred over NVLink-C2C (on Grace Blackwell) or PCIe.

The convenience is deceptive. Automatic on-demand migrations can introduce hidden latency: if a GPU thread accesses a page physically residing in CPU memory, the kernel stalls and waits for the transfer. On PCIe systems this can be orders of magnitude slower than accessing HBM. On Grace Blackwell with NVLink-C2C, transfers are faster, but still not free.

The solution is explicit placement management: cudaMemPrefetchAsync for proactive page migration to the GPU and cudaMemAdvise for driver hints about where data will be used. This turns migrations from synchronous page faults into asynchronous operations that can be overlapped with computation.

Diagram 13. Unified Memory

Pages migrate between CPU and GPU on demand

CPU GPU
Description: One pointer, one allocation. Page faults migrate data between RAM and HBM over NVLink-C2C or PCIe.

Diagram 14. Prefetch

cudaMemPrefetchAsync — proactive migration

NVLink-C2C cudaMemPrefetchAsync()
Description: Prefetch pages to the GPU ahead of time — before the kernel launches. Eliminates synchronous page-fault migrations.

Diagram 15. Driver Hints

cudaMemAdvise: preferred location, ReadMostly

preferred location / ReadMostly
Description: cudaMemAdvise tells the driver where to keep pages and how they will be used. Reduces the number of migrations.
cudaMemPrefetchAsync(ptr, size, gpuId, stream);
cudaMemAdvise(ptr, size, cudaMemAdviseSetPreferredLocation, gpuId);
cudaMemAdvise(ptr, size, cudaMemAdviseSetReadMostly, gpuId);
cudaStreamAttachMemAsync(stream, ptr, 0, cudaMemAttachSingle);
Section V Summary
Key point of the chapter

Unified Memory is convenient but requires explicit placement management. Automatic page-fault migrations can introduce hidden latency.

Practical consequence

Use cudaMemPrefetchAsync for proactive migration and cudaMemAdvise for driver hints. This turns synchronous migrations into asynchronous ones that can be overlapped with computation.

• • •

VIOccupancy and Latency Hiding

Quote from the book · Chapter 6

«GPUs sustain performance by running many warps concurrently so that when one warp stalls waiting for data, another warp can run. This ability to rapidly switch between warps allows a GPU to hide memory latency. As we described earlier, the fraction of an SM's capacity actually occupied by active warps is called occupancy».

«If occupancy is low (just a few active warps), an SM may sit idle while one warp is waiting on memory. This leads to poor SM utilization».

Exposition of content

Occupancy is the ratio of active warps per SM to the theoretical maximum (64 warps, 2048 threads on Blackwell). High occupancy lets the scheduler instantly switch to a ready warp when the current one stalls on memory. This is latency hiding.

But occupancy is not a goal in itself. If the kernel is memory-bound, pushing occupancy to 100% won't help — the bottleneck is the HBM bus, not the number of warps. More informative metrics are eligible warps per cycle and warp stall reasons: how many warps are actually ready to execute and what the rest are stalling on.

The chapter's example shows the difference between sequential and parallel vector addition. The sequential version uses one thread and loads the GPU at 1.5%. The parallel version distributes the work and loads the GPU at 95%. This illustrates that the GPU is a throughput processor and requires massive parallelism to be loaded.

Diagram 16. Timeline: Sequential vs Parallel

The parallel version does the same work in less time

sequential / parallel
Description: The sequential version (amber) does all the work on a single thread. The parallel version (cyan) spreads the load and finishes earlier.

Diagram 17. Vectorized Add

A + B = C — one thread per element

A + B = C (parallel)
Description: Each thread handles one element — vectorization replaces the loop with a parallel launch.
MetricaddSequentialaddParallel
Kernel time (ms)48.212.17
GPU utilization1.5%95%
Occupancy1.3%38.7%
Warp efficiency3.1%100%

To control occupancy at the compiler level, __launch_bounds__ annotations are used. They tell the compiler the maximum number of threads per block the kernel will be launched with and the minimum number of blocks that should fit per SM. The compiler limits registers per thread so these conditions are met. The runtime version is the CUDA Occupancy API:

__global__ __launch_bounds__(256, 16)
void myKernel(...) { /* ... */ }

int minGrid = 0, bestBlock = 0;
cudaOccupancyMaxPotentialBlockSize(&minGrid, &bestBlock,
                                   myKernel,
                                   0,
                                   0);
Section VI Summary
Key point of the chapter

Occupancy is a means to hide latency, not a goal. High occupancy helps only if the bottleneck is latency, not memory bandwidth.

Metrics more important than occupancy

Eligible warps per cycle and warp stall reasons show how many warps are actually ready to execute and what the rest are stalling on. These should guide tuning.

Practical consequence

Use __launch_bounds__ and the CUDA Occupancy API to control occupancy. Remember that increasing occupancy at the cost of registers can lead to spills and performance degradation.

• • •

VIICoalescing and Bank Conflicts

Quote from the book · Chapter 6

«Global memory accesses are fastest when threads in a warp access contiguous memory addresses that the hardware can combine into fewer, larger transactions. If threads access scattered or misaligned addresses, the device cannot coalesce requests into the minimal number of cache line transactions, which on modern GPUs are 128-byte lines composed of four 32-byte sectors».

Exposition of content

When 32 threads of a warp read global memory simultaneously, the GPU tries to coalesce those accesses into as few 128-byte transactions as possible. If threads access consecutive 32-bit elements, one transaction serves the whole warp. If threads are scattered across memory, each element requires its own transaction.

A similar problem arises in shared memory, where it is called bank conflicts. Shared memory consists of 32 banks of 4 bytes each. If several threads of a warp access the same bank, the accesses serialize. A classic case is column access to a row-major matrix. The solution is to add padding: one extra element per row. For a 32×32 matrix stored as smem[32][33], column accesses cause no conflicts.

int idx = blockIdx.x * blockDim.x + threadIdx.x;
float val = data[idx];

float val = data[threadIdx.x * 1024];

__shared__ float smem[32][33];
float val = smem[threadIdx.x][threadIdx.y];
Practice

When accessing shared memory by columns of a row-major matrix, add padding — one extra element per row (int stride = width + 1). This shifts the banks and eliminates conflicts. For a 32×32 matrix stored as smem[32][33], column accesses cause no conflicts.

Section VII Summary
Key point of the chapter

Coalescing is the basis of global memory bandwidth. Sequential access of 32 threads to 128 adjacent bytes — one transaction. Scattered access — 32 transactions.

Bank conflicts

Bank conflicts kill shared memory bandwidth. One element of padding is the simplest fix.

Practical consequence

Design data structures and indexing so that 32 threads of a warp access adjacent addresses. This rule is more important than any micro-optimization.

• • •

VIIIRoofline Model: Compute-Bound or Memory-Bound?

Quote from the book · Chapter 6

«A roofline model is a useful visualization that charts two hardware-imposed performance ceilings: one horizontal line at the processor's peak floating-point rate and one diagonal line set by the peak memory bandwidth. Together, these form a "roofline" envelope that reveals whether a given kernel is limited by computation (compute bound) or data movement (memory bound)».

Exposition of content

Roofline is a plot showing what limits kernel performance: compute or memory. The horizontal axis is arithmetic intensity (FLOP per byte of memory traffic), the vertical axis is achieved performance (FLOP/s).

Two lines form the "roof": the horizontal one is peak compute, the sloped one is peak memory bandwidth. Their intersection is the ridge point.

An important clarification: the position of the ridge point depends on the execution path. For FP32 on CUDA cores on Blackwell, peak is ~80 TFLOP/s, and HBM3e bandwidth is ~8 TB/s, giving a ridge point around 10 FLOP/byte. However, for Tensor Core paths the picture is fundamentally different: FP16 dense on B200 delivers ~2.25 PFLOPS, shifting the ridge point one to two orders of magnitude to the right — to hundreds of FLOP/byte. For FP8 it is even higher. This means the same kernel on the same chip can be memory-bound on CUDA cores and compute-bound on Tensor Cores — without any algorithmic change.

The chapter's example: a vector addition kernel reads two floats (8 bytes), adds them (1 FLOP), writes one float (4 bytes). Traffic: 12 bytes. Arithmetic intensity: 1/12 ≈ 0.083 FLOP/byte. This is 120× to the left of the FP32 ridge point — the kernel is strictly memory-bound. To move it to the right, one must either reduce bytes (e.g., switch to FP16) or increase compute per byte.

Diagram 18. Roofline

The slope is memory, the ceiling is compute

arithmetic intensity →
Description: Two lines form the "roof": the slope is peak memory bandwidth, the ceiling is peak compute. The ridge point depends on the execution path: ~10 FLOP/byte for FP32 on CUDA cores, one to two orders of magnitude higher for Tensor Core (FP16/FP8).

Transformers live in different roofline zones depending on the phase:

  • Prefill (long prompt processing) — a compute-heavy phase with high arithmetic intensity. Runs on Tensor Cores, where the ridge point is shifted orders of magnitude to the right, so the phase is compute-bound.
  • Decode (token-by-token generation) — each step requires loading all weights for a single token. Arithmetic intensity drops. Memory-bound.
Section VIII Summary
Key point of the chapter

Roofline determines strategy: left of the ridge point — optimize memory, right — optimize compute.

Clarification on precision

The ridge point depends on the execution path. For FP32 on Blackwell CUDA cores it is ~10 FLOP/byte; for Tensor Core paths (FP16/FP8/FP4) it is one to two orders of magnitude higher. The same kernel can be memory-bound on CUDA cores and compute-bound on Tensor Cores.

Transformers

Prefill is compute-bound, decode is memory-bound. This explains why LLM inference requires different optimization strategies at different phases.

• • •

IXProfiling and Compute Sanitizer

Quote from the book · Chapter 6

«Since CUDA applications can spawn thousands of threads per kernel, traditional debugging may fail to catch subtle memory bugs and race conditions. NVIDIA Compute Sanitizer, a functional-correctness suite included with the CUDA Toolkit, addresses these challenges by instrumenting code at runtime to find errors early in development».

Exposition of content

Correctness first, then speed. Compute Sanitizer checks memory, races, initialization, and synchronization. Nsight Systems shows the timeline, Nsight Compute shows single-kernel counters.

Nsight Systems gives the big picture: where the GPU is idle, where synchronization happens, whether copies overlap with compute. Nsight Compute shows details of a specific kernel: occupancy, memory throughput, warp stall reasons. Compute Sanitizer contains four tools: memcheck (memory errors), racecheck (races), initcheck (uninitialized memory), synccheck (synchronization).

Integrating Compute Sanitizer into CI with the --error-exitcode flag allows catching correctness regressions automatically. This is especially valuable when working with custom CUDA kernels, where traditional tests do not always cover all edge cases.

Diagram 19. Synchronous vs Asynchronous

An async launch returns control immediately

synchronous / asynchronous
Description: A synchronous launch (amber) blocks the host until the kernel completes. An asynchronous launch (cyan) queues the task and returns immediately.
ToolPurposeWhat it checks
Nsight Systems (nsys)CPU/GPU timelineGPU idle time, barriers, overlap of copies and compute, NVTX ranges
Nsight Compute (ncu)Single-kernel countersOccupancy, memory throughput, warp stall reasons, bank conflicts, register spills
Compute Sanitizer memcheckMemory errorsOut-of-bounds, misaligned access, device memory leaks
Compute Sanitizer racecheckShared memory racesWrite-after-write, write-after-read, read-after-write
Compute Sanitizer initcheckUninitialized memoryReads of uninitialized global data
Compute Sanitizer synccheckSynchronizationIncorrect barrier use, deadlocks
Section IX Summary
Key point of the chapter

Correctness first, then speed. Compute Sanitizer checks memory, races, initialization, and synchronization. Nsight Systems shows the timeline, Nsight Compute shows single-kernel counters.

Practical consequence

Integrate Compute Sanitizer into CI with the --error-exitcode flag to catch correctness regressions automatically. Use NVTX markers to tie problems to specific code regions.

Addition

Workflow order: first Nsight Systems — find the bottleneck in the overall picture. Then Nsight Compute — understand the details of a specific kernel. Only after that — optimize.

• • •

XForecast Verification and Current Data

Fregly's book was written in 2025. Below is a comparison of Chapter 6's provisions with open data about the current state of NVIDIA architecture and profiling practice.

Confirmed Forecast 1: SM architecture with four schedulers will persist

«Four independent warp schedulers allow up to four warps to issue instructions to the available pipelines on every cycle».

Confirmed. The SM architecture with four independent warp schedulers has remained the foundation of NVIDIA GPUs from Volta through Blackwell. On Blackwell the number of schedulers and dual-issue logic are preserved, but FP32 and INT32 were merged into unified CUDA cores, which changes ILP strategy for mixed streams.

Confirmed Forecast 2: occupancy will remain central but not the only metric

«High occupancy—enough warps to cover latency—maximizes throughput and minimizes idle stalls».

Confirmed. Occupancy remains an important metric, but modern profilers and practice show that eligible warps per cycle and warp stall reasons give a more complete picture. On Blackwell with its large register file and 126 MB L2 cache, many kernels achieve peak performance at moderate occupancy.

Confirmed Forecast 3: roofline will remain the key model for choosing optimization strategy

«The goal is to push the kernel's operational point to the right on the roofline to increase its arithmetic intensity and move closer to becoming compute bound».

Confirmed. Roofline analysis is built into Nsight Compute and is actively used in LLM kernel optimization. For Blackwell the ridge point has shifted due to increased FLOPS and HBM bandwidth, but the principle remains the same: first determine what limits the kernel — memory or compute. It is important to remember that the ridge point depends on the execution path: FP32 on CUDA cores gives ~10 FLOP/byte, Tensor Core paths (FP16/FP8/FP4) give one to two orders of magnitude higher.

Addition FP32 and INT32 on Blackwell

The chapter mentions dual-issue with one arithmetic and one memory instruction, but does not specify that on Blackwell FP32 and INT32 are merged into unified CUDA cores. This means mixed INT32/FP32 streams no longer benefit from dual-issue on a single core. Instead, ILP from two independent FP32 or INT32 instructions should be used to fill the pipeline.

Addition Tensor Memory (TMEM) and the Tensor Core ridge point

The chapter mentions TMEM as «dedicated ~256 KB per-SM on-chip memory». This is correct: on Blackwell TMEM is used by next-generation Tensor Core instructions (tcgen05), and it is a separate level of the memory hierarchy not addressed by ordinary pointers. For the programmer TMEM is managed by libraries and the compiler, but knowing about its existence helps understand why Tensor Core kernels can run faster than expected from HBM bandwidth alone. Separately, it should be noted that the roofline ridge point for Tensor Core paths is one to two orders of magnitude higher than for CUDA cores: FP16 dense ~2.25 PFLOPS on B200 gives a ridge point of hundreds of FLOP/byte, for FP8 even higher. This makes most matmul kernels compute-bound on Tensor Cores even at intensities that would be memory-bound for CUDA cores.

Confirmed Forecast 4: profiling tools will remain critically important

«Use NVIDIA Nsight Systems/Compute or PyTorch profiler to measure this ratio. Strive to improve this ratio, as goodput focuses on effective, useful GPU utilization».

Confirmed. Nsight Systems and Nsight Compute remain the de facto standard for CUDA kernel profiling. PyTorch profiler (based on Kineto) integrates with them, and MLPerf uses them for reproducible measurements. Compute Sanitizer is built into CI pipelines at large AI companies.

• • •

XIKey Points

  1. Multiples of 32 are law. Block size must be a multiple of warp size. Non-multiples create partial warps that occupy scheduler slots without benefit.
  2. 256 is a good starting point. Eight blocks of 256 threads fill an SM to 2048 resident threads (64 warps). This is the universal starting size for most kernels.
  3. Coalescing is the basis of throughput. Sequential access of 32 threads to 128 adjacent bytes — one transaction. Scattered access — 32 transactions.
  4. Bank conflicts kill SMEM bandwidth. One element of padding is the simplest fix. For a 32×32 matrix stored as smem[32][33], column accesses cause no conflicts.
  5. Async allocators — for long loops. cudaMallocAsync and cudaFreeAsync eliminate global synchronization that kills overlap of compute and copies.
  6. Unified Memory requires prefetching. cudaMemPrefetchAsync and cudaMemAdvise are mandatory on systems without NVLink-C2C. Even on Grace Blackwell explicit placement management is preferable.
  7. Roofline determines strategy. Left of the ridge point — optimize memory. Right — compute. The ridge point depends on the execution path: ~10 FLOP/byte for FP32 on CUDA cores, one to two orders of magnitude higher for Tensor Core (FP16/FP8/FP4).
  8. Occupancy ≠ performance. Enough warps to hide latency is sufficient — the rest may be counterproductive. More informative metrics are eligible warps per cycle and warp stall reasons.
  9. Correctness first, then speed. Compute Sanitizer before Nsight. CI integration with --error-exitcode catches correctness regressions automatically.
  10. Dual-issue on Blackwell has changed. FP32 and INT32 are merged into unified CUDA cores, so mixed streams no longer benefit from dual-issue on a single core. Instead, use ILP from two independent FP32 or INT32 instructions.
  11. TMEM is a new level of the hierarchy. Tensor Memory at 256 KB per SM is used by next-generation Tensor Core instructions. It is not addressed by ordinary pointers, but knowing about it helps understand Tensor Core kernel performance.
Review Summary

Chapter 6 of Fregly's book is a systematic breakdown of the hardware basis of GPU kernel execution. The key idea: the GPU is a hierarchical machine, from single-cycle registers to thousand-cycle HBM. Every kernel lives on this ladder, and its performance is determined by how efficiently it uses the near levels and avoids the far ones. Occupancy, coalescing, bank conflicts, roofline — these are not academic concepts but tools that, when applied correctly, yield speedups of tens of times. The addition on Blackwell architecture (FP32/INT32 merge, TMEM, NV-HBI) and the clarification on the execution-path dependence of the roofline ridge point refine these principles but do not overturn them.

Sources: Chris Fregly, «AI Systems Performance Engineering» (O'Reilly, 2026), Chapter 6 — quotes provided in limited volume for review and critical analysis purposes. NVIDIA CUDA C++ Programming Guide, CUDA Toolkit, Nsight Systems, Nsight Compute, Compute Sanitizer documentation; NVIDIA Blackwell B200/GB200 specifications; MLPerf Training/Inference v5.0.