ALPHA教程正在持续完善,部分内容仍待补充。反馈问题 ↗
Skip to content

第8章 Element-Wise:逐元素算子 ​

本章导读 ​

在第 1 篇中,我们以向量加法为例跑通了基准测试与性能剖析的全流程。进入算子实战的第一站,我们依然聚焦于看似最简单的加法运算 C[i] = A[i] + B[i],但这次的核心命题转变为:当我们手里有了一个能跑通的正确基线后,如何系统性地判断下一步值得改什么?改动之后又如何科学地决定该保留还是回退?

本章我们将分别沿着 HIP 与 Triton 两条路线展开优化探索。你将学习到如何通过受控实验逐一检验“相邻访存连续性”、“单线程循环工作量”与“向量化加载(float4)”对性能的真实影响;当遇到没有加速甚至反常变慢的尝试时,学会坦然记录并作出理性的工程决策。第 4–7 章建立的计时基准、Trace 分析与 Roofline 模型是本章的基石;文中的具体语法可按需随时查阅附录。

你可以先阅读两条路线共通的数学定义与数据流动画,再根据技术背景选择对应的实现入口:

阅读路线与前置知识
阅读路线推荐起点
第一次接触 GPU 算子编程建议先阅读附录 D:编程范式,建立基本图景后回到 8.1 节
倾向底层控制的 HIP 路线阅读 8.1–8.3 节公共基础,在实现节选择 HIP 标签,最后阅读跨路线总结
倾向现代分块抽象的 Triton 路线阅读 8.1–8.3 节公共基础,在实现节选择 Triton 标签,最后阅读跨路线总结

8.1 逐元素算子的数据依赖 ​

8.1.1 输出之间的独立性 ​

先看这个算子的数据依赖:一个输出需要哪些输入?计算它时,是否必须等待其他输出?

向量加法(Vector Add)的数学定义是:

Ci=Ai+Bi,0≤i<N

计算 C[3] 时,只需读取 A[3] 与 B[3] 并相加,不需要等待 C[0]、C[1] 或其他位置的结果。

因此,不同输出之间互不依赖。每个位置的计算都可以看作一项独立任务。

8.1 语义层 · Element-Wise位置 i 独立计算 C[i] = A[i] + B[i]
下标 iABCi = 0279相加1-13224-133325406655-236-2427189只处理自己的位置下标 0 的计算过程如下:C[0] = A[0] + B[0] = 2 + 7 = 9关键点只读取同一位置的两个输入。其他位置的计算不影响结果。逐元素算子可以大规模并行。在位置 i = 0:C[0] = A[0] + B[0] = 2 + 7 = 9其他位置的计算,不影响这个结果。下标 iABCi = 0279相加1-13224-133325406655-236-2427189只处理自己的位置下标 0 的计算过程如下:C[0] = A[0] + B[0] = 2 + 7 = 9关键点只读取同一位置的两个输入。其他位置的计算不影响结果。逐元素算子可以大规模并行。在位置 i = 0:C[0] = A[0] + B[0] = 2 + 7 = 9其他位置的计算,不影响这个结果。

位置 0:只读自己的两个输入位置 0 只读取 A[0]=2 与 B[0]=7,得到 C[0]=9,其他位置不受影响。

算法过程示意 · 数字定位到该步完成状态,可拖动进度或播放;动画速度不代表 GPU 耗时。

图 8.1 Vector Add 的逐元素依赖:当前位置只读取对应的两个输入位置。

图 8.1 每一步只高亮同一列的 A[i]、B[i] 与 C[i],用来说明一个输出依赖哪些输入。实际程序可以把不同位置分给多个线程并行处理,也可以让一个线程处理多个位置。计算任务最终由 GPU 的计算单元(CU)执行,任务数量不等于 CU 数量;动画的逐列展示也不代表真实执行顺序。

现代深度学习中大量的核心算子都属于这种逐元素算子(Element-Wise Operators):

算子单个输出的表达式Y[i] 依赖什么
Add(加法)Y[i] = A[i] + B[i]仅依赖同索引的 A[i] 与 B[i]
ReLU(激活)Y[i] = max(X[i], 0)仅依赖同索引的 X[i]
Scale & Bias(缩放平移)Y[i] = alpha * X[i] + beta仅依赖 X[i] 与全局常量标量
Clamp(截断)Y[i] = min(max(X[i], low), high)仅依赖 X[i] 与常量上下界

这些算子的共同点是:计算一个输出不需要其他输出的结果。程序可以通过索引公式与启动配置安排数据分工,再由硬件调度执行。同样的数学公式可以采用不同分工,后面的实验会比较这些选择对时间的影响。

8.1.2 HIP 与 Triton 的实现视角 ​

虽然数学语义相同,但 HIP 与 Triton 采用了两种截然不同的抽象视角来把这项任务分工给硬件:

  • HIP 采用“线程主导”视角(Thread-centric):代码描述的是单个线程的行为——“我是第几号线程,我该计算哪一个下标 i,我把对应元素读进来、相加、并写回”;
  • Triton 采用“分块主导”视角(Block/Tile-centric):代码描述的是一组连续数据的集合行为——“我是一个计算实例(Program),我圈定一段逻辑索引范围,整体加载这组数据张量、执行向量加法并整体写回”。

如果你希望进一步厘清 Thread 与 Program、Block 与 Tile 的概念映射,请随时参考附录 D:HIP 与 Triton 的编程范式。

8.1.3 与归约算子的区别 ​

为了更深刻地理解逐元素算子的特性,我们可以对比另一种截然相反的数据流动模式——归约(Reduction)。

求和归约的单个输出需要汇总全部输入(Y=∑Xi);Softmax 的每个输出也需要用到整行的指数和。并行实现时,我们需要安排多个线程怎样合并这些数据。这是第 9 章 Reduction与第 10 章 Normalization要解决的问题。

因此请记住:“输入输出形状一致”并不等于逐元素算子。逐元素算子的本质边界在于:任意输出位置 i 仅由对应输入位置 i 的数值决定,不发生任何跨维度的跨行跨列跨位置聚合。

8.2 固定数学语义与正确性标准 ​

8.2.1 四元素示例 ​

我们用一个仅包含 4 个元素的小数组作为微型示范:

text
A = [2, -1, 4, 3]
B = [7,  3, -1, 2]

按照逐位置相加的规则:

text
C[0] = 2  + 7  = 9
C[1] = -1 + 3  = 2
C[2] = 4  + -1 = 3
C[3] = 3  + 2  = 5

C = [9, 2, 3, 5]

这个微型算例涵盖了该算子的全部数学逻辑。当我们将数组规模扩大到几千万个元素时,底层数学运算并未发生任何变化,改变的仅仅是我们如何在物理芯片上组织并发访存。

8.2.2 参考实现与检查条件 ​

在优化过程中,“快但算错”没有任何意义。本章配套测试体系为 HIP 与 Triton 制定了完全统一的正确性红线:

项目固定方式
数学语义C[i] = A[i] + B[i]
数据类型32 位浮点数(FP32)
输入用同一确定性整数公式生成,再转成 FP32
参考结果CPU 逐元素执行同一个 FP32 加法
边界覆盖小于 wavefront、刚好等于 block、比 block 多 1、不能被 4 整除等长度
正确性正式计时前检查一次,计时后再检查一次
计时范围GPU event 包围一次 kernel 提交,不含分配、输入生成与 Host-to-Device 拷贝

为什么要测试非整除长度?例如,把 N=1027 个元素按每块 256 个元素划分,最后一块只剩 3 个有效元素。若采用“一线程处理一个元素、每 block 256 个线程”的初版 HIP 映射,最后一个 block 中就只有前 3 个线程需要读写数据,其余线程应跳过读写。

这里的线程数量取决于映射方式。后面的 grid-stride 循环与四元素分组需要按各自的索引检查尾部;Triton 的 mask 则标记逻辑元素位置,3 个有效位置不能直接理解成 3 个线程。无论怎样分工,都需要阻止越界读写。

Triton 测试脚本用 PyTorch 在 CPU 上计算参考结果,host_a 与 host_b 是两组输入:

python
reference = host_a + host_b

两条路线都逐个核对输出,要求结果有限、绝对误差不超过 1e-6。通过这些检查后,才比较运行时间。

8.2.3 非整除长度的边界 ​

处理尾部边界时,两种路线展现了各自的表达习惯:

  • HIP 初版用 if (index < size) 保护单个元素的读写;使用循环的版本,则在每次迭代时检查下标;
  • Triton 用张量掩码(Mask)mask = offsets < size 标记有效位置,并将它传给加载和写回操作。

无论采用哪种写法,都必须同时覆盖读操作与写操作的边界防护。

8.3 建立成本模型和瓶颈假设 ​

8.3.1 逻辑计算量与数据量 ​

针对 FP32 单精度下的单元素向量加法,我们来数一数最理想情况下的逻辑开销:

动作数量逻辑字节
读取 A[i]1 个 FP324 Byte
读取 B[i]1 个 FP324 Byte
执行加法1 次浮点加法1 FLOP
写回 C[i]1 个 FP324 Byte
合计12 Byte + 1 FLOP

由此推导出其算法理论算术强度:

AIlogical=1 FLOP12 Byte≈0.0833 FLOP/Byte

必须明确:这是由算法逻辑读写量推导得出的逻辑算术强度,而不是硬件总线上测得的物理传输比例。在真实芯片中,由于片上缓存复用、显存事务打包或非对齐造成的额外搬运,各存储层级实际承受的数据流量都会与算法逻辑字节数存在差异。

基于这一逻辑算术强度,我们可以确立本章优化的首要指导假设:

向量加法每读写 12 字节逻辑数据仅执行 1 次浮点计算,逻辑算术强度极低;在大规模数据输入下,算子更容易受限于显存数据供给,而非浮点计算能力。

我们在第 7 章已经系统学习了 Roofline 的分析原则。这一假设提示我们:优化工作应优先从内存访存排布、合并事务与总线利用率入手排查,同时注意控制核函数启动、线程分工与循环展开等结构性开销。

8.3.2 有效带宽怎样算 ​

本章所有版本的性能横向对比,均统一按照如下标准折算算法有效带宽:

BWeffective=3×N×4 Bytet

其中 t 是包围一次 kernel 提交的 GPU event 时间。这个指标适合在相同语义、相同 shape、相同计时范围下比较版本,但它不等于内存控制器实际传输了多少字节。物理流量必须由可用的硬件计数器或更进一步的分析支持。

8.4 实现向量加法 ​

一个有用的实验从问题开始。例如,相邻线程的地址分散后会怎样;让更少的 block 各自做更多工作是否划算;让一个 Triton program 处理更多元素是否值得。假设可以来自前面的数据依赖和成本模型,再由测量检验。

先区分三件事,避免把“测到了一个数字”误当作解释:

方法回答的问题何时执行
正确性检查输出仍符合加法语义吗?尾部有没有漏写或越界?每次改动后,先检查再计时
Benchmark(性能测量)相同条件下更快吗?收益超过运行波动了吗?每轮改动都做,与相关对照一起比较
Profiling(性能分析)哪项额外证据能帮助解释现象或排除一种猜测?有具体问题且工具能回答时再做

如果已经决定撤回一项没有稳定收益的改动,就可以结束这一轮。只有还想回答“为何没有收益”或核对某项执行细节时,才需要进一步分析。下面两条路线各保留自然基线;编号用于区分实验,不代表逐级提速。

准备环境。 在 GPU 机器上,从仓库根目录进入本篇;自带的 uv.lock 用于锁定依赖,不需要删除:

bash
cd code/part2-kernels &&
uv sync --locked &&
source ./activate-rocm.sh

首次使用先按环境准备完成驱动、uv 和系统编译依赖。后文命令均在 code/part2-kernels/ 执行。

新建一轮实验。 在当前目录执行以下命令,保存打印出来的路径:

bash
mkdir -p chapter8/results &&
RUN=$(mktemp -d chapter8/results/walkthrough-XXXXXX) &&
mkdir "$RUN/build" &&
printf '本轮结果目录:%s\n' "$RUN"

每次新建都会得到不同目录,保留旧结果。这里的 RUN 只在当前终端有效。若换了终端,从仓库根目录重新执行环境准备,然后输入之前保存的目录:

bash
read -r -p '粘贴上次输出的结果目录:' RUN &&
test -n "$RUN" && test -d "$RUN/build" && printf '继续使用结果目录:%s\n' "$RUN"

在提示处粘贴目录路径,不包含 RUN=,再按回车。只有看到确认信息才继续;目录不存在时先核对路径。继续原实验时不重建目录、不重复已有测量;需要重测或修改源码时,新建一轮并重新编译 HIP 程序。

命令开头会检查 RUN 是否已设置、目录是否存在,再进行写入。每条 benchmark 命令保存一个进程的 50 个样本。test ! -e ... && ... 防止追加到旧 CSV;文件已存在时程序不会运行。图中使用三个独立进程的结果,不能仅凭这里的一次运行挑选赢家。手工继续测第 2、3 个进程时,要同时将命令中的 benchmark-p1.samples.csv 和 --process 1 改为对应的 p2 / 2、p3 / 3;每一轮都依次运行本组全部候选,并轮换起始配置,保留所有结果。完成单项操作后,可用 8.8 的批量入口自动保存三进程比较与图。

固定比较条件。 本章数据来自 RX 9070 XT(gfx1201)、ROCm SDK 10.0.0、原生 Ubuntu 24.04;HIP 组件为 7.15.26333,Triton 为 3.8.0。

项目所有逐轮图采用的条件
输入N=16,777,216,FP32,确定性输入,seed=20260920
正确性输出先填 NaN;计时前后检查所有结果有限、绝对误差不超过 1e-6
计时预热 10 次、测量 50 次;分配、输入生成、拷贝、校验及 JIT 首次编译在计时之外
重复与图示每配置 3 个独立进程;柱长为三个进程 median 的中位数,误差线为这三个 median 的最小—最大范围,不是置信区间
执行条件轮换配置顺序;重复使用数组、不主动清缓存;保留桌面及已有后台上下文,不假定 GPU 独占

以下边界输出来自同一份 kernel 源码的独立正确性检查;它们与主输入的三进程计时分开保存。通过这些已选长度不能证明所有输入均正确,修改数学表达式或访问规则后仍需重新检查。

先看输出中的 precheck、postcheck、correct 是否全为 OK,再核对输入与配置,最后看 median_ms。effective_bandwidth_gbs 由逻辑字节数换算,不是硬件流量测量。计时口径沿用第 5 章,工具基础见第 6 章。

切换会保留各自的阅读位置

8.4.1 HIP 基线:一个线程处理一个元素 ​

本章的起点是自然的一线程一元素写法。相邻线程访问相邻元素,数组加法又没有跨元素依赖,所以不需要先加入 LDS 或线程间同步。

cpp
__global__ void vector_add_v0(const float* __restrict__ input_a,
                              const float* __restrict__ input_b,
                              float* __restrict__ output,
                              std::size_t size) {
    const std::size_t index =
        static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
    if (index < size) {
        output[index] = input_a[index] + input_b[index];
    }
}

N=16,777,216、每 block 256 个线程时,共需 65,536 个 block。最后一个 block 仍保留越界判断,以便同一份代码处理非整除长度。完整程序先准备输入与输出,对照 CPU 参考结果,再预热和计时。先看 vector_add_v0(),再看负责提交的 launch() 与组织测量的 run_version()。

完整源码:code/part2-kernels/chapter8/vector_add_hip.hip
cpp
// Chapter 8: Element-Wise Vector Add, HIP route.
//
// Performance results from this program are valid for the book only after it
// runs on the project baseline: Radeon RX 9070 XT, ROCm 10.0, native Ubuntu
// 24.04. The kernels deliberately keep the same c[i] = a[i] + b[i] semantics.

#include <hip/hip_runtime.h>

#include <algorithm>
#include <cmath>
#include <cctype>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <limits>
#include <string>
#include <vector>

