Skip to main content

xlog_runtime/
ilp_registry.rs

1//! ILP (Inductive Logic Programming) registry for tensor mask management.
2
3use std::collections::HashMap;
4use xlog_core::XlogError;
5use xlog_cuda::{CudaBuffer, CudaKernelProvider};
6
7/// Reads the device-side row count through the provider's public metadata API.
8pub fn read_device_row_count(
9    provider: &CudaKernelProvider,
10    buffer: &CudaBuffer,
11) -> Result<usize, XlogError> {
12    provider.device_row_count(buffer)
13}
14
15/// Registry for ILP tensor masks.
16pub struct IlpRegistry {
17    masks: HashMap<String, IlpMask>,
18}
19
20/// A registered ILP mask — Dense (imported via DLPack) or Sparse (candidate entries only).
21pub enum IlpMask {
22    /// Dense mask with hard and soft weight buffers.
23    Dense {
24        /// Hard mask (binary on/off).
25        hard: CudaBuffer,
26        /// Soft mask (continuous weights).
27        soft: CudaBuffer,
28        /// Number of body relations in the schema.
29        schema_size: usize,
30    },
31    /// Sparse mask listing active (i,j,k) entries on host.
32    Sparse {
33        /// Active rule entries.
34        active_entries: Vec<(u32, u32, u32)>,
35        /// Number of body relations in the schema.
36        schema_size: usize,
37    },
38    /// Sparse mask with device-resident active flags.
39    SparseDevice {
40        /// Candidate (i,j,k) entries in evaluation order.
41        candidate_order: Vec<(u32, u32, u32)>,
42        /// Device-resident boolean flags per candidate.
43        active_flags: CudaBuffer,
44        /// Number of currently selected candidates.
45        selected_count: usize,
46        /// Number of body relations in the schema.
47        schema_size: usize,
48    },
49}
50
51impl IlpMask {
52    /// Return the schema size (number of body relations).
53    pub fn schema_size(&self) -> usize {
54        match self {
55            IlpMask::Dense { schema_size, .. } => *schema_size,
56            IlpMask::Sparse { schema_size, .. } => *schema_size,
57            IlpMask::SparseDevice { schema_size, .. } => *schema_size,
58        }
59    }
60}
61
62/// Tag metadata from TensorMaskedJoin execution.
63/// Retains per-entry projected join buffers for batch credit queries.
64pub struct IlpTaggedResult {
65    /// Per-entry metadata and result buffers.
66    pub entries: Vec<IlpTagEntry>,
67}
68
69/// Metadata for a single active rule (i,j,k), its result cardinality,
70/// and the projected join result buffer (retained for batch credit queries).
71pub struct IlpTagEntry {
72    /// First body relation index.
73    pub i: u32,
74    /// Second body relation index.
75    pub j: u32,
76    /// Head relation index.
77    pub k: u32,
78    /// Number of result rows for this entry.
79    pub num_rows: u32,
80    /// The projected join result buffer, retained for batch credit queries.
81    pub buffer: Option<CudaBuffer>,
82}
83
84impl IlpRegistry {
85    /// Create an empty ILP registry.
86    pub fn new() -> Self {
87        Self {
88            masks: HashMap::new(),
89        }
90    }
91
92    /// Clear all registered masks, releasing GPU buffers.
93    pub fn clear(&mut self) {
94        self.masks.clear();
95    }
96
97    /// Register a dense ILP mask (hard + soft weight buffers).
98    pub fn insert_mask(
99        &mut self,
100        name: String,
101        hard: CudaBuffer,
102        soft: CudaBuffer,
103        schema_size: usize,
104    ) {
105        self.masks.insert(
106            name,
107            IlpMask::Dense {
108                hard,
109                soft,
110                schema_size,
111            },
112        );
113    }
114
115    /// Insert a mask built from sparse candidate data.
116    ///
117    /// Performs deterministic top-k ranking (desc soft value, then lower index)
118    /// and stores the selected (i,j,k) entries directly — no dense buffer.
119    pub fn insert_mask_from_sparse(
120        &mut self,
121        name: String,
122        schema_size: usize,
123        active_ijk: &[(u32, u32, u32)],
124        active_soft: &[f32],
125        budget: usize,
126    ) -> Result<(), XlogError> {
127        if active_ijk.len() != active_soft.len() {
128            return Err(XlogError::Execution(format!(
129                "active_ijk length {} != active_soft length {}",
130                active_ijk.len(),
131                active_soft.len()
132            )));
133        }
134
135        // Deterministic top-k over positive-probability candidates: descending
136        // soft value, then ascending index for ties.
137        let mut ranked: Vec<(usize, f32)> = active_soft.iter().copied().enumerate().collect();
138        ranked.retain(|(_, soft)| *soft > 0.0);
139        ranked.sort_by(|a, b| {
140            b.1.partial_cmp(&a.1)
141                .unwrap_or(std::cmp::Ordering::Equal)
142                .then(a.0.cmp(&b.0))
143        });
144        ranked.truncate(budget.min(ranked.len()));
145
146        let entries: Vec<(u32, u32, u32)> =
147            ranked.iter().map(|&(idx, _)| active_ijk[idx]).collect();
148
149        self.masks.insert(
150            name,
151            IlpMask::Sparse {
152                active_entries: entries,
153                schema_size,
154            },
155        );
156        Ok(())
157    }
158
159    /// Insert an already-selected sparse mask, preserving caller order exactly.
160    pub fn insert_selected_mask(
161        &mut self,
162        name: String,
163        schema_size: usize,
164        active_entries: &[(u32, u32, u32)],
165    ) {
166        self.masks.insert(
167            name,
168            IlpMask::Sparse {
169                active_entries: active_entries.to_vec(),
170                schema_size,
171            },
172        );
173    }
174
175    /// Insert a device-resident sparse mask with active flags on GPU.
176    pub fn insert_selected_mask_device(
177        &mut self,
178        name: String,
179        schema_size: usize,
180        candidate_order: Vec<(u32, u32, u32)>,
181        active_flags: CudaBuffer,
182        selected_count: usize,
183    ) {
184        self.masks.insert(
185            name,
186            IlpMask::SparseDevice {
187                candidate_order,
188                active_flags,
189                selected_count,
190                schema_size,
191            },
192        );
193    }
194
195    /// Look up a registered mask by name.
196    pub fn get_mask(&self, name: &str) -> Option<&IlpMask> {
197        self.masks.get(name)
198    }
199
200    /// Returns true if any registered mask uses the sparse-device representation.
201    pub fn has_sparse_device_mask(&self) -> bool {
202        self.masks
203            .values()
204            .any(|mask| matches!(mask, IlpMask::SparseDevice { .. }))
205    }
206}
207
208impl Default for IlpRegistry {
209    fn default() -> Self {
210        Self::new()
211    }
212}