1. 引言
在前面的文章中,我们已经了解了如何查询计数器属性、注册跟踪会话和管理访问权限。本文将深入分析性能数据采集的核心环节:启动跟踪、查询数据和停止跟踪。
这三个API是整个性能监控流程的关键操作步骤,它们直接与Linux内核的 perf_event 子系统交互,实现硬件性能计数器的实时采集。
2. API 概览
2.1 hsaKmtPmcStartTrace - 启动跟踪
HSAKMT_STATUS HSAKMTAPI hsaKmtPmcStartTrace(
HSATraceId TraceId, // 输入:跟踪会话ID
void *TraceBuffer, // 输入:数据缓冲区
HSAuint64 TraceBufferSizeBytes // 输入:缓冲区大小(字节)
);
功能:
- 启动所有已注册的性能计数器
- 关联数据缓冲区
- 通过
perf_event使能硬件计数器 - 更新跟踪状态
2.2 hsaKmtPmcQueryTrace - 查询数据
HSAKMT_STATUS HSAKMTAPI hsaKmtPmcQueryTrace(
HSATraceId TraceId // 输入:跟踪会话ID
);
功能:
- 从所有计数器读取当前值
- 将数据写入预分配的缓冲区
- 按照注册时的顺序组织数据
- 支持多次查询(累计值)
2.3 hsaKmtPmcStopTrace - 停止跟踪
HSAKMT_STATUS HSAKMTAPI hsaKmtPmcStopTrace(
HSATraceId TraceId // 输入:跟踪会话ID
);
功能:
- 停止所有性能计数器
- 通过
perf_event禁用硬件计数器 - 更新跟踪状态
- 保留最后的数据供读取
2.4 返回值
| 返回值 | 说明 |
|---|---|
HSAKMT_STATUS_SUCCESS | 操作成功 |
HSAKMT_STATUS_INVALID_PARAMETER | TraceId为0或缓冲区参数无效 |
HSAKMT_STATUS_INVALID_HANDLE | TraceId无效(魔数校验失败) |
HSAKMT_STATUS_ERROR | ioctl调用失败 |
HSAKMT_STATUS_UNAVAILABLE | perf_event文件描述符无效 |
HSAKMT_STATUS_NO_MEMORY | 缓冲区大小不足 |
3. 核心数据结构
3.1 perf_trace 结构(回顾)
struct perf_trace {
uint32_t magic4cc; // 魔数: 0x54415348 ("HSAT")
uint32_t gpu_id; // GPU标识
enum perf_trace_state state; // 跟踪状态
uint32_t num_blocks; // 硬件块数量
void *buf; // 数据缓冲区指针
uint64_t buf_size; // 缓冲区大小
struct perf_trace_block blocks[0]; // 块数组(可变长度)
};
3.2 perf_trace_state 枚举
enum perf_trace_state {
PERF_TRACE_STATE__STOPPED = 0, // 已停止
PERF_TRACE_STATE__STARTED = 1 // 已启动
};
状态转换图:
[初始创建] ─────────────────────────→ STOPPED
│
┌─────────────────────────────────────┘
│ hsaKmtPmcStartTrace()
↓
STARTED ←──────────────────────────────┐
│ │
│ hsaKmtPmcStopTrace() │ hsaKmtPmcStartTrace()
↓ │ (重新启动)
STOPPED ──────────────────────────────┘
3.3 perf_trace_block 结构(回顾)
struct perf_trace_block {
enum perf_block_id block_id; // 硬件块ID
uint32_t num_counters; // 计数器数量
uint64_t *counter_id; // 计数器ID数组
int *perf_event_fd; // perf_event文件描述符数组
};
3.4 perf_counts_values 结构
struct perf_counts_values {
union {
struct {
uint64_t val; // 计数器值
uint64_t ena; // 使能时间
uint64_t run; // 运行时间
};
uint64_t values[3];
};
};
字段说明:
val: 实际的计数器值(事件发生次数)ena: 计数器使能的时间(纳秒)run: 计数器实际运行的时间(纳秒)- 使用
ena和run可以计算计数器的活跃度:active_ratio = run / ena
4. hsaKmtPmcStartTrace 实现分析
4.1 完整流程图
hsaKmtPmcStartTrace(TraceId, TraceBuffer, TraceBufferSizeBytes)
↓
[1] 调试日志输出
↓
[2] 验证参数
├─ TraceId == 0 ? ──────────────→ 返回 INVALID_PARAMETER
├─ TraceBuffer == NULL ? ───────→ 返回 INVALID_PARAMETER
└─ TraceBufferSizeBytes == 0 ? ─→ 返回 INVALID_PARAMETER
↓
[3] 转换 TraceId 为 perf_trace 指针
↓
[4] 验证魔数
├─ magic4cc != HSA_PERF_MAGIC4CC → 返回 INVALID_HANDLE
└─ 匹配 → 继续
↓
[5] 遍历所有硬件块,启用计数器
│ for (i = 0; i < trace->num_blocks; i++)
│ ↓
│ [5.1] 调用 perf_trace_ioctl(ENABLE)
│ ├─ 成功 → 继续下一个块
│ └─ 失败 → 跳转到错误回滚
↓
[6] 所有块启用成功
↓
[7] 更新跟踪状态
├─ trace->state = STARTED
├─ trace->buf = TraceBuffer
└─ trace->buf_size = TraceBufferSizeBytes
↓
[8] 返回 SUCCESS
[错误回滚路径]
↓
[E1] 禁用已启用的块
│ for (j = i - 1; j >= 0; j--)
│ ↓
│ [E1.1] 调用 perf_trace_ioctl(DISABLE)
↓
[E2] 返回错误码
4.2 源码详解
步骤1-4:参数验证
HSAKMT_STATUS HSAKMTAPI hsaKmtPmcStartTrace(
HSATraceId TraceId,
void *TraceBuffer,
HSAuint64 TraceBufferSizeBytes)
{
struct perf_trace *trace =
(struct perf_trace *)PORT_UINT64_TO_VPTR(TraceId);
uint32_t i;
int32_t j;
HSAKMT_STATUS ret = HSAKMT_STATUS_SUCCESS;
pr_debug("[%s] Trace ID 0x%lx\n", __func__, TraceId);
// 验证所有必需参数
if (TraceId == 0 || !TraceBuffer || TraceBufferSizeBytes == 0)
return HSAKMT_STATUS_INVALID_PARAMETER;
// 验证TraceId的有效性(魔数检查)
if (trace->magic4cc != HSA_PERF_MAGIC4CC)
return HSAKMT_STATUS_INVALID_HANDLE;
关键点:
- 三个参数都不能为空/零
- 魔数验证确保TraceId未被破坏
- TraceBuffer由调用者分配,大小需满足最小要求
步骤5:启用所有计数器
// 遍历所有硬件块
for (i = 0; i < trace->num_blocks; i++) {
// 启用当前块的所有计数器
ret = perf_trace_ioctl(&trace->blocks[i],
PERF_EVENT_IOC_ENABLE);
if (ret != HSAKMT_STATUS_SUCCESS)
break; // 失败则退出循环
}
perf_trace_ioctl 调用:
static HSAKMT_STATUS perf_trace_ioctl(struct perf_trace_block *block,
uint32_t cmd)
{
uint32_t i;
// 对块中的每个计数器执行ioctl命令
for (i = 0; i < block->num_counters; i++) {
// 验证文件描述符有效性
if (block->perf_event_fd[i] < 0)
return HSAKMT_STATUS_UNAVAILABLE;
// 执行ioctl系统调用
if (ioctl(block->perf_event_fd[i], cmd, NULL))
return HSAKMT_STATUS_ERROR;
}
return HSAKMT_STATUS_SUCCESS;
}
PERF_EVENT_IOC_ENABLE 命令:
- Linux
perf_event子系统的标准命令 - 启用性能计数器的数据采集
- 从此刻开始累计事件计数
- 对应的内核操作:启动硬件计数器寄存器
步骤6:错误回滚机制
// 如果启用失败,需要回滚已启用的计数器
if (ret != HSAKMT_STATUS_SUCCESS) {
/* Disable enabled blocks before returning the failure. */
j = (int32_t)i; // i是失败时的块索引
// 从失败位置往回禁用已启用的块
while (--j >= 0)
perf_trace_ioctl(&trace->blocks[j],
PERF_EVENT_IOC_DISABLE);
return ret;
}
错误回滚的重要性:
- 保持一致性:要么全部启用,要么全部不启用
- 避免资源泄漏:部分启用的计数器会消耗资源
- 简化错误处理:调用者无需处理部分成功的情况
示例场景:
假设有3个块:Block0, Block1, Block2
1. Block0 启用成功 ✓
2. Block1 启用成功 ✓
3. Block2 启用失败 ✗
↓ 回滚操作
4. Block1 禁用 (j=1)
5. Block0 禁用 (j=0)
6. 返回错误
步骤7:保存缓冲区信息
// 所有计数器启用成功,更新跟踪状态
trace->state = PERF_TRACE_STATE__STARTED;
trace->buf = TraceBuffer;
trace->buf_size = TraceBufferSizeBytes;
return HSAKMT_STATUS_SUCCESS;
}
为什么保存缓冲区指针?
- Query操作需要知道数据写入位置
- Stop之后仍可能读取数据
- 验证缓冲区大小是否足够
5. hsaKmtPmcQueryTrace 实现分析
5.1 完整流程图
hsaKmtPmcQueryTrace(TraceId)
↓
[1] 验证 TraceId != 0
├─ No → 返回 INVALID_PARAMETER
└─ Yes → 继续
↓
[2] 转换为 perf_trace 指针
↓
[3] 验证魔数
├─ 不匹配 → 返回 INVALID_HANDLE
└─ 匹配 → 继续
↓
[4] 获取缓冲区指针
├─ buf = (uint64_t *)trace->buf
└─ buf_filled = 0
↓
[5] 双重循环:遍历块和计数器
│ for each block
│ for each counter
│ ↓
│ [5.1] 检查缓冲区空间
│ ├─ buf_filled + 8 > buf_size ?
│ │ → 返回 NO_MEMORY
│ └─ 继续
│ ↓
│ [5.2] 读取计数器值
│ │ query_trace(fd, buf)
│ ↓
│ [5.3] 更新指针和计数
│ ├─ buf++
│ └─ buf_filled += 8
↓
[6] 所有数据读取完成
↓
[7] 返回 SUCCESS
5.2 源码详解
步骤1-4:初始化
HSAKMT_STATUS HSAKMTAPI hsaKmtPmcQueryTrace(HSATraceId TraceId)
{
struct perf_trace *trace =
(struct perf_trace *)PORT_UINT64_TO_VPTR(TraceId);
uint32_t i, j;
HSAKMT_STATUS ret = HSAKMT_STATUS_SUCCESS;
uint64_t *buf;
uint64_t buf_filled = 0;
// 参数验证
if (TraceId == 0)
return HSAKMT_STATUS_INVALID_PARAMETER;
if (trace->magic4cc != HSA_PERF_MAGIC4CC)
return HSAKMT_STATUS_INVALID_HANDLE;
// 获取缓冲区指针(Start时保存的)
buf = (uint64_t *)trace->buf;
pr_debug("[%s] Trace buffer(%p): ", __func__, buf);
步骤5:读取所有计数器
// 双重循环:块 × 计数器
for (i = 0; i < trace->num_blocks; i++)
for (j = 0; j < trace->blocks[i].num_counters; j++) {
// 检查缓冲区是否有足够空间
buf_filled += sizeof(uint64_t); // 预计增加8字节
if (buf_filled > trace->buf_size)
return HSAKMT_STATUS_NO_MEMORY;
// 读取单个计数器的值
ret = query_trace(trace->blocks[i].perf_event_fd[j], buf);
if (ret != HSAKMT_STATUS_SUCCESS)
return ret;
// 调试输出(可选)
pr_debug("%lu_", *buf);
// 移动到下一个缓冲区位置
buf++;
}
pr_debug("\n");
return HSAKMT_STATUS_SUCCESS;
}
数据布局:
缓冲区内存布局(按注册顺序):
+--------------------+
| Block0_Counter0 | ← buf[0] (8 bytes)
+--------------------+
| Block0_Counter1 | ← buf[1]
+--------------------+
| ... |
+--------------------+
| Block0_CounterN |
+--------------------+
| Block1_Counter0 |
+--------------------+
| Block1_Counter1 |
+--------------------+
| ... |
+--------------------+
| BlockM_CounterK | ← buf[total-1]
+--------------------+
步骤6:query_trace 函数详解
static HSAKMT_STATUS query_trace(int fd, uint64_t *buf)
{
struct perf_counts_values content;
// 验证文件描述符
if (fd < 0)
return HSAKMT_STATUS_ERROR;
// 读取perf_event数据(24字节)
if (readn(fd, &content, sizeof(content)) != sizeof(content))
return HSAKMT_STATUS_ERROR;
// 只返回计数器值(忽略ena和run)
*buf = content.val;
return HSAKMT_STATUS_SUCCESS;
}
readn 函数(健壮的read封装):
static ssize_t readn(int fd, void *buf, size_t n)
{
size_t left = n;
ssize_t bytes;
while (left) {
bytes = read(fd, buf, left);
if (!bytes) // EOF
return (n - left);
if (bytes < 0) {
if (errno == EINTR) // 被信号中断,重试
continue;
else
return -errno; // 真正的错误
}
left -= bytes;
buf = VOID_PTR_ADD(buf, bytes);
}
return n; // 成功读取n字节
}
为什么需要readn?
read()可能返回少于请求的字节数- 处理信号中断(
EINTR) - 确保完整读取24字节的
perf_counts_values
5.3 perf_event 数据读取机制
用户空间 内核空间
│ │
│ read(perf_event_fd) │
├──────────────────────────>│
│ │ 读取硬件计数器寄存器
│ │ 读取时间戳(ena, run)
│ │ 组装 perf_counts_values
│ │
│<──────────────────────────┤ 返回24字节数据
│ │
│ 提取 content.val │
│ │
perf_event 文件描述符特性:
- 每个FD对应一个硬件计数器
- 读取操作是非破坏性的(计数器继续累计)
- 可以多次读取获取最新值
- 读取不会重置计数器
6. hsaKmtPmcStopTrace 实现分析
6.1 完整流程图
hsaKmtPmcStopTrace(TraceId)
↓
[1] 调试日志输出
↓
[2] 验证 TraceId != 0
├─ No → 返回 INVALID_PARAMETER
└─ Yes → 继续
↓
[3] 转换为 perf_trace 指针
↓
[4] 验证魔数
├─ 不匹配 → 返回 INVALID_HANDLE
└─ 匹配 → 继续
↓
[5] 遍历所有块,禁用计数器
│ for (i = 0; i < trace->num_blocks; i++)
│ ↓
│ [5.1] 调用 perf_trace_ioctl(DISABLE)
│ ├─ 失败 → 立即返回错误
│ └─ 成功 → 继续
↓
[6] 所有块禁用成功
↓
[7] 更新状态
├─ trace->state = STOPPED
└─ (保留 buf 和 buf_size)
↓
[8] 返回 SUCCESS
6.2 源码详解
HSAKMT_STATUS HSAKMTAPI hsaKmtPmcStopTrace(HSATraceId TraceId)
{
struct perf_trace *trace =
(struct perf_trace *)PORT_UINT64_TO_VPTR(TraceId);
uint32_t i;
HSAKMT_STATUS ret = HSAKMT_STATUS_SUCCESS;
pr_debug("[%s] Trace ID 0x%lx\n", __func__, TraceId);
// 参数验证
if (TraceId == 0)
return HSAKMT_STATUS_INVALID_PARAMETER;
if (trace->magic4cc != HSA_PERF_MAGIC4CC)
return HSAKMT_STATUS_INVALID_HANDLE;
// 禁用所有块的计数器
for (i = 0; i < trace->num_blocks; i++) {
ret = perf_trace_ioctl(&trace->blocks[i],
PERF_EVENT_IOC_DISABLE);
if (ret != HSAKMT_STATUS_SUCCESS)
return ret; // 遇到错误立即返回
}
// 更新状态为已停止
trace->state = PERF_TRACE_STATE__STOPPED;
return ret;
}
6.3 Stop vs Start 的差异
| 特性 | Start | Stop |
|---|---|---|
| 错误处理 | 需要回滚 | 不需要回滚 |
| 缓冲区 | 保存指针 | 保留指针 |
| 失败影响 | 部分启用会回滚 | 部分禁用仍返回错误 |
| ioctl命令 | PERF_EVENT_IOC_ENABLE | PERF_EVENT_IOC_DISABLE |
为什么Stop不需要回滚?
- 禁用操作失败通常是严重错误(文件描述符损坏等)
- 部分禁用不会造成资源泄漏
- 调用者应该检查返回值并处理异常
6.4 Stop之后的操作
// Stop之后仍可以查询最后的数据
hsaKmtPmcStopTrace(traceId);
hsaKmtPmcQueryTrace(traceId); // 合法:读取停止时刻的值
// 可以重新启动
hsaKmtPmcStartTrace(traceId, buffer, size); // 重新开始
// 或者释放资源
hsaKmtPmcReleaseTraceAccess(nodeId, traceId);
hsaKmtPmcUnregisterTrace(nodeId, traceId);
7. perf_event 子系统集成
7.1 perf_event 生命周期
[创建] perf_event_open()
↓
[配置] 设置event_attr
↓
[启用] ioctl(fd, PERF_EVENT_IOC_ENABLE)
↓ ↑
│ │ 可重复启用/禁用
↓ │
[读取] read(fd, &values, sizeof(values))
↓ ↑
│ │ 多次读取
↓ │
[禁用] ioctl(fd, PERF_EVENT_IOC_DISABLE)
↓
[关闭] close(fd)
7.2 PERF_EVENT_IOC_ENABLE 详解
功能:
- 启动硬件性能计数器
- 从当前值开始累计(不重置)
- 立即生效(内核写入硬件寄存器)
内核操作:
// 内核简化代码(概念性)
case PERF_EVENT_IOC_ENABLE:
// 1. 检查事件状态
if (event->state == PERF_EVENT_STATE_ACTIVE)
return 0; // 已经启用
// 2. 编程硬件计数器
event->pmu->start(event, 0);
// 3. 更新状态
event->state = PERF_EVENT_STATE_ACTIVE;
// 4. 记录启用时间
event->tstamp_enabled = perf_clock();
return 0;
7.3 PERF_EVENT_IOC_DISABLE 详解
功能:
- 停止硬件性能计数器
- 保留当前累计值
- 立即生效
内核操作:
// 内核简化代码(概念性)
case PERF_EVENT_IOC_DISABLE:
// 1. 检查事件状态
if (event->state != PERF_EVENT_STATE_ACTIVE)
return 0; // 已经停止
// 2. 停止硬件计数器
event->pmu->stop(event, PERF_EF_UPDATE);
// 3. 更新状态
event->state = PERF_EVENT_STATE_INACTIVE;
// 4. 记录运行时间
event->total_time_running += now - event->tstamp_running;
return 0;
7.4 read() 操作详解
返回的数据结构:
struct read_format {
u64 value; // 计数器的累计值
u64 time_enabled; // 计数器使能的总时间(纳秒)
u64 time_running; // 计数器实际运行的时间(纳秒)
};
为什么有两个时间?
time_enabled: 从第一次ENABLE到现在的总时间time_running: 实际计数的时间(可能因调度而中断)scaling_factor = time_running / time_enabled
示例:
假设监控一个事件1秒:
- time_enabled = 1,000,000,000 ns (1秒)
- time_running = 800,000,000 ns (0.8秒)
- value = 1000 (观察到1000次事件)
实际发生的事件数估计 = 1000 * (1.0 / 0.8) = 1250次
8. 完整使用示例
8.1 基本监控流程
#include <stdio.h>
#include <stdlib.h>
#include <hsakmt.h>
void basic_profiling_example(HSAuint32 nodeId)
{
HSAKMT_STATUS status;
HsaPmcTraceRoot traceRoot;
HsaCounter counters[4];
void *buffer = NULL;
// 1. 初始化计数器(假设已选择)
// ... 初始化 counters[] ...
// 2. 注册跟踪
status = hsaKmtPmcRegisterTrace(nodeId, 4, counters, &traceRoot);
if (status != HSAKMT_STATUS_SUCCESS) {
fprintf(stderr, "Register failed: %d\n", status);
return;
}
printf("Trace registered: ID=0x%lx\n", traceRoot.TraceId);
printf("Buffer size required: %lu bytes\n",
traceRoot.TraceBufferMinSizeBytes);
// 3. 获取访问权限
status = hsaKmtPmcAcquireTraceAccess(nodeId, traceRoot.TraceId);
if (status != HSAKMT_STATUS_SUCCESS) {
fprintf(stderr, "Acquire failed: %d\n", status);
goto cleanup_unregister;
}
// 4. 分配缓冲区
buffer = malloc(traceRoot.TraceBufferMinSizeBytes);
if (!buffer) {
fprintf(stderr, "Buffer allocation failed\n");
goto cleanup_release;
}
// 5. 启动跟踪 ★
status = hsaKmtPmcStartTrace(traceRoot.TraceId, buffer,
traceRoot.TraceBufferMinSizeBytes);
if (status != HSAKMT_STATUS_SUCCESS) {
fprintf(stderr, "Start failed: %d\n", status);
goto cleanup_free;
}
printf("Tracing started...\n");
// 6. 执行GPU工作负载
run_gpu_kernel();
// 7. 查询数据 ★
status = hsaKmtPmcQueryTrace(traceRoot.TraceId);
if (status != HSAKMT_STATUS_SUCCESS) {
fprintf(stderr, "Query failed: %d\n", status);
goto cleanup_stop;
}
// 8. 显示结果
uint64_t *results = (uint64_t *)buffer;
printf("\nCounter Results:\n");
for (int i = 0; i < 4; i++) {
printf(" Counter %d: %lu\n", i, results[i]);
}
// 9. 停止跟踪 ★
cleanup_stop:
status = hsaKmtPmcStopTrace(traceRoot.TraceId);
if (status != HSAKMT_STATUS_SUCCESS) {
fprintf(stderr, "Stop failed: %d\n", status);
}
cleanup_free:
free(buffer);
cleanup_release:
hsaKmtPmcReleaseTraceAccess(nodeId, traceRoot.TraceId);
cleanup_unregister:
hsaKmtPmcUnregisterTrace(nodeId, traceRoot.TraceId);
}
8.2 多次采样示例
void multi_sample_profiling(HSAuint32 nodeId, HsaCounter *counters, int count)
{
HSAKMT_STATUS status;
HsaPmcTraceRoot traceRoot;
void *buffer;
// 注册和准备
hsaKmtPmcRegisterTrace(nodeId, count, counters, &traceRoot);
hsaKmtPmcAcquireTraceAccess(nodeId, traceRoot.TraceId);
buffer = malloc(traceRoot.TraceBufferMinSizeBytes);
// 启动跟踪
hsaKmtPmcStartTrace(traceRoot.TraceId, buffer,
traceRoot.TraceBufferMinSizeBytes);
printf("Sampling every 100ms for 1 second...\n");
// 多次采样
for (int i = 0; i < 10; i++) {
// 执行一些工作
run_gpu_workload_100ms();
// 查询当前值
status = hsaKmtPmcQueryTrace(traceRoot.TraceId);
if (status != HSAKMT_STATUS_SUCCESS) {
fprintf(stderr, "Query %d failed\n", i);
break;
}
// 显示当前累计值
uint64_t *values = (uint64_t *)buffer;
printf("Sample %d: ", i);
for (int j = 0; j < count; j++) {
printf("%lu ", values[j]);
}
printf("\n");
}
// 停止和清理
hsaKmtPmcStopTrace(traceRoot.TraceId);
free(buffer);
hsaKmtPmcReleaseTraceAccess(nodeId, traceRoot.TraceId);
hsaKmtPmcUnregisterTrace(nodeId, traceRoot.TraceId);
}
8.3 暂停和恢复示例
void pause_resume_profiling(HSAuint32 nodeId)
{
HSAKMT_STATUS status;
HsaPmcTraceRoot traceRoot;
void *buffer;
// 初始化省略...
// 第一阶段:监控kernel1
printf("Phase 1: Profiling kernel1\n");
hsaKmtPmcStartTrace(traceRoot.TraceId, buffer, size);
run_kernel1();
hsaKmtPmcQueryTrace(traceRoot.TraceId);
print_results(buffer, "After kernel1");
hsaKmtPmcStopTrace(traceRoot.TraceId);
// 中间不监控
printf("\nPaused profiling\n");
run_some_other_work(); // 这部分不会被计数
// 第二阶段:继续监控kernel2
printf("\nPhase 2: Profiling kernel2\n");
hsaKmtPmcStartTrace(traceRoot.TraceId, buffer, size); // 重新启动
run_kernel2();
hsaKmtPmcQueryTrace(traceRoot.TraceId);
print_results(buffer, "After kernel2");
hsaKmtPmcStopTrace(traceRoot.TraceId);
// 清理省略...
}
8.4 差分分析示例
void differential_profiling(HSAuint32 nodeId)
{
HSAKMT_STATUS status;
HsaPmcTraceRoot traceRoot;
void *buffer;
uint64_t *values;
uint64_t baseline[16];
// 初始化省略...
values = (uint64_t *)buffer;
// 启动跟踪
hsaKmtPmcStartTrace(traceRoot.TraceId, buffer, size);
// 建立基线
hsaKmtPmcQueryTrace(traceRoot.TraceId);
memcpy(baseline, values, sizeof(baseline));
printf("Baseline established\n");
// 执行感兴趣的操作
run_target_kernel();
// 查询最终值
hsaKmtPmcQueryTrace(traceRoot.TraceId);
// 计算差值
printf("\nDifferential Results:\n");
for (int i = 0; i < counter_count; i++) {
uint64_t delta = values[i] - baseline[i];
printf(" Counter %d: %lu (delta: %lu)\n",
i, values[i], delta);
}
// 停止和清理
hsaKmtPmcStopTrace(traceRoot.TraceId);
// 清理省略...
}
9. 错误处理和调试
9.1 常见错误场景
错误1:缓冲区太小
// 错误示例
void buffer_too_small_error()
{
HsaPmcTraceRoot traceRoot;
// 注册了10个计数器...
void *buffer = malloc(40); // 只有40字节,需要80字节
hsaKmtPmcStartTrace(traceRoot.TraceId, buffer, 40); // 成功
HSAKMT_STATUS status = hsaKmtPmcQueryTrace(traceRoot.TraceId);
// 返回 HSAKMT_STATUS_NO_MEMORY
}
// 正确做法
void buffer_correct_size()
{
HsaPmcTraceRoot traceRoot;
// 注册了10个计数器...
// 使用API返回的大小
void *buffer = malloc(traceRoot.TraceBufferMinSizeBytes);
hsaKmtPmcStartTrace(traceRoot.TraceId, buffer,
traceRoot.TraceBufferMinSizeBytes);
hsaKmtPmcQueryTrace(traceRoot.TraceId); // 成功
}
错误2:在Stop后忘记重新Start
// 错误示例
void forgot_restart_error()
{
hsaKmtPmcStartTrace(traceId, buffer, size);
run_kernel1();
hsaKmtPmcStopTrace(traceId);
// 忘记重新Start
run_kernel2();
hsaKmtPmcQueryTrace(traceId); // 读取的是kernel1的数据
}
// 正确做法
void correct_restart()
{
hsaKmtPmcStartTrace(traceId, buffer, size);
run_kernel1();
hsaKmtPmcStopTrace(traceId);
// 重新启动
hsaKmtPmcStartTrace(traceId, buffer, size);
run_kernel2();
hsaKmtPmcQueryTrace(traceId); // 读取kernel2的数据
}
错误3:多线程竞态条件
// 潜在问题
void thread_race_condition()
{
// Thread 1
hsaKmtPmcStartTrace(traceId, buffer, size);
// Thread 2(同时)
hsaKmtPmcStopTrace(traceId); // 竞态!
}
// 解决方案:使用互斥锁
pthread_mutex_t trace_mutex = PTHREAD_MUTEX_INITIALIZER;
void thread_safe_tracing()
{
pthread_mutex_lock(&trace_mutex);
hsaKmtPmcStartTrace(traceId, buffer, size);
run_kernel();
hsaKmtPmcQueryTrace(traceId);
hsaKmtPmcStopTrace(traceId);
pthread_mutex_unlock(&trace_mutex);
}
9.2 调试技巧
技巧1:添加状态检查
void check_trace_state(struct perf_trace *trace, const char *operation)
{
printf("[%s] State check:\n", operation);
printf(" Magic: 0x%x (expected: 0x%x)\n",
trace->magic4cc, HSA_PERF_MAGIC4CC);
printf(" State: %s\n",
trace->state == PERF_TRACE_STATE__STARTED ? "STARTED" : "STOPPED");
printf(" Blocks: %u\n", trace->num_blocks);
printf(" Buffer: %p\n", trace->buf);
printf(" Buffer size: %lu\n", trace->buf_size);
}
技巧2:验证文件描述符
bool verify_perf_fds(struct perf_trace *trace)
{
for (uint32_t i = 0; i < trace->num_blocks; i++) {
struct perf_trace_block *block = &trace->blocks[i];
for (uint32_t j = 0; j < block->num_counters; j++) {
int fd = block->perf_event_fd[j];
if (fd < 0) {
fprintf(stderr, "Invalid FD: block=%u, counter=%u, fd=%d\n",
i, j, fd);
return false;
}
// 检查FD是否有效(通过fcntl)
if (fcntl(fd, F_GETFD) == -1) {
fprintf(stderr, "FD not open: block=%u, counter=%u, fd=%d\n",
i, j, fd);
return false;
}
}
}
return true;
}
技巧3:日志包装器
#define TRACE_START(id, buf, size) \
do { \
printf("[TRACE] Starting trace 0x%lx\n", id); \
HSAKMT_STATUS __s = hsaKmtPmcStartTrace(id, buf, size); \
printf("[TRACE] Start result: %d\n", __s); \
if (__s != HSAKMT_STATUS_SUCCESS) { \
fprintf(stderr, "[ERROR] Start failed at %s:%d\n", \
__FILE__, __LINE__); \
} \
} while(0)
#define TRACE_QUERY(id) \
do { \
printf("[TRACE] Querying trace 0x%lx\n", id); \
HSAKMT_STATUS __s = hsaKmtPmcQueryTrace(id); \
printf("[TRACE] Query result: %d\n", __s); \
if (__s != HSAKMT_STATUS_SUCCESS) { \
fprintf(stderr, "[ERROR] Query failed at %s:%d\n", \
__FILE__, __LINE__); \
} \
} while(0)
#define TRACE_STOP(id) \
do { \
printf("[TRACE] Stopping trace 0x%lx\n", id); \
HSAKMT_STATUS __s = hsaKmtPmcStopTrace(id); \
printf("[TRACE] Stop result: %d\n", __s); \
if (__s != HSAKMT_STATUS_SUCCESS) { \
fprintf(stderr, "[ERROR] Stop failed at %s:%d\n", \
__FILE__, __LINE__); \
} \
} while(0)
10. 性能考量
10.1 Start/Stop开销
// 测量Start/Stop开销
void measure_overhead()
{
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
hsaKmtPmcStartTrace(traceId, buffer, size);
clock_gettime(CLOCK_MONOTONIC, &end);
long start_ns = (end.tv_sec - start.tv_sec) * 1000000000L +
(end.tv_nsec - start.tv_nsec);
clock_gettime(CLOCK_MONOTONIC, &start);
hsaKmtPmcStopTrace(traceId);
clock_gettime(CLOCK_MONOTONIC, &end);
long stop_ns = (end.tv_sec - start.tv_sec) * 1000000000L +
(end.tv_nsec - start.tv_nsec);
printf("Start overhead: %ld ns\n", start_ns);
printf("Stop overhead: %ld ns\n", stop_ns);
}
// 典型结果(4个计数器):
// Start overhead: ~50-100 μs (包含4次ioctl)
// Stop overhead: ~50-100 μs
10.2 Query开销
// 测量Query开销
void measure_query_overhead(int counter_count)
{
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
hsaKmtPmcQueryTrace(traceId);
clock_gettime(CLOCK_MONOTONIC, &end);
long query_ns = (end.tv_sec - start.tv_sec) * 1000000000L +
(end.tv_nsec - start.tv_nsec);
printf("Query overhead (%d counters): %ld ns (%.2f ns/counter)\n",
counter_count, query_ns, (double)query_ns / counter_count);
}
// 典型结果:
// Query overhead (4 counters): ~5-10 μs (1-2 μs/counter)
// Query overhead (16 counters): ~20-40 μs (1-2 μs/counter)
10.3 优化建议
建议1:批量查询
// 不推荐:频繁查询
for (int i = 0; i < 1000; i++) {
run_small_kernel();
hsaKmtPmcQueryTrace(traceId); // 1000次查询
}
// 推荐:减少查询频率
hsaKmtPmcStartTrace(traceId, buffer, size);
for (int i = 0; i < 1000; i++) {
run_small_kernel();
}
hsaKmtPmcQueryTrace(traceId); // 1次查询
hsaKmtPmcStopTrace(traceId);
建议2:重用TraceId
// 不推荐:每次都重新注册
for (int i = 0; i < 10; i++) {
hsaKmtPmcRegisterTrace(..., &traceRoot);
hsaKmtPmcStartTrace(traceRoot.TraceId, ...);
// ... 监控 ...
hsaKmtPmcStopTrace(traceRoot.TraceId);
hsaKmtPmcUnregisterTrace(..., traceRoot.TraceId);
}
// 推荐:重用TraceId
hsaKmtPmcRegisterTrace(..., &traceRoot);
for (int i = 0; i < 10; i++) {
hsaKmtPmcStartTrace(traceRoot.TraceId, ...);
// ... 监控 ...
hsaKmtPmcStopTrace(traceRoot.TraceId);
}
hsaKmtPmcUnregisterTrace(..., traceRoot.TraceId);
建议3:选择合适的计数器数量
// 平衡需求和开销
int choose_counter_count(ProfileMode mode)
{
switch (mode) {
case PROFILE_MODE_QUICK:
return 4; // 快速分析,低开销
case PROFILE_MODE_NORMAL:
return 8; // 标准分析
case PROFILE_MODE_DETAILED:
return 16; // 详细分析,接受更高开销
default:
return 4;
}
}
11. 高级话题
11.1 与GPU调度的交互
GPU Timeline:
├─ Kernel1 ─┤ ├─ Kernel2 ─┤ ├─ Kernel3 ─┤
Tracing:
Start Stop
├───────────────────────────────────┤
计数器值 = Kernel1 + Kernel2 + Kernel3的累计
注意事项:
- 计数器在GPU空闲时也可能增长(背景活动)
- 多个kernel并发时,计数器累加所有活动
- 需要配合时间戳分析单个kernel
11.2 计数器溢出处理
// 64位计数器溢出需要很长时间
// 但32位计数器可能溢出
bool detect_overflow(uint64_t prev, uint64_t curr, int bits)
{
if (bits == 32) {
uint32_t prev32 = (uint32_t)prev;
uint32_t curr32 = (uint32_t)curr;
if (curr32 < prev32) {
printf("Warning: 32-bit counter overflow detected\n");
return true;
}
}
return false;
}
uint64_t handle_overflow(uint64_t prev, uint64_t curr, int bits)
{
if (bits == 32 && curr < prev) {
// 假设只溢出一次
return curr + (1ULL << 32);
}
return curr;
}
11.3 多GPU支持
void multi_gpu_profiling(int num_gpus)
{
HSATraceId trace_ids[MAX_GPUS];
void *buffers[MAX_GPUS];
// 为每个GPU注册跟踪
for (int i = 0; i < num_gpus; i++) {
HsaPmcTraceRoot traceRoot;
hsaKmtPmcRegisterTrace(i, counter_count, counters, &traceRoot);
trace_ids[i] = traceRoot.TraceId;
buffers[i] = malloc(traceRoot.TraceBufferMinSizeBytes);
}
// 同时启动所有GPU的跟踪
for (int i = 0; i < num_gpus; i++) {
hsaKmtPmcStartTrace(trace_ids[i], buffers[i], buffer_size);
}
// 执行工作负载(可能跨多个GPU)
run_multi_gpu_workload();
// 查询所有GPU的数据
for (int i = 0; i < num_gpus; i++) {
hsaKmtPmcQueryTrace(trace_ids[i]);
printf("GPU %d results:\n", i);
print_results(buffers[i], counter_count);
}
// 停止和清理
for (int i = 0; i < num_gpus; i++) {
hsaKmtPmcStopTrace(trace_ids[i]);
free(buffers[i]);
hsaKmtPmcUnregisterTrace(i, trace_ids[i]);
}
}
12. 常见问题 FAQ
Q1: Start之后立即Query会得到什么数据?
A: 会得到从Start到Query之间的累计值。如果没有执行任何GPU操作,值可能接近0或很小(背景活动)。
Q2: 可以在不Stop的情况下多次Query吗?
A: 可以!每次Query返回从Start到当前时刻的累计值。
hsaKmtPmcStartTrace(...);
run_phase1();
hsaKmtPmcQueryTrace(...); // 得到phase1的值
run_phase2();
hsaKmtPmcQueryTrace(...); // 得到phase1+phase2的累计值
Q3: Stop之后Query会返回什么?
A: 返回Stop时刻的计数器值。Stop不会清零计数器。
Q4: 重新Start会重置计数器吗?
A: 不会!计数器从停止时的值继续累加。如果需要重新开始,需要Unregister并重新Register。
// 第一次运行
hsaKmtPmcStartTrace(...);
run_work();
hsaKmtPmcQueryTrace(...); // 假设得到1000
hsaKmtPmcStopTrace(...);
// 第二次运行
hsaKmtPmcStartTrace(...); // 不会重置
run_work();
hsaKmtPmcQueryTrace(...); // 得到1000 + 新增值
Q5: 如何实现真正的重置?
A: 需要Unregister并重新Register:
// 停止并注销
hsaKmtPmcStopTrace(traceId);
hsaKmtPmcReleaseTraceAccess(nodeId, traceId);
hsaKmtPmcUnregisterTrace(nodeId, traceId);
// 重新注册(计数器归零)
hsaKmtPmcRegisterTrace(nodeId, count, counters, &newTraceRoot);
hsaKmtPmcAcquireTraceAccess(nodeId, newTraceRoot.TraceId);
Q6: Start失败后的状态是什么?
A: 跟踪状态保持STOPPED,所有计数器都未启用。可以重试Start或Unregister。
Q7: 缓冲区可以在Query之间更改吗?
A: 可以,但需要Stop后重新Start:
hsaKmtPmcStartTrace(traceId, buffer1, size);
// ...
hsaKmtPmcStopTrace(traceId);
// 更换缓冲区
hsaKmtPmcStartTrace(traceId, buffer2, size); // 新缓冲区
Q8: 计数器值会减少吗?
A: 正常情况下不会。如果观察到减少,可能是:
- 计数器溢出(32位)
- TraceId被破坏
- 驱动Bug
13. 总结
本文深入探讨了性能跟踪的核心操作:
核心要点:
-
Start操作:
- 启用所有perf_event文件描述符
- 实现错误回滚机制
- 保存缓冲区信息
-
Query操作:
- 从所有计数器读取累计值
- 按注册顺序组织数据
- 支持多次查询
-
Stop操作:
- 禁用所有计数器
- 保留最后的数据
- 允许重新Start
-
perf_event集成:
- 使用ioctl(ENABLE/DISABLE)控制
- 通过read()获取数据
- 支持时间信息(ena/run)
-
最佳实践:
- 合理规划查询频率
- 正确处理错误和回滚
- 注意计数器溢出
- 考虑多GPU场景
使用指南:
- Start前必须Acquire
- Query可多次调用
- Stop后仍可Query
- 重置需要Unregister
在下一篇文章中,我们将通过实际案例演示如何使用这套API进行GPU性能分析,包括内存带宽测量、计算效率分析、缓存命中率统计等实用场景。
777

被折叠的 条评论
为什么被折叠?



