繁体   English   中英

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

[英]Doing multiple matrix-matrix multiplications in one operation

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

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如何运作? 如果代码在某处可用?

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

CUBLAS 4.1包含在CUDA Toolkit v4.1中

CUBLAS BATCHED GEMM API提高了小矩阵批次的性能

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

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

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

然后使用数量的块启动内核log 2 N并成对处理乘法:

  • 步骤1:乘M 1 X M 2,M 3×M4 ...,M N - 2×M N-1,输出M '1,M' 2 .. M” N / 2
  • 步骤2:乘以M '1×M' 2,M '3×M' 4 ... M” N / 2 - 2×MN / 2-1,输出M'1,M'2 ..中号'' N / 4 ......

等等

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

更新

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

以100x2格式存储100个矩阵,每个块在内存中连续。 因此matrix[matrixnum,i,j]matricies[matrixnum*100*100 + i*100*50 + j*50*50]开始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来自全球
  • myblock [0] = myblock [3] +(myblock [1] * myblock [2])(矩阵mult和加法,全部在共享内存中)。

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

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

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

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

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是可用的下一个非允许许可证(BSD-3子句)。

注意:这不是CUBLAS(最初要求的)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM