Перейти к содержимому
ПереводыDatawhale и китайские курсы

Ли Хунъи, «Машинное обучение» 2026: FlashAttention, KV-кэш и позиционные эмбеддинги

Конспект «внутримодельного» блока курса «Машинное обучение» 2026 профессора Ли Хунъи: почему генерация медленная и как её ускоряют (FlashAttention с «исправляющим множителем», KV-кэш, MQA/GQA/MLA, скользящее окно, attention sink, обрезка кэша, кэширование префикса и цена токенов) и как Transformer узнаёт порядок токенов (синусоиды, ALiBi, RoPE, расширение контекста, NoPE).

  • Спрашивайте о цене ускорения. Ли предлагает рамку на две лекции: меняет ли метод результат внимания (точный или приближённый)? Требует ли он обучать/переделывать модель? И что ещё за него платим?
  • FlashAttention точен, подходит к любой модели и почти бесплатен: GPU считает быстро, а медленно перевозит данные между большой памятью (HBM) и крошечной быстрой (SRAM). Если разрешить себе «временно неверный» softmax и потом домножать на поправку, внимание считается за один проход без записи матрицы весов.
  • KV-кэш избавляет от пересчёта, но съедает память: в примере с архитектурой Gemma 2 27B — около 0,72 МБ на токен, на A100 80 ГБ влезает лишь ~114 тыс. токенов. Отсюда GQA, MLA, скользящее окно, attention sink, обрезка кэша.
  • Кэширование префикса — это деньги: кэшированные входные токены в API в несколько раз дешевле, поэтому всё неизменное в промпте агента ставят в начало, а изменяющееся (дату!) — в конец.
  • Позиции: важны относительные расстояния; RoPE кладёт их во вращение qq и kk. Контекст расширяют, «сжимая» углы низкочастотных пар (PI, NTK, YaRN, LongRoPE), а работы NoPE и DroPE показывают, что каузальная модель может обходиться без позиционного кодирования.
ЛекцияКогда смотретьНаш урок
FlashAttentionвместе с уроком 5.7.2 (перед ним — 5.7.1 про устройство GPU)5.7.1, 5.7.2
KV-кэшпосле 4.6.4, вместе с модулем 5.2 и уроком 7.1.24.6.4, 5.2.1, 5.2.2, 7.1.2
Позициипосле 4.5.2, вместе с 5.5.14.5.2, 5.5.1, 5.5.2

Курс предполагает, что вы уже понимаете устройство Transformer (лекция «Анатомия большой языковой модели» из курса осени 2025 года). Спекулятивное декодирование в этом году не читали — Ли отсылает к лекции 16 курса 2024 года (наш конспект).


Лекция «Ускоряем генерацию, часть 1: FlashAttention»

Заголовок раздела «Лекция «Ускоряем генерацию, часть 1: FlashAttention»»

Видео на YouTube · слайды inference.pdf, с. 1–28, учебный Colab.

Префилл и декодирование. Подсказка проходит модель целиком за один раз (prefill), затем токены порождаются по одному (decode). Для токена 4 внимание — это ai=q4⋅kia_i = q_4 \cdot k_i, softmax до a^i\hat a_i, взвешенная сумма viv_i.

Метафора Ли. Вычислительные блоки GPU — толпа «многоруких эльфов», считают мгновенно; верстак — встроенная SRAM, крошечная (обычно с десяток мегабайт); склад — HBM (на A100 — 80 ГБ). Всё, что считается, надо перенести со склада на верстак и обратно, и именно перевозка — узкое место. Главное ограничение: на верстак нельзя класть ничего, что растёт с длиной последовательности LL (в агентах LL — сотни тысяч), поэтому ключи режут на блоки по NN.

Наивный softmax — много походов на склад. Чтобы вычесть максимум (ради численной устойчивости), надо сначала просмотреть все aia_i; потом посчитать экспоненты; потом знаменатель; потом нормировать; потом взвешенную сумму — каждый шаг заново читает все блоки.

