Исследование производительности Laya MLX
Дата исследования: 2026-09-19. Цель: Apple M3 Max, 40 ядер GPU, 128 GiB унифицированной памяти, MLX/MLX Metal 0.32.2. Это статический обзор нативной среды выполнения, установленной реализации MLX, официальной документации и существующего JSON бенчмарка. Для этого исследования не запускался ни один GPU-бенчмарк или вывод модели. Ни одна из предложенных ниже оптимизаций не имеет измеренного ускорения в этом отчёте.
Первыми экспериментами должны стать компиляция всей модели и представительное пакетное планирование, а затем выборочное квантованное умножение матриц. Они касаются доминирующей повторяющейся работы. Специализированное ядро локального внимания — правдоподобный долгосрочный проект для длинных входов. Точное отсечение последнего слоя головы решений выполнимо, но его арифметическая экономия на уровне всей модели составляет всего несколько процентов. Большие улучшения без изменения чекпойнта потребуют улучшения плотного бэкбона, устранения по-настоящему избыточных запросов или нахождения измеренного узкого места реализации; просто замены активации или включения ещё одного флага внимания, скорее всего, недостаточно.
Что устанавливают существующие измерения
Ниже — существующие сквозные медианные задержки, включая подготовку промпта и форматирование результата, с синхронизированным завершением GPU, пятью прогревами и 50 измеренными итерациями. Загрузка и скачивание модели исключены. Бенчмарк допускает пакет из 64 вопросов; публичное значение по умолчанию среды выполнения — 16, поэтому результат на 50 вопросов не является конфигурацией API по умолчанию.
| Чекпойнт / точность | Короткий 1 вопрос | Короткий 10 вопросов | Короткий 50 вопросов | Длинный 1 вопрос | Длинный 10 вопросов |
|---|---|---|---|---|---|
| Laya MLX FP16 | 13.421 ms | 71.068 ms | 336.030 ms | 44.927 ms | 420.987 ms |
| Laya MLX FP32 | 15.954 ms | 98.820 ms | 450.712 ms | 61.331 ms | 534.242 ms |
| Laya stock Torch MPS FP32 | 24.918 ms | 95.265 ms | 497.856 ms | 65.581 ms | 586.594 ms |
| Multilingual MLX FP16 | 7.390 ms | 27.386 ms | 127.565 ms | 37.635 ms | 389.487 ms |
| Multilingual MLX FP32 | 7.988 ms | 32.337 ms | 151.387 ms | 47.208 ms | 451.331 ms |
| Multilingual stock Torch MPS FP32 | 19.349 ms | 43.158 ms | 194.171 ms | 52.939 ms | 534.492 ms |
Источники: Laya FP16, Laya FP32, Laya MPS, multilingual FP16, multilingual FP32 и multilingual MPS. Длинные входы содержат 512 токенов для Laya и 1024 для multilingual; поэтому сравнение их задержек на длинных входах не является сравнением при равной длине последовательности. Короткие дополненные длины равны 93 и 91 соответственно. Сравнения с Torch должны сохранять метку FP32: они сочетают смену бэкенда со сменой точности при сравнении с MLX FP16.
Есть заметная вариативность между запусками. Например, многоязычный запуск FP16 на 10 длинных вопросов имеет p50 389.487 ms, p95 462.319 ms и максимум 619.663 ms. Его медиана прямого прохода на один короткий вопрос — 8.023 ms, тогда как независимо измеренная сквозная медиана — 7.390 ms. Вычитание этих медиан дало бы бессмысленное отрицательное время предобработки. Текущие файлы не изолируют токенизатор, диспетчеризацию Python, отдельные ядра GPU или затраты на синхронизацию. Они устанавливают полезные базовые значения, а не диагностику узкого места на уровне ядра.
Отчёты о валидации фиксируют согласие argmax 63/63 для каждого из трёх чекпойнтов в FP32 и FP16, и 100 конечных детерминированных повторных вызовов на вариант: 378/378 согласий по ответам и 600 повторных вызовов всего. Это регрессионные проверки на небольшом корпусе фикстур, включая повторяющиеся вопросы. Они не являются доказательством того, что будущее квантование или изменения архитектуры сохранят общую точность на задачах.
Почему плотное умножение матриц заслуживает приоритета
Реализация модели применяет проекции QKV и вывода, плотный MLP энкодера и два обычных слоя головы решений трансформера к каждому дополненному токену. Пусть D — скрытый размер, I — промежуточный размер энкодера, N — число слоёв энкодера, а H — число слоёв головы решений. Число матричных весов, используемых на токен в этих блоках:
A = N * (4 * D^2 + 3 * D * I) + H * 12 * D^2
dense FLOPs per batch ~= 2 * B * L * A
dense attention FLOPs ~= 4 * B * (N + H) * L^2 * D
Эти оценки считают умножение и сложение раздельно и исключают нормализацию, активации, эмбеддинги, оценивание, маскирование, перемещение памяти и накладные расходы ядер. Это арифметическая модель, а не профиль времени выполнения.
| Семейство чекпойнта | D / I / слои энкодера | Веса эмбеддинга токенов | Основные пошаговые матричные веса A | Глобальные / локальные слои энкодера |
|---|---|---|---|---|
| Laya / typed decisions | 1024 / 2624 / 28 | 51,576,832 | 368,312,320 | 10 / 18 |
| Multilingual | 768 / 1152 / 22 | 196,608,000 | 124,452,864 | 8 / 14 |
Примерно 322 миллиона общих параметров многоязычного чекпойнта включают 196.6 миллиона параметров эмбеддинга. Для вывода собираются только выбранные строки эмбеддинга; это не проекция по всему словарю. Его основная пошаговая матричная нагрузка примерно в три раза меньше, чем у английской модели, несмотря на то, что их общие числа параметров выглядят куда ближе. Это согласуется с измеренным разрывом в задержке короткого пакета, хотя и не доказывает конкретного аппаратного узкого места. Квантование только эмбеддингов преимущественно уменьшило бы резидентный размер весов, особенно для multilingual; оно не обязано улучшать задержку вывода.
При текущем плотном пути внимания произведения внимания составляют около 1.5% смоделированных FLOPs при L=93 для Laya, 7.9% при L=512 для Laya и 23.3% при L=1024 для multilingual. Их доля от настенного времени может существенно отличаться. Профилировщик должен различать матричные ядра, ядра внимания, поэлементные ядра, построение графа на CPU и периоды простоя, прежде чем браться за пользовательскую работу на Metal.
Приоритизированные эксперименты
| Приоритет | Эксперимент | Лучшая цель | Основной компромисс / условие приёмки |
|---|---|---|---|
| P0 | Профилировать одну короткую и одну длинную форму; скомпилировать модель или блоки энкодера | Задержка одиночного запроса и диспетчеризация Python | Сохранить идентичные выходы в пределах существующего допуска точности; измерять компиляцию первого использования отдельно |
| P0 | Настроить пакетирование по фактическому бюджету токенов и распределению длин | Много разных вопросов и трафик смешанной длины | Оптимизировать пропускную способность при ограничениях по p95 задержки и памяти; учитывать очередь |
| P1 | Квантовать выбранные линейные слои бэкбона, начиная с 8 бит, затем 4 бита | Трафик весов и, возможно, плотный вывод | Ворота качества и калибровки; фактическая скорость на M3 Max может ухудшиться |
| P1 | Кэшировать общую токенизацию состояния и стабильные шаблоны вопросов | Много вопросов, разделяющих состояние или повторяющиеся рубрики | Точная идентичность токенов; ограниченный кэш; разделять кэшированные и некэшированные результаты |
| P1 | Вычислять только требуемые выходы последнего слоя головы | Любая нагрузка, особенно более длинные последовательности | Точное отсечение зависимостей; умеренная арифметическая экономия всей модели |
| P2 | Настоящее локально-оконное внимание с границами тайлов | Нагрузки на 512/1024 токена | Сложность нового ядра; сохранить двунаправленное окно и семантику дополнения |
| P2 | Сливать остаток/норму или GELU/вентиль только там, где профиль оправдывает | Накладные расходы малых ядер или трафик активаций | Существующие быстрые ядра уже покрывают многое из этого; сохранить точную семантику GELU |
| Отдельная продуктовая функция | Дедуплицировать идентичные входы прямого прохода | Нагрузки, которые действительно повторяют вопросы | Сообщать число уникальных выводов и попаданий в кэш; не подавать это как общее ускорение ядра |
Скомпилировать полный вычисляемый путь вывода
Agent.forward() в настоящее время конструирует массивы, вызывает self.model и вычисляет результат. Охватывающего mx.compile на уровне модели или блока нет. Компиляция фиксированной формы может уменьшить построение графа Python и объединить поддерживаемые операции. MLX документирует специализацию по форме и явный захват состояния; он также предупреждает, что бесформенная компиляция не может безопасно сохранять произвольные зависящие от формы операции Python. См. официальное руководство по компиляции.
Начните с скомпилированного вызываемого объекта, созданного после загрузки, приведения и вычисления весов, с обычной специализацией по форме. Сохраните существующий нескомпилированный вызываемый объект для сравнения на паритет и совместимости с CPU. Для замороженной модели вывода веса могут оставаться захваченными для этого экземпляра модели; если веса или структура модуля меняются, перестройте вызываемый объект или явно захватите соответствующее состояние. Не переиспользуйте скомпилированное замыкание при замене чекпойнтов.
Протестируйте обёртку всей модели и, если ограничения трассировки или стоимость компиляции делают это непривлекательным, скомпилируйте блоки энкодера и голову отдельно. Текущий код читает x.shape в целые числа Python, изменяет форму с явными значениями пакета/длины, создаёт маски arange(length) и индексирует маркеры, используя выведенный из формы диапазон строк. Применение shapeless=True к этому полному графу без переработки небезопасно. Вынесение динамического построения маски за пределы скомпилированного блока и использование независимых от формы операций flatten/unflatten может позволить позже бесформенный вариант; проверьте его при изменённых B, L и числе маркеров.
Бакеты форм могут ограничить повторную трассировку, но дополнение имеет вычислительную стоимость. Дополнение L=93 до 96 добавляет примерно 3.2% пошаговой работы; дополнение до 128 добавляет примерно 37.6%. Сравните компиляцию точной формы с небольшими кратностями длины и ограниченным набором бакетов, основанных на рабочей нагрузке. Включите (batch size, padded length, marker slots, dtype, device/model instance) в решения о политике кэша и измерьте задержку холодной компиляции и удерживаемую память при изменении форм.
Автономный бенчмарк forward в worker.py вызывает agent.model напрямую. Если компиляция добавлена только в Agent.forward, текущий бенчмарк прямого прохода её обошёл бы, тогда как сквозной бенчмарк использовал бы её. Оба пути должны явно выбирать одну и ту же реализацию-кандидат для осмысленного сравнения. Сохраняйте mx.eval и синхронизацию GPU в процедуре хронометража: измерение только построения графа не измеряло бы вывод.
Пакетировать по полезным токенам, затем исследовать планирование матриц
collate_items дополняет каждый фрагмент справа до его самой длинной последовательности; среда выполнения группирует вопросы в порядке вставки. Для неоднородного трафика сортируйте или группируйте по подготовленной длине, используйте бюджет токенов в дополнение к потолку числа вопросов и восстанавливайте исходные ID вопросов и порядок вывода. Сравнивайте пакеты по 1, 2, 4, 8, 16, 32 и 64 только там, где они представительны для сервиса. Для онлайн-запросов включайте время ожидания пакета; одни лишь офлайновые вопросов/секунду могут скрыть неприемлемую задержку.
Текущая короткая нагрузка на 50 вопросов тратит впустую примерно 8.9% дополненных токенов для Laya и 5.8% для multilingual. Длинные строки бенчмарка не имеют потерь на дополнение. Следовательно, удаление дополнения или сортировка сами по себе дают ограниченный арифметический выигрыш на этих фикстурах. Распределение смешанной длины, включая один длинный вопрос среди многих коротких, необходимо, чтобы выявить производственную выгоду. Удаление дополнения должно сохранять позиции RoPE каждого примера, позиции маркеров и границы внимания; конкатенация примеров в одну последовательность без изолирующей маски меняет модель.
Для плотных ядер исследуйте фактические формы и strides. QKV уже является единой проекцией, а две входные ветви MLP энкодера уже разделяют проекцию. Разделение их без разбора добавило бы запусков. Сравнивайте явное выравнивание непрерывного входа [B,L,D] в [B*L,D] только если профилировщик или трассировка диспетчеризации MLX показывает нежелательные пакетные GEMM; фреймворк, возможно, уже эффективно выравнивает. Одного лишь изучения исходников недостаточно, чтобы заявлять об упущенной оптимизации GEMM.
Не вставляйте синхронизацию после каждого слоя в производственной реализации. Текущая среда выполнения вычисляет один раз на фрагмент. Лишние ожидания могут убрать перекрытие CPU/GPU и скрыть улучшение планирования; профилирование на уровне слоёв должно быть отдельным диагностическим запуском.
Квантование: нацельтесь на бэкбон и реализуйте его контракт хранения
Установленная реализация квантованных слоёв MLX 0.32.2 предоставляет nn.quantize(..., class_predicate=...) и QuantizedLinear с умножением матриц только по весам через mx.quantized_matmul. Групповое аффинное квантование поддерживает эксперименты с 8 и 4 битами. Начните с линейных слоёв энкодера при размере группы 64, сохраняя активации, нормы, типовые эмбеддинги, scorer и голову действий в FP16. Затем независимо добавьте линейные слои головы решений и, опционально, квантование эмбеддингов. Измеряйте каждый вариант; ядра только по весам могут проигрывать GEMM FP16 при большом числе токенов.
Есть конкретные риски интеграции в текущем загрузчике и модели:
Agent.__init__приводит каждый сохранённый вес к типу с плавающей точкой и создаёт только плотные модули перед строгой загрузкой. Квантованный чекпойнт требует метаданных, описывающих выбранные модули, размер группы, разрядность и режим; создайте соответствующие квантованные модули перед загрузкой и сохраните упакованные целочисленные веса. Приведение упакованных весов к типу с плавающей точкой не является допустимой загрузкой.- Первый линейный слой головы действий имеет входную ширину
D+4, а именно 1028 или 772, которая не делится на аффинный размер группы 32, 64 или 128. Поэтому сплошной вызов квантования непригоден. Проверьте входную ширину каждого выбранного слоя перед конвертацией. DecisionModel.__call__выбирает тип данных входа головы действий изself.act_head.layers[0].weight.dtype. Для квантованного слоя этот вес был бы упакованным целочисленным хранилищем, а не желаемым типом данных активации. Исключение головы действий изначально избегает этого пути; поддержка её позже требует явного контракта типа данных активации.- Квантование меняет логиты и калиброванные вероятности. Существующее согласие на небольшой FP16-фикстуре недостаточно как доказательство качества 4 бит. Используйте отложенные размеченные задачи choice, score и noul, многоязычные входы, близкие решения, разное число вариантов и примеры эскалации. Отслеживайте согласие argmax, точность на задаче, ошибку score, дрейф вероятностей, калибровку и вероятности действий. Насыщенные выходы действий могут скрывать большие изменения логитов действий.
Для аффинных масштабов и смещений FP16 при размере группы 64 приблизительное хранение матрицы составляет bits/8 + 4/64 байт на параметр: 1.0625 байта при 8 битах и 0.5625 байта при 4 битах, против 2 байт при FP16. Это оценки хранения для квантованных матриц, исключающие другие тензоры и накладные расходы упаковки; они не являются оценками ускорения. Официальный API quantize описывает делимость групп и форматы.
Более новые низкобитные форматы следует оценивать на фактическом бэкенде M3 Max, а не предполагать, что они используют аппаратное обеспечение более поздних чипов Apple. Проверка доступности NAX в MLX требует более нового поколения архитектуры, чем записанное устройство applegpu_g15s. См. проверку устройства MLX 0.32.2.
Переиспользуйте подготовку CPU там, где входы действительно идентичны
build_sequence сериализует, очищает и токенизирует одно и то же состояние отдельно для каждого вопроса. Она также отдельно токенизирует каждую инструкцию и вариант. Rust-токенизатор уже используется напрямую; замена токенизации Transformers не является оставшейся оптимизацией.
Сериализуйте и очищайте состояние один раз на вызов prepare, кодируйте его один раз и нарезайте его ID токенов на доступное место каждого вопроса. Кэшируйте неизменяемые подготовленные префиксы вопросов, когда одна и та же рубрика используется между состояниями, с ключами, включающими идентичность/ревизию токенизатора, тип вопроса, упорядоченные критерии, сериализацию инструкции, очистку специальных токенов и бюджеты токенов. Ограниченные кэши не должны переиспользовать результаты после изменений токенизатора или конфигурации. Пакетное кодирование токенизатором — ещё один эксперимент при условии, что его выход точно совпадает с текущей последовательностью независимых кодирований.
Не токенизируйте заново конкатенированный промпт как замену конкатенации независимо закодированных частей: границы подслов могут измениться. Проверяйте побайтово входные ID, маски внимания, позиции маркеров, qtypes, поведение усечения, структурированные критерии, литералы масок, пустые входы и отображение вывода.
Для длинного бенчмарка состояние повторяет предложение 200 раз до усечения. Избегание N повторных кодирований состояния могло бы помочь подготовке на CPU, но существующая разница во времени между сквозным и прямым проходом не измеряет эту экономию. Бенчмаркируйте prepare, сборку/построение массива, прямой проход и постобработку независимо, затем подтвердите сквозной результат на неповторяющемся отложенном корпусе.
Декодерный KV-кэш не применим к этому энкодеру. Его первый слой — глобальное двунаправленное внимание; представления токенов состояния зависят от вопроса, вариантов и их позиций. Переиспользование скрытых состояний состояния или K/V между разными вопросами меняет результаты. Токенизация и идентичные результаты по всему входу могут кэшироваться; произвольное контекстное состояние энкодера — нет.
Точно отсечь выходы последней головы решений
После последнего HeadLayer используются только токен [CLS] и токены-маркеры вариантов. Более ранние слои головы всё равно должны производить все токены, потому что последний слой читает их K/V. Только в последнем слое:
- Нормализовать все входные токены и вычислить все K/V.
- Собрать Q в
[CLS]и допустимых позициях вариантов и прогнать эти запросы по полной маскированной последовательности K/V. - Применить проекцию вывода, остаток, вторую норму и сеть прямого распространения только к этим выбранным позициям.
- Использовать выбранный выход
[CLS]для головы действий и выбранные выходы маркеров для оценивания; сохранить дополнение маркеров и исходный порядок.
Первая реализация может сохранить слитую полную проекцию QKV и собрать Q после. Более агрессивный вариант разделяет её веса на полную KV-проекцию и Q-проекцию выбранных токенов. Это экономит больше арифметики, но может сделать планирование GEMM менее эффективным. Дублирующиеся дополненные индексы маркеров безвредны только если маскированные результаты остаются ненаблюдаемыми. Случаи с 1 вариантом, многими вариантами и переменными маркерами нуждаются в явных проверках паритета. Поскольку меньшее число запросов может выбрать другое ядро SDPA, математическая эквивалентность не подразумевает бит-идентичных результатов с плавающей точкой.
При R = 1 + number of option slots сохранение полного QKV убирает примерно 18 * B * (L-R) * D^2 плотных FLOPs и 4 * B * L * (L-R) * D FLOPs внимания из последнего слоя головы. Разделение Q/KV меняет плотный коэффициент с 18 на 20. Относительно приведённой выше арифметической оценки всей модели, при R=5 получаем:
| Чекпойнт / длина | Сохранить слитый полный QKV | Также вычислять только выбранный Q |
|---|---|---|
| Laya, L=93 | 2.44% | 2.70% |
| Laya, L=512 | 2.60% | 2.86% |
| Typed decisions, L=1024 | 2.66% | 2.90% |
| Multilingual, L=93 | 4.03% | 4.47% |
| Multilingual, L=1024 | 4.22% | 4.58% |
Это статические сокращения FLOP, а не предсказанные сокращения задержки. Метод убирает большую часть работы из одного слоя головы, а не большую часть работы из модели. Он полезен потому, что сохраняет зависимости и реализуем без переобучения, а не потому, что обещает кратное ускорение всей модели.
Стройте по-настоящему локальное внимание только после измерения его вклада
Все вызовы внимания энкодера уже используют mx.fast.scaled_dot_product_attention; RoPE уже mx.fast.rope; nn.LayerNorm вызывает примитив быстрой нормализации. API внимания MLX принимает булевы маски и выполняет softmax в FP32. Текущий размер головы — 64. Диспетчеризация Metal MLX 0.32.2 поддерживает эту форму с масками-массивами и не выбирает неслётный запасной вариант для неё во время вывода. Нет свидетельств того, что булева маска Laya отключает слитое внимание. force_fused=True, доступный в установленной версии, полезен как диагностическое утверждение, но его не следует рекламировать здесь как новый быстрый путь.
Оставшееся ограничение — структурная разреженность. Реализация строит плотную локальную булеву маску формы [B,1,L,L]. В обычном ядре внимания Metal некозальный цикл проходит по всему диапазону тайлов KV; маска-массив применяется к оценкам после умножения QK. Это сохраняет семантику локального внимания, не используя локальный диапазон тайлов.
Точное специализированное ядро может ограничить каждый тайл запросов перекрывающимся окном K/V, сохранить накопление softmax в FP32 и избежать плотной маски L×L. Правильное окно двунаправленное и инклюзивное: abs(query_position - key_position) <= 64. Внутренние запросы могут видеть 129 позиций, несмотря на название конфигурации local_attention=128. Слои полного внимания и оба слоя головы решений должны оставаться глобальными. Дополненные ключи должны оставаться исключёнными, а неиспользуемые дополненные запросы нуждаются в определённом конечном поведении.
Менее трудоёмкий прототип может группировать блоки запросов с перекрывающимися срезами K/V и вызывать существующий SDPA с меньшей точной маской. Используйте уже позиционированные RoPE Q/K или явно сохраняйте абсолютные смещения. Предпочитайте пакетные блоки множеству вызовов Python и учитывайте дублирующееся материализование K/V. Этот прототип может проигрывать текущему ядру на коротких длинах; это эксперимент на корректность и точку окупаемости перед поддержкой пользовательского Metal.
Максимальное сокращение смоделированных FLOPs всей модели от удаления всех запрещённых пар локального внимания мало для коротких входов и более многообещающе для длинных:
| Чекпойнт / длина | Идеальное сокращение общих FLOP от точной локальной разреженности |
|---|---|
| Laya, L=93 | 0.086% |
| Laya, L=512 | 3.61% |
| Typed decisions, L=1024 | 7.69% |
| Multilingual, L=93 | 0.147% |
| Multilingual, L=1024 | 11.92% |
Оценки используют local_pairs = L*(2*r+1) - r*(r+1) для L>r, при r=64. Они включают все плотные проекции и оба слоя головы решений полного внимания. Они исключают генерацию маски и трафик памяти. Выгода по времени выполнения может превышать или быть ниже доли FLOP, потому что внимание и GEMM имеют разную эффективность; установить это может только профилирование. При 8192 токенах компромисс был бы иным, но поставляемые агенты ограничивают входы 512 или 1024, поэтому заявление о 8192 токенах потребовало бы отдельно поддерживаемой нагрузки.
Fusion за пределами компиляции
Установленный точный nn.gelu уже декорирован бесформенной компиляцией, а nn.Linear уже использует учитывающий смещение addmm, когда это уместно. Компиляция всего блока может всё же слить GELU с его умножением на вентиль, остаточными сложениями, приведениями, масками и небольшими операциями признаков оценок. Исследуйте скомпилированный граф ядра, прежде чем реализовывать эквивалентное пользовательское ядро.
Если трафик активаций остаётся значительным, сделайте прототип точной fusion GELU-и-вентиль или остатка-и-LayerNorm. Сохраните текущий точный GELU на основе erf; аппроксимация tanh или sigmoid меняет модель и требует отдельных измерений качества. Исследуйте фактические strides и копии Q/K/V, прежде чем добавлять преобразования раскладки: реализация полного внимания MLX принимает непрерывный размер головы с иными striding и записывает раскладку вывода, удобную для объединения голов. Безусловная непрерывная копия может добавить работы.
Softmax маркеров, сортировка top-two, энтропия, небольшая голова действий и форматирование результата NumPy — законные будущие цели только при наличии измерений. Позиций вариантов мало по сравнению с сотнями операций трансформера полной ширины, поэтому оптимизировать их первыми вряд ли позволит обратиться к доминирующему пути.
Защищайте смысл бенчмарка, стремясь к агрессивным выигрышам
Текущий генератор нагрузки циклически перебирает три определения вопросов, чтобы построить 5, 10 или 50 вопросов. В этих пакетах не более трёх уникальных входов модели. Точная дедупликация по вызову может избежать избыточного вывода в реальном приложении, но она непропорционально улучшила бы эти фикстуры. Держите эту функцию отдельно от оптимизации ядер и сообщайте questions, unique_forward_inputs, попадания в кэш и фактически вычисленные токены. Восстанавливайте каждый исходный ответ, используя его собственные метки, упорядоченные критерии и метаданные калибровки. Сохраните набор с 50 действительно разными вопросами и набор с намеренно продублированными входами.
Не используйте кросс-вызовное кэширование результатов для первичного бенчмарка вывода: он повторно вызывает ровно один и тот же запрос. Сообщайте о любом эксперименте с кэшем как о таковом. Бенчмарки кэша токенизатора должны включать как сценарий с повторяющейся рубрикой, так и сценарий со свежим входом.
Для каждого кандидата используйте этот дизайн эксперимента:
- Держите цель постоянной. Записывайте ревизию/хеш источника, ревизию модели, dtype, флаги компиляции, выбранные квантованные модули, ID токенов или их хеш, форму, число маркеров, политику пакетирования, прогрев, синхронизацию и устройство. Существующий
input_sha256хеширует состояние/вопросы, а не фактические тензоры токенов; он не устанавливает идентичность тензоров между токенизаторами. Перезапускайте базовую линию с той же финальной ревизии источника, потому что исторические хеши источника в JSON различаются. - Разделяйте классы нагрузки. Используйте фиксированные формы для контролируемых сравнений ядер; реальные переменные длины для планирования и компиляции; разные вопросы для пропускной способности; повторяющиеся рубрики для законного кэширования CPU; и намеренно продублированные вопросы для дедупликации. Включайте длинные хвосты и разное число вариантов. Держите исходный текст и усечение идентичными внутри каждого сравнения бэкендов.
- Измеряйте холодное и тёплое поведение. Записывайте загрузку модели и первую компиляцию отдельно. Измеряйте подготовку, построение графа, синхронизированный вычисленный прямой проход, преобразование вывода и сквозную задержку, не считая независимые медианы аддитивными. Профилируйте ядра в отдельном запуске, потому что трассировка может возмущать задержку.
- Меняйте по одной оптимизации за раз. Отбирайте с существующим числом итераций, затем повторяйте финалистов в чередующихся блоках baseline/кандидат и собирайте достаточно выборок для правдоподобного p95, например не менее 200 измеренных запросов на нагрузку. Используйте один активный GPU-бенчмарк, согласованные условия питания/тепла и сохраняйте сырые выборки. Требуйте улучшение больше измеренной вариативности запусков.
- Проверяйте корректность и стабильность. Сравнивайте с MLX при том же dtype и существующим эталоном FP32; обеспечивайте идентичность токенов для изменений планирования и подготовки CPU. Проверяйте длины вокруг 64, 128 и границ бакетов; пакеты вокруг пределов фрагментов; один и много вариантов; многоязычные входы; дополнение; и изменения формы после компиляции. Повторяйте запросы с меняющейся формой, следите за активной памятью и памятью кэша после прогрева и проверяйте конечные детерминированные результаты в каждой конфигурации.
- Применяйте более строгие ворота к приближённым изменениям. Квантование, аппроксимация активации, отсечение токенов, ранние выходы и дистилляция требуют отложенных результатов по задаче и калибровке сверх небольшой регрессионной фикстуры. Сохраняйте отдельную идентичность модели и метку бенчмарка при изменении обученного поведения. Более быстрый многоязычный чекпойнт или дистиллированная модель — это другая модель, а не ускорение идентичного чекпойнта Laya.
Для любого измеренного горячего места, занимающего долю f сквозного времени и ускоряемого в s раз, используйте границу Амдала 1 / (1 - f + f/s) для оценки ожидаемого общего влияния. Используйте измеренную долю времени для f; приведённые выше арифметические доли не являются её заменой. Следующее конкретное инженерное решение должно следовать за абляцией компиляции/пакетирования и профилем ядер для короткого и длинного случаев, а не за непроверенным множителем.
Заметки о документации и воспроизводимости
Поиск в документации использовал требуемый рабочий процесс Context7: одно разрешение library MLX, затем отдельные запросы к официальной документации по компиляции и быстрому вниманию, используя /websites/ml-explore_github_io_mlx_build_html (всего три команды). Установленные .pyi и исходники Python были изучены для проверки поведения MLX 0.32.2, включая force_fused, API квантования, скомпилированный GELU и быстрый LayerNorm. Привязанные к версии исходники апстрима на C++/Metal были прочитаны для деталей диспетчеризации и цикла тайлов. Ни одна библиотека не была обновлена для этого исследования.
Два базовых файла FP16, использованные для подробных наблюдений, имели на момент проверки следующие дайджесты SHA-256:
laya-mlx-float16.json
63146dd664d039dde1a728b17aad896e491bd01ea36ea0786953691180e55b09
laya-multilingual-mlx-float16.json
9af74bd5a11e4edc15e6a8c9dc929a7b9fd2d19cb06f348cd0e04a076f912473
Основной процесс бенчмарка всё ещё производил дополнительные артефакты во время этого исследования. Таблица намеренно ссылается на полные базовые файлы, уже доступные на момент обзора, и не делает заявлений о неизмеренных реализациях-кандидатах.