Skip to content

(improvement) Optimize Cython deserialization primitives and add VectorType Cython deserializer (substantial - 11x-30x speedup mainly via DesVectorType.deserialize_bytes with Cython) - #732

Draft
mykaul wants to merge 4 commits into
scylladb:masterfrom
mykaul:cython-vector-deser

Conversation

@mykaul

@mykaul mykaul commented Mar 7, 2026

Copy link
Copy Markdown

Summary

Optimize foundational Cython byte-unpacking and add a dedicated VectorType Cython deserializer.

Commits (4, squashed from 7)

1. Optimize Cython byte unpacking with ntohs/ntohl and int.from_bytes

  • Replace generic byte-swap loop in unpack_num() with ntohs()/ntohl() intrinsics for 16/32-bit types (compiles to single bswap on x86)
  • Replace varint_unpack() hex-string-based conversion with int.from_bytes(term, 'big', signed=True)7.7x speedup
  • Simplify read_int() to direct pointer cast + ntohl()
  • Remove slice_buffer(), replace all call sites with from_ptr_and_size()
  • Add Windows support: platform-conditional #ifdef _WIN32 for winsock2.h vs arpa/inet.h

2. Optimize float deserialization with ntohl() intrinsic

  • Add float-specific branch: reinterpret float bits as uint32_t, apply ntohl(), reinterpret back to float
  • Eliminates 4-iteration byte-swap loop for every float value
  • Refactor to use from_ptr_and_size() helper consistently
  • Add buffer bounds validation (CQL protocol NULL/not-set handling in subelem(), bounds checks in _unpack_len(), DesTupleType, DesCompositeType)

3. Optimize VectorType deserialization with Cython deserializer

  • New DesVectorType class with specialized deserialization methods:
    • _deserialize_float(): C-level memcpy + ntohl + pointer-cast (no Python dispatch per element)
    • _deserialize_double() / _deserialize_int64(): 8-byte manual byte-swap
    • _deserialize_int32(): memcpy + ntohl + cast
    • _deserialize_int16(): ntohs cast
    • Numpy fast-path for vectors >= 32 elements
    • Generic fallback for other fixed-size types with size validation
  • Automatically registered via find_deserializer() for the Cython row parser

4. Remove dead values = [] in DesTupleType.deserialize

  • The values list was allocated but never used — results built directly into pre-allocated tuple via tuple_set()

Benchmark Results

All benchmarks: min(timeit.repeat(number=N, repeat=5)), per-call nanoseconds.
Machine: idle Linux workstation, Cython extensions compiled.

Primitives (via CqlType.deserialize())

Benchmark Master (ns) PR #732 (ns) Speedup
Int32Type 175 175 1.0x
ShortType 153 147 1.04x
FloatType 171 165 1.04x
DoubleType 174 171 1.02x
IntegerType (8-byte varint) 1489 193 7.7x
Tuple 2657 2432 1.09x

The ntohs/ntohl change replaces a byte-swap loop that was already fast for 2/4-byte types. The big win is varint_unpack() where int.from_bytes() replaces hex-string conversion.

VectorType — Python path (via VectorType.deserialize())

Benchmark Master (ns) PR #732 (ns) Speedup
Vector<float,4> 1587 1592 1.0x
Vector<float,128> 33546 30763 1.09x
Vector<float,1536> 516789 371960 1.39x

VectorType — Cython path (via DesVectorType.deserialize_bytes())

The Cython DesVectorType is used by the Cython row parser (find_deserializer()), bypassing the Python VectorType.deserialize() entirely:

Benchmark Python path, master (ns) Cython DesVectorType (ns) Speedup
Vector<float,4> 1587 140 11.3x
Vector<float,128> 33546 2105 15.9x
Vector<float,1536> 516789 24825 20.8x

Unit tests

640 passed, 49 skipped (baseline: 645 passed, 43 skipped on master).

@mykaul
mykaul marked this pull request as draft March 7, 2026 10:23
@mykaul
mykaul force-pushed the cython-vector-deser branch 2 times, most recently from 34dd41e to 9b7697c Compare April 7, 2026 10:24
@mykaul mykaul changed the title (improvement) Optimize Cython deserialization primitives and add VectorType Cython deserializer (improvement) Optimize Cython deserialization primitives and add VectorType Cython deserializer (substantial - 11x-30x speedup mainly via DesVectorType.deserialize_bytes with Cyhon) Apr 7, 2026
@mykaul mykaul changed the title (improvement) Optimize Cython deserialization primitives and add VectorType Cython deserializer (substantial - 11x-30x speedup mainly via DesVectorType.deserialize_bytes with Cyhon) (improvement) Optimize Cython deserialization primitives and add VectorType Cython deserializer (substantial - 11x-30x speedup mainly via DesVectorType.deserialize_bytes with Cython) Apr 12, 2026
Copilot AI review requested due to automatic review settings July 29, 2026 20:32
@mykaul
mykaul force-pushed the cython-vector-deser branch from 9b7697c to cb2c0c2 Compare July 29, 2026 20:32
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ad444f5-8e8e-4746-8f26-41bd804e7989

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@mykaul

