1use std::fmt;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum Backend {
37 Cuda,
39 Cpu,
41}
42
43impl Backend {
44 pub const fn code(self) -> &'static str {
46 match self {
47 Self::Cuda => "cuda",
48 Self::Cpu => "cpu",
49 }
50 }
51}
52
53impl fmt::Display for Backend {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.write_str(self.code())
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum ExecutionPolicy {
62 PreferGpu,
64 RequireGpu,
66}
67
68impl ExecutionPolicy {
69 pub const fn code(self) -> &'static str {
71 match self {
72 Self::PreferGpu => "prefer_gpu",
73 Self::RequireGpu => "require_gpu",
74 }
75 }
76}
77
78impl fmt::Display for ExecutionPolicy {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.write_str(self.code())
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86#[non_exhaustive]
87pub enum FallbackReason {
88 CudaFeatureNotBuilt,
90 DriverRuntimeFailure,
92 DeviceUnavailable,
94 UnsupportedHardware,
96 KernelSpecializationUnavailable,
98 InvalidInput,
100}
101
102impl FallbackReason {
103 pub const fn code(self) -> &'static str {
105 match self {
106 Self::CudaFeatureNotBuilt => "cuda_feature_not_built",
107 Self::DriverRuntimeFailure => "driver_runtime_failure",
108 Self::DeviceUnavailable => "device_unavailable",
109 Self::UnsupportedHardware => "unsupported_hardware",
110 Self::KernelSpecializationUnavailable => "kernel_specialization_unavailable",
111 Self::InvalidInput => "invalid_input",
112 }
113 }
114}
115
116impl fmt::Display for FallbackReason {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.write_str(self.code())
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
124pub struct ComputeCapability {
125 pub major: u32,
126 pub minor: u32,
127}
128
129pub const REQUIRED_COMPUTE_CAPABILITY: ComputeCapability =
134 include!(concat!(env!("OUT_DIR"), "/compiled_cuda_capability.rs"));
135
136impl ComputeCapability {
137 pub const REQUIRED: Self = REQUIRED_COMPUTE_CAPABILITY;
139
140 pub const fn meets_minimum(self) -> bool {
145 (self.major as u64) * 10 + self.minor as u64
146 >= (Self::REQUIRED.major as u64) * 10 + Self::REQUIRED.minor as u64
147 }
148}
149
150impl fmt::Display for ComputeCapability {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 write!(f, "{}.{}", self.major, self.minor)
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
158pub struct KernelAvailability {
159 pub compiled: bool,
161 pub spiking_network: Option<bool>,
163 pub vector_similarity: Option<bool>,
165 pub satsolver: Option<bool>,
167 pub ternary_gemm: Option<bool>,
169}
170
171impl KernelAvailability {
172 pub const fn not_compiled() -> Self {
174 Self {
175 compiled: false,
176 spiking_network: None,
177 vector_similarity: None,
178 satsolver: None,
179 ternary_gemm: None,
180 }
181 }
182
183 pub const fn compiled_unverified() -> Self {
185 Self {
186 compiled: true,
187 spiking_network: None,
188 vector_similarity: None,
189 satsolver: None,
190 ternary_gemm: None,
191 }
192 }
193
194 pub const fn all_available() -> Self {
196 Self {
197 compiled: true,
198 spiking_network: Some(true),
199 vector_similarity: Some(true),
200 satsolver: Some(true),
201 ternary_gemm: Some(true),
202 }
203 }
204
205 pub const fn all_unavailable() -> Self {
207 Self {
208 compiled: true,
209 spiking_network: Some(false),
210 vector_similarity: Some(false),
211 satsolver: Some(false),
212 ternary_gemm: Some(false),
213 }
214 }
215
216 pub const fn any_runtime_unavailable(self) -> bool {
218 matches!(self.spiking_network, Some(false))
219 || matches!(self.vector_similarity, Some(false))
220 || matches!(self.satsolver, Some(false))
221 || matches!(self.ternary_gemm, Some(false))
222 }
223
224 pub const fn all_runtime_available(self) -> bool {
226 matches!(self.spiking_network, Some(true))
227 && matches!(self.vector_similarity, Some(true))
228 && matches!(self.satsolver, Some(true))
229 && matches!(self.ternary_gemm, Some(true))
230 }
231}
232
233impl Default for KernelAvailability {
234 fn default() -> Self {
235 Self::not_compiled()
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Hash)]
243pub struct CapabilityFacts {
244 pub cuda_built: bool,
245 pub runtime_available: bool,
246 pub device_available: bool,
247 pub compute_capability: Option<ComputeCapability>,
248 pub kernels: KernelAvailability,
249}
250
251impl CapabilityFacts {
252 pub const fn not_built() -> Self {
254 Self {
255 cuda_built: false,
256 runtime_available: false,
257 device_available: false,
258 compute_capability: None,
259 kernels: KernelAvailability::not_compiled(),
260 }
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct FallbackRecord {
267 pub reason: FallbackReason,
268 pub selected_backend: Backend,
269 pub detail: String,
271}
272
273impl FallbackRecord {
274 pub fn cpu(reason: FallbackReason, detail: impl Into<String>) -> Self {
276 Self {
277 reason,
278 selected_backend: Backend::Cpu,
279 detail: sanitize_diagnostic(&detail.into()),
280 }
281 }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct CapabilityReport {
287 pub cuda_built: bool,
288 pub runtime_available: bool,
289 pub device_available: bool,
290 pub compute_capability: Option<ComputeCapability>,
291 pub kernels: KernelAvailability,
292 pub selected_backend: Backend,
293 pub fallback: Option<FallbackRecord>,
294}
295
296impl CapabilityReport {
297 pub fn gpu_usable(&self) -> bool {
299 self.selected_backend == Backend::Cuda && self.fallback.is_none()
300 }
301
302 pub fn select_backend(&self, policy: ExecutionPolicy) -> Result<Backend, FallbackRecord> {
307 match policy {
308 ExecutionPolicy::PreferGpu => Ok(self.selected_backend),
309 ExecutionPolicy::RequireGpu => {
310 if self.gpu_usable() {
311 Ok(Backend::Cuda)
312 } else {
313 Err(self.fallback.clone().unwrap_or_else(|| {
314 FallbackRecord::cpu(
315 FallbackReason::DeviceUnavailable,
316 "GPU was required but is not usable",
317 )
318 }))
319 }
320 }
321 }
322 }
323}
324
325#[must_use]
334pub fn probe_capabilities() -> CapabilityReport {
335 #[cfg(feature = "cuda")]
336 let _restore_current_context = crate::gpu::context::CurrentContextGuard::capture();
337 crate::GpuAccelerator::new().capabilities().clone()
338}
339
340#[must_use]
342pub fn evaluate_capabilities(facts: &CapabilityFacts) -> CapabilityReport {
343 let (selected_backend, fallback) = select_from_facts(facts);
344 CapabilityReport {
345 cuda_built: facts.cuda_built,
346 runtime_available: facts.runtime_available,
347 device_available: facts.device_available,
348 compute_capability: facts.compute_capability,
349 kernels: facts.kernels,
350 selected_backend,
351 fallback,
352 }
353}
354
355pub(crate) fn apply_failure_to_facts(facts: &mut CapabilityFacts, reason: FallbackReason) {
360 match reason {
361 FallbackReason::CudaFeatureNotBuilt => {
362 facts.cuda_built = false;
363 facts.kernels = KernelAvailability::not_compiled();
364 }
365 FallbackReason::DriverRuntimeFailure => {
366 if !facts.kernels.all_runtime_available() {
367 facts.runtime_available = false;
368 }
369 }
370 FallbackReason::DeviceUnavailable => {
371 facts.device_available = false;
372 }
373 FallbackReason::UnsupportedHardware
374 | FallbackReason::KernelSpecializationUnavailable
375 | FallbackReason::InvalidInput => {}
376 }
377}
378
379fn select_from_facts(facts: &CapabilityFacts) -> (Backend, Option<FallbackRecord>) {
380 if !facts.cuda_built {
381 return cpu(
382 FallbackReason::CudaFeatureNotBuilt,
383 "crate built without the cuda feature",
384 );
385 }
386 if !facts.runtime_available {
387 return cpu(
388 FallbackReason::DriverRuntimeFailure,
389 "CUDA driver or runtime init failed",
390 );
391 }
392 if !facts.device_available {
393 return cpu(
394 FallbackReason::DeviceUnavailable,
395 "no CUDA device is accessible",
396 );
397 }
398 match facts.compute_capability {
399 None => {
400 return cpu(
401 FallbackReason::DriverRuntimeFailure,
402 "compute capability query failed",
403 );
404 }
405 Some(cc) if !cc.meets_minimum() => {
406 return cpu(
407 FallbackReason::UnsupportedHardware,
408 format!(
409 "compute capability {cc} is below required {}",
410 ComputeCapability::REQUIRED
411 ),
412 );
413 }
414 Some(_) => {}
415 }
416 if !facts.kernels.compiled {
417 return cpu(
418 FallbackReason::CudaFeatureNotBuilt,
419 "required PTX families were not compiled into this binary",
420 );
421 }
422 if !facts.kernels.all_runtime_available() {
423 return cpu(
424 FallbackReason::KernelSpecializationUnavailable,
425 "required kernel specialization is unavailable",
426 );
427 }
428 (Backend::Cuda, None)
429}
430
431fn cpu(reason: FallbackReason, detail: impl Into<String>) -> (Backend, Option<FallbackRecord>) {
432 (Backend::Cpu, Some(FallbackRecord::cpu(reason, detail)))
433}
434
435#[must_use]
437pub fn sanitize_diagnostic(input: &str) -> String {
438 if input.is_empty() {
439 return String::new();
440 }
441 let tokens = diagnostic_tokens(input);
442 let mut out = Vec::with_capacity(tokens.len());
443 let mut index = 0;
444 while index < tokens.len() {
445 if let Some((redacted, consumed)) = redact_authorization_sequence(&tokens[index..]) {
446 out.extend(redacted);
447 index += consumed;
448 continue;
449 }
450 if let Some((redacted, consumed)) = redact_secret_sequence(&tokens[index..]) {
451 out.extend(redacted);
452 index += consumed;
453 continue;
454 }
455 if json_secret_key_without_value(tokens[index]) && index + 1 < tokens.len() {
456 out.push(sanitize_token(tokens[index]));
457 out.push(redact_following_value_token(tokens[index + 1]));
458 index += 2;
459 continue;
460 }
461 out.push(sanitize_token(tokens[index]));
462 index += 1;
463 }
464 out.join(" ")
465}
466
467fn redact_secret_sequence(tokens: &[&str]) -> Option<(Vec<String>, usize)> {
468 let (_, core, suffix) = split_wrapping_punct(tokens[0]);
469 if core.contains('=')
470 || core
471 .split_once(':')
472 .is_some_and(|(_, value)| !value.is_empty())
473 || json_assignment_parts(core).is_some()
474 {
475 return None;
476 }
477 let key = core
478 .trim_end_matches(':')
479 .trim_matches(|ch| matches!(ch, '"' | '\''))
480 .trim_start_matches('-');
481 if key.is_empty()
482 || key.chars().any(char::is_whitespace)
483 || !is_secret_key(&key.to_ascii_lowercase())
484 {
485 return None;
486 }
487
488 let key_has_separator = core.ends_with(':') || suffix.contains(':');
489 if key_has_separator {
490 let value = tokens.get(1)?;
491 return Some((
492 vec![
493 sanitize_token(tokens[0]),
494 redact_following_value_token(value),
495 ],
496 2,
497 ));
498 }
499
500 if matches!(tokens.get(1).copied(), Some("=" | ":")) {
501 let value = tokens.get(2)?;
502 return Some((
503 vec![
504 sanitize_token(tokens[0]),
505 sanitize_token(tokens[1]),
506 redact_following_value_token(value),
507 ],
508 3,
509 ));
510 }
511
512 let value = tokens.get(1)?;
513 Some((
514 vec![
515 sanitize_token(tokens[0]),
516 redact_following_value_token(value),
517 ],
518 2,
519 ))
520}
521
522fn diagnostic_tokens(input: &str) -> Vec<&str> {
523 let mut tokens = Vec::new();
524 let mut start = None;
525 let mut quote = None;
526
527 for (index, ch) in input.char_indices() {
528 if start.is_none() {
529 if ch.is_whitespace() {
530 continue;
531 }
532 start = Some(index);
533 }
534
535 match quote {
536 Some(open) if ch == open => quote = None,
537 Some(_) => {}
538 None if matches!(ch, '"' | '\'')
539 && quote_opens_group(input, start.expect("token start is set"), index) =>
540 {
541 quote = Some(ch);
542 }
543 None if ch.is_whitespace() => {
544 if let Some(token_start) = start.take() {
545 tokens.push(&input[token_start..index]);
546 }
547 }
548 None => {}
549 }
550 }
551
552 if let Some(token_start) = start {
553 tokens.push(&input[token_start..]);
554 }
555 tokens
556}
557
558fn quote_opens_group(input: &str, token_start: usize, quote_index: usize) -> bool {
559 let before_quote = &input[token_start..quote_index];
560 before_quote.ends_with('=') || before_quote.chars().all(|ch| matches!(ch, '(' | '[' | '{'))
561}
562
563fn sanitize_token(tok: &str) -> String {
564 if tok.contains([',', ';']) {
565 let mut sanitized = String::with_capacity(tok.len());
566 let mut field_start = 0;
567 for (index, separator) in tok.match_indices([',', ';']) {
568 sanitized.push_str(&sanitize_token(&tok[field_start..index]));
569 sanitized.push_str(separator);
570 field_start = index + separator.len();
571 }
572 sanitized.push_str(&sanitize_token(&tok[field_start..]));
573 return sanitized;
574 }
575 let (prefix, core, suffix) = split_wrapping_punct(tok);
576 let lower = core.to_ascii_lowercase();
577 if is_user_path(&lower) {
578 return format!("{prefix}<path>{suffix}");
579 }
580 if let Some(redacted) = redact_authorization_core(core) {
581 return format!("{prefix}{redacted}{suffix}");
582 }
583 if let Some(redacted) = redact_json_assignment(core) {
584 return format!("{prefix}{redacted}{suffix}");
585 }
586 if let Some((key, value)) = lower.split_once('=') {
587 let orig_key = core.split_once('=').map(|(k, _)| k).unwrap_or(core);
588 let value_quote = value.chars().next().filter(|ch| matches!(ch, '"' | '\''));
589 let bare_value = value_quote
590 .and_then(|quote| value.strip_prefix(quote))
591 .unwrap_or(value);
592 let assignment_suffix = value_quote
593 .and_then(|quote| suffix.strip_prefix(quote))
594 .unwrap_or(suffix);
595 if is_secret_key(key.trim_start_matches('-')) {
596 return format!("{prefix}{orig_key}=<redacted>{assignment_suffix}");
597 }
598 if is_user_path(bare_value) {
599 return format!("{prefix}{orig_key}=<path>{assignment_suffix}");
600 }
601 }
602 if let Some((key, value)) = lower.split_once(':')
603 && !value.is_empty()
604 {
605 let orig_key = core.split_once(':').map(|(key, _)| key).unwrap_or(core);
606 let normalized_key = key.trim_start_matches('-');
607 if is_secret_key(normalized_key) {
608 return format!("{prefix}{orig_key}:<redacted>{suffix}");
609 }
610 if is_user_path(value) {
611 return format!("{prefix}{orig_key}:<path>{suffix}");
612 }
613 }
614 tok.to_string()
615}
616
617fn redact_authorization_sequence(tokens: &[&str]) -> Option<(Vec<String>, usize)> {
618 let (header_prefix, header_core, header_suffix) = split_wrapping_punct(tokens[0]);
619 if attached_authorization_scheme(header_core) {
620 if tokens.len() < 2 {
621 return None;
622 }
623 return Some((
624 vec![
625 format!("{header_prefix}{header_core}{header_suffix}"),
626 redact_following_value_token(tokens[1]),
627 ],
628 2,
629 ));
630 }
631 if !authorization_header_token(header_core, header_suffix) {
632 return None;
633 }
634 if redact_authorization_core(header_core).is_some() {
635 return None;
636 }
637 if tokens.len() < 2 || !is_http_auth_scheme_token(tokens[1]) {
638 return None;
639 }
640 let mut redacted = vec![
641 format!("{header_prefix}{header_core}{header_suffix}"),
642 sanitize_token(tokens[1]),
643 ];
644 if tokens.len() >= 3 && !json_secret_key_without_value(tokens[2]) {
645 let (_, scheme_core, _) = split_wrapping_punct(tokens[1]);
646 if scheme_core.split_whitespace().nth(1).is_none() {
647 redacted.push(redact_following_value_token(tokens[2]));
648 return Some((redacted, 3));
649 }
650 }
651 Some((redacted, 2))
652}
653
654fn attached_authorization_scheme(core: &str) -> bool {
655 core.to_ascii_lowercase()
656 .strip_prefix("authorization:")
657 .is_some_and(|rest| matches!(rest, "bearer" | "basic"))
658}
659
660fn authorization_header_token(core: &str, suffix: &str) -> bool {
661 let lower = core.to_ascii_lowercase();
662 if lower == "authorization" {
663 return suffix.starts_with(':');
664 }
665 lower
666 .strip_prefix("authorization:")
667 .map(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace))
668 .unwrap_or(false)
669}
670
671fn is_http_auth_scheme_token(tok: &str) -> bool {
672 let (_, core, _) = split_wrapping_punct(tok);
673 let scheme = core
674 .split(|ch: char| ch.is_whitespace() || matches!(ch, '"' | '\''))
675 .find(|part| !part.is_empty())
676 .unwrap_or(core);
677 scheme.eq_ignore_ascii_case("bearer") || scheme.eq_ignore_ascii_case("basic")
678}
679
680fn redact_authorization_core(core: &str) -> Option<String> {
681 let lower = core.to_ascii_lowercase();
682 const HEADER: &str = "authorization:";
683 let after_header = lower.strip_prefix(HEADER)?;
684 let trim_len = after_header.len() - after_header.trim_start().len();
685 let rest_lower = after_header.trim_start();
686 for scheme in ["bearer", "basic"] {
687 let Some(after_scheme) = rest_lower.strip_prefix(scheme) else {
688 continue;
689 };
690 if after_scheme.is_empty() {
691 return None;
692 }
693 let sep = after_scheme.chars().next()?;
694 if !(sep.is_whitespace() || matches!(sep, '"' | '\'')) {
695 continue;
696 }
697 let prefix_len = HEADER.len() + trim_len + scheme.len();
698 let orig_prefix = &core[..prefix_len];
699 if sep == '"' {
700 return Some(format!("{orig_prefix}\"<redacted>\""));
701 }
702 if sep == '\'' {
703 return Some(format!("{orig_prefix}'<redacted>'"));
704 }
705 return Some(format!("{orig_prefix} <redacted>"));
706 }
707 None
708}
709
710fn json_assignment_parts(core: &str) -> Option<(&str, &'static str, &str)> {
711 const SEPARATORS: [&str; 6] = [r#"":""#, "':'", r#"":'"#, r#"':""#, r#"":"#, "':"];
712 for sep in SEPARATORS {
713 if let Some((key, value)) = core.split_once(sep) {
714 if key.is_empty() || key.chars().any(char::is_whitespace) {
715 continue;
716 }
717 return Some((key, sep, value));
718 }
719 }
720 None
721}
722
723fn redact_json_assignment(core: &str) -> Option<String> {
724 let (key, sep, value) = json_assignment_parts(core)?;
725 let key_lower = key
726 .trim_matches(|ch| matches!(ch, '"' | '\''))
727 .to_ascii_lowercase();
728 let (bare_value, remainder) = split_json_value(value, sep);
729 let replacement = if is_secret_key(&key_lower) {
730 "<redacted>"
731 } else if is_user_path(&bare_value.to_ascii_lowercase()) {
732 "<path>"
733 } else {
734 return None;
735 };
736 Some(format!("{key}{sep}{replacement}{remainder}"))
737}
738
739fn split_json_value<'a>(value: &'a str, sep: &str) -> (&'a str, &'a str) {
740 if let Some(quote) = sep.chars().last().filter(|ch| matches!(ch, '"' | '\''))
741 && let Some(idx) = value.find(quote)
742 {
743 return (&value[..idx], &value[idx..]);
744 }
745 (value, "")
746}
747
748fn json_secret_key_without_value(tok: &str) -> bool {
749 let (_, core, suffix) = split_wrapping_punct(tok);
750 if json_assignment_parts(core).is_some() {
751 return false;
752 }
753 let had_colon = suffix.contains(':') || core.ends_with(':');
754 if !had_colon {
755 return false;
756 }
757 let key = core
758 .trim_end_matches(':')
759 .trim_matches(|ch| matches!(ch, '"' | '\''));
760 if key.is_empty() || key.chars().any(char::is_whitespace) {
761 return false;
762 }
763 is_secret_key(&key.to_ascii_lowercase())
764}
765
766fn redact_following_value_token(tok: &str) -> String {
767 let (prefix, core, suffix) = split_wrapping_punct(tok);
768 if is_user_path(&core.to_ascii_lowercase()) {
769 return format!("{prefix}<path>{suffix}");
770 }
771 format!("{prefix}<redacted>{suffix}")
772}
773
774fn split_wrapping_punct(tok: &str) -> (&str, &str, &str) {
775 let prefix_len: usize = tok
776 .chars()
777 .take_while(|c| matches!(c, '(' | '[' | '{' | '"' | '\''))
778 .map(char::len_utf8)
779 .sum();
780 let suffix_len: usize = tok
781 .chars()
782 .rev()
783 .take_while(|c| matches!(c, ')' | ']' | '}' | '"' | '\'' | ',' | ';' | '.' | ':'))
784 .map(char::len_utf8)
785 .sum();
786 if prefix_len + suffix_len >= tok.len() {
787 return ("", tok, "");
788 }
789 let core_end = tok.len() - suffix_len;
790 (
791 &tok[..prefix_len],
792 &tok[prefix_len..core_end],
793 &tok[core_end..],
794 )
795}
796
797fn is_user_path(lower: &str) -> bool {
798 lower.starts_with('/')
799 || lower.starts_with(r"\\")
800 || lower.contains("/.ssh/")
801 || lower.contains("/.aws/")
802 || (lower.len() >= 3
803 && lower.as_bytes()[1] == b':'
804 && (lower.as_bytes()[2] == b'\\' || lower.as_bytes()[2] == b'/'))
805}
806
807fn is_secret_key(key: &str) -> bool {
808 let key = key.replace('-', "_");
809 matches!(
810 key.as_str(),
811 "token"
812 | "password"
813 | "secret"
814 | "authorization"
815 | "api_key"
816 | "credential"
817 | "access_key"
818 | "secret_key"
819 ) || key.ends_with("token")
820 || key.ends_with("password")
821 || key.ends_with("secret")
822 || key.ends_with("api_key")
823 || key.ends_with("access_key")
824 || key.ends_with("secret_key")
825 || key.ends_with("credential")
826 || key.ends_with("authorization")
827 || key.contains("_secret_")
828 || key.contains("_access_key")
829 || key.contains("_api_key")
830 || key.contains("_secret_key")
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 struct Case {
838 name: &'static str,
839 facts: CapabilityFacts,
840 backend: Backend,
841 reason: Option<FallbackReason>,
842 }
843
844 fn facts(
845 cuda_built: bool,
846 runtime: bool,
847 device: bool,
848 cc: Option<ComputeCapability>,
849 kernels: KernelAvailability,
850 ) -> CapabilityFacts {
851 CapabilityFacts {
852 cuda_built,
853 runtime_available: runtime,
854 device_available: device,
855 compute_capability: cc,
856 kernels,
857 }
858 }
859
860 #[test]
861 fn decision_table_covers_build_runtime_device_kernel_outcomes() {
862 let sm89 = ComputeCapability { major: 8, minor: 9 };
863 let sm120 = ComputeCapability {
864 major: 12,
865 minor: 0,
866 };
867 let sm121 = ComputeCapability {
868 major: 12,
869 minor: 1,
870 };
871
872 let cases = [
873 Case {
874 name: "cpu-only build",
875 facts: CapabilityFacts::not_built(),
876 backend: Backend::Cpu,
877 reason: Some(FallbackReason::CudaFeatureNotBuilt),
878 },
879 Case {
880 name: "driver/runtime failure",
881 facts: facts(
882 true,
883 false,
884 false,
885 None,
886 KernelAvailability::compiled_unverified(),
887 ),
888 backend: Backend::Cpu,
889 reason: Some(FallbackReason::DriverRuntimeFailure),
890 },
891 Case {
892 name: "device unavailable",
893 facts: facts(
894 true,
895 true,
896 false,
897 None,
898 KernelAvailability::compiled_unverified(),
899 ),
900 backend: Backend::Cpu,
901 reason: Some(FallbackReason::DeviceUnavailable),
902 },
903 Case {
904 name: "CC query failed",
905 facts: facts(
906 true,
907 true,
908 true,
909 None,
910 KernelAvailability::compiled_unverified(),
911 ),
912 backend: Backend::Cpu,
913 reason: Some(FallbackReason::DriverRuntimeFailure),
914 },
915 Case {
916 name: "unsupported hardware",
917 facts: facts(
918 true,
919 true,
920 true,
921 Some(sm89),
922 KernelAvailability::compiled_unverified(),
923 ),
924 backend: Backend::Cpu,
925 reason: Some(FallbackReason::UnsupportedHardware),
926 },
927 Case {
928 name: "kernel specialization missing",
929 facts: facts(
930 true,
931 true,
932 true,
933 Some(sm120),
934 KernelAvailability::all_unavailable(),
935 ),
936 backend: Backend::Cpu,
937 reason: Some(FallbackReason::KernelSpecializationUnavailable),
938 },
939 Case {
940 name: "compiled but JIT not attempted (probe)",
941 facts: facts(
942 true,
943 true,
944 true,
945 Some(sm120),
946 KernelAvailability::compiled_unverified(),
947 ),
948 backend: Backend::Cpu,
949 reason: Some(FallbackReason::KernelSpecializationUnavailable),
950 },
951 Case {
952 name: "all kernels available",
953 facts: facts(
954 true,
955 true,
956 true,
957 Some(sm121),
958 KernelAvailability::all_available(),
959 ),
960 backend: Backend::Cuda,
961 reason: None,
962 },
963 Case {
964 name: "PTX not compiled despite cuda_built flag",
965 facts: facts(
966 true,
967 true,
968 true,
969 Some(sm120),
970 KernelAvailability::not_compiled(),
971 ),
972 backend: Backend::Cpu,
973 reason: Some(FallbackReason::CudaFeatureNotBuilt),
974 },
975 ];
976
977 for case in cases {
978 let report = evaluate_capabilities(&case.facts);
979 assert_eq!(
980 report.selected_backend, case.backend,
981 "{}: backend",
982 case.name
983 );
984 assert_eq!(
985 report.fallback.as_ref().map(|f| f.reason),
986 case.reason,
987 "{}: reason",
988 case.name
989 );
990 if let Some(fb) = &report.fallback {
991 assert_eq!(fb.selected_backend, Backend::Cpu, "{}", case.name);
992 assert_eq!(fb.reason.code(), fb.reason.to_string());
993 assert!(!fb.detail.contains("/home/"), "{}", case.name);
994 } else {
995 assert!(report.gpu_usable(), "{}", case.name);
996 }
997 }
998 }
999
1000 #[test]
1001 fn require_gpu_policy_fails_closed_on_cpu_selection() {
1002 let report = evaluate_capabilities(&CapabilityFacts::not_built());
1003 let err = report
1004 .select_backend(ExecutionPolicy::RequireGpu)
1005 .unwrap_err();
1006 assert_eq!(err.reason, FallbackReason::CudaFeatureNotBuilt);
1007 assert_eq!(err.selected_backend, Backend::Cpu);
1008 assert_eq!(
1009 report.select_backend(ExecutionPolicy::PreferGpu).unwrap(),
1010 Backend::Cpu
1011 );
1012 }
1013
1014 #[test]
1015 fn require_gpu_policy_accepts_cuda_selection() {
1016 let report = evaluate_capabilities(&facts(
1017 true,
1018 true,
1019 true,
1020 Some(ComputeCapability::REQUIRED),
1021 KernelAvailability::all_available(),
1022 ));
1023 assert_eq!(
1024 report.select_backend(ExecutionPolicy::RequireGpu).unwrap(),
1025 Backend::Cuda
1026 );
1027 }
1028
1029 #[test]
1030 fn fallback_reason_codes_are_stable() {
1031 let expected = [
1032 (
1033 FallbackReason::CudaFeatureNotBuilt,
1034 "cuda_feature_not_built",
1035 ),
1036 (
1037 FallbackReason::DriverRuntimeFailure,
1038 "driver_runtime_failure",
1039 ),
1040 (FallbackReason::DeviceUnavailable, "device_unavailable"),
1041 (FallbackReason::UnsupportedHardware, "unsupported_hardware"),
1042 (
1043 FallbackReason::KernelSpecializationUnavailable,
1044 "kernel_specialization_unavailable",
1045 ),
1046 (FallbackReason::InvalidInput, "invalid_input"),
1047 ];
1048 for (reason, code) in expected {
1049 assert_eq!(reason.code(), code);
1050 assert_eq!(reason.to_string(), code);
1051 }
1052 }
1053
1054 #[test]
1055 fn stream_setup_driver_failure_keeps_observed_runtime_and_device() {
1056 let mut facts = facts(
1057 true,
1058 true,
1059 true,
1060 Some(ComputeCapability::REQUIRED),
1061 KernelAvailability::all_available(),
1062 );
1063 apply_failure_to_facts(&mut facts, FallbackReason::DriverRuntimeFailure);
1064
1065 assert!(facts.runtime_available);
1066 assert!(facts.device_available);
1067 assert!(facts.kernels.all_runtime_available());
1068
1069 let mut report = evaluate_capabilities(&facts);
1070 report.selected_backend = Backend::Cpu;
1071 report.fallback = Some(FallbackRecord::cpu(
1072 FallbackReason::DriverRuntimeFailure,
1073 "stream creation failed",
1074 ));
1075
1076 assert!(report.runtime_available);
1077 assert!(report.device_available);
1078 assert_eq!(report.selected_backend, Backend::Cpu);
1079 assert!(!report.gpu_usable());
1080 assert_eq!(
1081 report.fallback.as_ref().map(|fb| fb.reason),
1082 Some(FallbackReason::DriverRuntimeFailure)
1083 );
1084 }
1085
1086 #[test]
1087 fn driver_runtime_failure_before_jit_still_clears_runtime() {
1088 let mut facts = facts(
1089 true,
1090 true,
1091 true,
1092 Some(ComputeCapability::REQUIRED),
1093 KernelAvailability::compiled_unverified(),
1094 );
1095 apply_failure_to_facts(&mut facts, FallbackReason::DriverRuntimeFailure);
1096 assert!(!facts.runtime_available);
1097 }
1098
1099 #[test]
1100 fn compute_capability_floor_tracks_compiled_target() {
1101 let required = ComputeCapability::REQUIRED;
1102 let arch_digits: String = env!("MYELIN_COMPILED_CUDA_ARCH")
1103 .chars()
1104 .skip_while(|ch| !ch.is_ascii_digit())
1105 .take_while(char::is_ascii_digit)
1106 .collect();
1107 let encoded = arch_digits.parse::<u32>().expect("validated build target");
1108 assert_eq!(required.major, encoded / 10);
1109 assert_eq!(required.minor, encoded % 10);
1110 assert!(required.meets_minimum());
1111 assert!(
1112 ComputeCapability {
1113 major: required.major + 1,
1114 minor: 0,
1115 }
1116 .meets_minimum()
1117 );
1118 if required.major > 0 {
1119 assert!(
1120 !ComputeCapability {
1121 major: required.major - 1,
1122 minor: 9,
1123 }
1124 .meets_minimum()
1125 );
1126 }
1127 }
1128
1129 #[test]
1130 fn sanitize_diagnostic_redacts_user_paths_and_secrets() {
1131 let raw = "failed /home/alice/.ssh/id_rsa token=supersecret C:\\Users\\bob\\key.pem ok";
1132 let clean = sanitize_diagnostic(raw);
1133 assert!(!clean.contains("/home/alice"));
1134 assert!(!clean.contains("supersecret"));
1135 assert!(!clean.contains("bob"));
1136 assert!(clean.contains("<path>"));
1137 assert!(clean.contains("token=<redacted>"));
1138 assert!(clean.contains("ok"));
1139 }
1140
1141 #[test]
1142 fn sanitize_diagnostic_keeps_stable_reason_tokens() {
1143 let s = sanitize_diagnostic("cuda_feature_not_built sm_120 driver_runtime_failure");
1144 assert_eq!(s, "cuda_feature_not_built sm_120 driver_runtime_failure");
1145 }
1146
1147 #[test]
1148 fn sanitize_diagnostic_redacts_paths_after_assignment_keys() {
1149 let clean = sanitize_diagnostic(
1150 "path=/home/alice/private.ptx file=/Users/bob/key cache=C:\\Users\\eve\\cache.bin",
1151 );
1152 assert_eq!(clean, "path=<path> file=<path> cache=<path>");
1153 }
1154
1155 #[test]
1156 fn sanitize_diagnostic_redacts_quoted_assignment_paths() {
1157 let cases = [
1158 (
1159 r#"module="/home/alice/private.ptx" tail"#,
1160 "module=<path> tail",
1161 ),
1162 (
1163 r#"module="/home/alice/My Models/private.ptx" tail"#,
1164 "module=<path> tail",
1165 ),
1166 (
1167 r#"file='C:\Users\bob\private.ptx' tail"#,
1168 "file=<path> tail",
1169 ),
1170 ];
1171
1172 for (input, expected) in cases {
1173 assert_eq!(sanitize_diagnostic(input), expected, "input={input}");
1174 }
1175 }
1176
1177 #[test]
1178 fn sanitize_diagnostic_does_not_treat_contractions_as_quotes() {
1179 assert_eq!(
1180 sanitize_diagnostic("driver can't load /home/alice/private.ptx"),
1181 "driver can't load <path>"
1182 );
1183 }
1184
1185 #[test]
1186 fn sanitize_diagnostic_redacts_namespaced_credentials() {
1187 let clean = sanitize_diagnostic(
1188 "AWS_SECRET_ACCESS_KEY=abc MY_API_KEY=def service_authorization=ghi AWS_ACCESS_KEY_ID=AKIA x-api-key=xyz",
1189 );
1190 assert_eq!(
1191 clean,
1192 "AWS_SECRET_ACCESS_KEY=<redacted> MY_API_KEY=<redacted> service_authorization=<redacted> AWS_ACCESS_KEY_ID=<redacted> x-api-key=<redacted>"
1193 );
1194 }
1195
1196 #[test]
1197 fn sanitize_diagnostic_redacts_separated_credentials() {
1198 let cases = [
1199 ("password = hunter2 ok", "password = <redacted> ok"),
1200 ("token: hunter2 ok", "token: <redacted> ok"),
1201 ("--api-key hunter2 ok", "--api-key <redacted> ok"),
1202 (
1203 "AWS_SECRET_ACCESS_KEY = abc ok",
1204 "AWS_SECRET_ACCESS_KEY = <redacted> ok",
1205 ),
1206 ("token:hunter2 ok", "token:<redacted> ok"),
1207 ("api-key:supersecret ok", "api-key:<redacted> ok"),
1208 (
1209 "state=bad,password=hunter2 ok",
1210 "state=bad,password=<redacted> ok",
1211 ),
1212 (
1213 "state=bad;file=/workspace/alice/private.ptx ok",
1214 "state=bad;file=<path> ok",
1215 ),
1216 ];
1217
1218 for (input, expected) in cases {
1219 assert_eq!(sanitize_diagnostic(input), expected, "input={input}");
1220 assert!(
1221 !sanitize_diagnostic(input).contains("hunter2"),
1222 "input={input}"
1223 );
1224 }
1225 }
1226
1227 #[test]
1228 fn sanitize_diagnostic_redacts_arbitrary_absolute_paths() {
1229 let clean = sanitize_diagnostic(
1230 "failed /workspace/alice/private.ptx file=/mnt/data/model.ptx system=/usr/local/cuda",
1231 );
1232 assert_eq!(clean, "failed <path> file=<path> system=<path>");
1233 }
1234
1235 #[test]
1236 fn sanitize_diagnostic_redacts_windows_absolute_paths_without_users() {
1237 let cases = [
1238 (
1239 r"failed C:\workspace\alice\private.ptx ok",
1240 "failed <path> ok",
1241 ),
1242 (r"file=C:\builds\alice\secret.ptx", "file=<path>"),
1243 (
1244 r"cache=D:/builds/alice/cache.bin leftover",
1245 "cache=<path> leftover",
1246 ),
1247 (
1248 r"failed \\server\share\alice\private.ptx ok",
1249 "failed <path> ok",
1250 ),
1251 (r"file=\\server\share\alice\private.ptx", "file=<path>"),
1252 (r"device=\\?\C:\private.ptx", "device=<path>"),
1253 (
1254 r#"module="C:\workspace\alice\private.ptx" tail"#,
1255 "module=<path> tail",
1256 ),
1257 ];
1258 for (input, expected) in cases {
1259 assert_eq!(sanitize_diagnostic(input), expected, "input={input}");
1260 assert!(
1261 !sanitize_diagnostic(input)
1262 .to_ascii_lowercase()
1263 .contains("alice"),
1264 "input={input}"
1265 );
1266 }
1267 }
1268
1269 #[test]
1270 fn sanitize_diagnostic_redacts_http_authorization_headers() {
1271 let cases = [
1272 (
1273 "probe Authorization: Bearer supersecret ok",
1274 "probe Authorization: Bearer <redacted> ok",
1275 ),
1276 (
1277 "probe Authorization: Basic dXNlcjpwYXNz ok",
1278 "probe Authorization: Basic <redacted> ok",
1279 ),
1280 (
1281 r#"probe "Authorization: Bearer supersecret" ok"#,
1282 r#"probe "Authorization: Bearer <redacted>" ok"#,
1283 ),
1284 (
1285 "probe authorization: bearer SUPERSECRET ok",
1286 "probe authorization: bearer <redacted> ok",
1287 ),
1288 (
1289 "probe Authorization:Bearer supersecret ok",
1290 "probe Authorization:Bearer <redacted> ok",
1291 ),
1292 (
1293 "probe Authorization:Basic dXNlcjpwYXNz ok",
1294 "probe Authorization:Basic <redacted> ok",
1295 ),
1296 ];
1297 for (input, expected) in cases {
1298 assert_eq!(sanitize_diagnostic(input), expected, "input={input}");
1299 assert!(
1300 !sanitize_diagnostic(input)
1301 .to_ascii_lowercase()
1302 .contains("supersecret"),
1303 "input={input}"
1304 );
1305 }
1306 }
1307
1308 #[test]
1309 fn sanitize_diagnostic_redacts_json_credential_fields() {
1310 let cases = [
1311 (
1312 r#"{"authToken":"supersecret","ok":true}"#,
1313 r#"{"authToken":"<redacted>","ok":true}"#,
1314 ),
1315 (
1316 r#"{"password":"hunter2"} tail"#,
1317 r#"{"password":"<redacted>"} tail"#,
1318 ),
1319 (
1320 r#"{"authToken": "supersecret"} tail"#,
1321 r#"{"authToken": "<redacted>"} tail"#,
1322 ),
1323 (
1324 r#"{"file":"/home/alice/private.ptx"}"#,
1325 r#"{"file":"<path>"}"#,
1326 ),
1327 ];
1328 for (input, expected) in cases {
1329 assert_eq!(sanitize_diagnostic(input), expected, "input={input}");
1330 }
1331 }
1332}