Разреженное внимание — NSA и DSA
Как перестать платить O(n²) за длинный контекст — скользящее окно и «стоки» внимания, три ветви NSA, молниеносный индексатор DSA из DeepSeek-V3.2 и куда разреженное внимание движется в 2026 году.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В прошлом уроке мы научили модель не ломаться на 128K токенов. Но платить за такой контекст всё ещё дорого. На 128K одна голова одного слоя при prefill считает оценок внимания (scores, ; дальше будем коротко называть их «скорами») (с причинной маской вдвое меньше, но порядок тот же — квадратичная сложность вниманияквадратичная сложность вниманияEN quadratic complexity (O(n²))Каждый из n токенов сравнивается с каждым — n² оценок на голову. При длинном контексте это главная стоимость внимания; отсюда FlashAttention, разреженное и линейное внимание.). При генерации каждый новый токен читает из памяти весь KV-кэш, и чем длиннее диалог агента, тем медленнее и дороже каждый следующий токен.
При этом почти вся «работа» внимания приходится на немногие ключи. Если бы мы заранее знали, какие ключи важны, можно было бы считать внимание только по ним. Это и есть разреженное вниманиеразреженное вниманиеEN sparse attention.
В 2025 году DeepSeek сделала это дважды. В феврале вышла исследовательская статья NSA (лучшая статья ACL 2025). В сентябре в продакшен пошла DSA в DeepSeek-V3.2-Exp, и компания снизила цены API более чем вдвое. В 2026 году DSA позаимствовал GLM-5, а разреженное и сжатое внимание стало нормой у моделей с контекстом в миллион токенов.
Где внимание разрежено само по себе
Заголовок раздела «Где внимание разрежено само по себе»Если нарисовать веса внимания обученной модели на длинном тексте, почти всегда видны три шаблона:
- Локальный. Токен смотрит на ближайших соседей — это синтаксис, согласование слов, продолжение фразы.
- «Сток». Огромный вес у самых первых токенов, даже если они ничего не значат (например,
<|im_start|>). - Дальние «иголки». Несколько мест далеко в прошлом, где лежит нужное: имя героя, определение переменной, условие задачи.
Всё остальное — тысячи ключей с почти нулевыми весами. Отсюда две идеи разрежения: фиксированный шаблон (окно, первые токены) и отбор по содержанию (найти «иголки» для конкретного запроса).
flowchart LR A["Полное внимание<br/>все n ключей"] --> B["Фиксированный шаблон<br/>окно + первые токены"] A --> C["Отбор по содержанию<br/>top-k блоков или токенов"] B --> D["Mistral, Gemma, MiMo<br/>StreamingLLM"] C --> E["NSA, MoBA, DSA<br/>MSA (MiniMax)"]
Скользящее окно и «стоки» внимания
Заголовок раздела «Скользящее окно и «стоки» внимания»Внимание со скользящим окномвнимание со скользящим окном (SWA)EN sliding window attention (SWA)Токен видит только w последних токенов. Стоимость O(n·w) вместо O(n²), KV-кэш не больше w позиций на слой. (SWA) — самый простой шаблон: токен видит только последних токенов. Стоимость падает с до , а KV-кэш не превышает позиций на слой: старые записи вытесняются по кругу (кольцевой буфер).
Mistral 7B (2023) использовала окно . Окна разных слоёв «складываются»: на втором слое токен видит токены, которые сами видели на 4096 назад. Через 32 слоя теоретический охват — около токенов, как рецептивное поле в свёрточной сети (урок 3.4.3). Но передача информации «по цепочке» через слои — с потерями: факт, лежащий за окном, нельзя прочитать напрямую.
Поэтому чистое окно сегодня почти не используют. Используют гибрид: несколько слоёв с окном, затем один слой полного внимания. У Gemma 3 — пять локальных слоёв с окном 1024 на один глобальный. У Xiaomi MiMo-V2-Flash — пять слоёв с окном всего в 128 токенов на один глобальный: KV-кэш почти не зависит от длины контекста.
«Сток» внимания
Заголовок раздела ««Сток» внимания»Сток внимания«сток» внимания (attention sink)EN attention sinkПервые токены получают аномально большой вес внимания независимо от смысла — туда softmax «сливает» лишнюю массу. Не путайте со сходным китайским термином из d2l-zh («схождение внимания»): там так переведено attention pooling (взвешенное усреднение значений). вы уже встречали в уроке 4.4.2: веса softmax обязаны суммироваться в единицу, и когда голове «не на что смотреть», она сливает лишнюю массу на первые токены — они видны всем последующим. Для окна это критично. В работе StreamingLLM (MIT и Meta, 2023) генерировали бесконечно длинный текст с окном и выбрасывали самые старые токены: perplexity взрывалась ровно в момент, когда из окна уходили первые токены. Выбросили сток — веса распределились по случайным токенам, и генерация сломалась.
Лечение простое: держать в кэше 4 первых токена плюс окно. С этим трюком Llama 2 и другие модели стабильно генерировали до 4 миллионов токенов. NSA, как мы увидим, тоже всегда оставляет начальный блок. А в следующем уроке вы узнаете, как Qwen убрала сток архитектурно — выходным гейтом.
Отбор по содержанию: три проблемы
Заголовок раздела «Отбор по содержанию: три проблемы»Идея кажется простой: для каждого запроса найти top- самых релевантных ключей и считать внимание только по ним. Но на пути три проблемы.
1. Чтобы найти лучших, нужны скоры всех. Честные скоры для всех ключей — это уже , ровно то, от чего мы бежим. Выход: оценивать грубо — по сжатым блокам (NSA, MoBA) или маленьким дешёвым индексатором (DSA).
2. GPU не любит «дырявые» данные. Прочитать 1024 случайных токена из памяти намного медленнее, чем 16 непрерывных кусков по 64 токена. Тензорные ядра тоже работают с плитками. Поэтому на практике выбирают блоки: блочно-разреженное вниманиеблочно-разреженное вниманиеEN block-sparse attentionЗапрос выбирает не отдельные токены, а целые блоки ключей (например, по 64). Так удобнее GPU — память читается непрерывными кусками..
3. Разрежение после обучения ломает выученное. Можно взять обычную модель и разредить внимание только на инференсе. Так работают MInference, Quest и похожие методы; Qwen2.5-1M использует такой подход для ускорения prefill на миллионе токенов. Но модель училась на полном внимании и рассчитывает на веса, которые мы выбросили. К тому же обучение такие методы не ускоряют. Отсюда слово Native в NSA: модель с самого начала учится с разреженным вниманием и подстраивается под него.
NSA: три ветви
Заголовок раздела «NSA: три ветви»Native Sparse AttentionNative Sparse Attention (NSA)EN Native Sparse Attention (NSA)Разреженное внимание DeepSeek (2025) — три ветви (сжатые блоки, отобранные top-n блоки, скользящее окно), смешанные гейтом; обучается с нуля. (DeepSeek, Пекинский университет и Вашингтонский университет, февраль 2025) даёт каждому запросу три взгляда на прошлое и смешивает их:
Здесь — ключи и значения, которые ветвь отобрала или построила, а гейты считает маленькая сеть с сигмоидой по входу. У каждой ветви свои проекции K и V.
flowchart TB Q["Запрос q_t"] --> CMP["Сжатая ветвь<br/>блоки 32 токена, шаг 16<br/>→ ~t/16 сжатых ключей"] Q --> SLC["Ветвь отбора<br/>16 лучших блоков по 64 токена"] Q --> WIN["Окно<br/>последние 512 токенов"] CMP -->|"скоры сжатых блоков<br/>подсказывают, где важное"| SLC CMP --> G["Сумма с гейтами g_cmp, g_slc, g_win"] SLC --> G WIN --> G G --> O["Выход o_t"]
Сжатая ветвь (compression). Прошлое режется на блоки по токена с шагом (блоки перекрываются). Каждый блок обучаемая MLP сворачивает в один ключ и одно значение. На позиции таких «конспектов» около . Это грубая карта всего контекста.
Ветвь отбора (selection). Внимание запроса к сжатым ключам уже говорит, где важное. Эти веса пересчитываются в оценки крупных блоков по токена, и берутся лучших (среди них всегда начальный блок и два локальных). По этим токенам считается точное внимание. Отдельный индексатор не нужен: оценки достаются бесплатно из сжатой ветви. Все головы одной GQA-группы (урок 5.2.1) выбирают одни и те же блоки, поэтому каждый блок KV читается из памяти один раз на всю группу.
Окно (sliding window). Последние токенов. Зачем отдельная ветвь, если соседей и так найдут? Локальные зависимости учатся легче всего, и без отдельного окна они «перетягивают» обучение остальных ветвей на себя. Выделенное окно освобождает сжатие и отбор для дальних связей.
Сколько это экономит
Заголовок раздела «Сколько это экономит»Посчитаем ключи, которые читает один запрос: сжатых, отобранных и в окне.
| Контекст | Полное внимание | NSA | Во сколько раз меньше чтений |
|---|---|---|---|
| 8K | 8 192 | ≈ 2 048 | 4.0 |
| 16K | 16 384 | ≈ 2 560 | 6.4 |
| 32K | 32 768 | ≈ 3 584 | 9.1 |
| 64K | 65 536 | ≈ 5 632 | 11.6 |
Декодирование упирается в чтение KV-кэша из памяти (урок 4.6.4), поэтому ожидаемое ускорение равно сокращению чтений. Авторы NSA оценивают ускорение декодирования так же, по объёму чтений, — и получают ту же таблицу: до 11.6 раза на 64K. При обучении на 64K их ядро на Triton (языке для написания GPU-ядер на Python-подобном синтаксисе, урок 5.7.3), по замерам авторов, ускорило прямой проход до 9 раз, обратный — до 6 раз.
По качеству 27-миллиардная MoE-модель с NSA, по данным статьи, не уступила такой же модели с полным вниманием на общих бенчмарках, обошла её на длинноконтекстных задачах и нашла «иголку» на всех позициях контекста 64K.
MoBA — тот же замысел у Moonshot
Заголовок раздела «MoBA — тот же замысел у Moonshot»В тот же день, когда DeepSeek анонсировала NSA (18 февраля 2025), Moonshot AI опубликовала MoBA (Mixture of Block Attention). Идея — применить принцип MoE (урок 5.3) к вниманию: блоки KV — это «эксперты», а запрос через гейт выбирает top- блоков по скалярному произведению со средним ключом блока. Свой текущий блок виден всегда. MoBA позволяет переключать слои между полным и разреженным вниманием, и, по словам авторов, она уже обслуживала длинноконтекстные запросы Kimi.
DSA: молниеносный индексатор
Заголовок раздела «DSA: молниеносный индексатор»DeepSeek Sparse AttentionDeepSeek Sparse Attention (DSA)EN DeepSeek Sparse Attention (DSA)Разреженное внимание DeepSeek-V3.2 — дешёвый индексатор выбирает 2048 прошлых токенов для каждого запроса, полное MLA считается только по ним. Позже взято в GLM-5. появилась в DeepSeek-V3.2-Exp (29 сентября 2025). Это та же V3.1-Terminus, дообученная с единственным изменением архитектуры — DSA. Механизм состоит из двух частей: индексатора и отбора токенов.
Lightning indexerмолниеносный индексатор (lightning indexer)EN lightning indexerМаленький модуль внимания в DSA (несколько голов, FP8, ReLU вместо softmax), который оценивает все прошлые токены, чтобы выбрать top-k. — крошечная модель внимания, которая для запроса оценивает каждый прошлый токен :
Что здесь написано: у индексатора маленьких голов. Каждая сравнивает свой запрос с одним общим ключом прошлого токена, отрицательные значения обрезает ReLU, а результаты складываются с весами , которые тоже зависят от запроса. Нет softmax, мало голов, всё в FP8 — поэтому индексатор и «молниеносный».
Отбор. Для каждого запроса берутся 2048 токенов с наибольшим , и обычное MLA считается только по ним. DSA выбирает отдельные токены, а не блоки, и проблема №2 здесь мягче. MLA на инференсе работает в режиме MQA (урок 5.2.2): все 128 голов делят один латентный вектор на токен. Один отбор обслуживает все головы, и каждый выбранный токен — это непрерывные 576 чисел в памяти.
Сложность основного внимания падает с до . Индексатор по-прежнему смотрит на все токены, то есть формально остаётся , но каждая его пара стоит в разы дешевле. На 128K основное внимание читает ключей.
Как обучали индексатор
Заголовок раздела «Как обучали индексатор»Обучение шло в два этапа, оба на последовательностях длиной 128K:
- Dense warm-up. Вся модель заморожена, работает полное внимание. Обучается только индексатор: его распределение должно повторять настоящее распределение внимания (сумма по всем головам, нормированная). Loss — KL-дивергенция из урока 1.3.3. 1000 шагов по 16 последовательностей — 2.1 млрд токенов.
- Sparse training. Включается отбор top-2048, обучаются все параметры. Основная модель учится на обычном loss языковой модели, индексатор — на той же KL, но только по выбранным токенам; градиенты двух целей не смешиваются. 15 000 шагов по 480 последовательностей — 943.7 млрд токенов.
Итог: по бенчмаркам V3.2-Exp на уровне V3.1-Terminus, а стоимость обработки длинного контекста заметно ниже. Финальная DeepSeek-V3.2 (декабрь 2025) сохранила DSA.
| Скользящее окно | NSA | MoBA | DSA | |
|---|---|---|---|---|
| Что выбирает | последние токенов | 16 блоков + сжатие + окно | top- блоков | top-2048 токенов |
| Чем оценивает | ничем (шаблон) | скоры сжатой ветви | средний ключ блока | lightning indexer |
| Обучение | с нуля или дообучение | с нуля (native) | с нуля или дообучение | дообучение из плотной модели |
| Где | Mistral, Gemma, MiMo | исследование DeepSeek | Kimi | DeepSeek-V3.2, GLM-5 |
Тренд 2026: сжатие плюс отбор
Заголовок раздела «Тренд 2026: сжатие плюс отбор»Три идеи NSA — сжать дальнее, точно прочитать важное, всегда видеть ближнее — пережили саму NSA и разошлись по индустрии в разных сочетаниях. Похожий принцип Су Цзяньлинь сформулировал для позиций, разбирая экстраполяцию по длине: «ближнее сохранить, дальнее сжать» (внутри окна точные расстояния, за ним — сжатые). В разреженном внимании та же логика применяется уже к самим ключам.
- DeepSeek-V4 (апрель 2026, контекст 1M). По техотчёту, слои двух типов чередуются: CSA сжимает KV в 4 раза по оси токенов и поверх выбирает top- индексатором, HCA сжимает в 128 раз, но внимание по сжатому плотное. Ближнее оба типа видят без сжатия — через ветвь скользящего окна.
- MiniMax M3 (июнь 2026) — MiniMax Sparse Attention: индекс выбирает top- блоков KV отдельно для каждой GQA-группы (16 блоков по 128 токенов, то есть 2048 токенов). По аннотации статьи, на модели 109B при контексте 1M вычисления внимания на токен падают в 28.4 раза при качестве на уровне GQA.
- Qwen3.8-Next (август 2026) соединяет обе линии модуля: на три слоя линейного Gated DeltaNet (о нём — следующий урок) приходится слой разреженного Qwen Sparse Attention. У LongCat-2.0 (Meituan, июнь 2026) — своё LongCat Sparse Attention.
Подробнее о фронтире — в уроке 9.2.1.
Практика
Заголовок раздела «Практика»Задание в браузере
Заголовок раздела «Задание в браузере»Реализуйте маски скользящего окна и top-k (как в DSA) и внимание по произвольной маске. Тесты проверят предельные случаи: окно шире последовательности и должны давать обычное причинное внимание.
import numpy as np
def sliding_window_mask(T, w):
"""Маска внимания со скользящим окном.
Возвращает булев массив (T, T): mask[i, j] = True, если запрос i видит ключ j.
Условия: ключ не из будущего (j <= i) и не дальше окна (i - j < w).
Пример для T=4, w=2:
[[1 0 0 0]
[1 1 0 0]
[0 1 1 0]
[0 0 1 1]]
"""
# TODO: удобно построить i = np.arange(T)[:, None] и j = np.arange(T)[None, :]
...
def topk_mask(scores, k):
"""Маска top-k по оценкам индексатора (как в DeepSeek Sparse Attention).
scores: (T, T) — оценки важности ключа j для запроса i (чем больше, тем важнее).
Для каждой строки i выберите среди РАЗРЕШЁННЫХ ключей j <= i те k, у которых оценка
больше всего (если ключей меньше k — берите все). Будущее не выбирать никогда.
Возвращает булев массив (T, T).
"""
T = scores.shape[0]
mask = np.zeros((T, T), dtype=bool)
# TODO: для каждой строки i возьмите scores[i, : i + 1] и найдите индексы k наибольших
# (np.argsort(...)[-k:] — индексы k наибольших по возрастанию)
...
return mask
def sparse_attention(Q, K, V, mask):
"""Внимание только по разрешённым парам (запрос i, ключ j), где mask[i, j] = True.
Q, K: (T, d), V: (T, d_v), mask: (T, T) булев. Масштаб 1/sqrt(d).
Запрещённые пары получают -inf до softmax. Возвращает (T, d_v).
"""
# TODO: scores = Q @ K.T / sqrt(d); np.where(mask, scores, -np.inf);
# устойчивый softmax по строкам (вычтите максимум строки); веса @ V
...
# Проверьте себя кнопкой «Запустить»:
m = sliding_window_mask(5, 2)
print(m if m is None else m.astype(int))
rng = np.random.default_rng(0)
s = rng.normal(size=(5, 5))
tm = topk_mask(s, 2)
print(tm.astype(int)) # в каждой строке не больше 2 единиц, все не выше диагонали
Q, K, V = rng.normal(size=(3, 5, 4))
out = sparse_attention(Q, K, V, np.tril(np.ones((5, 5), dtype=bool)))
print(None if out is None else out.shape) # (5, 4)
«Иголка в стоге сена» на NumPy
Заголовок раздела ««Иголка в стоге сена» на NumPy»Сравним четыре стратегии на одном запросе. В «стоге» 8192 случайных ключа, на позициях 3000–3015 спрятана «иголка» — 16 токенов, связанных с запросом. Метрика — косинус между выходом стратегии и выходом полного внимания (1.0 — совпадение).
import numpy as np
rng = np.random.default_rng(0)T, d = 8192, 64 # 8K прошлых токенов, голова размера 64q = rng.normal(size=d) # запрос последнего токенаK = rng.normal(size=(T, d)) # ключи «стога сена»V = rng.normal(size=(T, d))needle = np.arange(3000, 3016) # «иголка»: 16 токенов подряд, связанных с запросомK[needle] = 8 * np.sqrt(d) * q / (q @ q) + 0.1 * rng.normal(size=(16, d)) # скор ≈ 8V[needle] = 3.0 # и их значения узнаваемы
def attend(idx): """Обычное внимание запроса q только по ключам с номерами idx.""" s = K[idx] @ q / np.sqrt(d) w = np.exp(s - s.max()) w /= w.sum() return w @ V[idx], w[np.isin(idx, needle)].sum() # np.isin: какие из idx входят в needle → их суммарный вес
full, w_full = attend(np.arange(T))print(f"вес иголки при полном внимании: {w_full:.2f}")print(f"{'метод':<24}{'ключей':>7}{'доля':>8}{'cos с полным':>14}")
def report(name, idx): out, _ = attend(idx) cos = out @ full / np.linalg.norm(out) / np.linalg.norm(full) print(f"{name:<24}{len(idx):>7}{len(idx) / T:>8.1%}{cos:>14.3f}")
w = 512report("окно 512", np.arange(T - w, T)) # только последние 512 токеновreport("случайные 256 ключей", np.sort(rng.choice(T, 256, replace=False)))
B, n_sel = 64, 16 # NSA-подобный отбор: блоки по 64, берём 16 лучшихblocks = K.reshape(T // B, B, d).mean(axis=1) # «сжатые» ключи — среднее по блокуbest = np.argsort(blocks @ q)[-n_sel:] # top-16 блоков по сжатому скоруsel = np.concatenate([np.arange(b * B, (b + 1) * B) for b in best])report("top-16 блоков + окно", np.union1d(sel, np.arange(T - w, T))) # union1d — объединение без повторов
P = rng.normal(size=(d, 16)) / 4 # DSA-подобный индексатор: проекция в 16 измеренийidx_score = np.maximum((K @ P) @ (q @ P), 0) # ReLU, как в lightning indexerreport("индексатор, top-256", np.sort(np.argsort(idx_score)[-256:]))Ожидаемый вывод:
вес иголки при полном внимании: 0.79метод ключей доля cos с полнымокно 512 512 6.2% 0.093случайные 256 ключей 256 3.1% 0.004top-16 блоков + окно 1408 17.2% 1.000индексатор, top-256 256 3.1% 1.000Окно не видит иголку — косинус около нуля. Случайные 256 ключей тоже промахиваются: разрежение работает, только если выбирать по содержанию. Блочный отбор по сжатым ключам (1408, а не 1536 ключей: два лучших блока попали в окно) и даже грубый индексатор в 16 измерениях с ReLU находят иголку и дают тот же ответ, что и полное внимание, читая 3–17% ключей.
Пример специально «удобный»: внимание здесь сосредоточено на иголке (0.79 всего веса). Если веса размазаны ровно, никакой отбор не спасёт, и ответ будет отличаться. Поэтому NSA и DSA проверяют на реальных длинноконтекстных задачах.
Разреженная маска на PyTorch: FlexAttention
Заголовок раздела «Разреженная маска на PyTorch: FlexAttention»В PyTorch с версии 2.5 есть FlexAttention: маску задают функцией от индексов, а библиотека сама строит блочную маску и пропускает пустые блоки. Вот окно со «стоком»:
import warnings
import torchimport torch.nn.functional as Ffrom torch.nn.attention.flex_attention import create_block_mask, flex_attention
warnings.filterwarnings("ignore") # на CPU без torch.compile Flex предупреждает, что считает «в лоб»torch.manual_seed(0)B, H, T, D = 1, 4, 1024, 64WINDOW, SINK = 256, 4q, k, v = (torch.randn(B, H, T, D) for _ in range(3))
def window_with_sink(b, h, q_idx, kv_idx): """Причинное окно + первые SINK токенов («сток» внимания) видны всегда.""" causal = kv_idx <= q_idx near = q_idx - kv_idx < WINDOW return causal & (near | (kv_idx < SINK))
# FlexAttention режет матрицу на блоки 128×128 и пропускает пустые блоки целикомblock_mask = create_block_mask(window_with_sink, B=None, H=None, Q_LEN=T, KV_LEN=T, device="cpu")out_flex = flex_attention(q, k, v, block_mask=block_mask)
# Проверка: то же самое через обычную плотную маскуi, j = torch.arange(T)[:, None], torch.arange(T)[None, :]dense = (j <= i) & ((i - j < WINDOW) | (j < SINK))out_ref = F.scaled_dot_product_attention(q, k, v, attn_mask=dense)print("макс. расхождение с плотной маской:", f"{(out_flex - out_ref).abs().max().item():.1e}")print("доля пар от причинного внимания:", f"{dense.sum().item() / (j <= i).sum().item():.3f}")print(block_mask)Ожидаемый вывод (вид BlockMask может немного отличаться в разных версиях PyTorch):
макс. расхождение с плотной маской: 6.6e-07доля пар от причинного внимания: 0.443BlockMask(shape=(1, 1, 1024, 1024), sparsity=59.38%,(0, 0)██████████████████████ ████████ ████████ ████████ ██████)Картинка — это и есть блочная разреженность: столбец слева — блок со «стоком», диагональная полоса — окно. 59% блоков пропускаются целиком. Именно на этом уровне — блоки, а не отдельные токены — работают ядра NSA и MoBA.
Проверь себя
Заголовок раздела «Проверь себя»- Внимание на длинном контексте почти разреженное: соседи, «сток» в начале и немногие дальние «иголки».
- Скользящее окно дёшево, но слепо к дальнему; сегодня его чередуют с полными слоями и держат первые токены-«стоки».
- NSA — три ветви (сжатие, отбор 16 блоков, окно 512) с гейтами; обучается с нуля и на 64K читает в 11.6 раза меньше.
- DSA — дешёвый lightning indexer выбирает 2048 токенов для полного MLA; индексатор сначала учат повторять полное внимание через KL.
- Тренд 2026 года — сочетания сжатия и отбора (DeepSeek-V4, MiniMax M3, Qwen, LongCat).
Дальше: другой путь к длинному контексту — убрать softmax и превратить внимание в RNN с памятью фиксированного размера. Линейное внимание, Lightning Attention, Gated DeltaNet и честный разговор о том, почему MiniMax от него отказалась, — Линейное внимание и гибриды.