BLIS: недостающую среднюю ступеньку построили тридцать лет назад
Третья часть разговора про программирование Apple Scalable Matrix Extension (SME2) начинается с неловкого тупика, в который неизбежно упираешься, если у тебя есть только "голый" FMOPA-цикл. Да, тот самый минималистичный фрагмент из нескольких строк, который честно перемножает два блока и аккуратно копит внешние произведения в ZA. Он работает. Он даже внушает уважение своей прямолинейностью. Но в роли BLAS-кирпичика он почти бесполезен.
У такого ядра нет важнейших деталей, без которых GEMM не становится настоящим GEMM: отсутствуют коэффициенты α и β, нет штатного режима "прибавить результат к уже существующей C", не поддерживаются транспонирования, фиксирован один тип данных, а производительность обрывается, как только матрицы перестают помещаться в L1. Ровно то, что не даёт учебное руководство "на один тайл", не даёт и самодельное микроядро: атом в руках есть, а лестницы вокруг - нет. И это как раз та самая "середина", которой не хватает в обучении SME: между понятной демкой снизу и нечитаемыми промышленными ядрами сверху.
Инстинкт подсказывает начать "строить наружу": добавить блокирование под кэш, придумать упаковку, прикрутить многопоточность, написать эпилоги с α/β, закрыть краевые случаи, расширить набор типов данных... И так легко потратить месяцы, а на выходе получить - в лучшем случае - среднюю по качеству копию того, что уже давно существует и отточено поколениями оптимизаторов.
Середину этой лестницы действительно сделали ещё в 1990‑х, а в 2010‑х оформили как фреймворк. Причём так, что внутри есть "гнездо", идеально подходящее под ваш атомарный FMOPA-кусочек. Имя этой середины - BLIS.
С BLIS полезно познакомиться не как с "ещё одной реализацией BLAS", а как с формой мышления о GEMM. Стоит один раз увидеть, как он устроен, и задача "перенести GEMM на SME" перестаёт быть многомесячным проектом. Она превращается в упражнение из серии "заполните пропуски": что именно нужно написать под новую архитектуру, а что уже относится к готовой, проверенной "мебели".
В основе BLIS - наблюдение, которое связывают с работами Кадзусигэ Гото и которое позже было аккуратно формализовано исследователями из Техасского университета в Остине (Филд Ван Зи и Роберт ван де Гейн). Если взять любую по-настоящему быструю реализацию GEMM - неважно, под x86, POWER, ARM или под экзотику, давно ушедшую со сцены - и посмотреть "сквозь детали", выяснится странная вещь: это одна и та же программа. Везде одни и те же идеи: операнды заранее укладываются в удобные панели, три измерения M/N/K режутся на блоки так, чтобы данные попадали в нужный уровень кэша в нужный момент, а в самом основании сидит маленькое вручную вылизанное микроядро, которое перемножает узкий срез A на узкий срез B.
Различается не "алгоритм", а лишь два компонента: крошечное ядро внизу и несколько чисел, описывающих размеры блоков и параметры памяти конкретного процессора. Блокирование одинаково, упаковка одинаковая, порядок циклов одинаковый. Вариативность архитектур - это оболочка. Суть различий умещается в единичном фрагменте - микроядре - и в конфигурации размеров.
BLIS делает очевидный (но почему-то редко сделанный раньше) шаг: общую часть он пишет один раз - переносимым C-кодом - а "места различий" объявляет как точки подключения. Эту переносимую махину обычно называют макроядром: именно оно отвечает за всё, что вы не хотите переписывать на каждый новый ISA. Там живут объектный API, интерфейсы CBLAS и Fortran, обработка транспонирования и нормализация порядков хранения, многоуровневое блокирование под кэши, расписание упаковок, многопоточность и весь ворох неприятных граничных случаев, когда размеры матрицы не кратны "красивым" блокам. Это тысячи строк практического знания о том, как GEMM должен жить в реальной системе, а не на идеальной доске.
А от вас BLIS требует "мебель", которая вставляется в заранее подготовленные посадочные места. В первую очередь - микроядро: умножить одну упакованную микропанель A на одну упакованную микропанель B и накопить результат в маленьком тайле. Именно здесь и оказывается ваш FMOPA-атом: он не выбрасывается, он наконец получает контекст, в котором становится полезным.
Дальше начинается самое приятное: вместо того чтобы расписывать пять слоёв обвязки вручную, вы проверяете, какие элементы уже предоставляет BLIS, и отмечаете, что нужно адаптировать под Apple SME2. И вот почему оставшаяся часть работы выглядит как "анкета": большинство сложных решений - про кэш, упаковку, порядок обхода, хвосты и параллелизм - уже приняты и закодированы.
Что это даёт именно при переносе GEMM на Apple SME2
Во-первых, BLIS чётко разделяет ответственность: микроядро занимается вычислением, а остальной мир - доставкой данных и корректностью "в большом". Для SME2 это особенно важно, потому что сам матричный движок соблазняет писать "всё в одном месте", а потом мучиться от падения производительности за пределами L1 и от сложности поддержки режимов BLAS.
Во-вторых, в рамках BLIS проще объективно измерять прогресс. Если вы меняете только микроядро и параметры блоков, то любая регрессия почти всегда локализуется: либо вы недокормили вычислительный блок данными (упаковка/страйды/предвыборка), либо микроядро не использует ресурсы SME2 так, как ожидается.
В-третьих, BLIS дисциплинирует работу с "хвостами" - теми самыми краевыми случаями, когда размеры не кратны MR/NR. На уровне демки их обычно игнорируют, а в промышленной реализации именно они определяют, будет ли библиотека пригодна для жизни. У SME2 это критично: если хвосты решать наивно, можно легко уничтожить выигрыш от быстрых тайлов.
Пять циклов BLIS и почему это правильная рамка для матричного движка
Классический GEMM в стиле BLIS строится как система вложенных циклов, где наружные уровни отвечают за крупные блоки (под LLC и L2), а внутренние - за питание микроядра данными, уже уложенными в формат "как любит вычислитель". Снаружи вы управляете тем, *какие* панели A и B попадут в кэш, внутри - тем, *как* именно они будут перемножены.
Для SME2 это совпадает с реальностью железа: матричный движок может быть очень быстрым, но только если он постоянно получает данные в предсказуемом, плотном виде. Поэтому "пять циклов" - не академическая церемония, а практический способ сделать так, чтобы микроядро всегда работало на горячем, а не ждало память.
Что обычно приходится добавить поверх FMOPA, если идти без BLIS (и почему лучше не надо)
1) Упаковка A и B в панели, причём с учётом выравнивания, форматирования и оптимального шага по памяти.
2) Подбор размеров блоков под конкретные кэши и пропускную способность памяти.
3) Корректная поддержка α/β и аккуратный эпилог записи в C.
4) Транспонирования и сочетания порядков хранения (row/col-major).
5) Многопоточность и разделение работы без конфликтов по кеш-линии и без ложного совместного использования.
BLIS закрывает это "снаружи" и освобождает вас для того, что действительно уникально: эффективно выразить микроядро на SME2 и подобрать параметры блокирования.
Практический чек-лист: на что смотреть, адаптируя микроядро под SME2
- Форма тайла (MR×NR): она должна соответствовать тому, как SME2 лучше всего копит и выгружает результат.
- Схема накопления и выгрузки: важно минимизировать лишние преобразования при записи в C и учесть β-ветку (C = αAB + βC).
- Точность и типы: даже если стартуете с одного типа данных, заранее продумайте, как микроядро будет расширяться на другие форматы (например, float/double).
- Стоимость упаковки: выигрыш от быстрого ядра легко "съедается" тяжёлой упаковкой, если не попасть в баланс.
- Поведение за пределами L1: именно здесь проявляется качество макро-архитектуры, и здесь BLIS особенно полезен - он уже заточен под жизнь в иерархии памяти.
Почему "мидл" - это не "три года опыта", а тип ответственности
В контексте таких задач "средний уровень" - не про стаж, а про способность удерживать архитектурную рамку: понимать, где заканчивается микрооптимизация и начинается системный дизайн. В переносе GEMM на SME2 мидл-уровень - это умение не переписывать весь мир ради пары процентов, а встроиться в существующую структуру, которая уже умеет быть BLAS-совместимой, масштабируемой и предсказуемой.
И ещё один важный тормоз: когда хайп мешает инженерии
Матричные движки часто обсуждают через призму ИИ, и из-за этого решения становятся нервными: хочется "сразу максимальную скорость на больших матрицах", "сразу всё под инференс", "сразу победить бенчмарки". На практике же перенос GEMM - это скучная дисциплина: корректность, хвосты, упаковка, стабильная производительность на разных размерах. Фреймворк уровня BLIS возвращает проект из мира эмоций в мир процедур: вот точки расширения, вот критерии готовности, вот где скорость, а где надёжность.
Именно поэтому BLIS выглядит как та самая недостающая средняя ступенька: он соединяет "четыре строки FMOPA" и "нечитаемое промышленное ядро" понятной конструкцией, в которой микроядро - единственная по-настоящему архитектурно-специфичная часть, а всё остальное уже давно собрано, проверено и ждёт, когда вы вставите свой атом на место.


