一个用Verilog实现的最小化GPU,旨在从底层学习GPU工作原理。
由<15个完全文档化的Verilog文件构建,包含完整的架构和指令集文档、可用的矩阵加法/乘法内核,以及对内核仿真和执行跟踪的全面支持。
如果你想学习CPU是如何从架构到控制信号全方位工作的,网上有很多资源可以帮助你。
但GPU并非如此。
由于GPU市场竞争激烈,所有现代架构的低层技术细节仍然是专有的。
虽然有很多资源可以学习GPU编程,但几乎没有资源可以学习GPU在硬件层面是如何工作的。
最好的选择是查看像Miaow和VeriGPU这样的开源GPU实现,并尝试弄清楚它们是如何运作的。这是具有挑战性的,因为这些项目旨在功能完整且可用,所以它们相当复杂。
这就是我构建tiny-gpu的原因!
[!重要]
tiny-gpu是一个最小化的GPU实现,旨在从底层学习GPU的工作原理。
特别是,随着通用GPU(GPGPU)和像Google的TPU这样的机器学习加速器的趋势,tiny-gpu专注于突出所有这些架构的一般原则,而不是图形特定硬件的细节。
考虑到这个动机,我们可以通过去除构建生产级显卡所涉及的大部分复杂性来简化GPU,并专注于对所有这些现代硬件加速器至关重要的核心元素。
这个项目主要关注探索:
在理解了本项目中阐述的基础知识之后,你可以查看高级功能部分,以了解生产级GPU中一些最重要的优化(实现起来更具挑战性),这些优化可以提高性能。
tiny-gpu被设计为一次执行一个内核。
为了启动一个内核,我们需要执行以下操作:
GPU本身由以下单元组成:
设备控制寄存器通常存储指定内核应如何在GPU上执行的元数据。
在这种情况下,设备控制寄存器只存储thread_count - 活动内核要启动的总线程数。
一旦内核被启动,调度器就是实际管理线程分配到不同计算核心的单元。
调度器将线程组织成可以在单个核心上并行执行的组,称为块,并将这些块发送到可用的核心进行处理。
一旦所有块都被处理完毕,调度器就会报告内核执行完成。
GPU被设计为与外部全局内存接口。在这里,为了简单起见,数据内存和程序内存被分开。
tiny-gpu数据内存有以下规格:
tiny-gpu程序内存有以下规格:
全局内存有固定的读/写带宽,但所有核心的传入请求可能远远超过外部内存实际能够处理的请求。
内存控制器跟踪所有从计算核心发出的内存请求,根据实际外部内存带宽限制请求,并将外部内存的响应传回适当的资源。
每个内存控制器根据全局内存的带宽有固定数量的通道。
多个核心经常从全局内存请求相同的数据。反复访问全局内存的成本很高,而且由于数据已经被获取过一次,将其存储在设备上的SRAM中以便在后续请求中更快地检索会更有效率。
这正是缓存的用途。从外部内存检索的数据存储在缓存中,并可以在后续请求中从那里检索,从而释放内存带宽以用于 新数据。
每个核心都有一定数量的计算资源,通常围绕它可以支持的特定数量的线程构建。为了最大化并行化,需要对这些资源进行最优管理以最大化资源利用率。
在这个简化的GPU中,每个核心一次处理一个块,对于块中的每个线程,核心都有一个专用的ALU、LSU、PC和寄存器文件。管理这些资源上的线程指令执行是GPU中最具挑战性的问题之一。
每个核心都有一个单一的调度器来管理线程的执行。
tiny-gpu调度器在接收新块之前执行单个块的指令直至完成,并且同步且顺序地执行所有线程的指令。
在更高级的调度器中,使用流水线等技术来流式执行多个后续指令,以在前面的指令完全完成之前最大化资源利用率。此外,warp调度可以用于并行执行块内的多批线程。
调度器必须解决的主要约束是与从全局内存加载和存储数据相关的延迟。虽然大多数指令可以同步执行,但这些加载-存储操作是异步的,这意味着指令执行的其余部分必须围绕这些长等待时间构建。
异步地从程序内存中获取当前程序计数器处的指令(在执行单个块后,大多数实际上应该从缓存中获取)。
将获取的指令解码为线程执行的控制信号。
每个线程都有自己专用的寄存器文件集。寄存器文件保存每个线程执行计算的数据,这使得同指令多数据(SIMD)模式成为可能。
重要的是,每个寄存器文件都包含一些只读寄存器,存储有关当前正在本地执行的块和线程的数据,使得可以根据本地线程ID执行具有不同数据的内核。
每个线程都有专用的算术逻辑单元来执行计算。处理 ADD、SUB、MUL、DIV 等算术指令。
还处理 CMP 比较指令,该指令实际上输出两个寄存器之差的结果是负数、零还是正数 - 并将结果存储在 PC 单元的 NZP 寄存器中。
每个线程都有专用的加载-存储单元来访问全局数据内存。
处理 LDR 和 STR 指令 - 并处理内存请求被内存控制器处理和传递的异步等待时间。
每个单元都有专用的程序计数器,用于确定每个线程要执行的下一条指令。
默认情况下,PC 在每条指令后递增 1。
通过 BRnzp 指令,NZP 寄存器会检查 NZP 寄存器(由之前的 CMP 指令设置)是否匹配某个条件 - 如果匹配,它将分支到程序内存的特定行。这就是实现循环和条件语句的方式。
由于线程是并行处理的,tiny-gpu 假设所有线程在每条指令后都"收敛"到相同的程序计数器 - 这是为简单起见而做出的一个简单假设。
在真实的 GPU 中,单个线程可以分支到不同的 PC,导致分支分歧,即原本一起处理的一组线程需要分裂为单独执行。

