CUDA - CUBLAS: решение многих (3x3) плотных линейных систем

Я пытаюсь решить около 1200000 линейных систем (3x3, Ax=B) с помощью CUDA 10.1, в частности, используя библиотеку CUBLAS. Я взял пример из этот (полезный!) пост и переписал предложенный код в версии Unified Memory. Алгоритм сначала выполняет факторизацию LU с помощью функции cublasgetrfBatched(), за которой следуют два последовательных вызова функции cublastrsm(), которая решает верхние или нижние треугольные линейные системы. Код прикреплен ниже. Он корректно работает примерно до 10000 матриц, и в этом случае для факторизации LU требуется ~570 мс (на NVIDIA GeForce 930MX) и ~311 мс для решения систем.

Мои проблемы/вопросы:

  1. Проблема с перегрузкой: происходит сбой при выделении памяти для более чем 10k матриц. Почему? Как я могу улучшить свой код, чтобы решить всю партию из 1,2 миллиона матриц?

  2. Проблема времени: моей целью было бы решить все эти системы менее чем за 1 секунду. Я в настоящее время следую правильному подходу? Любые предложения в противном случае?

  3. Было бы возможно и/или полезно, и если да, то как использовать «потоки» пакетов из 10 тыс. матриц?

Код:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <vector>
#include <ctime>
#include <ratio>
#include <chrono>
#include <random>
#include <time.h>
#include <math.h>

// CUDA
#include <cuda.h>
#include <cuda_runtime.h>
#include "device_launch_parameters.h"
#include <cusolverDn.h>

//#include "Utilities.cuh"

using namespace std;
using namespace std::chrono;

/************************************/
/* COEFFICIENT REARRANGING FUNCTION */
/************************************/
void rearrange(double** vec, int* pivotArray, int N, int numMatrices) {
  for (int nm = 0; nm < numMatrices; nm++) {
    for (int i = 0; i < N; i++) {
      double temp = vec[nm][i];
      vec[nm][i] = vec[nm][pivotArray[N*i + nm] - 1];
      vec[nm][pivotArray[N * i + nm] - 1] = temp;
    }
  }
}


/************************************/
/* MAIN  */
/************************************/
int main() {

  const int N = 3; 
  const int numMatrices = 10000; // I want 1200000

  // random generator to fill matrices and coefficients
  random_device device;
  mt19937 generator(device());
  uniform_real_distribution<double> distribution(1., 5.);

  //ALLOCATE MEMORY - using unified memory
  double** h_A;
  cudaMallocManaged(&h_A, sizeof(double*) * numMatrices);
  for (int nm = 0; nm < numMatrices; nm++) {
    cudaMallocManaged(&(h_A[nm]), sizeof(double) * N * N);
  }

  double** h_b;
  cudaMallocManaged(&h_b, sizeof(double*) * numMatrices);
  for (int nm = 0; nm < numMatrices; nm++) {
    cudaMallocManaged(&(h_b[nm]), sizeof(double) * N );
  }
  cout << " memory allocated" << endl;

  // FILL MATRICES
  for (int nm = 0; nm < numMatrices; nm++) {
    for (int i = 0; i < N; i++) {
      for (int j = 0; j < N; j++) {
        h_A[nm][j * N + i] = distribution(generator);
      }
    }
  }
  cout << " Matrix filled " << endl;

  // FILL COEFFICIENTS
  for (int nm = 0; nm < numMatrices; nm++) {
    for (int i = 0; i < N; i++) {
      h_b[nm][i] = distribution(generator);
    }
  }
  cout << " Coeff. vector filled " << endl;
  cout << endl;

  // --- CUDA solver initialization
  cublasHandle_t cublas_handle;
  cublasCreate_v2(&cublas_handle);
  int* PivotArray;
  cudaMallocManaged(&PivotArray, N * numMatrices * sizeof(int));
  int* infoArray;
  cudaMallocManaged(&infoArray, numMatrices * sizeof(int));

  //CUBLAS LU SOLVER
  high_resolution_clock::time_point t1 = high_resolution_clock::now();
  cublasDgetrfBatched(cublas_handle, N, h_A, N, PivotArray, infoArray, numMatrices);
  cudaDeviceSynchronize();
  high_resolution_clock::time_point t2 = high_resolution_clock::now();
  duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
  cout << "It took " << time_span.count() * 1000. << " milliseconds." << endl;


  for (int i = 0; i < numMatrices; i++)
    if (infoArray[i] != 0) {
      fprintf(stderr, "Factorization of matrix %d Failed: Matrix may be singular\n", i);
    }

 // rearrange coefficient 
 // (temporarily on CPU, this step will be on a GPU Kernel as well)
  high_resolution_clock::time_point tA = high_resolution_clock::now();
  rearrange(h_b, PivotArray, N, numMatrices);
  high_resolution_clock::time_point tB = high_resolution_clock::now();
  duration<double> time_spanA = duration_cast<duration<double>>(tB - tA);
  cout << "rearrangement took " << time_spanA.count() * 1000. << " milliseconds." << endl;

//INVERT UPPER AND LOWER TRIANGULAR MATRICES 
  // --- Function solves the triangular linear system with multiple right-hand sides
  // --- Function overrides b as a result 
  const double alpha = 1.f;
  high_resolution_clock::time_point t3 = high_resolution_clock::now();
  cublasDtrsmBatched(cublas_handle, CUBLAS_SIDE_LEFT, CUBLAS_FILL_MODE_LOWER, CUBLAS_OP_N, CUBLAS_DIAG_UNIT, N, 1, &alpha, h_A, N, h_b, N, numMatrices);
  cublasDtrsmBatched(cublas_handle, CUBLAS_SIDE_LEFT, CUBLAS_FILL_MODE_UPPER, CUBLAS_OP_N, CUBLAS_DIAG_NON_UNIT, N, 1, &alpha, h_A, N, h_b, N, numMatrices);
  cudaDeviceSynchronize();
  high_resolution_clock::time_point t4 = high_resolution_clock::now();
  duration<double> time_span2 = duration_cast<duration<double>>(t4 - t3);
  cout << "second step took " << time_span2.count() * 1000. << " milliseconds." << endl;
  
  // --- Free resources
  if (h_A) cudaFree(h_A);
  if (h_b) cudaFree(h_b);
 
  cudaDeviceReset();

  return 0;
}


