Skip to main content

cubecl_wgpu/compute/
server.rs

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