tiny-gpu 实现了一个简单的 11 条指令 ISA,旨在支持简单的内核,用于概念验证,如矩阵加法和矩阵乘法(在本页面下方有实现)。
为此,它支持以下指令:
BRnzp - 分支指令,如果 NZP 寄存器匹配指令中的 nzp 条件,则跳转到程序内存的另一行。CMP - 比较两个寄存器的值,并将结果存储在 NZP 寄存器中,以供后续的 BRnzp 指令使用。ADD、SUB、MUL、DIV - 基本算术运算,用于支持张量数学。LDR - 从全局内存加载数据。STR - 将数据存储到全局内存。CONST - 将常量值加载到寄存器中。RET - 表示当前线程已到达执行结束。每个寄存器由 4 位指定,这意味着总共有 16 个寄存器。前 13 个寄存器 R0 - R12 是支持读写的空闲寄存器。最后 3 个寄存器是特殊的只读寄存器,用于提供对 SIMD 至关重要的 %blockIdx、%blockDim 和 %threadIdx。
每个核心遵循以下控制流程,通过不同阶段执行每条指令:
FETCH - 从程序内存中获取当前程序计数器位置的下一条指令。DECODE - 将指令解码为控制信号。REQUEST - 如果需要,从全局内存请求数据(如果是 LDR 或 STR 指令)。WAIT - 如果适用,等待全局内存的数据。EXECUTE - 对数据执行任何计算。UPDATE - 更新寄存器文件和 NZP 寄存器。为了简单易懂,控制流程以这种方式布局。
实际上,可以压缩这些步骤中的几个以优化处理时间,GPU 还可以使用流水线技术来在核心资源上流式处理和协调多条指令的执行,而无需等待前面的指令完成。