#define HIP_CHECK(call)                                                        \
    do {                                                                       \
        const hipError_t error = (call);                                       \
        if (error != hipSuccess) {                                             \
            std::fprintf(stderr, "HIP error at %s:%d: %s (%s)\n",             \
                         __FILE__, __LINE__, hipGetErrorString(error), #call);  \
            std::exit(EXIT_FAILURE);                                           \
        }                                                                      \
    } while (0)

namespace {

constexpr int kWaveSize = 32;
constexpr int kRoundsPerLane = 32;

__global__ void vector_add_v0(const float* __restrict__ input_a,
                              const float* __restrict__ input_b,
                              float* __restrict__ output,
                              std::size_t size) {
    const std::size_t index =
        static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
    if (index < size) {
        output[index] = input_a[index] + input_b[index];
    }
}

// Each wave handles the same 32 x 32-element tile in both v1 kernels. Only
// the order in which lanes visit that tile changes, so grid size, loop count,
// arithmetic, and total logical bytes remain fixed.
__global__ void vector_add_v1_contiguous(const float* __restrict__ input_a,
                                         const float* __restrict__ input_b,
                                         float* __restrict__ output,
                                         std::size_t size) {
    const int waves_per_block = blockDim.x / kWaveSize;
    const int wave_in_block = threadIdx.x / kWaveSize;
    const int lane = threadIdx.x % kWaveSize;
    const std::size_t wave =
        static_cast<std::size_t>(blockIdx.x) * waves_per_block + wave_in_block;
    const std::size_t base = wave * kWaveSize * kRoundsPerLane;

#pragma unroll 1
    for (int round = 0; round < kRoundsPerLane; ++round) {
        const std::size_t index =
            base + static_cast<std::size_t>(round) * kWaveSize + lane;
        if (index < size) {
            output[index] = input_a[index] + input_b[index];
        }
    }
}

__global__ void vector_add_v1_strided(const float* __restrict__ input_a,
                                      const float* __restrict__ input_b,
                                      float* __restrict__ output,
                                      std::size_t size) {
    const int waves_per_block = blockDim.x / kWaveSize;
    const int wave_in_block = threadIdx.x / kWaveSize;
    const int lane = threadIdx.x % kWaveSize;
    const std::size_t wave =
        static_cast<std::size_t>(blockIdx.x) * waves_per_block + wave_in_block;
    const std::size_t base = wave * kWaveSize * kRoundsPerLane;

#pragma unroll 1
    for (int round = 0; round < kRoundsPerLane; ++round) {
        const std::size_t index =
            base + static_cast<std::size_t>(lane) * kRoundsPerLane + round;
        if (index < size) {
            output[index] = input_a[index] + input_b[index];
        }
    }
}

__global__ void vector_add_v2_grid_stride(const float* __restrict__ input_a,
                                          const float* __restrict__ input_b,
                                          float* __restrict__ output,
                                          std::size_t size) {
    const std::size_t thread =
        static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
    const std::size_t grid_stride =
        static_cast<std::size_t>(gridDim.x) * blockDim.x;

    for (std::size_t index = thread; index < size; index += grid_stride) {
        output[index] = input_a[index] + input_b[index];
    }
}

__global__ void vector_add_v3_float4(const float* __restrict__ input_a,
                                     const float* __restrict__ input_b,
                                     float* __restrict__ output,
                                     std::size_t size) {
    const auto* input_a4 = reinterpret_cast<const float4*>(input_a);
    const auto* input_b4 = reinterpret_cast<const float4*>(input_b);
    auto* output4 = reinterpret_cast<float4*>(output);

    const std::size_t thread =
        static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
    const std::size_t grid_stride =
        static_cast<std::size_t>(gridDim.x) * blockDim.x;
    const std::size_t vector_count = size / 4;

    for (std::size_t vector_index = thread;
         vector_index < vector_count;
         vector_index += grid_stride) {
        const float4 a = input_a4[vector_index];
        const float4 b = input_b4[vector_index];
        output4[vector_index] =
            make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
    }

    // hipMalloc returns suitably aligned base pointers. The scalar loop keeps
    // the final 0-3 values correct when size is not divisible by four.
    for (std::size_t index = vector_count * 4 + thread;
         index < size;
         index += grid_stride) {
        output[index] = input_a[index] + input_b[index];
    }
}

struct Args {
    std::string version = "all";
    std::size_t size = 16ULL * 1024 * 1024;
    int block = 256;
    int grid = 0;
    int warmup = 10;
    int repeat = 50;
    std::size_t seed = 20260716;
    std::string samples;
    std::string config_id;
    int process = 1;
};

struct Version {
    std::string name;
    enum class Kind {
        v0,
        v1_contiguous,
        v1_strided,
        v2,
        v3,
    } kind;
};

struct Timing {
    double minimum_ms;
    double median_ms;
    double mean_ms;
    std::vector<double> samples_ms;
};

struct Validation {
    bool correct;
    double max_absolute_error;
    std::size_t first_bad_index;
};

[[noreturn]] void usage_error(const std::string& message) {
    std::fprintf(
        stderr,
        "%s\nUsage: vector_add_hip [--version all|v0|v1|v1-contiguous|"
        "v1-strided|v2|v3] [--size N] [--block N] [--grid N] "
        "[--warmup N] [--repeat N] [--seed N] "
        "[--samples CSV] [--config-id ID] [--process N]\n",
        message.c_str());
    std::exit(EXIT_FAILURE);
}

Args parse_args(int argc, char** argv) {
    Args args;
    for (int index = 1; index < argc; ++index) {
        const std::string option = argv[index];
        auto next = [&]() -> std::string {
            if (index + 1 >= argc) {
                usage_error("missing value for " + option);
            }
            return argv[++index];
        };

        try {
            if (option == "--version") {
                args.version = next();
            } else if (option == "--size") {
                args.size = std::stoull(next());
            } else if (option == "--block") {
                args.block = std::stoi(next());
            } else if (option == "--grid") {
                args.grid = std::stoi(next());
            } else if (option == "--warmup") {
                args.warmup = std::stoi(next());
            } else if (option == "--repeat") {
                args.repeat = std::stoi(next());
            } else if (option == "--seed") {
                args.seed = std::stoull(next());
            } else if (option == "--samples") {
                args.samples = next();
            } else if (option == "--config-id") {
                args.config_id = next();
            } else if (option == "--process") {
                args.process = std::stoi(next());
            } else {
                usage_error("unknown option: " + option);
            }
        } catch (const std::exception&) {
            usage_error("invalid value for " + option);
        }
    }

    if (args.size == 0) {
        usage_error("--size must be greater than zero");
    }
    if (args.block <= 0 || args.block > 1024 || args.block % kWaveSize != 0) {
        usage_error("--block must be a positive multiple of 32 and at most 1024");
    }
    if (args.grid < 0 || args.warmup < 0 || args.repeat <= 0) {
        usage_error("--grid/--warmup must be non-negative and --repeat positive");
    }
    if (args.process <= 0 || !std::all_of(args.config_id.begin(), args.config_id.end(),
            [](unsigned char c) { return std::isalnum(c) || c == '-' || c == '_' || c == '.'; })) {
        usage_error("--process must be positive; --config-id uses letters, digits, '-', '_' or '.'");
    }
    return args;
}

std::vector<Version> selected_versions(const std::string& selection) {
    const Version v0{"hip-v0", Version::Kind::v0};
    const Version v1_contiguous{
        "hip-v1-contiguous", Version::Kind::v1_contiguous};
    const Version v1_strided{"hip-v1-strided", Version::Kind::v1_strided};
    const Version v2{"hip-v2", Version::Kind::v2};
    const Version v3{"hip-v3", Version::Kind::v3};

    if (selection == "all") {
        return {v0, v1_contiguous, v1_strided, v2, v3};
    }
    if (selection == "v0") {
        return {v0};
    }
    if (selection == "v1") {
        return {v1_contiguous, v1_strided};
    }
    if (selection == "v1-contiguous") {
        return {v1_contiguous};
    }
    if (selection == "v1-strided") {
        return {v1_strided};
    }
    if (selection == "v2") {
        return {v2};
    }
    if (selection == "v3") {
        return {v3};
    }
    usage_error("unknown version: " + selection);
}

std::size_t ceil_div(std::size_t numerator, std::size_t denominator) {
    return numerator / denominator + (numerator % denominator != 0);
}

unsigned int checked_grid(std::size_t grid) {
    if (grid == 0 || grid > std::numeric_limits<unsigned int>::max()) {
        usage_error("calculated grid is outside the supported range");
    }
    return static_cast<unsigned int>(grid);
}

unsigned int grid_for(const Version& version,
                      const Args& args,
                      const hipDeviceProp_t& properties) {
    if (version.kind == Version::Kind::v0) {
        return checked_grid(ceil_div(args.size, args.block));
    }
    if (version.kind == Version::Kind::v1_contiguous ||
        version.kind == Version::Kind::v1_strided) {
        return checked_grid(ceil_div(
            args.size,
            static_cast<std::size_t>(args.block) * kRoundsPerLane));
    }

    const std::size_t work_items = version.kind == Version::Kind::v3
        ? std::max<std::size_t>(1, args.size / 4)
        : args.size;
    const std::size_t full_grid = ceil_div(work_items, args.block);
    if (args.grid > 0) {
        return checked_grid(std::min<std::size_t>(args.grid, full_grid));
    }
    const std::size_t persistent_grid =
        std::max(1, properties.multiProcessorCount) * 8ULL;
    return checked_grid(std::min(full_grid, persistent_grid));
}

void launch(const Version& version,
            unsigned int grid,
            int block,
            const float* input_a,
            const float* input_b,
            float* output,
            std::size_t size) {
    const dim3 grid_dim(grid);
    const dim3 block_dim(static_cast<unsigned int>(block));
    switch (version.kind) {
        case Version::Kind::v0:
            hipLaunchKernelGGL(vector_add_v0, grid_dim, block_dim, 0, 0,
                               input_a, input_b, output, size);
            break;
        case Version::Kind::v1_contiguous:
            hipLaunchKernelGGL(vector_add_v1_contiguous,
                               grid_dim, block_dim, 0, 0,
                               input_a, input_b, output, size);
            break;
        case Version::Kind::v1_strided:
            hipLaunchKernelGGL(vector_add_v1_strided,
                               grid_dim, block_dim, 0, 0,
                               input_a, input_b, output, size);
            break;
        case Version::Kind::v2:
            hipLaunchKernelGGL(vector_add_v2_grid_stride,
                               grid_dim, block_dim, 0, 0,
                               input_a, input_b, output, size);
            break;
        case Version::Kind::v3:
            hipLaunchKernelGGL(vector_add_v3_float4,
                               grid_dim, block_dim, 0, 0,
                               input_a, input_b, output, size);
            break;
    }
    HIP_CHECK(hipGetLastError());
}

void make_inputs(std::vector<float>& input_a,
                 std::vector<float>& input_b,
                 std::vector<float>& reference,
                 std::size_t seed) {
    for (std::size_t index = 0; index < input_a.size(); ++index) {
        const long long a_integer = static_cast<long long>(
            (index * 17ULL + seed) % 2048ULL) - 1024;
        const long long b_integer = static_cast<long long>(
            (index * 29ULL + seed * 3ULL) % 4096ULL) - 2048;
        input_a[index] = static_cast<float>(a_integer) / 1024.0F;
        input_b[index] = static_cast<float>(b_integer) / 2048.0F;
        reference[index] = input_a[index] + input_b[index];
    }
}

Validation validate(const float* device_output,
                    const std::vector<float>& reference,
                    std::vector<float>& host_output) {
    HIP_CHECK(hipMemcpy(host_output.data(), device_output,
                        host_output.size() * sizeof(float),
                        hipMemcpyDeviceToHost));

    double max_error = 0.0;
    std::size_t first_bad = reference.size();
    for (std::size_t index = 0; index < reference.size(); ++index) {
        const double error = std::abs(
            static_cast<double>(host_output[index]) - reference[index]);
        max_error = std::max(max_error, error);
        if ((!std::isfinite(host_output[index]) || error > 1.0e-6) &&
            first_bad == reference.size()) {
            first_bad = index;
        }
    }
    return {first_bad == reference.size(), max_error, first_bad};
}

Timing benchmark(const Version& version,
                 unsigned int grid,
                 const Args& args,
                 const float* input_a,
                 const float* input_b,
                 float* output) {
    for (int iteration = 0; iteration < args.warmup; ++iteration) {
        launch(version, grid, args.block, input_a, input_b, output, args.size);
    }
    HIP_CHECK(hipDeviceSynchronize());

    std::vector<hipEvent_t> starts(args.repeat);
    std::vector<hipEvent_t> ends(args.repeat);
    for (int iteration = 0; iteration < args.repeat; ++iteration) {
        HIP_CHECK(hipEventCreate(&starts[iteration]));
        HIP_CHECK(hipEventCreate(&ends[iteration]));
    }

    for (int iteration = 0; iteration < args.repeat; ++iteration) {
        HIP_CHECK(hipEventRecord(starts[iteration]));
        launch(version, grid, args.block, input_a, input_b, output, args.size);
        HIP_CHECK(hipEventRecord(ends[iteration]));
    }
    HIP_CHECK(hipDeviceSynchronize());

    std::vector<double> times_ms(args.repeat);
    for (int iteration = 0; iteration < args.repeat; ++iteration) {
        float elapsed_ms = 0.0F;
        HIP_CHECK(hipEventElapsedTime(
            &elapsed_ms, starts[iteration], ends[iteration]));
        times_ms[iteration] = elapsed_ms;
        HIP_CHECK(hipEventDestroy(starts[iteration]));
        HIP_CHECK(hipEventDestroy(ends[iteration]));
    }

    const std::vector<double> samples_ms = times_ms;
    std::sort(times_ms.begin(), times_ms.end());
    const std::size_t middle = times_ms.size() / 2;
    const double median = times_ms.size() % 2 == 0
        ? (times_ms[middle - 1] + times_ms[middle]) / 2.0
        : times_ms[middle];
    double sum = 0.0;
    for (const double value : times_ms) {
        sum += value;
    }
    return {times_ms.front(), median, sum / times_ms.size(), samples_ms};
}

void write_samples(const Version& version, const Args& args,
                   unsigned int grid, const Timing& timing) {
    if (args.samples.empty()) return;
    std::FILE* file = std::fopen(args.samples.c_str(), "a+");
    if (!file) usage_error("cannot open sample CSV: " + args.samples);
    std::fseek(file, 0, SEEK_END);
    if (std::ftell(file) == 0) {
        std::fprintf(file, "config_id,implementation,runtime,shape,dtype,block,grid,num_warps,warmup,repeat,seed,process,sample,ms\n");
    }
    const std::string id = args.config_id.empty() ? version.name : args.config_id;
    for (std::size_t i = 0; i < timing.samples_ms.size(); ++i) {
        std::fprintf(file, "%s,%s,hip,%zu,float32,%d,%u,0,%d,%d,%zu,%d,%zu,%.12g\n",
                     id.c_str(), version.name.c_str(), args.size, args.block, grid,
                     args.warmup, args.repeat, args.seed, args.process, i,
                     timing.samples_ms[i]);
    }
    if (std::ferror(file)) { std::fclose(file); usage_error("failed to write sample CSV"); }
    if (std::fclose(file) != 0) usage_error("failed to close sample CSV");
}

bool run_version(const Version& version,
                 const Args& args,
                 const hipDeviceProp_t& properties,
                 const float* device_a,
                 const float* device_b,
                 float* device_output,
                 const std::vector<float>& reference,
                 std::vector<float>& host_output) {
    const unsigned int grid = grid_for(version, args, properties);
    const std::size_t bytes = args.size * sizeof(float);

    HIP_CHECK(hipMemset(device_output, 0xFF, bytes));
    launch(version, grid, args.block,
           device_a, device_b, device_output, args.size);
    HIP_CHECK(hipDeviceSynchronize());
    const Validation precheck =
        validate(device_output, reference, host_output);
    if (!precheck.correct) {
        std::fprintf(
            stderr,
            "precheck failed for %s at index %zu: got=%g expected=%g\n",
            version.name.c_str(), precheck.first_bad_index,
            host_output[precheck.first_bad_index],
            reference[precheck.first_bad_index]);
        std::printf(
            "RESULT operator=vector-add implementation=%s runtime=hip "
            "shape=%zu dtype=float32 "
            "block=%d grid=%u warmup=%d repeat=%d seed=%zu timed=0 "
            "correct=FAIL precheck=FAIL postcheck=NA min_ms=NA "
            "median_ms=NA mean_ms=NA effective_bandwidth_gbs=NA "
            "max_abs_error=%.9g\n",
            version.name.c_str(), args.size, args.block, grid,
            args.warmup, args.repeat, args.seed,
            precheck.max_absolute_error);
        return false;
    }

    const Timing timing = benchmark(
        version, grid, args, device_a, device_b, device_output);
    const Validation postcheck =
        validate(device_output, reference, host_output);
    if (postcheck.correct) write_samples(version, args, grid, timing);
    const double logical_bytes =
        3.0 * static_cast<double>(args.size) * sizeof(float);
    const double effective_bandwidth_gbs =
        logical_bytes / (timing.median_ms * 1.0e-3) / 1.0e9;

    std::printf(
        "RESULT operator=vector-add implementation=%s runtime=hip "
        "shape=%zu dtype=float32 "
        "block=%d grid=%u warmup=%d repeat=%d seed=%zu timed=1 "
        "correct=%s precheck=OK postcheck=%s min_ms=%.6f "
        "median_ms=%.6f mean_ms=%.6f effective_bandwidth_gbs=%.6f "
        "max_abs_error=%.9g\n",
        version.name.c_str(), args.size, args.block, grid,
        args.warmup, args.repeat, args.seed,
        postcheck.correct ? "OK" : "FAIL",
        postcheck.correct ? "OK" : "FAIL",
        timing.minimum_ms, timing.median_ms, timing.mean_ms,
        effective_bandwidth_gbs, postcheck.max_absolute_error);
    return postcheck.correct;
}

}  // namespace

int main(int argc, char** argv) {
    const Args args = parse_args(argc, argv);
    const std::vector<Version> versions = selected_versions(args.version);

    int device = 0;
    hipDeviceProp_t properties{};
    HIP_CHECK(hipGetDevice(&device));
    HIP_CHECK(hipGetDeviceProperties(&properties, device));
    std::printf(
        "ENV runtime=hip gpu=\"%s\" arch=%s multi_processor_count=%d wave_size=%d\n",
        properties.name, properties.gcnArchName,
        properties.multiProcessorCount, properties.warpSize);

    const std::size_t bytes = args.size * sizeof(float);
    std::vector<float> host_a(args.size);
    std::vector<float> host_b(args.size);
    std::vector<float> reference(args.size);
    std::vector<float> host_output(args.size);
    make_inputs(host_a, host_b, reference, args.seed);

    float* device_a = nullptr;
    float* device_b = nullptr;
    float* device_output = nullptr;
    HIP_CHECK(hipMalloc(&device_a, bytes));
    HIP_CHECK(hipMalloc(&device_b, bytes));
    HIP_CHECK(hipMalloc(&device_output, bytes));
    HIP_CHECK(hipMemcpy(
        device_a, host_a.data(), bytes, hipMemcpyHostToDevice));
    HIP_CHECK(hipMemcpy(
        device_b, host_b.data(), bytes, hipMemcpyHostToDevice));

    bool all_correct = true;
    for (const Version& version : versions) {
        all_correct = run_version(
            version, args, properties,
            device_a, device_b, device_output,
            reference, host_output) && all_correct;
    }

    HIP_CHECK(hipFree(device_a));
    HIP_CHECK(hipFree(device_b));
    HIP_CHECK(hipFree(device_output));
    return all_correct ? EXIT_SUCCESS : EXIT_FAILURE;
}

这份文件包含本路线所有候选。后面只需定位相应函数并改变运行参数,不必复制多份主机程序。先编译一次;已有可执行文件时直接复用:

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
if [[ ! -x "$RUN/build/vector_add_hip" ]]; then
  hipcc --offload-arch=gfx1201 -O3 -std=c++17 \
    chapter8/vector_add_hip.hip -o "$RUN/build/vector_add_hip"
fi

--offload-arch=gfx1201 选择本章显卡的编译目标。若改了源码,请新建 RUN 后重新编译,避免新源码与旧可执行文件混用。

运行与校验

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir -p "$RUN/configs/hip-v0" && \
test ! -e "$RUN/configs/hip-v0/benchmark-p1.samples.csv" && \
"$RUN/build/vector_add_hip" \
  --version v0 --block 256 \
  --size 16777216 --warmup 10 --repeat 50 --seed 20260920 \
  --samples "$RUN/configs/hip-v0/benchmark-p1.samples.csv" \
  --config-id hip-v0 --process 1
运行输出:hip-v0,第 1 个独立进程
text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v0 runtime=hip shape=16777216 dtype=float32 block=256 grid=65536 warmup=10 repeat=50 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.348321 median_ms=0.349441 mean_ms=0.349336 effective_bandwidth_gbs=576.139024 max_abs_error=0

先核对 shape=16777216、block=256、grid=65536 和三个 OK。这次单进程的 median_ms=0.349441 是 50 次 event 时间的中位数。另两个独立进程分别为 0.333301、0.334861 ms;图中采用三者中位数 0.334861 ms,范围 0.333301–0.349441 ms。较慢的一次也保留,不能只取最快值。

基线分析:计时区间里实际执行了什么? 这一处 trace 用来核对目标 kernel 的起止时间和实际启动规模,建立后续读报告的方法。它不负责证明“已经达到带宽极限”。

保持当前目录和 RUN,另启动一次带 profiler 的进程。分析时预热 5 次、记录 10 次;这次终端打印的 event 时间不进入 benchmark。inspect_trace.py 只用 Python 标准库读取 CSV,不运行 GPU。

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir "$RUN/configs/hip-v0/profile" && \
rocprofv3 --kernel-trace --hip-trace --stats \
  --output-format csv pftrace \
  --output-directory "$RUN/configs/hip-v0/profile" \
  --output-file hip-v0 -- \
  "$RUN/build/vector_add_hip" \
  --version v0 --block 256 \
  --size 16777216 --warmup 5 --repeat 10 --seed 20260920 \
  --config-id hip-v0 --process 1 && \
python chapter8/inspect_trace.py \
  "$RUN/configs/hip-v0/profile/hip-v0_kernel_trace.csv" \
  --kernel vector_add_v0 --skip 6 --take 10
性能分析原始输出:hip-v0(终端、生成记录与 CSV)

程序 stdout

text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v0 runtime=hip shape=16777216 dtype=float32 block=256 grid=65536 warmup=5 repeat=10 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.335961 median_ms=0.337041 mean_ms=0.336981 effective_bandwidth_gbs=597.335629 max_abs_error=0

Profiler stderr:文件生成记录

text
W0920 08:17:17.555068 1401833 simple_timer.cpp:55] [rocprofv3] tool initialization ::     0.001453 sec
W0920 08:17:17.555337 1401833 tool.cpp:3348] HIP (compiler) version 7.15.0 initialized (instance=0)
W0920 08:17:17.555379 1401833 simple_timer.cpp:55] [rocprofv3] 'chapter8/results/walkthrough-20260920/build/vector_add_hip --version v0 --size 16777216 --block 256 --warmup 5 --repeat 10 --seed 20260920 --config-id hip-v0 --process 1' ::     0.000000 sec
W0920 08:17:17.555529 1401833 tool.cpp:3348] HIP (runtime) version 7.15.0 initialized (instance=0)
W0920 08:17:17.561246 1401833 tool.cpp:3348] HSA version 1.21.0 initialized (instance=0)
W0920 08:17:17.658814 1401833 queue_controller.cpp:170] [queue-intercept] device-memory ring-buffer requested but profiling requires system-memory InterceptQueue; falling back to system-memory ring for agent 100431044319616 (priority and CU-mask from the descriptor are not preserved)
W0920 08:17:17.767101 1401833 simple_timer.cpp:55] [rocprofv3] 'chapter8/results/walkthrough-20260920/build/vector_add_hip --version v0 --size 16777216 --block 256 --warmup 5 --repeat 10 --seed 20260920 --config-id hip-v0 --process 1' ::     0.211721 sec
E0920 08:17:17.796019 1401833 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/configs/hip-v0/profile/hip-v0_kernel_stats.csv
E0920 08:17:17.796170 1401833 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/configs/hip-v0/profile/hip-v0_kernel_trace.csv
E0920 08:17:17.813048 1401833 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/configs/hip-v0/profile/hip-v0_hip_api_stats.csv
E0920 08:17:17.813188 1401833 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/configs/hip-v0/profile/hip-v0_hip_api_trace.csv
E0920 08:17:17.818296 1401833 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/configs/hip-v0/profile/hip-v0_agent_info.csv
E0920 08:17:17.818407 1401833 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/configs/hip-v0/profile/hip-v0_domain_stats.csv
[153.869]       perfetto.cc:47314 Configured tracing session 1, #sources:1, duration:0 ms, #buffers:1, total buffer size:1048576 KB, total sessions:1, uid:0 session name: ""
E0920 08:17:17.846910 1401833 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/configs/hip-v0/profile/hip-v0_results.pftrace
[153.888]       perfetto.cc:48889 Tracing session 1 ended, total sessions:0
W0920 08:17:17.849113 1401833 simple_timer.cpp:55] [rocprofv3] output generation ::     0.067913 sec
W0920 08:17:17.849147 1401833 simple_timer.cpp:55] [rocprofv3] tool finalization ::     0.067986 sec

