Пользователь запустил Qwen 3.5 122b - большую MoE-модель - на связке GPU+CPU через llama.cpp. Часть слоёв он перенёс на процессор Intel с гибридной архитектурой (P-cores + E-cores). Результат оказался парадоксальным: включение производительных P-cores снизило общую скорость генерации по сравнению с конфигурацией, где работали только энергоэффективные E-cores. Цифры из теста: на одних E-cores система выдавала порядка 3.2 токен/с, а при добавлении P-cores скорость падала до 2.1 токен/с. Задержка первого токена также увеличивалась. Это противоречит базовому предположению - более высокая тактовая частота и больший объём кэша L2 на ядро у P-cores должны давать преимущество, а не отставание.
Причина кроется в архитектурных ограничениях современных процессоров Intel и особенностях работы llama.cpp с гетерогенными наборами ядер. Ключевых факторов три: конкуренция за общий L3-кэш, пропускная способность кольцевой шины и неоптимальное распределение потоков под нагрузкой offloading. Разберём каждый из них на уровне железа и покажем, как настроить affinity в Docker, чтобы выжать максимум из вашего сетапа.
Неожиданный бенчмарк: когда P-cores замедляют инференс
Исходный эксперимент проводился на системе с процессором Intel Core i9-13900K (8 P-cores, 16 E-cores) и видеокартой NVIDIA RTX 4090. Модель Qwen 3.5 122b в квантовании q8_0 занимает около 130 ГБ - полностью в VRAM она не помещается. Через llama.cpp разработчик выгрузил 20 из 80 слоёв на CPU, остальное оставил на GPU. Тестировались три конфигурации ядер:
- Только E-cores (16 ядер): 3.2 токен/с на декодинге, задержка первого токена ~4.5 секунды.
- Только P-cores (8 ядер): 1.8 токен/с, задержка ~6.8 секунды.
- Смешанный режим (все 24 ядра): 2.1 токен/с, задержка ~5.9 секунды.
Парадокс налицо: 8 P-cores с частотой 5.8 ГГц проигрывают 16 E-cores с частотой 4.3 ГГц, а их добавление к E-cores ухудшает результат последних. Это не ошибка конкретного сетапа - эффект воспроизводится на разных моделях MoE при offloading части слоёв. Схожие наблюдения встречаются в тестах запуска MoE-моделей на одном GPU, где узким местом становится пропускная способность памяти. Здесь же bottleneck смещается в процессорную подсистему.
Архитектурные причины: кэш, шина и гетерогенность
Гибридная архитектура Intel 12-го поколения и новее объединяет на одном кристалле два типа ядер с разными характеристиками. P-cores оптимизированы под однопоточную производительность: высокая частота, большой L2-кэш (2 МБ на ядро в Raptor Lake), поддержка Hyper-Threading. E-cores собраны в кластеры по 4 ядра с общим L2-кэшем (4 МБ на кластер) и нацелены на многопоточную пропускную способность. Объединяет их общий L3-кэш и кольцевая шина, через которую проходят все запросы к памяти и периферии, включая PCIe-трафик от GPU.
При offloading слоёв MoE-модели через llama.cpp возникает специфическая нагрузка. Каждый шаг инференса требует обмена данными между GPU и CPU: активации передаются через PCIe, CPU вычисляет свои слои, результат возвращается обратно. Одновременно идёт чтение весов из оперативной памяти - для квантования q8_0 это около 130 ГБ данных, которые нужно прокачивать с каждым токеном. Пропускная способность DDR5-5600 в двухканальном режиме составляет примерно 90 ГБ/с, и это число делится между всеми активными ядрами и PCIe-трафиком.
Роль кэш-памяти и квантования q8_0
Квантование q8_0 хранит каждый вес модели в 8 битах (1 байт). Это компромисс между качеством и размером: точность выше, чем у q4_0 или q5_0, но объём данных, которые нужно прочитать из памяти для генерации одного токена, значительно больше. Для модели размером 130 ГБ каждый токен требует чтения всех весов - это 130 ГБ данных. Если бы модель полностью помещалась в L3-кэш процессора, скорость упиралась бы только в вычислительные возможности ядер. Но L3-кэш у i9-13900K составляет 36 МБ - в 3600 раз меньше размера модели.
Каждое ядро при вычислениях постоянно запрашивает новые порции весов из оперативной памяти. Эти запросы проходят через L3-кэш. Когда в работе участвуют и P-cores, и E-cores, их потоки конкурируют за ограниченный объём кэша. P-cores с их высокой частотой генерируют запросы агрессивнее - они быстрее обрабатывают данные и быстрее требуют новые. Это приводит к вытеснению полезных данных из L3-кэша (cache thrashing). E-cores, работающие медленнее, успевают использовать данные в кэше до их вытеснения. P-cores создают «шторм запросов», от которого страдают все ядра. Результат - рост промахов мимо кэша и увеличение среднего времени доступа к памяти.
Влияние кольцевой шины и PCIe-трафика
Кольцевая шина (ring bus) в процессорах Intel соединяет все ядра, L3-кэш, контроллер памяти и PCIe-контроллер. Это общая среда передачи данных с фиксированной пропускной способностью. В конфигурации с offloading через неё одновременно проходят три типа трафика:
- Запросы P-cores и E-cores к контроллеру памяти за весами модели.
- Передача активаций между GPU и CPU через PCIe-контроллер.
- Служебный трафик когерентности кэша между ядрами.
Пропускная способность кольцевой шины в Alder Lake и Raptor Lake составляет около 100-120 ГБ/с на ядро в пике, но это значение делится между всеми активными агентами. Когда P-cores активно запрашивают данные, они занимают слоты на шине, увеличивая задержки для PCIe-трафика. GPU, ожидающий данные от CPU, простаивает. Возникает каскадный эффект: P-cores создают congestion на шине, GPU ждёт, общая пропускная способность системы падает. E-cores с их меньшей частотой запросов создают меньше давления на шину, оставляя больше пропускной способности для критически важного PCIe-обмена.
Дополнительный фактор - планировщик llama.cpp. Движок не имеет встроенной логики для работы с гетерогенными ядрами Intel Thread Director. Он распределяет потоки равномерно, не учитывая, что часть ядер быстрее, а часть медленнее. Быстрые P-cores завершают свои вычисления раньше и простаивают в ожидании синхронизации с E-cores. Это снижает утилизацию и добавляет накладные расходы на барьерную синхронизацию.
Практическое решение: настройка affinity в Docker
Самый эффективный способ устранения проблемы - изоляция CPU-вычислений на E-cores и исключение P-cores из работы llama.cpp. Это устраняет конкуренцию за кэш и шину, а также проблему неравномерной загрузки ядер. Настройка делается через привязку процессов к конкретным ядрам (CPU affinity).
Сначала нужно определить номера E-cores в вашей системе. Команда lscpu -e выведет таблицу с колонками CPU, CORE, SOCKET и информацией о частоте. E-cores обычно имеют более низкую максимальную частоту и сгруппированы в кластеры. В типичном i9-13900K это ядра с 16 по 31 (16 E-cores без Hyper-Threading).
Пример конфигурации для Qwen 3.5 122b
Запуск llama.cpp в Docker с привязкой к E-cores:
docker run --rm \ --cpuset-cpus="16-31" \ -v /path/to/models:/models \ ghcr.io/ggerganov/llama.cpp:full \ -m /models/qwen3.5-122b-q8_0.gguf \ -ngl 60 \ -t 16 \ -p "Your prompt here"
Флаг --cpuset-cpus="16-31" ограничивает контейнер только E-cores. Параметр -ngl 60 указывает, что 60 слоёв остаются на GPU, а остальные идут на CPU. Ключ -t 16 задаёт число потоков - оно должно совпадать с количеством доступных E-cores. Ожидаемый результат для конфигурации из бенчмарка: возврат к 3.2 токен/с вместо 2.1 токен/с в смешанном режиме.
Для docker-compose аналогичная настройка выглядит так:
services:
llama:
image: ghcr.io/ggerganov/llama.cpp:full
cpuset: "16-31"
volumes:
- /path/to/models:/models
command: >
-m /models/qwen3.5-122b-q8_0.gguf
-ngl 60
-t 16
-p "Your prompt"
Если вы запускаете llama.cpp без контейнеризации, используйте taskset:
taskset -c 16-31 ./llama-cli \ -m /models/qwen3.5-122b-q8_0.gguf \ -ngl 60 \ -t 16 \ -p "Your prompt"
Переменная окружения OMP_NUM_THREADS=16 дополнительно ограничивает число потоков OpenMP, если llama.cpp собран с его поддержкой. Полезная практика - оставить 1-2 P-cores для системных задач и обработки PCIe-прерываний, чтобы GPU не конкурировал с llama.cpp за ресурсы процессора. Для этого можно использовать изоляцию ядер через isolcpus в параметрах загрузки ядра Linux.
Дополнительные факторы и ограничения
Эффект падения производительности от P-cores проявляется при одновременном соблюдении нескольких условий. Модель должна использовать offloading значительной части слоёв на CPU - если 90% модели на GPU, а на CPU уходит 1-2 слоя, разница будет незаметна. Квантование q8_0 усугубляет проблему из-за высоких требований к пропускной способности памяти - на q4_0 эффект может быть слабее. Версия llama.cpp тоже влияет: сборки с поддержкой Intel oneMKL могут иначе распределять нагрузку. Актуальные тесты стоит сверять с последними релизами.
Тип GPU определяет интенсивность PCIe-трафика. На RTX 4090 с PCIe 4.0 x16 пропускная способность составляет ~32 ГБ/с - это создаёт заметную нагрузку на кольцевую шину. На картах с PCIe 3.0 x8 трафик меньше, и эффект от P-cores может быть не таким выраженным. Архитектура AMD с 3D V-Cache (например, Ryzen 9 7950X3D) показывает другое поведение: большой L3-кэш (128 МБ) снижает зависимость от пропускной способности памяти, и там P-cores могут давать преимущество. Но это тема для отдельного тестирования.
Альтернативный подход - полностью отказаться от offloading и запускать модель в режиме CPU-only, если объём оперативной памяти позволяет. Практическое тестирование Qwen3.5 122B на CPU показывает, что даже при низкой скорости 2.9 токен/с качество ответов может быть выше, чем у меньших моделей. Для CPU-only сценариев стоит обратить внимание на специализированные движки - Project Zero на чистом C99 демонстрирует 36 токен/с на Xeon за счёт агрессивных оптимизаций.
Стоит учитывать и альтернативные стратегии запуска MoE-моделей. Оптимизация инференса DeepSeek-V4-Flash на одном B300 показывает, как expert parallel и правильная конфигурация vLLM раскрывают потенциал железа. А разбор запуска GLM 5.2 на AMD EPYC 9654 даёт конкретные шаги по ускорению декодинга с 5 до 25 токен/с через настройку KV-кэша.
Выводы: как выбрать стратегию для вашего сетапа
P-cores в процессорах Intel не являются универсальным ускорителем для задач инференса с offloading. Их высокая частота и агрессивная работа с памятью могут создавать узкие места на кольцевой шине и в общем L3-кэше, особенно при использовании квантования q8_0. Практический алгоритм для вашего сетапа:
- Проведите собственный бенчмарк с тремя конфигурациями: только E-cores, только P-cores, все ядра. Замерьте токен/с и задержку первого токена на одинаковых промптах.
- Если P-cores дают падение производительности - изолируйте E-cores через
--cpuset-cpusв Docker илиtasksetна голом железе. Оставьте 1-2 P-cores для системных нужд. - При постоянной работе с offloading рассмотрите апгрейд на платформу с большим L3-кэшем (AMD X3D) или серверный процессор с большей пропускной способностью памяти.
Понимание архитектурных ограничений избавляет от дорогих ошибок при сборке системы. Не всегда больше ядер и выше частота означают лучшую производительность - в memory-bound задачах с гетерогенной нагрузкой побеждает сбалансированная конфигурация.