Вопрос предполагает, что он задается в контексте машинного обучения, и поэтому основное внимание уделяется вычислениям с одинарной точностью и, в частности, использованию формата IEEE-754 binary32
. Аскер заявляет, что графические процессоры NVIDIA представляют интерес. Я сосредоточусь на использовании этих графических процессоров с помощью CUDA, поскольку я не знаком с привязками Python для CUDA.
Говоря о FLOPS, существуют различные школы мысли о том, как их считать помимо простых сложений и умножений. Графические процессоры, например, вычисляют деления и квадратные корни в программном обеспечении. Менее двусмысленно идентифицировать инструкции с плавающей запятой и подсчитывать их, что я и сделаю здесь. Обратите внимание, что не все инструкции с плавающей запятой будут выполняться с одинаковой пропускной способностью, и это также может зависеть от архитектуры графического процессора. Некоторую важную информацию о пропускной способности инструкций можно найти в Руководстве по программированию CUDA.
Начиная с архитектуры Тьюринга (вычислительная способность 7.5), графические процессоры включают инструкцию MUFU.TANH
для вычисления гиперболического тангенса одинарной точности с точностью около 16 бит. Функции одинарной точности, поддерживаемые многофункциональным блоком (MUFU), обычно вычисляются посредством квадратичной интерполяции в таблицах, хранящихся в ПЗУ. Насколько я могу судить, MUFU.TANH
отображается на уровне виртуального языка ассемблера PTX, но не (начиная с CUDA 11.2) как встроенная функция устройства.
Но учитывая, что функциональность представлена на уровне PTX, мы можем легко создать собственную встроенную функцию с помощью одной строки встроенного ассемблера:
// Compute hyperbolic tangent for >= sm75. maxulperr = 133.95290, maxrelerr = 1.1126e-5
__forceinline__ __device__ float __tanhf (float a)
{
asm ("tanh.approx.f32 %0,%1; \n\t" : "=f"(a) : "f"(a));
return a;
}
На более старых архитектурах графических процессоров с вычислительной мощностью ‹ 7.5 мы можем реализовать встроенную функцию с очень близкими характеристиками с помощью алгебраического преобразования и использования машинных инструкций MUFU.EX2
и MUFU.RCP
, которые вычисляют экспоненциальное основание 2 и обратное значение соответственно. Для аргументов, малых по величине, мы можем использовать tanh(x) = x и экспериментально определить хорошую точку переключения между двумя приближениями.
// like copysignf(); when first argument is known to be positive
__forceinline__ __device__ float copysignf_pos (float a, float b)
{
return __int_as_float (__float_as_int (a) | (__float_as_int (b) & 0x80000000));
}
// Compute hyperbolic tangent for < sm_75. maxulperr = 108.82848, maxrelerr = 9.3450e-6
__forceinline__ __device__ float __tanhf (float a)
{
const float L2E = 1.442695041f;
float e, r, s, t, d;
s = fabsf (a);
t = -L2E * 2.0f * s;
asm ("ex2.approx.ftz.f32 %0,%1;\n\t" : "=f"(e) : "f"(t));
d = e + 1.0f;
asm ("rcp.approx.ftz.f32 %0,%1;\n\t" : "=f"(r) : "f"(d));
r = fmaf (e, -r, r);
if (s < 4.997253418e-3f) r = a;
if (!isnan (a)) r = copysignf_pos (r, a);
return r;
}
Компиляция этого кода с помощью CUDA 11.2 для цели sm_70
, а затем дизассемблирование двоичного кода с помощью cuobjdump --dump-sass
показывает восемь инструкций с плавающей запятой. Мы также можем видеть, что полученный машинный код (SASS) не имеет ответвлений.
Если нам нужен гиперболический тангенс с полной точностью одинарной точности, мы можем использовать минимаксную полиномиальную аппроксимацию для аргументов, малых по величине, при этом используя алгебраическое преобразование и машинные инструкции MUFU.EX2
и MUFU.RCP
для аргументов, больших по величине. За пределами определенной величины аргумента результат будет ±1.
// Compute hyperbolic tangent. maxulperr = 1.81484, maxrelerr = 1.9547e-7
__forceinline__ __device__ float my_tanhf (float a)
{
const float L2E = 1.442695041f;
float p, s, t, r;
t = fabsf (a);
if (t >= 307.0f/512.0f) { // 0.599609375
r = L2E * 2.0f * t;
asm ("ex2.approx.ftz.f32 %0,%1;\n\t" : "=f"(r) : "f"(r));
r = 1.0f + r;
asm ("rcp.approx.ftz.f32 %0,%1;\n\t" : "=f"(r) : "f"(r));
r = fmaf (r, -2.0f, 1.0f);
if (t >= 9.03125f) r = 1.0f;
r = copysignf_pos (r, a);
} else {
s = a * a;
p = 1.57394409e-2f; // 0x1.01e000p-6
p = fmaf (p, s, -5.23025580e-2f); // -0x1.ac766ap-5
p = fmaf (p, s, 1.33152470e-1f); // 0x1.10b23ep-3
p = fmaf (p, s, -3.33327681e-1f); // -0x1.5553dap-2
p = fmaf (p, s, 0.0f);
r = fmaf (p, a, a);
}
return r;
}
Этот код содержит ветвь, зависящую от данных, и взгляд на машинный код, сгенерированный CUDA 11.2 для цели sm75
, показывает, что ветвь сохраняется. Это означает, что, как правило, во всех активных потоках некоторые из них будут следовать одной стороне ветви, а остальные — другой стороне ветви, что потребует последующей синхронизации. Таким образом, чтобы получить реалистичное представление о необходимых вычислительных затратах, нам нужно объединить количество инструкций с плавающей запятой для обоих путей выполнения. Получается тринадцать инструкций с плавающей запятой.
Границы ошибок в приведенных выше комментариях к коду были установлены исчерпывающими тестами со всеми возможными аргументами одинарной точности.
person
njuffa
schedule
07.04.2021
flops
, которая сообщала вам, сколько операций она выполнила. Это было на удивление полезно, так как можно было сделать первое приближение производительности C-реализации алгоритма в реальном времени. В MatLab этого больше нет, поскольку большая часть кода является внешним кодом (например, FFTW вместо FFT.m). - person bazza   schedule 26.03.2017