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, ¤t_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(¤t_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(¤t_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}