从零用 Verilog 搭建一个能跑的 GPU:tiny-gpu 完整指南(1337 行代码跑通矩阵乘法)
2026/9/13 10:31:45 网站建设 项目流程

从零用 Verilog 搭建一个能跑的 GPU:tiny-gpu 完整指南(1337 行代码跑通矩阵乘法)

【免费下载链接】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 内部到底怎么干活?商业架构文档基本都封锁了,开源 GPU 项目又动辄上万行。tiny-gpu 用 12 个 Verilog 文件、共 1337 行代码,造出一台能仿真矩阵乘法的极简 GPU。下面带你装好环境、跑通内核,再逐模块拆它的原理。

快速上手:3 步装好仿真环境

先让 GPU 转起来,原理稍后再说。仿真链路是:iverilog(Icarus Verilog 编译器)+ cocotb(Python 写测试激励的框架)+ sv2v(把 SystemVerilog 转成 Verilog 的工具)。

# 1. 拿到代码 git clone https://gitcode.com/GitHub_Trending/ti/tiny-gpu cd tiny-gpu # 2. 安装工具链 brew install icarus-verilog # Linux 可用包管理器装 iverilog pip3 install cocotb # 3. 下载 sv2v 对应平台的预编译二进制,解压后放进 PATH # 4. 准备构建目录 mkdir build

装完工具后,确认iverilog --versioncocotb-config --prefixsv2v --version三条命令都有输出。

最小命令:跑通一个矩阵乘法内核

项目用 Makefile 把编译和仿真串好了,矩阵乘法和矩阵加法各一个测试用例:

make test_matmul # 4 个线程算 2x2 矩阵乘法 make test_matadd # 8 个线程做向量加法

命令跑完后会生成一个日志文件放在test/logs目录下,里面按周期记录了每个线程的 PC、寄存器、ALU 输出和内存操作,文末还会打印最终的数据内存状态。

内核启动与内存:GPU 是怎么领到任务的?

这部分回答一个基础问题:内核代码和数据从哪来、GPU 靠什么知道该干多少活。

启动一个内核需要 4 步:把内核机器码写进程序内存、把运算数据写进数据内存、在设备控制寄存器(DCR,见 src/dcr.sv)里写入要启动的线程总数、把 start 信号拉高。

内存规格刻意做小,方便你一眼看全:

  • 数据内存:8 位地址(256 行),每行 8 位,能存 0~255 的数
  • 程序内存:8 位地址,每行 16 位,正好装一条指令
  • 两套内存各配一个内存控制器,负责给请求排队、按带宽限流、把响应送回正确的线程

DCR 里只存一个数:thread_count。真正的分发是调度器(Dispatcher,见 src/dispatch.sv)干的——它把线程按每块 4 个分组,一块一块派给计算核心,全部分发完才上报内核执行完毕。

计算核心内部:4 个线程如何共享一颗核心

这一节拆解"一块 4 线程的活,硬件怎么并行做完"。

核心模块 src/core.sv 用THREADS_PER_BLOCK参数控制线程数,默认 4。每个线程都有一整套独立硬件:

  • ALU(src/alu.sv):算 ADD/SUB/MUL/DIV,还负责 CMP 比较并把"负/零/正"结果写进 NZP 寄存器
  • LSU(src/lsu.sv):负责 LDR/STR 访存,并扛住异步等待
  • PC(src/pc.sv):程序计数器,每条指令默认 +1,遇 BRnzp 按 NZP 状态跳转——循环和条件就这样实现
  • 寄存器堆(src/registers.sv):16 个寄存器,R0~R12 可读写,R13~R15 是只读的%blockIdx%blockDim%threadIdx

那 3 个只读寄存器就是 SIMD(单指令多数据)的关键:同一条指令,每个线程拿自己的线程号去算地址,各干各的数据。

核心里的调度器(src/scheduler.sv)策略很朴素:一个块从头执行到 RET 才接下一个块,块内所有线程锁步同步。它主要要对付的敌人是访存延迟——算术指令一个周期就出结果,LDR/STR 却要在 WAIT 状态空转若干周期。

指令集与六阶段流水线:11 条指令够写内核吗

这里回答"为什么 11 条指令就能表达循环和分支"。

指令固定 16 位,前 4 位操作码,后面分寄存器/立即数字段:

11 条指令分四类:算术(ADD/SUB/MUL/DIV)、访存(LDR/STR)、控制(CMP/BRnzp)、杂项(CONST 装立即数、RET 结束线程、NOP 空转)。矩阵乘法内核里,CMP R9, R2BRn LOOP就是那个 k < N 的循环。

每条指令在核心状态机里走 6 个阶段(状态机实现在 src/controller.sv 和 src/fetcher.sv):

  1. FETCH 取指 → 2. DECODE 译码 → 3. REQUEST 发访存请求 → 4. WAIT 等内存响应 → 5. EXECUTE 执行 → 6. UPDATE 写回寄存器

非访存指令会很快跳过 3、4 两阶段;这个六段划分牺牲了速度,换来的是每一拍你都能在波形里看懂在干什么。

实战验证:核对矩阵乘法的结果

用 2x2 矩阵复现一遍,输入两个矩阵都是[[1,2],[3,4]],按行优先平铺在数据内存地址 0~7。

make test_matmul cat test/logs/*.log # 打开生成的日志

预期结果与手工计算对照:

A x B = [[1,2],[3,4]] x [[1,2],[3,4]] C[0][0] = 1*1 + 2*3 = 7 C[0][1] = 1*2 + 2*4 = 10 C[1][0] = 3*1 + 4*3 = 15 C[1][1] = 3*2 + 4*4 = 22

对照要点:

  • 日志开头的 data memory 状态应看到1 2 3 4 / 1 2 3 4
  • 日志末尾 4 个线程(每线程负责 C 的一个元素)把7 10 15 22写进了地址 8~11
  • 测试脚本 test/test_matmul.py 最后会断言这 4 个值,全对才算过

每个周期的执行快照长这样,能直接看到 4 个线程各自的 PC、寄存器值和 LSU 状态:

延伸路径:跑通之后改哪里

README 的 Next Steps 列表就是路线图,对应源码入口:

  1. 指令缓存与多级缓存:现在取指直接走程序内存,加一层缓存能省带宽。入口在 src/fetcher.sv 和 src/gpu.sv(顶层已留了 cache 位置,标注 WIP)。
  2. 流水线与 warp 调度:调度器目前严格"一条指令跑完再下一条",改成指令重叠或分 warp 轮转,能显著提升核心利用率。入口 src/scheduler.sv、src/controller.sv。
  3. 分支发散与访存合并:现在假设所有线程每条指令后 PC 必然收敛;放开这个假设,或把相邻地址请求合并成一笔事务,都是真实 GPU 的核心难题。入口 src/pc.sv 和 src/lsu.sv。

想加新指令,从 src/decoder.sv 的操作码译码和 test/helpers/format.py 的反汇编格式同步改起即可。

关键资源

  • 完整架构与 ISA 文档:README.md
  • 硬件源码(12 个 Verilog 模块):src/
  • 两个内核的仿真用例与辅助脚本:test/
  • 架构图、ISA 表、执行轨迹图:docs/images/

打开 test/test_matmul.py 和 src/controller.sv 对照着读一遍,你手里就有了一台可以逐拍解剖的 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

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询