Skip to main content

oxicuda_launch/
dynamic_parallelism.rs

1//! Dynamic parallelism support for device-side kernel launches.
2//!
3//! CUDA dynamic parallelism allows kernels running on the GPU to launch
4//! child kernels without returning to the host. This module provides
5//! configuration, planning, and PTX code generation for nested kernel
6//! launches.
7//!
8//! # Architecture requirements
9//!
10//! Dynamic parallelism requires compute capability 3.5+ (sm_35). All
11//! [`SmVersion`] variants in this crate are sm_75+, so they all support
12//! dynamic parallelism.
13//!
14//! # CUDA nesting limits
15//!
16//! - Maximum nesting depth: 24
17//! - Default pending launch limit: 2048
18//! - Each pending launch consumes device memory for bookkeeping
19//!
20//! # Example
21//!
22//! ```rust
23//! use oxicuda_launch::dynamic_parallelism::{
24//!     DynamicParallelismConfig, ChildKernelSpec, GridSpec,
25//!     validate_dynamic_config, plan_dynamic_launch,
26//!     generate_child_launch_ptx, generate_device_sync_ptx,
27//!     estimate_launch_overhead, max_nesting_for_sm,
28//! };
29//! use oxicuda_launch::Dim3;
30//! use oxicuda_ptx::arch::SmVersion;
31//! use oxicuda_ptx::PtxType;
32//!
33//! let config = DynamicParallelismConfig {
34//!     max_nesting_depth: 4,
35//!     max_pending_launches: 2048,
36//!     sync_depth: 2,
37//!     child_grid: Dim3::x(128),
38//!     child_block: Dim3::x(256),
39//!     child_shared_mem: 0,
40//!     sm_version: SmVersion::Sm80,
41//! };
42//!
43//! validate_dynamic_config(&config).ok();
44//! let plan = plan_dynamic_launch(&config).ok();
45//!
46//! let child = ChildKernelSpec {
47//!     name: "child_kernel".to_string(),
48//!     param_types: vec![PtxType::U64, PtxType::U32],
49//!     grid_dim: GridSpec::Fixed(Dim3::x(128)),
50//!     block_dim: Dim3::x(256),
51//!     shared_mem_bytes: 0,
52//! };
53//!
54//! let ptx = generate_child_launch_ptx("parent_kernel", &child, SmVersion::Sm80);
55//! let sync_ptx = generate_device_sync_ptx(SmVersion::Sm80);
56//! let overhead = estimate_launch_overhead(4, 2048);
57//! let max_depth = max_nesting_for_sm(SmVersion::Sm80);
58//! ```
59
60use std::fmt;
61
62use oxicuda_ptx::PtxType;
63use oxicuda_ptx::arch::SmVersion;
64use oxicuda_ptx::error::PtxGenError;
65
66use crate::error::LaunchError;
67use crate::grid::Dim3;
68
69// ---------------------------------------------------------------------------
70// Constants
71// ---------------------------------------------------------------------------
72
73/// Maximum nesting depth allowed by CUDA hardware.
74const CUDA_MAX_NESTING_DEPTH: u32 = 24;
75
76/// Default maximum number of pending (un-synchronized) child launches.
77const DEFAULT_MAX_PENDING_LAUNCHES: u32 = 2048;
78
79/// Base memory overhead per pending launch in bytes.
80/// This accounts for the device-side launch descriptor, parameter storage,
81/// and internal bookkeeping structures.
82const BASE_LAUNCH_OVERHEAD_BYTES: u64 = 2048;
83
84/// Additional overhead per nesting level in bytes.
85/// Deeper nesting requires additional stack frames and synchronization state.
86const PER_DEPTH_OVERHEAD_BYTES: u64 = 4096;
87
88// ---------------------------------------------------------------------------
89// DynamicParallelismConfig
90// ---------------------------------------------------------------------------
91
92/// Configuration for dynamic parallelism (device-side kernel launches).
93///
94/// Controls nesting depth, pending launch limits, synchronization behavior,
95/// and child kernel launch dimensions.
96///
97/// # CUDA constraints
98///
99/// - `max_nesting_depth` must be in `1..=24`.
100/// - `max_pending_launches` must be at least 1.
101/// - `sync_depth` must be less than or equal to `max_nesting_depth`.
102/// - All grid and block dimensions must be non-zero.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct DynamicParallelismConfig {
105    /// Maximum nesting depth for child kernel launches (CUDA limit: 24).
106    pub max_nesting_depth: u32,
107    /// Maximum number of pending (un-synchronized) child launches (default 2048).
108    pub max_pending_launches: u32,
109    /// Depth at which to insert synchronization barriers.
110    ///
111    /// Child kernels launched at depths >= `sync_depth` will synchronize
112    /// before returning to the parent, preventing unbounded pending launches.
113    pub sync_depth: u32,
114    /// Grid dimensions for child kernel launches.
115    pub child_grid: Dim3,
116    /// Block dimensions for child kernel launches.
117    pub child_block: Dim3,
118    /// Dynamic shared memory allocation for child kernels (bytes).
119    pub child_shared_mem: u32,
120    /// Target GPU architecture.
121    pub sm_version: SmVersion,
122}
123
124impl DynamicParallelismConfig {
125    /// Creates a new configuration with default values.
126    ///
127    /// Defaults:
128    /// - `max_nesting_depth`: 4
129    /// - `max_pending_launches`: 2048
130    /// - `sync_depth`: 2
131    /// - `child_grid`: 128 blocks
132    /// - `child_block`: 256 threads
133    /// - `child_shared_mem`: 0
134    #[must_use]
135    pub fn new(sm_version: SmVersion) -> Self {
136        Self {
137            max_nesting_depth: 4,
138            max_pending_launches: DEFAULT_MAX_PENDING_LAUNCHES,
139            sync_depth: 2,
140            child_grid: Dim3::x(128),
141            child_block: Dim3::x(256),
142            child_shared_mem: 0,
143            sm_version,
144        }
145    }
146}
147
148impl fmt::Display for DynamicParallelismConfig {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(
151            f,
152            "DynParallelism(depth={}, pending={}, sync@{}, grid={}, block={}, smem={}, {})",
153            self.max_nesting_depth,
154            self.max_pending_launches,
155            self.sync_depth,
156            self.child_grid,
157            self.child_block,
158            self.child_shared_mem,
159            self.sm_version,
160        )
161    }
162}
163
164// ---------------------------------------------------------------------------
165// DynamicLaunchPlan
166// ---------------------------------------------------------------------------
167
168/// A validated plan for a dynamic (device-side) kernel launch.
169///
170/// Contains the configuration, kernel names, and estimated resource usage.
171/// Created by [`plan_dynamic_launch`].
172#[derive(Debug, Clone)]
173pub struct DynamicLaunchPlan {
174    /// The validated configuration.
175    pub config: DynamicParallelismConfig,
176    /// Name of the parent kernel that launches child kernels.
177    pub parent_kernel_name: String,
178    /// Name of the child kernel to be launched from device code.
179    pub child_kernel_name: String,
180    /// Estimated total number of child kernel launches.
181    pub estimated_child_launches: u64,
182    /// Estimated memory overhead per launch in bytes.
183    pub memory_overhead_bytes: u64,
184}
185
186impl fmt::Display for DynamicLaunchPlan {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        write!(
189            f,
190            "DynamicLaunchPlan {{ parent: '{}', child: '{}', \
191             est_launches: {}, overhead: {} bytes, config: {} }}",
192            self.parent_kernel_name,
193            self.child_kernel_name,
194            self.estimated_child_launches,
195            self.memory_overhead_bytes,
196            self.config,
197        )
198    }
199}
200
201// ---------------------------------------------------------------------------
202// ChildKernelSpec
203// ---------------------------------------------------------------------------
204
205/// Specification for a child kernel to be launched from device code.
206///
207/// Describes the kernel signature, grid/block dimensions, and shared
208/// memory requirements needed to generate the device-side launch PTX.
209#[derive(Debug, Clone)]
210pub struct ChildKernelSpec {
211    /// Name of the child kernel function.
212    pub name: String,
213    /// PTX types of the kernel parameters, in order.
214    pub param_types: Vec<PtxType>,
215    /// How the grid dimensions are determined.
216    pub grid_dim: GridSpec,
217    /// Block dimensions (threads per block).
218    pub block_dim: Dim3,
219    /// Dynamic shared memory in bytes.
220    pub shared_mem_bytes: u32,
221}
222
223// ---------------------------------------------------------------------------
224// GridSpec
225// ---------------------------------------------------------------------------
226
227/// Specifies how child kernel grid dimensions are determined.
228///
229/// Device-side kernel launches can use fixed grid sizes, data-dependent
230/// sizes derived from kernel parameters, or per-thread launches.
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub enum GridSpec {
233    /// A constant grid size known at code generation time.
234    Fixed(Dim3),
235    /// Grid size derived from a kernel parameter at runtime.
236    ///
237    /// The `param_index` identifies which parameter of the parent kernel
238    /// contains the element count. The generated PTX computes the grid
239    /// size as `ceil(param / block_size)`.
240    DataDependent {
241        /// Index of the parent kernel parameter holding the element count.
242        param_index: u32,
243    },
244    /// Launch one child kernel per thread in the parent kernel.
245    ///
246    /// Each thread in the parent launches exactly one child grid.
247    /// The child grid size is typically 1 block.
248    ThreadDependent,
249}
250
251impl fmt::Display for GridSpec {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        match self {
254            Self::Fixed(dim) => write!(f, "Fixed({dim})"),
255            Self::DataDependent { param_index } => {
256                write!(f, "DataDependent(param[{param_index}])")
257            }
258            Self::ThreadDependent => write!(f, "ThreadDependent"),
259        }
260    }
261}
262
263// ---------------------------------------------------------------------------
264// Validation
265// ---------------------------------------------------------------------------
266
267/// Validates a dynamic parallelism configuration.
268///
269/// Checks all CUDA hardware constraints:
270/// - Nesting depth must be in `1..=24`.
271/// - Pending launches must be at least 1.
272/// - Sync depth must not exceed nesting depth.
273/// - All child grid and block dimensions must be non-zero.
274/// - Total threads per child block must not exceed the architecture limit.
275/// - Child shared memory must not exceed the architecture limit.
276///
277/// # Errors
278///
279/// Returns [`LaunchError`] describing the first constraint violation found.
280pub fn validate_dynamic_config(config: &DynamicParallelismConfig) -> Result<(), LaunchError> {
281    // Nesting depth
282    if config.max_nesting_depth == 0 || config.max_nesting_depth > CUDA_MAX_NESTING_DEPTH {
283        return Err(LaunchError::InvalidDimension {
284            dim: "max_nesting_depth",
285            value: config.max_nesting_depth,
286        });
287    }
288
289    // Pending launches
290    if config.max_pending_launches == 0 {
291        return Err(LaunchError::InvalidDimension {
292            dim: "max_pending_launches",
293            value: 0,
294        });
295    }
296
297    // Sync depth
298    if config.sync_depth > config.max_nesting_depth {
299        return Err(LaunchError::InvalidDimension {
300            dim: "sync_depth",
301            value: config.sync_depth,
302        });
303    }
304
305    // Child grid dimensions
306    if config.child_grid.x == 0 {
307        return Err(LaunchError::InvalidDimension {
308            dim: "child_grid.x",
309            value: 0,
310        });
311    }
312    if config.child_grid.y == 0 {
313        return Err(LaunchError::InvalidDimension {
314            dim: "child_grid.y",
315            value: 0,
316        });
317    }
318    if config.child_grid.z == 0 {
319        return Err(LaunchError::InvalidDimension {
320            dim: "child_grid.z",
321            value: 0,
322        });
323    }
324
325    // Child block dimensions
326    if config.child_block.x == 0 {
327        return Err(LaunchError::InvalidDimension {
328            dim: "child_block.x",
329            value: 0,
330        });
331    }
332    if config.child_block.y == 0 {
333        return Err(LaunchError::InvalidDimension {
334            dim: "child_block.y",
335            value: 0,
336        });
337    }
338    if config.child_block.z == 0 {
339        return Err(LaunchError::InvalidDimension {
340            dim: "child_block.z",
341            value: 0,
342        });
343    }
344
345    // Block size limit
346    let max_threads = config.sm_version.max_threads_per_block();
347    let block_total = config.child_block.total();
348    if block_total > max_threads {
349        return Err(LaunchError::BlockSizeExceedsLimit {
350            requested: block_total,
351            max: max_threads,
352        });
353    }
354
355    // Shared memory limit
356    let max_smem = config.sm_version.max_shared_mem_per_block();
357    if config.child_shared_mem > max_smem {
358        return Err(LaunchError::SharedMemoryExceedsLimit {
359            requested: config.child_shared_mem,
360            max: max_smem,
361        });
362    }
363
364    Ok(())
365}
366
367// ---------------------------------------------------------------------------
368// Planning
369// ---------------------------------------------------------------------------
370
371/// Creates a validated launch plan from a dynamic parallelism configuration.
372///
373/// Validates the configuration, then estimates the number of child launches
374/// and per-launch memory overhead. The parent and child kernel names are
375/// generated from the configuration.
376///
377/// # Errors
378///
379/// Returns [`LaunchError`] if the configuration is invalid.
380pub fn plan_dynamic_launch(
381    config: &DynamicParallelismConfig,
382) -> Result<DynamicLaunchPlan, LaunchError> {
383    validate_dynamic_config(config)?;
384
385    let parent_grid_total = config.child_grid.total() as u64;
386    let estimated_child_launches =
387        parent_grid_total.saturating_mul(config.child_block.total() as u64);
388    let memory_overhead_bytes =
389        estimate_launch_overhead(config.max_nesting_depth, config.max_pending_launches);
390
391    Ok(DynamicLaunchPlan {
392        config: config.clone(),
393        parent_kernel_name: String::from("parent_kernel"),
394        child_kernel_name: String::from("child_kernel"),
395        estimated_child_launches,
396        memory_overhead_bytes,
397    })
398}
399
400// ---------------------------------------------------------------------------
401// Overhead estimation
402// ---------------------------------------------------------------------------
403
404/// Estimates the device memory overhead for dynamic parallelism in bytes.
405///
406/// The overhead comes from:
407/// - Per-launch descriptors (`BASE_LAUNCH_OVERHEAD_BYTES` per pending launch)
408/// - Per-depth stack and synchronization state (`PER_DEPTH_OVERHEAD_BYTES` per level)
409///
410/// # Arguments
411///
412/// - `depth` — maximum nesting depth
413/// - `pending` — maximum number of pending (un-synchronized) launches
414///
415/// # Returns
416///
417/// Estimated total overhead in bytes.
418pub fn estimate_launch_overhead(depth: u32, pending: u32) -> u64 {
419    let per_launch = BASE_LAUNCH_OVERHEAD_BYTES.saturating_mul(pending as u64);
420    let per_depth = PER_DEPTH_OVERHEAD_BYTES.saturating_mul(depth as u64);
421    per_launch.saturating_add(per_depth)
422}
423
424/// Returns the maximum supported nesting depth for a given SM version.
425///
426/// All architectures from sm_35 onward support dynamic parallelism with
427/// a hardware maximum of 24 nesting levels. The available SM versions
428/// in this crate (sm_75+) all support the full nesting depth.
429///
430/// For practical purposes, deep nesting (>8) is rarely beneficial due
431/// to launch overhead and memory consumption.
432pub fn max_nesting_for_sm(sm: SmVersion) -> u32 {
433    // All supported SM versions (75+) support dynamic parallelism.
434    // Newer architectures have the same 24-level limit but with
435    // improved launch latency.
436    match sm {
437        SmVersion::Sm75 => CUDA_MAX_NESTING_DEPTH,
438        SmVersion::Sm80 | SmVersion::Sm86 => CUDA_MAX_NESTING_DEPTH,
439        SmVersion::Sm89 => CUDA_MAX_NESTING_DEPTH,
440        SmVersion::Sm90 | SmVersion::Sm90a => CUDA_MAX_NESTING_DEPTH,
441        SmVersion::Sm100 => CUDA_MAX_NESTING_DEPTH,
442        SmVersion::Sm120 => CUDA_MAX_NESTING_DEPTH,
443    }
444}
445
446// ---------------------------------------------------------------------------
447// PTX generation
448// ---------------------------------------------------------------------------
449
450/// Generates PTX code for a device-side child kernel launch.
451///
452/// Produces a `.func` that computes the child grid dimensions according to
453/// the [`GridSpec`], obtains a parameter buffer from the CUDA device runtime
454/// via `cudaGetParameterBufferV2`, marshals the kernel arguments into that
455/// buffer, and enqueues the child grid with `cudaLaunchDeviceV2`. The real
456/// `cudaError_t` returned by `cudaLaunchDeviceV2` is propagated to the
457/// caller.
458///
459/// The generated module declares `cudaGetParameterBufferV2` and
460/// `cudaLaunchDeviceV2` as `.extern .func` prototypes using the device
461/// runtime ABI, so it must be linked against `cudadevrt` at JIT/load time.
462///
463/// # Arguments
464///
465/// - `parent_name` — name of the parent kernel (used for symbol naming)
466/// - `child` — specification of the child kernel to launch
467/// - `sm` — target architecture for PTX ISA version selection
468///
469/// # Errors
470///
471/// Returns [`PtxGenError`] if the child specification is invalid or
472/// the target architecture does not support dynamic parallelism
473/// (all sm_75+ architectures do).
474pub fn generate_child_launch_ptx(
475    parent_name: &str,
476    child: &ChildKernelSpec,
477    sm: SmVersion,
478) -> Result<String, PtxGenError> {
479    // Validate child spec
480    if child.name.is_empty() {
481        return Err(PtxGenError::GenerationFailed(
482            "child kernel name must not be empty".to_string(),
483        ));
484    }
485    if child.block_dim.x == 0 || child.block_dim.y == 0 || child.block_dim.z == 0 {
486        return Err(PtxGenError::GenerationFailed(
487            "child block dimensions must be non-zero".to_string(),
488        ));
489    }
490
491    let (isa_major, isa_minor) = sm.ptx_isa_version();
492    let target = sm.as_ptx_str();
493
494    let mut ptx = String::with_capacity(2048);
495
496    // PTX header
497    ptx.push_str(&format!(
498        "// Dynamic parallelism: {parent_name} -> {child_name}\n",
499        child_name = child.name,
500    ));
501    ptx.push_str(&format!(
502        ".version {isa_major}.{isa_minor}\n\
503         .target {target}\n\
504         .address_size 64\n\n"
505    ));
506
507    // CUDA device runtime prototypes (linked against cudadevrt).
508    append_device_runtime_externs(&mut ptx);
509
510    // Extern declaration for the child kernel
511    ptx.push_str(&format!(
512        "// Child kernel declaration\n\
513         .extern .entry {child_name}(\n",
514        child_name = child.name,
515    ));
516    for (i, ptype) in child.param_types.iter().enumerate() {
517        let comma = if i + 1 < child.param_types.len() {
518            ","
519        } else {
520            ""
521        };
522        ptx.push_str(&format!(
523            "    .param {ty} _param_{i}{comma}\n",
524            ty = ptype.as_ptx_str(),
525        ));
526    }
527    ptx.push_str(")\n\n");
528
529    // Launch helper function
530    let func_name = format!(
531        "__{parent_name}_launch_{child_name}",
532        child_name = child.name
533    );
534    ptx.push_str("// Device-side launch helper\n");
535    ptx.push_str(&format!(".func (.param .s32 _retval) {func_name}(\n"));
536
537    // Parameters for the launch helper (same as child kernel params)
538    for (i, ptype) in child.param_types.iter().enumerate() {
539        let comma = if i + 1 < child.param_types.len() {
540            ","
541        } else {
542            ""
543        };
544        ptx.push_str(&format!(
545            "    .param {ty} arg_{i}{comma}\n",
546            ty = ptype.as_ptx_str(),
547        ));
548    }
549    ptx.push_str(")\n{\n");
550
551    // Register declarations
552    ptx.push_str("    // Register declarations\n");
553    ptx.push_str("    .reg .s32 %retval;\n");
554    ptx.push_str("    .reg .u32 %grid_x, %grid_y, %grid_z;\n");
555    ptx.push_str("    .reg .u32 %block_x, %block_y, %block_z;\n");
556    ptx.push_str("    .reg .u32 %shared_mem;\n");
557    ptx.push_str("    .reg .u64 %stream;\n");
558    // Registers and address-taken symbol needed for the device-runtime calls.
559    ptx.push_str("    .reg .u64 %func_addr, %param_buf;\n");
560    ptx.push_str("    .reg .pred %p_buf_null;\n");
561
562    // Additional registers for data-dependent grid
563    if let GridSpec::DataDependent { .. } = &child.grid_dim {
564        ptx.push_str("    .reg .u32 %n_elements, %block_size;\n");
565    }
566    if matches!(&child.grid_dim, GridSpec::ThreadDependent) {
567        ptx.push_str("    .reg .u32 %tid_x, %ntid_x, %ctaid_x;\n");
568    }
569
570    ptx.push('\n');
571
572    // Set grid dimensions based on GridSpec
573    match &child.grid_dim {
574        GridSpec::Fixed(dim) => {
575            ptx.push_str(&format!(
576                "    // Fixed grid dimensions\n\
577                 mov.u32 %grid_x, {gx};\n\
578                 mov.u32 %grid_y, {gy};\n\
579                 mov.u32 %grid_z, {gz};\n",
580                gx = dim.x,
581                gy = dim.y,
582                gz = dim.z,
583            ));
584        }
585        GridSpec::DataDependent { param_index } => {
586            ptx.push_str(&format!(
587                "    // Data-dependent grid: ceil(param[{param_index}] / block.x)\n\
588                 ld.param.u32 %n_elements, [arg_{param_index}];\n\
589                 mov.u32 %block_size, {bx};\n\
590                 add.u32 %grid_x, %n_elements, %block_size;\n\
591                 sub.u32 %grid_x, %grid_x, 1;\n\
592                 div.u32 %grid_x, %grid_x, %block_size;\n\
593                 mov.u32 %grid_y, 1;\n\
594                 mov.u32 %grid_z, 1;\n",
595                bx = child.block_dim.x,
596            ));
597        }
598        GridSpec::ThreadDependent => {
599            ptx.push_str(
600                "    // Thread-dependent: one child launch per parent thread\n\
601                 mov.u32 %tid_x, %tid.x;\n\
602                 mov.u32 %ntid_x, %ntid.x;\n\
603                 mov.u32 %ctaid_x, %ctaid.x;\n\
604                 // Each thread launches a 1-block child grid\n\
605                 mov.u32 %grid_x, 1;\n\
606                 mov.u32 %grid_y, 1;\n\
607                 mov.u32 %grid_z, 1;\n",
608            );
609        }
610    }
611
612    // Set block dimensions
613    ptx.push_str(&format!(
614        "\n    // Block dimensions\n\
615         mov.u32 %block_x, {bx};\n\
616         mov.u32 %block_y, {by};\n\
617         mov.u32 %block_z, {bz};\n",
618        bx = child.block_dim.x,
619        by = child.block_dim.y,
620        bz = child.block_dim.z,
621    ));
622
623    // Shared memory and stream
624    ptx.push_str(&format!(
625        "\n    // Shared memory and stream (NULL = default stream)\n\
626         mov.u32 %shared_mem, {smem};\n\
627         mov.u64 %stream, 0;\n",
628        smem = child.shared_mem_bytes,
629    ));
630
631    // Take the address of the child kernel entry. cudaGetParameterBufferV2
632    // and cudaLaunchDeviceV2 identify the kernel by its entry-point address.
633    ptx.push_str(&format!(
634        "\n    // Resolve child kernel entry address\n\
635         mov.u64 %func_addr, {child_name};\n",
636        child_name = child.name,
637    ));
638
639    // Obtain a device-runtime parameter buffer sized for the child grid.
640    // cudaGetParameterBufferV2 reserves storage in the device launch pool;
641    // the kernel arguments are then written into the returned buffer.
642    ptx.push_str(
643        "\n    // Allocate parameter buffer via the CUDA device runtime\n\
644         // void *cudaGetParameterBufferV2(void *func,\n\
645         //                                dim3 gridDim, dim3 blockDim,\n\
646         //                                unsigned int sharedMem)\n\
647         {\n\
648         .param .b64 retval_pb;\n\
649         .param .b64 param0_pb;\n\
650         .param .align 4 .b8 param1_pb[12];\n\
651         .param .align 4 .b8 param2_pb[12];\n\
652         .param .b32 param3_pb;\n\
653         st.param.b64 [param0_pb], %func_addr;\n\
654         st.param.b32 [param1_pb+0], %grid_x;\n\
655         st.param.b32 [param1_pb+4], %grid_y;\n\
656         st.param.b32 [param1_pb+8], %grid_z;\n\
657         st.param.b32 [param2_pb+0], %block_x;\n\
658         st.param.b32 [param2_pb+4], %block_y;\n\
659         st.param.b32 [param2_pb+8], %block_z;\n\
660         st.param.b32 [param3_pb], %shared_mem;\n\
661         call.uni (retval_pb), cudaGetParameterBufferV2,\n\
662         \x20    (param0_pb, param1_pb, param2_pb, param3_pb);\n\
663         ld.param.b64 %param_buf, [retval_pb];\n\
664         }\n",
665    );
666
667    // A NULL parameter buffer means the device launch pool is exhausted;
668    // report cudaErrorLaunchPendingCountExceeded (== 209) to the caller.
669    ptx.push_str(
670        "\n    // Bail out if the device runtime could not provide a buffer\n\
671         setp.eq.u64 %p_buf_null, %param_buf, 0;\n\
672         @%p_buf_null mov.s32 %retval, 209;\n\
673         @%p_buf_null bra $L_launch_done;\n",
674    );
675
676    // Marshal the child kernel arguments into the parameter buffer.
677    // cudaGetParameterBufferV2 guarantees the buffer is aligned for the
678    // largest parameter; each argument is written at its natural offset.
679    // Loads and stores go through the type's bit-width form so the move is
680    // independent of the argument's value type.
681    ptx.push_str("\n    // Marshal child kernel arguments into the buffer\n");
682    let mut buf_offset: u32 = 0;
683    for (i, ptype) in child.param_types.iter().enumerate() {
684        let size = ptx_param_size_bytes(*ptype);
685        // Natural alignment: round the running offset up to the type size.
686        buf_offset = align_up(buf_offset, size);
687        let move_ty = ptx_param_move_type(*ptype);
688        let scratch = format!("%arg_scratch_{i}");
689        ptx.push_str(&format!(
690            "    .reg {move_ty} {scratch};\n\
691             ld.param{move_ty} {scratch}, [arg_{i}];\n\
692             st.global{move_ty} [%param_buf+{buf_offset}], {scratch};\n",
693        ));
694        buf_offset = buf_offset.saturating_add(size);
695    }
696
697    // Enqueue the child grid on the requested stream. cudaLaunchDeviceV2
698    // consumes the formatted parameter buffer produced above and returns a
699    // cudaError_t describing whether the grid was successfully enqueued.
700    ptx.push_str(&format!(
701        "\n    // Launch child kernel {child_name} via the CUDA device runtime\n\
702         // cudaError_t cudaLaunchDeviceV2(void *parameterBuffer,\n\
703         //                                cudaStream_t stream)\n\
704         {{\n\
705         .param .b32 retval_ld;\n\
706         .param .b64 param0_ld;\n\
707         .param .b64 param1_ld;\n\
708         st.param.b64 [param0_ld], %param_buf;\n\
709         st.param.b64 [param1_ld], %stream;\n\
710         call.uni (retval_ld), cudaLaunchDeviceV2, (param0_ld, param1_ld);\n\
711         ld.param.b32 %retval, [retval_ld];\n\
712         }}\n",
713        child_name = child.name,
714    ));
715
716    // Store return value and close function
717    ptx.push_str(
718        "\n$L_launch_done:\n\
719         \x20   st.param.s32 [_retval], %retval;\n\
720         ret;\n\
721         }\n",
722    );
723
724    Ok(ptx)
725}
726
727/// Returns the byte size of a [`PtxType`] when laid out in a launch
728/// parameter buffer.
729///
730/// The CUDA device runtime parameter buffer stores each kernel argument at
731/// its natural size and alignment. This delegates to [`PtxType::size_bytes`]
732/// so it stays correct as new types are added to the IR.
733fn ptx_param_size_bytes(ty: PtxType) -> u32 {
734    // `size_bytes()` returns at most 16, well within `u32` range.
735    u32::try_from(ty.size_bytes()).unwrap_or(u32::MAX)
736}
737
738/// Returns the bit-width PTX type suffix (`.b8`/`.b16`/`.b32`/`.b64`/`.b128`)
739/// used for the `ld.param` / `st.global` pair that copies a kernel argument
740/// into the parameter buffer.
741///
742/// Arguments are moved through their bit-typed form so the copy matches the
743/// destination layout regardless of the register's value type (integer,
744/// float, packed or sub-word).
745fn ptx_param_move_type(ty: PtxType) -> &'static str {
746    match ptx_param_size_bytes(ty) {
747        0 | 1 => ".b8",
748        2 => ".b16",
749        3..=4 => ".b32",
750        5..=8 => ".b64",
751        _ => ".b128",
752    }
753}
754
755/// Rounds `value` up to the next multiple of `align`.
756///
757/// `align` is a power-of-two type size produced by [`ptx_param_size_bytes`];
758/// an `align` of zero or one leaves `value` unchanged.
759fn align_up(value: u32, align: u32) -> u32 {
760    if align <= 1 {
761        return value;
762    }
763    value.div_ceil(align).saturating_mul(align)
764}
765
766/// Emits the `.extern .func` prototypes for the CUDA device runtime
767/// (`cudadevrt`) entry points used by device-side kernel launches.
768///
769/// The generated module links against `cudadevrt` at JIT/load time. The
770/// prototypes use the device-runtime ABI exactly:
771///
772/// - `cudaGetParameterBufferV2` — reserves a parameter buffer; takes the
773///   kernel entry address, the grid and block `dim3` values (each a
774///   12-byte, 4-aligned aggregate) and the dynamic shared-memory size,
775///   and returns a `void*` buffer pointer.
776/// - `cudaLaunchDeviceV2` — enqueues the child grid; takes the formatted
777///   parameter buffer and a `cudaStream_t`, and returns a `cudaError_t`.
778fn append_device_runtime_externs(ptx: &mut String) {
779    ptx.push_str(
780        "// CUDA device runtime (cudadevrt) entry points — resolved at link time\n\
781         .extern .func (.param .b64 func_retval0) cudaGetParameterBufferV2\n\
782         (\n\
783         \x20   .param .b64 cudaGetParameterBufferV2_param_0,\n\
784         \x20   .param .align 4 .b8 cudaGetParameterBufferV2_param_1[12],\n\
785         \x20   .param .align 4 .b8 cudaGetParameterBufferV2_param_2[12],\n\
786         \x20   .param .b32 cudaGetParameterBufferV2_param_3\n\
787         )\n\
788         ;\n\
789         .extern .func (.param .b32 func_retval0) cudaLaunchDeviceV2\n\
790         (\n\
791         \x20   .param .b64 cudaLaunchDeviceV2_param_0,\n\
792         \x20   .param .b64 cudaLaunchDeviceV2_param_1\n\
793         )\n\
794         ;\n\n",
795    );
796}
797
798/// Generates PTX code for device-side synchronization.
799///
800/// Produces a `.func` that declares `cudaDeviceSynchronize` as an
801/// `.extern .func` prototype using the CUDA device runtime ABI and issues a
802/// `call.uni` to it. The real `cudaError_t` it returns is propagated to the
803/// caller. The call synchronizes all pending child kernel launches within
804/// the current thread's scope; the generated module must be linked against
805/// `cudadevrt` at JIT/load time.
806///
807/// # Arguments
808///
809/// - `sm` — target architecture for PTX ISA version selection
810///
811/// # Errors
812///
813/// Returns [`PtxGenError`] if PTX generation fails.
814pub fn generate_device_sync_ptx(sm: SmVersion) -> Result<String, PtxGenError> {
815    let (isa_major, isa_minor) = sm.ptx_isa_version();
816    let target = sm.as_ptx_str();
817
818    let ptx = format!(
819        "// Device-side synchronization\n\
820         .version {isa_major}.{isa_minor}\n\
821         .target {target}\n\
822         .address_size 64\n\
823         \n\
824         // CUDA device runtime (cudadevrt) entry point — resolved at link time\n\
825         // cudaError_t cudaDeviceSynchronize(void)\n\
826         .extern .func (.param .b32 func_retval0) cudaDeviceSynchronize\n\
827         ;\n\
828         \n\
829         // cudaDeviceSynchronize() from device code.\n\
830         // Blocks the calling thread until every child grid it launched has\n\
831         // completed, then returns the device runtime cudaError_t status.\n\
832         .func (.param .s32 _retval) __device_synchronize()\n\
833         {{\n\
834         .reg .s32 %retval;\n\
835         \n\
836         // Invoke the device runtime synchronization entry point.\n\
837         {{\n\
838         .param .b32 retval_sync;\n\
839         call.uni (retval_sync), cudaDeviceSynchronize, ();\n\
840         ld.param.b32 %retval, [retval_sync];\n\
841         }}\n\
842         \n\
843         st.param.s32 [_retval], %retval;\n\
844         ret;\n\
845         }}\n"
846    );
847
848    Ok(ptx)
849}
850
851// ---------------------------------------------------------------------------
852// Tests
853// ---------------------------------------------------------------------------
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858
859    fn default_config() -> DynamicParallelismConfig {
860        DynamicParallelismConfig::new(SmVersion::Sm80)
861    }
862
863    // -- Validation tests --
864
865    #[test]
866    fn validate_default_config_ok() {
867        let config = default_config();
868        assert!(validate_dynamic_config(&config).is_ok());
869    }
870
871    #[test]
872    fn validate_zero_nesting_depth_fails() {
873        let mut config = default_config();
874        config.max_nesting_depth = 0;
875        let err = validate_dynamic_config(&config);
876        assert!(err.is_err());
877        let err = err.err();
878        assert!(matches!(
879            err,
880            Some(LaunchError::InvalidDimension {
881                dim: "max_nesting_depth",
882                ..
883            })
884        ));
885    }
886
887    #[test]
888    fn validate_excessive_nesting_depth_fails() {
889        let mut config = default_config();
890        config.max_nesting_depth = 25;
891        let err = validate_dynamic_config(&config);
892        assert!(err.is_err());
893    }
894
895    #[test]
896    fn validate_max_nesting_depth_boundary() {
897        let mut config = default_config();
898        config.max_nesting_depth = CUDA_MAX_NESTING_DEPTH;
899        config.sync_depth = CUDA_MAX_NESTING_DEPTH;
900        assert!(validate_dynamic_config(&config).is_ok());
901    }
902
903    #[test]
904    fn validate_zero_pending_launches_fails() {
905        let mut config = default_config();
906        config.max_pending_launches = 0;
907        assert!(validate_dynamic_config(&config).is_err());
908    }
909
910    #[test]
911    fn validate_sync_depth_exceeds_nesting_fails() {
912        let mut config = default_config();
913        config.max_nesting_depth = 4;
914        config.sync_depth = 5;
915        assert!(validate_dynamic_config(&config).is_err());
916    }
917
918    #[test]
919    fn validate_zero_child_block_fails() {
920        let mut config = default_config();
921        config.child_block = Dim3::new(0, 256, 1);
922        assert!(validate_dynamic_config(&config).is_err());
923    }
924
925    #[test]
926    fn validate_zero_child_grid_fails() {
927        let mut config = default_config();
928        config.child_grid = Dim3::new(128, 0, 1);
929        assert!(validate_dynamic_config(&config).is_err());
930    }
931
932    #[test]
933    fn validate_block_size_exceeds_limit() {
934        let mut config = default_config();
935        // 32 * 32 * 2 = 2048, exceeds 1024 max
936        config.child_block = Dim3::new(32, 32, 2);
937        let err = validate_dynamic_config(&config);
938        assert!(matches!(
939            err,
940            Err(LaunchError::BlockSizeExceedsLimit { .. })
941        ));
942    }
943
944    #[test]
945    fn validate_shared_mem_exceeds_limit() {
946        let mut config = default_config();
947        config.child_shared_mem = 500_000; // exceeds any SM limit
948        let err = validate_dynamic_config(&config);
949        assert!(matches!(
950            err,
951            Err(LaunchError::SharedMemoryExceedsLimit { .. })
952        ));
953    }
954
955    // -- Plan generation tests --
956
957    #[test]
958    fn plan_dynamic_launch_ok() {
959        let config = default_config();
960        let plan = plan_dynamic_launch(&config);
961        assert!(plan.is_ok());
962        let plan = plan.ok();
963        assert!(plan.is_some());
964        if let Some(plan) = plan {
965            assert!(plan.estimated_child_launches > 0);
966            assert!(plan.memory_overhead_bytes > 0);
967            assert_eq!(plan.parent_kernel_name, "parent_kernel");
968            assert_eq!(plan.child_kernel_name, "child_kernel");
969        }
970    }
971
972    #[test]
973    fn plan_dynamic_launch_invalid_config_fails() {
974        let mut config = default_config();
975        config.max_nesting_depth = 0;
976        let plan = plan_dynamic_launch(&config);
977        assert!(plan.is_err());
978    }
979
980    #[test]
981    fn plan_display() {
982        let config = default_config();
983        let plan = plan_dynamic_launch(&config);
984        if let Ok(plan) = plan {
985            let display = format!("{plan}");
986            assert!(display.contains("parent_kernel"));
987            assert!(display.contains("child_kernel"));
988            assert!(display.contains("bytes"));
989        }
990    }
991
992    // -- Overhead estimation tests --
993
994    #[test]
995    fn estimate_overhead_basic() {
996        let overhead = estimate_launch_overhead(1, 1);
997        assert_eq!(
998            overhead,
999            BASE_LAUNCH_OVERHEAD_BYTES + PER_DEPTH_OVERHEAD_BYTES
1000        );
1001    }
1002
1003    #[test]
1004    fn estimate_overhead_default() {
1005        let overhead = estimate_launch_overhead(4, 2048);
1006        let expected = BASE_LAUNCH_OVERHEAD_BYTES * 2048 + PER_DEPTH_OVERHEAD_BYTES * 4;
1007        assert_eq!(overhead, expected);
1008    }
1009
1010    #[test]
1011    fn estimate_overhead_zero() {
1012        let overhead = estimate_launch_overhead(0, 0);
1013        assert_eq!(overhead, 0);
1014    }
1015
1016    // -- SM nesting tests --
1017
1018    #[test]
1019    fn max_nesting_all_sm_versions() {
1020        assert_eq!(max_nesting_for_sm(SmVersion::Sm75), 24);
1021        assert_eq!(max_nesting_for_sm(SmVersion::Sm80), 24);
1022        assert_eq!(max_nesting_for_sm(SmVersion::Sm86), 24);
1023        assert_eq!(max_nesting_for_sm(SmVersion::Sm89), 24);
1024        assert_eq!(max_nesting_for_sm(SmVersion::Sm90), 24);
1025        assert_eq!(max_nesting_for_sm(SmVersion::Sm90a), 24);
1026        assert_eq!(max_nesting_for_sm(SmVersion::Sm100), 24);
1027        assert_eq!(max_nesting_for_sm(SmVersion::Sm120), 24);
1028    }
1029
1030    // -- PTX generation tests --
1031
1032    #[test]
1033    fn generate_child_launch_ptx_basic() {
1034        let child = ChildKernelSpec {
1035            name: "child_add".to_string(),
1036            param_types: vec![PtxType::U64, PtxType::U64, PtxType::U32],
1037            grid_dim: GridSpec::Fixed(Dim3::x(64)),
1038            block_dim: Dim3::x(256),
1039            shared_mem_bytes: 0,
1040        };
1041        let result = generate_child_launch_ptx("parent_add", &child, SmVersion::Sm80);
1042        assert!(result.is_ok());
1043        let ptx = result.ok();
1044        assert!(ptx.is_some());
1045        if let Some(ptx) = ptx {
1046            assert!(ptx.contains("child_add"));
1047            assert!(ptx.contains("parent_add"));
1048            assert!(ptx.contains(".version 7.0"));
1049            assert!(ptx.contains("sm_80"));
1050            assert!(ptx.contains("mov.u32 %grid_x, 64"));
1051            assert!(ptx.contains(".u64"));
1052            assert!(ptx.contains(".u32"));
1053        }
1054    }
1055
1056    #[test]
1057    fn generate_child_launch_ptx_emits_device_runtime_externs() {
1058        // The generated module must declare the cudadevrt entry points so it
1059        // links against the CUDA device runtime at JIT/load time.
1060        let child = ChildKernelSpec {
1061            name: "child_dr".to_string(),
1062            param_types: vec![PtxType::U64, PtxType::U32],
1063            grid_dim: GridSpec::Fixed(Dim3::x(32)),
1064            block_dim: Dim3::x(128),
1065            shared_mem_bytes: 0,
1066        };
1067        let result = generate_child_launch_ptx("parent_dr", &child, SmVersion::Sm80);
1068        assert!(result.is_ok());
1069        if let Ok(ptx) = result {
1070            // External prototypes with the device-runtime ABI.
1071            assert!(
1072                ptx.contains(".extern .func (.param .b64 func_retval0) cudaGetParameterBufferV2")
1073            );
1074            assert!(ptx.contains(".extern .func (.param .b32 func_retval0) cudaLaunchDeviceV2"));
1075            // dim3 grid/block aggregates are 12-byte, 4-aligned params.
1076            assert!(ptx.contains(".param .align 4 .b8 cudaGetParameterBufferV2_param_1[12]"));
1077            assert!(ptx.contains(".param .align 4 .b8 cudaGetParameterBufferV2_param_2[12]"));
1078            // Real call.uni invocations into the device runtime.
1079            assert!(ptx.contains("call.uni (retval_pb), cudaGetParameterBufferV2"));
1080            assert!(
1081                ptx.contains("call.uni (retval_ld), cudaLaunchDeviceV2, (param0_ld, param1_ld);")
1082            );
1083        }
1084    }
1085
1086    #[test]
1087    fn generate_child_launch_ptx_sets_up_parameter_buffer() {
1088        // The parameter buffer must be obtained, NULL-checked, and have the
1089        // child arguments marshalled into it before the launch.
1090        let child = ChildKernelSpec {
1091            name: "child_buf".to_string(),
1092            param_types: vec![PtxType::U64, PtxType::U64, PtxType::F32],
1093            grid_dim: GridSpec::Fixed(Dim3::x(16)),
1094            block_dim: Dim3::x(64),
1095            shared_mem_bytes: 256,
1096        };
1097        let result = generate_child_launch_ptx("parent_buf", &child, SmVersion::Sm90);
1098        assert!(result.is_ok());
1099        if let Ok(ptx) = result {
1100            // Parameter-buffer plumbing.
1101            assert!(ptx.contains(".reg .u64 %func_addr, %param_buf;"));
1102            assert!(ptx.contains("mov.u64 %func_addr, child_buf;"));
1103            assert!(ptx.contains("ld.param.b64 %param_buf, [retval_pb];"));
1104            // grid / block / smem are stored into the buffer-allocation params.
1105            assert!(ptx.contains("st.param.b32 [param1_pb+0], %grid_x;"));
1106            assert!(ptx.contains("st.param.b32 [param2_pb+8], %block_z;"));
1107            assert!(ptx.contains("st.param.b32 [param3_pb], %shared_mem;"));
1108            // NULL parameter buffer bails out with the pending-count error.
1109            assert!(ptx.contains("setp.eq.u64 %p_buf_null, %param_buf, 0;"));
1110            assert!(ptx.contains("@%p_buf_null mov.s32 %retval, 209;"));
1111            // Arguments marshalled at natural offsets: u64,u64,f32 -> 0,8,16.
1112            assert!(ptx.contains("st.global.b64 [%param_buf+0],"));
1113            assert!(ptx.contains("st.global.b64 [%param_buf+8],"));
1114            assert!(ptx.contains("st.global.b32 [%param_buf+16],"));
1115            // The launch return code is captured into %retval.
1116            assert!(ptx.contains("ld.param.b32 %retval, [retval_ld];"));
1117        }
1118    }
1119
1120    #[test]
1121    fn generate_child_launch_ptx_no_stub_comments() {
1122        // The stale placeholder comments must be gone once implemented.
1123        let child = ChildKernelSpec {
1124            name: "child_clean".to_string(),
1125            param_types: vec![PtxType::U64],
1126            grid_dim: GridSpec::Fixed(Dim3::x(8)),
1127            block_dim: Dim3::x(32),
1128            shared_mem_bytes: 0,
1129        };
1130        let result = generate_child_launch_ptx("parent_clean", &child, SmVersion::Sm80);
1131        assert!(result.is_ok());
1132        if let Ok(ptx) = result {
1133            assert!(!ptx.contains("We model this with"));
1134            assert!(!ptx.contains("actual device-side launch uses cudaLaunchDeviceV2"));
1135            assert!(!ptx.contains("mov.s32 %retval, 0; // cudaSuccess"));
1136        }
1137    }
1138
1139    #[test]
1140    fn generate_child_launch_ptx_aligns_mixed_arguments() {
1141        // A u32 followed by a u64 must pad the u64 to an 8-byte offset.
1142        let child = ChildKernelSpec {
1143            name: "child_align".to_string(),
1144            param_types: vec![PtxType::U32, PtxType::U64],
1145            grid_dim: GridSpec::Fixed(Dim3::x(4)),
1146            block_dim: Dim3::x(32),
1147            shared_mem_bytes: 0,
1148        };
1149        let result = generate_child_launch_ptx("parent_align", &child, SmVersion::Sm80);
1150        assert!(result.is_ok());
1151        if let Ok(ptx) = result {
1152            assert!(ptx.contains("st.global.b32 [%param_buf+0],"));
1153            // u64 padded from offset 4 up to its natural 8-byte boundary.
1154            assert!(ptx.contains("st.global.b64 [%param_buf+8],"));
1155        }
1156    }
1157
1158    #[test]
1159    fn generate_child_launch_ptx_data_dependent() {
1160        let child = ChildKernelSpec {
1161            name: "child_scale".to_string(),
1162            param_types: vec![PtxType::U64, PtxType::U32],
1163            grid_dim: GridSpec::DataDependent { param_index: 1 },
1164            block_dim: Dim3::x(128),
1165            shared_mem_bytes: 1024,
1166        };
1167        let result = generate_child_launch_ptx("parent_scale", &child, SmVersion::Sm90);
1168        assert!(result.is_ok());
1169        if let Ok(ptx) = result {
1170            assert!(ptx.contains("Data-dependent"));
1171            assert!(ptx.contains("arg_1"));
1172            assert!(ptx.contains("div.u32"));
1173        }
1174    }
1175
1176    #[test]
1177    fn generate_child_launch_ptx_thread_dependent() {
1178        let child = ChildKernelSpec {
1179            name: "child_per_thread".to_string(),
1180            param_types: vec![PtxType::U64],
1181            grid_dim: GridSpec::ThreadDependent,
1182            block_dim: Dim3::x(32),
1183            shared_mem_bytes: 0,
1184        };
1185        let result = generate_child_launch_ptx("parent", &child, SmVersion::Sm80);
1186        assert!(result.is_ok());
1187        if let Ok(ptx) = result {
1188            assert!(ptx.contains("Thread-dependent"));
1189            assert!(ptx.contains("%tid.x"));
1190        }
1191    }
1192
1193    #[test]
1194    fn generate_child_launch_ptx_empty_name_fails() {
1195        let child = ChildKernelSpec {
1196            name: String::new(),
1197            param_types: vec![],
1198            grid_dim: GridSpec::Fixed(Dim3::x(1)),
1199            block_dim: Dim3::x(1),
1200            shared_mem_bytes: 0,
1201        };
1202        let result = generate_child_launch_ptx("parent", &child, SmVersion::Sm80);
1203        assert!(result.is_err());
1204    }
1205
1206    #[test]
1207    fn generate_child_launch_ptx_zero_block_fails() {
1208        let child = ChildKernelSpec {
1209            name: "child".to_string(),
1210            param_types: vec![],
1211            grid_dim: GridSpec::Fixed(Dim3::x(1)),
1212            block_dim: Dim3::new(0, 1, 1),
1213            shared_mem_bytes: 0,
1214        };
1215        let result = generate_child_launch_ptx("parent", &child, SmVersion::Sm80);
1216        assert!(result.is_err());
1217    }
1218
1219    #[test]
1220    fn generate_device_sync_ptx_basic() {
1221        let result = generate_device_sync_ptx(SmVersion::Sm80);
1222        assert!(result.is_ok());
1223        if let Ok(ptx) = result {
1224            assert!(ptx.contains("__device_synchronize"));
1225            assert!(ptx.contains(".version 7.0"));
1226            assert!(ptx.contains("sm_80"));
1227            assert!(ptx.contains("cudaDeviceSynchronize"));
1228        }
1229    }
1230
1231    #[test]
1232    fn generate_device_sync_ptx_hopper() {
1233        let result = generate_device_sync_ptx(SmVersion::Sm90);
1234        assert!(result.is_ok());
1235        if let Ok(ptx) = result {
1236            assert!(ptx.contains(".version 8.0"));
1237            assert!(ptx.contains("sm_90"));
1238        }
1239    }
1240
1241    #[test]
1242    fn generate_device_sync_ptx_emits_real_extern_and_call() {
1243        // The sync helper must declare the cudadevrt entry point and call it.
1244        let result = generate_device_sync_ptx(SmVersion::Sm80);
1245        assert!(result.is_ok());
1246        if let Ok(ptx) = result {
1247            assert!(ptx.contains(".extern .func (.param .b32 func_retval0) cudaDeviceSynchronize"));
1248            assert!(ptx.contains("call.uni (retval_sync), cudaDeviceSynchronize, ();"));
1249            // The real return code is stored into %retval.
1250            assert!(ptx.contains("ld.param.b32 %retval, [retval_sync];"));
1251        }
1252    }
1253
1254    #[test]
1255    fn generate_device_sync_ptx_no_stub_comments() {
1256        // The stale placeholder comments must be gone once implemented.
1257        let result = generate_device_sync_ptx(SmVersion::Sm80);
1258        assert!(result.is_ok());
1259        if let Ok(ptx) = result {
1260            assert!(!ptx.contains("(placeholder)"));
1261            assert!(!ptx.contains("For code generation, we emit the call pattern"));
1262            assert!(!ptx.contains("mov.s32 %retval, 0;"));
1263        }
1264    }
1265
1266    #[test]
1267    fn ptx_param_layout_helpers() {
1268        // Buffer size matches PtxType::size_bytes for representative types.
1269        assert_eq!(ptx_param_size_bytes(PtxType::U8), 1);
1270        assert_eq!(ptx_param_size_bytes(PtxType::F16), 2);
1271        assert_eq!(ptx_param_size_bytes(PtxType::U32), 4);
1272        assert_eq!(ptx_param_size_bytes(PtxType::U64), 8);
1273        assert_eq!(ptx_param_size_bytes(PtxType::B128), 16);
1274        // Bit-width move type selection.
1275        assert_eq!(ptx_param_move_type(PtxType::S8), ".b8");
1276        assert_eq!(ptx_param_move_type(PtxType::BF16), ".b16");
1277        assert_eq!(ptx_param_move_type(PtxType::F32), ".b32");
1278        assert_eq!(ptx_param_move_type(PtxType::F64), ".b64");
1279        assert_eq!(ptx_param_move_type(PtxType::B128), ".b128");
1280        // align_up rounds to the next multiple, identity below 2.
1281        assert_eq!(align_up(0, 8), 0);
1282        assert_eq!(align_up(4, 8), 8);
1283        assert_eq!(align_up(8, 8), 8);
1284        assert_eq!(align_up(9, 4), 12);
1285        assert_eq!(align_up(7, 1), 7);
1286    }
1287
1288    // -- Display tests --
1289
1290    #[test]
1291    fn config_display() {
1292        let config = default_config();
1293        let display = format!("{config}");
1294        assert!(display.contains("depth=4"));
1295        assert!(display.contains("pending=2048"));
1296        assert!(display.contains("sync@2"));
1297        assert!(display.contains("sm_80"));
1298    }
1299
1300    #[test]
1301    fn grid_spec_display() {
1302        assert_eq!(format!("{}", GridSpec::Fixed(Dim3::x(64))), "Fixed(64)");
1303        assert_eq!(
1304            format!("{}", GridSpec::DataDependent { param_index: 2 }),
1305            "DataDependent(param[2])"
1306        );
1307        assert_eq!(format!("{}", GridSpec::ThreadDependent), "ThreadDependent");
1308    }
1309}