SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-27

셀프 호스팅 LLM 동시 사용자 증가 시 느려지는 이유

셀프 호스팅 LLM에서 사용자가 늘어날 때 속도가 저하되는 원인을 분석합니다. Ollama의 기본 병렬 처리 설정값과 배치 처리, KV 캐시, 메모리 대역폭이 동시 접속 성능에 미치는 영향을 설명하며, 서버 효율을 최적화하여 대기열 문제를 해결하는 구체적인 방법을 제시합니다.

셀프 호스팅 LLM은 왜 사용자가 늘어나면 느려지나요?

셀프 호스팅 LLM에서 동시 사용자 5명이 접속했을 때 속도가 저하되는 이유는 서버가 한 번에 하나의 응답만 생성하고 나머지 4명은 대기열에서 기다리기 때문입니다. Ollama 문서에서는 기본 설정에 대해 다음과 같이 명확히 밝히고 있습니다. OLLAMA_NUM_PARALLEL는 "각 모델이 동시에 처리할 최대 병렬 요청 수이며, 기본값은 1입니다." 시스템에 문제가 있는 것이 아닙니다. 5명 중 4명은 순서를 기다리고 있는 상태입니다.

이 문제를 해결하기 위해 더 큰 서버를 도입하는 경우는 드뭅니다. 해결책은 여러 요청을 동일한 포워드 패스(forward pass)에서 모델로 밀어 넣는 서빙 엔진을 사용하고, 동시에 모든 사용자의 대화 내용을 유지할 수 있는 충분한 여유 메모리를 확보하는 것입니다. 두 가지 요소 모두 중요하며, 특히 후자가 실제 서비스의 처리 한계를 결정합니다.

모든 요청이 거치는 두 단계

Prefill은 프롬프트 전체를 한 번에 읽어 들여 어텐션 캐시를 구축합니다. 모든 프롬프트 토큰이 모델을 함께 통과하므로, prefill은 하나의 거대한 행렬 곱셈 연산이며 산술 처리량(arithmetic throughput)에 의해 제한됩니다. 반면 Decode는 답변을 한 번에 한 토큰씩 작성합니다. 각 토큰을 생성할 때마다 모델의 전체 가중치를 메모리에서 다시 읽어야 하지만, 단일 토큰에 수행되는 산술 연산량은 매우 적습니다. 따라서 Decode는 메모리 대역폭(memory bandwidth)에 의해 제한됩니다.

이러한 비대칭성이 바로 배치(batching)가 효과적인 이유입니다. 한 사용자를 위해 디코딩할 때 토큰당 약 5 GB의 가중치를 읽게 되는데, 이때 대부분의 산술 연산 장치는 유휴 상태로 남습니다. 여기에 두 번째 요청을 추가하면 엔진은 동일한 5 GB를 한 번만 읽고 두 개의 토큰을 계산해 냅니다. 두 번째 사용자를 처리하는 데 드는 추가 시간은 거의 없습니다. 요청을 엄격하게 순차적으로 처리하면 이러한 이점을 모두 잃게 됩니다.

사용자가 체감하는 성능은 두 가지 수치로 설명됩니다. TTFT(time to first token)는 대기 시간과 prefill 시간을 합친 것입니다. ITL(inter-token latency)은 스트리밍되는 토큰 사이의 간격이며, 이는 decode 단계에 의해 결정됩니다. 서버가 느리다면 보통 이 두 가지 중 하나에서 문제가 발생하며, 해결 방법도 서로 다릅니다. 설정을 변경하기 전에 어떤 문제와 싸우고 있는지 파악하는 것이 중요하며, prefill과 decode의 시간을 개별적으로 측정하는 것이 그 확인 방법입니다.

정적 배칭은 가장 느린 응답을 기다리게 만듭니다

정적 배칭은 가장 단순한 방식이며, 애플리케이션 코드에서 직접 요청을 그룹화할 때 나타나는 형태입니다. 엔진은 N개의 요청을 수집하여 함께 실행하며, 그룹 내에서 가장 긴 생성이 완료될 때까지 모든 슬롯을 점유합니다.

1,200 토큰 요약을 요청한 사용자가 있으면, 한 줄짜리 답변 4개도 배치 내에 묶여 있게 됩니다. 배치는 가장 느린 작업이 완료되기 전까지 슬롯을 해제하지 않기 때문입니다.

