скомпилировать постоянный массив памяти в немедленное значение в CUDA

Я пишу код для аппроксимации функции с использованием степенных рядов и хотел бы использовать инструкцию #pragma unroll и FMA, например:

__constant__ double coeff[5] = {1.0,2.0,3.0,4.0,5.0}; /* constant is fake here */

__device__ double some_function(double x) {
  double y;
  int i;
  y = coeff[0];
#pragma unroll
  for(i=1;i<5;i++) y = y*x + coeff[i];
  return y;
}

Код будет скомпилирован в сборку следующим образом:

ld.const.f64    %fd33, [coeff];
ld.const.f64    %fd34, [coeff+8];
fma.rn.f64      %fd35, %fd33, %fd32, %fd34;
ld.const.f64    %fd36, [coeff+16];
fma.rn.f64      %fd37, %fd35, %fd32, %fd36;
ld.const.f64    %fd38, [coeff+24];
fma.rn.f64      %fd39, %fd37, %fd32, %fd38;
ld.const.f64    %fd40, [coeff+32];
fma.rn.f64      %fd41, %fd39, %fd32, %fd40;

Я хочу избежать использования постоянной памяти и использовать немедленное значение следующим образом:

mov.f64         %fd248, 0d3ED0EE258B7A8B04;
mov.f64         %fd249, 0d3EB1380B3AE80F1E;
fma.rn.f64      %fd250, %fd249, %fd247, %fd248;
mov.f64         %fd251, 0d3EF3B2669F02676F;
fma.rn.f64      %fd252, %fd250, %fd247, %fd251;
mov.f64         %fd253, 0d3F1745CBA9AB0956;
fma.rn.f64      %fd254, %fd252, %fd247, %fd253;
mov.f64         %fd255, 0d3F3C71C72D1B5154;
fma.rn.f64      %fd256, %fd254, %fd247, %fd255;
mov.f64         %fd257, 0d3F624924923BE72D;
fma.rn.f64      %fd258, %fd256, %fd247, %fd257;
mov.f64         %fd259, 0d3F8999999999A3C4;
fma.rn.f64      %fd260, %fd258, %fd247, %fd259;
mov.f64         %fd261, 0d3FB5555555555554;
fma.rn.f64      %fd262, %fd260, %fd247, %fd261;

Я знаю, что для этого можно использовать макрос #define, но это очень неудобно, когда много коэффициентов.

Есть ли какой-либо модификатор типа данных C (или параметры компилятора), который мог бы преобразовать мой массив коэффициентов в немедленные значения вместо использования постоянной памяти?

Я пробовал, и это не работает для static double, static __constant__ double и static __device__ double.

и мой последний вопрос: я думаю, использование немедленного значения должно быть быстрее, чем использование постоянной памяти?


person wonghang    schedule 20.01.2015    source источник
comment
Для производительности важен конечный машинный код (SASS), а не промежуточное представление (PTX). В зависимости от вашей целевой архитектуры инструкция FMA может ссылаться на константную память напрямую без отдельных инструкций загрузки, и это настолько быстро, насколько это возможно (унифицированный доступ к константной памяти практически так же быстр, как доступ к регистру). Ваш код, скомпилированный для sm_35, приводит к следующей последовательности: DFMA R2, R4, c[0x3][0x0], R2; DFMA R2, R2, R4, c[0x3][0x10]; DFMA R2, R2, R4, c[0x3][0x18]; DFMA R4, R2, R4, c[0x3][0x20];   -  person njuffa    schedule 20.01.2015
comment
Даже если производительность такая же, избавление от нее также сэкономит мой постоянный объем памяти (всего 64 КБ на моей видеокарте).   -  person wonghang    schedule 21.01.2015


Ответы (2)


Хорошо, то, что вы пытаетесь сделать, невозможно (по крайней мере, не с CUDA) так, как вы пытаетесь, и это потому, что CUDA запрещает объявления массивов static const в глобальной области видимости. CUDA требует, чтобы каждый глобальный массив был назначен определенному адресному пространству (__device__, __contant__ и т. д.).

Однако с некоторыми неудобствами это возможно.

Я собрал несколько ответов SO:

C++11: вычисление времени компиляции массива

Можно ли разработать статический цикл for в С++?< /а>

, пожалуйста, уважайте работу и добавили немного CUDA.

А, вот и ты:

Вы хотите, чтобы компилятор делал всю грязную работу за вас, и поэтому вы должны все и вся оценивать во время компиляции:

Сначала нам нужен статический массив, в котором мы можем хранить коэффициенты:

template <unsigned int index, long long... remPack> struct getValue;

