cuda编程笔记(27)-- NVTX的使用

NVTX用于在代码中插入 标记(markers)范围(ranges) ,方便在 Nsight Systems / Nsight Compute / Visual Profiler 等工具中可视化性能分析信息。

下面介绍NVTX的C风格接口,使用时需要#include <nvtx3/nvToolsExt.h>

标记(Marker)

cpp 复制代码
nvtxMarkA(const char* msg);//插入一个简单的标记(ANSI 字符串)
nvtxMarkW(const wchar_t* msg);//插入宽字符标记
nvtxMarkEx(const nvtxEventAttributes_t* eventAttrib);//插入带颜色、类别等属性的标记

范围(Range Push/Pop)

cpp 复制代码
nvtxRangePushA(const char* msg);//开始一个范围(ANSI 字符串描述)
nvtxRangePushW(const wchar_t* msg);//开始一个范围(宽字符)
nvtxRangePushEx(const nvtxEventAttributes_t* eventAttrib);//开始一个带属性的范围
nvtxRangePop();//结束最近的一个范围

命名(Naming)

cpp 复制代码
nvtxNameOsThreadA(uint32_t threadId, const char* name);//给CPU线程命名
nvtxNameCategoryA(uint32_t category, const char* name);//给类别命名

结构体:nvtxEventAttributes_t

用于给标记或范围添加更多信息(颜色、消息、类别等):

cpp 复制代码
typedef struct nvtxEventAttributes_t {
    uint16_t version;           // 必须设为 NVTX_VERSION
    uint16_t size;              // sizeof(nvtxEventAttributes_t)
    uint32_t category;          // 用户定义的类别ID
    uint32_t colorType;         // NVTX_COLOR_ARGB 表示使用 color
    uint32_t color;             // ARGB 颜色值(如 0xFF00FF00 表示绿色)
    uint32_t messageType;       // NVTX_MESSAGE_TYPE_ASCII/WCHAR
    const void* message;        // 消息内容
} nvtxEventAttributes_t;

常见宏定义:

cpp 复制代码
#define NVTX_COLOR_ARGB          1
#define NVTX_MESSAGE_TYPE_ASCII  1
#define NVTX_MESSAGE_TYPE_UNICODE 2

用法示例

简单标记

cpp 复制代码
#include <nvtx3/nvToolsExt.h>

void compute() {
    nvtxMarkA("Start compute kernel");
    // your CUDA kernel launch here
}

在 Nsight Systems 里可以看到一条 "Start compute kernel" 的绿色标记。

范围标记(范围 push/pop)

cpp 复制代码
#include <nvtx3/nvToolsExt.h>

void myFunction() {
    nvtxRangePushA("myFunction");
    // 执行的代码块,也可以有核函数
    nvtxRangePop();
}

会在时间轴中显示一个名为 "myFunction" 的矩形区间。

带颜色的范围

cpp 复制代码
nvtxEventAttributes_t eventAttrib = {0};
eventAttrib.version = NVTX_VERSION;
eventAttrib.size = NVTX_EVENT_ATTRIB_STRUCT_SIZE;
eventAttrib.colorType = NVTX_COLOR_ARGB;
eventAttrib.color = 0xFFFF0000; // 红色
eventAttrib.messageType = NVTX_MESSAGE_TYPE_ASCII;
eventAttrib.message.ascii = "Matrix Multiply";

nvtxRangePushEx(&eventAttrib);
// 调用 CUDA kernel
nvtxRangePop();

给线程命名

cpp 复制代码
#include <thread>
#include <nvtx3/nvToolsExt.h>

void worker() {
    nvtxNameOsThreadA(GetCurrentThreadId(), "WorkerThread");
    nvtxRangePushA("WorkSection");
    // do work
    nvtxRangePop();
}

int main() {
    std::thread t(worker);
    t.join();
}

Nsight 中的效果

