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}