GPU应用程序扩展机制:无需更改源代码修改行为

本文档探讨了在不需要对原始应用程序进行源代码更改的情况下,扩展和修改GPU应用程序行为的各种机制。我们将研究可以修改GPU行为的哪些方面,哪些方法需要GPU端代码,以及这些功能与eBPF类似功能的比较。

本文档探讨了在不需要对原始应用程序进行源代码更改的情况下,扩展和修改GPU应用程序行为的各种机制。我们将研究可以修改GPU行为的哪些方面,哪些方法需要GPU端代码,以及这些功能与eBPF类似功能的比较。

目录

  1. 简介
  2. GPU计算栈中的扩展点
  3. API拦截和重定向
  4. 内存管理扩展
  5. 执行控制扩展
  6. 运行时行为修改
  7. 内核调度操作
  8. 多GPU分配
  9. 与eBPF功能的比较
  10. 案例研究
  11. 未来方向
  12. 参考文献

简介

GPU应用程序通常有一些用户可能希望修改的行为,而无需更改原始源代码:

  • 资源分配(内存、计算)
  • 调度优先级和策略
  • 错误处理机制
  • 性能特征
  • 监控和调试能力

虽然CPU受益于像eBPF这样允许动态行为修改的高级检测工具,但GPU有不同的编程模型,这影响了扩展的实现方式。本文档探索GPU生态系统中可能的功能以及不同方法的权衡。

GPU计算栈中的扩展点

GPU计算栈提供了几个可以修改行为的层:

┌─────────────────────────────┐
│    应用程序                 │ ← 源代码修改(不是我们的重点)
├─────────────────────────────┤
│    GPU框架/库               │ ← 库替换/包装
│    (TensorFlow, PyTorch)    │
├─────────────────────────────┤
│    CUDA运行时API            │ ← API拦截
├─────────────────────────────┤
│    CUDA驱动API              │ ← 驱动API拦截
├─────────────────────────────┤
│    GPU驱动                  │ ← 驱动补丁(需要特权)
├─────────────────────────────┤
│    GPU硬件                  │ ← 固件修改(很少可能)
└─────────────────────────────┘

每一层提供不同的扩展能力和限制:

层级扩展灵活性运行时开销实现复杂性所需权限
框架低-中
运行时API
驱动API很高
GPU驱动极高最小很高Root/管理员
GPU固件有限极高Root + 专业知识

API拦截和重定向

最灵活和可访问的GPU应用程序扩展方法是API拦截,它不需要GPU端代码。

CUDA运行时API拦截

可以修改的内容

  • 内存分配和传输
  • 内核启动参数
  • 流和事件管理
  • 设备选择和管理

实现方法

  1. LD_PRELOAD机制(Linux):

    // 拦截cudaMalloc的示例
    void* cudaMalloc(void** devPtr, size_t size) {
        // 调用真实的cudaMalloc
        void* result = real_cudaMalloc(devPtr, size);
        
        // 添加自定义行为
        log_allocation(*devPtr, size);
        
        return result;
    }
  2. DLL注入(Windows):

    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
        if (fdwReason == DLL_PROCESS_ATTACH) {
            // 钩住CUDA函数
            HookFunction("cudaMalloc", MyCudaMalloc);
        }
        return TRUE;
    }
  3. NVIDIA拦截库:专门为CUDA API拦截设计的框架。

示例:内存跟踪拦截器

// track_cuda_memory.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <cuda_runtime.h>
 
// 函数指针类型
typedef cudaError_t (*cudaMalloc_t)(void**, size_t);
typedef cudaError_t (*cudaFree_t)(void*);
 
// 原始函数指针
static cudaMalloc_t real_cudaMalloc = NULL;
static cudaFree_t real_cudaFree = NULL;
 
// 跟踪总分配内存
static size_t total_allocated = 0;
 
// 拦截的cudaMalloc
cudaError_t cudaMalloc(void** devPtr, size_t size) {
    if (!real_cudaMalloc)
        real_cudaMalloc = (cudaMalloc_t)dlsym(RTLD_NEXT, "cudaMalloc");
    
    cudaError_t result = real_cudaMalloc(devPtr, size);
    
    if (result == cudaSuccess) {
        total_allocated += size;
        printf("CUDA Malloc: %zu bytes at %p (Total: %zu)\n", 
               size, *devPtr, total_allocated);
    }
    
    return result;
}
 
