- •1 Обозначения и сокращения
- •Введение
- •3 Обзор существующих работ
- •4 Постановка задачи
- •5 Метод решения
- •5.1 Расщепление
- •5.2 Адвекция
- •5.3 Проекция
- •6 Выбор метода выполнения этапа проекции
- •7 Полный многосеточный метод
- •8 Реализация этапа проекции
- •8.1 Входные данные
- •8.2 Сглаживатель
- •8.3 Расчёт невязки
- •8.4 Расчёт нормы невязки
- •8.5 Операторы огрубления и пролонгации
- •8.6 Взаимодействие между несколькими gpu
- •8.7 Результаты тестирования и валидации
- •9 Результаты
- •10 Список литературы
8.3 Расчёт невязки
Расчёт невязки является операцией по реализации очень похожей на операцию сглаживания, поэтому работа с граничными условиями характерная для сглаживания характерна и для нее.
Невязка рассчитывается по формуле:
,
т.е.
Где количество соседей, считается так же, как в сглаживателе. Сокращение слагаемых происходит тем же образом.
8.4 Расчёт нормы невязки
Чтобы уменьшить время расчёта, выполнение критерия останова проверялось не после V-цикла, а после выполнения итераций предварительного сглаживания на самой точной сетке. Такой подход, во-первых, позволяет сэкономить на расчёте невязки, так как в случае невыполнения критерия останова её все равно пришлось бы считать, а, во-вторых, на времени выполнения V-цикла. Так как операции предварительного сглаживания на точной сетке производятся гораздо быстрее, чем весь V-цикл, если критерий останова достигается за счет сглаживания до начала огрубления, то нет необходимости выполнять последующие вычисления.
В
качестве метрики для проверки критерия
останова использовалась норма Гёльдера:
Особенностью реализации подсчёта нормы на GPU является редукция переменных из warp’ов, которые, вообще говоря, могут не выполняться одновременно.
Был использован алгоритм, заключающийся в следующей последовательности действий:
Редукция внутри одного thread’а.
Редукция внутри одного warp’а.
Редукция внутри одного CUDA-блока.
Редукция внутри всего CUDA-grid’а.
Начиная с архитектуры Kepler, для видеокарт NVidia стало возможным получение в CUDA-потоке данных, лежащих в регистрах соседних CUDA-потоков. Так как доступ к файлу регистров такой же быстрый, как доступ к L1-кэшу, данный подход в комбинации с методом сдваивания является наилучшим для редукции внутри одного warp’a.
real warpReduceMax(real val) {
//Цикл по всем thread'ам warp'а, осуществляющий сдваивание
for (int offset = warpSize / 2; offset > 0; offset /= 2){
//Чтение данных из регистра другого thread’а
real val2 = __shfl_down(val, offset);
val = (val > val2) ? val : val2;
}
return val;
}
На языке CUDA его можно описать так:
Данный подход не применим внутри всего CUDA-блока, т.к. чтение из соседних регистров возможно только внутри warp’a.
Для вычислений внутри блока в CUDA есть еще один тип памяти, работа с которой по производительности эквивалентна работе с L1 кэшем – shared (разделяемая память). Доступ к общей для нескольких thread’ов shared памяти возможен только внутри одного CUDA-блока, поэтому с ее использованием мы не можем произвести редукцию по всему grid’у, но можем выполнить ее внутри одного CUDA-block’a следующим образом:
Выделяем внутри для каждого блока массив в shared-памяти, вмещающий по одному числу с плавающей запятой, для каждого warp’a. Внутри одного блока может быть не более 1024 потоков – значит для того, чтобы вместить информацию из всех warp’ов достаточно 1024/32 =32 ячеек массива.
Нулевой поток каждого warp’а записывает результат внутренней редукции warp’а в отведенную для него ячейку.
Происходит синхронизация потоков.
Нулевой warp блока осуществляет редукцию указанного массива с помощью приведенного ранее алгоритма.
#typedef
unsigned
long
long
int
uint64 __device__
double
atomicMax(double
* const
address,
const
double
value) { if
(*address
>= value) { return
*address; }
uint64
* const
address_as_i = (uint64
*)address; uint64
old = *address_as_i, assumed;
do { assumed
= old; if
(__longlong_as_double(assumed) >= value) { break; } //выполняем
сравнение и замену внутри атомарной
операции old
= atomicCAS(address_as_i, assumed, __double_as_longlong(value)); }
while
(assumed != old); return
__longlong_as_double(assumed); }
