FinCodeCow · Аналитика AI-инфраструктуры

Оптимизация шаблонов доступа к памяти GPU: от объединения загрузок до асинхронных конвейеров TMA

Обзор седьмой главы книги 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).

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

Современные графические процессоры NVIDIA — от Hopper до Blackwell — способны выполнять десятки терафлопс в формате FP32 и ещё больше с использованием Tensor Core. Но между теоретическим пиком и реальной производительностью стоит один барьер: подсистема памяти. Данные должны поступать в вычислительные блоки достаточно быстро, чтобы процессор не простаивал. Именно здесь большинство ядер теряют производительность — не из-за нехватки арифметики, а из-за неэффективного обращения к памяти.

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

«По мере роста и усложнения AI-моделей память GPU часто становится узким местом, стоящим между теоретической вычислительной способностью и реальной производительностью».

«На современном GPU, таком как Blackwell, пропускная способность HBM3e достигает 8 ТБ/с на устройство. В конфигурации Grace Blackwell GB200 и GB300 эта цифра удваивается до 16 ТБ/с на оба GPU. Необъединённые обращения к памяти оставят большую часть этой пропускной способности неиспользованной из-за избыточных транзакций и простоев».

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

В главе подчёркивается, что оптимизация памяти — это не одна техника, а последовательность шагов, каждый из которых снимает очередной барьер. Сначала объединение загрузок (coalescing) решает проблему фрагментированных транзакций. Затем векторизация сокращает число инструкций. Тайлинг устраняет повторные загрузки. Устранение конфликтов банков убирает сериализацию разделяемой памяти. Warp shuffle заменяет обмен через память на обмен через регистры. Кэш только для чтения снижает нагрузку на DRAM. И, наконец, TMA с асинхронными конвейерами скрывает задержку HBM за вычислениями.

Каждый шаг подтверждается конкретными метриками Nsight Compute: Global Memory Load Efficiency, Average Sectors per Request, DRAM throughput, SM Active %, bank conflicts, warp stall reasons. Глава последовательно показывает, как эти метрики меняются при переходе от наивного кода к оптимизированному.

Ключевая идея главы: производительность GPU-ядра определяется не количеством арифметических инструкций, а тем, насколько хорошо оно использует иерархию памяти. Ядро, которое при каждом умножении обращается к HBM, будет тратить 800 циклов на ожидание. Ядро, которое загружает тайл один раз, переиспользует его через shared memory, обменивается значениями через регистры и асинхронно предзагружает следующий тайл через TMA, превращает эти 800 циклов ожидания в 800 циклов полезной работы.

• • •

IПамять как узкое место

GPU располагает многоуровневой иерархией памяти, и цена доступа к каждому уровню различается на порядки. Регистры — самый быстрый уровень: задержка составляет один цикл, но их объём ограничен (до 65 536 32-битовых регистров на SM у Blackwell). Разделяемая память (shared memory) обеспечивает задержку около 30 циклов и объём до 228 КБ на SM. Кэш L2, общий для всех SM, добавляет примерно 200 циклов задержки. И, наконец, глобальная память — HBM3e на Blackwell — обеспечивает задержку около 800 циклов, но при этом обладает наибольшим объёмом: до 192 ГБ на устройство и пропускной способностью до 8 ТБ/с (16 ТБ/с в двухчиповых конфигурациях Grace Blackwell).

Ключевое наблюдение: задержка доступа к HBM примерно в 800 раз больше, чем к регистру. Если каждый warp будет ждать 800 циклов на каждую загрузку, производительность упадёт кратно. Поэтому вся оптимизация сводится к одному принципу: меньше обращаться к HBM, больше — к ближним уровням иерархии, и не давать GPU простаивать в ожидании данных.

Каждый приём, рассмотренный ниже, иллюстрируется кодом на CUDA C++ и PyTorch, а также метриками профилировщика Nsight Compute, чтобы было видно не только «что изменить», но и «почему это помогает».

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

Память GPU — основное узкое место современных AI-нагрузок. Задержка HBM (~800 циклов) на порядки превышает задержку регистров (1 цикл).

Приводимые в главе цифры

HBM3e на Blackwell: до 8 ТБ/с на GPU, 192 (180 полезных) ГБ на B200, 16 ТБ/с в двухчиповых конфигурациях. Пропускная способность памяти растёт медленнее, чем вычислительная способность.

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

Задача оптимизации — сместить баланс от HBM к регистрам, shared memory и L2. Каждый приём из главы — шаг в этом направлении.

• • •

IIОбъединение загрузок: почему 32 потока должны идти рядом

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

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

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

Если же потоки разбегаются по разрозненным адресам — шаговая индексация, нерегулярный доступ, невыровненный старт, — аппаратура не может объединить запросы. Вместо одной широкой транзакции warp выдаёт множество мелких, каждая из которых тянет из HBM 32-байтовый сектор, но полезные байты в нём — лишь малая часть. В худшем случае число секторов на запрос может приближаться к 32.

Схема 1. Объединённый и необъединённый доступ

32 lane-запроса сливаются в одну 128-байтовую транзакцию — или взрываются во множество

Coalesced Uncoalesced T0 T1 T2 T3 T4 T5 T6 T7 1 × 128-byte transaction T0 T1 T2 T3 T4 T5 T6 T7 8 separate transactions
Описание: Слева — 32 lane-запроса сливаются в одну 128-байтовую транзакцию (4 сектора по 32 байта). Справа — memory controller выдаёт отдельную транзакцию на каждый lane: DRAM throughput падает до ~25%, ядро работает примерно в 3,7 раза медленнее.

В Nsight Compute раздел Memory Workload Analysis покажет пониженную эффективность загрузки глобальной памяти (Global Memory Load Efficiency), повышенное число чтений секторов DRAM и среднее количество секторов на запрос выше 4,0. Процент пропускной способности DRAM останется значительно ниже пикового, подтверждая, что warp тратит циклы в ожидании памяти вместо нагрузки на ALU.

Схема 2. Три паттерна доступа: coalesced, strided, random

Шаг индексации напрямую определяет число транзакций на warp

Coalesced Strided Random T0 T1 T2 T3 T4 T5 T6 T7 1 × 128 B transaction T0 T1 T2 T3 T4 T5 T6 T7 2+ transactions (50% waste) T0 T1 T2 T3 T4 T5 T6 T7 up to 8 transactions
Описание: Coalesced — потоки идут по адресам 0, 1, 2, …, 7 → одна транзакция на 128 байт. Strided — шаг 2, используется половина каждого сегмента → 2 и более транзакций. Random — потоки бьют в произвольные адреса, DRAM throughput падает в разы.

Чтобы выйти из режима ограниченности памятью, можно реорганизовать данные так, чтобы каждый из 32 потоков warp'а загружал непрерывные элементы. Либо индексируйте массив как input[idx], где idx = blockIdx.x * blockDim.x + threadIdx.x, либо перейдите к компоновке «структура массивов» (SoA) вместо «массива структур» (AoS), чтобы поток i всегда обращался к элементу i.

Схема 3. AoS против SoA: как компоновка данных убивает coalescing

Массив структур даёт stride 3, структура массивов — stride 1

AoS (struct x,y,z) SoA (x[], y[], z[]) x y z x y z x y z x y z stride 3 → uncoalesced x x x x x x x x y y y y y y z z z z z z stride 1 → fully coalesced
Описание: AoS перемежает поля x y z | x y z | … — чтение только x даёт stride 3 и scatter-обращения. SoA раскладывает поля в отдельные массивы — доступ становится непрерывным, coalescing срабатывает идеально.

Пример «до» (C++): необъединённый доступ с шагом

#include <cuda_runtime.h>
#include <iostream>

__global__ void uncoalescedCopy(const float* __restrict__ in,
                                float* __restrict__ out,
                                int N, int stride) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        out[idx] = in[idx * stride];
    }
}

Пример «до» (PyTorch)

import torch

def uncoalesced_copy(input_tensor, stride):
    flat = input_tensor.contiguous().view(-1)
    idx = torch.arange(0, flat.numel(), stride,
                       device=flat.device, dtype=torch.long)
    return torch.index_select(flat, 0, idx)

В необъединённой версии запрос памяти каждого warp'а разбивается в среднем на 8 отдельных 32-байтовых секторов. Шаблон доступа охватывает две кэш-линии на warp, раздувая количество транзакций и истощая конвейер памяти. Nsight Compute сообщает лишь около 25% от устойчивого пикового значения пропускной способности DRAM.

Пример «после» (C++): объединённый доступ

