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}