英特尔/AMD CPU矩阵乘法的最大FLOPS是多少?

3

我用于估算英特尔CPU最大FLOPs/s的公式是

Max SP FLOPs/s = frequencey * 4 SSE(8AVX) * 2 (MAC) * number of cores (not HW threads)
Max DP FLOPs/s = 0.5 * Max SP FLOPs/s

我所说的MAC是指CPU可以同时执行一次SSE(AVX)乘法和加法。在我使用的系统上,负载下的最大频率为2.66 GHz。它只有SSE指令集,并且核心数(不是硬件线程数)为4。这样就得到了:最大SP FLOPs/s = 85.12 GFLOPs/s。
矩阵乘法的FLOPs数量大约为2*n*m*k。对于n=1000的方阵,这是2*10E9个FLOPs(20亿个FLOPs)。一旦我知道时间,我就可以估算FLOPs/s。
然而,我自己的代码最好的结果只有大约40 SP GFLOPs/s,例如n=1000。我用Eigen得到的结果也差不多。这大约是45%的效率。我的最大计算是否有误?对于大型稠密矩阵乘法,英特尔CPU的最佳效率是多少?有没有一篇论文描述这一点?
我知道在GPU上效率可以达到60%以上。 http://www.anandtech.com/show/6774/nvidias-geforce-gtx-titan-part-2-titans-performance-unveiled/3 编辑: 当n=500时,我得到了类似的结果,这很容易适配到我的系统的12MB L3缓存中,因此缓存似乎不是限制因素(虽然也许我可以更有效地使用它)。
编辑2: Eigen基准测试显示其与MKL一样好(对于SSE)。 它们使用Intel(R)Core(TM)2 Quad CPU Q9400 @ 2.66GHz。 因此,2.66 * 2(DP SSE)* 2 MAC * 4个核心= 42.25 DP GFLOPs / s。 您可以在图表上看到它们都少于20。 像我这样的大约有45%。 http://eigen.tuxfamily.org/index.php?title=Benchmark

http://ark.intel.com/products/35365/Intel-Core2-Quad-Processor-Q9400-6M-Cache-2_66-GHz-1333-MHz-FSB

编辑3: 这是我的代码,供有兴趣的人参考。我可以获得比这更好一点的结果,但不会好多少。我正在使用Agner Fog的vectorclass进行SEE/AVX优化,并将Vec8f设置为float8和Vec4d设置为double4。

//SGEMM and AVX call MM_tile<float, float8>(nthreads, a, b, c, n, m, k);
template <typename ftype, typename floatn>
void GEMM_tile(const int nthreads, const ftype*A , const ftype* B, ftype* C, const int N, const int M, const int K) {       
    for(int i=0; i<N; i++) {
       for(int j=0; j<K; j++) {
           C[K*i + j] = 0;
       }
    }   
    const int nc = 32;
    const int kc = 32;
    const int mc = 32;
    omp_set_num_threads(nthreads);
    #pragma omp parallel for if(nthreads>1)
    for(int ii=0; ii<N; ii+=nc) {
        for(int jj=0; jj<K; jj+=kc)
            for(int ll=0; ll<M; ll+=mc) {
                const int nb = min(N-ii, nc);
                const int kb = min(K-jj, kc);
                const int mb = min(M-ll, mc);
                MM_block<ftype, floatn>(nb, mb, kb, &A[M*ii+ll], N, &B[K*ll+jj], K, &C[K*ii+jj], K );
            }
     }
}

