写点什么

spirv 运行在显卡上

作者:Miracle
  • 2025-09-24
    四川
  • 本文字数:4960 字

    阅读完需:约 16 分钟

spirv 运行在显卡上

Vulkan 在现代绝大多数硬件上都能运行,包括 英伟达 AMD 的显卡 包括 Intel 的集成显卡,甚至包括了 各种移动端的 手机显示芯片,所以 我们这个语言就有了巨大的现实意义,能在手机上 嵌入平台上 把真实编码的算力提升 上万倍

因为正常情况下 我们是没有办法使用显卡上的算力的 ,除非


  • 游戏厂商使用 引擎 API 进行的 图形处理

  • 使用 CUDA OpenCL 等等开发的库

  • 使用 pyTorch 等等开发的 人工智能应用


但是你自己写自己的算法和代码,是没机会使用到这些算力的,这可是一个巨大的资源浪费啊


除了 Apple 封闭的体系以外,Vulkan 就是算力的未来


我们最早选择的是 Rust 库 是 webgpu,但是很遗憾 在 0.13 版本之后 移除了 spirv 加载的功能,因为他更加专注于 Web 平台 所以 力推自己的 WGSL 语言

这玩意跟我的 目的有点类似,但是 是强类型 无法原型运行起来的,所以,为什么不直接用 Rust 呢

不是我们的选择目标


那么在 rust 的世界里,只剩下一个选择 Vulkan 的官方绑定了,为了方便我们选择一个更多封装的 库

vulkano = "0.35.2"

显卡可以加载的 程序 简单来看我们把它叫做 Shader Module

基本流程是创建 管线 PipeLine

创建 存储的槽位 Layout

创建输入输出缓冲区 Buffer

将缓冲区绑定到 管线 Bind 通过描述符。

绑定 Shader

Dispatch 发射到显卡 执行任务,然后 读取回来结果


我们实现一个简单的 Vulkan Runtime

先看看要用到的引用 有点吓人 不过没事 大部分都只用到一次 作为初始化

use anyhow::Result;use vulkano::VulkanLibrary;use vulkano::device::QueueFlags;use vulkano::device::{Device, DeviceCreateInfo, QueueCreateInfo};use vulkano::instance::{Instance, InstanceCreateFlags, InstanceCreateInfo};use vulkano::pipeline::{ComputePipeline, Pipeline, PipelineLayout, PipelineShaderStageCreateInfo, compute::ComputePipelineCreateInfo, layout::PipelineDescriptorSetLayoutCreateInfo};use vulkano::shader::{ShaderModule, ShaderModuleCreateInfo};
use vulkano::buffer::{Buffer, BufferContents, BufferCreateInfo, BufferUsage};use vulkano::descriptor_set::WriteDescriptorSet;use vulkano::descriptor_set::allocator::StandardDescriptorSetAllocator;use vulkano::memory::allocator::StandardMemoryAllocator;use vulkano::memory::allocator::{AllocationCreateInfo, MemoryTypeFilter};
use vulkano::command_buffer::AutoCommandBufferBuilder;use vulkano::command_buffer::CommandBufferUsage;use vulkano::command_buffer::allocator::StandardCommandBufferAllocator;use vulkano::descriptor_set::DescriptorSet;use vulkano::pipeline::PipelineBindPoint;use vulkano::sync;use vulkano::sync::GpuFuture;
复制代码


下面是第一步初始化,发现和创建 Vulkan 的设备

