Ollama не использует все ядра CPU: причины и решение
Вы взяли сервер на 16 vCPU, запустили модель, открыли htop — и половина ядер стоит без дела, а скорость генерации та же, что была на восьми. За картинкой «Ollama не использует все ядра CPU» прячутся три разные причины, и лечатся они по-разному. Ниже — как отличить одну от другой за пять команд.
Содержание
- Сначала измерьте: какая именно картина у вас на ядрах
- Причина 1. Ollama считает физические ядра, а не гипертреды
- Причина 2. Ядра есть, но их забрал cgroup
- Причина 3. Ядра простаивают из-за памяти, а не из-за настроек
- Как правильно задать num_thread (и почему переменной окружения нет)
- Когда виноват не Ollama: NUMA, steal time, AVX и частота
- Какой сервер брать под Ollama на CPU в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество 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_thread | prompt eval, tok/s | eval rate, tok/s |
|---|---|---|
| 2 | 12,6 | 5,7 |
| 4 | 31,0 | 7,6 |
| 8 | 61,7 | 7,6 |
| 16 | 68,0 | 7,4 |
| 32 | 10,9 | 0,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_M | 2,0 ГБ | ~12 |
| qwen2.5:7b Q4_K_M | 4,7 ГБ | ~5 |
| qwen2.5:14b Q4_K_M | 9,0 ГБ | ~2,7 |
| gemma3:27b Q4_K_M | 17 ГБ | ~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 — десятки моделей в одном окне. Оплата картой РФ и по СБП.