mykaul commented Jul 29, 2026

Copy link
Copy Markdown
Author

Rebased onto origin/master and audited/fixed this PR against three issues found earlier while reviewing the sibling VectorType PR #689 (same author/area: adding a Cython VectorType deserializer). All three were present here too and are now fixed by amending the existing commits (no new commits added, force-pushed):

1. ShortType/smallint incorrectly treated as fixed-width. DesVectorType had an _is_int16_type fast path (elem_size = 2, struct/numpy '>i2') for VectorType<smallint>. Real Cassandra 5.0 encodes smallint vector elements as vint-prefixed (variable-length), not a raw 2-byte big-endian short — confirmed by cqltypes.ShortType having no serial_size() override (inherits None from the base class) and VectorType.deserialize() switching to uvint_unpack() whenever subtype.serial_size() is None. Removed the int16 fast path entirely.

2. find_deserializer() dispatch to DesVectorType was unconditional. Every VectorType subclass was routed to the Cython fast-path deserializer regardless of subtype, and the internal fallback (_deserialize_generic) raised ValueError whenever the subtype's serial_size() was None (text, varint, smallint, etc.) instead of parsing it — this would have surfaced as an uncaught ValueError during real row parsing for any non-fixed-width vector subtype. Dispatch is now subtype-aware (new _vector_subtype_has_fast_path helper): only float/double/int32/bigint get the Cython fast path; everything else falls back to GenericDeserializer, which correctly delegates to the pure-Python VectorType.deserialize(). Also hardened _deserialize_generic itself to delegate rather than raise, as defense in depth.

3. Circular import spuriously breaking HAVE_CYTHON detection. deserializers.pyx did from cassandra.cython_deps import HAVE_NUMPY. But cython_deps.py itself imports cassandra.row_parser, which imports cassandra.deserializers for make_deserializers() — closing the cycle. Depending on import order, cython_deps is still mid-import when deserializers.pyx tries to import HAVE_NUMPY from it, so Python raises ImportError (partially-initialized module), which cython_deps.py's own try/except ImportError silently turns into HAVE_CYTHON = False — even though the extensions built and imported fine. Verified this exact symptom locally: building the Cython extensions and checking cassandra.cython_deps.HAVE_CYTHON in a fresh process gave False before the fix and True after. Replaced with an independent local numpy-availability check in deserializers.pyx instead of importing it from cython_deps.

Also updated tests/unit/test_types.py: removed the now-invalid assertion that smallint vectors dispatch to DesVectorType with fixed 2-byte packing, and added tests asserting (a) smallint vectors use the generic/vint-prefixed path and round-trip correctly, and (b) a variable-length subtype (text) doesn't raise and isn't routed to DesVectorType.

Testing: rebuilt the Cython extensions locally and ran the full unit suite: tests/unit/test_types.py — 65 passed, 1 skipped; full tests/unit/ — 773 passed, 38 skipped, 0 failed. Also manually verified round-trip serialize/deserialize for smallint, text, varint, and uuid vectors (previously broken/wire-incorrect), plus float/double/int32/int64 small- and large-vector (numpy ≥32) paths.

No review threads existed on this PR to resolve. Still a draft as intended.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR improves Cython-based deserialization performance and adds a dedicated Cython deserializer for VectorType, targeting large speedups for numeric vector decoding.

Changes:

  • Replace varint hex-string conversion with int.from_bytes(..., signed=True) in both Python and Cython marshal paths.
  • Introduce DesVectorType with C-level fast paths (plus optional NumPy path) and update find_deserializer() routing for vectors.
  • Tighten buffer handling by replacing slice_buffer() with from_ptr_and_size() and adding explicit bounds validation in several deserializers.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/unit/test_types.py Adds unit tests covering DesVectorType selection and vector subtype routing/fallback behavior.