template <unsigned int index, long long In, long long... remPack>
struct getValue<index, In, remPack...> {
  static const long long value = getValue<index - 1, remPack...>::value;
};

template <long long In, long long... remPack>
struct getValue<1, In, remPack...> {
  static const long long value = In;
};

template <long long... T> struct static_array {
  template <int idx> static __device__ int get() { return getValue<idx, T...>::value; }
};

Этот static_array хранит значения в системе типов C++ как long long. Я вернусь к этому позже в ответе.

Далее следует развернуть цикл for. Опять же, для этого используйте метапрограммирование шаблонов:

template <int First, int Last, template <int> class Fn> struct static_for {
  __device__ double operator()(double x, double y) const {
      return static_for<First + 1, Last, Fn>()(x, Fn<First + 1>()(x, y));
  }
};

template <int N, template <int> class Fn> struct static_for<N, N, Fn> {
  __device__ double operator()(double x, double y) const { return y; }
};

Поскольку мы делаем все статические во время компиляции, необходимо перемещать вход и выход каждого «цикла» через параметры и возвращаемое выражение operator().

Это решение очень статично, и с помощью большего количества метапрограмм шаблонов вы можете улучшить его.

Хорошо, теперь самое интересное. Расчеты:

template <int i> struct Function {
  __device__ double operator()(double x, double y) {
    double c = __longlong_as_double(static_array<12, 34, 22, 55, 24>::get<i>());
    return y * x + c;
  }
};

__device__ double some_function(double x) {
  return static_for<0, 5, Function>()(x, 0.0);
}

Система типов C++ допускает только целые типы в качестве нетиповых параметров шаблона, поэтому мы должны хранить наши doubles в long longs, а затем использовать функцию CUDA __longlong_as_double(), чтобы преобразовать их обратно. Это то, что мы должны принять на данный момент, и это может стать для вас препятствием, потому что это уже не «просто». Однако написать преобразователь double в long long не так уж сложно.

Все вычисления заключены в функторном объекте, который получает счетчик поездок из нашего static_loop в качестве аргумента шаблона. С помощью этого «счетчика пути» времени компиляции мы можем получить доступ к static_array преобразованию long long версии двойного возврата и вычислить FMA.

Благодаря компилятору CUDA (который здесь действительно хорошо справляется), это код PTX (nvcc -ptx -arch=sm_35 test.cu). Я использовал версию 7.0 RC1:

.visible .func  (.param .b64 func_retval0) _Z13some_functiond(
        .param .b64 _Z13some_functiond_param_0
)
{
        .reg .f64       %fd<7>;

        ld.param.f64    %fd1, [_Z13some_functiond_param_0];
        fma.rn.f64      %fd2, %fd1, 0d0000000000000000, 0d000000000000000C;
        fma.rn.f64      %fd3, %fd2, %fd1, 0d0000000000000022;
        fma.rn.f64      %fd4, %fd3, %fd1, 0d0000000000000016;
        fma.rn.f64      %fd5, %fd4, %fd1, 0d0000000000000037;
        fma.rn.f64      %fd6, %fd5, %fd1, 0d0000000000000018;
        st.param.f64    [func_retval0+0], %fd6;
        ret;
}
person Michael Haidl    schedule 20.01.2015
comment
Большое спасибо за ваши усилия. Я думаю, это должно сработать. Но я ничего не знаю о метапрограммировании C++. Первоначально я думаю, что это должно работать, добавляя некоторые параметры компилятора. Учитывая сложность этого метода. Вместо этого я попытаюсь написать 20-30 строк __fma_rn(...). - person wonghang; 21.01.2015

По крайней мере, в Cuda 8 локальные массивы constexpr работают нормально, т.е. для развернутых циклов *.ptx содержит непосредственные значения, а не ссылки на память. Пример (непроверенный):

#define COEFF_VALUES { 1.0, 2.0, 3.0, 4.0, 5.0 }

__device__ double some_function( double x )
{
    constexpr double coeff[ 5 ] = COEFF_VALUES;
    double y;
    int i;
    y = coeff[ 0 ];
#pragma unroll
    for( i = 1; i < 5; i++ ) y = y*x + coeff[ i ];
    return y;
}

Компилируется в такой код:

add.f64     %fd2, %fd1, 0d4000000000000000;
fma.rn.f64  %fd3, %fd1, %fd2, 0d4008000000000000;
fma.rn.f64  %fd4, %fd1, %fd3, 0d4010000000000000;
fma.rn.f64  %fd5, %fd1, %fd4, 0d4014000000000000;
person Soonts    schedule 28.04.2018