use crate::error::{CudaError, CudaResult};
use crate::occupancy_ext::{DeviceOccupancyInfo, OccupancyCalculator, OccupancyEstimate};
fn slots_for_type(reg_type: &str) -> RegClass {
let t = reg_type.strip_prefix('.').unwrap_or(reg_type);
match t {
"pred" => RegClass::Predicate,
"b64" | "s64" | "u64" | "f64" => RegClass::General(2),
"b128" => RegClass::General(4),
_ => RegClass::General(1),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RegClass {
General(u32),
Predicate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PtxRegisterUsage {
general_slots: u32,
predicate_count: u32,
directive_count: u32,
}
impl PtxRegisterUsage {
#[must_use]
pub fn registers_per_thread(&self) -> u32 {
self.general_slots
}
#[must_use]
pub fn predicate_registers(&self) -> u32 {
self.predicate_count
}
#[must_use]
pub fn directive_count(&self) -> u32 {
self.directive_count
}
fn add(&mut self, class: RegClass, count: u32) {
self.directive_count = self.directive_count.saturating_add(1);
match class {
RegClass::General(slots) => {
self.general_slots = self
.general_slots
.saturating_add(slots.saturating_mul(count));
}
RegClass::Predicate => {
self.predicate_count = self.predicate_count.saturating_add(count);
}
}
}
}
fn parse_reg_count(name_token: &str) -> Option<u32> {
if let Some(open) = name_token.find('<') {
let close = name_token[open + 1..].find('>')? + open + 1;
let digits = name_token[open + 1..close].trim();
digits.parse::<u32>().ok()
} else {
Some(1)
}
}
fn parse_reg_line(line: &str) -> Option<Vec<(RegClass, u32)>> {
let trimmed = strip_line_comment(line).trim();
let rest = trimmed.strip_prefix(".reg")?;
let rest = rest.trim_start();
let mut tokens = rest.split_whitespace();
let type_token = tokens.next()?;
if !type_token.starts_with('.') {
return None;
}
let class = slots_for_type(type_token);
let names_part = rest[type_token.len()..].trim().trim_end_matches(';');
if names_part.is_empty() {
return None;
}
let mut out = Vec::new();
for name in names_part.split(',') {
let name = name.trim();
if name.is_empty() {
continue;
}
let count = parse_reg_count(name)?;
out.push((class, count));
}
if out.is_empty() { None } else { Some(out) }
}
fn strip_line_comment(line: &str) -> &str {
match line.find("//") {
Some(idx) => &line[..idx],
None => line,
}
}
#[derive(Debug, Clone)]
pub struct PtxKernel {
name: String,
usage: PtxRegisterUsage,
}
impl PtxKernel {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn usage(&self) -> PtxRegisterUsage {
self.usage
}
#[must_use]
pub fn registers_per_thread(&self) -> u32 {
self.usage.registers_per_thread()
}
}
fn extract_entry_name(after_entry: &str) -> Option<String> {
let s = after_entry.trim_start();
let end = s
.find(|c: char| c == '(' || c == '{' || c.is_whitespace())
.unwrap_or(s.len());
let name = &s[..end];
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
#[derive(Debug, Clone, Default)]
pub struct OccupancyFromPtx {
kernels: Vec<PtxKernel>,
}
impl OccupancyFromPtx {
pub fn parse(ptx: &str) -> CudaResult<Self> {
let mut kernels: Vec<PtxKernel> = Vec::new();
let mut current: Option<usize> = None;
let mut brace_depth: i32 = 0;
for raw_line in ptx.lines() {
let line = strip_line_comment(raw_line);
if let Some(pos) = line.find(".entry") {
let after = &line[pos + ".entry".len()..];
if let Some(name) = extract_entry_name(after) {
kernels.push(PtxKernel {
name,
usage: PtxRegisterUsage::default(),
});
current = Some(kernels.len() - 1);
}
}
let opens = line.matches('{').count() as i32;
let closes = line.matches('}').count() as i32;
if current.is_some() {
if let Some(decls) = parse_reg_line(line) {
if let Some(idx) = current {
for (class, count) in decls {
kernels[idx].usage.add(class, count);
}
}
}
}
brace_depth += opens;
brace_depth -= closes;
if brace_depth <= 0 {
brace_depth = 0;
if closes > 0 {
current = None;
}
}
}
if kernels.is_empty() {
return Err(CudaError::InvalidValue);
}
Ok(Self { kernels })
}
#[must_use]
pub fn kernels(&self) -> &[PtxKernel] {
&self.kernels
}
#[must_use]
pub fn kernel(&self, name: &str) -> Option<&PtxKernel> {
self.kernels.iter().find(|k| k.name == name)
}
#[must_use]
pub fn register_usage(&self, name: &str) -> Option<PtxRegisterUsage> {
self.kernel(name).map(PtxKernel::usage)
}
pub fn estimate_for(
&self,
kernel_name: &str,
sm_major: u32,
sm_minor: u32,
block_size: u32,
shared_memory: u32,
) -> CudaResult<OccupancyEstimate> {
let kernel = self.kernel(kernel_name).ok_or(CudaError::NotFound)?;
let info = DeviceOccupancyInfo::for_compute_capability(sm_major, sm_minor);
let calc = OccupancyCalculator::new(info);
Ok(calc.estimate_occupancy(block_size, kernel.registers_per_thread(), shared_memory))
}
pub fn estimate_with_info(
&self,
kernel_name: &str,
info: DeviceOccupancyInfo,
block_size: u32,
shared_memory: u32,
) -> CudaResult<OccupancyEstimate> {
let kernel = self.kernel(kernel_name).ok_or(CudaError::NotFound)?;
let calc = OccupancyCalculator::new(info);
Ok(calc.estimate_occupancy(block_size, kernel.registers_per_thread(), shared_memory))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::occupancy_ext::LimitingFactor;
const SAXPY: &str = r#"
.version 8.0
.target sm_90
.address_size 64
.visible .entry saxpy(
.param .u64 saxpy_param_0
)
{
.reg .f32 %f<8>;
.reg .b32 %r<4>;
.reg .pred %p<2>;
ret;
}
"#;
#[test]
fn parse_reg_count_handles_brackets_and_bare() {
assert_eq!(parse_reg_count("%f<10>"), Some(10));
assert_eq!(parse_reg_count("%rd<4>"), Some(4));
assert_eq!(parse_reg_count("%tid"), Some(1));
assert_eq!(parse_reg_count("%p<3>"), Some(3));
}
#[test]
fn parse_reg_count_rejects_garbage() {
assert_eq!(parse_reg_count("%f<abc>"), None);
assert_eq!(parse_reg_count("%f<"), None);
}
#[test]
fn slots_for_type_matches_register_widths() {
assert_eq!(slots_for_type(".f32"), RegClass::General(1));
assert_eq!(slots_for_type(".b32"), RegClass::General(1));
assert_eq!(slots_for_type(".f64"), RegClass::General(2));
assert_eq!(slots_for_type(".u64"), RegClass::General(2));
assert_eq!(slots_for_type(".b128"), RegClass::General(4));
assert_eq!(slots_for_type(".pred"), RegClass::Predicate);
assert_eq!(slots_for_type("f64"), RegClass::General(2));
}
#[test]
fn parse_reg_line_single_typed() {
let decls = parse_reg_line(".reg .f32 %f<8>;").expect("should parse");
assert_eq!(decls, vec![(RegClass::General(1), 8)]);
}
#[test]
fn parse_reg_line_multi_declaration() {
let decls = parse_reg_line(".reg .b32 %r<4>, %q<3>;").expect("should parse");
assert_eq!(
decls,
vec![(RegClass::General(1), 4), (RegClass::General(1), 3)]
);
}
#[test]
fn parse_reg_line_strips_comment() {
let decls = parse_reg_line(".reg .f64 %fd<2>; // doubles").expect("should parse");
assert_eq!(decls, vec![(RegClass::General(2), 2)]);
}
#[test]
fn parse_reg_line_rejects_non_reg() {
assert!(parse_reg_line(".visible .entry foo() {").is_none());
assert!(parse_reg_line("ret;").is_none());
assert!(parse_reg_line(".version 8.0").is_none());
}
#[test]
fn register_usage_sums_general_slots() {
let from_ptx = OccupancyFromPtx::parse(SAXPY).expect("valid PTX");
let usage = from_ptx.register_usage("saxpy").expect("kernel present");
assert_eq!(usage.registers_per_thread(), 12);
assert_eq!(usage.predicate_registers(), 2);
assert_eq!(usage.directive_count(), 3);
}
#[test]
fn f64_registers_count_double() {
let ptx = r#"
.visible .entry dgemm() {
.reg .f64 %fd<10>;
.reg .b32 %r<2>;
ret;
}
"#;
let from_ptx = OccupancyFromPtx::parse(ptx).expect("valid");
assert_eq!(from_ptx.kernel("dgemm").unwrap().registers_per_thread(), 22);
}
#[test]
fn predicates_do_not_consume_general_registers() {
let ptx = r#"
.visible .entry only_preds() {
.reg .pred %p<16>;
ret;
}
"#;
let from_ptx = OccupancyFromPtx::parse(ptx).expect("valid");
let k = from_ptx.kernel("only_preds").unwrap();
assert_eq!(k.registers_per_thread(), 0);
assert_eq!(k.usage().predicate_registers(), 16);
}
#[test]
fn multiple_kernels_attributed_separately() {
let ptx = r#"
.visible .entry ka() {
.reg .f32 %f<4>;
ret;
}
.visible .entry kb() {
.reg .f32 %f<16>;
.reg .b32 %r<8>;
ret;
}
"#;
let from_ptx = OccupancyFromPtx::parse(ptx).expect("valid");
assert_eq!(from_ptx.kernels().len(), 2);
assert_eq!(from_ptx.kernel("ka").unwrap().registers_per_thread(), 4);
assert_eq!(from_ptx.kernel("kb").unwrap().registers_per_thread(), 24);
}
#[test]
fn module_scope_reg_before_entry_ignored() {
let ptx = r#"
.reg .b32 %global_dummy<4>;
.visible .entry k() {
.reg .f32 %f<2>;
ret;
}
"#;
let from_ptx = OccupancyFromPtx::parse(ptx).expect("valid");
assert_eq!(from_ptx.kernel("k").unwrap().registers_per_thread(), 2);
}
#[test]
fn parse_errors_when_no_entry() {
let ptx = ".version 8.0\n.target sm_70\n";
assert!(matches!(
OccupancyFromPtx::parse(ptx),
Err(CudaError::InvalidValue)
));
}
#[test]
fn estimate_for_produces_nonzero_occupancy() {
let from_ptx = OccupancyFromPtx::parse(SAXPY).expect("valid");
let est = from_ptx
.estimate_for("saxpy", 9, 0, 256, 0)
.expect("kernel present");
assert!(est.occupancy_ratio > 0.0);
assert!(est.active_warps_per_sm <= est.max_warps_per_sm);
}
#[test]
fn estimate_for_unknown_kernel_errors() {
let from_ptx = OccupancyFromPtx::parse(SAXPY).expect("valid");
assert!(matches!(
from_ptx.estimate_for("nope", 9, 0, 256, 0),
Err(CudaError::NotFound)
));
}
#[test]
fn high_register_pressure_lowers_occupancy() {
let heavy_ptx = r#"
.visible .entry heavy() {
.reg .b32 %r<128>;
ret;
}
"#;
let heavy = OccupancyFromPtx::parse(heavy_ptx).expect("valid");
let est_heavy = heavy.estimate_for("heavy", 8, 6, 256, 0).unwrap();
assert_eq!(est_heavy.limiting_factor, LimitingFactor::Registers);
let light_ptx = r#"
.visible .entry light() {
.reg .b32 %r<8>;
ret;
}
"#;
let light = OccupancyFromPtx::parse(light_ptx).expect("valid");
let est_light = light
.estimate_with_info(
"light",
DeviceOccupancyInfo::for_compute_capability(8, 6),
256,
0,
)
.unwrap();
assert_ne!(est_light.limiting_factor, LimitingFactor::Registers);
assert!(est_heavy.occupancy_ratio < est_light.occupancy_ratio);
}
#[test]
fn visible_and_plain_entry_both_parsed() {
let ptx = r#"
.entry plain() {
.reg .f32 %f<3>;
ret;
}
"#;
let from_ptx = OccupancyFromPtx::parse(ptx).expect("valid");
assert_eq!(from_ptx.kernel("plain").unwrap().registers_per_thread(), 3);
}
}