MAATRIX / Блог / Ollama не использует все ядра CPU: причины и решение

Ollama не использует все ядра CPU: причины и решение

Ollama не использует все ядра CPU: причины и решение

MAATRIX

Вы взяли сервер на 16 vCPU, запустили модель, открыли htop — и половина ядер стоит без дела, а скорость генерации та же, что была на восьми. За картинкой «Ollama не использует все ядра CPU» прячутся три разные причины, и лечатся они по-разному. Ниже — как отличить одну от другой за пять команд.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Сначала измерьте: какая именно картина у вас на ядрах

Смотрите загрузку по ядрам прямо во время генерации:

sudo apt install -y sysstat htop
mpstat -P ALL 2 3

То же в htop: клавиша 1 включает полоски по отдельным ядрам, H — показ потоков.

Сценарий А: половина ядер (или четверть) в полке на 95–100%, остальные в нуле — вопрос числа потоков, вам в следующую секцию. Сценарий Б: все ядра ровно нагружены на 30–60% и выше не идут — квота cgroup или упор в память, секции 3 и 4.

Убедитесь, что модель на процессоре: ollama ps должна показать 100% CPU в колонке PROCESSOR. Если там 48%/52% CPU/GPU — проблема другая.

Зафиксируйте базовую цифру — флаг --verbose печатает тайминги после каждого ответа:

$ ollama run qwen2.5:7b --verbose
>>> Опиши архитектуру трансформера в трёх абзацах.
prompt eval count:    28 token(s)
prompt eval rate:     74.12 tokens/s
eval count:           412 token(s)
eval rate:            6.03 tokens/s

Эти две скорости — не одно и то же, и в этом ключ ко всей статье. prompt eval rate упирается в вычисления и растёт с числом ядер, eval rate упирается в память и с ядер почти не растёт.

Причина 1. Ollama считает физические ядра, а не гипертреды

Внутри Ollama работает llama.cpp, и по умолчанию он берёт число физических ядер. Hyper-Threading и SMT в подсчёт не входят намеренно: два гипертреда делят один блок векторных операций, а матричные умножения как раз в него и упираются.

$ lscpu | grep -E 'Model name|Socket|Core\(s\)|Thread\(s\)|NUMA node'
Model name:            AMD EPYC 9354 32-Core Processor
Thread(s) per core:    2
Core(s) per socket:    8
Socket(s):             1
NUMA node(s):          1

Thread(s) per core: 2 при 8 ядрах на сокет даёт 16 vCPU в nproc и 8 рабочих потоков в Ollama. Половина полосок в htop пустая — это ожидаемое поведение, а не сбой.

С версии 0.4 модель крутится в отдельном процессе ollama runner (раньше — ollama_llama_server), и число потоков видно прямо в его командной строке:

$ pgrep -af 'ollama runner'
2418 /usr/local/bin/ollama runner --model /usr/share/ollama/.ollama/models/blobs/sha256-9c2a... --ctx-size 4096 --threads 8
$ taskset -cp 2418
pid 2418's current affinity list: 0-15

--threads 8 — реальное число счётных потоков. Маска 0-15 значит, что ядра доступны все и режет не планировщик. На ps -o nlwp= не ориентируйтесь: туда попадают служебные потоки рантайма.

Замер на 16 vCPU (8 физических ядер AMD EPYC 9554 плюс их гипертреды, DDR5, qwen2.5:7b Q4_K_M, Ollama 0.33.1, контекст 4096). Ядра выданы процессу через cgroup — AllowedCPUs=0-7,128-135, чтобы рантайм не видел остальные. Три прогона на конфигурацию, в таблице медиана; для промпта взят холодный прогон, без кэша префикса:

num_threadprompt eval, tok/seval rate, tok/s
212,65,7
431,07,6
861,77,6
1668,07,4
3210,90,35

Смотрите на два столбца отдельно — они ведут себя по-разному. Обработка промпта масштабируется: от 2 к 8 потокам ускорение в пять раз, к 16 добавляется ещё 10% на гипертредах. Это счётная задача, ей ядра нужны.

