광고
~ / posts / developer
💻 개발자

LLM 서빙 엔진별 샘플링 차이, vLLM·llama.cpp 기본값과 적용 순서

transformers 로 평가하던 오픈 모델을 vLLM 이나 llama.cpp 로 옮기면 같은 프롬프트, 같은 temperature·top_p 인데도 답의 결이 달라지는 경우가 많아요. 결론부터 말하면 엔진마다 필터를 거는 순서와, 요청에 값을 안 넣었을 때 쓰는 기본값이 달라서예요. llama.cpp 는 temperature 를 맨 마지막에 적용하고, vLLM 은 min-p 를 top-p 보다 먼저 걸어요. 그래서 엔진을 바꿀 때는 숫자를 그대로 옮기지 말고 값을 전부 명시하고, 남는 후보 수로 확인해야 해요.

이 글은 샘플링이 어떻게 동작하는지보다, 실제로 서빙 엔진을 고르고 옮길 때 필요한 숫자를 다뤄요. 기본값과 순서는 2026년 11월 6일에 각 프로젝트의 main 브랜치 소스를 직접 열어 확인했어요. 버전이 바뀌면 달라질 수 있으니 쓰는 버전의 소스로 한 번 더 보세요.

광고

엔진별 샘플링 기본값 비교표

요청에 아무 값도 넣지 않았을 때 쓰이는 값부터 확인해요. 여기가 다르면 같은 클라이언트 코드라도 엔진만 바꿔서 출력이 달라져요.

엔진temperaturetop_ptop_k (끄는 값)min_p0 을 넣으면
transformers GenerationConfig1.01.050 (0)없음(끔)do_sample=False 가 그리디
vLLM SamplingParams1.01.00 (0 또는 -1)0.0temperature 0 이면 그리디, top_p·top_k·min_p 초기화
SGLang SamplingParams1.01.0전체 어휘 (-1)0.0temperature 0 이면 top_k=1 로 바꿔 그리디
llama.cpp (CLI·서버)0.80.9540 (0)0.05temperature 0 이하면 그리디

출처는 transformers 생성 설정 문서, vLLM SamplingParams 소스, SGLang SamplingParams 소스, llama.cpp common.h 예요.

표에서 볼 점은 두 가지예요.

모델 generation_config.json 이 기본값을 덮어요

엔진 기본값 위에 한 겹이 더 있어요. 모델 폴더의 generation_config.json 이에요. vLLM 모델 설정 소스에서 generation_config 옵션의 기본값은 "auto" 이고, 이때 모델 경로의 설정을 읽어 기본 샘플링 값으로 써요. "vllm" 으로 주면 이 파일을 읽지 않고 엔진 기본값만 쓰고, override_generation_config 로 일부만 덮을 수도 있어요.

주요 공개 모델의 파일을 열어 보면 값이 제각각이에요(2026-11-06 확인).

모델do_sampletemperaturetop_ptop_k
Qwen3-8Btrue0.60.9520
DeepSeek-R1true0.60.95없음
Mistral-Small-3.2-24B-Instruct-2506true0.15없음없음
gpt-oss-20btrue없음없음없음

같은 Mistral Small 3.2 를 띄워도 vLLM(auto)에서는 temperature 0.15 로, llama.cpp 에서는 값을 안 넣으면 0.8 로 돌아요. 다섯 배가 넘는 차이예요. 반대로 gpt-oss-20b 처럼 파일에 샘플링 값이 없으면 vLLM 도 엔진 기본값 1.0 을 써요.

API 로 쓰는 상용 모델은 이 손잡이 자체를 막아 둔 경우가 많아요. 어느 API 가 temperature 를 받는지, 다수결로 여러 번 뽑을 때 비용이 얼마인지는 LLM temperature 설정 고르기에 따로 정리했어요. 이 글은 직접 서빙하는 경우만 다뤄요.

엔진별 샘플링 적용 순서

기본값을 모두 명시해도 차이가 남는 이유가 순서예요. 각 소스에서 확인한 순서는 이래요.

transformers·vLLM·llama.cpp 세 엔진의 샘플링 단계를 가로 띠로 나란히 놓고, temperature 와 min-p 의 위치가 엔진마다 다른 것을 색으로 표시한 그림
같은 네 단계라도 temperature(주황)와 min-p(청록) 자리가 달라요

순서 차이가 결과를 바꾸는 경우는 두 갈래예요.

  1. temperature 가 자르기보다 앞이냐 뒤냐. transformers·vLLM 은 temperature 를 먼저 걸고 그 분포로 top-p 누적합과 min-p 문턱을 계산해요. 그래서 temperature 를 올리면 같은 top_p 로도 후보가 늘어요. llama.cpp 는 원래 분포(temperature 1)로 먼저 자르고, 살아남은 후보 안에서만 temperature 를 적용해요. 후보 수는 temperature 와 무관하고, temperature 는 남은 후보 사이의 확률 차이만 바꿔요.
  2. min-p 와 top-p 를 같이 켰을 때 어느 쪽이 먼저냐. 먼저 자른 쪽이 남은 확률을 재정규화하므로, 뒤에 오는 필터가 보는 분포가 달라져요. vLLM 은 min-p 를 먼저, transformers·llama.cpp 는 top-p 를 먼저 걸어요.

