CUDA编程(十)

使用Kahan’s Summation Formula提高精度

上一次我们准备去并行一个矩阵乘法。然后我们在GPU上完毕了这个程序,当然是非常单纯的把任务分配给各个线程。也没有经过优化。终于我们看到,执行效率相当的低下,可是更重要的是出现了一个我们之前做整数立方和没遇到的问题,那就是浮点数精度损失的问题。

关注GPU运算的精度问题:

在程序的最后。我们计算了精度误差,发现最大相对误差偏高,而一般理想上应该要低于 1e-6。

我们之前将评估CUDA程序的时候也提过了。精度是CUDA程序须要重点评估的一个点,那么我们该怎样解决问题呢?我们先分析一下原因。

出现精度问题的解决办法:

事实上计算结果的误差偏高的原因非常简单。在 CPU 上进行计算时,我们使用 double(即 64 bits 浮点数)来累进计算过程。而在 GPU 上则仅仅能用 float(32 bits 浮点数)。

在累加大量数字的时候,由于累加结果非常快会变大。因此后面的数字非常easy被舍去过多的位数。

这里可能说的不是非常清楚。看完以下这个样例就清楚了。

浮点数的大数吃小数问题:

浮点数的精度:

大家应该非常清楚,浮点数在内存中是按科学计数法来存储的,分为符号位,指数位。和尾数位。

float和double各段的位数各自是:

float:

1bit(符号位) 8bits(指数位) 23bits(尾数位)

double:

1bit(符号位) 11bits(指数位) 52bits(尾数位)

float和double的精度是由尾数的位数来决定的:

float: 2^23 = 8388608。一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字。

double: 2^52 = 4503599627370496,一共16位,同理。double的精度为15~16位。

大数吃小数:

float由于位数相较于double要短不少,所以非常easy出现大数吃小数的问题:

比方我们用两个float相加:

#include <stdio.h>

int main()
{
float a = 100998;
float b = 2.338; a = a + b; printf("the sum is %f", a); }

a+b 应该等于 101000.338,前面说了float的精度有6~7位,所以38可能会被截掉,3不一定,可是8必定会被截掉。我们能够实际输出一下看看:

结果是:the sum is 101000.335938

由于%f是输出double类型。能够看到转换后8这位已经没了,33是正常的。

从这里能够看到一个加法过程就没了0.008,要是加1000次。一个整8就没了。

这就是大数吃小数问题。

Kahan’s Summation Formula:

如今我们就要想办法解决问题了,我们看到标题中这个看起来非常高大上的名字,这个也叫作kahan求和算法,我们接下来就要用kahan求和来避免这样的精度损失的情况。

名字非常高大上,可是原理非常小儿科,小学生也知道,缺的我们想办法再补回来:

所以我们用一个temp变量来记住损失掉的部分,等下次加法的时候再加回去就好了。

temp= (a+b)-a-b; 在上面那个问题中 temp = -0.008,在下次计算的时候加和到下一个加数就能够一定程度的减小误差。

Kahan’s Summation Formula伪代码:

function KahanSum(input)
var sum = 0.0
var c = 0.0 //A running compensation for lost low-order bits.
for i = 1 to input.length do
y = input[i] - c //So far, so good: c is zero.
t = sum + y //Alas, sum is big, y small, so low-order digits of y are lost.
c = (t - sum) - y //(t - sum) recovers the high-order part of y; subtracting y recovers -(low part of y)
sum = t //Algebraically, c should always be zero. Beware eagerly optimising compilers!
//Next time around, the lost low part will be added to y in a fresh attempt.
return sum

提高矩阵乘法的精度:

看着伪代码比着葫芦画瓢还是比較简单的,我们仅仅须要更改核函数中的加和部分就可以:

原版

    //计算矩阵乘法
if (row < n && column < n)
{
float t = 0; for (i = 0; i < n; i++)
{
t += a[row * n + i] * b[i * n + column];
}
c[row * n + column] = t;
}

改版

    //计算矩阵乘法
if (row < n && column < n)
{
float t = 0;
float y = 0; for (i = 0; i < n; i++)
{
float r; y -= a[row * n + i] * b[i * n + column];
r = t - y;
y = (r - t) + y;
t = r;
}
c[row * n + column] = t;
}

完整程序:

#include <stdio.h>
#include <stdlib.h>
#include <time.h> //CUDA RunTime API
#include <cuda_runtime.h> #define THREAD_NUM 256 #define MATRIX_SIZE 1000 const int blocks_num = MATRIX_SIZE*(MATRIX_SIZE + THREAD_NUM - 1) / THREAD_NUM; //打印设备信息
void printDeviceProp(const cudaDeviceProp &prop)
{
printf("Device Name : %s.\n", prop.name);
printf("totalGlobalMem : %d.\n", prop.totalGlobalMem);
printf("sharedMemPerBlock : %d.\n", prop.sharedMemPerBlock);
printf("regsPerBlock : %d.\n", prop.regsPerBlock);
printf("warpSize : %d.\n", prop.warpSize);
printf("memPitch : %d.\n", prop.memPitch);
printf("maxThreadsPerBlock : %d.\n", prop.maxThreadsPerBlock);
printf("maxThreadsDim[0 - 2] : %d %d %d.\n", prop.maxThreadsDim[0], prop.maxThreadsDim[1], prop.maxThreadsDim[2]);
printf("maxGridSize[0 - 2] : %d %d %d.\n", prop.maxGridSize[0], prop.maxGridSize[1], prop.maxGridSize[2]);
printf("totalConstMem : %d.\n", prop.totalConstMem);
printf("major.minor : %d.%d.\n", prop.major, prop.minor);
printf("clockRate : %d.\n", prop.clockRate);
printf("textureAlignment : %d.\n", prop.textureAlignment);
printf("deviceOverlap : %d.\n", prop.deviceOverlap);
printf("multiProcessorCount : %d.\n", prop.multiProcessorCount);
} //CUDA 初始化
bool InitCUDA()
{
int count; //取得支持Cuda的装置的数目
cudaGetDeviceCount(&count); if (count == 0)
{
fprintf(stderr, "There is no device.\n"); return false;
} int i; for (i = 0; i < count; i++)
{ cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, i);
//打印设备信息
printDeviceProp(prop); if (cudaGetDeviceProperties(&prop, i) == cudaSuccess)
{
if (prop.major >= 1)
{
break;
}
}
} if (i == count)
{
fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
return false;
} cudaSetDevice(i); return true; } //生成随机矩阵
void matgen(float* a, int n)
{
int i, j; for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
{ a[i * n + j] = (float)rand() / RAND_MAX + (float)rand() / (RAND_MAX * RAND_MAX); }
}
} // __global__ 函数 并行计算矩阵乘法
__global__ static void matMultCUDA(const float* a, const float* b, float* c, int n, clock_t* time)
{ //表示眼下的 thread 是第几个 thread(由 0 開始计算)
const int tid = threadIdx.x; //表示眼下的 thread 属于第几个 block(由 0 開始计算)
const int bid = blockIdx.x; //从 bid 和 tid 计算出这个 thread 应该计算的 row 和 column
const int idx = bid * THREAD_NUM + tid;
const int row = idx / n;
const int column = idx % n; int i; //记录运算開始的时间
clock_t start; //仅仅在 thread 0(即 threadIdx.x = 0 的时候)进行记录,每一个 block 都会记录開始时间及结束时间
if (tid == 0) time[bid] = clock(); //计算矩阵乘法
if (row < n && column < n)
{
float t = 0; //temp变量
float y = 0; for (i = 0; i < n; i++)
{
float r; y -= a[row * n + i] * b[i * n + column];
r = t - y;
y = (r - t) + y;
t = r;
}
c[row * n + column] = t;
} //计算时间,记录结果。仅仅在 thread 0(即 threadIdx.x = 0 的时候)进行,每一个 block 都会记录開始时间及结束时间
if (tid == 0)
{
time[bid + blocks_num] = clock();
}
} int main()
{ //CUDA 初始化
if (!InitCUDA()) return 0; //定义矩阵
float *a, *b, *c, *d; int n = MATRIX_SIZE; //分配内存
a = (float*)malloc(sizeof(float)* n * n);
b = (float*)malloc(sizeof(float)* n * n);
c = (float*)malloc(sizeof(float)* n * n);
d = (float*)malloc(sizeof(float)* n * n); //设置随机数种子
srand(0); //随机生成矩阵
matgen(a, n);
matgen(b, n); /*把数据拷贝到显卡内存中*/
float *cuda_a, *cuda_b, *cuda_c; clock_t* time; //cudaMalloc 取得一块显卡内存
cudaMalloc((void**)&cuda_a, sizeof(float)* n * n);
cudaMalloc((void**)&cuda_b, sizeof(float)* n * n);
cudaMalloc((void**)&cuda_c, sizeof(float)* n * n);
cudaMalloc((void**)&time, sizeof(clock_t)* blocks_num * 2); //cudaMemcpy 将产生的矩阵拷贝到显卡内存中
//cudaMemcpyHostToDevice - 从内存拷贝到显卡内存
//cudaMemcpyDeviceToHost - 从显卡内存拷贝到内存
cudaMemcpy(cuda_a, a, sizeof(float)* n * n, cudaMemcpyHostToDevice);
cudaMemcpy(cuda_b, b, sizeof(float)* n * n, cudaMemcpyHostToDevice); // 在CUDA 中执行函数 语法:函数名称<<<block 数目, thread 数目, shared memory 大小>>>(參数...);
matMultCUDA << < blocks_num, THREAD_NUM, 0 >> >(cuda_a, cuda_b, cuda_c, n, time); /*把结果从显示芯片复制回主内存*/ clock_t time_use[blocks_num * 2]; //cudaMemcpy 将结果从显存中复制回内存
cudaMemcpy(c, cuda_c, sizeof(float)* n * n, cudaMemcpyDeviceToHost);
cudaMemcpy(&time_use, time, sizeof(clock_t)* blocks_num * 2, cudaMemcpyDeviceToHost); //Free
cudaFree(cuda_a);
cudaFree(cuda_b);
cudaFree(cuda_c);
cudaFree(time); //把每一个 block 最早的開始时间。和最晚的结束时间相减。取得总执行时间
clock_t min_start, max_end; min_start = time_use[0]; max_end = time_use[blocks_num]; for (int i = 1; i < blocks_num; i++)
{
if (min_start > time_use[i]) min_start = time_use[i]; if (max_end < time_use[i + blocks_num]) max_end = time_use[i + blocks_num];
} //核函数执行时间
clock_t final_time = max_end - min_start; //CPU矩阵乘法,存入矩阵d
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
double t = 0; for (int k = 0; k < n; k++)
{ t += a[i * n + k] * b[k * n + j]; } d[i * n + j] = t; }
} //验证正确性与精确性 float max_err = 0; float average_err = 0; for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (d[i * n + j] != 0)
{
//fabs求浮点数x的绝对值
float err = fabs((c[i * n + j] - d[i * n + j]) / d[i * n + j]); if (max_err < err) max_err = err; average_err += err;
}
}
} printf("Max error: %g Average error: %g\n", max_err, average_err / (n * n)); printf("gputime: %d\n", final_time); return 0; }

