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 let input_buf = self.execute_node(input)?;
219 let input_rows = input_buf.num_rows();
220 let start = self.profiler.start_op();
221 let result = self.execute_groupby(&input_buf, key_cols, aggs)?;
222 if let Some(start) = start {
223 let mem = self.provider.memory().allocated_bytes();
224 self.profiler
225 .record_op("groupby", input_rows, result.num_rows(), start, mem);
226 self.profiler.record_peak_memory(mem);
227 }
228 Ok(result)
229 }
230
231 RirNode::Union { inputs } => {
232 let mut buffers = Vec::with_capacity(inputs.len());
233 let mut input_rows = 0u64;
234 for input in inputs {
235 let buf = self.execute_node(input)?;
236 input_rows += buf.num_rows();
237 buffers.push(buf);
238 }
239 let start = self.profiler.start_op();
240 let result = self.execute_union(&buffers)?;
241 if let Some(start) = start {
242 let mem = self.provider.memory().allocated_bytes();
243 self.profiler
244 .record_op("union", input_rows, result.num_rows(), start, mem);
245 self.profiler.record_peak_memory(mem);
246 }
247 Ok(result)
248 }
249
250 RirNode::Distinct { input, key_cols } => {
251 let input_buf = self.execute_node(input)?;
252 let input_rows = input_buf.num_rows();
253 let start = self.profiler.start_op();
254 let result = self.execute_distinct(&input_buf, key_cols)?;
255 if let Some(start) = start {
256 let mem = self.provider.memory().allocated_bytes();
257 self.profiler
258 .record_op("dedup", input_rows, result.num_rows(), start, mem);
259 self.profiler.record_peak_memory(mem);
260 }
261 Ok(result)
262 }
263
264 RirNode::Diff { left, right } => {
265 let left_buf = self.execute_node(left)?;
266 let right_buf = self.execute_node(right)?;
267 let input_rows = left_buf.num_rows() + right_buf.num_rows();
268 let start = self.profiler.start_op();
269 let result = self.execute_diff(&left_buf, &right_buf)?;
270 if let Some(start) = start {
271 let mem = self.provider.memory().allocated_bytes();
272 self.profiler
273 .record_op("diff", input_rows, result.num_rows(), start, mem);
274 self.profiler.record_peak_memory(mem);
275 }
276 Ok(result)
277 }
278
279 RirNode::Fixpoint {
280 scc_id,
281 base,
282 recursive,
283 delta_rel,
284 full_rel,
285 } => {
286 self.execute_fixpoint(*scc_id, base, recursive, *delta_rel, *full_rel)
288 }
289 RirNode::TensorMaskedJoin {
290 mask_name,
291 schema_size,
292 left_keys,
293 right_keys,
294 rel_index,
295 head_rel_name,
296 max_active_rules,
297 head_projection,
298 ..
299 } => self.execute_tensor_masked_join(
300 mask_name,
301 *schema_size,
302 left_keys,
303 right_keys,
304 rel_index,
305 head_rel_name,
306 *max_active_rules,
307 head_projection,
308 ),
309 RirNode::MultiWayJoin { fallback, .. } | RirNode::ChainJoin { fallback, .. } => {
316 self.execute_node(fallback)
317 }
318 }
319 }
320
321 fn project_chain_parts(node: &RirNode) -> (&RirNode, Vec<&[ProjectExpr]>) {
322 let mut projections = Vec::new();
323 let mut base = node;
324 while let RirNode::Project { input, columns } = base {
325 projections.push(columns.as_slice());
326 base = input;
327 }
328 projections.reverse();
329 (base, projections)
330 }
331
332 fn execute_project_chain(&mut self, node: &RirNode) -> Result<CudaBuffer> {
333 let (base, projections) = Self::project_chain_parts(node);
334 let mut result = self.execute_node(base)?;
335 for columns in projections {
336 let input = result;
337 let input_rows = input.num_rows();
338 let start = self.profiler.start_op();
339 let projected = self.execute_project(&input, columns)?;
340 if let Some(start) = start {
341 let mem = self.provider.memory().allocated_bytes();
342 self.profiler
343 .record_op("project", input_rows, projected.num_rows(), start, mem);
344 self.profiler.record_peak_memory(mem);
345 }
346 result = projected;
347 }
348 Ok(result)
349 }
350
351 #[allow(clippy::too_many_arguments)]
355 fn execute_join(
356 &mut self,
357 left: &CudaBuffer,
358 right: &CudaBuffer,
359 left_keys: &[usize],
360 right_keys: &[usize],
361 join_type: JoinType,
362 left_rel: Option<RelId>,
363 right_rel: Option<RelId>,
364 ) -> Result<CudaBuffer> {
365 let cuda_join_type = match join_type {
368 JoinType::Inner => CudaJoinType::Inner,
369 JoinType::Semi => CudaJoinType::Semi,
370 JoinType::Anti => CudaJoinType::Anti,
371 JoinType::LeftOuter => CudaJoinType::LeftOuter,
372 };
373
374 let mut out: Option<CudaBuffer> = None;
379
380 if eligible_for_nested_loop(left, right, left_keys, right_keys, join_type) {
392 let num_left = self.provider.device_row_count(left)? as u64;
393 let num_right = self.provider.device_row_count(right)? as u64;
394 let in_threshold = num_left
395 .checked_mul(num_right)
396 .map(|p| p <= NESTED_LOOP_TOTAL_THRESHOLD)
397 .unwrap_or(false);
398 if in_threshold {
399 out = Some(self.provider.nested_loop_join_v2_inner_u32_1key(
400 left,
401 right,
402 left_keys[0],
403 right_keys[0],
404 )?);
405 self.nested_loop_dispatch_count += 1;
406 }
407 }
408
409 if out.is_none() && self.config.resolved_persistent_hash_indexes() {
415 if let Some(build_rel) = right_rel {
416 let build_heat = self
417 .stats
418 .get_relation_stats(build_rel)
419 .map(|s| s.heat)
420 .unwrap_or(0.0);
421 let est_index_bytes = estimate_join_index_bytes(right, right_keys);
422 let budget_bytes = self.provider.memory().budget().device_bytes;
423 let remaining_bytes = self.provider.memory().remaining_bytes();
424
425 let should_index = self.join_index_cache.should_build(
426 est_index_bytes,
427 build_heat,
428 remaining_bytes,
429 budget_bytes,
430 );
431
432 if let Some(build_name) = self.get_rel_name(build_rel).map(|s| s.to_string()) {
433 if let Some(version) = self.store.version(&build_name) {
434 let key = JoinIndexKey::new(
435 build_rel,
436 version,
437 right_keys.to_vec(),
438 right.schema(),
439 self.provider.device().ordinal() as u32,
440 );
441
442 let indexed_result = {
443 self.join_index_cache.get(&key).map(|index| {
444 self.provider.hash_join_v2_with_index(
445 left,
446 right,
447 left_keys,
448 right_keys,
449 cuda_join_type,
450 index,
451 None,
452 )
453 })
454 };
455 if let Some(indexed_result) = indexed_result {
456 match indexed_result {
457 Ok(joined) => out = Some(joined),
458 Err(err) if is_join_index_mismatch(&err) => {
459 self.join_index_cache.remove_stale(&key);
460 }
461 Err(err) => return Err(err),
462 }
463 } else if should_index {
464 let background_build = self
465 .config
466 .resolved_persistent_hash_index_background_build();
467 if background_build {
468 self.join_index_cache.record_background_build_request();
469 }
470 let build_result = if background_build {
471 self.provider
472 .build_join_index_v2_background(right, right_keys)
473 } else {
474 self.provider.build_join_index_v2(right, right_keys)
475 };
476 match build_result {
477 Ok(index) => {
478 if background_build {
479 self.join_index_cache.record_background_build_complete();
480 self.join_index_cache.insert(key, index);
481 self.join_index_cache.record_background_build_deferred();
482 if let Some(stats) =
483 self.stats.get_relation_stats_mut(build_rel)
484 {
485 stats.has_index = true;
486 }
487 } else {
488 match self.provider.hash_join_v2_with_index(
489 left,
490 right,
491 left_keys,
492 right_keys,
493 cuda_join_type,
494 &index,
495 None,
496 ) {
497 Ok(joined) => {
498 self.join_index_cache.insert(key, index);
499 if let Some(stats) =
500 self.stats.get_relation_stats_mut(build_rel)
501 {
502 stats.has_index = true;
503 }
504 out = Some(joined);
505 }
506 Err(err) if is_join_index_mismatch(&err) => {}
507 Err(err) => return Err(err),
508 }
509 }
510 }
511 Err(_) => {
512 }
514 }
515 }
516 }
517 }
518 }
519 } let out = match out {
522 Some(buf) => buf,
523 None => {
524 self.provider
525 .hash_join_v2(left, right, left_keys, right_keys, cuda_join_type)?
526 }
527 };
528
529 if let (Some(l), Some(r)) = (left_rel, right_rel) {
530 let input_rows = left.num_rows().saturating_mul(right.num_rows());
531 self.record_adaptive_join_observation(
532 l,
533 r,
534 left_keys,
535 right_keys,
536 input_rows,
537 out.num_rows(),
538 );
539 self.stats.record_join_result(
540 l,
541 r,
542 left_keys.to_vec(),
543 right_keys.to_vec(),
544 input_rows,
545 out.num_rows(),
546 );
547 }
548
549 Ok(out)
550 }
551
552 fn execute_groupby(
556 &self,
557 input: &CudaBuffer,
558 key_cols: &[usize],
559 aggs: &[(usize, AggOp)],
560 ) -> Result<CudaBuffer> {
561 if aggs.is_empty() {
562 return self.provider.dedup(input, key_cols);
564 }
565
566 self.provider.groupby_multi_agg(input, key_cols, aggs)
568 }
569
570 pub(super) fn execute_union(&self, inputs: &[CudaBuffer]) -> Result<CudaBuffer> {
574 if inputs.is_empty() {
575 return self.provider.create_empty_buffer(Schema::new(vec![]));
576 }
577
578 if inputs.len() == 1 {
579 return self.clone_buffer(&inputs[0]);
580 }
581
582 let input_refs: Vec<&CudaBuffer> = inputs.iter().collect();
585 self.provider.union_many_gpu(&input_refs)
586 }
587
588 pub(super) fn execute_distinct(
592 &self,
593 input: &CudaBuffer,
594 key_cols: &[usize],
595 ) -> Result<CudaBuffer> {
596 self.provider.dedup(input, key_cols)
597 }
598
599 pub(super) fn execute_diff(&self, left: &CudaBuffer, right: &CudaBuffer) -> Result<CudaBuffer> {
603 self.provider.diff_gpu(left, right)
604 }
605
606 #[allow(clippy::too_many_arguments)]
607 fn execute_tensor_masked_join(
608 &mut self,
609 mask_name: &str,
610 schema_size: usize,
611 left_keys: &[usize],
612 right_keys: &[usize],
613 rel_index: &[(RelId, String)],
614 head_rel_name: &str,
615 max_active_rules: usize,
616 head_projection: &[usize],
617 ) -> Result<CudaBuffer> {
618 let ilp_mask = match self.ilp_registry.get_mask(mask_name) {
622 Some(mask) => mask,
623 None => {
624 self.ilp_last_result = Some(IlpTaggedResult {
625 entries: Vec::new(),
626 });
627 let schema = self
629 .store
630 .get(head_rel_name)
631 .map(|buf| buf.schema().clone())
632 .ok_or_else(|| {
633 XlogError::Execution(format!(
634 "TensorMaskedJoin: head relation '{}' not found in store \
635 (was load_facts_into_store called?)",
636 head_rel_name
637 ))
638 })?;
639 return self.provider.create_empty_buffer(schema);
640 }
641 };
642
643 let start = self.profiler.start_op();
644
645 let head_k = rel_index
646 .iter()
647 .position(|(_, name)| name == head_rel_name)
648 .ok_or_else(|| {
649 XlogError::Execution(format!(
650 "TensorMaskedJoin: head relation '{}' not found in rel_index",
651 head_rel_name
652 ))
653 })? as u32;
654
655 let mut tag_entries: Vec<IlpTagEntry> = Vec::new();
656 let mut process_rule = |i: u32,
657 j: u32,
658 k: u32,
659 strict_candidate_idx: Option<usize>,
660 strict_flags: Option<&CudaBuffer>|
661 -> Result<()> {
662 if k != head_k {
663 return Ok(());
664 }
665
666 let (_, left_name) = &rel_index[i as usize];
667 let (_, right_name) = &rel_index[j as usize];
668
669 let left_buf = match self.store.get(left_name) {
670 Some(buf) if buf.arity() > 0 => buf,
671 _ => return Ok(()),
672 };
673 let right_buf = match self.store.get(right_name) {
674 Some(buf) if buf.arity() > 0 => buf,
675 _ => return Ok(()),
676 };
677
678 let left_max_key = left_keys.iter().copied().max().unwrap_or(0);
685 let right_max_key = right_keys.iter().copied().max().unwrap_or(0);
686 if left_buf.arity() <= left_max_key || right_buf.arity() <= right_max_key {
687 return Ok(());
688 }
689
690 let joined = self.provider.hash_join_v2(
691 left_buf,
692 right_buf,
693 left_keys,
694 right_keys,
695 CudaJoinType::Inner,
696 )?;
697
698 let projected = if !head_projection.is_empty() && head_projection.len() < joined.arity()
702 {
703 let proj_exprs: Vec<ProjectExpr> = head_projection
704 .iter()
705 .map(|&col| ProjectExpr::Column(col))
706 .collect();
707 self.execute_project(&joined, &proj_exprs)?
708 } else {
709 joined
710 };
711
712 let projected = if let (Some(candidate_idx), Some(active_flags)) =
713 (strict_candidate_idx, strict_flags)
714 {
715 self.provider.filter_buffer_by_candidate_flag(
716 &projected,
717 active_flags,
718 candidate_idx,
719 )?
720 } else {
721 projected
722 };
723
724 let num_rows = read_device_row_count(&self.provider, &projected)? as u32;
726
727 if num_rows > 0 {
728 tag_entries.push(IlpTagEntry {
729 i,
730 j,
731 k,
732 num_rows,
733 buffer: Some(projected),
734 });
735 }
736 Ok(())
737 };
738
739 let active_rule_count = match ilp_mask {
740 IlpMask::Dense { hard, soft, .. } => {
741 let active_rules = self.provider.extract_active_rule_indices(
742 hard,
743 soft,
744 schema_size,
745 max_active_rules,
746 )?;
747 let count = active_rules.len() as u64;
748 for &(i, j, k) in &active_rules {
749 process_rule(i, j, k, None, None)?;
750 }
751 count
752 }
753 IlpMask::Sparse { active_entries, .. } => {
754 let limit = max_active_rules.min(active_entries.len());
755 for &(i, j, k) in &active_entries[..limit] {
756 process_rule(i, j, k, None, None)?;
757 }
758 limit as u64
759 }
760 IlpMask::SparseDevice {
761 candidate_order,
762 active_flags,
763 selected_count,
764 ..
765 } => {
766 if *selected_count > 0 {
767 for (candidate_idx, &(i, j, k)) in candidate_order.iter().enumerate() {
768 process_rule(i, j, k, Some(candidate_idx), Some(active_flags))?;
769 }
770 }
771 (*selected_count).min(max_active_rules) as u64
772 }
773 };
774
775 let mut bufs_by_k: HashMap<u32, Vec<&CudaBuffer>> = HashMap::new();
777 for entry in &tag_entries {
778 if let Some(ref buf) = entry.buffer {
779 bufs_by_k.entry(entry.k).or_default().push(buf);
780 }
781 }
782
783 for (k, buffers) in bufs_by_k {
784 let (_, target_name) = &rel_index[k as usize];
785
786 let union_buf = self.provider.union_many_gpu(&buffers)?;
791
792 if let Some(existing) = self.store.get(target_name) {
794 let delta = self.provider.diff_gpu(&union_buf, existing)?;
795 if !delta.is_empty() {
796 let merged = self.provider.union_gpu(existing, &delta)?;
797 self.store_put(target_name, merged);
798 }
799 } else {
800 let key_cols: Vec<usize> = (0..union_buf.arity()).collect();
801 let deduped = self.provider.dedup(&union_buf, &key_cols)?;
802 self.store_put(target_name, deduped);
803 }
804 }
805
806 self.ilp_last_result = Some(IlpTaggedResult {
808 entries: tag_entries,
809 });
810
811 if let Some(start) = start {
812 let mem = self.provider.memory().allocated_bytes();
813 self.profiler
814 .record_op("TensorMaskedJoin", 0, active_rule_count, start, mem);
815 }
816
817 let schema = self
819 .store
820 .get(head_rel_name)
821 .map(|buf| buf.schema().clone())
822 .ok_or_else(|| {
823 XlogError::Execution(format!(
824 "TensorMaskedJoin: head relation '{}' not found in store \
825 (was load_facts_into_store called?)",
826 head_rel_name
827 ))
828 })?;
829 self.provider.create_empty_buffer(schema)
830 }
831}