Перейти к содержимому
Ступень 55.5 Длинный контекст

Линейное внимание и гибриды

Как убрать softmax и превратить внимание в RNN с памятью фиксированного размера — ядерный трюк, Lightning Attention MiniMax, Gated DeltaNet и gated attention в Qwen3-Next, KDA в Kimi Linear — и почему MiniMax в M2 вернулась к полному вниманию.

УРОКstage-5/5-5-long-context/03-linear-attention.mdx2 ч 10 минУрок 3 из 3
После урока вы сможете
  • Вывести линейное внимание через ядерный трюк и показать, что параллельная, рекуррентная и чанковая формы дают один и тот же результат
  • Объяснить, почему память фиксированного размера ограничивает точный поиск по контексту и зачем нужны затухание, гейты и дельта-правило
  • Описать гибриды MiniMax-01 (Lightning Attention), Qwen3-Next (Gated DeltaNet и gated attention) и Kimi Linear (KDA и MLA)
  • Взвесить аргументы MiniMax за возврат к полному вниманию в M2 и объяснить, где гибриды выигрывают, а где рискуют

Разреженное внимание из прошлого урока экономит вычисления, но не память: NSA и DSA хранят KV-кэш всех прошлых токенов и лишь читают его часть. Если бы все 48 слоёв модели вроде Qwen3-Next были обычным вниманием (2 KV-головы по 256), кэш одной последовательности в миллион токенов весил бы около 92 ГиБ в BF16.

Радикальная альтернатива — память фиксированного размера, как у RNN из урока 3.4.3. Мостом служит линейное вниманиелинейное вниманиеEN linear attention. При обучении оно параллельно, как трансформер. При генерации это RNN: на каждом шаге обновляет состояние постоянного размера, и стоимость токена не зависит от длины контекста.

У этой идеи драматичная история. В 2020 году она казалась курьёзом. В 2025-м сразу три китайские лаборатории построили на ней крупные открытые модели: MiniMax-01 (январь), Qwen3-Next (сентябрь), Kimi Linear (октябрь). А MiniMax в том же октябре отказалась от неё в M2 и подробно объяснила почему. В конце урока мы честно взвесим обе стороны.

Причинное softmax-внимание для запроса ii:

oi=∑j≤iexp⁡(qi⊤kj/d) vj∑j≤iexp⁡(qi⊤kj/d).o_i = \frac{\sum_{j \le i} \exp(q_i^\top k_j / \sqrt d)\, v_j}{\sum_{j \le i} \exp(q_i^\top k_j / \sqrt d)} .

Экспонента «склеивает» qiq_i и kjk_j так, что их не разделить: для каждого нового запроса надо заново пройти по всем ключам. Отсюда O(n2)O(n^2).

Теперь заменим сходство exp⁡(q⊤k)\exp(q^\top k) на скалярное произведение признаков φ(q)⊤φ(k)\varphi(q)^\top \varphi(k), где φ\varphi — отображение признаковотображение признаков (feature map)EN feature mapФункция φ, которая переводит запрос или ключ в неотрицательные признаки, например φ(x) = elu(x) + 1. с неотрицательными значениями. Это ядерный трюкядерный трюкEN kernel trickЗамена функции сходства sim(q, k) скалярным произведением признаков φ(q)·φ(k). В линейном внимании это позволяет переставить скобки и получить O(n) вместо O(n²).. Тогда

oi=∑j≤i(φ(qi)⊤φ(kj)) vj∑j≤iφ(qi)⊤φ(kj)=(∑j≤ivj φ(kj)⊤⏞Si) φ(qi)(∑j≤iφ(kj)⏟zi) ⁣⊤φ(qi).o_i = \frac{\sum_{j \le i} \big(\varphi(q_i)^\top \varphi(k_j)\big)\, v_j}{\sum_{j \le i} \varphi(q_i)^\top \varphi(k_j)} = \frac{\Big(\overbrace{\textstyle\sum_{j \le i} v_j\, \varphi(k_j)^\top}^{S_i}\Big)\, \varphi(q_i)}{\Big(\underbrace{\textstyle\sum_{j \le i} \varphi(k_j)}_{z_i}\Big)^{\!\top} \varphi(q_i)} .

