Всё началось с простой, на первый взгляд, задачи.

Я хотел использовать локально Qwen3.8–27B именно в BF16. Причём не исходный PyTorch‑чекпойнт через случайный слой совместимости, а mlx-community/Qwen3.8-27B-bf16 — специально подготовленную в формате MLX версию для Apple Silicon. Вся линейка Qwen3.8 от mlx‑community конвертирована именно для запуска на процессорах Apple.

То есть модель уже была адаптирована под архитектуру Mac. Без 8-битной или 4-битной квантизации, потому что исходный приоритет был не в том, чтобы любой ценой получить красивую цифру в бенчмарке. Мне нужна была максимальная точность модели для сложного анализа, работы с кодом и длинных рассуждений.

Казалось, что с железом проблем точно не будет.

Моя конфигурация — Mac Studio 2025 года (Mac15,14, заказной номер Z1CE001BMZP/A) с Apple M3 Ultra: 32 ядра CPU, из них 24 производительных, 80 ядер GPU, 512 ГБ объединённой памяти и SSD на 2 ТБ. Заявленная Apple пропускная способность памяти — 819 ГБ/с.

То есть перед нами не ноутбук, который случайно заставили считать большую языковую модель. Это максимальная конфигурация M3 Ultra по CPU, GPU и объёму памяти.

И на ней оптимизированная для Apple Silicon версия Qwen3.8–27B в BF16 выдавала всего 12 токенов в секунду.

Сначала результат выглядел подозрительно. В машине десятки CPU‑ и GPU‑ядер, модель целиком помещается в память, используется нативный для Apple Silicon фреймворк MLX. Почему тогда скорость совсем не похожа на то, чего ждёшь от топового GPU?

Я начал искать узкое место.

Почему 80 ядер GPU не спасли BF16

Главная ошибка — считать, что скорость генерации LLM определяется прежде всего количеством вычислительных ядер.

При обработке входного запроса GPU действительно может хорошо распараллелить вычисления. Но генерация ответа устроена иначе: каждый следующий токен зависит от предыдущего. Пока модель не получила токен № 1, она не может полноценно вычислить токен № 2.

И вот здесь начинается самое интересное.

Qwen3.8–27B содержит около 27 млрд параметров. В BF16 на каждый параметр приходится два байта, поэтому только веса модели занимают примерно 54 ГБ. При генерации каждого нового токена плотная модель должна последовательно пройти через все слои, а значит — прочитать из памяти почти весь массив весов.

Такой режим называется memory‑bound: скорость ограничивает не количество арифметических блоков GPU, а то, насколько быстро данные успевают приехать к этим блокам. Для генерации с одним запросом многие ядра могут закончить свою часть вычислений и просто ждать следующую порцию весов. Это известная особенность фазы декодирования LLM — тот же механизм подробно разбирается, например, в исследовании систем инференса LLM на OSDI.

Грубая арифметика выглядит так:

819 ГБ/с ÷ 54 ГБ ≈ 15 токенов/с

Это идеальный теоретический потолок, в котором нет накладных расходов фреймворка, синхронизаций, работы с KV‑кэшем и других операций. Реальные 12 токенов в секунду внезапно перестают выглядеть провалом. Наоборот, для обычной последовательной генерации BF16-модели это результат довольно близкий к физическому пределу машины.

512 ГБ объединённой памяти позволяют загрузить огромную модель. Но объём памяти и скорость памяти — не одно и то же. Объединённая архитектура убирает лишнее копирование между RAM и видеопамятью, но не отменяет необходимость читать десятки гигабайт весов на каждом шаге.

CPU‑ядер здесь тоже нельзя просто прибавить к GPU‑ядрам. Они выполняют другую работу и не превращают последовательную генерацию в параллельную. Проблема была не в том, что приложение «не видело» часть M3 Ultra.

Проблема была в самой механике авторегрессионной генерации.

Два пути в обход ограничения

Если память нельзя заставить мгновенно стать в несколько раз быстрее, остаётся другой вариант: получать больше одного токена за дорогой проход основной модели.

Так я вышел на speculative decoding — спекулятивную генерацию. Её идея в том, что более лёгкий механизм заранее предлагает сразу несколько следующих токенов, а большая модель проверяет их за один проход. Если гипотеза верна, стоимость чтения весов распределяется уже не на один токен, а на целый принятый блок.

Для Qwen3.8–27B рассматривались два варианта:

  • MTP, встроенный в модель механизм предсказания нескольких следующих токенов;

  • DFlash2, отдельная draft‑модель на основе блочной диффузии (block diffusion), которая параллельно предлагает блок токенов для проверки основной моделью. Именно так этот механизм описан в документации модели DFlash2.

В oMLX эти режимы являются альтернативными: одновременно включить MTP и DFlash нельзя. Поэтому расследование разделилось на две ветки.

О результатах MTP я расскажу отдельно. А в этой статье пойдём по следу DFlash2.

Почему именно oMLX

Основная модель уже была скачана через LM Studio. Поэтому логичным первым вопросом было: зачем вообще ставить ещё один сервер?

Ответ — DFlash2.

LM Studio умеет обычное speculative decoding: небольшая draft‑модель последовательно предлагает токены, а основная их проверяет. Именно такой механизм описан в официальной документации LM Studio. Но DFlash2 — не просто ещё одна маленькая чат‑модель с тем же словарём. Это отдельный block‑diffusion drafter, для которого движок должен уметь параллельно построить блок кандидатов, проверить его основной моделью, принять совпавшую часть и корректно откатить кэш после первого расхождения.

В использованной версии LM Studio такого режима для Qwen3.8–27B‑DFlash2 не было.

С mlx-vlm ситуация тоньше. В проекте уже появилась поддержка DFlash для части моделей Qwen, поэтому утверждать, что библиотека «вообще не знает DFlash», было бы неправильно. Но для конкретной связки Qwen3.8-27B + Qwen3.8-27B-DFlash2 мне нужен был не экспериментальный фрагмент кода, а подтверждённый серверный контур с управляемыми параметрами и возможностью проверить, какой движок реально обслужил запрос.

Из рассмотренных вариантов это дал oMLX:

  • отдельное включение DFlash для каждой target‑модели;

  • выбор конкретной draft‑модели;

  • настройки Draft Quantization, Window, Sink и Verify Mode;

  • OpenAI‑совместимый API для последующего бенчмарка;

  • серверный лог с фактической загрузкой DFlashEngine, acceptance и количеством циклов.

То есть oMLX был выбран не потому, что у него приятнее интерфейс. На момент эксперимента это был единственный из проверенных мной вариантов, где поддержка DFlash2 именно для Qwen3.8–27B была не предположением, а воспроизводимым фактом. Сама интеграция DFlash в oMLX реализует полный цикл: draft, verify, accept и replay.

При этом модели не пришлось скачивать заново: каталог ~/.lmstudio/models был добавлен в oMLX как Model Directory. LM Studio осталось хранилищем уже загруженных весов, а oMLX стало движком эксперимента.

Улика № 1: переключатель ещё ничего не доказывает

Первым делом нужно было убедиться, что DFlash действительно работает, а не просто числится включённым.

На этом этапе target‑модель была только одна — исходная mlx-community/Qwen3.8-27B-bf16. Цель расследования пока не менялась: сохранить BF16 и найти способ ускорить её без снижения разрядности весов.

В пару к ней была подключена draft‑модель incoai/Qwen3.8-27B-DFlash2.

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

Поэтому главный показатель — не только скорость draft‑модели, но и acceptance, то есть доля принятых предположений.

А главным свидетелем стал серверный лог:

DFlashEngine loaded: target=... draft=...

После генерации в нём появлялась ещё одна запись:

DFlash generation complete: ... acceptance=... cycles=...

Вот это уже доказательство. DFlash не просто выбран в интерфейсе — движок загрузился, отработал генерацию и показал acceptance.

Расследование можно было продолжать.

Первый подозреваемый: размер окна

Исходная конфигурация выглядела так:

  • Draft Window — 2048;

  • Draft Sink — 0;

  • Verify Mode — adaptive;

  • Draft Quantization — OFF.

На ней BF16-версия Qwen3.8–27B показала 17,9 токена в секунду при acceptance 68,3%.

Возник естественный вопрос: а что, если уменьшить окно до 1024? Меньше контекста для draft‑модели, меньше вычислений — вроде бы должно стать быстрее.

Не стало.

Скорость упала до 13,8 токена в секунду, а acceptance — до 58,8%. То есть draft‑модель начала чаще промахиваться, и экономия на размере окна не компенсировала потери при проверке.

Это важный момент. В системах спекулятивной генерации нельзя оптимизировать один компонент в отрыве от всей цепочки. Локально операция может стать дешевле. Но если после неё основная модель вынуждена чаще переделывать работу, общая производительность падает.

У окна 2048 появилось алиби.

Второй подозреваемый: квантизация draft‑модели

Следующая гипотеза выглядела ещё убедительнее. Если квантизация ускоряет модель, почему бы не квантовать DFlash draft?

Проверили два варианта:

Draft‑конфигурация

Скорость

Acceptance

Quantization OFF

17,9 ток/с

68,3%

W8A16

14,3 ток/с

58,0%

W4A16

13,3 ток/с

55,3%

И вот здесь расследование стало интересным.

Квантованная draft‑модель действительно может вычисляться быстрее. Только системе от этого не легче: качество её предположений снижается, основная модель принимает меньше токенов, и итоговый throughput становится хуже.

Мы ускорили помощника, который начал чаще приносить следователю неправильные улики.

В результате работы стало не меньше, а больше.

Вывод получился довольно жёстким: Draft Quantization нужно оставить выключенной. По крайней мере, для этой модели и этой проверенной конфигурации.

Неожиданный поворот: квантовать нужно было не того

После всех настроек результат BF16 стал понятен. Без DFlash модель выдавала 12,0 токена в секунду. С DFlash2, окном 2048 и неквантованной draft‑моделью — 17,9 токена в секунду при acceptance 68,3%.