执行结果:

我们看到结果还是效果还是非常不错的,我们上次的结果是:

Max error:2.07589e-006

Average error :3.3492e-007

gpu time:189967999

而眼下的结果是:

Max error:1.19206e-007

Average error :7.70641e-010

gpu time:210779939

我们能够看到准确度确实有了非常大的提升,当然效率还是一如既往地慢,只是我们至少把精度问题给攻克了。

总结:

之前我们用CUDA完毕了矩阵乘法,可是当然会存在非常多问题,除了速度问题。GPU浮点数运算的精度也非常差,本篇博客从出现误差的原理(浮点数大数吃小数)分析,使用了Kahan’s Summation Formula在一定程度上攻克了CUDA运算float精度不够的情况。接下来我们会着手去解决速度问题~

希望我的博客能帮助到大家~

參考资料:《深入浅出谈CUDA》

CUDA编程(十)使用Kahan&#39;s Summation Formula提高精度的更多相关文章

  1. 【CUDA开发】CUDA编程接口(一)------一十八般武器

    子曰:工欲善其事,必先利其器.我们要把显卡作为通用并行处理器来做并行算法处理,就得知道CUDA给我提供了什么样的接口,就得了解CUDA作为通用高性能计算平台上的一十八般武器.(如果你想自己开发驱动,自 ...

  2. 不同版本CUDA编程的问题

    1 无法装上CUDA的toolkit 卸载所有的NVIDIA相关的app,包括NVIDIA的显卡驱动,然后重装. 2之前的文件打不开,one or more projects in the solut ...

  3. CUDA编程之快速入门

    CUDA(Compute Unified Device Architecture)的中文全称为计算统一设备架构.做图像视觉领域的同学多多少少都会接触到CUDA,毕竟要做性能速度优化,CUDA是个很重要 ...

  4. 详解CUDA编程

    CUDA 是 NVIDIA 的 GPGPU 模型,它使用 C 语言为基础,可以直接以大多数人熟悉的 C 语言,写出在显示芯片上执行的程序,而不需要去学习特定的显示芯片的指令或是特殊的结构.” 编者注: ...

  5. CUDA编程之快速入门【转】

    https://www.cnblogs.com/skyfsm/p/9673960.html CUDA(Compute Unified Device Architecture)的中文全称为计算统一设备架 ...

  6. cuda编程基础

    转自: http://blog.csdn.net/augusdi/article/details/12529247 CUDA编程模型 CUDA编程模型将CPU作为主机,GPU作为协处理器(co-pro ...

  7. CUDA学习笔记(一)——CUDA编程模型

    转自:http://blog.sina.com.cn/s/blog_48b9e1f90100fm56.html CUDA的代码分成两部分,一部分在host(CPU)上运行,是普通的C代码:另一部分在d ...

  8. CUDA编程

    目录: 1.什么是CUDA 2.为什么要用到CUDA 3.CUDA环境搭建 4.第一个CUDA程序 5. CUDA编程 5.1. 基本概念 5.2. 线程层次结构 5.3. 存储器层次结构 5.4. ...

  9. CUDA编程-(1)Tesla服务器Kepler架构和万年的HelloWorld

    结合CUDA范例精解以及CUDA并行编程.由于正在学习CUDA,CUDA用的比较多,因此翻译一些个人认为重点的章节和句子,作为学习,程序将通过NVIDIA K40服务器得出结果.如果想通过本书进行CU ...

随机推荐

  1. vue 源码自问自答-响应式原理

    vue 源码自问自答-响应式原理 最近看了 Vue 源码和源码分析类的文章,感觉明白了很多,但是仔细想想却说不出个所以然. 所以打算把自己掌握的知识,试着组织成自己的语言表达出来 不打算平铺直叙的写清 ...

  2. 学习PyQuery库

    学习PyQuery库 好了,又是学习的时光啦,今天学习pyquery 来进行网页解析 常规导入模块(PyQuery库中的pyquery类) from pyquery import PyQuery as ...

  3. 【HDU 2028】Lowest Common Multiple Plus

    Problem Description 求n个数的最小公倍数. Input 输入包含多个测试实例,每个测试实例的开始是一个正整数n,然后是n个正整数. Output 为每组测试数据输出它们的最小公倍数 ...

  4. Mac系统下VirtualBox装Centos7设置静态IP并连网

    用Virtualbox装了三台Centos7,现在需要设置成三台之间可以相互通信,并且三台都可以连外网. 需求如下: 1. 三台内部相互通信 2. 可以上外网 3. 主机可以虚拟机可以相互通信(she ...

  5. 【07】Firebug监控网络情况

    [07] Firebug监控网络情况 Firebug可以监控网页中每个文件加载的时间. 打开Firebug.点击"网络",然后确定已经启用,重新载入当前页面.Firebug显示如下 ...

  6. PS学习笔记(04)

    Photoshop滤镜的安装 Photoshop滤镜的默认格式为.8bf(也有些滤镜为exe格式的可执行文件),如果你下载的是压缩包,请解压之后再安装. 方法一: 如果你下载的滤镜为exe的可执行文件 ...

  7. 大数据学习——flume日志分类采集汇总

    1. 案例场景 A.B两台日志服务机器实时生产日志主要类型为access.log.nginx.log.web.log 现在要求: 把A.B 机器中的access.log.nginx.log.web.l ...

  8. 大数据学习——hadoop2.x集群搭建

    1.准备Linux环境 1.0先将虚拟机的网络模式选为NAT 1.1修改主机名 vi /etc/sysconfig/network NETWORKING=yes HOSTNAME=itcast ### ...

  9. bzoj1086 [SCOI2005]王室联邦 树分块

    [bzoj1086][SCOI2005]王室联邦 2014年11月14日2,6590 Description “余”人国的国王想重新编制他的国家.他想把他的国家划分成若干个省,每个省都由他们王室联邦的 ...

  10. react.js 父子组件数据绑定实时通讯

    import React,{Component} from 'react' import ReactDOM from 'react-dom' class ChildCounter extends Co ...