Mac Pro 2013 запускає модель 35B: читає 66 ток/с (llama-bench pp512), пише ~11 ток/с

Зміст

Залізо: Mac Pro 6,1 · 2× AMD FirePro D700 (Tahiti, GCN1, 6 ГБ GDDR5, 264 ГБ/с кожна за паспортом AMD) · Xeon E5-1680 v2 (без AVX2) · 64 ГБ ОЗУ · macOS 12.7.6 — остання версія, яку ця машина офіційно підтримує. Ціна: цей екземпляр куплено вживаним за 1300 зл. Польська стартова ціна моделі (число третьої сторони) починалася з 13 299 зл за базову конфігурацію, а в базовій стояли найслабші відеокарти; пара D700 була платним апгрейдом при замовленні на дорожчій конфігурації, тож нова ця машина коштувала істотно більше за базу. Стартової ціни саме цієї конфігурації ми не встановили й не наводимо. Правило: кожне наше число виміряне на цій машині, і для наших чисел є журнали вимірювань; ці журнали лежать у репозиторії в logs/. Числа без журналу позначено в тексті як «не зажурнальовано». Числа третіх сторін (блог, Apple, issue, ціни) позначено як такі. Патчі, логи і скрипти публічні в репозиторії: https://github.com/SynenergyAI/mac-pro-2013-llama-metal.

1. Чий код ми міряли

Попередні версії цієї статті називали виміряне дерево «стоком» і «апстрімом llama.cpp». Воно не було ні тим, ні іншим. Це ai-janitor/llama-metal на коміті 72ba023: окремий репозиторій одного автора, не форк на GitHub, створений 16 лютого 2026 року, і на 72ba023 його історія була одним корінним комітом без батьків. Сам він описує себе як «Metal GPU backend for llama.cpp — AMD/Intel non-Apple-Silicon support». Коміту 72ba023 в ggml-org/llama.cpp немає. Ми жодного разу не перевірили, чий код міряємо.

Цей репозиторій переписав код Metal; серед змін, які перелічує його корінний коміт, — визначення ширини SIMD під час роботи для заліза з шириною 16, 32 і 64. В основному ggml-org/llama.cpp немає теки ggml-metal/matmul/ і немає chunks_per_thread у коді Metal. Тож чотири дефекти — баг редукції на хвилі 64, баг завантажувача q6_K, падіння векторного шляху з pp320 і помилка збірки через відсутній #include <string> — належать ai-janitor/llama-metal, а не llama.cpp. Далі «базовий стан» означає ai-janitor/llama-metal 72ba023 плюс лише те, що названо.

Віддаймо належне: ai-janitor/llama-metal — єдиний знайдений нами шлях, який узагалі змушує Metal рахувати на цих відеокартах. Усе про Metal у цій статті стоїть на ньому.

2. Основний llama.cpp на цьому залізі

Потім ми виміряли основний ggml-org/llama.cpp на коміті 7cf1c54. Походження: неглибокий клон, що містить лише цей коміт; його локальний хеш дерева, 860d4eb…, дорівнює хешу дерева, який API GitHub повертає для 7cf1c54 в ggml-org/llama.cpp; усі наші локальні зміни незакомічені й виключені.

Він збирається і бачить обидві карти. З Metal на macOS 12 збірка завершується з кодом 0, без доданого #include <string>. Він перелічує MTL0 і MTL1, «AMD Radeon HD - FirePro D700», по 6144 MiB, сімейство Common3, і повідомляє simdgroup reduction = false.

Множення матриць на цих картах він не виконує. test-backend-ops на карті 0: усі 1649 випадків MUL_MAT і всі 904 випадки MUL_MAT_ID — «not supported». Причина в коді: ggml-metal-device.m, рядки 1119–1120, вмикають SIMD-редукції лише для пристроїв Apple7 або Metal3, а рядки 1834–1841 дозволяють MUL_MAT лише з ними. Тож множення матриць на цих картах основний llama.cpp віддає процесору. Ми не знайшли жодного публічного issue, PR, обговорення чи документа, де б ця перевірка обговорювалася; єдине письмове окреслення меж — README, де Metal указано для Apple Silicon. Issue #19563 показує, що деякі дискретні відеокарти AMD повідомляють Metal3, проходять цю перевірку й видають неправильний вивід.

З -ngl 99 він падає: ggml-metal-device.m:2327: GGML_ASSERT(buf_src) у ggml_metal_buffer_set_tensor. Вимога Apple: makeBuffer(bytesNoCopy:…) потребує вирівняного по сторінці вказівника і довжини, що дає вирівняну по сторінці область. Тензори GGUF упаковані щільно, і на D700 виклик повертає nil; яку саме умову порушено, ми не встановлювали. Це падіння вже відоме. Issue #26949 від 12 серпня 2026 описує саме цей механізм на Intel Mac з дискретною відеокартою AMD і дає причину та виправлення у два рядки; його закрито ботом як застаріле (not planned) 10 жовтня 2026 — того самого дня, коли ми його відтворили. Ми знайшли його не першими; ми підтвердили його на ще одній машині.

Попередні спроби для цього класу заліза, жодну не влито: #15228 (відкрито 11 серпня 2025, закрито ботом як застаріле (not planned) 3 травня 2026), #19527 (PR, відкритий 11 лютого 2026 автором ai-janitor — тим самим, що й llama-metal; досі відкритий і не влитий), #19563 (відкрито 12 лютого 2026, закрито ботом як застаріле (not planned) 7 травня 2026). Це факти; жодних висновків про мотиви будь-кого ми не робимо.

3. Одна таблиця, одні умови

Це центральне порівняння статті. Усі рядки: Qwen2.5-Coder-7B-Instruct Q4_K_M, llama-bench -p 512 -n 128 -t 2 -fa 0 -r 3; рядки відеокарт — на двох картах, -sm layer -ts 1/1. Це рівно умови блогу, крім його налаштування контексту, якого llama-bench виразити не може.

Рядокчитання промпту pp512генерація tg128Правильність, той самий бінарник
процесор, основний 7cf1c54, -ngl 020,58 ток/с (20,60 / 20,53 / 20,58)3,42 ток/с (3,43 / 3,40 / 3,42)процесор
Metal, ai-janitor/llama-metal + наші виправлення52,36 ток/с (52,37 / 52,36 / 52,31)11,73 ток/с (11,75 / 11,73 / 11,66)941/941, 616/616
Vulkan/MoltenVK, незмінений основний 7cf1c5475,46 ток/с (74,97 / 75,78 / 75,46)14,61 ток/с (14,57 / 14,63 / 14,61)1017/1128, 817/921 — провали лише в i-квантах
Linux / Mesa RADV, блог третьої сторони, та сама машина—21,39 ток/сми не міряли

Застереження, і місце йому саме тут: -t 2 — налаштування блогу, і воно калічить процесор. У наших прогонах генерація Metal трималася біля 11–12 ток/с і з -t 8 на одній карті, і з -t 2 на двох картах; вплив потоків окремо ми не ізолювали. Тож відношення генерації «відеокарта проти процесора» в цій таблиці завищують виграш відеокарти. З однаковими потоками, одна карта, -t 8 з обох боків, Metal генерував 11,15 ток/с проти 9,89 ток/с у процесора (+12,7%); той самий прогін процесора дав pp512 20,46 ток/с. На macOS виграш Metal на генерації проти процесора з усіма потоками скромний; великий виграш — на читанні промпту. Прогону MoltenVK з -t 8 у нас немає, тож відношення MoltenVK до найкращого процесорного результату ми не наводимо.

Відношення генерації, чинні лише в межах цієї таблиці і лише при -t 2: Metal/процесор ×3,43 і MoltenVK/процесор ×4,27 — обидва завищені через скалічений процесор, див. вище; відношення відеокарти до відеокарти від цього не залежить: MoltenVK/Metal ×1,25. Відношення читання промпту, та сама таблиця: MoltenVK/Metal ×1,44; Metal/процесор ×2,54. Рядок Linux/RADV — лише контекст: інша ОС, інший драйвер і бінарник, не наш контрольований прогін. Рядок Metal — перший із двох повторів; другий дав 52,32 / 11,70 ток/с.

Збірка Vulkan — незмінений основний llama.cpp: хеш дерева 860d4eb, нуль наших змін у ggml-vulkan. Її правильність, та сама збірка, карта Vulkan0: MUL_MAT — пройдено 1017 з 1128, MUL_MAT_ID — пройдено 817 з 921. Усі провали — в i-квантах (iq2_xxs, iq2_xs, iq2_s, iq1_s, iq1_m, iq3_s, iq3_xxs); f16, f32, bf16, q4_0, q4_1, q4_K, q5_K, q6_K і q8_0 мають нуль провалів, тож рядок Q4_K_M коректний. Провали i-квантів ми наводимо як побачені; чи вони вже відомі в llama.cpp, ми не перевіряли.

Прочитання, а не вимірювання: при довгих входах і коротких виводах домінує вартість читання промпту, і саме там ці карти на macOS допомагають найбільше.

4. ai-janitor/llama-metal 72ba023 на macOS 12: помилка збірки і збій запуску

Не збирається. Чистий ai-janitor/llama-metal на коміті 72ba023 на macOS 12 не компілюється: ggml-backend.cpp:1183, «implicit instantiation of undefined template std::basic_string». Бракує одного #include <string>; основний 7cf1c54 збирається без нього (розділ 2). Різниця нашого дерева з чистим 72ba023 у цьому файлі — рівно цей один рядок. Усе, що нижче назване «базовий стан», — це 72ba023 плюс цей рядок; інакше міряти було б нічого.

Не запускається. Навіть зібраний, базовий стан падає на ggml-metal-context.m:415: GGML_ASSERT(buf_dst) failed уже під час llama-bench: Metal повертає nil там, де код чекає буфер. Запускати модель його змушує окрема наша зміна — запасний шлях без копіювання: якщо Metal вернув nil, буфер створюється і дані копіюються вручну. Правильності вона не змінює, але без неї ні базовий стан, ні «базовий стан + фікс хвилі 64» не доходять до першого токена. Це той самий клас проблеми, що й падіння buf_src основного llama.cpp з розділу 2.

Звідси: у базового стану ai-janitor/llama-metal на цьому залізі немає швидкості. Немає «було», з яким можна порівнятися, доки не додано цей запасний шлях. Він не повільний і не фальшиво швидкий — він не стартує.

Щойно додано запасний шлях без копіювання, усі стани векторного шляху показали ~9,95 ток/с на pp256 (серія по патчах у logs/bench_series.log: 9,95): базовий стан, базовий стан з фіксом хвилі 64 і наш повний стан з вимкненим тайлінгом (окремий прогін дав 9,94). ~9,95 — цифра векторного шляху, а не властивість якогось одного стану.

5. Драбина правильності

Кожен щабель зібрано з нуля з чистого чекауту ai-janitor/llama-metal 72ba023 плюс лише названа зміна. Виняток — щабель 929/941: його зібрано з цілих файлів, і варіант хвилі 64 у ньому — з патча 0006, а не 0003 (див. UNVERIFIED.md у репозиторії). Metal, карта 0, test-backend-ops test. Інструмент друкує «N/M tests passed»; попередні версії цієї статті читали перше число як провали.

