transformers 로 평가하던 오픈 모델을 vLLM 이나 llama.cpp 로 옮기면 같은 프롬프트, 같은 temperature·top_p 인데도 답의 결이 달라지는 경우가 많아요. 결론부터 말하면 엔진마다 필터를 거는 순서와, 요청에 값을 안 넣었을 때 쓰는 기본값이 달라서예요. llama.cpp 는 temperature 를 맨 마지막에 적용하고, vLLM 은 min-p 를 top-p 보다 먼저 걸어요. 그래서 엔진을 바꿀 때는 숫자를 그대로 옮기지 말고 값을 전부 명시하고, 남는 후보 수로 확인해야 해요.
이 글은 샘플링이 어떻게 동작하는지보다, 실제로 서빙 엔진을 고르고 옮길 때 필요한 숫자를 다뤄요. 기본값과 순서는 2026년 11월 6일에 각 프로젝트의 main 브랜치 소스를 직접 열어 확인했어요. 버전이 바뀌면 달라질 수 있으니 쓰는 버전의 소스로 한 번 더 보세요.
엔진별 샘플링 기본값 비교표
요청에 아무 값도 넣지 않았을 때 쓰이는 값부터 확인해요. 여기가 다르면 같은 클라이언트 코드라도 엔진만 바꿔서 출력이 달라져요.
| 엔진 | temperature | top_p | top_k (끄는 값) | min_p | 0 을 넣으면 |
|---|---|---|---|---|---|
transformers GenerationConfig | 1.0 | 1.0 | 50 (0) | 없음(끔) | do_sample=False 가 그리디 |
vLLM SamplingParams | 1.0 | 1.0 | 0 (0 또는 -1) | 0.0 | temperature 0 이면 그리디, top_p·top_k·min_p 초기화 |
SGLang SamplingParams | 1.0 | 1.0 | 전체 어휘 (-1) | 0.0 | temperature 0 이면 top_k=1 로 바꿔 그리디 |
| llama.cpp (CLI·서버) | 0.8 | 0.95 | 40 (0) | 0.05 | temperature 0 이하면 그리디 |
출처는 transformers 생성 설정 문서, vLLM SamplingParams 소스, SGLang SamplingParams 소스, llama.cpp common.h 예요.
표에서 볼 점은 두 가지예요.
- llama.cpp 는 기본값부터 샘플링을 걸어요. 다른 엔진은 기본이 "자르지 않음"인데, llama.cpp 는 아무것도 안 넣어도 top-k 40·top-p 0.95·min-p 0.05 세 개가 동시에 걸리고 temperature 도 0.8 이에요.
- '끔'을 뜻하는 값이 달라요. vLLM 은 top_k 0 을 권장하고 -1 도 받아요. SGLang 은 -1 이 전체 어휘예요. transformers 는 0 이 끔이고, 아무 값도 안 주면 50 이 걸려요. 클라이언트에서 top_k 를 -1 로 고정해 두면 엔진에 따라 해석이 다를 수 있어요.
모델 generation_config.json 이 기본값을 덮어요
엔진 기본값 위에 한 겹이 더 있어요. 모델 폴더의 generation_config.json 이에요. vLLM 모델 설정 소스에서 generation_config 옵션의 기본값은 "auto" 이고, 이때 모델 경로의 설정을 읽어 기본 샘플링 값으로 써요. "vllm" 으로 주면 이 파일을 읽지 않고 엔진 기본값만 쓰고, override_generation_config 로 일부만 덮을 수도 있어요.
주요 공개 모델의 파일을 열어 보면 값이 제각각이에요(2026-11-06 확인).
| 모델 | do_sample | temperature | top_p | top_k |
|---|---|---|---|---|
| Qwen3-8B | true | 0.6 | 0.95 | 20 |
| DeepSeek-R1 | true | 0.6 | 0.95 | 없음 |
| Mistral-Small-3.2-24B-Instruct-2506 | true | 0.15 | 없음 | 없음 |
| gpt-oss-20b | true | 없음 | 없음 | 없음 |
같은 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:
_get_logits_processor의 샘플링 블록이TemperatureLogitsWarper→TopKLogitsWarper→TopPLogitsWarper→MinPLogitsWarper순으로 붙어요(generation/utils.py). - vLLM v1:
Sampler문서 주석이 패널티 → temperature → argmax 를 바꾸지 않는 로짓 프로세서(기본 min-p) → top-k·top-p 순서를 적어 두었어요(v1 sampler.py). - llama.cpp: 기본
samplers목록이 penalties → dry → top_n_sigma → top_k → typ_p → top_p → min_p → xtc → temperature 예요. 서버 README 도--samplers의 기본값을 같은 순서로 적어요(llama.cpp 서버 README).
순서 차이가 결과를 바꾸는 경우는 두 갈래예요.
- temperature 가 자르기보다 앞이냐 뒤냐. transformers·vLLM 은 temperature 를 먼저 걸고 그 분포로 top-p 누적합과 min-p 문턱을 계산해요. 그래서 temperature 를 올리면 같은 top_p 로도 후보가 늘어요. llama.cpp 는 원래 분포(temperature 1)로 먼저 자르고, 살아남은 후보 안에서만 temperature 를 적용해요. 후보 수는 temperature 와 무관하고, temperature 는 남은 후보 사이의 확률 차이만 바꿔요.
- min-p 와 top-p 를 같이 켰을 때 어느 쪽이 먼저냐. 먼저 자른 쪽이 남은 확률을 재정규화하므로, 뒤에 오는 필터가 보는 분포가 달라져요. vLLM 은 min-p 를 먼저, transformers·llama.cpp 는 top-p 를 먼저 걸어요.
같은 설정, 엔진별로 남는 후보 수 계산
말로만 보면 차이가 작아 보여서 직접 계산했어요. "오늘 점심은" 다음 토큰 후보 12개에 아래 로짓을 줬어요. 실제 모델 출력이 아니라 계산용 예시예요.
| 후보 | 김치찌개 | 된장찌개 | 비빔밥 | 라면 | 짜장면 | 돈까스 | 국수 | 샐러드 | 피자 | 햄버거 | 회의 | 자동차 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 로짓 | 4.0 | 3.6 | 3.2 | 3.0 | 2.6 | 2.3 | 2.0 | 1.6 | 1.2 | 0.9 | -0.5 | -1.5 |
| 확률(T=1) | 0.306 | 0.205 | 0.138 | 0.113 | 0.076 | 0.056 | 0.041 | 0.028 | 0.019 | 0.014 | 0.003 | 0.001 |
필터는 세 엔진의 순서를 그대로 흉내 낸 numpy 코드로 돌렸어요. top-p 는 누적 확률이 p 이상이 되는 최소 집합, min-p 는 1등 확률 × min_p 이상, top-k 는 상위 k 개를 남기는 정의 그대로예요.
몇 개를 손으로 따라가 보면 이유가 보여요.
- Qwen3 권장값(T 0.6·top_k 20·top_p 0.95): transformers·vLLM 은 T=0.6 으로 뾰족해진 분포에서 누적합이 0.968 이 되는 돈까스까지 6개를 남겨요. llama.cpp 는 원래 분포에서 0.963 이 되는 샐러드까지 8개를 남긴 뒤 T=0.6 을 걸어요. 모델 제작사가 transformers 기준으로 정한 값이라면, llama.cpp 에서는 후보가 더 넓게 열려요.
- T 1.5·min_p 0.1: transformers·vLLM 은 T=1.5 로 평평해진 분포에서 1등 0.227 의 10%(0.0227)를 문턱으로 써서 햄버거(0.029)까지 10개가 남아요. llama.cpp 는 원래 분포의 1등 0.306 의 10%(0.0306)로 잘라 국수까지 7개만 남아요. llama.cpp 에서 temperature 를 올려도 후보는 늘지 않는다는 게 숫자로 보여요.
- T 1.0·top_p 0.8·min_p 0.2: 순서만 다른 경우예요. transformers 는 top-p 로 짜장면까지 5개(합 0.838)를 남기고, 그 안에서 min-p 문턱(재정규화된 1등 0.366 × 0.2 = 0.073)을 짜장면(0.091)이 넘어 5개 그대로예요. vLLM 은 min-p 를 먼저 걸어 원래 분포의 문턱 0.061 에서 돈까스(0.056)를 자르고, 남은 5개를 재정규화한 분포로 top-p 0.8 을 계산해 라면까지 4개가 남아요.
후보 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번 평균 내고, 이를 세 번 돌린 가운데 값을 적었어요.
이 숫자를 GPU 서빙에 그대로 옮기면 안 돼요. GPU 에서는 이 연산이 훨씬 빠르고, 엔진도 정렬을 피하는 커널을 써요. vLLM 은 CUDA 에서 하드웨어가 지원하면 FlashInfer 샘플러로 top-k·top-p 를 처리하고, 이 동작은 VLLM_USE_FLASHINFER_SAMPLER 환경 변수로 끌 수 있어요(기본값 켬, vLLM top-k·top-p 샘플러 소스).
실무에서 볼 점은 이래요.
- CPU 로 돌리는 llama.cpp·소형 서버에서는 필터 비용이 토큰당 시간에 직접 더해져요. llama.cpp 기본 체인은 top-k 40 이 top-p 보다 먼저 걸려 정렬할 후보가 40개로 줄어 있어요. top_k 를 0 으로 끄고 top_p 만 쓰면 정렬 대상이 어휘 전체가 돼요.
- GPU 서빙에서는 필터 비용보다 배치 크기·KV 캐시가 처리량을 좌우해요. 동시 요청 수 계산은 vLLM KV 캐시 용량 계산을 참고하세요.
상황별 엔진·설정 추천
| 상황 | 추천 | 이유 |
|---|---|---|
| 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 양자화 형식 고르기에 정리했어요.
서빙 엔진 바꿀 때 샘플링 점검표
- 지금 실제로 걸린 값을 적어요. transformers 는
model.generation_config, vLLM 은 서버 시작 로그와--generation-config설정, llama.cpp 는--samplers와 네 값을 기록해요. 두 설정을 JSON 으로 뽑아 JSON 비교 도구로 나란히 보면 빠진 값이 바로 보여요. - 새 엔진 요청에 네 값을 전부 넣어요. temperature·top_p·top_k·min_p 를 모두 명시하고, "끔"은 엔진이 권장하는 값(vLLM top_k 0, SGLang -1, llama.cpp 0)으로 넣어요.
- 자르기 필터는 하나만 켜요. top-p 와 min-p 중 하나만 쓰면 순서 차이가 사라져요. top-k 를 같이 쓴다면 큰 값(예: 40 이상)으로 두어 안전장치 역할만 하게 해요.
- temperature 위치를 확인해요. llama.cpp 에서 transformers 와 같은 동작을 원하면
--samplers순서에서 temperature 를 자르기 단계 앞으로 옮기고, 그 뒤에 출력을 비교해요. - 출력을 숫자로 비교해요. 같은 프롬프트 수십 개를 두 엔진에서 여러 번씩 생성해 평균 응답 길이, 길이 한도에 걸린 비율, 형식 실패율, 서로 다른 답의 개수를 봐요. 감으로 "비슷하다"고 넘기지 않아요.
- logprobs 로 후보를 직접 봐요. 두 엔진 모두 상위 토큰 확률을 돌려주면 한 스텝에서 몇 개가 남는지 확인할 수 있어요. vLLM 은
logprobs를 처리 전·후 어느 값으로 줄지 옵션이 있으니 어떤 모드인지 같이 적어요. - 엔진 버전을 고정해요. 기본값과 순서는 소스에서 바뀔 수 있어요. 버전을 올릴 때마다 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배 느렸어요.
정리
- 적용 순서가 엔진마다 달라요. transformers·vLLM 은 temperature 를 먼저, llama.cpp 는 맨 나중에 걸고, min-p 와 top-p 의 앞뒤는 vLLM 만 반대예요.
- 기본값도 달라요. llama.cpp 는 아무것도 안 넣어도 temperature 0.8·top_k 40·top_p 0.95·min_p 0.05 이고, vLLM 은 모델의 generation_config.json(Mistral Small 3.2 는 temperature 0.15)을 먼저 읽어요.
- 12개 후보 예시에서 여섯 설정 모두 엔진에 따라 후보 수가 달라졌어요. 엔진을 바꿀 때는 값을 전부 명시하고, 자르기 필터는 하나만 켜고, 출력 지표로 비교하세요.
- 필터 비용은 top-p 정렬이 가장 커요(CPU 측정에서 top-k 의 약 9배). GPU 서빙은 전용 커널이 처리하니, CPU 배포에서만 비용을 따져 보면 돼요.
더 깊이 읽기
이 글은 실무에서 판단할 때 필요한 것 위주로 정리했어요. 같은 주제의 내부 동작 원리는 devboi 기술 블로그의 LLM 샘플링 원리 — temperature·top-p·top-k에서 도식과 코드로 단계별로 풀었어요.