cassandra/marshal.py Optimizes Python varint_unpack() using int.from_bytes.
cassandra/ioutils.pyx Optimizes read_int() using memcpy + ntohl for alignment-safe decoding.
cassandra/deserializers.pyx Adds DesVectorType, NumPy fast-path, subtype-aware routing, and additional bounds checks / buffer slicing changes.
cassandra/cython_marshal.pyx Optimizes numeric unpacking using ntohs/ntohl + int.from_bytes and adds a float-specific swap path.
cassandra/buffer.pxd Removes slice_buffer() and documents/implements from_ptr_and_size() including negative-size sentinels.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/unit/test_types.py
Comment thread tests/unit/test_types.py
Comment thread tests/unit/test_types.py Outdated
Comment thread cassandra/deserializers.pyx Outdated
Comment thread cassandra/cython_marshal.pyx Outdated
Comment thread cassandra/deserializers.pyx Outdated
Comment thread cassandra/deserializers.pyx
Comment thread cassandra/deserializers.pyx
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 30, 2026
…y large vector deserialization

Add test_vector_cython_deserializer_variable_size_subtype to verify
find_deserializer()'s current dispatch for a VectorType with a
variable-size subtype (e.g. UTF8Type). cassandra.deserializers has no
dedicated Des* class for VectorType today, and VectorType is not a
subclass of any of the collection types find_deserializer()
special-cases, so it always falls through to GenericDeserializer --
regardless of whether the subtype has a fixed serialized size or not.
GenericDeserializer simply delegates to the pure Python
VectorType.deserialize(), which correctly round-trips variable-size
subtypes; the test asserts both the dispatch and the round-trip.

