Skip to main content

xlog_cuda/provider/
filter.rs

1//! Filter and compare operations on GPU buffers.
2//!
3//! Generic versions of `filter`, `compare_columns`, plus the shared helpers
4//! `compare_const_mask` and `compare_columns_mask` (moved from mod.rs).
5
6use std::marker::PhantomData;
7
8use crate::{DeviceRepr, DeviceSlice, KernelScalar, LaunchAsync, LaunchConfig};
9use xlog_core::{Result, ScalarType, XlogError};
10
11use super::{filter_kernels, scan_kernels, RawCudaView, FILTER_MODULE, SCAN_MODULE};
12use crate::device_runtime::StreamId;
13use crate::launch::LaunchRecorder;
14use crate::memory::{CudaColumn, TrackedCudaSlice};
15use crate::type_seam::GpuScalar;
16use crate::{CompareOp, CudaBuffer};
17
18impl super::CudaKernelProvider {
19    // ------------------------------------------------------------------
20    // Generic public API
21    // ------------------------------------------------------------------
22
23    /// Generic compare-columns: produce a device mask for `left <op> right`.
24    ///
25    /// Replaces: `compare_columns_u32`, `compare_columns_i32`, `compare_columns_i64`,
26    /// `compare_columns_u64`, `compare_columns_f32`, `compare_columns_f64`,
27    /// `compare_columns_u8`.
28    pub fn compare_columns<T: GpuScalar>(
29        &self,
30        input: &CudaBuffer,
31        left: usize,
32        right: usize,
33        op: CompareOp,
34    ) -> Result<TrackedCudaSlice<u8>> {
35        self.compare_columns_mask::<T>(
36            input,
37            left,
38            right,
39            op,
40            T::allowed_scalar_types(),
41            T::compare_col_kernel(),
42        )
43    }
44
45    /// Generic filter: keep rows where `column[col] <op> value`.
46    ///
47    /// Dispatches between fused compare+scan+compact (u32, f64) and
48    /// mask+compact (all other types).
49    ///
50    /// Replaces: `filter_u32`, `filter_f64`, `filter_i32`, `filter_u64`,
51    /// `filter_f32`, `filter_bool`.
52    pub fn filter<T: GpuScalar>(
53        &self,
54        input: &CudaBuffer,
55        col: usize,
56        value: T,
57        op: CompareOp,
58    ) -> Result<CudaBuffer> {
59        // Opt-in dispatch: route through the recorded filter path
60        // when the env var is set AND the manager is runtime-backed
61        // AND a launch stream can be acquired from the pool.
62        // Default behavior is unchanged (legacy fused / mask+compact).
63        if Self::use_recorded_filters_env() {
64            if let Some(launch_stream) = self.recorded_op_stream_or_init() {
65                return self.filter_recorded::<T>(input, col, value, op, launch_stream);
66            }
67        }
68
69        if input.is_empty() {
70            return self.create_empty_buffer(input.schema.clone());
71        }
72
73        if T::filter_scan_phase1_kernel().is_some() {
74            // Fused compare+scan+compact path (u32, f64).
75            self.filter_fused_scan::<T>(input, col, value, op)
76        } else {
77            // Mask + compact path (i32, u64, f32, i64, u8, bool).
78            let mask = self.compare_const_mask::<T>(
79                input,
80                col,
81                value,
82                op,
83                T::allowed_scalar_types(),
84                T::filter_compare_kernel(),
85            )?;
86            self.filter_by_device_mask(input, &mask)
87        }
88    }
89
90    /// Strict-recorder, end-to-end variant of [`Self::filter`] —
91    /// the first composed migrated DATA path.
92    ///
93    /// Composes [`Self::compare_const_mask_recorded`] and
94    /// [`Self::compact_buffer_by_device_mask_counted_recorded`]
95    /// on a single `launch_stream`. Each primitive builds its
96    /// own [`crate::launch::LaunchRecorder`], records its uses,
97    /// preflights, runs its kernels, and commits independently.
98    ///
99    /// Composition correctness rests on the runtime's
100    /// "record-all, wait-all" semantics: every
101    /// `record_block_use` call APPENDS a fresh event to the
102    /// live entry's `last_use_events: Vec<CudaEvent>`, and
103    /// `deallocate` waits on EVERY event in that vector before
104    /// queueing `cuMemFreeAsync`. So the compare's commit and
105    /// the compact's later commit each push their own event
106    /// for `input.column[i]` (and other shared buffers), and
107    /// the deallocate gates the free behind both — closing
108    /// the cross-stream lifetime gap end-to-end. (Latest-event
109    /// coalescing per `(block, launch_stream)` is a possible
110    /// future optimization; today every recorded use is
111    /// retained and waited on.)
112    ///
113    /// # Dispatch
114    /// For types with a fused `filter_compare_*_scan_phase1`
115    /// kernel (`u32`, `f64`), routes to
116    /// [`Self::filter_fused_scan_recorded`] — single-pass
117    /// compare+scan+compact mirror of the legacy fast path.
118    /// For all other types, composes
119    /// [`Self::compare_const_mask_recorded`] +
120    /// [`Self::compact_buffer_by_device_mask_counted_recorded`].
121    ///
122    /// # Errors
123    /// Propagates the structured `XlogError::Kernel` errors
124    /// produced by either underlying recorded primitive
125    /// (legacy manager, unresolved launch_stream, external
126    /// column, preflight / commit failures, kernel launch
127    /// failures, `cu_stream.synchronize()` before host scalar
128    /// read).
129    pub fn filter_recorded<T: GpuScalar>(
130        &self,
131        input: &CudaBuffer,
132        col: usize,
133        value: T,
134        op: CompareOp,
135        launch_stream: StreamId,
136    ) -> Result<CudaBuffer> {
137        if input.is_empty() {
138            return self.create_empty_buffer(input.schema.clone());
139        }
140        if T::filter_scan_phase1_kernel().is_some() {
141            return self.filter_fused_scan_recorded::<T>(input, col, value, op, launch_stream);
142        }
143        let d_mask = self.compare_const_mask_recorded::<T>(input, col, value, op, launch_stream)?;
144        self.compact_buffer_by_device_mask_counted_recorded(input, &d_mask, launch_stream)
145    }
146
147    /// Strict-recorder variant of [`Self::filter_fused_scan`] —
148    /// the migrated fused compare+scan+compact fast path for
149    /// `u32` and `f64`.
150    ///
151    /// Mirrors the legacy chain on a single explicit
152    /// `launch_stream`:
153    ///   1. `filter_compare_T_scan_phase1` — fused compare +
154    ///      block-local scan that produces `d_mask`,
155    ///      `d_prefix_sum`, `d_block_sums` in one launch.
156    ///   2. When `num_blocks > 1`,
157    ///      `multiblock_scan_u32_inplace_on_stream` on
158    ///      `d_block_sums` followed by
159    ///      `multiblock_scan_phase3` to propagate block offsets
160    ///      into `d_prefix_sum`.
161    ///   3. `capture_compact_count` — writes `d_out_count` for
162    ///      the masked total.
163    ///   4. `cu_stream.synchronize()` — explicitly orders the
164    ///      host scalar read of `d_out_count` against the
165    ///      pending capture kernel.
166    ///   5. `dtoh_scalar_untracked(&d_out_count, 0)` →
167    ///      `output_rows`.
168    ///   6. Per-input-column `compact_bytes_by_mask` on the
169    ///      same `launch_stream`.
170    ///
171    /// # Strict-mode contract
172    /// Identical to
173    /// [`Self::compact_buffer_by_device_mask_counted_recorded`]:
174    /// `input.num_rows_device()` and every `input.column(i)`
175    /// recorded as reads BEFORE preflight; every fresh
176    /// runtime-backed allocation (`d_mask`, `d_prefix_sum`,
177    /// `d_block_sums`, `d_out_count`, each `dst_col`) recorded
178    /// via `write` BEFORE preflight; the recorder snapshots
179    /// block identity at record time and drops the source
180    /// borrow, so kernel `&mut` borrows after preflight remain
181    /// valid before the kernels enqueue.
182    ///
183    /// # Panics
184    /// `T::filter_scan_phase1_kernel()` must be `Some` —
185    /// callers should only reach this method for `u32` / `f64`.
186    pub fn filter_fused_scan_recorded<T: GpuScalar>(
187        &self,
188        input: &CudaBuffer,
189        col: usize,
190        value: T,
191        op: CompareOp,
192        launch_stream: StreamId,
193    ) -> Result<CudaBuffer> {
194        let runtime = self.memory.runtime().ok_or_else(|| {
195            XlogError::Kernel(
196                "filter_fused_scan_recorded requires a runtime-backed GpuMemoryManager \
197                 (constructed via with_runtime)"
198                    .to_string(),
199            )
200        })?;
201        let pool = runtime.stream_pool();
202        let cu_stream = pool.resolve(launch_stream).ok_or_else(|| {
203            XlogError::Kernel(format!(
204                "filter_fused_scan_recorded: launch_stream StreamId({}) does not resolve",
205                launch_stream.0
206            ))
207        })?;
208
209        if input.num_rows() > u32::MAX as u64 {
210            return Err(XlogError::Kernel(format!(
211                "filter supports at most {} rows, got {}",
212                u32::MAX,
213                input.num_rows()
214            )));
215        }
216        if col >= input.arity() {
217            return Err(XlogError::Kernel(format!(
218                "Column index {} out of bounds (arity {})",
219                col,
220                input.arity()
221            )));
222        }
223        let col_type = input
224            .schema()
225            .column_type(col)
226            .ok_or_else(|| XlogError::Kernel("Missing column type".into()))?;
227        if !T::allowed_scalar_types().contains(&col_type) {
228            return Err(XlogError::Kernel(format!(
229                "Column {} is {:?} (expected one of {:?})",
230                col,
231                col_type,
232                T::allowed_scalar_types()
233            )));
234        }
235        if input.is_empty() {
236            return self.create_empty_buffer(input.schema.clone());
237        }
238
239        let n = input.num_rows() as usize;
240        let num_rows = input.num_rows() as u32;
241        let block_size = 256u32;
242        let num_blocks = num_rows.div_ceil(block_size);
243        let row_cap = u64::from(num_rows);
244        let device = self.device.inner();
245
246        let col_data = input
247            .column(col)
248            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col)))?;
249        let col_view = Self::column_as_typed_view::<T>(col_data, n)?;
250
251        // Allocate ALL fresh runtime-backed buffers BEFORE the
252        // recorder (Rust drop order — these are recorded as
253        // pre-launch writes via the standard `write` API; the
254        // recorder snapshots block identity at record time so
255        // the kernel `&mut` borrow after preflight is unaffected).
256        let d_mask = self.memory.alloc::<u8>(n)?;
257        let d_prefix_sum = self.memory.alloc::<u32>(n)?;
258        let mut d_block_sums = self.memory.alloc::<u32>(num_blocks as usize)?;
259        let mut d_out_count = self.memory.alloc::<u32>(1)?;
260
261        let mut dst_cols: Vec<TrackedCudaSlice<u8>> = Vec::with_capacity(input.columns.len());
262        for col_idx in 0..input.columns.len() {
263            let elem_size = input
264                .schema
265                .column_type(col_idx)
266                .map(|t| t.size_bytes())
267                .unwrap_or(4);
268            let output_bytes = (row_cap as usize) * elem_size;
269            dst_cols.push(self.memory.alloc::<u8>(output_bytes)?);
270        }
271
272        let mut rec = LaunchRecorder::new_strict(launch_stream);
273        rec.read(input.num_rows_device());
274        for col_idx in 0..input.columns.len() {
275            let src_col = input
276                .column(col_idx)
277                .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
278            rec.read_column(src_col);
279        }
280        rec.write(&d_mask);
281        rec.write(&d_prefix_sum);
282        rec.write(&d_block_sums);
283        rec.write(&d_out_count);
284        for dst_col in &dst_cols {
285            rec.write(dst_col);
286        }
287        rec.preflight(runtime).map_err(|e| {
288            XlogError::Kernel(format!(
289                "filter_fused_scan_recorded: launch recorder preflight failed: {}",
290                e
291            ))
292        })?;
293
294        // Step 1: fused compare + block-local scan.
295        let scan_kernel_name = T::filter_scan_phase1_kernel()
296            .expect("filter_fused_scan_recorded called without scan phase1 kernel");
297        let filter_scan_fn = device
298            .get_func(FILTER_MODULE, scan_kernel_name)
299            .ok_or_else(|| XlogError::Kernel(format!("{} kernel not found", scan_kernel_name)))?;
300        // SAFETY: filter_compare_*_scan_phase1(column, constant, num_rows,
301        // num_rows_device, op, mask, prefix_sum, block_sums)
302        unsafe {
303            filter_scan_fn.clone().launch_on_stream(
304                &cu_stream,
305                LaunchConfig {
306                    grid_dim: (num_blocks, 1, 1),
307                    block_dim: (block_size, 1, 1),
308                    shared_mem_bytes: 0,
309                },
310                (
311                    &col_view,
312                    value,
313                    num_rows,
314                    input.num_rows_device(),
315                    op as u8,
316                    &d_mask,
317                    &d_prefix_sum,
318                    &d_block_sums,
319                ),
320            )
321        }
322        .map_err(|e| {
323            XlogError::Kernel(format!("{} (on_stream) failed: {}", scan_kernel_name, e))
324        })?;
325
326        // Step 2: multi-block scan propagation (only when there
327        // is more than one block).
328        if num_blocks > 1 {
329            self.multiblock_scan_u32_inplace_on_stream(
330                &mut d_block_sums,
331                num_blocks,
332                &cu_stream,
333                launch_stream,
334                runtime,
335            )?;
336
337            let phase3_fn = device
338                .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE3)
339                .ok_or_else(|| {
340                    XlogError::Kernel("Failed to get multiblock_scan_phase3 kernel".to_string())
341                })?;
342            // SAFETY: multiblock_scan_phase3(prefix_sum, block_offsets, n)
343            unsafe {
344                phase3_fn.clone().launch_on_stream(
345                    &cu_stream,
346                    LaunchConfig {
347                        grid_dim: (num_blocks, 1, 1),
348                        block_dim: (block_size, 1, 1),
349                        shared_mem_bytes: 0,
350                    },
351                    (&d_prefix_sum, &d_block_sums, num_rows),
352                )
353            }
354            .map_err(|e| {
355                XlogError::Kernel(format!("multiblock_scan_phase3 (on_stream) failed: {}", e))
356            })?;
357        }
358
359        // Step 3: capture compact count on launch_stream.
360        let capture_fn = device
361            .get_func(FILTER_MODULE, filter_kernels::CAPTURE_COMPACT_COUNT)
362            .ok_or_else(|| {
363                XlogError::Kernel("capture_compact_count kernel not found".to_string())
364            })?;
365        // SAFETY: capture_compact_count(prefix_sum, mask, n, out_count)
366        unsafe {
367            capture_fn.clone().launch_on_stream(
368                &cu_stream,
369                LaunchConfig {
370                    grid_dim: (1, 1, 1),
371                    block_dim: (1, 1, 1),
372                    shared_mem_bytes: 0,
373                },
374                (&d_prefix_sum, &d_mask, num_rows, &mut d_out_count),
375            )
376        }
377        .map_err(|e| {
378            XlogError::Kernel(format!("capture_compact_count (on_stream) failed: {}", e))
379        })?;
380
381        // Step 4: explicit barrier before host scalar read.
382        // Non-blocking streams do NOT get default-stream
383        // implicit synchronization, so the dtoh_scalar_untracked
384        // call below would otherwise race the still-pending
385        // capture kernel.
386        cu_stream.synchronize().map_err(|e| {
387            XlogError::Kernel(format!(
388                "filter_fused_scan_recorded: launch_stream synchronize before host scalar \
389                 read failed: {}",
390                e
391            ))
392        })?;
393        let output_rows = self.dtoh_scalar_untracked(&d_out_count, 0)? as u64;
394
395        // Step 5: per-column compact_bytes_by_mask on
396        // launch_stream. Same shape as
397        // compact_buffer_by_device_mask_counted_recorded; only
398        // run when there are rows to keep.
399        if output_rows > 0 {
400            let compact_fn = device
401                .get_func(FILTER_MODULE, filter_kernels::COMPACT_BYTES_BY_MASK)
402                .ok_or_else(|| {
403                    XlogError::Kernel("compact_bytes_by_mask kernel not found".to_string())
404                })?;
405            let cfg = LaunchConfig {
406                grid_dim: (num_blocks, 1, 1),
407                block_dim: (block_size, 1, 1),
408                shared_mem_bytes: 0,
409            };
410            for (col_idx, dst_col) in dst_cols.iter().enumerate() {
411                let src_col = input
412                    .column(col_idx)
413                    .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
414                let elem_size = input
415                    .schema
416                    .column_type(col_idx)
417                    .map(|t| t.size_bytes())
418                    .unwrap_or(4) as u32;
419                // SAFETY: compact_bytes_by_mask(input, mask, prefix_sum, n, elem_size, output)
420                unsafe {
421                    compact_fn.clone().launch_on_stream(
422                        &cu_stream,
423                        cfg,
424                        (
425                            src_col,
426                            &d_mask,
427                            &d_prefix_sum,
428                            num_rows,
429                            elem_size,
430                            dst_col,
431                        ),
432                    )
433                }
434                .map_err(|e| {
435                    XlogError::Kernel(format!("compact_bytes_by_mask (on_stream) failed: {}", e))
436                })?;
437            }
438        }
439
440        // Record fresh writes via the post-preflight escape
441        // hatch and commit.
442        rec.commit(runtime).map_err(|e| {
443            XlogError::Kernel(format!(
444                "filter_fused_scan_recorded: launch recorder commit failed: {}",
445                e
446            ))
447        })?;
448
449        let new_columns: Vec<CudaColumn> = dst_cols.into_iter().map(|s| s.into()).collect();
450        Ok(CudaBuffer::from_columns_with_host_count(
451            new_columns,
452            row_cap,
453            d_out_count,
454            input.schema.clone(),
455            output_rows as u32,
456        ))
457    }
458
459    /// Strict-recorder, end-to-end variant of column-column
460    /// filter: keep rows where `column[left] <op> column[right]`.
461    ///
462    /// Composes [`Self::compare_columns_mask_recorded`] and
463    /// [`Self::compact_buffer_by_device_mask_counted_recorded`]
464    /// on a single `launch_stream`. Same composition contract
465    /// as [`Self::filter_recorded`]: each primitive builds its
466    /// own recorder and commits independently; the runtime
467    /// appends every recorded event to `last_use_events`, and
468    /// `deallocate` waits on every event, so input columns
469    /// referenced by BOTH the compare AND the per-column
470    /// compacts are correctly gated end-to-end.
471    ///
472    /// # Errors
473    /// Propagates the structured `XlogError::Kernel` errors
474    /// produced by either underlying recorded primitive
475    /// (legacy manager, unresolved launch_stream, external
476    /// column on either input side, preflight / commit
477    /// failures, kernel launch failures,
478    /// `cu_stream.synchronize()` before host scalar read).
479    pub fn filter_columns_recorded<T: GpuScalar>(
480        &self,
481        input: &CudaBuffer,
482        left: usize,
483        right: usize,
484        op: CompareOp,
485        launch_stream: StreamId,
486    ) -> Result<CudaBuffer> {
487        if input.is_empty() {
488            return self.create_empty_buffer(input.schema.clone());
489        }
490        let d_mask =
491            self.compare_columns_mask_recorded::<T>(input, left, right, op, launch_stream)?;
492        self.compact_buffer_by_device_mask_counted_recorded(input, &d_mask, launch_stream)
493    }
494
495    // ------------------------------------------------------------------
496    // Private helpers (moved from mod.rs)
497    // ------------------------------------------------------------------
498
499    /// Generate a device mask by comparing a column against a constant value.
500    ///
501    /// Each output byte is 1 if the comparison holds, 0 otherwise.
502    pub(crate) fn compare_const_mask<T: KernelScalar>(
503        &self,
504        input: &CudaBuffer,
505        col: usize,
506        value: T,
507        op: CompareOp,
508        allowed_types: &[ScalarType],
509        kernel: &str,
510    ) -> Result<TrackedCudaSlice<u8>> {
511        if input.num_rows() > u32::MAX as u64 {
512            return Err(XlogError::Kernel(format!(
513                "Filter supports at most {} rows, got {}",
514                u32::MAX,
515                input.num_rows()
516            )));
517        }
518        if col >= input.arity() {
519            return Err(XlogError::Kernel(format!(
520                "Column index {} out of bounds (arity {})",
521                col,
522                input.arity()
523            )));
524        }
525
526        if input.is_empty() {
527            return self.memory.alloc::<u8>(0);
528        }
529
530        let col_type = input
531            .schema()
532            .column_type(col)
533            .ok_or_else(|| XlogError::Kernel("Missing column type".into()))?;
534        if !allowed_types.contains(&col_type) {
535            return Err(XlogError::Kernel(format!(
536                "Column {} is {:?} (expected {:?})",
537                col, col_type, allowed_types
538            )));
539        }
540
541        let num_rows = input.num_rows() as u32;
542        let expected_bytes = (num_rows as usize)
543            .checked_mul(std::mem::size_of::<T>())
544            .ok_or_else(|| XlogError::Kernel("filter compare size overflow".into()))?;
545        let col_data = input
546            .column(col)
547            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col)))?;
548        if col_data.num_bytes() != expected_bytes {
549            return Err(XlogError::Kernel(format!(
550                "Column {} has {} bytes but expected {} for {} rows",
551                col,
552                col_data.num_bytes(),
553                expected_bytes,
554                input.num_rows()
555            )));
556        }
557
558        let block_size = 256u32;
559        let num_blocks = num_rows.div_ceil(block_size);
560        let config = LaunchConfig {
561            grid_dim: (num_blocks, 1, 1),
562            block_dim: (block_size, 1, 1),
563            shared_mem_bytes: 0,
564        };
565
566        let mut d_mask = self.memory.alloc::<u8>(num_rows as usize)?;
567        let func = self
568            .device
569            .inner()
570            .get_func(FILTER_MODULE, kernel)
571            .ok_or_else(|| XlogError::Kernel("filter compare kernel not found".into()))?;
572
573        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
574        unsafe {
575            func.clone()
576                .launch(config, (col_data, value, num_rows, op as u8, &mut d_mask))
577        }
578        .map_err(|e| XlogError::Kernel(format!("filter compare failed: {}", e)))?;
579
580        Ok(d_mask)
581    }
582
583    /// Strict-recorder variant of [`Self::compare_const_mask`].
584    ///
585    /// Runs the filter compare kernel on the caller-supplied
586    /// `launch_stream` and threads the column read through the
587    /// runtime via [`LaunchRecorder`]. This is the second
588    /// migrated launch path (after `memset_recorded`) and the
589    /// first kernel-driven one — it is intentionally a sibling
590    /// of the legacy [`Self::compare_const_mask`] rather than a
591    /// replacement. Existing callers stay on the legacy path
592    /// until the broader filter migration lands.
593    ///
594    /// # Strict-mode contract
595    /// * Requires the provider's manager to be built via
596    ///   [`crate::GpuMemoryManager::with_runtime`]; otherwise
597    ///   returns `XlogError::Kernel` before any allocation.
598    /// * `input.column(col)` is recorded as a read; external
599    ///   (`CudaColumn::Dlpack` / `CudaColumn::ArrowDevice`)
600    ///   columns are rejected at preflight, before the kernel
601    ///   is enqueued.
602    /// * `d_mask` is freshly allocated through the same
603    ///   runtime-backed manager. By construction its
604    ///   `runtime_block()` is `Some`. Its owning slice is
605    ///   registered as a write before preflight, which snapshots
606    ///   the allocation identity without retaining the borrow.
607    ///   Preflight queues its dependency waits, the kernel is
608    ///   then enqueued with `&mut d_mask`, and commit afterward
609    ///   finalizes the use by publishing the write event. Future
610    ///   recorded launchers must likewise register each owning
611    ///   slice or column before preflight.
612    ///
613    /// # Errors
614    ///   * `XlogError::Kernel` if the manager has no runtime,
615    ///     or if `launch_stream` does not resolve.
616    ///   * `XlogError::Kernel` from preflight (external column,
617    ///     unsupported active resource).
618    ///   * `XlogError::Kernel` from the underlying CUDA launch.
619    ///   * `XlogError::Kernel` from commit on transient
620    ///     `record_block_use` failure.
621    pub fn compare_const_mask_recorded<T: GpuScalar>(
622        &self,
623        input: &CudaBuffer,
624        col: usize,
625        value: T,
626        op: CompareOp,
627        launch_stream: StreamId,
628    ) -> Result<TrackedCudaSlice<u8>> {
629        let allowed_types = T::allowed_scalar_types();
630        let kernel = T::filter_compare_kernel();
631        let runtime = self.memory.runtime().ok_or_else(|| {
632            XlogError::Kernel(
633                "compare_const_mask_recorded requires a runtime-backed GpuMemoryManager \
634                 (constructed via with_runtime)"
635                    .to_string(),
636            )
637        })?;
638        let pool = runtime.stream_pool();
639        let cu_stream = pool.resolve(launch_stream).ok_or_else(|| {
640            XlogError::Kernel(format!(
641                "compare_const_mask_recorded: launch_stream StreamId({}) does not resolve",
642                launch_stream.0
643            ))
644        })?;
645
646        if input.num_rows() > u32::MAX as u64 {
647            return Err(XlogError::Kernel(format!(
648                "Filter supports at most {} rows, got {}",
649                u32::MAX,
650                input.num_rows()
651            )));
652        }
653        if col >= input.arity() {
654            return Err(XlogError::Kernel(format!(
655                "Column index {} out of bounds (arity {})",
656                col,
657                input.arity()
658            )));
659        }
660
661        if input.is_empty() {
662            return self.memory.alloc::<u8>(0);
663        }
664
665        let col_type = input
666            .schema()
667            .column_type(col)
668            .ok_or_else(|| XlogError::Kernel("Missing column type".into()))?;
669        if !allowed_types.contains(&col_type) {
670            return Err(XlogError::Kernel(format!(
671                "Column {} is {:?} (expected {:?})",
672                col, col_type, allowed_types
673            )));
674        }
675
676        let num_rows = input.num_rows() as u32;
677        let expected_bytes = (num_rows as usize)
678            .checked_mul(std::mem::size_of::<T>())
679            .ok_or_else(|| XlogError::Kernel("filter compare size overflow".into()))?;
680        let col_data = input
681            .column(col)
682            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col)))?;
683        if col_data.num_bytes() != expected_bytes {
684            return Err(XlogError::Kernel(format!(
685                "Column {} has {} bytes but expected {} for {} rows",
686                col,
687                col_data.num_bytes(),
688                expected_bytes,
689                input.num_rows()
690            )));
691        }
692
693        let block_size = 256u32;
694        let num_blocks = num_rows.div_ceil(block_size);
695        let config = LaunchConfig {
696            grid_dim: (num_blocks, 1, 1),
697            block_dim: (block_size, 1, 1),
698            shared_mem_bytes: 0,
699        };
700
701        let mut d_mask = self.memory.alloc::<u8>(num_rows as usize)?;
702        let func = self
703            .device
704            .inner()
705            .get_func(FILTER_MODULE, kernel)
706            .ok_or_else(|| XlogError::Kernel("filter compare kernel not found".into()))?;
707
708        // Strict recorder + PREFLIGHT before the kernel queues.
709        // External columns (DLPack / Arrow) and unsupported
710        // active resources are caught here without any CUDA
711        // work in flight.
712        let mut rec = LaunchRecorder::new_strict(launch_stream);
713        rec.read_column(col_data);
714        rec.write(&d_mask);
715        rec.preflight(runtime).map_err(|e| {
716            XlogError::Kernel(format!(
717                "compare_const_mask_recorded: launch recorder preflight failed: {}",
718                e
719            ))
720        })?;
721
722        // SAFETY: PTX kernel signature matches the params tuple;
723        // col_data is validated above and lives through the
724        // launch (held by `input`); d_mask was allocated by the
725        // same runtime-backed manager and matches `num_rows`.
726        // launch_on_stream queues on `cu_stream` and returns
727        // immediately.
728        unsafe {
729            func.clone().launch_on_stream(
730                &cu_stream,
731                config,
732                (col_data, value, num_rows, op as u8, &mut d_mask),
733            )
734        }
735        .map_err(|e| {
736            XlogError::Kernel(format!("compare_const_mask_recorded launch failed: {}", e))
737        })?;
738
739        // Finalize the preflighted write registration AFTER the
740        // launch enqueues: commit publishes the write event for
741        // future dependent uses. See the "Strict-mode contract"
742        // on this method.
743        rec.commit(runtime).map_err(|e| {
744            XlogError::Kernel(format!(
745                "compare_const_mask_recorded: launch recorder commit failed: {}",
746                e
747            ))
748        })?;
749
750        Ok(d_mask)
751    }
752
753    /// Generate a device mask by comparing two columns element-wise.
754    pub(crate) fn compare_columns_mask<T: DeviceRepr>(
755        &self,
756        input: &CudaBuffer,
757        left: usize,
758        right: usize,
759        op: CompareOp,
760        allowed_types: &[ScalarType],
761        kernel: &str,
762    ) -> Result<TrackedCudaSlice<u8>> {
763        if input.num_rows() > u32::MAX as u64 {
764            return Err(XlogError::Kernel(format!(
765                "Filter supports at most {} rows, got {}",
766                u32::MAX,
767                input.num_rows()
768            )));
769        }
770        if left >= input.arity() || right >= input.arity() {
771            return Err(XlogError::Kernel(format!(
772                "Column indices {} or {} out of bounds (arity {})",
773                left,
774                right,
775                input.arity()
776            )));
777        }
778
779        if input.is_empty() {
780            return self.memory.alloc::<u8>(0);
781        }
782
783        let left_type = input
784            .schema()
785            .column_type(left)
786            .ok_or_else(|| XlogError::Kernel("Missing left column type".into()))?;
787        let right_type = input
788            .schema()
789            .column_type(right)
790            .ok_or_else(|| XlogError::Kernel("Missing right column type".into()))?;
791
792        if left_type != right_type {
793            return Err(XlogError::Kernel(
794                "Column-column compare requires matching types".into(),
795            ));
796        }
797        if !allowed_types.contains(&left_type) {
798            return Err(XlogError::Kernel(format!(
799                "Column type {:?} not supported for compare",
800                left_type
801            )));
802        }
803
804        let num_rows = input.num_rows() as u32;
805        let expected_bytes = (num_rows as usize)
806            .checked_mul(std::mem::size_of::<T>())
807            .ok_or_else(|| XlogError::Kernel("compare columns size overflow".into()))?;
808
809        let left_col = input
810            .column(left)
811            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", left)))?;
812        let right_col = input
813            .column(right)
814            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", right)))?;
815
816        if left_col.num_bytes() != expected_bytes || right_col.num_bytes() != expected_bytes {
817            return Err(XlogError::Kernel(format!(
818                "Compare columns expect {} bytes per column for {} rows",
819                expected_bytes,
820                input.num_rows()
821            )));
822        }
823
824        let block_size = 256u32;
825        let num_blocks = num_rows.div_ceil(block_size);
826        let config = LaunchConfig {
827            grid_dim: (num_blocks, 1, 1),
828            block_dim: (block_size, 1, 1),
829            shared_mem_bytes: 0,
830        };
831
832        let mut d_mask = self.memory.alloc::<u8>(num_rows as usize)?;
833        let func = self
834            .device
835            .inner()
836            .get_func(FILTER_MODULE, kernel)
837            .ok_or_else(|| XlogError::Kernel("filter compare kernel not found".into()))?;
838
839        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
840        unsafe {
841            func.clone().launch(
842                config,
843                (left_col, right_col, num_rows, op as u8, &mut d_mask),
844            )
845        }
846        .map_err(|e| XlogError::Kernel(format!("filter compare failed: {}", e)))?;
847
848        Ok(d_mask)
849    }
850
851    /// Strict-recorder variant of [`Self::compare_columns_mask`].
852    ///
853    /// Runs the column-column compare kernel on the
854    /// caller-supplied `launch_stream` and threads BOTH column
855    /// reads through the runtime via [`LaunchRecorder`]. Sibling
856    /// of the legacy [`Self::compare_columns_mask`]; existing
857    /// callers stay on the legacy path.
858    ///
859    /// # Strict-mode contract
860    /// * Requires the provider's manager to be built via
861    ///   [`crate::GpuMemoryManager::with_runtime`]; otherwise
862    ///   returns `XlogError::Kernel` before any allocation.
863    /// * `input.column(left)` and `input.column(right)` are both
864    ///   recorded as reads BEFORE preflight. External (DLPack /
865    ///   Arrow) columns on either side are rejected at preflight,
866    ///   before the kernel is enqueued.
867    /// * `d_mask` is freshly allocated by the same runtime-backed
868    ///   manager; its write is recorded via the standard `write`
869    ///   API BEFORE preflight (the recorder snapshots block
870    ///   identity, so the kernel `&mut d_mask` borrow after
871    ///   preflight is unaffected).
872    ///
873    /// # Errors
874    ///   * `XlogError::Kernel` if the manager has no runtime,
875    ///     or if `launch_stream` does not resolve.
876    ///   * `XlogError::Kernel` from preflight (external column
877    ///     on either side, unsupported active resource).
878    ///   * `XlogError::Kernel` from the underlying CUDA launch.
879    ///   * `XlogError::Kernel` from commit on transient
880    ///     `record_block_use` failure.
881    pub fn compare_columns_mask_recorded<T: GpuScalar>(
882        &self,
883        input: &CudaBuffer,
884        left: usize,
885        right: usize,
886        op: CompareOp,
887        launch_stream: StreamId,
888    ) -> Result<TrackedCudaSlice<u8>> {
889        let allowed_types = T::allowed_scalar_types();
890        let kernel = T::compare_col_kernel();
891
892        let runtime = self.memory.runtime().ok_or_else(|| {
893            XlogError::Kernel(
894                "compare_columns_mask_recorded requires a runtime-backed GpuMemoryManager \
895                 (constructed via with_runtime)"
896                    .to_string(),
897            )
898        })?;
899        let pool = runtime.stream_pool();
900        let cu_stream = pool.resolve(launch_stream).ok_or_else(|| {
901            XlogError::Kernel(format!(
902                "compare_columns_mask_recorded: launch_stream StreamId({}) does not resolve",
903                launch_stream.0
904            ))
905        })?;
906
907        if input.num_rows() > u32::MAX as u64 {
908            return Err(XlogError::Kernel(format!(
909                "Filter supports at most {} rows, got {}",
910                u32::MAX,
911                input.num_rows()
912            )));
913        }
914        if left >= input.arity() || right >= input.arity() {
915            return Err(XlogError::Kernel(format!(
916                "Column indices {} or {} out of bounds (arity {})",
917                left,
918                right,
919                input.arity()
920            )));
921        }
922
923        if input.is_empty() {
924            return self.memory.alloc::<u8>(0);
925        }
926
927        let left_type = input
928            .schema()
929            .column_type(left)
930            .ok_or_else(|| XlogError::Kernel("Missing left column type".into()))?;
931        let right_type = input
932            .schema()
933            .column_type(right)
934            .ok_or_else(|| XlogError::Kernel("Missing right column type".into()))?;
935        if left_type != right_type {
936            return Err(XlogError::Kernel(
937                "Column-column compare requires matching types".into(),
938            ));
939        }
940        if !allowed_types.contains(&left_type) {
941            return Err(XlogError::Kernel(format!(
942                "Column type {:?} not supported for compare",
943                left_type
944            )));
945        }
946
947        let num_rows = input.num_rows() as u32;
948        let expected_bytes = (num_rows as usize)
949            .checked_mul(std::mem::size_of::<T>())
950            .ok_or_else(|| XlogError::Kernel("compare columns size overflow".into()))?;
951        let left_col = input
952            .column(left)
953            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", left)))?;
954        let right_col = input
955            .column(right)
956            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", right)))?;
957        if left_col.num_bytes() != expected_bytes || right_col.num_bytes() != expected_bytes {
958            return Err(XlogError::Kernel(format!(
959                "Compare columns expect {} bytes per column for {} rows",
960                expected_bytes,
961                input.num_rows()
962            )));
963        }
964
965        let block_size = 256u32;
966        let num_blocks = num_rows.div_ceil(block_size);
967        let config = LaunchConfig {
968            grid_dim: (num_blocks, 1, 1),
969            block_dim: (block_size, 1, 1),
970            shared_mem_bytes: 0,
971        };
972
973        let mut d_mask = self.memory.alloc::<u8>(num_rows as usize)?;
974        let func = self
975            .device
976            .inner()
977            .get_func(FILTER_MODULE, kernel)
978            .ok_or_else(|| XlogError::Kernel("filter compare kernel not found".into()))?;
979
980        // Record BOTH column reads BEFORE preflight. Strict mode
981        // catches external columns on either side here, before
982        // any CUDA work is queued.
983        let mut rec = LaunchRecorder::new_strict(launch_stream);
984        rec.read_column(left_col);
985        rec.read_column(right_col);
986        rec.write(&d_mask);
987        rec.preflight(runtime).map_err(|e| {
988            XlogError::Kernel(format!(
989                "compare_columns_mask_recorded: launch recorder preflight failed: {}",
990                e
991            ))
992        })?;
993
994        // SAFETY: PTX kernel signature matches the params tuple;
995        // both columns are validated above and live through the
996        // launch (held by `input`); d_mask was allocated by the
997        // same runtime-backed manager and matches `num_rows`.
998        // launch_on_stream queues on `cu_stream` and returns
999        // immediately.
1000        unsafe {
1001            func.clone().launch_on_stream(
1002                &cu_stream,
1003                config,
1004                (left_col, right_col, num_rows, op as u8, &mut d_mask),
1005            )
1006        }
1007        .map_err(|e| {
1008            XlogError::Kernel(format!(
1009                "compare_columns_mask_recorded launch failed: {}",
1010                e
1011            ))
1012        })?;
1013
1014        // d_mask was registered as a write before preflight;
1015        // preflight waited for dependencies, and the kernel is now
1016        // enqueued. Commit publishes the write event for future uses.
1017        rec.commit(runtime).map_err(|e| {
1018            XlogError::Kernel(format!(
1019                "compare_columns_mask_recorded: launch recorder commit failed: {}",
1020                e
1021            ))
1022        })?;
1023
1024        Ok(d_mask)
1025    }
1026
1027    // ------------------------------------------------------------------
1028    // Fused compare+scan+compact path (generic over T)
1029    // ------------------------------------------------------------------
1030
1031    /// Fused compare+scan+compact filter. Used for types with a dedicated
1032    /// `filter_scan_phase1` kernel (u32 and f64).
1033    fn filter_fused_scan<T: GpuScalar>(
1034        &self,
1035        input: &CudaBuffer,
1036        col: usize,
1037        value: T,
1038        op: CompareOp,
1039    ) -> Result<CudaBuffer> {
1040        if input.num_rows() > u32::MAX as u64 {
1041            return Err(XlogError::Kernel(format!(
1042                "filter supports at most {} rows, got {}",
1043                u32::MAX,
1044                input.num_rows()
1045            )));
1046        }
1047
1048        let n = input.num_rows() as usize;
1049        let num_rows = input.num_rows() as u32;
1050        let device = self.device.inner();
1051
1052        // Validate column index
1053        if col >= input.arity() {
1054            return Err(XlogError::Kernel(format!(
1055                "Column index {} out of bounds (arity {})",
1056                col,
1057                input.arity()
1058            )));
1059        }
1060
1061        // Validate column type
1062        let col_type = input
1063            .schema()
1064            .column_type(col)
1065            .ok_or_else(|| XlogError::Kernel("Missing column type".into()))?;
1066        if !T::allowed_scalar_types().contains(&col_type) {
1067            return Err(XlogError::Kernel(format!(
1068                "Column {} is {:?} (expected one of {:?})",
1069                col,
1070                col_type,
1071                T::allowed_scalar_types()
1072            )));
1073        }
1074
1075        // Get the filter column as a typed view
1076        let col_data = input
1077            .column(col)
1078            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col)))?;
1079        let col_view = Self::column_as_typed_view::<T>(col_data, n)?;
1080
1081        let block_size = 256u32;
1082        let num_blocks = num_rows.div_ceil(block_size);
1083        let config = LaunchConfig {
1084            grid_dim: (num_blocks, 1, 1),
1085            block_dim: (block_size, 1, 1),
1086            shared_mem_bytes: 0,
1087        };
1088
1089        // Fused compare + scan phase1.
1090        let d_mask = self.memory.alloc::<u8>(n)?;
1091        let d_prefix_sum = self.memory.alloc::<u32>(n)?;
1092        let mut d_block_sums = self.memory.alloc::<u32>(num_blocks as usize)?;
1093
1094        let scan_kernel_name = T::filter_scan_phase1_kernel()
1095            .expect("filter_fused_scan called without scan phase1 kernel");
1096        let filter_scan_fn = device
1097            .get_func(FILTER_MODULE, scan_kernel_name)
1098            .ok_or_else(|| XlogError::Kernel(format!("{} kernel not found", scan_kernel_name)))?;
1099
1100        // SAFETY: filter_compare_*_scan_phase1(column, constant, num_rows, num_rows_device, op, mask, prefix_sum, block_sums)
1101        unsafe {
1102            filter_scan_fn.clone().launch(
1103                config,
1104                (
1105                    &col_view,
1106                    value,
1107                    num_rows,
1108                    input.num_rows_device(),
1109                    op as u8,
1110                    &d_mask,
1111                    &d_prefix_sum,
1112                    &d_block_sums,
1113                ),
1114            )
1115        }
1116        .map_err(|e| XlogError::Kernel(format!("{} failed: {}", scan_kernel_name, e)))?;
1117
1118        if num_blocks > 1 {
1119            self.multiblock_scan_u32_inplace(&mut d_block_sums, num_blocks)?;
1120
1121            let phase3_fn = device
1122                .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE3)
1123                .ok_or_else(|| {
1124                    XlogError::Kernel("Failed to get multiblock_scan_phase3 kernel".to_string())
1125                })?;
1126
1127            // SAFETY: multiblock_scan_phase3(uint32_t* prefix_sum, const uint32_t* block_offsets, uint32_t n)
1128            unsafe {
1129                phase3_fn.clone().launch(
1130                    LaunchConfig {
1131                        grid_dim: (num_blocks, 1, 1),
1132                        block_dim: (block_size, 1, 1),
1133                        shared_mem_bytes: 0,
1134                    },
1135                    (&d_prefix_sum, &d_block_sums, num_rows),
1136                )
1137            }
1138            .map_err(|e| XlogError::Kernel(format!("multiblock_scan_phase3 failed: {}", e)))?;
1139        }
1140
1141        self.device.synchronize()?;
1142
1143        let d_out_count = self.capture_compact_count(&d_prefix_sum, &d_mask, num_rows)?;
1144        self.compact_buffer_by_device_mask_device_count(input, &d_mask, &d_prefix_sum, d_out_count)
1145    }
1146
1147    // ------------------------------------------------------------------
1148    // Generic column view helper
1149    // ------------------------------------------------------------------
1150
1151    /// Reinterpret a `CudaColumn` as a typed `RawCudaView<T>` for kernel access.
1152    ///
1153    /// This is the generic equivalent of `column_as_u32_view`, `column_as_f64_view`, etc.
1154    fn column_as_typed_view<'a, T: GpuScalar>(
1155        col: &'a CudaColumn,
1156        num_elements: usize,
1157    ) -> Result<RawCudaView<'a, T>> {
1158        let required_bytes = num_elements * T::BYTE_WIDTH;
1159        if col.num_bytes() < required_bytes {
1160            return Err(XlogError::Kernel(format!(
1161                "Column has {} bytes but {} required for {} elements of size {}",
1162                col.num_bytes(),
1163                required_bytes,
1164                num_elements,
1165                T::BYTE_WIDTH,
1166            )));
1167        }
1168        let ptr = *col.device_ptr();
1169        if T::BYTE_WIDTH > 1 && !(ptr as usize).is_multiple_of(T::BYTE_WIDTH) {
1170            return Err(XlogError::Kernel(format!(
1171                "Column device pointer is not {}-byte aligned",
1172                T::BYTE_WIDTH,
1173            )));
1174        }
1175        Ok(RawCudaView {
1176            ptr,
1177            len: num_elements,
1178            stream: col.stream().clone(),
1179            _marker: PhantomData,
1180        })
1181    }
1182
1183    // ------------------------------------------------------------------
1184    // Host-mask prefix sum + filter/compact infrastructure
1185    // (moved from mod.rs)
1186    // ------------------------------------------------------------------
1187
1188    pub fn prefix_sum_mask(&self, mask: &[u8]) -> Result<(Vec<u32>, u32)> {
1189        if mask.is_empty() {
1190            return Ok((vec![], 0));
1191        }
1192
1193        let n = mask.len();
1194        if n > u32::MAX as usize {
1195            return Err(XlogError::Kernel(format!(
1196                "Mask length {} exceeds u32::MAX",
1197                n
1198            )));
1199        }
1200
1201        // For small inputs, use CPU scan (faster than kernel launch overhead)
1202        if n <= 256 {
1203            return self.prefix_sum_mask_cpu(mask);
1204        }
1205
1206        // For larger inputs, use multi-block GPU scan
1207        self.prefix_sum_mask_gpu_multiblock(mask)
1208    }
1209
1210    /// CPU implementation for small prefix sums (avoids kernel launch overhead)
1211    fn prefix_sum_mask_cpu(&self, mask: &[u8]) -> Result<(Vec<u32>, u32)> {
1212        let mut prefix_sum = Vec::with_capacity(mask.len());
1213        let mut sum = 0u32;
1214        for &m in mask {
1215            prefix_sum.push(sum);
1216            sum += m as u32;
1217        }
1218        Ok((prefix_sum, sum))
1219    }
1220
1221    /// Multi-block GPU implementation for large prefix sums
1222    /// Uses three-phase algorithm:
1223    /// 1. Each block computes local exclusive scan and outputs block total
1224    /// 2. Scan the block totals to get block offsets
1225    /// 3. Add block offsets to each element
1226    fn prefix_sum_mask_gpu_multiblock(&self, mask: &[u8]) -> Result<(Vec<u32>, u32)> {
1227        let n = mask.len();
1228        let device = self.device.inner();
1229        let block_size = 256u32;
1230        let num_blocks = (n as u32).div_ceil(block_size);
1231
1232        // Upload mask to GPU
1233        let d_mask = self
1234            .htod_sync_copy_tracked(mask)
1235            .map_err(|e| XlogError::Kernel(format!("Failed to upload mask: {}", e)))?;
1236
1237        // Allocate output for prefix sum (using memory manager for budget enforcement)
1238        let d_prefix_sum = self.memory.alloc::<u32>(n)?;
1239
1240        // Allocate block sums array (using memory manager for budget enforcement)
1241        let mut d_block_sums = self.memory.alloc::<u32>(num_blocks as usize)?;
1242
1243        // Phase 1: Block-level exclusive scans + collect block totals
1244        let phase1_fn = device
1245            .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE1)
1246            .ok_or_else(|| {
1247                XlogError::Kernel("Failed to get multiblock_scan_phase1 kernel".to_string())
1248            })?;
1249
1250        // SAFETY: Kernel parameters match expected signature:
1251        // multiblock_scan_phase1(const uint8_t* mask, uint32_t* prefix_sum, uint32_t* block_sums, uint32_t n)
1252        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1253        unsafe {
1254            phase1_fn.clone().launch(
1255                LaunchConfig {
1256                    grid_dim: (num_blocks, 1, 1),
1257                    block_dim: (block_size, 1, 1),
1258                    shared_mem_bytes: 0,
1259                },
1260                (&d_mask, &d_prefix_sum, &d_block_sums, n as u32),
1261            )
1262        }
1263        .map_err(|e| {
1264            XlogError::Kernel(format!("Failed to launch multiblock_scan_phase1: {}", e))
1265        })?;
1266
1267        // Phase 2: Scan block sums (only if we have more than 1 block)
1268        if num_blocks > 1 {
1269            self.multiblock_scan_u32_inplace(&mut d_block_sums, num_blocks)?;
1270
1271            // Phase 3: Add block offsets
1272            let phase3_fn = device
1273                .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE3)
1274                .ok_or_else(|| {
1275                    XlogError::Kernel("Failed to get multiblock_scan_phase3 kernel".to_string())
1276                })?;
1277
1278            // SAFETY: Kernel parameters match expected signature:
1279            // multiblock_scan_phase3(uint32_t* prefix_sum, const uint32_t* block_offsets, uint32_t n)
1280            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1281            unsafe {
1282                phase3_fn.clone().launch(
1283                    LaunchConfig {
1284                        grid_dim: (num_blocks, 1, 1),
1285                        block_dim: (block_size, 1, 1),
1286                        shared_mem_bytes: 0,
1287                    },
1288                    (&d_prefix_sum, &d_block_sums, n as u32),
1289                )
1290            }
1291            .map_err(|e| {
1292                XlogError::Kernel(format!("Failed to launch multiblock_scan_phase3: {}", e))
1293            })?;
1294        }
1295
1296        // Synchronize and download results
1297        self.device.synchronize()?;
1298
1299        let prefix_sum = device
1300            .dtoh_sync_copy(&d_prefix_sum)
1301            .map_err(|e| XlogError::Kernel(format!("Failed to download prefix_sum: {}", e)))?;
1302
1303        // Compute count from the last prefix sum value + last mask value
1304        let count = prefix_sum[n - 1] + mask[n - 1] as u32;
1305
1306        Ok((prefix_sum, count))
1307    }
1308
1309    /// Filter buffer by pre-computed mask.
1310    ///
1311    /// # Arguments
1312    /// * `input` - The input buffer to filter
1313    /// * `mask` - Mask slice where non-zero means keep the row
1314    ///
1315    /// # Errors
1316    /// Returns error if mask length doesn't match buffer rows.
1317    pub fn filter_by_mask(&self, input: &CudaBuffer, mask: &[u8]) -> Result<CudaBuffer> {
1318        if input.num_rows() == 0 {
1319            return self.create_empty_buffer(input.schema.clone());
1320        }
1321
1322        let n = input.num_rows() as usize;
1323        if mask.len() != n {
1324            return Err(XlogError::Kernel(format!(
1325                "Mask length {} doesn't match buffer rows {}",
1326                mask.len(),
1327                n
1328            )));
1329        }
1330
1331        // Compute prefix sum and count
1332        let (prefix_sum, count) = self.prefix_sum_mask(mask)?;
1333
1334        if count == 0 {
1335            return self.create_empty_buffer(input.schema.clone());
1336        }
1337
1338        // Compact all columns using mask
1339        self.compact_buffer_by_mask(input, mask, &prefix_sum, count as u64)
1340    }
1341
1342    /// Compact buffer columns using mask and prefix sum indices
1343    fn compact_buffer_by_mask(
1344        &self,
1345        input: &CudaBuffer,
1346        mask: &[u8],
1347        prefix_sum: &[u32],
1348        output_count: u64,
1349    ) -> Result<CudaBuffer> {
1350        // Upload mask and prefix sum to GPU
1351        let d_mask = self
1352            .htod_sync_copy_tracked(mask)
1353            .map_err(|e| XlogError::Kernel(format!("Failed to upload mask: {}", e)))?;
1354        let d_prefix_sum = self
1355            .htod_sync_copy_tracked(prefix_sum)
1356            .map_err(|e| XlogError::Kernel(format!("Failed to upload prefix_sum: {}", e)))?;
1357
1358        self.compact_buffer_by_device_mask(input, &d_mask, &d_prefix_sum, output_count)
1359    }
1360
1361    /// Compact a buffer using a device-resident mask.
1362    ///
1363    /// Computes prefix sum and output count fully on-device.
1364    /// Strict-recorder variant of
1365    /// [`Self::compact_buffer_by_device_mask_counted`] — the
1366    /// first migrated COMPACT path.
1367    ///
1368    /// The compact pipeline is a multi-kernel chain:
1369    /// `mask_clamp_rows` → `multiblock_scan_phase1` →
1370    /// `multiblock_scan_u32_inplace_on_stream` (recursive,
1371    /// only when `num_blocks > 1`) → `multiblock_scan_phase3` →
1372    /// `capture_compact_count` → host scalar read of
1373    /// `d_out_count` → per-column `compact_bytes_by_mask`.
1374    /// **Every kernel runs on the same explicit `launch_stream`
1375    /// via `launch_on_stream`**, and the host scalar read at
1376    /// the chain's middle is explicitly ordered by
1377    /// `cu_stream.synchronize()` — non-blocking streams do
1378    /// NOT get default-stream implicit ordering.
1379    ///
1380    /// # Strict-mode contract
1381    /// * Requires the provider's manager to be built via
1382    ///   [`crate::GpuMemoryManager::with_runtime`]; otherwise
1383    ///   returns `XlogError::Kernel` before any allocation.
1384    /// * `d_mask` is recorded as a read.
1385    /// * `input.num_rows_device()` is recorded as a read.
1386    /// * Each `input.column(i)` is recorded as a read; external
1387    ///   columns on any side are rejected at preflight, before
1388    ///   any CUDA work is enqueued.
1389    /// * Every fresh runtime-backed allocation that this
1390    ///   function makes (`d_mask_clamped`, `d_prefix_sum`,
1391    ///   `d_block_sums`, `d_out_count`, each `dst_col`) is
1392    ///   recorded via `write` BEFORE the
1393    ///   kernel chain enqueues. Locals that drop at end-of-scope
1394    ///   (`d_mask_clamped`, `d_prefix_sum`, `d_block_sums`)
1395    ///   stay safe because the runtime's deallocate queues
1396    ///   `cuStreamWaitEvent(alloc_stream, recorded_event)`
1397    ///   BEFORE `cuMemFreeAsync`, gating the free on the
1398    ///   launch_stream chain.
1399    /// * Intermediate `block_sums` allocations created by the
1400    ///   recursive scan helper are recorded directly inside the
1401    ///   helper (they don't outlive the helper call).
1402    ///
1403    /// # Errors
1404    ///   * `XlogError::Kernel` if the manager has no runtime,
1405    ///     or if `launch_stream` does not resolve.
1406    ///   * `XlogError::Kernel` from preflight (external column
1407    ///     on any side, unsupported active resource).
1408    ///   * `XlogError::Kernel` from any underlying CUDA launch
1409    ///     or from the launch_stream synchronize before the
1410    ///     host scalar read.
1411    ///   * `XlogError::Kernel` from commit on transient
1412    ///     `record_block_use` failure.
1413    pub fn compact_buffer_by_device_mask_counted_recorded(
1414        &self,
1415        input: &CudaBuffer,
1416        d_mask: &TrackedCudaSlice<u8>,
1417        launch_stream: StreamId,
1418    ) -> Result<CudaBuffer> {
1419        let runtime = self.memory.runtime().ok_or_else(|| {
1420            XlogError::Kernel(
1421                "compact_buffer_by_device_mask_counted_recorded requires a \
1422                 runtime-backed GpuMemoryManager (constructed via with_runtime)"
1423                    .to_string(),
1424            )
1425        })?;
1426        let pool = runtime.stream_pool();
1427        let cu_stream = pool.resolve(launch_stream).ok_or_else(|| {
1428            XlogError::Kernel(format!(
1429                "compact_buffer_by_device_mask_counted_recorded: launch_stream \
1430                 StreamId({}) does not resolve",
1431                launch_stream.0
1432            ))
1433        })?;
1434
1435        let n = input.num_rows() as u32;
1436        if n == 0 {
1437            return self.create_empty_buffer(input.schema.clone());
1438        }
1439        // The bound `d_mask.len() >= n (row_cap)` is too
1440        // strict — `mask_clamp_rows` only reads `d_mask[i]`
1441        // for `i < num_rows_device` (logical count). Real
1442        // callers (hash_join_semi/anti recorded) pass masks
1443        // sized to logical count which can be < row_cap. We
1444        // require only that the mask is non-empty; OOB reads
1445        // are bounded by the kernel's own check against the
1446        // device-resident logical row count.
1447        if d_mask.is_empty() {
1448            return Err(XlogError::Kernel(
1449                "compact_buffer_by_device_mask_counted_recorded: empty d_mask".to_string(),
1450            ));
1451        }
1452
1453        let device = self.device.inner();
1454        let block_size = 256u32;
1455        let num_blocks = n.div_ceil(block_size);
1456        let row_cap = u64::from(n);
1457
1458        // Allocate ALL fresh runtime-backed buffers up front,
1459        // BEFORE the recorder is constructed. The recorder
1460        // snapshots each block's identity at record time and
1461        // drops the slice borrow, so the buffers can be
1462        // mutably borrowed by kernel launches after preflight.
1463        // Output column sizes are known up front from
1464        // `row_cap = n`, so this
1465        // is sound — the host scalar read of `d_out_count` only
1466        // tells us `output_rows`, which we use as metadata, not
1467        // for sizing.
1468        let mut d_mask_clamped = self.memory.alloc::<u8>(n as usize)?;
1469        let d_prefix_sum = self.memory.alloc::<u32>(n as usize)?;
1470        let mut d_block_sums = self.memory.alloc::<u32>(num_blocks as usize)?;
1471        let mut d_out_count = self.memory.alloc::<u32>(1)?;
1472
1473        let mut dst_cols: Vec<TrackedCudaSlice<u8>> = Vec::with_capacity(input.columns.len());
1474        for col_idx in 0..input.columns.len() {
1475            let elem_size = input
1476                .schema
1477                .column_type(col_idx)
1478                .map(|t| t.size_bytes())
1479                .unwrap_or(4);
1480            let output_bytes = (row_cap as usize) * elem_size;
1481            dst_cols.push(self.memory.alloc::<u8>(output_bytes)?);
1482        }
1483
1484        // Build recorder, record reads BEFORE preflight.
1485        let mut rec = LaunchRecorder::new_strict(launch_stream);
1486        rec.read(d_mask);
1487        rec.read(input.num_rows_device());
1488        for col_idx in 0..input.columns.len() {
1489            let src_col = input
1490                .column(col_idx)
1491                .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
1492            rec.read_column(src_col);
1493        }
1494        rec.write(&d_mask_clamped);
1495        rec.write(&d_prefix_sum);
1496        rec.write(&d_block_sums);
1497        rec.write(&d_out_count);
1498        for dst_col in &dst_cols {
1499            rec.write(dst_col);
1500        }
1501        rec.preflight(runtime).map_err(|e| {
1502            XlogError::Kernel(format!(
1503                "compact_buffer_by_device_mask_counted_recorded: launch recorder \
1504                 preflight failed: {}",
1505                e
1506            ))
1507        })?;
1508
1509        // Step 1: mask_clamp_rows on launch_stream.
1510        let clamp_fn = device
1511            .get_func(FILTER_MODULE, filter_kernels::MASK_CLAMP_ROWS)
1512            .ok_or_else(|| XlogError::Kernel("mask_clamp_rows kernel not found".to_string()))?;
1513        // SAFETY: mask_clamp_rows(in_mask, num_rows_device, row_cap, out_mask)
1514        unsafe {
1515            clamp_fn.clone().launch_on_stream(
1516                &cu_stream,
1517                LaunchConfig {
1518                    grid_dim: (num_blocks, 1, 1),
1519                    block_dim: (block_size, 1, 1),
1520                    shared_mem_bytes: 0,
1521                },
1522                (d_mask, input.num_rows_device(), n, &mut d_mask_clamped),
1523            )
1524        }
1525        .map_err(|e| XlogError::Kernel(format!("mask_clamp_rows (on_stream) failed: {}", e)))?;
1526
1527        // Step 2: multiblock_scan_phase1 on launch_stream.
1528        let phase1_fn = device
1529            .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE1)
1530            .ok_or_else(|| {
1531                XlogError::Kernel("Failed to get multiblock_scan_phase1 kernel".to_string())
1532            })?;
1533        // SAFETY: multiblock_scan_phase1(const u8 mask, u32 prefix_sum, u32 block_sums, u32 n)
1534        unsafe {
1535            phase1_fn.clone().launch_on_stream(
1536                &cu_stream,
1537                LaunchConfig {
1538                    grid_dim: (num_blocks, 1, 1),
1539                    block_dim: (block_size, 1, 1),
1540                    shared_mem_bytes: 0,
1541                },
1542                (&d_mask_clamped, &d_prefix_sum, &d_block_sums, n),
1543            )
1544        }
1545        .map_err(|e| {
1546            XlogError::Kernel(format!("multiblock_scan_phase1 (on_stream) failed: {}", e))
1547        })?;
1548
1549        // Step 3: scan inplace on block_sums + phase3 propagate
1550        // (only when there is more than one block).
1551        if num_blocks > 1 {
1552            self.multiblock_scan_u32_inplace_on_stream(
1553                &mut d_block_sums,
1554                num_blocks,
1555                &cu_stream,
1556                launch_stream,
1557                runtime,
1558            )?;
1559
1560            let phase3_fn = device
1561                .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE3)
1562                .ok_or_else(|| {
1563                    XlogError::Kernel("Failed to get multiblock_scan_phase3 kernel".to_string())
1564                })?;
1565            // SAFETY: multiblock_scan_phase3(prefix_sum, block_offsets, n)
1566            unsafe {
1567                phase3_fn.clone().launch_on_stream(
1568                    &cu_stream,
1569                    LaunchConfig {
1570                        grid_dim: (num_blocks, 1, 1),
1571                        block_dim: (block_size, 1, 1),
1572                        shared_mem_bytes: 0,
1573                    },
1574                    (&d_prefix_sum, &d_block_sums, n),
1575                )
1576            }
1577            .map_err(|e| {
1578                XlogError::Kernel(format!("multiblock_scan_phase3 (on_stream) failed: {}", e))
1579            })?;
1580        }
1581
1582        // Step 4: capture_compact_count on launch_stream.
1583        // (`d_out_count` was pre-allocated up front; see header.)
1584        let capture_fn = device
1585            .get_func(FILTER_MODULE, filter_kernels::CAPTURE_COMPACT_COUNT)
1586            .ok_or_else(|| {
1587                XlogError::Kernel("capture_compact_count kernel not found".to_string())
1588            })?;
1589        // SAFETY: capture_compact_count(prefix_sum, mask, n, out_count)
1590        //
1591        // Use the clamped mask, not the caller's original mask.
1592        // Some recorded callers provide a mask sized to the
1593        // device-resident logical row count while `n` is the
1594        // buffer row capacity. `mask_clamp_rows` expanded that
1595        // shorter domain into a row-capacity-sized mask with
1596        // slack rows forced to zero; every downstream consumer
1597        // in this compaction chain must use that expanded mask.
1598        unsafe {
1599            capture_fn.clone().launch_on_stream(
1600                &cu_stream,
1601                LaunchConfig {
1602                    grid_dim: (1, 1, 1),
1603                    block_dim: (1, 1, 1),
1604                    shared_mem_bytes: 0,
1605                },
1606                (&d_prefix_sum, &d_mask_clamped, n, &mut d_out_count),
1607            )
1608        }
1609        .map_err(|e| {
1610            XlogError::Kernel(format!("capture_compact_count (on_stream) failed: {}", e))
1611        })?;
1612
1613        // Explicit ordering for the host scalar read of
1614        // `d_out_count`. `dtoh_scalar_untracked` routes its
1615        // copy through the device's default cudarc stream,
1616        // which does NOT get implicit synchronization with the
1617        // non-blocking `launch_stream`. Without this barrier
1618        // we would race the still-pending capture kernel.
1619        cu_stream.synchronize().map_err(|e| {
1620            XlogError::Kernel(format!(
1621                "compact_buffer_by_device_mask_counted_recorded: launch_stream \
1622                 synchronize before host scalar read failed: {}",
1623                e
1624            ))
1625        })?;
1626
1627        let output_rows = self.dtoh_scalar_untracked(&d_out_count, 0)? as u64;
1628
1629        // Step 5: per-column compact_bytes_by_mask on
1630        // launch_stream. Only run when output_rows > 0; an
1631        // empty mask still allocates row_cap-sized columns
1632        // (matching legacy) but skips the kernel.
1633        if output_rows > 0 {
1634            let compact_fn = device
1635                .get_func(FILTER_MODULE, filter_kernels::COMPACT_BYTES_BY_MASK)
1636                .ok_or_else(|| {
1637                    XlogError::Kernel("compact_bytes_by_mask kernel not found".to_string())
1638                })?;
1639            let grid_size = n.div_ceil(block_size);
1640            let cfg = LaunchConfig {
1641                grid_dim: (grid_size, 1, 1),
1642                block_dim: (block_size, 1, 1),
1643                shared_mem_bytes: 0,
1644            };
1645            for (col_idx, dst_col) in dst_cols.iter().enumerate() {
1646                let src_col = input
1647                    .column(col_idx)
1648                    .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
1649                let elem_size = input
1650                    .schema
1651                    .column_type(col_idx)
1652                    .map(|t| t.size_bytes())
1653                    .unwrap_or(4) as u32;
1654                // SAFETY: compact_bytes_by_mask(input, mask, prefix_sum, n, elem_size, output)
1655                //
1656                // Same domain rule as capture_compact_count:
1657                // compact over the row-capacity-sized clamped
1658                // mask so rows >= logical_count are never
1659                // materialized from valid-looking slack.
1660                unsafe {
1661                    compact_fn.clone().launch_on_stream(
1662                        &cu_stream,
1663                        cfg,
1664                        (
1665                            src_col,
1666                            &d_mask_clamped,
1667                            &d_prefix_sum,
1668                            n,
1669                            elem_size,
1670                            dst_col,
1671                        ),
1672                    )
1673                }
1674                .map_err(|e| {
1675                    XlogError::Kernel(format!("compact_bytes_by_mask (on_stream) failed: {}", e))
1676                })?;
1677            }
1678        }
1679
1680        // Record fresh writes via the post-preflight escape
1681        // hatch. ALL fresh runtime-backed allocations made by
1682        // this function are recorded so that drops at
1683        // end-of-scope (or on the returned buffer's drop) are
1684        // correctly serialized with the launch_stream chain.
1685        rec.commit(runtime).map_err(|e| {
1686            XlogError::Kernel(format!(
1687                "compact_buffer_by_device_mask_counted_recorded: launch recorder \
1688                 commit failed: {}",
1689                e
1690            ))
1691        })?;
1692
1693        let new_columns: Vec<CudaColumn> = dst_cols.into_iter().map(|s| s.into()).collect();
1694        Ok(CudaBuffer::from_columns_with_host_count(
1695            new_columns,
1696            row_cap,
1697            d_out_count,
1698            input.schema.clone(),
1699            output_rows as u32,
1700        ))
1701    }
1702
1703    pub fn compact_buffer_by_device_mask_counted(
1704        &self,
1705        input: &CudaBuffer,
1706        d_mask: &TrackedCudaSlice<u8>,
1707    ) -> Result<CudaBuffer> {
1708        let n = input.num_rows() as u32;
1709        if n == 0 {
1710            return self.create_empty_buffer(input.schema.clone());
1711        }
1712        if n as usize > d_mask.len() {
1713            return Err(XlogError::Kernel(format!(
1714                "compact_buffer_by_device_mask_counted: mask len {} < rows {}",
1715                d_mask.len(),
1716                n
1717            )));
1718        }
1719
1720        let device = self.device.inner();
1721        let block_size = 256u32;
1722        let num_blocks = n.div_ceil(block_size);
1723
1724        let mut d_mask_clamped = self.memory.alloc::<u8>(n as usize)?;
1725        let clamp_fn = device
1726            .get_func(FILTER_MODULE, filter_kernels::MASK_CLAMP_ROWS)
1727            .ok_or_else(|| XlogError::Kernel("mask_clamp_rows kernel not found".to_string()))?;
1728
1729        // SAFETY: mask_clamp_rows(in_mask, num_rows_device, row_cap, out_mask)
1730        unsafe {
1731            clamp_fn.clone().launch(
1732                LaunchConfig {
1733                    grid_dim: (num_blocks, 1, 1),
1734                    block_dim: (block_size, 1, 1),
1735                    shared_mem_bytes: 0,
1736                },
1737                (d_mask, input.num_rows_device(), n, &mut d_mask_clamped),
1738            )
1739        }
1740        .map_err(|e| XlogError::Kernel(format!("mask_clamp_rows failed: {}", e)))?;
1741
1742        let d_prefix_sum = self.memory.alloc::<u32>(n as usize)?;
1743        let mut d_block_sums = self.memory.alloc::<u32>(num_blocks as usize)?;
1744
1745        let phase1_fn = device
1746            .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE1)
1747            .ok_or_else(|| {
1748                XlogError::Kernel("Failed to get multiblock_scan_phase1 kernel".to_string())
1749            })?;
1750
1751        // SAFETY: multiblock_scan_phase1(const uint8_t* mask, uint32_t* prefix_sum, uint32_t* block_sums, uint32_t n)
1752        unsafe {
1753            phase1_fn.clone().launch(
1754                LaunchConfig {
1755                    grid_dim: (num_blocks, 1, 1),
1756                    block_dim: (block_size, 1, 1),
1757                    shared_mem_bytes: 0,
1758                },
1759                (&d_mask_clamped, &d_prefix_sum, &d_block_sums, n),
1760            )
1761        }
1762        .map_err(|e| XlogError::Kernel(format!("multiblock_scan_phase1 failed: {}", e)))?;
1763
1764        if num_blocks > 1 {
1765            self.multiblock_scan_u32_inplace(&mut d_block_sums, num_blocks)?;
1766
1767            let phase3_fn = device
1768                .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE3)
1769                .ok_or_else(|| {
1770                    XlogError::Kernel("Failed to get multiblock_scan_phase3 kernel".to_string())
1771                })?;
1772
1773            // SAFETY: multiblock_scan_phase3(prefix_sum, block_offsets, n)
1774            unsafe {
1775                phase3_fn.clone().launch(
1776                    LaunchConfig {
1777                        grid_dim: (num_blocks, 1, 1),
1778                        block_dim: (block_size, 1, 1),
1779                        shared_mem_bytes: 0,
1780                    },
1781                    (&d_prefix_sum, &d_block_sums, n),
1782                )
1783            }
1784            .map_err(|e| XlogError::Kernel(format!("multiblock_scan_phase3 failed: {}", e)))?;
1785        }
1786
1787        let d_out_count = self.capture_compact_count(&d_prefix_sum, d_mask, n)?;
1788
1789        self.compact_buffer_by_device_mask_device_count(input, d_mask, &d_prefix_sum, d_out_count)
1790    }
1791
1792    pub(crate) fn capture_compact_count(
1793        &self,
1794        d_prefix_sum: &cudarc::driver::CudaSlice<u32>,
1795        d_mask: &cudarc::driver::CudaSlice<u8>,
1796        n: u32,
1797    ) -> Result<TrackedCudaSlice<u32>> {
1798        let mut d_out_count = self.memory.alloc::<u32>(1)?;
1799        let device = self.device.inner();
1800        let capture_fn = device
1801            .get_func(FILTER_MODULE, filter_kernels::CAPTURE_COMPACT_COUNT)
1802            .ok_or_else(|| {
1803                XlogError::Kernel("capture_compact_count kernel not found".to_string())
1804            })?;
1805        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1806        unsafe {
1807            capture_fn.clone().launch(
1808                LaunchConfig {
1809                    grid_dim: (1, 1, 1),
1810                    block_dim: (1, 1, 1),
1811                    shared_mem_bytes: 0,
1812                },
1813                (d_prefix_sum, d_mask, n, &mut d_out_count),
1814            )
1815        }
1816        .map_err(|e| XlogError::Kernel(format!("capture_compact_count failed: {}", e)))?;
1817        Ok(d_out_count)
1818    }
1819
1820    pub(crate) fn compact_buffer_by_device_mask_device_count(
1821        &self,
1822        input: &CudaBuffer,
1823        d_mask: &cudarc::driver::CudaSlice<u8>,
1824        d_prefix_sum: &cudarc::driver::CudaSlice<u32>,
1825        d_out_count: TrackedCudaSlice<u32>,
1826    ) -> Result<CudaBuffer> {
1827        let mask_len = u32::try_from(d_mask.len()).map_err(|_| {
1828            XlogError::Kernel(format!(
1829                "compact_buffer_by_device_mask_device_count: mask len {} exceeds u32::MAX",
1830                d_mask.len()
1831            ))
1832        })?;
1833        let prefix_len = u32::try_from(d_prefix_sum.len()).map_err(|_| {
1834            XlogError::Kernel(format!(
1835                "compact_buffer_by_device_mask_device_count: prefix sum len {} exceeds u32::MAX",
1836                d_prefix_sum.len()
1837            ))
1838        })?;
1839        if prefix_len < mask_len {
1840            return Err(XlogError::Kernel(format!(
1841                "compact_buffer_by_device_mask_device_count: prefix sum len {} < mask len {}",
1842                prefix_len, mask_len
1843            )));
1844        }
1845        if mask_len as u64 > input.num_rows() {
1846            return Err(XlogError::Kernel(format!(
1847                "compact_buffer_by_device_mask_device_count: mask len {} > row cap {}",
1848                mask_len,
1849                input.num_rows()
1850            )));
1851        }
1852        if mask_len == 0 {
1853            return self.create_empty_buffer(input.schema.clone());
1854        }
1855        let n = mask_len;
1856        let device = self.device.inner();
1857
1858        let compact_fn = device
1859            .get_func(FILTER_MODULE, filter_kernels::COMPACT_BYTES_BY_MASK)
1860            .ok_or_else(|| {
1861                XlogError::Kernel("compact_bytes_by_mask kernel not found".to_string())
1862            })?;
1863
1864        let block_size = 256u32;
1865        let grid_size = n.div_ceil(block_size);
1866        let config = LaunchConfig {
1867            grid_dim: (grid_size, 1, 1),
1868            block_dim: (block_size, 1, 1),
1869            shared_mem_bytes: 0,
1870        };
1871
1872        // The compact kernel only writes `output_rows` elements, but downstream
1873        // metadata must still remember the logical span covered by the mask.
1874        // This keeps host-visible `row_cap` aligned with the masked prefix while
1875        // the device-side row count tracks the actual compacted cardinality.
1876        let output_rows = self.dtoh_scalar_untracked(&d_out_count, 0)? as u64;
1877        let row_cap = u64::from(n);
1878
1879        if output_rows == 0 {
1880            let mut new_columns = Vec::with_capacity(input.columns.len());
1881            for col_idx in 0..input.columns.len() {
1882                let elem_size = input
1883                    .schema
1884                    .column_type(col_idx)
1885                    .map(|t| t.size_bytes())
1886                    .unwrap_or(4);
1887                let output_bytes = (row_cap as usize) * elem_size;
1888                new_columns.push(self.memory.alloc::<u8>(output_bytes)?.into());
1889            }
1890            let d_zero_rows = self.upload_device_row_count(0)?;
1891            return Ok(CudaBuffer::from_columns_with_host_count(
1892                new_columns,
1893                row_cap,
1894                d_zero_rows,
1895                input.schema.clone(),
1896                0,
1897            ));
1898        }
1899
1900        let mut new_columns = Vec::with_capacity(input.columns.len());
1901        for col_idx in 0..input.columns.len() {
1902            let src_col = input
1903                .column(col_idx)
1904                .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
1905
1906            let elem_size = input
1907                .schema
1908                .column_type(col_idx)
1909                .map(|t| t.size_bytes())
1910                .unwrap_or(4) as u32;
1911
1912            let output_bytes = (row_cap as usize) * (elem_size as usize);
1913            let dst_col = self.memory.alloc::<u8>(output_bytes)?;
1914
1915            // SAFETY: Kernel signature matches:
1916            // compact_bytes_by_mask(input, mask, prefix_sum, num_rows, elem_size, output)
1917            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1918            unsafe {
1919                compact_fn.clone().launch(
1920                    config,
1921                    (src_col, d_mask, d_prefix_sum, n, elem_size, &dst_col),
1922                )
1923            }
1924            .map_err(|e| XlogError::Kernel(format!("compact_bytes_by_mask failed: {}", e)))?;
1925
1926            new_columns.push(dst_col.into());
1927        }
1928
1929        self.device.synchronize()?;
1930
1931        Ok(CudaBuffer::from_columns_with_host_count(
1932            new_columns,
1933            row_cap,
1934            d_out_count,
1935            input.schema.clone(),
1936            output_rows as u32,
1937        ))
1938    }
1939
1940    fn compact_buffer_by_device_mask(
1941        &self,
1942        input: &CudaBuffer,
1943        d_mask: &cudarc::driver::CudaSlice<u8>,
1944        d_prefix_sum: &cudarc::driver::CudaSlice<u32>,
1945        output_count: u64,
1946    ) -> Result<CudaBuffer> {
1947        let n = input.num_rows() as u32;
1948        let device = self.device.inner();
1949
1950        // Get compact kernel
1951        let compact_fn = device
1952            .get_func(FILTER_MODULE, filter_kernels::COMPACT_BYTES_BY_MASK)
1953            .ok_or_else(|| {
1954                XlogError::Kernel("compact_bytes_by_mask kernel not found".to_string())
1955            })?;
1956
1957        let block_size = 256u32;
1958        let grid_size = n.div_ceil(block_size);
1959        let config = LaunchConfig {
1960            grid_dim: (grid_size, 1, 1),
1961            block_dim: (block_size, 1, 1),
1962            shared_mem_bytes: 0,
1963        };
1964
1965        // Compact each column
1966        let mut new_columns = Vec::with_capacity(input.columns.len());
1967        for col_idx in 0..input.columns.len() {
1968            let src_col = input
1969                .column(col_idx)
1970                .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
1971
1972            let elem_size = input
1973                .schema
1974                .column_type(col_idx)
1975                .map(|t| t.size_bytes())
1976                .unwrap_or(4) as u32;
1977
1978            let output_bytes = (output_count as usize) * (elem_size as usize);
1979            let dst_col = self.memory.alloc::<u8>(output_bytes)?;
1980
1981            // SAFETY: Kernel signature matches:
1982            // compact_bytes_by_mask(input, mask, prefix_sum, num_rows, elem_size, output)
1983            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1984            unsafe {
1985                compact_fn.clone().launch(
1986                    config,
1987                    (src_col, d_mask, d_prefix_sum, n, elem_size, &dst_col),
1988                )
1989            }
1990            .map_err(|e| XlogError::Kernel(format!("compact_bytes_by_mask failed: {}", e)))?;
1991
1992            new_columns.push(dst_col.into());
1993        }
1994
1995        self.device.synchronize()?;
1996
1997        self.buffer_from_columns(new_columns, output_count, input.schema.clone())
1998    }
1999
2000    pub fn filter_by_device_mask(
2001        &self,
2002        input: &CudaBuffer,
2003        d_mask: &cudarc::driver::CudaSlice<u8>,
2004    ) -> Result<CudaBuffer> {
2005        if input.is_empty() {
2006            return self.create_empty_buffer(input.schema().clone());
2007        }
2008
2009        if input.num_rows() > u32::MAX as u64 {
2010            return Err(XlogError::Kernel(format!(
2011                "Device-mask filtering supports at most {} rows, got {}",
2012                u32::MAX,
2013                input.num_rows()
2014            )));
2015        }
2016
2017        let n = input.num_rows() as u32;
2018        let device = self.device.inner();
2019
2020        let block_size = 256u32;
2021        let num_blocks = n.div_ceil(block_size);
2022
2023        let mut d_mask_clamped = self.memory.alloc::<u8>(n as usize)?;
2024        let clamp_fn = device
2025            .get_func(FILTER_MODULE, filter_kernels::MASK_CLAMP_ROWS)
2026            .ok_or_else(|| XlogError::Kernel("mask_clamp_rows kernel not found".to_string()))?;
2027
2028        // SAFETY: mask_clamp_rows(in_mask, num_rows_device, row_cap, out_mask)
2029        unsafe {
2030            clamp_fn.clone().launch(
2031                LaunchConfig {
2032                    grid_dim: (num_blocks, 1, 1),
2033                    block_dim: (block_size, 1, 1),
2034                    shared_mem_bytes: 0,
2035                },
2036                (d_mask, input.num_rows_device(), n, &mut d_mask_clamped),
2037            )
2038        }
2039        .map_err(|e| XlogError::Kernel(format!("mask_clamp_rows failed: {}", e)))?;
2040
2041        let d_prefix_sum = self.memory.alloc::<u32>(n as usize)?;
2042        let mut d_block_sums = self.memory.alloc::<u32>(num_blocks as usize)?;
2043
2044        let phase1_fn = device
2045            .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE1)
2046            .ok_or_else(|| {
2047                XlogError::Kernel("Failed to get multiblock_scan_phase1 kernel".to_string())
2048            })?;
2049
2050        // SAFETY: multiblock_scan_phase1(const uint8_t* mask, uint32_t* prefix_sum, uint32_t* block_sums, uint32_t n)
2051        unsafe {
2052            phase1_fn.clone().launch(
2053                LaunchConfig {
2054                    grid_dim: (num_blocks, 1, 1),
2055                    block_dim: (block_size, 1, 1),
2056                    shared_mem_bytes: 0,
2057                },
2058                (&d_mask_clamped, &d_prefix_sum, &d_block_sums, n),
2059            )
2060        }
2061        .map_err(|e| XlogError::Kernel(format!("multiblock_scan_phase1 failed: {}", e)))?;
2062
2063        if num_blocks > 1 {
2064            self.multiblock_scan_u32_inplace(&mut d_block_sums, num_blocks)?;
2065
2066            let phase3_fn = device
2067                .get_func(SCAN_MODULE, scan_kernels::MULTIBLOCK_SCAN_PHASE3)
2068                .ok_or_else(|| {
2069                    XlogError::Kernel("Failed to get multiblock_scan_phase3 kernel".to_string())
2070                })?;
2071
2072            // SAFETY: multiblock_scan_phase3(uint32_t* prefix_sum, const uint32_t* block_offsets, uint32_t n)
2073            unsafe {
2074                phase3_fn.clone().launch(
2075                    LaunchConfig {
2076                        grid_dim: (num_blocks, 1, 1),
2077                        block_dim: (block_size, 1, 1),
2078                        shared_mem_bytes: 0,
2079                    },
2080                    (&d_prefix_sum, &d_block_sums, n),
2081                )
2082            }
2083            .map_err(|e| XlogError::Kernel(format!("multiblock_scan_phase3 failed: {}", e)))?;
2084        }
2085
2086        self.device.synchronize()?;
2087
2088        let d_out_count = self.capture_compact_count(&d_prefix_sum, &d_mask_clamped, n)?;
2089        self.compact_buffer_by_device_mask_device_count(
2090            input,
2091            &d_mask_clamped,
2092            &d_prefix_sum,
2093            d_out_count,
2094        )
2095    }
2096}