Skip to main content

cubecl_server/compiler/
loader.rs

1//! The kernels a device has loaded, and the one path that loads more.
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use cubecl_common::profile::Instant;
8use cubecl_environment::collections::{HashMap, HashSet};
9use cubecl_runtime::execution::{KernelHandling, KernelOutcome, KernelRegistration, SettledKernel};
10
11use super::{
12    ArtifactCompiler, ArtifactId, BatchOutcome, CompilationBatchRecording, CompilationOutcome,
13    CompilationRecording, CompilationTarget, VariantOf,
14};
15use crate::config::RuntimeConfig;
16use crate::kernel::CubeKernel;
17use crate::logging::ServerLogger;
18use crate::server::LaunchError;
19
20/// Every kernel a device has loaded, in front of the backend that compiles and
21/// loads the ones it has not.
22///
23/// A kernel it does not hold is read from the compilation store, or failing
24/// that compiled and stored, then loaded — in that order on every backend, and
25/// recorded the same way: what differs between backends is the
26/// [`CompilationTarget`] it is generic over.
27///
28/// Kernels can also be [queued](Self::enqueue) and compiled together: the
29/// half of the work any thread can do then runs on as many threads as
30/// there are kernels, up to the configured
31/// [parallelism](crate::config::compilation::CompilationConfig::parallelism),
32/// each taking the next kernel as it finishes one. However long the queue,
33/// it compiles in one phase that lasts about as long as its slowest kernel.
34pub struct KernelLoader<T: CompilationTarget> {
35    target: T,
36    loaded: CompilationCache<ArtifactId<VariantOf<T>>, T::Loaded>,
37    /// The kernels queued and not compiled yet.
38    queue: KernelQueue<VariantOf<T>>,
39    /// Why each queued kernel of the latest batch that failed to compile
40    /// failed, until its own launch reports it: the launch is where the
41    /// failure has outputs to carry, and compiling the kernel again there
42    /// would only fail again. The next batch drops what is left, so a kernel
43    /// whose launch never came compiles again rather than keeping a stale
44    /// error: what the launch is spared is one compile, not every later one.
45    failed: HashMap<ArtifactId<VariantOf<T>>, LaunchError>,
46    /// How many threads compile at once.
47    parallelism: usize,
48}
49
50impl<T: CompilationTarget> KernelLoader<T> {
51    /// A loader holding nothing yet, bound to the active environment exactly
52    /// when `target` [persists](CompilationTarget::persists) what it compiles.
53    pub fn new(target: T) -> Self {
54        let loaded = CompilationCache::new(target.persists());
55        let parallelism = crate::config::CubeClRuntimeConfig::get()
56            .compilation
57            .parallelism();
58
59        Self {
60            target,
61            loaded,
62            queue: KernelQueue::new(),
63            failed: HashMap::new(),
64            parallelism,
65        }
66    }
67
68    /// The kernel loaded for `id`, if it is.
69    #[cfg(test)]
70    fn get(&mut self, id: &ArtifactId<VariantOf<T>>) -> Option<&T::Loaded> {
71        self.loaded.get(id)
72    }
73
74    /// The kernel loaded for `kernel`, under `id`, loading it first if it is
75    /// not. A kernel is about to execute, so every queued kernel is compiled
76    /// first — together with this one when it needs compiling.
77    ///
78    /// `id` is `kernel`'s, with what the launch adds to it: the launch already
79    /// built the kernel's id, and building it again on every launch costs. A
80    /// kernel already loaded, with nothing queued, costs one lookup and a
81    /// clone of what was loaded; `id` is only cloned to compile.
82    ///
83    /// # Errors
84    ///
85    /// The validation, compilation or load of `kernel` that failed, now or
86    /// when it was queued. The failure is reported once and not kept, so the
87    /// next launch of the kernel tries again.
88    pub fn load(
89        &mut self,
90        kernel: &dyn CubeKernel,
91        id: &ArtifactId<VariantOf<T>>,
92        logger: &ServerLogger,
93    ) -> Result<T::Loaded, LaunchError> {
94        if self.queue.is_empty()
95            && let Some(loaded) = self.loaded.get(id)
96        {
97            return Ok(loaded.clone());
98        }
99        self.load_missing(kernel, id, logger)
100    }
101
102    /// [`load`](Self::load) past its one lookup: the kernel is not loaded, or
103    /// the queue has to compile first.
104    #[cold]
105    fn load_missing(
106        &mut self,
107        kernel: &dyn CubeKernel,
108        id: &ArtifactId<VariantOf<T>>,
109        logger: &ServerLogger,
110    ) -> Result<T::Loaded, LaunchError> {
111        loop {
112            // A failure is reported as it stands only when nothing else is
113            // queued: a batch drops it, and the kernel compiles in that batch.
114            let reported = self.failed.contains_key(id) && self.queue.is_empty();
115            let missing = !self.loaded.contains(id) && !reported;
116            let asked = missing.then(|| Request {
117                id: id.clone(),
118                kernel,
119                registration: None,
120            });
121            self.compile_queue(asked, logger);
122
123            if let Some(err) = self.failed.remove(id) {
124                return Err(err);
125            }
126            // An environment switch during the batch drops every kernel loaded
127            // before it, this one too when it was: it loads again, from the
128            // store of the environment switched to.
129            if let Some(loaded) = self.loaded.get(id) {
130                return Ok(loaded.clone());
131            }
132        }
133    }
134
135    /// Compiles every queued kernel now, rather than inside the next
136    /// [`load`](Self::load), so the launch that follows pays for its own
137    /// kernel only. A queued kernel that fails reports it when it is launched.
138    pub fn compile_queued(&mut self, logger: &ServerLogger) {
139        self.compile_queue(None, logger);
140    }
141
142    /// Compiles the queue and `asked` together, keeping each kernel or why it
143    /// failed.
144    fn compile_queue(&mut self, asked: Option<Request<'_, VariantOf<T>>>, logger: &ServerLogger) {
145        if self.queue.is_empty() && asked.is_none() {
146            return;
147        }
148        let mut queue = self.queue.take();
149        // A failure an earlier batch left unreported is dropped: its kernel
150        // compiles again when it is launched or queued.
151        self.failed.clear();
152        // Asked for and queued, it takes the registration it was queued
153        // with; asked for alone, it registers with the override open now.
154        let asked = asked.map(|mut asked| {
155            asked.registration = match queue.iter_mut().find(|queued| queued.id == asked.id) {
156                Some(queued) => queued.registration.take(),
157                None => KernelRegistration::register(),
158            };
159            asked
160        });
161        let mut requests: Vec<Request<'_, VariantOf<T>>> = queue
162            .iter_mut()
163            .filter(|queued| asked.as_ref().is_none_or(|asked| queued.id != asked.id))
164            .map(Queued::request)
165            .collect();
166        requests.extend(asked);
167        for outcome in self.compile(requests, logger) {
168            self.keep(outcome);
169        }
170    }
171
172    /// Queues `kernel` under `variant`, to be compiled with the others when the
173    /// next kernel is [loaded](Self::load) for a launch, and only then. One
174    /// already loaded or queued is left alone; one that failed is tried again,
175    /// its failure dropped.
176    ///
177    /// The queue does not compile on its own when it grows: one phase over
178    /// everything queued lasts about as long as its slowest kernel, where
179    /// two phases would each wait on theirs.
180    pub fn enqueue(&mut self, kernel: Box<dyn CubeKernel>, variant: VariantOf<T>) {
181        let id = ArtifactId {
182            kernel: kernel.id(),
183            variant,
184        };
185        if self.loaded.contains(&id) || self.queue.contains(&id) {
186            return;
187        }
188        self.failed.remove(&id);
189        self.queue.push(id, kernel, KernelRegistration::register());
190    }
191
192    /// Keeps a loaded kernel, or the reason it failed.
193    fn keep(&mut self, outcome: JobOutcome<T>) {
194        match outcome.result {
195            Ok(loaded) => self.loaded.insert(outcome.id, loaded),
196            Err(err) => {
197                self.failed.insert(outcome.id, err);
198            }
199        }
200    }
201
202    /// Obtains each requested kernel's artifact from the store or the
203    /// compiler, and loads it.
204    ///
205    /// The store and the loads are reached in order, on this thread; lowering
206    /// and finalizing run on several threads at once when more than one
207    /// kernel has that step to take.
208    fn compile(
209        &mut self,
210        requests: Vec<Request<'_, VariantOf<T>>>,
211        logger: &ServerLogger,
212    ) -> Vec<JobOutcome<T>> {
213        let batch = CompilationBatchRecording::new();
214        let mut jobs: Vec<Job<'_, T>> = requests.into_iter().map(Job::new).collect();
215        let jobs_len = jobs.len();
216
217        for job in jobs.iter_mut() {
218            job.look_up(&mut self.target);
219        }
220        // Each kernel settles as the batch finishes with it, so a
221        // collector's count moves while the batch runs: those the store held
222        // now, those compiled as each thread finishes one.
223        jobs.iter_mut().for_each(Job::settle);
224
225        let compiler = self.target.compiler();
226        let mut missing: Vec<&mut Job<'_, T>> =
227            jobs.iter_mut().filter(|job| job.is_missing()).collect();
228        let threads = self.parallelism.min(missing.len()).max(1);
229        if missing.len() > 1 {
230            log::info!(
231                target: "cubecl::compilation",
232                "Compiling {} kernels together ({} asked, the rest read from the store) on {} threads",
233                missing.len(),
234                jobs_len,
235                threads,
236            );
237        }
238        let reuses = self.target.persists();
239        for_each_at_once(&mut missing, self.parallelism, |job| {
240            job.lower(compiler, logger);
241            // With no artifact to reuse it by, a kernel finalizes on the thread
242            // that lowered it, without waiting for the rest of the batch to
243            // lower: the batch lasts about as long as its slowest kernel, not
244            // its slowest lowering plus its slowest finalizing.
245            if job.is_lowered() && !(reuses && job.has_source()) {
246                job.finalize(compiler);
247            }
248            job.settle();
249        });
250
251        let mut finalizing = HashSet::new();
252        for job in jobs.iter_mut() {
253            job.reuse(&mut self.target, &mut finalizing);
254        }
255
256        let leading: Vec<(usize, String)> = jobs
257            .iter()
258            .enumerate()
259            .filter_map(|(index, job)| match &job.step {
260                Step::Lowered {
261                    source: Some(source),
262                    ..
263                } => Some((index, source.clone())),
264                _ => None,
265            })
266            .collect();
267        let compiler = self.target.compiler();
268        // Those the store took by their source settle now; those waiting on
269        // another kernel of the batch with the same source settle once it is
270        // done, as they are loaded.
271        jobs.iter_mut().for_each(Job::settle);
272        let mut lowered: Vec<&mut Job<'_, T>> =
273            jobs.iter_mut().filter(|job| job.is_lowered()).collect();
274        for_each_at_once(&mut lowered, self.parallelism, |job| {
275            job.finalize(compiler);
276            job.settle();
277        });
278
279        // A kernel waiting on a source that failed to finalize fails the same
280        // way, rather than finalizing it again, one after another, on this
281        // thread.
282        let failed: HashMap<String, LaunchError> = leading
283            .into_iter()
284            .filter_map(|(index, source)| match &jobs[index].step {
285                Step::Failed(err) => Some((source, err.clone())),
286                _ => None,
287            })
288            .collect();
289        for job in jobs.iter_mut() {
290            if let Step::Waiting { source, .. } = &job.step
291                && let Some(err) = failed.get(source)
292            {
293                job.step = Step::Failed(err.clone());
294            }
295        }
296
297        let outcomes: Vec<JobOutcome<T>> = jobs
298            .into_iter()
299            .map(|job| job.load(&mut self.target))
300            .collect();
301        batch.close(BatchOutcome {
302            kernels: jobs_len,
303            threads,
304            stored: outcomes.iter().any(|outcome| outcome.stored),
305        });
306        outcomes
307    }
308}
309
310impl<T: CompilationTarget + core::fmt::Debug> core::fmt::Debug for KernelLoader<T> {
311    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312        // The loaded kernels are driver handles, which say nothing in a log.
313        f.debug_struct("KernelLoader")
314            .field("target", &self.target)
315            .field("queued", &self.queue.len())
316            .field("failed", &self.failed.len())
317            .field("parallelism", &self.parallelism)
318            .finish_non_exhaustive()
319    }
320}
321
322/// How a [`Job`] ended: its kernel loaded, or why it was not.
323struct JobOutcome<T: CompilationTarget> {
324    id: ArtifactId<VariantOf<T>>,
325    result: Result<T::Loaded, LaunchError>,
326    /// Whether the compilation store took its artifact.
327    stored: bool,
328}
329
330/// The kernels a [`KernelLoader`] holds loaded — pipelines, modules — in front
331/// of the compilation store its target persists to.
332///
333/// Entries are dropped when the environment switches, because the map is bound
334/// to an environment exactly as the store it mirrors is. One served after a
335/// switch would describe the environment that is gone, and, worse, would never
336/// be written to the new environment's store, so a bundle exported from that
337/// environment would silently be missing that kernel. This is the same contract
338/// a [`Store`](cubecl_environment::persistence::Store) applies to itself, for
339/// the state a store cannot see — see
340/// [`cubecl_environment::environment::generation`].
341///
342/// Every accessor resets before it answers, so the loader has nothing to
343/// remember beyond using this in place of a plain map.
344#[derive(Debug)]
345struct CompilationCache<K, V> {
346    entries: HashMap<K, V>,
347    /// The generation the entries were built under, or `None` when the cache
348    /// mirrors no store and so is unbound.
349    generation: Option<u32>,
350}
351
352impl<K: Eq + core::hash::Hash, V> CompilationCache<K, V> {
353    /// An empty cache, bound to the active environment when `bound`: when
354    /// its artifacts persist in a store that a switch replaces. One with no
355    /// persistent store to mirror is never reset — with nothing persisted, a
356    /// switch changes nothing about what it holds, so resetting it would only
357    /// buy a redundant compilation, the same reason the autotune cache
358    /// survives a switch when its persistent cache is off.
359    fn new(bound: bool) -> Self {
360        Self {
361            entries: HashMap::new(),
362            generation: bound.then(cubecl_environment::environment::generation),
363        }
364    }
365
366    /// The kernel loaded for `key`, if it is still valid.
367    fn get(&mut self, key: &K) -> Option<&V> {
368        self.reset_if_switched();
369        self.entries.get(key)
370    }
371
372    /// Whether a kernel is loaded for `key` and still valid.
373    fn contains(&mut self, key: &K) -> bool {
374        self.reset_if_switched();
375        self.entries.contains_key(key)
376    }
377
378    /// Keeps a freshly loaded kernel.
379    fn insert(&mut self, key: K, value: V) {
380        self.reset_if_switched();
381        self.entries.insert(key, value);
382    }
383
384    /// Drops every entry when the environment switched since the last access,
385    /// adopting the new generation so one switch costs one reset.
386    fn reset_if_switched(&mut self) {
387        let Some(generation) = self.generation else {
388            return;
389        };
390
391        let current = cubecl_environment::environment::generation();
392        if current == generation {
393            return;
394        }
395
396        log::debug!("Environment switched, dropping the in-memory compilation cache");
397        self.generation = Some(current);
398        self.entries.clear();
399    }
400}
401
402/// The kernels queued for compilation and not compiled yet, each once.
403struct KernelQueue<V> {
404    kernels: Vec<Queued<V>>,
405    /// Their ids, so queuing one more costs a lookup rather than a scan.
406    ids: HashSet<ArtifactId<V>>,
407}
408
409impl<V: Clone + Eq + core::hash::Hash> KernelQueue<V> {
410    fn new() -> Self {
411        Self {
412            kernels: Vec::new(),
413            ids: HashSet::new(),
414        }
415    }
416
417    fn is_empty(&self) -> bool {
418        self.kernels.is_empty()
419    }
420
421    fn len(&self) -> usize {
422        self.kernels.len()
423    }
424
425    fn contains(&self, id: &ArtifactId<V>) -> bool {
426        self.ids.contains(id)
427    }
428
429    /// Queues `kernel` under `id`, which is not queued yet.
430    fn push(
431        &mut self,
432        id: ArtifactId<V>,
433        kernel: Box<dyn CubeKernel>,
434        registration: Option<KernelRegistration>,
435    ) {
436        self.ids.insert(id.clone());
437        self.kernels.push(Queued {
438            id,
439            kernel,
440            registration,
441        });
442    }
443
444    /// Every queued kernel, leaving the queue empty.
445    fn take(&mut self) -> Vec<Queued<V>> {
446        self.ids.clear();
447        core::mem::take(&mut self.kernels)
448    }
449}
450
451/// A kernel queued for compilation.
452struct Queued<V> {
453    id: ArtifactId<V>,
454    kernel: Box<dyn CubeKernel>,
455    /// The registration it was queued with, handed to the batch that
456    /// compiles it.
457    registration: Option<KernelRegistration>,
458}
459
460impl<V: Clone> Queued<V> {
461    fn request(&mut self) -> Request<'_, V> {
462        Request {
463            id: self.id.clone(),
464            kernel: &*self.kernel,
465            registration: self.registration.take(),
466        }
467    }
468}
469
470/// A kernel to compile, queued or asked for.
471struct Request<'k, V> {
472    id: ArtifactId<V>,
473    kernel: &'k dyn CubeKernel,
474    /// Its registration, if an override was open to register it.
475    registration: Option<KernelRegistration>,
476}
477
478/// One kernel on its way through [`KernelLoader::compile`].
479struct Job<'k, T: CompilationTarget> {
480    id: ArtifactId<VariantOf<T>>,
481    kernel: &'k dyn CubeKernel,
482    recording: CompilationRecording,
483    step: Step<T>,
484    /// Where it stands in its collector's count.
485    count: JobCount,
486}
487
488/// Where a [`Job`] stands in the count of the collector it was registered
489/// with.
490enum JobCount {
491    /// No override was open to register it.
492    Uncounted,
493    /// Registered, its outcome not known yet.
494    Registered(KernelRegistration),
495    /// Its outcome counted; what its load does is counted next.
496    Settled(SettledKernel),
497}
498
499impl JobCount {
500    /// Count what the job's load did, once it settled.
501    fn record(&mut self, load: KernelHandling) {
502        if let Self::Settled(settled) = self {
503            settled.record(load);
504        }
505    }
506}
507
508/// How far a [`Job`] got.
509enum Step<T: CompilationTarget> {
510    /// The store holds nothing for it: it has to be compiled.
511    Missing,
512    /// Lowered, with the text its artifact can be reused by.
513    Lowered {
514        lowered: <T::Compiler as ArtifactCompiler>::Lowered,
515        source: Option<String>,
516    },
517    /// Lowered to the same source as an earlier kernel of the batch, whose
518    /// artifact it reuses once that one is stored rather than finalizing the
519    /// same text twice.
520    Waiting {
521        lowered: <T::Compiler as ArtifactCompiler>::Lowered,
522        source: String,
523    },
524    /// An artifact to load, and how it was obtained.
525    Ready {
526        artifact: <T::Compiler as ArtifactCompiler>::Artifact,
527        source: Option<String>,
528        outcome: CompilationOutcome,
529    },
530    Failed(LaunchError),
531}
532
533impl<T: CompilationTarget> Step<T> {
534    /// How the kernel ended, once it has: an artifact to load, compiled or
535    /// taken from the store, or a failure.
536    fn outcome(&self) -> Option<KernelOutcome> {
537        match self {
538            Self::Ready {
539                outcome: CompilationOutcome::Compiled,
540                ..
541            } => Some(KernelOutcome::Compiled),
542            Self::Ready {
543                outcome: CompilationOutcome::Loaded | CompilationOutcome::Rekeyed,
544                ..
545            } => Some(KernelOutcome::Loaded),
546            Self::Failed(_) => Some(KernelOutcome::Failed),
547            Self::Missing | Self::Lowered { .. } | Self::Waiting { .. } => None,
548        }
549    }
550}
551
552impl<'k, T: CompilationTarget> Job<'k, T> {
553    fn new(request: Request<'k, VariantOf<T>>) -> Self {
554        Self {
555            recording: CompilationRecording::new(&request.id.kernel),
556            id: request.id,
557            kernel: request.kernel,
558            step: Step::Missing,
559            count: match request.registration {
560                Some(registration) => JobCount::Registered(registration),
561                None => JobCount::Uncounted,
562            },
563        }
564    }
565
566    /// Settle it where it was registered, once the batch has it — compiled,
567    /// read from the store, or failed. It settles once, and a job still on
568    /// its way settles nothing yet.
569    fn settle(&mut self) {
570        let Some(outcome) = self.step.outcome() else {
571            return;
572        };
573        self.count = match core::mem::replace(&mut self.count, JobCount::Uncounted) {
574            JobCount::Registered(registration) => JobCount::Settled(registration.settle(outcome)),
575            count => count,
576        };
577    }
578
579    fn is_missing(&self) -> bool {
580        matches!(self.step, Step::Missing)
581    }
582
583    fn is_lowered(&self) -> bool {
584        matches!(self.step, Step::Lowered { .. })
585    }
586
587    /// Whether it lowered to text an artifact can be reused by.
588    fn has_source(&self) -> bool {
589        matches!(
590            self.step,
591            Step::Lowered {
592                source: Some(_),
593                ..
594            }
595        )
596    }
597
598    /// Takes what the store holds for the kernel, if it holds anything.
599    fn look_up(&mut self, target: &mut T) {
600        if let Some(artifact) = target.stored(&self.id) {
601            self.step = Step::Ready {
602                artifact,
603                source: None,
604                outcome: CompilationOutcome::Loaded,
605            };
606        }
607    }
608
609    /// Lowers a kernel the store did not hold.
610    fn lower(&mut self, compiler: &T::Compiler, logger: &ServerLogger) {
611        let started = Instant::now();
612        let (kernel, id, recording) = (self.kernel, &self.id, &mut self.recording);
613        self.step = match contained(|| compiler.lower(kernel, id, recording, logger)) {
614            Ok(lowered) => Step::Lowered {
615                source: T::Compiler::source(&lowered).map(String::from),
616                lowered,
617            },
618            Err(err) => Step::Failed(err),
619        };
620        self.recording.worked(started.elapsed());
621    }
622
623    /// Takes an artifact another kernel finalized from the same source, if
624    /// the store holds one; or, when an earlier kernel of this batch already
625    /// took that source or is about to finalize it, waits to take it from the
626    /// store once that kernel is stored. `finalizing` is the sources the batch
627    /// takes or finalizes so far. Without a store there is nothing to take or
628    /// wait for, and every kernel finalizes its own.
629    fn reuse(&mut self, target: &mut T, finalizing: &mut HashSet<String>) {
630        let Step::Lowered {
631            source: Some(source),
632            ..
633        } = &self.step
634        else {
635            return;
636        };
637        if !target.persists() {
638            return;
639        }
640
641        if finalizing.contains(source) {
642            let Step::Lowered {
643                lowered,
644                source: Some(source),
645            } = core::mem::replace(&mut self.step, Step::Missing)
646            else {
647                unreachable!("matched right above");
648            };
649            self.step = Step::Waiting { lowered, source };
650            return;
651        }
652
653        finalizing.insert(source.clone());
654        if let Some(artifact) = target.stored_for_source(source) {
655            self.step = Step::Ready {
656                artifact,
657                source: Some(source.clone()),
658                outcome: CompilationOutcome::Rekeyed,
659            };
660        }
661    }
662
663    /// Finalizes a lowered kernel nothing could be reused for.
664    fn finalize(&mut self, compiler: &T::Compiler) {
665        let Step::Lowered { lowered, source } = core::mem::replace(&mut self.step, Step::Missing)
666        else {
667            unreachable!("only lowered jobs are finalized");
668        };
669
670        let started = Instant::now();
671        let id = &self.id;
672        self.step = match contained(|| compiler.finalize(id, lowered)) {
673            Ok(artifact) => Step::Ready {
674                artifact,
675                source,
676                outcome: CompilationOutcome::Compiled,
677            },
678            Err(err) => Step::Failed(err),
679        };
680        self.recording.worked(started.elapsed());
681    }
682
683    /// Loads the artifact on the device, keeps it in the store when it was
684    /// not read from there, and closes the record.
685    ///
686    /// A waiting job takes what the kernel it waited for stored, now that the
687    /// batch has stored it, and finalizes its own only when there is no store
688    /// to take it from.
689    fn load(mut self, target: &mut T) -> JobOutcome<T> {
690        if let Step::Waiting { .. } = self.step {
691            let Step::Waiting { lowered, source } =
692                core::mem::replace(&mut self.step, Step::Missing)
693            else {
694                unreachable!("matched right above");
695            };
696            self.step = match target.stored_for_source(&source) {
697                Some(artifact) => Step::Ready {
698                    artifact,
699                    source: Some(source),
700                    outcome: CompilationOutcome::Rekeyed,
701                },
702                None => Step::Lowered {
703                    lowered,
704                    source: Some(source),
705                },
706            };
707            if self.is_lowered() {
708                self.finalize(target.compiler());
709            }
710        }
711        // Every job ends here, so none is left unsettled whatever step a
712        // batch grows: a waiting one settles now, its source done.
713        self.settle();
714
715        let mut stored = false;
716        let result = match self.step {
717            Step::Ready {
718                artifact,
719                source,
720                outcome,
721            } => {
722                let started = Instant::now();
723                match target.load(&self.id, &artifact) {
724                    Ok(loaded) => {
725                        self.recording.worked(started.elapsed());
726                        // What the store gave is in it already.
727                        if outcome != CompilationOutcome::Loaded {
728                            stored = target.store(&self.id, artifact, source.as_deref());
729                        }
730                        if stored {
731                            self.count.record(KernelHandling::Stored);
732                        }
733                        self.recording.close(outcome, stored);
734                        Ok(loaded)
735                    }
736                    Err(err) => {
737                        // Settled as obtained before the device saw it: the
738                        // refusal is counted on its own.
739                        self.count.record(KernelHandling::Refused);
740                        Err(err.into())
741                    }
742                }
743            }
744            Step::Failed(err) => Err(err),
745            Step::Missing | Step::Lowered { .. } | Step::Waiting { .. } => {
746                unreachable!("every job is lowered, then finalized or reused, or failed")
747            }
748        };
749        JobOutcome {
750            id: self.id,
751            result,
752            stored,
753        }
754    }
755}
756
757/// Runs one kernel's compiler step, a panic inside it becoming that kernel's
758/// failure with the panic's message.
759///
760/// A batch compiles many kernels at once, and every launch of the batch
761/// waits on it: left to unwind, one kernel the compiler cannot handle would
762/// fail the load of every other kernel compiled beside it, and an autotune
763/// round would blame each of its candidates for the one that broke.
764fn contained<R>(step: impl FnOnce() -> Result<R, LaunchError>) -> Result<R, LaunchError> {
765    #[cfg(feature = "std")]
766    {
767        std::panic::catch_unwind(core::panic::AssertUnwindSafe(step)).unwrap_or_else(|payload| {
768            let message = payload
769                .downcast_ref::<String>()
770                .map(String::as_str)
771                .or_else(|| payload.downcast_ref::<&str>().copied())
772                .unwrap_or("no message");
773            Err(LaunchError::CompilationError(
774                crate::compiler::CompilationError::Generic {
775                    reason: alloc::format!("the compiler panicked: {message}"),
776                    backtrace: cubecl_environment::backtrace::BackTrace::capture(),
777                },
778            ))
779        })
780    }
781    #[cfg(not(feature = "std"))]
782    step()
783}
784
785/// Runs `work` on every job, on up to `parallelism` threads at once, each
786/// taking the next job as it finishes one: kernels differ by orders of
787/// magnitude in how long they take to compile, so a fixed split would leave
788/// threads idle behind the slowest share.
789fn for_each_at_once<J: Send>(jobs: &mut [J], parallelism: usize, work: impl Fn(&mut J) + Sync) {
790    #[cfg(all(feature = "std", not(target_family = "wasm")))]
791    if jobs.len() > 1 && parallelism > 1 {
792        use core::sync::atomic::{AtomicUsize, Ordering};
793
794        let slots: Vec<std::sync::Mutex<&mut J>> =
795            jobs.iter_mut().map(std::sync::Mutex::new).collect();
796        let next = AtomicUsize::new(0);
797        let panic = std::thread::scope(|scope| {
798            let workers: Vec<_> = (0..parallelism.min(slots.len()))
799                .map(|_| {
800                    scope.spawn(|| {
801                        while let Some(slot) = slots.get(next.fetch_add(1, Ordering::Relaxed)) {
802                            // Each slot is taken by exactly one thread, so the
803                            // lock never waits; it only proves that to the
804                            // compiler.
805                            work(&mut slot.lock().unwrap_or_else(|err| err.into_inner()));
806                        }
807                    })
808                })
809                .collect();
810            // Every worker is joined, so the scope itself never panics: left
811            // to it, a worker's panic comes out as "a scoped thread panicked",
812            // its message gone.
813            workers
814                .into_iter()
815                .fold(None, |first, worker| first.or(worker.join().err()))
816        });
817        // The first panic goes on with its own payload, as it would have
818        // on one thread.
819        if let Some(payload) = panic {
820            std::panic::resume_unwind(payload);
821        }
822        return;
823    }
824
825    let _ = parallelism;
826    jobs.iter_mut().for_each(work);
827}
828
829/// Every test is serial: one that records reads the session every other one
830/// would write its compilations to.
831#[cfg(test)]
832mod tests {
833    use super::*;
834    use crate::compiler::CompilationError;
835    use crate::id::KernelId;
836    use crate::kernel::{KernelDefinition, KernelMetadata};
837    use alloc::collections::BTreeSet;
838    // `serial_test`'s macro expands to `vec!`, which a `no_std` crate has to
839    // bring in itself.
840    use alloc::vec;
841    use core::sync::atomic::{AtomicUsize, Ordering};
842    use cubecl_environment::backtrace::BackTrace;
843    use std::sync::Mutex;
844
845    /// Nothing compiles before something asks: queuing is free, and a load
846    /// compiles the queue with the kernel it asked for.
847    #[test]
848    #[serial_test::serial(records)]
849    fn a_load_compiles_the_queue() {
850        let mut loader = loader(8);
851        let logger = ServerLogger::default();
852        for number in 0..3 {
853            loader.enqueue(Box::new(Numbered(number)), ());
854        }
855        assert_eq!(
856            loader.target.compiler.lowered(),
857            0,
858            "queuing compiles nothing"
859        );
860
861        assert_eq!(loader.load(&Numbered(3), &id(3), &logger).unwrap(), 3);
862        assert_eq!(
863            loader.target.compiler.lowered(),
864            4,
865            "the queue compiled with it"
866        );
867        for number in 0..3 {
868            assert_eq!(loader.get(&id(number)), Some(&number));
869        }
870    }
871
872    /// A collector registers each kernel as it is queued under one of its
873    /// overrides, or as a launch asks for it unqueued, and every one of them
874    /// settles once the batch has it — under a later override too. A kernel
875    /// queued with no override open counts nowhere.
876    #[test]
877    #[serial_test::serial(records)]
878    fn a_batch_settles_every_kernel_to_the_collector_that_registered_it() {
879        use cubecl_runtime::execution::{ProcessMode, ProcessModeOverride, StatisticsCollector};
880
881        let mut loader = loader(2);
882        let logger = ServerLogger::default();
883        loader.enqueue(Box::new(Numbered(9)), ());
884        let collector = StatisticsCollector::new();
885        let compile = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
886        for number in 0..5 {
887            loader.enqueue(Box::new(Numbered(number)), ());
888        }
889        // Queued twice, registered once.
890        loader.enqueue(Box::new(Numbered(0)), ());
891        core::mem::drop(compile);
892        let queued = collector.statistics().compilation;
893        assert_eq!((queued.registered, queued.settled()), (5, 0));
894
895        let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
896        loader.load(&Numbered(5), &id(5), &logger).unwrap();
897        let batch = collector.statistics().compilation;
898        assert_eq!(
899            (batch.registered, batch.settled(), batch.failed),
900            (6, 6, 0),
901            "the kernel queued with no override open counts nowhere"
902        );
903        assert_eq!(batch.compiled + batch.loaded, 6);
904
905        loader.load(&Numbered(5), &id(5), &logger).unwrap();
906        assert_eq!(
907            collector.statistics().compilation,
908            batch,
909            "a loaded kernel is not registered again"
910        );
911        core::mem::drop(tune);
912    }
913
914    /// The compilation statistics of `collector`, as (registered, compiled,
915    /// loaded, failed, refused).
916    fn counted(
917        collector: &cubecl_runtime::execution::StatisticsCollector,
918    ) -> (usize, usize, usize, usize, usize) {
919        let statistics = collector.statistics().compilation;
920        (
921            statistics.registered,
922            statistics.compiled,
923            statistics.loaded,
924            statistics.failed,
925            statistics.refused,
926        )
927    }
928
929    /// A kernel that fails to compile settles as failed, is not registered
930    /// again while its failure is reported, and registers again each time
931    /// it is tried again.
932    #[test]
933    #[serial_test::serial(records)]
934    fn a_failure_settles_as_failed_and_a_retry_registers_again() {
935        use cubecl_runtime::execution::{ProcessMode, ProcessModeOverride, StatisticsCollector};
936
937        let collector = StatisticsCollector::new();
938        let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
939        let mut loader = loader(4);
940        loader.target.compiler.failing = Some(1);
941        let logger = ServerLogger::default();
942        for number in 0..3 {
943            loader.enqueue(Box::new(Numbered(number)), ());
944        }
945        loader.load(&Numbered(3), &id(3), &logger).unwrap();
946        assert_eq!(counted(&collector), (4, 3, 0, 1, 0));
947
948        assert!(loader.load(&Numbered(1), &id(1), &logger).is_err());
949        assert_eq!(counted(&collector), (4, 3, 0, 1, 0), "reported, not tried");
950        assert!(loader.load(&Numbered(1), &id(1), &logger).is_err());
951        assert_eq!(counted(&collector), (5, 3, 0, 2, 0), "tried again");
952
953        loader.target.compiler.failing = None;
954        loader.load(&Numbered(1), &id(1), &logger).unwrap();
955        assert_eq!(counted(&collector), (6, 4, 0, 2, 0));
956        core::mem::drop(tune);
957    }
958
959    /// Kernels of a batch that share a source settle with it: the one that
960    /// finalizes it as compiled, those that take its artifact as loaded, or
961    /// every one of them as failed when it fails.
962    #[test]
963    #[serial_test::serial(records)]
964    fn kernels_sharing_a_source_settle_with_it() {
965        use cubecl_runtime::execution::{ProcessMode, ProcessModeOverride, StatisticsCollector};
966
967        let logger = ServerLogger::default();
968        for (refusing, expected) in [(false, (4, 1, 3, 0, 0)), (true, (4, 0, 0, 4, 0))] {
969            let collector = StatisticsCollector::new();
970            let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
971            let mut loader = loader(4);
972            loader.target.compiler.shared_source = true;
973            loader.target.compiler.refusing = refusing;
974            for number in 0..3 {
975                loader.enqueue(Box::new(Numbered(number)), ());
976            }
977            let _ = loader.load(&Numbered(3), &id(3), &logger);
978            assert_eq!(counted(&collector), expected, "refusing: {refusing}");
979            // What the store keeps is what it is given by source: every kernel
980            // of a source that finalized, none of one that failed.
981            let stored = collector.statistics().compilation.stored;
982            assert_eq!(stored, if refusing { 0 } else { 4 }, "refusing: {refusing}");
983            core::mem::drop(tune);
984        }
985    }
986
987    /// A kernel the device refuses was already counted as compiled: the
988    /// refusal is counted beside it.
989    #[test]
990    #[serial_test::serial(records)]
991    fn a_refused_kernel_is_counted_beside_its_outcome() {
992        use cubecl_runtime::execution::{ProcessMode, ProcessModeOverride, StatisticsCollector};
993
994        let collector = StatisticsCollector::new();
995        let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
996        let mut loader = loader(4);
997        loader.target.rejecting = Some(1);
998        let logger = ServerLogger::default();
999        for number in 0..3 {
1000            loader.enqueue(Box::new(Numbered(number)), ());
1001        }
1002        loader.load(&Numbered(3), &id(3), &logger).unwrap();
1003        assert_eq!(loader.get(&id(1)), None);
1004        assert_eq!(counted(&collector), (4, 4, 0, 0, 1));
1005        assert_eq!(
1006            collector.statistics().compilation.stored,
1007            0,
1008            "a store keeps by source, and these kernels have none"
1009        );
1010        core::mem::drop(tune);
1011    }
1012
1013    /// A batch with a kernel that panics settles every kernel of it, the one
1014    /// that panicked as failed and every other one as it would have.
1015    #[test]
1016    #[serial_test::serial(records)]
1017    fn a_panicking_batch_settles_every_kernel() {
1018        use cubecl_runtime::execution::{ProcessMode, ProcessModeOverride, StatisticsCollector};
1019
1020        let collector = StatisticsCollector::new();
1021        let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
1022        let mut loader = loader(4);
1023        loader.target.compiler.panicking = Some(2);
1024        let logger = ServerLogger::default();
1025        for number in 0..4 {
1026            loader.enqueue(Box::new(Numbered(number)), ());
1027        }
1028        loader.load(&Numbered(4), &id(4), &logger).unwrap();
1029        let statistics = collector.statistics().compilation;
1030        assert_eq!(statistics.registered, 5);
1031        assert_eq!(statistics.settled(), 5);
1032        assert_eq!(statistics.failed, 1, "only the kernel that panicked");
1033        assert_eq!(statistics.abandoned, 0);
1034        core::mem::drop(tune);
1035    }
1036
1037    /// Asked to, the queue compiles without a launch, and the launch that
1038    /// follows pays a lookup.
1039    #[test]
1040    #[serial_test::serial(records)]
1041    fn the_queue_compiles_when_asked() {
1042        let mut loader = loader(8);
1043        let logger = ServerLogger::default();
1044        for number in 0..3 {
1045            loader.enqueue(Box::new(Numbered(number)), ());
1046        }
1047        loader.compile_queued(&logger);
1048        assert_eq!(loader.target.compiler.lowered(), 3);
1049        assert_eq!(loader.load(&Numbered(0), &id(0), &logger).unwrap(), 0);
1050        assert_eq!(
1051            loader.target.compiler.lowered(),
1052            3,
1053            "nothing left to compile"
1054        );
1055    }
1056
1057    /// A queue longer than the threads compiling it waits for its launch, and
1058    /// then compiles whole.
1059    #[test]
1060    #[serial_test::serial(records)]
1061    fn a_long_queue_compiles_in_one_phase() {
1062        let mut loader = loader(2);
1063        let logger = ServerLogger::default();
1064        for number in 0..5 {
1065            loader.enqueue(Box::new(Numbered(number)), ());
1066        }
1067        assert_eq!(loader.target.compiler.lowered(), 0, "the queue waits");
1068        loader.load(&Numbered(5), &id(5), &logger).unwrap();
1069        assert_eq!(
1070            loader.target.compiler.lowered(),
1071            6,
1072            "with the kernel launched"
1073        );
1074    }
1075
1076    /// A kernel queued twice, or queued once loaded, compiles once.
1077    #[test]
1078    #[serial_test::serial(records)]
1079    fn a_kernel_compiles_once() {
1080        let mut loader = loader(8);
1081        let logger = ServerLogger::default();
1082        loader.enqueue(Box::new(Numbered(0)), ());
1083        loader.load(&Numbered(0), &id(0), &logger).unwrap();
1084        loader.enqueue(Box::new(Numbered(0)), ());
1085        loader.load(&Numbered(0), &id(0), &logger).unwrap();
1086        assert_eq!(loader.target.compiler.lowered(), 1);
1087    }
1088
1089    /// A queued kernel that fails does not take the others with it, and its
1090    /// launch reports the failure without compiling it again; the launch after
1091    /// that tries again.
1092    #[test]
1093    #[serial_test::serial(records)]
1094    fn a_failure_is_reported_by_its_own_launch() {
1095        let mut loader = loader(8);
1096        loader.target.compiler.failing = Some(1);
1097        let logger = ServerLogger::default();
1098        for number in 0..3 {
1099            loader.enqueue(Box::new(Numbered(number)), ());
1100        }
1101        loader.load(&Numbered(3), &id(3), &logger).unwrap();
1102        assert_eq!(loader.get(&id(0)), Some(&0));
1103        assert_eq!(loader.get(&id(1)), None);
1104        assert_eq!(loader.get(&id(2)), Some(&2));
1105        assert_eq!(loader.target.compiler.lowered(), 4);
1106
1107        assert!(loader.load(&Numbered(1), &id(1), &logger).is_err());
1108        assert_eq!(
1109            loader.target.compiler.lowered(),
1110            4,
1111            "reported, not compiled again"
1112        );
1113        assert!(loader.load(&Numbered(1), &id(1), &logger).is_err());
1114        assert_eq!(
1115            loader.target.compiler.lowered(),
1116            5,
1117            "the next launch tries again"
1118        );
1119    }
1120
1121    /// A failure the next batch finds unreported is dropped, not kept: the
1122    /// kernel's launch compiles it again.
1123    #[test]
1124    #[serial_test::serial(records)]
1125    fn a_stale_failure_compiles_again() {
1126        let mut loader = loader(8);
1127        loader.target.compiler.failing = Some(1);
1128        let logger = ServerLogger::default();
1129        loader.enqueue(Box::new(Numbered(1)), ());
1130        loader.load(&Numbered(0), &id(0), &logger).unwrap();
1131        loader.enqueue(Box::new(Numbered(2)), ());
1132        loader.load(&Numbered(0), &id(0), &logger).unwrap();
1133        assert_eq!(loader.target.compiler.lowered(), 3);
1134
1135        loader.target.compiler.failing = None;
1136        assert_eq!(loader.load(&Numbered(1), &id(1), &logger).unwrap(), 1);
1137        assert_eq!(loader.target.compiler.lowered(), 4, "compiled again");
1138    }
1139
1140    /// A kernel queued again after it failed is tried again.
1141    #[test]
1142    #[serial_test::serial(records)]
1143    fn a_failure_queued_again_is_retried() {
1144        let mut loader = loader(8);
1145        loader.target.compiler.failing = Some(1);
1146        let logger = ServerLogger::default();
1147        loader.enqueue(Box::new(Numbered(1)), ());
1148        loader.load(&Numbered(0), &id(0), &logger).unwrap();
1149        assert_eq!(loader.get(&id(1)), None);
1150
1151        loader.target.compiler.failing = None;
1152        loader.enqueue(Box::new(Numbered(1)), ());
1153        loader.compile_queued(&logger);
1154        assert_eq!(loader.get(&id(1)), Some(&1));
1155    }
1156
1157    /// A kernel that panics on a compiling thread fails its own load, with
1158    /// its message, and only its own: every other kernel of the batch loads.
1159    #[test]
1160    #[serial_test::serial(records)]
1161    fn a_panic_fails_only_its_own_kernel() {
1162        let mut loader = loader(4);
1163        loader.target.compiler.panicking = Some(2);
1164        let logger = ServerLogger::default();
1165        for number in 0..4 {
1166            loader.enqueue(Box::new(Numbered(number)), ());
1167        }
1168        loader.load(&Numbered(4), &id(4), &logger).unwrap();
1169        for number in [0, 1, 3] {
1170            assert_eq!(loader.get(&id(number)), Some(&number));
1171        }
1172        let err = loader
1173            .load(&Numbered(2), &id(2), &logger)
1174            .expect_err("the kernel that panicked fails");
1175        assert!(
1176            alloc::format!("{err}").contains("kernel 2 panicked"),
1177            "the failure keeps the panic's message: {err}"
1178        );
1179    }
1180
1181    /// A batch is recorded once, with the wall time its kernels' work, spread
1182    /// over its threads, actually took.
1183    #[test]
1184    #[cfg(persistence)]
1185    #[serial_test::serial(records)]
1186    fn a_batch_is_recorded_beside_its_kernels() {
1187        use crate::compiler::{CompilationBatchRecord, CompilationRecord};
1188        use crate::config::RuntimeConfig;
1189        use cubecl_environment::persistence::Database;
1190        use cubecl_environment::records::{self, RecordLevel, Records, RecordsConfig};
1191
1192        let root = tempfile::tempdir().unwrap();
1193        let _ = crate::config::CubeClRuntimeConfig::get();
1194        cubecl_environment::environment::set_root(root.path());
1195        records::configure(RecordsConfig {
1196            level: RecordLevel::Basic,
1197            ..Default::default()
1198        });
1199
1200        let mut loader = loader(4);
1201        loader.target.compiler.slow = true;
1202        // A source the store takes: a session that changes nothing keeps
1203        // nothing it observed.
1204        loader.target.compiler.shared_source = true;
1205        let logger = ServerLogger::default();
1206        for number in 0..3 {
1207            loader.enqueue(Box::new(Numbered(number)), ());
1208        }
1209        loader.load(&Numbered(3), &id(3), &logger).unwrap();
1210
1211        let database = Database::open_active().unwrap();
1212        let records = Records::new(&database);
1213        let batches = records.read::<CompilationBatchRecord>();
1214        let kernels = records.read::<CompilationRecord>();
1215        assert_eq!(batches.len(), 1);
1216        assert_eq!(kernels.len(), 4);
1217        records::configure(RecordsConfig {
1218            level: RecordLevel::Off,
1219            ..Default::default()
1220        });
1221        let batch = &batches[0].record;
1222        assert_eq!((batch.kernels, batch.threads), (4, 4));
1223        let work: core::time::Duration = kernels.iter().map(|kernel| kernel.record.work).sum();
1224        assert!(
1225            batch.wall < work,
1226            "four threads: {:?} of wall for {work:?} of work",
1227            batch.wall
1228        );
1229    }
1230
1231    /// Kernels waiting on a source that fails to finalize fail with it: the
1232    /// source is finalized once, not once per kernel.
1233    #[test]
1234    #[serial_test::serial(records)]
1235    fn a_failed_source_is_finalized_once() {
1236        let mut loader = loader(8);
1237        loader.target.compiler.shared_source = true;
1238        loader.target.compiler.refusing = true;
1239        let logger = ServerLogger::default();
1240        for number in 0..3 {
1241            loader.enqueue(Box::new(Numbered(number)), ());
1242        }
1243        assert!(loader.load(&Numbered(3), &id(3), &logger).is_err());
1244        assert_eq!(loader.target.compiler.finalized.load(Ordering::Relaxed), 1);
1245        for number in 0..3 {
1246            assert_eq!(loader.get(&id(number)), None);
1247        }
1248    }
1249
1250    /// A kernel with no source to reuse an artifact by finalizes on the
1251    /// thread that lowered it, without waiting for the batch.
1252    #[test]
1253    #[serial_test::serial(records)]
1254    fn a_kernel_without_a_source_finalizes_where_it_lowered() {
1255        let mut loader = loader(4);
1256        loader.target.compiler.slow = true;
1257        let logger = ServerLogger::default();
1258        for number in 0..3 {
1259            loader.enqueue(Box::new(Numbered(number)), ());
1260        }
1261        loader.load(&Numbered(3), &id(3), &logger).unwrap();
1262        let lowered = loader.target.compiler.lowered_on.lock().unwrap().clone();
1263        let finalized = loader.target.compiler.finalized_on.lock().unwrap().clone();
1264        assert_eq!(lowered.len(), 4);
1265        assert_eq!(lowered, finalized);
1266    }
1267
1268    /// A kernel about to execute compiles the queue even when it is loaded
1269    /// itself: the queue promises to be compiled by then.
1270    #[test]
1271    #[serial_test::serial(records)]
1272    fn a_loaded_kernel_still_compiles_the_queue() {
1273        let mut loader = loader(8);
1274        let logger = ServerLogger::default();
1275        loader.load(&Numbered(0), &id(0), &logger).unwrap();
1276        loader.enqueue(Box::new(Numbered(1)), ());
1277        loader.load(&Numbered(0), &id(0), &logger).unwrap();
1278        assert_eq!(loader.get(&id(1)), Some(&1));
1279    }
1280
1281    /// Kernels of one batch that lower to the same source finalize it once:
1282    /// the others take the artifact the first one stored.
1283    #[test]
1284    #[serial_test::serial(records)]
1285    fn one_source_finalizes_once() {
1286        let mut loader = loader(8);
1287        loader.target.compiler.shared_source = true;
1288        let logger = ServerLogger::default();
1289        for number in 0..4 {
1290            loader.enqueue(Box::new(Numbered(number)), ());
1291        }
1292        loader.load(&Numbered(4), &id(4), &logger).unwrap();
1293        assert_eq!(loader.target.compiler.finalized.load(Ordering::Relaxed), 1);
1294        for number in 0..5 {
1295            assert!(loader.get(&id(number)).is_some());
1296        }
1297    }
1298
1299    /// A source the store already holds serves every kernel of a batch that
1300    /// lowers to it: none of them finalizes it again.
1301    #[test]
1302    #[serial_test::serial(records)]
1303    fn a_stored_source_serves_the_whole_batch() {
1304        let mut loader = loader(8);
1305        loader.target.compiler.shared_source = true;
1306        loader.target.by_source.insert("shared".into(), 99);
1307        let logger = ServerLogger::default();
1308        for number in 0..2 {
1309            loader.enqueue(Box::new(Numbered(number)), ());
1310        }
1311        loader.load(&Numbered(2), &id(2), &logger).unwrap();
1312        assert_eq!(loader.target.compiler.finalized.load(Ordering::Relaxed), 0);
1313        for number in 0..3 {
1314            assert_eq!(loader.get(&id(number)), Some(&99));
1315        }
1316    }
1317
1318    /// Without a store there is no artifact to wait for: every kernel
1319    /// finalizes its own, on the compiling threads.
1320    #[test]
1321    #[serial_test::serial(records)]
1322    fn without_a_store_each_kernel_finalizes_its_own() {
1323        let mut loader = KernelLoader::new(Fake {
1324            storeless: true,
1325            ..Fake::default()
1326        });
1327        loader.parallelism = 8;
1328        loader.target.compiler.shared_source = true;
1329        let logger = ServerLogger::default();
1330        for number in 0..3 {
1331            loader.enqueue(Box::new(Numbered(number)), ());
1332        }
1333        loader.load(&Numbered(3), &id(3), &logger).unwrap();
1334        assert_eq!(loader.target.compiler.finalized.load(Ordering::Relaxed), 4);
1335    }
1336
1337    /// A queue is lowered on several threads at once.
1338    #[test]
1339    #[serial_test::serial(records)]
1340    fn a_queue_compiles_on_several_threads() {
1341        let mut loader = loader(4);
1342        loader.target.compiler.slow = true;
1343        let logger = ServerLogger::default();
1344        for number in 0..8 {
1345            loader.enqueue(Box::new(Numbered(number)), ());
1346        }
1347        loader.load(&Numbered(8), &id(8), &logger).unwrap();
1348        let threads = loader.target.compiler.threads.lock().unwrap().len();
1349        assert!(threads > 1, "lowered on {threads} thread");
1350    }
1351
1352    fn loader(parallelism: usize) -> KernelLoader<Fake> {
1353        let mut loader = KernelLoader::new(Fake::default());
1354        loader.parallelism = parallelism;
1355        loader
1356    }
1357
1358    fn id(number: u32) -> ArtifactId<()> {
1359        ArtifactId {
1360            kernel: Numbered(number).id(),
1361            variant: (),
1362        }
1363    }
1364
1365    /// A kernel the fake compiler tells apart by its number, and never
1366    /// expands.
1367    #[derive(Debug)]
1368    struct Numbered(u32);
1369
1370    impl KernelMetadata for Numbered {
1371        fn id(&self) -> KernelId {
1372            KernelId::new::<Self>().info(self.0)
1373        }
1374
1375        fn address_type(&self) -> cubecl_ir::ElemType {
1376            cubecl_ir::ElemType::UInt(cubecl_ir::UIntKind::U32)
1377        }
1378    }
1379
1380    impl CubeKernel for Numbered {
1381        fn define(&self) -> KernelDefinition {
1382            unreachable!("the fake compiler reads the number, not the definition")
1383        }
1384    }
1385
1386    /// A target whose kernels are their numbers, keeping its artifacts by
1387    /// source only.
1388    #[derive(Debug, Default)]
1389    struct Fake {
1390        compiler: FakeCompiler,
1391        by_source: alloc::collections::BTreeMap<String, u32>,
1392        /// Whether it keeps nothing between runs, by source or otherwise.
1393        storeless: bool,
1394        /// The number of the kernel the device refuses to load.
1395        rejecting: Option<u32>,
1396    }
1397
1398    #[derive(Debug, Default)]
1399    struct FakeCompiler {
1400        lowered: AtomicUsize,
1401        /// The number of the kernel that fails to lower.
1402        failing: Option<u32>,
1403        /// The number of the kernel whose lowering panics.
1404        panicking: Option<u32>,
1405        /// Whether lowering takes long enough for other threads to pick up
1406        /// the rest of the queue.
1407        slow: bool,
1408        /// The threads lowering ran on.
1409        threads: Mutex<BTreeSet<String>>,
1410        /// Whether every kernel lowers to the same source.
1411        shared_source: bool,
1412        finalized: AtomicUsize,
1413        /// Whether finalizing fails.
1414        refusing: bool,
1415        /// The thread each kernel lowered on, by number.
1416        lowered_on: Mutex<alloc::collections::BTreeMap<u32, String>>,
1417        /// The thread each kernel finalized on, by number.
1418        finalized_on: Mutex<alloc::collections::BTreeMap<u32, String>>,
1419    }
1420
1421    impl FakeCompiler {
1422        fn lowered(&self) -> usize {
1423            self.lowered.load(Ordering::Relaxed)
1424        }
1425    }
1426
1427    impl ArtifactCompiler for FakeCompiler {
1428        type Variant = ();
1429        type Lowered = (u32, Option<String>);
1430        type Artifact = u32;
1431
1432        fn lower(
1433            &self,
1434            kernel: &dyn CubeKernel,
1435            id: &ArtifactId<()>,
1436            _recording: &mut CompilationRecording,
1437            _logger: &ServerLogger,
1438        ) -> Result<(u32, Option<String>), LaunchError> {
1439            self.lowered.fetch_add(1, Ordering::Relaxed);
1440            self.threads
1441                .lock()
1442                .unwrap()
1443                .insert(alloc::format!("{:?}", std::thread::current().id()));
1444            if self.slow {
1445                std::thread::sleep(core::time::Duration::from_millis(20));
1446            }
1447
1448            let number = (0..64)
1449                .find(|number| Numbered(*number).id() == id.kernel)
1450                .expect("a numbered kernel");
1451            assert_eq!(kernel.id(), id.kernel);
1452            self.lowered_on
1453                .lock()
1454                .unwrap()
1455                .insert(number, alloc::format!("{:?}", std::thread::current().id()));
1456            if self.panicking == Some(number) {
1457                panic!("kernel {number} panicked");
1458            }
1459            match self.failing == Some(number) {
1460                true => Err(LaunchError::Unknown {
1461                    reason: "refused".into(),
1462                    backtrace: BackTrace::capture(),
1463                }),
1464                false => Ok((number, self.shared_source.then(|| "shared".into()))),
1465            }
1466        }
1467
1468        fn source(lowered: &(u32, Option<String>)) -> Option<&str> {
1469            lowered.1.as_deref()
1470        }
1471
1472        fn finalize(
1473            &self,
1474            _id: &ArtifactId<()>,
1475            lowered: (u32, Option<String>),
1476        ) -> Result<u32, LaunchError> {
1477            self.finalized.fetch_add(1, Ordering::Relaxed);
1478            self.finalized_on.lock().unwrap().insert(
1479                lowered.0,
1480                alloc::format!("{:?}", std::thread::current().id()),
1481            );
1482            match self.refusing {
1483                true => Err(LaunchError::Unknown {
1484                    reason: "refused".into(),
1485                    backtrace: BackTrace::capture(),
1486                }),
1487                false => Ok(lowered.0),
1488            }
1489        }
1490    }
1491
1492    impl CompilationTarget for Fake {
1493        type Compiler = FakeCompiler;
1494        type Loaded = u32;
1495
1496        fn compiler(&self) -> &FakeCompiler {
1497            &self.compiler
1498        }
1499
1500        fn persists(&self) -> bool {
1501            !self.storeless
1502        }
1503
1504        fn stored(&mut self, _id: &ArtifactId<()>) -> Option<u32> {
1505            None
1506        }
1507
1508        fn load(
1509            &mut self,
1510            loaded: &ArtifactId<()>,
1511            artifact: &u32,
1512        ) -> Result<u32, CompilationError> {
1513            match self.rejecting.is_some_and(|number| *loaded == id(number)) {
1514                true => Err(CompilationError::Validation {
1515                    reason: "rejected".into(),
1516                    backtrace: BackTrace::capture(),
1517                }),
1518                false => Ok(*artifact),
1519            }
1520        }
1521
1522        fn stored_for_source(&mut self, source: &str) -> Option<u32> {
1523            self.by_source.remove(source)
1524        }
1525
1526        fn store(&mut self, _id: &ArtifactId<()>, artifact: u32, source: Option<&str>) -> bool {
1527            if let Some(source) = source {
1528                self.by_source.insert(source.into(), artifact);
1529            }
1530            source.is_some()
1531        }
1532    }
1533}