原始 kernel trace CSV

text
"Kind","Agent_Id","Queue_Id","Stream_Id","Thread_Id","Dispatch_Id","Kernel_Id","Kernel_Name","Correlation_Id","Start_Timestamp","End_Timestamp","LDS_Block_Size","Scratch_Size","VGPR_Count","Accum_VGPR_Count","SGPR_Count","Workgroup_Size_X","Workgroup_Size_Y","Workgroup_Size_Z","Grid_Size_X","Grid_Size_Y","Grid_Size_Z"
"KERNEL_DISPATCH","Agent 1",2,0,1401833,1,4,"__amd_rocclr_fillBufferUnAligned",14,908153729517605,908153729558685,0,0,8,0,128,256,1,1,8192,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,2,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",17,908153741205911,908153741882753,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,3,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",23,908153773225567,908153773685129,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,4,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",27,908153773689409,908153774155491,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,5,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",31,908153774160891,908153774733293,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,6,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",35,908153774747493,908153775111294,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,7,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",39,908153775116854,908153775481176,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,8,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",65,908153775686576,908153776012898,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,9,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",71,908153776029138,908153776355819,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,10,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",77,908153776371859,908153776698100,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,11,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",83,908153776714052,908153777040893,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,12,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",89,908153777056613,908153777382814,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,13,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",95,908153777398534,908153777725255,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,14,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",101,908153777741015,908153778067216,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,15,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",107,908153778083176,908153778409058,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,16,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",113,908153778424658,908153778751539,0,0,8,0,128,256,1,1,16777216,1,1
"KERNEL_DISPATCH","Agent 1",2,0,1401833,17,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",119,908153778767179,908153779092500,0,0,8,0,128,256,1,1,16777216,1,1
筛选后的分析结果:hip-v0
text
kernel_name="(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)"
csv_rows=17 target_rows=16 skipped_rows=6 kept_rows=10
sort=Start_Timestamp_ascending
first_start_ns=908153775686576 first_end_ns=908153776012898 first_duration_ns=326322
kernel_min_us=325.321000 kernel_median_us=326.281500 kernel_max_us=326.881000
grid_work_items_xyz=16777216x1x1 workgroup_size_xyz=256x1x1 workgroups_xyz=65536x1x1
workgroups_total=65536
vgpr_count=8 sgpr_count=128 lds_bytes=0 scratch_bytes=0

先找 vector_add_v0:原始 CSV 还包含填充输出的辅助 kernel,目标记录共有 16 条。去掉 1 次预检和 5 次预热,保留 10 次,这就是命令中 --skip 6 --take 10 的含义;整份 kernel_stats 的平均值包含预检与预热,不能直接作正式成绩。

再核对时间:第一条保留记录为 908153776012898 − 908153775686576 = 326322 ns,即 326.322 μs;10 次的中位数为 326.2815 μs。它来自独立采集,与 benchmark 的 event 时间不是同一次运行,不能相减来估算 CPU 开销。

最后核对启动规模:grid_work_items_xyz=16777216x1x1 除以 workgroup_size_xyz=256x1x1 得到 65,536 个 block。这里的 Grid X 是工作项数。vgpr_count=8、scratch_bytes=0 描述资源分配,不能单独证明动态占用率或带宽瓶颈。

这次核对确认了目标、规模和统计区间。下一项假设可以直接来自地址安排,无需先为每个候选再采一份相同报告。分析脚本全文和时间线读法见 8.5。

8.4.2 地址对照:只改变同轮访问顺序 ​

基线已经采用连续访问;不能把故意改坏它再改回来称为新的优化。这里另设一对实验,检验“同轮地址排列是否值得关注”,再把得到的判断带回正常实现。

base 是这组线程所负责片段的起点,round 是当前循环轮次。真实代码让一个 wave32 处理 1,024 个元素:32 个 lane 各处理 32 个元素,共循环 32 轮。两版的 grid、block、循环轮数、加法次数和输入完全相同,仅下标排列不同:

cpp
// hip-v1-contiguous:同一轮,相邻 lane 读取相邻位置
index = base + round * 32 + lane;

// hip-v1-strided:同一轮,相邻 lane 的位置相隔 32 个元素
index = base + lane * 32 + round;

两种排法最终都会覆盖同一片输入,也产生同一份输出。区别发生在同一轮请求哪些地址。

8.4 HIP · 合并访存 · 8-lane 教学缩略同样 8 个 lane,地址排法决定触及几组
32B 为教学分桶 · 非硬件事务计数 合并 · 连续顺序index = round × 8 + laneL0L1L2L3L4L5L6L7×0G0×0G1×0G2×0G3等待第 1 轮…分散 · 跨步顺序index = lane × 4 + roundL0L1L2L3L4L5L6L7×0G0×0G1×0G2×0G3等待第 1 轮… 两边最终覆盖相同的 32 个元素;真实时间由下一张实测图比较。

左右滑动画布,查看完整过程

同一批元素,两种下标排法同样 8 个 lane、同样 32 个元素。唯一变量:每轮内 lane 到下标的排列——左边 index = round×8+lane(连续),右边 index = lane×4+round(跨步)。

算法过程示意 · 数字定位到该步完成状态,可拖动进度或播放;动画速度不代表 GPU 耗时。

图 8.2 地址排列的缩略演示:8 个 lane 各处理 4 个元素。32B 分组只帮助观察地址是否集中,不是 RX 9070 XT 的实测事务计数。

图把真实代码的 32 个 lane × 32 轮缩成 8 个 lane × 4 轮,保留同轮连续或分散的差别。先在 图 8.2 中看一轮:连续排列的地址集中在一起,跨步排列散布在多组。再播放全部四轮,确认两边最终没有遗漏或重复。真实性能要看独立计时:

两次运行分别调用同一文件的 vector_add_v1_contiguous() 与 vector_add_v1_strided(),由 --version 选择。完整主机程序见本路线源码。

运行与校验

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir -p "$RUN/configs/hip-v1-contiguous" && \
test ! -e "$RUN/configs/hip-v1-contiguous/benchmark-p1.samples.csv" && \
"$RUN/build/vector_add_hip" \
  --version v1-contiguous --block 256 \
  --size 16777216 --warmup 10 --repeat 50 --seed 20260920 \
  --samples "$RUN/configs/hip-v1-contiguous/benchmark-p1.samples.csv" \
  --config-id hip-v1-contiguous --process 1
运行输出:hip-v1-contiguous,第 1 个独立进程
text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v1-contiguous runtime=hip shape=16777216 dtype=float32 block=256 grid=2048 warmup=10 repeat=50 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.366922 median_ms=0.370081 mean_ms=0.370749 effective_bandwidth_gbs=544.006097 max_abs_error=0

这是本轮的连续访问对照。输出应为 grid=2048、block=256,且前后检查通过;本次单进程中位数为 0.370081 ms。

运行与校验

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir -p "$RUN/configs/hip-v1-strided" && \
test ! -e "$RUN/configs/hip-v1-strided/benchmark-p1.samples.csv" && \
"$RUN/build/vector_add_hip" \
  --version v1-strided --block 256 \
  --size 16777216 --warmup 10 --repeat 50 --seed 20260920 \
  --samples "$RUN/configs/hip-v1-strided/benchmark-p1.samples.csv" \
  --config-id hip-v1-strided --process 1
运行输出:hip-v1-strided,第 1 个独立进程
text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v1-strided runtime=hip shape=16777216 dtype=float32 block=256 grid=2048 warmup=10 repeat=50 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=2.427285 median_ms=2.538206 mean_ms=2.540035 effective_bandwidth_gbs=79.318476 max_abs_error=0

跨步版保持同样的输入、grid、block 与循环工作量,输出仍然正确;本次单进程中位数为 2.538206 ms。接下来用三进程结果确认差异是否只是偶然波动。

相同工作量下连续访问与跨步访问的时间比较

图 8.3 只改变同轮地址排列的 HIP 对照;柱长为三个独立进程 median 的中位数,误差线为进程范围。

连续版为 0.369681 ms,跨步版为 2.538206 ms,后者用时约为前者的 6.87 倍,两者的进程范围明显分离。

受控对照支持“同轮地址排列显著影响当前实现的时间”。它同时改变 A/B 的读取与 C 的写入排列,尚未把时间分摊给读、写或具体缓存行为;图中的地址分组也不是实测内存事务。基线 v0 本来就连续访问,因此这不是把 v0 加速了 6.87 倍。

本轮的决定是保留同轮相邻访问。再采一份仅重复“跨步版更慢”的 trace,不会改变这个决定。若要继续量化物理流量或缓存命中率,需要有效的硬件计数器;本机本次没有取得相应有效数据,边界见 8.5.3。

8.4.3 Grid-Stride:减少 block 数是否有收益 ​

基线启动了 65,536 个 block,每个线程只做一次加法。一个可以验证的想法是:让线程循环处理多个位置,能否用更少的 block 完成同样的工作?

hip-v2 用整个 grid 的线程数作为步长:

cpp
const std::size_t thread =
    static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
const std::size_t grid_stride =
    static_cast<std::size_t>(gridDim.x) * blockDim.x;

for (std::size_t index = thread; index < size; index += grid_stride) {
    output[index] = input_a[index] + input_b[index];
}

假设整个 grid 有 1,024 个线程,线程 0 处理 0、1024、2048…,线程 1 处理 1、1025、2049…。单个线程跨着读,同一轮的相邻线程仍然连续读,与上一轮的跨步反例不同。

不能只把 v0 与一个缩小了 grid 的 v2 相比,就把差异都归给 block 数。因此分两步观察:先让 v2 同样使用 65,536 个 block,再固定这份循环代码,只把 grid 改为 2,048、256。输入、block size、计时方式保持不变。

本轮重点阅读完整程序中的 vector_add_v2_grid_stride() 与 grid_for()。三次使用同一份循环代码,分别给出明确的 grid 参数。

保持 65,536 个 block。

运行与校验

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir -p "$RUN/configs/hip-v2-g65536" && \
test ! -e "$RUN/configs/hip-v2-g65536/benchmark-p1.samples.csv" && \
"$RUN/build/vector_add_hip" \
  --version v2 --block 256 --grid 65536 \
  --size 16777216 --warmup 10 --repeat 50 --seed 20260920 \
  --samples "$RUN/configs/hip-v2-g65536/benchmark-p1.samples.csv" \
  --config-id hip-v2-g65536 --process 1
运行输出:hip-v2-g65536,第 1 个独立进程
text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v2 runtime=hip shape=16777216 dtype=float32 block=256 grid=65536 warmup=10 repeat=50 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.354801 median_ms=0.355121 mean_ms=0.355214 effective_bandwidth_gbs=566.923949 max_abs_error=0

先核对 grid=65536 与 v0 相同。此时检验的是换成循环结构的效果,不能把它和后面的 grid 缩小混为一项改动。本次单进程中位数为 0.355121 ms。

减少到 2,048 个 block。

运行与校验

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir -p "$RUN/configs/hip-v2-g2048" && \
test ! -e "$RUN/configs/hip-v2-g2048/benchmark-p1.samples.csv" && \
"$RUN/build/vector_add_hip" \
  --version v2 --block 256 --grid 2048 \
  --size 16777216 --warmup 10 --repeat 50 --seed 20260920 \
  --samples "$RUN/configs/hip-v2-g2048/benchmark-p1.samples.csv" \
  --config-id hip-v2-g2048 --process 1
运行输出:hip-v2-g2048,第 1 个独立进程
text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v2 runtime=hip shape=16777216 dtype=float32 block=256 grid=2048 warmup=10 repeat=50 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.359441 median_ms=0.361721 mean_ms=0.361690 effective_bandwidth_gbs=556.579759 max_abs_error=0

输出中的 grid=2048 表明配置已经生效。平均每线程处理 32 个元素,总加法数量没有减少;本次单进程中位数为 0.361721 ms。

减少到 256 个 block。

运行与校验

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir -p "$RUN/configs/hip-v2-g256" && \
test ! -e "$RUN/configs/hip-v2-g256/benchmark-p1.samples.csv" && \
"$RUN/build/vector_add_hip" \
  --version v2 --block 256 --grid 256 \
  --size 16777216 --warmup 10 --repeat 50 --seed 20260920 \
  --samples "$RUN/configs/hip-v2-g256/benchmark-p1.samples.csv" \
  --config-id hip-v2-g256 --process 1