같은 설정, 엔진별로 남는 후보 수 계산

말로만 보면 차이가 작아 보여서 직접 계산했어요. "오늘 점심은" 다음 토큰 후보 12개에 아래 로짓을 줬어요. 실제 모델 출력이 아니라 계산용 예시예요.

후보김치찌개된장찌개비빔밥라면짜장면돈까스국수샐러드피자햄버거회의자동차
로짓4.03.63.23.02.62.32.01.61.20.9-0.5-1.5
확률(T=1)0.3060.2050.1380.1130.0760.0560.0410.0280.0190.0140.0030.001

필터는 세 엔진의 순서를 그대로 흉내 낸 numpy 코드로 돌렸어요. top-p 는 누적 확률이 p 이상이 되는 최소 집합, min-p 는 1등 확률 × min_p 이상, top-k 는 상위 k 개를 남기는 정의 그대로예요.

같은 설정에서 엔진별로 남는 후보 수 (12개 후보 예시)
transformersvLLMllama.cpp
02.557.510T0.6 k20 p0.95 · transformers 6개T0.6 k20 p0.95 · vLLM 6개T0.6 k20 p0.95 · llama.cpp 8개T0.6 k20 p0.95T1.5 p0.9 · transformers 8개T1.5 p0.9 · vLLM 8개T1.5 p0.9 · llama.cpp 7개T1.5 p0.9T1.5 minp0.1 · transformers 10개T1.5 minp0.1 · vLLM 10개T1.5 minp0.1 · llama.cpp 7개T1.5 minp0.1T1.0 p0.8 minp0.2 · transformers 5개T1.0 p0.8 minp0.2 · vLLM 4개T1.0 p0.8 minp0.2 · llama.cpp 5개T1.0 p0.8 minp0.2T0.8 k40 p0.95 minp0.05 · transformers 7개T0.8 k40 p0.95 minp0.05 · vLLM 6개T0.8 k40 p0.95 minp0.05 · llama.cpp 8개T0.8 k40 p0.95 minp0.05T0.6 p0.8 minp0.15 · transformers 3개T0.6 p0.8 minp0.15 · vLLM 3개T0.6 p0.8 minp0.15 · llama.cpp 5개T0.6 p0.8 minp0.15

첫 설정은 Qwen3-8B 권장값, 다섯째는 llama.cpp 기본값이에요. 여섯 설정 모두에서 적어도 한 엔진의 후보 수가 달랐어요.

단위: 개 · 자료: 위 로짓 12개에 각 엔진 소스의 적용 순서를 numpy 로 재현해 직접 계산 (2026-11-06)
표로 보기
구분transformersvLLMllama.cpp
T0.6 k20 p0.95668
T1.5 p0.9887
T1.5 minp0.110107
T1.0 p0.8 minp0.2545
T0.8 k40 p0.95 minp0.05768
T0.6 p0.8 minp0.15335

몇 개를 손으로 따라가 보면 이유가 보여요.

후보 1개 차이가 작아 보여도 생성은 수백 스텝 이어져요. 스텝마다 꼬리 후보가 하나씩 더 열리거나 닫히면 긴 답에서는 문체·길이·탈선 빈도가 눈에 띄게 달라질 수 있어요. 그래서 엔진을 옮길 때는 숫자를 똑같이 맞췄다고 끝내지 말고, 아래 점검표처럼 실제 출력을 비교해야 해요.

샘플링 필터 연산 지연 — 어휘 크기별 측정

샘플링 파라미터는 모델 순전파 연산량을 바꾸지 않지만, 필터 자체에도 비용이 있어요. 특히 top-p 는 누적합을 내려고 어휘 전체를 정렬해야 해요. 어휘 크기는 모델 config.json 기준으로 Qwen3 151,936개, DeepSeek-R1 129,280개, gpt-oss-20b 201,088개, Mistral Small 3.2 131,072개예요.

Apple M1 CPU 에서 numpy 2.4.0 으로 한 스텝(배치 1) 필터 시간을 200번 평균 내고, 이를 세 번 돌린 가운데 값을 적었어요.

