Что квантовано, а что нет
Qwen3.8-27B — гибридная модель: из 64 слоёв 48 используют линейное внимание
(Gated DeltaNet), 16 — полное. Плюс зрительная башня и голова multi-token
prediction.
Table with columns: компонент, состояние, почему| компонент | состояние | почему |
|---|
| MLP всех 64 слоёв | INT8 | 192 модуля |
| q/k/v/o 16 слоёв полного внимания | INT8 | 64 модуля |
linear_attn.* — 48 слоёв Gated DeltaNet | BF16 | линейное внимание переносит квантование хуже MLP и полного внимания |
| зрительная башня | BF16 | 333 тензора, ни один не сжат |
lm_head, embed_tokens | BF16 | |
| MTP-голова | BF16 | обычный форвард её не задевает, калибровочных активаций для неё нет |
Итого под INT8 ушло 18.79 млрд параметров из 256 модулей.
Калибровка
599 диалогов, шесть корзин. Всё пропущено через apply_chat_template с
дефолтным reasoning_effort, то есть в том же виде, в каком модель принимает
запросы в работе.
Table with columns: корзина, примеров, источник| корзина | примеров | источник |
|---|
| Код | 100 | bigcode/the-stack-smol-xl — Python, Go, TypeScript, Java, C++, Rust, SQL, Shell |
| Инструкции по коду | 100 | nickrosh/Evol-Instruct-Code-80k-v1 |
| Русский язык | 100 | , фильтр , |
Длина: медиана 1083 токена, максимум 4096.
Сам калибровочный набор не публикуется — выше перечислены все источники и
правила отбора, по ним он воспроизводим.
Два решения, которые влияют на результат:
- Код лежит в ответе ассистента, а не в запросе. Калибровка должна
повторять распределение активаций при работе, а код модель в основном
порождает, а не читает.
- Корзина с рассуждениями сгенерирована, а не взята готовой. Модель по
умолчанию рассуждает, и
<think> — отдельный блок в её шаблоне; без этой
корзины калибровка не покрывала бы основной режим работы. Готовых наборов с
трассами под нужный профиль найти не удалось.
Замеры
Сравнение с оригиналом BF16 на отложенном наборе из 100 сплошных текстов — тех
же источников, что и калибровка, но без пересечения с ней (проверка по
тексту запроса). Обе модели обслуживались одним и тем же vLLM 0.26.0 на одной
A100 80GB с одинаковыми параметрами.
Table with columns: корзина, BF16, INT8, дельта| корзина | BF16 | INT8 | дельта |
|---|
| код (n=40) | 1.9544 | 1.9544 | < 0.01 % |
| русский (n=40) | 2.8550 | 2.8542 | −0.03 % |
| общий текст (n=20) | 6.7840 | 6.7759 | −0.12 % |
| всего (n=100) | 2.9171 | 2.9160 | |
Расхождение на всех корзинах ниже десятой доли процента. То, что INT8 местами
оказался чуть ниже BF16, — шум на выборке в сотню текстов, а не превосходство
кванта над оригиналом.
Отдельно проверено, что модели действительно различаются: при temperature=0
из 50 текстовых ответов дословно совпали 30, различаются 20; из 12 ответов по
картинкам различаются 3. То есть поведение не идентично — разница просто не
проявляется в агрегированной перплексии.
Чего этот замер не показывает. Это не бенчмарк: ни HumanEval, ни MMLU, ни
MERA здесь не гонялись. Перплексия отвечает ровно на один вопрос — «насколько
квантование исказило распределение», — и на него отвечает уверенно. Качество
на задачах надо мерить отдельно.
Как запускать
vllm serve utrominon/Qwen3.8-27B-AWQ-BF16-INT8 \
--quantization=compressed-tensors \
--max-model-len=131072 \
--reasoning-parser=qwen3 \
--enable-auto-tool-choice --tool-call-parser=qwen3_coder
Проверено на vLLM 0.26.0 (CUDA 13, A100 80GB). --reasoning-parser=qwen3
обязателен, иначе содержимое <think> приезжает прямо в content.
Рецепт
from compressed_tensors.quantization import QuantizationArgs, QuantizationScheme
from llmcompressor.modifiers.gptq import GPTQModifier
from llmcompressor.modifiers.transform.awq import AWQModifier
SCHEME = QuantizationScheme(
targets=["Linear"],
weights=QuantizationArgs(
num_bits=8, type="int", strategy="group", group_size=128,
symmetric=False, dynamic=False, observer="mse",
),
)
IGNORE = [
r"re:.*\.visual\..*", r"re:^visual\..*",
r"re:.*lm_head", r"re:.*embed_tokens$",
r"re:.*\.linear_attn\..*",
r"re:^mtp\..*",
]
recipe = [
AWQModifier(duo_scaling="both"),
GPTQModifier(config_groups={"group_0": SCHEME}, ignore=IGNORE, actorder=None),
]
Сборка заняла 3 ч 30 мин на одной A100 80GB (AWQ ≈ 1 ч 55 мин, GPTQ ≈ 1 ч 25 мин).
Три вещи, на которые стоит обратить внимание при воспроизведении:
llmcompressor нужно ставить из git, а не из tarball GitHub: часть
подпакетов — неявные namespace-пакеты без __init__.py, при сборке колеса
из архива они теряются.
transformers не строит MTP-голову для Qwen3_5ForConditionalGeneration —
её нет ни в дереве модулей, ни в state_dict. Обычный save_pretrained
молча выбрасывает все 15 тензоров mtp.*. Здесь они возвращены
пост-обработкой и оставлены в BF16.
- Пик памяти приходится не на счёт, а на фазу
Compressing model, где все
256 квантованных тензоров материализуются в памяти перед записью.
Планировать RAM надо по этому моменту.
Ограничения
- Качество на изображениях измерено слабо. Зрительная башня не квантована,
но визуальные токены проходят через те же INT8-слои, что и текст. Корзина с
картинками в калибровке эту дыру закрывает на входе; в самой калибровке —
схемы, диаграммы, графики и инфографика, бытовых фотографий нет.
- Калибровка двуязычная. Английский и русский. Базовая модель многоязычна,
и на остальных языках эта сборка ничем не лучше любой другой.
- Слои линейного внимания оставлены в BF16 — это осознанный размен качества на
размер. Более агрессивный рецепт дал бы меньший файл.
Лицензия
Apache 2.0, наследуется от базовой модели
Qwen/Qwen3.8-27B.
Квантование не меняет условий использования.