__global__ void coalescedCopy(const float* __restrict__ in,
                              float* __restrict__ out,
                              int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        out[idx] = in[idx];
    }
}

Объединённая версия на PyTorch — это просто out = inp.clone(), поскольку clone() на непрерывном тензоре использует векторизованное копирование памяти под капотом. Можно также применить torch.compile с mode="max-autotune", чтобы TorchInductor выбрал объединённые и векторизованные расписания.

МетрикаДо (необъединённый)После (объединённый)
Пропускная способность DRAM (% от пика)25%90% (в 3,6 раза)
Эффективность загрузки глобальной памяти23%99%
Среднее число секторов на запрос8,04,0 (оптимально)
Активность SM %62%99%
Время выполнения ядра4,8 мс1,3 мс (в 3,7 раза)
Итоги раздела II
Ключевое положение главы

Объединение загрузок — базовый уровень оптимизации памяти. Аппаратура GPU способна объединить 32 запроса warp'а в одну 128-байтовую транзакцию, но только если потоки обращаются к непрерывным адресам.

Приводимые в главе цифры

Необъединённый доступ: 25% DRAM throughput, 8 секторов на запрос. Объединённый: 90% DRAM throughput, 4 сектора на запрос. Ускорение ядра — 3,7×.

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

Проверяйте Global Memory Load Efficiency в Nsight Compute. Если она ниже 90% — reorganize данные (SoA вместо AoS, непрерывная индексация).

• • •

IIIВекторизация: меньше инструкций — больше данных

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

«Хотя объединение памяти — это аппаратная оптимизация времени выполнения на GPU NVIDIA, векторизованный доступ к памяти — это стратегия времени компиляции, в которой каждая инструкция загрузки или сохранения явно извлекает несколько смежных элементов (например, float4 или 16 байт) на поток. Это сокращает количество инструкций и устраняет накладные расходы на сшивание».

Объединение — это аппаратная оптимизация времени выполнения: GPU сам «склеивает» 32 запроса по 4 байта в широкую транзакцию. Но под капотом остаются 32 отдельные инструкции загрузки, по одной на поток. Векторизация устраняет этот избыток: каждый поток читает не один float, а сразу четыре — типом float4 (16 байт).

Когда все 32 потока warp'а выдают загрузку float4, они вместе извлекают 32 × 16 = 512 байт непрерывных данных, которые контроллер памяти разбивает ровно на четыре 128-байтовые транзакции. Каждая транзакция состоит из четырёх 32-байтовых секторов, каждый полностью используемый — идеальные 4,0 сектора на транзакцию. По сравнению со скалярным случаем число инструкций загрузки на поток падает в 4 раза.

Для использования векторизованных загрузок указатели данных должны быть выровнены по ширине вектора. Функции выделения памяти CUDA, такие как cudaMalloc, возвращают указатели, выровненные как минимум по 256 байт. Однако добавление смещения элемента может нарушить выравнивание:

auto ptr4 = reinterpret_cast<const float4*>(ptr);

Пример «после» (C++): векторизованное копирование с float4

__global__ void copyVector16B(const float4* __restrict__ in,
                              float4* __restrict__ out,
                              int N4) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N4) {
        out[idx] = in[idx];
    }
}

32-байтовые векторные загрузки на Blackwell (CUDA 13)

На архитектуре Blackwell доступен ещё более широкий вектор — 32 байта на поток (8 float). CUDA не предоставляет встроенного типа float8, поэтому нужно определить собственную структуру с 32-байтовым выравниванием:

struct alignas(32) float8 { float v[8]; };

__global__ void copyVector32B(const float8* __restrict__ in,
                              float8* __restrict__ out, int N8) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N8) {
        out[idx] = in[idx];
    }
}

При 32-байтовых поточных загрузках warp перемещает 1024 байта (32 потока × 32 байта), что отображается на 8 × 128-байтовых линий. На Hopper та же операция потребует двух 16-байтовых загрузок на поток, поскольку Hopper не поддерживает 32-байтовые векторные инструкции.

МетрикаДо (скалярный)После (векторизованный)
Эффективность загрузки глобальной памяти28%97%
Среднее число секторов на запрос31,84,0
Пропускная способность DRAM (% от пика)25%90% (в 3,6 раза)
Время выполнения ядра4,2 мс1,2 мс (в 3,5 раза)
Итоги раздела III
Ключевое положение главы

Векторизация — сокращение числа инструкций за счёт ширины вектора. float4 (16 байт) на Hopper, 32-байтовые векторы на Blackwell.

Приводимые в главе цифры

Скалярный доступ: 28% load efficiency, 31,8 сектора на запрос. Векторизованный: 97% load efficiency, 4,0 сектора. Ускорение ядра — 3,5×.

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

Проверяйте выравнивание указателей. cudaMalloc возвращает выравнивание не менее 256 байт, но смещения могут его нарушить. На Blackwell используйте 32-байтовые векторы с alignas(32).

• • •

IVТайлинг: загрузи один раз — используй много раз

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

«Распространённая ловушка производительности — повторное чтение одних и тех же данных из глобальной памяти. Тайлинг — это техника, позволяющая избежать этого, загружая фрагменты данных в более быструю on-chip разделяемую память и переиспользуя эти фрагменты многими потоками».

Объединение и векторизация ускоряют саму передачу данных, но не решают проблему повторных загрузок. Наивное умножение матриц N × N загружает каждый элемент матрицы A из HBM N раз — по разу для каждой строки B, с которой он умножается. Это приводит к N–1 избыточным загрузкам на элемент.

Тайлинг решает проблему: блок потоков загружает подматрицу (тайл) 32 × 32 из A и B в разделяемую память ровно один раз, а затем все потоки блока используют эти данные для 32 × 32 = 1024 умножений-накоплений. Каждый элемент A и B теперь загружается из HBM один раз на тайл вместо 32 раз.

#define TILE_SIZE 32

__global__ void tiledMatMul(const float* A, const float* B,
                            float* C, int N) {
    __shared__ float sA[TILE_SIZE][TILE_SIZE];
    __shared__ float sB[TILE_SIZE][TILE_SIZE];

    int row = blockIdx.y * TILE_SIZE + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    float sum = 0.0f;

    for (int t = 0; t < N; t += TILE_SIZE) {
        if (row < N && (t + threadIdx.x) < N)
            sA[threadIdx.y][threadIdx.x] = A[row * N + t + threadIdx.x];
        else sA[threadIdx.y][threadIdx.x] = 0.0f;

        if ((t + threadIdx.y) < N && col < N)
            sB[threadIdx.y][threadIdx.x] = B[(t + threadIdx.y) * N + col];
        else sB[threadIdx.y][threadIdx.x] = 0.0f;

        __syncthreads();

        for (int k = 0; k < TILE_SIZE; ++k)
            sum += sA[threadIdx.y][k] * sB[k][threadIdx.x];

        __syncthreads();
    }

    if (row < N && col < N) C[row * N + col] = sum;
}
МетрикаДо (наивное)После (тайловое)Примечания
Пропускная способность DRAM (% от пика)90%25%Тайловое ядро эффективнее — байты переиспользуются
Достигнутая занятость (%)42%89%Больше резидентных warp'ов
Пропускная способность FP15 GFLOPS170 GFLOPSПереиспользование повышает устойчивые вычисления
Сектора загрузки глобальной памяти9 8001 200Каждый элемент извлекается один раз на тайл
Пропускная способность shared memory (% от пика)52%99%Шаблон доступа избегает конфликтов банков

Падение пропускной способности DRAM с 90% до 25% — это не ухудшение, а улучшение. Наивное ядро жадно пожирает полосу, но большая часть байтов бесполезна. Тайловое ядро потребляет меньше полосы, потому что каждый байт используется многократно. Арифметическая интенсивность возрастает с 1,5 FLOP на байт до 8 FLOP на байт, и ядро переходит из режима «ограничено памятью» в режим «ограничено вычислениями».

Осторожно

Может возникнуть соблазн увеличить размер тайла до 64 × 64 или 128 × 128. Однако большие тайлы потребляют больше регистров и shared memory. Тайл 64 × 64 требует около 22 регистров на поток и ~16 КБ shared memory на входной тайл. Всегда проверяйте баланс между повторным использованием и занятостью через cudaOccupancyMaxPotentialBlockSize или отчёт Nsight Compute.

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

Тайлинг — переиспользование данных в shared memory. Каждый элемент загружается один раз и используется многократно.

Приводимые в главе цифры

