如何在给定 PTX 文件中为 运行 内核创建可执行文件?

How can I create an executable to run a kernel in a given PTX file?

据我所知,您需要一个主机代码(针对 CPU)和一个设备代码(针对 GPU),没有它们您就无法 运行 GPU 上的东西。

我正在学习 PTX ISA,但我不知道如何在 Windows 上执行它。我需要一个 .cu 文件来 运行 它还是有另一种方法来 运行 它?

TL;DR:

How can I assemble .ptx file and host code file and make a executable file?

您使用CUDA driver API. Relevant sample codes are vectorAddDrv (or perhaps any other driver API sample code) as well as ptxjit.

Do I need a .cu file to run it or is there another way to run it?

如果您从 PTX 格式的设备代码开始,则不需要 .cu 文件(也不需要 nvcc)来使用驱动程序 API 方法。

详情:

这个答案的其余部分并不是驱动程序 API 编程的教程(使用已经给出的参考文献和 API 参考手册 here), nor is it intended to be a tutorial on PTX programming. For PTX programming I refer you to the PTX documentation.

首先,我们需要一个适当的 PTX 内核定义。 (为此,我不会编写自己的内核 PTX 代码,而是使用 CUDA 11.1 工具包中 vectorAddDrv 示例代码中的代码,通过 nvcc -ptx vectorAdd_kernel.cu):

vectorAdd_kernel.ptx:

.version 7.1
.target sm_52
.address_size 64

        // .globl       VecAdd_kernel

.visible .entry VecAdd_kernel(
        .param .u64 VecAdd_kernel_param_0,
        .param .u64 VecAdd_kernel_param_1,
        .param .u64 VecAdd_kernel_param_2,
        .param .u32 VecAdd_kernel_param_3
)
{
        .reg .pred      %p<2>;
        .reg .f32       %f<4>;
        .reg .b32       %r<6>;
        .reg .b64       %rd<11>;


        ld.param.u64    %rd1, [VecAdd_kernel_param_0];
        ld.param.u64    %rd2, [VecAdd_kernel_param_1];
        ld.param.u64    %rd3, [VecAdd_kernel_param_2];
        ld.param.u32    %r2, [VecAdd_kernel_param_3];
        mov.u32         %r3, %ntid.x;
        mov.u32         %r4, %ctaid.x;
        mov.u32         %r5, %tid.x;
        mad.lo.s32      %r1, %r3, %r4, %r5;
        setp.ge.s32     %p1, %r1, %r2;
        @%p1 bra        $L__BB0_2;

        cvta.to.global.u64      %rd4, %rd1;
        mul.wide.s32    %rd5, %r1, 4;
        add.s64         %rd6, %rd4, %rd5;
        cvta.to.global.u64      %rd7, %rd2;
        add.s64         %rd8, %rd7, %rd5;
        ld.global.f32   %f1, [%rd8];
        ld.global.f32   %f2, [%rd6];
        add.f32         %f3, %f2, %f1;
        cvta.to.global.u64      %rd9, %rd3;
        add.s64         %rd10, %rd9, %rd5;
        st.global.f32   [%rd10], %f3;

$L__BB0_2:
        ret;

}

我们还需要一个驱动程序 API C++ 源代码文件来完成所有主机端工作以加载并启动该内核。我将再次使用 vectorAddDrv 示例项目(.cpp 文件)中的源代码,并进行修改以加载 PTX 而不是 fatbin:

vectorAddDrv.cpp:

// Vector addition: C = A + B.

// Includes
#include <stdio.h>
#include <string>
#include <iostream>
#include <cstring>
#include <fstream>
#include <streambuf>
#include <cuda.h>
#include <cmath>
#include <vector>

#define CHK(X) if ((err = X) != CUDA_SUCCESS) printf("CUDA error %d at %d\n", (int)err, __LINE__)

// Variables
CUdevice cuDevice;
CUcontext cuContext;
CUmodule cuModule;
CUfunction vecAdd_kernel;
CUresult err;
CUdeviceptr d_A;
CUdeviceptr d_B;
CUdeviceptr d_C;

