Производительность MoE на RTX 5090 проседает в полтора раза, и дело вовсе не в роутинге
Пожалуй, начну с пары метрик, которые меня озадачили.
RTX 5090, неизменный билд llama.cpp (b10326, коммит 3653e6d6d), единственный драйвер, квантование Q4_0, размер батча — 1. Плотная модель Qwen3.5-9B задействует 72% пропускной способности памяти видеокарты. В то же время MoE-модель Qwen3.5-35B-A3B на том же железе утилизирует лишь 41%.
Сразу расставим точки над i, чтобы избежать поспешных выводов: архитектура MoE при этом абсолютный лидер по скорости — 317 токенов в секунду против 240 у плотной. Тем не менее, она недобирает примерно полтора раза от собственного потенциала, и этот дефицит поддается точному измерению и анализу.
Маршрутизация экспертов тут практически ни при чем (я проверил это отдельно).
Коротко о результатах. Измерено: пропускная способность памяти, зависимость скорости от объема считываемых данных для реального ядра ggml, детальный профиль всех матвеков по трассе, а также накладные расходы серверной обвязки. Спрогнозировано и подтверждено: производительность альтернативной модели (погрешность 3.7%) и скорость генерации на четырех различных глубинах контекста (в пределах 2.4%). Смоделировано, но пока не протестировано в бою: прирост от укрупнения ядер в диапазоне от 1.38 до 1.54 раза.
Далее по тексту под «полосой» понимается пропускная способность памяти (ПСП), под «матвеком» — матрично-векторное умножение (mul_mat_vec), отвечающее за декодирование при батче 1, а GDN обозначает Gated DeltaNet — рекуррентные слои, на которые приходится около трех четвертей обеих архитектур.
Стенд
|
|
|
|---|---|
|
карта |
RTX 5090, 32 ГБ GDDR7, sm_120, 170 мультипроцессоров |
|
драйвер |
580.159.03 |
|
llama.cpp |
b10326, коммит |
|
квант |
Q4_0 в терминологии llama.cpp, де-факто гибрид: префикс q6_K, часть тензоров q8_0 и q5_0, треть матвеков в f32 |
|
полоса паспортная |
1792 ГБ/с |
|
полоса измеренная |
1671.0 ГБ/с (93% от заявленной) |
|
пик кривой ядра ggml |
1683 ГБ/с |
|
пик кривой torch-редукции |
1638 ГБ/с |
Я замерял ПСП самостоятельно, не полагаясь на сухие спецификации. Паспортных значений достичь невозможно, и опора на них гарантированно исказит все дальнейшие расчеты. Последовательное чтение буфера объемом 1.5 ГиБ продемонстрировало результат в 1671.0 ГБ/с при разбросе всего в 0.18% по итогам шестидесяти итераций. Три различных ядра редукции (sum для f32, max для f32, sum для f16) сошлись с погрешностью в пределах 0.34%, подтверждая, что тестируется именно подсистема памяти, а не особенности конкретной функции.
Замеры производительности выполнялись по пять повторов на каждую точку:
llama-bench -m .gguf -p 0 -n 512 -d 0 -r 5 \
-ngl 99 -sm none -fa 1 -ctk f16 -ctv f16 -t 16
Все базовые метрики справедливы исключительно для пустого контекста (-d 0). Это принципиальный момент: количество байт на токен варьируется в зависимости от глубины, и приведенная ниже таблица актуальна только для нулевой точки. Я также профилировал матрицу на значениях 4096, 16384 и 32768; при активированном FlashAttention (fa=1) и KV-кэше в формате f16 падение производительности на 32K составляет 14.5% — с 317.45 до 271.53 токенов в секунду.
Объем данных на один токен вычисляется на основе метаданных GGUF с учетом реальных типов тензоров, а не номинальной разрядности кванта:
|
|
MoE 35B-A3B |
плотная 9B |
|---|---|---|
|
слоев |
41 (11 внимания, 30 GDN) |
33 (9 внимания, 24 GDN) |
|
эксперты |
256 всего, на токен обрабатывается 8 маршрутизируемых плюс 1 общий |
нет |
|
веса на токен |
2.007 ГБ |
4.900 ГБ |
|
состояние GDN (чтение/запись) |
0.126 ГБ |
0.101 ГБ |
|
всего на токен |
2.133 ГБ |
5.000 ГБ |
|
замер, т/с |
317.45 (разброс 0.74%) |
239.62 ± 0.86 |
Эмбеддинги намеренно исключены из расчетов: token_embd.weight весит 286 МБ, но для каждого токена из него считывается лишь одна строка размером 1.1 КиБ. Выходная голова output.weight, напротив, считывается полностью на каждом шаге генерации и учтена в бюджете. Попытка учесть эмбеддинг целиком раздула бы показатель весов на 14%, что превысило бы многие исследуемые далее эффекты.
Исходя из этого рассчитывается реальная утилизация. MoE: 2.133 ГБ обрабатываются за 3.150 мс, что эквивалентно 677 ГБ/с (или 41% от эталонных 1671). Плотная модель: 5.000 ГБ за 4.173 мс, что дает 1198 ГБ/с (72%).
Три гипотезы, которые не подтвердились
Прежде чем искать нетривиальные причины, я проверил очевидные предположения.
Первая догадка касалась графов CUDA: у MoE топология тензоров при динамической маршрутизации меняется от токена к токену, из-за чего кэширование графа могло давать сбой. Однако трассировка в nsys зафиксировала 127 запусков графа на 128 токенов декодирования, а llama.cpp не выдала ни единого предупреждения об отмнеке графов. Механизм успешно работает на обеих моделях.
Второе подозрение пало на саму логику маршрутизации. Ядра topk_moe в совокупности с выборкой строк отнимают 7.0% процессорного времени GPU, в то время как на долю матвеков приходится 51%. Маршрутизация обходится не бесплатно, но объяснить столь существенный разрыв она не может.
Третье: простаивание графического чипа между вызовами. Утилита nvidia-smi dmon во время «чистого» прогона без активного профайлера зафиксировала загрузку потоковых мультипроцессоров (SM) на уровне 97%. Устройство практически не простаивает — оно просто выполняет работу медленнее, чем позволяет пропускная способность памяти.
Именно этот факт стал ключом к разгадке. Если GPU загружен на 97%, а ПСП выбрана всего на 41%, это значит, что ядра выполняются, но обращаются к памяти неэффективно.
Ядру необходимо запрашивать достаточное количество байт
Логика проста: пока объем считываемых данных одним ядром мал, оно не успевает загрузить контроллер памяти запросами и выйти на пиковую пропускную способность.
Проверку следует проводить на реальном исполняемом ядре и с холодным кэшем. Я написал утилиту на ggml, которая формирует N уникальных матриц общим объемом 4 ГиБ и последовательно вызывает для них тот же самый метод mul_mat_vec_q, что задействован в инференсе моделей. Рабочий набор данных в сорок раз превышает размер кэша L2 (96 МиБ), поэтому к моменту повторного обращения матрица гарантированно вымывается из кэша. Это в точности воссоздает паттерн декодирования: каждый вес считывается ровно один раз на токен без повторного использования.
|
МБ на ядро |
1.0 |
2.1 |
4.2 |
8.4 |
16.8 |
33.6 |
67 |
134 |
537 |
|---|---|---|---|---|---|---|---|---|---|
|
ГБ/с |
381 |
617 |
899 |
1171 |
1390 |
1528 |
1608 |
1646 |
1683 |
|
% от пика |
23% |
37% |
53% |
70% |
83% |
91% |
96% |
98% |
100% |
Пиковое значение кривой составило 1683 ГБ/с против 1671 ГБ/с, независимо зафиксированных базовым тестом. Расхождение составляет всего 0.7%. Разница в трех значениях пропускной способности, указанных в шапке, обусловлена спецификой использованных ядер, что наглядно демонстрирует некорректность понятия «память карты» вне контекста конкретной нагрузки.
Какая доля из этого — накладные расходы на запуск
Резонный вопрос: не фиксирую ли я в данной кривой банальные издержки на инициализацию ядер? Для блока размером 1 МБ время жизни ядра составляет 2.75 мкс, в то время как оверхед узла CUDA-графа (измеренный отдельно) равен 0.94 мкс. Это примерно треть от общего времени.
Исключим эту константу из каждого замера:
|
МБ на ядро |
1.0 |
4.2 |
16.8 |
33.6 |
134 |
537 |
|---|---|---|---|---|---|---|
|
как измерено |
23% |
53% |
83% |
91% |
98% |
100% |
|
без учета запуска |
33% |
67% |
89% |
95% |
99% |
100% |
Кривая не выпрямляется линейно. Даже при нулевой стоимости инициализации чтение порций по 4.2 МБ задействует лишь две трети пропускной способности. Порог выхода на 90-процентную эффективность смещается с 34 МБ примерно к 18 МБ. Иными словами, накладные расходы на запуск объясняют лишь треть дефицита на малых объемах, тогда как остальные две трети приходятся непосредственно на специфику разгона памяти.
В дальнейшем я опираюсь на эмпирическую кривую «как есть», поскольку в реальном движке издержки на запуск никуда не исчезают. Тем не менее, природа явления комплексная, и сводить её исключительно к оверхеду запусков нельзя.
Позиционирование моделей на этой кривой
Количество матвеков на токен получено из той же трассировки nsys:
|
|
матвеков на токен |
байт на матвек |
доля полосы |
|---|---|---|---|
|
MoE 35B-A3B |
437.8 |
4.6 МБ |
55% |
|
плотная 9B |
220.4 |
22.2 МБ |
86% |
В этом и заключается корень проблемы. Однако усредненные цифры скрывают детали, поэтому ниже приведена пошаговая разбивка всех 437.8 операций умножения по сетке запусков напрямую из трассировщика:
|
тип весов |
grid.x |
запусков на токен |
на слой |
медиана, мкс |
доля времени |
|---|---|---|---|---|---|
|
q4_0 |
512 |
76.2 |
1.86 |
9.22 |
24.0% |
|
q4_0 |
8192 |
36.6 |
0.89 |
9.54 |
14.8% |
|
q6_K |
248320 |
1.0 |
голова |
256.35 |
11.2% |
|
q4_0 |
2048 |
26.4 |
0.64 |
5.02 |
8.0% |
|
q8_0 |
512 |
60.9 |
1.49 |
2.72 |
7.7% |
|
f32 |
256 |
40.6 |
0.99 |
4.38 |
7.7% |
|
q4_0 |
4096 |
30.5 |
0.74 |
5.15 |
6.7% |
|
f32 |
32 |
60.9 |
1.49 |
1.98 |
5.0% |
|
q5_0 |
512 |
40.6 |
0.99 |
2.50 |
4.6% |
|
q8_0 |
2048 |
14.2 |
0.35 |
7.42 |
4.5% |
|
f32 |
1 |
40.6 |
0.99 |
1.44 |
2.6% |
|
q6_K |
8192 |
4.1 |
0.10 |
11.87 |
2.1% |
|
q4_1 |
512 |
5.1 |
0.12 |
5.09 |
1.1% |
|
итого |
|
437.8 |
|
|
100% |
Доля времени здесь рассчитывается как сумма реальных длительностей, а не через медиану на число вызовов, поэтому простое перемножение колонок не сойдется напрямую.
Таблица вскрывает два неочевидных факта.
Экспертные блоки обрабатываются пакетами, а не по отдельности. На q4_0 приходится в сумме 4.1 запуска на слой вместо 24, которые наблюдались бы при циклическом обходе восьми экспертов с тремя проекциями. Сигнатура mul_mat_vec_q содержит указатель на массив индексов, реализуя косвенную адресацию. Следовательно, дело вовсе не в выделении персонального ядра под каждого эксперта. Проблема в том, что восемь экспертов из 256 в совокупности дают слишком малый объем данных: даже будучи объединенными в единый вызов, они исчисляются лишь мегабайтами, из-за чего ядро неизбежно оказывается ниже порогового значения.
Выходная голова исполняется единичным запуском на токен и считывает 417 МБ. Она утилизирует 1601 ГБ/с, что соответствует 96% пропускной способности. Остальные 436.8 вызовов в среднем оперируют порциями по 3.64 МБ и демонстрируют 767 ГБ/с (46% от пика). Синтетическая кривая для объема 3.64 МБ прогнозирует 841 ГБ/с (50%), а несколько заниженные показатели в трассировке обусловлены накладнымими расходами самого профайлера. Иными словами, реальные ядра (включая экспертные с косвенной адресацией) ложатся на синтетическую кривую с точностью до нескольких процентов. В рамках одной модели уживаются одно ядро на предельной скорости и сотни ядер, работающих на половинной мощности.
Здесь может возникнуть возражение: у экспертных ядер параметр grid.x равен всего 512 блокам на 170 мультипроцессоров, то есть видеокарта недозагружена, и дело якобы в этом, а не в пропускной способности. Однако это опровергается анализом длины строк: при K = 1024, 4096 и 16384 количество блоков изменяется в шестнадцать раз при неизменном объеме считывания в 4.2 МБ, но пропускная способность стабильно держится в пределах 4%, выдавая 869, 845 и 836 ГБ/с. Варьирование плотности блоков GPU в шестнадцать раз не оказало влияния на результат.
Плотная модель считывает свои матрицы целиком, преодолевает пороговое значение и эффективно утилизирует полосу памяти.
Валидация: прогнозирование другой модели
Все описанные закономерности выведены на базе MoE. Чтобы убедиться в универсальности модели, а не в подгонке под ответ, я взял полученную для MoE кривую и спрогнозировал производительность плотной Qwen3.5-9B, которая имеет совершенно иную архитектуру: 33 слоя вместо 41 и полное отсутствие экспертов.
Прогноз был зафиксирован до проведения замеров:
матвеков на токен 220.4 (из профиля)
байт на матвек 22.2 МБ
доля полосы по кривой 1445 ГБ/с (86% от пика)
время матвеков 3.390 мс
остальные 638 ядер по 0.88 мкс 0.561 мс
состояние GDN 0.072 мс
итого 4.023 мс (эквивалентно 248.6 т/с)
Фактический замер: 4.173 мс, то есть 239.62 ± 0.86 т/с. Погрешность составила 3.7%, причем прогноз оказался чуть оптимистичнее реальности.
Оставшиеся три процента отклонения, вероятнее всего, заложены в оценке мелких ядер, которая снималась на MoE, тогда как набор ядер у плотной модели отличается.
Вторая проверка по другой оси
Перенос между архитектурами — лишь одна плоскость для проверки. Существует и вторая, абсолютно бесплатная: глубина контекста. Количество матвеков от неё не зависит, меняется лишь объем KV-кэша, который легко рассчитывается из метаданных (22528 байт на токен контекста для одиннадцати слоев внимания). Это означает, что модель обязана безошибочно предсказать поведение кривой при изменении глубины без дополнительных калибровок:
|
глубина |
KV на токен |
прогноз |
факт |
погрешность |
|---|---|---|---|---|
|
0 |
5.8 МБ |
317.1 |
317.45 |
−0.1% |
|
4096 |
98.0 МБ |
311.6 |
309.88 |
+0.6% |
|
16384 |
374.9 МБ |
296.3 |
289.52 |
+2.4% |
|
32768 |
744.0 МБ |
278.1 |
271.53 |
+2.4% |
Абсолютная скорость спрогнозирована с точностью до 2.4% во всем диапазоне глубин. Само падение производительности оценено в 12.3% против фактических 14.5%, то есть модель недооценивает влияние глубины примерно на пятую часть эффекта — очевидно, не учитывая какие-то нюансы KV-тракта. Тем не менее, эта проверка весомее первой, поскольку там сопоставлялись две родственные модели, а здесь проверяется принципиально иная физическая величина.
Порог насыщения растет пропорционально ширине чипа
Этот блок наименее фундаментален, и я предупрежу об этом заранее.
У меня нет сборки бенчмарков ggml под мобильную RTX 4060, поэтому напрямую сопоставить карты с помощью штатного ядра не удалось. Обе колонки ниже получены с помощью иного, более простого синтетического ядра (редукция через PyTorch на тех же холодных срезах). Они сопоставимы между собой, но не бьются с абсолютными цифрами из предыдущего раздела:
|
|
RTX 4060 |
RTX 5090 |
соотношение |
|---|---|---|---|
|
мультипроцессоров |
24 |
170 |
7.1× |
|
измеренная ПСП |
249 ГБ/с |
1638 ГБ/с |
6.6× |
|
порог 80% полосы |
8.4 МБ |
67.1 МБ |
8.0× |
|
порог 90% полосы |
16.8 МБ |
134.2 МБ |
8.0× |
Есть две причины относиться к этим данным с осторожностью. Шаг сканирования составлял коэффициент 2, поэтому порог определен с точностью до множителя, и уверенно отделить масштабирование по числу вычислительных блоков (7.1×) от роста пропускной способности (6.6×) на таких выборках нельзя. Кроме того, абсолютное значение порога зависит от специфики ядра: редукция в PyTorch на 5090 выходит на 90% полосы при 134 МБ, в то время как нативному матвеку из ggml требуется лишь 34 МБ (четверократно меньше). Следовательно, порог — это не константа подсистемы памяти, а характеристика связки «видеокарта плюс исполняемое ядро».
Главный вывод здесь носит качественный характер. Чем мощнее и шире графический чип, тем крупнее должны быть вычислительные блоки. Код, оптимизированный под скромную 4060, на 5090 окажется слишком мелкозернистым. Для точных цифр требуется детальное сканирование с мелким шагом на обеих картах с использованием единого ядра.
Как измерять точно не стоит
Наступив на все возможные грабли, я выделил четыре ключевые ловушки.
Кэш L2 нивелирует наивный перебор размеров
Первое, что приходит на ум: запустить последовательность чтений размером от 64 КБ до 64 МБ и зафиксировать момент выхода на плато. Емкость L2-кэша у 5090 составляет 96 МиБ, а у 4060 — 32 МиБ. Весь диапазон целиком умещается в кэше, поэтому плато достигается на первых же килобайтах. На 64 МиБ я зафиксировал абсурдные 4285 ГБ/с при физическом пределе памяти в 1683 ГБ/с.
Проблема решается организацией циклических проходов: каждый запуск должен обращаться к новому срезу крупного пула памяти, гарантируя, что к моменту возврата старые данные успели вымыться из кэша.
Инструмент test-backend-ops perf из состава llama.cpp предназначен для тестирования нативных ядер ggml на заданных формах. Казалось бы, идеальное решение. Однако для q4_0 при m=4096, k=14336, n=1 он выдал 9.04 мкс на вызов. Размер матрицы составляет 33 МБ, что дает расчетную пропускную способность в 3654 ГБ/с — вдвое выше физического предела.
Фокус кроется в логике бенчмарка: в режиме perf он многократно дублирует один и тот же узел графа. В результате матрица мертво оседает в кэше L2 и в дальнейшем считывается исключительно оттуда. Любой бенчмарк, допускающий переиспользование тензоров, измеряет исключительно производительность кэша, тогда как при генерации токенов каждый вес считывается ровно один раз.
Для оценки накладных расходов на запуск я сначала задействовал PyTorch: запускал поток из N тривиальных ядер и делил суммарное время на N. Получалось от 9 до 18 микросекунд на вызов, причем величина почти не зависела от N.
Эти цифры оказались мусором. Как только процессор передавал очередь, видеокарта завершала работу за 0.02 микросекунды. Иными словами, чип простаивал все остальное время, а я измерял задержки питоновского диспетчера. Один вызов функций PyTorch из интерпретатора Python обходится в единицы и десятки микросекунд, на порядок превосходя реальное время старта самого ядра.
Достоверные результаты дает исключительно профилирование через захваченные CUDA-графы: в цикле replay() накладные расходы Python отсутствуют. В этом режиме накладные составили 0.94 микросекунды на узел.
Ключ nsys profile --cuda-graph-trace=node необходим, чтобы разглядеть отдельные ядра внутри графа. Без него весь граф схлопывается в единую строку, и вопрос о количестве ядер на токен повисает в воздухе.
Однако подобная трассировка искажает тайминги, причем неравномерно. Суммарная длительность всех ядер составила 4.558 мс на токен против 3.150 мс в чистом прогоне. Поскольку ядра выполняются последовательно в рамках одного потока, их сумма не может превышать общее время токена, что указывает на раздувание метрик.
Искажение носит нелинейный характер. Матвеки со средним временем жизни около 5 микросекунд переоцениваются примерно в 1.1 раза. Короткие же ядра длительностью порядка микросекунды уходят в двукратный отрыв. Следовательно, относительные доли времени из таких трасс брать можно, а абсолютные значения — нет.
На RTX 4060 аналогичная трассировка показала 22.271 мс на токен против 22.297 мс в чистом замене, то есть погрешность практически нулевая. Поскольку тамошние ядра выполняются примерно в тридцать раз дольше, постоянная погрешность профилировщика на их фоне растворяется.
Что в итоге достается пользователю
Это наиболее прикладная часть исследования, и она выходит за рамки чистого железа.
Утилита llama-bench оценивает исключительно движок. На практике пользователи взаимодействуют с llama-server, который поверх базового инференса вешает сэмплирование, детокенизацию, управление слотами и HTTP-транспорт. Тот же стенд, та же модель, генерация 512 токенов, три повтора на конфигурацию:
|
|
мс на токен |
накопительный итог |
|---|---|---|
|
движок |
3.150 |
3.150 |
|
цикл сервера с жадным сэмплером |
+0.279 |
3.429 |
|
реальный сэмплер (top_k 40, top_p 0.9, min_p, repeat_penalty) |
+0.723 |
4.152 |
|
HTTP-обвязка и префилл на запрос |
+0.291 |
4.443 |
Вместо 317 токенов в секунду, заявленных бенчмарком, пользователь получает 225 т/с. Падение составляет 29%.
Наименее эффективным элементом здесь выступает сэмплир: 0.723 мс на токен забрал на себя почти четверть времени работы всего движка. Это чисто процессорная нагрузка на словарь размером в четверть миллиона токенов, не имеющая никакого отношения к GPU. Потоковая передача ответов (стриминг), вопреки опасениям, не стоит ничего: 225.8 т/с без стриминга против 225.1 с ним.
Пара слов о драйверах. Переход с версии 570.195.03 на 580.159.03 дает прирост скорости декодирования в 8.0% по медиане из двенадцати точек, а на префилле — 5.8%. Перплексия при этом идентична до четвертого знака (5.7732 ± 0.0434 на обоих драйверах), количество ядер на токен также совпадает. Механизм ускорения понятен по кривой: для ядра размером 1 МБ новый драйвер обеспечивает прибавку в 8.6%, для 33.6 МБ — уже 2.0%, а на 537 МБ она падает до 0.3%. Драйвер оптимизировал процедуру старта ядер, а не их выполнение. Это подтверждается и стоимостью графов: принудительное отключение GGML_CUDA_DISABLE_GRAPHS=1 на старом драйвере приводит к просадке на 26.8%, тогда как новый теряет лишь 14.0%.
Что из этого следует
Далее идут прогнозируемые, а не измеренные величины. Сделаю оговорку: собственные модифицированные ядра я не писал, а статус аналогичных наработок в экосистеме (например, в ik_llama.cpp или майнлайне) не проверял.
Если сократить количество матвеков K по сравнению с текущими 438, каждый из них будет считывать объем 2.007 ГБ / K, а итоговая доля пропускной способности легко считывается с кривой. Здесь есть тонкость, способная изменить результат вдвое. Ядро quantize_q8_1 вызывается ровно один раз на квантованный матвек: 438 матвеков минус 142 в f32 дают 296 процедур квантования, что в точности совпадает с замерами. Восемь экспертов одного слоя обращаются к единому вектору активаций, поэтому их слияние оставляет лишь одно квантование вместо восьми. Однако проекции разных слоев считывают независимые активации, и там квантование не сокращается.
Отсюда вырисовываются два сценария:
|
матвеков на токен |
на слой |
МБ на ядро |
% полосы |
т/с (квантование сокращается) |
т/с (квантование сохраняется) |
|---|---|---|---|---|---|
|
438 |
10.7 |
4.6 |
55% |
317 |
317 |
|
219 |
5.3 |
9.2 |
71% |
392 (1.24× |
373 (1.18× |
|
109 |
2.7 |
18.3 |
84% |
447 (1.41× |
411 (1.30× |
|
41 |
1.0 |
49.0 |
93% |
489 (1.54× |
438 (1.38× |
Колонка с долей полосы взята напрямую из эмпирической кривой. Значения скорости в двух крайних правых столбцах получены экстраполяцией: помимо матвеков, на токен приходится 1422 других ядра совокупной стоимостью около 1.0 мс. Я оценил их вклад в 0.88 микросекунды на единицу, просто разделив остаток времени на их количество. Оценка откалибрована по единственной точке. Она неплохо коррелирует с независимо измеренной стоимостью узла графа (0.94 мкс), но в области малых N не проверялась.
Таблица демонстрирует неприятный нюанс. Укрупнение одних лишь матвеков упирается в потолок прироста 1.4–1.5 раза даже при сведении к одному матвеку на слой, поскольку накладные расходы на мелкие ядра никуда не исчезают. Чтобы пробить этот барьер, придется оптимизировать и сопутствующую мелочь.
Есть и еще один неожиданный момент. Серверная обвязка стоимостью 1.29 мс на токен от фузии ядер никуда не денется. В результате ускорение движка в 1.54 раза дойдет до пользователя лишь как 1.33×, тогда как идеальный рантайм, упершийся в предел памяти, дал бы максимум 1.73×. При этом сэмплирование обходится в 0.72 мс и выполняется силами CPU без единой строчки на CUDA. Потери здесь сопоставимы со всем потенциальным выигрышем от фузии, а трудозатраты на оптимизацию несопоставимо ниже. Начинать стоило бы именно с этого.
Оговорки и допущения
Усредненный размер вместо точной суммы по ядрам. Я разделил общий объем байт на количество матвеков и сопоставил с кривой единую долю ПСП, хотя корректнее суммировать метрики для каждого ядра: кривая нелинейна, а распределение размеров бимодально. Разделение на две группы и пересчет по кривой дали 2.139 мс против 2.150 мс при усреднении (разница 0.5%). Погрешность есть и смещена в предсказуемую сторону, но на общую картину она не влияет. Не стоит путать это с трассировочными цифрами (1601 и 767 ГБ/с), которые занижены из-за оверхеда профайлера.
Треть матвеков оценивалась по чужой кривой. Из 437.8 матвеков 142.2 выполняются в f32, в то время как кривая снята на q4_0. Для f16 на порциях в 4.2 МБ отклонение достигает 21%, так что здесь оценка также смещена, хотя в категорию «55% пропускной способности» это ложится без поправок.
Фактическая ПСП не замерялась аппаратными счетчиками. Показатель получен делением объема переданных данных на время, а не через метрику dram__throughput в Nsight Compute. Полноценное профилирование через ncu на арендованных мощностях оказалось заблокировано: контейнер запущен без флага CAP_SYS_ADMIN, и счетчики производительности скрыты даже от суперпользователя. Снять ограничения изнутри нельзя, требуется выделенный под с соответствующими привилегиями.
Паттерн MoE исследован не до конца. Синтетический бенчмарк прогоняет стандартные матвеки по разным матрицам, тогда как реальная маршрутизация в llama.cpp может задействовать механизмы с косвенной адресацией строк. Разница в паттернах доступа возможна, но я её не измерял.
Тестировался исключительно формат Q4_0. Кривые для q8_0, q6_K и f16 также снимались: они практически неразличимы между собой, за исключением того, что f16 на 4.2 МБ работает на 21% быстрее q4_0, и этот разрыв нивелируется к отметке в 537 МБ. Это означает, что распаковка является фиксированной надбавкой на ядро, а не потерей пропускной способности. K-квантования, используемые по умолчанию в большинстве сценариев, не проверялись.
Рассматривался только одиночный батч. Исследование изначально фокусировалось на режиме single stream, поэтому размеры батча 2, 4 и 8 не тестировались. Вероятно, это самый простой и перспективный тест из оставшихся: с ростом батча нагрузка на экспертов увеличивается, и утилизация памяти должна пойти вверх по той же кривой.
Альтернативные рантаймы не сопоставлялись. vLLM, SGLang и TensorRT-LLM на этом стенде не запускались. Тезис о «лежащих на столе полуторакратных резервах» проверен лишь на одном рантайме, и без кросс-фреймворковых тестов остается утверждением в адрес llama.cpp, а не железа в целом.
Исследованы лишь две модели из одного семейства — Qwen3.5 9B и 35B-A3B. Оборудования тоже два: RTX 5090 и мобильная RTX 4060.
Облачная инфраструктура вместо специализированной лаборатории. Все замеры для 5090 выполнены на арендованных инстансах. Три пода с разной конфигурацией процессоров продемонстрировали согласованные результаты, что добавляет уверенности, но полноценным лабораторным стендом облако не является.
Калибровочная кривая строится с помощью скрипта на ggml размером около ста строк. Его можно скомпилировать с любой версией llama.cpp и воспроизвести на собственном GPU за вечер. Исходный код и сырые датасеты будут опубликованы отдельно.
ИИ принесет SpaceX 99% стоимости через 5 лет: искусственный интеллект почти обогнал ракеты и спутники
Новый мини-ПК Minisforum M1 с Core i9-13900HX и богатым набором портов оценен в $400
Как прекратить поиски места для плат и приступить к тестированию
Наследие Nokia: HMD выпустит около 10 кнопочных телефонов до конца 2026 года
«МегаФон» улучшил мобильную связь в аэропортах и на вокзалах по всей стране
Сбер презентовал «ГигаАгента» — автономного универсального ИИ-агента в ответ на OpenClaw
Раскрыты все характеристики Honor Robot Phone до официального старта продаж
Анонсирован компактный игровой ПК Piston V с круглым тачскрином и двумя ОС