1use crate::GpuOptimError;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct SmResourceLimits {
68 pub compute_capability: (u32, u32),
70
71 pub warp_size: u32,
73
74 pub max_threads_per_block: u32,
76
77 pub max_threads_per_sm: u32,
79
80 pub max_warps_per_sm: u32,
82
83 pub max_blocks_per_sm: u32,
85
86 pub registers_per_sm: u32,
88
89 pub register_alloc_granularity: u32,
92
93 pub shared_mem_per_sm_bytes: usize,
95}
96
97impl SmResourceLimits {
98 #[must_use]
108 pub const fn sm_70() -> Self {
109 Self {
110 compute_capability: (7, 0),
111 warp_size: 32,
112 max_threads_per_block: 1024,
113 max_threads_per_sm: 2048,
114 max_warps_per_sm: 64,
115 max_blocks_per_sm: 32,
116 registers_per_sm: 65536,
117 register_alloc_granularity: 256,
118 shared_mem_per_sm_bytes: 98_304,
119 }
120 }
121
122 #[must_use]
131 pub const fn sm_75() -> Self {
132 Self {
133 compute_capability: (7, 5),
134 warp_size: 32,
135 max_threads_per_block: 1024,
136 max_threads_per_sm: 1024,
137 max_warps_per_sm: 32,
138 max_blocks_per_sm: 16,
139 registers_per_sm: 65536,
140 register_alloc_granularity: 256,
141 shared_mem_per_sm_bytes: 65_536,
142 }
143 }
144
145 #[must_use]
153 pub const fn sm_80() -> Self {
154 Self {
155 compute_capability: (8, 0),
156 warp_size: 32,
157 max_threads_per_block: 1024,
158 max_threads_per_sm: 2048,
159 max_warps_per_sm: 64,
160 max_blocks_per_sm: 32,
161 registers_per_sm: 65536,
162 register_alloc_granularity: 256,
163 shared_mem_per_sm_bytes: 167_936,
164 }
165 }
166
167 #[must_use]
176 pub const fn sm_86() -> Self {
177 Self {
178 compute_capability: (8, 6),
179 warp_size: 32,
180 max_threads_per_block: 1024,
181 max_threads_per_sm: 1536,
182 max_warps_per_sm: 48,
183 max_blocks_per_sm: 16,
184 registers_per_sm: 65536,
185 register_alloc_granularity: 256,
186 shared_mem_per_sm_bytes: 102_400,
187 }
188 }
189
190 #[must_use]
198 pub const fn sm_90() -> Self {
199 Self {
200 compute_capability: (9, 0),
201 warp_size: 32,
202 max_threads_per_block: 1024,
203 max_threads_per_sm: 2048,
204 max_warps_per_sm: 64,
205 max_blocks_per_sm: 32,
206 registers_per_sm: 65536,
207 register_alloc_granularity: 256,
208 shared_mem_per_sm_bytes: 233_472,
209 }
210 }
211
212 pub fn from_compute_capability(compute_capability: (u32, u32)) -> Result<Self, GpuOptimError> {
226 let (major, minor) = compute_capability;
227 match (major, minor) {
228 (7, 0) | (7, 2) => Ok(Self::sm_70()),
229 (7, 5) => Ok(Self::sm_75()),
230 (8, 0) => Ok(Self::sm_80()),
231 (8, 6) | (8, 7) | (8, 9) => Ok(Self::sm_86()),
232 (9, 0) => Ok(Self::sm_90()),
233 _ => Err(GpuOptimError::UnsupportedOperation(format!(
234 "no CUDA occupancy model for compute capability {major}.{minor}"
235 ))),
236 }
237 }
238
239 pub fn from_device_capabilities(
247 capabilities: &crate::backends::DeviceCapabilities,
248 ) -> Result<Self, GpuOptimError> {
249 let mut limits = Self::from_compute_capability(capabilities.compute_capability)?;
250 if capabilities.max_threads_per_block > 0 {
251 limits.max_threads_per_block = capabilities.max_threads_per_block;
252 }
253 Ok(limits)
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub struct KernelResourceUsage {
260 pub registers_per_thread: u32,
262
263 pub shared_mem_per_block_bytes: usize,
266
267 pub threads_per_block: u32,
269}
270
271impl KernelResourceUsage {
272 #[must_use]
274 pub const fn new(
275 registers_per_thread: u32,
276 shared_mem_per_block_bytes: usize,
277 threads_per_block: u32,
278 ) -> Self {
279 Self {
280 registers_per_thread,
281 shared_mem_per_block_bytes,
282 threads_per_block,
283 }
284 }
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum OccupancyLimiter {
290 Warps,
292 Registers,
294 SharedMemory,
296 BlocksPerSm,
298 ThreadsPerBlock,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq)]
305pub struct OccupancyResult {
306 pub active_warps_per_sm: u32,
308
309 pub active_blocks_per_sm: u32,
311
312 pub max_warps_per_sm: u32,
314
315 pub occupancy: f64,
317
318 pub limiter: OccupancyLimiter,
320}
321
322const fn round_up_to_multiple(value: u64, granularity: u64) -> u64 {
329 if granularity == 0 {
330 return value;
331 }
332 match value.div_ceil(granularity).checked_mul(granularity) {
333 Some(rounded) => rounded,
334 None => u64::MAX,
335 }
336}
337
338const fn clamp_u64_to_u32(value: u64) -> u32 {
340 if value > u32::MAX as u64 {
341 u32::MAX
342 } else {
343 value as u32
344 }
345}
346
347const fn clamp_usize_to_u32(value: usize) -> u32 {
349 if value > u32::MAX as usize {
350 u32::MAX
351 } else {
352 value as u32
353 }
354}
355
356pub fn calculate_occupancy(
367 usage: &KernelResourceUsage,
368 limits: &SmResourceLimits,
369) -> Result<OccupancyResult, GpuOptimError> {
370 let warp_size = limits.warp_size;
371 let threads_per_block = usage.threads_per_block;
372
373 if warp_size == 0 {
374 return Err(GpuOptimError::InvalidState(
375 "warp_size must be greater than zero".to_string(),
376 ));
377 }
378 if threads_per_block == 0 {
379 return Err(GpuOptimError::InvalidState(
380 "threads_per_block must be greater than zero".to_string(),
381 ));
382 }
383
384 if threads_per_block > limits.max_threads_per_block {
387 return Ok(OccupancyResult {
388 active_warps_per_sm: 0,
389 active_blocks_per_sm: 0,
390 max_warps_per_sm: limits.max_warps_per_sm,
391 occupancy: 0.0,
392 limiter: OccupancyLimiter::ThreadsPerBlock,
393 });
394 }
395
396 let warps_per_block = threads_per_block.div_ceil(warp_size);
397
398 let warps_limit = limits.max_warps_per_sm / warps_per_block;
400
401 let register_limit = if usage.registers_per_thread == 0 {
404 u32::MAX
405 } else {
406 let raw_registers =
407 u64::from(usage.registers_per_thread).saturating_mul(u64::from(threads_per_block));
408 let registers_per_block =
409 round_up_to_multiple(raw_registers, u64::from(limits.register_alloc_granularity))
410 .max(1);
411 clamp_u64_to_u32(u64::from(limits.registers_per_sm) / registers_per_block)
412 };
413
414 let shared_mem_limit = match limits
418 .shared_mem_per_sm_bytes
419 .checked_div(usage.shared_mem_per_block_bytes)
420 {
421 Some(blocks) => clamp_usize_to_u32(blocks),
422 None => u32::MAX,
423 };
424
425 let block_cap_limit = limits.max_blocks_per_sm;
427
428 let candidates = [
431 (warps_limit, OccupancyLimiter::Warps),
432 (register_limit, OccupancyLimiter::Registers),
433 (shared_mem_limit, OccupancyLimiter::SharedMemory),
434 (block_cap_limit, OccupancyLimiter::BlocksPerSm),
435 ];
436
437 let mut active_blocks = candidates[0].0;
438 let mut limiter = candidates[0].1;
439 for &(value, candidate_limiter) in &candidates[1..] {
440 if value < active_blocks {
441 active_blocks = value;
442 limiter = candidate_limiter;
443 }
444 }
445
446 let active_warps = active_blocks.saturating_mul(warps_per_block);
447 let occupancy = if limits.max_warps_per_sm == 0 {
448 0.0
449 } else {
450 f64::from(active_warps) / f64::from(limits.max_warps_per_sm)
451 };
452
453 Ok(OccupancyResult {
454 active_warps_per_sm: active_warps,
455 active_blocks_per_sm: active_blocks,
456 max_warps_per_sm: limits.max_warps_per_sm,
457 occupancy,
458 limiter,
459 })
460}
461
462pub fn optimal_block_size<F>(
478 registers_per_thread: u32,
479 shared_mem_per_block: F,
480 limits: &SmResourceLimits,
481) -> Result<(u32, OccupancyResult), GpuOptimError>
482where
483 F: Fn(u32) -> usize,
484{
485 if limits.warp_size == 0 {
486 return Err(GpuOptimError::InvalidState(
487 "warp_size must be greater than zero".to_string(),
488 ));
489 }
490
491 let mut best: Option<(u32, OccupancyResult)> = None;
492 let mut block_size = limits.warp_size;
493 while block_size <= limits.max_threads_per_block {
494 let usage = KernelResourceUsage {
495 registers_per_thread,
496 shared_mem_per_block_bytes: shared_mem_per_block(block_size),
497 threads_per_block: block_size,
498 };
499 let result = calculate_occupancy(&usage, limits)?;
500
501 let replace = match &best {
505 None => true,
506 Some((best_block_size, best_result)) => {
507 result.active_warps_per_sm > best_result.active_warps_per_sm
508 || (result.active_warps_per_sm == best_result.active_warps_per_sm
509 && block_size > *best_block_size)
510 }
511 };
512 if replace {
513 best = Some((block_size, result));
514 }
515
516 block_size += limits.warp_size;
517 }
518
519 best.ok_or_else(|| {
520 GpuOptimError::InvalidState(
521 "no block size that is a multiple of warp_size fits within max_threads_per_block"
522 .to_string(),
523 )
524 })
525}
526
527pub fn occupancy_for_launch(
534 config: &crate::backends::LaunchConfig,
535 registers_per_thread: u32,
536 limits: &SmResourceLimits,
537) -> Result<OccupancyResult, GpuOptimError> {
538 let (block_x, block_y, block_z) = config.block_size;
539 let threads_per_block = block_x
540 .checked_mul(block_y)
541 .and_then(|partial| partial.checked_mul(block_z))
542 .ok_or_else(|| {
543 GpuOptimError::InvalidState("block_size dimension product overflows u32".to_string())
544 })?;
545
546 let usage = KernelResourceUsage {
547 registers_per_thread,
548 shared_mem_per_block_bytes: config.shared_memory_size,
549 threads_per_block,
550 };
551 calculate_occupancy(&usage, limits)
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 fn approx(actual: f64, expected: f64) -> bool {
560 (actual - expected).abs() < 1e-9
561 }
562
563 #[test]
564 fn textbook_sm80_256_threads_32_registers() {
565 let limits = SmResourceLimits::sm_80();
569 let usage = KernelResourceUsage::new(32, 0, 256);
570 let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
571
572 assert_eq!(result.active_blocks_per_sm, 8);
573 assert_eq!(result.active_warps_per_sm, 64);
574 assert_eq!(result.max_warps_per_sm, 64);
575 assert!(approx(result.occupancy, 1.0));
576 assert_eq!(result.limiter, OccupancyLimiter::Warps);
578 }
579
580 #[test]
581 fn register_bound_sm80() {
582 let limits = SmResourceLimits::sm_80();
584 let usage = KernelResourceUsage::new(64, 0, 256);
585 let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
586
587 assert_eq!(result.active_blocks_per_sm, 4);
588 assert_eq!(result.active_warps_per_sm, 32);
589 assert!(approx(result.occupancy, 0.5));
590 assert_eq!(result.limiter, OccupancyLimiter::Registers);
591 }
592
593 #[test]
594 fn shared_memory_bound_sm80() {
595 let limits = SmResourceLimits::sm_80();
598 let usage = KernelResourceUsage::new(16, 48 * 1024, 128);
599 let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
600
601 assert_eq!(result.active_blocks_per_sm, 3);
602 assert_eq!(result.active_warps_per_sm, 12);
603 assert!(approx(result.occupancy, 12.0 / 64.0));
604 assert_eq!(result.limiter, OccupancyLimiter::SharedMemory);
605 }
606
607 #[test]
608 fn block_cap_dominates_with_tiny_blocks_sm80() {
609 let limits = SmResourceLimits::sm_80();
612 let usage = KernelResourceUsage::new(0, 0, 32);
613 let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
614
615 assert_eq!(result.active_blocks_per_sm, 32);
616 assert_eq!(result.active_warps_per_sm, 32);
617 assert!(approx(result.occupancy, 0.5));
618 assert_eq!(result.limiter, OccupancyLimiter::BlocksPerSm);
619 }
620
621 #[test]
622 fn register_allocation_granularity_rounds_up() {
623 let limits = SmResourceLimits::sm_80();
626 let usage = KernelResourceUsage::new(33, 0, 96);
627 let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
628
629 assert_eq!(result.active_blocks_per_sm, 19);
630 assert_eq!(result.active_warps_per_sm, 57);
631 assert!(approx(result.occupancy, 57.0 / 64.0));
632 assert_eq!(result.limiter, OccupancyLimiter::Registers);
633 }
634
635 #[test]
636 fn warps_per_block_uses_ceiling() {
637 let limits = SmResourceLimits::sm_80();
639 let usage = KernelResourceUsage::new(0, 0, 100);
640 let result = calculate_occupancy(&usage, &limits).expect("valid configuration");
641 assert_eq!(result.active_blocks_per_sm, 16);
644 assert_eq!(result.active_warps_per_sm, 64);
645 assert_eq!(result.limiter, OccupancyLimiter::Warps);
646 }
647
648 #[test]
649 fn threads_exceeding_hardware_limit_report_threadsperblock() {
650 let limits = SmResourceLimits::sm_80();
651 let usage = KernelResourceUsage::new(32, 0, 2048); let result = calculate_occupancy(&usage, &limits).expect("returns zero-occupancy result");
653
654 assert_eq!(result.active_blocks_per_sm, 0);
655 assert_eq!(result.active_warps_per_sm, 0);
656 assert!(approx(result.occupancy, 0.0));
657 assert_eq!(result.limiter, OccupancyLimiter::ThreadsPerBlock);
658 }
659
660 #[test]
661 fn zero_threads_is_an_error() {
662 let limits = SmResourceLimits::sm_80();
663 let usage = KernelResourceUsage::new(32, 0, 0);
664 assert!(calculate_occupancy(&usage, &limits).is_err());
665 }
666
667 #[test]
668 fn optimal_block_size_prefers_full_occupancy_and_largest_block() {
669 let limits = SmResourceLimits::sm_80();
672 let (block_size, result) =
673 optimal_block_size(32, |_| 0, &limits).expect("a candidate exists");
674
675 assert_eq!(block_size % limits.warp_size, 0);
676 assert!(block_size <= limits.max_threads_per_block);
677 assert_eq!(block_size, 1024);
678 assert!(approx(result.occupancy, 1.0));
679
680 let spot = calculate_occupancy(&KernelResourceUsage::new(32, 0, 256), &limits)
683 .expect("valid configuration");
684 assert!(result.occupancy >= spot.occupancy);
685 }
686
687 #[test]
688 fn optimal_block_size_finds_register_heavy_sweet_spot() {
689 let limits = SmResourceLimits::sm_80();
693 let (block_size, result) =
694 optimal_block_size(96, |_| 0, &limits).expect("a candidate exists");
695
696 assert_eq!(result.active_warps_per_sm, 21);
697 assert!(approx(result.occupancy, 21.0 / 64.0));
698 assert_eq!(result.limiter, OccupancyLimiter::Registers);
699
700 let mut probe = limits.warp_size;
703 while probe <= limits.max_threads_per_block {
704 let candidate = calculate_occupancy(&KernelResourceUsage::new(96, 0, probe), &limits)
705 .expect("valid configuration");
706 assert!(result.active_warps_per_sm >= candidate.active_warps_per_sm);
707 if candidate.active_warps_per_sm == result.active_warps_per_sm {
708 assert!(probe <= block_size);
709 }
710 probe += limits.warp_size;
711 }
712 assert_eq!(block_size, 672);
713 }
714
715 #[test]
716 fn optimal_block_size_supports_block_dependent_shared_memory() {
717 let limits = SmResourceLimits::sm_80();
719 let (block_size, result) = optimal_block_size(16, |threads| threads as usize * 4, &limits)
720 .expect("a candidate exists");
721
722 assert_eq!(block_size % limits.warp_size, 0);
723 assert!(block_size <= limits.max_threads_per_block);
724 assert!(result.occupancy > 0.0);
725 }
726
727 #[test]
728 fn from_compute_capability_maps_known_architectures() {
729 assert_eq!(
730 SmResourceLimits::from_compute_capability((7, 0)).expect("known"),
731 SmResourceLimits::sm_70()
732 );
733 assert_eq!(
734 SmResourceLimits::from_compute_capability((7, 5)).expect("known"),
735 SmResourceLimits::sm_75()
736 );
737 assert_eq!(
738 SmResourceLimits::from_compute_capability((8, 0)).expect("known"),
739 SmResourceLimits::sm_80()
740 );
741 assert_eq!(
742 SmResourceLimits::from_compute_capability((8, 6)).expect("known"),
743 SmResourceLimits::sm_86()
744 );
745 assert_eq!(
746 SmResourceLimits::from_compute_capability((9, 0)).expect("known"),
747 SmResourceLimits::sm_90()
748 );
749 assert_eq!(
751 SmResourceLimits::from_compute_capability((7, 2)).expect("nearest"),
752 SmResourceLimits::sm_70()
753 );
754 assert_eq!(
755 SmResourceLimits::from_compute_capability((8, 9)).expect("nearest"),
756 SmResourceLimits::sm_86()
757 );
758 }
759
760 #[test]
761 fn unknown_compute_capability_is_an_error() {
762 assert!(SmResourceLimits::from_compute_capability((5, 0)).is_err());
763 assert!(SmResourceLimits::from_compute_capability((10, 0)).is_err());
764 assert!(SmResourceLimits::from_compute_capability((0, 0)).is_err());
765 }
766
767 #[test]
768 fn architecture_constants_are_consistent() {
769 for limits in [
770 SmResourceLimits::sm_70(),
771 SmResourceLimits::sm_75(),
772 SmResourceLimits::sm_80(),
773 SmResourceLimits::sm_86(),
774 SmResourceLimits::sm_90(),
775 ] {
776 assert_eq!(limits.warp_size, 32);
777 assert_eq!(limits.max_threads_per_block, 1024);
778 assert_eq!(limits.register_alloc_granularity, 256);
779 assert_eq!(limits.registers_per_sm, 65536);
780 assert!(limits.max_warps_per_sm > 0);
781 assert!(limits.max_blocks_per_sm > 0);
782 assert_eq!(
784 limits.max_threads_per_sm,
785 limits.max_warps_per_sm * limits.warp_size
786 );
787 }
788 assert_eq!(SmResourceLimits::sm_75().max_warps_per_sm, 32);
790 assert_eq!(SmResourceLimits::sm_75().max_blocks_per_sm, 16);
791 assert_eq!(SmResourceLimits::sm_86().max_warps_per_sm, 48);
792 assert_eq!(SmResourceLimits::sm_90().shared_mem_per_sm_bytes, 233_472);
793 }
794
795 #[test]
796 fn from_device_capabilities_uses_compute_capability() {
797 let caps = crate::backends::DeviceCapabilities {
798 name: "A100 (test)".to_string(),
799 total_memory: 0,
800 available_memory: 0,
801 supports_f16: true,
802 supports_bf16: true,
803 supports_tensor_cores: true,
804 max_threads_per_block: 1024,
805 max_shared_memory_per_block: 49152,
806 multiprocessor_count: 108,
807 compute_capability: (8, 0),
808 };
809 let limits = SmResourceLimits::from_device_capabilities(&caps).expect("cuda device");
810 assert_eq!(limits.max_warps_per_sm, 64);
811 assert_eq!(limits.max_blocks_per_sm, 32);
812 assert_eq!(limits.max_threads_per_block, 1024);
813 }
814
815 #[test]
816 fn from_device_capabilities_rejects_non_cuda_devices() {
817 let caps = crate::backends::DeviceCapabilities {
818 name: "CPU (test)".to_string(),
819 total_memory: 0,
820 available_memory: 0,
821 supports_f16: false,
822 supports_bf16: false,
823 supports_tensor_cores: false,
824 max_threads_per_block: 1,
825 max_shared_memory_per_block: 0,
826 multiprocessor_count: 1,
827 compute_capability: (0, 0),
828 };
829 assert!(SmResourceLimits::from_device_capabilities(&caps).is_err());
830 }
831
832 #[test]
833 fn occupancy_for_launch_matches_direct_calculation() {
834 let limits = SmResourceLimits::sm_80();
835 let config = crate::backends::LaunchConfig {
836 grid_size: (128, 1, 1),
837 block_size: (256, 1, 1),
838 shared_memory_size: 0,
839 stream: None,
840 };
841 let result = occupancy_for_launch(&config, 32, &limits).expect("valid launch");
842 assert_eq!(result.active_blocks_per_sm, 8);
843 assert!(approx(result.occupancy, 1.0));
844 }
845}