Перейти к содержанию
Новое AiManual теперь в MAX Подписаться
Публикация AiManual

Разбор запуска GLM 5.2 на CPU: почему декодинг медленный и как это исправить

Запустили GLM 5.2 на AMD EPYC 9654 и получили 5-9 токенов/с вместо ожидаемых 30+? Разбираем реальный кейс: почему контекст 131K и KV-кэш Q8_0 убивают производит

Коротко

Что будет в материале

  1. 01

    Контекст проблемы: железо, модель и первые результаты

  2. 02

    Почему декодинг в 10 раз медленнее префилла: анализ узких мест

  3. 03

    Пошаговая оптимизация: от параметров запуска до прироста скорости

  4. 04

    Сравнение с другими моделями и выводы о применимости

Контекст проблемы: железо, модель и первые результаты

Сервер AMD EPYC 9654 с 96 ядрами и 768 ГБ оперативной памяти. Модель GLM 5.2 в квантизации Q4_K_M. Параметры запуска: контекст 131K, KV-кэш в Q8_0, активные флаги DSA и MLA, настроенная NUMA-аффинность, блокировка памяти через mlock и отключённый mmap. Результат - скорость обработки промпта (префилл) 60 токенов/с, скорость декодирования 5-9 токенов/с. Разрыв между префиллом и декодингом почти десятикратный. Это не брак модели и не дефект процессора. Это конфигурация, которая загоняет производительность в узкое место.

Первичный анализ указывает на четыре фактора, которые совместно обрушивают скорость генерации. Размер контекста 131K создаёт колоссальную вычислительную нагрузку на каждый шаг декодинга. KV-кэш в Q8_0 удваивает требования к пропускной способности памяти. Избыточное число потоков на декодинг вносит накладные расходы на синхронизацию. Флаги DSA и MLA, ориентированные на архитектуру Intel, на AMD EPYC могут работать с эмуляцией или вовсе давать обратный эффект. Ниже каждый пункт разбирается детально, с цифрами и конкретными рекомендациями.

Почему декодинг в 10 раз медленнее префилла: анализ узких мест

Префилл и декодинг - два принципиально разных режима работы. При обработке промпта модель вычисляет внимание параллельно для всех токенов входной последовательности. Ядра загружены равномерно, пропускная способность памяти используется эффективно. Декодинг - автогрессивный процесс: каждый следующий токен вычисляется последовательно, и на каждом шаге модель взаимодействует со всем накопленным KV-кэшем. При длине контекста 131K это 131 тысяча операций внимания на один токен. Параллелить здесь нечего, и любое узкое место в подсистеме памяти становится критическим.

Влияние размера контекста на скорость декодирования

Механизм внимания масштабируется квадратично относительно длины последовательности. На практике это означает: при переходе с 32K на 131K объём вычислений для одного шага декодинга возрастает примерно в 4 раза. Префилл страдает меньше, потому что матричные умножения идут пачками и хорошо векторизуются. Декодинг упирается в последовательный доступ к памяти - каждый новый токен требует прочитать весь KV-кэш.

Снижение контекста до 32-48K даёт прирост скорости декодинга в 2-3 раза. Это не линейная зависимость: при уменьшении контекста кэш начинает помещаться в кэш-память процессора L3, и задержки доступа к данным резко падают. EPYC 9654 имеет 384 МБ L3-кэша, и при контексте 32K с KV-кэшем Q4_0 значительная часть данных остаётся на кристалле. Для большинства практических задач - чат-боты, суммаризация документов, генерация кода - 32K контекста достаточно. Длинный контекст в 131K оправдан только для задач анализа больших корпусов текста, где он действительно нужен, но тогда придётся мириться с низкой скоростью либо переходить на GPU-инференс, как в конфигурации с 8× GB10, где декодинг достигает 33-54 токенов/с.

Квантизация KV-кэша: Q8_0 против Q4_0