이로 인해 두 가지 비용이 발생합니다. 완료된 시퀀스가 유용한 연산을 수행하지 않으면서 슬롯을 계속 점유하므로, 출력 길이가 달라질 때마다 실제 처리량(throughput)이 감소합니다. 챗봇의 출력 길이는 매우 가변적입니다. 배치가 구성된 직후에 도착한 요청은 전체 배치가 비워질 때까지 대기해야 프리필(prefill)을 시작할 수 있습니다. 즉, 해당 요청의 TTFT(Time To First Token)가 다른 사용자의 긴 글에 의해 결정되는 것입니다.

연속 배치 처리는 토큰 단위로 요청을 수락하고 종료합니다

연속 배치 처리(continuous batching)는 단일 디코딩 단계 수준에서 스케줄링을 수행합니다. 각 단계가 끝날 때마다 스케줄러는 종료 토큰을 출력한 시퀀스를 즉시 제거하고, 빈 슬롯에 대기 중인 요청을 수락합니다. 40단계에서 끝나는 응답은 배치가 완전히 종료될 때까지 기다리지 않고 40단계에서 즉시 슬롯을 반환합니다.

이는 특수한 기능이 아닙니다. llama-server 문서는 -cb, --cont-batching를 "연속 배치 처리(동적 배치 처리라고도 함) 활성화 여부 (기본값: 활성화)"로 설명하며, vLLM은 이 개념을 중심으로 설계되었습니다. Ollama 또한 병렬 요청을 처리합니다. 기본 설정이 동시성 개수를 1로 제한하고 있기 때문에, 많은 사용자가 자신의 하드웨어로는 동시 처리가 불가능하다고 오해합니다. 하지만 이는 설정상의 제한일 뿐입니다.

연속 배치 처리의 성능 결과는 일반적으로 여유 연산 자원과 수십 GB의 캐시 메모리를 갖춘 데이터센터용 카드에서 측정됩니다. 이러한 결과의 경향성은 개인용 서버에서도 동일하게 나타나지만, 절대적인 규모는 다릅니다. 그 이유는 아래의 메모리 섹션에서 설명합니다.

Prefill은 decode와 동일한 연산 자원을 두고 경쟁합니다

4개의 응답이 스트리밍되는 도중에 새로운 요청이 들어오면, 해당 요청의 프롬프트는 먼저 prefill 과정을 거쳐야 하며 prefill은 연산 집약적인 작업입니다. 스케줄러가 이 prefill에 별도의 단계를 할당하면, 스트리밍 중인 4명의 사용자는 그동안 토큰을 하나도 받지 못합니다. 프롬프트가 길 경우, 이는 모든 열려 있는 창에서 눈에 띄는 멈춤 현상으로 나타납니다. 누군가 전송 버튼을 누를 때마다 서버가 멈칫거린다고 사용자들이 말할 때, 그들이 느끼는 끊김 현상이 바로 이것입니다.

Chunked prefill은 긴 프롬프트를 여러 조각으로 나누어, 각 조각을 실행 중인 decode 단계와 동일한 단계에서 섞어서 처리합니다. vLLM 튜닝 가이드는 이 트레이드오프를 명확히 설명합니다. chunk 예산이 작을수록 "decode를 느리게 만드는 prefill 작업이 줄어들어 더 나은 ITL을 달성"할 수 있고, 값이 클수록 "배치 내에서 더 많은 prefill 토큰을 처리할 수 있어 더 나은 TTFT(Time To First Token)를 달성"할 수 있습니다. 즉, 응답이 시작되기를 기다리는 사용자와 텍스트가 스트리밍되는 것을 지켜보는 사용자 중 누구의 경험을 우선할지 선택해야 합니다.

프롬프트 길이는 이러한 현상이 얼마나 심각하게 나타날지를 결정합니다. 6,000 토큰의 프롬프트에 200 토큰의 답변이 달리는 경우, 6,000 토큰의 prefill 작업이 200번의 decode 단계와 맞서게 됩니다. RAG(Retrieval-augmented generation) 기반 채팅이나 긴 시스템 프롬프트는 모두 이러한 상황을 유발하므로, prefill은 무시할 수 있는 수준을 넘어 사용자가 대기하게 만드는 주된 요인이 됩니다. 긴 부분이 반복될 때는 Prefix caching이 도움이 됩니다. vLLM은 --enable-prefix-caching 기능을 제공하며, 이는 공유 프롬프트 접두사에 대해 캐시를 재사용함으로써 모든 요청마다 다시 계산할 필요를 없애줍니다.

