课程总结 -- CPU/GPU并行计算基础(CPU篇)

原创 2016年01月25日 04:13:24

上学期选修了Prof. Tolga Soyata的“GPU Parallel Programming using C/C++”课程。该课程主要分两部分:前半部分通过讲解CPU并行程序来介绍并行计算的原理和思路;后半部分讲解如何用CUDA在GPU上进行并行计算。本文纪录其中的基础要点和关键实现方式。通过本文大家可以了解到:1、如何用CPU进行多线程并行计算;2、CUDA是什么,GPU如何进行并行计算;3、并行计算的优越性;4、内存的应用对程序性能的影响。

本文分两部分:第一部分是CPU计算部分,第二部分是GPU计算部分。其中主要介绍实现的具体方法(理论也讲不清楚)。内容参考课上课件和代码,以及教材:CUDA BY EXAMPLE


0、串行程序

在接下来的几个小节,我们会用不同的方式完成同一个任务:纵向翻转一张3200x1600的图片,并比较它们之间的性能差异。首先我们来看的是采用串行方法的实现,即不使用并行计算,对逐个像素进行交换操作。为了简化读写等无关操作,我们用opencv进行图像的读写,具体代码见这里

性能:使用串行计算,其速度为平均每张图片约 107ms。

1、第一个CPU并行程序

分析一下这个任务,我们可以发现:每一行执行的操作是独立且相同的。因此我们其实可以让不同行同时执行同样的操作,这便是并行的程序。具体来说,假设我们有n个并行的线程,我们便可以让每个线程完成1/n的任务,比如线程1完成[1...colsn]列,线程2完成[(colsn+1)...2colsn]列,以此类推。理想状况下,通过n个线程同时工作,我们可以将程序的运行时间降低n倍。

利用CPU实现上述并行程序需要用到pthread。Pthread是线程的POSIX标准,定义了创建和操作线程的一套API。以下将一些重要的核心代码列出,完整代码可见这里

  • 引用pthread库及声明需要用到的变量
#include <pthread.h>
#define MAX_THREADS 64

long  NumThreads;    // number of threads work in parallel
int ThParam[MAX_THREADS];     // store thread parameters
pthread_t ThHandle[MAX_THREADS];    //store thread handles
pthread_attr_t ThAttr;    // pthread attribute
  • pthread初始化
pthread_attr_init(&ThAttr);
pthread_attr_setdetachstate(&ThAttr, PTHREAD_CREATE_JOINABLE);
  • 分配线程和结束
       for (i=0; i<NumThreads; i++) {
            ThParam[i] = i;
            // important!! lauch threads
            ThErr = pthread_create(&ThHandle[i], &ThAttr, MFlip, (void *)&ThParam[i]);
            if (ThErr != 0) {
                printf("\nThread Creation Error %d.\n", ThErr);
                exit(EXIT_FAILURE);
            }
        }

        for (i=0; i<NumThreads; i++) {
            // important!! join all threads
            pthread_join(ThHandle[i], NULL);
        }
  • 根据线程ID决定改线程开始执行的列数,及该线程退出语句
long ts = *((int *) tid);    // thread ID
ts *= rows / NumThreads;    // start index
...
pthread_exit(NULL);

并行程序的性能如下:

线程数 1 2 4 8 16 32 64
时间(ms) 135.54 91.72 52.18 27.99 15.13 13.62 14.40

从上表可以看出:

  • 线程数为1时,由于增加了调用pthread的一些开销,程序比上一小节的会慢一些
  • 逐步增加线程数,可以看到接近线性的加速(增加两倍线程数,时间减少两倍),从中可以看到并行程序的有效性
  • 当线程数增加到一定程度后(上表为32),速度提升会遇到瓶颈,即不能永远的达到线性加速。其中的原因比较复杂(其实是我没完全听懂。。),可能涉及内存的共享、系统CPU核的数量、调度等问题,在此不妄加评论了。

2、改善并行程序

在这个小节中,我们会看到合理的内存应用和程序执行方式会对程序性能造成多大的影响。

我们先来分析一下上一小节的并行程序。我们将数据按照线程数量分割成等量的小块,并让不同线程同时执行各个小部分,这种并行方式可以成为数据并行。这种并行方式的好处是每个线程执行的任务都是相同的,只需要标记不同的处理位置。但是这样就足够了吗?并非如此。在上述程序中,我们没有考虑过程序的内存访问模式(Memory Access Pattern)。

