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

Эффективность CUDA-ядер и арифметическая интенсивность

Обзор главы книги Chris Fregly «AI Systems Performance Engineering» (O'Reilly, 2026) — от модели Roofline и многоуровневого тайлинга до Tensor Cores, TMEM, TMA, CUTLASS и тонкой настройки PTX
Об источнике

Настоящий материал представляет собой обзор главы 9 книги 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).

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

§Контекст главы

Глава 9 книги Fregly посвящена методам повышения эффективности CUDA-ядер за счёт роста арифметической интенсивности — отношения числа операций с плавающей запятой к объёму данных, переданных из памяти. Материал охватывает как концептуальные основы (модель Roofline), так и конкретные приёмы: многоуровневое тайлирование, слияние ядер, структурированную разреженность, понижение точности, Tensor Cores, TMEM, TMA, CUTLASS и тонкую настройку PTX. Ниже последовательно изложены ключевые положения главы, а также приведены уточнения по актуальным данным о платформе и проверка прогнозов.

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

«Даже если удалось полностью скрыть задержки за счёт массивного параллелизма и высокого ILP, производительность ядра всё ещё может упираться в то, сколько полезной работы оно выполняет на каждый байт, прочитанный из памяти. Арифметическая интенсивность, её также называют операционной интенсивностью, измеряет, сколько операций с плавающей запятой выполняется на каждый байт данных, переданных из памяти, или FLOPS на байт».

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

В главе арифметическая интенсивность рассматривается как центральная метрика, определяющая, в каком режиме работает ядро — memory-bound или compute-bound. Даже полностью скрытые задержки не помогают, если ядро выполняет слишком мало вычислений на каждый прочитанный байт: оно упирается в пропускную способность памяти, а не в вычислительную мощность. Именно поэтому все последующие приёмы — тайлинг, слияние, понижение точности, Tensor Cores — направлены на одну цель: повысить числитель (FLOPS) или уменьшить знаменатель (байты) в этой дроби.

• • •

IАрифметическая интенсивность и модель Roofline

Арифметическая интенсивность (её также называют операционной интенсивностью) — это метрика, показывающая, сколько операций с плавающей запятой выполняется на каждый байт данных, переданных из памяти. Измеряется в FLOPS на байт:

I = FLOPs / Bytes [FLOP/B]

Новые поколения GPU наращивают вычислительную мощность заметно быстрее, чем пропускную способность памяти. Этот разрыв делает повышение арифметической интенсивности всё более критичным. Например, на NVIDIA H100 пиковая производительность BF16 составляет ~990 TFLOPS, а пропускная способность HBM — ~3,35 ТБ/с. Точка перегиба (ridge point) — отношение этих величин — равна примерно 296 FLOP/байт. На Blackwell B200 с поддержкой NVFP4 пиковая производительность достигает 10 петаFLOPS, а пропускная способность HBM3e — около 8 ТБ/с, что сдвигает точку перегиба ещё выше. Чем выше интенсивность, тем больше вычислений приходится на каждый загруженный байт — а значит, тем полнее используется вычислительный потенциал GPU.

Арифметическая интенсивность — ключевая метрика в модели производительности Roofline. Эта модель визуально отображает производительность ядра (FLOPs/sec) в зависимости от арифметической интенсивности (FLOPs/byte). На графике видны два аппаратных «потолка»:

  • Наклонная линия из начала координат — ограничение по пропускной способности памяти: P = B_peak × I.
  • Горизонтальная линия — ограничение по вычислительной мощности: P = F_peak.

Достижимая производительность — минимум из этих двух:

P_attainable = min(F_peak, B_peak × I)

Это позволяет сразу понять, является ли ядро memory-bound или compute-bound. Ядро с интенсивностью ниже точки перегиба I* = F_peak / B_peak находится в memory-bound режиме — его скорость упирается в пропускную способность памяти. Ядро с интенсивностью выше I* — compute-bound, оно использует ALU и Tensor Cores близко к их пиковым возможностям.

На практике графики Roofline можно строить с помощью Nsight Compute, который включает режим анализа Roofline. С его помощью можно проверить, было ли ядро изначально ограничено памятью или вычислениями, а затем отслеживать улучшения по мере оптимизаций. Анализ Speed of Light в Nsight Compute дополняет Roofline, показывая конкретные причины простоев — «Memory Throttle», промахи кэша, зависимости конвейера.

Цель всегда одна: повышать арифметическую интенсивность там, где это возможно. Добиться этого можно несколькими способами:

  • Тайлинг с повторным использованием данных — загрузка подматрицы в shared memory, где каждый элемент участвует в десятках или сотнях операций.
  • Кэширование в L1/shared memory — удержание промежуточных результатов на кристалле.
  • Слияние нескольких ядер в одно (kernel fusion) — чтобы промежуточные результаты не записывались в глобальную память.
  • Использование типов данных пониженной точности (FP16, FP8, FP4) — сокращение объёма передаваемых данных и задействование Tensor Cores.
  • Увеличение размера батча — амортизация загрузки весов по большему числу вычислений.
  • Пересчёт вместо хранения — если пересчёт в регистрах дешевле загрузки из HBM.

Современные компиляторные фреймворки, такие как TorchInductor в PyTorch, автоматически выполняют часть этих оптимизаций: удерживают вычисления на GPU, сокращают трафик к внешней памяти и повышают эффективную арифметическую интенсивность.

Стоит помнить, что не каждая задача позволяет легко повысить арифметическую интенсивность — это ограничено характеристиками алгоритма. Поэлементные операции (ReLU, dropout, bias-add) имеют интенсивность порядка 0,125–0,25 FLOP/байт и катастрофически memory-bound по отдельности. Матричные умножения, напротив, имеют интенсивность 2MNK / (MN + NK + MK), которая растёт с размером матриц и легко превышает точку перегиба.

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

Арифметическая интенсивность измеряется в FLOP/байт и определяет, в каком режиме работает ядро — memory-bound или compute-bound. Точка перегиба I* = F_peak / B_peak — граница между этими режимами.

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

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

Ограничение

Поэлементные операции принципиально memory-bound (0,125–0,25 FLOP/байт). Матричные умножения растут в интенсивности с размером матриц и легко становятся compute-bound.

• • •

IIМногоуровневое микротайлирование и программная предвыборка

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

«Как обсуждалось в главе 7, тайлинг (он же чанкинг или блочная обработка) и повторное использование данных — эффективный способ повысить арифметическую интенсивность. В той главе мы показали, как загрузка небольшой подматрицы (тайла) из A и B в shared memory позволяет каждому байту, прочитанному из глобальной памяти, участвовать во множестве операций умножения-сложения на скорости статической оперативной памяти (SRAM)».

«Это внутри-SM повторное использование (регистр → SMEM → DRAM) сокращает рабочий набор на каждом уровне — и минимизирует трафик к внешней памяти».

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

В главе тайлинг описывается как базовый приём повышения интенсивности. Загрузка тайла T × T в shared memory позволяет каждому прочитанному байту участвовать в O(T) операций. Многоуровневое микротайлирование идёт дальше: после shared memory данные загружаются в регистры векторными типами (float4, <half2>), и повторное использование происходит уже на уровне регистров. Иерархия «регистр → shared memory → DRAM» сокращает рабочий набор на каждом уровне.

Всякий раз, когда код перестраивается так, что каждый элемент загружается один раз и используется десятки или сотни раз — как при тайлинге — соотношение FLOPs на байт умножается на коэффициент повторного использования. Например, при типичном матричном умножении C = A × B с тайлом T × T глобальные чтения сокращаются с O(N) на результат до O(N/T) на тайл плюс O(T) повторного использования внутри shared memory. Для тайла 128 × 128 с размером K-измерения 64 арифметическая интенсивность составляет:

I = (128 × 128 × 64 × 2) / ((128 × 64 + 64 × 128) × 2) ≈ 64 FLOP/байт

Это на несколько порядков выше, чем у наивной поэлементной операции.

Помимо простого тайлинга в shared memory, можно ещё больше повысить интенсивность и усилить ILP с помощью многоуровневого тайлинга. При таком подходе после размещения тайла в shared memory каждый поток загружает микротайлы в регистры, используя векторные типы вроде float4 и <half2>. Таким образом, повторные операции выполняются целиком в регистрах.

Необходимо обеспечивать коалесцирование глобальных чтений при заполнении shared memory и выравнивать/свиззлить данные в shared memory, чтобы избежать конфликтов банков. Конфликты банков shared memory могут снизить эффективную пропускную способность в 2–32 раза, поэтому свиззлинг (перестановка индексов) — стандартный приём для bank-free доступа.

На современных GPU эти шаги внутреннего тайлинга часто покрываются с помощью API фрагментов MMA. Аппаратное обеспечение перемещает данные между shared memory и Tensor Memory (TMEM) неявно, используя инструкции Tensor Core. На Blackwell инструкции tcgen05 неявно переносят данные между shared memory и TMEM, используя отдельное адресное пространство TMEM. TMEM — это выделенный буфер SRAM на 256 КБ на каждый SM (512 столбцов × 128 строк × 32 бита), который существенно снижает давление на регистровый файл и уменьшает задержку промаха кэша примерно на 58% (с ~1000 циклов на H200 до ~420 циклов на B200).

Тесно связанный приём — программная предвыборка, часто реализуемая как двойная буферизация. Вместо ожидания завершения вычислений над текущим тайлом можно заранее выдать асинхронные загрузки следующего тайла в shared memory. Это перекрывает передачу DRAM → SMEM с текущими арифметическими операциями.

При использовании унифицированной памяти на суперчипе CPU-GPU, таком как Grace Blackwell, можно задействовать cudaMemPrefetchAsync(), чтобы намекнуть, что тайл скоро понадобится. Это подсказывает среде выполнения мигрировать страницы по NVLink-C2C. Однако предвыборка — это лишь подсказка, а не гарантия. Опытные пользователи часто предпочитают явный cudaMemcpy или выделение закреплённой памяти, чтобы полностью избежать накладных расходов на миграцию страниц.

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

Тайлинг повышает арифметическую интенсивность пропорционально коэффициенту повторного использования. Многоуровневое тайлирование (регистр → SMEM → DRAM) сокращает рабочий набор на каждом уровне.

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

Коалесцирование глобальных чтений и свиззлинг shared memory обязательны. Программная предвыборка (двойная буферизация) перекрывает DRAM → SMEM с вычислениями.

Ограничение

Предвыборка через cudaMemPrefetchAsync на суперчипах — лишь подсказка. Для критичных данных предпочтителен явный cudaMemcpy.

• • •

IIIВнутриядерный конвейеринг и двойная буферизация

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

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

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

В главе внутриядерный конвейеринг описывается как развитие идеи latency hiding. Если традиционно GPU полагается на переключение между варпами, то конвейеринг перекрывает память и вычисления внутри одного варпа или ядра. Ключевой инструмент — CUDA Pipeline API с двухстадийным объектом cuda::pipeline_shared_state<cuda::thread_scope_block, 2>. Две стадии — producer и consumer — перекрывают загрузку следующего тайла с вычислениями над текущим.

Традиционно GPU полагаются на многопоточность на уровне варпов для скрытия задержек. Пока один варп ждёт загрузки из памяти, другие варпы продолжают вычисления. Внутриядерный конвейеринг идёт дальше, перекрывая память и вычисления внутри одного варпа или ядра.

Ключевые вызовы CUDA Pipeline API:

  • pipe.producer_acquire() — резервирует следующую стадию конвейера для записи.
  • pipe.producer_commit() — сигнализирует, что ранее выданные асинхронные операции для этой стадии готовы к потреблению.
  • pipe.consumer_wait() — ожидает завершения ранее зафиксированных операций для этой стадии.
  • pipe.consumer_release() — освобождает текущую стадию для повторного использования.

Две стадии конвейера перекрывают загрузки из глобальной памяти с вычислениями. На первой стадии (Stage 0) один варп в потоковом блоке выдаёт асинхронную предвыборку следующего тайла в разделяемую память. Предвыборка порождает кооперативные копии cuda::memcpy_async, которые понижаются до per-thread cp.async в разделяемую память.

Пока Stage 0 производит (загружает) данные в одном варпе, остальные варпы потокового блока потребляют (вычисляют) загруженные данные во второй стадии (Stage 1). Эта простая реализация «производитель–потребитель» скрывает задержку DRAM за счёт текущих вычислений.

Схема 1. Двухстадийный паттерн «производитель–потребитель»

CUDA Pipeline API: производитель и потребитель перекрываются

Глобальная память Производитель cuda::memcpy_async producer_acquire / commit Потребитель вычисление тайла consumer_wait / release Разделяемая память (двойной буфер) тайл A тайл B
Производитель и потребитель перекрываются через cuda::pipeline

Ядро выделяет одну непрерывную область разделяемой памяти для тайлов A и B через extern __shared__ float shared_mem[]. Оно делит этот буфер на четыре подобласти — две для A и две для B — с помощью арифметики указателей. Это позволяет реализовать настоящую двойную буферизацию без дополнительных динамических выделений.

Перед входом в основной цикл ядро асинхронно предзагружает первые STAGES тайлов, используя асинхронные копии, привязанные к конвейеру: producer_acquire() → memcpy_async() → producer_commit(). Варпы-потребители используют consumer_wait() и consumer_release(), чтобы вычисления начинались точно тогда, когда предзагруженный тайл готов.

Одновременно другие варпы в блоке вызывают pipe.consumer_wait(). Это эффективно приостанавливает только те потоки, которые зависят от данных в текущем буфере, до тех пор, пока производитель не зафиксирует его. После завершения ожидания каждый поток вызывает device-функцию computeTile(...) для выполнения своего точечного произведения TILE_SIZE x TILE_SIZE. Результаты инкрементально накапливаются в регистрах.

Завершив вычисления на текущем буфере, варпы вызывают pipe.consumer_release(), чтобы освободить эту стадию для повторного использования в последующих итерациях. Этот тонкий release предотвращает блокировку всего блока и максимизирует перекрытие между вычислениями и передачей данных.

Эта простая схема с двойной буферизацией примерно в 2 раза быстрее наивного тайлинга, как показано в таблице, где сравнивается производительность наивной реализации тайлинга и оптимизированной реализации конвейера с двойной буферизацией.

МетрикаНаивный тайлингДвухстадийный конвейер
Время выполнения ядра41,3 мс20,5 мс (~2× быстрее)
SM Active %68%92% (+24%)

Примечание: числовые значения приведены для иллюстрации концепций.

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

Внутриядерный конвейеринг перекрывает память и вычисления внутри одного ядра. Двухстадийный паттерн производитель–потребитель через CUDA Pipeline API даёт примерно 2× ускорение относительно наивного тайлинга.

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

Тонкая синхронизация (consumer_wait / release) освобождает только зависимые потоки, а не весь блок. SM Active % растёт с 68% до 92%.

Ограничение

Двойная буферизация требует аккуратного управления размером буферов. Чрезмерное увеличение числа стадий может снизить occupancy из-за роста shared memory на блок.

• • •

IVСпециализация варпов и модель «производитель–потребитель»

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

«Специализация варпов расширяет двойную буферизацию, назначая операции варпам, которые используют различное аппаратное обеспечение: перемещение данных (например, TMA) и вычисления (например, Tensor Cores). Это контрастирует с повторным использованием одних и тех же варпов для загрузки данных и вычислений».

«Такой тип специализации позволяет каждому набору варпов иметь собственные последовательности инструкций. Таким образом, инструкции выдаются и выполняются непрерывно, не прерываясь другими типами операций».

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

В главе специализация варпов описывается как развитие идеи конвейеринга. Если при двойной буферизации одни и те же варпы и загружают, и считают, то при специализации роли разделяются: одни варпы (загрузчик) только загружают данные через TMA или cuda::memcpy_async, другие (вычислитель) — только вычисляют, третьи (сохранитель) — только записывают результаты. Поскольку каждый варп имеет собственный планировщик, GPU может выдавать инструкции загрузки, математики и записи из разных варпов в одном цикле.

Схема 2. Ядро без специализации варпов

Каждый варп сам загружает данные и сам считает — появляются простои

Каждый варп: Загрузка → Вычисления → Запись (нет разделения ролей) Разделяемая память (без разделения) буфер A / B / C — конкурирующий доступ Варп 0 Загрузка Вычисления Запись Смешивает все роли Варп 1 Загрузка Вычисления Запись Смешивает все роли Варп 2 Загрузка Вычисления Запись Смешивает все роли Возможны пузыри: пока варп загружает — вычисления простаивают
Без специализации: каждый варп выполняет смесь загрузки и вычислений

Такой тип специализации позволяет каждому набору варпов иметь собственные последовательности инструкций. Таким образом, инструкции выдаются и выполняются непрерывно, не прерываясь другими типами операций. В частности, специализация варпов позволяет назначить один набор «производящих» (producer) варпов для асинхронной предзагрузки тайлов через cuda::memcpy_async. Затем все остальные «потребляющие» (consumer) варпы выполняют вычисления.

Схема 3. Ядро со специализацией варпов

Варпы-загрузчики доставляют данные, варпы-вычислители считают

Варпы-загрузчики TMA / cuda::memcpy_async варп 0 → тайл A варп 1 → тайл B варп 2 → тайл C Варпы-вычислители Tensor Cores / FMA варп 4 → GEMM варп 5 → GEMM варп 6 → GEMM Разделяемая память A B Варпы-производители пишут в SMEM → Варпы-потребители читают оттуда → overlap без барьеров на весь блок
Со специализацией: один набор варпов загружает, другой вычисляет

Здесь четыре варпа назначены на роль производителей, а остальные восемь — на роль потребителей. Как и в большинстве паттернов «производитель–потребитель», можно назначить разное количество варпов для производителя и потребителя.

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

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

Интересный паттерн для специализации варпов — использование трёх различных типов варпов: «загрузчик», «вычислитель» и «сохранитель». Варп-загрузчик помещает тайлы в очередь конвейера. Варп-вычислитель запускает вычислительное ядро на каждом тайле. Варп-сохранитель записывает результаты.

Схема 4. Трёхролевой конвейер со специализацией варпов

Отдельные варпы для загрузчика, вычислителя и сохранителя — непрерывный конвейер

