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;
26use crate::trace_op;
27
28pub mod kernels;
29pub mod rules;
30
31/// Extension trait for optimizing array trees using reduce/reduce_parent rules.
32pub trait ArrayOptimizer {
33    /// Optimize the root array node by running reduce and reduce_parent rules to fixpoint.
34    ///
35    /// This uses only static rules registered on encoding vtables. Use [`Self::optimize_ctx`]
36    /// when a session-scoped [`kernels::ArrayKernels`] registry should participate.
37    fn optimize(&self) -> VortexResult<ArrayRef>;
38
39    /// Optimize the root array node using static rules and the active
40    /// [`kernels::ArrayKernels`] registry on `session`, if any.
41    ///
42    /// Session kernels are checked for each `(parent_encoding_id, child_encoding_id)` pair before
43    /// the child's static `PARENT_RULES`. The registry comes from the [`kernels::KernelSession`] on
44    /// `session`, if any. If `session` does not contain a [`kernels::KernelSession`], this behaves
45    /// like [`Self::optimize`].
46    fn optimize_ctx(&self, session: &VortexSession) -> VortexResult<ArrayRef>;
47
48    /// Optimize the entire array tree recursively (root and all descendants).
49    ///
50    /// This uses the same session-aware rule ordering as [`Self::optimize_ctx`] for every node in
51    /// the tree.
52    fn optimize_recursive(&self, session: &VortexSession) -> VortexResult<ArrayRef>;
53}
54
55impl ArrayOptimizer for ArrayRef {
56    fn optimize(&self) -> VortexResult<ArrayRef> {
57        Ok(try_optimize(self, None)?.unwrap_or_else(|| self.clone()))
58    }
59
60    fn optimize_ctx(&self, session: &VortexSession) -> VortexResult<ArrayRef> {
61        Ok(try_optimize(self, Some(session))?.unwrap_or_else(|| self.clone()))
62    }
63
64    fn optimize_recursive(&self, session: &VortexSession) -> VortexResult<ArrayRef> {
65        Ok(try_optimize_recursive(self, session)?.unwrap_or_else(|| self.clone()))
66    }
67}
68
69fn try_optimize(
70    array: &ArrayRef,
71    session: Option<&VortexSession>,
72) -> VortexResult<Option<ArrayRef>> {
73    let mut current_array = array.clone();
74    let mut any_optimizations = false;
75    let array_ref = session.map(|s| s.kernels());
76
77    trace_op!(record_optimize_start(array, session.is_some()));
78
79    // Apply reduction rules to the current array until no more rules apply.
80    for _ in 0..=100 {
81        trace_op!(record_optimize_loop_start(&current_array));
82
83        if let Some(new_array) = current_array.reduce()? {
84            current_array = new_array;
85            any_optimizations = true;
86            trace_op!(record_optimize_loop_end());
87            continue;
88        }
89
90        trace_op!(record_optimize_reduce_none(&current_array));
91
92        // Apply parent reduction rules to each slot in the context of the current array.
93        // Its important to take all slots here, as `current_array` can change inside the loop.
94        let mut parent_reduced = None;
95        for (slot_idx, slot) in current_array.slots().iter().enumerate() {
96            let Some(child) = slot else { continue };
97
98            // Session kernels take precedence over the child encoding's static PARENT_RULES.
99            if let Some(array_ref) = &array_ref
100                && let Some(plugins) =
101                    array_ref.find_reduce_parent(current_array.encoding_id(), child.encoding_id())
102            {
103                #[allow(clippy::unused_enumerate_index)]
104                for (_plugin_idx, plugin) in plugins.as_ref().iter().enumerate() {
105                    if let Some(new_array) = plugin(child, &current_array, slot_idx)? {
106                        trace_op!(record_session_parent_reduce_applied(
107                            &current_array,
108                            child,
109                            slot_idx,
110                            _plugin_idx,
111                            &new_array,
112                        ));
113                        parent_reduced = Some(new_array);
114                        break;
115                    }
116                    trace_op!(record_session_parent_reduce_declined(
117                        &current_array,
118                        child,
119                        slot_idx,
120                        _plugin_idx,
121                    ));
122                }
123                if parent_reduced.is_some() {
124                    break;
125                }
126            }
127
128            if let Some(new_array) = child.reduce_parent(&current_array, slot_idx)? {
129                parent_reduced = Some(new_array);
130                break;
131            }
132        }
133
134        if let Some(new_array) = parent_reduced {
135            // If the parent was replaced, then we attempt to reduce it again.
136            current_array = new_array;
137            any_optimizations = true;
138            trace_op!(record_optimize_loop_end());
139            continue;
140        }
141
142        trace_op!(record_optimize_parent_reduce_none(&current_array));
143        trace_op!(record_optimize_loop_end());
144
145        // No more optimizations can be applied
146        trace_op!(record_optimize_done(&current_array, any_optimizations));
147
148        if any_optimizations {
149            return Ok(Some(current_array));
150        } else {
151            return Ok(None);
152        }
153    }
154
155    vortex_bail!("Exceeded maximum optimization iterations (possible infinite loop)");
156}
157
158fn try_optimize_recursive(
159    array: &ArrayRef,
160    session: &VortexSession,
161) -> VortexResult<Option<ArrayRef>> {
162    let mut current_array = array.clone();
163    let mut any_optimizations = false;
164
165    trace_op!(record_optimize_recursive_start(array));
166
167    if let Some(new_array) = try_optimize(&current_array, Some(session))? {
168        current_array = new_array;
169        any_optimizations = true;
170    }
171
172    let mut new_slots = SmallVec::with_capacity(current_array.slots().len());
173    let mut any_slot_optimized = false;
174    for slot in current_array.slots() {
175        match slot {
176            Some(child) => {
177                if let Some(new_child) = try_optimize_recursive(child, session)? {
178                    trace_op!(record_optimize_recursive_slot(
179                        new_slots.len(),
180                        child,
181                        &new_child,
182                    ));
183                    new_slots.push(Some(new_child));
184                    any_slot_optimized = true;
185                } else {
186                    new_slots.push(Some(child.clone()));
187                }
188            }
189            None => new_slots.push(None),
190        }
191    }
192
193    if any_slot_optimized {
194        // SAFETY: optimizer rules only replace child slots with logically equivalent arrays, so
195        // parent logical values and statistics remain valid.
196        current_array = unsafe { current_array.with_slots(new_slots) }?;
197        any_optimizations = true;
198    }
199
200    if any_optimizations {
201        Ok(Some(current_array))
202    } else {
203        Ok(None)
204    }
205}