Три строки кода ускорили декодирование LLM на 22,8% — без потери качества
Независимый разработчик нашёл способ на четверть ускорить генерацию текста в llama.cpp на длинных контекстах. Метод: не деквантизовать то, что модель игнорирует.
Независимый разработчик Tom Turney нашёл способ на четверть ускорить генерацию текста в llama.cpp на длинных контекстах. Метод простой: не деквантизовать то, что модель и так игнорирует. Минимальная правка в ядре flash attention, максимальный эффект на контексте от 8K токенов.
Зачем нужна деквантизация и почему она тормозит
При генерации текста LLM хранит промежуточные данные в KV-кеше, массиве ключей (K) и значений (V) для механизма внимания. Без них модели пришлось бы пересчитывать всю историю диалога при каждом новом токене.
На длинных контекстах KV-кеш занимает гигабайты памяти. Стандартное решение — квантизация: сжатие из 16 бит до 3–4. TurboQuant от Google (ICLR 2026) сжимает кеш в 4,6 раза с потерей перплексии около 1%. Но при генерации каждого токена сжатые блоки нужно деквантизовать, то есть преобразовать обратно в числа с плавающей точкой.
На Apple M5 Max деквантизация TurboQuant съедает 14–34% времени декодирования. При контексте 32K токенов скорость падает с 78,3 tok/s (потолок без деквантизации) до 47,0 tok/s. Штраф — 40%.
14 неудачных попыток
Прежде чем найти решение, Turney перебрал 14 альтернативных реализаций деквантизации на Apple Silicon: регистровые массивы, битовую арифметику, SIMD-шаффлы, FMA без ветвлений, слитые блочные операции. Ни одна не обогнала базовую таблицу подстановки (LUT) в константной памяти GPU.
На Apple Silicon 4 дивергентных чтения из константной памяти быстрее любой арифметики, которая даёт тот же результат. Это аппаратный пол, ниже которого не опуститься.
Остался один рычаг: сократить количество позиций, которые вообще нужно деквантизовать.
Пропускать вместо ускорения
В ядре flash attention веса внимания (softmax) вычисляются из ключей (K) до того, как начинается работа со значениями (V). На момент деквантизации V уже известно, какие позиции контекста получили значимый вес, а какие нет.
При длинном контексте (32K+) более 90% весов внимания ничтожно малы, ниже 10⁻⁶. Модель «смотрит» на несколько десятков ключевых позиций, остальные практически не влияют на результат.
Sparse V dequantization использует этот факт: если вес внимания для позиции ниже порога (10⁻⁶), её деквантизация и аккумуляция пропускаются целиком. Реализация в Metal-ядре:
const float attn_weight = float(ss[NE*cc + ty]);
if (attn_weight < 1e-6f) continue;
// ... existing V dequant and accumulation ...Предыдущие 14 подходов пытались сделать каждую из N операций быстрее, а это ограничено аппаратным полом. Sparse V убирает (1−p)×N операций целиком. Выигрыш растёт с длиной контекста, потому что разреженность внимания тоже растёт.

Результаты: от +1,4% до +22,8%
Turney тестировал метод на Apple M5 Max с моделью Qwen 3.5 35B (MoE) и TurboQuant (turbo3) в llama.cpp:
| Контекст | Без sparse V (tok/s) | С sparse V (tok/s) | Прирост | Отношение к q8_0 |
|---|---|---|---|---|
| Короткий | 76,5 | 77,6 | +1,4% | 0,90× |
| 4K | 72,0 | 74,9 | +4,0% | — |
| 8K | 66,9 | 71,7 | +7,2% | — |
| 16K | 58,9 | 66,5 | +12,9% | 0,92× |
| 32K | 47,0 | 57,7 | +22,8% | 0,93× |
Чем длиннее контекст, тем больше прирост. На 32K токенов модель с 3,5-битным KV-кешем работает на 93% скорости несжатого q8_0, то есть почти на паритете.
Качество не пострадало, а поиск стал точнее
Перплексия при включении sparse V осталась численно идентичной варианту без него. На корпусе wikitext-103 (50 чанков, контекст 32K, CI ±0,021) дельта составила ровно 0,0000. Turney проверил это на контекстах 8K, 16K и 32K, и везде разница нулевая.
Отдельно интересен результат в тесте needle-in-a-haystack (NIAH), где модель ищет конкретный факт в длинном контексте:
| Конфигурация | Результат NIAH |
|---|---|
| q8_0 (без квантизации V) | 7/9 |
| turbo3 без sparse V | 7/9 |
| turbo3 + sparse V | 9/9 (100%) |
Гипотеза автора: позиции с весом внимания ниже 10⁻⁶ несут нулевой полезный сигнал, но при деквантизации добавляют квантизационный шум. Sparse V убирает этот шум, и соотношение сигнал/шум в выходе механизма внимания улучшается.
Работает с любым форматом квантизации
Sparse V работает на уровне механизма внимания. Веса softmax служат сигналом для пропуска вычислений, а softmax одинаков вне зависимости от способа сжатия K и V.
Turney проверил метод на трёх форматах KV-кеша:
| Формат | Бит | Перплексия (Δ ON/OFF) | NIAH (Δ) | Прирост скорости |
|---|---|---|---|---|
| turbo3 (TurboQuant) | 3,5 | 0,0000 | 7/9 → 9/9 | +22,8% (32K) |
| q8_0 | 8,0 | 0,0000 | без изменений | +5,0% (короткий) |
| q4_0 | 4,0 | 0,0000 | без изменений | в пределах шума |
Выигрыш от пропуска тем больше, чем дороже сама деквантизация. turbo3 использует преобразование Уолша-Адамара и полярную декомпозицию, поэтому эффект максимален. q4_0 деквантизуется простым масштабированием, и пропуск дешёвой операции даёт мало. Но ни в одном случае метод не ухудшил ни перплексию, ни поиск.
Почему K-деквантизацию пропускать нельзя
Ключи (K) нужны для вычисления весов внимания. Чтобы узнать, какие позиции имеют ничтожный вес, сначала нужно деквантизовать все ключи и посчитать softmax. Turney описывает двухпроходный механизм, где первый проход вычисляет приблизительные веса для фильтрации K-позиций, как направление будущей работы.
Что дальше
Код и бенчмарки опубликованы в открытом доступе: TheTom/turboquant_plus (бенчмарки и диагностика), TheTom/llama-cpp-turboquant (реализация). Для воспроизведения результатов достаточно собрать с флагом TURBO_SPARSE_V=1 и запустить llama-bench на разных длинах контекста.
Следующий шаг — портирование на CUDA и ROCm. На GPU NVIDIA профиль узкого места другой (тензорные ядра и HBM вместо unified memory Apple), но сам принцип пропуска работы для позиций с малым весом внимания универсален.
Если метод примут в основной llama.cpp, sparse V можно будет включить по умолчанию: на коротком контексте он ничего не стоит, на длинном ощутимо ускоряет генерацию.
Читайте также:
- Qwen: обзор языковой модели от Alibaba — модель Qwen 3.5 35B использовалась в тестах sparse V
- NVIDIA H100 стоит дороже, чем три года назад: что происходит с рынком GPU — почему оптимизации инференса на потребительском железе так важны