ЗАГРУЗЧИК TMA / cp.async Глобальная → SMEM producer_commit ВЫЧИСЛИТЕЛЬ Tensor Cores / FMA consumer_wait производство C СОХРАНИТЕЛЬ запись результатов SMEM → Глобальная consumer_release Никаких __syncthreads() на весь блок — только точечные producer/consumer handoffs
Три роли работают параллельно, создавая непрерывный конвейер

Этот конвейер со специализацией варпов устраняет простои, которые не может устранить последовательный цикл загрузки-вычисления с двойной буферизацией в одном варпе. Эффективное перекрытие передачи данных и вычислений при специализации варпов повышает загрузку GPU — особенно для длительных циклов и persistent-ядер.

Ещё один паттерн специализации варпов — модификация трёхролевого конвейера. Он назначает набор варпов на загрузчик памяти, но затем использует два набора потребительских варпов, которые «пинг-понгуют» между ролями вычислителя и сохранителя. Эта трёхролевая архитектура реализована в CUTLASS как gemm_tma_warpspecialized_pingpong.

На практике специализация варпов чрезвычайно эффективна для выжимания последних крупиц производительности. Фактически, FlashAttention v3 приписывает свои ускорения частично конвейерам со специализацией варпов, которые перекрывают GEMM и вычисления softmax — вместе с передачей данных — чтобы держать все аппаратные блоки занятыми.

Совет

Специализация варпов применяется для сценариев с несбалансированной нагрузкой или скрытия задержек — особенно когда вычислений в одном варпе недостаточно для скрытия задержки загрузки памяти. Однако если ядро небольшое — или крайне memory-bound — более простая схема с двойной буферизацией может дать аналогичные преимущества без дополнительной сложности кода.

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

Специализация варпов разделяет роли: производитель (загрузка), вычислитель (вычисления), сохранитель (запись). Это позволяет выдавать инструкции из разных варпов в одном цикле.

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

FlashAttention v3 использует специализацию варпов для перекрытия GEMM и softmax. CUTLASS предоставляет готовую реализацию gemm_tma_warpspecialized_pingpong.

Ограничение

Для небольших или крайне memory-bound ядер специализация может не оправдать сложности кода — простая двойная буферизация даёт сопоставимый эффект.

• • •

VКластеры блоков потоков и распределённая разделяемая память

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

«Кластер блоков потоков (или кластер кооперативных массивов потоков [CTA]) — это аппаратная иерархия. Он предоставляет подмножество SM вашей кооперативной сетке — и оставляет остальные свободными для использования другими ядрами… GPU предоставляет распределённую разделяемую память (DSMEM) для использования кластером блоков потоков через эти блоки».

«Сохраняя межблочные данные во внутрикристальной памяти, DSMEM значительно повышает эффективную арифметическую интенсивность, поскольку блоки могут обмениваться промежуточными результатами или выполнять редукции без обращения к глобальной памяти».

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

В главе кластеры блоков описываются как аппаратная иерархия, промежуточная между блоком и всей сеткой. Блоки внутри кластера гарантированно ко-планируются на одном GPC, что позволяет им обмениваться данными через DSMEM — распределённую разделяемую память. Это принципиально меняет класс задач, которые можно решать: вместо того чтобы выгружать промежуточные результаты в глобальную память, блоки могут обмениваться ими на кристалле.

На современных GPU можно расширить идею тайлинга с повторным использованием, используя кластеры блоков потоков из Cooperative Groups. Они позволяют нескольким блокам потоков совместно использовать данные через распределённую shared memory (DSMEM).

Кластеры блоков потоков напрямую повышают арифметическую интенсивность. Например, кластер из четырёх блоков потоков может совместно загрузить один тайл, используя функцию multicast в Tensor Memory Accelerator (TMA). Каждый тайл разделяется между четырьмя блоками так, что трафик глобальной памяти для этого тайла амортизируется по всему кластеру. Тайлы загружаются только один раз и переиспользуются всеми четырьмя блоками. Это может сократить трафик глобальной памяти до 4× в кластере 2×2, уменьшая знаменатель — количество байт, перемещаемых из глобальной памяти.

Схема 5. Кластеры потоковых блоков на GPC

Блоки одного кластера гарантированно попадают на один GPU Processing Cluster

GPC 0 Кластер 0 CTA 0 CTA 1 Кластер 1 Кластер 2 (8 CTA) GPC 1 Кластер 3 Кластер 4 Все CTA кластера гарантированно на одном GPC → DSMEM и cluster.sync() работают
Гарантия планировщика: блоки кластера всегда на одном GPC

Blackwell поддерживает кластеры блоков потоков размером до 16 блоков, если включить непортируемый размер кластера сверх стандартного лимита в 8 CTA. Для этого нужно установить атрибут cudaFuncAttributeNonPortableClusterSizeAllowed для ядра. Большие кластеры могут повысить повторное использование, но способны снизить occupancy, поэтому профилируйте перед включением 16. Кроме того, кластеры ко-планируются в GPC; если размер кластера не делит число SM на GPC, часть SM простаивает.

Важно отметить, что DSMEM позволяет CTA внутри кластера читать общую shared memory, но такие загрузки пакетизируются и их пропускная способность заметно ниже локального SMEM (около 128 байт/такт), поэтому не следует относиться к удалённой DSMEM как к локальной shared memory. Для реальной меж-CTA пропускной способности применяется cp.async.bulk.

Кластеры потоковых блоков открывают новые стратегии распараллеливания нагрузок, которые ранее требовали коммуникации через глобальную память или множества запусков ядер. Например, представьте большое матричное умножение, в котором выходной тайл слишком велик для обработки одним потоковым блоком из-за ограничений разделяемой памяти.

С кластерами потоковых блоков и DSMEM два блока могут образовать кластер и напрямую разделить общую область разделяемой памяти на кристалле, чтобы бесшовно объединить свои результаты с помощью аппаратно-поддерживаемых примитивов. Аппаратное обеспечение DSMEM позволяет одному SM выполнять загрузки/сохранения/атомарные операции в разделяемую память другого SM через быструю сеть.

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

Кластеры блоков — аппаратная иерархия, промежуточная между блоком и сеткой. DSMEM позволяет блокам обмениваться данными на кристалле, сокращая трафик глобальной памяти до 4×.

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

TMA multicast позволяет загрузить тайл один раз и раздать всем блокам кластера. Blackwell поддерживает до 16 блоков в кластере при явном разрешении.

Ограничение

Удалённая DSMEM пакетизируется и имеет пропускную способность около 128 байт/такт. Для реальной меж-CTA пропускной способности применяется cp.async.bulk.

• • •

VIСлияние ядер (kernel fusion)

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

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

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

В главе слияние ядер рассматривается как способ устранить промежуточные записи в глобальную память. Поэлементные операции, объединённые в одно ядро, удерживают промежуточные значения в регистрах. Пример с sin и sqrt показывает, что интенсивность растёт с 0,083 до 0,25 FLOP/байт — в 3 раза. Дополнительные микрооптимизации включают замену деления на rsqrtf и трансляцию скалярного результата через shared memory.

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

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

Например, наивная реализация запускает два ядра. Первое читает x и записывает y в глобальную память. Второе читает y и записывает z:

y = sin(x);
z = sqrt(y);

Здесь каждый элемент обрабатывается дважды: один раз после sin(x) и один раз после sqrt(y). Арифметическая интенсивность каждого ядра очень низкая, поскольку на каждый элемент приходится лишь одна математическая функция на операцию загрузки/сохранения. В отличие от этого, слитое ядро выполняет тот же набор операций за один проход:

z[i] = sqrt(sin(x[i]));

Каждый x[i] загружается один раз, затем и sin, и sqrt применяются в регистрах, и только финальный z[i] записывается в память. Поскольку промежуточный y никогда не уходит в глобальную память, эффективные FLOPS на байт резко возрастают, приближая операцию к вычислительному потолку.

Вертикальное слияние объединяет последовательные операции над одними и теми же данными (как в примере выше), а горизонтальное слияние объединяет параллельные операции по данным (например, bias-add и активация после матричного умножения, выполненные в эпилоге GEMM). Оба типа слияния повышают арифметическую интенсивность.

Пример: L2-нормализация

Рассмотрим конкретный пример. Предположим, нужно L2-нормализовать каждую строку длины hidden в двумерном тензоре x (форма [batch, hidden]). Для каждой строки b вычисляется норма norm_b = sqrt(sum_i x[b,i]*x[b,i] + eps), после чего записывается y[b,i] = x[b,i] / norm_b для всех i.

Наивная реализация состояла бы из трёх отдельных ядер: возведение в квадрат, редукция, деление. Каждое ядро выполняет примерно 1 FLOP на элемент и требует 12 байт трафика на элемент. Итого арифметическая интенсивность 3/36 ≈ 0,083 FLOP/байт.

Вместо этого можно объединить все три шага в одно ядро:

__global__ void fusedL2Norm(const float* __restrict__ x,
                            float* __restrict__ y,
                            int hidden) {
  extern __shared__ float sdata[];
  const int batch = blockIdx.x;
  const int tid = threadIdx.x;
  const float* batch_ptr = x + size_t(batch) * hidden;
  float local = 0.f;
  for (int i = tid; i < hidden; i += blockDim.x) {
    float v = batch_ptr[i];
    local = fmaf(v, v, local);
  }
  sdata[tid] = local;
  __syncthreads();
  for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1) {
    if (tid < offset) sdata[tid] += sdata[tid + offset];
    __syncthreads();
  }
  float inv = rsqrtf(sdata[0]);
  float* out_batch = y + size_t(batch) * hidden;
  for (int i = tid; i < hidden; i += blockDim.x) {
    out_batch[i] = batch_ptr[i] * inv;
  }
}

В этом слитом ядре каждый поток обходит свой срез x[b,*] дважды: один раз для накопления суммы квадратов и один раз для записи нормализованных выходов. Глобальный трафик — около 12 байт на элемент. На каждый элемент ядро выполняет примерно 1 умножение + 1 сложение во время редукции и 1 умножение при нормализации — итого ~3 FLOP. sqrt и rsqrt амортизируются по всей строке.

Арифметическая интенсивность слитого ядра: 3 / 12 ≈ 0,25 FLOP/байт — в 3 раза выше, чем у наивного варианта.

Дополнительные микрооптимизации: замена деления на умножение на rsqrtf; выполнение одного rsqrtf в одном потоке и трансляция скалярного результата другим потокам через shared memory.

Слияние не только повышает арифметическую интенсивность и сдвигает ядро в сторону compute-bound на Roofline, но и экономит пропускную способность памяти. Однако слишком агрессивное слияние может увеличить давление на регистры и снизить occupancy. Если использование регистров становится чрезмерным и начинается spilling в локальную память, выгода от слияния может быть перекрыта дополнительным трафиком памяти.

Когда слияние вредно: если объединяемые операции имеют радикально разные оптимальные размеры тайлов, разные требования к памяти или если одна операция compute-bound, а другая memory-bound с совершенно другим шаблоном доступа, их слияние может ухудшить производительность.

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

Слияние ядер устраняет промежуточные записи в глобальную память. Пример с sin и sqrt повышает интенсивность с 0,083 до 0,25 FLOP/байт.

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

Вертикальное слияние объединяет последовательные операции, горизонтальное — параллельные (например, эпилог GEMM). rsqrtf вместо деления и broadcast через shared memory дают дополнительный выигрыш.

Ограничение

Слишком агрессивное слияние ведёт к register spilling и снижению occupancy. Профилируйте с Nsight Compute.

• • •

VIIСтруктурированная разреженность

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

«На современных GPU разреженность 2:4 ускоряется аппаратно с помощью Sparse Tensor Cores и cuSPARSELt. Схема 2:4 означает, что ровно два из каждых четырёх последовательных весов ненулевые. Создание такой разреженности иногда называют прунингом».

«Обрезая половину весов в шаблоне 2:4, каждая загрузка памяти теперь доставляет вдвое больше ненулевых значений, которые реально участвуют в умножении».

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

В главе структурированная разреженность 2:4 описывается как аппаратно ускоряемая схема, при которой ровно два из каждых четырёх последовательных весов ненулевые. Это даёт почти 2× сокращение глобальных чтений памяти и повышение эффективной арифметической интенсивности. В PyTorch применяется to_sparse_semi_structured(). Ускорение наиболее заметно на больших батчах, где амортизируются накладные расходы на обработку индексов.

На современных GPU разреженность 2:4 ускоряется аппаратно с помощью Sparse Tensor Cores и cuSPARSELt. Схема 2:4 означает, что ровно два из каждых четырёх последовательных весов ненулевые. Создание такой разреженности иногда называют прунингом.

Обрезая половину весов в шаблоне 2:4, каждая загрузка памяти теперь доставляет вдвое больше ненулевых значений, которые реально участвуют в умножении. Это немедленно сокращает глобальные чтения памяти и повышает эффективную арифметическую интенсивность почти в 2 раза.

Структурированная разреженность применяется после обучения модели. Модель обрезается и оптимизируется для инференса. В PyTorch применяется to_sparse_semi_structured() для конвертации обученных плотных модулей в формат 2:4 перед развёртыванием разреженных GEMM на Sparse Tensor Cores.

После конвертации модели будут вызываться оптимизированные разреженные GEMM-ядра, работающие на Sparse Tensor Cores. Sparse Tensor Cores могут приближаться к 2× ускорению по сравнению с плотными аналогами для многих задач инференса — особенно при подаче больших батчей входных данных, так как это амортизирует накладные расходы на запуск ядер и обработку индексов сжатого представления.

Ограничения разреженности 2:4:

  • Реальное ускорение сильно зависит от структуры матриц, плотности паттернов и overhead на хранение индексов. Для малых матриц разрежённость может даже замедлить выполнение.
  • Ускорение предполагает, что точность модели сохраняется. На практике важно дообучить или тщательно откалибровать модель после прунинга.
  • Структурированная разреженность обычно применяется к задачам инференса. Во время обучения градиенты не получают выгоды от разрежённости 2:4.
  • Перед применением разреженности важно сначала реализовать фундаментальные оптимизации: коалесцировать все глобальные загрузки, переиспользовать данные через тайлинг и объединять поэлементные операции.
Итоги раздела VII
Ключевое положение главы

Разреженность 2:4 ускоряется аппаратно Sparse Tensor Cores и даёт почти 2× сокращение глобальных чтений и рост эффективной арифметической интенсивности.

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

В PyTorch применяется to_sparse_semi_structured(). Наибольший выигрыш — на больших батчах инференса, где амортизируются накладные расходы.

Ограничение

Для малых матриц разрежённость может замедлить выполнение. Требуется калибровка и проверка точности. Во время обучения градиенты не получают выгоды.

• • •

VIIIКомпромисс между пересчётом и хранением

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

«Также вместо хранения или загрузки предварительно вычисленных значений (например, x²) рассмотрите возможность их пересчёта по требованию, если узким местом является пропускная способность памяти. Например, повторное вычисление x*x в регистрах часто быстрее, чем загрузка ранее вычисленного x² из глобальной памяти».

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

В главе пересчёт описывается как способ обменять вычислительные ресурсы на пропускную способность памяти. Классический пример — FlashAttention, который пересчитывает статистики softmax вместо хранения полной матрицы N × N. Это превращает O(N²) доступов к HBM в O(N² / T). Решение о пересчёте или хранении зависит от соотношения compute/memory bandwidth на конкретном GPU.

Вместо хранения или загрузки предварительно вычисленных значений (например, x²) возможен их пересчёт по требованию, если узким местом является пропускная способность памяти. Например, повторное вычисление x*x в регистрах часто быстрее, чем загрузка ранее вычисленного x² из глобальной памяти.

Многие движки инференса LLM используют этот приём для экономии памяти. Вместо хранения больших тензоров активаций в HBM и их чтения обратно позже они могут пересчитывать определённые слои и активации на лету. Это аналогично activation checkpointing в контексте обучения модели.

Классический пример — FlashAttention, который пересчитывает статистики softmax в прямом проходе вместо хранения полной матрицы внимания N × N в HBM. Это обменивает несколько дополнительных FLOPS на драматическое сокращение доступа к глобальной памяти, превращая O(N²) доступов к HBM в O(N² / T), где T — размер тайла.

Пересчёт улучшает эффективные FLOPS/байт и позволяет вместить большие модели в меньший объём памяти. Решение о том, пересчитывать или хранить, зависит от соотношения вычислительной мощности и пропускной способности памяти на конкретном GPU: если ядро memory-bound, пересчёт почти всегда выгоден; если compute-bound — хранение может быть предпочтительнее.

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

Пересчёт обменивает вычисления на пропускную способность памяти. FlashAttention — классический пример: O(N²) доступов к HBM превращаются в O(N² / T).

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

Для memory-bound ядер пересчёт почти всегда выгоден. Для compute-bound — хранение может быть предпочтительнее.

• • •

IXPyTorch и арифметическая интенсивность

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

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

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

В главе подчёркивается, что PyTorch автоматически применяет многие оптимизации: тайлинг, слияние поэлементных операций, использование cuDNN и cuBLAS. SDPA может направлять вычисления к FlashAttention, memory-efficient или cuDNN бэкендам. Вложенные тензоры (NestedTensor) позволяют представить батчи переменной длины без паддинга, что повышает эффективность глобальных загрузок.

В PyTorch многие из описанных идей применяются автоматически. Компилятор PyTorch (torch.compile) может автоматически сливать цепочки поэлементных операций и даже некоторые редукции. Он использует оптимизации на уровне графа выполнения, чтобы удерживать данные на GPU и максимально их переиспользовать.

Поскольку под капотом используются оптимизированные библиотеки, такие как cuDNN и cuBLAS, PyTorch выполняет тайлинг и использует shared memory при выполнении матричных операций через torch.matmul. Кроме того, scaled_dot_product_attention (SDPA) в PyTorch может направлять вычисления к бэкендам FlashAttention, memory-efficient или cuDNN в зависимости от форм тензоров и типов данных:

torch.nn.attention.sdpa_kernel(SDPBackend.FLASH_ATTENTION)

Важно отметить, что хотя PyTorch может распознавать и компилировать большинство операций, некоторые нестандартные операции или пользовательские CUDA-операции могут не сливаться. В таких случаях может потребоваться ручная оптимизация.

Вложенные тензоры (nested tensors), или ragged tensors, позволяют представлять батчи входных данных переменной длины без паддинга. Каждый вложенный тензор упаковывает последовательности переменной длины в единый эффективный буфер. NestedTensor предоставляет обычный интерфейс тензора, но устраняет ненужный zero-padding. Таким образом, глобальные загрузки памяти становятся более эффективными, потому что каждый загруженный байт полезен в вычислении.

