Ваша модель Qwen 3.5 вдруг стала тупить? Возможно, это не железо, а баг
Запускаете Qwen 3.5 (например, актуальную на апрель 2026 года версию Qwen3.5-72B-2026-03) через llama.cpp. Первые запросы летят быстро. Потом - паузы. Генерация замедляется в два, а то и в три раза. В oMLX.ai та же история: контекстный кеш будто сходит с ума, съедая лишние гигабайты памяти.
Вы проверяете температуру GPU, обновляете драйверы, ругаетесь на производителей видеокарт. А проблема - в одной строчке кода. Вернее, в её отсутствии.
На 08.04.2026 этот баг актуален для всех версий Qwen 3.5, конвертированных в GGUF формат. Особенно критично для long-context моделей (128K+) и агентских сценариев с tool calling.
Что ломается и как это заметить
Симптомы кажутся размытыми, пока не узнаешь, куда смотреть.
- Деградация производительности во времени: первый промпт - 50 токенов в секунду, десятый - 20. Без изменения длины контекста.
- Скачки потребления памяти: llama.cpp внезапно запрашивает на 15-20% больше VRAM после нескольких итераций диалога.
- Артефакты в логах: при детальном логировании (--log-disable в llama.cpp) видите повторяющиеся служебные токены
<|im_start|>там, где их быть не должно. - Сломанный кеш в oMLX.ai: система сообщает о cache hit, но latency не уменьшается. Иногда вообще падает с ошибкой переполнения буфера кеша.
В чём связь между этими симптомами? Все они указывают на один корень: неправильную работу ключевого механизма кеширования контекста (KV-cache).
Диагностика: ищем не там, где потеряли, а где светло
Первое, что делают 90% разработчиков - начинают ковырять настройки quantization или параметры инференса. Это тупик.
Правильный путь - смотреть на сырой промпт, который уходит в модель. Запустите llama.cpp с флагом --prompt-cache и --log-disable, затем сохраните промпты до и после нескольких итераций.
./main -m qwen3.5-72b.Q4_K_M.gguf -p "Тестовый запрос" --interactive --log-disable --prompt-cache cache.bin --prompt-cache-allЧто вы увидите? В идеале, для каждого нового пользовательского сообщения в диалоге, модель должна получать только новые токены. Старые - должны браться из кеша.
А в реальности - после второго сообщения система начинает подмешивать в промпт закрывающие теги предыдущих ответов или дублировать служебные разделители. Для модели это как читать книгу, где каждую главу начинают с абзаца из предыдущей. Она сбивается, кеш становится невалидным, и inference начинается почти с нуля.
1Корень зла: сломанная логика в Jinja-шаблоне
Откройте стандартный chat template для Qwen 3.5. В llama.cpp он лежит в chat_templates/qwen.jinja. В oMLX.ai - встроен в рантайм. Нас интересует блок обработки истории диалога.
Оригинальный код (упрощённо) выглядит так:
{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}<|im_start|>user
{{ message['content'] }}<|im_end|>
<|im_start|>assistant
{% endif %}{% endfor %}Кажется, всё нормально? Нет. Проблема в том, как этот шаблон применяется при инкрементальной генерации (когда у нас уже есть часть ответа от ассистента).
Когда система пытается добавить новое сообщение пользователя к существующему кешу, она должна вставить только токены нового пользовательского промпта. Но из-за ошибки в условных операторах шаблон пересобирает всю историю с начала, включая старые теги <|im_end|>.
Для модели токен <|im_end|> - это маркер конца сообщения. Если он появляется в середине контекста, ломается вся логика attention mask. Кеш становится непригодным для reuse.
Особенно критично это для режима "reasoning" (think step) в Qwen 3.5 Next. Модель начинает "думать" внутри тегов, а шаблон их дублирует, создавая бесконечные циклы. Мы писали об этом в статье о критических багах парсера.
2Исправление: патчим шаблон на лету
Не нужно ждать фикса от разработчиков llama.cpp (хотя на 08.04.2026 он ещё не выпущен). Исправляем сами.
Создайте новый файл qwen_fixed.jinja рядом с моделью GGUF. Содержимое:
{{ bos_token }}{% for message in messages %}{% if message['role'] == 'user' %}<|im_start|>user
{{ message['content'] }}<|im_end|>
<|im_start|>assistant
{% elif message['role'] == 'assistant' %}{{ message['content'] }}{% if not loop.last %}<|im_end|>
{% endif %}{% endif %}{% endfor %}Ключевое изменение: убрано автоматическое добавление <|im_start|>assistant для каждого сообщения пользователя. Вместо этого тег ассистента добавляется только когда действительно начинается ответ модели. Это предотвращает дублирование.
Второе: закрывающий тег <|im_end|> добавляется только если после ответа ассистента есть ещё сообщения в истории (проверка {% if not loop.last %}).
3Применяем исправление в llama.cpp
Запускайте модель с указанием кастомного шаблона:
./main -m qwen3.5-72b.Q4_K_M.gguf --chat-template ./qwen_fixed.jinja -p "Ваш запрос"Для постоянного использования конвертируйте модель с исправленным шаблоном внутрь GGUF файла (это немного хакерский способ, но работает):
python convert.py --outfile qwen_fixed.gguf --chat-template ./qwen_fixed.jinja исходная_модель/4Настройка для oMLX.ai
В oMLX.ai (актуальная версия на апрель 2026 - 2.3.x) процесс сложнее, потому что система использует собственный компилятор шаблонов.
Вам нужно создать custom chat handler. Пример конфигурации в YAML:
model_config:
name: "qwen3.5-72b"
chat_template: |
{{ bos_token }}{% for message in messages %}
{% if message['role'] == 'user' %}
<|im_start|>user
{{ message['content'] }}<|im_end|>
<|im_start|>assistant
{% elif message['role'] == 'assistant' %}
{{ message['content'] }}
{% if not loop.last %}<|im_end|>
{% endif %}
{% endif %}
{% endfor %}
cache_config:
reuse_context: true
max_reuse_tokens: 16384Важный момент: в oMLX.ai нужно явно включить reuse_context и задать max_reuse_tokens. Иначе система будет игнорировать кеш, даже с исправленным шаблоном.
Ошибки, которые вы совершите (и как их избежать)
| Ошибка | Последствие | Исправление |
|---|---|---|
| Пропустить проверку loop.last | Модель не завершит ответ, создавая бесконечную генерацию | Всегда добавляйте условие для последнего элемента |
| Использовать старый шаблон с новыми моделями Qwen Next | Сломается reasoning и tool calling | Берите шаблоны только из официального репозитория на дату 08.04.2026 |
| Забыть про bos_token в начале | Модель будет неправильно интерпретировать начало последовательности | Всегда включайте {{ bos_token }} в первый блок |
Самая коварная ошибка: думать, что исправление шаблона автоматически решит все проблемы с производительностью. Нет. После патча нужно сбросить кеш и перезапустить инференс-сессию. В llama.cpp удалите файл cache.bin. В oMLX.ai перезапустите контейнер модели.
Что в итоге? Производительность возвращается
После применения исправления:
- Скорость генерации стабилизируется. Не будет деградации на длинных диалогах.
- Потребление памяти снизится на 15-30% для контекстов от 8K токенов.
- Кеш начнёт работать правильно: cache hit rate в oMLX.ai поднимется с 40-50% до 85-90%.
Это не магия. Это просто исправление одной логической ошибки, которая стоила вам сотен часов процессорного времени.
Последний совет: никогда не доверяйте стандартным шаблонам вслепую. Особенно в быстро развивающейся экосистеме локальных LLM. Всегда проверяйте, что на самом деле уходит в модель. Инструменты вроде --log-disable в llama.cpp или дебаг-режим в oMLX.ai - ваши лучшие друзья.
И да, если вы столкнулись с ошибкой "Failed to parse at pos" после манипуляций с шаблонами, у нас есть отдельное руководство по её исправлению. Сохраните его в закладках.
Теперь идите и заставьте свою Qwen 3.5 летать. Она этого достойна.