在 Nsight Systems/Compute 里,这些标记会被可视化为:

  • 竖线 (来自 nvtxMarkA

  • 彩色区块 (来自 nvtxRangePush/Pop

  • 带标签的线程名称 (来自 nvtxNameOsThreadA

可以清晰看到:

  • 不同阶段的执行时间;

  • 不同线程的任务分布;

  • 哪些 CUDA kernel 对应哪些逻辑区间。

借用nvtx分析任务队列

在上一节cuda编程笔记(26)-- 核函数使用任务队列-CSDN博客,我们实现了任务队列,刚好可以用nvtx标记,然后用nsys去分析一下。

cpp 复制代码
#ifndef __CUDACC__
#define __CUDACC__
#endif
#include <cuda_runtime.h>
#include <cstdio>
#include<nvtx3/nvToolsExt.h>
#include <nvtx3/nvToolsExtCudaRt.h>
struct Task {
    int id;
    int depth;
};

// ------------------- 全局任务队列定义 -------------------
struct TaskQueue {
    Task* tasks;
    int* head; // 消费位置
    int* tail; // 生产位置
};

__device__ TaskQueue gQueue;

// ------------------- 初始化队列 -------------------
__global__ void initQueue(Task* buf, int* head, int* tail) {
    if (threadIdx.x == 0 && blockIdx.x == 0) {
        gQueue.tasks = buf;
        gQueue.head = head;
        gQueue.tail = tail;
        *gQueue.head = 0;
        *gQueue.tail = 0;
    }
}

// ------------------- 设备函数:入队 / 出队 -------------------
__device__ bool fetchTask(Task& out) {
    int idx = atomicAdd(gQueue.head, 1);
    if (idx >= *gQueue.tail) {
        // 没有任务可取,撤销本次取
        atomicSub(gQueue.head, 1);
        return false;
    }
    out = gQueue.tasks[idx];
    return true;
}

__device__ void pushTask(const Task& t) {
    int idx = atomicAdd(gQueue.tail, 1);
    gQueue.tasks[idx] = t;
}

// ------------------- 生产者核函数 -------------------
__global__ void producerKernel(int numTasks) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid < numTasks) {
        Task t{ tid, 0 };
        pushTask(t);
        printf("Producer %d 生产任务 id=%d\n", tid, t.id);
    }
}

// ------------------- 消费者核函数(Block级消费) -------------------
__global__ void consumerKernel() {
    __shared__ Task task;
    __shared__ bool hasTask;

    while (true) {
        if (threadIdx.x == 0)
            hasTask = fetchTask(task);
        __syncthreads();

        if (!hasTask) break; // 队列空,结束

        printf("Consumer Block %d Thread %d 消费任务 id=%d\n",
            blockIdx.x, threadIdx.x, task.id);

        // 模拟执行任务(防止编译器优化掉)
        for (int i = 0; i < 1000; ++i) __threadfence();

        __syncthreads();
    }
}

// ------------------- Host 端 -------------------
int main() {
    const int CAPACITY = 128;
    const int NUM_TASKS = 16;

    Task* d_tasks;
    int* d_head, * d_tail;
    cudaMalloc(&d_tasks, CAPACITY * sizeof(Task));
    cudaMalloc(&d_head, sizeof(int));
    cudaMalloc(&d_tail, sizeof(int));

    // 给 stream 命名(这样在 nsys 的 timeline 上会显示更友好的名字)
    cudaStream_t streamProd, streamCons;
    cudaStreamCreate(&streamProd);
    cudaStreamCreate(&streamCons);
    nvtxNameCudaStreamA(streamProd, "Producer Stream");//给流命名,这个只能在GUI上看到,用命令行分析是不会显示的
    nvtxNameCudaStreamA(streamCons, "Consumer Stream");

    // NVTX: 标记初始化阶段
    nvtxRangePushA("Initialize Queue");
    initQueue<<<1, 1>>>(d_tasks, d_head, d_tail);
    cudaDeviceSynchronize();
    nvtxRangePop();

    // NVTX: 标记生产者阶段(这个 push 标记出现在 host timeline)
    nvtxRangePushA("Producer Phase");
    producerKernel<<<2, 8, 0, streamProd>>>(NUM_TASKS);
    nvtxRangePop();

    // NVTX: 标记消费者阶段
    nvtxRangePushA("Consumer Phase");
    consumerKernel<<<4, 32, 0, streamCons>>>();
    nvtxRangePop();

    // 等待全部完成并给 NVTX 一个同步标记区间(可选)
    nvtxRangePushA("Synchronize");
    cudaDeviceSynchronize();
    nvtxRangePop();

    cudaFree(d_tasks);
    cudaFree(d_head);
    cudaFree(d_tail);
    cudaStreamDestroy(streamProd);
    cudaStreamDestroy(streamCons);
    return 0;
}

由于我这个代码是运行在没有GUI的Linux平台上的,所以没办法用可视化的图片去演示,只能用命令去分析了

我们可以写一个脚本去执行需要运行的命令

bash 复制代码
#!/bin/bash
set -e

# ---------------------- 配置区 ----------------------
EXE_NAME="build/my_cuda_program"
PROFILE_NAME="task_queue_profile"
NSYS_BIN="/usr/local/cuda-12.4/nsight-systems-2023.4.4/bin/nsys"
# ---------------------------------------------------
# 清理旧文件
echo "🧹 清理旧的分析文件..."
rm -f ${PROFILE_NAME}.nsys-rep ${PROFILE_NAME}.sqlite *.csv

# 执行性能分析
echo "🚀 开始 Nsight Systems 分析..."
${NSYS_BIN} profile \
    --stats=true \
    --sample=none \
    --trace=cuda,nvtx,osrt \
    --cuda-memory-usage=true \
    --force-overwrite=true \
    -o ${PROFILE_NAME} \
    ${EXE_NAME}

# 生成 SQLite 数据库
echo "📦 生成 SQLite 数据库..."
${NSYS_BIN} stats ${PROFILE_NAME}.nsys-rep \
    --force-export=true > /dev/null

# 导出 NVTX 区间统计 (CPU 端阶段)
echo "🧠 导出 NVTX 区间统计到 nvtxsum.csv..."
${NSYS_BIN} stats ${PROFILE_NAME}.nsys-rep \
    --report nvtxsum \
    --format csv \
    --force-export=true > nvtxsum.csv

# 导出 GPU kernel 统计 (GPU 端执行)
echo "⚙️ 导出 GPU kernel 统计到 cuda_gpu_kern_sum.csv..."
${NSYS_BIN} stats ${PROFILE_NAME}.nsys-rep \
    --report cuda_gpu_kern_sum \
    --format csv \
    --force-export=true > cuda_gpu_kern_sum.csv

echo "✅ 分析完成!结果文件:"
echo "   - ${PROFILE_NAME}.nsys-rep"
echo "   - ${PROFILE_NAME}.sqlite"
echo "   - nvtxsum.csv"
echo "   - cuda_gpu_kern_sum.csv"

先看一下nvtxsum的时间

bash 复制代码
 Time (%)  Total Time (ns)  Instances   Avg (ns)     Med (ns)    Min (ns)   Max (ns)   StdDev (ns)   Style        Range      
 --------  ---------------  ---------  -----------  -----------  ---------  ---------  -----------  -------  ----------------
     54.3        5,283,469          1  5,283,469.0  5,283,469.0  5,283,469  5,283,469          0.0  PushPop  Initialize Queue
     22.9        2,223,260          1  2,223,260.0  2,223,260.0  2,223,260  2,223,260          0.0  PushPop  Producer Phase  
     22.4        2,179,312          1  2,179,312.0  2,179,312.0  2,179,312  2,179,312          0.0  PushPop  Synchronize     
      0.4           41,517          1     41,517.0     41,517.0     41,517     41,517          0.0  PushPop  Consumer Phase

然后看一下cuda_gpu_kern_sum的时间

cpp 复制代码
 Time (%)  Total Time (ns)  Instances  Avg (ns)   Med (ns)   Min (ns)  Max (ns)  StdDev (ns)               Name              
 --------  ---------------  ---------  ---------  ---------  --------  --------  -----------  -------------------------------
     95.8          953,526          1  953,526.0  953,526.0   953,526   953,526          0.0  consumerKernel()               
      4.0           40,000          1   40,000.0   40,000.0    40,000    40,000          0.0  producerKernel(int)            
      0.2            1,696          1    1,696.0    1,696.0     1,696     1,696          0.0  initQueue(Task *, int *, int *)

可以发现一个问题,在cpu端统计的时间上,初始化队列的时间是最久的,但是在GPU端,初始化的时间又很短,这是为什么?

cpp 复制代码
    // NVTX: 标记初始化阶段
    nvtxRangePushA("Initialize Queue");
    initQueue<<<1, 1>>>(d_tasks, d_head, d_tail);
    cudaDeviceSynchronize();
    nvtxRangePop();

因为我们在这个range里调用了主机端的同步等待。这里必须得等,因为我们的生产和消费的核函数是放到其他流里去执行了,而初始化是在默认流,所以必须等这里初始化好,所以cpu端耗费了很多时间。实际上GPU上初始化很快就完成了

C++风格的nvtx

对于简单的PushA和Pop,nvtx的C++风格提供了RAII的写法。

cpp 复制代码
#ifndef __CUDACC__
#define __CUDACC__
#endif
#include <cuda_runtime.h>
#include <cstdio>
#include <nvtx3/nvToolsExt.h>
#include <nvtx3/nvToolsExtCudaRt.h>
#include <nvtx3/nvtx3.hpp>

struct Task {
    int id;
    int depth;
};

// ------------------- 全局任务队列 -------------------
struct TaskQueue {
    Task* tasks;
    int* head;
    int* tail;
};

__device__ TaskQueue gQueue;

// ------------------- 初始化队列 -------------------
__global__ void initQueue(Task* buf, int* head, int* tail) {
    if (threadIdx.x == 0 && blockIdx.x == 0) {
        gQueue.tasks = buf;
        gQueue.head = head;
        gQueue.tail = tail;
        *gQueue.head = 0;
        *gQueue.tail = 0;
    }
}

// ------------------- 设备函数:入队 / 出队 -------------------
__device__ bool fetchTask(Task& out) {
    int idx = atomicAdd(gQueue.head, 1);
    if (idx >= *gQueue.tail) {
        atomicSub(gQueue.head, 1);
        return false;
    }
    out = gQueue.tasks[idx];
    return true;
}

__device__ void pushTask(const Task& t) {
    int idx = atomicAdd(gQueue.tail, 1);
    gQueue.tasks[idx] = t;
}

// ------------------- 生产者核函数 -------------------
__global__ void producerKernel(int numTasks) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid < numTasks) {
        Task t{ tid, 0 };
        pushTask(t);
        printf("Producer %d 生产任务 id=%d\n", tid, t.id);
    }
}

// ------------------- 消费者核函数 -------------------
__global__ void consumerKernel() {
    __shared__ Task task;
    __shared__ bool hasTask;

    while (true) {
        if (threadIdx.x == 0)
            hasTask = fetchTask(task);
        __syncthreads();

        if (!hasTask) break;

        printf("Consumer Block %d Thread %d 消费任务 id=%d\n",
               blockIdx.x, threadIdx.x, task.id);

        for (int i = 0; i < 1000; ++i) __threadfence();
        __syncthreads();
    }
}

// ------------------- Host 端 -------------------
int main() {
    const int CAPACITY = 128;
    const int NUM_TASKS = 16;

    Task* d_tasks;
    int* d_head;
    int* d_tail;
    cudaMalloc(&d_tasks, CAPACITY * sizeof(Task));
    cudaMalloc(&d_head, sizeof(int));
    cudaMalloc(&d_tail, sizeof(int));

    // ------------------- CUDA Stream -------------------
    cudaStream_t streamProd, streamCons;
    cudaStreamCreate(&streamProd);
    cudaStreamCreate(&streamCons);
    nvtxNameCudaStreamA(streamProd, "Producer Stream");//给流命名,这个只能在GUI上看到,用命令行分析是不会显示的
    nvtxNameCudaStreamA(streamCons, "Consumer Stream");
    // ------------------- 初始化队列 -------------------
    {
        // RAII Host timeline
        nvtx3::scoped_range r{"Initialize Queue"};
        initQueue<<<1, 1>>>(d_tasks, d_head, d_tail);
    }

    // ------------------- 生产者阶段 -------------------
    {
        nvtx3::scoped_range r{"Producer Phase"};  // Host timeline
        producerKernel<<<2, 8, 0, streamProd>>>(NUM_TASKS);
    }

    // ------------------- 消费者阶段 -------------------
    {
        nvtx3::scoped_range r{"Consumer Phase"};  // Host timeline
        consumerKernel<<<4, 32, 0, streamCons>>>();
    }

    // ------------------- 同步阶段 -------------------
    {
        nvtx3::scoped_range r{"Synchronize"};     // Host timeline
        cudaDeviceSynchronize();
    }

    cudaFree(d_tasks);
    cudaFree(d_head);
    cudaFree(d_tail);
    cudaStreamDestroy(streamProd);
    cudaStreamDestroy(streamCons);

    return 0;
}

