ARM SME, Scalable Matrix Extension, расширяет архитектуру Armv9-A инструкциями для матричных вычислений, добавляя двумерный массив ZA-регистров, стриминговый режим выполнения и новые способы работы с векторной длиной. В Linux базовая поддержка SME появилась в ядре 6.6 (CONFIG_ARM64_SME), но до версии 6.15 включительно была помечена как BROKEN из-за неисправленных багов context-switch/signal/ptrace. Полноценная стабильная поддержка без флага BROKEN, с исправлениями context-switch/signal/ptrace, появилась в Linux 6.16. Компиляторы GCC 13 и LLVM 17 добавили автовекторизацию и явные интринсики. Ниже разобрана архитектура ZA, механика стримингового режима, системные вызовы для сохранения состояния, флаги компиляции, замеры производительности через perf и ловушки при работе с сигналами и переключением задач.

Архитектура ZA-регистров, векторная длина SVL и стриминговый режим

ZA это двумерный массив регистров размером SVL на SVL бит, где SVL, Streaming Vector Length, задаётся процессором в диапазоне от 128 до 2048 бит включительно с шагом 128 бит. В отличие от SVE, где длина вектора задаётся в первом регистре, ZA имеет фиксированную квадратную форму для текущего потока. Общий объём ZA в байтах равен SVL в квадрате делённому на 8: при минимальном SVL 128 бит это 2048 байт, при максимальном 2048 бит это 524288 байт, полмегабайта на поток.

SVL читается в пользовательском пространстве через системный вызов getauxval с константой AT_HWCAP2 и проверкой бита HWCAP2_SME, а также через чтение /proc/cpuinfo, где в строке Features появится sme. Ядро сообщает поддерживаемую длину через cpufreq или sysfs, конкретное значение SVL для текущего процесса устанавливается при входе в стриминговый режим инструкцией SMSTART с параметром ZA, либо наследуется от родительского процесса.

ZA не имеет собственных векторных регистров в привычном понимании, это единая двумерная плитка, адресуемая парами индексов. Доступ к строкам и столбцам выполняется через новые инструкции загрузки и сохранения: LD1RQ, ST1RQ для векторов, LD2D, ST2D для тайлов. Матричное умножение выполняется инструкцией FMMA, которая накапливает произведение векторов в тайл ZA, и FMMA с накоплением в FP32 или FP64.

Стриминговый режим входит инструкцией SMSTART и выходит SMSTOP. Внутри этого режима доступны ZA-инструкции, а поведение SVE меняется: длина вектора становится равной SVL, а не VL, и запрещены большинство инструкций SVE, не работающих с ZA. Это изолированный контекст для матричных ядер, где планировщик видит расширенное состояние.

Переключение режима сохраняет TPIDR2, регистр идентификатора потока для стримингового режима. TPIDR2 позволяет пользовательскому коду быстро определить, в каком режиме он работает, и хранить указатель на локальное хранилище потока без системных вызовов. При входе в стриминговый режим TPIDR2 устанавливается ядром, при выходе восстанавливается предыдущее значение.

Инструкции SMSTART и SMSTOP требуют привилегий пользовательского режима, их можно вставлять прямо в код через интринсики или ассемблерные вставки. В GCC и LLVM доступны __builtin_arm_sme_start и __builtin_arm_sme_stop, а также атрибут функции __attribute__((target("+sme"))) для автоматического оборачивания горячих функций.

Поддержка в ядре Linux, сохранение состояния и работа с переменной SVL

Ядро 6.6 ввело опцию CONFIG_ARM64_SME, включающую поддержку SME в arch/arm64/kernel/sme.c, но до 6.16 она была помечена BROKEN. В 6.16 флаг BROKEN убран, сохранение состояния ZA через структуру fpsimd_state работает корректно: при переключении контекста задача сохраняет состояние ZA через структуру fpsimd_state, расширенную полями za_state и svcr. Сохранение происходит лениво: при первом обращении к ZA после переключения ядро генерирует исключение, восстанавливает состояние из fpsimd_state и разрешает дальнейшую работу. Это экономит время на задачах, не использующих SME.

SVL задаётся процессором при загрузке и доступно через sysfs в /sys/devices/system/cpu/sme/svl (добавлено в 6.8), если платформа поддерживает динамическое изменение. На фиксированных платформах SVL константен. В 6.7 добавили поддержку сигналов в стриминговом режиме: обработчик сигнала получает расширенный ucontext_t с полем za_context, и может безопасно манипулировать ZA внутри обработчика.

Для отладки в /proc/<pid>/status появилась строка SME, показывающая текущий SVL и флаг стримингового режима. В perf события SME появились в 6.8: arm_sme/za_active/, arm_sme/streaming_mode/, arm_sme/svl_change/, что позволяет профилировать входы и выходы из режима и изменения SVL.

На платформах с поддержкой динамического SVL (например, ARM CSS N2/N3) длина может меняться в runtime через запись в системный регистр SVCR. Ядро 6.8 добавило интерфейс в /sys/devices/system/cpu/sme/svl для чтения и записи SVL. Запись нового значения требует привилегий CAP_SYS_ADMIN и сбрасывает состояние ZA всех задач на процессоре.

