Linux GPU 计算子系统实战:Vulkan Compute Shader 编写与 DRM 渲染节点深入

引言

GPU 已经从单纯的图形渲染加速器演变为通用并行计算引擎。在深度学习推理、科学计算、视频编加密、密码学等场景中,GPU 的吞吐量往往是 CPU 的 10—100 倍。然而,与成熟的 CUDA 生态相,Vulkan Compute 路径的开源性和跨平台性使其在嵌入式、机密计算、异构集群调度等领域具有不可替代的优势。

本文将深入探讨以下核心问题:

  • Linux DRM(Direct Rendering Manager)子系统如何管理 GPU 设备,KMS 与 Render Node 的本质区别是什么
  • Vulkan Compute 管线从 SPIR-V shader 到物理设备命令提交的完整链路
  • DMA-BUF 零拷贝共享机制如何实现 GPU—GPU、GPU—CPU 间的高效数据传输
  • 内核 GPU 调度器(amdgpu/nouveau/sched_ext)如何处理多进程并发与优先级抢占
  • 调试与观测工具(renderdoc、perf、ftrace GPU 跟踪点)的实战用法

通过本文,读者将掌握从零编写一个 GPU 计算管线并部署到 Linux 生产环境的全部工程能力。

一、Linux DRM 子系统架构全景

1.1 DRM 的设计哲学

DRM(Direct Rendering Manager)是 Linux 内核中管理 GPU 硬件的子系统。它的设计目标是让用户空间通过标准化接口安全、高效地控制 GPU。关键设计原则包括:

  • 权限隔离:只有 DRM Master(持有 DRM device 文件描述符)才能提交渲染命令;Render Node(/dev/dri/renderD*)则无 Master 权限,仅供计算/显示输出
  • 内存管理:GEM(Graphics Execution Manager)提供跨驱动的缓冲区抽象,DMA-BUF 实现跨设备零拷贝共享
  • 同步原语:dma_fence 提供 GPU→CPU、GPU→GPU 的异步同步机制

1.2 KMS vs Render Node

现代 Linux 系统中,GPU 设备在 /dev/dri/ 下呈现两类节点:


/dev/dri/card0   — 主设备(Principal),具有 KMS + Render 能力
/dev/dri/renderD128 — 渲染节点,仅具有计算/渲染输出能力

关键区别:

能力 card0 (card) renderD* (render)
KMS(显示模式设置) ✓ ✗
DRM Master(ioctl 控制) ✓ ✗
GPU 渲染计算 ✓ ✓
DMA-BUF 导入/导出 ✓ ✓

Render Node 的引入至关重要:容器和沙箱环境只需挂载 /dev/dri/renderD128 即可获得完整的 GPU 计算能力,而无法通过任何 ioctl 控制显示输出。这是 Kubernetes 上 GPU 共享调度的基础。

1.3 DRM 驱动核心数据结构

以 amdgpu 驱动为例,关键结构如下:


// drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c (简化)
struct amdgpu_device {
    struct drm_device drm_dev;          // 继承 DRM 基类
    struct pci_device *pdev;
    void __iomem *rmmio;                // MMIO 寄存器基地址
    struct amdgpu_ring rings[MAX_RING]; // 计算/图形/DMA 环形缓冲
    struct amdgpu_sa_manager ring_tmp_bo;
    struct mutex gmc_lock;              // GMC(图形内存控制器)锁
    uint64_t visible_vram_size;         // 可被 CPU 映射的 VRAM 区域
    // ...
};

struct amdgpu_ring {
    struct amdgpu_device *adev;
    struct amdgpu_bo *ring_obj;         // GEM 对象:环形缓冲
    uint32_t *ring;                     // 映射后的 CPU 虚拟地址
    uint64_t ring_size;                 // 环形缓冲大小(通常 64KB—256KB)
    uint32_t wptr;                      // 写指针
    uint32_t rptr;                      // 读指针
    atomic64_t last_signal;             // 最近完成 fence 序号
};

每个 amdgpu_ring 本质上是一个 GPU 环形命令缓冲:CPU 端写入命令,GPU 端顺序执行。不同类型的 ring(GFX/Compute/DMA/sDMA)负责不同任务,由内核调度器统一管理。

1.4 DRM 文件操作流程

用户空间访问 DRM 的标准流程:


#include <xf86drm.h>
#include <xf86drmMode.h>

int open_render_node(void) {
    // 枚举所有 render 节点
    drmDevicePtr devices[MAX_DRM_DEVICES];
    int count = drmGetDevices2(0, devices, MAX_DRM_DEVICES);
    
    for (int i = 0; i < count; i++) {
        if (devices[i]->available_nodes & (1 << DRM_NODE_RENDER)) {
            const char *node = devices[i]->nodes[DRM_NODE_RENDER];
            int fd = open(node, O_RDWR | O_CLOEXEC);
            // 验证 GPU 版本
            drmVersionPtr ver = drmGetVersion(fd);
            printf("GPU Driver: %s %d.%d.%d\n", 
                   ver->name, ver->version_major, 
                   ver->version_minor, ver->version_patchlevel);
            drmFreeVersion(ver);
            return fd;
        }
    }
    return -1;
}

二、Vulkan Compute 管线全流程

2.1 实例创建与物理设备枚举

Vulkan 实例是所有后续操作的根上下文。通过实例,我们可以枚举系统中所有可用的物理 GPU 设备:


VkInstance instance;
VkInstanceCreateInfo instance_info = {
    .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
    .pApplicationInfo = &(VkApplicationInfo){
        .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
        .pApplicationName = "ComputeDemo",
        .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
        .apiVersion = VK_API_VERSION_1_3,
    },
    .enabledExtensionCount = 1,
    .ppEnabledExtensionNames = (const char*[]) {
        VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME
    },
};
vkCreateInstance(&instance_info, NULL, &instance);

uint32_t device_count = 0;
vkEnumeratePhysicalDevices(instance, &device_count, NULL);
VkPhysicalDevice *devices = malloc(device_count * sizeof(VkPhysicalDevice));
vkEnumeratePhysicalDevices(instance, &device_count, devices);

for (uint32_t i = 0; i < device_count; i++) {
    VkPhysicalDeviceProperties2 props2 = {
        .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
    };
    VkPhysicalDeviceVulkan13Properties vk13_props = {
        .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_PROPERTIES,
    };
    props2.pNext = &vk13_props;
    vkGetPhysicalDeviceProperties2(devices[i], &props2);
    
    printf("GPU[%d]: %s (Vulkan %d.%d.%d)\n",
           i, props2.properties.deviceName,
           VK_VERSION_MAJOR(props2.properties.apiVersion),
           VK_VERSION_MINOR(props2.properties.apiVersion),
           VK_VERSION_PATCH(props2.properties.apiVersion));
    
    // 检查计算队列族
    uint32_t qc = 0;
    vkGetPhysicalDeviceQueueFamilyProperties(devices[i], &qc, NULL);
    VkQueueFamilyProperties *qprops = malloc(qc * sizeof(VkQueueFamilyProperties));
    vkGetPhysicalDeviceQueueFamilyProperties(devices[i], &qc, qprops);
    
    for (uint32_t j = 0; j < qc; j++) {
        if (qprops[j].queueFlags & VK_QUEUE_COMPUTE_BIT) {
            printf("  Compute Queue[%d]: count=%d\n", j, qprops[j].queueCount);
        }
    }
    free(qprops);
}

2.2 为什么直接用 libdrm 浏览 DRM 设备并不够

Vulkan 看似封装了 DRM,但在生产场景中,我们仍然需要直接操作 DRM API。原因:

  1. DMA-BUF 导入导出(VK_EXT_external_memory_dma_buf)需要显式管理 PRIME 文件描述符
  2. Timeline Semaphore 与 DMA-Fence 互操作(VK_KHR_external_semaphore_fd)需要对接内核 fence
  3. 显存固定大小的优化(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 与 GEM 的 VRAM 驻留直接相关)
  4. 异构集成场景下,Vulkan 无法直接操作其他厂商硬件(如 FPGA、NPU),必须走 DRM 直接通信

2.3 计算管线的核心结构

Vulkan Compute 管线的关键对象层次:


Instance → PhysicalDevice → LogicalDevice → Queue
                                          → CommandPool → CommandBuffer
                                          → DescriptorPool → DescriptorSet
                                          → PipelineLayout → ComputePipeline
                                          → Buffer → DeviceMemory

完整的管线搭建代码如下:


// 创建逻辑设备,指定计算队列
float queue_priority = 1.0f;
VkDeviceQueueCreateInfo queue_info = {
    .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
    .queueFamilyIndex = compute_queue_family,
    .queueCount = 1,
    .pQueuePriorities = &queue_priority,
};

VkPhysicalDeviceVulkan13Features vk13_features = {
    .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_PROPERTIES,
    .synchronization2 = VK_TRUE,  // 启用同步2简化管线屏障
};

VkPhysicalDeviceVulkan12Features vk12_features = {
    .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_PROPERTIES,
    .pNext = &vk13_features,
    .timelineSemaphore = VK_TRUE,
};

VkDeviceCreateInfo device_info = {
    .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
    .pNext = &vk12_features,
    .queueCreateInfoCount = 1,
    .pQueueCreateInfos = &queue_info,
};

VkDevice device;
vkCreateDevice(physical_device, &device_info, NULL, &device);

VkQueue compute_queue;
vkGetDeviceQueue(device, compute_queue_family, 0, &compute_queue);

2.4 编写 SPIR-V Compute Shader

Vulkan 不接受 GLSL 源代码,直接消费 SPIR-V 中间表示。以下是一个向量加法的计算着色器:


#version 450
layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in;

layout(set = 0, binding = 0) buffer InputA { float data[]; } input_a;
layout(set = 0, binding = 1) buffer InputB { float data[]; } input_b;
layout(set = 0, binding = 2) buffer Output { float data[]; } output_data;

layout(push_constant) uniform Params {
    uint element_count;
} params;

void main() {
    uint idx = gl_GlobalInvocationID.x;
    if (idx < params.element_count) {
        output_data.data[idx] = input_a.data[idx] + input_b.data[idx];
    }
}

编译为 SPIR-V:


glslangValidator -V shader.comp -o shader.spv
# 或使用 shaderc(glslc)
glslc -fshader-stage=compute shader.comp -o shader.spv --target-env=vulkan1.3

使用 spirv-dis 反汇编可查看指令序列,对于性能调优很重要:


spirv-dis shader.spv | head -40

SPIR-V 的关键元数据:


; Version: 1.0
; Generator: Khronos Glslang Reference Front End; 10
; Bound: 32            ; 所有 ID 的最大值 + 1
; Schema: 0
               OpCapability Shader
          %1 = OpExtInstImport "GLSL.std.450"
               OpMemoryModel Logical GLSL450
               OpEntryPoint GLCompute %main "main" %gl_GlobalInvocationID
               OpExecutionMode %main LocalSize 256 1 1
               OpDecorate %gl_GlobalInvocationID BuiltIn GlobalInvocationID

2.5 描述符与管线布局

描述符集(Descriptor Sets)是 CPU 向 GPU 推送数据的方式,类似于 CUDA 的内核参数:


// 描述符集布局
VkDescriptorSetLayoutBinding bindings[] = {
    { .binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
      .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT },
    { .binding = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
      .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT },
    { .binding = 2, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
      .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT },
};

VkDescriptorSetLayoutCreateInfo dsl_info = {
    .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
    .bindingCount = 3,
    .pBindings = bindings,
};

VkDescriptorSetLayout desc_layout;
vkCreateDescriptorSetLayout(device, &dsl_info, NULL, &desc_layout);

// 管线布局:包含描述符集布局与 Push Constant 范围
VkPushConstantRange pcr = {
    .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
    .offset = 0,
    .size = sizeof(uint32_t),
};

VkPipelineLayoutCreateInfo pl_info = {
    .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
    .setLayoutCount = 1,
    .pSetLayouts = &desc_layout,
    .pushConstantRangeCount = 1,
    .pPushConstantRanges = &pcr,
};

VkPipelineLayout pipeline_layout;
vkCreatePipelineLayout(device, &pl_info, NULL, &pipeline_layout);

// 创建计算管线
VkShaderModuleCreateInfo sm_info = {
    .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
    .codeSize = spv_size,
    .pCode = spv_code,
};
VkShaderModule shader_module;
vkCreateShaderModule(device, &sm_info, NULL, &shader_module);

