
1. 개요 (Overview)
RAG(Retrieval-Augmented Generation)를 구축한다고 하면 다들 벡터 DB나 LLM 모델 선정에는 목숨을 걸면서, 정작 데이터 전처리의 핵심인 '청킹(Chunking)'은 대수롭지 않게 여깁니다. 파이썬으로 대충 text.split()을 하거나, 생각 없이 랭체인(LangChain) 기본 설정을 가져다 씁니다.
하지만 데이터 규모가 위키백과 수준으로 커지는 순간, 그 안일한 코드는 시스템의 병목이 됩니다. 우리는 최근 청킹 라이브러리인 chonkie를 개발하며 이 문제를 밑바닥(Metal) 레벨까지 파고들었습니다. 결론부터 말하자면, 고성능 청킹은 추상화된 파이썬 함수가 아니라 CPU 명령어 레벨의 최적화 싸움입니다.
2. 현황 및 문제점 (Current Status & Problems)
2.1 멍청한 청킹 (Naive Chunking)의 대가
가장 흔한 실수는 N개의 문자(Character) 단위로 자르는 것입니다."The quick brown fox jumps over the la", "zy dog."
이런 식으로 문장이 끊기면 의미(Semantic)가 파괴됩니다. 검색 품질이 떨어지는 건 당연한 수순입니다.
2.2 의미론적 청킹의 딜레마
똑똑한 개발자라면 마침표(.), 물음표(?), 줄바꿈(\n) 같은 구분자(Delimiter)를 기준으로 자르려 할 것입니다. 하지만 여기서 성능 이슈가 발생합니다. 파이썬의 루프나 정규표현식은 느립니다. 수억 개의 문서를 처리할 때, CPU 사이클은 텍스트를 훑는 데에만 낭비됩니다.
2.3 하드웨어의 한계 테스트
우리는 질문했습니다. "모든 추상화를 걷어내고 하드웨어 한계까지 밀어붙이면 텍스트 청킹은 얼마나 빨라질 수 있는가?" 그 답은 memchr와 SIMD(Single Instruction Multiple Data)에 있었습니다.
3. 기술적 해결 방안 (Technical Solution)
단순히 코드를 줄이는 게 아닙니다. 바이트(Byte) 단위의 저수준 최적화 전략이 필요합니다.
3.1 memchr: 바이트 검색의 가속화
Rust의 memchr 크레이트를 벤치마킹하며 두 가지 핵심 최적화 기법을 적용했습니다.
[표 1] 최적화 기법 비교
- SWAR (SIMD Within A Register): 64비트 레지스터를 활용해 8바이트를 한 번에 처리. 루프(Loop)와 분기(Branch)를 제거함.
- AVX2 (벡터 명령어 x86_64): 한 번에 32바이트를 로드하여 비교. 대량의 데이터 처리 시 압도적 성능.
SWAR의 핵심은 XOR 연산입니다. 찾고자 하는 바이트(Needle)를 레지스터 전체에 펼쳐놓고(Splat), 청크와 XOR 연산을 수행합니다. 일치하는 바이트는 0이 됩니다. 이후 비트 연산으로 0이 존재하는지 감지합니다. 루프를 도는 것보다 훨씬 빠릅니다.
3.2 3개의 바늘(Needle) 제한과 룩업 테이블
memchr는 1~3개의 구분자까지는 효율적입니다. 하지만 4개 이상의 구분자(예: \n, ., ?, !, ;)를 쓰려고 하면 연산 오버헤드가 급증합니다. 구분자가 늘어날 때마다 비교 연산과 OR 연산 비용이 추가되기 때문입니다.
이때는 고전적인 룩업 테이블(Lookup Table)로 회귀해야 합니다.
- 256 크기의
bool배열을 생성합니다. - 각 바이트를 인덱스로 사용하여
table[byte]를 조회합니다. - 이는 분기 예측 실패가 없는 O(1) 연산입니다. SIMD보다는 느리지만, 구분자가 많을 때는 훨씬 효율적입니다.
3.3 역발상: 뒤에서부터 검색하라 (Reverse Search)
가장 중요한 최적화는 검색의 '방향'입니다. 보통 0번 인덱스부터 앞으로(Forward) 검색하지만, 이는 비효율적입니다.
- Forward Search (비효율): 윈도우 시작점부터 끝까지 훑으며 구분자를 찾습니다. 경계를 넘을 때까지 발견된 모든 구분자의 위치를 기억해야 합니다. 연산 횟수가 많고 상태 관리가 필요합니다.
- Backward Search (효율):
chunk_size위치(예: 100바이트)에서 시작해 뒤로(Reverse) 검색합니다. 가장 먼저 발견되는 구분자가 곧 최적의 분할 지점입니다. 단 한 번의 검색으로 끝납니다.
4. 핵심 요약 및 제언 (Conclusion)
고성능 RAG 시스템을 구축하고 싶다면 다음 세 가지를 기억하십시오.
- SIMD를 활용하십시오: 바이트 검색은 CPU 벡터 명령어를 타야 합니다. 파이썬 레벨에서 문자열을 조작하고 있다면 이미 느린 겁니다.
- 구분자 개수에 민감해지십시오: 구분자가 3개 이하라면
memchr같은 최적화 라이브러리를, 그 이상이라면 룩업 테이블 방식을 사용하십시오. - 뒤에서 찾으십시오: 청킹의 목적은 '최대 크기 내에서 가장 마지막 의미 단위'를 찾는 것입니다. 뒤에서부터 찾는 것이 논리적으로, 연산적으로 무조건 유리합니다.
코드는 자산이 아니라 부채입니다. 느리고 멍청한 코드를 서버에 쌓아두지 마십시오. 하드웨어가 어떻게 동작하는지 이해하고, 그에 맞춰 로직을 작성해야 클라우드 비용 청구서를 보고 놀라지 않을 것입니다.


