Многопоточность в машинном обучении — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Параграф про линейную регрессию)
(Подсветка кода)
Строка 35: Строка 35:
  
 
Пример перемножения матриц на cuBLAS
 
Пример перемножения матриц на cuBLAS
  void gpu_blas_mmul(cublasHandle_t &handle, const float *A, const float *B, float *C, const int m, const int k, const int n) {
+
    <font color="#B00040">void</font> <font color="#0000FF">gpu_blas_mmul</font>(cublasHandle_t &handle, <font color="#008000">'''const'''</font> <font color="#B00040">float</font> *A, <font color="#008000">'''const'''</font> <font color="#B00040">float</font> *B, <font color="#B00040">float</font> *C, <font color="#008000">'''const'''</font> <font color="#B00040">int</font> m, <font color="#008000">'''const'''</font> <font color="#B00040">int</font> k, <font color="#008000">'''const'''</font> <font color="#B00040">int</font> n) {
       int lda = m, ldb = k, ldc = m;
+
       <font color="#B00040">int</font> lda = m, ldb = k, ldc = m;
       const float alf = 1;
+
       <font color="#008000">'''const'''</font> <font color="#B00040">float</font> alf = 1;
       const float bet = 0;
+
       <font color="#008000">'''const'''</font> <font color="#B00040">float</font> bet = 0;
       const float *alpha = &alf;
+
       <font color="#008000">'''const'''</font> <font color="#B00040">float</font> *alpha = &alf;
       const float *beta = &bet;
+
       <font color="#008000">'''const'''</font> <font color="#B00040">float</font> *beta = &bet;
       // Do the actual multiplication
+
       <font color="#408080">''// Do the actual multiplication''</font>
 
       cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, m, n, k, alpha, A, lda, B, ldb, beta, C, ldc);
 
       cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, m, n, k, alpha, A, lda, B, ldb, beta, C, ldc);
 
   }
 
   }
  
 
Пример перемножения матриц на PyCUDA
 
Пример перемножения матриц на PyCUDA
   import pycuda.gpuarray as gpuarray
+
   <font color="#008000">'''import'''</font> <font color="#0000FF">'''pycuda.gpuarray'''</font> <font color="#008000">'''as'''</font> <font color="#0000FF">'''gpuarray'''</font>
   import numpy as np
+
   <font color="#008000">'''import'''</font> <font color="#0000FF">'''numpy'''</font> <font color="#008000">'''as'''</font> <font color="#0000FF">'''np'''</font>
   import skcuda.linalg as linalg
+
   <font color="#008000">'''import'''</font> <font color="#0000FF">'''skcuda.linalg'''</font> <font color="#008000">'''as'''</font> <font color="#0000FF">'''linalg'''</font>
   # --- Initializations
+
   <font color="#408080">''# --- Initializations''</font>
   import pycuda.autoinit
+
   <font color="#008000">'''import'''</font> <font color="#0000FF">'''pycuda.autoinit'''</font>
 
   linalg.init()
 
   linalg.init()
 
    
 
    
Строка 61: Строка 61:
 
   C_gpu = linalg.dot(A_gpu, B_gpu)
 
   C_gpu = linalg.dot(A_gpu, B_gpu)
 
    
 
    
   print(np.dot(A, B))
+
   <font color="#008000">print</font>(np.dot(A, B))
   print(C_gpu)
+
   <font color="#008000">print</font>(C_gpu)
  
 
Наивная реализация перемножения матриц на OpenCL
 
Наивная реализация перемножения матриц на OpenCL
   // First naive implementation
