Documentation Index
Fetch the complete documentation index at: https://docs.pixeltable.com/llms.txt
Use this file to discover all available pages before exploring further.
Connected to Pixeltable database at: postgresql+psycopg://postgres:@/pixeltable?host=/Users/asiegel/.pixeltable/pgdata
Created directory `udf_demo`.
Created table `strings`.
Inserting rows into `strings`: 2 rows [00:00, 763.99 rows/s]
Inserted 2 rows with 0 errors.
What is a UDF?
A Pixeltable UDF is just a Python function that is marked with the@pxt.udf decorator.
add_one would be an
ordinary Python function that operates on integers. Adding @pxt.udf
converts it into a Pixeltable function that operates on columns of
integers. The decorated function can then be used directly to define
computed columns; Pixeltable will orchestrate its execution across all
the input data.
For our first working example, let’s do something slightly more
interesting: write a function to extract the longest word from a
sentence. (If there are ties for the longest word, we choose the first
word among those ties.) In Python, that might look something like this:
‘check’
The longest_word Python function isn’t a Pixeltable UDF (yet); it
operates on individual strings, not columns of strings. Adding the
decorator turns it into a UDF:
Computing cells: 100%|███████████████████████████████████████████| 2/2 [00:00<00:00, 138.28 cells/s]
Added 2 column values with 0 errors.
Computing cells: 0%| | 0/3 [00:00<?, ? cells/s]
Inserting rows into `strings`: 1 rows [00:00, 255.24 rows/s]
Computing cells: 100%|███████████████████████████████████████████| 3/3 [00:00<00:00, 364.69 cells/s]
Inserted 1 row with 0 errors.
Oops, those trailing punctuation marks are kind of annoying. Let’s add
another column, this time using the handy strip_punctuation parameter
from our UDF. (We could alternatively drop the first column before
adding the new one, but for purposes of this tutorial it’s convenient to
see how Pixeltable executes both variants side-by-side.) Note how
columns such as t.input and constants such as True can be freely
intermixed as arguments to the UDF.
Computing cells: 100%|███████████████████████████████████████████| 3/3 [00:00<00:00, 252.91 cells/s]
Added 3 column values with 0 errors.
Types in UDFs
You might have noticed that thelongest_word UDF has type hints in
its signature.
sentence parameter, strip_punctuation parameter, and return
value all have explicit types (str, bool, and str respectively).
In general Python code, type hints are usually optional. But Pixeltable
is a database system: everything in Pixeltable must have a type. And
since Pixeltable is also an orchestrator - meaning it sets up workflows
and computed columns before executing them - these types need to be
known in advance. That’s the reasoning behind a fundamental principle of
Pixeltable UDFs:
- Type hints are required.
intfloatstrlist(can optionally be parameterized, e.g.,list[str])dict(can optionally be parameterized, e.g.,dict[str, int])PIL.Image.Image
Local and module UDFs
Thelongest_word UDF that we defined above is a local UDF: it was
defined directly in our notebook, rather than in a module that we
imported. Many other UDFs, including all of Pixeltable’s built-in
functions, are defined in modules. We encountered a few of these in the
10-Minute Tour tutorial: the huggingface.detr_for_object_detection and
openai.chat_completions functions. (Although these are built-in
functions, they behave the same way as UDFs, and in fact they’re defined
the same way under the covers.)
There is an important difference between the two. When you add a module
UDF such as openai.chat_completions to a table, Pixeltable stores a
reference to the corresponding Python function in the module. If you
later restart your Python runtime and reload Pixeltable, then Pixeltable
will re-import the module UDF when it loads the computed column. This
means that any code changes made to the UDF will be picked up at that
time, and the new version of the UDF will be used in any future
execution.
Conversely, when you add a local UDF to a table, the entire code for
the UDF is serialized and stored in the table. This ensures that if you
restart your notebook kernel (say), or even delete the notebook
entirely, the UDF will continue to function. However, it also means that
if you modify the UDF code, the updated logic will not be reflected in
any existing Pixeltable columns.
To see how this works in practice, let’s modify our longest_word UDF
so that if strip_punctuation is True, then we remove only a single
punctuation mark from the end of each word.
Computing cells: 0%| | 0/5 [00:00<?, ? cells/s]
Inserting rows into `strings`: 1 rows [00:00, 242.01 rows/s]
Computing cells: 100%|███████████████████████████████████████████| 5/5 [00:00<00:00, 623.99 cells/s]
Inserted 1 row with 0 errors.
But if we add a new column that references the longest_word UDF,
Pixeltable will use the updated version.
Computing cells: 100%|███████████████████████████████████████████| 4/4 [00:00<00:00, 348.89 cells/s]
Added 4 column values with 0 errors.
The general rule is: changes to module UDFs will affect any future
execution; changes to local UDFs will only affect new columns that are
defined using the new version of the UDF.
Batching
Pixeltable provides several ways to optimize UDFs for better performance. One of the most common is batching, which is particularly important for UDFs that involve GPU operations. Ordinary UDFs process one row at a time, meaning the UDF will be invoked exactly once per row processed. Conversely, a batched UDF processes several rows at a time; the specific number is user-configurable. As an example, let’s modify ourlongest_word UDF to take a batched
parameter. Here’s what it looks like:
- The parameter
batch_size=16has been added to the@pxt.udfdecorator, specifying the batch size; - The
sentencesparameter has changed fromstrtoBatch[str]; - The return type has also changed from
strtoBatch[str]; and - Instead of processing a single sentence, the UDF is processing a
Batchof sentences and returning the resultBatch.
Batch[str]? Functionally, it’s simply a list[str],
and you can use it exactly like a list[str] in any Python code. The
only difference is in the type hint; a type hint of Batch[str] tells
Pixeltable, “My data consists of individual strings that I want you to
process in batches”. Conversely, a type hint of list[str] would mean,
“My data consists of lists of strings that I want you to process one
at a time”.
Notice that the strip_punctuation parameter is not wrapped in a
Batch type. This because strip_punctuation controls the behavior of
the UDF, rather than being part of the input data. When we use the
batched longest_word UDF, the strip_punctuation parameter will
always be a constant, not a column.
Let’s put the new, batched UDF to work.
Computing cells: 100%|███████████████████████████████████████████| 4/4 [00:00<00:00, 353.90 cells/s]
Added 4 column values with 0 errors.
As expected, the output of the longest_word_3_batched column is
identical to the longest_word_3 column. Under the covers, though,
Pixeltable is orchestrating execution in batches of 16. That probably
won’t have much performance impact on our toy example, but for GPU-bound
computations such as text or image embeddings, it can make a substantial
difference.
UDAs (aggregate UDFs)
Ordinary UDFs are always one-to-one on rows: each row of input generates one UDF output value. Functions that aggregate data, conversely, are many-to-one, and in Pixeltable they are represented by a related abstraction, the UDA (User-Defined Aggregate). Pixeltable has a number of built-in UDAs; if you’ve worked through the Fundamentals tutorial, you’ll have already encountered a few of them, such assum and count. In this section, we’ll show how to define
your own custom UDAs. For demonstration purposes, let’s start by
creating a table containing all the integers from 0 to 49.
Created table `values`.
Inserting rows into `values`: 50 rows [00:00, 9267.95 rows/s]
Inserted 50 rows with 0 errors.
UpdateStatus(num_rows=50, num_computed_values=0, num_excs=0, updated_cols=[], cols_with_excs=[])
If we wanted to compute their sum using the built-in sum aggregate,
we’d do it like this:
n // 10 (corresponding to the tens
digit of each integer) and sum each group:
pxt.Aggregator Python class and decorate it with the @pxt.uda
decorator, similar to what we did for UDFs. The subclass must implement
three methods:
__init__()- initializes the aggregator; can be used to parameterize aggregator behaviorupdate()- updates the internal state of the aggregator with a new valuevalue()- retrieves the current value held by the aggregator
cur_sum, which
holds a running total of the squares of all the values we’ve seen.