为什么BLAS dsyrk比天真的C实现更精确?

时间:2016-10-27 14:16:05

标签: c precision blas

我试图找出BLAS dsyrk对称矩阵乘积A'*A为什么比做同样事情的C例程更精确。

以下是我对此进行测试的方法:我使用以下Python代码使用mpmath包非常精确地计算产品:

#!/usr/bin/env python
# file: make_precise.py

import mpmath

DPS = 100

def write_matrix(A, label):
    fname = './dataset_%s.txt' % label
    with open(fname, 'w') as fid:
        for i in range(A.rows):
            for j in range(A.cols):
                pre = '' if j == 0 else ' '
                fid.write(pre + mpmath.nstr(A[i, j], DPS))
            fid.write('\n')
    print("Created %s" % fname)


def main():
    mpmath.mp.dps = DPS

    A = mpmath.randmatrix(10000, 10)
    write_matrix(A, 'A')

    AA = A.T * A
    write_matrix(AA, 'AA')


if __name__ == '__main__':
    main()

mpmath包使用大约100个小数的精度计算产品A'*A。现在在C中我将使用dsyrk计算的BLAS计算产品的精度与标准C代码中计算的乘积的精度进行比较。 "天真" C代码基于dsyrk.c的第332-350行。我用来比较实现的代码是:

// file: minimal.c
#include <stdio.h>
#include <stdlib.h>
#include <cblas.h>
#include <math.h>

#define N_OBS 10000
#define N_VAR 10

// generate datasets with:
// python make_precise.py

// compile with:
// gcc minimal.c -o minimal -lcblas -lm

void dsyrk_aa(const double *A, double *AA)
{
    cblas_dsyrk(CblasRowMajor, CblasUpper, CblasTrans, N_VAR, N_OBS, 1.0,
            A, N_VAR, 0., AA, N_VAR);
}


void naive_aa(double *A, double *AA)
{
    int i, j, k;
    double temp;

    for (j=0; j<N_VAR; j++) {
        for (k=j; k<N_VAR; k++) {
            temp = 0.0;
            for (i=0; i<N_OBS; i++) {
                temp += A[i*N_VAR+k] * A[i*N_VAR+j];
            }
            AA[j*N_VAR+k] = temp;
        }
    }
}


double *read_data(const char *name, int rows, int cols)
{
    int i, j;
    double value, *M = NULL;

    char filename[1024];
    sprintf(filename, "./dataset_%s.txt", name);
    FILE *fid = fopen(filename, "r");

    M = malloc(sizeof(double)*rows*cols);

    for (i=0; i<rows; i++) {
        for (j=0; j<cols; j++) {
            fscanf(fid, "%lf", &value);
            M[i*cols+j] = value;
        }
    }

    fclose(fid);
    return M;
}


double dist_from_true(double *A, double *B, int rows, int cols)
{
    int i, j;
    double dist = 0.0;
    for (i=0; i<rows; i++)
        for (j=i; j<cols; j++)
            dist += fabs(A[i*cols+j] - B[i*cols+j]);
    return dist;
}


int main()
{
    double d1, d2;
    double *A = NULL,
           *AA = NULL;

    double *AA1 = calloc(N_VAR*N_VAR, sizeof(double));
    double *AA2 = calloc(N_VAR*N_VAR, sizeof(double));

    A = read_data("A", N_OBS, N_VAR);
    AA = read_data("AA", N_VAR, N_VAR);

    dsyrk_aa(A, AA1);
    naive_aa(A, AA2);

    d1 = dist_from_true(AA, AA1, N_VAR, N_VAR);
    d2 = dist_from_true(AA, AA2, N_VAR, N_VAR);

    free(A);
    free(AA);

    free(AA1);
    free(AA2);

    printf("Dsyrk:  \t%.16f\n", d1);
    printf("Naive:  \t%.16f\n", d2);

    return EXIT_SUCCESS;
}

请注意,我只计算两个例程中的上三角形。当然,通过将数据集读入C,我们将失去一些精度,因为所有内容都将存储为double。但是,我们将与使用mpmath计算的真实产品进行比较,因此我们应该能够比较两种产品的精度。我得到的结果是:

Dsyrk:      0.0000000000923137
Naive:      0.0000000003306013

因此,对于BLAS,绝对误差比C实现小约3倍。这对于多个数据集和多个工作站(都运行Linux)是可重现的。我知道差异似乎可以忽略不计,但我处理的是较大的数据集,其中误差会随着时间的推移而累积。

我的问题是:这种差异来自哪里,我能做些什么来使C实现与BLAS实现一样精确?

提前感谢您的时间!

更新

我重新编译了ATLAS,以查看编译dsyrk例程时使用的编译器标志。我把它缩小到这条线:

/usr/bin/x86_64-pc-linux-gnu-gcc-6.2.1 -o ATL_drefsyrkLN.o -c -DL2SIZE=33554432 -I/tmp/ATLAS/build/include -I/tmp/ATLAS/build/..//include -I/tmp/ATLAS/build/..//include/contrib -DAdd_ -DF77_INTEGER=int -DStringSunStyle -DATL_OS_Linux -DATL_ARCH_Corei4 -DATL_CPUMHZ=3200 -DATL_AVXMAC -DATL_AVX -DATL_SSE3 -DATL_SSE2 -DATL_SSE1 -DATL_USE64BITS -DATL_GAS_x8664 -m64 -DATL_DYLIBS -DPentiumCPS=3200.000 -DATL_FULL_LAPACK -DATL_NCPU=4 -fomit-frame-pointer -mfpmath=sse -O2 -mavx2 -mfma -fPIC -m64 -fPIC /tmp/ATLAS/build/..//src/blas/reference/level3/ATL_drefsyrkLN.c

我认为重要的标志是:

-m64 -fomit-frame-pointer -mfpmath=sse -O2 -mavx2 -mfma -fPIC

但是,在编译上面的最小示例时使用:

gcc -o minimal -m64 -fomit-frame-pointer -mfpmath=sse -O2 -mavx2 -mfma -fPIC minimal.c -lcblas -lm

精度结果不受影响。非常感谢任何帮助。

2 个答案:

答案 0 :(得分:1)

BLAS是一套极其优化的例程,作者,贡献者和用户在高度尊重的同时保持速度和精度也就不足为奇了。

通过复制(C)BLAS代码,您可以使您自己的实现完全一样精确。请考虑许可证,这不是您的代码。

通常差异来自特定的操作顺序,编译器选项等......没有直接的方法可以指导您获得更好的精度一般

答案 1 :(得分:1)

发布解决方案作为未来参考的答案。 @rubenvb的提示结果证明是有用的。我通过cblas_dsyrk例程跟踪了ATLAS库中的确切路径。具体如下:

  1. cblas_dsyrk
  2. ATL_syrk
  3. ATL_dsprk
  4. ATL_dsprk_rK
  5. ATL_dprk_kmm
  6. ATL_gNBmm
  7. ATL_dpKBmm
  8. ATL_dgpKBmm
  9. ATL_dJIK0x0x56TN56x56x0_a1_bX
  10. ATL_dJIK0x0x56TN1x1x4_a1_bX(完成所有工作)
  11. 现在,&#34;块大小&#34; ATLAS用于我的CPU是NB = 56。为了便于测试,我因此将测试数据集减少为此的倍数(上面minimal.c中的N_OBS = 9968)。如果不是这种情况,那么将为剩余的行调用类似于列表中最后两个的一些例程。

    随着最终功能的追踪,我开始测试结果矩阵AA的单个元素的值。我注意到对ATL_dprk_kmm的调用实际上执行了三次,初始A矩阵的块(行)不同。这些块的大小分别为3472,3472和3024行。对于这些块中的每一个,构造单独的结果矩阵AA,其最终被添加到用户的AA矩阵。这立即导致了解决方案,在朴素函数中我们也在块中计算AA

    void newnaive_aa(const double *A, double *AA)
    {
        int i, j, k, blk_start, blk_end;
        double temp;
        double *tmpAA = calloc(N_VAR*N_VAR, sizeof(double));
    
        // block 1
        blk_start = 0; blk_end = 3472;
        for (j=0; j<N_VAR; j++) {
            for (k=j; k<N_VAR; k++) {
                temp = 0.0;
                for (i=blk_start; i<blk_end; i++) {
                    temp += A[i*N_VAR+k] * A[i*N_VAR+j];
                }
                tmpAA[j*N_VAR+k] = temp;
            }
        }
        // copy matrix over
        for (j=0; j<N_VAR*N_VAR; j++)
            AA[j] += tmpAA[j];
    
        // block 2
        blk_start = 3472; blk_end = 6944;
        for (j=0; j<N_VAR; j++) {
            for (k=j; k<N_VAR; k++) {
                temp = 0.0;
                for (i=blk_start; i<blk_end; i++) {
                    temp += A[i*N_VAR+k] * A[i*N_VAR+j];
                }
                tmpAA[j*N_VAR+k] = temp;
            }
        }
        // copy matrix over
        for (j=0; j<N_VAR*N_VAR; j++)
            AA[j] += tmpAA[j];
    
        // block 3
        blk_start = 6944; blk_end = 9968;
        for (j=0; j<N_VAR; j++) {
            for (k=j; k<N_VAR; k++) {
                temp = 0.0;
                for (i=blk_start; i<blk_end; i++) {
                    temp += A[i*N_VAR+k] * A[i*N_VAR+j];
                }
                tmpAA[j*N_VAR+k] = temp;
            }
        }
        // copy matrix over
        for (j=0; j<N_VAR*N_VAR; j++)
            AA[j] += tmpAA[j];
    
        free(tmpAA);
    }
    

    使用这个新实现,获得与dsyrk调用完全相同的精度。

    另请注意,对于包含较少行数(例如N_OBS = 1000)的数据集,原始naive_aa的精度已与dsyrk结果的精度相同。之所以这样做是显而易见的,因为该数据集大小不需要中间结果。