在Vulkan中,队列族(Queue family)实际上是什么?

41
我目前正在学习Vulkan,现在我只是将每个命令拆开并检查结构体,试图理解它们的含义。
目前我正在分析队列族(QueueFamilies),以下是我的代码:
vector<vk::QueueFamilyProperties> queue_families = device.getQueueFamilyProperties();
for(auto &q_family : queue_families)
{
    cout << "Queue number: "  + to_string(q_family.queueCount) << endl;
    cout << "Queue flags: " + to_string(q_family.queueFlags) << endl;
}

这将产生以下输出:
Queue number: 16
Queue flags: {Graphics | Compute | Transfer | SparseBinding}
Queue number: 1
Queue flags: {Transfer}
Queue number: 8
Queue flags: {Compute}

嗯,天真地说,我是这样理解的:
有3个队列族,其中一个队列族有16个队列,都能进行图形、计算、传输和稀疏绑定操作(不知道最后两个是什么)
另一个队列族有1个队列,只能进行传输操作(不管那是什么)
最后一个队列族有8个队列,能进行计算操作。
每个队列族都是什么?我知道它是我们发送执行命令(如绘制和交换缓冲区)的地方,但这只是一个相对宽泛的解释,我希望能得到更加专业和详细的答案。
这两个额外的标志是什么?传输和稀疏绑定?
最后,为什么我们需要多个命令队列?我们有什么需求?

稀疏绑定的解释:http://asawicki.info/news_1698_vulkan_sparse_binding_-_a_quick_overview.html。基本上,它是一种在GPU上使用分页的方式,就像在CPU上一样,而不是一次性绑定所有资源,这使您可以移动内存以防止碎片化,而无需重新创建所有资源。 - Krupip
2个回答

72
要理解队列族,首先必须理解队列。
队列是你提交命令缓冲区的地方,提交到队列的命令缓冲区按顺序[1]执行。除非你使用VkSemaphore显式同步它们,否则提交到不同队列的命令缓冲区之间是无序的。你只能一次从一个线程向队列提交工作,但不同的线程可以同时向不同的队列提交工作。
每个队列只能执行特定类型的操作。图形队列可以运行由vkCmdDraw*命令启动的图形管线。计算队列可以运行由vkCmdDispatch*启动的计算管线。传输队列可以执行从vkCmdCopy*进行的传输(复制)操作。稀疏绑定队列可以使用vkQueueBindSparse更改稀疏资源到内存的绑定(注意,这是直接提交到队列的操作,而不是命令缓冲区中的命令)。某些队列可以执行多种类型的操作。在规范中,每个可以提交到队列的命令都有一个“命令属性”表,列出了可以执行该命令的队列类型。
队列族只是描述具有相同属性的一组队列。所以在你的例子中,设备支持三种类型的队列:
  • 有一种类型可以进行图形、计算、传输和稀疏绑定操作,您可以创建最多16个此类型的队列。

  • 另一种类型只能进行传输操作,您只能创建一个此类型的队列。通常,这是用于在离散GPU上在主机和设备内存之间异步进行数据DMA传输,以便传输可以与独立的图形/计算操作同时进行。

  • 最后,您可以创建最多8个仅能进行计算操作的队列。

一些队列可能只对应于主机端调度程序中的单独队列,而其他队列可能对应于硬件中的实际独立队列。例如,许多GPU只有一个硬件图形队列,因此即使从支持图形的队列族创建了两个VkQueue,提交到这些队列的命令缓冲区将独立地通过内核驱动程序的命令缓冲区调度程序进行处理,但在GPU上以某种串行顺序执行。但是,某些GPU具有多个仅计算的硬件队列,因此计算队列族的两个VkQueue实际上可能会独立且并发地通过整个GPU进行处理。Vulkan不公开此功能。

底线是:根据并发性来决定可以有多少个有用的队列。对于许多应用程序来说,一个“通用”队列就足够了。更高级的应用程序可能会有一个图形+计算队列,一个专门用于异步计算工作的计算队列,以及一个用于异步DMA的传输队列。然后将您想要的内容映射到可用的内容;您可能需要自己进行多路复用,例如在没有计算队列族的设备上,您可以创建多个图形+计算队列,或者自己将异步计算作业序列化到单个图形+计算队列上。
注:有点过于简化了。它们会按顺序“开始”,但之后可以独立进行并且可以无序完成。不过,并不保证不同队列的独立进展。对于这个问题,我就说到这里。

3
你一次只能从一个线程向队列提交工作。我理解为,两个线程可以向同一个队列提交命令,但需要进行同步,以确保只有其中一个线程在任何时候提交。这样理解正确吗? - Makogan
3
没错。Vulkan 将其称为“外部同步”,许多对象都是这样进行外部同步的,这意味着您不能同时在两个线程上操作该对象(除非所有操作都是只读的)。 - Jesse Hall
这并没有真正解释什么是稀疏绑定。 - Krupip
1
我认为那有点超出范围了。但是稀疏绑定操作基本上是“更新页面表,将图像或缓冲区的区域[X,X']映射到内存的字节[Y,Y']”。它们被排队并受到队列同步的影响,以便相对于图形/计算/传输操作以正确的顺序进行,而不需要更重量级的“等待栅栏,执行操作,然后提交依赖工作”的同步。 - Jesse Hall
3
根据问问题者发布的示例(包括传输操作在内的16个通用队列,但只有1个传输队列),使用更为严格的队列是否有任何优势? - Charlie Su

23

队列是一种接收包含特定类型(由 Family 标识确定)操作的命令缓冲区的东西。提交给队列的命令具有提交顺序,因此它们受管线屏障、子通道依赖项和事件的同步控制(在不同队列之间需要使用信号量或更好的方式)。

有一个技巧:COMPUTEGRAPHICS 总是可以隐式接受 TRANSFER 工作负载(即使 QueueFamilyProperties 没有列出它。请参见下面 VkQueueFlagBits 规范说明中的注释)。

传输用于复制和 Blit 命令。稀疏是一种类似分页的东西;它允许将多个内存句柄绑定到单个图像,并允许稍后重新绑定不同的内存。

在规范中,下面给出的每个vkCmd* 命令总是说明了“支持的队列类型”。

队列族是一组具有特殊关系的队列。某些东西只限于单个队列族,例如图像(它们必须在队列族之间传输)或命令池(只为给定队列族创建命令缓冲区,不允许其他队列族使用)。理论上,在某些奇特的设备上,可能有更多具有相同标识的队列族。

这基本上就是 Vulkan 规范所保证的一切内容。在 KhronosGroup/Vulkan-Docs#569 查看是否有问题。


还有一些供应商特定的材料,例如:

GPU 拥有异步图形引擎、计算引擎和复制\DMA 引擎。显然,图形和计算会争夺 GPU 的同一计算单元。

他们通常只有一个图形前端,这是图形操作的瓶颈,因此使用多个图形队列没有意义。

计算有两种运行模式:同步计算(公开为 GRAPHICS|COMPUTE 家族)和异步计算(公开为仅 COMPUTE 家族)。第一种是安全选择。第二种可以为您提供约 10% 的性能,但更加棘手,并需要更多的工作。AMD 的文章建议始终将第一种作为基线。

理论上,GPU 上可以有和 Compute Unit 数量相同数量的计算队列。但 AMD 认为多于两个异步计算队列没有好处,因此只公开了两个。NVIDIA 则使用全部数量。
复制/DMA 引擎(仅公开为“TRANSFER”系列)主要用于 CPU⇄GPU 传输。它们通常无法实现 GPU 内部拷贝的全速吞吐量。因此,除非有一些驱动程序的魔法,否则应该使用异步传输系列进行 CPU⇄GPU 传输(以获得异步属性,同时能够无障碍地执行图形操作)。对于 GPU 内部拷贝,大多数情况下最好使用“GRAPHICS|TRANSFER”系列。

1
一个现有的队列家族如何适应这个问题?从我所看到的,大多数人的行为表现得好像它几乎总是图形队列家族。例外是各种在线教程,它们通常假设它们是不同的,尽管我认为他们这样做是为了教育目的 - 显示如何使用同步功能。 - janekb04
1
@热衷于3D图形编程... 是的,几乎可以确定会有GRAPHICS+COMPUTE+PRESENT队列族。虽然这只是一种理论上的可能性,但如果支持的话,现在不支持该队列上的present(如果根本支持的话;Vulkan确实允许无头和仅计算的实现); 我甚至建议删除这种可能性,因为我觉得每天都有人闲聊这个问题,而它在真正的硬件中并不存在。 - krOoze
@热衷于3D图形编程的朋友们... PS:一个不错的折中方案是只使用VK_SHARING_MODE_CONCURRENT,即使队列不同也几乎没有影响。从技术上讲,可能会降低性能,但是谁会在不存在的情况下关心呢?如果有人制造出这样的假想硬件,那么可能需要重新优化。 - krOoze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接