Маленькая модель на 0.6B держит квантование лучше, чем «крупная» на 1B: измерил деградацию function-calling на 4 ГБ VRAM

от автора

Началось всe с очень приземленной задачи. У меня ноутбук с RTX 3050 и четырьмя гигабайтами видеопамяти, и я хочу гонять на нeм маленькую модель, которая умеет вызывать инструменты: заполнять JSON по схеме, выбирать нужную функцию, не звать еe там, где не надо. Чтобы модель влезла, еe приходится квантовать, и вот тут возникает вопрос, на который я так и не нашeл честного ответа: насколько сильно квантование ломает именно способность к вызову инструментов, а не абстрактное «качество» модели.

Обычно рассуждают так. Берут табличку с BFCL или ToolACE, смотрят на средний балл и прикидывают: раз модель побольше, значит и запас прочности у неe больше, а если квант Q4 «безопасен» для одной модели, то и для соседней, наверное, тоже сойдeт. Обе эти интуиции у меня на данных не подтвердились, причeм одна из них развалилась особенно наглядно. Я собрал бенчмарк QuantCall, прогнал через него три модели на разных уровнях квантования против задач BFCL, и главный результат оказался таким: то, насколько модель просядет от квантования, определяется не еe размером, а тем, из какого она семейства.

Конкретно: Llama-3.2-1B, у которой на бумаге в полтора раза больше параметров, теряет валидность вызовов на каждом проверенном уровне квантования, вплоть до самого щадящего Q8_0. А Qwen3-0.6B, которая почти вдвое меньше, держится ровно, и первая статистически значимая просадка у неe появляется только на Q4_K_M, да и то не по валидности схемы, а по корректности аргументов. Размер тут не спасает, спасает семейство.

Таблица лидерборда QuantCall с прогонами, отсортированная по FCR

Таблица лидерборда QuantCall с прогонами, отсортированная по FCR

Верхняя часть живой таблицы, отсортированной по FCR. Обе модели Qwen3, и 0.6B, и 1.7B, держатся кучно в районе 0.87–0.88 по SVR и занимают весь топ. Строки Llama-3.2-1B в этот кадр не попали — у неe FCR заметно ниже, и в таблице она висит существенно дальше вниз, что само по себе уже показательно.

Зачем свой бенчмарк, если есть BFCL

Возражение понятное: BFCL уже всe измерил, зачем городить свое. Но меня интересует не тот вопрос, на который отвечает BFCL. Публичные лидерборды показывают, насколько модель вообще умеет вызывать инструменты, и почти всегда сравнивают модели между собой в полной точности. Мой вопрос уже: у конкретной модели, которую я собираюсь квантовать под свое железо, как именно поедет кривая деградации по уровням кванта, и в какой момент ей перестанет можно доверять заполнение аргументов. Это ровно то, что решает, влезет модель в мои четыре гигабайта или нет, и стоит ли после этого пускать еe к инструментам.

Поэтому весь бенчмарк устроен так, чтобы его можно было повторить у себя, ничего не заплатив и никуда не сходив по сети. Задачи берутся из BFCL v4, в основном простой tier с одиночными вызовами и tier на нерелевантность, где правильный ответ это вообще не звать инструмент. Каждая конфигурация прогоняется по 200 примеров на сид, в три сида, жадным декодированием, чтобы числа были воспроизводимыми, а не плавали от запуска к запуску. Считаю несколько метрик, и они специально разведены: SVR это доля структурно валидных вызовов, TSA это доля правильно выбранных имeн функций, AC это корректность значений аргументов через сопоставление AST, Abst это точность воздержания на нерелевантных задачах, а FCR это их взвешенная сумма. Разводить их важно, потому что, как выяснилось дальше, модель может уверенно проходить одну метрику и заваливать соседнюю по совершенно конкретной технической причине.

Самая живая находка: Llama заворачивает числа в строки

Прежде чем поверить агрегатам, я сел и прочитал сырой вывод модели на конкретных примерах. Числа в таблице это одно, а понимание, почему они такие, приходит только когда видишь, что модель реально выдает. И на Llama-3.2-1B картинка объяснилась мгновенно. У неe низкая корректность аргументов не потому, что она путает значения, а потому, что она стабильно отдает числа в виде строк: там, где схема ждет 10, модель пишет "10".

{"name": "calculate_triangle_area", "parameters": {"base": "10", "height": "5", "unit": "units"}}

