use std::marker::PhantomData;
use super::storage::{WgpuResource, WgpuStorage};
use crate::WgpuCompiler;
use crate::schedule::{BindingsResource, ScheduleTask, ScheduledWgpuBackend};
use alloc::sync::Arc;
use cubecl_common::pool::LeasePool;
use cubecl_common::{
bytes::Bytes,
profile::{ProfileDuration, TimingMethod},
};
use cubecl_core::server::{Binding, StreamErrorMode};
use cubecl_core::zspace::Shape;
use cubecl_core::{
MemoryConfiguration, WgpuCompilationOptions,
prelude::*,
server::{
CopyDescriptor, IoError, KernelArguments, LaunchError, ProfileError, ProfilingToken,
ServerCommunication, ServerError, ServerUtilities,
},
zspace::{Strides, strides},
};
use cubecl_environment::backtrace::BackTrace;
use cubecl_environment::future::DynFut;
#[cfg(feature = "spirv")]
use cubecl_environment::persistence::Store;
use cubecl_environment::stream::StreamId;
use cubecl_ir::MemoryDeviceProperties;
use cubecl_runtime::allocator::ContiguousMemoryLayoutPolicy;
#[cfg(feature = "spirv")]
use cubecl_runtime::compiler::{KernelCacheKey, compilation_store, store_compiled};
use cubecl_runtime::memory_management::{ManagedMemoryHandle, MemoryUsage, SharedMemoryBindings};
use cubecl_runtime::{
compiler::{CompilationCache, CubeTask},
config::{CubeClRuntimeConfig, RuntimeConfig},
dry_run::LaunchMode,
logging::ServerLogger,
memory_management::MemoryAllocationMode,
server::ComputeServer,
storage::ManagedResource,
stream::scheduler::{
SchedulerMultiStream, SchedulerMultiStreamOptions, SchedulerStrategy,
SchedulerStreamBackend,
},
validation::{validate_cube_dim, validate_units},
};
use wgpu::ComputePipeline;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamsTransfer {
Immediate,
Uniform,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompilerInfo {
Vulkan { params_transfer: ParamsTransfer },
Metal,
WGSL,
None,
}
#[derive(Debug)]
pub struct WgpuServer<C: WgpuCompiler> {
pub(crate) device: wgpu::Device,
streams_pool: Vec<StreamId>,
pipelines: CompilationCache<KernelId, (Arc<ComputePipeline>, CompilerInfo)>,
scheduler: SchedulerMultiStream<ScheduledWgpuBackend>,
#[cfg(feature = "spirv")]
pub(crate) spirv_cache: Option<Store<(u64, KernelCacheKey), cubecl_spirv::SpirvCacheEntry>>,
pub compilation_options: WgpuCompilationOptions,
pub(crate) backend: wgpu::Backend,
pub(crate) utilities: Arc<ServerUtilities<Self>>,
shared_bindings_pool: LeasePool<SharedMemoryBindings>,
_compiler: PhantomData<C>,
}
impl<C: WgpuCompiler> ServerCommunication for WgpuServer<C> {
const SERVER_COMM_ENABLED: bool = false;
}
impl<C: WgpuCompiler> WgpuServer<C> {
#[allow(clippy::too_many_arguments)]
pub fn new(
memory_properties: MemoryDeviceProperties,
memory_config: MemoryConfiguration,
compilation_options: WgpuCompilationOptions,
device: wgpu::Device,
queue: wgpu::Queue,
tasks_max: usize,
backend: wgpu::Backend,
timing_method: TimingMethod,
utilities: ServerUtilities<Self>,
) -> Self {
#[cfg(feature = "spirv")]
let adapter_info = device.adapter_info();
let backend_scheduler = ScheduledWgpuBackend::new(
device.clone(),
queue.clone(),
memory_properties,
memory_config,
timing_method,
backend,
tasks_max,
utilities.logger.clone(),
compilation_options.supports_vulkan_compiler,
);
let config = CubeClRuntimeConfig::get();
let max_streams = config.streaming.max_streams;
#[cfg(feature = "spirv")]
let spirv_cache = compilation_store(
"vulkan",
format!("spirv_{}_{}", adapter_info.vendor, adapter_info.device),
);
#[cfg(feature = "spirv")]
let pipelines = CompilationCache::mirroring(&spirv_cache);
#[cfg(not(feature = "spirv"))]
let pipelines = CompilationCache::unbound();
Self {
compilation_options,
streams_pool: Vec::new(),
device,
pipelines,
scheduler: SchedulerMultiStream::new(
utilities.logger.clone(),
backend_scheduler,
SchedulerMultiStreamOptions {
max_streams,
max_tasks: tasks_max,
strategy: SchedulerStrategy::Interleave,
},
),
#[cfg(feature = "spirv")]
spirv_cache,
backend,
utilities: Arc::new(utilities),
shared_bindings_pool: LeasePool::with_capacity(tasks_max * max_streams as usize),
_compiler: PhantomData,
}
}
fn prepare_bindings(
&mut self,
bindings: KernelArguments,
compiler_info: CompilerInfo,
) -> Result<BindingsResource, IoError> {
let mut resources = Vec::with_capacity(bindings.buffers.len());
for b in bindings.buffers.into_iter() {
let stream = self.scheduler.stream(&b.stream);
let resource = stream.mem_manage.get_resource(b)?;
resources.push(resource);
}
Ok(BindingsResource {
resources,
info: bindings.info,
compiler_info,
})
}
fn pipeline(
&mut self,
kernel: <Self as ComputeServer>::Kernel,
bindings: &KernelArguments,
mode: ExecutionMode,
) -> Result<(Arc<ComputePipeline>, CompilerInfo), LaunchError> {
let mut kernel_id = kernel.id();
kernel_id.mode(mode);
if let Some(pipeline) = self.pipelines.get(&kernel_id) {
return Ok(pipeline.clone());
}
let definition = kernel.define();
let cached = self.load_cached_pipeline(&kernel_id, &definition, bindings, mode)?;
if let Some(Ok(pipeline)) = cached {
self.pipelines.insert(kernel_id, pipeline.clone());
return Ok(pipeline);
}
validate_cube_dim(&self.utilities.properties, &kernel_id)?;
validate_units(&self.utilities.properties, &kernel_id)?;
let mut compiler = C::init(self.backend, &self.compilation_options);
let mut compiled = compiler.compile_kernel(self, kernel, definition, mode)?;
if self.scheduler.logger.compilation_source_activated() {
compiled.debug_info = Some(DebugInformation::new(
compiler.lang_tag(),
kernel_id.clone(),
));
}
self.scheduler.logger.log_compilation(&compiled);
compiler.validate_ir(&compiled.repr, &self.utilities.properties)?;
let (compiler_info, auto_repr) = compiler.normalize_repr(compiled.repr);
let repr = auto_repr.as_ref().map(|r| r.as_ref());
let module = self.create_module(
&compiled.entrypoint_name,
kernel_id.cube_dim,
repr,
&compiled.source,
mode,
)?;
let pipeline = self.create_pipeline(&compiled.entrypoint_name, repr, module, bindings);
self.pipelines
.insert(kernel_id.clone(), (pipeline.clone(), compiler_info));
#[cfg(feature = "spirv")]
if let Some(Err(key)) = cached
&& let Some(crate::AutoRepresentation::SpirV(kernel)) = auto_repr
{
let cache = self.spirv_cache.as_mut().unwrap();
store_compiled(
cache,
key,
cubecl_spirv::SpirvCacheEntry::new(compiled.entrypoint_name, kernel),
);
}
Ok((pipeline, compiler_info))
}
}
impl<C: WgpuCompiler> ComputeServer for WgpuServer<C> {
type Kernel = Box<dyn CubeTask<C>>;
type Storage = WgpuStorage;
type MemoryLayoutPolicy = ContiguousMemoryLayoutPolicy;
type Info = wgpu::Backend;
fn logger(&self) -> Arc<ServerLogger> {
self.scheduler.logger.clone()
}
fn utilities(&self) -> Arc<ServerUtilities<Self>> {
self.utilities.clone()
}
fn staging(
&mut self,
_sizes: &[usize],
_stream_id: StreamId,
) -> Result<Vec<Bytes>, ServerError> {
Err(IoError::UnsupportedIoOperation {
backtrace: BackTrace::capture(),
}
.into())
}
fn initialize_memory(&mut self, memory: ManagedMemoryHandle, size: u64, stream_id: StreamId) {
let stream = self.scheduler.stream(&stream_id);
let reserved = stream
.empty(size)
.unwrap_or_else(|err| panic!("failed to reserve {size} bytes of device memory: {err}"));
stream.mem_manage.bind(reserved, memory);
}
fn read(
&mut self,
descriptors: Vec<CopyDescriptor>,
stream_id: StreamId,
) -> DynFut<Result<Vec<Bytes>, ServerError>> {
let mut streams = vec![stream_id];
let mut resources = Vec::with_capacity(descriptors.len());
for desc in descriptors {
if contiguous_strides(&desc.shape) != desc.strides {
return Box::pin(async {
Err(IoError::UnsupportedStrides {
backtrace: BackTrace::capture(),
}
.into())
});
}
if !streams.contains(&desc.handle.stream) {
streams.push(desc.handle.stream);
}
let stream = self.scheduler.stream(&desc.handle.stream);
let resource = match stream.mem_manage.get_resource(desc.handle) {
Ok(val) => val,
Err(err) => return Box::pin(async move { Err(err.into()) }),
};
resources.push((resource, desc.shape, desc.elem_size));
}
self.scheduler.execute_streams(streams);
let stream = self.scheduler.stream(&stream_id);
stream.read_resources(resources)
}
fn write(&mut self, descriptors: Vec<(CopyDescriptor, Bytes)>, stream_id: StreamId) {
for (desc, data) in descriptors {
let stream = self.scheduler.stream(&desc.handle.stream);
if contiguous_strides(&desc.shape) != desc.strides {
stream.error(ServerError::Io(IoError::UnsupportedStrides {
backtrace: BackTrace::capture(),
}));
return;
}
let resource = match stream.mem_manage.get_resource(desc.handle) {
Ok(r) => r,
Err(err) => {
stream.error(ServerError::Io(err));
return;
}
};
let task = ScheduleTask::Write {
data,
buffer: resource,
};
self.scheduler.register(stream_id, task, &[]);
}
}
fn get_resource(
&mut self,
binding: Binding,
stream_id: StreamId,
) -> Result<ManagedResource<WgpuResource>, ServerError> {
let mut streams = vec![stream_id];
if binding.stream != stream_id {
streams.push(binding.stream);
}
self.scheduler.execute_streams(streams);
let stream = self.scheduler.stream(&binding.stream);
let memory = binding.memory.clone();
let resource = stream.mem_manage.get_resource(binding)?;
Ok(ManagedResource::new(memory, resource))
}
unsafe fn launch(
&mut self,
kernel: Self::Kernel,
count: CubeCount,
args: KernelArguments,
mode: ExecutionMode,
stream_id: StreamId,
launch_mode: LaunchMode,
) {
let (pipeline, compiler_info) = match self.pipeline(kernel, &args, mode) {
Ok(val) => val,
Err(err) => {
let stream = self.scheduler.stream(&stream_id);
stream.errors.push(ServerError::Launch(err));
return;
}
};
if launch_mode.is_skipped() {
return;
}
self.streams_pool.clear();
let mut shared_inputs = self.shared_bindings_pool.acquire();
args.buffers.iter().for_each(|b| {
self.streams_pool.push(b.stream);
if b.stream != stream_id {
shared_inputs.push(b.memory.clone());
}
});
let resources = match self.prepare_bindings(args, compiler_info) {
Ok(val) => val,
Err(err) => {
let stream = self.scheduler.stream(&stream_id);
stream.errors.push(ServerError::Io(err));
return;
}
};
let task = ScheduleTask::Execute {
pipeline,
count,
resources,
shared_inputs,
};
self.scheduler.register(stream_id, task, &self.streams_pool);
}
fn flush(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
self.scheduler.execute_streams(vec![stream_id]);
let stream = self.scheduler.stream(&stream_id);
stream.flush(StreamErrorMode {
ignore: false,
flush: true,
})
}
fn sync(&mut self, stream_id: StreamId) -> DynFut<Result<(), ServerError>> {
self.scheduler.execute_streams(vec![stream_id]);
let stream = self.scheduler.stream(&stream_id);
stream.sync()
}
fn start_profile(&mut self, stream_id: StreamId) -> Result<ProfilingToken, ServerError> {
self.scheduler.execute_streams(vec![stream_id]);
let stream = self.scheduler.stream(&stream_id);
stream.start_profile()
}
fn end_profile(
&mut self,
stream_id: StreamId,
token: ProfilingToken,
) -> Result<ProfileDuration, ProfileError> {
self.scheduler.execute_streams(vec![stream_id]);
let stream = self.scheduler.stream(&stream_id);
stream.end_profile(token)
}
fn memory_usage(&mut self, stream_id: StreamId) -> Result<MemoryUsage, ServerError> {
self.scheduler.execute_streams(vec![stream_id]);
let stream = self.scheduler.stream(&stream_id);
Ok(stream.mem_manage.memory_usage())
}
fn stream_ids(&self) -> Vec<StreamId> {
self.scheduler.stream_ids().collect()
}
fn memory_cleanup(&mut self, stream_id: StreamId) {
self.scheduler.execute_streams(vec![stream_id]);
let stream = self.scheduler.stream(&stream_id);
stream.mem_manage.memory_cleanup(true);
}
fn allocation_mode(&mut self, mode: MemoryAllocationMode, stream_id: StreamId) {
self.scheduler.execute_streams(vec![stream_id]);
let stream = self.scheduler.stream(&stream_id);
stream.mem_manage.mode(mode);
}
fn configure_memory_pools(&mut self, config: MemoryConfiguration, stream_id: StreamId) -> bool {
self.scheduler
.backend_mut()
.factory()
.set_gpu_pools(config.clone());
let (_, props) = self.scheduler.backend_mut().factory().gpu_pools();
self.scheduler.execute_streams(vec![stream_id]);
let stream = self.scheduler.stream(&stream_id);
stream.mem_manage.configure_memory_pools(config, &props)
}
}
pub(crate) fn contiguous_strides(shape: &Shape) -> Strides {
let rank = shape.len();
let mut strides = strides![1; rank];
for i in (0..rank - 1).rev() {
strides[i] = strides[i + 1] * shape[i + 1];
}
strides
}