Skip to main content

xlog_cuda/provider/
wcoj_project.rs

1//! Owned, recorded helpers for WCOJ variable-ordering column
2//! projection on `CudaBuffer`s.
3//!
4//! Two helpers, both with the same shape:
5//!
6//! * [`CudaKernelProvider::wcoj_project_2col_swap_recorded`] —
7//!   2-col-only swap helper used by the variable-ordering dispatcher when
8//!   triangle non-default leaders need a `(col1, col0)` rotated
9//!   lookup atom before [`CudaKernelProvider::wcoj_layout_u32_recorded`]
10//!   can sort it.
11//!
12//! * [`CudaKernelProvider::wcoj_project_output_columns_recorded`] —
13//!   N-col arbitrary-permutation helper that the variable-ordering dispatcher
14//!   applies to the kernel-direct output buffer to remap kernel
15//!   columns into the rule's head order.
16//!
17//! Both helpers:
18//!
19//! 1. Build owned `TrackedCudaSlice<u8>` columns sized to the
20//!    source's `row_cap × dtype.size_bytes()`.
21//! 2. Allocate a fresh `num_rows_device` device scalar.
22//! 3. Use `LaunchRecorder::new_strict` + `preflight` to declare
23//!    reads (source columns + source `num_rows_device`) and
24//!    writes (new columns + new `num_rows_device`).
25//! 4. Issue per-column DtoD-async copies on the launch stream,
26//!    plus one DtoD-async copy for `num_rows_device`.
27//! 5. On any error after preflight, consume the recorder through
28//!    `abort`; once enqueue may have started, abort synchronizes
29//!    before cancelling reservations and preserves any cleanup
30//!    failure alongside the primary error.
31//! 6. Carry `cached_row_count` from `src` unchanged — logical
32//!    row count is invariant under column permutation.
33
34use cudarc::driver::sys;
35use xlog_core::{Result, Schema, XlogError};
36
37use crate::cuda_compat::DeviceSlice;
38use crate::device_runtime::StreamId;
39use crate::launch::LaunchRecorder;
40use crate::memory::{CudaColumn, TrackedCudaSlice};
41use crate::CudaBuffer;
42
43use super::CudaKernelProvider;
44
45impl CudaKernelProvider {
46    /// Produce an owned 2-col `CudaBuffer` whose columns are
47    /// `[src.col(1), src.col(0)]`. See module docs for the full
48    /// recorded / failure-drain contract.
49    ///
50    /// Used by the variable-ordering dispatcher when a triangle non-default
51    /// leader requires a col-swap before
52    /// [`Self::wcoj_layout_u32_recorded`] sorts the result. The
53    /// 4-cycle path is rotation-only and never invokes this
54    /// helper.
55    ///
56    /// # Errors
57    /// * `XlogError::Kernel` if the manager has no runtime, the
58    ///   stream doesn't resolve, the input isn't 2-col, or any
59    ///   queued DtoD copy fails. On any failure after the first
60    ///   queued copy, the launch stream is synchronized before
61    ///   the function returns.
62    pub fn wcoj_project_2col_swap_recorded(
63        &self,
64        src: &CudaBuffer,
65        launch_stream: StreamId,
66    ) -> Result<CudaBuffer> {
67        let runtime = self.memory().runtime().ok_or_else(|| {
68            XlogError::Kernel(
69                "wcoj_project_2col_swap_recorded requires a runtime-backed \
70                 GpuMemoryManager (constructed via with_runtime)"
71                    .to_string(),
72            )
73        })?;
74        let cu_stream = runtime
75            .stream_pool()
76            .resolve(launch_stream)
77            .ok_or_else(|| {
78                XlogError::Kernel(format!(
79                    "wcoj_project_2col_swap_recorded: launch_stream StreamId({}) does not resolve",
80                    launch_stream.0
81                ))
82            })?;
83
84        if src.arity() != 2 {
85            return Err(XlogError::Kernel(format!(
86                "wcoj_project_2col_swap_recorded: src must be 2-column, got arity {}",
87                src.arity()
88            )));
89        }
90
91        // Build the swapped schema. The (name, type) pairs are
92        // re-ordered; key_columns recomputes via Schema::new.
93        let swapped_schema = Schema::new(vec![
94            src.schema.columns[1].clone(),
95            src.schema.columns[0].clone(),
96        ])
97        .with_sort_labels(vec![
98            src.schema
99                .column_sort_label(1)
100                .unwrap_or("col1")
101                .to_string(),
102            src.schema
103                .column_sort_label(0)
104                .unwrap_or("col0")
105                .to_string(),
106        ])
107        .expect("swapped sort labels match schema arity");
108
109        // Empty buffer fast-path: arity=2 with row_cap=0 produces
110        // an empty owned buffer with the swapped schema. No DtoD
111        // copy is queued; failure-drain is moot.
112        if src.row_cap == 0 {
113            return self.create_empty_buffer(swapped_schema);
114        }
115
116        // Allocate the two output column buffers (sized in bytes
117        // from the source) plus a fresh num_rows_device scalar.
118        // SAFETY: src.column(0/1) are guaranteed Some (arity == 2);
119        // their byte length matches row_cap × dtype.size_bytes()
120        // by CudaBuffer construction invariants.
121        let bytes_col0 = src.column(0).expect("src.col0").len();
122        let bytes_col1 = src.column(1).expect("src.col1").len();
123        let new_col0: TrackedCudaSlice<u8> = self.memory.alloc::<u8>(bytes_col1)?;
124        let new_col1: TrackedCudaSlice<u8> = self.memory.alloc::<u8>(bytes_col0)?;
125        let new_num_rows: TrackedCudaSlice<u32> = self.memory.alloc::<u32>(1)?;
126
127        let mut rec = LaunchRecorder::new_strict(launch_stream);
128        rec.read_column(src.column(0).expect("src.col0"));
129        rec.read_column(src.column(1).expect("src.col1"));
130        rec.read(src.num_rows_device());
131        rec.write(&new_col0);
132        rec.write(&new_col1);
133        rec.write(&new_num_rows);
134        rec.preflight(runtime).map_err(|e| {
135            XlogError::Kernel(format!(
136                "wcoj_project_2col_swap_recorded: launch recorder preflight failed: {}",
137                e
138            ))
139        })?;
140
141        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
142            // SAFETY: all DtoD copies are between live device
143            // pointers on a stream the runtime owns. Sizes match
144            // the source columns exactly. cuMemcpyDtoDAsync_v2 is
145            // genuinely stream-asynchronous.
146            unsafe {
147                let res = sys::cuMemcpyDtoDAsync_v2(
148                    *new_col0.device_ptr(),
149                    *src.column(1).expect("src.col1").device_ptr(),
150                    bytes_col1,
151                    stream.stream().cu_stream(),
152                );
153                if res != sys::cudaError_enum::CUDA_SUCCESS {
154                    return Err(XlogError::Kernel(format!(
155                        "wcoj_project_2col_swap_recorded: dtod col1 → new_col0 failed: {:?}",
156                        res
157                    )));
158                }
159                let res = sys::cuMemcpyDtoDAsync_v2(
160                    *new_col1.device_ptr(),
161                    *src.column(0).expect("src.col0").device_ptr(),
162                    bytes_col0,
163                    stream.stream().cu_stream(),
164                );
165                if res != sys::cudaError_enum::CUDA_SUCCESS {
166                    return Err(XlogError::Kernel(format!(
167                        "wcoj_project_2col_swap_recorded: dtod col0 → new_col1 failed: {:?}",
168                        res
169                    )));
170                }
171                let res = sys::cuMemcpyDtoDAsync_v2(
172                    *new_num_rows.device_ptr(),
173                    *src.num_rows_device().device_ptr(),
174                    std::mem::size_of::<u32>(),
175                    stream.stream().cu_stream(),
176                );
177                if res != sys::cudaError_enum::CUDA_SUCCESS {
178                    return Err(XlogError::Kernel(format!(
179                        "wcoj_project_2col_swap_recorded: dtod num_rows_device failed: {:?}",
180                        res
181                    )));
182                }
183            }
184            Ok(())
185        };
186        // SAFETY: preflight retained the copied buffers and bound this stream.
187        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
188            .map_err(|error| error.into_xlog_error())?;
189
190        rec.commit().map_err(|e| {
191            XlogError::Kernel(format!(
192                "wcoj_project_2col_swap_recorded: launch recorder commit failed: {}",
193                e
194            ))
195        })?;
196
197        // Build the output buffer. Carry cached_row_count from src
198        // (logical row count is invariant under column permutation).
199        let columns: Vec<CudaColumn> = vec![new_col0.into(), new_col1.into()];
200        let buf = match src.cached_row_count() {
201            Some(host_count) => CudaBuffer::from_columns_with_host_count(
202                columns,
203                src.row_cap,
204                new_num_rows,
205                swapped_schema,
206                host_count,
207            ),
208            None => CudaBuffer::from_columns(columns, src.row_cap, new_num_rows, swapped_schema),
209        };
210        Ok(buf)
211    }
212
213    /// Produce an owned N-col `CudaBuffer` with columns
214    /// reordered per `perm` and the schema replaced with
215    /// `head_schema`.
216    ///
217    /// `perm[i]` is the source column index that becomes output
218    /// column `i`. The dispatcher uses this post-kernel to remap
219    /// the kernel-direct output (in leader's `(a, b, c[, d])`
220    /// order) into the rule's head order.
221    ///
222    /// See module docs for the full recorded / failure-drain
223    /// contract.
224    ///
225    /// # Errors
226    /// * `XlogError::Kernel` if the manager has no runtime, the
227    ///   stream doesn't resolve, `perm.len() != head_schema.arity()`,
228    ///   any `perm` index is ≥ `src.arity()`, or any queued DtoD
229    ///   copy fails. Failure-drain on Err.
230    pub fn wcoj_project_output_columns_recorded(
231        &self,
232        src: &CudaBuffer,
233        perm: &[usize],
234        head_schema: Schema,
235        launch_stream: StreamId,
236    ) -> Result<CudaBuffer> {
237        let runtime = self.memory().runtime().ok_or_else(|| {
238            XlogError::Kernel(
239                "wcoj_project_output_columns_recorded requires a runtime-backed \
240                 GpuMemoryManager (constructed via with_runtime)"
241                    .to_string(),
242            )
243        })?;
244        let cu_stream = runtime
245            .stream_pool()
246            .resolve(launch_stream)
247            .ok_or_else(|| {
248                XlogError::Kernel(format!(
249                    "wcoj_project_output_columns_recorded: launch_stream StreamId({}) does not resolve",
250                    launch_stream.0
251                ))
252            })?;
253
254        if perm.len() != head_schema.arity() {
255            return Err(XlogError::Kernel(format!(
256                "wcoj_project_output_columns_recorded: perm len {} must equal head_schema arity {}",
257                perm.len(),
258                head_schema.arity()
259            )));
260        }
261        for (i, &p) in perm.iter().enumerate() {
262            if p >= src.arity() {
263                return Err(XlogError::Kernel(format!(
264                    "wcoj_project_output_columns_recorded: perm[{}] = {} out of bounds (src arity {})",
265                    i,
266                    p,
267                    src.arity()
268                )));
269            }
270        }
271
272        // Empty-output short-circuit. WCOJ legitimately produces
273        // empty output — the helper must NOT divide-by-zero or
274        // refuse to materialize. cached_row_count == Some(0) +
275        // num_rows_device == 0 are produced via create_empty_buffer
276        // (sized to head_schema; its num_rows_device starts at 0).
277        if src.row_cap == 0 {
278            return self.create_empty_buffer(head_schema);
279        }
280
281        // Allocate one new column buffer per output position +
282        // a fresh num_rows_device scalar.
283        let mut new_columns: Vec<TrackedCudaSlice<u8>> = Vec::with_capacity(perm.len());
284        for &p in perm {
285            let bytes = src.column(p).expect("src column").len();
286            new_columns.push(self.memory.alloc::<u8>(bytes)?);
287        }
288        let new_num_rows: TrackedCudaSlice<u32> = self.memory.alloc::<u32>(1)?;
289
290        let mut rec = LaunchRecorder::new_strict(launch_stream);
291        for i in 0..src.arity() {
292            rec.read_column(src.column(i).expect("src.col"));
293        }
294        rec.read(src.num_rows_device());
295        for c in &new_columns {
296            rec.write(c);
297        }
298        rec.write(&new_num_rows);
299        rec.preflight(runtime).map_err(|e| {
300            XlogError::Kernel(format!(
301                "wcoj_project_output_columns_recorded: launch recorder preflight failed: {}",
302                e
303            ))
304        })?;
305
306        let enqueue = |stream: &crate::launch::CudaEnqueue<'_>| -> Result<()> {
307            for (i, &p) in perm.iter().enumerate() {
308                let src_col = src.column(p).expect("src column");
309                let bytes = src_col.len();
310                // SAFETY: device pointer is live, len matches source
311                // column. Stream is owned by the runtime.
312                unsafe {
313                    let res = sys::cuMemcpyDtoDAsync_v2(
314                        *new_columns[i].device_ptr(),
315                        *src_col.device_ptr(),
316                        bytes,
317                        stream.stream().cu_stream(),
318                    );
319                    if res != sys::cudaError_enum::CUDA_SUCCESS {
320                        return Err(XlogError::Kernel(format!(
321                            "wcoj_project_output_columns_recorded: dtod perm[{}] = src col {} failed: {:?}",
322                            i, p, res
323                        )));
324                    }
325                }
326            }
327            // num_rows_device DtoD-copy.
328            unsafe {
329                let res = sys::cuMemcpyDtoDAsync_v2(
330                    *new_num_rows.device_ptr(),
331                    *src.num_rows_device().device_ptr(),
332                    std::mem::size_of::<u32>(),
333                    stream.stream().cu_stream(),
334                );
335                if res != sys::cudaError_enum::CUDA_SUCCESS {
336                    return Err(XlogError::Kernel(format!(
337                        "wcoj_project_output_columns_recorded: dtod num_rows_device failed: {:?}",
338                        res
339                    )));
340                }
341            }
342            Ok(())
343        };
344        // SAFETY: preflight retained the copied buffers and bound this stream.
345        let rec = unsafe { rec.enqueue_prepared_with(&cu_stream, enqueue) }
346            .map_err(|error| error.into_xlog_error())?;
347
348        rec.commit().map_err(|e| {
349            XlogError::Kernel(format!(
350                "wcoj_project_output_columns_recorded: launch recorder commit failed: {}",
351                e
352            ))
353        })?;
354
355        let columns: Vec<CudaColumn> = new_columns.into_iter().map(|c| c.into()).collect();
356        let buf = match src.cached_row_count() {
357            Some(host_count) => CudaBuffer::from_columns_with_host_count(
358                columns,
359                src.row_cap,
360                new_num_rows,
361                head_schema,
362                host_count,
363            ),
364            None => CudaBuffer::from_columns(columns, src.row_cap, new_num_rows, head_schema),
365        };
366        Ok(buf)
367    }
368}