template <typename ftype, typename floatn>
void MM_block(int n, int m, int k, const ftype *a, const int stridea, 
                                   const ftype *b, const int strideb,
                                   ftype *c, const int stridec ) {
    const int vec_size = sizeof(floatn)/sizeof(ftype);
    for(int i=0; i<n; i+=4) {
        for(int j=0; j<k; j+=vec_size) {
            Dot4x4_vec_block<ftype, floatn>(m, &a[strideb*i], &b[j], &c[stridec*i + j], stridea, strideb, stridec);
    }
}

template <typename ftype, typename floatn>
inline void Dot4x4_vec_block(const int n, const ftype *a, const ftype *b, ftype *c, const int stridea, const int strideb, const int stridec) {
    floatn tmp0, tmp1, tmp2, tmp3;
    load(tmp0, &c[stridec*0]);
    load(tmp1, &c[stridec*1]);
    load(tmp2, &c[stridec*2]);
    load(tmp3, &c[stridec*3]);

    ftype *a0_ptr = (ftype*)&a[stridea*0];
    ftype *a1_ptr = (ftype*)&a[stridea*1];
    ftype *a2_ptr = (ftype*)&a[stridea*2];
    ftype *a3_ptr = (ftype*)&a[stridea*3];
    for(int i=0; i<n; i++) {
        floatn breg = floatn().load(&b[i*strideb + 0]);

        floatn areg0 = *a0_ptr++;
        floatn areg1 = *a1_ptr++;
        floatn areg2 = *a2_ptr++;
        floatn areg3 = *a3_ptr++;

        tmp0 += areg0 * breg;
        tmp1 += areg1 * breg;
        tmp2 += areg2 * breg;
        tmp3 += areg3 * breg;
}
    tmp0.store(&c[stridec*0]);
    tmp1.store(&c[stridec*1]);
    tmp2.store(&c[stridec*2]);
    tmp3.store(&c[stridec*3]);
}

稍后我会写更多内容,但是好的BLAS实现在现代x86硬件上渐近地达到峰值的约90%。 MKL和GotoBLAS都是很好的参考点。 超过大约60%需要仔细调整内部循环,并考虑指令延迟和端口利用率。 通常需要编写汇编代码,除非您碰巧遇到编译器处理得非常好的内在习惯用语。 - Stephen Canon
谢谢,90%正是我所期望的。我添加了一些有关Eigen与MKL的文本。他们声称它与MKL一样好(对于SSE)。有趣的是,我的公式预测超过40 DP GLOPs/s,而他们都没有达到20。也许我的公式乘以2的因素有误? - user2088790
我找到了一些用于分析MKL的代码。他们有一个i7-2600K的图表。他们得到了90 DP GFLOPs/s。而我在家里的2600K上只得到了50 DP GFLPS/s。这告诉我我的公式是正确的,而我的代码(以及Eigen的代码)是低效的。 http://software.intel.com/en-us/articles/a-simple-example-to-measure-the-performance-of-an-intel-mkl-function - user2088790
1个回答

0
通常,处理吞吐量的限制因素是内存带宽,特别是在您的工作集不适合CPU缓存的情况下(您的1000x1000浮点矩阵将占用约4 MB,而您的CPU可能具有2 MB的L3缓存)。这是一种情况,您的算法结构可以对其性能产生很大影响,但通常会遇到瓶颈,因为您正在等待来自内存层次结构中某个更高级别的值。
此外,您的理论数字假定您具有足够的指令而没有数据依赖关系,以使所有执行单元在每个周期上都得到任务。在实践中,这可能非常困难。我不确定一般矩阵乘法的最佳吞吐量是多少,但请查看this previous question以获取有关如何最大化指令吞吐量的信息。

我正在使用循环分块/阻塞来处理缓存。我听说可以使用多个级别的阻塞来处理L1、L2、L3。但我还没有这样做。我使用循环展开来处理数据依赖关系。总之,我的结果与Eigen差不多,我认为它是SSE(而不是AVX)的最先进技术。因此,如果我的代码效率低下,那么Eigen的效率也是如此。 - user2088790
我觉得您的代码并没有比人们期望的效率低很多。听起来您已经做了正确的事情,确保算法尽可能地友好于内存。 - Jason R
一个正确平铺的矩阵乘法算法在现代架构(包括x86)上从不受内存带宽限制。性能的限制因素是原始计算吞吐量。 - Stephen Canon
谢谢,这正是我所想的。我认为这是因为矩阵乘法是n^3阶,所以它花费更多的时间处理数据而不是读取数据,因此限制因素应该是处理能力。 - user2088790

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接