Как вы диагностируете, что проблема в memory bandwidth, а не в compute?

Краткий тезис

Диагностика узкого места между memory bandwidth (пропускной способностью памяти) и compute (вычислительной мощностью) основана на анализе поведения модели при изменении batch size, измерении утилизации вычислительных ядер и памяти, а также использовании профилировщиков. Ключевые индикаторы: рост FLOPs не приводит к увеличению latency, насыщение bandwidth при увеличении batch size, высокий memory stall ratio (доля тактов ожидания памяти) в профилировщике. Для точной диагностики применяют roofline model и сравнивают арифметическую интенсивность операций с пиковой производительностью.


1. Термины: Compute-bound и Memory-bound

В контексте инференса LLM (и любых нейросетей) операции делятся на два типа:

  • Compute-bound (ограниченные вычислениями) — время выполнения определяется скоростью арифметических операций (FP16, FP8 matmul). Пример: умножение матриц большого размера (например, в слоях attention при длинном контексте).
  • Memory-bound (ограниченные памятью) — время выполнения определяется скоростью доступа к данным (чтение/запись в HBM или SRAM). Пример: загрузка весов модели, операции нормализации, softmax, поэлементные функции.

В LLM инференсе авторегрессивная генерация (по одному токену) почти всегда memory-bound, потому что batch size мал (часто 1), а веса модели огромны — их приходится каждый раз загружать из HBM в SRAM. Prefill (обработка промпта) может быть compute-bound при достаточно длинном контексте.


2. Основные индикаторы memory bandwidth bottleneck

2.1 Поведение при изменении batch size

  • Compute-bound: при увеличении batch size latency почти не меняется (пока не упрётся в память), throughput растёт линейно.
  • Memory-bound: latency растёт пропорционально batch size (каждый дополнительный пример требует загрузки тех же весов), throughput быстро насыщается.

2.2 Низкая утилизация вычислительных ядер (MFU)

MFU (Model FLOPs Utilization) — отношение реально выполненных FLOPs к пиковой производительности устройства. Если MFU < 30% при batch size = 1, это сильный признак memory-bound. Высокий MFU (50–80%) указывает на compute-bound.

2.3 Memory stall ratio

Профилировщики (NVIDIA Nsight, PyTorch Profiler) показывают долю тактов, когда ядро GPU простаивает в ожидании данных из памяти. Если этот показатель > 40–50% — bottleneck в memory bandwidth.

2.4 Эффект от оптимизаций памяти

  • FlashAttention (уменьшает чтение/запись HBM) даёт значительный прирост скорости на memory-bound операциях, но почти не влияет на compute-bound.
  • Kernel fusion (объединение нескольких операций в один kernel) полезен для compute-bound, но для memory-bound он бесполезен, так как узкое место — не количество запусков, а пропускная способность памяти.

3. Инструменты профилирования

ИнструментНазначениеКлючевые метрики
NVIDIA Nsight SystemsСистемное профилированиеTimeline, utilization, memory throughput
NVIDIA Nsight ComputeДетальный анализ kernelMemory stall, occupancy, arithmetic intensity
PyTorch ProfilerВстроенный профилировщикSelf CUDA time, memory bandwidth, FLOPs
Roofline modelТеоретическая модельАрифметическая интенсивность vs пиковая производительность

Roofline model — график, где по оси X — арифметическая интенсивность (FLOPs / byte), по оси Y — производительность (FLOPs/s). Точка пересечения наклонной линии (bandwidth) и горизонтальной (compute) — ridge point. Если арифметическая интенсивность операции ниже ridge point — она memory-bound, выше — compute-bound.


4. Экспериментальный подход

4.1 Измерение latency при разных batch size

Запустите инференс модели с batch size 1, 2, 4, 8, 16 (насколько позволяет память). Замерьте среднюю latency на один запрос и throughput (запросов/сек).

4.2 Расчёт арифметической интенсивности

Для каждой операции (например, matmul в attention) можно оценить:

  • Количество FLOPs: 2 * M * N * K (для умножения матриц M×K и K×N).
  • Количество переданных байт: (MK + KN + M*N) * sizeof(element).

Если арифметическая интенсивность < 100–200 FLOPs/byte (зависит от GPU), операция memory-bound.

4.3 Использование профилировщика

Пример с PyTorch Profiler:

import torch
import torch.profiler

model = ...  # загруженная модель (например, GPT-2)
input_ids = torch.randint(0, 1000, (1, 128)).cuda()

with torch.profiler.profile(
    activities=[torch.profiler.ProfilerActivity.CUDA],
    record_shapes=True,
    with_stack=True
) as prof:
    with torch.no_grad():
        output = model(input_ids)

print(prof.key_averages().table(
    sort_by="self_cuda_time_total", row_limit=20
))

В выводе обратите внимание на kernels с высоким self_cuda_time_total и низкой occupancy. Если среди лидеров — операции загрузки весов (например, aten::mm с маленькими размерами), это memory-bound.


5. Таблица сравнения compute-bound vs memory-bound

ХарактеристикаCompute-boundMemory-bound
Latency при увеличении batch sizeПочти не меняетсяРастёт линейно
Throughput при увеличении batch sizeРастёт линейноНасыщается
MFU50–80%<30%
Memory stall ratioНизкий (<20%)Высокий (>40%)
Эффект FlashAttentionНебольшойЗначительный (2–5x)
Эффект kernel fusionПолезенБесполезен
Примеры операцийMatMul (большие размеры), GEMMLayerNorm, Softmax, загрузка весов, поэлементные операции

6. Связь с RAG и Agentic RAG

В Agentic RAG система часто выполняет множество маленьких инференсов: генерация одного токена, вызов инструмента, повторный retrieval. Каждый такой вызов — memory-bound из-за малого batch size. Это приводит к низкой утилизации GPU и высокому latency.

Способы смягчения

  • Batching запросов от разных агентов (если возможно).
  • KV cache — уменьшает объём пересылаемых данных при повторной генерации.
  • Speculative decoding — уменьшает число шагов генерации.
  • Модели меньшего размера (например, 7B вместо 70B) — снижают объём весов, но могут ухудшить качество.

Диагностика memory bandwidth bottleneck помогает выбрать правильную стратегию оптимизации: вместо ускорения вычислений (которые и так быстры) нужно уменьшать объём передаваемых данных.


7. Пет-проект для закрепления

Задача Профилировать инференс небольшой LLM (GPT-2) и определить, является ли генерация токенов memory-bound или compute-bound.

Инструменты PyTorch, transformers, PyTorch Profiler, matplotlib (для графиков).

Шаги:

  1. Загрузите модель GPT-2 (from transformers import GPT2LMHeadModel).
  2. Напишите функцию генерации одного токена (без model.generate, вручную цикл по токенам).
  3. Для batch size 1, 2, 4, 8 замерьте среднее время на один токен (latency) и throughput (токенов/сек).
  4. Используйте PyTorch Profiler для каждого batch size, запишите self_cuda_time_total для top-10 kernels.
  5. Постройте графики latency vs batch size и throughput vs batch size.
  6. Рассчитайте MFU: оцените теоретические FLOPs для одного forward pass (например, 2 * число параметров * длину последовательности) и сравните с реальным временем.
  7. Сделайте вывод: при каком batch size модель переходит из memory-bound в compute-bound?

Ожидаемый результат При batch size = 1 latency высокая, MFU низкий (<20%), throughput насыщается уже при batch size = 4–8. Профилировщик покажет, что основное время тратится на загрузку весов (kernels типа aten::mm с маленькими размерами). Это подтверждает memory-bound bottleneck.


8. Связь с другими вопросами

ВопросТема
312Как оптимизировать latency инференса LLM?
314Как работает FlashAttention?
315Как профилировать RAG-систему?
310Что такое KV cache и как он влияет на производительность?
311Как работает batching в LLM?
309Какие архитектурные решения влияют на throughput LLM?

9. Навигация


Навигация