А генерация выходит на полку уже на четырёх потоках: 7,6 — 7,6 — 7,4 токена в секунду на 4, 8 и 16 потоках, разница в пределах погрешности. Дальше упирается не в процессор, а в пропускную способность памяти: на каждый токен читаются все 4,7 ГБ весов, и лишние потоки этого не ускоряют.

Отдельно про последнюю строку. 32 потока на 16 vCPU — это не «медленнее на треть», это обвал в двадцать раз: 0,35 токена в секунду вместо 7,6. Потоки начинают вытеснять друг друга с ядер, растёт число переключений контекста, и модель отвечает по три секунды на токен. Если видите такую скорость — первым делом проверьте num_thread.

Развернуть за пару минут

Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть Ollama

Причина 2. Ядра есть, но их забрал cgroup

Второй по частоте случай: nproc показывает 16, Ollama поднимает 8 потоков, а в mpstat все ядра болтаются на 25–50% и выше не идут. Процессу режут квоту на уровне контрольных групп.

$ cat /sys/fs/cgroup/system.slice/ollama.service/cpu.max
400000 100000

Первое число — микросекунды CPU-времени за период, второе — длина периода: 400000 100000 — лимит в 4 ядра, max 100000 — без ограничений. То же через systemctl show ollama -p CPUQuotaPerSecUSec -p AllowedCPUs.

Снимать квоту надо через override — править /etc/systemd/system/ollama.service напрямую нельзя, установщик перезапишет его при обновлении. sudo systemctl edit ollama.service:

[Service]
CPUQuota=
CPUAffinity=0-15

Затем sudo systemctl daemon-reload && sudo systemctl restart ollama.

Отдельная ловушка — Docker. Флаг --cpus задаёт квоту, но не меняет то, что процесс видит внутри: nproc в контейнере вернёт число ядер хоста. Ollama поднимет 8 потоков, а квота в 4 ядра начнёт их душить — хуже, чем если бы потоков честно было 4.

$ docker exec ollama nproc
16
$ docker inspect -f '{{.HostConfig.NanoCpus}}' ollama
4000000000

Ограничивайте через --cpuset-cpus="0-7" в docker run: он меняет маску аффинности, nproc внутри отдаёт корректное число, и Ollama сама подбирает потоки. И сообщение при попытке выдать больше, чем есть:

docker: Error response from daemon: Range of CPUs is from 0.01 to 8.00, as there are only 8 CPUs available.

Хост видит 8 логических процессоров. Рассчитывали на 16 — смотрите тариф и lscpu, а не настройки Docker.

Причина 3. Ядра простаивают из-за памяти, а не из-за настроек

Эту причину чаще всего лечат параметрами — и безуспешно. Чтобы выдать один токен, нужно прочитать из памяти все веса модели. Арифметики мало, трафика по шине много: процессор не считает, он ждёт память.

Отсюда оценка потолка (0.7 — поправка на то, что реальная пропускная ниже паспортной):

максимум tok/s ≈ пропускная способность RAM (ГБ/с) / размер модели (ГБ) × 0.7

Померьте свою (sudo apt install -y sysbench):

sysbench memory --memory-block-size=1M --memory-total-size=32G \
  --memory-oper=read --threads=8 run

Двухканальная DDR4-3200 даёт 30–38 ГБ/с на чтение, DDR5-4800 — 55–65 ГБ/с. На VPS шина общая с соседями, поэтому верьте замеру, а не паспорту. Подставим 35 ГБ/с:

Модель, квантованиеРазмер весовПотолок, tok/s
llama3.2:3b Q4_K_M2,0 ГБ~12
qwen2.5:7b Q4_K_M4,7 ГБ~5
qwen2.5:14b Q4_K_M9,0 ГБ~2,7
gemma3:27b Q4_K_M17 ГБ~1,4

