Skip to content

[SPARK-60720][CONNECT] Encode guarded pandas string columns directly in Rust - #144

Closed
dbtsai wants to merge 23 commits into
apache:masterfrom
dbtsai:codex/spark-60720-narrow-publication-v13-candidate
Closed

dbtsai wants to merge 23 commits into
apache:masterfrom
dbtsai:codex/spark-60720-narrow-publication-v13-candidate

Conversation

@dbtsai

@dbtsai dbtsai commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Current change after the master rebase

Encode eligible pandas NumPy object-string columns directly into Arrow IPC in Rust for frames of at least 10,000 rows with an explicit native UTF8_BINARY string schema and exact str/nullable None cells. Preserve inline LocalRelation transport, Spark schema and supported fallback/error ordering for subclasses, surrogates, missing values and customized hooks. This remains stacked on PR #121 with current Index-iteration coverage and its inline bridge; PR #118/#108 are already in master. Guards cover a finite set of directly skipped hooks rather than arbitrary pandas customization.

Validation of the rebased change

Validated exact head e31aad6482c1b748924991a8c01aabbea74db385 against upstream master 014674e44470001f5c184846964c131d43f6e4fa. Formatting, 115 core and 419 API Rust unit tests, all API integration-target compilation with and without wasm-udf, the native extension build, and 979 offline/mock Python tests passed (8 skips). Committed whitespace checks passed. Workspace Clippy remains advisory and fails on unmodified master; diagnostics are compared separately in the rebase evidence. Full live-Spark parity and CI for the rewritten head remain pending. No new performance, memory, or RPC-cost measurement is claimed; historical results do not validate this rebased head.

Historical pre-rebase description, validation and performance evidence

The complete previous description is preserved below as historical evidence. Every test count, benchmark number, source/dependency/revision ID, RPC observation, source-equivalence claim, and use of “current”, “final”, “fresh” or “new” in this appendix refers to its original pre-rebase context. Master has since changed local inference and conversion, cached schema access and Arrow configuration handling. The current scope above takes precedence over stale runtime, dependency and per-call RPC statements below. None of these results validates or measures the November 5 rebased head. Prior failures, guard/cold-path costs, RPC/memory observations and live-server limitations remain preserved within their stated historical scope.

What changes were proposed in this pull request?

Pandas object columns containing strings currently materialize Python rows during createDataFrame, even with an explicit string schema. Encode eligible columns directly into Arrow IPC in Rust while preserving the existing inline LocalRelation transport and Spark schema.

The path requires an ordinary pandas DataFrame with at least 10,000 rows, concrete NumPy object storage, an explicit native UTF8_BINARY string schema, and exact str values or nullable None. Customized pandas/schema hooks, string subclasses, unsupported missing values and surrogates retain the existing conversion/error path. The required internal IPC bridge is included in this change.

Depends on PR121, including its PR118/PR108 prerequisites. The narrow delta against PR121 changes five production files and adds 19 test definitions.

Why are the changes needed?

The frozen client-only benchmark compares published PR121 c4284a3e0b71c8d624a1253c52337419761b2131 without the bridge against candidate d31870bc4ceb15c3cadc4bc0a44fcb16f7b5e723 with the complete fix, using separate fresh wheels/environments and unchanged workloads/validators. All 535 samples completed. An independent passive raw artifact audit cross-checks original validator receipts, hashes and all raw metrics; it does not replay Arrow decoding.

Medians below use five worker samples. Each string frame has 10,000 rows and three columns. Each focused warm sample averages 20 measured constructions after three warmups; focused first-construction samples measure one construction after zero warmups.

Case/profile Baseline ms Candidate ms Change
Ordinary pandas 2.3.3 object strings, warm 23.137 2.154 −90.7%
Ordinary pandas 2.3.3 object strings, first construction 24.298 2.935 −87.9%
Focused all-null strings, warm 2.868 0.876 −69.5%
Late subclass fallback, first construction 23.938 25.921 +8.3%
Early surrogate error, warm 1.415 1.659 +17.3%
Middle surrogate error, first construction 13.453 15.252 +13.4%

The middle-surrogate candidate maximum was 21.036 ms versus 13.605 ms baseline. The ordinary pandas 3.1 StringDtype case retains the fallback path and increased 1.95% warm/6.61% first-construction elapsed time. Ordinary Arrow input increased 15.0% in warm elapsed time while CPU decreased 8.4%. Other guarded workloads increased: the million-row pandas float case by 3.93% elapsed/5.04% CPU and UDF construction by 4.27% elapsed/3.92% CPU. Focused CPU medians increased 18.72% for early-surrogate warm errors, 12.17% for middle-surrogate first-construction errors and 7.75% for late-subclass first construction. These five-sample observations do not establish statistical significance or a general overhead bound.

