This is an automated email from the ASF dual-hosted git repository.
Yicong-Huang pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 15833ed8df2f [SPARK-58243][PYTHON] Remove unused
ApplyInPandasWithStateSerializer
15833ed8df2f is described below
commit 15833ed8df2f8e3512b269413107cec5d3feb03d
Author: Yicong Huang <[email protected]>
AuthorDate: Wed Jul 22 16:30:04 2026 +0000
[SPARK-58243][PYTHON] Remove unused ApplyInPandasWithStateSerializer
### What changes were proposed in this pull request?
Delete the now-unused `ApplyInPandasWithStateSerializer`. After SPARK-58189
refactored `SQL_GROUPED_MAP_PANDAS_UDF_WITH_STATE` to extract its logic into
`read_udfs()` (state management and Pandas conversion moved into the wrapper)
and use `ArrowStreamSerializer` directly, this class is dead code. Imports left
unused by the deletion (`CPickleSerializer`, `to_arrow_type`, `StringType`,
`BinaryType`, `LongType`, `IntegerType`) are removed, and a stale class-name
reference in a benchmark [...]
Part of SPARK-55384.
### Why are the changes needed?
The class is no longer referenced anywhere; removing it and its dead
imports keeps `serializers.py` lean.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Existing tests. No behavior change.
`pyspark.sql.tests.pandas.test_pandas_grouped_map_with_state` passes.
### Was this patch authored or co-authored using generative AI tooling?
No.
Closes #57409 from Yicong-Huang/SPARK-58243.
Authored-by: Yicong Huang <[email protected]>
Signed-off-by: Yicong-Huang <[email protected]>
(cherry picked from commit 600e36181180be3f62c88cd4eb1529411fd095c7)
Signed-off-by: Yicong-Huang <[email protected]>
---
python/benchmarks/bench_eval_type.py | 6 +-
python/pyspark/sql/pandas/serializers.py | 407 -------------------------------
2 files changed, 3 insertions(+), 410 deletions(-)
diff --git a/python/benchmarks/bench_eval_type.py
b/python/benchmarks/bench_eval_type.py
index 1632d9a85725..c7fe702930a1 100644
--- a/python/benchmarks/bench_eval_type.py
+++ b/python/benchmarks/bench_eval_type.py
@@ -2500,8 +2500,8 @@ class TransformWithStateRowInitStateUDFPeakmemBench(
# ``(key, pdfs, state)`` and returns ``Iterator[pandas.DataFrame]``, where
# ``state`` is a ``GroupState`` the UDF may read (``getOption``) and write
# (``update``/``remove``). Unlike TransformWithState, no state server socket is
-# involved: ``ApplyInPandasWithStateSerializer`` reconstructs each
``GroupState``
-# entirely from a metadata column carried inline in the Arrow stream.
+# involved: each ``GroupState`` is reconstructed entirely from a metadata
column
+# carried inline in the Arrow stream.
#
# The wire stream is a single plain Arrow IPC stream whose batch schema is the
# data columns followed by one trailing struct column (``__state``, matching
the
@@ -2510,7 +2510,7 @@ class TransformWithStateRowInitStateUDFPeakmemBench(
# state value), ``startOffset``, ``numRows``, ``isLastChunk``. Data and state
# columns must share a row count, so exactly one populated state row per data
# chunk sits at the top of each group's range and the remaining state rows are
-# null structs (which the serializer treats as end-of-data padding).
+# null structs (treated as end-of-data padding when read back).
class _ApplyInPandasWithStateBenchMixin:
diff --git a/python/pyspark/sql/pandas/serializers.py
b/python/pyspark/sql/pandas/serializers.py
index 0b3b143bcf2c..c035a20e37af 100644
--- a/python/pyspark/sql/pandas/serializers.py
+++ b/python/pyspark/sql/pandas/serializers.py
@@ -28,22 +28,16 @@ from pyspark.serializers import (
read_int,
write_int,
UTF8Deserializer,
- CPickleSerializer,
)
from pyspark.sql import Row
from pyspark.sql.conversion import (
ArrowBatchTransformer,
PandasToArrowConversion,
)
-from pyspark.sql.pandas.types import to_arrow_type
from pyspark.sql.types import (
DataType,
- StringType,
StructType,
- BinaryType,
StructField,
- LongType,
- IntegerType,
)
if TYPE_CHECKING:
@@ -435,407 +429,6 @@ class
ArrowStreamPandasUDFSerializer(ArrowStreamPandasSerializer):
return "ArrowStreamPandasUDFSerializer"
-class ApplyInPandasWithStateSerializer(ArrowStreamPandasUDFSerializer):
- """
- Serializer used by Python worker to evaluate UDF for
applyInPandasWithState.
-
- Parameters
- ----------
- timezone : str
- A timezone to respect when handling timestamp values
- safecheck : bool
- If True, conversion from Arrow to Pandas checks for overflow/truncation
- assign_cols_by_name : bool
- If True, then Pandas DataFrames will get columns by name
- state_object_schema : StructType
- The type of state object represented as Spark SQL type
- arrow_max_records_per_batch : int
- Limit of the number of records that can be written to a single
ArrowRecordBatch in memory.
- """
-
- def __init__(
- self,
- *,
- timezone,
- safecheck,
- assign_cols_by_name,
- prefer_int_ext_dtype,
- state_object_schema,
- arrow_max_records_per_batch,
- prefers_large_var_types,
- int_to_decimal_coercion_enabled,
- ):
- super().__init__(
- timezone=timezone,
- safecheck=safecheck,
- assign_cols_by_name=assign_cols_by_name,
- df_for_struct=False,
- struct_in_pandas="dict",
- ndarray_as_list=False,
- prefer_int_ext_dtype=prefer_int_ext_dtype,
- arrow_cast=True,
- input_type=None,
- int_to_decimal_coercion_enabled=int_to_decimal_coercion_enabled,
- prefers_large_types=prefers_large_var_types,
- )
- self.pickleSer = CPickleSerializer()
- self.utf8_deserializer = UTF8Deserializer()
- self.state_object_schema = state_object_schema
-
- self.result_count_df_type = StructType(
- [
- StructField("dataCount", IntegerType()),
- StructField("stateCount", IntegerType()),
- ]
- )
-
- self.result_count_pdf_arrow_type = to_arrow_type(
- self.result_count_df_type, timezone="UTC",
prefers_large_types=prefers_large_var_types
- )
-
- self.result_state_df_type = StructType(
- [
- StructField("properties", StringType()),
- StructField("keyRowAsUnsafe", BinaryType()),
- StructField("object", BinaryType()),
- StructField("oldTimeoutTimestamp", LongType()),
- ]
- )
-
- self.result_state_pdf_arrow_type = to_arrow_type(
- self.result_state_df_type, timezone="UTC",
prefers_large_types=prefers_large_var_types
- )
- self.arrow_max_records_per_batch = (
- arrow_max_records_per_batch if arrow_max_records_per_batch > 0
else 2**31 - 1
- )
-
- def load_stream(self, stream):
- """
- Read ArrowRecordBatches from stream, deserialize them to populate a
list of pair
- (data chunk, state), and convert the data into a list of pandas.Series.
-
- Please refer the doc of inner function `gen_data_and_state` for more
details how
- this function works in overall.
-
- In addition, this function further groups the return of
`gen_data_and_state` by the state
- instance (same semantic as grouping by grouping key) and produces an
iterator of data
- chunks for each group, so that the caller can lazily materialize the
data chunk.
- """
-
- import pyarrow as pa
- import json
- from itertools import groupby
- from pyspark.sql.streaming.state import GroupState
-
- def construct_state(state_info_col):
- """
- Construct state instance from the value of state information
column.
- """
-
- state_info_col_properties = state_info_col["properties"]
- state_info_col_key_row = state_info_col["keyRowAsUnsafe"]
- state_info_col_object = state_info_col["object"]
-
- state_properties = json.loads(state_info_col_properties)
- if state_info_col_object:
- state_object = self.pickleSer.loads(state_info_col_object)
- else:
- state_object = None
- state_properties["optionalValue"] = state_object
-
- return GroupState(
- keyAsUnsafe=state_info_col_key_row,
- valueSchema=self.state_object_schema,
- **state_properties,
- )
-
- def gen_data_and_state(batches):
- """
- Deserialize ArrowRecordBatches and return a generator of
- `(a list of pandas.Series, state)`.
-
- The logic on deserialization is following:
-
- 1. Read the entire data part from Arrow RecordBatch.
- 2. Read the entire state information part from Arrow RecordBatch.
- 3. Loop through each state information:
- 3.A. Extract the data out from entire data via the information
of data range.
- 3.B. Construct a new state instance if the state information is
the first occurrence
- for the current grouping key.
- 3.C. Leverage the existing state instance if it is already
available for the current
- grouping key. (Meaning it's not the first occurrence.)
- 3.D. Remove the cache of state instance if the state
information denotes the data is
- the last chunk for current grouping key.
-
- This deserialization logic assumes that Arrow RecordBatches
contain the data with the
- ordering that data chunks for same grouping key will appear
sequentially.
-
- This function must avoid materializing multiple Arrow
RecordBatches into memory at the
- same time. And data chunks from the same grouping key should
appear sequentially, to
- further group them based on state instance (same state instance
will be produced for
- same grouping key).
- """
-
- state_for_current_group = None
-
- for batch in batches:
- batch_schema = batch.schema
- data_schema = pa.schema([batch_schema[i] for i in range(0,
len(batch_schema) - 1)])
- state_schema = pa.schema(
- [
- batch_schema[-1],
- ]
- )
-
- batch_columns = batch.columns
- data_columns = batch_columns[0:-1]
- state_column = batch_columns[-1]
-
- data_batch = pa.RecordBatch.from_arrays(data_columns,
schema=data_schema)
- state_batch = pa.RecordBatch.from_arrays(
- [
- state_column,
- ],
- schema=state_schema,
- )
-
- state_pandas = ArrowBatchTransformer.to_pandas(
- state_batch,
- timezone=self._timezone,
- schema=None,
- struct_in_pandas=self._struct_in_pandas,
- ndarray_as_list=self._ndarray_as_list,
- prefer_int_ext_dtype=self._prefer_int_ext_dtype,
- df_for_struct=self._df_for_struct,
- )[0]
-
- for state_idx in range(0, len(state_pandas)):
- state_info_col = state_pandas.iloc[state_idx]
-
- if not state_info_col:
- # no more data with grouping key + state
- break
-
- data_start_offset = state_info_col["startOffset"]
- num_data_rows = state_info_col["numRows"]
- is_last_chunk = state_info_col["isLastChunk"]
-
- if state_for_current_group:
- # use the state, we already have state for same group
and there should be
- # some data in same group being processed earlier
- state = state_for_current_group
- else:
- # there is no state being stored for same group,
construct one
- state = construct_state(state_info_col)
-
- if is_last_chunk:
- # discard the state being cached for same group
- state_for_current_group = None
- elif not state_for_current_group:
- # there's no cached state but expected to have
additional data in same group
- # cache the current state
- state_for_current_group = state
-
- data_batch_for_group = data_batch.slice(data_start_offset,
num_data_rows)
- data_pandas = ArrowBatchTransformer.to_pandas(
- data_batch_for_group,
- timezone=self._timezone,
- schema=None,
- struct_in_pandas=self._struct_in_pandas,
- ndarray_as_list=self._ndarray_as_list,
- prefer_int_ext_dtype=self._prefer_int_ext_dtype,
- df_for_struct=self._df_for_struct,
- )
-
- # state info
- yield (
- data_pandas,
- state,
- )
-
- _batches = super(ArrowStreamPandasSerializer, self).load_stream(stream)
-
- data_state_generator = gen_data_and_state(_batches)
-
- # state will be same object for same grouping key
- for _state, _data in groupby(data_state_generator, key=lambda x: x[1]):
- yield (
- _data,
- _state,
- )
-
- def dump_stream(self, iterator, stream):
- """
- Read through an iterator of (iterator of pandas DataFrame, state),
serialize them to Arrow
- RecordBatches, and write batches to stream.
- """
-
- import pandas as pd
-
- def construct_state_pdf(state):
- """
- Construct a pandas DataFrame from the state instance.
- """
-
- state_properties = state.json().encode("utf-8")
- state_key_row_as_binary = state._keyAsUnsafe
- if state.exists:
- state_object =
self.pickleSer.dumps(state._value_schema.toInternal(state._value))
- else:
- state_object = None
- state_old_timeout_timestamp = state.oldTimeoutTimestamp
-
- state_dict = {
- "properties": [
- state_properties,
- ],
- "keyRowAsUnsafe": [
- state_key_row_as_binary,
- ],
- "object": [
- state_object,
- ],
- "oldTimeoutTimestamp": [
- state_old_timeout_timestamp,
- ],
- }
-
- return pd.DataFrame.from_dict(state_dict)
-
- def construct_record_batch(pdfs, pdf_data_cnt, pdf_schema, state_pdfs,
state_data_cnt):
- """
- Construct a new Arrow RecordBatch based on output pandas
DataFrames and states. Each
- one matches to the single struct field for Arrow schema. We also
need an extra one to
- indicate array length for data and state, so the return value of
Arrow RecordBatch will
- have schema with three fields, in `count`, `data`, `state` order.
- (Readers are expected to access the field via position rather than
the name. We do
- not guarantee the name of the field.)
-
- Note that Arrow RecordBatch requires all columns to have all same
number of rows,
- hence this function inserts empty data for count/state/data with
less elements to
- compensate.
- """
-
- max_data_cnt = max(1, max(pdf_data_cnt, state_data_cnt))
-
- # We only use the first row in the count column, and fill other
rows to be the same
- # value, hoping it is more friendly for compression, in case it is
needed.
- count_dict = {
- "dataCount": [pdf_data_cnt] * max_data_cnt,
- "stateCount": [state_data_cnt] * max_data_cnt,
- }
- count_pdf = pd.DataFrame.from_dict(count_dict)
-
- empty_row_cnt_in_data = max_data_cnt - pdf_data_cnt
- empty_row_cnt_in_state = max_data_cnt - state_data_cnt
-
- empty_rows_pdf = pd.DataFrame(
- dict.fromkeys(pdf_schema.names),
- index=[x for x in range(0, empty_row_cnt_in_data)],
- )
- empty_rows_state = pd.DataFrame(
- columns=["properties", "keyRowAsUnsafe", "object",
"oldTimeoutTimestamp"],
- index=[x for x in range(0, empty_row_cnt_in_state)],
- )
-
- pdfs.append(empty_rows_pdf)
- state_pdfs.append(empty_rows_state)
-
- merged_pdf = pd.concat(pdfs, ignore_index=True)
- merged_state_pdf = pd.concat(state_pdfs, ignore_index=True)
-
- # Create batch from list of DataFrames, each wrapped as a
StructArray.
- # Schema fields map to: _0=count, _1=output data, _2=state data
- # (types defined in __init__: result_count_df_type, pdf_schema,
result_state_df_type)
- data = [count_pdf, merged_pdf, merged_state_pdf]
- schema = StructType(
- [
- StructField("_0", self.result_count_df_type),
- StructField("_1", pdf_schema),
- StructField("_2", self.result_state_df_type),
- ]
- )
- return PandasToArrowConversion.convert(
- data,
- schema,
- timezone=self._timezone,
- safecheck=self._safecheck,
- arrow_cast=self._arrow_cast,
- assign_cols_by_name=self._assign_cols_by_name,
-
int_to_decimal_coercion_enabled=self._int_to_decimal_coercion_enabled,
- )
-
- def serialize_batches():
- """
- Read through an iterator of (iterator of pandas DataFrame, state),
and serialize them
- to Arrow RecordBatches.
-
- This function does batching on constructing the Arrow RecordBatch;
a batch will be
- serialized to the Arrow RecordBatch when the total number of
records exceeds the
- configured threshold.
- """
- # a set of variables for the state of current batch which will be
converted to Arrow
- # RecordBatch.
- pdfs = []
- state_pdfs = []
- pdf_data_cnt = 0
- state_data_cnt = 0
-
- return_schema = None
-
- for data in iterator:
- # data represents the result of each call of user function
- packaged_result = data[0]
-
- # There are two results from the call of user function:
- # 1) iterator of pandas DataFrame (output)
- # 2) updated state instance
- pdf_iter = packaged_result[0][0]
- state = packaged_result[0][1]
-
- # This is static and won't change across batches.
- return_schema = packaged_result[1]
-
- for pdf in pdf_iter:
- # We ignore empty pandas DataFrame.
- if len(pdf) > 0:
- pdf_data_cnt += len(pdf)
- pdfs.append(pdf)
-
- # If the total number of records in current batch
exceeds the configured
- # threshold, time to construct the Arrow RecordBatch
from the batch.
- if pdf_data_cnt > self.arrow_max_records_per_batch:
- batch = construct_record_batch(
- pdfs, pdf_data_cnt, return_schema, state_pdfs,
state_data_cnt
- )
-
- # Reset the variables to start with new batch for
further data.
- pdfs = []
- state_pdfs = []
- pdf_data_cnt = 0
- state_data_cnt = 0
-
- yield batch
-
- # This has to be performed 'after' evaluating all elements in
iterator, so that
- # the user function has been completed and the state is
guaranteed to be updated.
- state_pdf = construct_state_pdf(state)
-
- state_pdfs.append(state_pdf)
- state_data_cnt += 1
-
- # processed all output, but current batch may not be flushed yet.
- if pdf_data_cnt > 0 or state_data_cnt > 0:
- batch = construct_record_batch(
- pdfs, pdf_data_cnt, return_schema, state_pdfs,
state_data_cnt
- )
-
- yield batch
-
- batches = self._write_stream_start(serialize_batches(), stream)
- return ArrowStreamSerializer.dump_stream(self, batches, stream)
-
-
class TransformWithStateInPySparkRowSerializer(ArrowStreamUDFSerializer):
"""
Serializer used by Python worker to evaluate UDF for
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]