Skip to main content

cubecl_wgpu/compute/
server.rs

1use std::marker::PhantomData;
2
3use super::storage::{WgpuResource, WgpuStorage};
4use crate::WgpuCompiler;
5use crate::schedule::{BindingsResource, ScheduleTask, ScheduledWgpuBackend};
6use alloc::sync::Arc;
7use cubecl_common::pool::LeasePool;
8use cubecl_common::{
9    bytes::Bytes,
10    profile::{ProfileDuration, TimingMethod},
11};
12use cubecl_core::server::{Binding, StreamErrorMode};
13use cubecl_core::zspace::Shape;
14use cubecl_core::{
15    MemoryConfiguration, WgpuCompilationOptions,
16    prelude::*,
17    server::{
18        CopyDescriptor, IoError, KernelArguments, LaunchError, ProfileError, ProfilingToken,
19        ServerCommunication, ServerError, ServerUtilities,
20    },
21    zspace::{Strides, strides},
22};
23use cubecl_environment::backtrace::BackTrace;
24use cubecl_environment::future::DynFut;
25#[cfg(feature = "spirv")]
26use cubecl_environment::persistence::Store;
27use cubecl_environment::stream::StreamId;
28use cubecl_ir::MemoryDeviceProperties;
29use cubecl_runtime::allocator::ContiguousMemoryLayoutPolicy;
30#[cfg(feature = "spirv")]
31use cubecl_runtime::compiler::{KernelCacheKey, compilation_store, store_compiled};
32use cubecl_runtime::memory_management::{ManagedMemoryHandle, MemoryUsage, SharedMemoryBindings};
33use cubecl_runtime::{
34    compiler::{CompilationCache, CubeTask},
35    config::{CubeClRuntimeConfig, RuntimeConfig},
36    dry_run::LaunchMode,
37    logging::ServerLogger,
38    memory_management::MemoryAllocationMode,
39    server::ComputeServer,
40    storage::ManagedResource,
41    stream::scheduler::{
42        SchedulerMultiStream, SchedulerMultiStreamOptions, SchedulerStrategy,
43        SchedulerStreamBackend,
44    },
45    validation::{validate_cube_dim, validate_units},
46};
47use wgpu::ComputePipeline;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ParamsTransfer {
51    Immediate,
52    Uniform,
53}
54
55/// Compiler kind and info used when compiling a specific kernel. Used to determine parameter passing strategies.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum CompilerInfo {
58    Vulkan { params_transfer: ParamsTransfer },
59    Metal,
60    WGSL,
61    None,
62}
63
64/// Wgpu compute server.
65#[derive(Debug)]
66pub struct WgpuServer<C: WgpuCompiler> {
67    pub(crate) device: wgpu::Device,
68    // A buffer that can be used to store stream id without extra allocations.
69    streams_pool: Vec<StreamId>,
70    /// The pipelines built so far, in front of the SPIR-V store when there is
71    /// one.
72    pipelines: CompilationCache<KernelId, (Arc<ComputePipeline>, CompilerInfo)>,
73    scheduler: SchedulerMultiStream<ScheduledWgpuBackend>,
74    #[cfg(feature = "spirv")]
75    pub(crate) spirv_cache: Option<Store<(u64, KernelCacheKey), cubecl_spirv::SpirvCacheEntry>>,
76    pub compilation_options: WgpuCompilationOptions,
77    pub(crate) backend: wgpu::Backend,
78    pub(crate) utilities: Arc<ServerUtilities<Self>>,
79    /// Reusable buffers for the cross-stream input bindings of each launch.
80    shared_bindings_pool: LeasePool<SharedMemoryBindings>,
81    _compiler: PhantomData<C>,
82}
83
84impl<C: WgpuCompiler> ServerCommunication for WgpuServer<C> {
85    const SERVER_COMM_ENABLED: bool = false;
86}
87
88impl<C: WgpuCompiler> WgpuServer<C> {
89    /// Create a new server.
90    #[allow(clippy::too_many_arguments)]
91    pub fn new(
92        memory_properties: MemoryDeviceProperties,
93        memory_config: MemoryConfiguration,
94        compilation_options: WgpuCompilationOptions,
95        device: wgpu::Device,
96        queue: wgpu::Queue,
97        tasks_max: usize,
98        backend: wgpu::Backend,
99        timing_method: TimingMethod,
100        utilities: ServerUtilities<Self>,
101    ) -> Self {
102        #[cfg(feature = "spirv")]
103        let adapter_info = device.adapter_info();
104        let backend_scheduler = ScheduledWgpuBackend::new(
105            device.clone(),
106            queue.clone(),
107            memory_properties,
108            memory_config,
109            timing_method,
110            backend,
111            tasks_max,
112            utilities.logger.clone(),
113            compilation_options.supports_vulkan_compiler,
114        );
115
116        let config = CubeClRuntimeConfig::get();
117        let max_streams = config.streaming.max_streams;
118
119        #[cfg(feature = "spirv")]
120        let spirv_cache = compilation_store(
121            "vulkan",
122            format!("spirv_{}_{}", adapter_info.vendor, adapter_info.device),
123        );
124
125        // WGSL is compiled by the driver on every run, so without the SPIR-V
126        // store there is nothing persisted for a switch to invalidate.
127        #[cfg(feature = "spirv")]
128        let pipelines = CompilationCache::mirroring(&spirv_cache);
129        #[cfg(not(feature = "spirv"))]
130        let pipelines = CompilationCache::unbound();
131
132        Self {
133            compilation_options,
134            streams_pool: Vec::new(),
135            device,
136            pipelines,
137            scheduler: SchedulerMultiStream::new(
138                utilities.logger.clone(),
139                backend_scheduler,
140                SchedulerMultiStreamOptions {
141                    max_streams,
142                    max_tasks: tasks_max,
143                    strategy: SchedulerStrategy::Interleave,
144                },
145            ),
146            #[cfg(feature = "spirv")]
147            spirv_cache,
148            backend,
149            utilities: Arc::new(utilities),
150            shared_bindings_pool: LeasePool::with_capacity(tasks_max * max_streams as usize),
151            _compiler: PhantomData,
152        }
153    }
154
155    fn prepare_bindings(
156        &mut self,
157        bindings: KernelArguments,
158        compiler_info: CompilerInfo,
159    ) -> Result<BindingsResource, IoError> {
160        // Store all the resources we'll be using. This could be eliminated if
161        // there was a way to tie the lifetime of the resource to the memory handle.
162        let mut resources = Vec::with_capacity(bindings.buffers.len());
163
164        for b in bindings.buffers.into_iter() {
165            let stream = self.scheduler.stream(&b.stream);
166            let resource = stream.mem_manage.get_resource(b)?;
167            resources.push(resource);
168        }
169
170        Ok(BindingsResource {
171            resources,
172            info: bindings.info,
173            compiler_info,
174        })
175    }
176
177    fn pipeline(
178        &mut self,
179        kernel: <Self as ComputeServer>::Kernel,
180        bindings: &KernelArguments,
181        mode: ExecutionMode,
182    ) -> Result<(Arc<ComputePipeline>, CompilerInfo), LaunchError> {
183        let mut kernel_id = kernel.id();
184        kernel_id.mode(mode);
185
186        if let Some(pipeline) = self.pipelines.get(&kernel_id) {
187            return Ok(pipeline.clone());
188        }
189
190        let definition = kernel.define();
191        let cached = self.load_cached_pipeline(&kernel_id, &definition, bindings, mode)?;
192
193        if let Some(Ok(pipeline)) = cached {
194            self.pipelines.insert(kernel_id, pipeline.clone());
195            return Ok(pipeline);
196        }
197
198        validate_cube_dim(&self.utilities.properties, &kernel_id)?;
199        validate_units(&self.utilities.properties, &kernel_id)?;
200
201        let mut compiler = C::init(self.backend, &self.compilation_options);
202        let mut compiled = compiler.compile_kernel(self, kernel, definition, mode)?;
203
204        if self.scheduler.logger.compilation_source_activated() {
205            compiled.debug_info = Some(DebugInformation::new(
206                compiler.lang_tag(),
207                kernel_id.clone(),
208            ));
209        }
210        self.scheduler.logger.log_compilation(&compiled);
211
212        compiler.validate_ir(&compiled.repr, &self.utilities.properties)?;
213        let (compiler_info, auto_repr) = compiler.normalize_repr(compiled.repr);
214        let repr = auto_repr.as_ref().map(|r| r.as_ref());
215
216        // /!\ Do not delete the following commented code.
217        // This is useful while working on the metal compiler.
218        // Also the errors are printed nicely which is not the case when this is the runtime
219        // that does it.
220        // {
221        //     // Write shader in metal file then compile it for error
222        //     std::fs::write("shader.metal", &compiled.source).expect("should write to file");
223        //     let status = std::process::Command::new("xcrun")
224        //         .args(vec![
225        //             "-sdk",
226        //             "macosx",
227        //             "metal",
228        //             "-o",
229        //             "shader.ir",
230        //             "-c",
231        //             "shader.metal",
232        //             "-w",
233        //         ])
234        //         .status()
235        //         .expect("should launch the command");
236        //     if !status.success() {
237        //         println!("SOURCE:\n{}", compiled.source);
238        //         std::process::exit(status.code().unwrap());
239        //     }
240        // }
241
242        let module = self.create_module(
243            &compiled.entrypoint_name,
244            kernel_id.cube_dim,
245            repr,
246            &compiled.source,
247            mode,
248        )?;
249        let pipeline = self.create_pipeline(&compiled.entrypoint_name, repr, module, bindings);
250        self.pipelines
251            .insert(kernel_id.clone(), (pipeline.clone(), compiler_info));
252
253        #[cfg(feature = "spirv")]
254        if let Some(Err(key)) = cached
255            && let Some(crate::AutoRepresentation::SpirV(kernel)) = auto_repr
256        {
257            let cache = self.spirv_cache.as_mut().unwrap();
258            store_compiled(
259                cache,
260                key,
261                cubecl_spirv::SpirvCacheEntry::new(compiled.entrypoint_name, kernel),
262            );
263        }
264
265        Ok((pipeline, compiler_info))
266    }
267}
268
269impl<C: WgpuCompiler> ComputeServer for WgpuServer<C> {
270    type Kernel = Box<dyn CubeTask<C>>;
271    type Storage = WgpuStorage;
272    type MemoryLayoutPolicy = ContiguousMemoryLayoutPolicy;
273    type Info = wgpu::Backend;
274
275    fn logger(&self) -> Arc<ServerLogger> {
276        self.scheduler.logger.clone()
277    }
278
279    fn utilities(&self) -> Arc<ServerUtilities<Self>> {
280        self.utilities.clone()
281    }
282
283    fn staging(
284        &mut self,
285        _sizes: &[usize],
286        _stream_id: StreamId,
287    ) -> Result<Vec<Bytes>, ServerError> {
288        // TODO: Check if using a staging buffer is useful here.
289        Err(IoError::UnsupportedIoOperation {
290            backtrace: BackTrace::capture(),
291        }
292        .into())
293    }
294
295    fn initialize_memory(&mut self, memory: ManagedMemoryHandle, size: u64, stream_id: StreamId) {
296        let stream = self.scheduler.stream(&stream_id);
297        let reserved = stream
298            .empty(size)
299            .unwrap_or_else(|err| panic!("failed to reserve {size} bytes of device memory: {err}"));
300        stream.mem_manage.bind(reserved, memory);
301    }
302
303    fn read(
304        &mut self,
305        descriptors: Vec<CopyDescriptor>,
306        stream_id: StreamId,
307    ) -> DynFut<Result<Vec<Bytes>, ServerError>> {
308        let mut streams = vec![stream_id];
309        let mut resources = Vec::with_capacity(descriptors.len());
310        for desc in descriptors {
311            if contiguous_strides(&desc.shape) != desc.strides {
312                return Box::pin(async {
313                    Err(IoError::UnsupportedStrides {
314                        backtrace: BackTrace::capture(),
315                    }
316                    .into())
317                });
318            }
319            if !streams.contains(&desc.handle.stream) {
320                streams.push(desc.handle.stream);
321            }
322            let stream = self.scheduler.stream(&desc.handle.stream);
323            let resource = match stream.mem_manage.get_resource(desc.handle) {
324                Ok(val) => val,
325                Err(err) => return Box::pin(async move { Err(err.into()) }),
326            };
327            resources.push((resource, desc.shape, desc.elem_size));
328        }
329
330        self.scheduler.execute_streams(streams);
331
332        let stream = self.scheduler.stream(&stream_id);
333        stream.read_resources(resources)
334    }
335
336    fn write(&mut self, descriptors: Vec<(CopyDescriptor, Bytes)>, stream_id: StreamId) {
337        for (desc, data) in descriptors {
338            let stream = self.scheduler.stream(&desc.handle.stream);
339
340            if contiguous_strides(&desc.shape) != desc.strides {
341                stream.error(ServerError::Io(IoError::UnsupportedStrides {
342                    backtrace: BackTrace::capture(),
343                }));
344                return;
345            }
346
347            let resource = match stream.mem_manage.get_resource(desc.handle) {
348                Ok(r) => r,
349                Err(err) => {
350                    stream.error(ServerError::Io(err));
351                    return;
352                }
353            };
354            let task = ScheduleTask::Write {
355                data,
356                buffer: resource,
357            };
358
359            self.scheduler.register(stream_id, task, &[]);
360        }
361    }
362
363    fn get_resource(
364        &mut self,
365        binding: Binding,
366        stream_id: StreamId,
367    ) -> Result<ManagedResource<WgpuResource>, ServerError> {
368        let mut streams = vec![stream_id];
369        if binding.stream != stream_id {
370            streams.push(binding.stream);
371        }
372        self.scheduler.execute_streams(streams);
373        let stream = self.scheduler.stream(&binding.stream);
374        let memory = binding.memory.clone();
375        let resource = stream.mem_manage.get_resource(binding)?;
376
377        Ok(ManagedResource::new(memory, resource))
378    }
379
380    unsafe fn launch(
381        &mut self,
382        kernel: Self::Kernel,
383        count: CubeCount,
384        args: KernelArguments,
385        mode: ExecutionMode,
386        stream_id: StreamId,
387        launch_mode: LaunchMode,
388    ) {
389        let (pipeline, compiler_info) = match self.pipeline(kernel, &args, mode) {
390            Ok(val) => val,
391            Err(err) => {
392                // We make the stream that would execute the kernel in error.
393                let stream = self.scheduler.stream(&stream_id);
394                stream.errors.push(ServerError::Launch(err));
395                return;
396            }
397        };
398
399        if launch_mode.is_skipped() {
400            return;
401        }
402
403        self.streams_pool.clear();
404        // Reuse a pooled buffer to avoid allocating on every launch; it returns to the pool
405        // automatically when the guard drops.
406        let mut shared_inputs = self.shared_bindings_pool.acquire();
407        // Pin the memory of every input that lives on another stream (released in `WgpuStream::flush`).
408        args.buffers.iter().for_each(|b| {
409            self.streams_pool.push(b.stream);
410            if b.stream != stream_id {
411                shared_inputs.push(b.memory.clone());
412            }
413        });
414
415        let resources = match self.prepare_bindings(args, compiler_info) {
416            Ok(val) => val,
417            Err(err) => {
418                // We make the stream that would execute the kernel in error.
419                let stream = self.scheduler.stream(&stream_id);
420                stream.errors.push(ServerError::Io(err));
421                return;
422            }
423        };
424        let task = ScheduleTask::Execute {
425            pipeline,
426            count,
427            resources,
428            shared_inputs,
429        };
430
431        self.scheduler.register(stream_id, task, &self.streams_pool);
432    }
433
434    fn flush(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
435        self.scheduler.execute_streams(vec![stream_id]);
436
437        let stream = self.scheduler.stream(&stream_id);
438
439        stream.flush(StreamErrorMode {
440            ignore: false,
441            flush: true,
442        })
443    }
444
445    /// Returns the total time of GPU work this sync completes.
446    fn sync(&mut self, stream_id: StreamId) -> DynFut<Result<(), ServerError>> {
447        self.scheduler.execute_streams(vec![stream_id]);
448        let stream = self.scheduler.stream(&stream_id);
449
450        stream.sync()
451    }
452
453    fn start_profile(&mut self, stream_id: StreamId) -> Result<ProfilingToken, ServerError> {
454        self.scheduler.execute_streams(vec![stream_id]);
455        let stream = self.scheduler.stream(&stream_id);
456        stream.start_profile()
457    }
458
459    fn end_profile(
460        &mut self,
461        stream_id: StreamId,
462        token: ProfilingToken,
463    ) -> Result<ProfileDuration, ProfileError> {
464        self.scheduler.execute_streams(vec![stream_id]);
465        let stream = self.scheduler.stream(&stream_id);
466
467        stream.end_profile(token)
468    }
469
470    fn memory_usage(&mut self, stream_id: StreamId) -> Result<MemoryUsage, ServerError> {
471        self.scheduler.execute_streams(vec![stream_id]);
472        let stream = self.scheduler.stream(&stream_id);
473        Ok(stream.mem_manage.memory_usage())
474    }
475
476    fn stream_ids(&self) -> Vec<StreamId> {
477        self.scheduler.stream_ids().collect()
478    }
479
480    fn memory_cleanup(&mut self, stream_id: StreamId) {
481        self.scheduler.execute_streams(vec![stream_id]);
482        let stream = self.scheduler.stream(&stream_id);
483        stream.mem_manage.memory_cleanup(true);
484    }
485
486    fn allocation_mode(&mut self, mode: MemoryAllocationMode, stream_id: StreamId) {
487        self.scheduler.execute_streams(vec![stream_id]);
488        let stream = self.scheduler.stream(&stream_id);
489        stream.mem_manage.mode(mode);
490    }
491
492    fn configure_memory_pools(&mut self, config: MemoryConfiguration, stream_id: StreamId) -> bool {
493        // Streams created from now on build their main pool with the new
494        // layout; memory is per stream, so already-created streams keep theirs.
495        self.scheduler
496            .backend_mut()
497            .factory()
498            .set_gpu_pools(config.clone());
499        let (_, props) = self.scheduler.backend_mut().factory().gpu_pools();
500
501        // The calling stream's pools are rebuilt in place (kept, with a log,
502        // when something is still live in them).
503        self.scheduler.execute_streams(vec![stream_id]);
504        let stream = self.scheduler.stream(&stream_id);
505        stream.mem_manage.configure_memory_pools(config, &props)
506    }
507}
508
509pub(crate) fn contiguous_strides(shape: &Shape) -> Strides {
510    let rank = shape.len();
511    let mut strides = strides![1; rank];
512    for i in (0..rank - 1).rev() {
513        strides[i] = strides[i + 1] * shape[i + 1];
514    }
515    strides
516}