运行输出:hip-v2-g256,第 1 个独立进程
text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v2 runtime=hip shape=16777216 dtype=float32 block=256 grid=256 warmup=10 repeat=50 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.342720 median_ms=0.343640 mean_ms=0.343801 effective_bandwidth_gbs=585.864836 max_abs_error=0

此时平均每线程处理 256 个元素。确认 correct=OK 后,记录本次单进程中位数 0.343640 ms;不要因为这一项较小,就跳过重复测量。

固定 grid-stride kernel 后比较三种 grid 大小,并以原始基线作为参照

图 8.4 v0 是原始参照;v2 的三个配置使用同一份 kernel。缩小 grid 会同时增加每个线程的工作量,图中实际比较的是这项分工选择。

三个 v2 配置分别为 0.350301、0.357582、0.343640 ms,本轮都没有显示稳定优于 v0 的收益。减少 block 数会同时增加每个线程的循环量;预期中的管理成本下降,未必能抵消其他代价。

本轮保留 v0。这个决定只需要公平测量,不需要给三档 grid 分别再做完整 trace。若后续要判断是否启动了预期数量的 block,可针对那一档检查 Grid / Workgroup;若要解释运行时占用率或等待,还需要对应的有效证据,不能用 block 数替代它们。

8.4.4 float4:一次表达四个元素是否更快 ​

上一轮的 grid=256 中位数高于基线,进程范围仍有重叠。每个线程要循环多次,按四个元素一组读写,能否弥补一部分开销?为了检验这个想法,我们继续固定 grid=256、block=256,比较标量循环 v2 与向量类型 v3;图中同时保留原始 v0。这里没有把 256 当作已经选出的最优 grid。

本例从 hipMalloc 返回的对齐基地址开始;任意 input + 1 偏移不再保证适合强转为 float4*,复用这段代码时要先保证地址对齐。

float4 把四个 FP32 组合成一个 16 Byte 类型。v3 按四元素组读写,并为剩余的 0–3 个元素保留标量路径:

8.4 HIP · float4 向量化四个 FP32 组成 float4,尾部单独处理
输入 A · N = 18 个 FP32 · 每个占 4 Byte float4 v0float4 v1float4 v2float4 v30123456789101112131415161701632486472Byte标量路径A[16]、A[17] 逐个处理 float4 是源码类型重解释 ≠ 物理显存事务一定减少 输入 B 的读取与输出 C 的写回采用同样分组

左右滑动画布,查看完整过程

先把 A 看成 18 个独立元素N=18 个 FP32,每个占 4 Byte——这是 v0/v1/v2 标量世界的做法。

算法过程示意 · 数字定位到该步完成状态,可拖动进度或播放;动画速度不代表 GPU 耗时。

图 8.5 N=18 的源码分组:前 16 个元素组成四组,最后两个元素单独处理。图中只展开输入 A,B 和 C 使用相同分组。

cpp
const std::size_t vector_count = size / 4;
for (std::size_t vector_index = thread;
     vector_index < vector_count;
     vector_index += grid_stride) {
    const float4 a = input_a4[vector_index];
    const float4 b = input_b4[vector_index];
    output4[vector_index] =
        make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
}
for (std::size_t index = vector_count * 4 + thread;
     index < size;
     index += grid_stride) {
    output[index] = input_a[index] + input_b[index];
}

本轮的假设是向量类型可能改变生成的访存指令及循环工作量。源码写出 float4 本身不能证明事务减少;先通过包含尾部的正确性检查,再判断时间:

完整实现仍在HIP 源文件的 vector_add_v3_float4(),运行参数使用 --version v3。

先检查新增的尾部路径:N=2 只走标量尾部,N=6 同时包含一组 float4 和两个尾部元素,N=1026 检查较长向量部分后的两个元素。这里预热为 0、计时重复 1 次,程序仍会先做一次预检;目的是核对答案。

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
"$RUN/build/vector_add_hip" --version v3 --size 2 --block 256 --warmup 0 --repeat 1 --seed 20260920 --grid 256 && \
"$RUN/build/vector_add_hip" --version v3 --size 6 --block 256 --warmup 0 --repeat 1 --seed 20260920 --grid 256 && \
"$RUN/build/vector_add_hip" --version v3 --size 1026 --block 256 --warmup 0 --repeat 1 --seed 20260920 --grid 256
边界检查原始输出:float4 尾部 @ RX 9070 XT / ROCm 10.0

boundary-float4-n2.log

text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v3 runtime=hip shape=2 dtype=float32 block=256 grid=1 warmup=0 repeat=1 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.011160 median_ms=0.011160 mean_ms=0.011160 effective_bandwidth_gbs=0.002151 max_abs_error=0

boundary-float4-n6.log

text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v3 runtime=hip shape=6 dtype=float32 block=256 grid=1 warmup=0 repeat=1 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.011200 median_ms=0.011200 mean_ms=0.011200 effective_bandwidth_gbs=0.006429 max_abs_error=0

boundary-float4-n1026.log

text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v3 runtime=hip shape=1026 dtype=float32 block=256 grid=1 warmup=0 repeat=1 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.011400 median_ms=0.011400 mean_ms=0.011400 effective_bandwidth_gbs=1.080000 max_abs_error=0

三项记录均为 precheck=OK postcheck=OK correct=OK,max_abs_error=0。小输入下程序会收缩 grid,三份输出的实际 grid=1,不是请求上限 256。日志中的单次 event 数值不作性能比较;边界通过后,再回到相同主输入与实际 grid=256 的对照。

运行与校验

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir -p "$RUN/configs/hip-v3-g256" && \
test ! -e "$RUN/configs/hip-v3-g256/benchmark-p1.samples.csv" && \
"$RUN/build/vector_add_hip" \
  --version v3 --block 256 --grid 256 \
  --size 16777216 --warmup 10 --repeat 50 --seed 20260920 \
  --samples "$RUN/configs/hip-v3-g256/benchmark-p1.samples.csv" \
  --config-id hip-v3-g256 --process 1
运行输出:hip-v3-g256,第 1 个独立进程
text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v3 runtime=hip shape=16777216 dtype=float32 block=256 grid=256 warmup=10 repeat=50 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.340083 median_ms=0.342002 mean_ms=0.342412 effective_bandwidth_gbs=588.670796 max_abs_error=0

先核对 grid=256、block=256 与标量 v2 一致,前后检查通过;本次单进程中位数为 0.342002 ms。四元素分组同时改变循环工作量、地址表达和编译选择,不能仅凭源码中的类型推断访存事务减少。

相同 grid 和 block 下标量循环与 float4 循环的时间比较

图 8.6 固定 grid=256、block=256,比较 v2 与 v3。相同执行配置使这轮改动更容易解释。

标量 v2 为 0.343640 ms,四元素分组 v3 为 0.342121 ms。两者的三进程范围重叠,与 v0 也有重叠,尚无稳定收益足以替换简单基线。

因此本轮停止这项改动,保留 v0。只有还要回答“编译器是否生成了预期的宽指令”时,才继续看编译结果;即使发现指令变宽,也不能据此宣布速度会提高。8.5.3 的 ATT 选读只演示 v0 的源码与指令对应,不作为这里 v3 的解释证据。

8.4.5 HIP 路线的实验决策 ​

这条路线保留了三种可复用的方法:用相同工作量隔离地址排列,用同一循环 kernel 扫描 grid,用相同 grid/block 对照元素分组。每个对照都回答一个问题;没有稳定提速也能帮助决定停止哪项尝试。

当前主输入选择 v0。换成别的实际长度或布局时,应从基线重新比较,不能把某个 grid 或向量类型当成通用答案。

8.5 按问题选择性能分析 ​

打开 profiler 之前,先写一句它要回答的问题,再决定读什么字段。若报告无法改变下一步的选择,就不必为了流程完整而采集。

8.5.1 从问题到字段 ​

当前问题优先证据本章的使用位置
修改后是否正确,是否值得替换基线?正确性检查与独立进程 benchmark每轮必做;不需要 profiler 才能作取舍
运行的是预期 kernel 和启动规模吗?kernel 名称、Grid / Workgroup、目标调用次数HIP 基线 trace
kernel 在设备上何时开始、何时结束?起止时间戳与 CPU/GPU 时间线基线示范;后续有提交节奏疑问时再看
tile 改变后资源分配有变化吗?代表配置的 Workgroup、VGPR、LDS、ScratchTriton256 与2048配对分析
物理流量或缓存命中率是否变化?经过有效性核验的相应硬件计数器本机本次没有取得足够证据,不能用逻辑字节替代
源码对应哪些指令,局部 wave 在哪里等待?编译结果或局部指令跟踪进阶诊断;不能直接分摊整个 kernel 的时间

本章的 inspect_trace.py 先匹配一个确定的 kernel 名称,按起点排序,再去掉预检与预热。它会检查记录数量、必需列和各次启动字段是否一致;不满足预期时直接报错。命令中的 grid 仍要与结果人工对照。

完整分析脚本:code/part2-kernels/chapter8/inspect_trace.py
python
"""Read one rocprofv3 kernel-trace window using only Python's standard library."""
from __future__ import annotations

import argparse
import csv
import json
import math
from pathlib import Path
import re
import statistics


GRID_FIELDS = tuple(f"Grid_Size_{axis}" for axis in "XYZ")
GROUP_FIELDS = tuple(f"Workgroup_Size_{axis}" for axis in "XYZ")
RESOURCE_FIELDS = ("VGPR_Count", "SGPR_Count", "LDS_Block_Size", "Scratch_Size")
INTEGER_FIELDS = ("Start_Timestamp", "End_Timestamp", *GRID_FIELDS,
                  *GROUP_FIELDS, *RESOURCE_FIELDS)
REQUIRED_FIELDS = ("Kernel_Name", *INTEGER_FIELDS)


class TraceError(ValueError):
    """The CSV cannot support the requested comparison."""