person Kosuke88kk    schedule 03.11.2020    source источник
comment
Не связано с вашей проблемой памяти, но обратите внимание, что для таких маленьких матриц прямые методы могут быть более эффективными, чем разложение LU. Вы можете взглянуть на stackoverflow.com/ вопросы/983999/   -  person Damien    schedule 03.11.2020
comment
может представлять интерес: stackoverflow.com/a/55428924/1695960   -  person Robert Crovella    schedule 03.11.2020


Ответы (1)


Проблема с перегрузкой: происходит сбой при выделении памяти для более чем 10k матриц. Почему? Как я могу улучшить свой код, чтобы решить всю партию из 1,2 миллиона матриц?

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

  //ALLOCATE MEMORY - using unified memory
  double** h_A;
  cudaMallocManaged(&h_A, sizeof(double*) * numMatrices);
  for (int nm = 0; nm < numMatrices; nm++) {
    cudaMallocManaged(&(h_A[nm]), sizeof(double) * N * N);
  }

  double** h_b;
  cudaMallocManaged(&h_b, sizeof(double*) * numMatrices);
  for (int nm = 0; nm < numMatrices; nm++) {
    cudaMallocManaged(&(h_b[nm]), sizeof(double) * N );
  }

Проблема в том, что каждый вызов cudaMallocManaged имеет минимальную степень детализации. Это означает, что если вы запросите выделение 1 байта, он фактически израсходует что-то вроде 4 КБ памяти (я полагаю, что это гранулярность выделения Linux. Похоже, вы работаете в Windows, и я считаю, что гранулярность выделения Windows может быть больше) . Кроме того, это создает огромную неэффективную нагрузку передачи данных на подсистему управляемой памяти, когда вы запускаете ядро ​​(ядра будут запущены в ваших cublas вызовах).

Гораздо лучший способ сделать это — сделать одно большое выделение, а не выделение в цикле, а затем просто разделить это выделение, используя арифметику указателя. Код может выглядеть так:

  //ALLOCATE MEMORY - using unified memory
  double** h_A;
  cudaMallocManaged(&h_A, sizeof(double*) * numMatrices);
  cudaMallocManaged(&(h_A[0]), sizeof(double)*numMatrices*N*N);
  for (int nm = 1; nm < numMatrices; nm++) {
    h_A[nm] = h_A[nm-1]+ N * N;
  }

  double** h_b;
  cudaMallocManaged(&h_b, sizeof(double*) * numMatrices);
  cudaMallocManaged(&(h_b[0]), sizeof(double) * numMatrices * N);
  for (int nm = 1; nm < numMatrices; nm++) {
    h_b[nm] = h_b[nm-1] + N;
  }

Еще одним преимуществом этого является то, что процесс распределения выполняется немного быстрее.

Проблема времени: моей целью было бы решить все эти системы менее чем за 1 секунду. Я в настоящее время следую правильному подходу? Любые предложения в противном случае?

С этим изменением в вашем коде я могу успешно работать на графическом процессоре 1 ГБ (GeForce GT640) с:

const int numMatrices = 1200000;

с таким выводом:

$ ./t81
 memory allocated
 Matrix filled
 Coeff. vector filled

It took 70.3032 milliseconds.
rearrangement took 60.02 milliseconds.
second step took 156.067 milliseconds.

Ваш графический процессор может быть несколько медленнее, но я думаю, что общее время должно составлять менее 1 секунды.

Было бы возможно и/или полезно, и если да, то как использовать «потоки» пакетов из 10 тыс. матриц?

С указанным выше изменением, я не думаю, что вам нужно беспокоиться об этом. Потоки здесь не помогут с перекрытием вычислительных операций. Они могли бы помочь с перекрытием копирования/вычислений (хотя, возможно, не очень сильно на вашем GPU), но это было бы сложно реализовать в Windows с управляемой памятью. Для использования Windows я бы, вероятно, предложил переключиться на обычное разделение памяти хоста и устройства CUDA, если вы хотите изучить копировать/вычислять перекрытие.

Кроме того, вы можете получить набор вызовов cublas, которые сделают работу еще быстрее, используя прямую инверсию. CUBLAS имеет метод пакетной прямой инверсии. Обычно я бы не рекомендовал это для решения линейных уравнений, но это может быть полезно для набора инверсий 3x3 или 4x4, где вы можете легко проверить сингулярность с помощью метода определителя. Вот пример.

person Robert Crovella    schedule 04.11.2020
comment
Спасибо! Я протестировал код с этими исправлениями, и весь процесс занимает около 600 миллисекунд (первый шаг + перестановка + второй шаг), что довольно хорошо для моих целей. Однако я также протестировал код только с 3 матрицами, чтобы проверить согласованность результатов, и это не дает мне правильного решения для систем. Интересно, должен ли я изменить/исправить какой-либо индекс при заполнении матриц? - person Kosuke88kk; 04.11.2020
comment
Я не могу сказать вам, что не так с кодом, который вы не показали. Насколько мне известно, это изменение в схеме размещения не должно влиять на последующее использование в алгоритме или поведение алгоритма. - person Robert Crovella; 04.11.2020