ROCm rocr-libhsakmt性能跟踪与分析系列10-5:跟踪启动、数据采集与停止

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_PARAMETERTraceId为0或缓冲区参数无效
HSAKMT_STATUS_INVALID_HANDLETraceId无效(魔数校验失败)
HSAKMT_STATUS_ERRORioctl调用失败
HSAKMT_STATUS_UNAVAILABLEperf_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: 计数器实际运行的时间(纳秒)
  • 使用 enarun 可以计算计数器的活跃度: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 的差异

特性StartStop
错误处理需要回滚不需要回滚
缓冲区保存指针保留指针
失败影响部分启用会回滚部分禁用仍返回错误
ioctl命令PERF_EVENT_IOC_ENABLEPERF_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. 总结

本文深入探讨了性能跟踪的核心操作:

核心要点

  1. Start操作

    • 启用所有perf_event文件描述符
    • 实现错误回滚机制
    • 保存缓冲区信息
  2. Query操作

    • 从所有计数器读取累计值
    • 按注册顺序组织数据
    • 支持多次查询
  3. Stop操作

    • 禁用所有计数器
    • 保留最后的数据
    • 允许重新Start
  4. perf_event集成

    • 使用ioctl(ENABLE/DISABLE)控制
    • 通过read()获取数据
    • 支持时间信息(ena/run)
  5. 最佳实践

    • 合理规划查询频率
    • 正确处理错误和回滚
    • 注意计数器溢出
    • 考虑多GPU场景

使用指南

  • Start前必须Acquire
  • Query可多次调用
  • Stop后仍可Query
  • 重置需要Unregister

在下一篇文章中,我们将通过实际案例演示如何使用这套API进行GPU性能分析,包括内存带宽测量、计算效率分析、缓存命中率统计等实用场景。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeeplyMind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值