在一次操作中进行多个矩阵 - 矩阵乘法

时间:2012-02-09 18:21:19

标签: c++ c cuda blas cublas

我正在实现一种算法,实质上是一系列矩阵矩阵乘法,如下所示:

Res = M1.M2.M3. ... .Mn

我的矩阵实际上是100x100的小型浮点数,但序列非常长,大约数十亿。

我尝试使用CUBLAS来进行矩阵乘法,但这很慢,但我确实注意到了一些有趣的东西。

将100x100乘以100x100矩阵很慢,但是将1.000.000x100乘以100x100相对较快,这让我想到了。如果我从左到右扫描而不是并行扫描10.000次。这应该是非常快的,如果我在完成这项工作后将我的矩阵相乘,我会得到相同的结果 - 只是更快。

Res1 = M1.M2.M3. ... .Mn/1000-1
Res1 = M1+n/1000.M2+n/1000.M3+n/1000. ... .M2(n/1000)-1
...
Res1  = M1+999*n/1000.M2+999*n/1000.M3+999*n/1000. ... .M1000*(n/1000)-1
Res = Res1*Res2* ... *Res999 

M_1 ... M_n在一组约100个不同的矩阵中没有任何价值,因此空间消耗并不是真正的问题,我需要做的就是在一次操作中进行多次乘法。

现在这是我的问题。我已经完成了一个矩阵矩阵(sgemm)实现的灵感来自他们的文档中的一个nvidia演示,但它的顺序大约是cublas的4倍。有谁知道CUBLAS如何运作?如果代码在某个地方可用?

3 个答案:

答案 0 :(得分:11)

你看过最新的CUBLAS (version 4.1)吗?它包括一个新的批量GEMM模式,专门用于大批量的小矩阵矩阵乘法。我建议做一个成对的乘法树,正如Jonathan Dursi在他的回答中所建议的那样,使用CUBLAS批量API来加速它,而不是像他建议的那样编写你自己的自定义内核。

CUBLAS 4.1包含在CUDA Toolkit v4.1

CUBLAS BATCHED GEMM API IMPROVES PERFORMANCE OF BATCHES OF SMALL MATRICES

答案 1 :(得分:2)

问题是cublas等设计用于使用所有SM来乘以大矩阵。那不是你想要的;你想做很多小矩阵乘法。

可能有某种方法可以将其转化为CUBLAS可以为您做好的事情,但我没有看到它。我的建议如下:

编写一个使用一个线程块的内核来乘以两个小矩阵,然后输出结果。

然后用吨块启动内核日志 2 N并成对处理乘法:

  • 步骤1:乘以M 1 x M 2 ,M 3 x M 4 ... M N - 2 x M N-1 ,输出M' 1 ,M' 2 .. M'<子> N / 2
  • 步骤2:乘以M' 1 x M' 2 ,M' 3 x M' 4 ... M' N / 2 - 2 x M N / 2-1 ,输出M'' 1 ,M'' 2 .. M'' N / 4 ...

存在50%的内存开销因素,但我认为您可以通过这种方式更好地利用核心。

<强>更新

好吧,如果你真的不想分阶段这样做,你可以这样做,但它需要更多的编码,与你使用cuBLAS和异步的东西相比,性能可能会更差传递。我假设您使用的是Fermi,并且您已关闭L1缓存,因此每个SM有48K共享内存。

以100x2格式存储100个矩阵,每个块在内存中连续存在。因此matrix[matrixnum,i,j]matricies[matrixnum*100*100 + i*100*50 + j*50*50]开始。请注意,每个块都是50 * 50 * 4字节~10K,因此4可以很好地适应共享内存。

为每个4个线程块分配一个(Nmatricies / Nblocks)矩阵长链,乘以四个中的一个负责每个乘法块。

假设你是第4个中的第1个主题,你要成倍增加的第一个基础是AxB。你负责(1,1)的结果 - (AB) 1,1 = A 1,1 B 1,1 + A 1,2 * B 2,1 。您将A 1,1 预加载到共享内存中的myblock [0]。

  • 在myblock [1]中加载= B 1,1 来自全局内存
  • myblock [3] = myblock [0] * myblock [1](矩阵mult,全部在共享内存中)
  • 在myblock中加载[1] =来自全局
  • 1,2
  • 加载myblock [2] = B 2,1 from global
  • myblock [0] = myblock [3] +(myblock [1] * myblock [2])(矩阵mult和add,全部在共享内存中)。

现在,您可以对链中部分矩阵的其余部分重复此操作,仅在完成时输出。

当你完成后,你最终会得到全局内存中的(#SMs)matricies,它仍然必须成倍增加,但是全局内存中不会有任何额外的临时存储空间,你也不会不得不将数据复制到除原始基质之外的全局存储器以及要处理的那些列表中。

同样,没有真正的理由这样做,除了你不能分阶段将数据发送到GPU,并且性能几乎肯定会更糟;全局内存写入次数较少,但您可能会使用手动GEMM来支付费用。好消息是50不是8的倍数,所以你可能不会有太多的共享内存库冲突。

同样,对于奖励积分,您可以先预先计算所有成对矩阵产品的所有块,然后再列出列表长度的一半。

答案 2 :(得分:0)

LIBXSMM - 针对小型,密集或稀疏矩阵乘法的针对英特尔架构的库,小卷积正是为了利用小矩阵乘法的最佳性能。

与NVidia CUBLAS(或英特尔MKL)相比,LIBXSMM不依赖于批处理界面。相反,可以安排单独的呼叫并且还提供“下一个位置”,即,下一个乘法的操作数/矩阵所在的位置(在存储器中)。优点是不需要描述批处理的显式数据结构或索引格式。

#include <libxsmm.h>

int main()
{
  const libxsmm_gemm_prefetch_type prefetch = LIBXSMM_PREFETCH_AUTO;
  const double alpha = 1.0, beta = 1.0; /* accumulate C-matrix */
  const int m = 23, n = 23, k = 23;     /* some problem size */
  libxsmm_dmmfunction xmm = NULL;       /* function pointer */

  xmm = libxsmm_dmmdispatch(23, 23, 23, /* some problem size */
          NULL/*lda*/, NULL/*ldb*/, NULL/*ldc*/,
          &alpha, &beta, NULL/*flags*/,
          NULL/*&prefetch*/);

  if (xmm) { /* JiT'ted code has been generated */
#   pragma omp parallel for
    for (int i = 0; i < nbatch; ++i) {
      const double *const ai = a + i * asize;
      const double *const bi = b + i * bsize;
      /* e.g., matrix C is accumulated (instead of streamed) */
      double *const ci = c /*+ i * csize*/;
      /* optionally provide "next locations" */
      xmm(ai, bi, ci/*,
          ai + 1 * asize,
          bi + 1 * bsize,
          ci + 0 * csize
      */);
    }
  }
}

LIBXSMM生成高度优化和专用的代码(JiT),它利用最新的指令集扩展(SSE3,AVX,AVX2和AVX-512)。 LIBXSMM是available,属于非许可许可(BSD-3条款)。

注意:这与CUBLAS无关(原先要求)。