어휘 크기별 샘플링 필터 1스텝 시간 (M1 CPU·numpy·배치 1)
top-p(전체 정렬)top-k(부분 정렬)min-pargmax(그리디)
0102030403만2768개 · top-p(전체 정렬) 5.5ms3만2768개 · top-k(부분 정렬) 0.47ms3만2768개 · min-p 0.57ms3만2768개 · argmax(그리디) 0.05ms3만2768개12만9280개 · top-p(전체 정렬) 23.6ms12만9280개 · top-k(부분 정렬) 2.8ms12만9280개 · min-p 2.1ms12만9280개 · argmax(그리디) 0.24ms12만9280개15만1936개 · top-p(전체 정렬) 28.2ms15만1936개 · top-k(부분 정렬) 3.1ms15만1936개 · min-p 2.2ms15만1936개 · argmax(그리디) 0.28ms15만1936개20만1088개 · top-p(전체 정렬) 38.4ms20만1088개 · top-k(부분 정렬) 2.3ms20만1088개 · min-p 3.1ms20만1088개 · argmax(그리디) 0.32ms20만1088개

15만 개에서 top-p 는 top-k 의 약 9배, argmax 의 약 100배였어요. 어휘가 커질수록 정렬 비용이 가장 빨리 늘어요.

단위: ms · 자료: 정규분포 무작위 로짓으로 직접 측정 (2026-11-06). GPU 서빙 시간과는 다르고, 방식 사이 상대 비교용이에요.
표로 보기
구분top-p(전체 정렬)top-k(부분 정렬)min-pargmax(그리디)
3만2768개5.50.470.570.05
12만9280개23.62.82.10.24
15만1936개28.23.12.20.28
20만1088개38.42.33.10.32

이 숫자를 GPU 서빙에 그대로 옮기면 안 돼요. GPU 에서는 이 연산이 훨씬 빠르고, 엔진도 정렬을 피하는 커널을 써요. vLLM 은 CUDA 에서 하드웨어가 지원하면 FlashInfer 샘플러로 top-k·top-p 를 처리하고, 이 동작은 VLLM_USE_FLASHINFER_SAMPLER 환경 변수로 끌 수 있어요(기본값 켬, vLLM top-k·top-p 샘플러 소스).

실무에서 볼 점은 이래요.

광고
광고

상황별 엔진·설정 추천

상황추천이유
transformers 로 평가 → vLLM 으로 서빙같은 값을 명시, min-p 와 top-p 중 하나만둘 다 temperature 먼저라 비슷하지만, min-p·top-p 를 같이 켜면 순서가 달라요
transformers 로 평가 → llama.cpp 로 배포--samplers 로 temperature 를 앞에 두거나, 출력 비교 후 값 재조정기본 체인은 temperature 가 마지막이라 같은 값이라도 후보 수가 달라요
llama.cpp 를 기본값으로 바로 띄움temperature·top_k·top_p·min_p 를 모델 권장값으로 모두 명시기본값이 세 필터를 동시에 걸어 모델 제작사 설정과 달라요
모델 제작사 권장값이 있음vLLM generation_config auto 유지, 다른 엔진은 그 값을 복사파일 값이 곧 제작사가 검증한 설정이에요
엔진 기본값으로만 돌리고 싶음vLLM --generation-config vllm모델 파일 값이 몰래 걸리지 않아요
평가·회귀 테스트 재현이 필요그리디 + 응답 저장, 필요하면 vLLM 배치 불변 모드샘플링을 켜면 엔진마다 결과가 달라져 비교가 안 돼요

재현성 쪽은 vLLM 배치 불변성 문서에 따르면 VLLM_BATCH_INVARIANT=1 로 켜고, 연산 능력 8.0 이상 NVIDIA GPU·일부 AMD Instinct GPU·Triton 을 지원하는 Intel XPU 에서 돌아가요. 2026년 11월 기준 문서에 베타로 표시돼 있어요.

양자화 모델(GGUF 등)로 llama.cpp 를 쓸 계획이라면 형식별 메모리와 속도 차이는 LLM 양자화 형식 고르기에 정리했어요.

서빙 엔진 바꿀 때 샘플링 점검표