def analyze_trace(path: Path, kernel: str, skip: int, take: int) -> dict:
    if not kernel.strip():
        raise TraceError("--kernel must be a non-empty name substring")
    if skip < 0 or take <= 0:
        raise TraceError("--skip must be non-negative and --take must be positive")
    rows = []
    with path.open(encoding="utf-8-sig", newline="") as file:
        reader = csv.DictReader(file, strict=True)
        fields = reader.fieldnames or []
        if len(fields) != len(set(fields)):
            raise TraceError("CSV has duplicate column names")
        missing = sorted(set(REQUIRED_FIELDS) - set(fields))
        if missing:
            raise TraceError("CSV is missing required columns: " + ", ".join(missing))
        for row in reader:
            where = f"CSV line {reader.line_num}"
            if None in row or any(value is None for value in row.values()):
                raise TraceError(f"{where}: row width does not match the header")
            if not row["Kernel_Name"].strip():
                raise TraceError(f"{where}: Kernel_Name is empty")
            for field in INTEGER_FIELDS:
                value = row[field].strip()
                if not re.fullmatch(r"[0-9]+", value):
                    raise TraceError(f"{where}: {field} must be a non-negative integer, got {value!r}")
                row[field] = int(value)
            if row["End_Timestamp"] < row["Start_Timestamp"]:
                raise TraceError(f"{where}: End_Timestamp is earlier than Start_Timestamp")
            for grid_field, group_field in zip(GRID_FIELDS, GROUP_FIELDS):
                grid, group = row[grid_field], row[group_field]
                if grid <= 0 or group <= 0 or grid % group:
                    raise TraceError(f"{where}: {grid_field} must be a positive multiple of {group_field}")
            rows.append(row)

    targets = [row for row in rows if kernel in row["Kernel_Name"]]
    names = sorted({row["Kernel_Name"] for row in targets})
    if len(names) != 1:
        detail = "no kernel names" if not names else ", ".join(repr(name) for name in names)
        raise TraceError(f"--kernel must match exactly one distinct Kernel_Name; matched {detail}")
    expected = skip + take
    if len(targets) != expected:
        raise TraceError(f"expected exactly skip + take = {expected} target rows, found {len(targets)}")
    targets.sort(key=lambda row: row["Start_Timestamp"])
    first = targets[0]
    for row in targets[1:]:
        changed = [field for field in (*GRID_FIELDS, *GROUP_FIELDS, *RESOURCE_FIELDS)
                   if row[field] != first[field]]
        if changed:
            raise TraceError("target rows have different launch/resource fields: " + ", ".join(changed))
    kept = targets[skip:]
    durations = [row["End_Timestamp"] - row["Start_Timestamp"] for row in kept]
    grid = [first[field] for field in GRID_FIELDS]
    group = [first[field] for field in GROUP_FIELDS]
    groups = [g // w for g, w in zip(grid, group)]
    return {
        "csv_rows": len(rows), "kernel_name": names[0], "target_rows": len(targets),
        "skipped_rows": skip, "kept_rows": len(kept),
        "first_kept": {"start_ns": kept[0]["Start_Timestamp"],
                       "end_ns": kept[0]["End_Timestamp"], "duration_ns": durations[0]},
        "kernel_time_us": {"min": min(durations) / 1000,
                           "median": statistics.median(durations) / 1000,
                           "max": max(durations) / 1000},
        "launch": {"grid_work_items_xyz": grid, "workgroup_size_xyz": group,
                   "workgroups_xyz": groups, "workgroups_total": math.prod(groups)},
        "resources": dict(zip(("vgpr_count", "sgpr_count", "lds_bytes", "scratch_bytes"),
                              (first[field] for field in RESOURCE_FIELDS))),
    }


def print_report(report: dict) -> None:
    print("kernel_name=" + json.dumps(report["kernel_name"], ensure_ascii=False))
    print(" ".join(f"{key}={report[key]}" for key in
                   ("csv_rows", "target_rows", "skipped_rows", "kept_rows")))
    print("sort=Start_Timestamp_ascending")
    print(" ".join(f"first_{key}={value}" for key, value in report["first_kept"].items()))
    print(" ".join(f"kernel_{key}_us={value:.6f}" for key, value in report["kernel_time_us"].items()))
    launch = report["launch"]
    print(" ".join(f"{key}=" + "x".join(map(str, launch[key])) for key in
                   ("grid_work_items_xyz", "workgroup_size_xyz", "workgroups_xyz")))
    print(f"workgroups_total={launch['workgroups_total']}")
    print(" ".join(f"{key}={value}" for key, value in report["resources"].items()))


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("path", type=Path, help="rocprofv3 *_kernel_trace.csv")
    parser.add_argument("--kernel", required=True, help="case-sensitive Kernel_Name substring")
    parser.add_argument("--skip", required=True, type=int, help="target rows before the measured window")
    parser.add_argument("--take", required=True, type=int, help="exact number of measured target rows")
    parser.add_argument("--json", action="store_true", help="print the same report as JSON")
    args = parser.parse_args(argv)
    try:
        report = analyze_trace(args.path, args.kernel, args.skip, args.take)
    except (TraceError, OSError, UnicodeError, csv.Error) as error:
        parser.error(str(error))
    if args.json:
        print(json.dumps(report, indent=2, ensure_ascii=False))
    else:
        print_report(report)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

采集命令用 mkdir ... && rocprofv3 ... && python ... 连接。目录已存在或采集失败时停止后续步骤,避免覆盖旧记录或误读旧报告。采集过程中可能改变队列处理方式,因此其 event 时间始终与 benchmark 分开。

文件阅读用途
*_kernel_trace.csv逐条核对目标名称、时间戳和启动字段
*_hip_api_trace.csvCPU 侧 API 调用,用 Correlation_Id 关联提交与设备执行
*_kernel_stats.csv整次进程汇总,含预检与预热,不作正式成绩
*_results.pftrace查看 CPU/GPU 时间线与调用关系

工具的 Opened result file 记录位于 stderr,行首带 E;这些行报告文件生成。是否成功还要看退出状态、文件内容与目标记录,不能只看日志前缀。时间戳单位为 ns,分析脚本为 μs,benchmark 为 ms:1 ms = 1,000 μs = 1,000,000 ns。

8.5.2 时间线与工具适用范围 ​

先让工具回答一个具体问题:要知道哪次 kernel 慢,读时间线;要核对执行了多少工作,尝试有效的硬件计数器;要观察局部 wave 的指令轨迹,再进入 ATT。工具越底层,需要核对的采集条件也越多。

本章环境中的 rocprofv3 来自 ROCprofiler-SDK 1.3.5,工具自报 ROCm 10.0.0。ROCm 发行版、SDK 工具和 HIP 组件各有自己的版本号,不能要求它们都显示 10.0。

bash
rocprofv3 --version
查看本机 rocprofv3 版本输出

ROCm wheel 环境中的工具

text
             version: 1.3.5
        git_revision: 6b0e43f341195e203754e08f850e437ff2fc09f9
        library_arch: x86_64-unknown-linux-gnu
         system_name: Linux
    system_processor: x86_64
      system_version: 6.12.73-95.123.amzn2023.x86_64
         compiler_id: Clang
    compiler_version: 23.0.0
        rocm_version: 10.0.0

输出中的 version 与 rocm_version 对应工具版本和 ROCm 版本;system_version 是工具构建信息,实验机系统以本章的环境记录为准。

工具或功能在本章 RX 9070 XT 上的情况适合回答的问题
rocprofv3 kernel / HIP trace本章 11 个配置均已采集 CSV 与 Perfetto 时间线CPU 提交了什么,GPU 何时执行,实际启动规模是多少
rocprofv3 硬件计数器(PMC)wave 数取得可核对结果;本次缓存与流量指标返回零,不能用来解释访存效率指定 dispatch 的某类事件发生了多少次;先核对指标与采集是否有效
rocprofv3 --att 指令跟踪已取得单次 baseline dispatch 的原始跟踪与解码产物选定硬件位置上的 wave 执行了哪些指令;适合继续定位局部等待
ROCprofiler Systems官方 ROCm 10.0 矩阵列出 Radeon 支持;本章未实测此工具更大的应用中,CPU 调用栈、线程活动与 GPU 工作怎样关联
ROCprofiler Compute(原 Omniperf)当前官方兼容表未列 RX 9070 XT在其支持设备上使用硬件指标面板;本章不提供该工具的实操命令
Radeon GPU Profiler(RGP)官方 Linux Ubuntu 24.04 条目为 Vulkan only,不能直接套到这里的 HIP 程序需要同时符合 GPU、API 与操作系统要求,不能只看显卡型号

支持范围分别见 SDK tracing 文档、ROCm 10.0 兼容矩阵、Compute 支持表和 RGP Requirements。下面只展开本机已经验证过的路径。

先看时间线。 在 Perfetto 中选择 Open trace file,打开本次配置目录内的 *_results.pftrace。先找到 CPU 的 HIP API 轨道与 GPU kernel 轨道,再搜索目标函数名:CPU 的 launch 调用结束,不代表 GPU kernel 已经完成。展开进程轨道,搜索 vector_add_v0,可以依次定位 16 次目标调用;第一条是预检。点选 GPU 区间,在 Details 中看 Duration,在 Preceding Flows 中找到对应的 hipLaunchKernel;比较时仍应使用 8.4 中经过筛选的多次执行,而非随手选中一条。

同一个 profile 同时导出了 API trace 和 kernel trace,可以借助 Correlation_Id 对应一次提交与设备执行。时间线适合找执行先后和空隙,不能仅凭某段空白就断言 CPU 算力不足;同步、依赖和提交节奏都需要结合程序检查。当前命令没有开启 memory-copy trace,因此不把缺少拷贝轨道解释为没有发生数据复制。

本次 Perfetto 导入出现了一项 track_descriptor_conflicting_reservation 提示,目标 kernel 的 16 条记录仍可检索,数量与 CSV 一致。因此时间线用于观察调用关系,本章的筛选统计继续以原始 CSV 为准;不把查看器的导入提示当作 kernel 性能问题。

8.5.3 进阶诊断:计数器与指令跟踪 ​

前面的配置选择不依赖下面两项诊断。只有遇到对应问题时才展开;不能因为工具更底层,就默认它更适合回答当前问题。

计数器能否作为解释依据? 当前 wave 计数能与启动规模核对,但缓存与流量计数返回零,不足以解释地址对照的时间差。这里的第一步是确认测到的量有意义。

选读:核对 wave 计数,以及缓存与流量指标的有效性

计数器记录某类硬件事件的次数。先选择一个能用程序规模核对的量,比一开始就读几十个百分比更容易。本例选择 SQ_WAVES_sum,核对 baseline 启动的 wave 数。

下面使用 HIP v0 做独立工具核验。若前面只走了 Triton 路线,也先执行这里的编译命令;它生成的仍是 8.4.1 的完整程序。然后查询指标组合,采集 v0 预检和预热之后的 10 次执行:

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
if [[ ! -x "$RUN/build/vector_add_hip" ]]; then
  hipcc --offload-arch=gfx1201 -O3 -std=c++17 \
    chapter8/vector_add_hip.hip -o "$RUN/build/vector_add_hip"
fi && \
rocprofv3-avail -d 0 pmc-check SQ_WAVES_sum GRBM_GUI_ACTIVE && \
mkdir "$RUN/pmc-waves" && \
rocprofv3 --pmc SQ_WAVES_sum GRBM_GUI_ACTIVE \
  --kernel-include-regex vector_add_v0 --kernel-iteration-range 7-16 \
  --output-format csv --output-directory "$RUN/pmc-waves" \
  --output-file baseline -- \
  "$RUN/build/vector_add_hip" --version v0 --size 16777216 --block 256 \
  --warmup 5 --repeat 10 --seed 20260920

-d 0 选择当前查询到的设备;pmc-check 检查这组指标能否一起采集,最终还要看实际记录。--kernel-iteration-range 7-16 在这里筛选 计数器采集的目标调用;它不会替 8.4 的 kernel trace 删除预检与预热,所以 trace 仍然需要自己的筛选步骤。

查看计数器组合检查、运行输出与完整原始 CSV

组合检查

text
Following input counters can be collected together on GPU:0	SQ_WAVES_sum	GRBM_GUI_ACTIVE

采集进程的输出(不用于 benchmark)

text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v0 runtime=hip shape=16777216 dtype=float32 block=256 grid=65536 warmup=5 repeat=10 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.368002 median_ms=0.368702 mean_ms=1.549060 effective_bandwidth_gbs=546.041483 max_abs_error=0

Profiler 文件生成记录

text
W0920 08:28:05.453606 1407749 simple_timer.cpp:55] [rocprofv3] tool initialization ::     0.198063 sec
W0920 08:28:05.454094 1407749 simple_timer.cpp:55] [rocprofv3] 'chapter8/results/walkthrough-20260920/build/vector_add_hip --version v0 --size 16777216 --block 256 --warmup 5 --repeat 10 --seed 20260920' ::     0.000000 sec
W0920 08:28:05.459627 1407749 tool.cpp:3348] HSA version 1.21.0 initialized (instance=0)
W0920 08:28:05.553311 1407749 queue_controller.cpp:170] [queue-intercept] device-memory ring-buffer requested but profiling requires system-memory InterceptQueue; falling back to system-memory ring for agent 109707699042784 (priority and CU-mask from the descriptor are not preserved)
W0920 08:28:05.679374 1407749 simple_timer.cpp:55] [rocprofv3] 'chapter8/results/walkthrough-20260920/build/vector_add_hip --version v0 --size 16777216 --block 256 --warmup 5 --repeat 10 --seed 20260920' ::     0.225279 sec
E0920 08:28:05.700510 1407749 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/pmc-waves/baseline_counter_collection.csv
E0920 08:28:05.705534 1407749 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/pmc-waves/baseline_agent_info.csv
W0920 08:28:05.706677 1407749 simple_timer.cpp:55] [rocprofv3] output generation ::     0.015960 sec
W0920 08:28:05.706711 1407749 simple_timer.cpp:55] [rocprofv3] tool finalization ::     0.015996 sec

baseline_counter_collection.csv

text
"Correlation_Id","Dispatch_Id","Agent_Id","Queue_Id","Process_Id","Thread_Id","Grid_Size","Kernel_Id","Kernel_Name","Workgroup_Size","LDS_Block_Size","Scratch_Size","VGPR_Count","Accum_VGPR_Count","SGPR_Count","Counter_Name","Counter_Value","Start_Timestamp","End_Timestamp"
8,8,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",587211.000000,908801672169834,908801672534236
8,8,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801672169834,908801672534236
9,9,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",528736.000000,908801684159393,908801684486634
9,9,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801684159393,908801684486634
10,10,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",527469.000000,908801684534475,908801684861637
10,10,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801684534475,908801684861637
11,11,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",528474.000000,908801684908317,908801685235639
11,11,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801684908317,908801685235639
12,12,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",529355.000000,908801685282319,908801685609441
12,12,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801685282319,908801685609441
13,13,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",529368.000000,908801685655721,908801685982803
13,13,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801685655721,908801685982803
14,14,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",529518.000000,908801686028883,908801686356125
14,14,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801686028883,908801686356125
15,15,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",528427.000000,908801686402365,908801686729287
15,15,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801686402365,908801686729287
16,16,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",530411.000000,908801686775168,908801687103049
16,16,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801686775168,908801687103049
17,17,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GRBM_GUI_ACTIVE",529243.000000,908801687149930,908801687477012
17,17,"Agent 1",2,1407749,1407749,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"SQ_WAVES_sum",524288.000000,908801687149930,908801687477012

读取 baseline_counter_collection.csv 时,先找 Counter_Name=SQ_WAVES_sum。同一个 Dispatch_Id 下有不同计数器的记录,不要把 CSV 行数当作 kernel 次数。本次每个目标 dispatch 的值都是 524,288。本例实际采用 wave32,可以按分工核对:

65,536 blocks×256 threads32 threads/wave=524,288 waves

这说明本次 wave 计数与已知启动规模吻合。它并没有告诉我们显存传输了多少字节,也不表示 wave 数越少就越快。

我们还试了 GL2C_HIT_sum、GL2C_MISS_sum 与 FetchSize,本机当前电源状态下得到的记录均为零,没有取得足以解释缓存命中率或物理流量的有效证据。AMD 的 SDK 限制说明要求 gfx11/gfx12 计数器采集使用稳定 power state;本次保留机器原来的 AUTO 状态。因此本章不据这些零值计算命中率,也不把地址反例的慢直接换算成 DRAM 事务数。

查看本次缓存与流量计数器的尝试:命令及原始零值记录

两组分别采集,程序与参数不变;结果保存在不同目录。

bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir "$RUN/pmc-l2" && \
rocprofv3 --pmc GL2C_HIT_sum GL2C_MISS_sum \
  --kernel-include-regex vector_add_v0 --kernel-iteration-range 7-16 \
  --output-format csv --output-directory "$RUN/pmc-l2" \
  --output-file baseline -- \
  "$RUN/build/vector_add_hip" --version v0 --size 16777216 --block 256 \
  --warmup 5 --repeat 10 --seed 20260920

