Инженерное исследование: может ли этот порт на MLX стать ещё в 10× быстрее?
Дата: 2026-09-19. Машина: Apple M3 Max, 40 ядер GPU, 128 GiB унифицированной памяти, macOS 27.2, MLX / MLX Metal 0.32.2, вывод FP16. Этот отчёт содержит реальные локальные эксперименты, включая написанное вручную ядро Metal. Он не меняет производственную среду выполнения и не публикует квантованные веса.
Протестированные инженерные изменения не дают 10×. Чередующиеся измерения поддерживают скромные, зависящие от формы улучшения от компиляции и отсечения неиспользуемых выходов последнего слоя головы решений. Отдельные случаи улучшились примерно на 3–8% по парным медианам за раунд. Некоторые интервалы для более крупных пакетов не включают улучшения. Пользовательское ядро точного GELU/вентиля на основе erf было численно успешным, но не дало устойчивой дополнительной сквозной выгоды сверх компиляции MLX. Наивное 8-битное и 4-битное квантование бэкбона уменьшило хранение, не ускорило более крупные пилотные нагрузки и изменило предсказания или калиброванные вероятности.
Математические пределы и компромиссы аппроксимации рассматриваются отдельно в MATH_10X_RESEARCH.md. Исходный обзор реализации — в PERFORMANCE_RESEARCH.md; опубликованный бенчмарк чекпойнта остаётся BENCHMARKS.md.
Экспериментальные контроли и ограничения
Вся исследовательская работа на GPU выполнялась последовательно. Другая работа агента использовала
только CPU/файловую систему/сеть. Машина работала от сети питания, без записанного предупреждения pmset о тепле/производительности
и без сообщённого использования swap во время эксперимента. Обычная активность рабочего стола
продолжалась. Это не контролируемая термокамера и не иначе простаивающая
выделенная бенчмарк-машина.
Первые отборочные запуски выполняли каждого кандидата в свежем процессе, с 4–5 прогревами и 12–16 выборками. Они выявили существенный дрейф от запуска к запуску. Например, английский пилот с одним вопросом предположил улучшение компиляции в 1.24×, тогда как последующий чередующийся эксперимент обнаружил лишь около 1.03×. Поэтому задержки последовательного пилота — отборочное свидетельство, а не основное причинное заявление об ускорении.
Скрипт подтверждения paired.py:
- Чередует порядок кандидатов внутри каждого раунда и использует одни и те же входы для каждого кандидата в этом раунде.
- Меняет фактический текст состояния между раундами. Он генерирует до 16 вариантов состояния и сохраняет варианты с той же формой тензора; многоязычные короткие случаи имеют 10 таких вариантов, тогда как остальные отчётные случаи имеют 16.
- Использует разные вопросы на естественном языке, включая 50 разных инструкций для самой крупной короткой нагрузки. Он проверяет хеши входов и не кэширует ответы, не дедуплицирует вопросы и не переиспользует контекстные состояния энкодера.
- Вычисляет результаты и синхронизирует GPU перед остановкой каждого таймера. Он измеряет как подготовленные вызовы прямого прохода, так и публичный путь предсказания, включая токенизацию и форматирование вывода. Загрузка модели исключена.
- Выполняет 32 измеренных раунда для английского эксперимента голова/компиляция и 16 для многоязычных и пользовательских Metal-экспериментов, после прогрева. Каждый кандидат видит то же число раундов и ту же последовательность входов.
Эти входы отличаются от опубликованных базовых фикстур. Сравнения ниже — внутри исследовательского эксперимента, а не сравнения до/после, полученные делением несвязанных таблиц. Лимит пакета в исследовании — 64, тогда как выпущенный API по умолчанию использует 16. Повторяющиеся варианты состояния — это намеренные повторные измерения; кэша результатов нет.
analyze.py вычисляет по раундам
отношения eager_time / candidate_time и исследовательские процентильные бутстрап-интервалы для их медианы, используя 2 000 пересэмплирований индексов раундов. Эти интервалы
не учитывают все источники шума операционной системы или серийную корреляцию
и не являются заменой многосессионной репликации. Отношение независимо
вычисленных значений p50 может отличаться от медианы парного отношения.
Сырой JSON включает все тайминги, хеши входов, метаданные окружения, метрики паритета, и отпечаток источника, записанный на момент измерения. Скрипты эксперимента впоследствии были отформатированы и расширены непересекающимися опциональными кандидатами; более ранние отпечатки описывают те более ранние версии скриптов.
Компиляция и точное отсечение финальной головы
Сравнивались четыре пути:
- Eager: выпущенная
DecisionModelв FP16. - Compiled:
mx.compileвокруг загруженной, вычисленной, замороженной модели, с обычной специализацией по форме. - Selected Q + compiled: в последнем слое головы сохранить полноразмерную проекцию QKV и K/V, но выполнять только запросы внимания CLS/маркеров вариантов. Запускать проекцию вывода и FFN только на этих выбранных выходах.
- Full attention + selected outputs + compiled: сохранить исходный полноразмерный QKV и вызов SDPA, затем собрать выходы CLS/вариантов перед проекцией вывода и FFN. Это сохраняет исходную форму ядра внимания, убирая большую часть неиспользуемой плотной работы финальной головы.
Оба прототипа отсечения сохраняют математические зависимости модели. Они всё равно вычисляют все проекции QKV; они не реализуют дополнительную экономию проекции только Q из расчёта математической верхней границы. Изменение форм GEMM и SDPA может изменить округление с плавающей точкой. Ни один прототип не является декодерным кэшем, ранним выходом или аппроксимацией, отбрасывающей более ранние слои трансформера.
Сквозная задержка p50, миллисекунды:
| Модель / запрос | B × L | Eager | Compiled | Selected Q + compiled | Full attention + selected outputs + compiled |
|---|---|---|---|---|---|
| Английский короткий 1 | 1 × 78 | 16.628 | 16.185 | 15.925 | 15.636 |
| Английский короткий 16 | 16 × 82 | 116.920 | 113.700 | 112.009 | 110.217 |
| Английский длинный 1 | 1 × 512 | 53.921 | 53.078 | 52.301 | 52.121 |
| Английский длинный 8 | 8 × 512 | 531.166 | 518.428 | 504.135 | 488.980 |
| Английский короткий 50 | 50 × 82 | 456.333 | 439.013 | 445.223 | 438.293 |
| Многоязычный короткий 1 | 1 × 80 | 8.050 | 7.570 | 7.438 | 7.388 |
| Многоязычный короткий 16 | 16 × 83 | 44.351 | 43.830 | 42.281 | 42.968 |
| Многоязычный длинный 1 | 1 × 1024 | 41.964 | 42.017 | 40.492 | 41.120 |
| Многоязычный длинный 8 | 8 × 1024 | 326.327 | 323.053 | 327.842 | 319.010 |
Источники: английские парные данные и многоязычные парные данные.
Для пути полное-внимание/выбранные-выходы парное медианное ускорение и исследовательские 95%-е интервалы включают:
| Запрос | Медианное парное ускорение | Бутстрап-интервал |
|---|---|---|
| Английский короткий 1 | 1.049× | 1.043–1.056× |
| Английский короткий 16 | 1.059× | 1.033–1.077× |
| Английский длинный 1 | 1.039× | 1.027–1.052× |
| Английский длинный 8 | 1.061× | 1.020–1.095× |
| Английский короткий 50 | 1.022× | 0.977–1.050× |
| Многоязычный короткий 1 | 1.077× | 1.046–1.140× |
| Многоязычный короткий 16 | 1.042× | 1.017–1.067× |
| Многоязычный длинный 1 | 1.027× | 1.012–1.054× |
| Многоязычный длинный 8 | 1.067× | 0.958–1.082× |
Интервалы для английских 50 вопросов и многоязычного длинного пакета включают 1. Они не устанавливают воспроизводимое улучшение. Путь selected-Q несколько лучше для многоязычных случаев short-16 и long-1, но ни один путь отсечения не доминирует над всеми формами. Все интервалы кандидатов, измерения прямого прохода и сырые по-раундовые отношения — в paired_analysis.json.
Компиляция точно совпала с eager по логитам, логитам действий и калиброванным вероятностям на 1 530 сравнениях вопросов с изменённым входом по двум семействам моделей в этом эксперименте голова/компиляция. Оба пути отсечения совпали по всем 1 530 решениям argmax, с максимальным различием калиброванной вероятности 0.0001883. Путь отсечения с полным вниманием также прошёл отдельный набор из 63 вопросов-фикстур для каждой модели: 126/126 согласий, с максимальными различиями вероятностей 4.31e-5 для английского и 6.48e-6 для многоязычного. Это регрессионные проверки, а не заявление о точности на задаче на 1 530 независимо размеченных примерах.
Компиляция всей модели и по блокам обе прошли отбор. Эксперимент с блоками
также сохранил все 63 выхода английских фикстур, но не установил существенного
преимущества над компиляцией всей модели. Специализация по форме должна быть ограниченной в
сервисе. Модель использует зависящие от формы Python-переформы и маски, поэтому применение
shapeless=True без разбора небезопасно. Официальное руководство по компиляции
документирует специализацию по форме и захват состояния.
Первый вызов кандидата «вся английская модель» занял 2 166.7 ms, после чего
в том пилоте тёплый прямой проход p50 составил около 12.75 ms; первый вызов новой формы B16 занял
272.4 ms. Поле JSON называется cold_forward, но оно означает первый
вызов кандидата после эталонного eager-вывода, а не полностью холодное приложение
или только что инициализированный драйвер Metal. Последующие кандидаты переиспользовали ранее
скомпилированные ядра Metal, поэтому их времена первого вызова не являются контролируемым рейтингом
стоимости холодного старта. Активная/пиковая память MLX скомпилированного английского short-1 составила около
803.6/918.6 MiB в пилоте; у многоязычного — около 614.1/676.9 MiB. Эти
измерения распределителя не включают каждое выделение компилятора на стороне хоста и
не устанавливают пределы памяти при неограниченной смене форм. См.
пилот компиляции английского и
пилот компиляции многоязычного.
Выборочное квантование: полезная экономия хранения, непригодная как заявление о скорости
Прототип вызывает nn.quantize после загрузки плотной модели FP16. Он
выбирает только линейные модули encoder.layers.*, с аффинным размером группы 64, затем
компилирует получившуюся модель. Эмбеддинги, нормы, голова решений, scorer и
голова действий остаются в FP16. Это избегает приведения упакованных целочисленных весов через
текущий плотный загрузчик и избегает неделимой входной ширины 1028/772 головы действий. Никакой квантованный формат чекпойнта или контракт загрузки не поставляется.
Официальная реализация квантованного слоя MLX
предоставляет этот механизм выбора.
| Модель / точность энкодера | Общее хранение тензоров | Согласие на фикстуре | Наибольшее изменение вероятности на фикстуре | Согласие на разных нагрузках | Наибольшее изменение вероятности на разных нагрузках |
|---|---|---|---|---|---|
| Английский FP16 | 803.55 MiB | Эталон | — | Эталон | — |
| Английский 8 бит | 496.76 MiB | 62/63 | 0.0401 | 18/18 | 0.0312 |
| Английский 4 бита | 333.13 MiB | 50/63 | 0.3256 | 18/18 | 0.2224 |
| Многоязычный FP16 | 613.99 MiB | Эталон | — | Эталон | — |
| Многоязычный 8 бит | 515.38 MiB | 63/63 | 0.0133 | 26/26 | 0.0358 |
| Многоязычный 4 бита | 462.79 MiB | 63/63 | 0.1268 | 19/26 | 0.8008 |
Результат многоязычного 4-битного варианта иллюстрирует, почему одного небольшого набора фикстур недостаточно: его 63 argmax на фикстурах остались теми же, но 7 из 26 решений на разных нагрузках изменились. Это измерения согласия с FP16, а не измерения точности относительно эталона истины. Абсолютное изменение вероятности 0.8008 — это 80.08 процентного пункта.
На пилотных входах английского short-16 сквозная p50 eager/compiled FP16 составила 91.26/87.94 ms; скомпилированное 8-битное/4-битное — 96.66/93.20 ms. Квантование short-1 выглядело несколько быстрее в том отборочном запуске, тогда как более крупные формы — нет. Отбор многоязычных крупных форм также не показал выигрыша по скорости, но его последовательные запуски имели существенный дрейф. Эти наблюдения оправдывают отказ от безоговорочного заявления об ускорении или релизе, а не приписывание точных коэффициентов замедления без чередующейся квантованной репликации. Дальнейшая работа по квантованию требует калибровки с учётом активаций или дообучения и представительного размеченного набора качества.
Сырые источники: английский 8 бит, английский 4 бита, многоязычный 8 бит, многоязычный 4 бита.
Написанный вручную Metal: точная fusion GELU/вентиль была реализована и протестирована
kernels.py реализует реальное пользовательское ядро Metal, которое читает две конкатенированные ветви MLP, вычисляет тот же GELU на основе erf, умножает на вентиль и записывает один выход. Оно не подставляет tanh-GELU или сигмоидную аппроксимацию. Ядро использует собственные помощники erf и expm1 MLX v0.32.2, сохраняя их лицензии и уведомления в vendor/README.md. Оно явно поддерживает только FP16 и использует безопасный математический режим Metal. Официальное руководство по пользовательским ядрам описывает этот API и его управление математическим режимом.
По восьми представительным формам активаций 27 958 016 случайно сгенерированных выходных элементов FP16 имели в точности равные значения исходной операции. Микробенчмарк сравнивает численное равенство, а не знаковый бит нуля. Тесты всей модели с изменённым входом также совпали точно: 474/474 сравнения вопросов по двум семействам моделей, плюс оба набора из 63 вопросов-фикстур, с нулевым различием логитов, логитов действий и калиброванных вероятностей.
Этот результат корректности не превратился в устойчивое преимущество по скорости над
слитым скомпилированным выражением MLX. Например, при 1 312 токенах и промежуточной
ширине 2 624 синхронизированное время активации на вызов составило 0.378 ms для eager
GELU-затем-вентиль, 0.268 ms для mx.compile и 0.280 ms для пользовательского ядра. При
8 192 токенах и ширине 1 152 соответствующие значения были 0.846/0.764/0.714 ms.
Эти микробенчмарки включают накладные расходы диспетчеризации и синхронизации и
являются отборочными пробами; они не являются измерениями изолированного времени выполнения
на устройстве. Полные входы, сырые тайминги и проверки равенства — в
microbench.json.
Затем пользовательское ядро было установлено в каждый MLP энкодера и измерено в полной модели с чередующимся порядком кандидатов и изменяющимися входами:
| Модель / запрос | Оригинальный compiled p50 | Metal + compiled p50 |
|---|---|---|
| Английский короткий 1 | 23.795 ms | 23.837 ms |
| Английский короткий 16 | 142.716 ms | 139.355 ms |
| Английский длинный 1 | 68.241 ms | 68.982 ms |
| Многоязычный короткий 1 | 7.557 ms | 7.437 ms |
| Многоязычный короткий 16 | 49.683 ms | 50.301 ms |
| Многоязычный длинный 1 | 48.906 ms | 51.032 ms |
Парные запуски с полным пользовательским ядром используют второй экземпляр модели с идентичными весами, чтобы неизменённая и пользовательская реализации сосуществовали без мутации или устаревших скомпилированных захватов. Их абсолютные тайминги нельзя сравнивать с более ранним запуском отсечения головы. Скромные смешанные результаты не поддерживают публикацию пользовательского ядра как общего улучшения производительности. Источники: парные данные Metal английского и парные данные Metal многоязычного.
Где пользовательская инженерия заслуживала бы дальнейшего исследования
Модель уже вызывает mx.fast.scaled_dot_product_attention, mx.fast.rope,
и оптимизированную нормализацию слоя. Её путь SDPA с булевой маской D64 слит; нет
отсутствующего переключателя Flash Attention, который объяснял бы разрыв в 10×. Локальное внимание
всё ещё проходит по плотным тайлам ключей/значений. Настоящее ядро двунаправленного окна могло бы
пропускать эти тайлы, сохраняя инклюзивное расстояние <=64 и семантику дополнения,
но его возможность арифметии всей модели мала на коротких входах и
ограничена на опубликованных длинных формах. Существующий обзор исходников
и математический отчёт количественно оценивают это различие.
Полезные следующие проекты с их требованиями к доказательствам:
- Внимание с окном для длинных входов: специализировать границы тайлов для D64, фактического двунаправленного окна и дополненных пакетов. Сравнить со слитым плотным SDPA при 512/1024 токенах, а затем в полной модели. Это ядро не было построено или измерено в этом отчёте.
- Эпилоги плотных ядер и планирование: исследовать слияние эпилога MLP с вентилем в GEMM или улучшение планирования матриц при малом M. MLX уже использует специализированные реализации Metal GEMM, поэтому их замена требует реального профиля диспетчеризации/ядер и измеренных выигрышей для точных форм M/N/K. Результат для автономной активации показывает, почему одного ещё одного поэлементного ядра недостаточно.
- Пакетирование с учётом длины и общая подготовка CPU: сохранять точные входные ID, токенизируя общий текст состояния один раз перед построением каждой последовательности вопроса, и избегать дополнения малых элементов до несвязанных длинных. Пилот многоязычного long-8 потратил около 13.1 ms на подготовку входов против сотен миллисекунд сквозного времени. Даже полное устранение этой подготовки не дало бы 10× на этой нагрузке. Задержка в очереди и число уникальных выводов должны быть частью любого заявления о пакетировании.
- Меньший студент, отвечающий совместно: если 10× — продуктовое требование, дистиллируйте или перепроектируйте модель, чтобы убрать большую часть плотной работы или отвечать на многие фиксированные вопросы одним контекстным кодированием. Это меняет обученную модель и требует представительного размеченного обучения/оценки; это не оптимизация точного порта. Переиспользование произвольного контекстного состояния/KV между вопросами в текущем двунаправленном энкодере некорректно.
Восемь автономных проб GEMM проекции входа энкодера на FP16 достигли 0.66–11.55
TFLOP/s, включая синхронизацию на вызов. Крупная английская
проба M=4096, N=5248, K=1024 достигла 11.55 TFLOP/s; многоязычная
проба M=8192, N=2304, K=768 достигла 7.96 TFLOP/s. Это наблюдаемые значения
пропускной способности, а не пиковые характеристики аппаратного обеспечения или верхние границы пропускной способности полного графа.
Измерения при малом M особенно подвержены затратам на отправку и
синхронизацию; потоковый граф амортизирует их иначе. Они показывают,
какие формы заслуживают профилирования, а не доказательство того, что лучшего ядра быть не может.
Бюджеты пропускной способности 10× при той же работе из математического отчёта остаются теоретическими
требованиями, а не измеренными возможностями устройства.
Воспроизведение и решение о выпуске
Скрипты используют существующий .venv и локальные закреплённые чекпойнты. Запускайте команды GPU
последовательно, никогда вместе с формальным бенчмарком:
# Screening: repeat for eager, compiled, blocks, q8, q4, selected-compiled.
.venv/bin/python -m experiments.engineering.run_variants \
--model laya --variant compiled --iterations 12 --warmup 4 --quality \
--output experiments/engineering/reproduced-compiled.json
# Primary confirmation, including 50 genuinely different questions.
.venv/bin/python -m experiments.engineering.paired \
--model laya --iterations 32 \
--output experiments/engineering/reproduced-laya-paired.json
.venv/bin/python -m experiments.engineering.paired \
--model laya-multilingual --iterations 16 --cases short1,short16,long1,long8 \
--output experiments/engineering/reproduced-multilingual-paired.json
# Hand-written kernel microbench and complete-model comparison.
.venv/bin/python -m experiments.engineering.microbench
.venv/bin/python -m experiments.engineering.paired \
--model laya --iterations 16 --cases short1,short16,long1 --metal \
--output experiments/engineering/reproduced-metal-paired.json
.venv/bin/python -m experiments.engineering.run_variants \
--model laya --variant metal-compiled --iterations 5 --warmup 3 \
--cases short1 --quality --output experiments/engineering/reproduced-metal-quality.json
# CPU-only paired analysis.
.venv/bin/python -m experiments.engineering.analyze
Все экспериментальные Python-файлы проходят проверки форматирования и линтинга Ruff. Стабильная среда выполнения, исходные результаты бенчмарка и опубликованные чекпойнты FP16 остаются релизными артефактами. Компиляция и точное отсечение финальной головы — заслуживающие доверия опциональные будущие оптимизации после политики холодных форм/кэша и более широкой валидации качества; измеренные выигрыши не оправдывают молчаливого добавления задержки компиляции или пользовательского ядра в путь по умолчанию. Никакое ускорение в 10×, готовый к производству квантованный чекпойнт или измеренный выигрыш локально-оконного ядра не заявляется.