Как работает диспетчер процессора?
Диспетчер NumPy основан на многократной компиляции из одного источника, что означает взятие определенного источника и многократную его компиляцию с различными флагами компилятора, а также с различными определениями C, которые влияют на пути кода. Это позволяет определять наборы инструкций для каждого скомпилированного объекта в зависимости от требуемых оптимизаций и завершается связыванием возвращенных объектов вместе.
Этот механизм должен поддерживать все компиляторы и не требует никаких специфичных для компилятора расширений, но в то же время он добавляет несколько шагов к нормальной компиляции, которые объясняются следующим образом.
1- Настройка
Настройка необходимой оптимизации пользователем перед началом построения исходных файлов через два аргумента командной строки, как объяснено выше:
-
--cpu-baseline: минимальный набор требуемых оптимизаций. -
--cpu-dispatch: набор дополнительных оптимизаций, подлежащих распределению.
2- Определение среды
В этой части мы проверяем компилятор и архитектуру платформы и кэшируем некоторые промежуточные результаты для ускорения перестроения.
3- Проверка запрошенных оптимизаций
Проверяя их на компиляторе и определяя, что компилятор может поддержать в соответствии с запрошенными оптимизациями.
4- Генерация основного заголовка конфигурации
Сгенерированный заголовок _cpu_dispatch.h содержит все определения и заголовки наборов инструкций для необходимых оптимизаций, которые были проверены на предыдущем шаге.
Он также содержит дополнительные определения C, используемые для определения атрибутов модуля NumPy на уровне Python __cpu_baseline__ и __cpu_dispatch__.
Что содержится в этом заголовке?
Пример заголовка был динамически сгенерирован 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 optimizations. ** ** 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++ пока не поддерживается. - (Б) Разбор и проверка: На этом этапе исходные файлы, подлежащие распределению, которые были отфильтрованы на предыдущем шаге, анализируются и проверяются по операторам конфигурации для каждого из них поочередно, чтобы определить необходимые оптимизации.
-
(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"
-
(D) Заголовок конфигурации, подлежащей распределению: Инфраструктура генерирует заголовок конфигурации для каждого исходного файла, подлежащего распределению. Этот заголовок в основном содержит два абстрактных макроса 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 additional optimizations // but if the keyword 'baseline' provided within the configuration statements, // 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 additional 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' specified within the configuration statements #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 fits this requirement. // // @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 declarations 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-generated config header // which contains config macros 'NPY__CPU_DISPATCH_CALL' and // 'NPY__CPU_DISPATCH_BASELINE_CALL'. // it is highly recommended to include the config header before executing // 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–2024 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/2.0/reference/simd/how-it-works.html