엔진 전환 전·전환 중·전환 후 세 칸에 나눠, 기본값 기록, 값 전부 명시, 순서 확인, 출력 비교, 운영 지표 감시 항목을 체크 상자로 늘어놓은 점검표
왼쪽 칸부터 순서대로 확인해요
  1. 지금 실제로 걸린 값을 적어요. transformers 는 model.generation_config, vLLM 은 서버 시작 로그와 --generation-config 설정, llama.cpp 는 --samplers 와 네 값을 기록해요. 두 설정을 JSON 으로 뽑아 JSON 비교 도구로 나란히 보면 빠진 값이 바로 보여요.
  2. 새 엔진 요청에 네 값을 전부 넣어요. temperature·top_p·top_k·min_p 를 모두 명시하고, "끔"은 엔진이 권장하는 값(vLLM top_k 0, SGLang -1, llama.cpp 0)으로 넣어요.
  3. 자르기 필터는 하나만 켜요. top-p 와 min-p 중 하나만 쓰면 순서 차이가 사라져요. top-k 를 같이 쓴다면 큰 값(예: 40 이상)으로 두어 안전장치 역할만 하게 해요.
  4. temperature 위치를 확인해요. llama.cpp 에서 transformers 와 같은 동작을 원하면 --samplers 순서에서 temperature 를 자르기 단계 앞으로 옮기고, 그 뒤에 출력을 비교해요.
  5. 출력을 숫자로 비교해요. 같은 프롬프트 수십 개를 두 엔진에서 여러 번씩 생성해 평균 응답 길이, 길이 한도에 걸린 비율, 형식 실패율, 서로 다른 답의 개수를 봐요. 감으로 "비슷하다"고 넘기지 않아요.
  6. logprobs 로 후보를 직접 봐요. 두 엔진 모두 상위 토큰 확률을 돌려주면 한 스텝에서 몇 개가 남는지 확인할 수 있어요. vLLM 은 logprobs 를 처리 전·후 어느 값으로 줄지 옵션이 있으니 어떤 모드인지 같이 적어요.
  7. 엔진 버전을 고정해요. 기본값과 순서는 소스에서 바뀔 수 있어요. 버전을 올릴 때마다 1번 기록을 다시 떠서 비교해요.
광고
광고

자주 묻는 질문

vLLM 에서 temperature 를 안 넣었는데 1.0 이 아닌 것 같아요.

--generation-config 의 기본값이 auto 라서 모델 폴더의 generation_config.json 값이 기본 샘플링 값으로 걸려요. Qwen3-8B 라면 temperature 0.6·top_p 0.95·top_k 20 이 걸려요. 엔진 기본값을 쓰려면 --generation-config vllm 으로 띄우거나 요청에 값을 직접 넣으세요.

llama.cpp 에서 temperature 를 올려도 답이 별로 다양해지지 않아요.

기본 sampler 순서에서 temperature 가 맨 마지막이라, top_k 40·top_p 0.95·min_p 0.05 가 원래 분포로 먼저 후보를 정해요. temperature 는 남은 후보 사이 확률만 고르게 만들 뿐 후보를 더하지 않아요. 더 열고 싶다면 min_p·top_p 를 완화하거나 --samplers 에서 temperature 를 앞으로 옮겨요.

min_p 와 top_p 를 같이 쓰면 안 되나요?

쓸 수는 있지만 엔진마다 적용 순서가 달라 결과가 달라져요. 위 예시에서 top_p 0.8·min_p 0.2 는 transformers 에서 5개, vLLM 에서 4개가 남았어요. 여러 엔진을 오갈 계획이면 하나만 켜는 쪽이 비교와 재현이 쉬워요.

temperature 0 이면 엔진을 바꿔도 같은 답이 나오나요?

세 엔진 모두 temperature 0 을 그리디로 처리하지만, 모델 연산 커널과 배치 구성이 달라 로짓이 미세하게 달라지고 1·2등이 가까운 자리에서 답이 갈릴 수 있어요. 같은 엔진 안에서도 배치 크기에 따라 달라질 수 있어 vLLM 은 별도의 배치 불변 모드를 두고 있어요. 회귀 테스트는 응답을 저장해 비교하는 편이 확실해요.

필터 연산 때문에 top-p 대신 top-k 를 써야 하나요?

GPU 서빙에서는 엔진이 정렬을 피하는 커널을 쓰므로 품질 기준으로 고르면 돼요. CPU 에서 큰 어휘 모델을 돌린다면 top-k 를 큰 값으로 먼저 걸어 정렬 대상을 줄이는 llama.cpp 기본 구성이 비용 면에서 유리해요. 측정에서 15만 개 어휘의 전체 정렬은 부분 정렬보다 약 9배 느렸어요.

정리

더 깊이 읽기

이 글은 실무에서 판단할 때 필요한 것 위주로 정리했어요. 같은 주제의 내부 동작 원리는 devboi 기술 블로그의 LLM 샘플링 원리 — temperature·top-p·top-k에서 도식과 코드로 단계별로 풀었어요.

참고한 자료

#vLLM 샘플링 순서#llama.cpp samplers 순서#샘플링 기본값 비교#generation_config.json#min_p top_p 같이 쓰기#SGLang top_k -1#서빙 엔진 이전 점검#FlashInfer 샘플러
← 이전 글위상 정렬 Kahn vs DFS 고르는 법, 순환 의존 탐지 비용과 점검표다음 글 →스프링 배치 retry·skip 고르는 법, 예외 분류표와 재시도 비용
광고

더 둘러보기

💻 개발자 다른 도구

🔥 많이 찾는 도구

🔮 재미로 한 번 더

📝 읽어 볼 만한 글

글 전체 보기 →