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}