1use std::collections::HashSet;
77
78use xlog_core::{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 const ENV_USE_WCOJ_TRIANGLE_U32: &str = "XLOG_USE_WCOJ_TRIANGLE_U32";
98
99pub(super) fn wcoj_gate_enabled(config_override: Option<bool>) -> bool {
103 if let Some(v) = config_override {
104 return v;
105 }
106 std::env::var(ENV_USE_WCOJ_TRIANGLE_U32)
107 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
108 .unwrap_or(false)
109}
110
111pub const ENV_WCOJ_BLOCK_WORK_UNIT: &str = "XLOG_WCOJ_BLOCK_WORK_UNIT";
112pub(super) const WCOJ_BLOCK_WORK_UNIT_DEFAULT: u32 = 1024;
113pub(super) const WCOJ_BLOCK_WORK_UNIT_MAX: u32 = 8192;
114
115pub(super) fn wcoj_block_work_unit() -> u32 {
116 match std::env::var(ENV_WCOJ_BLOCK_WORK_UNIT) {
117 Ok(raw) => match raw.trim().parse::<u32>() {
118 Ok(v @ 1..=WCOJ_BLOCK_WORK_UNIT_MAX) => v,
119 Ok(v) => {
120 eprintln!(
121 "warning: {ENV_WCOJ_BLOCK_WORK_UNIT}={v} is outside 1..={WCOJ_BLOCK_WORK_UNIT_MAX}; \
122 using {WCOJ_BLOCK_WORK_UNIT_DEFAULT}"
123 );
124 WCOJ_BLOCK_WORK_UNIT_DEFAULT
125 }
126 Err(_) => {
127 eprintln!(
128 "warning: {ENV_WCOJ_BLOCK_WORK_UNIT}={raw:?} is not a u32; \
129 using {WCOJ_BLOCK_WORK_UNIT_DEFAULT}"
130 );
131 WCOJ_BLOCK_WORK_UNIT_DEFAULT
132 }
133 },
134 Err(_) => WCOJ_BLOCK_WORK_UNIT_DEFAULT,
135 }
136}
137
138pub(super) fn wcoj_adaptive_enabled(config_override: Option<bool>) -> bool {
139 config_override.unwrap_or(true)
140}
141
142pub const ENV_DISABLE_WCOJ_GROUPBY_FUSION: &str = "XLOG_DISABLE_WCOJ_GROUPBY_FUSION";
146
147pub(super) fn wcoj_groupby_fusion_disabled() -> bool {
148 std::env::var(ENV_DISABLE_WCOJ_GROUPBY_FUSION)
149 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
150 .unwrap_or(false)
151}
152
153pub const ENV_DISABLE_FREE_JOIN: &str = "XLOG_DISABLE_FREE_JOIN";
157
158pub(super) fn free_join_disabled() -> bool {
159 std::env::var(ENV_DISABLE_FREE_JOIN)
160 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
161 .unwrap_or(false)
162}
163
164pub const ENV_DISABLE_FACTORIZED_DELTA: &str = "XLOG_DISABLE_FACTORIZED_DELTA";
168
169pub(super) fn factorized_delta_disabled() -> bool {
170 std::env::var(ENV_DISABLE_FACTORIZED_DELTA)
171 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
172 .unwrap_or(false)
173}
174
175const FACTORIZED_DELTA_DEFAULT_MAX_DOMAIN: u32 = 1 << 14;
179
180fn factorized_delta_max_domain() -> u32 {
181 std::env::var("XLOG_FACTORIZED_DELTA_MAX_DOMAIN")
182 .ok()
183 .and_then(|v| v.parse::<u32>().ok())
184 .unwrap_or(FACTORIZED_DELTA_DEFAULT_MAX_DOMAIN)
185 .min(xlog_cuda::provider::FJ_DELTA_MAX_DOMAIN)
186}
187
188fn factorized_delta_max_table_bytes(budget_bytes: u64) -> u64 {
193 std::env::var("XLOG_FACTORIZED_DELTA_MAX_TABLE_BYTES")
194 .ok()
195 .and_then(|v| v.parse::<u64>().ok())
196 .unwrap_or(budget_bytes / 2)
197}
198
199fn factorized_delta_work_divisor() -> u64 {
203 std::env::var("XLOG_FACTORIZED_DELTA_WORK_DIVISOR")
204 .ok()
205 .and_then(|v| v.parse::<u64>().ok())
206 .filter(|&v| v >= 1)
207 .unwrap_or(8)
208}
209
210#[derive(Default)]
216pub(super) struct FactorizedDeltaCtx {
217 domain_by_key: std::collections::HashMap<(String, RelId), Option<u32>>,
218 static_norm_cache: std::collections::HashMap<(RelId, usize), CudaBuffer>,
219}
220
221pub const ENV_WCOJ_STRICT: &str = "XLOG_WCOJ_STRICT";
228
229pub(super) fn wcoj_strict_errors_enabled() -> bool {
230 std::env::var(ENV_WCOJ_STRICT)
231 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
232 .unwrap_or(false)
233}
234
235pub(super) fn wcoj_decline_on_error(
241 counter: &mut u64,
242 stage: &str,
243 err: xlog_core::XlogError,
244) -> Result<Option<CudaBuffer>> {
245 *counter += 1;
246 if wcoj_strict_errors_enabled() {
247 return Err(err);
248 }
249 eprintln!("warning: WCOJ {stage} pipeline error; declining to binary-join fallback: {err}");
250 Ok(None)
251}
252
253pub const ENV_WCOJ_CHAIN_ENABLE: &str = "XLOG_WCOJ_CHAIN_ENABLE";
257
258pub(super) fn chain_dispatch_enabled() -> bool {
259 std::env::var(ENV_WCOJ_CHAIN_ENABLE)
260 .map(|v| !(v == "0" || v.eq_ignore_ascii_case("false")))
261 .unwrap_or(true)
262}
263
264pub const ENV_USE_WCOJ_4CYCLE: &str = "XLOG_USE_WCOJ_4CYCLE";
278
279pub const ENV_USE_WCOJ_4CYCLE_ADAPTIVE: &str = "XLOG_USE_WCOJ_4CYCLE_ADAPTIVE";
281
282pub const ENV_DISABLE_WCOJ_4CYCLE: &str = "XLOG_DISABLE_WCOJ_4CYCLE";
284
285pub(super) fn wcoj_4cycle_gate_enabled(config_override: Option<bool>) -> bool {
287 if let Some(v) = config_override {
288 return v;
289 }
290 std::env::var(ENV_USE_WCOJ_4CYCLE)
291 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
292 .unwrap_or(false)
293}
294
295pub(super) fn wcoj_4cycle_adaptive_enabled(config_override: Option<bool>) -> bool {
305 if let Some(v) = config_override {
306 return v;
307 }
308 std::env::var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE)
309 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
310 .unwrap_or(false)
311}
312
313pub(super) fn wcoj_4cycle_disabled(config_override: Option<bool>) -> bool {
315 if let Some(v) = config_override {
316 return v;
317 }
318 std::env::var(ENV_DISABLE_WCOJ_4CYCLE)
319 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
320 .unwrap_or(false)
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq)]
325enum DispatchMode {
326 Force,
327 CostModel,
328}
329
330pub(super) struct ChainRirMatch {
333 pub rel_left: RelId,
334 pub rel_right: RelId,
335 pub left_key: usize,
336 pub right_key: usize,
337 pub output_columns: Vec<ProjectExpr>,
338}
339
340pub(super) fn match_chain_join(body: &RirNode) -> Option<ChainRirMatch> {
344 let RirNode::ChainJoin {
345 left,
346 right,
347 left_key,
348 right_key,
349 output_columns,
350 ..
351 } = body
352 else {
353 return None;
354 };
355 if *left_key >= 2 || *right_key >= 2 {
356 return None;
357 }
358 let rel_left = scan_rel(left)?;
359 let rel_right = scan_rel(right)?;
360 Some(ChainRirMatch {
361 rel_left,
362 rel_right,
363 left_key: *left_key,
364 right_key: *right_key,
365 output_columns: output_columns.clone(),
366 })
367}
368
369fn fallback_scan_filter_equivalents(node: &RirNode) -> (u64, u64) {
370 match node {
371 RirNode::Unit | RirNode::TensorMaskedJoin { .. } => (0, 0),
372 RirNode::Scan { .. } => (1, 0),
373 RirNode::Filter { input, .. } => {
374 let (scans, filters) = fallback_scan_filter_equivalents(input);
375 (scans, filters.saturating_add(1))
376 }
377 RirNode::Project { input, .. }
378 | RirNode::GroupBy { input, .. }
379 | RirNode::Distinct { input, .. } => fallback_scan_filter_equivalents(input),
380 RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
381 let left = fallback_scan_filter_equivalents(left);
382 let right = fallback_scan_filter_equivalents(right);
383 (
384 left.0.saturating_add(right.0),
385 left.1.saturating_add(right.1),
386 )
387 }
388 RirNode::ChainJoin { fallback, .. } | RirNode::MultiWayJoin { fallback, .. } => {
389 fallback_scan_filter_equivalents(fallback)
390 }
391 RirNode::Union { inputs } => inputs.iter().fold((0u64, 0u64), |total, input| {
392 let current = fallback_scan_filter_equivalents(input);
393 (
394 total.0.saturating_add(current.0),
395 total.1.saturating_add(current.1),
396 )
397 }),
398 RirNode::Fixpoint {
399 base, recursive, ..
400 } => {
401 let base = fallback_scan_filter_equivalents(base);
402 let recursive = fallback_scan_filter_equivalents(recursive);
403 (
404 base.0.saturating_add(recursive.0),
405 base.1.saturating_add(recursive.1),
406 )
407 }
408 }
409}
410
411fn record_chain_fallback_equivalents(
412 body: &RirNode,
413 installed: bool,
414 scan_equivalents: &mut u64,
415 filter_equivalents: &mut u64,
416) {
417 if !installed {
418 return;
419 }
420 let RirNode::ChainJoin { fallback, .. } = body else {
421 return;
422 };
423 let (scans, filters) = fallback_scan_filter_equivalents(fallback);
424 *scan_equivalents = scan_equivalents.saturating_add(scans);
425 *filter_equivalents = filter_equivalents.saturating_add(filters);
426}
427
428pub(super) struct TriangleRirMatch {
431 pub rel_xy: RelId,
434 pub rel_yz: RelId,
436 pub rel_xz: RelId,
439}
440
441pub(super) fn match_multiway_triangle(body: &RirNode) -> Option<TriangleRirMatch> {
454 let RirNode::MultiWayJoin {
455 inputs,
456 slot_vars,
457 output_columns,
458 ..
459 } = body
460 else {
461 return None;
462 };
463 if inputs.len() != 3 {
464 return None;
465 }
466 if !slot_vars_match_canonical_triangle(slot_vars) {
467 return None;
468 }
469 if !output_columns_match_canonical_triangle(output_columns) {
470 return None;
471 }
472 let rel_xy = scan_rel(&inputs[0])?;
473 let rel_yz = scan_rel(&inputs[1])?;
474 let rel_xz = scan_rel(&inputs[2])?;
475 Some(TriangleRirMatch {
476 rel_xy,
477 rel_yz,
478 rel_xz,
479 })
480}
481
482fn slot_vars_match_canonical_triangle(slot_vars: &[Vec<Option<u32>>]) -> bool {
487 if slot_vars.len() != 3 {
488 return false;
489 }
490 let s0 = &slot_vars[0];
491 let s1 = &slot_vars[1];
492 let s2 = &slot_vars[2];
493 if s0.len() != 2 || s1.len() != 2 || s2.len() != 2 {
494 return false;
495 }
496 let (a, b) = match (s0[0], s0[1]) {
497 (Some(a), Some(b)) if a != b => (a, b),
498 _ => return false,
499 };
500 let c = match (s1[0], s1[1]) {
501 (Some(b1), Some(c)) if b1 == b && c != a && c != b => c,
502 _ => return false,
503 };
504 matches!((s2[0], s2[1]), (Some(a2), Some(c2)) if a2 == a && c2 == c)
505}
506
507fn output_columns_match_canonical_triangle(cols: &[ProjectExpr]) -> bool {
519 if cols.len() != 3 {
520 return false;
521 }
522 let cols_pattern = (
523 matches!(cols[0], ProjectExpr::Column(0)),
524 matches!(cols[1], ProjectExpr::Column(1)) || matches!(cols[1], ProjectExpr::Column(2)),
525 matches!(cols[2], ProjectExpr::Column(3)),
526 );
527 cols_pattern == (true, true, true)
528}
529
530pub(super) struct FourCycleRirMatch {
538 pub rel_e1: RelId,
539 pub rel_e2: RelId,
540 pub rel_e3: RelId,
541 pub rel_e4: RelId,
542}
543
544pub(super) fn match_multiway_4cycle(body: &RirNode) -> Option<FourCycleRirMatch> {
552 let RirNode::MultiWayJoin {
553 inputs,
554 slot_vars,
555 output_columns,
556 ..
557 } = body
558 else {
559 return None;
560 };
561 if inputs.len() != 4 {
562 return None;
563 }
564 if !slot_vars_match_canonical_4cycle(slot_vars) {
565 return None;
566 }
567 if !output_columns_match_canonical_4cycle(output_columns) {
568 return None;
569 }
570 let rel_e1 = scan_rel(&inputs[0])?;
571 let rel_e2 = scan_rel(&inputs[1])?;
572 let rel_e3 = scan_rel(&inputs[2])?;
573 let rel_e4 = scan_rel(&inputs[3])?;
574 Some(FourCycleRirMatch {
575 rel_e1,
576 rel_e2,
577 rel_e3,
578 rel_e4,
579 })
580}
581
582fn slot_vars_match_canonical_4cycle(slot_vars: &[Vec<Option<u32>>]) -> bool {
587 if slot_vars.len() != 4 {
588 return false;
589 }
590 for s in slot_vars {
591 if s.len() != 2 {
592 return false;
593 }
594 }
595 let (a, b) = match (slot_vars[0][0], slot_vars[0][1]) {
596 (Some(a), Some(b)) if a != b => (a, b),
597 _ => return false,
598 };
599 let c = match (slot_vars[1][0], slot_vars[1][1]) {
600 (Some(b1), Some(c)) if b1 == b && c != a && c != b => c,
601 _ => return false,
602 };
603 let d = match (slot_vars[2][0], slot_vars[2][1]) {
604 (Some(c1), Some(d)) if c1 == c && d != a && d != b && d != c => d,
605 _ => return false,
606 };
607 matches!(
608 (slot_vars[3][0], slot_vars[3][1]),
609 (Some(d2), Some(a2)) if d2 == d && a2 == a
610 )
611}
612
613fn output_columns_match_canonical_4cycle(cols: &[ProjectExpr]) -> bool {
623 if cols.len() != 4 {
624 return false;
625 }
626 let exact = |idx: usize, want: usize| matches!(cols[idx], ProjectExpr::Column(c) if c == want);
627 let default_layout = exact(0, 0) && exact(1, 1) && exact(2, 3) && exact(3, 5);
629 let alt_layout = exact(0, 5) && exact(1, 0) && exact(2, 1) && exact(3, 3);
631 default_layout || alt_layout
632}
633
634fn scan_rel(node: &RirNode) -> Option<RelId> {
639 match node {
640 RirNode::Scan { rel } => Some(*rel),
641 _ => None,
642 }
643}
644
645#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649pub(super) enum WcojKeyWidth {
650 FourByte,
651 EightByte,
652}
653
654fn classify_two_col_wcoj_width(buf: &CudaBuffer) -> Option<WcojKeyWidth> {
668 if buf.arity() != 2 {
669 return None;
670 }
671 let c0 = buf.schema().column_type(0)?;
672 let c1 = buf.schema().column_type(1)?;
673 let w0 = scalar_wcoj_width(c0)?;
674 let w1 = scalar_wcoj_width(c1)?;
675 if w0 != w1 {
676 return None;
677 }
678 Some(w0)
679}
680
681fn scalar_wcoj_width(ty: xlog_core::ScalarType) -> Option<WcojKeyWidth> {
682 match ty {
683 xlog_core::ScalarType::U32 | xlog_core::ScalarType::Symbol => Some(WcojKeyWidth::FourByte),
684 xlog_core::ScalarType::U64 => Some(WcojKeyWidth::EightByte),
685 _ => None,
686 }
687}
688
689fn feedback_pair_from_var_order(
717 slot_rels: &[RelId],
718 var_order: Option<&VariableOrder>,
719) -> Option<(RelId, RelId, Vec<usize>, Vec<usize>)> {
720 if slot_rels.len() < 2 {
721 return None;
722 }
723 let Some(vo) = var_order else {
724 return Some((slot_rels[0], slot_rels[1], vec![1], vec![0]));
728 };
729 let leader_idx = vo.leader_idx as usize;
730 match slot_rels.len() {
731 3 => {
732 match leader_idx {
734 0 => Some((slot_rels[0], slot_rels[1], vec![1], vec![0])),
735 1 => {
736 Some((slot_rels[1], slot_rels[2], vec![1], vec![1]))
740 }
741 2 => {
742 Some((slot_rels[2], slot_rels[1], vec![1], vec![1]))
746 }
747 _ => None,
748 }
749 }
750 4 => {
751 if leader_idx >= 4 {
754 return None;
755 }
756 let slot1_input_idx = (leader_idx + 1) % 4;
757 Some((
758 slot_rels[leader_idx],
759 slot_rels[slot1_input_idx],
760 vec![1],
761 vec![0],
762 ))
763 }
764 _ => None,
765 }
766}
767
768fn perm_indices_from_kernel_output_cols(cols: &[ProjectExpr]) -> Result<Vec<usize>> {
769 let mut out = Vec::with_capacity(cols.len());
770 for c in cols {
771 match c {
772 ProjectExpr::Column(idx) => out.push(*idx),
773 other => {
774 return Err(xlog_core::XlogError::Kernel(format!(
775 "perm_indices_from_kernel_output_cols: \
776 kernel_output_cols must be ProjectExpr::Column(_), got {:?}",
777 other
778 )));
779 }
780 }
781 }
782 Ok(out)
783}
784
785fn build_triangle_head_schema(buf_xy: &CudaBuffer, buf_yz: &CudaBuffer) -> Result<Schema> {
790 let x_type = buf_xy.schema().column_type(0).ok_or_else(|| {
791 xlog_core::XlogError::Kernel("build_triangle_head_schema: e_xy.col0 type missing".into())
792 })?;
793 let y_type = buf_xy.schema().column_type(1).ok_or_else(|| {
794 xlog_core::XlogError::Kernel("build_triangle_head_schema: e_xy.col1 type missing".into())
795 })?;
796 let z_type = buf_yz.schema().column_type(1).ok_or_else(|| {
797 xlog_core::XlogError::Kernel("build_triangle_head_schema: e_yz.col1 type missing".into())
798 })?;
799 Schema::new(vec![
800 ("col0".to_string(), x_type),
801 ("col1".to_string(), y_type),
802 ("col2".to_string(), z_type),
803 ])
804 .with_sort_labels(vec![
805 buf_xy
806 .schema()
807 .column_sort_label(0)
808 .unwrap_or("col0")
809 .to_string(),
810 buf_xy
811 .schema()
812 .column_sort_label(1)
813 .unwrap_or("col1")
814 .to_string(),
815 buf_yz
816 .schema()
817 .column_sort_label(1)
818 .unwrap_or("col2")
819 .to_string(),
820 ])
821 .map_err(xlog_core::XlogError::Kernel)
822}
823
824fn build_4cycle_head_schema(
827 buf_e1: &CudaBuffer,
828 buf_e2: &CudaBuffer,
829 buf_e3: &CudaBuffer,
830) -> Result<Schema> {
831 let w_type = buf_e1.schema().column_type(0).ok_or_else(|| {
835 xlog_core::XlogError::Kernel("build_4cycle_head_schema: e_wx.col0 type missing".into())
836 })?;
837 let x_type = buf_e1.schema().column_type(1).ok_or_else(|| {
838 xlog_core::XlogError::Kernel("build_4cycle_head_schema: e_wx.col1 type missing".into())
839 })?;
840 let y_type = buf_e2.schema().column_type(1).ok_or_else(|| {
841 xlog_core::XlogError::Kernel("build_4cycle_head_schema: e_xy.col1 type missing".into())
842 })?;
843 let z_type = buf_e3.schema().column_type(1).ok_or_else(|| {
844 xlog_core::XlogError::Kernel("build_4cycle_head_schema: e_yz.col1 type missing".into())
845 })?;
846 let _: ScalarType = w_type;
850 Schema::new(vec![
851 ("col0".to_string(), w_type),
852 ("col1".to_string(), x_type),
853 ("col2".to_string(), y_type),
854 ("col3".to_string(), z_type),
855 ])
856 .with_sort_labels(vec![
857 buf_e1
858 .schema()
859 .column_sort_label(0)
860 .unwrap_or("col0")
861 .to_string(),
862 buf_e1
863 .schema()
864 .column_sort_label(1)
865 .unwrap_or("col1")
866 .to_string(),
867 buf_e2
868 .schema()
869 .column_sort_label(1)
870 .unwrap_or("col2")
871 .to_string(),
872 buf_e3
873 .schema()
874 .column_sort_label(1)
875 .unwrap_or("col3")
876 .to_string(),
877 ])
878 .map_err(xlog_core::XlogError::Kernel)
879}
880
881impl Executor {
882 pub(super) fn try_dispatch_wcoj_triangle(
893 &mut self,
894 rule: &CompiledRule,
895 ) -> Result<Option<CudaBuffer>> {
896 self.try_dispatch_wcoj_triangle_on_body(&rule.body)
899 }
900
901 fn wcoj_output_rows(buf: &CudaBuffer) -> Option<u64> {
908 buf.cached_row_count().map(u64::from)
911 }
912
913 fn record_wcoj_feedback(
966 &mut self,
967 slot_rels: &[RelId],
968 var_order: Option<&VariableOrder>,
969 output_rows: Option<u64>,
970 ) {
971 if slot_rels.len() < 2 {
972 return;
973 }
974 let Some(out_rows) = output_rows else {
975 return;
976 };
977 let Some((rel_a, rel_b, left_keys, right_keys)) =
984 feedback_pair_from_var_order(slot_rels, var_order)
985 else {
986 return;
987 };
988 let card_a = self
989 .stats
990 .get_relation_stats(rel_a)
991 .map(|s| s.cardinality)
992 .filter(|c| *c > 0);
993 let card_b = self
994 .stats
995 .get_relation_stats(rel_b)
996 .map(|s| s.cardinality)
997 .filter(|c| *c > 0);
998 let (Some(a), Some(b)) = (card_a, card_b) else {
999 return;
1000 };
1001 let input_rows = a.saturating_mul(b);
1002 self.stats
1005 .record_join_result(rel_a, rel_b, left_keys, right_keys, input_rows, out_rows);
1006 }
1007
1008 pub(super) fn try_dispatch_wcoj_triangle_on_body(
1015 &mut self,
1016 body: &RirNode,
1017 ) -> Result<Option<CudaBuffer>> {
1018 #[cfg(feature = "wcoj-phase-timing")]
1019 let wall_start = Instant::now();
1020 if self.config.wcoj_triangle_dispatch_disabled.unwrap_or(false) {
1030 return Ok(None);
1031 }
1032 let force_override = self.config.wcoj_triangle_dispatch;
1033 let force_on = wcoj_gate_enabled(force_override);
1034 let mode = if force_on {
1035 DispatchMode::Force
1036 } else {
1037 let force_explicit_off = matches!(force_override, Some(false));
1041 if force_explicit_off {
1042 return Ok(None);
1043 }
1044 let adaptive_override = self.config.wcoj_triangle_dispatch_adaptive;
1045 if wcoj_adaptive_enabled(adaptive_override) {
1046 DispatchMode::CostModel
1047 } else {
1048 return Ok(None);
1049 }
1050 };
1051
1052 let Some(matched) = match_multiway_triangle(body) else {
1054 return Ok(None);
1055 };
1056
1057 let name_xy = match self.get_rel_name(matched.rel_xy) {
1061 Some(s) => s.to_string(),
1062 None => return Ok(None),
1063 };
1064 let name_yz = match self.get_rel_name(matched.rel_yz) {
1065 Some(s) => s.to_string(),
1066 None => return Ok(None),
1067 };
1068 let name_xz = match self.get_rel_name(matched.rel_xz) {
1069 Some(s) => s.to_string(),
1070 None => return Ok(None),
1071 };
1072
1073 let buf_xy = match self.store.get(&name_xy) {
1078 Some(b) => b,
1079 None => return Ok(None),
1080 };
1081 let buf_yz = match self.store.get(&name_yz) {
1082 Some(b) => b,
1083 None => return Ok(None),
1084 };
1085 let buf_xz = match self.store.get(&name_xz) {
1086 Some(b) => b,
1087 None => return Ok(None),
1088 };
1089 let width = match (
1090 classify_two_col_wcoj_width(buf_xy),
1091 classify_two_col_wcoj_width(buf_yz),
1092 classify_two_col_wcoj_width(buf_xz),
1093 ) {
1094 (Some(a), Some(b), Some(c)) if a == b && b == c => a,
1095 _ => return Ok(None),
1096 };
1097
1098 if self.provider.memory().runtime().is_none() {
1109 return Ok(None);
1110 }
1111 let launch_stream = match self.wcoj_dispatch_stream_or_init() {
1112 Some(s) => s,
1113 None => return Ok(None),
1114 };
1115
1116 #[cfg(feature = "wcoj-phase-timing")]
1119 let mut classifier_ms: f32 = 0.0;
1120 if mode == DispatchMode::CostModel {
1121 #[cfg(feature = "wcoj-phase-timing")]
1122 let cls_start = Instant::now();
1123 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
1124 let slot_rels = [matched.rel_xy, matched.rel_yz, matched.rel_xz];
1125 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
1126 stats: &self.stats,
1127 launch_stream,
1128 width,
1129 slot_rels: &slot_rels,
1130 };
1131 let dispatch = model.should_dispatch_triangle(&ctx);
1132 #[cfg(feature = "wcoj-phase-timing")]
1133 {
1134 classifier_ms = cls_start.elapsed().as_secs_f64() as f32 * 1000.0;
1135 }
1136 if !dispatch {
1137 return Ok(None);
1138 }
1139 }
1140
1141 let var_order_opt: Option<&VariableOrder> = match body {
1144 RirNode::MultiWayJoin { var_order, .. } => var_order.as_ref(),
1145 _ => None,
1146 };
1147
1148 #[cfg(feature = "wcoj-phase-timing")]
1154 let mut layout_times: [f32; 3] = [0.0; 3];
1155 let dispatch_result = self.run_wcoj_triangle_pipeline(
1156 buf_xy,
1157 buf_yz,
1158 buf_xz,
1159 launch_stream,
1160 width,
1161 var_order_opt,
1162 #[cfg(feature = "wcoj-phase-timing")]
1163 &mut layout_times,
1164 );
1165 match dispatch_result {
1166 Ok(buf) => {
1167 let output_rows = Self::wcoj_output_rows(&buf);
1185 let slot_rels = [matched.rel_xy, matched.rel_yz, matched.rel_xz];
1186 self.record_wcoj_feedback(&slot_rels, var_order_opt, output_rows);
1187 self.wcoj_triangle_dispatch_count += 1;
1188 #[cfg(feature = "wcoj-phase-timing")]
1189 {
1190 let triangle_timing = self
1191 .provider
1192 .take_wcoj_triangle_phase_timing()
1193 .unwrap_or_default();
1194 let wall_ms = wall_start.elapsed().as_secs_f64() as f32 * 1000.0;
1195 let timing = super::wcoj_phase_timing::WcojDispatchPhaseTiming::new(
1196 classifier_ms,
1197 layout_times[0],
1198 layout_times[1],
1199 layout_times[2],
1200 triangle_timing,
1201 wall_ms,
1202 );
1203 if let Ok(mut g) = self.last_wcoj_phase_timing.lock() {
1204 *g = Some(timing);
1205 }
1206 }
1207 Ok(Some(buf))
1208 }
1209 Err(err) => wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "triangle", err),
1210 }
1211 }
1212
1213 #[allow(clippy::too_many_arguments)]
1224 fn run_wcoj_triangle_pipeline(
1225 &self,
1226 buf_xy: &CudaBuffer,
1227 buf_yz: &CudaBuffer,
1228 buf_xz: &CudaBuffer,
1229 launch_stream: StreamId,
1230 width: WcojKeyWidth,
1231 var_order: Option<&VariableOrder>,
1232 #[cfg(feature = "wcoj-phase-timing")] layout_times_ms: &mut [f32; 3],
1233 ) -> Result<CudaBuffer> {
1234 if let Some(vo) = var_order {
1240 return self.run_wcoj_triangle_pipeline_with_leader_order(
1241 buf_xy,
1242 buf_yz,
1243 buf_xz,
1244 launch_stream,
1245 width,
1246 vo,
1247 );
1248 }
1249 #[cfg(feature = "wcoj-phase-timing")]
1250 let mut time_layout =
1251 |f: &dyn Fn() -> Result<CudaBuffer>, slot: usize| -> Result<CudaBuffer> {
1252 let s = Instant::now();
1253 let r = f()?;
1254 layout_times_ms[slot] = s.elapsed().as_secs_f64() as f32 * 1000.0;
1255 Ok(r)
1256 };
1257 match width {
1258 WcojKeyWidth::FourByte => {
1259 #[cfg(feature = "wcoj-phase-timing")]
1260 let (layout_xy, layout_yz, layout_xz) = {
1261 let xy = time_layout(
1262 &|| {
1263 self.provider
1264 .wcoj_layout_u32_recorded(buf_xy, launch_stream)
1265 },
1266 0,
1267 )?;
1268 let yz = time_layout(
1269 &|| {
1270 self.provider
1271 .wcoj_layout_u32_recorded(buf_yz, launch_stream)
1272 },
1273 1,
1274 )?;
1275 let xz = time_layout(
1276 &|| {
1277 self.provider
1278 .wcoj_layout_u32_recorded(buf_xz, launch_stream)
1279 },
1280 2,
1281 )?;
1282 (xy, yz, xz)
1283 };
1284 #[cfg(not(feature = "wcoj-phase-timing"))]
1285 let layout_xy = self
1286 .provider
1287 .wcoj_layout_u32_recorded(buf_xy, launch_stream)?;
1288 #[cfg(not(feature = "wcoj-phase-timing"))]
1289 let layout_yz = self
1290 .provider
1291 .wcoj_layout_u32_recorded(buf_yz, launch_stream)?;
1292 #[cfg(not(feature = "wcoj-phase-timing"))]
1293 let layout_xz = self
1294 .provider
1295 .wcoj_layout_u32_recorded(buf_xz, launch_stream)?;
1296 let out = self.provider.wcoj_triangle_hg_u32_recorded(
1297 &layout_xy,
1298 &layout_yz,
1299 &layout_xz,
1300 wcoj_block_work_unit(),
1301 launch_stream,
1302 )?;
1303 self.provider.record_wcoj_triangle_hg_dispatch();
1304 Ok(out)
1305 }
1306 WcojKeyWidth::EightByte => {
1307 #[cfg(feature = "wcoj-phase-timing")]
1308 let (layout_xy, layout_yz, layout_xz) = {
1309 let xy = time_layout(
1310 &|| {
1311 self.provider
1312 .wcoj_layout_u64_recorded(buf_xy, launch_stream)
1313 },
1314 0,
1315 )?;
1316 let yz = time_layout(
1317 &|| {
1318 self.provider
1319 .wcoj_layout_u64_recorded(buf_yz, launch_stream)
1320 },
1321 1,
1322 )?;
1323 let xz = time_layout(
1324 &|| {
1325 self.provider
1326 .wcoj_layout_u64_recorded(buf_xz, launch_stream)
1327 },
1328 2,
1329 )?;
1330 (xy, yz, xz)
1331 };
1332 #[cfg(not(feature = "wcoj-phase-timing"))]
1333 let layout_xy = self
1334 .provider
1335 .wcoj_layout_u64_recorded(buf_xy, launch_stream)?;
1336 #[cfg(not(feature = "wcoj-phase-timing"))]
1337 let layout_yz = self
1338 .provider
1339 .wcoj_layout_u64_recorded(buf_yz, launch_stream)?;
1340 #[cfg(not(feature = "wcoj-phase-timing"))]
1341 let layout_xz = self
1342 .provider
1343 .wcoj_layout_u64_recorded(buf_xz, launch_stream)?;
1344 self.provider.wcoj_triangle_u64_recorded(
1345 &layout_xy,
1346 &layout_yz,
1347 &layout_xz,
1348 launch_stream,
1349 )
1350 }
1351 }
1352 }
1353
1354 fn run_wcoj_triangle_pipeline_with_leader_order(
1374 &self,
1375 buf_xy: &CudaBuffer,
1376 buf_yz: &CudaBuffer,
1377 buf_xz: &CudaBuffer,
1378 launch_stream: StreamId,
1379 width: WcojKeyWidth,
1380 var_order: &VariableOrder,
1381 ) -> Result<CudaBuffer> {
1382 let canonical: [&CudaBuffer; 3] = [buf_xy, buf_yz, buf_xz];
1383 let slot_inputs = self.prepare_leader_inputs(&canonical, var_order, launch_stream)?;
1384 if slot_inputs.len() != 3 {
1385 return Err(xlog_core::XlogError::Kernel(
1386 "run_wcoj_triangle_pipeline_with_leader_order: prepare_leader_inputs must return 3 slots"
1387 .to_string(),
1388 ));
1389 }
1390
1391 let head_schema = build_triangle_head_schema(buf_xy, buf_yz)?;
1397 let perm = perm_indices_from_kernel_output_cols(&var_order.kernel_output_cols)?;
1398
1399 let kernel_out: CudaBuffer = match width {
1400 WcojKeyWidth::FourByte => {
1401 let l0 = self
1402 .provider
1403 .wcoj_layout_u32_recorded(&slot_inputs[0], launch_stream)?;
1404 let l1 = self
1405 .provider
1406 .wcoj_layout_u32_recorded(&slot_inputs[1], launch_stream)?;
1407 let l2 = self
1408 .provider
1409 .wcoj_layout_u32_recorded(&slot_inputs[2], launch_stream)?;
1410 let out = self.provider.wcoj_triangle_hg_u32_recorded(
1411 &l0,
1412 &l1,
1413 &l2,
1414 wcoj_block_work_unit(),
1415 launch_stream,
1416 )?;
1417 self.provider.record_wcoj_triangle_hg_dispatch();
1418 out
1419 }
1420 WcojKeyWidth::EightByte => {
1421 let l0 = self
1422 .provider
1423 .wcoj_layout_u64_recorded(&slot_inputs[0], launch_stream)?;
1424 let l1 = self
1425 .provider
1426 .wcoj_layout_u64_recorded(&slot_inputs[1], launch_stream)?;
1427 let l2 = self
1428 .provider
1429 .wcoj_layout_u64_recorded(&slot_inputs[2], launch_stream)?;
1430 self.provider
1431 .wcoj_triangle_u64_recorded(&l0, &l1, &l2, launch_stream)?
1432 }
1433 };
1434
1435 self.provider.wcoj_project_output_columns_recorded(
1436 &kernel_out,
1437 &perm,
1438 head_schema,
1439 launch_stream,
1440 )
1441 }
1442
1443 pub fn wcoj_triangle_dispatch_count(&self) -> u64 {
1449 self.wcoj_triangle_dispatch_count
1450 }
1451
1452 pub fn wcoj_error_decline_count(&self) -> u64 {
1459 self.wcoj_error_decline_count
1460 }
1461
1462 pub fn free_join_dispatch_count(&self) -> u64 {
1465 self.free_join_dispatch_count
1466 }
1467
1468 pub fn factorized_delta_dispatch_count(&self) -> u64 {
1471 self.factorized_delta_dispatch_count
1472 }
1473
1474 fn factorized_delta_normalize_static(
1478 &self,
1479 buf: &CudaBuffer,
1480 key_col: usize,
1481 launch_stream: StreamId,
1482 ) -> Result<CudaBuffer> {
1483 if key_col == 0 {
1484 return self.provider.wcoj_layout_u32_recorded(buf, launch_stream);
1485 }
1486 let ty = |i: usize| {
1487 buf.schema().column_type(i).ok_or_else(|| {
1488 xlog_core::XlogError::Execution(format!(
1489 "factorized-delta: static column {i} type missing"
1490 ))
1491 })
1492 };
1493 let swapped = Schema::new(vec![("k".to_string(), ty(1)?), ("v".to_string(), ty(0)?)]);
1494 let projected = self.provider.wcoj_project_output_columns_recorded(
1495 buf,
1496 &[1, 0],
1497 swapped,
1498 launch_stream,
1499 )?;
1500 self.provider
1501 .wcoj_layout_u32_recorded(&projected, launch_stream)
1502 }
1503
1504 pub(super) fn try_dispatch_factorized_delta(
1521 &mut self,
1522 node: &RirNode,
1523 delta_rel: RelId,
1524 head_pred: &str,
1525 recursive_preds: &HashSet<String>,
1526 ctx: &mut FactorizedDeltaCtx,
1527 ) -> Result<Option<CudaBuffer>> {
1528 use xlog_cuda::provider::FjDeltaCols;
1529
1530 if factorized_delta_disabled() {
1531 return Ok(None);
1532 }
1533 let RirNode::ChainJoin {
1534 left,
1535 right,
1536 left_key,
1537 right_key,
1538 output_columns,
1539 ..
1540 } = node
1541 else {
1542 return Ok(None);
1543 };
1544 let (RirNode::Scan { rel: left_rel }, RirNode::Scan { rel: right_rel }) =
1545 (left.as_ref(), right.as_ref())
1546 else {
1547 return Ok(None);
1548 };
1549 let delta_on_left = match (*left_rel == delta_rel, *right_rel == delta_rel) {
1553 (true, false) => true,
1554 (false, true) => false,
1555 _ => return Ok(None),
1556 };
1557 let (delta_key, static_rel, static_key) = if delta_on_left {
1558 (*left_key, *right_rel, *right_key)
1559 } else {
1560 (*right_key, *left_rel, *left_key)
1561 };
1562 if delta_key > 1 || static_key > 1 {
1563 return Ok(None);
1564 }
1565 let delta_carry = 1 - delta_key;
1566 let static_value = 1 - static_key;
1567
1568 let (delta_off, static_off) = if delta_on_left { (0, 2) } else { (2, 0) };
1571 let carry_global = delta_off + delta_carry;
1572 let value_global = static_off + static_value;
1573 let [ProjectExpr::Column(out0), ProjectExpr::Column(out1)] = output_columns.as_slice()
1574 else {
1575 return Ok(None);
1576 };
1577 let (r_carry, r_value) = if (*out0, *out1) == (carry_global, value_global) {
1578 (0, 1)
1579 } else if (*out0, *out1) == (value_global, carry_global) {
1580 (1, 0)
1581 } else {
1582 return Ok(None);
1583 };
1584
1585 let binary_u32_class = |buf: &CudaBuffer| {
1587 buf.arity() == 2
1588 && (0..2).all(|i| {
1589 matches!(
1590 buf.schema().column_type(i),
1591 Some(ScalarType::U32) | Some(ScalarType::Symbol)
1592 )
1593 })
1594 };
1595 let Some(delta_name) = self.get_rel_name(delta_rel).map(str::to_string) else {
1596 return Ok(None);
1597 };
1598 let Some(static_name) = self.get_rel_name(static_rel).map(str::to_string) else {
1599 return Ok(None);
1600 };
1601 let Some(delta_buf) = self.store.get(&delta_name) else {
1602 return Ok(None);
1603 };
1604 let Some(static_buf) = self.store.get(&static_name) else {
1605 return Ok(None);
1606 };
1607 let Some(full_buf) = self.store.get(head_pred) else {
1608 return Ok(None);
1609 };
1610 if !binary_u32_class(delta_buf)
1611 || !binary_u32_class(static_buf)
1612 || !binary_u32_class(full_buf)
1613 {
1614 return Ok(None);
1615 }
1616 if self.provider.memory().runtime().is_none() {
1617 return Ok(None);
1618 }
1619 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
1620 return Ok(None);
1621 };
1622
1623 let domain_key = (head_pred.to_string(), static_rel);
1637 let domain = match ctx.domain_by_key.get(&domain_key) {
1638 Some(Some(d)) => *d,
1639 Some(None) => return Ok(None),
1640 None => {
1641 let max_id = match self.provider.fj_delta_columns_max_u32(
1642 &[
1643 (delta_buf, &[0, 1][..]),
1644 (static_buf, &[0, 1][..]),
1645 (full_buf, &[0, 1][..]),
1646 ],
1647 launch_stream,
1648 ) {
1649 Ok(m) => m,
1650 Err(err) => {
1651 return wcoj_decline_on_error(
1652 &mut self.wcoj_error_decline_count,
1653 "factorized-delta",
1654 err,
1655 );
1656 }
1657 };
1658 let decided = if max_id == u32::MAX {
1659 None
1660 } else {
1661 Some(max_id + 1)
1662 };
1663 ctx.domain_by_key.insert(domain_key, decided);
1664 match decided {
1665 Some(d) => d,
1666 None => return Ok(None),
1667 }
1668 }
1669 };
1670
1671 let n_delta = u64::from(self.buffer_row_count(delta_buf)?);
1672 let n_static = u64::from(self.buffer_row_count(static_buf)?);
1673 if n_delta == 0 || n_static == 0 {
1674 return Ok(None);
1675 }
1676
1677 let dense = domain <= factorized_delta_max_domain();
1684 if dense {
1685 let n_words = u64::from(domain.div_ceil(32)) * u64::from(domain);
1686 let work_est = n_delta.saturating_mul((n_static / u64::from(domain)).max(1));
1687 if work_est < n_words / factorized_delta_work_divisor() {
1688 return Ok(None);
1689 }
1690 }
1691
1692 let static_is_recursive = recursive_preds.contains(&static_name);
1698 let norm_owned;
1699 let static_norm: &CudaBuffer = if static_is_recursive {
1700 norm_owned =
1701 match self.factorized_delta_normalize_static(static_buf, static_key, launch_stream)
1702 {
1703 Ok(b) => b,
1704 Err(err) => {
1705 return wcoj_decline_on_error(
1706 &mut self.wcoj_error_decline_count,
1707 "factorized-delta",
1708 err,
1709 );
1710 }
1711 };
1712 &norm_owned
1713 } else {
1714 match ctx.static_norm_cache.entry((static_rel, static_key)) {
1715 std::collections::hash_map::Entry::Occupied(e) => &*e.into_mut(),
1716 std::collections::hash_map::Entry::Vacant(v) => {
1717 let norm = match self.factorized_delta_normalize_static(
1718 static_buf,
1719 static_key,
1720 launch_stream,
1721 ) {
1722 Ok(b) => b,
1723 Err(err) => {
1724 return wcoj_decline_on_error(
1725 &mut self.wcoj_error_decline_count,
1726 "factorized-delta",
1727 err,
1728 );
1729 }
1730 };
1731 &*v.insert(norm)
1732 }
1733 }
1734 };
1735
1736 let cols = FjDeltaCols {
1737 delta_carry,
1738 delta_key,
1739 r_carry,
1740 r_value,
1741 };
1742 if dense {
1743 match self.provider.fj_delta_novel_u32_recorded(
1744 delta_buf,
1745 static_norm,
1746 full_buf,
1747 cols,
1748 domain,
1749 launch_stream,
1750 ) {
1751 Ok(novel) => {
1752 self.factorized_delta_dispatch_count += 1;
1753 Ok(Some(novel))
1754 }
1755 Err(err) => wcoj_decline_on_error(
1756 &mut self.wcoj_error_decline_count,
1757 "factorized-delta",
1758 err,
1759 ),
1760 }
1761 } else {
1762 let max_table_bytes =
1766 factorized_delta_max_table_bytes(self.provider.memory().budget().device_bytes);
1767 match self.provider.fj_delta_sparse_novel_u32_recorded(
1768 delta_buf,
1769 static_norm,
1770 full_buf,
1771 cols,
1772 max_table_bytes,
1773 launch_stream,
1774 ) {
1775 Ok(Some(novel)) => {
1776 self.factorized_delta_dispatch_count += 1;
1777 Ok(Some(novel))
1778 }
1779 Ok(None) => Ok(None),
1780 Err(err) => wcoj_decline_on_error(
1781 &mut self.wcoj_error_decline_count,
1782 "factorized-delta",
1783 err,
1784 ),
1785 }
1786 }
1787 }
1788
1789 pub(super) fn try_dispatch_free_join(&mut self, node: &RirNode) -> Result<Option<CudaBuffer>> {
1805 use xlog_cuda::provider::{FjNode, FjPlan, FjSubAtom};
1806
1807 if free_join_disabled() {
1808 return Ok(None);
1809 }
1810 let RirNode::MultiWayJoin {
1811 inputs,
1812 slot_vars,
1813 output_columns,
1814 plan,
1815 ..
1816 } = node
1817 else {
1818 return Ok(None);
1819 };
1820 if !matches!(plan, Some(MultiwayPlan::FreeJoin)) {
1833 return Ok(None);
1834 }
1835 if inputs.len() < 3 {
1836 return Ok(None);
1837 }
1838 let mut bufs: Vec<&CudaBuffer> = Vec::with_capacity(inputs.len());
1843 let mut all_u32 = true;
1844 let mut all_u64 = true;
1845 for input in inputs {
1846 let RirNode::Scan { rel } = input else {
1847 return Ok(None);
1848 };
1849 let name = match self.get_rel_name(*rel) {
1850 Some(s) => s.to_string(),
1851 None => return Ok(None),
1852 };
1853 let Some(buf) = self.store.get(&name) else {
1854 return Ok(None);
1855 };
1856 for i in 0..buf.arity() {
1857 match buf.schema().column_type(i) {
1858 Some(ScalarType::U32 | ScalarType::Symbol) => all_u64 = false,
1859 Some(ScalarType::U64) => all_u32 = false,
1860 _ => return Ok(None),
1861 }
1862 }
1863 bufs.push(buf);
1864 }
1865 if !all_u32 && !all_u64 {
1866 return Ok(None);
1867 }
1868 {
1875 let slot_rels: Vec<RelId> = inputs
1876 .iter()
1877 .filter_map(|i| match i {
1878 RirNode::Scan { rel } => Some(*rel),
1879 _ => None,
1880 })
1881 .collect();
1882 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
1883 let width = if all_u32 {
1884 WcojKeyWidth::FourByte
1885 } else {
1886 WcojKeyWidth::EightByte
1887 };
1888 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
1889 stats: &self.stats,
1890 launch_stream: StreamId::DEFAULT,
1891 width,
1892 slot_rels: &slot_rels,
1893 };
1894 if model.factorized_loss_veto(&ctx) {
1895 return Ok(None);
1896 }
1897 }
1898 let mut class_to_var: Vec<u32> = Vec::new();
1900 let mut dense = |class: u32| -> usize {
1901 match class_to_var.iter().position(|c| *c == class) {
1902 Some(i) => i,
1903 None => {
1904 class_to_var.push(class);
1905 class_to_var.len() - 1
1906 }
1907 }
1908 };
1909 let mut atom_vars: Vec<Vec<usize>> = Vec::with_capacity(slot_vars.len());
1910 for (i, cols) in slot_vars.iter().enumerate() {
1911 if cols.len() != bufs[i].arity() {
1912 return Ok(None);
1913 }
1914 let mut vars = Vec::with_capacity(cols.len());
1915 for c in cols {
1916 let Some(class) = c else { return Ok(None) };
1917 vars.push(dense(*class));
1918 }
1919 atom_vars.push(vars);
1920 }
1921 let num_vars = class_to_var.len();
1922 let order: Vec<usize> = {
1937 let slot_rels: Vec<RelId> = inputs
1938 .iter()
1939 .filter_map(|i| match i {
1940 RirNode::Scan { rel } => Some(*rel),
1941 _ => None,
1942 })
1943 .collect();
1944 let cards: Vec<u64> = bufs.iter().map(|b| b.num_rows()).collect();
1945 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
1946 let width = if all_u32 {
1947 WcojKeyWidth::FourByte
1948 } else {
1949 WcojKeyWidth::EightByte
1950 };
1951 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
1952 stats: &self.stats,
1953 launch_stream: StreamId::DEFAULT,
1954 width,
1955 slot_rels: &slot_rels,
1956 };
1957 match model.plan_free_join_order(&ctx, &atom_vars, &cards) {
1958 super::wcoj_cost_model::FjOrderDecision::Decline => return Ok(None),
1959 super::wcoj_cost_model::FjOrderDecision::Reorder(o) => o,
1960 super::wcoj_cost_model::FjOrderDecision::KeepDefault => {
1961 (0..atom_vars.len()).collect()
1962 }
1963 }
1964 };
1965 let mut bound_at: Vec<Option<usize>> = vec![None; num_vars]; let mut nodes: Vec<FjNode> = Vec::new();
1972 for &i in &order {
1977 let vars = &atom_vars[i];
1978 let split = vars.iter().take_while(|v| bound_at[**v].is_some()).count();
1979 if vars[split..].iter().any(|v| bound_at[*v].is_some()) {
1980 return Ok(None);
1983 }
1984 if split > 0 {
1985 let probe = FjSubAtom {
1986 input_idx: i,
1987 var_positions: vars[..split].to_vec(),
1988 };
1989 if nodes.is_empty() {
1990 return Ok(None);
1991 }
1992 let target = vars[..split]
1993 .iter()
1994 .map(|v| bound_at[*v].expect("prefix vars are bound"))
1995 .max()
1996 .expect("split > 0");
1997 nodes[target].probes.push(probe);
1998 }
1999 if split < vars.len() {
2000 let cover_vars = vars[split..].to_vec();
2001 let mut seen = HashSet::new();
2002 if !cover_vars.iter().all(|v| seen.insert(*v)) {
2003 return Ok(None);
2004 }
2005 let k = nodes.len();
2006 for v in &cover_vars {
2007 bound_at[*v] = Some(k);
2008 }
2009 nodes.push(FjNode {
2010 cover: FjSubAtom {
2011 input_idx: i,
2012 var_positions: cover_vars,
2013 },
2014 probes: Vec::new(),
2015 });
2016 } else if nodes.is_empty() {
2017 return Ok(None);
2018 }
2019 }
2020 let mut col_to_var: Vec<usize> = Vec::new();
2023 for vars in &atom_vars {
2024 col_to_var.extend(vars.iter().copied());
2025 }
2026 let mut output_vars: Vec<usize> = Vec::with_capacity(output_columns.len());
2027 for oc in output_columns {
2028 let ProjectExpr::Column(c) = oc else {
2029 return Ok(None);
2030 };
2031 let Some(v) = col_to_var.get(*c) else {
2032 return Ok(None);
2033 };
2034 output_vars.push(*v);
2035 }
2036 let fj_plan = FjPlan {
2037 num_vars,
2038 nodes,
2039 output_vars,
2040 };
2041 if self.provider.memory().runtime().is_none() {
2042 return Ok(None);
2043 }
2044 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
2045 return Ok(None);
2046 };
2047 let outcome = if all_u32 {
2048 self.provider
2049 .free_join_execute_u32_recorded(&bufs, &fj_plan, launch_stream)
2050 } else {
2051 self.provider
2052 .free_join_execute_u64_recorded(&bufs, &fj_plan, launch_stream)
2053 };
2054 match outcome {
2055 Ok(buf) => {
2056 self.free_join_dispatch_count += 1;
2057 Ok(Some(buf))
2058 }
2059 Err(err) => wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "free-join", err),
2060 }
2061 }
2062
2063 fn try_dispatch_free_join_count(
2078 &mut self,
2079 node: &RirNode,
2080 group_cols: &[ProjectExpr],
2081 ) -> Result<Option<CudaBuffer>> {
2082 use xlog_cuda::provider::{FjNode, FjPlan, FjSubAtom};
2083
2084 if free_join_disabled() {
2085 return Ok(None);
2086 }
2087 let RirNode::MultiWayJoin {
2088 inputs,
2089 slot_vars,
2090 plan,
2091 ..
2092 } = node
2093 else {
2094 return Ok(None);
2095 };
2096 if !matches!(plan, Some(MultiwayPlan::FreeJoin)) {
2097 return Ok(None);
2098 }
2099 if inputs.len() < 3 {
2100 return Ok(None);
2101 }
2102 let mut bufs: Vec<&CudaBuffer> = Vec::with_capacity(inputs.len());
2103 for input in inputs {
2104 let RirNode::Scan { rel } = input else {
2105 return Ok(None);
2106 };
2107 let name = match self.get_rel_name(*rel) {
2108 Some(s) => s.to_string(),
2109 None => return Ok(None),
2110 };
2111 let Some(buf) = self.store.get(&name) else {
2112 return Ok(None);
2113 };
2114 let four_byte = (0..buf.arity()).all(|i| {
2115 matches!(
2116 buf.schema().column_type(i),
2117 Some(ScalarType::U32 | ScalarType::Symbol)
2118 )
2119 });
2120 if !four_byte {
2121 return Ok(None);
2122 }
2123 bufs.push(buf);
2124 }
2125 let mut class_to_var: Vec<u32> = Vec::new();
2128 let mut dense = |class: u32| -> usize {
2129 match class_to_var.iter().position(|c| *c == class) {
2130 Some(i) => i,
2131 None => {
2132 class_to_var.push(class);
2133 class_to_var.len() - 1
2134 }
2135 }
2136 };
2137 let mut atom_vars: Vec<Vec<usize>> = Vec::with_capacity(slot_vars.len());
2138 for (i, cols) in slot_vars.iter().enumerate() {
2139 if cols.len() != bufs[i].arity() {
2140 return Ok(None);
2141 }
2142 let mut vars = Vec::with_capacity(cols.len());
2143 for c in cols {
2144 let Some(class) = c else { return Ok(None) };
2145 vars.push(dense(*class));
2146 }
2147 atom_vars.push(vars);
2148 }
2149 let num_vars = class_to_var.len();
2150 let mut col_to_var: Vec<usize> = Vec::new();
2153 for vars in &atom_vars {
2154 col_to_var.extend(vars.iter().copied());
2155 }
2156 let Some(ProjectExpr::Column(key_col)) = group_cols.first() else {
2157 return Ok(None);
2158 };
2159 let Some(&group_var) = col_to_var.get(*key_col) else {
2160 return Ok(None);
2161 };
2162 let mut occurrences = vec![0usize; num_vars];
2166 for vars in &atom_vars {
2167 for &v in vars {
2168 occurrences[v] += 1;
2169 }
2170 }
2171 let mut bound_at: Vec<Option<usize>> = vec![None; num_vars];
2173 let mut nodes: Vec<FjNode> = Vec::new();
2174 for (i, vars) in atom_vars.iter().enumerate() {
2175 let split = vars.iter().take_while(|v| bound_at[**v].is_some()).count();
2176 if vars[split..].iter().any(|v| bound_at[*v].is_some()) {
2177 return Ok(None);
2179 }
2180 let mut keep_end = vars.len();
2181 while keep_end > split {
2182 let v = vars[keep_end - 1];
2183 if occurrences[v] == 1 && v != group_var {
2184 keep_end -= 1;
2185 } else {
2186 break;
2187 }
2188 }
2189 if split == 0 && keep_end == 0 {
2190 return Ok(None);
2194 }
2195 if split > 0 {
2196 let probe = FjSubAtom {
2197 input_idx: i,
2198 var_positions: vars[..split].to_vec(),
2199 };
2200 if nodes.is_empty() {
2201 return Ok(None);
2202 }
2203 let target = vars[..split]
2204 .iter()
2205 .map(|v| bound_at[*v].expect("prefix vars are bound"))
2206 .max()
2207 .expect("split > 0");
2208 nodes[target].probes.push(probe);
2209 }
2210 if split < keep_end {
2211 let cover_vars = vars[split..keep_end].to_vec();
2212 let mut seen = HashSet::new();
2213 if !cover_vars.iter().all(|v| seen.insert(*v)) {
2214 return Ok(None);
2215 }
2216 let k = nodes.len();
2217 for v in &cover_vars {
2218 bound_at[*v] = Some(k);
2219 }
2220 nodes.push(FjNode {
2221 cover: FjSubAtom {
2222 input_idx: i,
2223 var_positions: cover_vars,
2224 },
2225 probes: Vec::new(),
2226 });
2227 } else if nodes.is_empty() {
2228 return Ok(None);
2229 }
2230 }
2231 if bound_at[group_var].is_none() {
2232 return Ok(None);
2233 }
2234 let fj_plan = FjPlan {
2235 num_vars,
2236 nodes,
2237 output_vars: vec![group_var],
2238 };
2239 if self.provider.memory().runtime().is_none() {
2240 return Ok(None);
2241 }
2242 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
2243 return Ok(None);
2244 };
2245 match self
2246 .provider
2247 .free_join_count_by_root_u32_recorded(&bufs, &fj_plan, launch_stream)
2248 {
2249 Ok(buf) => {
2250 self.free_join_dispatch_count += 1;
2251 self.wcoj_groupby_fusion_dispatch_count += 1;
2252 Ok(Some(buf))
2253 }
2254 Err(err) => {
2255 wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "free-join-count", err)
2256 }
2257 }
2258 }
2259
2260 pub fn wcoj_groupby_fusion_dispatch_count(&self) -> u64 {
2264 self.wcoj_groupby_fusion_dispatch_count
2265 }
2266
2267 pub(super) fn try_dispatch_wcoj_groupby_root_agg(
2284 &mut self,
2285 input: &RirNode,
2286 key_cols: &[usize],
2287 aggs: &[(usize, xlog_core::AggOp)],
2288 ) -> Result<Option<CudaBuffer>> {
2289 use xlog_core::AggOp;
2290 if wcoj_groupby_fusion_disabled() {
2291 return Ok(None);
2292 }
2293 if key_cols != [0] {
2294 return Ok(None);
2295 }
2296 if aggs.len() != 1 {
2297 return Ok(None);
2298 }
2299 let (agg_col, agg_op) = aggs[0];
2300 if !matches!(agg_op, AggOp::Count | AggOp::Sum | AggOp::Min | AggOp::Max) {
2301 return Ok(None);
2302 }
2303 let RirNode::Project {
2304 input: multiway,
2305 columns,
2306 } = input
2307 else {
2308 return Ok(None);
2309 };
2310 if columns.is_empty() || !columns.iter().all(|c| matches!(c, ProjectExpr::Column(_))) {
2312 return Ok(None);
2313 }
2314 let key_is_col0 = matches!(columns[0], ProjectExpr::Column(0));
2319 let agg_value_col = if matches!(agg_op, AggOp::Count) {
2327 None
2328 } else {
2329 match columns.get(agg_col) {
2330 Some(ProjectExpr::Column(c)) if *c >= 1 => Some(*c),
2331 _ => return Ok(None),
2332 }
2333 };
2334 let Some(matched) = match_multiway_triangle(multiway) else {
2335 if key_is_col0 {
2339 if let Some(buf) =
2340 self.try_dispatch_wcoj_groupby_root_agg_4cycle(multiway, agg_op, agg_value_col)?
2341 {
2342 return Ok(Some(buf));
2343 }
2344 }
2345 if !matches!(agg_op, AggOp::Count) {
2350 return Ok(None);
2351 }
2352 if let Some(buf) =
2353 self.try_dispatch_wcoj_groupby_root_count_clique(multiway, columns)?
2354 {
2355 return Ok(Some(buf));
2356 }
2357 return self.try_dispatch_free_join_count(multiway, columns);
2361 };
2362 let agg_value = match agg_value_col {
2365 None => None,
2366 Some(1) => Some(WcojRootAggValue::Y),
2367 Some(2) => Some(WcojRootAggValue::Z),
2368 Some(_) => return Ok(None),
2369 };
2370 if !key_is_col0 {
2371 return Ok(None);
2372 }
2373 let name_xy = match self.get_rel_name(matched.rel_xy) {
2374 Some(s) => s.to_string(),
2375 None => return Ok(None),
2376 };
2377 let name_yz = match self.get_rel_name(matched.rel_yz) {
2378 Some(s) => s.to_string(),
2379 None => return Ok(None),
2380 };
2381 let name_xz = match self.get_rel_name(matched.rel_xz) {
2382 Some(s) => s.to_string(),
2383 None => return Ok(None),
2384 };
2385 let buf_xy = match self.store.get(&name_xy) {
2386 Some(b) => b,
2387 None => return Ok(None),
2388 };
2389 let buf_yz = match self.store.get(&name_yz) {
2390 Some(b) => b,
2391 None => return Ok(None),
2392 };
2393 let buf_xz = match self.store.get(&name_xz) {
2394 Some(b) => b,
2395 None => return Ok(None),
2396 };
2397 let width = match (
2398 classify_two_col_wcoj_width(buf_xy),
2399 classify_two_col_wcoj_width(buf_yz),
2400 classify_two_col_wcoj_width(buf_xz),
2401 ) {
2402 (
2403 Some(WcojKeyWidth::FourByte),
2404 Some(WcojKeyWidth::FourByte),
2405 Some(WcojKeyWidth::FourByte),
2406 ) => WcojKeyWidth::FourByte,
2407 (
2408 Some(WcojKeyWidth::EightByte),
2409 Some(WcojKeyWidth::EightByte),
2410 Some(WcojKeyWidth::EightByte),
2411 ) => WcojKeyWidth::EightByte,
2412 _ => return Ok(None),
2413 };
2414 {
2424 let slot_rels = [matched.rel_xy, matched.rel_yz, matched.rel_xz];
2425 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
2426 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
2427 stats: &self.stats,
2428 launch_stream: StreamId::DEFAULT,
2429 width,
2430 slot_rels: &slot_rels,
2431 };
2432 if model.factorized_loss_veto(&ctx) {
2433 return Ok(None);
2434 }
2435 }
2436 if matches!(width, WcojKeyWidth::FourByte) {
2443 match agg_value {
2444 Some(WcojRootAggValue::Y) => {
2445 if buf_xy.schema().column_type(1) != Some(xlog_core::ScalarType::U32) {
2446 return Ok(None);
2447 }
2448 }
2449 Some(WcojRootAggValue::Z) => {
2450 if buf_yz.schema().column_type(1) != Some(xlog_core::ScalarType::U32)
2451 || buf_xz.schema().column_type(1) != Some(xlog_core::ScalarType::U32)
2452 {
2453 return Ok(None);
2454 }
2455 }
2456 None => {}
2457 }
2458 }
2459 if self.provider.memory().runtime().is_none() {
2460 return Ok(None);
2461 }
2462 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
2463 return Ok(None);
2464 };
2465 let result = match (agg_value, width) {
2466 (None, WcojKeyWidth::FourByte) => {
2467 self.provider.wcoj_triangle_groupby_root_count_u32_recorded(
2468 buf_xy,
2469 buf_yz,
2470 buf_xz,
2471 wcoj_block_work_unit(),
2472 launch_stream,
2473 )
2474 }
2475 (None, WcojKeyWidth::EightByte) => {
2476 self.provider.wcoj_triangle_groupby_root_count_u64_recorded(
2477 buf_xy,
2478 buf_yz,
2479 buf_xz,
2480 wcoj_block_work_unit(),
2481 launch_stream,
2482 )
2483 }
2484 (Some(value), WcojKeyWidth::FourByte) => {
2485 self.provider.wcoj_triangle_groupby_root_agg_u32_recorded(
2486 buf_xy,
2487 buf_yz,
2488 buf_xz,
2489 agg_op,
2490 value,
2491 wcoj_block_work_unit(),
2492 launch_stream,
2493 )
2494 }
2495 (Some(value), WcojKeyWidth::EightByte) => {
2498 self.provider.wcoj_triangle_groupby_root_agg_u64_recorded(
2499 buf_xy,
2500 buf_yz,
2501 buf_xz,
2502 agg_op,
2503 value,
2504 wcoj_block_work_unit(),
2505 launch_stream,
2506 )
2507 }
2508 };
2509 match result {
2510 Ok(buf) => {
2511 self.wcoj_groupby_fusion_dispatch_count += 1;
2512 Ok(Some(buf))
2513 }
2514 Err(err) => {
2515 wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "groupby-fusion", err)
2516 }
2517 }
2518 }
2519
2520 fn try_dispatch_wcoj_groupby_root_agg_4cycle(
2552 &mut self,
2553 multiway: &RirNode,
2554 agg_op: xlog_core::AggOp,
2555 agg_value_col: Option<usize>,
2556 ) -> Result<Option<CudaBuffer>> {
2557 use xlog_core::AggOp;
2558 let Some(matched) = match_multiway_4cycle(multiway) else {
2559 return Ok(None);
2560 };
2561 let agg_value = match agg_value_col {
2564 None => None,
2565 Some(1) => Some(Wcoj4CycleRootAggValue::X),
2566 Some(2) => Some(Wcoj4CycleRootAggValue::Y),
2567 Some(3) => Some(Wcoj4CycleRootAggValue::Z),
2568 Some(_) => return Ok(None),
2569 };
2570 let name_e1 = match self.get_rel_name(matched.rel_e1) {
2571 Some(s) => s.to_string(),
2572 None => return Ok(None),
2573 };
2574 let name_e2 = match self.get_rel_name(matched.rel_e2) {
2575 Some(s) => s.to_string(),
2576 None => return Ok(None),
2577 };
2578 let name_e3 = match self.get_rel_name(matched.rel_e3) {
2579 Some(s) => s.to_string(),
2580 None => return Ok(None),
2581 };
2582 let name_e4 = match self.get_rel_name(matched.rel_e4) {
2583 Some(s) => s.to_string(),
2584 None => return Ok(None),
2585 };
2586 let buf_e1 = match self.store.get(&name_e1) {
2587 Some(b) => b,
2588 None => return Ok(None),
2589 };
2590 let buf_e2 = match self.store.get(&name_e2) {
2591 Some(b) => b,
2592 None => return Ok(None),
2593 };
2594 let buf_e3 = match self.store.get(&name_e3) {
2595 Some(b) => b,
2596 None => return Ok(None),
2597 };
2598 let buf_e4 = match self.store.get(&name_e4) {
2599 Some(b) => b,
2600 None => return Ok(None),
2601 };
2602 let width = match (
2603 classify_two_col_wcoj_width(buf_e1),
2604 classify_two_col_wcoj_width(buf_e2),
2605 classify_two_col_wcoj_width(buf_e3),
2606 classify_two_col_wcoj_width(buf_e4),
2607 ) {
2608 (
2609 Some(WcojKeyWidth::FourByte),
2610 Some(WcojKeyWidth::FourByte),
2611 Some(WcojKeyWidth::FourByte),
2612 Some(WcojKeyWidth::FourByte),
2613 ) => WcojKeyWidth::FourByte,
2614 (
2615 Some(WcojKeyWidth::EightByte),
2616 Some(WcojKeyWidth::EightByte),
2617 Some(WcojKeyWidth::EightByte),
2618 Some(WcojKeyWidth::EightByte),
2619 ) => WcojKeyWidth::EightByte,
2620 _ => return Ok(None),
2621 };
2622 if agg_value.is_some() && width != WcojKeyWidth::FourByte {
2625 return Ok(None);
2626 }
2627 let value_source = match agg_value {
2635 None => None,
2636 Some(Wcoj4CycleRootAggValue::X) => Some(buf_e1),
2637 Some(Wcoj4CycleRootAggValue::Y) => Some(buf_e2),
2638 Some(Wcoj4CycleRootAggValue::Z) => Some(buf_e3),
2639 };
2640 if let Some(src) = value_source {
2641 if src.schema().column_type(1) != Some(xlog_core::ScalarType::U32) {
2642 return Ok(None);
2643 }
2644 }
2645 if self.provider.memory().runtime().is_none() {
2646 return Ok(None);
2647 }
2648 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
2649 return Ok(None);
2650 };
2651 debug_assert!(
2652 agg_value.is_some() || matches!(agg_op, AggOp::Count),
2653 "non-Count aggregates resolve a value column above"
2654 );
2655 let result = match (agg_value, width) {
2656 (None, WcojKeyWidth::FourByte) => {
2657 self.provider.wcoj_4cycle_groupby_root_count_u32_recorded(
2658 buf_e1,
2659 buf_e2,
2660 buf_e3,
2661 buf_e4,
2662 wcoj_block_work_unit(),
2663 launch_stream,
2664 )
2665 }
2666 (None, WcojKeyWidth::EightByte) => {
2670 self.provider.wcoj_4cycle_groupby_root_count_u64_recorded(
2671 buf_e1,
2672 buf_e2,
2673 buf_e3,
2674 buf_e4,
2675 wcoj_block_work_unit(),
2676 launch_stream,
2677 )
2678 }
2679 (Some(value), _) => self.provider.wcoj_4cycle_groupby_root_agg_u32_recorded(
2680 buf_e1,
2681 buf_e2,
2682 buf_e3,
2683 buf_e4,
2684 agg_op,
2685 value,
2686 wcoj_block_work_unit(),
2687 launch_stream,
2688 ),
2689 };
2690 match result {
2691 Ok(buf) => {
2692 self.wcoj_groupby_fusion_dispatch_count += 1;
2693 Ok(Some(buf))
2694 }
2695 Err(err) => wcoj_decline_on_error(
2696 &mut self.wcoj_error_decline_count,
2697 "groupby-fusion-4cycle",
2698 err,
2699 ),
2700 }
2701 }
2702
2703 pub fn wcoj_4cycle_dispatch_count(&self) -> u64 {
2708 self.wcoj_4cycle_dispatch_count
2709 }
2710
2711 pub fn chain_dispatch_count(&self) -> u64 {
2714 self.chain_dispatch_count
2715 }
2716
2717 pub fn nested_loop_dispatch_count(&self) -> u64 {
2724 self.nested_loop_dispatch_count
2725 }
2726
2727 pub(super) fn try_dispatch_chain_on_body(
2738 &mut self,
2739 body: &RirNode,
2740 ) -> Result<Option<CudaBuffer>> {
2741 if !chain_dispatch_enabled() {
2742 return Ok(None);
2743 }
2744 let Some(matched) = match_chain_join(body) else {
2745 return Ok(None);
2746 };
2747
2748 let name_left = match self.get_rel_name(matched.rel_left) {
2749 Some(s) => s.to_string(),
2750 None => return Ok(None),
2751 };
2752 let name_right = match self.get_rel_name(matched.rel_right) {
2753 Some(s) => s.to_string(),
2754 None => return Ok(None),
2755 };
2756 let left = match self.store.get(&name_left) {
2757 Some(buf) => buf,
2758 None => return Ok(None),
2759 };
2760 let right = match self.store.get(&name_right) {
2761 Some(buf) => buf,
2762 None => return Ok(None),
2763 };
2764
2765 let num_left = self.provider.device_row_count(left)? as u64;
2766 let num_right = self.provider.device_row_count(right)? as u64;
2767 let in_threshold = num_left
2768 .checked_mul(num_right)
2769 .map(|p| p <= NESTED_LOOP_TOTAL_THRESHOLD)
2770 .unwrap_or(false);
2771 let four_byte = matches!(
2772 classify_two_col_wcoj_width(left),
2773 Some(WcojKeyWidth::FourByte)
2774 ) && matches!(
2775 classify_two_col_wcoj_width(right),
2776 Some(WcojKeyWidth::FourByte)
2777 );
2778
2779 let mut used_nested_loop = false;
2780 let joined = if four_byte {
2781 let left_sorted = self
2782 .provider
2783 .is_sorted_ascending_u32(left, matched.left_key)
2784 .unwrap_or(false);
2785 let right_sorted = self
2786 .provider
2787 .is_sorted_ascending_u32(right, matched.right_key)
2788 .unwrap_or(false);
2789 if left_sorted && right_sorted {
2790 if in_threshold {
2791 self.provider.sort_merge_join_v2_inner_u32_1key(
2792 left,
2793 right,
2794 matched.left_key,
2795 matched.right_key,
2796 )
2797 } else {
2798 let capacity = usize::try_from(num_left.min(num_right)).unwrap_or(usize::MAX);
2799 self.provider.sort_merge_join_v2_inner_u32_1key_bounded(
2800 left,
2801 right,
2802 matched.left_key,
2803 matched.right_key,
2804 capacity,
2805 )
2806 }
2807 } else if in_threshold {
2808 used_nested_loop = true;
2809 self.provider.nested_loop_join_v2_inner_u32_1key(
2810 left,
2811 right,
2812 matched.left_key,
2813 matched.right_key,
2814 )
2815 } else {
2816 self.provider.hash_join_v2(
2817 left,
2818 right,
2819 &[matched.left_key],
2820 &[matched.right_key],
2821 CudaJoinType::Inner,
2822 )
2823 }
2824 } else {
2825 self.provider.hash_join_v2(
2826 left,
2827 right,
2828 &[matched.left_key],
2829 &[matched.right_key],
2830 CudaJoinType::Inner,
2831 )
2832 };
2833
2834 let joined = match joined {
2835 Ok(buf) => buf,
2836 Err(err) => {
2837 record_chain_fallback_equivalents(
2838 body,
2839 false,
2840 &mut self.chain_fallback_scan_equivalents,
2841 &mut self.chain_fallback_filter_equivalents,
2842 );
2843 return wcoj_decline_on_error(
2844 &mut self.wcoj_error_decline_count,
2845 "chain-join",
2846 err,
2847 );
2848 }
2849 };
2850 let projected = match self.execute_project(&joined, &matched.output_columns) {
2851 Ok(buf) => buf,
2852 Err(err) => {
2853 record_chain_fallback_equivalents(
2854 body,
2855 false,
2856 &mut self.chain_fallback_scan_equivalents,
2857 &mut self.chain_fallback_filter_equivalents,
2858 );
2859 return wcoj_decline_on_error(
2860 &mut self.wcoj_error_decline_count,
2861 "chain-join-project",
2862 err,
2863 );
2864 }
2865 };
2866 self.stats.record_join_result(
2867 matched.rel_left,
2868 matched.rel_right,
2869 vec![matched.left_key],
2870 vec![matched.right_key],
2871 num_left.saturating_mul(num_right),
2872 joined.num_rows(),
2873 );
2874 if used_nested_loop {
2875 self.nested_loop_dispatch_count += 1;
2876 }
2877 record_chain_fallback_equivalents(
2878 body,
2879 true,
2880 &mut self.chain_fallback_scan_equivalents,
2881 &mut self.chain_fallback_filter_equivalents,
2882 );
2883 self.chain_dispatch_count += 1;
2884 Ok(Some(projected))
2885 }
2886
2887 pub(super) fn try_dispatch_wcoj_4cycle(
2903 &mut self,
2904 rule: &CompiledRule,
2905 ) -> Result<Option<CudaBuffer>> {
2906 self.try_dispatch_wcoj_4cycle_on_body(&rule.body)
2909 }
2910
2911 pub(super) fn try_dispatch_wcoj_4cycle_on_body(
2916 &mut self,
2917 body: &RirNode,
2918 ) -> Result<Option<CudaBuffer>> {
2919 if wcoj_4cycle_disabled(self.config.wcoj_4cycle_dispatch_disabled) {
2921 return Ok(None);
2922 }
2923 let force_override = self.config.wcoj_4cycle_dispatch;
2925 let force_on = wcoj_4cycle_gate_enabled(force_override);
2926 let mode = if force_on {
2927 DispatchMode::Force
2928 } else {
2929 if matches!(force_override, Some(false)) {
2932 return Ok(None);
2933 }
2934 let adaptive_override = self.config.wcoj_4cycle_dispatch_adaptive;
2935 if wcoj_4cycle_adaptive_enabled(adaptive_override) {
2936 DispatchMode::CostModel
2937 } else {
2938 return Ok(None);
2939 }
2940 };
2941
2942 let Some(matched) = match_multiway_4cycle(body) else {
2944 return Ok(None);
2945 };
2946
2947 let name_e1 = match self.get_rel_name(matched.rel_e1) {
2949 Some(s) => s.to_string(),
2950 None => return Ok(None),
2951 };
2952 let name_e2 = match self.get_rel_name(matched.rel_e2) {
2953 Some(s) => s.to_string(),
2954 None => return Ok(None),
2955 };
2956 let name_e3 = match self.get_rel_name(matched.rel_e3) {
2957 Some(s) => s.to_string(),
2958 None => return Ok(None),
2959 };
2960 let name_e4 = match self.get_rel_name(matched.rel_e4) {
2961 Some(s) => s.to_string(),
2962 None => return Ok(None),
2963 };
2964
2965 let buf_e1 = match self.store.get(&name_e1) {
2968 Some(b) => b,
2969 None => return Ok(None),
2970 };
2971 let buf_e2 = match self.store.get(&name_e2) {
2972 Some(b) => b,
2973 None => return Ok(None),
2974 };
2975 let buf_e3 = match self.store.get(&name_e3) {
2976 Some(b) => b,
2977 None => return Ok(None),
2978 };
2979 let buf_e4 = match self.store.get(&name_e4) {
2980 Some(b) => b,
2981 None => return Ok(None),
2982 };
2983 let width = match (
2984 classify_two_col_wcoj_width(buf_e1),
2985 classify_two_col_wcoj_width(buf_e2),
2986 classify_two_col_wcoj_width(buf_e3),
2987 classify_two_col_wcoj_width(buf_e4),
2988 ) {
2989 (Some(a), Some(b), Some(c), Some(d)) if a == b && b == c && c == d => a,
2990 _ => return Ok(None),
2991 };
2992
2993 if self.provider.memory().runtime().is_none() {
2997 return Ok(None);
2998 }
2999 let launch_stream = match self.wcoj_dispatch_stream_or_init() {
3000 Some(s) => s,
3001 None => return Ok(None),
3002 };
3003
3004 if mode == DispatchMode::CostModel {
3007 let model = super::wcoj_cost_model::build_wcoj_cost_model(&self.config);
3009 let slot_rels = [
3010 matched.rel_e1,
3011 matched.rel_e2,
3012 matched.rel_e3,
3013 matched.rel_e4,
3014 ];
3015 let ctx = super::wcoj_cost_model::WcojDispatchCtx {
3016 stats: &self.stats,
3017 launch_stream,
3018 width,
3019 slot_rels: &slot_rels,
3020 };
3021 let dispatch = model.should_dispatch_4cycle(&ctx);
3022 if !dispatch {
3023 return Ok(None);
3024 }
3025 }
3026
3027 let var_order_opt: Option<&VariableOrder> = match body {
3030 RirNode::MultiWayJoin { var_order, .. } => var_order.as_ref(),
3031 _ => None,
3032 };
3033
3034 let dispatch_result = self.run_wcoj_4cycle_pipeline(
3037 buf_e1,
3038 buf_e2,
3039 buf_e3,
3040 buf_e4,
3041 launch_stream,
3042 width,
3043 var_order_opt,
3044 );
3045 match dispatch_result {
3046 Ok(buf) => {
3047 let output_rows = Self::wcoj_output_rows(&buf);
3061 let slot_rels = [
3062 matched.rel_e1,
3063 matched.rel_e2,
3064 matched.rel_e3,
3065 matched.rel_e4,
3066 ];
3067 self.record_wcoj_feedback(&slot_rels, var_order_opt, output_rows);
3068 self.wcoj_4cycle_dispatch_count += 1;
3069 Ok(Some(buf))
3070 }
3071 Err(err) => wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "4-cycle", err),
3072 }
3073 }
3074
3075 #[allow(clippy::too_many_arguments)]
3077 fn run_wcoj_4cycle_pipeline(
3078 &self,
3079 buf_e1: &CudaBuffer,
3080 buf_e2: &CudaBuffer,
3081 buf_e3: &CudaBuffer,
3082 buf_e4: &CudaBuffer,
3083 launch_stream: StreamId,
3084 width: WcojKeyWidth,
3085 var_order: Option<&VariableOrder>,
3086 ) -> Result<CudaBuffer> {
3087 if let Some(vo) = var_order {
3088 return self.run_wcoj_4cycle_pipeline_with_leader_order(
3089 buf_e1,
3090 buf_e2,
3091 buf_e3,
3092 buf_e4,
3093 launch_stream,
3094 width,
3095 vo,
3096 );
3097 }
3098 match width {
3099 WcojKeyWidth::FourByte => {
3100 let layout_e1 = self
3101 .provider
3102 .wcoj_layout_u32_recorded(buf_e1, launch_stream)?;
3103 let layout_e2 = self
3104 .provider
3105 .wcoj_layout_u32_recorded(buf_e2, launch_stream)?;
3106 let layout_e3 = self
3107 .provider
3108 .wcoj_layout_u32_recorded(buf_e3, launch_stream)?;
3109 let layout_e4 = self
3110 .provider
3111 .wcoj_layout_u32_recorded(buf_e4, launch_stream)?;
3112 self.provider.wcoj_4cycle_u32_recorded(
3113 &layout_e1,
3114 &layout_e2,
3115 &layout_e3,
3116 &layout_e4,
3117 launch_stream,
3118 )
3119 }
3120 WcojKeyWidth::EightByte => {
3121 let layout_e1 = self
3122 .provider
3123 .wcoj_layout_u64_recorded(buf_e1, launch_stream)?;
3124 let layout_e2 = self
3125 .provider
3126 .wcoj_layout_u64_recorded(buf_e2, launch_stream)?;
3127 let layout_e3 = self
3128 .provider
3129 .wcoj_layout_u64_recorded(buf_e3, launch_stream)?;
3130 let layout_e4 = self
3131 .provider
3132 .wcoj_layout_u64_recorded(buf_e4, launch_stream)?;
3133 self.provider.wcoj_4cycle_u64_recorded(
3134 &layout_e1,
3135 &layout_e2,
3136 &layout_e3,
3137 &layout_e4,
3138 launch_stream,
3139 )
3140 }
3141 }
3142 }
3143
3144 #[allow(clippy::too_many_arguments)]
3150 fn run_wcoj_4cycle_pipeline_with_leader_order(
3151 &self,
3152 buf_e1: &CudaBuffer,
3153 buf_e2: &CudaBuffer,
3154 buf_e3: &CudaBuffer,
3155 buf_e4: &CudaBuffer,
3156 launch_stream: StreamId,
3157 width: WcojKeyWidth,
3158 var_order: &VariableOrder,
3159 ) -> Result<CudaBuffer> {
3160 let canonical: [&CudaBuffer; 4] = [buf_e1, buf_e2, buf_e3, buf_e4];
3161 let slot_inputs = self.prepare_leader_inputs(&canonical, var_order, launch_stream)?;
3162 if slot_inputs.len() != 4 {
3163 return Err(xlog_core::XlogError::Kernel(
3164 "run_wcoj_4cycle_pipeline_with_leader_order: prepare_leader_inputs must return 4 slots"
3165 .to_string(),
3166 ));
3167 }
3168
3169 let head_schema = build_4cycle_head_schema(buf_e1, buf_e2, buf_e3)?;
3170 let perm = perm_indices_from_kernel_output_cols(&var_order.kernel_output_cols)?;
3171
3172 let kernel_out: CudaBuffer = match width {
3173 WcojKeyWidth::FourByte => {
3174 let l0 = self
3175 .provider
3176 .wcoj_layout_u32_recorded(&slot_inputs[0], launch_stream)?;
3177 let l1 = self
3178 .provider
3179 .wcoj_layout_u32_recorded(&slot_inputs[1], launch_stream)?;
3180 let l2 = self
3181 .provider
3182 .wcoj_layout_u32_recorded(&slot_inputs[2], launch_stream)?;
3183 let l3 = self
3184 .provider
3185 .wcoj_layout_u32_recorded(&slot_inputs[3], launch_stream)?;
3186 self.provider
3187 .wcoj_4cycle_u32_recorded(&l0, &l1, &l2, &l3, launch_stream)?
3188 }
3189 WcojKeyWidth::EightByte => {
3190 let l0 = self
3191 .provider
3192 .wcoj_layout_u64_recorded(&slot_inputs[0], launch_stream)?;
3193 let l1 = self
3194 .provider
3195 .wcoj_layout_u64_recorded(&slot_inputs[1], launch_stream)?;
3196 let l2 = self
3197 .provider
3198 .wcoj_layout_u64_recorded(&slot_inputs[2], launch_stream)?;
3199 let l3 = self
3200 .provider
3201 .wcoj_layout_u64_recorded(&slot_inputs[3], launch_stream)?;
3202 self.provider
3203 .wcoj_4cycle_u64_recorded(&l0, &l1, &l2, &l3, launch_stream)?
3204 }
3205 };
3206
3207 self.provider.wcoj_project_output_columns_recorded(
3208 &kernel_out,
3209 &perm,
3210 head_schema,
3211 launch_stream,
3212 )
3213 }
3214
3215 pub fn prepare_leader_inputs(
3243 &self,
3244 canonical: &[&CudaBuffer],
3245 var_order: &VariableOrder,
3246 launch_stream: StreamId,
3247 ) -> Result<Vec<CudaBuffer>> {
3248 let n = canonical.len();
3249 if !(n == 3 || n == 4) {
3250 return Err(xlog_core::XlogError::Kernel(format!(
3251 "prepare_leader_inputs: canonical inputs must be 3 (triangle) or 4 (4-cycle), got {n}"
3252 )));
3253 }
3254 let leader_idx = var_order.leader_idx as usize;
3255 if leader_idx >= n {
3256 return Err(xlog_core::XlogError::Kernel(format!(
3257 "prepare_leader_inputs: leader_idx {leader_idx} out of range for arity {n}"
3258 )));
3259 }
3260 if var_order.lookup_perms.len() != n - 1 {
3261 return Err(xlog_core::XlogError::Kernel(format!(
3262 "prepare_leader_inputs: lookup_perms.len() = {} must equal {} (arity - 1)",
3263 var_order.lookup_perms.len(),
3264 n - 1
3265 )));
3266 }
3267 for (slot, lp) in var_order.lookup_perms.iter().enumerate() {
3268 let input_idx = lp.input_idx as usize;
3269 if input_idx >= n {
3270 return Err(xlog_core::XlogError::Kernel(format!(
3271 "prepare_leader_inputs: lookup_perms[{slot}].input_idx {input_idx} out of range for arity {n}"
3272 )));
3273 }
3274 }
3275 if n == 4 {
3277 for lp in &var_order.lookup_perms {
3278 if lp.swap_cols {
3279 return Err(xlog_core::XlogError::Kernel(
3280 "prepare_leader_inputs: 4-cycle does not support col-swaps".to_string(),
3281 ));
3282 }
3283 }
3284 }
3285
3286 let mut slots: Vec<CudaBuffer> = Vec::with_capacity(n);
3303 slots.push(self.clone_buffer_via_swap(canonical[leader_idx], launch_stream)?);
3305 for lp in &var_order.lookup_perms {
3306 let src = canonical[lp.input_idx as usize];
3307 let buf = if lp.swap_cols {
3308 self.provider
3309 .wcoj_project_2col_swap_recorded(src, launch_stream)?
3310 } else {
3311 self.clone_buffer_via_swap(src, launch_stream)?
3312 };
3313 slots.push(buf);
3314 }
3315 Ok(slots)
3316 }
3317
3318 fn clone_buffer_via_swap(
3324 &self,
3325 src: &CudaBuffer,
3326 launch_stream: StreamId,
3327 ) -> Result<CudaBuffer> {
3328 let once = self
3329 .provider
3330 .wcoj_project_2col_swap_recorded(src, launch_stream)?;
3331 self.provider
3332 .wcoj_project_2col_swap_recorded(&once, launch_stream)
3333 }
3334
3335 pub fn wcoj_dispatch_stream_or_init(&self) -> Option<StreamId> {
3352 if let Some(s) = self.wcoj_dispatch_stream.get() {
3353 return Some(*s);
3354 }
3355 let runtime = self.provider.memory().runtime()?;
3356 let stream = runtime.stream_pool().acquire().ok()?;
3357 let _ = self.wcoj_dispatch_stream.set(stream);
3358 self.wcoj_dispatch_stream.get().copied()
3359 }
3360}
3361
3362impl Executor {
3375 pub fn wcoj_clique5_dispatch_count(&self) -> u64 {
3380 self.wcoj_clique5_dispatch_count
3381 }
3382
3383 pub fn wcoj_clique6_dispatch_count(&self) -> u64 {
3387 self.wcoj_clique6_dispatch_count
3388 }
3389
3390 pub fn wcoj_clique7_dispatch_count(&self) -> u64 {
3394 self.wcoj_clique7_dispatch_count
3395 }
3396
3397 pub fn wcoj_clique8_dispatch_count(&self) -> u64 {
3401 self.wcoj_clique8_dispatch_count
3402 }
3403
3404 pub fn kclique_histogram_refresh_count(&self) -> u64 {
3407 self.kclique_histogram_refresh_count
3408 }
3409
3410 pub fn kclique_histogram_refresh_nanos(&self) -> u128 {
3413 self.kclique_histogram_refresh_nanos
3414 }
3415
3416 pub(super) fn try_dispatch_wcoj_clique5(
3419 &mut self,
3420 rule: &CompiledRule,
3421 ) -> Result<Option<CudaBuffer>> {
3422 self.try_dispatch_wcoj_clique5_on_body(&rule.body)
3423 }
3424
3425 pub(super) fn try_dispatch_wcoj_clique6(
3427 &mut self,
3428 rule: &CompiledRule,
3429 ) -> Result<Option<CudaBuffer>> {
3430 self.try_dispatch_wcoj_clique6_on_body(&rule.body)
3431 }
3432
3433 pub(super) fn try_dispatch_wcoj_clique7(
3435 &mut self,
3436 rule: &CompiledRule,
3437 ) -> Result<Option<CudaBuffer>> {
3438 self.try_dispatch_wcoj_clique7_on_body(&rule.body)
3439 }
3440
3441 pub(super) fn try_dispatch_wcoj_clique8(
3443 &mut self,
3444 rule: &CompiledRule,
3445 ) -> Result<Option<CudaBuffer>> {
3446 self.try_dispatch_wcoj_clique8_on_body(&rule.body)
3447 }
3448
3449 pub(super) fn try_dispatch_wcoj_clique5_on_body(
3451 &mut self,
3452 body: &RirNode,
3453 ) -> Result<Option<CudaBuffer>> {
3454 self.try_dispatch_wcoj_clique_k_on_body(body, 5)
3455 }
3456
3457 pub(super) fn try_dispatch_wcoj_clique6_on_body(
3459 &mut self,
3460 body: &RirNode,
3461 ) -> Result<Option<CudaBuffer>> {
3462 self.try_dispatch_wcoj_clique_k_on_body(body, 6)
3463 }
3464
3465 pub(super) fn try_dispatch_wcoj_clique7_on_body(
3467 &mut self,
3468 body: &RirNode,
3469 ) -> Result<Option<CudaBuffer>> {
3470 self.try_dispatch_wcoj_clique_k_on_body(body, 7)
3471 }
3472
3473 pub(super) fn try_dispatch_wcoj_clique8_on_body(
3475 &mut self,
3476 body: &RirNode,
3477 ) -> Result<Option<CudaBuffer>> {
3478 self.try_dispatch_wcoj_clique_k_on_body(body, 8)
3479 }
3480
3481 fn try_dispatch_wcoj_clique_k_on_body(
3485 &mut self,
3486 body: &RirNode,
3487 k: usize,
3488 ) -> Result<Option<CudaBuffer>> {
3489 let expected_edges = k * (k - 1) / 2;
3490 let RirNode::MultiWayJoin {
3492 inputs,
3493 plan,
3494 var_order,
3495 ..
3496 } = body
3497 else {
3498 return Ok(None);
3499 };
3500 if matches!(plan, Some(MultiwayPlan::PlannedHashRoute { .. })) {
3501 return Ok(None);
3502 }
3503 if inputs.len() != expected_edges {
3504 return Ok(None);
3505 }
3506 let kclique = match var_order.as_ref().and_then(|order| order.kclique.as_ref()) {
3507 Some(plan) if usize::from(plan.k) == k => plan,
3508 _ => return Ok(None),
3509 };
3510 let mut rel_ids: Vec<RelId> = Vec::with_capacity(expected_edges);
3512 for input in inputs {
3513 let RirNode::Scan { rel } = input else {
3514 return Ok(None);
3515 };
3516 rel_ids.push(*rel);
3517 }
3518 let mut raw_bufs: Vec<&CudaBuffer> = Vec::with_capacity(expected_edges);
3520 for rid in &rel_ids {
3521 let name = match self.rel_names.get(rid) {
3522 Some(n) => n.clone(),
3523 None => return Ok(None),
3524 };
3525 match self.store.get(&name) {
3526 Some(b) => raw_bufs.push(b),
3527 None => return Ok(None),
3528 }
3529 }
3530 let launch_stream = match self.wcoj_dispatch_stream_or_init() {
3532 Some(s) => s,
3533 None => return Ok(None),
3534 };
3535 let first_ty = match raw_bufs[0].schema().column_type(0) {
3539 Some(t) => t,
3540 None => return Ok(None),
3541 };
3542 let is_u64 = matches!(first_ty, xlog_core::ScalarType::U64);
3543 let is_4byte = matches!(
3544 first_ty,
3545 xlog_core::ScalarType::U32 | xlog_core::ScalarType::Symbol
3546 );
3547 if !is_u64 && !is_4byte {
3548 return Ok(None);
3549 }
3550 let Some(plan_params) = kclique_dispatch_params(kclique, k) else {
3551 return Ok(None);
3552 };
3553 let head_schema = match build_kclique_head_schema(&raw_bufs, k) {
3554 Some(schema) => schema,
3555 None => return Ok(None),
3556 };
3557 let output_perm = match kclique_output_perm(kclique, k) {
3558 Some(perm) => perm,
3559 None => return Ok(None),
3560 };
3561 let laid_out = match self.orient_and_layout_kclique_edges(
3567 &raw_bufs,
3568 &plan_params,
3569 is_u64,
3570 launch_stream,
3571 ) {
3572 Ok(bufs) => bufs,
3573 Err(err) => {
3574 return wcoj_decline_on_error(
3575 &mut self.wcoj_error_decline_count,
3576 "k-clique-layout",
3577 err,
3578 )
3579 }
3580 };
3581 let edge_refs: Vec<&CudaBuffer> = laid_out.iter().collect();
3584 let result = match (k, is_u64) {
3586 (5, false) => {
3587 let arr: &[&CudaBuffer; 10] = match edge_refs.as_slice().try_into() {
3588 Ok(a) => a,
3589 Err(_) => return Ok(None),
3590 };
3591 self.provider.wcoj_clique5_u32_recorded_planned(
3592 arr,
3593 plan_params.leader_edge_idx,
3594 &plan_params.edge_order,
3595 &plan_params.iteration_order,
3596 launch_stream,
3597 )
3598 }
3599 (5, true) => {
3600 let arr: &[&CudaBuffer; 10] = match edge_refs.as_slice().try_into() {
3601 Ok(a) => a,
3602 Err(_) => return Ok(None),
3603 };
3604 self.provider.wcoj_clique5_u64_recorded_planned(
3605 arr,
3606 plan_params.leader_edge_idx,
3607 &plan_params.edge_order,
3608 &plan_params.iteration_order,
3609 launch_stream,
3610 )
3611 }
3612 (6, false) => {
3613 let arr: &[&CudaBuffer; 15] = match edge_refs.as_slice().try_into() {
3614 Ok(a) => a,
3615 Err(_) => return Ok(None),
3616 };
3617 self.provider.wcoj_clique6_u32_recorded_planned(
3618 arr,
3619 plan_params.leader_edge_idx,
3620 &plan_params.edge_order,
3621 &plan_params.iteration_order,
3622 launch_stream,
3623 )
3624 }
3625 (6, true) => {
3626 let arr: &[&CudaBuffer; 15] = match edge_refs.as_slice().try_into() {
3627 Ok(a) => a,
3628 Err(_) => return Ok(None),
3629 };
3630 self.provider.wcoj_clique6_u64_recorded_planned(
3631 arr,
3632 plan_params.leader_edge_idx,
3633 &plan_params.edge_order,
3634 &plan_params.iteration_order,
3635 launch_stream,
3636 )
3637 }
3638 (7, false) => {
3639 let arr: &[&CudaBuffer; 21] = match edge_refs.as_slice().try_into() {
3640 Ok(a) => a,
3641 Err(_) => return Ok(None),
3642 };
3643 self.provider.wcoj_clique7_u32_recorded_planned(
3644 arr,
3645 plan_params.leader_edge_idx,
3646 &plan_params.edge_order,
3647 &plan_params.iteration_order,
3648 launch_stream,
3649 )
3650 }
3651 (7, true) => {
3652 let arr: &[&CudaBuffer; 21] = match edge_refs.as_slice().try_into() {
3653 Ok(a) => a,
3654 Err(_) => return Ok(None),
3655 };
3656 self.provider.wcoj_clique7_u64_recorded_planned(
3657 arr,
3658 plan_params.leader_edge_idx,
3659 &plan_params.edge_order,
3660 &plan_params.iteration_order,
3661 launch_stream,
3662 )
3663 }
3664 (8, false) => {
3665 let arr: &[&CudaBuffer; 28] = match edge_refs.as_slice().try_into() {
3666 Ok(a) => a,
3667 Err(_) => return Ok(None),
3668 };
3669 self.provider.wcoj_clique8_u32_recorded_planned(
3670 arr,
3671 plan_params.leader_edge_idx,
3672 &plan_params.edge_order,
3673 &plan_params.iteration_order,
3674 launch_stream,
3675 )
3676 }
3677 (8, true) => {
3678 let arr: &[&CudaBuffer; 28] = match edge_refs.as_slice().try_into() {
3679 Ok(a) => a,
3680 Err(_) => return Ok(None),
3681 };
3682 self.provider.wcoj_clique8_u64_recorded_planned(
3683 arr,
3684 plan_params.leader_edge_idx,
3685 &plan_params.edge_order,
3686 &plan_params.iteration_order,
3687 launch_stream,
3688 )
3689 }
3690 _ => return Ok(None),
3691 };
3692 match result {
3695 Ok(buf) => {
3696 let buf = if output_perm.iter().copied().eq(0..output_perm.len()) {
3697 buf
3698 } else {
3699 self.provider.wcoj_project_output_columns_recorded(
3700 &buf,
3701 &output_perm,
3702 head_schema,
3703 launch_stream,
3704 )?
3705 };
3706 match k {
3707 5 => self.wcoj_clique5_dispatch_count += 1,
3708 6 => self.wcoj_clique6_dispatch_count += 1,
3709 7 => self.wcoj_clique7_dispatch_count += 1,
3710 8 => self.wcoj_clique8_dispatch_count += 1,
3711 _ => {}
3712 }
3713 Ok(Some(buf))
3714 }
3715 Err(err) => wcoj_decline_on_error(&mut self.wcoj_error_decline_count, "k-clique", err),
3716 }
3717 }
3718
3719 fn orient_and_layout_kclique_edges(
3728 &self,
3729 raw_bufs: &[&CudaBuffer],
3730 plan_params: &KCliqueDispatchParams,
3731 is_u64: bool,
3732 launch_stream: StreamId,
3733 ) -> Result<Vec<CudaBuffer>> {
3734 let mut laid_out: Vec<CudaBuffer> = Vec::with_capacity(plan_params.edge_permutation.len());
3735 for (slot, &input_idx) in plan_params.edge_permutation.iter().enumerate() {
3736 let src = raw_bufs[input_idx];
3737 let swapped = if plan_params.swap_slots.contains(&slot) {
3738 Some(
3739 self.provider
3740 .wcoj_project_2col_swap_recorded(src, launch_stream)?,
3741 )
3742 } else {
3743 None
3744 };
3745 let oriented = swapped.as_ref().unwrap_or(src);
3746 let res = if plan_params.required_sort_slots.contains(&slot) {
3747 if is_u64 {
3748 self.provider
3749 .wcoj_layout_sort_u64_recorded(oriented, launch_stream)
3750 } else {
3751 self.provider
3752 .wcoj_layout_sort_u32_recorded(oriented, launch_stream)
3753 }
3754 } else if is_u64 {
3755 self.provider
3756 .wcoj_layout_u64_recorded(oriented, launch_stream)
3757 } else {
3758 self.provider
3759 .wcoj_layout_u32_recorded(oriented, launch_stream)
3760 };
3761 laid_out.push(res?);
3762 }
3763 Ok(laid_out)
3764 }
3765
3766 fn try_dispatch_wcoj_groupby_root_count_clique(
3783 &mut self,
3784 multiway: &RirNode,
3785 group_cols: &[ProjectExpr],
3786 ) -> Result<Option<CudaBuffer>> {
3787 let RirNode::MultiWayJoin {
3788 inputs,
3789 plan,
3790 var_order,
3791 ..
3792 } = multiway
3793 else {
3794 return Ok(None);
3795 };
3796 if matches!(plan, Some(MultiwayPlan::PlannedHashRoute { .. })) {
3797 return Ok(None);
3798 }
3799 let kclique = match var_order.as_ref().and_then(|order| order.kclique.as_ref()) {
3800 Some(plan) => plan,
3801 None => return Ok(None),
3802 };
3803 let k = usize::from(kclique.k);
3804 if !matches!(k, 5 | 6) {
3805 return Ok(None);
3806 }
3807 let expected_edges = k * (k - 1) / 2;
3808 if inputs.len() != expected_edges {
3809 return Ok(None);
3810 }
3811 let Some(ProjectExpr::Column(root_var)) = group_cols.first() else {
3813 return Ok(None);
3814 };
3815 let Some(positions) = live_kclique_variable_positions(kclique, k) else {
3816 return Ok(None);
3817 };
3818 if *root_var >= k || positions[*root_var] != 0 {
3819 return Ok(None);
3820 }
3821 let mut rel_ids: Vec<RelId> = Vec::with_capacity(expected_edges);
3823 for input in inputs {
3824 let RirNode::Scan { rel } = input else {
3825 return Ok(None);
3826 };
3827 rel_ids.push(*rel);
3828 }
3829 let mut raw_bufs: Vec<&CudaBuffer> = Vec::with_capacity(expected_edges);
3830 for rid in &rel_ids {
3831 let name = match self.rel_names.get(rid) {
3832 Some(n) => n.clone(),
3833 None => return Ok(None),
3834 };
3835 match self.store.get(&name) {
3836 Some(b) => raw_bufs.push(b),
3837 None => return Ok(None),
3838 }
3839 }
3840 for buf in &raw_bufs {
3841 if classify_two_col_wcoj_width(buf) != Some(WcojKeyWidth::FourByte) {
3842 return Ok(None);
3843 }
3844 }
3845 if self.provider.memory().runtime().is_none() {
3846 return Ok(None);
3847 }
3848 let Some(launch_stream) = self.wcoj_dispatch_stream_or_init() else {
3849 return Ok(None);
3850 };
3851 let Some(plan_params) = kclique_dispatch_params(kclique, k) else {
3852 return Ok(None);
3853 };
3854 let laid_out = match self.orient_and_layout_kclique_edges(
3855 &raw_bufs,
3856 &plan_params,
3857 false,
3858 launch_stream,
3859 ) {
3860 Ok(bufs) => bufs,
3861 Err(err) => {
3862 return wcoj_decline_on_error(
3863 &mut self.wcoj_error_decline_count,
3864 "groupby-fusion-clique-layout",
3865 err,
3866 )
3867 }
3868 };
3869 let edge_refs: Vec<&CudaBuffer> = laid_out.iter().collect();
3870 let result = match k {
3871 5 => {
3872 let arr: &[&CudaBuffer; 10] = match edge_refs.as_slice().try_into() {
3873 Ok(a) => a,
3874 Err(_) => return Ok(None),
3875 };
3876 self.provider
3877 .wcoj_clique5_groupby_root_count_u32_recorded_planned(
3878 arr,
3879 plan_params.leader_edge_idx,
3880 &plan_params.edge_order,
3881 &plan_params.iteration_order,
3882 launch_stream,
3883 )
3884 }
3885 _ => {
3886 let arr: &[&CudaBuffer; 15] = match edge_refs.as_slice().try_into() {
3887 Ok(a) => a,
3888 Err(_) => return Ok(None),
3889 };
3890 self.provider
3891 .wcoj_clique6_groupby_root_count_u32_recorded_planned(
3892 arr,
3893 plan_params.leader_edge_idx,
3894 &plan_params.edge_order,
3895 &plan_params.iteration_order,
3896 launch_stream,
3897 )
3898 }
3899 };
3900 match result {
3901 Ok(buf) => {
3902 self.wcoj_groupby_fusion_dispatch_count += 1;
3903 Ok(Some(buf))
3904 }
3905 Err(err) => wcoj_decline_on_error(
3906 &mut self.wcoj_error_decline_count,
3907 "groupby-fusion-clique",
3908 err,
3909 ),
3910 }
3911 }
3912}
3913
3914#[derive(Debug)]
3915struct KCliqueDispatchParams {
3916 edge_permutation: Vec<usize>,
3917 edge_order: Vec<u8>,
3918 iteration_order: Vec<u8>,
3919 leader_edge_idx: u32,
3920 swap_slots: HashSet<usize>,
3921 required_sort_slots: HashSet<usize>,
3922}
3923
3924fn kclique_dispatch_params(plan: &KCliqueVariableOrder, k: usize) -> Option<KCliqueDispatchParams> {
3925 let expected_edges = k * (k - 1) / 2;
3926 let edge_permutation = live_kclique_edge_permutation(plan, expected_edges)?;
3927 let positions = live_kclique_variable_positions(plan, k)?;
3928 let mut edge_order = vec![u8::MAX; expected_edges];
3929
3930 for (slot, &edge_idx) in edge_permutation.iter().enumerate() {
3931 let (left, right) = clique_edge_pair(edge_idx, k)?;
3932 let left_pos = positions[left];
3933 let right_pos = positions[right];
3934 let logical_edge =
3935 clique_edge_idx_runtime(left_pos.min(right_pos), left_pos.max(right_pos), k)?;
3936 edge_order[logical_edge] = u8::try_from(slot).ok()?;
3937 }
3938 if edge_order.contains(&u8::MAX) {
3939 return None;
3940 }
3941 let leader_edge_idx = u32::from(edge_order[clique_edge_idx_runtime(0, 1, k)?]);
3942 let iteration_order: Vec<u8> = (0..k)
3943 .map(|idx| u8::try_from(idx).ok())
3944 .collect::<Option<_>>()?;
3945
3946 let swap_slots: HashSet<usize> = plan
3947 .column_swaps
3948 .iter()
3949 .filter(|swap| swap.swap_cols)
3950 .map(|swap| usize::from(swap.edge_slot))
3951 .collect();
3952 if swap_slots.iter().any(|slot| *slot >= expected_edges) {
3953 return None;
3954 }
3955 let required_sort_slots: HashSet<usize> = plan
3956 .sorted_layout_requirements
3957 .edge_slots
3958 .iter()
3959 .copied()
3960 .map(usize::from)
3961 .collect();
3962 if required_sort_slots
3963 .iter()
3964 .any(|slot| *slot >= expected_edges)
3965 {
3966 return None;
3967 }
3968
3969 Some(KCliqueDispatchParams {
3970 edge_permutation,
3971 edge_order,
3972 iteration_order,
3973 leader_edge_idx,
3974 swap_slots,
3975 required_sort_slots,
3976 })
3977}
3978
3979fn live_kclique_edge_permutation(
3980 plan: &KCliqueVariableOrder,
3981 expected_edges: usize,
3982) -> Option<Vec<usize>> {
3983 let values: Vec<usize> = plan
3984 .edge_permutation
3985 .iter()
3986 .copied()
3987 .take_while(|value| *value != u8::MAX)
3988 .map(usize::from)
3989 .collect();
3990 if values.len() != expected_edges {
3991 return None;
3992 }
3993 let mut seen = vec![false; expected_edges];
3994 for &value in &values {
3995 if value >= expected_edges || seen[value] {
3996 return None;
3997 }
3998 seen[value] = true;
3999 }
4000 Some(values)
4001}
4002
4003fn live_kclique_variable_positions(plan: &KCliqueVariableOrder, k: usize) -> Option<Vec<usize>> {
4004 let mut positions = Vec::with_capacity(k);
4005 let mut seen = vec![false; k];
4006 for original_var in 0..k {
4007 let pos = usize::from(*plan.variable_positions.get(original_var)?);
4008 if pos >= k || seen[pos] {
4009 return None;
4010 }
4011 seen[pos] = true;
4012 positions.push(pos);
4013 }
4014 Some(positions)
4015}
4016
4017fn clique_edge_idx_runtime(i: usize, j: usize, k: usize) -> Option<usize> {
4018 if !(i < j && j < k) {
4019 return None;
4020 }
4021 Some(i * (k - 1) - i.saturating_sub(1) * i / 2 + (j - i - 1))
4022}
4023
4024fn clique_edge_pair(edge_idx: usize, k: usize) -> Option<(usize, usize)> {
4025 let mut idx = 0usize;
4026 for i in 0..k {
4027 for j in (i + 1)..k {
4028 if idx == edge_idx {
4029 return Some((i, j));
4030 }
4031 idx += 1;
4032 }
4033 }
4034 None
4035}
4036
4037fn build_kclique_head_schema(raw_bufs: &[&CudaBuffer], k: usize) -> Option<Schema> {
4038 let mut columns = Vec::with_capacity(k);
4039 for variable in 0..k {
4040 let (edge_idx, col_idx) = if variable == 0 {
4041 (clique_edge_idx_runtime(0, 1, k)?, 0)
4042 } else {
4043 (clique_edge_idx_runtime(0, variable, k)?, 1)
4044 };
4045 let ty = raw_bufs.get(edge_idx)?.schema().column_type(col_idx)?;
4046 columns.push((format!("col{}", variable), ty));
4047 }
4048 Some(Schema::new(columns))
4049}
4050
4051fn kclique_output_perm(plan: &KCliqueVariableOrder, k: usize) -> Option<Vec<usize>> {
4052 let positions = live_kclique_variable_positions(plan, k)?;
4053 Some(positions)
4054}
4055
4056#[cfg(test)]
4057mod tests {
4058 use std::sync::{Mutex, OnceLock};
4059
4060 use super::{
4061 chain_dispatch_enabled, match_chain_join, match_multiway_triangle,
4062 record_chain_fallback_equivalents, wcoj_adaptive_enabled, wcoj_gate_enabled,
4063 ENV_USE_WCOJ_TRIANGLE_U32, ENV_WCOJ_CHAIN_ENABLE,
4064 };
4065 use xlog_core::RelId;
4066 use xlog_ir::rir::ProjectExpr;
4067 use xlog_ir::RirNode;
4068
4069 fn canonical_multiway() -> RirNode {
4070 RirNode::MultiWayJoin {
4071 inputs: vec![
4072 RirNode::Scan { rel: RelId(1) },
4073 RirNode::Scan { rel: RelId(2) },
4074 RirNode::Scan { rel: RelId(3) },
4075 ],
4076 slot_vars: vec![
4077 vec![Some(0u32), Some(1)],
4078 vec![Some(1u32), Some(2)],
4079 vec![Some(0u32), Some(2)],
4080 ],
4081 output_columns: vec![
4082 ProjectExpr::Column(0),
4083 ProjectExpr::Column(1),
4084 ProjectExpr::Column(3),
4085 ],
4086 fallback: Box::new(RirNode::Unit),
4087 plan: None,
4088 var_order: None,
4089 }
4090 }
4091
4092 fn canonical_chain_join() -> RirNode {
4093 RirNode::ChainJoin {
4094 left: Box::new(RirNode::Scan { rel: RelId(1) }),
4095 right: Box::new(RirNode::Scan { rel: RelId(2) }),
4096 left_key: 1,
4097 right_key: 0,
4098 output_columns: vec![ProjectExpr::Column(0), ProjectExpr::Column(3)],
4099 fallback: Box::new(RirNode::Unit),
4100 }
4101 }
4102
4103 #[test]
4104 fn match_chain_returns_two_rels_and_keys() {
4105 let node = canonical_chain_join();
4106 let m = match_chain_join(&node).expect("must match canonical chain");
4107 assert_eq!(m.rel_left, RelId(1));
4108 assert_eq!(m.rel_right, RelId(2));
4109 assert_eq!(m.left_key, 1);
4110 assert_eq!(m.right_key, 0);
4111 assert_eq!(
4112 m.output_columns,
4113 vec![ProjectExpr::Column(0), ProjectExpr::Column(3)]
4114 );
4115 }
4116
4117 #[test]
4118 fn match_chain_rejects_non_scan_inputs() {
4119 let mut node = canonical_chain_join();
4120 if let RirNode::ChainJoin { left, .. } = &mut node {
4121 **left = RirNode::Unit;
4122 }
4123 assert!(match_chain_join(&node).is_none());
4124 }
4125
4126 #[test]
4127 fn match_chain_rejects_multiway_triangle() {
4128 let node = canonical_multiway();
4129 assert!(match_chain_join(&node).is_none());
4130 }
4131
4132 fn chain_with_scan_filter_fallback() -> RirNode {
4133 let mut node = canonical_chain_join();
4134 let RirNode::ChainJoin { fallback, .. } = &mut node else {
4135 unreachable!("canonical chain shape")
4136 };
4137 **fallback = RirNode::Union {
4138 inputs: vec![
4139 RirNode::Filter {
4140 input: Box::new(RirNode::Scan { rel: RelId(1) }),
4141 predicate: xlog_ir::Expr::And(Vec::new()),
4142 },
4143 RirNode::Project {
4144 input: Box::new(RirNode::Filter {
4145 input: Box::new(RirNode::Scan { rel: RelId(2) }),
4146 predicate: xlog_ir::Expr::And(Vec::new()),
4147 }),
4148 columns: vec![ProjectExpr::Column(0)],
4149 },
4150 ],
4151 };
4152 node
4153 }
4154
4155 #[test]
4156 fn matched_chain_success_records_embedded_fallback_equivalents() {
4157 let node = chain_with_scan_filter_fallback();
4158 let mut scans = 7;
4159 let mut filters = 11;
4160
4161 record_chain_fallback_equivalents(&node, true, &mut scans, &mut filters);
4162
4163 assert_eq!(scans, 9);
4164 assert_eq!(filters, 13);
4165 }
4166
4167 #[test]
4168 fn matched_chain_decline_records_no_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, false, &mut scans, &mut filters);
4174
4175 assert_eq!(scans, 7);
4176 assert_eq!(filters, 11);
4177 }
4178
4179 #[test]
4180 fn chain_dispatch_env_defaults_on_and_can_disable() {
4181 static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
4182 let _guard = ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
4183 let old = std::env::var(ENV_WCOJ_CHAIN_ENABLE).ok();
4184 unsafe {
4187 std::env::remove_var(ENV_WCOJ_CHAIN_ENABLE);
4188 }
4189 assert!(chain_dispatch_enabled());
4190 unsafe {
4191 std::env::set_var(ENV_WCOJ_CHAIN_ENABLE, "0");
4192 }
4193 assert!(!chain_dispatch_enabled());
4194 unsafe {
4195 std::env::set_var(ENV_WCOJ_CHAIN_ENABLE, "false");
4196 }
4197 assert!(!chain_dispatch_enabled());
4198 unsafe {
4199 std::env::set_var(ENV_WCOJ_CHAIN_ENABLE, "1");
4200 }
4201 assert!(chain_dispatch_enabled());
4202 unsafe {
4203 match old {
4204 Some(v) => std::env::set_var(ENV_WCOJ_CHAIN_ENABLE, v),
4205 None => std::env::remove_var(ENV_WCOJ_CHAIN_ENABLE),
4206 }
4207 }
4208 }
4209
4210 #[test]
4211 fn match_canonical_returns_three_rels() {
4212 let node = canonical_multiway();
4213 let m = match_multiway_triangle(&node).expect("must match canonical triangle");
4214 assert_eq!(m.rel_xy, RelId(1));
4215 assert_eq!(m.rel_yz, RelId(2));
4216 assert_eq!(m.rel_xz, RelId(3));
4217 }
4218
4219 #[test]
4220 fn match_rejects_non_multiway_body() {
4221 let node = RirNode::Scan { rel: RelId(1) };
4222 assert!(match_multiway_triangle(&node).is_none());
4223 }
4224
4225 #[test]
4226 fn match_rejects_rotated_output_columns() {
4227 let mut node = canonical_multiway();
4228 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4229 *output_columns = vec![
4230 ProjectExpr::Column(1),
4231 ProjectExpr::Column(0),
4232 ProjectExpr::Column(3),
4233 ];
4234 }
4235 assert!(match_multiway_triangle(&node).is_none());
4236 }
4237
4238 #[test]
4243 fn match_accepts_z_shared_triangle_output_columns() {
4244 let mut node = canonical_multiway();
4245 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4246 *output_columns = vec![
4247 ProjectExpr::Column(0),
4248 ProjectExpr::Column(2),
4249 ProjectExpr::Column(3),
4250 ];
4251 }
4252 let m = match_multiway_triangle(&node)
4253 .expect("matcher must accept the Z-shared output-column layout");
4254 assert_eq!(m.rel_xy, RelId(1));
4255 assert_eq!(m.rel_yz, RelId(2));
4256 assert_eq!(m.rel_xz, RelId(3));
4257 }
4258
4259 #[test]
4262 fn match_rejects_invalid_triangle_output_columns() {
4263 let mut node = canonical_multiway();
4264 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4265 *output_columns = vec![
4266 ProjectExpr::Column(0),
4267 ProjectExpr::Column(3),
4268 ProjectExpr::Column(3),
4269 ];
4270 }
4271 assert!(match_multiway_triangle(&node).is_none());
4272 }
4273
4274 #[test]
4275 fn match_rejects_arity_mismatched_output_columns() {
4276 let mut node = canonical_multiway();
4277 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4278 *output_columns = vec![ProjectExpr::Column(0), ProjectExpr::Column(1)];
4279 }
4280 assert!(match_multiway_triangle(&node).is_none());
4281 }
4282
4283 #[test]
4284 fn match_rejects_malformed_slot_vars() {
4285 let mut node = canonical_multiway();
4287 if let RirNode::MultiWayJoin { slot_vars, .. } = &mut node {
4288 *slot_vars = vec![
4289 vec![Some(0u32), Some(1)],
4290 vec![Some(1u32), Some(2)],
4291 vec![Some(0u32), Some(1)],
4292 ];
4293 }
4294 assert!(match_multiway_triangle(&node).is_none());
4295 }
4296
4297 #[test]
4298 fn match_rejects_repeated_var_in_slot() {
4299 let mut node = canonical_multiway();
4300 if let RirNode::MultiWayJoin { slot_vars, .. } = &mut node {
4301 *slot_vars = vec![
4303 vec![Some(0u32), Some(0)],
4304 vec![Some(1u32), Some(2)],
4305 vec![Some(0u32), Some(2)],
4306 ];
4307 }
4308 assert!(match_multiway_triangle(&node).is_none());
4309 }
4310
4311 #[test]
4312 fn match_rejects_non_scan_input() {
4313 let mut node = canonical_multiway();
4314 if let RirNode::MultiWayJoin { inputs, .. } = &mut node {
4315 inputs[0] = RirNode::Unit;
4316 }
4317 assert!(match_multiway_triangle(&node).is_none());
4318 }
4319
4320 #[test]
4321 fn match_rejects_input_arity_mismatch() {
4322 let mut node = canonical_multiway();
4323 if let RirNode::MultiWayJoin { inputs, .. } = &mut node {
4324 inputs.pop();
4325 }
4326 assert!(match_multiway_triangle(&node).is_none());
4327 }
4328
4329 fn env_lock() -> &'static Mutex<()> {
4330 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
4331 LOCK.get_or_init(|| Mutex::new(()))
4332 }
4333
4334 struct EnvSnapshot {
4335 force: Option<String>,
4336 }
4337
4338 impl EnvSnapshot {
4339 fn capture_and_clear() -> Self {
4340 let snapshot = Self {
4341 force: std::env::var(ENV_USE_WCOJ_TRIANGLE_U32).ok(),
4342 };
4343
4344 unsafe {
4347 std::env::remove_var(ENV_USE_WCOJ_TRIANGLE_U32);
4348 }
4349
4350 snapshot
4351 }
4352 }
4353
4354 impl Drop for EnvSnapshot {
4355 fn drop(&mut self) {
4356 unsafe {
4359 match self.force.take() {
4360 Some(v) => std::env::set_var(ENV_USE_WCOJ_TRIANGLE_U32, v),
4361 None => std::env::remove_var(ENV_USE_WCOJ_TRIANGLE_U32),
4362 }
4363 }
4364 }
4365 }
4366
4367 fn with_wcoj_env<R>(f: impl FnOnce() -> R) -> R {
4368 let _guard = env_lock().lock().expect("WCOJ env lock poisoned");
4369 let _snapshot = EnvSnapshot::capture_and_clear();
4370 f()
4371 }
4372
4373 fn set_env(name: &str, value: &str) {
4374 unsafe {
4377 std::env::set_var(name, value);
4378 }
4379 }
4380
4381 #[test]
4382 fn stats_gate_defaults_on_when_env_unset() {
4383 with_wcoj_env(|| {
4384 assert!(wcoj_adaptive_enabled(None));
4385 assert!(wcoj_adaptive_enabled(Some(true)));
4386 assert!(!wcoj_adaptive_enabled(Some(false)));
4387 });
4388 }
4389
4390 #[test]
4391 fn config_controls_stats_gate() {
4392 with_wcoj_env(|| {
4393 assert!(wcoj_adaptive_enabled(Some(true)));
4394 assert!(!wcoj_adaptive_enabled(Some(false)));
4395 });
4396 }
4397
4398 #[test]
4399 fn force_resolver_config_still_overrides_env() {
4400 with_wcoj_env(|| {
4401 set_env(ENV_USE_WCOJ_TRIANGLE_U32, "1");
4402 assert!(wcoj_gate_enabled(None));
4403 assert!(!wcoj_gate_enabled(Some(false)));
4404
4405 set_env(ENV_USE_WCOJ_TRIANGLE_U32, "0");
4406 assert!(!wcoj_gate_enabled(None));
4407 assert!(wcoj_gate_enabled(Some(true)));
4408 });
4409 }
4410
4411 #[test]
4416 fn error_decline_counts_and_falls_back_by_default() {
4417 with_wcoj_env(|| {
4418 let mut counter = 0u64;
4419 let err = xlog_core::XlogError::Kernel("synthetic layout failure".to_string());
4420 let out = super::wcoj_decline_on_error(&mut counter, "triangle", err)
4421 .expect("default mode must decline to the binary-join fallback, not error");
4422 assert!(out.is_none(), "decline must hand control to the fallback");
4423 assert_eq!(counter, 1, "every error decline must be counted");
4424 });
4425 }
4426
4427 #[test]
4428 fn error_decline_propagates_under_strict_env() {
4429 with_wcoj_env(|| {
4430 set_env(super::ENV_WCOJ_STRICT, "1");
4431 let mut counter = 0u64;
4432 let err = xlog_core::XlogError::Kernel("synthetic layout failure".to_string());
4433 let out = super::wcoj_decline_on_error(&mut counter, "triangle", err);
4434 unsafe {
4436 std::env::remove_var(super::ENV_WCOJ_STRICT);
4437 }
4438 match out {
4439 Err(err) => assert!(
4440 err.to_string().contains("synthetic layout failure"),
4441 "strict mode must surface the original error: {err}"
4442 ),
4443 Ok(_) => panic!("XLOG_WCOJ_STRICT=1 must propagate the pipeline error"),
4444 }
4445 assert_eq!(counter, 1, "strict mode still counts the decline");
4446 });
4447 }
4448
4449 use super::{
4454 match_multiway_4cycle, wcoj_4cycle_adaptive_enabled, wcoj_4cycle_disabled,
4455 wcoj_4cycle_gate_enabled, ENV_DISABLE_WCOJ_4CYCLE, ENV_USE_WCOJ_4CYCLE,
4456 ENV_USE_WCOJ_4CYCLE_ADAPTIVE,
4457 };
4458
4459 struct EnvSnapshot4Cycle {
4460 force: Option<String>,
4461 adaptive: Option<String>,
4462 disable: Option<String>,
4463 }
4464
4465 impl EnvSnapshot4Cycle {
4466 fn capture_and_clear() -> Self {
4467 let snap = Self {
4468 force: std::env::var(ENV_USE_WCOJ_4CYCLE).ok(),
4469 adaptive: std::env::var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE).ok(),
4470 disable: std::env::var(ENV_DISABLE_WCOJ_4CYCLE).ok(),
4471 };
4472 unsafe {
4474 std::env::remove_var(ENV_USE_WCOJ_4CYCLE);
4475 std::env::remove_var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE);
4476 std::env::remove_var(ENV_DISABLE_WCOJ_4CYCLE);
4477 }
4478 snap
4479 }
4480 }
4481
4482 impl Drop for EnvSnapshot4Cycle {
4483 fn drop(&mut self) {
4484 unsafe {
4486 match self.force.take() {
4487 Some(v) => std::env::set_var(ENV_USE_WCOJ_4CYCLE, v),
4488 None => std::env::remove_var(ENV_USE_WCOJ_4CYCLE),
4489 }
4490 match self.adaptive.take() {
4491 Some(v) => std::env::set_var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE, v),
4492 None => std::env::remove_var(ENV_USE_WCOJ_4CYCLE_ADAPTIVE),
4493 }
4494 match self.disable.take() {
4495 Some(v) => std::env::set_var(ENV_DISABLE_WCOJ_4CYCLE, v),
4496 None => std::env::remove_var(ENV_DISABLE_WCOJ_4CYCLE),
4497 }
4498 }
4499 }
4500 }
4501
4502 fn with_4cycle_env<R>(f: impl FnOnce() -> R) -> R {
4503 let _guard = env_lock().lock().expect("4-cycle env lock poisoned");
4504 let _snap = EnvSnapshot4Cycle::capture_and_clear();
4505 f()
4506 }
4507
4508 #[test]
4509 fn force_4cycle_resolver_defaults_off_when_env_unset() {
4510 with_4cycle_env(|| {
4511 assert!(!wcoj_4cycle_gate_enabled(None));
4512 assert!(wcoj_4cycle_gate_enabled(Some(true)));
4513 assert!(!wcoj_4cycle_gate_enabled(Some(false)));
4514 });
4515 }
4516
4517 #[test]
4518 fn force_4cycle_resolver_env_can_enable() {
4519 with_4cycle_env(|| {
4520 set_env(ENV_USE_WCOJ_4CYCLE, "1");
4521 assert!(wcoj_4cycle_gate_enabled(None));
4522 set_env(ENV_USE_WCOJ_4CYCLE, "true");
4523 assert!(wcoj_4cycle_gate_enabled(None));
4524 set_env(ENV_USE_WCOJ_4CYCLE, "0");
4525 assert!(!wcoj_4cycle_gate_enabled(None));
4526 });
4527 }
4528
4529 #[test]
4534 fn adaptive_4cycle_resolver_defaults_off_when_env_unset() {
4535 with_4cycle_env(|| {
4536 assert!(
4537 !wcoj_4cycle_adaptive_enabled(None),
4538 "4-cycle adaptive must be OPT-IN by default (unlike triangle's default-on)"
4539 );
4540 assert!(wcoj_4cycle_adaptive_enabled(Some(true)));
4541 assert!(!wcoj_4cycle_adaptive_enabled(Some(false)));
4542 });
4543 }
4544
4545 #[test]
4546 fn adaptive_4cycle_resolver_env_can_enable() {
4547 with_4cycle_env(|| {
4548 set_env(ENV_USE_WCOJ_4CYCLE_ADAPTIVE, "1");
4549 assert!(wcoj_4cycle_adaptive_enabled(None));
4550 set_env(ENV_USE_WCOJ_4CYCLE_ADAPTIVE, "0");
4551 assert!(!wcoj_4cycle_adaptive_enabled(None));
4552 set_env(ENV_USE_WCOJ_4CYCLE_ADAPTIVE, "true");
4553 assert!(wcoj_4cycle_adaptive_enabled(None));
4554 });
4555 }
4556
4557 #[test]
4558 fn kill_4cycle_resolver_honors_env_and_config() {
4559 with_4cycle_env(|| {
4560 assert!(!wcoj_4cycle_disabled(None));
4561 set_env(ENV_DISABLE_WCOJ_4CYCLE, "1");
4562 assert!(wcoj_4cycle_disabled(None));
4563 assert!(!wcoj_4cycle_disabled(Some(false)));
4564 set_env(ENV_DISABLE_WCOJ_4CYCLE, "0");
4565 assert!(wcoj_4cycle_disabled(Some(true)));
4566 });
4567 }
4568
4569 fn canonical_4cycle_multiway() -> RirNode {
4570 RirNode::MultiWayJoin {
4571 inputs: vec![
4572 RirNode::Scan { rel: RelId(1) },
4573 RirNode::Scan { rel: RelId(2) },
4574 RirNode::Scan { rel: RelId(3) },
4575 RirNode::Scan { rel: RelId(4) },
4576 ],
4577 slot_vars: vec![
4578 vec![Some(0u32), Some(1)],
4579 vec![Some(1u32), Some(2)],
4580 vec![Some(2u32), Some(3)],
4581 vec![Some(3u32), Some(0)],
4582 ],
4583 output_columns: vec![
4584 ProjectExpr::Column(0),
4585 ProjectExpr::Column(1),
4586 ProjectExpr::Column(3),
4587 ProjectExpr::Column(5),
4588 ],
4589 fallback: Box::new(RirNode::Unit),
4590 plan: None,
4591 var_order: None,
4592 }
4593 }
4594
4595 #[test]
4596 fn match_4cycle_canonical_returns_four_rels() {
4597 let node = canonical_4cycle_multiway();
4598 let m = match_multiway_4cycle(&node).expect("must match canonical 4-cycle");
4599 assert_eq!(m.rel_e1, RelId(1));
4600 assert_eq!(m.rel_e2, RelId(2));
4601 assert_eq!(m.rel_e3, RelId(3));
4602 assert_eq!(m.rel_e4, RelId(4));
4603 }
4604
4605 #[test]
4606 fn match_4cycle_rejects_non_multiway() {
4607 assert!(match_multiway_4cycle(&RirNode::Scan { rel: RelId(1) }).is_none());
4608 }
4609
4610 #[test]
4611 fn match_4cycle_rejects_triangle_shape() {
4612 let triangle = RirNode::MultiWayJoin {
4614 inputs: vec![
4615 RirNode::Scan { rel: RelId(1) },
4616 RirNode::Scan { rel: RelId(2) },
4617 RirNode::Scan { rel: RelId(3) },
4618 ],
4619 slot_vars: vec![
4620 vec![Some(0u32), Some(1)],
4621 vec![Some(1u32), Some(2)],
4622 vec![Some(0u32), Some(2)],
4623 ],
4624 output_columns: vec![
4625 ProjectExpr::Column(0),
4626 ProjectExpr::Column(1),
4627 ProjectExpr::Column(3),
4628 ],
4629 fallback: Box::new(RirNode::Unit),
4630 plan: None,
4631 var_order: None,
4632 };
4633 assert!(match_multiway_4cycle(&triangle).is_none());
4634 }
4635
4636 #[test]
4637 fn match_4cycle_rejects_rotated_output_columns() {
4638 let mut node = canonical_4cycle_multiway();
4639 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4640 output_columns.swap(0, 1);
4641 }
4642 assert!(match_multiway_4cycle(&node).is_none());
4643 }
4644
4645 #[test]
4650 fn match_4cycle_accepts_alt_grouping_output_columns() {
4651 let mut node = canonical_4cycle_multiway();
4652 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4653 *output_columns = vec![
4654 ProjectExpr::Column(5),
4655 ProjectExpr::Column(0),
4656 ProjectExpr::Column(1),
4657 ProjectExpr::Column(3),
4658 ];
4659 }
4660 let m = match_multiway_4cycle(&node)
4661 .expect("matcher must accept the Alt-grouping output-column layout");
4662 assert_eq!(m.rel_e1, RelId(1));
4666 assert_eq!(m.rel_e2, RelId(2));
4667 assert_eq!(m.rel_e3, RelId(3));
4668 assert_eq!(m.rel_e4, RelId(4));
4669 }
4670
4671 #[test]
4675 fn match_4cycle_rejects_invalid_output_columns() {
4676 let mut node = canonical_4cycle_multiway();
4677 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4678 *output_columns = vec![
4679 ProjectExpr::Column(1),
4680 ProjectExpr::Column(0),
4681 ProjectExpr::Column(3),
4682 ProjectExpr::Column(5),
4683 ];
4684 }
4685 assert!(match_multiway_4cycle(&node).is_none());
4686 }
4687
4688 #[test]
4689 fn match_4cycle_rejects_arity_mismatched_output_columns() {
4690 let mut node = canonical_4cycle_multiway();
4691 if let RirNode::MultiWayJoin { output_columns, .. } = &mut node {
4692 output_columns.pop();
4693 }
4694 assert!(match_multiway_4cycle(&node).is_none());
4695 }
4696
4697 #[test]
4698 fn match_4cycle_rejects_unclosed_cycle() {
4699 let mut node = canonical_4cycle_multiway();
4702 if let RirNode::MultiWayJoin { slot_vars, .. } = &mut node {
4703 slot_vars[3] = vec![Some(3), Some(99)];
4704 }
4705 assert!(match_multiway_4cycle(&node).is_none());
4706 }
4707
4708 #[test]
4709 fn match_4cycle_rejects_non_scan_input() {
4710 let mut node = canonical_4cycle_multiway();
4711 if let RirNode::MultiWayJoin { inputs, .. } = &mut node {
4712 inputs[0] = RirNode::Unit;
4713 }
4714 assert!(match_multiway_4cycle(&node).is_none());
4715 }
4716
4717 #[test]
4718 fn match_4cycle_rejects_input_arity_mismatch() {
4719 let mut node = canonical_4cycle_multiway();
4720 if let RirNode::MultiWayJoin { inputs, .. } = &mut node {
4721 inputs.push(RirNode::Scan { rel: RelId(5) });
4722 }
4723 assert!(match_multiway_4cycle(&node).is_none());
4724 }
4725}