Что произошло: число φ(qi)⊤φ(kj)\varphi(q_i)^\top \varphi(k_j) — скаляр, его можно переставить, и φ(qi)\varphi(q_i) выносится из суммы. Сумма SiS_i (матрица dv×dkd_v \times d_k) и ziz_i (вектор) от запроса не зависят, их можно накапливать. Это тот же приём ассоциативности, что и поглощение матриц в MLA: (QK⊤)V=Q(K⊤V)(QK^\top)V = Q(K^\top V). Первая скобка стоит O(n2d)O(n^2 d), вторая — O(nd2)O(n d^2). При n=106n = 10^6 и d=128d = 128 это в 8000 раз меньше.

Katharopoulos и соавторы (2020) взяли φ(x)=elu(x)+1\varphi(x) = \mathrm{elu}(x) + 1: x+1x + 1 при x>0x > 0 и exe^x иначе. Пример: q=(1,−1)q = (1, -1), k=(2,0)k = (2, 0). Softmax-сходство eq⋅k=e2≈7.39e^{q \cdot k} = e^2 \approx 7.39. Линейное: φ(q)=(2,0.37)\varphi(q) = (2, 0.37), φ(k)=(3,1)\varphi(k) = (3, 1), сходство 6+0.37=6.376 + 0.37 = 6.37. Другое число, но тоже положительное и тоже больше для похожих векторов. Статья называлась «Transformers are RNNs» — почему, станет ясно в следующем разделе.

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

1. Параллельная форма (обучение). Как обычное внимание, только без softmax: матрица A=φ(Q)φ(K)⊤A = \varphi(Q)\varphi(K)^\top, причинная маска, нормировка по строкам. Все позиции считаются сразу — удобно для GPU, но снова O(n2)O(n^2).

2. Рекуррентная форма (генерация). На шаге tt:

St=St−1+vt φ(kt)⊤,zt=zt−1+φ(kt),ot=St φ(qt)zt⊤φ(qt).S_t = S_{t-1} + v_t\, \varphi(k_t)^\top, \qquad z_t = z_{t-1} + \varphi(k_t), \qquad o_t = \frac{S_t\, \varphi(q_t)}{z_t^\top \varphi(q_t)} .

Вместо KV-кэша, растущего с каждым токеном, — состояние SS размера dv×dkd_v \times d_k. Для головы 128 это 128⋅128=16 384128 \cdot 128 = 16\,384 чисел, хоть после десяти токенов, хоть после миллиона.

3. Чанковая формачанковая (блочно-параллельная) формаEN chunkwise parallel formПоследовательность режут на блоки — внутри блока внимание считается матрицами (как при обучении), между блоками передаётся рекуррентное состояние. Так устроены Lightning Attention и ядра fla. (обучение на длинных текстах). Последовательность режут на блоки по CC токенов. Для блока bb:

Ob=[(φ(Qb)φ(Kb)⊤)⊙M]Vb⏟внутри блока: матрица C×C  +  φ(Qb) Sb−1⊤⏟все прошлые блоки,Sb=Sb−1+Vb⊤φ(Kb)\begin{gathered} O_b = \underbrace{\big[(\varphi(Q_b)\varphi(K_b)^\top) \odot M\big] V_b}_{\text{внутри блока: матрица } C \times C} \; + \; \underbrace{\varphi(Q_b)\, S_{b-1}^\top}_{\text{все прошлые блоки}}, \\ S_b = S_{b-1} + V_b^\top \varphi(K_b) \end{gathered}

(плюс такая же нормировка). Внутри блока — маленькая матрица на тензорных ядрах, между блоками — состояние. Стоимость O(nCd+nd2)O(n C d + n d^2): линейная по длине, но с хорошей загрузкой GPU. Именно так устроены Lightning Attention и ядра библиотеки flash-linear-attention.

flowchart LR
B1["Блок 1<br/>матрица C×C"] -->|"S₁"| B2["Блок 2<br/>матрица C×C<br/>+ Q₂·S₁"]
B2 -->|"S₂"| B3["Блок 3<br/>матрица C×C<br/>+ Q₃·S₂"]
B3 -->|"S₃"| B4["…"]
Чанковая форма: внутри блока — как трансформер, между блоками — как RNN
ФормаКогдаСтоимость на токенПамять
Параллельнаяобучение на коротких текстахрастёт с nnматрица n×nn \times n
Рекуррентнаягенерацияпостоянная, O(d2)O(d^2)состояние dv×dkd_v \times d_k
Чанковаяобучение и prefill на длинных текстахпостоянная, O(Cd+d2)O(Cd + d^2)состояние + блок C×CC \times C

Память должна забывать: затухание и гейты

Заголовок раздела «Память должна забывать: затухание и гейты»

В формуле выше каждый прошлый токен входит в SS с весом 1 навсегда. Память «засоряется»: через миллион токенов в ней каша из всего подряд. Лекарство — затухание: перед записью нового токена старое состояние умножают на γ<1\gamma \lt 1. Здесь и дальше для краткости пишем kk вместо φ(k)\varphi(k) и опускаем знаменатель — почему его можно убрать, скажем чуть ниже.

St=γ St−1+vtkt⊤⟺O=((QK⊤)⊙D)V,Dij={γ i−j,j≤i0,j>i\begin{gathered} S_t = \gamma\, S_{t-1} + v_t k_t^\top \quad\Longleftrightarrow\quad O = \big((QK^\top) \odot D\big) V, \\ D_{ij} = \begin{cases} \gamma^{\,i-j}, & j \le i \\ 0, & j \gt i \end{cases} \end{gathered}

Слева рекуррентная форма, справа параллельная: маска DD с затуханием по расстоянию. Так устроены RetNet (Microsoft Research Asia, 2023) и Lightning Attention. Затухание заодно кодирует порядок токенов: чем дальше токен, тем меньше его вес.

Следующий шаг — сделать затухание зависящим от входа: αt=σ(…xt)\alpha_t = \sigma(\ldots x_t) вместо константы γ\gamma. Тогда Dij=αj+1αj+2⋯αiD_{ij} = \alpha_{j+1} \alpha_{j+2} \cdots \alpha_i, и модель сама решает, когда забывать — например, на границе документов. Это гейт, как в LSTM; так работают GLA и Mamba2 (модель пространства состояниймодель пространства состояний (SSM)EN state space model (SSM)Семейство рекуррентных моделей с линейным обновлением состояния (S4, Mamba). Близкий родственник линейного внимания.).

Ещё одна деталь. Знаменатель zt⊤φ(qt)z_t^\top \varphi(q_t) из статьи 2020 года оказался источником нестабильности, поэтому в современных вариантах (TransNormer, Lightning, GLA, DeltaNet) его обычно убирают, а после внимания ставят нормализацию слоя.

Lightning AttentionLightning AttentionEN Lightning AttentionЛинейное внимание с тайлингом — маскированная матрица внутри блока и состояние KᵀV между блоками. Основа MiniMax-01 (7 линейных слоёв на 1 softmax). — линейное внимание с затуханием в чанковой форме, реализованное по принципам FlashAttention: блоки грузятся в быструю память GPU, внутриблочная часть считается матрицами, межблочная — через состояние K⊤VK^\top V. Скорость обучения на токен почти не зависит от длины последовательности. (Сам FlashAttention подробно — в уроке 5.7.2.)

MiniMax-01 (январь 2025) стала первой открытой моделью такого масштаба на линейном внимании: 456B параметров, из них 45.9B активных (MoE), 80 слоёв. Чистое линейное внимание плохо ищет по контексту, поэтому архитектура — гибридное вниманиегибридное вниманиеEN hybrid attentionАрхитектура, где большинство слоёв — дешёвые (линейные или со скользящим окном), а каждый k-й слой — полное внимание.: после каждых 7 слоёв Lightning Attention идёт 1 слой обычного softmax-внимания. Модель обучали на контексте 1M токенов, на инференсе она принимает до 4M. MiniMax-M1 (июнь 2025) сохранила гибрид и стала рассуждающей моделью. По оценке авторов, при генерации 100K токенов она тратит около четверти FLOPs DeepSeek-R1.

Почему вообще нужен softmax-слой? Ответ — в ёмкости памяти.

Ёмкость памяти: что теряет фиксированное состояние

Заголовок раздела «Ёмкость памяти: что теряет фиксированное состояние»

Матрица 32×3232 \times 32 — это 1024 числа, но точно хранить она может не больше 32 пар «ключ → значение»: по одной на каждое из 32 ортогональных направлений ключа. Проверим, что происходит дальше. Запишем nn случайных пар в три вида памяти и попробуем по каждому ключу прочитать значение. Метрика — косинус между прочитанным и настоящим значением (1.0 — идеально).

import numpy as np
rng = np.random.default_rng(0)
d = 32 # размер ключа и значения: память S — матрица 32×32
def cos_rows(a, b):
return np.sum(a * b, axis=1) / np.linalg.norm(a, axis=1) / np.linalg.norm(b, axis=1)
def experiment(n):
"""Записываем n пар (ключ, значение), затем по каждому ключу пытаемся прочитать значение."""
K = rng.normal(size=(n, d))
K /= np.linalg.norm(K, axis=1, keepdims=True) # единичные ключи
V = rng.normal(size=(n, d))
logits = 20 * K @ K.T # softmax-внимание: хранит ВСЕ пары (KV-кэш)
w = np.exp(logits - logits.max(axis=1, keepdims=True))
soft = (w / w.sum(axis=1, keepdims=True)) @ V
S_lin, S_delta = np.zeros((d, d)), np.zeros((d, d)) # линейное внимание: память фиксированного размера
for k, v in zip(K, V):
S_lin += np.outer(v, k) # просто прибавляем v kᵀ
S_delta += np.outer(v - S_delta @ k, k) # дельта-правило: пишем только поправку
lin, delta = K @ S_lin.T, K @ S_delta.T # чтение: S k
return cos_rows(soft, V).mean(), cos_rows(lin, V).mean(), cos_rows(delta, V).mean(), cos_rows(delta, V)[-4:].mean()
print(f"{'пар':>4}{'softmax':>9}{'линейное':>10}{'дельта':>8}{'дельта, 4 последних':>21}")
for n in [8, 32, 128, 512]:
s, l, dl, last = experiment(n)
print(f"{n:>4}{s:>9.3f}{l:>10.3f}{dl:>8.3f}{last:>21.3f}")

Ожидаемый вывод:

пар softmax линейное дельта дельта, 4 последних
8 1.000 0.899 0.930 0.987
32 1.000 0.723 0.701 0.959
128 1.000 0.437 0.235 0.979
512 1.000 0.249 0.067 0.951

Softmax-внимание помнит всё, потому что его «память» — весь KV-кэш, растущий с nn. Линейная память 32×3232 \times 32 деградирует, как только пар становится сравнимо с dd: записи накладываются друг на друга. Это фундаментальный компромисс, и его измеряют задачами ассоциативного поиска вроде MQAR (multi-query associative recall). Отсюда гибриды: дешёвые линейные слои плюс немного полного внимания для точного поиска.

Последний столбец показывает другое свойство: дельта-правило держит свежие записи почти идеально (0.95–0.99), даже когда всё остальное забыто. Его память — не куча, куда всё сваливают, а тетрадь, в которой старое переписывают новым.

дельта-правилодельта-правилоEN delta ruleОбновление памяти S ← S + β(v − S k)kᵀ — сначала читаем, что уже записано по ключу k, и записываем только поправку. Старое значение по тому же ключу стирается. пришло из классики нейросетей (правило Уидроу–Хоффа) и в линейное внимание попало через DeltaNet (Schlag и соавторы, 2021):

St=St−1+βt(vt−St−1kt) kt⊤.S_t = S_{t-1} + \beta_t \big(v_t - S_{t-1} k_t\big)\, k_t^\top .

Что здесь написано: St−1ktS_{t-1} k_t — то, что память сейчас выдаёт по ключу ktk_t. Разность vt−St−1ktv_t - S_{t-1}k_t — ошибка. Записываем только её со «скоростью обучения» βt∈(0,1)\beta_t \in (0, 1). При β=1\beta = 1 и единичном kk чтение по ktk_t сразу после записи вернёт ровно vtv_t. Пример: по ключу k=(1,0)k = (1, 0) записали v1=(1,2)v_1 = (1, 2), потом v2=(5,−1)v_2 = (5, -1). Обычное линейное внимание прочитает сумму (6,1)(6, 1), дельта-правило — (5,−1)(5, -1).

Долго дельта-правило считалось только рекуррентно, то есть медленно. В 2024 году Ян Сунлинь (MIT) с соавторами нашли способ параллелить его по длине последовательности. Затем вышла Gated DeltaNetGated DeltaNet (GDN)EN Gated DeltaNetДельта-правило плюс зависящее от входа затухание состояния α_t. Основа Qwen3-Next и Qwen3.5 (три слоя GDN на один слой внимания). (NVIDIA и MIT, ICLR 2025), соединившая затухание и дельта-правило:

St=αt St−1(I−βtktkt⊤)+βt vtkt⊤.S_t = \alpha_t\, S_{t-1}\big(I - \beta_t k_t k_t^\top\big) + \beta_t\, v_t k_t^\top .

αt\alpha_t отвечает за быстрое забывание всего (новая тема), βt\beta_t — за точную перезапись по одному ключу. Kimi Delta AttentionKimi Delta Attention (KDA)EN Kimi Delta Attention (KDA)Вариант Gated DeltaNet от Moonshot с поканальным (а не скалярным) затуханием. Kimi Linear чередует 3 слоя KDA с 1 слоем MLA. (KDA) от Moonshot идёт дальше: вместо скаляра αt\alpha_t — диагональная матрица Diag(αt)\mathrm{Diag}(\alpha_t), то есть каждый канал памяти забывает со своей скоростью.

Qwen3-Next-80B-A3B (сентябрь 2025): 80B параметров, 3B активных, 48 слоёв по схеме 12×(3×Gated DeltaNet+1×gated attention)12 \times (3 \times \text{Gated DeltaNet} + 1 \times \text{gated attention}), после каждого — MoE с 512 экспертами (10 маршрутизируемых и 1 общий). Нативный контекст 262 144 токена, с YaRN (урок 5.5.1) — около миллиона. По данным Qwen, базовая модель обходит Qwen3-32B-Base, потратив на обучение около 10% её стоимости, а на контекстах больше 32K даёт примерно в 10 раз большую пропускную способность. В феврале 2026 года та же схема 3:1 легла в основу флагмана Qwen3.5.

flowchart LR
subgraph R["Повтор × 12"]
  direction LR
  G1["Gated DeltaNet<br/>+ MoE"] --> G2["Gated DeltaNet<br/>+ MoE"] --> G3["Gated DeltaNet<br/>+ MoE"] --> A["Gated attention<br/>16 Q / 2 KV голов<br/>+ MoE"]
end
Qwen3-Next: на три линейных слоя — один слой полного внимания с выходным гейтом

Что это даёт по памяти (по конфигурации из карточки модели): слой внимания с 2 KV-головами по 256 хранит 1024 числа на токен. 12 таких слоёв на контексте 262K — 6 ГиБ кэша в BF16 на последовательность; если бы все 48 слоёв были вниманием, было бы 24 ГиБ. Состояния 36 слоёв Gated DeltaNet (32 головы по 128×128128 \times 128) занимают 72 МиБ в FP32 — при любой длине. Ещё одна деталь слоёв Gated DeltaNet (и KDA у Kimi): перед вычислением запросов, ключей и значений стоит короткая причинная depthwise-свёртка по времени с ядром 4 (linear_conv_kernel_dim = 4 в конфигурации Qwen3-Next). Она дёшево подмешивает к каждому токену три предыдущих; как устроены такие свёртки — в уроке 3.5.1.

Gated attentionвнимание с выходным гейтом (gated attention)EN gated attentionПосле softmax-внимания выход каждой головы поэлементно умножается на сигмоидный гейт σ(xW). Убирает attention sink; работа Qwen — лучшая статья NeurIPS 2025. — отдельная работа Qwen (arXiv:2505.06708), получившая награду за лучшую статью NeurIPS 2025. После обычного внимания выход каждой головы поэлементно умножают на сигмоидный гейт от входа: Y=SDPA(Q,K,V)⊙σ(XWg)Y = \mathrm{SDPA}(Q, K, V) \odot \sigma(X W_g). Авторы сравнили более 30 вариантов на MoE-моделях 15B и плотных 1.7B, обученных на 3.5 трлн токенов. Гейт добавляет нелинейность между WVW_V и WOW_O, делает внимание разреженнее и убирает attention sink (урок 4.4.2, прошлый урок): голове больше не нужно сливать лишнюю массу на первый токен, она может просто «закрыть» свой выход. По данным статьи, в базовой модели на первый токен в среднем приходилось 46.7% внимания, с гейтом — 4.8%. Заодно обучение стало стабильнее и терпимее к большому learning rate.

Kimi Linear (октябрь 2025): 48B параметров, 3B активных, KDA и MLA в пропорции 3:1. Слои MLA работают без позиционного кодирования (NoPE): порядок токенов несут рекуррентные слои KDA. По данным Moonshot, на контексте 1M модель расходует на KV-кэш до 75% меньше памяти и декодирует до 6 раз быстрее, чем такая же модель на полном MLA, а в честном сравнении на одинаковых данных обходит её по качеству. Ядро KDA открыто в библиотеке fla. В 2026 году эта линия дошла до флагмана Kimi K3.

Честный разговор: почему MiniMax вернулась к полному вниманию

Заголовок раздела «Честный разговор: почему MiniMax вернулась к полному вниманию»

В октябре 2025 года MiniMax выпустила M2 (230B-A10B) — и в ней полное внимание. Команда, которая первой масштабировала линейное внимание до 456B, отказалась от него. В блоге «Why Did M2 End Up as a Full Attention Model?» объяснено почему, а позже те же доводы вошли в техотчёт серии M2 (arXiv:2605.26494).

1. Качество там, где не смотрели. Во время работы над MiniMax-Text-01 гибрид Lightning и полного внимания выглядел не хуже чистого полного внимания на MMLU, BBH, MATH, LongBench (тесты на знания, рассуждения, школьную и олимпиадную математику и длинный контекст). Но на большем масштабе проявились явные провалы в сложных многошаговых рассуждениях — то есть ровно там, где нужен точный поиск по контексту из нашего эксперимента с памятью.

2. Оценка — узкое место. Стандартные бенчмарки насыщены и не ловят такие слабости. Команда построила прокси-метрики для многошагового поиска, но нельзя быть уверенным, что на ещё большем масштабе они продолжат предсказывать реальные задачи и что нет других скрытых слабостей. Проверить это — значит обучить большую модель целиком.

3. Инфраструктура не готова. Рекуррентные и сжатые состояния чувствительны к пониженной точности. У многих реализаций нет нормального prefix cachingпрефикс-кэш (кэширование префиксов)EN prefix cachingЕсли начало нового запроса совпадает с уже обработанным (системный промпт, история диалога), его KV-кэш берут готовым и prefill делают только для «хвоста». (повторного использования KV-кэша для общего начала разных запросов, урок 7.1.2) — а агенты для кода прогоняют сотни траекторий с одним и тем же репозиторием в контексте, и переиспользование кэша для них критично. Неясно, как сочетать такие слои со спекулятивным декодированием.

Вывод MiniMax: в промышленной системе эффективное внимание пока не обыгрывает полное однозначно, и выигрыш в скорости не окупил рисков для агентных задач. В июне 2026 года, в M3, MiniMax вернулась к экономии — но выбрала не линейное, а разреженное внимание MSA.

При этом Qwen и Moonshot в те же месяцы сделали ставку на гибриды и подтвердили её флагманами Qwen3.5 и Kimi K3. Обе стороны опираются на эксперименты, и спор не закрыт. Что из него стоит вынести инженеру:

Полное (с GQA или MLA)Разреженное (NSA, DSA)Гибрид с линейным
Точный поиск по контекстулучшийпочти как у полного, если отбор хорошийтолько в полных слоях
KV-кэшрастёт с nnрастёт с nn (сжатие помогает)растёт только в полных слоях (в 4–8 раз медленнее)
Вычисления на токенрастут с nnрастут медленно: дорогое внимание — по kk ключам, дешёвый отбор — по всемрастут только в полных слоях
Зрелость инфраструктурывысокаясредняяниже
  • Бенчмарки молчат о том, чего не измеряют. Проверяйте архитектуру на целевой задаче: агенты, многошаговый поиск, длинный код.
  • Инфраструктура — часть архитектуры. Prefix caching, квантование и спекулятивное декодирование решают не меньше, чем loss.
  • Гибриды держат полные слои не случайно: именно они отвечают за точное «вспоминание».

Реализуйте линейное внимание тремя способами — параллельно, рекуррентно и блоками, как Lightning Attention, — и убедитесь, что выходы совпадают, а состояние не растёт с длиной.

Три формы линейного вниманияnumpy · нужен интернет6 тестов
import numpy as np


def feature_map(x):
    """Отображение признаков линейного внимания: phi(x) = elu(x) + 1.

    Поэлементно: x + 1 при x > 0 и exp(x) при x <= 0. Результат всегда положителен,
    поэтому «веса» phi(q)·phi(k) тоже положительны — как у softmax.
    """
    # TODO: np.where(условие, если_да, если_нет)
    ...


def linear_attention_parallel(Q, K, V):
    """Причинное линейное внимание в параллельной форме (как при обучении).

    Q, K: (T, d_k), V: (T, d_v).
    A = phi(Q) @ phi(K).T, обнулить будущее (выше диагонали);
    выход строки i: (A[i] @ V) / A[i].sum().
    """
    # TODO
    ...


def linear_attention_recurrent(Q, K, V):
    """Та же функция в рекуррентной форме (как RNN при генерации).

    Состояние: S (d_v, d_k) = сумма outer(v_j, phi(k_j)) и z (d_k,) = сумма phi(k_j) по j <= t.
    На шаге t: сначала обновить S и z токеном t, затем
        out[t] = S @ phi(q_t) / (z @ phi(q_t)).
    Возвращает (out, S, z) — выход (T, d_v) и состояние после последнего токена.
    """
    T, d_k = Q.shape
    d_v = V.shape[1]
    S = np.zeros((d_v, d_k))
    z = np.zeros(d_k)
    out = np.zeros((T, d_v))
    # TODO: цикл по t
    ...
    return out, S, z


def linear_attention_chunked(Q, K, V, C):
    """Чанковая форма (идея Lightning Attention): блоки по C токенов.

    Внутри блока — параллельная форма с причинной маской,
    между блоками — состояние S, z от всех предыдущих блоков.
    Для блока rows = [a, b):
        A_in = tril(phi(Q_b) @ phi(K_b).T)
        числитель = A_in @ V_b + phi(Q_b) @ S.T
        знаменатель = A_in.sum(1) + phi(Q_b) @ z
    После блока: S += V_b.T @ phi(K_b), z += phi(K_b).sum(0).
    Последний блок может быть короче C.
    """
    # TODO
    ...


# Проверьте себя кнопкой «Запустить»:
rng = np.random.default_rng(0)
Q, K, V = rng.normal(size=(3, 6, 4))
print(feature_map(np.array([-1.0, 0.0, 2.0])))     # ожидается [0.3679 1. 3.]
p = linear_attention_parallel(Q, K, V)
r, S, z = linear_attention_recurrent(Q, K, V)
c = linear_attention_chunked(Q, K, V, 4)
if p is not None and c is not None:
    print("параллельная = рекуррентная:", np.allclose(p, r))
    print("параллельная = чанковая:", np.allclose(p, c))
print("размер состояния:", S.shape)               # (4, 4) — не зависит от длины

Затухание: рекуррентная и параллельная формы

Заголовок раздела «Затухание: рекуррентная и параллельная формы»

Проверим формулу с γ\gamma из раздела о затухании: RNN-шаги и одна матричная операция с маской DD дают одно и то же.

import numpy as np
rng = np.random.default_rng(0)
T, d = 6, 4
Q, K, V = rng.normal(size=(3, T, d)) # массив (3, T, d) распаковывается по первой оси в три матрицы (T, d)
gamma = 0.9 # затухание: старое «забывается» на 10% за шаг
# Рекуррентная форма: S_t = γ·S_{t-1} + v_t k_tᵀ, o_t = S_t q_t
S = np.zeros((d, d))
out_rec = np.zeros((T, d))
for t in range(T):
S = gamma * S + np.outer(V[t], K[t])
out_rec[t] = S @ Q[t]
# Параллельная форма: O = ((Q Kᵀ) ⊙ D) V, D[i, j] = γ^(i-j) при j <= i, иначе 0
i, j = np.arange(T)[:, None], np.arange(T)[None, :]
D = np.where(j <= i, gamma ** (i - j), 0.0)
out_par = ((Q @ K.T) * D) @ V
print("макс. расхождение форм:", f"{np.abs(out_rec - out_par).max():.1e}")
print("маска затухания D:")
print(D.round(2))

Ожидаемый вывод:

макс. расхождение форм: 1.1e-15
маска затухания D:
[[1. 0. 0. 0. 0. 0. ]
[0.9 1. 0. 0. 0. 0. ]
[0.81 0.9 1. 0. 0. 0. ]
[0.73 0.81 0.9 1. 0. 0. ]
[0.66 0.73 0.81 0.9 1. 0. ]
[0.59 0.66 0.73 0.81 0.9 1. ]]

Маска DD — та же причинная маска, только вместо единиц ниже диагонали стоят степени γ\gamma. С гейтом αt\alpha_t вместо константы в клетках будут произведения α\alpha, а с дельта-правилом формула усложняется, но идея «матрица при обучении, рекурсия при генерации» сохраняется.

Проверь себя6 вопросов
1 Сколько операций (по порядку величины) нужно, чтобы посчитать Q(K⊤V)Q(K^\top V) для nn токенов и размерности dd?
2 Сколько чисел хранит рекуррентное состояние SS одной головы линейного внимания при dk=dv=128d_k = d_v = 128 после миллиона токенов?
3 Какие утверждения о чанковой форме (Lightning Attention) верны?Можно выбрать несколько ответов
4 Память SS, ключ k=(1,0)k = (1, 0). Записали v1=(1,2)v_1 = (1, 2), затем v2=(5,−1)v_2 = (5, -1) по дельта-правилу с β=1\beta = 1. Что вернёт чтение SkS k?
5 Почему MiniMax в M2 вернулась к полному вниманию?
6 В каких моделях часть слоёв — линейное внимание?Можно выбрать несколько ответов
  • Ядерный трюк eq⋅k→φ(q)⋅φ(k)e^{q \cdot k} \to \varphi(q)\cdot\varphi(k) позволяет переставить скобки: (QK⊤)V=Q(K⊤V)(QK^\top)V = Q(K^\top V), и внимание становится линейным по длине.
  • Одно вычисление — три формы: параллельная для обучения, рекуррентная для генерации (состояние dv×dkd_v \times d_k вместо KV-кэша) и чанковая (Lightning Attention) для длинных текстов.
  • Фиксированная память ограничена по ёмкости. Затухание и гейты учат её забывать, дельта-правило — перезаписывать; Gated DeltaNet и KDA объединяют оба приёма.
  • Гибриды 3:1 (Qwen3-Next, Kimi Linear) и 7:1 (MiniMax-01) держат немного полного внимания для точного поиска. MiniMax в M2 вернулась к полному вниманию из-за многошаговых рассуждений, сложности оценки и незрелой инфраструктуры — спор не закрыт.

Дальше: модуль 5.6 — соберём всё, что знаем об архитектуре и обучении, в собственную маленькую LLM по мотивам MiniMind.