가장 먼저 고갈되는 메모리는 KV cache입니다

활성 대화의 모든 토큰은 모델의 각 레이어에 키 벡터와 값 벡터를 남깁니다. 이것이 KV cache(key/value cache)이며, 디코딩 과정에서 새로운 토큰마다 전체 프롬프트를 다시 계산하지 않게 해주는 핵심 요소입니다. 토큰당 크기는 모델의 구조에 따라 고정됩니다. 즉, 2(키 1개, 값 1개) 곱하기 레이어 수, 곱하기 키/값 헤드 수, 곱하기 헤드 차원, 곱하기 값당 바이트 수입니다. 이 수치들은 모델의 config.json에서 확인할 수 있습니다.

한 번 계산해보면 메모리 한계는 더 이상 미스터리가 아닙니다. 36개 레이어, 8개 키/값 헤드, 128 헤드 차원을 가진 일반적인 8B 모델이 16비트 캐시를 유지할 경우, 토큰당 2 36 8 128 2 바이트가 소모됩니다. 이는 147,456 바이트, 즉 약 144 KiB입니다. 따라서 8,192 토큰 대화 하나당 약 1.2 GB의 캐시가 필요합니다. 대화 5개라면 모델 가중치 외에 약 6 GB가 추가로 필요하며, 이것이 수용 가능한 사용자 수를 결정하는 실질적인 답입니다.

동시성은 컨텍스트를 배가시키며, 도구들은 이를 명시적으로 밝히고 있습니다. Ollama의 FAQ는 다음과 같이 설명합니다: "특정 모델에 대한 병렬 요청 처리는 병렬 요청 수만큼 컨텍스트 크기를 증가시킵니다. 예를 들어, 4개의 병렬 요청이 있는 2K 컨텍스트는 8K 컨텍스트와 추가적인 메모리 할당을 유발합니다." 필요한 RAM은 OLLAMA_NUM_PARALLEL에 OLLAMA_CONTEXT_LENGTH를 곱한 값에 비례합니다. llama-server에서 -c로 요청한 컨텍스트는 -np 슬롯 전체에 분할되므로, 슬롯 수를 늘리는 것만으로도 각 요청이 담을 수 있는 용량은 줄어듭니다. 슬롯당 컨텍스트는 추측하지 말고 시작 로그에서 직접 확인하십시오.

vLLM은 대신 미리 할당(preallocate)하는 방식을 사용합니다. --gpu-memory-utilization(기본값 0.92)은 "모델 실행기에 사용할 GPU 메모리의 비율"입니다. 가중치를 로드하고 남은 모든 메모리는 페이징된 KV 풀이 되며, 이 풀이 부족해지면 스케줄러는 요청을 실패시키는 대신 제거(evict)합니다:

WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.

vLLM의 V1 엔진에서 기본 선점(preemption) 모드는 RECOMPUTE이므로, 제거된 요청은 캐시를 버리고 다시 진입할 때 프리필(prefill) 과정을 반복합니다. 이 작업은 두 번 수행됩니다. 문서는 "선점과 재계산은 종단 간 지연 시간에 악영향을 미칠 수 있다"고 경고하며, 이 로그 라인은 평균 지연 시간은 정상인데 특정 사용자만 유독 오래 기다려야 했던 이유를 설명하는 가장 좋은 근거입니다. disable_log_stats=False을 설정하여 누적 횟수를 기록하거나, vLLM이 노출하는 Prometheus 메트릭에서 선점 카운터를 확인하십시오.

동시 사용자 2명, 5명, 20명일 때의 변화

사용자 2명. 캐시 여유가 있는 GPU에서는 거의 차이가 느껴지지 않습니다. 두 번째 디코딩 스트림이 첫 번째 스트림에 얹혀서 처리되므로 추가 시간이 거의 들지 않기 때문입니다. 반면 4~8 GB RAM을 갖춘 CPU 전용 VPS에서는 비용이 발생합니다. 두 스트림이 소수의 vCPU와 RAM 대역폭을 공유하므로 각 사용자는 초당 토큰 처리량(TPS)이 절반으로 줄어드는 것을 체감하게 되며, 캐시 요구량은 훨씬 적은 예산 안에서 두 배로 늘어납니다.

