Skip to main content

xlog_cuda/provider/
wcoj_metadata.rs

1use std::collections::BTreeMap;
2use std::ffi::c_void;
3
4use cudarc::driver::sys;
5use xlog_core::{AggOp, Result, ScalarType, Schema, XlogError};
6
7use super::{arith_kernels, wcoj_kernels, CudaKernelProvider, ARITH_MODULE, WCOJ_MODULE};
8use crate::device_runtime::StreamId;
9use crate::launch::LaunchRecorder;
10use crate::memory::{CudaColumn, DeviceMemoryView, DeviceRead, TrackedCudaSlice};
11use crate::wcoj_metadata::{
12    Wcoj4CycleRootAggValue, WcojCycle4HgWorkPlanU32, WcojCycle4HgWorkPlanU64, WcojRelationMetadata,
13    WcojRootAggValue, WcojTriangleHgCountPhaseU32, WcojTriangleHgWorkPlanU32,
14    WcojTriangleHgWorkPlanU64,
15};
16use crate::{AsKernelParam, CudaBuffer, LaunchAsync, LaunchConfig};
17
18const BLOCK_SIZE: u32 = 256;
19const HG_COUNT_BLOCK_SIZE: u32 = 512;
20
21impl CudaKernelProvider {
22    pub fn wcoj_build_metadata_u32_recorded(
23        &self,
24        input: &CudaBuffer,
25        key_col_idx: usize,
26        launch_stream: StreamId,
27    ) -> Result<WcojRelationMetadata<u32>> {
28        self.validate_metadata_column(input, key_col_idx, MetadataWidth::U32)?;
29        let keys = &metadata_column_u32(input, key_col_idx)?;
30        self.build_metadata_u32_from_column(input, key_col_idx, keys, launch_stream)
31    }
32
33    pub fn wcoj_build_metadata_u64_recorded(
34        &self,
35        input: &CudaBuffer,
36        key_col_idx: usize,
37        launch_stream: StreamId,
38    ) -> Result<WcojRelationMetadata<u64>> {
39        self.validate_metadata_column(input, key_col_idx, MetadataWidth::U64)?;
40        let keys = &metadata_column_u64(input, key_col_idx)?;
41        self.build_metadata_u64_from_column(input, key_col_idx, keys, launch_stream)
42    }
43
44    pub fn wcoj_triangle_hg_work_plan_u32_recorded(
45        &self,
46        e_xy: &CudaBuffer,
47        e_yz: &CudaBuffer,
48        e_xz: &CudaBuffer,
49        block_work_unit: u32,
50        launch_stream: StreamId,
51    ) -> Result<WcojTriangleHgWorkPlanU32> {
52        let ctx = "wcoj_triangle_hg_work_plan_u32_recorded";
53        if block_work_unit == 0 {
54            return Err(XlogError::Kernel(format!(
55                "{ctx}: block_work_unit must be nonzero"
56            )));
57        }
58        validate_binary_u32(ctx, "e_xy", e_xy)?;
59        validate_binary_u32(ctx, "e_yz", e_yz)?;
60        validate_binary_u32(ctx, "e_xz", e_xz)?;
61
62        let n_xy = self.metadata_logical_rows(e_xy)?;
63        let n_yz = self.metadata_logical_rows(e_yz)?;
64        let n_xz = self.metadata_logical_rows(e_xz)?;
65        let prefix_len = n_xy
66            .checked_add(1)
67            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: prefix length overflow")))?;
68        let mut xy_work_prefix = self.memory().alloc::<u32>(prefix_len as usize)?;
69        let mut xy_yz_start = self.memory().alloc::<u32>(n_xy as usize)?;
70        let mut xy_yz_end = self.memory().alloc::<u32>(n_xy as usize)?;
71        let mut xy_xz_start = self.memory().alloc::<u32>(n_xy as usize)?;
72        let mut xy_xz_end = self.memory().alloc::<u32>(n_xy as usize)?;
73
74        if n_xy == 0 {
75            let block_counts = self.memory().alloc::<u32>(1)?;
76            let block_offsets = self.memory().alloc::<u32>(1)?;
77            let scratch_x = self.memory().alloc::<u32>(1)?;
78            let scratch_y = self.memory().alloc::<u32>(1)?;
79            let scratch_z = self.memory().alloc::<u32>(1)?;
80            return Ok(WcojTriangleHgWorkPlanU32 {
81                xy_work_prefix,
82                xy_yz_start,
83                xy_yz_end,
84                xy_xz_start,
85                xy_xz_end,
86                block_counts,
87                block_offsets,
88                scratch_x,
89                scratch_y,
90                scratch_z,
91                total_work: 0,
92                block_work_unit,
93                row_count: 0,
94            });
95        }
96
97        let runtime = self.memory().runtime().ok_or_else(|| {
98            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
99        })?;
100        let cu_stream = runtime
101            .stream_pool()
102            .resolve(launch_stream)
103            .ok_or_else(|| {
104                XlogError::Kernel(format!(
105                    "{ctx}: launch_stream StreamId({}) does not resolve",
106                    launch_stream.0
107                ))
108            })?;
109
110        let xy_col0 = &metadata_column_u32(e_xy, 0)?;
111        let xy_col1 = &metadata_column_u32(e_xy, 1)?;
112        let yz_col0 = &metadata_column_u32(e_yz, 0)?;
113        let xz_col0 = &metadata_column_u32(e_xz, 0)?;
114
115        let mut scan_scratch = self.multiblock_scan_u32_scratch_for_len(prefix_len)?;
116        let mut rec = LaunchRecorder::new_strict(launch_stream);
117        rec.read(e_xy.num_rows_device());
118        rec.read(e_yz.num_rows_device());
119        rec.read(e_xz.num_rows_device());
120        rec.read_column(e_xy.column(0).expect("xy.col0"));
121        rec.read_column(e_xy.column(1).expect("xy.col1"));
122        rec.read_column(e_yz.column(0).expect("yz.col0"));
123        rec.read_column(e_xz.column(0).expect("xz.col0"));
124        rec.write(&xy_work_prefix);
125        rec.write(&xy_yz_start);
126        rec.write(&xy_yz_end);
127        rec.write(&xy_xz_start);
128        rec.write(&xy_xz_end);
129        for level in scan_scratch.levels() {
130            rec.read_write(level);
131        }
132        rec.preflight(runtime)
133            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
134        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
135            let kernel = self
136                .device()
137                .inner()
138                .get_func(
139                    WCOJ_MODULE,
140                    wcoj_kernels::WCOJ_TRIANGLE_BUILD_HG_WORK_PLAN_U32,
141                )
142                .ok_or_else(|| {
143                    XlogError::Kernel(
144                        "wcoj_triangle_build_hg_work_plan_u32 kernel not found".to_string(),
145                    )
146                })?;
147            let grid = n_xy.div_ceil(BLOCK_SIZE);
148            unsafe {
149                kernel
150                    .clone()
151                    .launch_in(
152                        stream,
153                        LaunchConfig {
154                            grid_dim: (grid, 1, 1),
155                            block_dim: (BLOCK_SIZE, 1, 1),
156                            shared_mem_bytes: 0,
157                        },
158                        (
159                            xy_col0,
160                            xy_col1,
161                            n_xy,
162                            yz_col0,
163                            n_yz,
164                            xz_col0,
165                            n_xz,
166                            &mut xy_work_prefix,
167                            &mut xy_yz_start,
168                            &mut xy_yz_end,
169                            &mut xy_xz_start,
170                            &mut xy_xz_end,
171                        ),
172                    )
173                    .map_err(|e| {
174                        XlogError::Kernel(format!(
175                            "wcoj_triangle_build_hg_work_plan_u32 launch failed: {e}"
176                        ))
177                    })?;
178            }
179            self.multiblock_scan_u32_inplace_on_stream(
180                &mut xy_work_prefix,
181                prefix_len,
182                stream,
183                &mut scan_scratch,
184            )?;
185            Ok(())
186        };
187        // SAFETY: preflight retained the accessed buffers and bound this stream.
188        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
189            .map_err(|error| error.into_xlog_error())?;
190
191        rec.commit()
192            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
193        cu_stream
194            .synchronize()
195            .map_err(|e| XlogError::Kernel(format!("{ctx}: stream sync failed: {e}")))?;
196        let total_work = self.dtoh_scalar_untracked::<u32>(&xy_work_prefix, n_xy as usize)?;
197        let scratch_slots = if total_work == 0 {
198            1usize
199        } else {
200            let grid = total_work.div_ceil(block_work_unit);
201            (grid as usize)
202                .checked_mul(block_work_unit as usize)
203                .ok_or_else(|| XlogError::Kernel(format!("{ctx}: scratch slot overflow")))?
204        };
205        let scratch_x = self.memory().alloc::<u32>(scratch_slots)?;
206        let scratch_y = self.memory().alloc::<u32>(scratch_slots)?;
207        let scratch_z = self.memory().alloc::<u32>(scratch_slots)?;
208        let grid = if total_work == 0 {
209            1
210        } else {
211            total_work.div_ceil(block_work_unit)
212        };
213        let block_counts = self.memory().alloc::<u32>(grid as usize)?;
214        let block_offsets = self.memory().alloc::<u32>(grid as usize)?;
215
216        Ok(WcojTriangleHgWorkPlanU32 {
217            xy_work_prefix,
218            xy_yz_start,
219            xy_yz_end,
220            xy_xz_start,
221            xy_xz_end,
222            block_counts,
223            block_offsets,
224            scratch_x,
225            scratch_y,
226            scratch_z,
227            total_work,
228            block_work_unit,
229            row_count: n_xy,
230        })
231    }
232
233    pub fn wcoj_triangle_count_hg_u32_recorded(
234        &self,
235        e_yz: &CudaBuffer,
236        e_xz: &CudaBuffer,
237        plan: &WcojTriangleHgWorkPlanU32,
238        launch_stream: StreamId,
239    ) -> Result<CudaBuffer> {
240        let ctx = "wcoj_triangle_count_hg_u32_recorded";
241        validate_binary_u32(ctx, "e_yz", e_yz)?;
242        validate_binary_u32(ctx, "e_xz", e_xz)?;
243        let n_yz = self.metadata_logical_rows(e_yz)?;
244        let n_xz = self.metadata_logical_rows(e_xz)?;
245        let grid = if plan.total_work == 0 {
246            1
247        } else {
248            plan.total_work.div_ceil(plan.block_work_unit)
249        };
250        let bytes_count = (grid as usize)
251            .checked_mul(std::mem::size_of::<u32>())
252            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: count byte size overflow")))?;
253        let mut count_bytes = self.memory().alloc::<u8>(bytes_count)?;
254        let d_num_rows = self.memory().alloc::<u32>(1)?;
255
256        let runtime = self.memory().runtime().ok_or_else(|| {
257            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
258        })?;
259        let cu_stream = runtime
260            .stream_pool()
261            .resolve(launch_stream)
262            .ok_or_else(|| {
263                XlogError::Kernel(format!(
264                    "{ctx}: launch_stream StreamId({}) does not resolve",
265                    launch_stream.0
266                ))
267            })?;
268        let yz_col1 = &metadata_column_u32(e_yz, 1)?;
269        let xz_col1 = &metadata_column_u32(e_xz, 1)?;
270
271        let mut rec = LaunchRecorder::new_strict(launch_stream);
272        rec.read(e_yz.num_rows_device());
273        rec.read(e_xz.num_rows_device());
274        rec.read_column(e_yz.column(1).expect("yz.col1"));
275        rec.read_column(e_xz.column(1).expect("xz.col1"));
276        rec.read(&plan.xy_work_prefix);
277        rec.read(&plan.xy_yz_start);
278        rec.read(&plan.xy_yz_end);
279        rec.read(&plan.xy_xz_start);
280        rec.read(&plan.xy_xz_end);
281        rec.write(&count_bytes);
282        rec.write(&d_num_rows);
283        rec.preflight(runtime)
284            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
285        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
286            self.initialize_launch_metadata_u32(
287                grid,
288                &d_num_rows,
289                stream,
290                &format!("{ctx}: d_num_rows"),
291            )?;
292
293            let kernel = self
294                .device()
295                .inner()
296                .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_TRIANGLE_COUNT_HG_U32)
297                .ok_or_else(|| {
298                    XlogError::Kernel("wcoj_triangle_count_hg_u32 kernel not found".to_string())
299                })?;
300            let count_u32 = &mut reinterpret_u8_as_u32(&mut count_bytes)?;
301            let mut params: Vec<*mut c_void> = vec![
302                yz_col1.as_kernel_param(),
303                n_yz.as_kernel_param(),
304                xz_col1.as_kernel_param(),
305                n_xz.as_kernel_param(),
306                (&plan.xy_work_prefix).as_kernel_param(),
307                (&plan.xy_yz_start).as_kernel_param(),
308                (&plan.xy_yz_end).as_kernel_param(),
309                (&plan.xy_xz_start).as_kernel_param(),
310                (&plan.xy_xz_end).as_kernel_param(),
311                plan.row_count.as_kernel_param(),
312                plan.total_work.as_kernel_param(),
313                plan.block_work_unit.as_kernel_param(),
314                count_u32.as_kernel_param(),
315            ];
316            unsafe {
317                kernel
318                    .clone()
319                    .launch_in(
320                        stream,
321                        LaunchConfig {
322                            grid_dim: (grid, 1, 1),
323                            block_dim: (BLOCK_SIZE, 1, 1),
324                            shared_mem_bytes: 0,
325                        },
326                        &mut params,
327                    )
328                    .map_err(|e| XlogError::Kernel(format!("{ctx}: launch failed: {e}")))?;
329            }
330            Ok(())
331        };
332        // SAFETY: preflight retained the accessed buffers and bound this stream.
333        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
334            .map_err(|error| error.into_xlog_error())?;
335
336        rec.commit()
337            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
338        cu_stream
339            .synchronize()
340            .map_err(|e| XlogError::Kernel(format!("{ctx}: stream sync failed: {e}")))?;
341
342        let schema = Schema::new(vec![("count".to_string(), ScalarType::U32)]);
343        Ok(CudaBuffer::from_columns_with_host_count(
344            vec![count_bytes.into()],
345            grid as u64,
346            d_num_rows,
347            schema,
348            grid,
349        ))
350    }
351
352    pub fn wcoj_triangle_hg_u32_recorded(
353        &self,
354        e_xy: &CudaBuffer,
355        e_yz: &CudaBuffer,
356        e_xz: &CudaBuffer,
357        block_work_unit: u32,
358        launch_stream: StreamId,
359    ) -> Result<CudaBuffer> {
360        let ctx = "wcoj_triangle_hg_u32_recorded";
361        validate_binary_u32(ctx, "e_xy", e_xy)?;
362        validate_binary_u32(ctx, "e_yz", e_yz)?;
363        validate_binary_u32(ctx, "e_xz", e_xz)?;
364        let plan = self.wcoj_triangle_hg_work_plan_u32_recorded(
365            e_xy,
366            e_yz,
367            e_xz,
368            block_work_unit,
369            launch_stream,
370        )?;
371        self.wcoj_triangle_hg_u32_with_plan_recorded(e_xy, e_yz, e_xz, &plan, launch_stream)
372    }
373
374    /// Aggregate-fused triangle group-by-root count: evaluate
375    /// `q(X, count) :- e_xy(X,Y), e_yz(Y,Z), e_xz(X,Z)` grouped by the
376    /// variable-order root X, WITHOUT materializing the triangle rows.
377    ///
378    /// Pipeline (all recorded; the triangle result never exists as rows):
379    /// 1. the standard histogram-guided work plan;
380    /// 2. `wcoj_triangle_groupby_root_count_hg_u32` accumulates per-e_xy-row
381    ///    match counts (integer atomicAdd — order-insensitive, deterministic
382    ///    values) into a zero-initialized `n_xy`-long array;
383    /// 3. a 2-column (X, count) staging buffer over the *input* rows is
384    ///    compacted to count>0 rows (group-by over the join result must not
385    ///    emit roots with no completion) and reduced per X via the recorded
386    ///    groupby Sum (rows are already X-sorted because e_xy is lex-sorted).
387    ///
388    /// All reduction work is O(n_xy) — input-sized, never join-output-sized.
389    ///
390    /// Output schema matches the unfused materialize+groupby-count baseline:
391    /// `col0` = X (e_xy.col0 type, U32/Symbol), `col1` = count (U64).
392    ///
393    /// # Errors
394    /// * `XlogError::Kernel` if the manager has no runtime, the launch
395    ///   stream does not resolve, an input is not 2-column U32/Symbol, or
396    ///   any kernel launch fails.
397    pub fn wcoj_triangle_groupby_root_count_u32_recorded(
398        &self,
399        e_xy: &CudaBuffer,
400        e_yz: &CudaBuffer,
401        e_xz: &CudaBuffer,
402        block_work_unit: u32,
403        launch_stream: StreamId,
404    ) -> Result<CudaBuffer> {
405        let ctx = "wcoj_triangle_groupby_root_count_u32_recorded";
406        // Layout-normalize per dispatch (sorted-fast-path clone when the
407        // input is already lex-sorted + unique): the fused path must give
408        // the same guarantee as the unfused pipeline instead of trusting
409        // store-buffer sortedness — unsorted/duplicated inputs previously
410        // produced silently wrong (empty) fused results.
411        let e_xy = &self.wcoj_layout_u32_recorded(e_xy, launch_stream)?;
412        let e_yz = &self.wcoj_layout_u32_recorded(e_yz, launch_stream)?;
413        let e_xz = &self.wcoj_layout_u32_recorded(e_xz, launch_stream)?;
414        validate_binary_u32(ctx, "e_xy", e_xy)?;
415        validate_binary_u32(ctx, "e_yz", e_yz)?;
416        validate_binary_u32(ctx, "e_xz", e_xz)?;
417        let plan = self.wcoj_triangle_hg_work_plan_u32_recorded(
418            e_xy,
419            e_yz,
420            e_xz,
421            block_work_unit,
422            launch_stream,
423        )?;
424        let n_xy = plan.row_count;
425        let x_type = e_xy.schema().column_type(0).expect("xy.col0 type");
426        let out_schema = Schema::new(vec![
427            ("x".to_string(), x_type),
428            ("count".to_string(), ScalarType::U64),
429        ]);
430        if n_xy == 0 || plan.total_work == 0 {
431            return self.create_empty_buffer(out_schema);
432        }
433
434        let runtime = self.memory().runtime().ok_or_else(|| {
435            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
436        })?;
437        let cu_stream = runtime
438            .stream_pool()
439            .resolve(launch_stream)
440            .ok_or_else(|| {
441                XlogError::Kernel(format!(
442                    "{ctx}: launch_stream StreamId({}) does not resolve",
443                    launch_stream.0
444                ))
445            })?;
446
447        let yz_col1 = &metadata_column_u32(e_yz, 1)?;
448        let xz_col1 = &metadata_column_u32(e_xz, 1)?;
449        let n_yz = self.metadata_logical_rows(e_yz)?;
450        let n_xz = self.metadata_logical_rows(e_xz)?;
451
452        // Per-e_xy-row match counters, zero-initialized. Allocated as the
453        // u8-backed column layout so the array doubles as the staging
454        // buffer's count column after the kernel fills it.
455        let mut row_counts = self
456            .memory()
457            .alloc::<u8>(n_xy as usize * std::mem::size_of::<u32>())?;
458        self.device()
459            .inner()
460            .memset_zeros(&mut row_counts)
461            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row counts failed: {e}")))?;
462
463        let grid = plan.total_work.div_ceil(plan.block_work_unit);
464        let mut rec = LaunchRecorder::new_strict(launch_stream);
465        rec.read(e_xy.num_rows_device());
466        rec.read(e_yz.num_rows_device());
467        rec.read(e_xz.num_rows_device());
468        rec.read_column(e_yz.column(1).expect("yz.col1"));
469        rec.read_column(e_xz.column(1).expect("xz.col1"));
470        rec.read(&plan.xy_work_prefix);
471        rec.read(&plan.xy_yz_start);
472        rec.read(&plan.xy_yz_end);
473        rec.read(&plan.xy_xz_start);
474        rec.read(&plan.xy_xz_end);
475        rec.write(&row_counts);
476        rec.preflight(runtime)
477            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
478        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
479            {
480                let kernel = self
481                    .device()
482                    .inner()
483                    .get_func(
484                        WCOJ_MODULE,
485                        wcoj_kernels::WCOJ_TRIANGLE_GROUPBY_ROOT_COUNT_HG_U32,
486                    )
487                    .ok_or_else(|| {
488                        XlogError::Kernel(
489                            "wcoj_triangle_groupby_root_count_hg_u32 kernel not found".to_string(),
490                        )
491                    })?;
492                let mut params: Vec<*mut c_void> = vec![
493                    yz_col1.as_kernel_param(),
494                    n_yz.as_kernel_param(),
495                    xz_col1.as_kernel_param(),
496                    n_xz.as_kernel_param(),
497                    (&plan.xy_work_prefix).as_kernel_param(),
498                    (&plan.xy_yz_start).as_kernel_param(),
499                    (&plan.xy_yz_end).as_kernel_param(),
500                    (&plan.xy_xz_start).as_kernel_param(),
501                    (&plan.xy_xz_end).as_kernel_param(),
502                    plan.row_count.as_kernel_param(),
503                    plan.total_work.as_kernel_param(),
504                    plan.block_work_unit.as_kernel_param(),
505                    (&row_counts).as_kernel_param(),
506                ];
507                unsafe {
508                    kernel
509                        .clone()
510                        .launch_in(
511                            stream,
512                            LaunchConfig {
513                                grid_dim: (grid, 1, 1),
514                                block_dim: (BLOCK_SIZE, 1, 1),
515                                shared_mem_bytes: 0,
516                            },
517                            &mut params,
518                        )
519                        .map_err(|e| {
520                            XlogError::Kernel(format!("{ctx}: groupby-count launch failed: {e}"))
521                        })?;
522                }
523            }
524            Ok(())
525        };
526        // SAFETY: preflight retained the accessed buffers and bound this stream.
527        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
528            .map_err(|error| error.into_xlog_error())?;
529
530        rec.commit()
531            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
532
533        // Staging buffer (X, count) over the n_xy input rows: X is a
534        // device-to-device copy of e_xy.col0; the count column is the
535        // kernel-filled array. Rows stay lex-sorted by X.
536        let x_src = match e_xy.column(0).expect("xy.col0") {
537            CudaColumn::Owned(slice) => slice,
538            _ => {
539                return Err(XlogError::Kernel(format!(
540                    "{ctx}: e_xy.col0 must be an owned CudaColumn"
541                )))
542            }
543        };
544        let x_copy = self
545            .memory()
546            .alloc::<u8>(n_xy as usize * std::mem::size_of::<u32>())?;
547        // Explicit-length copy: layout-normalized columns are allocated at
548        // capacity, which can exceed the logical n_xy * 4 bytes a full-slice
549        // typed copy would assert on.
550        unsafe {
551            let res = sys::cuMemcpyDtoD_v2(
552                *x_copy.device_ptr(),
553                *x_src.device_ptr(),
554                n_xy as usize * std::mem::size_of::<u32>(),
555            );
556            if res != sys::cudaError_enum::CUDA_SUCCESS {
557                return Err(XlogError::Kernel(format!(
558                    "{ctx}: copy X column failed: {res:?}"
559                )));
560            }
561        }
562        let mut d_num_rows = self.memory().alloc::<u32>(1)?;
563        self.device()
564            .inner()
565            .dtod_copy(e_xy.num_rows_device(), &mut d_num_rows)
566            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy row count failed: {e}")))?;
567        let staging_schema = Schema::new(vec![
568            ("x".to_string(), x_type),
569            ("count".to_string(), ScalarType::U32),
570        ]);
571        let staging = CudaBuffer::from_columns_with_host_count(
572            vec![x_copy.into(), row_counts.into()],
573            n_xy as u64,
574            d_num_rows,
575            staging_schema,
576            n_xy,
577        );
578
579        // Keep only roots with at least one completed triangle, then reduce
580        // per X. Both steps run over input-sized data.
581        let mask = self.compare_const_mask_recorded::<u32>(
582            &staging,
583            1,
584            0u32,
585            crate::CompareOp::Gt,
586            launch_stream,
587        )?;
588        let compacted =
589            self.compact_buffer_by_device_mask_counted_recorded(&staging, &mask, launch_stream)?;
590        self.groupby_multi_agg_recorded(
591            &compacted,
592            &[0],
593            &[(1, xlog_core::AggOp::Sum)],
594            launch_stream,
595        )
596    }
597
598    /// Aggregate-fused triangle group-by-root sum/min/max: evaluate
599    /// `q(X, agg(V)) :- e_xy(X,Y), e_yz(Y,Z), e_xz(X,Z)` with
600    /// `agg ∈ {Sum, Min, Max}` and `V ∈ {Y, Z}` grouped by the
601    /// variable-order root X, WITHOUT materializing the triangle rows.
602    ///
603    /// Pipeline (all recorded; the triangle result never exists as rows):
604    /// 1. the standard histogram-guided work plan;
605    /// 2. the per-op fused kernel accumulates, per e_xy row, a match count
606    ///    (compaction mask) and the per-row partial aggregate (integer
607    ///    atomics — order-insensitive, deterministic values). Sum partials
608    ///    are u64 (a per-row partial can exceed `u32::MAX`); min partials
609    ///    start at `u32::MAX`, max partials at 0;
610    /// 3. a 3-column (X, count, agg) staging buffer over the *input* rows
611    ///    is compacted to count>0 rows (groups with no completion must be
612    ///    absent) and reduced per X via the recorded groupby with the same
613    ///    `AggOp` (Sum over the u64 partials; Min/Max over u32).
614    ///
615    /// All reduction work is O(n_xy) — input-sized, never join-output-sized.
616    ///
617    /// Output schema matches the unfused materialize+groupby baseline:
618    /// `col0` = X (e_xy.col0 type, U32/Symbol), `col1` = U64 for Sum,
619    /// U32 for Min/Max.
620    ///
621    /// Bag semantics: every (Y, Z) completion contributes its value,
622    /// exactly like aggregating the materialized projection.
623    ///
624    /// # Errors
625    /// * `XlogError::Kernel` if `agg_op` is not Sum/Min/Max, the value
626    ///   columns are not plain U32, the manager has no runtime, the launch
627    ///   stream does not resolve, an input is not 2-column U32/Symbol, or
628    ///   any kernel launch fails.
629    #[expect(
630        clippy::too_many_arguments,
631        reason = "three relation inputs and launch controls mirror the recorded kernel contract"
632    )]
633    pub fn wcoj_triangle_groupby_root_agg_u32_recorded(
634        &self,
635        e_xy: &CudaBuffer,
636        e_yz: &CudaBuffer,
637        e_xz: &CudaBuffer,
638        agg_op: AggOp,
639        value: WcojRootAggValue,
640        block_work_unit: u32,
641        launch_stream: StreamId,
642    ) -> Result<CudaBuffer> {
643        let ctx = "wcoj_triangle_groupby_root_agg_u32_recorded";
644        // Layout-normalize per dispatch (sorted-fast-path clone when the
645        // input is already lex-sorted + unique): the fused path must give
646        // the same guarantee as the unfused pipeline instead of trusting
647        // store-buffer sortedness — unsorted/duplicated inputs previously
648        // produced silently wrong (empty) fused results.
649        let e_xy = &self.wcoj_layout_u32_recorded(e_xy, launch_stream)?;
650        let e_yz = &self.wcoj_layout_u32_recorded(e_yz, launch_stream)?;
651        let e_xz = &self.wcoj_layout_u32_recorded(e_xz, launch_stream)?;
652        let (kernel_name, agg_elem_size, agg_scalar, agg_name) = match agg_op {
653            AggOp::Sum => (
654                wcoj_kernels::WCOJ_TRIANGLE_GROUPBY_ROOT_SUM_HG_U32,
655                std::mem::size_of::<u64>(),
656                ScalarType::U64,
657                "sum_0",
658            ),
659            AggOp::Min => (
660                wcoj_kernels::WCOJ_TRIANGLE_GROUPBY_ROOT_MIN_HG_U32,
661                std::mem::size_of::<u32>(),
662                ScalarType::U32,
663                "min_0",
664            ),
665            AggOp::Max => (
666                wcoj_kernels::WCOJ_TRIANGLE_GROUPBY_ROOT_MAX_HG_U32,
667                std::mem::size_of::<u32>(),
668                ScalarType::U32,
669                "max_0",
670            ),
671            other => {
672                return Err(XlogError::Kernel(format!(
673                    "{ctx}: unsupported AggOp {other:?} (Sum/Min/Max only; use \
674                     wcoj_triangle_groupby_root_count_u32_recorded for Count)"
675                )))
676            }
677        };
678        validate_binary_u32(ctx, "e_xy", e_xy)?;
679        validate_binary_u32(ctx, "e_yz", e_yz)?;
680        validate_binary_u32(ctx, "e_xz", e_xz)?;
681        // The aggregate value is arithmetic: require plain U32 value
682        // columns (Symbol ids are not summable/orderable data).
683        let value_cols: &[(&CudaBuffer, &str)] = match value {
684            WcojRootAggValue::Y => &[(e_xy, "e_xy")],
685            WcojRootAggValue::Z => &[(e_yz, "e_yz"), (e_xz, "e_xz")],
686        };
687        for (buf, label) in value_cols {
688            let ty = buf.schema().column_type(1).expect("validated 2-col");
689            if ty != ScalarType::U32 {
690                return Err(XlogError::Kernel(format!(
691                    "{ctx}: {label}.col1 supplies the aggregate value and must be U32, got {ty:?}"
692                )));
693            }
694        }
695
696        let plan = self.wcoj_triangle_hg_work_plan_u32_recorded(
697            e_xy,
698            e_yz,
699            e_xz,
700            block_work_unit,
701            launch_stream,
702        )?;
703        let n_xy = plan.row_count;
704        let x_type = e_xy.schema().column_type(0).expect("xy.col0 type");
705        let out_schema = Schema::new(vec![
706            ("x".to_string(), x_type),
707            (agg_name.to_string(), agg_scalar),
708        ]);
709        if n_xy == 0 || plan.total_work == 0 {
710            return self.create_empty_buffer(out_schema);
711        }
712
713        let runtime = self.memory().runtime().ok_or_else(|| {
714            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
715        })?;
716        let cu_stream = runtime
717            .stream_pool()
718            .resolve(launch_stream)
719            .ok_or_else(|| {
720                XlogError::Kernel(format!(
721                    "{ctx}: launch_stream StreamId({}) does not resolve",
722                    launch_stream.0
723                ))
724            })?;
725
726        let yz_col1 = &metadata_column_u32(e_yz, 1)?;
727        let xz_col1 = &metadata_column_u32(e_xz, 1)?;
728        let xy_col1 = &metadata_column_u32(e_xy, 1)?;
729        let n_yz = self.metadata_logical_rows(e_yz)?;
730        let n_xz = self.metadata_logical_rows(e_xz)?;
731        let value_from_z: u32 = match value {
732            WcojRootAggValue::Y => 0,
733            WcojRootAggValue::Z => 1,
734        };
735
736        // Per-e_xy-row match counters + aggregate partials, allocated as
737        // the u8-backed column layout so the arrays double as the staging
738        // buffer's columns after the kernel fills them.
739        let mut row_counts = self
740            .memory()
741            .alloc::<u8>(n_xy as usize * std::mem::size_of::<u32>())?;
742        self.device()
743            .inner()
744            .memset_zeros(&mut row_counts)
745            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row counts failed: {e}")))?;
746        let mut row_agg = self.memory().alloc::<u8>(n_xy as usize * agg_elem_size)?;
747        self.device()
748            .inner()
749            .memset_zeros(&mut row_agg)
750            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row aggregates failed: {e}")))?;
751
752        let grid = plan.total_work.div_ceil(plan.block_work_unit);
753        let mut rec = LaunchRecorder::new_strict(launch_stream);
754        rec.read(e_xy.num_rows_device());
755        rec.read(e_yz.num_rows_device());
756        rec.read(e_xz.num_rows_device());
757        rec.read_column(e_yz.column(1).expect("yz.col1"));
758        rec.read_column(e_xz.column(1).expect("xz.col1"));
759        rec.read_column(e_xy.column(1).expect("xy.col1"));
760        rec.read(&plan.xy_work_prefix);
761        rec.read(&plan.xy_yz_start);
762        rec.read(&plan.xy_yz_end);
763        rec.read(&plan.xy_xz_start);
764        rec.read(&plan.xy_xz_end);
765        rec.write(&row_counts);
766        rec.write(&row_agg);
767        rec.preflight(runtime)
768            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
769        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
770            if matches!(agg_op, AggOp::Min) {
771                // Min identity: u32::MAX (compaction drops untouched rows).
772                let fill = self
773                    .device()
774                    .inner()
775                    .get_func(ARITH_MODULE, arith_kernels::ARITH_FILL_CONST_U32)
776                    .ok_or_else(|| {
777                        XlogError::Kernel("arith_fill_const_u32 kernel not found".to_string())
778                    })?;
779                let row_agg_u32 = &mut reinterpret_u8_as_u32(&mut row_agg)?;
780                // SAFETY: arith_fill_const_u32(value, n, output)
781                unsafe {
782                    fill.clone()
783                        .launch_in(
784                            stream,
785                            LaunchConfig::for_num_elems(n_xy),
786                            (u32::MAX, n_xy, &mut *row_agg_u32),
787                        )
788                        .map_err(|e| {
789                            XlogError::Kernel(format!("{ctx}: min identity fill failed: {e}"))
790                        })?;
791                }
792            }
793            {
794                let kernel = self
795                    .device()
796                    .inner()
797                    .get_func(WCOJ_MODULE, kernel_name)
798                    .ok_or_else(|| XlogError::Kernel(format!("{kernel_name} kernel not found")))?;
799                let mut params: Vec<*mut c_void> = vec![
800                    yz_col1.as_kernel_param(),
801                    n_yz.as_kernel_param(),
802                    xz_col1.as_kernel_param(),
803                    n_xz.as_kernel_param(),
804                    xy_col1.as_kernel_param(),
805                    value_from_z.as_kernel_param(),
806                    (&plan.xy_work_prefix).as_kernel_param(),
807                    (&plan.xy_yz_start).as_kernel_param(),
808                    (&plan.xy_yz_end).as_kernel_param(),
809                    (&plan.xy_xz_start).as_kernel_param(),
810                    (&plan.xy_xz_end).as_kernel_param(),
811                    plan.row_count.as_kernel_param(),
812                    plan.total_work.as_kernel_param(),
813                    plan.block_work_unit.as_kernel_param(),
814                    (&row_counts).as_kernel_param(),
815                    (&row_agg).as_kernel_param(),
816                ];
817                unsafe {
818                    kernel
819                        .clone()
820                        .launch_in(
821                            stream,
822                            LaunchConfig {
823                                grid_dim: (grid, 1, 1),
824                                block_dim: (BLOCK_SIZE, 1, 1),
825                                shared_mem_bytes: 0,
826                            },
827                            &mut params,
828                        )
829                        .map_err(|e| {
830                            XlogError::Kernel(format!("{ctx}: groupby-agg launch failed: {e}"))
831                        })?;
832                }
833            }
834            Ok(())
835        };
836        // SAFETY: preflight retained the accessed buffers and bound this stream.
837        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
838            .map_err(|error| error.into_xlog_error())?;
839
840        rec.commit()
841            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
842
843        // Staging buffer (X, count, agg) over the n_xy input rows: X is a
844        // device-to-device copy of e_xy.col0; count and agg are the
845        // kernel-filled arrays. Rows stay lex-sorted by X.
846        let x_src = match e_xy.column(0).expect("xy.col0") {
847            CudaColumn::Owned(slice) => slice,
848            _ => {
849                return Err(XlogError::Kernel(format!(
850                    "{ctx}: e_xy.col0 must be an owned CudaColumn"
851                )))
852            }
853        };
854        let x_copy = self
855            .memory()
856            .alloc::<u8>(n_xy as usize * std::mem::size_of::<u32>())?;
857        // Explicit-length copy: layout-normalized columns are allocated at
858        // capacity, which can exceed the logical n_xy * 4 bytes a full-slice
859        // typed copy would assert on.
860        unsafe {
861            let res = sys::cuMemcpyDtoD_v2(
862                *x_copy.device_ptr(),
863                *x_src.device_ptr(),
864                n_xy as usize * std::mem::size_of::<u32>(),
865            );
866            if res != sys::cudaError_enum::CUDA_SUCCESS {
867                return Err(XlogError::Kernel(format!(
868                    "{ctx}: copy X column failed: {res:?}"
869                )));
870            }
871        }
872        let mut d_num_rows = self.memory().alloc::<u32>(1)?;
873        self.device()
874            .inner()
875            .dtod_copy(e_xy.num_rows_device(), &mut d_num_rows)
876            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy row count failed: {e}")))?;
877        let staging_schema = Schema::new(vec![
878            ("x".to_string(), x_type),
879            ("count".to_string(), ScalarType::U32),
880            ("agg".to_string(), agg_scalar),
881        ]);
882        let staging = CudaBuffer::from_columns_with_host_count(
883            vec![x_copy.into(), row_counts.into(), row_agg.into()],
884            n_xy as u64,
885            d_num_rows,
886            staging_schema,
887            n_xy,
888        );
889
890        // Keep only roots with at least one completed triangle, then reduce
891        // per X with the same AggOp. Both steps run over input-sized data.
892        let mask = self.compare_const_mask_recorded::<u32>(
893            &staging,
894            1,
895            0u32,
896            crate::CompareOp::Gt,
897            launch_stream,
898        )?;
899        let compacted =
900            self.compact_buffer_by_device_mask_counted_recorded(&staging, &mask, launch_stream)?;
901        self.groupby_multi_agg_recorded(&compacted, &[0], &[(2, agg_op)], launch_stream)
902    }
903
904    /// U64-key aggregate-fused triangle count sibling of
905    /// [`Self::wcoj_triangle_groupby_root_count_u32_recorded`]: evaluate
906    /// `q(X, count)` over the triangle shape grouped by the root X for U64
907    /// relations, WITHOUT materializing the triangle rows.
908    ///
909    /// The recorded groupby is U32/Symbol-key only, so the per-X reduction
910    /// reuses the WCOJ relation metadata instead: e_xy is lex-sorted, so
911    /// `wcoj_build_metadata_u64_recorded` yields one (unique X, group start)
912    /// pair per root, and `wcoj_groupby_root_segment_sum_counts_u32`
913    /// accumulates the per-row match counts into per-unique-root u64
914    /// totals (integer atomicAdd — deterministic). Roots with zero
915    /// completions are compacted away. All reduction work is O(n_xy).
916    ///
917    /// Output schema matches the unfused materialize+groupby baseline:
918    /// `col0` = X (U64), `col1` = count (U64).
919    ///
920    /// # Errors
921    /// * `XlogError::Kernel` if the manager has no runtime, the launch
922    ///   stream does not resolve, an input is not 2-column U64, or any
923    ///   kernel launch fails.
924    pub fn wcoj_triangle_groupby_root_count_u64_recorded(
925        &self,
926        e_xy: &CudaBuffer,
927        e_yz: &CudaBuffer,
928        e_xz: &CudaBuffer,
929        block_work_unit: u32,
930        launch_stream: StreamId,
931    ) -> Result<CudaBuffer> {
932        let ctx = "wcoj_triangle_groupby_root_count_u64_recorded";
933        // Layout-normalize per dispatch (sorted-fast-path clone when the
934        // input is already lex-sorted + unique): the fused path must give
935        // the same guarantee as the unfused pipeline instead of trusting
936        // store-buffer sortedness — unsorted/duplicated inputs previously
937        // produced silently wrong (empty) fused results.
938        let e_xy = &self.wcoj_layout_u64_recorded(e_xy, launch_stream)?;
939        let e_yz = &self.wcoj_layout_u64_recorded(e_yz, launch_stream)?;
940        let e_xz = &self.wcoj_layout_u64_recorded(e_xz, launch_stream)?;
941        validate_binary_u64(ctx, "e_xy", e_xy)?;
942        validate_binary_u64(ctx, "e_yz", e_yz)?;
943        validate_binary_u64(ctx, "e_xz", e_xz)?;
944        let plan = self.wcoj_triangle_hg_work_plan_u64_recorded(
945            e_xy,
946            e_yz,
947            e_xz,
948            block_work_unit,
949            launch_stream,
950        )?;
951        let n_xy = plan.row_count;
952        let out_schema = Schema::new(vec![
953            ("x".to_string(), ScalarType::U64),
954            ("count".to_string(), ScalarType::U64),
955        ]);
956        if n_xy == 0 || plan.total_work == 0 {
957            return self.create_empty_buffer(out_schema);
958        }
959
960        let runtime = self.memory().runtime().ok_or_else(|| {
961            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
962        })?;
963        let cu_stream = runtime
964            .stream_pool()
965            .resolve(launch_stream)
966            .ok_or_else(|| {
967                XlogError::Kernel(format!(
968                    "{ctx}: launch_stream StreamId({}) does not resolve",
969                    launch_stream.0
970                ))
971            })?;
972
973        let yz_col1 = &metadata_column_u64(e_yz, 1)?;
974        let xz_col1 = &metadata_column_u64(e_xz, 1)?;
975        let n_yz = self.metadata_logical_rows(e_yz)?;
976        let n_xz = self.metadata_logical_rows(e_xz)?;
977
978        // Per-e_xy-row match counters, zero-initialized.
979        let mut row_counts = self.memory().alloc::<u32>(n_xy as usize)?;
980        self.device()
981            .inner()
982            .memset_zeros(&mut row_counts)
983            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row counts failed: {e}")))?;
984
985        let grid = plan.total_work.div_ceil(plan.block_work_unit);
986        let mut rec = LaunchRecorder::new_strict(launch_stream);
987        rec.read(e_xy.num_rows_device());
988        rec.read(e_yz.num_rows_device());
989        rec.read(e_xz.num_rows_device());
990        rec.read_column(e_yz.column(1).expect("yz.col1"));
991        rec.read_column(e_xz.column(1).expect("xz.col1"));
992        rec.read(&plan.xy_work_prefix);
993        rec.read(&plan.xy_yz_start);
994        rec.read(&plan.xy_yz_end);
995        rec.read(&plan.xy_xz_start);
996        rec.read(&plan.xy_xz_end);
997        rec.write(&row_counts);
998        rec.preflight(runtime)
999            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
1000        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
1001            {
1002                let kernel = self
1003                    .device()
1004                    .inner()
1005                    .get_func(
1006                        WCOJ_MODULE,
1007                        wcoj_kernels::WCOJ_TRIANGLE_GROUPBY_ROOT_COUNT_HG_U64,
1008                    )
1009                    .ok_or_else(|| {
1010                        XlogError::Kernel(
1011                            "wcoj_triangle_groupby_root_count_hg_u64 kernel not found".to_string(),
1012                        )
1013                    })?;
1014                let mut params: Vec<*mut c_void> = vec![
1015                    yz_col1.as_kernel_param(),
1016                    n_yz.as_kernel_param(),
1017                    xz_col1.as_kernel_param(),
1018                    n_xz.as_kernel_param(),
1019                    (&plan.xy_work_prefix).as_kernel_param(),
1020                    (&plan.xy_yz_start).as_kernel_param(),
1021                    (&plan.xy_yz_end).as_kernel_param(),
1022                    (&plan.xy_xz_start).as_kernel_param(),
1023                    (&plan.xy_xz_end).as_kernel_param(),
1024                    plan.row_count.as_kernel_param(),
1025                    plan.total_work.as_kernel_param(),
1026                    plan.block_work_unit.as_kernel_param(),
1027                    (&row_counts).as_kernel_param(),
1028                ];
1029                unsafe {
1030                    kernel
1031                        .clone()
1032                        .launch_in(
1033                            stream,
1034                            LaunchConfig {
1035                                grid_dim: (grid, 1, 1),
1036                                block_dim: (BLOCK_SIZE, 1, 1),
1037                                shared_mem_bytes: 0,
1038                            },
1039                            &mut params,
1040                        )
1041                        .map_err(|e| {
1042                            XlogError::Kernel(format!("{ctx}: groupby-count launch failed: {e}"))
1043                        })?;
1044                }
1045            }
1046            Ok(())
1047        };
1048        // SAFETY: preflight retained the accessed buffers and bound this stream.
1049        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
1050            .map_err(|error| error.into_xlog_error())?;
1051
1052        rec.commit()
1053            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
1054
1055        // Per-X reduction via the relation metadata: one (unique X, group
1056        // start) pair per root; e_xy is lex-sorted by X so group rows are
1057        // contiguous.
1058        let meta = self.wcoj_build_metadata_u64_recorded(e_xy, 0, launch_stream)?;
1059        let key_count = meta.key_count;
1060        if key_count == 0 {
1061            return self.create_empty_buffer(out_schema);
1062        }
1063        let mut sums = self
1064            .memory()
1065            .alloc::<u8>(key_count as usize * std::mem::size_of::<u64>())?;
1066        self.device()
1067            .inner()
1068            .memset_zeros(&mut sums)
1069            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero group sums failed: {e}")))?;
1070
1071        let mut rec_sum = LaunchRecorder::new_strict(launch_stream);
1072        rec_sum.read(&row_counts);
1073        rec_sum.read(&meta.prefix_sum);
1074        rec_sum.write(&sums);
1075        rec_sum
1076            .preflight(runtime)
1077            .map_err(|e| XlogError::Kernel(format!("{ctx}: reduce preflight failed: {e}")))?;
1078        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
1079            {
1080                let kernel = self
1081                    .device()
1082                    .inner()
1083                    .get_func(
1084                        WCOJ_MODULE,
1085                        wcoj_kernels::WCOJ_GROUPBY_ROOT_SEGMENT_SUM_COUNTS_U32,
1086                    )
1087                    .ok_or_else(|| {
1088                        XlogError::Kernel(
1089                            "wcoj_groupby_root_segment_sum_counts_u32 kernel not found".to_string(),
1090                        )
1091                    })?;
1092                let reduce_grid = n_xy.div_ceil(BLOCK_SIZE);
1093                let mut params: Vec<*mut c_void> = vec![
1094                    (&row_counts).as_kernel_param(),
1095                    n_xy.as_kernel_param(),
1096                    (&meta.prefix_sum).as_kernel_param(),
1097                    key_count.as_kernel_param(),
1098                    (&sums).as_kernel_param(),
1099                ];
1100                unsafe {
1101                    kernel
1102                        .clone()
1103                        .launch_in(
1104                            stream,
1105                            LaunchConfig {
1106                                grid_dim: (reduce_grid, 1, 1),
1107                                block_dim: (BLOCK_SIZE, 1, 1),
1108                                shared_mem_bytes: 0,
1109                            },
1110                            &mut params,
1111                        )
1112                        .map_err(|e| {
1113                            XlogError::Kernel(format!("{ctx}: reduce launch failed: {e}"))
1114                        })?;
1115                }
1116            }
1117            Ok(())
1118        };
1119        // SAFETY: preflight retained the accessed buffers and bound this stream.
1120        let rec_sum = unsafe { rec_sum.enqueue_prepared_with(&cu_stream, enqueue) }
1121            .map_err(|error| error.into_xlog_error())?;
1122
1123        rec_sum
1124            .commit()
1125            .map_err(|e| XlogError::Kernel(format!("{ctx}: reduce commit failed: {e}")))?;
1126
1127        // (unique X, total) buffer over the key_count roots, then drop the
1128        // roots with no completion. The copies run on launch_stream and the
1129        // fresh destination blocks are registered through the strict
1130        // recorder BEFORE the enqueue — a raw async copy into a freshly
1131        // pool-allocated block without recording is a visibility race.
1132        let x_copy = self
1133            .memory()
1134            .alloc::<u8>(key_count as usize * std::mem::size_of::<u64>())?;
1135        let d_num_rows = self.memory().alloc::<u32>(1)?;
1136        let mut rec_copy = LaunchRecorder::new_strict(launch_stream);
1137        rec_copy.read(&meta.unique_keys);
1138        rec_copy.write(&x_copy);
1139        rec_copy.write(&d_num_rows);
1140        rec_copy
1141            .preflight(runtime)
1142            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy preflight failed: {e}")))?;
1143        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
1144            unsafe {
1145                let res = sys::cuMemcpyDtoDAsync_v2(
1146                    *x_copy.device_ptr(),
1147                    *meta.unique_keys.device_ptr(),
1148                    key_count as usize * std::mem::size_of::<u64>(),
1149                    stream.stream().cu_stream(),
1150                );
1151                if res != sys::cudaError_enum::CUDA_SUCCESS {
1152                    return Err(XlogError::Kernel(format!(
1153                        "{ctx}: DtoD unique keys copy failed: {res:?}"
1154                    )));
1155                }
1156            }
1157            self.initialize_launch_metadata_u32(
1158                key_count,
1159                &d_num_rows,
1160                stream,
1161                &format!("{ctx}: d_num_rows"),
1162            )?;
1163            Ok(())
1164        };
1165        // SAFETY: preflight retained the accessed buffers and bound this stream.
1166        let rec_copy = unsafe { rec_copy.enqueue_prepared_with(&cu_stream, enqueue) }
1167            .map_err(|error| error.into_xlog_error())?;
1168
1169        rec_copy
1170            .commit()
1171            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy commit failed: {e}")))?;
1172        cu_stream
1173            .synchronize()
1174            .map_err(|e| XlogError::Kernel(format!("{ctx}: stream sync failed: {e}")))?;
1175        let staging_schema = Schema::new(vec![
1176            ("x".to_string(), ScalarType::U64),
1177            ("count".to_string(), ScalarType::U64),
1178        ]);
1179        let staging = CudaBuffer::from_columns_with_host_count(
1180            vec![x_copy.into(), sums.into()],
1181            u64::from(key_count),
1182            d_num_rows,
1183            staging_schema,
1184            key_count,
1185        );
1186        let mask = self.compare_const_mask_recorded::<u64>(
1187            &staging,
1188            1,
1189            0u64,
1190            crate::CompareOp::Gt,
1191            launch_stream,
1192        )?;
1193        self.compact_buffer_by_device_mask_counted_recorded(&staging, &mask, launch_stream)
1194    }
1195
1196    /// U64-key aggregate-fused triangle sum/min/max sibling of
1197    /// [`Self::wcoj_triangle_groupby_root_agg_u32_recorded`]: evaluate
1198    /// `q(X, agg(V)) :- e_xy(X,Y), e_yz(Y,Z), e_xz(X,Z)` with
1199    /// `agg ∈ {Sum, Min, Max}` and `V ∈ {Y, Z}` over U64 relations,
1200    /// grouped by the variable-order root X, WITHOUT materializing the
1201    /// triangle rows.
1202    ///
1203    /// The recorded groupby is U32/Symbol-key only, so the per-X reduction
1204    /// reuses the WCOJ relation metadata (one unique root per group, e_xy
1205    /// lex-sorted) like the u64 count path:
1206    /// 1. the per-op fused kernel accumulates, per e_xy row, a match count
1207    ///    and a u64 aggregate partial (integer atomics — deterministic;
1208    ///    sum wraps on overflow exactly like `groupby_sum_u64`; min
1209    ///    partials start at `u64::MAX`, max partials at 0);
1210    /// 2. `wcoj_groupby_root_segment_sum_counts_u32` reduces per-row match
1211    ///    counts to per-unique-root totals (the presence mask), and the
1212    ///    per-op `wcoj_groupby_root_segment_{sum,min,max}_values_u64`
1213    ///    kernel folds the per-row partials into per-unique-root u64
1214    ///    aggregates, skipping zero-match rows;
1215    /// 3. a (X, agg) staging buffer over the unique roots is compacted to
1216    ///    count>0 groups.
1217    ///
1218    /// All reduction work is O(n_xy) — input-sized, never join-output-sized.
1219    ///
1220    /// Output schema matches the unfused materialize+groupby baseline
1221    /// (legacy groupby widened to u64 values): `col0` = X (U64),
1222    /// `col1` = U64 for sum, min and max alike.
1223    ///
1224    /// # Errors
1225    /// * `XlogError::Kernel` if `agg_op` is not Sum/Min/Max, the manager
1226    ///   has no runtime, the launch stream does not resolve, an input is
1227    ///   not 2-column U64, or any kernel launch fails.
1228    #[expect(
1229        clippy::too_many_arguments,
1230        reason = "three relation inputs and launch controls mirror the recorded kernel contract"
1231    )]
1232    pub fn wcoj_triangle_groupby_root_agg_u64_recorded(
1233        &self,
1234        e_xy: &CudaBuffer,
1235        e_yz: &CudaBuffer,
1236        e_xz: &CudaBuffer,
1237        agg_op: AggOp,
1238        value: WcojRootAggValue,
1239        block_work_unit: u32,
1240        launch_stream: StreamId,
1241    ) -> Result<CudaBuffer> {
1242        let ctx = "wcoj_triangle_groupby_root_agg_u64_recorded";
1243        // Layout-normalize per dispatch (sorted-fast-path clone when the
1244        // input is already lex-sorted + unique): the fused path must give
1245        // the same guarantee as the unfused pipeline instead of trusting
1246        // store-buffer sortedness — unsorted/duplicated inputs previously
1247        // produced silently wrong (empty) fused results.
1248        let e_xy = &self.wcoj_layout_u64_recorded(e_xy, launch_stream)?;
1249        let e_yz = &self.wcoj_layout_u64_recorded(e_yz, launch_stream)?;
1250        let e_xz = &self.wcoj_layout_u64_recorded(e_xz, launch_stream)?;
1251        let (kernel_name, segment_kernel_name, agg_name) = match agg_op {
1252            AggOp::Sum => (
1253                wcoj_kernels::WCOJ_TRIANGLE_GROUPBY_ROOT_SUM_HG_U64,
1254                wcoj_kernels::WCOJ_GROUPBY_ROOT_SEGMENT_SUM_VALUES_U64,
1255                "sum_0",
1256            ),
1257            AggOp::Min => (
1258                wcoj_kernels::WCOJ_TRIANGLE_GROUPBY_ROOT_MIN_HG_U64,
1259                wcoj_kernels::WCOJ_GROUPBY_ROOT_SEGMENT_MIN_VALUES_U64,
1260                "min_0",
1261            ),
1262            AggOp::Max => (
1263                wcoj_kernels::WCOJ_TRIANGLE_GROUPBY_ROOT_MAX_HG_U64,
1264                wcoj_kernels::WCOJ_GROUPBY_ROOT_SEGMENT_MAX_VALUES_U64,
1265                "max_0",
1266            ),
1267            other => {
1268                return Err(XlogError::Kernel(format!(
1269                    "{ctx}: unsupported AggOp {other:?} (Sum/Min/Max only; use \
1270                     wcoj_triangle_groupby_root_count_u64_recorded for Count)"
1271                )))
1272            }
1273        };
1274        validate_binary_u64(ctx, "e_xy", e_xy)?;
1275        validate_binary_u64(ctx, "e_yz", e_yz)?;
1276        validate_binary_u64(ctx, "e_xz", e_xz)?;
1277        let plan = self.wcoj_triangle_hg_work_plan_u64_recorded(
1278            e_xy,
1279            e_yz,
1280            e_xz,
1281            block_work_unit,
1282            launch_stream,
1283        )?;
1284        let n_xy = plan.row_count;
1285        let out_schema = Schema::new(vec![
1286            ("x".to_string(), ScalarType::U64),
1287            (agg_name.to_string(), ScalarType::U64),
1288        ]);
1289        if n_xy == 0 || plan.total_work == 0 {
1290            return self.create_empty_buffer(out_schema);
1291        }
1292
1293        let runtime = self.memory().runtime().ok_or_else(|| {
1294            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
1295        })?;
1296        let cu_stream = runtime
1297            .stream_pool()
1298            .resolve(launch_stream)
1299            .ok_or_else(|| {
1300                XlogError::Kernel(format!(
1301                    "{ctx}: launch_stream StreamId({}) does not resolve",
1302                    launch_stream.0
1303                ))
1304            })?;
1305
1306        let yz_col1 = &metadata_column_u64(e_yz, 1)?;
1307        let xz_col1 = &metadata_column_u64(e_xz, 1)?;
1308        let xy_col1 = &metadata_column_u64(e_xy, 1)?;
1309        let n_yz = self.metadata_logical_rows(e_yz)?;
1310        let n_xz = self.metadata_logical_rows(e_xz)?;
1311        let value_from_z: u32 = match value {
1312            WcojRootAggValue::Y => 0,
1313            WcojRootAggValue::Z => 1,
1314        };
1315
1316        // Per-e_xy-row match counters + u64 aggregate partials.
1317        let mut row_counts = self.memory().alloc::<u32>(n_xy as usize)?;
1318        self.device()
1319            .inner()
1320            .memset_zeros(&mut row_counts)
1321            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row counts failed: {e}")))?;
1322        let mut row_agg = self
1323            .memory()
1324            .alloc::<u8>(n_xy as usize * std::mem::size_of::<u64>())?;
1325        self.device()
1326            .inner()
1327            .memset_zeros(&mut row_agg)
1328            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row aggregates failed: {e}")))?;
1329
1330        let grid = plan.total_work.div_ceil(plan.block_work_unit);
1331        let mut rec = LaunchRecorder::new_strict(launch_stream);
1332        rec.read(e_xy.num_rows_device());
1333        rec.read(e_yz.num_rows_device());
1334        rec.read(e_xz.num_rows_device());
1335        rec.read_column(e_yz.column(1).expect("yz.col1"));
1336        rec.read_column(e_xz.column(1).expect("xz.col1"));
1337        rec.read_column(e_xy.column(1).expect("xy.col1"));
1338        rec.read(&plan.xy_work_prefix);
1339        rec.read(&plan.xy_yz_start);
1340        rec.read(&plan.xy_yz_end);
1341        rec.read(&plan.xy_xz_start);
1342        rec.read(&plan.xy_xz_end);
1343        rec.write(&row_counts);
1344        rec.write(&row_agg);
1345        rec.preflight(runtime)
1346            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
1347        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
1348            if matches!(agg_op, AggOp::Min) {
1349                // Min identity: u64::MAX (compaction drops untouched groups).
1350                let fill = self
1351                    .device()
1352                    .inner()
1353                    .get_func(ARITH_MODULE, arith_kernels::ARITH_FILL_CONST_U64)
1354                    .ok_or_else(|| {
1355                        XlogError::Kernel("arith_fill_const_u64 kernel not found".to_string())
1356                    })?;
1357                let row_agg_u64 = &mut reinterpret_u8_as_u64(&mut row_agg)?;
1358                // SAFETY: arith_fill_const_u64(value, n, output)
1359                unsafe {
1360                    fill.clone()
1361                        .launch_in(
1362                            stream,
1363                            LaunchConfig::for_num_elems(n_xy),
1364                            (u64::MAX, n_xy, &mut *row_agg_u64),
1365                        )
1366                        .map_err(|e| {
1367                            XlogError::Kernel(format!("{ctx}: min identity fill failed: {e}"))
1368                        })?;
1369                }
1370            }
1371            {
1372                let kernel = self
1373                    .device()
1374                    .inner()
1375                    .get_func(WCOJ_MODULE, kernel_name)
1376                    .ok_or_else(|| XlogError::Kernel(format!("{kernel_name} kernel not found")))?;
1377                let mut params: Vec<*mut c_void> = vec![
1378                    yz_col1.as_kernel_param(),
1379                    n_yz.as_kernel_param(),
1380                    xz_col1.as_kernel_param(),
1381                    n_xz.as_kernel_param(),
1382                    xy_col1.as_kernel_param(),
1383                    value_from_z.as_kernel_param(),
1384                    (&plan.xy_work_prefix).as_kernel_param(),
1385                    (&plan.xy_yz_start).as_kernel_param(),
1386                    (&plan.xy_yz_end).as_kernel_param(),
1387                    (&plan.xy_xz_start).as_kernel_param(),
1388                    (&plan.xy_xz_end).as_kernel_param(),
1389                    plan.row_count.as_kernel_param(),
1390                    plan.total_work.as_kernel_param(),
1391                    plan.block_work_unit.as_kernel_param(),
1392                    (&row_counts).as_kernel_param(),
1393                    (&row_agg).as_kernel_param(),
1394                ];
1395                unsafe {
1396                    kernel
1397                        .clone()
1398                        .launch_in(
1399                            stream,
1400                            LaunchConfig {
1401                                grid_dim: (grid, 1, 1),
1402                                block_dim: (BLOCK_SIZE, 1, 1),
1403                                shared_mem_bytes: 0,
1404                            },
1405                            &mut params,
1406                        )
1407                        .map_err(|e| {
1408                            XlogError::Kernel(format!("{ctx}: groupby-agg launch failed: {e}"))
1409                        })?;
1410                }
1411            }
1412            Ok(())
1413        };
1414        // SAFETY: preflight retained the accessed buffers and bound this stream.
1415        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
1416            .map_err(|error| error.into_xlog_error())?;
1417
1418        rec.commit()
1419            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
1420
1421        // Per-X reduction via the relation metadata: one (unique X, group
1422        // start) pair per root; e_xy is lex-sorted by X so group rows are
1423        // contiguous.
1424        let meta = self.wcoj_build_metadata_u64_recorded(e_xy, 0, launch_stream)?;
1425        let key_count = meta.key_count;
1426        if key_count == 0 {
1427            return self.create_empty_buffer(out_schema);
1428        }
1429        let mut count_sums = self
1430            .memory()
1431            .alloc::<u8>(key_count as usize * std::mem::size_of::<u64>())?;
1432        self.device()
1433            .inner()
1434            .memset_zeros(&mut count_sums)
1435            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero group counts failed: {e}")))?;
1436        let mut group_agg = self
1437            .memory()
1438            .alloc::<u8>(key_count as usize * std::mem::size_of::<u64>())?;
1439        self.device()
1440            .inner()
1441            .memset_zeros(&mut group_agg)
1442            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero group aggregates failed: {e}")))?;
1443
1444        let mut rec_reduce = LaunchRecorder::new_strict(launch_stream);
1445        rec_reduce.read(&row_counts);
1446        rec_reduce.read(&row_agg);
1447        rec_reduce.read(&meta.prefix_sum);
1448        rec_reduce.write(&count_sums);
1449        rec_reduce.write(&group_agg);
1450        rec_reduce
1451            .preflight(runtime)
1452            .map_err(|e| XlogError::Kernel(format!("{ctx}: reduce preflight failed: {e}")))?;
1453        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
1454            if matches!(agg_op, AggOp::Min) {
1455                let fill = self
1456                    .device()
1457                    .inner()
1458                    .get_func(ARITH_MODULE, arith_kernels::ARITH_FILL_CONST_U64)
1459                    .ok_or_else(|| {
1460                        XlogError::Kernel("arith_fill_const_u64 kernel not found".to_string())
1461                    })?;
1462                let group_agg_u64 = &mut reinterpret_u8_as_u64(&mut group_agg)?;
1463                // SAFETY: arith_fill_const_u64(value, n, output)
1464                unsafe {
1465                    fill.clone()
1466                        .launch_in(
1467                            stream,
1468                            LaunchConfig::for_num_elems(key_count),
1469                            (u64::MAX, key_count, &mut *group_agg_u64),
1470                        )
1471                        .map_err(|e| {
1472                            XlogError::Kernel(format!("{ctx}: group min identity fill failed: {e}"))
1473                        })?;
1474                }
1475            }
1476            let reduce_grid = n_xy.div_ceil(BLOCK_SIZE);
1477            {
1478                let kernel = self
1479                    .device()
1480                    .inner()
1481                    .get_func(
1482                        WCOJ_MODULE,
1483                        wcoj_kernels::WCOJ_GROUPBY_ROOT_SEGMENT_SUM_COUNTS_U32,
1484                    )
1485                    .ok_or_else(|| {
1486                        XlogError::Kernel(
1487                            "wcoj_groupby_root_segment_sum_counts_u32 kernel not found".to_string(),
1488                        )
1489                    })?;
1490                let mut params: Vec<*mut c_void> = vec![
1491                    (&row_counts).as_kernel_param(),
1492                    n_xy.as_kernel_param(),
1493                    (&meta.prefix_sum).as_kernel_param(),
1494                    key_count.as_kernel_param(),
1495                    (&count_sums).as_kernel_param(),
1496                ];
1497                unsafe {
1498                    kernel
1499                        .clone()
1500                        .launch_in(
1501                            stream,
1502                            LaunchConfig {
1503                                grid_dim: (reduce_grid, 1, 1),
1504                                block_dim: (BLOCK_SIZE, 1, 1),
1505                                shared_mem_bytes: 0,
1506                            },
1507                            &mut params,
1508                        )
1509                        .map_err(|e| {
1510                            XlogError::Kernel(format!("{ctx}: count reduce launch failed: {e}"))
1511                        })?;
1512                }
1513            }
1514            {
1515                let kernel = self
1516                    .device()
1517                    .inner()
1518                    .get_func(WCOJ_MODULE, segment_kernel_name)
1519                    .ok_or_else(|| {
1520                        XlogError::Kernel(format!("{segment_kernel_name} kernel not found"))
1521                    })?;
1522                let mut params: Vec<*mut c_void> = vec![
1523                    (&row_counts).as_kernel_param(),
1524                    (&row_agg).as_kernel_param(),
1525                    n_xy.as_kernel_param(),
1526                    (&meta.prefix_sum).as_kernel_param(),
1527                    key_count.as_kernel_param(),
1528                    (&group_agg).as_kernel_param(),
1529                ];
1530                unsafe {
1531                    kernel
1532                        .clone()
1533                        .launch_in(
1534                            stream,
1535                            LaunchConfig {
1536                                grid_dim: (reduce_grid, 1, 1),
1537                                block_dim: (BLOCK_SIZE, 1, 1),
1538                                shared_mem_bytes: 0,
1539                            },
1540                            &mut params,
1541                        )
1542                        .map_err(|e| {
1543                            XlogError::Kernel(format!("{ctx}: agg reduce launch failed: {e}"))
1544                        })?;
1545                }
1546            }
1547            Ok(())
1548        };
1549        // SAFETY: preflight retained the accessed buffers and bound this stream.
1550        let rec_reduce = unsafe { rec_reduce.enqueue_prepared_with(&cu_stream, enqueue) }
1551            .map_err(|error| error.into_xlog_error())?;
1552
1553        rec_reduce
1554            .commit()
1555            .map_err(|e| XlogError::Kernel(format!("{ctx}: reduce commit failed: {e}")))?;
1556
1557        // (unique X, agg) staging plus a counts-only buffer whose mask
1558        // drops groups with no completion. Fresh destination blocks are
1559        // registered through the strict recorder BEFORE the enqueue (a raw
1560        // async copy into a freshly pool-allocated block without recording
1561        // is a visibility race).
1562        let x_copy = self
1563            .memory()
1564            .alloc::<u8>(key_count as usize * std::mem::size_of::<u64>())?;
1565        let d_num_rows_agg = self.memory().alloc::<u32>(1)?;
1566        let d_num_rows_counts = self.memory().alloc::<u32>(1)?;
1567        let mut rec_copy = LaunchRecorder::new_strict(launch_stream);
1568        rec_copy.read(&meta.unique_keys);
1569        rec_copy.write(&x_copy);
1570        rec_copy.write(&d_num_rows_agg);
1571        rec_copy.write(&d_num_rows_counts);
1572        rec_copy
1573            .preflight(runtime)
1574            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy preflight failed: {e}")))?;
1575        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
1576            unsafe {
1577                let res = sys::cuMemcpyDtoDAsync_v2(
1578                    *x_copy.device_ptr(),
1579                    *meta.unique_keys.device_ptr(),
1580                    key_count as usize * std::mem::size_of::<u64>(),
1581                    stream.stream().cu_stream(),
1582                );
1583                if res != sys::cudaError_enum::CUDA_SUCCESS {
1584                    return Err(XlogError::Kernel(format!(
1585                        "{ctx}: DtoD unique keys copy failed: {res:?}"
1586                    )));
1587                }
1588            }
1589            self.initialize_launch_metadata_u32(
1590                key_count,
1591                &d_num_rows_agg,
1592                stream,
1593                &format!("{ctx}: d_num_rows_agg"),
1594            )?;
1595            self.initialize_launch_metadata_u32(
1596                key_count,
1597                &d_num_rows_counts,
1598                stream,
1599                &format!("{ctx}: d_num_rows_counts"),
1600            )?;
1601            Ok(())
1602        };
1603        // SAFETY: preflight retained the accessed buffers and bound this stream.
1604        let rec_copy = unsafe { rec_copy.enqueue_prepared_with(&cu_stream, enqueue) }
1605            .map_err(|error| error.into_xlog_error())?;
1606
1607        rec_copy
1608            .commit()
1609            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy commit failed: {e}")))?;
1610        cu_stream
1611            .synchronize()
1612            .map_err(|e| XlogError::Kernel(format!("{ctx}: stream sync failed: {e}")))?;
1613
1614        let counts_schema = Schema::new(vec![("count".to_string(), ScalarType::U64)]);
1615        let counts_buf = CudaBuffer::from_columns_with_host_count(
1616            vec![count_sums.into()],
1617            u64::from(key_count),
1618            d_num_rows_counts,
1619            counts_schema,
1620            key_count,
1621        );
1622        let staging = CudaBuffer::from_columns_with_host_count(
1623            vec![x_copy.into(), group_agg.into()],
1624            u64::from(key_count),
1625            d_num_rows_agg,
1626            out_schema,
1627            key_count,
1628        );
1629        let mask = self.compare_const_mask_recorded::<u64>(
1630            &counts_buf,
1631            0,
1632            0u64,
1633            crate::CompareOp::Gt,
1634            launch_stream,
1635        )?;
1636        self.compact_buffer_by_device_mask_counted_recorded(&staging, &mask, launch_stream)
1637    }
1638
1639    pub fn wcoj_triangle_hg_count_phase_u32_recorded(
1640        &self,
1641        e_xy: &CudaBuffer,
1642        e_yz: &CudaBuffer,
1643        e_xz: &CudaBuffer,
1644        plan: &WcojTriangleHgWorkPlanU32,
1645        launch_stream: StreamId,
1646    ) -> Result<WcojTriangleHgCountPhaseU32> {
1647        let ctx = "wcoj_triangle_hg_count_phase_u32_recorded";
1648        validate_binary_u32(ctx, "e_xy", e_xy)?;
1649        validate_binary_u32(ctx, "e_yz", e_yz)?;
1650        validate_binary_u32(ctx, "e_xz", e_xz)?;
1651        let grid = plan.total_work.div_ceil(plan.block_work_unit);
1652        if grid > 1024 {
1653            return Err(XlogError::Kernel(format!(
1654                "{ctx}: spike phase path requires grid <= 1024, got {grid}"
1655            )));
1656        }
1657        let total_rows_device = self.memory().alloc::<u32>(1)?;
1658        if plan.total_work == 0 {
1659            return Ok(WcojTriangleHgCountPhaseU32 {
1660                total_rows_device,
1661                total_rows: 0,
1662            });
1663        }
1664
1665        let runtime = self.memory().runtime().ok_or_else(|| {
1666            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
1667        })?;
1668        let cu_stream = runtime
1669            .stream_pool()
1670            .resolve(launch_stream)
1671            .ok_or_else(|| {
1672                XlogError::Kernel(format!(
1673                    "{ctx}: launch_stream StreamId({}) does not resolve",
1674                    launch_stream.0
1675                ))
1676            })?;
1677
1678        let yz_col1 = &metadata_column_u32(e_yz, 1)?;
1679        let xz_col1 = &metadata_column_u32(e_xz, 1)?;
1680        let n_yz = self.metadata_logical_rows(e_yz)?;
1681        let n_xz = self.metadata_logical_rows(e_xz)?;
1682
1683        let mut rec_hg = LaunchRecorder::new_strict(launch_stream);
1684        rec_hg.read(e_xy.num_rows_device());
1685        rec_hg.read(e_yz.num_rows_device());
1686        rec_hg.read(e_xz.num_rows_device());
1687        rec_hg.read_column(e_yz.column(1).expect("yz.col1"));
1688        rec_hg.read_column(e_xz.column(1).expect("xz.col1"));
1689        rec_hg.read(&plan.xy_work_prefix);
1690        rec_hg.read(&plan.xy_yz_start);
1691        rec_hg.read(&plan.xy_yz_end);
1692        rec_hg.read(&plan.xy_xz_start);
1693        rec_hg.read(&plan.xy_xz_end);
1694        rec_hg.read_write(&plan.block_counts);
1695        rec_hg.read_write(&plan.block_offsets);
1696        rec_hg.write(&total_rows_device);
1697        rec_hg
1698            .preflight(runtime)
1699            .map_err(|e| XlogError::Kernel(format!("{ctx}: count preflight failed: {e}")))?;
1700        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
1701            {
1702                let kernel = self
1703                    .device()
1704                    .inner()
1705                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_TRIANGLE_COUNT_HG_U32)
1706                    .ok_or_else(|| {
1707                        XlogError::Kernel("wcoj_triangle_count_hg_u32 kernel not found".to_string())
1708                    })?;
1709                let mut params: Vec<*mut c_void> = vec![
1710                    yz_col1.as_kernel_param(),
1711                    n_yz.as_kernel_param(),
1712                    xz_col1.as_kernel_param(),
1713                    n_xz.as_kernel_param(),
1714                    (&plan.xy_work_prefix).as_kernel_param(),
1715                    (&plan.xy_yz_start).as_kernel_param(),
1716                    (&plan.xy_yz_end).as_kernel_param(),
1717                    (&plan.xy_xz_start).as_kernel_param(),
1718                    (&plan.xy_xz_end).as_kernel_param(),
1719                    plan.row_count.as_kernel_param(),
1720                    plan.total_work.as_kernel_param(),
1721                    plan.block_work_unit.as_kernel_param(),
1722                    (&plan.block_counts).as_kernel_param(),
1723                ];
1724                unsafe {
1725                    kernel
1726                        .clone()
1727                        .launch_in(
1728                            stream,
1729                            LaunchConfig {
1730                                grid_dim: (grid, 1, 1),
1731                                block_dim: (BLOCK_SIZE, 1, 1),
1732                                shared_mem_bytes: 0,
1733                            },
1734                            &mut params,
1735                        )
1736                        .map_err(|e| {
1737                            XlogError::Kernel(format!("{ctx}: count launch failed: {e}"))
1738                        })?;
1739                }
1740            }
1741            {
1742                let kernel = self
1743                    .device()
1744                    .inner()
1745                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_SCAN_HG_BLOCK_COUNTS_U32)
1746                    .ok_or_else(|| {
1747                        XlogError::Kernel(
1748                            "wcoj_scan_hg_block_counts_u32 kernel not found".to_string(),
1749                        )
1750                    })?;
1751                let mut params: Vec<*mut c_void> = vec![
1752                    (&plan.block_counts).as_kernel_param(),
1753                    grid.as_kernel_param(),
1754                    (&plan.block_offsets).as_kernel_param(),
1755                    (&total_rows_device).as_kernel_param(),
1756                ];
1757                unsafe {
1758                    kernel
1759                        .clone()
1760                        .launch_in(
1761                            stream,
1762                            LaunchConfig {
1763                                grid_dim: (1, 1, 1),
1764                                block_dim: (1024, 1, 1),
1765                                shared_mem_bytes: 0,
1766                            },
1767                            &mut params,
1768                        )
1769                        .map_err(|e| {
1770                            XlogError::Kernel(format!("{ctx}: scan launch failed: {e}"))
1771                        })?;
1772                }
1773            }
1774            Ok(())
1775        };
1776        // SAFETY: preflight retained the accessed buffers and bound this stream.
1777        let rec_hg = unsafe { rec_hg.enqueue_prepared_with(&cu_stream, enqueue) }
1778            .map_err(|error| error.into_xlog_error())?;
1779
1780        rec_hg
1781            .commit()
1782            .map_err(|e| XlogError::Kernel(format!("{ctx}: count commit failed: {e}")))?;
1783        cu_stream
1784            .synchronize()
1785            .map_err(|e| XlogError::Kernel(format!("{ctx}: count stream sync failed: {e}")))?;
1786        let total_rows = self
1787            .dtoh_scalar_untracked::<u32>(&total_rows_device, 0)
1788            .map_err(|e| XlogError::Kernel(format!("{ctx}: read total rows failed: {e}")))?;
1789        Ok(WcojTriangleHgCountPhaseU32 {
1790            total_rows_device,
1791            total_rows,
1792        })
1793    }
1794
1795    pub fn wcoj_triangle_hg_materialize_phase_u32_recorded(
1796        &self,
1797        e_xy: &CudaBuffer,
1798        e_yz: &CudaBuffer,
1799        e_xz: &CudaBuffer,
1800        plan: &WcojTriangleHgWorkPlanU32,
1801        count: WcojTriangleHgCountPhaseU32,
1802        launch_stream: StreamId,
1803    ) -> Result<CudaBuffer> {
1804        let ctx = "wcoj_triangle_hg_materialize_phase_u32_recorded";
1805        validate_binary_u32(ctx, "e_xy", e_xy)?;
1806        validate_binary_u32(ctx, "e_yz", e_yz)?;
1807        validate_binary_u32(ctx, "e_xz", e_xz)?;
1808        let out_schema = Schema::new(vec![
1809            (
1810                "x".to_string(),
1811                e_xy.schema().column_type(0).expect("xy.col0 type"),
1812            ),
1813            (
1814                "y".to_string(),
1815                e_xy.schema().column_type(1).expect("xy.col1 type"),
1816            ),
1817            (
1818                "z".to_string(),
1819                e_yz.schema().column_type(1).expect("yz.col1 type"),
1820            ),
1821        ]);
1822        if count.total_rows == 0 {
1823            return self.create_empty_buffer(out_schema);
1824        }
1825        let grid = plan.total_work.div_ceil(plan.block_work_unit);
1826        if grid > 1024 {
1827            return Err(XlogError::Kernel(format!(
1828                "{ctx}: spike phase path requires grid <= 1024, got {grid}"
1829            )));
1830        }
1831        let runtime = self.memory().runtime().ok_or_else(|| {
1832            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
1833        })?;
1834        let cu_stream = runtime
1835            .stream_pool()
1836            .resolve(launch_stream)
1837            .ok_or_else(|| {
1838                XlogError::Kernel(format!(
1839                    "{ctx}: launch_stream StreamId({}) does not resolve",
1840                    launch_stream.0
1841                ))
1842            })?;
1843        let xy_col0 = &metadata_column_u32(e_xy, 0)?;
1844        let xy_col1 = &metadata_column_u32(e_xy, 1)?;
1845        let yz_col1 = &metadata_column_u32(e_yz, 1)?;
1846        let xz_col1 = &metadata_column_u32(e_xz, 1)?;
1847        let n_yz = self.metadata_logical_rows(e_yz)?;
1848        let n_xz = self.metadata_logical_rows(e_xz)?;
1849        let bytes_per_col = (count.total_rows as usize)
1850            .checked_mul(std::mem::size_of::<u32>())
1851            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: output byte size overflow")))?;
1852        let mut out_x = self.memory().alloc::<u8>(bytes_per_col)?;
1853        let mut out_y = self.memory().alloc::<u8>(bytes_per_col)?;
1854        let mut out_z = self.memory().alloc::<u8>(bytes_per_col)?;
1855        let mut rec_mat = LaunchRecorder::new_strict(launch_stream);
1856        rec_mat.read(e_xy.num_rows_device());
1857        rec_mat.read(e_yz.num_rows_device());
1858        rec_mat.read(e_xz.num_rows_device());
1859        rec_mat.read_column(e_xy.column(0).expect("xy.col0"));
1860        rec_mat.read_column(e_xy.column(1).expect("xy.col1"));
1861        rec_mat.read_column(e_yz.column(1).expect("yz.col1"));
1862        rec_mat.read_column(e_xz.column(1).expect("xz.col1"));
1863        rec_mat.read(&plan.xy_work_prefix);
1864        rec_mat.read(&plan.xy_yz_start);
1865        rec_mat.read(&plan.xy_yz_end);
1866        rec_mat.read(&plan.xy_xz_start);
1867        rec_mat.read(&plan.xy_xz_end);
1868        rec_mat.read(&plan.block_offsets);
1869        rec_mat.write(&out_x);
1870        rec_mat.write(&out_y);
1871        rec_mat.write(&out_z);
1872        rec_mat
1873            .preflight(runtime)
1874            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize preflight failed: {e}")))?;
1875        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
1876            {
1877                let kernel = self
1878                    .device()
1879                    .inner()
1880                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_TRIANGLE_MATERIALIZE_HG_U32)
1881                    .ok_or_else(|| {
1882                        XlogError::Kernel(
1883                            "wcoj_triangle_materialize_hg_u32 kernel not found".to_string(),
1884                        )
1885                    })?;
1886                let out_x_u32 = &mut reinterpret_u8_as_u32(&mut out_x)?;
1887                let out_y_u32 = &mut reinterpret_u8_as_u32(&mut out_y)?;
1888                let out_z_u32 = &mut reinterpret_u8_as_u32(&mut out_z)?;
1889                let mut params: Vec<*mut c_void> = vec![
1890                    xy_col0.as_kernel_param(),
1891                    xy_col1.as_kernel_param(),
1892                    yz_col1.as_kernel_param(),
1893                    n_yz.as_kernel_param(),
1894                    xz_col1.as_kernel_param(),
1895                    n_xz.as_kernel_param(),
1896                    (&plan.xy_work_prefix).as_kernel_param(),
1897                    (&plan.xy_yz_start).as_kernel_param(),
1898                    (&plan.xy_yz_end).as_kernel_param(),
1899                    (&plan.xy_xz_start).as_kernel_param(),
1900                    (&plan.xy_xz_end).as_kernel_param(),
1901                    plan.row_count.as_kernel_param(),
1902                    plan.total_work.as_kernel_param(),
1903                    plan.block_work_unit.as_kernel_param(),
1904                    (&plan.block_offsets).as_kernel_param(),
1905                    count.total_rows.as_kernel_param(),
1906                    out_x_u32.as_kernel_param(),
1907                    out_y_u32.as_kernel_param(),
1908                    out_z_u32.as_kernel_param(),
1909                ];
1910                unsafe {
1911                    kernel
1912                        .clone()
1913                        .launch_in(
1914                            stream,
1915                            LaunchConfig {
1916                                grid_dim: (grid, 1, 1),
1917                                block_dim: (BLOCK_SIZE, 1, 1),
1918                                shared_mem_bytes: 0,
1919                            },
1920                            &mut params,
1921                        )
1922                        .map_err(|e| {
1923                            XlogError::Kernel(format!("{ctx}: materialize launch failed: {e}"))
1924                        })?;
1925                }
1926            }
1927            Ok(())
1928        };
1929        // SAFETY: preflight retained the accessed buffers and bound this stream.
1930        let rec_mat = unsafe { rec_mat.enqueue_prepared_with(&cu_stream, enqueue) }
1931            .map_err(|error| error.into_xlog_error())?;
1932
1933        rec_mat
1934            .commit()
1935            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize commit failed: {e}")))?;
1936        cu_stream.synchronize().map_err(|e| {
1937            XlogError::Kernel(format!("{ctx}: materialize stream sync failed: {e}"))
1938        })?;
1939        Ok(CudaBuffer::from_columns_with_host_count(
1940            vec![out_x.into(), out_y.into(), out_z.into()],
1941            count.total_rows as u64,
1942            count.total_rows_device,
1943            out_schema,
1944            count.total_rows,
1945        ))
1946    }
1947
1948    pub fn wcoj_triangle_hg_u32_with_plan_recorded(
1949        &self,
1950        e_xy: &CudaBuffer,
1951        e_yz: &CudaBuffer,
1952        e_xz: &CudaBuffer,
1953        plan: &WcojTriangleHgWorkPlanU32,
1954        launch_stream: StreamId,
1955    ) -> Result<CudaBuffer> {
1956        let ctx = "wcoj_triangle_hg_u32_with_plan_recorded";
1957        validate_binary_u32(ctx, "e_xy", e_xy)?;
1958        validate_binary_u32(ctx, "e_yz", e_yz)?;
1959        validate_binary_u32(ctx, "e_xz", e_xz)?;
1960        let out_schema = Schema::new(vec![
1961            (
1962                "x".to_string(),
1963                e_xy.schema().column_type(0).expect("xy.col0 type"),
1964            ),
1965            (
1966                "y".to_string(),
1967                e_xy.schema().column_type(1).expect("xy.col1 type"),
1968            ),
1969            (
1970                "z".to_string(),
1971                e_yz.schema().column_type(1).expect("yz.col1 type"),
1972            ),
1973        ]);
1974        if plan.total_work == 0 {
1975            return self.create_empty_buffer(out_schema);
1976        }
1977
1978        let grid = plan.total_work.div_ceil(plan.block_work_unit);
1979        let bytes_count = (grid as usize)
1980            .checked_mul(std::mem::size_of::<u32>())
1981            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: count byte size overflow")))?;
1982        let mut local_counts = None;
1983        let mut local_offsets = None;
1984        if grid > 1024 {
1985            local_counts = Some(self.memory().alloc::<u32>(grid as usize)?);
1986            local_offsets = Some(self.memory().alloc::<u32>(grid as usize)?);
1987        }
1988        let total_rows_device = self.memory().alloc::<u32>(1)?;
1989
1990        let runtime = self.memory().runtime().ok_or_else(|| {
1991            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
1992        })?;
1993        let cu_stream = runtime
1994            .stream_pool()
1995            .resolve(launch_stream)
1996            .ok_or_else(|| {
1997                XlogError::Kernel(format!(
1998                    "{ctx}: launch_stream StreamId({}) does not resolve",
1999                    launch_stream.0
2000                ))
2001            })?;
2002
2003        let xy_col0 = &metadata_column_u32(e_xy, 0)?;
2004        let xy_col1 = &metadata_column_u32(e_xy, 1)?;
2005        let yz_col1 = &metadata_column_u32(e_yz, 1)?;
2006        let xz_col1 = &metadata_column_u32(e_xz, 1)?;
2007        let n_yz = self.metadata_logical_rows(e_yz)?;
2008        let n_xz = self.metadata_logical_rows(e_xz)?;
2009
2010        let count_u32 = if grid <= 1024 {
2011            &plan.block_counts
2012        } else {
2013            local_counts
2014                .as_ref()
2015                .expect("local HG counts allocated when grid exceeds single-block scan")
2016        };
2017        let mut scan_scratch =
2018            self.multiblock_scan_u32_scratch_for_len(if grid > 1024 { grid } else { 0 })?;
2019        let mut rec_hg = LaunchRecorder::new_strict(launch_stream);
2020        rec_hg.read(e_xy.num_rows_device());
2021        rec_hg.read(e_yz.num_rows_device());
2022        rec_hg.read(e_xz.num_rows_device());
2023        rec_hg.read_column(e_xy.column(0).expect("xy.col0"));
2024        rec_hg.read_column(e_xy.column(1).expect("xy.col1"));
2025        rec_hg.read_column(e_yz.column(1).expect("yz.col1"));
2026        rec_hg.read_column(e_xz.column(1).expect("xz.col1"));
2027        rec_hg.read(&plan.xy_work_prefix);
2028        rec_hg.read(&plan.xy_yz_start);
2029        rec_hg.read(&plan.xy_yz_end);
2030        rec_hg.read(&plan.xy_xz_start);
2031        rec_hg.read(&plan.xy_xz_end);
2032        rec_hg.read_write(count_u32);
2033        if grid <= 1024 {
2034            rec_hg.read_write(&plan.block_offsets);
2035        } else {
2036            rec_hg.read_write(
2037                local_offsets
2038                    .as_ref()
2039                    .expect("local HG offsets allocated when grid exceeds single-block scan"),
2040            );
2041        }
2042        rec_hg.write(&total_rows_device);
2043        rec_hg.read_write(&plan.scratch_x);
2044        rec_hg.read_write(&plan.scratch_y);
2045        rec_hg.read_write(&plan.scratch_z);
2046        for level in scan_scratch.levels() {
2047            rec_hg.read_write(level);
2048        }
2049        rec_hg
2050            .preflight(runtime)
2051            .map_err(|e| XlogError::Kernel(format!("{ctx}: HG preflight failed: {e}")))?;
2052        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
2053            {
2054                let kernel = self
2055                    .device()
2056                    .inner()
2057                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_TRIANGLE_COUNT_HG_CACHED_U32)
2058                    .ok_or_else(|| {
2059                        XlogError::Kernel(
2060                            "wcoj_triangle_count_hg_cached_u32 kernel not found".to_string(),
2061                        )
2062                    })?;
2063                let mut params: Vec<*mut c_void> = vec![
2064                    xy_col0.as_kernel_param(),
2065                    xy_col1.as_kernel_param(),
2066                    yz_col1.as_kernel_param(),
2067                    n_yz.as_kernel_param(),
2068                    xz_col1.as_kernel_param(),
2069                    n_xz.as_kernel_param(),
2070                    (&plan.xy_work_prefix).as_kernel_param(),
2071                    (&plan.xy_yz_start).as_kernel_param(),
2072                    (&plan.xy_yz_end).as_kernel_param(),
2073                    (&plan.xy_xz_start).as_kernel_param(),
2074                    (&plan.xy_xz_end).as_kernel_param(),
2075                    plan.row_count.as_kernel_param(),
2076                    plan.total_work.as_kernel_param(),
2077                    plan.block_work_unit.as_kernel_param(),
2078                    count_u32.as_kernel_param(),
2079                    (&plan.scratch_x).as_kernel_param(),
2080                    (&plan.scratch_y).as_kernel_param(),
2081                    (&plan.scratch_z).as_kernel_param(),
2082                ];
2083                unsafe {
2084                    kernel
2085                        .clone()
2086                        .launch_in(
2087                            stream,
2088                            LaunchConfig {
2089                                grid_dim: (grid, 1, 1),
2090                                block_dim: (HG_COUNT_BLOCK_SIZE, 1, 1),
2091                                shared_mem_bytes: 0,
2092                            },
2093                            &mut params,
2094                        )
2095                        .map_err(|e| {
2096                            XlogError::Kernel(format!("{ctx}: cached count launch failed: {e}"))
2097                        })?;
2098                }
2099            }
2100            if grid <= 1024 {
2101                let kernel = self
2102                    .device()
2103                    .inner()
2104                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_SCAN_HG_BLOCK_COUNTS_U32)
2105                    .ok_or_else(|| {
2106                        XlogError::Kernel(
2107                            "wcoj_scan_hg_block_counts_u32 kernel not found".to_string(),
2108                        )
2109                    })?;
2110                let mut params: Vec<*mut c_void> = vec![
2111                    count_u32.as_kernel_param(),
2112                    grid.as_kernel_param(),
2113                    (&plan.block_offsets).as_kernel_param(),
2114                    (&total_rows_device).as_kernel_param(),
2115                ];
2116                unsafe {
2117                    kernel
2118                        .clone()
2119                        .launch_in(
2120                            stream,
2121                            LaunchConfig {
2122                                grid_dim: (1, 1, 1),
2123                                block_dim: (1024, 1, 1),
2124                                shared_mem_bytes: 0,
2125                            },
2126                            &mut params,
2127                        )
2128                        .map_err(|e| {
2129                            XlogError::Kernel(format!("{ctx}: HG block-count scan failed: {e}"))
2130                        })?;
2131                }
2132            } else {
2133                let offsets_mut = local_offsets
2134                    .as_mut()
2135                    .expect("local HG offsets allocated when grid exceeds single-block scan");
2136                unsafe {
2137                    let res = sys::cuMemcpyDtoDAsync_v2(
2138                        *offsets_mut.device_ptr(),
2139                        *count_u32.device_ptr(),
2140                        bytes_count,
2141                        stream.stream().cu_stream(),
2142                    );
2143                    if res != sys::cudaError_enum::CUDA_SUCCESS {
2144                        return Err(XlogError::Kernel(format!(
2145                            "{ctx}: DtoD count to offsets failed: {res:?}"
2146                        )));
2147                    }
2148                }
2149                self.multiblock_scan_u32_inplace_on_stream(
2150                    offsets_mut,
2151                    grid,
2152                    stream,
2153                    &mut scan_scratch,
2154                )?;
2155                let total_kernel = self
2156                    .device()
2157                    .inner()
2158                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_COMPUTE_TOTAL)
2159                    .ok_or_else(|| {
2160                        XlogError::Kernel("wcoj_compute_total kernel not found".to_string())
2161                    })?;
2162                let mut params: Vec<*mut c_void> = vec![
2163                    count_u32.as_kernel_param(),
2164                    (&*offsets_mut).as_kernel_param(),
2165                    grid.as_kernel_param(),
2166                    (&total_rows_device).as_kernel_param(),
2167                ];
2168                unsafe {
2169                    total_kernel
2170                        .clone()
2171                        .launch_in(
2172                            stream,
2173                            LaunchConfig {
2174                                grid_dim: (1, 1, 1),
2175                                block_dim: (1, 1, 1),
2176                                shared_mem_bytes: 0,
2177                            },
2178                            &mut params,
2179                        )
2180                        .map_err(|e| {
2181                            XlogError::Kernel(format!("{ctx}: HG total reducer failed: {e}"))
2182                        })?;
2183                }
2184            }
2185            Ok(())
2186        };
2187        // SAFETY: preflight retained the accessed buffers and bound this stream.
2188        let rec_hg = unsafe { rec_hg.enqueue_prepared_with(&cu_stream, enqueue) }
2189            .map_err(|error| error.into_xlog_error())?;
2190
2191        rec_hg
2192            .commit()
2193            .map_err(|e| XlogError::Kernel(format!("{ctx}: HG count commit failed: {e}")))?;
2194        cu_stream
2195            .synchronize()
2196            .map_err(|e| XlogError::Kernel(format!("{ctx}: count stream sync failed: {e}")))?;
2197        let total_rows = self
2198            .dtoh_scalar_untracked::<u32>(&total_rows_device, 0)
2199            .map_err(|e| XlogError::Kernel(format!("{ctx}: read total rows failed: {e}")))?;
2200        if total_rows == 0 {
2201            return self.create_empty_buffer(out_schema);
2202        }
2203
2204        let bytes_per_col = (total_rows as usize)
2205            .checked_mul(std::mem::size_of::<u32>())
2206            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: output byte size overflow")))?;
2207        let mut out_x = self.memory().alloc::<u8>(bytes_per_col)?;
2208        let mut out_y = self.memory().alloc::<u8>(bytes_per_col)?;
2209        let mut out_z = self.memory().alloc::<u8>(bytes_per_col)?;
2210        let materialize_offsets = if grid <= 1024 {
2211            &plan.block_offsets
2212        } else {
2213            local_offsets
2214                .as_ref()
2215                .expect("local HG offsets allocated when grid exceeds single-block scan")
2216        };
2217        let mut rec_mat = LaunchRecorder::new_strict(launch_stream);
2218        rec_mat.read(count_u32);
2219        rec_mat.read(materialize_offsets);
2220        rec_mat.read(&plan.scratch_x);
2221        rec_mat.read(&plan.scratch_y);
2222        rec_mat.read(&plan.scratch_z);
2223        rec_mat.write(&out_x);
2224        rec_mat.write(&out_y);
2225        rec_mat.write(&out_z);
2226        rec_mat
2227            .preflight(runtime)
2228            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize preflight failed: {e}")))?;
2229        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
2230            {
2231                let kernel = self
2232                    .device()
2233                    .inner()
2234                    .get_func(
2235                        WCOJ_MODULE,
2236                        wcoj_kernels::WCOJ_TRIANGLE_MATERIALIZE_HG_CACHED_U32,
2237                    )
2238                    .ok_or_else(|| {
2239                        XlogError::Kernel(
2240                            "wcoj_triangle_materialize_hg_cached_u32 kernel not found".to_string(),
2241                        )
2242                    })?;
2243                let out_x_u32 = &mut reinterpret_u8_as_u32(&mut out_x)?;
2244                let out_y_u32 = &mut reinterpret_u8_as_u32(&mut out_y)?;
2245                let out_z_u32 = &mut reinterpret_u8_as_u32(&mut out_z)?;
2246                let mut params: Vec<*mut c_void> = vec![
2247                    count_u32.as_kernel_param(),
2248                    materialize_offsets.as_kernel_param(),
2249                    plan.block_work_unit.as_kernel_param(),
2250                    total_rows.as_kernel_param(),
2251                    (&plan.scratch_x).as_kernel_param(),
2252                    (&plan.scratch_y).as_kernel_param(),
2253                    (&plan.scratch_z).as_kernel_param(),
2254                    out_x_u32.as_kernel_param(),
2255                    out_y_u32.as_kernel_param(),
2256                    out_z_u32.as_kernel_param(),
2257                ];
2258                unsafe {
2259                    kernel
2260                        .clone()
2261                        .launch_in(
2262                            stream,
2263                            LaunchConfig {
2264                                grid_dim: (grid, 1, 1),
2265                                block_dim: (BLOCK_SIZE, 1, 1),
2266                                shared_mem_bytes: 0,
2267                            },
2268                            &mut params,
2269                        )
2270                        .map_err(|e| {
2271                            XlogError::Kernel(format!("{ctx}: materialize launch failed: {e}"))
2272                        })?;
2273                }
2274            }
2275            Ok(())
2276        };
2277        // SAFETY: preflight retained the accessed buffers and bound this stream.
2278        let rec_mat = unsafe { rec_mat.enqueue_prepared_with(&cu_stream, enqueue) }
2279            .map_err(|error| error.into_xlog_error())?;
2280
2281        rec_mat
2282            .commit()
2283            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize commit failed: {e}")))?;
2284        cu_stream.synchronize().map_err(|e| {
2285            XlogError::Kernel(format!("{ctx}: materialize stream sync failed: {e}"))
2286        })?;
2287
2288        Ok(CudaBuffer::from_columns_with_host_count(
2289            vec![out_x.into(), out_y.into(), out_z.into()],
2290            total_rows as u64,
2291            total_rows_device,
2292            out_schema,
2293            total_rows,
2294        ))
2295    }
2296
2297    pub fn wcoj_triangle_hg_work_plan_u64_recorded(
2298        &self,
2299        e_xy: &CudaBuffer,
2300        e_yz: &CudaBuffer,
2301        e_xz: &CudaBuffer,
2302        block_work_unit: u32,
2303        launch_stream: StreamId,
2304    ) -> Result<WcojTriangleHgWorkPlanU64> {
2305        let ctx = "wcoj_triangle_hg_work_plan_u64_recorded";
2306        if block_work_unit == 0 {
2307            return Err(XlogError::Kernel(format!(
2308                "{ctx}: block_work_unit must be nonzero"
2309            )));
2310        }
2311        validate_binary_u64(ctx, "e_xy", e_xy)?;
2312        validate_binary_u64(ctx, "e_yz", e_yz)?;
2313        validate_binary_u64(ctx, "e_xz", e_xz)?;
2314
2315        let n_xy = self.metadata_logical_rows(e_xy)?;
2316        let n_yz = self.metadata_logical_rows(e_yz)?;
2317        let n_xz = self.metadata_logical_rows(e_xz)?;
2318        let prefix_len = n_xy
2319            .checked_add(1)
2320            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: prefix length overflow")))?;
2321        let mut xy_work_prefix = self.memory().alloc::<u32>(prefix_len as usize)?;
2322        let mut xy_yz_start = self.memory().alloc::<u32>(n_xy as usize)?;
2323        let mut xy_yz_end = self.memory().alloc::<u32>(n_xy as usize)?;
2324        let mut xy_xz_start = self.memory().alloc::<u32>(n_xy as usize)?;
2325        let mut xy_xz_end = self.memory().alloc::<u32>(n_xy as usize)?;
2326
2327        if n_xy == 0 {
2328            let block_counts = self.memory().alloc::<u32>(1)?;
2329            let block_offsets = self.memory().alloc::<u32>(1)?;
2330            return Ok(WcojTriangleHgWorkPlanU64 {
2331                xy_work_prefix,
2332                xy_yz_start,
2333                xy_yz_end,
2334                xy_xz_start,
2335                xy_xz_end,
2336                block_counts,
2337                block_offsets,
2338                total_work: 0,
2339                block_work_unit,
2340                row_count: n_xy,
2341            });
2342        }
2343
2344        let runtime = self.memory().runtime().ok_or_else(|| {
2345            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
2346        })?;
2347        let cu_stream = runtime
2348            .stream_pool()
2349            .resolve(launch_stream)
2350            .ok_or_else(|| {
2351                XlogError::Kernel(format!(
2352                    "{ctx}: launch_stream StreamId({}) does not resolve",
2353                    launch_stream.0
2354                ))
2355            })?;
2356
2357        let xy_col0 = &metadata_column_u64(e_xy, 0)?;
2358        let xy_col1 = &metadata_column_u64(e_xy, 1)?;
2359        let yz_col0 = &metadata_column_u64(e_yz, 0)?;
2360        let xz_col0 = &metadata_column_u64(e_xz, 0)?;
2361
2362        let mut scan_scratch = self.multiblock_scan_u32_scratch_for_len(prefix_len)?;
2363        let mut rec = LaunchRecorder::new_strict(launch_stream);
2364        rec.read(e_xy.num_rows_device());
2365        rec.read(e_yz.num_rows_device());
2366        rec.read(e_xz.num_rows_device());
2367        rec.read_column(e_xy.column(0).expect("xy.col0"));
2368        rec.read_column(e_xy.column(1).expect("xy.col1"));
2369        rec.read_column(e_yz.column(0).expect("yz.col0"));
2370        rec.read_column(e_xz.column(0).expect("xz.col0"));
2371        rec.write(&xy_work_prefix);
2372        rec.write(&xy_yz_start);
2373        rec.write(&xy_yz_end);
2374        rec.write(&xy_xz_start);
2375        rec.write(&xy_xz_end);
2376        for level in scan_scratch.levels() {
2377            rec.read_write(level);
2378        }
2379        rec.preflight(runtime)
2380            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
2381        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
2382            let kernel = self
2383                .device()
2384                .inner()
2385                .get_func(
2386                    WCOJ_MODULE,
2387                    wcoj_kernels::WCOJ_TRIANGLE_BUILD_HG_WORK_PLAN_U64,
2388                )
2389                .ok_or_else(|| {
2390                    XlogError::Kernel(
2391                        "wcoj_triangle_build_hg_work_plan_u64 kernel not found".to_string(),
2392                    )
2393                })?;
2394            let grid = n_xy.div_ceil(BLOCK_SIZE);
2395            unsafe {
2396                kernel
2397                    .clone()
2398                    .launch_in(
2399                        stream,
2400                        LaunchConfig {
2401                            grid_dim: (grid, 1, 1),
2402                            block_dim: (BLOCK_SIZE, 1, 1),
2403                            shared_mem_bytes: 0,
2404                        },
2405                        (
2406                            xy_col0,
2407                            xy_col1,
2408                            n_xy,
2409                            yz_col0,
2410                            n_yz,
2411                            xz_col0,
2412                            n_xz,
2413                            &mut xy_work_prefix,
2414                            &mut xy_yz_start,
2415                            &mut xy_yz_end,
2416                            &mut xy_xz_start,
2417                            &mut xy_xz_end,
2418                        ),
2419                    )
2420                    .map_err(|e| {
2421                        XlogError::Kernel(format!(
2422                            "wcoj_triangle_build_hg_work_plan_u64 launch failed: {e}"
2423                        ))
2424                    })?;
2425            }
2426            self.multiblock_scan_u32_inplace_on_stream(
2427                &mut xy_work_prefix,
2428                prefix_len,
2429                stream,
2430                &mut scan_scratch,
2431            )?;
2432            Ok(())
2433        };
2434        // SAFETY: preflight retained the accessed buffers and bound this stream.
2435        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
2436            .map_err(|error| error.into_xlog_error())?;
2437
2438        rec.commit()
2439            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
2440        cu_stream
2441            .synchronize()
2442            .map_err(|e| XlogError::Kernel(format!("{ctx}: stream sync failed: {e}")))?;
2443        let total_work = self.dtoh_scalar_untracked::<u32>(&xy_work_prefix, n_xy as usize)?;
2444        let grid = if total_work == 0 {
2445            1
2446        } else {
2447            total_work.div_ceil(block_work_unit)
2448        };
2449        let block_counts = self.memory().alloc::<u32>(grid as usize)?;
2450        let block_offsets = self.memory().alloc::<u32>(grid as usize)?;
2451
2452        Ok(WcojTriangleHgWorkPlanU64 {
2453            xy_work_prefix,
2454            xy_yz_start,
2455            xy_yz_end,
2456            xy_xz_start,
2457            xy_xz_end,
2458            block_counts,
2459            block_offsets,
2460            total_work,
2461            block_work_unit,
2462            row_count: n_xy,
2463        })
2464    }
2465
2466    pub fn wcoj_triangle_hg_u64_recorded(
2467        &self,
2468        e_xy: &CudaBuffer,
2469        e_yz: &CudaBuffer,
2470        e_xz: &CudaBuffer,
2471        block_work_unit: u32,
2472        launch_stream: StreamId,
2473    ) -> Result<CudaBuffer> {
2474        let ctx = "wcoj_triangle_hg_u64_recorded";
2475        validate_binary_u64(ctx, "e_xy", e_xy)?;
2476        validate_binary_u64(ctx, "e_yz", e_yz)?;
2477        validate_binary_u64(ctx, "e_xz", e_xz)?;
2478        let plan = self.wcoj_triangle_hg_work_plan_u64_recorded(
2479            e_xy,
2480            e_yz,
2481            e_xz,
2482            block_work_unit,
2483            launch_stream,
2484        )?;
2485        let out_schema = Schema::new(vec![
2486            ("col0".to_string(), ScalarType::U64),
2487            ("col1".to_string(), ScalarType::U64),
2488            ("col2".to_string(), ScalarType::U64),
2489        ]);
2490        if plan.total_work == 0 {
2491            return self.create_empty_buffer(out_schema);
2492        }
2493
2494        let grid = plan.total_work.div_ceil(plan.block_work_unit);
2495        let bytes_count = (grid as usize)
2496            .checked_mul(std::mem::size_of::<u32>())
2497            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: count byte size overflow")))?;
2498        let mut local_counts = None;
2499        let mut local_offsets = None;
2500        if grid > 1024 {
2501            local_counts = Some(self.memory().alloc::<u32>(grid as usize)?);
2502            local_offsets = Some(self.memory().alloc::<u32>(grid as usize)?);
2503        }
2504        let total_rows_device = self.memory().alloc::<u32>(1)?;
2505
2506        let runtime = self.memory().runtime().ok_or_else(|| {
2507            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
2508        })?;
2509        let cu_stream = runtime
2510            .stream_pool()
2511            .resolve(launch_stream)
2512            .ok_or_else(|| {
2513                XlogError::Kernel(format!(
2514                    "{ctx}: launch_stream StreamId({}) does not resolve",
2515                    launch_stream.0
2516                ))
2517            })?;
2518
2519        let xy_col0 = &metadata_column_u64(e_xy, 0)?;
2520        let xy_col1 = &metadata_column_u64(e_xy, 1)?;
2521        let yz_col1 = &metadata_column_u64(e_yz, 1)?;
2522        let xz_col1 = &metadata_column_u64(e_xz, 1)?;
2523        let n_yz = self.metadata_logical_rows(e_yz)?;
2524        let n_xz = self.metadata_logical_rows(e_xz)?;
2525
2526        let count_u32 = if grid <= 1024 {
2527            &plan.block_counts
2528        } else {
2529            local_counts
2530                .as_ref()
2531                .expect("local HG counts allocated when grid exceeds single-block scan")
2532        };
2533        let mut scan_scratch =
2534            self.multiblock_scan_u32_scratch_for_len(if grid > 1024 { grid } else { 0 })?;
2535        let mut rec_hg = LaunchRecorder::new_strict(launch_stream);
2536        rec_hg.read(e_xy.num_rows_device());
2537        rec_hg.read(e_yz.num_rows_device());
2538        rec_hg.read(e_xz.num_rows_device());
2539        rec_hg.read_column(e_yz.column(1).expect("yz.col1"));
2540        rec_hg.read_column(e_xz.column(1).expect("xz.col1"));
2541        rec_hg.read(&plan.xy_work_prefix);
2542        rec_hg.read(&plan.xy_yz_start);
2543        rec_hg.read(&plan.xy_yz_end);
2544        rec_hg.read(&plan.xy_xz_start);
2545        rec_hg.read(&plan.xy_xz_end);
2546        rec_hg.read_write(count_u32);
2547        if grid <= 1024 {
2548            rec_hg.read_write(&plan.block_offsets);
2549        } else {
2550            rec_hg.read_write(
2551                local_offsets
2552                    .as_ref()
2553                    .expect("local HG offsets allocated when grid exceeds single-block scan"),
2554            );
2555        }
2556        rec_hg.write(&total_rows_device);
2557        for level in scan_scratch.levels() {
2558            rec_hg.read_write(level);
2559        }
2560        rec_hg
2561            .preflight(runtime)
2562            .map_err(|e| XlogError::Kernel(format!("{ctx}: HG preflight failed: {e}")))?;
2563        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
2564            {
2565                let kernel = self
2566                    .device()
2567                    .inner()
2568                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_TRIANGLE_COUNT_HG_U64)
2569                    .ok_or_else(|| {
2570                        XlogError::Kernel("wcoj_triangle_count_hg_u64 kernel not found".to_string())
2571                    })?;
2572                let mut params: Vec<*mut c_void> = vec![
2573                    yz_col1.as_kernel_param(),
2574                    n_yz.as_kernel_param(),
2575                    xz_col1.as_kernel_param(),
2576                    n_xz.as_kernel_param(),
2577                    (&plan.xy_work_prefix).as_kernel_param(),
2578                    (&plan.xy_yz_start).as_kernel_param(),
2579                    (&plan.xy_yz_end).as_kernel_param(),
2580                    (&plan.xy_xz_start).as_kernel_param(),
2581                    (&plan.xy_xz_end).as_kernel_param(),
2582                    plan.row_count.as_kernel_param(),
2583                    plan.total_work.as_kernel_param(),
2584                    plan.block_work_unit.as_kernel_param(),
2585                    count_u32.as_kernel_param(),
2586                ];
2587                unsafe {
2588                    kernel
2589                        .clone()
2590                        .launch_in(
2591                            stream,
2592                            LaunchConfig {
2593                                grid_dim: (grid, 1, 1),
2594                                block_dim: (BLOCK_SIZE, 1, 1),
2595                                shared_mem_bytes: 0,
2596                            },
2597                            &mut params,
2598                        )
2599                        .map_err(|e| {
2600                            XlogError::Kernel(format!("{ctx}: count launch failed: {e}"))
2601                        })?;
2602                }
2603            }
2604            if grid <= 1024 {
2605                let kernel = self
2606                    .device()
2607                    .inner()
2608                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_SCAN_HG_BLOCK_COUNTS_U32)
2609                    .ok_or_else(|| {
2610                        XlogError::Kernel(
2611                            "wcoj_scan_hg_block_counts_u32 kernel not found".to_string(),
2612                        )
2613                    })?;
2614                let mut params: Vec<*mut c_void> = vec![
2615                    count_u32.as_kernel_param(),
2616                    grid.as_kernel_param(),
2617                    (&plan.block_offsets).as_kernel_param(),
2618                    (&total_rows_device).as_kernel_param(),
2619                ];
2620                unsafe {
2621                    kernel
2622                        .clone()
2623                        .launch_in(
2624                            stream,
2625                            LaunchConfig {
2626                                grid_dim: (1, 1, 1),
2627                                block_dim: (1024, 1, 1),
2628                                shared_mem_bytes: 0,
2629                            },
2630                            &mut params,
2631                        )
2632                        .map_err(|e| {
2633                            XlogError::Kernel(format!("{ctx}: HG block-count scan failed: {e}"))
2634                        })?;
2635                }
2636            } else {
2637                let offsets_mut = local_offsets
2638                    .as_mut()
2639                    .expect("local HG offsets allocated when grid exceeds single-block scan");
2640                unsafe {
2641                    let res = sys::cuMemcpyDtoDAsync_v2(
2642                        *offsets_mut.device_ptr(),
2643                        *count_u32.device_ptr(),
2644                        bytes_count,
2645                        stream.stream().cu_stream(),
2646                    );
2647                    if res != sys::cudaError_enum::CUDA_SUCCESS {
2648                        return Err(XlogError::Kernel(format!(
2649                            "{ctx}: DtoD count to offsets failed: {res:?}"
2650                        )));
2651                    }
2652                }
2653                self.multiblock_scan_u32_inplace_on_stream(
2654                    offsets_mut,
2655                    grid,
2656                    stream,
2657                    &mut scan_scratch,
2658                )?;
2659                let total_kernel = self
2660                    .device()
2661                    .inner()
2662                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_COMPUTE_TOTAL)
2663                    .ok_or_else(|| {
2664                        XlogError::Kernel("wcoj_compute_total kernel not found".to_string())
2665                    })?;
2666                let mut params: Vec<*mut c_void> = vec![
2667                    count_u32.as_kernel_param(),
2668                    (&*offsets_mut).as_kernel_param(),
2669                    grid.as_kernel_param(),
2670                    (&total_rows_device).as_kernel_param(),
2671                ];
2672                unsafe {
2673                    total_kernel
2674                        .clone()
2675                        .launch_in(
2676                            stream,
2677                            LaunchConfig {
2678                                grid_dim: (1, 1, 1),
2679                                block_dim: (1, 1, 1),
2680                                shared_mem_bytes: 0,
2681                            },
2682                            &mut params,
2683                        )
2684                        .map_err(|e| {
2685                            XlogError::Kernel(format!("{ctx}: HG total reducer failed: {e}"))
2686                        })?;
2687                }
2688            }
2689            Ok(())
2690        };
2691        // SAFETY: preflight retained the accessed buffers and bound this stream.
2692        let rec_hg = unsafe { rec_hg.enqueue_prepared_with(&cu_stream, enqueue) }
2693            .map_err(|error| error.into_xlog_error())?;
2694
2695        rec_hg
2696            .commit()
2697            .map_err(|e| XlogError::Kernel(format!("{ctx}: HG count commit failed: {e}")))?;
2698        cu_stream
2699            .synchronize()
2700            .map_err(|e| XlogError::Kernel(format!("{ctx}: count stream sync failed: {e}")))?;
2701        let total_rows = self
2702            .dtoh_scalar_untracked::<u32>(&total_rows_device, 0)
2703            .map_err(|e| XlogError::Kernel(format!("{ctx}: read total rows failed: {e}")))?;
2704        if total_rows == 0 {
2705            return self.create_empty_buffer(out_schema);
2706        }
2707
2708        let bytes_per_col = (total_rows as usize)
2709            .checked_mul(std::mem::size_of::<u64>())
2710            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: output byte size overflow")))?;
2711        let mut out_x = self.memory().alloc::<u8>(bytes_per_col)?;
2712        let mut out_y = self.memory().alloc::<u8>(bytes_per_col)?;
2713        let mut out_z = self.memory().alloc::<u8>(bytes_per_col)?;
2714        let materialize_offsets = if grid <= 1024 {
2715            &plan.block_offsets
2716        } else {
2717            local_offsets
2718                .as_ref()
2719                .expect("local HG offsets allocated when grid exceeds single-block scan")
2720        };
2721        let mut rec_mat = LaunchRecorder::new_strict(launch_stream);
2722        rec_mat.read(materialize_offsets);
2723        rec_mat.read(e_xy.num_rows_device());
2724        rec_mat.read(e_yz.num_rows_device());
2725        rec_mat.read(e_xz.num_rows_device());
2726        rec_mat.read_column(e_xy.column(0).expect("xy.col0"));
2727        rec_mat.read_column(e_xy.column(1).expect("xy.col1"));
2728        rec_mat.read_column(e_yz.column(1).expect("yz.col1"));
2729        rec_mat.read_column(e_xz.column(1).expect("xz.col1"));
2730        rec_mat.read(&plan.xy_work_prefix);
2731        rec_mat.read(&plan.xy_yz_start);
2732        rec_mat.read(&plan.xy_yz_end);
2733        rec_mat.read(&plan.xy_xz_start);
2734        rec_mat.read(&plan.xy_xz_end);
2735        rec_mat.write(&out_x);
2736        rec_mat.write(&out_y);
2737        rec_mat.write(&out_z);
2738        rec_mat
2739            .preflight(runtime)
2740            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize preflight failed: {e}")))?;
2741        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
2742            {
2743                let kernel = self
2744                    .device()
2745                    .inner()
2746                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_TRIANGLE_MATERIALIZE_HG_U64)
2747                    .ok_or_else(|| {
2748                        XlogError::Kernel(
2749                            "wcoj_triangle_materialize_hg_u64 kernel not found".to_string(),
2750                        )
2751                    })?;
2752                let out_x_u64 = &mut reinterpret_u8_as_u64(&mut out_x)?;
2753                let out_y_u64 = &mut reinterpret_u8_as_u64(&mut out_y)?;
2754                let out_z_u64 = &mut reinterpret_u8_as_u64(&mut out_z)?;
2755                let mut params: Vec<*mut c_void> = vec![
2756                    xy_col0.as_kernel_param(),
2757                    xy_col1.as_kernel_param(),
2758                    yz_col1.as_kernel_param(),
2759                    n_yz.as_kernel_param(),
2760                    xz_col1.as_kernel_param(),
2761                    n_xz.as_kernel_param(),
2762                    (&plan.xy_work_prefix).as_kernel_param(),
2763                    (&plan.xy_yz_start).as_kernel_param(),
2764                    (&plan.xy_yz_end).as_kernel_param(),
2765                    (&plan.xy_xz_start).as_kernel_param(),
2766                    (&plan.xy_xz_end).as_kernel_param(),
2767                    plan.row_count.as_kernel_param(),
2768                    plan.total_work.as_kernel_param(),
2769                    plan.block_work_unit.as_kernel_param(),
2770                    materialize_offsets.as_kernel_param(),
2771                    total_rows.as_kernel_param(),
2772                    out_x_u64.as_kernel_param(),
2773                    out_y_u64.as_kernel_param(),
2774                    out_z_u64.as_kernel_param(),
2775                ];
2776                unsafe {
2777                    kernel
2778                        .clone()
2779                        .launch_in(
2780                            stream,
2781                            LaunchConfig {
2782                                grid_dim: (grid, 1, 1),
2783                                block_dim: (BLOCK_SIZE, 1, 1),
2784                                shared_mem_bytes: 0,
2785                            },
2786                            &mut params,
2787                        )
2788                        .map_err(|e| {
2789                            XlogError::Kernel(format!("{ctx}: materialize launch failed: {e}"))
2790                        })?;
2791                }
2792            }
2793            Ok(())
2794        };
2795        // SAFETY: preflight retained the accessed buffers and bound this stream.
2796        let rec_mat = unsafe { rec_mat.enqueue_prepared_with(&cu_stream, enqueue) }
2797            .map_err(|error| error.into_xlog_error())?;
2798
2799        rec_mat
2800            .commit()
2801            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize commit failed: {e}")))?;
2802        cu_stream.synchronize().map_err(|e| {
2803            XlogError::Kernel(format!("{ctx}: materialize stream sync failed: {e}"))
2804        })?;
2805
2806        Ok(CudaBuffer::from_columns_with_host_count(
2807            vec![out_x.into(), out_y.into(), out_z.into()],
2808            total_rows as u64,
2809            total_rows_device,
2810            out_schema,
2811            total_rows,
2812        ))
2813    }
2814
2815    pub fn wcoj_4cycle_hg_work_plan_u32_recorded(
2816        &self,
2817        e1: &CudaBuffer,
2818        e2: &CudaBuffer,
2819        e3: &CudaBuffer,
2820        e4: &CudaBuffer,
2821        block_work_unit: u32,
2822        launch_stream: StreamId,
2823    ) -> Result<WcojCycle4HgWorkPlanU32> {
2824        let ctx = "wcoj_4cycle_hg_work_plan_u32_recorded";
2825        if block_work_unit == 0 {
2826            return Err(XlogError::Kernel(format!(
2827                "{ctx}: block_work_unit must be nonzero"
2828            )));
2829        }
2830        validate_binary_u32(ctx, "e1", e1)?;
2831        validate_binary_u32(ctx, "e2", e2)?;
2832        validate_binary_u32(ctx, "e3", e3)?;
2833        validate_binary_u32(ctx, "e4", e4)?;
2834
2835        let n_e1 = self.metadata_logical_rows(e1)?;
2836        let n_e2 = self.metadata_logical_rows(e2)?;
2837        let n_e3 = self.metadata_logical_rows(e3)?;
2838        let prefix_len = n_e1
2839            .checked_add(1)
2840            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: prefix length overflow")))?;
2841        let e2_prefix_len = n_e2
2842            .checked_add(1)
2843            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: e2 prefix length overflow")))?;
2844        let mut e1_work_prefix = self.memory().alloc::<u32>(prefix_len as usize)?;
2845        let mut e2_work_prefix = self.memory().alloc::<u32>(e2_prefix_len as usize)?;
2846        let mut e1_e2_start = self.memory().alloc::<u32>(n_e1 as usize)?;
2847        let mut e1_e2_end = self.memory().alloc::<u32>(n_e1 as usize)?;
2848
2849        if n_e1 == 0 || n_e2 == 0 || n_e3 == 0 || self.metadata_logical_rows(e4)? == 0 {
2850            let block_counts = self.memory().alloc::<u32>(1)?;
2851            let block_offsets = self.memory().alloc::<u32>(1)?;
2852            return Ok(WcojCycle4HgWorkPlanU32 {
2853                e1_work_prefix,
2854                e2_work_prefix,
2855                e1_e2_start,
2856                e1_e2_end,
2857                block_counts,
2858                block_offsets,
2859                total_work: 0,
2860                block_work_unit,
2861                row_count: n_e1,
2862            });
2863        }
2864
2865        let runtime = self.memory().runtime().ok_or_else(|| {
2866            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
2867        })?;
2868        let cu_stream = runtime
2869            .stream_pool()
2870            .resolve(launch_stream)
2871            .ok_or_else(|| {
2872                XlogError::Kernel(format!(
2873                    "{ctx}: launch_stream StreamId({}) does not resolve",
2874                    launch_stream.0
2875                ))
2876            })?;
2877
2878        let e1_col1 = &metadata_column_u32(e1, 1)?;
2879        let e2_col0 = &metadata_column_u32(e2, 0)?;
2880        let e2_col1 = &metadata_column_u32(e2, 1)?;
2881        let e3_col0 = &metadata_column_u32(e3, 0)?;
2882
2883        let mut scan_scratch =
2884            self.multiblock_scan_u32_scratch_for_len(e2_prefix_len.max(prefix_len))?;
2885        let mut rec = LaunchRecorder::new_strict(launch_stream);
2886        rec.read(e1.num_rows_device());
2887        rec.read(e2.num_rows_device());
2888        rec.read(e3.num_rows_device());
2889        rec.read_column(e1.column(1).expect("e1.col1"));
2890        rec.read_column(e2.column(0).expect("e2.col0"));
2891        rec.read_column(e2.column(1).expect("e2.col1"));
2892        rec.read_column(e3.column(0).expect("e3.col0"));
2893        rec.read_write(&e2_work_prefix);
2894        rec.write(&e1_work_prefix);
2895        rec.write(&e1_e2_start);
2896        rec.write(&e1_e2_end);
2897        for level in scan_scratch.levels() {
2898            rec.read_write(level);
2899        }
2900        rec.preflight(runtime)
2901            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
2902        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
2903            let e2_kernel = self
2904                .device()
2905                .inner()
2906                .get_func(
2907                    WCOJ_MODULE,
2908                    wcoj_kernels::WCOJ_4CYCLE_BUILD_E2_WORK_PREFIX_U32,
2909                )
2910                .ok_or_else(|| {
2911                    XlogError::Kernel(
2912                        "wcoj_4cycle_build_e2_work_prefix_u32 kernel not found".to_string(),
2913                    )
2914                })?;
2915            let e2_grid = n_e2.div_ceil(BLOCK_SIZE);
2916            unsafe {
2917                e2_kernel
2918                    .clone()
2919                    .launch_in(
2920                        stream,
2921                        LaunchConfig {
2922                            grid_dim: (e2_grid, 1, 1),
2923                            block_dim: (BLOCK_SIZE, 1, 1),
2924                            shared_mem_bytes: 0,
2925                        },
2926                        (e2_col1, n_e2, e3_col0, n_e3, &mut e2_work_prefix),
2927                    )
2928                    .map_err(|e| {
2929                        XlogError::Kernel(format!(
2930                            "wcoj_4cycle_build_e2_work_prefix_u32 launch failed: {e}"
2931                        ))
2932                    })?;
2933            }
2934            self.multiblock_scan_u32_inplace_on_stream(
2935                &mut e2_work_prefix,
2936                e2_prefix_len,
2937                stream,
2938                &mut scan_scratch,
2939            )?;
2940
2941            let kernel = self
2942                .device()
2943                .inner()
2944                .get_func(
2945                    WCOJ_MODULE,
2946                    wcoj_kernels::WCOJ_4CYCLE_BUILD_HG_WORK_PLAN_U32,
2947                )
2948                .ok_or_else(|| {
2949                    XlogError::Kernel(
2950                        "wcoj_4cycle_build_hg_work_plan_u32 kernel not found".to_string(),
2951                    )
2952                })?;
2953            let grid = n_e1.div_ceil(BLOCK_SIZE);
2954            unsafe {
2955                kernel
2956                    .clone()
2957                    .launch_in(
2958                        stream,
2959                        LaunchConfig {
2960                            grid_dim: (grid, 1, 1),
2961                            block_dim: (BLOCK_SIZE, 1, 1),
2962                            shared_mem_bytes: 0,
2963                        },
2964                        (
2965                            e1_col1,
2966                            n_e1,
2967                            e2_col0,
2968                            n_e2,
2969                            &e2_work_prefix,
2970                            &mut e1_work_prefix,
2971                            &mut e1_e2_start,
2972                            &mut e1_e2_end,
2973                        ),
2974                    )
2975                    .map_err(|e| {
2976                        XlogError::Kernel(format!(
2977                            "wcoj_4cycle_build_hg_work_plan_u32 launch failed: {e}"
2978                        ))
2979                    })?;
2980            }
2981            self.multiblock_scan_u32_inplace_on_stream(
2982                &mut e1_work_prefix,
2983                prefix_len,
2984                stream,
2985                &mut scan_scratch,
2986            )?;
2987            Ok(())
2988        };
2989        // SAFETY: preflight retained the accessed buffers and bound this stream.
2990        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
2991            .map_err(|error| error.into_xlog_error())?;
2992
2993        rec.commit()
2994            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
2995        cu_stream
2996            .synchronize()
2997            .map_err(|e| XlogError::Kernel(format!("{ctx}: stream sync failed: {e}")))?;
2998        let total_work = self.dtoh_scalar_untracked::<u32>(&e1_work_prefix, n_e1 as usize)?;
2999        let grid = if total_work == 0 {
3000            1
3001        } else {
3002            total_work.div_ceil(block_work_unit)
3003        };
3004        let block_counts = self.memory().alloc::<u32>(grid as usize)?;
3005        let block_offsets = self.memory().alloc::<u32>(grid as usize)?;
3006
3007        Ok(WcojCycle4HgWorkPlanU32 {
3008            e1_work_prefix,
3009            e2_work_prefix,
3010            e1_e2_start,
3011            e1_e2_end,
3012            block_counts,
3013            block_offsets,
3014            total_work,
3015            block_work_unit,
3016            row_count: n_e1,
3017        })
3018    }
3019
3020    pub fn wcoj_4cycle_hg_u32_recorded(
3021        &self,
3022        e1: &CudaBuffer,
3023        e2: &CudaBuffer,
3024        e3: &CudaBuffer,
3025        e4: &CudaBuffer,
3026        block_work_unit: u32,
3027        launch_stream: StreamId,
3028    ) -> Result<CudaBuffer> {
3029        let ctx = "wcoj_4cycle_hg_u32_recorded";
3030        validate_binary_u32(ctx, "e1", e1)?;
3031        validate_binary_u32(ctx, "e2", e2)?;
3032        validate_binary_u32(ctx, "e3", e3)?;
3033        validate_binary_u32(ctx, "e4", e4)?;
3034        let plan = self.wcoj_4cycle_hg_work_plan_u32_recorded(
3035            e1,
3036            e2,
3037            e3,
3038            e4,
3039            block_work_unit,
3040            launch_stream,
3041        )?;
3042        let out_schema = Schema::new(vec![
3043            (
3044                "col0".to_string(),
3045                e1.schema().column_type(0).expect("e1.col0 type"),
3046            ),
3047            (
3048                "col1".to_string(),
3049                e1.schema().column_type(1).expect("e1.col1 type"),
3050            ),
3051            (
3052                "col2".to_string(),
3053                e2.schema().column_type(1).expect("e2.col1 type"),
3054            ),
3055            (
3056                "col3".to_string(),
3057                e3.schema().column_type(1).expect("e3.col1 type"),
3058            ),
3059        ]);
3060        if plan.total_work == 0 {
3061            return self.create_empty_buffer(out_schema);
3062        }
3063
3064        let grid = plan.total_work.div_ceil(plan.block_work_unit);
3065        let bytes_count = (grid as usize)
3066            .checked_mul(std::mem::size_of::<u32>())
3067            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: count byte size overflow")))?;
3068        let mut local_counts = None;
3069        let mut local_offsets = None;
3070        if grid > 1024 {
3071            local_counts = Some(self.memory().alloc::<u32>(grid as usize)?);
3072            local_offsets = Some(self.memory().alloc::<u32>(grid as usize)?);
3073        }
3074        let total_rows_device = self.memory().alloc::<u32>(1)?;
3075
3076        let runtime = self.memory().runtime().ok_or_else(|| {
3077            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
3078        })?;
3079        let cu_stream = runtime
3080            .stream_pool()
3081            .resolve(launch_stream)
3082            .ok_or_else(|| {
3083                XlogError::Kernel(format!(
3084                    "{ctx}: launch_stream StreamId({}) does not resolve",
3085                    launch_stream.0
3086                ))
3087            })?;
3088
3089        let e1_col0 = &metadata_column_u32(e1, 0)?;
3090        let e1_col1 = &metadata_column_u32(e1, 1)?;
3091        let e2_col1 = &metadata_column_u32(e2, 1)?;
3092        let e3_col0 = &metadata_column_u32(e3, 0)?;
3093        let e3_col1 = &metadata_column_u32(e3, 1)?;
3094        let e4_col0 = &metadata_column_u32(e4, 0)?;
3095        let e4_col1 = &metadata_column_u32(e4, 1)?;
3096        let n_e3 = self.metadata_logical_rows(e3)?;
3097        let n_e4 = self.metadata_logical_rows(e4)?;
3098
3099        let count_u32 = if grid <= 1024 {
3100            &plan.block_counts
3101        } else {
3102            local_counts
3103                .as_ref()
3104                .expect("local HG counts allocated when grid exceeds single-block scan")
3105        };
3106        let mut scan_scratch =
3107            self.multiblock_scan_u32_scratch_for_len(if grid > 1024 { grid } else { 0 })?;
3108        let mut rec_hg = LaunchRecorder::new_strict(launch_stream);
3109        rec_hg.read(e1.num_rows_device());
3110        rec_hg.read(e2.num_rows_device());
3111        rec_hg.read(e3.num_rows_device());
3112        rec_hg.read(e4.num_rows_device());
3113        rec_hg.read_column(e1.column(0).expect("e1.col0"));
3114        rec_hg.read_column(e1.column(1).expect("e1.col1"));
3115        rec_hg.read_column(e2.column(1).expect("e2.col1"));
3116        rec_hg.read_column(e3.column(0).expect("e3.col0"));
3117        rec_hg.read_column(e3.column(1).expect("e3.col1"));
3118        rec_hg.read_column(e4.column(0).expect("e4.col0"));
3119        rec_hg.read_column(e4.column(1).expect("e4.col1"));
3120        rec_hg.read(&plan.e1_work_prefix);
3121        rec_hg.read(&plan.e2_work_prefix);
3122        rec_hg.read(&plan.e1_e2_start);
3123        rec_hg.read(&plan.e1_e2_end);
3124        rec_hg.read_write(count_u32);
3125        if grid <= 1024 {
3126            rec_hg.read_write(&plan.block_offsets);
3127        } else {
3128            rec_hg.read_write(
3129                local_offsets
3130                    .as_ref()
3131                    .expect("local HG offsets allocated when grid exceeds single-block scan"),
3132            );
3133        }
3134        rec_hg.write(&total_rows_device);
3135        for level in scan_scratch.levels() {
3136            rec_hg.read_write(level);
3137        }
3138        rec_hg
3139            .preflight(runtime)
3140            .map_err(|e| XlogError::Kernel(format!("{ctx}: HG preflight failed: {e}")))?;
3141        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
3142            {
3143                let kernel = self
3144                    .device()
3145                    .inner()
3146                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_4CYCLE_COUNT_HG_U32)
3147                    .ok_or_else(|| {
3148                        XlogError::Kernel("wcoj_4cycle_count_hg_u32 kernel not found".to_string())
3149                    })?;
3150                let mut params: Vec<*mut c_void> = vec![
3151                    e1_col0.as_kernel_param(),
3152                    e1_col1.as_kernel_param(),
3153                    plan.row_count.as_kernel_param(),
3154                    e2_col1.as_kernel_param(),
3155                    e3_col0.as_kernel_param(),
3156                    e3_col1.as_kernel_param(),
3157                    n_e3.as_kernel_param(),
3158                    e4_col0.as_kernel_param(),
3159                    e4_col1.as_kernel_param(),
3160                    n_e4.as_kernel_param(),
3161                    (&plan.e1_work_prefix).as_kernel_param(),
3162                    (&plan.e2_work_prefix).as_kernel_param(),
3163                    (&plan.e1_e2_start).as_kernel_param(),
3164                    (&plan.e1_e2_end).as_kernel_param(),
3165                    plan.total_work.as_kernel_param(),
3166                    plan.block_work_unit.as_kernel_param(),
3167                    count_u32.as_kernel_param(),
3168                ];
3169                unsafe {
3170                    kernel
3171                        .clone()
3172                        .launch_in(
3173                            stream,
3174                            LaunchConfig {
3175                                grid_dim: (grid, 1, 1),
3176                                block_dim: (BLOCK_SIZE, 1, 1),
3177                                shared_mem_bytes: 0,
3178                            },
3179                            &mut params,
3180                        )
3181                        .map_err(|e| {
3182                            XlogError::Kernel(format!("{ctx}: count launch failed: {e}"))
3183                        })?;
3184                }
3185            }
3186            if grid <= 1024 {
3187                let kernel = self
3188                    .device()
3189                    .inner()
3190                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_SCAN_HG_BLOCK_COUNTS_U32)
3191                    .ok_or_else(|| {
3192                        XlogError::Kernel(
3193                            "wcoj_scan_hg_block_counts_u32 kernel not found".to_string(),
3194                        )
3195                    })?;
3196                let mut params: Vec<*mut c_void> = vec![
3197                    count_u32.as_kernel_param(),
3198                    grid.as_kernel_param(),
3199                    (&plan.block_offsets).as_kernel_param(),
3200                    (&total_rows_device).as_kernel_param(),
3201                ];
3202                unsafe {
3203                    kernel
3204                        .clone()
3205                        .launch_in(
3206                            stream,
3207                            LaunchConfig {
3208                                grid_dim: (1, 1, 1),
3209                                block_dim: (1024, 1, 1),
3210                                shared_mem_bytes: 0,
3211                            },
3212                            &mut params,
3213                        )
3214                        .map_err(|e| XlogError::Kernel(format!("{ctx}: scan failed: {e}")))?;
3215                }
3216            } else {
3217                let offsets_mut = local_offsets
3218                    .as_mut()
3219                    .expect("local HG offsets allocated when grid exceeds single-block scan");
3220                unsafe {
3221                    let res = sys::cuMemcpyDtoDAsync_v2(
3222                        *offsets_mut.device_ptr(),
3223                        *count_u32.device_ptr(),
3224                        bytes_count,
3225                        stream.stream().cu_stream(),
3226                    );
3227                    if res != sys::cudaError_enum::CUDA_SUCCESS {
3228                        return Err(XlogError::Kernel(format!(
3229                            "{ctx}: DtoD count to offsets failed: {res:?}"
3230                        )));
3231                    }
3232                }
3233                self.multiblock_scan_u32_inplace_on_stream(
3234                    offsets_mut,
3235                    grid,
3236                    stream,
3237                    &mut scan_scratch,
3238                )?;
3239                let total_kernel = self
3240                    .device()
3241                    .inner()
3242                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_COMPUTE_TOTAL)
3243                    .ok_or_else(|| {
3244                        XlogError::Kernel("wcoj_compute_total kernel not found".to_string())
3245                    })?;
3246                let mut params: Vec<*mut c_void> = vec![
3247                    count_u32.as_kernel_param(),
3248                    (&*offsets_mut).as_kernel_param(),
3249                    grid.as_kernel_param(),
3250                    (&total_rows_device).as_kernel_param(),
3251                ];
3252                unsafe {
3253                    total_kernel
3254                        .clone()
3255                        .launch_in(
3256                            stream,
3257                            LaunchConfig {
3258                                grid_dim: (1, 1, 1),
3259                                block_dim: (1, 1, 1),
3260                                shared_mem_bytes: 0,
3261                            },
3262                            &mut params,
3263                        )
3264                        .map_err(|e| XlogError::Kernel(format!("{ctx}: total failed: {e}")))?;
3265                }
3266            }
3267            Ok(())
3268        };
3269        // SAFETY: preflight retained the accessed buffers and bound this stream.
3270        let rec_hg = unsafe { rec_hg.enqueue_prepared_with(&cu_stream, enqueue) }
3271            .map_err(|error| error.into_xlog_error())?;
3272
3273        rec_hg
3274            .commit()
3275            .map_err(|e| XlogError::Kernel(format!("{ctx}: count commit failed: {e}")))?;
3276        cu_stream
3277            .synchronize()
3278            .map_err(|e| XlogError::Kernel(format!("{ctx}: count stream sync failed: {e}")))?;
3279        let total_rows = self
3280            .dtoh_scalar_untracked::<u32>(&total_rows_device, 0)
3281            .map_err(|e| XlogError::Kernel(format!("{ctx}: read total rows failed: {e}")))?;
3282        if total_rows == 0 {
3283            return self.create_empty_buffer(out_schema);
3284        }
3285
3286        let bytes_per_col = (total_rows as usize)
3287            .checked_mul(std::mem::size_of::<u32>())
3288            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: output byte size overflow")))?;
3289        let mut out_w = self.memory().alloc::<u8>(bytes_per_col)?;
3290        let mut out_x = self.memory().alloc::<u8>(bytes_per_col)?;
3291        let mut out_y = self.memory().alloc::<u8>(bytes_per_col)?;
3292        let mut out_z = self.memory().alloc::<u8>(bytes_per_col)?;
3293        let materialize_offsets = if grid <= 1024 {
3294            &plan.block_offsets
3295        } else {
3296            local_offsets
3297                .as_ref()
3298                .expect("local HG offsets allocated when grid exceeds single-block scan")
3299        };
3300        let mut rec_mat = LaunchRecorder::new_strict(launch_stream);
3301        rec_mat.read(materialize_offsets);
3302        rec_mat.read(e1.num_rows_device());
3303        rec_mat.read(e2.num_rows_device());
3304        rec_mat.read(e3.num_rows_device());
3305        rec_mat.read(e4.num_rows_device());
3306        rec_mat.read_column(e1.column(0).expect("e1.col0"));
3307        rec_mat.read_column(e1.column(1).expect("e1.col1"));
3308        rec_mat.read_column(e2.column(1).expect("e2.col1"));
3309        rec_mat.read_column(e3.column(0).expect("e3.col0"));
3310        rec_mat.read_column(e3.column(1).expect("e3.col1"));
3311        rec_mat.read_column(e4.column(0).expect("e4.col0"));
3312        rec_mat.read_column(e4.column(1).expect("e4.col1"));
3313        rec_mat.read(&plan.e1_work_prefix);
3314        rec_mat.read(&plan.e2_work_prefix);
3315        rec_mat.read(&plan.e1_e2_start);
3316        rec_mat.read(&plan.e1_e2_end);
3317        rec_mat.write(&out_w);
3318        rec_mat.write(&out_x);
3319        rec_mat.write(&out_y);
3320        rec_mat.write(&out_z);
3321        rec_mat
3322            .preflight(runtime)
3323            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize preflight failed: {e}")))?;
3324        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
3325            {
3326                let kernel = self
3327                    .device()
3328                    .inner()
3329                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_4CYCLE_MATERIALIZE_HG_U32)
3330                    .ok_or_else(|| {
3331                        XlogError::Kernel(
3332                            "wcoj_4cycle_materialize_hg_u32 kernel not found".to_string(),
3333                        )
3334                    })?;
3335                let out_w_u32 = &mut reinterpret_u8_as_u32(&mut out_w)?;
3336                let out_x_u32 = &mut reinterpret_u8_as_u32(&mut out_x)?;
3337                let out_y_u32 = &mut reinterpret_u8_as_u32(&mut out_y)?;
3338                let out_z_u32 = &mut reinterpret_u8_as_u32(&mut out_z)?;
3339                let mut params: Vec<*mut c_void> = vec![
3340                    e1_col0.as_kernel_param(),
3341                    e1_col1.as_kernel_param(),
3342                    plan.row_count.as_kernel_param(),
3343                    e2_col1.as_kernel_param(),
3344                    e3_col0.as_kernel_param(),
3345                    e3_col1.as_kernel_param(),
3346                    n_e3.as_kernel_param(),
3347                    e4_col0.as_kernel_param(),
3348                    e4_col1.as_kernel_param(),
3349                    n_e4.as_kernel_param(),
3350                    (&plan.e1_work_prefix).as_kernel_param(),
3351                    (&plan.e2_work_prefix).as_kernel_param(),
3352                    (&plan.e1_e2_start).as_kernel_param(),
3353                    (&plan.e1_e2_end).as_kernel_param(),
3354                    plan.total_work.as_kernel_param(),
3355                    plan.block_work_unit.as_kernel_param(),
3356                    materialize_offsets.as_kernel_param(),
3357                    total_rows.as_kernel_param(),
3358                    out_w_u32.as_kernel_param(),
3359                    out_x_u32.as_kernel_param(),
3360                    out_y_u32.as_kernel_param(),
3361                    out_z_u32.as_kernel_param(),
3362                ];
3363                unsafe {
3364                    kernel
3365                        .clone()
3366                        .launch_in(
3367                            stream,
3368                            LaunchConfig {
3369                                grid_dim: (grid, 1, 1),
3370                                block_dim: (BLOCK_SIZE, 1, 1),
3371                                shared_mem_bytes: 0,
3372                            },
3373                            &mut params,
3374                        )
3375                        .map_err(|e| {
3376                            XlogError::Kernel(format!("{ctx}: materialize launch failed: {e}"))
3377                        })?;
3378                }
3379            }
3380            Ok(())
3381        };
3382        // SAFETY: preflight retained the accessed buffers and bound this stream.
3383        let rec_mat = unsafe { rec_mat.enqueue_prepared_with(&cu_stream, enqueue) }
3384            .map_err(|error| error.into_xlog_error())?;
3385
3386        rec_mat
3387            .commit()
3388            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize commit failed: {e}")))?;
3389        cu_stream.synchronize().map_err(|e| {
3390            XlogError::Kernel(format!("{ctx}: materialize stream sync failed: {e}"))
3391        })?;
3392
3393        Ok(CudaBuffer::from_columns_with_host_count(
3394            vec![out_w.into(), out_x.into(), out_y.into(), out_z.into()],
3395            total_rows as u64,
3396            total_rows_device,
3397            out_schema,
3398            total_rows,
3399        ))
3400    }
3401
3402    /// Aggregate-fused 4-cycle group-by-root count: evaluate
3403    /// `q(W, count) :- e1(W,X), e2(X,Y), e3(Y,Z), e4(Z,W)` grouped by the
3404    /// variable-order root W, WITHOUT materializing the 4-cycle rows.
3405    ///
3406    /// Pipeline (all recorded; the 4-cycle result never exists as rows):
3407    /// 1. the standard 4-cycle histogram-guided work plan;
3408    /// 2. `wcoj_4cycle_groupby_root_count_hg_u32` accumulates, per e1 row,
3409    ///    a match count (integer atomicAdd — order-insensitive,
3410    ///    deterministic values);
3411    /// 3. a (W, count) staging buffer over the *input* rows is compacted
3412    ///    to count>0 rows (roots with no completion must be absent) and
3413    ///    reduced per W via the recorded groupby Sum.
3414    ///
3415    /// All reduction work is O(n_e1) — input-sized, never join-output-sized.
3416    ///
3417    /// Output schema matches the unfused materialize+groupby baseline:
3418    /// `col0` = W (e1.col0 type, U32/Symbol), `col1` = count (U64).
3419    ///
3420    /// # Errors
3421    /// * `XlogError::Kernel` if the manager has no runtime, the launch
3422    ///   stream does not resolve, an input is not 2-column U32/Symbol, or
3423    ///   any kernel launch fails.
3424    pub fn wcoj_4cycle_groupby_root_count_u32_recorded(
3425        &self,
3426        e1: &CudaBuffer,
3427        e2: &CudaBuffer,
3428        e3: &CudaBuffer,
3429        e4: &CudaBuffer,
3430        block_work_unit: u32,
3431        launch_stream: StreamId,
3432    ) -> Result<CudaBuffer> {
3433        let ctx = "wcoj_4cycle_groupby_root_count_u32_recorded";
3434        // Layout-normalize per dispatch (sorted-fast-path clone when the
3435        // input is already lex-sorted + unique): the fused path must give
3436        // the same guarantee as the unfused pipeline instead of trusting
3437        // store-buffer sortedness — unsorted/duplicated inputs previously
3438        // produced silently wrong (empty) fused results.
3439        let e1 = &self.wcoj_layout_u32_recorded(e1, launch_stream)?;
3440        let e2 = &self.wcoj_layout_u32_recorded(e2, launch_stream)?;
3441        let e3 = &self.wcoj_layout_u32_recorded(e3, launch_stream)?;
3442        let e4 = &self.wcoj_layout_u32_recorded(e4, launch_stream)?;
3443        validate_binary_u32(ctx, "e1", e1)?;
3444        validate_binary_u32(ctx, "e2", e2)?;
3445        validate_binary_u32(ctx, "e3", e3)?;
3446        validate_binary_u32(ctx, "e4", e4)?;
3447        let plan = self.wcoj_4cycle_hg_work_plan_u32_recorded(
3448            e1,
3449            e2,
3450            e3,
3451            e4,
3452            block_work_unit,
3453            launch_stream,
3454        )?;
3455        let n_e1 = plan.row_count;
3456        let w_type = e1.schema().column_type(0).expect("e1.col0 type");
3457        let out_schema = Schema::new(vec![
3458            ("w".to_string(), w_type),
3459            ("count".to_string(), ScalarType::U64),
3460        ]);
3461        if n_e1 == 0 || plan.total_work == 0 {
3462            return self.create_empty_buffer(out_schema);
3463        }
3464
3465        let runtime = self.memory().runtime().ok_or_else(|| {
3466            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
3467        })?;
3468        let cu_stream = runtime
3469            .stream_pool()
3470            .resolve(launch_stream)
3471            .ok_or_else(|| {
3472                XlogError::Kernel(format!(
3473                    "{ctx}: launch_stream StreamId({}) does not resolve",
3474                    launch_stream.0
3475                ))
3476            })?;
3477
3478        let e1_col0 = &metadata_column_u32(e1, 0)?;
3479        let e1_col1 = &metadata_column_u32(e1, 1)?;
3480        let e2_col1 = &metadata_column_u32(e2, 1)?;
3481        let e3_col0 = &metadata_column_u32(e3, 0)?;
3482        let e3_col1 = &metadata_column_u32(e3, 1)?;
3483        let e4_col0 = &metadata_column_u32(e4, 0)?;
3484        let e4_col1 = &metadata_column_u32(e4, 1)?;
3485        let n_e3 = self.metadata_logical_rows(e3)?;
3486        let n_e4 = self.metadata_logical_rows(e4)?;
3487
3488        // Per-e1-row match counters, zero-initialized. Allocated as the
3489        // u8-backed column layout so the array doubles as the staging
3490        // buffer's count column after the kernel fills it.
3491        let mut row_counts = self
3492            .memory()
3493            .alloc::<u8>(n_e1 as usize * std::mem::size_of::<u32>())?;
3494        self.device()
3495            .inner()
3496            .memset_zeros(&mut row_counts)
3497            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row counts failed: {e}")))?;
3498
3499        let grid = plan.total_work.div_ceil(plan.block_work_unit);
3500        let mut rec = LaunchRecorder::new_strict(launch_stream);
3501        rec.read(e1.num_rows_device());
3502        rec.read(e2.num_rows_device());
3503        rec.read(e3.num_rows_device());
3504        rec.read(e4.num_rows_device());
3505        rec.read_column(e1.column(0).expect("e1.col0"));
3506        rec.read_column(e1.column(1).expect("e1.col1"));
3507        rec.read_column(e2.column(1).expect("e2.col1"));
3508        rec.read_column(e3.column(0).expect("e3.col0"));
3509        rec.read_column(e3.column(1).expect("e3.col1"));
3510        rec.read_column(e4.column(0).expect("e4.col0"));
3511        rec.read_column(e4.column(1).expect("e4.col1"));
3512        rec.read(&plan.e1_work_prefix);
3513        rec.read(&plan.e2_work_prefix);
3514        rec.read(&plan.e1_e2_start);
3515        rec.read(&plan.e1_e2_end);
3516        rec.write(&row_counts);
3517        rec.preflight(runtime)
3518            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
3519        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
3520            {
3521                let kernel = self
3522                    .device()
3523                    .inner()
3524                    .get_func(
3525                        WCOJ_MODULE,
3526                        wcoj_kernels::WCOJ_4CYCLE_GROUPBY_ROOT_COUNT_HG_U32,
3527                    )
3528                    .ok_or_else(|| {
3529                        XlogError::Kernel(
3530                            "wcoj_4cycle_groupby_root_count_hg_u32 kernel not found".to_string(),
3531                        )
3532                    })?;
3533                let mut params: Vec<*mut c_void> = vec![
3534                    e1_col0.as_kernel_param(),
3535                    e1_col1.as_kernel_param(),
3536                    plan.row_count.as_kernel_param(),
3537                    e2_col1.as_kernel_param(),
3538                    e3_col0.as_kernel_param(),
3539                    e3_col1.as_kernel_param(),
3540                    n_e3.as_kernel_param(),
3541                    e4_col0.as_kernel_param(),
3542                    e4_col1.as_kernel_param(),
3543                    n_e4.as_kernel_param(),
3544                    (&plan.e1_work_prefix).as_kernel_param(),
3545                    (&plan.e2_work_prefix).as_kernel_param(),
3546                    (&plan.e1_e2_start).as_kernel_param(),
3547                    (&plan.e1_e2_end).as_kernel_param(),
3548                    plan.total_work.as_kernel_param(),
3549                    plan.block_work_unit.as_kernel_param(),
3550                    (&row_counts).as_kernel_param(),
3551                ];
3552                unsafe {
3553                    kernel
3554                        .clone()
3555                        .launch_in(
3556                            stream,
3557                            LaunchConfig {
3558                                grid_dim: (grid, 1, 1),
3559                                block_dim: (BLOCK_SIZE, 1, 1),
3560                                shared_mem_bytes: 0,
3561                            },
3562                            &mut params,
3563                        )
3564                        .map_err(|e| {
3565                            XlogError::Kernel(format!("{ctx}: groupby-count launch failed: {e}"))
3566                        })?;
3567                }
3568            }
3569            Ok(())
3570        };
3571        // SAFETY: preflight retained the accessed buffers and bound this stream.
3572        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
3573            .map_err(|error| error.into_xlog_error())?;
3574
3575        rec.commit()
3576            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
3577
3578        // Staging buffer (W, count) over the n_e1 input rows: W is a
3579        // device-to-device copy of e1.col0; the count column is the
3580        // kernel-filled array. Rows stay lex-sorted by W.
3581        let w_src = match e1.column(0).expect("e1.col0") {
3582            CudaColumn::Owned(slice) => slice,
3583            _ => {
3584                return Err(XlogError::Kernel(format!(
3585                    "{ctx}: e1.col0 must be an owned CudaColumn"
3586                )))
3587            }
3588        };
3589        let mut w_copy = self
3590            .memory()
3591            .alloc::<u8>(n_e1 as usize * std::mem::size_of::<u32>())?;
3592        self.device()
3593            .inner()
3594            .dtod_copy(w_src, &mut w_copy)
3595            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy W column failed: {e}")))?;
3596        let mut d_num_rows = self.memory().alloc::<u32>(1)?;
3597        self.device()
3598            .inner()
3599            .dtod_copy(e1.num_rows_device(), &mut d_num_rows)
3600            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy row count failed: {e}")))?;
3601        let staging_schema = Schema::new(vec![
3602            ("w".to_string(), w_type),
3603            ("count".to_string(), ScalarType::U32),
3604        ]);
3605        let staging = CudaBuffer::from_columns_with_host_count(
3606            vec![w_copy.into(), row_counts.into()],
3607            n_e1 as u64,
3608            d_num_rows,
3609            staging_schema,
3610            n_e1,
3611        );
3612
3613        // Keep only roots with at least one completed 4-cycle, then reduce
3614        // per W. Both steps run over input-sized data.
3615        let mask = self.compare_const_mask_recorded::<u32>(
3616            &staging,
3617            1,
3618            0u32,
3619            crate::CompareOp::Gt,
3620            launch_stream,
3621        )?;
3622        let compacted =
3623            self.compact_buffer_by_device_mask_counted_recorded(&staging, &mask, launch_stream)?;
3624        self.groupby_multi_agg_recorded(
3625            &compacted,
3626            &[0],
3627            &[(1, xlog_core::AggOp::Sum)],
3628            launch_stream,
3629        )
3630    }
3631
3632    /// Aggregate-fused 4-cycle group-by-root sum/min/max: evaluate
3633    /// `q(W, agg(V)) :- e1(W,X), e2(X,Y), e3(Y,Z), e4(Z,W)` with
3634    /// `agg ∈ {Sum, Min, Max}` and `V ∈ {X, Y, Z}` grouped by the
3635    /// variable-order root W, WITHOUT materializing the 4-cycle rows.
3636    ///
3637    /// Pipeline (all recorded; the 4-cycle result never exists as rows):
3638    /// 1. the standard 4-cycle histogram-guided work plan;
3639    /// 2. the per-op fused kernel accumulates, per e1 row, a match count
3640    ///    (compaction mask) and the per-row partial aggregate (integer
3641    ///    atomics — order-insensitive, deterministic values). Sum partials
3642    ///    are u64 (a per-row partial can exceed `u32::MAX`); min partials
3643    ///    start at `u32::MAX`, max partials at 0;
3644    /// 3. a 3-column (W, count, agg) staging buffer over the *input* rows
3645    ///    is compacted to count>0 rows (roots with no completion must be
3646    ///    absent) and reduced per W via the recorded groupby with the same
3647    ///    `AggOp` (Sum over the u64 partials; Min/Max over u32).
3648    ///
3649    /// All reduction work is O(n_e1) — input-sized, never join-output-sized.
3650    ///
3651    /// Output schema matches the unfused materialize+groupby baseline:
3652    /// `col0` = W (e1.col0 type, U32/Symbol), `col1` = U64 for Sum,
3653    /// U32 for Min/Max.
3654    ///
3655    /// Bag semantics: every (X, Y, Z) completion contributes its value,
3656    /// exactly like aggregating the materialized projection.
3657    ///
3658    /// # Errors
3659    /// * `XlogError::Kernel` if `agg_op` is not Sum/Min/Max, the value
3660    ///   column is not plain U32, the manager has no runtime, the launch
3661    ///   stream does not resolve, an input is not 2-column U32/Symbol, or
3662    ///   any kernel launch fails.
3663    #[expect(
3664        clippy::too_many_arguments,
3665        reason = "the recorded four-cycle aggregate mirrors its four inputs, aggregate selector, work unit, and stream"
3666    )]
3667    pub fn wcoj_4cycle_groupby_root_agg_u32_recorded(
3668        &self,
3669        e1: &CudaBuffer,
3670        e2: &CudaBuffer,
3671        e3: &CudaBuffer,
3672        e4: &CudaBuffer,
3673        agg_op: AggOp,
3674        value: Wcoj4CycleRootAggValue,
3675        block_work_unit: u32,
3676        launch_stream: StreamId,
3677    ) -> Result<CudaBuffer> {
3678        let ctx = "wcoj_4cycle_groupby_root_agg_u32_recorded";
3679        // Layout-normalize per dispatch (sorted-fast-path clone when the
3680        // input is already lex-sorted + unique): the fused path must give
3681        // the same guarantee as the unfused pipeline instead of trusting
3682        // store-buffer sortedness — unsorted/duplicated inputs previously
3683        // produced silently wrong (empty) fused results.
3684        let e1 = &self.wcoj_layout_u32_recorded(e1, launch_stream)?;
3685        let e2 = &self.wcoj_layout_u32_recorded(e2, launch_stream)?;
3686        let e3 = &self.wcoj_layout_u32_recorded(e3, launch_stream)?;
3687        let e4 = &self.wcoj_layout_u32_recorded(e4, launch_stream)?;
3688        let (kernel_name, agg_elem_size, agg_scalar, agg_name) = match agg_op {
3689            AggOp::Sum => (
3690                wcoj_kernels::WCOJ_4CYCLE_GROUPBY_ROOT_SUM_HG_U32,
3691                std::mem::size_of::<u64>(),
3692                ScalarType::U64,
3693                "sum_0",
3694            ),
3695            AggOp::Min => (
3696                wcoj_kernels::WCOJ_4CYCLE_GROUPBY_ROOT_MIN_HG_U32,
3697                std::mem::size_of::<u32>(),
3698                ScalarType::U32,
3699                "min_0",
3700            ),
3701            AggOp::Max => (
3702                wcoj_kernels::WCOJ_4CYCLE_GROUPBY_ROOT_MAX_HG_U32,
3703                std::mem::size_of::<u32>(),
3704                ScalarType::U32,
3705                "max_0",
3706            ),
3707            other => {
3708                return Err(XlogError::Kernel(format!(
3709                    "{ctx}: unsupported AggOp {other:?} (Sum/Min/Max only; use \
3710                     wcoj_4cycle_groupby_root_count_u32_recorded for Count)"
3711                )))
3712            }
3713        };
3714        validate_binary_u32(ctx, "e1", e1)?;
3715        validate_binary_u32(ctx, "e2", e2)?;
3716        validate_binary_u32(ctx, "e3", e3)?;
3717        validate_binary_u32(ctx, "e4", e4)?;
3718        // The aggregate value is arithmetic: require a plain U32 value
3719        // column (Symbol ids are not summable/orderable data). The column
3720        // checked is exactly the one the kernel reads the value from —
3721        // and the one whose type the materialized (W, X, Y, Z) baseline
3722        // schema carries (`build_4cycle_head_schema`).
3723        let (value_buf, value_label) = match value {
3724            Wcoj4CycleRootAggValue::X => (e1, "e1"),
3725            Wcoj4CycleRootAggValue::Y => (e2, "e2"),
3726            Wcoj4CycleRootAggValue::Z => (e3, "e3"),
3727        };
3728        {
3729            let ty = value_buf.schema().column_type(1).expect("validated 2-col");
3730            if ty != ScalarType::U32 {
3731                return Err(XlogError::Kernel(format!(
3732                    "{ctx}: {value_label}.col1 supplies the aggregate value and must be U32, \
3733                     got {ty:?}"
3734                )));
3735            }
3736        }
3737
3738        let plan = self.wcoj_4cycle_hg_work_plan_u32_recorded(
3739            e1,
3740            e2,
3741            e3,
3742            e4,
3743            block_work_unit,
3744            launch_stream,
3745        )?;
3746        let n_e1 = plan.row_count;
3747        let w_type = e1.schema().column_type(0).expect("e1.col0 type");
3748        let out_schema = Schema::new(vec![
3749            ("w".to_string(), w_type),
3750            (agg_name.to_string(), agg_scalar),
3751        ]);
3752        if n_e1 == 0 || plan.total_work == 0 {
3753            return self.create_empty_buffer(out_schema);
3754        }
3755
3756        let runtime = self.memory().runtime().ok_or_else(|| {
3757            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
3758        })?;
3759        let cu_stream = runtime
3760            .stream_pool()
3761            .resolve(launch_stream)
3762            .ok_or_else(|| {
3763                XlogError::Kernel(format!(
3764                    "{ctx}: launch_stream StreamId({}) does not resolve",
3765                    launch_stream.0
3766                ))
3767            })?;
3768
3769        let e1_col0 = &metadata_column_u32(e1, 0)?;
3770        let e1_col1 = &metadata_column_u32(e1, 1)?;
3771        let e2_col1 = &metadata_column_u32(e2, 1)?;
3772        let e3_col0 = &metadata_column_u32(e3, 0)?;
3773        let e3_col1 = &metadata_column_u32(e3, 1)?;
3774        let e4_col0 = &metadata_column_u32(e4, 0)?;
3775        let e4_col1 = &metadata_column_u32(e4, 1)?;
3776        let n_e3 = self.metadata_logical_rows(e3)?;
3777        let n_e4 = self.metadata_logical_rows(e4)?;
3778        let value_sel: u32 = match value {
3779            Wcoj4CycleRootAggValue::X => 0,
3780            Wcoj4CycleRootAggValue::Y => 1,
3781            Wcoj4CycleRootAggValue::Z => 2,
3782        };
3783
3784        // Per-e1-row match counters + aggregate partials, allocated as
3785        // the u8-backed column layout so the arrays double as the staging
3786        // buffer's columns after the kernel fills them.
3787        let mut row_counts = self
3788            .memory()
3789            .alloc::<u8>(n_e1 as usize * std::mem::size_of::<u32>())?;
3790        self.device()
3791            .inner()
3792            .memset_zeros(&mut row_counts)
3793            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row counts failed: {e}")))?;
3794        let mut row_agg = self.memory().alloc::<u8>(n_e1 as usize * agg_elem_size)?;
3795        self.device()
3796            .inner()
3797            .memset_zeros(&mut row_agg)
3798            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row aggregates failed: {e}")))?;
3799
3800        let grid = plan.total_work.div_ceil(plan.block_work_unit);
3801        let mut rec = LaunchRecorder::new_strict(launch_stream);
3802        rec.read(e1.num_rows_device());
3803        rec.read(e2.num_rows_device());
3804        rec.read(e3.num_rows_device());
3805        rec.read(e4.num_rows_device());
3806        rec.read_column(e1.column(0).expect("e1.col0"));
3807        rec.read_column(e1.column(1).expect("e1.col1"));
3808        rec.read_column(e2.column(1).expect("e2.col1"));
3809        rec.read_column(e3.column(0).expect("e3.col0"));
3810        rec.read_column(e3.column(1).expect("e3.col1"));
3811        rec.read_column(e4.column(0).expect("e4.col0"));
3812        rec.read_column(e4.column(1).expect("e4.col1"));
3813        rec.read(&plan.e1_work_prefix);
3814        rec.read(&plan.e2_work_prefix);
3815        rec.read(&plan.e1_e2_start);
3816        rec.read(&plan.e1_e2_end);
3817        rec.write(&row_counts);
3818        rec.write(&row_agg);
3819        rec.preflight(runtime)
3820            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
3821        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
3822            if matches!(agg_op, AggOp::Min) {
3823                // Min identity: u32::MAX (compaction drops untouched rows).
3824                let fill = self
3825                    .device()
3826                    .inner()
3827                    .get_func(ARITH_MODULE, arith_kernels::ARITH_FILL_CONST_U32)
3828                    .ok_or_else(|| {
3829                        XlogError::Kernel("arith_fill_const_u32 kernel not found".to_string())
3830                    })?;
3831                let row_agg_u32 = &mut reinterpret_u8_as_u32(&mut row_agg)?;
3832                // SAFETY: arith_fill_const_u32(value, n, output)
3833                unsafe {
3834                    fill.clone()
3835                        .launch_in(
3836                            stream,
3837                            LaunchConfig::for_num_elems(n_e1),
3838                            (u32::MAX, n_e1, &mut *row_agg_u32),
3839                        )
3840                        .map_err(|e| {
3841                            XlogError::Kernel(format!("{ctx}: min identity fill failed: {e}"))
3842                        })?;
3843                }
3844            }
3845            {
3846                let kernel = self
3847                    .device()
3848                    .inner()
3849                    .get_func(WCOJ_MODULE, kernel_name)
3850                    .ok_or_else(|| XlogError::Kernel(format!("{kernel_name} kernel not found")))?;
3851                let mut params: Vec<*mut c_void> = vec![
3852                    e1_col0.as_kernel_param(),
3853                    e1_col1.as_kernel_param(),
3854                    plan.row_count.as_kernel_param(),
3855                    e2_col1.as_kernel_param(),
3856                    e3_col0.as_kernel_param(),
3857                    e3_col1.as_kernel_param(),
3858                    n_e3.as_kernel_param(),
3859                    e4_col0.as_kernel_param(),
3860                    e4_col1.as_kernel_param(),
3861                    n_e4.as_kernel_param(),
3862                    value_sel.as_kernel_param(),
3863                    (&plan.e1_work_prefix).as_kernel_param(),
3864                    (&plan.e2_work_prefix).as_kernel_param(),
3865                    (&plan.e1_e2_start).as_kernel_param(),
3866                    (&plan.e1_e2_end).as_kernel_param(),
3867                    plan.total_work.as_kernel_param(),
3868                    plan.block_work_unit.as_kernel_param(),
3869                    (&row_counts).as_kernel_param(),
3870                    (&row_agg).as_kernel_param(),
3871                ];
3872                unsafe {
3873                    kernel
3874                        .clone()
3875                        .launch_in(
3876                            stream,
3877                            LaunchConfig {
3878                                grid_dim: (grid, 1, 1),
3879                                block_dim: (BLOCK_SIZE, 1, 1),
3880                                shared_mem_bytes: 0,
3881                            },
3882                            &mut params,
3883                        )
3884                        .map_err(|e| {
3885                            XlogError::Kernel(format!("{ctx}: groupby-agg launch failed: {e}"))
3886                        })?;
3887                }
3888            }
3889            Ok(())
3890        };
3891        // SAFETY: preflight retained the accessed buffers and bound this stream.
3892        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
3893            .map_err(|error| error.into_xlog_error())?;
3894
3895        rec.commit()
3896            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
3897
3898        // Staging buffer (W, count, agg) over the n_e1 input rows: W is a
3899        // device-to-device copy of e1.col0; count and agg are the
3900        // kernel-filled arrays. Rows stay lex-sorted by W.
3901        let w_src = match e1.column(0).expect("e1.col0") {
3902            CudaColumn::Owned(slice) => slice,
3903            _ => {
3904                return Err(XlogError::Kernel(format!(
3905                    "{ctx}: e1.col0 must be an owned CudaColumn"
3906                )))
3907            }
3908        };
3909        let w_copy = self
3910            .memory()
3911            .alloc::<u8>(n_e1 as usize * std::mem::size_of::<u32>())?;
3912        // Explicit-length copy: layout-normalized columns are allocated at
3913        // capacity, which can exceed the logical n_e1 * 4 bytes a full-slice
3914        // typed copy would assert on.
3915        unsafe {
3916            let res = sys::cuMemcpyDtoD_v2(
3917                *w_copy.device_ptr(),
3918                *w_src.device_ptr(),
3919                n_e1 as usize * std::mem::size_of::<u32>(),
3920            );
3921            if res != sys::cudaError_enum::CUDA_SUCCESS {
3922                return Err(XlogError::Kernel(format!(
3923                    "{ctx}: copy W column failed: {res:?}"
3924                )));
3925            }
3926        }
3927        let mut d_num_rows = self.memory().alloc::<u32>(1)?;
3928        self.device()
3929            .inner()
3930            .dtod_copy(e1.num_rows_device(), &mut d_num_rows)
3931            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy row count failed: {e}")))?;
3932        let staging_schema = Schema::new(vec![
3933            ("w".to_string(), w_type),
3934            ("count".to_string(), ScalarType::U32),
3935            ("agg".to_string(), agg_scalar),
3936        ]);
3937        let staging = CudaBuffer::from_columns_with_host_count(
3938            vec![w_copy.into(), row_counts.into(), row_agg.into()],
3939            n_e1 as u64,
3940            d_num_rows,
3941            staging_schema,
3942            n_e1,
3943        );
3944
3945        // Keep only roots with at least one completed 4-cycle, then reduce
3946        // per W with the same AggOp. Both steps run over input-sized data.
3947        let mask = self.compare_const_mask_recorded::<u32>(
3948            &staging,
3949            1,
3950            0u32,
3951            crate::CompareOp::Gt,
3952            launch_stream,
3953        )?;
3954        let compacted =
3955            self.compact_buffer_by_device_mask_counted_recorded(&staging, &mask, launch_stream)?;
3956        self.groupby_multi_agg_recorded(&compacted, &[0], &[(2, agg_op)], launch_stream)
3957    }
3958
3959    /// U64-key aggregate-fused 4-cycle count sibling of
3960    /// [`Self::wcoj_4cycle_groupby_root_count_u32_recorded`]: evaluate
3961    /// `q(W, count) :- e1(W,X), e2(X,Y), e3(Y,Z), e4(Z,W)` grouped by the
3962    /// variable-order root W for U64 relations, WITHOUT materializing the
3963    /// 4-cycle rows.
3964    ///
3965    /// The recorded groupby is U32/Symbol-key only, so the per-W reduction
3966    /// reuses the WCOJ relation metadata instead (mirroring
3967    /// [`Self::wcoj_triangle_groupby_root_count_u64_recorded`]): e1 is
3968    /// lex-sorted, so `wcoj_build_metadata_u64_recorded` yields one
3969    /// (unique W, group start) pair per root, and
3970    /// `wcoj_groupby_root_segment_sum_counts_u32` accumulates the per-row
3971    /// match counts into per-unique-root u64 totals (integer atomicAdd —
3972    /// deterministic). Roots with zero completions are compacted away.
3973    /// All reduction work is O(n_e1).
3974    ///
3975    /// Output schema matches the unfused materialize+groupby baseline:
3976    /// `col0` = W (U64), `col1` = count (U64).
3977    ///
3978    /// # Errors
3979    /// * `XlogError::Kernel` if the manager has no runtime, the launch
3980    ///   stream does not resolve, an input is not 2-column U64, or any
3981    ///   kernel launch fails.
3982    pub fn wcoj_4cycle_groupby_root_count_u64_recorded(
3983        &self,
3984        e1: &CudaBuffer,
3985        e2: &CudaBuffer,
3986        e3: &CudaBuffer,
3987        e4: &CudaBuffer,
3988        block_work_unit: u32,
3989        launch_stream: StreamId,
3990    ) -> Result<CudaBuffer> {
3991        let ctx = "wcoj_4cycle_groupby_root_count_u64_recorded";
3992        // Layout-normalize per dispatch (sorted-fast-path clone when the
3993        // input is already lex-sorted + unique): the fused path must give
3994        // the same guarantee as the unfused pipeline instead of trusting
3995        // store-buffer sortedness — unsorted/duplicated inputs previously
3996        // produced silently wrong (empty) fused results.
3997        let e1 = &self.wcoj_layout_u64_recorded(e1, launch_stream)?;
3998        let e2 = &self.wcoj_layout_u64_recorded(e2, launch_stream)?;
3999        let e3 = &self.wcoj_layout_u64_recorded(e3, launch_stream)?;
4000        let e4 = &self.wcoj_layout_u64_recorded(e4, launch_stream)?;
4001        validate_binary_u64(ctx, "e1", e1)?;
4002        validate_binary_u64(ctx, "e2", e2)?;
4003        validate_binary_u64(ctx, "e3", e3)?;
4004        validate_binary_u64(ctx, "e4", e4)?;
4005        let plan = self.wcoj_4cycle_hg_work_plan_u64_recorded(
4006            e1,
4007            e2,
4008            e3,
4009            e4,
4010            block_work_unit,
4011            launch_stream,
4012        )?;
4013        let n_e1 = plan.row_count;
4014        let out_schema = Schema::new(vec![
4015            ("w".to_string(), ScalarType::U64),
4016            ("count".to_string(), ScalarType::U64),
4017        ]);
4018        if n_e1 == 0 || plan.total_work == 0 {
4019            return self.create_empty_buffer(out_schema);
4020        }
4021
4022        let runtime = self.memory().runtime().ok_or_else(|| {
4023            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
4024        })?;
4025        let cu_stream = runtime
4026            .stream_pool()
4027            .resolve(launch_stream)
4028            .ok_or_else(|| {
4029                XlogError::Kernel(format!(
4030                    "{ctx}: launch_stream StreamId({}) does not resolve",
4031                    launch_stream.0
4032                ))
4033            })?;
4034
4035        let e1_col0 = &metadata_column_u64(e1, 0)?;
4036        let e1_col1 = &metadata_column_u64(e1, 1)?;
4037        let e2_col1 = &metadata_column_u64(e2, 1)?;
4038        let e3_col0 = &metadata_column_u64(e3, 0)?;
4039        let e3_col1 = &metadata_column_u64(e3, 1)?;
4040        let e4_col0 = &metadata_column_u64(e4, 0)?;
4041        let e4_col1 = &metadata_column_u64(e4, 1)?;
4042        let n_e3 = self.metadata_logical_rows(e3)?;
4043        let n_e4 = self.metadata_logical_rows(e4)?;
4044
4045        // Per-e1-row match counters, zero-initialized.
4046        let mut row_counts = self.memory().alloc::<u32>(n_e1 as usize)?;
4047        self.device()
4048            .inner()
4049            .memset_zeros(&mut row_counts)
4050            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero row counts failed: {e}")))?;
4051
4052        let grid = plan.total_work.div_ceil(plan.block_work_unit);
4053        let mut rec = LaunchRecorder::new_strict(launch_stream);
4054        rec.read(e1.num_rows_device());
4055        rec.read(e2.num_rows_device());
4056        rec.read(e3.num_rows_device());
4057        rec.read(e4.num_rows_device());
4058        rec.read_column(e1.column(0).expect("e1.col0"));
4059        rec.read_column(e1.column(1).expect("e1.col1"));
4060        rec.read_column(e2.column(1).expect("e2.col1"));
4061        rec.read_column(e3.column(0).expect("e3.col0"));
4062        rec.read_column(e3.column(1).expect("e3.col1"));
4063        rec.read_column(e4.column(0).expect("e4.col0"));
4064        rec.read_column(e4.column(1).expect("e4.col1"));
4065        rec.read(&plan.e1_work_prefix);
4066        rec.read(&plan.e2_work_prefix);
4067        rec.read(&plan.e1_e2_start);
4068        rec.read(&plan.e1_e2_end);
4069        rec.write(&row_counts);
4070        rec.preflight(runtime)
4071            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
4072        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
4073            {
4074                let kernel = self
4075                    .device()
4076                    .inner()
4077                    .get_func(
4078                        WCOJ_MODULE,
4079                        wcoj_kernels::WCOJ_4CYCLE_GROUPBY_ROOT_COUNT_HG_U64,
4080                    )
4081                    .ok_or_else(|| {
4082                        XlogError::Kernel(
4083                            "wcoj_4cycle_groupby_root_count_hg_u64 kernel not found".to_string(),
4084                        )
4085                    })?;
4086                let mut params: Vec<*mut c_void> = vec![
4087                    e1_col0.as_kernel_param(),
4088                    e1_col1.as_kernel_param(),
4089                    plan.row_count.as_kernel_param(),
4090                    e2_col1.as_kernel_param(),
4091                    e3_col0.as_kernel_param(),
4092                    e3_col1.as_kernel_param(),
4093                    n_e3.as_kernel_param(),
4094                    e4_col0.as_kernel_param(),
4095                    e4_col1.as_kernel_param(),
4096                    n_e4.as_kernel_param(),
4097                    (&plan.e1_work_prefix).as_kernel_param(),
4098                    (&plan.e2_work_prefix).as_kernel_param(),
4099                    (&plan.e1_e2_start).as_kernel_param(),
4100                    (&plan.e1_e2_end).as_kernel_param(),
4101                    plan.total_work.as_kernel_param(),
4102                    plan.block_work_unit.as_kernel_param(),
4103                    (&row_counts).as_kernel_param(),
4104                ];
4105                unsafe {
4106                    kernel
4107                        .clone()
4108                        .launch_in(
4109                            stream,
4110                            LaunchConfig {
4111                                grid_dim: (grid, 1, 1),
4112                                block_dim: (BLOCK_SIZE, 1, 1),
4113                                shared_mem_bytes: 0,
4114                            },
4115                            &mut params,
4116                        )
4117                        .map_err(|e| {
4118                            XlogError::Kernel(format!("{ctx}: groupby-count launch failed: {e}"))
4119                        })?;
4120                }
4121            }
4122            Ok(())
4123        };
4124        // SAFETY: preflight retained the accessed buffers and bound this stream.
4125        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
4126            .map_err(|error| error.into_xlog_error())?;
4127
4128        rec.commit()
4129            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
4130
4131        // Per-W reduction via the relation metadata: one (unique W, group
4132        // start) pair per root; e1 is lex-sorted by W so group rows are
4133        // contiguous.
4134        let meta = self.wcoj_build_metadata_u64_recorded(e1, 0, launch_stream)?;
4135        let key_count = meta.key_count;
4136        if key_count == 0 {
4137            return self.create_empty_buffer(out_schema);
4138        }
4139        let mut sums = self
4140            .memory()
4141            .alloc::<u8>(key_count as usize * std::mem::size_of::<u64>())?;
4142        self.device()
4143            .inner()
4144            .memset_zeros(&mut sums)
4145            .map_err(|e| XlogError::Kernel(format!("{ctx}: zero group sums failed: {e}")))?;
4146
4147        let mut rec_sum = LaunchRecorder::new_strict(launch_stream);
4148        rec_sum.read(&row_counts);
4149        rec_sum.read(&meta.prefix_sum);
4150        rec_sum.write(&sums);
4151        rec_sum
4152            .preflight(runtime)
4153            .map_err(|e| XlogError::Kernel(format!("{ctx}: reduce preflight failed: {e}")))?;
4154        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
4155            {
4156                let kernel = self
4157                    .device()
4158                    .inner()
4159                    .get_func(
4160                        WCOJ_MODULE,
4161                        wcoj_kernels::WCOJ_GROUPBY_ROOT_SEGMENT_SUM_COUNTS_U32,
4162                    )
4163                    .ok_or_else(|| {
4164                        XlogError::Kernel(
4165                            "wcoj_groupby_root_segment_sum_counts_u32 kernel not found".to_string(),
4166                        )
4167                    })?;
4168                let reduce_grid = n_e1.div_ceil(BLOCK_SIZE);
4169                let mut params: Vec<*mut c_void> = vec![
4170                    (&row_counts).as_kernel_param(),
4171                    n_e1.as_kernel_param(),
4172                    (&meta.prefix_sum).as_kernel_param(),
4173                    key_count.as_kernel_param(),
4174                    (&sums).as_kernel_param(),
4175                ];
4176                unsafe {
4177                    kernel
4178                        .clone()
4179                        .launch_in(
4180                            stream,
4181                            LaunchConfig {
4182                                grid_dim: (reduce_grid, 1, 1),
4183                                block_dim: (BLOCK_SIZE, 1, 1),
4184                                shared_mem_bytes: 0,
4185                            },
4186                            &mut params,
4187                        )
4188                        .map_err(|e| {
4189                            XlogError::Kernel(format!("{ctx}: reduce launch failed: {e}"))
4190                        })?;
4191                }
4192            }
4193            Ok(())
4194        };
4195        // SAFETY: preflight retained the accessed buffers and bound this stream.
4196        let rec_sum = unsafe { rec_sum.enqueue_prepared_with(&cu_stream, enqueue) }
4197            .map_err(|error| error.into_xlog_error())?;
4198
4199        rec_sum
4200            .commit()
4201            .map_err(|e| XlogError::Kernel(format!("{ctx}: reduce commit failed: {e}")))?;
4202
4203        // (unique W, total) buffer over the key_count roots, then drop the
4204        // roots with no completion. The copies run on launch_stream and the
4205        // fresh destination blocks are registered through the strict
4206        // recorder BEFORE the enqueue — a raw async copy into a freshly
4207        // pool-allocated block without recording is a visibility race.
4208        let w_copy = self
4209            .memory()
4210            .alloc::<u8>(key_count as usize * std::mem::size_of::<u64>())?;
4211        let d_num_rows = self.memory().alloc::<u32>(1)?;
4212        let mut rec_copy = LaunchRecorder::new_strict(launch_stream);
4213        rec_copy.read(&meta.unique_keys);
4214        rec_copy.write(&w_copy);
4215        rec_copy.write(&d_num_rows);
4216        rec_copy
4217            .preflight(runtime)
4218            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy preflight failed: {e}")))?;
4219        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
4220            unsafe {
4221                let res = sys::cuMemcpyDtoDAsync_v2(
4222                    *w_copy.device_ptr(),
4223                    *meta.unique_keys.device_ptr(),
4224                    key_count as usize * std::mem::size_of::<u64>(),
4225                    stream.stream().cu_stream(),
4226                );
4227                if res != sys::cudaError_enum::CUDA_SUCCESS {
4228                    return Err(XlogError::Kernel(format!(
4229                        "{ctx}: DtoD unique keys copy failed: {res:?}"
4230                    )));
4231                }
4232            }
4233            self.initialize_launch_metadata_u32(
4234                key_count,
4235                &d_num_rows,
4236                stream,
4237                &format!("{ctx}: d_num_rows"),
4238            )?;
4239            Ok(())
4240        };
4241        // SAFETY: preflight retained the accessed buffers and bound this stream.
4242        let rec_copy = unsafe { rec_copy.enqueue_prepared_with(&cu_stream, enqueue) }
4243            .map_err(|error| error.into_xlog_error())?;
4244
4245        rec_copy
4246            .commit()
4247            .map_err(|e| XlogError::Kernel(format!("{ctx}: copy commit failed: {e}")))?;
4248        cu_stream
4249            .synchronize()
4250            .map_err(|e| XlogError::Kernel(format!("{ctx}: stream sync failed: {e}")))?;
4251        let staging_schema = Schema::new(vec![
4252            ("w".to_string(), ScalarType::U64),
4253            ("count".to_string(), ScalarType::U64),
4254        ]);
4255        let staging = CudaBuffer::from_columns_with_host_count(
4256            vec![w_copy.into(), sums.into()],
4257            u64::from(key_count),
4258            d_num_rows,
4259            staging_schema,
4260            key_count,
4261        );
4262        let mask = self.compare_const_mask_recorded::<u64>(
4263            &staging,
4264            1,
4265            0u64,
4266            crate::CompareOp::Gt,
4267            launch_stream,
4268        )?;
4269        self.compact_buffer_by_device_mask_counted_recorded(&staging, &mask, launch_stream)
4270    }
4271
4272    pub fn wcoj_4cycle_hg_work_plan_u64_recorded(
4273        &self,
4274        e1: &CudaBuffer,
4275        e2: &CudaBuffer,
4276        e3: &CudaBuffer,
4277        e4: &CudaBuffer,
4278        block_work_unit: u32,
4279        launch_stream: StreamId,
4280    ) -> Result<WcojCycle4HgWorkPlanU64> {
4281        let ctx = "wcoj_4cycle_hg_work_plan_u64_recorded";
4282        if block_work_unit == 0 {
4283            return Err(XlogError::Kernel(format!(
4284                "{ctx}: block_work_unit must be nonzero"
4285            )));
4286        }
4287        validate_binary_u64(ctx, "e1", e1)?;
4288        validate_binary_u64(ctx, "e2", e2)?;
4289        validate_binary_u64(ctx, "e3", e3)?;
4290        validate_binary_u64(ctx, "e4", e4)?;
4291
4292        let n_e1 = self.metadata_logical_rows(e1)?;
4293        let n_e2 = self.metadata_logical_rows(e2)?;
4294        let n_e3 = self.metadata_logical_rows(e3)?;
4295        let prefix_len = n_e1
4296            .checked_add(1)
4297            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: prefix length overflow")))?;
4298        let e2_prefix_len = n_e2
4299            .checked_add(1)
4300            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: e2 prefix length overflow")))?;
4301        let mut e1_work_prefix = self.memory().alloc::<u32>(prefix_len as usize)?;
4302        let mut e2_work_prefix = self.memory().alloc::<u32>(e2_prefix_len as usize)?;
4303        let mut e1_e2_start = self.memory().alloc::<u32>(n_e1 as usize)?;
4304        let mut e1_e2_end = self.memory().alloc::<u32>(n_e1 as usize)?;
4305
4306        if n_e1 == 0 || n_e2 == 0 || n_e3 == 0 || self.metadata_logical_rows(e4)? == 0 {
4307            let block_counts = self.memory().alloc::<u32>(1)?;
4308            let block_offsets = self.memory().alloc::<u32>(1)?;
4309            return Ok(WcojCycle4HgWorkPlanU64 {
4310                e1_work_prefix,
4311                e2_work_prefix,
4312                e1_e2_start,
4313                e1_e2_end,
4314                block_counts,
4315                block_offsets,
4316                total_work: 0,
4317                block_work_unit,
4318                row_count: n_e1,
4319            });
4320        }
4321
4322        let runtime = self.memory().runtime().ok_or_else(|| {
4323            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
4324        })?;
4325        let cu_stream = runtime
4326            .stream_pool()
4327            .resolve(launch_stream)
4328            .ok_or_else(|| {
4329                XlogError::Kernel(format!(
4330                    "{ctx}: launch_stream StreamId({}) does not resolve",
4331                    launch_stream.0
4332                ))
4333            })?;
4334
4335        let e1_col1 = &metadata_column_u64(e1, 1)?;
4336        let e2_col0 = &metadata_column_u64(e2, 0)?;
4337        let e2_col1 = &metadata_column_u64(e2, 1)?;
4338        let e3_col0 = &metadata_column_u64(e3, 0)?;
4339
4340        let mut scan_scratch =
4341            self.multiblock_scan_u32_scratch_for_len(e2_prefix_len.max(prefix_len))?;
4342        let mut rec = LaunchRecorder::new_strict(launch_stream);
4343        rec.read(e1.num_rows_device());
4344        rec.read(e2.num_rows_device());
4345        rec.read(e3.num_rows_device());
4346        rec.read_column(e1.column(1).expect("e1.col1"));
4347        rec.read_column(e2.column(0).expect("e2.col0"));
4348        rec.read_column(e2.column(1).expect("e2.col1"));
4349        rec.read_column(e3.column(0).expect("e3.col0"));
4350        rec.read_write(&e2_work_prefix);
4351        rec.write(&e1_work_prefix);
4352        rec.write(&e1_e2_start);
4353        rec.write(&e1_e2_end);
4354        for level in scan_scratch.levels() {
4355            rec.read_write(level);
4356        }
4357        rec.preflight(runtime)
4358            .map_err(|e| XlogError::Kernel(format!("{ctx}: preflight failed: {e}")))?;
4359        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
4360            let e2_kernel = self
4361                .device()
4362                .inner()
4363                .get_func(
4364                    WCOJ_MODULE,
4365                    wcoj_kernels::WCOJ_4CYCLE_BUILD_E2_WORK_PREFIX_U64,
4366                )
4367                .ok_or_else(|| {
4368                    XlogError::Kernel(
4369                        "wcoj_4cycle_build_e2_work_prefix_u64 kernel not found".to_string(),
4370                    )
4371                })?;
4372            let e2_grid = n_e2.div_ceil(BLOCK_SIZE);
4373            unsafe {
4374                e2_kernel
4375                    .clone()
4376                    .launch_in(
4377                        stream,
4378                        LaunchConfig {
4379                            grid_dim: (e2_grid, 1, 1),
4380                            block_dim: (BLOCK_SIZE, 1, 1),
4381                            shared_mem_bytes: 0,
4382                        },
4383                        (e2_col1, n_e2, e3_col0, n_e3, &mut e2_work_prefix),
4384                    )
4385                    .map_err(|e| {
4386                        XlogError::Kernel(format!(
4387                            "wcoj_4cycle_build_e2_work_prefix_u64 launch failed: {e}"
4388                        ))
4389                    })?;
4390            }
4391            self.multiblock_scan_u32_inplace_on_stream(
4392                &mut e2_work_prefix,
4393                e2_prefix_len,
4394                stream,
4395                &mut scan_scratch,
4396            )?;
4397
4398            let kernel = self
4399                .device()
4400                .inner()
4401                .get_func(
4402                    WCOJ_MODULE,
4403                    wcoj_kernels::WCOJ_4CYCLE_BUILD_HG_WORK_PLAN_U64,
4404                )
4405                .ok_or_else(|| {
4406                    XlogError::Kernel(
4407                        "wcoj_4cycle_build_hg_work_plan_u64 kernel not found".to_string(),
4408                    )
4409                })?;
4410            let grid = n_e1.div_ceil(BLOCK_SIZE);
4411            unsafe {
4412                kernel
4413                    .clone()
4414                    .launch_in(
4415                        stream,
4416                        LaunchConfig {
4417                            grid_dim: (grid, 1, 1),
4418                            block_dim: (BLOCK_SIZE, 1, 1),
4419                            shared_mem_bytes: 0,
4420                        },
4421                        (
4422                            e1_col1,
4423                            n_e1,
4424                            e2_col0,
4425                            n_e2,
4426                            &e2_work_prefix,
4427                            &mut e1_work_prefix,
4428                            &mut e1_e2_start,
4429                            &mut e1_e2_end,
4430                        ),
4431                    )
4432                    .map_err(|e| {
4433                        XlogError::Kernel(format!(
4434                            "wcoj_4cycle_build_hg_work_plan_u64 launch failed: {e}"
4435                        ))
4436                    })?;
4437            }
4438            self.multiblock_scan_u32_inplace_on_stream(
4439                &mut e1_work_prefix,
4440                prefix_len,
4441                stream,
4442                &mut scan_scratch,
4443            )?;
4444            Ok(())
4445        };
4446        // SAFETY: preflight retained the accessed buffers and bound this stream.
4447        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
4448            .map_err(|error| error.into_xlog_error())?;
4449
4450        rec.commit()
4451            .map_err(|e| XlogError::Kernel(format!("{ctx}: commit failed: {e}")))?;
4452        cu_stream
4453            .synchronize()
4454            .map_err(|e| XlogError::Kernel(format!("{ctx}: stream sync failed: {e}")))?;
4455        let total_work = self.dtoh_scalar_untracked::<u32>(&e1_work_prefix, n_e1 as usize)?;
4456        let grid = if total_work == 0 {
4457            1
4458        } else {
4459            total_work.div_ceil(block_work_unit)
4460        };
4461        let block_counts = self.memory().alloc::<u32>(grid as usize)?;
4462        let block_offsets = self.memory().alloc::<u32>(grid as usize)?;
4463
4464        Ok(WcojCycle4HgWorkPlanU64 {
4465            e1_work_prefix,
4466            e2_work_prefix,
4467            e1_e2_start,
4468            e1_e2_end,
4469            block_counts,
4470            block_offsets,
4471            total_work,
4472            block_work_unit,
4473            row_count: n_e1,
4474        })
4475    }
4476
4477    pub fn wcoj_4cycle_hg_u64_recorded(
4478        &self,
4479        e1: &CudaBuffer,
4480        e2: &CudaBuffer,
4481        e3: &CudaBuffer,
4482        e4: &CudaBuffer,
4483        block_work_unit: u32,
4484        launch_stream: StreamId,
4485    ) -> Result<CudaBuffer> {
4486        let ctx = "wcoj_4cycle_hg_u64_recorded";
4487        validate_binary_u64(ctx, "e1", e1)?;
4488        validate_binary_u64(ctx, "e2", e2)?;
4489        validate_binary_u64(ctx, "e3", e3)?;
4490        validate_binary_u64(ctx, "e4", e4)?;
4491        let plan = self.wcoj_4cycle_hg_work_plan_u64_recorded(
4492            e1,
4493            e2,
4494            e3,
4495            e4,
4496            block_work_unit,
4497            launch_stream,
4498        )?;
4499        let out_schema = Schema::new(vec![
4500            ("col0".to_string(), ScalarType::U64),
4501            ("col1".to_string(), ScalarType::U64),
4502            ("col2".to_string(), ScalarType::U64),
4503            ("col3".to_string(), ScalarType::U64),
4504        ]);
4505        if plan.total_work == 0 {
4506            return self.create_empty_buffer(out_schema);
4507        }
4508
4509        let grid = plan.total_work.div_ceil(plan.block_work_unit);
4510        let bytes_count = (grid as usize)
4511            .checked_mul(std::mem::size_of::<u32>())
4512            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: count byte size overflow")))?;
4513        let mut local_counts = None;
4514        let mut local_offsets = None;
4515        if grid > 1024 {
4516            local_counts = Some(self.memory().alloc::<u32>(grid as usize)?);
4517            local_offsets = Some(self.memory().alloc::<u32>(grid as usize)?);
4518        }
4519        let total_rows_device = self.memory().alloc::<u32>(1)?;
4520
4521        let runtime = self.memory().runtime().ok_or_else(|| {
4522            XlogError::Kernel(format!("{ctx} requires a runtime-backed GpuMemoryManager"))
4523        })?;
4524        let cu_stream = runtime
4525            .stream_pool()
4526            .resolve(launch_stream)
4527            .ok_or_else(|| {
4528                XlogError::Kernel(format!(
4529                    "{ctx}: launch_stream StreamId({}) does not resolve",
4530                    launch_stream.0
4531                ))
4532            })?;
4533
4534        let e1_col0 = &metadata_column_u64(e1, 0)?;
4535        let e1_col1 = &metadata_column_u64(e1, 1)?;
4536        let e2_col1 = &metadata_column_u64(e2, 1)?;
4537        let e3_col0 = &metadata_column_u64(e3, 0)?;
4538        let e3_col1 = &metadata_column_u64(e3, 1)?;
4539        let e4_col0 = &metadata_column_u64(e4, 0)?;
4540        let e4_col1 = &metadata_column_u64(e4, 1)?;
4541        let n_e3 = self.metadata_logical_rows(e3)?;
4542        let n_e4 = self.metadata_logical_rows(e4)?;
4543
4544        let count_u32 = if grid <= 1024 {
4545            &plan.block_counts
4546        } else {
4547            local_counts
4548                .as_ref()
4549                .expect("local HG counts allocated when grid exceeds single-block scan")
4550        };
4551        let mut scan_scratch =
4552            self.multiblock_scan_u32_scratch_for_len(if grid > 1024 { grid } else { 0 })?;
4553        let mut rec_hg = LaunchRecorder::new_strict(launch_stream);
4554        rec_hg.read(e1.num_rows_device());
4555        rec_hg.read(e2.num_rows_device());
4556        rec_hg.read(e3.num_rows_device());
4557        rec_hg.read(e4.num_rows_device());
4558        rec_hg.read_column(e1.column(0).expect("e1.col0"));
4559        rec_hg.read_column(e1.column(1).expect("e1.col1"));
4560        rec_hg.read_column(e2.column(1).expect("e2.col1"));
4561        rec_hg.read_column(e3.column(0).expect("e3.col0"));
4562        rec_hg.read_column(e3.column(1).expect("e3.col1"));
4563        rec_hg.read_column(e4.column(0).expect("e4.col0"));
4564        rec_hg.read_column(e4.column(1).expect("e4.col1"));
4565        rec_hg.read(&plan.e1_work_prefix);
4566        rec_hg.read(&plan.e2_work_prefix);
4567        rec_hg.read(&plan.e1_e2_start);
4568        rec_hg.read(&plan.e1_e2_end);
4569        rec_hg.read_write(count_u32);
4570        if grid <= 1024 {
4571            rec_hg.read_write(&plan.block_offsets);
4572        } else {
4573            rec_hg.read_write(
4574                local_offsets
4575                    .as_ref()
4576                    .expect("local HG offsets allocated when grid exceeds single-block scan"),
4577            );
4578        }
4579        rec_hg.write(&total_rows_device);
4580        for level in scan_scratch.levels() {
4581            rec_hg.read_write(level);
4582        }
4583        rec_hg
4584            .preflight(runtime)
4585            .map_err(|e| XlogError::Kernel(format!("{ctx}: HG preflight failed: {e}")))?;
4586        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
4587            {
4588                let kernel = self
4589                    .device()
4590                    .inner()
4591                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_4CYCLE_COUNT_HG_U64)
4592                    .ok_or_else(|| {
4593                        XlogError::Kernel("wcoj_4cycle_count_hg_u64 kernel not found".to_string())
4594                    })?;
4595                let mut params: Vec<*mut c_void> = vec![
4596                    e1_col0.as_kernel_param(),
4597                    e1_col1.as_kernel_param(),
4598                    plan.row_count.as_kernel_param(),
4599                    e2_col1.as_kernel_param(),
4600                    e3_col0.as_kernel_param(),
4601                    e3_col1.as_kernel_param(),
4602                    n_e3.as_kernel_param(),
4603                    e4_col0.as_kernel_param(),
4604                    e4_col1.as_kernel_param(),
4605                    n_e4.as_kernel_param(),
4606                    (&plan.e1_work_prefix).as_kernel_param(),
4607                    (&plan.e2_work_prefix).as_kernel_param(),
4608                    (&plan.e1_e2_start).as_kernel_param(),
4609                    (&plan.e1_e2_end).as_kernel_param(),
4610                    plan.total_work.as_kernel_param(),
4611                    plan.block_work_unit.as_kernel_param(),
4612                    count_u32.as_kernel_param(),
4613                ];
4614                unsafe {
4615                    kernel
4616                        .clone()
4617                        .launch_in(
4618                            stream,
4619                            LaunchConfig {
4620                                grid_dim: (grid, 1, 1),
4621                                block_dim: (BLOCK_SIZE, 1, 1),
4622                                shared_mem_bytes: 0,
4623                            },
4624                            &mut params,
4625                        )
4626                        .map_err(|e| {
4627                            XlogError::Kernel(format!("{ctx}: count launch failed: {e}"))
4628                        })?;
4629                }
4630            }
4631            if grid <= 1024 {
4632                let kernel = self
4633                    .device()
4634                    .inner()
4635                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_SCAN_HG_BLOCK_COUNTS_U32)
4636                    .ok_or_else(|| {
4637                        XlogError::Kernel(
4638                            "wcoj_scan_hg_block_counts_u32 kernel not found".to_string(),
4639                        )
4640                    })?;
4641                let mut params: Vec<*mut c_void> = vec![
4642                    count_u32.as_kernel_param(),
4643                    grid.as_kernel_param(),
4644                    (&plan.block_offsets).as_kernel_param(),
4645                    (&total_rows_device).as_kernel_param(),
4646                ];
4647                unsafe {
4648                    kernel
4649                        .clone()
4650                        .launch_in(
4651                            stream,
4652                            LaunchConfig {
4653                                grid_dim: (1, 1, 1),
4654                                block_dim: (1024, 1, 1),
4655                                shared_mem_bytes: 0,
4656                            },
4657                            &mut params,
4658                        )
4659                        .map_err(|e| XlogError::Kernel(format!("{ctx}: scan failed: {e}")))?;
4660                }
4661            } else {
4662                let offsets_mut = local_offsets
4663                    .as_mut()
4664                    .expect("local HG offsets allocated when grid exceeds single-block scan");
4665                unsafe {
4666                    let res = sys::cuMemcpyDtoDAsync_v2(
4667                        *offsets_mut.device_ptr(),
4668                        *count_u32.device_ptr(),
4669                        bytes_count,
4670                        stream.stream().cu_stream(),
4671                    );
4672                    if res != sys::cudaError_enum::CUDA_SUCCESS {
4673                        return Err(XlogError::Kernel(format!(
4674                            "{ctx}: DtoD count to offsets failed: {res:?}"
4675                        )));
4676                    }
4677                }
4678                self.multiblock_scan_u32_inplace_on_stream(
4679                    offsets_mut,
4680                    grid,
4681                    stream,
4682                    &mut scan_scratch,
4683                )?;
4684                let total_kernel = self
4685                    .device()
4686                    .inner()
4687                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_COMPUTE_TOTAL)
4688                    .ok_or_else(|| {
4689                        XlogError::Kernel("wcoj_compute_total kernel not found".to_string())
4690                    })?;
4691                let mut params: Vec<*mut c_void> = vec![
4692                    count_u32.as_kernel_param(),
4693                    (&*offsets_mut).as_kernel_param(),
4694                    grid.as_kernel_param(),
4695                    (&total_rows_device).as_kernel_param(),
4696                ];
4697                unsafe {
4698                    total_kernel
4699                        .clone()
4700                        .launch_in(
4701                            stream,
4702                            LaunchConfig {
4703                                grid_dim: (1, 1, 1),
4704                                block_dim: (1, 1, 1),
4705                                shared_mem_bytes: 0,
4706                            },
4707                            &mut params,
4708                        )
4709                        .map_err(|e| XlogError::Kernel(format!("{ctx}: total failed: {e}")))?;
4710                }
4711            }
4712            Ok(())
4713        };
4714        // SAFETY: preflight retained the accessed buffers and bound this stream.
4715        let rec_hg = unsafe { rec_hg.enqueue_prepared_with(&cu_stream, enqueue) }
4716            .map_err(|error| error.into_xlog_error())?;
4717
4718        rec_hg
4719            .commit()
4720            .map_err(|e| XlogError::Kernel(format!("{ctx}: count commit failed: {e}")))?;
4721        cu_stream
4722            .synchronize()
4723            .map_err(|e| XlogError::Kernel(format!("{ctx}: count stream sync failed: {e}")))?;
4724        let total_rows = self
4725            .dtoh_scalar_untracked::<u32>(&total_rows_device, 0)
4726            .map_err(|e| XlogError::Kernel(format!("{ctx}: read total rows failed: {e}")))?;
4727        if total_rows == 0 {
4728            return self.create_empty_buffer(out_schema);
4729        }
4730
4731        let bytes_per_col = (total_rows as usize)
4732            .checked_mul(std::mem::size_of::<u64>())
4733            .ok_or_else(|| XlogError::Kernel(format!("{ctx}: output byte size overflow")))?;
4734        let mut out_w = self.memory().alloc::<u8>(bytes_per_col)?;
4735        let mut out_x = self.memory().alloc::<u8>(bytes_per_col)?;
4736        let mut out_y = self.memory().alloc::<u8>(bytes_per_col)?;
4737        let mut out_z = self.memory().alloc::<u8>(bytes_per_col)?;
4738        let materialize_offsets = if grid <= 1024 {
4739            &plan.block_offsets
4740        } else {
4741            local_offsets
4742                .as_ref()
4743                .expect("local HG offsets allocated when grid exceeds single-block scan")
4744        };
4745        let mut rec_mat = LaunchRecorder::new_strict(launch_stream);
4746        rec_mat.read(materialize_offsets);
4747        rec_mat.read(e1.num_rows_device());
4748        rec_mat.read(e2.num_rows_device());
4749        rec_mat.read(e3.num_rows_device());
4750        rec_mat.read(e4.num_rows_device());
4751        rec_mat.read_column(e1.column(0).expect("e1.col0"));
4752        rec_mat.read_column(e1.column(1).expect("e1.col1"));
4753        rec_mat.read_column(e2.column(1).expect("e2.col1"));
4754        rec_mat.read_column(e3.column(0).expect("e3.col0"));
4755        rec_mat.read_column(e3.column(1).expect("e3.col1"));
4756        rec_mat.read_column(e4.column(0).expect("e4.col0"));
4757        rec_mat.read_column(e4.column(1).expect("e4.col1"));
4758        rec_mat.read(&plan.e1_work_prefix);
4759        rec_mat.read(&plan.e2_work_prefix);
4760        rec_mat.read(&plan.e1_e2_start);
4761        rec_mat.read(&plan.e1_e2_end);
4762        rec_mat.write(&out_w);
4763        rec_mat.write(&out_x);
4764        rec_mat.write(&out_y);
4765        rec_mat.write(&out_z);
4766        rec_mat
4767            .preflight(runtime)
4768            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize preflight failed: {e}")))?;
4769        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
4770            {
4771                let kernel = self
4772                    .device()
4773                    .inner()
4774                    .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_4CYCLE_MATERIALIZE_HG_U64)
4775                    .ok_or_else(|| {
4776                        XlogError::Kernel(
4777                            "wcoj_4cycle_materialize_hg_u64 kernel not found".to_string(),
4778                        )
4779                    })?;
4780                let out_w_u64 = &mut reinterpret_u8_as_u64(&mut out_w)?;
4781                let out_x_u64 = &mut reinterpret_u8_as_u64(&mut out_x)?;
4782                let out_y_u64 = &mut reinterpret_u8_as_u64(&mut out_y)?;
4783                let out_z_u64 = &mut reinterpret_u8_as_u64(&mut out_z)?;
4784                let mut params: Vec<*mut c_void> = vec![
4785                    e1_col0.as_kernel_param(),
4786                    e1_col1.as_kernel_param(),
4787                    plan.row_count.as_kernel_param(),
4788                    e2_col1.as_kernel_param(),
4789                    e3_col0.as_kernel_param(),
4790                    e3_col1.as_kernel_param(),
4791                    n_e3.as_kernel_param(),
4792                    e4_col0.as_kernel_param(),
4793                    e4_col1.as_kernel_param(),
4794                    n_e4.as_kernel_param(),
4795                    (&plan.e1_work_prefix).as_kernel_param(),
4796                    (&plan.e2_work_prefix).as_kernel_param(),
4797                    (&plan.e1_e2_start).as_kernel_param(),
4798                    (&plan.e1_e2_end).as_kernel_param(),
4799                    plan.total_work.as_kernel_param(),
4800                    plan.block_work_unit.as_kernel_param(),
4801                    materialize_offsets.as_kernel_param(),
4802                    total_rows.as_kernel_param(),
4803                    out_w_u64.as_kernel_param(),
4804                    out_x_u64.as_kernel_param(),
4805                    out_y_u64.as_kernel_param(),
4806                    out_z_u64.as_kernel_param(),
4807                ];
4808                unsafe {
4809                    kernel
4810                        .clone()
4811                        .launch_in(
4812                            stream,
4813                            LaunchConfig {
4814                                grid_dim: (grid, 1, 1),
4815                                block_dim: (BLOCK_SIZE, 1, 1),
4816                                shared_mem_bytes: 0,
4817                            },
4818                            &mut params,
4819                        )
4820                        .map_err(|e| {
4821                            XlogError::Kernel(format!("{ctx}: materialize launch failed: {e}"))
4822                        })?;
4823                }
4824            }
4825            Ok(())
4826        };
4827        // SAFETY: preflight retained the accessed buffers and bound this stream.
4828        let rec_mat = unsafe { rec_mat.enqueue_prepared_with(&cu_stream, enqueue) }
4829            .map_err(|error| error.into_xlog_error())?;
4830
4831        rec_mat
4832            .commit()
4833            .map_err(|e| XlogError::Kernel(format!("{ctx}: materialize commit failed: {e}")))?;
4834        cu_stream.synchronize().map_err(|e| {
4835            XlogError::Kernel(format!("{ctx}: materialize stream sync failed: {e}"))
4836        })?;
4837
4838        Ok(CudaBuffer::from_columns_with_host_count(
4839            vec![out_w.into(), out_x.into(), out_y.into(), out_z.into()],
4840            total_rows as u64,
4841            total_rows_device,
4842            out_schema,
4843            total_rows,
4844        ))
4845    }
4846
4847    fn validate_metadata_column(
4848        &self,
4849        input: &CudaBuffer,
4850        key_col_idx: usize,
4851        width: MetadataWidth,
4852    ) -> Result<()> {
4853        if key_col_idx >= input.arity() {
4854            return Err(XlogError::Kernel(format!(
4855                "wcoj_build_metadata: key column {} out of range for arity {}",
4856                key_col_idx,
4857                input.arity()
4858            )));
4859        }
4860        let ty = input.schema().column_type(key_col_idx).ok_or_else(|| {
4861            XlogError::Kernel(format!(
4862                "wcoj_build_metadata: column {} type missing",
4863                key_col_idx
4864            ))
4865        })?;
4866        match (width, ty) {
4867            (MetadataWidth::U32, ScalarType::U32 | ScalarType::Symbol)
4868            | (MetadataWidth::U64, ScalarType::U64) => Ok(()),
4869            (MetadataWidth::U32, other) => Err(XlogError::Kernel(format!(
4870                "wcoj_build_metadata_u32_recorded: column {} must be U32 or Symbol, got {:?}",
4871                key_col_idx, other
4872            ))),
4873            (MetadataWidth::U64, other) => Err(XlogError::Kernel(format!(
4874                "wcoj_build_metadata_u64_recorded: column {} must be U64, got {:?}",
4875                key_col_idx, other
4876            ))),
4877        }
4878    }
4879
4880    fn build_metadata_u32_from_column(
4881        &self,
4882        input: &CudaBuffer,
4883        key_col_idx: usize,
4884        keys: &DeviceMemoryView<u32>,
4885        launch_stream: StreamId,
4886    ) -> Result<WcojRelationMetadata<u32>> {
4887        let n = self.metadata_logical_rows(input)?;
4888        if n == 0 {
4889            return Ok(WcojRelationMetadata {
4890                unique_keys: self.memory().alloc::<u32>(0)?,
4891                fan_out: self.memory().alloc::<u32>(0)?,
4892                prefix_sum: self.memory().alloc::<u32>(0)?,
4893                per_candidate_root: BTreeMap::new(),
4894                total: 0,
4895                key_count: 0,
4896                row_count: 0,
4897            });
4898        }
4899        let mut boundary_mask = self.memory().alloc::<u32>(n as usize)?;
4900        let mut boundary_prefix = self.memory().alloc::<u32>(n as usize)?;
4901        self.mark_metadata_boundaries_u32(
4902            input,
4903            key_col_idx,
4904            keys,
4905            n,
4906            &mut boundary_mask,
4907            &mut boundary_prefix,
4908            launch_stream,
4909        )?;
4910        let key_count = self.metadata_scanned_count(&boundary_mask, &boundary_prefix, n)?;
4911
4912        let mut unique_keys = self.memory().alloc::<u32>(key_count as usize)?;
4913        let mut fan_out = self.memory().alloc::<u32>(key_count as usize)?;
4914        let mut prefix_sum = self.memory().alloc::<u32>(key_count as usize)?;
4915        self.scatter_metadata_u32(
4916            input,
4917            key_col_idx,
4918            keys,
4919            n,
4920            &boundary_mask,
4921            &boundary_prefix,
4922            &mut unique_keys,
4923            &mut fan_out,
4924            &mut prefix_sum,
4925            launch_stream,
4926        )?;
4927
4928        Ok(WcojRelationMetadata {
4929            unique_keys,
4930            fan_out,
4931            prefix_sum,
4932            per_candidate_root: BTreeMap::new(),
4933            total: u64::from(n),
4934            key_count,
4935            row_count: n,
4936        })
4937    }
4938
4939    fn build_metadata_u64_from_column(
4940        &self,
4941        input: &CudaBuffer,
4942        key_col_idx: usize,
4943        keys: &DeviceMemoryView<u64>,
4944        launch_stream: StreamId,
4945    ) -> Result<WcojRelationMetadata<u64>> {
4946        let n = self.metadata_logical_rows(input)?;
4947        if n == 0 {
4948            return Ok(WcojRelationMetadata {
4949                unique_keys: self.memory().alloc::<u64>(0)?,
4950                fan_out: self.memory().alloc::<u32>(0)?,
4951                prefix_sum: self.memory().alloc::<u32>(0)?,
4952                per_candidate_root: BTreeMap::new(),
4953                total: 0,
4954                key_count: 0,
4955                row_count: 0,
4956            });
4957        }
4958        let mut boundary_mask = self.memory().alloc::<u32>(n as usize)?;
4959        let mut boundary_prefix = self.memory().alloc::<u32>(n as usize)?;
4960        self.mark_metadata_boundaries_u64(
4961            input,
4962            key_col_idx,
4963            keys,
4964            n,
4965            &mut boundary_mask,
4966            &mut boundary_prefix,
4967            launch_stream,
4968        )?;
4969        let key_count = self.metadata_scanned_count(&boundary_mask, &boundary_prefix, n)?;
4970
4971        let mut unique_keys = self.memory().alloc::<u64>(key_count as usize)?;
4972        let mut fan_out = self.memory().alloc::<u32>(key_count as usize)?;
4973        let mut prefix_sum = self.memory().alloc::<u32>(key_count as usize)?;
4974        self.scatter_metadata_u64(
4975            input,
4976            key_col_idx,
4977            keys,
4978            n,
4979            &boundary_mask,
4980            &boundary_prefix,
4981            &mut unique_keys,
4982            &mut fan_out,
4983            &mut prefix_sum,
4984            launch_stream,
4985        )?;
4986
4987        Ok(WcojRelationMetadata {
4988            unique_keys,
4989            fan_out,
4990            prefix_sum,
4991            per_candidate_root: BTreeMap::new(),
4992            total: u64::from(n),
4993            key_count,
4994            row_count: n,
4995        })
4996    }
4997
4998    #[expect(
4999        clippy::too_many_arguments,
5000        reason = "metadata boundary marking mirrors the fixed u32 kernel ABI and its scratch/output owners"
5001    )]
5002    fn mark_metadata_boundaries_u32(
5003        &self,
5004        input: &CudaBuffer,
5005        key_col_idx: usize,
5006        keys: &DeviceMemoryView<u32>,
5007        n: u32,
5008        boundary_mask: &mut TrackedCudaSlice<u32>,
5009        boundary_prefix: &mut TrackedCudaSlice<u32>,
5010        launch_stream: StreamId,
5011    ) -> Result<()> {
5012        let runtime = self.memory().runtime().ok_or_else(|| {
5013            XlogError::Kernel(
5014                "wcoj_build_metadata_u32_recorded requires a runtime-backed GpuMemoryManager"
5015                    .to_string(),
5016            )
5017        })?;
5018        let cu_stream = runtime
5019            .stream_pool()
5020            .resolve(launch_stream)
5021            .ok_or_else(|| {
5022                XlogError::Kernel(format!(
5023                    "wcoj_build_metadata_u32_recorded: launch_stream StreamId({}) does not resolve",
5024                    launch_stream.0
5025                ))
5026            })?;
5027        let mut scan_scratch = self.multiblock_scan_u32_scratch_for_len(n)?;
5028        let mut rec = LaunchRecorder::new_strict(launch_stream);
5029        rec.read(input.num_rows_device());
5030        rec.read_column(input.column(key_col_idx).expect("metadata key column"));
5031        rec.write(boundary_mask);
5032        rec.write(boundary_prefix);
5033        for level in scan_scratch.levels() {
5034            rec.read_write(level);
5035        }
5036        rec.preflight(runtime).map_err(|e| {
5037            XlogError::Kernel(format!(
5038                "wcoj_build_metadata_u32_recorded: mark preflight failed: {e}"
5039            ))
5040        })?;
5041        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
5042            let kernel = self
5043                .device()
5044                .inner()
5045                .get_func(
5046                    WCOJ_MODULE,
5047                    wcoj_kernels::WCOJ_BUILD_METADATA_MARK_BOUNDARIES_U32,
5048                )
5049                .ok_or_else(|| {
5050                    XlogError::Kernel(
5051                        "wcoj_build_metadata_mark_boundaries_u32 kernel not found".to_string(),
5052                    )
5053                })?;
5054            let grid = n.div_ceil(BLOCK_SIZE);
5055            unsafe {
5056                kernel
5057                    .clone()
5058                    .launch_in(
5059                        stream,
5060                        LaunchConfig {
5061                            grid_dim: (grid, 1, 1),
5062                            block_dim: (BLOCK_SIZE, 1, 1),
5063                            shared_mem_bytes: 0,
5064                        },
5065                        (keys, n, &mut *boundary_mask, &mut *boundary_prefix),
5066                    )
5067                    .map_err(|e| {
5068                        XlogError::Kernel(format!(
5069                            "wcoj_build_metadata_mark_boundaries_u32 launch failed: {e}"
5070                        ))
5071                    })?;
5072            }
5073            self.multiblock_scan_u32_inplace_on_stream(
5074                boundary_prefix,
5075                n,
5076                stream,
5077                &mut scan_scratch,
5078            )?;
5079            Ok(())
5080        };
5081        // SAFETY: preflight retained the accessed buffers and bound this stream.
5082        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
5083            .map_err(|error| error.into_xlog_error())?;
5084
5085        rec.commit().map_err(|e| {
5086            XlogError::Kernel(format!(
5087                "wcoj_build_metadata_u32_recorded: mark commit failed: {e}"
5088            ))
5089        })?;
5090        cu_stream.synchronize().map_err(|e| {
5091            XlogError::Kernel(format!(
5092                "wcoj_build_metadata_u32_recorded: mark stream sync failed: {e}"
5093            ))
5094        })?;
5095        Ok(())
5096    }
5097
5098    #[expect(
5099        clippy::too_many_arguments,
5100        reason = "metadata boundary marking mirrors the fixed u64 kernel ABI and its scratch/output owners"
5101    )]
5102    fn mark_metadata_boundaries_u64(
5103        &self,
5104        input: &CudaBuffer,
5105        key_col_idx: usize,
5106        keys: &DeviceMemoryView<u64>,
5107        n: u32,
5108        boundary_mask: &mut TrackedCudaSlice<u32>,
5109        boundary_prefix: &mut TrackedCudaSlice<u32>,
5110        launch_stream: StreamId,
5111    ) -> Result<()> {
5112        let runtime = self.memory().runtime().ok_or_else(|| {
5113            XlogError::Kernel(
5114                "wcoj_build_metadata_u64_recorded requires a runtime-backed GpuMemoryManager"
5115                    .to_string(),
5116            )
5117        })?;
5118        let cu_stream = runtime
5119            .stream_pool()
5120            .resolve(launch_stream)
5121            .ok_or_else(|| {
5122                XlogError::Kernel(format!(
5123                    "wcoj_build_metadata_u64_recorded: launch_stream StreamId({}) does not resolve",
5124                    launch_stream.0
5125                ))
5126            })?;
5127        let mut scan_scratch = self.multiblock_scan_u32_scratch_for_len(n)?;
5128        let mut rec = LaunchRecorder::new_strict(launch_stream);
5129        rec.read(input.num_rows_device());
5130        rec.read_column(input.column(key_col_idx).expect("metadata key column"));
5131        rec.write(boundary_mask);
5132        rec.write(boundary_prefix);
5133        for level in scan_scratch.levels() {
5134            rec.read_write(level);
5135        }
5136        rec.preflight(runtime).map_err(|e| {
5137            XlogError::Kernel(format!(
5138                "wcoj_build_metadata_u64_recorded: mark preflight failed: {e}"
5139            ))
5140        })?;
5141        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
5142            let kernel = self
5143                .device()
5144                .inner()
5145                .get_func(
5146                    WCOJ_MODULE,
5147                    wcoj_kernels::WCOJ_BUILD_METADATA_MARK_BOUNDARIES_U64,
5148                )
5149                .ok_or_else(|| {
5150                    XlogError::Kernel(
5151                        "wcoj_build_metadata_mark_boundaries_u64 kernel not found".to_string(),
5152                    )
5153                })?;
5154            let grid = n.div_ceil(BLOCK_SIZE);
5155            unsafe {
5156                kernel
5157                    .clone()
5158                    .launch_in(
5159                        stream,
5160                        LaunchConfig {
5161                            grid_dim: (grid, 1, 1),
5162                            block_dim: (BLOCK_SIZE, 1, 1),
5163                            shared_mem_bytes: 0,
5164                        },
5165                        (keys, n, &mut *boundary_mask, &mut *boundary_prefix),
5166                    )
5167                    .map_err(|e| {
5168                        XlogError::Kernel(format!(
5169                            "wcoj_build_metadata_mark_boundaries_u64 launch failed: {e}"
5170                        ))
5171                    })?;
5172            }
5173            self.multiblock_scan_u32_inplace_on_stream(
5174                boundary_prefix,
5175                n,
5176                stream,
5177                &mut scan_scratch,
5178            )?;
5179            Ok(())
5180        };
5181        // SAFETY: preflight retained the accessed buffers and bound this stream.
5182        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
5183            .map_err(|error| error.into_xlog_error())?;
5184
5185        rec.commit().map_err(|e| {
5186            XlogError::Kernel(format!(
5187                "wcoj_build_metadata_u64_recorded: mark commit failed: {e}"
5188            ))
5189        })?;
5190        cu_stream.synchronize().map_err(|e| {
5191            XlogError::Kernel(format!(
5192                "wcoj_build_metadata_u64_recorded: mark stream sync failed: {e}"
5193            ))
5194        })?;
5195        Ok(())
5196    }
5197
5198    #[expect(
5199        clippy::too_many_arguments,
5200        reason = "u32 metadata scattering mirrors the fixed kernel ABI and all device output owners"
5201    )]
5202    fn scatter_metadata_u32(
5203        &self,
5204        input: &CudaBuffer,
5205        key_col_idx: usize,
5206        keys: &DeviceMemoryView<u32>,
5207        n: u32,
5208        boundary_mask: &TrackedCudaSlice<u32>,
5209        boundary_prefix: &TrackedCudaSlice<u32>,
5210        unique_keys: &mut TrackedCudaSlice<u32>,
5211        fan_out: &mut TrackedCudaSlice<u32>,
5212        prefix_sum: &mut TrackedCudaSlice<u32>,
5213        launch_stream: StreamId,
5214    ) -> Result<()> {
5215        let runtime = self.memory().runtime().ok_or_else(|| {
5216            XlogError::Kernel(
5217                "wcoj_build_metadata_u32_recorded requires a runtime-backed GpuMemoryManager"
5218                    .to_string(),
5219            )
5220        })?;
5221        let cu_stream = runtime
5222            .stream_pool()
5223            .resolve(launch_stream)
5224            .ok_or_else(|| {
5225                XlogError::Kernel(format!(
5226                    "wcoj_build_metadata_u32_recorded: launch_stream StreamId({}) does not resolve",
5227                    launch_stream.0
5228                ))
5229            })?;
5230        let mut rec = LaunchRecorder::new_strict(launch_stream);
5231        rec.read(input.num_rows_device());
5232        rec.read_column(input.column(key_col_idx).expect("metadata key column"));
5233        rec.read(boundary_mask);
5234        rec.read(boundary_prefix);
5235        rec.write(unique_keys);
5236        rec.write(fan_out);
5237        rec.write(prefix_sum);
5238        rec.preflight(runtime).map_err(|e| {
5239            XlogError::Kernel(format!(
5240                "wcoj_build_metadata_u32_recorded: scatter preflight failed: {e}"
5241            ))
5242        })?;
5243        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
5244            let kernel = self
5245                .device()
5246                .inner()
5247                .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_BUILD_METADATA_SCATTER_U32)
5248                .ok_or_else(|| {
5249                    XlogError::Kernel(
5250                        "wcoj_build_metadata_scatter_u32 kernel not found".to_string(),
5251                    )
5252                })?;
5253            let grid = n.div_ceil(BLOCK_SIZE);
5254            unsafe {
5255                kernel
5256                    .clone()
5257                    .launch_in(
5258                        stream,
5259                        LaunchConfig {
5260                            grid_dim: (grid, 1, 1),
5261                            block_dim: (BLOCK_SIZE, 1, 1),
5262                            shared_mem_bytes: 0,
5263                        },
5264                        (
5265                            keys,
5266                            n,
5267                            boundary_mask,
5268                            boundary_prefix,
5269                            &mut *unique_keys,
5270                            &mut *fan_out,
5271                            &mut *prefix_sum,
5272                        ),
5273                    )
5274                    .map_err(|e| {
5275                        XlogError::Kernel(format!(
5276                            "wcoj_build_metadata_scatter_u32 launch failed: {e}"
5277                        ))
5278                    })?;
5279            }
5280            Ok(())
5281        };
5282        // SAFETY: preflight retained the accessed buffers and bound this stream.
5283        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
5284            .map_err(|error| error.into_xlog_error())?;
5285
5286        rec.commit().map_err(|e| {
5287            XlogError::Kernel(format!(
5288                "wcoj_build_metadata_u32_recorded: scatter commit failed: {e}"
5289            ))
5290        })?;
5291        cu_stream.synchronize().map_err(|e| {
5292            XlogError::Kernel(format!(
5293                "wcoj_build_metadata_u32_recorded: scatter stream sync failed: {e}"
5294            ))
5295        })?;
5296        Ok(())
5297    }
5298
5299    #[expect(
5300        clippy::too_many_arguments,
5301        reason = "u64 metadata scattering mirrors the fixed kernel ABI and all device output owners"
5302    )]
5303    fn scatter_metadata_u64(
5304        &self,
5305        input: &CudaBuffer,
5306        key_col_idx: usize,
5307        keys: &DeviceMemoryView<u64>,
5308        n: u32,
5309        boundary_mask: &TrackedCudaSlice<u32>,
5310        boundary_prefix: &TrackedCudaSlice<u32>,
5311        unique_keys: &mut TrackedCudaSlice<u64>,
5312        fan_out: &mut TrackedCudaSlice<u32>,
5313        prefix_sum: &mut TrackedCudaSlice<u32>,
5314        launch_stream: StreamId,
5315    ) -> Result<()> {
5316        let runtime = self.memory().runtime().ok_or_else(|| {
5317            XlogError::Kernel(
5318                "wcoj_build_metadata_u64_recorded requires a runtime-backed GpuMemoryManager"
5319                    .to_string(),
5320            )
5321        })?;
5322        let cu_stream = runtime
5323            .stream_pool()
5324            .resolve(launch_stream)
5325            .ok_or_else(|| {
5326                XlogError::Kernel(format!(
5327                    "wcoj_build_metadata_u64_recorded: launch_stream StreamId({}) does not resolve",
5328                    launch_stream.0
5329                ))
5330            })?;
5331        let mut rec = LaunchRecorder::new_strict(launch_stream);
5332        rec.read(input.num_rows_device());
5333        rec.read_column(input.column(key_col_idx).expect("metadata key column"));
5334        rec.read(boundary_mask);
5335        rec.read(boundary_prefix);
5336        rec.write(unique_keys);
5337        rec.write(fan_out);
5338        rec.write(prefix_sum);
5339        rec.preflight(runtime).map_err(|e| {
5340            XlogError::Kernel(format!(
5341                "wcoj_build_metadata_u64_recorded: scatter preflight failed: {e}"
5342            ))
5343        })?;
5344        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
5345            let kernel = self
5346                .device()
5347                .inner()
5348                .get_func(WCOJ_MODULE, wcoj_kernels::WCOJ_BUILD_METADATA_SCATTER_U64)
5349                .ok_or_else(|| {
5350                    XlogError::Kernel(
5351                        "wcoj_build_metadata_scatter_u64 kernel not found".to_string(),
5352                    )
5353                })?;
5354            let grid = n.div_ceil(BLOCK_SIZE);
5355            unsafe {
5356                kernel
5357                    .clone()
5358                    .launch_in(
5359                        stream,
5360                        LaunchConfig {
5361                            grid_dim: (grid, 1, 1),
5362                            block_dim: (BLOCK_SIZE, 1, 1),
5363                            shared_mem_bytes: 0,
5364                        },
5365                        (
5366                            keys,
5367                            n,
5368                            boundary_mask,
5369                            boundary_prefix,
5370                            &mut *unique_keys,
5371                            &mut *fan_out,
5372                            &mut *prefix_sum,
5373                        ),
5374                    )
5375                    .map_err(|e| {
5376                        XlogError::Kernel(format!(
5377                            "wcoj_build_metadata_scatter_u64 launch failed: {e}"
5378                        ))
5379                    })?;
5380            }
5381            Ok(())
5382        };
5383        // SAFETY: preflight retained the accessed buffers and bound this stream.
5384        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
5385            .map_err(|error| error.into_xlog_error())?;
5386
5387        rec.commit().map_err(|e| {
5388            XlogError::Kernel(format!(
5389                "wcoj_build_metadata_u64_recorded: scatter commit failed: {e}"
5390            ))
5391        })?;
5392        cu_stream.synchronize().map_err(|e| {
5393            XlogError::Kernel(format!(
5394                "wcoj_build_metadata_u64_recorded: scatter stream sync failed: {e}"
5395            ))
5396        })?;
5397        Ok(())
5398    }
5399
5400    fn metadata_scanned_count(
5401        &self,
5402        boundary_mask: &TrackedCudaSlice<u32>,
5403        boundary_prefix: &TrackedCudaSlice<u32>,
5404        n: u32,
5405    ) -> Result<u32> {
5406        let last = n - 1;
5407        let prefix_last = self.dtoh_scalar_untracked::<u32>(boundary_prefix, last as usize)?;
5408        let mask_last = self.dtoh_scalar_untracked::<u32>(boundary_mask, last as usize)?;
5409        Ok(prefix_last + mask_last)
5410    }
5411
5412    fn metadata_logical_rows(&self, input: &CudaBuffer) -> Result<u32> {
5413        if let Some(cached) = input.cached_row_count() {
5414            return Ok(cached);
5415        }
5416        self.dtoh_scalar_untracked::<u32>(input.num_rows_device(), 0)
5417    }
5418}
5419
5420#[derive(Clone, Copy)]
5421enum MetadataWidth {
5422    U32,
5423    U64,
5424}
5425
5426fn metadata_column_u32(input: &CudaBuffer, key_col_idx: usize) -> Result<DeviceMemoryView<u32>> {
5427    let col = input.column(key_col_idx).ok_or_else(|| {
5428        XlogError::Kernel(format!(
5429            "wcoj_build_metadata_u32_recorded: column {} not found",
5430            key_col_idx
5431        ))
5432    })?;
5433    // SAFETY: unsigned integers accept every bit pattern. The checked cast
5434    // preserves the storage owner and validates byte length and alignment.
5435    unsafe { col.device_view().cast() }
5436        .ok_or_else(|| XlogError::Kernel("invalid u32 device column layout".into()))
5437}
5438
5439fn metadata_column_u64(input: &CudaBuffer, key_col_idx: usize) -> Result<DeviceMemoryView<u64>> {
5440    let col = input.column(key_col_idx).ok_or_else(|| {
5441        XlogError::Kernel(format!(
5442            "wcoj_build_metadata_u64_recorded: column {} not found",
5443            key_col_idx
5444        ))
5445    })?;
5446    // SAFETY: unsigned integers accept every bit pattern. The checked cast
5447    // preserves the storage owner and validates byte length and alignment.
5448    unsafe { col.device_view().cast() }
5449        .ok_or_else(|| XlogError::Kernel("invalid u64 device column layout".into()))
5450}
5451
5452fn validate_binary_u32(ctx: &str, label: &str, input: &CudaBuffer) -> Result<()> {
5453    if input.arity() != 2 {
5454        return Err(XlogError::Kernel(format!(
5455            "{ctx}: {label} must be 2-column, got arity {}",
5456            input.arity()
5457        )));
5458    }
5459    for col_idx in 0..2 {
5460        let ty = input.schema().column_type(col_idx).ok_or_else(|| {
5461            XlogError::Kernel(format!("{ctx}: {label}.col{col_idx} type missing"))
5462        })?;
5463        if !matches!(ty, ScalarType::U32 | ScalarType::Symbol) {
5464            return Err(XlogError::Kernel(format!(
5465                "{ctx}: {label}.col{col_idx} must be U32 or Symbol, got {:?}",
5466                ty
5467            )));
5468        }
5469    }
5470    Ok(())
5471}
5472
5473fn validate_binary_u64(ctx: &str, label: &str, input: &CudaBuffer) -> Result<()> {
5474    if input.arity() != 2 {
5475        return Err(XlogError::Kernel(format!(
5476            "{ctx}: {label} must be 2-column, got arity {}",
5477            input.arity()
5478        )));
5479    }
5480    for col_idx in 0..2 {
5481        let ty = input.schema().column_type(col_idx).ok_or_else(|| {
5482            XlogError::Kernel(format!("{ctx}: {label}.col{col_idx} type missing"))
5483        })?;
5484        if !matches!(ty, ScalarType::U64) {
5485            return Err(XlogError::Kernel(format!(
5486                "{ctx}: {label}.col{col_idx} must be U64, got {:?}",
5487                ty
5488            )));
5489        }
5490    }
5491    Ok(())
5492}
5493
5494fn reinterpret_u8_as_u32(slice: &mut TrackedCudaSlice<u8>) -> Result<DeviceMemoryView<u32>> {
5495    // SAFETY: u32 accepts every bit pattern; the cast checks extent/alignment.
5496    unsafe { slice.device_view().cast() }
5497        .ok_or_else(|| XlogError::Kernel("invalid u32 scratch allocation layout".into()))
5498}
5499
5500fn reinterpret_u8_as_u64(slice: &mut TrackedCudaSlice<u8>) -> Result<DeviceMemoryView<u64>> {
5501    // SAFETY: u64 accepts every bit pattern; the cast checks extent/alignment.
5502    unsafe { slice.device_view().cast() }
5503        .ok_or_else(|| XlogError::Kernel("invalid u64 scratch allocation layout".into()))
5504}