Трюк: «сначала ошибёмся, потом поправим». Примем максимум первого блока d1d_1 за глобальный и посчитаем частичную сумму s1s_1. Встретив в следующем блоке больший максимум d2d_2, не перечитываем старое — домножаем:

sk=sk−1 e dk−1−dk+∑i∈блок ke ai−dk,dk=max⁡(dk−1, max⁡i∈блок kai).s_k = s_{k-1}\, e^{\,d_{k-1}-d_k} + \sum_{i \in \text{блок } k} e^{\,a_i - d_k}, \qquad d_k = \max(d_{k-1},\ \max_{i \in \text{блок } k} a_i).

Ли называет это «всего лишь одной формулой» — но весь FlashAttention повторяет её снова и снова. Следующий вопрос лектора: обязательно ли сначала вычислить веса внимания, чтобы взять взвешенную сумму? Нет: читаем вместе с ключами и значения vv и сразу копим выход с «неправильными» весами, а затем поправляем и его:

Ok=Ok−1 sk−1sk e dk−1−dk+∑i∈блок ke ai−dksk vi.O_k = O_{k-1}\,\frac{s_{k-1}}{s_k}\, e^{\,d_{k-1}-d_k} + \sum_{i \in \text{блок } k} \frac{e^{\,a_i - d_k}}{s_k}\, v_i .

После последнего блока OO в точности равен обычному ∑ia^ivi\sum_i \hat a_i v_i, а матрица весов так и не была записана в память. Побочный эффект: в Hugging Face с FlashAttention нельзя достать веса внимания для анализа — их просто нет.

Что показывает Colab курса. PyTorch scaled_dot_product_attention с бэкендами MATH и FLASH_ATTENTION: результаты совпадают до ~10−710^{-7}; на A100 при длине 4096 FlashAttention примерно в 9 раз быстрее (в сохранённом выводе 9,49×). На реальной модели (google/gemma-3-4b-it, eager против sdpa) на коротких входах разницы почти нет — много времени уходит на полносвязные слои, — а на длинных выигрыш заметен; если удлинить вход ещё в 10 раз — CUDA out of memory. Переполнился склад, а не верстак: почему — следующая лекция. Обычно PyTorch и так выбирает FlashAttention по умолчанию, то есть вы им, скорее всего, уже пользуетесь.

Лекция «Ускоряем генерацию, часть 2: KV-кэш»

Заголовок раздела «Лекция «Ускоряем генерацию, часть 2: KV-кэш»»

Видео на YouTube · слайды inference.pdf, с. 29–55.

Что такое KV-кэш. После префилла запросы qq выбрасывают, а ключи и значения хранят: для нового токена нужны только его собственные q,k,vq, k, v и старые k,vk, v. Ли шутит, что cache и cash звучат одинаково — и в конце лекции окажется, что кэш действительно про деньги.

Почему кэш переполняет склад. Ключи и значения хранятся для каждого токена, каждой головы и каждого слоя. Пример по архитектуре Gemma 2 27B (если считать, что у всех 32 голов свои k,vk, v):

46 слоёв×32 головы×128×2 байта (FP16)×2 (k и v)=753 664 байта≈0,72 МБ на токен,46\ \text{слоёв} \times 32\ \text{головы} \times 128 \times 2\ \text{байта (FP16)} \times 2\ (k \text{ и } v) = 753\,664\ \text{байта} \approx 0{,}72\ \text{МБ на токен},

