1use serde::Deserialize;
8use std::fs;
9use std::path::Path;
10use std::sync::LazyLock;
11
12use crate::DeviceProfile;
13
14const MAX_DEVICE_SIGNATURE_TOML_BYTES: u64 = 256 * 1024;
15
16static BUILTIN_SIGNATURE_TABLE: LazyLock<Result<DeviceSignatureTable, String>> =
26 LazyLock::new(|| {
27 #[cfg(test)]
28 BUILTIN_SIGNATURE_TABLE_PARSES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
29 let mut signatures = vec![DeviceSignature::from_toml_str(
30 DeviceSignature::BUILTIN_BLACKWELL_120,
31 )?];
32 signatures.sort_unstable_by(|left, right| left.id.cmp(&right.id));
33 Ok(DeviceSignatureTable { signatures })
34 });
35
36#[cfg(test)]
42static BUILTIN_SIGNATURE_TABLE_PARSES: std::sync::atomic::AtomicUsize =
43 std::sync::atomic::AtomicUsize::new(0);
44
45#[derive(Clone, Debug, Deserialize, PartialEq)]
47pub struct DeviceSignature {
48 pub id: String,
50 pub family: String,
52 #[serde(default)]
54 pub architecture_generation: Option<u32>,
55 #[serde(default)]
57 pub device_name_contains: Vec<String>,
58 pub max_sm: u32,
60 pub warp_size: u32,
62 pub regs_per_thread_max: u32,
64 pub shared_mem_per_sm_kb: u32,
66 pub l1_kb: u32,
68 pub l2_kb: u32,
70 pub mem_bw_gbps: u32,
72 pub tensor_core_supported: bool,
74 #[serde(default)]
76 pub tensor_core_dtypes: Vec<String>,
77 pub ideal_unroll_depth: u32,
79 pub ideal_vector_pack_bits: u32,
81 pub ideal_workgroup_tile: [u32; 3],
83 pub bank_count: u32,
85 pub bank_width_bytes: u32,
87}
88
89#[derive(Clone, Debug, Default, PartialEq)]
91pub struct DeviceSignatureTable {
92 signatures: Vec<DeviceSignature>,
93}
94
95impl DeviceSignature {
96 pub const BUILTIN_BLACKWELL_120: &'static str = include_str!("../devices/blackwell_120.toml");
98
99 pub fn from_toml_str(source: &str) -> Result<Self, String> {
106 let signature: Self = toml::from_str(source)
107 .map_err(|error| format!("device signature TOML parse failed. Fix: {error}"))?;
108 signature.validate()?;
109 Ok(signature)
110 }
111
112 pub fn validate(&self) -> Result<(), String> {
118 if self.id.trim().is_empty() {
119 return Err("device signature id is empty. Fix: set a stable id.".to_string());
120 }
121 if self.family.trim().is_empty() {
122 return Err(
123 "device signature family is empty. Fix: set the architecture family.".to_string(),
124 );
125 }
126 if self.warp_size == 0 || !self.warp_size.is_power_of_two() {
127 return Err(format!(
128 "device signature `{}` has invalid warp_size {}. Fix: use a non-zero power of two.",
129 self.id, self.warp_size
130 ));
131 }
132 if self.ideal_vector_pack_bits == 0 || self.ideal_vector_pack_bits % 32 != 0 {
133 return Err(format!(
134 "device signature `{}` has invalid ideal_vector_pack_bits {}. Fix: use a positive multiple of 32.",
135 self.id, self.ideal_vector_pack_bits
136 ));
137 }
138 if self.ideal_workgroup_tile.contains(&0) {
139 return Err(format!(
140 "device signature `{}` has a zero ideal_workgroup_tile axis. Fix: every axis must be positive.",
141 self.id
142 ));
143 }
144 if self.bank_count == 0 || self.bank_width_bytes == 0 {
145 return Err(format!(
146 "device signature `{}` has invalid shared-memory bank metadata. Fix: bank_count and bank_width_bytes must be non-zero.",
147 self.id
148 ));
149 }
150 validate_kib_projection(self.shared_mem_per_sm_kb, "shared_mem_per_sm_kb", &self.id)?;
151 validate_kib_projection(self.l1_kb, "l1_kb", &self.id)?;
152 validate_kib_projection(self.l2_kb, "l2_kb", &self.id)?;
153 Ok(())
154 }
155
156 #[must_use]
158 pub fn apply_to_profile(&self, mut profile: DeviceProfile) -> DeviceProfile {
159 profile.subgroup_size = self.warp_size;
160 profile.supports_tensor_cores = self.tensor_core_supported;
161 profile.has_subgroup_shuffle = self.warp_size > 0;
162 profile.has_shared_memory |= self.shared_mem_per_sm_kb > 0;
163 if profile.max_shared_memory_bytes == 0 {
164 profile.max_shared_memory_bytes =
165 kib_to_bytes_checked(self.shared_mem_per_sm_kb, "shared_mem_per_sm_kb", &self.id);
166 }
167 profile.compute_units = self.max_sm;
168 profile.regs_per_thread_max = self.regs_per_thread_max;
169 profile.l1_cache_bytes = kib_to_bytes_checked(self.l1_kb, "l1_kb", &self.id);
170 profile.l2_cache_bytes = kib_to_bytes_checked(self.l2_kb, "l2_kb", &self.id);
171 profile.mem_bw_gbps = self.mem_bw_gbps;
172 profile.ideal_unroll_depth = self.ideal_unroll_depth;
173 profile.ideal_vector_pack_bits = self.ideal_vector_pack_bits;
174 profile.ideal_workgroup_tile = self.ideal_workgroup_tile;
175 profile.shared_memory_bank_count = self.bank_count;
176 profile.shared_memory_bank_width_bytes = self.bank_width_bytes;
177 profile
178 }
179
180 #[must_use]
183 pub fn matches_architecture_generation(&self, generation: u32) -> bool {
184 self.architecture_generation == Some(generation)
185 || self.id.rsplit('_').next().and_then(parse_u32) == Some(generation)
186 }
187
188 #[must_use]
191 pub fn matches_device_name(&self, device_name: &str) -> bool {
192 let device_name = device_name.to_ascii_lowercase();
193 self.device_name_contains
194 .iter()
195 .any(|needle| device_name.contains(&needle.to_ascii_lowercase()))
196 }
197}
198
199impl DeviceSignatureTable {
200 pub fn builtins() -> Result<Self, String> {
217 Self::builtins_ref().cloned()
218 }
219
220 pub fn builtins_ref() -> Result<&'static Self, String> {
226 BUILTIN_SIGNATURE_TABLE.as_ref().map_err(Clone::clone)
227 }
228
229 pub fn load_dir(dir: impl AsRef<Path>) -> Result<Self, String> {
236 let dir = dir.as_ref();
237 let entries = fs::read_dir(dir).map_err(|error| {
238 format!(
239 "device signature directory `{}` cannot be read. Fix: create it or pass the correct path: {error}",
240 dir.display()
241 )
242 })?;
243 let mut signatures = Vec::new();
244 for entry in entries {
245 let entry = entry.map_err(|error| {
246 format!(
247 "device signature directory `{}` contains an unreadable entry. Fix: {error}",
248 dir.display()
249 )
250 })?;
251 let path = entry.path();
252 if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
253 continue;
254 }
255 let source = read_device_signature_toml(&path).map_err(|error| {
256 format!(
257 "device signature file `{}` cannot be read. Fix: {error}",
258 path.display()
259 )
260 })?;
261 let signature = DeviceSignature::from_toml_str(&source)
262 .map_err(|error| format!("{} in `{}`", error, path.display()))?;
263 signatures.push(signature);
264 }
265 signatures.sort_unstable_by(|left, right| left.id.cmp(&right.id));
266 dedupe_signature_ids(&signatures)?;
267 Ok(Self { signatures })
268 }
269
270 #[must_use]
272 pub fn signatures(&self) -> &[DeviceSignature] {
273 &self.signatures
274 }
275
276 #[must_use]
278 pub fn get(&self, id: &str) -> Option<&DeviceSignature> {
279 self.signatures
280 .binary_search_by(|signature| signature.id.as_str().cmp(id))
281 .ok()
282 .and_then(|index| self.signatures.get(index))
283 }
284
285 #[must_use]
287 pub fn find_architecture_generation(&self, generation: u32) -> Option<&DeviceSignature> {
288 self.signatures
289 .iter()
290 .find(|signature| signature.matches_architecture_generation(generation))
291 }
292
293 #[must_use]
295 pub fn find_device_name(&self, device_name: &str) -> Option<&DeviceSignature> {
296 self.signatures
297 .iter()
298 .find(|signature| signature.matches_device_name(device_name))
299 }
300
301 #[must_use]
303 pub fn apply_generation_to_profile(
304 &self,
305 generation: u32,
306 profile: DeviceProfile,
307 ) -> DeviceProfile {
308 self.find_architecture_generation(generation)
309 .map_or(profile, |signature| signature.apply_to_profile(profile))
310 }
311
312 #[must_use]
314 pub fn apply_device_name_to_profile(
315 &self,
316 device_name: &str,
317 profile: DeviceProfile,
318 ) -> DeviceProfile {
319 self.find_device_name(device_name)
320 .map_or(profile, |signature| signature.apply_to_profile(profile))
321 }
322}
323
324fn read_device_signature_toml(path: &Path) -> std::io::Result<String> {
325 use std::io::Read as _;
326
327 let mut file = fs::File::open(path)?;
328 let metadata = file.metadata()?;
329 if metadata.len() > MAX_DEVICE_SIGNATURE_TOML_BYTES {
330 return Err(std::io::Error::new(
331 std::io::ErrorKind::InvalidData,
332 format!("device signature TOML exceeds {MAX_DEVICE_SIGNATURE_TOML_BYTES} byte limit"),
333 ));
334 }
335 let mut text = String::with_capacity(metadata.len() as usize);
336 file.by_ref()
337 .take(MAX_DEVICE_SIGNATURE_TOML_BYTES + 1)
338 .read_to_string(&mut text)?;
339 if text.len() as u64 > MAX_DEVICE_SIGNATURE_TOML_BYTES {
340 return Err(std::io::Error::new(
341 std::io::ErrorKind::InvalidData,
342 "device signature TOML exceeded bounded read limit",
343 ));
344 }
345 Ok(text)
346}
347
348fn dedupe_signature_ids(signatures: &[DeviceSignature]) -> Result<(), String> {
349 for pair in signatures.windows(2) {
350 if pair[0].id == pair[1].id {
351 return Err(format!(
352 "duplicate device signature id `{}`. Fix: keep exactly one TOML file per id.",
353 pair[0].id
354 ));
355 }
356 }
357 Ok(())
358}
359
360fn validate_kib_projection(value: u32, field: &str, id: &str) -> Result<(), String> {
361 value.checked_mul(1024).map(|_| ()).ok_or_else(|| {
362 format!(
363 "device signature `{id}` field {field}={value} KiB overflows u32 bytes. Fix: split the architecture record or lower the Tier-B value; silent saturation corrupts GPU resource planning."
364 )
365 })
366}
367
368fn kib_to_bytes_checked(value: u32, field: &str, id: &str) -> u32 {
369 let _ = (field, id);
370 value.saturating_mul(1024)
371}
372
373fn parse_u32(value: &str) -> Option<u32> {
374 let mut out = 0u32;
375 for byte in value.bytes() {
376 if !byte.is_ascii_digit() {
377 return None;
378 }
379 out = out.checked_mul(10)?.checked_add(u32::from(byte - b'0'))?;
380 }
381 Some(out)
382}
383
384#[cfg(test)]
385mod tests {
386 use super::{DeviceSignature, DeviceSignatureTable};
387 use crate::DeviceProfile;
388
389 const SAMPLE: &str = r#"
390id = "sample_arch"
391family = "sample"
392max_sm = 128
393warp_size = 32
394regs_per_thread_max = 255
395shared_mem_per_sm_kb = 128
396l1_kb = 128
397l2_kb = 98304
398mem_bw_gbps = 1700
399tensor_core_supported = true
400tensor_core_dtypes = ["f16", "bf16", "tf32"]
401ideal_unroll_depth = 8
402ideal_vector_pack_bits = 128
403ideal_workgroup_tile = [16, 16, 1]
404bank_count = 32
405bank_width_bytes = 4
406"#;
407
408 #[test]
409 fn parses_and_validates_signature() {
410 let signature = DeviceSignature::from_toml_str(SAMPLE).unwrap();
411
412 assert_eq!(signature.id, "sample_arch");
413 assert_eq!(signature.architecture_generation, None);
414 assert_eq!(signature.warp_size, 32);
415 assert!(signature.tensor_core_supported);
416 }
417
418 #[test]
419 fn rejects_invalid_warp_size() {
420 let err =
421 DeviceSignature::from_toml_str(&SAMPLE.replace("warp_size = 32", "warp_size = 48"))
422 .unwrap_err();
423
424 assert!(err.contains("warp_size"));
425 }
426
427 #[test]
428 fn applies_architecture_facts_to_profile() {
429 let signature = DeviceSignature::from_toml_str(SAMPLE).unwrap();
430 let profile = signature.apply_to_profile(DeviceProfile::conservative("test"));
431
432 assert_eq!(profile.subgroup_size, 32);
433 assert_eq!(profile.max_shared_memory_bytes, 128 * 1024);
434 assert_eq!(profile.compute_units, 128);
435 assert_eq!(profile.ideal_vector_pack_bits, 128);
436 assert_eq!(profile.shared_memory_bank_width_bytes, 4);
437 assert!(profile.supports_tensor_cores);
438 }
439
440 #[test]
441 fn preserves_live_shared_memory_per_workgroup_limit() {
442 let signature = DeviceSignature::from_toml_str(SAMPLE).unwrap();
443 let mut live = DeviceProfile::conservative("native");
444 live.max_shared_memory_bytes = 48 * 1024;
445 let profile = signature.apply_to_profile(live);
446
447 assert_eq!(profile.max_shared_memory_bytes, 48 * 1024);
448 assert!(profile.has_shared_memory);
449 assert_eq!(profile.shared_memory_bank_count, 32);
450 }
451
452 #[test]
453 fn loads_directory_in_id_order() {
454 let dir = tempfile::tempdir().unwrap();
455 std::fs::write(
456 dir.path().join("b.toml"),
457 SAMPLE.replace("sample_arch", "b"),
458 )
459 .unwrap();
460 std::fs::write(
461 dir.path().join("a.toml"),
462 SAMPLE.replace("sample_arch", "a"),
463 )
464 .unwrap();
465
466 let table = DeviceSignatureTable::load_dir(dir.path()).unwrap();
467
468 assert_eq!(table.signatures()[0].id, "a");
469 assert_eq!(table.signatures()[1].id, "b");
470 assert!(table.get("b").is_some());
471 }
472
473 #[test]
474 fn repository_device_signatures_load() {
475 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../devices");
476 let table = DeviceSignatureTable::load_dir(dir).unwrap();
477
478 assert!(table.get("blackwell_120").is_some());
479 }
480
481 #[test]
482 fn builtins_match_generation_and_device_name() {
483 let table = DeviceSignatureTable::builtins().unwrap();
484 let signature = table.find_architecture_generation(120).unwrap();
485
486 assert_eq!(signature.id, "blackwell_120");
487 assert!(table.find_device_name("RTX 5090").is_some());
488 }
489
490 #[test]
491 fn builtin_signature_materially_projects_planner_fields() {
492 let table = DeviceSignatureTable::builtins().unwrap();
493 let signature = table.find_architecture_generation(120).unwrap();
494 let profile =
495 table.apply_generation_to_profile(120, DeviceProfile::conservative("backend"));
496
497 assert_eq!(profile.ideal_unroll_depth, signature.ideal_unroll_depth);
498 assert_eq!(
499 profile.ideal_vector_pack_bits,
500 signature.ideal_vector_pack_bits
501 );
502 assert_eq!(profile.ideal_workgroup_tile, signature.ideal_workgroup_tile);
503 assert_eq!(profile.shared_memory_bank_count, signature.bank_count);
504 }
505
506 #[test]
519 fn builtin_signature_table_is_parsed_once_per_process() {
520 use std::sync::atomic::Ordering;
521
522 let first = DeviceSignatureTable::builtins().expect("Fix: builtin signatures must load");
523 for _ in 0..1_000 {
524 let repeated =
525 DeviceSignatureTable::builtins().expect("Fix: builtin signatures must load");
526 assert_eq!(
527 repeated, first,
528 "Fix: the memoized builtin table must return the same signatures on every call."
529 );
530 }
531 for _ in 0..1_000 {
532 let borrowed = DeviceSignatureTable::builtins_ref()
533 .expect("Fix: builtin signatures must load by reference");
534 assert_eq!(
535 *borrowed, first,
536 "Fix: builtins_ref must borrow the same table that builtins clones."
537 );
538 }
539
540 assert_eq!(
541 super::BUILTIN_SIGNATURE_TABLE_PARSES.load(Ordering::Relaxed),
542 1,
543 "Fix: the compiled-in device signature TOML must be parsed exactly once per process. \
544 2001 calls produced a different parse count, so the memo was bypassed or reset."
545 );
546 }
547}