一个用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,添加您想要的任何改进😄
一键生成PPT和Word,让学习生活更轻松
讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。
深度推理能力全新升级,全面对标OpenAI o1
科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。
一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型
Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持 语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。
字节跳动发布的AI编程神器IDE
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。
AI助力,做PPT更简单!
咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制 作出高品质 PPT,满足不同场景演示需求。
选题、配图、成文,一站式创作,让内容运营更高效
讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。
专业的AI公文写作平台,公文写作神器
AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。
OpenAI Agents SDK,助力开发者便捷使用 OpenAI 相关功能。
openai-agents-python 是 OpenAI 推出的一款强大 Python SDK,它为开发者提供了与 OpenAI 模型交互的高效工具,支持工具调用、结果处理、追踪等功能,涵盖多种应用场景,如研究助手、财务研究等,能显著提升开发效率,让开发者更轻松地利用 OpenAI 的技术优势。
高分辨率纹理 3D 资产生成
Hunyuan3D-2 是腾讯开发的用于 3D 资产生成的强大工具,支持从文本描述、单张图片或多视角图片生成 3D 模型,具备快速形状生成能力,可生成带纹理的高质量 3D 模型,适用于多个领域,为 3D 创作提供了高效解决方案。
一个具备存储、管理和客户端操作等多种功能的分布式文件系统相关项目。
3FS 是一个功能强大的分布式文件系统项目,涵盖了存储引擎、元数据管理、客户端工具等多个模块。它支持多种文件操作,如创建文件和目录、设置布局等,同时具备高效的事件循环、节点选择和协程池管理等特性。适用于需要大规模数据存储和管理的场景,能够提高系统的性能和可靠性,是分布式存储领域的优质解决方案。
最新AI工具、AI资讯
独家AI资源、AI项目落地
微信扫一扫关注公众号