ticket #000039: --quant {float32,int8} + int8 head-to-head
Wire vector quantization (the §3.1 production knob): arborist embed
--quant int8 [--rebuild]. The chunk_vecs vec0 column becomes int8[384]
vs float[384] per quant; the quant folds into VEC_BACKEND_VERSION
(...-384int8-... / ...-384float32-...) and vec_meta records it per
shard. Switching quant on an existing chunk_vecs requires --rebuild
(the vec0 element type can't be altered in place — embed_documents
raises ValueError telling you to --rebuild).
int8 serialization: scale each bge component by 127 (theoretical
[-1,1] range), clamp to [-127,127], round, serialize_int8. The same
scaling on the query vector → distances comparable; cosine is
scale-invariant so the uniform x127 cancels in the ranking.
sqlite-vec v0.1.9 quirk worked around: a bare blob inserted into a
vec0 column is interpreted as float32 regardless of the column's
declared type — int8 vectors MUST be wrapped in vec_int8(...). So
the INSERT and the MATCH now wrap the blob in vec_f32(?) (float32)
or vec_int8(?) (int8) — constructor name from a fixed dict, no
injection surface. (Discovered the hard way: a bare int8 blob into
an int8[384] column → "expected int8, but a float32 vector was
provided".)
VecBackend reads the quant from the existing chunk_vecs schema (or
defaults to float32) so search uses the matching wrapper. New module
exports: QUANTS, EMBED_QUANT, vec_backend_version(quant), existing_quant.
int8 head-to-head on crawl_appliedcombinatorics_org.db (168 chunks):
- storage: float32 1,597,440 B -> int8 417,792 B = 3.8x smaller
(~4x at corpus scale where the 1024-vector blocks fill; the
~28 KB of vec0 metadata doesn't quarter, hence 3.8 not 4.0).
- recall vs the float32 baseline:
Q "how many ways to choose k things from n":
identical top-5 (Combinations, Permutations, Exercises,
Derangements, Graph Coloring).
Q "pigeonhole principle counting":
identical top-2 (Graph Coloring, Exercises); ranks 3-4 swap
Derangements <-> Permutations at Δdistance 0.002 — sub-noise.
- embed speed unchanged (~3.9 chunks/s — model-load-dominated).
Conclusion: int8 is the obvious production config (§3.1's +6%-tax
recommendation confirmed empirically). v1 default stays float32 for
max fidelity; flipping the default to int8 is a fox call.
CLI (arborist/cli.py): arborist embed --quant {float32,int8}; output
JSON gains "quant"; embed_documents ValueError → exit 2 with the
"--rebuild" hint.
tests/test_search_vec.py (9 -> 16): test_int8_quant_roundtrips
(int8[384] schema, vec_meta version, search round-trip, quant
inferred by VecBackend), test_quant_mismatch_requires_rebuild,
test_invalid_quant_rejected.
#000039 status updated. Full suite: 2343 passed, 28 skipped.
(Unrelated parallel-clone work in the tree — Makefile, arborist/qa/
verify.py, bench/fixtures/5f/*, tests/test_bench_batteries.py,
tests/test_verify.py — is #000046's hard-fixture tier, not touched.)
This commit is contained in:
parent
7a64939e2b
commit
06f5a11651
4 changed files with 202 additions and 41 deletions
|
|
@ -203,3 +203,47 @@ def test_embed_rebuild_re_embeds_all(db):
|
|||
"SELECT value FROM vec_meta WHERE key='backend_version'"
|
||||
).fetchone()
|
||||
assert row[0] == VEC_BACKEND_VERSION
|
||||
|
||||
|
||||
def test_int8_quant_roundtrips(db):
|
||||
"""quant='int8': table is int8[384], vec_meta records the int8 version,
|
||||
search round-trips (a query equal to a chunk's content → that chunk on
|
||||
top, since same float vec → same int8 vec → distance ~0)."""
|
||||
from arborist.search.vec import existing_quant, vec_backend_version
|
||||
conn, docs = db
|
||||
n = embed_documents(conn, embedder=_stub_embedder, quant="int8", rebuild=True)
|
||||
assert n == 3
|
||||
assert existing_quant(conn) == "int8"
|
||||
sql = conn.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE name='chunk_vecs'"
|
||||
).fetchone()[0]
|
||||
assert "int8[384]" in sql
|
||||
assert conn.execute(
|
||||
"SELECT value FROM vec_meta WHERE key='backend_version'"
|
||||
).fetchone()[0] == vec_backend_version("int8")
|
||||
|
||||
backend = VecBackend(conn, embedder=_stub_embedder) # quant inferred from table
|
||||
assert backend.quant == "int8"
|
||||
hits = backend.search(docs[2][3], limit=3) # query == doc C's content
|
||||
assert hits and hits[0].document_root == docs[2][0]
|
||||
assert hits[0].audit_mode is AuditMode.UNGROUNDED
|
||||
|
||||
|
||||
def test_quant_mismatch_requires_rebuild(db):
|
||||
"""Switching quant on an existing chunk_vecs without --rebuild errors —
|
||||
the vec0 column element type can't be altered in place."""
|
||||
conn, _ = db
|
||||
embed_documents(conn, embedder=_stub_embedder, quant="float32") # default
|
||||
with pytest.raises(ValueError, match="rebuild"):
|
||||
embed_documents(conn, embedder=_stub_embedder, quant="int8") # no rebuild
|
||||
# ...but with rebuild it switches cleanly.
|
||||
embed_documents(conn, embedder=_stub_embedder, quant="int8", rebuild=True)
|
||||
from arborist.search.vec import existing_quant
|
||||
assert existing_quant(conn) == "int8"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["fp16", "binary", "int4", "", None])
|
||||
def test_invalid_quant_rejected(db, bad):
|
||||
conn, _ = db
|
||||
with pytest.raises(ValueError, match="quant"):
|
||||
embed_documents(conn, embedder=_stub_embedder, quant=bad) # type: ignore[arg-type]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue