1#[allow(
3 non_camel_case_types,
4 non_snake_case,
5 non_upper_case_globals,
6 dead_code,
7 missing_docs,
8 unsafe_op_in_unsafe_fn,
9 clippy::all
10)]
11mod ffi;
12mod native;
13mod report;
14mod source;
15use crate::{
16 Error, Result,
17 bundle::{self, Lock},
18};
19pub use report::{
20 CompileReport, EntryChange, EntryResources, Expansion, ReportGuidance, ReportSuggestion,
21 WaitCounts, WaitReason, WaitReasonChange,
22};
23use serde::{Deserialize, Serialize};
24pub use source::{CxxSource, CxxStandard, Source};
25use std::{
26 collections::{BTreeMap, HashMap, HashSet},
27 fs,
28 num::NonZeroUsize,
29 path::{Path, PathBuf},
30 sync::{Arc, Mutex},
31};
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
37#[serde(rename_all = "lowercase")]
38pub enum Severity {
39 Note,
41 Warning,
43 Error,
45}
46impl From<u32> for Severity {
47 fn from(native: u32) -> Self {
50 match native {
51 0 => Self::Note,
52 1 => Self::Warning,
53 _ => Self::Error,
54 }
55 }
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
60pub struct Diagnostic {
61 pub severity: Severity,
63 pub code: String,
65 pub message: String,
67 pub formatted_text: String,
69 pub parameters: Vec<DiagnosticParameter>,
71 #[serde(default)]
73 pub source: String,
74 pub line: u32,
76 pub column: u32,
78 #[serde(default)]
80 pub related_locations: Vec<RelatedLocation>,
81 #[serde(default)]
83 pub related_location_omitted_count: usize,
84}
85
86#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
88pub struct DiagnosticParameter {
89 pub name: String,
91 pub value: String,
93}
94
95#[derive(Clone, Debug, Serialize, Deserialize)]
97pub struct RelatedLocation {
98 pub label: String,
100 pub source: String,
102 pub line: u32,
104 pub column: u32,
106 pub end_line: u32,
108 pub end_column: u32,
110}
111#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum ProcessorMode {
115 #[default]
117 Default,
118 ComputeUnit,
120 WorkgroupProcessor,
122}
123
124#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
126pub struct SanitizerChecks {
127 pub access: bool,
129 pub value: bool,
131 pub operation: bool,
133 pub race: bool,
135}
136impl SanitizerChecks {
137 pub(super) fn bits(self) -> u64 {
138 u64::from(self.access)
139 | (u64::from(self.value) << 1)
140 | (u64::from(self.operation) << 2)
141 | (u64::from(self.race) << 3)
142 }
143}
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
146pub enum SanitizerReporting {
147 #[default]
149 Default,
150 Trap,
152 ReportOnly,
154}
155#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
158pub struct SanitizerOptions {
159 pub checks: SanitizerChecks,
161 pub reporting: SanitizerReporting,
163}
164
165#[derive(Clone, Debug)]
167pub struct CompilerOptions {
168 pub workers: NonZeroUsize,
170 pub target: crate::Target,
172 pub module_cache_capacity: usize,
175 pub processor_mode: ProcessorMode,
177 pub sanitizer: SanitizerOptions,
179}
180impl Default for CompilerOptions {
181 fn default() -> Self {
182 Self {
183 target: crate::Target::default(),
184 module_cache_capacity: 64,
185 processor_mode: ProcessorMode::Default,
186 sanitizer: SanitizerOptions::default(),
187 workers: NonZeroUsize::new(
188 std::thread::available_parallelism()
189 .map_or(1, NonZeroUsize::get)
190 .min(4),
191 )
192 .unwrap(),
193 }
194 }
195}
196impl CompilerOptions {
197 #[must_use]
199 pub fn for_target(target: &crate::Target) -> Self {
200 Self {
201 target: target.clone(),
202 ..Self::default()
203 }
204 }
205 #[must_use]
207 pub fn for_stream(stream: &crate::Stream) -> Self {
208 Self::for_target(stream.target())
209 }
210}
211struct Inner {
212 modules: Mutex<HashMap<String, Arc<ModuleData>>>,
214 native: native::Prepared,
215 target: crate::Target,
216 workers: NonZeroUsize,
217 module_cache_capacity: usize,
218 processor_mode: ProcessorMode,
219 sanitizer: SanitizerOptions,
220 path: PathBuf,
221 identity: String,
222}
223
224#[derive(Hash, PartialEq, Eq)]
225struct SharedCompilerKey {
226 path: PathBuf,
227 target: String,
228 workers: NonZeroUsize,
229 module_cache_capacity: usize,
230 processor_mode: ProcessorMode,
231 sanitizer: SanitizerOptions,
232}
233#[derive(Clone)]
235pub struct Compiler(Arc<Inner>);
236impl std::fmt::Debug for Compiler {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 f.debug_struct("Compiler")
239 .field("library", &self.0.path)
240 .field("identity", &self.0.identity)
241 .finish_non_exhaustive()
242 }
243}
244impl Compiler {
245 pub fn resolve(library: Option<&Path>) -> Result<Self> {
250 Self::with_options(library, CompilerOptions::default())
251 }
252 pub fn for_stream(library: Option<&Path>, stream: &crate::Stream) -> Result<Self> {
254 Self::shared(library, CompilerOptions::for_stream(stream))
255 }
256 pub fn for_target(library: Option<&Path>, target: &crate::Target) -> Result<Self> {
258 Self::shared(library, CompilerOptions::for_target(target))
259 }
260 pub fn shared(library: Option<&Path>, options: CompilerOptions) -> Result<Self> {
269 static RESOLVED: Mutex<Option<HashMap<SharedCompilerKey, Compiler>>> = Mutex::new(None);
270 let key = SharedCompilerKey {
271 path: Self::resolved_library(library)?,
272 target: options.target.as_str().to_owned(),
273 workers: options.workers,
274 module_cache_capacity: options.module_cache_capacity,
275 processor_mode: options.processor_mode,
276 sanitizer: options.sanitizer,
277 };
278 let mut cache = RESOLVED
279 .lock()
280 .map_err(|_| Error::Message("compiler cache poisoned".into()))?;
281 let cache = cache.get_or_insert_with(HashMap::new);
282 if let Some(compiler) = cache.get(&key) {
283 return Ok(compiler.clone());
284 }
285 let compiler = Self::with_options(Some(&key.path), options)?;
286 cache.insert(key, compiler.clone());
287 Ok(compiler)
288 }
289
290 fn resolved_library(library: Option<&Path>) -> Result<PathBuf> {
292 match library
293 .map(Path::to_path_buf)
294 .or_else(|| std::env::var_os("HRX_LOOM_LIBRARY").map(PathBuf::from))
295 {
296 Some(p) => fs::canonicalize(&p)
297 .map_err(|e| Error::from(e).context(format!("compiler library {}", p.display()))),
298 None => Ok(fs::canonicalize(bundle::resolve()?.join("libloomc.so"))?),
299 }
300 }
301
302 pub fn with_options(library: Option<&Path>, options: CompilerOptions) -> Result<Self> {
304 let path = Self::resolved_library(library)?;
305 let identity = bundle::file_digest(&path)?;
306 let native = native::Prepared::open(
307 &path,
308 &identity,
309 options.workers.get(),
310 &options.target,
311 options.processor_mode,
312 options.sanitizer,
313 )?;
314 if bundle::file_digest(&path)? != identity {
315 return Err(Error::Message(
316 "compiler library changed while loading".into(),
317 ));
318 }
319 Ok(Self(Arc::new(Inner {
320 modules: Mutex::new(HashMap::new()),
321 native,
322 target: options.target,
323 workers: options.workers,
324 module_cache_capacity: options.module_cache_capacity,
325 processor_mode: options.processor_mode,
326 sanitizer: options.sanitizer,
327 path,
328 identity,
329 })))
330 }
331 pub fn workers(&self) -> NonZeroUsize {
333 self.0.workers
334 }
335
336 pub fn compile_all(&self, requests: &[(&Module, &Specialization)]) -> Vec<Result<Artifact>> {
344 let limit = self.0.workers.get().min(requests.len());
345 if limit <= 1 {
346 return requests
347 .iter()
348 .map(|(module, spec)| module.compile(spec))
349 .collect();
350 }
351 let next = std::sync::atomic::AtomicUsize::new(0);
352 let slots: Vec<Mutex<Option<Result<Artifact>>>> =
353 requests.iter().map(|_| Mutex::new(None)).collect();
354 std::thread::scope(|scope| {
355 for _ in 0..limit {
356 scope.spawn(|| {
357 loop {
358 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
359 let Some((module, spec)) = requests.get(index) else {
360 return;
361 };
362 let outcome = module.compile(spec);
363 *slots[index].lock().unwrap_or_else(|e| e.into_inner()) = Some(outcome);
364 }
365 });
366 }
367 });
368 slots
369 .into_iter()
370 .map(|slot| {
371 slot.into_inner()
372 .unwrap_or_else(|e| e.into_inner())
373 .expect("every request slot is filled before the scope ends")
374 })
375 .collect()
376 }
377
378 pub fn target(&self) -> &crate::Target {
380 &self.0.target
381 }
382 pub fn identity(&self) -> &str {
384 &self.0.identity
385 }
386 pub fn library_path(&self) -> &Path {
388 &self.0.path
389 }
390 pub fn module(&self, source: &str) -> Module {
393 self.source_module(vec![Source::loom("kernel.loom", source)])
394 }
395 pub fn sources(&self, mut sources: Vec<Source>) -> Result<Module> {
398 if sources.is_empty() {
399 return Err(Error::Message(
400 "a compilation module requires source inputs".into(),
401 ));
402 }
403 let mut identifiers = std::collections::BTreeSet::new();
404 for source in &mut sources {
405 source.validate()?;
406 let identifier = match source {
407 Source::Cxx(unit) => {
408 unit.normalize()?;
409 &unit.identifier
410 }
411 Source::Loom { identifier, .. } => identifier,
412 };
413 if !identifiers.insert(identifier.clone()) {
414 return Err(Error::Message("duplicate source identifier".into()));
415 }
416 }
417 Ok(self.source_module(sources))
418 }
419 pub fn import_cxx(&self, source: CxxSource) -> Result<Module> {
421 self.sources(vec![Source::Cxx(source)])
422 }
423 fn source_module(&self, sources: Vec<Source>) -> Module {
424 let digest = bundle::digest(
425 &serde_json::to_vec(&sources).expect("source inputs contain only JSON-safe values"),
426 );
427 let mut modules = self.0.modules.lock().unwrap_or_else(|e| e.into_inner());
428 let data = if let Some(data) = modules.get(&digest) {
429 data.clone()
430 } else {
431 let data = Arc::new(ModuleData {
432 sources,
433 digest: digest.clone(),
434 index: Mutex::new(None),
435 });
436 if self.0.module_cache_capacity != 0 {
437 if modules.len() >= self.0.module_cache_capacity {
438 let victim = modules.keys().next().cloned().unwrap();
439 modules.remove(&victim);
440 }
441 modules.insert(digest, data.clone());
442 }
443 data
444 };
445 Module {
446 compiler: self.clone(),
447 data,
448 }
449 }
450 pub fn trim(&self) {
453 self.0
454 .modules
455 .lock()
456 .unwrap_or_else(|e| e.into_inner())
457 .clear();
458 self.0.native.trim();
459 }
460}
461fn summarize(diagnostics: &[Diagnostic]) -> String {
468 let errors = || diagnostics.iter().filter(|d| d.severity >= Severity::Error);
469 let Some(first) = errors().next() else {
470 return diagnostics
471 .iter()
472 .map(|d| d.message.as_str())
473 .collect::<Vec<_>>()
474 .join("\n");
475 };
476 let mut message = if first.line == 0 {
477 first.message.clone()
478 } else {
479 format!("{}:{}: {}", first.line, first.column, first.message)
480 };
481 if let Some(hint) = hint(&first.message) {
482 message.push_str("\nhint: ");
483 message.push_str(hint);
484 }
485 for related in &first.related_locations {
486 message.push_str(&format!(
487 "\n{}:{}:{}: {}",
488 related.source, related.line, related.column, related.label
489 ));
490 }
491 if first.related_location_omitted_count != 0 {
492 message.push_str(&format!(
493 "\n({} additional related location(s) omitted)",
494 first.related_location_omitted_count
495 ));
496 }
497 let suppressed = errors().count() - 1;
498 if suppressed > 0 {
499 message.push_str(&format!(
500 "\n({suppressed} later error(s) suppressed as cascade; see Error::Compile diagnostics)"
501 ));
502 }
503 message
504}
505
506fn hint(message: &str) -> Option<&'static str> {
508 if message.contains("Low representation contract") && message.contains(".generic.") {
509 return Some(
510 "a generic target has no low-asm contract; name a bare architecture \
511 such as gfx1151 in amdgpu.target<...> to use hand-written asm",
512 );
513 }
514 None
515}
516
517#[cfg(test)]
518mod diagnostic_tests {
519 use super::{Diagnostic, Severity, summarize};
520 fn diagnostic(severity: u32, line: u32, message: &str) -> Diagnostic {
521 Diagnostic {
522 severity: Severity::from(severity),
523 code: String::new(),
524 message: message.into(),
525 formatted_text: message.into(),
526 parameters: Vec::new(),
527 source: String::new(),
528 line,
529 column: 1,
530 related_locations: Vec::new(),
531 related_location_omitted_count: 0,
532 }
533 }
534 #[test]
535 fn the_first_error_leads_and_its_cascade_is_counted_not_printed() {
536 let mut all = vec![
537 diagnostic(1, 3, "unused binding"),
538 diagnostic(
539 2,
540 7,
541 "unknown Low representation contract amdgpu.gfx11.generic.core",
542 ),
543 ];
544 all.extend((0..12).map(|i| diagnostic(2, 8 + i, "expected expression")));
545 let rendered = summarize(&all);
546 assert!(rendered.starts_with("7:1: unknown Low representation contract"));
547 assert!(rendered.contains("hint: a generic target has no low-asm contract"));
548 assert!(rendered.contains("12 later error(s) suppressed"));
549 assert!(!rendered.contains("expected expression"));
550 let single = summarize(&[diagnostic(2, 4, "type mismatch")]);
552 assert_eq!(single, "4:1: type mismatch");
553 }
554}
555
556struct ModuleData {
557 sources: Vec<Source>,
558 digest: String,
559 index: Mutex<Option<(Arc<native::Index>, Vec<Diagnostic>)>>,
560}
561#[derive(Clone)]
563pub struct Module {
564 data: Arc<ModuleData>,
566 compiler: Compiler,
567}
568#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
570#[serde(rename_all = "snake_case")]
571pub enum ReportMode {
572 #[default]
574 None,
575 Summary,
577 Details,
579}
580
581#[derive(Clone, Copy, Debug, Default)]
583pub enum TraceFormat {
584 Text,
586 #[default]
588 JsonLines,
589}
590#[derive(Clone, Debug)]
592pub struct TraceOptions {
593 pub format: TraceFormat,
595 pub max_bytes: usize,
597 pub before: Vec<String>,
599 pub after: Vec<String>,
602}
603impl Default for TraceOptions {
604 fn default() -> Self {
605 Self {
606 format: TraceFormat::JsonLines,
607 max_bytes: 16 * 1024 * 1024,
608 before: Vec::new(),
609 after: Vec::new(),
610 }
611 }
612}
613
614#[derive(Clone, Debug, Default)]
616pub struct Specialization {
617 symbol: String,
619 config: BTreeMap<String, String>,
621 report: ReportMode,
623}
624impl Specialization {
625 pub fn new(symbol: impl Into<String>) -> Self {
627 Self {
628 symbol: symbol.into(),
629 ..Self::default()
630 }
631 }
632 #[must_use]
634 pub fn with_config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
635 self.config.insert(key.into(), value.into());
636 self
637 }
638 pub fn set_config(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
640 self.config.insert(key.into(), value.into());
641 self
642 }
643 pub fn set_symbol(&mut self, symbol: impl Into<String>) -> &mut Self {
645 self.symbol = symbol.into();
646 self
647 }
648 pub fn replace_config(&mut self, config: BTreeMap<String, String>) -> &mut Self {
650 self.config = config;
651 self
652 }
653 #[must_use]
655 pub fn with_report(mut self, report: ReportMode) -> Self {
656 self.report = report;
657 self
658 }
659 pub fn set_report(&mut self, report: ReportMode) -> &mut Self {
661 self.report = report;
662 self
663 }
664 #[must_use]
666 pub fn symbol(&self) -> &str {
667 &self.symbol
668 }
669 #[must_use]
671 pub fn configuration(&self) -> &BTreeMap<String, String> {
672 &self.config
673 }
674 #[must_use]
676 pub fn report_mode(&self) -> ReportMode {
677 self.report
678 }
679 fn validate(&self) -> Result<()> {
680 let valid = |s: &str| {
681 !s.is_empty()
682 && s.bytes()
683 .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
684 };
685 if !valid(&self.symbol) || self.config.keys().any(|k| !valid(k)) {
686 return Err(Error::Message(
687 "invalid Loom export or configuration key".into(),
688 ));
689 }
690 Ok(())
691 }
692}
693#[derive(Clone, Debug, Serialize, Deserialize)]
694struct Record {
695 key: String,
696 compiler: String,
697 target: String,
698 #[serde(default)]
699 processor_mode: ProcessorMode,
700 symbol: String,
701 sha256: String,
702 diagnostics: Vec<Diagnostic>,
703 report: Option<CompileReport>,
704 manifest: Option<serde_json::Value>,
705 launch_sha256: Option<String>,
706}
707#[derive(Clone, Debug)]
709pub struct Artifact {
710 bytes: Arc<[u8]>,
711 launch_bytes: Option<Arc<[u8]>>,
712 launch_loader: native::LaunchLoader,
713 path: PathBuf,
714 record: Record,
715}
716impl Artifact {
717 pub fn bytes(&self) -> &[u8] {
719 &self.bytes
720 }
721 pub fn manifest(&self) -> Option<&serde_json::Value> {
723 self.record.manifest.as_ref()
724 }
725 pub fn launch_bytes(&self) -> Option<&[u8]> {
727 self.launch_bytes.as_deref()
728 }
729 pub fn launch_program(&self, export: &str) -> Result<LaunchProgram> {
732 self.launch_loader.load(
733 self.launch_bytes.as_deref().ok_or_else(|| {
734 Error::Unsupported("this target has no host launch program".into())
735 })?,
736 export,
737 )
738 }
739 pub fn path(&self) -> &Path {
741 &self.path
742 }
743 pub fn compiler_identity(&self) -> &str {
745 &self.record.compiler
746 }
747 pub fn target(&self) -> &str {
749 &self.record.target
750 }
751 pub fn processor_mode(&self) -> ProcessorMode {
753 self.record.processor_mode
754 }
755 pub fn symbol(&self) -> &str {
757 &self.record.symbol
758 }
759 pub fn diagnostics(&self) -> &[Diagnostic] {
761 &self.record.diagnostics
762 }
763 pub fn report(&self) -> Option<&CompileReport> {
765 self.record.report.as_ref()
766 }
767}
768#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
770pub struct LaunchConfig {
771 pub workgroup_count: [u32; 3],
773 pub workgroup_size: [u32; 3],
775 pub workgroup_cluster_size: [u32; 3],
777 pub subgroup_size: u32,
779 pub workgroup_storage_bytes: u64,
781}
782
783pub struct LaunchProgram(native::LaunchProgram);
786impl LaunchProgram {
787 pub fn evaluate(&mut self, workload_argument_bits: &[u64]) -> Result<LaunchConfig> {
790 self.0.evaluate(workload_argument_bits)
791 }
792}
793
794struct Compiled {
795 bytes: Arc<[u8]>,
796 diagnostics: Vec<Diagnostic>,
797 report: Option<serde_json::Value>,
798 manifest: Option<serde_json::Value>,
799 launch_bytes: Option<Arc<[u8]>>,
800}
801impl Module {
802 pub fn identity(&self) -> &str {
804 &self.data.digest
805 }
806 pub fn key(&self, spec: &Specialization) -> Result<String> {
808 spec.validate()?;
809 let base = bundle::digest(&serde_json::to_vec(&(
810 "loomc-v5-sanitizers",
811 self.compiler.0.sanitizer,
812 self.compiler.identity(),
813 self.identity(),
814 &spec.symbol,
815 self.compiler.0.target.as_str(),
816 &spec.config,
817 spec.report,
818 ))?);
819 if self.compiler.0.processor_mode == ProcessorMode::Default {
822 Ok(base)
823 } else {
824 Ok(bundle::digest(&serde_json::to_vec(&(
825 "loomc-processor-mode-v1",
826 base,
827 self.compiler.0.processor_mode,
828 ))?))
829 }
830 }
831 pub fn compile(&self, spec: &Specialization) -> Result<Artifact> {
834 self.compile_impl(spec, None)
835 }
836 pub fn compile_traced(
840 &self,
841 spec: &Specialization,
842 options: &TraceOptions,
843 writer: &mut dyn std::io::Write,
844 ) -> Result<Artifact> {
845 self.compile_impl(spec, Some((options, writer)))
846 }
847 fn compile_impl(
848 &self,
849 spec: &Specialization,
850 trace: Option<(&TraceOptions, &mut dyn std::io::Write)>,
851 ) -> Result<Artifact> {
852 let key = self.key(spec)?;
853 let cache = &bundle::kernel_cache()?;
854 let dir = cache.join(&key);
855 if trace.is_none()
856 && let Some(a) = cached(&dir, &key, &self.compiler.0.native)
857 {
858 return Ok(a);
859 }
860 bundle::create_cache_dir(cache)?;
861 let _lock = Lock::acquire(&cache.join(format!("{key}.lock")))?;
862 if trace.is_none()
863 && let Some(a) = cached(&dir, &key, &self.compiler.0.native)
864 {
865 return Ok(a);
866 }
867 let (index, mut diagnostics) = {
868 let mut slot = self.data.index.lock().unwrap_or_else(|e| e.into_inner());
869 if slot.is_none() {
870 let (index, diagnostics) = self.compiler.0.native.index(&self.data.sources)?;
871 *slot = Some((Arc::new(index), diagnostics));
872 }
873 slot.as_ref().unwrap().clone()
874 };
875 let compiled = self
876 .compiler
877 .0
878 .native
879 .compile(&index, spec, trace)
880 .map_err(|e| {
881 e.context(format!(
882 "compiling {} with {}",
883 spec.symbol,
884 self.compiler.library_path().display()
885 ))
886 })?;
887 diagnostics.extend(compiled.diagnostics);
888 let record = Record {
889 key,
890 compiler: self.compiler.identity().into(),
891 target: self.compiler.0.target.as_str().into(),
892 processor_mode: self.compiler.0.processor_mode,
893 symbol: spec.symbol.clone(),
894 sha256: bundle::digest(&compiled.bytes),
895 launch_sha256: compiled.launch_bytes.as_deref().map(bundle::digest),
896 manifest: compiled.manifest,
897 diagnostics,
898 report: compiled
899 .report
900 .map(|json| {
901 CompileReport::new(
902 self.compiler.identity().into(),
903 self.compiler.0.target.as_str().into(),
904 self.compiler.0.processor_mode,
905 json,
906 )
907 })
908 .transpose()?,
909 };
910 let staging = tempfile::tempdir_in(cache)?;
911 let filename = self.compiler.target().artifact_filename();
912 let output = staging.path().join(filename);
913 fs::write(&output, &compiled.bytes)?;
914 if let Some(bytes) = &compiled.launch_bytes {
915 fs::write(staging.path().join("launch.loombc"), bytes)?;
916 fs::File::open(staging.path().join("launch.loombc"))?.sync_all()?;
917 }
918 let metadata = serde_json::to_vec(&record)?;
919 fs::write(staging.path().join("artifact.json"), &metadata)?;
920 fs::write(
921 staging.path().join("artifact.sha256"),
922 bundle::digest(&metadata),
923 )?;
924 for file in [filename, "artifact.json", "artifact.sha256"] {
925 fs::File::open(staging.path().join(file))?.sync_all()?;
926 }
927 fs::File::open(staging.path())?.sync_all()?;
928 if dir.exists() {
929 fs::remove_dir_all(&dir)?;
930 }
931 bundle::publish(staging, &dir)?;
932 fs::File::open(cache)?.sync_all()?;
933 Ok(Artifact {
934 bytes: compiled.bytes,
935 launch_bytes: compiled.launch_bytes,
936 launch_loader: self.compiler.0.native.launch_loader(),
937 path: dir.join(filename),
938 record,
939 })
940 }
941}
942fn cached(dir: &Path, key: &str, native: &native::Prepared) -> Option<Artifact> {
943 let metadata = fs::read(dir.join("artifact.json")).ok()?;
944 if fs::read_to_string(dir.join("artifact.sha256")).ok()? != bundle::digest(&metadata) {
945 return None;
946 }
947 let record: Record = serde_json::from_slice(&metadata).ok()?;
948 if record.key != key {
949 return None;
950 }
951 if let Ok(file) = fs::File::open(dir.join("artifact.json")) {
956 let _ = file.set_times(fs::FileTimes::new().set_accessed(std::time::SystemTime::now()));
957 }
958 let target = crate::Target::new(&record.target).ok()?;
959 let path = dir.join(target.artifact_filename());
960 let bytes = fs::read(&path).ok()?;
961 if bytes.is_empty() || bundle::digest(&bytes) != record.sha256 {
962 return None;
963 }
964 let launch_bytes = if let Some(hash) = &record.launch_sha256 {
965 let bytes = fs::read(dir.join("launch.loombc")).ok()?;
966 if bytes.is_empty() || &bundle::digest(&bytes) != hash {
967 return None;
968 }
969 Some(Arc::from(bytes))
970 } else {
971 None
972 };
973 Some(Artifact {
974 bytes: bytes.into(),
975 launch_bytes,
976 launch_loader: native.launch_loader(),
977 path,
978 record,
979 })
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 #[test]
987 #[ignore = "requires libloomc.so, no GPU"]
988 fn shared_compilers_respect_all_limits_and_reuse_matching_options() -> Result<()> {
989 let options = CompilerOptions {
990 workers: NonZeroUsize::new(2).unwrap(),
991 module_cache_capacity: 2,
992 ..Default::default()
993 };
994 let first = Compiler::shared(None, options.clone())?;
995 let same = Compiler::shared(None, options.clone())?;
996 assert!(Arc::ptr_eq(&first.0, &same.0));
997 let serial_options = CompilerOptions {
998 workers: NonZeroUsize::new(1).unwrap(),
999 ..options
1000 };
1001 let serial = Compiler::shared(None, serial_options.clone())?;
1002 assert_eq!(serial.workers().get(), 1);
1003 assert!(!Arc::ptr_eq(&first.0, &serial.0));
1004 let uncached = Compiler::shared(
1005 None,
1006 CompilerOptions {
1007 module_cache_capacity: 0,
1008 ..serial_options
1009 },
1010 )?;
1011 assert!(!Arc::ptr_eq(&serial.0, &uncached.0));
1012 serial.module("// retained");
1013 uncached.module("// not retained");
1014 assert!(!serial.0.modules.lock().unwrap().is_empty());
1015 assert!(uncached.0.modules.lock().unwrap().is_empty());
1016 Ok(())
1017 }
1018
1019 #[test]
1020 #[ignore = "requires libloomc.so, no GPU"]
1021 fn processor_modes_have_distinct_compilers_and_artifacts() -> Result<()> {
1022 let default = Compiler::shared(None, CompilerOptions::default())?;
1023 let options = CompilerOptions {
1024 processor_mode: ProcessorMode::ComputeUnit,
1025 ..Default::default()
1026 };
1027 let cu = Compiler::shared(None, options.clone())?;
1028 let same = Compiler::shared(None, options)?;
1029 assert!(Arc::ptr_eq(&cu.0, &same.0));
1030 assert!(!Arc::ptr_eq(&default.0, &cu.0));
1031 let source = include_str!("../tests/kernels/euler.loom");
1032 let spec = euler_spec();
1033 let wgp = default.module(source).compile(&spec)?;
1034 let unit = cu.module(source).compile(&spec)?;
1035 assert_ne!(wgp.path(), unit.path());
1036 assert_eq!(unit.processor_mode(), ProcessorMode::ComputeUnit);
1037 let cached = cu.module(source).compile(&spec)?;
1038 assert_eq!(cached.processor_mode(), ProcessorMode::ComputeUnit);
1039 assert_eq!(unit.bytes(), cached.bytes());
1040 let differences: Vec<_> = wgp
1041 .bytes()
1042 .iter()
1043 .zip(unit.bytes())
1044 .filter(|(a, b)| a != b)
1045 .collect();
1046 assert_eq!(wgp.bytes().len(), unit.bytes().len());
1047 assert_eq!(differences.len(), 1);
1048 assert_eq!(*differences[0].0 ^ *differences[0].1, 0x20);
1049 Ok(())
1050 }
1051
1052 fn euler_spec() -> Specialization {
1053 let mut spec = Specialization::new("krea2_euler");
1054 spec.set_config("krea2.euler.grid_x", "1");
1055 spec.set_config("krea2.euler.grid_y", "1");
1056 spec
1057 }
1058
1059 #[test]
1060 #[ignore = "requires libloomc.so, no GPU"]
1061 fn reporting_preserves_shared_requests_and_can_be_replaced_from_a_callback() -> Result<()> {
1062 use std::sync::atomic::{AtomicUsize, Ordering};
1063 let kernels = Kernels::new(Compiler::resolve(None)?);
1064 let clone = kernels.clone();
1065 let pending =
1066 kernels.request(include_str!("../tests/kernels/euler.loom"), &euler_spec())?;
1067 let calls = Arc::new(AtomicUsize::new(0));
1068 let reported = calls.clone();
1069 let weak = Arc::downgrade(&kernels.0);
1070 let kernels = kernels.reporting(move |_| {
1071 reported.fetch_add(1, Ordering::SeqCst);
1072 let next = reported.clone();
1075 Kernels(weak.upgrade().unwrap()).reporting(move |_| {
1076 next.fetch_add(10, Ordering::SeqCst);
1077 });
1078 });
1079 assert!(Arc::ptr_eq(&kernels.0, &clone.0));
1080 assert!(Arc::ptr_eq(&kernels.0, &pending.cache));
1081 for cache in [&clone.0, &pending.cache] {
1082 let mut attempts = 0;
1083 let outcome = cache.build_with(1, |artifact| {
1084 attempts += 1;
1085 assert_eq!(artifact.record.key, pending.key);
1086 Err(Error::Message("injected load failure".into()))
1087 });
1088 assert_eq!(outcome.unwrap_err().to_string(), "injected load failure");
1089 assert_eq!(attempts, 1);
1090 }
1091 assert_eq!(calls.load(Ordering::SeqCst), 11);
1092 assert_eq!(kernels.0.locked()?.waiting.len(), 1);
1093 Ok(())
1094 }
1095
1096 #[test]
1097 #[ignore = "requires libloomc.so, no GPU"]
1098 fn requests_during_build_are_deduplicated_and_new_requests_survive() -> Result<()> {
1099 let kernels = Kernels::new(Compiler::resolve(None)?);
1100 let source = include_str!("../tests/kernels/euler.loom");
1101 let spec = euler_spec();
1102 let pending = kernels.request(source, &spec)?;
1103 let mut other_spec = spec.clone();
1104 other_spec.set_config("krea2.euler.grid_x", "2");
1105 let other_key = kernels.compiler().module(source).key(&other_spec)?;
1106 let outcome = kernels.0.build_with(1, |_| {
1107 std::thread::scope(|scope| {
1110 scope
1111 .spawn(|| {
1112 for _ in 0..2 {
1113 let duplicate = kernels.request(source, &spec).unwrap();
1114 assert_eq!(duplicate.key, pending.key);
1115 kernels.request(source, &other_spec).unwrap();
1116 }
1117 })
1118 .join()
1119 .unwrap();
1120 });
1121 assert_eq!(kernels.0.locked().unwrap().waiting.len(), 1);
1122 Err(Error::Message("injected load failure".into()))
1123 });
1124 assert_eq!(outcome.unwrap_err().to_string(), "injected load failure");
1125 {
1126 let state = kernels.0.locked()?;
1127 let waiting: Vec<_> = state.waiting.iter().map(|r| r.0.as_str()).collect();
1128 assert_eq!(waiting, vec![pending.key.as_str(), other_key.as_str()]);
1129 assert!(state.active.is_empty());
1130 }
1131 let mut attempts = Vec::new();
1132 let outcome = kernels.0.build_with(1, |artifact| {
1133 attempts.push(artifact.record.key.clone());
1134 Err(Error::Message("injected retry failure".into()))
1135 });
1136 assert!(outcome.is_err());
1137 assert_eq!(attempts, vec![pending.key, other_key]);
1138 assert_eq!(kernels.0.locked()?.waiting.len(), 2);
1139 Ok(())
1140 }
1141
1142 #[test]
1143 #[ignore = "requires libloomc.so, no GPU"]
1144 fn compile_and_load_failures_remain_requested_for_retry() -> Result<()> {
1145 let kernels = Kernels::new(Compiler::resolve(None)?);
1146 let source = include_str!("../tests/kernels/euler.loom");
1147 let bad = kernels.request(source, &Specialization::new("missing_export"))?;
1148 let good = kernels.request(source, &euler_spec())?;
1149 for _ in 0..2 {
1150 let mut attempted = Vec::new();
1151 let result = kernels.0.build_with(1, |artifact| {
1152 attempted.push(artifact.record.key.clone());
1153 Err(Error::Message("injected transient load failure".into()))
1154 });
1155 let error = result.unwrap_err().to_string();
1156 assert!(
1157 !error.contains("injected transient load failure"),
1158 "the first compile failure must be returned: {error}"
1159 );
1160 assert_eq!(
1161 attempted,
1162 vec![good.key.clone()],
1163 "the later request was attempted"
1164 );
1165 let state = kernels.0.locked()?;
1166 let waiting: Vec<_> = state.waiting.iter().map(|r| r.0.as_str()).collect();
1167 assert_eq!(waiting, vec![bad.key.as_str(), good.key.as_str()]);
1168 }
1169 assert!(good.built().is_none());
1170 Ok(())
1171 }
1172
1173 #[test]
1174 #[ignore = "requires libloomc.so, no GPU"]
1175 fn concurrent_build_waits_for_the_active_batch_and_retries_its_failure() -> Result<()> {
1176 use std::sync::mpsc;
1177 use std::time::Duration;
1178 let kernels = Kernels::new(Compiler::resolve(None)?);
1179 let pending =
1180 kernels.request(include_str!("../tests/kernels/euler.loom"), &euler_spec())?;
1181 let (entered_tx, entered_rx) = mpsc::channel();
1182 let (release_tx, release_rx) = mpsc::channel();
1183 let (started_tx, started_rx) = mpsc::channel();
1184 let (done_tx, done_rx) = mpsc::channel();
1185 std::thread::scope(|scope| {
1186 let first_cache = &kernels.0;
1187 let first = scope.spawn(move || {
1188 first_cache.build_with(1, |_| {
1189 entered_tx.send(()).unwrap();
1190 release_rx.recv_timeout(Duration::from_secs(10)).unwrap();
1191 Err(Error::Message("first load failed".into()))
1192 })
1193 });
1194 entered_rx.recv_timeout(Duration::from_secs(10)).unwrap();
1195 assert!(
1196 kernels.0.locked().unwrap().waiting.is_empty(),
1197 "batch has drained"
1198 );
1199 let second = scope.spawn(|| {
1200 started_tx.send(()).unwrap();
1201 let result = kernels
1202 .0
1203 .build_with(1, |_| Err(Error::Message("retry failed".into())));
1204 done_tx.send(result.map_err(|e| e.to_string())).unwrap();
1205 });
1206 started_rx.recv_timeout(Duration::from_secs(10)).unwrap();
1207 let early = done_rx.recv_timeout(Duration::from_millis(100));
1208 release_tx.send(()).unwrap();
1209 assert!(first.join().unwrap().is_err());
1210 second.join().unwrap();
1211 assert!(
1212 matches!(early, Err(mpsc::RecvTimeoutError::Timeout)),
1213 "build returned before the active batch finished"
1214 );
1215 assert_eq!(done_rx.recv().unwrap(), Err("retry failed".into()));
1216 });
1217 assert!(pending.built().is_none());
1218 assert_eq!(kernels.0.locked()?.waiting.len(), 1);
1219 Ok(())
1220 }
1221
1222 #[test]
1223 #[ignore = "requires libloomc.so, no GPU"]
1224 fn module_cache_eviction_preserves_live_modules() -> Result<()> {
1225 let compiler = Compiler::with_options(
1226 None,
1227 CompilerOptions {
1228 module_cache_capacity: 2,
1229 ..Default::default()
1230 },
1231 )?;
1232 let first = compiler.module(include_str!("../tests/kernels/euler.loom"));
1233 let first_id = first.identity().to_owned();
1234 for i in 0..8 {
1235 compiler.module(&format!("// source {i}"));
1236 assert!(compiler.0.modules.lock().unwrap().len() <= 2);
1237 }
1238 compiler.trim();
1239 assert!(compiler.0.modules.lock().unwrap().is_empty());
1240 assert_eq!(first.identity(), first_id);
1241 let mut spec = Specialization::new("krea2_euler");
1242 spec.set_config("krea2.euler.grid_x", "1");
1243 spec.set_config("krea2.euler.grid_y", "1");
1244 assert!(!first.compile(&spec)?.bytes().is_empty());
1245 Ok(())
1246 }
1247}
1248
1249#[derive(Clone)]
1263pub struct Kernels(Arc<Cache>);
1264
1265struct Cache {
1266 compiler: Compiler,
1267 state: Mutex<Loaded>,
1268 build: Mutex<()>,
1270 report: Mutex<Option<Reporter>>,
1271}
1272
1273type Reporter = Arc<dyn Fn(&Artifact) + Send + Sync>;
1275
1276#[derive(Default)]
1277struct Loaded {
1278 device: Option<usize>,
1280 ready: HashMap<String, crate::Kernel>,
1281 waiting: Vec<(String, String, Specialization)>,
1283 active: HashSet<String>,
1285}
1286
1287pub struct Pending {
1295 key: String,
1296 cache: Arc<Cache>,
1297 cell: std::sync::OnceLock<crate::Kernel>,
1301}
1302
1303impl Clone for Pending {
1304 fn clone(&self) -> Self {
1305 Self {
1306 key: self.key.clone(),
1307 cache: self.cache.clone(),
1308 cell: self.cell.clone(),
1309 }
1310 }
1311}
1312
1313impl Pending {
1314 pub fn built(&self) -> Option<&crate::Kernel> {
1320 if self.cell.get().is_none() {
1321 let found = self.cache.locked().ok()?.ready.get(&self.key).cloned()?;
1322 let _ = self.cell.set(found);
1323 }
1324 self.cell.get()
1325 }
1326
1327 pub unsafe fn resolve(&self, stream: &crate::Stream) -> Result<&crate::Kernel> {
1333 if self.built().is_some() {
1334 return Ok(self.cell.get().expect("just built"));
1335 }
1336 let outcome = unsafe { self.cache.build(stream) };
1341 match self.built() {
1342 Some(_) => Ok(self.cell.get().expect("just built")),
1343 None => Err(outcome
1344 .err()
1345 .unwrap_or_else(|| Error::Message("kernel was never requested".into()))),
1346 }
1347 }
1348}
1349
1350impl Kernels {
1351 pub fn keyed<K>(self) -> KeyedKernels<K> {
1358 KeyedKernels {
1359 kernels: self,
1360 keys: Mutex::new(HashMap::new()),
1361 }
1362 }
1363
1364 pub fn new(compiler: Compiler) -> Self {
1366 Self(Arc::new(Cache {
1367 compiler,
1368 state: Mutex::new(Loaded::default()),
1369 build: Mutex::new(()),
1370 report: Mutex::new(None),
1371 }))
1372 }
1373
1374 pub fn reporting(self, report: impl Fn(&Artifact) + Send + Sync + 'static) -> Self {
1384 let previous = self
1385 .0
1386 .report
1387 .lock()
1388 .unwrap_or_else(|poisoned| poisoned.into_inner())
1389 .replace(Arc::new(report));
1390 drop(previous);
1392 self
1393 }
1394
1395 pub fn compiler(&self) -> &Compiler {
1397 &self.0.compiler
1398 }
1399
1400 pub fn len(&self) -> usize {
1402 self.0.locked().map_or(0, |state| state.ready.len())
1403 }
1404
1405 pub fn is_empty(&self) -> bool {
1407 self.len() == 0
1408 }
1409
1410 pub unsafe fn get(
1417 &self,
1418 stream: &crate::Stream,
1419 source: &str,
1420 spec: &Specialization,
1421 ) -> Result<crate::Kernel> {
1422 let module = self.0.compiler.module(source);
1423 let key = module.key(spec)?;
1424 unsafe { self.get_module(stream, &module, key, spec) }
1426 }
1427
1428 unsafe fn get_module(
1429 &self,
1430 stream: &crate::Stream,
1431 module: &Module,
1432 key: String,
1433 spec: &Specialization,
1434 ) -> Result<crate::Kernel> {
1435 {
1436 let mut state = self.0.locked()?;
1437 state.claim(stream)?;
1438 if let Some(kernel) = state.ready.get(&key) {
1439 return Ok(kernel.clone());
1440 }
1441 }
1442 let _build = self
1445 .0
1446 .build
1447 .lock()
1448 .map_err(|_| Error::Message("kernel build poisoned".into()))?;
1449 if let Some(kernel) = self.0.locked()?.ready.get(&key) {
1450 return Ok(kernel.clone());
1451 }
1452 let artifact = module.compile(spec)?;
1453 self.0.reported(&artifact);
1454 let kernel = unsafe { stream.load_artifact(&artifact) }?;
1456 let mut state = self.0.locked()?;
1457 state.waiting.retain(|(waiting, ..)| *waiting != key);
1458 state.ready.insert(key, kernel.clone());
1459 Ok(kernel)
1460 }
1461
1462 pub fn request(&self, source: &str, spec: &Specialization) -> Result<Pending> {
1467 let key = self.0.compiler.module(source).key(spec)?;
1468 let mut state = self.0.locked()?;
1469 let known = state.ready.contains_key(&key)
1470 || state.active.contains(&key)
1471 || state.waiting.iter().any(|(waiting, ..)| *waiting == key);
1472 if !known {
1473 state
1474 .waiting
1475 .push((key.clone(), source.to_owned(), spec.clone()));
1476 }
1477 drop(state);
1478 Ok(Pending {
1479 key,
1480 cache: self.0.clone(),
1481 cell: std::sync::OnceLock::new(),
1482 })
1483 }
1484
1485 pub unsafe fn build(&self, stream: &crate::Stream) -> Result<()> {
1491 unsafe { self.0.build(stream) }
1493 }
1494}
1495
1496impl Cache {
1497 fn locked(&self) -> Result<std::sync::MutexGuard<'_, Loaded>> {
1498 self.state
1499 .lock()
1500 .map_err(|_| Error::Message("kernel cache poisoned".into()))
1501 }
1502
1503 fn reported(&self, artifact: &Artifact) {
1504 let report = self
1505 .report
1506 .lock()
1507 .unwrap_or_else(|poisoned| poisoned.into_inner())
1508 .clone();
1509 if let Some(report) = report {
1511 report(artifact);
1512 }
1513 }
1514
1515 unsafe fn build(&self, stream: &crate::Stream) -> Result<()> {
1523 self.build_with(stream.device_id(), |artifact| {
1524 unsafe { stream.load_artifact(artifact) }
1526 })
1527 }
1528
1529 fn build_with(
1530 &self,
1531 device: usize,
1532 mut load: impl FnMut(&Artifact) -> Result<crate::Kernel>,
1533 ) -> Result<()> {
1534 let _build = self
1535 .build
1536 .lock()
1537 .map_err(|_| Error::Message("kernel build poisoned".into()))?;
1538 let waiting = {
1539 let mut state = self.locked()?;
1540 state.claim_device(device)?;
1541 let waiting = std::mem::take(&mut state.waiting);
1542 state
1543 .active
1544 .extend(waiting.iter().map(|(key, ..)| key.clone()));
1545 waiting
1546 };
1547 if waiting.is_empty() {
1548 return Ok(());
1549 }
1550 let modules: Vec<Module> = waiting
1551 .iter()
1552 .map(|(_, source, _)| self.compiler.module(source))
1553 .collect();
1554 let requests: Vec<(&Module, &Specialization)> = modules
1555 .iter()
1556 .zip(waiting.iter().map(|(_, _, spec)| spec))
1557 .collect();
1558 let built = self.compiler.compile_all(&requests);
1559
1560 let mut failure = None;
1561 let mut unbuilt = Vec::new();
1562 for (request, outcome) in waiting.into_iter().zip(built) {
1563 let loaded = outcome.and_then(|artifact| {
1564 self.reported(&artifact);
1565 load(&artifact)
1566 });
1567 match loaded {
1568 Ok(kernel) => {
1569 self.locked()?.ready.insert(request.0.clone(), kernel);
1570 }
1571 Err(error) => {
1572 unbuilt.push(request);
1573 failure = failure.or(Some(error));
1574 }
1575 }
1576 }
1577 {
1578 let mut state = self.locked()?;
1579 unbuilt.append(&mut state.waiting);
1580 state.waiting = unbuilt;
1581 state.active.clear();
1582 }
1583 match failure {
1584 Some(error) => Err(error),
1585 None => Ok(()),
1586 }
1587 }
1588}
1589
1590pub struct KeyedKernels<K> {
1597 kernels: Kernels,
1598 keys: Mutex<HashMap<K, String>>,
1599}
1600
1601impl<K: Eq + std::hash::Hash> KeyedKernels<K> {
1602 pub fn kernels(&self) -> &Kernels {
1604 &self.kernels
1605 }
1606
1607 pub unsafe fn request_or_insert_with<S: AsRef<str>>(
1615 &self,
1616 stream: &crate::Stream,
1617 key: K,
1618 request: impl FnOnce(&K) -> Result<(S, Specialization)>,
1619 ) -> Result<Pending> {
1620 let mut keys = self
1621 .keys
1622 .lock()
1623 .map_err(|_| Error::Message("kernel key index poisoned".into()))?;
1624 self.kernels.0.locked()?.claim(stream)?;
1625 if let Some(artifact) = keys.get(&key) {
1626 return Ok(Pending {
1627 key: artifact.clone(),
1628 cache: self.kernels.0.clone(),
1629 cell: std::sync::OnceLock::new(),
1630 });
1631 }
1632 let (source, spec) = request(&key)?;
1633 let pending = self.kernels.request(source.as_ref(), &spec)?;
1634 keys.insert(key, pending.key.clone());
1635 Ok(pending)
1636 }
1637
1638 pub unsafe fn get_or_insert_with<'s>(
1649 &self,
1650 stream: &crate::Stream,
1651 key: K,
1652 request: impl FnOnce(&K) -> Result<(&'s str, Specialization)>,
1653 ) -> Result<crate::Kernel> {
1654 let mut keys = self
1655 .keys
1656 .lock()
1657 .map_err(|_| Error::Message("kernel key index poisoned".into()))?;
1658 {
1659 let mut state = self.kernels.0.locked()?;
1660 state.claim(stream)?;
1661 if let Some(artifact) = keys.get(&key) {
1662 if let Some(kernel) = state.ready.get(artifact) {
1663 return Ok(kernel.clone());
1664 }
1665 let pending = Pending {
1666 key: artifact.clone(),
1667 cache: self.kernels.0.clone(),
1668 cell: std::sync::OnceLock::new(),
1669 };
1670 drop(state);
1671 drop(keys);
1672 return unsafe { pending.resolve(stream) }.cloned();
1674 }
1675 }
1676 let (source, spec) = request(&key)?;
1677 let module = self.kernels.compiler().module(source);
1678 let artifact = module.key(&spec)?;
1679 let kernel = unsafe {
1681 self.kernels
1682 .get_module(stream, &module, artifact.clone(), &spec)
1683 }?;
1684 keys.insert(key, artifact);
1685 Ok(kernel)
1686 }
1687}
1688
1689impl Loaded {
1690 fn claim(&mut self, stream: &crate::Stream) -> Result<()> {
1692 self.claim_device(stream.device_id())
1693 }
1694
1695 fn claim_device(&mut self, device: usize) -> Result<()> {
1696 match self.device {
1697 Some(owner) if owner != device => Err(Error::Message(
1698 "a kernel cache serves one device: an executable loaded on another is not \
1699 dispatchable here"
1700 .into(),
1701 )),
1702 Some(_) => Ok(()),
1703 None => {
1704 self.device = Some(device);
1705 Ok(())
1706 }
1707 }
1708 }
1709}
1710
1711impl std::fmt::Debug for Kernels {
1712 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1713 f.debug_struct("Kernels")
1714 .field("loaded", &self.len())
1715 .finish_non_exhaustive()
1716 }
1717}