VkComputePipelineCreateInfo cp_info = {
    .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
    .stage = {
        .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
        .stage = VK_SHADER_STAGE_COMPUTE_BIT,
        .module = shader_module,
        .pName = "main",
    },
    .layout = pipeline_layout,
};

VkPipeline compute_pipeline;
vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &cp_info, NULL, &compute_pipeline);

2.6 命令提交与同步

GPU 命令缓冲、提交与同步是生产级应用中最容易出错的环节:


// 分配命令缓冲
VkCommandBufferAllocateInfo alloc_info = {
    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
    .commandPool = cmd_pool,
    .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
    .commandBufferCount = 1,
};

VkCommandBuffer cmd_buf;
vkAllocateCommandBuffers(device, &alloc_info, &cmd_buf);

// 开始录制
VkCommandBufferBeginInfo begin = {
    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
    .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
};
vkBeginCommandBuffer(cmd_buf, &begin);

// 绑定管线与描述符集
vkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, compute_pipeline);
vkCmdBindDescriptorSets(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE,
                         pipeline_layout, 0, 1, &desc_set, 0, NULL);

// 推送常量
vkCmdPushConstants(cmd_buf, pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT,
                    0, sizeof(uint32_t), &element_count);

// 分发计算:groups = ceil(element_count / 256)
uint32_t group_count = (element_count + 255) / 256;
vkCmdDispatch(cmd_buf, group_count, 1, 1);

vkEndCommandBuffer(cmd_buf);

// 使用 Timeline Semaphore 同步
VkSemaphoreTypeCreateInfo tsi = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO,
    .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE,
    .initialValue = 0,
};

VkSemaphoreCreateInfo si = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
    .pNext = &tsi,
};
VkSemaphore timeline_sem;
vkCreateSemaphore(device, &si, NULL, &timeline_sem);

VkTimelineSemaphoreSubmitInfo tss = {
    .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO,
    .signalSemaphoreValueCount = 1,
    .pSignalSemaphoreValues = (uint64_t[]){ 1 },
};

VkSubmitInfo submit = {
    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
    .pNext = &tss,
    .commandBufferCount = 1,
    .pCommandBuffers = &cmd_buf,
    .signalSemaphoreCount = 1,
    .pSignalSemaphores = &timeline_sem,
};

vkQueueSubmit(compute_queue, 1, &submit, VK_NULL_HANDLE);

// CPU 端等待完成
vkWaitSemaphores(device, &(VkSemaphoreWaitInfo){
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO,
    .semaphoreCount = 1,
    .pSemaphores = &timeline_sem,
    .pValues = (uint64_t[]){ 1 },
}, UINT64_MAX);

vkQueueWaitIdle(compute_queue); // 确保 GPU 端命令执行完毕

三、DMA-BUF:GPU 间零拷贝共享

3.1 什么是 DMA-BUF

DMA-BUF 是 Linux 内核提供的跨驱动缓冲区共享框架。它的核心价值在于消除了 GPU 间数据拷贝的成本。在 Vulkan 中,通过 VK_EXT_external_memory_dma_buf 扩展导入 DMA-BUF 文件描述符作为 DeviceMemory,即可在单一进程内实现 GPU→GPU 的无拷贝数据流动。

3.2 DMA-BUF 在 DRM 中的生命周期


// 创建 DMA-BUF(内存分配并导出为文件描述符)
struct drm_prime_handle prime = {
    .handle = gem_handle,       // GEM 句柄
    .flags = DRM_RDWR,
    .fd = -1,                   // 输出参数
};

// 用户态:导出为 fd
drmIoctl(fd, DRM_IOCTL_PRIME_HANDLE_TO_FD, &prime);
int dma_buf_fd = prime.fd;

// 导入方:从 fd 获取 GEM 句柄
struct drm_prime_handle prime_in = {
    .fd = dma_buf_fd,
    .flags = DRM_RDWR,
    .handle = 0,  // 输出
};
drmIoctl(other_fd, DRM_IOCTL_PRIME_FD_TO_HANDLE, &prime_in);

// 再创建新的 GEM buffer object 引用该内存
struct drm_gem_open gem_open = {
    .name = 0,
    .handle = prime_in.handle,
    .size = size,
};
drmIoctl(other_fd, DRM_IOCTL_GEM_OPEN, &gem_open);

3.3 Vulkan + DMA-BUF 集成

将 DMA-BUF 内存导入 Vulkan 的标准流程:


VkExternalMemoryBufferCreateInfo em_buf_info = {
    .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO,
    .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
};

VkBufferCreateInfo buf_info = {
    .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
    .pNext = &em_buf_info,
    .size = buffer_size,
    .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
    .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
};

VkBuffer buffer;
vkCreateBuffer(device, &buf_info, NULL, &buffer);

// 获取内存需求(对齐 & 类型位掩码)
VkMemoryRequirements mem_req;
vkGetBufferMemoryRequirements(device, buffer, &mem_req);

// 构建导入信息
VkMemoryFdPropertiesKHR fd_props = {
    .sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR,
};
vkGetMemoryFdPropertiesKHR(device, 
    VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
    dma_buf_fd, &fd_props);

VkImportMemoryFdInfoKHR import_info = {
    .sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR,
    .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
    .fd = dma_buf_fd,
};

VkMemoryAllocateInfo alloc_info = {
    .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
    .pNext = &import_info,
    .allocationSize = mem_req.size,
    .memoryTypeIndex = find_memory_type(mem_req.memoryTypeBits,
                                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT),
};

VkDeviceMemory memory;
vkAllocateMemory(device, &alloc_info, NULL, &memory);
vkBindBufferMemory(device, buffer, memory, 0);

3.4 跨 GPU 零拷贝的实际应用场景

在多 GPU 系统中(典型如 AMD APU + dGPU 或 Intel iGPU + NVIDIA dGPU),DMA-BUF 可实现零拷贝:

  1. GPU A 计算输出 → GPU B 后处理:GPU A 将结果写入导出为 DMA-BUF 的 GEM 缓冲,GPU B 直接导入为 Vulkan DeviceMemory,无需 CPU 内存中转
  2. 显示输出 (KMS) → 计算捕获:KMS framebuffer 可直接导入为 Vulkan 输入数据
  3. 摄像头/视频解码器 → GPU 计算:V4L2 导出 DMA-BUF,Vulkan 直接作为纹理

3.5 DMA-Fence 与同步

DMA-BUF 共享时必须保证同步。Linux 内核通过 dma_fence 提供异步完成通知:


// DRM 提交时指定 in-fence(等待上游完成)
struct drm_amdgpu_cs_fence_dep deps[] = {
    { .handle = upstream_fence_handle,
      .ip_type = AMDGPU_HW_IP_GFX,
      .ip_instance = 0,
      .ring = 0,
      .context = context_id },
};

// 在 Vulkan 中通过外部 fence 对接
VkImportSemaphoreFdInfoKHR import_sem = {
    .sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR,
    .semaphore = vulkan_semaphore,
    .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT,
    .fd = fence_fd,
};
vkImportSemaphoreFdKHR(device, &import_sem);

四、内核 GPU 调度器深入分析

4.1 DRM 调度器 (drm_sched) 机制

DRM 子系统中的 drm_sched 负责管理多个 GPU 上下文(context)间的命令提交。关键数据结构:


// drivers/gpu/drm/sched_main.c
struct drm_gpu_scheduler {
    atomic_t hw_rq_count;           // 硬件运行队列计数
    atomic64_t job_id_count;        // 全局 job 序号
    struct list_head ring_mirror_list; // 待处理作业链表
    struct drm_sched_rq *sched_rq[MAX_PRIORITY]; // 按优先级分级的运行队列
    struct dma_fence *last_scheduled; // 上次调度的 fence
    atomic64_t hw_submission_limit;  // 硬件并发提交上限
};

struct drm_sched_entity {
    struct list_head rq;            // 所属运行队列
    struct drm_gpu_scheduler *sched;
    atomic_t fence_seq;             // entity 内 fence 序号
    uint32_t priority;              // 作业优先级(继承 Djikstra 优先级继承)
    struct spsc_queue job_queue;    // 单个 entity 的作业队列(无锁 SPSC)
};

drm_sched 的调度算法本质是带优先级的 Round-Robin:

  1. 每个作业(job)关联一个实体(entity),每个 entity 属于一个优先级队列(普通/高/内核级)
  2. 同一优先级内按 FIFO 轮转:每次调度器 tick 从各 entity 取出一个 job 送入硬件环形缓冲
  3. 优先级继承:高优先级作业到达时,当前运行的低优先级 job 可能被打断(取决于硬件抢占能力)

4.2 amdgpu 的硬件调度与多引擎

amdgpu 驱动支持四种硬件引擎(IP block):

安全隔离(多租户) 受限 最佳
IP Block 作用 drm_sched 用法
GFX 图形渲染(Draw / 3D) 图形命令提交,带 KMS 翻页同步
Compute 计算着色器 独立作业,通常优先级高于图形
SDMA 系统 DMA 引擎 显存搬移(页表更新、资源迁移)

对于纯计算场景(我们的 Vulkan 应用),不需要操作 GFX 引擎,所有命令通过 Compute IP 提交。运行时 /sys/kernel/debug/dri/0/amdgpu_sched_jobs 可查看 schedule 状态。

4.3 GPU 抢占与优先级反转问题

现代 GPU(如 AMD RDNA2+/NVIDIA Pascal+)支持硬件级抢占,但存在延迟和开销。生产场景常见问题:

  1. 长计算作业阻塞显示刷新:若 GFX ring 上有一个长时间计算任务,KMS 无法及时刷新帧,导致显示卡顿。解决方案:将计算任务限制在 Compute IP,GFX ring 仅用于轻量翻页
  2. 多进程竞争:两个容器共享同一 GPU 时,drm_sched 按优先级轮转,可能导致饥饿。需要 cgroup v2 限制(gpu.max, gpu.weight)
  3. 内存压力导致抖动:当应用分配过多 VRAM,内核 GPU 驱动物理页回收机制触发 TDR(Timeout Detection and Recovery),需通过 amdgpu.gttsize / amdgpu.vramlimit 限制单应用最大显存用量

4.4 cgroup v2 与 GPU 资源控制

Linux 6.14+ 引入了 cgroup v2 的 GPU 控制器(多个开发分支基础上,基于 sched_ext/BPF cgroup 设备控制器扩展)。当前的实用方案包括:


# 通过 cgroup v2 限制 GPU 计算配额(via device cgroup)
echo "+device" > /sys/fs/cgroup/cgroup.subtree_control

# 为 Docker 容器挂载 render 节点
docker run --device /dev/dri/renderD128:/dev/dri/renderD128 \
           --gpus=none \            # 不通过 nvidia-container-toolkit
           --security-opt seccomp=unconfined \
           gpu-workload

# cgroup 设备访问白名单:仅允许 GPU 设备
echo 'c 226:128 rw' > /sys/fs/cgroup/mycontainer/devices.allow

4.5 BPF 调度器扩展 (sched_ext) 在 GPU 上的应用

sched_ext 允许使用 BPF 程序替换 CPU 调度算法。对于 GPU 调度,AMD 和 Intel 正在推动相同的思路:


// BPF:GPU 作业优先级提升的简单示例
SEC("struct_ops/gpu_sched_select_cpu")
u64 BPF_PROG(gpu_sched_select_cpu, 
             struct gpu_job *job, 
             const struct cpumask *cpus)
{
    // 根据 VRAM 局部性选择最近的 CPU 核心
    u32 preferred_cpu = find_nearest_cpu(job->gpu_device_numa_node);
    if (cpumask_test_cpu(preferred_cpu, cpus))
        return preferred_cpu;
    return bpf_cpumask_any_and_distribute(cpus, cpus);
}

char _license[] SEC("license") = "GPL";

五、调试与性能观测工具

5.1 DRM 调试接口


# 查看 DRM 设备配置
cat /sys/kernel/debug/dri/0/state 2>/dev/null || \
  cat /sys/kernel/debug/dri/0/i915_display_info

# 实时监控 GPU 使用率(通过 trace events)
echo 'amdgpu_*' > /sys/kernel/debug/tracing/set_event
cat /sys/kernel/debug/tracing/trace_pipe

# DRM 状态查询(drm_info)
drm_info | less

5.2 Perf 集成 GPU 跟踪

GPU 活动可以通过 DRM tracepoints 由 perf 采样:


# 列出可用的 GPU 相关 tracepoints
perf list | grep -E 'amdgpu|drm|gpu'

# 记录 GPU 作业提交与完成
perf record -e 'amdgpu_cs_ioctl,amdgpu_sched_run_job,amdgpu_vm_flush' -a sleep 10
perf script | head -80

# 输出示例:
//PID   TID   TIME        EVENT
// 12045 [012] 1696543.123: amdgpu_cs_ioctl:  ring=0, pid=12045, context=128, seq=1042
// 12045 [012] 1696543.456: amdgpu_sched_run_job: ring=0, job_id=1042, fence=0x1234abcd
// ...

5.3 使用 RenderDoc 捕获 Vulkan 帧

RenderDoc 是 Vulkan/OpenCL 调试的利器,但 Linux 上对 Compute 的支持有限。实际工作中更推荐使用 VK_LAYER_KHRONOS_validation:


# 启用验证层
export VK_LAYER_PATH=/usr/share/vulkan/explicit_layer.d
export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation

# 运行 Vulkan 验证层会输出:
# - 描述符绑定是否符合 SPIR-V 声明
# - 命令缓冲结束时是否有未释放的 fence
# - 内存销毁前是否有未完成的队列操作
# - 同步屏障是否合理

./my_compute_app

5.4 GPU 性能计数器(AMD ROCm-SMI / nvidia-smi)


# AMD GPU 详细信息
rocm-smi --showproductname --showbus --showmeminfo vram \
         --showpids --showtemp --showfan --showclocks

# 查看 GPU 进程占用
cat /sys/kernel/debug/dri/0/amdgpu_gem_info 2>/dev/null

# NVIDIA
nvidia-smi dmon -s pucvmet -d 1

六、生产部署的十个实战建议

  1. 始终使用 Render Node(/dev/dri/renderD*):避免不必要的 KMS 权限,增强安全隔离
  2. 启用 AnMode (Compute 模式):对于 AMD GPU,设置 amdgpu.runpm=0 避免运行时电源管理导致计算中断
  3. 显式管理 DMA-BUF 对齐:VkMemoryRequirements.alignment 通常为 256B—4KB,错误的对齐会导致内存访问异常
  4. 避免 Compute + 图形争用:若系统同时运行图形渲染,使用 VK_QUEUE_COMPUTE_BIT 并指定独立的队列族
  5. 限制单应用的 VRAM:通过 amdgpu.vramlimit_pct 或 nvidia-smi -i 0 -c 1 设定 Compute 独占模式
  6. 使用 Timeline Semaphore 而非Fence:跨队列同步时,Timeline Semaphore 提供更灵活的 wait-before-signal 语义
  7. 批处理多个 Dispatch:减少 kernel launch overhead,将多次小 Dispatch 合并为少量大 Dispatch
  8. 静态验证 SPIR-V:部署前使用 spirv-val 检查 SPIR-V 合法性,避免运行时产生设备丢失错误
  9. 监控 TDR 事件:dmesg | grep amgpu 中若出现 "ring gfx timeout",说明作业耗时过长,需优化或切分
  10. 生产环境禁用 Validation Layer:验证层增加约 15—30% 的 CPU 开销,仅开发环境启用

结语

Linux GPU 计算栈是一个多层次、跨用户态和内核态的复杂系统。用户态 Vulkan API 将 GPU 计算抽象为管线化的命令提交,而 DRM 子系统则在底层处理设备初始化、内存管理、调度与同步。DMA-BUF 打通了 GPU 间高带宽共享的通道,drm_sched 与 sched_ext 则在多进程争用时提供了公平性与性能保障。

掌握这些机制后,工程师不仅能在生产环境中高效部署 GPU 计算任务,还能在出现 TDR、显存碎片化、抢占延迟等疑难问题时快速定位并解决。GPU 已不再是「黑盒加速器」,Linux 开源生态正在使它成为可观测、可调试、可优化的通用计算平台。

参考资料

  • Linux DRM 子系统文档:https://dri.freedesktop.org/docs/drm/
  • Vulkan 规范:https://www.khronos.org/registry/vulkan/
  • AMDGPU 内核文档:https://kernel.org/doc/html/latest/gpu/amdgpu.html
  • SPIR-V 规范:https://www.khronos.org/registry/SPR-V/
  • DMA-BUF 文档:https://docs.kernel.org/driver-api/dma-buf.html
  • Render Doc:https://renderdoc.org/
  • sched_ext:https://github.com/sched-ext/scx
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
VCN/UVD 视频编解码 独立上下文中处理多媒体流