Наивное ядро: 15 GFLOPS, 9 800 секторов. Тайловое: 170 GFLOPS, 1 200 секторов. Арифметическая интенсивность растёт с 1,5 до 8 FLOP/байт.

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

Балансируйте размер тайла с бюджетом регистров и shared memory. Проверяйте занятость (occupancy) через CUDA Occupancy API.

• • •

VКонфликты банков: невидимый тормоз разделяемой памяти

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

«На современных GPU NVIDIA, включая Blackwell, разделяемая память имеет 32 банка с шириной банка 4 байта (адреса отображаются по модулю 32). Как таковой, warp, шагающий на 128 байт (32 float × 4 байта), отображает все потоки в один банк. Если несколько потоков в warp'е обращаются к одному банку, возникает конфликт банков. Это заставляет обращения к памяти сериализоваться, что сводит на нет преимущество в скорости разделяемой памяти».

Разделяемая память на GPU разбита на 32 банка с шириной 4 байта (адреса отображаются по модулю 128 байт). Если несколько потоков warp'а обращаются к одному банку, возникает конфликт — обращения сериализуются. 32-сторонний конфликт означает, что 32 параллельных чтения превращаются в 32 последовательных, уничтожая преимущество разделяемой памяти в скорости.

Схема 4. Идеальный случай: ни одного конфликта

32 lane, 32 банка, каждый поток бьёт в свой банк — 1 цикл

32 banks · 32 lanes · 1 cycle 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 B15 all 32 requests serviced in parallel — 1 cycle
Описание: Shared memory устроена как 32 банка × 4 байта. Если каждый поток в warp обращается к своему банку, все запросы обслуживаются одновременно за один цикл. Shared memory throughput ≈ 100%.

Классический сценарий конфликта — транспозиция матрицы с тайлом 32 × 32. Запись в разделяемую память идёт по строкам (tile[ty][tx]) и объединена. Но чтение — по столбцам (tile[tx][ty]): при фиксированном threadIdx.x и варьируемом threadIdx.y все 32 потока попадают в один банк, потому что строки отстоят на 128 байт. Это полный 32-сторонний конфликт.

Схема 5. Двухсторонний и 16-сторонний конфликты

Чем больше потоков в одном банке — тем сильнее сериализация

2-way conflict 16-way conflict B0 B1 B2 B3 B4 B5 B6 B7 0 1 2 3 4 5 B0 B1 B2 B3 B4 B5 B6 B7 0 1 2 3 4 5 6 7 accesses serialized — throughput collapses
Описание: 2-way: два потока в один банк → 2 replay, ~2× медленнее. 16-way: 16 потоков бьют в один банк → 16 replay. Nsight Compute покажет рост bank conflicts и падение shared-memory throughput.

Аналогичная проблема возникает при шаге, равном числу банков:

__shared__ float arr[32 * warpCount];
float x = arr[threadIdx.x * 32];  // 32-way conflict: все в банке 0

Схема 6. Padding как лекарство от конфликтов

Одна лишняя колонка сдвигает строки в разные банки

32 × 32 (no padding) — 32-way conflict 32 × 33 (+1 pad) — conflict-free column 0 → all hit bank 0 column 0 → banks shift by 1 each row
Описание: В 32 × 32 тайле все строки имеют ширину 32 float = 128 B. Поток i, читающий tile[i][0], попадает в банк 0 вместе с остальными. Добавляем одну padding-колонку (32 × 33) — шаг становится 132 B, банки сдвигаются, конфликты исчезают.

Пример «до» (C++): наивная транспозиция

#define TILE_DIM 32

__global__ void transposeNaive(const float *idata,
                               float *odata, int width) {
    __shared__ float tile[TILE_DIM][TILE_DIM];
    int x = blockIdx.x * TILE_DIM + threadIdx.x;
    int y = blockIdx.y * TILE_DIM + threadIdx.y;
    tile[threadIdx.y][threadIdx.x] = idata[y * width + x];
    __syncthreads();
    odata[x * width + y] = tile[threadIdx.x][threadIdx.y];
}

Пример «после» (C++): транспозиция с дополнением

#define TILE_DIM 32
#define PAD 1

__global__ void transposePadded(const float *idata,
                                float *odata, int width) {
    __shared__ float tile[TILE_DIM][TILE_DIM + PAD];
    int x = blockIdx.x * TILE_DIM + threadIdx.x;
    int y = blockIdx.y * TILE_DIM + threadIdx.y;
    tile[threadIdx.y][threadIdx.x] = idata[y * width + x];
    __syncthreads();
    odata[x * width + y] = tile[threadIdx.x][threadIdx.y];
}
МетрикаДо (без дополнения)После (с дополнением)
Конфликты банков при загрузке shared memory4,8 млн0
Использование shared memory (% от пика)52%100%
Доля простоев warp'ов (память)38%0,5%
Время выполнения ядра4 мс1,3 мс (в 3 раза)

Альтернатива дополнению — swizzling, преобразование индексов, которое «перемешивает» потоки по банкам без увеличения расхода памяти:

int bank_idx = threadIdx.x ^ (threadIdx.y & 0x1F);

Библиотека CUTLASS от NVIDIA активно использует swizzling в своих итераторах тайлов. Дополнение проще, swizzling экономнее по памяти — выбор зависит от контекста.

Исключение

Если все 32 потока в warp'е обращаются к точно одному и тому же адресу в одном банке, аппаратура выполнит широковещательную рассылку значения всем потокам за один цикл. Любой другой сценарий с двумя и более различными адресами в одном банке вызовет конфликт.

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

Конфликты банков — скрытая стоимость shared memory. Padding (33 вместо 32) или swizzling устраняют конфликты.

Приводимые в главе цифры

Без дополнения: 4,8 млн конфликтов, 52% shared memory throughput. С дополнением: 0 конфликтов, 100% throughput. Ускорение ядра — 3×.

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

Проверяйте bank conflicts в Nsight Compute. Если они есть — добавьте padding или используйте swizzling. Стоимость дополнения — ~3% памяти, выигрыш — до 3× по времени.

• • •

VIWarp shuffle: обмен через регистры, мимо памяти

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

«Наиболее распространёнными являются интринсики __shfl_sync (shuffle). __shfl_sync позволяет транслировать значение от одного потока всем остальным потокам в warp'е. Вы также можете выполнять warp-уровневые сокращения без записи в разделяемую память».

Если потоки одного warp'а должны обменяться данными, разделяемая память — не единственный путь. Интринсики __shfl_sync позволяют передавать значения напрямую между регистрами потоков, минуя разделяемую память. Конфликты банков при этом невозможны — банки памяти не задействованы.

unsigned mask = __activemask();
float val = threadVal;

for (int offset = 16; offset > 0; offset >>= 1) {
    float other = __shfl_down_sync(mask, val, offset);
    val += other;
}

Каждый __shfl_down_sync напрямую читает регистр другой полосы, уменьшая число активных потоков вдвое на каждом шаге, пока полоса 0 не накопит полную сумму. За 5 итераций (16 → 8 → 4 → 2 → 1) все 32 потока получают общую сумму. Поскольку вся коммуникация остаётся в регистрах, нет трафика разделяемой памяти, нет конфликтов банков и нет лишних синхронизаций.

Ограничение: shuffle работает только внутри одного warp'а (32 потока). Для обмена между warp'ами по-прежнему нужна разделяемая или глобальная память с надлежащей синхронизацией.

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

Warp shuffle — обмен данными через регистры, а не через shared memory. Конфликты банков невозможны.

Ограничение

Shuffle работает только внутри одного warp'а. Для межwarp-обмена нужна shared memory или глобальная память.

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

Для редукций, префиксных сумм и обмена значениями внутри warp'а используйте __shfl_sync. Это быстрее и не создаёт конфликтов.

• • •

VIIКэш данных только для чтения: бесплатный ускоритель

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

«Когда все потоки читают одни и те же значения или поток перечитывает данные, которые не меняются, неиспользование механизмов кэширования GPU может стать узким местом… Отметьте данные, доступные только для чтения, как const __restrict__, чтобы задействовать низколатентный кэш только для чтения, сокращая трафик DRAM и повышая активность SM».

На современных GPU загрузки из глобальной памяти автоматически кэшируются в L2 и часто в L1. Но есть специализированный путь данных только для чтения, который имеет более низкую задержку для широковещательных обращений и не вытесняет другие кэшированные данные. Это не то же самое, что 64-килобайтный кэш константной памяти — кэш только для чтения значительно больше и предназначен для больших массивов неизменяемых данных, таких как векторы внедрения (embedding) в NLP-моделях.