KV-кэш хранит ключи и значения для каждого токена контекста. В Q8_0 каждый элемент занимает 8 бит, в Q4_0 - 4 бита. При контексте 131K и размерности модели GLM 5.2 (предположительно 8192 для ключей) объём кэша в Q8_0 превышает 20 ГБ. Это больше, чем весь L3-кэш процессора, и каждый шаг декодинга требует чтения этих 20 ГБ из оперативной памяти.

Пропускная способность памяти на EPYC 9654 с 12 каналами DDR5-4800 составляет около 460 ГБ/с. Теоретически 20 ГБ можно прочитать за 43 мс. На практике последовательный доступ к разрозненным областям памяти, промахи TLB и конкуренция с другими процессами снижают эффективную пропускную способность в 2-3 раза. Переход на Q4_0 вдвое сокращает объём кэша - до 10 ГБ - и соответственно снижает нагрузку на подсистему памяти. Потеря точности при переходе с Q8_0 на Q4_0 для KV-кэша минимальна и редко заметна в качестве генерации, что подтверждается тестами на схожих архитектурах, например, при сравнении квантизаций Qwen3.5 122B, где снижение точности кэша не приводило к деградации ответов.

Потоки и многопоточное декодирование: как не навредить

96 ядер EPYC 9654 - это преимущество для префилла, где вычисления легко распараллеливаются. Для декодинга большое число потоков становится проблемой. Декодинг одного токена требует последовательного выполнения операций: загрузка эмбеддинга, вычисление attention, проход через feed-forward слои. Каждый поток обрабатывает свою часть матрицы, но после каждого слоя требуется барьерная синхронизация - все потоки должны завершить работу, прежде чем перейти к следующему слою. При 96 потоках накладные расходы на синхронизацию могут съедать до 30-40% процессорного времени.

Оптимальное число потоков для декодинга на EPYC 9654 - 8-16. Этого достаточно, чтобы нагрузить доступные каналы памяти, но синхронизация остаётся быстрой. Префилл можно оставить на всех 96 ядрах, указав отдельный параметр для батчевой обработки. В llama.cpp это флаг --threads-batch. Рекомендуемая конфигурация: -t 16 для декодинга, -tb 96 для префилла. Аналогичный подход с разделением потоков применяется при оптимизации инференса на AMD Ryzen AI Halo, где ручная настройка числа потоков даёт 10-15% прироста производительности.

Пошаговая оптимизация: от параметров запуска до прироста скорости

Соберём все рекомендации в конкретную команду запуска. Исходный проблемный конфиг выглядел примерно так:

./llama-cli -m glm-5.2-q4_k_m.gguf -c 131072 --kv-cache-dtype q8_0 -t 96 --mlock --no-mmap --numa

Оптимизированный вариант с пояснением каждого изменения:

./llama-cli -m glm-5.2-q4_k_m.gguf \
  -c 32768 \                    # Снижен контекст до 32K
  --kv-cache-dtype q4_0 \       # KV-кэш в 4 бита вместо 8
  -t 16 \                        # Потоки для декодинга
  -tb 96 \                       # Потоки для префилла
  --mlock \                      # Блокировка страниц в RAM
  --no-mmap \                    # Отключение mmap
  --numa distribute \            # Распределение по NUMA-узлам
  --no-dsa --no-mla              # Отключение DSA и MLA

Ожидаемый результат: скорость декодинга вырастет с 5-9 до 15-25 токенов/с. Префилл останется на уровне 50-60 токенов/с или незначительно снизится из-за уменьшения числа потоков. Субъективно это разница между «невозможно работать» и «приемлемо для диалогового режима».

Тестирование без DSA и MLA: когда флаги мешают

DSA (Data Streaming Accelerator) и MLA (Memory Latency Accelerator) - технологии Intel для ускорения операций с памятью и снижения задержек. На процессорах AMD они либо эмулируются через программные прослойки, либо не поддерживаются вовсе. В обоих случаях включение этих флагов добавляет накладные расходы без прироста производительности. На некоторых конфигурациях эмуляция DSA может замедлить декодинг на 10-20%.

