<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>算子开发 on Lorn</title><link>https://july-h5kf3.github.io/categories/%E7%AE%97%E5%AD%90%E5%BC%80%E5%8F%91/</link><description>Recent content in 算子开发 on Lorn</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Thu, 27 Aug 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://july-h5kf3.github.io/categories/%E7%AE%97%E5%AD%90%E5%BC%80%E5%8F%91/index.xml" rel="self" type="application/rss+xml"/><item><title>现代GPU编程指南（开篇）：Hopper 新特性总览</title><link>https://july-h5kf3.github.io/p/hopper-cuda-00-intro/</link><pubDate>Thu, 27 Aug 2026 09:00:00 +0800</pubDate><guid>https://july-h5kf3.github.io/p/hopper-cuda-00-intro/</guid><description>&lt;p&gt;这是「现代GPU编程指南」系列的开篇。之所以想单独开一个系列，是因为 Hopper（H100 / H800，计算能力 &lt;code&gt;sm_90&lt;/code&gt;）相比 Ampere 并不只是&amp;quot;更大更快&amp;quot;，而是引入了一批&lt;strong&gt;需要改变编程范式&lt;/strong&gt;才能吃满的新硬件特性。本篇先做一个总览，后续每篇再展开一个专题，并尽量用 GEMM / FlashAttention 这类真实算子把它们串起来。&lt;/p&gt;&#10;&lt;h2 id="hopper-带来了什么"&gt;Hopper 带来了什么&#10;&lt;/h2&gt;&lt;h3 id="1-thread-block-cluster-与分布式共享内存dsmem"&gt;1. Thread Block Cluster 与分布式共享内存（DSMEM）&#10;&lt;/h3&gt;&lt;p&gt;Hopper 在 grid → block 之间新增了一层 &lt;strong&gt;Thread Block Cluster&lt;/strong&gt;。同一个 cluster 内的若干 block 会被调度到同一个 GPC 内的相邻 SM 上，从而可以：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;通过 &lt;strong&gt;分布式共享内存（Distributed Shared Memory）&lt;/strong&gt; 直接读写同 cluster 内其它 block 的 shared memory；&lt;/li&gt;&#10;&lt;li&gt;使用 &lt;strong&gt;cluster 级别的同步原语&lt;/strong&gt;（cluster barrier）。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这让&amp;quot;比一个 block 更大、又比走 global memory 更快&amp;quot;的协作成为可能，对 tiling 策略的设计影响很大。&lt;/p&gt;&#10;&lt;h3 id="2-tmatensor-memory-accelerator"&gt;2. TMA：Tensor Memory Accelerator&#10;&lt;/h3&gt;&lt;p&gt;TMA 是一个&lt;strong&gt;专用的异步批量拷贝引擎&lt;/strong&gt;，负责 global memory 与 shared memory 之间的多维 tile 搬运。它的价值在于：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;用一个 &lt;strong&gt;copy descriptor&lt;/strong&gt; 描述多维张量的搬运，一条指令搬一整块，不再需要大量线程手写地址计算；&lt;/li&gt;&#10;&lt;li&gt;天然异步，把访存和计算重叠起来，同时&lt;strong&gt;省下寄存器和线程&lt;/strong&gt;去做真正的计算。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="3-wgmmawarpgroup-级异步-tensor-core"&gt;3. WGMMA：warpgroup 级异步 Tensor Core&#10;&lt;/h3&gt;&lt;p&gt;Hopper 的 Tensor Core 通过 &lt;strong&gt;WGMMA（warpgroup MMA）&lt;/strong&gt; 以 &lt;strong&gt;warpgroup（128 线程）&lt;/strong&gt; 为粒度、以&lt;strong&gt;异步&lt;/strong&gt;方式发射矩阵乘累加，并原生支持 &lt;strong&gt;FP8（e4m3 / e5m2）&lt;/strong&gt;。相比 Ampere 的 &lt;code&gt;mma&lt;/code&gt;，它的吞吐更高，也更依赖异步流水线来喂数据。&lt;/p&gt;&#10;&lt;h3 id="4-异步事务屏障mbarrier与流水线"&gt;4. 异步事务屏障（mbarrier）与流水线&#10;&lt;/h3&gt;&lt;p&gt;要把 TMA 的异步搬运和 WGMMA 的异步计算真正重叠起来，就需要 &lt;strong&gt;异步事务屏障（mbarrier / async transaction barrier）&lt;/strong&gt; 来构建生产者–消费者流水线：一边 TMA 往 shared memory 里灌数据、累加 transaction count，另一边 WGMMA 等待到齐后开算。&lt;/p&gt;&#10;&lt;h3 id="5-其它值得关注的点"&gt;5. 其它值得关注的点&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;FP8 与 Transformer Engine&lt;/strong&gt;：配合 WGMMA 的 FP8，训练/推理里低精度 GEMM 成为一等公民；&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;setmaxnreg&lt;/code&gt;&lt;/strong&gt;：warpgroup 之间可以&lt;strong&gt;动态重新分配寄存器&lt;/strong&gt;，让&amp;quot;搬运 warpgroup&amp;quot;少占、&amp;ldquo;计算 warpgroup&amp;quot;多占；&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;更大的 shared memory&lt;/strong&gt;（每 SM 可配置到约 228KB）与 &lt;strong&gt;DPX&lt;/strong&gt; 指令（加速动态规划类算子）。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="本系列规划"&gt;本系列规划&#10;&lt;/h2&gt;&lt;p&gt;后续计划按下面的顺序展开（会随写随调）：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Thread Block Cluster 与分布式共享内存&lt;/li&gt;&#10;&lt;li&gt;TMA 异步数据搬运&lt;/li&gt;&#10;&lt;li&gt;WGMMA 与异步 Tensor Core&lt;/li&gt;&#10;&lt;li&gt;用 mbarrier 构建生产者–消费者流水线&lt;/li&gt;&#10;&lt;li&gt;FP8 与 Transformer Engine 实战&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;最后会用一个&lt;strong&gt;从 naive 到 Hopper-optimized 的 GEMM / FlashAttention&lt;/strong&gt; 案例，把上面这些特性组合起来，看看它们各自贡献了多少性能。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;本篇为系列总览，具体的代码与实验会放在后续各篇。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>GPU 体系结构 —— H100 的硬件架构</title><link>https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/</link><pubDate>Thu, 27 Aug 2026 10:00:00 +0800</pubDate><guid>https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/</guid><description>&#10; &lt;blockquote&gt;&#10; &lt;p&gt;💡 &lt;strong&gt;建议搭配阅读&lt;/strong&gt;：&lt;a class="link" href="https://cudacourseh100.github.io/h100visu/" target="_blank" rel="noopener"&#10; &gt;H100 硬件架构可视化演示&lt;/a&gt; —— 一个交互式网页，完整演示了 H100 的硬件架构。阅读本文前可以先花几分钟浏览，快速建立对 H100 硬件的整体认识。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;h2 id="gpu-的基本组成原理"&gt;GPU 的基本组成原理&#10;&lt;/h2&gt;&lt;p&gt;一个 GPU 芯片可以被划分为以下几个主要组成部分：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;NVIDIA GigaThread Engine（巨线程引擎）&lt;/li&gt;&#10;&lt;li&gt;8 个 GPC（Graphics Processing Cluster，图形处理集群）&lt;/li&gt;&#10;&lt;li&gt;HBM3 Stacks（HBM3 显存堆栈）&lt;/li&gt;&#10;&lt;li&gt;HBM3 Memory Controllers（HBM3 内存控制器）&lt;/li&gt;&#10;&lt;li&gt;PCI Express 5.0 Host Interface（PCIe 5.0 主机接口）&lt;/li&gt;&#10;&lt;li&gt;NVLink Switches、Ports、Hub（NVLink 交换机、端口和 Hub）&lt;/li&gt;&#10;&lt;li&gt;L2 Cache Slices（L2 缓存分片）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="nvidia-gigathread-engine"&gt;NVIDIA GigaThread Engine&#10;&lt;/h2&gt;&lt;p&gt;GigaThread Engine 译作巨线程引擎，这是 GPU 上负责调度 Kernel 以及 Thread Block 的硬件。&lt;/p&gt;&#10;&lt;p&gt;当一个 Kernel 启动后，GigaThread Engine 会负责将 Kernel 中的 Thread Block（CTA）分配给各个 SM，并跟踪每个 CTA 当前的状态，如哪些 CTA 还没开始执行、哪些 CTA 正在执行、哪些 CTA 执行完毕。当某个 SM 有足够的资源、可以容纳另一个 CTA 时，GigaThread Engine 以及相关的前端调度逻辑就会把下一个 CTA 分给这个 SM。&lt;/p&gt;&#10;&lt;p&gt;同时它还会处理和强制执行一些资源约束，如 Occupancy 限制、寄存器资源控制等。&lt;/p&gt;&#10;&lt;p&gt;在 Hopper 架构的显卡上，GigaThread Engine 还能够理解和处理 Cluster 相关的资源调度。&lt;/p&gt;&#10;&lt;h2 id="hbm3"&gt;HBM3&#10;&lt;/h2&gt;&lt;p&gt;HBM3 是一种 off-chip 的设备内存，也就是我们常说的 Global Memory 的物理介质，在 H100 上其容量为 80GB，理论带宽为 3.35TB/s。&lt;/p&gt;&#10;&lt;p&gt;其组织结构大致如下：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;HBM3 Stack：在 H100 上总共有 6 个 HBM3 Stack，但是出于良率考虑，只使用了其中的 5 个。&lt;/li&gt;&#10;&lt;li&gt;总线宽度共 5120 bit（每个 HBM3 Stack 的位宽为 1024 bit）。&lt;/li&gt;&#10;&lt;li&gt;通过 10 个独立的 512-bit memory controller 连接 HBM（每个 HBM3 Stack 两个）。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;通常来说，我们一次从 HBM 中请求数据的流程如下：&lt;/p&gt;&#10;&lt;p&gt;首先从 SM 进入 L1D 缓存或合并器，若没有相关数据则进入 L2 缓存，若还没有则通过 Memory Partition / Crossbar 来确定这次请求应该由哪一个 memory controller 处理，最后由 Memory Controller 在 HBM3 Stack 中取出对应的 cache line 返回数据并写入 L1D Cache。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="一次从 HBM 请求数据的流程：SM → L1D（miss）→ L2（miss）→ Crossbar 路由到对应的 Memory Controller → 从 HBM3 Stack 取出 cache line，原路返回并沿途写入 L2 与 L1D" class="gallery-image" data-flex-basis="576px" data-flex-grow="240" height="400" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/hbm3_request_flow.gif" srcset="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/hbm3_request_flow_hu_f78da972317848e9.gif 800w, https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/hbm3_request_flow.gif 960w" width="960"&gt;&lt;/p&gt;&#10;&lt;p&gt;其中需要强调的是，Memory Controller 负责把 GPU 的内存访问请求转换成符合 HBM/DRAM 协议和时序的实际读写操作，并进行调度以尽可能提高内存带宽。&lt;/p&gt;&#10;&lt;h2 id="l2-cache"&gt;L2 Cache&#10;&lt;/h2&gt;&lt;p&gt;在 H100 上有 50MB 的 L2 Cache，它们被划分为了两个 25MB 的分区。由于这种分区的特性，同一个 GPC 中的 SM 与其直接相连的 L2 Cache 分区之间会具有更近的访问路径。因此，当我们的内存访问命中这些相邻的 L2 Cache 分区中的 Cache Line 时，可以获得更低的有效访问延迟和更高的带宽。&lt;/p&gt;&#10;&lt;p&gt;L2 Cache 采用 128 字节的 Cache Line，以及 32 字节的 Sector。也就是说一次内存访问最多可以访问一个 128 字节 Cache Line 的 1-4 个 sector。如果内存访问的 Coalescing（合并）做得不好，就可能导致每个请求涉及的 sector 数量急剧增加，从而降低内存的访问效率。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Coalesced access：一个 warp 的地址被合并到尽量少的 sector 中" class="gallery-image" data-flex-basis="823px" data-flex-grow="343" height="373" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.1.png" srcset="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.1_hu_18c9682afd134035.png 800w, https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.1.png 1280w" width="1280"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="反面例子" class="gallery-image" data-flex-basis="843px" data-flex-grow="351" height="364" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.2.png" srcset="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.2_hu_ffcbc6e6062e3d17.png 800w, https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.2.png 1280w" width="1280"&gt;&lt;/p&gt;&#10;&lt;h2 id="gpc"&gt;GPC&#10;&lt;/h2&gt;&lt;p&gt;GPC 是一组 SM（18 个）。每个 GPC 与物理上邻近的 L2 Cache 分区之间访问延迟更低（即前文提到的亲和性），但任何 SM 都可以访问全部的 L2 Cache。当需要把数据取到 shared memory 时，数据路径是 HBM → L2 → L1。GPC 还能让各 SM 之间共享 DSMEM（Distributed Shared Memory，详见文末的 Thread Block Cluster）。&lt;/p&gt;&#10;&lt;p&gt;GPC 中有 TPC，每个 TPC 包含两个 SM。TPC 主要是硬件物理实现层面的分组概念，对 CUDA 编程模型基本不可见。&lt;/p&gt;&#10;&lt;h2 id="sm"&gt;SM&#10;&lt;/h2&gt;&lt;p&gt;如下图所示是一个 SM，它是 GPU 的基本执行单元，负责执行 CUDA Kernel 的 thread block。其基本由 4 个相同的子部分组成，并且这四个子部分我们称作 SMSP 或者 quadrant。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="H100 SM 架构：4 个 SMSP（各含 Warp Scheduler、Register File、INT32/FP32/FP64 单元与第四代 Tensor Core），下方为 TMA 和 256KB 的 L1/Shared Memory" class="gallery-image" data-flex-basis="208px" data-flex-grow="87" height="1280" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.3.png" srcset="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.3_hu_7e8859b88d9c8e61.png 800w, https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/fig1.3.png 1114w" width="1114"&gt;&lt;/p&gt;&#10;&lt;p&gt;整体上，每个 SM 由 4 个 SMSP 组成，每个 SM 都包含 L1 指令缓存以及大小为 256KB 的数据缓存/shared memory、TMA，与共享的 texture unit。&lt;/p&gt;&#10;&lt;p&gt;而每个子部分包含的内容如下：&lt;/p&gt;&#10;&lt;h3 id="fp32-cuda-core"&gt;FP32 CUDA Core&#10;&lt;/h3&gt;&lt;p&gt;它们是通用图形计算或 compute shading 的主力单元。H100 的每个 SM 上有 128 个 FP32 CUDA Core。因此总共有 16896 个活跃的 CUDA Core（132 个 SM）。&lt;/p&gt;&#10;&lt;h3 id="sfu"&gt;SFU&#10;&lt;/h3&gt;&lt;p&gt;SFU 通常负责复杂数学函数的计算，如正余弦、对数、指数、平方根、倒数这类。每个 SM 有 16 个 SFU，也就是每个 SMSP 有 4 个。每个 SFU 每 cycle 可以完成 1 个线程的运算，所以每个 SM 的 SFU 吞吐是 16 个线程/cycle——也就是说一条 warp 级（32 线程）的 SFU 指令需要 2 个 cycle 才能执行完。也因此，如果一个 warp 里所有 thread 都在调用这些复杂的数学函数，SFU 就会成为瓶颈。&lt;/p&gt;&#10;&lt;h3 id="int-unit"&gt;INT unit&#10;&lt;/h3&gt;&lt;p&gt;这是标准的整数 ALU，用来做内存寻址、循环控制以及一般的整数运算。每个 SM 有 64 个 int unit。这些 unit 与 CUDA Core 以及其他计算 core 是可以并行执行的，也就是说 GPU 可以一边计算内存地址（INT），一边处理数据（FP），互不 stall。&lt;/p&gt;&#10;&lt;h3 id="fp64-unit"&gt;FP64 Unit&#10;&lt;/h3&gt;&lt;p&gt;H100 有独立于 FP32 的专用 FP64 core。每个 SM 有 64 个 FP64 Core。&lt;/p&gt;&#10;&lt;p&gt;一般用于科学计算等场景。&lt;/p&gt;&#10;&lt;h3 id="tensor-core"&gt;Tensor Core&#10;&lt;/h3&gt;&lt;p&gt;Tensor Core 是专门加速矩阵乘加运算（D = A × B + C）的单元，深度学习中的绝大多数计算都发生在它上面。H100 的每个 SM 配有 4 个 Tensor Core（每个 SMSP 1 个），全卡共 528 个。&lt;/p&gt;&#10;&lt;p&gt;H100 搭载的是第四代 Tensor Core：新增了对 FP8 精度的支持，并引入了 WGMMA 指令——矩阵乘可以异步执行，且操作数可以直接来自 Shared Memory。TMA 把数据异步搬进 Shared Memory，WGMMA 再异步地从 Shared Memory 取数计算，两者配合就构成了 Hopper 异步流水线的核心。（Tensor Core 的详细机制会在后续章节单独介绍。）&lt;/p&gt;&#10;&lt;h3 id="loadstore-unitlsu"&gt;Load/Store Unit（LSU）&#10;&lt;/h3&gt;&lt;p&gt;它负责执行每个线程的内存指令如 store、load、atomic 等。&lt;/p&gt;&#10;&lt;p&gt;每个 SMSP 有 8 个专用的 LSU，每个 SM 一共 32 个，都是直连 L1、L2 缓存的。&lt;/p&gt;&#10;&lt;p&gt;当 warp 执行 ld/st/atom 时，LSU 会把 32 个 thread 的地址做 coalescing，拼成 cache-line / sector 请求，再去查 L1 data cache。命中就很快回到 register；miss 则继续往 L2 / DRAM 走，回来时也可能填回 L1。&lt;/p&gt;&#10;&lt;p&gt;访问能 coalesced 时，LSU 会把这个 warp 的 32 个地址合并成尽量少的 cache-line，发给 L1/L2 的请求就更少，replay / stall 减少，有效带宽也更高。&lt;/p&gt;&#10;&lt;p&gt;这里需要注意的是 TMA 会直接绕过整个 L1 Cache，进入 L2 Cache 或 DRAM 读取数据并将其放入 Smem 中。&lt;/p&gt;&#10;&lt;h3 id="unified-shared-memory--l1-cache"&gt;Unified Shared Memory + L1 Cache&#10;&lt;/h3&gt;&lt;p&gt;每个 SM 共 256 KB，全卡合计带宽约 33 TB/s，分成 32 个 bank，每个 bank 宽 32 bit（4 byte）。其中 shared memory 大小是 228 KB。&lt;/p&gt;&#10;&lt;p&gt;无冲突时，load / store 各 128 B/cycle。TMA 异步拷贝能打到接近峰值带宽，并和计算重叠。Bank 按连续的 4-byte word 交错分布在相邻 bank 上。（对于 bank 的理解，可以想象成体育场的门）&lt;/p&gt;&#10;&lt;p&gt;L1 cache 相当于 coalescing buffer：把 warp 要的数据收齐，再高效送出去。&lt;/p&gt;&#10;&lt;p&gt;Cache line = 128 byte，Sector = 32 byte，cache 可以按 sector 逐块填充。&lt;/p&gt;&#10;&lt;p&gt;每个 SM 可配置的 shared memory 上限是 228 KB。&lt;/p&gt;&#10;&lt;p&gt;每个 thread block 上限是 227 KB，因为 CUDA 预留了 1 KB。&lt;/p&gt;&#10;&lt;h3 id="registers"&gt;Registers&#10;&lt;/h3&gt;&lt;p&gt;每个 thread 都有一套私有的片上 register。带宽最高、延迟最低，每个 SM 256 KB。每个 SM 每 cycle 最多 128 次读/写。&lt;/p&gt;&#10;&lt;p&gt;Register 经常是瓶颈。如果 kernel 每个 thread 用 R 个 register，那最多常驻 thread 数大约是 floor(65536 / R)（再按 block 粒度和其它限制取整/封顶）。到 128 register/thread 时，每个 block 最多 512 个 thread。&lt;/p&gt;&#10;&lt;p&gt;Register 的基本单位是 32-bit。硬件 register file 本质上就是 32-bit 槽位。用 FP16 或 FP8 时，必须用 packed datatype，才能把 2/4 个元素塞进同一个 register。&lt;/p&gt;&#10;&lt;p&gt;访问 register file 比访问 shared memory 还快 30–50 倍，比访问 HBM3 快 1000 倍以上。&lt;/p&gt;&#10;&lt;p&gt;Register 用量在编译期就定了，不是运行时决定的。&lt;/p&gt;&#10;&lt;p&gt;编译器 register 不够用时，会把数据 spill 到 local memory，比 register 慢很多。CUDA 13.0 加了优先 spill 到 shared memory 的支持，只有 shared memory 也不够时才退回 local memory。&lt;/p&gt;&#10;&lt;p&gt;哪怕只多几个（比如 63 → 65 register/thread），常驻 warp 数也可能掉下去，因为分配会按内部粒度取整。活跃 warp 变少，性能就下来。&lt;/p&gt;&#10;&lt;h2 id="warp"&gt;Warp&#10;&lt;/h2&gt;&lt;p&gt;我们通常称 32 个线程组成的一组是一个 warp，它们从同一个 thread block 里被创建，每个 cycle 共享同一个 warp scheduler 调度，各自私有 register，但执行相同的指令流。&lt;/p&gt;&#10;&lt;p&gt;而 Warp scheduler 通常是负责指令发射的单元，每个 cycle 选出就绪的 warp 来发射指令，并处理 warp 级别的分支和 divergence。维护一个 scoreboard，记录哪些 warp 真正就绪。发射前确保源 register 已经从 register file 读出，或已从 pipeline 转发过来。&lt;/p&gt;&#10;&lt;p&gt;所谓 divergence（分支发散），是指同一个 warp 内的 32 个线程走进了不同的分支（例如 if/else 的两侧）。由于整个 warp 共享同一条指令流，硬件无法让两部分线程同时走两条路径，只能把各分支&lt;strong&gt;串行&lt;/strong&gt;执行：先执行 if 分支，同时用 active mask 屏蔽走 else 的线程，然后再反过来。原本一条指令的工作变成了多条，warp 内的分支越碎，性能损失就越大——因此写 kernel 时应尽量避免 warp 内的分支发散。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;为什么 GPU 要让尽可能多的 warp 常驻在 SM 上？&lt;/strong&gt; 答案是为了隐藏内存访问的延迟（Latency Hiding）。一次 HBM 访问需要数百个 cycle，如果 SM 上只有少量 warp，一旦它们全部陷入内存等待，计算单元就只能空转。而常驻 warp 足够多时，Warp Scheduler 每个 cycle 都能挑出一个已就绪的 warp 发射指令——某个 warp 发起内存请求后转入等待，它空出的执行槽立刻被其他就绪的 warp 顶上，内存延迟就这样被&amp;quot;藏&amp;quot;在了别人的计算之下。这也是前文反复强调的 Occupancy（由 register、shared memory 用量共同决定）对性能如此关键的根本原因。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Latency Hiding：某个 warp 等待内存时，Warp Scheduler 立即切换到其他就绪的 warp，SM 因此始终保持忙碌" class="gallery-image" data-flex-basis="704px" data-flex-grow="293" height="300" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/latency_hiding.gif" srcset="https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/latency_hiding_hu_bdbf6bc8101d096c.gif 800w, https://july-h5kf3.github.io/p/hopper-cuda-01-gpu-arch/latency_hiding.gif 880w" width="880"&gt;&lt;/p&gt;&#10;&lt;h2 id="动手实验"&gt;动手实验&#10;&lt;/h2&gt;&lt;p&gt;本章的四个硬件概念（Coalescing、Occupancy 与 Latency Hiding、Bank Conflict、SFU 吞吐）各配了一个可编译运行的小实验，放在 &lt;strong&gt;&lt;a class="link" href="https://github.com/July-h5kf3/HopperLearing/tree/main/assignment/assignment01" target="_blank" rel="noopener"&#10; &gt;Assignment 01&lt;/a&gt;&lt;/strong&gt;。读完本章建议亲手跑一遍——纸上的硬件数字变成实测数据，理解会深很多。&lt;/p&gt;&#10;&lt;h2 id="h100-的重大变革"&gt;H100 的重大变革&#10;&lt;/h2&gt;&lt;p&gt;H100 拥有 80GB 的 HBM3 内存，内存带宽达到 3.35 TB/s。&lt;/p&gt;&#10;&lt;p&gt;拥有 132 个 SM，每个 SM 拥有 4 个 Tensor Core，总共有 528 个 Tensor Core。&lt;/p&gt;&#10;&lt;p&gt;Hopper 架构最大的特性在于引入了异步特性：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;TMA&lt;/strong&gt;：每个 SM 有一个 TMA 单元，这是一个用于将 Tensor 数据拷贝操作从 SM 中卸载出去的硬件单元。在先前的架构中，我们通常需要让每个线程执行相应的计算、遍历数据区域、并发射多条指令，才能完成 Global Memory 到 Shared Memory 的数据传输。而有了 TMA 之后，我们可以通过 TMA Descriptor 来异步地执行这些操作。只需要一个线程发起整个数据搬运操作，剩下的数据移动工作由硬件在后台完成。&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;同步拷贝&lt;/strong&gt;：如果一个拷贝是同步的，那么发起这个拷贝操作的线程在拷贝完成前就无法继续执行接下来的指令了，也就是说当控制流重新回到这个线程时，这个数据拷贝操作已经完成了。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;异步拷贝&lt;/strong&gt;：如果一个拷贝是异步的，那么发起拷贝操作仅仅意味着启动了数据传输，发起者（这个线程）可以在数据传输进行时继续执行其他工作，之后在真正需要使用这些数据之前，再去等待或检查拷贝是否完成。这种方式比同步拷贝更加高效，能够让我们有效地掩盖内存访问带来的延迟。（例如在处理第 i 块数据时，预取第 i+1 块数据）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;第四代 Tensor Core&lt;/strong&gt;：FP8 的支持（1979 TFLOPs），WGMMA&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Thread Block Cluster&lt;/strong&gt;：Hopper 在 Thread Block 和 Grid 之间引入的新层级。同一个 Cluster 内的多个 Thread Block 会被硬件保证共同调度到同一个 GPC 内的不同 SM 上执行，这些 Block 之间可以通过 Distributed Shared Memory（DSMEM）直接读写彼此的 Shared Memory，并在 Cluster 范围内做同步。这让跨 SM 的线程协作第一次有了硬件级的支持，而不必再绕道 Global Memory。（更详细的内容会在后续章节介绍。）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Triton</title><link>https://july-h5kf3.github.io/p/triton/</link><pubDate>Tue, 14 Apr 2026 21:00:00 +0800</pubDate><guid>https://july-h5kf3.github.io/p/triton/</guid><description>&lt;h2 id="motivation"&gt;Motivation&#10;&lt;/h2&gt;&lt;p&gt;CS336课程中在介绍实现FlashAttention2的时候介绍了Triton，我发现这是一个比CUDA更加方便的GPU编程语言。&lt;/p&gt;&#10;&lt;p&gt;另一方面许多大厂高性能计算岗位包括但不限于算子开发都逐渐趋向于使用Triton，因此目前看来学习Triton是一件从投资未来的角度来看性价比很高的事情&lt;/p&gt;&#10;&lt;h2 id="introduction"&gt;Introduction&#10;&lt;/h2&gt;&lt;p&gt;Triton 是 OpenAI 开发的一种面向深度学习与高性能计算的编程语言及编译器，旨在简化 GPU 高性能算子的开发过程，并帮助开发者更容易地写出高效代码。&lt;/p&gt;&#10;&lt;p&gt;Triton的&lt;a class="link" href="https://triton-lang.cn/main/programming-guide/chapter-1/introduction.html" target="_blank" rel="noopener"&#10; &gt;官方文档&lt;/a&gt;指出,其项目动机来自于基于分块算法的编程范式可以促进构建用于神经网络的高性能计算内核。与传统 CUDA 编程更强调线程级的执行方式不同，Triton 提供了更高层次的块级抽象，开发者可以直接围绕数据块来组织计算，而不必过多关注底层线程调度细节。&lt;/p&gt;&#10;&lt;p&gt;我们可以通过矩阵乘法的例子来更加直观的理解。&lt;/p&gt;&#10;&lt;p&gt;CUDA编程模型:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#pragma parallel&#10;for(int m = 0; m &amp;lt; M; m++)&#10;#pragma parallel&#10;for(int n = 0; n &amp;lt; N; n++){&#10; float acc = 0;&#10; for(int k = 0; k &amp;lt; K; k++)&#10; acc += A[m, k] * B[k, n];&#10;&#10; C[m, n] = acc;&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Triton编程模型:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#pragma parallel&#10;for(int m = 0; m &amp;lt; M; m += MB)&#10;#pragma parallel&#10;for(int n = 0; n &amp;lt; N; n += NB){&#10; float acc[MB, NB] = 0;&#10; for(int k = 0; k &amp;lt; K; k += KB)&#10; acc += A[m:m+MB, k:k+KB]&#10; @ B[k:k+KB, n:n+NB];&#10; C[m:m+MB, n:n+NB] = acc;&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="gpu"&gt;GPU&#10;&lt;/h2&gt;&lt;p&gt;在具体介绍Triton之前，我们先简单了解一下GPU相关的知识，以便减少后续专业术语带来的知识bias。&lt;/p&gt;&#10;&lt;p&gt;以下内容来自于本人于南开大学计算机体系结构期末突击的笔记。&lt;/p&gt;&#10;&lt;p&gt;相较于CPU而言，GPU的设计思路可以概括为三点:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;简化流水线，增加核数&lt;/strong&gt;：具体而言，对流水线进行瘦身，去掉乱序执行，分支预测等复杂逻辑，节省的空间用来增加大量的计算核心，从而能够同时处理大量数据&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;多个计算单元共用一条指令&lt;/strong&gt;：GPU 擅长处理“很多数据做同一种计算”的场景，例如对向量中的每个元素做相同变换。为此，它通常让一组线程(这样的一组线程称为Warp)按照相同的程序逻辑并行执行，只是每个线程处理的数据不同。这种执行方式通常称为 SIMT。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;驻留大量线程&lt;/strong&gt;：GPU 会同时准备很多线程。当一部分线程因为访存而需要等待时，硬件可以立即切换去执行另一部分线程，从而减少计算单元空闲的时间。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;我们接下来以数组加法来介绍GPU编程模型中的几个基本概念，这些概念有助于理解后续的并行执行，分块计算和访存优化。&lt;/p&gt;&#10;&lt;p&gt;假设我们要计算数组加法:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;for i in range(1024):&#10;&#9;C[i] = A[i] + B[i]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;那么一个GPU Kernel会以大量线程的形式启动。所有线程执行相同的Kernel代码，但每个线程会根据自己的编号处理不同位置的数据。在本例中，每个线程可以分别负责不同的i&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Thread:即线程，这是程序员视角下最基本的并行单位。每个线程执行同一份kernel代码，但处理的数据不同&lt;/li&gt;&#10;&lt;li&gt;Block:多个Thread会组成一个Block。其意义在于&lt;strong&gt;协作&lt;/strong&gt;，即同一个block内的线程可以通过共享内存交换数据，也可以使用同步原语协调执行不同 block 之间通常不能直接同步或共享局部数据。&lt;/li&gt;&#10;&lt;li&gt;Grid:一次Kernel启动产生的所有线程构成一个grid。grid只是一次kernel启动时的逻辑组织方式，本身并不对应某个具体的硬件结构&lt;/li&gt;&#10;&lt;li&gt;Warp:在硬件执行层面，线程通常不会以单个 thread 为单位独立调度，而是会进一步组成 warp。以 NVIDIA GPU 为例，一个 warp 通常包含 32 个线程。warp 内的线程会执行相同的指令流程，但处理各自的数据。这意味着，虽然我们在编程时使用的是 thread 和 block 这些抽象概念，但在硬件上，block 内的线程最终仍会被拆分为多个 warp 来执行。&lt;/li&gt;&#10;&lt;li&gt;SM:流处理器，GPU上的block会被分配到SM上执行。一个SM可以同时驻留多个warp，甚至是多个block。当某个warp因访存等高延迟操作暂时停顿时，硬件调度器可以快速切换到其他就绪的 warp 继续执行，从而隐藏延迟、提升整体吞吐。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="weighted-sum"&gt;Weighted Sum&#10;&lt;/h2&gt;&lt;p&gt;我们接下来通过“加权求和”的例子来具体了解Triton的知识以及它是如何与Pytorch进行协同工作。&lt;/p&gt;&#10;&lt;h4 id="forward-pass"&gt;Forward Pass&#10;&lt;/h4&gt;&lt;p&gt;“加权求和”的前向传播过程可以通过如下形式进行描述:&lt;/p&gt;&#10;&lt;p&gt;​&#9;给定一个输入矩阵&lt;code&gt;x&lt;/code&gt;,我们将它的每个元素乘以一个按列加权的向量&lt;code&gt;w&lt;/code&gt;,然后对每一行求和，最终得到矩阵&lt;code&gt;x&lt;/code&gt;与向量&lt;code&gt;w&lt;/code&gt;的加权和。&lt;/p&gt;&#10;&lt;p&gt;用python代码描述则是:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;def weighted_sum(x,weight):&#10;&#9;return (weight*x).sum(axis=-1)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;而我们想把这个过程用Triton进行并行化，那么首先需要分析可并行性，我们可以发现矩阵与向量的乘积在最后求和得到结果之前各个元素的运算都是相互独立的，因此每个元素的计算都是可并行的。&lt;/p&gt;&#10;&lt;p&gt;因此在编写Triton Kernel时，我们会让每个program instance(Triton中一组执行同一段程序的线程块)负责&lt;code&gt;x&lt;/code&gt;中某一块行的加权和，并把对应的标量输出写入输出张量。&lt;/p&gt;&#10;&lt;p&gt;例如，假设&lt;code&gt;x&lt;/code&gt;的形状是&lt;code&gt;[ROWS,D]&lt;/code&gt;,&lt;code&gt;weight&lt;/code&gt;的形状是&lt;code&gt;[D]&lt;/code&gt;，我们会让每个program instance负责&lt;code&gt;x&lt;/code&gt;中若干行的加权和的计算，得到每个块行的加权和结果，最后将结果均写入输出张量。&lt;/p&gt;&#10;&lt;p&gt;与直接把tensor作为参数不同，我们会在kernel中传入:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;指向张量首元素的指针&lt;/li&gt;&#10;&lt;li&gt;每个tensor的stride，其含义是告诉我们如何沿着各个维度移动&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;我们可以利用这些 stride，结合 program ID，将工作分配给不同实例（例如，第 &lt;code&gt;i&lt;/code&gt; 个实例处理 &lt;code&gt;x&lt;/code&gt; 的第 &lt;code&gt;i&lt;/code&gt; 个行块），从而加载当前实例所负责的 &lt;code&gt;x&lt;/code&gt; 的一块数据。&lt;/p&gt;&#10;&lt;p&gt;在本例中，Triton的Forward Pass和PyTorch的主要区别在于:Triton需要显式地进行指针运算以及load/store操作。&lt;/p&gt;&#10;&lt;p&gt;我们来看具体的代码:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;import triton&#10;import triton.language as tl&#10;@triton.jit &#10;def weighted_sum_fwd(&#10;&#9;x_ptr,weight_ptr,&#9;&#9;&#9;#输入指针&#10;&#9;output_ptr,&#9;&#9;&#9;&#9;&#9;#输出指针&#10;&#9;x_stride_row,x_stride_dim,&#9;#stride告诉我们在tensor的每个轴上移动一个元素需要跳多远&#10;&#9;weight_stride_dim,&#9;&#9;&#9;#通常是1&#10;&#9;output_stride_dim,&#9;&#9;&#9;#通常是1&#10;&#9;ROWS,D,&#10;&#9;ROWS_TILE_SIZE,D_TILE_SIZE,&#9;#tile形状必须在编译期已知&#10;):&#10;&#9;#每个instance负责计算x中一个行块的加权和&#10;&#9;#`tl.program_id`用于查看当前运行的是哪个线程块&#10;&#9;row_tile_idx = tl.program_id(0)&#10;&#9;&#10;&#9;#block pointer 允许我们从一个N维内存区域中选取数据块&#10;&#9;#并在这个区域中移动所选的数据块&#10;&#9;#在使用block pointer时必须知道:&#10;&#9;#- tensor 第一个元素的指针&#10;&#9;#- tensor 的整体形状&#10;&#9;#- 每个维度的stride&#10;&#9;#- 起始block的N维坐标&#10;&#9;#- 每次load/store的block形状&#10;&#9;#- 内存中维度从主到次的顺序&#10;&#9;x_block_ptr = tl.make_block_ptr(&#10;&#9;&#9;x_ptr,&#10;&#9;&#9;shape=(ROWS,D),&#10;&#9;&#9;strides=(x_stride_row,x_stride_dim),&#10;&#9;&#9;offsets=(row_tile_idx * ROW_TILE_SIZE,0),&#10;&#9;&#9;block_shape=(ROW_TILE_SIZE,D_TILE_SIZE),&#10;&#9;&#9;order(1,0),&#10;&#9;)&#10;&#9;weight_block_ptr=tl.make_block_ptr(&#10;&#9;&#9;weight_ptr,&#10;&#9;&#9;shape=(D,),&#10;&#9;&#9;strides=(weight_stride_dim,),&#10;&#9;&#9;offsets=(0,),&#10;&#9;&#9;block_shape=(D_TILE_SIZE,),&#10;&#9;&#9;order(0,),&#10;&#9;)&#10;&#9;output_block_ptr=tl.make_block_ptr(&#10;&#9;&#9;output_ptr,&#10;&#9;&#9;shape=(ROWS,),&#10;&#9;&#9;strides=(output_stride_row,),&#10;&#9;&#9;offsets=(row_tile_idx*ROWS_TILE_SIZE,),&#10;&#9;&#9;block_shape=(ROWS_TILE_SIZE,),&#10;&#9;&#9;order(0,),&#10;&#9;)&#10;&#9;#初始化一个用于写入的buffer&#10;&#9;output = tl.zeros((ROWS_TILE_SIZE,),dtype=tl.float32)&#10;&#9;for i in range(tl.cdiv(D,D_TILE,SIZE)):&#10;&#9;&#9;#加载当前block pointer指向的数据块&#10;&#9;&#9;#因为ROW_TILE_SIZE可能不整除ROWS，&#10;&#9;&#9;#D_TILE_SIZE 也可能不能整除 D，&#10;&#9;&#9;#所以两个维度都需要进行边界检查&#10;&#9;&#9;row = tl.load(x_block_ptr,boundary_check=(0,1),padding_option=&amp;#34;zero&amp;#34;)&#10;&#9;&#9;weight = tl.load(weight_block_ptr,boundary_check=(0,),padding_option=&amp;#34;zero&amp;#34;)&#10;&#9;&#9;#计算当前行块的加权和&#10;&#9;&#9;output += tl.sum(row * weight[None,:],axis=1)&#10;&#9;&#9;#将指针移动到下一个tile,这里参数时(行偏移，列偏移)&#10;&#9;&#9;x_block_ptr = x_block_ptr.advance(0,D_TILE_SIZE)&#9;#在最后一个维度上前进D_TILE_SIZE&#10;&#9;&#9;weight_block_ptr = weight_block_ptr.advance((D_TILE_SIZE,))&#10;&#9;# 将输出写入 output block pointer（每行一个标量）&#10; # 因为 ROWS_TILE_SIZE 可能不能整除 ROWS，所以需要边界检查&#10;&#9;tl.store(output_block_ptr,output,boundary_check(0,))&#10;&#9;&#9;&#10;&#9;&#9;&#10;&#9;&#9;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接下来我们将这个实现的Kernel封装到一个Pytorch &lt;code&gt;AutoGrad&lt;/code&gt;函数中，使其能够与PyTorch生态协同工作(即接收 Tensor 输入、输出 Tensor，并在 backward pass 中和 autograd 引擎协作):&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;class WeightedSumFunc(torch.autograd.Function):&#10;&#9;@staticmethod&#10;&#9;def forward(ctx,x,weight):#这里ctx指上下文&#10;&#9;&#9;#将x和weight缓存起来，以便在backward中使用&#10;&#9;&#9;#backward时我们只会接收到输出张量的梯度&#10;&#9;&#9;# 需要利用这些缓存来计算 x 和 weight 的梯度&#10;&#9;&#9;D,output_dims = x.shape[-1],x.shape[:-1]&#10;&#9;&#9;input_shape = x.shape&#10;&#9;&#9;x = rearrange(x,&amp;#34;... d -&amp;gt; (...) d&amp;#34;)&#10;&#9;&#9;ctx.save_for_backward(x,weight)&#10;&#9;&#9;&#10;&#9;&#9;assert len(weight.shape) == 1 and weight.shape[0] == D, &amp;#34;维度不匹配&amp;#34;&#10; assert x.is_cuda and weight.is_cuda, &amp;#34;期望输入是 CUDA tensor&amp;#34;&#10; assert x.is_contiguous(), &amp;#34;我们的指针运算默认 x 是连续的&amp;#34;&#10; &#10; ctx.D_TILE_SIZE = triton.next_power_of_2(D) # 16 #大致让dim维度循环16次&#10; ctx.ROWS_TILE_SIZE = 16&#9;&#9;&#9;&#9;&#9;&#9;# 每个线程一次处理16个batch元素&#10; ctx.input_shape = input_shape&#10; # 需要初始化一个空的结果 tensor&#10; # 注意：这里的元素未必初始化为 0&#10; y = torch.empty(output_dims,device = x.device)&#10; &#10; # 以 1D launch grid 启动 kernel&#10; n_rows = y.numel()&#10; weighted_sum_fwd[(cdiv(n_rows,ctx.ROWS_TILE_SIZE),)](&#9;#和CUDA的&amp;lt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&amp;gt;有点像，作用是定义grid大小&#10; &#9;x,weight,&#10; &#9;y,&#10; &#9;x.stride(0),x.stride(1),&#10; &#9;weight.stride(0),&#10; &#9;y.stride(0),&#10; &#9;ROWS = n_rows,D=D,&#10; &#9;ROWS_TILE_SIZE=ctx.ROWS_TILE_SIZE, D_TILE_SIZE=ctx.D_TILE_SIZE,&#10; )&#10; return y.view(input_shape[:-1])&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="backward-pass"&gt;Backward Pass&#10;&lt;/h4&gt;&lt;p&gt;由于我们定义了自己的kernel，因此也需要自己编写backward函数。&lt;/p&gt;&#10;&lt;p&gt;在forward pass中，我们拿到了层的输入，并计算其输出。而在backward pass中我们会拿到目标函数对该层输出的梯度，然后需要计算目标函数对每个输入的梯度。&lt;/p&gt;&#10;&lt;p&gt;在这个例子中，我们的操作有两个输入:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;矩阵$x \in \mathbb{R}^{n\times h}$&lt;/li&gt;&#10;&lt;li&gt;权重向量$w \in \mathbb{R}^h$&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;记我们的操作为$f(x,w)$，其输出属于$\mathbb{R}^n$&lt;/p&gt;&#10;&lt;p&gt;若给定损失函数L对该层输出的梯度$\nabla_{f(x,w)}L$,则根据多元链式法则，可以得到关于x和w的梯度:&#10;&lt;/p&gt;&#10;$$&#10;(\nabla_xL)_{ij}=\sum_{k=1}^n\frac{\partial f(x,w)_k}{\partial x_{ij}}(\nabla_{f(x,w)}L)_k = w_j \cdot (\nabla_{f(x,w)}L)_i\\&#10;(\nabla_w L)_{j} = \sum_{i=1}^n\frac{\partial f(x,w)_i}{\partial w_j}(\nabla_{f(x,w)}L)_i=\sum_{i=1}^nx_{ij}\cdot(\nabla_{f(x,w)}L)_i&#10;$$&lt;p&gt;&#10;这里给出一个很简单的backward计算公式。&lt;/p&gt;&#10;&lt;p&gt;为了得到关于x的backward结果，我们根据上式计算w和$\nabla f(x,w)$的外积。而为了得到关于w的backward结果(即$(\nabla_w L)_j$),我们必须把输入与对应输出的梯度逐行相乘并求和。&lt;/p&gt;&#10;&lt;p&gt;按照同样的思路，我们来实现这个kernel:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def weighted_sum_backward(&#10;&#9;x_ptr,weight_ptr,&#10;&#9;grad_output_ptr,&#10;&#9;grad_x_ptr,partial_grad_weight_ptr,&#10;&#9;stride_xr,stride_xd,&#10;&#9;stride_wd,&#10;&#9;stride_gr,&#10;&#9;stride_gxr,stride_gxd,&#10;&#9;stride_gwb, stride_gwd,&#10; NUM_ROWS, D,&#10; ROWS_TILE_SIZE, D_TILE_SIZE,&#10;):&#10;&#9;row_tile_idx = tl.program_id(0)&#10;&#9;n_row_tiles = tl.num_programs(0)&#10;&#9;#输入&#10;&#9;grad_output_block_ptr = tl.make_block_ptr(&#10;&#9;&#9;grad_output_ptr,&#10;&#9;&#9;shape=(NUM_ROWS,),strides=(stride_gr,),&#10;&#9;&#9;offsets=(row_tile_idx*ROW_TILE_SIZE,),&#10;&#9;&#9;block_shape=(ROWS_TILE_SIZE,),&#10;&#9;&#9;order=(0,),&#10;&#9;)&#10;&#9;x_block_ptr = tl.make_block_ptr(&#10;&#9;&#9;x_ptr,&#10;&#9;&#9;shape=(NUM_ROWS,D,),strides=(stride_xr,stride_xd),&#10;&#9;&#9;offsets=(row_tile_idx * ROW_TILE_SIZE,0),&#10;&#9;&#9;block_shape=(ROWS_TILE_SIZE,D_TILE_SIZE),&#10;&#9;&#9;order=(1,0),&#10;&#9;)&#10;&#9;weight_block_ptr = tl.make_block_ptr(&#10;&#9;&#9;weight_ptr,&#10;&#9;&#9;shape=(D,),strides=(stride_wd,),&#10;&#9;&#9;offsets=(0,),block_shape=(D_TILE_SIZE,),&#10;&#9;&#9;order=(0,),&#10;&#9;)&#10;&#9;grad_x_block_ptr = tl.make_block_ptr(&#10;&#9;&#9;grad_x_ptr,&#10;&#9;&#9;shape=(NUM_ROWS,D,),strides=(stride_gxr,stride_gxd),&#10;&#9;&#9;offsets=(row_tile_idx*ROW_TILE_SIZE,D_TILE_SIZE),&#10;&#9;&#9;block_shape=(ROW_TILE_SIZE,D_TILE_SIZE),&#10;&#9;&#9;order=(1,0),&#10;&#9;)&#10;&#9;partial_grad_weight_block_ptr = tl.make_block_ptr(&#10;&#9;&#9;partial_grad_weight_ptr,&#10;&#9;&#9;shape=(n_row_tiles,D,),strides=(stride_gwb,stride_gwd),&#10;&#9;&#9;offsets=(row_tile_idx,0),&#10;&#9;&#9;block_shape=(1,D_TILE_SIZE),&#10;&#9;&#9;order=(1,0),&#10;&#9;)&#10;&#9;for i in range(tl.cdiv(D,D_TILE_SIZE)):&#10;&#9;&#9;grad_output = tl.load(&#10;&#9;&#9;&#9;grad_output_block_ptr,&#10;&#9;&#9;&#9;boundary_check=(0,),&#10;&#9;&#9;&#9;padding_option=&amp;#34;zero&amp;#34;,&#10;&#9;&#9;)&#10;&#9;&#9;weight = tl.load(&#10;&#9;&#9;&#9;weight_block_ptr,&#10;&#9;&#9;&#9;boundary_check=(0,),&#10;&#9;&#9;&#9;padding_option=&amp;#34;zero&amp;#34;,&#10;&#9;&#9;)&#10;&#9;&#9;grad_x_row = grad_output[:,None] * weight[None,:]&#10;&#9;&#9;tl.store(grad_x_block_ptr,grad_x_row,boundary_chekc=(0,1))&#10;&#9;&#9;&#10;&#9;&#9;row = tl.load(&#10;&#9;&#9;&#9;x_block_ptr,&#10;&#9;&#9;&#9;boundary_check=(0,1),&#10;&#9;&#9;&#9;padding_option=&amp;#34;zero&amp;#34;,&#10;&#9;&#9;)&#10;&#9;&#9;grad_weight_row = tl.sum(row * grad_output[:,None],axis=0,keep_dims=True)&#10;&#9;&#9;tl.store(partial_grad_weight_block_ptr,grad_weight_row,boundary_check=(1,))&#10;&#9;&#9;&#10;&#9;&#9;x_block_ptr = x_block_ptr.advance((0,D_TILE_SIZE))&#10;&#9;&#9;weight_block_ptr = weight_block_ptr.advance((D_TILE_SIZE,))&#10;&#9;&#9;partial_grad_weight_block_ptr = parital_grad_weight_block_ptr.advance((0,D_TILE_SIZE))&#10;&#9;&#9;grad_x_block_ptr = grad_x_block_ptr.advance((0,D_TILE_SIZE))&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每个 kernel instance 只负责处理 &lt;code&gt;x&lt;/code&gt; 的一个行块，但我们现在需要沿着 &lt;code&gt;x&lt;/code&gt; 的行方向求和。&#10;因此，在 backward kernel 内，我们&lt;strong&gt;不直接完成这个总和&lt;/strong&gt;，而是假设 &lt;code&gt;partial_grad_weight_ptr&lt;/code&gt; 是一个形状为 &lt;code&gt;n_row_tiles × H&lt;/code&gt; 的矩阵，其中第一维表示来自每个行块的局部归约结果。&lt;/p&gt;&#10;&lt;p&gt;也就是说：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;kernel 内部：只在当前行块内完成局部归约&lt;/li&gt;&#10;&lt;li&gt;kernel 外部：再用 &lt;code&gt;torch.sum&lt;/code&gt; 把所有行块的结果加起来，得到最终的 $\nabla_w$&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这样一来，&lt;code&gt;autograd.Function&lt;/code&gt; 的最后一部分就相对简单了：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;class WeightSumFunc(torch.autograd.Function):&#10;&#9;@staticmethod&#10;&#9;def forward(ctx,x,weight):&#10;&#9;&#9;#这里省略，前面定义过了&#10;&#9;@staticmethod&#10;&#9;def backward(ctx,grad_out):&#10;&#9;&#9;x,weight = ctx.saved_tensors&#10;&#9;&#9;ROWS_TILE_SIZE,D_TILE_SIZE = ctx.ROWS_TILE_SIZE,ctx.D_TILE_SIZE&#10;&#9;&#9;n_rows,D = x.shape()&#10;&#9;&#9;&#10;&#9;&#9;#我们的策略是：每个线程块先写入一个partial buffer，然后对这个buffer做归约得到最终梯度&#10;&#9;&#9;parital_grad_weight = torch.empty(&#10;&#9;&#9;&#9;(cdiv(n_rows,ROWS_TILE_SIZE),D),&#10;&#9;&#9;&#9;device=x.device,&#10;&#9;&#9;&#9;dtype=x.dtype&#10;&#9;&#9;)&#10;&#9;&#9;grad_x = torch.empty_like(x)&#10;&#9;&#9;weighted_sum_backward[(cdiv(n_rows,ROWS_TILE_SIZE),)](&#10;&#9;&#9;&#9;x,weight,&#10;&#9;&#9;&#9;grad_out,&#10;&#9;&#9;&#9;grad_x,partial_grad_weight,&#10;&#9;&#9;&#9;x.stride(0),x.stride(1),&#10;&#9;&#9;&#9;weight.stride(0),&#10;&#9;&#9;&#9;grad_out.stride(0),&#10;&#9;&#9;&#9;grad_x.stride(0), grad_x.stride(1),&#10; partial_grad_weight.stride(0), partial_grad_weight.stride(1),&#10; NUM_ROWS=n_rows, D=D,&#10; ROWS_TILE_SIZE=ROWS_TILE_SIZE, D_TILE_SIZE=D_TILE_SIZE,&#10;&#9;&#9;)&#10;&#9;&#9;grad_weight = partial_grad_weight.sum(axis=0)&#10;&#9;&#9;return grad_x,grad_weight&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最后我们就可以得到一个函数，它的使用方法和&lt;code&gt;torch.nn.functional&lt;/code&gt;里的函数很相似:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;f_weightedsum = WeightedSumFunc.apply&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们可以对两个PyTorch tensor &lt;code&gt;x&lt;/code&gt;和&lt;code&gt;w&lt;/code&gt;调用这个函数，就会得到类似如下的结果:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;tensor([ 90.8563, -93.6815, -80.8884, ..., 103.4840, -21.4634, -24.0192],&#10; device=&amp;#39;cuda:0&amp;#39;, grad_fn=&amp;lt;WeightedSumFuncBackward&amp;gt;)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;请注意输出 tensor 上附带的 &lt;code&gt;grad_fn&lt;/code&gt; —— 这表明 PyTorch 已经知道：当这个 tensor 出现在计算图中并需要做 backward pass 时，应该调用什么函数。&lt;/p&gt;&#10;&lt;p&gt;至此，我们就完成了这个 &lt;strong&gt;weighted sum 操作的 Triton 实现&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;对于其它算子，我们可以按照同样的思路进行。实际上重点还是在于我们应该分析出哪些是可以并行的，program instance该如何划分。&lt;/p&gt;&#10;&lt;p&gt;简单来说，在设计Triton算子的时候我们需要考虑三个问题:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;输出的自然分块是什么？例如在Weighted Sum中Y[0],Y[1]相互独立按行产生，那么Program Instance可以负责生成一个&lt;code&gt;ROW_TILE_SIZE&lt;/code&gt;大小的行块的结果。&lt;/li&gt;&#10;&lt;li&gt;一个块的输出需要哪些输入？&lt;/li&gt;&#10;&lt;li&gt;这些输入能不能以连续，重复利用的方式被加载？&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这三个问题虽然简单，但是在Triton编程中很重要，在实际问题中也很难立即给出标准答案，需要我们不断试错积累经验。&lt;/p&gt;&#10;&lt;p&gt;建议参考仓库(Assignment2-systems/Triton/WeightedSum.py)中的代码，不要直接复制粘贴，不然会出现一些奇奇怪怪的错误，这里的代码是参考了CS336 Assignment 2中的代码.&lt;/p&gt;&#10;&lt;h2 id="triton-puzzles-lite"&gt;Triton Puzzles (Lite)&#10;&lt;/h2&gt;&lt;p&gt;为了更深一步的学习Triton,我在网上查找到了一份通过做题来学习Triton的repo。&lt;/p&gt;&#10;&lt;p&gt;原始的repo是&lt;a class="link" href="https://github.com/srush/Triton-Puzzles" target="_blank" rel="noopener"&#10; &gt;srush/Triton-Puzzles: Puzzles for learning Triton&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;但是有一个更好的实现版本是:&lt;a class="link" href="https://github.com/SiriusNEO/Triton-Puzzles-Lite" target="_blank" rel="noopener"&#10; &gt;SiriusNEO/Triton-Puzzles-Lite: Puzzles for learning Triton, play it with minimal environment configuration!&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;因此我们还是以这个仓库为题面来进行解答。&lt;/p&gt;&#10;&lt;p&gt;另外需要强调的一点是，正如Assignment2实验手册中提到的那样，目前大多教程没有使用更新的，更方便的block pointer抽象。因此在此基础上，在本blog中，我将以block pointer抽象来完成这些任务。&lt;/p&gt;&#10;&lt;p&gt;之所以说block pointer更加方便，是因为在Triton中，我们的Program Instance的操作对象是划分后的Tile，直观来看就是一个矩形区域。在使用block pointer之前，我们需要得到这个处理对象需要通过Program ID以及SIZE用公式去计算。例如我们想知道处理一个&lt;code&gt;x[ROWS,D]&lt;/code&gt;的tile，我们需要写:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;row_offsets = row_tile_idx * ROWS_TILE_SIZE + tl.arrange(0,ROWS_TILE_SIZE)&#10;col_offsets = tl.arrange(0,D_TILE_SIZE)&#10;#计算出当前块负责哪些行和哪些列&#10;x_ptrs = x_ptr + row_offsets[:,None] * x_stride_row + col_offsets[None,:] * x_stride_dim&#10;#然后把他们拼接为二维地址&#10;mask = (row_offsets[:, None] &amp;lt; ROWS) &amp;amp; (col_offsets[None, :] &amp;lt; D)&#10;#然后再load&#10;x = tl.load(x_ptrs, mask=mask, other=0.0)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;而使用block pointer的话，我们就可以写:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;x_block_ptr = tl.make_block_ptr(...)&#10;x = tl.load(x_block,boundary_check=(0,1),padding=&amp;#34;zero&amp;#34;)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样做就省去了大量的地址计算，一方面更加便捷，另一方面代码的可读性也大大提升了。接下来我们看看具体的问题以及如何解决吧？&lt;/p&gt;&#10;&lt;h3 id="puzzles-12-constant-add"&gt;Puzzles 1/2 Constant Add&#10;&lt;/h3&gt;&lt;p&gt;问题描述:&lt;/p&gt;&#10;&lt;p&gt;对向量中的每个元素都加上常数10.使用一个程序ID轴。&lt;/p&gt;&#10;&lt;p&gt;问题解答：&lt;/p&gt;&#10;&lt;p&gt;我们按照上面Weighted Sum中&lt;code&gt;make_block_ptr&lt;/code&gt;的方式定义好&lt;code&gt;x_block_ptr&lt;/code&gt;以及&lt;code&gt;z_block_ptr&lt;/code&gt;，然后&lt;code&gt;tl.load&lt;/code&gt;出x，进行运算得到z最后进行&lt;code&gt;tl.store&lt;/code&gt;即可。&lt;/p&gt;&#10;&lt;p&gt;由于N0不一定能被B0整除，因此需要进行boundary check&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def add_kernel(x_ptr, z_ptr, N0, B0: tl.constexpr):&#10; row_tile_idx = tl.program_id(0)&#10; x_block_ptr = tl.make_block_ptr(&#10; base=x_ptr,&#10; shape=(N0,),&#10; strides=(1,),&#10; offsets=(row_tile_idx * B0,),&#10; block_shape=(B0,),&#10; order=(0,)&#10; )&#10; z_block_ptr = tl.make_block_ptr(&#10; base=z_ptr,&#10; shape=(N0,),&#10; strides=(1,),&#10; offsets=(row_tile_idx * B0,),&#10; block_shape=(B0,),&#10; order=(0,)&#10; )&#10; x = tl.load(x_block_ptr,boundary_check=(0,),padding_option=&amp;#39;zero&amp;#39;)&#10; z = x + 10.0&#10; tl.store(z_block_ptr, z, boundary_check=(0,))&#10; # Finish me!&#10; return&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="puzzles-34-outer-vector-add"&gt;Puzzles 3/4 Outer Vector Add&#10;&lt;/h3&gt;&lt;p&gt;问题描述:&lt;/p&gt;&#10;&lt;p&gt;把两个向量按广播方式相加，得到一个二维结果矩阵&lt;code&gt;z&lt;/code&gt;。&#10;&lt;/p&gt;&#10;$$&#10;z_{j,i} = x_i+y_j \quad i=1,\dots,B_0,j=1,\dots,B_1&#10;$$&lt;p&gt;问题解答:&lt;/p&gt;&#10;&lt;p&gt;这个可以利用Python中的broadcast机制:&lt;code&gt;x[None,:]+y[:,None]&lt;/code&gt;。在计算时会先将x的形状变为&lt;code&gt;(1,N0)&lt;/code&gt;,y变为&lt;code&gt;(N1,1)&lt;/code&gt;然后相加的时候触发广播机制，两者的 shape 均广播到 &lt;code&gt;(N1, N0)&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;在Triton中也有同样的机制，我们可以利用这个得到最终的结果。&lt;/p&gt;&#10;&lt;p&gt;考虑到N0,N1,B0,B1的差异，我们的&lt;code&gt;grid&lt;/code&gt;相应地也要变成二维的，第一个维度是走&lt;code&gt;x&lt;/code&gt;的tile，第二个维度走&lt;code&gt;y&lt;/code&gt;的tile&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def add_vec_block_kernel(&#10; x_ptr, y_ptr, z_ptr, N0, N1, B0: tl.constexpr, B1: tl.constexpr&#10;):&#10; x_tile_idx = tl.program_id(0)&#10; y_tile_idx = tl.program_id(1)&#10;&#10; x_block_ptr = tl.make_block_ptr(&#10; base=x_ptr,&#10; shape=(N0,),&#10; strides=(1,),&#10; offsets=(x_tile_idx * B0,),&#10; block_shape=(B0,),&#10; order=(0,)&#10; )&#10; y_block_ptr = tl.make_block_ptr(&#10; base=y_ptr,&#10; shape=(N1,),&#10; strides=(1,),&#10; offsets=(y_tile_idx * B1,),&#10; block_shape=(B1,),&#10; order=(0,)&#10; )&#10; z_block_ptr = tl.make_block_ptr(&#10; base=z_ptr,&#10; shape=(N1, N0),&#10; strides=(N0, 1),&#10; offsets=(y_tile_idx * B1, x_tile_idx * B0),&#10; block_shape=(B1, B0),&#10; order=(0, 1)&#10; )&#10; x = tl.load(x_block_ptr,boundary_check=(0,),padding_option=&amp;#39;zero&amp;#39;)&#10; y = tl.load(y_block_ptr,boundary_check=(0,),padding_option=&amp;#39;zero&amp;#39;)&#10; z = x[None, :] + y[:, None]&#10; tl.store(z_block_ptr, z, boundary_check=(0,1))&#10; # Finish me!&#10; return&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="puzzles-5-fused-outer-multiplication"&gt;Puzzles 5 Fused Outer Multiplication&#10;&lt;/h3&gt;&lt;p&gt;问题描述:&lt;/p&gt;&#10;&lt;p&gt;将行向量&lt;code&gt;x&lt;/code&gt;与列向量&lt;code&gt;y&lt;/code&gt;做外积，然后对结果矩阵的每个元素应用ReLU.&#10;&lt;/p&gt;&#10;$$&#10;z_{j,i} = \max(0,x_i\cdot y_j),\quad i = 1,\dots,N_0,j=1,\dots,N_1&#10;$$&lt;p&gt;问题解答：&lt;/p&gt;&#10;&lt;p&gt;这一问实际上是与Puzzle 3/4类似的，只有符号从加法变成了乘法。对于ReLU操作直接将z与0取max就行:&lt;code&gt;z = tl.maximum(0,z)&lt;/code&gt;&lt;/p&gt;&#10;&lt;h3 id="puzzles-6-fused-outer-multiplication---backwards"&gt;Puzzles 6 Fused Outer Multiplication - Backwards&#10;&lt;/h3&gt;&lt;p&gt;问题描述：&lt;/p&gt;&#10;&lt;p&gt;对如下函数操作进行反向传播:矩阵&lt;code&gt;x&lt;/code&gt;与向量&lt;code&gt;y&lt;/code&gt;按行相乘，再经过ReLU。&lt;/p&gt;&#10;$$&#10;f(x,y) = \text{relu}(x_{j,i}\times y_j),\quad i = 1 \dots N_0,j = 1 \dots N_1\\&#10;dx_{j,i} = f'_x(x,y)_{j,i}\times dz_{j,i} = 1(x_{j,i}\cdot y_j &gt;0)\cdot y_j \cdot dz_{j,i}&#10;$$&lt;p&gt;&#10;其中$1(\cdots)$表示指示函数，括号内条件满足时为1，否则为0&lt;/p&gt;&#10;&lt;p&gt;问题解答：&lt;/p&gt;&#10;&lt;p&gt;按照公式实现即可，没有较为复杂的逻辑，因此不贴代码了。&lt;/p&gt;&#10;&lt;h3 id="puzzles-7--long-sum"&gt;Puzzles 7 Long Sum&#10;&lt;/h3&gt;&lt;p&gt;问题描述:&lt;/p&gt;&#10;&lt;p&gt;给定一个二维张量&lt;code&gt;x&lt;/code&gt;，对其每一行进行求和。&lt;/p&gt;&#10;&lt;p&gt;问题解答:&lt;/p&gt;&#10;&lt;p&gt;这是一个二维的张量，我们在分块计算的时候虽然直观的想法是每行进行并行计算，但是考虑到每行有T个元素&#9;，而我们设计的块的大小是$B_0\times B_1$，而B1远小于B0,因此我们需要沿着第二维分块累加，这也是为什么题目提醒我们需要使用for循环。&lt;/p&gt;&#10;&lt;p&gt;当我们使用block pointer时，我们可以直接用&lt;code&gt;tl.advance(pointer,(stride_dim1,...))&lt;/code&gt;的方式移动指针。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def sum_kernel(x_ptr, z_ptr, N0, N1, T, B0: tl.constexpr, B1: tl.constexpr):&#10; # Finish me!&#10; tile_id = tl.program_id(0)&#10; x_block_ptr = tl.make_block_ptr(&#10; base=x_ptr,&#10; shape=(N0, T),&#10; strides=(T, 1),&#10; offsets=(tile_id * B0, 0),&#10; block_shape=(B0, B1),&#10; order=(1, 0)&#10; )&#10; z_block_ptr = tl.make_block_ptr(&#10; base=z_ptr,&#10; shape=(N0,),&#10; strides=(1,),&#10; offsets=(tile_id * B0,),&#10; block_shape=(B0,),&#10; order=(0,)&#10; )&#10; z = tl.zeros((B0,), dtype=tl.float32)&#10; for i in range(0, T, B1):&#10; x = tl.load(x_block_ptr, boundary_check=(0, 1), padding_option=&amp;#39;zero&amp;#39;)&#10; z += tl.sum(x,1)&#10; x_block_ptr = tl.advance(x_block_ptr,(0,B1))&#10; tl.store(z_block_ptr, z, boundary_check=(0,))&#10; return&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="puzzles-8-long-softmax"&gt;Puzzles 8 Long Softmax&#10;&lt;/h3&gt;&lt;p&gt;问题描述:&lt;/p&gt;&#10;&lt;p&gt;对一批logits做Softmax。需要保证数值稳定，即减去最大值后求Softmax&#10;&lt;/p&gt;&#10;$$&#10;z_{i,j} = \text{softmax}(x_{i,1},\dots,x_{i,T}) \quad i = 1,\dots,N_0&#10;$$&lt;p&gt;另外需要注意在Triton中建议不要直接用&lt;code&gt;exp&lt;/code&gt;而是用&lt;code&gt;exp2&lt;/code&gt;.$\text{exp}(x) = 2^{\log_2(e)x}$&lt;/p&gt;&#10;&lt;p&gt;问题解答:&lt;/p&gt;&#10;&lt;p&gt;我们先来做没有优化的三个for的版本。实际上我们可以将任务做一个拆分。因为我们是分行块处理的，但是tile的大小&lt;code&gt;[B0,B1]&lt;/code&gt;其中$B1 &lt; T$，因此我们需要for循环来得到:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;每行的max&lt;/li&gt;&#10;&lt;li&gt;每行的sum&lt;/li&gt;&#10;&lt;li&gt;每行的结果&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;因此需要3个for循环。因为后续我们将通过Online Softmax算法来实现更加高效实用的版本，因此这里就不贴出具体的代码了，思路和Puzzle 7类似。&lt;/p&gt;&#10;&lt;p&gt;Online Softmax的思路其实比较简单，就是第一个for和第二个for可以合并!&lt;/p&gt;&#10;&lt;p&gt;考虑两个tile，第一个tile的max我们记为$m_1$,那么此时局部求和的结果为:&#10;&lt;/p&gt;&#10;$$&#10;sum = \sum_{i = 0}^{B_0-1} \exp(x_i - m_1)&#10;$$&lt;p&gt;&#10;第二个tile的max记为$m_2(m_2 &gt; m_1)$,我们记$a = m_1 - m_2$,那么此时我们可以更新局部求和结果为:&#10;&lt;/p&gt;&#10;$$&#10;sum' = sum \times \exp(a) + \sum_{B_0}^{2B_0-1}\exp(x_i - m_2)&#10;$$&lt;p&gt;&#10;由此我们可以通过一个for循环，便得到了求最终结果需要的max以及sum。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def softmax_kernel(&#10; x_ptr, z_ptr, N0, N1, T, B0: tl.constexpr, B1: tl.constexpr&#10;):&#10; &amp;#34;&amp;#34;&amp;#34;3 loops ver.&amp;#34;&amp;#34;&amp;#34;&#10; block_id_i = tl.program_id(0)&#10; log2_e = 1.44269504&#10; x_block_ptr = tl.make_block_ptr(&#10; base=x_ptr,&#10; shape=(N0, T),&#10; strides=(T, 1),&#10; offsets=(block_id_i * B0, 0),&#10; block_shape=(B0, B1),&#10; order=(1, 0)&#10; )&#10; z_block_ptr = tl.make_block_ptr(&#10; base=z_ptr,&#10; shape=(N0, T),&#10; strides=(T, 1),&#10; offsets=(block_id_i * B0, 0),&#10; block_shape=(B0, B1),&#10; order=(1, 0)&#10; )&#10; z = tl.zeros((B0, B1), dtype=tl.float32)&#10; x_max = tl.full((B0,), float(&amp;#34;-inf&amp;#34;), dtype=tl.float32)&#10; sum = tl.zeros((B0,), dtype=tl.float32)&#10; x_block_ptr1 = x_block_ptr&#10; for i in range(0,T,B1):&#10; x = tl.load(x_block_ptr1, boundary_check=(0, 1), padding_option=&amp;#39;zero&amp;#39;)&#10; new_x_max = tl.maximum(x_max, tl.max(x,1))&#10; if i == 0:&#10; x_max = new_x_max&#10; sum = tl.sum(tl.exp2(log2_e * (x - x_max[:, None])),1)&#10; else:&#10; scale = tl.exp2(log2_e * (x_max - new_x_max))&#10; sum = sum * scale + tl.sum(tl.exp2(log2_e * (x - new_x_max[:, None])),1)&#10; x_max = new_x_max&#10; x_block_ptr1 = tl.advance(x_block_ptr1,(0,B1))&#10; for i in range(0,T,B1):&#10; x = tl.load(x_block_ptr, boundary_check=(0, 1), padding_option=&amp;#39;zero&amp;#39;)&#10; x = x - x_max[:, None]&#10; x_exp = tl.exp2(log2_e * x)&#10; z = x_exp / sum[:, None]&#10; tl.store(z_block_ptr, z, boundary_check=(0, 1))&#10; x_block_ptr = tl.advance(x_block_ptr,(0,B1))&#10; z_block_ptr = tl.advance(z_block_ptr,(0,B1))&#10; # Finish me!&#10; return&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="puzzles-9-simple-flashattention"&gt;Puzzles 9 Simple FlashAttention&#10;&lt;/h3&gt;&lt;p&gt;问题描述:&lt;/p&gt;&#10;&lt;p&gt;不使用 program。大小 B0 表示在总共 N0 个 q 中，本次要处理的一批。序列长度是 T。每次处理 B1 &amp;lt; T 个元素（k、v） ，其中 B1 是某个块大小。&#10;&lt;/p&gt;&#10;$$&#10;z_i = \sum_{j=1}^T \text{softmax}(q_ik_1,\dots,q_ik_T)_j v_j&#10;$$&lt;p&gt;&#10;这个问题可以通过类似于Online Softmax的思路来解决。&lt;/p&gt;&#10;&lt;p&gt;问题解答:(为便于理解，推荐读者用纸笔画一下)&lt;/p&gt;&#10;&lt;p&gt;考虑q的一个大小为&lt;code&gt;[B0=2]&lt;/code&gt;的tile中元素为&lt;code&gt;[q_0,q_1]&lt;/code&gt;,k和v的一个大小为&lt;code&gt;[B1=2]&lt;/code&gt;的tile中的元素为&lt;code&gt;[k_1,k_2]&lt;/code&gt;,&lt;code&gt;[v_1,v_2]&lt;/code&gt;，那么对应的注意力矩阵元素为一个大小为&lt;code&gt;[B0,B1]&lt;/code&gt;的二维矩阵，其中元素为&lt;code&gt;[s_11,s_12,s_21,s_22]&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;仅考虑结果中的第一个元素$z_1$,此时其累加和为:$z_1 = \text{softmax}(s_{11})v_1+\text{softmax}(s_{12})v_2 = \frac{\exp(s_{11}-m_1)}{l_1}v_1+\frac{\exp(s_12-m_1)}{l_1}v_2$,其中$m_1$为此时的局部最大值，$l_1$为此时的累计指数和.&lt;/p&gt;&#10;&lt;p&gt;计算完毕后，指针沿着axis=1的维度进行移动(步长为B1)，此时k和v的一个大小为&lt;code&gt;[B1=2]&lt;/code&gt;的tile中的元素为&lt;code&gt;[k_3,k_4]&lt;/code&gt;,&lt;code&gt;[v_3,v_4]&lt;/code&gt;，那么对应的注意力矩阵元素为一个大小为&lt;code&gt;[B0,B1]&lt;/code&gt;的二维矩阵，其中元素为&lt;code&gt;[s_13,s_14,s_23,s_24]&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;同样考虑结果中的第一个元素$z_1$,此时最大值更新为$m_2$,指数累计和应更新为$l_2 = l_1\times(\text{scale}=\exp(m_1-m_2))+\sum_{j=3}^4 \exp(s_{1j}-m_2)$.&lt;/p&gt;&#10;&lt;p&gt;那么我们分两部分考虑此时的结果$z_1$,一部分是新的注意力权重$s_{13},s_{14}$与$v_3,v_4$的加权和，这部分直接和之前一样相加就行，而另一部分则为之前计算出来的贡献，这部分由于m和l的更新，也要对应更新。&lt;/p&gt;&#10;&lt;p&gt;更新可以从分子和分母考虑。分子上和Online Softmax类似，需要乘以$\exp(m_1-m_2)$,而分母则需要进行替换为$l_2$,因此最终我们可以更新局部加权和$z_1$为:&#10;&lt;/p&gt;&#10;$$&#10;z_1' = z_1\times scale\times \frac{l_1}{l_2} + \sum_{j=3}^4\frac{\exp(s_{1j}-m_2)}{l_2}v_j&#10;$$&lt;p&gt;&#10;由此不断迭代，便能得到最终结果。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def flashatt_kernel(&#10; q_ptr, k_ptr, v_ptr, z_ptr, N0, T, B0: tl.constexpr, B1: tl.constexpr&#10;):&#10; block_id_i = tl.program_id(0)&#10; log2_e = 1.44269504&#10; myexp = lambda x: tl.exp2(log2_e * x)&#10; q_block_ptr = tl.make_block_ptr(&#10; base=q_ptr,&#10; shape=(N0,),&#10; strides=(1,),&#10; offsets=(block_id_i * B0,),&#10; block_shape=(B0,),&#10; order=(0,)&#10; )&#10; k_block_ptr = tl.make_block_ptr(&#10; base=k_ptr,&#10; shape=(T,),&#10; strides=(1,),&#10; offsets=(0,),&#10; block_shape=(B1,),&#10; order=(0,)&#10; )&#10; v_block_ptr = tl.make_block_ptr(&#10; base=v_ptr,&#10; shape=(T,),&#10; strides=(1,),&#10; offsets=(0,),&#10; block_shape=(B1,),&#10; order=(0,)&#10; )&#10; z_block_ptr = tl.make_block_ptr(&#10; base=z_ptr,&#10; shape=(N0,),&#10; strides=(1,),&#10; offsets=(block_id_i * B0,),&#10; block_shape=(B0,),&#10; order=(0,)&#10; )&#10;&#10; qk_max = tl.full((B0,), float(&amp;#34;-inf&amp;#34;), dtype=tl.float32)&#10; qk_sum = tl.zeros((B0,), dtype=tl.float32)&#10; o = tl.zeros((B0,), dtype=tl.float32)&#10; for i in range(0, T, B1):&#10; q = tl.load(q_block_ptr, boundary_check=(0,), padding_option=&amp;#39;zero&amp;#39;)&#10; k = tl.load(k_block_ptr, boundary_check=(0,), padding_option=&amp;#39;zero&amp;#39;)&#10; v = tl.load(v_block_ptr, boundary_check=(0,), padding_option=&amp;#39;zero&amp;#39;)&#10; qk = q[:, None] * k[None, :]&#10; new_qk_max = tl.maximum(qk_max,tl.max(qk, 1))&#10; if i == 0:&#10; qk_max = new_qk_max&#10; qk_exp = myexp(qk - qk_max[:,None])&#10; qk_sum = tl.sum(qk_exp, 1)&#10; o = tl.sum(qk_exp * v[None, :] / qk_sum[:,None], 1)&#10; else:&#10; qk_exp = myexp(qk - new_qk_max[:,None])&#10; scale = myexp(qk_max - new_qk_max)&#10; scale1 = scale * qk_sum&#10; qk_sum = qk_sum * scale + tl.sum(qk_exp, 1)&#10; o = o * scale1 / qk_sum + tl.sum(qk_exp * v[None, :] / qk_sum[:,None], 1)&#10; qk_max = new_qk_max&#10; k_block_ptr = tl.advance(k_block_ptr,(B1,))&#10; v_block_ptr = tl.advance(v_block_ptr,(B1,))&#10; tl.store(z_block_ptr, o, boundary_check=(0,))&#10; # Finish me!&#10; return&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="puzzles-10-two-dimensional-convolution"&gt;Puzzles 10 Two Dimensional Convolution&#10;&lt;/h3&gt;&lt;p&gt;问题描述:&lt;/p&gt;&#10;&lt;p&gt;实现一个带批处理的二维卷积。使用一个program id 轴。块大小为&lt;code&gt;B0&lt;/code&gt;表示在&lt;code&gt;N0&lt;/code&gt;中一次处理多少个batch。图像&lt;code&gt;x&lt;/code&gt;的大小为&lt;code&gt;H x W&lt;/code&gt;，并且只有一个通道；卷积核&lt;code&gt;k&lt;/code&gt;的大小为&lt;code&gt;kH x kW&lt;/code&gt;&#10;&lt;/p&gt;&#10;$$&#10;z_{i, j, l} = \sum_{oj, ol}^{j+oj\le H, l+ol\le W} k_{oj,ol} \times x_{i,j + oj, l + ol} &#10; \text{ for } i = 1\ldots N_0 \text{ for } j = 1\ldots H \text{ for } l = 1\ldots W&#10;$$&lt;p&gt;问题解决：(为了方便理解，最好对照题面的图来理解)&lt;/p&gt;&#10;&lt;p&gt;我们考虑如何分块，题目中要求的是按照批维度进行分块拆分并行。那么块的第一维就是B0,注意到卷积运算每次只会用到图像矩阵中KW*KH大小的子矩阵，因此块最合适的大小就是&lt;code&gt;[B0,KH,KW]&lt;/code&gt;。这个块与卷积核进行卷积运算，得到一个标量，因此结果的块的大小就是&lt;code&gt;[B0,1,1]&lt;/code&gt;，那么这样就能写出block pointer的定义。&lt;/p&gt;&#10;&lt;p&gt;接下来模拟二维卷积运算就行了，根据相关知识，我们知道卷积运算就是卷积核在图像矩阵上进行平移(这里默认步长为1).那么我们用两个for循环来模拟这个平移计算的流程就行，然后在内层for进行一次后沿着dim=1平移一格，在外层for进行一次后沿dim=0平移一格。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def conv2d_kernel(&#10; x_ptr, k_ptr, z_ptr, N0, H, W, KH: tl.constexpr, KW: tl.constexpr, B0: tl.constexpr&#10;):&#10; block_id_i = tl.program_id(0)&#10; k_block_ptr = tl.make_block_ptr(&#10; base=k_ptr,&#10; shape=(KH, KW),&#10; strides=(KW, 1),&#10; offsets=(0, 0),&#10; block_shape=(KH, KW),&#10; order=(1, 0),&#10; )&#10; k = tl.load(k_block_ptr, boundary_check=(0, 1), padding_option=&amp;#34;zero&amp;#34;)&#10;&#10; x_row_block_ptr = tl.make_block_ptr(&#10; base=x_ptr,&#10; shape=(N0, H, W),&#10; strides=(H * W, W, 1),&#10; offsets=(block_id_i * B0, 0, 0),&#10; block_shape=(B0, KH, KW),&#10; order=(2, 1, 0),&#10; )&#10; z_row_block_ptr = tl.make_block_ptr(&#10; base=z_ptr,&#10; shape=(N0, H, W),&#10; strides=(H * W, W, 1),&#10; offsets=(block_id_i * B0, 0, 0),&#10; block_shape=(B0, 1, 1),&#10; order=(2, 1, 0),&#10; )&#10;&#10; for _ in range(H):&#10; x_block_ptr = x_row_block_ptr&#10; z_block_ptr = z_row_block_ptr&#10; for _ in range(W):&#10; x = tl.load(x_block_ptr, boundary_check=(0, 1, 2), padding_option=&amp;#34;zero&amp;#34;)&#10; z = (x * k[None, :, :]).sum(1).sum(1)&#10; tl.store(z_block_ptr, z[:, None, None], boundary_check=(0, 1, 2))&#10; x_block_ptr = tl.advance(x_block_ptr, (0, 0, 1))&#10; z_block_ptr = tl.advance(z_block_ptr, (0, 0, 1))&#10; x_row_block_ptr = tl.advance(x_row_block_ptr, (0, 1, 0))&#10; z_row_block_ptr = tl.advance(z_row_block_ptr, (0, 1, 0))&#10;&#10; return&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="puzzles-11-matrix-multiplication"&gt;Puzzles 11 Matrix Multiplication&#10;&lt;/h3&gt;&lt;p&gt;问题描述：&lt;/p&gt;&#10;&lt;p&gt;使用三条program id轴。块大小&lt;code&gt;B2&lt;/code&gt;表示在&lt;code&gt;N2&lt;/code&gt;中要处理的Batch数量。块大小&lt;code&gt;B0&lt;/code&gt;表示在&lt;code&gt;N0&lt;/code&gt;中要处理的行数，块大小&lt;code&gt;B1&lt;/code&gt;表示在&lt;code&gt;N1&lt;/code&gt;中要处理的列数。&lt;/p&gt;&#10;&lt;p&gt;中间维度的大小为&lt;code&gt;MID&lt;/code&gt;&#10;&lt;/p&gt;&#10;$$&#10;z_{i, j, k} = \sum_{l} x_{i,j, l} \times y_{i, l, k}\quad &#10;\text{for } i = 1\ldots N_2,\ j = 1\ldots N_0,\ k = 1\ldots N_1&#10;$$&lt;p&gt;Hint:可以使用&lt;code&gt;tl.dot&lt;/code&gt;，它可以计算一个更小规模的矩阵乘法&lt;/p&gt;&#10;&lt;p&gt;问题解答:&lt;/p&gt;&#10;&lt;p&gt;我们知道两个矩阵的形状分别为&lt;code&gt;shape(x) = [N2,N0,MID],shape(y) = [N2,MID,N1]&lt;/code&gt;.我们分块的话，显然需要在MID维度上进行拆分，那么两个矩阵对应的块大小为:&lt;code&gt;[B2,B0,B_MID],[B2,B_MID,B1]&lt;/code&gt;,然后将这些小矩阵的乘积进行累加，便能得到最终的结果。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def dot_kernel(&#10; x_ptr,&#10; y_ptr,&#10; z_ptr,&#10; N0,&#10; N1,&#10; N2,&#10; MID,&#10; B0: tl.constexpr,&#10; B1: tl.constexpr,&#10; B2: tl.constexpr,&#10; B_MID: tl.constexpr,&#10;):&#10; block_id_j = tl.program_id(0)&#10; block_id_k = tl.program_id(1)&#10; block_id_i = tl.program_id(2)&#10; # Finish me!&#10; x_block_ptr = tl.make_block_ptr(&#10; base=x_ptr,&#10; shape=(N2,N0,MID),&#10; strides=(N0 * MID,MID,1),&#10; offsets=(block_id_i * B2, block_id_j * B0, 0),&#10; block_shape=(B2, B0, B_MID),&#10; order=(0, 1, 2),&#10; )&#10; y_block_ptr = tl.make_block_ptr(&#10; base=y_ptr,&#10; shape=(N2,MID,N1),&#10; strides=(MID * N1,N1,1),&#10; offsets=(block_id_i * B2, 0, block_id_k * B1),&#10; block_shape=(B2, B_MID, B1),&#10; order=(0, 1, 2),&#10; )&#10; z_block_ptr = tl.make_block_ptr(&#10; base=z_ptr,&#10; shape=(N2,N0,N1),&#10; strides=(N0 * N1,N1,1),&#10; offsets=(block_id_i * B2, block_id_j * B0, block_id_k * B1),&#10; block_shape=(B2, B0, B1),&#10; order=(0, 1, 2),&#10; )&#10; z = tl.zeros((B2, B0, B1), dtype=tl.float32)&#10; for i in range(0, MID, B_MID):&#10; x = tl.load(x_block_ptr, boundary_check=(0,1,2),padding_option=&amp;#39;zero&amp;#39;)&#10; y = tl.load(y_block_ptr, boundary_check=(0,1,2),padding_option=&amp;#39;zero&amp;#39;)&#10; z += tl.dot(x,y)&#10; x_block_ptr = tl.advance(x_block_ptr,(0,0,B_MID))&#10; y_block_ptr = tl.advance(y_block_ptr,(0,B_MID,0))&#10; tl.store(z_block_ptr, z, boundary_check=(0,1,2))&#10; return&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="puzzles-12-quantized-matrix-mult"&gt;Puzzles 12 Quantized Matrix Mult&#10;&lt;/h3&gt;&lt;p&gt;问题描述:&lt;/p&gt;&#10;&lt;p&gt;我们将对一个矩阵乘法进行量化压缩，具体而言是将权重矩阵以更低精度存储，并额外配合一个偏移项和缩放项。&lt;/p&gt;&#10;&lt;p&gt;在本问题中，我们的&lt;code&gt;weight&lt;/code&gt;将以4bit存储。一个32位整数中可以存放&lt;code&gt;FPINT&lt;/code&gt;个这样的值。此外，对每个连续&lt;code&gt;group&lt;/code&gt;个权重，我们还会额外存储:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;1个&lt;code&gt;scale&lt;/code&gt;浮点值&lt;/li&gt;&#10;&lt;li&gt;1个&lt;code&gt;shift&lt;/code&gt;的4bit值&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这些&lt;code&gt;scale&lt;/code&gt;和&lt;code&gt;shift&lt;/code&gt;是按&lt;code&gt;weight&lt;/code&gt;的列来存储的。而&lt;code&gt;activation&lt;/code&gt;则单独以普通浮点数格式存储。&#10;&lt;/p&gt;&#10;$$&#10;z_{j, k} = \sum_{l} sc_{j, \frac{l}{g}} (w_{j, l} - sh_{j, \frac{l}{g}}) \times y_{l, k}&#10;\quad \text{for } j = 1\ldots N_0,\ k = 1\ldots N_1&#10;$$&lt;p&gt;&#10;其中&lt;code&gt;g&lt;/code&gt;表示分组大小。&lt;/p&gt;&#10;&lt;p&gt;问题解答:&lt;/p&gt;&#10;&lt;p&gt;本题的主要难点，不在于计算，而是张量的形状处理。我们先来梳理以下完整的操作流程。&lt;/p&gt;&#10;&lt;p&gt;首先我们还是按照正常的矩阵乘法在MID维度上进行拆分，然后由于权重是以&lt;code&gt;INT8&lt;/code&gt;格式存储的，我们还需要将其提取出来(利用位运算即可)。然后每个GROUP我们都会存储一个浮点型的&lt;code&gt;scale&lt;/code&gt;和&lt;code&gt;INT4&lt;/code&gt;类型的&lt;code&gt;shift&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;为了方便后续理解，我们看看各个分块的大小(仅看输入):&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;激活值,这个最好理解，它是以浮点型存储的，然后因为是在MID维度做了拆分，因此块大小为&lt;code&gt;[B_MID,B_1]&lt;/code&gt;,需要注意访问的顺序是列优先的，因此&lt;code&gt;order = (0,1)&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;权重矩阵，这个需要考虑到&lt;code&gt;INT32&lt;/code&gt;存储&lt;code&gt;INT8&lt;/code&gt;带来的形状差异，我们记一个&lt;code&gt;INT32&lt;/code&gt;可以存储&lt;code&gt;FPINT&lt;/code&gt;个&lt;code&gt;INT4&lt;/code&gt;,那么权重矩阵实际的大小为&lt;code&gt;[N0,MID//FPINT]&lt;/code&gt;,那么对应的块大小就是&lt;code&gt;[B0,B_MID//FPINT]&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;scale,它是浮点型存储的，并且是一个GROUP(在MID维度分组)一个,那么其形状就是&lt;code&gt;[N0,GROUP]&lt;/code&gt;,对应块大小为&lt;code&gt;[B0,GROUP]&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;offset,它与Scale类似，是一个GROUP一个，但是它是以&lt;code&gt;INT4&lt;/code&gt;存储的，因此也需要考虑&lt;code&gt;FPINT&lt;/code&gt;的事情，它的实际大小就是:&lt;code&gt;[B0,GROUP//FPINT]&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;分完块之后就需要把用&lt;code&gt;int4&lt;/code&gt;的块提取为&lt;code&gt;int32&lt;/code&gt;格式的，方便后续计算。&lt;/p&gt;&#10;&lt;p&gt;然后需要考虑形状的问题了，我们提取出来的&lt;code&gt;INT32&lt;/code&gt;格式的Weight的张量形状为&lt;code&gt;[B0,B_MID // FPINT,FPINT]&lt;/code&gt;,但是我们的&lt;code&gt;scale&lt;/code&gt;的形状为&lt;code&gt;[B0,GROUP]&lt;/code&gt;，我们无法将其直接与Weight进行运算得到dequant的权重，因此我们需要将其reshape为&lt;code&gt;[B0,GROUP,B_MID//GROUP]&lt;/code&gt;.&lt;code&gt;offsets&lt;/code&gt;也同理，需要从&lt;code&gt;[B0, GROUP // FPINT, FPINT]&lt;/code&gt;处理为&lt;code&gt;[B0,GROUP]&lt;/code&gt;。之后借助广播进行dequant后，再进行矩阵乘法。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;@triton.jit&#10;def quant_dot_kernel(&#10; scale_ptr,&#10; offset_ptr,&#10; weight_ptr,&#10; activation_ptr,&#10; z_ptr,&#10; N0,&#10; N1,&#10; MID,&#10; B0: tl.constexpr,&#10; B1: tl.constexpr,&#10; B_MID: tl.constexpr,&#10;):&#10; block_id_j = tl.program_id(0)&#10; block_id_k = tl.program_id(1)&#10;&#10; def extract(x):&#10; over = tl.arange(0,8) * 4&#10; mask = 2**4 - 1&#10; return (x[:,:, None] &amp;gt;&amp;gt; over) &amp;amp; mask&#10;&#10; activation_block_ptr = tl.make_block_ptr(&#10; base = activation_ptr,&#10; shape = (MID,N1),&#10; strides = (N1,1),&#10; offsets = (0, block_id_k * B1),&#10; block_shape = (B_MID, B1),&#10; order = (0,1)&#10; )&#10; weight_block_ptr = tl.make_block_ptr(&#10; base = weight_ptr,&#10; shape = (N0,MID // FPINT),&#10; strides = (MID // FPINT,1),&#10; offsets = (block_id_j * B0, 0),&#10; block_shape = (B0, B_MID // FPINT),&#10; order = (1,0)&#10; )&#10; scale_block_ptr = tl.make_block_ptr(&#10; base = scale_ptr,&#10; shape = (N0, GROUP),&#10; strides = (GROUP,1),&#10; offsets = (block_id_j * B0, 0),&#10; block_shape = (B0, GROUP),&#10; order = (1,0)&#10; )&#10; offset_block_ptr = tl.make_block_ptr(&#10; base = offset_ptr,&#10; shape = (N0,GROUP // FPINT),&#10; strides = (GROUP // FPINT,1),&#10; offsets = (block_id_j * B0, 0),&#10; block_shape = (B0, GROUP // FPINT),&#10; order = (1,0)&#10; )&#10; z_block_ptr = tl.make_block_ptr(&#10; base = z_ptr,&#10; shape = (N0,N1),&#10; strides = (N1,1),&#10; offsets = (block_id_j * B0, block_id_k * B1),&#10; block_shape = (B0, B1),&#10; order = (1,0)&#10; )&#10; z = tl.zeros((B0, B1), dtype=tl.float32)&#10; for i in range(0, MID, B_MID):&#10; scale_fp32 = tl.load(scale_block_ptr, boundary_check=(0,1), padding_option=&amp;#39;zero&amp;#39;)#[B0,GROUP]&#10; &#10; offset_int32 = extract(tl.load(offset_block_ptr, boundary_check=(0,1), padding_option=&amp;#39;zero&amp;#39;))#[B0, GROUP // FPINT, FPINT]&#10; offset_int32 = offset_int32.reshape(B0, GROUP)#[B0, GROUP]&#10; &#10; weight_int32 = extract(tl.load(weight_block_ptr, boundary_check=(0,1), padding_option=&amp;#39;zero&amp;#39;))#[B0, B_MID // FPINT, FPINT]&#10; weight_int32 = weight_int32.reshape(B0, B_MID)#[B0, B_MID]&#10; weight_int32 = weight_int32.reshape(B0,GROUP,B_MID // GROUP)#[B0, GROUP, B_MID // GROUP]&#10;&#10; activation_fp32 = tl.load(activation_block_ptr, boundary_check=(0,1), padding_option=&amp;#39;zero&amp;#39;)&#10; &#10; weight_fp32 = scale_fp32[:,:,None] * (weight_int32 - offset_int32[:,:,None])&#10; weight_fp32 = weight_fp32.reshape(B0,B_MID)&#10;&#10; z += tl.dot(weight_fp32, activation_fp32)&#10; scale_block_ptr = tl.advance(scale_block_ptr,(0, GROUP))&#10; offset_block_ptr = tl.advance(offset_block_ptr,(0, GROUP // FPINT))&#10; weight_block_ptr = tl.advance(weight_block_ptr,(0, B_MID // FPINT))&#10; activation_block_ptr = tl.advance(activation_block_ptr,(B_MID, 0))&#10; tl.store(z_block_ptr, z, boundary_check=(0,1))&#10;&#10; # Finish me!&#10; return&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="reference"&gt;Reference&#10;&lt;/h2&gt;&lt;ol&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://zhuanlan.zhihu.com/p/20539246076" target="_blank" rel="noopener"&#10; &gt;namoe的解答&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://zhuanlan.zhihu.com/p/5964285807" target="_blank" rel="noopener"&#10; &gt;Lite作者的解答&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://www.bilibili.com/video/BV193fFYkE7P/" target="_blank" rel="noopener"&#10; &gt;先进编译实验室的讲解&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;CS336 Assignment2-systems的实验指导手册&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://courses.cs.washington.edu/courses/cse599m/23sp/notes/flashattn.pdf" target="_blank" rel="noopener"&#10; &gt;CSE559M Note&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item></channel></rss>