Warm ordinary held-process RSS under pandas 2.3.3 was:

Input Baseline MiB Candidate MiB
Pandas object strings, 10,000 × 3 113.672 107.000
Pandas array values, 10,000 × 3 121.703 121.047
Wide pandas input, 10,000 × 32 118.328 118.953

Measured construction starts after interpreter/library/session/input preparation. Ordinary first-construction timing also follows an untimed version call; the focused worker has no explicit version call. First-construction samples measure one constructor call without constructor warmup. Cold-interpreter costs were not measured. Focused sampled-peak/post-GC RSS was not collected; ordinary post-GC RSS includes later correctness work.

The client is 4.3.0 on Python 3.12.14 with pandas 2.3.3/3.1.0, NumPy 2.5.4, PyArrow 26.0.0 and grpcio 1.84.1. A separate official PySpark 5.0.0.dev0 reference at Apache Spark ca1514a2b4e72f9529871f34d09881cd4921df82 is measured under pandas 2.3.3 only. The Rust-client and official PySpark versions differ, so the official result provides separate context. Improvements above compare the two Rust-client revisions; historical minimal/combined-tree timings were not substituted.

Does this PR introduce any user-facing change?

Eligible pandas string input uses the column encoder. Tests preserve values, nulls, field names/metadata, error ordering and inline transport/configuration behavior. Paired ordinary 20-counter RPC maps are equal. Focused intercepted RPC method maps are zero; their separate inferred zero 20-counter maps do not establish a complete end-to-end transport observation.

How was this patch tested?

  • Both exact revisions pass formatting and release library tests: 394 client tests and 95 core tests each. Native and host wasm-udf feature test compilation each produce 37 test executables; no WASM-target execution is claimed.
  • Fresh wheels pass complete same-source test_arrow_handoff.py and test_dropin_offline.py using pytest --rootdir=python --import-mode=importlib -q -rs. Baseline: 522 passed/9 skipped for each pandas version. Candidate: 681 passed/9 skipped with pandas 2.3.3; 679 passed/11 skipped with pandas 3.1.0. The two extra skips concern unavailable older infer-string storage behavior.
  • All 531 baseline collected cases/outcomes are preserved. The 19 added definitions cover 159 cases per pandas version, including complete IPC cells/schema, subclass/surrogate fallback, descriptors/aliases, normalization and backing-array hooks, column storage and large inline input.
  • An independent passive raw artifact audit cross-checks original validator receipts/hashes and all raw metrics for all 535 samples: 430 IPC, 60 expected-error and 45 guard samples, with raw requests/IPC, distributions and adverse costs retained. It does not replay Arrow decoding. All six supervised process groups are absent before releasing the shared lock; four actual role packages and the separate official reference retain their required identities.

Package checks cover existing dependency RECORD-listed non-bytecode files and recorded PySpark import origins; they do not establish continuous isolation, unlisted/shadow dependencies or loaded dependency origins. Probe receipts cover outer role/reference commands; nested internal check_output partial stdout is not separately receipted.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: OpenAI Codex (GPT-6)

This pull request and its description were written by Isaac.

dbtsai and others added 23 commits November 5, 2026 15:59
…mantics

Use explicit LongType schema names when constructing Arrow columns. Limit the
columnar path to concrete pandas/NumPy arrays and native schema descriptors,
including authentic converter aliases retained by already imported modules.
Keep nullable values in non-nullable fields on the native row path so local
validation still precedes configuration RPCs. Support warm Arrow importers that
resolve converters locally.

Add mock-RPC regressions for all emitted integer cells, schema metadata and
nullability, custom conversion hooks, and repeated warm construction.

Co-authored-by: Isaac <no-reply@databricks.com>
Build the subclass-label fallback fixture with an explicit object Index and
assert its concrete label type. Pandas 3 otherwise normalizes constructor
labels to builtin strings before the unchanged integer helper sees them.

The original regression passes on pandas 2.3.3 and fails on 3.1.0; the fixed
regression and full CI-selected Arrow/drop-in suites pass on both versions.
Runtime Python and native sources remain unchanged.

Co-authored-by: Isaac <no-reply@databricks.com>
Decline noncanonical columns indexes and their direct string-storage hooks before the integer fast path inspects labels or normalizes cells. Keep native tolist errors and mutations in their original order and cover public construction on both pandas versions.

Co-authored-by: Isaac <no-reply@databricks.com>
Decline class-level columns storage descriptors before reading labels. Keep the single native columns storage read before pandas normalization, and cover public construction with inferred, named and explicit schemas on both pandas versions.

Co-authored-by: Isaac <no-reply@databricks.com>
Avoid loading optional Arrow label modules for fallback inputs, and decline
integer Arrow conversion when the loaded importer AtomicType alias is absent.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
@HyukjinKwon

