15个Verilog文件造出一颗GPU:tiny-gpu极简并行架构拆解
【免费下载链接】tiny-gpuA minimal GPU design in Verilog to learn how GPUs work from the ground up项目地址: https://gitcode.com/GitHub_Trending/ti/tiny-gpu
如果你只能给一颗GPU写11条指令,你会怎么选?tiny-gpu给出了答案——这个极简GPU用不到2000行Verilog把GPU并行计算的骨架完整搭了出来:取指、译码、多线程SIMD执行、异步内存访问,一个不少,图形渲染一概不要。
🏭 最小可运行的GPU长什么样?
tiny-gpu的定位一句话:剔除一切图形渲染相关硬件,只保留"通用计算GPU"的核心——并行执行的计算核心、块级线程调度和带带宽控制的内存控制器。
仓库分三层,结构一目了然:
- 硬件实现:
src/下的12个SystemVerilog模块,如 顶层GPU、计算核心、指令译码器、算术逻辑单元、线程调度器、取指单元、装载存储单元 - 内核代码:
gds/下是烧进程序内存的内核镜像,对应矩阵加法和矩阵乘法两个程序 - 仿真测试:
test/下的Python + cocotb脚本负责驱动仿真、校验结果、打印执行轨迹
整体架构长这样,左侧是GPU顶层(控制寄存器、分发器、计算核心、内存控制器),右侧是单个计算核心内部:
核心模块的参数化设计值得看一眼,线程数、地址位宽、数据位宽全由参数决定:
// src/core.sv module core #( parameter DATA_MEM_ADDR_BITS = 8, parameter DATA_MEM_DATA_BITS = 8, parameter PROGRAM_MEM_ADDR_BITS = 8, parameter PROGRAM_MEM_DATA_BITS = 16, parameter THREADS_PER_BLOCK = 4 ) ( input wire clk, input wire reset, input wire start, output wire done, ... );想改成8线程并行?改一个参数的事。
🧵 一个线程的"一生":GPU指令流水线六步拆解
现在追踪一条MUL指令在核心里走完全部生命周期的过程。
FETCH(取指):取指单元从程序内存里把PC指向的那条16位指令读出来——相当于拿到一张订单。
DECODE(译码):译码器把指令拆成控制信号:目标寄存器是几、源操作数是几、该启用哪条数据通路——相当于看懂订单上点的是什么。
REQUEST(请求):如果是LDR/STR访存指令,就向内存控制器发请求——相当于点外卖下单。非访存指令直接跳过这步。
WAIT(等待):等内存应答。真实GPU里这是最耗时的阶段,tiny-gpu把它单独做成一个状态,整个控制流都围绕这个长延迟来搭。
EXECUTE(执行):ALU按译码结果做加法、乘法、比较——骑手送到了,后厨开始做菜。
UPDATE(更新):结果写回本线程的寄存器堆,PC加1——上菜,开下一单。
六步串起来的核心控制流如下图所示:
你注意看,这条流水线上有个巧妙的设计:每个线程的寄存器堆里预置了三个只读特殊寄存器——%blockIdx(块编号)、%blockDim(块维度)、%threadIdx(线程编号)。当内核执行MUL R0, %blockIdx, %blockDim再ADD R0, R0, %threadIdx时,同一条指令被"分裂"到4个线程上,各用各的%threadIdx算出不同的全局索引,各取各的数据——这就是SIMD执行模型的全部硬件秘密。代价是调度器假设所有线程每条指令结束后都收敛回同一个PC,后面的扩展章节会讲怎么打破这个假设。
📜 11条指令撑起整个GPU:极简ISA设计
tiny-gpu的指令集按用途正好分三组。
数据搬运:LDR、STR负责全局内存读写,CONST把立即数装进寄存器。
算术运算:ADD、SUB、MUL、DIV四则运算,CMP比较两个寄存器并把负/零/正结果写进NZP标志位。
控制流:BRnzp根据NZP标志跳转,RET标记线程执行结束,外加一个NOP空操作。加上CMP和BRnzp这套组合,循环和条件语句都能写出来。
x86有数千条指令,tiny-gpu只留下能跑矩阵运算的骨架。指令是16位定长:高4位操作码,低12位操作数字段——4位寄存器编号正好覆盖16个寄存器(R0~R12通用,后3个是上面说的只读线程寄存器),低8位在CONST里复用为立即数。定长的好处很实际:decoder.sv 的译码逻辑就是一张case表,硬件成本几乎可以忽略。
🏁 从零到跑通:编译、仿真与第一次矩阵乘法
环境三件套:iverilog(Verilog编译器)、cocotb(Python测试框架)、sv2v(SystemVerilog转Verilog)。
# 安装 Verilog 工具链 brew install icarus-verilog pip3 install cocotb # 下载 sv2v 最新版,解压后放进 PATH # 准备构建目录 mkdir build然后只关心两条命令:
make test_matadd # 矩阵加法:8线程各算一个元素 make test_matmul # 矩阵乘法:2x2矩阵,4线程并行make目标会自动编译整个RTL、转换成Verilog、生成测试平台并运行仿真。跑完后去test/logs看日志:开头是初始数据内存,中间是逐周期的执行轨迹,结尾是最终数据内存。
矩阵乘法测试的输入是A = [[1,2],[3,4]]、B = [[1,2],[3,4]],4个线程一人认领C矩阵的一个元素(线程编号算出行列号,内层循环做点积)。跑对的标准很简单:日志末尾从地址8开始的4个字节应当是C = [[7, 11], [15, 19]]。日志片段长这样,一行就是一个周期内所有线程的指令、PC和寄存器状态:
⚡ 从玩具到真实:tiny-gpu的扩展边界
如果fork一份仓库想动手加功能,有三个方向值得挑。
分支发散:当前硬件假设一个块内所有线程每条指令后PC一致,所以矩阵乘法内核特意写成"所有分支都汇合"的样子。想打破它,得从 controller.sv 和 scheduler.sv 入手——让调度器管理多个批次(warp),各自持有PC,等它们重新汇合再合并执行。这是真实GPU里最经典的难题之一。
共享内存:在核心内部给一个块加一片SRAM,让块内线程交换中间结果,不用反复走全局内存。入口是core.sv,再配合 decoder.sv 新增一对片上访问指令。
内存合并:一批线程经常访问连续地址,却各自排队请求。在内存控制器这一层把排队中的相邻请求打包成一次传输,能显著提升带宽利用率,可以看 lsu.sv 和gpu.sv顶部的控制器接口。
三个方向难度递增:改ISA加指令最轻(decoder.sv一张case表的事),动调度策略最重。欢迎挑一个试试。
延伸阅读与资源
- README.md:架构、ISA和执行模型的完整说明
- src/controller.sv:核心控制状态机,六步流水线的实现处
- src/scheduler.sv:块级线程调度逻辑
- test/test_matmul.py:完整的仿真驱动与结果校验
【免费下载链接】tiny-gpuA minimal GPU design in Verilog to learn how GPUs work from the ground up项目地址: https://gitcode.com/GitHub_Trending/ti/tiny-gpu
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考