Настройка llama.cpp и Qwen 3.5 27B для 2000+ TPS: гайд по классификации документов | AiManual
AiManual Logo Ai / Manual.
13 Мар 2026 Гайд

Как настроить llama.cpp и Qwen 3.5 27B для рекордной TPS при классификации документов

Детальное руководство по настройке llama.cpp сервера и Qwen 3.5 27B модели для обработки более 2000 документов в секунду. Оптимизация батчинга, контекста 128k и

Почему ваша LLM медленная? Проблема не в железе, а в подходе

Вы загружаете модель, отправляете запрос и ждете 2-3 секунды. Для чата это терпимо. Но что если вам нужно классифицировать миллион документов? Входящие письма, юридические контракты, технические спецификации. Ждать неделю? Нет.

Типичная ошибка – воспринимать LLM как черный ящик для последовательных запросов. На 13 марта 2026 года это уже архаизм. Современная задача – потоковая обработка, где на входе тысячи токенов (текст документа), а на выходе – один (категория). Задержка в 50 мс на запрос превращается в часы простоя инфраструктуры.

💡
TPS (Transactions Per Second) в контексте LLM – это не просто скорость генерации токенов. Это количество завершенных бизнес-задач в секунду. Классификация документа – одна транзакция. Наша цель – поднять этот показатель с 10-50 до 2000+.

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 как о магическом шаре, который "думает". Нагрузочное тестирование, профилирование, чтение логов. Ваш ИИ-пайплайн должен быть так же измерим и управляем, как любая база данных. Только тогда вы получите не просто работающую систему, а оружие для бизнеса.

Подписаться на канал