(improvement) serializers: add Cython-optimized serialization for VectorType (up to 30x faster - 823us -> 4us!) - #748
Conversation
…nt.bind() When Cython serializers (from cassandra.serializers) are available and no column encryption policy is active, BoundStatement.bind() now uses pre-built Serializer objects cached on the PreparedStatement instead of calling cqltype classmethods. This avoids per-value Python method dispatch overhead and enables the ~30x vector serialization speedup from the Cython serializers module. The bind loop is split into three paths: 1. Column encryption policy path (unchanged behavior) 2. Cython serializers path (new fast path) 3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check) Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy bind split).
There was a problem hiding this comment.
Pull request overview
This PR introduces a new Cython extension module to accelerate CQL value serialization—especially VectorType—using the same general “typed Serializer object + factory lookup” approach as the existing Cython deserialization stack.
Changes:
- Add
cassandra/serializers.pyximplementing Cython serializers forFloatType,DoubleType,Int32Type, and an optimizedVectorTypeserializer with generic fallback. - Add
find_serializer()/make_serializers()factory helpers for serializer creation. - Add
cassandra/serializers.pxdto expose theSerializerinterface to other Cython modules.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| cassandra/serializers.pyx | New Cython-optimized serialization implementations and factory lookup. |
| cassandra/serializers.pxd | Cython declarations for the Serializer interface. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…nt.bind() When Cython serializers (from cassandra.serializers) are available and no column encryption policy is active, BoundStatement.bind() now uses pre-built Serializer objects cached on the PreparedStatement instead of calling cqltype classmethods. This avoids per-value Python method dispatch overhead and enables the ~30x vector serialization speedup from the Cython serializers module. The bind loop is split into three paths: 1. Column encryption policy path (unchanged behavior) 2. Cython serializers path (new fast path) 3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check) Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy bind split).
…lizers Address all 8 Copilot review comments on PR scylladb#748: - Add _check_float_range() for float overflow detection matching struct.pack - Add _check_int32_range() for int32 bounds checking before C cast - Wire bounds checks into SerFloatType, SerInt32Type, and VectorType fast-paths - Replace malloc/free with PyBytes_FromStringAndSize(NULL,n)+PyBytes_AS_STRING - Add empty vector early return (b'') before allocation - Remove unused uint32_t cimport and libc.stdlib import - Add comprehensive test suite (67 tests) covering equivalence, overflow, special values, vectors, round-trips, and factory functions
There was a problem hiding this comment.
Pull request overview
Adds a new Cython serialization module to speed up VectorType (and a few common scalar subtypes) while keeping wire-format output identical to existing cqltypes.*.serialize() implementations.
Changes:
- Introduce
cassandra/serializers.pyx+.pxdimplementingSerializerclasses, including a specializedSerVectorTypewith float/double/int32 fast-paths. - Add serializer lookup/batch factories (
find_serializer,make_serializers). - Add unit tests validating byte-for-byte equivalence and basic round-trips for the new serializers.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
cassandra/serializers.pyx |
New Cython serializers, including optimized VectorType serialization and factory lookup functions. |
cassandra/serializers.pxd |
Cython declarations for the Serializer base class. |
tests/unit/test_serializers.py |
New unit tests covering scalar/vector equivalence, round-trips, and factory behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…nt.bind() When Cython serializers (from cassandra.serializers) are available and no column encryption policy is active, BoundStatement.bind() now uses pre-built Serializer objects cached on the PreparedStatement instead of calling cqltype classmethods. This avoids per-value Python method dispatch overhead and enables the ~30x vector serialization speedup from the Cython serializers module. The bind loop is split into three paths: 1. Column encryption policy path (unchanged behavior) 2. Cython serializers path (new fast path) 3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check) Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy bind split).
There was a problem hiding this comment.
Pull request overview
Adds a new Cython extension module (cassandra.serializers) that provides optimized serialization implementations for common scalar types and VectorType, intended to mirror the existing deserializers.pyx architecture and improve vector-heavy workloads.
Changes:
- Introduces
cassandra/serializers.pyx+.pxdimplementingSerializerclasses,SerVectorType, and factory helpers (find_serializer,make_serializers). - Adds unit tests validating byte-for-byte equivalence vs. Python
cqltype.serialize()and basic factory behavior. - Implements float/double/int32 vector fast-paths via preallocated contiguous buffers and endian swapping.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
cassandra/serializers.pyx |
New Cython-optimized serializers, including VectorType specialized paths and serializer lookup/factory functions. |
cassandra/serializers.pxd |
Cython declarations for the Serializer base class API. |
tests/unit/test_serializers.py |
New tests for serializer equivalence, edge cases, and factory helpers under Cython. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…lizers Address all 8 Copilot review comments on PR scylladb#748: - Add _check_float_range() for float overflow detection matching struct.pack - Add _check_int32_range() for int32 bounds checking before C cast - Wire bounds checks into SerFloatType, SerInt32Type, and VectorType fast-paths - Replace malloc/free with PyBytes_FromStringAndSize(NULL,n)+PyBytes_AS_STRING - Add empty vector early return (b'') before allocation - Remove unused uint32_t cimport and libc.stdlib import - Add comprehensive test suite (67 tests) covering equivalence, overflow, special values, vectors, round-trips, and factory functions
91fe2ed to
91cd1ef
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
CI flakiness above - fix sent - #767 |
…nt.bind() When Cython serializers (from cassandra.serializers) are available and no column encryption policy is active, BoundStatement.bind() now uses pre-built Serializer objects cached on the PreparedStatement instead of calling cqltype classmethods. This avoids per-value Python method dispatch overhead and enables the ~30x vector serialization speedup from the Cython serializers module. The bind loop is split into three paths: 1. Column encryption policy path (unchanged behavior) 2. Cython serializers path (new fast path) 3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check) Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy bind split).
Restore serializers.pyx/pxd from the PR scylladb#748 branch and add SerDateType that serializes datetime/date/numeric values to 8-byte big-endian int64 millisecond timestamps entirely in C, avoiding Python-level struct.pack. Uses the same timedelta arithmetic as the pure-Python DateType.serialize (Item B) but with C-level int64 byte-swapping. Benchmark shows ~1.5x speedup over the already-optimized Python path for datetime serialization (253 ns vs 381 ns per call).
…lizers Address all 8 Copilot review comments on PR scylladb#748: - Add _check_float_range() for float overflow detection matching struct.pack - Add _check_int32_range() for int32 bounds checking before C cast - Wire bounds checks into SerFloatType, SerInt32Type, and VectorType fast-paths - Replace malloc/free with PyBytes_FromStringAndSize(NULL,n)+PyBytes_AS_STRING - Add empty vector early return (b'') before allocation - Remove unused uint32_t cimport and libc.stdlib import - Add comprehensive test suite (67 tests) covering equivalence, overflow, special values, vectors, round-trips, and factory functions
c77c7b4 to
97cdad0
Compare
…nt.bind() When Cython serializers (from cassandra.serializers) are available and no column encryption policy is active, BoundStatement.bind() now uses pre-built Serializer objects cached on the PreparedStatement instead of calling cqltype classmethods. This avoids per-value Python method dispatch overhead and enables the ~30x vector serialization speedup from the Cython serializers module. The bind loop is split into three paths: 1. Column encryption policy path (unchanged behavior) 2. Cython serializers path (new fast path) 3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check) Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy bind split).
97cdad0 to
de14827
Compare
…nt.bind() When Cython serializers (from cassandra.serializers) are available and no column encryption policy is active, BoundStatement.bind() now uses pre-built Serializer objects cached on the PreparedStatement instead of calling cqltype classmethods. This avoids per-value Python method dispatch overhead and enables the ~30x vector serialization speedup from the Cython serializers module. The bind loop is split into three paths: 1. Column encryption policy path (unchanged behavior) 2. Cython serializers path (new fast path) 3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check) Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy bind split).
Restore serializers.pyx/pxd from the PR scylladb#748 branch and add SerDateType that serializes datetime/date/numeric values to 8-byte big-endian int64 millisecond timestamps entirely in C, avoiding Python-level struct.pack. Uses the same timedelta arithmetic as the pure-Python DateType.serialize (Item B) but with C-level int64 byte-swapping. Benchmark shows ~1.5x speedup over the already-optimized Python path for datetime serialization (253 ns vs 381 ns per call).
…nt.bind() When Cython serializers (from cassandra.serializers) are available and no column encryption policy is active, BoundStatement.bind() now uses pre-built Serializer objects cached on the PreparedStatement instead of calling cqltype classmethods. This avoids per-value Python method dispatch overhead and enables the ~30x vector serialization speedup from the Cython serializers module. The bind loop is split into three paths: 1. Column encryption policy path (unchanged behavior) 2. Cython serializers path (new fast path) 3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check) Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy bind split).
…torType Add cassandra/serializers.pyx and cassandra/serializers.pxd implementing Cython-optimized serialization that mirrors the deserializers.pyx architecture. Implements type-specialized serializers for the three subtypes commonly used in vector columns: - SerFloatType: 4-byte big-endian IEEE 754 float - SerDoubleType: 8-byte big-endian double - SerInt32Type: 4-byte big-endian signed int32 SerVectorType pre-allocates a contiguous buffer and uses C-level byte swapping for float/double/int32 vectors, with a generic fallback for other subtypes. GenericSerializer delegates to the Python-level cqltype.serialize() classmethod. Factory functions find_serializer() and make_serializers() allow easy lookup and batch creation of serializers for column types. Benchmarks show ~30x speedup over the current io.BytesIO baseline and ~3x speedup over Python struct.pack for Vector<float, 1536> serialization. No setup.py changes needed - the existing cassandra/*.pyx glob already picks up new .pyx files. Also, following review: - Drop the unrelated obj_array() empty-input guard from cassandra/deserializers.pyx. ParseDesc.deserializers is a typed `Deserializer[::1]` memoryview assigned unconditionally in ParseDesc.__init__, so an empty list would still crash there regardless of what obj_array() returns -- the guard didn't fix anything reachable, and this PR doesn't otherwise touch deserialization. - Clarify the docstring on serializers.pyx's own (still present, and directly used/tested) obj_array() empty-input guard so it documents that it is defensive only, and does not by itself make an empty-input call safe end-to-end for a hypothetical future caller that assigns the result to a typed ``Serializer[::1]`` memoryview. - Add a regression test locking in that Vector<smallint> stays on the variable-length (vint-length-prefixed) generic fallback rather than being treated as fixed-width: ShortType.serial_size() correctly returns None (no override), matching real Cassandra wire behavior where smallint vector elements are vint-prefixed, not fixed 2-byte values. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a buffer fast-path to SerVectorType for float32/float64/int32 NumPy arrays: bind the input directly as a typed memoryview and byte-swap in place, skipping per-element Python object creation/indexing entirely. Also, following review: - Fix tests/unit/cython/test_serializers.py importing numpy unconditionally at module scope, which would break test collection for the whole file in a Cython-without-numpy environment (numpy is a dev/test dependency, not a hard runtime requirement). Import numpy lazily inside the numpy-only tests and gate them with the shared `numpytest` skip decorator instead of the unconditional module-level import. - Add regression tests for the `buf_size == 0` guard in each buffer fast-path (float/double/int32), using zero-dimension NumPy arrays, so the empty-memoryview-dereference guard doesn't regress silently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
de14827 to
d69ef80
Compare
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
Rebased onto latest Fast-path gating — not applicable here: this PR only introduces the Buffer bounds safety — audited the three ShortType/smallint — not hardcoded as fixed-width: Also cleaned up two smaller issues found during the audit:
All prior review threads were already resolved. Rebuilt the Cython extensions from scratch and ran |
Restore serializers.pyx/pxd from the PR scylladb#748 branch and add SerDateType that serializes datetime/date/numeric values to 8-byte big-endian int64 millisecond timestamps entirely in C, avoiding Python-level struct.pack. Uses the same timedelta arithmetic as the pure-Python DateType.serialize (Item B) but with C-level int64 byte-swapping. Benchmark shows ~1.5x speedup over the already-optimized Python path for datetime serialization (253 ns vs 381 ns per call).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (5)
cassandra/serializers.pyx:554
make_serializers()is documented to return an object-array / typed-memoryview style container, but for empty inputobj_array()returns a plain Python list. This makes the return type value-dependent, which can break callers that always treat the result as a memoryview (especially in Cython code). Prefer returning a consistent type: either (a) always return a Python list frommake_serializers()(and adjust docs/tests accordingly), or (b) return a zero-lengthobject[:]memoryview by allocating a minimal array and slicing to[:0](or any other approach that preserves a memoryview type even when empty).
def make_serializers(cqltypes_list):
cassandra/serializers.pyx:563
make_serializers()is documented to return an object-array / typed-memoryview style container, but for empty inputobj_array()returns a plain Python list. This makes the return type value-dependent, which can break callers that always treat the result as a memoryview (especially in Cython code). Prefer returning a consistent type: either (a) always return a Python list frommake_serializers()(and adjust docs/tests accordingly), or (b) return a zero-lengthobject[:]memoryview by allocating a minimal array and slicing to[:0](or any other approach that preserves a memoryview type even when empty).
return obj_array([find_serializer(ct) for ct in cqltypes_list])
def obj_array(list objs):
cassandra/serializers.pyx:583
make_serializers()is documented to return an object-array / typed-memoryview style container, but for empty inputobj_array()returns a plain Python list. This makes the return type value-dependent, which can break callers that always treat the result as a memoryview (especially in Cython code). Prefer returning a consistent type: either (a) always return a Python list frommake_serializers()(and adjust docs/tests accordingly), or (b) return a zero-lengthobject[:]memoryview by allocating a minimal array and slicing to[:0](or any other approach that preserves a memoryview type even when empty).
if not objs:
return objs
cdef object[:] arr
cdef Py_ssize_t i
arr = cython_array(shape=(len(objs),), itemsize=sizeof(void *), format="O")
for i, obj in enumerate(objs):
arr[i] = obj
return arr
cassandra/serializers.pyx:455
- The int32 NumPy fast-path uses
int[::1], which binds to Cint(platform-dependent width) rather than a guaranteed 32-bit type. Since the serializer is specifically forInt32Type, preferint32_t[::1]to precisely match the wire type and to avoid any edge-case ABI/platform mismatch. This also makes the intent clearer and keeps the typed-memoryview constraint aligned with the advertised int32-only fast-path.
cdef inline object _serialize_int32_buffer(self, object values):
cassandra/serializers.pyx:464
- The int32 NumPy fast-path uses
int[::1], which binds to Cint(platform-dependent width) rather than a guaranteed 32-bit type. Since the serializer is specifically forInt32Type, preferint32_t[::1]to precisely match the wire type and to avoid any edge-case ABI/platform mismatch. This also makes the intent clearer and keeps the typed-memoryview constraint aligned with the advertised int32-only fast-path.
cdef int[::1] view
| cdef bint is_little_endian | ||
| from cassandra.util import is_little_endian |
…nt.bind() When Cython serializers (from cassandra.serializers) are available and no column encryption policy is active, BoundStatement.bind() now uses pre-built Serializer objects cached on the PreparedStatement instead of calling cqltype classmethods. This avoids per-value Python method dispatch overhead and enables the ~30x vector serialization speedup from the Cython serializers module. The bind loop is split into three paths: 1. Column encryption policy path (unchanged behavior) 2. Cython serializers path (new fast path) 3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check) Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy bind split).
Restore serializers.pyx/pxd from the PR scylladb#748 branch and add SerDateType that serializes datetime/date/numeric values to 8-byte big-endian int64 millisecond timestamps entirely in C, avoiding Python-level struct.pack. Uses the same timedelta arithmetic as the pure-Python DateType.serialize (Item B) but with C-level int64 byte-swapping. Benchmark shows ~1.5x speedup over the already-optimized Python path for datetime serialization (253 ns vs 381 ns per call).
Summary
Adds
cassandra/serializers.pyxandcassandra/serializers.pxdimplementing Cython-optimized serialization that mirrors thedeserializers.pyxarchitecture.What's included
SerFloatType(4-byte IEEE 754),SerDoubleType(8-byte),SerInt32Type(4-byte signed) -- the three subtypes commonly used in vector columnsPyBytes_FromStringAndSize(NULL, ...)and uses C-level byte swapping for float/double/int32 vectors, with a generic fallback for other subtypesfloat[::1],double[::1],int[::1]) read directly from the buffer protocol -- bypassing Python object creation entirelycqltype.serialize()classmethod for all other typesfind_serializer(cqltype)andmake_serializers(cqltypes_list)for easy lookup and batch creationArchitecture
Mirrors
deserializers.pyxexactly:Deserializerbase classSerializerbase classDesFloatType,DesDoubleType,DesInt32TypeSerFloatType,SerDoubleType,SerInt32TypeDesVectorType(type-specialized)SerVectorType(type-specialized)GenericDeserializerGenericSerializerfind_deserializer()find_serializer()make_deserializers()make_serializers()Benchmark
Measured with
min()oftimeit.repeat(repeat=7, number=50_000)on a quiet machine (load <2.5).Vector<float, N>serialization, comparing Python baseline (VectorType.serializeviaio.BytesIO) vs CythonSerVectorType.List/tuple inputs
float[128]float[1536]NumPy float32 array inputs (buffer fast-path)
float[128]float[1536]End-to-end: NumPy vs Python baseline
float[128]float[1536]Tests
tests/unit/cython/test_serializers.py