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::SessionExt;
36use vortex_session::SessionGuard;
37use vortex_session::SessionVar;
38use vortex_session::VortexSession;
39use vortex_session::registry::Id;
40use vortex_utils::aliases::DefaultHashBuilder;
41use vortex_utils::aliases::hash_map::HashMap;
42
43use crate::ArrayRef;
44use crate::ExecutionCtx;
45use crate::arc_swap_map::ArcSwapMap;
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/// Session-scoped registry of optimizer kernel functions.
176///
177/// Each kernel kind has its own storage map, keyed by `(outer_id, child_id)`. Registering
178/// functions for an existing key appends them to that key's ordered list.
179#[derive(Clone, Debug)]
180pub struct ArrayKernels {
181    reduce_parent: ArcSwapMap<ReduceParentFnId, Arc<[ReduceParentFn]>>,
182    execute_parent: ArcSwapMap<ExecuteParentFnId, Arc<[ExecuteParentKernelRef]>>,
183}
184
185impl Default for ArrayKernels {
186    fn default() -> ArrayKernels {
187        let this = Self::empty();
188        this.register_builtin_reduce_parent();
189        this
190    }
191}
192
193impl ArrayKernels {
194    /// Create an empty [`ArrayKernels`] with no kernels registered.
195    pub fn empty() -> Self {
196        Self {
197            reduce_parent: ArcSwapMap::default(),
198            execute_parent: ArcSwapMap::default(),
199        }
200    }
201
202    fn register_builtin_reduce_parent(&self) {
203        self.register_reduce_parent(
204            Cast.id(),
205            Struct.id(),
206            &[struct_cast_reduce_parent as ReduceParentFn],
207        );
208    }
209
210    /// Register [`ReduceParentFn`]s for `(parent, child)`.
211    ///
212    /// The optimizer invokes these functions in registration order when it sees a parent with
213    /// encoding id `parent` holding a child with encoding id `child` during a `reduce_parent`
214    /// step, before trying the child encoding's static `PARENT_RULES`. `parent` is usually the
215    /// parent array's encoding id. For `ScalarFnArray`, it is the scalar function id, for example
216    /// `Cast.id()`.
217    ///
218    /// If functions have already been registered for the same pair, these functions are appended
219    /// after them.
220    pub fn register_reduce_parent(&self, parent: Id, child: Id, fns: &[ReduceParentFn]) {
221        self.reduce_parent
222            .extend(hash_fn_id(parent, child).into(), fns);
223    }
224
225    /// Look up the [`ReduceParentFn`]s registered for `(parent, child)`.
226    ///
227    /// Returns an owned [`Arc`] so the session-variable borrow can be dropped before invoking the
228    /// functions.
229    pub fn find_reduce_parent(&self, parent: Id, child: Id) -> Option<Arc<[ReduceParentFn]>> {
230        self.reduce_parent.get(&hash_fn_id(parent, child))
231    }
232
233    /// Register [`ExecuteParentFn`]s for `(parent, child)`.
234    ///
235    /// The executor invokes these functions in registration order when it sees a parent with
236    /// encoding id `parent` holding a child with encoding id `child` during a parent execution
237    /// step.
238    ///
239    /// If functions have already been registered for the same pair, these functions are appended
240    /// after them.
241    pub fn register_execute_parent(&self, parent: Id, child: Id, fns: &[ExecuteParentFn]) {
242        let kernels: Vec<ExecuteParentKernelRef> = fns
243            .iter()
244            .map(|f| Arc::new(ExecuteParentFnKernel(*f)) as ExecuteParentKernelRef)
245            .collect();
246        self.execute_parent
247            .extend(hash_fn_id(parent, child).into(), kernels.as_slice());
248    }
249
250    /// Register a typed [`ExecuteParentKernel`] for `(parent, child.id())`.
251    ///
252    /// The executor invokes registered kernels in registration order before falling through to
253    /// later registered kernels for the same key. `parent` is usually the parent array's encoding
254    /// id. For `ScalarFnArray`, it is the scalar function id, for example `Cast.id()`.
255    ///
256    /// If kernels have already been registered for the same pair, this kernel is appended after
257    /// them; registering for an existing key cannot override built-in kernels installed earlier.
258    pub fn register_execute_parent_kernel<V, K>(&self, parent: Id, child: V, kernel: K)
259    where
260        V: VTable,
261        K: ExecuteParentKernel<V>,
262    {
263        let child_id = child.id();
264        self.execute_parent.push(
265            hash_fn_id(parent, child_id).into(),
266            Arc::new(RegisteredExecuteParentKernel {
267                _child: child,
268                kernel,
269            }) as ExecuteParentKernelRef,
270        );
271    }
272
273    /// Returns true when one or more execute-parent kernels are registered for `(parent, child)`.
274    pub fn has_execute_parent(&self, parent: Id, child: Id) -> bool {
275        self.execute_parent
276            .get(&hash_fn_id(parent, child))
277            .is_some()
278    }
279
280    /// Return the currently published execute-parent kernel snapshot.
281    pub(crate) fn execute_parent_snapshot(&self) -> Arc<ParentExecutionKernels> {
282        self.execute_parent.snapshot()
283    }
284}
285
286fn hash_fn_id(parent: Id, child: Id) -> u64 {
287    FN_HASHER.hash_one((parent, child))
288}
289
290/// Return the registry key for execute-parent kernels registered for `(parent, child)`.
291pub(crate) fn execute_parent_key(parent: Id, child: Id) -> u64 {
292    hash_fn_id(parent, child)
293}
294
295/// Session-scoped holder for the optimizer kernel registry.
296///
297/// `KernelSession` is the session variable that owns an [`ArrayKernels`] registry. Its [`Default`]
298/// implementation installs vortex-array's built-in parent-reduce and execute-parent kernels,
299/// mirroring how [`ScalarFnSession`](crate::scalar_fn::session::ScalarFnSession) and the other
300/// session variables register their built-ins.
301#[derive(Clone, Debug)]
302pub struct KernelSession {
303    kernels: ArrayKernels,
304}
305
306impl KernelSession {
307    /// Create a [`KernelSession`] with an empty kernel registry.
308    pub fn empty() -> Self {
309        Self {
310            kernels: ArrayKernels::empty(),
311        }
312    }
313
314    /// Returns the [`ArrayKernels`] registry held by this session.
315    pub fn kernels(&self) -> &ArrayKernels {
316        &self.kernels
317    }
318}
319
320/// Derefs to the held [`ArrayKernels`] registry, so a [`KernelSession`] (or a
321/// [`SessionGuard<KernelSession>`](SessionGuard) read from a session) can be used wherever an
322/// `&ArrayKernels` is expected.
323impl Deref for KernelSession {
324    type Target = ArrayKernels;
325
326    fn deref(&self) -> &ArrayKernels {
327        &self.kernels
328    }
329}
330
331impl Default for KernelSession {
332    fn default() -> Self {
333        // `ArrayKernels::default` installs the built-in parent-reduce kernels. The execute-parent
334        // kernels are registered by the per-encoding `initialize` functions, which operate on a
335        // session. `KernelSession` clones share their registry storage, so kernels registered into
336        // the temporary session land in `this.kernels`.
337        let this = Self {
338            kernels: ArrayKernels::default(),
339        };
340        let session = VortexSession::empty().with_some(this.clone());
341        crate::arrays::initialize(&session);
342        this
343    }
344}
345
346impl SessionVar for KernelSession {
347    fn as_any(&self) -> &dyn Any {
348        self
349    }
350
351    fn as_any_mut(&mut self) -> &mut dyn Any {
352        self
353    }
354}
355
356/// Extension trait for accessing the optimizer kernel registry from a [`VortexSession`].
357pub trait ArrayKernelsExt: SessionExt {
358    /// Returns the session's [`KernelSession`], inserting a default one (with the built-in
359    /// kernels) if it does not exist.
360    ///
361    /// The returned [`SessionGuard`] borrows the session snapshot it was read from (so the registry
362    /// stays alive even if the session is concurrently mutated) and derefs through [`KernelSession`]
363    /// to the [`ArrayKernels`] registry, so it can be used wherever an `&ArrayKernels` is expected.
364    /// The registry shares its storage with the session, so kernels registered through it remain
365    /// visible to the session.
366    fn kernels(&self) -> SessionGuard<'_, KernelSession> {
367        self.get::<KernelSession>()
368    }
369}
370
371impl<S: SessionExt> ArrayKernelsExt for S {}
372
373#[cfg(test)]
374mod tests {
375    use vortex_session::VortexSession;
376
377    use super::ArrayKernelsExt;
378    use super::KernelSession;
379    use crate::ArrayVTable;
380    use crate::arrays::Bool;
381    use crate::scalar_fn::ScalarFnVTable;
382    use crate::scalar_fn::fns::binary::Binary;
383
384    #[test]
385    fn kernel_session_default_registers_builtin_kernels() {
386        let session = VortexSession::empty().with::<KernelSession>();
387
388        assert!(session.kernels().has_execute_parent(Binary.id(), Bool.id()));
389    }
390
391    #[test]
392    fn initialize_registers_builtin_kernels_into_empty_kernel_session() {
393        let session = VortexSession::empty().with_some(KernelSession::empty());
394
395        assert!(!session.kernels().has_execute_parent(Binary.id(), Bool.id()));
396
397        crate::initialize(&session);
398
399        assert!(session.kernels().has_execute_parent(Binary.id(), Bool.id()));
400    }
401
402    #[test]
403    fn kernels_inserts_default_kernel_session() {
404        let session = VortexSession::empty();
405
406        // `kernels()` uses `get`, so it inserts a default `KernelSession` (with the built-in
407        // kernels) rather than returning `None`.
408        assert!(session.kernels().has_execute_parent(Binary.id(), Bool.id()));
409    }
410}