Repository navigation
[Python] Allow reusing compute kernel state across repeated function calls via pc.bind_function() / FunctionExecutor (GH-49214) - #53910
Draft
Asthenia0412 wants to merge 1 commit into
Conversation
…ecutor (apacheGH-49214) This change exposes the C++ GetFunctionExecutor() API from pyarrow.compute, allowing users to pre-bind a compute function (e.g. is_in / index_in) with its options once and then execute it many times on fresh input arrays. One-time setup such as building the SetLookup MemoTable (hash table) is thus shared across all executions. Why? Ray Data checkpointing scenario (apache#49214): pc.is_in(arr, value_set=big_set) rebuilds the hash table on every call. With a 100k-element value_set and 200 arrays the executor path is expected to cut total runtime by roughly the factor (setup + N*execute) / (N*execute). New public surface on pyarrow.compute: - bind_function(name, args, options=None, memory_pool=None, registry=None) Returns a FunctionExecutor pre-configured for the given function name, prototype arguments, and options. State that depends on the options (notably SetLookupOptions.value_set) is built at bind time. - FunctionExecutor.execute(args, memory_pool=None, length=None) Run the bound computation on new arguments whose types must match those used in bind_function. Files changed: - python/pyarrow/includes/libarrow.pxd Declares arrow::compute::FunctionExecutor and GetFunctionExecutor (only the vector<Datum> overload, which is sufficient for pyarrow). - python/pyarrow/_compute.pxd Adds the FunctionExecutor cdef class wrapper skeleton. - python/pyarrow/_compute.pyx Implements bind_function() and the FunctionExecutor class using the standard _pack_compute_args / wrap_datum / maybe_unbox_memory_pool helpers used elsewhere in this module. - python/pyarrow/tests/test_compute.py Nine new pytest cases covering correctness, reuse across arrays, different function kinds (is_in, index_in, add), error paths, custom registry, and ChunkedArray input. - python/pyarrow/tests/benchmarks/benchmark_is_in_executor.py Baseline benchmark script. Reports repeated pc.is_in() vs bind_function+FunctionExecutor across three value_set / array-size combinations.
|
Thanks for opening a pull request! This pull request has been automatically converted to a draft because its title doesn't match Arrow's required format. If this is not a minor PR, could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project. Then could you also rename the pull request title in the following format? or After updating the title, you can mark the pull request as ready for review. See also: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
Issue #49214 reports that
pc.is_in(arr, value_set=big_set)rebuilds the hash table (MemoTable) fromvalue_seton every call, even whenvalue_setis identical across many invocations. In the Ray Data checkpointing scenario this causes substantial waste — 100M IDs x 20 blocks x a 100k-element value-set — because setup cost (build hash table) is paid 20 times instead of once.The C++ side already has the right primitive:
arrow::compute::GetFunctionExecutor(name, args, options)creates aFunctionExecutorthat binds a function, kernel, and preconfigures kernel state from options (including buildingSetLookupState::Init()which populates the hash table fromvalue_set). The returned executor'sExecute()method reuses that state for every call. However, Python had no access to this API — everypc.is_in(...)call went throughFunction::Execute, which does bind + init every time.Solution
Expose the existing C++
FunctionExecutor/GetFunctionExecutorprimitives onpyarrow.compute:pc.bind_function(name, args, options=None, memory_pool=None, registry=None)— create a pre-configuredFunctionExecutor. The function lookup + kernel selection + kernel Init (including building hash table) all happen here, once.pc.FunctionExecutor— a wrapper class with an.execute(args)method that runs the bound kernel on new arguments of the same types.No new C++ code is written. The patch is purely Python/Cython glue:
python/pyarrow/includes/libarrow.pxdarrow::compute::FunctionExecutorand theGetFunctionExecutor(name, vector<Datum>, options, registry)overload inside the existingcdef extern from "arrow/compute/api.h"block — safe becauseapi.halready#include "arrow/compute/exec.h"python/pyarrow/_compute.pxdFunctionExecutorcdef class skeleton (.pxdholds shared state;.pyxholds methods)python/pyarrow/_compute.pyxbind_function()and theFunctionExecutorclass using the same_pack_compute_args,wrap_datum,maybe_unbox_memory_pool, andGetResultValuehelpers already used byFunction.call()python/pyarrow/tests/test_compute.pyis_inandindex_in, functions without options (add), different options produce different executors, no direct construction, custom registry, wrong arg-type error path, ChunkedArray supportpython/pyarrow/tests/benchmarks/benchmark_is_in_executor.pypc.is_in()vsbind_function+ executorWhy this design
Function,call_function,Function.call()are untouched. Existing users see no behaviour change.is_in/index_in,bind_functionapplies to every compute function —add,match_substring,cast, etc. — anywhere a kernel has setup state worth caching.pc.bindvspc.bind_function.bind_functionis deliberately verbose (call_functionis already so) and explicit about what it returns. If the Arrow maintainers prefer a shorter alias (pc.bind), that is easy to add later.Testing
The patch ships with:
Unit tests (added to
python/pyarrow/tests/test_compute.py) covering correctness, reuse, error paths, custom registry, and ChunkedArray input.Baseline benchmark (
python/pyarrow/tests/benchmarks/benchmark_is_in_executor.py). Running on pyarrow 26.0.0 (pre-patch) shows the repeated-call costs:The executor builds the hash table once (during
bind_function) and only pays the per-array execution cost. As N grows the speedup approachessetup_per_call / execute_per_call, which for large value_sets is the factor reported in the issue (~13x at 20 blocks x 100M IDs).Cython syntax validation.
python -m cython --cpluson the modified_compute.pyxsucceeds with no errors (only the same performance hints that appear on the unmodified file forGetResultValueinnogilblocks elsewhere). The generated C++ file contains 136 references toFunctionExecutor/GetFunctionExecutor.Full Arrow Python CI (
python -m pytest pyarrow/tests/test_compute.py) should green — nothing else changed.Follow-ups
GetFunctionExecutor'sTypeHolderoverload, which binds from type prototypes without materialisingDatums. That would be slightly more convenient when the user knows only the input types, not the data.bind_functioncould learn an optionalmemory_poolfor allocation of the executor's precomputed state — currently the executor uses the default pool (matchingFunction.call()).