1use crate::memory::DeviceMemoryView;
43use std::sync::atomic::Ordering;
44
45use crate::memory::{CudaBuffer, TrackedCudaSlice};
46use crate::{LaunchAsync, LaunchConfig};
47use xlog_core::{Result, ScalarType, XlogError};
48
49use super::{ilp_exact_nary_kernels, ILP_EXACT_NARY_MODULE};
50
51const ILP_EXACT_NARY_BLOCK_SIZE: u32 = 256;
56const _: () = assert!(
57 ILP_EXACT_NARY_BLOCK_SIZE.is_power_of_two() && ILP_EXACT_NARY_BLOCK_SIZE <= 256,
58 "block size must stay a power of two within the kernel's shared scratch",
59);
60
61const NARY_MAX_BODY_ATOMS: u32 = 8;
65const NARY_MAX_JOIN_VARS: u32 = 8;
66const NARY_MAX_HEAD_ARITY: u32 = 8;
67const NARY_MAX_ATOM_ARITY: u32 = 8;
72
73const NARY_JOIN_FLAG: u32 = 0x8000_0000;
74const NARY_INDEX_MASK: u32 = 0xFF;
75
76const U64_SIZE: usize = std::mem::size_of::<u64>();
77
78pub struct IlpExactNaryPatterns<'a> {
80 pub body_offset: &'a [u32],
81 pub body_len: &'a [u32],
82 pub atom_candidate_slot: &'a [u32],
83 pub atom_arity: &'a [u32],
84 pub atom_binding_offset: &'a [u32],
85 pub binding_codes: &'a [u32],
86 pub head_arity: u32,
87}
88
89pub struct IlpExactNaryRequest<'a> {
93 pub body_offset: &'a [u32],
94 pub body_len: &'a [u32],
95 pub atom_candidate_slot: &'a [u32],
96 pub atom_arity: &'a [u32],
97 pub atom_binding_offset: &'a [u32],
98 pub binding_codes: &'a [u32],
99 pub cand_values: &'a [u64],
104 pub cand_value_offset: &'a [u32],
106 pub cand_rows: &'a [u32],
107 pub pos_values: &'a [u64],
110 pub neg_values: &'a [u64],
111 pub head_arity: u32,
112}
113
114impl<'a> IlpExactNaryRequest<'a> {
115 fn patterns(&self) -> IlpExactNaryPatterns<'a> {
116 IlpExactNaryPatterns {
117 body_offset: self.body_offset,
118 body_len: self.body_len,
119 atom_candidate_slot: self.atom_candidate_slot,
120 atom_arity: self.atom_arity,
121 atom_binding_offset: self.atom_binding_offset,
122 binding_codes: self.binding_codes,
123 head_arity: self.head_arity,
124 }
125 }
126}
127
128fn nary_err(detail: impl std::fmt::Display) -> XlogError {
129 XlogError::Kernel(format!("ilp_exact_nary_score: {detail}"))
130}
131
132fn validate_batch(
135 patterns: &IlpExactNaryPatterns<'_>,
136 cand_value_offset: &[u32],
137 cand_rows: &[u32],
138 values_len: u64,
139) -> Result<u32> {
140 let pattern_count = patterns.body_offset.len();
141 if pattern_count == 0 {
142 return Err(nary_err("empty pattern batch"));
143 }
144 if patterns.body_len.len() != pattern_count {
145 return Err(nary_err("body_len length != pattern count"));
146 }
147 let atoms = patterns.atom_candidate_slot.len();
148 if patterns.atom_arity.len() != atoms || patterns.atom_binding_offset.len() != atoms {
149 return Err(nary_err("atom array lengths disagree"));
150 }
151 let bindings = patterns.binding_codes.len();
152 let slots = cand_value_offset.len();
153 if cand_rows.len() != slots {
154 return Err(nary_err("cand_rows length != cand_value_offset length"));
155 }
156 if patterns.head_arity == 0 {
157 return Err(nary_err("head_arity must be >= 1"));
158 }
159 if patterns.head_arity > NARY_MAX_HEAD_ARITY {
160 return Err(nary_err(format!(
161 "head_arity {} exceeds device bound {NARY_MAX_HEAD_ARITY}",
162 patterns.head_arity
163 )));
164 }
165
166 let mut slot_arity: Vec<Option<u32>> = vec![None; slots];
169 for (pattern, (&offset, &len)) in patterns
170 .body_offset
171 .iter()
172 .zip(patterns.body_len)
173 .enumerate()
174 {
175 if len == 0 {
176 return Err(nary_err(format!("pattern {pattern} has an empty body")));
177 }
178 if len > NARY_MAX_BODY_ATOMS {
179 return Err(nary_err(format!(
180 "pattern {pattern} has {len} body atoms; device bound is \
181 {NARY_MAX_BODY_ATOMS}"
182 )));
183 }
184 let end = offset
185 .checked_add(len)
186 .ok_or_else(|| nary_err("body offset overflow"))?;
187 if end as usize > atoms {
188 return Err(nary_err(format!(
189 "pattern {pattern} body [{offset}, {end}) exceeds {atoms} atoms"
190 )));
191 }
192 for atom in offset as usize..end as usize {
193 let slot = patterns.atom_candidate_slot[atom] as usize;
194 if slot >= slots {
195 return Err(nary_err(format!(
196 "atom {atom} references candidate slot {slot} of {slots}"
197 )));
198 }
199 let arity = patterns.atom_arity[atom];
200 if arity == 0 {
201 return Err(nary_err(format!("atom {atom} has arity 0")));
202 }
203 if arity > NARY_MAX_ATOM_ARITY {
204 return Err(nary_err(format!(
205 "atom {atom} has arity {arity} > the device contract's \
206 {NARY_MAX_ATOM_ARITY}"
207 )));
208 }
209 match slot_arity[slot] {
210 None => slot_arity[slot] = Some(arity),
211 Some(existing) if existing == arity => {}
212 Some(existing) => {
213 return Err(nary_err(format!(
214 "candidate slot {slot} read at arity {arity} and \
215 arity {existing}; relation arity must be consistent"
216 )));
217 }
218 }
219 let rows = cand_rows[slot] as u64;
220 let value_end = cand_value_offset[slot] as u64 + rows * arity as u64;
221 if value_end > values_len {
222 return Err(nary_err(format!(
223 "candidate slot {slot} needs values up to {value_end}, \
224 buffer holds {values_len}"
225 )));
226 }
227 let binding_offset = patterns.atom_binding_offset[atom];
228 let binding_end = binding_offset
229 .checked_add(arity)
230 .ok_or_else(|| nary_err("binding offset overflow"))?;
231 if binding_end as usize > bindings {
232 return Err(nary_err(format!(
233 "atom {atom} bindings [{binding_offset}, {binding_end}) \
234 exceed {bindings} codes"
235 )));
236 }
237 for position in binding_offset as usize..binding_end as usize {
238 let code = patterns.binding_codes[position];
239 let index = code & NARY_INDEX_MASK;
240 if code & NARY_JOIN_FLAG != 0 {
241 if index >= NARY_MAX_JOIN_VARS {
242 return Err(nary_err(format!(
243 "binding {position} join index {index} >= device \
244 bound {NARY_MAX_JOIN_VARS}"
245 )));
246 }
247 } else if index >= patterns.head_arity {
248 return Err(nary_err(format!(
249 "binding {position} head index {index} >= head arity \
250 {}",
251 patterns.head_arity
252 )));
253 }
254 }
255 }
256 }
257
258 u32::try_from(pattern_count).map_err(|_| nary_err("pattern count exceeds u32"))
259}
260
261fn validate_request(request: &IlpExactNaryRequest<'_>) -> Result<(u32, u32, u32)> {
262 let head_arity = request.head_arity.max(1) as usize;
263 if !request.pos_values.len().is_multiple_of(head_arity) {
264 return Err(nary_err("pos_values length not a multiple of head_arity"));
265 }
266 if !request.neg_values.len().is_multiple_of(head_arity) {
267 return Err(nary_err("neg_values length not a multiple of head_arity"));
268 }
269 let num_patterns = validate_batch(
270 &request.patterns(),
271 request.cand_value_offset,
272 request.cand_rows,
273 request.cand_values.len() as u64,
274 )?;
275 let num_pos = u32::try_from(request.pos_values.len() / head_arity)
276 .map_err(|_| nary_err("positive tuple count exceeds u32"))?;
277 let num_neg = u32::try_from(request.neg_values.len() / head_arity)
278 .map_err(|_| nary_err("negative tuple count exceeds u32"))?;
279 Ok((num_patterns, num_pos, num_neg))
280}
281
282fn u64_view(slice: &TrackedCudaSlice<u8>, elements: usize) -> DeviceMemoryView<u64> {
283 unsafe { slice.view().cast::<u64>() }
285 .and_then(|view| view.try_slice(..elements))
286 .expect("validated u64 arena extent")
287}
288
289fn require_u64_columns(buf: &CudaBuffer, label: &str) -> Result<(u32, u32)> {
291 let arity = u32::try_from(buf.arity()).map_err(|_| nary_err(format!("{label}: arity")))?;
292 if arity == 0 {
293 return Err(nary_err(format!("{label}: buffer has arity 0")));
294 }
295 for column in 0..buf.arity() {
296 match buf.schema().column_type(column) {
297 Some(ScalarType::U64) => {}
298 other => {
299 return Err(nary_err(format!(
300 "{label}: column {column} is {other:?}, expected U64"
301 )));
302 }
303 }
304 }
305 let rows = buf
306 .cached_row_count()
307 .ok_or_else(|| nary_err(format!("{label}: cached_row_count absent")))?;
308 Ok((arity, rows))
309}
310
311impl super::CudaKernelProvider {
312 pub fn ilp_exact_nary_score(
319 &self,
320 request: &IlpExactNaryRequest<'_>,
321 ) -> Result<(Vec<u32>, Vec<u32>)> {
322 let (num_patterns, num_pos, num_neg) = validate_request(request)?;
323
324 macro_rules! upload_u64_bytes {
325 ($host:expr) => {{
326 let host: &[u64] = $host;
327 let bytes: Vec<u8> = host.iter().flat_map(|v| v.to_le_bytes()).collect();
328 let mut buf = self.memory.alloc::<u8>(bytes.len().max(U64_SIZE))?;
329 if !bytes.is_empty() {
330 self.htod_sync_copy_into_tracked(&bytes, &mut buf)
331 .map_err(|e| nary_err(format!("h2d u64 values: {e}")))?;
332 }
333 buf
334 }};
335 }
336
337 let cand_values_buf = upload_u64_bytes!(request.cand_values);
338 let pos_values_buf = upload_u64_bytes!(request.pos_values);
339 let neg_values_buf = upload_u64_bytes!(request.neg_values);
340
341 self.launch_nary(
342 &request.patterns(),
343 num_patterns,
344 num_pos,
345 num_neg,
346 request.cand_value_offset,
347 request.cand_rows,
348 u64_view(&cand_values_buf, request.cand_values.len()),
349 u64_view(&pos_values_buf, request.pos_values.len()),
350 u64_view(&neg_values_buf, request.neg_values.len()),
351 )
352 }
353
354 pub fn ilp_exact_nary_score_device(
364 &self,
365 patterns: &IlpExactNaryPatterns<'_>,
366 candidates: &[&CudaBuffer],
367 positives: &CudaBuffer,
368 negatives: &CudaBuffer,
369 ) -> Result<(Vec<u32>, Vec<u32>)> {
370 let mut cand_value_offset: Vec<u32> = Vec::with_capacity(candidates.len());
372 let mut cand_rows: Vec<u32> = Vec::with_capacity(candidates.len());
373 let mut arities: Vec<u32> = Vec::with_capacity(candidates.len());
374 let mut total_elems: u32 = 0;
375 for (slot, buf) in candidates.iter().enumerate() {
376 let (arity, rows) = require_u64_columns(buf, &format!("candidate[{slot}]"))?;
377 cand_value_offset.push(total_elems);
378 cand_rows.push(rows);
379 arities.push(arity);
380 let elems = arity
381 .checked_mul(rows)
382 .and_then(|e| total_elems.checked_add(e))
383 .ok_or_else(|| nary_err("candidate value count exceeds u32"))?;
384 total_elems = elems;
385 }
386 let num_patterns =
387 validate_batch(patterns, &cand_value_offset, &cand_rows, total_elems as u64)?;
388
389 for (atom, (&slot, &claimed)) in patterns
394 .atom_candidate_slot
395 .iter()
396 .zip(patterns.atom_arity.iter())
397 .enumerate()
398 {
399 let actual = arities.get(slot as usize).copied().ok_or_else(|| {
400 nary_err(format!("atom {atom}: candidate slot {slot} out of range"))
401 })?;
402 if claimed != actual {
403 return Err(nary_err(format!(
404 "atom {atom} claims arity {claimed} for candidate slot \
405 {slot}, but that relation has {actual} columns; the \
406 kernel would read past the slot into the next relation",
407 )));
408 }
409 }
410
411 let (pos_arity, num_pos) = require_u64_columns(positives, "positives")?;
412 let (neg_arity, num_neg) = require_u64_columns(negatives, "negatives")?;
413 if pos_arity != patterns.head_arity {
414 return Err(nary_err(format!(
415 "positives arity {pos_arity} != head arity {}",
416 patterns.head_arity
417 )));
418 }
419 if neg_arity != patterns.head_arity && num_neg != 0 {
425 return Err(nary_err(format!(
426 "negatives arity {neg_arity} != head arity {}",
427 patterns.head_arity
428 )));
429 }
430
431 let concat =
433 |bufs: &[(&CudaBuffer, u32, u32)], total: usize| -> Result<TrackedCudaSlice<u8>> {
434 let mut out = self.memory.alloc::<u8>((total * U64_SIZE).max(U64_SIZE))?;
435 let device = self.device.inner();
436 let mut element_offset: usize = 0;
437 for (buf, arity, rows) in bufs {
438 let rows = *rows as usize;
439 for column in 0..*arity as usize {
440 if rows == 0 {
441 continue;
442 }
443 let bytes = rows * U64_SIZE;
444 let col = buf
445 .column(column)
446 .ok_or_else(|| nary_err(format!("missing column {column}")))?;
447 let src = self.column_bytes_view(col, bytes)?;
448 let byte_offset = element_offset * U64_SIZE;
449 let mut dst = out.slice_mut(byte_offset..byte_offset + bytes);
450 device
451 .dtod_copy(&src, &mut dst)
452 .map_err(|e| nary_err(format!("d2d column concat: {e}")))?;
453 element_offset += rows;
454 }
455 }
456 Ok(out)
457 };
458
459 let cand_triples: Vec<(&CudaBuffer, u32, u32)> = candidates
460 .iter()
461 .zip(arities.iter().zip(cand_rows.iter()))
462 .map(|(buf, (&a, &r))| (*buf, a, r))
463 .collect();
464 let cand_values_buf = concat(&cand_triples, total_elems as usize)?;
465 let pos_elems = (pos_arity as usize) * (num_pos as usize);
466 let pos_values_buf = concat(&[(positives, pos_arity, num_pos)], pos_elems)?;
467 let neg_elems = (neg_arity as usize) * (num_neg as usize);
468 let neg_values_buf = concat(&[(negatives, neg_arity, num_neg)], neg_elems)?;
469
470 self.launch_nary(
471 patterns,
472 num_patterns,
473 num_pos,
474 num_neg,
475 &cand_value_offset,
476 &cand_rows,
477 u64_view(&cand_values_buf, total_elems as usize),
478 u64_view(&pos_values_buf, pos_elems),
479 u64_view(&neg_values_buf, neg_elems),
480 )
481 }
482
483 #[expect(
486 clippy::too_many_arguments,
487 reason = "the launch core mirrors the fixed n-ary kernel inputs and its explicit output counters"
488 )]
489 fn launch_nary(
490 &self,
491 patterns: &IlpExactNaryPatterns<'_>,
492 num_patterns: u32,
493 num_pos: u32,
494 num_neg: u32,
495 cand_value_offset: &[u32],
496 cand_rows: &[u32],
497 cand_values: DeviceMemoryView<u64>,
498 pos_values: DeviceMemoryView<u64>,
499 neg_values: DeviceMemoryView<u64>,
500 ) -> Result<(Vec<u32>, Vec<u32>)> {
501 let device = self.device.inner();
502
503 let atoms = patterns.atom_candidate_slot.len();
507 let mut batch_host: Vec<u32> = Vec::with_capacity(
508 2 * num_patterns as usize + 3 * atoms + patterns.binding_codes.len(),
509 );
510 batch_host.extend_from_slice(patterns.body_offset);
511 batch_host.extend_from_slice(patterns.body_len);
512 batch_host.extend_from_slice(patterns.atom_candidate_slot);
513 batch_host.extend_from_slice(patterns.atom_arity);
514 batch_host.extend_from_slice(patterns.atom_binding_offset);
515 batch_host.extend_from_slice(patterns.binding_codes);
516 let params_host: Vec<u32> = vec![
517 num_patterns,
518 u32::try_from(atoms).map_err(|_| nary_err("atom count exceeds u32"))?,
519 num_pos,
520 num_neg,
521 patterns.head_arity,
522 ];
523
524 macro_rules! upload_u32 {
525 ($name:ident, $host:expr) => {{
526 let host: &[u32] = $host;
527 let mut buf = self.memory.alloc::<u32>(host.len().max(1))?;
528 if !host.is_empty() {
529 self.htod_sync_copy_into_tracked(host, &mut buf)
530 .map_err(|e| {
531 nary_err(format!(concat!("h2d ", stringify!($name), ": {}"), e))
532 })?;
533 }
534 buf
535 }};
536 }
537
538 let batch_buf = upload_u32!(batch, &batch_host);
539 let params_buf = upload_u32!(params, ¶ms_host);
540 let cand_value_offset_buf = upload_u32!(cand_value_offset, cand_value_offset);
541 let cand_rows_buf = upload_u32!(cand_rows, cand_rows);
542
543 let mut pos_covered_buf = self.memory.alloc::<u32>(num_patterns as usize)?;
544 let mut neg_covered_buf = self.memory.alloc::<u32>(num_patterns as usize)?;
545 let func = device
548 .get_func(
549 ILP_EXACT_NARY_MODULE,
550 ilp_exact_nary_kernels::ILP_EXACT_NARY_SCORE,
551 )
552 .ok_or_else(|| nary_err("kernel not loaded"))?;
553 unsafe {
554 func.launch(
555 LaunchConfig {
556 grid_dim: (num_patterns, 1, 1),
557 block_dim: (ILP_EXACT_NARY_BLOCK_SIZE, 1, 1),
558 shared_mem_bytes: 0,
559 },
560 (
561 &batch_buf,
562 ¶ms_buf,
563 &cand_values,
564 &cand_value_offset_buf,
565 &cand_rows_buf,
566 &pos_values,
567 &neg_values,
568 &mut pos_covered_buf,
569 &mut neg_covered_buf,
570 ),
571 )
572 .map_err(|e| nary_err(format!("launch: {e}")))?;
573 }
574 self.device.synchronize()?;
575
576 let mut pos_covered = vec![0u32; num_patterns as usize];
577 self.d2h_transfer_count.fetch_add(1, Ordering::Relaxed);
578 device
579 .dtoh_sync_copy_into(&pos_covered_buf, &mut pos_covered)
580 .map_err(|e| nary_err(format!("dtoh pos_covered: {e}")))?;
581 let mut neg_covered = vec![0u32; num_patterns as usize];
582 self.d2h_transfer_count.fetch_add(1, Ordering::Relaxed);
583 device
584 .dtoh_sync_copy_into(&neg_covered_buf, &mut neg_covered)
585 .map_err(|e| nary_err(format!("dtoh neg_covered: {e}")))?;
586 Ok((pos_covered, neg_covered))
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use xlog_core::{MemoryBudget, ScalarType, Schema};
600
601 use super::{IlpExactNaryPatterns, IlpExactNaryRequest};
602 use crate::CudaKernelProvider;
603
604 fn make_provider() -> Option<CudaKernelProvider> {
624 fn skip_unless_required(
625 context: &str,
626 error: impl std::fmt::Display,
627 ) -> Option<CudaKernelProvider> {
628 if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") {
629 panic!("XLOG_REQUIRE_CUDA=1 but CUDA is unavailable ({context}): {error}");
630 }
631 eprintln!("Skipping n-ary launcher test: CUDA unavailable ({context}): {error}");
632 None
633 }
634
635 let budget = MemoryBudget::with_limit(1024 * 1024 * 1024);
636 match crate::CudaProviderBuilder::new(0, budget).build() {
637 Ok(provider) => Some(provider),
638 Err(error) => skip_unless_required("CudaProviderBuilder::build", error),
639 }
640 }
641
642 const JOIN: u32 = 0x8000_0000;
643
644 fn columnar_buffer(provider: &CudaKernelProvider, columns: &[&[u64]]) -> crate::CudaBuffer {
647 let rows = columns[0].len();
648 let schema = Schema::new(
649 (0..columns.len())
650 .map(|i| (format!("arg{i}"), ScalarType::U64))
651 .collect(),
652 );
653 if rows == 0 {
654 return provider.create_empty_buffer(schema).expect("empty buffer");
655 }
656 let device = provider.device().inner();
657 let mut device_columns = Vec::with_capacity(columns.len());
658 for column in columns {
659 assert_eq!(column.len(), rows, "ragged columns");
660 let bytes: Vec<u8> = column.iter().flat_map(|v| v.to_le_bytes()).collect();
661 let mut buf = provider.memory().alloc::<u8>(bytes.len()).expect("alloc");
662 device
663 .htod_sync_copy_into(&bytes, &mut buf)
664 .expect("h2d column");
665 device_columns.push(buf.into());
666 }
667 provider
668 .buffer_from_columns(device_columns, rows as u64, schema)
669 .expect("buffer_from_columns")
670 }
671
672 #[test]
676 fn nary_kernel_matches_binary_chain_fixture() {
677 let Some(provider) = make_provider() else {
678 return;
679 };
680 let request = IlpExactNaryRequest {
681 body_offset: &[0],
682 body_len: &[2],
683 atom_candidate_slot: &[0, 1],
684 atom_arity: &[2, 2],
685 atom_binding_offset: &[0, 2],
686 binding_codes: &[0, JOIN, JOIN, 1],
688 cand_values: &[1, 2, 2, 3, 2, 3, 4, 4, 5, 6],
690 cand_value_offset: &[0, 4],
691 cand_rows: &[2, 3],
692 pos_values: &[1, 2, 4, 5],
694 neg_values: &[7, 8],
695 head_arity: 2,
696 };
697 let (pos, neg) = provider.ilp_exact_nary_score(&request).unwrap();
698 assert_eq!(pos, vec![2]);
699 assert_eq!(neg, vec![0]);
700 }
701
702 #[test]
706 fn nary_device_ingest_matches_host_path() {
707 let Some(provider) = make_provider() else {
708 return;
709 };
710 let p_b = columnar_buffer(&provider, &[&[1, 2], &[2, 3]]);
711 let p_c = columnar_buffer(&provider, &[&[2, 3, 4], &[4, 5, 6]]);
712 let positives = columnar_buffer(&provider, &[&[1, 2], &[4, 5]]);
713 let negatives = columnar_buffer(&provider, &[&[7], &[8]]);
714 let patterns = IlpExactNaryPatterns {
715 body_offset: &[0],
716 body_len: &[2],
717 atom_candidate_slot: &[0, 1],
718 atom_arity: &[2, 2],
719 atom_binding_offset: &[0, 2],
720 binding_codes: &[0, JOIN, JOIN, 1],
721 head_arity: 2,
722 };
723 let (pos, neg) = provider
724 .ilp_exact_nary_score_device(&patterns, &[&p_b, &p_c], &positives, &negatives)
725 .unwrap();
726 assert_eq!(pos, vec![2]);
727 assert_eq!(neg, vec![0]);
728 }
729
730 #[test]
734 fn nary_kernel_matches_ternary_reference_fixture() {
735 let Some(provider) = make_provider() else {
736 return;
737 };
738 let request = IlpExactNaryRequest {
739 body_offset: &[0],
740 body_len: &[2],
741 atom_candidate_slot: &[0, 1],
742 atom_arity: &[3, 2],
743 atom_binding_offset: &[0, 3],
744 binding_codes: &[0, 1, JOIN, JOIN, 2],
745 cand_values: &[1, 4, 2, 5, 9, 8, 9, 8, 3, 7],
747 cand_value_offset: &[0, 6],
748 cand_rows: &[2, 2],
749 pos_values: &[1, 4, 1, 2, 5, 2, 3, 6, 7],
751 neg_values: &[],
752 head_arity: 3,
753 };
754 let (pos, neg) = provider.ilp_exact_nary_score(&request).unwrap();
755 assert_eq!(pos, vec![1]);
756 assert_eq!(neg, vec![0]);
757 }
758
759 #[test]
762 fn nary_kernel_backtracks_across_atoms() {
763 let Some(provider) = make_provider() else {
764 return;
765 };
766 let request = IlpExactNaryRequest {
767 body_offset: &[0],
768 body_len: &[2],
769 atom_candidate_slot: &[0, 1],
770 atom_arity: &[2, 2],
771 atom_binding_offset: &[0, 2],
772 binding_codes: &[0, JOIN, JOIN, 1],
773 cand_values: &[1, 1, 8, 9, 9, 2],
775 cand_value_offset: &[0, 4],
776 cand_rows: &[2, 1],
777 pos_values: &[1, 1, 2, 3],
779 neg_values: &[],
780 head_arity: 2,
781 };
782 let (pos, neg) = provider.ilp_exact_nary_score(&request).unwrap();
783 assert_eq!(pos, vec![1]);
784 assert_eq!(neg, vec![0]);
785 }
786
787 #[test]
788 fn validation_refuses_malformed_batches_without_a_device() {
789 use super::validate_request;
792
793 let base = IlpExactNaryRequest {
794 body_offset: &[0],
795 body_len: &[1],
796 atom_candidate_slot: &[0],
797 atom_arity: &[2],
798 atom_binding_offset: &[0],
799 binding_codes: &[0, 1],
800 cand_values: &[1, 2],
801 cand_value_offset: &[0],
802 cand_rows: &[1],
803 pos_values: &[1, 2],
804 neg_values: &[],
805 head_arity: 2,
806 };
807 assert!(validate_request(&base).is_ok());
808
809 let empty = IlpExactNaryRequest {
810 body_offset: &[],
811 body_len: &[],
812 ..base
813 };
814 assert!(validate_request(&empty).is_err());
815
816 let bad_slot = IlpExactNaryRequest {
817 atom_candidate_slot: &[5],
818 ..base
819 };
820 assert!(validate_request(&bad_slot).is_err());
821
822 let short_values = IlpExactNaryRequest {
823 cand_rows: &[9],
824 ..base
825 };
826 assert!(validate_request(&short_values).is_err());
827
828 let head_out_of_range = IlpExactNaryRequest {
829 binding_codes: &[0, 7],
830 ..base
831 };
832 assert!(validate_request(&head_out_of_range).is_err());
833
834 let ragged_tuples = IlpExactNaryRequest {
835 pos_values: &[1, 2, 3],
836 ..base
837 };
838 assert!(validate_request(&ragged_tuples).is_err());
839
840 let wide_head = IlpExactNaryRequest {
841 head_arity: 9,
842 pos_values: &[1, 2, 3, 4, 5, 6, 7, 8, 9],
843 binding_codes: &[0, 1],
844 ..base
845 };
846 assert!(validate_request(&wide_head).is_err());
847 }
848}