1use std::collections::HashSet;
77
78use xlog_core::{resolve_bool, RelId, Result, ScalarType, Schema};
79use xlog_cuda::device_runtime::StreamId;
80use xlog_cuda::provider::NESTED_LOOP_TOTAL_THRESHOLD;
81use xlog_cuda::wcoj_metadata::{Wcoj4CycleRootAggValue, WcojRootAggValue};
82use xlog_cuda::CudaBuffer;
83use xlog_cuda::JoinType as CudaJoinType;
84use xlog_ir::{
85 rir::{KCliqueVariableOrder, MultiwayPlan, ProjectExpr, VariableOrder},
86 CompiledRule, RirNode,
87};
88
89use super::Executor;
90
91#[cfg(feature = "wcoj-phase-timing")]
92use std::time::Instant;
93
94pub(super) const ENV_USE_WCOJ_TRIANGLE_U32: &str = "XLOG_USE_WCOJ_TRIANGLE_U32";
96
97pub(super) const ENV_DISABLE_WCOJ_TRIANGLE: &str = "XLOG_DISABLE_WCOJ_TRIANGLE";
99
100pub(super) fn wcoj_gate_enabled(config_override: Option<bool>) -> Result<bool> {
102 resolve_bool(config_override, ENV_USE_WCOJ_TRIANGLE_U32, false)
103}
104
105pub(super) fn wcoj_triangle_disabled(config_override: Option<bool>) -> Result<bool> {
107 resolve_bool(config_override, ENV_DISABLE_WCOJ_TRIANGLE, false)
108}
109
110pub(super) const ENV_WCOJ_BLOCK_WORK_UNIT: &str = "XLOG_WCOJ_BLOCK_WORK_UNIT";
111pub(super) const WCOJ_BLOCK_WORK_UNIT_DEFAULT: u32 = 1024;
112pub(super) const WCOJ_BLOCK_WORK_UNIT_MAX: u32 = 8192;
113
114pub(super) fn wcoj_block_work_unit() -> u32 {
115 match std::env::var(ENV_WCOJ_BLOCK_WORK_UNIT) {
116 Ok(raw) => match raw.trim().parse::<u32>() {
117 Ok(v @ 1..=WCOJ_BLOCK_WORK_UNIT_MAX) => v,
118 Ok(v) => {
119 eprintln!(
120 "warning: {ENV_WCOJ_BLOCK_WORK_UNIT}={v} is outside 1..={WCOJ_BLOCK_WORK_UNIT_MAX}; \
121 using {WCOJ_BLOCK_WORK_UNIT_DEFAULT}"
122 );
123 WCOJ_BLOCK_WORK_UNIT_DEFAULT
124 }
125 Err(_) => {
126 eprintln!(
127 "warning: {ENV_WCOJ_BLOCK_WORK_UNIT}={raw:?} is not a u32; \
128 using {WCOJ_BLOCK_WORK_UNIT_DEFAULT}"
129 );
130 WCOJ_BLOCK_WORK_UNIT_DEFAULT
131 }
132 },
133 Err(_) => WCOJ_BLOCK_WORK_UNIT_DEFAULT,
134 }
135}
136
137pub(super) fn wcoj_adaptive_enabled(config_override: Option<bool>) -> bool {
138 config_override.unwrap_or(true)
139}
140
141pub(super) const ENV_DISABLE_WCOJ_GROUPBY_FUSION: &str = "XLOG_DISABLE_WCOJ_GROUPBY_FUSION";
145
146pub(super) fn wcoj_groupby_fusion_disabled() -> Result<bool> {
147 resolve_bool(None, ENV_DISABLE_WCOJ_GROUPBY_FUSION, false)
148}
149
150pub(super) const ENV_DISABLE_FREE_JOIN: &str = "XLOG_DISABLE_FREE_JOIN";
154
155pub(super) fn free_join_disabled() -> Result<bool> {
156 resolve_bool(None, ENV_DISABLE_FREE_JOIN, false)
157}
158
159pub(super) const ENV_DISABLE_FACTORIZED_DELTA: &str = "XLOG_DISABLE_FACTORIZED_DELTA";
163
164pub(super) fn factorized_delta_disabled() -> Result<bool> {
165 resolve_bool(None, ENV_DISABLE_FACTORIZED_DELTA, false)
166}
167
168const FACTORIZED_DELTA_DEFAULT_MAX_DOMAIN: u32 = 1 << 14;
172
173fn factorized_delta_max_domain() -> u32 {
174 std::env::var("XLOG_FACTORIZED_DELTA_MAX_DOMAIN")
175 .ok()
176 .and_then(|v| v.parse::<u32>().ok())
177 .unwrap_or(FACTORIZED_DELTA_DEFAULT_MAX_DOMAIN)
178 .min(xlog_cuda::provider::FJ_DELTA_MAX_DOMAIN)
179}
180
181fn factorized_delta_max_table_bytes(budget_bytes: u64) -> u64 {
186 std::env::var("XLOG_FACTORIZED_DELTA_MAX_TABLE_BYTES")
187 .ok()
188 .and_then(|v| v.parse::<u64>().ok())
189 .unwrap_or(budget_bytes / 2)
190}
191
192fn factorized_delta_work_divisor() -> u64 {
196 std::env::var("XLOG_FACTORIZED_DELTA_WORK_DIVISOR")
197 .ok()
198 .and_then(|v| v.parse::<u64>().ok())
199 .filter(|&v| v >= 1)
200 .unwrap_or(8)
201}
202
203#[derive(Default)]
209pub(super) struct FactorizedDeltaCtx {
210 domain_by_key: std::collections::HashMap<(String, RelId), Option<u32>>,
211 static_norm_cache: std::collections::HashMap<(RelId, usize), CudaBuffer>,
212}
213
214pub(super) const ENV_WCOJ_STRICT: &str = "XLOG_WCOJ_STRICT";
221
222pub(super) fn wcoj_strict_errors_enabled() -> Result<bool> {
223 resolve_bool(None, ENV_WCOJ_STRICT, false)
224}
225
226pub(super) fn wcoj_decline_on_error(
232 counter: &mut u64,
233 stage: &str,
234 err: xlog_core::XlogError,
235) -> Result<Option<CudaBuffer>> {
236 *counter += 1;
237 if wcoj_strict_errors_enabled()? {
238 return Err(err);
239 }
240 eprintln!("warning: WCOJ {stage} pipeline error; declining to binary-join fallback: {err}");
241 Ok(None)
242}
243
244pub(super) const ENV_WCOJ_CHAIN_ENABLE: &str = "XLOG_WCOJ_CHAIN_ENABLE";
248
249pub(super) fn chain_dispatch_enabled() -> Result<bool> {
250 resolve_bool(None, ENV_WCOJ_CHAIN_ENABLE, true)
251}
252
253pub(super) const ENV_USE_WCOJ_4CYCLE: &str = "XLOG_USE_WCOJ_4CYCLE";
267
268pub(super) const ENV_USE_WCOJ_4CYCLE_ADAPTIVE: &str = "XLOG_USE_WCOJ_4CYCLE_ADAPTIVE";
270
271pub(super) const ENV_DISABLE_WCOJ_4CYCLE: &str = "XLOG_DISABLE_WCOJ_4CYCLE";
273
274pub(super) fn wcoj_4cycle_gate_enabled(config_override: Option<bool>) -> Result<bool> {
276 resolve_bool(config_override, ENV_USE_WCOJ_4CYCLE, false)
277}
278
279pub(super) fn wcoj_4cycle_adaptive_enabled(config_override: Option<bool>) -> Result<bool> {
289 resolve_bool(config_override, ENV_USE_WCOJ_4CYCLE_ADAPTIVE, false)
290}
291
292pub(super) fn wcoj_4cycle_disabled(config_override: Option<bool>) -> Result<bool> {
294 resolve_bool(config_override, ENV_DISABLE_WCOJ_4CYCLE, false)
295}
296
297#[derive(Clone, Copy, Debug, PartialEq, Eq)]
299enum DispatchMode {
300 Force,
301 CostModel,
302}
303
304pub(super) struct ChainRirMatch {
307 pub rel_left: RelId,
308 pub rel_right: RelId,
309 pub left_key: usize,
310 pub right_key: usize,
311 pub output_columns: Vec<ProjectExpr>,
312}
313
314pub(super) fn match_chain_join(body: &RirNode) -> Option<ChainRirMatch> {
318 let RirNode::ChainJoin {
319 left,
320 right,
321 left_key,
322 right_key,
323 output_columns,
324 ..
325 } = body
326 else {
327 return None;
328 };
329 if *left_key >= 2 || *right_key >= 2 {
330 return None;
331 }
332 let rel_left = scan_rel(left)?;
333 let rel_right = scan_rel(right)?;
334 Some(ChainRirMatch {
335 rel_left,
336 rel_right,
337 left_key: *left_key,
338 right_key: *right_key,
339 output_columns: output_columns.clone(),
340 })
341}
342
343fn fallback_scan_filter_equivalents(node: &RirNode) -> (u64, u64) {
344 match node {
345 RirNode::Unit | RirNode::TensorMaskedJoin { .. } => (0, 0),
346 RirNode::Scan { .. } => (1, 0),
347 RirNode::Filter { input, .. } => {
348 let (scans, filters) = fallback_scan_filter_equivalents(input);
349 (scans, filters.saturating_add(1))
350 }
351 RirNode::Project { input, .. }
352 | RirNode::GroupBy { input, .. }
353 | RirNode::Distinct { input, .. } => fallback_scan_filter_equivalents(input),
354 RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
355 let left = fallback_scan_filter_equivalents(left);
356 let right = fallback_scan_filter_equivalents(right);
357 (
358 left.0.saturating_add(right.0),
359 left.1.saturating_add(right.1),
360 )
361 }
362 RirNode::ChainJoin { fallback, .. } | RirNode::MultiWayJoin { fallback, .. } => {
363 fallback_scan_filter_equivalents(fallback)
364 }
365 RirNode::Union { inputs } => inputs.iter().fold((0u64, 0u64), |total, input| {
366 let current = fallback_scan_filter_equivalents(input);
367 (
368 total.0.saturating_add(current.0),
369 total.1.saturating_add(current.1),
370 )
371 }),
372 RirNode::Fixpoint {
373 base, recursive, ..
374 } => {
375 let base = fallback_scan_filter_equivalents(base);
376 let recursive = fallback_scan_filter_equivalents(recursive);
377 (
378 base.0.saturating_add(recursive.0),
379 base.1.saturating_add(recursive.1),
380 )
381 }
382 }
383}
384
385fn record_chain_fallback_equivalents(
386 body: &RirNode,
387 installed: bool,
388 scan_equivalents: &mut u64,
389 filter_equivalents: &mut u64,
390) {
391 if !installed {
392 return;
393 }
394 let RirNode::ChainJoin { fallback, .. } = body else {
395 return;
396 };
397 let (scans, filters) = fallback_scan_filter_equivalents(fallback);
398 *scan_equivalents = scan_equivalents.saturating_add(scans);
399 *filter_equivalents = filter_equivalents.saturating_add(filters);
400}
401
402pub(super) struct TriangleRirMatch {
405 pub rel_xy: RelId,
408 pub rel_yz: RelId,
410 pub rel_xz: RelId,
413}
414
415pub(super) fn match_multiway_triangle(body: &RirNode) -> Option<TriangleRirMatch> {
428 let RirNode::MultiWayJoin {
429 inputs,
430 slot_vars,
431 output_columns,
432 ..
433 } = body
434 else {
435 return None;
436 };
437 if inputs.len() != 3 {
438 return None;
439 }
440 if !slot_vars_match_canonical_triangle(slot_vars) {
441 return None;
442 }
443 if !output_columns_match_canonical_triangle(output_columns) {
444 return None;
445 }
446 let rel_xy = scan_rel(&inputs[0])?;
447 let rel_yz = scan_rel(&inputs[1])?;
448 let rel_xz = scan_rel(&inputs[2])?;
449 Some(TriangleRirMatch {
450 rel_xy,
451 rel_yz,
452 rel_xz,
453 })
454}
455
456fn slot_vars_match_canonical_triangle(slot_vars: &[Vec<Option<u32>>]) -> bool {
461 if slot_vars.len() != 3 {
462 return false;
463 }
464 let s0 = &slot_vars[0];
465 let s1 = &slot_vars[1];
466 let s2 = &slot_vars[2];
467 if s0.len() != 2 || s1.len() != 2 || s2.len() != 2 {
468 return false;
469 }
470 let (a, b) = match (s0[0], s0[1]) {
471 (Some(a), Some(b)) if a != b => (a, b),
472 _ => return false,
473 };
474 let c = match (s1[0], s1[1]) {
475 (Some(b1), Some(c)) if b1 == b && c != a && c != b => c,
476 _ => return false,
477 };
478 matches!((s2[0], s2[1]), (Some(a2), Some(c2)) if a2 == a && c2 == c)
479}
480
481fn output_columns_match_canonical_triangle(cols: &[ProjectExpr]) -> bool {
493 if cols.len() != 3 {
494 return false;
495 }
496 let cols_pattern = (
497 matches!(cols[0], ProjectExpr::Column(0)),
498 matches!(cols[1], ProjectExpr::Column(1)) || matches!(cols[1], ProjectExpr::Column(2)),
499 matches!(cols[2], ProjectExpr::Column(3)),
500 );
501 cols_pattern == (true, true, true)
502}
503
504pub(super) struct FourCycleRirMatch {
512 pub rel_e1: RelId,
513 pub rel_e2: RelId,
514 pub rel_e3: RelId,
515 pub rel_e4: RelId,
516}
517
518pub(super) fn match_multiway_4cycle(body: &RirNode) -> Option<FourCycleRirMatch> {
526 let RirNode::MultiWayJoin {
527 inputs,
528 slot_vars,
529 output_columns,
530 ..
531 } = body
532 else {
533 return None;
534 };
535 if inputs.len() != 4 {
536 return None;
537 }
538 if !slot_vars_match_canonical_4cycle(slot_vars) {
539 return None;
540 }
541 if !output_columns_match_canonical_4cycle(output_columns) {
542 return None;
543 }
544 let rel_e1 = scan_rel(&inputs[0])?;
545 let rel_e2 = scan_rel(&inputs[1])?;
546 let rel_e3 = scan_rel(&inputs[2])?;
547 let rel_e4 = scan_rel(&inputs[3])?;
548 Some(FourCycleRirMatch {
549 rel_e1,
550 rel_e2,
551 rel_e3,
552 rel_e4,
553 })
554}
555
556fn slot_vars_match_canonical_4cycle(slot_vars: &[Vec<Option<u32>>]) -> bool {
561 if slot_vars.len() != 4 {
562 return false;
563 }
564 for s in slot_vars {
565 if s.len() != 2 {
566 return false;
567 }
568 }
569 let (a, b) = match (slot_vars[0][0], slot_vars[0][1]) {
570 (Some(a), Some(b)) if a != b => (a, b),
571 _ => return false,
572 };
573 let c = match (slot_vars[1][0], slot_vars[1][1]) {
574 (Some(b1), Some(c)) if b1 == b && c != a && c != b => c,
575 _ => return false,
576 };
577 let d = match (slot_vars[2][0], slot_vars[2][1]) {
578 (Some(c1), Some(d)) if c1 == c && d != a && d != b && d != c => d,
579 _ => return false,
580 };
581 matches!(
582 (slot_vars[3][0], slot_vars[3][1]),
583 (Some(d2), Some(a2)) if d2 == d && a2 == a
584 )
585}
586
587fn output_columns_match_canonical_4cycle(cols: &[ProjectExpr]) -> bool {
597 if cols.len() != 4 {
598 return false;
599 }
600 let exact = |idx: usize, want: usize| matches!(cols[idx], ProjectExpr::Column(c) if c == want);
601 let default_layout = exact(0, 0) && exact(1, 1) && exact(2, 3) && exact(3, 5);
603 let alt_layout = exact(0, 5) && exact(1, 0) && exact(2, 1) && exact(3, 3);
605 default_layout || alt_layout
606}
607
608fn scan_rel(node: &RirNode) -> Option<RelId> {
613 match node {
614 RirNode::Scan { rel } => Some(*rel),
615 _ => None,
616 }
617}
618
619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623pub(super) enum WcojKeyWidth {
624 FourByte,
625 EightByte,
626}
627
628fn classify_two_col_wcoj_width(buf: &CudaBuffer) -> Option<WcojKeyWidth> {
642 if buf.arity() != 2 {
643 return None;
644 }
645 let c0 = buf.schema().column_type(0)?;
646 let c1 = buf.schema().column_type(1)?;
647 let w0 = scalar_wcoj_width(c0)?;
648 let w1 = scalar_wcoj_width(c1)?;
649 if w0 != w1 {
650 return None;
651 }
652 Some(w0)
653}
654
655fn scalar_wcoj_width(ty: xlog_core::ScalarType) -> Option<WcojKeyWidth> {
656 match ty {
657 xlog_core::ScalarType::U32 | xlog_core::ScalarType::Symbol => Some(WcojKeyWidth::FourByte),
658 xlog_core::ScalarType::U64 => Some(WcojKeyWidth::EightByte),
659 _ => None,
660 }
661}
662
663fn feedback_pair_from_var_order(
691 slot_rels: &[RelId],
692 var_order: Option<&VariableOrder>,
693) -> Option<(RelId, RelId, Vec<usize>, Vec<usize>)> {
694 if slot_rels.len() < 2 {
695 return None;
696 }
697 let Some(vo) = var_order else {
698 return Some((slot_rels[0], slot_rels[1], vec![1], vec![0]));
702 };
703 let leader_idx = vo.leader_idx as usize;
704 match slot_rels.len() {
705 3 => {
706 match leader_idx {
708 0 => Some((slot_rels[0], slot_rels[1], vec![1], vec![0])),
709 1 => {
710 Some((slot_rels[1], slot_rels[2], vec![1], vec![1]))
714 }
715 2 => {
716 Some((slot_rels[2], slot_rels[1], vec![1], vec![1]))
720 }
721 _ => None,
722 }
723 }
724 4 => {
725 if leader_idx >= 4 {
728 return None;
729 }
730 let slot1_input_idx = (leader_idx + 1) % 4;
731 Some((
732 slot_rels[leader_idx],
733 slot_rels[slot1_input_idx],
734 vec![1],
735 vec![0],
736 ))
737 }
738 _ => None,
739 }
740}
741
742fn perm_indices_from_kernel_output_cols(cols: &[ProjectExpr]) -> Result<Vec<usize>> {
743 let mut out = Vec::with_capacity(cols.len());
744 for c in cols {
745 match c {
746 ProjectExpr::Column(idx) => out.push(*idx),
747 other => {
748 return Err(xlog_core::XlogError::Kernel(format!(
749 "perm_indices_from_kernel_output_cols: \
750 kernel_output_cols must be ProjectExpr::Column(_), got {:?}",
751 other
752 )));
753 }
754 }
755 }
756 Ok(out)
757}
758
759fn build_triangle_head_schema(buf_xy: &CudaBuffer, buf_yz: &CudaBuffer) -> Result<Schema> {
764 let x_type = buf_xy.schema().column_type(0).ok_or_else(|| {
765 xlog_core::XlogError::Kernel("build_triangle_head_schema: e_xy.col0 type missing".into())
766 })?;
767 let y_type = buf_xy.schema().column_type(1).ok_or_else(|| {
768 xlog_core::XlogError::Kernel("build_triangle_head_schema: e_xy.col1 type missing".into())
769 })?;
770 let z_type = buf_yz.schema().column_type(1).ok_or_else(|| {
771 xlog_core::XlogError::Kernel("build_triangle_head_schema: e_yz.col1 type missing".into())
772 })?;
773 Schema::new(vec![
774 ("col0".to_string(), x_type),
775 ("col1".to_string(), y_type),
776 ("col2".to_string(), z_type),
777 ])
778 .with_sort_labels(vec![
779 buf_xy
780 .schema()
781 .column_sort_label(0)
782 .unwrap_or("col0")
783 .to_string(),
784 buf_xy
785 .schema()
786 .column_sort_label(1)
787 .unwrap_or("col1")
788 .to_string(),
789 buf_yz
790 .schema()
791 .column_sort_label(1)
792 .unwrap_or("col2")
793 .to_string(),
794 ])
795 .map_err(xlog_core::XlogError::Kernel)
796}
797
798fn build_4cycle_head_schema(
801 buf_e1: &CudaBuffer,
802 buf_e2: &CudaBuffer,
803 buf_e3: &CudaBuffer,
804) -> Result<Schema> {
805 let w_type = buf_e1.schema().column_type(0).ok_or_else(|| {
809 xlog_core::XlogError::Kernel("build_4cycle_head_schema: e_wx.col0 type missing".into())
810 })?;
811 let x_type = buf_e1.schema().column_type(1).ok_or_else(|| {
812 xlog_core::XlogError::Kernel("build_4cycle_head_schema: e_wx.col1 type missing".into())
813 })?;
814 let y_type = buf_e2.schema().column_type(1).ok_or_else(|| {
815 xlog_core::XlogError::Kernel("build_4cycle_head_schema: e_xy.col1 type missing".into())
816 })?;
817 let z_type = buf_e3.schema().column_type(1).ok_or_else(|| {
818 xlog_core::XlogError::Kernel("build_4cycle_head_schema: e_yz.col1 type missing".into())
819 })?;
820 let _: ScalarType = w_type;
824 Schema::new(vec![
825 ("col0".to_string(), w_type),
826 ("col1".to_string(), x_type),
827 ("col2".to_string(), y_type),
828 ("col3".to_string(), z_type),
829 ])
830 .with_sort_labels(vec![
831 buf_e1
832 .schema()
833 .column_sort_label(0)
834 .unwrap_or("col0")
835 .to_string(),
836 buf_e1
837 .schema()
838 .column_sort_label(1)
839 .unwrap_or("col1")
840 .to_string(),
841 buf_e2
842 .schema()
843 .column_sort_label(1)
844 .unwrap_or("col2")
845 .to_string(),
846 buf_e3
847 .schema()
848 .column_sort_label(1)
849 .unwrap_or("col3")
850 .to_string(),
851 ])
852 .map_err(xlog_core::XlogError::Kernel)
853}
854
855impl Executor {
856 pub(super) fn try_dispatch_wcoj_triangle(
867 &mut self,
868 rule: &CompiledRule,
869 ) -> Result<Option<CudaBuffer>> {
870 self.try_dispatch_wcoj_triangle_on_body(&rule.body)
873 }
874
875 fn wcoj_output_rows(buf: &CudaBuffer) -> Option<u64> {
882 buf.cached_row_count().map(u64::from)
885 }
886
887 fn record_wcoj_feedback(
940 &mut self,
941 slot_rels: &[RelId],
942 var_order: Option<&VariableOrder>,
943 output_rows: Option<u64>,
944 ) {
945 if slot_rels.len() < 2 {
946 return;
947 }
948 let Some(out_rows) = output_rows else {
949 return;
950 };
951 let Some((rel_a, rel_b, left_keys, right_keys)) =
958 feedback_pair_from_var_order(slot_rels, var_order)
959 else {
960 return;
961 };
962 let card_a = self
963 .stats
964 .get_relation_stats(rel_a)
965 .map(|s| s.cardinality)
966 .filter(|c| *c > 0);
967 let card_b = self
968 .stats
969 .get_relation_stats(rel_b)
970 .map(|s| s.cardinality)
971 .filter(|c| *c > 0);
972 let (Some(a), Some(b)) = (card_a, card_b) else {
973 return;
974 };
975 let input_rows = a.saturating_mul(b);
976 self.stats
979 .record_join_result(rel_a, rel_b, left_keys, right_keys, input_rows, out_rows);
980 }
981
982 pub(super) fn try_dispatch_wcoj_triangle_on_body(
989 &mut self,
990 body: &RirNode,
991 ) -> Result<Option<CudaBuffer>> {
992 #[cfg(feature = "wcoj-phase-timing")]
993 let wall_start = Instant::now();
994 if wcoj_triangle_disabled(self.config.wcoj_triangle_dispatch_disabled)? {
1004 return Ok(None);
1005 }
1006 let force_override = self.config.wcoj_triangle_dispatch;
1007 let force_on = wcoj_gate_enabled(force_override)?;
1008 let mode = if force_on {
1009 DispatchMode::Force
1010 } else {
1011 let force_explicit_off = matches!(force_override, Some(false));
1015 if force_explicit_off {
1016 return Ok(None);
1017 }
1018 let adaptive_override = self.config.wcoj_triangle_dispatch_adaptive;
1019 if wcoj_adaptive_enabled(adaptive_override) {
1020 DispatchMode::CostModel
1021 } else {
1022 return Ok(None);
1023 }
1024 };
1025
1026 let Some(matched) = match_multiway_triangle(body) else {
1028 return Ok(None);
1029 };
1030
1031 let name_xy = match self.get_rel_name(matched.rel_xy) {
1035 Some(s) => s.to_string(),
1036 None => return Ok(None),
1037 };
1038 let name_yz = match self.get_rel_name(matched.rel_yz) {
1039 Some(s) => s.to_string(),
1040 None => return Ok(None),
1041 };
1042 let name_xz = match self.get_rel_name(matched.rel_xz) {
1043 Some(s) => s.to_string(),
1044 None => return Ok(None),
1045 };
1046
1047 let buf_xy = match self.store.get(&name_xy) {
1052 Some(b) => b,
1053 None => return Ok(None),
1054 };
1055 let buf_yz = match self.store.get(&name_yz) {
1056 Some(b) => b,
1057 None => return Ok(None),
1058 };
1059 let buf_xz = match self.store.get(&name_xz) {
1060 Some(b) => b,
1061 None => return Ok(None),
1062 };
1063 let width = match (
1064 classify_two_col_wcoj_width(buf_xy),
1065 classify_two_col_wcoj_width(buf_yz),
1066 classify_two_col_wcoj_width(buf_xz),
1067 ) {
1068 (Some(a), Some(b), Some(c)) if a == b && b == c => a,
1069 _ => return Ok(None),
1070 };
1071
1072 if self.provider.memory().runtime().is_none() {
1083 return Ok(None);
1084 }
1085 let launch_stream = match self.wcoj_dispatch_stream_or_init() {
1086 Some(s) => s,
1087 None => return Ok(None),
1088 };
1089
1090 #[cfg(feature = "wcoj-phase-timing")]
1093 let mut classifier_ms: f32 = 0.0;
1094 if mode == DispatchMode::CostModel {
1095 #[cfg(feature = "wcoj-phase-timing")]
1096 let cls_start = Instant::now();
1097 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
1098 let slot_rels = [matched.rel_xy, matched.rel_yz, matched.rel_xz];
1099 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
1100 stats: &self.stats,
1101 slot_rels: &slot_rels,
1102 };
1103 let dispatch = model.should_dispatch_triangle(&ctx);
1104 #[cfg(feature = "wcoj-phase-timing")]
1105 {
1106 classifier_ms = cls_start.elapsed().as_secs_f64() as f32 * 1000.0;
1107 }
1108 if !dispatch {
1109 return Ok(None);
1110 }
1111 }
1112
1113 let var_order_opt: Option<&VariableOrder> = match body {
1116 RirNode::MultiWayJoin { var_order, .. } => var_order.as_ref(),
1117 _ => None,
1118 };
1119
1120 #[cfg(feature = "wcoj-phase-timing")]
1126 let mut layout_times: [f32; 3] = [0.0; 3];
1127 let dispatch_result = self.run_wcoj_triangle_pipeline(
1128 buf_xy,
1129 buf_yz,
1130 buf_xz,
1131 launch_stream,
1132 width,
1133 var_order_opt,
1134 #[cfg(feature = "wcoj-phase-timing")]
1135 &mut layout_times,
1136 );
1137 match dispatch_result {
1138 Ok(buf) => {
1139 let output_rows = Self::wcoj_output_rows(&buf);
1157 let slot_rels = [matched.rel_xy, matched.rel_yz, matched.rel_xz];
1158 self.record_wcoj_feedback(&slot_rels, var_order_opt, output_rows);
1159 self.wcoj_triangle_dispatch_count += 1;
1160 #[cfg(feature = "wcoj-phase-timing")]
1161 {
1162 let triangle_timing = self
1163 .provider
1164 .take_wcoj_triangle_phase_timing()
1165 .unwrap_or_default();
1166 let wall_ms = wall_start.elapsed().as_secs_f64() as f32 * 1000.0;
1167 let timing = super::wcoj_phase_timing::WcojDispatchPhaseTiming::new(
1168 classifier_ms,
1169 layout_times[0],
1170 layout_times[1],
1171 layout_times[2],
1172 triangle_timing,
1173 wall_ms,
1174 );
1175 if let Ok(mut g) = self.last_wcoj_phase_timing.lock() {
1176 *g = Some(timing);
1177 }
1178 }
1179 Ok(Some(buf))
1180 }
1181 Err(err) => wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "triangle", err),
1182 }
1183 }
1184
1185 #[cfg_attr(
1196 feature = "wcoj-phase-timing",
1197 expect(
1198 clippy::too_many_arguments,
1199 reason = "triangle dispatch keeps three typed inputs, stream, width, variable order, and optional timing output explicit"
1200 )
1201 )]
1202 fn run_wcoj_triangle_pipeline(
1203 &self,
1204 buf_xy: &CudaBuffer,
1205 buf_yz: &CudaBuffer,
1206 buf_xz: &CudaBuffer,
1207 launch_stream: StreamId,
1208 width: WcojKeyWidth,
1209 var_order: Option<&VariableOrder>,
1210 #[cfg(feature = "wcoj-phase-timing")] layout_times_ms: &mut [f32; 3],
1211 ) -> Result<CudaBuffer> {
1212 if let Some(vo) = var_order {
1218 return self.run_wcoj_triangle_pipeline_with_leader_order(
1219 buf_xy,
1220 buf_yz,
1221 buf_xz,
1222 launch_stream,
1223 width,
1224 vo,
1225 );
1226 }
1227 #[cfg(feature = "wcoj-phase-timing")]
1228 let mut time_layout =
1229 |f: &dyn Fn() -> Result<CudaBuffer>, slot: usize| -> Result<CudaBuffer> {
1230 let s = Instant::now();
1231 let r = f()?;
1232 layout_times_ms[slot] = s.elapsed().as_secs_f64() as f32 * 1000.0;
1233 Ok(r)
1234 };
1235 match width {
1236 WcojKeyWidth::FourByte => {
1237 #[cfg(feature = "wcoj-phase-timing")]
1238 let (layout_xy, layout_yz, layout_xz) = {
1239 let xy = time_layout(
1240 &|| {
1241 self.provider
1242 .wcoj_layout_u32_recorded(buf_xy, launch_stream)
1243 },
1244 0,
1245 )?;
1246 let yz = time_layout(
1247 &|| {
1248 self.provider
1249 .wcoj_layout_u32_recorded(buf_yz, launch_stream)
1250 },
1251 1,
1252 )?;
1253 let xz = time_layout(
1254 &|| {
1255 self.provider
1256 .wcoj_layout_u32_recorded(buf_xz, launch_stream)
1257 },
1258 2,
1259 )?;
1260 (xy, yz, xz)
1261 };
1262 #[cfg(not(feature = "wcoj-phase-timing"))]
1263 let layout_xy = self
1264 .provider
1265 .wcoj_layout_u32_recorded(buf_xy, launch_stream)?;
1266 #[cfg(not(feature = "wcoj-phase-timing"))]
1267 let layout_yz = self
1268 .provider
1269 .wcoj_layout_u32_recorded(buf_yz, launch_stream)?;
1270 #[cfg(not(feature = "wcoj-phase-timing"))]
1271 let layout_xz = self
1272 .provider
1273 .wcoj_layout_u32_recorded(buf_xz, launch_stream)?;
1274 let out = self.provider.wcoj_triangle_hg_u32_recorded(
1275 &layout_xy,
1276 &layout_yz,
1277 &layout_xz,
1278 wcoj_block_work_unit(),
1279 launch_stream,
1280 )?;
1281 self.provider.record_wcoj_triangle_hg_dispatch();
1282 Ok(out)
1283 }
1284 WcojKeyWidth::EightByte => {
1285 #[cfg(feature = "wcoj-phase-timing")]
1286 let (layout_xy, layout_yz, layout_xz) = {
1287 let xy = time_layout(
1288 &|| {
1289 self.provider
1290 .wcoj_layout_u64_recorded(buf_xy, launch_stream)
1291 },
1292 0,
1293 )?;
1294 let yz = time_layout(
1295 &|| {
1296 self.provider
1297 .wcoj_layout_u64_recorded(buf_yz, launch_stream)
1298 },
1299 1,
1300 )?;
1301 let xz = time_layout(
1302 &|| {
1303 self.provider
1304 .wcoj_layout_u64_recorded(buf_xz, launch_stream)
1305 },
1306 2,
1307 )?;
1308 (xy, yz, xz)
1309 };
1310 #[cfg(not(feature = "wcoj-phase-timing"))]
1311 let layout_xy = self
1312 .provider
1313 .wcoj_layout_u64_recorded(buf_xy, launch_stream)?;
1314 #[cfg(not(feature = "wcoj-phase-timing"))]
1315 let layout_yz = self
1316 .provider
1317 .wcoj_layout_u64_recorded(buf_yz, launch_stream)?;
1318 #[cfg(not(feature = "wcoj-phase-timing"))]
1319 let layout_xz = self
1320 .provider
1321 .wcoj_layout_u64_recorded(buf_xz, launch_stream)?;
1322 self.provider.wcoj_triangle_u64_recorded(
1323 &layout_xy,
1324 &layout_yz,
1325 &layout_xz,
1326 launch_stream,
1327 )
1328 }
1329 }
1330 }
1331
1332 fn run_wcoj_triangle_pipeline_with_leader_order(
1352 &self,
1353 buf_xy: &CudaBuffer,
1354 buf_yz: &CudaBuffer,
1355 buf_xz: &CudaBuffer,
1356 launch_stream: StreamId,
1357 width: WcojKeyWidth,
1358 var_order: &VariableOrder,
1359 ) -> Result<CudaBuffer> {
1360 let canonical: [&CudaBuffer; 3] = [buf_xy, buf_yz, buf_xz];
1361 let slot_inputs = self.prepare_leader_inputs(&canonical, var_order, launch_stream)?;
1362 if slot_inputs.len() != 3 {
1363 return Err(xlog_core::XlogError::Kernel(
1364 "run_wcoj_triangle_pipeline_with_leader_order: prepare_leader_inputs must return 3 slots"
1365 .to_string(),
1366 ));
1367 }
1368
1369 let head_schema = build_triangle_head_schema(buf_xy, buf_yz)?;
1375 let perm = perm_indices_from_kernel_output_cols(&var_order.kernel_output_cols)?;
1376
1377 let kernel_out: CudaBuffer = match width {
1378 WcojKeyWidth::FourByte => {
1379 let l0 = self
1380 .provider
1381 .wcoj_layout_u32_recorded(&slot_inputs[0], launch_stream)?;
1382 let l1 = self
1383 .provider
1384 .wcoj_layout_u32_recorded(&slot_inputs[1], launch_stream)?;
1385 let l2 = self
1386 .provider
1387 .wcoj_layout_u32_recorded(&slot_inputs[2], launch_stream)?;
1388 let out = self.provider.wcoj_triangle_hg_u32_recorded(
1389 &l0,
1390 &l1,
1391 &l2,
1392 wcoj_block_work_unit(),
1393 launch_stream,
1394 )?;
1395 self.provider.record_wcoj_triangle_hg_dispatch();
1396 out
1397 }
1398 WcojKeyWidth::EightByte => {
1399 let l0 = self
1400 .provider
1401 .wcoj_layout_u64_recorded(&slot_inputs[0], launch_stream)?;
1402 let l1 = self
1403 .provider
1404 .wcoj_layout_u64_recorded(&slot_inputs[1], launch_stream)?;
1405 let l2 = self
1406 .provider
1407 .wcoj_layout_u64_recorded(&slot_inputs[2], launch_stream)?;
1408 self.provider
1409 .wcoj_triangle_u64_recorded(&l0, &l1, &l2, launch_stream)?
1410 }
1411 };
1412
1413 self.provider.wcoj_project_output_columns_recorded(
1414 &kernel_out,
1415 &perm,
1416 head_schema,
1417 launch_stream,
1418 )
1419 }
1420
1421 pub fn wcoj_triangle_dispatch_count(&self) -> u64 {
1427 self.wcoj_triangle_dispatch_count
1428 }
1429
1430 pub fn wcoj_error_decline_count(&self) -> u64 {
1437 self.wcoj_error_decline_count
1438 }
1439
1440 pub fn wcoj_fallback_stats(&self) -> crate::profiler::WcojFallbackStats {
1442 self.wcoj_fallback
1443 }
1444
1445 pub(super) fn record_wcoj_body_fallback(&mut self, body: &RirNode) {
1447 match body {
1448 RirNode::ChainJoin { .. } => {
1449 self.wcoj_fallback.chain = self.wcoj_fallback.chain.saturating_add(1);
1450 }
1451 RirNode::MultiWayJoin {
1452 plan: Some(MultiwayPlan::FreeJoin),
1453 ..
1454 } => {
1455 self.wcoj_fallback.free_join = self.wcoj_fallback.free_join.saturating_add(1);
1456 }
1457 RirNode::MultiWayJoin {
1458 plan: Some(MultiwayPlan::PlannedHashRoute { .. }),
1459 ..
1460 } => {
1461 self.wcoj_fallback.planned_hash = self.wcoj_fallback.planned_hash.saturating_add(1);
1462 }
1463 RirNode::MultiWayJoin { .. } => {
1464 self.wcoj_fallback.dedicated_multiway =
1465 self.wcoj_fallback.dedicated_multiway.saturating_add(1);
1466 }
1467 _ => {}
1468 }
1469 }
1470
1471 pub(super) fn record_factorized_delta_fallback(&mut self, body: &RirNode) {
1473 if matches!(body, RirNode::ChainJoin { .. }) {
1474 self.wcoj_fallback.factorized_delta =
1475 self.wcoj_fallback.factorized_delta.saturating_add(1);
1476 }
1477 }
1478
1479 pub(super) fn record_wcoj_groupby_fallback(&mut self, input: &RirNode) {
1481 if matches!(input, RirNode::MultiWayJoin { .. }) {
1482 self.wcoj_fallback.groupby_fusion = self.wcoj_fallback.groupby_fusion.saturating_add(1);
1483 }
1484 }
1485
1486 pub fn free_join_dispatch_count(&self) -> u64 {
1489 self.free_join_dispatch_count
1490 }
1491
1492 pub fn factorized_delta_dispatch_count(&self) -> u64 {
1495 self.factorized_delta_dispatch_count
1496 }
1497
1498 fn factorized_delta_normalize_static(
1502 &self,
1503 buf: &CudaBuffer,
1504 key_col: usize,
1505 launch_stream: StreamId,
1506 ) -> Result<CudaBuffer> {
1507 if key_col == 0 {
1508 return self.provider.wcoj_layout_u32_recorded(buf, launch_stream);
1509 }
1510 let ty = |i: usize| {
1511 buf.schema().column_type(i).ok_or_else(|| {
1512 xlog_core::XlogError::Execution(format!(
1513 "factorized-delta: static column {i} type missing"
1514 ))
1515 })
1516 };
1517 let swapped = Schema::new(vec![("k".to_string(), ty(1)?), ("v".to_string(), ty(0)?)]);
1518 let projected = self.provider.wcoj_project_output_columns_recorded(
1519 buf,
1520 &[1, 0],
1521 swapped,
1522 launch_stream,
1523 )?;
1524 self.provider
1525 .wcoj_layout_u32_recorded(&projected, launch_stream)
1526 }
1527
1528 pub(super) fn try_dispatch_factorized_delta(
1545 &mut self,
1546 node: &RirNode,
1547 delta_rel: RelId,
1548 head_pred: &str,
1549 recursive_preds: &HashSet<String>,
1550 ctx: &mut FactorizedDeltaCtx,
1551 ) -> Result<Option<CudaBuffer>> {
1552 use xlog_cuda::provider::FjDeltaCols;
1553
1554 if factorized_delta_disabled()? {
1555 return Ok(None);
1556 }
1557 let RirNode::ChainJoin {
1558 left,
1559 right,
1560 left_key,
1561 right_key,
1562 output_columns,
1563 ..
1564 } = node
1565 else {
1566 return Ok(None);
1567 };
1568 let (RirNode::Scan { rel: left_rel }, RirNode::Scan { rel: right_rel }) =
1569 (left.as_ref(), right.as_ref())
1570 else {
1571 return Ok(None);
1572 };
1573 let delta_on_left = match (*left_rel == delta_rel, *right_rel == delta_rel) {
1577 (true, false) => true,
1578 (false, true) => false,
1579 _ => return Ok(None),
1580 };
1581 let (delta_key, static_rel, static_key) = if delta_on_left {
1582 (*left_key, *right_rel, *right_key)
1583 } else {
1584 (*right_key, *left_rel, *left_key)
1585 };
1586 if delta_key > 1 || static_key > 1 {
1587 return Ok(None);
1588 }
1589 let delta_carry = 1 - delta_key;
1590 let static_value = 1 - static_key;
1591
1592 let (delta_off, static_off) = if delta_on_left { (0, 2) } else { (2, 0) };
1595 let carry_global = delta_off + delta_carry;
1596 let value_global = static_off + static_value;
1597 let [ProjectExpr::Column(out0), ProjectExpr::Column(out1)] = output_columns.as_slice()
1598 else {
1599 return Ok(None);
1600 };
1601 let (r_carry, r_value) = if (*out0, *out1) == (carry_global, value_global) {
1602 (0, 1)
1603 } else if (*out0, *out1) == (value_global, carry_global) {
1604 (1, 0)
1605 } else {
1606 return Ok(None);
1607 };
1608
1609 let binary_u32_class = |buf: &CudaBuffer| {
1611 buf.arity() == 2
1612 && (0..2).all(|i| {
1613 matches!(
1614 buf.schema().column_type(i),
1615 Some(ScalarType::U32) | Some(ScalarType::Symbol)
1616 )
1617 })
1618 };
1619 let Some(delta_name) = self.get_rel_name(delta_rel).map(str::to_string) else {
1620 return Ok(None);
1621 };
1622 let Some(static_name) = self.get_rel_name(static_rel).map(str::to_string) else {
1623 return Ok(None);
1624 };
1625 let Some(delta_buf) = self.store.get(&delta_name) else {
1626 return Ok(None);
1627 };
1628 let Some(static_buf) = self.store.get(&static_name) else {
1629 return Ok(None);
1630 };
1631 let Some(full_buf) = self.store.get(head_pred) else {
1632 return Ok(None);
1633 };
1634 if !binary_u32_class(delta_buf)
1635 || !binary_u32_class(static_buf)
1636 || !binary_u32_class(full_buf)
1637 {
1638 return Ok(None);
1639 }
1640 if self.provider.memory().runtime().is_none() {
1641 return Ok(None);
1642 }
1643 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
1644 return Ok(None);
1645 };
1646
1647 let domain_key = (head_pred.to_string(), static_rel);
1661 let domain = match ctx.domain_by_key.get(&domain_key) {
1662 Some(Some(d)) => *d,
1663 Some(None) => return Ok(None),
1664 None => {
1665 let max_id = match self.provider.fj_delta_columns_max_u32(
1666 &[
1667 (delta_buf, &[0, 1][..]),
1668 (static_buf, &[0, 1][..]),
1669 (full_buf, &[0, 1][..]),
1670 ],
1671 launch_stream,
1672 ) {
1673 Ok(m) => m,
1674 Err(err) => {
1675 return wcoj_decline_on_error(
1676 &mut self.wcoj_error_decline_count,
1677 "factorized-delta",
1678 err,
1679 );
1680 }
1681 };
1682 let decided = if max_id == u32::MAX {
1683 None
1684 } else {
1685 Some(max_id + 1)
1686 };
1687 ctx.domain_by_key.insert(domain_key, decided);
1688 match decided {
1689 Some(d) => d,
1690 None => return Ok(None),
1691 }
1692 }
1693 };
1694
1695 let n_delta = u64::from(self.buffer_row_count(delta_buf)?);
1696 let n_static = u64::from(self.buffer_row_count(static_buf)?);
1697 if n_delta == 0 || n_static == 0 {
1698 return Ok(None);
1699 }
1700
1701 let dense = domain <= factorized_delta_max_domain();
1708 if dense {
1709 let n_words = u64::from(domain.div_ceil(32)) * u64::from(domain);
1710 let work_est = n_delta.saturating_mul((n_static / u64::from(domain)).max(1));
1711 if work_est < n_words / factorized_delta_work_divisor() {
1712 return Ok(None);
1713 }
1714 }
1715
1716 let static_is_recursive = recursive_preds.contains(&static_name);
1722 let norm_owned;
1723 let static_norm: &CudaBuffer = if static_is_recursive {
1724 norm_owned =
1725 match self.factorized_delta_normalize_static(static_buf, static_key, launch_stream)
1726 {
1727 Ok(b) => b,
1728 Err(err) => {
1729 return wcoj_decline_on_error(
1730 &mut self.wcoj_error_decline_count,
1731 "factorized-delta",
1732 err,
1733 );
1734 }
1735 };
1736 &norm_owned
1737 } else {
1738 match ctx.static_norm_cache.entry((static_rel, static_key)) {
1739 std::collections::hash_map::Entry::Occupied(e) => &*e.into_mut(),
1740 std::collections::hash_map::Entry::Vacant(v) => {
1741 let norm = match self.factorized_delta_normalize_static(
1742 static_buf,
1743 static_key,
1744 launch_stream,
1745 ) {
1746 Ok(b) => b,
1747 Err(err) => {
1748 return wcoj_decline_on_error(
1749 &mut self.wcoj_error_decline_count,
1750 "factorized-delta",
1751 err,
1752 );
1753 }
1754 };
1755 &*v.insert(norm)
1756 }
1757 }
1758 };
1759
1760 let cols = FjDeltaCols {
1761 delta_carry,
1762 delta_key,
1763 r_carry,
1764 r_value,
1765 };
1766 if dense {
1767 match self.provider.fj_delta_novel_u32_recorded(
1768 delta_buf,
1769 static_norm,
1770 full_buf,
1771 cols,
1772 domain,
1773 launch_stream,
1774 ) {
1775 Ok(novel) => {
1776 self.factorized_delta_dispatch_count += 1;
1777 Ok(Some(novel))
1778 }
1779 Err(err) => wcoj_decline_on_error(
1780 &mut self.wcoj_error_decline_count,
1781 "factorized-delta",
1782 err,
1783 ),
1784 }
1785 } else {
1786 let max_table_bytes =
1790 factorized_delta_max_table_bytes(self.provider.memory().budget().device_bytes);
1791 match self.provider.fj_delta_sparse_novel_u32_recorded(
1792 delta_buf,
1793 static_norm,
1794 full_buf,
1795 cols,
1796 max_table_bytes,
1797 launch_stream,
1798 ) {
1799 Ok(Some(novel)) => {
1800 self.factorized_delta_dispatch_count += 1;
1801 Ok(Some(novel))
1802 }
1803 Ok(None) => Ok(None),
1804 Err(err) => wcoj_decline_on_error(
1805 &mut self.wcoj_error_decline_count,
1806 "factorized-delta",
1807 err,
1808 ),
1809 }
1810 }
1811 }
1812
1813 pub(super) fn try_dispatch_free_join(&mut self, node: &RirNode) -> Result<Option<CudaBuffer>> {
1829 use xlog_cuda::provider::{FjNode, FjPlan, FjSubAtom};
1830
1831 if free_join_disabled()? {
1832 return Ok(None);
1833 }
1834 let RirNode::MultiWayJoin {
1835 inputs,
1836 slot_vars,
1837 output_columns,
1838 plan,
1839 ..
1840 } = node
1841 else {
1842 return Ok(None);
1843 };
1844 if !matches!(plan, Some(MultiwayPlan::FreeJoin)) {
1857 return Ok(None);
1858 }
1859 if inputs.len() < 3 {
1860 return Ok(None);
1861 }
1862 let mut bufs: Vec<&CudaBuffer> = Vec::with_capacity(inputs.len());
1867 let mut all_u32 = true;
1868 let mut all_u64 = true;
1869 for input in inputs {
1870 let RirNode::Scan { rel } = input else {
1871 return Ok(None);
1872 };
1873 let name = match self.get_rel_name(*rel) {
1874 Some(s) => s.to_string(),
1875 None => return Ok(None),
1876 };
1877 let Some(buf) = self.store.get(&name) else {
1878 return Ok(None);
1879 };
1880 for i in 0..buf.arity() {
1881 match buf.schema().column_type(i) {
1882 Some(ScalarType::U32 | ScalarType::Symbol) => all_u64 = false,
1883 Some(ScalarType::U64) => all_u32 = false,
1884 _ => return Ok(None),
1885 }
1886 }
1887 bufs.push(buf);
1888 }
1889 if !all_u32 && !all_u64 {
1890 return Ok(None);
1891 }
1892 {
1899 let slot_rels: Vec<RelId> = inputs
1900 .iter()
1901 .filter_map(|i| match i {
1902 RirNode::Scan { rel } => Some(*rel),
1903 _ => None,
1904 })
1905 .collect();
1906 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
1907 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
1908 stats: &self.stats,
1909 slot_rels: &slot_rels,
1910 };
1911 if model.factorized_loss_veto(&ctx) {
1912 return Ok(None);
1913 }
1914 }
1915 let mut class_to_var: Vec<u32> = Vec::new();
1917 let mut dense = |class: u32| -> usize {
1918 match class_to_var.iter().position(|c| *c == class) {
1919 Some(i) => i,
1920 None => {
1921 class_to_var.push(class);
1922 class_to_var.len() - 1
1923 }
1924 }
1925 };
1926 let mut atom_vars: Vec<Vec<usize>> = Vec::with_capacity(slot_vars.len());
1927 for (i, cols) in slot_vars.iter().enumerate() {
1928 if cols.len() != bufs[i].arity() {
1929 return Ok(None);
1930 }
1931 let mut vars = Vec::with_capacity(cols.len());
1932 for c in cols {
1933 let Some(class) = c else { return Ok(None) };
1934 vars.push(dense(*class));
1935 }
1936 atom_vars.push(vars);
1937 }
1938 let num_vars = class_to_var.len();
1939 let order: Vec<usize> = {
1954 let slot_rels: Vec<RelId> = inputs
1955 .iter()
1956 .filter_map(|i| match i {
1957 RirNode::Scan { rel } => Some(*rel),
1958 _ => None,
1959 })
1960 .collect();
1961 let cards: Vec<u64> = bufs.iter().map(|b| b.num_rows()).collect();
1962 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
1963 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
1964 stats: &self.stats,
1965 slot_rels: &slot_rels,
1966 };
1967 match model.plan_free_join_order(&ctx, &atom_vars, &cards) {
1968 super::wcoj_cost_model::FjOrderDecision::Decline => return Ok(None),
1969 super::wcoj_cost_model::FjOrderDecision::Reorder(o) => o,
1970 super::wcoj_cost_model::FjOrderDecision::KeepDefault => {
1971 (0..atom_vars.len()).collect()
1972 }
1973 }
1974 };
1975 let mut bound_at: Vec<Option<usize>> = vec![None; num_vars]; let mut nodes: Vec<FjNode> = Vec::new();
1982 for &i in &order {
1987 let vars = &atom_vars[i];
1988 let split = vars.iter().take_while(|v| bound_at[**v].is_some()).count();
1989 if vars[split..].iter().any(|v| bound_at[*v].is_some()) {
1990 return Ok(None);
1993 }
1994 if split > 0 {
1995 let probe = FjSubAtom {
1996 input_idx: i,
1997 var_positions: vars[..split].to_vec(),
1998 };
1999 if nodes.is_empty() {
2000 return Ok(None);
2001 }
2002 let target = vars[..split]
2003 .iter()
2004 .map(|v| bound_at[*v].expect("prefix vars are bound"))
2005 .max()
2006 .expect("split > 0");
2007 nodes[target].probes.push(probe);
2008 }
2009 if split < vars.len() {
2010 let cover_vars = vars[split..].to_vec();
2011 let mut seen = HashSet::new();
2012 if !cover_vars.iter().all(|v| seen.insert(*v)) {
2013 return Ok(None);
2014 }
2015 let k = nodes.len();
2016 for v in &cover_vars {
2017 bound_at[*v] = Some(k);
2018 }
2019 nodes.push(FjNode {
2020 cover: FjSubAtom {
2021 input_idx: i,
2022 var_positions: cover_vars,
2023 },
2024 probes: Vec::new(),
2025 });
2026 } else if nodes.is_empty() {
2027 return Ok(None);
2028 }
2029 }
2030 let mut col_to_var: Vec<usize> = Vec::new();
2033 for vars in &atom_vars {
2034 col_to_var.extend(vars.iter().copied());
2035 }
2036 let mut output_vars: Vec<usize> = Vec::with_capacity(output_columns.len());
2037 for oc in output_columns {
2038 let ProjectExpr::Column(c) = oc else {
2039 return Ok(None);
2040 };
2041 let Some(v) = col_to_var.get(*c) else {
2042 return Ok(None);
2043 };
2044 output_vars.push(*v);
2045 }
2046 let fj_plan = FjPlan {
2047 num_vars,
2048 nodes,
2049 output_vars,
2050 };
2051 if self.provider.memory().runtime().is_none() {
2052 return Ok(None);
2053 }
2054 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
2055 return Ok(None);
2056 };
2057 let outcome = if all_u32 {
2058 self.provider
2059 .free_join_execute_u32_recorded(&bufs, &fj_plan, launch_stream)
2060 } else {
2061 self.provider
2062 .free_join_execute_u64_recorded(&bufs, &fj_plan, launch_stream)
2063 };
2064 match outcome {
2065 Ok(buf) => {
2066 self.free_join_dispatch_count += 1;
2067 Ok(Some(buf))
2068 }
2069 Err(err) => wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "free-join", err),
2070 }
2071 }
2072
2073 fn try_dispatch_free_join_count(
2088 &mut self,
2089 node: &RirNode,
2090 group_cols: &[ProjectExpr],
2091 ) -> Result<Option<CudaBuffer>> {
2092 use xlog_cuda::provider::{FjNode, FjPlan, FjSubAtom};
2093
2094 if free_join_disabled()? {
2095 return Ok(None);
2096 }
2097 let RirNode::MultiWayJoin {
2098 inputs,
2099 slot_vars,
2100 plan,
2101 ..
2102 } = node
2103 else {
2104 return Ok(None);
2105 };
2106 if !matches!(plan, Some(MultiwayPlan::FreeJoin)) {
2107 return Ok(None);
2108 }
2109 if inputs.len() < 3 {
2110 return Ok(None);
2111 }
2112 let mut bufs: Vec<&CudaBuffer> = Vec::with_capacity(inputs.len());
2113 for input in inputs {
2114 let RirNode::Scan { rel } = input else {
2115 return Ok(None);
2116 };
2117 let name = match self.get_rel_name(*rel) {
2118 Some(s) => s.to_string(),
2119 None => return Ok(None),
2120 };
2121 let Some(buf) = self.store.get(&name) else {
2122 return Ok(None);
2123 };
2124 let four_byte = (0..buf.arity()).all(|i| {
2125 matches!(
2126 buf.schema().column_type(i),
2127 Some(ScalarType::U32 | ScalarType::Symbol)
2128 )
2129 });
2130 if !four_byte {
2131 return Ok(None);
2132 }
2133 bufs.push(buf);
2134 }
2135 let mut class_to_var: Vec<u32> = Vec::new();
2138 let mut dense = |class: u32| -> usize {
2139 match class_to_var.iter().position(|c| *c == class) {
2140 Some(i) => i,
2141 None => {
2142 class_to_var.push(class);
2143 class_to_var.len() - 1
2144 }
2145 }
2146 };
2147 let mut atom_vars: Vec<Vec<usize>> = Vec::with_capacity(slot_vars.len());
2148 for (i, cols) in slot_vars.iter().enumerate() {
2149 if cols.len() != bufs[i].arity() {
2150 return Ok(None);
2151 }
2152 let mut vars = Vec::with_capacity(cols.len());
2153 for c in cols {
2154 let Some(class) = c else { return Ok(None) };
2155 vars.push(dense(*class));
2156 }
2157 atom_vars.push(vars);
2158 }
2159 let num_vars = class_to_var.len();
2160 let mut col_to_var: Vec<usize> = Vec::new();
2163 for vars in &atom_vars {
2164 col_to_var.extend(vars.iter().copied());
2165 }
2166 let Some(ProjectExpr::Column(key_col)) = group_cols.first() else {
2167 return Ok(None);
2168 };
2169 let Some(&group_var) = col_to_var.get(*key_col) else {
2170 return Ok(None);
2171 };
2172 let mut occurrences = vec![0usize; num_vars];
2176 for vars in &atom_vars {
2177 for &v in vars {
2178 occurrences[v] += 1;
2179 }
2180 }
2181 let mut bound_at: Vec<Option<usize>> = vec![None; num_vars];
2183 let mut nodes: Vec<FjNode> = Vec::new();
2184 for (i, vars) in atom_vars.iter().enumerate() {
2185 let split = vars.iter().take_while(|v| bound_at[**v].is_some()).count();
2186 if vars[split..].iter().any(|v| bound_at[*v].is_some()) {
2187 return Ok(None);
2189 }
2190 let mut keep_end = vars.len();
2191 while keep_end > split {
2192 let v = vars[keep_end - 1];
2193 if occurrences[v] == 1 && v != group_var {
2194 keep_end -= 1;
2195 } else {
2196 break;
2197 }
2198 }
2199 if split == 0 && keep_end == 0 {
2200 return Ok(None);
2204 }
2205 if split > 0 {
2206 let probe = FjSubAtom {
2207 input_idx: i,
2208 var_positions: vars[..split].to_vec(),
2209 };
2210 if nodes.is_empty() {
2211 return Ok(None);
2212 }
2213 let target = vars[..split]
2214 .iter()
2215 .map(|v| bound_at[*v].expect("prefix vars are bound"))
2216 .max()
2217 .expect("split > 0");
2218 nodes[target].probes.push(probe);
2219 }
2220 if split < keep_end {
2221 let cover_vars = vars[split..keep_end].to_vec();
2222 let mut seen = HashSet::new();
2223 if !cover_vars.iter().all(|v| seen.insert(*v)) {
2224 return Ok(None);
2225 }
2226 let k = nodes.len();
2227 for v in &cover_vars {
2228 bound_at[*v] = Some(k);
2229 }
2230 nodes.push(FjNode {
2231 cover: FjSubAtom {
2232 input_idx: i,
2233 var_positions: cover_vars,
2234 },
2235 probes: Vec::new(),
2236 });
2237 } else if nodes.is_empty() {
2238 return Ok(None);
2239 }
2240 }
2241 if bound_at[group_var].is_none() {
2242 return Ok(None);
2243 }
2244 let fj_plan = FjPlan {
2245 num_vars,
2246 nodes,
2247 output_vars: vec![group_var],
2248 };
2249 if self.provider.memory().runtime().is_none() {
2250 return Ok(None);
2251 }
2252 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
2253 return Ok(None);
2254 };
2255 match self
2256 .provider
2257 .free_join_count_by_root_u32_recorded(&bufs, &fj_plan, launch_stream)
2258 {
2259 Ok(buf) => {
2260 self.free_join_dispatch_count += 1;
2261 self.wcoj_groupby_fusion_dispatch_count += 1;
2262 Ok(Some(buf))
2263 }
2264 Err(err) => {
2265 wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "free-join-count", err)
2266 }
2267 }
2268 }
2269
2270 pub fn wcoj_groupby_fusion_dispatch_count(&self) -> u64 {
2274 self.wcoj_groupby_fusion_dispatch_count
2275 }
2276
2277 pub(super) fn try_dispatch_wcoj_groupby_root_agg(
2294 &mut self,
2295 input: &RirNode,
2296 key_cols: &[usize],
2297 aggs: &[(usize, xlog_core::AggOp)],
2298 ) -> Result<Option<CudaBuffer>> {
2299 use xlog_core::AggOp;
2300 if wcoj_groupby_fusion_disabled()? {
2301 return Ok(None);
2302 }
2303 if key_cols != [0] {
2304 return Ok(None);
2305 }
2306 if aggs.len() != 1 {
2307 return Ok(None);
2308 }
2309 let (agg_col, agg_op) = aggs[0];
2310 if !matches!(agg_op, AggOp::Count | AggOp::Sum | AggOp::Min | AggOp::Max) {
2311 return Ok(None);
2312 }
2313 let RirNode::Project {
2314 input: multiway,
2315 columns,
2316 } = input
2317 else {
2318 return Ok(None);
2319 };
2320 if columns.is_empty() || !columns.iter().all(|c| matches!(c, ProjectExpr::Column(_))) {
2322 return Ok(None);
2323 }
2324 let key_is_col0 = matches!(columns[0], ProjectExpr::Column(0));
2329 let agg_value_col = if matches!(agg_op, AggOp::Count) {
2337 None
2338 } else {
2339 match columns.get(agg_col) {
2340 Some(ProjectExpr::Column(c)) if *c >= 1 => Some(*c),
2341 _ => return Ok(None),
2342 }
2343 };
2344 let Some(matched) = match_multiway_triangle(multiway) else {
2345 if key_is_col0 {
2349 if let Some(buf) =
2350 self.try_dispatch_wcoj_groupby_root_agg_4cycle(multiway, agg_op, agg_value_col)?
2351 {
2352 return Ok(Some(buf));
2353 }
2354 }
2355 if !matches!(agg_op, AggOp::Count) {
2360 return Ok(None);
2361 }
2362 if let Some(buf) =
2363 self.try_dispatch_wcoj_groupby_root_count_clique(multiway, columns)?
2364 {
2365 return Ok(Some(buf));
2366 }
2367 return self.try_dispatch_free_join_count(multiway, columns);
2371 };
2372 let agg_value = match agg_value_col {
2375 None => None,
2376 Some(1) => Some(WcojRootAggValue::Y),
2377 Some(2) => Some(WcojRootAggValue::Z),
2378 Some(_) => return Ok(None),
2379 };
2380 if !key_is_col0 {
2381 return Ok(None);
2382 }
2383 let name_xy = match self.get_rel_name(matched.rel_xy) {
2384 Some(s) => s.to_string(),
2385 None => return Ok(None),
2386 };
2387 let name_yz = match self.get_rel_name(matched.rel_yz) {
2388 Some(s) => s.to_string(),
2389 None => return Ok(None),
2390 };
2391 let name_xz = match self.get_rel_name(matched.rel_xz) {
2392 Some(s) => s.to_string(),
2393 None => return Ok(None),
2394 };
2395 let buf_xy = match self.store.get(&name_xy) {
2396 Some(b) => b,
2397 None => return Ok(None),
2398 };
2399 let buf_yz = match self.store.get(&name_yz) {
2400 Some(b) => b,
2401 None => return Ok(None),
2402 };
2403 let buf_xz = match self.store.get(&name_xz) {
2404 Some(b) => b,
2405 None => return Ok(None),
2406 };
2407 let width = match (
2408 classify_two_col_wcoj_width(buf_xy),
2409 classify_two_col_wcoj_width(buf_yz),
2410 classify_two_col_wcoj_width(buf_xz),
2411 ) {
2412 (
2413 Some(WcojKeyWidth::FourByte),
2414 Some(WcojKeyWidth::FourByte),
2415 Some(WcojKeyWidth::FourByte),
2416 ) => WcojKeyWidth::FourByte,
2417 (
2418 Some(WcojKeyWidth::EightByte),
2419 Some(WcojKeyWidth::EightByte),
2420 Some(WcojKeyWidth::EightByte),
2421 ) => WcojKeyWidth::EightByte,
2422 _ => return Ok(None),
2423 };
2424 {
2434 let slot_rels = [matched.rel_xy, matched.rel_yz, matched.rel_xz];
2435 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
2436 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
2437 stats: &self.stats,
2438 slot_rels: &slot_rels,
2439 };
2440 if model.factorized_loss_veto(&ctx) {
2441 return Ok(None);
2442 }
2443 }
2444 if matches!(width, WcojKeyWidth::FourByte) {
2451 match agg_value {
2452 Some(WcojRootAggValue::Y)
2453 if buf_xy.schema().column_type(1) != Some(xlog_core::ScalarType::U32) =>
2454 {
2455 return Ok(None);
2456 }
2457 Some(WcojRootAggValue::Z)
2458 if buf_yz.schema().column_type(1) != Some(xlog_core::ScalarType::U32)
2459 || buf_xz.schema().column_type(1) != Some(xlog_core::ScalarType::U32) =>
2460 {
2461 return Ok(None);
2462 }
2463 Some(_) | None => {}
2464 }
2465 }
2466 if self.provider.memory().runtime().is_none() {
2467 return Ok(None);
2468 }
2469 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
2470 return Ok(None);
2471 };
2472 let result = match (agg_value, width) {
2473 (None, WcojKeyWidth::FourByte) => {
2474 self.provider.wcoj_triangle_groupby_root_count_u32_recorded(
2475 buf_xy,
2476 buf_yz,
2477 buf_xz,
2478 wcoj_block_work_unit(),
2479 launch_stream,
2480 )
2481 }
2482 (None, WcojKeyWidth::EightByte) => {
2483 self.provider.wcoj_triangle_groupby_root_count_u64_recorded(
2484 buf_xy,
2485 buf_yz,
2486 buf_xz,
2487 wcoj_block_work_unit(),
2488 launch_stream,
2489 )
2490 }
2491 (Some(value), WcojKeyWidth::FourByte) => {
2492 self.provider.wcoj_triangle_groupby_root_agg_u32_recorded(
2493 buf_xy,
2494 buf_yz,
2495 buf_xz,
2496 agg_op,
2497 value,
2498 wcoj_block_work_unit(),
2499 launch_stream,
2500 )
2501 }
2502 (Some(value), WcojKeyWidth::EightByte) => {
2505 self.provider.wcoj_triangle_groupby_root_agg_u64_recorded(
2506 buf_xy,
2507 buf_yz,
2508 buf_xz,
2509 agg_op,
2510 value,
2511 wcoj_block_work_unit(),
2512 launch_stream,
2513 )
2514 }
2515 };
2516 match result {
2517 Ok(buf) => {
2518 self.wcoj_groupby_fusion_dispatch_count += 1;
2519 Ok(Some(buf))
2520 }
2521 Err(err) => {
2522 wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "groupby-fusion", err)
2523 }
2524 }
2525 }
2526
2527 fn try_dispatch_wcoj_groupby_root_agg_4cycle(
2559 &mut self,
2560 multiway: &RirNode,
2561 agg_op: xlog_core::AggOp,
2562 agg_value_col: Option<usize>,
2563 ) -> Result<Option<CudaBuffer>> {
2564 use xlog_core::AggOp;
2565 let Some(matched) = match_multiway_4cycle(multiway) else {
2566 return Ok(None);
2567 };
2568 let agg_value = match agg_value_col {
2571 None => None,
2572 Some(1) => Some(Wcoj4CycleRootAggValue::X),
2573 Some(2) => Some(Wcoj4CycleRootAggValue::Y),
2574 Some(3) => Some(Wcoj4CycleRootAggValue::Z),
2575 Some(_) => return Ok(None),
2576 };
2577 let name_e1 = match self.get_rel_name(matched.rel_e1) {
2578 Some(s) => s.to_string(),
2579 None => return Ok(None),
2580 };
2581 let name_e2 = match self.get_rel_name(matched.rel_e2) {
2582 Some(s) => s.to_string(),
2583 None => return Ok(None),
2584 };
2585 let name_e3 = match self.get_rel_name(matched.rel_e3) {
2586 Some(s) => s.to_string(),
2587 None => return Ok(None),
2588 };
2589 let name_e4 = match self.get_rel_name(matched.rel_e4) {
2590 Some(s) => s.to_string(),
2591 None => return Ok(None),
2592 };
2593 let buf_e1 = match self.store.get(&name_e1) {
2594 Some(b) => b,
2595 None => return Ok(None),
2596 };
2597 let buf_e2 = match self.store.get(&name_e2) {
2598 Some(b) => b,
2599 None => return Ok(None),
2600 };
2601 let buf_e3 = match self.store.get(&name_e3) {
2602 Some(b) => b,
2603 None => return Ok(None),
2604 };
2605 let buf_e4 = match self.store.get(&name_e4) {
2606 Some(b) => b,
2607 None => return Ok(None),
2608 };
2609 let width = match (
2610 classify_two_col_wcoj_width(buf_e1),
2611 classify_two_col_wcoj_width(buf_e2),
2612 classify_two_col_wcoj_width(buf_e3),
2613 classify_two_col_wcoj_width(buf_e4),
2614 ) {
2615 (
2616 Some(WcojKeyWidth::FourByte),
2617 Some(WcojKeyWidth::FourByte),
2618 Some(WcojKeyWidth::FourByte),
2619 Some(WcojKeyWidth::FourByte),
2620 ) => WcojKeyWidth::FourByte,
2621 (
2622 Some(WcojKeyWidth::EightByte),
2623 Some(WcojKeyWidth::EightByte),
2624 Some(WcojKeyWidth::EightByte),
2625 Some(WcojKeyWidth::EightByte),
2626 ) => WcojKeyWidth::EightByte,
2627 _ => return Ok(None),
2628 };
2629 if agg_value.is_some() && width != WcojKeyWidth::FourByte {
2632 return Ok(None);
2633 }
2634 let value_source = match agg_value {
2642 None => None,
2643 Some(Wcoj4CycleRootAggValue::X) => Some(buf_e1),
2644 Some(Wcoj4CycleRootAggValue::Y) => Some(buf_e2),
2645 Some(Wcoj4CycleRootAggValue::Z) => Some(buf_e3),
2646 };
2647 if let Some(src) = value_source {
2648 if src.schema().column_type(1) != Some(xlog_core::ScalarType::U32) {
2649 return Ok(None);
2650 }
2651 }
2652 if self.provider.memory().runtime().is_none() {
2653 return Ok(None);
2654 }
2655 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
2656 return Ok(None);
2657 };
2658 debug_assert!(
2659 agg_value.is_some() || matches!(agg_op, AggOp::Count),
2660 "non-Count aggregates resolve a value column above"
2661 );
2662 let result = match (agg_value, width) {
2663 (None, WcojKeyWidth::FourByte) => {
2664 self.provider.wcoj_4cycle_groupby_root_count_u32_recorded(
2665 buf_e1,
2666 buf_e2,
2667 buf_e3,
2668 buf_e4,
2669 wcoj_block_work_unit(),
2670 launch_stream,
2671 )
2672 }
2673 (None, WcojKeyWidth::EightByte) => {
2677 self.provider.wcoj_4cycle_groupby_root_count_u64_recorded(
2678 buf_e1,
2679 buf_e2,
2680 buf_e3,
2681 buf_e4,
2682 wcoj_block_work_unit(),
2683 launch_stream,
2684 )
2685 }
2686 (Some(value), _) => self.provider.wcoj_4cycle_groupby_root_agg_u32_recorded(
2687 buf_e1,
2688 buf_e2,
2689 buf_e3,
2690 buf_e4,
2691 agg_op,
2692 value,
2693 wcoj_block_work_unit(),
2694 launch_stream,
2695 ),
2696 };
2697 match result {
2698 Ok(buf) => {
2699 self.wcoj_groupby_fusion_dispatch_count += 1;
2700 Ok(Some(buf))
2701 }
2702 Err(err) => wcoj_decline_on_error(
2703 &mut self.wcoj_error_decline_count,
2704 "groupby-fusion-4cycle",
2705 err,
2706 ),
2707 }
2708 }
2709
2710 pub fn wcoj_4cycle_dispatch_count(&self) -> u64 {
2715 self.wcoj_4cycle_dispatch_count
2716 }
2717
2718 pub fn chain_dispatch_count(&self) -> u64 {
2721 self.chain_dispatch_count
2722 }
2723
2724 pub fn nested_loop_dispatch_count(&self) -> u64 {
2731 self.nested_loop_dispatch_count
2732 }
2733
2734 pub(super) fn try_dispatch_chain_on_body(
2745 &mut self,
2746 body: &RirNode,
2747 ) -> Result<Option<CudaBuffer>> {
2748 if !chain_dispatch_enabled()? {
2749 return Ok(None);
2750 }
2751 let Some(matched) = match_chain_join(body) else {
2752 return Ok(None);
2753 };
2754
2755 let name_left = match self.get_rel_name(matched.rel_left) {
2756 Some(s) => s.to_string(),
2757 None => return Ok(None),
2758 };
2759 let name_right = match self.get_rel_name(matched.rel_right) {
2760 Some(s) => s.to_string(),
2761 None => return Ok(None),
2762 };
2763 let left = match self.store.get(&name_left) {
2764 Some(buf) => buf,
2765 None => return Ok(None),
2766 };
2767 let right = match self.store.get(&name_right) {
2768 Some(buf) => buf,
2769 None => return Ok(None),
2770 };
2771
2772 let num_left = self.provider.device_row_count(left)? as u64;
2773 let num_right = self.provider.device_row_count(right)? as u64;
2774 let in_threshold = num_left
2775 .checked_mul(num_right)
2776 .map(|p| p <= NESTED_LOOP_TOTAL_THRESHOLD)
2777 .unwrap_or(false);
2778 let four_byte = matches!(
2779 classify_two_col_wcoj_width(left),
2780 Some(WcojKeyWidth::FourByte)
2781 ) && matches!(
2782 classify_two_col_wcoj_width(right),
2783 Some(WcojKeyWidth::FourByte)
2784 );
2785
2786 let mut used_nested_loop = false;
2787 let joined = if four_byte {
2788 let left_sorted = self
2789 .provider
2790 .is_sorted_ascending_u32(left, matched.left_key)
2791 .unwrap_or(false);
2792 let right_sorted = self
2793 .provider
2794 .is_sorted_ascending_u32(right, matched.right_key)
2795 .unwrap_or(false);
2796 if left_sorted && right_sorted {
2797 if in_threshold {
2798 self.provider.sort_merge_join_v2_inner_u32_1key(
2799 left,
2800 right,
2801 matched.left_key,
2802 matched.right_key,
2803 )
2804 } else {
2805 let capacity = usize::try_from(num_left.min(num_right)).unwrap_or(usize::MAX);
2806 self.provider.sort_merge_join_v2_inner_u32_1key_bounded(
2807 left,
2808 right,
2809 matched.left_key,
2810 matched.right_key,
2811 capacity,
2812 )
2813 }
2814 } else if in_threshold {
2815 used_nested_loop = true;
2816 self.provider.nested_loop_join_v2_inner_u32_1key(
2817 left,
2818 right,
2819 matched.left_key,
2820 matched.right_key,
2821 )
2822 } else {
2823 self.provider.hash_join_v2(
2824 left,
2825 right,
2826 &[matched.left_key],
2827 &[matched.right_key],
2828 CudaJoinType::Inner,
2829 )
2830 }
2831 } else {
2832 self.provider.hash_join_v2(
2833 left,
2834 right,
2835 &[matched.left_key],
2836 &[matched.right_key],
2837 CudaJoinType::Inner,
2838 )
2839 };
2840
2841 let joined = match joined {
2842 Ok(buf) => buf,
2843 Err(err) => {
2844 record_chain_fallback_equivalents(
2845 body,
2846 false,
2847 &mut self.chain_fallback_scan_equivalents,
2848 &mut self.chain_fallback_filter_equivalents,
2849 );
2850 return wcoj_decline_on_error(
2851 &mut self.wcoj_error_decline_count,
2852 "chain-join",
2853 err,
2854 );
2855 }
2856 };
2857 let projected = match self.execute_project(&joined, &matched.output_columns) {
2858 Ok(buf) => buf,
2859 Err(err) => {
2860 record_chain_fallback_equivalents(
2861 body,
2862 false,
2863 &mut self.chain_fallback_scan_equivalents,
2864 &mut self.chain_fallback_filter_equivalents,
2865 );
2866 return wcoj_decline_on_error(
2867 &mut self.wcoj_error_decline_count,
2868 "chain-join-project",
2869 err,
2870 );
2871 }
2872 };
2873 self.stats.record_join_result(
2874 matched.rel_left,
2875 matched.rel_right,
2876 vec![matched.left_key],
2877 vec![matched.right_key],
2878 num_left.saturating_mul(num_right),
2879 joined.num_rows(),
2880 );
2881 if used_nested_loop {
2882 self.nested_loop_dispatch_count += 1;
2883 }
2884 record_chain_fallback_equivalents(
2885 body,
2886 true,
2887 &mut self.chain_fallback_scan_equivalents,
2888 &mut self.chain_fallback_filter_equivalents,
2889 );
2890 self.chain_dispatch_count += 1;
2891 Ok(Some(projected))
2892 }
2893
2894 pub(super) fn try_dispatch_wcoj_4cycle(
2910 &mut self,
2911 rule: &CompiledRule,
2912 ) -> Result<Option<CudaBuffer>> {
2913 self.try_dispatch_wcoj_4cycle_on_body(&rule.body)
2916 }
2917
2918 pub(super) fn try_dispatch_wcoj_4cycle_on_body(
2923 &mut self,
2924 body: &RirNode,
2925 ) -> Result<Option<CudaBuffer>> {
2926 if wcoj_4cycle_disabled(self.config.wcoj_4cycle_dispatch_disabled)? {
2928 return Ok(None);
2929 }
2930 let force_override = self.config.wcoj_4cycle_dispatch;
2932 let force_on = wcoj_4cycle_gate_enabled(force_override)?;
2933 let mode = if force_on {
2934 DispatchMode::Force
2935 } else {
2936 if matches!(force_override, Some(false)) {
2939 return Ok(None);
2940 }
2941 let adaptive_override = self.config.wcoj_4cycle_dispatch_adaptive;
2942 if wcoj_4cycle_adaptive_enabled(adaptive_override)? {
2943 DispatchMode::CostModel
2944 } else {
2945 return Ok(None);
2946 }
2947 };
2948
2949 let Some(matched) = match_multiway_4cycle(body) else {
2951 return Ok(None);
2952 };
2953
2954 let name_e1 = match self.get_rel_name(matched.rel_e1) {
2956 Some(s) => s.to_string(),
2957 None => return Ok(None),
2958 };
2959 let name_e2 = match self.get_rel_name(matched.rel_e2) {
2960 Some(s) => s.to_string(),
2961 None => return Ok(None),
2962 };
2963 let name_e3 = match self.get_rel_name(matched.rel_e3) {
2964 Some(s) => s.to_string(),
2965 None => return Ok(None),
2966 };
2967 let name_e4 = match self.get_rel_name(matched.rel_e4) {
2968 Some(s) => s.to_string(),
2969 None => return Ok(None),
2970 };
2971
2972 let buf_e1 = match self.store.get(&name_e1) {
2975 Some(b) => b,
2976 None => return Ok(None),
2977 };
2978 let buf_e2 = match self.store.get(&name_e2) {
2979 Some(b) => b,
2980 None => return Ok(None),
2981 };
2982 let buf_e3 = match self.store.get(&name_e3) {
2983 Some(b) => b,
2984 None => return Ok(None),
2985 };
2986 let buf_e4 = match self.store.get(&name_e4) {
2987 Some(b) => b,
2988 None => return Ok(None),
2989 };
2990 let width = match (
2991 classify_two_col_wcoj_width(buf_e1),
2992 classify_two_col_wcoj_width(buf_e2),
2993 classify_two_col_wcoj_width(buf_e3),
2994 classify_two_col_wcoj_width(buf_e4),
2995 ) {
2996 (Some(a), Some(b), Some(c), Some(d)) if a == b && b == c && c == d => a,
2997 _ => return Ok(None),
2998 };
2999
3000 if self.provider.memory().runtime().is_none() {
3004 return Ok(None);
3005 }
3006 let launch_stream = match self.wcoj_dispatch_stream_or_init() {
3007 Some(s) => s,
3008 None => return Ok(None),
3009 };
3010
3011 if mode == DispatchMode::CostModel {
3014 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
3016 let slot_rels = [
3017 matched.rel_e1,
3018 matched.rel_e2,
3019 matched.rel_e3,
3020 matched.rel_e4,
3021 ];
3022 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
3023 stats: &self.stats,
3024 slot_rels: &slot_rels,
3025 };
3026 let dispatch = model.should_dispatch_4cycle(&ctx);
3027 if !dispatch {
3028 return Ok(None);
3029 }
3030 }
3031
3032 let var_order_opt: Option<&VariableOrder> = match body {
3035 RirNode::MultiWayJoin { var_order, .. } => var_order.as_ref(),
3036 _ => None,
3037 };
3038
3039 let dispatch_result = self.run_wcoj_4cycle_pipeline(
3042 buf_e1,
3043 buf_e2,
3044 buf_e3,
3045 buf_e4,
3046 launch_stream,
3047 width,
3048 var_order_opt,
3049 );
3050 match dispatch_result {
3051 Ok(buf) => {
3052 let output_rows = Self::wcoj_output_rows(&buf);
3066 let slot_rels = [
3067 matched.rel_e1,
3068 matched.rel_e2,
3069 matched.rel_e3,
3070 matched.rel_e4,
3071 ];
3072 self.record_wcoj_feedback(&slot_rels, var_order_opt, output_rows);
3073 self.wcoj_4cycle_dispatch_count += 1;
3074 Ok(Some(buf))
3075 }
3076 Err(err) => wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "4-cycle", err),
3077 }
3078 }
3079
3080 #[expect(
3082 clippy::too_many_arguments,
3083 reason = "four-cycle dispatch keeps four typed edge inputs plus stream, width, and variable order explicit"
3084 )]
3085 fn run_wcoj_4cycle_pipeline(
3086 &self,
3087 buf_e1: &CudaBuffer,
3088 buf_e2: &CudaBuffer,
3089 buf_e3: &CudaBuffer,
3090 buf_e4: &CudaBuffer,
3091 launch_stream: StreamId,
3092 width: WcojKeyWidth,
3093 var_order: Option<&VariableOrder>,
3094 ) -> Result<CudaBuffer> {
3095 if let Some(vo) = var_order {
3096 return self.run_wcoj_4cycle_pipeline_with_leader_order(
3097 buf_e1,
3098 buf_e2,
3099 buf_e3,
3100 buf_e4,
3101 launch_stream,
3102 width,
3103 vo,
3104 );
3105 }
3106 match width {
3107 WcojKeyWidth::FourByte => {
3108 let layout_e1 = self
3109 .provider
3110 .wcoj_layout_u32_recorded(buf_e1, launch_stream)?;
3111 let layout_e2 = self
3112 .provider
3113 .wcoj_layout_u32_recorded(buf_e2, launch_stream)?;
3114 let layout_e3 = self
3115 .provider
3116 .wcoj_layout_u32_recorded(buf_e3, launch_stream)?;
3117 let layout_e4 = self
3118 .provider
3119 .wcoj_layout_u32_recorded(buf_e4, launch_stream)?;
3120 self.provider.wcoj_4cycle_u32_recorded(
3121 &layout_e1,
3122 &layout_e2,
3123 &layout_e3,
3124 &layout_e4,
3125 launch_stream,
3126 )
3127 }
3128 WcojKeyWidth::EightByte => {
3129 let layout_e1 = self
3130 .provider
3131 .wcoj_layout_u64_recorded(buf_e1, launch_stream)?;
3132 let layout_e2 = self
3133 .provider
3134 .wcoj_layout_u64_recorded(buf_e2, launch_stream)?;
3135 let layout_e3 = self
3136 .provider
3137 .wcoj_layout_u64_recorded(buf_e3, launch_stream)?;
3138 let layout_e4 = self
3139 .provider
3140 .wcoj_layout_u64_recorded(buf_e4, launch_stream)?;
3141 self.provider.wcoj_4cycle_u64_recorded(
3142 &layout_e1,
3143 &layout_e2,
3144 &layout_e3,
3145 &layout_e4,
3146 launch_stream,
3147 )
3148 }
3149 }
3150 }
3151
3152 #[expect(
3158 clippy::too_many_arguments,
3159 reason = "leader-ordered four-cycle dispatch keeps four canonical inputs and the exact launch contract explicit"
3160 )]
3161 fn run_wcoj_4cycle_pipeline_with_leader_order(
3162 &self,
3163 buf_e1: &CudaBuffer,
3164 buf_e2: &CudaBuffer,
3165 buf_e3: &CudaBuffer,
3166 buf_e4: &CudaBuffer,
3167 launch_stream: StreamId,
3168 width: WcojKeyWidth,
3169 var_order: &VariableOrder,
3170 ) -> Result<CudaBuffer> {
3171 let canonical: [&CudaBuffer; 4] = [buf_e1, buf_e2, buf_e3, buf_e4];
3172 let slot_inputs = self.prepare_leader_inputs(&canonical, var_order, launch_stream)?;
3173 if slot_inputs.len() != 4 {
3174 return Err(xlog_core::XlogError::Kernel(
3175 "run_wcoj_4cycle_pipeline_with_leader_order: prepare_leader_inputs must return 4 slots"
3176 .to_string(),
3177 ));
3178 }
3179
3180 let head_schema = build_4cycle_head_schema(buf_e1, buf_e2, buf_e3)?;
3181 let perm = perm_indices_from_kernel_output_cols(&var_order.kernel_output_cols)?;
3182
3183 let kernel_out: CudaBuffer = match width {
3184 WcojKeyWidth::FourByte => {
3185 let l0 = self
3186 .provider
3187 .wcoj_layout_u32_recorded(&slot_inputs[0], launch_stream)?;
3188 let l1 = self
3189 .provider
3190 .wcoj_layout_u32_recorded(&slot_inputs[1], launch_stream)?;
3191 let l2 = self
3192 .provider
3193 .wcoj_layout_u32_recorded(&slot_inputs[2], launch_stream)?;
3194 let l3 = self
3195 .provider
3196 .wcoj_layout_u32_recorded(&slot_inputs[3], launch_stream)?;
3197 self.provider
3198 .wcoj_4cycle_u32_recorded(&l0, &l1, &l2, &l3, launch_stream)?
3199 }
3200 WcojKeyWidth::EightByte => {
3201 let l0 = self
3202 .provider
3203 .wcoj_layout_u64_recorded(&slot_inputs[0], launch_stream)?;
3204 let l1 = self
3205 .provider
3206 .wcoj_layout_u64_recorded(&slot_inputs[1], launch_stream)?;
3207 let l2 = self
3208 .provider
3209 .wcoj_layout_u64_recorded(&slot_inputs[2], launch_stream)?;
3210 let l3 = self
3211 .provider
3212 .wcoj_layout_u64_recorded(&slot_inputs[3], launch_stream)?;
3213 self.provider
3214 .wcoj_4cycle_u64_recorded(&l0, &l1, &l2, &l3, launch_stream)?
3215 }
3216 };
3217
3218 self.provider.wcoj_project_output_columns_recorded(
3219 &kernel_out,
3220 &perm,
3221 head_schema,
3222 launch_stream,
3223 )
3224 }
3225
3226 pub fn prepare_leader_inputs(
3254 &self,
3255 canonical: &[&CudaBuffer],
3256 var_order: &VariableOrder,
3257 launch_stream: StreamId,
3258 ) -> Result<Vec<CudaBuffer>> {
3259 let n = canonical.len();
3260 if !(n == 3 || n == 4) {
3261 return Err(xlog_core::XlogError::Kernel(format!(
3262 "prepare_leader_inputs: canonical inputs must be 3 (triangle) or 4 (4-cycle), got {n}"
3263 )));
3264 }
3265 let leader_idx = var_order.leader_idx as usize;
3266 if leader_idx >= n {
3267 return Err(xlog_core::XlogError::Kernel(format!(
3268 "prepare_leader_inputs: leader_idx {leader_idx} out of range for arity {n}"
3269 )));
3270 }
3271 if var_order.lookup_perms.len() != n - 1 {
3272 return Err(xlog_core::XlogError::Kernel(format!(
3273 "prepare_leader_inputs: lookup_perms.len() = {} must equal {} (arity - 1)",
3274 var_order.lookup_perms.len(),
3275 n - 1
3276 )));
3277 }
3278 for (slot, lp) in var_order.lookup_perms.iter().enumerate() {
3279 let input_idx = lp.input_idx as usize;
3280 if input_idx >= n {
3281 return Err(xlog_core::XlogError::Kernel(format!(
3282 "prepare_leader_inputs: lookup_perms[{slot}].input_idx {input_idx} out of range for arity {n}"
3283 )));
3284 }
3285 }
3286 if n == 4 {
3288 for lp in &var_order.lookup_perms {
3289 if lp.swap_cols {
3290 return Err(xlog_core::XlogError::Kernel(
3291 "prepare_leader_inputs: 4-cycle does not support col-swaps".to_string(),
3292 ));
3293 }
3294 }
3295 }
3296
3297 let mut slots: Vec<CudaBuffer> = Vec::with_capacity(n);
3314 slots.push(self.clone_buffer_via_swap(canonical[leader_idx], launch_stream)?);
3316 for lp in &var_order.lookup_perms {
3317 let src = canonical[lp.input_idx as usize];
3318 let buf = if lp.swap_cols {
3319 self.provider
3320 .wcoj_project_2col_swap_recorded(src, launch_stream)?
3321 } else {
3322 self.clone_buffer_via_swap(src, launch_stream)?
3323 };
3324 slots.push(buf);
3325 }
3326 Ok(slots)
3327 }
3328
3329 fn clone_buffer_via_swap(
3335 &self,
3336 src: &CudaBuffer,
3337 launch_stream: StreamId,
3338 ) -> Result<CudaBuffer> {
3339 let once = self
3340 .provider
3341 .wcoj_project_2col_swap_recorded(src, launch_stream)?;
3342 self.provider
3343 .wcoj_project_2col_swap_recorded(&once, launch_stream)
3344 }
3345
3346 pub fn wcoj_dispatch_stream_or_init(&self) -> Option<StreamId> {
3363 if let Some(s) = self.wcoj_dispatch_stream.get() {
3364 return Some(*s);
3365 }
3366 let runtime = self.provider.memory().runtime()?;
3367 let stream = runtime.stream_pool().acquire().ok()?;
3368 let _ = self.wcoj_dispatch_stream.set(stream);
3369 self.wcoj_dispatch_stream.get().copied()
3370 }
3371}
3372
3373impl Executor {
3386 pub fn wcoj_clique5_dispatch_count(&self) -> u64 {
3391 self.wcoj_clique5_dispatch_count
3392 }
3393
3394 pub fn wcoj_clique6_dispatch_count(&self) -> u64 {
3398 self.wcoj_clique6_dispatch_count
3399 }
3400
3401 pub fn wcoj_clique7_dispatch_count(&self) -> u64 {
3405 self.wcoj_clique7_dispatch_count
3406 }
3407
3408 pub fn wcoj_clique8_dispatch_count(&self) -> u64 {
3412 self.wcoj_clique8_dispatch_count
3413 }
3414
3415 pub fn kclique_histogram_refresh_count(&self) -> u64 {
3418 self.kclique_histogram_refresh_count
3419 }
3420
3421 pub fn kclique_histogram_refresh_nanos(&self) -> u128 {
3424 self.kclique_histogram_refresh_nanos
3425 }
3426
3427 pub(super) fn try_dispatch_wcoj_clique5(
3430 &mut self,
3431 rule: &CompiledRule,
3432 ) -> Result<Option<CudaBuffer>> {
3433 self.try_dispatch_wcoj_clique5_on_body(&rule.body)
3434 }
3435
3436 pub(super) fn try_dispatch_wcoj_clique6(
3438 &mut self,
3439 rule: &CompiledRule,
3440 ) -> Result<Option<CudaBuffer>> {
3441 self.try_dispatch_wcoj_clique6_on_body(&rule.body)
3442 }
3443
3444 pub(super) fn try_dispatch_wcoj_clique7(
3446 &mut self,
3447 rule: &CompiledRule,
3448 ) -> Result<Option<CudaBuffer>> {
3449 self.try_dispatch_wcoj_clique7_on_body(&rule.body)
3450 }
3451
3452 pub(super) fn try_dispatch_wcoj_clique8(
3454 &mut self,
3455 rule: &CompiledRule,
3456 ) -> Result<Option<CudaBuffer>> {
3457 self.try_dispatch_wcoj_clique8_on_body(&rule.body)
3458 }
3459
3460 pub(super) fn try_dispatch_wcoj_clique5_on_body(
3462 &mut self,
3463 body: &RirNode,
3464 ) -> Result<Option<CudaBuffer>> {
3465 self.try_dispatch_wcoj_clique_k_on_body(body, 5)
3466 }
3467
3468 pub(super) fn try_dispatch_wcoj_clique6_on_body(
3470 &mut self,
3471 body: &RirNode,
3472 ) -> Result<Option<CudaBuffer>> {
3473 self.try_dispatch_wcoj_clique_k_on_body(body, 6)
3474 }
3475
3476 pub(super) fn try_dispatch_wcoj_clique7_on_body(
3478 &mut self,
3479 body: &RirNode,
3480 ) -> Result<Option<CudaBuffer>> {
3481 self.try_dispatch_wcoj_clique_k_on_body(body, 7)
3482 }
3483
3484 pub(super) fn try_dispatch_wcoj_clique8_on_body(
3486 &mut self,
3487 body: &RirNode,
3488 ) -> Result<Option<CudaBuffer>> {
3489 self.try_dispatch_wcoj_clique_k_on_body(body, 8)
3490 }
3491
3492 fn try_dispatch_wcoj_clique_k_on_body(
3496 &mut self,
3497 body: &RirNode,
3498 k: usize,
3499 ) -> Result<Option<CudaBuffer>> {
3500 let expected_edges = k * (k - 1) / 2;
3501 let RirNode::MultiWayJoin {
3503 inputs,
3504 plan,
3505 var_order,
3506 ..
3507 } = body
3508 else {
3509 return Ok(None);
3510 };
3511 if matches!(plan, Some(MultiwayPlan::PlannedHashRoute { .. })) {
3512 return Ok(None);
3513 }
3514 if inputs.len() != expected_edges {
3515 return Ok(None);
3516 }
3517 let kclique = match var_order.as_ref().and_then(|order| order.kclique.as_ref()) {
3518 Some(plan) if usize::from(plan.k) == k => plan,
3519 _ => return Ok(None),
3520 };
3521 let mut rel_ids: Vec<RelId> = Vec::with_capacity(expected_edges);
3523 for input in inputs {
3524 let RirNode::Scan { rel } = input else {
3525 return Ok(None);
3526 };
3527 rel_ids.push(*rel);
3528 }
3529 let mut raw_bufs: Vec<&CudaBuffer> = Vec::with_capacity(expected_edges);
3531 for rid in &rel_ids {
3532 let name = match self.rel_names.get(rid) {
3533 Some(n) => n.clone(),
3534 None => return Ok(None),
3535 };
3536 match self.store.get(&name) {
3537 Some(b) => raw_bufs.push(b),
3538 None => return Ok(None),
3539 }
3540 }
3541 let launch_stream = match self.wcoj_dispatch_stream_or_init() {
3543 Some(s) => s,
3544 None => return Ok(None),
3545 };
3546 let first_ty = match raw_bufs[0].schema().column_type(0) {
3550 Some(t) => t,
3551 None => return Ok(None),
3552 };
3553 let is_u64 = matches!(first_ty, xlog_core::ScalarType::U64);
3554 let is_4byte = matches!(
3555 first_ty,
3556 xlog_core::ScalarType::U32 | xlog_core::ScalarType::Symbol
3557 );
3558 if !is_u64 && !is_4byte {
3559 return Ok(None);
3560 }
3561 let Some(plan_params) = kclique_dispatch_params(kclique, k) else {
3562 return Ok(None);
3563 };
3564 let head_schema = match build_kclique_head_schema(&raw_bufs, k) {
3565 Some(schema) => schema,
3566 None => return Ok(None),
3567 };
3568 let output_perm = match kclique_output_perm(kclique, k) {
3569 Some(perm) => perm,
3570 None => return Ok(None),
3571 };
3572 let laid_out = match self.orient_and_layout_kclique_edges(
3578 &raw_bufs,
3579 &plan_params,
3580 is_u64,
3581 launch_stream,
3582 ) {
3583 Ok(bufs) => bufs,
3584 Err(err) => {
3585 return wcoj_decline_on_error(
3586 &mut self.wcoj_error_decline_count,
3587 "k-clique-layout",
3588 err,
3589 )
3590 }
3591 };
3592 let edge_refs: Vec<&CudaBuffer> = laid_out.iter().collect();
3595 let result = match (k, is_u64) {
3597 (5, false) => {
3598 let arr: &[&CudaBuffer; 10] = match edge_refs.as_slice().try_into() {
3599 Ok(a) => a,
3600 Err(_) => return Ok(None),
3601 };
3602 self.provider.wcoj_clique5_u32_recorded_planned(
3603 arr,
3604 plan_params.leader_edge_idx,
3605 &plan_params.edge_order,
3606 &plan_params.iteration_order,
3607 launch_stream,
3608 )
3609 }
3610 (5, true) => {
3611 let arr: &[&CudaBuffer; 10] = match edge_refs.as_slice().try_into() {
3612 Ok(a) => a,
3613 Err(_) => return Ok(None),
3614 };
3615 self.provider.wcoj_clique5_u64_recorded_planned(
3616 arr,
3617 plan_params.leader_edge_idx,
3618 &plan_params.edge_order,
3619 &plan_params.iteration_order,
3620 launch_stream,
3621 )
3622 }
3623 (6, false) => {
3624 let arr: &[&CudaBuffer; 15] = match edge_refs.as_slice().try_into() {
3625 Ok(a) => a,
3626 Err(_) => return Ok(None),
3627 };
3628 self.provider.wcoj_clique6_u32_recorded_planned(
3629 arr,
3630 plan_params.leader_edge_idx,
3631 &plan_params.edge_order,
3632 &plan_params.iteration_order,
3633 launch_stream,
3634 )
3635 }
3636 (6, true) => {
3637 let arr: &[&CudaBuffer; 15] = match edge_refs.as_slice().try_into() {
3638 Ok(a) => a,
3639 Err(_) => return Ok(None),
3640 };
3641 self.provider.wcoj_clique6_u64_recorded_planned(
3642 arr,
3643 plan_params.leader_edge_idx,
3644 &plan_params.edge_order,
3645 &plan_params.iteration_order,
3646 launch_stream,
3647 )
3648 }
3649 (7, false) => {
3650 let arr: &[&CudaBuffer; 21] = match edge_refs.as_slice().try_into() {
3651 Ok(a) => a,
3652 Err(_) => return Ok(None),
3653 };
3654 self.provider.wcoj_clique7_u32_recorded_planned(
3655 arr,
3656 plan_params.leader_edge_idx,
3657 &plan_params.edge_order,
3658 &plan_params.iteration_order,
3659 launch_stream,
3660 )
3661 }
3662 (7, true) => {
3663 let arr: &[&CudaBuffer; 21] = match edge_refs.as_slice().try_into() {
3664 Ok(a) => a,
3665 Err(_) => return Ok(None),
3666 };
3667 self.provider.wcoj_clique7_u64_recorded_planned(
3668 arr,
3669 plan_params.leader_edge_idx,
3670 &plan_params.edge_order,
3671 &plan_params.iteration_order,
3672 launch_stream,
3673 )
3674 }
3675 (8, false) => {
3676 let arr: &[&CudaBuffer; 28] = match edge_refs.as_slice().try_into() {
3677 Ok(a) => a,
3678 Err(_) => return Ok(None),
3679 };
3680 self.provider.wcoj_clique8_u32_recorded_planned(
3681 arr,
3682 plan_params.leader_edge_idx,
3683 &plan_params.edge_order,
3684 &plan_params.iteration_order,
3685 launch_stream,
3686 )
3687 }
3688 (8, true) => {
3689 let arr: &[&CudaBuffer; 28] = match edge_refs.as_slice().try_into() {
3690 Ok(a) => a,
3691 Err(_) => return Ok(None),
3692 };
3693 self.provider.wcoj_clique8_u64_recorded_planned(
3694 arr,
3695 plan_params.leader_edge_idx,
3696 &plan_params.edge_order,
3697 &plan_params.iteration_order,
3698 launch_stream,
3699 )
3700 }
3701 _ => return Ok(None),
3702 };
3703 match result {
3706 Ok(buf) => {
3707 let buf = if output_perm.iter().copied().eq(0..output_perm.len()) {
3708 buf
3709 } else {
3710 self.provider.wcoj_project_output_columns_recorded(
3711 &buf,
3712 &output_perm,
3713 head_schema,
3714 launch_stream,
3715 )?
3716 };
3717 match k {
3718 5 => self.wcoj_clique5_dispatch_count += 1,
3719 6 => self.wcoj_clique6_dispatch_count += 1,
3720 7 => self.wcoj_clique7_dispatch_count += 1,
3721 8 => self.wcoj_clique8_dispatch_count += 1,
3722 _ => {}
3723 }
3724 Ok(Some(buf))
3725 }
3726 Err(err) => wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "k-clique", err),
3727 }
3728 }
3729
3730 fn orient_and_layout_kclique_edges(
3739 &self,
3740 raw_bufs: &[&CudaBuffer],
3741 plan_params: &KCliqueDispatchParams,
3742 is_u64: bool,
3743 launch_stream: StreamId,
3744 ) -> Result<Vec<CudaBuffer>> {
3745 let mut laid_out: Vec<CudaBuffer> = Vec::with_capacity(plan_params.edge_permutation.len());
3746 for (slot, &input_idx) in plan_params.edge_permutation.iter().enumerate() {
3747 let src = raw_bufs[input_idx];
3748 let swapped = if plan_params.swap_slots.contains(&slot) {
3749 Some(
3750 self.provider
3751 .wcoj_project_2col_swap_recorded(src, launch_stream)?,
3752 )
3753 } else {
3754 None
3755 };
3756 let oriented = swapped.as_ref().unwrap_or(src);
3757 let res = if plan_params.required_sort_slots.contains(&slot) {
3758 if is_u64 {
3759 self.provider
3760 .wcoj_layout_sort_u64_recorded(oriented, launch_stream)
3761 } else {
3762 self.provider
3763 .wcoj_layout_sort_u32_recorded(oriented, launch_stream)
3764 }
3765 } else if is_u64 {
3766 self.provider
3767 .wcoj_layout_u64_recorded(oriented, launch_stream)
3768 } else {
3769 self.provider
3770 .wcoj_layout_u32_recorded(oriented, launch_stream)
3771 };
3772 laid_out.push(res?);
3773 }
3774 Ok(laid_out)
3775 }
3776
3777 fn try_dispatch_wcoj_groupby_root_count_clique(
3794 &mut self,
3795 multiway: &RirNode,
3796 group_cols: &[ProjectExpr],
3797 ) -> Result<Option<CudaBuffer>> {
3798 let RirNode::MultiWayJoin {
3799 inputs,
3800 plan,
3801 var_order,
3802 ..
3803 } = multiway
3804 else {
3805 return Ok(None);
3806 };
3807 if matches!(plan, Some(MultiwayPlan::PlannedHashRoute { .. })) {
3808 return Ok(None);
3809 }
3810 let kclique = match var_order.as_ref().and_then(|order| order.kclique.as_ref()) {
3811 Some(plan) => plan,
3812 None => return Ok(None),
3813 };
3814 let k = usize::from(kclique.k);
3815 if !matches!(k, 5 | 6) {
3816 return Ok(None);
3817 }
3818 let expected_edges = k * (k - 1) / 2;
3819 if inputs.len() != expected_edges {
3820 return Ok(None);
3821 }
3822 let Some(ProjectExpr::Column(root_var)) = group_cols.first() else {
3824 return Ok(None);
3825 };
3826 let Some(positions) = live_kclique_variable_positions(kclique, k) else {
3827 return Ok(None);
3828 };
3829 if *root_var >= k || positions[*root_var] != 0 {
3830 return Ok(None);
3831 }
3832 let mut rel_ids: Vec<RelId> = Vec::with_capacity(expected_edges);
3834 for input in inputs {
3835 let RirNode::Scan { rel } = input else {
3836 return Ok(None);
3837 };
3838 rel_ids.push(*rel);
3839 }
3840 let mut raw_bufs: Vec<&CudaBuffer> = Vec::with_capacity(expected_edges);
3841 for rid in &rel_ids {
3842 let name = match self.rel_names.get(rid) {
3843 Some(n) => n.clone(),
3844 None => return Ok(None),
3845 };
3846 match self.store.get(&name) {
3847 Some(b) => raw_bufs.push(b),
3848 None => return Ok(None),
3849 }
3850 }
3851 for buf in &raw_bufs {
3852 if classify_two_col_wcoj_width(buf) != Some(WcojKeyWidth::FourByte) {
3853 return Ok(None);
3854 }
3855 }
3856 if self.provider.memory().runtime().is_none() {
3857 return Ok(None);
3858 }
3859 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
3860 return Ok(None);
3861 };
3862 let Some(plan_params) = kclique_dispatch_params(kclique, k) else {
3863 return Ok(None);
3864 };
3865 let laid_out = match self.orient_and_layout_kclique_edges(
3866 &raw_bufs,
3867 &plan_params,
3868 false,
3869 launch_stream,
3870 ) {
3871 Ok(bufs) => bufs,
3872 Err(err) => {
3873 return wcoj_decline_on_error(
3874 &mut self.wcoj_error_decline_count,
3875 "groupby-fusion-clique-layout",
3876 err,
3877 )
3878 }
3879 };
3880 let edge_refs: Vec<&CudaBuffer> = laid_out.iter().collect();
3881 let result = match k {
3882 5 => {
3883 let arr: &[&CudaBuffer; 10] = match edge_refs.as_slice().try_into() {
3884 Ok(a) => a,
3885 Err(_) => return Ok(None),
3886 };
3887 self.provider
3888 .wcoj_clique5_groupby_root_count_u32_recorded_planned(
3889 arr,
3890 plan_params.leader_edge_idx,
3891 &plan_params.edge_order,
3892 &plan_params.iteration_order,
3893 launch_stream,
3894 )
3895 }
3896 _ => {
3897 let arr: &[&CudaBuffer; 15] = match edge_refs.as_slice().try_into() {
3898 Ok(a) => a,
3899 Err(_) => return Ok(None),
3900 };
3901 self.provider
3902 .wcoj_clique6_groupby_root_count_u32_recorded_planned(
3903 arr,
3904 plan_params.leader_edge_idx,
3905 &plan_params.edge_order,
3906 &plan_params.iteration_order,
3907 launch_stream,
3908 )
3909 }
3910 };
3911 match result {
3912 Ok(buf) => {
3913 self.wcoj_groupby_fusion_dispatch_count += 1;
3914 Ok(Some(buf))
3915 }
3916 Err(err) => wcoj_decline_on_error(
3917 &mut self.wcoj_error_decline_count,
3918 "groupby-fusion-clique",
3919 err,
3920 ),
3921 }
3922 }
3923}
3924
3925#[derive(Debug)]
3926struct KCliqueDispatchParams {
3927 edge_permutation: Vec<usize>,
3928 edge_order: Vec<u8>,
3929 iteration_order: Vec<u8>,
3930 leader_edge_idx: u32,
3931 swap_slots: HashSet<usize>,
3932 required_sort_slots: HashSet<usize>,
3933}
3934
3935fn kclique_dispatch_params(plan: &KCliqueVariableOrder, k: usize) -> Option<KCliqueDispatchParams> {
3936 let expected_edges = k * (k - 1) / 2;
3937 let edge_permutation = live_kclique_edge_permutation(plan, expected_edges)?;
3938 let positions = live_kclique_variable_positions(plan, k)?;
3939 let mut edge_order = vec![u8::MAX; expected_edges];
3940
3941 for (slot, &edge_idx) in edge_permutation.iter().enumerate() {
3942 let (left, right) = clique_edge_pair(edge_idx, k)?;
3943 let left_pos = positions[left];
3944 let right_pos = positions[right];
3945 let logical_edge =
3946 clique_edge_idx_runtime(left_pos.min(right_pos), left_pos.max(right_pos), k)?;
3947 edge_order[logical_edge] = u8::try_from(slot).ok()?;
3948 }
3949 if edge_order.contains(&u8::MAX) {
3950 return None;
3951 }
3952 let leader_edge_idx = u32::from(edge_order[clique_edge_idx_runtime(0, 1, k)?]);
3953 let iteration_order: Vec<u8> = (0..k)
3954 .map(|idx| u8::try_from(idx).ok())
3955 .collect::<Option<_>>()?;
3956
3957 let swap_slots: HashSet<usize> = plan
3958 .column_swaps
3959 .iter()
3960 .filter(|swap| swap.swap_cols)
3961 .map(|swap| usize::from(swap.edge_slot))
3962 .collect();
3963 if swap_slots.iter().any(|slot| *slot >= expected_edges) {
3964 return None;
3965 }
3966 let required_sort_slots: HashSet<usize> = plan
3967 .sorted_layout_requirements
3968 .edge_slots
3969 .iter()
3970 .copied()
3971 .map(usize::from)
3972 .collect();
3973 if required_sort_slots
3974 .iter()
3975 .any(|slot| *slot >= expected_edges)
3976 {
3977 return None;
3978 }
3979
3980 Some(KCliqueDispatchParams {
3981 edge_permutation,
3982 edge_order,
3983 iteration_order,
3984 leader_edge_idx,
3985 swap_slots,
3986 required_sort_slots,
3987 })
3988}
3989
3990fn live_kclique_edge_permutation(
3991 plan: &KCliqueVariableOrder,
3992 expected_edges: usize,
3993) -> Option<Vec<usize>> {
3994 let values: Vec<usize> = plan
3995 .edge_permutation
3996 .iter()
3997 .copied()
3998 .take_while(|value| *value != u8::MAX)
3999 .map(usize::from)
4000 .collect();
4001 if values.len() != expected_edges {
4002 return None;
4003 }
4004 let mut seen = vec![false; expected_edges];
4005 for &value in &values {
4006 if value >= expected_edges || seen[value] {
4007 return None;
4008 }
4009 seen[value] = true;
4010 }
4011 Some(values)
4012}
4013
4014fn live_kclique_variable_positions(plan: &KCliqueVariableOrder, k: usize) -> Option<Vec<usize>> {
4015 let mut positions = Vec::with_capacity(k);
4016 let mut seen = vec![false; k];
4017 for original_var in 0..k {
4018 let pos = usize::from(*plan.variable_positions.get(original_var)?);
4019 if pos >= k || seen[pos] {
4020 return None;
4021 }
4022 seen[pos] = true;
4023 positions.push(pos);
4024 }
4025 Some(positions)
4026}
4027
4028fn clique_edge_idx_runtime(i: usize, j: usize, k: usize) -> Option<usize> {
4029 if !(i < j && j < k) {
4030 return None;
4031 }
4032 Some(i * (k - 1) - i.saturating_sub(1) * i / 2 + (j - i - 1))
4033}
4034
4035fn clique_edge_pair(edge_idx: usize, k: usize) -> Option<(usize, usize)> {
4036 let mut idx = 0usize;
4037 for i in 0..k {
4038 for j in (i + 1)..k {
4039 if idx == edge_idx {
4040 return Some((i, j));
4041 }
4042 idx += 1;
4043 }
4044 }
4045 None
4046}
4047
4048fn build_kclique_head_schema(raw_bufs: &[&CudaBuffer], k: usize) -> Option<Schema> {
4049 let mut columns = Vec::with_capacity(k);
4050 for variable in 0..k {
4051 let (edge_idx, col_idx) = if variable == 0 {
4052 (clique_edge_idx_runtime(0, 1, k)?, 0)
4053 } else {
4054 (clique_edge_idx_runtime(0, variable, k)?, 1)
4055 };
4056 let ty = raw_bufs.get(edge_idx)?.schema().column_type(col_idx)?;
4057 columns.push((format!("col{}", variable), ty));
4058 }
4059 Some(Schema::new(columns))
4060}
4061
4062fn kclique_output_perm(plan: &KCliqueVariableOrder, k: usize) -> Option<Vec<usize>> {
4063 let positions = live_kclique_variable_positions(plan, k)?;
4064 Some(positions)
4065}
4066
4067#[cfg(test)]
4068mod tests {
4069 use std::sync::{Mutex, OnceLock};
4070
4071 use super::{
4072 chain_dispatch_enabled, match_chain_join, match_multiway_triangle,
4073 record_chain_fallback_equivalents, wcoj_adaptive_enabled, wcoj_gate_enabled,
4074 wcoj_triangle_disabled, ENV_DISABLE_WCOJ_TRIANGLE, ENV_USE_WCOJ_TRIANGLE_U32,
4075 ENV_WCOJ_CHAIN_ENABLE,
4076 };
4077 use xlog_core::RelId;
4078 use xlog_ir::rir::ProjectExpr;
4079 use xlog_ir::RirNode;
4080
4081 fn canonical_multiway() -> RirNode {
4082 RirNode::MultiWayJoin {
4083 inputs: vec![
4084 RirNode::Scan { rel: RelId(1) },
4085 RirNode::Scan { rel: RelId(2) },
4086 RirNode::Scan { rel: RelId(3) },
4087 ],
4088 slot_vars: vec![
4089 vec![Some(0u32), Some(1)],
4090 vec![Some(1u32), Some(2)],
4091 vec![Some(0u32), Some(2)],
4092 ],
4093 output_columns: vec![
4094 ProjectExpr::Column(0),
4095 ProjectExpr::Column(1),
4096 ProjectExpr::Column(3),
4097 ],
4098 fallback: Box::new(RirNode::Unit),
4099 plan: None,
4100 var_order: None,
4101 }
4102 }
4103
4104 fn canonical_chain_join() -> RirNode {
4105 RirNode::ChainJoin {
4106 left: Box::new(RirNode::Scan { rel: RelId(1) }),
4107 right: Box::new(RirNode::Scan { rel: RelId(2) }),
4108 left_key: 1,
4109 right_key: 0,
4110 output_columns: vec![ProjectExpr::Column(0), ProjectExpr::Column(3)],
4111 fallback: Box::new(RirNode::Unit),
4112 }
4113 }
4114
4115 #[test]
4116 fn match_chain_returns_two_rels_and_keys() {
4117 let node = canonical_chain_join();
4118 let m = match_chain_join(&node).expect("must match canonical chain");
4119 assert_eq!(m.rel_left, RelId(1));
4120 assert_eq!(m.rel_right, RelId(2));
4121 assert_eq!(m.left_key, 1);
4122 assert_eq!(m.right_key, 0);
4123 assert_eq!(
4124 m.output_columns,
4125 vec![ProjectExpr::Column(0), ProjectExpr::Column(3)]
4126 );
4127 }
4128
4129 #[test]
4130 fn match_chain_rejects_non_scan_inputs() {
4131 let mut node = canonical_chain_join();
4132 if let RirNode::ChainJoin { left, .. } = &mut node {
4133 **left = RirNode::Unit;
4134 }
4135 assert!(match_chain_join(&node).is_none());
4136 }
4137
4138 #[test]
4139 fn match_chain_rejects_multiway_triangle() {
4140 let node = canonical_multiway();
4141 assert!(match_chain_join(&node).is_none());
4142 }
4143
4144 fn chain_with_scan_filter_fallback() -> RirNode {
4145 let mut node = canonical_chain_join();
4146 let RirNode::ChainJoin { fallback, .. } = &mut node else {
4147 unreachable!("canonical chain shape")
4148 };
4149 **fallback = RirNode::Union {
4150 inputs: vec![
4151 RirNode::Filter {
4152 input: Box::new(RirNode::Scan { rel: RelId(1) }),
4153 predicate: xlog_ir::Expr::And(Vec::new()),
4154 },
4155 RirNode::Project {
4156 input: Box::new(RirNode::Filter {
4157 input: Box::new(RirNode::Scan { rel: RelId(2) }),
4158 predicate: xlog_ir::Expr::And(Vec::new()),
4159 }),
4160 columns: vec![ProjectExpr::Column(0)],
4161 },
4162 ],
4163 };
4164 node
4165 }
4166
4167 #[test]
4168 fn matched_chain_success_records_embedded_fallback_equivalents() {
4169 let node = chain_with_scan_filter_fallback();
4170 let mut scans = 7;
4171 let mut filters = 11;
4172
4173 record_chain_fallback_equivalents(&node, true, &mut scans, &mut filters);
4174
4175 assert_eq!(scans, 9);
4176 assert_eq!(filters, 13);
4177 }
4178
4179 #[test]
4180 fn matched_chain_decline_records_no_embedded_fallback_equivalents() {
4181 let node = chain_with_scan_filter_fallback();
4182 let mut scans = 7;
4183 let mut filters = 11;
4184
4185 record_chain_fallback_equivalents(&node, false, &mut scans, &mut filters);
4186
4187 assert_eq!(scans, 7);
4188 assert_eq!(filters, 11);
4189 }
4190
4191 #[test]
4192 fn chain_dispatch_env_defaults_on_and_can_disable() {
4193 static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
4194 let _guard = ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
4195 let old = std::env::var(ENV_WCOJ_CHAIN_ENABLE).ok();
4196 unsafe {
4199 std::env::remove_var(ENV_WCOJ_CHAIN_ENABLE);
4200 }
4201 assert!(chain_dispatch_enabled().unwrap());
4202 unsafe {
4203 std::env::set_var(ENV_WCOJ_CHAIN_ENABLE, "0");
4204 }
4205 assert!(!chain_dispatch_enabled().unwrap());
4206 unsafe {
4207 std::env::set_var(ENV_WCOJ_CHAIN_ENABLE, "false");
4208 }
4209 assert!(!chain_dispatch_enabled().unwrap());
4210 unsafe {
4211 std::env::set_var(ENV_WCOJ_CHAIN_ENABLE, "1");
4212 }
4213 assert!(chain_dispatch_enabled().unwrap());
4214 unsafe {
4215 match old {
4216 Some(v) => std::env::set_var(ENV_WCOJ_CHAIN_ENABLE, v),
4217 None => std::env::remove_var(ENV_WCOJ_CHAIN_ENABLE),
4218 }
4219 }
4220 }
4221
4222 #[test]
4223 fn match_canonical_returns_three_rels() {
4224 let node = canonical_multiway();
4225 let m = match_multiway_triangle(&node).expect("must match canonical triangle");
4226 assert_eq!(m.rel_xy, RelId(1));
4227 assert_eq!(m.rel_yz, RelId(2));
4228 assert_eq!(m.rel_xz, RelId(3));
4229 }
4230
4231 #[test]
4232 fn match_rejects_non_multiway_body() {
4233 let node = RirNode::Scan { rel: RelId(1) };
4234 assert!(match_multiway_triangle(&node).is_none());
4235 }
4236
4237 #[test]
4238 fn match_rejects_rotated_output_columns() {
4239 let mut node = canonical_multiway();
4240 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4241 *output_columns = vec![
4242 ProjectExpr::Column(1),
4243 ProjectExpr::Column(0),
4244 ProjectExpr::Column(3),
4245 ];
4246 }
4247 assert!(match_multiway_triangle(&node).is_none());
4248 }
4249
4250 #[test]
4255 fn match_accepts_z_shared_triangle_output_columns() {
4256 let mut node = canonical_multiway();
4257 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4258 *output_columns = vec![
4259 ProjectExpr::Column(0),
4260 ProjectExpr::Column(2),
4261 ProjectExpr::Column(3),
4262 ];
4263 }
4264 let m = match_multiway_triangle(&node)
4265 .expect("matcher must accept the Z-shared output-column layout");
4266 assert_eq!(m.rel_xy, RelId(1));
4267 assert_eq!(m.rel_yz, RelId(2));
4268 assert_eq!(m.rel_xz, RelId(3));
4269 }
4270
4271 #[test]
4274 fn match_rejects_invalid_triangle_output_columns() {
4275 let mut node = canonical_multiway();
4276 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4277 *output_columns = vec![
4278 ProjectExpr::Column(0),
4279 ProjectExpr::Column(3),
4280 ProjectExpr::Column(3),
4281 ];
4282 }
4283 assert!(match_multiway_triangle(&node).is_none());
4284 }
4285
4286 #[test]
4287 fn match_rejects_arity_mismatched_output_columns() {
4288 let mut node = canonical_multiway();
4289 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4290 *output_columns = vec![ProjectExpr::Column(0), ProjectExpr::Column(1)];
4291 }
4292 assert!(match_multiway_triangle(&node).is_none());
4293 }
4294
4295 #[test]
4296 fn match_rejects_malformed_slot_vars() {
4297 let mut node = canonical_multiway();
4299 if let RirNode::MultiWayJoin { slot_vars, .. } = &mut node {
4300 *slot_vars = vec![
4301 vec![Some(0u32), Some(1)],
4302 vec![Some(1u32), Some(2)],
4303 vec![Some(0u32), Some(1)],
4304 ];
4305 }
4306 assert!(match_multiway_triangle(&node).is_none());
4307 }
4308
4309 #[test]
4310 fn match_rejects_repeated_var_in_slot() {
4311 let mut node = canonical_multiway();
4312 if let RirNode::MultiWayJoin { slot_vars, .. } = &mut node {
4313 *slot_vars = vec![
4315 vec![Some(0u32), Some(0)],
4316 vec![Some(1u32), Some(2)],
4317 vec![Some(0u32), Some(2)],
4318 ];
4319 }
4320 assert!(match_multiway_triangle(&node).is_none());
4321 }
4322
4323 #[test]
4324 fn match_rejects_non_scan_input() {
4325 let mut node = canonical_multiway();
4326 if let RirNode::MultiWayJoin { inputs, .. } = &mut node {
4327 inputs[0] = RirNode::Unit;
4328 }
4329 assert!(match_multiway_triangle(&node).is_none());
4330 }
4331
4332 #[test]
4333 fn match_rejects_input_arity_mismatch() {
4334 let mut node = canonical_multiway();
4335 if let RirNode::MultiWayJoin { inputs, .. } = &mut node {
4336 inputs.pop();
4337 }
4338 assert!(match_multiway_triangle(&node).is_none());
4339 }
4340
4341 fn env_lock() -> &'static Mutex<()> {
4342 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
4343 LOCK.get_or_init(|| Mutex::new(()))
4344 }
4345
4346 struct EnvSnapshot {
4347 force: Option<String>,
4348 disable: Option<String>,
4349 }
4350
4351 impl EnvSnapshot {
4352 fn capture_and_clear() -> Self {
4353 let snapshot = Self {
4354 force: std::env::var(ENV_USE_WCOJ_TRIANGLE_U32).ok(),
4355 disable: std::env::var(ENV_DISABLE_WCOJ_TRIANGLE).ok(),
4356 };
4357
4358 unsafe {
4361 std::env::remove_var(ENV_USE_WCOJ_TRIANGLE_U32);
4362 std::env::remove_var(ENV_DISABLE_WCOJ_TRIANGLE);
4363 }
4364
4365 snapshot
4366 }
4367 }
4368
4369 impl Drop for EnvSnapshot {
4370 fn drop(&mut self) {
4371 unsafe {
4374 match self.force.take() {
4375 Some(v) => std::env::set_var(ENV_USE_WCOJ_TRIANGLE_U32, v),
4376 None => std::env::remove_var(ENV_USE_WCOJ_TRIANGLE_U32),
4377 }
4378 match self.disable.take() {
4379 Some(v) => std::env::set_var(ENV_DISABLE_WCOJ_TRIANGLE, v),
4380 None => std::env::remove_var(ENV_DISABLE_WCOJ_TRIANGLE),
4381 }
4382 }
4383 }
4384 }
4385
4386 fn with_wcoj_env<R>(f: impl FnOnce() -> R) -> R {
4387 let _guard = env_lock().lock().expect("WCOJ env lock poisoned");
4388 let _snapshot = EnvSnapshot::capture_and_clear();
4389 f()
4390 }
4391
4392 fn set_env(name: &str, value: &str) {
4393 unsafe {
4396 std::env::set_var(name, value);
4397 }
4398 }
4399
4400 #[test]
4401 fn stats_gate_defaults_on_when_env_unset() {
4402 with_wcoj_env(|| {
4403 assert!(wcoj_adaptive_enabled(None));
4404 assert!(wcoj_adaptive_enabled(Some(true)));
4405 assert!(!wcoj_adaptive_enabled(Some(false)));
4406 });
4407 }
4408
4409 #[test]
4410 fn config_controls_stats_gate() {
4411 with_wcoj_env(|| {
4412 assert!(wcoj_adaptive_enabled(Some(true)));
4413 assert!(!wcoj_adaptive_enabled(Some(false)));
4414 });
4415 }
4416
4417 #[test]
4418 fn force_resolver_config_still_overrides_env() {
4419 with_wcoj_env(|| {
4420 set_env(ENV_USE_WCOJ_TRIANGLE_U32, "1");
4421 assert!(wcoj_gate_enabled(None).unwrap());
4422 assert!(!wcoj_gate_enabled(Some(false)).unwrap());
4423
4424 set_env(ENV_USE_WCOJ_TRIANGLE_U32, "0");
4425 assert!(!wcoj_gate_enabled(None).unwrap());
4426 assert!(wcoj_gate_enabled(Some(true)).unwrap());
4427 });
4428 }
4429
4430 #[test]
4431 fn triangle_kill_switch_honors_environment_and_explicit_config() {
4432 with_wcoj_env(|| {
4433 assert!(!wcoj_triangle_disabled(None).unwrap());
4434 set_env(ENV_DISABLE_WCOJ_TRIANGLE, "yes");
4435 assert!(wcoj_triangle_disabled(None).unwrap());
4436 assert!(!wcoj_triangle_disabled(Some(false)).unwrap());
4437 set_env(ENV_DISABLE_WCOJ_TRIANGLE, "off");
4438 assert!(wcoj_triangle_disabled(Some(true)).unwrap());
4439 });
4440 }
4441
4442 #[test]
4443 fn malformed_wcoj_boolean_is_a_typed_configuration_error() {
4444 with_wcoj_env(|| {
4445 set_env(ENV_USE_WCOJ_TRIANGLE_U32, "sometimes");
4446 assert!(matches!(
4447 wcoj_gate_enabled(None),
4448 Err(xlog_core::XlogError::Configuration { ref name, .. })
4449 if name == ENV_USE_WCOJ_TRIANGLE_U32
4450 ));
4451 assert!(!wcoj_gate_enabled(Some(false)).unwrap());
4452 });
4453 }
4454
4455 #[test]
4460 fn error_decline_counts_and_falls_back_by_default() {
4461 with_wcoj_env(|| {
4462 let mut counter = 0u64;
4463 let err = xlog_core::XlogError::Kernel("synthetic layout failure".to_string());
4464 let out = super::wcoj_decline_on_error(&mut counter, "triangle", err)
4465 .expect("default mode must decline to the binary-join fallback, not error");
4466 assert!(out.is_none(), "decline must hand control to the fallback");
4467 assert_eq!(counter, 1, "every error decline must be counted");
4468 });
4469 }
4470
4471 #[test]
4472 fn error_decline_propagates_under_strict_env() {
4473 with_wcoj_env(|| {
4474 set_env(super::ENV_WCOJ_STRICT, "1");
4475 let mut counter = 0u64;
4476 let err = xlog_core::XlogError::Kernel("synthetic layout failure".to_string());
4477 let out = super::wcoj_decline_on_error(&mut counter, "triangle", err);
4478 unsafe {
4480 std::env::remove_var(super::ENV_WCOJ_STRICT);
4481 }
4482 match out {
4483 Err(err) => assert!(
4484 err.to_string().contains("synthetic layout failure"),
4485 "strict mode must surface the original error: {err}"
4486 ),
4487 Ok(_) => panic!("XLOG_WCOJ_STRICT=1 must propagate the pipeline error"),
4488 }
4489 assert_eq!(counter, 1, "strict mode still counts the decline");
4490 });
4491 }
4492
4493 use super::{
4498 match_multiway_4cycle, wcoj_4cycle_adaptive_enabled, wcoj_4cycle_disabled,
4499 wcoj_4cycle_gate_enabled, ENV_DISABLE_WCOJ_4CYCLE, ENV_USE_WCOJ_4CYCLE,
4500 ENV_USE_WCOJ_4CYCLE_ADAPTIVE,
4501 };
4502
4503 struct EnvSnapshot4Cycle {
4504 force: Option<String>,
4505 adaptive: Option<String>,
4506 disable: Option<String>,
4507 }
4508
4509 impl EnvSnapshot4Cycle {
4510 fn capture_and_clear() -> Self {
4511 let snap = Self {
4512 force: std::env::var(ENV_USE_WCOJ_4CYCLE).ok(),
4513 adaptive: std::env::var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE).ok(),
4514 disable: std::env::var(ENV_DISABLE_WCOJ_4CYCLE).ok(),
4515 };
4516 unsafe {
4518 std::env::remove_var(ENV_USE_WCOJ_4CYCLE);
4519 std::env::remove_var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE);
4520 std::env::remove_var(ENV_DISABLE_WCOJ_4CYCLE);
4521 }
4522 snap
4523 }
4524 }
4525
4526 impl Drop for EnvSnapshot4Cycle {
4527 fn drop(&mut self) {
4528 unsafe {
4530 match self.force.take() {
4531 Some(v) => std::env::set_var(ENV_USE_WCOJ_4CYCLE, v),
4532 None => std::env::remove_var(ENV_USE_WCOJ_4CYCLE),
4533 }
4534 match self.adaptive.take() {
4535 Some(v) => std::env::set_var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE, v),
4536 None => std::env::remove_var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE),
4537 }
4538 match self.disable.take() {
4539 Some(v) => std::env::set_var(ENV_DISABLE_WCOJ_4CYCLE, v),
4540 None => std::env::remove_var(ENV_DISABLE_WCOJ_4CYCLE),
4541 }
4542 }
4543 }
4544 }
4545
4546 fn with_4cycle_env<R>(f: impl FnOnce() -> R) -> R {
4547 let _guard = env_lock().lock().expect("4-cycle env lock poisoned");
4548 let _snap = EnvSnapshot4Cycle::capture_and_clear();
4549 f()
4550 }
4551
4552 #[test]
4553 fn force_4cycle_resolver_defaults_off_when_env_unset() {
4554 with_4cycle_env(|| {
4555 assert!(!wcoj_4cycle_gate_enabled(None).unwrap());
4556 assert!(wcoj_4cycle_gate_enabled(Some(true)).unwrap());
4557 assert!(!wcoj_4cycle_gate_enabled(Some(false)).unwrap());
4558 });
4559 }
4560
4561 #[test]
4562 fn force_4cycle_resolver_env_can_enable() {
4563 with_4cycle_env(|| {
4564 set_env(ENV_USE_WCOJ_4CYCLE, "1");
4565 assert!(wcoj_4cycle_gate_enabled(None).unwrap());
4566 set_env(ENV_USE_WCOJ_4CYCLE, "true");
4567 assert!(wcoj_4cycle_gate_enabled(None).unwrap());
4568 set_env(ENV_USE_WCOJ_4CYCLE, "0");
4569 assert!(!wcoj_4cycle_gate_enabled(None).unwrap());
4570 });
4571 }
4572
4573 #[test]
4578 fn adaptive_4cycle_resolver_defaults_off_when_env_unset() {
4579 with_4cycle_env(|| {
4580 assert!(
4581 !wcoj_4cycle_adaptive_enabled(None).unwrap(),
4582 "4-cycle adaptive must be OPT-IN by default (unlike triangle's default-on)"
4583 );
4584 assert!(wcoj_4cycle_adaptive_enabled(Some(true)).unwrap());
4585 assert!(!wcoj_4cycle_adaptive_enabled(Some(false)).unwrap());
4586 });
4587 }
4588
4589 #[test]
4590 fn adaptive_4cycle_resolver_env_can_enable() {
4591 with_4cycle_env(|| {
4592 set_env(ENV_USE_WCOJ_4CYCLE_ADAPTIVE, "1");
4593 assert!(wcoj_4cycle_adaptive_enabled(None).unwrap());
4594 set_env(ENV_USE_WCOJ_4CYCLE_ADAPTIVE, "0");
4595 assert!(!wcoj_4cycle_adaptive_enabled(None).unwrap());
4596 set_env(ENV_USE_WCOJ_4CYCLE_ADAPTIVE, "true");
4597 assert!(wcoj_4cycle_adaptive_enabled(None).unwrap());
4598 });
4599 }
4600
4601 #[test]
4602 fn kill_4cycle_resolver_honors_env_and_config() {
4603 with_4cycle_env(|| {
4604 assert!(!wcoj_4cycle_disabled(None).unwrap());
4605 set_env(ENV_DISABLE_WCOJ_4CYCLE, "1");
4606 assert!(wcoj_4cycle_disabled(None).unwrap());
4607 assert!(!wcoj_4cycle_disabled(Some(false)).unwrap());
4608 set_env(ENV_DISABLE_WCOJ_4CYCLE, "0");
4609 assert!(wcoj_4cycle_disabled(Some(true)).unwrap());
4610 });
4611 }
4612
4613 fn canonical_4cycle_multiway() -> RirNode {
4614 RirNode::MultiWayJoin {
4615 inputs: vec![
4616 RirNode::Scan { rel: RelId(1) },
4617 RirNode::Scan { rel: RelId(2) },
4618 RirNode::Scan { rel: RelId(3) },
4619 RirNode::Scan { rel: RelId(4) },
4620 ],
4621 slot_vars: vec![
4622 vec![Some(0u32), Some(1)],
4623 vec![Some(1u32), Some(2)],
4624 vec![Some(2u32), Some(3)],
4625 vec![Some(3u32), Some(0)],
4626 ],
4627 output_columns: vec![
4628 ProjectExpr::Column(0),
4629 ProjectExpr::Column(1),
4630 ProjectExpr::Column(3),
4631 ProjectExpr::Column(5),
4632 ],
4633 fallback: Box::new(RirNode::Unit),
4634 plan: None,
4635 var_order: None,
4636 }
4637 }
4638
4639 #[test]
4640 fn match_4cycle_canonical_returns_four_rels() {
4641 let node = canonical_4cycle_multiway();
4642 let m = match_multiway_4cycle(&node).expect("must match canonical 4-cycle");
4643 assert_eq!(m.rel_e1, RelId(1));
4644 assert_eq!(m.rel_e2, RelId(2));
4645 assert_eq!(m.rel_e3, RelId(3));
4646 assert_eq!(m.rel_e4, RelId(4));
4647 }
4648
4649 #[test]
4650 fn match_4cycle_rejects_non_multiway() {
4651 assert!(match_multiway_4cycle(&RirNode::Scan { rel: RelId(1) }).is_none());
4652 }
4653
4654 #[test]
4655 fn match_4cycle_rejects_triangle_shape() {
4656 let triangle = RirNode::MultiWayJoin {
4658 inputs: vec![
4659 RirNode::Scan { rel: RelId(1) },
4660 RirNode::Scan { rel: RelId(2) },
4661 RirNode::Scan { rel: RelId(3) },
4662 ],
4663 slot_vars: vec![
4664 vec![Some(0u32), Some(1)],
4665 vec![Some(1u32), Some(2)],
4666 vec![Some(0u32), Some(2)],
4667 ],
4668 output_columns: vec![
4669 ProjectExpr::Column(0),
4670 ProjectExpr::Column(1),
4671 ProjectExpr::Column(3),
4672 ],
4673 fallback: Box::new(RirNode::Unit),
4674 plan: None,
4675 var_order: None,
4676 };
4677 assert!(match_multiway_4cycle(&triangle).is_none());
4678 }
4679
4680 #[test]
4681 fn match_4cycle_rejects_rotated_output_columns() {
4682 let mut node = canonical_4cycle_multiway();
4683 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4684 output_columns.swap(0, 1);
4685 }
4686 assert!(match_multiway_4cycle(&node).is_none());
4687 }
4688
4689 #[test]
4694 fn match_4cycle_accepts_alt_grouping_output_columns() {
4695 let mut node = canonical_4cycle_multiway();
4696 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4697 *output_columns = vec![
4698 ProjectExpr::Column(5),
4699 ProjectExpr::Column(0),
4700 ProjectExpr::Column(1),
4701 ProjectExpr::Column(3),
4702 ];
4703 }
4704 let m = match_multiway_4cycle(&node)
4705 .expect("matcher must accept the Alt-grouping output-column layout");
4706 assert_eq!(m.rel_e1, RelId(1));
4710 assert_eq!(m.rel_e2, RelId(2));
4711 assert_eq!(m.rel_e3, RelId(3));
4712 assert_eq!(m.rel_e4, RelId(4));
4713 }
4714
4715 #[test]
4719 fn match_4cycle_rejects_invalid_output_columns() {
4720 let mut node = canonical_4cycle_multiway();
4721 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4722 *output_columns = vec![
4723 ProjectExpr::Column(1),
4724 ProjectExpr::Column(0),
4725 ProjectExpr::Column(3),
4726 ProjectExpr::Column(5),
4727 ];
4728 }
4729 assert!(match_multiway_4cycle(&node).is_none());
4730 }
4731
4732 #[test]
4733 fn match_4cycle_rejects_arity_mismatched_output_columns() {
4734 let mut node = canonical_4cycle_multiway();
4735 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4736 output_columns.pop();
4737 }
4738 assert!(match_multiway_4cycle(&node).is_none());
4739 }
4740
4741 #[test]
4742 fn match_4cycle_rejects_unclosed_cycle() {
4743 let mut node = canonical_4cycle_multiway();
4746 if let RirNode::MultiWayJoin { slot_vars, .. } = &mut node {
4747 slot_vars[3] = vec![Some(3), Some(99)];
4748 }
4749 assert!(match_multiway_4cycle(&node).is_none());
4750 }
4751
4752 #[test]
4753 fn match_4cycle_rejects_non_scan_input() {
4754 let mut node = canonical_4cycle_multiway();
4755 if let RirNode::MultiWayJoin { inputs, .. } = &mut node {
4756 inputs[0] = RirNode::Unit;
4757 }
4758 assert!(match_multiway_4cycle(&node).is_none());
4759 }
4760
4761 #[test]
4762 fn match_4cycle_rejects_input_arity_mismatch() {
4763 let mut node = canonical_4cycle_multiway();
4764 if let RirNode::MultiWayJoin { inputs, .. } = &mut node {
4765 inputs.push(RirNode::Scan { rel: RelId(5) });
4766 }
4767 assert!(match_multiway_4cycle(&node).is_none());
4768 }
4769}