我试图找出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
精度结果不受影响。非常感谢任何帮助。
答案 0 :(得分:1)
BLAS是一套极其优化的例程,作者,贡献者和用户在高度尊重的同时保持速度和精度也就不足为奇了。
通过复制(C)BLAS代码,您可以使您自己的实现完全一样精确。请考虑许可证,这不是您的代码。
通常差异来自特定的操作顺序,编译器选项等......没有直接的方法可以指导您获得更好的精度一般。
答案 1 :(得分:1)
发布解决方案作为未来参考的答案。 @rubenvb的提示结果证明是有用的。我通过cblas_dsyrk
例程跟踪了ATLAS库中的确切路径。具体如下:
现在,&#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
结果的精度相同。之所以这样做是显而易见的,因为该数据集大小不需要中间结果。