Цифры, которые заставляют задуматься
Вчера запустил Qwen-3 Coder 32B на двух системах. Одинаковое железо - RTX 4090, Ryzen 9 7950X, 64GB DDR5. Одинаковая модель - Qwen2.5-Coder-32B-Instruct-Q4_K_M.gguf. Одинаковые параметры - температура 0.7, контекст 4096 токенов.
Llama.cpp выдает 42 токена в секунду. Ollama - 25.
Разница 68%.
Не статистическая погрешность, не "ну примерно одинаково". Это пропасть. На генерации 1000 токенов разница составляет 17 секунд против 40. На реальной задаче по рефакторингу кода - минута ожидания вместо 25 секунд.
Важно: я говорю не о синтетических бенчмарках с идеальными условиями. Это реальные измерения при генерации Python кода на задачах с HumanEval. Токены разные, сложность разная - но разница стабильна.
Почему 70%, а не 5-10%?
Первая реакция - "ну, разные движки, разная оптимизация". Так говорят те, кто не копал глубже. Разница в 5-10% - это оптимизация. 70% - это фундаментальная разница в архитектуре.
Начинаем копать.
1Слой за слоем: как движки обрабатывают внимание
Llama.cpp использует собственные CUDA ядра для вычисления внимания. Не cuBLAS, не библиотеки из коробки. Свои, заточенные под специфику трансформеров в GGUF формате.
Ollama же использует llama.cpp как бэкенд (сюрприз, да?), но оборачивает его в несколько слоев абстракции. И каждый слой - это overhead.
Но даже если убрать все обертки, разница остается. Потому что...
2Конвейер против пакетной обработки
Llama.cpp обрабатывает генерацию как конвейер. Пока один слой работает на GPU, CPU уже готовит данные для следующего. Перекрытие вычислений и передачи данных.
Ollama по умолчанию использует более простую схему: загрузили всё в GPU, обработали, выгрузили результат. Нет перекрытия. Особенно заметно на моделях 32B+, где слои не помещаются целиком в кэш GPU.
В статье про сборку llama.cpp я подробно разбирал, как заставить движок использовать все ресурсы железа. Те же принципы здесь, но Ollama не дает доступа к низкоуровневой настройке.
Multi-GPU: где разница становится 3х
Добавляем вторую RTX 4090. Теперь у нас 48GB VRAM, модель загружается целиком без оффлоадинга.
| Конфигурация | Llama.cpp (t/s) | Ollama (t/s) | Разница |
|---|---|---|---|
| 1x RTX 4090 | 42 | 25 | 68% |
| 2x RTX 4090 (NVLink) | 78 | 28 | 179% |
| 2x RTX 4090 (без NVLink) | 65 | 27 | 141% |
Видите? С двумя картами llama.cpp ускоряется почти вдвое. Ollama - на 3 токена в секунду. Потому что multi-GPU поддержка в Ollama - это просто распределение слоев между картами. Нет оптимизации передачи данных, нет перекрытия вычислений между картами.
Llama.cpp же использует техники из RPC-сервера даже в локальном режиме: асинхронные передачи, кэширование промежуточных результатов, балансировку нагрузки между картами.
Контекст и батчинг: скрытый убийца производительности
Qwen-3 Coder 32B - модель для генерации кода. Работает с длинными контекстами (до 128K в оригинале, но мы тестируем на 4096).
При генерации кода часто нужно:
- Иметь в контексте весь файл (500-2000 строк)
- Добавлять системный промпт с инструкциями
- Генерировать по 100-500 токенов за раз
Llama.cpp обрабатывает длинный контекст эффективнее за счет:
- Кэширования позиционных эмбеддингов
- Инкрементального расширения контекста
- Оптимизированного KV-кэша
Ollama каждый раз пересчитывает позиционные эмбеддинги для всего контекста. На 4096 токенах это 10-15% от времени генерации.
Проверьте: если у вас в Ollama контекст 4096, но реально используется 500 токенов - вы все равно платите производительностью за полный контекст. В llama.cpp можно динамически выделять память под KV-кэш.
Батчинг: почему Ollama проигрывает даже на одной задаче
"Батчинг же для обработки нескольких запросов параллельно" - скажете вы. Да. Но есть нюанс.
Даже при обработке одного запроса llama.cpp использует микробатчинг внутри. Разбивает вычисления внутри слоя на более мелкие пакеты, чтобы лучше использовать SIMD инструкции и кэш GPU.
Ollama обрабатывает всё последовательно. Слой за слоем, операция за операцией. Нет реорганизации вычислений для лучшей локализации данных.
На Qwen-3 Coder 32B это особенно критично: модель имеет 60 слоев, каждый слой - это матричные умножения разной размерности. Без интеллектуального батчинга GPU простаивает между операциями.
Практический тест: замеряем реальную разницу
Берем задачу из HumanEval: написать функцию для вычисления n-го числа Фибоначчи с мемоизацией.
Промпт: 150 токенов (описание + сигнатура функции)
Ожидаемый вывод: 80-120 токенов
| Метрика | Llama.cpp | Ollama |
|---|---|---|
| Время до первого токена | 120 мс | 350 мс |
| Среднее время на токен | 23.8 мс | 40.0 мс |
| Общее время (100 токенов) | 2.50 сек | 4.35 сек |
| Потребление VRAM | 18.2 GB | 19.8 GB |
Ollama тратит почти втрое больше времени на инициализацию. Потому что готовит весь конвейер заранее, выделяет память под максимальный возможный вывод, инициализирует структуры данных, которые могут не понадобиться.
Llama.cpp делает lazy initialization: выделяет память по мере необходимости, инициализирует структуры когда они реально нужны.
Можно ли ускорить Ollama до уровня llama.cpp?
Теоретически - да. Практически - нет, без переписывания половины кода.
Но можно сократить разницу с 70% до 30-40%. Вот что помогает:
3Настройка переменных окружения
OLLAMA_NUM_PARALLEL - увеличить с 1 до количества ядер CPU
OLLAMA_MAX_LOADED_MODELS - уменьшить до 1, если не нужна быстрая смена моделей
OLLAMA_KEEP_ALIVE - установить в -1, если модель работает постоянно
4Модификация Modelfile
Параметр PARAMETER num_threads установить в количество физических ядер
PARAMETER numa распределить память между NUMA нодами
Системный промпт сделать короче или вынести в отдельный вызов
Но даже с этими оптимизациями вы упретесь в архитектурные ограничения Ollama. Потому что...
Главная проблема: Ollama создавалась для удобства, а не для скорости
Проект начинался как простой способ запускать модели на Mac. Drag-and-drop, автоматическое скачивание, красивый веб-интерфейс.
Llama.cpp создавался как research проект для максимальной производительности на ограниченном железе. Каждая миллисекунда на счету.
Это различие в философии проявляется везде:
- Ollama кэширует модели в удобном для пользователя формате
- Llama.cpp кэширует промежуточные результаты вычислений
- Ollama оптимизирована для быстрого старта
- Llama.cpp оптимизирована для длительной работы
- Ollama жертвует памятью ради простоты
- Llama.cpp жертвует всем ради памяти и скорости
В обзоре фреймворков я уже отмечал: выбирать между удобством и скоростью. Сейчас видно, насколько эта дихотомия реальна.
Когда Ollama все еще имеет смысл
Несмотря на все цифры, Ollama не стоит списывать со счетов. Есть сценарии, где 70% разницы в скорости - приемлемая плата за удобство:
- Быстрое прототипирование - когда нужно за 5 минут запустить модель и проверить идею
- Обучение и демонстрации - когда важна простота установки и настройки
- Мультимодельные сценарии - когда постоянно переключаешься между разными моделями
- Работа на слабом железе - когда модель все равно упирается в GPU, и оптимизации llama.cpp не дадут преимущества
Но для production, для генерации кода в CI/CD, для автоматического рефакторинга - каждый токен на счету. Здесь выбор очевиден.
Что будет с Qwen-3 Coder 32B через год?
Модели становятся больше. Контексты - длиннее. Qwen-3 Coder 32B сегодня - это золотая середина между качеством и требовательностью к ресурсам. Но уже появляются 40B модели, которые бьют рекорды в кодинге.
Разница в 70% на 32B модели превратится в разницу в 2-3 раза на 70B модели. Потому что overhead Ollama растет нелинейно с размером модели.
Мой прогноз: через год мы увидим либо:
- Полную переработку архитектуры Ollama с фокусом на производительность
- Появление новых оберток над llama.cpp, которые сохранят удобство Ollama, но добавят оптимизации
- Переход сообщества на raw llama.cpp с кастомными скриптами для удобства
Пока что, если вы генерируете код на Qwen-3 Coder 32B и похожих моделях - тестируйте оба варианта. Замеряйте реальную производительность на своих задачах. И помните: удобство - это хорошо, но время генерации - это деньги. Особенно когда ждешь ответа от модели в середине рабочего дня.