turbovec: 60GB의 비용 없이 구현하는 로컬 RAG
요약
turbovec은 대규모 벡터 코퍼스를 위한 훈련 불필요 양자화 라이브러리입니다. 무작위 회전 기술을 통해 데이터 분포에 상관없이 고효율 압축을 구현하며, 메모리 사용량을 획기적으로 줄이면서도 빠른 검색 성능을 제공합니다.
핵심 포인트
- 무작위 회전을 통해 사전 훈련 없이 최적의 양자화 가능
- 1,000만 개 문서 기준 메모리 사용량을 약 8배 절감
- SIMD 커널 활용으로 FAISS 대비 12-20% 빠른 성능 구현
- MSE 왜곡을 정보 이론적 하한선에 근접하게 유지
1536차원의 float32 임베딩 (embedding)은 6 KB입니다. 1,000만 개의 문서로 구성된 코퍼스 (corpus)는 인덱스 오버헤드 (index overhead)를 고려하기 전의 원시 벡터 (raw vectors)만으로도 대략 60 GB에 달합니다. 이는 노트북의 RAM에 담을 수 없으며, 64 GB 용량의 머신이라 할지라도 다른 작업을 위한 여유 공간이 전혀 남지 않게 됩니다.
저는 계속해서 FAISS를 사용해 왔습니다. 잘 작동하지만, 두 가지 마찰 지점에 계속 부딪혔습니다. 첫째, 훈련 (training)을 위해서는 사전에 코퍼스의 대표 샘플이 필요하다는 점이고, 둘째, 압축 품질이 해당 샘플이 실제 분포 (distribution)와 얼마나 잘 일치하느냐에 달려 있다는 점입니다. 데이터 분포가 변하면 다시 구축해야 합니다.
turbovec은 이 두 가지 문제를 모두 해결하며, TurboQuant 논문 (arXiv 2025년 4월, Google Research + NYU)은 왜 이 방식이 훈련 단계를 완전히 건너뛸 수 있는지에 대한 수학적 근거를 설명합니다.
TurboQuant이 실제로 하는 일
핵심 아이디어는 수학적 트릭입니다. 벡터를 압축하기 전에 무작위 회전 (random rotation)을 적용하는 것입니다.
회전 후에는 각 좌표 (coordinate)가 고차원에서 가우시안 분포 $N(0, 1/d)$로 수렴하는 스케일링된 베타 분포 (Beta distribution)를 따르게 됩니다. 또한 좌표들은 거의 독립적이게 됩니다. 이러한 조합이 훈련이 필요 없는 양자화 (quantization)를 가능하게 합니다. 즉, 사전에 데이터가 필요 없이 순수 수학만으로 최적의 버킷 경계 (bucket boundaries)를 미리 계산할 수 있습니다.
알고리즘의 4단계는 다음과 같습니다:
- 각 벡터를 단위 길이로 정규화 (Normalize) 합니다. 이때 노름 (norm) 값을 float로 저장합니다.
- 고정된 무작위 회전 행렬 (random rotation matrix)을 적용합니다 (설정 시 한 번만 계산되며 전체 인덱스에 동일한 행렬을 사용합니다).
- 미리 계산된 버킷 경계에 따라 각 회전된 좌표를 양자화 (Quantize) 합니다. 4비트 (4-bit) 기준으로는 좌표당 16개의 버킷이 생성됩니다.
- 정수들을 패킹 (Pack) 합니다. 1536차원 벡터는 6,144 바이트 (float32)에서 384 바이트로 줄어듭니다.
1,000만 개의 문서 코퍼스의 경우, 약 60 GB의 float32 데이터가 4비트에서는 약 7.5 GB로 줄어들어 8배의 감소 효과를 얻습니다. 논문에 따르면 MSE 왜곡 (MSE distortion)은 어떤 비트 너비(bit-width)에서도 정보 이론적 하한선 (information-theoretic lower bound)의 $\sqrt{3}\cdot\pi/2 \approx 2.7$ 배 이내에 머무름을 증명하며, 이는 훈련이 필요 없는 방식치고는 매우 정밀한 수치입니다. 특히 4비트에서 MSE는 약 0.009입니다.
검색 과정에서는 벡터를 압축 해제하지 않습니다. 쿼리를 동일한 도메인으로 한 번 회전시킨 후, SIMD 커널(ARM의 NEON, x86의 AVX-512)을 사용하여 코드북 중심점(codebook centroids)과 점수를 비교합니다. turbovec 자체 벤치마크에 따르면, ARM 환경에서 FAISS IndexPQFastScan보다 12-20% 더 빠른 성능을 보여줍니다.
처음에 제가 간과했던 부분: MSE와 내적(inner product)은 서로 다른 문제입니다
RAG(검색 증강 생성)에서 중요한 것은 유사도 점수(similarity scores)를 보존하는 것이며, MSE(평균 제곱 오차)에 최적화된 양자화기(quantizers)는 이를 수행하지 못합니다.
벡터 인덱스를 검색할 때, 여러분은 쿼리와의 내적(dot product)이 가장 높은 저장된 벡터를 찾는 것입니다. TurboQuant 논문은 재구성 정확도(reconstruction accuracy)만을 순수하게 최적화한 양자화기가 내적 추정치에 편향(bias)을 유발한다는 것을 증명합니다. 압축된 벡터는 정확하게 재구성되지만, 쿼리 벡터와의 유사도 점수는 체계적으로 어긋나게 됩니다. 결과적으로 잘못된 최근접 이웃(nearest neighbors)을 얻게 됩니다.
TurboQuant은 2단계 접근 방식을 통해 이를 해결합니다. 1단계에서는 목표 예산보다 1비트 적은 수준에서 MSE 양자화를 적용하여(예: 총 4비트를 원한다면 3비트 적용), 재구성 오차를 최소화하고 잔차(residual)를 최대한 줄입니다. 2단계에서는 해당 잔차를 가져와 QJL(Quantized Johnson-Lindenstrauss)이라고 불리는 1비트 무작위 투영 변환(random projection transform)을 적용합니다. QJL은 최적의 1비트 내적 양자화기입니다. 이는 sign(random_matrix · vector)를 사용하여 차원당 잔차를 단 1비트로 줄이며, 논문은 이 방식이 결합된 추정치를 편향되지 않게(unbiased) 만든다는 것을 증명합니다.
이 모든 과정은 데이터 무관(data-oblivious)하게 작동합니다. 인덱스에 추가하는 첫 번째 벡터부터 바로 작동합니다. 그 결과, 목표 비트 너비(bit-width)에서 거의 최적에 가까운 재구성 정확도와 편향되지 않은 유사도 점수를 얻을 수 있습니다.
긴 문맥을 처리하는 LLM(대규모 언어 모델)의 KV 캐시 압축(Attention의 Key와 Value 저장)을 위해, 논문은 LongBench-E에서 Llama-3.1-8B를 테스트했습니다. 채널당 3.5비트는 양자화되지 않은 품질과 일치하며, 2.5비트는 미미한 성능 저하만을 보이면서도 캐시를 5배 이상 압축합니다. 내적의 무편향성(unbiasedness) 특성이 바로 어텐션(attention) 계산에서 이 방식이 작동하게 만드는 핵심입니다.
실무적인 부분: 단 하나의 임포트(import) 교체
turbovec은 LangChain, LlamaIndex, Haystack, 그리고 Agno의 인메모리 벡터 저장소(in-memory vector stores)를 즉시 교체할 수 있는(drop-in replacements) 라이브러리를 제공합니다. LangChain의 경우 다음과 같습니다:
pip install turbovec[langchain]
# Before
from langchain_core.vectorstores import InMemoryVectorStore
...
파이프라인의 다른 모든 부분은 동일하게 유지됩니다. 저는 기존 LangChain 프로젝트에 이를 몇 분 만에 교체해 보았습니다. 메모리 사용량은 약 8배 감소했고, 검색(retrieval) 속도는 약간 더 빨라졌습니다.
IdMapIndex의 경우 (삭제 후에도 유지되는 안정적인 ID가 필요한 경우):
from turbovec import IdMapIndex
import numpy as np
...
pgvector 벤치마크가 실제로 보여주는 것
저는 pgvector와 함께 사용하기 위해 turboquant를 탐색해 왔습니다. 성능을 평가하기 위해 Johann-Peter Hartmann이 만든 RAG 벤치마크를 실행했습니다.
저장 공간(storage)과 인덱스 스캔(index scan)에서의 이점은 실질적입니다. 4비트(4-bit) 설정 시 벡터 컬럼의 크기가 약 8배 줄어들며, 메모리를 통해 이동하는 데이터 양이 훨씬 적어지기 때문에 인덱스 스캔이 더 빠르게 실행됩니다. 대규모 코퍼스(corpus)에서는 이 차이가 매우 유의미합니다.
검색 품질(retrieval quality) 측면의 이야기는 다소 복잡합니다. pgvector 내부에서 양자화(quantizing)를 수행하면 전체 float32 검색과 비교했을 때 재현율(recall)이 눈에 띄게 저하됩니다. top-k 윈도우에서 실제 상위 후보들을 놓칠 수 있습니다. TurboQuant의 비편향성(unbiasedness) 증명은 수학적으로 정확하지만, 비편향 내적 추정치(unbiased inner product estimates)는 4비트에서 여전히 분산(variance)을 수반하며, 밀집 검색(dense retrieval)에서 이 분산은 결과값을 흔들어 놓습니다. float32에서 두 번째로 좋았던 문서가 4비트에서는 top-10 안에 나타나지 않을 수도 있습니다.
이러한 트레이드오프(trade-off)가 여전히 합리적인 두 가지 경우가 있습니다: 근사 검색(approximate retrieval)이 허용되는 저장 공간 제약적 배포 환경, 또는 어차피 크로스 인코더(cross-encoder)로 재순위화(rerank)를 수행하는 파이프라인(재순위화 모델이 검색 노이즈를 복구함)입니다. 만약 실제 최상위 결과를 놓치는 것이 중요한 의미를 갖는 시맨틱 검색(semantic search)을 운영 중이라면, 적용하기 전에 홀드아웃 세트(held-out set)에서 재현율을 측정하십시오.
자신의 코퍼스를 대상으로 직접 이 비교를 수행하고 싶다면, 제가 사용한 벤치마크 설정은 다음과 같습니다:
import numpy as np
import time
from turbovec import IdMapIndex
...
훈련 단계도, 코드북 (codebook) 워밍업도 필요 없습니다. 인덱스는 첫 번째 add_with_ids 호출 직후 바로 검색 가능한 상태가 됩니다. 실제 임베딩 (embeddings)과 ID를 교체한 다음, 동일한 비트 너비(bit-width)를 가진 FAISS IndexPQFastScan을 대상으로 동일한 타이밍 루프를 실행하여 직접 비교해 보십시오.
FAISS가 여전히 적합한 도구인 경우
turbovec는 인메모리 플랫 인덱스 (in-memory flat index)입니다. 즉, 매 쿼리마다 모든 벡터를 검색합니다. 단일 머신에서 수백만 개의 벡터를 다루는 경우에는 문제가 없습니다. 하지만 수억 개의 벡터 규모가 되면 검색 범위를 줄이기 위해 IVF 파티셔닝 (IVF partitioning)이 필요하며, FAISS는 이를 지원합니다.
ARM 환경에서의 결과는 명확합니다. turbovec는 일반적인 구성에서 FAISS IndexPQFastScan보다 12-20% 더 나은 성능을 보입니다. x86 환경은 조건에 따라 다릅니다. 4비트(4-bit) 환경에서는 더 타이트한 캐시 라인 (cache lines)과 빠른 비트 언패킹 (bit-unpacking) 덕분에 turbovec가 1-6% 앞섭니다. 단일 스레드(single-threaded) 2비트 환경에서는 두 방식의 차이가 1% 이내로 나타납니다. AVX-512 하드웨어 기반의 멀티 스레드(multi-threaded) 2비트 환경에서는 FAISS가 2-4% 앞서 나갑니다. FAISS는 병렬 스윕 (concurrent sweeps) 중 비트 조작을 위해 AVX-512 VBMI를 활용하는데, 이는 turbovec가 아직 사용하지 않는 명령어 경로 (instruction path)입니다. 스레드 수가 많은 엔터프라이즈급 x86 서버의 2비트 환경에서는 이 차이가 실질적으로 나타납니다.
고차원 (d=1536, d=3072)에서는 turbovec가 R@1에서 FAISS와 대등하거나 더 나은 성능을 보입니다. 두 방식 모두 k=4-8 범위에서 재현율 (recall) 1.0으로 수렴합니다. d=200 (GloVe 영역)에서는 무작위 회전 (random rotation)을 통한 근사 가우시안 (near-Gaussian) 근사가 저차원에서 약해지기 때문에 turbovec가 R@1에서 뒤처집니다.
결론적인 규칙은 다음과 같습니다: 현대적인 임베딩 차원을 사용하는 로컬 RAG에는 turbovec를, 매우 큰 코퍼스 (corpora), GPU 가속 검색, 또는 AVX-512 서버에서의 멀티 스레드 2비트 조회를 위해서는 FAISS를 사용하십시오.
제가 활용하고 있는 용도
저는 ThoughtForge에서 공간별 의미론적 검색 (per-space semantic search)을 위해 turbovec를 실행하고 있습니다. nomic-embed-text-v1.5 모델은 768차원 임베딩을 생성하며, 4비트 압축 시 전체 인덱스 크기가 매우 작아 애플리케이션 시작 시 로딩하는 데 1초도 걸리지 않습니다. 로컬 임베딩, 로컬 인덱스를 사용하여 데이터가 머신 외부로 유출되지 않습니다.
만약 로컬 RAG를 구축 중인데 float32 메모리 벽 (memory wall)에 부딪혔다면, 이것이 제가 가장 먼저 시도해 볼 만한 방법입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기