FAISS 내부 들여다보기: 10억 개 벡터 유사도 검색의 원리
Inside FAISS: Billion-Scale Similarity Search
TL;DR Highlight
IVF로 건너뛰고 PQ로 압축하면 10억 벡터도 RAM에 올라간다
Who Should Read
RAG·시맨틱 검색에 FAISS 인덱스를 쓰면서 메모리와 지연 시간 때문에 인덱스 선택이 고민인 백엔드 개발자
Core Mechanics
- 브루트포스는 10억 개에서 불가능하다
- SIFT 10억 개(D=128, float 4바이트)를 그대로 담으면 512GB RAM이 필요하다.
- 쿼리 하나마다 거리 계산을 10억 번 해야 한다.
- IVF는 후보 구역만 훑는다
- K-Means(비슷한 것끼리 묶는 군집화)로 공간을 Voronoi 셀로 나눈다.
- 쿼리가 속한 셀과 이웃 셀 몇 개만 거리 계산한다.
- PQ는 벡터를 8바이트로 압축한다
- 128차원 벡터를 m=8개 조각으로 자르고 조각마다 256개 centroid(대표 벡터) 중 하나의 번호만 저장한다.
- 512바이트가 8바이트로 줄어 64배 압축이다.
- 조각으로 나누면 코드북이 128KB로 준다
- 통짜 코드북으로 64비트를 만들려면 2^64개 centroid가 필요해서 저장·학습·검색 모두 불가능하다.
- 조각별 코드북은 centroid 2,048개만 저장하면 되고 총 128KB라 L2 캐시에 들어간다.
- 거리 계산은 표 조회와 덧셈으로 끝난다
- 쿼리는 원본 그대로 두고 조각별 거리표(LUT)를 미리 만드는 방식이 ADC이며, FAISS 기본값이다.
- 후보 하나당 표 조회 m번과 덧셈 m-1번이면 되고 곱셈과 제곱근은 필요 없다.
- IVFPQ는 잔차를 PQ로 인코딩한다
- 원본 벡터 대신 coarse centroid와의 차이(residual)를 압축한다.
- 잔차는 0 근처에 몰려 같은 바이트 예산으로 복원이 더 정확하다.
- GPU에서는 연산보다 메모리 대역폭이 병목이다
- 거리표를 shared memory에 복사하면 접근이 약 400사이클에서 약 25사이클로 줄어든다.
- 서브 밀리초 지연은 GPU 구현의 영역이다.
Community Reactions
- PQ 코드의 정확한 위치를 확인하라는 의견은 없고, 대신 원 논문이 동료 심사를 거치지 않았다는 지적이 있었다
- 댓글 작성자는 요즘은 그런 논문을 읽는 것이 시간 낭비라고 배웠다는 의견을 냈다.
- 시각화 자체는 훌륭하다고 평가했다.
- FAISS는 쓸 만하지만 패키지 호환이 아쉽다는 경험
- 댓글 작성자는 특정 용도에서 FAISS가 매우 좋았다고 했다.
- faiss-cpu 외에는 요즘 대부분의 패키지와 맞게 업데이트되지 않는다고 했다.
- Meta가 왜 유지보수를 줄였는지 궁금하다는 질문
- 속도 때문인지 우선순위 때문인지 묻는 댓글이 있었지만 답은 없었다.
- 인터랙티브 사이트가 훌륭하다는 반응
- 다른 댓글은 감사 인사와 함께 매우 훌륭한 인터랙티브 웹사이트라고 평했다.
How to Apply
- 현재 인덱스의 메모리 계산부터 해 보기
- 오늘 벡터 수 n과 차원 D로 n×D×4바이트를 계산해 Flat 인덱스 메모리를 구하라.
- 그 값이 서버 RAM의 절반을 넘으면 IVF나 IVFPQ 후보로 올려라.
- Flat 대비 IVF 속도·재현율 비교하기
- 실제 임베딩 샘플 10만 개 정도로 IndexFlatL2와 IndexIVFFlat을 둘 다 만들어라.
- nprobe를 1, 8, 64로 바꿔 가며 검색 시간과 Flat 결과와의 일치율을 기록하라.
- 메모리가 모자라면 IVFPQ로 바꾸기
- 인덱스 팩토리 문자열 'IVF1024,PQ8'로 생성해 코드가 벡터당 8바이트인지 확인하라.
- 정확도 하락이 허용 범위인지 Flat 결과와 비교해 결정하라.
- RAG 검색 설정을 조절 가능한 다이얼로 문서화하기
- 지연 시간, 메모리, 재현율 목표를 한 줄씩 적고 nprobe와 PQ 바이트 수를 거기에 맞춰라.
Try It
# 터미널에서 실행 (pip install faiss-cpu numpy)
import time
import numpy as np
import faiss
d, n, nq, k = 64, 200_000, 100, 5
rng = np.random.default_rng(0)
xb = rng.random((n, d), dtype="float32")
xq = rng.random((nq, d), dtype="float32")
flat = faiss.IndexFlatL2(d)
flat.add(xb)
t = time.time(); _, I0 = flat.search(xq, k)
print("Flat", round(time.time() - t, 4), "s")
quant = faiss.IndexFlatL2(d)
ivf = faiss.IndexIVFFlat(quant, d, 1024)
ivf.train(xb); ivf.add(xb)
for nprobe in (1, 8, 64):
ivf.nprobe = nprobe
t = time.time(); _, I = ivf.search(xq, k)
rec = np.mean([len(set(I[i]) & set(I0[i])) / k for i in range(nq)])
print("IVF nprobe", nprobe, round(time.time() - t, 4), "s recall", round(rec, 3))
pq = faiss.index_factory(d, "IVF1024,PQ8")
pq.train(xb); pq.add(xb)
pq.nprobe = 8
_, I = pq.search(xq, k)
print("IVFPQ recall", np.mean([len(set(I[i]) & set(I0[i])) / k for i in range(nq)]))Terminology
관련 논문
트랜스포머는 '기억 저장소'에서 처음부터 만들어 낼 수 있었다
선형층→MLP→self-attention을 기억 저장소로 보면 용량이 64→약 2,700→100만 이상
400년치 고문서에 AI를 돌렸더니 잊힌 운석·코뿔소·화산 폭발이 나왔다
임베딩 검색과 싼 필터 모델로 400년 고문서를 뒤져 미기록 사건 발굴
EmbeddingGemma 2: 텍스트·이미지·오디오·비디오를 한 벡터 공간에 넣는 오픈 경량 임베딩 모델
740M 오픈 모델 하나로 텍스트·이미지·오디오·비디오를 같은 벡터 공간에서 검색
RAG-NAROK: 출처를 직접 반박하는 문서로 RAG 지식베이스를 오염시키는 공격
검색된 출처를 겨냥해 반박문을 심었더니 1위 검색과 탐지 회피가 높아졌다
텍스트냐 이미지냐 둘 다냐: 멀티모달 문서 QA 입력 방식 비교
문서 QA에서 페이지 이미지가 정확도는 높지만 느려서, 질문별 라우터로 지연 30% 줄였다
MS 임원이 AI 스크래핑을 '인류 역사상 최대의 노동 도둑질'이라 불렀다는 NYT 소송 공개 문서
NYT 소송 공개 문서에서 MS·OpenAI 내부의 스크래핑 인식이 드러났다
임베딩의 보편적 기하 구조 활용: 짝 데이터 없이 임베딩 공간 간 변환
짝 데이터 없이 임베딩을 다른 모델 공간으로 옮겼고, 벡터 DB 보안 위험이 드러났다
Better Together: 강한 RAG 베이스라인 위에서 쿼리 리라이팅 조합하기
쿼리 리라이팅을 합치고 점수 낮을 때만 돌렸더니 비용 40%로 효과 절반