코드 의미 검색을 만들었다
이날은 코드 의미 검색 기능을 만들면서 onnxruntime 교착 상태를 디버깅한 날이다.
핵심 교훈은 “멈춘 이벤트 루프는 자기 자신을 감시할 수 없다”였다.
그리고 통과한 테스트가 실제 규모에서는 아무 의미가 없었다는 것도 깨달았다.
Cursor 에디터의 @codebase 같은 의미 검색을 pi 에이전트에 추가했다.
키워드 검색만으로는 한계가 있다.
“인증”이라고 검색해도 authenticate 함수를 못 찾는다.
의미가 같은데 단어가 다르니까. 해결은 키워드 점수랑 의미 점수를 합치는 거였다.
키워드는 BM25로 매칭하고 의미는 임베딩 벡터 코사인 유사도로 매칭한다.
두 점수를 역순위 융합으로 합친다.
비용은 인덱싱 시점에, 질의 시점이 아니라
여기서 중요한 원칙이 있다.
비싼 작업은 인덱싱할 때 하고 질의할 때는 싸게 해야 한다.
인덱싱할 때 각 코드 조각의 임베딩을 미리 계산한다.
질의할 때는 질의 임베딩만 계산해서 코사인 유사도로 비교한다.
수십 밀리초면 된다.
반대로, 질의마다 LLM을 호출하면 1~3초가 걸린다.
명백한 안티패턴이다.
데몬이 응답하지 않았다
코드 인덱싱 데몬이 살아있는데 응답하지 않았다.
프로세스는 있고 모델도 156메가바이트 로드되어 있고 CPU도 1.5% 쓰고 있었다.
근데 상태 확인에 응답이 없었다.
백그라운드 인덱싱이 4%에서 멈춰 있었다.
원인: 동기 호출이 이벤트 루프를 얼렸다
onnxruntime의 session.run()은 동기 네이티브 호출이었다.
비동기 스케줄링 함수로 비동기 래핑을 해도, 실제 네이티브 바인딩 실행 자체가 이벤트 루프 전체를 막는다.
반복적인 재진입 부하가 걸리면 onnxruntime 내부 스레드풀이 영구 교착 상태에 빠진다.
이벤트 루프가 영원히 멈춘다.
멈춘 루프는 자기 자신을 못 본다
여기서 핵심 교훈이 나온다.
멈춘 이벤트 루프는 자기 자신을 감시할 수 없다.
모든 와치독 타이머가 같은 루프에서 돈다.
루프가 멈추면 와치독도 멈춘다.
같은 프로세스 안에서 자가 치유는 불가능하다.
별도 프로세스나 스레드가 필요하다.
해결: 별도 워커 스레드
추론을 전부 워커 스레드로 격리했다.
메인 스레드는 소켓과 큐와 와치독만 처리한다.
워커가 멈추면, 건강한 메인 스레드의 와치독이 워커를 종료하고 재시작한다.
onnxruntime을 버리고 llama.cpp로
근데 onnxruntime이 워커 스레드에서도 불안정했다.
환경 의존적인 네이티브 교착 상태가 계속됐다.
그래서 추론 엔진을 전부 onnxruntime에서 llama.cpp로 바꿨다.
속도도 3배 빨라졌다.
p50 1.39밀리초 vs 3.17밀리초. 교착 상태도 없어졌다.
백엔드 교체는 엔진만 바꾸는 게 아니다
네이티브 런타임을 교체하면 5개의 연쇄 함정이 나타났다.
데몬이 인덱서의 작업 디렉토리를 물려받아서 디렉토리 에러. 모델 이름이 여전히 이전 엔진을 가리킴.
캐시 디렉토리 불일치. 잔류 프로세스.
설정 동기화. 백엔드 교체는 “엔진 교체 + 활성 설정 동기화 + 잔류 프로세스 정리”를 한 묶음으로 해야 한다.
엔진만 바꾸면 안 된다.
통과한 테스트가 실제 규모에서는 의미가 없었다
이게 가장 뼈아픈 교훈이었다.
검증 루프가 통과했다.
26개 단위 테스트 + 3개 E2E 테스트. 전부 통과했다.
근데 이 테스트들은 전부 가짜 임베딩 모델을 쓰거나, 2~3개 조각짜리 데이터로 돌아갔다.
실제 프로젝트 규모의 백그라운드 인덱싱 병목은 한 번도 테스트되지 않았다.
테스트가 통과해도 “검증”이 아니다
게이트 검증은 테스트가 덮는 범위 안에서만 유효하다.
가짜 전송 계층으로 통과한 테스트가 실제 규모에서도 통과한다는 보장은 없다.
“프로덕션 준비 완료”라고 선언하기 전에, 최소 하나의 실제 규모 통합 테스트를 돌려야 한다.
가짜 데이터로 통과한 테스트는 위로할 뿐이다.
마무리
이날의 핵심은 두 가지였다.
첫째, 멈춘 이벤트 루프는 자기 자신을 감시할 수 없다.
같은 루프에서 자가 치유는 불가능하다.
별도 프로세스나 스레드가 필요하다.
이건 모든 동기 교착 상태에 적용된다.
둘째, 통과한 테스트가 실제 규모에서 통과한다는 보장은 없다.
테스트가 덮는 범위를 정확히 알아야 한다.
가짜 데이터로 통과한 테스트는 “이 로직은 맞다”를 보일 뿐, “이 규모에서 작동한다”를 보이지 않는다.
비용은 인덱싱 시점에. 질의는 싸게.
그리고 테스트는 실제 규모로.