Skip to main content

xlog_prob/compilation/
gpu_pir_intern.rs

1//! GPU PIR interner (device-side hash-consing).
2//!
3//! Implements deterministic, memory-bounded interning of PIR node batches on GPU.
4
5use std::ffi::c_void;
6use std::sync::Arc;
7
8use cudarc::driver::LaunchConfig;
9use xlog_core::{Result, XlogError};
10use xlog_cuda::memory::TrackedCudaSlice;
11use xlog_cuda::provider::{pir_kernels, scan_kernels, RadixSortScratch, PIR_MODULE, SCAN_MODULE};
12use xlog_cuda::{AsKernelParam, CudaKernelProvider, LaunchAsync};
13
14use crate::compilation::gpu_pir::{GpuPirGraph, PIR_AND, PIR_CONST};
15
16/// Host-side PIR batch (for tests and host-driven workflows).
17#[derive(Debug, Clone)]
18pub struct PirBatch {
19    /// Opcode for every candidate node.
20    pub node_type: Vec<u8>,
21    /// Leaf identifier for every candidate literal node.
22    pub leaf_id: Vec<u32>,
23    /// Choice-variable identifier for every candidate decision node.
24    pub decision_var: Vec<u32>,
25    /// False-branch child for every candidate decision node.
26    pub decision_child_false: Vec<u32>,
27    /// True-branch child for every candidate decision node.
28    pub decision_child_true: Vec<u32>,
29    /// CSR offsets into [`Self::children`].
30    pub child_offsets: Vec<u32>,
31    /// Flattened child identifiers.
32    pub children: Vec<u32>,
33}
34
35impl PirBatch {
36    /// Returns the number of candidate nodes.
37    pub fn len(&self) -> usize {
38        self.node_type.len()
39    }
40
41    /// Returns whether the batch contains no candidate nodes.
42    pub fn is_empty(&self) -> bool {
43        self.node_type.is_empty()
44    }
45
46    /// Build a batch of AND nodes with the given child lists.
47    ///
48    /// This helper is primarily used by tests.
49    pub fn and_or_batch(children: Vec<Vec<u32>>) -> Self {
50        let num_nodes = children.len();
51        let mut node_type = vec![PIR_AND; num_nodes];
52        let leaf_id = vec![0u32; num_nodes];
53        let decision_var = vec![0u32; num_nodes];
54        let decision_child_false = vec![0u32; num_nodes];
55        let decision_child_true = vec![0u32; num_nodes];
56
57        let mut child_offsets = Vec::with_capacity(num_nodes + 1);
58        let mut flat_children = Vec::new();
59        child_offsets.push(0);
60        for kids in children {
61            flat_children.extend(kids);
62            child_offsets.push(flat_children.len() as u32);
63        }
64
65        if node_type.is_empty() {
66            node_type = Vec::new();
67        }
68
69        Self {
70            node_type,
71            leaf_id,
72            decision_var,
73            decision_child_false,
74            decision_child_true,
75            child_offsets,
76            children: flat_children,
77        }
78    }
79
80    /// Validates the batch layout and uploads it to the device.
81    pub fn to_device(&self, provider: &Arc<CudaKernelProvider>) -> Result<GpuPirBatch> {
82        let num_nodes = self.node_type.len();
83        if self.leaf_id.len() != num_nodes
84            || self.decision_var.len() != num_nodes
85            || self.decision_child_false.len() != num_nodes
86            || self.decision_child_true.len() != num_nodes
87        {
88            return Err(XlogError::Compilation(
89                "PirBatch: array length mismatch".to_string(),
90            ));
91        }
92        if self.child_offsets.len() != num_nodes + 1 {
93            return Err(XlogError::Compilation(
94                "PirBatch: child_offsets must be len num_nodes+1".to_string(),
95            ));
96        }
97        if let Some(&last) = self.child_offsets.last() {
98            if last as usize != self.children.len() {
99                return Err(XlogError::Compilation(
100                    "PirBatch: child_offsets last entry must equal children len".to_string(),
101                ));
102            }
103        }
104
105        let memory = provider.memory();
106
107        let mut d_node_type = memory.alloc::<u8>(num_nodes)?;
108        let mut d_leaf_id = memory.alloc::<u32>(num_nodes)?;
109        let mut d_decision_var = memory.alloc::<u32>(num_nodes)?;
110        let mut d_decision_child_false = memory.alloc::<u32>(num_nodes)?;
111        let mut d_decision_child_true = memory.alloc::<u32>(num_nodes)?;
112        let mut d_child_offsets = memory.alloc::<u32>(self.child_offsets.len())?;
113        let mut d_children = memory.alloc::<u32>(self.children.len())?;
114
115        provider
116            .htod_sync_copy_into_tracked(&self.node_type, &mut d_node_type)
117            .map_err(|e| XlogError::Kernel(format!("PirBatch upload node_type: {}", e)))?;
118        provider
119            .htod_sync_copy_into_tracked(&self.leaf_id, &mut d_leaf_id)
120            .map_err(|e| XlogError::Kernel(format!("PirBatch upload leaf_id: {}", e)))?;
121        provider
122            .htod_sync_copy_into_tracked(&self.decision_var, &mut d_decision_var)
123            .map_err(|e| XlogError::Kernel(format!("PirBatch upload decision_var: {}", e)))?;
124        provider
125            .htod_sync_copy_into_tracked(&self.decision_child_false, &mut d_decision_child_false)
126            .map_err(|e| {
127                XlogError::Kernel(format!("PirBatch upload decision_child_false: {}", e))
128            })?;
129        provider
130            .htod_sync_copy_into_tracked(&self.decision_child_true, &mut d_decision_child_true)
131            .map_err(|e| {
132                XlogError::Kernel(format!("PirBatch upload decision_child_true: {}", e))
133            })?;
134        provider
135            .htod_sync_copy_into_tracked(&self.child_offsets, &mut d_child_offsets)
136            .map_err(|e| XlogError::Kernel(format!("PirBatch upload child_offsets: {}", e)))?;
137        provider
138            .htod_sync_copy_into_tracked(&self.children, &mut d_children)
139            .map_err(|e| XlogError::Kernel(format!("PirBatch upload children: {}", e)))?;
140
141        Ok(GpuPirBatch {
142            node_type: d_node_type,
143            leaf_id: d_leaf_id,
144            decision_var: d_decision_var,
145            decision_child_false: d_decision_child_false,
146            decision_child_true: d_decision_child_true,
147            child_offsets: d_child_offsets,
148            children: d_children,
149        })
150    }
151}
152
153/// Device-resident PIR batch.
154pub struct GpuPirBatch {
155    /// Device opcodes for candidate nodes.
156    pub node_type: TrackedCudaSlice<u8>,
157    /// Device leaf identifiers for candidate literal nodes.
158    pub leaf_id: TrackedCudaSlice<u32>,
159    /// Device choice-variable identifiers for candidate decision nodes.
160    pub decision_var: TrackedCudaSlice<u32>,
161    /// Device false-branch children for candidate decision nodes.
162    pub decision_child_false: TrackedCudaSlice<u32>,
163    /// Device true-branch children for candidate decision nodes.
164    pub decision_child_true: TrackedCudaSlice<u32>,
165    /// Device CSR offsets into [`Self::children`].
166    pub child_offsets: TrackedCudaSlice<u32>,
167    /// Device-resident flattened child identifiers.
168    pub children: TrackedCudaSlice<u32>,
169}
170
171impl GpuPirBatch {
172    /// Returns the number of candidate nodes.
173    pub fn num_nodes(&self) -> usize {
174        self.node_type.len()
175    }
176
177    /// Returns the number of flattened child references.
178    pub fn num_children(&self) -> usize {
179        self.children.len()
180    }
181}
182
183/// GPU PIR interner (device-side).
184pub struct GpuPirInterner {
185    provider: Arc<CudaKernelProvider>,
186    node_cap: u32,
187    child_cap: u32,
188    graph: GpuPirGraph,
189    graph_hashes: TrackedCudaSlice<u64>,
190    num_nodes: TrackedCudaSlice<u32>,
191    num_children: TrackedCudaSlice<u32>,
192}
193
194impl GpuPirInterner {
195    /// Allocates an interner with fixed node and child capacities.
196    pub fn new(provider: &Arc<CudaKernelProvider>, node_cap: u32, child_cap: u32) -> Result<Self> {
197        if node_cap < 2 {
198            return Err(XlogError::Compilation(
199                "GpuPirInterner requires node_cap >= 2 for const nodes".to_string(),
200            ));
201        }
202        let memory = provider.memory();
203        let device = provider.device().inner();
204
205        let mut node_type = memory.alloc::<u8>(node_cap as usize)?;
206        let mut child_offsets = memory.alloc::<u32>((node_cap as usize) + 1)?;
207        let mut children = memory.alloc::<u32>(child_cap as usize)?;
208        let mut leaf_id = memory.alloc::<u32>(node_cap as usize)?;
209        let mut decision_var = memory.alloc::<u32>(node_cap as usize)?;
210        let mut decision_child_false = memory.alloc::<u32>(node_cap as usize)?;
211        let mut decision_child_true = memory.alloc::<u32>(node_cap as usize)?;
212        let mut graph_hashes = memory.alloc::<u64>(node_cap as usize)?;
213
214        device
215            .memset_zeros(&mut node_type)
216            .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init node_type: {}", e)))?;
217        device
218            .memset_zeros(&mut child_offsets)
219            .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init child_offsets: {}", e)))?;
220        device
221            .memset_zeros(&mut children)
222            .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init children: {}", e)))?;
223        device
224            .memset_zeros(&mut decision_var)
225            .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init decision_var: {}", e)))?;
226        device
227            .memset_zeros(&mut decision_child_false)
228            .map_err(|e| {
229                XlogError::Kernel(format!("GpuPirInterner init decision_child_false: {}", e))
230            })?;
231        device.memset_zeros(&mut decision_child_true).map_err(|e| {
232            XlogError::Kernel(format!("GpuPirInterner init decision_child_true: {}", e))
233        })?;
234        device
235            .memset_zeros(&mut graph_hashes)
236            .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init graph_hashes: {}", e)))?;
237
238        let mut leaf_id_host = vec![0u32; node_cap as usize];
239        if node_cap > 1 {
240            leaf_id_host[1] = 1;
241        }
242        provider
243            .htod_launch_metadata_sync_copy_into(&leaf_id_host, &mut leaf_id)
244            .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init leaf_id: {}", e)))?;
245
246        let hash_fn = device
247            .get_func(PIR_MODULE, pir_kernels::PIR_HASH_KEYS)
248            .ok_or_else(|| XlogError::Kernel("pir_hash_keys not found".to_string()))?;
249        let num_const = 2u32;
250        let block_size = 256u32;
251        let grid_const = num_const.div_ceil(block_size);
252        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
253        unsafe {
254            hash_fn.clone().launch(
255                LaunchConfig {
256                    grid_dim: (grid_const.max(1), 1, 1),
257                    block_dim: (block_size, 1, 1),
258                    shared_mem_bytes: 0,
259                },
260                (
261                    &node_type,
262                    &leaf_id,
263                    &decision_var,
264                    &decision_child_false,
265                    &decision_child_true,
266                    &child_offsets,
267                    &children,
268                    num_const,
269                    &mut graph_hashes,
270                ),
271            )
272        }
273        .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init hash: {}", e)))?;
274
275        let mut num_nodes = memory.alloc::<u32>(1)?;
276        let mut num_children = memory.alloc::<u32>(1)?;
277        provider
278            .htod_launch_metadata_sync_copy_into(&[2u32], &mut num_nodes)
279            .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init num_nodes: {}", e)))?;
280        provider
281            .htod_launch_metadata_sync_copy_into(&[0u32], &mut num_children)
282            .map_err(|e| XlogError::Kernel(format!("GpuPirInterner init num_children: {}", e)))?;
283
284        Ok(Self {
285            provider: Arc::clone(provider),
286            node_cap,
287            child_cap,
288            graph: GpuPirGraph {
289                node_type,
290                child_offsets,
291                children,
292                leaf_id,
293                decision_var,
294                decision_child_false,
295                decision_child_true,
296            },
297            graph_hashes,
298            num_nodes,
299            num_children,
300        })
301    }
302
303    /// Returns the device-resident interned graph.
304    pub fn graph(&self) -> &GpuPirGraph {
305        &self.graph
306    }
307
308    /// Uploads and interns a host batch, returning canonical node identifiers.
309    pub fn intern_batch(&mut self, batch: &PirBatch) -> Result<TrackedCudaSlice<u32>> {
310        if batch.node_type.contains(&PIR_CONST) {
311            return Err(XlogError::Compilation(
312                "GpuPirInterner does not accept PIR_CONST in batches".to_string(),
313            ));
314        }
315        let mut device_batch = batch.to_device(&self.provider)?;
316        self.intern_device_batch(&mut device_batch)
317    }
318
319    /// Interns a device-resident batch and returns canonical node identifiers.
320    pub fn intern_device_batch(
321        &mut self,
322        batch: &mut GpuPirBatch,
323    ) -> Result<TrackedCudaSlice<u32>> {
324        let num_nodes = batch.num_nodes();
325        if num_nodes == 0 {
326            return self.provider.memory().alloc::<u32>(0);
327        }
328        let num_nodes_u32 = u32::try_from(num_nodes).map_err(|_| {
329            XlogError::Compilation("GpuPirInterner: num_nodes overflow".to_string())
330        })?;
331        let num_children = batch.num_children();
332        let num_children_u32 = u32::try_from(num_children).map_err(|_| {
333            XlogError::Compilation("GpuPirInterner: num_children overflow".to_string())
334        })?;
335
336        let device = self.provider.device().inner();
337        let memory = self.provider.memory();
338        let block_size = 256u32;
339
340        // Canonicalize AND/OR children (sort + dedup) into new buffers.
341        let mut canon_child_offsets = memory.alloc::<u32>(num_nodes + 1)?;
342        let mut canon_children = memory.alloc::<u32>(num_children)?;
343
344        if num_children_u32 == 0 {
345            device.memset_zeros(&mut canon_child_offsets).map_err(|e| {
346                XlogError::Kernel(format!("GpuPirInterner zero child_offsets: {}", e))
347            })?;
348        } else {
349            let mut parent_ids = memory.alloc::<u32>(num_children)?;
350            let fill_fn = device
351                .get_func(PIR_MODULE, pir_kernels::PIR_FILL_CHILD_PARENTS)
352                .ok_or_else(|| XlogError::Kernel("pir_fill_child_parents not found".to_string()))?;
353            let grid_nodes = num_nodes_u32.div_ceil(block_size);
354            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
355            unsafe {
356                fill_fn.clone().launch(
357                    LaunchConfig {
358                        grid_dim: (grid_nodes, 1, 1),
359                        block_dim: (block_size, 1, 1),
360                        shared_mem_bytes: 0,
361                    },
362                    (&batch.child_offsets, num_nodes_u32, &mut parent_ids),
363                )
364            }
365            .map_err(|e| XlogError::Kernel(format!("pir_fill_child_parents failed: {}", e)))?;
366
367            let mut sort_scratch = RadixSortScratch::new(&self.provider, num_children_u32)?;
368            self.provider.radix_sort_u32_pairs(
369                &mut batch.children,
370                &mut parent_ids,
371                num_children_u32,
372                &mut sort_scratch,
373            )?;
374            self.provider.radix_sort_u32_pairs(
375                &mut parent_ids,
376                &mut batch.children,
377                num_children_u32,
378                &mut sort_scratch,
379            )?;
380
381            let mut pair_unique_mask = memory.alloc::<u8>(num_children)?;
382            let mark_pairs = device
383                .get_func(PIR_MODULE, pir_kernels::PIR_MARK_UNIQUE_PAIRS)
384                .ok_or_else(|| XlogError::Kernel("pir_mark_unique_pairs not found".to_string()))?;
385            let grid_pairs = num_children_u32.div_ceil(block_size);
386            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
387            unsafe {
388                mark_pairs.clone().launch(
389                    LaunchConfig {
390                        grid_dim: (grid_pairs, 1, 1),
391                        block_dim: (block_size, 1, 1),
392                        shared_mem_bytes: 0,
393                    },
394                    (
395                        &parent_ids,
396                        &batch.children,
397                        num_children_u32,
398                        &mut pair_unique_mask,
399                    ),
400                )
401            }
402            .map_err(|e| XlogError::Kernel(format!("pir_mark_unique_pairs failed: {}", e)))?;
403
404            let pair_prefix = self
405                .provider
406                .scan_u8_mask_device(&pair_unique_mask, num_children_u32)?;
407
408            let mut unique_pairs_total = memory.alloc::<u32>(1)?;
409            device
410                .memset_zeros(&mut unique_pairs_total)
411                .map_err(|e| XlogError::Kernel(format!("zero unique_pairs_total: {}", e)))?;
412            let count_mask = device
413                .get_func(SCAN_MODULE, scan_kernels::COUNT_MASK)
414                .ok_or_else(|| XlogError::Kernel("count_mask not found".to_string()))?;
415            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
416            unsafe {
417                count_mask.clone().launch(
418                    LaunchConfig {
419                        grid_dim: (grid_pairs, 1, 1),
420                        block_dim: (block_size, 1, 1),
421                        shared_mem_bytes: 0,
422                    },
423                    (&pair_unique_mask, num_children_u32, &mut unique_pairs_total),
424                )
425            }
426            .map_err(|e| XlogError::Kernel(format!("count_mask (pairs) failed: {}", e)))?;
427
428            let mut canon_parent = memory.alloc::<u32>(num_children)?;
429            let compact_pairs = device
430                .get_func(PIR_MODULE, pir_kernels::PIR_COMPACT_PAIRS)
431                .ok_or_else(|| XlogError::Kernel("pir_compact_pairs not found".to_string()))?;
432            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
433            unsafe {
434                compact_pairs.clone().launch(
435                    LaunchConfig {
436                        grid_dim: (grid_pairs, 1, 1),
437                        block_dim: (block_size, 1, 1),
438                        shared_mem_bytes: 0,
439                    },
440                    (
441                        &parent_ids,
442                        &batch.children,
443                        &pair_unique_mask,
444                        &pair_prefix,
445                        num_children_u32,
446                        &mut canon_parent,
447                        &mut canon_children,
448                    ),
449                )
450            }
451            .map_err(|e| XlogError::Kernel(format!("pir_compact_pairs failed: {}", e)))?;
452
453            let mut child_counts = memory.alloc::<u32>(num_nodes)?;
454            device
455                .memset_zeros(&mut child_counts)
456                .map_err(|e| XlogError::Kernel(format!("zero child_counts: {}", e)))?;
457            let count_children = device
458                .get_func(PIR_MODULE, pir_kernels::PIR_COUNT_CHILDREN)
459                .ok_or_else(|| XlogError::Kernel("pir_count_children not found".to_string()))?;
460            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
461            unsafe {
462                count_children.clone().launch(
463                    LaunchConfig {
464                        grid_dim: (grid_pairs, 1, 1),
465                        block_dim: (block_size, 1, 1),
466                        shared_mem_bytes: 0,
467                    },
468                    (
469                        &canon_parent,
470                        &unique_pairs_total,
471                        num_nodes_u32,
472                        &mut child_counts,
473                    ),
474                )
475            }
476            .map_err(|e| XlogError::Kernel(format!("pir_count_children failed: {}", e)))?;
477
478            self.provider
479                .exclusive_scan_u32_inplace(&mut child_counts, num_nodes_u32)?;
480
481            let write_offsets = device
482                .get_func(PIR_MODULE, pir_kernels::PIR_WRITE_CHILD_OFFSETS)
483                .ok_or_else(|| {
484                    XlogError::Kernel("pir_write_child_offsets not found".to_string())
485                })?;
486            let grid_nodes = num_nodes_u32.div_ceil(block_size);
487            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
488            unsafe {
489                write_offsets.clone().launch(
490                    LaunchConfig {
491                        grid_dim: (grid_nodes.max(1), 1, 1),
492                        block_dim: (block_size, 1, 1),
493                        shared_mem_bytes: 0,
494                    },
495                    (
496                        &child_counts,
497                        num_nodes_u32,
498                        &unique_pairs_total,
499                        &mut canon_child_offsets,
500                    ),
501                )
502            }
503            .map_err(|e| XlogError::Kernel(format!("pir_write_child_offsets failed: {}", e)))?;
504        }
505
506        // Hash and pack keys for nodes.
507        let mut hashes = memory.alloc::<u64>(num_nodes)?;
508        let hash_fn = device
509            .get_func(PIR_MODULE, pir_kernels::PIR_HASH_KEYS)
510            .ok_or_else(|| XlogError::Kernel("pir_hash_keys not found".to_string()))?;
511        let grid_nodes = num_nodes_u32.div_ceil(block_size);
512        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
513        unsafe {
514            hash_fn.clone().launch(
515                LaunchConfig {
516                    grid_dim: (grid_nodes, 1, 1),
517                    block_dim: (block_size, 1, 1),
518                    shared_mem_bytes: 0,
519                },
520                (
521                    &batch.node_type,
522                    &batch.leaf_id,
523                    &batch.decision_var,
524                    &batch.decision_child_false,
525                    &batch.decision_child_true,
526                    &canon_child_offsets,
527                    &canon_children,
528                    num_nodes_u32,
529                    &mut hashes,
530                ),
531            )
532        }
533        .map_err(|e| XlogError::Kernel(format!("pir_hash_keys failed: {}", e)))?;
534
535        let mut key_tag = memory.alloc::<u32>(num_nodes)?;
536        let mut key_payload = memory.alloc::<u32>(num_nodes)?;
537        let mut key_child_len = memory.alloc::<u32>(num_nodes)?;
538        let pack_fn = device
539            .get_func(PIR_MODULE, pir_kernels::PIR_PACK_KEYS)
540            .ok_or_else(|| XlogError::Kernel("pir_pack_keys not found".to_string()))?;
541        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
542        unsafe {
543            pack_fn.clone().launch(
544                LaunchConfig {
545                    grid_dim: (grid_nodes, 1, 1),
546                    block_dim: (block_size, 1, 1),
547                    shared_mem_bytes: 0,
548                },
549                (
550                    &batch.node_type,
551                    &batch.leaf_id,
552                    &batch.decision_var,
553                    &batch.decision_child_false,
554                    &batch.decision_child_true,
555                    &canon_child_offsets,
556                    num_nodes_u32,
557                    &mut key_tag,
558                    &mut key_payload,
559                    &mut key_child_len,
560                ),
561            )
562        }
563        .map_err(|e| XlogError::Kernel(format!("pir_pack_keys failed: {}", e)))?;
564
565        // Sort indices by (hash, tag, payload, len).
566        let mut indices = memory.alloc::<u32>(num_nodes)?;
567        self.provider.init_indices(&mut indices, num_nodes_u32)?;
568        let mut keys = memory.alloc::<u32>(num_nodes)?;
569        let mut node_sort = RadixSortScratch::new(&self.provider, num_nodes_u32)?;
570
571        self.provider
572            .gather_u32_by_indices(&key_child_len, &indices, &mut keys, num_nodes_u32)?;
573        self.provider.radix_sort_u32_pairs(
574            &mut keys,
575            &mut indices,
576            num_nodes_u32,
577            &mut node_sort,
578        )?;
579
580        self.provider
581            .gather_u32_by_indices(&key_payload, &indices, &mut keys, num_nodes_u32)?;
582        self.provider.radix_sort_u32_pairs(
583            &mut keys,
584            &mut indices,
585            num_nodes_u32,
586            &mut node_sort,
587        )?;
588
589        self.provider
590            .gather_u32_by_indices(&key_tag, &indices, &mut keys, num_nodes_u32)?;
591        self.provider.radix_sort_u32_pairs(
592            &mut keys,
593            &mut indices,
594            num_nodes_u32,
595            &mut node_sort,
596        )?;
597
598        self.provider
599            .gather_u64_lo_by_indices(&hashes, &indices, &mut keys, num_nodes_u32)?;
600        self.provider.radix_sort_u32_pairs(
601            &mut keys,
602            &mut indices,
603            num_nodes_u32,
604            &mut node_sort,
605        )?;
606
607        self.provider
608            .gather_u64_hi_by_indices(&hashes, &indices, &mut keys, num_nodes_u32)?;
609        self.provider.radix_sort_u32_pairs(
610            &mut keys,
611            &mut indices,
612            num_nodes_u32,
613            &mut node_sort,
614        )?;
615
616        // Gather sorted node arrays.
617        let mut sorted_node_type = memory.alloc::<u8>(num_nodes)?;
618        let mut sorted_leaf_id = memory.alloc::<u32>(num_nodes)?;
619        let mut sorted_decision_var = memory.alloc::<u32>(num_nodes)?;
620        let mut sorted_decision_child_false = memory.alloc::<u32>(num_nodes)?;
621        let mut sorted_decision_child_true = memory.alloc::<u32>(num_nodes)?;
622
623        self.provider.gather_u8_by_indices(
624            &batch.node_type,
625            &indices,
626            &mut sorted_node_type,
627            num_nodes_u32,
628        )?;
629        self.provider.gather_u32_by_indices(
630            &batch.leaf_id,
631            &indices,
632            &mut sorted_leaf_id,
633            num_nodes_u32,
634        )?;
635        self.provider.gather_u32_by_indices(
636            &batch.decision_var,
637            &indices,
638            &mut sorted_decision_var,
639            num_nodes_u32,
640        )?;
641        self.provider.gather_u32_by_indices(
642            &batch.decision_child_false,
643            &indices,
644            &mut sorted_decision_child_false,
645            num_nodes_u32,
646        )?;
647        self.provider.gather_u32_by_indices(
648            &batch.decision_child_true,
649            &indices,
650            &mut sorted_decision_child_true,
651            num_nodes_u32,
652        )?;
653
654        // Build sorted child offsets/children.
655        let mut sorted_child_len = memory.alloc::<u32>(num_nodes)?;
656        self.provider.gather_u32_by_indices(
657            &key_child_len,
658            &indices,
659            &mut sorted_child_len,
660            num_nodes_u32,
661        )?;
662        self.provider
663            .exclusive_scan_u32_inplace(&mut sorted_child_len, num_nodes_u32)?;
664
665        let mut sorted_child_offsets = memory.alloc::<u32>(num_nodes + 1)?;
666        let write_offsets = device
667            .get_func(PIR_MODULE, pir_kernels::PIR_WRITE_CHILD_OFFSETS)
668            .ok_or_else(|| XlogError::Kernel("pir_write_child_offsets not found".to_string()))?;
669        let total_children_view = canon_child_offsets.slice(num_nodes..(num_nodes + 1));
670        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
671        unsafe {
672            write_offsets.clone().launch(
673                LaunchConfig {
674                    grid_dim: (grid_nodes.max(1), 1, 1),
675                    block_dim: (block_size, 1, 1),
676                    shared_mem_bytes: 0,
677                },
678                (
679                    &sorted_child_len,
680                    num_nodes_u32,
681                    &total_children_view,
682                    &mut sorted_child_offsets,
683                ),
684            )
685        }
686        .map_err(|e| XlogError::Kernel(format!("pir_write_child_offsets(sorted) failed: {}", e)))?;
687
688        let mut sorted_children = memory.alloc::<u32>(num_children)?;
689        let gather_children = device
690            .get_func(PIR_MODULE, pir_kernels::PIR_GATHER_CHILDREN)
691            .ok_or_else(|| XlogError::Kernel("pir_gather_children not found".to_string()))?;
692        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
693        unsafe {
694            gather_children.clone().launch(
695                LaunchConfig {
696                    grid_dim: (grid_nodes, 1, 1),
697                    block_dim: (block_size, 1, 1),
698                    shared_mem_bytes: 0,
699                },
700                (
701                    &indices,
702                    &canon_child_offsets,
703                    &canon_children,
704                    &sorted_child_offsets,
705                    num_nodes_u32,
706                    &mut sorted_children,
707                ),
708            )
709        }
710        .map_err(|e| XlogError::Kernel(format!("pir_gather_children failed: {}", e)))?;
711
712        // Recompute hashes in sorted order for uniqueness checks.
713        let mut sorted_hashes = memory.alloc::<u64>(num_nodes)?;
714        let hash_fn = device
715            .get_func(PIR_MODULE, pir_kernels::PIR_HASH_KEYS)
716            .ok_or_else(|| XlogError::Kernel("pir_hash_keys not found".to_string()))?;
717        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
718        unsafe {
719            hash_fn.clone().launch(
720                LaunchConfig {
721                    grid_dim: (grid_nodes, 1, 1),
722                    block_dim: (block_size, 1, 1),
723                    shared_mem_bytes: 0,
724                },
725                (
726                    &sorted_node_type,
727                    &sorted_leaf_id,
728                    &sorted_decision_var,
729                    &sorted_decision_child_false,
730                    &sorted_decision_child_true,
731                    &sorted_child_offsets,
732                    &sorted_children,
733                    num_nodes_u32,
734                    &mut sorted_hashes,
735                ),
736            )
737        }
738        .map_err(|e| XlogError::Kernel(format!("pir_hash_keys(sorted) failed: {}", e)))?;
739
740        let hash_table = self
741            .provider
742            .build_hash_table_u64(&self.graph_hashes, self.node_cap)?;
743        let existing_id = memory.alloc::<u32>(num_nodes)?;
744        let find_existing = device
745            .get_func(PIR_MODULE, pir_kernels::PIR_FIND_EXISTING)
746            .ok_or_else(|| XlogError::Kernel("pir_find_existing not found".to_string()))?;
747        let mut find_params: Vec<*mut c_void> = vec![
748            (&sorted_hashes).as_kernel_param(),
749            (&sorted_node_type).as_kernel_param(),
750            (&sorted_leaf_id).as_kernel_param(),
751            (&sorted_decision_var).as_kernel_param(),
752            (&sorted_decision_child_false).as_kernel_param(),
753            (&sorted_decision_child_true).as_kernel_param(),
754            (&sorted_child_offsets).as_kernel_param(),
755            (&sorted_children).as_kernel_param(),
756            num_nodes_u32.as_kernel_param(),
757            (&self.graph.node_type).as_kernel_param(),
758            (&self.graph.child_offsets).as_kernel_param(),
759            (&self.graph.children).as_kernel_param(),
760            (&self.graph.leaf_id).as_kernel_param(),
761            (&self.graph.decision_var).as_kernel_param(),
762            (&self.graph.decision_child_false).as_kernel_param(),
763            (&self.graph.decision_child_true).as_kernel_param(),
764            (&self.num_nodes).as_kernel_param(),
765            (&hash_table.bucket_offsets).as_kernel_param(),
766            (&hash_table.bucket_counts).as_kernel_param(),
767            (&hash_table.bucket_entries).as_kernel_param(),
768            (&hash_table.bucket_entry_hashes).as_kernel_param(),
769            hash_table.bucket_mask.as_kernel_param(),
770            (&existing_id).as_kernel_param(),
771        ];
772        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
773        unsafe {
774            find_existing.clone().launch(
775                LaunchConfig {
776                    grid_dim: (grid_nodes, 1, 1),
777                    block_dim: (block_size, 1, 1),
778                    shared_mem_bytes: 0,
779                },
780                &mut find_params,
781            )
782        }
783        .map_err(|e| XlogError::Kernel(format!("pir_find_existing failed: {}", e)))?;
784
785        // Mark unique nodes in sorted order.
786        let mut unique_mask = memory.alloc::<u8>(num_nodes)?;
787        let mark_unique = device
788            .get_func(PIR_MODULE, pir_kernels::PIR_MARK_UNIQUE)
789            .ok_or_else(|| XlogError::Kernel("pir_mark_unique not found".to_string()))?;
790        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
791        unsafe {
792            mark_unique.clone().launch(
793                LaunchConfig {
794                    grid_dim: (grid_nodes, 1, 1),
795                    block_dim: (block_size, 1, 1),
796                    shared_mem_bytes: 0,
797                },
798                (
799                    &sorted_hashes,
800                    &sorted_node_type,
801                    &sorted_leaf_id,
802                    &sorted_decision_var,
803                    &sorted_decision_child_false,
804                    &sorted_decision_child_true,
805                    &sorted_child_offsets,
806                    &sorted_children,
807                    num_nodes_u32,
808                    &mut unique_mask,
809                ),
810            )
811        }
812        .map_err(|e| XlogError::Kernel(format!("pir_mark_unique failed: {}", e)))?;
813
814        let mut new_mask = memory.alloc::<u8>(num_nodes)?;
815        let mark_new = device
816            .get_func(PIR_MODULE, pir_kernels::PIR_MARK_NEW_GROUPS)
817            .ok_or_else(|| XlogError::Kernel("pir_mark_new_groups not found".to_string()))?;
818        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
819        unsafe {
820            mark_new.clone().launch(
821                LaunchConfig {
822                    grid_dim: (grid_nodes, 1, 1),
823                    block_dim: (block_size, 1, 1),
824                    shared_mem_bytes: 0,
825                },
826                (&unique_mask, &existing_id, num_nodes_u32, &mut new_mask),
827            )
828        }
829        .map_err(|e| XlogError::Kernel(format!("pir_mark_new_groups failed: {}", e)))?;
830
831        let unique_prefix = self
832            .provider
833            .scan_u8_mask_device(&unique_mask, num_nodes_u32)?;
834
835        let new_prefix = self
836            .provider
837            .scan_u8_mask_device(&new_mask, num_nodes_u32)?;
838
839        let mut new_nodes_total = memory.alloc::<u32>(1)?;
840        device
841            .memset_zeros(&mut new_nodes_total)
842            .map_err(|e| XlogError::Kernel(format!("zero new_nodes_total: {}", e)))?;
843        let count_mask = device
844            .get_func(SCAN_MODULE, scan_kernels::COUNT_MASK)
845            .ok_or_else(|| XlogError::Kernel("count_mask not found".to_string()))?;
846        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
847        unsafe {
848            count_mask.clone().launch(
849                LaunchConfig {
850                    grid_dim: (grid_nodes, 1, 1),
851                    block_dim: (block_size, 1, 1),
852                    shared_mem_bytes: 0,
853                },
854                (&new_mask, num_nodes_u32, &mut new_nodes_total),
855            )
856        }
857        .map_err(|e| XlogError::Kernel(format!("count_mask (new nodes) failed: {}", e)))?;
858
859        let mut group_node_id = memory.alloc::<u32>(num_nodes)?;
860        let build_group = device
861            .get_func(PIR_MODULE, pir_kernels::PIR_BUILD_GROUP_IDS)
862            .ok_or_else(|| XlogError::Kernel("pir_build_group_ids not found".to_string()))?;
863        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
864        unsafe {
865            build_group.clone().launch(
866                LaunchConfig {
867                    grid_dim: (grid_nodes, 1, 1),
868                    block_dim: (block_size, 1, 1),
869                    shared_mem_bytes: 0,
870                },
871                (
872                    &unique_mask,
873                    &unique_prefix,
874                    &existing_id,
875                    &new_prefix,
876                    &self.num_nodes,
877                    num_nodes_u32,
878                    &mut group_node_id,
879                ),
880            )
881        }
882        .map_err(|e| XlogError::Kernel(format!("pir_build_group_ids failed: {}", e)))?;
883
884        let mut graph_child_counts = memory.alloc::<u32>(num_nodes)?;
885        let build_counts = device
886            .get_func(PIR_MODULE, pir_kernels::PIR_BUILD_GRAPH_CHILD_COUNTS)
887            .ok_or_else(|| {
888                XlogError::Kernel("pir_build_graph_child_counts not found".to_string())
889            })?;
890        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
891        unsafe {
892            build_counts.clone().launch(
893                LaunchConfig {
894                    grid_dim: (grid_nodes, 1, 1),
895                    block_dim: (block_size, 1, 1),
896                    shared_mem_bytes: 0,
897                },
898                (
899                    &sorted_node_type,
900                    &sorted_child_offsets,
901                    &new_mask,
902                    num_nodes_u32,
903                    &mut graph_child_counts,
904                ),
905            )
906        }
907        .map_err(|e| XlogError::Kernel(format!("pir_build_graph_child_counts failed: {}", e)))?;
908
909        let mut graph_children_total = memory.alloc::<u32>(1)?;
910        device
911            .memset_zeros(&mut graph_children_total)
912            .map_err(|e| XlogError::Kernel(format!("zero graph_children_total: {}", e)))?;
913        let sum_counts = device
914            .get_func(PIR_MODULE, pir_kernels::PIR_SUM_COUNTS)
915            .ok_or_else(|| XlogError::Kernel("pir_sum_counts not found".to_string()))?;
916        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
917        unsafe {
918            sum_counts.clone().launch(
919                LaunchConfig {
920                    grid_dim: (grid_nodes, 1, 1),
921                    block_dim: (block_size, 1, 1),
922                    shared_mem_bytes: 0,
923                },
924                (
925                    &graph_child_counts,
926                    num_nodes_u32,
927                    &mut graph_children_total,
928                ),
929            )
930        }
931        .map_err(|e| XlogError::Kernel(format!("pir_sum_counts failed: {}", e)))?;
932
933        self.provider
934            .exclusive_scan_u32_inplace(&mut graph_child_counts, num_nodes_u32)?;
935
936        let out_ids = memory.alloc::<u32>(num_nodes)?;
937        let emit = device
938            .get_func(PIR_MODULE, pir_kernels::PIR_EMIT_NODES_AND_IDS)
939            .ok_or_else(|| XlogError::Kernel("pir_emit_nodes_and_ids not found".to_string()))?;
940        let mut emit_params: Vec<*mut c_void> = vec![
941            (&sorted_node_type).as_kernel_param(),
942            (&sorted_leaf_id).as_kernel_param(),
943            (&sorted_decision_var).as_kernel_param(),
944            (&sorted_decision_child_false).as_kernel_param(),
945            (&sorted_decision_child_true).as_kernel_param(),
946            (&sorted_child_offsets).as_kernel_param(),
947            (&sorted_children).as_kernel_param(),
948            (&unique_mask).as_kernel_param(),
949            (&unique_prefix).as_kernel_param(),
950            (&group_node_id).as_kernel_param(),
951            (&graph_child_counts).as_kernel_param(),
952            (&indices).as_kernel_param(),
953            num_nodes_u32.as_kernel_param(),
954            (&self.num_nodes).as_kernel_param(),
955            (&self.num_children).as_kernel_param(),
956            self.node_cap.as_kernel_param(),
957            self.child_cap.as_kernel_param(),
958            (&self.graph.node_type).as_kernel_param(),
959            (&self.graph.child_offsets).as_kernel_param(),
960            (&self.graph.children).as_kernel_param(),
961            (&self.graph.leaf_id).as_kernel_param(),
962            (&self.graph.decision_var).as_kernel_param(),
963            (&self.graph.decision_child_false).as_kernel_param(),
964            (&self.graph.decision_child_true).as_kernel_param(),
965            (&new_mask).as_kernel_param(),
966            (&sorted_hashes).as_kernel_param(),
967            (&self.graph_hashes).as_kernel_param(),
968            (&out_ids).as_kernel_param(),
969        ];
970        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
971        unsafe {
972            emit.clone().launch(
973                LaunchConfig {
974                    grid_dim: (grid_nodes, 1, 1),
975                    block_dim: (block_size, 1, 1),
976                    shared_mem_bytes: 0,
977                },
978                &mut emit_params,
979            )
980        }
981        .map_err(|e| XlogError::Kernel(format!("pir_emit_nodes_and_ids failed: {}", e)))?;
982
983        let update_counts = device
984            .get_func(PIR_MODULE, pir_kernels::PIR_UPDATE_COUNTS)
985            .ok_or_else(|| XlogError::Kernel("pir_update_counts not found".to_string()))?;
986        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
987        unsafe {
988            update_counts.clone().launch(
989                LaunchConfig {
990                    grid_dim: (1, 1, 1),
991                    block_dim: (1, 1, 1),
992                    shared_mem_bytes: 0,
993                },
994                (
995                    &new_nodes_total,
996                    &graph_children_total,
997                    self.node_cap,
998                    self.child_cap,
999                    &mut self.num_nodes,
1000                    &mut self.num_children,
1001                ),
1002            )
1003        }
1004        .map_err(|e| XlogError::Kernel(format!("pir_update_counts failed: {}", e)))?;
1005        // No device synchronize: returns device-resident IDs; same-stream ordering suffices.
1006        Ok(out_ids)
1007    }
1008}