Приложение ArrayFire CUDA очень медленно работает в первую минуту

Я пишу тестовую программу, используя ArrayFire, работающую на Windows 10 + Nvidia Gtx 970. Программа предназначена для обучения нейронной сети с помощью решателя SGD. Таким образом, основным вычислением является итерация по обновлению параметров сети. Итерация находится в функции с именем step().

Программа делает то, что ожидается, за исключением того, что в первую минуту она работает очень медленно. Ниже приведен вывод программы. Первый столбец — прошедшее время.

ArrayFire v3.5.1 (CUDA, 64-bit Windows, build 0a675e8)
Platform: CUDA Toolkit 8, Driver: CUDA Driver Version: 8000
[0] GeForce GTX 970, 4096 MB, CUDA Compute 5.2
  time epochs training error
     5  0.002 5.6124567
     6  0.007 5.5981609
     7  0.010 5.3560046
     8  0.015 5.2485286
     9  0.020 5.1370633
    10  0.022 5.1081303
     ....
    52  0.148 3.2528560
    53  0.150 3.2425120
    54  0.153 3.2180901
    55  0.155 3.2048657
    56  0.157 3.1949191
    57  0.158 3.1816899
    58  0.160 3.1717312
    59  0.162 3.1597322
    60  0.165 3.1370639
    60  0.498 2.1359600
    61  0.548 2.0685355
    61  0.882 1.7098215
    62  0.943 1.6575973
    62  1.277 1.4156345
    63  1.343 1.3845720
    63  1.677 1.1789854
    64  1.733 1.1549067
    64  2.067 1.0162785
     ....
    71  4.517 0.4732214
    71  4.850 0.4522045
    72  4.910 0.4501807
    72  5.243 0.4355422
    73  5.305 0.4307187

Как видите, за первую минуту он не закончил даже 1/5 эпохи. Но через одну минуту он внезапно ускорился и завершил одну эпоху примерно за 4 секунды.

О том же говорят и данные профилирования: в первую минуту среднее время выполнения функции step() составляет около 500 мс, но после первой минуты оно падает до 6 мс.

Визуальный профилировщик Nvidia показывает, что ядро ​​почти все время бездействует в первую минуту.

Я понятия не имею, что могло вызвать изменение производительности до|после первой минуты. Любая помощь приветствуется.


person Bo Tian    schedule 08.05.2018    source источник


Ответы (1)


ArrayFire использует компиляцию JIT во время выполнения для объединения нескольких вызовов функций. Поэтому, когда вы выполняете добавление или любую другую операцию с элементами, ArrayFire создаст собственное ядро ​​​​и выполнит это ядро. Это имеет некоторые накладные расходы, когда вы впервые генерируете это ядро, но эти ядра кэшируются, и дополнительные вызовы не нужно компилировать. Обычно требуется всего пара итераций, прежде чем дополнительные компиляции не потребуются. Странно, что ядра работают медленно даже после примерно 60 итераций.

Ядра JIT оцениваются с использованием внутренней эвристики, основанной на памяти и размере ядер. Возможно, ваше приложение не оптимальным образом запускает ядра и вызывает дополнительные компиляции ядра. Вы можете обойти это, форсировав оценку, вызвав функцию eval для переменной. Вот надуманный пример:

array a = randu(10, 10);
array b = randu(10, 10);
for(int i = 0; i < 100; i++) {
      a += b / 4;
      b *= i;
      eval(a, b);
}

Здесь вы оцениваете дерево JIT для переменных a и b на каждой итерации. Это позволит повторно использовать одно и то же ядро ​​на каждой итерации вместо создания ядра для разных кратных итераций.

Следует отметить, что поэлементно и некоторые условные функции, такие как select и shift, подвергаются JIT-компиляции. Другие функции принудительно оценивают свои параметры перед использованием. Кроме того, если вы слишком часто оцениваете, вы снизите производительность вашего приложения.

person Umar Arshad    schedule 08.05.2018
comment
Несмотря на то, что я не совсем понимаю, как это работает, добавление eval() действительно значительно повышает производительность. Есть ли дальнейшие чтения по этой теме? Например, в какой ситуации добавление eval(), скорее всего, улучшит производительность? В настоящее время я добавил eval() везде, где это возможно в коде. Но, как вы также упомянули, добавление слишком большого количества eval() снизит производительность. Так есть ли самый эффективный способ сделать это? - person Bo Tian; 10.05.2018
comment
Обычно я помещаю evals в конец циклов и функций, выполняющих множество операций, ЕСЛИ я чувствую, что в этом есть польза. Вы хотите избежать добавления слишком большого количества eval, потому что тогда вы будете запускать много меньших ядер, которые будут неэффективны. Я бы посоветовал вам избегать evals в вашей программе и добавлять их только в том случае, если они дают ощутимую разницу в производительности после того, как вы закончите свой алгоритм. - person Umar Arshad; 10.05.2018
comment
Я сделал еще несколько тестов. Кажется, мне нужно только коснуться глобальных массивов, используемых непосредственно в функции step(). Переменные, используемые в подфункциях и временных массивах в step() (с областью действия функции), не имеют значения. - person Bo Tian; 10.05.2018