Skip to content

[Python] Unroll 2-component KV fast path in AbstractComponentCoderImpl - #41216

Draft
damccorm wants to merge 2 commits into
apache:masterfrom
damccorm:perf/py-component-coder-kv-fastpath
Draft

damccorm wants to merge 2 commits into
apache:masterfrom
damccorm:perf/py-component-coder-kv-fastpath

Conversation

@damccorm

Copy link
Copy Markdown
Contributor

Description

Optimizes AbstractComponentCoderImpl in sdks/python/apache_beam/coders/coder_impl.py and sdks/python/apache_beam/coders/coder_impl.pxd:

  1. Unroll 2-component (KV) fast path in encode_to_stream and decode_from_stream:
    • Stores Cython attributes _num_coders, _is_kv, _coder_0, and _coder_1 when len(self._coder_impls) == 2 so 2-tuples (KV pairs) encode and decode directly without range() loop overhead or list comprehension + tuple() allocation on every element.

Benchmarks & Performance Impact

  • Isolated Microbenchmark Rerun (upstream/master vs upstream/master + this PR alone): bench_17_component_coder_kv_fastpath (c2feb961eef) (on branch perf/py-benchmarks-suite) — 500,000 2-tuple (KV) encode + decode round-trips in TupleCoderImpl improved from 220.40 ms -> 124.94 ms (1.76x faster, -43.3%, -95.46 ms) in compiled Cython.
  • Per-Change Attribution (Most to Least Impact, 500,000 calls each in compiled Cython):
    1. Most impact (60.7% of stream savings, -38.66 ms): 2-component encode_to_stream fast path (_coder_0.encode_to_stream / _coder_1.encode_to_stream) — 74.72 ms -> 36.06 ms (2.07x faster, -51.7%).
    2. Second (39.3% of stream savings, -25.06 ms): 2-component decode_from_stream fast path ((_coder_0.decode_from_stream, _coder_1.decode_from_stream) without list comprehension + tuple()) — 62.73 ms -> 37.67 ms (1.67x faster, -39.9%).
  • End-to-End Cloud Dataflow Benchmark (core_window_row_combine): Included in run_core_window_row_combine_pipeline (c2feb961eef) (3,000,000 generated events -> 9,000,000 sliding-windowed elements on a single n2-standard-4 worker), which improved end-to-end worker stage execution time by 1.80x (-44.5%, 599.6s -> 332.9s) and reduced total billable Dataflow vCPU/Memory/PD usage by -26.1% (1,134 -> 838 vCPU-s).

Testing

  • Verified with pytest sdks/python/apache_beam/coders/coders_test.py sdks/python/apache_beam/coders/fast_coders_test.py.

@damccorm

Copy link
Copy Markdown
Contributor Author

/gemini review

@github-actions github-actions Bot added python core size/s S: 10-49 lines changed labels Nov 11, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Review Summary

This PR introduces a Cython-friendly fast path in AbstractComponentCoderImpl for 2-component (KV) tuples by caching _coder_0, _coder_1, _num_coders, and _is_kv at initialization time. The optimization is clean, preserves existing wire format and nesting semantics, and yields substantial throughput gains for KV encoding/decoding.

🔍 General Feedback

  • In decode_from_stream, the non-KV fallback path replaced the c-bound loop variable with self._coder_impls[i].decode_from_stream(...), which loses the CoderImpl static Cython type on tuple indexing and falls back to Python attribute lookup.
  • coder_impl.pxd declares last=int in @cython.locals for get_estimated_size_and_observables, where last is not defined.

💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.

c.decode_from_stream(
in_stream, nested or i + 1 < len(self._coder_impls))
for i, c in enumerate(self._coder_impls)
self._coder_impls[i].decode_from_stream(in_stream, nested or i < last)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium: Because _coder_impls is typed as cdef tuple, indexing self._coder_impls[i] returns an untyped Python object rather than CoderImpl. Previously, for i, c in enumerate(self._coder_impls) bound c to @cython.locals(c=CoderImpl) so c.decode_from_stream(...) used Cython's C-level cpdef vtable dispatch. Casting with cython.cast(CoderImpl, self._coder_impls[i]) preserves the typed C call for non-2-component tuples.

Suggested change
self._coder_impls[i].decode_from_stream(in_stream, nested or i < last)
cython.cast(CoderImpl, self._coder_impls[i]).decode_from_stream(in_stream, nested or i < last)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7e134ed: kept for i, c in enumerate(self._coder_impls) so c remains typed as CoderImpl via @cython.locals(c=CoderImpl, i=int, last=int).

cpdef decode_from_stream(self, InputStream stream, bint nested)

@cython.locals(c=CoderImpl)
@cython.locals(c=CoderImpl, i=int, last=int)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low: get_estimated_size_and_observables in coder_impl.py was not updated to define last, so last=int in @cython.locals here is unused (and c=CoderImpl on line 163 is also unused if decode_from_stream does not bind a local variable c).

Suggested change
@cython.locals(c=CoderImpl, i=int, last=int)
@cython.locals(c=CoderImpl, i=int)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7e134ed: reverted @cython.locals on get_estimated_size_and_observables back to @cython.locals(c=CoderImpl).

@damccorm

Copy link
Copy Markdown
Contributor Author

/gemini review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Review Summary

This PR adds a fast path for 2-component (KV) coders in AbstractComponentCoderImpl.encode_to_stream and AbstractComponentCoderImpl.decode_from_stream, along with corresponding Cython attribute and local variable declarations in coder_impl.pxd. The implementation preserves exact nested encoding/decoding semantics and component length validation while eliminating loop and list-allocation overhead on the hot path.

🔍 General Feedback

  • The 2-component unrolling accurately preserves the nested=True behavior for the first component and forwards nested to the second component.
  • Passing a 2-tuple directly to _construct_from_components in decode_from_stream cleanly takes advantage of TupleCoderImpl._construct_from_components (tuple(components)) returning the existing tuple without extra allocation.
  • Cython cdef attributes and @cython.locals declarations in coder_impl.pxd are properly aligned with the implementation in coder_impl.py.

💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.

@codecov

codecov Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 57.64%. Comparing base (d3078c1) to head (7e134ed).
⚠️ Report is 83 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #41216      +/-   ##
============================================
+ Coverage     57.48%   57.64%   +0.15%     
  Complexity     2492     2492              
============================================
  Files          1204     1198       -6     
  Lines        171265   171215      -50     
  Branches       2103     2103              
============================================
+ Hits          98456    98700     +244     
+ Misses        70157    69863     -294     
  Partials       2652     2652              
Flag Coverage Δ
python 80.55% <100.00%> (+0.33%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@damccorm damccorm added the ready-for-human Draft PR with all checks passing and all review comments resolved, ready for human review label Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core performance python ready-for-human Draft PR with all checks passing and all review comments resolved, ready for human review size/s S: 10-49 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant