Skip to content

Commit c63bebe

Browse files
committed
[SPARK-59441][PYTHON] Simplify the UDF/UDTF runner contract to (func, serializer)
### What changes were proposed in this pull request? `read_udfs` and `read_udtf` in `python/pyspark/worker.py` previously returned a four-element tuple `(func, profiler, deserializer, serializer)` in which, for every UDF/UDTF eval type, `profiler` was always `None` and `deserializer` was always the same object as `serializer`. Both now return just `(func, serializer)`, and the UDF/UDTF branches of the worker dispatch are merged since the two functions share a signature. The classic RDD command path (`NON_UDF`, via `read_command`) keeps the four-element contract because its pickled command genuinely carries an independent input deserializer (distinct from the output serializer) and its own profiler. ### Why are the changes needed? This is preparatory refactoring for the extensible eval-type handler model tracked by the parent SPARK-59415: shrinking the contract now means each future handler produces `(func, serializer)` rather than a four-element tuple. The dropped elements were dead weight for UDF/UDTF, and the boundary between them and `NON_UDF` is intrinsic rather than incidental: - Profiler: SQL UDF profiling is folded into `func` in `read_single_udf` (per-UDF `result_id`, per-batch, scoped to the user function), so no profiler is returned separately. The RDD profiler is instead a pluggable instance sent over the wire that must wrap the whole task (`profiler.profile(run_process)`) to capture its lazily-consumed output. Neither can move to the other's boundary without changing what is measured, so only `NON_UDF` returns a profiler. - Deserializer: UDF/UDTF read input and write output through a single serializer; only the RDD command has an input deserializer distinct from its output serializer. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? No behavior change; the existing per-eval-type UDF/UDTF suites cover these paths. Also verified locally across representative eval types via `read_udfs` (plain Python UDF, scalar pandas UDF, grouped-map and cogrouped-map `applyInPandas`, grouped-agg pandas UDF, `mapInPandas`), via `read_udtf` (a Python UDTF), and via the `NON_UDF` classic RDD path (`rdd.map`), all with identical results. ### Was this patch authored or co-authored using generative AI tooling? No. Closes #58745 from Yicong-Huang/simplify-runner-contract. Authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Signed-off-by: Yicong-Huang <17627829+Yicong-Huang@users.noreply.github.com>
1 parent 80479fa commit c63bebe

1 file changed

Lines changed: 48 additions & 77 deletions

File tree

python/pyspark/worker.py

