haiodo.blog
oQ в oMLX: что я нашёл про кванты Qwen3.8 и где мой замер разошёлся с ожиданиями

oQ в oMLX: что я нашёл про кванты Qwen3.8 и где мой замер разошёлся с ожиданиями

Месяц назад я мерил движки для локальной Qwen3.8-27B. С тех пор oMLX улучшил свой квантизатор oQ, на Hugging Face появились десятки oQ-сборок Qwen3.8, а в 0.7.1.dev1 oMLX научился отдавать модели-решения. Собрал, что известно, и перемерил на своём M4 Max 48 GB: oQ4e против обычного 4-bit, с MTP и без, и модель-решение clef на 600 размеченных адресах.

С чего началось


В сентябре я мерил восемь движков для локальной 27B и в итоге померил свой ноутбук. Выигрывал тогда не движок, а квант и то, сколько памяти остаётся системе. С тех пор вокруг Qwen3.8 на Mac поменялось достаточно, чтобы собрать второй заход.

Собирал не руками. Тему qwen-3-8 ведёт мой исследовательский CLI pro2you: он гоняет поиск по одобренным запросам, качает страницы, вытаскивает из них факты с цитатами и раз в раунд пересобирает сводку. В этот раунд я добавил запросы про oMLX 0.7.1 и квантизацию oQ, а ещё научил инструмент ходить в API Hugging Face: страницу модели он теперь читает не как HTML, а как карточку с лицензией, параметрами, файлами по форматам и списком квантов.

Итог раунда: 59 запросов, 161 новый адрес, 261 скачанная страница, плюс 493 факта. Всего по теме 5160 фактов. Ниже только то, у чего есть первоисточник. Где цифра чужая и никем не перепроверена, я так и пишу.

Что такое oQ


oQ - квантизатор, встроенный в oMLX. На выходе обычная MLX-модель в safetensors, её читают и mlx-lm, и mlx-vlm, свой загрузчик не нужен (документация oQ).

Главное отличие от обычного mlx-lm 4-bit - смешанная точность. Перед записью oQ прогоняет калибровочные промпты, меряет, насколько каждый слой страдает от квантования, и докидывает биты туда, где это помогает сильнее всего. Общий размер при этом держится около выбранного уровня.

Робот-ювелир раскладывает монеты по ящикам: в важные много, в остальные по одной
УровеньБазовые битыБит на весКогда брать
oQ222.8-3.0огромные MoE, которые иначе не влезают
oQ333.5-3.7мало памяти
oQ444.6-4.7вариант по умолчанию
oQ555.5-5.7качество выше
oQ666.5-6.7очень близко к оригиналу
oQ888.5всё квантованное в 8 бит

Есть ещё промежуточные oQ2.5, oQ2.7 и oQ3.5 для MoE. Для плотных моделей документация советует начинать с oQ3e: на 2 битах плотная модель теряет заметно больше, чем MoE.

Буква e в названии - это oQe. Раскладка битов та же, размер и скорость те же, отличается округление. oQe сначала собирает матрицу важности (imatrix, как в llama.cpp) и при округлении бережёт каналы, по которым идёт основной сигнал.

Что oQ не трогает или держит в повышенной точности:

Суффикс -fp16 значит, что неквантованные веса и масштабы лежат в float16, а не в bfloat16.

Что поменялось в квантизации


Повод для захода - новость, что в 0.7.1.dev1 поменялся способ квантизации. В документации oQ есть раздел про это: в oQe добавили повторную подгонку масштаба и смещения взвешенными наименьшими квадратами, PR #4385. Подгонка повторяется несколько раз, и результат оставляют, только если ошибка падает.

МодельОригиналoQ3e доoQ3e послеoQ4e доoQ4e после
Qwen3.5-9B76.53 (18 GB)72.5373.79 (4.6 GB)75.1177.05 (5.6 GB)
Qwen3.6-35B-A3B82.68 (67 GB)81.5882.68 (16 GB)82.7983.05 (20 GB)

Это замер автора oMLX: MMLU 1000, Winogrande 300, MBPP 300 и GSM8K 300, 1900 вопросов, greedy, без размышлений. По его же словам, KL-дивергенция к оригиналу падает на 12-15%.

Два замечания. Первое: oQ4e “после” выше оригинала у обеих моделей. Это значит, что разница в полбалла на 1900 вопросах лежит в шуме, и читать таблицу надо как “oQ4e почти не теряет”, а не как “квант лучше оригинала”. Второе: подгонка #4385 вошла в 0.7.1.dev1, это прямо сказано в release notes. Там же оговорка: меняются только новые конвертации. oQe-сборки, собранные раньше, подгонки не получают.

Ранняя версия того же сравнения, ещё без подгонки, есть для Qwen3.6-27B, ближайшего родственника нашей модели: оригинал 85.50 (55.6 GB), mlx-lm 4-bit 84.25 (16.1 GB), oQ4 84.50, oQ4e 84.81 (16.7 GB). То есть oQ4e забирает больше половины того, что теряет обычный 4-bit, ценой 0.6 GB.

Какие oQ-сборки Qwen3.8 есть на Hub


Цифры загрузок - из API Hugging Face, это число за последние 30 дней на 2026-10-11.

СборкаЗагрузок за 30 дней
Jundot/Qwen3.8-Flash-Next-oQ4e-mtp19 931
Jundot/Qwen3.8-27B-oQ4e-mtp9 886
Jundot/Qwen3.8-27B-oQ8e-mtp1 844
Jundot/Qwen3.8-27B-oQ6e-mtp1 608
Jundot/Qwen3.8-27B-oQ4e-fp16-mtp1 267
Jundot/Qwen3.8-27B-oQ8e-fp16-mtp693

Jundot - автор oMLX, это официальные сборки. Вокруг них уже выросло сообщество: thederby выложил полную линейку oQ4e-oQ8e для 27B, notnaurw - три варианта oQ5e с разными стратегиями раскладки битов (SAGE-PATHS, SAGE-ARCS, SAGE-CREST), scottlowry - oQ-сборки дообученных Swift-1.5 и ThinkingCap. У SAGE-CREST автор приводит KL на 128K контекста на 25% лучше равномерного 6-bit (карточка). Это его замер, я не перепроверял.

Почти все сборки сохраняют MTP-голову и визуальный энкодер. На SAGE-PATHS посчитали, что энкодер весит около 0.92 GB в bf16 и в чисто текстовой работе лежит мёртвым грузом (карточка). У oQ для этого есть опция Text Only.

Отдельно про Flash-Next. Это MoE на 125B, из них 6B активных, плюс n-gram таблица на 51B и MTP на 4B (карточка umixer). Самая скачиваемая oQ-сборка вообще - именно она. Цифры 45 tok/s на M4 Max и 25 tok/s на M2 Ultra для неё гуляют по статьям, но первоисточника с замером я не нашёл.

Мой замер: M4 Max 48 GB


Чужие таблицы ниже в основном про M5 и машины на 128 GB. У меня M4 Max с 40-ядерным GPU и 48 GB, из которых Metal отдаёт около 40 GB. Поэтому главное я перемерил сам, встроенным бенчмарком oMLX 0.7.1.dev1, тем же, что пишет в базу omlx.ai.

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

Контекст 1k, 4k, 16k и 32k, генерация 128 токенов, два набора текстов (проза и Python), каждый прогон дважды. Docker и лишние модели перед замером выгрузил: в первой попытке защита памяти oMLX обрывала длинные контексты.

Проза (novel_en), среднее двух прогонов, в скобках разброс:

ВариантКонтекстPP, tok/sTG, tok/sПик памяти
MLX 4bit1k25428.317.0 GB
4k26228.018.3 GB
16k24826.019.2 GB
32k23024.520.4 GB
oQ4e без MTP1k25027.817.6 GB
4k25726.418.9 GB
16k25025.119.8 GB
32k23323.821.0 GB
oQ4e с MTP1k24933.8 (31.3-36.2)19.7 GB
4k25534.9 (34.2-35.6)21.0 GB
16k24140.3 (38.8-41.7)22.0 GB
32k22929.3, один прогон23.3 GB

Время до первого токена на 32k у всех трёх одинаковое: 140-146 секунд.

Что из этого следует.

Сам квант скорости не даёт. Без MTP oQ4e на 2-6% медленнее равномерного 4-bit по генерации, префилл тот же. Файл на 2.2 GB больше, а генерация на Mac упирается в пропускную способность памяти. Часть разницы может быть и от нагрева: 4-bit шёл первым в каждой серии, а к oQ4e чип уже уходил в тепловое состояние 2. oQ4e берут ради качества, а не ради скорости.

Скорость даёт MTP. На прозе плюс 20-60%: 34-40 tok/s вместо 25-28. На 32k выигрыш падает до 23%. Цена - около 2.3 GB памяти сверху. На 48 GB это заметно: первый прогон MTP на 32k защита памяти oMLX остановила, не хватило 0.27 GB.

Маленький робот с фонарём бежит впереди и ставит флажки, большой робот идёт следом и проверяет каждый

На коде цифры MTP не годятся для выводов. На Python я получил 50-55 tok/s на 1k, 40 и 82 tok/s в двух прогонах на 16k и 101-106 tok/s на 32k. Генерация идёт всего 128 токенов, и на длинном куске Python-кода модель, похоже, продолжает то, что уже было в промпте. Черновик MTP тогда угадывается почти всегда. Без MTP на тех же текстах обе сборки дают 23-28 tok/s, как на прозе.

Qwen3.8-Flash-Next на 48 GB не влезает ни в каком oQ. oQ3 весит около 84 GB, oQ4e ещё больше. Все красивые цифры Flash-Next ниже - для машин от 128 GB.

Для сравнения: база прогонов oMLX


У oMLX есть открытая база замеров: omlx.ai/benchmarks. Пользователи прогоняют встроенный бенчмарк и отправляют результат, на 2026-10-11 там 524 164 записи. В таблице сайта есть чип, память, модель, квант, длина контекста, скорость префилла (PP) и генерации (TG). Версии oMLX и времени до первого токена в общем списке нет, они есть только на странице отдельного прогона.

Ниже - все прогоны Qwen3.8, которые попали в первую страницу фильтра по модели. Каждая строка - один чужой прогон, повторов и разброса нет. Чип и память указаны в первой строке группы, ниже идут прогоны той же машины на другом контексте.

Qwen3.8-27B

ЧипПамятьСборкаКонтекстPP, tok/sTG, tok/s
M4 Pro 20c48 GBMLX 4bit1k12715.4
4k13215.0
M4 Pro48 GBMLX 4bit16k9417.4
M5 Pro 16c48 GBOptiQ 4bit, uncensored1k1908.4
4k2158.1
M5 Pro 20c64 GBMTPLX 4bit32k41310.6
M4 Max 40c128 GBMLX 4bit4k24927.4
16k22724.7
M5 Max 40c128 GBMLX 4bit4k100732.0
16k88630.6
M5 Max 40c128 GBoQ4e-mtp, heretic-ara1k72848.1
4k87954.1
8k80362.6
16k75573.0
M5 Max 40c64 GBoQ4e-mtp16k96683.6
128k61947.6
M5 Ultra 80c96 GBoQ4e-mtp1k139252.2
4k155950.8

Строки M4 Pro на 16k и M4 Max взяты со страниц отдельных прогонов (M4 Pro, M4 Max), там же видно версию: oMLX 0.5.7 для M4 Max. На M4 Pro 16k первый токен пришёл через 2.9 минуты.

Qwen3.8-Flash-Next

ЧипПамятьСборкаКонтекстPP, tok/sTG, tok/s
M3 Max 40c128 GBoQ4e-mtp1k45447.4
4k56832.2
M5 Max 40c128 GBoQ3-mtp1k50252.4
4k87161.2
8k140161.6
16k171573.8
M5 Max 40c128 GBoQ4e-mtp1k113076.7
4k202282.0
16k263384.5
64k240670.1
128k231162.0
195k231373.5
M5 Max 40c128 GBoQ8e-mtp64k39811.3
64k4047.5
M3 Ultra 60c256 GBoQ6e-mtp4k101987.9
128k99357.5
M3 Ultra 80c512 GBoQ6e-mtp, uncensored1k102284.7
4k132079.6
195k127564.6

Что я из этого забираю.

Префилл на M5 вырос в разы, генерация - нет. MLX 4bit на одном и том же 4k: M4 Max даёт 249 tok/s префилла, M5 Max - 1007, вчетверо больше. Генерация при этом 27.4 против 32.0, плюс 17%. Префилл упирается в вычисления, генерация - в пропускную способность памяти, и M5 прибавил в первом. Для агента, который каждый раз читает длинный контекст, это важнее, чем tok/s в чате. Оговорка: это разные люди и, скорее всего, разные версии oMLX.

oQ4e-mtp против обычного 4bit - в 2-2.7 раза по генерации. На M5 Max на 16k: MLX 4bit 30.6 tok/s, oQ4e-mtp 73.0 и 83.6 в двух разных прогонах. Это не чистый эффект кванта: в -mtp сборках работает спекулятивное декодирование, и именно оно даёт основной прирост. Сколько из разницы приходится на oQ, а сколько на MTP, по этой базе не разделить.

Flash-Next почти не замечает длину контекста. На M5 Max с oQ4e-mtp генерация держится в коридоре 62-85 tok/s от 1k до 195k токенов, префилл - выше 2000 tok/s. У плотной 27B на том же чипе oQ4e-mtp на 128k падает до 47.6. Это сходится с архитектурой: у Flash-Next 6B активных параметров и три четверти слоёв на линейном внимании (Gated DeltaNet), KV-кэш растёт только у 12 слоёв из 48.

oQ8e Flash-Next на 128 GB - ловушка. 7.5-11.3 tok/s против 70 у oQ4e на том же M5 Max. По моей оценке, 8-битная сборка модели на 180B параметров вместе с n-gram таблицей не помещается в 128 GB, и oMLX подкачивает веса. В базе это не подписано, вывод мой.

Внизу линейки 27B на грани. M4 Pro и M5 Pro с 48 GB дают 8-15 tok/s, а OptiQ на M5 Pro 16c медленнее, чем MLX 4bit на M4 Pro. Почему так, по строке в базе не понять.

Про MTP есть и обратный пример. У halfpennymac на M4 Pro MTP в llama.cpp замедлила генерацию на 26%, с 10.7 до 7.9 tok/s, хотя черновик принимался в 65% случаев. Так что “-mtp всегда быстрее” - это про oMLX на чипах с запасом пропускной способности, а не правило.

Мой замер: модель-решение через oMLX


Тут я проверил сам. В pro2you релевантность найденной страницы решает не чат-модель, а модель-решение: она читает текст один раз и возвращает вероятность для каждого варианта ответа, ничего не генерируя. Я использую clef-flash от Cloudflare (Qwen3.5-9B плюс голова решения) через /v1/systemone в llama.cpp, квант GGUF Q4_K_M.

Робот-судья с циферблатом вместо головы читает лист и раскладывает страницы по двум корзинам

В oMLX 0.7.1.dev1 появилась поддержка тех же моделей через тот же /v1/systemone (#4315). В заметке к релизу отдельно сказано, что oQ оставляет голову clef нетронутой, и есть готовые Jundot/clef-oQ4e и Jundot/clef-flash-oQ4e.

Набор: 600 адресов, размеченных тремя моделями-судьями (qwen3.8-27b, gemma-4-31b, gpt-oss-120b), правильный ответ - решение большинства. Вопрос один: “по теме ли страница”. Запросы идут последовательно, на M4 Max 48 GB.

clef-flash Q4_K_M, llama.cppclef-flash-oQ4e, oMLXclef-oQ4e (27B), oMLX
AUC по большинству судей0.9510.9820.986
AUC там, где судьи единогласны0.9850.9971.000
Точность при пороге 0.50.9100.9080.907
Лучший порог и точность при нём0.50 / 0.9100.29 / 0.9280.17 / 0.933
Вероятность “да”: 5%, медиана, 95%0.29 / 0.43 / 0.750.01 / 0.05 / 0.960.01 / 0.07 / 0.98
Время на запрос: медиана / p950.60 / 0.88 с0.55 / 0.89 с2.35 / 4.73 с

Самое интересное не в AUC, а в предпоследней строке. У llama.cpp вероятности сжаты в коридор от 0.29 до 0.75: модель “почти никогда не уверена”. У oQ4e они разъезжаются от 0.01 до 0.96. Похоже, это и есть сохранённая голова: в GGUF Q4_K_M она квантуется вместе со всем остальным, и шкала размазывается. Проверить это напрямую я не могу - сравнивать надо ту же голову в fp16 внутри GGUF, такой сборки у меня нет.

Практическое следствие: порог из старого конфига переносить нельзя. При 0.5 clef-flash-oQ4e почти не ошибается в “да” (аккуратность 0.991), но пропускает каждую пятую страницу по теме (полнота 0.800). Порог приходится опускать:

ПорогПолнотаАккуратностьТочность
0.050.9620.8470.907
0.100.9210.9000.920
0.200.8640.9500.920
0.290.8490.9870.928
0.500.8000.9910.908

Для фильтра находок мне важнее не потерять страницу по теме, чем пропустить лишнюю, поэтому беру 0.1: полнота выше, чем у llama.cpp при 0.5 (0.921 против 0.868), при сопоставимой аккуратности (0.900 против 0.924).

27B-версия clef почти не добавляет: AUC 0.986 против 0.982, а на единогласных адресах она не ошиблась ни разу. Но каждый запрос в четыре раза дольше. Для фильтра, который гоняет сотни страниц за прогон, это плохой обмен.

У clef-flash скорость одинаковая на обоих движках, ошибок 500 за 600 запросов ни одной ни у одной из oMLX-моделей. У llama.cpp они случались, поэтому в конфиге стояли повторы.

Чего я не нашёл


Что я забрал себе