GPGPU-sim - Performance Simulation Engine
1 Performance Model Software Objects
ldst_unit *m_ldst_unit;
前面的 m 可能表示这个类型的变量
1.2 SIMT Core Class
SIMT Core 中的微架构在 shader.h/cc 的类 shader_core_ctx 中实现
- shd_warp_t objects 的集合用于建模每个 warp 在 core 中的状态
- simt_stack object, 处理每个 warp 的分支
- set of scheduler_unit obj, 选择 set 中 warp 的一条 or 多条指令发射执行
- Scoreboard obj 处理 data hazard
- opndcoll_rfu_t obj, model operand collector
- set of
simd_function_unit
obj 实现 ALU pipeline ldst_unit
实现 memory pipelineshader_memroy_interface
将 SIMT Core 连接到相应的 SIMT Core Cluster
每个 core cycle, 调用 shader_core_ctx::cycle()
来模拟 SIMT Core 的一个 cycle。cycle function 以按从下往上的顺序 (也就是从 writeback() 到 fetch()) 调用下列函数
- fetch()
- decode()
- issue()
- read_operand()
- execute()
- writeback()
多个流水线阶段通过一组指向 warp_inst_t
的流水线寄存器链接 (除了 Fetch and Decode, 它们通过 ifetch_buffer_t
连接)
也就是 Fetch 和 Decode 函数中的变量 m_inst_fetch_buffer
当访问特定于 SIMT core 的配置选项时,每个 shader_core_ctx
对象引用一个公共 shader_core_config
对象。所有 shader_core_ctx
对象也链接到 shader_core_stats
对象的一个普通实例,该对象跟踪所有 SIMT core 的一组性能测量值。
1.2.1 Fetch and Decode Software Model
I-Buffer 在 shd_warp_t
类中作为数组实现,每个 shd_warp_t
有集合 m_ibuffer,默认大小为 2
可以修改 IBUFFER_SIZE 来调整每个 warp 的 I-Buffer slot 大小
shd_warp_t
也有标志位决定 warp 的 eligibility 以备发射。解码的指令存在 ibuffer_entry,作为指针指向 warp_inst_t
object, warp_inst_t
保留指令使用的操作类型和操作数的信息
Fetch
如果 decode stage 没有 stall, 即 m_inst_fetch_buffer
没有有效指令,那么 fetch unit 就可以工作 (shader_core_ctx::m_inst_fetch_buffer
作为 fetch 和 decode stage 之间的 pipeline register)
也就是 fetch() 函数中的 !m_inst_fetch_buffer.m_valid 时进入最外层 if 语句
外层循环用于实现轮询调度器,最新调度的 warp id 存在 m_last_warp_fetched
。
在 fetch() 函数中,进入最外层 if 语句,以及第2层中的 else 语句后,
- 第3层中的第1个 if 语句检查 warp 是否已经完成执行
- 第3层中的第2个 if 语句完成实际的从指令 cache 中取数据 (hit),或者生成内存访问 (miss) 的操作。
- 第2个 if 语句主要是检查当前 warp 对应的 entry 是否已经存储了有效的指令
- 条件语句中的
m_warp[warp_id]->ibuffer_empty()
- 条件语句中的
- 第2个 if 语句主要是检查当前 warp 对应的 entry 是否已经存储了有效的指令
Decode
decode stage 简单地检查 shader_core_ctx::m_inst_fetch_buffer
,然后开始解码指令,当前配置是一个周期解码2条指令,并将其存入对应的 I-Buffer entry (也就是 m_ibuffer
)
1.2.2 Schedule and Issue Software Model
每个 core 中,有数量可配置的 scheduler unit. 在函数 shader_core_ctx::issue()
中,会使用一个 for loop 遍历这些 scheduler unit,每个 scheduler unit 都调用 scheduler_unit::cycle()
函数
- 在
scheduler_unit::cycle()
函数中,通过调用shader_core_ctx::issue_warp()
将指令发送到执行流水线 - 在
shader_core_ctx::issue_warp()
函数中,指令通过调用shader_core_ctx::func_exec_inst()
来执行,调用updateSIMTStack()
中的simt_stack::update()
来更新 SIMT Stack。指令也会因为warp_t:set_membar()
以及set_t::warp_reaches_barrier
而等待/释放
另一方面,寄存器信息由 Scoreboard::reserveRegisters()
保存。scheduler_unit::m_sp_out, scheduler_unit::m_sfu_out, scheduler_unit::m_mem_out
分别指向 SP, SFU, Mem 流水线的 issue stage and execution stage 之间的第一个 pipeline register。这也是为什么在每条指令发射之前都要检查这些单元
1.2.3 SIMT Stack Software Model
每个scheduler unit 有一个 SIMT stacks. 每个 SIMT stack 对应一个 warp
所以 SIMT Stack 可以作为 warp scheduler unit 中的硬件实现,为一个 warp 服务
在 scheduler_unit::cycle()
函数中,被调度的 warp 对应的 SIMT Stack 中的栈顶 entry 决定被发射的指令。栈顶 entry 的 PC value 通常与该 warp 对应 I-Buffer 下一条指令的 PC value 一致 (一致说明没有出现分支)。否则出现 control hazard, 它们如果不匹配,I-Buffer 中的指令会被 flush.
也就是说这是一个简单的分支跳转检测机制。SIMT Stack 的栈顶存放的是下一条指令,也就是 next PC value. 无跳转情况下 I-Buffer 中的下一条指令的 PC value 应该和 SIMT Stack 栈顶的 next PC value 一致。
如果不一致说明出现了跳转,那么在 I-Buffer 中的下一条指令的 PC value 是无效的,需要刷掉 I-Buffer
❓ SIMT Stack 在类 simt_stack
中实现。SIMT Stack 在每次发射后通过函数 simt_stack::update(...)
更新。函数 simt_stack::update(...)
(in abstarct_hardware_model.cc) 实现了在发散点和聚合点所需的算法。Functional execution (参见4.5,这个部分讲述了指令的执行) 是在更新 SIMT stack 之前的发射阶段执行的。这允许发射阶段拥有每个线程的 next PC 的信息,因此,可以根据需要更新 SIMT stack.
Functional execution 是什么?可能要看了4.5才知道
1.2.4 Scoreboard Software Model
scoreboard unit 在 shader_core_ctx
作为成员对象被实例化,通过引用 (指针) 传递到 scheduler_unit
。
理解为 scoreboard 作为 core 的一个硬件单元
它存储了 shader core id (m_sid
) 和一个通过 warp id 索引的寄存器表 (reg_table
)。寄存器表存储了每个 warp 保留的寄存器的数量。 函数 Scoreboard::reserveRegisters(...), Scoreboard::releaseRegisters(...) and Scoreboard::checkCollision(...)
分别用于保留寄存器,释放寄存器,在 warp 发射前检查冲突。
1.2.5 Operand Collector Software Model
Operand Collector 建模在主流水线的一个阶段,由函数 shader_core_ctx::cycle()
调用执行。这个阶段由函数 shader_core_ctx::read_operands()
表示。关于 ALU Pipeline 的更多细节在下一节 1.2.6
类 opndcoll_rfu_t
基于寄存器文件单元建模了 operand collector. 包括 collector unit 集合,仲裁器,dispatch 单元的抽象。
opndcoll_rfu_t::allocate_cu(...)
用于将 warp_inst_t
分配到给定 operand collector 集合中的空闲 operand collector. 它也将所有源操作数的读请求添加到仲裁器中相应的 bank 队列
然而,opndcoll_rfu_t::allocate_reads(...)
处理没有冲突的读请求,也就是不同寄存器 bank 中并且没有进入同一个 operand collector 的读请求将从仲裁器队列中出队。写请求总是优先于读请求。
函数 opndcoll_rfu_t::dispatch_ready_cu()
会 dispatch ready operand collectors 的操作数寄存器到执行阶段
函数 opndcoll_rfu_t::writeback( const warp_inst_t &inst )
在内存流水线的写回阶段被调用。用于分配写请求。
上述内容总结了建模 operand collector 的主要函数的重点,更多的细节在源代码中的 shader.cc/h
中的类 opndcoll_rfu_t
1.2.6 ALU Pipeline Software Model
SP 单元和 SFU 单元的时间模型主要在 shader.h
中的类 pipelined_simd_unit
中实现。建模这两个单元的特定类 (sp_unit
and sfu
) 派生自这个类 (pipelined_simd_unit
),其中包含被重写的成员函数 can_issue()
,用于指定单元可执行的指令类型。
比如源代码中,类
sfu
可以执行执行类型SFU_OP, ALU_SFU_OP, DP_OP
SP 单元通过流水线寄存器 OC_EX_SP
连接 operand collector, SFU 单元通过流水线寄存器 OC_EX_SFU
连接 operand collector. 两个单元通过流水线寄存器 WB_EX
共享写回阶段。为了避免两个单元在写回阶段冲突而停滞,每一条进入任一单元的指令都必须在结果总线(m_result_bus
)中分配一个 slot,然后才会被发送到指定单元 (细节在 shader_core_ctx::execute()
)
OC -> operand collector
下图说明了 pipelined_simd_unit
如何建模不同类型指令的吞吐量和延迟
在每个 pipelined_simd_unit
, 成员函数 issue(warp_inst_t*&)
将给定的流水线寄存器的内容移动到 m_dispatch_reg
. 指令在 m_dispatch_reg
中等待 initiation_interval
cycles. 同时,没有其他指令被发射到这个单元,所以这个等待建模了指令的吞吐量。在等待后,指令派遣 (dispatch) 到内部流水线寄存器 m_pipeline_reg
以建模延迟 (可以得到指令等待了多久才被派遣到 m_pipeline_reg
)。派遣位置是确定的,这样花费在 m_dispatch_reg
中的时间也被计入延迟。每个周期,指令将通过流水线寄存器前进,最终进入 m_result_port
,这是 SP and SFU 单元的公共回写阶段的共享流水线寄存器 (WB_EX
)。
每种指令类型的吞吐量和延迟在 cuda-sim.cc
中的ptx_instruction::set_opcode_and_latency()
中指定。这个函数在预解码期间被调用。
指令吞吐量,一个 cycle 内处理指令的条数,也就是 ipc?
延迟也就是指令等待的时间
指令执行吞吐一般指的是每个时钟周期内可以执行的指令数目,不同指令的吞吐会有所不同。通常GPU的指令吞吐用每个SM每周期可以执行多少指令来计量。对于多数算术逻辑指令而言,指令执行吞吐只与SM内的单元有关,整个GPU的吞吐就是每个SM的吞吐乘以SM的数目。
主要受以下因素影响
- 功能单元的数目
- 指令Dispatch Port和Dispatch Unit的吞吐。
- 一个 warp 的指令要发射,首先要 eligible, 也就是没有等待 cache miss, 通过了 scoreboard 等待。
- 其次要被 warp scheduler 选中,由 Dispatch Unit 发送到相应的 Dispatch Port. Kepler、Maxwell和Pascal是一个Warp Scheduler有两个Dispatch Unit,所以每cycle最多可以发射两个指令,也就是双发射。而Turing、Ampere每个Warp Scheduler只有一个Dispatch Unit,没有双发射,那每个周期就最多只能发一个指令。但是Kepler、Maxwell和Pascal都是一个Scheduler带32个单元(这里指full-throughput的单元),每周期都可以发新的warp。而Turing、Ampere是一个Scheduler带16个单元,每个指令要发两cycle,从而空出另一个cycle给别的指令用。
- 最后要求Dispatch Port或其他资源不被占用,port被占的原因可能是前一个指令的执行吞吐小于发射吞吐,导致要Dispatch多次,比如Turing的两个FFMA至少要stall 2cycle,LDG之类的指令至少是4cycle。更详细的介绍大家可以参考之前的专栏文章。
- GPR读写吞吐。绝大部分的指令都要涉及GPR的读写,由于Register File每个bank每个cycle的吞吐是有限的(一般是32bit),如果一个指令读取的GPR过多或是GPR之间有bank conflict,都会导致指令吞吐受影响。GPR的吞吐设计是影响指令发射的重要原因之一,有的时候甚至占主导地位,功能单元的数目配置会根据它和指令集功能的设计来定。比如NV常用的配置是4个Bank,每个bank每个周期可以输出一个32bit的GPR。这样FFMA这种指令就是3输入1输出,在没有bank conflict的时候可以一个cycle读完。其他如DFMA、HFMA2指令也会根据实际的输入输出需求,进行功能单元的配置。
- 很多指令有replay的逻辑(参考Greg Smith在StackOverflow上的一个回答)。这就意味着有的指令一次发射可能不够。这并不是之前提过的由于功能单元少而连续占用多轮dispath port,而是指令处理的逻辑上有需要分批或是多次处理的部分。比如constant memory做立即数时的cache miss,memory load时的地址分散,shared memory的bank conflict,atomic的地址conflict,甚至是普通的cache miss或是TLB的miss之类。根据上面Greg的介绍,Maxwell之前,这些replay都是在warp scheduler里做的,maxwell开始将它们下放到了各级功能单元,从而节约最上层的发射吞吐。不过,只要有replay,相应dispath port的占用应该是必然的,这样同类指令的总发射和执行吞吐自然也就会受影响。
- L1 cache miss 也有进行 replay 的逻辑
1.2.7 Memory Stage Software Model
shader.cc
中的类 ldst_unit
实现了 shader 流水线的内存阶段。这个类实例化了所有 in-shader 内存的操作: texture (m_L1T
), constant (m_L1C
) and data (m_L1D
). ldst_unit::cycle()
implements the guts of the unit’s operation and is pumped m_config->mem_warp_parts
times pre core cycle. 完全对齐的内存访问 (fully coalesced memory access) 可以在一个 shader cycle 内被处理。ldst_unit::cycle()
处理来自 interconnect 的内存响应 (存在 m_response_fifo
), 填充 cache 并将存储标记为完成。这个函数还 cycles the caches 以便它们可以将未命中数据的请求发送给 interconnect.
每种 L1 内存类型的 cache 访问分别在 shared_cycle(), constant_cycle(), texture_cycle()
and memory_cycle()
中完成。memory_cycle()
用于访问 L1 data cache. 每个函数会调用 process_memory_access_queue()
, 这是一个通用函数,它从指令内部访问队列中提取访问并将这个请求发送到 cache 中。如果访问不能再这个 cycle 中被处理 (即没有未命中,也没有命中 (也就是之前介绍中提到的第三个状态 reserved fail),这可能发生在系统队列满的时候,或是所有 cache line reserved 但还没有 fill),那么会在下个 cycle 再次试图访问。
值得注意的是,并不是所有的指令都到达单元的写回阶段。在所有请求的 cache block 命中时,所有的 ST 指令和 LD 指令都将在 cycle
函数中退出流水线。这是因为它们不需要等待来自 interconnect 的响应,并且可以 by-pass 写回逻辑,该逻辑保存指令所请求的 cache line 和那些已经返回的 cache line。
1.2.8 Cache Software Model
gpu-cache.h
实现了 ldst_unit
用到的所有 cache. constant cache and data cache 都包含一个成员对象 tag_array
,用于实现保留和替换的逻辑。函数 probe()
检查 cache block 地址而不影响相关数据的 LRU 位置, 函数 access()
旨在建模影响 LRU 位置的查找,生成未命中和访问的统计信息。MSHR 用类 msgr_table
建模,它建模了一个全相联 table, 合并有限数量的请求。请求通过 next_access()
函数从MSHR释放。
类 read_only_cache
被 constant cache 使用,并作为类 data_cache
的基类。这个层次结构可能有点令人困惑,因为 R/W data cache 是从 read_only_cache
扩展的 (理解为 R/W data cache 就是在只读 cache 类基础上加了一些功能来实现,从代码角度很好理解这句话)。原因是它们共享很多相同功能的函数,除了函数 access
需要写 data_cacje
这一点有所区别。L2 cache 也是通过类 data_cache
实现。
这一点从 C 语言中类的角度去理解就好
类 tex_cache
实现 texture cache. 它没有使用 tag_array
或是 mshr_table
,因为它的操作和传统 cache 不太一样
1.2.9 Thread Block / CTA / Work Group Scheduling
Thread Block 向 SIMT cores 的调度在 shader_core_ctx::issue_block2core(...)
中实现。一个 core 中可以并行调度的最大 block 数量由函数 shader_core_config::max_cta(...)
计算。这个函数基于程序中定义的 ThreadPerBlock (CUDA 编程中由程序员定义), 以及每个线程的寄存器使用情况、共享内存使用情况以及每个 core 的最大线程块数量的配置限制来计算上述 可以并行调度的最大 block 数量。具体地说,计算可以分配给 SIMT core 的 block 数量,上面的每个标准都是限制因素。其中的最小值是可以分配给 SIMT core 的最大 block 数量。
各种资源中的短板决定了最多可以有多少个 block 并行。CUDA 编程的知识
在函数 shader_core_ctx::issue_block2core(...)
中,block size 首先被填充为 warp size 的倍数。然后决定一个空闲硬件 thread id 的范围。通过调用函数 ptx_sim_init_thread
来初始化每个线程的函数状态。调用函数 shader_core_ctx::init_warp
初始化 SIMT stack and warp state.
当每条线程完成,SIMT core 调用函数 shader_core_ctx::init_warp(...)
来更新 active thread block 的状态。当 block 内所有线程完成执行, 同一个函数会减少 core 上 active block 的数量,允许在下一 cycle 调度更多 block. 从 pending kernels 中选择要调度的新线程块。
2 Interconnection Network
Interconnection Network 接口有以下几个功能。这些函数在 interconnect_interface.cpp
中实现。这些函数被封装在 icnt_wrapper.cpp
中。使用icnt_wrapper.cpp
的最初目的是允许其他 network 模拟器连接到 GPGPU-Sim。
init_interconnect()
: 初始化网络模拟器。它的输入是互连网络的配置文件和SIMT核心团簇和内存节点的数量。interconnect_push()
: 指定源节点、目的节点、指向要传输的数据包的指针和数据包大小(以字节为单位)。interconnect_pop()
: 获取一个节点号作为输入,并返回一个指向在该节点等待被弹出的数据包的指针。如果没有数据包,则返回NULL。interconnect_has_buffer()
: 获取作为输入要发送的节点号和数据包大小,如果源节点的输入缓冲区有足够的空间,则返回1 (true)。advance_interconnect()
: 应该在每个互连时钟周期被调用。顾名思义,它在一个周期内完成网络的所有内部步骤。interconnect_busy()
: 如果网络中有一个正在传输的数据包,则返回1 (true)interconnect_stats()
: 打印网络统计信息。
5 Memory Partition
l2cache.cc/h 中有 class memory_partition_unit
II. L2 Cache Model
mem_fetch *mf = m_L2cache->next_access();
产生等待 fill MSHR entry 的内存请求的 reply,