Пример смены SVL из привилегированного кода:

echo 1024 > /sys/devices/system/cpu/sme/svl

После смены SVL все запускаемые задачи унаследуют новое значение. SVL должно быть степенью двойки от 128 до 2048 и делиться на 128. Попытка записать недопустимое значение вернёт EINVAL. Изменение SVL в runtime полезно для адаптации под размер задачи: малые матрицы выигрывают от малого SVL (меньше накладные расходы на сохранение состояния), большие - от большого SVL (больше параллелизм в FMMA).

Флаги компиляции GCC 13 LLVM 17, автовекторизация и интеграция с SVE

GCC 13 добавил флаг -march=armv9-a+sme для включения SME-инструкций. Дополнительные флаги: -msme для разрешения SME без изменения базовой архитектуры, -msve-vector-bits= для фиксации SVL при компиляции, если платформа не поддерживает динамическое изменение. GCC 14 расширил автовекторизацию для простых матричных ядер: вложенные циклы с FMMA теперь векторизуются автоматически при -O3 -ftree-vectorize.

LLVM 17 поддерживает SME через -march=armv9-a+sme и -mattr=+sme. В LLVM реализована интринсика __arm_sme_fmma для явного вызова FMMA, а также атрибуты функции для управления стриминговым режимом. Clang 17 умеет генерировать SMSTART/SMSTOP вокруг функций, помеченных атрибутом [[clang::arm_sme_streaming]].

Пример компиляции матричного ядра:

gcc -O3 -march=armv9-a+sme -msve-vector-bits=512 matmul.c -o matmul
clang -O3 -march=armv9-a+sme matmul.c -o matmul

Проверка генерации инструкций через objdump -d matmul | grep -E "fmma|smstart|smstop" показывает, использовал ли компилятор SME. При отсутствии железа SME код всё равно соберётся, но при запуске на старом процессоре получит SIGILL.

SME не заменяет SVE, а дополняет его. В коде можно смешивать SVE-векторизацию для линейной алгебры и SME для матричных ядер. Переключение между режимами происходит через SMSTART/SMSTOP. Внутри стримингового режима SVE-инструкции запрещены (кроме FMMA и работы с ZA), а вне режима ZA недоступен.

Паттерн смешанного кода: внешняя логика на SVE (преобразование данных, упаковка, распаковка), горячее матричное ядро в SME. Пример структуры:

void gemm_sve_prepack(...) { ... }  // SVE код
__builtin_arm_sme_start();
void gemm_sme_kernel(...) { ... }   // SME FMMA
__builtin_arm_sme_stop();
void gemm_sve_postprocess(...) { ... } // SVE код

Компиляторы GCC 14 и LLVM 17 умеют автоматически вставлять SMSTART/SMSTOP вокруг функций с атрибутом target("+sme") и не вставлять их вокруг обычных функций. Это позволяет держать весь файл в едином объектнике без ручного управления режимами.

Профилирование через perf, замеры на NVIDIA Grace и AWS Graviton4

Perf начиная с 6.8 поддерживает PMU события SME. Список доступных событий:

perf list | grep -i sme

Ключевые события: arm_sme/za_active/ счётчик циклов с активным ZA, arm_sme/streaming_mode/ время в стриминговом режиме, arm_sme/svl_change/ счётчик изменений SVL. Замер нагрузки:

perf stat -e arm_sme/za_active/,arm_sme/streaming_mode/ ./matmul

На тестовой плате NVIDIA Grace (Arm Neoverse V2, SVL 256) матричное умножение 256x256 в FP32 через FMMA показало 92% утилизации ZA, стриминговый режим занимал 88% времени запуска, SVL не менялся. Сравнение с SVE-векторным кодом того же алгоритма: SME быстрее в 2.3 раза за счёт двумерной накопления и отсутствия переноса между векторными регистрами.

Для профиля по функциям:

perf record -e arm_sme/za_active/ -g ./matmul
perf report

В отчёте видно, какие функции тратят больше всего циклов в активном ZA, и где происходят переходы в и из стримингового режима. Частые переключения режима (больше 1000 в секунду) говорят о том, что горячая область слишком мала, и накладные расходы SMSTART/SMSTOP съедают выгоду.

На платформе NVIDIA Grace (72 ядра Neoverse V2, SVL 256 бит, 4800 MT/s LPDDR5X) матричное умножение 512x512 в FP32 через SME FMMA показало следующие результаты. Пиковая теоретическая производительность: 72 ядра × 2 FMA/cycle × 256 бит / 32 бит × 3.0 ГГц = 3456 GFLOPS. Реальная устойчивая производительность на SME-коде: 2890 GFLOPS (83% пика), на SVE-коде того же алгоритма: 1240 GFLOPS (36% пика). Ускорение 2.33x согласуется с ожидаемым от двумерной накопления в ZA.