Вложенные тензоры полезны для LLM с последовательностями переменной длины. При использовании вложенных тензоров такие операции, как attention и batch-matrix multiplies, извлекают из памяти только необходимые данные. Это приближает ядро к режиму compute-bound на модели Roofline и помогает сократить простои, связанные с ограничением памяти.

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

PyTorch автоматически применяет тайлинг, слияние и оптимизированные библиотеки. SDPA выбирает лучший бэкенд для attention.

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

Вложенные тензоры (NestedTensor) устраняют zero-padding для последовательностей переменной длины, повышая эффективность загрузок.

Ограничение

Нестандартные и пользовательские операции могут не сливаться. Требуется ручная оптимизация.

• • •

XСмешанная точность и использование Tensor Cores

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

«Современные NVIDIA GPU реализуют вычисления пониженной точности, такие как TF32, FP16, FP8, FP4 и INT8, в Tensor Cores. Каждый SM в Blackwell имеет 256 КБ внутрикристальной памяти TMEM, выделенной для данных Tensor Core».

«При правильном использовании эти возможности могут превратить когда-то ограниченное памятью ядро с интенсивными тензорными операциями в полностью compute-bound, повысив арифметическую интенсивность (FLOPS на байт) в 2, 4 или даже 8 раз».

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

В главе смешанная точность и Tensor Cores описываются как способ радикально повысить арифметическую интенсивность. Понижение точности сокращает объём передаваемых данных, а Tensor Cores выполняют матричные операции с высокой пропускной способностью. Blackwell добавляет TMEM — 256 КБ SRAM на SM для аккумуляторов Tensor Core, что снижает давление на регистровый файл и меняет модель программирования по сравнению с Hopper.

Современные NVIDIA GPU реализуют вычисления пониженной точности, такие как TF32, FP16, FP8, FP4 и INT8, в Tensor Cores. Каждый SM в Blackwell имеет 256 КБ внутрикристальной памяти TMEM, выделенной для данных Tensor Core. Также имеется специализированный блок TMA, который асинхронно копирует тайлы между глобальной памятью и shared memory. Инструкции Tensor Core (например, tcgen05.mma) затем неявно перемещают операнды и аккумуляторы между shared memory и TMEM.

Ключевое архитектурное отличие Blackwell от Hopper: на Hopper результаты MMA были жёстко привязаны к регистровому файлу варп-группы, а на Blackwell аккумуляторы живут в TMEM, что декопулирует владение результатами от отдельных потоков. Инструкции tcgen05 являются CTA-scoped — один поток выдаёт инструкцию от имени всего CTA, в отличие от warpgroup-scoped wgmma на Hopper. Это даёт большую гибкость в планировании: одни потоки могут оркестрировать вычисления, другие — перемещать данные, и весь CTA действует как исполнительная единица.

При правильном использовании эти возможности могут превратить когда-то ограниченное памятью ядро с интенсивными тензорными операциями в полностью compute-bound, повысив арифметическую интенсивность (FLOPS на байт) в 2, 4 или даже 8 раз.

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

Tensor Cores и пониженная точность могут повысить арифметическую интенсивность в 2–8 раз. TMEM (256 КБ на SM) хранит аккумуляторы, разгружая регистры.

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

Ключевое отличие от Hopper: tcgen05 — CTA-scoped, а не warpgroup-scoped. CTA действует как единая исполнительная единица.

• • •

XIПодача данных в Tensor Cores через TMEM и TMA

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

«В основе высокопроизводительных тензорных вычислений лежит TMEM — буфер SRAM на 256 КБ на каждый SM. Однако на высоком уровне программисты не выделяют и не управляют TMEM явно. TMEM обрабатывается аппаратным обеспечением или библиотеками при использовании операций Tensor Core».

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

В главе TMEM описывается как ключевой архитектурный элемент Blackwell. 256 КБ SRAM на SM служат буфером для аккумуляторов Tensor Core. Программисты не управляют TMEM явно — это делают библиотеки и аппаратура. TMA отвечает за перемещение данных между глобальной памятью и shared memory. CUTLASS и Transformer Engine управляют всей этой сложностью автоматически.

В основе высокопроизводительных тензорных вычислений лежит TMEM — буфер SRAM на 256 КБ на каждый SM. Программисты не выделяют и не управляют TMEM явно — TMEM обрабатывается аппаратным обеспечением или библиотеками при использовании операций Tensor Core.

Под капотом Blackwell использует инструкции tcgen05.mma, которые работают с TMEM для хранения операндов и аккумуляторов. CUTLASS и библиотечные ядра управляют необходимым выделением и использованием через конфигурацию ядра и ассемблер PTX. Transformer Engine использует TMEM для промежуточных результатов, что снижает зависимости MMA от регистров.

Высокоуровневые API, такие как CUTLASS, автоматически обрабатывают всю эту сложность. При запуске операции Tensor Core MMA с использованием интринсиков CUDA MMA или GEMM из CUTLASS реализация управляет перемещением операндов через shared memory и TMEM. TMA потоково передаёт тайлы между глобальной памятью и shared memory, а инструкции Tensor Core неявно перемещают операнды между shared memory и TMEM.

Для выполнения передачи данных из глобальной памяти в shared memory применяется TMA или cuda::memcpy_async с конвейером из CUDA Pipeline API (<cuda/pipeline>). Пример двухступенчатого конвейера с двойной буферизацией:

#include <cuda/pipeline>
#include <cooperative_groups.h>
namespace cg = cooperative_groups;
extern "C" __global__
void stage_ab_tiles(const float* __restrict__ globalA,
                    const float* __restrict__ globalB,
                    float* __restrict__ outC,
                    int tile_elems,
                    int num_tiles) {
  assert((tile_elems % (32 * 4)) == 0);
  extern __shared__ float smem[];
  auto block = cg::this_thread_block();
  float* A0 = smem + 0 * tile_elems;
  float* A1 = smem + 1 * tile_elems;
  float* B0 = smem + 2 * tile_elems;
  float* B1 = smem + 3 * tile_elems;
  constexpr auto scope = cuda::thread_scope_block;
  constexpr int stages = 2;
  __shared__ cuda::pipeline_shared_state<scope, stages> pstate;
  auto pipe = cuda::make_pipeline(block, &pstate);
  pipe.producer_acquire();
  cuda::memcpy_async(block, A0, globalA + 0 * tile_elems,
                     cuda::aligned_size_t<32>{tile_elems * sizeof(float)}, pipe);
  cuda::memcpy_async(block, B0, globalB + 0 * tile_elems,
                     cuda::aligned_size_t<32>{tile_elems * sizeof(float)}, pipe);
  pipe.producer_commit();
  for (int t = 1; t < num_tiles; ++t) {
    pipe.producer_acquire();
    cuda::memcpy_async(block, (t & 1) ? A1 : A0,
                       globalA + t * tile_elems,
                       cuda::aligned_size_t<32>{tile_elems*sizeof(float)}, pipe);
    cuda::memcpy_async(block, (t & 1) ? B1 : B0,
                       globalB + t * tile_elems,
                       cuda::aligned_size_t<32>{tile_elems*sizeof(float)}, pipe);
    pipe.producer_commit();
    pipe.consumer_wait();
    float* prevA = (t & 1) ? A0 : A1;
    float* prevB = (t & 1) ? B0 : B1;
    pipe.consumer_release();
  }
  pipe.consumer_wait();
  int last = (num_tiles - 1) & 1;
  float* lastA = last ? A1 : A0;
  float* lastB = last ? B1 : B0;
  pipe.consumer_release();
}

При запуске этого ядра установите размер динамической shared memory равным 4 * tile_elems * sizeof(float), чтобы выделить A0, A1, B0 и B1. Этот шаблон двойной буферизации гарантирует, что как только один тайл находится в shared memory, Tensor Cores могут начать его обработку. Тем временем cuda::memcpy_async загружает следующий тайл в shared memory параллельно.

Выбор между TMA и cp.async (LDGSTS): TMA оптимизирован для больших регулярных блоков и достигает пиковой пропускной способности ~7,2 ТБ/с, но требует большого объёма данных в pipeline (typically >64 КиБ in-flight) для насыщения. cp.async (LDGSTS) лучше подходит для нерегулярных паттернов и меньших объёмов (<32 КиБ in-flight), достигая насыщения ~6,6 ТБ/с при ~32 КиБ in-flight. Выбирайте путь копирования по шаблону доступа, а не по идеологии.

Схема 6. Барьер уровня блока останавливает все потоки

Пока не все потоки достигли __syncthreads() — никто не продолжает

Потоки (варпы) Варп 0 Варп 1 Варп 2 Варп 3 ✓ __syncthreads() После барьера Никто не прошёл пока все не прибудут Барьер уровня блока = простаивающие варпы. Используйте pipeline API для точечной синхронизации.
Барьер уровня блока останавливает все варпы, даже готовые
Итоги раздела XI
Ключевое положение главы

TMEM — 256 КБ SRAM на SM для аккумуляторов Tensor Core. TMA перемещает тайлы между глобальной памятью и shared memory. CUDA Pipeline API управляет двойной буферизацией.

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

TMA оптимизирован для больших регулярных блоков (>64 КиБ in-flight). cp.async (LDGSTS) — для нерегулярных и меньших объёмов (<32 КиБ in-flight).

• • •

XIITF32 и автоматическая смешанная точность

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

«Хотя Tensor Cores изначально проектировались для FP16, они также поддерживают TF32, который находится между FP32 и FP16. TF32 использует 8-битный порядок, как FP32, и 10-битную мантиссу, как FP16».

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

В главе TF32 описывается как простой способ ускорить FP32-вычисления без изменения кода: достаточно установить torch.set_float32_matmul_precision('high'). Автоматическая смешанная точность (AMP) расширяет эту идею, выбирая оптимальную точность per-layer. BF16 предпочтительнее FP16 благодаря более широкому диапазону порядка.

Хотя Tensor Cores изначально проектировались для FP16, они также поддерживают TF32, который находится между FP32 и FP16. TF32 использует 8-битный порядок, как FP32, и 10-битную мантиссу, как FP16. TF32 выполняется на Tensor Cores с существенно более высокой пропускной способностью, чем FP32 на CUDA-ядрах, сохраняя при этом диапазон порядка FP32. В PyTorch включить TF32 так же просто, как установить:

import torch
torch.set_float32_matmul_precision('high')

После установки этих флагов высокоуровневые операции, такие как torch.matmul и torch.nn.Linear, автоматически выполняются как ядра TF32 Tensor Core, а не в FP32 на стандартных CUDA-ядрах.

Помимо TF32, автоматическая смешанная точность (AMP) в PyTorch может выбирать оптимальную точность (FP16 или BF16) для каждой операции и накапливать результаты в FP32 для стабильности. BF16 помогает избежать проблем с переполнением, присущих FP16:

with torch.amp.autocast("cuda", dtype=torch.bfloat16):
    output = model(input)

Под капотом TorchInductor автоматически сливает эти преобразования точности, чтобы гарантировать: большие операции GEMM выполняются на Tensor Cores в FP16 или TF32, накопление остаётся в FP32 для числовой стабильности, небольшие «чувствительные» ядра, такие как layer normalization и softmax, выполняются в FP32, а GradScaler предотвращает underflow при обучении с FP16. BF16 имеет диапазон порядка FP32, поэтому GradScaler обычно не нужен при обучении с BF16.

При использовании структурированной разреженности или экстремально низкой точности (FP8/FP4) размер батча или гранулярность тайла должны быть достаточно велики, чтобы TMEM и Tensor Cores оставались полностью загруженными. Малые батчи несут накладные расходы, включая преобразования форматов, обработку разреженных индексов, нерегулярные шаблоны памяти. Батч размером 128 или 256 полностью задействует конвейер TMEM и обеспечит производительность, близкую к пиковой.

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

TF32 включается одной строкой и даёт ускорение FP32-вычислений. AMP выбирает точность per-layer. BF16 предпочтительнее FP16 благодаря диапазону порядка.

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

Для FP8/FP4 и разреженности необходимы большие батчи (128–256) для полной загрузки TMEM и Tensor Cores.

• • •

XIIIBF16/FP16, FP8 и FP4 пониженной точности

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

«BF16/FP16 (половинная точность) поддерживается уже несколько поколений GPU, но Tensor Cores на современных GPU часто могут поддерживать более 90% пиковой пропускной способности BF16/FP16, что примерно в 4 раза превышает пиковую пропускную способность FP32».

«Чтобы ещё больше повысить пропускную способность, можно использовать FP8. Уменьшая 16-битные веса на 50% до 8 бит, вы вдвое сокращаете трафик памяти — и вдвое увеличиваете количество весов, загружаемых за одну транзакцию HBM».

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

В главе понижение точности описывается как множитель производительности: FP8 даёт 2× к FP16, FP4 — до 4×. NVFP4 с микро-масштабированием позволяет сохранить точность при 4-битных весах. B200 обеспечивает 10 петаFLOPS (плотных) в NVFP4; B300 — 15 петаFLOPS.

BF16/FP16 поддерживается уже несколько поколений GPU, но Tensor Cores на современных GPU часто могут поддерживать более 90% пиковой пропускной способности BF16/FP16, что примерно в 4 раза превышает пиковую пропускную способность FP32.

Обучение с FP16 использует более узкий 5-битный порядок, чем FP32, поэтому очень малые значения градиентов могут исчезать в ноль (underflow) без применения масштабирования потерь (loss scaling). BF16 соответствует 8-битному диапазону порядка FP32 и изначально избегает underflow — масштабирование потерь требуется редко. BF16 обычно предпочтительнее для обучения на современных GPU.

Чтобы ещё больше повысить пропускную способность, можно использовать FP8. Уменьшая 16-битные веса на 50% до 8 бит, вы вдвое сокращаете трафик памяти — и вдвое увеличиваете количество весов, загружаемых за одну транзакцию HBM. На практике матричные умножения FP8 с накоплением FP32 или TF32 достигают в 2–3 раза больших TFLOPS, чем BF16/FP16.

Для решения проблем точности при очень низкой точности Transformer Engine поддерживает FP8, а также 4-битный формат NVFP4 от NVIDIA с микро-масштабированием. NVFP4 применяет двухуровневое масштабирование, комбинируя масштабирование на уровне микроблока (каждые 32 элемента вдоль K-измерения имеют общий масштабный коэффициент в формате E8M0) и масштаб более высокого уровня. Blackwell B200 обеспечивает 10 петаFLOPS (плотных) в NVFP4, тогда как пиковая производительность FP32 составляет около 80 тераFLOPS (плотных). Blackwell B300 (Ultra) — 15 петаFLOPS (плотных) в NVFP4.

Чувствительность слоёв к квантованию: на практике разные слои трансформера имеют разную чувствительность к понижению точности. Слои attention (особенно QK-умножение) и FFN обычно более устойчивы к FP8/FP4, чем normalization и softmax, которые требуют FP32 для стабильности. Рекомендуется калибровка модели с перебором точности по слоям и измерением метрик качества (perplexity, accuracy) для каждого варианта.

Поскольку точность настолько низка, 256 КБ TMEM на SM могут вместить большие тайлы FP4 (например, 256×256), что дополнительно увеличивает внутрикристальное повторное использование и повышает производительность.

Каждое снижение точности удваивает или учетверяет количество операций на байт и, следовательно, повышает арифметическую интенсивность. Когда TMEM/TMA перекрывают память и вычисления, эти форматы низкой точности превращают ранее memory-bound ядра в полностью compute-bound, полностью задействуя вычислительные движки Tensor Core.

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

FP8 даёт ~2× к FP16, FP4 — до 4×. NVFP4 с микро-масштабированием сохраняет точность при 4-битных весах. B200 — 10 петаFLOPS, B300 — 15 петаFLOPS в NVFP4.

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

Чувствительность слоёв к квантованию различается. Attention и FFN более устойчивы к FP8/FP4, чем normalization и softmax, которым нужен FP32.

• • •

XIVINT8 и инструкции DP4A для инференса

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

«Задачи инференса LLM обычно могут допускать квантование до пониженной точности INT8, поддерживаемое современными GPU с помощью инструкций DP4A (SIMD dot-product) на обычных CUDA-ядрах и инструкций целочисленного матричного умножения-накопления (MMA) на Tensor Cores».

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

В главе INT8 описывается как способ сократить трафик памяти для весов на 75% и увеличить пропускную способность Tensor Core. DP4A выполняет четыре INT8 MAC за инструкцию. В PyTorch поддержка INT8 обеспечивается через TorchAO и вендорные бэкенды.

Задачи инференса LLM обычно могут допускать квантование до пониженной точности INT8, поддерживаемое современными GPU с помощью инструкций DP4A (SIMD dot-product) на обычных CUDA-ядрах и инструкций целочисленного матричного умножения-накопления (MMA) на Tensor Cores. На уровне инструкций DP4A выполняет четыре операции умножения-накопления INT8 (MAC) за инструкцию по сравнению с одной операцией FMA FP32 за инструкцию.

Поскольку трафик весов для INT8 составляет всего один байт на элемент вместо четырёх байт для FP32, трафик памяти для весов сокращается на 75%. Задачи инференса INT8 могут значительно превосходить FP32 благодаря более высокой пиковой пропускной способности INT8 Tensor Core и сниженному трафику памяти — каждый GPU может обрабатывать примерно в 4 раза больше данных в секунду из памяти при использовании весов INT8.

На момент написания поддержка INT8-квантования в PyTorch обеспечивается через TorchAO и вендорные бэкенды. Квантованные модули работают с использованием выделенных ядер INT8. Использование cuBLASLt или CUTLASS для низкоуровневого INT8 GEMM обеспечивает задействование Tensor Core.

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

INT8 сокращает трафик весов на 75% и использует DP4A на CUDA-ядрах или INT8 MMA на Tensor Cores.

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

TorchAO и cuBLASLt/CUTLASS обеспечивают задействование INT8 Tensor Core.

• • •

XVTransformer Engine и TMEM в деталях

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

«Современные NVIDIA GPU включают Transformer Engine, который сочетает аппаратную поддержку Tensor Core для форматов пониженной точности с программной средой выполнения для масштабирования и приведения типов… Рабочий процесс Transformer Engine прост. Сначала ядро выдаёт вызов MMA или запускает GEMM из CUTLASS. Затем прошивка Transformer Engine организует копирование TMA (или cuda::memcpy_async) весов и активаций из HBM в shared memory».

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

