Блог AST-SoftPro
Qwen3.8-27B на локальном железе: бенчмарк длинного контекста
Зачем бенчить длинный контекст
Когда мы в AST-Soft переводим рабочие сценарии с облачных API на локальные LLM, главный вопрос — не «можно ли запустить 27B-модель на двух RTX 4070», а «как она себя поведёт, когда промпт уедет за 30k токенов». Документы на 60–100k — это повседневность для RAG-ассистента: PDF-книги, репозитории, длинные логи. На коротких контекстах всё летает, а на длинных — графики проваливаются вниз, потому что attention квадратичный.
Мы прогнали одинаковую батарею тестов на двух стендах с Qwen3.8-27B в llama.cpp — Q4_K_M на Ubuntu-сервере с 2×RTX 4070 (24 ГБ VRAM, Xeon E5-2680 v4, 64 ГБ DDR4 ECC) и Q6_K_M на Windows-десктопе с 2×RTX 5060 Ti 16 ГБ (32 ГБ VRAM, i5-12600K, 32 ГБ DDR4). Один и тот же бинарь, одинаковые флаги спекулятивного декодинга, одинаковый набор промптов.
Стенды
| Параметр | Стенд A (Ubuntu Server) | Стенд B (Windows) |
|---|---|---|
| CPU | 2× Intel Xeon E5-2680 v4 @ 2.40 ГГц, 14c/28t × 2 = 28c/56t, NUMA | Intel Core i5-12600K, 10c/16t |
| RAM | 64 ГБ DDR4 ECC (4 канала, ~2133 МГц) | 32 ГБ DDR4 |
| Материнская плата | X99 MD8 (серверная, ECC) | десктопная LGA1700 |
| GPU | 2× RTX 4070 12 ГБ, sm_89, без NVLink | 2× RTX 5060 Ti 16 ГБ, sm_120 |
| VRAM | 24 ГБ (tensor split) | 32 ГБ (tensor split) |
| llama.cpp | build-ubuntu (NCCL off, tensor split, flash-attn on) | build-windows (tensor split, flash-attn on) |
| Модель | Qwen3.8-27B-Q4_K_M.gguf (17.1 ГБ) | Qwen3.8-27B-UD-Q6_K_M.gguf (23.1 ГБ) |
| KV-cache | q8_0 | q8_0 |
| Спекуляция | MTP + ngram-mod (драфт-токены со 2 попытками) | MTP + ngram-mod |
| Контекст | 100 096 | 128 000 |
Стенд B — две RTX 5060 Ti по 16 ГБ, итого 32 ГБ VRAM, чего хватает под 128k. У стенда A — две карты по 12 ГБ без NVLink, шард через --split-mode tensor, размер контекста упирается в суммарную VRAM (q8 KV-cache на 100k токенов для 27B занимает ~2.6 ГБ). По процессорам: 56 потоков Xeon-сервера vs 16 потоков i5-12600K — для prompt-bound задач (длинный контекст) разница не критична, упираемся в GPU.
Методика
Для каждого размера контекста мы:
- Собирали «идеальный» промпт точно на N токенов через
/tokenize+/detokenize(важно: 4 символа ≈ 1 токен работает только для английского; кириллица даёт ~2.5 символа на токен, и без пре-токенизации легко уйти в 400 Bad Request). - Отправляли
POST /completionсcache_prompt: false— чтобы промпт обрабатывался с нуля, без KV-кеша от прошлых запросов. - Гоняли генерацию 64 токенов (для теста prompt eval) и 256/1024/2048 (для теста спекуляции).
Все числа ниже — из встроенных таймингов llama.cpp (prompt_per_second, predicted_per_second), а не из wall-clock. Это честнее, потому что включает только compute, а не сетевой round-trip.
Откуда TG больше 100 tok/s — про спекулятивный декодинг
Если вы впервые видите 200–420 tok/s у 27B-модели на двух RTX 4070 — реакция «это нереально» правильная. Без спекуляции llama.cpp выдаёт 30–60 tok/s на таком железе. Цифры в таблице ускорены — и вот как именно.
Оба стенда запущены с MTP + ngram-драфтом:
--spec-type draft-mtp,ngram-mod
--spec-draft-n-max 2 # до 2 драфт-токенов за шаг target-модели
--spec-ngram-mod-n-match 24
--spec-ngram-mod-n-min 24
--spec-ngram-mod-n-max 86
Идея: на каждом forward-проходе target-модель проверяет не один свой токен, а пачку драфт-токенов, предложенных MTP-головой и ngram-матчером. Если драфт угадал — токены идут в выход без дополнительных forward-ов. Чем длиннее генерация, тем точнее работает ngram (он смотрит на N=24 предыдущих токенов и ищет повторяющиеся паттерны в собственной истории выхода).
Доказательство — прямо в логе llama.cpp (/tmp/llama-server-q4.log):
task 142 (tg=1024): draft acceptance = 0.99270, mean len = 2.98
task 489 (tg=2048): draft acceptance = 0.99901, mean len = 60.21
task 7184 (длинная): draft acceptance = 0.72284, mean len = 3.36
Строка mean len = 60.21 в task 489 — это средняя длина удачно угаданной драфт-пачки. На пике ngram выдавал по 60 токенов за один шаг target-модели, и target выполнил всего 34 шага вместо 2048. Поэтому 420 tok/s — это темп выдачи токенов клиенту, а не скорость target forward-pass как такового.
В таблице ниже «TG tok/s» означает именно predicted_per_second из ответа llama.cpp — общее число токенов, попавших в predicted_n, делённое на predicted_ms. То есть «сколько токенов в секунду доходит до пользователя», включая драфт-угадывания.
Если нужен «честный» target-only темп, надо гонять тот же бенч с --spec-type none. Тогда 27B-Q4 на 2×4070 выдаёт ~60 tok/s на коротком контексте и проседает до 30 на pp 100k. Разница в 7 раз между 420 и 60 — и есть вклад спекулятивного декодинга.
Prompt Processing: как деградирует скорость
Это главная таблица, ради которой всё затевалось. pp tok/s — сколько токенов промпта модель обрабатывает в секунду. На коротких контекстах она близка к пику пропускной способности attention-слоёв, на длинных падает из-за O(n²).
| Промпт (токены) | Стенд A (Q4, 2×4070) | Стенд B (Q6, 2×5060 Ti) | Δ A vs B |
|---|---|---|---|
| 512 | 880 | 535 | +64% |
| 2 048 | 1 023 | 588 | +74% |
| 8 192 | 1 041 | 628 | +66% |
| 16 384 | 968 | 631 | +53% |
| 32 768 | 825 | 600 | +38% |
| 65 536 | 913 | 531 | +72% |
| 98 304 | 818 | 477 | +71% |
| 100 000 (max A) | 811 | — | — |
| 120 000 (max B) | — | 448 | — |
Парадокс: Q4 на двух 4070 обгоняет Q6 на двух 5060 Ti на всех размерах, кроме совсем коротких. На 512 токенов стенд B не успевает даже раскочегариться — MTP-драфт не набрал статистику для ngram-матчинга, генерация всего 64 токена, спекуляция не помогает. На длинных промптах 2×4070 в среднем на 60–70% быстрее по пропускной способности.
Деградация pp на 100k относительно пика (1041 tok/s на 8k):
- Стенд A: с 1041 до 811 → –22%
- Стенд B: с 631 до 448 → –29%
Это лучше, чем «идеальные» O(n²)-ожидания — благодаря flash-attn и q8 KV-cache attention реально близок к O(n·√n). Но всё равно ощутимо: 100k промпт на стенде A обрабатывается 130 секунд, на стенде B — 208 секунд на 98k и 272 секунды на 120k.
Генерация: токены в секунду на выходе
Генерация — другая история. Здесь важна per-token latency, и она зависит от того, насколько эффективно работает спекулятивный декодер (MTP + ngram). Чтобы не путать с PP, в этом разделе две отдельные таблицы: сначала tg на длинном промпте (показывает, как растущий KV-cache давит на декод-шаг), потом tg на коротком промпте (показывает, как ngram-драфт набирает статистику).
Генерация на длинном контексте (фиксированный tg=64)
| Тест | Стенд A tg tok/s | Стенд B tg tok/s | Комментарий |
|---|---|---|---|
| pp 8k + tg 64 | 62 | 74 | короткая генерация на длинном контексте |
| pp 32k + tg 64 | 62 | 73 | то же, но KV вдвое больше |
| pp 65k + tg 64 | 71 | 52 | — |
| pp 100k + tg 64 | 50 | — | — |
Генерация на коротком промпте (фиксированный pp=512)
| Тест | Стенд A tg tok/s | Стенд B tg tok/s | Комментарий |
|---|---|---|---|
| pp 512 + tg 256 | 196 | 191 | короткая генерация, драфт ещё не разогнался |
| pp 512 + tg 1024 | 85 | 300 | драфт набрал статистику на стенде B; на A — просадка из-за CPU-самплера (CPU fallback на 2×GPU, см. лог) |
| pp 512 + tg 2048 | 420 | 328 | пик спекуляции на обоих стендах |
Закономерности:
- Короткая генерация (tg 64–256) на длинном контексте проседает — каждый декод-шаг читает весь KV-кэш, а ngram не успевает накопить достаточно токенов для матчинга (драфт принимает 2–3 токена из 5, средняя длина 2.98 на tg=256).
- Длинная генерация (tg 1024–2048) — ngram-драфт набирает силу: на 2×5060 Ti видим 300 tok/s уже на 1024 (драфт принимает 6+ токенов за шаг), на 2×4070 пик 420 tok/s на 2048.
- На tg=1024 картина несимметричная: 5060 Ti — 300 tok/s (драфт хорошо матчит), 4070 — 85 tok/s (CPU-fallback самплера на tensor-split). К tg=2048 стенд A выходит на пик 420 tok/s, обгоняя B (328). В реальном сценарии «впихнуть 200-строчную функцию» обе машины выдают её за 4–5 секунд.
Качество: текст, код, tool calling
Скорость — это полдела. Мы прогнали мини-бенч из 8 кейсов на обоих стендах. Поскольку модель одна и та же, разницы в качестве не ожидалось — её и не оказалось: 7 из 8 PASS на обоих.
| Категория | Кейс | Стенд A | Стенд B |
|---|---|---|---|
| Текст | Столица Бразилии (одно слово) | Бразилиа ✅ | Бразилиа ✅ |
| Текст | Логика: 3 яблока, отдал половину → 1.5 | 1,5 ✅ | 1,5 ✅ |
| Текст | Суммаризация в ≤20 слов | 13 слов ✅ | 13 слов ✅ |
| Код | Python FizzBuzz 1–15 | def + for + FizzBuzz ✅ | ✅ |
| Код | JavaScript факториал (стрелочная) | ❌ нет return | ❌ нет return |
| Код | SQL: топ-3 клиентов по сумме | SELECT + JOIN + ORDER BY + LIMIT 3 ✅ | ✅ |
| Tools | get_weather(city="Москва") | вызвал ✅ | ✅ |
| Tools | search_web + (опц.) send_email | вызвал search_web ✅ | ✅ |
Единственный общий fail — JS-факториал без явного return. Модель выдаёт n => n * factorial(n-1) в одну строку и полагается на неявный return стрелочной функции без фигурных скобок — а это синтаксически некорректно (нет базового случая, и сама запись не вернёт значение для n=0). В реальной кодогенерации это не проблема: модель даёт рабочий код, если просить «с базовым случаем и без рекурсии в одну строку».
Tool calling работает стабильно: /v1/chat/completions с массивом tools корректно отдаёт tool_calls с правильными аргументами. Это критично для агентских сценариев, и Qwen3.8-27B здесь показывает себя как полноценная замена облачным моделям уровня GPT-4o-mini.
Что мы вынесли
- Для 27B-квантовок длинный контекст — это про пропускную способность attention, а не про память. 2×4070 с q8 KV уверенно держит 100k без вылета по VRAM. Спекулятивный декодер MTP + ngram даёт 200–400 tok/s на длинной генерации — это уже «комфортная» скорость для интерактивной работы.
- Две карты Ada (sm_89) бьют две карты Blackwell (sm_120) на pp tok/s в широком диапазоне размеров промпта. Не потому что «Ada быстрее», а потому что Q4 на 2×GPU с тензорным шардом — это больше совокупной пропускной способности памяти и более низкие задержки на коротких пайплайнах, чем Q6 на тех же двух GPU.
- Качество не деградирует при квантовании Q4_K_M vs Q6_K_M на наших тестах — что логично: разница между Q4 и Q6 на русском/английском тексте и коде минимальна и перекрывается эффектом temperature=0.
- Tool calling работает out-of-the-box через OpenAI-совместимый эндпоинт. Это снимает главный барьер для перевода агентов с облака на self-hosted.
Когда что выбирать
- Стенд A (Q4, 2×4070 + 2×Xeon E5-2680 v4): оптимален для RAG-сценариев с длинными документами (10k–100k промпт) и короткими ответами. Дешевле по железу, быстрее на pp. Хорош как self-hosted в офисном сервере (X99 + б/у Xeon — низкий TCO).
- Стенд B (Q6, 2×5060 Ti 16 ГБ + i5-12600K): выигрывает на длинной генерации (tg > 1000) и на максимальном контексте 128k. Лучше, если важно качество в маргинальных кейсах (сложная математика, редкие языки). Десктопный форм-фактор с DDR4 и одним CPU проще в обслуживании.
В следующей статье — замеры vLLM с FP8-квантованием и сравнение MTP против чистого ngram-драфта. Если у вас есть свои сценарии, которые вы хотели бы увидеть в бенче — пишите, добавим.