// 拦截的cudaFree
cudaError_t cudaFree(void* devPtr) {
    if (!real_cudaFree)
        real_cudaFree = (cudaFree_t)dlsym(RTLD_NEXT, "cudaFree");
    
    // 我们需要一个映射表来跟踪每个指针的大小以便准确计算
    printf("CUDA Free: %p\n", devPtr);
    
    return real_cudaFree(devPtr);
}

用法:LD_PRELOAD=./libtrack_cuda_memory.so ./my_cuda_app

GPU虚拟化和API远程处理

更高级的API拦截方法可以完全重定向GPU操作:

  • NVIDIA CUDA vGPU:将API调用重定向到虚拟机管理程序控制的GPU的虚拟化技术
  • rCUDA:拦截API调用并将其转发到远程服务器的远程CUDA执行框架

内存管理扩展

可以修改的内容

  1. 内存分配策略

    • 自定义分配大小(例如,舍入到特定边界)
    • 分配池以减少碎片
    • 多个内核之间的设备内存优先级
  2. 内存传输优化

    • 自动固定内存使用
    • 小传输的批处理
    • 传输过程中的压缩
  3. 内存访问模式

    • 内存预取
    • 自定义缓存策略

是否需要GPU代码?

大多数内存管理扩展可以完全通过API拦截从CPU端实现。但是,一些高级优化可能需要GPU端修改:

仅CPU端(不需要GPU代码)

  • 分配时机和批处理
  • 主机-设备传输优化
  • 内存池管理

需要GPU代码

  • 内核内的自定义内存访问模式
  • 专门的缓存策略
  • 内核内的数据预取

示例:内存池拦截器

// CUDA分配的简单内存池
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <cuda_runtime.h>
#include <map>
#include <vector>
 
// 原始函数指针
static cudaError_t (*real_cudaMalloc)(void**, size_t) = NULL;
static cudaError_t (*real_cudaFree)(void*) = NULL;
 
// 内存池结构
struct MemBlock {
    void* ptr;
    size_t size;
    bool in_use;
};
 
std::vector<MemBlock> memory_pool;
std::map<void*, size_t> allocation_map;
 
// 带池化的拦截cudaMalloc
cudaError_t cudaMalloc(void** devPtr, size_t size) {
    if (!real_cudaMalloc)
        real_cudaMalloc = (cudaError_t(*)(void**, size_t))dlsym(RTLD_NEXT, "cudaMalloc");
    
    // 将大小向上舍入以减少碎片(例如,到256字节边界)
    size_t aligned_size = (size + 255) & ~255;
    
    // 尝试在池中找到空闲块
    for (auto& block : memory_pool) {
        if (!block.in_use && block.size >= aligned_size) {
            block.in_use = true;
            *devPtr = block.ptr;
            allocation_map[block.ptr] = aligned_size;
            return cudaSuccess;
        }
    }
    
    // 如果没有找到则分配新块
    void* new_ptr;
    cudaError_t result = real_cudaMalloc(&new_ptr, aligned_size);
    
    if (result == cudaSuccess) {
        memory_pool.push_back({new_ptr, aligned_size, true});
        allocation_map[new_ptr] = aligned_size;
        *devPtr = new_ptr;
    }
    
    return result;
}
 
// 带池化的拦截cudaFree
cudaError_t cudaFree(void* devPtr) {
    if (!real_cudaFree)
        real_cudaFree = (cudaError_t(*)(void*))dlsym(RTLD_NEXT, "cudaFree");
    
    // 将块标记为空闲但不实际释放内存
    for (auto& block : memory_pool) {
        if (block.ptr == devPtr) {
            block.in_use = false;
            allocation_map.erase(devPtr);
            return cudaSuccess;
        }
    }
    
    // 如果在池中未找到,则使用常规释放
    return real_cudaFree(devPtr);
}
 
// 添加函数在应用退出时实际释放所有池化内存
__attribute__((destructor)) void cleanup_memory_pool() {
    for (auto& block : memory_pool) {
        real_cudaFree(block.ptr);
    }
    memory_pool.clear();
}

执行控制扩展