то есть на 80 ГБ — около 114 тысяч токенов, а агентам нужно больше. Способы «уместить больше»:

  1. Делить k,vk, v между запросами. MQA — одна пара k,vk, v на все головы (по словам Ли, качество страдает); GQA — пара на группу голов (Llama, Gemma). Почему делят именно k,vk, v, а не qq? Потому что хранятся только k,vk, v.
  2. Сжимать k,vk, v — MLA (DeepSeek-V2): хранить маленький латентный вектор cic_i, а ключи и значения получать умножением на матрицы. Распаковывать не нужно: q⊤WKci=(WK⊤q)⊤ciq^\top W_K c_i = (W_K^\top q)^\top c_i — достаточно один раз преобразовать запрос, а взвешенную сумму считать в сжатом пространстве и распаковать один раз в конце. Это точное тождество, не приближение; модель нужно обучать так с самого начала.
  3. Ограничить область внимания. Скользящее окно (например, 4096 токенов) даёт кэшу фиксированный предел, а многослойность позволяет всё равно «видеть» далеко (в GPT-OSS слои с окном чередуются с полными). StreamingLLM: чистое окно ломается на длинных входах, но если всегда держать в окне первые несколько токенов, качество возвращается — веса внимания обязаны суммироваться в 1, и «когда смотреть не на что», модель по привычке смотрит на первый токен (attention sink).
  4. Выбрасывать неиспользуемые k,vk, v (Scissorhands, H2O): внимание сосредоточено на немногих токенах; то, на что долго никто не смотрит, удаляют. Scissorhands сжимал кэш в 5 раз почти без потерь, но на трудных задачах такая обрезка вредит — тема ещё исследуется.

Кэш между диалогами — это деньги. «Всем привет, я Дацзинь» и «Всем привет, я Сяоцзинь» совпадают в первых пяти токенах — их k,vk, v можно переиспользовать. А дальше даже одинаковый иероглиф «цзинь» уже не совпадёт: представление токена зависит от всего, что было до него. Переиспользуется только точно совпадающий префикс. Поэтому провайдеры делают скидку: в прайсе OpenAI на слайде кэшированный ввод в 10 раз дешевле обычного. Для агентов это золото: в системном промпте всё неизменное (инструменты, правила, личность, список навыков) ставьте в начало, изменчивое (например, текущую дату) — в конец; и шаблоны пишите так, чтобы переменные шли последними («забронируй рейс из x в y» + значения x, y в конце). В работе «Don’t Break the Cache» на долгих агентных задачах кэширование снизило стоимость на 46–80 % в зависимости от модели.

Итоговая таблица Ли:

МетодЧто делаетМеняет внимание?Нужно обучать?Другая цена
FlashAttentionменьше перевозок данныхнетнетчуть больше вычислений, сложнее понять
KV-кэшхранить посчитанные k,vk, vнетнетпамять
MQAобщие k,vk, v для всех запросовдадаможет заметно ухудшить модель
GQAобщие k,vk, v по группамдада
MLAсжатые k,vk, vдада
Скользящее окноограничить область вниманиядапо-разному
StreamingLLMокно + первые токеныдапо-разному
Обрезка KV-кэшавыбросить k,vk, vданетможет заметно ухудшить модель
Спекулятивное декодированиемаленькая модель «предсказывает»нет (теоретически)нетлишние вычисления малой модели

Лекция «Как Transformer узнаёт порядок токенов: абсолютные, относительные, RoPE и вовсе без позиций»

Заголовок раздела «Лекция «Как Transformer узнаёт порядок токенов: абсолютные, относительные, RoPE и вовсе без позиций»»

Видео на YouTube · слайды pos.pdf (64 слайда), Colab.

Зачем позиции. Для токена D внимание к «A B C» и к «C B A» даёт одну и ту же взвешенную сумму, а «ты бьёшь меня» и «я бью тебя» — противоположности.

1. Абсолютные позиции. К вектору токена прибавляется вектор позиции. Синусоидальный вариант «Attention Is All You Need» — набор часовых стрелок: высокочастотные пары координат — «секундные», различают соседей; низкочастотные — «часовые», различают далёкие позиции (на слайде: полный оборот за 6,3, 628,3 и 54 410,1 позиции у разных пар). Но важно относительное расстояние: связь «кошка — рыба» не должна зависеть от того, стоит фраза в начале или на 101-й позиции. Если разложить (xm+pm)⋅(xn+pn)(x_m + p_m)\cdot(x_n + p_n), в синусоидальном варианте не видно, чтобы «позиционный» член зависел только от m−nm - n.

2. Относительные позиции. ALiBi: из оценки внимания вычитается b (m−n)b\,(m-n) — «чем дальше, тем меньше внимания, вот и всё»; bb задают вручную, разным головам разный. T5: добавка к оценке зависит от расстояния и обучается.

