Skip to main content

vortex_array/optimizer/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! The optimizer applies metadata-only rewrite rules (`reduce` and `reduce_parent`) in a
5//! fixpoint loop until no more transformations are possible.
6//!
7//! Optimization runs between execution steps, which is what enables cross-step optimizations:
8//! after a child is decoded, new `reduce_parent` rules may match that were previously blocked.
9//!
10//! There are three public entry points on [`ArrayOptimizer`]:
11//!
12//! - [`ArrayOptimizer::optimize`] uses only static rules registered on encoding vtables.
13//! - [`ArrayOptimizer::optimize_ctx`] also consults the session's active
14//!   [`kernels::ArrayKernels`] registry before static parent-reduce rules, so this is the entry
15//!   point used by execution.
16//! - [`ArrayOptimizer::optimize_recursive`] applies the session-aware optimizer to the root and
17//!   every descendant.
18
19use smallvec::SmallVec;
20use vortex_error::VortexResult;
21use vortex_error::vortex_bail;
22use vortex_session::VortexSession;
23
24use crate::ArrayRef;
25use crate::optimizer::kernels::ArrayKernelsExt;
26
27pub mod kernels;
28pub mod rules;
29
30/// Extension trait for optimizing array trees using reduce/reduce_parent rules.
31pub trait ArrayOptimizer {
32    /// Optimize the root array node by running reduce and reduce_parent rules to fixpoint.
33    ///
34    /// This uses only static rules registered on encoding vtables. Use [`Self::optimize_ctx`]
35    /// when a session-scoped [`kernels::ArrayKernels`] registry should participate.
36    fn optimize(&self) -> VortexResult<ArrayRef>;
37
38    /// Optimize the root array node using static rules and the active
39    /// [`kernels::ArrayKernels`] registry on `session`, if any.
40    ///
41    /// Session kernels are checked for each `(parent_encoding_id, child_encoding_id)` pair before
42    /// the child's static `PARENT_RULES`. The registry comes from the [`kernels::KernelSession`] on
43    /// `session`, if any. If `session` does not contain a [`kernels::KernelSession`], this behaves
44    /// like [`Self::optimize`].
45    fn optimize_ctx(&self, session: &VortexSession) -> VortexResult<ArrayRef>;
46
47    /// Optimize the entire array tree recursively (root and all descendants).
48    ///
49    /// This uses the same session-aware rule ordering as [`Self::optimize_ctx`] for every node in
50    /// the tree.
51    fn optimize_recursive(&self, session: &VortexSession) -> VortexResult<ArrayRef>;
52}
53
54impl ArrayOptimizer for ArrayRef {
55    fn optimize(&self) -> VortexResult<ArrayRef> {
56        Ok(try_optimize(self, None)?.unwrap_or_else(|| self.clone()))
57    }
58
59    fn optimize_ctx(&self, session: &VortexSession) -> VortexResult<ArrayRef> {
60        Ok(try_optimize(self, Some(session))?.unwrap_or_else(|| self.clone()))
61    }
62
63    fn optimize_recursive(&self, session: &VortexSession) -> VortexResult<ArrayRef> {
64        Ok(try_optimize_recursive(self, session)?.unwrap_or_else(|| self.clone()))
65    }
66}
67
68fn try_optimize(
69    array: &ArrayRef,
70    session: Option<&VortexSession>,
71) -> VortexResult<Option<ArrayRef>> {
72    let mut current_array = array.clone();
73    let mut any_optimizations = false;
74    let array_ref = session.map(|s| s.kernels());
75
76    // Apply reduction rules to the current array until no more rules apply.
77    let mut loop_counter = 0;
78    'outer: loop {
79        if loop_counter > 100 {
80            vortex_bail!("Exceeded maximum optimization iterations (possible infinite loop)");
81        }
82        loop_counter += 1;
83
84        if let Some(new_array) = current_array.reduce()? {
85            current_array = new_array;
86            any_optimizations = true;
87            continue;
88        }
89
90        // Apply parent reduction rules to each slot in the context of the current array.
91        // Its important to take all slots here, as `current_array` can change inside the loop.
92        for (slot_idx, slot) in current_array.slots().iter().enumerate() {
93            let Some(child) = slot else { continue };
94
95            // Session kernels take precedence over the child encoding's static PARENT_RULES.
96            if let Some(array_ref) = &array_ref
97                && let Some(plugins) =
98                    array_ref.find_reduce_parent(current_array.encoding_id(), child.encoding_id())
99            {
100                for plugin in plugins.as_ref() {
101                    if let Some(new_array) = plugin(child, &current_array, slot_idx)? {
102                        current_array = new_array;
103                        any_optimizations = true;
104                        continue 'outer;
105                    }
106                }
107            }
108
109            if let Some(new_array) = child.reduce_parent(&current_array, slot_idx)? {
110                // If the parent was replaced, then we attempt to reduce it again.
111                current_array = new_array;
112                any_optimizations = true;
113
114                // Continue to the start of the outer loop
115                continue 'outer;
116            }
117        }
118
119        // No more optimizations can be applied
120        break;
121    }
122
123    if any_optimizations {
124        Ok(Some(current_array))
125    } else {
126        Ok(None)
127    }
128}
129
130fn try_optimize_recursive(
131    array: &ArrayRef,
132    session: &VortexSession,
133) -> VortexResult<Option<ArrayRef>> {
134    let mut current_array = array.clone();
135    let mut any_optimizations = false;
136
137    if let Some(new_array) = try_optimize(&current_array, Some(session))? {
138        current_array = new_array;
139        any_optimizations = true;
140    }
141
142    let mut new_slots = SmallVec::with_capacity(current_array.slots().len());
143    let mut any_slot_optimized = false;
144    for slot in current_array.slots() {
145        match slot {
146            Some(child) => {
147                if let Some(new_child) = try_optimize_recursive(child, session)? {
148                    new_slots.push(Some(new_child));
149                    any_slot_optimized = true;
150                } else {
151                    new_slots.push(Some(child.clone()));
152                }
153            }
154            None => new_slots.push(None),
155        }
156    }
157
158    if any_slot_optimized {
159        // SAFETY: optimizer rules only replace child slots with logically equivalent arrays, so
160        // parent logical values and statistics remain valid.
161        current_array = unsafe { current_array.with_slots(new_slots) }?;
162        any_optimizations = true;
163    }
164
165    if any_optimizations {
166        Ok(Some(current_array))
167    } else {
168        Ok(None)
169    }
170}