ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

tiny-gpu:12个Verilog文件、11条指令,一台读得完的GPU

tiny-gpu:12个Verilog文件、11条指令,一台读得完的GPU tiny-gpu12个Verilog文件、11条指令一台读得完的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一条指令能让4个线程同时算数吗GPU的架构细节普遍封闭开源实现动辄几万行。tiny-gpu把GPU压到12个Verilog文件、11条指令覆盖取指到访存的完整数据通路你能画出计算核心的六阶段状态机能译码任意一条16位指令能对着仿真trace逐周期核对寄存器。拆解设计取舍这颗GPU砍了什么、留了什么完整图形GPU要背渲染管线光栅化、纹理采样、帧缓冲回写外加多级缓存、warp调度、分支发散管理。这套硬件和算矩阵几乎没关系。tiny-gpu把这刀直接砍下去block/thread两级并行、SIMD执行单元、内存控制器和指令集是GPGPU的必经之路全部保留与图形有关的东西一概不要连指令流水线、warp都不做并且假设所有线程的分支必然汇聚。取舍逻辑很直接砍掉图形渲染要读的东西只剩十几份源码再不做流水线控制流每个阶段就是状态机里一个状态六段流程对应六个case分支读起来没有绕路的空间。代价是周期数不好看——但对看懂GPU怎么工作这个目标来说关键结构完整暴露比性能更重要。仓库文件结构一目了然每个文件一个职责src/gpu.sv顶层模块连接DCR、调度器、计算核心与两个内存控制器src/core.sv计算核心1套取指/译码单元加每线程专用的ALU/LSU/寄存器堆/PCsrc/scheduler.sv核心内部的六阶段控制状态机src/decoder.sv16位指令译码为控制信号src/controller.sv内存控制器限流请求、复用访存通道src/dcr.sv设备控制寄存器存放内核的总线程数src/dispatch.sv把线程切成block分发给空闲核心src/fetcher.sv、src/alu.sv、src/lsu.sv、src/pc.sv、src/registers.sv每线程功能单元gds/0/gpu.gds、gds/1/gpu.gdsEDA流产出的GDSII版图文件test/test_matadd.py、test/test_matmul.py矩阵加/乘内核的cocotb仿真脚本Makefile编译与测试入口整体结构是分层清晰的顶层gpu中间是若干计算核心默认NUM_CORES 2参数可配下面是程序/数据两个内存控制器最外层全局内存拆成程序内存每行16位一条指令和数据内存每行8位共256行这张图里最值得注意的是Cache夹在计算核心和内存控制器之间——它是设计中唯一标注WIP的部件也是项目自己承认的没做完。拆开核心六阶段状态机与每线程专用硬件看计算核心。它的输入是调度器给的start脉冲、block_id和该block的线程数输出只有done信号和寄存器/内存写回。中间的一切由scheduler驱动IDLE、FETCH、DECODE、REQUEST、WAIT、EXECUTE、UPDATE、DONE七个状态其中DECODE、REQUEST、EXECUTE、UPDATE各一个周期穿过唯一的可变延迟点在WAIT——这里有个绕不过去的硬约束LDR/STR要访问外部内存响应时间不受核心控制。tiny-gpu的处理方式不是隐藏延迟而是显式等下面这段是整个状态机里唯一的轮询逐个线程检查LSU状态全部完成才放行WAIT: begin // Wait for all LSUs to finish their request before continuing reg any_lsu_waiting 1b0; for (int i 0; i THREADS_PER_BLOCK; i) begin // Make sure no lsu_state REQUESTING or WAITING if (lsu_state[i] 2b01 || lsu_state[i] 2b10) begin any_lsu_waiting 1b1; break; end end // If no LSU is waiting for a response, move onto the next stage if (!any_lsu_waiting) begin core_state EXECUTE; end end如果这条指令是纯ALU运算4个LSU全停在IDLEWAIT一个周期穿过如果有任何一个线程发出访存请求整个block就停在这里直到所有LSU的ready都回来。这解释了块内同步执行的代价也顺带保证了block内所有线程永远处于同一条PC上。反过来推想消除这个等待就得让不同线程在不同指令上前进——warp调度和流水线正是真实GPU做的事。换个角度看状态机管的是指令怎么走而同一条指令算多份数据靠的是寄存器。src/core.sv用一个generate循环为每个线程生成独立的ALU、LSU、寄存器堆和PC译码出的控制信号广播给全部4个线程各线程只换寄存器里的数据。关键是每个寄存器堆有16个寄存器最后3个只读下面这4行是整个GPU里区分线程身份的唯一数据来源——同一条指令读到不同的%threadIdx地址自然不同// Initialize read-only registers registers[13] 8b0; // %blockIdx registers[14] THREADS_PER_BLOCK; // %blockDim registers[15] THREAD_ID; // %threadIdxSIMD因此不是指令级的技巧而是寄存器级的事实内核算出i blockIdx * blockDim threadIdx后4个线程的i分别是0/1/2/3同一条LDR就打到4个不同地址。指令集本身很小11条、16位定长高4位操作码中间4位目的寄存器低8位再拆成源寄存器或8位立即数看LDR的编码0111 dddd ssss xxxx低4位干脆留空——加载只需要存进哪个寄存器和从哪个地址取地址就复用算术指令里Rs所在的字段。字段布局统一译码器只解析4位操作码加三个4位字段不需要任何额外的格式判断。这里有个坑。UPDATE阶段只有一个8位的current_pc驱动所有线程取指UPDATE: begin if (decoded_ret) begin // If we reach a RET instruction, this block is done executing done 1; core_state DONE; end else begin // TODO: Branch divergence. For now assume all next_pc converge current_pc next_pc[THREADS_PER_BLOCK-1]; // Update is synchronous so we move on after one cycle core_state FETCH; end end如果4个线程分支都落到同一个PC这行赋值完全正确如果分叉了硬件只取最后一个线程的next_pc内核会安静地算错。所以样例内核里的BRnzp都写成所有线程条件同时成立的形式比如循环次数对谁都等于N。代码里的TODO注释已经把话说透这是为简单做的naive假设也是全项目最明确的扩展入口。跑通矩阵乘法一条命令验证GPU环境需要iverilog开源Verilog仿真器、cocotbPython仿真框架、sv2v把SystemVerilog翻译成iverilog能编的Verilog-2012再mkdir build。之后一条命令make test_matmul跑完得到的不是一行pass/fail而是test/logs里一份完整日志开头是初始数据内存0~3是矩阵A4~7是矩阵B都是[1,2,3,4]行优先中间是逐周期trace结尾是最终内存状态。trace每个周期记录每个核心、每个线程的PC、当前指令、核心状态和寄存器值。可以亲眼看到关键节点执行到LDR时核心状态停在WAIT若干周期CMP/BRnzp走完k0、1两轮循环后STR把结果写进地址8开始的4行。数字可以手算验证A[[1,2],[3,4]]、B[[1,2],[3,4]]则C的四个元素依次是7、10、15、22C[0][0]1×12×3C[1][1]3×24×4。测试脚本直接对结果地址逐个assert日志末尾的内存dump里对应4行应当就是7、10、15、22。make test_matadd则是8个线程各加一个元素8线程切成两个block分给两个核心。这份轨迹里最值得盯的是寄存器行%blockDim4每个线程的%threadIdx差1——SIMD的数据通路直接摊在眼前。划出边界架构的上限在哪又能从哪里扩展先说它做不到什么只能跑所有线程分支都汇聚的内核指令严格串行访存等待期间ALU全部空转缓存还是WIP数据内存只有256行×8位一次只能跑一个内核没有抢占。这些不是缺陷是可读性的定价。想补上的话每一处都有明确的入手点分支发散改src/scheduler.sv的UPDATE状态为每个线程维护PC数组next_pc信号组本来就每个线程一份等所有线程next_pc一致才统一更新current_pc分叉期间按线程屏蔽ALU/LSU的enable指令流水线同文件的状态机里让下一条指令的FETCH/DECODE在WAIT期间启动先把指令预取进寄存器六段串行就变成重叠流水内存合并src/controller.sv的IDLE状态现在是一个通道捡一个请求可以在这里加相邻地址合并逻辑把多个LSU的邻接请求合成一笔传输新指令src/decoder.sv的操作码表是一个10项的localparam加一个case加指令就是补一个操作码、勾一组decoded_*控制信号ALU/LSU侧大多已就位。tiny-gpu用最小的状态集说清了两件事SIMD的本质是同一份控制信号乘以不同的寄存器数据而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),仅供参考
返回列表