Load a model once per partition
To load a model or client once per partition, put the setup before the loop in mapInArrow, mapInPandas or an iterator pandas_udf. In an iterator arrow_udf, code before the loop runs once per batch instead.
Check targets:
pysail==0.7.3 with pyspark-client==4.1.3The Python assertions below check this behavior. Details attributed to external documentation are outside those assertions. Newer releases may behave differently.
Run the check
Follow the test setup, then run from the repository root (sail-examples/). The harness applies the configuration declared in this check.
Sail
uv run pytest 'tests/test_facts.py::test_fact[udf-setup-per-partition[sail]]'Python code
udf-setup-per-partition.py79 lines
FACT = {
"id": "udf-setup-per-partition",
"title": "Load a model once per partition",
"category": "python-udfs",
"core": True,
"text": """To load a model or client once per partition, put the setup before the loop in
`mapInArrow`, `mapInPandas` or an iterator `pandas_udf`. In an iterator `arrow_udf`, code before the
loop runs once per batch instead.""",
"engines": ["sail"],
"order": 2,
}
import tempfile
from collections.abc import Iterator
from pathlib import Path
import pandas as pd
import pyarrow as pa
from pyspark.sql import functions as F
from pyspark.sql.functions import arrow_udf, pandas_udf
def check(spark, engine):
log_dir = Path(tempfile.mkdtemp())
log_path = str(log_dir)
def mark(kind, what):
with open(f"{log_path}/{kind}-{what}.log", "a") as f:
f.write("x\n")
def count(kind, what):
path = log_dir / f"{kind}-{what}.log"
return len(path.read_text().splitlines()) if path.exists() else 0
@arrow_udf("long")
def arrow_iter(it: Iterator[pa.Array]) -> Iterator[pa.Array]:
mark("arrow_udf", "setup")
for array in it:
mark("arrow_udf", "batch")
yield array
@pandas_udf("long")
def pandas_iter(it: Iterator[pd.Series]) -> Iterator[pd.Series]:
mark("pandas_udf", "setup")
for series in it:
mark("pandas_udf", "batch")
yield series
def map_arrow(it):
mark("mapInArrow", "setup")
for batch in it:
mark("mapInArrow", "batch")
yield batch
def map_pandas(it):
mark("mapInPandas", "setup")
for frame in it:
mark("mapInPandas", "batch")
yield frame
partitions = 2
base = spark.range(0, 8000).repartition(partitions)
base.select(arrow_iter("id").alias("y")).agg(F.sum("y")).collect()
base.select(pandas_iter("id").alias("y")).agg(F.sum("y")).collect()
base.mapInArrow(map_arrow, "id long").agg(F.sum("id")).collect()
base.mapInPandas(map_pandas, "id long").agg(F.sum("id")).collect()
kinds = ("arrow_udf", "pandas_udf", "mapInArrow", "mapInPandas")
observed = {k: {"setup": count(k, "setup"), "batches": count(k, "batch")} for k in kinds}
for kind in ("pandas_udf", "mapInArrow", "mapInPandas"):
assert observed[kind]["setup"] == partitions, (
f"{kind} setup should run once per partition: {observed}"
)
assert observed[kind]["batches"] > partitions, f"{kind} should see several batches: {observed}"
arrow = observed["arrow_udf"]
assert arrow["setup"] == arrow["batches"] > partitions, (
f"arrow_udf setup should run per batch: {observed}"
)
return observed