Ускорение — примерно 1,49 раза, или +49%.

Уже неплохо. Но всё ещё недостаточно для комфортной интерактивной работы с длинными ответами. Окно уменьшать нельзя — скорость падает. Квантовать draft‑модель тоже нельзя — вместе с acceptance падает итоговый throughput. Возможности ускорить именно BF16 в рамках проверенных настроек практически закончились.

И только тогда появилась 8-bit‑версия.

Я решил оставить найденную конфигурацию DFlash2 без изменений, заменить саму target‑модель на mlx-community/Qwen3.8-27B-8bit и посмотреть, как квантизация основной модели повлияет на скорость всей связки.

Сначала я запустил 8-bit без DFlash. Она показала 21,4 токена в секунду — уже быстрее BF16 с DFlash2.

Затем снова подключил к 8-bit ту же неквантованную Qwen3.8-27B-DFlash2 с окном 2048 и режимом проверки adaptive.

И картина изменилась:

Target‑модель

DFlash

Скорость

Acceptance

Qwen3.8–27B‑bf16

OFF

12,0 ток/с

Qwen3.8–27B‑bf16

ON

17,9 ток/с

68,3%

Qwen3.8–27B-8bit

OFF

21,4 ток/с

Qwen3.8–27B-8bit

ON

38,0 ток/с

66,8%

DFlash ускорил 8-bit‑версию примерно в 1,78 раза, то есть на 78%.

А связка 8-bit + DFlash оказалась в 2,12 раза быстрее, чем BF16 + DFlash: 38,0 против 17,9 токена в секунду.

Парадокс только кажущийся. Квантизация основной target‑модели уменьшила стоимость проверки. При этом точная, неквантованная draft‑модель сохранила достаточно высокий acceptance — 66,8%. Один участник связки стал дешевле, а второй не потерял способность хорошо предсказывать.

Именно баланс, а не одна «волшебная» настройка, дал лучший результат.

Конфигурация‑победитель

По итогам текущих прогонов лидер выглядит так:

  • Target — Qwen3.8-27B-8bit;

  • Draft — Qwen3.8-27B-DFlash2;

  • Draft Quantization — OFF;

  • Draft Window — 2048;

  • Draft Sink — 0;

  • Verify Mode — adaptive;

  • L1 In‑memory cache — ON;

  • Max entries — 4;

  • Cache size — 8 ГиБ;

  • Max Context — unlimited.

Результат — 38,0 токена в секунду при acceptance 66,8%.

Разумеется, это не универсальный паспорт скорости Qwen3.8–27B. Цифры относятся к конкретной машине, версиям моделей и параметрам запуска. На другом запросе или при другой длине генерации результат может отличаться, поэтому важен не один красивый замер, а воспроизводимая серия тестов.

Для локальной 27-миллиардной модели это уже не просто «вау, запустилось». Это скорость, с которой системой действительно удобно пользоваться в повседневной работе.

Но дело пока нельзя закрывать.

Скорость нашли. А что с качеством?

Мы нашли самую быструю конфигурацию. Но скорость генерации и качество ответа — не одно и то же.

8-bit‑версия может выдавать 38 токенов в секунду и при этом хуже справляться со сложным рассуждением, кодом, поиском ошибок или точным следованием инструкции. Именно по этим скоростным прогонам этого нельзя было ни подтвердить, ни опровергнуть.

Поэтому я провёл отдельное сравнение уже трёх версий Qwen3.8–27B — BF16, 8-bit и 4-bit — по нескольким категориям:

  • reasoning;

  • coding;

  • поиск ошибок в коде;

  • instruction following;

  • сложный анализ.

Код в нём проверялся компиляцией и hidden tests, а не другой LLM, выставляющей оценку по собственному впечатлению. Иначе мы просто заменили бы одно вероятностное мнение другим и назвали это измерением.

Результаты оказались неожиданнее, чем разница в скорости. Но смешивать два расследования в одной статье было бы неправильно: здесь мы искали способ ускорить локальную BF16-модель, а там уже выясняли, какой ценой это ускорение достаётся и всегда ли большее количество reasoning означает более высокое качество.

Мораль этого расследования простая.

Самая быстрая деталь не всегда делает быстрее всю систему. Квантованная draft‑модель проиграла из‑за падения acceptance. Квантованная target‑модель, наоборот, дала лучший результат, потому что сохранила сильного «напарника» и удешевила проверку его гипотез.

По скорости победитель найден: Qwen3.8–27B-8bit + DFlash2, 38 токенов в секунду.

По качеству следствие уже проведено. Результаты — в следующей статье.

Дисклеймер. Сравнение качества Qwen3.8–27B в BF16, 8-bit и 4-bit уже завершено. Это не анонс будущего эксперимента: бенчмарк проведён, код проверен компиляторами и hidden tests, результаты проанализированы. В следующей статье я покажу цифры и разберу главный парадокс — почему максимальный режим размышлений оказался не гарантией качества, а в некоторых задачах мешал модели дойти до рабочего результата.