Engineering
FAISS에서 SQ8까지
벡터 검색 엔진의 기본 저장 포맷을 찾기까지. FAISS를 기준선으로 두고 F32, SQ8, SQ4, SQ1, TQ4를 비교한 개발 기록.
벡터 검색 성능 이야기를 할 때 FAISS는 여전히 가장 먼저 떠오르는 기준선입니다. 우리도 출발점은 같았습니다. 다만 목표는 FAISS를 그대로 복제하는 것이 아니라, 우리 엔진 안에서 실제로 더 좋은 기본값이 무엇인지 찾는 것이었습니다.
질문은 이것이었습니다.
검색 엔진을 직접 운영한다면, 어떤 저장 포맷이 지연시간, 메모리 사용량, 검색 품질 사이에서 가장 좋은 균형을 만들어낼까?
이번 벤치마크 라운드 끝에 얻은 답은 SQ8이었습니다.
이 글은 그 결론에 도달하기까지의 과정을 정리한 개발 기록입니다.
무엇을 해결하려고 했나
우리가 원한 것은 세 가지였습니다.
FAISS와 비슷한 급의 지연시간F32보다 훨씬 낮은 메모리 사용량- 기본값으로 써도 될 만큼 안정적인 검색 품질
문제는 이 세 가지가 서로 다른 방향으로 당긴다는 점입니다. 압축을 세게 걸수록 메모리는 줄지만, 지연시간이나 랭킹 품질은 무너지기 쉬워집니다.
그래서 처음부터 “가장 많이 압축되는 포맷”을 찾기보다, “운영 기본값으로 가장 잘 버티는 포맷”을 찾는 방식으로 접근했습니다.
비교한 후보들
이번에 비교한 저장 포맷은 다음과 같습니다.
F32: 압축 없는 기준선SQ8: 8비트 scalar quantization,F32대비 4배 압축SQ4: 4비트 scalar quantization,F32대비 8배 압축SQ1: 1비트 sign quantization,F32대비 32배 압축TQ4: TurboQuant 아이디어를 참고한 실험 경로
여기서 중요한 점은 F32를 끝까지 같이 가져갔다는 것입니다. 그래야 압축 포맷끼리 상대 비교만 하는 것이 아니라, 실제 품질 손실과 실제 지연시간 이득을 함께 볼 수 있습니다.
1단계: FAISS를 구현 목표가 아니라 기준선으로 두기
이번 라운드에서 사용한 FAISS HNSW 기준 수치는 다음과 같았습니다.
| Engine | p50(us) | p95(us) | p99(us) | QPS | MB |
|---|---|---|---|---|---|
| FAISS HNSW | 621 | 941 | 1653 | 1503 | 4096.0 |
이 기준선이 주는 의미는 단순합니다.
- 어떤 포맷이 FAISS보다 훨씬 느리면 기본값 후보로 보기 어렵다
- 조금 느리더라도 훨씬 작으면 여지는 있다
- 더 빠르고 더 작으면 강력한 후보가 된다
즉, FAISS는 따라가야 할 구현이 아니라 넘어서거나 최소한 버텨야 할 성능 기준이었습니다.
2단계: 200K에서 먼저 후보를 걸러내기
모든 포맷을 바로 1M까지 밀어붙이는 대신, 먼저 200K에서 걸러냈습니다.
조건은 다음과 같았습니다.
dim = 1024top_k = 10- 쿼리
1000개 flush -> compact -> preload -> search
결과는 이랬습니다.
| Format | Build | p50(us) | p95(us) | p99(us) | QPS | MB |
|---|---|---|---|---|---|---|
| F32+HNSW | 142s | 545 | 697 | 880 | 1772 | 819.2 |
| SQ8+HNSW | 165s | 221 | 349 | 554 | 4207 | 204.8 |
| SQ4+HNSW | 148s | 660 | 797 | 980 | 1479 | 102.4 |
| SQ1+HamHNSW | 358s | 1290 | 1777 | 2161 | 732 | 25.6 |
이 표에서 바로 보이는 사실이 몇 가지 있었습니다.
먼저 SQ1은 후보군에서 사실상 빠졌습니다. 압축률은 인상적이지만, 기본값으로 쓰기에는 지연시간 비용이 너무 컸습니다.
SQ4는 여기서는 꽤 흥미로웠습니다. 작고, 완전히 무너지지도 않았기 때문입니다. 그래서 “혹시 1M에서도 sweet spot이 될 수 있을까?”라는 기대를 남겼습니다.
하지만 가장 눈에 띈 것은 SQ8이었습니다.
F32보다 훨씬 작고F32보다 훨씬 빠르고- 이 시점에서는 품질 경고도 없었습니다
즉, SQ8은 처음으로 “이건 진짜 기본값 후보 같다”는 느낌을 준 포맷이었습니다.
3단계: 속도만 보지 말고 검색 품질 확인하기
압축 포맷이 빠르고 작아 보여도, 검색 품질이 무너지면 기본값으로는 쓸 수 없습니다.
그래서 SQ8에 대해서는 따로 F32와 정면 비교하는 recall 테스트를 넣었습니다.
조건:
1024d10,000vectors50queries- brute-force top-10을 ground truth로 사용
결과:
| Metric | Value |
|---|---|
| F32 recall@10 | 0.9960 |
| SQ8 recall@10 | 0.9800 |
| Recall delta | 0.0160 |
이 수치가 결정적이었습니다.
SQ8은 무손실 포맷이 아닙니다. 하지만 기본값을 결정하는 데 필요한 건 완전한 보존이 아니라, 손실이 충분히 작아서 성능과 메모리 이득이 훨씬 크다는 사실입니다.
이번 결과에서는 바로 그 조건을 만족했습니다.
4단계: 1M에서 최종 비교하기
최종적으로 1M 스케일에서 남은 핵심 비교는 SQ8과 SQ4였습니다.
| Format | Build | p50(us) | p95(us) | p99(us) | QPS | MB |
|---|---|---|---|---|---|---|
| SQ8+HNSW | 839s | 277 | 392 | 502 | 3400 | 1024.0 |
| SQ4+HNSW | 781s | 860 | 1048 | 1221 | 1138 | 512.0 |
| FAISS HNSW reference | - | 621 | 941 | 1653 | 1503 | 4096.0 |
이 표가 사실상 최종 결론을 만들었습니다.
이 숫자가 뜻하는 것
SQ8은:
- FAISS 기준보다 메모리를 4배 적게 쓰고
- 이 측정 환경에서는 FAISS 기준보다 더 낮은 지연시간을 보였고
- QPS도 더 높았습니다
반면 SQ4는:
- 메모리를
SQ8보다 절반 더 아끼지만 1M스케일에서는 지연시간과 처리량을 너무 많이 내줬습니다
여기서 중요한 건 압축률 자체가 아니라 전체 운영 지점입니다.
기본값의 역할은 “가장 많이 압축되는 포맷”이 되는 것이 아니라, 대부분의 실제 워크로드에서 가장 균형 잡힌 성능을 제공하는 것입니다. 1M에서는 그 균형점이 분명히 SQ8 쪽이었습니다.
5단계: 더 강한 압축을 원하면 TurboQuant 계열이 답일까
자연스럽게 다음 질문이 나옵니다.
SQ8은 좋고SQ4는 아쉽다면, 더 똑똑한 4비트 경로는 가능하지 않을까?
그래서 SQ4를 고치는 대신, 별도의 TQ4 실험 경로를 만들어봤습니다. 방향은 TurboQuant 아이디어를 참고했지만, 어디까지나 첫 실험이었습니다.
결과는 좋지 않았습니다.
Recall:
| Metric | Value |
|---|---|
| F32 recall@10 | 0.9920 |
| TQ4 recall@10 | 0.4900 |
| Recall delta | 0.5020 |
200K latency:
| Format | Build | p50(us) | p95(us) | p99(us) | QPS | MB |
|---|---|---|---|---|---|---|
| SQ8+HNSW | 138s | 211 | 247 | 281 | 4645 | 204.8 |
| TQ4+HNSW | 144s | 3803 | 4115 | 4300 | 263 | 104.0 |
즉, 첫 번째 TurboQuant 계열 실험은 저장 용량 말고는 거의 이점이 없었습니다. 품질도 나빴고, 속도도 너무 느렸습니다.
이 결과가 말해주는 것은 두 가지입니다.
- 지금 당장은
SQ8이 훨씬 더 실용적이다 - TurboQuant 계열을 다시 하려면 “조금 더 손보는 수준”이 아니라, 논문에 더 가까운 새로운 구현으로 다시 접근해야 한다
최종 선택
결론은 단순합니다.
- 기존 컬렉션은 그대로 둔다
- 새 컬렉션은
SQ8을 기본값으로 사용한다 - 미지원 차원은
F32로 안전하게 fallback 한다 - 특정 컬렉션을 다른 포맷으로 바꿔야 하면 raw source data 기준으로 재빌드한다
이 방식의 장점은 명확합니다. 성능 이득은 바로 가져가되, 운영 마이그레이션 프로젝트로 키우지 않아도 됩니다.
한 줄 요약
전체 흐름을 한 표로 줄이면 이렇습니다.
| 단계 | 결론 |
|---|---|
| FAISS baseline | 넘어야 할 외부 기준선 |
| F32 baseline | 속도와 recall의 기준점 |
| SQ1 | 기본값으로는 너무 느림 |
| SQ4 | 흥미롭지만 1M에서는 SQ8보다 약함 |
| SQ8 | 가장 좋은 전체 균형 |
| TQ4 first pass | 아직 경쟁력 없음 |
그리고 정말 한 문장으로 요약하면 이렇습니다.
SQ8을 고른 이유는 가장 많이 압축되기 때문이 아니라, 압축 포맷 중 처음으로 “기본값처럼 동작”했기 때문입니다.
최종 벤치
이번 라운드에서 하나만 남겨야 한다면 이 표입니다.
| Format | p50(us) | p95(us) | p99(us) | QPS | MB |
|---|---|---|---|---|---|
| SQ8+HNSW | 277 | 392 | 502 | 3400 | 1024.0 |
| SQ4+HNSW | 860 | 1048 | 1221 | 1138 | 512.0 |
| FAISS HNSW | 621 | 941 | 1653 | 1503 | 4096.0 |
이 표가 이번 선택을 만들었습니다.