ЩабельMUL_MATMUL_MAT_ID
базовий стан + запасний шлях без копіюванняпройдено 433 з 869, провалено 436пройдено 162 з 616, провалено 454
+ фікс хвилі 64пройдено 869/869пройдено 616/616
+ тайловий диспетчпройдено 869/869 (без змін)пройдено 616/616 (без змін)
+ наші 72 тестові випадки (m = 128)пройдено 929 з 941, 12 провалів — усі q6_Kпройдено 616/616
+ фікс вирівнювання q6_Kпройдено 941/941пройдено 616/616

Три речі з цієї таблиці.

По-перше, один фікс хвилі 64 закриває всі провали базового стану відразу — усі 436 провалів MUL_MAT і всі 454 провали MUL_MAT_ID зникають разом. Раніше ми наводили проміжний щабель 534/869; на свіжих збірках він не відтворився, тож ми його знімаємо й нічим не заміняємо.

По-друге, тайловий диспетч правильності не змінює взагалі. Це чисто швидкісна зміна. Старий текст давав інше враження — виправляємо: 869/869 до нього і 869/869 після.

По-третє, знаменники різні, і їх не можна порівнювати мовчки. 941 = 869 випадків MUL_MAT базового стану плюс 72 наші: по 12 на f16, q4_0, q8_0, q4_K, q5_K і q6_K, усі з m = 128 рядків. Тобто 433/869 і 941/941 — це різні набори тестів, і ставити їх в одне відношення не можна. Підрахунки MUL_MAT_ID наших випадків не містять.

Це не екзотичний випадок квантування. У базовому стані провалюються 192 з 262 випадків MUL_MAT для f16 і 143 з 198 для f32, а також кванти (провалено з усього: q4_0 14 з 43, q4_1 16 з 43, q8_0 16 з 43, q4_K 16 з 41, q5_K і q6_K по 4 з 11, iq4_xs і q3_K по 11 з 11). Тобто ai-janitor/llama-metal на 72ba023 на цьому класі відеокарт неправильно множить звичайні матриці f16 і f32. У повному стані провалів нуль у кожному типі.

Баг 1: хвиля 64 (ai-janitor/llama-metal). Кілька його ядер множення матриці на вектор рахують chunks_per_thread = 32 / NW. При NW = 64 це 0, і ядра не роблять жодної роботи. Патч торкається 13 розмічених місць: 10 власне виправлень і 3 оптимізації.

Баг 2: тайловий завантажувач q6_K (ai-janitor/llama-metal). Він бере два значення з одного байта, переплутує нібли й віднімає 32 від беззнакового числа. На довгих промптах модель видавала @@@@. Наявні тести беруть m = 16 рядків, а тайловому ядру потрібно m ≥ 64 (повний тайл) і n ≥ 16, тож до нього вони не доходять. Саме тому доданий набір тестів був потрібен: на 869 наявних випадках баг непомітний, а наші 72 знаходять 12 провалів, і всі 12 — q6_K. Блок q6_K має 210 байт, тож кожен другий блок вирівняний лише на 2 байти, а наше прочитання побачених провалів, не підтверджене джерелом, — що GCN до Vega при невирівняному 4-байтному читанні повертає байти з вирівняної адреси.

Окремо, і не в примітці: у дереві, де розроблялися ядра множення матриці на вектор, test-backend-ops test -o MUL_MAT показує один провал — q2_K, m = 16, n = 8, k = 4096. Він однаковий до й після того переписування, тобто з ним не пов'язаний; виправлення q2_K у те дерево не переносили.

Урок: короткий запит плюс бенчмарк — це не перевірка правильності. Потрібен довгий реальний промпт і прогін тестів поруч.

6. Зламаний стан бенчмаркається швидше за правильний

Це урок роботи з Metal, який варто зберегти, і він вимірюється двома рядками. ai-janitor/llama-metal 72ba023, серія по патчах, одна карта, Qwen2.5-Coder-7B-Instruct Q4_K_M, -ngl 99 -fa 0 -t 4 -r 3, кожна довжина окремим запуском, збірки з нуля (https://github.com/SynenergyAI/mac-pro-2013-llama-metal, logs/bench_series.log, рядки 7 і 10; правильність — logs/series_run.log, рядки 2 і 3):

СтанПравильність, MUL_MATгенерація tg128
базовий стан + запасний шлях без копіювання (патч 0002)пройдено 433 з 869, провалено 43667,03 ток/с (66,77 / 67,03 / 67,11) — НЕПРАВИЛЬНО
+ фікс хвилі 64 (патч 0003)пройдено 869/8697,45 ток/с (7,49 / 7,41 / 7,45)

Зламаний стан швидший приблизно в 9 разів, бо його ядра множення матриці на вектор на хвилі 64 пропускають свою роботу: вони рахують нуль шматків на потік і виходять, хоча 433 з 869 випадків при цьому проходять. Бенчмарк цього не бачить — він міряє час, а не вивід.

Аномалію відтворено з чистої збірки, з таблицею правильності поруч. Щойно хвилю 64 виправлено, 67,03 перетворюється на 7,45. Раніший прогін на іншому дереві дав для того самого зламаного стану 67,12; серія вище його заміняє.

Тому: цифра бенчмарку без перевірки правильності не вартує нічого. Тут вона була завищена приблизно в дев'ять разів, і жодна помилка ніде не з'явилася.

7. Чесне «до і після» всередині ai-janitor/llama-metal

Ця пара внутрішня для ai-janitor/llama-metal, і вона легітимна тільки тому, що обидві її сторони проходять перевірку правильності. «До» — перший правильний стан серії по патчах, патч 0003 (869/869 і 616/616). «Після» — уся серія, патч 0008 (941/941 і 616/616). Одна карта, Qwen2.5-Coder-7B-Instruct Q4_K_M, -ngl 99 -fa 0 -t 4 -r 3 (https://github.com/SynenergyAI/mac-pro-2013-llama-metal, logs/bench_series.log, рядки 8–10 і 23–25):

до, 0003 (869/869)після, 0008 (941/941)
читання промпту pp2569,95 ток/с (9,96 / 9,95 / 9,92)49,23 ток/с (49,11 / 49,26 / 49,23) — ×4,95
генерація tg1287,45 ток/с (7,49 / 7,41 / 7,45)12,89 ток/с (12,92 / 12,89 / 12,89)
читання промпту pp512падає (див. розділ 9)52,58 ток/с (52,66 / 52,58 / 52,08)

Коротко: патчі роблять читання промпту ~у 5 разів швидшим (×4,95). На pp512 пари немає: стан «до» там падає, тож порівнювати нема з чим. Ці цифри для однієї карти з -t 4 не можна ділити на цифри для двох карт з -t 2 з розділу 3.

Чого тут більше немає. Пару «9,9 → 52,33, ×5,29» ми знімаємо: вона складала числа з різних довжин промпту (pp256 і pp512) і подавала цифру векторного шляху як швидкість базового стану. Також ранішу пару «до і після», виміряну на іншому дереві, ми замінили серією по патчах вище.

8. Інші публічні заміри цієї машини

Публічний запис по цій машині існує. Раніше ми писали, що публічний замір цього заліза існує лише один. Це теж було неправильно. Джерел щонайменше три:

  • запис у блозі від 26 травня 2026 на тій самій машині з парою D700, але під Ubuntu з Mesa RADV і Vulkan, тобто з прибраним macOS: Qwen2.5-Coder-7B-Instruct Q4_K_M — 21,39 ток/с генерації, Qwen3.6-35B-A3B Q4_K_M — 7,04;
  • issue #19615 у llama.cpp (https://github.com/ggml-org/llama.cpp/issues/19615; лютий 2026, закрито як дублікат; числа третьої сторони, ми їх не перевіряли): пара D700 з DeepSeek-R1-Distill-Qwen-14B Q4_K_M, ~11 ток/с генерації і ~70 ток/с читання промпту з обхідними налаштуваннями RADV — інша модель, умови не узгоджені з нашими;
  • issue #29596 у llama.cpp (https://github.com/ggml-org/llama.cpp/issues/29596; 28 вересня 2026, відкрите): та сама машина, відеокарти ПОВІЛЬНІШІ за процесор — числа третьої сторони; які з них pp, а які tg, з тікета однозначно не видно.

Що з цього справді можна твердити: ми не знайшли публічних цифр для цього заліза через Metal, і два баги в ядрах, які ми виправили, — в ai-janitor/llama-metal; патчі готуємо для його автора (https://github.com/SynenergyAI/mac-pro-2013-llama-metal). Обидва твердження вузькі саме настільки.

Для контексту: прогін блогу на Linux/RADV генерував 21,39 ток/с на моделі 7B; вони мали Linux з відкритим драйвером і більше свободи, ми лишилися на оригінальному macOS 12, тож це не контрольоване порівняння. Прочитання, а не вимірювання: генерація обмежена пропускною здатністю пам'яті, а за нашим розрахунком (не замір) генерація 11,7 ток/с через Metal — це близько 25% від виміряних нами 221 ГБ/с читання Metal-ядром або ~21% від паспортних 264 ГБ/с. На macOS незмінений основний llama.cpp через MoltenVK уже генерує в ×1,25 швидше за наш шлях Metal.

Для публічних крос-залізних таблиць є ще один щабель: Llama-2-7B Q4_0 (файл з TheBloke/Llama-2-7B-GGUF — модель підтверджено), -ngl 99 -fa 0 -p 512 -n 128 -r 3, ai-janitor/llama-metal з нашими виправленнями, потоки за замовчуванням:

pp512tg128
одна карта54,69 ток/с (54,69 / 55,33 / 54,65)7,92 ток/с (7,92 / 7,91 / 7,93)
дві карти (-sm layer -ts 1/1)55,04 ток/с (55,04 / 55,04 / 55,08)8,48 ток/с (8,48 / 8,48 / 8,47)

9. Межі падінь в ai-janitor/llama-metal

Падіння на ggml-metal-context.m:319 ми спершу приписали своїй збірці. Виміряні межі показали інше: падіння відтворюється у векторному шляху ai-janitor/llama-metal, і в його базовому стані теж; причину падіння на :319 не встановлено. Основний llama.cpp тут порівняти не можна: через Metal він не виконує MUL_MAT на цих картах (розділ 2).

  • Векторний шлях на одній карті падає вже з pp320: pp320, pp384 і pp448 дають код 134 і :319, а pp256 проходить. Те саме в обох щаблях, зібраних на базовому стані, — pp256 є, pp512 падає.
  • Наш повний стан на двох картах чистий на pp320, pp384, pp448 і pp512, і при розподілах -ts 1/1, -ts 4/6 і -ts 6/4 — усюди приблизно 51–52 ток/с, і від розподілу між картами нічого не залежить.
  • Наш повний стан на одній карті на pp512 працює: 52,48.

Тайловий шлях обходить це падіння векторного шляху — і це справжня частина того, що дає наша зміна, окремо від швидкості.

Одне падіння нашого повного стану на двох картах ми бачили раніше, і воно не відтворилось: та сама команда пройшла 2 рази з 2. Воно стояло в черзі одразу після двох інших процесів, що впали. Записуємо його як одне невідтворене падіння й не будуємо на ньому жодного твердження.

10. SIMD-редукції на GCN1 працюють і на ширині 64

Основний llama.cpp вмикає SIMD-редукції лише для пристроїв Apple7 або Metal3 (розділ 2). D700 не підходить ні під те, ні під інше, тож множення матриць на ній основний llama.cpp віддає процесору. У таблицях можливостей Metal від Apple SIMD-редукції й перестановки вказані для сімейства Mac2, тож залізо їх має; публічного обговорення того, чому перевірку проведено саме тут, ми не знайшли. ai-janitor/llama-metal переписує редукції так, щоб не залежати від ширини хвилі, і наша робота почалася з нього. Мікротест на карті 0 (4096 груп × 64 потоки, 256 редукцій на потік):

РедукціяКоректноЧас
simd_sum на 64 лініяхтак993 мкс
simd_shuffle_xor у межах 32 ліній + LDSтак1663 мкс
quad_sum + дерево в LDSтак2059 мкс
дерево в LDSтак2064 мкс
послідовний цикл у LDSтак5628 мкс
simd_shuffle_xor через межу 32 лінійтак11898 мкс (емулюється)

Кожен із шести варіантів міряли п'ять разів; у таблиці стоять медіани. Для найкращого варіанта п'ять прогонів дали 993, 993, 1025, 1010, 985 мкс — медіана 993, найкращий одиничний прогін 985 (журнал: logs/simd-microbench.log).

Наш ранній висновок, що примусові SIMD-редукції «дають сміття на цій відеокарті», був хибним. Сміття давали ядра, які припускали ширину хвилі 32. Переписані так, щоб не залежати від ширини хвилі, редукції працюють — і найшвидший варіант саме той, якого перевірка основного llama.cpp на цій відеокарті не дозволяє.

11. Тайловий шлях

В ai-janitor/llama-metal на відеокартах без матричних блоків усі квантовані множення ішли у векторне ядро за будь-якого розміру пакета, хоча тайлові ядра для q4_K і q6_K існували. Поріг (GGML_METAL_TILED_MIN_BATCH, за замовчуванням 16) порівнюється з шириною пакета n; окремо від нього тайловий шлях потребує m ≥ 64 рядків матриці ваг.

Тайлінг ізольовано — це один і той самий бінарник нашого повного стану з піднятим порогом і без нього, тож різниця — рівно тайловий шлях проти векторного, і правильність обох сторін 941/941. Одна карта, pp256: 9,94 → 49,02 ток/с (×4,93) (окремий прогін, не серія по патчах). На pp512 векторна сторона падає, тож там пари немає (розділ 9).

Окремі ядерні заміри й ще одна пара, обидві сторони — наше дерево, векторний шлях проти тайлового (тут m — рядки матриці ваг, n — ширина пакета):

  • множення q4_K при n = 512: 123,0 → 668,3 GFLOPS;
  • множення q6_K при n = 512: 137,7 → 704,2 GFLOPS;
  • 14B на двох картах (pp256): 4,81 → 24,78 ток/с.

На правильність тайловий диспетч, нагадаємо, не впливає зовсім: 869/869 до нього і 869/869 після. Що він дає — це швидкість і обхід дефекту векторного шляху.

12. Переписані ядра множення матриці на вектор

Ядро q4_K читає 33 МБ ваг за 449 мкс, тобто ~73,5 ГБ/с з паспортних 264 — близько 28% пропускної здатності. Ні трафік ваг, ні арифметика самі собою не пояснюють цього розриву:

  • без розпакування нібблів час ядра зменшувався на 30% (не зажурнальовано);
  • ваги з кешу замість пам'яті зменшували час на 11% (не зажурнальовано);
  • більше хвиль робили ядро повільнішим: з одним рядком на хвилю — 557 / 551 / 555 мкс, з двома (базовий варіант) — 444 / 446 / 443 мкс; три прогони на кожен варіант (журнал: logs/ablation-q4K-rows.log).

Цю пару переміряно за наявності іншого навантаження, тож невелике збурення можливе. Несуча частина тут — відношення 1,25, а не абсолютні значення; 444 мкс узгоджується з «було» 449 мкс.

Кожна хвиля заново читає свій шматок вхідного вектора. Для m = 4096, k = 14336 це ~117 МБ трафіку через кеш проти 33 МБ ваг. Нова розкладка: 8 потоків на суперблок, одне 16-байтне читання для квантів і одне для заголовка блоку, 8 рядків на хвилю, щоб шматок вектора використовувався повторно.

Умови вимірювання для цього розділу. Значення в мкс — з test-backend-ops perf -o MUL_MAT на збірці ai-janitor/llama-metal на коміті 72ba023; «було» — його ядро множення матриці на вектор як є, «стало» — наше переписане. Цифри генерації — серія по патчах: одна карта, Qwen2.5-Coder-7B-Instruct Q4_K_M, -ngl 99 -fa 0 -t 4 -r 3, tg128 (https://github.com/SynenergyAI/mac-pro-2013-llama-metal, logs/bench_series.log, рядки 16, 19, 22, 25). Патч 0006 задіює всі 64 лінії, 0007 переписує ядро q4_K, 0008 — ядро q6_K.

Ядро (m × k; m — рядки матриці ваг, n — ширина пакета)БулоСтало
q4_K 4096 × 14336449 мкс357 мкс (−20%)
q4_K 14336 × 4096528 мкс407 мкс (−23%)
q6_K 4096 × 14336883 мкс596 мкс (−33%)
Модель, генерація tg128 (Metal)0005+ 0006 (64 лінії)+ 0007 (q4_K)+ 0008 (q6_K)
Qwen2.5-Coder-7B Q4_K_M7,4211,2012,2112,89 ток/с

Приписувати поліпшення 883 → 596 мкс самому лише вирівнюванню не можна: у тому ж переписуванні змінилися три речі одночасно — вирівняне читання парами 16-бітних значень, 8 рядків на хвилю замість 2 і ширина завантаження (одне 16-байтне читання на суперблок). Ефект сплутаний.

Після переписування те саме ядро q4_K робить ті самі 33 МБ за 357 мкс, тобто ~92,4 ГБ/с, близько 35% з паспортних 264. Це цифри одного ядра. Генерація цілої моделі — інша величина: за нашим розрахунком (не замір) вона навіть після цієї роботи використовує близько чверті пропускної здатності пам'яті. Це запас, який у нашого шляху Metal ще лишається.

13. Vulkan/MoltenVK: попередня пара і тюнінг

MoltenVK з Vulkan SDK 1.4.328.1, встановлений у домашню теку без прав адміністратора. SDK 1.4.357 ми не встановили: за нашим спостереженням (не зажурнальовано) його інсталятор потребує macOS 13.

Кожен вивід з 5 окремих запитів (непублічні довгі українські тексти) ми порівняли з виводом Metal на тих самих входах — неформальна, не сліпа перевірка — і побачили ті самі факти й цифри. Цифри правильності Vulkan — це цифри незміненої основної збірки з розділу 3.

Пару Metal↔Vulkan виміряно як справжню пару: одна команда, одна сесія, одне дерево і дві збірки — по одній на бекенд (збірка Metal — з нашими переписаними ядрами з розділу 12), карта 0, -p 512 -n 64 -r 3 -fa 0. gemma-4-E4B на обох бекендах запускається з -ot per_layer_token_embd.weight=CPU — цього раніше не було сказано, а воно важить. Виміряні відношення: gemma-4-E4B — ×1,44 на читанні промпту і ×1,36 на генерації; Qwen2.5-7B — ×1,41 і ×1,08. Це відношення для однієї карти з -n 64 з іншої збірки, і поєднувати їх з відношеннями розділу 3 не можна.

Metal (доопрацьований)Vulkan
gemma-4-E4B, читання 512 / генерація76,63 / 11,77 ток/с110,28 / 15,99 ток/с (×1,44 / ×1,36)
Qwen2.5-7B, читання 512 / генерація52,47 / 12,89 ток/с74,23 / 13,98 ток/с (×1,41 / ×1,08)

Налаштування важили менше, ніж очікували. Допоміг лише -ub 1024 -b 2048: +4,6% і +3,6% до читання промпту на двох довжинах промпту (одна карта, мала модель). З восьми інших параметрів (MVK_CONFIG_*, GGML_VK_* і прапорці llama.cpp для розміру пакета, фьюжену та f16) п'ять не дали нічого (±0,3%), а три зробили гірше: -ub 2048 коштував 8,7% генерації, вимкнення фьюжену — 8,8% генерації, вимкнення f16 — 1,7% читання промпту. Паралельні слоти (-np 4) підняли сумарну пропускну здатність (читання плюс генерація) на 5,8%, а саму генерацію — на 24% (14,62 → 18,14 ток/с) (журнал: logs/vulkan-tuning-e4b.log).

14. Три пастки MoltenVK

Кеш промптів в ОЗУ (Vulkan/MoltenVK). Після кожного запиту сервер копіює кеш контексту з відеокарти в оперативну пам'ять. Через MoltenVK це займає 21,3 і 23,5 с для Qwen2.5-7B на одній карті, 56,4 і 138,8 с для Qwen3-8B на двох картах, а в налаштуванні з E4B ми бачили одну паузу 13,2 с; лог у цей час мовчить. Стеля тут — 138,8 с, а не 85 с, як ми писали раніше: це випадок, коли наше власне число було заниженим. У перевірених нами випадках — конфігураціях у таблиці нижче — --cache-ram 0 паузу знімає, а --no-cache-prompt — ні (журнал: logs/cache-pause-qwen25-7b.log).

Qwen2.5-7B-Instruct Q4_K_M на Vulkan/MoltenVK, один видимий пристрій, контекст 8192, 4 потоки, flash attention вимкнено, 3 запити поспіль (повний час / обчислення, с)123
за замовчуванням34 / 3464 / 4286 / 62
--no-cache-prompt34 / 3471 / 5093 / 69
--cache-ram 034 / 3442 / 4262 / 62

-ub 1024 з великим контекстом (Vulkan/MoltenVK, Qwen3-8B на двох картах). Qwen3-8B з розміром контексту 12 тис. на промптах 1,8–2,8 тис. токенів генерувала 2,8 ток/с, а потім упала з GPU Timeout (DeviceLost) (журнал: logs/qwen3-8b-two-cards.log). З -ub 512 — 5 запитів за 348 с без помилок, на промптах 1,8–10 тис. токенів і генерації 12,6–8,2 ток/с. llama-bench з малим контекстом цього не відтворює. Наша версія — буфери уваги без flash attention, але вона не підтверджена. -ub зустрічається у статті у чотирьох значеннях, і всі чотири справжні, кожне у своїх умовах: -ub 1024 на Vulkan з E4B на одній карті, -ub 512 на Vulkan на двох картах, де -ub 1024 обривався на Qwen3-8B, -ub 256 у конфігурації, на якій ми зупинилися, з gemma-4-12B на двох картах, і -ub 2048 як параметр тюнінгу на одній карті (розділ 13), де він коштував 8,7% генерації. Це чотири вимірювання в чотирьох контекстах, а не одна рекомендація.

--swa-full без flash attention (gemma-4-12B). У gemma-4-12B шари мають різну розмірність V. Без flash attention сервер доповнює кеш V до 2048 на шар, обидві карти заповнюються, і генерація падає до 0,45 ток/с — виміряно на Vulkan, gemma-4-12B, дві карти, з --swa-full і вимкненим flash attention. Без --swa-full і з контекстом 17 тис. — 7,0–7,7 ток/с від коротких до довгих запитів, медіана 7,5 (Vulkan, gemma-4-12B на двох картах, -ub 256, контекст 17408 — конфігурація, на якій ми зупинилися). Flash attention тут на Vulkan був повільнішим. Виміряно однією сесією на тій самій машині (Vulkan, gemma-4-12B QAT q4_0, дві карти, -ts 4,6 -ub 512, контекст 17408, промпт 16 030 токенів): з вимкненим flash attention читання промпту 66,2 ток/с і генерація 5,9 ток/с, з увімкненим — 17,9 і 2,2 ток/с, тобто приблизно ×3,7 на читанні промпту і ×2,7 на генерації (журнал: logs/flash-attention-gemma12b.log). Вільної відеопам'яті з увімкненим flash attention було навіть більше (карта 1: 1,27 ГБ проти 0,47 ГБ), тож нестача вільної відеопам'яті цього не пояснює — flash attention тут на Vulkan був повільнішим; механізм не ізольовано. Саме через це в конфігурації, на якій ми зупинилися, стоїть -fa off. Хронологія важлива: спершу був Metal, потім Vulkan — і --swa-full у нас було ввімкнено на обох бекендах. Це факт про те, що було налаштовано, а не доказ, що прапорець був потрібен: ціну 0,45 ток/с ми виміряли на іншій моделі й іншій конфігурації (Vulkan, gemma-4-12B, дві карти). Тобто в одному налаштуванні прапорець нешкідливий або потрібний, а в іншому — дорогий, і що він дає на налаштуванні з E4B, ми не міряли.

15. Qwen3.6-35B-A3B (розріджені експерти) на цій машині

Читання промпту 66 ток/с (llama-bench pp512); генерація ~11 ток/с (tg128 10,99 при -t 8, 9,01 при -t 2) з 10 шарами експертів на відеокартах. Найкраща опублікована цифра, яку ми знайшли для цієї моделі на цьому залізі, — 7,04 ток/с (блог, Linux/RADV, -t 2); цифри читання промпту для цієї моделі в блозі немає. Вони мали Linux з відкритим драйвером і більше свободи; ми лишилися на оригінальному macOS 12. Відтворіть або побийте наш результат.

На справжніх промптах через llama-server читання промпту нижче, ніж у llama-bench pp512: 42,7 ток/с (10 шарів експертів на картах, промпти ~2,5 тис. токенів(журнал: logs/server-35b-real-prompts.log), 39,0 ток/с (усі експерти на процесорі, контекст 65 тис.), 34,3 ток/с на промпті 37,5 тис. токенів і 31,3 ток/с на 61,7 тис.

Умови: основний ggml-org/llama.cpp 7cf1c54, Vulkan через MoltenVK, дві карти, -sm layer -ts 1/1 -fa 0 -p 512 -n 128 -r 3. Файл: unsloth/Qwen3.6-35B-A3B-MTP-GGUF, Qwen3.6-35B-A3B-UD-Q4_K_M.gguf. Шари експертів, розміщені на відеокартах, поділено порівну між двома картами через -ot; решта тензорів експертів лишається на процесорі.

Розміщення експертівПотокиpp512 ток/сtg128 ток/сЛог
усі шари експертів на процесорі (-ncmoe 41)260,777,95https://github.com/SynenergyAI/mac-pro-2013-llama-metal, logs/moltenvk_35b.log
10 шарів експертів на відеокартах266,149,01https://github.com/SynenergyAI/mac-pro-2013-llama-metal, logs/moltenvk_35b_experts_on_gpu.log
усі шари експертів на процесорі (-ncmoe 41)860,9410,13https://github.com/SynenergyAI/mac-pro-2013-llama-metal, logs/moltenvk_35b.log
10 шарів експертів на відеокартах866,3910,99https://github.com/SynenergyAI/mac-pro-2013-llama-metal, logs/moltenvk_35b_experts_on_gpu.log
Блог, Linux/RADV2—7,04цифра третьої сторони, 26 травня 2026

Застереження: розміщення експертів у блозі невідоме, а кількість потоків у рядках різна.

16. Що ми писали хибно

Ця стаття коштує стільки, скільки коштують її виправлення, тож вони стоять тут, а не в примітках.

  • «Чотири дефекти належать апстріму llama.cpp». Хибно. Баг хвилі 64, баг завантажувача q6_K, падіння векторного шляху з pp320 і відсутній #include <string> належать ai-janitor/llama-metal на 72ba023, окремому репозиторію. Ми жодного разу не перевірили, чий код міряємо (розділи 1 і 4).
  • «Провалено 433 з 869 випадків MUL_MAT». Хибна полярність: інструмент друкує пройдені. Пройдено 433, провалено 436; для MUL_MAT_ID пройдено 162 з 616, провалено 454 (розділ 5).
  • «Перевірка сімейства GPU в основному llama.cpp — свідомий обережний вибір дизайну». Не встановлено: публічного обговорення цієї перевірки ми не знайшли взагалі; єдине письмове окреслення меж — README, де Metal указано для Apple Silicon (розділ 2).
  • «Падіння основного llama.cpp з вивантаженням на GPU — наша перша знахідка в llama.cpp». Хибно: його описано в issue #26949 на два місяці раніше. Ми підтвердили його на ще одній машині (розділ 2).
  • «Vulkan видобуває з карт те, чого не видобуває Metal». Перебільшено: на macOS розрив на генерації — ×1,25 (MoltenVK проти нашого шляху Metal) (розділ 3).
  • «Примусові SIMD-редукції дають сміття на цій відеокарті». Хибно. Сміття давали ядра, які припускали ширину хвилі 32; самі редукції на 64 лініях працюють і найшвидші (розділ 10).
  • «Пауза кешу промптів доходить до 85 с». Наше власне число було заниженим: стеля — 138,8 с (розділ 14).
  • Раніші цифри «до і після» і генерації після переписування ядер. Їх виміряно на іншому дереві; розділи 6, 7 і 12 тепер спираються на серію по патчах у https://github.com/SynenergyAI/mac-pro-2013-llama-metal, logs/bench_series.log.
  • «Читання промпту 9,9 → 52,33, ×5,29». Знято. Відношення складало різні довжини промпту (pp256 і pp512) і подавало цифру векторного шляху як швидкість базового стану. Чесне відношення — ×4,95 на pp256 (розділи 4 і 7).
  • «Після часткового фікса хвилі 64 — MUL_MAT 534/869». Знято без заміни: на свіжих збірках чистого чекауту цей проміжний щабель не відтворився, а повний фікс дає 869/869 одразу (розділ 5).
  • «Падіння на двох картах — дефект нашої збірки». Знято: та сама команда пройшла 2 рази з 2. Записуємо як одне невідтворене падіння. Справжня межа падінь — у векторному шляху ai-janitor/llama-metal, і в його базовому стані теж (розділ 9).
  • «Публічний замір цього заліза існує лише один». Хибно: публічних джерел щонайменше три. Вузьке й правильне твердження — що ми не знайшли публічних цифр через Metal (розділ 8).

17. Обмеження

  • Одна машина.
  • Через Metal генерація досі використовує приблизно чверть пропускної здатності пам'яті; незмінений основний llama.cpp через MoltenVK генерує в ×1,25 швидше за наш шлях Metal (розділ 3). Цифри Linux/RADV — контекст з іншої ОС, драйвера і бінарника, а не контрольоване нами порівняння.
  • Умову контексту з блогу відтворити не вдалося: у llama-bench такого параметра немає, тож узято стандартний щабель -p 512 -n 128.
  • Чи вже відомі в llama.cpp провали i-квантів на MoltenVK з розділу 3, ми не перевіряли.
  • Споживання електроенергії ми самі не міряли. У власному опублікованому документі Apple (https://support.apple.com/102839) для конфігурації з парою FirePro D700 стоїть 44 Вт у простої і 270 Вт максимум; цей рядок — для конкретної конфігурації (12 ядер, 64 ГБ), і Apple міряла своє власне навантаження, а не LLM-інференс. Окремо в технічних характеристиках Apple (дані третьої сторони) зазначено «максимальну тривалу потужність» 450 Вт — це електричний номінал машини, а НЕ споживання, і ці два числа часто плутають. Нашого споживання під LLM-навантаженням немає, тож про вартість роботи стаття нічого не твердить.
  • Сліпого оцінювання якості виводу не було ніде в статті.

Конфігурація, на якій ми зупинилися

  • Vulkan/MoltenVK, gemma-4-12B QAT q4_0 на обох картах, контекст 17408
  • Metal з нашими виправленнями, як резерв: єдиний відомий нам стан Metal, у якому множення матриць на цих картах рахується правильно

Висновок ще раз. На 13-річному Mac Pro з оригінальним macOS 12 Qwen3.6-35B читає 66 ток/с (llama-bench pp512; 31–43 ток/с на справжніх промптах через llama-server) і пише ~11 ток/с (розділ 15); найкраща опублікована цифра, яку ми знайшли для цієї моделі на цьому залізі, — 7,04 ток/с. Для моделі 7B незмінений основний llama.cpp через Vulkan/MoltenVK не потребує жодного нашого патча (pp512 75,46 проти 52,36 ток/с, tg128 14,61 проти 11,73 ток/с у нашого шляху Metal, розділ 3). Цінність нашої роботи з Metal — коректність: ai-janitor/llama-metal у базовому стані рахував неправильно (провалено 436 з 869 випадків MUL_MAT), з нашими виправленнями проходить 941/941 і 616/616, а патчі готуємо для його автора.

Лишається відкритим: наскільки далі ця машина зайде з іншим розміщенням експертів і кількістю потоків. Відтворіть або побийте наш результат: https://github.com/SynenergyAI/mac-pro-2013-llama-metal.

Примітки щодо API для gemma 4 і Qwen3.6 у llama.cpp: шаблони чату знають лише enable_thinking, тож рівні reasoning_effort не впливають. Бюджет на запит — thinking_budget_tokens. Не надсилайте бюджет 0 разом з enable_thinking: false: за нашим спостереженням (не зажурнальовано) у цій збірці це ламає відповідь.

Як відтворити

# Vulkan SDK без прав адміністратора (1.4.357 потребує macOS 13)
./vulkansdk-macOS-1.4.328.1.app/Contents/MacOS/vulkansdk-macOS-1.4.328.1 \
  --root "$HOME/VulkanSDK/1.4.328.1" --accept-licenses --default-answer --confirm-command install
source ~/VulkanSDK/1.4.328.1/setup-env.sh
# llama.cpp = незмінений основний ggml-org/llama.cpp; ми міряли коміт 7cf1c54
cmake -S llama.cpp -B build-vk -DGGML_METAL=OFF -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build-vk -j6 --target llama-server llama-bench test-backend-ops
build-vk/bin/test-backend-ops test -b Vulkan0 -o MUL_MAT
build-vk/bin/llama-bench -m model.gguf -ngl 99 -sm layer -ts 1/1 -fa 0 -t 2 -p 512 -n 128 -r 3

Перевірка правильності на Metal для ai-janitor/llama-metal з нашими патчами — на тому самому бінарнику, яким міряєте швидкість, і до заміру, а не після:

build/bin/test-backend-ops test -o MUL_MAT
build/bin/test-backend-ops test -o MUL_MAT_ID

У репозиторії (https://github.com/SynenergyAI/mac-pro-2013-llama-metal): патчі Metal для ai-janitor/llama-metal, логи, скрипти, мікротест SIMD і додані тестові випадки.

Питання щодо цього вимірювання: info@synenergy.ai ← Дослідження