사용자 5명. 이 지점부터는 기본 설정만으로는 부족하며, 대기열 문제가 발생하기 시작합니다. OLLAMA_NUM_PARALLEL 값이 1일 경우, 4명은 긴 답변을 요청한 사람이 끝날 때까지 기다려야 하며, 자신의 차례가 오면 정상적인 속도를 경험합니다. 병렬 처리 수를 늘리면 문제의 양상이 바뀝니다. 8K 컨텍스트를 사용하는 슬롯 5개는 총 40K 토큰의 캐시 공간을 요구합니다. 이 용량이 VRAM에 들어가지 않으면 엔진은 레이어를 시스템 RAM으로 오프로드하며, RAM에도 들어가지 않으면 시스템은 스왑을 발생시켜 초당 토큰 처리량이 급격히 붕괴합니다.

사용자 20명. 채팅 UI를 사용하는 20명의 사용자가 항상 20개의 동시 요청을 보내는 것은 아닙니다. 하드웨어를 구매하기 전에 이 점을 이해하는 것이 가장 중요합니다. 사람은 답변을 읽고 다음 질문을 하기까지 20~60초 동안 생각하므로, 세션의 대부분은 유휴 상태입니다. 하지만 20개의 에이전트나 20개의 문서 요약 작업은 유휴 시간 없이 20개의 실제 스트림이 계속 돌아가는 상황을 의미합니다. 이는 완전히 다른 사양의 장비가 필요합니다. 자신의 Ollama 서버에 코딩 에이전트를 연결한 개발자 한 명은 첫 번째 사례보다 두 번째 사례에 가깝습니다. 에이전트는 작업이 실행되는 동안 멈추지 않고 계속 요청을 보내며, 사람이 읽는 동안 발생하는 휴지기를 갖지 않기 때문입니다.

사용자가 동시 접속 상태입니까, 아니면 단순히 로그인만 되어 있습니까?

시스템 규모를 산정하기 전에 처리 중인 요청(in flight) 수를 계산하십시오. 계산식은 간단합니다. 처리 중인 요청 수는 사용자 수에 턴당 생성 시간(초)을 곱한 뒤, 턴 사이의 간격(초)으로 나눈 값입니다.

  1. 먼저 프리필(prefill)과 디코딩을 모두 포함하여 본인의 단일 스트림 속도를 측정하십시오. 타인의 하드웨어 사양에서 가져온 수치를 사용하지 마십시오. 본인의 장비에서 초당 토큰 처리 속도 측정을 수행하고 그 결과를 사용하십시오.
  2. 듀티 사이클(duty cycle)을 추정하십시오. 예를 들어 채팅 사용자 20명이 턴당 12초 동안 생성하고, 90초마다 한 번씩 턴을 수행한다면 20 * 12 / 90을 계산하여 약 2.7개의 요청이 동시에 처리 중인 것으로 볼 수 있습니다.
  3. 슬롯 수를 이 값보다 약간 높게 설정한 뒤 메모리 용량을 확인하십시오. '슬롯 수 * 요청당 컨텍스트 크기'가 실제 가용 캐시 토큰 용량 내에 들어와야 합니다.
  4. 큐(queue)를 짧게 유지하여 오버플로우 발생 시 즉시 명확하게 실패하도록 하십시오.

가용 캐시 토큰은 가중치(weights)를 제외한 잔여 메모리를 앞선 섹션에서 계산한 토큰당 비용으로 나눈 값입니다. 16비트 정밀도로 8B 모델을 구동하는 24 GB 그래픽 카드는 가중치에 약 16 GB를 사용하며, 기본 활용도 기준으로 약 6 GB의 가용 캐시를 확보합니다. 이는 8K 컨텍스트 대화 약 5개 분량입니다. 더 많은 대화를 수용하려면 요청당 컨텍스트 길이를 줄이거나 캐시를 8비트로 저장하십시오(llama-server는 --cache-type-k q8_0을 사용합니다). 두 방법 모두 무언가를 희생하여 동시 처리 능력을 확보하는 것이므로, 하드웨어에 투자하기 전에 해당 트레이드오프의 실질적인 내용을 확인하는 것이 좋습니다. GPU VPS가 API 토큰 비용 대비 손익분기점을 넘는 지점.

Ollama의 기본 설정이 부족해지는 경우

systemd로 관리되는 데몬에는 셸에서 export한 환경 변수가 전달되지 않으므로, 서비스 유닛을 통해 병렬 처리 개수를 늘려야 합니다.