Copy link
Copy Markdown
Member

Thank you for this. As agreed with the maintainers, it continues in #208: this branch does not allow edits by maintainers, so #208 carries its commits plus the merge with master (which now has #121) and with #207, the replacement of its sibling #143, as the two share the inline IPC bridge and the dispatcher, and the review changes (encoding each string once instead of twice, dropping the converter checks the string path never reaches with the tests that only exercised them, and relaxing the test that pinned master's pandas read counts). It keeps SPARK-60720. #145, stacked on this, stays open.

@HyukjinKwon HyukjinKwon closed this Nov 6, 2026
HyukjinKwon added a commit to dbtsai/spark-connect-rust that referenced this pull request Nov 6, 2026
Stack this on the replacements of apache#143 and apache#144 (apache#207 and apache#208), on the current master,
which has apache#121, apache#128, apache#130 and apache#134 that this branch carried copies of: take the stacked
branch's files, and add the date hook after the string one, and the date functions and
tests after theirs.

Co-authored-by: Isaac <no-reply@databricks.com>
HyukjinKwon added a commit that referenced this pull request Nov 6, 2026
…in Rust

### What changes were proposed in this pull request?

This replaces #144 by DB Tsai, which does not allow edits by maintainers, with its commits plus the merge with master and the review changes below. It is stacked on #207 (SPARK-60727), with which it shares the inline IPC bridge and the dispatcher; until #207 merges, its diff includes #207's.

`createDataFrame(pandas_df, schema)`, for a pandas DataFrame of at least 10,000 rows whose columns are all object columns of exact `str` values and `None`, with an explicit `StructType` of `StringType` (UTF8_BINARY) fields, now encodes the Arrow IPC in Rust (`_pandas_string_ipc` in `crates/pyspark-rs/src/pandas_input.rs`) from the NumPy object arrays, and sends it inline as the row path does, instead of materializing Python rows.

Everything else keeps the row path, with its errors: subclasses of `str`, other objects (including `NaN`, `pd.NA` and numbers), `None` in a non-nullable field, strings with lone surrogates (which cannot be encoded to UTF-8), other collations, smaller frames, other dtypes and schemas, and customized pandas or `pyspark.sql.types` classes, as for #121 (SPARK-60676).

Review changes on top of #144:
- `_pandas_string_ipc` encoded every string to UTF-8 twice, in a validation pass and while building; it now builds in one pass and falls back on the first string that cannot be encoded.
- Dropped the checks of the aliases of the Arrow importer and of `pyspark.sql.pandas.types`: the path calls the Rust encoder with the native schema and never calls them. The two tests that only exercised them went with them.
- A test that required a second row conversion to read `Index._data` exactly as often as the first now checks that the path declines without more reads than the row path, as for #121.

### Why are the changes needed?

[SPARK-60720](https://issues.apache.org/jira/browse/SPARK-60720): converting each string through Python rows dominates `createDataFrame` of large text frames. Measured on macOS against a local Spark 4.3.0 server, as the client CPU time of `createDataFrame` of 200,000 rows of three string columns (ASCII, empty, NUL, accents, CJK, emoji, combining marks and nulls) with an explicit schema (median of 5): 216 ms on the current master, 60 ms with #144 as it was, and 43 ms with this change (official PySpark 4.3.0: 51 ms). All give the same rows.

### Does this PR introduce _any_ user-facing change?

No, other than speed.

### How was this patch tested?

- New tests in `python/tests/test_arrow_handoff.py`, against the mock Arrow input service, check every IPC cell (Unicode, NUL, empty strings, nulls, all-null columns), field names, nullability and metadata, the inline transport with any relation-cache setting, that no configuration is read, that surrogates fall back before any configuration read, and that each unsupported or customized input keeps the row path with its errors.
- Against the current master, merged into #207 and this branch, and a local Spark 4.3.0 server: the lib and every integration test of `apache-spark-connect` and `apache-spark-connect-core`, e2e included (789 passed), the build of all tests with `wasm-udf`, the offline Python suites (1,614 passed, 8 skipped), the live drop-in and Arrow suites (86 passed) and the live cases (10 passed). `cargo fmt --all --check` passes.

### Was this patch authored or co-authored using generative AI tooling?

Generated-by: OpenAI Codex (GPT-6); the merge with master, the review changes and the measurement by Claude Opus 5.5

This pull request and its description were written by Isaac.

Closes #208 from HyukjinKwon/SPARK-60720-pandas-strings.

Lead-authored-by: DB Tsai <dbtsai@dbtsai.com>
Co-authored-by: DB Tsai <db.tsai@databricks.com>
Co-authored-by: Hyukjin Kwon <hyukjin.kwon@databricks.com>
Signed-off-by: Hyukjin Kwon <hyukjin.kwon@databricks.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants