Skip to content

Commit 73a272d

Browse files
Yicong-Huangzhengruifeng
authored andcommitted
[SPARK-56189][PYTHON] Refactor SQL_WINDOW_AGG_ARROW_UDF
### What changes were proposed in this pull request? Refactor `SQL_WINDOW_AGG_ARROW_UDF` to be self-contained in `read_udfs()`, moving bounded/unbounded window logic from wrapper functions and the old mapper into a single execution block that uses `ArrowStreamGroupSerializer` as pure I/O. This is a re-submission of #55123 which was reverted due to CI failure caused by using a non-existent `num_dfs` parameter on `ArrowStreamSerializer`. This version uses `ArrowStreamGroupSerializer` instead. ### Why are the changes needed? Part of [SPARK-55388](https://issues.apache.org/jira/browse/SPARK-55388). ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? Existing tests. ASV micro-benchmarks with `repeat=(3, 5)` show no regression: **SQL_WINDOW_AGG_ARROW_UDF (time)** | Scenario | UDF | Before | After | Change | |---|---|---|---|---| | few_groups_sm | sum | 8.73±0.06ms | 8.70±0.09ms | ~neutral | | few_groups_sm | mean_multi | 7.70±0.2ms | 7.82±0.1ms | ~neutral | | few_groups_lg | sum | 31.6±0.2ms | 31.6±0.5ms | ~neutral | | few_groups_lg | mean_multi | 29.6±0.2ms | 29.6±0.4ms | ~neutral | | many_groups_sm | sum | 244±4ms | 241±4ms | ~neutral | | many_groups_sm | mean_multi | 208±4ms | 206±2ms | ~neutral | | many_groups_lg | sum | 135±0.4ms | 134±0.3ms | ~neutral | | many_groups_lg | mean_multi | 124±2ms | 120±0.6ms | -3% | | wide_cols | sum | 72.7±0.1ms | 69.4±2ms | -5% | | wide_cols | mean_multi | 70.0±0.9ms | 69.9±0.3ms | ~neutral | **Peak memory**: No change (468M-506M for all scenarios). ### Was this patch authored or co-authored using generative AI tooling? No. Closes #55153 from Yicong-Huang/refactor/window-agg-arrow-udf. Authored-by: Yicong-Huang <17627829+Yicong-Huang@users.noreply.github.com> Signed-off-by: Ruifeng Zheng <ruifengz@apache.org>
1 parent b580b4f commit 73a272d

1 file changed

Lines changed: 68 additions & 111 deletions

File tree

python/pyspark/worker.py

Lines changed: 68 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
TransformWithStateInPySparkRowSerializer,
6969
TransformWithStateInPySparkRowInitStateSerializer,
7070
ArrowStreamAggPandasUDFSerializer,
71-
ArrowStreamAggArrowUDFSerializer,
7271
ArrowBatchUDFSerializer,
7372
ArrowStreamUDTFSerializer,
7473
ArrowStreamArrowUDTFSerializer,
@@ -1089,26 +1088,6 @@ def wrap_window_agg_pandas_udf(
10891088
)
10901089

10911090

1092-
def wrap_window_agg_arrow_udf(f, args_offsets, kwargs_offsets, return_type, runner_conf, udf_index):
1093-
window_bound_types_str = runner_conf.get("window_bound_types")
1094-
window_bound_type = [t.strip().lower() for t in window_bound_types_str.split(",")][udf_index]
1095-
if window_bound_type == "bounded":
1096-
return wrap_bounded_window_agg_arrow_udf(
1097-
f, args_offsets, kwargs_offsets, return_type, runner_conf
1098-
)
1099-
elif window_bound_type == "unbounded":
1100-
return wrap_unbounded_window_agg_arrow_udf(
1101-
f, args_offsets, kwargs_offsets, return_type, runner_conf
1102-
)
1103-
else:
1104-
raise PySparkRuntimeError(
1105-
errorClass="INVALID_WINDOW_BOUND_TYPE",
1106-
messageParameters={
1107-
"window_bound_type": window_bound_type,
1108-
},
1109-
)
1110-
1111-
11121091
def wrap_unbounded_window_agg_pandas_udf(f, args_offsets, kwargs_offsets, return_type, runner_conf):
11131092
func, args_kwargs_offsets = wrap_kwargs_support(f, args_offsets, kwargs_offsets)
11141093

@@ -1128,27 +1107,6 @@ def wrapped(*series):
11281107
)
11291108

11301109

