前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >DAY52:阅读scheduling

DAY52:阅读scheduling

作者头像
GPUS Lady
发布2018-08-01 14:58:53
4580
发布2018-08-01 14:58:53
举报
文章被收录于专栏:GPUS开发者GPUS开发者
我们正带领大家开始阅读英文的《CUDA C Programming Guide》,今天是第52天,我们正在讲解CUDA C语法,希望在接下来的48天里,您可以学习到原汁原味的CUDA,同时能养成英文阅读的习惯。

今天的内容比较特殊,因为这个部分并没有出现在NVIDIA 在线版的《CUDA C Programming Guide》,但是如果你下载了CUDA,里面会带一份PDF电子档的版本,你会发现这个版本确实有这个章节。这个章节蛮重要的,虽然我们不明白为啥没有出现在在线版里,我们还是决定讲一讲。

本文备注/经验分享:

本章节主要涉及两点, 一个是新引入的nanosleep, 另外一个则是一个锁的优化版本的实现.从以前的章节中, 大家都知道, 只要NV开始引入的东西, 那么一般都会在后期发挥重要用途. 例如前几天我们刚刚说过的warp vote, warp shuffle, FP16半精度,以及还有更早的INT8之类的. 而今天的__nanosleep()扩展函数, 则是从CUDA 9.2和计算能力7.0+开始引入的新硬件特性.允许你的一个或者多个线程(请注意不是warp)进入暂停调度状态.也就是字面意思的休眠, 过了一定的纳秒后(如果是1Ghz的GPU频率, 那么正好1ns等于1个周期, 其他频率请读者自行换算) 读者可能会问, GPU不是应当充分忙碌才好吗?不是你之前发的章节, 还要要通过TLP(线程间并行), 和ILP(线程内部指令并行)来提高GPU的忙碌程度, 来掩盖延迟吗?为何NV却在新卡中反其道而行之, 弄了这么一个暂停调度(也就是说, 将暂停执行)一定时间的函数? 因为有的时候, 一些特殊的算法, 并非是越快执行越好的. 本章节后面实质性的给出了一个例子.是一个互斥锁(mutex)的实现. mutex是互相排斥的缩写, 指的是计算机程序里面,用来保护某些不能被同时访问的资源(例如, 你需要同时将2个不同位置的int都+1, 统一完成后才能继续, 中途不允许被打断,而GPU常规状态总是大量线程在并行的, 不使用一定的手段---例如本章节的互斥锁, 是无法做到的),有了这种互斥锁后, 才能在越来越复杂的GPU环境中,例如GTX1080才20个SM, 到了Titan-V就已经80个SM了,使用越来越复杂的算法.而本次NV引入的NANOSLEEP, 则可以有效的提升该mutex锁的性能.实际上, NV官方曾经在今年春天的时候,推介过一次这个.当时有个图, 我印象还很深刻(不是GTC), 使用了新的__nanosleep()后, 竞争mutex剧烈的应用场合,性能提升了大约10-100倍.因此可以看出此新扩展函数, 还是很有用的. 考虑到很多读者并非来自CS行业, 我们继续简单的说一下后面这个例子,实际上这个例子在之前的原子操作章节(前几天)遇到过.只是今天的这个章节上, NV提供了一个稍微优化点的明确的mutex版本. 如同任何锁一样, 都具有2个基本操作, 和如下流程: (1)基本操作1: 多个线程竞争一个锁的所有权, 互斥锁(mutex)最终只能有1个归属权. 也就是最终只有1个线程能竞争成功(如同很多同时追你的帅哥们, 最终只有1人会成功) (2)取得锁的所有权的线程(竞争成功的), 可以操作被该锁保护的资源(例如刚才说的2个int, 都把它们+1) (3)基本操作2: 完成操作过程后, 可以直接释放掉所有权. 此时可以循环到1, 其他线程可以继续竞争得到刚才释放掉的所有权(等于一个妹子分手了, 另外一个帅哥就可以上来竞争了) 这是一个典型的mutex锁的使用流程. 对于一些不能直接通过前几天的原子操作章节里面的基本原子操作函数完成的任务(例如刚才说的2个int的+1, 而不是1个---后者可以直接原子操作) 需要使用类似这种风格进行. 所以你看到了, 实质上的对mutex的利用其实可以明确的分成两点: (1)竞争锁和释放锁, 这往往是由成熟的库或者自行写的代码或者第三方提供的代码完成(例如本章节NV提供的); (2)你自己夹杂在中间的处理逻辑. 这个是用户自己提供或者说进行的; 所以一般的来说, 我建议大家尽量将前一部分采用公开的成熟的实现, 这样比较安全.而中间的部分则是自己可以发挥.这样对你好,也对我们好. 这是对锁的简单介绍.(互斥锁. 其它种类的锁请自行谷歌) 回到NV的具体代码, 我们可以简单的分析看到: mutex是一个锁(的位置), 里面是通过存放0或者1代表锁空闲, 可以被取得; 还是锁当前被占用的状态(本章节没说, 但是从代码中可以看出来). 而NV已经为你写好了加锁和解锁函数, 在加锁函数中: while(atomicCAS(mutex, 0, 1) == 1) 而解锁函数中: atomicExch(mutex, 0); 我们首先可以看到, 该锁的2个函数是通过基本的原子操作函数来构建的. 这也是我们前几天我们在原子操作的章节里说, 这些基本的原子操作函数是实现高层逻辑的基础.然后再请注意, 竞争取得锁的过程是一个循环, 反复尝试.而释放锁的过程却是一句简单的普通语句(无循环). 大家可以想想为什么. 其实很简单, 因为互斥锁每时每刻都只能有1个线程取得了所有权, 其他线程都在失败后(未能取得)继续循环, 尝试下一次机会(还记得刚才的帅哥追妹子的例子么). 因为mutex这里指向的位置, 当存放为0的时候才代表能取得, 只有当atomicCAS的结果返回是0, 不是1, 才代表成功了. 而一旦成功, 就立刻改变里面的值为1(被占用),其他人只能继续尝试.而解锁的时候, 因为理所当然的有所有权, 直接设定为0即可. 这是一个很简单的过程.如果刚才的循环while后面直接是分号. 该锁的2个函数(加解锁)已经可用了. 但是没完, NV在括号里面引入了__nanosleep(), 很好的提高了该锁的性能(还记得刚才说的NV春天发布的推介么),这里实际上存在一个忙等和避让的概念. 因为你知道, 刚才我们说过, 使用该锁是一个三步的过程: (1)加锁, 取得所有权. (2)慢慢处理被锁保护的资源 (3)处理完成后, 解锁. 实际上你可以看到, 一旦一个线程取得了锁, 就会进入过程2, 而此时过程2可能消耗不定的时间, 例如可能需要20ns才能完成. 如果此时, 其他未能取得所有权的线程, 在这个20ns时间内, 依然反复尝试取得锁, 实际上是一种浪费(因为完全不存在可能)——就如同一个妹子刚刚确定了恋爱关系, 你还在不停的献殷勤, 显然是无用的。 此时持续的竞争的这种浪费, 占用了宝贵的SP资源, 也占用量宝贵的功耗预算.而此时如果像NV这个代码这样, 加入了一定量的时间的等待, 则可以释放出来宝贵的SP周期, 例如可以贡献给竞争成功的那个线程更快的执行, 让他能尽快的释放掉锁.反而可能对性能和整体功耗有利.这就是为何之前开头的时候说, 有的时候并非让GPU不停的忙碌就好.(这就如同你估计妹子谈恋爱大约经过1个月后就会分手, 你可以先不理这个妹子1个月, 然后1个月后再考虑重新追她. 更有效率的),所以你看到, __nanosleep还是很有用的.不仅仅适合对于锁.对于很多其他lock-free的算法, 一些需要轮询(polling)的场合(轮询就是程序反复查询一个状态是否成立或者满足),也可以考虑使用它. 都是很好的应用场景. 本章节大致说了这些, 但实际上, 我们不仅仅只应当看到这么一点.

__nanosleep目前被编译为单独的一条指令(nanosleep指令, 同名) 从之前我们曾经说过的7.0+开始放宽SIMT的限制了(还记得我们之前说过SIMT带来的弊端和代价么? 还记得为何新版本的一些函数变成了__sync后缀了么?),到今天__nanosleep的提供.从本质上来说, 7.0+的GPU越来越靠拢CPU的核心的执行风格了.(例如某种程度上不再绑定32个线程在一起, 而像CPU那样能自由执行) 所以今天引入了__nanosleep()来支援一些原本CPU上常用的算法也很正常了. 所以不仅仅传统的GPU代码可以很好的运行, 一些更接近以前的一些CPU风格的代码或者算法, 也开始有用武之地了.这是一个很好的改变. 所有的CUDA用户都应该迎接这种改变. 以及, 本章节还需要注意的是,这里的休眠引入了指数避让.第一次避让8ns, 失败后避让16ns, 再次失败后避让32ns,直到最大限制256ns,还有其他的避让方式的. 感兴趣的用户可以自行搜索.

有不明白的地方,请在本文后留言

或者在我们的技术论坛bbs.gpuworld.cn上发帖

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GPUS开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档