use std::fmt::Write as FmtWrite;
use crate::arch::SmVersion;
use crate::error::PtxGenError;
pub const CP_ASYNC_MIN_SM: SmVersion = SmVersion::Sm80;
const CG_REQUIRED_BYTES: u32 = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CpAsyncCachePolicy {
CacheAll,
CacheGlobal,
}
impl CpAsyncCachePolicy {
#[must_use]
pub const fn modifier(self) -> &'static str {
match self {
Self::CacheAll => "ca",
Self::CacheGlobal => "cg",
}
}
#[must_use]
pub const fn resolve_for_bytes(self, bytes: u32) -> Self {
match self {
Self::CacheGlobal if bytes == CG_REQUIRED_BYTES => Self::CacheGlobal,
Self::CacheGlobal | Self::CacheAll => Self::CacheAll,
}
}
}
#[derive(Debug, Clone)]
pub struct CpAsyncGenerator {
pub cache_policy: CpAsyncCachePolicy,
pub bypass_bytes: u32,
pub stages: u32,
}
impl CpAsyncGenerator {
pub fn new(
cache_policy: CpAsyncCachePolicy,
bypass_bytes: u32,
stages: u32,
) -> Result<Self, PtxGenError> {
if !matches!(bypass_bytes, 4 | 8 | 16) {
return Err(PtxGenError::GenerationFailed(format!(
"cp.async copy size must be 4, 8, or 16 bytes, got {bypass_bytes}"
)));
}
if stages < 2 {
return Err(PtxGenError::GenerationFailed(format!(
"cp.async pipeline needs at least 2 stages, got {stages}"
)));
}
Ok(Self {
cache_policy,
bypass_bytes,
stages,
})
}
#[must_use]
pub const fn effective_policy(&self) -> CpAsyncCachePolicy {
self.cache_policy.resolve_for_bytes(self.bypass_bytes)
}
#[must_use]
pub const fn in_flight_groups(&self) -> u32 {
self.stages - 1
}
#[must_use]
pub fn supports_async(sm: SmVersion) -> bool {
sm >= CP_ASYNC_MIN_SM && sm.capabilities().has_cp_async
}
#[must_use]
pub fn copy_opcode(&self) -> String {
let policy = self.effective_policy();
match policy {
CpAsyncCachePolicy::CacheGlobal => {
"cp.async.cg.global.L2::128B".to_string()
}
CpAsyncCachePolicy::CacheAll => "cp.async.ca.global".to_string(),
}
}
#[must_use]
pub fn generate_copy(&self, sm: SmVersion, dst_shared: &str, src_global: &str) -> String {
if Self::supports_async(sm) {
format!(
"{} [{dst_shared}], [{src_global}], {};\n",
self.copy_opcode(),
self.bypass_bytes
)
} else {
self.generate_sync_fallback(dst_shared, src_global)
}
}
#[must_use]
pub fn generate_sync_fallback(&self, dst_shared: &str, src_global: &str) -> String {
let mut out = String::with_capacity(128);
out.push_str(" // async copy unavailable (< sm_80): synchronous fallback\n");
match self.bypass_bytes {
4 => {
let _ = writeln!(out, " ld.global.b32 %cpa_tmp_b32, [{src_global}];");
let _ = writeln!(out, " st.shared.b32 [{dst_shared}], %cpa_tmp_b32;");
}
8 => {
let _ = writeln!(out, " ld.global.b64 %cpa_tmp_b64, [{src_global}];");
let _ = writeln!(out, " st.shared.b64 [{dst_shared}], %cpa_tmp_b64;");
}
_ => {
let _ = writeln!(
out,
" ld.global.v4.b32 {{%cpa_v0, %cpa_v1, %cpa_v2, %cpa_v3}}, [{src_global}];"
);
let _ = writeln!(
out,
" st.shared.v4.b32 [{dst_shared}], {{%cpa_v0, %cpa_v1, %cpa_v2, %cpa_v3}};"
);
}
}
out
}
#[must_use]
pub fn generate_commit(sm: SmVersion) -> String {
if Self::supports_async(sm) {
" cp.async.commit_group;\n".to_string()
} else {
" // cp.async.commit_group elided (< sm_80)\n".to_string()
}
}
#[must_use]
pub fn generate_wait(sm: SmVersion, n: u32) -> String {
if Self::supports_async(sm) {
format!(" cp.async.wait_group {n};\n")
} else {
" // cp.async.wait_group elided (< sm_80)\n".to_string()
}
}
#[must_use]
pub fn generate_wait_all(sm: SmVersion) -> String {
Self::generate_wait(sm, 0)
}
#[must_use]
pub fn generate_pipeline_loop(&self, sm: SmVersion) -> String {
let mut out = String::with_capacity(1024);
let async_ok = Self::supports_async(sm);
let wait_n = self.in_flight_groups();
let _ = writeln!(
out,
" // === cp.async {}-stage software pipeline ({}, {} B/copy) ===",
self.stages,
self.effective_policy().modifier(),
self.bypass_bytes
);
let _ = writeln!(out, " // Prologue: prime {wait_n} pipeline stage(s)");
for stage in 0..wait_n {
let _ = writeln!(out, " // prologue stage {stage}");
out.push_str(" ");
out.push_str(&self.generate_copy(sm, "%stage_dst", "%stage_src"));
out.push_str(&Self::generate_commit(sm));
}
let _ = writeln!(out, "$CP_ASYNC_LOOP:");
let _ = writeln!(
out,
" // Wait until only {wait_n} group(s) remain in flight, then consume the oldest"
);
out.push_str(&Self::generate_wait(sm, wait_n));
if async_ok {
let _ = writeln!(
out,
" bar.sync 0; // ensure the awaited stage is visible to all threads"
);
}
let _ = writeln!(out, " // <consume arrived stage here>");
let _ = writeln!(out, " // Issue the next stage into the rotating buffer");
out.push_str(" ");
out.push_str(&self.generate_copy(sm, "%stage_dst", "%stage_src"));
out.push_str(&Self::generate_commit(sm));
let _ = writeln!(out, " // loop-control: @%p_more bra $CP_ASYNC_LOOP;");
let _ = writeln!(out, " // Epilogue: drain all remaining in-flight groups");
out.push_str(&Self::generate_wait_all(sm));
if async_ok {
let _ = writeln!(out, " bar.sync 0;");
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_valid_sizes() {
for bytes in [4u32, 8, 16] {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, bytes, 2);
assert!(generator.is_ok(), "{bytes} bytes should be valid");
}
}
#[test]
fn test_new_rejects_bad_size() {
for bytes in [0u32, 1, 2, 3, 5, 12, 32] {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, bytes, 2);
assert!(generator.is_err(), "{bytes} bytes must be rejected");
}
}
#[test]
fn test_new_rejects_too_few_stages() {
for stages in [0u32, 1] {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, stages);
assert!(generator.is_err(), "{stages} stages must be rejected");
}
assert!(CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, 2).is_ok());
}
#[test]
fn test_policy_modifier_tokens() {
assert_eq!(CpAsyncCachePolicy::CacheAll.modifier(), "ca");
assert_eq!(CpAsyncCachePolicy::CacheGlobal.modifier(), "cg");
}
#[test]
fn test_cg_resolves_to_ca_below_16_bytes() {
assert_eq!(
CpAsyncCachePolicy::CacheGlobal.resolve_for_bytes(4),
CpAsyncCachePolicy::CacheAll
);
assert_eq!(
CpAsyncCachePolicy::CacheGlobal.resolve_for_bytes(8),
CpAsyncCachePolicy::CacheAll
);
assert_eq!(
CpAsyncCachePolicy::CacheGlobal.resolve_for_bytes(16),
CpAsyncCachePolicy::CacheGlobal
);
}
#[test]
fn test_effective_policy_follows_size() {
let cg16 = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, 3)
.expect("valid cp.async config");
assert_eq!(cg16.effective_policy(), CpAsyncCachePolicy::CacheGlobal);
let cg8 = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 8, 3)
.expect("valid cp.async config");
assert_eq!(cg8.effective_policy(), CpAsyncCachePolicy::CacheAll);
}
#[test]
fn test_copy_opcode_cg16_has_l2_hint() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, 3)
.expect("valid cp.async config");
assert_eq!(generator.copy_opcode(), "cp.async.cg.global.L2::128B");
}
#[test]
fn test_copy_opcode_ca_has_no_l2_hint() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, 16, 3)
.expect("valid cp.async config");
assert_eq!(generator.copy_opcode(), "cp.async.ca.global");
assert!(!generator.copy_opcode().contains("L2::128B"));
}
#[test]
fn test_copy_opcode_cg8_downgrades_to_ca() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 8, 3)
.expect("valid cp.async config");
assert_eq!(generator.copy_opcode(), "cp.async.ca.global");
assert!(!generator.copy_opcode().contains("cg"));
}
#[test]
fn test_generate_copy_cg16_structure() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, 3)
.expect("valid cp.async config");
let ptx = generator.generate_copy(SmVersion::Sm80, "%sh", "%gl");
assert!(ptx.contains("cp.async.cg.global.L2::128B"));
assert!(ptx.contains("[%sh], [%gl], 16;"));
}
#[test]
fn test_generate_copy_ca_sizes() {
for bytes in [4u32, 8, 16] {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, bytes, 3)
.expect("valid cp.async config");
let ptx = generator.generate_copy(SmVersion::Sm90, "%sh", "%gl");
assert!(ptx.contains("cp.async.ca.global"));
assert!(
ptx.contains(&format!("[%sh], [%gl], {bytes};")),
"byte size {bytes} must appear in operand list: {ptx}"
);
}
}
#[test]
fn test_generate_copy_supported_on_all_async_archs() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, 2)
.expect("valid cp.async config");
for sm in [
SmVersion::Sm80,
SmVersion::Sm86,
SmVersion::Sm89,
SmVersion::Sm90,
SmVersion::Sm90a,
SmVersion::Sm100,
SmVersion::Sm120,
] {
let ptx = generator.generate_copy(sm, "%sh", "%gl");
assert!(
ptx.contains("cp.async"),
"sm {sm} should emit a cp.async copy"
);
assert!(!ptx.contains("fallback"), "sm {sm} should not fall back");
}
}
#[test]
fn test_supports_async_gate() {
assert!(!CpAsyncGenerator::supports_async(SmVersion::Sm75));
assert!(CpAsyncGenerator::supports_async(SmVersion::Sm80));
assert!(CpAsyncGenerator::supports_async(SmVersion::Sm90));
}
#[test]
fn test_fallback_on_sm75_no_cp_async() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, 3)
.expect("valid cp.async config");
let ptx = generator.generate_copy(SmVersion::Sm75, "%sh", "%gl");
assert!(!ptx.contains("cp.async"));
assert!(ptx.contains("ld.global.v4.b32"));
assert!(ptx.contains("st.shared.v4.b32"));
}
#[test]
fn test_fallback_widths_match_size() {
let cases = [
(4u32, "ld.global.b32", "st.shared.b32"),
(8u32, "ld.global.b64", "st.shared.b64"),
(16u32, "ld.global.v4.b32", "st.shared.v4.b32"),
];
for (bytes, ld, st) in cases {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, bytes, 2)
.expect("valid cp.async config");
let ptx = generator.generate_copy(SmVersion::Sm75, "%sh", "%gl");
assert!(
ptx.contains(ld),
"{bytes} B fallback should use {ld}: {ptx}"
);
assert!(
ptx.contains(st),
"{bytes} B fallback should use {st}: {ptx}"
);
}
}
#[test]
fn test_commit_and_wait_async() {
assert_eq!(
CpAsyncGenerator::generate_commit(SmVersion::Sm80),
" cp.async.commit_group;\n"
);
assert_eq!(
CpAsyncGenerator::generate_wait(SmVersion::Sm80, 2),
" cp.async.wait_group 2;\n"
);
assert_eq!(
CpAsyncGenerator::generate_wait_all(SmVersion::Sm90),
" cp.async.wait_group 0;\n"
);
}
#[test]
fn test_commit_and_wait_elided_pre_ampere() {
assert!(
!CpAsyncGenerator::generate_commit(SmVersion::Sm75).contains("cp.async.commit_group;")
);
assert!(
!CpAsyncGenerator::generate_wait(SmVersion::Sm75, 2).contains("cp.async.wait_group 2;")
);
}
#[test]
fn test_in_flight_groups_is_stages_minus_one() {
assert_eq!(
CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, 16, 2)
.expect("valid cp.async config")
.in_flight_groups(),
1
);
assert_eq!(
CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, 16, 4)
.expect("valid cp.async config")
.in_flight_groups(),
3
);
}
#[test]
fn test_pipeline_loop_has_all_phases() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, 3)
.expect("valid cp.async config");
let ptx = generator.generate_pipeline_loop(SmVersion::Sm80);
assert!(ptx.contains("Prologue"));
assert!(ptx.contains("$CP_ASYNC_LOOP:"));
assert!(ptx.contains("Epilogue"));
assert!(ptx.contains("cp.async.commit_group"));
assert!(ptx.contains("cp.async.wait_group"));
assert!(ptx.contains("cp.async.cg.global.L2::128B"));
}
#[test]
fn test_pipeline_loop_wait_group_uses_stages_minus_one() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, 16, 4)
.expect("valid cp.async config");
let ptx = generator.generate_pipeline_loop(SmVersion::Sm80);
assert!(
ptx.contains("cp.async.wait_group 3;"),
"steady-state wait must drain to stages-1 groups: {ptx}"
);
assert!(ptx.contains("cp.async.wait_group 0;"));
}
#[test]
fn test_pipeline_prologue_commit_count_matches_warmup_stages() {
let stages = 3u32;
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, stages)
.expect("valid cp.async config");
let ptx = generator.generate_pipeline_loop(SmVersion::Sm80);
let commit_count = ptx.matches("cp.async.commit_group;").count();
assert_eq!(
commit_count, stages as usize,
"expected {stages} commit_group instructions, got {commit_count}"
);
}
#[test]
fn test_pipeline_loop_falls_back_pre_ampere() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheGlobal, 16, 3)
.expect("valid cp.async config");
let ptx = generator.generate_pipeline_loop(SmVersion::Sm75);
assert!(!ptx.contains("cp.async.cg"));
assert!(!ptx.contains("cp.async.commit_group;"));
assert!(!ptx.contains("cp.async.wait_group 0;"));
assert!(ptx.contains("ld.global.v4.b32"));
assert!(ptx.contains("st.shared.v4.b32"));
}
#[test]
fn test_pipeline_loop_two_stage_minimal() {
let generator = CpAsyncGenerator::new(CpAsyncCachePolicy::CacheAll, 8, 2)
.expect("valid cp.async config");
let ptx = generator.generate_pipeline_loop(SmVersion::Sm86);
assert!(ptx.contains("cp.async.wait_group 1;"));
let commit_count = ptx.matches("cp.async.commit_group;").count();
assert_eq!(commit_count, 2, "2-stage fragment commits prologue + body");
}
}