Cuda 如何编译PTX代码

Cuda 如何编译PTX代码,cuda,nvcc,ptx,Cuda,Nvcc,Ptx,我需要修改PTX代码并直接编译它。原因是我希望有一些特定的指令紧跟在一起,并且很难编写一个cuda代码来生成我的目标PTX代码,所以我需要直接修改PTX代码。 问题是我可以将它编译成(fatbin和cubin),但我不知道如何将它们(.fatbin和.cubin)编译成“X.o”文件 您可以使用CUDA中的cuModuleLoad*函数在运行时加载cubin或fatbin: 您可以使用它将PTX包含到构建中,尽管该方法有些复杂。例如,将其.cu文件编译为不同体系结构的PTX文件,然后将其转换为包

我需要修改PTX代码并直接编译它。原因是我希望有一些特定的指令紧跟在一起,并且很难编写一个cuda代码来生成我的目标PTX代码,所以我需要直接修改PTX代码。
问题是我可以将它编译成(fatbin和cubin),但我不知道如何将它们(.fatbin和.cubin)编译成“X.o”文件

您可以使用CUDA中的cuModuleLoad*函数在运行时加载cubin或fatbin:


您可以使用它将PTX包含到构建中,尽管该方法有些复杂。例如,将其.cu文件编译为不同体系结构的PTX文件,然后将其转换为包含PTX代码的.h文件作为“C”数组,然后在构建过程中仅从其中一个文件中包含它

通常,在处理cubin或ptx文件时,使用CUDA驱动程序API而不是运行时API;这样,您可以在运行时使用
cuModuleLoadDataEx
手动加载ptx或cubin文件。
如果您想坚持使用运行时API,您需要手动模拟NVCC的功能,但这并没有(完全)文档化。我只是在如何做这件事上找到这个

可能有一种方法可以通过一系列有序的
nvcc
命令来做到这一点,但我没有意识到,也没有发现它

然而,一种可能的方法是中断并重新启动cuda编译序列,并在此期间(在重新启动之前)编辑ptx文件,尽管这种方法很混乱。这是基于提供的信息,我不认为这是一个标准的方法,所以你的里程可能会有所不同。可能有很多我没有考虑过的场景,这些场景不起作用或不可行

为了解释这一点,我将提供一个示例代码:

#include <stdio.h>

__global__ void mykernel(int *data){

  (*data)++;
}

int main(){

  int *d_data, h_data = 0;
  cudaMalloc((void **)&d_data, sizeof(int));
  cudaMemcpy(d_data, &h_data, sizeof(int), cudaMemcpyHostToDevice);
  mykernel<<<1,1>>>(d_data);
  cudaMemcpy(&h_data, d_data, sizeof(int), cudaMemcpyDeviceToHost);
  printf("data = %d\n", h_data);
  return 0;
}
(假设源文件名为t266.cu)

相反,根据参考手册,我们将编译如下:

nvcc -arch=sm_20 -o t266 t266.cu 
nvcc -arch=sm_20 -keep -o t266 t266.cu
这将生成可执行文件,但将保留所有中间文件,包括
t266.ptx
(其中包含
mykernel
的ptx代码)

如果我们只是在此时运行可执行文件,我们将得到如下输出:

$ ./t266
data = 1
$
ptxas  -arch=sm_20 -m64  "t266.ptx"  -o "t266.sm_20.cubin"
fatbinary --create="t266.fatbin" -64 --key="xxxxxxxxxx" --ident="t266.cu" "--image=profile=sm_20,file=t266.sm_20.cubin" "--image=profile=compute_20,file=t266.ptx" --embedded-fatbin="t266.fatbin.c" --cuda
gcc -D__CUDA_ARCH__=200 -E -x c++   -DCUDA_DOUBLE_MATH_FUNCTIONS   -D__CUDA_PREC_DIV -D__CUDA_PREC_SQRT "-I/usr/local/cuda/bin/..//include"   -m64 -o "t266.cu.cpp.ii" "t266.cudafe1.cpp"
gcc -c -x c++ "-I/usr/local/cuda/bin/..//include"   -fpreprocessed -m64 -o "t266.o" "t266.cu.cpp.ii"
nvlink --arch=sm_20 --register-link-binaries="t266_dlink.reg.c" -m64   "-L/usr/local/cuda/bin/..//lib64" "t266.o"  -o "t266_dlink.sm_20.cubin"
fatbinary --create="t266_dlink.fatbin" -64 --key="t266_dlink" --ident="t266.cu " -link "--image=profile=sm_20,file=t266_dlink.sm_20.cubin" --embedded-fatbin="t266_dlink.fatbin.c"
gcc -c -x c++ -DFATBINFILE="\"t266_dlink.fatbin.c\"" -DREGISTERLINKBINARYFILE="\"t266_dlink.reg.c\"" -I. "-I/usr/local/cuda/bin/..//include"   -m64 -o "t266_dlink.o" "/usr/local/cuda/bin/crt/link.stub"
g++ -m64 -o "t266" -Wl,--start-group "t266_dlink.o" "t266.o"   "-L/usr/local/cuda/bin/..//lib64" -lcudart_static  -lrt -lpthread -ldl  -Wl,--end-group
下一步是编辑ptx文件,以进行我们想要的任何更改。在本例中,我们将让内核将2添加到
data
变量中,而不是添加1。相关线路为:

    add.s32         %r2, %r1, 2;
                              ^
                              |
                          change the 1 to a 2 here
现在是混乱的部分。下一步是捕获所有中间编译命令,以便我们可以重新运行其中一些命令:

nvcc -dryrun -arch=sm_20 -o t266 t266.cu --keep 2>dryrun.out
(此处使用
stderr
的linux重定向)。然后,我们要编辑该
dryrun.out
文件,以便:

  • 我们在创建ptx文件后保留所有命令,直到文件结束。创建ptx文件的行很明显,它指定了
    -o“t266.ptx”
  • 我们去掉每行开头的前导
    #$
    ,因此实际上我们正在创建一个脚本
  • 当我执行上述两个步骤时,我得到了如下脚本:

    $ ./t266
    data = 1
    $
    
    ptxas  -arch=sm_20 -m64  "t266.ptx"  -o "t266.sm_20.cubin"
    fatbinary --create="t266.fatbin" -64 --key="xxxxxxxxxx" --ident="t266.cu" "--image=profile=sm_20,file=t266.sm_20.cubin" "--image=profile=compute_20,file=t266.ptx" --embedded-fatbin="t266.fatbin.c" --cuda
    gcc -D__CUDA_ARCH__=200 -E -x c++   -DCUDA_DOUBLE_MATH_FUNCTIONS   -D__CUDA_PREC_DIV -D__CUDA_PREC_SQRT "-I/usr/local/cuda/bin/..//include"   -m64 -o "t266.cu.cpp.ii" "t266.cudafe1.cpp"
    gcc -c -x c++ "-I/usr/local/cuda/bin/..//include"   -fpreprocessed -m64 -o "t266.o" "t266.cu.cpp.ii"
    nvlink --arch=sm_20 --register-link-binaries="t266_dlink.reg.c" -m64   "-L/usr/local/cuda/bin/..//lib64" "t266.o"  -o "t266_dlink.sm_20.cubin"
    fatbinary --create="t266_dlink.fatbin" -64 --key="t266_dlink" --ident="t266.cu " -link "--image=profile=sm_20,file=t266_dlink.sm_20.cubin" --embedded-fatbin="t266_dlink.fatbin.c"
    gcc -c -x c++ -DFATBINFILE="\"t266_dlink.fatbin.c\"" -DREGISTERLINKBINARYFILE="\"t266_dlink.reg.c\"" -I. "-I/usr/local/cuda/bin/..//include"   -m64 -o "t266_dlink.o" "/usr/local/cuda/bin/crt/link.stub"
    g++ -m64 -o "t266" -Wl,--start-group "t266_dlink.o" "t266.o"   "-L/usr/local/cuda/bin/..//lib64" -lcudart_static  -lrt -lpthread -ldl  -Wl,--end-group
    
    最后,执行上面的脚本。(在linux中,您可以使用
    chmod+x dryrun.out
    或类似工具使此脚本文件可执行。)如果您在编辑
    .ptx
    文件时没有犯任何错误,则所有命令都应成功完成,并创建一个新的
    t266
    可执行文件

    运行该文件时,我们观察到:

    $ ./t266
    data = 2
    $
    

    这表明我们的更改是成功的。

    我迟到了,但我确实做到了:获取CUDA fat二进制文件,解析PTX,并在将结果发送给驱动程序以便在GPU上执行之前对其进行修改。您还可以选择打印修改后的PTX。

    这一系列nvcc命令似乎可以实现这一目的。有关更多详细信息,请参阅

    创建要修改的ptx文件

    nvcc file1.cu file2.cu file3.cu -rdc=true --ptx
    
    将ptx文件链接到对象文件中

    nvcc file1.ptx file2.ptx file3.ptx -dlink
    
    我在Windows上这样做,所以它弹出了一个_dlink.obj。正如文档所指出的,主机代码到此为止已被丢弃。跑

    nvcc file1.cu file2.cu file3.cu -rdc=true --compile
    
    创建对象文件。对于Windows,它们将是
    .obj
    ,对于Linux,它们将是
    .o
    。然后创建一个库输出文件

    nvcc file1.obj file2.obj file3.obj a_dlink.obj --lib -o myprogram.lib
    
    然后跑

    nvcc myprogram.lib
    
    在Windows上弹出可执行的
    a.exe
    ,在Linux上弹出可执行的
    a.out
    。此过程也适用于
    cubin
    fatbin
    文件。只需将这些名称替换为
    ptx

    就可以使用NVTRC了-很简单! 扩展@ArtemB的答案:

    nVIDIA提供了一个实时编译(RTC)库。有一个例子说明了它是如何作为CUDA样本的一部分使用的;你可以访问它

    该示例实际上从CUDA代码开始,但中间步骤是将PTX代码创建为纯C字符串(`char*)。从这里开始,基本上就是这样:

    char* ptx;
    size_t ptxSize;
    
    // ... populate ptx and ptxSize somehow ...
    
    CUcontext context;
    CUdevice cuDevice;
    
    // These next few lines simply initialize your work with the CUDA driver,
    // they're not specific to PTX compilation
    cuInit(0);
    cuDeviceGet(&cuDevice, 0); // or some other device on your system
    cuCtxCreate(&context, 0, cuDevice);
    
    // The magic happens here:
    CUmodule module;
    cuModuleLoadDataEx(&module, ptx, 0, 0, 0));
    
    // And here is how you use your compiled PTX
    CUfunction kernel_addr;
    cuModuleGetFunction(&kernel_addr, module, "my_kernel_name");
    cuLaunchKernel(kernel_addr, 
       // launch parameters go here
       // kernel arguments go here
    );
    
    注:

    • 我已经删除了所有错误检查,以免使示例代码杂乱无章,但是请检查代码中的错误
    • 您需要将您的程序链接到NVRTC库-它与主CUDA和CUDA驱动程序库分开。在linux上,它被称为
      libnvrtc.so

    • 此外,还有一个CUDA演示了如何使用驱动程序API加载PTX,并展示了它如何与运行时API进行交互。如果使用PTX相对狭窄,为了实现特定的指令排序,您可能还需要考虑使用内联PTX。有一个CUDA和一个支持。如果您愿意的话,这些方法将允许您完全避免使用驱动程序API。我支持Robert Crovella关于内联PTX的建议。对于中小型代码块,我发现内联PTX通常是实现对生成代码的更多控制的最简单、最轻松的方法(因为PTX是编译的,所以不可能完全控制)。根据您的用例,考虑在您选择的编程语言中编写一个简单的任务专用PTX代码生成器,我已经将它用于我自己的几个项目。