use xlog_core::{CostModelKind, RelId, RuntimeConfig};
use xlog_cuda::device_runtime::StreamId;
use xlog_stats::StatsManager;
use super::wcoj_dispatch::WcojKeyWidth;
#[allow(dead_code)]
pub(super) struct WcojDispatchCtx<'a> {
pub stats: &'a StatsManager,
pub launch_stream: StreamId,
pub width: WcojKeyWidth,
pub slot_rels: &'a [RelId],
}
pub(super) enum FjOrderDecision {
KeepDefault,
Reorder(Vec<usize>),
Decline,
}
const FJ_PEAK_TOLERANCE_NUM: u128 = 6;
const FJ_PEAK_TOLERANCE_DEN: u128 = 5;
fn within_peak_tolerance(peak: u64, baseline: u64) -> bool {
(peak as u128) * FJ_PEAK_TOLERANCE_DEN <= (baseline as u128) * FJ_PEAK_TOLERANCE_NUM
}
const DEFAULT_JOIN_SELECTIVITY_DEN: u128 = 10;
pub(super) trait WcojCostModel: Send + Sync {
fn should_dispatch_triangle(&self, ctx: &WcojDispatchCtx) -> bool;
fn should_dispatch_4cycle(&self, ctx: &WcojDispatchCtx) -> bool;
fn factorized_loss_veto(&self, ctx: &WcojDispatchCtx) -> bool {
let _ = ctx;
false
}
fn plan_free_join_order(
&self,
ctx: &WcojDispatchCtx,
atom_vars: &[Vec<usize>],
cards: &[u64],
) -> FjOrderDecision {
let _ = (ctx, atom_vars, cards);
FjOrderDecision::KeepDefault
}
}
pub(super) const MIN_CARDINALITY_BINARY_INTERMEDIATE: u64 = 4_096;
pub(super) const LARGE_CARDINALITY_BINARY_INTERMEDIATE: u64 = 1_000_000;
#[derive(Default)]
pub(super) struct SkewClassifierCostModel;
impl WcojCostModel for SkewClassifierCostModel {
fn should_dispatch_triangle(&self, _ctx: &WcojDispatchCtx) -> bool {
false
}
fn should_dispatch_4cycle(&self, _ctx: &WcojDispatchCtx) -> bool {
false
}
}
pub(super) struct CardinalityAwareCostModel {
min_binary_intermediate: u64,
large_binary_intermediate: u64,
}
impl Default for CardinalityAwareCostModel {
fn default() -> Self {
Self {
min_binary_intermediate: MIN_CARDINALITY_BINARY_INTERMEDIATE,
large_binary_intermediate: LARGE_CARDINALITY_BINARY_INTERMEDIATE,
}
}
}
impl CardinalityAwareCostModel {
fn populated_cards(&self, ctx: &WcojDispatchCtx) -> Option<Vec<u64>> {
ctx.slot_rels
.iter()
.map(|r| {
ctx.stats
.get_relation_stats(*r)
.map(|s| s.cardinality)
.filter(|c| *c > 0)
})
.collect()
}
fn decide_from_cardinality(&self, binary_est: u64) -> bool {
binary_est >= self.large_binary_intermediate || binary_est >= self.min_binary_intermediate
}
}
impl WcojCostModel for CardinalityAwareCostModel {
fn should_dispatch_triangle(&self, ctx: &WcojDispatchCtx) -> bool {
debug_assert_eq!(
ctx.slot_rels.len(),
3,
"triangle ctx must carry exactly 3 slot relations"
);
if self.populated_cards(ctx).is_none() {
return false;
}
let binary_est =
ctx.stats
.estimate_join_cardinality(ctx.slot_rels[0], ctx.slot_rels[1], &[1], &[0]);
self.decide_from_cardinality(binary_est)
}
fn should_dispatch_4cycle(&self, ctx: &WcojDispatchCtx) -> bool {
debug_assert_eq!(
ctx.slot_rels.len(),
4,
"4-cycle ctx must carry exactly 4 slot relations"
);
if self.populated_cards(ctx).is_none() {
return false;
}
let binary_est =
ctx.stats
.estimate_join_cardinality(ctx.slot_rels[0], ctx.slot_rels[1], &[1], &[0]);
self.decide_from_cardinality(binary_est)
}
fn factorized_loss_veto(&self, ctx: &WcojDispatchCtx) -> bool {
let cards = match self.populated_cards(ctx) {
Some(c) => c,
None => return false,
};
cards.iter().copied().max().unwrap_or(0) < self.min_binary_intermediate
}
fn plan_free_join_order(
&self,
ctx: &WcojDispatchCtx,
atom_vars: &[Vec<usize>],
cards: &[u64],
) -> FjOrderDecision {
let n = atom_vars.len();
if n < 3 || cards.len() != n || ctx.slot_rels.len() != n {
return FjOrderDecision::KeepDefault;
}
let binary_peak = match best_greedy_peak(ctx, atom_vars, cards, false) {
Some((_, p)) => p,
None => return FjOrderDecision::KeepDefault,
};
let traversal: Vec<usize> = (0..n).collect();
match eval_order_peak(ctx, atom_vars, cards, &traversal, true) {
Some(default_peak) => {
if default_peak < self.min_binary_intermediate
|| within_peak_tolerance(default_peak, binary_peak)
{
return FjOrderDecision::KeepDefault;
}
}
None => {
if binary_peak < self.min_binary_intermediate {
return FjOrderDecision::KeepDefault;
}
}
}
match best_greedy_peak(ctx, atom_vars, cards, true) {
Some((order, fj_peak)) if within_peak_tolerance(fj_peak, binary_peak) => {
FjOrderDecision::Reorder(order)
}
_ => FjOrderDecision::Decline,
}
}
}
pub(super) fn build_wcoj_cost_model(config: &RuntimeConfig) -> Box<dyn WcojCostModel> {
match config.resolved_wcoj_cost_model() {
CostModelKind::SkewClassifier => Box::new(SkewClassifierCostModel),
CostModelKind::Cardinality => Box::new(CardinalityAwareCostModel::default()),
}
}
fn best_greedy_peak(
ctx: &WcojDispatchCtx,
atom_vars: &[Vec<usize>],
cards: &[u64],
constrained: bool,
) -> Option<(Vec<usize>, u64)> {
let n = atom_vars.len();
let mut best: Option<(Vec<usize>, u64)> = None;
for leader in 0..n {
if let Some((order, peak)) = greedy_from_leader(ctx, atom_vars, cards, constrained, leader)
{
if best.as_ref().map_or(true, |(_, bp)| peak < *bp) {
best = Some((order, peak));
}
}
}
best
}
fn greedy_from_leader(
ctx: &WcojDispatchCtx,
atom_vars: &[Vec<usize>],
cards: &[u64],
constrained: bool,
leader: usize,
) -> Option<(Vec<usize>, u64)> {
let n = atom_vars.len();
let mut order = Vec::with_capacity(n);
let mut placed = vec![false; n];
let mut bound: std::collections::HashSet<usize> = std::collections::HashSet::new();
order.push(leader);
placed[leader] = true;
bound.extend(atom_vars[leader].iter().copied());
let mut running = cards[leader].max(1);
let mut peak = running;
for _ in 1..n {
let mut choice: Option<(usize, u64)> = None;
for a in 0..n {
if placed[a] || !is_addable(&atom_vars[a], &bound, constrained) {
continue;
}
let new_running = estimate_join_onto_prefix(ctx, atom_vars, cards, &order, running, a);
if choice.as_ref().map_or(true, |(_, c)| new_running < *c) {
choice = Some((a, new_running));
}
}
let (a, new_running) = choice?;
order.push(a);
placed[a] = true;
bound.extend(atom_vars[a].iter().copied());
running = new_running;
peak = peak.max(running);
}
Some((order, peak))
}
fn eval_order_peak(
ctx: &WcojDispatchCtx,
atom_vars: &[Vec<usize>],
cards: &[u64],
order: &[usize],
constrained: bool,
) -> Option<u64> {
let mut bound: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut running = 0u64;
let mut peak = 0u64;
for (step, &a) in order.iter().enumerate() {
if step == 0 {
running = cards[a].max(1);
} else {
if !is_addable(&atom_vars[a], &bound, constrained) {
return None;
}
running = estimate_join_onto_prefix(ctx, atom_vars, cards, &order[..step], running, a);
}
bound.extend(atom_vars[a].iter().copied());
peak = peak.max(running);
}
Some(peak)
}
fn is_addable(
a_vars: &[usize],
bound: &std::collections::HashSet<usize>,
constrained: bool,
) -> bool {
if !a_vars.iter().any(|v| bound.contains(v)) {
return false; }
if constrained {
let split = a_vars.iter().take_while(|v| bound.contains(v)).count();
if split == 0 || a_vars[split..].iter().any(|v| bound.contains(v)) {
return false;
}
}
true
}
fn estimate_join_onto_prefix(
ctx: &WcojDispatchCtx,
atom_vars: &[Vec<usize>],
cards: &[u64],
placed: &[usize],
running: u64,
a: usize,
) -> u64 {
let mut best_new = u64::MAX;
for &p in placed {
let (p_keys, a_keys) = shared_keys(&atom_vars[p], &atom_vars[a]);
if p_keys.is_empty() {
continue;
}
let pairwise = pairwise_estimate(ctx, cards, p, a, &p_keys, &a_keys);
let fan_out = pairwise as f64 / (cards[p].max(1)) as f64;
let new_running = ((running as f64) * fan_out).ceil().max(1.0);
let new_running = if new_running >= u64::MAX as f64 {
u64::MAX
} else {
new_running as u64
};
best_new = best_new.min(new_running);
}
best_new.max(1)
}
fn shared_keys(p_vars: &[usize], a_vars: &[usize]) -> (Vec<usize>, Vec<usize>) {
let mut p_keys = Vec::new();
let mut a_keys = Vec::new();
for (pi, pv) in p_vars.iter().enumerate() {
if let Some(ai) = a_vars.iter().position(|av| av == pv) {
p_keys.push(pi);
a_keys.push(ai);
}
}
(p_keys, a_keys)
}
fn pairwise_estimate(
ctx: &WcojDispatchCtx,
cards: &[u64],
p: usize,
a: usize,
p_keys: &[usize],
a_keys: &[usize],
) -> u64 {
let stats_present = ctx
.stats
.get_relation_stats(ctx.slot_rels[p])
.map_or(false, |s| s.cardinality > 0)
&& ctx
.stats
.get_relation_stats(ctx.slot_rels[a])
.map_or(false, |s| s.cardinality > 0);
if stats_present {
ctx.stats
.estimate_join_cardinality(ctx.slot_rels[p], ctx.slot_rels[a], p_keys, a_keys)
} else {
let prod = (cards[p] as u128) * (cards[a] as u128) / DEFAULT_JOIN_SELECTIVITY_DEN;
prod.min(u64::MAX as u128).max(1) as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
fn triangle_ctx<'a>(stats: &'a StatsManager, slot_rels: &'a [RelId; 3]) -> WcojDispatchCtx<'a> {
WcojDispatchCtx {
stats,
launch_stream: StreamId::DEFAULT,
width: WcojKeyWidth::FourByte,
slot_rels,
}
}
fn cycle4_ctx<'a>(stats: &'a StatsManager, slot_rels: &'a [RelId; 4]) -> WcojDispatchCtx<'a> {
WcojDispatchCtx {
stats,
launch_stream: StreamId::DEFAULT,
width: WcojKeyWidth::FourByte,
slot_rels,
}
}
fn stats_with_cards(cards: &[u64]) -> StatsManager {
let mut stats = StatsManager::new();
for (i, c) in cards.iter().enumerate() {
let rid = RelId(i as u32);
stats.register_relation(rid);
stats.update_cardinality(rid, *c);
}
stats
}
fn cost_model_env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
struct CostModelEnvSnapshot(Option<String>);
impl CostModelEnvSnapshot {
fn capture_and_clear() -> Self {
let prior = std::env::var("XLOG_WCOJ_COST_MODEL").ok();
unsafe {
std::env::remove_var("XLOG_WCOJ_COST_MODEL");
}
Self(prior)
}
}
impl Drop for CostModelEnvSnapshot {
fn drop(&mut self) {
unsafe {
match self.0.take() {
Some(value) => std::env::set_var("XLOG_WCOJ_COST_MODEL", value),
None => std::env::remove_var("XLOG_WCOJ_COST_MODEL"),
}
}
}
}
fn with_cost_model_env<R>(f: impl FnOnce() -> R) -> R {
let _guard = cost_model_env_lock()
.lock()
.expect("cost-model env lock poisoned");
let _snapshot = CostModelEnvSnapshot::capture_and_clear();
f()
}
#[test]
fn cardinality_thresholds_pinned_in_default() {
let m = CardinalityAwareCostModel::default();
assert_eq!(
m.min_binary_intermediate,
MIN_CARDINALITY_BINARY_INTERMEDIATE
);
assert_eq!(
m.large_binary_intermediate,
LARGE_CARDINALITY_BINARY_INTERMEDIATE
);
}
#[test]
fn triangle_declines_when_any_slot_card_missing() {
let mut stats = StatsManager::new();
stats.register_relation(RelId(0));
stats.update_cardinality(RelId(0), 1000);
stats.register_relation(RelId(1));
stats.update_cardinality(RelId(1), 1000);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let m = CardinalityAwareCostModel::default();
assert!(!m.should_dispatch_triangle(&ctx));
}
#[test]
fn triangle_dispatches_when_binary_est_above_min_threshold() {
let stats = stats_with_cards(&[1_000, 1_000, 1_000]);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let m = CardinalityAwareCostModel::default();
assert!(m.should_dispatch_triangle(&ctx));
}
#[test]
fn triangle_declines_when_binary_est_below_min_threshold() {
let stats = stats_with_cards(&[50, 50, 50]);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let m = CardinalityAwareCostModel::default();
assert!(!m.should_dispatch_triangle(&ctx));
}
#[test]
fn cycle4_dispatches_when_binary_est_above_min_threshold() {
let stats = stats_with_cards(&[1_000, 1_000, 1_000, 1_000]);
let slots = [RelId(0), RelId(1), RelId(2), RelId(3)];
let ctx = cycle4_ctx(&stats, &slots);
let m = CardinalityAwareCostModel::default();
assert!(m.should_dispatch_4cycle(&ctx));
}
#[test]
fn factory_uses_cardinality_default_cost_model() {
with_cost_model_env(|| {
let stats = stats_with_cards(&[1_000, 1_000, 1_000]);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let model = build_wcoj_cost_model(&RuntimeConfig::default());
assert!(
model.should_dispatch_triangle(&ctx),
"bare default must use CardinalityAwareCostModel"
);
});
}
#[test]
fn factorized_veto_fires_when_all_inputs_small() {
let stats = stats_with_cards(&[50, 50, 50]);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let m = CardinalityAwareCostModel::default();
assert!(m.factorized_loss_veto(&ctx));
}
#[test]
fn factorized_veto_declines_when_any_input_large() {
let stats = stats_with_cards(&[50, MIN_CARDINALITY_BINARY_INTERMEDIATE, 50]);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let m = CardinalityAwareCostModel::default();
assert!(!m.factorized_loss_veto(&ctx));
}
#[test]
fn factorized_veto_fail_open_when_any_stat_missing() {
let mut stats = StatsManager::new();
stats.register_relation(RelId(0));
stats.update_cardinality(RelId(0), 50);
stats.register_relation(RelId(1));
stats.update_cardinality(RelId(1), 50);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let m = CardinalityAwareCostModel::default();
assert!(!m.factorized_loss_veto(&ctx));
}
#[test]
fn factorized_veto_general_arity_free_join_shape() {
let stats = stats_with_cards(&[10, 10, 10, 10, 10]);
let slots = [RelId(0), RelId(1), RelId(2), RelId(3), RelId(4)];
let ctx = WcojDispatchCtx {
stats: &stats,
launch_stream: StreamId::DEFAULT,
width: WcojKeyWidth::FourByte,
slot_rels: &slots,
};
let m = CardinalityAwareCostModel::default();
assert!(
m.factorized_loss_veto(&ctx),
"all-small >=3-input body must veto"
);
let stats_big = stats_with_cards(&[10, 10, 2_000_000, 10, 10]);
let ctx_big = WcojDispatchCtx {
stats: &stats_big,
launch_stream: StreamId::DEFAULT,
width: WcojKeyWidth::FourByte,
slot_rels: &slots,
};
assert!(
!m.factorized_loss_veto(&ctx_big),
"a large input must NOT veto (factorized may win)"
);
}
#[test]
fn factorized_veto_skew_classifier_never_vetoes() {
let stats = stats_with_cards(&[10, 10, 10]);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let m = SkewClassifierCostModel;
assert!(!m.factorized_loss_veto(&ctx));
}
#[test]
fn factory_honors_env_skew_opt_out() {
with_cost_model_env(|| {
unsafe {
std::env::set_var("XLOG_WCOJ_COST_MODEL", "skew");
}
let stats = stats_with_cards(&[1_000, 1_000, 1_000]);
let slots = [RelId(0), RelId(1), RelId(2)];
let ctx = triangle_ctx(&stats, &slots);
let model = build_wcoj_cost_model(&RuntimeConfig::default());
assert!(
!model.should_dispatch_triangle(&ctx),
"env skew opt-out must bypass cardinality dispatch"
);
});
}
fn nway_ctx<'a>(stats: &'a StatsManager, slot_rels: &'a [RelId]) -> WcojDispatchCtx<'a> {
WcojDispatchCtx {
stats,
launch_stream: StreamId::DEFAULT,
width: WcojKeyWidth::FourByte,
slot_rels,
}
}
fn slots(n: usize) -> Vec<RelId> {
(0..n as u32).map(RelId).collect()
}
#[test]
fn order_planner_declines_unreorderable_blowup_chain() {
let cards = [100u64, 100, 10_000, 1];
let stats = stats_with_cards(&cards); let rels = slots(4);
let ctx = nway_ctx(&stats, &rels);
let atom_vars = vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 4]];
let model = CardinalityAwareCostModel::default();
assert!(
matches!(
model.plan_free_join_order(&ctx, &atom_vars, &cards),
FjOrderDecision::Decline
),
"FJ-forced chain order blows up vs the tail-first binary plan → decline"
);
}
#[test]
fn order_planner_keeps_default_for_small_chain() {
let cards = [3u64, 3, 2];
let stats = stats_with_cards(&cards);
let rels = slots(3);
let ctx = nway_ctx(&stats, &rels);
let atom_vars = vec![vec![0, 1], vec![1, 2], vec![2, 3]];
let model = CardinalityAwareCostModel::default();
assert!(
matches!(
model.plan_free_join_order(&ctx, &atom_vars, &cards),
FjOrderDecision::KeepDefault
),
"small chain below the worthwhile threshold: keep default, FJ fires"
);
}
#[test]
fn order_planner_keeps_default_for_triangle() {
let cards = [100u64, 100, 100];
let stats = stats_with_cards(&cards);
let rels = slots(3);
let ctx = nway_ctx(&stats, &rels);
let atom_vars = vec![vec![0, 1], vec![1, 2], vec![0, 2]];
let model = CardinalityAwareCostModel::default();
assert!(
matches!(
model.plan_free_join_order(&ctx, &atom_vars, &cards),
FjOrderDecision::KeepDefault
),
"symmetric triangle: default order already competitive → keep it"
);
}
#[test]
fn order_planner_reorders_alternating_cycle() {
let cards = [10_000u64, 1, 10_000, 1];
let stats = stats_with_cards(&cards);
let rels = slots(4);
let ctx = nway_ctx(&stats, &rels);
let atom_vars = vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 0]];
let model = CardinalityAwareCostModel::default();
match model.plan_free_join_order(&ctx, &atom_vars, &cards) {
FjOrderDecision::Reorder(order) => {
let mut sorted = order.clone();
sorted.sort_unstable();
assert_eq!(sorted, vec![0, 1, 2, 3], "order must be a permutation");
assert_ne!(order, vec![0, 1, 2, 3], "must differ from traversal order");
assert!(
eval_order_peak(&ctx, &atom_vars, &cards, &order, true).is_some(),
"reordered plan must be prefix-key-joinable"
);
}
other => panic!("expected Reorder, got {:?}", DecisionDbg(&other)),
}
}
#[test]
fn order_planner_disabled_under_skew_model() {
let cards = [100u64, 100, 10_000, 1];
let stats = stats_with_cards(&cards);
let rels = slots(4);
let ctx = nway_ctx(&stats, &rels);
let atom_vars = vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 4]];
let model = SkewClassifierCostModel;
assert!(matches!(
model.plan_free_join_order(&ctx, &atom_vars, &cards),
FjOrderDecision::KeepDefault
));
}
struct DecisionDbg<'a>(&'a FjOrderDecision);
impl std::fmt::Debug for DecisionDbg<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
FjOrderDecision::KeepDefault => write!(f, "KeepDefault"),
FjOrderDecision::Reorder(o) => write!(f, "Reorder({o:?})"),
FjOrderDecision::Decline => write!(f, "Decline"),
}
}
}
}