Функцию выбрала правильно, структуру вызова собрала правильно, значения по смыслу тоже правильные. Но валидация по JSON Schema честно отклоняет строку там, где объявлено число, и метрика корректности аргументов проседает. Это не придирка бенчмарка и не баг парсера, это ровно то поведение, которое сломает инструмент, если тот на входе ждет число: типизированный обработчик получит строку и либо упадeт, либо тихо сделает не то. Именно поэтому у Llama низкий AC в абсолютных числах вообще на всех уровнях кванта, еще до всякой деградации, и именно поэтому квантование, которое добивает и без того шаткую модель, бьет по ней заметнее.

Вывод Llama-3.2-1B на fp16: числовые аргументы base и height завернуты в кавычки

Вывод Llama-3.2-1B на fp16: числовые аргументы base и height завернуты в кавычки

Тот же самый вызов, снятый вживую. Функция и структура выбраны верно, а вот 10 и 5 модель написала строками.

Семейство важнее размера: что именно показали числа

Теперь по существу, откуда взялся главный вывод. Базовые линии я снимал в полной точности fp16 везде, где это было возможно. Qwen3-0.6B в fp16 даeт SVR 0.877 и AC 0.605, и при спуске по квантам почти не двигается: на Q8_0 и Q5_K_M ни одна метрика значимо не проседает, а на Q4_K_M впервые появляется значимая просадка, но только по корректности аргументов и по агрегату FCR, а сама валидность схемы держится. То есть даже на самом агрессивном из проверенных квантов у этой модели ломается не структура вызова, а точность значений, и то в пределах нескольких процентов.

Llama-3.2-1B в fp16 стартует совсем с другого уровня, SVR всего 0.327, и дальше становится только хуже. Значимая просадка валидности вызовов у неe фиксируется на каждом уровне кванта, включая самый мягкий Q8_0, а не только на агрессивном Q4. На Q4_K_M просадка SVR самая большая, доверительный интервал на дельту к базовой линии лежит в районе четырех-пяти процентных пунктов и уверенно не пересекает ноль. Разница с Qwen3 не в том, что одна модель чуть лучше другой, а в том, что у них качественно разное поведение под давлением: одна теряет точность аргументов на пределе, другая начинает разваливаться структурно с первого же шага квантования.

Отдельная история вышла с третьей моделью, Qwen3-1.7B, и она же честно вскрыла ограничение железа. Еe веса в fp16 весят около четырех гигабайт и просто не влезают в мою карту при вменяемой длине контекста: на n_ctx=4096 и даже на 2048 я получал CUDA out-of-memory, а грузилась она только на 512, что для промптов BFCL с их схемами инструментов бесполезно мало. Поэтому для этой модели базовой линией служит не fp16, а Q8_0, и я это честно проговариваю, а не заметаю под ковeр. Так вот, у Qwen3-1.7B значимой деградации нет ни на одном из проверенных квантов. Что складывается в общую картину: обе модели семейства Qwen3, и меньшая, и большая, ведут себя устойчиво, а выпадает из ряда именно Llama, и выпадает по признаку семейства, а не размера.

Держится ли это на задачах посложнее? Да, и становится хуже

Первый tier это самые простые задачи BFCL, один вызов или ни одного. Логично было проверить, не привязана ли находка к этой простоте, поэтому я прогнал более тяжелую смесь: параллельные вызовы, где в одном примере надо позвать несколько инструментов сразу, вместе с задачами в стиле ToolACE, где каталоги инструментов длиннее и многословнее. Прогнал для обоих семейств, которые влезают в fp16, на двух крайних квантах, fp16 и Q4_K_M, по три сида.

Результат не просто повторился, он усилился. У Llama-3.2-1B валидность вызовов на этих тяжелых задачах падает при квантовании до Q4_K_M на 23 абсолютных процентных пункта, с 0.572 до 0.338, и доверительный интервал на дельту лежит в районе двадцати-двадцати шести пунктов. Это примерно в пять раз больше, чем скромные 4.7 пункта, которые та же модель теряла на простом tier. То есть простые задачи не преувеличивали проблему, они еe недооценивали: чем ближе нагрузка к настоящей агентной работе с несколькими одновременными вызовами и жирными каталогами, тем сильнее квантование бьет по Llama. Qwen3-0.6B при этом по валидности схемы снова не шелохнулась, а еe просадка по корректности аргументов осталась небольшой и значимой, ровно как на простом tier.

Pareto-фронт QuantCall: FCR против пикового VRAM, точки Qwen3 плотной группой сверху, точки Llama-3.2-1B заметно ниже

Pareto-фронт QuantCall: FCR против пикового VRAM, точки Qwen3 плотной группой сверху, точки Llama-3.2-1B заметно ниже

Диаграмма Парето по всем прогонам разом. Кластер Qwen3 держится плотно наверху вне зависимости от кванта, а точки Llama-3.2-1B лежат заметно ниже — семейное разделение видно даже без чтения таблицы.

Почему я вообще перепроверял парсер, а не поверил числам

Тут важно рассказать, как я чуть не опубликовал ерунду. В более раннем прогоне у меня фигурировала модель Llama-3.2-3B, и она показывала SVR около 0.33-0.36 при том, что имена функций выбирала правильно в 68-70 процентах случаев. Такое расхождение это красный флаг: если модель почти всегда попадает в нужную функцию, у неe не может быть настолько низкой валидности вызовов, что-то не сходится в измерении, а не в модели.

Я полез в код и нашeл ровно то, чего боялся. Функция, которая должна была выбирать парсер под конкретный бэкенд и формат чата, полностью игнорировала свой аргумент и всегда возвращала один и тот же наивный парсер по сырому JSON, хотя в кодовой базе уже лежали и правильный Hermes-парсер для формата <tool_call>, и парсер под шаблоны GGUF. Наивный парсер сканировал весь вывод модели в поисках сбалансированных фигурных скобок и мог зацепить мусор из блока рассуждений. Я это починил, привязал парсер к формату чата, и отдельно провeл аудит: взял первые десять примеров простого tier для новой основной модели Qwen3, прогнал и глазами сверил каждый сырой вывод с распарсенным. Десять из десяти вызовов распарсились корректно, ноль сбоев, ноль лишних вызовов. Только после этого я разрешил себе доверять полному прогону.

Где я ошибся сам: история про GBNF

Была у меня гипотеза, которая казалась очевидной: если модель иногда выдает невалидную структуру, надо просто заставить еe грамматикой. Constrained decoding через GBNF физически не даeт модели сойти с формата, значит валидность вызовов должна вырасти. Я это реализовал и прогнал, и получил сразу два урока, оба неприятных.

Первый урок методологический. Моя первая грамматика умела описывать только сам вызов инструмента и не умела описывать «не звать ничего». А в бенчмарке есть целый tier на нерелевантность, где правильный ответ это как раз воздержаться. Грамматика структурно принуждала модель звать инструмент там, где звать не надо, и точность воздержания у Qwen3-0.6B в fp16 рухнула с 0.878 на свободном декодировании до 0.143 под этой грамматикой, а вместе с ней просел и агрегат, потому что воздержание входит в него четвертью веса. Это был не шум, а баг измерения, который я сам себе устроил. Починил я его объединенной грамматикой, где корневое правило разрешает либо валидный вызов, либо произвольный свободный текст, и проверил на примерах, что теперь задачи на вызов дают структуру, а задачи на нерелевантность спокойно воздерживаются текстом.

Второй урок про саму гипотезу, и он еще интереснее. После починки грамматики оказалось, что constrained decoding у Qwen3 не улучшает ни валидность схемы, ни корректность аргументов сколько-нибудь заметно: все дельты сидят в пределах одного-двух процентных пунктов в обе стороны, что при одном сиде неотличимо от шума. Причина простая: Qwen3 и без всякой грамматики надежно выдает корректный JSON вызова, там просто нечего чинить. Зато цена оказалась вполне реальной и измеримой: грамматика замедляла обработку на 6-86 процентов в зависимости от модели и кванта, и разброс здесь не случайный, а системный. Для Qwen3-0.6B накладные расходы держатся в районе 55-86%, а для Qwen3-1.7B — всего 6-18%, что объяснимо, ведь постоянные потокенные издержки грамматики сильнее заметны на фоне и без того быстрого прогона маленькой модели. Итог честный и не в пользу моей исходной идеи: для этого семейства моделей и этого бенчмарка грамматика ничего не восстанавливает по качеству, но стоит ощутимого времени. Это может быть иначе для моделей, которые сами по себе плохо держат JSON, но у Qwen3 это именно так.

Была и совсем отдельная неприятность по дороге. Первая же сборка грамматик под схемы BFCL уронила весь процесс в segfault с кодом 139. Причина оказалась в том, что GBNF-парсер llama.cpp падает на именах правил, где смешаны подчеркивание и дефис, а имена свойств в JSON Schema сплошь и рядом содержат подчеркивания, тогда как моя генерация правил под массивы дописывала суффикс через дефис. Починил нормализацией имeн к одному разделителю, закрыл регрессионным тестом.

Таблица результатов constrained decoding: SVR и AC почти не меняются, замедление 6-86%

Таблица результатов constrained decoding: SVR и AC почти не меняются, замедление 6-86%

