공부/인공지능

게이트를 만들었다가 걷어냈다 — 로컬 LLM 멈춤의 진짜 원인 찾기

TechToast 2026. 10. 5. 23:45
반응형

게이트를 만들었다가 걷어냈다 — 로컬 LLM 멈춤의 진짜 원인 찾기

 

컨텍스트가 큰 로컬 LLM을 여러 세션으로 나눠 쓰다 보면, 세션을 셋째 개 열 때쯤 전체 요청이 뻗어버리는 일이 반복됐습니다. 대부분의 운영 이야기에서 이 시점의 결론은 하나로 정해져 있습니다. 「KV 캐시가 부족해서 그렇다」는 것. 저도 그랬습니다.

 

하지만 이번에는 그 결론이 틀렸습니다. 그래서 원인을 고치겠다고 게이트웨이를 먼저 만들었고, 한참을 돌고 나서 그 게이트를 통째로 걷어냈습니다. 만들었다가 지운, 그 과정과 이유를 적어봅니다.

증상과 오진: 「컨텍스트를 줄이자」가 아니라 「접수 창구를 만들자」

멈춤 현상은 이렇게 옵니다. 한두 개 세션은 거뜬히 돌아가는데, 세 번째 세션에 요청이 들어오면 전체가 미적대다가 폴백 모델로 떨어집니다. 운용 중인 모델은 컨텍스트가 꽤 큰 모델이라, 대화를 길게 유지하는 세션이 늘어날수록 각 세션의 컨텍스트 합이 커집니다.

 

제가 처음에 내린 진단은 이랬습니다. 「각 세션의 컨텍스트를 다 합치면 KV 캐시 풀을 넘어가서, 모델이 기존 내용을 밀어내는 선점(preemption)이 일어나고 그 결과 전체가 느려지거나 멈춘다.」 그래서 요청을 큐에 줄 세워 보내주는, 이른바 Admission Gate를 FastAPI로 직접 만들어 메인 경로에 끼워 넣었습니다. 건물 입구에 접수 창구를 하나 세워서, 들어오는 손님을 순서대로 들여보내되 풀이 꽉 찼으면 로비에서 기다리게 하는 셈입니다. 한 손님이 들어가는 사이 다른 손님을 몰아내지 않으려는 설계였습니다.

게이트의 문제: 접수 창구가 자체로 장애 지점이 된다

게이트는 잘 작동했습니다. 다만 그게 문제의 해결책이 아니라, 문제의 일부가 된다는 걸 깨달았습니다. 게이트는 어디까지나 별도 프로세스입니다. 이 접수 창구가 죽으면 모든 요청이 그대로 폴백 모델로 떨어집니다. 즉 「모델이 멈추는」 일을 막으려 만든 장치 그 자체가, 지금까지 없던 단일 장애 지점(SPOF)이 된 겁니다. 시스템 서비스로 전환해 안정화하긴 했지만, 어차피 불필요한 경유지 하나를 더 두고, 그 경유지가 죽을 리스크까지 짊어진 셈이라 마음이 편치 않았습니다.

재점검: 느려진 진짜 원인은 전혀 다른 곳에 있었다

한참을 운영하다 문득 발상을 돌렸습니다. 정말 컨텍스트가 풀을 넘었나. 부하 테스트로 직접 재보니, 실제로 느려졌던 원인은 컨텍스트 부족이 아니라 모델 엔진의 Prefill 성능 저하였습니다. Prefill은 모델이 입력 문장을 처음부터 끝까지 읽으며 중간 계산을 쌓는 단계인데, 이 단계의 속도가 운용 시간이 쌓일수록 계속 떨어지고 있었습니다. 어느 순간 읽기 속도가 바닥을 치면 그 뒤로 오는 모든 요청이 병목에 걸렸습니다.

 

그리고 이 성능 저하는 엔진(모델 서버)을 재시작하는 것 하나로 풀렸습니다. 컨텍스트를 아껴쓰거나 요청을 줄 세우거나, 그런 게 아니라 그냥 다시 켜니 말끔히 돌아왔습니다.

게이트 제거와 직결 복구: 원래 이미 그 기능이 있었다

진짜 원인을 찾고 나니 보이는 게 있었습니다. vLLM에는 --scheduler-reserve-full-isl 옵션이 있는데, 이 값은 기본이 True입니다. 이 옵션은 「요청이 가진 컨텍스트가 KV 캐시에 통째로 들어갈 자리가 없으면, 기존 내용을 밀어내는 선점을 하지 않고 그냥 큐에서 기다리게」 만듭니다. 즉 제가 FastAPI로 만든 접수 창구의 일을, 모델 엔진이 처음부터 기본값으로 하고 있었던 것입니다.

 

그래서 외부 게이트를 제거하고 메인 경로를 게이트 경유에서 모델 서버 직결로 복구했습니다. 흐름은 이렇게 바뀌었습니다. [게이트 경유] → [직결].

 

이번에 실측한 숫자도 남겨둡니다. KV 캐시 풀 크기는 약 2.42M 토큰입니다. 다중 노드로 분산된 TP2 구성이라 /metrics의 노드당 값이 아니라 부팅 로그의 GPU KV cache size로 확인했습니다. 이 풀을 기준으로 1M 풀컨텍스트로는 동시성 한계가 약 2.31배였습니다. 즉 풀컨텍스트 세션 두 개가 사실상 한계고, 세 번째는 대기열에 밀리거나 폴백됩니다. 처음에 「컨텍스트 부족」이라고 오진했던 근거가 바로 이 숫자였습니다. 세 번째가 안 되는 건 어쨌든 사실이었지만, 그건 멈춤의 직접 원인이 아니었습니다.

 

한 가지 덧붙이면 --watermark는 적용하지 않았습니다. 이 옵션은 KV 풀의 일부 비율을 여유분으로 비워두게 하는데, 전체 풀의 15%를 비워두면 「딱 들어갈 뻔한」 큰 컨텍스트 요청이 그 여유분 때문에 대기열로 밀려나 오히려 불리합니다.

사용자 평가 / 한계

좋은 점부터. 「컨텍스트 부족」이라는 눈속임을 벗어냈고, 가끔은 엔진 재시작 하나로 풀리는 경우가 있다는 걸 알게 됐습니다. 그리고 불필요한 외부 게이트라는 단일 장애 지점을 없앤 것도 명확한 이득입니다.

 

아쉬운 점도 정직하게 적습니다. 처음에 오진을 해서 게이트를 만드느라 시간을 꽤 썼습니다. vLLM의 네이티브 옵션 하나를 먼저 확인했더라면 한참을 아꼈을 겁니다. 원인을 고치기 전에, 엔진이 이미 해주는 게 있는지부터 봤어야 했습니다.

 

후속 계획으로는, 멀티 유저나 여러 파드(컨테이너)로 이관할 때는 이 게이트를 재사용할 여지가 있습니다. 그때까지는 보관해두고, 그 전에는 기본 기능만으로 충분하다는 게 지금의 결론입니다.

반응형