1131-
def wrap_unbounded_window_agg_arrow_udf(f, args_offsets, kwargs_offsets, return_type, runner_conf):
1132-
func, args_kwargs_offsets = wrap_kwargs_support(f, args_offsets, kwargs_offsets)
1133-
1134-
# This is similar to wrap_unbounded_window_agg_pandas_udf, the only difference
1135-
# is that this function is for arrow udf.
1136-
arrow_return_type = to_arrow_type(
1137-
return_type, timezone="UTC", prefers_large_types=runner_conf.use_large_var_types
1138-
)
1139-
1140-
def wrapped(*series):
1141-
import pyarrow as pa
1142-
1143-
result = func(*series)
1144-
return pa.repeat(result, len(series[0]))
1145-
1146-
return (
1147-
args_kwargs_offsets,
1148-
lambda *a: (wrapped(*a), arrow_return_type),
1149-
)
1150-
1151-
11521110
def wrap_bounded_window_agg_pandas_udf(f, args_offsets, kwargs_offsets, return_type, runner_conf):
11531111
# args_offsets should have at least 2 for begin_index, end_index.
11541112
assert len(args_offsets) >= 2, len(args_offsets)
@@ -1189,35 +1147,6 @@ def wrapped(begin_index, end_index, *series):
11891147
)
11901148

11911149

1192-
def wrap_bounded_window_agg_arrow_udf(f, args_offsets, kwargs_offsets, return_type, runner_conf):
1193-
# args_offsets should have at least 2 for begin_index, end_index.
1194-
assert len(args_offsets) >= 2, len(args_offsets)
1195-
func, args_kwargs_offsets = wrap_kwargs_support(f, args_offsets[2:], kwargs_offsets)
1196-
1197-
arrow_return_type = to_arrow_type(
1198-
return_type, timezone="UTC", prefers_large_types=runner_conf.use_large_var_types
1199-
)
1200-
1201-
def wrapped(begin_index, end_index, *series):
1202-
import pyarrow as pa
1203-
1204-
assert isinstance(begin_index, pa.Int32Array), type(begin_index)
1205-
assert isinstance(end_index, pa.Int32Array), type(end_index)
1206-
1207-
result = []
1208-
for i in range(len(begin_index)):
1209-
offset = begin_index[i].as_py()
1210-
length = end_index[i].as_py() - offset
1211-
series_slices = [s.slice(offset=offset, length=length) for s in series]
1212-
result.append(func(*series_slices))
1213-
return pa.array(result)
1214-
1215-
return (
1216-
args_offsets[:2] + args_kwargs_offsets,
1217-
lambda *a: (wrapped(*a), arrow_return_type),
1218-
)
1219-
1220-
12211150
def wrap_kwargs_support(f, args_offsets, kwargs_offsets):
12221151
if len(kwargs_offsets):
12231152
keys = list(kwargs_offsets.keys())
@@ -1432,9 +1361,7 @@ def read_single_udf(pickleSer, infile, eval_type, runner_conf, udf_index):
14321361
func, args_offsets, kwargs_offsets, return_type, runner_conf, udf_index
14331362
)
14341363
elif eval_type == PythonEvalType.SQL_WINDOW_AGG_ARROW_UDF:
1435-
return wrap_window_agg_arrow_udf(
1436-
func, args_offsets, kwargs_offsets, return_type, runner_conf, udf_index
1437-
)
1364+
return func, args_offsets, kwargs_offsets, return_type
14381365
elif eval_type == PythonEvalType.SQL_BATCHED_UDF:
14391366
return wrap_udf(func, args_offsets, kwargs_offsets, return_type)
14401367
else:
@@ -2648,7 +2575,7 @@ def read_udfs(pickleSer, infile, eval_type, runner_conf, eval_conf):
26482575
):
26492576
ser = ArrowStreamGroupSerializer(write_start_stream=True)
26502577
elif eval_type == PythonEvalType.SQL_WINDOW_AGG_ARROW_UDF:
2651-
ser = ArrowStreamAggArrowUDFSerializer(safecheck=True, arrow_cast=True)
2578+
ser = ArrowStreamGroupSerializer(write_start_stream=True)
26522579
elif eval_type in (
26532580
PythonEvalType.SQL_GROUPED_AGG_PANDAS_UDF,
26542581
PythonEvalType.SQL_GROUPED_AGG_PANDAS_ITER_UDF,
@@ -2969,6 +2896,72 @@ def func(split_index: int, batches: Iterator[Any]) -> Iterator[pa.RecordBatch]:
29692896
# profiling is not supported for UDF
29702897
return func, None, ser, ser
29712898

2899+
if eval_type == PythonEvalType.SQL_WINDOW_AGG_ARROW_UDF:
2900+
import pyarrow as pa
2901+
2902+
window_bound_types_str = runner_conf.get("window_bound_types")
2903+
window_bound_types = [t.strip().lower() for t in window_bound_types_str.split(",")]
2904+
2905+
col_names = ["_%d" % i for i in range(len(udfs))]
2906+
return_schema = to_arrow_schema(
2907+
StructType([StructField(name, rt) for name, (_, _, _, rt) in zip(col_names, udfs)]),
2908+
timezone="UTC",
2909+
prefers_large_types=runner_conf.use_large_var_types,
2910+
)
2911+
2912+
def func(split_index: int, batches: Iterator[Any]) -> Iterator[pa.RecordBatch]:
2913+
for group_batches in batches:
2914+
batch_list = list(group_batches)
2915+
if not batch_list:
2916+
continue
2917+
if hasattr(pa, "concat_batches"):
2918+
concatenated = pa.concat_batches(batch_list)
2919+
else:
2920+
# pyarrow.concat_batches not supported before 19.0.0
2921+
# remove this once we drop support for old versions
2922+
concatenated = pa.RecordBatch.from_struct_array(
2923+
pa.concat_arrays([b.to_struct_array() for b in batch_list])
2924+
)
2925+
num_rows = concatenated.num_rows
2926+
2927+
result_arrays = []
2928+
for udf_index, (udf_func, args_offsets, kwargs_offsets, _) in enumerate(udfs):
2929+
bound_type = window_bound_types[udf_index]
2930+
if bound_type == "unbounded":
2931+
result = udf_func(
2932+
*[concatenated.column(o) for o in args_offsets],
2933+
**{k: concatenated.column(v) for k, v in kwargs_offsets.items()},
2934+
)
2935+
result_arrays.append(pa.repeat(result, num_rows))
2936+
elif bound_type == "bounded":
2937+
begin_col = concatenated.column(args_offsets[0])
2938+
end_col = concatenated.column(args_offsets[1])
2939+
results = []
2940+
for i in range(num_rows):
2941+
offset = begin_col[i].as_py()
2942+
length = end_col[i].as_py() - offset
2943+
slices = [
2944+
concatenated.column(o).slice(offset=offset, length=length)
2945+
for o in args_offsets[2:]
2946+
]
2947+
kw_slices = {
2948+
k: concatenated.column(v).slice(offset=offset, length=length)
2949+
for k, v in kwargs_offsets.items()
2950+
}
2951+
results.append(udf_func(*slices, **kw_slices))
2952+
result_arrays.append(pa.array(results))
2953+
else:
2954+
raise PySparkRuntimeError(
2955+
errorClass="INVALID_WINDOW_BOUND_TYPE",
2956+
messageParameters={"window_bound_type": bound_type},
2957+
)
2958+
2959+
batch = pa.RecordBatch.from_arrays(result_arrays, col_names)
2960+
yield ArrowBatchTransformer.enforce_schema(batch, return_schema)
2961+
2962+
# profiling is not supported for UDF
2963+
return func, None, ser, ser
2964+
29722965
is_scalar_iter = eval_type == PythonEvalType.SQL_SCALAR_PANDAS_ITER_UDF
29732966
is_map_pandas_iter = eval_type == PythonEvalType.SQL_MAP_PANDAS_ITER_UDF
29742967

@@ -3393,42 +3386,6 @@ def mapper(batch_iter):
33933386
)
33943387
return f(series_iter)
33953388

