Skip to main content

cubecl_wgpu/compute/
server.rs

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