1use xlog_core::{RelId, Result, ScalarType, XlogError};
19use xlog_cuda::{CudaBuffer, CudaKernelProvider, IlpExactNaryPatterns};
20
21use crate::nary::{enumerate_patterns, NaryEnumerationConfig, NaryRulePattern};
22use crate::nary_layout::{flatten_patterns, NARY_MAX_ATOM_ARITY};
23use crate::reduce::reduce_nary;
24
25const NARY_MAX_HEAD_ARITY: usize = 8;
30
31#[derive(Debug, Clone, Copy)]
33pub struct NaryInductionConfig {
34 pub enumeration: NaryEnumerationConfig,
36 pub k: u32,
38}
39
40pub struct InduceExactNaryRequest<'a> {
49 pub head_rel_idx: RelId,
50 pub candidates: &'a [(RelId, &'a CudaBuffer)],
51 pub positives: &'a CudaBuffer,
52 pub negatives: Option<&'a CudaBuffer>,
53 pub config: NaryInductionConfig,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ScoredNaryCandidate {
59 pub head_rel_idx: RelId,
60 pub body_rel_idxs: Vec<RelId>,
64 pub pattern: NaryRulePattern,
66 pub positives_covered: u32,
67 pub negatives_covered: u32,
68 pub local_rank: u32,
69 pub next_positives_covered: u32,
70 pub next_negatives_covered: u32,
71 pub tie_class_size: u32,
72}
73
74#[derive(Debug, Clone, Default, PartialEq, Eq)]
80pub struct NaryInductionResult {
81 pub candidates: Vec<ScoredNaryCandidate>,
82 pub total_scored: u32,
83 pub candidate_count: u32,
84 pub positive_count: u32,
85 pub negative_count: u32,
86}
87
88pub fn induce_exact_nary(
103 provider: &CudaKernelProvider,
104 request: &InduceExactNaryRequest<'_>,
105) -> Result<NaryInductionResult> {
106 if request.candidates.is_empty() {
108 return Ok(NaryInductionResult::default());
109 }
110
111 let (head_arity_u32, pos_count) = require_u64_columnar(request.positives, "positives")?;
115 let head_arity = head_arity_u32 as usize;
116 if head_arity == 0 || head_arity > NARY_MAX_HEAD_ARITY {
117 return Err(XlogError::Type(format!(
118 "induce_exact_nary: positives arity {head_arity} outside supported \
119 head arity 1..={NARY_MAX_HEAD_ARITY}",
120 )));
121 }
122 let neg_count = match request.negatives {
123 Some(neg) => {
124 let (neg_arity, neg_count) = require_u64_columnar(neg, "negatives")?;
125 if neg_arity != head_arity_u32 {
126 return Err(XlogError::Type(format!(
127 "induce_exact_nary: negatives arity {neg_arity} != head arity {head_arity}",
128 )));
129 }
130 neg_count
131 }
132 None => 0,
133 };
134 let mut candidate_arities: Vec<u8> = Vec::with_capacity(request.candidates.len());
135 for (i, (_, buf)) in request.candidates.iter().enumerate() {
136 let (arity, _) = require_u64_columnar(buf, &format!("candidate[{i}]"))?;
137 if arity == 0 || arity as usize > NARY_MAX_ATOM_ARITY {
138 return Err(XlogError::Type(format!(
139 "induce_exact_nary: candidate[{i}] arity {arity} outside supported \
140 atom arity 1..={NARY_MAX_ATOM_ARITY}",
141 )));
142 }
143 candidate_arities.push(arity as u8);
144 }
145 let candidate_count = request.candidates.len() as u32;
146
147 let patterns = enumerate_patterns(
150 head_arity as u8,
151 &candidate_arities,
152 &request.config.enumeration,
153 )?;
154
155 let counts_only = NaryInductionResult {
156 candidates: Vec::new(),
157 total_scored: 0,
158 candidate_count,
159 positive_count: pos_count,
160 negative_count: neg_count,
161 };
162 if patterns.is_empty() || request.config.k == 0 || pos_count == 0 {
163 return Ok(counts_only);
167 }
168
169 let layout = flatten_patterns(&patterns)
173 .map_err(|e| XlogError::Type(format!("induce_exact_nary: flatten: {e}")))?;
174 let patterns_view = IlpExactNaryPatterns {
175 body_offset: &layout.body_offset,
176 body_len: &layout.body_len,
177 atom_candidate_slot: &layout.atom_candidate_slot,
178 atom_arity: &layout.atom_arity,
179 atom_binding_offset: &layout.atom_binding_offset,
180 binding_codes: &layout.binding_codes,
181 head_arity: head_arity_u32,
182 };
183
184 let empty_neg_holder: Option<CudaBuffer> = if request.negatives.is_none() {
188 Some(provider.create_empty_buffer(request.positives.schema().clone())?)
189 } else {
190 None
191 };
192 let negatives: &CudaBuffer = match request.negatives {
193 Some(b) => b,
194 None => empty_neg_holder
195 .as_ref()
196 .expect("holder populated in the None branch above"),
197 };
198
199 let candidate_buffers: Vec<&CudaBuffer> = request.candidates.iter().map(|(_, b)| *b).collect();
202 let (pos_covered, neg_covered) = provider.ilp_exact_nary_score_device(
203 &patterns_view,
204 &candidate_buffers,
205 request.positives,
206 negatives,
207 )?;
208 if pos_covered.len() != patterns.len() || neg_covered.len() != patterns.len() {
209 return Err(XlogError::Execution(format!(
210 "induce_exact_nary: launcher returned {}/{} counts for {} patterns",
211 pos_covered.len(),
212 neg_covered.len(),
213 patterns.len(),
214 )));
215 }
216 let coverage: Vec<(u32, u32)> = pos_covered.into_iter().zip(neg_covered).collect();
217 let kept = reduce_nary(&coverage, request.config.k);
218
219 let mut candidates = Vec::with_capacity(kept.len());
220 for keep in kept {
221 let pattern = patterns
222 .get(keep.pattern_idx)
223 .ok_or_else(|| {
224 XlogError::Execution(format!(
225 "induce_exact_nary: reduction returned pattern index {} for {} patterns",
226 keep.pattern_idx,
227 patterns.len(),
228 ))
229 })?
230 .clone();
231 let body_rel_idxs = pattern
232 .body
233 .iter()
234 .map(|atom| request.candidates[atom.candidate_slot as usize].0)
235 .collect();
236 candidates.push(ScoredNaryCandidate {
237 head_rel_idx: request.head_rel_idx,
238 body_rel_idxs,
239 pattern,
240 positives_covered: keep.positives_covered,
241 negatives_covered: keep.negatives_covered,
242 local_rank: keep.local_rank,
243 next_positives_covered: keep.next_positives_covered,
244 next_negatives_covered: keep.next_negatives_covered,
245 tie_class_size: keep.tie_class_size,
246 });
247 }
248
249 Ok(NaryInductionResult {
250 candidates,
251 total_scored: patterns.len() as u32,
252 candidate_count,
253 positive_count: pos_count,
254 negative_count: neg_count,
255 })
256}
257
258fn require_u64_columnar(buf: &CudaBuffer, label: &str) -> Result<(u32, u32)> {
259 let arity = buf.arity();
260 for col_idx in 0..arity {
261 let t = buf.schema().column_type(col_idx).ok_or_else(|| {
262 XlogError::Type(format!(
263 "induce_exact_nary: {label} buffer column {col_idx} has no schema type",
264 ))
265 })?;
266 if t != ScalarType::U64 {
267 return Err(XlogError::Type(format!(
268 "induce_exact_nary: {label} buffer column {col_idx} has type {t:?}, expected U64",
269 )));
270 }
271 }
272 let rows = buf.cached_row_count().ok_or_else(|| {
273 XlogError::Execution(format!(
274 "induce_exact_nary: {label} buffer has no cached row count \
275 (device-resident ingest should populate it; required to avoid \
276 a hot-loop device-to-host transfer)",
277 ))
278 })?;
279 let arity = u32::try_from(arity)
280 .map_err(|_| XlogError::Type(format!("induce_exact_nary: {label} arity exceeds u32")))?;
281 Ok((arity, rows))
282}