Я использую С++ и CUDA/C и хочу написать код для конкретной проблемы, и я столкнулся с довольно сложной проблемой сокращения.
Мой опыт параллельного программирования не является незначительным, но довольно ограниченным, и я не могу полностью понять специфику этой проблемы. Я сомневаюсь, что есть удобный или даже "простой" способ справиться с проблемами, с которыми я сталкиваюсь, но, возможно, я ошибаюсь. Если есть какие-либо ресурсы (например, статьи, книги, веб-ссылки,...) или ключевые слова, охватывающие эти или подобные проблемы, сообщите мне.
Я попытался максимально обобщить весь случай и сохранить его абстрактным, а не размещать слишком много кода.
Макет...
У меня есть система из N элементов и элементов результата. (Например, я буду использовать N = 8, но N может быть любым интегральным значением больше трех.)
static size_t const N = 8;
double init_values[N], result[N];
Мне нужно рассчитать почти каждую (не все, что я боюсь) уникальную перестановку значений init без самодиагностики.
Это означает расчет f(init_values[0],init_values[1])
, f(init_values[0],init_values[2])
,..., f(init_values[0],init_values[N-1])
, f(init_values[1],init_values[2])
,..., f(init_values[1],init_values[N-1])
,... и т.д.
Это фактически виртуальная треугольная матрица, которая имеет форму, показанную на следующем рисунке.
P 0 1 2 3 4 5 6 7
|---------------------------------------
0| x
|
1| 0 x
|
2| 1 2 x
|
3| 3 4 5 x
|
4| 6 7 8 9 x
|
5| 10 11 12 13 14 x
|
6| 15 16 17 18 19 20 x
|
7| 21 22 23 24 25 26 27 x
Каждый элемент является функцией соответствующих элементов столбца и строки в init_values
.
P[i] (= P[row(i)][col(i]) = f(init_values[col(i)], init_values[row(i)])
то есть.
P[11] (= P[5][1]) = f(init_values[1], init_values[5])
Возможны (N*N-N)/2 = 28
уникальные комбинации (Примечание: P[1][5]==P[5][1]
, поэтому мы имеем только нижнюю (или верхнюю) треугольную матрицу), используя пример N = 8
.
Основная проблема
Результирующий массив вычисляется из P как сумма элементов строки минус сумма соответствующих элементов столбца. Например, результат в позиции 3 будет рассчитываться как сумма строки 3 минус сумма столбца три.
result[3] = (P[3]+P[4]+P[5]) - (P[9]+P[13]+P[18]+P[24])
result[3] = sum_elements_row(3) - sum_elements_column(3)
Я попытался проиллюстрировать его на рисунке с N = 4.
Как следствие, верно следующее:
- Операции
-
N-1
(потенциальная параллельная запись) будут выполняться на каждомresult[i]
-
result[i]
будет записыватьN-(i+1)
из вычитаний иi
дополнений - Исходя из каждого
P[i][j]
будет вычитание наr[j]
и добавление кr[i]
Здесь возникают основные проблемы:
- Использование одного потока для вычисления каждого P и непосредственного обновления результата приведет к тому, что несколько ядер попытаются записать их в одно и то же место (N-1 потоков каждый).
- Сохранение всей матрицы P для последующего этапа восстановления, с другой стороны, очень дорогое с точки зрения потребления памяти и, следовательно, невозможно для очень больших систем.
Идея иметь unqiue, общий вектор результата для каждого блока потоков также невозможна. (N из 50k составляет 2,5 миллиарда элементов P и, следовательно, [при максимальном количестве 1024 потоков на блок] минимальное количество 2,4 миллиона блоков, потребляющих более 900 ГБ памяти, если каждый блок имеет свой собственный массив результатов с 50-кратными двойными элементами.)
Я думаю, что я мог бы справиться с уменьшением для более статического поведения, но эта проблема довольно динамична с точки зрения потенциального параллельного доступа к записи в памяти. (Или можно ли обрабатывать его каким-то "основным" типом сокращения?)
Добавление некоторых осложнений...
К сожалению, в зависимости от входа (произвольного пользователя), который не зависит от начальных значений, некоторые элементы P необходимо пропустить. Предположим, что нам нужно пропустить перестановки P [6], P [14] и P [18]. Поэтому у нас осталось 24 комбинации, которые нужно вычислить.
Как сообщить ядру, какие значения нужно пропустить? Я придумал три подхода, каждый из которых имеет заметные недостатки, если N очень большой (например, несколько десятков тысяч элементов).
1. Сохраните все комбинации...
... с их соответствующим индексом строки и столбца struct combo { size_t row,col; };
, которые должны быть рассчитаны в vector<combo>
и работать с этим вектором. (используется текущей реализацией)
std::vector<combo> elements;
// somehow fill
size_t const M = elements.size();
for (size_t i=0; i<M; ++i)
{
// do the necessary computations using elements[i].row and elements[i].col
}
Это решение потребляет много памяти, так как только "несколько" (может даже быть десять тысяч элементов, но это мало чем отличается от нескольких миллиардов), но оно позволяет избежать
- вычисления индексации
- обнаружение удаленных элементов
для каждого элемента из P, который является недостатком второго подхода.
2. Управлять всеми элементами P и найти удаленные элементы
Если я хочу работать с каждым элементом из P и избегать вложенных циклов (которые я не мог воспроизвести очень хорошо в cuda), мне нужно сделать что-то вроде этого:
size_t M = (N*N-N)/2;
for (size_t i=0; i<M; ++i)
{
// calculate row indices from `i`
double tmp = sqrt(8.0*double(i+1))/2.0 + 0.5;
double row_d = floor(tmp);
size_t current_row = size_t(row_d);
size_t current_col = size_t(floor(row_d*(ict-row_d)-0.5));
// check whether the current combo of row and col is not to be removed
if (!removes[current_row].exists(current_col))
{
// do the necessary computations using current_row and current_col
}
}
В первом примере вектор removes
очень мал, в отличие от вектора elements
, но дополнительные вычисления для получения current_row
, current_col
и if-ветки очень неэффективны.
(Помните, что мы все еще говорим о миллиардах оценок.)
3. Управляйте всеми элементами P и удаляйте элементы после этого
Еще одна идея, которую я имел, заключалась в том, чтобы независимо вычислить все допустимые и недействительные комбинации. Но, к сожалению, из-за ошибок суммирования верно следующее утверждение:
calc_non_skipped() != calc_all() - calc_skipped()
Есть ли удобный, известный, высокопроизводительный способ получения желаемых результатов от начальных значений?
Я знаю, что этот вопрос довольно сложный и, возможно, ограниченный по значимости. Тем не менее, я надеюсь, что некоторые иллюминативные ответы помогут мне решить мои проблемы.
Текущая реализация
В настоящее время это реализовано как код CPU с OpenMP.
Сначала я создал вектор вышеупомянутого combo
, который хранит каждый P, который нужно вычислить, и передайте его в параллельный цикл.
Каждый поток снабжен частным результирующим вектором, а критический раздел в конце параллельной области используется для правильного суммирования.