+
   <font color="#408080">''// First naive implementation''</font>
   __kernel void myGEMM1(const int M, const int N, const int K,
+
   __kernel <font color="#B00040">void</font> myGEMM1(<font color="#008000">'''const'''</font> <font color="#B00040">int</font> M, <font color="#008000">'''const'''</font> <font color="#B00040">int</font> N, <font color="#008000">'''const'''</font> <font color="#B00040">int</font> K,
                         const __global float *A,
+
                         <font color="#008000">'''const'''</font> __global <font color="#B00040">float</font> *A,
                         const __global float *B,
+
                         <font color="#008000">'''const'''</font> __global <font color="#B00040">float</font> *B,
                         __global float *C) {
+
                         __global <font color="#B00040">float</font> *C) {
 
        
 
        
       // Thread identifiers
+
       <font color="#408080">''// Thread identifiers''</font>
       const int globalRow = get_global_id(0); // Row ID of C (0..M)
+
       <font color="#008000">'''const'''</font> <font color="#B00040">int</font> globalRow = get_global_id(0); <font color="#408080">''// Row ID of C (0..M)''</font>
       const int globalCol = get_global_id(1); // Col ID of C (0..N)
+
       <font color="#008000">'''const'''</font> <font color="#B00040">int</font> globalCol = get_global_id(1); <font color="#408080">''// Col ID of C (0..N)''</font>
 
        
 
        
       // Compute a single element (loop over K)
+
       <font color="#408080">''// Compute a single element (loop over K)''</font>
       float acc = 0.0f;
+
       <font color="#B00040">float</font> acc = 0.0f;
       for (int k = 0; k < K; k++) {
+
       <font color="#008000">'''for'''</font> (<font color="#B00040">int</font> k = 0; k < K; k++) {
 
           acc += A[k * M + globalRow] * B[globalCol * K + k];
 
           acc += A[k * M + globalRow] * B[globalCol * K + k];
 
       }
 
       }
 
        
 
        
       // Store the result
+
       <font color="#408080">''// Store the result''</font>
 
       C[globalCol * M + globalRow] = acc;
 
       C[globalCol * M + globalRow] = acc;
 
   }
 
   }

Версия 04:38, 9 января 2021

Следует выделить следующие виды параллелизма:

  • Параллелизм на уровне инструкций (ILP[1]): несколько инструкций исполняются одновременно.
  • Параллелизм типа одна инструкция множество данных (SIMD[2]): одна операция применяется к множеству данных
  • Многопоточный параллелизм: несколько независимых рабочих потоков взаимодействуют через абстракцию совместно используемой памяти.
  • Распределенные вычисления: несколько независимых рабочих компьютеров взаимодействуют по сети. (MLlib[3] на Spark, Mahout[4] на Hadoop)

Идеи используемые для ускорения вычислений в ML

Параллелизм для ускорения линейной алгебры.

Многие операции линейной алгебры, например, векторное сложение, произведение матриц и вычисление нормы состоят из большого количества независимых операций. Поэтому можно сильно повысить их производительность как за счёт ILP и SIMD параллелизма для маленьких данных, так и за счёт многопоточности для больших данных. От ускорения линейной алгебры особенно выигрывают нейронные сети, так как большую часть времени их работы занимает умножение матриц.

Иногда необходимо выполнить операцию с объектам имеющими разнаю размерность, но которые можно привести к одной размерности повторением одного из объектов вдоль одной или нескольких осей. Например, если нужно прибавить к каждой строке матрицы вектор или домножить вектор на число. В таком случае можно не писать цикл в явном виде, а использовать broadcast операции. При этом задача оптимизации переходит к разработчику библиотеки, который может обеспечить лучший параллелизм операций за счет доступа к внутренностям библиотеки.

Примеры оптимизаций:

  • Высоко оптимизированные тензорные библиотеки для арифметики.
  • Алгоритмы в терминах матричных операций, а не векторных операций, насколько это возможно.
  • Broadcast операции вместо циклов.
  • Распараллеленные реализации некоторых специальных операций (таких как свертки для CNN).

Параллелизм в оптимизации гиперпараметров

Для параллельной оптимизации гиперпараметров можно использовать поиск по решётке или случайный поиск в которых мы можем оценить параметры независимо. Такая оптимизации часто встречаются в библиотеках машинного обучения.

Параллелизм кросс-валидации

Полная кросс-валидация, k-fold, t×k-fold, Leave-One-Out легко распараллеливаются на несколько потоков, каждый из которых работает на своем разбиении данных

ParallelCrossValidation.png

Параллелизм GPU[5]

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

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

  • CUDA[6] — язык параллельного программирования/вычислительная платформа для вычислений общего назначения на графическом процессоре
  • cuBLAS[7] — библиотека представляет собой реализацию BLAS (базовых подпрограмм линейной алгебры) поверх среды выполнения CUDA.
  • OpenCL[8]— фреймворк для написания компьютерных программ, связанных с параллельными вычислениями на различных графических и центральных процессорах, а также FPGA

