Оптимизации SIMD
NumPy предоставляет набор макросов, которые определяют Универсальные инструкции, чтобы абстрагировать типичные платформенно-зависимые инструкции, так что код SIMD нужно писать только один раз. Есть три уровня:
- Код пишется с использованием универсальных макросов инструкций с защитными условиями, которые включат использование макросов только тогда, когда компилятор их распознаёт. В NumPy они используются для построения нескольких петель ufunc. Текущая политика заключается в создании трёх петель: Одна петля — по умолчанию и не использует никаких инструкций. Одна использует минимальный набор инструкций, необходимых для архитектуры. И третья написана с использованием максимального возможного набора инструкций.
- На стадии компиляции команда distutils используется для определения минимальных и максимальных функций для поддержки, исходя из выбора пользователя и поддержки компилятора. Соответствующие макросы накладываются на платформенно/архитектурно-зависимые инструкции, и компилируются три петли.
- Во время импорта во время выполнения процессор проверяется на наличие поддерживаемых функций инструкций. Используется механизм для получения указателя на наиболее подходящую функцию, и именно она будет вызвана для функции.
Параметры сборки для компиляции
-
--cpu-baseline: минимальный набор необходимых оптимизаций. Значение по умолчанию —min, которое предоставляет минимальные функции процессора, которые могут безопасно работать на широком спектре платформ в рамках семейства процессоров. -
--cpu-dispatch: набор дополнительных оптимизаций, которые применяются динамически. Значение по умолчанию —max -xop -fma4, которое включает все функции процессора, за исключением устаревших функций AMD (в случае x86).
Аргументы команд доступны в build, build_clib, и build_ext. Если build_clib или build_ext не указаны пользователем, то вместо этого будут использованы аргументы build, которые также содержат значения по умолчанию.
Имена оптимизаций могут быть функциями процессора или группами функций, которые объединяют несколько функций или специальными параметрами для выполнения ряда процедур.
В следующих таблицах представлены текущие поддерживаемые оптимизации, отсортированные по убыванию важности.
x86 — Названия функций процессора
Имя | Подразумевает |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
x86 — Группы имён
Имя | Объединяет | Подразумевает |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
IBM/POWER big-endian — Имена функций процессора
Имя | Подразумевает |
|---|---|
| |
|
|
|
|
IBM/POWER little-endian — Имена функций процессора
Имя | Подразумевает |
|---|---|
|
|
|
|
|
|
ARMv7/A32 — Имена функций процессора
Имя | Подразумевает |
|---|---|
| |
|
|
|
|
|
|
|
|
|
|
|
|
ARMv8/A64 — Имена функций процессора
Имя | Подразумевает |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
В то время как вышеприведённые таблицы основаны на компиляторе GCC, следующие таблицы демонстрируют различия в других компиляторах:
x86::Intel Compiler - Имена функций процессора
Имя | Подразумевает |
|---|---|
|
|
|
|
|
|
Примечание
Следующие функции не поддерживаются компилятором x86::Intel: XOP FMA4
x86::Microsoft Visual C/C++ - Имена функций процессора
Имя | Подразумевает |
|---|---|
|
|
|
|
|
|
|
|
Примечание
Следующие функции не поддерживаются компилятором x86::Microsoft Visual C/C++: AVX512_KNL AVX512_KNM
Специальные параметры
-
NONE: отключение всех функций -
-
NATIVE: Включает все функции процессора, которые поддерживаются текущей -
машиной. Этот процесс основан на флагах компилятора (
-march=native, -xHost, /QxHost)
-
-
MIN: Включает минимальные функции процессора, которые могут безопасно работать на широком спектре платформ:Для архитектуры
Возвращает
x86SSESSE2x8664-bit modeSSESSE2SSE3IBM/POWERbig-endian modeNONEIBM/POWERlittle-endian modeVSXVSX2ARMHFNONEARM64AARCH64NEONNEON_FP16NEON_VFPV4ASIMD -
MAX: Включает все поддерживаемые функции процессора компилятором и платформой. -
Operators-/+: удаление или добавление функций, полезно с параметрамиMAX,MINиNATIVE.
ПРИМЕЧАНИЯ
- Функции процессора и другие параметры нечувствительны к регистру.
- Порядок запрошенных оптимизаций не имеет значения.
- В качестве разделителя можно использовать запятые или пробелы, например,
--cpu-dispatch= “avx2 avx512f” или--cpu-dispatch= “avx2, avx512f”, но аргументы должны быть заключены в кавычки. - Операнд
+добавляется только для формальных целей. Например:--cpu-baseline= "min avx2"эквивалентно--cpu-baseline="min + avx2".--cpu-baseline="min,avx2"эквивалентно--cpu-baseline`="min,+avx2". - Если функция процессора не поддерживается пользователем платформой или компилятором, она будет пропущена, а не вызовет ошибку.
- Любая указанная функция процессора, которая является базовой частью функций процессора, будет пропущена.
- Аргумент
--cpu-baselineпринудительно включает подразумеваемые функции, например,--cpu-baseline=”sse42” эквивалентно--cpu-baseline=”sse sse2 sse3 ssse3 sse41 popcnt sse42” - Значение
--cpu-baselineбудет обрабатываться как «родной», если флаг компилятора-march=nativeили-xHostилиQxHostвключён через переменную средыCFLAGS. - Процесс проверки запрошенных оптимизаций в отношении
--cpu-baselineне строгий. Например, если пользователь запросилAVX2, но компилятор его не поддерживает, мы просто пропускаем его и возвращаем максимальную оптимизацию, которую может обработать компилятор в зависимости от подразумеваемых функцийAVX2, предположим,AVX. - Пользователь всегда должен проверять окончательный отчёт в журнале сборки, чтобы убедиться в включённых функциях.
Особые случаи
Взаимосвязанные функции процессора: Некоторые исключительные условия требуют объединения определённых функций процессора для определённых компиляторов или архитектур, что делает невозможным их отдельное использование. Эти условия можно разделить на две части:
-
Совместимость архитектуры: Необходимость согласования определённых функций процессора, которые гарантированно поддерживаются последовательными поколениями одной и той же архитектуры, например:
- В ppc64le
VSX(ISA 2.06)иVSX2(ISA 2.07)подразумевают друг друга, так как первое поколение, поддерживающее режим little-endian, — это Power-8`(ISA 2.07)` - В AArch64
NEONFP16VFPV4ASIMDподразумевают друг друга, так как они являются частью базовой функциональности оборудования.
- В ppc64le
-
Совместимость компиляции: Не все компиляторы C/C++ обеспечивают независимую поддержку всех функций процессора. Например, компилятор Intel не предоставляет отдельных флагов для
AVX2иFMA3. Это логично, так как все процессоры Intel, которые имеютAVX2, также поддерживаютFMA3и наоборот, но такой подход несовместим с другими процессорами x86 от AMD или VIA. Поэтому в описании функций процессора между компиляторами C/C++ есть различия, как показано в таблицах выше.
Поведение и ошибки
Использование и примеры
Отчёт и трассировка
Понимание диспетчеризации процессора. Как работает диспетчер NumPy?
Диспетчер NumPy основан на многократной компиляции из разных источников. Это значит, что определённый исходный код компилируется несколько раз с разными флагами компилятора и с разными определениями C, которые влияют на пути кода для включения наборов инструкций для каждого скомпилированного объекта в зависимости от необходимых оптимизаций, а затем возвращённые объекты объединяются.
Эта механика должна поддерживать все компиляторы и не требует никаких специфичных для компилятора расширений, но в то же время добавляет несколько этапов обычной компиляции, которые объясняются ниже:
1- Настройка
Настройка необходимой оптимизации пользователем перед началом сборки исходных файлов через два аргумента командной строки, как описано выше:
-
--cpu-baseline: минимальный набор необходимых оптимизаций. -
--cpu-dispatch: набор дополнительных оптимизаций, подлежащих диспетчеризации.
2- Определение среды
В этой части проверяется архитектура компилятора и платформы, а также кэшируются некоторые промежуточные результаты для ускорения пересборки.
3- Валидация запрошенных оптимизаций
Проверка их на соответствие компилятору и выяснение того, что компилятор может поддерживать в соответствии с запрошенными оптимизациями.
4- Генерация основного файла конфигурации
Сгенерированный заголовок _cpu_dispatch.h содержит все определения и заголовки наборов инструкций для необходимых оптимизаций, проверенных на предыдущем шаге.
Он также содержит дополнительные определения на C, используемые для определения атрибутов модуля NumPy на уровне Python __cpu_baseline__ и __cpu_dispaٍtch__.
Что содержится в этом заголовке?
Пример заголовка был динамически сгенерирован компилятором gcc на машине x86. Компилятор поддерживает --cpu-baseline="sse sse2 sse3" и --cpu-dispatch="ssse3 sse41", и результат представлен ниже.
// The header should be located at numpy/numpy/core/src/common/_cpu_dispatch.h /**NOTE ** C definitions prefixed with "NPY_HAVE_" represent ** the required optimzations. ** ** C definitions prefixed with 'NPY__CPU_TARGET_' are protected and ** shouldn't be used by any NumPy C sources. */ /******* baseline features *******/ /** SSE **/ #define NPY_HAVE_SSE 1 #include <xmmintrin.h> /** SSE2 **/ #define NPY_HAVE_SSE2 1 #include <emmintrin.h> /** SSE3 **/ #define NPY_HAVE_SSE3 1 #include <pmmintrin.h> /******* dispatch-able features *******/ #ifdef NPY__CPU_TARGET_SSSE3 /** SSSE3 **/ #define NPY_HAVE_SSSE3 1 #include <tmmintrin.h> #endif #ifdef NPY__CPU_TARGET_SSE41 /** SSE41 **/ #define NPY_HAVE_SSE41 1 #include <smmintrin.h> #endif
Базовые функции — это минимальный набор необходимых оптимизаций, настроенных через --cpu-baseline. Они не имеют защит препроцессора и всегда включены, что означает их использование в любом исходном коде.
Значит ли это, что инфраструктура NumPy передает флаги компилятора для базовых функций всем исходным кодам?
Безусловно, да. Но передающиеся исходные коды обрабатываются по-другому.
Что произойдёт, если пользователь укажет определённые базовые функции во время сборки, но во время выполнения машина не поддерживает даже эти функции? Будет ли скомпилированный код вызван через одно из этих определений, или, возможно, сам компилятор автоматически сгенерирует/векторизует определённый фрагмент кода на основе предоставленных флагов компилятора?
Во время загрузки модуля NumPy существует этап проверки, который обнаруживает такое поведение. Он вызовет ошибку Python во время выполнения, чтобы проинформировать пользователя. Это делается для предотвращения достижения процессором ошибки нелегальной инструкции, вызывающей ошибку сегментации.
Передающиеся функции — это наш набор дополнительных оптимизаций, которые были настроены через --cpu-dispatch. Они не активированы по умолчанию и всегда защищены другими определениями на C, префикс которых NPY__CPU_TARGET_. Определения на C NPY__CPU_TARGET_ активируются только в передающихся исходных кодах.
5- Передающиеся исходные коды и инструкции конфигурации
Передающиеся исходные коды — это специальные файлы на языке C, которые могут быть скомпилированы несколько раз с различными флагами компилятора и различными определениями на C. Это влияет на пути кода для включения определённых наборов инструкций для каждого скомпилированного объекта в соответствии с «инструкциями конфигурации», которые должны быть объявлены между комментариями C (/**/) и начинаться со специальной метки @targets в начале каждого передающегося исходного кода. В то же время передающиеся исходные коды будут обрабатываться как обычные исходные коды C, если оптимизация была отключена аргументом командной строки --disable-optimization.
Что такое инструкции конфигурации?
Инструкции конфигурации — это своего рода ключевые слова, объединённые вместе для определения необходимой оптимизации для передаваемого исходного кода.
Пример:
/*@targets avx2 avx512f vsx2 vsx3 asimd asimdhp */ // C code
Ключевые слова в основном представляют дополнительные оптимизации, настроенные через --cpu-dispatch, но они также могут представлять другие варианты, такие как:
- Группы целей: предварительно сконфигурированные инструкции конфигурации, используемые для управления необходимыми оптимизациями извне передающегося исходного кода.
- Политики: наборы параметров, используемых для изменения поведения по умолчанию или принуждения компиляторов к выполнению определённых действий.
- «baseline»: уникальное ключевое слово, представляющее минимальные оптимизации, настроенные через
--cpu-baseline
Инфраструктура NumPy обрабатывает передающиеся исходные коды в четыре этапа:
-
(А) Распознавание: Как и шаблоны исходного кода и F2PY, передающиеся исходные коды требуют специального расширения
*.dispatch.cдля обозначения файлов передающегося исходного кода на C, и для C++*.dispatch.cppили*.dispatch.cxxПРИМЕЧАНИЕ: C++ пока не поддерживается. - (Б) Разбор и проверка: На этом этапе передающиеся исходные коды, отфильтрованные на предыдущем шаге, разбираются и проверяются с помощью инструкций конфигурации для каждого из них по отдельности, чтобы определить необходимые оптимизации.
-
(В) Оборачивание: Этот подход используется инфраструктурой NumPy, который доказал свою достаточную гибкость для компиляции одного исходного кода несколько раз с различными определениями на C и флагами, которые влияют на пути кода. Процесс достигается путём создания временного исходного кода C для каждой необходимой оптимизации, связанной с дополнительной оптимизацией, содержащей объявления определений на C и включающей вовлечённый исходный код с помощью директивы C #include. Для более подробного понимания обратите внимание на следующий код для AVX512F:
/* * this definition is used by NumPy utilities as suffixes for the * exported symbols */ #define NPY__CPU_TARGET_CURRENT AVX512F /* * The following definitions enable * definitions of the dispatch-able features that are defined within the main * configuration header. These are definitions for the implied features. */ #define NPY__CPU_TARGET_SSE #define NPY__CPU_TARGET_SSE2 #define NPY__CPU_TARGET_SSE3 #define NPY__CPU_TARGET_SSSE3 #define NPY__CPU_TARGET_SSE41 #define NPY__CPU_TARGET_POPCNT #define NPY__CPU_TARGET_SSE42 #define NPY__CPU_TARGET_AVX #define NPY__CPU_TARGET_F16C #define NPY__CPU_TARGET_FMA3 #define NPY__CPU_TARGET_AVX2 #define NPY__CPU_TARGET_AVX512F // our dispatch-able source #include "/the/absuolate/path/of/hello.dispatch.c"
-
(Г) Заголовок конфигурации для передачи: Инфраструктура генерирует заголовок конфигурации для каждого передающегося исходного кода, который в основном содержит два абстрактных макроса C, используемых для идентификации сгенерированных объектов, чтобы они могли использоваться для диспетчеризации во время выполнения определённых символов из сгенерированных объектов любым исходным кодом C. Он также используется для объявления вперёд.
Сгенерированный заголовок берёт имя передающегося исходного кода, исключая расширение, и заменяет его на «.h». Например, предположим, что у нас есть передающийся исходный код под названием hello.dispatch.c и содержащий следующее:
// hello.dispatch.c /*@targets baseline sse42 avx512f */ #include <stdio.h> #include "numpy/utils.h" // NPY_CAT, NPY_TOSTR #ifndef NPY__CPU_TARGET_CURRENT // wrapping the dispatch-able source only happens to the addtional optimizations // but if the keyword 'baseline' provided within the configuration statments, // the infrastructure will add extra compiling for the dispatch-able source by // passing it as-is to the compiler without any changes. #define CURRENT_TARGET(X) X #define NPY__CPU_TARGET_CURRENT baseline // for printing only #else // since we reach to this point, that's mean we're dealing with // the addtional optimizations, so it could be SSE42 or AVX512F #define CURRENT_TARGET(X) NPY_CAT(NPY_CAT(X, _), NPY__CPU_TARGET_CURRENT) #endif // Macro 'CURRENT_TARGET' adding the current target as suffux to the exported symbols, // to avoid linking duplications, NumPy already has a macro called // 'NPY_CPU_DISPATCH_CURFX' similar to it, located at // numpy/numpy/core/src/common/npy_cpu_dispatch.h // NOTE: we tend to not adding suffixes to the baseline exported symbols void CURRENT_TARGET(simd_whoami)(const char *extra_info) { printf("I'm " NPY_TOSTR(NPY__CPU_TARGET_CURRENT) ", %s\n", extra_info); }Теперь предположим, что вы добавили hello.dispatch.c в дерево исходного кода, тогда инфраструктура должна сгенерировать временный заголовок конфигурации под названием hello.dispatch.h, к которому можно обратиться из любого исходного кода в дереве исходного кода, и он должен содержать следующий код:
#ifndef NPY__CPU_DISPATCH_EXPAND_ // To expand the macro calls in this header #define NPY__CPU_DISPATCH_EXPAND_(X) X #endif // Undefining the following macros, due to the possibility of including config headers // multiple times within the same source and since each config header represents // different required optimizations according to the specified configuration // statements in the dispatch-able source that derived from it. #undef NPY__CPU_DISPATCH_BASELINE_CALL #undef NPY__CPU_DISPATCH_CALL // nothing strange here, just a normal preprocessor callback // enabled only if 'baseline' spesfied withiin the configration statments #define NPY__CPU_DISPATCH_BASELINE_CALL(CB, ...) \ NPY__CPU_DISPATCH_EXPAND_(CB(__VA_ARGS__)) // 'NPY__CPU_DISPATCH_CALL' is an abstract macro is used for dispatching // the required optimizations that specified within the configuration statements. // // @param CHK, Expected a macro that can be used to detect CPU features // in runtime, which takes a CPU feature name without string quotes and // returns the testing result in a shape of boolean value. // NumPy already has macro called "NPY_CPU_HAVE", which fit this requirment. // // @param CB, a callback macro that expected to be called multiple times depending // on the required optimizations, the callback should receive the following arguments: // 1- The pending calls of @param CHK filled up with the required CPU features, // that need to be tested first in runtime before executing call belong to // the compiled object. // 2- The required optimization name, same as in 'NPY__CPU_TARGET_CURRENT' // 3- Extra arguments in the macro itself // // By default the callback calls are sorted depending on the highest interest // unless the policy "$keep_sort" was in place within the configuration statements // see "Dive into the CPU dispatcher" for more clarification. #define NPY__CPU_DISPATCH_CALL(CHK, CB, ...) \ NPY__CPU_DISPATCH_EXPAND_(CB((CHK(AVX512F)), AVX512F, __VA_ARGS__)) \ NPY__CPU_DISPATCH_EXPAND_(CB((CHK(SSE)&&CHK(SSE2)&&CHK(SSE3)&&CHK(SSSE3)&&CHK(SSE41)), SSE41, __VA_ARGS__))Пример использования заголовка конфигурации с учётом вышеизложенного:
// NOTE: The following macros are only defined for demonstration purposes only. // NumPy already has a collections of macros located at // numpy/numpy/core/src/common/npy_cpu_dispatch.h, that covers all dispatching // and declarations scenarios. #include "numpy/npy_cpu_features.h" // NPY_CPU_HAVE #include "numpy/utils.h" // NPY_CAT, NPY_EXPAND // An example for setting a macro that calls all the exported symbols at once // after checking if they're supported by the running machine. #define DISPATCH_CALL_ALL(FN, ARGS) \ NPY__CPU_DISPATCH_CALL(NPY_CPU_HAVE, DISPATCH_CALL_ALL_CB, FN, ARGS) \ NPY__CPU_DISPATCH_BASELINE_CALL(DISPATCH_CALL_BASELINE_ALL_CB, FN, ARGS) // The preprocessor callbacks. // The same suffixes as we define it in the dispatch-able source. #define DISPATCH_CALL_ALL_CB(CHECK, TARGET_NAME, FN, ARGS) \ if (CHECK) { NPY_CAT(NPY_CAT(FN, _), TARGET_NAME) ARGS; } #define DISPATCH_CALL_BASELINE_ALL_CB(FN, ARGS) \ FN NPY_EXPAND(ARGS); // An example for setting a macro that calls the exported symbols of highest // interest optimization, after checking if they're supported by the running machine. #define DISPATCH_CALL_HIGH(FN, ARGS) \ if (0) {} \ NPY__CPU_DISPATCH_CALL(NPY_CPU_HAVE, DISPATCH_CALL_HIGH_CB, FN, ARGS) \ NPY__CPU_DISPATCH_BASELINE_CALL(DISPATCH_CALL_BASELINE_HIGH_CB, FN, ARGS) // The preprocessor callbacks // The same suffixes as we define it in the dispatch-able source. #define DISPATCH_CALL_HIGH_CB(CHECK, TARGET_NAME, FN, ARGS) \ else if (CHECK) { NPY_CAT(NPY_CAT(FN, _), TARGET_NAME) ARGS; } #define DISPATCH_CALL_BASELINE_HIGH_CB(FN, ARGS) \ else { FN NPY_EXPAND(ARGS); } // NumPy has a macro called 'NPY_CPU_DISPATCH_DECLARE' can be used // for forward declrations any kind of prototypes based on // 'NPY__CPU_DISPATCH_CALL' and 'NPY__CPU_DISPATCH_BASELINE_CALL'. // However in this example, we just handle it manually. void simd_whoami(const char *extra_info); void simd_whoami_AVX512F(const char *extra_info); void simd_whoami_SSE41(const char *extra_info); void trigger_me(void) { // bring the auto-gernreated config header // which contains config macros 'NPY__CPU_DISPATCH_CALL' and // 'NPY__CPU_DISPATCH_BASELINE_CALL'. // it highely recomaned to include the config header before exectuing // the dispatching macros in case if there's another header in the scope. #include "hello.dispatch.h" DISPATCH_CALL_ALL(simd_whoami, ("all")) DISPATCH_CALL_HIGH(simd_whoami, ("the highest interest")) // An example of including multiple config headers in the same source // #include "hello2.dispatch.h" // DISPATCH_CALL_HIGH(another_function, ("the highest interest")) }
Погружение в диспетчер процессора
Базовая конфигурация
Диспетчер
Группы и политики
Примеры
Отчёт и трассировка
© 2005–2022 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.21/reference/simd/simd-optimizations.html