一般下载的cuda是不会提供nvtx3.hpp的,如果想要完整的nvtx3文件夹(包括nvtx3.hpp),可以去官方github下载项目:GitHub - NVIDIA/NVTX: The NVIDIA® Tools Extension SDK (NVTX) is a C-based Application Programming Interface (API) for annotating events, code ranges, and resources in your applications.

如果只是想要头文件,也可以通过我的网盘下载:

通过网盘分享的文件:nvtx3.tar

链接: https://pan.baidu.com/s/1s64AO2f_l_J-80jxyu-Rgg?pwd=oopp 提取码: oopp 复制这段内容后打开百度网盘手机App,操作更方便哦

--来自百度网盘超级会员v6的分享

相关推荐
今天只学一颗糖3 小时前
Linux学习笔记--查询_唤醒方式读取输入数据
笔记·学习
Kay_Liang3 小时前
数据仓库入门:从超市小票看懂数仓
数据仓库·笔记·数据分析
汇能感知12 小时前
光谱相机的探测器阵列
经验分享·笔记·科技
CHHC188013 小时前
vSIM / SoftSIM笔记
笔记
逆小舟14 小时前
【C/C++】指针
c语言·c++·笔记·学习
Cathy Bryant19 小时前
球极平面投影
经验分享·笔记·数学建模
Larry_Yanan20 小时前
QML学习笔记(三十一)QML的Flow定位器
java·前端·javascript·笔记·qt·学习·ui
The_Killer.20 小时前
近世代数(抽象代数)详细笔记--环(也有域的相关内容)
笔记·学习·抽象代数·
Larry_Yanan20 小时前
QML学习笔记(三十)QML的布局器(Layouts)
c++·笔记·qt·学习·ui