__global__ void lookup(const float* __restrict__ table,
                       float* out, int N) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        int t = idx % T;
        out[idx] = table[t];
    }
}

Без __restrict__ компилятор не может доказать, что table не алиасит с out, и использует обычный путь глобальной памяти. С __restrict__ загрузки идут через кэш только для чтения, который поддерживает широковещание: если все 32 потока читают один и тот же адрес, значение доставляется за один цикл вместо 32 отдельных обращений.

МетрикаДо (без __restrict__)После (с __restrict__)
Эффективность загрузки глобальной памяти52%97%
Пропускная способность DRAM600 ГБ/с200 ГБ/с
Пропускная способность чтения L21 500 ГБ/с1 800 ГБ/с
Активность SM %45%93%
Время выполнения ядра2,5 мс1,0 мс
Итоги раздела VII
Ключевое положение главы

Кэш только для чтения — специализированный путь данных с более низкой задержкой для широковещательных обращений. Активируется через const __restrict__.

Приводимые в главе цифры

Без __restrict__: 52% load efficiency, 600 ГБ/с DRAM. С __restrict__: 97% load efficiency, 200 ГБ/с DRAM (снижение трафика). Ускорение ядра — 2,5×.

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

Помечайте read-only данные как const __restrict__. Это бесплатно и даёт до 2,5× ускорения для lookup-ядер.

• • •

VIIITMA и асинхронные конвейеры

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

«CUDA Pipeline API вместе с аппаратным движком Tensor Memory Accelerator (TMA) поднимают эту идею на уровень thread-block'а. Вместо того чтобы каждый warp использовал блоки загрузки/сохранения (LD/ST) SM для извлечения данных из глобальной памяти, вы можете вызвать движок TMA для асинхронного извлечения целого тайла из глобальной памяти в разделяемую память».

«TMA способна на массовые копии 1D–5D с произвольными шагами между глобальной и разделяемой памятью без блокировки конвейера инструкций SM».

Самый передовой приём — асинхронная передача данных с помощью Tensor Memory Accelerator (TMA). TMA — это аппаратный DMA-движок, который перемещает данные между глобальной памятью и разделяемой памятью без участия вычислительных блоков SM. Пока TMA загружает следующий тайл, SM вычисляет на предыдущем. TMA способна на массовые копии 1D–5D с произвольными шагами, включая объединение и многомерные передачи.

Схема 7. TMA — асинхронная загрузка из HBM в shared memory

SM считает, TMA параллельно тянет следующий тайл

HBM Global Memory tile 0 tile 1 tile 2 tile 3 TMA DMA engine Shared Memory SMEM buffer 1 buffer 2 SM compute TMA fetches tile N+1 ⟷ SM computes tile N — latency hidden
Описание: TMA — отдельный DMA-движок внутри SM. Ядро вызывает cuda::memcpy_async, и TMA асинхронно тащит тайлы HBM → SMEM. Пока TMA качает тайл N+1, SM считает над тайлом N (double buffering). Освобождаются LD/ST-юниты, скрывается ~800-цикловая latency HBM.

Чтобы запустить передачу TMA, используется cuda::memcpy_async() вместе с cuda::pipeline:

cuda::memcpy_async(sharedBuf,
                   globalPtr + offset,
                   cuda::aligned_size_t<16>(bytes),
                   pipe);
pipe.producer_commit();

Полный пример двойной буферизации с TMA

__shared__ float tile0[TILE_SIZE];
__shared__ float tile1[TILE_SIZE];
float* tiles[2] = { tile0, tile1 };

__shared__ cuda::pipeline_shared_state<
    cuda::thread_scope_block, 2> state;
auto pipe = cuda::make_pipeline(
    cuda::this_thread_block(), &state);

pipe.producer_acquire();
cuda::memcpy_async(tiles[0], global_ptr,
    cuda::aligned_size_t<32>{bytes}, pipe);
pipe.producer_commit();

for (int t = 1; t < nTiles; ++t) {
    pipe.consumer_wait();
    processTile(tiles[(t - 1) & 1]);
    pipe.consumer_release();

    pipe.producer_acquire();
    cuda::memcpy_async(tiles[t & 1],
        global_ptr + t * TILE_SIZE,
        cuda::aligned_size_t<32>{bytes}, pipe);
    pipe.producer_commit();
}

pipe.consumer_wait();
processTile(tiles[(nTiles - 1) & 1]);
pipe.consumer_release();
МетрикаДо (без предвыборки)После (асинхронная предвыборка)
Эффективность загрузки глобальной памяти23%99%
Среднее число секторов на запрос6,44,0
Пропускная способность DRAM (% от пика)25%90%
Активность SM %62%100%
Время выполнения ядра18 мс7 мс
Итоги раздела VIII
Ключевое положение главы

TMA — аппаратный DMA-движок, освобождающий SM от перемещения данных. Double buffering скрывает задержку HBM за вычислениями.

Приводимые в главе цифры

Без TMA: 23% load efficiency, 18 мс. С TMA: 99% load efficiency, 7 мс. Ускорение ядра — 2,6×.

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

Для memory-bound ядер с тайловой обработкой используйте cuda::memcpy_async с TMA. Это освобождает LD/ST-юниты для вычислений.

• • •

IXЧек-лист оптимизации памяти GPU

Все приёмы можно свести к нескольким принципам:

  • Объединяйте загрузки. Потоки warp'а должны читать соседние адреса. Переходите от AoS к SoA, убирайте шаговые индексы. Цель — 4,0 сектора на запрос, эффективность загрузки около 100%.
  • Векторизуйте. Используйте float4 (16 байт на поток) на Hopper или 32-байтовые векторы на Blackwell (CUDA 13). Проверяйте выравнивание указателей — cudaMalloc возвращает выравнивание не менее 256 байт, но смещения могут его нарушить.
  • Тайлируйте. Загружайте тайлы 32 × 32 в разделяемую память один раз, используйте многократно. Подбирайте размер тайла под бюджет регистров и shared memory.
  • Устраняйте конфликты банков. Дополняйте массивы (33 вместо 32 столбцов) или используйте swizzling индексов. Стоимость дополнения — около 3% памяти, выигрыш — до 3× по времени.
  • Используйте shuffle. Для межпоточного обмена внутри warp'а — __shfl_sync, __shfl_down_sync, __shfl_xor_sync. Конфликты банков невозможны, синхронизация не нужна.
  • Помечайте read-only данные как const __restrict__ — это включает кэш только для чтения и сокращает DRAM-трафик в 3 раза.
  • Асинхронные конвейеры. cuda::memcpy_async с TMA и двойной буферизацией скрывает задержку HBM за вычислениями.
  • Профилируйте. Nsight Compute — для поточных метрик (эффективность загрузки, конфликты банков, активность SM, причины простоев warp'ов). Nsight Systems — для временных шкал перекрытия и простоев на уровне системы.
Итоги раздела IX
Ключевое положение главы

Чек-лист из восьми пунктов: coalescing → vectorization → tiling → bank conflicts → shuffle → read-only cache → TMA → profiling.

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

Проходите по чек-листу последовательно. Каждый пункт подтверждается метриками Nsight Compute.

• • •

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

Глава 7 книги Fregly была написана в 2025 году. С тех пор появились новые данные, которые позволяют проверить прогнозы и уточнить цифры.

Подтверждено TMA как ключевая технология

В главе TMA рекомендуется для асинхронной загрузки тайлов. Открытые данные подтверждают: на Blackwell TMA стала стандартом для высокопроизводительных ядер. CUTLASS, Triton и TorchInductor генерируют код с использованием TMA. NVIDIA продолжает развивать TMA в Rubin — с ещё более широкими возможностями.

Подтверждено 32-байтовые векторы на Blackwell

В главе упоминается поддержка 32-байтовых векторных загрузок на Blackwell (CUDA 13). Это подтверждено: sm_100 поддерживает ld.global.v8.f32 и st.global.v8.f32. На Hopper (sm_90) максимальная ширина — 16 байт.

Подтверждено Рост пропускной способности памяти

В главе указано 8 ТБ/с на B200. Открытые данные: B200 — 8 ТБ/с HBM3e, B300 — 8 ТБ/с HBM3e, Rubin — 22 ТБ/с HBM4. Пропускная способность памяти растёт, но всё ещё отстаёт от роста вычислительной способности.

Подтверждено Эффективность TMA в реальных нагрузках

В главе TMA даёт 2,6× ускорения в примере. Открытые данные: документация CUTLASS указывает на «nearly optimal utilization of peak theoretical throughput» для тюнингованных GEMM-ядер. Внимание к TMA растёт с каждым поколением GPU.

Дополнение Новые форматы и техники

Глава не упоминает FP4 + микрошкалирование (NVFP4) для памяти. В 2025 году Blackwell добавил аппаратную поддержку NVFP4 с блочным микрошкалированием. Это позволяет хранить веса в 4-битном формате с сохранением точности, что вдвое сокращает трафик памяти. Также появилась аппаратная декомпрессия — она подробно рассмотрена в обзоре главы 5.

• • •

XIЗаключение

Производительность GPU-ядра определяется не количеством арифметических инструкций, а тем, насколько хорошо оно использует иерархию памяти. Ядро, которое при каждом умножении обращается к HBM, будет тратить 800 циклов на ожидание — независимо от того, насколько быстр его вычислительный блок. Ядро, которое загружает тайл один раз, переиспользует его через shared memory, обменивается значениями через регистры и асинхронно предзагружает следующий тайл через TMA, превращает эти 800 циклов ожидания в 800 циклов полезной работы.

Переход от «ограничено памятью» к «ограничено вычислениями» — это и есть цель оптимизации. Когда узкое место смещается с памяти на арифметику, производительность ядра начинает приближаться к теоретическому пику GPU. Каждый из описанных приёмов — объединение, векторизация, тайлинг, устранение конфликтов банков, shuffle, кэш только для чтения, TMA-конвейеры — это один шаг на этом пути. И каждый шаг подтверждается конкретными метриками профилировщика.

Итог

Память — не «просто ещё один ресурс». На GPU она определяет, будет ли вычислительный блок простаивать или работать на пике. Объединяйте, векторизуйте, тайлируйте, устраняйте конфликты, используйте TMA — и ваши ядра перестанут ждать данные, начав считать.

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

Цель оптимизации памяти — сместить узкое место с HBM на арифметику. Все приёмы главы служат этой цели.

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

Профилируйте каждое изменение. Nsight Compute покажет, сдвинулось ли узкое место.

Источники: Chris Fregly, «AI Systems Performance Engineering» (O'Reilly, 2026), Глава 7 — цитаты приводятся в ограниченном объёме в целях обзора и критического анализа. Документация NVIDIA CUDA C++ Programming Guide; NVIDIA Nsight Compute documentation; NVIDIA Blackwell architecture whitepaper; CUTLASS documentation; Triton documentation.

FinCodeCow · AI Infrastructure Review

GPU Memory Access Patterns: from Load Coalescing to TMA Async Pipelines

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

This material is a review of Chapter 7 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 the platform, 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.

Modern NVIDIA GPUs — from Hopper to Blackwell — deliver tens of teraflops in FP32 and even more with Tensor Cores. But between the theoretical peak and real performance there's one barrier: the memory subsystem. Data must reach compute units fast enough to keep the processor busy. And this is exactly where most kernels lose performance — not from a shortage of arithmetic, but from inefficient memory access.

Quote from the book · Chapter 7

«As AI models grow in size and complexity, a GPU's memory system often becomes the bottleneck that stands between theoretical compute capability and real-world performance».

«On a Blackwell GPU, per-device HBM3e bandwidth is up to 8 TB/s. Within the Grace Blackwell GB200 and GB300 (two GPU superchips), this increases to 16 TB/s across both GPUs. Using uncoalesced memory accesses will leave most of this bandwidth unused due to excess memory transactions and stalls».

Exposition of content

In the chapter, it is emphasized that memory optimization is not one technique but a sequence of steps, each removing the next barrier. First, load coalescing solves the problem of fragmented transactions. Then vectorization reduces instruction count. Tiling eliminates redundant loads. Eliminating bank conflicts removes serialization of shared memory. Warp shuffle replaces memory exchange with register exchange. The read-only cache reduces DRAM load. And finally, TMA with async pipelines hides HBM latency behind computation.

Each step is confirmed by concrete Nsight Compute metrics: Global Memory Load Efficiency, Average Sectors per Request, DRAM throughput, SM Active %, bank conflicts, warp stall reasons. The chapter consistently shows how these metrics change when moving from naive to optimized code.

The key idea of the chapter: GPU kernel performance is determined not by the number of arithmetic instructions but by how well the kernel uses the memory hierarchy. A kernel that hits HBM on every multiply will spend 800 cycles waiting. A kernel that loads a tile once, reuses it through shared memory, exchanges values through registers, and asynchronously prefetches the next tile via TMA turns those 800 waiting cycles into 800 cycles of useful work.

• • •

IMemory as the Bottleneck

The GPU has a multi-level memory hierarchy, and the cost of accessing each level differs by orders of magnitude. Registers are the fastest level: latency is one cycle, but capacity is limited (up to 65,536 32-bit registers per SM on Blackwell). Shared memory offers ~30 cycles of latency and up to 228 KB per SM. L2 cache, shared across all SMs, adds roughly 200 cycles. Finally, global memory — HBM3e on Blackwell — has ~800 cycles of latency, but the largest capacity: up to 192 GB per device and up to 8 TB/s bandwidth (16 TB/s in dual-chip Grace Blackwell configurations).

The key observation: HBM access latency is roughly 800× that of a register. If every warp waits 800 cycles per load, throughput collapses. All optimization therefore reduces to one principle: touch HBM less, touch the near levels of the hierarchy more, and don't let the GPU sit idle waiting for data.

Every technique below is illustrated with CUDA C++ and PyTorch code, plus Nsight Compute profiler metrics — so you see not only what to change, but why it helps.

Section I Summary
Key point of the chapter

GPU memory is the main bottleneck of modern AI workloads. HBM latency (~800 cycles) is orders of magnitude higher than register latency (1 cycle).

Figures cited in the chapter

HBM3e on Blackwell: up to 8 TB/s per GPU, 192 (180 usable) GB on B200, 16 TB/s in dual-chip configurations. Memory bandwidth grows slower than compute capability.

Practical consequence

The optimization task is to shift the balance from HBM to registers, shared memory, and L2. Every technique in the chapter is a step in this direction.

• • •

IILoad Coalescing: Why 32 Threads Must Walk Together

Quote from the book · Chapter 7

«The memory access pattern of your code can greatly impact performance. 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. This results in many more memory transactions retrieving unused data, which quickly eats up memory bandwidth».

Global memory access is fastest when the 32 threads of a warp read a contiguous memory region. The GPU hardware can merge 32 separate requests into the minimum number of 128-byte transactions — with perfect alignment this becomes one transaction per warp. Each 128-byte cache line consists of four 32-byte sectors, and optimal access fits exactly four sectors per request.

If threads scatter across non-contiguous addresses — strided indexing, irregular access, unaligned start — the hardware can't merge requests. Instead of one wide transaction, the warp issues many small ones, each pulling a 32-byte sector from HBM while only a fraction of those bytes are useful. In the worst case, sectors per request can approach 32.

Diagram 1. Coalesced vs Uncoalesced Access

32 lane requests merge into one 128-byte transaction — or explode into many

Coalesced Uncoalesced T0 T1 T2 T3 T4 T5 T6 T7 1 × 128-byte transaction T0 T1 T2 T3 T4 T5 T6 T7 8 separate transactions
Description: Left — 32 lane requests merge into a single 128-byte transaction (four 32-byte sectors). Right — the memory controller issues a separate transaction per lane: DRAM throughput drops to ~25%, kernel runs ~3.7× slower.

In Nsight Compute's Memory Workload Analysis you'll see lower Global Memory Load Efficiency, more DRAM sector reads, and average sectors per request above 4.0. DRAM throughput percentage stays well below peak, confirming that warps are burning cycles waiting on memory instead of feeding the ALUs.

Diagram 2. Three Access Patterns: Coalesced, Strided, Random

The stride directly determines the transaction count per warp

Coalesced Strided Random T0 T1 T2 T3 T4 T5 T6 T7 1 × 128 B transaction T0 T1 T2 T3 T4 T5 T6 T7 2+ transactions (50% waste) T0 T1 T2 T3 T4 T5 T6 T7 up to 8 transactions
Description: Coalesced — threads access addresses 0, 1, 2, …, 7 → one 128-byte transaction. Strided — stride 2, only half of each segment is used → 2+ transactions. Random — threads hit arbitrary addresses, DRAM throughput collapses.

To break out of the memory-bound regime, reorganize data so that each of the warp's 32 threads loads contiguous elements. Either index as input[idx], where idx = blockIdx.x * blockDim.x + threadIdx.x, or switch from array-of-structures (AoS) to structure-of-arrays (SoA) layout so thread i always touches element i.

Diagram 3. AoS vs SoA: How Layout Kills Coalescing

Array of structures gives stride 3, structure of arrays gives stride 1

AoS (struct x,y,z) SoA (x[], y[], z[]) x y z x y z x y z x y z stride 3 → uncoalesced x x x x x x x x y y y y y y z z z z z z stride 1 → fully coalesced
Description: AoS interleaves fields x y z | x y z | … — reading only x gives stride 3 and scattered access. SoA lays fields out in separate arrays — access becomes contiguous and coalescing works perfectly.

Before (C++): Uncoalesced Strided Access

#include <cuda_runtime.h>
#include <iostream>

__global__ void uncoalescedCopy(const float* __restrict__ in,
                                float* __restrict__ out,
                                int N, int stride) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        out[idx] = in[idx * stride];
    }
}

Before (PyTorch)

import torch

def uncoalesced_copy(input_tensor, stride):
    flat = input_tensor.contiguous().view(-1)
    idx = torch.arange(0, flat.numel(), stride,
                       device=flat.device, dtype=torch.long)
    return torch.index_select(flat, 0, idx)

In the uncoalesced version each warp's memory request fragments into an average of 8 separate 32-byte sectors. The access pattern spans two cache lines per warp, inflating transaction count and starving the memory pipeline. Nsight Compute reports only about 25% of steady-state peak DRAM throughput.

After (C++): Coalesced Access

__global__ void coalescedCopy(const float* __restrict__ in,
                              float* __restrict__ out,
                              int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        out[idx] = in[idx];
    }
}

The coalesced PyTorch version is simply out = inp.clone(), since clone() on a contiguous tensor uses vectorized memory copy under the hood. You can also apply torch.compile with mode="max-autotune" to let TorchInductor pick coalesced and vectorized schedules.

MetricBefore (uncoalesced)After (coalesced)
DRAM throughput (% of peak)25%90% (3.6×)
Global memory load efficiency23%99%
Average sectors per request8.04.0 (optimal)
SM activity %62%99%
Kernel execution time4.8 ms1.3 ms (3.7×)
Section II Summary
Key point of the chapter

Load coalescing is the basic level of memory optimization. GPU hardware can merge 32 warp requests into one 128-byte transaction, but only if threads access contiguous addresses.

Figures cited in the chapter

Uncoalesced: 25% DRAM throughput, 8 sectors per request. Coalesced: 90% DRAM throughput, 4 sectors per request. Kernel speedup — 3.7×.

Practical consequence

Check Global Memory Load Efficiency in Nsight Compute. If it's below 90% — reorganize data (SoA instead of AoS, contiguous indexing).

• • •

IIIVectorization: Fewer Instructions, More Data

Quote from the book · Chapter 7

«While memory coalescing is a runtime hardware optimization on NVIDIA GPUs, vectorized memory access is a compile-time strategy in which each load or store instruction explicitly fetches multiple contiguous elements (e.g., float4, or 16 bytes) per thread. This reduces instruction count and eliminates stitching overhead».

Coalescing is a runtime hardware optimization: the GPU itself fuses 32 four-byte requests into one wide transaction. But underneath there are still 32 separate load instructions, one per thread. Vectorization removes this overhead: each thread reads not one float but four at once — via the float4 type (16 bytes).

When all 32 warp threads issue a float4 load, together they fetch 32 × 16 = 512 bytes of contiguous data, which the memory controller splits into exactly four 128-byte transactions. Each transaction consists of four fully-used 32-byte sectors — perfect 4.0 sectors per transaction. Compared to the scalar case, the number of load instructions per thread drops 4×.

To use vectorized loads, data pointers must be aligned to the vector width. CUDA allocators like cudaMalloc return pointers aligned to at least 256 bytes. However, adding an offset may break alignment:

auto ptr4 = reinterpret_cast<const float4*>(ptr);

After (C++): Vectorized Copy with float4

__global__ void copyVector16B(const float4* __restrict__ in,
                              float4* __restrict__ out,
                              int N4) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N4) {
        out[idx] = in[idx];
    }
}

32-byte Vector Loads on Blackwell (CUDA 13)

On Blackwell a wider vector is available — 32 bytes per thread (8 floats). CUDA doesn't provide a built-in float8 type, so define your own struct with 32-byte alignment:

struct alignas(32) float8 { float v[8]; };

__global__ void copyVector32B(const float8* __restrict__ in,
                              float8* __restrict__ out, int N8) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N8) {
        out[idx] = in[idx];
    }
}

With 32-byte per-thread loads, a warp moves 1024 bytes (32 threads × 32 bytes), mapping to 8 × 128-byte lines. On Hopper the same operation requires two 16-byte loads per thread, since Hopper doesn't support 32-byte vector instructions.

MetricBefore (scalar)After (vectorized)
Global memory load efficiency28%97%
Average sectors per request31.84.0
DRAM throughput (% of peak)25%90% (3.6×)
Kernel execution time4.2 ms1.2 ms (3.5×)
Section III Summary
Key point of the chapter

Vectorization is reducing instruction count by widening the vector. float4 (16 bytes) on Hopper, 32-byte vectors on Blackwell.

Figures cited in the chapter

Scalar: 28% load efficiency, 31.8 sectors per request. Vectorized: 97% load efficiency, 4.0 sectors. Kernel speedup — 3.5×.

Practical consequence

Check pointer alignment. cudaMalloc returns at least 256-byte alignment, but offsets can break it. On Blackwell use 32-byte vectors with alignas(32).

• • •

IVTiling: Load Once, Use Many Times

Quote from the book · Chapter 7

«A common performance pitfall is repeatedly reading the same data from global memory. Tiling is a technique to avoid this by loading chunks of data into faster on-chip shared memory—and reusing those chunks across many threads».

Coalescing and vectorization speed up the transfer itself but don't solve the re-loading problem. Naive N × N matrix multiplication loads every element of A from HBM N times — once for each row of B it's multiplied against. That's N–1 redundant loads per element.

Tiling solves it: a block of threads loads a 32 × 32 sub-matrix (tile) of A and B into shared memory exactly once, then all block threads use that data for 32 × 32 = 1024 multiply-accumulates. Each A and B element is now loaded from HBM once per tile instead of 32 times.

#define TILE_SIZE 32

__global__ void tiledMatMul(const float* A, const float* B,
                            float* C, int N) {
    __shared__ float sA[TILE_SIZE][TILE_SIZE];
    __shared__ float sB[TILE_SIZE][TILE_SIZE];

    int row = blockIdx.y * TILE_SIZE + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    float sum = 0.0f;

    for (int t = 0; t < N; t += TILE_SIZE) {
        if (row < N && (t + threadIdx.x) < N)
            sA[threadIdx.y][threadIdx.x] = A[row * N + t + threadIdx.x];
        else sA[threadIdx.y][threadIdx.x] = 0.0f;

        if ((t + threadIdx.y) < N && col < N)
            sB[threadIdx.y][threadIdx.x] = B[(t + threadIdx.y) * N + col];
        else sB[threadIdx.y][threadIdx.x] = 0.0f;

        __syncthreads();

        for (int k = 0; k < TILE_SIZE; ++k)
            sum += sA[threadIdx.y][k] * sB[k][threadIdx.x];

        __syncthreads();
    }

    if (row < N && col < N) C[row * N + col] = sum;
}
MetricBefore (naive)After (tiled)Notes
DRAM throughput (% of peak)90%25%Tiled kernel is efficient — bytes reused
Achieved occupancy (%)42%89%More resident warps
FP throughput15 GFLOPS170 GFLOPSReuse boosts sustained compute
Global memory load sectors9,8001,200Each element fetched once per tile
Shared memory throughput (% of peak)52%99%Access pattern avoids bank conflicts

The drop in DRAM throughput from 90% to 25% is not a regression — it's the win. The naive kernel greedily consumes bandwidth but most bytes are useless. The tiled kernel consumes less bandwidth because each byte is used many times. Arithmetic intensity rises from 1.5 FLOP/byte to 8 FLOP/byte, and the kernel shifts from memory-bound to compute-bound.

Watch out

It's tempting to grow tiles to 64 × 64 or 128 × 128 for even more reuse. But larger tiles consume more registers and shared memory. A 64 × 64 tile needs ~22 registers per thread and ~16 KB shared memory per input tile. Always balance reuse against occupancy using cudaOccupancyMaxPotentialBlockSize or Nsight Compute reports.