可以修改的内容

  1. 内核启动配置

    • 块和网格维度
    • 共享内存分配
    • 流分配
  2. 内核执行时机

    • 内核启动批处理
    • 执行优先级
    • 多个内核之间的工作分配
  3. 错误处理和恢复

    • CUDA错误的自定义处理
    • 失败操作的自动重试
    • 优雅降级策略

是否需要GPU代码?

基本执行控制可以通过API拦截处理,但高级优化可能需要GPU端代码:

仅CPU端(不需要GPU代码)

  • 启动配置
  • 流管理
  • 基本错误处理

需要GPU代码

  • 内核融合或拆分
  • 内核内的高级错误恢复
  • 内核内的动态工作负载平衡

示例:内核启动优化器

// kernel_optimizer.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <cuda_runtime.h>
 
// 原始内核启动函数
typedef cudaError_t (*cudaLaunchKernel_t)(const void*, dim3, dim3, void**, size_t, cudaStream_t);
static cudaLaunchKernel_t real_cudaLaunchKernel = NULL;
 
// 优化的内核启动
cudaError_t cudaLaunchKernel(const void* func, dim3 gridDim, dim3 blockDim, 
                            void** args, size_t sharedMem, cudaStream_t stream) {
    if (!real_cudaLaunchKernel)
        real_cudaLaunchKernel = (cudaLaunchKernel_t)dlsym(RTLD_NEXT, "cudaLaunchKernel");
    
    // 获取设备属性
    cudaDeviceProp prop;
    int device;
    cudaGetDevice(&device);
    cudaGetDeviceProperties(&prop, device);
    
    // 优化块大小以获得更好的占用率
    if (blockDim.x * blockDim.y * blockDim.z <= 256) {
        // 调整块大小以更好地利用SM
        dim3 optimizedBlockDim;
        optimizedBlockDim.x = 256;
        optimizedBlockDim.y = 1;
        optimizedBlockDim.z = 1;
        
        // 调整网格大小以维持总线程
        dim3 optimizedGridDim;
        int original_total = gridDim.x * gridDim.y * gridDim.z * 
                            blockDim.x * blockDim.y * blockDim.z;
        int threads_per_block = optimizedBlockDim.x * optimizedBlockDim.y * 
                               optimizedBlockDim.z;
        int num_blocks = (original_total + threads_per_block - 1) / threads_per_block;
        
        optimizedGridDim.x = num_blocks;
        optimizedGridDim.y = 1;
        optimizedGridDim.z = 1;
        
        // 使用优化配置启动
        return real_cudaLaunchKernel(func, optimizedGridDim, optimizedBlockDim, 
                                   args, sharedMem, stream);
    }
    
    // 回退到原始配置
    return real_cudaLaunchKernel(func, gridDim, blockDim, args, sharedMem, stream);
}

运行时行为修改

可以修改的内容

  1. JIT编译行为

    • 优化级别
    • 目标架构
    • 代码生成选项
  2. 错误检测和报告

    • 增强的错误检查
    • 自定义日志和诊断信息
    • 性能异常检测
  3. 设备管理

    • 多GPU负载平衡
    • 电源和热管理
    • 容错策略

是否需要GPU代码?

许多运行时行为可以通过API拦截和环境变量修改,但一些高级功能需要GPU端代码:

仅CPU端(不需要GPU代码)

  • JIT编译标志
  • 设备选择和配置
  • 错误处理策略

需要GPU代码

  • 内核内的自定义错误检查
  • 专门的容错机制
  • 运行时自适应算法

示例:错误弹性扩展

// error_resilience.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <cuda_runtime.h>
#include <stdio.h>
 
// 原始函数指针
typedef cudaError_t (*cudaLaunchKernel_t)(const void*, dim3, dim3, void**, size_t, cudaStream_t);
static cudaLaunchKernel_t real_cudaLaunchKernel = NULL;
 
// 跟踪内核启动以便重试
struct KernelInfo {
    const void* func;
    dim3 gridDim;
    dim3 blockDim;
    void** args;  // 注意:没有深度复制args是不安全的
    size_t sharedMem;
    int retries;
};
 
#define MAX_TRACKED_KERNELS 100
static KernelInfo kernel_history[MAX_TRACKED_KERNELS];
static int kernel_count = 0;
 
// 具有自动重试功能的增强内核启动
cudaError_t cudaLaunchKernel(const void* func, dim3 gridDim, dim3 blockDim, 
                            void** args, size_t sharedMem, cudaStream_t stream) {
    if (!real_cudaLaunchKernel)
        real_cudaLaunchKernel = (cudaLaunchKernel_t)dlsym(RTLD_NEXT, "cudaLaunchKernel");
    
    // 保存内核信息以便潜在重试
    if (kernel_count < MAX_TRACKED_KERNELS) {
        kernel_history[kernel_count].func = func;
        kernel_history[kernel_count].gridDim = gridDim;
        kernel_history[kernel_count].blockDim = blockDim;
        kernel_history[kernel_count].args = args;  // 注意:这是浅拷贝
        kernel_history[kernel_count].sharedMem = sharedMem;
        kernel_history[kernel_count].retries = 0;
    }
    int current_kernel = kernel_count++;
    
    // 启动内核
    cudaError_t result = real_cudaLaunchKernel(func, gridDim, blockDim, 
                                              args, sharedMem, stream);
    
    // 检查错误并在需要时重试
    if (result != cudaSuccess) {
        printf("内核启动失败: %s\n", cudaGetErrorString(result));
        
        if (kernel_history[current_kernel].retries < 3) {
            printf("重试内核启动(尝试 %d)...\n", 
                   kernel_history[current_kernel].retries + 1);
            
            // 重置设备以从错误中恢复
            cudaDeviceReset();
            
            // 增加重试计数
            kernel_history[current_kernel].retries++;
            
            // 重试启动
            result = real_cudaLaunchKernel(func, gridDim, blockDim, 
                                         args, sharedMem, stream);
        }
    }
    
    return result;
}

内核调度操作

可以修改的内容

  1. 内核优先级

    • 分配执行优先级
    • 抢占控制(在支持的情况下)
    • 执行顺序
  2. 流管理

    • 自定义流创建和同步
    • 跨流的工作分配
    • 依赖关系管理
  3. 并发内核执行

    • 控制并行内核执行
    • 内核之间的资源分区

是否需要GPU代码?

大多数调度操作可以从CPU端完成,但细粒度控制可能需要GPU代码:

仅CPU端(不需要GPU代码)

  • 流创建和管理
  • 基本优先级设置
  • 内核启动顺序

需要GPU代码

  • GPU内的动态工作负载平衡
  • 内核之间的细粒度同步
  • 内核内的自定义调度算法

示例:基于优先级的调度器

// priority_scheduler.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <cuda_runtime.h>
#include <queue>
#include <mutex>
#include <thread>
#include <condition_variable>
 
// 原始函数指针
typedef cudaError_t (*cudaLaunchKernel_t)(const void*, dim3, dim3, void**, size_t, cudaStream_t);
static cudaLaunchKernel_t real_cudaLaunchKernel = NULL;
 
// 带优先级的内核任务
struct KernelTask {
    const void* func;
    dim3 gridDim;
    dim3 blockDim;
    void** args;
    size_t sharedMem;
    cudaStream_t stream;
    int priority;  // 数字越大 = 优先级越高
    
    bool operator<(const KernelTask& other) const {
        return priority < other.priority; // 优先队列是最大堆
    }
};
 
// 内核的优先级队列
std::priority_queue<KernelTask> kernel_queue;
std::mutex queue_mutex;
std::condition_variable queue_condition;
bool scheduler_running = false;
std::thread scheduler_thread;
 
// 在后台运行的调度器函数
void scheduler_function() {
    while (scheduler_running) {
        KernelTask task;
        
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            queue_condition.wait(lock, []{
                return !kernel_queue.empty() || !scheduler_running;
            });
            
            if (!scheduler_running) break;
            
            task = kernel_queue.top();
            kernel_queue.pop();
        }
        
        // 启动最高优先级的内核
        real_cudaLaunchKernel(task.func, task.gridDim, task.blockDim, 
                             task.args, task.sharedMem, task.stream);
    }
}
 
// 如果未运行则启动调度器
void ensure_scheduler_running() {
    if (!scheduler_running) {
        scheduler_running = true;
        scheduler_thread = std::thread(scheduler_function);
    }
}
 
// 基于优先级的内核启动
cudaError_t cudaLaunchKernel(const void* func, dim3 gridDim, dim3 blockDim, 
                            void** args, size_t sharedMem, cudaStream_t stream) {
    if (!real_cudaLaunchKernel)
        real_cudaLaunchKernel = (cudaLaunchKernel_t)dlsym(RTLD_NEXT, "cudaLaunchKernel");
    
    ensure_scheduler_running();
    
    // 确定内核优先级(示例:基于网格大小)
    int priority = gridDim.x * gridDim.y * gridDim.z;
    
    // 创建任务并添加到队列
    KernelTask task = {func, gridDim, blockDim, args, sharedMem, stream, priority};
    
    {
        std::lock_guard<std::mutex> lock(queue_mutex);
        kernel_queue.push(task);
    }
    
    queue_condition.notify_one();
    
    return cudaSuccess; // 注意:这在内核实际启动之前返回
}
 
// 在程序退出时清理调度器
__attribute__((destructor)) void cleanup_scheduler() {
    if (scheduler_running) {
        scheduler_running = false;
        queue_condition.notify_all();
        scheduler_thread.join();
    }
}

多GPU分配

可以修改的内容

  1. 工作负载分配

    • 在GPU之间自动分配工作
    • 基于GPU能力的负载均衡
    • 数据局部性优化
  2. 跨GPU的内存管理

    • 透明数据镜像
    • 跨GPU内存访问优化
    • 统一内存增强
  3. 同步策略

    • 自定义屏障和同步点
    • 通信优化
    • 依赖关系管理

是否需要GPU代码?

基本的多GPU支持可以通过API拦截实现,但高效实现通常需要GPU端修改:

仅CPU端(不需要GPU代码)

  • 基本工作分配
  • 跨GPU的内存分配
  • 高级同步

需要GPU代码

  • 高效的GPU间通信
  • 自定义数据共享机制
  • GPU端工作负载平衡

示例:简单多GPU分配器

// multi_gpu_distributor.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <cuda_runtime.h>
#include <vector>
#include <map>
 
// 原始函数指针
typedef cudaError_t (*cudaLaunchKernel_t)(const void*, dim3, dim3, void**, size_t, cudaStream_t);
static cudaLaunchKernel_t real_cudaLaunchKernel = NULL;
 
// 跟踪可用GPU
static int num_gpus = 0;
static std::vector<cudaStream_t> gpu_streams;
static std::map<void*, std::vector<void*>> memory_mirrors;
static int next_gpu = 0;
 
// 初始化多GPU环境
void init_multi_gpu() {
    if (num_gpus > 0) return; // 已初始化
    
    cudaGetDeviceCount(&num_gpus);
    if (num_gpus <= 1) num_gpus = 1; // 回退到单GPU
    
    // 为每个GPU创建一个流
    for (int i = 0; i < num_gpus; i++) {
        cudaSetDevice(i);
        cudaStream_t stream;
        cudaStreamCreate(&stream);
        gpu_streams.push_back(stream);
    }
}
 
// 分布式内核启动
cudaError_t cudaLaunchKernel(const void* func, dim3 gridDim, dim3 blockDim, 
                            void** args, size_t sharedMem, cudaStream_t stream) {
    if (!real_cudaLaunchKernel)
        real_cudaLaunchKernel = (cudaLaunchKernel_t)dlsym(RTLD_NEXT, "cudaLaunchKernel");
    
    init_multi_gpu();
    
    if (num_gpus <= 1) {
        // 单GPU模式
        return real_cudaLaunchKernel(func, gridDim, blockDim, args, sharedMem, stream);
    }
    
    // 简单的轮询分配
    int gpu_id = next_gpu;
    next_gpu = (next_gpu + 1) % num_gpus;
    
    cudaSetDevice(gpu_id);
    
    // 调整网格维度以适应多GPU
    dim3 adjusted_grid = gridDim;
    adjusted_grid.x = (gridDim.x + num_gpus - 1) / num_gpus; // 分割工作
    
    // 在选定的GPU上启动
    return real_cudaLaunchKernel(func, adjusted_grid, blockDim, 
                               args, sharedMem, gpu_streams[gpu_id]);
}
 
// 带镜像的内存分配
cudaError_t cudaMalloc(void** devPtr, size_t size) {
    static auto real_cudaMalloc = (cudaError_t(*)(void**, size_t))dlsym(RTLD_NEXT, "cudaMalloc");
    
    init_multi_gpu();
    
    if (num_gpus <= 1) {
        return real_cudaMalloc(devPtr, size);
    }
    
    // 在主GPU上分配
    cudaSetDevice(0);
    cudaError_t result = real_cudaMalloc(devPtr, size);
    if (result != cudaSuccess) return result;
    
    // 在其他GPU上分配镜像
    std::vector<void*> mirrors;
    mirrors.push_back(*devPtr); // 原始指针
    
    for (int i = 1; i < num_gpus; i++) {
        cudaSetDevice(i);
        void* mirror_ptr;
        result = real_cudaMalloc(&mirror_ptr, size);
        if (result != cudaSuccess) {
            // 失败时清理
            for (void* ptr : mirrors) {
                cudaFree(ptr);
            }
            return result;
        }
        mirrors.push_back(mirror_ptr);
    }
    
    // 存储镜像以备后用
    memory_mirrors[*devPtr] = mirrors;
    
    return cudaSuccess;
}

与eBPF功能的比较

eBPF为CPU提供了动态检测功能,GPU世界中没有完全等效的功能,但我们可以比较不同的方法:

eBPF功能GPU等效功能实现复杂性局限性
动态代码加载JIT编译需要专门工具
内核检测API拦截限于API边界
进程监控CUPTI回调对内核内部可见性有限
网络包过滤N/AN/A没有直接等效物
性能监控NVTX, CUPTI需要外部分析
安全强制执行API验证执行点有限

主要区别

  1. 运行时安全保证

    • eBPF:静态验证确保程序安全
    • GPU:对动态代码没有等效安全验证
  2. 观察范围

    • eBPF:跨进程的系统范围可见性
    • GPU:限于单一应用或驱动程序级别
  3. 权限要求

    • eBPF:需要不同级别的权限
    • GPU:API拦截通常不需要特殊权限
  4. 与硬件集成

    • eBPF:与CPU和操作系统深度集成
    • GPU:受供应商提供的接口限制

案例研究

案例研究1:透明多GPU加速

挑战:在不更改代码的情况下加速单GPU应用以使用多个GPU。

解决方案:API拦截库,它:

  1. 拦截内存分配和内核启动
  2. 将数据分配到可用GPU上
  3. 重写内核启动以处理数据分区
  4. 将结果收集回主GPU

结果示例(非真实结果)

  • 在内存受限的应用中,2个GPU可获得1.8倍加速
  • 计算密集型应用由于同步开销而扩展有限
  • 不需要源代码更改

案例研究2:自适应内存管理

挑战:减少深度学习框架中的内存分配开销和碎片。

解决方案:内存池扩展,它:

  1. 拦截所有CUDA内存分配
  2. 维护预分配内存池
  3. 基于使用模式实现自定义分配策略
  4. 延迟实际释放直到内存压力需要

结果示例(非真实结果)

  • 对于具有大量小张量分配的模型,训练时间减少30%
  • 通过更好的碎片管理,峰值内存使用减少15%
  • 与现有框架兼容,无需源代码更改

未来方向

GPU扩展机制的格局继续演变:

  1. 硬件级可扩展性

    • GPU供应商可能提供更多的定制运行时行为钩子
    • 安全动态代码加载的硬件支持(GPU版eBPF)
  2. 统一编程模型

    • SYCL、oneAPI和类似框架可能提供更多扩展点
    • 跨CPU和GPU的异构编程模型
  3. 操作系统级GPU资源管理

    • 将GPU资源集成到操作系统调度框架中
    • 操作系统级别对GPU资源的细粒度控制
  4. AI辅助扩展

    • 动态修改GPU应用行为的自动优化系统
    • 预测和适应应用需求的机器学习模型

参考文献

  1. NVIDIA. "CUDA Driver API." https://docs.nvidia.com/cuda/cuda-driver-api/
  2. Gregg, Brendan. "BPF Performance Tools: Linux System and Application Observability." Addison-Wesley Professional, 2019.
  3. NVIDIA. "CUPTI: CUDA Profiling Tools Interface." https://docs.nvidia.com/cuda/cupti/