spirv 运行在显卡上
- 2025-09-24 四川
本文字数:4960 字
阅读完需:约 16 分钟

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 加入
还未添加个人简介
评论