每个核心内的每个线程都遵循上述执行路径,对其专用寄存器文件中的数据执行计算。
这类似于标准的 CPU 图,功能上也非常相似。主要区别在于 %blockIdx、%blockDim 和 %threadIdx 值位于每个线程的只读寄存器中,实现了 SIMD 功能。
我使用我的 ISA 编写了矩阵加法和矩阵乘法内核,作为概念验证,以演示 SIMD 编程和在我的 GPU 上的执行。此存储库中的测试文件能够完全模拟这些内核在 GPU 上的执行,生成数据内存状态和完整的执行跟踪。
这个矩阵加法内核通过在单独的线程中执行 8 个元素的逐元素加法来加两个 1 x 8 矩阵。
这个演示利用了 %blockIdx、%blockDim 和 %threadIdx 寄存器来展示这个 GPU 上的 SIMD 编程。它还使用了 LDR 和 STR 指令,这需要异步内存管理。
matadd.asm
.threads 8 .data 0 1 2 3 4 5 6 7 ; 矩阵 A (1 x 8) .data 0 1 2 3 4 5 6 7 ; 矩阵 B (1 x 8) MUL R0, %blockIdx, %blockDim ADD R0, R0, %threadIdx ; i = blockIdx * blockDim + threadIdx CONST R1, #0 ; baseA (矩阵 A 的基地址) CONST R2, #8 ; baseB (矩阵 B 的基地址) CONST R3, #16 ; baseC (矩阵 C 的基地址) ADD R4, R1, R0 ; addr(A[i]) = baseA + i LDR R4, R4 ; 从全局内存加载 A[i] ADD R5, R2, R0 ; addr(B[i]) = baseB + i LDR R5, R5 ; 从全局内存加载 B[i] ADD R6, R4, R5 ; C[i] = A[i] + B[i] ADD R7, R3, R0 ; addr(C[i]) = baseC + i STR R7, R6 ; 将 C[i] 存储到全局内存 RET ; 内核结束
矩阵乘法内核将两个 2x2 矩阵相乘。它执行相关行和列的点积的元素逐个计算,并使用 CMP 和 BRnzp 指令来演示线程内的分支(值得注意的是,所有分支都收敛,因此这个内核在当前的 tiny-gpu 实现上工作)。
matmul.asm
.threads 4 .data 1 2 3 4 ; 矩阵 A (2 x 2) .data 1 2 3 4 ; 矩阵 B (2 x 2) MUL R0, %blockIdx, %blockDim ADD R0, R0, %threadIdx ; i = blockIdx * blockDim + threadIdx CONST R1, #1 ; 递增值 CONST R2, #2 ; N (矩阵内部维度) CONST R3, #0 ; baseA (矩阵 A 的基地址) CONST R4, #4 ; baseB (矩阵 B 的基地址) CONST R5, #8 ; baseC (矩阵 C 的基地址) DIV R6, R0, R2 ; row = i // N MUL R7, R6, R2 SUB R7, R0, R7 ; col = i % N CONST R8, #0 ; acc = 0 CONST R9, #0 ; k = 0 LOOP: MUL R10, R6, R2 ADD R10, R10, R9 ADD R10, R10, R3 ; addr(A[i]) = row * N + k + baseA LDR R10, R10 ; 从全局内存加载 A[i] MUL R11, R9, R2 ADD R11, R11, R7 ADD R11, R11, R4 ; B[i]的地址 = k * N + col + baseB LDR R11, R11 ; 从全局内存加载B[i] MUL R12, R10, R11 ADD R8, R8, R12 ; acc = acc + A[i] * B[i] ADD R9, R9, R1 ; k自增 CMP R9, R2 BRn LOOP ; 当k < N时循环 ADD R9, R5, R0 ; C[i]的地址 = baseC + i STR R9, R8 ; 将C[i]存储到全局内存 RET ; 内核结束
tiny-gpu设置为模拟执行上述两个内核。在模拟之前,您需要安装iverilog和cocotb:
brew install icarus-verilog和pip3 install cocotb安装Verilog编译器mkdir build。安装好先决条件后,您可以使用make test_matadd和make test_matmul运行内核模拟。
执行模拟将在test/logs中输出一个日志文件,其中包含初始数据内存状态、内核的完整执行跟踪以及最终数据内存状态。
如果查看每个日志文件开头记录的初始数据内存状态,您应该能看到计算的两个起始矩阵,在文件末尾的最终数据内存中,您还应该能看到结果矩阵。
以下是执行跟踪的示例,显示了每个周期内每个核心中每个线程的执行情况,包括当前指令、PC、寄存器值、状态等。