В главе Transformer Engine описывается как комбинация аппаратной поддержки низкой точности и программной среды. TMEM — 256 КБ SRAM на SM, где хранятся результаты Tensor Core. Инструкции tcgen05.mma перемещают операнды между shared memory и TMEM. Ограничение UMMA на Blackwell: операнд A может быть из TMEM или SMEM, B — только из SMEM.

Современные NVIDIA GPU включают Transformer Engine, который сочетает аппаратную поддержку Tensor Core для форматов пониженной точности с программной средой выполнения для масштабирования и приведения типов. Ядра в cuBLASLt, cuDNN, CUTLASS или OpenAI Triton выполняют инструкции cp.async или передачи TMA в shared memory. Инструкции Tensor Core затем неявно перемещают операнды между shared memory и TMEM.

TMEM — это буфер SRAM на 256 КБ на SM, который Transformer Engine и Tensor Cores используют для хранения результатов (вместо регистров). На практике TMEM не выделяется явно — всё обрабатывается аппаратным обеспечением.

Рабочий процесс Transformer Engine:

  1. Ядро выдаёт вызов MMA или запускает GEMM из CUTLASS.
  2. Прошивка Transformer Engine организует копирование TMA (или cuda::memcpy_async) весов и активаций из HBM в shared memory.
  3. Инструкции Tensor Core (например, tcgen05.mma) неявно перемещают операнды между SMEM и TMEM во время конвейера MMA.
  4. Частичные суммы накапливаются в TMEM с более высокой точностью (BF16, FP16, FP32).
  5. Результаты записываются из TMEM в shared или глобальную память коалесцированно.

В идеале веса находятся в FP8 или FP4, а активации приводятся к FP8/FP4, когда это возможно — иначе активации могут оставаться в формате FP16/FP32. Операции Tensor Core MMA выполняются с низкой точностью, например FP8 × FP8 с накоплением более высокой точности или FP16 × FP16 с накоплением FP32.

Если создаётся собственный цикл по тайлам, можно перекрыть перемещение данных с вычислениями Tensor Core с помощью cuda::memcpy_async и CUDA Pipeline API. Пока Tensor Cores обрабатывают текущий тайл, cuda::memcpy_async потоково передаёт следующий тайл из HBM в shared memory. Это перекрытие помогает скрыть задержку памяти и поддерживать занятость Tensor Cores.

Ограничение UMMA на Blackwell: операнд A может поступать из TMEM или SMEM, операнд B — только из SMEM. Комбинация A из TMEM и B из TMEM не поддерживается. Это ограничение влияет на структуру ядра и требует планирования размещения данных.

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

Transformer Engine управляет низкой точностью автоматически. TMEM (256 КБ) хранит аккумуляторы. TMA перемещает тайлы между HBM и SMEM.

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

Ограничение UMMA: A может быть из TMEM или SMEM, B — только из SMEM.

• • •

XVIИспользование CUTLASS для оптимальной арифметической интенсивности

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

«С CUTLASS вы пишете один шаблонный вызов, и он автоматически применяет множество продвинутых оптимизаций: тайлинг в shared memory, асинхронные передачи памяти и двойную буферизацию с помощью буфера TMEM на 256 КБ на SM».

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

В главе CUTLASS описывается как способ получить производительность ручного MMA-ядра без недель настройки. CUTLASS автоматически применяет тайлинг, двойную буферизацию, warp specialization, кластеры блоков и TMA multicast. Производительность CUTLASS сопоставима с ручным ядром в пределах ~2%.

Один из самых простых способов задействовать описанные оптимизации — использовать библиотеку NVIDIA CUTLASS. С CUTLASS вы пишете один шаблонный вызов, и он автоматически применяет множество продвинутых оптимизаций: тайлинг в shared memory, асинхронные передачи памяти, двойную буферизацию с помощью буфера TMEM на 256 КБ на SM, warp specialization, кластеры блоков потоков и TMA multicast с распределённой shared memory.

Например, предположим, нужно вычислить GEMM C = A * B с входами половинной точности и выходом половинной точности, накапливая в FP16 или FP32:

#include <cutlass/numeric_types.h>
#include <cutlass/gemm/device/gemm.h>
using Gemm = cutlass::gemm::device::Gemm<
    cutlass::half_t,
    cutlass::layout::RowMajor,
    cutlass::half_t,
    cutlass::layout::ColumnMajor,
    cutlass::half_t,
    cutlass::layout::RowMajor,
    float,
    cutlass::arch::OpClassTensorOp,
    cutlass::arch::Sm100
>;
Gemm gemm_op;
cutlass::Status status = gemm_op(
    { M, N, K },
    float(1.0f),
    A_d, lda,
    B_d, ldb,
    float(0.0f),
    C_d, ldc
);

При компиляции и запуске этого кода CUTLASS автоматически выбирает тайлы, размещает их в shared memory, генерирует асинхронные копии памяти, использует инструкции Tensor Core tcgen05.mma, накапливает частичные суммы с указанной точностью, записывает результаты из TMEM коалесцированно, и использует кластеры блоков потоков, когда это выгодно.

МетрикаРучное MMA-ядроCUTLASS GEMM
Утилизация Tensor Core~98%~98%
Регистров на поток~52~60 (немного выше)
Shared memory на блок (CTA)~2 КБ~4 КБ
Трудоёмкость разработкиВысокаяНизкая (конфигурация шаблона)

CUTLASS соответствует или превосходит производительность ручного MMA-ядра в пределах около 2%. Небольшие увеличения использования регистров и shared memory связаны с тем, что CUTLASS обобщает ядро для гибкости. Требуется всего несколько строк шаблонного кода вместо недель низкоуровневой настройки.

Шаблоны CUTLASS поддерживают типы операндов FP4, FP8, FP16, TF32 и INT8. Для сборки под конкретную архитектуру необходимо указать цель с суффиксом a (например, sm100a для Blackwell), поскольку инструкции tcgen05 относятся к категории «architecture-accelerated features» без гарантий прямой совместимости. Код, скомпилированный для SM100, несовместим с GeForce RTX 50 (SM120).

Оптимизированные библиотеки, такие как cuBLAS, построены на CUTLASS. Высокоуровневые библиотеки, такие как PyTorch, вызывают эти оптимизированные библиотеки для многих ядер. По мере того как всё больше операторов в PyTorch используют CUTLASS под капотом, можно задействовать те же оптимизации, не написав ни строчки CUDA C++.

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

CUTLASS даёт производительность ручного MMA-ядра в пределах ~2% при минимальных усилиях. Автоматически применяются тайлинг, двойная буферизация, warp specialization, кластеры, TMA multicast.

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

Для сборки под SM100 используется суффикс a (sm100a). Код несовместим с SM120.

• • •

XVIIВстраиваемый PTX и тонкая настройка SASS

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

«Для тех, кто готов выйти за пределы C++ и погрузиться в низкоуровневые микрооптимизации, CUDA позволяет использовать встраиваемый код Parallel Thread Execution (PTX) и SASS (ассемблер NVIDIA) для извлечения последних крупиц производительности, которые могут остаться на столе».

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

В главе встраиваемый PTX описывается как инструмент последней инстанции. Типичные сценарии: использование инструкции без прямого интринсика CUDA, модификаторы кэша, ручная перестановка инструкций, чтение специальных регистров. Пример с cp.async.bulk.prefetch.L2.global показывает предвыборку в L2. Таблица сравнения показывает 7% улучшение при ручной настройке.

Для тех, кто готов выйти за пределы C++ и погрузиться в низкоуровневые микрооптимизации, CUDA позволяет использовать встраиваемый код Parallel Thread Execution (PTX) и SASS (ассемблер NVIDIA) для извлечения последних крупиц производительности.

PTX — это низкоуровневая виртуальная машина параллельного выполнения потоков. Высокоуровневый компилятор (например, CUDA C++) генерирует инструкции PTX, которые транслируются в машинные инструкции целевой архитектуры. SASS — это низкоуровневый язык ассемблера, который фактически выполняется нативно на оборудовании NVIDIA GPU.

Типичные сценарии для встраиваемого PTX:

  • Использование инструкции GPU, для которой нет прямого интринсика CUDA.
  • Применение модификаторов загрузки памяти (подсказок кэша) к конкретным доступам.
  • Вставка барьеров памяти или синхронизации в точных точках.
  • Ручная перестановка инструкций для избежания простоев конвейера.
  • Чтение специальных регистров (SM ID, warp lane ID).

Пример предвыборки в L2:

__global__ void PrefetchExample(const float *in, float *out) {
  int idx = blockIdx.x * blockDim.x + threadIdx.x;
  asm volatile("cp.async.bulk.prefetch.L2.global [%0], %1;"
               :: "l"(in + idx + 32), "n"(128));
  float x = in[idx];
  out[idx] = x;
}

Управление поведением кэша через встраиваемый PTX:

asm("ld.global.cg.f32 %0, [%1];" : "=f"(val) : "l"(ptr))

Загрузка float с модификатором .cg («cache global») означает, что мы хотим кэшировать данные в L2, но обойти кэш L1.

Доступ к специальным регистрам:

asm("mov.u32 %0, %smid;" : "=r"(smid))

Некоторые разработчики использовали %smid в постоянных ядрах (persistent kernels), чтобы только один блок на SM выполнял определённую работу.

ВерсияПростои варпов (память)Issue IPCВремя ядраУскорение
Оптимизированный C++ (расписание компилятора)35% циклов1.51.07 мс1.0× базовое
Настроенный вручную PTX (ручное планирование и подсказки)20% циклов1.61.00 мс1.07×

После настройки простои памяти ядра уменьшились за счёт перекрытия загрузок, а подсказки кэша снизили часть задержки. Чистое улучшение — 7% по общему времени выполнения ядра.

Встраиваемый PTX/SASS — инструмент оптимизации последней инстанции. Обеспечивает максимальный контроль ценой сложности. Применяется в последнюю очередь, когда нужна аппаратная функция, недоступная в CUDA C++, — или когда точно определён небольшой фрагмент кода, где планирование компилятора можно улучшить.

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

Встраиваемый PTX/SASS — инструмент последней инстанции. Типичные сценарии: модификаторы кэша, ручная перестановка инструкций, доступ к специальным регистрам.

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

Пример с cp.async.bulk.prefetch.L2.global показывает предвыборку в L2. Ручная настройка даёт ~7% улучшения при уже оптимизированном C++.

• • •

XVIIIИспользование inline PTX в DeepSeek для оптимизации доступа к памяти

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

«Хорошо известный пример пользовательского PTX — из библиотеки DeepEP для экспертного параллелизма DeepSeek. Эта библиотека использовала специальную инструкцию PTX ld.global.nc.l1::no_allocate.l2::256b для оптимизации доступа к глобальной памяти, обходя выделение в кэше L1, сохраняя критичные данные и используя 256-байтовые блоки кэша L2».

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

В главе DeepEP описывается как пример использования недокументированной PTX-инструкции для тонкой настройки кэша на H800. Инструкция ld.global.nc.l1::no_allocate.l2::256b обходит L1, заставляя каждую загрузку извлекать свежие данные из L2 или глобальной памяти. DeepEP также выделяет 20 SM из 132 на H800 для сетевого общения.

Разбор инструкции

Разберём компоненты инструкции ld.global.nc.l1::no_allocate.l2::256b:

  • ld.global — загрузка из глобальной памяти.
  • .nc — некогерентная (non-coherent) загрузка, использующая текстурный кэш, который обеспечивает более высокую пропускную способность для доступа только для чтения.
  • .l1::no_allocate — не выделять данные в кэше L1, чтобы не вытеснять горячие данные, находящиеся там.
  • .l2::256b — извлекать по 256 байт за раз в L2, увеличивая эффективную ширину транзакции.

Обходя L1, загрузки могут потоково передавать большие блоки данных непосредственно в L2 без вытеснения часто используемых данных, находящихся в L1.

Контекст и риски

Эта инструкция не является частью официальной спецификации PTX ISA от NVIDIA (хотя позже было обнаружено, что модификаторы .L1::no_allocate и .L2::256B документированы в PTX ISA). Инженеры DeepSeek обнаружили её «вне документации» для точной настройки поведения кэша на их варианте Hopper GPU H800, ограниченном экспортными правилами США.

Ключевая проблема, которую решает инструкция: стандартная некогерентная загрузка (ld.global.nc) может возвращать устаревшие данные, поскольку текстурный кэш не поддерживает когерентность с глобальной памятью. Добавление .l1::no_allocate обходит L1, заставляя каждую загрузку извлекать свежие данные из L2 или глобальной памяти.

DeepEP также выделяет 20 SM из 132 на H800 исключительно для сетевого общения, чтобы L1-кэш этих SM оставался «тёплым» для сетевого кода. Это снижает вычислительную производительность на ~15%, но значительно повышает пропускную способность связи.

Риск совместимости

Использование PTX таким образом несёт риски: инструкция ld.global.nc.l1::no_allocate.l2::256b не гарантирует стабильность между поколениями GPU. Код, полагающийся на неё, может сломаться или дать некорректные результаты на будущих архитектурах. Настройка DeepEP от DeepSeek включает флаг времени сборки DISABLE_AGGRESSIVE_PTX_INSTRS=1, чтобы отключить эти агрессивные инструкции при возникновении проблем совместимости.

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

DeepEP использует недокументированную PTX-инструкцию ld.global.nc.l1::no_allocate.l2::256b для обхода L1 и потоковой передачи данных в L2. Это даёт значительное ускорение на H800.

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

Риск: инструкция не гарантирует стабильность между поколениями GPU. DeepEP имеет флаг DISABLE_AGGRESSIVE_PTX_INSTRS=1 для отключения.

• • •

XIXПрактический чек-лист оптимизации ядра

Прежде чем переходить к низкоуровневым оптимизациям, убедитесь, что фундамент на месте:

  1. Профилируйте с помощью Nsight Systems для общего обзора и Nsight Compute для детального анализа ядра.
  2. Постройте Roofline — определите, memory-bound или compute-bound ваше ядро.
  3. Коалесцируйте все глобальные загрузки — это базовое требование.
  4. Тайлируйте данные в shared memory для повторного использования.
  5. Сливайте поэлементные операции, чтобы исключить промежуточные записи в HBM.
  6. Увеличьте размер батча для амортизации загрузки весов.
  7. Используйте пониженную точность (TF32, FP16, BF16, FP8, FP4) где возможно.
  8. Задействуйте Tensor Cores через CUTLASS или PyTorch SDPA.
  9. Примените структурированную разреженность 2:4 для инференса после калибровки.
  10. Рассмотрите пересчёт вместо хранения для memory-bound ядер.
  11. Проверьте occupancy — не слишком ли много регистров? Нет ли spilling?
  12. Только потом переходите к встраиваемому PTX/SASS.

Следуя этому систематическому рабочему процессу профилирования, диагностики и применения целевых оптимизаций — от настройки occupancy и уточнений на уровне warp-а до ILP и высокой арифметической интенсивности через тайлинг, слияние и Tensor Cores — можно превратить memory-bound ядро GPU в compute-bound. Это может дать большое ускорение, а в сильно memory-bound задачах иногда и на порядок.

Сначала используется системный профилировщик (например, NVIDIA Nsight Systems), чтобы соотнести активность CPU, GPU-ядра и трафик NVLink/NVSwitch по нескольким GPU. Затем Nsight Compute для глубокого погружения в детали каждого ядра.

Насыщение интерконнектов и пропускной способности памяти произойдёт задолго до того, как ядра смогут полностью использовать вычислительные возможности множества взаимосвязанных GPU (например, 72 в случае NVL72). Поэтому повышение арифметической интенсивности ядер — ключ к эффективному масштабированию для много-GPU задач обучения и инференса.

Итог

Повышение арифметической интенсивности — это систематическая работа: сначала профилирование и Roofline, затем коалесцирование и тайлинг, слияние, понижение точности и Tensor Cores, и лишь в самом конце — ручной PTX/SASS. Каждый уровень даёт свой прирост, и правильный порядок шагов важнее, чем отдельная «серебряная пуля».

• • •

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

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

Подтверждено Прогноз 1: понижение точности даёт кратный рост производительности

«Переход FP16 → FP8 даёт ~2× пропускной способности; FP16 → FP4 — до ~4×. FP4 вдвое уменьшает объём на параметр по сравнению с FP8».

Подтверждено. Blackwell B200 обеспечивает 10 петаFLOPS (плотных) в NVFP4 против ~80 тераFLOPS в FP32 — рост в ~125 раз. B300 даёт 15 петаFLOPS в NVFP4. Переход на FP8 действительно удваивает пропускную способность Tensor Core, а FP4 — учетверяет. Уточнение: реальный выигрыш зависит от чувствительности слоёв к квантованию. Attention и FFN устойчивее к FP8/FP4, чем normalization и softmax, которым нужен FP32.

Подтверждено Прогноз 2: TMEM (256 КБ) снижает давление на регистры

«TMEM — это выделенный буфер SRAM на 256 КБ на каждый SM, который существенно снижает давление на регистровый файл и уменьшает задержку промаха кэша примерно на 58%».

Подтверждено. Архитектура Blackwell действительно переносит аккумуляторы Tensor Core в TMEM, разгружая регистровый файл. Инструкции tcgen05.mma являются CTA-scoped, а не warpgroup-scoped, как wgmma на Hopper. Это меняет модель программирования: CTA действует как единая исполнительная единица, и одни потоки могут оркестрировать вычисления, а другие — перемещать данные.

Подтверждено Прогноз 3: CUTLASS даёт производительность ручного ядра в пределах ~2%

«CUTLASS соответствует или превосходит производительность ручного MMA-ядра в пределах около 2%».

Подтверждено. CUTLASS продолжает развиваться и поддерживать новые архитектуры. Однако для сборки под SM100 требуется суффикс a (sm100a), поскольку tcgen05 относится к «architecture-accelerated features» без гарантий прямой совместимости. Код, скомпилированный для SM100, несовместим с GeForce RTX 50 (SM120).

Подтверждено Прогноз 4: PTX-инструкция DeepSeek не гарантирует стабильность между поколениями

«Инструкция ld.global.nc.l1::no_allocate.l2::256b не гарантирует стабильность между поколениями GPU».