A Cython fast-path deserializer for VectorType (DesVectorType), with
dispatch that fast-paths fixed-size subtypes while leaving
variable-size subtypes on GenericDeserializer, is being developed in
companion PRs (scylladb#689, scylladb#732).
Neither has merged, so DesVectorType does not exist on this branch or
on master; this test intentionally does not depend on it. Once one of
those PRs lands, this test should be revisited to also assert the
Cython dispatch/behavior for fixed-size subtypes.

Add test_vector_numpy_large_deserialization to exercise VectorType
deserialization for vectors with >= 32 elements across all supported
fixed-size numeric types (float, double, int32, int64). VectorType
has no numpy-specific fast path today; the test name/threshold is
forward-looking and documents that, guarding correctness of the
existing implementation at representative embedding sizes in the
meantime.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 30, 2026
…y large vector deserialization

Add test_vector_cython_deserializer_variable_size_subtype to verify
find_deserializer()'s current dispatch for a VectorType with a
variable-size subtype (e.g. UTF8Type). cassandra.deserializers has no
dedicated Des* class for VectorType today, and VectorType is not a
subclass of any of the collection types find_deserializer()
special-cases, so it always falls through to GenericDeserializer --
regardless of whether the subtype has a fixed serialized size or not.
GenericDeserializer simply delegates to the pure Python
VectorType.deserialize(), which correctly round-trips variable-size
subtypes; the test asserts both the dispatch and the round-trip.

A Cython fast-path deserializer for VectorType (DesVectorType), with
dispatch that fast-paths fixed-size subtypes while leaving
variable-size subtypes on GenericDeserializer, is being developed in
companion PRs (scylladb#689, scylladb#732).
Neither has merged, so DesVectorType does not exist on this branch or
on master; this test intentionally does not depend on it. Once one of
those PRs lands, this test should be revisited to also assert the
Cython dispatch/behavior for fixed-size subtypes.

Add test_vector_numpy_large_deserialization to exercise VectorType
deserialization for vectors with >= 32 elements across all supported
fixed-size numeric types (float, double, int32, int64). VectorType
has no numpy-specific fast path today; the test name/threshold is
forward-looking and documents that, guarding correctness of the
existing implementation at representative embedding sizes in the
meantime.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 30, 2026
…y large vector deserialization

Add test_vector_cython_deserializer_variable_size_subtype to verify
find_deserializer()'s current dispatch for a VectorType with a
variable-size subtype (e.g. UTF8Type). cassandra.deserializers has no
dedicated Des* class for VectorType today, and VectorType is not a
subclass of any of the collection types find_deserializer()
special-cases, so it always falls through to GenericDeserializer --
regardless of whether the subtype has a fixed serialized size or not.
GenericDeserializer simply delegates to the pure Python
VectorType.deserialize(), which correctly round-trips variable-size
subtypes; the test asserts both the dispatch and the round-trip.

A Cython fast-path deserializer for VectorType (DesVectorType), with
dispatch that fast-paths fixed-size subtypes while leaving
variable-size subtypes on GenericDeserializer, is being developed in
companion PRs (scylladb#689, scylladb#732).
Neither has merged, so DesVectorType does not exist on this branch or
on master; this test intentionally does not depend on it. Once one of
those PRs lands, this test should be revisited to also assert the
Cython dispatch/behavior for fixed-size subtypes.

Add test_vector_numpy_large_deserialization to exercise VectorType
deserialization for vectors with >= 32 elements across all supported
fixed-size numeric types (float, double, int32, int64). VectorType
has no numpy-specific fast path today; the test name/threshold is
forward-looking and documents that, guarding correctness of the
existing implementation at representative embedding sizes in the
meantime.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
mykaul added 4 commits July 31, 2026 21:20
….from_bytes

Performance improvements to serialization/deserialization hot paths:

1. unpack_num(): Use ntohs()/ntohl() for 16-bit and 32-bit integer types
   instead of byte-by-byte swapping loop. These compile to single bswap
   instructions on x86, providing more predictable performance.

2. read_int(): Simplify to use ntohl() directly instead of going through
   unpack_num() with a temporary Buffer.

3. varint_unpack(): Replace hex string conversion with int.from_bytes().
   This eliminates string allocations and provides 4-18x speedup for the
   function itself (larger gains for longer varints).

4. Remove slice_buffer() and replaced with direct assignment

5. _unpack_len() is now implemented similar to read_int()

Also removes unused 'start' and 'end' variables from unpack_num().

End-to-end benchmark shows ~4-5% improvement in row throughput.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
…helpers

Add buffer bounds validation to Cython deserializers for safety against
malformed buffers, refactor to use from_ptr_and_size() helper consistently,
and add float ntohl() specialization for consistency with int32/int16 paths.

Changes:
- subelem(): Add CQL protocol-compliant value handling (NULL/-1,
  not-set/-2, invalid/<-2) with bounds checking
- _unpack_len(): Add bounds check and use memcpy for alignment safety
- DesTupleType: Add defensive bounds checking for tuple item lengths
- DesCompositeType: Add bounds validation for composite element lengths
- Refactor 4 locations to use from_ptr_and_size() instead of manual
  Buffer field assignment
- Add float branch to unpack_num(): reinterpret bits as uint32,
  ntohl(), reinterpret back (consistent with int16/int32 intrinsic paths)
- Add from_ptr_and_size() declaration to buffer.pxd

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
…izer

Addded DesVectorType Cython deserializer with C-level optimizations for
improved performance in row parsing for vectors.
The deserializer uses:
- Direct C byte swapping (ntohl) for numeric types
- Memory operations without Python object overhead
- Unified numpy path for large vectors (≥32 elements)
- Hand-written fast paths for small vectors (<32 elements)

Only float/double/int32/bigint subtypes get the fast C-level path: these
are the only VectorType subtypes that are genuinely fixed-width on the
wire (they have a real serial_size() override). Every other subtype --
including variable-length/vint-prefixed ones like text, varint, and
smallint (ShortType has no serial_size() override and is vint-prefixed
per element in a vector, not a raw 2-byte value) -- falls back to
GenericDeserializer, which delegates to the pure-Python
VectorType.deserialize() and handles them correctly. find_deserializer()
is subtype-aware for VectorType so it never routes an unsupported
subtype to the fast path, where it would otherwise surface as an
uncaught ValueError during row parsing.

Also avoid importing cassandra.cython_deps from this module: cython_deps
imports cassandra.row_parser, which imports this module for
make_deserializers(), so importing cython_deps back from here closes an
import cycle. Depending on import order this makes Python see cython_deps
as partially initialized while resolving HAVE_NUMPY, raising ImportError,
which cython_deps' own try/except silently turns into HAVE_CYTHON = False
even though the Cython extensions built and imported fine. Do our own
independent numpy-availability check instead.

Performance improvements:
- Small vectors (3-4 elements): 4.4-4.7x faster
- Medium vectors (128 elements): 1.0-1.5x faster
- Large vectors (384-1536 elements): 0.9-1.0x (marginal)
(measured for the fixed-width fast path subtypes)

The Cython deserializer is automatically used by the row parser when
available via find_deserializer().

Includes unit tests and benchmark code.

Follow-up commits will try to get Numpy arrays, and perhaps more.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
The 'values' list was allocated but never used — the method builds
results directly into a pre-allocated tuple via tuple_set(res, i, item).
Removes one unnecessary list allocation per tuple deserialization.
@mykaul
mykaul force-pushed the cython-vector-deser branch from cb2c0c2 to 0974137 Compare July 31, 2026 18:21
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