Section IV Summary
Key point of the chapter

Tiling is reusing data in shared memory. Each element is loaded once and used many times.

Figures cited in the chapter

Naive: 15 GFLOPS, 9,800 sectors. Tiled: 170 GFLOPS, 1,200 sectors. Arithmetic intensity rises from 1.5 to 8 FLOP/byte.

Practical consequence

Balance tile size with register and shared memory budgets. Check occupancy via CUDA Occupancy API.

• • •

VBank Conflicts: The Invisible Shared Memory Brake

Quote from the book · Chapter 7

«On modern NVIDIA GPUs, including Blackwell, shared memory has 32 banks with a 4-byte bank width (i.e., addresses map mod 32). As such, a warp that strides by 128 B (32 floats × 4 B) maps all threads to the same bank. If multiple threads in a warp access the same bank, a bank conflict occurs. This forces the memory accesses to serialize, which negates the speed advantage of shared memory».

Shared memory on GPUs is split into 32 banks, each 4 bytes wide (addresses map modulo 128 bytes). If multiple warp threads access the same bank, a conflict occurs and accesses serialize. A 32-way conflict turns 32 parallel reads into 32 sequential ones, destroying shared memory's latency advantage.

Diagram 4. Ideal Case: No Bank Conflicts

32 lanes, 32 banks, each thread hits its own bank — 1 cycle

32 banks · 32 lanes · 1 cycle 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 B0B1 B2B3 B4B5 B6B7 B8B9 B10B11 B12B13 B14B15 all 32 requests serviced in parallel — 1 cycle
Description: Shared memory is 32 banks × 4 bytes. If each warp thread hits its own bank, all requests are serviced simultaneously in one cycle. Shared memory throughput ≈ 100%.

The classic conflict scenario is a 32 × 32 matrix transpose tile. Writes to shared memory go by rows (tile[ty][tx]) and coalesce. Reads go by columns (tile[tx][ty]): with threadIdx.x fixed and threadIdx.y varying, all 32 threads land in the same bank, because rows are 128 bytes apart. That's a full 32-way conflict.

Diagram 5. Two-Way and 16-Way Bank Conflicts

The more threads hit one bank, the worse the serialization

2-way conflict 16-way conflict B0B1 B2B3 B4B5 B6B7 0 1 2 3 4 5 B0B1 B2B3 B4B5 B6B7 0 1 2 3 4 5 6 7 accesses serialized — throughput collapses
Description: 2-way: two threads hit one bank → 2 replays, ~2× slower. 16-way: 16 threads hit one bank → 16 replays. Nsight Compute will show rising bank conflicts and falling shared-memory throughput.

A similar problem arises when the stride equals the bank count:

__shared__ float arr[32 * warpCount];
float x = arr[threadIdx.x * 32];  // 32-way conflict: all in bank 0

Diagram 6. Padding as a Cure for Bank Conflicts

One extra column shifts each row into a different bank

32 × 32 (no padding) — 32-way conflict 32 × 33 (+1 pad) — conflict-free column 0 → all hit bank 0 column 0 → banks shift by 1 each row
Description: In a 32 × 32 tile every row is 32 floats = 128 B wide. Thread i reading tile[i][0] lands in bank 0 together with everyone else. Add one padding column (32 × 33) — the stride becomes 132 B, banks shift, conflicts vanish.

Before (C++): Naive Transpose

#define TILE_DIM 32

__global__ void transposeNaive(const float *idata,
                               float *odata, int width) {
    __shared__ float tile[TILE_DIM][TILE_DIM];
    int x = blockIdx.x * TILE_DIM + threadIdx.x;
    int y = blockIdx.y * TILE_DIM + threadIdx.y;
    tile[threadIdx.y][threadIdx.x] = idata[y * width + x];
    __syncthreads();
    odata[x * width + y] = tile[threadIdx.x][threadIdx.y];
}

After (C++): Padded Transpose

#define TILE_DIM 32
#define PAD 1

__global__ void transposePadded(const float *idata,
                                float *odata, int width) {
    __shared__ float tile[TILE_DIM][TILE_DIM + PAD];
    int x = blockIdx.x * TILE_DIM + threadIdx.x;
    int y = blockIdx.y * TILE_DIM + threadIdx.y;
    tile[threadIdx.y][threadIdx.x] = idata[y * width + x];
    __syncthreads();
    odata[x * width + y] = tile[threadIdx.x][threadIdx.y];
}
MetricBefore (no pad)After (padded)
Shared memory bank conflicts4.8 M0
Shared memory throughput (% of peak)52%100%
Warp stall (memory)38%0.5%
Kernel execution time4 ms1.3 ms (3×)

An alternative to padding is swizzling — an index transformation that shuffles threads across banks without spending extra memory:

int bank_idx = threadIdx.x ^ (threadIdx.y & 0x1F);

NVIDIA's CUTLASS library uses swizzling heavily in its tile iterators. Padding is simpler, swizzling saves memory — the choice depends on context.

Exception

If all 32 warp threads read exactly the same address in one bank, the hardware broadcasts the value to all threads in a single cycle. Any other pattern with two or more distinct addresses in the same bank triggers a conflict.

Section V Summary
Key point of the chapter

Bank conflicts are the hidden cost of shared memory. Padding (33 instead of 32) or swizzling eliminates conflicts.

Figures cited in the chapter

Without padding: 4.8M conflicts, 52% shared memory throughput. With padding: 0 conflicts, 100% throughput. Kernel speedup — 3×.

Practical consequence

Check bank conflicts in Nsight Compute. If present — add padding or use swizzling. Padding costs ~3% memory, reward up to 3× in time.

• • •

VIWarp Shuffle: Communication Through Registers

Quote from the book · Chapter 7

«The most common are the __shfl_sync intrinsics (shuffle). __shfl_sync lets you broadcast a value from one thread to all other threads in a warp. You can also perform warp-level reductions without ever writing to shared memory».

If threads within a warp need to exchange data, shared memory isn't the only path. The __shfl_sync intrinsics let values pass directly between thread registers, bypassing shared memory. Bank conflicts are impossible — no memory banks are involved.

unsigned mask = __activemask();
float val = threadVal;

for (int offset = 16; offset > 0; offset >>= 1) {
    float other = __shfl_down_sync(mask, val, offset);
    val += other;
}

Each __shfl_down_sync directly reads another lane's register, halving the active lanes each step until lane 0 accumulates the full sum. In 5 iterations (16 → 8 → 4 → 2 → 1) all 32 threads receive the total. Because all communication stays in registers, there's no shared memory traffic, no bank conflicts, and no extra synchronization.

Restriction: shuffle works only within one warp (32 threads). Inter-warp communication still requires shared or global memory with proper synchronization.

Section VI Summary
Key point of the chapter

Warp shuffle is register-level communication. Bank conflicts are impossible.

Limitation

Shuffle works only within one warp. Inter-warp communication requires shared or global memory.

Practical consequence

For reductions, prefix sums, and value exchange within a warp, use __shfl_sync. It's faster and creates no conflicts.

• • •

VIIRead-Only Data Cache: A Free Accelerator

Quote from the book · Chapter 7

«When all threads read the same values or a thread rereads data that does not change, failing to use the GPU's caching mechanisms can bottleneck performance… Mark read-only data as const __restrict__ to tap into the low-latency read-only cache, cutting DRAM traffic and lifting SM activity».

On modern GPUs, global memory loads are automatically cached in L2 and often in L1. But there's a dedicated read-only data path with lower latency for broadcast access, and it doesn't evict other cached data. This is not the same as the 64 KB constant cache — the read-only cache is much larger and meant for big immutable arrays like embedding tables in NLP models.

__global__ void lookup(const float* __restrict__ table,
                       float* out, int N) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        int t = idx % T;
        out[idx] = table[t];
    }
}

Without __restrict__ the compiler can't prove table doesn't alias out, so it uses the ordinary global memory path. With __restrict__ loads go through the read-only cache, which supports broadcast: if all 32 threads read the same address, the value arrives in one cycle instead of 32 separate accesses.

MetricBefore (no __restrict__)After (with __restrict__)
Global memory load efficiency52%97%
DRAM throughput600 GB/s200 GB/s
L2 read throughput1,500 GB/s1,800 GB/s
SM activity %45%93%
Kernel execution time2.5 ms1.0 ms
Section VII Summary
Key point of the chapter

The read-only cache is a specialized data path with lower latency for broadcast access. Activated via const __restrict__.

Figures cited in the chapter