Данные из docs/constrained_decoding_findings.md. Качество топчется в пределах шума в обе стороны, а колонка замедления вся в плюсе — вот и вся цена грамматики здесь.

Меняет ли что-то сам бэкенд, если точность держать одинаковой

Еще одна проверка, которую нельзя было пропустить. Все находки выше сняты на бэкенде llama-cpp с GGUF-весами, и напрашивался вопрос: а не добавляет ли сам движок инференса своего эффекта поверх квантования, ведь точность и бэкенд это две разные оси. Я прогнал ту же Qwen3-0.6B через бэкенд transformers на PyTorch в bf16, без всякого GGUF, при совпадающей точности. SVR, AC и FCR у неe оказались статистически неотличимы между llama-cpp и transformers, то есть на этой модели сам движок метрику не двигает, и задокументированная деградация это эффект квантования, а не артефакт конкретной реализации сервинга. Для полноты я отдельно прогнал еще и третью связку модель-бэкенд, Gemma через локальный сервер LM Studio по openai-совместимому протоколу с HTTP по кругу, и получил правдоподобные, корректно распарсенные числа. По ходу этой проверки, кстати, нашлись и починились два бага в сетевом слое, но это уже история для README.

Где я сам себе не до конца верю

Дальше список ограничений, и он не для галочки. Без него любая цифра выше звучит увереннее, чем заслуживает.

Пар «модель и квант» для основной таблицы восемь, и это мало. Знак и направление находки устойчивы, семейство Qwen3 держится, Llama сыпется, но к точным величинам просадок стоит относиться как к оценкам, а доверять в первую очередь направлению.

Моделей три: Qwen3-0.6B, Qwen3-1.7B и Llama-3.2-1B. Про Qwen3-1.7B я уже сказал честно, еe базовая линия это Q8_0, а не fp16, из-за нехватки видеопамяти на моей карте, а не из-за того, что мне так удобнее.

Тяжелые задачи с параллельными вызовами и ToolACE я прогнал не по всей лесенке квантов, а только на двух крайних точках, fp16 и Q4_K_M, по три сида. Это сознательный компромисс по времени: каждый такой прогон на 200 примеров идeт по двадцать-сорок минут, длиннее простых задач, и я отвечал на вопрос «сохраняется ли направление находки», а не «как выглядит вся кривая ответов». На первый вопрос ответ однозначный, размер эффекта не оставляет места сомнению, на второй у меня данных нет, и я это не выдаю за большее.

И еще одна честная деталь измерения: на тяжeлом tier точность воздержания тривиально равна единице, потому что там нет задач на нерелевантность, а значит агрегат FCR на нeм завышен на постоянную величину, и сравнивать FCR между разными смесями задач нельзя. Верить на этом tier надо SVR и AC, что я и делаю. Плюс метрики внутри одного прогона считаются по объединенному набору задач, а не отдельно по каждому tier, потому что результаты не сохраняются с разбивкой по примерам. Это ограничение всего проекта, и я его помечаю, а не прячу.

Ничего из этого я не подаю убедительнее, чем оно есть. Если у вас есть карта на четыре гигабайта и хочется прогнать всe самому или добавить числа со своего железа, проект специально собран так, чтобы заводиться из коробки одной командой.

Куда смотреть

Весь код и полное описание методики, откуда взята каждая цифра в этом посте, лежат в репозитории: github.com/Happynood/quant-toolcall-bench. Там же в README ссылки на всe остальное: набор для оценки с версионированными сэмплами (quantcall-suite), сырые результаты по каждому сиду вместе с данными для проверок значимости (quantcall-results) и живой лидерборд с таблицей и диаграммой Парето в виде Gradio Space (quantcall-leaderboard). Каждый файл результата несeт манифест с git-хешем, хешем конфига, хешем сэмпла данных и отпечатком железа, чтобы воспроизводимость была не на словах.

А дальше меня потянуло на следующий вопрос, из которого вырос соседний проект. Хорошо, на курируемых схемах BFCL квантование ломает Llama и щадит Qwen3, но эти схемы аккуратные и короткие, а что будет, если направить те же модели и те же кванты на схемы серверов Model Context Protocol, которые никто не причeсывал под бенчмарк. Ответ оказался неожиданным настолько, что заслуживает отдельной статьи, и вот она: проверка того же вопроса на MCP-серверах (статья готова, ссылку прикреплю после публикации). Если коротко, рейтинг деградации с BFCL на MCP-схемы не переносится вообще, но подробности там.

ссылка на оригинал статьи https://habr.com/ru/articles/1056656/