mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-03 00:20:57 -05:00
491 lines
17 KiB
Rust
491 lines
17 KiB
Rust
use std::sync::Arc;
|
|
|
|
use ash::vk;
|
|
|
|
use crate::*;
|
|
|
|
/// Timeout to wait for GPU execution in drop function.
|
|
static DROP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30 * 60);
|
|
|
|
/// GPU execution context.
|
|
/// It records commands and run them on GPU.
|
|
/// It keeps track of resources used in the commands.
|
|
/// Warnings!
|
|
/// Context is not thread safe.
|
|
/// Execution order is not guaranteed. Don't rely on it.
|
|
/// If you need to run commands in specific order, use `wait_finish` method.
|
|
/// And start next command after previous one is finished.
|
|
pub struct Context {
|
|
// Which device to execute on.
|
|
device: Arc<Device>,
|
|
|
|
// GPU execution handler.
|
|
vk_queue: vk::Queue,
|
|
|
|
// Command buffer is created using command pool.
|
|
vk_command_pool: vk::CommandPool,
|
|
|
|
// Command buffer is used to record commands to execute.
|
|
vk_command_buffer: vk::CommandBuffer,
|
|
|
|
// Synchronization fence to wait for GPU execution.
|
|
vk_fence: vk::Fence,
|
|
|
|
// Resources used in the context.
|
|
resources: Vec<Arc<dyn Resource>>,
|
|
}
|
|
|
|
impl Context {
|
|
pub fn new(device: Arc<Device>) -> GpuResult<Self> {
|
|
// Get GPU execution queue from device.
|
|
let queue = device.compute_queue();
|
|
|
|
// Create command pool.
|
|
let command_pool_create_info = vk::CommandPoolCreateInfo::default()
|
|
.queue_family_index(queue.vk_queue_family_index as u32)
|
|
.flags(vk::CommandPoolCreateFlags::default());
|
|
let vk_command_pool = unsafe {
|
|
device
|
|
.vk_device()
|
|
.create_command_pool(&command_pool_create_info, device.cpu_allocation_callbacks())?
|
|
};
|
|
|
|
// Create fence to wait for GPU execution.
|
|
// We create fence as signaled because we reset fence before start.
|
|
let fence_create_info =
|
|
vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED);
|
|
let vk_fence = unsafe {
|
|
device
|
|
.vk_device()
|
|
.create_fence(&fence_create_info, device.cpu_allocation_callbacks())
|
|
};
|
|
let vk_fence = match vk_fence {
|
|
Ok(fence) => fence,
|
|
Err(e) => {
|
|
// If fence creation failed, destroy created command pool and return error.
|
|
unsafe {
|
|
device
|
|
.vk_device()
|
|
.destroy_command_pool(vk_command_pool, device.cpu_allocation_callbacks());
|
|
}
|
|
return Err(GpuError::from(e));
|
|
}
|
|
};
|
|
|
|
let mut context = Self {
|
|
vk_queue: queue.vk_queue,
|
|
device,
|
|
vk_command_pool,
|
|
vk_command_buffer: vk::CommandBuffer::null(),
|
|
vk_fence,
|
|
resources: Vec::new(),
|
|
};
|
|
|
|
context.init_command_buffer()?;
|
|
|
|
Ok(context)
|
|
}
|
|
|
|
pub fn dispatch(&mut self, x: usize, y: usize, z: usize) -> GpuResult<()> {
|
|
if self.vk_command_buffer == vk::CommandBuffer::null() {
|
|
self.init_command_buffer()?;
|
|
}
|
|
|
|
let max_compute_work_group_count = self.device.max_compute_work_group_count();
|
|
if x > max_compute_work_group_count[0]
|
|
|| y > max_compute_work_group_count[1]
|
|
|| z > max_compute_work_group_count[2]
|
|
{
|
|
return Err(GpuError::OutOfBounds(
|
|
"Dispatch work group size is out of bounds".to_string(),
|
|
));
|
|
}
|
|
|
|
unsafe {
|
|
self.device.vk_device().cmd_dispatch(
|
|
self.vk_command_buffer,
|
|
x as u32,
|
|
y as u32,
|
|
z as u32,
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Barrier for buffers.
|
|
/// It's used to synchronize access to buffers between different shaders.
|
|
/// By Vulkan specification, resources between different shaders/transfers must be synchronized.
|
|
/// Example of compute shader sync:
|
|
/// https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#compute-to-compute-dependencies
|
|
/// In practice, we use `context.wait_finish()` to synchronize between different shaders.
|
|
/// But, for additional safety, we have this `barrier_buffers` to synchronize access to buffers.
|
|
pub fn barrier_buffers(&mut self, buffers: &[Arc<Buffer>]) -> GpuResult<()> {
|
|
if self.vk_command_buffer == vk::CommandBuffer::null() {
|
|
self.init_command_buffer()?;
|
|
}
|
|
|
|
let buffer_memory_barriers = buffers
|
|
.iter()
|
|
.map(|buffer| {
|
|
self.resources.push(buffer.clone() as Arc<dyn Resource>);
|
|
vk::BufferMemoryBarrier::default()
|
|
.buffer(buffer.vk_buffer())
|
|
.offset(0)
|
|
.size(buffer.size() as vk::DeviceSize)
|
|
.src_queue_family_index(
|
|
self.device.compute_queue().vk_queue_family_index as u32,
|
|
)
|
|
.dst_queue_family_index(
|
|
self.device.compute_queue().vk_queue_family_index as u32,
|
|
)
|
|
.src_access_mask(vk::AccessFlags::SHADER_WRITE)
|
|
.dst_access_mask(
|
|
vk::AccessFlags::TRANSFER_READ
|
|
| vk::AccessFlags::SHADER_READ
|
|
| vk::AccessFlags::SHADER_WRITE,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
unsafe {
|
|
self.device.vk_device().cmd_pipeline_barrier(
|
|
self.vk_command_buffer,
|
|
vk::PipelineStageFlags::COMPUTE_SHADER,
|
|
vk::PipelineStageFlags::ALL_COMMANDS,
|
|
vk::DependencyFlags::empty(),
|
|
&[],
|
|
&buffer_memory_barriers,
|
|
&[],
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Bind pipeline to the context.
|
|
/// It means which shader and binded resources to shader will be used.
|
|
/// It records command to run it on GPU after `run` call.
|
|
pub fn bind_pipeline(
|
|
&mut self,
|
|
pipeline: Arc<Pipeline>,
|
|
descriptor_sets: &[Arc<DescriptorSet>],
|
|
) -> GpuResult<()> {
|
|
if self.vk_command_buffer == vk::CommandBuffer::null() {
|
|
self.init_command_buffer()?;
|
|
}
|
|
|
|
unsafe {
|
|
self.device.vk_device().cmd_bind_pipeline(
|
|
self.vk_command_buffer,
|
|
vk::PipelineBindPoint::COMPUTE,
|
|
pipeline.vk_pipeline(),
|
|
);
|
|
}
|
|
|
|
unsafe {
|
|
if !descriptor_sets.is_empty() {
|
|
let vk_descriptor_sets: Vec<_> = descriptor_sets
|
|
.iter()
|
|
.map(|set| set.as_ref().vk_descriptor_set())
|
|
.collect();
|
|
self.device.vk_device().cmd_bind_descriptor_sets(
|
|
self.vk_command_buffer,
|
|
vk::PipelineBindPoint::COMPUTE,
|
|
pipeline.vk_pipeline_layout(),
|
|
0,
|
|
&vk_descriptor_sets,
|
|
&[],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add resources to the list to keep them alive.
|
|
self.resources.extend(
|
|
descriptor_sets
|
|
.iter()
|
|
.map(|r| r.clone() as Arc<dyn Resource>),
|
|
);
|
|
self.resources.push(pipeline);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Copy data from one buffer to another. It records command to run it on GPU after `run` call.
|
|
pub fn copy_gpu_buffer(
|
|
&mut self,
|
|
src: Arc<Buffer>,
|
|
dst: Arc<Buffer>,
|
|
src_offset: usize,
|
|
dst_offset: usize,
|
|
size: usize,
|
|
) -> GpuResult<()> {
|
|
if size == 0 {
|
|
return Ok(());
|
|
}
|
|
|
|
if self.vk_command_buffer == vk::CommandBuffer::null() {
|
|
self.init_command_buffer()?;
|
|
}
|
|
|
|
if src.size() < src_offset + size || dst.size() < dst_offset + size {
|
|
return Err(GpuError::OutOfBounds(
|
|
"Buffer copy out of bounds".to_string(),
|
|
));
|
|
}
|
|
|
|
let buffer_copy = vk::BufferCopy::default()
|
|
.src_offset(src_offset as vk::DeviceSize)
|
|
.dst_offset(dst_offset as vk::DeviceSize)
|
|
.size(size as vk::DeviceSize);
|
|
unsafe {
|
|
self.device.vk_device().cmd_copy_buffer(
|
|
self.vk_command_buffer,
|
|
src.vk_buffer(),
|
|
dst.vk_buffer(),
|
|
&[buffer_copy],
|
|
);
|
|
}
|
|
|
|
// Add resources to the list to keep them alive.
|
|
self.resources.push(src);
|
|
self.resources.push(dst);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Clear buffer with zeros command. It records command to run it on GPU after `run` call.
|
|
pub fn clear_buffer(&mut self, buffer: Arc<Buffer>) -> GpuResult<()> {
|
|
if buffer.size() == 0 {
|
|
return Ok(());
|
|
}
|
|
|
|
if !buffer.size().is_multiple_of(std::mem::size_of::<u32>()) {
|
|
return Err(GpuError::OutOfBounds(
|
|
"Buffer size must be a multiple of `uint32` size to clear it".to_string(),
|
|
));
|
|
}
|
|
|
|
if self.vk_command_buffer == vk::CommandBuffer::null() {
|
|
self.init_command_buffer()?;
|
|
}
|
|
|
|
unsafe {
|
|
self.device.vk_device().cmd_fill_buffer(
|
|
self.vk_command_buffer,
|
|
buffer.vk_buffer(),
|
|
0,
|
|
buffer.size() as vk::DeviceSize,
|
|
0,
|
|
);
|
|
}
|
|
|
|
// Add resources to the list to keep them alive.
|
|
self.resources.push(buffer);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Run the recorded commands on GPU.
|
|
/// Warning: order of recorded commands is not guaranteed. Don't rely on it.
|
|
pub fn run(&mut self) -> GpuResult<()> {
|
|
if self.vk_command_buffer == vk::CommandBuffer::null() {
|
|
// Nothing to run.
|
|
return Ok(());
|
|
}
|
|
|
|
// Finish recording of command buffer.
|
|
let end_record_result = unsafe {
|
|
self.device
|
|
.vk_device()
|
|
.end_command_buffer(self.vk_command_buffer)
|
|
};
|
|
|
|
// If command buffer recording failed, destroy created command buffer and return error.
|
|
if let Err(e) = end_record_result {
|
|
self.destroy_command_buffer();
|
|
return Err(GpuError::from(e));
|
|
}
|
|
|
|
// Reset fence to unsignaled state.
|
|
let fence_reset_result = unsafe { self.device.vk_device().reset_fences(&[self.vk_fence]) };
|
|
if let Err(e) = fence_reset_result {
|
|
self.destroy_command_buffer();
|
|
return Err(GpuError::from(e));
|
|
}
|
|
|
|
// Start execution of recorded commands.
|
|
let submit_buffers = [self.vk_command_buffer];
|
|
let submit_info = vec![vk::SubmitInfo::default().command_buffers(&submit_buffers)];
|
|
let submit_result = unsafe {
|
|
self.device
|
|
.vk_device()
|
|
.queue_submit(self.vk_queue, &submit_info, self.vk_fence)
|
|
};
|
|
|
|
if let Err(e) = submit_result {
|
|
// If submit failed, destroy created command buffer and return error.
|
|
// It's important here to avoid fence waiting of non-started command buffer.
|
|
self.destroy_command_buffer();
|
|
return Err(GpuError::from(e));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Wait for GPU execution to finish.
|
|
pub fn wait_finish(&mut self, timeout: std::time::Duration) -> GpuResult<()> {
|
|
if self.vk_command_buffer == vk::CommandBuffer::null() {
|
|
// Nothing to wait for.
|
|
return Ok(());
|
|
}
|
|
|
|
// Get the current status of fence.
|
|
let fence_status = unsafe {
|
|
self.device
|
|
.vk_device()
|
|
.get_fence_status(self.vk_fence)
|
|
.map_err(GpuError::from)
|
|
};
|
|
|
|
match fence_status {
|
|
Ok(true) => {
|
|
// GPU execution finished already, clear command buffer and return.
|
|
self.destroy_command_buffer();
|
|
Ok(())
|
|
}
|
|
Ok(false) => {
|
|
// GPU is processing. Wait for signal with timeout.
|
|
let wait_result = unsafe {
|
|
self.device
|
|
.vk_device()
|
|
.wait_for_fences(&[self.vk_fence], true, timeout.as_nanos() as u64)
|
|
.map_err(GpuError::from)
|
|
};
|
|
|
|
if matches!(wait_result, Err(GpuError::Timeout)) {
|
|
// If we detect timeout, don't clear command buffer, just return a timeout error.
|
|
Err(GpuError::Timeout)
|
|
} else {
|
|
// If the error is not a timeout, clear command buffer.
|
|
self.destroy_command_buffer();
|
|
wait_result
|
|
}
|
|
}
|
|
Err(e) => {
|
|
// By Vulkan specification, error while getting fence status
|
|
// may happen is special cases like hardware device lost.
|
|
// In this cases we don't care about status of gpu execution and just clear resources.
|
|
self.destroy_command_buffer();
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn init_command_buffer(&mut self) -> GpuResult<()> {
|
|
if self.vk_command_buffer != vk::CommandBuffer::null() {
|
|
return Err(GpuError::Other(
|
|
"Vulkan command buffer was already created".to_string(),
|
|
));
|
|
}
|
|
|
|
// Create new command buffer from pool.
|
|
let command_buffer_allocate_info = vk::CommandBufferAllocateInfo::default()
|
|
.command_pool(self.vk_command_pool)
|
|
.level(vk::CommandBufferLevel::PRIMARY)
|
|
.command_buffer_count(1);
|
|
self.vk_command_buffer = unsafe {
|
|
self.device
|
|
.vk_device()
|
|
.allocate_command_buffers(&command_buffer_allocate_info)?[0]
|
|
};
|
|
|
|
let command_buffer_begin_info =
|
|
vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::default());
|
|
//.inheritance_info(..);
|
|
|
|
let begin_result = unsafe {
|
|
self.device
|
|
.vk_device()
|
|
.begin_command_buffer(self.vk_command_buffer, &command_buffer_begin_info)
|
|
};
|
|
|
|
// If command buffer creation failed, destroy created command buffer and return error.
|
|
if let Err(e) = begin_result {
|
|
self.destroy_command_buffer();
|
|
return Err(GpuError::from(e));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn destroy_command_buffer(&mut self) {
|
|
if self.vk_command_buffer != vk::CommandBuffer::null() {
|
|
unsafe {
|
|
self.device
|
|
.vk_device()
|
|
.free_command_buffers(self.vk_command_pool, &[self.vk_command_buffer]);
|
|
}
|
|
self.vk_command_buffer = vk::CommandBuffer::null();
|
|
}
|
|
self.resources.clear();
|
|
}
|
|
}
|
|
|
|
impl Drop for Context {
|
|
fn drop(&mut self) {
|
|
let wait_result = self.wait_finish(DROP_TIMEOUT);
|
|
match wait_result {
|
|
Err(GpuError::Timeout) => {
|
|
// Timeout reached, resources are still in use.
|
|
// Vulkan API cannot stop GPU execution.
|
|
// This sutiation may appear if shader has infinite loop, etc.
|
|
// There is no good way to handle this error.
|
|
// So just log it and ignore resources deallocation.
|
|
// This approach may cause memory leaks and used gpu kernels,
|
|
// but it's better than potential segfault.
|
|
log::error!("Failed to wait for GPU context to finish");
|
|
|
|
// Error was logged, do memory leak to keep the gpu running.
|
|
let resources = self.resources.clone();
|
|
self.resources.clear();
|
|
for resource in resources {
|
|
// !!!!!!!!!
|
|
std::mem::forget(resource);
|
|
}
|
|
}
|
|
// If there is no timeout, we can safely deallocate resources.
|
|
wait_result => {
|
|
wait_result.unwrap_or_else(|e|
|
|
// Cannot return error from Drop trait.
|
|
// Log it instead.
|
|
log::error!("Error while clear GPU context: {e:?}"));
|
|
|
|
// If command buffer was not destroyed, destroy it.
|
|
// This situation may appear if `wait_finish` is an error.
|
|
self.destroy_command_buffer();
|
|
|
|
// Destroy fence.
|
|
if self.vk_fence != vk::Fence::null() {
|
|
unsafe {
|
|
self.device
|
|
.vk_device()
|
|
.destroy_fence(self.vk_fence, self.device.cpu_allocation_callbacks());
|
|
}
|
|
self.vk_fence = vk::Fence::null();
|
|
}
|
|
|
|
// Destroy command pool.
|
|
if self.vk_command_pool != vk::CommandPool::null() {
|
|
unsafe {
|
|
self.device.vk_device().destroy_command_pool(
|
|
self.vk_command_pool,
|
|
self.device.cpu_allocation_callbacks(),
|
|
);
|
|
}
|
|
self.vk_command_pool = vk::CommandPool::null();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|