Float16 Vector Storage Guide¶
ApexBase supports FLOAT16_VECTOR columns for storing embedding vectors in half-precision (float16) format. Compared to the default float32 FixedList column, float16 halves the storage footprint and enables hardware-accelerated SIMD distance kernels on ARM and x86 platforms.
This matters most for agent memory and RAG: a long-term memory table keeps growing
while the model's context window does not. Half-precision embeddings halve the
bytes scanned per recall query, so you can keep more history on one machine and
still run exact TopK over the full table. The same column can feed
hybrid_search() (BM25 plus vector fusion) and be streamed with
execute_stream() when a read is too large to materialize — both covered below.
Table of Contents¶
- When to use float16
- Creating a float16 table
- Inserting vectors
- Querying — TopK and SQL
- SIMD acceleration details
- Quantization error and precision
- Full working example
- Comparison: float16 vs float32
When to use float16¶
Use float16 (FLOAT16_VECTOR) |
Stick with float32 (default FixedList) |
|---|---|
| Large embedding tables (>1M rows, high dim) | Embeddings with high dynamic range (e.g., raw logits) |
| Memory-constrained environments | Downstream code that expects exact float32 fidelity |
| Apple Silicon / AWS Graviton (NEON fp16 hardware) | Dimensions not divisible by 8 (the 8-wide kernels fall back to a scalar tail) |
| Models that already quantize (CLIP, text embeddings) | Prototype / exploration phase where precision matters |
For most modern embedding models (CLIP, BERT, Sentence-Transformers, OpenAI text-embedding-*), float16 quantization error is well below retrieval noise.
v2.0.0 (V5 GA)
FLOAT16_VECTOR is a physical fixed-width vector column, not part of the
V5 logical-type section (uuid, decimal(p,s), the time family,
list<T>, extension codecs); describe_table() reports it as
float16_vector. The 2.0.0 release also puts hybrid retrieval, streaming
reads and bounded execution in the same engine: hybrid_search() fuses
BM25 with a float16 vector branch, execute_stream() yields large reads
batch by batch, and APEX_QUERY_MEMORY_MB / APEX_SPILL_DIR keep
sort/aggregation/join state bounded. hybrid_search() requires a filesystem
database.
Creating a float16 table¶
from apexbase import ApexClient
client = ApexClient("./mydb")
# SQL CREATE TABLE with FLOAT16_VECTOR column
client.execute("CREATE TABLE embeddings (doc_id TEXT, vec FLOAT16_VECTOR)")
client.use_table("embeddings")
Accepted type name aliases (all equivalent):
FLOAT16_VECTOR(recommended)FLOAT16VECTORF16_VECTOR
Inserting vectors¶
Recommended: batch store with numpy arrays¶
Use a batch call for best throughput. The fixedlist path converts source vectors to float32 bytes and then the storage layer applies a single f32→f16 conversion, giving correct float16 data.
import numpy as np
n, dim = 50_000, 128
vecs = np.random.rand(n, dim).astype(np.float32) # source in float32
# Batch list-of-dicts
client.store([{"doc_id": str(i), "vec": vecs[i]} for i in range(n)])
# Columnar dict — fastest for large homogeneous batches
client.store({
"doc_id": [str(i) for i in range(n)],
"vec": [vecs[i] for i in range(n)],
})
numpy float16 and Python-list source data¶
If your upstream model already produces float16 numpy arrays, they are accepted directly. Numeric Python lists/tuples are also accepted, including single-record writes.
vecs_f16 = np.random.rand(n, dim).astype(np.float16)
client.store([{"doc_id": str(i), "vec": vecs_f16[i]} for i in range(n)])
# Single-record writes are correct too — keep the column dimension consistent
client.store({"doc_id": "one", "vec": [0.1] * dim})
Fixed dimension
The column's dimension is fixed by the first valid write. A later store with
a different length raises OSError: column 'vec' stores <D>-dimension
vectors; row 0 has <N> — keep every row at the same dimension.
Querying — TopK and SQL¶
The query API is identical to float32 columns. Query vectors are always provided as float32 — the runtime converts them internally.
topk_distance¶
query = np.random.rand(128).astype(np.float32)
results = client.topk_distance("vec", query, k=10, metric="l2")
df = results.to_pandas()
# df columns: _id (int64), dist (float64) — sorted nearest first
All six metrics are supported:
metric |
Description |
|---|---|
'l2' |
Euclidean distance √Σ(aᵢ−bᵢ)² |
'l2_squared' |
Squared L2 Σ(aᵢ−bᵢ)² |
'cosine' / 'cosine_distance' |
1 − cos(a, b) |
'dot' / 'inner_product' |
Negative dot product (for min-heap) |
'l1' / 'manhattan' |
Σ |
'linf' / 'chebyshev' |
max |
batch_topk_distance¶
N = 100
queries = np.random.rand(N, 128).astype(np.float32)
result = client.batch_topk_distance("vec", queries, k=10, metric="l2")
# result.shape == (N, 10, 2)
# result[i, :, 0] = _id of top-10 for query i
# result[i, :, 1] = distances
SQL distance functions¶
array_distance, l1_distance, linf_distance all work on FLOAT16_VECTOR columns:
# The table stores 128-dimension vectors; build the literal with the same dim.
query_vec = ", ".join(["0.1"] * 128)
client.execute(f"""
SELECT doc_id,
array_distance(vec, [{query_vec}]) AS l2_dist,
l1_distance(vec, [{query_vec}]) AS l1_dist
FROM embeddings
ORDER BY l2_dist
LIMIT 5
""")
SQL TopK via topk_distance + explode_rename:
q_str = ",".join(f"{v:.6f}" for v in query)
client.execute(f"""
SELECT explode_rename(topk_distance(vec, [{q_str}], 10, 'l2'), '_id', 'dist')
FROM embeddings
""")
Hybrid retrieval and streaming¶
hybrid_search() fuses the float16 vector branch with a BM25 text branch and
needs a filesystem database; execute_stream() reads large projections batch by
batch on any database:
import numpy as np
from apexbase import ApexClient
client = ApexClient("./rag_db") # persistent directory: hybrid uses the mmap scan
client.execute("CREATE TABLE docs (title TEXT, body TEXT, vec FLOAT16_VECTOR)")
client.execute("CREATE FTS INDEX ON docs (title, body)")
vecs = np.random.rand(3, 128).astype(np.float32)
client.store({
"title": ["Agent memory", "Rust guide", "Python tutorial"],
"body": ["retrieval over long-term agent memory",
"systems programming", "learn python"],
"vec": [vecs[i] for i in range(3)],
})
# Fuse BM25 and float16 vector recall in one call (RRF by default).
hits = client.hybrid_search(
np.random.rand(128).astype(np.float32),
"agent memory",
k=3, vector_col="vec",
)
print(hits.to_pandas()) # _id, score
# Stream large reads batch by batch instead of materializing them.
for batch in client.execute_stream("SELECT title, body FROM docs"):
print(batch.num_rows)
client.close()
See Hybrid Search for fusion / alpha, and
Vector Quantization when half precision is not
small enough.
SIMD acceleration details¶
Float16 distance kernels use native hardware half-precision instructions when available, detected at runtime:
aarch64 — NEON fp16 (fp16 feature flag)¶
Applies to: Apple M1/M2/M3/M4, AWS Graviton 3/4, Ampere Altra.
- Uses
FCVTL/FCVTL2to widen f16→f32 in-register (no memory round-trip) - 8 elements loaded per NEON vector, widened to two 128-bit f32 registers
- Avoids the scalar per-element widen step of the fallback path; the end-to-end gain depends on dimension, data layout, and whether the scan is memory-bound, so measure it for your workload rather than assuming a fixed speedup
x86_64 — AVX2 + F16C¶
Applies to: Intel Haswell+ (2013+), AMD Ryzen (2017+).
- Uses
_mm256_cvtph_psto convert 8 f16 → 8 f32 per instruction - 256-bit AVX2 arithmetic on the widened values
- Falls back to scalar if
f16coravx2is not detected
Scalar fallback¶
All platforms have a scalar fallback that converts f16→f32 element-by-element. Performance is comparable to a plain float32 scan of the same data (same number of distance operations, but on narrower storage).
Runtime dispatch (no user action required)¶
CPU query at startup → select kernel → cache decision
↙ ↘
NEON fp16 AVX2+F16C
(aarch64) (x86_64)
↓ neither ↓
scalar fallback
Quantization error and precision¶
Float16 (IEEE 754 binary16) has:
- 10-bit mantissa → ~3.3 decimal digits of precision
- Range: ±65504
- Machine epsilon: ~9.77 × 10⁻⁴
For typical embedding ranges (values in [−1, 1]):
import numpy as np
def f16_quantize(v):
return v.astype(np.float16).astype(np.float32)
rng = np.random.default_rng(0)
dim = 128
vec = rng.random(dim).astype(np.float32) * 2 - 1 # uniform [-1, 1]
query = rng.random(dim).astype(np.float32) * 2 - 1
exact = float(np.sqrt(np.sum((vec - query) ** 2)))
f16_dist = float(np.sqrt(np.sum((f16_quantize(vec) - query) ** 2)))
rel_err = abs(exact - f16_dist) / exact
print(f"exact: {exact:.4f} f16: {f16_dist:.4f} rel_err: {rel_err:.2e}")
# exact: 10.0652 f16: 10.0653 rel_err: 1.49e-05
For top-k retrieval, quantization error is well within retrieval noise for dimensions ≥ 32 and any standard embedding model.
Full working example¶
import numpy as np
from apexbase import ApexClient
# 1. Create database and table
client = ApexClient("./embeddings_db")
client.execute("CREATE TABLE docs (title TEXT, vec FLOAT16_VECTOR)")
client.use_table("docs")
# 2. Generate and store embeddings (float32 source → auto-quantized to f16)
rng = np.random.default_rng(42)
n, dim = 10_000, 384
vecs = rng.random((n, dim), dtype=np.float32)
titles = [f"document_{i}" for i in range(n)]
client.store({"title": titles, "vec": [vecs[i] for i in range(n)]})
# 3. Nearest-neighbour search
query = rng.random(dim, dtype=np.float32)
# Single query
results = client.topk_distance("vec", query, k=5, metric="l2")
print(results.to_pandas())
# Batch query
queries = rng.random((20, dim), dtype=np.float32)
batch_results = client.batch_topk_distance("vec", queries, k=5, metric="cosine")
print(batch_results.shape) # (20, 5, 2)
# SQL query
q_str = ",".join(f"{v:.6f}" for v in query)
sql_results = client.execute(f"""
SELECT title, array_distance(vec, [{q_str}]) AS dist
FROM docs
ORDER BY dist
LIMIT 5
""")
print(sql_results.to_pandas())
client.close()
Comparison: float16 vs float32¶
FLOAT16_VECTOR |
Default FixedList (float32) |
|
|---|---|---|
| Bytes per element | 2 | 4 |
| Storage for 1M × dim=128 | ~256 MB | ~512 MB |
| Distance precision | ~3.3 decimal digits | ~7.2 decimal digits |
| SIMD on Apple Silicon | NEON fp16 | NEON f32 |
| SIMD on x86_64 | AVX2+F16C | AVX2 f32 |
| Query API | identical | identical |
| Recommended for | production, large tables | prototyping, high-precision needs |
Tip: For the fastest possible inserts, use the columnar dict API:
This bypasses the row-oriented path entirely and feeds data directly to the Rust columnar encoder.