Пример перемножения матриц на cuBLAS

    void gpu_blas_mmul(cublasHandle_t &handle, const float *A, const float *B, float *C, const int m, const int k, const int n) {
     int lda = m, ldb = k, ldc = m;
     const float alf = 1;
     const float bet = 0;
     const float *alpha = &alf;
     const float *beta = &bet;
     // Do the actual multiplication
     cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, m, n, k, alpha, A, lda, B, ldb, beta, C, ldc);
 }

Пример перемножения матриц на PyCUDA

 import pycuda.gpuarray as gpuarray
 import numpy as np
 import skcuda.linalg as linalg
 # --- Initializations
 import pycuda.autoinit
 linalg.init()
  
 A = np.array(([1, 2, 3], [4, 5, 6])).astype(np.float64)
 B = np.array(([7, 8, 1, 5], [9, 10, 0, 9], [11, 12, 5, 5])).astype(np.float64)
  
 A_gpu = gpuarray.to_gpu(A)
 B_gpu = gpuarray.to_gpu(B)
  
 C_gpu = linalg.dot(A_gpu, B_gpu)
  
 print(np.dot(A, B))
 print(C_gpu)

Наивная реализация перемножения матриц на OpenCL

 // First naive implementation
 __kernel void myGEMM1(const int M, const int N, const int K,
                       const __global float *A,
                       const __global float *B,
                       __global float *C) {
      
     // Thread identifiers
     const int globalRow = get_global_id(0); // Row ID of C (0..M)
     const int globalCol = get_global_id(1); // Col ID of C (0..N)
      
     // Compute a single element (loop over K)
     float acc = 0.0f;
     for (int k = 0; k < K; k++) {
         acc += A[k * M + globalRow] * B[globalCol * K + k];
     }
      
     // Store the result
     C[globalCol * M + globalRow] = acc;
 }

Параллелизм в стохастическом градиентном спуске

Можно запустить внешний цикл стохастического градиентного спуска (SGD) параллельно в пуле потоков и использовать конструкции синхронизации, такие как блокировки, чтобы предотвратить состояние гонки. Однако из-за накладных расходов на синхронизацию ускорение может получиться маленьким.

Еще более интересная идея называется асинхронным SGD или Hogwild A Lock-Free Approach to Parallelizing Stochastic Gradient Descent[9]. SGD запускается параллельно в несколько потоков без какой-либо синхронизации. Теперь состояния гонки могут возникнуть, но во многих случаях это хорошо, потому что они просто немного изменяют шум и ошибки уже присутствующие из-за случайного выбора градиента.

Параллелизм в методе k ближайших соседей

Основное время работы метода k ближайших соседей составляет поиск ближайших соседей. Так как расстояния до разных объектов независимы, то можно разбить объекты на группы, параллельно решить задачу во всех группах, а потом объединить результат Implementation of a Parallel K-Nearest Neighbor Algorithm Using MPI[10]. Альтернативный подход — параллельная сортировка всех объектов, например, с использованием битонной сортировки Parallel Search of k-Nearest Neighbors with Synchronous Operations[11].

Параллелизм в методе опорных веторов

Вычислительная сложность метода опорных векторов заключается в минимизации квадратичной функции. Первый вариант распараллеливания задачи — добавление параллелизма в алгоритм в явном виде, например, параллельная оптимизация большего количества переменных в SMO Parallel Support Vector Machines[12]. Второй подход — запись алгоритма через матричные операции, которые легко параллелизируемы Multiplicative Updates for Nonnegative Quadratic Programming[13].

Параллелизм в линейной регрессии

При использовании метода наименьших квадратов поиск коэффициентов регрессии сводится к нахождению псевдообратной матрицы. Хотя псевдообратную матрицу можно вычислить через обратную и для этого существуют параллельные алгоритмы, такой подход остается непрактичным. Более популярный способ, основанный на сингулярном разложении, можно сделать параллельным, если использовать в процессе метод Якоби для собственных значений и на каждом шаге обрабатывать несколько строк и столбцов[14]. Также можно использовать параллельный алгоритм для QR-разложения как это сделано в ScaLAPACK[15].

См. также

Примечания

Источники информации

  1. Principles of Large-Scale Machine Learning
  2. cuBLAS library user guide
  3. Matrix multiplication on GPU using CUDA with CUBLAS
  4. A short notice on performing matrix multiplications in PyCUDA
  5. CUDA C++ Programming Guide
  6. OpenCL SGEMM tuning for Kepler