chapter02 CUDA编程模型 CUDA Programming Model

Chapter 02 CUDA Progeamming Model

2.0 WHAT’S IN THIS CHAPTER?

  • Writing a CUDA program
  • Executing a kernel function
  • Organizing threads with grids and blocks
  • Measuring GPU performance

你可以在很多平系统上写CUDA程序,包括嵌入式设备、平板电脑、笔记本电脑、台式机、工作站以及HPC clustered 系统;
C语言就可以写CUDA程序;
本章将以向量相加和矩阵相加为例子介绍如何写CUDA程序。

2.1 INTRODUCING THE CUDA PROGRAMMING MODEL

编程模型呈现的是计算机架构的抽象,它是程序在硬件上实现的桥梁。
所以CUDA C是编译型语言,不是解释型语言。

在这里插入图片描述

通信抽象(Communication Abstraction)是program和programming model implementation之间的边界。

  • program:program是写给programming model的,它指示program的各个组件如何共享信息并协调其活动;
  • programming model:它是特定计算机架构逻辑层面的体现。它通过汇编语言(compiler/libarary)实现。 一般以体现在编程语言或编程环境中。
  • ps:
    • 图中看到,编译器(汇编语言)向下通过操作系统控制硬件,向上实现编程模型(programming model);
    • 编程模型向上是applications,也就是the program。
    • 所以类比,C语言可以认为是一种编程模型的体现,它也是一种计算机架构(CPU架构)的抽象。我们用C语言开发的applications就是the program;CUDA C语言也是一种编程模型的体现;
    • 因此,所谓编程模型狭义地可以理解为一种编程语言(编译型),广义地可以理解为我们要用到的语法,内存结构,线程结构等这些我们写程序时我们能控制的部分;我们所控制的是硬件,所以编程模型是计算机架构的抽象; CUDA 编程模型包含了CPU和GPU。

CUDA programming model有以下特征:

  • 通过一种层次结构在GPU上组织线程;
  • 通过一种层次结构在GPU上访问内存。

后面将会有三章内容讲这两个方面。
作为开发人员,可以从三个层面理解并行计算:

  • Domain level
    • 在算法设计时,主要关心的就是这个层面,如何分解数据和函数,是其能够更好地并行地解决问题。
  • Logic level
    • 当进入到开发阶段时,你所关心的变成如何组织并发的threads,在此时,你所思考的是逻辑层面,以保证线程和计算正确地解决问题。
  • Hardware level
    • 在硬件层面,理解threads如何映射到cores,能帮助提升性能。

2.1.1 CUDA Programming Structure

  • Host: CPU和它的memory
  • Device: GPU和它的memory
    从CUDA 6开始,NVIDIA提供了Unified Memory,架起了host和device memory之间的桥梁,这让你用单一个指针就可以访问CPU和GPU内存。在chapter04中将详细讲,因为理解如何控制host和device memory很重要,暂时先忽略。
  • kernel是CUDA programming model的重要组成部分。
    开发者可以用sequential code写kernel,不用管底层创建和管理上千个threads的细节。
    一个典型的CUDA program包含serial code和parallel code。
    一个典型的CUDA program的process flow服从以下形式:
  1. 从CPU复制数据到GPU;
  2. 调用kernel对GPU memory中的数据进行运算;
  3. 从GPU内存复制数据回CPU内存。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eBg7u4E4-1616139777946)(2021-03-04-10-11-46.png)]

2.1.2 Managing Memory

CUDA runtime 提供了functions以分配和释放device memory,下表是standard C 和CUDA C相对应的functions:
在这里插入图片描述

一个简单的device memory分级示意图,包含了两个主要的组成部分:global memory和shared memory.chapter04、05会详细讲。

在这里插入图片描述

以下通过一个向量相加的例子,认识如何将数据在host和device之间传输:
在这里插入图片描述

  • 完整代码:
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>


__global__ void sum_array_device(float* a, float* b, float* c, const int n) {
	int i = threadIdx.x;
	if (i < n) c[i] = a[i] + b[i];
}

void initial_data2(float* ip, int size) {
	// generate different seed for random number
	time_t t;
	srand((unsigned int)time(&t));
	for (int i = 0; i < size; i++) {
		ip[i] = (float)(rand() & 0xFF) / 10.0f;
	}
}