Without __restrict__: 52% load efficiency, 600 GB/s DRAM. With __restrict__: 97% load efficiency, 200 GB/s DRAM (traffic reduction). Kernel speedup — 2.5×.

Practical consequence

Mark read-only data as const __restrict__. It's free and delivers up to 2.5× speedup for lookup kernels.

• • •

VIIITMA and Async Pipelines

Quote from the book · Chapter 7

«CUDA's Pipeline API together with the Tensor Memory Accelerator (TMA) hardware engine take this idea to the thread-block level. Instead of having each warp use the SM's load and store (LD/ST) units to fetch data from global memory, you can invoke the TMA engine to asynchronously fetch an entire tile from global memory into shared memory».

«TMA is capable of 1D–5D bulk copies and arbitrary strides between global and shared memory without blocking the SM instruction pipeline».

The most advanced technique is asynchronous data transfer with the Tensor Memory Accelerator (TMA). TMA is a hardware DMA engine that moves data between global and shared memory without SM compute blocks. While TMA loads the next tile, the SM computes on the previous one. TMA can handle bulk 1D–5D copies with arbitrary strides, including coalescing and multidimensional transfers.

Diagram 7. TMA — Async Fetch from HBM to Shared Memory

SM computes, TMA pulls the next tile in parallel

HBM Global Memory tile 0 tile 1 tile 2 tile 3 TMA DMA engine Shared Memory SMEM buffer 1 buffer 2 SM compute TMA fetches tile N+1 ⟷ SM computes tile N — latency hidden
Description: TMA is a dedicated DMA engine inside the SM. The kernel issues cuda::memcpy_async and TMA asynchronously pulls tiles HBM → SMEM. While TMA fetches tile N+1, the SM computes on tile N (double buffering). LD/ST units are freed, the ~800-cycle HBM latency is hidden.

To launch a TMA transfer, use cuda::memcpy_async() with cuda::pipeline:

cuda::memcpy_async(sharedBuf,
                   globalPtr + offset,
                   cuda::aligned_size_t<16>(bytes),
                   pipe);
pipe.producer_commit();

Full Double-Buffering Example with TMA

__shared__ float tile0[TILE_SIZE];
__shared__ float tile1[TILE_SIZE];
float* tiles[2] = { tile0, tile1 };

__shared__ cuda::pipeline_shared_state<
    cuda::thread_scope_block, 2> state;
auto pipe = cuda::make_pipeline(
    cuda::this_thread_block(), &state);

pipe.producer_acquire();
cuda::memcpy_async(tiles[0], global_ptr,
    cuda::aligned_size_t<32>{bytes}, pipe);
pipe.producer_commit();

for (int t = 1; t < nTiles; ++t) {
    pipe.consumer_wait();
    processTile(tiles[(t - 1) & 1]);
    pipe.consumer_release();

    pipe.producer_acquire();
    cuda::memcpy_async(tiles[t & 1],
        global_ptr + t * TILE_SIZE,
        cuda::aligned_size_t<32>{bytes}, pipe);
    pipe.producer_commit();
}

pipe.consumer_wait();
processTile(tiles[(nTiles - 1) & 1]);
pipe.consumer_release();
MetricBefore (no prefetch)After (async prefetch)
Global memory load efficiency23%99%
Average sectors per request6.44.0
DRAM throughput (% of peak)25%90%
SM activity %62%100%
Kernel execution time18 ms7 ms
Section VIII Summary
Key point of the chapter

TMA is a hardware DMA engine that frees the SM from moving data. Double buffering hides HBM latency behind computation.

Figures cited in the chapter

Without TMA: 23% load efficiency, 18 ms. With TMA: 99% load efficiency, 7 ms. Kernel speedup — 2.6×.

Practical consequence

For memory-bound tiled kernels, use cuda::memcpy_async with TMA. It frees LD/ST units for compute.

• • •

IXGPU Memory Optimization Checklist

All techniques reduce to a few principles:

  • Coalesce loads. Warp threads must read neighboring addresses. Go from AoS to SoA, eliminate strided indexing. Target 4.0 sectors per request, ~100% load efficiency.
  • Vectorize. Use float4 (16 bytes per thread) on Hopper, or 32-byte vectors on Blackwell (CUDA 13). Check pointer alignment — cudaMalloc returns at least 256-byte alignment but offsets can break it.
  • Tile. Load 32 × 32 tiles into shared memory once, reuse many times. Balance tile size against register and shared memory budgets.
  • Eliminate bank conflicts. Pad arrays (33 instead of 32 columns) or use index swizzling. Padding costs ~3% extra memory, rewards up to 3× in time.
  • Use shuffle. For in-warp communication — __shfl_sync, __shfl_down_sync, __shfl_xor_sync. No bank conflicts, no synchronization needed.
  • Mark read-only data as const __restrict__ — enables the read-only cache and cuts DRAM traffic 3×.
  • Async pipelines. cuda::memcpy_async with TMA and double buffering hides HBM latency behind compute.
  • Profile. Nsight Compute for per-thread metrics (load efficiency, bank conflicts, SM activity, warp stall reasons). Nsight Systems for system-level overlap timelines.
Section IX Summary
Key point of the chapter

The eight-point checklist: coalescing → vectorization → tiling → bank conflicts → shuffle → read-only cache → TMA → profiling.

Practical consequence

Go through the checklist sequentially. Each point is confirmed by Nsight Compute metrics.

• • •

XForecast Verification and Current Data

Chapter 7 of Fregly's book was written in 2025. Since then, new data has emerged that allows verification of forecasts and clarification of figures.

Confirmed TMA as a key technology

The chapter recommends TMA for asynchronous tile loading. Open data confirms: on Blackwell, TMA has become the standard for high-performance kernels. CUTLASS, Triton, and TorchInductor generate code using TMA. NVIDIA continues to develop TMA in Rubin — with even wider capabilities.

Confirmed 32-byte vectors on Blackwell

The chapter mentions support for 32-byte vector loads on Blackwell (CUDA 13). This is confirmed: sm_100 supports ld.global.v8.f32 and st.global.v8.f32. On Hopper (sm_90) the maximum width is 16 bytes.

Confirmed Memory bandwidth growth

The chapter cites 8 TB/s on B200. Open data: B200 — 8 TB/s HBM3e, B300 — 8 TB/s HBM3e, Rubin — 22 TB/s HBM4. Memory bandwidth is growing, but still lags behind compute capability growth.

Confirmed TMA efficiency in real workloads

The chapter shows TMA giving 2.6× speedup in an example. Open data: CUTLASS documentation states «nearly optimal utilization of peak theoretical throughput» for tuned GEMM kernels. Attention to TMA grows with each GPU generation.

Addition New formats and techniques

The chapter does not mention FP4 + microscaling (NVFP4) for memory. In 2025, Blackwell added hardware support for NVFP4 with block-wise microscaling. This allows storing weights in a 4-bit format while preserving accuracy, halving memory traffic. Hardware decompression also emerged — it is covered in detail in the review of Chapter 5.

• • •

XIConclusion

GPU kernel performance is determined not by the number of arithmetic instructions but by how well the kernel uses the memory hierarchy. A kernel that hits HBM on every multiply will spend 800 cycles waiting — no matter how fast its compute block is. A kernel that loads a tile once, reuses it through shared memory, exchanges values through registers, and asynchronously prefetches the next tile via TMA turns those 800 waiting cycles into 800 cycles of useful work.

Moving from memory-bound to compute-bound is the goal of optimization. When the bottleneck shifts from memory to arithmetic, kernel performance approaches the theoretical GPU peak. Each of the techniques described — coalescing, vectorization, tiling, eliminating bank conflicts, shuffle, read-only cache, TMA pipelines — is a step on that path. And each step is backed by concrete profiler metrics.

Summary

Memory is not "just another resource". On the GPU it determines whether the compute block sits idle or runs at peak. Coalesce, vectorize, tile, eliminate conflicts, use TMA — and your kernels will stop waiting for data and start computing.

Section XI Summary
Key point of the chapter

The goal of memory optimization is to shift the bottleneck from HBM to arithmetic. All techniques in the chapter serve this goal.

Practical consequence

Profile every change. Nsight Compute will show whether the bottleneck has shifted.

Sources: Chris Fregly, «AI Systems Performance Engineering» (O'Reilly, 2026), Chapter 7 — quotes provided in limited volume for review and critical analysis purposes. NVIDIA CUDA C++ Programming Guide; NVIDIA Nsight Compute documentation; NVIDIA Blackwell architecture whitepaper; CUTLASS documentation; Triton documentation.