sudo systemctl edit ollama.service
[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"
sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama ps

systemctl show 명령을 실행하면 방금 설정한 세 가지 변수가 출력되어야 합니다. 만약 출력되지 않는다면 드롭인 파일이 저장되지 않은 것이며, 이후 어떤 작업을 수행해도 소용이 없습니다. ollama ps 명령은 로드된 모델의 크기를 보여주는데, 이는 가중치 자체보다 큽니다. 8,192 토큰을 처리하는 슬롯 4개가 그 옆에 32,768 토큰의 캐시를 예약하기 때문입니다. 모든 모델을 GPU에 올릴 것으로 예상했음에도 PROCESSOR 열에 모델의 일부가 CPU에 있다고 표시된다면, 그래픽 카드에 남은 용량보다 더 많은 캐시를 할당한 것입니다. 두 숫자 중 하나를 줄이십시오. 일반적으로 컨텍스트 크기를 줄이는 것이 더 안전하지만, 윈도우 크기가 너무 작으면 오류를 발생시키는 대신 긴 프롬프트를 조용히 잘라버립니다. 따라서 단순히 모델이 들어갈 때까지 숫자를 낮추기보다는 num_ctx를 의도적으로 설정하는 것이 좋습니다.

큐 기본값도 다시 살펴볼 필요가 있습니다. Ollama는 최대 OLLAMA_MAX_QUEUE개의 요청을 대기열에 넣으며, "기본값은 512"입니다. 이 숫자를 넘어서면 "서버 과부하를 나타내는 503 오류"를 반환합니다. 한 번에 4개씩 처리하는 서버에서 512개 깊이의 큐를 유지하는 것은 지킬 수 없는 약속입니다. 300번째 순서에 있는 클라이언트는 자신의 차례가 오기 훨씬 전에 타임아웃되기 때문입니다. 큐를 짧게 설정하면 애플리케이션이 재시도하거나 보고할 수 있는 오류가 즉시 반환되므로, 영원히 끝나지 않는 로딩 상태보다 낫습니다.

실제로 테스트해 보십시오. 두 개의 터미널에서 동시에 요청을 두 개 보내고 두 응답을 모두 지켜보십시오. 첫 번째 요청이 끝날 때까지 두 번째 요청에서 아무런 반응이 없다면, 병렬 처리 설정이 적용되지 않은 것입니다.

실제 서빙 엔진이 비용 효율을 내는 시점

vLLM은 GPU에 여유 자원이 있고 4개 이상의 요청이 실제로 동시에 처리되는 상황에서 추가적인 설정 비용을 상쇄합니다. vLLM의 스케줄러는 토큰 단위로 작동하며, 캐시를 페이징하여 빈 조각을 재사용하고, 남는 VRAM을 유휴 상태로 두는 대신 동시성 확보에 활용합니다. 2026년 8월 기준으로 문서화된 설치 및 실행 명령어는 다음과 같습니다.

uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instruct
curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "Qwen/Qwen2.5-1.5B-Instruct",
        "messages": [{"role": "user", "content": "Say hello."}]
    }'

choices 배열이 포함된 응답은 서버가 가동 중이며 모델이 로드되었음을 의미합니다. 부하가 걸린 상황에서 중요한 설정값은 두 가지입니다. --max-num-seqs는 "단일 반복에서 처리할 최대 시퀀스 수"를 의미하며, --max-num-batched-tokens은 "단일 반복에서 처리할 수 있는 최대 토큰 수"를 의미합니다. 전자는 동시성을 제한하고, 후자는 앞서 설명한 청크 단위 프리필(chunked prefill) 예산을 결정합니다.

동시 요청이 4개 미만이거나 지원되는 GPU가 없는 환경에서 vLLM을 사용하는 것은 복잡도만 높일 뿐 이득이 거의 없습니다. vLLM은 CUDA 기반 카드를 요구하며 시작 시 메모리의 대부분을 점유하는데, 이는 4GB에서 8GB 용량의 VPS에서는 부적절한 선택입니다. 이러한 환경에서는 더 짧은 컨텍스트를 가진 작은 모델과 직접 제어 가능한 큐를 사용하는 것이 좋습니다. 서빙 엔진으로서의 Ollama와 vLLM의 차이에서 이 선택에 대해 자세히 다루며, VPS에서 Qwen 3 8B 실행하기에서는 사용자를 한 명 추가하기 전 중형 모델이 요구하는 자원 수준을 확인할 수 있습니다.