impl Default for Runtime {    fn default() -> Self {        let library = VulkanLibrary::new().expect("no local Vulkan library/DLL");        let instance = Instance::new(library, InstanceCreateInfo { flags: InstanceCreateFlags::ENUMERATE_PORTABILITY, ..Default::default() }).unwrap();        let physical_device = instance.enumerate_physical_devices().expect("could not enumerate devices").next().expect("no devices available");        let queue_family_index = physical_device            .queue_family_properties()            .iter()            .enumerate()            .position(|(_queue_family_index, queue_family_properties)| queue_family_properties.queue_flags.contains(QueueFlags::GRAPHICS))            .expect("couldn't find a graphical queue family") as u32;
let mut features = physical_device.supported_features().clone(); features.shader_float64 = true; features.shading_rate_image = false;
let (device, mut queues) = Device::new(physical_device, DeviceCreateInfo { enabled_features: features, queue_create_infos: vec![QueueCreateInfo { queue_family_index, ..Default::default() }], ..Default::default() }).unwrap(); let queue = queues.next().unwrap(); Self{ device, queue } }}
复制代码

这是一个简单的执行流程

pub fn run(&mut self, shader: &[u32])-> Result<()> {        let shader = unsafe { ShaderModule::new(self.device.clone(), ShaderModuleCreateInfo::new(shader))? };
let entry_point = shader.entry_point("main").unwrap(); let stage = PipelineShaderStageCreateInfo::new(entry_point); let layout = PipelineLayout::new(self.device.clone(), PipelineDescriptorSetLayoutCreateInfo::from_stages([&stage]) .into_pipeline_layout_create_info(self.device.clone())?)?;
let pipeline = ComputePipeline::new(self.device.clone(), None, ComputePipelineCreateInfo::stage_layout(stage, layout))?; let memory_allocator = std::sync::Arc::new(StandardMemoryAllocator::new_default(self.device.clone())); let command_buffer_allocator = std::sync::Arc::new(StandardCommandBufferAllocator::new(self.device.clone(), Default::default())); let descriptor_set_allocator = std::sync::Arc::new(StandardDescriptorSetAllocator::new(self.device.clone(), Default::default())); let pipeline_layout = pipeline.layout();
let layout = &pipeline.layout().set_layouts()[0]; let set = DescriptorSet::new(descriptor_set_allocator, layout.clone(), [WriteDescriptorSet::buffer(0, buffer.clone())], [])?;
let mut builder = AutoCommandBufferBuilder::primary(command_buffer_allocator, self.queue.queue_family_index(), CommandBufferUsage::OneTimeSubmit).unwrap();
builder.bind_pipeline_compute(pipeline.clone())?; builder.bind_descriptor_sets(PipelineBindPoint::Compute, pipeline_layout.clone(), 0, set)?; //let copy_info = CopyBufferInfoTyped::buffers(buffer.clone().reinterpret::<[u8]>(),buffer.clone().reinterpret::<[u8]>()); //builder.copy_buffer(copy_info)?; let start = std::time::Instant::now(); unsafe { builder.dispatch([1, 1, 100]) }.unwrap(); let command_buffer = builder.build().unwrap();
let future = sync::now(self.device).then_execute(self.queue.clone(), command_buffer).unwrap().then_signal_fence_and_flush().unwrap(); future.wait(None).unwrap();
Ok(()) }
复制代码

里面缺少一个最关键的部分,就是 Buffer 的处理,众所不周知 Shader 的内存是很复杂的,对于宿主程序来说,写到显存 从显存读取 ,读写显存 都需要用不同的描述符 和槽位 但是最难的还是 如何把 Rust 的类型 映射到 buffer

我们使用 vulkano 提供的辅助结构

  let buffer = Buffer::new_slice::<Params>(        memory_allocator.clone(),        BufferCreateInfo { usage: BufferUsage::STORAGE_BUFFER | BufferUsage::TRANSFER_SRC | BufferUsage::TRANSFER_DST, ..Default::default() },        AllocationCreateInfo { memory_type_filter: MemoryTypeFilter::PREFER_DEVICE | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE, ..Default::default() },        1,    )?;
复制代码

这是创建一个 大小为 Params 的切片 然后使用那些 标志位来初始化 缓冲区

我们使用模板技术 可以这样改写 run Shader 的函数


    pub fn run<T: BufferContents, F: FnMut(&mut T)>(&mut self, shader: &[u32], mut f: F, dim: [u32; 3])-> Result<Subbuffer<[T]>> {        let shader = unsafe { ShaderModule::new(self.device.clone(), ShaderModuleCreateInfo::new(shader))? };        let entry_point = shader.entry_point("main").unwrap();        let stage = PipelineShaderStageCreateInfo::new(entry_point);        let layout = PipelineLayout::new(self.device.clone(), PipelineDescriptorSetLayoutCreateInfo::from_stages([&stage])            .into_pipeline_layout_create_info(self.device.clone())?)?;        let pipeline = ComputePipeline::new(self.device.clone(), None, ComputePipelineCreateInfo::stage_layout(stage, layout))?;        let memory_allocator = std::sync::Arc::new(StandardMemoryAllocator::new_default(self.device.clone()));        let command_buffer_allocator = std::sync::Arc::new(StandardCommandBufferAllocator::new(self.device.clone(), Default::default()));
let buffer = Buffer::new_slice::<T>( memory_allocator.clone(), BufferCreateInfo { usage: BufferUsage::STORAGE_BUFFER | BufferUsage::TRANSFER_SRC | BufferUsage::TRANSFER_DST, ..Default::default() }, AllocationCreateInfo { memory_type_filter: MemoryTypeFilter::PREFER_DEVICE | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE, ..Default::default() }, 1, )?; { let mut mapping = buffer.write()?; f(&mut mapping.as_mut()[0]); } let descriptor_set_allocator = std::sync::Arc::new(StandardDescriptorSetAllocator::new(self.device.clone(), Default::default())); let pipeline_layout = pipeline.layout();
let layout = &pipeline.layout().set_layouts()[0]; let set = DescriptorSet::new(descriptor_set_allocator, layout.clone(), [WriteDescriptorSet::buffer(0, buffer.clone())], [])?;
let mut builder = AutoCommandBufferBuilder::primary(command_buffer_allocator, self.queue.queue_family_index(), CommandBufferUsage::OneTimeSubmit).unwrap();
builder.bind_pipeline_compute(pipeline.clone())?; builder.bind_descriptor_sets(PipelineBindPoint::Compute, pipeline_layout.clone(), 0, set)?; //let copy_info = CopyBufferInfoTyped::buffers(buffer.clone().reinterpret::<[u8]>(),buffer.clone().reinterpret::<[u8]>()); //builder.copy_buffer(copy_info)?; let start = std::time::Instant::now(); unsafe { builder.dispatch(dim) }.unwrap(); let command_buffer = builder.build().unwrap(); let future = sync::now(self.device.clone()).then_execute(self.queue.clone(), command_buffer).unwrap().then_signal_fence_and_flush().unwrap(); future.wait(None).unwrap(); //let data_buffer_content = buffer.read().unwrap(); Ok(buffer) }
复制代码

使用一个 f 初始化数据

返回一个 Subbuffer<[T] 包括了 显存 拿回来的数据

可以用 read().unwrap()[0] 获取里面的数据


为了满足对齐的标准 我们的 T 要求满足 BufferContents 约束

我们使用 [u32; 1024] 作为 类型调用 Runtime

        let mut b = spirv::SpirvBuilder::default();        println!("{:?}", b.import_module(Module::from(compiler)));        let spirv_code = b.assemble();        let mut rt = spirv::Runtime::default();        println!("{:?}", rt.run::<[u32; 1024], _>(&spirv_code, |buf| {} , [1, 1, 100]));
复制代码


但是出错了,

奇怪的是 spirv-val 没有错误,这是一个合法的 Shader

所以问题出在 vulkano 库

好吧,我们需要在类型系统里面加上 struct 了

为了这碗醋,必须提前把饺子包好了,本来 Struct 的 类型系统是准备放在后面处理的。


用户头像

Miracle

关注

三十年资深码农 2019-10-25 加入

还未添加个人简介

评论

发布
暂无评论
spirv 运行在显卡上_Miracle_InfoQ写作社区