Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 19 additions & 60 deletions cuda-oxide/crates/cuda-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1015,10 +1015,14 @@ pub fn tile_export(attr: TokenStream, item: TokenStream) -> TokenStream {
// NVVM Attributes for Device FFI
// ============================================================================

/// Marks a device function as convergent.
/// Marks an external device function declaration as convergent.
///
/// Convergent functions must be called by all threads in a warp/block together.
/// This prevents the optimizer from moving calls across control flow boundaries.
/// LLVM forbids transformations that add control dependencies to the call,
/// including before external device definitions are linked. The annotation
/// does not make threads reach the call or guarantee reconvergence. Callers
/// must satisfy the function's warp or block participation requirements.
/// Every declaration of the same symbol reachable in a device module must
/// agree on this annotation; conflicting declarations are rejected.
///
/// # When to Use
///
Expand All @@ -1039,77 +1043,32 @@ pub fn tile_export(attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Generated LLVM IR
///
/// ```llvm
/// declare float @cub_block_reduce_sum(float, ptr) #0
/// attributes #0 = { convergent nounwind }
/// declare float @cub_block_reduce_sum(float, ptr) convergent
/// ```
#[proc_macro_attribute]
pub fn convergent(_attr: TokenStream, item: TokenStream) -> TokenStream {
// This is a marker attribute - just pass through the item unchanged.
// The collector will read this attribute and apply the LLVM convergent attribute.
item
// The procedural attribute disappears during expansion. A doc alias is
// retained in metadata even for private foreign items, unlike doc comments.
let marker = reserved_oxide_symbols::DEVICE_EXTERN_CONVERGENT_MARKER;
let item = proc_macro2::TokenStream::from(item);
quote::quote!(#[doc(alias = #marker)] #item).into()
}

/// Marks a device function as pure (no side effects).
///
/// Pure functions only depend on their inputs and have no side effects.
/// This enables aggressive optimizations like CSE and dead code elimination.
///
/// # When to Use
///
/// - Math functions that don't access memory
/// - Functions that compute results purely from input arguments
/// Compatibility annotation for a pure device function.
///
/// # Example
///
/// ```ignore
/// #[device]
/// extern "C" {
/// #[pure]
/// fn fast_rsqrt(x: f32) -> f32;
/// }
/// ```
///
/// # Generated LLVM IR
///
/// ```llvm
/// declare float @fast_rsqrt(float) #0
/// attributes #0 = { nounwind readnone }
/// ```
/// This annotation is currently inactive: it records no compiler effect and
/// emits no LLVM memory-effect attributes.
#[proc_macro_attribute]
pub fn pure(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Marker attribute - collector will read and apply LLVM readnone attribute
item
}

/// Marks a device function as read-only.
/// Compatibility annotation for a read-only device function.
///
/// Read-only functions may read memory but never write to it.
/// This enables optimizations like load hoisting and caching.
///
/// # When to Use
///
/// - Lookup table functions
/// - Functions that only read from input arrays
///
/// # Example
///
/// ```ignore
/// #[device]
/// extern "C" {
/// #[readonly]
/// fn lookup_table(table: *const f32, idx: i32) -> f32;
/// }
/// ```
///
/// # Generated LLVM IR
///
/// ```llvm
/// declare float @lookup_table(ptr, i32) #0
/// attributes #0 = { nounwind readonly }
/// ```
/// This annotation is currently inactive: it records no compiler effect and
/// emits no LLVM memory-effect attributes.
#[proc_macro_attribute]
pub fn readonly(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Marker attribute - collector will read and apply LLVM readonly attribute
item
}

Expand Down
16 changes: 5 additions & 11 deletions cuda-oxide/crates/cuda-oxide-codegen/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,19 @@ pub struct DeviceExternDecl {

/// NVVM attributes for device extern declarations.
///
/// NOTE: These attributes are currently **not emitted** to the LLVM IR output.
/// When linking LTOIR via nvJitLink, the external library's LTOIR already contains
/// proper attributes (convergent, nounwind, memory, etc.) on the function DEFINITIONS.
/// nvJitLink uses the definition's attributes during LTO, making attributes on our
/// declarations redundant.
///
/// This struct is retained for the pipeline API but values are not used in code generation.
/// Convergence is emitted on each marked declaration before libNVVM optimization.
/// Memory-effect annotations remain inactive.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct DeviceExternAttrs {
/// Function is convergent (all threads must execute together).
/// NOTE: Not currently emitted to LLVM IR.
/// Function has convergence requirements for its participating threads.
pub is_convergent: bool,

/// Function is pure (no side effects, result depends only on inputs).
/// NOTE: Not currently emitted to LLVM IR.
/// Inactive; not emitted to LLVM IR.
pub is_pure: bool,

/// Function is read-only (only reads memory, doesn't write).
/// NOTE: Not currently emitted to LLVM IR.
/// Inactive; not emitted to LLVM IR.
pub is_readonly: bool,
}

Expand Down
62 changes: 61 additions & 1 deletion cuda-oxide/crates/cuda-oxide-codegen/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ pub fn add_device_extern_declarations(
region.iter(ctx).next().expect("Module should have a block")
};

let declared_symbols: HashMap<_, _> = block
let mut declared_symbols: HashMap<_, _> = block
.deref(ctx)
.iter(ctx)
.filter_map(|op| {
Expand Down Expand Up @@ -140,6 +140,7 @@ pub fn add_device_extern_declarations(

// Insert at the front of the module (declarations come before definitions)
func_op.get_operation().insert_at_front(block, ctx);
declared_symbols.insert(decl.export_name.clone(), func_type);
}

Ok(())
Expand Down Expand Up @@ -196,3 +197,62 @@ fn device_extern_type_to_pliron(
}
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::export::DeviceExternAttrs;
use llvm_export::export::{NvvmExportConfig, export_module_with_externs};
use pliron::builtin::ops::ModuleOp;
use pliron::common_traits::Verify;

#[test]
fn repeated_externs_keep_one_operation_and_preserve_conflict_checks() {
let mut ctx = Context::new();
let module = ModuleOp::new(&mut ctx, "externs".try_into().unwrap());
let declaration = DeviceExternDecl {
export_name: "external".to_string(),
param_types: vec![DeviceExternType::Integer(32)],
return_type: DeviceExternType::Void,
attrs: DeviceExternAttrs::default(),
};
let mut externs = [declaration.clone(), declaration];
add_device_extern_declarations(&mut ctx, module.get_operation(), &externs).unwrap();
let declarations = {
let region = module.get_operation().deref(&ctx).get_region(0).deref(&ctx);
let block = region.iter(&ctx).next().unwrap();
block
.deref(&ctx)
.iter(&ctx)
.filter(|&op| {
Operation::get_op::<llvm_export::ops::FuncOp>(op, &ctx).is_some_and(
|function| function.get_symbol_name(&ctx).to_string() == "external",
)
})
.count()
};
assert_eq!(declarations, 1);
module.get_operation().deref(&ctx).verify(&ctx).unwrap();
let ir = export_module_with_externs(&ctx, &module, &externs, &NvvmExportConfig::default())
.unwrap();
assert_eq!(ir.matches("declare void @external(i32)").count(), 1);

// Lowering shares only the FuncOp. The complete declarations still
// reach export, where differing convergence requirements are rejected.
externs[1].attrs.is_convergent = true;
add_device_extern_declarations(&mut ctx, module.get_operation(), &externs).unwrap();
let error =
export_module_with_externs(&ctx, &module, &externs, &NvvmExportConfig::default())
.unwrap_err();
assert!(error.contains("convergent is false versus true"), "{error}");

externs[1].param_types[0] = DeviceExternType::Float32;
let error =
add_device_extern_declarations(&mut ctx, module.get_operation(), &externs).unwrap_err();
assert!(
error
.to_string()
.contains("conflicts with the call-site declaration")
);
}
}
16 changes: 6 additions & 10 deletions cuda-oxide/crates/llvm-export/src/export/externs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,22 +225,18 @@ pub struct DeviceExternDecl {

/// NVVM attributes for device extern declarations.
///
/// NOTE: These attributes are currently **not emitted** to the LLVM IR output.
/// When linking LTOIR via nvJitLink, the external library's LTOIR already contains
/// proper attributes (convergent, nounwind, memory, etc.) on the function DEFINITIONS.
/// nvJitLink uses the definition's attributes during LTO, making attributes on
/// declarations redundant.
///
/// This struct is retained for potential future use or for debugging/inspection.
/// Convergence is emitted on the declaration before libNVVM optimization.
/// Memory-effect annotations remain inactive; they are not inferred from external
/// definitions that become available only during LTO.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct DeviceExternAttrs {
/// Function is convergent (all threads must execute together).
/// Function has convergence requirements for its participating threads.
pub is_convergent: bool,

/// Function is pure (no side effects). Maps to LLVM `readnone`.
/// Function is pure (no side effects). Inactive for Rust source annotations.
pub is_pure: bool,

/// Function is read-only (only reads memory). Maps to LLVM `readonly`.
/// Function is read-only (only reads memory). Inactive for Rust source annotations.
pub is_readonly: bool,
}

Expand Down
28 changes: 18 additions & 10 deletions cuda-oxide/crates/llvm-export/src/export/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,10 +487,17 @@ fn index_device_externs(
validate_device_extern_function_shape(state, decl)?;
if let Some(previous) = state.device_externs.get(&decl.export_name) {
if previous != decl {
return Err(format!(
let conflict = format!(
"conflicting device-extern declarations for `@{}`",
decl.export_name
));
);
if previous.attrs.is_convergent != decl.attrs.is_convergent {
return Err(format!(
"{conflict}: convergent is {} versus {}",
previous.attrs.is_convergent, decl.attrs.is_convergent
));
}
return Err(conflict);
}
continue;
}
Expand Down Expand Up @@ -610,12 +617,9 @@ pub(super) fn export_module_with_externs_impl(

// 2. Device extern declarations (before function definitions)
//
// NOTE: We intentionally do NOT emit LLVM attributes on these declarations.
// The external LTOIR (from nvcc -dc -dlto) already contains proper attributes
// (convergent, nounwind, memory, etc.) on the function DEFINITIONS.
// When nvJitLink performs LTO linking, it uses the definition's attributes.
// Attributes on declarations are redundant and were causing issues where
// all externs incorrectly got the same attribute group.
// Convergence must be visible before libNVVM optimizes this module, not
// only when external definitions arrive at LTO. Emit it per declaration
// so unrelated externs do not inherit a shared attribute group.
if !device_externs.is_empty() {
writeln!(
&mut output,
Expand Down Expand Up @@ -643,7 +647,11 @@ pub(super) fn export_module_with_externs_impl(
}
param.write_llvm_with_attr(&mut output, state.legacy_typed_pointers())?;
}
writeln!(&mut output, ")").unwrap();
write!(&mut output, ")").unwrap();
if decl.attrs.is_convergent {
write!(&mut output, " convergent").unwrap();
}
writeln!(&mut output).unwrap();
}
writeln!(&mut output).unwrap();
}
Expand Down Expand Up @@ -711,7 +719,7 @@ pub(super) fn export_module_with_externs_impl(
}

// 6. Emit attribute groups for convergent intrinsics used by module functions
// Note: Device extern declarations no longer get attribute groups - see section 2 comment.
// Device extern convergence is emitted inline, independently of this group.
if state.convergent_used {
writeln!(&mut output).unwrap();
writeln!(&mut output, "attributes #0 = {{ convergent }}").unwrap();
Expand Down
Loading
Loading