통념이 감추고 있는 트레이드오프

Continuous batching은 전체 처리량을 높이며, 대기 중인 요청이 더 빨리 시작되므로 일반적으로 중간 지연 시간(median latency)도 개선합니다. 하지만 꼬리 지연 시간(tail latency)은 반대 방향으로 움직이며, 이 절반의 이야기는 거의 언급되지 않습니다.

단계마다 시퀀스가 추가될 때마다 약간의 작업이 늘어나므로, 배치가 채워질수록 모든 사용자의 ITL은 상승합니다. 새로 도착한 요청의 prefill은 스트리밍 중인 사용자가 가졌어야 할 단계의 일부를 점유합니다. 캐시 압박이 발생하면 스케줄러는 선점(preempt)을 수행하며, 이로 인해 절반쯤 생성된 요청은 다시 prefill의 시작 단계로 돌아갑니다.

채팅 UI는 평균이 아닌 꼬리 지연 시간을 보여줍니다. 문장 중간에 2초 동안 멈추는 스트림은 전체 완료 시간이 짧더라도 사용자에게는 고장 난 것처럼 보입니다. 예상되는 부하 환경에서 p95 TTFT와 p95 ITL을 측정하십시오. 그리고 초당 평균 토큰 수는 사용자 경험을 설명하는 지표가 아닌, 용량 산정 수치로 다루어야 합니다.

실무적인 설정은 여기서 도출됩니다. 메모리가 허용하는 한도보다 약간 낮게 동시성(concurrency)을 제한하여 엔진이 선점을 수행할 필요가 없도록 만드십시오. 4초를 기다린 뒤 매끄럽게 스트리밍되는 사용자가 즉시 시작했다가 두 번 멈추는 사용자보다 더 만족하기 때문에, 스래싱(thrashing)이 발생하는 깊은 배치보다는 짧고 예측 가능한 대기열이 더 낫습니다.

속도가 느릴 때 확인해야 할 사항

사용자별 응답은 정상이나 대기 시간이 긴 경우. 이는 속도 문제가 아니라 큐(queue) 문제입니다. 먼저 병렬 처리 설정을 확인하십시오. 모델이 한 번에 하나의 요청만 처리하도록 올바르게 서비스되고 있는 상태입니다.

Ollama에서 HTTP 503 오류가 발생하는 경우. 큐가 가득 찬 상태입니다. 서버가 실제로 용량 한계에 도달했거나, 부하를 분산하기 위해 OLLAMA_MAX_QUEUE 설정이 의도적으로 낮게 지정된 경우입니다. 후자는 의도한 대로 동작하는 것입니다.

CPU 서버에서 부하 발생 시 초당 토큰 처리량(tokens per second)이 급감하는 경우. 문제가 발생할 때 vmstat 1을 실행하십시오. si 및 so 열의 값이 0이 아니라면 시스템이 스왑(swapping) 중임을 의미하며, 이는 매 토큰마다 디스크에서 가중치를 읽어오고 있다는 뜻입니다. 설정 변경으로는 해결할 수 없습니다. 모델 크기를 줄이거나 슬롯 수를 줄이십시오.

10명 중 1명의 사용자만 유독 오래 대기하는 경우. vLLM 로그에서 preempted을 검색하십시오. 일반적으로 선점(preemption)과 그에 따른 재계산이 원인이며, 이는 허용된 컨텍스트 길이에 비해 캐시가 과도하게 할당되었음을 의미합니다.

서버가 유휴 상태임에도 TTFT(Time To First Token)가 느린 경우. 이는 동시성 문제가 아니라 프리필(prefill) 문제입니다. 긴 프롬프트는 첫 토큰이 생성되기까지 상당한 시간이 소요되므로, 하드웨어를 점검하기 전에 프롬프트 크기와 프리픽스 캐싱(prefix caching)을 먼저 확인하십시오. 만약 긴 대기 시간이 한동안 조용하다가 첫 번째로 요청을 보낸 사용자에게만 발생하고 이후 사용자들은 정상이라면, 이는 프리필 문제가 아닙니다. Ollama가 모델을 메모리에서 해제했다가 다시 디스크에서 가중치를 읽어오는 상황이므로, 요청 간 모델 상주 유지 설정을 통해 이 가능성을 배제하는 것이 좋습니다.

FAQ

