1use 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
20pub struct KernelLoader<T: CompilationTarget> {
35 target: T,
36 loaded: CompilationCache<ArtifactId<VariantOf<T>>, T::Loaded>,
37 queue: KernelQueue<VariantOf<T>>,
39 failed: HashMap<ArtifactId<VariantOf<T>>, LaunchError>,
46 parallelism: usize,
48}
49
50impl<T: CompilationTarget> KernelLoader<T> {
51 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 #[cfg(test)]
70 fn get(&mut self, id: &ArtifactId<VariantOf<T>>) -> Option<&T::Loaded> {
71 self.loaded.get(id)
72 }
73
74 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 #[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 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 if let Some(loaded) = self.loaded.get(id) {
130 return Ok(loaded.clone());
131 }
132 }
133 }
134
135 pub fn compile_queued(&mut self, logger: &ServerLogger) {
139 self.compile_queue(None, logger);
140 }
141
142 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 self.failed.clear();
152 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 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 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 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 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 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 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 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 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
322struct JobOutcome<T: CompilationTarget> {
324 id: ArtifactId<VariantOf<T>>,
325 result: Result<T::Loaded, LaunchError>,
326 stored: bool,
328}
329
330#[derive(Debug)]
345struct CompilationCache<K, V> {
346 entries: HashMap<K, V>,
347 generation: Option<u32>,
350}
351
352impl<K: Eq + core::hash::Hash, V> CompilationCache<K, V> {
353 fn new(bound: bool) -> Self {
360 Self {
361 entries: HashMap::new(),
362 generation: bound.then(cubecl_environment::environment::generation),
363 }
364 }
365
366 fn get(&mut self, key: &K) -> Option<&V> {
368 self.reset_if_switched();
369 self.entries.get(key)
370 }
371
372 fn contains(&mut self, key: &K) -> bool {
374 self.reset_if_switched();
375 self.entries.contains_key(key)
376 }
377
378 fn insert(&mut self, key: K, value: V) {
380 self.reset_if_switched();
381 self.entries.insert(key, value);
382 }
383
384 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
402struct KernelQueue<V> {
404 kernels: Vec<Queued<V>>,
405 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 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 fn take(&mut self) -> Vec<Queued<V>> {
446 self.ids.clear();
447 core::mem::take(&mut self.kernels)
448 }
449}
450
451struct Queued<V> {
453 id: ArtifactId<V>,
454 kernel: Box<dyn CubeKernel>,
455 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
470struct Request<'k, V> {
472 id: ArtifactId<V>,
473 kernel: &'k dyn CubeKernel,
474 registration: Option<KernelRegistration>,
476}
477
478struct Job<'k, T: CompilationTarget> {
480 id: ArtifactId<VariantOf<T>>,
481 kernel: &'k dyn CubeKernel,
482 recording: CompilationRecording,
483 step: Step<T>,
484 count: JobCount,
486}
487
488enum JobCount {
491 Uncounted,
493 Registered(KernelRegistration),
495 Settled(SettledKernel),
497}
498
499impl JobCount {
500 fn record(&mut self, load: KernelHandling) {
502 if let Self::Settled(settled) = self {
503 settled.record(load);
504 }
505 }
506}
507
508enum Step<T: CompilationTarget> {
510 Missing,
512 Lowered {
514 lowered: <T::Compiler as ArtifactCompiler>::Lowered,
515 source: Option<String>,
516 },
517 Waiting {
521 lowered: <T::Compiler as ArtifactCompiler>::Lowered,
522 source: String,
523 },
524 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 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 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 fn has_source(&self) -> bool {
589 matches!(
590 self.step,
591 Step::Lowered {
592 source: Some(_),
593 ..
594 }
595 )
596 }
597
598 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 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 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 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 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 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 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 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
757fn 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
785fn 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 work(&mut slot.lock().unwrap_or_else(|err| err.into_inner()));
806 }
807 })
808 })
809 .collect();
810 workers
814 .into_iter()
815 .fold(None, |first, worker| first.or(worker.join().err()))
816 });
817 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#[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 use alloc::vec;
841 use core::sync::atomic::{AtomicUsize, Ordering};
842 use cubecl_environment::backtrace::BackTrace;
843 use std::sync::Mutex;
844
845 #[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 #[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 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[derive(Debug, Default)]
1389 struct Fake {
1390 compiler: FakeCompiler,
1391 by_source: alloc::collections::BTreeMap<String, u32>,
1392 storeless: bool,
1394 rejecting: Option<u32>,
1396 }
1397
1398 #[derive(Debug, Default)]
1399 struct FakeCompiler {
1400 lowered: AtomicUsize,
1401 failing: Option<u32>,
1403 panicking: Option<u32>,
1405 slow: bool,
1408 threads: Mutex<BTreeSet<String>>,
1410 shared_source: bool,
1412 finalized: AtomicUsize,
1413 refusing: bool,
1415 lowered_on: Mutex<alloc::collections::BTreeMap<u32, String>>,
1417 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}