Почему ваша LLM медленная? Проблема не в железе, а в подходе
Вы загружаете модель, отправляете запрос и ждете 2-3 секунды. Для чата это терпимо. Но что если вам нужно классифицировать миллион документов? Входящие письма, юридические контракты, технические спецификации. Ждать неделю? Нет.
Типичная ошибка – воспринимать LLM как черный ящик для последовательных запросов. На 13 марта 2026 года это уже архаизм. Современная задача – потоковая обработка, где на входе тысячи токенов (текст документа), а на выходе – один (категория). Задержка в 50 мс на запрос превращается в часы простоя инфраструктуры.
Qwen 3.5 27B и llama.cpp: почему этот дуэт бьет все рекорды
В 2026 году на рынке сотни моделей. Но для высоконагруженной классификации нужен специфический профиль: хорошее понимание контекста, стабильность вывода и, главное, эффективность при квантовании. Qwen 3.5 27B (актуальная версия на март 2026) – золотая середина.
Она достаточно большая, чтобы точно понимать смысл длинных документов (поддерживает контекст до 128k), и при этом ее архитектура отлично переносит агрессивное квантование до уровня Q5_K_XL без потери качества в задачах классификации. Llama.cpp – не просто раннер, а высокооптимизированный движок инференса на C/C++, который умеет параллелить вычисления на уровне батча, а не только на уровне запроса. Это ключ.
Пока другие фреймворки тратят время на переключение контекста между запросами, llama.cpp складывает их в один большой тензор и обрабатывает за один проход через модель. Разница – на порядок.
1 Собираем llama.cpp с нуля: никаких готовых бинарников
Первая ошибка – скачать скомпилированный бинарник. Он собран под усредненное железо. Ваш GPU (надеюсь, это хотя бы RTX 4090 или A100) – уникален. Сборка под вашу архитектуру CUDA, вашу версию компилятора дает прирост 15-20% сразу.
# Клонируем репозиторий. На 13.03.2026 основной бранч - master.
git clone https://github.com/ggerganov/llama.cpp.git
cd llama.cpp
# Переключаемся на последний стабильный коммит (на текущий момент это обязательно)
git checkout $(git describe --tags --abbrev=0)
# Собираем с поддержкой CUDA 13.5 (актуально на март 2026)
make clean
LLAMA_CUBLAS=1 make -j$(nproc) server
Флаг LLAMA_CUBLAS=1 включает вычисления на GPU через cuBLAS. Если у вас несколько карт, добавьте LLAMA_CUDA_NVCC="-arch=native" для компиляции под вашу конкретную архитектуру. Это не магия, это необходимость.
Не используйте флаг LLAMA_CUDA_FORCE_DMMV просто так. Он принудительно использует один тип матричного умножения и может замедлить работу на новых картах серии 50xx. Оставьте выбор оптимального ядра движку.
2 Выбор и загрузка модели: Q5_K_XL – наш чемпион
Берем оригинальную Qwen 3.5 27B Instruct в формате GGUF. Забудьте про FP16 – она съест 50+ GB VRAM. Нам нужен квант. Но какой? Q4_0 слишком грубый, Q8_0 почти не дает выигрыша в скорости. Q5_K_XL – идеальный баланс между качеством (падение точности менее 1% для классификации) и размером (~17 GB).
# Скачиваем модель. На март 2026 актуальный источник - Hugging Face.
# Ищем файл: Qwen3.5-27B-Instruct-Q5_K_XL.gguf
# Пример команды с использованием wget:
wget -c https://huggingface.co/Qwen/Qwen3.5-27B-Instruct-GGUF/resolve/main/Qwen3.5-27B-Instruct-Q5_K_XL.gguf -O models/qwen3.5-27b-q5_k_xl.gguf
Почему не Qwen 3.5 32B или 72B? Потому что закон убывающей отдачи. Для точной классификации документов 27B параметров более чем достаточно, а разница в скорости инференса будет колоссальной. Если вы сомневаетесь в выборе модели для своих задач, посмотрите наше сравнение моделей под 128 ГБ VRAM.
3 Запуск сервера: флаги, от которых зависит всё
Теперь самое важное – запуск сервера с правильными параметрами. Вот команда, которая запускает движок в режиме, готовом к батчингу.
./server -m ./models/qwen3.5-27b-q5_k_xl.gguf \
-c 131072 \ # Контекст 128k (работаем с длинными документами)
-b 512 \ # Размер пакета (batch size) для прогнозирования токенов
--batch-size 32 \ # Количество последовательностей, которые можно обработать параллельно (параллельный батчинг)
--ubatch-size 512 \ # Физический размер тензора для вычислений (критически важно!)
--ctx-size 2048 \ # Размер контекста для кеша K/V (можно меньше, если документы короче)
-ngl 99 \ # Слои на GPU (все, что возможно)
-cba \ # Кеширование батча внимания (новая фича 2026 года, ускоряет повторные вычисления)
--port 8080 \
--host 0.0.0.0 \
--log-format json \
--metrics
Давайте разберем ключевые моменты, потому что слепое копирование здесь сломает вам систему.
- -c 131072: Устанавливает максимальный контекст модели. Для Qwen 3.5 это 128k. Но это не значит, что каждый запрос будет таким длинным. Это лимит.
- --batch-size 32: Это количество независимых запросов, которые сервер может принять и обработать параллельно. Если отправляете 100 документов, сервер возьмет первые 32 и начнет работу.
- --ubatch-size 512: Самая важная настройка. Определяет, сколько токенов обрабатывается за один физический вызов GPU. Если у вас документы по 2000 токенов, а ubatch-size 512, модель будет обрабатывать каждый документ по частям, но параллельно для всего батча. Значение 512 – оптимально для карт с 24+ GB VRAM. Для меньшей памяти уменьшайте до 256 или 128.
Подробнее о каждом аргументе можно прочитать в нашем гиде "Аргументы llama.cpp: от слепого копирования к осознанной настройке".
4 Клиент, который не просит, а требует: пишем эффективный отправитель
Сервер готов. Теперь нужен клиент, который не будет отправлять запросы по одному. Он должен формировать батчи и отправлять их пачками. Вот пример на Python с использованием asyncio и aiohttp для асинхронной отправки.
import aiohttp
import asyncio
import json
from typing import List
class LlamaCPPClient:
def __init__(self, base_url: str = "http://localhost:8080"):
self.base_url = base_url
self.session = None
async def __aenter__(self):
self.session = aiohttp.ClientSession()
return self
async def __aexit__(self, *args):
await self.session.close()
async def classify_batch(self, texts: List[str], system_prompt: str) -> List[str]:
"""Отправка батча документов на классификацию."""
tasks = []
for text in texts:
# Промпт для классификации. В выводе просим ТОЛЬКО название категории.
full_prompt = f"""{system_prompt}
Документ:
{text[:120000]} # Обрезаем, если документ длиннее контекста
Категория документа (только название):"""
payload = {
"prompt": full_prompt,
"n_predict": 5, # Нам нужно всего несколько токенов на выходе!
"temperature": 0.1, # Минимальная случайность для стабильности
"stop": ["\n"], # Останавливаемся на новой строке
"batch_size": len(texts) # Сообщаем серверу размер ожидаемого батча
}
task = self.session.post(f"{self.base_url}/completion", json=payload)
tasks.append(task)
# Отправляем все запросы конкурентно
responses = await asyncio.gather(*tasks, return_exceptions=True)
categories = []
for resp in responses:
if isinstance(resp, Exception):
categories.append("ERROR")
continue
result = await resp.json()
categories.append(result['content'].strip())
return categories
# Пример использования
async def main():
documents = ["Длинный текст договора...", "Техническое описание продукта..."] * 100 # 200 документов
system_prompt = "Ты – эксперт по классификации документов. Определи категорию: Договор, Инструкция, Отчет, Письмо."
async with LlamaCPPClient() as client:
categories = await client.classify_batch(documents, system_prompt)
print(f"Обработано {len(categories)} документов")
if __name__ == "__main__":
asyncio.run(main())
Обратите внимание на "n_predict": 5. Мы просим модель сгенерировать не более 5 токенов. Ведь нам нужна только категория. Это резко сокращает время выполнения. Сервер, видя одинаковую длину вывода и параллельный батчинг, оптимизирует вычисления.
Как достичь 2000 TPS: метрики и подводные камни
Настройка завершена. Запускаем тест: 10 000 документов по 1000 токенов каждый. При правильной конфигурации (RTX 4090, 24 GB VRAM, 32-ядерный CPU) вы должны увидеть следующие цифры:
| Параметр | Значение (оптимальное) | Что ломает |
|---|---|---|
| Скорость обработки батча (32 документа) | ~1200 мс | Слишком большой ubatch-size приводит к нехватке VRAM. |
| Время на документ (в батче) | ~37.5 мс | Последовательная отправка запросов вместо батча. |
| Теоретический максимум TPS | ~2600 | Неправильный промпт, где модель генерирует лишний текст. |
| Использование VRAM | 20-22 GB | Попытка использовать контекст 128k для всех документов одновременно. |
Реальный TPS будет ниже теоретического из-за накладных расходов сети и сериализации. Но 2000 TPS – достижимая цифра. Секрет в том, что загрузка GPU должна быть близка к 100% все время. Если ваш график использования GPU похож на гребенку – вы делаете что-то не так. Батчи должны подаваться непрерывно.
Ошибки, которые сведут на нет всю оптимизацию
- Динамический контекст для каждого запроса. Если один документ на 100 токенов, а другой на 100 000, сервер будет вынужден пересчитывать кеш внимания под каждый размер. Стандартизируйте вход – обрезайте или разделяйте документы на чанки фиксированной длины.
- Игнорирование логов сервера. Включите
--metricsи смотрите наavg_batch_load_time. Если он растет – вы исчерпали ресурсы. - Токенизация на стороне клиента. Не рассчитывайте длину промпта в символах. Используйте токенизатор модели. В llama.cpp есть API для этого. Отправка запроса, который не влезает в контекст, приведет к тихому обрезанию и ошибкам классификации.
- Попытка уместить все на одной карте. Если документы действительно огромные (например, целые книги), а TPS все еще критичен, смотрите в сторону распределенной инференса на несколько GPU.
Самая обидная ошибка: забыть про --ubatch-size. Без него llama.cpp будет обрабатывать токены последовательно даже внутри одного документа, и вы никогда не увидите больше 100 TPS, как бы круто ни было железо.
Что дальше? Замена классики на VLM
Вы достигли 2000 TPS для текстовых документов. Отлично. А если документы – это сканы с текстом, таблицами и схемами? Текстовая модель слепа. Следующий шаг – мультимодальная классификация с помощью Qwen3.5-VL 2B или аналогичной компактной VLM. Принцип тот же: батчинг, квантование, но теперь нужно эффективно кодировать изображения. И здесь нас ждет новый вызов – оптимизация пайплайна обработки картинок. Но это уже тема для другого раза. Если хотите подготовиться, изучите наш гайд по настройке LoRA для Qwen3-VL 2B.
Главное – перестать думать о LLM как о магическом шаре, который "думает". Нагрузочное тестирование, профилирование, чтение логов. Ваш ИИ-пайплайн должен быть так же измерим и управляем, как любая база данных. Только тогда вы получите не просто работающую систему, а оружие для бизнеса.