void printf_output2(float* a, float* b, float* c, const int n) {
	for (int idx = 0; idx < n; idx++)
		printf("\n %f + %f = %f", a[idx], b[idx], c[idx]);
}

int main_sum_array_device() {
	int nElem = 1024;
	size_t nBytes = nElem * sizeof(float);
	//分配主机内存
	float* h_A, * h_B, * h_C;
	h_A = (float*)malloc(nBytes);
	h_B = (float*)malloc(nBytes);
	h_C = (float*)malloc(nBytes);

	//在主机上初始化数组h_A和h_B
	initial_data2(h_A, nElem);
	initial_data2(h_B, nElem);

	//在GPU上分配内存
	float* d_A, * d_B, * d_C;
	cudaMalloc((float**)&d_A, nBytes);
	cudaMalloc((float**)&d_B, nBytes);
	cudaMalloc((float**)&d_C, nBytes);

	//将主机上的数组复制到GPU分配好的内存上
	cudaMemcpy(d_A, h_A, nBytes, cudaMemcpyHostToDevice);
	cudaMemcpy(d_B, h_B, nBytes, cudaMemcpyHostToDevice);

	//在CPU上调用kernel,完成数组相加
	dim3 block(nElem);
	dim3 grid(1);
	sum_array_device << <grid, block >> > (d_A, d_B, d_C, nElem);

	//将GPU上的计算结果复制到主机上
	cudaMemcpy(h_C, d_C, nBytes, cudaMemcpyDeviceToHost);

	//打印计算结果
	printf_output2(h_A, h_B, h_C, nElem);

	//释放主机内存
	free(h_A);
	free(h_B);
	free(h_C);

	//释放GPU内存
	cudaFree(d_C);
	cudaFree(d_A);
	cudaFree(d_B);

	return(0);
}

2.1.3 Organizing Threads

主机上调用了kernel函数之后,程序执行会移步到GPU上,生成大量的threads,每个threads执行kernel函数指定它们做的事情,因此知道如何组织threads在CUDA编程中很重要。
CUDA暴露了一个两个级别的thread分级:grids of blocks和blocks of threads,如下图:

在这里插入图片描述

由一次kernel函数调用所产生的所有threads的集合称为grid;在一个grid中的所有threads共享一块global memory;一个grid由很多个thread block组成;一个thread block包含了一组可以相互协作的threads,它们通过:

  • Block-local synchronization
  • Block-local shared memory

进行协作。在不同thread block中间的thread是不能相互协作的。
Threads通过以下坐标变量来区分彼此:

  • blockIdx (block index within a grid)
  • threadIdx (thread index within a block)

这些变量是内置的,预初始化的变量,可以通过kernel函数直接访问;当执行一个kernel函数的时候,坐标变量blockIdxthreadIdx会被CUDA runtime分配给每一个thread;你可以根据这个坐标将数据分给不同的threads。

坐标变量的类型是uint3,是一个CUDA内置的向量类型,继承了原始的int类型,它们是两个structure(结构体)分别包含了3个uint(无符号整型)类型的变量:

  • blockIdx.x
  • blockIdx.y
  • blockIdx.z
  • threadIdx.x
  • threadIdx.y
  • threadIdx.z

CUDA在三维上组织girds和blocks。前面的图就展示了一个包含2维block的2维grid。block和gird的维度通过两个内置的变量指定:

  • blockDim (block dimension, measured in threads)
  • gridDim (grid dimension, measured in blocks)

这两个变量是dim3类型,一个以uint3类型为基础的整数向量类型,用以指定维度。每一个dim3类型的组件都可以通过xyz字段访问,例如:

  • blockDim.x
  • blockDim.y
  • blockDim.z

任何没有指定的字段都初始化为1。
一般情况下,一个grid由2维的blocks组成,block由3维的threads组成,如下图例子:
在这里插入图片描述

在一个CUDA项目中,有两组独立的grid和block变量:

  • 手动定义的dim3类型数据
  • 预定义的uint3类型数据

在主机端,开发者用dim3类型数据定义grid和block的维度,作为驱动kernel的一部分;当开始执行kernel的时候,CUDA runtime会生成对应的在kernel内部可以访问的内置的,预初始化uint3类型的grid、block和thread变量。dim3类型的变量只能在主机端可见,uint3类型的变量只能在device端可见。

比如一个1D的block,包含3个threads,一个1D的grid,包含若干个blocks:

dim3 block(3);
dim3 grid((nElem+block.x-1)/block.x);

CUDA 区别于其他编程模型的一个特征就是,它暴露了一个两个级别的thread分级,因为grid和block的维度会影响性能,暴露这个简单的abstraction提供了开发者一个额外的性能优化途径。

有一些grid和block的维度的限制, 其中一个主要的受限因素是硬件的计算资源,比如rigisters,shared memory等等。

grid和block是thread分级逻辑层面的体现。

2.1.4 Launching a CUDA Kernel

C语言的函数调用:function_name (argument list);

CUDA kernel的调用仅需在括号前加三个三角括号:kernel_name<<<grid, block>>>(argument list);

括号中第一个参数是gird,指定了grid的维度和启动的block的个数;第二个参数是block的维度,指定了每一个block里面包含的thread的个数。

指定了这两个参数就意味着指定了这个kernel总共包含的threads,同时也指定了kernel所使用的thread布局。

同一个block中的threads可以相互协作,不同的block不行。对于同一个问题,你可以用不同的grid和block布局在组织threads。

  • 比如一个32个元素的数据,你可以让一个block包含8个threads,这样就需要4个blocks:
    kernel_name<<<4,8>>>(argument list);
    在这里插入图片描述

因为数据是线性地存储在global memory中的,所以可以用内置的blockIdx.xthreadIdx.x来确定grid中的一个thread,从而建立一个theads和data元素之间的一一对应的映射。

  • 如果你让一个block包含了32个threads,这时只需要一个block:
    kernel_name<<<1,32>>>(...);
  • 如果一个block只有一个thread,就需要32个block:
    kernel_name<<<32,1>>>(...);

一次kernel的调用时和主机的thread是异步的(asynchronous),kernel调用后,控制立马还给了主机;当然你可以用以下代码强制主机等待kernel执行完:cudaError_t cudaDeviceSynchronize(void);;有一些CUDA runtime的API是默认同步的(synchronization),比如cudaMeccpy(...),就默认主机代码必须等待kernel将数据复制到主机才能执行。

2.1.5 Writing Your Kernel

在一个kernel函数中,你定义一个thread的计算以及这个thread对数据的访问;当执行这个kernel函数的时候,许多不同的threads会并行地执行相同的计算;kernel定义会用__global__声明,且必须有一个void的返回类型:
__global__ vioid kernel_name(...);

函数类型限定符:
在这里插入图片描述

kernel函数限制:
在这里插入图片描述

以一个例子作为说明:

C code on host:

void sumArraysOnHost(float *A, float *B, float *C, const int N) {
 for (int i = 0; i < N; i++)
 C[i] = A[i] + B[i];
}

CUDA C code on host:

__global__ void sumArraysOnGPU(float *A, float *B, float *C) {
 int i = threadIdx.x;
 C[i] = A[i] + B[i];
}

和C语言代码相比,CUDA C的kernel里面,循环不见了,内置的thread坐标变量代替了数组的索引,而且也没有引用N,因为kernel已经隐式地定义了调用N个threads。

2.1.6 Verifying Your Kernel

你需要一个HOST函数来验证kernel函数有没有算对;
或者将kernel函数设置为<<<1,1>>>,这就和sequential code一样。

2.1.7 Handling Errors

2.1.8 Compiling and Executing

2.2 TIMING YOUR KERNEL

2.3 ORGANIZING PARALLEL THREADS

用一个矩阵相加的例子说明。

2.3.1 Indexing Matrices with Blocks and Threads

一般地,一个矩阵是以行为主的方法线性地存储在global memory里面的。比如一个8x6的矩阵:
在这里插入图片描述

在一个矩阵相加的kernel里面,通常每个thread会获得一个数据元素进行计算,第一步需要做的就是通过thread index 访问global memory里面的数据;一般有三种索引需要你将它们相互联系:

  • Thread and block index
  • Coordinate of a given point in the matrix
  • Offset in linear global memory