// Host code
int main(int argc, char **argv)
{
    printf("Vector Addition (Driver API)\n");
    int N = 50000, devID = 0;
    size_t  size = N * sizeof(float);

    // Initialize
    CHK(cuInit(0));
    CHK(cuDeviceGet(&cuDevice, devID));
    // Create context
    CHK(cuCtxCreate(&cuContext, 0, cuDevice));
    // Load PTX file
    std::ifstream my_file("vectorAdd_kernel.ptx");
    std::string my_ptx((std::istreambuf_iterator<char>(my_file)), std::istreambuf_iterator<char>());
    // Create module from PTX
    CHK(cuModuleLoadData(&cuModule, my_ptx.c_str()));

    // Get function handle from module
    CHK(cuModuleGetFunction(&vecAdd_kernel, cuModule, "VecAdd_kernel"));

    // Allocate/initialize vectors in host memory
    std::vector<float> h_A(N, 1.0f);
    std::vector<float> h_B(N, 2.0f);
    std::vector<float> h_C(N);

    // Allocate vectors in device memory
    CHK(cuMemAlloc(&d_A, size));
    CHK(cuMemAlloc(&d_B, size));
    CHK(cuMemAlloc(&d_C, size));

    // Copy vectors from host memory to device memory
    CHK(cuMemcpyHtoD(d_A, h_A.data(), size));
    CHK(cuMemcpyHtoD(d_B, h_B.data(), size));

    // Grid/Block configuration
    int threadsPerBlock = 256;
    int blocksPerGrid   = (N + threadsPerBlock - 1) / threadsPerBlock;

    void *args[] = { &d_A, &d_B, &d_C, &N };

    // Launch the CUDA kernel
    CHK(cuLaunchKernel(vecAdd_kernel,  blocksPerGrid, 1, 1,
                               threadsPerBlock, 1, 1,
                               0,
                               NULL, args, NULL));

    // Copy result from device memory to host memory
    // h_C contains the result in host memory
    CHK(cuMemcpyDtoH(h_C.data(), d_C, size));

    // Verify result
    for (int i = 0; i < N; ++i)
    {
        float sum = h_A[i] + h_B[i];
        if (fabs(h_C[i] - sum) > 1e-7f)
        {
            printf("mismatch!");
            break;
        }
    }
    return 0;
}

(请注意,我已经删除了各种项目,例如释放调用。这是为了演示整体方法;上面的代码只是一个演示。)

在 Linux:

我们可以编译运行代码如下:

$ g++ vectorAddDrv.cpp -o vectorAddDrv -I/usr/local/cuda/include -L/usr/local/cuda/lib64 -lcuda
$ ./vectorAddDrv
Vector Addition (Driver API)
$

在 Windows/Visual Studio 中:在 visual studio 中创建一个新的 C++ 项目。将上面的.cpp 文件添加到项目中。确保 vectorAdd_kernel.ptx 文件与构建的可执行文件位于同一目录中。您还需要修改项目定义以指向 CUDA 包含文件和 CUDA 库文件的位置。这是我在 VS2019 中所做的:

  1. 文件...新建...项目...控制台应用程序...创建
  2. 用上面的 .cpp 文件内容替换给定 .cpp 文件的内容
  3. 将项目目标更改为 x64
  4. 在项目中...属性
  • 将平台更改为 x64
  • 在配置属性中...C/C++...常规...附加包含目录,将路径添加到 CUDA 工具包包含目录,在我的机器上是 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.1\include
  • 在配置属性...Linker...General...Additional Library Directories中,添加CUDA工具包库目录的路径,在我的机器上是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.1\lib\x64
  • 在配置属性...链接器...输入...附加依赖项中,添加 cuda.lib 文件(用于驱动程序 API 库)
  1. 保存项目属性,然后执行构建....重建
  2. 从控制台输出中,找到构建的可执行文件的位置。确保 vectorAdd_kernel.ptx 文件在该目录中,并且 运行 该目录中的可执行文件。 (即打开命令提示符。更改到该目录。运行 来自命令提示符的应用程序)

注意:如果您没有使用 CUDA 11.1 工具包或更新版本,或者如果您 运行正在使用计算能力为 5.0 或更低的 GPU,则上述 PTX 代码将不起作用,因此此示例将不会逐字工作。不过整体的方法是可以的,这个问题不是PTX代码怎么写。

编辑:回答评论中的问题:

What if you wanted the binary to not have to build anything at runtime? i.e. assemble the PTX and stick it in a binary with the compiled host-side code?

我不知道 NVIDIA 工具链提供了执行此操作的方法。从我的角度来看,创建这些统一的二进制文件几乎是 domain of the runtime API

然而,从上面示例中的驱动程序 API 流程可以看出,基本过程似乎很明显:我们是从 .cubin 还是 .ptx 开始文件,无论哪种方式,文件都被加载到一个字符串中,然后该字符串被传递给 cuModuleLoad()。因此,使用实用程序从 .cubin 二进制文件构建字符串,然后将其合并到构建过程中似乎并不困难。

我真的只是在这里闲逛,您应该自行承担使用它的风险,并且可能有许多我没有考虑过的因素。这部分我将在 linux 上进行演示。这是该实用程序的源代码和构建示例:

$ cat f2s.cpp
// Includes
#include <stdio.h>
#include <string>
#include <iostream>
#include <cstring>
#include <fstream>
#include <streambuf>

int main(int argc, char **argv)
{
    std::ifstream my_file("vectorAdd_kernel.cubin");
    std::string my_bin((std::istreambuf_iterator<char>(my_file)), std::istreambuf_iterator<char>());
    std::cout << "unsigned char my_bin[] = {";
    for (int i = 0; i < my_bin.length()-1; i++) std::cout << (int)(unsigned char)my_bin[i] << ",";
    std::cout << (int)(unsigned char)my_bin[my_bin.length()-1] << "};";
    return 0;
}
$ g++ f2s.cpp -o f2s
$

这里的下一步是创建一个 .cubin 文件以供使用。在上面的示例中,我通过 nvcc -ptx vectorAdd_kernel.cu 创建了 ptx 文件。我们可以将其更改为 nvcc -cubin vectorAdd_kernel.cu 或者您可以使用任何您喜欢的方法来生成 .cubin 文件。

创建 cubin 文件后,我们需要将其转换为可以纳入我们的 C++ 代码构建过程的内容。这就是 f2s 实用程序的目的。你会像这样使用它:

./f2s > my_bin.h

(可能允许 f2s 实用程序接受输入文件名作为命令行参数会很好。练习留给 reader。这仅适用于 demonstration/amusement .)

创建完上面的头文件后,我们需要修改我们的.cpp文件如下:

$ cat vectorAddDrv_bin.cpp
// Vector addition: C = A + B.

// Includes
#include <stdio.h>
#include <string>
#include <iostream>
#include <cstring>
#include <fstream>
#include <streambuf>
#include <cuda.h>
#include <cmath>
#include <vector>
#include <my_bin.h>
#define CHK(X) if ((err = X) != CUDA_SUCCESS) printf("CUDA error %d at %d\n", (int)err, __LINE__)

// Variables
CUdevice cuDevice;
CUcontext cuContext;
CUmodule cuModule;
CUfunction vecAdd_kernel;
CUresult err;
CUdeviceptr d_A;
CUdeviceptr d_B;
CUdeviceptr d_C;

// Host code
int main(int argc, char **argv)
{
    printf("Vector Addition (Driver API)\n");
    int N = 50000, devID = 0;
    size_t  size = N * sizeof(float);

    // Initialize
    CHK(cuInit(0));
    CHK(cuDeviceGet(&cuDevice, devID));
    // Create context
    CHK(cuCtxCreate(&cuContext, 0, cuDevice));
    // Create module from "binary string"
    CHK(cuModuleLoadData(&cuModule, my_bin));

    // Get function handle from module
    CHK(cuModuleGetFunction(&vecAdd_kernel, cuModule, "VecAdd_kernel"));

    // Allocate/initialize vectors in host memory
    std::vector<float> h_A(N, 1.0f);
    std::vector<float> h_B(N, 2.0f);
    std::vector<float> h_C(N);

    // Allocate vectors in device memory
    CHK(cuMemAlloc(&d_A, size));
    CHK(cuMemAlloc(&d_B, size));
    CHK(cuMemAlloc(&d_C, size));

    // Copy vectors from host memory to device memory
    CHK(cuMemcpyHtoD(d_A, h_A.data(), size));
    CHK(cuMemcpyHtoD(d_B, h_B.data(), size));

    // Grid/Block configuration
    int threadsPerBlock = 256;
    int blocksPerGrid   = (N + threadsPerBlock - 1) / threadsPerBlock;

    void *args[] = { &d_A, &d_B, &d_C, &N };

    // Launch the CUDA kernel
    CHK(cuLaunchKernel(vecAdd_kernel,  blocksPerGrid, 1, 1,
                               threadsPerBlock, 1, 1,
                               0,
                               NULL, args, NULL));

    // Copy result from device memory to host memory
    // h_C contains the result in host memory
    CHK(cuMemcpyDtoH(h_C.data(), d_C, size));

    // Verify result
    for (int i = 0; i < N; ++i)
    {
        float sum = h_A[i] + h_B[i];
        if (fabs(h_C[i] - sum) > 1e-7f)
        {
         printf("mismatch!");
            break;
        }
    }
    return 0;
}
$ g++ vectorAddDrv_bin.cpp -o vectorAddDrv_bin -I/usr/local/cuda/include -L/usr/local/cuda/lib64 -lcuda -I.
$ ./vectorAddDrv_bin
Vector Addition (Driver API)
$

这似乎有效。 YMMV。为了进一步娱乐,这种方法似乎创造了一种混淆形式:

$ cuobjdump -sass vectorAdd_kernel.cubin

     code for sm_52
       Function : VecAdd_kernel
     .headerflags    @"EF_CUDA_SM52 EF_CUDA_PTX_SM(EF_CUDA_SM52)"
                                                                                 /* 0x001cfc00e22007f6 */
        /*0008*/                   MOV R1, c[0x0][0x20] ;                        /* 0x4c98078000870001 */
        /*0010*/                   S2R R0, SR_CTAID.X ;                          /* 0xf0c8000002570000 */
        /*0018*/                   S2R R2, SR_TID.X ;                            /* 0xf0c8000002170002 */
                                                                                 /* 0x001fd842fec20ff1 */
        /*0028*/                   XMAD.MRG R3, R0.reuse, c[0x0] [0x8].H1, RZ ;  /* 0x4f107f8000270003 */
        /*0030*/                   XMAD R2, R0.reuse, c[0x0] [0x8], R2 ;         /* 0x4e00010000270002 */
        /*0038*/                   XMAD.PSL.CBCC R0, R0.H1, R3.H1, R2 ;          /* 0x5b30011800370000 */
                                                                                 /* 0x001ff400fd4007ed */
        /*0048*/                   ISETP.GE.AND P0, PT, R0, c[0x0][0x158], PT ;  /* 0x4b6d038005670007 */
        /*0050*/                   NOP ;                                         /* 0x50b0000000070f00 */
        /*0058*/               @P0 EXIT ;                                        /* 0xe30000000000000f */
                                                                                 /* 0x081fd800fea207f1 */
        /*0068*/                   SHL R6, R0.reuse, 0x2 ;                       /* 0x3848000000270006 */
        /*0070*/                   SHR R0, R0, 0x1e ;                            /* 0x3829000001e70000 */
        /*0078*/                   IADD R4.CC, R6.reuse, c[0x0][0x140] ;         /* 0x4c10800005070604 */
                                                                                 /* 0x001fd800fe0207f2 */
        /*0088*/                   IADD.X R5, R0.reuse, c[0x0][0x144] ;          /* 0x4c10080005170005 */
        /*0090*/         {         IADD R2.CC, R6, c[0x0][0x148] ;               /* 0x4c10800005270602 */
        /*0098*/                   LDG.E R4, [R4]         }
                                                                                 /* 0xeed4200000070404 */
                                                                                 /* 0x001fd800f62007e2 */
        /*00a8*/                   IADD.X R3, R0, c[0x0][0x14c] ;                /* 0x4c10080005370003 */
        /*00b0*/                   LDG.E R2, [R2] ;                              /* 0xeed4200000070202 */
        /*00b8*/                   IADD R6.CC, R6, c[0x0][0x150] ;               /* 0x4c10800005470606 */
                                                                                 /* 0x001fc420fe4007f7 */
        /*00c8*/                   IADD.X R7, R0, c[0x0][0x154] ;                /* 0x4c10080005570007 */
        /*00d0*/                   FADD R0, R2, R4 ;                             /* 0x5c58000000470200 */
        /*00d8*/                   STG.E [R6], R0 ;                              /* 0xeedc200000070600 */
                                                                                 /* 0x001ffc00ffe007ea */
        /*00e8*/                   NOP ;                                         /* 0x50b0000000070f00 */
        /*00f0*/                   EXIT ;                                        /* 0xe30000000007000f */
        /*00f8*/                   BRA 0xf8 ;                                    /* 0xe2400fffff87000f */
       ..........


$ cuobjdump -sass vectorAddDrv_bin
cuobjdump info    : File 'vectorAddDrv_bin' does not contain device code
$

英雄联盟