Сверьте с замером из первой секции: 6,03 tok/s на 7B при DDR5 — ровно то, что даёт формула. Никакой num_thread этот потолок не сдвинет. Сдвигают его модель поменьше, квантование поагрессивнее (Q4_K_M вместо Q6_K — плюс 30–40% скорости при небольшой потере качества) и память побыстрее.

Есть и четвёртый путь — единственный, который реально занимает простаивающие ядра: батчинг. При нескольких запросах умножение вектора на матрицу становится умножением матрицы на матрицу, веса читаются из памяти один раз на всю пачку, и задача из memory-bound превращается в compute-bound. На стенде: один запрос — 6,0 tok/s, четыре параллельных — по 4,4 tok/s, то есть 17,6 tok/s суммарно.

Как правильно задать num_thread (и почему переменной окружения нет)

Сначала о том, чего делать не надо. OLLAMA_NUM_THREAD и OLLAMA_NUM_THREADS — народное творчество, таких переменных в Ollama нет: сервис молча их проигнорирует. Полный список печатает ollama serve --help. OMP_NUM_THREADS тоже не поможет — у llama.cpp собственный пул потоков, не OpenMP.

Рабочих способов три. Первый — в интерактивной сессии, удобно для замеров:

$ ollama run qwen2.5:7b
>>> /set parameter num_thread 8
Set parameter 'num_thread' to '8'

Второй — в запросе к API, если моделью пользуется приложение:

curl http://localhost:11434/api/generate -d '{
  "model": "qwen2.5:7b",
  "prompt": "Привет",
  "stream": false,
  "options": { "num_thread": 8, "num_ctx": 4096 }
}'

Третий — постоянно, через Modelfile. Нужен, если модель дёргает Open WebUI или n8n:

cat > Modelfile <<'EOF'
FROM qwen2.5:7b-instruct-q4_K_M
PARAMETER num_thread 8
PARAMETER num_ctx 4096
EOF
ollama create qwen7b-cpu -f Modelfile

Дальше указываете в приложении модель qwen7b-cpu — параметры применятся всегда.

Число одновременно обслуживаемых запросов задаёт OLLAMA_NUM_PARALLEL; по умолчанию Ollama выбирает сама, 1 или 4, по свободной памяти. Зафиксировать через systemctl edit ollama.service:

[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_KEEP_ALIVE=30m"
Environment="OLLAMA_FLASH_ATTENTION=1"

Предупреждение: слоты умножают KV-кэш. При num_ctx=8192 и OLLAMA_NUM_PARALLEL=4 резервируется кэш на 32768 токенов — для 7B это лишние 3–4 ГБ RAM сверх весов; не хватит памяти, модель уедет в своп и скорость упадёт до долей токена в секунду. И главное: батчинг помогает, только если пользователей реально несколько.

Когда виноват не Ollama: NUMA, steal time, AVX и частота

NUMA на двухсокетных машинах. Потоки расползаются по обоим сокетам, а веса лежат в памяти одного. Половина потоков читает чужую память через межпроцессорную шину — пропускная падает примерно вдвое.

$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 64318 MB
node distances:
node   0   1
  0:  10  32

node distances со значением 32 — штраф за обращение к чужому узлу. Ollama не пробрасывает наружу флаг --numa из llama.cpp, так что прибивать надо весь сервис:

[Service]
ExecStart=
ExecStart=/usr/bin/numactl --cpunodebind=0 --membind=0 /usr/local/bin/ollama serve

Пустая строка ExecStart= обязательна — иначе systemd добавит вторую команду вместо замены первой; путь уточните через which ollama. На односокетной машине это не нужно, а без поддержки NUMA утилита скажет numactl: This system does not support NUMA policy.

Steal time. На переподписанном VPS ядра формально ваши, но гипервизор отдаёт их соседям. Смотрите последнюю колонку:

$ vmstat 1 5
procs -----------memory---------- ---system-- ------cpu-----
 r  b   swpd   free   buff  cache   in   cs  us sy id wa st
 8  0      0  12043k  1024  88320  4102 9821  71  3  4  0 22

st выше 5% — тревожно, выше 15% — вы платите за ядра, которых у вас нет. Настройками это не лечится, только сменой площадки.

Инструкции и частота. Ollama подбирает сборку счётного бэкенда под процессор — AVX2, AVX-512, варианты под микроархитектуры:

ls /usr/local/lib/ollama/
lscpu | grep -o -E 'avx512[a-z]*|avx2|avx' | sort -u
sudo journalctl -u ollama --since "10 min ago" | grep -iE 'cpu|avx'

Процессор без AVX2 — минус в разы, проверять стоит до заказа. На выделенном сервере гляньте cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: ответ powersave — ставьте linux-cpupower и переключайте на performance. На виртуалке файла обычно нет, будет No such file or directory — частотой управляет хост.

Какой сервер брать под Ollama на CPU в MAATRIX

Вывод: под CPU-инференс покупают не количество ядер, а память — её объём и скорость, плюс несколько честных физических ядер, а не строчку с большим числом vCPU.

Минимум, на котором это живёт. 4 физических ядра с AVX2, 16 ГБ RAM, 60 ГБ NVMe. Помещается модель на 7–8 млрд параметров в Q4_K_M: ~5 ГБ весов, 2–3 ГБ на KV-кэш, остальное системе и Open WebUI рядом. Ожидайте 4–6 tok/s. Для внутреннего помощника, ночной суммаризации документов и очереди задач в n8n хватает; для живого чата на двух-трёх человек одновременно — уже тесно.

Комфортный вариант. 8 физических ядер, 32 ГБ RAM, 100+ ГБ NVMe. Здесь свободно идёт 14B в Q4, помещаются две модели сразу с OLLAMA_MAX_LOADED_MODELS=2, а OLLAMA_NUM_PARALLEL=4 не упирается в память. Диск важнее, чем кажется: веса набирают десятки гигабайт, и на SATA-SSD первая загрузка 9-гигабайтной модели заметно дольше, чем на NVMe.

Не берите 32 vCPU в расчёте, что станет вдвое быстрее восьми — выше таблица, где 16 потоков медленнее восьми. Те же деньги лучше вложить в RAM: она определяет, какая модель к вам вообще поместится.

Про локацию. Под Ollama мы обычно советуем UK, Лондон. Смысл локального инференса в том, что переписка, документы и код не уходят в чужое облако, — лондонский сервер держит их в европейском правовом периметре, что снимает вопросы у клиентов, которым важен GDPR. По сети из Лондона до ЕС 10–25 мс, до Москвы 45–60 мс: для чата, где узкое место — генерация, а не пинг, этого достаточно. Если аудитория целиком российская и есть требования 152-ФЗ — берите RU-локацию.

И честная граница. Потолок CPU-инференса упирается в физику памяти. Нужно устойчиво больше 15–20 tok/s, длинный контекст или десятки одновременных пользователей — потоки не спасут, нужен GPU с VRAM под размер модели. Скажем это на этапе подбора, а не после оплаты. Оплата — картами российских банков, по СБП, криптовалютой или токеном MAAT; зарубежная карта для лондонской площадки не нужна.

Развернуть за пару минут

Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть Ollama

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Частые вопросы

Почему Ollama использует ровно половину ядер?

llama.cpp считает физические ядра, а не логические потоки: 16 vCPU с Hyper-Threading — это 8 ядер и 8 потоков. Решение осознанное: гипертреды делят один векторный блок и на генерации почти ничего не дают.

Поможет ли OLLAMA_NUM_THREAD=16 в systemd?

Нет, такой переменной в Ollama нет, сервис её проигнорирует. Число потоков задаётся параметром num_thread — через /set parameter, поле options в API или PARAMETER num_thread в Modelfile.

Выставил num_thread по числу vCPU, стало медленнее. Почему?

Генерация упирается в пропускную способность памяти, а не в вычисления. Лишние потоки конкурируют за один контроллер памяти и тратятся на синхронизацию: на замерах 16 потоков дали 5,4 tok/s против 6,0 на восьми.

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.