Подтверждено. Этот прогноз остаётся в силе: использование недокументированных PTX-инструкций несёт риски совместимости. DeepEP имеет флаг DISABLE_AGGRESSIVE_PTX_INSTRS=1 для отключения. Рекомендуется использовать документированные модификаторы (evict_last, cache_modifier) там, где это возможно.

Частично подтверждено Прогноз 5: NVL72 требует баланса между вычислениями и коммуникациями

«Насыщение интерконнектов и пропускной способности памяти произойдёт задолго до того, как ядра смогут полностью использовать вычислительные возможности множества взаимосвязанных GPU (например, 72 в случае NVL72)».

Частично подтверждено. NVL72 действительно смещает баланс в сторону коммуникаций. Однако NVLink/NVSwitch внутри стойки настолько быстры (~130 ТБ/с), что для многих нагрузок коммуникации перестают быть узким местом внутри стойки, но остаются таковыми при выходе за её пределы. При планировании заданий важно учитывать топологию: все GPU задания должны по возможности оставаться внутри одного NVLink-домена.

• • •

XXIДополнительные замечания, выходящие за пределы главы

Глава описывает методы оптимизации ядер, но оставляет за скобками несколько аспектов, которые проявились в реальной практике.

Сложность отладки на уровне PTX/SASS

Дополнительные замечания

Глава рекомендует встраиваемый PTX как инструмент последней инстанции, но не описывает сложности отладки. Код на PTX/SASS сложнее поддерживать, он непереносим между архитектурами и требует глубокого понимания аппаратуры. При обновлении GPU-поколения такой код приходится переписывать. Это существенный аргумент в пользу использования CUTLASS и высокоуровневых библиотек, которые автоматически адаптируются к новым архитектурам.

Экосистема вокруг Tensor Cores

Дополнительные замечания

Глава описывает Tensor Cores и CUTLASS, но не упоминает экосистему инструментов: NVIDIA предоставляет cuBLASLt, cuDNN, TensorRT, а также поддержку в PyTorch и Triton. Выбор инструмента зависит от задачи: для исследовательской работы достаточно torch.compile и SDPA; для продакшена — TensorRT или CUTLASS; для нестандартных ядер — Triton или CUDA C++.

Влияние разреженности на обучение

Дополнительные замечания

Глава описывает структурированную разреженность 2:4 для инференса, но не рассматривает её влияние на обучение. На практике разреженность может использоваться и при обучении, но с осторожностью: градиенты не получают выгоды от 2:4, а поддержание разреженности в градиентах сложно. Рекомендуется применять разреженность после обучения, с калибровкой и проверкой точности.

Ограничения SDPA и FlashAttention

Дополнительные замечания

Глава упоминает PyTorch SDPA и FlashAttention, но не описывает их ограничения. FlashAttention требует определённых форм тензоров и типов данных. SDPA может не выбрать FlashAttention для некоторых конфигураций. Рекомендуется проверять, какой бэкенд фактически используется, и при необходимости указывать его явно через torch.nn.attention.sdpa_kernel.

Memory-bound vs compute-bound в контексте NVL72

Дополнительные замечания

Глава формулирует принцип: повышение арифметической интенсивности превращает memory-bound ядра в compute-bound. Однако в контексте NVL72 с его ~130 ТБ/с совокупной пропускной способности NVLink баланс смещается: коммуникации внутри стойки становятся настолько быстрыми, что для многих нагрузок узким местом становятся вычисления, а не память. Это означает, что для NVL72 оптимизация ядер может быть даже важнее, чем для традиционных кластеров.

• • •

XXIIДополнительные выводы

  1. Арифметическая интенсивность — центральная метрика. Она определяет режим работы ядра и направление оптимизации. Модель Roofline даёт визуальную основу для принятия решений.
  2. Тайлинг и слияние — базовые приёмы. Они дают наибольший эффект при минимальных усилиях. Начинать нужно с них.
  3. Понижение точности — множитель производительности. FP8 и FP4 дают кратный рост без изменения алгоритма. Но требуется калибровка и проверка точности.
  4. Tensor Cores и TMEM меняют модель программирования. На Blackwell аккумуляторы живут в TMEM, а инструкции CTA-scoped. Это даёт больше гибкости, но требует понимания новой архитектуры.
  5. CUTLASS — оптимальный выбор для большинства случаев. Он даёт производительность ручного ядра в пределах ~2% без недель настройки.
  6. PTX/SASS — инструмент последней инстанции. Применять только после исчерпания высокоуровневых оптимизаций.
  7. Контекст NVL72 меняет акценты. Внутри стойки коммуникации настолько быстры, что оптимизация ядер может быть даже важнее, чем для традиционных кластеров.
Общий итог обзора

Глава 9 книги Fregly — систематический разбор методов повышения эффективности CUDA-ядер. Ключевая идея: повышение арифметической интенсивности — от тайлинга и слияния до понижения точности и Tensor Cores — превращает memory-bound ядра в compute-bound. Для инженера это означает, что понимание иерархии памяти и принципов работы Tensor Cores важнее, чем знание конкретных цифр текущего поколения. По мере движения к моделям в сотни триллионов параметров и системам вроде NVL72 совместное проектирование железа, софта и алгоритмов перестаёт быть опцией и становится единственным путём вперёд.

Источники: Chris Fregly, «AI Systems Performance Engineering» (O'Reilly, 2026), Глава 9 — цитаты приводятся в ограниченном объёме в целях обзора и критического анализа. NVIDIA CUTLASS documentation; NVIDIA Tensor Memory Accelerator documentation; NVIDIA Transformer Engine documentation; PyTorch documentation; MLPerf Training/Inference v5.0; открытые публикации DeepSeek.

FinCodeCow · AI Infrastructure Review

CUDA Kernel Efficiency and Arithmetic Intensity

A review of Chapter 9 of Chris Fregly's «AI Systems Performance Engineering» (O'Reilly, 2026) — from the Roofline model and multilevel tiling to Tensor Cores, TMEM, TMA, CUTLASS, and fine-tuning with PTX
About the source

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

§Chapter Context

Chapter 9 of Fregly's book is dedicated to methods for improving CUDA kernel efficiency by increasing arithmetic intensity — the ratio of floating-point operations to the volume of data transferred from memory. The material covers both conceptual foundations (the Roofline model) and specific techniques: multilevel tiling, kernel fusion, structured sparsity, precision reduction, Tensor Cores, TMEM, TMA, CUTLASS, and fine-tuning with PTX. Key points of the chapter are presented below, along with clarifications based on current data about the platform and forecast verification.

Quote from the book · Chapter 9

«Even if you fully hide latency with massive parallelism and high ILP, a kernel's performance may still be limited by how much useful work it does per memory access. Arithmetic intensity, also called operational intensity, measures how many floating-point operations are performed per byte of data transferred from memory, or FLOPS per byte».

Exposition of content

In the chapter, arithmetic intensity is presented as the central metric determining whether a kernel operates in memory-bound or compute-bound mode. Even fully hidden latencies don't help if the kernel performs too little computation per byte read: it hits the memory bandwidth ceiling, not the compute ceiling. That is why all subsequent techniques — tiling, fusion, precision reduction, Tensor Cores — aim at one goal: increase the numerator (FLOPS) or decrease the denominator (bytes) in this fraction.

• • •

IArithmetic Intensity and the Roofline Model

Arithmetic intensity (also called operational intensity) is a metric showing how many floating-point operations are performed per byte of data transferred from memory. It is measured in FLOPS per byte:

I = FLOPs / Bytes [FLOP/B]

New GPU generations increase compute power noticeably faster than memory bandwidth. This gap makes improving arithmetic intensity increasingly critical. For example, on NVIDIA H100, peak BF16 performance is ~990 TFLOPS, while HBM bandwidth is ~3.35 TB/s. The ridge point — the ratio of these values — is about 296 FLOP/byte. On Blackwell B200 with NVFP4 support, peak performance reaches 10 petaFLOPS while HBM3e bandwidth is around 8 TB/s, pushing the ridge point even higher. The higher the intensity, the more compute per loaded byte — and the more fully the GPU's compute potential is used.

Arithmetic intensity is a key metric in the Roofline performance model. This model visually displays kernel performance (FLOPs/sec) as a function of arithmetic intensity (FLOPs/byte). The plot shows two hardware "ceilings":

  • Slanted line from the origin — memory bandwidth limit: P = B_peak × I.
  • Horizontal line — compute power limit: P = F_peak.

Achievable performance is the minimum of the two:

P_attainable = min(F_peak, B_peak × I)

This immediately tells you whether the kernel is memory-bound or compute-bound. A kernel with intensity below the ridge point I* = F_peak / B_peak is in the memory-bound regime. A kernel with intensity above I* is compute-bound, using ALU and Tensor Cores close to their peak capabilities.

In practice, Roofline plots can be built with Nsight Compute, which includes a Roofline analysis mode. It lets you check whether a kernel was originally limited by memory or compute, and then track improvements as you optimize. The Speed of Light analysis in Nsight Compute complements Roofline, showing the specific causes of stalls — "Memory Throttle", cache misses, pipeline dependencies.

The goal is always the same: increase arithmetic intensity where possible. This can be achieved in several ways:

  • Tiling with data reuse — loading a submatrix into shared memory where each element participates in dozens or hundreds of operations.
  • Caching in L1/shared memory — keeping intermediate results on-chip.
  • Kernel fusion — so intermediate results are never written to global memory.
  • Reduced-precision data types (FP16, FP8, FP4) — reducing transferred data volume and enabling Tensor Cores.
  • Larger batch size — amortizing weight loading across more compute.
  • Recompute instead of store — when recomputing in registers is cheaper than loading from HBM.

Modern compiler frameworks such as TorchInductor in PyTorch automatically perform some of these optimizations: they keep computations on the GPU, reduce external memory traffic, and increase effective arithmetic intensity.

Not every problem allows you to easily increase arithmetic intensity — that's limited by the algorithm's characteristics. Element-wise operations (ReLU, dropout, bias-add) have intensity around 0.125–0.25 FLOP/byte and are catastrophically memory-bound individually. Matrix multiplications, in contrast, have intensity 2MNK / (MN + NK + MK), which grows with matrix size and easily exceeds the ridge point.

Section I Summary
Key point of the chapter

Arithmetic intensity is measured in FLOP/byte and determines whether a kernel is memory-bound or compute-bound. The ridge point I* = F_peak / B_peak is the boundary between these regimes.

Practical consequence

Nsight Compute builds Roofline plots and shows stall causes. The goal is to increase intensity by all available means: tiling, fusion, precision reduction, larger batches.

Limitation

Element-wise operations are fundamentally memory-bound (0.125–0.25 FLOP/byte). Matrix multiplications grow in intensity with matrix size and easily become compute-bound.

• • •

IIMultilevel Microtiling and Software Prefetching

Quote from the book · Chapter 9

«As discussed in Chapter 7, tiling (aka chunking or blocking) and data reuse are an effective way to raise arithmetic intensity. In that chapter, we showed how loading a small submatrix (tile) of A and B into shared memory lets each byte fetched from global memory be used for many multiply-accumulate operations at static random-access memory (SRAM) speed».

«This intra-SM reuse (register → SMEM → DRAM) reduces the working set at every level—and minimizes off-chip traffic».

Exposition of content

In the chapter, tiling is described as a basic technique for increasing intensity. Loading a T × T tile into shared memory lets each byte read participate in O(T) operations. Multilevel microtiling goes further: after shared memory, data is loaded into registers using vector types (float4, <half2>), and reuse occurs at the register level. The hierarchy "register → shared memory → DRAM" shrinks the working set at each level.

Whenever code is restructured so that each element is loaded once and used dozens or hundreds of times — as in tiling — the FLOPs-per-byte ratio is multiplied by the reuse factor. For example, a typical matrix multiply C = A × B with a T × T tile reduces global reads from O(N) per result to O(N/T) per tile plus O(T) reuse inside shared memory. For a 128 × 128 tile with K-dimension size 64, the arithmetic intensity is:

I = (128 × 128 × 64 × 2) / ((128 × 64 + 64 × 128) × 2) ≈ 64 FLOP/byte

That is orders of magnitude higher than a naive element-wise operation.

Beyond simple shared-memory tiling, you can raise intensity further and strengthen ILP using multilevel tiling. In this approach, after a tile is placed in shared memory, each thread loads microtiles into registers using vector types like float4 and <half2>. Reuse operations are then performed entirely in registers.

You must ensure coalesced global reads when filling shared memory and align/swizzle data in shared memory to avoid bank conflicts. Bank conflicts in shared memory can reduce effective bandwidth by 2–32×, so swizzling (index permutation) is a standard technique for bank-free access.

On modern GPUs, these inner tiling steps are often covered by MMA fragment APIs. Hardware moves data between shared memory and Tensor Memory (TMEM) implicitly using Tensor Core instructions. On Blackwell, tcgen05 instructions implicitly move data between shared memory and TMEM using a separate TMEM address space. TMEM is a dedicated 256 KB SRAM buffer per SM (512 columns × 128 rows × 32 bits) that substantially reduces register-file pressure and reduces cache miss latency by about 58% (from ~1000 cycles on H200 to ~420 cycles on B200).

A closely related technique is software prefetching, often implemented as double buffering. Instead of waiting for the current tile's computation to finish, you can issue async loads of the next tile into shared memory ahead of time. This overlaps DRAM → SMEM transfer with current arithmetic.

With unified memory on a CPU-GPU superchip like Grace Blackwell, you can use cudaMemPrefetchAsync() to hint that a tile will be needed soon. This tells the runtime to migrate pages over NVLink-C2C. However, prefetch is only a hint, not a guarantee. Experienced users often prefer explicit cudaMemcpy or pinned memory allocation to avoid page migration overhead entirely.

Section II Summary
Key point of the chapter

Tiling increases arithmetic intensity proportionally to the reuse factor. Multilevel tiling (register → SMEM → DRAM) shrinks the working set at each level.

Practical consequence

Coalescing global reads and swizzling shared memory are mandatory. Software prefetching (double buffering) overlaps DRAM → SMEM with compute.

Limitation

Prefetching via cudaMemPrefetchAsync on superchips is only a hint. For critical data, explicit cudaMemcpy is preferred.

• • •

IIIIntra-kernel Pipelining and Double Buffering

Quote from the book · Chapter 10

«Intra-kernel pipelining refers to a set of techniques that overlap memory operations and computations within a single kernel execution… Traditionally, GPUs rely on warp-level multithreading to hide latency. While one warp stalls on a memory load, other warps proceed with computation… Intra-kernel pipelining pushes this further by overlapping memory and compute within the same warp or kernel».

Exposition of content

In the chapter, intra-kernel pipelining is described as a development of the latency hiding idea. While GPUs traditionally rely on switching between warps, pipelining overlaps memory and compute within a single warp or kernel. The key tool is the CUDA Pipeline API with a two-stage object cuda::pipeline_shared_state<cuda::thread_scope_block, 2>. Two stages — producer and consumer — overlap loading the next tile with computing on the current one.

Traditionally, GPUs rely on warp-level multithreading to hide latency. While one warp waits for a memory load, other warps continue computing. Intra-kernel pipelining goes further, overlapping memory and compute within a single warp or kernel.

Key CUDA Pipeline API calls:

  • pipe.producer_acquire() — reserves the next pipeline stage for writing.
  • pipe.producer_commit() — signals that previously issued async operations for that stage are ready to be consumed.
  • pipe.consumer_wait() — waits for previously committed operations for that stage.
  • pipe.consumer_release() — releases the current stage for reuse.

Two pipeline stages overlap global memory loads with compute. In the first stage (Stage 0), one warp in the thread block issues an async prefetch of the next tile into shared memory. The prefetch generates cooperative cuda::memcpy_async copies that lower to per-thread cp.async into shared memory.

While Stage 0 produces (loads) data in one warp, other warps of the thread block consume (compute on) loaded data in the second stage (Stage 1). This simple producer–consumer implementation hides DRAM latency behind current computations.

Figure 1. Two-stage producer–consumer pattern

CUDA Pipeline API: producer and consumer overlap

Global memory Producer cuda::memcpy_async producer_acquire / commit Consumer compute tile consumer_wait / release Shared memory (double buffer) tile A tile B
Producer and consumer overlap via cuda::pipeline

The kernel allocates one contiguous region of shared memory for A and B tiles via extern __shared__ float shared_mem[]. It splits this buffer into four sub-regions — two for A and two for B — using pointer arithmetic. This allows true double buffering without additional dynamic allocations.

Before entering the main loop, the kernel asynchronously preloads the first STAGES tiles using async copies tied to the pipeline: producer_acquire() → memcpy_async() → producer_commit(). Consumer warps use consumer_wait() and consumer_release() so that computation starts exactly when a preloaded tile is ready.

Meanwhile, other warps in the block call pipe.consumer_wait(). This effectively suspends only those threads that depend on the current buffer data, until the producer commits it. After the wait, each thread calls the device function computeTile(...) to perform its TILE_SIZE x TILE_SIZE dot product. Results are incrementally accumulated in registers.

After computing on the current buffer, warps call pipe.consumer_release() to release that stage for reuse in subsequent iterations. This fine-grained release avoids blocking the entire block and maximizes overlap between compute and data transfer.

This simple double-buffering scheme is roughly 2× faster than naive tiling, as shown in the table comparing naive tiling and the optimized double-buffered pipeline.

MetricNaive tilingTwo-stage pipeline
Kernel runtime41.3 ms20.5 ms (~2× faster)
SM Active %68%92% (+24%)

Note: numeric values are for illustration of concepts.

Section III Summary
Key point of the chapter

Intra-kernel pipelining overlaps memory and compute within a single kernel. The two-stage producer–consumer pattern via the CUDA Pipeline API gives roughly 2× speedup over naive tiling.

Practical consequence

Fine-grained synchronization (consumer_wait / release) releases only dependent threads, not the whole block. SM Active % grows from 68% to 92%.

Limitation

Double buffering requires careful buffer size management. Excessive stages can reduce occupancy due to increased shared memory per block.

• • •

IVWarp Specialization and the Producer–Consumer Model

Quote from the book · Chapter 10

«Warp specialization extends double buffering by assigning operations to warps that use different hardware, such as data movement (e.g., TMA) and compute (e.g., Tensor Cores). This is in contrast to reusing the same warps for both loading data and computing».

