Skip to main content

xlog_cuda/provider/
ilp.rs

1//! ILP (Inductive Logic Programming) kernel operations: credit/loss, COO fill, CSR histogram, reduce_sum.
2
3use crate::memory::{DeviceMemoryView, DeviceRead};
4
5use crate::{DeviceSlice, LaunchAsync, LaunchConfig};
6use xlog_core::{Result, ScalarType, Schema, XlogError};
7
8use super::{ilp_credit_kernels, ilp_kernels, ILP_CREDIT_MODULE, ILP_MODULE};
9use crate::memory::{CudaBuffer, CudaColumn, TrackedCudaSlice};
10
11impl super::CudaKernelProvider {
12    fn ilp_i32_view(&self, col: &CudaColumn, num_elements: usize) -> Result<DeviceMemoryView<i32>> {
13        let required_bytes = num_elements * std::mem::size_of::<i32>();
14        if col.num_bytes() < required_bytes {
15            return Err(XlogError::Kernel(format!(
16                "Column has {} bytes but {} required for {} i32 elements",
17                col.num_bytes(),
18                required_bytes,
19                num_elements
20            )));
21        }
22        let ptr = *col.device_ptr();
23        if !(ptr as usize).is_multiple_of(std::mem::align_of::<i32>()) {
24            return Err(XlogError::Kernel(
25                "Column device pointer is not i32-aligned".to_string(),
26            ));
27        }
28        // SAFETY: integer representations accept every bit pattern.
29        unsafe { col.device_view().slice(..required_bytes).cast() }
30            .ok_or_else(|| XlogError::Kernel("device column cast is invalid".into()))
31    }
32
33    fn ilp_i64_view(&self, col: &CudaColumn, num_elements: usize) -> Result<DeviceMemoryView<i64>> {
34        let required_bytes = num_elements * std::mem::size_of::<i64>();
35        if col.num_bytes() < required_bytes {
36            return Err(XlogError::Kernel(format!(
37                "Column has {} bytes but {} required for {} i64 elements",
38                col.num_bytes(),
39                required_bytes,
40                num_elements
41            )));
42        }
43        let ptr = *col.device_ptr();
44        if !(ptr as usize).is_multiple_of(std::mem::align_of::<i64>()) {
45            return Err(XlogError::Kernel(
46                "Column device pointer is not i64-aligned".to_string(),
47            ));
48        }
49        // SAFETY: integer representations accept every bit pattern.
50        unsafe { col.device_view().slice(..required_bytes).cast() }
51            .ok_or_else(|| XlogError::Kernel("device column cast is invalid".into()))
52    }
53
54    pub fn build_selected_id_mask(
55        &self,
56        ids_buf: &CudaBuffer,
57        candidate_count: usize,
58    ) -> Result<CudaBuffer> {
59        let selected_len = usize::try_from(ids_buf.num_rows())
60            .map_err(|_| XlogError::Kernel("selected id row count overflow".to_string()))?;
61        let candidate_count_u32 = u32::try_from(candidate_count).map_err(|_| {
62            XlogError::Kernel(format!(
63                "candidate count {} exceeds u32::MAX for strict sparse mask",
64                candidate_count
65            ))
66        })?;
67
68        let mut active_flags = self.memory.alloc::<u32>(candidate_count)?;
69        if candidate_count > 0 {
70            self.device
71                .inner()
72                .memset_zeros(&mut active_flags)
73                .map_err(|e| XlogError::Kernel(format!("zero strict sparse mask: {}", e)))?;
74        }
75
76        if selected_len > 0 {
77            let selected_len_u32 = u32::try_from(selected_len).map_err(|_| {
78                XlogError::Kernel(format!(
79                    "selected id count {} exceeds u32::MAX for strict sparse mask",
80                    selected_len
81                ))
82            })?;
83            let block_size = 256u32;
84            let grid_size = selected_len_u32.div_ceil(block_size);
85            let ids_col = ids_buf
86                .column(0)
87                .ok_or_else(|| XlogError::Kernel("selected id buffer has no column".to_string()))?;
88            match ids_buf.schema().column_type(0).ok_or_else(|| {
89                XlogError::Kernel("selected id buffer has no schema type".to_string())
90            })? {
91                ScalarType::U32 | ScalarType::Symbol => {
92                    let ids_view = self.column_as_u32_view(ids_col, selected_len)?;
93                    let func = self
94                        .device
95                        .inner()
96                        .get_func(ILP_MODULE, ilp_kernels::ILP_MARK_SELECTED_IDS_U32)
97                        .ok_or_else(|| {
98                            XlogError::Kernel(
99                                "ilp_mark_selected_ids_u32 kernel not found".to_string(),
100                            )
101                        })?;
102                    // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
103                    unsafe {
104                        func.clone().launch(
105                            LaunchConfig {
106                                grid_dim: (grid_size, 1, 1),
107                                block_dim: (block_size, 1, 1),
108                                shared_mem_bytes: 0,
109                            },
110                            (
111                                &ids_view,
112                                selected_len_u32,
113                                candidate_count_u32,
114                                &mut active_flags,
115                            ),
116                        )
117                    }
118                    .map_err(|e| {
119                        XlogError::Kernel(format!(
120                            "strict sparse selected-id scatter failed: {}",
121                            e
122                        ))
123                    })?;
124                }
125                ScalarType::I32 => {
126                    let ids_view = self.ilp_i32_view(ids_col, selected_len)?;
127                    let func = self
128                        .device
129                        .inner()
130                        .get_func(ILP_MODULE, ilp_kernels::ILP_MARK_SELECTED_IDS_I32)
131                        .ok_or_else(|| {
132                            XlogError::Kernel(
133                                "ilp_mark_selected_ids_i32 kernel not found".to_string(),
134                            )
135                        })?;
136                    // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
137                    unsafe {
138                        func.clone().launch(
139                            LaunchConfig {
140                                grid_dim: (grid_size, 1, 1),
141                                block_dim: (block_size, 1, 1),
142                                shared_mem_bytes: 0,
143                            },
144                            (
145                                &ids_view,
146                                selected_len_u32,
147                                candidate_count_u32,
148                                &mut active_flags,
149                            ),
150                        )
151                    }
152                    .map_err(|e| {
153                        XlogError::Kernel(format!(
154                            "strict sparse selected-id scatter failed: {}",
155                            e
156                        ))
157                    })?;
158                }
159                ScalarType::I64 => {
160                    let ids_view = self.ilp_i64_view(ids_col, selected_len)?;
161                    let func = self
162                        .device
163                        .inner()
164                        .get_func(ILP_MODULE, ilp_kernels::ILP_MARK_SELECTED_IDS_I64)
165                        .ok_or_else(|| {
166                            XlogError::Kernel(
167                                "ilp_mark_selected_ids_i64 kernel not found".to_string(),
168                            )
169                        })?;
170                    // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
171                    unsafe {
172                        func.clone().launch(
173                            LaunchConfig {
174                                grid_dim: (grid_size, 1, 1),
175                                block_dim: (block_size, 1, 1),
176                                shared_mem_bytes: 0,
177                            },
178                            (
179                                &ids_view,
180                                selected_len_u32,
181                                candidate_count_u32,
182                                &mut active_flags,
183                            ),
184                        )
185                    }
186                    .map_err(|e| {
187                        XlogError::Kernel(format!(
188                            "strict sparse selected-id scatter failed: {}",
189                            e
190                        ))
191                    })?;
192                }
193                ScalarType::U64 => {
194                    let ids_view = self.column_as_u64_view(ids_col, selected_len)?;
195                    let func = self
196                        .device
197                        .inner()
198                        .get_func(ILP_MODULE, ilp_kernels::ILP_MARK_SELECTED_IDS_U64)
199                        .ok_or_else(|| {
200                            XlogError::Kernel(
201                                "ilp_mark_selected_ids_u64 kernel not found".to_string(),
202                            )
203                        })?;
204                    // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
205                    unsafe {
206                        func.clone().launch(
207                            LaunchConfig {
208                                grid_dim: (grid_size, 1, 1),
209                                block_dim: (block_size, 1, 1),
210                                shared_mem_bytes: 0,
211                            },
212                            (
213                                &ids_view,
214                                selected_len_u32,
215                                candidate_count_u32,
216                                &mut active_flags,
217                            ),
218                        )
219                    }
220                    .map_err(|e| {
221                        XlogError::Kernel(format!(
222                            "strict sparse selected-id scatter failed: {}",
223                            e
224                        ))
225                    })?;
226                }
227                other => {
228                    return Err(XlogError::Kernel(format!(
229                        "selected candidate ids must be I32/I64/U32/U64, got {:?}",
230                        other
231                    )));
232                }
233            }
234
235            self.device
236                .synchronize()
237                .map_err(|e| XlogError::Kernel(format!("strict sparse scatter sync: {}", e)))?;
238        }
239
240        let d_num_rows = self.upload_device_row_count(candidate_count_u32)?;
241        Ok(CudaBuffer::from_columns_with_host_count(
242            vec![active_flags.into_bytes().into()],
243            candidate_count as u64,
244            d_num_rows,
245            Schema::new(vec![("active".to_string(), ScalarType::U32)]),
246            candidate_count_u32,
247        ))
248    }
249
250    pub fn validate_selected_ids(
251        &self,
252        ids_buf: &CudaBuffer,
253        candidate_count: usize,
254    ) -> Result<()> {
255        let selected_len = usize::try_from(ids_buf.num_rows())
256            .map_err(|_| XlogError::Kernel("selected id row count overflow".to_string()))?;
257        let candidate_count_u32 = u32::try_from(candidate_count).map_err(|_| {
258            XlogError::Kernel(format!(
259                "candidate count {} exceeds u32::MAX for strict sparse mask",
260                candidate_count
261            ))
262        })?;
263
264        if selected_len == 0 {
265            return Ok(());
266        }
267
268        let selected_len_u32 = u32::try_from(selected_len).map_err(|_| {
269            XlogError::Kernel(format!(
270                "selected id count {} exceeds u32::MAX for strict sparse mask",
271                selected_len
272            ))
273        })?;
274        let block_size = 256u32;
275        let grid_size = selected_len_u32.div_ceil(block_size);
276        let ids_col = ids_buf
277            .column(0)
278            .ok_or_else(|| XlogError::Kernel("selected id buffer has no column".to_string()))?;
279
280        let mut seen_flags = self.memory.alloc::<u32>(candidate_count)?;
281        if candidate_count > 0 {
282            self.device
283                .inner()
284                .memset_zeros(&mut seen_flags)
285                .map_err(|e| {
286                    XlogError::Kernel(format!("zero strict sparse validation flags: {}", e))
287                })?;
288        }
289
290        let mut error_code = self.memory.alloc::<u32>(1)?;
291        let mut error_pos = self.memory.alloc::<u32>(1)?;
292        self.device
293            .inner()
294            .memset_zeros(&mut error_code)
295            .map_err(|e| XlogError::Kernel(format!("zero strict sparse error code: {}", e)))?;
296        self.device
297            .inner()
298            .memset_zeros(&mut error_pos)
299            .map_err(|e| XlogError::Kernel(format!("zero strict sparse error pos: {}", e)))?;
300
301        match ids_buf
302            .schema()
303            .column_type(0)
304            .ok_or_else(|| XlogError::Kernel("selected id buffer has no schema type".to_string()))?
305        {
306            ScalarType::U32 | ScalarType::Symbol => {
307                let ids_view = self.column_as_u32_view(ids_col, selected_len)?;
308                let func = self
309                    .device
310                    .inner()
311                    .get_func(ILP_MODULE, ilp_kernels::ILP_VALIDATE_SELECTED_IDS_U32)
312                    .ok_or_else(|| {
313                        XlogError::Kernel(
314                            "ilp_validate_selected_ids_u32 kernel not found".to_string(),
315                        )
316                    })?;
317                // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
318                unsafe {
319                    func.clone().launch(
320                        LaunchConfig {
321                            grid_dim: (grid_size, 1, 1),
322                            block_dim: (block_size, 1, 1),
323                            shared_mem_bytes: 0,
324                        },
325                        (
326                            &ids_view,
327                            selected_len_u32,
328                            candidate_count_u32,
329                            &mut seen_flags,
330                            &mut error_code,
331                            &mut error_pos,
332                        ),
333                    )
334                }
335                .map_err(|e| {
336                    XlogError::Kernel(format!(
337                        "strict sparse selected-id validation failed: {}",
338                        e
339                    ))
340                })?;
341            }
342            ScalarType::I32 => {
343                let ids_view = self.ilp_i32_view(ids_col, selected_len)?;
344                let func = self
345                    .device
346                    .inner()
347                    .get_func(ILP_MODULE, ilp_kernels::ILP_VALIDATE_SELECTED_IDS_I32)
348                    .ok_or_else(|| {
349                        XlogError::Kernel(
350                            "ilp_validate_selected_ids_i32 kernel not found".to_string(),
351                        )
352                    })?;
353                // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
354                unsafe {
355                    func.clone().launch(
356                        LaunchConfig {
357                            grid_dim: (grid_size, 1, 1),
358                            block_dim: (block_size, 1, 1),
359                            shared_mem_bytes: 0,
360                        },
361                        (
362                            &ids_view,
363                            selected_len_u32,
364                            candidate_count_u32,
365                            &mut seen_flags,
366                            &mut error_code,
367                            &mut error_pos,
368                        ),
369                    )
370                }
371                .map_err(|e| {
372                    XlogError::Kernel(format!(
373                        "strict sparse selected-id validation failed: {}",
374                        e
375                    ))
376                })?;
377            }
378            ScalarType::I64 => {
379                let ids_view = self.ilp_i64_view(ids_col, selected_len)?;
380                let func = self
381                    .device
382                    .inner()
383                    .get_func(ILP_MODULE, ilp_kernels::ILP_VALIDATE_SELECTED_IDS_I64)
384                    .ok_or_else(|| {
385                        XlogError::Kernel(
386                            "ilp_validate_selected_ids_i64 kernel not found".to_string(),
387                        )
388                    })?;
389                // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
390                unsafe {
391                    func.clone().launch(
392                        LaunchConfig {
393                            grid_dim: (grid_size, 1, 1),
394                            block_dim: (block_size, 1, 1),
395                            shared_mem_bytes: 0,
396                        },
397                        (
398                            &ids_view,
399                            selected_len_u32,
400                            candidate_count_u32,
401                            &mut seen_flags,
402                            &mut error_code,
403                            &mut error_pos,
404                        ),
405                    )
406                }
407                .map_err(|e| {
408                    XlogError::Kernel(format!(
409                        "strict sparse selected-id validation failed: {}",
410                        e
411                    ))
412                })?;
413            }
414            ScalarType::U64 => {
415                let ids_view = self.column_as_u64_view(ids_col, selected_len)?;
416                let func = self
417                    .device
418                    .inner()
419                    .get_func(ILP_MODULE, ilp_kernels::ILP_VALIDATE_SELECTED_IDS_U64)
420                    .ok_or_else(|| {
421                        XlogError::Kernel(
422                            "ilp_validate_selected_ids_u64 kernel not found".to_string(),
423                        )
424                    })?;
425                // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
426                unsafe {
427                    func.clone().launch(
428                        LaunchConfig {
429                            grid_dim: (grid_size, 1, 1),
430                            block_dim: (block_size, 1, 1),
431                            shared_mem_bytes: 0,
432                        },
433                        (
434                            &ids_view,
435                            selected_len_u32,
436                            candidate_count_u32,
437                            &mut seen_flags,
438                            &mut error_code,
439                            &mut error_pos,
440                        ),
441                    )
442                }
443                .map_err(|e| {
444                    XlogError::Kernel(format!(
445                        "strict sparse selected-id validation failed: {}",
446                        e
447                    ))
448                })?;
449            }
450            other => {
451                return Err(XlogError::Kernel(format!(
452                    "selected candidate ids must be I32/I64/U32/U64, got {:?}",
453                    other
454                )));
455            }
456        }
457
458        self.device
459            .synchronize()
460            .map_err(|e| XlogError::Kernel(format!("strict sparse validation sync: {}", e)))?;
461
462        let error_code_host = self.dtoh_scalar_untracked(&error_code, 0)?;
463        if error_code_host == 0 {
464            return Ok(());
465        }
466        let error_pos_host = self.dtoh_scalar_untracked(&error_pos, 0)?;
467        match error_code_host {
468            1 => Err(XlogError::Kernel(format!(
469                "selected candidate id out of range at position {}",
470                error_pos_host
471            ))),
472            2 => Err(XlogError::Kernel(format!(
473                "duplicate selected candidate id at position {}",
474                error_pos_host
475            ))),
476            code => Err(XlogError::Kernel(format!(
477                "strict sparse selected-id validation failed with error code {}",
478                code
479            ))),
480        }
481    }
482
483    pub fn filter_buffer_by_candidate_flag(
484        &self,
485        input: &CudaBuffer,
486        candidate_flags: &CudaBuffer,
487        candidate_idx: usize,
488    ) -> Result<CudaBuffer> {
489        if input.is_empty() {
490            return self.create_empty_buffer(input.schema().clone());
491        }
492        if candidate_idx >= candidate_flags.num_rows() as usize {
493            return Err(XlogError::Kernel(format!(
494                "candidate flag index {} out of range [0, {})",
495                candidate_idx,
496                candidate_flags.num_rows()
497            )));
498        }
499
500        let flag_col = candidate_flags
501            .column(0)
502            .ok_or_else(|| XlogError::Kernel("candidate flag buffer has no column".to_string()))?;
503        let flag_view = self.column_as_u32_view(flag_col, candidate_flags.num_rows() as usize)?;
504        let row_count = u32::try_from(input.num_rows()).map_err(|_| {
505            XlogError::Kernel(format!(
506                "strict sparse row count {} exceeds u32::MAX",
507                input.num_rows()
508            ))
509        })?;
510        let candidate_idx_u32 = u32::try_from(candidate_idx).map_err(|_| {
511            XlogError::Kernel(format!(
512                "candidate flag index {} exceeds u32::MAX",
513                candidate_idx
514            ))
515        })?;
516
517        let mut row_mask = self.memory.alloc::<u8>(row_count as usize)?;
518        let func = self
519            .device
520            .inner()
521            .get_func(ILP_MODULE, ilp_kernels::ILP_BROADCAST_CANDIDATE_FLAG)
522            .ok_or_else(|| {
523                XlogError::Kernel("ilp_broadcast_candidate_flag kernel not found".to_string())
524            })?;
525        let block_size = 256u32;
526        let grid_size = row_count.div_ceil(block_size);
527        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
528        unsafe {
529            func.clone().launch(
530                LaunchConfig {
531                    grid_dim: (grid_size, 1, 1),
532                    block_dim: (block_size, 1, 1),
533                    shared_mem_bytes: 0,
534                },
535                (&flag_view, candidate_idx_u32, row_count, &mut row_mask),
536            )
537        }
538        .map_err(|e| XlogError::Kernel(format!("strict sparse flag broadcast failed: {}", e)))?;
539
540        self.filter_by_device_mask(input, &row_mask)
541    }
542
543    // ─── ILP credit kernel launchers ───────────────────────────────────
544
545    /// Launch `ilp_coo_fill` kernel: writes `(compacted_fact_indices[i], cidx)`
546    /// pairs at `coo_fact[offset..]` and `coo_cand[offset..]`.
547    pub fn ilp_coo_fill_launch(
548        &self,
549        compacted_fact_indices: &TrackedCudaSlice<u32>,
550        cidx: u32,
551        count: u32,
552        offset: u32,
553        coo_fact: &mut TrackedCudaSlice<u32>,
554        coo_cand: &mut TrackedCudaSlice<u32>,
555    ) -> Result<()> {
556        if count == 0 {
557            return Ok(());
558        }
559        let func = self
560            .device
561            .inner()
562            .get_func(ILP_CREDIT_MODULE, ilp_credit_kernels::ILP_COO_FILL)
563            .ok_or_else(|| XlogError::Kernel("ilp_coo_fill kernel not found".to_string()))?;
564        let block_size = 256u32;
565        let grid_size = count.div_ceil(block_size);
566        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
567        unsafe {
568            func.clone().launch(
569                LaunchConfig {
570                    grid_dim: (grid_size, 1, 1),
571                    block_dim: (block_size, 1, 1),
572                    shared_mem_bytes: 0,
573                },
574                (
575                    compacted_fact_indices,
576                    cidx,
577                    count,
578                    offset,
579                    coo_fact,
580                    coo_cand,
581                ),
582            )
583        }
584        .map_err(|e| XlogError::Kernel(format!("ilp_coo_fill failed: {}", e)))?;
585        self.device.synchronize()?;
586        Ok(())
587    }
588
589    /// Launch `ilp_credit_forward_f32`: CSR credit gather + clamp + NLL loss.
590    /// Returns `(credit_out, loss_contrib)` device slices of length `num_facts`.
591    pub fn ilp_credit_forward_f32_launch(
592        &self,
593        row_offsets: &TrackedCudaSlice<u32>,
594        col_indices: &TrackedCudaSlice<u32>,
595        cand_probs: &CudaColumn, // raw byte column from CudaBuffer
596        is_positive: &TrackedCudaSlice<u8>,
597        num_facts: u32,
598        eps: f32,
599    ) -> Result<(TrackedCudaSlice<f32>, TrackedCudaSlice<f32>)> {
600        let mut credit_out = self.memory.alloc::<f32>(num_facts as usize)?;
601        let mut loss_contrib = self.memory.alloc::<f32>(num_facts as usize)?;
602        if num_facts == 0 {
603            return Ok((credit_out, loss_contrib));
604        }
605        let func = self
606            .device
607            .inner()
608            .get_func(
609                ILP_CREDIT_MODULE,
610                ilp_credit_kernels::ILP_CREDIT_FORWARD_F32,
611            )
612            .ok_or_else(|| {
613                XlogError::Kernel("ilp_credit_forward_f32 kernel not found".to_string())
614            })?;
615        let block_size = 256u32;
616        let grid_size = num_facts.div_ceil(block_size);
617        // reinterpret the u8 byte column as f32 for the kernel
618        let cand_view = unsafe { cand_probs.device_view().cast::<f32>() }
619            .and_then(|view| view.try_slice(..cudarc::driver::DeviceSlice::len(cand_probs) / 4))
620            .ok_or_else(|| XlogError::Kernel("device scalar view is invalid".into()))?;
621        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
622        unsafe {
623            func.clone().launch(
624                LaunchConfig {
625                    grid_dim: (grid_size, 1, 1),
626                    block_dim: (block_size, 1, 1),
627                    shared_mem_bytes: 0,
628                },
629                (
630                    row_offsets,
631                    col_indices,
632                    &cand_view,
633                    is_positive,
634                    num_facts,
635                    eps,
636                    &mut credit_out,
637                    &mut loss_contrib,
638                ),
639            )
640        }
641        .map_err(|e| XlogError::Kernel(format!("ilp_credit_forward_f32 failed: {}", e)))?;
642        self.device.synchronize()?;
643        Ok((credit_out, loss_contrib))
644    }
645
646    /// Launch `ilp_credit_forward_f64`: CSR credit gather + clamp + NLL loss.
647    /// Returns `(credit_out, loss_contrib)` device slices of length `num_facts`.
648    pub fn ilp_credit_forward_f64_launch(
649        &self,
650        row_offsets: &TrackedCudaSlice<u32>,
651        col_indices: &TrackedCudaSlice<u32>,
652        cand_probs: &CudaColumn, // raw byte column from CudaBuffer
653        is_positive: &TrackedCudaSlice<u8>,
654        num_facts: u32,
655        eps: f64,
656    ) -> Result<(TrackedCudaSlice<f64>, TrackedCudaSlice<f64>)> {
657        let mut credit_out = self.memory.alloc::<f64>(num_facts as usize)?;
658        let mut loss_contrib = self.memory.alloc::<f64>(num_facts as usize)?;
659        if num_facts == 0 {
660            return Ok((credit_out, loss_contrib));
661        }
662        let func = self
663            .device
664            .inner()
665            .get_func(
666                ILP_CREDIT_MODULE,
667                ilp_credit_kernels::ILP_CREDIT_FORWARD_F64,
668            )
669            .ok_or_else(|| {
670                XlogError::Kernel("ilp_credit_forward_f64 kernel not found".to_string())
671            })?;
672        let block_size = 256u32;
673        let grid_size = num_facts.div_ceil(block_size);
674        let cand_view = unsafe { cand_probs.device_view().cast::<f64>() }
675            .and_then(|view| view.try_slice(..cudarc::driver::DeviceSlice::len(cand_probs) / 8))
676            .ok_or_else(|| XlogError::Kernel("device scalar view is invalid".into()))?;
677        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
678        unsafe {
679            func.clone().launch(
680                LaunchConfig {
681                    grid_dim: (grid_size, 1, 1),
682                    block_dim: (block_size, 1, 1),
683                    shared_mem_bytes: 0,
684                },
685                (
686                    row_offsets,
687                    col_indices,
688                    &cand_view,
689                    is_positive,
690                    num_facts,
691                    eps,
692                    &mut credit_out,
693                    &mut loss_contrib,
694                ),
695            )
696        }
697        .map_err(|e| XlogError::Kernel(format!("ilp_credit_forward_f64 failed: {}", e)))?;
698        self.device.synchronize()?;
699        Ok((credit_out, loss_contrib))
700    }
701
702    /// Launch `ilp_credit_backward_f32`: gradient scatter via CSR + atomicAdd.
703    /// Returns `d_cand_probs` gradient of length `num_cands` (zeroed, then accumulated).
704    pub fn ilp_credit_backward_f32_launch(
705        &self,
706        row_offsets: &TrackedCudaSlice<u32>,
707        col_indices: &TrackedCudaSlice<u32>,
708        credit_out: &TrackedCudaSlice<f32>,
709        is_positive: &TrackedCudaSlice<u8>,
710        num_facts: u32,
711        num_cands: u32,
712    ) -> Result<TrackedCudaSlice<f32>> {
713        let mut d_grad = self.memory.alloc::<f32>(num_cands as usize)?;
714        self.device
715            .inner()
716            .memset_zeros(&mut d_grad)
717            .map_err(|e| XlogError::Kernel(format!("Failed to zero grad: {}", e)))?;
718        if num_facts == 0 {
719            return Ok(d_grad);
720        }
721        let func = self
722            .device
723            .inner()
724            .get_func(
725                ILP_CREDIT_MODULE,
726                ilp_credit_kernels::ILP_CREDIT_BACKWARD_F32,
727            )
728            .ok_or_else(|| {
729                XlogError::Kernel("ilp_credit_backward_f32 kernel not found".to_string())
730            })?;
731        let block_size = 256u32;
732        let grid_size = num_facts.div_ceil(block_size);
733        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
734        unsafe {
735            func.clone().launch(
736                LaunchConfig {
737                    grid_dim: (grid_size, 1, 1),
738                    block_dim: (block_size, 1, 1),
739                    shared_mem_bytes: 0,
740                },
741                (
742                    row_offsets,
743                    col_indices,
744                    credit_out,
745                    is_positive,
746                    num_facts,
747                    &mut d_grad,
748                ),
749            )
750        }
751        .map_err(|e| XlogError::Kernel(format!("ilp_credit_backward_f32 failed: {}", e)))?;
752        self.device.synchronize()?;
753        Ok(d_grad)
754    }
755
756    /// Launch `ilp_credit_backward_f64`: gradient scatter via CSR + atomicAdd.
757    /// Returns `d_cand_probs` gradient of length `num_cands` (zeroed, then accumulated).
758    pub fn ilp_credit_backward_f64_launch(
759        &self,
760        row_offsets: &TrackedCudaSlice<u32>,
761        col_indices: &TrackedCudaSlice<u32>,
762        credit_out: &TrackedCudaSlice<f64>,
763        is_positive: &TrackedCudaSlice<u8>,
764        num_facts: u32,
765        num_cands: u32,
766    ) -> Result<TrackedCudaSlice<f64>> {
767        let mut d_grad = self.memory.alloc::<f64>(num_cands as usize)?;
768        self.device
769            .inner()
770            .memset_zeros(&mut d_grad)
771            .map_err(|e| XlogError::Kernel(format!("Failed to zero grad: {}", e)))?;
772        if num_facts == 0 {
773            return Ok(d_grad);
774        }
775        let func = self
776            .device
777            .inner()
778            .get_func(
779                ILP_CREDIT_MODULE,
780                ilp_credit_kernels::ILP_CREDIT_BACKWARD_F64,
781            )
782            .ok_or_else(|| {
783                XlogError::Kernel("ilp_credit_backward_f64 kernel not found".to_string())
784            })?;
785        let block_size = 256u32;
786        let grid_size = num_facts.div_ceil(block_size);
787        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
788        unsafe {
789            func.clone().launch(
790                LaunchConfig {
791                    grid_dim: (grid_size, 1, 1),
792                    block_dim: (block_size, 1, 1),
793                    shared_mem_bytes: 0,
794                },
795                (
796                    row_offsets,
797                    col_indices,
798                    credit_out,
799                    is_positive,
800                    num_facts,
801                    &mut d_grad,
802                ),
803            )
804        }
805        .map_err(|e| XlogError::Kernel(format!("ilp_credit_backward_f64 failed: {}", e)))?;
806        self.device.synchronize()?;
807        Ok(d_grad)
808    }
809
810    /// GPU-side sum reduction (f32).
811    ///
812    /// Sums `n` elements of `input` on device and returns a single-element
813    /// device buffer containing the result.  The caller must zero the output
814    /// buffer *before* launching the kernel — this function handles that.
815    pub fn ilp_reduce_sum_f32_launch(
816        &self,
817        input: &TrackedCudaSlice<f32>,
818        n: u32,
819    ) -> Result<TrackedCudaSlice<f32>> {
820        let mut d_result = self.memory.alloc::<f32>(1)?;
821        self.device
822            .inner()
823            .memset_zeros(&mut d_result)
824            .map_err(|e| XlogError::Kernel(format!("ilp_reduce_sum_f32 zero result: {}", e)))?;
825
826        if n == 0 {
827            return Ok(d_result);
828        }
829
830        let func = self
831            .device
832            .inner()
833            .get_func(ILP_MODULE, ilp_kernels::ILP_REDUCE_SUM_F32)
834            .ok_or_else(|| XlogError::Kernel("ilp_reduce_sum_f32 not found".to_string()))?;
835        let block_size = 256u32;
836        let grid_size = n.div_ceil(block_size);
837        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
838        unsafe {
839            func.clone().launch(
840                LaunchConfig {
841                    grid_dim: (grid_size, 1, 1),
842                    block_dim: (block_size, 1, 1),
843                    shared_mem_bytes: 0,
844                },
845                (input, n, &mut d_result),
846            )
847        }
848        .map_err(|e| XlogError::Kernel(format!("ilp_reduce_sum_f32: {}", e)))?;
849        self.device.synchronize()?;
850        Ok(d_result)
851    }
852
853    /// GPU-side sum reduction (f64).
854    ///
855    /// Sums `n` elements of `input` on device and returns a single-element
856    /// device buffer containing the result.  Requires sm_60+ for double
857    /// atomicAdd (this project targets sm_75 baseline).
858    pub fn ilp_reduce_sum_f64_launch(
859        &self,
860        input: &TrackedCudaSlice<f64>,
861        n: u32,
862    ) -> Result<TrackedCudaSlice<f64>> {
863        let mut d_result = self.memory.alloc::<f64>(1)?;
864        self.device
865            .inner()
866            .memset_zeros(&mut d_result)
867            .map_err(|e| XlogError::Kernel(format!("ilp_reduce_sum_f64 zero result: {}", e)))?;
868
869        if n == 0 {
870            return Ok(d_result);
871        }
872
873        let func = self
874            .device
875            .inner()
876            .get_func(ILP_MODULE, ilp_kernels::ILP_REDUCE_SUM_F64)
877            .ok_or_else(|| XlogError::Kernel("ilp_reduce_sum_f64 not found".to_string()))?;
878        let block_size = 256u32;
879        let grid_size = n.div_ceil(block_size);
880        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
881        unsafe {
882            func.clone().launch(
883                LaunchConfig {
884                    grid_dim: (grid_size, 1, 1),
885                    block_dim: (block_size, 1, 1),
886                    shared_mem_bytes: 0,
887                },
888                (input, n, &mut d_result),
889            )
890        }
891        .map_err(|e| XlogError::Kernel(format!("ilp_reduce_sum_f64: {}", e)))?;
892        self.device.synchronize()?;
893        Ok(d_result)
894    }
895
896    /// Fill COO arrays from a device-side mask and prefix-sum.
897    ///
898    /// For each set bit in `mask`, writes the corresponding `fact_indices` entry
899    /// into `coo_fact` and `cand_value` into `coo_cand` at the position
900    /// determined by `d_offsets[offset_idx] + prefix_sum[tid]`.
901    ///
902    /// Parameters:
903    /// - `offset_idx`: index into `d_offsets` for the write base position
904    /// - `cand_value`: actual candidate index to write into `coo_cand`
905    ///
906    /// This keeps COO assembly fully on device, eliminating the mask D2H transfer.
907    #[expect(
908        clippy::too_many_arguments,
909        reason = "the launch surface mirrors the fixed COO-fill kernel ABI and keeps every device buffer explicit"
910    )]
911    pub fn ilp_coo_fill_from_mask_launch(
912        &self,
913        mask: &TrackedCudaSlice<u8>,
914        prefix_sum: &TrackedCudaSlice<u32>,
915        fact_indices: &TrackedCudaSlice<u32>,
916        offset_idx: u32,
917        cand_value: u32,
918        num_query: u32,
919        d_offsets: &TrackedCudaSlice<u32>,
920        coo_fact: &mut TrackedCudaSlice<u32>,
921        coo_cand: &mut TrackedCudaSlice<u32>,
922    ) -> Result<()> {
923        if num_query == 0 {
924            return Ok(());
925        }
926        let func = self
927            .device()
928            .inner()
929            .get_func(ILP_MODULE, ilp_kernels::ILP_COO_FILL_FROM_MASK)
930            .ok_or_else(|| XlogError::Kernel("ilp_coo_fill_from_mask not found".to_string()))?;
931        let block_size = 256u32;
932        let grid_size = num_query.div_ceil(block_size);
933        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
934        unsafe {
935            func.clone().launch(
936                LaunchConfig {
937                    grid_dim: (grid_size, 1, 1),
938                    block_dim: (block_size, 1, 1),
939                    shared_mem_bytes: 0,
940                },
941                (
942                    mask,
943                    prefix_sum,
944                    fact_indices,
945                    offset_idx,
946                    cand_value,
947                    num_query,
948                    d_offsets,
949                    coo_fact,
950                    coo_cand,
951                ),
952            )
953        }
954        .map_err(|e| XlogError::Kernel(format!("ilp_coo_fill_from_mask: {}", e)))?;
955        self.device()
956            .inner()
957            .synchronize()
958            .map_err(|e| XlogError::Kernel(format!("ilp_coo_fill_from_mask sync: {}", e)))?;
959        Ok(())
960    }
961
962    /// Build a histogram of fact indices from sorted COO data.
963    ///
964    /// For each entry in `sorted_facts[0..nnz]`, atomically increments
965    /// the corresponding bin in the output histogram. The result is a
966    /// device-side count array of length `num_facts`, suitable for
967    /// prefix-sum to produce CSR `row_offsets`.
968    ///
969    /// The caller provides sorted fact indices; the histogram is
970    /// zero-initialized internally.
971    pub fn ilp_csr_histogram_launch(
972        &self,
973        sorted_facts: &TrackedCudaSlice<u32>,
974        nnz: u32,
975        num_facts: u32,
976    ) -> Result<TrackedCudaSlice<u32>> {
977        let mut d_hist = self.memory().alloc::<u32>(num_facts as usize)?;
978        self.device()
979            .inner()
980            .memset_zeros(&mut d_hist)
981            .map_err(|e| XlogError::Kernel(format!("ilp_csr_histogram zero hist: {}", e)))?;
982
983        if nnz == 0 {
984            return Ok(d_hist);
985        }
986
987        let func = self
988            .device()
989            .inner()
990            .get_func(ILP_MODULE, ilp_kernels::ILP_CSR_HISTOGRAM)
991            .ok_or_else(|| XlogError::Kernel("ilp_csr_histogram kernel not found".to_string()))?;
992
993        let block_size = 256u32;
994        let grid_size = nnz.div_ceil(block_size);
995
996        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
997        unsafe {
998            func.clone()
999                .launch(
1000                    cudarc::driver::LaunchConfig {
1001                        grid_dim: (grid_size, 1, 1),
1002                        block_dim: (block_size, 1, 1),
1003                        shared_mem_bytes: 0,
1004                    },
1005                    (sorted_facts, nnz, num_facts, &mut d_hist),
1006                )
1007                .map_err(|e| XlogError::Kernel(format!("ilp_csr_histogram launch: {}", e)))?;
1008        }
1009
1010        self.device()
1011            .inner()
1012            .synchronize()
1013            .map_err(|e| XlogError::Kernel(format!("ilp_csr_histogram sync: {}", e)))?;
1014
1015        Ok(d_hist)
1016    }
1017}