Skip to main content

vortex_array/optimizer/
kernels.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Session-scoped registry for optimizer kernels.
5//!
6//! [`ArrayKernels`] stores function pointers that participate in array optimization and execution
7//! without adding rules or kernels to an encoding vtable. The optimizer consults it for
8//! parent-reduce rewrites before the child encoding's static `PARENT_RULES`, and the executor
9//! consults it for parent execution. A registered function can therefore add support for an
10//! extension encoding or take precedence over a built-in rule. When several functions are
11//! registered for the same key and kind, they are tried in registration order until one applies.
12//!
13//! Kernel entries are addressed by `(outer_id, child_id)`. For parent-reduce and execute-parent
14//! kernels, `outer_id` is the id returned by the parent array's `encoding_id()` and `child_id` is
15//! the child array's `encoding_id()`. For [`ScalarFn`](crate::arrays::ScalarFn) parents, the
16//! parent id is the scalar function id.
17//!
18//! Because registered functions have different signatures for each kernel kind, the registry
19//! maintains one storage map per function type rather than a single type-erased map.
20//!
21//! [`KernelSession`] is the session variable that owns this registry. Its [`Default`]
22//! implementation installs vortex-array's built-in parent-reduce and execute-parent kernels, so a
23//! session built with [`KernelSession`] participates in the same optimizations and fused execution
24//! as the built-in encodings.
25
26use std::any::Any;
27use std::borrow::Borrow;
28use std::fmt::Debug;
29use std::hash::BuildHasher;
30use std::ops::Deref;
31use std::sync::Arc;
32use std::sync::LazyLock;
33
34use vortex_error::VortexResult;
35use vortex_session::ArcSwapMap;
36use vortex_session::SessionExt;
37use vortex_session::SessionGuard;
38use vortex_session::SessionVar;
39use vortex_session::VortexSession;
40use vortex_session::registry::Id;
41use vortex_utils::aliases::DefaultHashBuilder;
42use vortex_utils::aliases::hash_map::HashMap;
43
44use crate::ArrayRef;
45use crate::ExecutionCtx;
46use crate::array::VTable;
47use crate::arrays::Struct;
48use crate::arrays::struct_::compute::rules::struct_cast_reduce_parent;
49use crate::kernel::ExecuteParentKernel;
50use crate::matcher::Matcher;
51use crate::scalar_fn::ScalarFnVTable;
52use crate::scalar_fn::fns::cast::Cast;
53
54/// Shared hasher used to combine `(outer, child)` tuples into registry keys.
55static FN_HASHER: LazyLock<DefaultHashBuilder> = LazyLock::new(DefaultHashBuilder::default);
56
57/// Function pointer for a plugin-provided parent-reduce rewrite.
58///
59/// The optimizer calls this with the matched `child`, its `parent`, and the slot index where the
60/// child appears. Return `Ok(Some(new_parent))` to replace the parent, or `Ok(None)` when the
61/// rewrite does not apply.
62///
63/// Implementations must preserve the parent's logical length and dtype, matching the invariant
64/// required of static parent-reduce rules.
65pub type ReduceParentFn =
66    fn(child: &ArrayRef, parent: &ArrayRef, child_idx: usize) -> VortexResult<Option<ArrayRef>>;
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
69#[repr(transparent)]
70struct ReduceParentFnId(u64);
71
72impl From<u64> for ReduceParentFnId {
73    fn from(id: u64) -> Self {
74        Self(id)
75    }
76}
77
78impl Borrow<u64> for ReduceParentFnId {
79    fn borrow(&self) -> &u64 {
80        &self.0
81    }
82}
83
84/// Function pointer for a plugin-provided parent execution.
85///
86/// The executor calls this with the matched `child`, its `parent`, the slot index where the child
87/// appears, and the current [`ExecutionCtx`]. Return `Ok(Some(new_parent))` to replace the parent
88/// with an executed result, or `Ok(None)` when the kernel does not apply.
89///
90/// Implementations must preserve the parent's logical length and dtype, matching the invariant
91/// required of static `execute_parent` kernels.
92pub type ExecuteParentFn = fn(
93    child: &ArrayRef,
94    parent: &ArrayRef,
95    child_idx: usize,
96    ctx: &mut ExecutionCtx,
97) -> VortexResult<Option<ArrayRef>>;
98
99/// Type-erased execute-parent kernel stored in the session registry.
100pub trait DynExecuteParentKernel: Debug + Send + Sync + 'static {
101    /// Attempt to execute the parent array fused with the child array.
102    fn execute_parent(
103        &self,
104        child: &ArrayRef,
105        parent: &ArrayRef,
106        child_idx: usize,
107        ctx: &mut ExecutionCtx,
108    ) -> VortexResult<Option<ArrayRef>>;
109}
110
111pub(crate) type ExecuteParentKernelRef = Arc<dyn DynExecuteParentKernel>;
112
113pub(crate) type ParentExecutionKernels = HashMap<ExecuteParentFnId, Arc<[ExecuteParentKernelRef]>>;
114
115#[derive(Debug)]
116struct ExecuteParentFnKernel(ExecuteParentFn);
117
118impl DynExecuteParentKernel for ExecuteParentFnKernel {
119    fn execute_parent(
120        &self,
121        child: &ArrayRef,
122        parent: &ArrayRef,
123        child_idx: usize,
124        ctx: &mut ExecutionCtx,
125    ) -> VortexResult<Option<ArrayRef>> {
126        self.0(child, parent, child_idx, ctx)
127    }
128}
129
130#[derive(Debug)]
131struct RegisteredExecuteParentKernel<V, K> {
132    _child: V,
133    kernel: K,
134}
135
136impl<V, K> DynExecuteParentKernel for RegisteredExecuteParentKernel<V, K>
137where
138    V: VTable,
139    K: ExecuteParentKernel<V>,
140{
141    fn execute_parent(
142        &self,
143        child: &ArrayRef,
144        parent: &ArrayRef,
145        child_idx: usize,
146        ctx: &mut ExecutionCtx,
147    ) -> VortexResult<Option<ArrayRef>> {
148        let Some(child) = child.as_opt::<V>() else {
149            return Ok(None);
150        };
151        let Some(parent) = K::Parent::try_match(parent) else {
152            return Ok(None);
153        };
154
155        self.kernel.execute_parent(child, parent, child_idx, ctx)
156    }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
160#[repr(transparent)]
161pub(crate) struct ExecuteParentFnId(u64);
162
163impl From<u64> for ExecuteParentFnId {
164    fn from(id: u64) -> Self {
165        Self(id)
166    }
167}
168
169impl Borrow<u64> for ExecuteParentFnId {
170    fn borrow(&self) -> &u64 {
171        &self.0
172    }
173}
174
175/// Registry of [`ReduceParentFn`]s, keyed by hashed `(parent, child)` pair.
176type ReduceParentRegistry = ArcSwapMap<ReduceParentFnId, Arc<[ReduceParentFn]>>;
177/// Registry of [`ExecuteParentKernelRef`]s, keyed by hashed `(parent, child)` pair.
178type ExecuteParentRegistry = ArcSwapMap<ExecuteParentFnId, Arc<[ExecuteParentKernelRef]>>;
179
180/// Session-scoped registry of optimizer kernel functions.
181///
182/// Each kernel kind has its own storage map, keyed by `(outer_id, child_id)`. Registering
183/// functions for an existing key appends them to that key's ordered list.
184#[derive(Clone, Debug)]
185pub struct ArrayKernels {
186    reduce_parent: ReduceParentRegistry,
187    execute_parent: ExecuteParentRegistry,
188}
189
190impl Default for ArrayKernels {
191    fn default() -> ArrayKernels {
192        let this = Self::empty();
193        this.register_builtin_reduce_parent();
194        this
195    }
196}
197
198impl ArrayKernels {
199    /// Create an empty [`ArrayKernels`] with no kernels registered.
200    pub fn empty() -> Self {
201        Self {
202            reduce_parent: ReduceParentRegistry::default(),
203            execute_parent: ExecuteParentRegistry::default(),
204        }
205    }
206
207    fn register_builtin_reduce_parent(&self) {
208        self.register_reduce_parent(
209            Cast.id(),
210            Struct.id(),
211            &[struct_cast_reduce_parent as ReduceParentFn],
212        );
213    }
214
215    /// Register [`ReduceParentFn`]s for `(parent, child)`.
216    ///
217    /// The optimizer invokes these functions in registration order when it sees a parent with
218    /// encoding id `parent` holding a child with encoding id `child` during a `reduce_parent`
219    /// step, before trying the child encoding's static `PARENT_RULES`. `parent` is usually the
220    /// parent array's encoding id. For `ScalarFnArray`, it is the scalar function id, for example
221    /// `Cast.id()`.
222    ///
223    /// If functions have already been registered for the same pair, these functions are appended
224    /// after them.
225    pub fn register_reduce_parent(&self, parent: Id, child: Id, fns: &[ReduceParentFn]) {
226        self.reduce_parent
227            .extend(hash_fn_id(parent, child).into(), fns);
228    }
229
230    /// Look up the [`ReduceParentFn`]s registered for `(parent, child)`.
231    ///
232    /// Returns an owned [`Arc`] so the session-variable borrow can be dropped before invoking the
233    /// functions.
234    pub fn find_reduce_parent(&self, parent: Id, child: Id) -> Option<Arc<[ReduceParentFn]>> {
235        self.reduce_parent.get(&hash_fn_id(parent, child))
236    }
237
238    /// Register [`ExecuteParentFn`]s for `(parent, child)`.
239    ///
240    /// The executor invokes these functions in registration order when it sees a parent with
241    /// encoding id `parent` holding a child with encoding id `child` during a parent execution
242    /// step.
243    ///
244    /// If functions have already been registered for the same pair, these functions are appended
245    /// after them.
246    pub fn register_execute_parent(&self, parent: Id, child: Id, fns: &[ExecuteParentFn]) {
247        let kernels: Vec<ExecuteParentKernelRef> = fns
248            .iter()
249            .map(|f| Arc::new(ExecuteParentFnKernel(*f)) as ExecuteParentKernelRef)
250            .collect();
251        self.execute_parent
252            .extend(hash_fn_id(parent, child).into(), kernels.as_slice());
253    }
254
255    /// Register a typed [`ExecuteParentKernel`] for `(parent, child.id())`.
256    ///
257    /// The executor invokes registered kernels in registration order before falling through to
258    /// later registered kernels for the same key. `parent` is usually the parent array's encoding
259    /// id. For `ScalarFnArray`, it is the scalar function id, for example `Cast.id()`.
260    ///
261    /// If kernels have already been registered for the same pair, this kernel is appended after
262    /// them; registering for an existing key cannot override built-in kernels installed earlier.
263    pub fn register_execute_parent_kernel<V, K>(&self, parent: Id, child: V, kernel: K)
264    where
265        V: VTable,
266        K: ExecuteParentKernel<V>,
267    {
268        let child_id = child.id();
269        self.execute_parent.push(
270            hash_fn_id(parent, child_id).into(),
271            Arc::new(RegisteredExecuteParentKernel {
272                _child: child,
273                kernel,
274            }) as ExecuteParentKernelRef,
275        );
276    }
277
278    /// Returns true when one or more execute-parent kernels are registered for `(parent, child)`.
279    pub fn has_execute_parent(&self, parent: Id, child: Id) -> bool {
280        self.execute_parent
281            .get(&hash_fn_id(parent, child))
282            .is_some()
283    }
284
285    /// Return the currently published execute-parent kernel snapshot.
286    pub(crate) fn execute_parent_snapshot(&self) -> Arc<ParentExecutionKernels> {
287        self.execute_parent.snapshot()
288    }
289}
290
291fn hash_fn_id(parent: Id, child: Id) -> u64 {
292    FN_HASHER.hash_one((parent, child))
293}
294
295/// Return the registry key for execute-parent kernels registered for `(parent, child)`.
296pub(crate) fn execute_parent_key(parent: Id, child: Id) -> u64 {
297    hash_fn_id(parent, child)
298}
299
300/// Session-scoped holder for the optimizer kernel registry.
301///
302/// `KernelSession` is the session variable that owns an [`ArrayKernels`] registry. Its [`Default`]
303/// implementation installs vortex-array's built-in parent-reduce and execute-parent kernels,
304/// mirroring how [`ScalarFnSession`](crate::scalar_fn::session::ScalarFnSession) and the other
305/// session variables register their built-ins.
306#[derive(Clone, Debug)]
307pub struct KernelSession {
308    kernels: ArrayKernels,
309}
310
311impl KernelSession {
312    /// Create a [`KernelSession`] with an empty kernel registry.
313    pub fn empty() -> Self {
314        Self {
315            kernels: ArrayKernels::empty(),
316        }
317    }
318
319    /// Returns the [`ArrayKernels`] registry held by this session.
320    pub fn kernels(&self) -> &ArrayKernels {
321        &self.kernels
322    }
323}
324
325/// Derefs to the held [`ArrayKernels`] registry, so a [`KernelSession`] (or a
326/// [`SessionGuard<KernelSession>`](SessionGuard) read from a session) can be used wherever an
327/// `&ArrayKernels` is expected.
328impl Deref for KernelSession {
329    type Target = ArrayKernels;
330
331    fn deref(&self) -> &ArrayKernels {
332        &self.kernels
333    }
334}
335
336impl Default for KernelSession {
337    fn default() -> Self {
338        // `ArrayKernels::default` installs the built-in parent-reduce kernels. The execute-parent
339        // kernels are registered by the per-encoding `initialize` functions, which operate on a
340        // session. `KernelSession` clones share their registry storage, so kernels registered into
341        // the temporary session land in `this.kernels`.
342        let this = Self {
343            kernels: ArrayKernels::default(),
344        };
345        let session = VortexSession::empty().with_some(this.clone());
346        crate::arrays::initialize(&session);
347        this
348    }
349}
350
351impl SessionVar for KernelSession {
352    fn as_any(&self) -> &dyn Any {
353        self
354    }
355
356    fn as_any_mut(&mut self) -> &mut dyn Any {
357        self
358    }
359}
360
361/// Extension trait for accessing the optimizer kernel registry from a [`VortexSession`].
362pub trait ArrayKernelsExt: SessionExt {
363    /// Returns the session's [`KernelSession`], inserting a default one (with the built-in
364    /// kernels) if it does not exist.
365    ///
366    /// The returned [`SessionGuard`] borrows the session snapshot it was read from (so the registry
367    /// stays alive even if the session is concurrently mutated) and derefs through [`KernelSession`]
368    /// to the [`ArrayKernels`] registry, so it can be used wherever an `&ArrayKernels` is expected.
369    /// The registry shares its storage with the session, so kernels registered through it remain
370    /// visible to the session.
371    fn kernels(&self) -> SessionGuard<'_, KernelSession> {
372        self.get::<KernelSession>()
373    }
374}
375
376impl<S: SessionExt> ArrayKernelsExt for S {}
377
378#[cfg(test)]
379mod tests {
380    use vortex_session::VortexSession;
381
382    use super::ArrayKernelsExt;
383    use super::KernelSession;
384    use crate::ArrayVTable;
385    use crate::arrays::Bool;
386    use crate::scalar_fn::ScalarFnVTable;
387    use crate::scalar_fn::fns::binary::Binary;
388
389    #[test]
390    fn kernel_session_default_registers_builtin_kernels() {
391        let session = VortexSession::empty().with::<KernelSession>();
392
393        assert!(session.kernels().has_execute_parent(Binary.id(), Bool.id()));
394    }
395
396    #[test]
397    fn initialize_registers_builtin_kernels_into_empty_kernel_session() {
398        let session = VortexSession::empty().with_some(KernelSession::empty());
399
400        assert!(!session.kernels().has_execute_parent(Binary.id(), Bool.id()));
401
402        crate::initialize(&session);
403
404        assert!(session.kernels().has_execute_parent(Binary.id(), Bool.id()));
405    }
406
407    #[test]
408    fn kernels_inserts_default_kernel_session() {
409        let session = VortexSession::empty();
410
411        // `kernels()` uses `get`, so it inserts a default `KernelSession` (with the built-in
412        // kernels) rather than returning `None`.
413        assert!(session.kernels().has_execute_parent(Binary.id(), Bool.id()));
414    }
415}