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