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的分享