Skip to main content

xlog_core/
config.rs

1//! Configuration types for XLOG runtime
2
3/// GPU memory budget configuration.
4///
5/// Use [`MemoryBudget::default()`] or the builder methods ([`MemoryBudget::from_device_memory`],
6/// [`MemoryBudget::with_limit`], [`MemoryBudget::with_ooc`]) to construct.
7#[derive(Debug, Clone)]
8#[non_exhaustive]
9pub struct MemoryBudget {
10    /// Maximum device memory to use in bytes
11    pub device_bytes: u64,
12    /// Allow out-of-core execution (spill to host)
13    pub allow_ooc: bool,
14    /// Abort on memory budget exceeded (vs try to continue)
15    pub abort_on_exceed: bool,
16}
17
18impl Default for MemoryBudget {
19    fn default() -> Self {
20        Self {
21            device_bytes: 0, // Will be set from device query
22            allow_ooc: false,
23            abort_on_exceed: true,
24        }
25    }
26}
27
28impl MemoryBudget {
29    /// Create a budget using 80% of available device memory
30    pub fn from_device_memory(total_bytes: u64) -> Self {
31        Self {
32            device_bytes: (total_bytes as f64 * 0.8) as u64,
33            allow_ooc: false,
34            abort_on_exceed: true,
35        }
36    }
37
38    /// Create a budget with explicit byte limit
39    pub fn with_limit(device_bytes: u64) -> Self {
40        Self {
41            device_bytes,
42            allow_ooc: false,
43            abort_on_exceed: true,
44        }
45    }
46
47    /// Enable out-of-core mode
48    pub fn with_ooc(mut self) -> Self {
49        self.allow_ooc = true;
50        self
51    }
52}
53
54/// Runtime configuration for XLOG execution.
55///
56/// Use [`RuntimeConfig::default()`] and the builder methods to construct.
57#[derive(Debug, Clone)]
58#[non_exhaustive]
59pub struct RuntimeConfig {
60    /// Memory budget settings
61    pub memory: MemoryBudget,
62    /// Use deterministic execution (may be slower)
63    pub deterministic: bool,
64    /// Enable profiling (row counts, memory tracking)
65    pub profile: bool,
66    /// Maximum fixpoint iterations before abort
67    pub max_iterations: u32,
68    /// Opt-in: enforce the strict deterministic-Datalog D2H gate during
69    /// `Executor::execute_plan`. When `true`, any data-plane device-to-host
70    /// transfer (column downloads, internal `dtoh_sync_copy_into_tracked`
71    /// calls) returns `XlogError::Execution` and increments the provider's
72    /// `deterministic_d2h_violation_count`. Metadata reads via
73    /// `dtoh_scalar_untracked` remain allowed.
74    ///
75    /// Default `false`: v0.5.5 still has known data-plane D2H paths in
76    /// relational set difference and binary-join count/materialize that are
77    /// scheduled for replacement before the default flips.
78    pub strict_deterministic_d2h: bool,
79    /// Override the env-driven WCOJ triangle dispatch gate
80    /// (`XLOG_USE_WCOJ_TRIANGLE_U32`). `None` (default) consults
81    /// the env var; `Some(true)` / `Some(false)` force the
82    /// runtime to ignore the env and use the explicit value.
83    /// Test-only knob — production callers should leave this
84    /// `None` and configure via the env var.
85    pub wcoj_triangle_dispatch: Option<bool>,
86    /// Override the stats-backed WCOJ triangle dispatch gate.
87    /// `None` uses the production default. `Some(true)` enables
88    /// the cardinality model; `Some(false)` disables this runtime's
89    /// default stats-backed decision.
90    pub wcoj_triangle_dispatch_adaptive: Option<bool>,
91    /// Runtime-local hard stop for WCOJ triangle dispatch.
92    /// `Some(true)` pins dispatch off across force and stats mode.
93    /// `Some(false)` leaves dispatch available for this runtime.
94    /// `None` uses the production default.
95    pub wcoj_triangle_dispatch_disabled: Option<bool>,
96
97    /// v0.6.5 slice 2 — force gate for the 4-cycle WCOJ dispatch.
98    /// `Some(true)` / env `XLOG_USE_WCOJ_4CYCLE=1` forces every
99    /// recognized 4-cycle to dispatch the GPU kernel. `Some(false)` is explicit force-off. `None`
100    /// (default) consults the env.
101    pub wcoj_4cycle_dispatch: Option<bool>,
102    /// v0.6.5 slice 2 — adaptive opt-in for 4-cycle WCOJ. **Unlike
103    /// triangle, 4-cycle adaptive is opt-in by default**, not
104    /// default-on: `None` resolves to `false`. Default-on for
105    /// 4-cycle is a separate follow-up slice gated by bench evidence.
106    pub wcoj_4cycle_dispatch_adaptive: Option<bool>,
107    /// v0.6.5 slice 2 — kill switch for 4-cycle WCOJ. Same shape
108    /// as triangle's kill switch: beats force + adaptive.
109    pub wcoj_4cycle_dispatch_disabled: Option<bool>,
110    /// v0.6.5 W2.5 — selects the runtime WCOJ cost model.
111    /// `None` resolves by env/default precedence; see
112    /// [`RuntimeConfig::with_wcoj_cost_model`].
113    pub wcoj_cost_model: Option<CostModelKind>,
114    /// v0.8.6 G086_CSE — runtime common subexpression elimination.
115    ///
116    /// `Some(true)` enables structural CSE for safe deterministic subplans.
117    /// `Some(false)` disables it. `None` consults `XLOG_CSE`; unset defaults
118    /// to disabled so existing runtime behavior is preserved unless the caller
119    /// opts in.
120    pub common_subexpression_elimination: Option<bool>,
121    /// v0.8.6 G086_ADAPT — runtime adaptive re-optimization adoption gate.
122    ///
123    /// `Some(true)` allows an executor to compare a baseline plan against a
124    /// compiler-supplied candidate plan using runtime telemetry, adopt the
125    /// candidate only when deterministic mis-plan thresholds trigger, and roll
126    /// back on adverse candidates. `Some(false)` disables the adoption path.
127    /// `None` consults `XLOG_ADAPTIVE_REOPT`; unset defaults to disabled.
128    pub adaptive_reoptimization: Option<bool>,
129    /// Minimum mis-plan ratio required before the executor attempts to adopt a
130    /// candidate re-optimized plan. `None` consults
131    /// `XLOG_ADAPTIVE_REOPT_MIN_RATIO`; unset or invalid values default to 1.2.
132    pub adaptive_reoptimization_min_misplan_ratio: Option<f64>,
133    /// v0.8.6 G086_INDEX — persistent hash index manager gate.
134    ///
135    /// `Some(true)` enables persistent build-side hash index reuse in the
136    /// existing executor join-index cache. `Some(false)` disables the manager.
137    /// `None` consults `XLOG_PERSISTENT_HASH_INDEXES`; unset defaults to
138    /// enabled to preserve the existing adaptive-indexing behavior.
139    pub persistent_hash_indexes: Option<bool>,
140    /// v0.8.6 G086_INDEX — record background-build mode for the persistent
141    /// hash index manager. The current runtime keeps builds on the existing
142    /// provider path but records background build requests/completions so the
143    /// transition to recorded asynchronous builds has stable telemetry.
144    pub persistent_hash_index_background_build: Option<bool>,
145}
146
147/// v0.6.5 W2.5 cost-model selector for WCOJ dispatch.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum CostModelKind {
150    /// Legacy skew-classifier opt-out selector.
151    ///
152    /// On current G38 integration code the GPU classifier surface is absent,
153    /// so this selector is implemented as a conservative opt-out from
154    /// 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 production default). Test-only knob.
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    /// v0.6.5 slice 2 — 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    /// v0.6.5 slice 2 — 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    /// v0.6.5 slice 2 — 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) -> bool {
300        if let Some(enabled) = self.common_subexpression_elimination {
301            return enabled;
302        }
303
304        std::env::var("XLOG_CSE")
305            .map(|raw| {
306                matches!(
307                    raw.trim().to_ascii_lowercase().as_str(),
308                    "1" | "true" | "on" | "yes"
309                )
310            })
311            .unwrap_or(false)
312    }
313
314    /// Resolve adaptive runtime re-optimization by config/env/default.
315    pub fn resolved_adaptive_reoptimization(&self) -> bool {
316        if let Some(enabled) = self.adaptive_reoptimization {
317            return enabled;
318        }
319
320        std::env::var("XLOG_ADAPTIVE_REOPT")
321            .map(|raw| {
322                matches!(
323                    raw.trim().to_ascii_lowercase().as_str(),
324                    "1" | "true" | "on" | "yes"
325                )
326            })
327            .unwrap_or(false)
328    }
329
330    /// Resolve the deterministic mis-plan threshold for adaptive re-optimization.
331    pub fn resolved_adaptive_reoptimization_min_misplan_ratio(&self) -> f64 {
332        const DEFAULT_MIN_RATIO: f64 = 1.2;
333        if let Some(value) = self.adaptive_reoptimization_min_misplan_ratio {
334            return sanitize_adaptive_reoptimization_ratio(value, DEFAULT_MIN_RATIO);
335        }
336
337        std::env::var("XLOG_ADAPTIVE_REOPT_MIN_RATIO")
338            .ok()
339            .and_then(|raw| raw.trim().parse::<f64>().ok())
340            .map(|value| sanitize_adaptive_reoptimization_ratio(value, DEFAULT_MIN_RATIO))
341            .unwrap_or(DEFAULT_MIN_RATIO)
342    }
343
344    /// Resolve persistent hash-index reuse by config/env/default.
345    pub fn resolved_persistent_hash_indexes(&self) -> bool {
346        if let Some(enabled) = self.persistent_hash_indexes {
347            return enabled;
348        }
349
350        std::env::var("XLOG_PERSISTENT_HASH_INDEXES")
351            .map(|raw| {
352                !matches!(
353                    raw.trim().to_ascii_lowercase().as_str(),
354                    "0" | "false" | "off" | "no"
355                )
356            })
357            .unwrap_or(true)
358    }
359
360    /// Resolve background-build telemetry for persistent hash indexes.
361    pub fn resolved_persistent_hash_index_background_build(&self) -> bool {
362        if let Some(enabled) = self.persistent_hash_index_background_build {
363            return enabled;
364        }
365
366        std::env::var("XLOG_PERSISTENT_HASH_INDEX_BACKGROUND_BUILD")
367            .map(|raw| {
368                matches!(
369                    raw.trim().to_ascii_lowercase().as_str(),
370                    "1" | "true" | "on" | "yes"
371                )
372            })
373            .unwrap_or(false)
374    }
375
376    /// Resolve the effective WCOJ cost-model selector.
377    pub fn resolved_wcoj_cost_model(&self) -> CostModelKind {
378        if let Some(kind) = self.wcoj_cost_model {
379            return kind;
380        }
381        let raw = std::env::var("XLOG_WCOJ_COST_MODEL").ok();
382        let normalized = raw.as_deref().map(|s| s.trim().to_ascii_lowercase());
383        match normalized.as_deref() {
384            Some("cardinality") => CostModelKind::Cardinality,
385            Some("skew") | Some("skewclassifier") | Some(_) => CostModelKind::SkewClassifier,
386            None => CostModelKind::Cardinality,
387        }
388    }
389}
390
391fn sanitize_adaptive_reoptimization_ratio(value: f64, fallback: f64) -> f64 {
392    if value.is_finite() && value >= 1.0 {
393        value
394    } else {
395        fallback
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_memory_budget_default() {
405        let budget = MemoryBudget::default();
406        assert!(!budget.allow_ooc);
407        assert!(budget.abort_on_exceed);
408    }
409
410    #[test]
411    fn test_runtime_config_default() {
412        let config = RuntimeConfig::default();
413        assert!(config.deterministic);
414        assert!(!config.profile);
415    }
416
417    #[test]
418    fn test_memory_budget_from_device() {
419        let budget = MemoryBudget::from_device_memory(10_000_000_000);
420        assert_eq!(budget.device_bytes, 8_000_000_000);
421    }
422}