perf(retain): optimize embedding_to_pgvector serialization via zero-copy orjson - #3815
Open
Sanderhoff-alt wants to merge 1 commit into
Open
Conversation
Strix Security ReviewWarning This pull request has 1 commit after the last Strix review ( No security issues found. Updated for Reviewed by Strix |
Sanderhoff-alt
force-pushed
the
perf/optimize-embedding-to-pgvector
branch
3 times, most recently
from
August 26, 2026 14:12
3da552e to
2e14c11
Compare
Retain and import paths convert float embeddings to pgvector vector literals
for asyncpg binding (insert_facts_batch, compute_semantic_links_within_batch,
update_memory_unit_embedding).
The baseline implementation used a Python generator:
"[" + ",".join(repr(float(value)) for value in embedding) + "]"
For 500 facts (768,000 floats at 1536d), this allocated 768,000 PyFloat objects
and 768,000 PyUnicode strings, taking ~220 ms CPU time and ~15 MB heap memory.
The optimized implementation leverages np.frombuffer on PackedEmbedding
(array('f')) for zero-copy buffer views, and orjson.OPT_SERIALIZE_NUMPY to
format floats directly into the output byte buffer using Rust Ryu SIMD:
* Promotes numpy to explicit direct dependency across hindsight-api and dev;
* Formats shortest float32 representation (byte-identical Postgres storage);
* Isolates _repr_literal fallback helper for non-finite and non-float inputs;
* Unifies _dumps_or_repr_fallback with single payload parameter and no option branching;
* Streamlines embedding_to_pgvector into a concise polymorphic dispatcher;
* Seamlessly supports array('f'), list[float], tuple, ndarray, and str.
Measured on Apple Silicon via vector-serialization-bench (best of 5 repeats):
workload baseline prod speedup peak alloc
single_bge_384 (1x 384d) 0.136 ms 0.040 ms 3.4x 36K -> 13K
single_openai_1536 (1x 1536d) 0.489 ms 0.085 ms 5.8x 144K -> 50K
batch_20_gemini_768 (20x 768d) 4.580 ms 0.514 ms 8.9x 356K -> 185K
batch_200_openai_1536 (200x 1536d) 92.64 ms 9.45 ms 9.8x 6.1M -> 3.4M
batch_500_large_doc (500x 1536d) 221.78 ms 23.84 ms 9.3x 15.0M -> 8.4M
batch_200_raw_list (200x 1536d) 87.70 ms 11.48 ms 7.6x 6.1M -> 6.0M
Throughput increased from 3.3 Mfloat/s to 32.5 Mfloat/s (~9.8x speedup on
typical retain batches), with ~44% peak memory reduction on 500-fact batches.
Includes unit tests in test_packed_embeddings.py covering bit-identical float32
roundtrips, custom non-serializable objects, non-f array fallthrough, tuples,
ndarrays, and non-finites.
Sanderhoff-alt
force-pushed
the
perf/optimize-embedding-to-pgvector
branch
from
August 26, 2026 14:19
2e14c11 to
319a3a6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary & Key Results
This PR optimizes
embedding_to_pgvector—the foundational vector literal serialization operator called on critical Retain, link generation, and import paths when binding embeddings to PostgreSQLvectorcolumns.By declaring
numpyas an explicit core dependency and replacing the Python generator and element-wiserepr(float())formatting with a zero-copy buffer view (np.frombufferonPackedEmbedding) and RustRyuSIMD float serialization (orjson.OPT_SERIALIZE_NUMPY), we achieve:PyFloatorPyUnicodeobject allocations;array('f'),list[float],tuple,np.ndarray,str).1. Problem Analysis & Bottleneck Trace
During Retain (
insert_facts_batch,compute_semantic_links_ann), fact embeddings must be formatted as pgvector text literals ("[0.1,0.2,...]") for asyncpg to bind to the PostgreSQLvectorcolumn.Previous Implementation:
Allocation & Latency Bottleneck:
flowchart TD subgraph Baseline [Baseline Python Execution: ~220 ms / 500 facts] A["500 Facts @ 1536d (768,000 floats)"] --> B["768,000 float() unpackings -> 768,000 PyFloat objects"] --> C["768,000 repr() calls -> 768,000 PyUnicode string objects"] --> D["join() collects 768,000 strings into list -> ~15 MB heap"] --> E["Final string concatenation -> Heavy GC churn"] endFor a typical 500-fact document batch at 1536d, this allocated over 1.53 million transient Python heap objects and consumed ~220ms of pure single-core CPU time.
2. Technical Architecture & Solution
flowchart LR subgraph Input [Polymorphic Input] P1["PackedEmbedding\n(array 'f', 4-byte C array)"] P2["np.ndarray\n(float32 / float64)"] P3["list[float] / tuple\n(Raw API / JSON Import)"] P4["str\n(Literal passthrough)"] end subgraph FastPath [Zero-Copy Rust Ryu SIMD Engine] P1 -->|np.frombuffer\nzero-copy view| View["C-contiguous ndarray view"] P2 --> View View -->|orjson.OPT_SERIALIZE_NUMPY| Ryu["Rust Ryu float formatter\nDirect ASCII byte buffer"] P3 -->|orjson.dumps| Ryu P4 -->|Identity check| Out["Postgres literal '[0.1,0.2,...]'"] Ryu --> Out end subgraph Fallback [Safety Fallback] Ryu -->|Contains NaN / Inf / -Inf| Safe["_repr_literal fallback\n(IEEE 754 compliance)"] Safe --> Out endKey Technical Details & Invariants:
"numpy>=1.26.0"directly inhindsight-api-slimandhindsight-dev(eliminating brittle transitive dependency assumptions and ghost degradation paths).PackedEmbedding(stored as continuous 32-bit floats viaarray('f')per PR fix(retain): bound retain's memory by a budget instead of by the document (#3756) #3763 / issue Retain holds a whole document in memory: embeddings as list[float] are 74% of peak, and nothing bounds the pipeline by chunk count #3756) is converted to a NumPy buffer view vianp.frombuffer(embedding, dtype=np.float32). Zero memory copies or allocations occur.0.1vs legacy0.10000000149011612). When parsed back by PostgreSQL as a 32-bit float vector, the stored bytes are bit-identical._repr_literal&_dumps_or_repr_fallback): Finite IEEE 754 float JSON formatting strictly consists of digits, decimal points, signs, exponents, commas, and brackets ([0-9.-e,[]]). The substring"null"can only appear if the vector containsNaN,Inf, or-Inf, which orjson encodes asnullper JSON specification. When detected (or on type errors), it cleanly falls back to_repr_literal(matching legacy baseline byte-for-byte).embedding_to_pgvectorfunction is a clean polymorphic dispatcher with no defensive dead code.3. Comprehensive Benchmark & Throughput Results
Tested on Apple Silicon using the standardized microbenchmark suite (
hindsight-dev/benchmarks/micro/vector_serialization.py, best of 5 repeats):single_bge_384single_openai_1536batch_20_gemini_768batch_200_openai_1536batch_500_large_docbatch_200_raw_listThroughput Scaling Analysis:
2.9 ~ 3.2 Mfloat/sup to8.7 ~ 18.5 Mfloat/s(+200% ~ +472%);3.2 ~ 3.5 Mfloat/sup to30.3 ~ 32.5 Mfloat/s(+776% ~ +879%), reaching steady-state peak throughput of 32.50 Mfloat/s onbatch_200_openai_1536.4. Conformance & Test Verification
All edge cases and mathematical constraints are validated:
Unit tests in
hindsight-api-slim/tests/test_packed_embeddings.py(7/7 passed).5. Changes Summary
hindsight-api-slim/pyproject.toml:"numpy>=1.26.0"to coredependencies.hindsight-api-slim/hindsight_api/engine/retain/types.py:numpy/orjsonimports and streamlinedembedding_to_pgvectordispatcher with_dumps_or_repr_fallbackand isolated_repr_literal.hindsight-api-slim/hindsight_api/engine/retain/link_utils.py:import numpy as npto top-level module import.hindsight-api-slim/tests/test_packed_embeddings.py:hindsight-dev/pyproject.toml:"numpy>=1.26.0"dependency and registeredvector-serialization-benchCLI command.hindsight-dev/benchmarks/micro/vector_serialization.py:scripts/benchmarks/run-vector-serialization-bench.sh: