Три строки кода ускорили декодирование LLM на 22,8% — без потери качества

Независимый разработчик нашёл способ на четверть ускорить генерацию текста в llama.cpp на длинных контекстах. Метод: не деквантизовать то, что модель игнорирует.

Три строки кода ускорили декодирование LLM на 22,8% — без потери качества

Независимый разработчик 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 операций целиком. Выигрыш растёт с длиной контекста, потому что разреженность внимания тоже растёт.

Репозиторий TheTom/turboquant_plus на GitHub с документацией и бенчмарками sparse V dequantization
Репозиторий turboquant_plus на GitHub. Источник: github.com/TheTom/turboquant_plus

Результаты: от +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,577,6+1,4%0,90×
4K72,074,9+4,0%
8K66,971,7+7,2%
16K58,966,5+12,9%0,92×
32K47,057,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 V7/9
turbo3 + sparse V9/9 (100%)

Гипотеза автора: позиции с весом внимания ниже 10⁻⁶ несут нулевой полезный сигнал, но при деквантизации добавляют квантизационный шум. Sparse V убирает этот шум, и соотношение сигнал/шум в выходе механизма внимания улучшается.

Работает с любым форматом квантизации

Sparse V работает на уровне механизма внимания. Веса softmax служат сигналом для пропуска вычислений, а softmax одинаков вне зависимости от способа сжатия K и V.

Turney проверил метод на трёх форматах KV-кеша:

ФорматБитПерплексия (Δ ON/OFF)NIAH (Δ)Прирост скорости
turbo3 (TurboQuant)3,50,00007/9 → 9/9+22,8% (32K)
q8_08,00,0000без изменений+5,0% (короткий)
q4_04,00,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 можно будет включить по умолчанию: на коротком контексте он ничего не стоит, на длинном ощутимо ускоряет генерацию.


Читайте также:

Telegram-канал @toolarium