毫无疑问,MFlip() 函数是一个内存密集型(memory-intensive)的函数。因为它对每个像素并没有进行任何计算,但是需要频繁的对内存进行读取和写入(为了交换像素值)。对于这一类的函数,其内存访问模式会大大地影响程序的性能。

从直观上我们便能体会到上节中程序的内存访问模式有多么糟糕。每次交换两个像素值,程序都要从内存中读取两个不同位置的数据,并且这两个位置相隔非常的远。而且每次从内存中读取一个像素的值(1比特)也是非常浪费资源的做法。再加上多线程同时进行不同位置的内存读写操作,会使程序的内存访问变得非常低效。从理论上讲,为了达到好的DRAM读写性能,我们需要遵守以下几条规则(为了描述的准确性直接上原文):
DRAM

对比一下我们可以发现,上节的程序违反了Granularity的规则,即每次都读写过于少量的数据,造成读写时资源的浪费。另外,上节的程序并没有利用好缓存(cache memory),因为它从来没有进行数据的复用。

因此,我们来改进上节的程序,使其能满足上述的几点规则。从上表可以看到,虽然图片数据存储在DRAM中,我们并不希望频繁地对它进行访问。因此我们可以一次把一行图片的数据读取到一个临时缓冲区中,再对其中的数据进行处理。这样的好处不仅能够减少对DRAM的访问,还能充分利用L1缓存的作用对读取的数据进行重复利用。核心实现代码如下,整体代码在这里

  • 准备两个缓冲区用来存放两行的数据
unsigned char Buffer1[16384];    // This is the buffer to get the first row of image;
unsigned char Buffer2[16384];    // This is the buffer to get the second row of image;
  • 读取数据进入缓冲区
// important!! copy data to cache memory
memcpy((void *)Buffer1, TheImage.ptr<uchar>(r), cols*sizeof(unsigned char));
memcpy((void *)Buffer2, TheImage.ptr<uchar>(rows-(r+1)), cols*sizeof(unsigned char));
  • 交换数据(从缓冲区写入内存)
memcpy((void *) TheImage.ptr<uchar>(r), (void *)Buffer2, cols*sizeof(unsigned char));
memcpy((void *) TheImage.ptr<uchar>(rows-(r+1)), (void *)Buffer1, cols*sizeof(unsigned char));

性能如下:(注意,这里的提速有点夸张,个人认为除了内存访问模式的影响以为,还和之前程序中直接对opencv的矩阵对象进行操作有关)

线程数 1 2 4 8 16 32 64
时间(ms) 1.65 0.95 0.96 0.85 0.86 1.42 2.52

3、同步和异步

前面介绍的并行方式都属于同步并行(synchronized),即程序等待所有并行的线程都执行完当前的任务,再执行下一步工作。这样可能会出现的一个问题是:有些线程由于某些原因执行速度变慢了,则所有线程都会受到它的影响而滞后,因为它们需要等待所有线程都完成任务。换句话说,具有木桶效应。

除了同步并行以外,我们还有另一种并行方式叫作异步并行(asynchronized)。这种并行方式可以使线程无需等待其它线程的工作情况,而直接进行其它任务。下图可以看到它们的关系:

async

这里我们不讨论同步和异步之间的优劣,只介绍如何实现一个异步的并行程序。与同步并行不同,异步并行会出现线程之间对资源访问的冲突问题。解决这个问题的一种方法是使用互斥量(mutex)。简单来说就是通过对一个共享变量的上锁(lock)和解锁(unlock)来保证在同一时期只有一个线程对共享资源进行修改,从而解决冲突的问题。下图说明了其工作原理:

mutex

利用互斥量实现异步并行的核心代码如下,完整代码见这里。注意我们不再将数据等分为n份,而是每次让一个空闲的线程执行一行,若完成则等待下一次分配任务。

  • 定义和初始化mutex
pthread_mutex_t CounterMutex;    // define mutex
...
        // initialize mutex
        pthread_mutex_init(&CounterMutex, NULL);

        pthread_mutex_lock(&CounterMutex);
        NextRowToProcess = 0;
        for (i=0; i<NumThreads; i++) {
            ThParam[i] = 0;
        }
        pthread_mutex_unlock(&CounterMutex);
  • 利用mutex互斥地读取和修改下一行的值
// get the next row number
        pthread_mutex_lock(&CounterMutex);    // lock it before accessing
        r = NextRowToProcess;
        NextRowToProcess ++;

        pthread_mutex_unlock(&CounterMutex);    // unlock it after accessing