3. RoPE (Llama, Qwen, Gemma). Каждую пару координат qq и kk поворачиваем на угол nθin\theta_i, где nn — позиция; в скалярное произведение входит только разность углов (m−n)θi(m-n)\theta_i, поэтому «кошка» и «рыба» на позициях 1 и 3 дают ту же оценку, что на 101 и 103. Не меняет вычисление внимания и совместим с KV-кэшем. Углы θi=1/100002i/d\theta_i = 1/10000^{2i/d} — та же «секундная/часовая» структура, только вместо сложения — вращение. Распространённое мнение «RoPE ослабляет внимание с расстоянием» Ли опровергает со ссылкой на «Round and Round We Go!»: затухания нет, и это не обязательно плохо — запрос можно обучить смотреть ровно на два токена назад.

4. Обучение на коротком — применение к длинному. На обучении ключ поворачивался максимум на NθN\theta, на длинном тесте — на 2Nθ2N\theta: модель никогда не видела таких направлений. Исправления:

МетодИдеяЦена
Position Interpolationсжать все позиции в обучающий диапазон (1, 2, 3, 4 → 0,5, 1, 1,5, 2)нужно дообучение
частотный подходбыстрые пары и так «прокрутились» много раз — их не трогаем; сжимаем только медленныерешить, какие пары «быстрые»
NTK-aware scalingплавно: самая быстрая пара не меняется, самая медленная сжимается в LL разидея из поста на Reddit, не из статьи
YaRN«статейная» версия частотного подхода
динамическое масштабированиесжимать только когда вход длиннее обучающего (иначе короткие тексты портятся)коэффициент зависит от длины
LongRoPEчастотный + динамический подход, коэффициенты ищутся эволюционным поискомнужен поиск

5. А нужны ли позиции вообще? В каузальном декодере каждый токен видит только себя и прошлое: в «кошка ест рыбу?» и «рыба ест кошку?» последний токен видит одно и то же множество слов, но промежуточные токены видят разные префиксы, и через слои порядок всё равно «просачивается». NoPE: без позиционного кодирования длина обобщается лучше, чем с APE, T5, ALiBi или RoPE (на их задачах рассуждения и математики). DroPE: позиции помогают сойтись при предобучении, но мешают обобщаться на длину; после обучения их можно убрать и после короткой перекалибровки удлинять контекст без обучения.


  1. Онлайн-softmax. Напишите на NumPy функцию, которая считает softmax-взвешенную сумму блоками по формулам выше, за один проход, и сравните с обычным расчётом на случайных данных (ошибка должна быть около 10−1510^{-15} в float64).
  2. Калькулятор KV-кэша. Посчитайте байты на токен и максимальный контекст на одной RTX 4080 32 ГБ для Qwen3-8B и Qwen3-32B (возьмите числа слоёв и KV-голов из их config.json; вычтите память под веса).
  3. Кэш префикса. Отправьте в vLLM с --enable-prefix-caching 20 запросов с одинаковым длинным системным промптом — сначала с датой в начале промпта, потом в конце; сравните время до первого токена.
EnglishРусскийПояснение
Prefill / Decodeпрефилл / декодированиеобработка подсказки / генерация по токену
HBM / SRAM«склад» / «верстак»большая медленная / маленькая быстрая память GPU
online softmaxонлайн-softmaxпоблочный softmax с поправками
KV CacheKV-кэшсохранённые ключи и значения
MQA / GQA / MLAобщие / групповые / латентные k,vk, vспособы уменьшить кэш
Sliding Windowскользящее окновнимание только на последние WW токенов
attention sink«сток внимания»первые токены, куда уходит «лишнее» внимание
prompt cachingкэширование префиксапереиспользование k,vk, v между запросами
Positional Embeddingпозиционный эмбеддинг
RoPEвращательное позиционное кодирование
Train short, test long«учим на коротком — применяем к длинному»расширение контекста
NoPEбез позиционного кодированияNo Positional Embedding