2.2 物理设备评估与选择 本节摘要:多 GPU 环境是常态而不是例外。本节把「选一块设备」从直觉题变成打分题:枚举只是入口,真正的评估要过队列族、交换链支持、显存容量、特性开关这四道硬门槛,再按需求加权。交付一个可直接抄进工程的评分函数与决策流程图。 手头这台机器上的每块 GPU 到底能干什么、不能干什么?把这个问题交给猜,工程就会在某个客户的怪机器上翻车。本节承接 2.1 拿到的 VkInstance,完成第 2 章的第二道手续:把系统里所有设备档案枚举出来,逐项核对硬性要求,给合格者打分,选出那个将陪伴整个程序生命周期的 VkPhysicalDevice。 一、枚举只是拿到名录 枚举本身很简单,难的是认识到返回的是「档案」而不是「设备」——对它只能查询,不能执行。1.
本节摘要:多 GPU 环境是常态而不是例外。本节把「选一块设备」从直觉题变成打分题:枚举只是入口,真正的评估要过队列族、交换链支持、显存容量、特性开关这四道硬门槛,再按需求加权。交付一个可直接抄进工程的评分函数与决策流程图。
手头这台机器上的每块 GPU 到底能干什么、不能干什么?把这个问题交给猜,工程就会在某个客户的怪机器上翻车。本节承接 2.1 拿到的 VkInstance,完成第 2 章的第二道手续:把系统里所有设备档案枚举出来,逐项核对硬性要求,给合格者打分,选出那个将陪伴整个程序生命周期的 VkPhysicalDevice。
枚举本身很简单,难的是认识到返回的是「档案」而不是「设备」——对它只能查询,不能执行。1.2 节的铁律在此第一次发挥实际作用:
uint32_t count = 0; vkEnumeratePhysicalDevices(instance, &count, NULL); if (count == 0) { /* 大厅里没有任何受理网点,直接退出 */ } std::vector<VkPhysicalDevice> devices(count); vkEnumeratePhysicalDevices(instance, &count, devices.data());
返回顺序没有规范保证——笔记本上独显与集显谁在前面取决于驱动,所以「取第一块」永远不该出现在代码里,取而代之的是下面这套评估流程。
评估前先明确你程序的最低需求,以下四项任一不满足就一票否决。
队列族完整性:至少存在一个同时支持 GRAPHICS 与 COMPUTE 的族;若做窗口呈现,还需要一个支持 PRESENT 呈现的族(可以与图形族同族,也可以不同族,2.5 节会用到这个区分)。查询接口是 vkGetPhysicalDeviceQueueFamilyProperties。
交换链支持:设备级扩展清单里必须含 VK_KHR_swapchain,且至少一种 VkSurfaceFormatKHR 与一种 VkPresentModeKHR 可用(查询对象是你的窗口表面,所以本步要等 2.4 节的表面创建之后做完整版;做设备初筛时可以先查扩展是否存在)。
显存预算:vkGetPhysicalDeviceMemoryProperties 汇总各类型堆的大小,叠加 vkGetPhysicalDeviceProperties 的 maxMemoryAllocationCount 上限。纹理密集型应用要在这一步把「设备本地显存总量」与你的资产规模对账。
特性开关:vkGetPhysicalDeviceFeatures2 返回的整张特性表里,你依赖的项必须为 VK_TRUE。注意 Vulkan 1.0 核心特性在 Features 结构里,扩展特性要挂在 pNext 链上逐个查询——查询不全,后面启用时会无声失败。

门槛筛完后,剩下的候选按项目需求加权。下面是一个教学用的评分骨架,权重按你的需求调整:
int scoreDevice(VkPhysicalDevice dev, VkSurfaceKHR surface) { int score = 0; VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(dev, &props); if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) score += 1000; // 独显大权重 else if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU) score += 200; // 门槛:图形与计算族;加分:有独立传输族可做异步上传 uint32_t famCount = 0; vkGetPhysicalDeviceQueueFamilyProperties(dev, &famCount, nullptr); std::vector<VkQueueFamilyProperties> fams(famCount); vkGetPhysicalDeviceQueueFamilyProperties(dev, &famCount, fams.data()); bool gfx = false, xferOnly = false; for (const auto& f : fams) { if (f.queueFlags & VK_QUEUE_GRAPHICS_BIT) gfx = true; if ((f.queueFlags & VK_QUEUE_TRANSFER_BIT) && !(f.queueFlags & (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT))) xferOnly = true; } if (!gfx) return 0; // 硬门槛不过,直接出局 if (xferOnly) score += 100; // 独立传输族,异步上传可期 // 门槛:交换链扩展与表面支持 uint32_t extCount = 0; vkEnumerateDeviceExtensionProperties(dev, NULL, &extCount, NULL); std::vector<VkExtensionProperties> exts(extCount); vkEnumerateDeviceExtensionProperties(dev, NULL, &extCount, exts.data()); bool swap = false; for (const auto& e : exts) if (!strcmp(e.extensionName, VK_KHR_SWAPCHAIN_EXTENSION_NAME)) swap = true; if (!swap) return 0; // 显存预算:设备本地堆求和 VkPhysicalDeviceMemoryProperties mem; vkGetPhysicalDeviceMemoryProperties(dev, &mem); VkDeviceSize local = 0; for (uint32_t i = 0; i < mem.memoryHeapCount; ++i) if (mem.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) local += mem.memoryHeaps[i].size; score += (int)(local / (1024ull * 1024ull * 1024ull)); // 每GB计一分 return score; }
这段代码刻意把「门槛」写成 return 0、「偏好」写成加分——两者用不同机制表达,读代码的人一眼能分清哪些是底线哪些是口味。
背景:一款运行在大量办公笔记本上的轻量三维查看器,客户机器普遍是「集显加独显」混装,且独显常是省电模式。用户抱怨启动后风扇狂转但画面并无改善。
操作:评估函数照常跑,但团队发现打分制会稳定偏向独显,而这款应用的场景规模根本用不满集显。于是调整策略:把「显存预算」从加权项升级为门槛——资产总量低于某阈值时,独显不再获得额外加权;同时用 props.vendorID 与驱动版本过滤掉已知有呈现问题的老驱动版本,命中者降级处理。
结果:低配场景下选择集显,功耗与噪音回落,帧率无感知差异;只有加载超大装配体时才切到独显。两套路径共用同一份渲染代码,因为对应用来说,设备差异已经被查询数据吸收了。
解读:选设备的本质是「需求与档案的对账」。打分制只是对账的机械化表达,真正重要的是承认:同一款产品在不同机器上的正确答案可能不同,而这个不同可以被代码自动决策,不必留给用户手动切换。
变式:渲染农场与离线工具常反过来——要求「每块设备都开一个账户」做并行渲染,评估函数的产出就变成一张有序清单而不是单一胜者。同一套查询代码,消费方式随架构变化。
评估结束并不等于查询结束。选中的 VkPhysicalDevice 会在后面无数地方被再次查询:内存类型(第 3 章)、格式支持(第 4 章)、时间戳周期(第 8 章)。工程上值得把这些查询结果整理成一份「设备档案快照」结构体,初始化时填好、全局只读共享——毕竟档案是只读的,查一次就够了。