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(optimize_owned(self.clone(), None)?.0)
63    }
64
65    fn optimize_ctx(&self, session: &VortexSession) -> VortexResult<ArrayRef> {
66        Ok(optimize_owned(self.clone(), Some(session))?.0)
67    }
68
69    fn optimize_recursive(&self, session: &VortexSession) -> VortexResult<ArrayRef> {
70        Ok(try_optimize_recursive(self.clone(), session)?.0)
71    }
72}
73
74fn optimize_owned(
75    mut current_array: ArrayRef,
76    session: Option<&VortexSession>,
77) -> VortexResult<(ArrayRef, bool)> {
78    let mut any_optimizations = false;
79    let session_kernels = session.map(|session| session.kernels());
80
81    trace_op!(record_optimize_start(&current_array, session.is_some()));
82
83    for _ in 0..=MAX_OPTIMIZER_REWRITE_PASS {
84        trace_op!(record_optimize_loop_start(&current_array));
85
86        if let Some(new_array) = current_array.reduce()? {
87            current_array = new_array;
88            any_optimizations = true;
89            trace_op!(record_optimize_loop_end());
90            continue;
91        }
92
93        trace_op!(record_optimize_reduce_none(&current_array));
94
95        // Try children in order; the first parent rewrite restarts the fixpoint loop.
96        let mut reduced_parent = None;
97        for (slot_idx, slot) in current_array.slots().iter().enumerate() {
98            let Some(child) = slot else {
99                continue;
100            };
101
102            // Session kernels take precedence over the child's static parent-reduce rules.
103            if let Some(session_kernels) = &session_kernels
104                && let Some(new_array) =
105                    try_session_parent_reduce(session_kernels, &current_array, child, slot_idx)?
106            {
107                reduced_parent = Some(new_array);
108                break;
109            }
110
111            if let Some(new_array) = child.reduce_parent(&current_array, slot_idx)? {
112                reduced_parent = Some(new_array);
113                break;
114            }
115        }
116
117        if let Some(new_array) = reduced_parent {
118            current_array = new_array;
119            any_optimizations = true;
120            trace_op!(record_optimize_loop_end());
121            continue;
122        }
123
124        trace_op!(record_optimize_parent_reduce_none(&current_array));
125        trace_op!(record_optimize_loop_end());
126
127        trace_op!(record_optimize_done(&current_array, any_optimizations));
128
129        return Ok((current_array, any_optimizations));
130    }
131
132    vortex_bail!("Exceeded maximum optimization iterations (possible infinite loop)");
133}
134
135fn try_session_parent_reduce(
136    kernels: &ArrayKernels,
137    parent: &ArrayRef,
138    child: &ArrayRef,
139    slot_idx: usize,
140) -> VortexResult<Option<ArrayRef>> {
141    let Some(reduce_parent_fns) =
142        kernels.find_reduce_parent(parent.encoding_id(), child.encoding_id())
143    else {
144        return Ok(None);
145    };
146
147    #[allow(clippy::unused_enumerate_index)]
148    for (_kernel_idx, reduce_parent) in reduce_parent_fns.iter().enumerate() {
149        if let Some(new_array) = reduce_parent(child, parent, slot_idx)? {
150            trace_op!(record_session_parent_reduce_applied(
151                parent,
152                child,
153                slot_idx,
154                _kernel_idx,
155                &new_array,
156            ));
157
158            return Ok(Some(new_array));
159        }
160
161        trace_op!(record_session_parent_reduce_declined(
162            parent,
163            child,
164            slot_idx,
165            _kernel_idx,
166        ));
167    }
168
169    Ok(None)
170}
171
172fn try_optimize_recursive(
173    current_array: ArrayRef,
174    session: &VortexSession,
175) -> VortexResult<(ArrayRef, bool)> {
176    trace_op!(record_optimize_recursive_start(&current_array));
177
178    let (mut current_array, mut any_optimizations) = optimize_owned(current_array, Some(session))?;
179
180    let mut new_slots = SmallVec::with_capacity(current_array.slots().len());
181    let mut any_slot_optimized = false;
182    for slot in current_array.slots() {
183        match slot {
184            Some(child) => {
185                let (new_child, new_slot_optimized) =
186                    try_optimize_recursive(child.clone(), session)?;
187                if new_slot_optimized {
188                    trace_op!(record_optimize_recursive_slot(
189                        new_slots.len(),
190                        child,
191                        &new_child,
192                    ));
193                }
194                new_slots.push(Some(new_child));
195                any_slot_optimized |= new_slot_optimized;
196            }
197            None => new_slots.push(None),
198        }
199    }
200
201    if any_slot_optimized {
202        // SAFETY: optimizer rules only replace child slots with logically equivalent arrays, so
203        // parent logical values and statistics remain valid.
204        current_array = unsafe { current_array.with_slots(new_slots) }?;
205        any_optimizations = true;
206    }
207
208    Ok((current_array, any_optimizations))
209}