Рекомендация однозначная: отключить оба флага, замерить скорость, и только если есть прирост - оставить отключёнными. В llama.cpp с июля 2026 года эти флаги управляются параметрами --no-dsa и --no-mla. В более старых версиях может потребоваться сборка без соответствующих опций компиляции.

Тонкая настройка NUMA и блокировка памяти

EPYC 9654 - односокетный процессор, но с несколькими NUMA-узлами внутри кристалла (обычно 4 узла на 96 ядер). Параметр --numa distribute распределяет память модели равномерно по всем узлам, что увеличивает агрегированную пропускную способность. Без этого флага вся модель может разместиться в памяти одного узла, и остальные каналы DDR5 будут простаивать.

Флаг --mlock блокирует страницы виртуальной памяти в физической RAM, запрещая ядру выгружать их в swap. На сервере с 768 ГБ RAM swap вряд ли используется, но mlock предотвращает page faults при первом обращении к страницам - модель загружается сразу в физическую память. --no-mmap отключает отображение файла модели в память через mmap, вместо этого данные читаются в буфер. Это увеличивает время загрузки, но устраняет непредсказуемые задержки ввода-вывода во время инференса.

Для двухсокетных систем рекомендация иная: привязывать процесс к одному сокету через numactl --membind=0 --cpunodebind=0, чтобы избежать пересечения трафика между сокетами через межпроцессорные линки, пропускная способность которых ограничена.

Сравнение с другими моделями и выводы о применимости

Скорость 15-25 токенов/с для модели класса 70B+ параметров на CPU - хороший результат. Для сравнения: Qwen3.5 122B на аналогичном оборудовании с 64 ГБ RAM даёт 2.9 токенов/с, а DeepSeek V4 Flash в IQ3_XXS на связке RTX 3090 + 128 ГБ DDR4 показывает 18-19 токенов/с в генерации. GLM 5.2 в Q4_K_M занимает промежуточное положение - быстрее более тяжёлых MoE-моделей, но медленнее специализированных решений с GPU-оффлоудом.

Практические сценарии для 15-25 токенов/с:

  • Диалоговые чат-боты - приемлемо, ответ формируется за 5-10 секунд
  • Суммаризация документов - хорошо, документ обрабатывается за 1-2 минуты
  • Генерация кода - приемлемо для коротких функций, медленно для больших файлов
  • Потоковая генерация в реальном времени - недостаточно, требуется 30+ токенов/с

Если задача требует скорости выше 30 токенов/с на CPU, стоит рассмотреть модели меньшего размера или техники спекулятивного декодинга. Например, n-gram драфтеры в llama.cpp ускоряют генерацию кода на Qwen 3.6 27B до 6 раз без потери точности. Для GLM 5.2 этот метод пока не протестирован, но архитектурно совместим.

Заключение: чек-лист для быстрой оптимизации CPU-инференса

Шесть шагов, которые превращают 5-9 токенов/с в 15-25 токенов/с на серверных CPU:

  1. Снизить размер контекста до реально необходимого - 32K вместо 131K
  2. Переключить KV-кэш на Q4_0 - вдвое меньше данных, минимальная потеря точности
  3. Разделить потоки: 8-16 для декодинга, все ядра для префилла
  4. Включить многопоточное декодирование через --threads-batch
  5. Отключить DSA и MLA на AMD-процессорах
  6. Использовать mlock и отключить mmap для стабильности задержек

Эти шаги универсальны для большинства CPU-серверов и не требуют модификации модели или перехода на другое железо. Если после всех оптимизаций скорость остаётся ниже ожидаемой, узким местом может быть одноканальный режим памяти или неверная конфигурация BIOS - проверьте, что все 12 каналов DDR5 активны и работают на заявленной частоте.

Подписаться на канал