1use std::collections::HashMap;
4
5use xlog_core::{AggOp, RelId, Result, ScalarType, Schema, XlogError};
6use xlog_cuda::provider::NESTED_LOOP_TOTAL_THRESHOLD;
7use xlog_cuda::{CudaBuffer, JoinType as CudaJoinType};
8use xlog_ir::{JoinType, ProjectExpr, RirNode};
9
10use crate::ilp_registry::{read_device_row_count, IlpMask, IlpTagEntry, IlpTaggedResult};
11
12use super::join_cache::{estimate_join_index_bytes, JoinIndexKey};
13use super::Executor;
14
15fn eligible_for_nested_loop(
40 left: &CudaBuffer,
41 right: &CudaBuffer,
42 left_keys: &[usize],
43 right_keys: &[usize],
44 join_type: JoinType,
45) -> bool {
46 if join_type != JoinType::Inner {
47 return false;
48 }
49 if left_keys.len() != 1 || right_keys.len() != 1 {
50 return false;
51 }
52 let lt = left.schema().column_type(left_keys[0]);
53 let rt = right.schema().column_type(right_keys[0]);
54 lt == rt && matches!(lt, Some(ScalarType::U32) | Some(ScalarType::Symbol))
55}
56
57fn is_join_index_mismatch(err: &XlogError) -> bool {
58 matches!(
59 err,
60 XlogError::Kernel(msg)
61 if msg.contains("Join index row count does not match right relation")
62 || msg.contains("Join index key columns do not match requested right_keys")
63 )
64}
65
66impl Executor {
67 pub(super) fn execute_scan(&mut self, rel: RelId) -> Result<CudaBuffer> {
69 let name = self
70 .get_rel_name(rel)
71 .ok_or_else(|| XlogError::Execution(format!("Unknown relation: RelId({})", rel.0)))?;
72
73 let buffer = self
74 .store
75 .get(name)
76 .ok_or_else(|| XlogError::Execution(format!("Relation not found: {}", name)))?;
77
78 self.stats.record_access(rel);
79 self.stats.update_cardinality(rel, buffer.num_rows());
80 self.stats.update_byte_size(rel, buffer.estimated_bytes());
81
82 self.clone_buffer(buffer)
83 }
84
85 pub fn execute_node(&mut self, node: &RirNode) -> Result<CudaBuffer> {
99 if !self.common_subexpression_enabled()? || !Self::is_common_subexpression_cacheable(node) {
100 return self.execute_node_uncached(node);
101 }
102
103 let Some(key) = self.common_subexpression_key(node) else {
104 return self.execute_node_uncached(node);
105 };
106
107 if self.common_subexpression_cache.contains_key(&key) {
108 let cached = self
109 .common_subexpression_cache
110 .remove(&key)
111 .expect("cache key checked above");
112 let result = self.clone_buffer(&cached)?;
113 self.common_subexpression_cache.insert(key, cached);
114 self.common_subexpression_stats.hits =
115 self.common_subexpression_stats.hits.saturating_add(1);
116 return Ok(result);
117 }
118
119 self.common_subexpression_stats.misses =
120 self.common_subexpression_stats.misses.saturating_add(1);
121 let result = self.execute_node_uncached(node)?;
122 let cached = self.clone_buffer(&result)?;
123 self.common_subexpression_cache.insert(key, cached);
124 Ok(result)
125 }
126
127 fn execute_node_uncached(&mut self, node: &RirNode) -> Result<CudaBuffer> {
128 match node {
129 RirNode::Unit => {
130 let mut d_num_rows = self.provider.memory().alloc::<u32>(1)?;
132 self.provider
133 .htod_launch_metadata_sync_copy_into(&[1u32], &mut d_num_rows)
134 .map_err(|e| {
135 XlogError::Kernel(format!("Failed to create unit row count: {}", e))
136 })?;
137 Ok(CudaBuffer::from_columns(
138 Vec::new(),
139 1,
140 d_num_rows,
141 Schema::new(vec![]),
142 ))
143 }
144
145 RirNode::Scan { rel } => {
146 let start = self.profiler.start_op();
147 let result = self.execute_scan(*rel)?;
148 if let Some(start) = start {
149 let mem = self.provider.memory().allocated_bytes();
150 self.profiler
151 .record_op("scan", 0, result.num_rows(), start, mem);
152 self.profiler.record_peak_memory(mem);
153 }
154 Ok(result)
155 }
156
157 RirNode::Filter { input, predicate } => {
158 let input_buf = self.execute_node(input)?;
159 let input_rows = input_buf.num_rows();
160 let start = self.profiler.start_op();
161 let result = self.execute_filter(&input_buf, predicate)?;
162 if let Some(start) = start {
163 let mem = self.provider.memory().allocated_bytes();
164 self.profiler
165 .record_op("filter", input_rows, result.num_rows(), start, mem);
166 self.profiler.record_peak_memory(mem);
167 }
168 Ok(result)
169 }
170
171 RirNode::Project { .. } => self.execute_project_chain(node),
172
173 RirNode::Join {
174 left,
175 right,
176 left_keys,
177 right_keys,
178 join_type,
179 } => {
180 let left_rel = match left.as_ref() {
181 RirNode::Scan { rel } => Some(*rel),
182 _ => None,
183 };
184 let right_rel = match right.as_ref() {
185 RirNode::Scan { rel } => Some(*rel),
186 _ => None,
187 };
188 let left_buf = self.execute_node(left)?;
189 let right_buf = self.execute_node(right)?;
190 let input_rows = left_buf.num_rows() + right_buf.num_rows();
191 let start = self.profiler.start_op();
192 let result = self.execute_join(
193 &left_buf, &right_buf, left_keys, right_keys, *join_type, left_rel, right_rel,
194 )?;
195 if let Some(start) = start {
196 let mem = self.provider.memory().allocated_bytes();
197 self.profiler
198 .record_op("join", input_rows, result.num_rows(), start, mem);
199 self.profiler.record_peak_memory(mem);
200 }
201 Ok(result)
202 }
203
204 RirNode::GroupBy {
205 input,
206 key_cols,
207 aggs,
208 } => {
209 if let Some(fused) =
214 self.try_dispatch_wcoj_groupby_root_agg(input, key_cols, aggs)?
215 {
216 return Ok(fused);
217 }
218 self.record_wcoj_groupby_fallback(input);
219 let input_buf = self.execute_node(input)?;
220 let input_rows = input_buf.num_rows();
221 let start = self.profiler.start_op();
222 let result = self.execute_groupby(&input_buf, key_cols, aggs)?;
223 if let Some(start) = start {
224 let mem = self.provider.memory().allocated_bytes();
225 self.profiler
226 .record_op("groupby", input_rows, result.num_rows(), start, mem);
227 self.profiler.record_peak_memory(mem);
228 }
229 Ok(result)
230 }
231
232 RirNode::Union { inputs } => {
233 let mut buffers = Vec::with_capacity(inputs.len());
234 let mut input_rows = 0u64;
235 for input in inputs {
236 let buf = self.execute_node(input)?;
237 input_rows += buf.num_rows();
238 buffers.push(buf);
239 }
240 let start = self.profiler.start_op();
241 let result = self.execute_union(&buffers)?;
242 if let Some(start) = start {
243 let mem = self.provider.memory().allocated_bytes();
244 self.profiler
245 .record_op("union", input_rows, result.num_rows(), start, mem);
246 self.profiler.record_peak_memory(mem);
247 }
248 Ok(result)
249 }
250
251 RirNode::Distinct { input, key_cols } => {
252 let input_buf = self.execute_node(input)?;
253 let input_rows = input_buf.num_rows();
254 let start = self.profiler.start_op();
255 let result = self.execute_distinct(&input_buf, key_cols)?;
256 if let Some(start) = start {
257 let mem = self.provider.memory().allocated_bytes();
258 self.profiler
259 .record_op("dedup", input_rows, result.num_rows(), start, mem);
260 self.profiler.record_peak_memory(mem);
261 }
262 Ok(result)
263 }
264
265 RirNode::Diff { left, right } => {
266 let left_buf = self.execute_node(left)?;
267 let right_buf = self.execute_node(right)?;
268 let input_rows = left_buf.num_rows() + right_buf.num_rows();
269 let start = self.profiler.start_op();
270 let result = self.execute_diff(&left_buf, &right_buf)?;
271 if let Some(start) = start {
272 let mem = self.provider.memory().allocated_bytes();
273 self.profiler
274 .record_op("diff", input_rows, result.num_rows(), start, mem);
275 self.profiler.record_peak_memory(mem);
276 }
277 Ok(result)
278 }
279
280 RirNode::Fixpoint {
281 scc_id,
282 base,
283 recursive,
284 delta_rel,
285 full_rel,
286 } => {
287 self.execute_fixpoint(*scc_id, base, recursive, *delta_rel, *full_rel)
289 }
290 RirNode::TensorMaskedJoin {
291 mask_name,
292 schema_size,
293 left_keys,
294 right_keys,
295 rel_index,
296 head_rel_name,
297 max_active_rules,
298 head_projection,
299 ..
300 } => self.execute_tensor_masked_join(
301 mask_name,
302 *schema_size,
303 left_keys,
304 right_keys,
305 rel_index,
306 head_rel_name,
307 *max_active_rules,
308 head_projection,
309 ),
310 RirNode::MultiWayJoin { fallback, .. } | RirNode::ChainJoin { fallback, .. } => {
317 self.record_wcoj_body_fallback(node);
318 self.execute_node(fallback)
319 }
320 }
321 }
322
323 fn project_chain_parts(node: &RirNode) -> (&RirNode, Vec<&[ProjectExpr]>) {
324 let mut projections = Vec::new();
325 let mut base = node;
326 while let RirNode::Project { input, columns } = base {
327 projections.push(columns.as_slice());
328 base = input;
329 }
330 projections.reverse();
331 (base, projections)
332 }
333
334 fn execute_project_chain(&mut self, node: &RirNode) -> Result<CudaBuffer> {
335 let (base, projections) = Self::project_chain_parts(node);
336 let mut result = self.execute_node(base)?;
337 for columns in projections {
338 let input = result;
339 let input_rows = input.num_rows();
340 let start = self.profiler.start_op();
341 let projected = self.execute_project(&input, columns)?;
342 if let Some(start) = start {
343 let mem = self.provider.memory().allocated_bytes();
344 self.profiler
345 .record_op("project", input_rows, projected.num_rows(), start, mem);
346 self.profiler.record_peak_memory(mem);
347 }
348 result = projected;
349 }
350 Ok(result)
351 }
352
353 #[expect(
357 clippy::too_many_arguments,
358 reason = "join dispatch keeps the two inputs, key sets, join semantics, and optional relation identities explicit"
359 )]
360 fn execute_join(
361 &mut self,
362 left: &CudaBuffer,
363 right: &CudaBuffer,
364 left_keys: &[usize],
365 right_keys: &[usize],
366 join_type: JoinType,
367 left_rel: Option<RelId>,
368 right_rel: Option<RelId>,
369 ) -> Result<CudaBuffer> {
370 let cuda_join_type = match join_type {
373 JoinType::Inner => CudaJoinType::Inner,
374 JoinType::Semi => CudaJoinType::Semi,
375 JoinType::Anti => CudaJoinType::Anti,
376 JoinType::LeftOuter => CudaJoinType::LeftOuter,
377 };
378
379 let mut out: Option<CudaBuffer> = None;
384
385 if eligible_for_nested_loop(left, right, left_keys, right_keys, join_type) {
397 let num_left = self.provider.device_row_count(left)? as u64;
398 let num_right = self.provider.device_row_count(right)? as u64;
399 let in_threshold = num_left
400 .checked_mul(num_right)
401 .map(|p| p <= NESTED_LOOP_TOTAL_THRESHOLD)
402 .unwrap_or(false);
403 if in_threshold {
404 out = Some(self.provider.nested_loop_join_v2_inner_u32_1key(
405 left,
406 right,
407 left_keys[0],
408 right_keys[0],
409 )?);
410 self.nested_loop_dispatch_count += 1;
411 }
412 }
413
414 if out.is_none() && self.config.resolved_persistent_hash_indexes()? {
420 if let Some(build_rel) = right_rel {
421 let build_heat = self
422 .stats
423 .get_relation_stats(build_rel)
424 .map(|s| s.heat)
425 .unwrap_or(0.0);
426 let est_index_bytes = estimate_join_index_bytes(right, right_keys);
427 let budget_bytes = self.provider.memory().budget().device_bytes;
428 let remaining_bytes = self.provider.memory().remaining_bytes();
429
430 let should_index = self.join_index_cache.should_build(
431 est_index_bytes,
432 build_heat,
433 remaining_bytes,
434 budget_bytes,
435 );
436
437 if let Some(build_name) = self.get_rel_name(build_rel).map(|s| s.to_string()) {
438 if let Some(version) = self.store.version(&build_name) {
439 let key = JoinIndexKey::new(
440 build_rel,
441 version,
442 right_keys.to_vec(),
443 right.schema(),
444 self.provider.device().ordinal() as u32,
445 );
446
447 let indexed_result = {
448 self.join_index_cache.get(&key).map(|index| {
449 self.provider.hash_join_v2_with_index(
450 left,
451 right,
452 left_keys,
453 right_keys,
454 cuda_join_type,
455 index,
456 None,
457 )
458 })
459 };
460 if let Some(indexed_result) = indexed_result {
461 match indexed_result {
462 Ok(joined) => out = Some(joined),
463 Err(err) if is_join_index_mismatch(&err) => {
464 self.join_index_cache.remove_stale(&key);
465 }
466 Err(err) => return Err(err),
467 }
468 } else if should_index {
469 let background_build = self
470 .config
471 .resolved_persistent_hash_index_background_build()?;
472 if background_build {
473 self.join_index_cache.record_background_build_request();
474 }
475 let build_result = if background_build {
476 self.provider
477 .build_join_index_v2_background(right, right_keys)
478 } else {
479 self.provider.build_join_index_v2(right, right_keys)
480 };
481 match build_result {
482 Ok(index) => {
483 if background_build {
484 self.join_index_cache.record_background_build_complete();
485 self.join_index_cache.insert(key, index);
486 self.join_index_cache.record_background_build_deferred();
487 if let Some(stats) =
488 self.stats.get_relation_stats_mut(build_rel)
489 {
490 stats.has_index = true;
491 }
492 } else {
493 match self.provider.hash_join_v2_with_index(
494 left,
495 right,
496 left_keys,
497 right_keys,
498 cuda_join_type,
499 &index,
500 None,
501 ) {
502 Ok(joined) => {
503 self.join_index_cache.insert(key, index);
504 if let Some(stats) =
505 self.stats.get_relation_stats_mut(build_rel)
506 {
507 stats.has_index = true;
508 }
509 out = Some(joined);
510 }
511 Err(err) if is_join_index_mismatch(&err) => {}
512 Err(err) => return Err(err),
513 }
514 }
515 }
516 Err(_) => {
517 }
519 }
520 }
521 }
522 }
523 }
524 } let out = match out {
527 Some(buf) => buf,
528 None => {
529 self.provider
530 .hash_join_v2(left, right, left_keys, right_keys, cuda_join_type)?
531 }
532 };
533
534 if let (Some(l), Some(r)) = (left_rel, right_rel) {
535 let input_rows = left.num_rows().saturating_mul(right.num_rows());
536 self.record_adaptive_join_observation(
537 l,
538 r,
539 left_keys,
540 right_keys,
541 input_rows,
542 out.num_rows(),
543 );
544 self.stats.record_join_result(
545 l,
546 r,
547 left_keys.to_vec(),
548 right_keys.to_vec(),
549 input_rows,
550 out.num_rows(),
551 );
552 }
553
554 Ok(out)
555 }
556
557 fn execute_groupby(
561 &self,
562 input: &CudaBuffer,
563 key_cols: &[usize],
564 aggs: &[(usize, AggOp)],
565 ) -> Result<CudaBuffer> {
566 if aggs.is_empty() {
567 return self.provider.dedup(input, key_cols);
569 }
570
571 self.provider.groupby_multi_agg(input, key_cols, aggs)
573 }
574
575 pub(super) fn execute_union(&self, inputs: &[CudaBuffer]) -> Result<CudaBuffer> {
579 if inputs.is_empty() {
580 return self.provider.create_empty_buffer(Schema::new(vec![]));
581 }
582
583 if inputs.len() == 1 {
584 return self.clone_buffer(&inputs[0]);
585 }
586
587 let input_refs: Vec<&CudaBuffer> = inputs.iter().collect();
590 self.provider.union_many_gpu(&input_refs)
591 }
592
593 pub(super) fn execute_distinct(
597 &self,
598 input: &CudaBuffer,
599 key_cols: &[usize],
600 ) -> Result<CudaBuffer> {
601 self.provider.dedup(input, key_cols)
602 }
603
604 pub(super) fn execute_diff(&self, left: &CudaBuffer, right: &CudaBuffer) -> Result<CudaBuffer> {
608 self.provider.diff_gpu(left, right)
609 }
610
611 #[expect(
612 clippy::too_many_arguments,
613 reason = "tensor-masked join dispatch keeps the registered mask, relation catalog, output identity, and projection contract explicit"
614 )]
615 fn execute_tensor_masked_join(
616 &mut self,
617 mask_name: &str,
618 schema_size: usize,
619 left_keys: &[usize],
620 right_keys: &[usize],
621 rel_index: &[(RelId, String)],
622 head_rel_name: &str,
623 max_active_rules: usize,
624 head_projection: &[usize],
625 ) -> Result<CudaBuffer> {
626 let ilp_mask = match self.ilp_registry.get_mask(mask_name) {
630 Some(mask) => mask,
631 None => {
632 self.ilp_last_result = Some(IlpTaggedResult {
633 entries: Vec::new(),
634 });
635 let schema = self
637 .store
638 .get(head_rel_name)
639 .map(|buf| buf.schema().clone())
640 .ok_or_else(|| {
641 XlogError::Execution(format!(
642 "TensorMaskedJoin: head relation '{}' not found in store \
643 (was load_facts_into_store called?)",
644 head_rel_name
645 ))
646 })?;
647 return self.provider.create_empty_buffer(schema);
648 }
649 };
650
651 let start = self.profiler.start_op();
652
653 let head_k = rel_index
654 .iter()
655 .position(|(_, name)| name == head_rel_name)
656 .ok_or_else(|| {
657 XlogError::Execution(format!(
658 "TensorMaskedJoin: head relation '{}' not found in rel_index",
659 head_rel_name
660 ))
661 })? as u32;
662
663 let mut tag_entries: Vec<IlpTagEntry> = Vec::new();
664 let mut process_rule = |i: u32,
665 j: u32,
666 k: u32,
667 strict_candidate_idx: Option<usize>,
668 strict_flags: Option<&CudaBuffer>|
669 -> Result<()> {
670 if k != head_k {
671 return Ok(());
672 }
673
674 let (_, left_name) = &rel_index[i as usize];
675 let (_, right_name) = &rel_index[j as usize];
676
677 let left_buf = match self.store.get(left_name) {
678 Some(buf) if buf.arity() > 0 => buf,
679 _ => return Ok(()),
680 };
681 let right_buf = match self.store.get(right_name) {
682 Some(buf) if buf.arity() > 0 => buf,
683 _ => return Ok(()),
684 };
685
686 let left_max_key = left_keys.iter().copied().max().unwrap_or(0);
693 let right_max_key = right_keys.iter().copied().max().unwrap_or(0);
694 if left_buf.arity() <= left_max_key || right_buf.arity() <= right_max_key {
695 return Ok(());
696 }
697
698 let joined = self.provider.hash_join_v2(
699 left_buf,
700 right_buf,
701 left_keys,
702 right_keys,
703 CudaJoinType::Inner,
704 )?;
705
706 let projected = if !head_projection.is_empty() && head_projection.len() < joined.arity()
710 {
711 let proj_exprs: Vec<ProjectExpr> = head_projection
712 .iter()
713 .map(|&col| ProjectExpr::Column(col))
714 .collect();
715 self.execute_project(&joined, &proj_exprs)?
716 } else {
717 joined
718 };
719
720 let projected = if let (Some(candidate_idx), Some(active_flags)) =
721 (strict_candidate_idx, strict_flags)
722 {
723 self.provider.filter_buffer_by_candidate_flag(
724 &projected,
725 active_flags,
726 candidate_idx,
727 )?
728 } else {
729 projected
730 };
731
732 let num_rows = read_device_row_count(&self.provider, &projected)? as u32;
734
735 if num_rows > 0 {
736 tag_entries.push(IlpTagEntry {
737 i,
738 j,
739 k,
740 num_rows,
741 buffer: Some(projected),
742 });
743 }
744 Ok(())
745 };
746
747 let active_rule_count = match ilp_mask {
748 IlpMask::Dense { hard, soft, .. } => {
749 let active_rules = self.provider.extract_active_rule_indices(
750 hard,
751 soft,
752 schema_size,
753 max_active_rules,
754 )?;
755 let count = active_rules.len() as u64;
756 for &(i, j, k) in &active_rules {
757 process_rule(i, j, k, None, None)?;
758 }
759 count
760 }
761 IlpMask::Sparse { active_entries, .. } => {
762 let limit = max_active_rules.min(active_entries.len());
763 for &(i, j, k) in &active_entries[..limit] {
764 process_rule(i, j, k, None, None)?;
765 }
766 limit as u64
767 }
768 IlpMask::SparseDevice {
769 candidate_order,
770 active_flags,
771 selected_count,
772 ..
773 } => {
774 if *selected_count > 0 {
775 for (candidate_idx, &(i, j, k)) in candidate_order.iter().enumerate() {
776 process_rule(i, j, k, Some(candidate_idx), Some(active_flags))?;
777 }
778 }
779 (*selected_count).min(max_active_rules) as u64
780 }
781 };
782
783 let mut bufs_by_k: HashMap<u32, Vec<&CudaBuffer>> = HashMap::new();
785 for entry in &tag_entries {
786 if let Some(ref buf) = entry.buffer {
787 bufs_by_k.entry(entry.k).or_default().push(buf);
788 }
789 }
790
791 for (k, buffers) in bufs_by_k {
792 let (_, target_name) = &rel_index[k as usize];
793
794 let union_buf = self.provider.union_many_gpu(&buffers)?;
799
800 if let Some(existing) = self.store.get(target_name) {
802 let delta = self.provider.diff_gpu(&union_buf, existing)?;
803 if !delta.is_empty() {
804 let merged = self.provider.union_gpu(existing, &delta)?;
805 self.store_put(target_name, merged);
806 }
807 } else {
808 let key_cols: Vec<usize> = (0..union_buf.arity()).collect();
809 let deduped = self.provider.dedup(&union_buf, &key_cols)?;
810 self.store_put(target_name, deduped);
811 }
812 }
813
814 self.ilp_last_result = Some(IlpTaggedResult {
816 entries: tag_entries,
817 });
818
819 if let Some(start) = start {
820 let mem = self.provider.memory().allocated_bytes();
821 self.profiler
822 .record_op("TensorMaskedJoin", 0, active_rule_count, start, mem);
823 }
824
825 let schema = self
827 .store
828 .get(head_rel_name)
829 .map(|buf| buf.schema().clone())
830 .ok_or_else(|| {
831 XlogError::Execution(format!(
832 "TensorMaskedJoin: head relation '{}' not found in store \
833 (was load_facts_into_store called?)",
834 head_rel_name
835 ))
836 })?;
837 self.provider.create_empty_buffer(schema)
838 }
839}