零、前言:
此demo的Github仓库(持续更新):github.com/Conqueror71…
📕欢迎访问:
个人博客:conqueror712.github.io/
知乎:www.zhihu.com/people/soeu…
Bilibili:space.bilibili.com/57089326
掘金:juejin.cn/user/129787…
有任何疏忽和错误欢迎各位读者指出!
一、ltrace & nvprof的力所不能及
书接上文,我们分析了一段.cu
代码,但是只输出了add函数的性能分析,这显然不是我们想要的。
如果想分析一下CUDA内置的一些函数,例如cudaDeviceSynchronize
或者cudaMallocManaged
,这时候就不能用ltrace或nvprof了。
ltrace和nvprof主要是用于跟踪和分析函数调用和GPU操作,而不是CUDA内置函数的性能分析。
因此,如果想分析CUDA内置函数的性能,NVVP或Nsight Systems是不错的选择。这些工具可以提供更详细和全面的性能分析,包括GPU硬件指标、内存访问模式等。
不过,我们当前的目的并不是基于什么工具来做,而是直接做一个工具,用来进行CUDA程序的调试。
二、重新编写CUDA函数的实现
在这里,我们可以用Rust来实现,当然也可以用C/C++来实现,我们不妨就先使用Rust来测试,毕竟Rust的实现是有demo的。
1 Rust的cuGetProcAddress_v2实现
参考代码预览:
#[no_mangle]
pub unsafe extern "C" fn cuGetProcAddress_v2(
symbol: *const c_char,
pfn: *mut *mut c_void,
cudaVersion: c_int,
flags: cuuint64_t,
status: *mut CUdriverProcAddressQueryResult,
) -> CUresult {
let lookup: libloading::Symbol CUresult,
> = LIBCUDA.get(b"cuGetProcAddress_v2").unwrap();
let res = lookup(symbol, pfn, cudaVersion, flags, status);
let symbol = CStr::from_ptr(symbol);
TABEL
.lock()
.unwrap()
.insert((symbol.into(), cudaVersion, flags), *pfn as _);
match (symbol.to_str().unwrap(), cudaVersion, flags) {
("cuInit", 2000, 0) => {
*pfn = cuInit as _;
}
("cuDeviceGetCount", 2000, 0) => {
*pfn = cuDeviceGetCount as _;
}
("cuDeviceGet", 2000, 0) => {
*pfn = cuDeviceGet as _;
}
("cuDeviceGetName", 2000, 0) => {
*pfn = cuDeviceGetName as _;
}
("cuDeviceGetAttribute", 2000, 0) => {
*pfn = cuDeviceGetAttribute as _;
}
("cuDeviceTotalMem", 3020, 0) => {
*pfn = cuDeviceTotalMem_v2 as _;
}
("cuGetProcAddress", _, 0) => {
*pfn = cuGetProcAddress_v2 as _;
}
("cuGetExportTable", 3000, 0) => {
*pfn = cuGetExportTable as _;
}
_ => {
eprintln!(
"cuGetProcAddress_v2({:?}, {:?}, {}, {}, {:?}) -> {:?}",
symbol,
pfn.as_ref(),
cudaVersion,
flags,
status.as_ref(),
res
);
}
}
res
}
以上代码中使用了#[no_mangle]
属性,该属性是Rust语言的标准属性之一,用于控制Rust编译器生成的符号名是否进行名称重整mangle。这个属性的作用是告诉Rust编译器不要对这个函数的符号名进行重整,以便在调用这个函数时能够正确地链接到它的实现。
代码中还使用了libloading
库来动态加载CUDA库中的函数,并将它们插入到一个哈希表中。这个哈希表是用来存储已经加载的函数指针,以便在后续的调用中能够快速地查找到它们。
至于match
表达式,是用来来匹配symbol
、cudaVersion
和flags
这三个参数的值,根据不同的参数值来设置不同的函数指针。这个match
表达式是为了在实现cuGetProcAddress_v2
函数时,模拟CUDA库中的实现,以便在调用这个函数时能够正确地返回相应的函数指针。
当然,其他的CUDA函数我们没有给出,但也是如法炮制。
2 整体逻辑:
随便在一个地方写一个程序,里面调用了cuGetProcAddress_v2
这个函数;
LD_PRELOAD
环境变量就会把正在执行的程序拦截下来,就在调用之前拦截,然后移花接木到我们自己的实现上去;
这期间会把原本要传入真正函数的参数也给传到我们自己的实现里面去,调用完成之后返回的结果会假装成原本的函数返回的结果,继续程序的执行。
三、配置环境
操作系统:Ubuntu20.04
1 克隆仓库
git clone git@github.com:NickCao/cumu.git
2 安装Rust & Cargo
curl https://sh.rustup.rs -sSf | sh
3 添加config文件配置镜像源
具体位置在/home/.cargo/config,配置文件example如下:
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 指定镜像
replace-with = 'tuna'
# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
4 下载安装CUDA
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda --fix-missing
5 配置环境变量
export PATH=/usr/local/cuda-12.2/bin${PATH:+:${PATH}}
export LD_LIBRARY_PATH=/usr/local/cuda-12.2/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
export LD_LIBRARY_PATH=/usr/local/cuda-12.2/lib64:$LD_LIBRARY_PATH
export PATH=/usr/local/cuda-12.2/bin:$PATH
记得配完之后在nano里面写入,否则下次开机就没了。
6 常见问题
-
如果warning,那可能是网络的问题;
-
如果error,那可能需要
sudo apt-get install libclang-dev
安装之后检查一下
ldconfig -p | grep libclang
再设置一个环境变量
export LIBCLANG_PATH=/usr/lib/x86_64-linux-gnu/
-
如果缺少cuda.h,
export BINDGEN_EXTRA_CLANG_ARGS="-I /usr/local/cuda-12.2/include"
还报下面的错就是CUDA版本的问题,要CUDA12以上。
四、Cargo构建项目
在根目录下执行:cargo build --release
最后出现如下图所示情况即视为成功。
另外,还有一点我们要明确,重定义(redefinition)和重载(overloading)是两个不同的概念:
- 重定义指的是在同一作用域内,使用相同的标识符(如变量名、函数名等)进行多次定义的情况。例如,在同一个函数中定义同名的变量,或者在同一个命名空间中定义同名的函数。
- 重载指的是在同一作用域内,使用相同的函数名但参数个数或类型不同的多个函数的情况。例如,在 C++ 中,可以定义多个名为
print
的函数,但是它们有不同的参数类型和个数。
FIN