如果您正在开发、测试或使用OpenCL应用程序,那么您的目标很可能是要利用异构计算的强大功能。通过在GPU上的执行,OpenCL实现了通用的工作负载的加速,但是,在虚拟机中的工作负载又当如何?是否有可能利用OpenCL对虚拟GPU上的工作负载进行加速?开发人员如何实现这一目标?
在本文中,我们将研究VirtIO-GPU(一种基于半虚拟化技术的图形适配器)以及用于VirtIO-GPU的VCL(由高通科技公司提供的一种OpenCL驱动程序)。通过VCL,您可以利用主机的GPU硬件来加速虚拟客户机中的OpenCL应用程序。
我们如何利用主机GPU加速虚拟机中的操作?
虚拟机的功能如同处于一台计算机中的另一台计算机,其目的是要加快内容的执行速度。硬件虚拟化包括一台主机(即底层机)和一台客户机(即在主机上面运行的虚拟机)。此外,为了增强虚拟机的性能,硬件辅助虚拟化和半虚拟化等技术已经发展到可以直接在主机上执行客户机内容的程度,如在CPU(例如: Linux内核虚拟机)或GPU上执行。
如何利用主机上的GPU是 可以通过不同方式解决的复杂问题。我们的解决方案涉及API远程处理、设备仿真和将API调用从客户机转发到主机执行的组合。要做到这一点,从客户的角度来看,虚拟GPU的作用很像物理显卡;因此,客户机操作系统需要相应的设备驱动程序。
VirtIO-GPU就是这样一种驱动程序。虚拟GPU是一种基于半虚拟化的图形适配器和OASIS标准;自2013年起,Linux内核已经为其提供了一种内核驱动程序。VirtIO-GPU支持Virgl和Venus等图形API用户空间驱动程序,可以满足支持DRM的Unix客户机中大部分图形应用程序的要求。
但是,仍然缺少一个Khronos API:OpenCL。的确,可以利用 clvk或ANGLE等项目在Vulkan API上运行OpenCL应用程序;但是,这类项目都有自己的局限性:clvk依赖于Venus,这种工具相对较新,可能无法用于某些设备;而ANGLE则有很多依赖项,仅支持一组有限的设备。
考虑到这些限制条件,我们意识到需要一种OpenCL驱动程序,以利用主机GPU来加速客户机的工作负载。因此,我们开始开发VCL,这是一种为VirtIO-GPU开发的紧凑型OpenCL驱动程序,并且仅仅依赖于libdrm。
VCL架构
如下文框图所示,VCL从其他mesa驱动程序中获得了大量灵感。
VCL架构:包括客户机上的VCL驱动程序和主机上的vcomp virglrenderer后端(用红色高亮标出)。
在客户机的底层是一个利用libdrm与VirtIO-GPU交互、从而驱动从客户机到主机所有通信的层。通过利用Virgl和Venus常见的技术,该层通过VirtIO-GPU将API调用从客户机有效转发至主机。该驱动程序采用Rust编写,因此,在客户机的最顶层是一个以Rusticl的方式实现OpenCL API的层。
输出参数
许多OpenCL函数均期望可供实现存储返回值的输出参数。在VCL传输层中,命令识别码与每一个OpenCL函数相关联,该识别码与传递给函数的参数一起被转发给主机。实际上,驱动程序将一个或多个命令编码到缓存中,然后将其传递给EXECBUFFER ioctl。
在主机和客户机未共享内存的环境中,向主机传递输出参数涉及传输指向客户机地址空间的指针。该指针无法被主机解引用,因为它不在主机的地址空间中,但是主机仍然需要该指针指向的数值。一种解决方案是同时传递指针及其所引用的数值,对于指向数组的指针,需要传递数组的大小及其所有数值。
主机在virgl_renderer_submit_cmd()中接收命令缓存,并将其转发给vcomp_context_submit_cmd()函数。主机对命令缓存进行解码并执行命令。由于客户机指针不能在主机上使用,因此定义了局部变量,并将其用作调用相应OpenCL函数的输出参数,此时,应当将返回值和输出值发送回客户机。这一过程类似于编码——命令步骤,因为我们为客户机编码了应答。
由于不存在ioctl反向通道,那么我们如何将应答发送回客户机?客户机分配了Virgl资源,将其用作应答缓冲区,主机对应答进行编码,并使用客户机所创建的资源。从客户机的角度来看,EXECBUFFER ioctl是处于阻塞状态的,因此一旦阻塞结束,就会触发来自主机的传输,并将用于读取应答的资源映射为对应的VirtIO-GPU ioctl,如下所示。
从客户机到主机执行命令缓存的顺序图
VirtIO-GPU资源
有两种用于创建资源的VirtIO-GPU ioctls:
- CREATE_RESOURCE
- CREATE_RESOURCE_BLOB
虽然blob(Binary Large Object,大型二进制对象)资源需要共享内存,但在无法获得共享内存时,可以使用常规资源来传输数据。
virglrenderer主要使用OpenGL上下文的各种资源,但是,在请求一个使用CUSTOM_BINDING的资源时,virglrenderer仅创建一种由主机内存支持的virgl/vrend资源。
在使用相应的ioctl创建各种资源时,QEMU将附加指向客户机内存中某一目标的iovecs。如果利用EXECBUFFER中的自定义命令创建资源,则不会发生此步骤。
一旦主机将数据写入到iovec中,客户机就可以触发来自主机的传输,等待传输完成,并映射资源内存以读取应答,如下所示。
创建从主机到客户机资源的顺序图
命令与应答缓存
vcomp上下文从VCL驱动程序接收带有OpenCL命令的缓存,并且为大多数命令生成应答。vcomp上下文对前几个字节进行解码以获得命令识别码。对于每个命令识别码均有一个调度函数,该函数继续对命令参数进行解码,并对应答(如果有的话)进行编码。应答被编码至应答缓存中,稍后由客户机解码。
在没有共享内存的情况下,我们无法在客户机驱动程序和主机后端使用相同的地址,这是因为客户机上的内存分配位于与主机不同的地址空间中,反之亦然。客户机需要为应答缓存创建带有自定义绑定的VirtIO-GPU资源,在主机上,该VirtIO-GPU资源对应于由主机内存支持的virgl/vrend资源,并带有指向客户机内存的关联 iovec 。
从主机的角度来看,vcomp并不知道在何处对应答进行编码,直至客户机驱动程序告诉主机后端要使用哪种资源。该过程通过客户机的自定义命令clSetReplyBufferMESA(resource_id)完成,因此,该命令必须在任何其他需要应答的命令之前发送。参看下图。
创建从主机到客户机应答资源的顺序图
传输层
传输层是用于编码和解码OpenCL命令及其参数的函数集合,这与Venus驱动程序中使用的方法相同;由于某些谷歌贡献者的工作,大部分该类代码均由venus协议存储库中的python脚本生成。Venus协议源代码的生成分为两个步骤:
- 解析包含Vulkan应用程序接口规范的XML文件。
- 根据规范生成Venus协议代码。
与Vulkan应用程序接口类似,存在一个包含OpenCL规范的官方XML——cl.xml——因此可以通过类似的方式生成VCL协议源代码。我们克隆了venus协议,并根据我们的需要对其进行了大量修改,所以我们现在称它为vcl协议。
另一个virglrenderer上下文
创建一种新的virglrenderer上下文需要CONTEXT_INIT VirtIO-GPU内核参数,并且可以通过5.16及以上的内核版本获得该内核参数。如果没有该内核参数,我们只能创建VIRGL/VIRGL2上下文,并仅向这些类型的上下文提交命令。
CONTEXT_INIT内核参数为VirtIO-GPU带来全新的CONTEXT_INIT ioctl。ioctl通过VCL驱动程序查询OpenCL平台的结果,并触发主机virglrenderer/vcomp上下文的创建。然后,客户机能够将下一个命令(例如创建资源、传输数据、映射资源内存和执行命令)发送到主机上正确的virglrenderer上下文。
通过查询该ioctl的内核实现情况,我们可以发现设置进程权限识别码保存在vfpriv.context_init中,并通过其发出创建上下文命令。该项操作通过 Qemu的virtio-gpu-virgl,将context_init(值为VCL capset)传递给virglrenderer。
主机后端
由CONTEXT_INIT ioctl触发,通过virglrenderer/src/vcl/vcomp_context.c中的vcomp_context_create()在主机上创建vcomp上下文。
vcomp上下文保持了一个调度对象,其中包含每个OpenCL命令的函数指针。该调度对象由vcomp_context_init_dispatch()初始化,它将该类函数指针设置为各种vcomp_dispatch_*()函数。
vcomp上下文通过vcomp_context_submit_cmd()从EXEC_BUFFER ioctl接收命令缓存,其中包含一个或多个命令。vcomp上下文对命令缓存进行解码,并通过vcl_dispatch_command()将命令逐一调度给相应的vcomp_dispatch_*()函数。
添加全新OpenCL函数的支持意味着实现相应的调度函数,并将其注册到调度对象。
句柄映射
如要支持cl_khr_icd,则驱动程序对象必须与接口控制文件兼容。换句话说,驱动程序对象应当是这样的:
struct _cl_<object>
{
struct _cl_icd_dispatch *dispatch;
// ... remainder of internal data
};
在Rust中,驱动程序对象是这样的:
#[repr(C)]
pub struct _cl_<object> {
dispatch: *mut _cl_icd_dispatch,
// ...
}
这导致我们无法使用主机提供的句柄。假设我们调用clGetPlatformIDs()直接从主机获取一个platform_id句柄,该句柄是一个指向主机内存的指针,如果主机对OpenCL的实现支持cl_khr_icd,则会在platform_id->dispatch中设置一个调度指针。
不幸的是,我们无法在客户机上使用该指针,一旦我们试图对该指针进行解引用,由于客户机和主机地址空间之间的差异,我们会得到一个段错误。
Venus采用的解决方案是在客户机驱动程序中创建多个对象,并保持客户机句柄和主机句柄之间的映射。
当客户机创建一个新的VkInstance时,客户机会调用vkCreateInstance(),以传递一个输出指针,由其预期在何处返回句柄。在主机上,解码器会保持一个哈希表——decoder->object_table ——将来自客户机的指针作为键,将vkr_objects作为值。vcl驱动程序遵循相同的方法,因此对于每个客户机句柄,主机上均会有一个相应的vcomp_object。
不规则API
但是,请注意,某些OpenCL函数并不规则。例如,当客户机创建cl_context时,客户机会调用clCreateContext(),而不是使用输出参数,并由其预期作为返回值的句柄。这是一个问题,因为我们需要将客户机指针传递给主机以进行句柄映射。作为参考,在下文中提供了该函数的完整签名:
cl_context clCreateContext(
const cl_context_properties* properties,
cl_uint num_devices,
const cl_device_id* devices,
void (CL_CALLBACK* pfn_notify)(const char*, const void*, size_t, void*),
void* user_data,
cl_int* errcode_ret
);
为了解决这个问题,并且对于所有将句柄作为函数返回值返回的OpenCL函数,我们引入了一组自定义的OpenCL函数。该类函数将句柄放在输出参数中,并返回cl_int,以进行错误报告。
在实践中,首先在客户机驱动程序中创建一个OpenCL对象,然后通过自定义OpenCL函数(例如:clCreateContextMESA())将指向其句柄的指针作为输出参数发送。在主机上,在向对象表中插入一个新的vcomp对象时,解码器使用来自客户机的指针作为键。作为参考,在下文中提供了该函数的完整签名:
cl_int clCreateContextMESA(
const cl_context_properties* properties,
cl_uint num_devices,
const cl_device_id* devices,
void (CL_CALLBACK* pfn_notify)(const char*, const void*, size_t, void*),
void* user_data,
cl_context* out_context
);
基准
为了说明使用VCL和VirtIO-GPU的硬件加速带来的性能提升,下面是我们在cl-mem和clpeak中进行的一些基准测试的结果。
最显著的区别在于单精度计算:
比较VCL和Rusticl的单精度计算基准测试
红色表示Rusticl/llvmpipe(完全在客户机上运行的软件OpenCL实施)。绿色表示VCL(由主机的集成显卡支持)。在没有硬件加速的情况下,Rusticl的结果不到200 GFLOPS;加速后,VCL可达到2000 – 2400 GFLOPS。
在整型计算中,这种差异同样惊人:
比较VCL和Rusticl整型计算的快速24位基准测试。绿色代表Vcl的性能,红色代表Rusticl。
在没有加速的情况下,Rusticl最多可以达到150 GIOPS,而VCL和硬件加速则可以达到600 GIOPS以上。
硬件加速的好处在这些测试中更加明显:
- 整型计算(int、int2、int4、int8、int16)
- 字符型(8位)计算
- 短整型(16位)计算
请注意,VCL中的复制操作(从客户机到主机和从主机到客户机)会影响内存:
比较VCL和Rusticl的内存基准测试
后续步骤
目前,VCL驱动程序提供了OpenCL 1.0中可用的大部分功能,并成功完成了600多项piglit测试。该驱动程序没有集成编译器功能;相反,该驱动程序将程序源直接传输到主机,因此,该驱动程序依赖于主机驱动程序,允许大多数程序测试相对容易地通过。
是否有兴趣尝试VCL或为其开发做出贡献?下文提供了一些选择:
- 为您自己构建VCL,甚至将其包含在您的产品中。注意,其中存在多个依赖项,并且您应该有一些构建mesa的经验。mesa文档是一个很好的起点。
- 等待将VCL包含在您的下一个发行版升级中(当其最终被社区接受时)。
- 审核mesa和virglrenderer的合并请求,然后做出贡献。
- 请看2024年Linaro Connect大会上的演示文件“QQVP:高通公司的SystemC和QEMU建模解决方案”。
目前,我们在 开发人员Discord 上有一个专门的公共项目频道。加入志同道合的开发者社区,相互联系,获得支持并交换想法。
在所发布内容中表达的观点仅为原作者的个人观点,并不代表高通公司或其子公司(以下简称为“高通公司”)的观点。所提供的内容仅供参考之用,而并不意味着高通公司或任何其他方的赞同或表述。本网站同样可以提供非高通公司网站和资源的链接或参考。高通公司对于可能通过本网站引用、访问、或链接的任何非高通公司网站或第三方资源并没有做出任何类型的任何声明、保证、或其他承诺。
高通品牌产品均为高通科技公司和/或其子公司的产品。
关于作者
安东尼奥·卡贾诺
马可·利埃贝尔