test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
mkdir "$RUN/pmc-fetch" && \
rocprofv3 --pmc FetchSize \
  --kernel-include-regex vector_add_v0 --kernel-iteration-range 7-16 \
  --output-format csv --output-directory "$RUN/pmc-fetch" \
  --output-file baseline -- \
  "$RUN/build/vector_add_hip" --version v0 --size 16777216 --block 256 \
  --warmup 5 --repeat 10 --seed 20260920

缓存计数器:采集进程输出

text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v0 runtime=hip shape=16777216 dtype=float32 block=256 grid=65536 warmup=5 repeat=10 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.381122 median_ms=0.381562 mean_ms=0.421637 effective_bandwidth_gbs=527.638639 max_abs_error=0

缓存计数器:原始 CSV

text
"Correlation_Id","Dispatch_Id","Agent_Id","Queue_Id","Process_Id","Thread_Id","Grid_Size","Kernel_Id","Kernel_Name","Workgroup_Size","LDS_Block_Size","Scratch_Size","VGPR_Count","Accum_VGPR_Count","SGPR_Count","Counter_Name","Counter_Value","Start_Timestamp","End_Timestamp"
8,8,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802309333301,908802309697943
8,8,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802309333301,908802309697943
9,9,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802309758303,908802310263826
9,9,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802309758303,908802310263826
10,10,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802310323706,908802310650747
10,10,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802310323706,908802310650747
11,11,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802310710388,908802311037749
11,11,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802310710388,908802311037749
12,12,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802311097390,908802311424631
12,12,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802311097390,908802311424631
13,13,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802311484351,908802311811633
13,13,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802311484351,908802311811633
14,14,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802311870913,908802312197915
14,14,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802311870913,908802312197915
15,15,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802312256915,908802312584156
15,15,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802312256915,908802312584156
16,16,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802312643517,908802312970678
16,16,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802312643517,908802312970678
17,17,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_HIT_sum",0.00000000e+00,908802313030039,908802313356960
17,17,"Agent 1",2,1407761,1407761,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"GL2C_MISS_sum",0.00000000e+00,908802313030039,908802313356960

FetchSize:采集进程输出

text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v0 runtime=hip shape=16777216 dtype=float32 block=256 grid=65536 warmup=5 repeat=10 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.399335 median_ms=0.400135 mean_ms=1.500717 effective_bandwidth_gbs=503.147292 max_abs_error=0

FetchSize:原始 CSV

text
"Correlation_Id","Dispatch_Id","Agent_Id","Queue_Id","Process_Id","Thread_Id","Grid_Size","Kernel_Id","Kernel_Name","Workgroup_Size","LDS_Block_Size","Scratch_Size","VGPR_Count","Accum_VGPR_Count","SGPR_Count","Counter_Name","Counter_Value","Start_Timestamp","End_Timestamp"
8,8,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802923101432,908802923429234
9,9,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802934145686,908802934473258
10,10,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802934563220,908802934890512
11,11,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802934968195,908802935295607
12,12,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802935373409,908802935700741
13,13,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802935778384,908802936106076
14,14,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802936183559,908802936510691
15,15,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802936588494,908802936915826
16,16,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802936993149,908802937320601
17,17,"Agent 1",2,1407771,1407771,16777216,23,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)",256,0,0,8,0,128,"FetchSize",0.00000000e+00,908802937397804,908802937725055

这里核对的是目标 dispatch 的 Counter_Value,不是上方采集进程打印的 event 耗时。全零尚不能解释原来的快慢,下一步应先核对采集条件,而不是用这些零值计算命中率或带宽。

一行加法对应哪些指令? 下例跟踪 baseline v0 的一个局部硬件位置,将源码对应到实际加载、等待、计算与写回。它用于理解指令报告,不用来解释 v3 或把局部周期换算成整卡瓶颈。

选读:用 ATT 查看 baseline 的源码与指令对应

若要观察某行源码对应哪些指令,可以用局部 wave 跟踪补充证据。下面只演示 baseline 的源码对应,不归因某个优化版本的时间差。SDK 的 ATT 支持表已列出 gfx1201,模式为 trace-only;本例不使用 Instinct 示例里的 --att-perfcounters。

这是进阶选读,不影响前面完成一轮优化实验。本章提供一个短脚本,仍使用完整的 vector_add_hip.hip:单独编译带行号的诊断程序,选择 v0 的第 7 次调用,即预检与 5 次预热之后的一次调用,再限定一个 shader engine 与 SIMD。

完整采集脚本:code/part2-kernels/chapter8/profile_att.sh
bash
#!/usr/bin/env bash
# Run from code/part2-kernels after source ./activate-rocm.sh.
set -euo pipefail
if [[ $# != 1 || -z "$1" ]]; then
  echo 'Usage: bash chapter8/profile_att.sh NEW_OUTPUT_DIRECTORY' >&2
  exit 2
fi
att_dir=$1
if [[ -e "$att_dir" ]]; then
  echo "Output already exists: $att_dir" >&2
  exit 2
fi
[[ -f chapter8/vector_add_hip.hip ]] || { echo 'Run from code/part2-kernels.' >&2; exit 2; }
mapfile -t sdk_libs < <(python - <<'PY'
from pathlib import Path
import sysconfig
site = Path(sysconfig.get_path('purelib'))
for package in ('_rocm_sdk_devel', '_rocm_sdk_core'):
    print(site / package / 'lib')
PY
)
[[ ${#sdk_libs[@]} == 2 && -d "${sdk_libs[0]}" && -d "${sdk_libs[1]}" ]] || {
  echo 'Activate the ROCm 10.0 environment first.' >&2; exit 2;
}
mkdir -p "$att_dir/build" "$att_dir/trace"
export LD_LIBRARY_PATH="${sdk_libs[0]}:${sdk_libs[1]}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
exec 3>"$att_dir/commands.log"
export BASH_XTRACEFD=3
set -x
hipcc --offload-arch=gfx1201 -O3 -std=c++17 -gline-tables-only \
  chapter8/vector_add_hip.hip -o "$att_dir/build/vector_add_hip_debug" \
  >"$att_dir/compile.stdout.log" 2>"$att_dir/compile.stderr.log"
rocprofv3 --att --att-library-path "${sdk_libs[@]}" \
  --att-simd-select 0 --att-shader-engine-mask 0x1 \
  --att-buffer-size 67108864 --kernel-include-regex vector_add_v0 \
  --kernel-iteration-range 7 --output-format csv \
  --output-directory "$att_dir/trace" --output-file baseline -- \
  "$att_dir/build/vector_add_hip_debug" --version v0 --size 16777216 \
  --block 256 --warmup 5 --repeat 1 --seed 20260920 \
  >"$att_dir/profile.stdout.log" 2>"$att_dir/profile.stderr.log"
python chapter8/validate_att.py "$att_dir/trace" \
  >"$att_dir/validation.stdout.log" 2>"$att_dir/validation.stderr.log" || {
    cat "$att_dir/validation.stderr.log" >&2; exit 1;
  }
set +x
cat "$att_dir/validation.stdout.log"
echo "ATT captured and decoded: $att_dir/trace"
echo 'One selected dispatch / shader engine / SIMD; not whole-GPU counters.'

脚本最后会调用随附的 validate_att.py,核对目标 dispatch 的指令统计、kernel 名称和解码 JSON;产物缺失或为空时会报错,不会只凭 profiler 正常退出就宣布解码成功。

完整产物检查脚本:code/part2-kernels/chapter8/validate_att.py
python
"""Validate the single-dispatch ATT output expected by profile_att.sh."""
from __future__ import annotations

import argparse
import csv
import json
from pathlib import Path
import re


def validate_att(directory: Path) -> dict:
    candidates = sorted(directory.glob('stats_*.csv'))
    if len(candidates) != 1:
        raise ValueError('expected exactly one decoded stats CSV')
    stats = candidates[0]
    match = re.fullmatch(r'stats_(ui_output_agent_\d+_dispatch_8)\.csv', stats.name)
    if match is None:
        raise ValueError('expected decoded stats for dispatch 8')
    integer_fields = ('CodeObj', 'Vaddr', 'Hitcount', 'Latency', 'Stall', 'Idle')
    required = {*integer_fields, 'Instruction', 'Source'}
    with stats.open(newline='') as file:
        reader = csv.DictReader(file, strict=True)
        fields = reader.fieldnames or []
        if len(fields) != len(set(fields)) or not required <= set(fields):
            raise ValueError('stats CSV has missing or duplicate required columns')
        rows = list(reader)
    if not rows:
        raise ValueError('stats CSV contains no instruction rows')
    for row in rows:
        if None in row or any(value is None for value in row.values()):
            raise ValueError('stats CSV row width does not match its header')
        if not row['Instruction'].strip():
            raise ValueError('stats CSV contains an empty instruction')
        if any(re.fullmatch(r'[0-9]+', row[field]) is None for field in integer_fields):
            raise ValueError('stats CSV counts and addresses must be non-negative integers')
    if not any(re.search(r'\bvector_add_v0\(', row['Source']) for row in rows):
        raise ValueError('stats CSV does not identify vector_add_v0')
    if not any(int(row['Hitcount']) > 0 for row in rows):
        raise ValueError('stats CSV contains no positive instruction Hitcount')
    ui = directory / match[1]
    json_files = sorted(ui.glob('*.json'))
    if not json_files or not (ui / 'code.json').is_file():
        raise ValueError('corresponding UI directory or code.json is missing')
    for path in json_files:
        document = json.loads(path.read_text())
        if not document:
            raise ValueError(f'empty decoded JSON: {path.name}')
    code = json.loads((ui / 'code.json').read_text())
    if not isinstance(code, dict) or not code.get('code'):
        raise ValueError('code.json contains no decoded instructions')
    if not list(ui.glob('*_wv*.json')):
        raise ValueError('UI directory contains no decoded wave JSON')
    return dict(dispatch=8, kernel='vector_add_v0', instruction_rows=len(rows),
                json_files=len(json_files), stats=stats.name, ui=ui.name)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('directory', type=Path)
    args = parser.parse_args()
    try:
        result = validate_att(args.directory)
    except (OSError, ValueError, csv.Error) as error:
        parser.exit(1, f'ATT validation failed: {error}\n')
    print('ATT_VALIDATED ' + ' '.join(f'{key}={value}' for key, value in result.items()))
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
bash
test -d "${RUN:?请先完成环境与结果目录准备}/build" && \
bash chapter8/profile_att.sh "$RUN/att"
查看 ATT 运行结果、采集记录与解码后的指令统计

脚本的终端输出

text
ATT_VALIDATED dispatch=8 kernel=vector_add_v0 instruction_rows=29 json_files=4134 stats=stats_ui_output_agent_3208_dispatch_8.csv ui=ui_output_agent_3208_dispatch_8
ATT captured and decoded: chapter8/results/walkthrough-20260920/att-verified/trace
One selected dispatch / shader engine / SIMD; not whole-GPU counters.

采集进程输出(不用于 benchmark)

text
ENV runtime=hip gpu="AMD Radeon RX 9070 XT" arch=gfx1201 multi_processor_count=32 wave_size=32
RESULT operator=vector-add implementation=hip-v0 runtime=hip shape=16777216 dtype=float32 block=256 grid=65536 warmup=5 repeat=1 seed=20260920 timed=1 correct=OK precheck=OK postcheck=OK min_ms=0.389084 median_ms=0.389084 mean_ms=0.389084 effective_bandwidth_gbs=517.437330 max_abs_error=0

Profiler 记录

text
W0920 08:41:31.355825 1414466 simple_timer.cpp:55] [rocprofv3] tool initialization ::     0.065424 sec
W0920 08:41:31.356418 1414466 simple_timer.cpp:55] [rocprofv3] 'chapter8/results/walkthrough-20260920/att-verified/build/vector_add_hip_debug --version v0 --size 16777216 --block 256 --warmup 5 --repeat 1 --seed 20260920' ::     0.000000 sec
W0920 08:41:31.365662 1414466 tool.cpp:3348] HSA version 1.21.0 initialized (instance=0)
W0920 08:41:31.460928 1414466 queue_controller.cpp:170] [queue-intercept] device-memory ring-buffer requested but profiling requires system-memory InterceptQueue; falling back to system-memory ring for agent 107007324322592 (priority and CU-mask from the descriptor are not preserved)
E0920 08:41:31.478486 1414466 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/att-verified/trace/baseline_gfx1201_code_object_id_1.out
E0920 08:41:31.491417 1414466 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/att-verified/trace/baseline_gfx1201_code_object_id_2.out
E0920 08:41:31.537903 1414468 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/att-verified/trace/baseline_3208_shader_engine_0_8.att
W0920 08:41:31.566081 1414466 simple_timer.cpp:55] [rocprofv3] 'chapter8/results/walkthrough-20260920/att-verified/build/vector_add_hip_debug --version v0 --size 16777216 --block 256 --warmup 5 --repeat 1 --seed 20260920' ::     0.209663 sec
E0920 08:41:31.579793 1414466 output_stream.cpp:110] Opened result file: chapter8/results/walkthrough-20260920/att-verified/trace/baseline_agent_info.csv
W0920 08:41:32.202873 1414466 simple_timer.cpp:55] [rocprofv3] output generation ::     0.623269 sec
W0920 08:41:32.202914 1414466 simple_timer.cpp:55] [rocprofv3] tool finalization ::     0.623313 sec

stats_ui_output_agent_3208_dispatch_8.csv 的归档副本