对于任何想要运行模拟或使用此仓库的人,如果遇到任何问题,请随时在twitter上给我发私信 - 我希望您能成功运行它!
为了简单起见,现代GPU中实现的许多额外功能大大提高了性能和功能,但tiny-gpu省略了这些功能。在本节中,我们将讨论一些最关键的功能。
在现代GPU中,使用多个不同级别的缓存来最小化需要从全局内存访问的数据量。tiny-gpu仅在请求内存的各个计算单元和存储最近缓存数据的内存控制器之间实现了一层缓存。
实现多层缓存允许将频繁访问的数据缓存在更靠近使用位置的地方(某些 缓存位于各个计算核心内),最大限度地减少这些数据的加载时间。
使用不同的缓存算法来最大化缓存命中率 - 这是可以改进以优化内存访问的关键维度。
此外,GPU通常使用共享内存,使同一块内的线程可以访问单个内存空间,用于与其他线程共享结果。
GPU使用的另一个关键内存优化是内存合并。并行运行的多个线程通常需要访问内存中的连续地址(例如,一组线程访问矩阵中的相邻元素)- 但每个内存请求都是单独提交的。
内存合并用于分析排队的内存请求,并将相邻的请求合并为单个事务,最大限度地减少用于寻址的时间,并一起处理所有请求。
在tiny-gpu的控制流程中,核心在开始执行下一条指令之前,会等待一组线程执行完一条指令。
现代GPU使用流水线同时流式执行多条顺序指令,同时确保相互依赖的指令仍按顺序执行。
这有助于最大化核心内的资源利用率,因为资源不会在等待时(例如:在异步内存请求期间)闲置。
用于最大化核心资源利用率的另一种策略是线程束调度。这种方法涉及将块分解为可以一起执行的单个线程批次。
通过在一个线程束等待时执行另一个线程束的指令,多个线程束可以同时在单个核心上执行。这类似于流水线,但处理的是来自不同线程的指令。
tiny-gpu假设单个批次中的所有线程在每条指令之后都会在相同的PC上结束,这意味着线程可以在整个生命周期内并行执行。
实际上,各个线程可能会根据其数据而彼此分歧并分支到不同的行。由于PC不同,这些线程需要分成不同的执行线路,这需要管理分歧的线程并注意线程何时再次汇合。
现代GPU的另一个核心功能是能够设置障碍,使块中的线程组可以同步并等待同一块中的所有其他线程达到某个点后再继续执行。
这在线程需要相互交换共享数据的情况下很有用,因为它们可以确保数据已被完全处理。
我想在未来做出的更新以改进设计,欢迎其他人也贡献:
对于任何想要尝试或做出贡献的人,欢迎提交PR,添加您想要的任何改进😄


稳定高效的流量提升解决方案,助力品牌曝光
稳定高效的流量提升解决方案,助力品牌曝光


最新版Sora2模型免费使用,一键生成无水印视频
最新版Sora2模型免费使用,一键生成无水印视频


实时语音翻译/同声传译工具
Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。


选题、配图、成文,一站式创作,让内容运营更高效
讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。


AI辅助编程,代码自动修复
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。


最强AI数据分析助手
小浣熊家族Raccoon,您的AI智能助手,致力于通过先进的人工智能技术,为用户提供高效、便捷的智能服务。无论是日常咨询还是专业问题解答,小浣熊都能以快速、准确的响应满足您的需求,让您的生活更加智能便捷。


像人一样思考的AI智能体
imini 是一款超级AI智能体,能根据人类指令,自主思考、自主完成、并且交付结果的AI智能体。


AI数字人视频创作平台
Keevx 一款开箱即用的AI数字人视频创作平台,广泛适用于电商广告、企业培训与社媒宣传,让全球企业与个人创作者无需拍摄剪辑,就能快速生成多语言、高质量的专业视频。


一站式AI创作平台
提供 AI 驱动的图片、视频生成及数字人等功能,助力创意创作


AI办公助手,复杂任务高效处理
AI办公助手,复杂任务高效处理。办公效率低?扣子空间AI助手支持播客生成、PPT制作、网页开发及报告写作,覆盖科研、商业、舆情等领域的专家Agent 7x24小时响应,生活工作无缝切换,提升50%效率!
最新AI工具、AI资讯
独家AI资源、AI项目落地

微信扫一扫关注公众号