«This type of specialization allows each set of warps to have their own instruction sequences. As such, instructions are issued and executed continuously without being interrupted by other types of operations».

Exposition of content

In the chapter, warp specialization is described as a development of the pipelining idea. While double buffering uses the same warps for both loading and computing, specialization separates roles: some warps (loader) only load data via TMA or cuda::memcpy_async, others (compute) only compute, and still others (storer) only write results. Since each warp has its own scheduler, the GPU can issue load, math, and store instructions from different warps in one cycle.

Figure 2. Non-warp-specialized kernel

Each warp both loads data and computes — causing stalls

Each warp: Load → Compute → Store (no role separation) Shared memory (no separation) buffer A / B / C — competing access Warp 0 Load Compute Store Mixes all roles Warp 1 Load Compute Store Mixes all roles Warp 2 Load Compute Store Mixes all roles Bubbles possible: while a warp loads, compute idles
Without specialization: each warp mixes load and compute

This kind of specialization lets each set of warps have its own instruction stream. As a result, instructions are issued and executed continuously, not interrupted by other operation types. In particular, warp specialization lets you assign one set of "producer" warps to asynchronously prefetch tiles via cuda::memcpy_async. All other "consumer" warps then perform computations.

Figure 3. Warp-specialized kernel

Loader warps deliver data, compute warps do the math

Loader Warps TMA / cuda::memcpy_async warp 0 → tile A warp 1 → tile B warp 2 → tile C Compute Warps Tensor Cores / FMA warp 4 → GEMM warp 5 → GEMM warp 6 → GEMM Shared Memory A B Producer warps write to SMEM → Consumer warps read from there → overlap without block-wide barriers
With specialization: one set of warps loads, another computes

Here four warps are assigned producer roles and the rest consumer roles. As in most producer–consumer patterns, you can assign different numbers of warps to producer and consumer.

Since each warp has its own scheduler, the GPU can issue a load instruction, a math instruction, and a store instruction — all in one cycle from different warps using different schedulers. So an SM with multiple schedulers can issue a memory instruction from warp 0, a math instruction from warp 1, and so on — in one cycle.

This effectively creates block-level multi-issue between warps. This is not possible with double buffering within a single warp, since a single warp's scheduler can issue only one instruction per cycle.

An interesting warp specialization pattern uses three different warp types: "loader", "computer", and "storer". The loader warp puts tiles into the pipeline queue. The computer warp runs the compute kernel on each tile. The storer warp writes results back.

Figure 4. Three-role warp-specialized pipeline

Separate warps for loader, compute, and storer — continuous pipeline

LOADER TMA / cp.async Global → SMEM producer_commit COMPUTE Tensor Cores / FMA consumer_wait produces C STORER writeback SMEM → Global consumer_release No block-wide __syncthreads() — only fine-grained producer/consumer handoffs
Three roles work in parallel, creating a continuous pipeline

This warp-specialized pipeline eliminates stalls that a sequential load-compute loop with single-warp double buffering cannot. Effective overlap of data transfer and compute with warp specialization increases GPU utilization — especially for long loops and persistent kernels.

Another warp specialization pattern is a modification of the three-role pipeline. It assigns a set of warps to the memory loader as before, but then uses two sets of consumer warps that "ping-pong" between computer and storer roles. This three-role warp-specialized architecture is implemented in CUTLASS as gemm_tma_warpspecialized_pingpong.

In practice, warp specialization is extremely effective for squeezing out the last bits of performance. In fact, FlashAttention v3 attributes its speedups partly to warp-specialized pipelines that overlap GEMM and softmax computations — along with data transfers — to keep all hardware blocks busy.

Tip

Use warp specialization for unbalanced workloads or latency hiding — especially when the compute within one warp is insufficient to hide memory load latency. However, if the kernel is small — or extremely memory-bound — a simpler double-buffering scheme may give similar benefits without extra code complexity.

Section IV Summary
Key point of the chapter

Warp specialization separates roles: producer (load), compute (math), storer (writeback). This allows issuing instructions from different warps in one cycle.

Practical consequence

FlashAttention v3 uses warp specialization to overlap GEMM and softmax. CUTLASS provides a ready implementation: gemm_tma_warpspecialized_pingpong.

Limitation

For small or extremely memory-bound kernels, specialization may not justify code complexity — simple double buffering gives a comparable effect.

• • •

VThread Block Clusters and Distributed Shared Memory

Quote from the book · Chapter 10

«A thread block cluster (or cooperative thread array [CTA] cluster), is a hardware-level hierarchy. It grants a subset of SMs to your cooperative grid—and leaves the remainder free for other kernels to use… The GPU provides distributed shared memory (DSMEM) for the thread block cluster to use across those blocks».

«By keeping interblock data inside on-chip memory, DSMEM significantly increases effective arithmetic intensity since blocks can exchange intermediate results or perform reductions without round-tripping to global memory».

Exposition of content

In the chapter, thread block clusters are described as a hardware hierarchy intermediate between the block and the entire grid. Blocks within a cluster are guaranteed to be co-scheduled on the same GPC, which allows them to exchange data via DSMEM — distributed shared memory. This fundamentally changes the class of problems that can be solved: instead of offloading intermediate results to global memory, blocks can exchange them on-die.

On modern GPUs, you can extend the reuse-tiling idea by using thread block clusters from Cooperative Groups. They let multiple thread blocks share data through distributed shared memory (DSMEM).

Thread block clusters directly increase arithmetic intensity. For example, a cluster of four thread blocks can jointly load one tile using the multicast feature of the Tensor Memory Accelerator (TMA). Each tile is shared among the four blocks so that global memory traffic for that tile is amortized across the cluster. Tiles are loaded only once and reused by all four blocks. This can cut global memory traffic by up to 4× in a 2×2 cluster, reducing the denominator — the number of bytes moved from global memory.

Figure 5. Thread block clusters on a GPC

Blocks of one cluster are guaranteed to land on the same GPU Processing Cluster

GPC 0 Cluster 0 CTA 0 CTA 1 Cluster 1 Cluster 2 (8 CTAs) GPC 1 Cluster 3 Cluster 4 All CTAs of a cluster are guaranteed on one GPC → DSMEM and cluster.sync() work
Scheduler guarantee: cluster blocks always on one GPC

Blackwell supports thread block clusters of up to 16 blocks if you enable the non-portable cluster size beyond the standard 8 CTA limit. This requires setting the cudaFuncAttributeNonPortableClusterSizeAllowed attribute for the kernel. Larger clusters can increase reuse but may reduce occupancy, so profile before enabling 16. Also, clusters are co-scheduled within a GPC; if the cluster size doesn't divide the number of SMs per GPC, some SMs idle.

Note that DSMEM lets CTAs in a cluster read shared memory of other CTAs, but those loads are packetized and their bandwidth is noticeably lower than local SMEM (around 128 bytes/cycle), so remote DSMEM should not be treated as local shared memory. For real inter-CTA throughput, use cp.async.bulk.

Thread block clusters open new strategies for parallelizing workloads that previously required global memory communication or multiple kernel launches. For example, imagine a large matrix multiplication where the output tile is too large for one thread block to handle due to shared memory constraints.

With thread block clusters and DSMEM, those two blocks can form a cluster and directly share a common on-chip shared memory region to seamlessly combine their results with hardware-supported primitives. DSMEM hardware lets one SM perform loads/stores/atomics into another SM's shared memory through a fast network.

Section V Summary
Key point of the chapter

Clusters are a hardware hierarchy intermediate between the block and the grid. DSMEM lets blocks exchange data on-die, reducing global memory traffic by up to 4×.

Practical consequence

TMA multicast loads a tile once and distributes it to all blocks in the cluster. Blackwell supports up to 16 blocks per cluster when explicitly allowed.

Limitation

Remote DSMEM is packetized and has bandwidth around 128 bytes/cycle. For real inter-CTA throughput, use cp.async.bulk.

• • •

VIKernel Fusion

Quote from the book · Chapter 9

«Another way to increase arithmetic intensity is to fuse multiple operations—or loop iterations—into one operation. By fusing multiple kernels together, the data loaded from memory can be used for several computations and iterations before being written back».

Exposition of content

In the chapter, kernel fusion is presented as a way to eliminate intermediate writes to global memory. Element-wise operations combined into a single kernel keep intermediate values in registers. The sin and sqrt example shows intensity growing from 0.083 to 0.25 FLOP/byte — a 3× gain. Additional micro-optimizations include replacing division with rsqrtf and broadcasting the scalar result through shared memory.

Another way to increase arithmetic intensity is to merge multiple operations or loop iterations into a single operation. By fusing several kernels together, data loaded from memory can be used for multiple computations and iterations before writing back.

Element-wise operations (also called pointwise) apply a simple computation independently to each tensor element. Fusing such operations eliminates excess memory traffic by keeping intermediate values on-chip. This increases work per byte read from global memory — raising arithmetic intensity.

For example, a naive implementation launches two kernels. The first reads x and writes y to global memory. The second reads y and writes z:

y = sin(x);
z = sqrt(y);

Here each element is processed twice: once after sin(x) and once after sqrt(y). Arithmetic intensity of each kernel is very low, since each element has just one math function per load/store. In contrast, a fused kernel performs the same operations in one pass:

z[i] = sqrt(sin(x[i]));

Each x[i] is loaded once, then both sin and sqrt are applied in registers, and only the final z[i] is written to memory. Since the intermediate y never goes to global memory, effective FLOPS per byte rises sharply, bringing the operation closer to the compute ceiling.

Vertical fusion combines sequential operations on the same data (as in the example above), while horizontal fusion combines parallel operations over data (e.g., bias-add and activation after matrix multiplication, performed in the GEMM epilogue). Both types of fusion increase arithmetic intensity.

Example: L2 normalization

Consider a concrete example. Suppose we need to L2-normalize each row of length hidden in a 2D tensor x (shape [batch, hidden]). For each row b, compute the norm norm_b = sqrt(sum_i x[b,i]*x[b,i] + eps), then write y[b,i] = x[b,i] / norm_b for all i.

A naive implementation would consist of three separate kernels: square, reduce, divide. Each kernel performs about 1 FLOP per element and requires 12 bytes of traffic per element. That gives intensity 3/36 ≈ 0.083 FLOP/byte.

Instead, you can fuse all three steps into one kernel:

__global__ void fusedL2Norm(const float* __restrict__ x,
                            float* __restrict__ y,
                            int hidden) {
  extern __shared__ float sdata[];
  const int batch = blockIdx.x;
  const int tid = threadIdx.x;
  const float* batch_ptr = x + size_t(batch) * hidden;
  float local = 0.f;
  for (int i = tid; i < hidden; i += blockDim.x) {
    float v = batch_ptr[i];
    local = fmaf(v, v, local);
  }
  sdata[tid] = local;
  __syncthreads();
  for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1) {
    if (tid < offset) sdata[tid] += sdata[tid + offset];
    __syncthreads();
  }
  float inv = rsqrtf(sdata[0]);
  float* out_batch = y + size_t(batch) * hidden;
  for (int i = tid; i < hidden; i += blockDim.x) {
    out_batch[i] = batch_ptr[i] * inv;
  }
}

In this fused kernel, each thread walks its slice x[b,*] twice: once for accumulating the sum of squares and once for writing normalized outputs. Global traffic is about 12 bytes per element. Per element, the kernel does roughly 1 multiply + 1 add during reduction and 1 multiply during normalization — about 3 FLOP. sqrt and rsqrt are amortized over the whole row.

Fused kernel arithmetic intensity: 3 / 12 ≈ 0.25 FLOP/byte — 3× higher than the naive version.

Additional micro-optimizations: replace division with a multiply by rsqrtf; perform one rsqrtf in a single thread and broadcast the scalar result to other threads through shared memory.

Fusion not only increases arithmetic intensity and shifts the kernel toward compute-bound on the Roofline, but also saves memory bandwidth. However, overly aggressive fusion can increase register pressure and reduce occupancy. If register usage becomes excessive and spilling to local memory begins, the benefit of fusion may be offset by extra memory traffic.

When fusion hurts: if the operations being merged have radically different optimal tile sizes, different memory requirements, or if one operation is compute-bound and the other is memory-bound with a completely different access pattern, fusing them may hurt performance.

Section VI Summary
Key point of the chapter

Kernel fusion eliminates intermediate writes to global memory. The sin and sqrt example raises intensity from 0.083 to 0.25 FLOP/byte.

Practical consequence

Vertical fusion combines sequential operations, horizontal fusion combines parallel ones (e.g., GEMM epilogue). rsqrtf instead of division and broadcast through shared memory give additional gains.

Limitation

Overly aggressive fusion leads to register spilling and reduced occupancy. Profile with Nsight Compute.

• • •

VIIStructured Sparsity

Quote from the book · Chapter 9

«On modern GPUs, 2:4 structured sparsity is accelerated in hardware by Sparse Tensor Cores and cuSPARSELt. 2:4 means that exactly two out of every four consecutive weights are nonzero. Creating this type of sparsity is sometimes called pruning».

«By pruning half of the weights into a 2:4 pattern, each memory load now delivers twice as many nonzero values that actually participate in multiplication».

Exposition of content

In the chapter, 2:4 structured sparsity is described as a hardware-accelerated scheme where exactly two of every four consecutive weights are non-zero. This gives almost 2× reduction in global memory reads and an increase in effective arithmetic intensity. In PyTorch, to_sparse_semi_structured() is used. The speedup is most noticeable with large batches, where index handling overhead is amortized.

On modern GPUs, 2:4 sparsity is accelerated in hardware by Sparse Tensor Cores and cuSPARSELt. The 2:4 pattern means exactly two of every four consecutive weights are non-zero. Creating such sparsity is sometimes called pruning.

By pruning half the weights in a 2:4 pattern, each memory load now delivers twice as many non-zero values that actually participate in multiplication. This immediately cuts global memory reads and increases effective arithmetic intensity by almost 2×.

Structured sparsity is applied after model training. The model is pruned and optimized for inference. In PyTorch, use to_sparse_semi_structured() to convert trained dense modules to the 2:4 format before deploying sparse GEMMs on Sparse Tensor Cores.

After conversion, the model calls optimized sparse GEMM kernels running on Sparse Tensor Cores. Sparse Tensor Cores can approach 2× speedup over dense counterparts for many inference tasks — especially with large input batches, since that amortizes the overhead of launching kernels and processing the compressed representation's indices.

Limitations of 2:4 sparsity:

  • Real speedup heavily depends on matrix structure, pattern density, and index storage overhead. For small matrices, sparsity may even slow execution.
  • Speedup assumes model accuracy is preserved. In practice, it is important to fine-tune or carefully calibrate the model after pruning.
  • Structured sparsity is usually applied to inference. During training, gradients do not benefit from 2:4 sparsity.
  • Before applying sparsity, it's important to first implement fundamental optimizations: coalesce all global loads, reuse data via tiling, and combine element-wise operations.
Section VII Summary
Key point of the chapter

2:4 sparsity is hardware-accelerated by Sparse Tensor Cores and gives almost 2× reduction in global reads and growth in effective arithmetic intensity.

Practical consequence

PyTorch uses to_sparse_semi_structured(). The greatest gain is with large inference batches, where overheads are amortized.

Limitation

For small matrices, sparsity may slow execution. Calibration and accuracy validation are required. During training, gradients do not benefit.

• • •

VIIIRecompute vs. Store Trade-off

Quote from the book · Chapter 9

«Also, instead of storing or loading precomputed values (e.g., x²), consider recomputing them on demand if memory bandwidth is the bottleneck. For instance, repeatedly computing x*x in registers is often faster than loading a previously computed x² from global memory».

Exposition of content

In the chapter, recompute is described as a way to trade compute resources for memory bandwidth. The classic example is FlashAttention, which recomputes softmax statistics instead of storing the full N × N matrix. This turns O(N²) HBM accesses into O(N² / T). The decision to recompute or store depends on the compute-to-memory-bandwidth ratio on the specific GPU.

Instead of storing or loading precomputed values (e.g., x²), consider recomputing them on demand if memory bandwidth is the bottleneck. For example, recomputing x*x in registers is often faster than loading a previously computed x² from global memory.

Many LLM inference engines use this technique to save memory. Instead of storing large activation tensors in HBM and reading them back later, they can recompute certain layers and activations on the fly. This is analogous to activation checkpointing in model training.

The classic example is FlashAttention, which recomputes softmax statistics in the forward pass instead of storing the full N × N attention matrix in HBM. This trades a few extra FLOPS for a dramatic reduction in global memory access, turning O(N²) HBM accesses into O(N² / T), where T is the tile size.

Recompute improves effective FLOPS/byte and allows larger models to fit in less memory. The decision to recompute or store depends on the compute-to-memory-bandwidth ratio on the specific GPU: if a kernel is memory-bound, recompute is almost always beneficial; if compute-bound, storing may be preferable.

Section VIII Summary
Key point of the chapter

Recompute trades computation for memory bandwidth. FlashAttention is the classic example: O(N²) HBM accesses become O(N² / T).

Practical consequence

For memory-bound kernels, recompute is almost always beneficial. For compute-bound, storing may be preferable.

• • •

IXPyTorch and Arithmetic Intensity

Quote from the book · Chapter 9

«In PyTorch, many of these ideas are automatically applied… The PyTorch compiler can automatically fuse chains of elementwise operations—and even some reductions. It uses execution-graph-level optimizations to keep data on the GPU and reuse it as much as possible».

Exposition of content

In the chapter, it is emphasized that PyTorch automatically applies many optimizations: tiling, element-wise fusion, use of cuDNN and cuBLAS. SDPA can route computation to FlashAttention, memory-efficient, or cuDNN backends. Nested tensors (NestedTensor) allow representing variable-length batches without padding, improving the efficiency of global loads.

In PyTorch, many of these ideas are applied automatically. The PyTorch compiler (torch.compile) can automatically fuse chains of element-wise operations and even some reductions. It uses execution-graph-level optimizations to keep data on the GPU and maximize its reuse.

Because it uses optimized libraries such as cuDNN and cuBLAS under the hood, PyTorch performs tiling and uses shared memory for you during matrix operations via torch.matmul. In addition, scaled_dot_product_attention (SDPA) in PyTorch can route computation to FlashAttention, memory-efficient, or cuDNN backends depending on tensor shapes and data types:

torch.nn.attention.sdpa_kernel(SDPBackend.FLASH_ATTENTION)