text
"CodeObj","Vaddr","Instruction","Hitcount","Latency","Stall","Idle","Source"
2,13568,"; _ZN12_GLOBAL__N_113vector_add_v0EPKfS1_Pfm",0,0,0,0,"(anonymous namespace)::vector_add_v0(float const*, float const*, float*, unsigned long)"
2,13568,"s_clause 0x1",4124,4124,0,131694,"<environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:258 -> <environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:317 -> <walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13572,"s_load_b32 s8, s[0:1], 0x2c",4124,11020,6896,0,"<environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:258 -> <environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:317 -> <walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13580,"s_load_b256 s[0:7], s[0:1], 0x0",4124,4143,19,3,"<environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:258 -> <environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:317 -> <walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13588,"v_mov_b32_e32 v1, 0",4124,4124,0,1806,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13592,"s_wait_kmcnt 0x0",4124,159748,155624,0,"<environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:258 -> <environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:317 -> <walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13596,"s_and_b32 s8, s8, 0xffff",4124,4124,0,47,"<environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:258 -> <environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:317 -> <walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13604,"s_delay_alu instid0(VALU_DEP_1) | instid1(SALU_CYCLE_1)",0,0,0,0,"<environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:258 -> <environment-part-root>/.venv/lib/python3.12/site-packages/_rocm_sdk_devel/include/hip/amd_detail/amd_hip_runtime.h:317 -> <walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13608,"v_mad_co_u64_u32 v[0:1], null, s8, ttmp9, v[0:1]",4124,16496,0,34124,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13616,"s_delay_alu instid0(VALU_DEP_1)",0,0,0,0,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:40"
2,13620,"v_cmp_gt_u64_e32 vcc_lo, s[6:7], v[0:1]",4124,8248,0,17131,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:41"
2,13624,"s_and_saveexec_b32 s6, vcc_lo",4124,4124,0,62166,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:41"
2,13628,"s_cbranch_execz 27",4124,4124,0,32920,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:41"
2,13632,"v_lshlrev_b64_e32 v[0:1], 2, v[0:1]",4124,8248,0,9557,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:?"
2,13636,"s_delay_alu instid0(VALU_DEP_1) | instskip(NEXT) | instid1(VALU_DEP_1)",0,0,0,0,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:?"
2,13640,"v_add_co_u32 v2, vcc_lo, s0, v0",4124,4124,0,16613,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:?"
2,13648,"v_add_co_ci_u32_e64 v3, null, s1, v1, vcc_lo",4124,5267,1143,16446,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:?"
2,13656,"v_add_co_u32 v4, vcc_lo, s2, v0",4124,4124,0,71,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:?"
2,13664,"s_wait_alu depctr_va_vcc(0)",0,0,0,0,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:?"
2,13668,"v_add_co_ci_u32_e64 v5, null, s3, v1, vcc_lo",4124,4124,0,66921,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:?"
2,13676,"global_load_b32 v2, v[2:3], off",4124,8255,7,61975,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:42"
2,13688,"global_load_b32 v3, v[4:5], off",4124,8248,0,0,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:42"
2,13700,"v_add_co_u32 v0, vcc_lo, s4, v0",4124,4124,0,34588,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:42"
2,13708,"s_wait_alu depctr_va_vcc(0)",0,0,0,0,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:42"
2,13712,"v_add_co_ci_u32_e64 v1, null, s5, v1, vcc_lo",4124,4124,0,62432,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:42"
2,13720,"s_wait_loadcnt 0x0",4124,11111887,11107763,0,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:42"
2,13724,"v_add_f32_e32 v2, v2, v3",4124,4124,0,124,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:42"
2,13728,"global_store_b32 v[0:1], v2, off",4124,12372,0,65950,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:42"
2,13740,"s_endpgm",4124,1481481,0,0,"<walkthrough-root>/code/part2-kernels/chapter8/vector_add_hip.hip:44"

本次生成原始 .att、对应 code object,以及解码后的 stats_ui_output_agent_3208_dispatch_8.csv 与 ui_output_agent_3208_dispatch_8/;文件名中的 agent 编号可能随运行变化。终端中的 ATT_VALIDATED 确认目标为 dispatch 8、kernel 为 vector_add_v0,并检查了 29 行指令统计和 4,134 个非空的有效 JSON 文件。输出记录中只有选定的一次 dispatch。脚本显式指定当前 ROCm wheel 的解码库目录,是因为本次直接采用默认搜索路径时出现了 Error loading decoder: 37;指定路径后采集与解码均成功。脚本会保留实际命令、编译日志、采集日志与产物检查结果。

先读 Instruction 与 Source 两列。本次 vector_add_hip.hip:42 的一行加法,映射出两条 global_load_b32,随后是 s_wait_loadcnt、v_add_f32_e32 与 global_store_b32。它把源码中的“读两个数、等待数据、相加、写回”对应到了真实指令。

再看 Hitcount:上述指令在本次局部跟踪中各记录了 4,124 次。它是对已追踪 wave 累加的该指令执行次数;本例每个被跟踪的 wave 执行这些指令各一次。它和上一节全 dispatch 的 524,288 个 wave 不是同一范围。Latency、Stall 与 Idle 是周期统计,不是 ms;即使等待指令对应较大数值,也需要结合跟踪范围、依赖和受控对照分析,不能直接宣布它贡献了整个 kernel 的多少百分比。字段定义见 ATT Stats CSV。

这些轨迹只代表选定硬件位置与这一次调用。局部 wave 的指令时间不能直接加成整个 GPU 的 kernel 时间,也不能拿启用 ATT 后的 event 耗时替换 benchmark。解码目录可交给 ROCprof Compute Viewer继续查看;本章验证到采集与解码,尚未验证该查看器在本机上的图形展示。

8.6 HIP 与 Triton 对照 ​

8.6.1 两条路线调整的对象 ​

问题HIP 直接表达Triton 直接表达
一份工作负责哪些数据线程下标、循环步长、向量分组program 的 offsets 与逻辑 tile
尾部怎样保护标量 if 与标量尾部循环load/store 各自的 mask
本章改变的执行粒度固定 kernel 后调整 grid固定 num_warps 后调整 BLOCK_SIZE
谁完成底层映射源码指定线程到地址的关系,编译器生成指令编译器将 tile 分配给 lane、寄存器与执行组

HIP 的 thread 与 Triton 的 program 不是一一对应关系。比较两种实现时,先核对负责哪些输出、读哪些输入、怎样处理边界,再核对时间与资源,而不是把两种语言的参数名称直接互换。

8.6.2 将每轮结果放回原始基线 ​

HIP 与 Triton 向量加法各版本的完整时间汇总

图 8.9 相同主输入下的代表配置总览;下表列出全部 11 个配置。地址对照是独立诊断,不是从 v0 开始的线性升级链;这张图用于回看各轮选择,不替代前面的受控比较。

配置Median(ms)三进程范围(ms)
hip-v00.3348610.333301–0.349441
hip-v1-contiguous0.3696810.366240–0.370081
hip-v1-strided2.5382062.505867–2.576887
hip-v2-g655360.3503010.332861–0.355121
hip-v2-g20480.3575820.353582–0.361721
hip-v2-g2560.3436400.334301–0.344421
hip-v3-g2560.3421210.342002–0.342500
triton-t0-b2560.3340610.333781–0.349521
triton-t1-b5120.3354410.335401–0.340481
triton-t1-b10240.3377010.336301–0.338220
triton-t1-b20480.3412410.340961–0.343221

hip-v0 与 triton-t0-b256 的三进程测量范围高度重叠,基于当前严谨的统计数据,我们不能得出“某种语言在性能上压倒另一种”的轻率结论。更为客观的认识是:在当前的大规模输入下,两条路线最自然的基线实现均已展现出极高的访存效率;后续试图通过更复杂的技巧进行重构时,唯有拿出同口径下更显著的提速证据,才值得替换掉简洁明了的基线。

正文引用的单进程输出、三进程统计与五张对比图表,均完整归档于配套代码库的 evidence/walkthrough/ 目录中,确保所有实验数据源头均可独立追溯与复核。

8.7 负结果、适用边界与下一步 ​

一次改动没有提速,也能帮助我们判断在当前条件下是否值得保留。记录观察结果时,要同时说明它还不能解释什么:

观察到的实测现象据此可以作出的工程决定依然不能直接推论的假设
受控地址排列对比出现 6.87 倍断崖落差坚决保留同轮连续访存模式,在后续所有算子中持续审视访问连续性不能脱离硬件计数器直接将理论分桶等同于真实 DRAM 事务数
Grid 或 Tile 缩放未见稳定收益保留最初简洁的配置,不盲目为了减少线程/Program 而加重单任务负担不能推广认为“更少的 Block/Program 在所有场景下都普遍低效”
引入 float4 向量化后无稳定提速保留原生标量加法写法,避免徒增代码复杂度不能断言“向量类型在硬件层面上绝对没有生成更宽的访存指令”

本章得出的最佳配置结论严格受限于当前的大规模输入。当计算场景切换到小尺寸张量或非连续内存布局时,应当重新运行基线并评估候选版本,切忌将大数组下的经验生搬硬套。

如果未来我们需要实现 ReLU、GELU 或 Scale & Bias,由于输出元素依然保持严格独立,本章建立的网格划分与边界保护策略均可直接迁移复用。如果考虑将 Add 与后续的激活算子进行算子融合,则必须严格测量“两次独立算子调用”与“单次融合内核调用”的完整端到端路径,相关的融合收益与代价我们将在第 12 章深入探讨。

8.8 复跑与练习 ​

8.8.1 复现逐轮实验 ​

在仓库根目录进入本篇环境:

bash
cd code/part2-kernels &&
uv sync --locked &&
source ./activate-rocm.sh

单项命令帮助你理解一个改动。要比较运行波动,用批量入口自动运行 11 个配置,每个配置启动 3 个独立进程,并轮换运行顺序。这里显式选择 main:做小边界检查与 benchmark,不采集 profiler。输出目录必须尚不存在:

bash
python chapter8/run_rounds.py --output chapter8/results/my-run --phase main

程序默认同样为 main。它在测量前检查 1、31、32、33、255、256、257、1027 等小长度;主输入每个进程仍执行前后检查。profile 与 validation 是额外阶段,不是每次实验必做的下一步;profile 会采集全部配置,当前章节通常只需 8.4 中针对问题的单项命令。已有主实验目录不会被覆盖。

若需要在独立进程中再次确认选定配置,并检查各 tile 的专属边界,可以执行下面的补充验收。这是单独的验收步骤,不包含在上面的默认运行中:

bash
python chapter8/verify_rounds.py \
  --frozen-run chapter8/results/my-run \
  --output chapter8/results/my-confirmation

确认步骤复用 my-run 中保存的 kernel,结果写进新目录;不覆盖原扫描。8.4 的单项命令用于看清每一步,这里的运行器用于自动保存完整的多进程矩阵。

从自己的汇总重画本章五类比较图:

bash
python chapter8/plot_rounds.py \
  --summary chapter8/results/my-run/summary/summary.csv \
  --manifest chapter8/results/my-run/summary/manifest.json \
  --out-dir chapter8/results/my-run/figures

运行目录中,samples/ 保存每次 event 时间,commands.jsonl 保存实际命令,logs/ 保存环境、程序输出和检查结果,profiles/ 保存原始 trace,source/ 保存该次源码,summary/ 保存汇总与环境清单。仅执行 main 时,profiles/ 为空是正常现象。上面的绘图命令显式读取你自己的汇总;不要省略数据参数,否则脚本的默认值会读取另一组归档。正文五张图对应 evidence/walkthrough/;evidence/rounds/ 是先前完整运行器实验的独立快照,两组不混算成绩。

采集日期、源码身份与详细条件保存在每组实验记录中。新增结果以自己的运行目录保存,原始样本、环境、源码哈希和实际参数一起保留;不同日期、shape 或计时区间的结果不混成一个中位数。

8.8.2 练习与验收 ​

  1. 选择一个新的非整除长度,先预测最后一个 HIP block 和 Triton program 中哪些位置有效,再检查输出。正确性通过后才计时。
  2. 固定 v2 的源码与 block size,增加一个 grid 候选。把结果与现有 grid 配置放在同一张图中,说明这次是否值得保留。
  3. 固定 Triton 的 num_warps=4,改变一个 tile 候选;记录 program 数和实测范围。若差异很小,说明为什么还不能宣布胜者。
  4. 将算术替换为 output[i] = alpha * input[i] + beta。重新计算逻辑字节与 FLOP,先写出改动假设,再校验和比较;如果需要 profiling,写明它要回答的问题与预期字段。

完成时应能独立回答:我改了哪项分工,为什么值得试,正确性怎样确认,比较图支持什么决定,还有什么原因没有证实。如果决定进一步 profiling,还要说明现有测量缺少什么证据、工具能否取得它,以及不同结果会怎样改变下一步。某个优化一定成功,不是验收条件。

本章小结 ​

  • 输出独立,分工可以改变:一个输出只依赖对应输入,可以比较不同的线程分工与数据块大小。低算术强度提示优先检查数据访问,但不能据此断定显存带宽是唯一瓶颈。
  • 两条路线各有控制方式:HIP 从线程索引出发,比较地址排列、grid-stride 循环与四元素分组;Triton 从分块张量出发,比较 tile 大小。
  • 每轮都检查并复测:先核对结果,再按相同口径测量,与基线及上一轮比较。独立复测与样本范围帮助判断收益是否稳定,没有提速的结果也要保留。
  • 有具体问题时再做 profiling:正确性与性能比较可以支持保留或回退的决定;需要进一步解释时,再选择能够提供相关证据的工具。

接下来的第 9 章:Reduction 归约算子研究一个输出依赖多个输入的情况:怎样组织线程协作,并把各组的局部结果合并为最终答案。

延伸阅读 ​