3396-
elif eval_type == PythonEvalType.SQL_WINDOW_AGG_ARROW_UDF:
3397-
import pyarrow as pa
3398-
3399-
# For SQL_WINDOW_AGG_ARROW_UDF,
3400-
# convert iterator of batch columns to a concatenated RecordBatch
3401-
def mapper(a):
3402-
# a is Iterator[Tuple[pa.Array, ...]] - convert to RecordBatch
3403-
batches = []
3404-
for batch_columns in a:
3405-
# batch_columns is Tuple[pa.Array, ...] - convert to RecordBatch
3406-
batch = pa.RecordBatch.from_arrays(
3407-
batch_columns, names=["_%d" % i for i in range(len(batch_columns))]
3408-
)
3409-
batches.append(batch)
3410-
3411-
# Concatenate all batches into one
3412-
if hasattr(pa, "concat_batches"):
3413-
concatenated_batch = pa.concat_batches(batches)
3414-
else:
3415-
# pyarrow.concat_batches not supported before 19.0.0
3416-
# remove this once we drop support for old versions
3417-
concatenated_batch = pa.RecordBatch.from_struct_array(
3418-
pa.concat_arrays([b.to_struct_array() for b in batches])
3419-
)
3420-
3421-
# Extract series using offsets (concatenated_batch.columns[o] gives pa.Array)
3422-
result = tuple(
3423-
f(*[concatenated_batch.columns[o] for o in arg_offsets]) for arg_offsets, f in udfs
3424-
)
3425-
# In the special case of a single UDF this will return a single result rather
3426-
# than a tuple of results; this is the format that the JVM side expects.
3427-
if len(result) == 1:
3428-
return result[0]
3429-
else:
3430-
return result
3431-
34323389
elif eval_type in (
34333390
PythonEvalType.SQL_GROUPED_AGG_PANDAS_UDF,
34343391
PythonEvalType.SQL_WINDOW_AGG_PANDAS_UDF,

0 commit comments

Comments
 (0)