Skip to content

feat: NumPy-accelerated vector serialization in VectorType (huge improvements in some use cases 60us -> 0.6us) - #793

Draft
mykaul wants to merge 3 commits into
scylladb:masterfrom
mykaul:numpy-vector-serialize
Draft

feat: NumPy-accelerated vector serialization in VectorType (huge improvements in some use cases 60us -> 0.6us)#793
mykaul wants to merge 3 commits into
scylladb:masterfrom
mykaul:numpy-vector-serialize

Conversation

@mykaul

@mykaul mykaul commented Apr 5, 2026

Copy link
Copy Markdown

Summary

  • Add three fast serialization paths to VectorType for fixed-size numeric subtypes (float, double, int, bigint), delivering 70–300× speedups for high-dimensional vector inserts
  • Add serialize_numpy_bulk() classmethod for batch workloads (e.g. loading embeddings from Parquet files in VectorDBBench)
  • NumPy remains optional; all new code is guarded by try/except ImportError

Motivation

The VectorDBBench use case (https://github.com/scylladb/VectorDBBench) pipelines data as Parquet → PyArrow/NumPy → wire. The existing VectorType.serialize() performed 768+ individual struct.pack('>f', val) + BytesIO.write() calls per vector, making serialization the bottleneck for bulk inserts.

What's new

Three fast paths in VectorType.serialize():

  1. bytes/bytearray passthrough – if the caller already holds a correctly-sized blob (e.g. from serialize_numpy_bulk()), return it directly with zero conversion
  2. NumPy ndarray fast path – for a 1-D ndarray, byte-swap to big-endian via np.asarray(v, dtype='>f4').tobytes() instead of per-element struct.pack
  3. serialize_numpy_bulk() classmethod – byte-swap an entire 2-D array (N_rows × dim) once and slice the raw buffer into a list[bytes]

Benchmark results

768-dim float32, 100-row batches (Python 3.14, NumPy 2.3, best of 3):

Method ns/call µs/row Speedup
list (baseline) 6,196,012 61.96
numpy (per-row) 85,245 0.85 73×
bulk (serialize_numpy_bulk) 41,239 0.41 150×
bytes passthrough 17,980 0.18 345×
bulk+passthrough (end-to-end) 60,216 0.60 103×

In absolute terms: serializing 100 × 768-dim float32 vectors drops from ~6.2 ms (baseline) to ~60 µs (bulk+passthrough end-to-end) — a 103× improvement. The bottleneck was 76,800 individual struct.pack('>f', val) + BytesIO.write() calls (768 elements × 100 rows); NumPy replaces all of that with a single array byte-swap + buffer slice.

768-dim float32, single vector (latency per call):

Method ns/call Speedup
list (baseline) 65,181
numpy (per-row) 963 68×
bytes passthrough 237 275×

Per-vector latency: a single 768-dim vector goes from ~65 µs to under 1 µs via the NumPy path, or 237 ns if pre-serialized bytes are reused.

Safety

  • Variable-size subtypes (smallint, tinyint, text) are excluded from the fast path and continue to use the original element-by-element serialization
  • Passing raw bytes for a variable-size subtype now raises TypeError explicitly (rather than silently falling through)
  • _numpy_dtype is only set when subtype.serial_size() is not None and the typename is in the 4-entry _NUMPY_DTYPE_MAP
  • Comprehensive unit tests (97 pass) cover correctness, round-trip fidelity, error handling, and fallback behavior

Commits

  1. feat: NumPy-accelerated vector serialization – all code + 97 unit tests
  2. bench: add vector NumPy serialization benchmark – standalone benchmark script with ns/call column
  3. docs: document NumPy-accelerated vector serialization – usage examples in docs/performance.rst

@mykaul

mykaul commented Apr 5, 2026

Copy link
Copy Markdown
Author

CC @swasik

@mykaul
mykaul marked this pull request as draft April 5, 2026 06:52
@mykaul
mykaul force-pushed the numpy-vector-serialize branch 3 times, most recently from fba36f3 to 9e4bf4a Compare April 5, 2026 13:06
@mykaul mykaul changed the title feat: NumPy-accelerated vector serialization in VectorType feat: NumPy-accelerated vector serialization in VectorType (hugs improvements in some use cases) Apr 7, 2026
@mykaul mykaul changed the title feat: NumPy-accelerated vector serialization in VectorType (hugs improvements in some use cases) feat: NumPy-accelerated vector serialization in VectorType (huge improvements in some use cases 60us -> 0.6us) Apr 12, 2026
Copilot AI review requested due to automatic review settings July 29, 2026 20:37
@mykaul
mykaul force-pushed the numpy-vector-serialize branch from 9e4bf4a to 5d891a8 Compare July 29, 2026 20:37
@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: fe815eaa-9506-45a8-9835-b1cda66cc4b5

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 current master (picked up the two DRIVER-153 commits) and did a full audit against the three issue classes we hit on the sibling VectorType PR (#689):

  1. ShortType/smallint treated as fixed-width in the vector fast path — not present. _NUMPY_DTYPE_MAP only has 4 entries (float/double/int/bigint), and ShortType/ByteType don't define serial_size() (inherits None from _CassandraType), so apply_parameters() never sets _numpy_dtype for them. Already covered by test_numpy_dtype_none_for_variable_size_numeric_subtype.
  2. Missing buf.size vs. destination-stride check before memcpy (numpy_parser.pyx-class bug) — not applicable. This PR only adds serialization (Python list/ndarray → wire bytes via tobytes()/bytes slicing); it doesn't touch numpy_parser.pyx or populate any preallocated destination array from a raw server buffer. (For the record: that specific numpy_parser.pyx bug is real and still unfixed on master today — fix lives on a separate, unmerged branch — but it's unrelated to this PR's diff.)
  3. Circular import via from cassandra.cython_deps import HAVE_NUMPY on the row_parser import chain — not present. This PR defines its own independent _HAVE_NUMPY in cqltypes.py via a plain import numpy, rather than importing from cassandra.cython_deps. Verified in fresh processes, both import orders (cqltypes first and protocol first): HAVE_CYTHON and HAVE_NUMPY both correctly report True.

Also rebuilt the Cython/numpy extensions from scratch and ran the full unit suite: tests/unit/test_types.py (102 passed, 1 skipped) and tests/unit/ as a whole (810 passed, 38 skipped, 0 failed).

CI: all real checks (Docs build, Integration tests across 3.11-3.14t, security/snyk) are green. "Test wheels building" shows startup_failure, but that's a pre-existing, repo-wide CI infra issue currently affecting a large number of unrelated open PRs (confirmed by checking the workflow queue) — not caused by this branch.

No unresolved review threads / pending review requests at this time.

Net result: no code changes were needed beyond the rebase itself (force-pushed with --force-with-lease). Keeping this as draft.

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.

Adds optional NumPy-accelerated serialization paths for VectorType to significantly reduce per-vector overhead in bulk workloads, plus documentation and benchmarking to validate the improvements.

Changes:

  • Add NumPy ndarray fast path and a bytes/bytearray passthrough path to VectorType.serialize()
  • Add VectorType.serialize_numpy_bulk() for batch conversion from 2-D NumPy arrays
  • Add unit tests, documentation updates, and a standalone benchmark script

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
cassandra/cqltypes.py Implements NumPy dtype caching, ndarray fast path, bytes passthrough, and serialize_numpy_bulk()
tests/unit/test_types.py Adds NumPy-focused unit tests for correctness, error handling, and bulk serialization
docs/performance.rst Documents the new fast paths and provides usage guidance
benchmarks/bench_vector_numpy_serialize.py Adds a reproducible benchmark for list vs NumPy vs bulk vs passthrough serialization

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

Comment thread cassandra/cqltypes.py
Comment thread cassandra/cqltypes.py Outdated
Comment thread cassandra/cqltypes.py Outdated
Comment thread tests/unit/test_types.py
Comment thread docs/performance.rst
mykaul added 3 commits July 31, 2026 00:42
Add three fast paths to VectorType.serialize() for the common case of
fixed-size numeric subtypes (float, double, int, bigint):

1. bytes/bytearray passthrough – skip all conversion when the caller
   already holds a correctly-sized blob (e.g. from serialize_numpy_bulk).

2. NumPy ndarray fast path – convert a 1-D numpy array to big-endian
   bytes via asarray(dtype=...).tobytes() instead of 768+ individual
   struct.pack + BytesIO.write calls.

3. serialize_numpy_bulk() classmethod – byte-swap an entire 2-D array
   (N rows × dim columns) once and slice the raw buffer, yielding one
   bytes object per row with zero per-element overhead.

Benchmarks on 768-dim float32 vectors show 70-300× speedups depending
on the path, directly benefiting bulk-insert workloads such as loading
embeddings from Parquet files (VectorDBBench use case).

NumPy remains an optional dependency; all new code is guarded by
try/except ImportError.  Variable-size subtypes (smallint, tinyint,
text, etc.) are excluded and continue to use the original
element-by-element path.

Unit tests cover correctness, round-trip fidelity, error handling, and
fallback behavior for all three paths.
Standalone benchmark comparing the four serialization paths for
VectorType across dimensions (128, 768, 1536) and batch sizes
(1, 100, 10000):

  - list (element-by-element)   – baseline
  - numpy (per-row ndarray)     – single-row fast path
  - bulk (serialize_numpy_bulk) – batch fast path
  - bytes passthrough (bind)    – pre-serialized blob

Includes auto-calibrated iteration counts and correctness verification.
Add a new section to docs/performance.rst covering the three fast
serialization paths (single-row ndarray, bulk serialize_numpy_bulk,
bytes passthrough) with usage examples and supported subtype list.
Copilot AI review requested due to automatic review settings July 30, 2026 21:44
@mykaul
mykaul force-pushed the numpy-vector-serialize branch from 5d891a8 to 1d24619 Compare July 30, 2026 21:44

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.

🟡 Not ready to approve

Variable-width byte input does not meet the stated safety contract, and the documented example is not runnable as written.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (5)

cassandra/cqltypes.py:1581

  • The public method's Raises section omits two implemented failures: a TypeError for unsafe dtype conversion and a ValueError when the input is not a 2-D ndarray. Documenting only column-count validation leaves callers with an incomplete API contract.
        TypeError
            If NumPy is not available or ``cls._numpy_dtype`` is ``None``
            (subtype not supported for the fast path).
        ValueError
            If the second dimension of *vectors* does not match

cassandra/cqltypes.py:1526

  • Variable-width vectors still accept or mishandle raw bytes because this condition simply falls through when expected is None. For example, a vector<tinyint, 4> receives a four-byte value as four elements, while other byte lengths raise ValueError; neither behavior provides the explicit TypeError promised by the PR's safety contract. Reject bytes before the element-by-element path when no validated serialized length exists.

This issue also appears on line 1577 of the same file.

        if expected is not None and isinstance(v, (bytes, bytearray)):

docs/performance.rst:68

  • This example raises NameError because it calls lookup_casstype() below without importing it. Import the helper alongside VectorType so the documented bulk workflow is runnable.
    from cassandra.cqltypes import VectorType

docs/performance.rst:90

  • smallint and tinyint are fixed-width CQL numeric types, so describing them as variable-length is inaccurate. They fall back here because they are not in the new NumPy dtype map (and currently do not expose serial_size()), not because their values have variable width.
**Supported subtypes** – the fast paths are available for ``float``
(``>f4``), ``double`` (``>f8``), ``int`` (``>i4``), and ``bigint``
(``>i8``).  Variable-length subtypes (``smallint``, ``tinyint``, text
types, etc.) fall back to the original element-by-element serialization
automatically.

benchmarks/bench_vector_numpy_serialize.py:24

  • The implementation no longer slices one whole-array bytes buffer; it calls tobytes() separately for each row. Update this benchmark description so it measures and documents the implementation actually in this PR.
  3. bulk path      – serialize_numpy_bulk() on a 2-D array (one byte-swap for
                      the entire batch, then bytes slicing)
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

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