Note that while PyTorch can recognize and compile most operations, some non-standard operations or custom CUDA ops may not fuse. In such cases, manual optimization may be needed.

Nested tensors (ragged tensors) allow representing batches of variable-length inputs without padding. Each nested tensor packs variable-length sequences into a single efficient buffer. NestedTensor provides a normal tensor interface but eliminates unnecessary zero-padding. As a result, global memory loads become more efficient, because every loaded byte is useful for computation.

Nested tensors are useful for LLMs with variable-length sequences. When using nested tensors, operations like attention and batch matrix multiplies fetch only the necessary data from memory. This brings the kernel closer to compute-bound on the Roofline and helps reduce stalls related to memory limitation.

Section IX Summary
Key point of the chapter

PyTorch automatically applies tiling, fusion, and optimized libraries. SDPA chooses the best backend for attention.

Practical consequence

Nested tensors (NestedTensor) eliminate zero-padding for variable-length sequences, improving load efficiency.

Limitation

Non-standard and custom operations may not fuse. Manual optimization is required.

• • •

XMixed Precision and Tensor Cores

Quote from the book · Chapter 9

«Modern NVIDIA GPUs implement reduced-precision computations like TF32, FP16, FP8, FP4, and INT8 in Tensor Cores. Each SM in Blackwell has a 256 KB on-chip TMEM dedicated to Tensor Core data».

«When used correctly, these features can transform a once memory-bound, tensor-heavy kernel into a fully compute-bound one by raising arithmetic intensity (FLOPS per byte) by 2×, 4×, or even 8×».

Exposition of content

In the chapter, mixed precision and Tensor Cores are described as ways to radically increase arithmetic intensity. Lowering precision reduces transferred data volume, while Tensor Cores perform matrix operations at high throughput. Blackwell adds TMEM — 256 KB SRAM per SM for Tensor Core accumulators, reducing register file pressure and changing the programming model compared to Hopper.

Modern NVIDIA GPUs implement reduced-precision computation such as TF32, FP16, FP8, FP4 and INT8 in Tensor Cores. Each SM in Blackwell has 256 KB of on-chip Tensor Memory (TMEM) dedicated to Tensor Core data. There is also a specialized TMA unit that asynchronously copies tiles between global memory and shared memory. Tensor Core instructions (e.g., tcgen05.mma) then implicitly move operands and accumulators between shared memory and TMEM.

The key architectural difference between Blackwell and Hopper: on Hopper, MMA results were tightly coupled to the warpgroup register file, while on Blackwell accumulators live in TMEM, decoupling result ownership from individual threads. tcgen05 instructions are CTA-scoped — one thread issues an instruction on behalf of the entire CTA, unlike warpgroup-scoped wgmma on Hopper. This gives more scheduling flexibility: some threads can orchestrate computation, others can move data, and the whole CTA acts as an execution unit.

Used properly, these capabilities can turn a once memory-bound tensor-intensive kernel into a fully compute-bound one, boosting arithmetic intensity (FLOPS per byte) by 2×, 4×, or even 8×.

Section X Summary
Key point of the chapter

Tensor Cores and reduced precision can boost arithmetic intensity 2–8×. TMEM (256 KB per SM) stores accumulators, offloading registers.

Practical consequence

Key difference from Hopper: tcgen05 is CTA-scoped, not warpgroup-scoped. The CTA acts as a single execution unit.

• • •

XIFeeding Tensor Cores via TMEM and TMA

Quote from the book · Chapter 9

«At the heart of high-throughput tensor computation is TMEM, a 256 KB SRAM buffer per SM. At a high level, programmers do not explicitly allocate or manage TMEM, however. TMEM is handled by the hardware or libraries when you use Tensor Core operations».

Exposition of content

In the chapter, TMEM is described as a key architectural element of Blackwell. 256 KB SRAM per SM serves as a buffer for Tensor Core accumulators. Programmers do not manage TMEM explicitly — libraries and hardware do. TMA moves data between global memory and shared memory. CUTLASS and Transformer Engine handle all this complexity automatically.

At the heart of high-performance tensor computation lies TMEM — a 256 KB SRAM buffer per SM. Programmers do not explicitly allocate or manage TMEM — TMEM is handled by hardware or libraries when using Tensor Core operations.

Under the hood, Blackwell uses tcgen05.mma instructions that work with TMEM for storing operands and accumulators. CUTLASS and library kernels manage the necessary allocation and use through kernel configuration and PTX assembly. Transformer Engine uses TMEM for intermediate results, reducing MMA dependencies on registers.

High-level APIs such as CUTLASS handle all this complexity automatically. When you launch a Tensor Core MMA operation using CUDA MMA intrinsics or CUTLASS GEMM, the implementation manages operand movement through shared memory and TMEM. TMA streams tiles between global memory and shared memory, and Tensor Core instructions implicitly move operands between shared memory and TMEM.

To transfer data from global memory to shared memory, use TMA or cuda::memcpy_async with the CUDA Pipeline API (<cuda/pipeline>). Example of a two-stage double-buffered pipeline:

#include <cuda/pipeline>
#include <cooperative_groups.h>
namespace cg = cooperative_groups;
extern "C" __global__
void stage_ab_tiles(const float* __restrict__ globalA,
                    const float* __restrict__ globalB,
                    float* __restrict__ outC,
                    int tile_elems,
                    int num_tiles) {
  assert((tile_elems % (32 * 4)) == 0);
  extern __shared__ float smem[];
  auto block = cg::this_thread_block();
  float* A0 = smem + 0 * tile_elems;
  float* A1 = smem + 1 * tile_elems;
  float* B0 = smem + 2 * tile_elems;
  float* B1 = smem + 3 * tile_elems;
  constexpr auto scope = cuda::thread_scope_block;
  constexpr int stages = 2;
  __shared__ cuda::pipeline_shared_state<scope, stages> pstate;
  auto pipe = cuda::make_pipeline(block, &pstate);
  pipe.producer_acquire();
  cuda::memcpy_async(block, A0, globalA + 0 * tile_elems,
                     cuda::aligned_size_t<32>{tile_elems * sizeof(float)}, pipe);
  cuda::memcpy_async(block, B0, globalB + 0 * tile_elems,
                     cuda::aligned_size_t<32>{tile_elems * sizeof(float)}, pipe);
  pipe.producer_commit();
  for (int t = 1; t < num_tiles; ++t) {
    pipe.producer_acquire();
    cuda::memcpy_async(block, (t & 1) ? A1 : A0,
                       globalA + t * tile_elems,
                       cuda::aligned_size_t<32>{tile_elems*sizeof(float)}, pipe);
    cuda::memcpy_async(block, (t & 1) ? B1 : B0,
                       globalB + t * tile_elems,
                       cuda::aligned_size_t<32>{tile_elems*sizeof(float)}, pipe);
    pipe.producer_commit();
    pipe.consumer_wait();
    float* prevA = (t & 1) ? A0 : A1;
    float* prevB = (t & 1) ? B0 : B1;
    pipe.consumer_release();
  }
  pipe.consumer_wait();
  int last = (num_tiles - 1) & 1;
  float* lastA = last ? A1 : A0;
  float* lastB = last ? B1 : B0;
  pipe.consumer_release();
}

When launching this kernel, set the dynamic shared memory size to 4 * tile_elems * sizeof(float) to allocate A0, A1, B0, and B1 in shared memory. This double-buffering pattern ensures that as soon as one tile is in shared memory, Tensor Cores can start processing it. Meanwhile, cuda::memcpy_async loads the next tile into shared memory in parallel.

Choosing between TMA and cp.async (LDGSTS): TMA is optimized for large regular blocks and reaches peak throughput of ~7.2 TB/s but requires a large data volume in the pipeline (typically >64 KiB in-flight) to saturate. cp.async (LDGSTS) is better suited for irregular patterns and smaller volumes (<32 KiB in-flight), saturating at ~6.6 TB/s with ~32 KiB in-flight. Choose the copy path by the access pattern, not by ideology.

Figure 6. Block-wide barrier blocks all threads

Until all threads reach __syncthreads(), nobody continues

Threads (warps) Warp 0 Warp 1 Warp 2 Warp 3 ✓ __syncthreads() After the barrier Nobody passed until all arrive Block-wide barrier = idle warps. Use pipeline API for fine-grained sync.
Block-level barrier stalls all warps, even ready ones
Section XI Summary
Key point of the chapter

TMEM is 256 KB SRAM per SM for Tensor Core accumulators. TMA moves tiles between global memory and shared memory. CUDA Pipeline API manages double buffering.

Practical consequence

TMA is optimized for large regular blocks (>64 KiB in-flight). cp.async (LDGSTS) is for irregular and smaller volumes (<32 KiB in-flight).

• • •

XIITF32 and Automatic Mixed Precision

Quote from the book · Chapter 9

«While Tensor Cores were originally designed for FP16, they also support TF32, which sits between FP32 and FP16. TF32 uses an 8-bit exponent like FP32 and a 10-bit mantissa like FP16».

Exposition of content

In the chapter, TF32 is described as an easy way to speed up FP32 computations without code changes: just set torch.set_float32_matmul_precision('high'). Automatic mixed precision (AMP) extends this idea by selecting the optimal precision per layer. BF16 is preferable to FP16 due to its wider exponent range.

Although Tensor Cores were originally designed for FP16, they also support TF32, which sits between FP32 and FP16. TF32 uses an 8-bit exponent like FP32 and a 10-bit mantissa like FP16. TF32 runs on Tensor Cores at substantially higher throughput than FP32 on CUDA cores, while preserving FP32's exponent range. In PyTorch, enabling TF32 is as simple as:

import torch
torch.set_float32_matmul_precision('high')

Once these flags are set, high-level operations like torch.matmul and torch.nn.Linear automatically execute as TF32 Tensor Core kernels rather than FP32 on standard CUDA cores.

Beyond TF32, automatic mixed precision (AMP) in PyTorch can choose the optimal precision (FP16 or BF16) for each operation and accumulate results in FP32 for stability. BF16 helps avoid overflow issues inherent to FP16:

with torch.amp.autocast("cuda", dtype=torch.bfloat16):
    output = model(input)

Under the hood, TorchInductor automatically fuses these precision conversions to ensure: large GEMMs run on Tensor Cores in FP16 or TF32, accumulation remains in FP32 for numerical stability, small "sensitive" kernels like layer normalization and softmax run in FP32, and GradScaler prevents underflow during FP16 training. BF16 has FP32's exponent range, so GradScaler is typically unnecessary with BF16.

When using structured sparsity or extremely low precision (FP8/FP4), make sure the batch size or tile granularity is large enough that TMEM and Tensor Cores stay fully loaded. Small batches carry overhead, including format conversions, sparse index handling, irregular memory patterns. A batch size of 128 or 256 fully engages the TMEM pipeline and delivers near-peak performance.

Section XII Summary
Key point of the chapter

TF32 is enabled with one line and speeds up FP32 computations. AMP selects precision per layer. BF16 is preferable to FP16 due to exponent range.

Practical consequence

For FP8/FP4 and sparsity, large batches (128–256) are needed to fully load TMEM and Tensor Cores.

• • •

XIIIBF16/FP16, FP8, and FP4 Reduced Precision

Quote from the book · Chapter 9

«BF16/FP16 (half-precision) has been supported for many GPU generations, but Tensor Cores on modern GPUs can often sustain greater than 90% of the BF16/FP16 peak throughput, around 4× the FP32 peak throughput».

«To push throughput even higher, you can use FP8. By reducing 16-bit weights by 50% down to 8 bits, you cut memory traffic in half—and double the number of weights loaded per HBM transaction».

Exposition of content

In the chapter, precision reduction is described as a performance multiplier: FP8 gives 2× over FP16, FP4 up to 4×. NVFP4 with micro-scaling preserves accuracy at 4-bit weights. B200 delivers 10 petaFLOPS (dense) in NVFP4; B300 — 15 petaFLOPS.

BF16/FP16 has been supported for several GPU generations, but Tensor Cores on modern GPUs often deliver more than 90% of peak BF16/FP16 throughput, about 4× FP32 peak.

FP16 training uses a narrower 5-bit exponent than FP32, so very small gradient values can vanish to zero (underflow) without loss scaling. BF16 matches FP32's 8-bit exponent range and inherently avoids underflow — loss scaling is rarely needed. BF16 is usually preferred for training on modern GPUs.

To push throughput higher, use FP8. By shrinking 16-bit weights 50% down to 8 bits, you halve memory traffic — and double the number of weights loaded per HBM transaction. In practice, FP8 matrix multiplies with FP32 or TF32 accumulation reach 2–3× higher TFLOPS than BF16/FP16.

To address precision issues at very low precision, Transformer Engine supports FP8 as well as NVIDIA's 4-bit NVFP4 format with micro-scaling. NVFP4 applies two-level scaling, combining microblock-level scaling (every 32 elements along the K-dimension share a scale factor in E8M0 format) with a higher-level scale. Blackwell B200 delivers 10 petaFLOPS (dense) in NVFP4, while FP32 peak is around 80 teraFLOPS (dense). Blackwell B300 (Ultra) — 15 petaFLOPS (dense) in NVFP4.

Layer sensitivity to quantization: in practice, different transformer layers have different sensitivity to precision reduction. Attention (especially QK multiplication) and FFN layers are usually more resilient to FP8/FP4 than normalization and softmax, which require FP32 for stability. Calibrating the model by sweeping precision across layers and measuring quality metrics (perplexity, accuracy) for each variant is recommended.

Because precision is so low, the 256 KB TMEM per SM can hold large FP4 tiles (e.g., 256×256), further increasing on-chip reuse and boosting performance.

Every precision reduction doubles or quadruples the number of operations per byte and thus increases arithmetic intensity. When TMEM/TMA overlap memory and compute, these low-precision formats turn previously memory-bound kernels into fully compute-bound ones, fully engaging Tensor Core compute engines.

Section XIII Summary
Key point of the chapter

FP8 gives ~2× over FP16, FP4 up to 4×. NVFP4 with micro-scaling preserves accuracy at 4-bit weights. B200 — 10 petaFLOPS, B300 — 15 petaFLOPS in NVFP4.

Practical consequence

Layer sensitivity to quantization varies. Attention and FFN are more resilient to FP8/FP4 than normalization and softmax, which need FP32.

• • •

XIVINT8 Reduced Precision and DP4A Instructions for Inference

Quote from the book · Chapter 9

«LLM inference use cases can typically tolerate reduced-precision INT8 quantization supported by modern GPUs using DP4A (SIMD dot-product) instructions on regular CUDA cores and integer matrix-multiply/accumulate (MMA) instructions on Tensor Cores».

Exposition of content

In the chapter, INT8 is described as a way to cut weight memory traffic by 75% and increase Tensor Core throughput. DP4A performs four INT8 MAC per instruction. In PyTorch, INT8 support is provided through TorchAO and vendor backends.

LLM inference tasks can typically tolerate quantization to reduced INT8 precision, supported by modern GPUs with DP4A (SIMD dot-product) instructions on regular CUDA cores and integer matrix-multiply-accumulate (MMA) instructions on Tensor Cores. At the instruction level, DP4A performs four INT8 multiply-accumulate (MAC) operations per instruction, versus one FP32 FMA per instruction.

Because INT8 weight traffic is just one byte per element instead of four bytes for FP32, memory traffic for weights is cut by 75%. INT8 inference can significantly outperform FP32 thanks to higher INT8 Tensor Core peak throughput and reduced memory traffic — each GPU can process roughly 4× more data per second from memory when using INT8 weights.

At the time of writing, INT8 quantization support in PyTorch is provided through TorchAO and vendor backends. Quantized modules run using dedicated INT8 kernels. Using cuBLASLt or CUTLASS for low-level INT8 GEMM ensures Tensor Core engagement.

Section XIV Summary
Key point of the chapter

INT8 cuts weight traffic by 75% and uses DP4A on CUDA cores or INT8 MMA on Tensor Cores.

Practical consequence

TorchAO and cuBLASLt/CUTLASS ensure INT8 Tensor Core engagement.

• • •

XVTransformer Engine and TMEM in Detail

Quote from the book · Chapter 9

«Modern NVIDIA GPUs include a Transformer Engine that combines Tensor Core hardware support for low-precision formats with a software runtime for scaling and casting… The Transformer Engine workflow is straightforward. First, your kernel issues a MMA call or launches a CUTLASS GEMM. Next, the Transformer Engine's firmware arranges for TMA (or cuda::memcpy_async) to copy weights and activations from HBM into shared memory».

Exposition of content

In the chapter, Transformer Engine is described as a combination of hardware support for low precision and a software runtime. TMEM is 256 KB SRAM per SM where Tensor Core results are stored. tcgen05.mma instructions move operands between shared memory and TMEM. The UMMA constraint on Blackwell: operand A can be from TMEM or SMEM, B only from SMEM.

Modern NVIDIA GPUs include Transformer Engine, which combines hardware Tensor Core support for reduced-precision formats with a software runtime for scaling and casting. Kernels in cuBLASLt, cuDNN, CUTLASS, or OpenAI Triton issue cp.async or TMA transfers into shared memory. Tensor Core instructions then implicitly move operands between shared memory and TMEM.

TMEM is a 256 KB SRAM buffer per SM that Transformer Engine and Tensor Cores use for storing results (instead of registers). In practice, you never explicitly allocate TMEM — it's handled by hardware.

Transformer Engine workflow:

  1. Your kernel issues an MMA call or launches a CUTLASS GEMM.
  2. Transformer Engine firmware orchestrates TMA (or cuda::memcpy_async) copies of weights and activations from HBM to shared memory.
  3. Tensor Core instructions (e.g., tcgen05.mma) implicitly move operands between SMEM and TMEM during the MMA pipeline.
  4. Partial sums accumulate in TMEM at higher precision (BF16, FP16, FP32).
  5. Results are written from TMEM to shared or global memory coalesced.

Ideally, weights are in FP8 or FP4, and activations are cast to FP8/FP4 where possible — otherwise activations may remain FP16/FP32. Tensor Core MMA operations run at low precision, e.g. FP8 × FP8 with higher-precision accumulation, or FP16 × FP16 with FP32 accumulation.

If you're building your own tile loop, you can overlap data movement with Tensor Core compute using cuda::memcpy_async and the CUDA Pipeline API. While Tensor Cores process the current tile, cuda::memcpy_async streams the next tile from HBM to shared memory. This overlap helps hide memory latency and keep Tensor Cores busy.

