У заметки есть тематически связанные: Большие LLM на маленькой видеокарте - впихнуть не впихуемое и Форк llama.cpp с MoE Hot-Cache и Qwen3.8-Flash-Next.
Зачем городить свой тест
Все опубликованные бенчмарки про код меряют что-то своё: HumanEval, SWE-bench, LiveCodeBench. Они полезны, но у них есть неприятное свойство: они не говорят, как модель поведёт себя на моих задачах. У меня это типовой Битрикс: разбор цен с разными валютами и разделителями, деревья категорий с агрегатами, кэши, санитайзинг пользовательского HTML, разбор выражений. Модель может быть гением на SWE-bench и при этом сыпаться на парсере цен, потому что не подумала про неразрывные пробелы.
Второй момент: локальные модели различаются не только качеством, но и скоростью. Одно дело, когда модель отдаёт ответ за 15 секунд, и совсем другое, когда она думает восемь минут. В работе это важнее, чем пара процентов в таблице.
Поэтому я сделал свой маленький турнир: одинаковые задачи для всех моделей, автоматическая проверка кода тестами и замер скорости. Никакой вкусовщины.
Железо и обвязка
- CPU: AMD Ryzen 5 7500F (6 ядер / 12 потоков)
- RAM: 64 ГБ DDR5
- GPU: NVIDIA RTX 5060 Ti 16 ГБ (Blackwell, sm_120)
- Диск: NVMe 937 ГБ
- ОС: Ubuntu 24.04
Модели крутятся на моём форке llama.cpp с MoE Hot-Cache, все описаны в одном конфиге и переключаются одним менеджером. Наружу смотрит OpenAI-совместимый API на порту 8080, поэтому к серверу цепляются и Continue, и Kilo, и любой другой клиент. Большие MoE-модели живут частично в RAM, горячие эксперты держатся в видеопамяти, подробности - в заметках по ссылкам выше.
Кого тестировал
В турнире участвовало восемь моделей из моего меню:
- Tiel-Coder-35B-A3B (35B MoE, Q8_0) - моя основная рабочая лошадка для кода
- Ornith-1.5-35B (35B MoE, Q8_0, с размышлениями) - база, на которой сделан Tiel
- Cyber-Tiel-Coder-35B-A3B (35B MoE, Q8_0) - расцензуренная версия Tiel
- Qwen3.6-Heretic (35B MoE, Q8_0, с размышлениями) - без цензуры, общие задачи
- Gemma-4-26B-A4B (26B MoE, Q8_0) - легковесная MoE от Google
- Gemma-4-12B-Heretic (12B dense, Q6_K) - маленькая и быстрая, с кастрированной цензурой
- MiMo-V2.6-Distill-9B (9B dense, Q8_0, abliterated) - свежий дистилл от Xiaomi
- Qwen3.8-Flash-Next (125B MoE, Q6_K) - самая крупная модель в моём распоряжении
Кого-то может удивить, что тут нет «чистых» чат-моделей. Так и задумано: тест про код, поэтому и набор соответствующий - кодеры, универсалы и расцензуренные варианты, которые я реально использую.
Пять задач
Задачи я придумывал с двумя условиями: они должны быть сложными и при этом проверяемыми автоматически. Всё это - типовые вещи из практики на Битриксе, только с утрированными крайними случаями.
Задача 1. Парсер цен (15 проверок)
Функция parsePriceToRub(string $input): ?array должна разобрать строку с ценой и вернуть сумму с валютой, либо null, если разобрать нельзя. Сложность в том, что форматов куча: «1 234,56 руб.», «€1,234.56», «1.234,56 €», «$1,234.56», префиксы «от» и «до», неразрывные и узкие неразрывные пробелы, а дробным считается последний из разделителей. Плюс надо не сломаться на мусоре вроде «бесплатно».
Задача 2. Агрегация дерева категорий (10 проверок)
Функция aggregateCategoryTree(array $categories, array $products): array получает плоский список категорий с PARENT_ID и товары с ценами, а возвращает по каждой категории сумму и количество товаров во всём поддереве. В данных специально подложены циклы (категория 1 ссылается на 2, а 2 на 1), битые PARENT_ID и цены, записанные строкой. Функция обязана не зацикливаться.
Задача 3. LRU-кэш с TTL (9 проверок)
Класс LruTtlCache с инжектируемыми часами: get, put с TTL, вытеснение самой давно не использованной записи при переполнении. Часы передаются снаружи, поэтому проверка времени детерминированная, без sleep.
Задача 4. Санитайзер HTML и XSS (13 проверок)
Функция sanitizeHtml(string $html): string без сторонних библиотек: разрешить только b, i и a с href, вырезать script и style вместе с содержимым, выкинуть все атрибуты кроме href, запретить javascript: и data:, экранировать пользовательский текст.
Задача 5. Парсер арифметики без eval (15 проверок)
Функция calcExpression(string $expr): float|int: приоритеты операций, скобки, унарный минус, пробелы, деление на ноль как исключение. Использовать eval запрещено.
Вот сигнатуры, которые должны были реализовать модели:
parsePriceToRub(string $input): ?array
aggregateCategoryTree(array $categories, array $products): array
LruTtlCache(int $capacity, \Closure $clock)
sanitizeHtml(string $html): string
calcExpression(string $expr): float|int
Всего 62 проверки. Эталонные решения, которые я написал сам, проходят их полностью (62 из 62), так что задачи решаемы, а тесты корректны.
Как считались баллы
Никакой оценки на глаз. Прогонщик делает следующее:
- отправляет задачу модели через API и сохраняет ответ целиком;
- вытаскивает из ответа блок кода;
- проверяет синтаксис через
php -l; - прогоняет тесты задачи и записывает результат вида «пройдено из скольких»;
- отдельно фиксирует скорость генерации и общее время на задачу.
Модели запускались в том режиме, в котором я их реально использую, то есть с включёнными размышлениями, где они есть в меню.
Таблица результатов
Задачи: З1 - парсер цен, З2 - дерево категорий, З3 - LRU-кэш, З4 - санитайзер HTML, З5 - парсер арифметики.
| Модель | З1 | З2 | З3 | З4 | З5 | Итого | % | t/s | Время |
|---|---|---|---|---|---|---|---|---|---|
| Qwen3.8-Flash-Next-Q6 | 12/15 | 10/10 | 9/9 | 11/13 | 15/15 | 57/62 | 92% | 7.7 | 36 мин |
| Gemma-4-26B-A4B-Q8 | 15/15 | 10/10 | 9/9 | 12/13 | 5/15 | 51/62 | 82% | 39.9 | 5.7 мин |
| Tiel-Coder-35B-A3B-Q8 | 15/15 | 10/10 | 9/9 | 12/13 | 4/15 | 50/62 | 81% | 41.0 | 5.7 мин |
| MiMo-V2.6-Distill-9B (abliterated) | 7/15 | 7/10 | 0/9 | 13/13 | 5/15 | 32/62 | 52% | 45.3 | 7.5 мин |
| Gemma-4-12B-Heretic-Q6 | 3/15 | 10/10 | 6/9 | 0/13 | 13/15 | 32/62 | 52% | 38.1 | 2 мин |
| Cyber-Tiel-Coder-Q8 (uncensored) | 12/15 | 10/10 | 0/9 | 9/13 | 0/15 | 31/62 | 50% | 35.5 | 9 мин |
| Qwen3.6-Heretic-Q8 (thinking) | 0/15 | 9/10 | 9/9 | 11/13 | 0/15 | 29/62 | 47% | 39.3 | 10 мин |
| Ornith-1.5-35B-Q8 (thinking) | 14/15 | 10/10 | 0/9 | 4/13 | 0/15 | 28/62 | 45% | 41.9 | 10 мин |
Выводы
По качеству победил Flash-Next, но пользоваться им каждый день неудобно
Самая крупная модель в моём зоопарке взяла 57 баллов из 62 (92%) и оказалась единственной, кто полностью решил парсер арифметики и LRU-кэш. То есть задачи на настоящую алгоритмику ей даются заметно лучше, чем моделям на 35B. Плата за это - скорость: 7.7 токена в секунду и 36 минут на пять задач против пяти с половиной минут у 35B-моделей. Как «тяжёлая артиллерия» для по-настоящему сложных мест он хорош, как ежедневный инструмент - больно.
Лучший баланс качества и скорости - Tiel-Coder и Gemma-4-26B-A4B
Обе набрали 81-82% при скорости около 40 токенов в секунду, и обе почти идеально прошли первые три задачи: цены, дерево категорий, LRU-кэш. Разница между ними в одну проверку на парсере, то есть в пределах шума. Это и есть те модели, которыми приятно работать: ответ на задачу за минуту, а не за десять.
Парсер арифметики оказался главным разделителем
Из восьми моделей задачу не осилили шесть. Полностью справился только Flash-Next, почти справилась маленькая Gemma-4-12B (13 из 15). Показательно: модели, которые бодро пишут код на шаблонах, спотыкаются там, где нужно аккуратно реализовать алгоритм и не забыть про унарный минус и деление на ноль.
Расцензуривание стоит качества, и это заметно
Tiel-Coder набрал 81%, а его же расцензуренная версия Cyber-Tiel - ровно 50%. Qwen3.6-Heretic вообще ушёл в бесконечный цикл на парсере, тест пришлось прерывать по таймауту. Если нужен именно uncensored для кода, готовьтесь отдать процентов тридцать качества. Для болтовни и творчества - пожалуйста, для кода - нет.
Маленькие модели годятся в помощники, но не в основную
MiMo-V2.6-Distill-9B оказался самым быстрым (45 t/s) и неожиданно лучшим в санитайзере HTML (13 из 13), но провалил кэш и цены. Gemma-4-12B-Heretic хороша на парсере, но полностью завалила санитайзер (ноль из тринадцати). Профили у моделей очень разные, и это, кстати, аргумент в пользу того, чтобы держать несколько моделей под разные задачи, а не искать одну универсальную.
Мелочи, которые вылезли по ходу
- Почти все модели возвращали код без открывающего тега php. Прогонщику пришлось его добавлять, иначе PHP считает файл обычным HTML и функции просто нет. В реальной работе это неважно, но говорит о том, что фразу «ответь только кодом» модели понимают по-разному.
- Думающие модели съедают бюджет токенов на размышления. На сложных задачах блок размышлений раздувается так, что на сам код места не остаётся. Пришлось поднимать лимит до восьми тысяч токенов, иначе ответы обрывались на полуслове.
- Тесты надо запускать с таймаутом. Одна из моделей сгенерировала парсер, который зациклился на некоторых выражениях. Это не ошибка теста, это реальный баг в коде модели, но без таймаута прогон просто зависает.
- Автоматические тесты ловят то, что не видно глазами. Код может выглядеть красиво и при этом не проходить простейшие крайние случаи. Именно поэтому я и не стал оценивать ответы вручную.
Что в итоге держу в работе
- Каждый день - Tiel-Coder-35B-A3B. Сорок токенов в секунду, 81% на моих задачах, отличные результаты на ценах, деревьях и кэше, то есть на том, что я и пишу.
- Сложная алгоритмика - Qwen3.8-Flash-Next. Когда задача действительно требует подумать (парсеры, нетривиальные структуры данных), лучше подождать, но получить 92%.
- Быстрые мелочи и зрение - MiMo-9B. Мгновенный, с рабочим зрением, хорош в разборе и чистке HTML.
- Расцензуренные версии для кода не беру. Размен качества слишком дорогой.
Итог
Чужой бенчмарк говорит, что модель умная. Мой тест говорит, справится ли она с моей задачей за приемлемое время. Второе мне, как разработчику, полезнее.
Про сам прогонщик, которым всё это мерялось, я написал отдельно: Как собрать свой прогонщик для локальных LLM. Там пошагово: как придумать задачи под свою работу, как написать к ним тесты и эталоны, как автоматизировать прогон по всем моделям и на какие грабли я наступил по дороге. Потому что правильный вопрос не «какая модель умнее вообще», а «какая справится с моей задачей за приемлемое время».