use crate::memory::manager::WgpuMemManager;
use super::{poll::WgpuPoll, timings::{QueryProfiler, TimestampQuerySetBudget, TimestampSampling}};
use crate::{WgpuResource, memory::controller::WgpuAllocController, schedule::ScheduleTask};
use ruda_core::{
backtrace::BackTrace,
bytes::Bytes,
profile::{ProfileDuration, TimingMethod},
};
use ruda_kernel::dsl::{
RudaCount, MemoryConfiguration,
future::{self, DynFut},
server::{IoError, ProfileError, ProfilingToken, ServerError, StreamErrorMode},
zspace::Shape,
};
use ruda_core::ir::MemoryDeviceProperties;
use ruda::runtime::{
logging::ServerLogger, memory_management::ManagedMemoryHandle,
timestamp_profiler::TimestampProfiler,
};
use std::{future::Future, num::NonZero, pin::Pin, sync::Arc};
use wgpu::ComputePipeline;
#[derive(Debug)]
enum Timings {
Unclaimed { budget: Arc<TimestampQuerySetBudget>, sampling: TimestampSampling },
Device(Box<QueryProfiler>),
System(TimestampProfiler),
}
impl Timings {
fn system() -> Self {
if cfg!(target_family = "wasm") {
panic!("Cannot profile on web assembly without timestamp_query feature as it requires blocking.");
}
Self::System(TimestampProfiler::default())
}
}
#[derive(Debug)]
pub struct WgpuStream {
pub mem_manage: WgpuMemManager,
pub device: wgpu::Device,
poison: super::device_poison::PoisonWatch,
compute_pass: Option<wgpu::ComputePass<'static>>,
timings: Timings,
tasks_count: usize,
tasks_max: usize,
queue: wgpu::Queue,
encoder: wgpu::CommandEncoder,
poll: WgpuPoll,
submission_load: SubmissionLoad,
pending_write_count: usize,
pending_address_pins: Vec<ruda::runtime::memory_management::MemoryResourcePin>,
}
impl WgpuStream {
pub fn new(
device: wgpu::Device,
queue: wgpu::Queue,
memory_properties: MemoryDeviceProperties,
memory_config: MemoryConfiguration,
timing_method: TimingMethod,
timing_budget: Arc<TimestampQuerySetBudget>,
timestamp_sampling: TimestampSampling,
tasks_max: usize,
logger: Arc<ServerLogger>,
poison: super::device_poison::PoisonWatch,
) -> Self {
let timings = if timing_method == TimingMethod::Device {
Timings::Unclaimed { budget: timing_budget, sampling: timestamp_sampling }
} else {
Timings::system()
};
let poll = WgpuPoll::new(device.clone());
#[allow(unused_mut)]
let mut mem_manage =
WgpuMemManager::new(device.clone(), queue.clone(), memory_properties, memory_config, logger);
Self {
mem_manage,
compute_pass: None,
timings,
poison,
encoder: {
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Ruda Tasks Encoder"),
})
},
device,
queue,
tasks_count: 0,
tasks_max,
poll,
submission_load: SubmissionLoad::default(),
pending_write_count: 0,
pending_address_pins: Vec::new(),
}
}
pub fn enqueue_task(&mut self, task: ScheduleTask) {
match task {
ScheduleTask::Write { data, buffer, destination: _destination } => {
if let Err(error) = self.flush(StreamErrorMode { ignore: true, flush: false }) {
self.error(error);
return;
}
self.write_to_buffer(&buffer, &data);
}
ScheduleTask::Execute {
pipeline,
count,
count_resource,
resources,
execution,
} => {
if let Err(error) = self.poison.check() {
self.error(error);
return;
}
let resources = resources.into_resources(self);
match self.register_pipeline(pipeline, resources.iter(), &count, count_resource.as_ref()) {
Ok(()) => execution.complete(),
Err(error) => {
self.error(error);
}
}
}
}
}
pub fn sync(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<(), ServerError>> + Send + 'static>> {
if let Err(error) = self.poison.check() { return Box::pin(async move { Err(error) }); }
let error_scope = self.device.push_error_scope(wgpu::ErrorFilter::Internal);
let flush_error = self
.flush(StreamErrorMode {
ignore: false,
flush: true,
})
.err();
let queue = self.queue.clone();
let error_future = error_scope.pop();
let poll = self.poll.start_polling();
let poison = self.poison.clone();
Box::pin(async move {
let (sender, receiver) = async_channel::bounded::<()>(1);
queue.on_submitted_work_done(move || {
let _ = sender.try_send(());
core::mem::drop(poll);
});
let _ = receiver.recv().await;
poison.check()?;
let scoped_error = error_future.await;
poison.check()?;
if let Some(error) = scoped_error {
return Err(ServerError::Generic {
reason: format!("{error}"),
backtrace: BackTrace::capture(),
});
}
match flush_error {
Some(err) => Err(err),
None => Ok(()),
}
})
}
pub fn empty(&mut self, size: u64) -> Result<ManagedMemoryHandle, IoError> {
self.poison.check().map_err(|error| IoError::Execution(Box::new(error)))?;
if self.mem_manage.relocation_pending(size) {
self.flush(StreamErrorMode { ignore: false, flush: false })
.map_err(|error| IoError::Execution(Box::new(error)))?;
}
self.mem_manage.reserve(size)
}
pub(crate) fn check_device(&self) -> Result<(), ServerError> { self.poison.check() }
pub fn error(&mut self, error: ServerError) {
self.profile_error(ProfileError::Server(Box::new(error.clone())));
}
pub(crate) fn create_uniform(&mut self, data: &[u8]) -> WgpuResource {
let resource = self.mem_manage.reserve_uniform(data.len() as u64);
self.write_to_buffer(&resource, data);
resource
}
fn flush_if_needed(&mut self) {
if self.tasks_count >= self.tasks_max {
let _ = self
.flush(StreamErrorMode {
ignore: true,
flush: false,
})
.ok();
}
}
pub fn flush(&mut self, _mode: StreamErrorMode) -> Result<(), ServerError> {
self.poison.check()?;
if self.tasks_count == 0 {
return Ok(());
}
self.compute_pass = None;
let tasks_encoder = {
std::mem::replace(&mut self.encoder, {
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Ruda Tasks Encoder"),
})
})
};
let index = self.queue.submit([tasks_encoder.finish()]);
self.pending_address_pins.clear();
self.submission_load
.regulate(&self.device, self.tasks_count, index);
self.mem_manage.memory_cleanup(false);
self.mem_manage.release_uniforms();
self.tasks_count = 0;
self.pending_write_count = 0;
self.poison.check()
}
fn register_pipeline<'a>(
&mut self,
pipeline: Arc<ComputePipeline>,
resources: impl Iterator<Item = &'a WgpuResource>,
dispatch: &RudaCount,
count_resource: Option<&WgpuResource>,
) -> Result<(), ServerError> {
if dispatch.is_empty() {
return Ok(());
}
let entries = resources
.enumerate()
.map(|(i, r)| {
if let Some(pin) = r.address_pin() { self.pending_address_pins.push(pin); }
wgpu::BindGroupEntry {
binding: i as u32,
resource: r.as_wgpu_bind_resource(),
}
})
.collect::<Vec<_>>();
let pass = self.compute_pass.get_or_insert_with(|| {
let writes = if let Timings::Device(query_time) = &mut self.timings {
query_time.register_profile_device(&self.device)
} else {
None
};
self.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: writes,
})
.forget_lifetime()
});
self.tasks_count += 1;
let group_layout = pipeline.get_bind_group_layout(0);
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &group_layout,
entries: &entries,
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group, &[]);
match dispatch.clone() {
RudaCount::Static(x, y, z) => {
pass.dispatch_workgroups(x, y, z);
}
RudaCount::Dynamic(_) => {
let res = count_resource.ok_or_else(|| IoError::NotFound {
reason: "Missing indirect dispatch buffer".into(),
backtrace: BackTrace::capture(),
})?;
if let Some(pin) = res.address_pin() { self.pending_address_pins.push(pin); }
pass.dispatch_workgroups_indirect(&res.buffer, res.offset);
}
}
self.flush_if_needed();
Ok(())
}
}
mod io;
mod profiling;
mod submission;
use submission::*;