두 번째 사용자가 접속하면 왜 셀프 호스팅 LLM이 느려지나요?

대부분의 경우 느려지는 것이 아니라 대기열에 들어가는 것입니다. Ollama는 기본적으로 OLLAMA_NUM_PARALLEL가 1로 설정되어 있어, 두 번째 요청은 첫 번째 요청이 마지막 토큰을 생성할 때까지 기다립니다. 다른 사용자가 대기하는 동안 한 사용자의 스트림 시간을 측정하여 두 경우를 구분하십시오. 만약 스트림이 시작된 후 토큰 생성 속도가 정상이라면 대기열 문제이므로 병렬 처리 수를 늘려 해결할 수 있습니다. 두 스트림 모두 속도가 절반으로 떨어진다면 메모리 대역폭을 실제로 공유하고 있는 것이며, 이는 하드웨어적 한계입니다.

작은 GPU 하나로 몇 명의 동시 사용자를 처리할 수 있나요?

사용자가 아닌 메모리 용량을 기준으로 계산하십시오. 먼저 모델 가중치를 고려하고, 그 다음 KV 캐시를 계산합니다. KV 캐시는 2 곱하기 레이어 수 곱하기 키/값 헤드 수 곱하기 헤드 차원 곱하기 바이트 수로 계산되며, 토큰당 그리고 활성 대화당 소모됩니다. 36개 레이어, 8개 키/값 헤드, 128 헤드 차원을 가진 일반적인 8B 모델은 16비트 기준 토큰당 약 144 KiB를 소모하므로, 8,192 토큰 대화에는 약 1.2 GB가 필요합니다. 해당 모델을 16비트로 올린 24 GB 카드에는 캐시용으로 약 6 GB가 남으며, 이는 전체 컨텍스트를 사용할 경우 약 5개의 대화를 처리할 수 있는 용량입니다. 컨텍스트 길이를 줄이면 더 많은 대화를 처리할 수 있습니다.

연속 배칭(continuous batching)을 사용하면 각 사용자의 응답이 느려지나요?

전체 배치가 끝날 때까지 기다릴 필요가 없으므로 중앙값 지연 시간은 보통 개선됩니다. 하지만 꼬리 지연 시간(tail latency)은 나빠집니다. 시퀀스가 추가될 때마다 모든 디코딩 단계에 작업량이 늘어나고, 새로 도착한 요청의 프리필(prefill)이 스트리밍 중인 사용자의 처리 시간을 일부 점유하며, 선점된 요청은 프리필을 두 번 수행해야 하기 때문입니다. 평균값은 지연을 숨기므로, 채팅창에서 체감되는 멈춤 현상을 정확히 파악하려면 평균이 아닌 p95 토큰 간 지연 시간을 측정하십시오.

OLLAMA_NUM_PARALLEL을 늘려야 할까요, 아니면 vLLM으로 옮겨야 할까요?

먼저 병렬 처리 수를 늘려보십시오. 비용이 들지 않고 설정 파일 하나만 추가하면 되며, 긴 답변 하나를 기다리는 4명의 사용자가 발생하는 일반적인 상황을 해결할 수 있습니다. 메모리가 한계점입니다. 병렬 요청은 유지해야 할 컨텍스트를 곱절로 늘리므로 레이어가 CPU로 밀려나지 않는지 확인하십시오. VRAM 여유가 있고 4개 이상의 요청이 실제로 동시에 처리되는 상황이라면 vLLM으로 전환하십시오. 그 지점부터는 페이징된 캐시와 토큰 단위 스케줄링이 비용보다 더 큰 이득을 제공합니다.

CPU 코어를 늘리면 느린 LLM 서버가 빨라지나요?

사용자가 가장 크게 체감하는 부분에는 도움이 되지 않습니다. 디코딩은 토큰마다 모델 전체를 메모리에서 읽어오므로 RAM 대역폭에 제한을 받으며, 대역폭이 포화 상태가 되면 코어를 늘려도 성능 향상이 없습니다. 프리필은 코어 수에 비례하여 성능이 향상되므로, 긴 프롬프트에서 첫 토큰이 생성되기까지의 시간을 단축할 수는 있습니다. 4~8 GB VPS 환경에서는 메모리 용량이 병목 현상의 주원인이므로, vCPU를 늘리기보다는 더 작은 모델을 사용하거나 컨텍스트 길이를 줄이는 것이 효과적인 해결책입니다.