Траектория «Параллельные вычисления» · линия C · конспект 3 из 8

Память GPU: шесть этажей

О чём эта тема
Иерархия памяти видеокарты — от регистров до глобальной DRAM, — функции cudaMalloc / cudaMemcpy / cudaFree и жизненный цикл данных «хост → устройство → хост», без которого не работает ни одна CUDA-программа.
Аннотация
У видеокарты не одна память, а шесть, и отличаются они друг от друга скоростью на два порядка. Конспект расставляет их по этажам: регистры и разделяемая память живут прямо на мультипроцессоре и работают почти мгновенно; глобальная, локальная, константная и текстурная — в DRAM видеокарты, до которой сотни тактов. Для каждого типа — кто его видит, сколько он живёт и когда его выбирать; всё сведено в таблицу и собственную схему. Практическая часть — протокол работы с глобальной памятью: выделить, скопировать с хоста, посчитать, скопировать обратно, освободить — с разбором каждой функции и её ошибок. Отдельно — как устроены банки разделяемой памяти и что такое конфликт банков. Тренажёр предлагает шесть сценариев: выберите правильный этаж для данных.
Пререквизиты
C01 — компиляция; C02 — грид, блоки, треды, SM: типы памяти привязаны ровно к этой иерархии.
Мотивация
В C02 ядро-«перекличка» ничего не вычисляло — ему не нужны были данные. Настоящему ядру нужны: массивы на входе, массивы на выходе. Но у хоста и устройства разные памяти: указатель на обычный массив C++ для видеокарты — бессмысленный адрес в чужом пространстве. Значит, данные придётся копировать между этими памятями, и то, как вы это делаете и где храните, определяет скорость программы сильнее, чем само вычисление: у опытных CUDA-программистов оптимизация почти всегда начинается со слова «память».

1. Две разные памяти и мост между ними

Оперативная память компьютера принадлежит хосту. У видеокарты — своя собственная DRAM (на коробке её называют «видеопамять», 8–24 ГБ у современных карт). Процессор не умеет напрямую адресовать память видеокарты, а видеокарта — память хоста; между ними — шина PCIe, по которой данные копируются. Отсюда протокол любой CUDA-программы, который вы уже видели в схеме и теперь выучите руками:

выделить память на устройстве
cudaMalloc
скопировать вход хост→устройство
cudaMemcpy H2D
запустить ядро
<<<…>>>
скопировать результат обратно
cudaMemcpy D2H
освободить
cudaFree

Три функции моста:

// выделить size байт в глобальной памяти устройства
cudaMalloc((void**)&dev_ptr, size);

// скопировать size байт; последний аргумент — направление:
// cudaMemcpyHostToDevice, cudaMemcpyDeviceToHost, cudaMemcpyDeviceToDevice
cudaMemcpy(dst, src, size, cudaMemcpyHostToDevice);

// освободить память устройства
cudaFree(dev_ptr);

Все три возвращают код cudaError_t: cudaSuccess при удаче, иначе — код ошибки (неверный указатель, перепутанное направление, нехватка памяти). Привычка проверять эти коды спасает часы отладки; систематически займёмся этим в C06, а пока запомните функцию-переводчик cudaGetErrorString(err) — она превращает код в человекочитаемую строку.

Типичная ошибка Передать в ядро указатель на память хоста: выделить массив через new и отдать его в kernel<<<…>>>(ptr) без cudaMalloc и cudaMemcpy. Компилятор не возразит — указатель есть указатель, — но на устройстве этот адрес указывает в никуда: ядро молча прочитает мусор или упадёт с «illegal memory access». Правило: в ядро идут только указатели, полученные от cudaMalloc.

2. Шесть этажей: от регистров до текстур

Теперь заглянем внутрь устройства. Память видеокарты делится на шесть типов, и ключ к пониманию — где физически находится каждый: прямо на кристалле мультипроцессора (быстро) или в DRAM за его пределами (сотни тактов латентности).

потоковый мультипроцессор (SM) — на кристалле, быстро регистры личные для каждого треда; самая быстрая память вообще; раздаются тредам при компиляции разделяемая (__shared__) общая для тредов ОДНОГО блока; почти как регистры по скорости; десятки КБ на блок; 32 банка шина к DRAM: сотни тактов латентности DRAM видеокарты — за пределами кристалла, медленно (кэшируется в L2/L1) глобальная видят все треды и хост; основной склад данных локальная переливы регистров и большие массивы треда константная только чтение из ядра; свой быстрый кэш текстурная только чтение; кэш «по соседям» для 2D
ТипГде физическиКто видитДоступВремя жизниСкорость
регистрыSMодин тредчтение/записьядромаксимальная
локальнаяDRAMодин тредчтение/записьядромедленно (это DRAM!)
разделяемаяSMтреды блокачтение/записьблокблизко к регистрам
глобальнаяDRAMвсе треды + хостчтение/записьпока не cudaFreeсотни тактов, кэш L2
константнаяDRAM + кэшвсе треды (чтение), хост (запись)только чтение из ядрапрограммабыстро при общем чтении
текстурнаяDRAM + кэшвсе треды (чтение)только чтение из ядрапрограммабыстро при 2D-локальности

Три уточнения, на которых часто спотыкаются старые учебники:

Константная память заслуживает одного абзаца практики: если все треды читают одни и те же неизменные данные — коэффициенты формулы, параметры модели, — их объявляют глобальной переменной со спецификатором __constant__ и заполняют с хоста функцией cudaMemcpyToSymbol. Чтение через константный кэш при таком сценарии почти бесплатно:

__constant__ float coeffs[64];                 // объявление на уровне файла

// на хосте: заполнить из обычного массива
cudaMemcpyToSymbol(coeffs, host_coeffs, 64 * sizeof(float));

// в ядре coeffs читается как обычный массив

Текстурная память — специализированный инструмент из графического прошлого GPU: кэш, оптимизированный под чтение «соседних» точек двумерных данных, плюс аппаратная интерполяция. В вычислительных программах её ниша узкая (обработка изображений), и в этой траектории мы ограничимся знакомством.

3. Разделяемая память: банки и конфликты

Разделяемая память объявляется внутри ядра спецификатором __shared__ и видна всем тредам блока — «общий склад бригады» из аналогии C02. Чтобы 32 треда варпа могли читать её одновременно, память нарезана на 32 банка: подряд идущие 32-битные слова лежат в подряд идущих банках. Если каждый тред варпа обращается к своему банку — все 32 обращения происходят за раз. Если два треда попали в разные слова одного банка — обращения выстраиваются в очередь: это конфликт банков, и обращение варпа замедляется во столько раз, какова глубина очереди.

__shared__ float a[256];
float x = a[threadIdx.x];        // конфликтов нет: тред k читает слово k — банк k % 32

__shared__ float b[32][32];
float y = b[threadIdx.x][0];     // конфликт 32-го порядка: столбец матрицы 32×32 —
                                 // все элементы в одном банке, варп идёт в очередь

Единственное исключение: если все треды читают одно и то же слово, конфликта нет — работает широковещательная рассылка. Стандартный рабочий цикл с разделяемой памятью выглядит так (подробно проживём его в C05):

загрузить данные
из глобальной в __shared__
__syncthreads()
считать
по быстрой памяти
__syncthreads()
записать результат
в глобальную
Типичная ошибка Забыть __syncthreads() между записью в разделяемую память и чтением из неё. Варпы блока выполняются не в ногу друг с другом: тред варпа 1 может прочитать ячейку раньше, чем тред варпа 0 успел её заполнить. Ошибка коварна тем, что на маленьких блоках (до 32 тредов — один варп) всё «случайно работает», а на рабочих размерах появляется мусор — иногда.

4. Тренажёр: куда положить данные

Тренажёр · выбор этажа памяти

Шесть сценариев из реальных программ. Для каждого выберите тип памяти, который здесь уместнее всего; после ответа — объяснение.

Разбор не начат.

Контрольные вопросы

Благодарность Линия C этой траектории опирается на курс «Программирование CUDA» Лаборатории суперкомпьютерных и квантовых вычислений ДВФУ — https://cc.dvfu.ru/.

Источники

  1. CUDA C++ Programming Guide. Memory Hierarchy // NVIDIA Docs : [сайт]. — URL: https://docs.nvidia.com/cuda/cuda-c-programming-guide/ (дата обращения: 09.07.2026).
  2. CUDA C++ Best Practices Guide. Memory Optimizations // NVIDIA Docs : [сайт]. — URL: https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/ (дата обращения: 09.07.2026).
  3. Using Shared Memory in CUDA C/C++ // NVIDIA Developer Blog : [сайт]. — URL: https://developer.nvidia.com/blog/using-shared-memory-cuda-cc/ (дата обращения: 09.07.2026).