Skip to main content

xlog_core/
config.rs

1//! Configuration types for XLOG runtime
2
3use crate::{resolve_bool, Result};
4
5/// GPU memory budget configuration.
6///
7/// Use [`MemoryBudget::default()`] or the builder methods ([`MemoryBudget::from_device_memory`],
8/// [`MemoryBudget::with_limit`], [`MemoryBudget::with_ooc`]) to construct.
9#[derive(Debug, Clone)]
10#[non_exhaustive]
11pub struct MemoryBudget {
12    /// Maximum device memory to use in bytes
13    pub device_bytes: u64,
14    /// Allow out-of-core execution (spill to host)
15    pub allow_ooc: bool,
16    /// Abort on memory budget exceeded (vs try to continue)
17    pub abort_on_exceed: bool,
18}
19
20impl Default for MemoryBudget {
21    fn default() -> Self {
22        Self {
23            device_bytes: 0, // Will be set from device query
24            allow_ooc: false,
25            abort_on_exceed: true,
26        }
27    }
28}
29
30impl MemoryBudget {
31    /// Create a budget using 80% of available device memory
32    pub fn from_device_memory(total_bytes: u64) -> Self {
33        Self {
34            device_bytes: (total_bytes as f64 * 0.8) as u64,
35            allow_ooc: false,
36            abort_on_exceed: true,
37        }
38    }
39
40    /// Create a budget with explicit byte limit
41    pub fn with_limit(device_bytes: u64) -> Self {
42        Self {
43            device_bytes,
44            allow_ooc: false,
45            abort_on_exceed: true,
46        }
47    }
48
49    /// Enable out-of-core mode
50    pub fn with_ooc(mut self) -> Self {
51        self.allow_ooc = true;
52        self
53    }
54}
55
56/// Runtime configuration for XLOG execution.
57///
58/// Use [`RuntimeConfig::default()`] and the builder methods to construct.
59#[derive(Debug, Clone)]
60#[non_exhaustive]
61pub struct RuntimeConfig {
62    /// Memory budget settings
63    pub memory: MemoryBudget,
64    /// Use deterministic execution (may be slower)
65    pub deterministic: bool,
66    /// Enable profiling (row counts, memory tracking)
67    pub profile: bool,
68    /// Maximum fixpoint iterations before abort
69    pub max_iterations: u32,
70    /// Opt-in: enforce the strict deterministic-Datalog D2H gate during
71    /// `Executor::execute_plan`. When `true`, any data-plane device-to-host
72    /// transfer (column downloads, internal `dtoh_sync_copy_into_tracked`
73    /// calls) returns `XlogError::Execution` and increments the provider's
74    /// `deterministic_d2h_violation_count`. Metadata reads via
75    /// `dtoh_scalar_untracked` remain allowed.
76    ///
77    /// Default `false`: the runtime still has known data-plane D2H paths in
78    /// relational set difference and binary-join count/materialize that are
79    /// scheduled for replacement before the default flips.
80    pub strict_deterministic_d2h: bool,
81    /// Override the env-driven WCOJ triangle dispatch gate
82    /// (`XLOG_USE_WCOJ_TRIANGLE_U32`). `None` (default) consults
83    /// the env var; `Some(true)` / `Some(false)` force the
84    /// runtime to ignore the env and use the explicit value.
85    /// Typed configuration is preferred for production callers that own a
86    /// runtime instance; `None` permits process-level environment configuration.
87    pub wcoj_triangle_dispatch: Option<bool>,
88    /// Override the stats-backed WCOJ triangle dispatch gate.
89    /// `None` uses the production default. `Some(true)` enables
90    /// the cardinality model; `Some(false)` disables this runtime's
91    /// default stats-backed decision.
92    pub wcoj_triangle_dispatch_adaptive: Option<bool>,
93    /// Runtime-local hard stop for WCOJ triangle dispatch.
94    /// `Some(true)` pins dispatch off across force and stats mode.
95    /// `Some(false)` leaves dispatch available for this runtime.
96    /// `None` consults `XLOG_DISABLE_WCOJ_TRIANGLE`; unset defaults to `false`.
97    pub wcoj_triangle_dispatch_disabled: Option<bool>,
98
99    /// Force gate for the 4-cycle WCOJ dispatch.
100    /// `Some(true)` / env `XLOG_USE_WCOJ_4CYCLE=1` forces every
101    /// recognized 4-cycle to dispatch the GPU kernel. `Some(false)` is
102    /// explicit force-off. `None` (default) consults the env.
103    pub wcoj_4cycle_dispatch: Option<bool>,
104    /// Adaptive opt-in for 4-cycle WCOJ. **Unlike triangle, 4-cycle
105    /// adaptive is opt-in by default**, not default-on: `None` resolves
106    /// to `false`. Default-on for 4-cycle requires separate benchmark evidence.
107    pub wcoj_4cycle_dispatch_adaptive: Option<bool>,
108    /// Kill switch for 4-cycle WCOJ. Same shape as triangle's kill switch:
109    /// beats force + adaptive.
110    pub wcoj_4cycle_dispatch_disabled: Option<bool>,
111    /// Selects the runtime WCOJ cost model.
112    /// `None` resolves by env/default precedence; see
113    /// [`RuntimeConfig::with_wcoj_cost_model`].
114    pub wcoj_cost_model: Option<CostModelKind>,
115    /// Runtime common subexpression elimination.
116    ///
117    /// `Some(true)` enables structural CSE for safe deterministic subplans.
118    /// `Some(false)` disables it. `None` consults `XLOG_CSE`; unset defaults
119    /// to disabled so existing runtime behavior is preserved unless the caller
120    /// opts in.
121    pub common_subexpression_elimination: Option<bool>,
122    /// Runtime adaptive re-optimization adoption gate.
123    ///
124    /// `Some(true)` allows an executor to compare a baseline plan against a
125    /// compiler-supplied candidate plan using runtime telemetry, adopt the
126    /// candidate only when deterministic mis-plan thresholds trigger, and roll
127    /// back on adverse candidates. `Some(false)` disables the adoption path.
128    /// `None` consults `XLOG_ADAPTIVE_REOPT`; unset defaults to disabled.
129    pub adaptive_reoptimization: Option<bool>,
130    /// Minimum mis-plan ratio required before the executor attempts to adopt a
131    /// candidate re-optimized plan. `None` consults
132    /// `XLOG_ADAPTIVE_REOPT_MIN_RATIO`; unset or invalid values default to 1.2.
133    pub adaptive_reoptimization_min_misplan_ratio: Option<f64>,
134    /// Persistent hash index manager gate.
135    ///
136    /// `Some(true)` enables persistent build-side hash index reuse in the
137    /// existing executor join-index cache. `Some(false)` disables the manager.
138    /// `None` consults `XLOG_PERSISTENT_HASH_INDEXES`; unset defaults to
139    /// enabled to preserve the existing adaptive-indexing behavior.
140    pub persistent_hash_indexes: Option<bool>,
141    /// Record background-build mode for the persistent hash index manager.
142    /// The current runtime keeps builds on the existing provider path but
143    /// records background build requests/completions so the transition to
144    /// recorded asynchronous builds has stable telemetry.
145    pub persistent_hash_index_background_build: Option<bool>,
146}
147
148/// Cost-model selector for WCOJ dispatch.
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum CostModelKind {
151    /// Legacy skew-classifier opt-out selector.
152    ///
153    /// The GPU skew-classifier surface is absent, so this selector is
154    /// implemented as a conservative opt-out from stats/cardinality dispatch.
155    SkewClassifier,
156    /// Stats/cardinality-backed dispatch selector.
157    Cardinality,
158}
159
160impl Default for RuntimeConfig {
161    fn default() -> Self {
162        Self {
163            memory: MemoryBudget::default(),
164            deterministic: true,
165            profile: false,
166            max_iterations: 1_000_000,
167            strict_deterministic_d2h: false,
168            wcoj_triangle_dispatch: None,
169            wcoj_triangle_dispatch_adaptive: None,
170            wcoj_triangle_dispatch_disabled: None,
171            wcoj_4cycle_dispatch: None,
172            wcoj_4cycle_dispatch_adaptive: None,
173            wcoj_4cycle_dispatch_disabled: None,
174            wcoj_cost_model: None,
175            common_subexpression_elimination: None,
176            adaptive_reoptimization: None,
177            adaptive_reoptimization_min_misplan_ratio: None,
178            persistent_hash_indexes: None,
179            persistent_hash_index_background_build: None,
180        }
181    }
182}
183
184impl RuntimeConfig {
185    /// Enable profiling
186    pub fn with_profiling(mut self) -> Self {
187        self.profile = true;
188        self
189    }
190
191    /// Set memory budget
192    pub fn with_memory(mut self, memory: MemoryBudget) -> Self {
193        self.memory = memory;
194        self
195    }
196
197    /// Enable the strict deterministic-Datalog D2H gate for this runtime.
198    pub fn with_strict_deterministic_d2h(mut self) -> Self {
199        self.strict_deterministic_d2h = true;
200        self
201    }
202
203    /// Override the env-driven WCOJ triangle dispatch gate. Pass
204    /// `Some(true)` / `Some(false)` to force the runtime to ignore
205    /// `XLOG_USE_WCOJ_TRIANGLE_U32`; `None` to consult the env var
206    /// (the process-level configuration path).
207    pub fn with_wcoj_triangle_dispatch(mut self, override_value: Option<bool>) -> Self {
208        self.wcoj_triangle_dispatch = override_value;
209        self
210    }
211
212    /// Override the stats-backed WCOJ triangle dispatch gate.
213    /// Force-WCOJ (`with_wcoj_triangle_dispatch(Some(true))`)
214    /// takes precedence.
215    pub fn with_wcoj_triangle_dispatch_adaptive(mut self, override_value: Option<bool>) -> Self {
216        self.wcoj_triangle_dispatch_adaptive = override_value;
217        self
218    }
219
220    /// Engage / disengage the runtime-local WCOJ triangle
221    /// dispatch hard stop.
222    pub fn with_wcoj_triangle_dispatch_disabled(mut self, override_value: Option<bool>) -> Self {
223        self.wcoj_triangle_dispatch_disabled = override_value;
224        self
225    }
226
227    /// Override the 4-cycle force gate.
228    /// `Some(true)` forces the GPU kernel; `Some(false)` is
229    /// explicit force-off; `None` consults `XLOG_USE_WCOJ_4CYCLE`.
230    pub fn with_wcoj_4cycle_dispatch(mut self, override_value: Option<bool>) -> Self {
231        self.wcoj_4cycle_dispatch = override_value;
232        self
233    }
234
235    /// Override the 4-cycle stats opt-in.
236    /// `Some(true)` engages the cardinality model; `Some(false)` skips it.
237    /// `None` resolves to `false` (opt-in by default — 4-cycle
238    /// does NOT inherit triangle's default-on behavior).
239    pub fn with_wcoj_4cycle_dispatch_adaptive(mut self, override_value: Option<bool>) -> Self {
240        self.wcoj_4cycle_dispatch_adaptive = override_value;
241        self
242    }
243
244    /// Engage / disengage the 4-cycle kill switch.
245    /// Same shape as the triangle kill switch.
246    pub fn with_wcoj_4cycle_dispatch_disabled(mut self, override_value: Option<bool>) -> Self {
247        self.wcoj_4cycle_dispatch_disabled = override_value;
248        self
249    }
250
251    /// Select which WCOJ cost-model implementation the runtime consults.
252    ///
253    /// Precedence:
254    /// 1. Explicit config field set here.
255    /// 2. `XLOG_WCOJ_COST_MODEL=cardinality` or `skew`.
256    /// 3. Default `Cardinality`.
257    pub fn with_wcoj_cost_model(mut self, kind: Option<CostModelKind>) -> Self {
258        self.wcoj_cost_model = kind;
259        self
260    }
261
262    /// Enable or disable runtime common subexpression elimination.
263    pub fn with_common_subexpression_elimination(mut self, override_value: Option<bool>) -> Self {
264        self.common_subexpression_elimination = override_value;
265        self
266    }
267
268    /// Enable or disable adaptive runtime re-optimization adoption.
269    pub fn with_adaptive_reoptimization(mut self, override_value: Option<bool>) -> Self {
270        self.adaptive_reoptimization = override_value;
271        self
272    }
273
274    /// Set the minimum mis-plan ratio for adaptive runtime re-optimization.
275    pub fn with_adaptive_reoptimization_min_misplan_ratio(
276        mut self,
277        override_value: Option<f64>,
278    ) -> Self {
279        self.adaptive_reoptimization_min_misplan_ratio = override_value;
280        self
281    }
282
283    /// Enable or disable persistent build-side hash index reuse.
284    pub fn with_persistent_hash_indexes(mut self, override_value: Option<bool>) -> Self {
285        self.persistent_hash_indexes = override_value;
286        self
287    }
288
289    /// Enable or disable persistent hash-index background-build telemetry.
290    pub fn with_persistent_hash_index_background_build(
291        mut self,
292        override_value: Option<bool>,
293    ) -> Self {
294        self.persistent_hash_index_background_build = override_value;
295        self
296    }
297
298    /// Resolve runtime common subexpression elimination by config/env/default.
299    pub fn resolved_common_subexpression_elimination(&self) -> Result<bool> {
300        resolve_bool(self.common_subexpression_elimination, "XLOG_CSE", false)
301    }
302
303    /// Resolve adaptive runtime re-optimization by config/env/default.
304    pub fn resolved_adaptive_reoptimization(&self) -> Result<bool> {
305        resolve_bool(self.adaptive_reoptimization, "XLOG_ADAPTIVE_REOPT", false)
306    }
307
308    /// Resolve the deterministic mis-plan threshold for adaptive re-optimization.
309    pub fn resolved_adaptive_reoptimization_min_misplan_ratio(&self) -> f64 {
310        const DEFAULT_MIN_RATIO: f64 = 1.2;
311        if let Some(value) = self.adaptive_reoptimization_min_misplan_ratio {
312            return sanitize_adaptive_reoptimization_ratio(value, DEFAULT_MIN_RATIO);
313        }
314
315        std::env::var("XLOG_ADAPTIVE_REOPT_MIN_RATIO")
316            .ok()
317            .and_then(|raw| raw.trim().parse::<f64>().ok())
318            .map(|value| sanitize_adaptive_reoptimization_ratio(value, DEFAULT_MIN_RATIO))
319            .unwrap_or(DEFAULT_MIN_RATIO)
320    }
321
322    /// Resolve persistent hash-index reuse by config/env/default.
323    pub fn resolved_persistent_hash_indexes(&self) -> Result<bool> {
324        resolve_bool(
325            self.persistent_hash_indexes,
326            "XLOG_PERSISTENT_HASH_INDEXES",
327            true,
328        )
329    }
330
331    /// Resolve background-build telemetry for persistent hash indexes.
332    pub fn resolved_persistent_hash_index_background_build(&self) -> Result<bool> {
333        resolve_bool(
334            self.persistent_hash_index_background_build,
335            "XLOG_PERSISTENT_HASH_INDEX_BACKGROUND_BUILD",
336            false,
337        )
338    }
339
340    /// Resolve the effective WCOJ cost-model selector.
341    pub fn resolved_wcoj_cost_model(&self) -> CostModelKind {
342        if let Some(kind) = self.wcoj_cost_model {
343            return kind;
344        }
345        let raw = std::env::var("XLOG_WCOJ_COST_MODEL").ok();
346        let normalized = raw.as_deref().map(|s| s.trim().to_ascii_lowercase());
347        match normalized.as_deref() {
348            Some("cardinality") => CostModelKind::Cardinality,
349            Some("skew") | Some("skewclassifier") | Some(_) => CostModelKind::SkewClassifier,
350            None => CostModelKind::Cardinality,
351        }
352    }
353}
354
355fn sanitize_adaptive_reoptimization_ratio(value: f64, fallback: f64) -> f64 {
356    if value.is_finite() && value >= 1.0 {
357        value
358    } else {
359        fallback
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::XlogError;
367    use std::sync::{Mutex, OnceLock};
368
369    fn env_lock() -> &'static Mutex<()> {
370        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
371        LOCK.get_or_init(|| Mutex::new(()))
372    }
373
374    struct EnvRestore {
375        name: &'static str,
376        value: Option<std::ffi::OsString>,
377    }
378
379    impl EnvRestore {
380        fn capture(name: &'static str) -> Self {
381            Self {
382                name,
383                value: std::env::var_os(name),
384            }
385        }
386    }
387
388    impl Drop for EnvRestore {
389        fn drop(&mut self) {
390            match self.value.take() {
391                Some(value) => std::env::set_var(self.name, value),
392                None => std::env::remove_var(self.name),
393            }
394        }
395    }
396
397    #[test]
398    fn test_memory_budget_default() {
399        let budget = MemoryBudget::default();
400        assert!(!budget.allow_ooc);
401        assert!(budget.abort_on_exceed);
402    }
403
404    #[test]
405    fn test_runtime_config_default() {
406        let config = RuntimeConfig::default();
407        assert!(config.deterministic);
408        assert!(!config.profile);
409    }
410
411    #[test]
412    fn explicit_boolean_config_precedes_invalid_environment() {
413        let _guard = env_lock().lock().unwrap();
414        let _restore = EnvRestore::capture("XLOG_CSE");
415        std::env::set_var("XLOG_CSE", "invalid");
416        let config = RuntimeConfig::default().with_common_subexpression_elimination(Some(false));
417        assert!(!config.resolved_common_subexpression_elimination().unwrap());
418    }
419
420    #[test]
421    fn malformed_boolean_environment_is_a_typed_error() {
422        let _guard = env_lock().lock().unwrap();
423        let _restore = EnvRestore::capture("XLOG_PERSISTENT_HASH_INDEXES");
424        std::env::set_var("XLOG_PERSISTENT_HASH_INDEXES", "sometimes");
425        assert!(matches!(
426            RuntimeConfig::default().resolved_persistent_hash_indexes(),
427            Err(XlogError::Configuration { ref name, .. })
428                if name == "XLOG_PERSISTENT_HASH_INDEXES"
429        ));
430    }
431
432    #[test]
433    fn test_memory_budget_from_device() {
434        let budget = MemoryBudget::from_device_memory(10_000_000_000);
435        assert_eq!(budget.device_bytes, 8_000_000_000);
436    }
437}