两级调度架构:任务级调度与硬件划分

好,咱们今天聊点硬核的。两级调度架构,说白了就是把调度这件事拆成两个层次来做。为什么要拆?你想想看,GPU里动辄几千个线程,如果让一个调度器管所有事,那这个调度器的逻辑复杂度、面积、功耗都会爆炸。我早年做过一个项目,一开始想用单级调度硬扛,结果综合出来的面积直接超标30%——嗯,从那以后我就老老实实做两级了。

任务级调度:CTA与Warp

先说说第一级,任务级调度。这一级管的是CTA(Cooperative Thread Array,也叫Thread Block)。CTA是用户程序里定义的一个线程组,它们共享shared memory,可以同步。调度器要做的,就是从所有待执行的CTA里挑一个出来,分配给某个SM。

我个人习惯把CTA调度看作“粗粒度调度”。为什么?因为CTA之间的依赖很少,基本上可以独立执行。调度器只需要维护一个待调度CTA队列,然后按某种策略(比如Round-Robin、优先级、或者最老优先)往外派发就行。

关键点:CTA调度器不关心内部线程怎么跑,它只负责把CTA“扔”给SM。至于SM怎么处理这个CTA,那是第二级的事。

第二级是Warp级调度。一个CTA里包含多个Warp(通常32线程一组)。SM拿到CTA后,Warp调度器开始干活——它要决定下一个发射哪个Warp的指令。这里有个坑:Warp之间可能有数据依赖,比如一个Warp要等另一个Warp写完shared memory才能读。所以Warp调度器必须维护每个Warp的状态:就绪、等待、阻塞等等。

我记得有一次调试一个性能问题,发现某个Warp一直处于“等待”状态,死活不发射。查了半天,原来是shared memory bank conflict导致写操作延迟,后续的读Warp被卡住了。后来我们在Warp调度器里加了一个“饥饿检测”逻辑,超过一定周期还没发射的Warp会被提升优先级——这招挺管用。

两级调度器硬件划分

硬件上怎么划分?我建议这样看:第一级调度器放在全局调度单元(Global Scheduler),第二级放在每个SM内部。这样划分的好处很明显——第一级不用管SM内部的细节,第二级不用管全局的CTA分配,各管各的,互不干扰。

层级 位置 调度对象 调度粒度 硬件复杂度
第一级 全局调度单元 CTA 粗粒度(数百线程)
第二级 SM内部 Warp 细粒度(32线程)

这里有个设计取舍:第一级调度器的队列深度设多大?设小了,SM容易饿着;设大了,面积受不了。我一般建议按SM数量的2~3倍来设。比如你有16个SM,那全局CTA队列深度设到32~48就差不多了。当然,具体还要看你的应用场景——如果是图形渲染,CTA数量多但生命周期短,队列可以浅一点;如果是通用计算,CTA可能跑很久,队列就得深一些。

避坑指南:我曾经在一个项目里把全局队列设得太深,结果综合出来的FIFO占了整个调度单元面积的40%。后来改成动态分配——用链表管理空闲槽位,队列深度可以灵活调整。虽然逻辑复杂了点,但面积省了将近一半。

调度流水线设计

调度器不是一拍就能出结果的。它需要读状态、做仲裁、写寄存器、更新队列——这些操作如果串行做,一个周期根本搞不定。所以我们要把它流水线化。

典型的调度流水线分三级:

  1. 取指阶段(Fetch):从待调度队列里取出下一个候选。如果是第一级,就是取CTA;如果是第二级,就是取Warp。
  2. 仲裁阶段(Arbiter):根据优先级、依赖关系、资源可用性做决策。这一级最复杂,因为要比较多个候选的状态。我习惯用Tree Arbiter结构,延迟小,面积也还行。
  3. 提交阶段(Commit):把调度结果写回去——更新队列、设置状态寄存器、通知执行单元。这一级要小心写冲突,尤其是多个调度器同时提交的时候。

嗯,这里要注意:流水线级数不是越多越好。级数多了,虽然频率能跑高,但调度延迟也大了。如果调度延迟超过一个Warp的执行时间,那流水线就白搭了——你还没调度完,人家已经跑完了。我一般控制在3~4级,再往上收益就不明显了。

警告:调度流水线里最容易出bug的地方是“空泡处理”。比如队列空了,流水线要不要stall?还是直接插入NOP?我见过一个设计,队列空的时候流水线继续跑,结果把无效的调度结果写进了执行单元,导致整个SM挂掉。所以一定要加valid信号,每个阶段都要检查。

最后说一个实战技巧:两级调度器之间怎么通信?第一级调度器把CTA分配给SM后,SM内部的Warp调度器才开始工作。但SM可能同时处理多个CTA,每个CTA有自己的Warp集合。这时候Warp调度器需要知道当前在跑哪个CTA,以及这个CTA的Warp数量。我建议在第一级调度器和SM之间加一个“CTA描述符”接口,包含CTA ID、Warp数量、shared memory大小等信息。这样Warp调度器拿到描述符后,就能自己管理了,不用再回头问全局调度器。

好了,两级调度架构的核心就这些。说白了就是:第一级管粗活,第二级管细活,中间用流水线串起来。你想想看,是不是这个理?