[FLINK-39986][table-planner][python] Support Python UDF deduplication in projection and condition - #28638
[FLINK-39986][table-planner][python] Support Python UDF deduplication in projection and condition#28638raoraoxiong wants to merge 2 commits into
Conversation
|
hi @liuyongvs @snuyanzin ,please take a look. |
|
01bb759 to
219c281
Compare
|
@flinkbot run azure |
|
@flinkbot re-run azure |
59c030c to
949b109
Compare
@snuyanzin Hi, I've update commits and fixed the CI failure, please take a look if you have a time, thanks |
|
@snuyanzin @liuyongvs Hi, I've update commits and fixed the CI failure, please take a look if you have a time, thanks |
|
would be great to see feedback from someone with python expertise may be @dianfu |
5e41422 to
5c792ba
Compare
dianfu
left a comment
There was a problem hiding this comment.
@raoraoxiong Thanks for this work! The overall direction LGTM.
Could you split this PR to ease the review? For example, at least two PRs: one for cross-Calc reuse and another for the duplication inside one Python operator.
| .cursor | ||
| .claude | ||
| .worktrees | ||
| .codebuddy |
There was a problem hiding this comment.
Unnecessary changes. Please submit in a separate PR.
5c792ba to
1116818
Compare
1116818 to
2518a3b
Compare
2518a3b to
26333c9
Compare
|
@raoraoxiong Hey, thanks for the update. Regarding to the latest PR, it addresses two problems, for problem: |
@dianfu Thanks for the review — agreed, this belongs in the planner. Looking into it, the duplication actually originates in the planner already. So I'll rework this PR to do it as a logical rule instead: split a calc with I'll push the reworked version shortly. |
… in projection and condition Identical deterministic Python UDF calls were shipped to the Python worker once per occurrence, causing redundant cross-process (JVM <-> Python Worker) communication and computation. The duplication originates in the planner: RexProgram already shares structurally identical expressions through RexLocalRef, but the Python calc translation calls RexProgram#expandLocalRef, which expands the shared reference back into independent expression trees. This is now addressed by two logical optimizer rules, so that neither CommonExecPythonCalc nor the projection codegen needs to be aware of it: - RemoteCalcConditionProjectionCseRule rewrites a Calc projection to reference UDF calls already computed by the Calc below it, which removes duplicates shared between a WHERE condition and a SELECT projection. - RemoteCalcProjectionCseRule splits a Calc containing duplicated deterministic calls into a bottom Calc computing each distinct call once plus a top Calc projecting the shared results back into their original positions. Both rules share their reusability predicate through RemoteCalcCseUtil so they agree on which calls may safely share a single evaluation. Non-deterministic calls are always evaluated independently. Generated-by: Claude-4.6-Opus
86ab950 to
1e9203e
Compare
|
@flinkbot run azure |
The previous run failed for infrastructure reasons unrelated to this change: the license check job died on an SSL handshake failure while downloading org.immutables:value from a Maven mirror (Unapproved: 0), and the connect test job was killed by the watchdog after producing no output for 900 seconds while packaging the planner source jar.
What is the purpose of the change
This PR is the first of two PRs implementing common sub-expression elimination (CSE) for Python UDFs (FLINK-39986). Each invocation of a Python UDF involves cross-process communication between the JVM and the Python worker, so duplicated calls are significantly more expensive than duplicated Java expressions.
The duplication actually originates in the planner.
RexProgramalready shares structurally identical expressions throughRexLocalRef, soSELECT udf(a), udf(a)reaches the physical node as a single shared call. It isRexProgram#expandLocalRefin the Python calc translation that expands that shared reference back into independent expression trees, which is what makes the same UDF get shipped to the worker once per occurrence.This PR therefore addresses it with two logical optimizer rules, covering two scenarios:
SELECT udf(a), udf(a)) are computed once and the shared result is projected back into the original positions.RemoteCalcSplitConditionRulesplits a Calc with Python UDFs in its condition, projections reuse the results already computed for the WHERE condition (e.g.SELECT udf(a) + 1 FROM T WHERE udf(a) > 0), including calls nested inside Java expressions.Because the deduplication happens during logical optimization, neither
CommonExecPythonCalcnor the projection codegen needs to be aware of it, and the result is visible inEXPLAINand assertable in plan tests.Non-deterministic calls are never deduplicated and are always evaluated independently.
Deduplication of Python UDF calls nested inside other Python UDF calls (e.g.
udf(udf(a))), which requires extending the JVM-to-worker protocol with result references, is addressed in the follow-up PR #28998.Brief change log
RemoteCalcProjectionCseRule, which splits a Calc containing duplicated deterministic remote calls into a bottom Calc computing each distinct call once plus a top Calc that is a pureRexInputRefprojection restoring the original output schemaRemoteCalcConditionProjectionCseRule, which rewrites a Calc projection to reference remote calls already computed by the Calc below itRemoteCalcCseUtilholding the shared reusability predicate, so both rules agree on which calls may safely share a single evaluationCONDITION_PROJECTION_CSEafterSPLIT_CONDITION, andPROJECTION_CSEafterREWRITE_PROJECTonce the projection has been normalizedRemoteCallFinder, so they are not Python-specific and can be reused for other remote call typesVerifying this change
This change added tests and can be verified as follows:
PythonCalcCseTest(+ plan XML): plan tests covering both rules — duplicates within a projection, duplicates with a forwarded field, duplicates shared between condition and projection, a shared call nested inside another UDF call, plus negative cases (distinct calls, different UDFs and non-deterministic calls are not deduplicated)test_python_local_ref_reuseinflink-python/pyflink/table/tests/test_udf.py: end-to-end tests verifying deterministic calls are reused and non-deterministic calls are not (UUID suffix in UDF output proves whether calls were deduplicated)Does this pull request potentially affect one of the following parts:
@Public(Evolving): (no)Documentation
AI Usage Disclosure
Generated-by: Claude-4.6-Opus