文章的开始,感谢2022CUDA夏令营中辛勤为学员们答疑的每一位老师和气氛组们,本文章的著作权属于他(她)们,作者仅负责整理归纳,如有错误,说明作者理解不到位。
1、报错:编译makefile时分隔符缺少
解决:使用tab
2、提问:同一文件夹下makefie文件多,如何调用
解决:默认Makefile, makefile, GNUmakefile, 其他文件默认必须-f指定,前三个后自动搜索。
3、提问:直接nvcc无法调用
解决:加上nvcc的路径,which nvcc可以看nvcc的路径,也可以考虑设定正确的PATH和LD_LIBRARY_PATH环境变量。后者适合动态链接了CUDA的runtime的.so的时候(默认是静态链接的,会生成一个较大的可执行文件,但会脱离对cuda的runtime动态库的依赖)。
4、提问:value sm_40 is not define
解决:40的计算能力被直接跳过,注意,想实验PTX计算能力,大于CUBIN计算能力的同学,可以用如下数值:35, 37, 50, 52, 53, 60, 61, 70, 72, 75, 80, 86,其他数值都是不存在的
5、提示:PTX指定的compute_xx,其中的xx必须小于或者等于,cubin的sm_xx
6、提问:调用__global__时候被直接renturn 0
解决:函数cudaDeviceSynchronize();的作用为同步主机与设备,可能在某些应用中忽略掉了。
7、提问:直接nvcc hello_from_gpu.cu -o hello_from_gpu arch和code不写
解决:会使用当前cuda toolkit的默认计算能力(注意!这可能超过了你的卡的计算能力,此时必须指定)。不指定会使用默认的计算能力。不一定是5.3,如果可能,尽量指定。以及,默认的指定会保留一份PTX。一般够用(除非你在11.x的toolkit上使用Kepler的卡,默认保留的Maxwell的PTX不能向下兼容)。
8、提问:makefile里 --device-c是啥呀
解决:等于-rdc=true -c两个选项,方便你多文件编译+链接,-c只是编译,不涉及到GPU架构。你可以单独指定(加个选项)。生成.o需要加上。可以直接理解成CPU上的-c, 因为CUDA 3.2之前的问题,导致了rdc的出现(3.2以前的CUDA要求跨文件隔离kernel和符号),所以-c变成了-rdc=true -c, 最终变成了-dc(–device-c)
9、提问:makefile的执行顺序
解决:根据标签的依赖关系执行
解决:SSMem和DSMem分别代表了静态和动态的shared memory。shared int dog[10]; //静态的 <<<*, , XX>>> <—动态的;stream就是<<<,,S>>> <—这里,因为不是从0开始,内部会保留一些,所以stream上去从某个数值开始很正常。
1、提问:想问下.cu的文件编译是只能用nvcc编译吗?
解决:不是,但是是最简单的
2、提问:Warp是什么
解答:Warp是GPU执行单位,是一组线程的集合,我理解warp是软件层面上的,但是和硬件线程相关
解答:
a、修改值,传指针,修改指针,传二重指针
b、CUDA函数里面凡是需要返回2个或者2个以上的返回值的,除了第一个返回值直接返回(一般都是成功/错误代码),后面都通过指针返回,如果很不幸的原本某个类型本身是一个指针,再要返回这个指针的时候,就变成了指针的指针了。
追问:为啥不是d_a=(int *)cudaMalloc(size)
解答:cuda编程的函数的返回值是预先定义过的,如果你的函数执行有问题,会通过返回值告诉主机
4、提问:indedx(x,y,z)z是竖轴的,物理上也是三维的?还是说只是逻辑上虚拟出来的呢?
解答:实际的访存往往也是Z轴,在线性地址上的跨度最大的;或者安排x/y/z填充warp的顺序,Z轴也是最后不优先变化的。如果这些可以算成“物理上”,则你可以这样认为。其他都可以认为是“虚拟的”。在计算能力5.0+的硬件上,warp是硬件+软件协同调度的。搜索maxwell + control code(将maxwell替换成其他架构),从google获取更多信息。
5、提问:(N + block_size - 1) / block_size 这个是固定写法吗
解答:其次,(n + block_size - 1) / block_size你可以这么理解,如果n= a * block_size+b(a为0 or 正整数,b为小于block_size的0 or 正整数), 那么(n + block_size - 1) / block_size等价于(a * block_size+b + block_size - 1) / block_size等价于a+(b+block_size-1)/block_size,由于会向下取整,所以(b+block_size-1)/block_size 等价于1,最终(n + block_size - 1) / block_size = a+1。如果n= a * block_size+b定义为,a个线程block+个数小于block_size的零散的thread, 那么上面推导出的那个a就是值a个block,而上面推导出的+1就是为个数小于block_size的零散的thread而专门设置的block
6、提问:dim3的架构是?
解答:dim3等于是一个特殊的int3, (x,y,z)中,不存在的分量被自动设定为1
7、提问:gridDim和blockDim,对应了<<<>>>里的哪两个配置参数
解答:菱形配置符号里的前两个参数是这样的。不过gridDim和blockDim仅在设备代码(GPU代码)中才有效。在Host端他们是普通的两个dim3结构体。
8、提问:如何查看每一维的最大size
解答:–CUDA C Programming Guide上按照计算能力给出的block最大形状,和grid最大性能。不过目前所有支持的卡都是block的3个分量,每个最大1024(同时总数量1024)限制;grid的3个分量(x,y,z), 最大分别是INT_MAX(即2^31 - 1),64K-1,64K-1, 同时无总数量的乘积上的额外限制。
追问:block里边的线程数量是不是有上限
解答:有的。1024线程的上限(还受限于其他因素,例如寄存器的使用量)。搜索occupancy_calculator.xlsx文件,以获取更多信息。
9、提问:就是grid size 设置比实际需求大很多
解答:没错。是这样的。过大的grid会导致一些额外的blocks上到SM上,但是这些blocks被if之类的条件给快速抑制掉了(结束掉了)。所以只要加了边界判断 只会导致效率低些 其他没什么影响
10、提问:老师,尖括号中,只要blocksize设置成32的倍数,warp就能最佳分配,那我设成32,64,128这样会有什么区别呢?
解答:
a、不同的大小可能会导致不同的性能变化。在你的卡(Jetson Nano上),我不建议你使用低于64(不含)的数值。因为该硬件设备最 大能上2048线程/SM,但最多只能同时上32个线程。这样小于64个线程/block,将影响最大驻留blocks能力(不一定会表现出来性能上的降低,但是有潜在影响)。其他的形状哪种能最佳性能,需要试验,这个我不能直接知道(你也不能),我们需要实验。
b、block共享shared memory,以及SM中的寄存器等资源有限,以及线程生成器的速度有上限,blocksize太大太小都不能最大化利用硬件提高效率,经验值似乎是256一般还行,但具体应用和具体kernel还要具体测试
11、提问:block_size(尖括号第二位)不是32倍数的后果是什么,也可以通过的
解答:正确使用了内置的索引和形状变量(threadIdx, blockDim; blockIdx, gridDim)不会有正确性的影响。但可能会导致性能下降。
追问:改成了33就分成2个warp?
解答:是的。超出哪怕1个线程,也会分配一个warp(浪费31/32的潜在执行能力)。
追问:也就是说gpu的内存调度是以block调度的,不是以warp调度的是吗?
解答:如果你这里的内存是指的shared memory的笔误,那么的确是以block为单位分配shared memory资源的。
12、提问:share memory大小
解答:计算能力从3.5到8.7的图
13、提问:怎么看一个sm里边几个cuda core
解答:这个不能通过API查询。只能检测计算能力后,写死。(例如7.5的计算能力,对应64个/SM)。而计算能力可以通过cudaGetDeviceProperties()获得,这样你再硬编码一个计算能力和SP数量/SM的对应关系的表格,就可以得到你的卡的每SM的SP个数了。这也是deviceQuery例子的做法,无法直接通过API得到。
14、提问:同个block中的线程只能在同一个sm中执行吗,一个SM上可以有多个block吗
解答:目前所有已知的计算能力(代)的卡或者嵌入式设备,1个block都只能在同1个SM上被执行,不能跨SM。反过来不成立。同1个SM上可以有不同的多个block。
解答:嗯嗯。warp不满(因为warp内分支)会影响性能。搜索divergent branch以获取更多信息。
16、提问:那这个调度过程是自动的吧
解答:block本身(整体看)的调度是自动的。SM上驻留的某1个block结束,下一个block(如果该block要求的资源足够)就会上来执行。
追问:比如一个SM上有四个warp的调度器(是不是可以理解成四发射)然后这时候我的blocksize是128,那这个SM在开始执行时(假设没有用到shared 等冲突资源)是调度一个block的4个warp一起上去执行,还是调度4个block每个block都是一个一个warp轮转执行
解答:4个调度器和4发射可能没有直接关系。某些计算能力上,1个调度器在某个特定的周期,可能同时issue两条指令。block一旦上到SM后,会被打散到schedulers中(目前所有计算能力),每个计算能力单独负责它下辖的warp,并不存在按照block自身的组合关系的调度的说法了。自由调度。(但如果等待在block级别的同步上,是另外一回事)搜索jia zhe字样,或者sub partition字样,或者micro benchmarking字样,能获得更多信息。以及,在这些相关的文章中。4个scheduler往往是按照warp的ID,拆分到特定的scheduler下的。网上有人测试出来了这个打散的具体映射关系(不过你可能一般用不到)。
17、提问:sm,warp,block,core绕晕了。。。一个core是对应多个线程吗?
解答:只能说,来自多个线程的指令,可能分时的在同1个SP(CUDA Core)上执行。其他执行单元也是这样,都是分时复用,服务多个线程的,但你不能说SP(CUDA Core)和线程有什么固定的对应关系。
追问:原来如此,那也就是说block在资源够的情况下就上SM,然后被打散成以warp调度,在这期间分配的shared等资源是固定的(从SM的shared里面分配出去每个block所需的大小)噫,我又想问寄存器是按照流处理器组织的还是按SM组织的(即一个特定的寄存器是只能被一个cuda core访问还是可以被SM中的任意一个处理器访问)因为我记得cuda中被挂起的线程是不释放寄存器的,如果是前者,那是不是说明一个warp被调度了之后,就算他因为访存什么的被阻塞,再回来执行的时候必须还在原来的core上执行。
解答:特定的某个寄存器,在某个warp存活期间(执行期间),将是固定分配给每个warp(中的某个线程的)。当这个warp挂了后(结束后),这个寄存器,可能又被分配给其他block中的其他warps了。如果你要讨论block被临时切出SM的情况下(例如CUDA Dynamic Programming或者Preemption),那么此时寄存器可能分配给其他人用了。临时性的线程不满足调度条件的话,寄存器资源不释放的(没死就一直属于自己)
18、提问:这么说也能优化 卷积么,思路和矩阵乘法差不多,先算线程索引,再循环乘
解答:–也能"并行"卷积。
19、提问:core和线程没有显式的对应关系,那gpu可以同时执行几十,百万个线程,也就是这些线程也只是并行的,并不是并发的,那为啥cpu也是多核,但同时的线程数会少那么多呢?
解答:
a、设计的方式不同,例如常见的CPU只有2路/4路超线程。但是1个SM却可以并发1024个/1536个/2048个线程。涉及的方式不同。CPU上线程少,但是1个线程前后依赖的指令可以尽快推进下去,擅长于尽快的干好1件事。GPU上线程多,但是需要较大的时间粒度,才能完成一个批次的执行,优化的是整体的吞吐率,而不是单一线程的延迟之类的。搜索"throughput processor"以获取更多信息。
b、CUDA中一百万个线程那肯定不是同时执行的,也是要排队的。只不过用户(程序员)不需要管怎么排队,就一下指派(assign)那么多都可以,只要不超过一些尺寸上限。
20、提问:我突然想到一个问题,如果一个问题数据规模很大,超过了int 或者 unsignd最大值,那肯定要用 long 或者 long long 了?
解答:直接上size_t, 因为CUDA现在只支持64-bit了。所以这个64-bit的size_t足够用。
21、提问:想请教一下,这一部分和普通的malloc的区别是啥,我看例程的后面还是需要复制到了Device中去了
解答:不一样的,你可以认为前者需要cudaMallocHost()/cudaFreeHost(), 后者需要malloc()/Free().,cudamallochost会保证申请的空间不会被交换到磁盘中,一直驻留在内存
22、提问:block里的线程在分配内存的时候是分配的shared memory里的还是分配的global里的呢?
解答:–如果你"分配"是指的cudaMalloc(), 则是全局的,而且所有的blocks可见。
23、提问:rgb.x rgb.y rgb.z 就能取出三个channel的值吗?
解答:对于float3,int3之类的可以这样写。其他类型看定义的成员。
24、提问: unchar3是啥类型呀
解答:uchar3是CUDA内置的具有3个分类的,每个分量为1B的类型。
25、提问:我有个问题,dimGrid和dimBlock的尺寸都可以随意自定义,是不是只要保证dimGrid 和dimBlock的两个方向上的总乘积,大于等于M*K就行。
解答:不行。每个方向都应该单独达到覆盖。而不是两个方向上的乘积达到或者超过。例如一个100x200的矩阵,单独用600 * 40的总线程形状去覆盖是不行的。虽然总乘积超过。但是单独看有一个方向没覆盖全。要拆开单独大于等于。不能用乘法.
26、提问:Thread不够,forloop那个循环,也就是申请的线程数,少于所求问题元素总数的那个循环不太理解
解答:每个thread和其他thread并无本质不同,连代码都执行的是同一份。唯一让它变得特别的,是它使用的下标。所以根据这个理解:如果原本有0,1,2,3,4,5号线程去完成任务,和你只有0号线程,但是它完成了原本0号的下标负责的对应的任务后,继续下标+1变成1号线程,也一样可以的。如此类推,哪怕只有1个线程,连续变身从0到5,和直接有5个线程,并无本质区别的。有个这个理解后,再扩大一下,如果原本是1000个线程,直接上1000个可以。如果上不了,只能上300个。那么这300个完成了前300号任务后,+= 总线程数,变身成为300-600号(不含),依然可以完成再300个不存在的线程的原本要负责的任务。这样,最终他们累加多次,就将前300号,中间300号,最后300号,以及剩下的100号(你需要用while或者if判断别干多了)。那么 ,如何得知每次while补充下一批要干的编号的这300人的300这个数字?它是从哪里来的?在1D线性话的情况下,它是blockDim.x * gridDim.x,这样就完成了while补齐线程数量不够的问题。
27、提问:如果只需要1000人干活,我上去分配了2000人(通过过多的blocks数量,例如)。怎么办?
解答:同理,检查任务坐标映射,不存在的任务不干即可(if屏蔽掉)。这样人数不齐,任务数多,你会用while追加上了;同时人多了,任务少了,你也会用if屏蔽掉了。
28、提问:老师,一个Warp能执行同一个问题,一个Block加入有4个Warp,是不是可以同时跑4个不同的问题
解答:如果问题的映射是1个warp才能处理1个问题(或者说1个点),则此时的确是4个warp能处理4个问题(点)的。
29、提问:老师们,cuda里把连续128bit的数据从global memery先复制到shared memory再复制到register,和先从gmem到reg再到smem,速度有差别吗
解答:直接复制到shared memory, 不使用特殊的写法,直接用等号的形式,例如:shared int kachi[…];kachi[xxx] = ptr[xxxx]; (其中ptr是一个指向显存/global memory某区域的指针)。这种写法实际上编译器,“会自动通过寄存器中转的”,和你手工:tmp = ptr[xxxx];dog[xxx] = tmp;并无本质区别。因为这两个是一回事,只是节省了你的中间变量的写法,实际代码生成是一样的,所以不会有任何差别。能直接一步从global memory(例如你的卡的显存), 将数据直接载入到shared memory, 需要计算能力8.0+的卡。
30、提问:内核函数里边怎么printf的
解答:内核实际上不能直接printf,因为GPU不能直接控制你的终端窗口,我们用到的printf实际上是模拟的:GPU里的线程先将结果字符串,保存到一个看不见的显存区域里,等kernel结束的时候,同步的时候,将这个看不见的区域里的准备号的字符串们,传回到Host端(也是自动的看不见的过程),然后再从Host上重新printf出来。
31、提问:话说课后作业:当我们能申请的线程数很少,远远不够的时候怎么办?
解答:用一个for或者while循环,每次变换这些线程们的坐标。直到坐标范围能覆盖所有要干的事情。
32、提问:那假如分配进程分配多了呢,多余的人会自动释放掉可以干别的事,还是只能在一边傻看着
解答:如果你的kernel里面是这样写的:
__global__ void baiyi(...)
{
if (...)
{
}
}
那么其他人会很快的结束的,并不会傻看。只有处于同一个warp中的,有部分warp中的线程还有活的人,才会导致该warp剩下的人在看。其他的warp整体无活的直接就死掉了。
1、提问:寄存器溢出会使用local memory,share memory溢出呢?
解答:不会,你申请用多少,就一共有多少,如果你下标超出范围越界了,一般kernel会挂掉(而不是自动通过慢速的其他memory来兜底)。这个主要是针对shared memory说的。寄存器溢出,是一个特殊的概念。本课程不准备多说。但是和下标越界无关。感兴趣可以自行搜索。
2、提问:shared memory在一个block之间共享,那多个block之间可以通信吗?
解答:
a、用全局内存,嗯,不同内存的访存速度不一样,想线程间通信就得花费一些时间
b、在目前已有的硬件(独立卡或者Jetson),多个block之间可以通信,但不能使用shared memory了(因为仅限于1个block内部)。多个blocks之间,可以通过慢速的global memory进行(依然比普通台式机的内存快得多一般)
3、提问:问一下如果一个sm里有64个cuda core,是不是在这个sm中的同一时间里最多有64个线程并行,最新的架构下,一个sm的cuda core是多少呢,趋势是越来越多么,cuda core的多少对不同的应用场景有什么影响呢
解答:一个SM里面,除了有SP(CUDA Core)外,还有其他的硬件,例如shared memory, LSU(Load/Store Unit), SFU(特殊功能单元,例如你算sin时候的就需要它)等等。而同时驻留在一个SM上的线程们,例如同时1024个好了(假设的),它们不会同时都在竞争SP资源(基本上是常规计算),有的可能在访问shared,有的可能在访问global, 有的可能在在转换float->int, 此时它们就可能被映射到除了SP(CUDA Core)以外的其他单元,这样这些单元上都有很多的线程(的指令)在忙碌,此时同时被执行中的线程数量就可能超过64(2个warp)了。
4、请问相邻线程的具体表现为?
解答:global memory上的合并访问,是从"1个warp"的角度说的,所以此时要切换到整体思维看下标是否连续之类的,而不是看一个线程是否连续的从左读到右。一个线程看起来在"连续的读取临近范围", 实际上却可能从warp的整体角度看,global memory是很分散的,不合并的。所以要看整体。因为不仅SM里的SP(所谓的CUDA Core)是形成了阵列, SM里的访存单元也不是单体,也是成阵列的组织的。它们每次服务的是warp的整体(并不精确的说法,不过可以先这样看)。所以服务效果的如何,要看这个整体的请求(读取或者写入global memory)的情况如何。而不是一个线程自己的。这点和CPU很不一样。GPU是一个高度并行化的组织形式,不仅仅GPU里有很多SM形成阵列,SM内部的各种执行单元,也形成了并行的阵列。这也是GPU的高性能的来源(之一)。
5、提问:那也得访问同个bank中的同个地址 才能bank conflict吧
解答:是的。除了Kepler的shared的1个bank能1个周期给出2个它里面不同深度位置的数值外,其他的所有计算能力都是1个bank,1个周期,只能给出1个位置的数值。所以如果对于4B的访问情况,的确需要warp访问1个bank的时候,固定在同一个4B位置里,才能广播。(试图访问不同深度位置,该bank将无能能力,只能拆分到多个周期完成请求。此时warp对shared memory的整体访问将出现bank conflict)。
补充:Kepler的shared memory是双端口SRAM。给好奇的同学。
6、提问:block和warp和bank的关系我感觉我混乱了
解答:block是线程的逻辑组织形式,当成上课的班级就好。warp相当于一个宿舍的人。至于bank,则是餐厅的打饭窗口。你一个宿舍的人(warp)尽量在不同的窗口(banks)打饭,才能效率最佳,否则大家都挤在红烧肉的窗口(bank), 会导致半天都打不完饭的。
7、提问: 这个阻塞非阻塞还是不太明白,是结束事件只能用阻塞吗,不然会报错
解答:使用2个CUDA Event进行设备端计时,只能在这2个event都实际发生后(即你要求的record操作被实际的记录后),才能用发生的两个时刻的差值,来代表中间被夹杂的其他操作的时间。所以一般情况下,使用Event 1 -> kernel -> Event 2的方式,必须至少等到Event2实际发生了才行。而这个等待,往往是阻塞的,这个最简单。(你可以用其他方式非阻塞式的轮询,但新手不建议搞这个)。
8、提问:老师请问一下,小红书上说,尽量做到合并写入,为啥在利用共享内存进行矩阵转置时,采用了这种非合并的方式写入呢
解答:
a、因为小红书是在对global memory说的要合并,而我们今天是说的shared memory. 这是两种不同的硬件,访问方式上特性不错。
b、简单点说, share mem 太快, 不需要. 而且shared mem是以bank的形式响应warp中的线程, 跟DRAM的响应形式是不一样的
9、提问:__host____device__这个关键字表示在设备主机都可运行是吗?
解答:编译分别为host和device生成CPU和GPU两种代码。至于运行不运行,得看你调用了没调用。–除了写了__global__和__device__都是在CPU上执行,至于以CUDA开头作为函数名,不代表它在GPU上执行。
10、提问:老师,这两个函数有啥区别呢
解答:前者取回保存的错误代码,然后reset掉它,后者取回,同时持续保留这个错误代码。前者等于
result = __hidden_error_code;
__hidden_error_code = GOOD;
后者等于:
result = __hidden_error_code;
11、提问:这里为啥需要轮询呢
解答:do {…} while(0) 是常见的定义临时变量,防止命名冲突的做法。注意,while(0)不是循环。
12、提问:那cudaEventQuery(start)有什么用?
解答:Windows上的效果是push command to device queue(隐含效果)。等效于cudaStreamFlush(NULL); //刷新默认流中的任务给设备。注意cuda并不存在一个streamFlush的函数。但是我们都用cudaEventQuery, cudaStreamQuery来强推任务(本课程不涉及)。
13、提问:这里为啥要start设置成非阻塞的,stop设置成阻塞的呢?
解答:cudaEventQuery不会导致阻塞,立刻返回。第二个stop需要等待他实际的发生。这样第二个发生了,自然第一个也发生了。再用他们的发生的时刻,求出差值。
14、提问:Shared Memory是可以被一个Block中的所有Thread来进行访问的,可以实现Block内的线程间的低开销通信。那一个warp里的threads可以属于不同的block吗
解答:不可以的。1个warp必须属于同1个block。一个blocks里有好多warps。每个warp不可拆分。
追问:所以这就是block必须是32倍数的原因
解答:–不是。可以有不完整的warp(例如11个线程的warp,剩下的21个线程不被激活)。
15、提问:老师,register是属于线程的,那多个线程并行的时候,对于register是排队用吗?
解答:寄存器组织形式上就适合warp整体去读,例如你的Maxwell,每个SM分区里,寄存器堆是有4组读写端口,每组端口能给1个warp提供32个4B数据/周期。所以无需排 队。直接就能并行。
16、提问:那老师,如果我设置的tile不是block_size,而是比它大或者小,那么在处理tile的赋值时就会出现问题?
解答:比他小, 可能会, 可能出现同一块存储空间要被多个线程读写的情况,需要加同步或者原子操作,尽量保证是你线程数的倍数
17、提问:哦哦,明白了,还有一个问题就是每个线程进入核函数都会有声明tile的那步
解答:哦哦,也就是进入都会声明一遍,但是是同一个,但不同的block的线程进来声明的虽然是同一个名字的tile,但存储空间已经变了
18、提问:shared memory可以不等于blocksize吗 如果不等于的话咋调整啊
解答:可以不相等,例如我有一个算法,需要一个256B的查找表,和一个1024线程的block。那么完全可以初始化这个表的时候,前256个线程,每人初始化进去1B;或者前32个线程,每人初始化完成4B。然后大家(1024个人)狂用。
19、提问:share mem 是怎么提高 执行效率的,为啥先存在share里边更加快,global -> share -> global 为啥比 global -> global快
解答:你可以理解成自动的cache往往效果不好(例如有时候存了你不需要的东西,有时候却不存你需要的东西),而shared memory往往作为你手工管理的L1的时候,你 自己决定存放什么。可以理解成高度并行化的cache,每个bank每个周期均可以独立工作。速度提升是在反复重复使用shared中的数据这个过程中发生的。这等于一个100% Hit Ratio的cache了。
追问:矩阵转置可以用sharemem提升速度吗
解答:shared memory上的纵向读取,在无bank conflict的情况下,和横向读取一样快,这样就规避掉了global memory上的纵向大跨度读写的负担。从shared上直接变换坐标读取,不需要单独专置的一个kernel,集成在另外的计算kernel中,完全免费和消除了手工转置的需要了。
20、提问:kernel 和 core有点蒙
解答:两个层面, kernel是函数, core是硬件核心
21、提问:您能再讲解一下block中thread数量与sm计算之间关系吗,刚刚没有听懂;还有就是block中thread大小对于share memory大小的关系(share memory大一点可以加速运算,是这个意思吗)
解答:block变大—>shared缓冲区对应变大---->重复使用的数据次数更多---->性能可能提升。block变大—>SM上的blocks变少---->blocks之间的交错执行(例如SM上有的block在访存,不占用SP. 有的在计算,占用SP,但不访存,组合起来能充分利用)机会降低—>性能下降。但是两个相反的效应叠加存在,所以直接问大或者小的blocks形状,那种更快,不能直接知道。
1、提问:请问一下,使用统一内存申请的空间在程序运行结束就自动销毁了是吗,不需要人为的free了
解答:如果你用__managed__声明的数组之类的,不用管,自动释放(在host process结束前的CUDA Context的隐式销毁的过程里). 如果你是用cudaMallocManaged分配的,也可以不管,但建议养成用cudaFree()释放的习惯,CPU上的进程结束的时候会释放所有资源,包括CPU上的,也包括GPU上的。
2、提问:请问统一内存可以理解为, 用__managed__定义,然后就不用数据传输过程 cpu gpu直接用吗?
解答:统一内存是指我们声明了managed,即便显存和内存是分开的,但两者在各自需要使用这个数据时,系统会自己进行传输,不需要我们显示的去copy,所以说还是会受制于pcie的带宽,但jetson的区别是jetson的内存和显存是公用的,所以不存在传输这个过程。而且统一内存有个好处,你可以申请超过设备内存大小的内存。统一内存也不是没有缺点,需要避免内存抖动……手动可以明确地指定方向,避免不必要的内存拷贝。手动对开发人员要求较高,但也容易实现比较好的性能。另外根据小红书的内容,统一内存在windows上好像还不是太完善……
3、提问:prop和whichdevice的作用是什么呢
解答:这个是个获取设备属性的api
4、提问:请问统一内存是igpu和cpu之间共享内存嘛?dgpu和cpu没法统一吗?这里igpu和dgpu是集显和独显的意思吗
解答:Unified Memory可以从逻辑上做到共享的效果,但是真正就地共享使用,无任何跨越PCI-E的隐式传输的,只有Jetson系列(集成GPU)。
5、提问:请问下 cudaMallocManaged() 和 cudaMalloc() 有啥本质上的区别吗?
解答:这个话题(本质的区别)太大了。暂时无法讨论。目前我建议你只理解前者是unified memory, 后者是普通global memory即可。
6、提问:我要定义个float5 ,怎么保证在gpu上呢?
解答:超过128-bit的结构体,或不能满足一定对齐要求的结构体,无法直接一次性完成读写。你的结构体将被拆分成多条访存指令。无意义。不过你可以定义(但无特别加速)。float4, int4,还有double2是最高级别能享受一次性整体读写的类型了。
7、提问:老师为啥用了原子操作就可以直接累加到结果,不用就得再开一个block来计算。
解答:因为他可以多个block的结果无干扰(不被打断或者出错的)直接就地累加。
8、提问:为什么 nvcc 编译cpp 会 报下面的错,变成gcc了?我记得好多工程里 cpp和cu 可以混编的啊
解答:因为nvcc会转到host compiler编译CPU部分,例如交给gcc。cpp不放核函数,cpp可以放CUDA运行时API函数,但是核函数要放在.cu,当然,可以全部.cu。你这个图片里的用.cu的话,会自动引入一些cuda的头文件的。直接交给gcc可能没有设定一些头文件的引入,从而找不到cudaMalloc()之类的基本函数的声明。解决方案可以要么改成.cu扩展名(文件改名),要么手工加上#include和-I。此外,像是此图中的__global__之类的,必须是.cu。所以你改名是最省心的。
1、提问:有一个点我还是没理解,核函数里面写的代码是每一个线程所要做的事吗?
解答:你申请的所有线程, 都会执行那个核函数的代码,他们是根据threadIdx, blockIdx, blockDim这些信息来判断要处理哪个数据
2、提问:是不是数据越大共享内存优势才明显呢
解答:不一定, 要看具体情况, 太大的shared mem会影响驻留在sm中block的数量
3、提问:constant memory cache是 on chip 的么,是每个sm都有一个么,每个sm的constant memory cache之间可以直接交互吗
解答:constant的cache有好几级,SM内部的确有,但是结构比较复杂,不能直接说每个SM里面有一个之类的说法。查看《Dissecting NVIDIA Turing GPU through microbenchmarking》之类的文章,会看到constant的cache的存在。里面有图。以及,这是一系列文章。各种架构的。很好看。
4、提问:一个grid中的block可以在多个SM中执行吗
解答:grid里的’blocks’,可能分布在多个SM中执行。你直接说"一个grid中的block"在多个SM上有歧义。容易让人以为是某个block,跨越了多个SM。
5、提问:每32个线程分为一组,称作一个warp。每个Warp会被分配到32个core上运行。
解答:后半句也不对。例如7.5的卡,16个SP/组 * 4组,一共64个SP在一个SM里头。这个SM里有4个scheduler,每个scheduler下辖16个SP,scheduler通过连续2个周期在16个SP执行两次相同操作的方式,来完成1个warp。所以固定说,某个warp分配到32个CUDA Core上运行是不对的。
6、提问:cuda event本质上是一个GPU时间戳吗
解答:这话片面了。计时只是它的一个功能(甚至是副面功能)。它主要是一个同步对象的(CUDA里最强大的同步对象)。不过我们本次课程没太多提到。但是你可能用到cudaEventSynchronize()的时候,可能会轻微的疑虑过这点。
7、提问: 没太搞懂统一内存的使用场景是什么。我嫌malloc太啰嗦,直接用__manage__可以么
解答:如果只是嫌弃malloc, 然后cudaMalloc, 然后cudaMemcpy的H->D传输太麻烦,这种情况可以用unified memory, 也可以努力一下手工来。见过很多人手工安排的复制效率更高的也是有的。但是unified memory的一个重要场合是结构体的成员之间的相互指向,这个时候如果用手工复制,非常麻烦。搜索unified memory + GTC字样,5-6年前的有一个经典幻灯片,里面有个能让你非常吃惊的例子。你感觉你完全可以手工复制,但是大部分的人都手工复制的不对的例子。这个例子直接上unified memory, 很好。另外一个则是,有的时候你有一个大缓冲区(例如1GB好了),里面的少量数据需要给kernel用(例如只有16MB好了),但是你不能在kernel的启动前知道这16MB是哪16MB。例如可能和kernel的某个输入参数有关,运行到一般才能决定是哪些,而且可能非常零散的分布在整个1GB的中间。此时如果你需要手工传输,你唯一的办法就只能复制1GB。如果你上unified memory, 则相关的硬件引擎可能会按需的只给你传输16MB(或者多一点点)。效率截然不同。此外,unified memory还有本群之前有同学说的,超量分配之类的做法,有更多其他用法(可以当作显存不足时候的虚拟显存用)。等等吧。
8、提问:请问能否用new申请动态统一内存。managed double *p =new double[MN];这样
解答:–不能。但是可以用cudaMallocManaged()动态申请。
9、提问:现在还有用cpu并行的么?发现gpu并行比cpu并行处理数据方便多了
解答:–有的,很多算法不适合GPU。例如很标量的处理的一些任务就不适合。再例如会导致warp内的32个人无法完全步调一致的执行某些分支(CPU代码里很常见)。这些都建议上CPU。只建议在有至少32+的并行度,或者需要高速存储器访问的时候,才上GPU。例如你的以太的挖掘。很需要后者。很标量的一些处理(并行度为1,可以多少这样等价),如果必须要参杂在GPU的内部处理中,等于看一个warp的原本32个线程构成的线程束,展开为32的宽度执行的好好的,突然需要被收缩到1的标量处理。传统上,CUDA这种我们让32个人重复执行32份操作只能,浪费了31/32的性能(因为都是重复的结果)。好在从计算能力7.5开始,每个SM内部单独添加了标量单元(你可以理解成简单的CPU在里面)。这样从计算能力7.5开始,SM的构造变成了CPU标量执行 + SP矢量执行的结构。能对于完全展开的(32并行)和完全收缩的两个极限情况进行有效处理。这样一个warp可以随时处于完全收缩的状态(纯标量计算),和完全展开的(32并行)的状态自由切换,硬件无压力处理。你可以理解成,从7.5开始的SM里面,变得更像CPU的一个核心了:有普通的标量指令,也有SIMD适量指令。经典的标量+矢量架构。而非我们本次课程说的纯矢量的了。搜索NVIDIA + Turing + Uniform Path + SP字样,你将学会如何激活标量路径(没错。目前CUDA编译器会看到无条件的warp里的所有线程执行一样的重复32次的代码,自动生成标量/uniform path的指令,交给标量单元完成)。搜索AMD + GCN + 矢量单元/简单CPU + SP字样获取原始作者的信息。
追问:搜索SGPR VGPR了解前者标量/矢量寄存器
解答:–这个是AMD的叫法。我们N卡这里叫uniform register和register。使用cuda-gdb或者nsight调试的时候,查看寄存器,配合7.5+的卡,能看到这两种寄存器的存在。前者warp共享(没错,你现在有了一种叫共享寄存器的东西了),后者同名寄存器每个线程有自己的值。调试的时候很容易发现这点。一些第三方的资料也有。
追问:cpu带上了向量化sse avx,gpu带上了标量部件,我们都有美好的未来
解答:但是要考虑到,两者的虽然样子变得像了。但是一个是为了延迟而设计的,一个是为了吞吐率而设计的。还是截然不同的东西。GPU上你容易达到接近理论峰值,CPU上比较困难。CPU上你容易将单一的事情,中间夹杂少量的展开的较多的运算尽可能短的延迟内完成,但是想充分优化用到里面的每个pipe/port的全部能力,还是很折腾,远远不如GPU方便。
追问:是从7.5开始,GPU越来越靠近CPU了么?
解答:不,只是添加了标量/uniform单元,和靠近CPU是两回事。我们和美帝都有市场经济,但是我们的制度是截然不同的。
10、提问:早年我听cuda的一些介绍里面提到,代码里面最好不要引入if之类的语句,会影响效率
解答:没有关系,该用就用。注意规避divergent branch就好(warp内分支)。普通branch(例如warp为单位的分支)基本无影响。例如我有3个warp,一个干task 1,一个干task 2,一个干task 3,其实都还好。NV的手册上有一个专门的data producer -> data consumer的例子。就是使用了分支。一个warp生成数据,另外一个warp消费数据。这是官方例子。(注意我们这个是简化的讨论,例如没有讨论分支带来的指令缓存之类的负面影响)
11、提问:问一下怎么查看自己电脑上显卡的sharemem和localmem大小
解答:–你可以直接运行NV的cuda samples例子的deviceQuery看你的SM里的shared memory的最大大小。也可能根据你的卡的型号,找到计算能力(GPU-Z的网站上有),然后用计算能力,查看里的计算能力的表格的相应shared memory部分。有更详细的信息。和文字说明。例如对于99KB的shared memory如何使用之类的。
1、提问:代码里没有用到sharemem的情况下,warp运行的时候会占用sharemem的资源吗
解答:不会
2、提问:localmem是globalmem的一部分吗,还是独立于globalmem之外的,localmem的数据搬运到寄存器里需要经过其他存储单元吗
解答:不是global mem的一部分, 不需要
3、提问:请问一下,我在整理复盘这个top20求值时候有个思路,因为是采用twopass的方法进行求解,但是第二次grid等于1时,申请的线程数其实是远小于输入数据的数量,这样在ken老师提供的insert_value函数会在最初赋值的时候调用很多次,如果多调用几次核函数,使最后一次就final的时候申请的线程数大于等于输入的数据,那么执行速度会更快吗
解答:如果你原始问题规模非常大。例如有16M组中间结果,那么直接将16M组中间结果只用1个block处理就比较吃亏了。此时再上一组很多个blocks的中间kernel处理一次就比较好了。当然,考虑到NX只有6个SM,最坏的情况下只能中间的额外kernel,同时驻留6个blocks(和blocks的资源使用,线程规模有关,不一定)。在NX上的最坏情况中间kernel只能提供6X的并行度。这里可能会影响和直接上最终1个block的时间对比。在台式卡上 + 中间结果很多组的时候,应当3次效果比较好。其他时候可能只有两次比较好。具体哪种在目标问题上更好,不能直接得到答案。但是你总是可以去实验。以及,你使用的是NX。不是Nano的。你的文章可能提到的Nano是其他同学的。你的这个硬件要稍微好一些。如果是在Nano上,情况会更加糟糕。根据block使用的资源不同,和线程规模不同。你中间执行的那个kernel,最坏情况只能有1个block,上在只有1个SM的nano身上。此时最坏的中间的真正的并行度只有1X。和直接上最终的kernel没太大区别了。这也可能是你为何没实验出来效果的一个因素。
追问:新启动kernel要花费时间吧?还是可以忽略不计?
解答:启动kernel本身(例如只有1个block,立刻return的kernel)几乎可以认为没有成本。但是kernel里面要处理数据的话,则整个执行时间就可能很长很长了(和实际启动有关)。空kernel只有us级别的时间占用的。
追问:新启动kernel是否会有准备数据的时间?因为如果数据规模非常大,中间结果可能内存不连续。
解答:Kernel启动大概有5微秒,有些时候合并能合并的kernel能加速一些,不过这种一般来说不是一个CUDA项目中最主要的优化。