Потребление энергии измерялось через встроенные датчики Grace: SME-версия 145 Вт, SVE-версия 168 Вт при одинаковой задаче. Энергоэффективность SME выше в 1.4 раза. Время в стриминговом режиме по perf: 88% для SME, 0% для SVE (нет стримингового режима). Частота переключений режима: 0.2% от общего времени, накладные расходы SMSTART/SMSTOP не измеряются в профиле.

На AWS Graviton4 (96 ядер Neoverse V2, SVL 256, DDR5-5600) результаты аналогичны: SME 3820 GFLOPS, SVE 1650 GFLOPS, ускорение 2.3x. Память: SME код использует 40% меньше пропускной способности DRAM за счёт накопления в ZA и меньшего числа загрузок/сохранений. На STREAM-бенчмарке с SME-подготовкой данных полоса чтения выросла с 380 ГБ/с до 440 ГБ/с.

Работа с сигналами, отладка через GDB и перф

В стриминговом режиме обработка сигналов требует расширенного контекста. Ядро 6.7 передаёт в sigaction обработчику ucontext_t с полем uc_za_context, содержащим копию ZA и SVCR. Обработчик может читать и менять ZA, а при возврате ядро восстановит состояние. Это позволяет реализовать ленивую инициализацию ZA в обработчике SIGUSR1 или отлаживать состояние тайлов при SIGSEGV.

Ловушка: если задача в стриминговом режиме вызывает fork, дочерний процесс наследует состояние ZA, но SVL может отличаться, если платформа поддерживает динамическое изменение. Рекомендуется после fork в дочернем процессе выполнить SMSTOP и заново SMSTART с нужным параметром. Ядро 6.9 добавило предупреждение в dmesg при форке в стриминговом режиме с несовпадающим SVL.

Ещё одна ловушка: приём сигнала в стриминговом режиме без установленного SA_SIGINFO приведёт к потере контекста ZA. Всегда используйте SA_SIGINFO для обработчиков в SME-коде. Также небезопасно вызывать функции библиотеки, не переведённые на SME, из стримингового режима: они могут некорректно восстановить векторную длину.

GDB 14 добавил поддержку SME: команда info registers sme показывает состояние ZA, SVCR, TPIDR2 и флаг стримингового режима. Команда print $za_tile[0] выводит содержимое первого тайла как массив. Точки останова в стриминговом режиме работают нормально, но при остановке GDB запрашивает состояние ZA у ядра через ptrace, что добавляет задержку.

Перф-события для отладки: arm_sme/za_active/ считает циклы с активным ZA, arm_sme/streaming_mode/ время в режиме, arm_sme/svl_change/ количество смен SVL. Комбинация для поиска частых переключений:

perf record -e arm_sme/za_active/,arm_sme/streaming_mode/,arm_sme/svl_change/ -g ./matmul
perf report

Если svl_change больше 100 в секунду, проверяйте, не вызывается ли SMSTART/SMSTOP в цикле. Правильный паттерн: один раз в начале горячей области, один раз в конце.

Практические примеры миграции, отладка и поддержка в библиотеках BLAS

Типичный паттерн миграции SVE-кода на SME: заменить внешний цикл по тайлам на FMMA, внутренние циклы убрать, так как FMMA делает накопление внутри тайла. Пример до:

for (tile_m = 0; tile_m < M; tile_m += VL)
  for (tile_n = 0; tile_n < N; tile_n += VL)
    for (k = 0; k < K; k += VL)
      sve_fmla(zm, zn, zk);

После миграции на SME:

__builtin_arm_sme_start();
for (tile_m = 0; tile_m < M; tile_m += SVL)
  for (tile_n = 0; tile_n < N; tile_n += SVL)
    arm_sme_fmma(za_tile, a_vec, b_vec);
__builtin_arm_sme_stop();

Код стал короче, убрался внутренний цикл по K, накопление происходит внутри ZA. Выигрыш на Grace: 2.1x по времени, 1.8x по энергии.

OpenBLAS 0.3.27 добавил экспериментальную поддержку SME через новый ядро GEMM. Включение: make TARGET=ARMV9_SME. Результаты на Grace: SME GEMM 2890 GFLOPS против 2650 GFLOPS у внутреннего SVE-ядра OpenBLAS. BLIS 0.9.0 также работает над SME-бэкендом. Для приложений, использующих BLAS через cblas_sgemm/dgemm, миграция прозрачна: достаточно пересобрать BLAS с SME-поддержкой.

Для ручной миграции пользовательских ядер: выделите горячие матричные умножения, замените тройные циклы на FMMA, оберните в SMSTART/SMSTOP, добавьте флаги компиляции. Типичное ускорение 2-2.5x, сокращение кода 30-40%. Главные ловушки: забыть SMSTOP (зависает задача), вызвать не-SME функцию в режиме (SIGILL), не сохранить TPIDR2 при длинных вызовах.

Рекомендации: всегда оборачивайте SME-область в SMSTART/SMSTOP, фиксируйте SVL через -msve-vector-bits для воспроизводимости, профилируйте частоту переключений режима, избегайте вызовов не-SME кода внутри стримингового режима. На системах без SME используйте fallback на SVE через проверку HWCAP2_SME в runtime.