对于给定的一个thread,你可以先通过将Thread and block index和Coordinate of a given point in the matrix一一对应以获得相对于Thread and block index的global memory中的偏移:

  • ix = threadIdx.x + blockIdx.x * blockDim.x;
    iy = threadIdx.y + blockIdx.y * blockDim.y;
    

然后将Coordinate of a given point in the matrix和global memory的地址对应:

  • idx = iy * nx + ix

三者之间的关系:(注意:ix是矩阵的列)

在这里插入图片描述

在这里插入图片描述

2.3.2 Summing Matrices with a 2D Grid and 2D Blocks

在这里插入图片描述

__global__ void sumMatrixOnGPU2D(float *MatA, float *MatB, float *MatC, int nx, int ny)
{
    unsigned int ix = threadIdx.x + blockIdx.x * blockDim.x;
    unsigned int iy = threadIdx.y + blockIdx.y * blockDim.y;
    unsigned int idx = iy * nx + ix;

    if (ix < nx && iy < ny)
        MatC[idx] = MatA[idx] + MatB[idx];
}

2.3.3 Summing Matrices with a 1D Grid and 1D Blocks

在这里插入图片描述

__global__ void sumMatrixOnGPU1D(float* MatA, float* MatB, float* MatC,
	int nx, int ny) {
	unsigned int ix = threadIdx.x + blockIdx.x * blockDim.x;
	if (ix < nx) {
		for (int iy = 0; iy < ny; iy++) {
			int idx = iy * nx + ix;
			MatC[idx] = MatA[idx] + MatB[idx];
		}
	}
}

2.3.4 Summing Matrices with a 2D Grid and 1D Blocks

在这里插入图片描述

__global__ void sumMatrixOnGPUMix(float *MatA, float *MatB, float *MatC, int nx, int ny)
{
    unsigned int ix = threadIdx.x + blockIdx.x * blockDim.x;
    unsigned int iy = blockIdx.y;
    unsigned int idx = iy * nx + ix;

    if (ix < nx && iy < ny)
        MatC[idx] = MatA[idx] + MatB[idx];
}

2.4 MANAGING DEVICES

pass

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
CUDA programming: a developer's guide to parallel computing with GPUs. by Shane Cook. Over the past five years there has been a revolution in computing brought about by a company that for successive years has emerged as one of the premier gaming hardware manufacturersdNVIDIA. With the introduction of the CUDA (Compute Unified Device Architecture) programming language, for the first time these hugely powerful graphics coprocessors could be used by everyday C programmers to offload computationally expensive work. From the embedded device industry, to home users, to supercomputers, everything has changed as a result of this. One of the major changes in the computer software industry has been the move from serial programming to parallel programming. Here, CUDA has produced great advances. The graphics processor unit (GPU) by its very nature is designed for high-speed graphics, which are inherently parallel. CUDA takes a simple model of data parallelism and incorporates it into a programming model without the need for graphics primitives. In fact, CUDA, unlike its predecessors, does not require any understanding or knowledge of graphics or graphics primitives. You do not have to be a games programmer either. The CUDA language makes the GPU look just like another programmable device. Throughout this book I will assume readers have no prior knowledge of CUDA, or of parallel programming. I assume they have only an existing knowledge of the C/C++ programming language. As we progress and you become more competent with CUDA, we’ll cover more advanced topics, taking you from a parallel unaware programmer to one who can exploit the full potential of CUDA. For programmers already familiar with parallel programming concepts and CUDA, we’ll be discussing in detail the architecture of the GPUs and how to get the most from each, including the latest Fermi and Kepler hardware. Literally anyone who can program in C or C++ can program with CUDA in a few hours given a little training. Getting from novice CUDA programmer, with a several times speedup to 10 times–plus speedup is what you should be capable of by the end of this book. The book is very much aimed at learning CUDA, but with a focus on performance, having first achieved correctness. Your level of skill and understanding of writing high-performance code, especially for GPUs, will hugely benefit from this text. This book is a practical guide to using CUDA in real applications, by real practitioners. At the same time, however, we cover the necessary theory and background so everyone, no matter what their background, can follow along and learn how to program in CUDA, making this book ideal for both professionals and those studying GPUs or parallel programming.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值