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 session-scoped [`ArrayKernels`] before
14//!   static parent-reduce rules, so this is the entry point used by execution.
15//! - [`ArrayOptimizer::optimize_recursive`] applies the session-aware optimizer to the root and
16//!   every descendant.
17
18use smallvec::SmallVec;
19use vortex_error::VortexResult;
20use vortex_error::vortex_bail;
21use vortex_session::SessionExt;
22use vortex_session::VortexSession;
23
24use crate::ArrayRef;
25use crate::optimizer::kernels::ArrayKernels;
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 session-registered [`ArrayKernels`] should participate.
36    fn optimize(&self) -> VortexResult<ArrayRef>;
37
38    /// Optimize the root array node using static rules and any [`ArrayKernels`] on `session`.
39    ///
40    /// Session kernels are checked for each `(parent_encoding_id, child_encoding_id)` pair before
41    /// the child's static `PARENT_RULES`. If `session` does not contain [`ArrayKernels`], this
42    /// behaves like [`Self::optimize`].
43    fn optimize_ctx(&self, session: &VortexSession) -> VortexResult<ArrayRef>;
44
45    /// Optimize the entire array tree recursively (root and all descendants).
46    ///
47    /// This uses the same session-aware rule ordering as [`Self::optimize_ctx`] for every node in
48    /// the tree.
49    fn optimize_recursive(&self, session: &VortexSession) -> VortexResult<ArrayRef>;
50}
51
52impl ArrayOptimizer for ArrayRef {
53    fn optimize(&self) -> VortexResult<ArrayRef> {
54        Ok(try_optimize(self, None)?.unwrap_or_else(|| self.clone()))
55    }
56
57    fn optimize_ctx(&self, session: &VortexSession) -> VortexResult<ArrayRef> {
58        Ok(try_optimize(self, Some(session))?.unwrap_or_else(|| self.clone()))
59    }
60
61    fn optimize_recursive(&self, session: &VortexSession) -> VortexResult<ArrayRef> {
62        Ok(try_optimize_recursive(self, session)?.unwrap_or_else(|| self.clone()))
63    }
64}
65
66fn try_optimize(
67    array: &ArrayRef,
68    session: Option<&VortexSession>,
69) -> VortexResult<Option<ArrayRef>> {
70    let mut current_array = array.clone();
71    let mut any_optimizations = false;
72    let array_ref = session.and_then(|s| s.get_opt::<ArrayKernels>());
73
74    // Apply reduction rules to the current array until no more rules apply.
75    let mut loop_counter = 0;
76    'outer: loop {
77        if loop_counter > 100 {
78            vortex_bail!("Exceeded maximum optimization iterations (possible infinite loop)");
79        }
80        loop_counter += 1;
81
82        if let Some(new_array) = current_array.reduce()? {
83            current_array = new_array;
84            any_optimizations = true;
85            continue;
86        }
87
88        // Apply parent reduction rules to each slot in the context of the current array.
89        // Its important to take all slots here, as `current_array` can change inside the loop.
90        for (slot_idx, slot) in current_array.slots().iter().enumerate() {
91            let Some(child) = slot else { continue };
92
93            // Session kernels take precedence over the child encoding's static PARENT_RULES.
94            if let Some(array_ref) = &array_ref
95                && let Some(plugins) =
96                    array_ref.find_reduce_parent(current_array.encoding_id(), child.encoding_id())
97            {
98                for plugin in plugins.as_ref() {
99                    if let Some(new_array) = plugin(child, &current_array, slot_idx)? {
100                        current_array = new_array;
101                        any_optimizations = true;
102                        continue 'outer;
103                    }
104                }
105            }
106
107            if let Some(new_array) = child.reduce_parent(&current_array, slot_idx)? {
108                // If the parent was replaced, then we attempt to reduce it again.
109                current_array = new_array;
110                any_optimizations = true;
111
112                // Continue to the start of the outer loop
113                continue 'outer;
114            }
115        }
116
117        // No more optimizations can be applied
118        break;
119    }
120
121    if any_optimizations {
122        Ok(Some(current_array))
123    } else {
124        Ok(None)
125    }
126}
127
128fn try_optimize_recursive(
129    array: &ArrayRef,
130    session: &VortexSession,
131) -> VortexResult<Option<ArrayRef>> {
132    let mut current_array = array.clone();
133    let mut any_optimizations = false;
134
135    if let Some(new_array) = try_optimize(&current_array, Some(session))? {
136        current_array = new_array;
137        any_optimizations = true;
138    }
139
140    let mut new_slots = SmallVec::with_capacity(current_array.slots().len());
141    let mut any_slot_optimized = false;
142    for slot in current_array.slots() {
143        match slot {
144            Some(child) => {
145                if let Some(new_child) = try_optimize_recursive(child, session)? {
146                    new_slots.push(Some(new_child));
147                    any_slot_optimized = true;
148                } else {
149                    new_slots.push(Some(child.clone()));
150                }
151            }
152            None => new_slots.push(None),
153        }
154    }
155
156    if any_slot_optimized {
157        current_array = current_array.with_slots(new_slots)?;
158        any_optimizations = true;
159    }
160
161    if any_optimizations {
162        Ok(Some(current_array))
163    } else {
164        Ok(None)
165    }
166}