Skip to main content

cubecl_wgpu/compute/
server.rs

1use cubecl_core::server::{DeviceCaptures, ServerStorage};
2use cubecl_server::kernel::BufferIOAttr;
3use cubecl_server::kernel::DebugInformation;
4use std::collections::HashMap;
5use std::marker::PhantomData;
6
7use super::graph::WgpuGraph;
8use super::storage::{WgpuResource, WgpuStorage};
9use crate::WgpuCompiler;
10use crate::backend::ModuleSource;
11use crate::schedule::{BindingsResource, ScheduleTask, ScheduledWgpuBackend};
12use alloc::sync::Arc;
13use cubecl_common::pool::LeasePool;
14use cubecl_common::{
15    bytes::Bytes,
16    profile::{ProfileDuration, TimingMethod},
17};
18use cubecl_core::server::{BufferBinding, KernelResource};
19use cubecl_core::zspace::Shape;
20use cubecl_core::{
21    MemoryConfiguration, WgpuCompilationOptions,
22    prelude::*,
23    server::{
24        CopyDescriptor, IoError, KernelArguments, LaunchError, ProfileError, ProfilingToken,
25        ServerCommunication, ServerError, ServerUtilities,
26    },
27    zspace::{Strides, strides},
28};
29use cubecl_environment::backtrace::BackTrace;
30use cubecl_environment::future::DynFut;
31#[cfg(feature = "spirv")]
32use cubecl_environment::persistence::Store;
33use cubecl_environment::stream::StreamId;
34use cubecl_ir::MemoryDeviceProperties;
35use cubecl_server::compiler::CompilationRecording;
36#[cfg(feature = "spirv")]
37use cubecl_server::compiler::{KernelCacheKey, compilation_store, store_compiled};
38use cubecl_server::memory_management::{
39    ManagedMemoryHandle, SharedMemoryBindings, StreamMemoryReport,
40    relocation::{RelocatingStreams, RelocationReason},
41};
42use cubecl_server::{
43    compiler::CompilationCache,
44    config::{CubeClRuntimeConfig, RuntimeConfig},
45    dry_run::LaunchMode,
46    id::GraphId,
47    kernel::CubeKernel,
48    logging::ServerLogger,
49    memory_management::MemoryAllocationMode,
50    server::Server,
51    storage::ManagedResource,
52    stream::scheduler::{SchedulerMultiStream, SchedulerMultiStreamOptions, SchedulerStrategy},
53    stream::{ExecuteScope, FailureStore, StreamCapture, WriteScoped, failed_writing},
54    validation::{validate_cube_dim, validate_units},
55};
56use wgpu::ComputePipeline;
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum ParamsTransfer {
60    Immediate,
61    Uniform,
62}
63
64/// Compiler kind and info used when compiling a specific kernel. Used to determine parameter passing strategies.
65/// What a launch needs from a compiled kernel: the pipeline, the parameter
66/// strategy, and the per-buffer IO the taint bookkeeping stages from. The IO
67/// rides in the cache because on a hit nothing else of the compilation
68/// survives.
69pub type PipelineEntry = (
70    Arc<ComputePipeline>,
71    CompilerInfo,
72    Option<Arc<[BufferIOAttr]>>,
73);
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum CompilerInfo {
77    Vulkan { params_transfer: ParamsTransfer },
78    Metal,
79    WGSL,
80    None,
81}
82
83/// Wgpu compute server.
84#[derive(Debug)]
85pub struct WgpuServer<C: WgpuCompiler> {
86    pub(crate) device: wgpu::Device,
87    // A buffer that can be used to store stream id without extra allocations.
88    streams_pool: Vec<StreamId>,
89    /// The pipelines built so far, in front of the SPIR-V store when there is
90    /// one.
91    pipelines: CompilationCache<KernelId, PipelineEntry>,
92    scheduler: SchedulerMultiStream<ScheduledWgpuBackend>,
93    #[cfg(feature = "spirv")]
94    pub(crate) spirv_cache: Option<Store<(u64, KernelCacheKey), cubecl_spirv::SpirvCacheEntry>>,
95    #[cfg(feature = "spirv")]
96    pub(crate) build_id: cubecl_common::hash::StableHash,
97    pub compilation_options: WgpuCompilationOptions,
98    pub(crate) backend: wgpu::Backend,
99    pub(crate) utilities: Arc<ServerUtilities>,
100    /// Reusable buffers for the cross-stream input bindings of each launch.
101    shared_bindings_pool: LeasePool<SharedMemoryBindings>,
102    /// Captured graphs owned by this server, keyed by the [`GraphId`] handed to
103    /// the client. `end_capture` inserts, `replay` looks up, `graph_destroy`
104    /// removes (dropping the [`WgpuGraph`] unpins the buffers it retained).
105    graphs: HashMap<GraphId, WgpuGraph>,
106    _compiler: PhantomData<C>,
107}
108
109impl<C: WgpuCompiler> ServerCommunication for WgpuServer<C> {}
110
111impl<C: WgpuCompiler> WriteScoped for WgpuServer<C> {
112    type Streams = SchedulerMultiStream<ScheduledWgpuBackend>;
113
114    fn write_streams(&mut self) -> &mut Self::Streams {
115        &mut self.scheduler
116    }
117
118    fn on_failure(&mut self, stream: StreamId, error: &ServerError) {
119        // Measured per stream on this backend, so the scope's stream is the
120        // one whose measurement a failure invalidates.
121        self.scheduler.stream(&stream).profile_failure(error);
122    }
123
124    fn capturing(&mut self, stream: StreamId) -> Option<&mut StreamCapture> {
125        Some(&mut self.scheduler.stream(&stream).capturing)
126    }
127}
128
129impl<C: WgpuCompiler> WgpuServer<C> {
130    /// Create a new server.
131    #[allow(clippy::too_many_arguments)]
132    pub fn new(
133        memory_properties: MemoryDeviceProperties,
134        memory_config: MemoryConfiguration,
135        compilation_options: WgpuCompilationOptions,
136        device: wgpu::Device,
137        queue: wgpu::Queue,
138        tasks_max: usize,
139        backend: wgpu::Backend,
140        timing_method: TimingMethod,
141        utilities: ServerUtilities,
142        captures: DeviceCaptures,
143    ) -> Self {
144        #[cfg(feature = "spirv")]
145        let adapter_info = device.adapter_info();
146        let backend_scheduler = ScheduledWgpuBackend::new(
147            device.clone(),
148            queue.clone(),
149            memory_properties,
150            memory_config,
151            timing_method,
152            backend,
153            tasks_max,
154            utilities.logger.clone(),
155            compilation_options.supports_vulkan_compiler,
156            captures,
157        );
158
159        let config = CubeClRuntimeConfig::get();
160        let max_streams = config.streaming.max_streams;
161
162        #[cfg(feature = "spirv")]
163        let spirv_cache = compilation_store(
164            "vulkan",
165            format!("spirv_{}_{}", adapter_info.vendor, adapter_info.device),
166        );
167
168        // WGSL is compiled by the driver on every run, so without the SPIR-V
169        // store there is nothing persisted for a switch to invalidate.
170        #[cfg(feature = "spirv")]
171        let pipelines = CompilationCache::mirroring(&spirv_cache);
172        #[cfg(not(feature = "spirv"))]
173        let pipelines = CompilationCache::unbound();
174
175        Self {
176            compilation_options,
177            streams_pool: Vec::new(),
178            device,
179            pipelines,
180            scheduler: SchedulerMultiStream::new(
181                utilities.logger.clone(),
182                backend_scheduler,
183                SchedulerMultiStreamOptions {
184                    max_streams,
185                    max_tasks: tasks_max,
186                    strategy: SchedulerStrategy::Interleave,
187                },
188            ),
189            #[cfg(feature = "spirv")]
190            spirv_cache,
191            #[cfg(feature = "spirv")]
192            build_id: cubecl_server::compiler::build_id_hash(),
193            backend,
194            utilities: Arc::new(utilities),
195            shared_bindings_pool: LeasePool::with_capacity(tasks_max * max_streams as usize),
196            graphs: HashMap::new(),
197            _compiler: PhantomData,
198        }
199    }
200
201    fn prepare_bindings(
202        &mut self,
203        stream_id: StreamId,
204        bindings: KernelArguments,
205        compiler_info: CompilerInfo,
206    ) -> Result<BindingsResource, IoError> {
207        // Store all the resources we'll be using. This could be eliminated if
208        // there was a way to tie the lifetime of the resource to the memory handle.
209        let mut resources = Vec::with_capacity(bindings.resources.len());
210
211        for resource in bindings.resources.into_iter() {
212            match resource {
213                KernelResource::Buffer(b) => {
214                    // A recording guards the pages it was given once it seals.
215                    self.scheduler
216                        .stream(&stream_id)
217                        .capturing
218                        .touch(b.stream, b.memory.descriptor().location());
219                    let stream = self.scheduler.stream(&b.stream);
220                    let resource = stream.resource(b)?;
221                    resources.push(resource);
222                }
223                KernelResource::TensorMap(_) => panic!("Tensor map not supported in wgpu"),
224            }
225        }
226
227        Ok(BindingsResource {
228            resources,
229            info: bindings.info,
230            compiler_info,
231        })
232    }
233
234    fn pipeline(
235        &mut self,
236        kernel: Box<dyn CubeKernel>,
237        bindings: &KernelArguments,
238    ) -> Result<PipelineEntry, LaunchError> {
239        let kernel_id = kernel.id();
240        let mode = kernel_id.mode;
241
242        if let Some(pipeline) = self.pipelines.get(&kernel_id) {
243            return Ok(pipeline.clone());
244        }
245
246        let mut recording = CompilationRecording::new(&kernel_id);
247        let cached = self.load_cached_pipeline(&kernel_id, bindings, mode)?;
248
249        if let Some(Ok(pipeline)) = cached {
250            self.pipelines.insert(kernel_id, pipeline.clone());
251            recording.loaded();
252            return Ok(pipeline);
253        }
254
255        validate_cube_dim(&self.utilities.properties, &kernel_id)?;
256        validate_units(&self.utilities.properties, &kernel_id)?;
257
258        let definition = cubecl_core::define_kernel(&*kernel)?;
259        recording.defined(&definition);
260
261        let mut compiler = C::init(self.backend, &self.compilation_options);
262        let mut compiled = compiler.compile_kernel(self, kernel, definition)?;
263
264        if self.scheduler.logger.compilation_source_activated() {
265            compiled.debug_info = Some(DebugInformation::new(
266                compiler.lang_tag(),
267                kernel_id.clone(),
268            ));
269        }
270        self.scheduler.logger.log_compilation(&compiled);
271
272        compiler.validate_ir(&compiled.repr, &self.utilities.properties)?;
273        // The compiled kernel's per-buffer answer, before the repr is
274        // consumed: what the write scope stages from.
275        let io = compiled.io.take().map(Arc::from);
276        let (compiler_info, auto_repr) = compiler.normalize_repr(compiled.repr);
277        let repr = auto_repr.as_ref().map(|r| r.as_ref());
278
279        // /!\ Do not delete the following commented code.
280        // This is useful while working on the metal compiler.
281        // Also the errors are printed nicely which is not the case when this is the runtime
282        // that does it.
283        // {
284        //     // Write shader in metal file then compile it for error
285        //     std::fs::write("shader.metal", &compiled.source).expect("should write to file");
286        //     let status = std::process::Command::new("xcrun")
287        //         .args(vec![
288        //             "-sdk",
289        //             "macosx",
290        //             "metal",
291        //             "-o",
292        //             "shader.ir",
293        //             "-c",
294        //             "shader.metal",
295        //             "-w",
296        //         ])
297        //         .status()
298        //         .expect("should launch the command");
299        //     if !status.success() {
300        //         println!("SOURCE:\n{}", compiled.source);
301        //         std::process::exit(status.code().unwrap());
302        //     }
303        // }
304
305        let module = self.create_module(
306            &compiled.entrypoint_name,
307            kernel_id.cube_dim.into(),
308            ModuleSource::resolve(repr, compiler.lang_tag(), &compiled.source)?,
309            mode,
310        )?;
311        let pipeline = self.create_pipeline(&compiled.entrypoint_name, repr, module, bindings)?;
312        self.pipelines.insert(
313            kernel_id.clone(),
314            (pipeline.clone(), compiler_info, io.clone()),
315        );
316
317        recording.source(&compiled.source);
318
319        // Only a SPIR-V kernel is stored: any other build changes nothing.
320        let stored = false;
321        #[cfg(feature = "spirv")]
322        let stored = match (cached, auto_repr) {
323            (Some(Err(key)), Some(crate::AutoRepresentation::SpirV(kernel))) => {
324                let cache = self.spirv_cache.as_mut().unwrap();
325                store_compiled(
326                    cache,
327                    key,
328                    cubecl_spirv::SpirvCacheEntry::new(compiled.entrypoint_name, kernel),
329                )
330            }
331            _ => stored,
332        };
333        recording.compiled(stored);
334
335        Ok((pipeline, compiler_info, io))
336    }
337}
338
339impl<C: WgpuCompiler> Server for WgpuServer<C> {
340    fn logger(&self) -> Arc<ServerLogger> {
341        self.scheduler.logger.clone()
342    }
343
344    fn utilities(&self) -> Arc<ServerUtilities> {
345        self.utilities.clone()
346    }
347
348    fn staging(
349        &mut self,
350        _sizes: &[usize],
351        _stream_id: StreamId,
352    ) -> Result<Vec<Bytes>, ServerError> {
353        // TODO: Check if using a staging buffer is useful here.
354        Err(IoError::UnsupportedIoOperation {
355            backtrace: BackTrace::capture(),
356        }
357        .into())
358    }
359
360    fn initialize_memory(
361        &mut self,
362        memory: ManagedMemoryHandle,
363        size: u64,
364        stream_id: StreamId,
365    ) -> Result<(), ServerError> {
366        let mut relocating = self.scheduler.relocating(stream_id);
367        relocating.relocate_when_wanted();
368        let (stream, failures) = self.scheduler.stream_and_failures(&stream_id);
369        let update = stream.capturing.page_update();
370        let reserved = match stream.memory.reserve(size, update, failures) {
371            Ok(reserved) => reserved,
372            // The recording now misses whatever this memory was for, and
373            // `end_capture` reports that: the handle stays unbound, and
374            // whatever uses it belongs to a recording that will not seal.
375            Err(err @ IoError::PageUpdateForbidden { .. }) => {
376                stream.capturing.fail(err.into());
377                return Ok(());
378            }
379            Err(err) => {
380                // Nothing to allocate: the buffer carries the error instead.
381                let err = ServerError::from(err);
382                failures.fail_unallocated(&memory, err.clone());
383                return Err(err);
384            }
385        };
386        if let Err(err) = stream.memory.bind(reserved, memory.clone(), 0, failures) {
387            let err = ServerError::from(err);
388            failures.fail_unallocated(&memory, err.clone());
389            return Err(err);
390        }
391        Ok(())
392    }
393
394    fn read(
395        &mut self,
396        descriptors: Vec<CopyDescriptor>,
397        stream_id: StreamId,
398    ) -> DynFut<Result<Vec<Bytes>, ServerError>> {
399        // A read is a host sync: it cannot be recorded, and the recorded work
400        // has not executed, so there is nothing meaningful to read anyway.
401        if let Err(err) = self
402            .scheduler
403            .stream(&stream_id)
404            .reject_while_recording("read")
405        {
406            return Box::pin(async move { Err(err) });
407        }
408
409        // Buffers another stream wrote are only as good as the work that wrote
410        // them; see `StreamPool::ensure_written`. The reader's own errors are
411        // surfaced by `read_resources`' flush further down.
412        if let Err(err) = self
413            .scheduler
414            .ensure_written(descriptors.iter().map(|d| &d.handle))
415        {
416            return Box::pin(async move { Err(err) });
417        }
418
419        let mut streams = vec![stream_id];
420        let mut resources = Vec::with_capacity(descriptors.len());
421        for desc in descriptors {
422            if contiguous_strides(&desc.shape) != desc.strides {
423                return Box::pin(async {
424                    Err(IoError::UnsupportedStrides {
425                        backtrace: BackTrace::capture(),
426                    }
427                    .into())
428                });
429            }
430            if !streams.contains(&desc.handle.stream) {
431                streams.push(desc.handle.stream);
432            }
433            let stream = self.scheduler.stream(&desc.handle.stream);
434            let resource = match stream.resource(desc.handle) {
435                Ok(val) => val,
436                Err(err) => return Box::pin(async move { Err(err.into()) }),
437            };
438            resources.push((resource, desc.shape, desc.elem_size));
439        }
440
441        self.scheduler.execute_streams(streams);
442
443        let stream = self.scheduler.stream(&stream_id);
444        stream.read_resources(resources, stream_id)
445    }
446
447    fn write(&mut self, descriptors: Vec<(CopyDescriptor, Bytes)>, stream_id: StreamId) {
448        // Writes go on the queue, not the encoder — they cannot be recorded
449        // into a software graph (v1; CUDA records them as memcpy nodes).
450        //
451        // Rejected lazily. When the caller is the stream recording the
452        // capture, the refusal dooms its `end_capture` rather than handing
453        // back a graph missing an operation. When it is a neighbour sharing
454        // the pooled stream, the write was never going into anyone's graph and
455        // the taint on its own destinations is the whole report — dooming a
456        // capture on it would charge one stream's window to another.
457        {
458            let recording = self
459                .scheduler
460                .stream(&stream_id)
461                .reject_while_recording("write");
462            if let Err(err) = recording {
463                // Nothing is copied, so every destination this call was given is
464                // left as it was — taint them, or a read of one on another
465                // logical stream finds no failure to fail on and copies stale
466                // bytes.
467                self.scheduler.taint(
468                    err.clone(),
469                    descriptors.iter().map(|(desc, _)| &desc.handle),
470                );
471                // The owner's own write dooms its capture: the recording is
472                // missing that operation and must not seal. A neighbour's
473                // refusal is not the capture's failure — the taint on its
474                // destinations is the whole report.
475                let stream = self.scheduler.stream(&stream_id);
476                if stream.capturing.owner() == Some(stream_id) {
477                    stream.capturing.fail(err);
478                }
479                return;
480            }
481        }
482        for (desc, data) in descriptors {
483            // Each copy runs in its own scope over its destination: the write
484            // that lands fills it, which is what releases an earlier
485            // failure's hold on it — a caller recovers by writing from the
486            // host as much as by relaunching — and a failure leaves it as it
487            // was, which is what a later read of it has to fail on. The scope
488            // queues failures on the caller's stream, the one that flushes
489            // them, even though the resource is resolved on the stream that
490            // owns the handle.
491            let mut written = self.write_set();
492            written.push(desc.handle.clone());
493            ExecuteScope::over(self, stream_id, written).execute(|server| {
494                if contiguous_strides(&desc.shape) != desc.strides {
495                    return Err(ServerError::Io(IoError::UnsupportedStrides {
496                        backtrace: BackTrace::capture(),
497                    }));
498                }
499
500                // The write is registered on the caller, so name the
501                // stream that owns the handle as an argument: its queued
502                // work has to land before this write overwrites the same
503                // memory.
504                let owner = desc.handle.stream;
505                let handle = desc.handle.clone();
506                let stream = server.scheduler.stream(&owner);
507                let resource = stream.resource(desc.handle).map_err(ServerError::Io)?;
508                let task = ScheduleTask::Write {
509                    data,
510                    buffer: resource,
511                    handle,
512                };
513
514                server.scheduler.register(stream_id, task, &[owner]);
515                Ok(())
516            });
517        }
518    }
519
520    fn check(
521        &mut self,
522        handles: Vec<BufferBinding>,
523        _stream_id: StreamId,
524    ) -> Result<(), ServerError> {
525        self.scheduler.ensure_written(handles.iter())
526    }
527
528    unsafe fn launch(
529        &mut self,
530        kernel: Box<dyn CubeKernel>,
531        count: CubeCount,
532        args: KernelArguments,
533        stream_id: StreamId,
534        launch_mode: LaunchMode,
535    ) {
536        // Compilation comes first — memoized, so a launch after the first
537        // pays a map lookup — because the write scope stages what the
538        // compiled kernel says it writes. A kernel that fails to compile has
539        // no IR and no compiled answer, so the caller's declared IO decides:
540        // only the declared outputs are left carrying the failure, never the
541        // buffers the kernel was only going to read — tainting those would
542        // refuse every later launch that shares them, an autotune sweep
543        // above all.
544        //
545        // A dry run stages none either way. It was never going to write, so a
546        // failure in it leaves nothing stale, and tainting its buffers would
547        // fail unrelated reads of memory the run deliberately left alone.
548        let kernel_id = kernel.id();
549        let (pipeline, compiler_info, io) = match self.pipeline(kernel, &args) {
550            Ok(val) => val,
551            Err(err) => {
552                let error = ServerError::Launch(err);
553                self.scheduler.stream(&stream_id).profile_failure(&error);
554                if !launch_mode.is_skipped() {
555                    let mut written = self.write_set();
556                    written.extend(args.buffers_written(None).cloned());
557                    failed_writing(self, stream_id, written, error);
558                }
559                return;
560            }
561        };
562        if launch_mode.is_skipped() {
563            return;
564        }
565
566        // Skip, do not taint: a launch whose input cannot be trusted does not
567        // run. Running it is not merely wasted device time — a buffer holding
568        // garbage can be read as a dynamic cube count or as gather indices,
569        // scattering into memory that carried no failure at all. The outputs
570        // take the failure that stopped the launch, exactly as a failed
571        // launch's would, so a read downstream fails on the root cause.
572        //
573        // Except while this stream records a graph: skipping would seal a
574        // recording missing an operation, and the replay contract has the
575        // caller write fresh inputs before each replay — clearing the very
576        // claim that would explain the hole. A doomed capture is refused at
577        // `end_capture` instead.
578        //
579        // The scope claims what the launch writes until the body proves the
580        // work enqueued, so a failure — or a panic — anywhere in it leaves a
581        // read of those buffers failing on the error rather than copying
582        // bytes nothing wrote.
583        let mut written = self.write_set();
584        written.extend(args.buffers_written(io.as_deref()).cloned());
585        // A dynamic count travels outside `resources`, so `buffers_read`
586        // never names it — yet the indirect dispatch reads it as its grid
587        // dimensions, which is exactly the garbage-as-cube-count read the
588        // skip exists to prevent.
589        let count_read = match &count {
590            CubeCount::Dynamic(binding) => Some(binding),
591            CubeCount::Static(..) => None,
592        };
593        ExecuteScope::launching(
594            self,
595            kernel_id,
596            stream_id,
597            args.buffers_read(io.as_deref()).chain(count_read),
598            written,
599        )
600        .execute(|server| {
601            server.streams_pool.clear();
602            // Reuse a pooled buffer to avoid allocating on every launch; it returns to the pool
603            // automatically when the guard drops.
604            let mut shared_inputs = server.shared_bindings_pool.acquire();
605            // Pin the memory of every input that lives on another stream (released in `WgpuStream::flush`).
606            args.resources.iter().for_each(|resource| match resource {
607                KernelResource::Buffer(b) => {
608                    server.streams_pool.push(b.stream);
609                    if b.stream != stream_id {
610                        shared_inputs.push(b.memory.clone());
611                    }
612                }
613                KernelResource::TensorMap(_) => {
614                    panic!("Tensor maps not supported in WGPU")
615                }
616            });
617
618            let resources = server
619                .prepare_bindings(stream_id, args, compiler_info)
620                .map_err(ServerError::Io)?;
621
622            let task = ScheduleTask::Execute {
623                pipeline,
624                count,
625                resources,
626                shared_inputs,
627            };
628
629            server
630                .scheduler
631                .register(stream_id, task, &server.streams_pool);
632            Ok(())
633        });
634    }
635
636    fn flush(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
637        self.scheduler.execute_streams(vec![stream_id]);
638
639        let stream = self.scheduler.stream(&stream_id);
640
641        stream.flush(stream_id)
642    }
643
644    /// Returns the total time of GPU work this sync completes.
645    fn sync(
646        &mut self,
647        handles: Vec<BufferBinding>,
648        stream_id: StreamId,
649    ) -> DynFut<Result<(), ServerError>> {
650        if let Err(err) = self
651            .scheduler
652            .stream(&stream_id)
653            .reject_while_recording("sync")
654        {
655            return Box::pin(async move { Err(err) });
656        }
657        // The claim check a read would have made, without the read; claims
658        // are set at enqueue time, so they are already in place.
659        if let Err(err) = self.scheduler.ensure_written(handles.iter()) {
660            return Box::pin(async move { Err(err) });
661        }
662        self.scheduler.execute_streams(vec![stream_id]);
663        let stream = self.scheduler.stream(&stream_id);
664
665        stream.sync(stream_id)
666    }
667
668    fn start_profile(&mut self, stream_id: StreamId) -> Result<ProfilingToken, ServerError> {
669        // Recorded launches do not execute, so a profile of the window would
670        // measure nothing.
671        self.scheduler
672            .stream(&stream_id)
673            .reject_while_recording("start_profile")?;
674        self.scheduler.execute_streams(vec![stream_id]);
675        let stream = self.scheduler.stream(&stream_id);
676        stream.start_profile(stream_id)
677    }
678
679    fn end_profile(
680        &mut self,
681        stream_id: StreamId,
682        token: ProfilingToken,
683    ) -> Result<ProfileDuration, ProfileError> {
684        self.scheduler.execute_streams(vec![stream_id]);
685        let stream = self.scheduler.stream(&stream_id);
686
687        stream.end_profile(token, stream_id)
688    }
689
690    fn abandon_profile(&mut self, stream_id: StreamId, token: ProfilingToken) {
691        // No `execute_streams`: the default reaches this through `end_profile`,
692        // which has to flush what it is about to measure. An abandon measures
693        // nothing, so it leaves the stream's queued work where it found it.
694        self.scheduler.stream(&stream_id).abandon_profile(token);
695    }
696
697    fn memory_report(&mut self, stream_id: StreamId) -> Option<StreamMemoryReport> {
698        self.scheduler.execute_streams(vec![stream_id]);
699        let stream = self.scheduler.stream(&stream_id);
700        Some(StreamMemoryReport {
701            stream: stream_id,
702            pools: stream.memory.memory_report(),
703            auxiliary: stream.auxiliary.report(),
704        })
705    }
706
707    fn stream_ids(&self) -> Vec<StreamId> {
708        self.scheduler.stream_ids().collect()
709    }
710
711    fn memory_cleanup(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
712        self.scheduler.execute_streams(vec![stream_id]);
713        self.scheduler
714            .relocating(stream_id)
715            .refuse_while_recording()?;
716        let stream = self.scheduler.stream(&stream_id);
717        // The info cache's buffers are live slices in the uniforms pool; an
718        // explicit cleanup exists to leave the pools empty, so every entry not
719        // pinned by a live graph goes too (entries are recreated on their next
720        // miss).
721        stream.info_cache.clear_unpinned();
722        self.scheduler.relocating(stream_id).reclaim()
723    }
724
725    fn allocation_mode(&mut self, mode: MemoryAllocationMode, stream_id: StreamId) {
726        self.scheduler.execute_streams(vec![stream_id]);
727        let stream = self.scheduler.stream(&stream_id);
728        stream.memory.mode(mode);
729    }
730
731    fn graph_prepare(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
732        // Drain queued tasks first so pre-capture work is not attributed to
733        // the capture window.
734        self.scheduler.execute_streams(vec![stream_id]);
735        let stream = self.scheduler.stream(&stream_id);
736
737        // The non-`NoCapture` state isolates this stream in the scheduler
738        // (see `requires_isolation`) until the capture ends.
739        stream.capturing.prepare(stream_id)?;
740
741        // The pages the recording touches are guarded for the graph's life,
742        // and a guarded page is never relocated: empty every stream's outdated
743        // pools now, since the recording may touch memory any stream owns.
744        self.scheduler
745            .relocating(stream_id)
746            .relocate_device(RelocationReason::Capture);
747        Ok(())
748    }
749
750    fn begin_capture(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
751        // Materialize the warmup work queued in the scheduler before the
752        // recording window opens.
753        self.scheduler.execute_streams(vec![stream_id]);
754        let stream = self.scheduler.stream(&stream_id);
755
756        stream.capturing.begin()?;
757
758        // Submit the warmup work and surface its failure now, so a warmup
759        // failure is reported here — where the diagnostic points at the cause
760        // — instead of dooming `end_capture` later.
761        let stream = self.scheduler.stream(&stream_id);
762        if let Err(err) = stream.flush(stream_id) {
763            // The capture never opened: return to `NoCapture`, so a failed
764            // `start_capture` leaves the stream fully usable and
765            // re-capturable.
766            stream.info_cache.capture_discard();
767            stream.capturing.abort();
768            return Err(err);
769        }
770        Ok(())
771    }
772
773    fn end_capture(&mut self, stream_id: StreamId) -> Result<GraphId, ServerError> {
774        // Materialize the recorded launches still queued in the scheduler.
775        self.scheduler.execute_streams(vec![stream_id]);
776        let stream = self.scheduler.stream(&stream_id);
777
778        // The capture is over even on the failure path below, so an error here
779        // doesn't leave the stream stuck in capture/persistent state — and it
780        // is over for a caller that does not own the window too, since that is
781        // a window nobody is coming back to close. Only its owner gets a graph
782        // out of it, and the errors raised inside belong to that owner rather
783        // than to whoever happens to be flushing.
784        let outcome = match stream.capturing.end(stream_id) {
785            Ok(outcome) => outcome,
786            Err(err) => {
787                // A capture prepared but never opened still holds the stream,
788                // and a `graph_prepare` retry is refused while it does.
789                // Closing is the only call the caller has left — a warmup that
790                // failed never reaches `start_capture` — so a close from
791                // `Prepare` unwinds it, as `begin_capture` does when the
792                // warmup flush fails, instead of leaving it held forever.
793                if stream.capturing.is_active() {
794                    stream.info_cache.capture_discard();
795                    stream.capturing.abort();
796                }
797                return Err(err);
798            }
799        };
800        let recording = stream.take_recording();
801        // The memory the recorded launches write. A graph that seals answers
802        // for it on a failed replay; one that does not is answered for here,
803        // since those launches never ran and now never will.
804        let written = stream.capturing.take_recorded();
805        let touched = stream.capturing.take_touched();
806
807        // A failure raised during the window — a rejected write, a failed or
808        // skipped launch — means the recording is missing an operation:
809        // reject the capture rather than hand back a graph that silently
810        // skips work. The window is doomed from the moment one lands, so what
811        // is read here arose inside it and nowhere else.
812        let doomed = stream.capturing.take_failure().map(|reason| {
813            ServerError::graph_state(format!(
814                "an operation inside the capture window failed, so the recording is missing \
815                 an operation and cannot seal: {reason}"
816            ))
817        });
818        let discarded = match outcome.is_abandoned() {
819            true => Some(outcome.abandoned_error(stream_id, doomed)),
820            false => doomed,
821        };
822        if let Some(err) = discarded {
823            stream.info_cache.capture_discard();
824            // The recording is thrown away, so the launches in it never run:
825            // every buffer they were given is left as it was. The caller gets
826            // the error below; the taint is what makes a read of one of those
827            // buffers fail on some other stream, which heard nothing.
828            self.scheduler.taint(err.clone(), written.iter());
829            return Err(err);
830        }
831
832        let id = GraphId::new();
833        // Seal the info-cache entries this capture pinned under the graph's
834        // id, so `graph_destroy` can release them later.
835        stream.info_cache.capture_commit(id);
836        // Guard every page the recording touched, on whichever stream owns
837        // it, so nothing on them moves or is reused for the graph's lifetime.
838        let guards = touched
839            .iter()
840            .filter_map(|page| {
841                self.scheduler
842                    .stream(&page.stream)
843                    .memory
844                    .guard(page.location)
845            })
846            .collect();
847        self.graphs.insert(
848            id,
849            WgpuGraph {
850                tasks: recording.tasks,
851                _guards: guards,
852                _uniforms: recording.uniform_pins,
853                _shared: recording.shared,
854                written,
855            },
856        );
857        Ok(id)
858    }
859
860    fn replay(&mut self, graph: GraphId, stream_id: StreamId) -> Result<(), ServerError> {
861        // Order the replay after previously queued work on this stream.
862        self.scheduler.execute_streams(vec![stream_id]);
863
864        // A use-after-free in the caller's own code, with the caller standing
865        // right there, so it is returned. Nothing to taint either — the
866        // graph is gone, and with it the record of which buffers its launches
867        // would have written.
868        let Some(wgpu_graph) = self.graphs.get(&graph) else {
869            return Err(ServerError::graph_state(
870                "replay was given an unknown or already-destroyed graph",
871            ));
872        };
873
874        // A replay writes the buffers its recorded launches were given, so it
875        // takes the same scope over that write set and settles it: a failed
876        // enqueue leaves them carrying the failure, and the next replay that
877        // lands releases the claim. Without the settle one transient failure
878        // would leave the graph's buffers unreadable forever — the graph
879        // retains their handles, so none of the shedding paths can ever fire
880        // for them, and the graph itself is the only thing that writes them.
881        let recorded = wgpu_graph.written.clone();
882        let mut written = self.write_set();
883        written.extend(recorded);
884        ExecuteScope::over(self, stream_id, written)
885            .execute(|server| {
886                server
887                    .scheduler
888                    .stream(&stream_id)
889                    .reject_while_recording("replay")?;
890                let wgpu_graph = server
891                    .graphs
892                    .get(&graph)
893                    .expect("checked above; nothing in the scope removes graphs");
894                let stream = server.scheduler.stream(&stream_id);
895                stream.replay_graph(wgpu_graph);
896                Ok(())
897            })
898            .into_result()
899    }
900
901    fn graph_destroy(&mut self, graph: GraphId, stream_id: StreamId) {
902        // No-op for an unknown id (e.g. a double release). The graph is held
903        // until the end of this function, so its pins outlive the flush below.
904        let Some(wgpu_graph) = self.graphs.remove(&graph) else {
905            return;
906        };
907        let stream = self.scheduler.stream(&stream_id);
908        // Submit any replay still sitting in the encoder before the pins drop:
909        // a `queue.write_buffer` onto a reclaimed slice runs at the *next*
910        // submit, ahead of everything already in the encoder, so it would reach
911        // the GPU before the still-unsubmitted replay that reads it. The `Write`
912        // path flushes on its own account, so this covers the writes that do
913        // not — the uniform uploads in `create_uniform`/`info_uniform`. Once the
914        // replay is submitted, queue ordering makes releasing the slices safe
915        // with no host sync, unlike CUDA.
916        stream.submit();
917        // Release the info-cache entries this graph pinned; entries no other
918        // live graph still pins are dropped, freeing their buffers.
919        stream.info_cache.graph_release(graph);
920        drop(wgpu_graph);
921    }
922}
923
924pub(crate) fn contiguous_strides(shape: &Shape) -> Strides {
925    let rank = shape.len();
926    let mut strides = strides![1; rank];
927    for i in (0..rank - 1).rev() {
928        strides[i] = strides[i + 1] * shape[i + 1];
929    }
930    strides
931}
932
933impl<C: WgpuCompiler> ServerStorage for WgpuServer<C> {
934    type Storage = WgpuStorage;
935
936    fn get_resource(
937        &mut self,
938        binding: BufferBinding,
939        stream_id: StreamId,
940    ) -> Result<ManagedResource<WgpuResource>, ServerError> {
941        // The same claim check a read makes: a buffer a failed launch never
942        // filled reports the failure rather than handing back a pointer to
943        // whatever was there before.
944        self.scheduler.ensure_written([&binding].into_iter())?;
945        let mut streams = vec![stream_id];
946        if binding.stream != stream_id {
947            streams.push(binding.stream);
948        }
949        self.scheduler.execute_streams(streams);
950        let stream = self.scheduler.stream(&binding.stream);
951        Ok(stream.memory.managed_resource(
952            binding.memory,
953            binding.offset_start,
954            binding.offset_end,
955        )?)
956    }
957}