性能如下:

线程数 1 2 4 8 16 32 64
时间(ms) 1.70 1.99 1.15 1.18 1.32 1.67 2.66

4、程序与性能(optional)

在这课程中,除了学习到并行计算的原理及实现方式外,我最大的感触便是程序的细微改动对性能巨大的影响。这些改动涉及到内存的访问、CPU计算量的减少等方面。下面这个例子中,我们用7种不同方式实现旋转图片的任务,其中每种实现逐步地对代码进行修改和优化。我们可以看见rotate7()比rotate()的性能要优越很多。由于篇幅关系就不再一一讲解了,具体代码请看这里

深度学习简介,GPU计算的原理,分布式机器学习原理

深度学习简介深度学习的概念源于人工神经网络的研究。含多隐层的多层感知器就是一种深度学习结构。深度学习通过组合低层特征形成更加抽象的高层表示属性类别或特征,以发现数据的分布式特征表示。深度学习采用的模型...
  • horsefoot
  • horsefoot
  • 2017年05月04日 20:48
  • 12704

GPU并行计算入门1——背景知识

专有名词: GPGPU 通用图形处理器 (英语:General-purpose computing on graphics processing units,简称GPGPU或GP²U),利用处理图形...
  • wanyfe
  • wanyfe
  • 2015年10月05日 21:04
  • 1489

GPU架构

摘要:首先我们可能有疑问,既然OpenCL具有平台无关性,我们为什么还要去研究不同厂商的特殊硬件设备呢? GPU架构 内容包括: 1.OpenCLspec和多核硬件的对应关系 ...
  • jaccen
  • jaccen
  • 2016年05月10日 22:30
  • 3058

CUDA 学习(二)、使用GPU理解并行计算

一、并发性        由于“易并行”不需要或者只需要少许线程间或线程块间通信,所以CUDA是很理想的并行求解平台。它用基于片上资源的、显式的通信原语来支持线程间通信。...
  • tiemaxiaosu
  • tiemaxiaosu
  • 2016年10月24日 11:40
  • 2031

TensorFlow多GPU并行计算

1. TensorFlow指定特定GPU或者CPU进行计算:说明:示例计算机为单CPU(编号为0),单GPU(编号为0),安装的TensorFlow为GPU版。本文的结构如下: 默认为GPU #0 指...
  • jasonzzj
  • jasonzzj
  • 2016年09月19日 19:11
  • 9284

浅谈多核CPU、多线程与并行计算

0.最近发觉自己博客转帖的太多,于是决定自己写一个原创的。笔者用过MPI和C#线程池,参加过比赛,有所感受,将近一年来,对多线程编程兴趣一直不减,一直有所关注,决定写篇文章,算是对知识的总结吧。有说的...
  • delacroix_xu
  • delacroix_xu
  • 2010年10月08日 20:58
  • 72095

并行计算简介和多核CPU编程Demo

并行计算简介和多核CPU编程DemoHouSisong@GMail.com  2007.01.16tag:多线程,并行计算,OpenMP,多核编程,工作线程池( 2008.01.19 更新  鉴于读者...
  • housisong
  • housisong
  • 2007年01月17日 00:54
  • 17820

【并行计算-CUDA开发】浅谈GPU并行计算新趋势

随着GPU的可编程性不断增强,GPU的应用能力已经远远超出了图形渲染任务,利用GPU完成通用计算的研究逐渐活跃起来,将GPU用于图形渲染以外领域的计算成为GPGPU(General Purpose c...
  • LG1259156776
  • LG1259156776
  • 2016年10月13日 11:07
  • 2878

并行算法的基本原理

并行算法的基本原理  并行算法就是用多台处理机联合求解问题的方法和步骤,其执行过程是指将给定的问题首先分解成若干个尽量相互独立的子问题,然后使用多台计算机同时求解它,从而最终求得原问题的解。并行算法是...
  • lycnjupt
  • lycnjupt
  • 2015年09月05日 16:30
  • 2047

课程总结 -- CPU/GPU并行计算基础(CPU篇)

上学期选修了Prof. Tolga Soyata的“GPU Parallel Programming using C/C++”课程。本文纪录其中的基础要点和关键实现方式。通过本文大家可以了解到:1、如...
  • yjn03151111
  • yjn03151111
  • 2016年01月25日 04:13
  • 2108
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:课程总结 -- CPU/GPU并行计算基础(CPU篇)
举报原因:
原因补充:

(最多只允许输入30个字)