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}