Lines changed: 48 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,7 +1522,7 @@ def func(split_index: int, data: Iterator["pa.RecordBatch"]) -> Iterator["pa.Rec
15221522
if cleanup is not None:
15231523
cleanup()
15241524

1525-
return func, None, ser, ser
1525+
return func, ser
15261526

15271527
elif (
15281528
eval_type == PythonEvalType.SQL_ARROW_TABLE_UDF
@@ -1686,7 +1686,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
16861686
if cleanup is not None:
16871687
cleanup()
16881688

1689-
return func, None, ser, ser
1689+
return func, ser
16901690

16911691
elif eval_type == PythonEvalType.SQL_ARROW_UDTF:
16921692
import pyarrow as pa
@@ -1794,7 +1794,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
17941794
if cleanup is not None:
17951795
cleanup()
17961796

1797-
return func, None, ser, ser
1797+
return func, ser
17981798

17991799
else:
18001800

@@ -1896,7 +1896,7 @@ def mapper(_, it):
18961896
if cleanup is not None:
18971897
cleanup()
18981898

1899-
return mapper, None, ser, ser
1899+
return mapper, ser
19001900

19011901

19021902
def _elementwise_renest(flat_values, shape_lengths, is_large):
@@ -2186,8 +2186,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
21862186
)
21872187
yield from map(ArrowBatchTransformer.wrap_struct, verified_iter)
21882188

2189-
# profiling is not supported for UDF
2190-
return func, None, ser, ser
2189+
return func, ser
21912190

21922191
if eval_type == PythonEvalType.SQL_SCALAR_ARROW_UDF:
21932192
import pyarrow as pa
@@ -2219,8 +2218,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
22192218
verify_scalar_result(output_batch, batch.num_rows)
22202219
yield output_batch
22212220

2222-
# profiling is not supported for UDF
2223-
return func, None, ser, ser
2221+
return func, ser
22242222

22252223
if eval_type == PythonEvalType.SQL_SCALAR_ARROW_ITER_UDF:
22262224
import pyarrow as pa
@@ -2279,8 +2277,7 @@ def process_results():
22792277
# Verify iterator consumed
22802278
verify_iterator_exhausted(args_iter)
22812279

2282-
# profiling is not supported for UDF
2283-
return func, None, ser, ser
2280+
return func, ser
22842281

22852282
if eval_type == PythonEvalType.SQL_GROUPED_AGG_ARROW_UDF:
22862283
import pyarrow as pa
@@ -2319,8 +2316,7 @@ def grouped_func(
23192316
batch = pa.RecordBatch.from_arrays(result_arrays, col_names)
23202317
yield ArrowBatchTransformer.enforce_schema(batch, return_schema)
23212318

2322-
# profiling is not supported for UDF
2323-
return grouped_func, None, ser, ser
2319+
return grouped_func, ser
23242320

23252321
if eval_type == PythonEvalType.SQL_GROUPED_AGG_ARROW_ITER_UDF:
23262322
import pyarrow as pa
@@ -2350,8 +2346,7 @@ def grouped_func(
23502346
batch = pa.RecordBatch.from_arrays([pa.array([result])], ["_0"])
23512347
yield ArrowBatchTransformer.enforce_schema(batch, return_schema)
23522348

2353-
# profiling is not supported for UDF
2354-
return grouped_func, None, ser, ser
2349+
return grouped_func, ser
23552350

23562351
if eval_type == PythonEvalType.SQL_GROUPED_AGG_ARROW_INCREMENTAL_PARTIAL_UDF:
23572352
import pyarrow as pa
@@ -2469,8 +2464,7 @@ def make_batch(entries: list) -> pa.RecordBatch:
24692464
for start in range(0, len(entries), cap):
24702465
yield make_batch(entries[start : start + cap])
24712466

2472-
# profiling is not supported for UDF
2473-
return func, None, ser, ser
2467+
return func, ser
24742468

24752469
if eval_type == PythonEvalType.SQL_GROUPED_AGG_ARROW_INCREMENTAL_FINAL_UDF:
24762470
import pyarrow as pa
@@ -2516,8 +2510,7 @@ def grouped_func(
25162510
batch = pa.RecordBatch.from_arrays(result_arrays, col_names)
25172511
yield ArrowBatchTransformer.enforce_schema(batch, return_schema)
25182512

2519-
# profiling is not supported for UDF
2520-
return grouped_func, None, ser, ser
2513+
return grouped_func, ser
25212514

25222515
if eval_type == PythonEvalType.SQL_GROUPED_AGG_PANDAS_UDF:
25232516
import pandas as pd
@@ -2560,8 +2553,7 @@ def grouped_func(
25602553
int_to_decimal_coercion_enabled=runner_conf.int_to_decimal_coercion_enabled,
25612554
)
25622555

2563-
# profiling is not supported for UDF
2564-
return grouped_func, None, ser, ser
2556+
return grouped_func, ser
25652557

25662558
if eval_type == PythonEvalType.SQL_GROUPED_AGG_PANDAS_ITER_UDF:
25672559
import pandas as pd
@@ -2606,8 +2598,7 @@ def grouped_func(
26062598
int_to_decimal_coercion_enabled=runner_conf.int_to_decimal_coercion_enabled,
26072599
)
26082600

2609-
# profiling is not supported for UDF
2610-
return grouped_func, None, ser, ser
2601+
return grouped_func, ser
26112602

26122603
if eval_type == PythonEvalType.SQL_WINDOW_AGG_ARROW_UDF:
26132604
import pyarrow as pa
@@ -2674,8 +2665,7 @@ def grouped_func(
26742665
batch = pa.RecordBatch.from_arrays(result_arrays, col_names)
26752666
yield ArrowBatchTransformer.enforce_schema(batch, return_schema)
26762667

2677-
# profiling is not supported for UDF
2678-
return grouped_func, None, ser, ser
2668+
return grouped_func, ser
26792669

26802670
if eval_type == PythonEvalType.SQL_WINDOW_AGG_ARROW_INCREMENTAL_UDF:
26812671
import pyarrow as pa
@@ -2769,8 +2759,7 @@ def grouped_func(
27692759
batch = pa.RecordBatch.from_arrays(result_arrays, col_names)
27702760
yield ArrowBatchTransformer.enforce_schema(batch, return_schema)
27712761

2772-
# profiling is not supported for UDF
2773-
return grouped_func, None, ser, ser
2762+
return grouped_func, ser
27742763

27752764
if eval_type == PythonEvalType.SQL_WINDOW_AGG_PANDAS_UDF:
27762765
import pandas as pd
@@ -2857,8 +2846,7 @@ def grouped_func(
28572846
int_to_decimal_coercion_enabled=runner_conf.int_to_decimal_coercion_enabled,
28582847
)
28592848

2860-
# profiling is not supported for UDF
2861-
return grouped_func, None, ser, ser
2849+
return grouped_func, ser
28622850

28632851
if eval_type == PythonEvalType.SQL_GROUPED_MAP_ARROW_UDF:
28642852
import pyarrow as pa
@@ -2918,8 +2906,7 @@ def grouped_func(
29182906
for batch in result.to_batches():
29192907
yield ArrowBatchTransformer.wrap_struct(batch)
29202908

2921-
# profiling is not supported for UDF
2922-
return grouped_func, None, ser, ser
2909+
return grouped_func, ser
29232910

29242911
if eval_type == PythonEvalType.SQL_GROUPED_MAP_ARROW_ITER_UDF:
29252912
import pyarrow as pa
@@ -2982,8 +2969,7 @@ def grouped_func(
29822969
for _ in value_batches:
29832970
pass
29842971

2985-
# profiling is not supported for UDF
2986-
return grouped_func, None, ser, ser
2972+
return grouped_func, ser
29872973

29882974
if eval_type == PythonEvalType.SQL_GROUPED_MAP_PANDAS_UDF:
29892975
import pandas as pd
@@ -3053,8 +3039,7 @@ def grouped_func(
30533039
)
30543040
del result
30553041

3056-
# profiling is not supported for UDF
3057-
return grouped_func, None, ser, ser
3042+
return grouped_func, ser
30583043

30593044
if eval_type == PythonEvalType.SQL_GROUPED_MAP_PANDAS_ITER_UDF:
30603045
import pandas as pd
@@ -3129,8 +3114,7 @@ def dataframe_iter():
31293114
for _ in group_iter:
31303115
pass
31313116

3132-
# profiling is not supported for UDF
3133-
return grouped_func, None, ser, ser
3117+
return grouped_func, ser
31343118

31353119
if eval_type == PythonEvalType.SQL_COGROUPED_MAP_ARROW_UDF:
31363120
import pyarrow as pa
@@ -3182,8 +3166,7 @@ def cogrouped_func(
31823166
for batch in result.to_batches():
31833167
yield ArrowBatchTransformer.wrap_struct(batch)
31843168

3185-
# profiling is not supported for UDF
3186-
return cogrouped_func, None, ser, ser
3169+
return cogrouped_func, ser
31873170

31883171
if eval_type == PythonEvalType.SQL_MAP_PANDAS_ITER_UDF:
31893172
import pandas as pd
@@ -3259,8 +3242,7 @@ def dataframe_iter():
32593242
int_to_decimal_coercion_enabled=runner_conf.int_to_decimal_coercion_enabled,
32603243
)
32613244

3262-
# profiling is not supported for UDF
3263-
return func, None, ser, ser
3245+
return func, ser
32643246

32653247
if eval_type == PythonEvalType.SQL_COGROUPED_MAP_PANDAS_UDF:
32663248
import pandas as pd
@@ -3328,8 +3310,7 @@ def cogrouped_func(
33283310
)
33293311
del result
33303312

3331-
# profiling is not supported for UDF
3332-
return cogrouped_func, None, ser, ser
3313+
return cogrouped_func, ser
33333314

33343315
if (
33353316
eval_type == PythonEvalType.SQL_ARROW_BATCHED_UDF
@@ -3421,8 +3402,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
34213402

34223403
yield pa.RecordBatch.from_arrays(output_arrays, col_names)
34233404

3424-
# profiling is not supported for UDF
3425-
return func, None, ser, ser
3405+
return func, ser
34263406

34273407
if (
34283408
eval_type == PythonEvalType.SQL_ARROW_BATCHED_UDF
@@ -3511,8 +3491,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
35113491
int_to_decimal_coercion_enabled=runner_conf.int_to_decimal_coercion_enabled,
35123492
)
35133493

3514-
# profiling is not supported for UDF
3515-
return func, None, ser, ser
3494+
return func, ser
35163495

35173496
if eval_type == PythonEvalType.SQL_ARROW_ELEMENTWISE_UDF:
35183497
# This path exchanges data with the JVM over Arrow, so PyArrow is required. Fail with a
@@ -3641,8 +3620,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
36413620

36423621
yield pa.RecordBatch.from_arrays(output_arrays, col_names)
36433622

3644-
# profiling is not supported for UDF
3645-
return func, None, ser, ser
3623+
return func, ser
36463624

36473625
if eval_type in (
36483626
PythonEvalType.SQL_SCALAR_PANDAS_ELEMENTWISE_UDF,
@@ -3767,8 +3745,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
37673745

37683746
yield pa.RecordBatch.from_arrays(output_arrays, col_names)
37693747

3770-
# profiling is not supported for UDF
3771-
return func, None, ser, ser
3748+
return func, ser
37723749

37733750
if eval_type in (
37743751
PythonEvalType.SQL_SCALAR_PANDAS_ITER_ELEMENTWISE_UDF,
@@ -3932,8 +3909,7 @@ def process_results():
39323909

39333910
yield from process_results()
39343911

3935-
# profiling is not supported for UDF
3936-
return func, None, ser, ser
3912+
return func, ser
39373913

39383914
if eval_type == PythonEvalType.SQL_SCALAR_PANDAS_UDF:
39393915
import pandas as pd
@@ -4004,8 +3980,7 @@ def func(split_index: int, data: Iterator[pa.RecordBatch]) -> Iterator[pa.Record
40043980
int_to_decimal_coercion_enabled=runner_conf.int_to_decimal_coercion_enabled,
40053981
)
40063982

4007-
# profiling is not supported for UDF
4008-
return func, None, ser, ser
3983+
return func, ser
40093984

40103985
if eval_type == PythonEvalType.SQL_SCALAR_PANDAS_ITER_UDF:
40113986
import pandas as pd
@@ -4081,8 +4056,7 @@ def process_results():
40814056
# Verify iterator consumed
40824057
verify_iterator_exhausted(args_iter)
40834058

4084-
# profiling is not supported for UDF
4085-
return func, None, ser, ser
4059+
return func, ser
40864060

40874061
if eval_type == PythonEvalType.SQL_TRANSFORM_WITH_STATE_PANDAS_UDF:
40884062
import pandas as pd
@@ -4224,8 +4198,7 @@ def convert_results(result_iter):
42244198
)
42254199
)
42264200

4227-
# profiling is not supported for UDF
4228-
return transform_with_state_func, None, ser, ser
4201+
return transform_with_state_func, ser
42294202

42304203
if eval_type == PythonEvalType.SQL_TRANSFORM_WITH_STATE_PANDAS_INIT_STATE_UDF:
42314204
import pandas as pd
@@ -4437,8 +4410,7 @@ def convert_results(
44374410
)
44384411
)
44394412

4440-
# profiling is not supported for UDF
4441-
return func, None, ser, ser
4413+
return func, ser
44424414

44434415
if eval_type == PythonEvalType.SQL_GROUPED_MAP_PANDAS_UDF_WITH_STATE:
44444416
import pandas as pd
@@ -4771,8 +4743,7 @@ def result_and_state_stream() -> Iterator[tuple]:
47714743
pdfs, pdf_data_cnt, return_type, state_pdfs, state_data_cnt
47724744
)
47734745

4774-
# profiling is not supported for UDF
4775-
return func, None, ser, ser
4746+
return func, ser
47764747

47774748
if eval_type == PythonEvalType.SQL_TRANSFORM_WITH_STATE_PYTHON_ROW_UDF:
47784749
import pyarrow as pa
@@ -4873,8 +4844,7 @@ def convert_results(result_rows: Iterable[Any]) -> Iterator["pa.RecordBatch"]:
48734844
)
48744845
)
48754846

4876-
# profiling is not supported for UDF
4877-
return func, None, ser, ser
4847+
return func, ser
48784848

48794849
if eval_type == PythonEvalType.SQL_TRANSFORM_WITH_STATE_PYTHON_ROW_INIT_STATE_UDF:
48804850
import pyarrow as pa
@@ -5054,8 +5024,7 @@ def convert_results(result_rows: Iterable[Any]) -> Iterator["pa.RecordBatch"]:
50545024
)
50555025
)
50565026

5057-
# profiling is not supported for UDF
5058-
return func, None, ser, ser
5027+
return func, ser
50595028

50605029
elif eval_type == PythonEvalType.SQL_BATCHED_UDF:
50615030
# Plain Python (pickle) UDFs, the only eval type reaching this branch. read_single_udf
@@ -5072,8 +5041,7 @@ def func(split_index: int, data: Iterator[Any]) -> Iterator[Any]:
50725041
for row in data
50735042
)
50745043

5075-
# profiling is not supported for UDF
5076-
return func, None, ser, ser
5044+
return func, ser
50775045

50785046
else:
50795047
raise ValueError("Unknown eval type: {}".format(eval_type))
@@ -5115,21 +5083,24 @@ def invoke_udf(message_receiver: SparkMessageReceiver, outfile: BinaryIO):
51155083
eval_type = init_info.eval_type
51165084
runner_conf = RunnerConf(init_info.runner_conf)
51175085
eval_conf = EvalConf(init_info.eval_conf)
5086+
# UDF and UDTF runners fold profiling into func at construction time (see
5087+
# read_single_udf); only the classic RDD command carries a profiler of its own.
5088+
profiler = None
51185089
if eval_type == PythonEvalType.NON_UDF:
51195090
assert isinstance(init_info.udf_info, (bytes, memoryview))
51205091
func, profiler, deserializer, serializer = read_command(pickleSer, init_info.udf_info)
5121-
elif eval_type in (
5122-
PythonEvalType.SQL_TABLE_UDF,
5123-
PythonEvalType.SQL_ARROW_TABLE_UDF,
5124-
PythonEvalType.SQL_ARROW_UDTF,
5125-
):
5126-
func, profiler, deserializer, serializer = read_udtf(
5127-
pickleSer, init_info.udf_info, eval_type, runner_conf, eval_conf
5128-
)
51295092
else:
5130-
func, profiler, deserializer, serializer = read_udfs(
5093+
# UDF and UDTF runners read input and write output through a single serializer.
5094+
is_udtf = eval_type in (
5095+
PythonEvalType.SQL_TABLE_UDF,
5096+
PythonEvalType.SQL_ARROW_TABLE_UDF,
5097+
PythonEvalType.SQL_ARROW_UDTF,
5098+
)
5099+
read = read_udtf if is_udtf else read_udfs
5100+
func, serializer = read(
51315101
pickleSer, init_info.udf_info, eval_type, runner_conf, eval_conf
51325102
)
5103+
deserializer = serializer
51335104

51345105
init_time = time.time()
51355106

0 commit comments

Comments
 (0)