UMMA constraint on Blackwell: operand A may come from TMEM or SMEM, operand B only from SMEM. A from TMEM and B from TMEM is not supported. This constraint affects kernel structure and requires planning data placement.

Section XV Summary
Key point of the chapter

Transformer Engine manages low precision automatically. TMEM (256 KB) stores accumulators. TMA moves tiles between HBM and SMEM.

Practical consequence

UMMA constraint: A can be from TMEM or SMEM, B only from SMEM.

• • •

XVIUsing CUTLASS for Optimal Arithmetic Intensity

Quote from the book · Chapter 9

«With CUTLASS, you write a single templated call, and it will automatically apply many advanced optimizations: shared-memory tiling, asynchronous memory transfers, and double buffering with the help of TMEM's 256 KB per-SM buffer».

Exposition of content

In the chapter, CUTLASS is described as a way to get manual MMA kernel performance without weeks of tuning. CUTLASS automatically applies tiling, double buffering, warp specialization, thread block clusters, and TMA multicast. CUTLASS performance is within ~2% of a manual kernel.

One of the easiest ways to engage the optimizations described above is to use the NVIDIA CUTLASS library. With CUTLASS you write a single template call, and it automatically applies many advanced optimizations: shared-memory tiling, asynchronous memory transfers, double buffering using the 256 KB TMEM buffer per SM, warp specialization, thread block clusters, and TMA multicast with distributed shared memory.

For example, suppose you want to compute GEMM C = A * B with half-precision inputs and half-precision output, accumulating in FP16 or FP32:

#include <cutlass/numeric_types.h>
#include <cutlass/gemm/device/gemm.h>
using Gemm = cutlass::gemm::device::Gemm<
    cutlass::half_t,
    cutlass::layout::RowMajor,
    cutlass::half_t,
    cutlass::layout::ColumnMajor,
    cutlass::half_t,
    cutlass::layout::RowMajor,
    float,
    cutlass::arch::OpClassTensorOp,
    cutlass::arch::Sm100
>;
Gemm gemm_op;
cutlass::Status status = gemm_op(
    { M, N, K },
    float(1.0f),
    A_d, lda,
    B_d, ldb,
    float(0.0f),
    C_d, ldc
);

When compiling and running this code, CUTLASS automatically selects tiles, places them in shared memory, generates asynchronous memory copies, uses tcgen05.mma Tensor Core instructions, accumulates partial sums at the specified precision, writes results from TMEM coalesced, and uses thread block clusters when beneficial.

MetricManual MMA kernelCUTLASS GEMM
Tensor Core utilization~98%~98%
Registers per thread~52~60 (slightly higher)
Shared memory per CTA~2 KB~4 KB
Development effortHighLow (template configuration)

CUTLASS matches or exceeds manual MMA kernel performance within about 2%. Small increases in register and shared memory usage are because CUTLASS generalizes the kernel for flexibility. A few lines of template code replace weeks of low-level tuning.

CUTLASS templates support FP4, FP8, FP16, TF32, and INT8 operand types. To build for a specific architecture, you must specify the target with an a suffix (e.g., sm100a for Blackwell), because tcgen05 instructions fall under "architecture-accelerated features" without forward compatibility guarantees. Code compiled for SM100 is incompatible with GeForce RTX 50 (SM120).

Optimized libraries like cuBLAS are built on CUTLASS. High-level libraries like PyTorch call these optimized libraries for many kernels. As more PyTorch operators use CUTLASS under the hood, you can benefit from the same optimizations without writing a single line of CUDA C++.

Section XVI Summary
Key point of the chapter

CUTLASS delivers manual MMA kernel performance within ~2% with minimal effort. It automatically applies tiling, double buffering, warp specialization, clusters, and TMA multicast.

Practical consequence

For SM100 builds, use the a suffix (sm100a). Code is incompatible with SM120.

• • •

XVIIInline PTX and SASS Fine-Tuning for Micro-Optimizations

Quote from the book · Chapter 9

«For those willing to venture beyond C++ and into low-level microoptimizations, CUDA allows inline Parallel Thread Execution (PTX) code and SASS (NVIDIA's assembly language) to bring out the last bits of performance that may be left on the table».

Exposition of content

In the chapter, inline PTX is described as a tool of last resort. Typical scenarios: using an instruction with no direct CUDA intrinsic, cache modifiers, manual instruction reordering, reading special registers. The example with cp.async.bulk.prefetch.L2.global demonstrates L2 prefetch. The comparison table shows a 7% improvement with manual tuning.

For those willing to go beyond C++ and dive into low-level micro-optimizations, CUDA allows using inline Parallel Thread Execution (PTX) code and SASS (NVIDIA assembler) to extract the last grains of performance.

PTX is a low-level virtual machine for parallel thread execution. A high-level compiler (e.g., CUDA C++) generates PTX instructions that are translated into machine instructions of the target architecture. SASS is the low-level assembly language that actually executes natively on NVIDIA GPU hardware.

Typical use cases for inline PTX:

  • Using a GPU instruction with no direct CUDA intrinsic.
  • Applying memory load modifiers (cache hints) to specific accesses.
  • Inserting memory or synchronization barriers at precise points.
  • Manually reordering instructions to avoid pipeline stalls.
  • Reading special registers (SM ID, warp lane ID).

Example of L2 prefetch:

__global__ void PrefetchExample(const float *in, float *out) {
  int idx = blockIdx.x * blockDim.x + threadIdx.x;
  asm volatile("cp.async.bulk.prefetch.L2.global [%0], %1;"
               :: "l"(in + idx + 32), "n"(128));
  float x = in[idx];
  out[idx] = x;
}

Controlling cache behavior with inline PTX:

asm("ld.global.cg.f32 %0, [%1];" : "=f"(val) : "l"(ptr))

Loading a float with the .cg modifier ("cache global") means we want to cache data in L2 but bypass the L1 cache.

Accessing special registers:

asm("mov.u32 %0, %smid;" : "=r"(smid))

Some developers have used %smid in persistent kernels so that only one block per SM performs a specific job.

VersionWarp stalls (memory)Issue IPCKernel timeSpeedup
Optimized C++ (compiler schedule)35% cycles1.51.07 ms1.0× baseline
Hand-tuned PTX (manual scheduling and hints)20% cycles1.61.00 ms1.07×

After tuning, the kernel's memory stalls decreased thanks to overlapping loads, and cache hints reduced some latency. The net improvement is 7% in total kernel runtime.

Inline PTX/SASS is a tool of last resort. It provides maximum control at the cost of complexity. It is recommended to use it last, when you need a hardware feature unavailable in CUDA C++ — or when you've precisely identified a small code fragment where the compiler schedule can be improved.

Section XVII Summary
Key point of the chapter

Inline PTX/SASS is a tool of last resort. Typical scenarios: cache modifiers, manual instruction reordering, special register access.

Practical consequence

The cp.async.bulk.prefetch.L2.global example demonstrates L2 prefetch. Manual tuning gives ~7% improvement on already optimized C++.

• • •

XVIIIUsing Inline PTX in DeepSeek for Memory Access Optimization

Quote from the book · Chapter 9

«A well-known example of custom PTX is from DeepSeek's DeepEP expert parallelism library. This library used a bespoke PTX instruction, ld.global.nc.l1::no_allocate.l2::256b, to optimize global memory access by bypassing L1 cache allocations, preserving critical data, and leveraging 256-byte L2 cache chunks».

Exposition of content

In the chapter, DeepEP is described as an example of using an undocumented PTX instruction to fine-tune cache behavior on H800. The instruction ld.global.nc.l1::no_allocate.l2::256b bypasses L1, forcing each load to fetch fresh data from L2 or global memory. DeepEP also dedicates 20 SMs out of 132 on H800 for network communication.

Instruction breakdown

Let's break down the components of ld.global.nc.l1::no_allocate.l2::256b:

  • ld.global — load from global memory.
  • .nc — non-coherent load using the texture cache, which provides higher throughput for read-only access.
  • .l1::no_allocate — do not allocate data in the L1 cache, so as not to evict hot data residing there.
  • .l2::256b — fetch 256 bytes at a time into L2, increasing effective transaction width.

By bypassing L1, loads can stream large blocks of data directly into L2 without evicting frequently used data residing in L1.

Context and risks

This instruction is not part of NVIDIA's official PTX ISA specification (though it was later discovered that the .L1::no_allocate and .L2::256B modifiers are documented in the PTX ISA). DeepSeek engineers found it "off the books" to fine-tune cache behavior on their Hopper H800 GPU variant, restricted by US export rules.

The key problem the instruction addresses: a standard non-coherent load (ld.global.nc) can return stale data because the texture cache doesn't maintain coherence with global memory. Adding .l1::no_allocate bypasses L1, forcing each load to fetch fresh data from L2 or global memory.

DeepEP also dedicates 20 SMs out of 132 on H800 exclusively for network communication, so the L1 cache of those SMs stays "warm" for network code. This reduces compute performance by ~15% but significantly improves communication throughput.

Compatibility risk

Using PTX this way carries risks: ld.global.nc.l1::no_allocate.l2::256b is not guaranteed stable across GPU generations. Code that relies on it may break or produce incorrect results on future architectures. DeepSeek's DeepEP build includes a build-time flag DISABLE_AGGRESSIVE_PTX_INSTRS=1 to disable these aggressive instructions when compatibility problems arise.

Section XVIII Summary
Key point of the chapter

DeepEP uses the undocumented PTX instruction ld.global.nc.l1::no_allocate.l2::256b to bypass L1 and stream data into L2. This gives significant speedup on H800.

Practical consequence

Risk: the instruction is not guaranteed stable across GPU generations. DeepEP has a flag DISABLE_AGGRESSIVE_PTX_INSTRS=1 to disable it.

• • •

XIXPractical Kernel Optimization Checklist

Before moving to low-level optimizations, make sure the foundation is in place:

  1. Profile with Nsight Systems for the big picture and Nsight Compute for detailed kernel analysis.
  2. Build a Roofline — determine whether your kernel is memory-bound or compute-bound.
  3. Coalesce all global loads — this is a baseline requirement.
  4. Tile data into shared memory for reuse.
  5. Fuse element-wise operations to eliminate intermediate HBM writes.
  6. Increase batch size to amortize weight loading.
  7. Use reduced precision (TF32, FP16, BF16, FP8, FP4) where possible.
  8. Engage Tensor Cores via CUTLASS or PyTorch SDPA.
  9. Apply 2:4 structured sparsity for inference after calibration.
  10. Consider recompute instead of storing for memory-bound kernels.
  11. Check occupancy — too many registers? Any spilling?
  12. Only then move to inline PTX/SASS.

Following this systematic workflow of profiling, diagnosing, and applying targeted optimizations — from occupancy tuning and warp-level refinements to ILP and high arithmetic intensity through tiling, fusion, and Tensor Cores — you can turn a memory-bound GPU kernel into a compute-bound one. This can yield a large speedup, and in heavily memory-bound tasks, sometimes an order of magnitude.

First use a system profiler (e.g., NVIDIA Nsight Systems) to correlate CPU activity, GPU kernels, and NVLink/NVSwitch traffic across multiple GPUs. Then use Nsight Compute to dive deep into each kernel's details.

Interconnect and memory bandwidth saturation will occur long before kernels can fully utilize the compute capabilities of many interconnected GPUs (e.g., 72 in the NVL72 case). Therefore, increasing kernel arithmetic intensity is the key to efficient scaling for multi-GPU training and inference workloads.

Summary

Improving arithmetic intensity is systematic work: first profiling and Roofline, then coalescing and tiling, fusion, precision reduction, and Tensor Cores, and only at the very end — manual PTX/SASS. Each layer gives its own gain, and the right order of steps matters more than any single "silver bullet".

• • •

XXForecast Verification and Current Data

Fregly's book was written in 2025. Since then, new data has emerged that allows verification of some of the chapter's claims and clarification of figures.

Confirmed Forecast 1: Precision reduction gives multiplicative throughput growth

«The transition FP16 → FP8 gives ~2× throughput; FP16 → FP4 up to ~4×. FP4 halves bytes per parameter compared to FP8».

Confirmed. Blackwell B200 delivers 10 petaFLOPS (dense) in NVFP4 versus ~80 teraFLOPS in FP32 — a ~125× gain. B300 delivers 15 petaFLOPS in NVFP4. The transition to FP8 indeed doubles Tensor Core throughput, and FP4 quadruples it. Clarification: the real gain depends on layer sensitivity to quantization. Attention and FFN are more resilient to FP8/FP4 than normalization and softmax, which require FP32.

Confirmed Forecast 2: TMEM (256 KB) reduces register pressure

«TMEM is a dedicated 256 KB SRAM buffer per SM that substantially reduces register-file pressure and reduces cache miss latency by about 58%».

Confirmed. Blackwell architecture indeed moves Tensor Core accumulators to TMEM, offloading the register file. tcgen05.mma instructions are CTA-scoped, not warpgroup-scoped like wgmma on Hopper. This changes the programming model: the CTA acts as a single execution unit, and some threads can orchestrate computation while others move data.

Confirmed Forecast 3: CUTLASS delivers manual kernel performance within ~2%

«CUTLASS matches or exceeds manual MMA kernel performance within about 2%».

Confirmed. CUTLASS continues to evolve and support new architectures. However, building for SM100 requires the a suffix (sm100a) because tcgen05 falls under "architecture-accelerated features" without forward compatibility guarantees. Code compiled for SM100 is incompatible with GeForce RTX 50 (SM120).

Confirmed Forecast 4: DeepSeek's PTX instruction is not guaranteed stable across generations

«The instruction ld.global.nc.l1::no_allocate.l2::256b is not guaranteed stable across GPU generations».

Confirmed. This forecast remains valid: using undocumented PTX instructions carries compatibility risks. DeepEP has a DISABLE_AGGRESSIVE_PTX_INSTRS=1 flag to disable it. It is recommended to use documented modifiers (evict_last, cache_modifier) where possible.

Partially confirmed Forecast 5: NVL72 requires balance between compute and communication

«Interconnect and memory bandwidth saturation will occur long before kernels can fully utilize the compute capabilities of many interconnected GPUs (e.g., 72 in the NVL72 case)».

Partially confirmed. NVL72 indeed shifts the balance toward communication. However, NVLink/NVSwitch inside the rack is so fast (~130 TB/s) that for many workloads communication stops being a bottleneck inside the rack, but remains one when leaving it. When planning jobs, it is important to consider topology: all GPU jobs should, where possible, remain within a single NVLink domain.

• • •

XXIAdditional Observations Beyond the Chapter

The chapter describes kernel optimization methods but omits several aspects that have emerged in real-world practice.

Complexity of debugging at the PTX/SASS level

Additional observations

The chapter recommends inline PTX as a tool of last resort but does not describe the debugging complexities. PTX/SASS code is harder to maintain, non-portable between architectures, and requires deep hardware understanding. When the GPU generation is upgraded, such code must be rewritten. This is a strong argument for using CUTLASS and high-level libraries that automatically adapt to new architectures.

Ecosystem around Tensor Cores

Additional observations

The chapter describes Tensor Cores and CUTLASS but does not mention the tool ecosystem: NVIDIA provides cuBLASLt, cuDNN, TensorRT, as well as support in PyTorch and Triton. Tool choice depends on the task: for research, torch.compile and SDPA are sufficient; for production — TensorRT or CUTLASS; for non-standard kernels — Triton or CUDA C++.

Impact of sparsity on training

Additional observations

The chapter describes 2:4 structured sparsity for inference but does not consider its impact on training. In practice, sparsity can be used in training too, but with caution: gradients do not benefit from 2:4, and maintaining sparsity in gradients is complex. It is recommended to apply sparsity after training, with calibration and accuracy validation.

Limitations of SDPA and FlashAttention

Additional observations

The chapter mentions PyTorch SDPA and FlashAttention but does not describe their limitations. FlashAttention requires specific tensor shapes and data types. SDPA may not select FlashAttention for some configurations. It is recommended to verify which backend is actually used and, if necessary, specify it explicitly via torch.nn.attention.sdpa_kernel.

Memory-bound vs compute-bound in the NVL72 context

Additional observations

The chapter formulates the principle: increasing arithmetic intensity turns memory-bound kernels into compute-bound ones. However, in the context of NVL72 with its ~130 TB/s aggregate NVLink bandwidth, the balance shifts: communication inside the rack becomes so fast that for many workloads the bottleneck becomes computation, not memory. This means that for NVL72, kernel optimization may be even more important than for traditional clusters.

• • •

XXIIAdditional Conclusions

  1. Arithmetic intensity is the central metric. It determines the operating mode of the kernel and the direction of optimization. The Roofline model provides a visual basis for decision-making.
  2. Tiling and fusion are basic techniques. They give the greatest effect with minimal effort. Start with them.
  3. Precision reduction is a performance multiplier. FP8 and FP4 give multiplicative gains without algorithmic changes. But calibration and accuracy validation are required.
  4. Tensor Cores and TMEM change the programming model. On Blackwell, accumulators live in TMEM, and instructions are CTA-scoped. This gives more flexibility but requires understanding the new architecture.
  5. CUTLASS is the optimal choice for most cases. It delivers manual kernel performance within ~2% without weeks of tuning.
  6. PTX/SASS is a tool of last resort. Apply only after exhausting high-level optimizations.
  7. The NVL72 context shifts priorities. Inside the rack, communication is so fast that kernel optimization may be even more important than for traditional clusters.
Overall Review Summary

Chapter 9 of Fregly's book is a systematic review of methods for improving CUDA kernel efficiency. The key idea: increasing arithmetic intensity — from tiling and fusion to precision reduction and Tensor Cores — turns memory-bound kernels into compute-bound ones. For an engineer, this means understanding memory hierarchy and Tensor Core principles is more important than knowing specific numbers of the current generation. As we move toward models with hundreds of trillions of parameters and systems like NVL72, co-design of hardware, software, and algorithms ceases to be optional and becomes the only way forward.

Sources: Chris Fregly, «AI Systems Performance Engineering» (O'Reilly, 2026), Chapter 9 — quotes provided in limited volume for review and critical analysis purposes. NVIDIA CUTLASS documentation; NVIDIA Tensor Memory Accelerator documentation; NVIDIA Transformer Engine documentation; PyTorch documentation; MLPerf Training/Inference v5.0; open DeepSeek publications.