English translation is not available yet. Showing Russian content.
Как работает speculative execution на GPU для LLM (branch prediction)?
Краткий тезис
GPU не использует сложное branch prediction (предсказание ветвлений) как CPU из-за архитектуры SIMT (Single Instruction, Multiple Threads). Вместо этого GPU применяет predicated execution (исполнение с предикатами]]): при ветвлении все потоки warp (группы из 32 потоков) выполняют обе ветки, а результаты маскируются. Для LLM-ядер критично минимизировать divergent control flow (расходящиеся ветвления) и использовать predicated инструкции (selp, setp). Пример — FlashAttention, где внутри блока потоков достигается uniform control flow (единообразный поток управления).
1. Термины и контекст
Speculative execution (execution|спекулятивное исполнение) — техника CPU, при которой процессор предсказывает исход условного перехода и начинает выполнять одну из веток до проверки условия. При ошибке предсказания результаты отбрасываются, что вызывает штраф (pipeline flush).
prediction|Branch prediction (предсказание ветвлений) — компонент CPU, предсказывающий, какая ветка будет выполнена. Современные CPU достигают точности >95%, но ошибки всё равно дороги.
SIMT (Single Instruction, Multiple Threads) — модель исполнения GPU: множество потоков выполняют одну и ту же инструкцию, но над разными данными. Потоки объединяются в warp (32 потока на NVIDIA GPU). Внутри warp все потоки синхронны и выполняют одинаковые инструкции.
execution|Predicated execution (исполнение с предикатами) — механизм GPU: вместо предсказания ветки, GPU выполняет обе ветки, но только для тех потоков, где условие истинно. Результаты для неактивных потоков маскируются (не записываются).
control control flow|Divergent control control flow (control control расходящееся управление) — ситуация, когда внутри warp разные потоки выбирают разные ветки условного оператора. Это снижает эффективность, так как все потоки выполняют обе ветки последовательно.
Uniform control flow (единообразное управление) — все потоки warp следуют одному пути, ветвления нет. Это оптимально для GPU.
2. Отличие CPU branch prediction от GPU predicated execution
| Характеристика | CPU | GPU |
|---|---|---|
| Модель исполнения | SIMD (векторная) + out-of-order | SIMT (массовый параллелизм) |
| Обработка ветвлений | Предсказание + спекулятивное исполнение | Predicated execution (выполнение обеих веток) |
| Штраф при ошибке | Pipeline flush (10–20 тактов) | Выполнение лишних инструкций (2× время) |
| Типичная точность | >95% | Не применимо (нет предсказания) |
| Инструкции | JMP, JE, CALL | SELP, SETP, @P (предикаты) |
| Пример | if (x > 0) y = a; else y = b; → предсказание | selp (select with predicate) — обе ветки вычисляются |
Ключевой вывод: GPU не делает speculative execution в классическом смысле. Вместо этого он жертвует производительностью части потоков ради простоты и масштабируемости.
3. Как GPU обрабатывает ветвления: детальный механизм
Рассмотрим код на CUDA:
__global__ void kernel(float* data, int n) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
if (idx < n) {
if (data[idx] > 0.0f) {
data[idx] = data[idx] * 2.0f;
} else {
data[idx] = data[idx] + 1.0f;
}
}
}
Шаги исполнения внутри warp
- Проверка условия: инструкция SETP (set predicate) вычисляет предикат
p = (data[idx] > 0.0f)для каждого потока. - Маскирование: GPU создаёт две маски — активные потоки для true-ветки (
p == true) и для false-ветки (p == false). - Выполнение true-ветки: все потоки warp выполняют инструкцию умножения, но только те, у кого
p == true, записывают результат. Остальные игнорируют запись (маскированы). - Выполнение false-ветки: аналогично, все потоки выполняют инструкцию сложения, записывают только те, у кого
p == false. - Синхронизация: warp завершает обе ветки, и все потоки продолжают с unified кода.
Проблема: если внутри warp есть дивергенция, GPU тратит время на выполнение обеих веток. Если одна ветка короткая, а другая длинная, эффективность падает до 50% (половина потоков простаивает).
Инструкция selp (select with predicate) — оптимизированная версия: вычисляет оба результата и выбирает нужный по предикату без явного ветвления. Пример:
float val = data[idx];
float true_res = val * 2.0f;
float false_res = val + 1.0f;
data[idx] = (val > 0.0f) ? true_res : false_res; // компилятор может заменить на selp
4. Влияние на LLM kernels
LLM-ядра (например, attention, feed-forward) часто содержат условные операции:
- Маски внимания (causal mask, padding mask) — для каждого токена нужно решить, учитывать ли другие токены.
- Softmax — вычисление экспонент и суммирование, где есть проверки на
-inf. - KV cache — управление памятью с условными копированиями.
- Activation functions (ReLU, GELU) — могут иметь ветвления (например,
if (x > 0)).
Пример дивергенции в attention:
for (int j = 0; j < seq_len; ++j) {
if (mask[idx * seq_len + j] > 0) { // padding
float score = query * key[j];
// ...
}
}
Здесь разные потоки warp могут иметь разные маски → дивергенция.
Последствия: снижение occupancy (загрузки GPU) и пропускной способности. Для LLM инференса это критично, так как latency чувствителен.
5. Оптимизации для LLM на GPU
5.1 Использование predicated инструкций
Компилятор NVIDIA (nvcc) автоматически заменяет простые if-else на selp, если обе ветки короткие. Разработчик может помочь:
- Избегать глубоких вложенных ветвлений.
- Использовать тернарный оператор
? :вместоif-else. - Применять встроенные функции
__ballot_sync,__any_syncдля проверки условий внутри warp.
5.2 Минимизация дивергенции
- Реструктуризация данных: группировать потоки так, чтобы внутри warp все шли по одному пути. Например, сортировать токены по длине маски (bucketing).
- Uniform branching: если условие одинаково для всех потоков warp (например,
if (blockIdx.x == 0)), дивергенции нет — это uniform control flow. - Loop unrolling: разворачивать циклы с известным числом итераций, чтобы компилятор мог оптимизировать.
5.3 Пример: FlashAttention
FlashAttention — алгоритм attention, который разбивает вычисления на блоки и использует tiling. Внутри каждого блока все потоки warp выполняют одинаковые операции: загрузка данных, умножение, softmax. Условия (например, проверка на конец последовательности) вынесены на уровень блоков, а не потоков. Это даёт uniform control flow и высокую эффективность.
// FlashAttention kernel — нет if внутри warp
for (int j = 0; j < num_blocks; ++j) {
float4 key_block = load_key_block(j);
float score = dot(query, key_block);
// softmax — uniform операции
}
6. Speculative execution в контексте LLM: не путать с speculative decoding
Speculative execution на GPU — это не то же самое, что speculative decoding (спекулятивная декодировка) для LLM. Speculative decoding — техника ускорения генерации текста, при которой маленькая модель (draft model) предсказывает несколько токенов, а большая модель проверяет их. Это не связано с branch prediction.
В контексте GPU speculative execution может относиться к warp scheduling — когда один warp простаивает (ждёт память), GPU переключается на другой warp. Это скрывает задержки, но не является предсказанием ветвлений.
7. Инструменты профилирования
NVIDIA Nsight Compute — профилировщик CUDA ядер. Позволяет увидеть:
- Divergence — процент дивергентных ветвлений в warp.
- Branch efficiency — доля потоков, которые реально выполнили инструкцию (не маскированы).
- Occupancy — загрузка SM (Streaming Multiprocessor).
Команда для профилирования:
ncu --set full -o profile ./my_llm_kernel
В GUI можно найти раздел Warp State Statistics и Branch Statistics.
8. Практические рекомендации для разработчиков LLM kernels
- Измеряйте дивергенцию — используйте Nsight Compute. Если divergence > 10%, ищите оптимизации.
- Используйте
__syncthreads()— синхронизация внутри блока может помочь унифицировать управление. - Предпочитайте
selp— компилятор часто сам заменяет, но можно явно написать через? :. - Группируйте данные — сортируйте последовательности по длине (bucketing) для uniform масок.
- Избегайте ветвлений в горячих циклах — выносите условия наружу.
- Используйте
#pragma unroll— помогает компилятору оптимизировать.
9. Связь с другими аспектами GPU
- Tensor Cores — специализированные блоки для матричных умножений. Они не поддерживают ветвления, поэтому код должен быть uniform.
- Memory coalescing — дивергенция может нарушить объединение запросов к памяти, снижая пропускную способность.
- Occupancy — дивергенция снижает количество активных warp на SM, уменьшая occupancy.
Пет-проект для закрепления
Задача: Написать простой CUDA kernel для фрагмента attention с дивергентным ветвлением и оптимизировать его.
Инструменты: CUDA Toolkit, Nsight Compute, Python (для тестов).
Шаги:
- Реализовать kernel, который для каждого потока вычисляет
score = dot(query, key), и еслиscore > threshold, то добавляет его в сумму (ветвление). - Измерить производительность и дивергенцию через
ncu. - Оптимизировать: заменить
ifнаselp(через тернарный оператор), переписать без ветвления, используя маску. - Сравнить время выполнения и branch efficiency.
Ожидаемый результат: Понимание, как дивергенция влияет на производительность, и умение применять predicated execution.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 710 | Как работает FlashAttention? |
| 712 | Что такое Tensor Cores и как они ускоряют LLM? |
| 715 | Как работает continuous batching для LLM? |
| 720 | Что такое speculative decoding? |
| 705 | Как устроен инференс LLM на GPU? |
| 730 | Как профилировать производительность LLM на GPU? |
Навигация
- Предыдущий: 710
- Следующий: 712
- Индекс: 00. Индекс разборов