
temperature 0에서도 LLM은 매번 다른 답을 내놓는다: 5개 모델 100회 실험 결과
요약
Temperature를 0으로 설정하고 seed를 고정하더라도 LLM의 출력이 매번 달라질 수 있음을 실험을 통해 증명합니다. 5개 모델을 대상으로 100회씩 테스트한 결과, 로컬 모델과 API 모델 모두에서 출력 불일치 현상이 관찰되었습니다.
핵심 포인트
- Temperature 0과 seed 고정 설정만으로는 완전한 결정성을 보장할 수 없음
- 로컬 모델(Llama 70B)조차 약 9%의 확률로 다른 출력을 생성함
- API 기반 클라우드 모델은 절반 이상의 높은 확률로 출력이 흔들림
- 출력을 캐시 키나 테스트 기대값으로 사용할 때 주의가 필요함
먼저 결론부터 말씀드리겠습니다. temperature를 0으로 설정하고, seed를 고정하더라도 LLM은 동일한 프롬프트에 대해 항상 같은 답을 반환하지 않습니다. 저는 5개의 모델에 동일한 질문을 각각 100번씩 던져 이를 실측했습니다. 가장 안정적인 로컬 모델조차 100번 중 9번은 다른 출력이 나왔으며, API를 통한 경우에는 절반 이상이 흔들렸습니다.
"temperature 0이면 결정적이다". 저는 줄곧 그렇게 믿어왔습니다. 테스트에서 LLM을 사용할 때도 그것을 전제로 삼았습니다. 하지만 그 전제가 틀렸었습니다. 이 기사는 그 100회 × 5개 모델의 기록과, 왜 결과가 흔들리는지에 대한 이야기입니다.
미리 선을 그어두겠습니다. 로컬 LLM에 관한 기사는 이전에도 쓴 적이 있습니다. 양자화(Quantization)로 VRAM을 어떻게 절약하는지(FP16은 낭비라는 이야기)나, 모델 A와 모델 B 중 어느 쪽이 더 똑똑한지에 대한 대결 같은 내용 말입니다.
이 기사는 그 어느 쪽도 아닙니다. 다루는 것은 "동일한 모델에, 동일한 입력을, 동일한 설정으로 던졌을 때, 출력이 일치하는가"뿐입니다. 보는 것은 성능의 높고 낮음이 아닙니다. 동일한 입력에 동일한 출력을 반환하는가, 그 한 점을 숫자로 확인합니다.
추측은 섞지 않았습니다. 조건을 먼저 제시합니다.
- 로컬: Ollama 경유,
temperature 0,seed고정, 요청은 1건씩 순차적으로 진행 - API: 2종류의 클라우드 모델,
temperature 0(seed 지정 불가) - 프롬프트: "1부터 100까지의 짝수의 합을 반환하는 Python 함수를 작성해줘"로 고정
- 시도: 각 모델 100회, 출력 텍스트를 완전 일치 여부로 비교
- 집계: 최다 빈도 출력 횟수(일치율)와 유니크(Unique)한 출력 종류 수
"완전 일치"는 단 한 글자라도 다르면 별개의 것으로 간주합니다. 주석의 말투나 공백 한 줄의 차이도 별개의 출력입니다. 너무 엄격한 기준처럼 보일 수도 있습니다. 하지만 출력을 그대로 테스트의 기대값(Expected value)이나 캐시 키(Cache key)로 사용하는 상황에서는, 단 한 글자의 차이가 곧 불일치로 이어집니다. 실무에서 곤란함을 느끼는 지점이 바로 이 정도의 입도(Granularity)이기 때문에, 일부러 완전 일치로 계산했습니다.
참고로 로컬의 seed 고정은 "동일한 난수 시드(Seed)를 부여하면 재현될 것이다"라는 기대하에 설정했습니다. 본래 이 설정으로는 결정적이어야 합니다. 그럼에도 불구하고 흔들렸다는 점이 이번 검증에서 가장 전달하고 싶은 부분입니다.
| 모델 | 실행 환경 | 최다 출력 일치 | 유니크 출력 수 |
|---|---|---|---|
| Llama 계열 70B | Ollama (seed 고정) | 91/100 | 6 |
| ... | ... | ... | ... |
가장 안정적인 Llama 계열조차 100번 중 9번은 다른 출력이었습니다. Qwen 계열은 3번에 1번꼴로 어긋났고, API를 통한 2개 모델은 절반 이상이 흔들렸습니다. GPT 계열은 유니크 출력이 38종류, 즉 100번을 던졌을 때 38가지의 미묘하게 다른 함수가 반환되었다는 뜻입니다.
흔들림의 내용은 예를 들어 다음과 같은 차이였습니다.
# 출력 패턴 A (91회 중 다수파)
def sum_even(n: int) -> int:
return sum(i for i in range(2, n + 1, 2))
...
둘 다 올바르게 작동합니다. 하지만 "동일한 입력에 동일한 출력"을 기대하고 있다면, 이 두 가지는 별개의 것입니다. 출력을 그대로 문자열 비교(String comparison)하는 테스트라면, 패턴 B가 반환되는 순간 실패하게 됩니다. 로직은 맞는데 말이죠.
seed를 고정해도, 로컬에서도, 완전한 재현은 불가능했습니다. "temperature 0 = 결정적"은 설정으로서는 맞을지 몰라도, 결과로서는 성립하지 않습니다.
짧은 사실 질문("일본의 수도는" 등)도 시도해 보았으나, 이 경우는 거의 모든 모델에서 100/100 일치했습니다. 흔들리는 것은 어느 정도 길이의 텍스트를 생성할 때입니다. 출력이 길어질수록 중간에 다른 단어를 선택하는 분기(Branch)가 늘어납니다.
즉, "흔들릴지 여부"를 결정한 것은 모델의 똑똑함보다는 출력의 길이와 실행 환경이었습니다. 똑똑한 모델일수록 안정적이라는 단순한 이야기가 아니었다는 점이 의외였습니다. 오히려 API를 통한 고성능 모델이 로컬의 작은 모델보다 더 크게 흔들리고 있습니다. 이는 성능의 차이라기보다, 나중에 설명할 실행 환경의 차이입니다.
애초에 temperature란 무엇인가. LLM은 다음 단어의 후보 각각에 확률을 가지고 있습니다. temperature는 그 확률 분포의 "뾰족함(Sharpness)"을 조절하는 노브(Knob)입니다. 0으로 설정하면 매번 가장 확률이 높은 단어를 선택하는(탐욕법, Greedy search) 동작이 됩니다.
여기까지는 결정적으로 보입니다. 실제로 샘플링(Sampling)의 랜덤성은 사라집니다. 그래서 많은 사람(저를 포함하여)이 "temperature 0 = 결정적"이라고 이해하고 있었습니다. 틀린 말은 아닙니다. 주사위는 확실히 멈춰 있습니다.
하지만 흔들립니다. 그 이유는 샘플링(Sampling) 전 단계인, 확률을 계산하는 단계에 있었습니다. 주사위를 던지기 전, 각 면에 확률을 할당하는 계산 그 자체가 매번 미세하게 다른 숫자를 내놓고 있었던 것입니다. 확률 1위인 단어가 근소한 차이로 2위와 바뀐다면, 탐욕법 (Greedy Method)은 다른 단어를 선택합니다. 주사위를 멈추더라도, 눈금이 나오는 표 자체가 새로 쓰여지고 있었던 셈입니다.
2025년에 Thinking Machines Lab이 공개한 분석(Defeating Nondeterminism in LLM Inference)이 이 원인을 명확히 밝혀냈습니다. 범인은 GPU 상의 계산 순서입니다.
부동 소수점 (Floating Point)의 덧셈은 순서를 바꾸면 결과가 미세하게 변합니다. (a + b) + c와 a + (b + c)가 마지막 자릿수에서 일치하지 않을 수 있기 때문입니다. GPU는 대량의 숫자를 병렬로 더하기 때문에, 어떤 순서로 합쳐지는지가 그때그때 스레드 (Thread)의 움직임에 따라 달라집니다.
더 까다로운 것은 배치 불변성 (Batch Invariance)의 결여입니다. 서버는 여러 사용자의 요청을 묶어서 처리(배치, Batch)합니다. 이 배치 사이즈는 그 순간의 혼잡도에 따라 변합니다. 배치 사이즈가 변하면 내부의 계산 전략이 바뀌고, 덧셈 순서가 바뀌며, 확률이 아주 미세하게 변합니다.
그 미세한 차이로 인해 가장 확률이 높은 단어가 뒤바뀝니다. 한 번 뒤바뀌면 그 이후의 생성은 다른 길을 가게 됩니다. 문장의 앞부분에서 단어 하나가 어긋나면, 그 이후의 내용이 통째로 바뀌는 것입니다. 이것이 출력이 길어질수록 흔들림이 커진 이유입니다. 짧은 사실 질문이 안정적이었던 이유는 분기할 여지가 거의 없었기 때문입니다.
그리고 이것이 API를 경유할 때 더 크게 흔들린 이유이기도 합니다. 클라우드는 타인의 요청과 함께 묶여 배치 처리됩니다. 내 옆에 어떤 요청이 몇 건이나 들어올지는 스스로 제어할 수 없습니다. 혼잡한 시간대와 한가한 시간대에 동일한 프롬프트의 결과가 달라질 수 있다는 뜻입니다. 로컬 환경이 상대적으로 안정적이었던 이유는 배치에 타인이 섞이지 않는 만큼 계산 조건이 일정하게 유지되기 쉬웠기 때문입니다.
로컬에서 시드 (Seed)를 고정해도 흔들린 이유는, 요청을 하나씩 보내더라도 GPU 내부의 병렬 계산 순서까지는 고정할 수 없기 때문입니다. 흔들리는 것은 난수 (Random Number) 때문이 아닙니다. 원인은 하드웨어의 계산 방식에 있습니다.
Thinking Machines Lab은 이를 해결하는 방법도 제시했습니다. 배치 사이즈가 변해도 계산 순서가 바뀌지 않는 '배치 불변 커널 (Batch-invariant Kernel)'을 정규화 (Normalization), 행렬 곱 (Matrix Multiplication), 어텐션 (Attention)의 세 곳에 구현하여 오픈 소스 추론 엔진인 vLLM에 통합했습니다. 흔들림의 근원이었던 세 가지 연산을 배치 크기에 상관없이 동일한 순서로 계산하도록 다시 만든 것입니다.
그 결과, 1,000회의 실행이 1,000회 모두 완전 일치하게 되었다고 합니다. 대가는 속도였습니다. 동일한 처리가 약 62% 느려졌다고 보고되었습니다 (26초에서 42초로). 재현성과 속도는 여기서도 트레이드오프 (Trade-off) 관계였습니다. 완전한 재현이 필요한 감사나 평가에서는 42초를 선택하고, 속도가 중요한 실서비스에서는 26초를 선택하는 식으로 상황에 맞춰 선택할 수밖에 없습니다.
반대로 효과가 없는 대책도 명확해졌습니다. 바로 재시도 (Retry)입니다. 처음에는 '흔들린다면 몇 번 던져서 다수결을 따지면 된다'고 생각했습니다. 하지만 이는 흔들림을 평균화할 뿐, 결정성 (Determinism)을 얻지는 못합니다. 던질 때마다 다수결 결과가 바뀔 수 있기 때문입니다. 근본 원인이 계산 순서에 있는 이상, 단순히 횟수로 밀어붙여도 사라지지 않았습니다.
'출력이 조금 흔들리는 정도는 괜찮지 않을까'라고 생각할지도 모릅니다. 저도 그렇게 생각했습니다. 하지만 이는 세 가지 상황에서 실질적인 피해를 줍니다.
테스트: LLM의 출력을 기대값과 비교하는 테스트는 어쩌다 운 좋게 통과하고 있는 것일지도 모릅니다. CI (지속적 통합)에서 갑자기 실패했다가, 재실행하면 통과하는 현상. 그것이 불안정성의 한 원인입니다.
LLM-as-Judge: AI가 AI의 출력을 채점하는 평가에서는, 같은 답에 서로 다른 점수가 매겨집니다. 평가의 토대가 흔들립니다.
캐시 (Cache): 입력을 키 (Key)로 하여 출력을 캐싱하는 설계는 '같은 입력이라면 같은 출력'이라는 전제에 기반합니다. 그 전제가 무너지면 '캐시에 있는 답'과 '지금 생성한 답'이 어긋나게 되어, 디버깅하기 어려운 버그가 됩니다.
세 번째는 은근히 무서운 부분입니다. 예를 들어 동일한 질문에 대한 LLM의 답변을 캐시해 두었다가, 두 번째 요청부터는 캐시를 반환하는 설계는 흔히 사용됩니다. 하지만 캐시를 생성할 때와 캐시를 불러올 때의 답변이 다르다면, 사용자에게는 '똑같은 걸 물었는데 답이 다르네'라고 보이게 됩니다. 원인이 비결정성 (Nondeterminism)이라는 것을 눈치채지 못하면, 캐시 버그를 끝없이 의심하게 될 것입니다.
특히 테스트는 로컬 LLM뿐만 아니라 API를 사용하는 모든 엔지니어와 관련이 있습니다. "내 테스트는 어쩌다 운 좋게 통과하고 있는 것일지도 모른다". 이 관점을 가지느냐에 따라 불안정한 CI (Continuous Integration)를 대하는 태도가 달라집니다.
실제로 저도 이것 때문에 한 번 고생한 적이 있습니다. LLM의 출력을 기대값과 완전 일치(Exact Match)로 비교하는 테스트를 작성했는데, 로컬에서는 통과했지만 CI에서만 가끔 실패했습니다. 몇 번이고 재실행하면 통과했기에 "환경 문제겠지"라며 치부해 버렸습니다. 원인은 바로 이 비결정성 (Nondeterminism)이었습니다. 테스트 자체가 흔들리는 대상을 흔들리지 않는다는 전제로 검증하고 있었던 것입니다. 깨닫기까지 몇 시간을 허비했습니다.
제가 바꾼 것은 세 가지입니다.
- LLM의 출력을 "완전 일치"로 비교하는 테스트를 중단하고, 구조나 수치 범위로 검증한다
- 재현성이 필요할 때 (평가·감사)는 배치 불변 (Batch Invariance) 추론을 선택하거나, 느려짐을 감수한다
- "temperature 0 이니까 결정적이다"라는 전제를 설계에서 제외한다
첫 번째가 가장 효과적이었습니다. 테스트의 사고방식을 "같은 문자열이 반환되는가"에서 "충족해야 할 성질을 만족하는가"로 바꿉니다.
# 흔들림에 취약한 테스트 (완전 일치를 기대)
def test_sum_even_brittle():
code = llm_generate("짝수의 합을 반환하는 함수 sum_even 을 작성해줘")
...
검증하는 것은 출력 문자열이 아닙니다. 출력이 충족해야 할 동작 (Behavior)입니다. 이렇게 하면 패턴 A가 반환되든 패턴 B가 반환되든 테스트는 통과합니다. LLM의 출력을 테스트할 때는 표기보다 결과를 본다. 이것이 기본 자세가 되었습니다.
평가 파이프라인 (LLM-as-Judge)에서는 한 단계 더 주의를 기울입니다. 채점이 흔들린다면, 동일한 출력을 여러 번 채점하여 다수결을 따르거나 점수 차이가 오차 범위 내인지 확인합니다. 단 한 번의 채점을 절대시하지 않는 것입니다.
지식은 인생의 난이도를 낮춘다고 저는 믿습니다. "temp 0은 결정적이다"라는 고정관념을 하나 버리는 것만으로도, CI의 알 수 없는 불안정성에 휘둘리는 시간이 줄어듭니다. 제가 몇 시간을 허비했던 구덩이에 누군가가 빠지지 않을 수 있다면, 이 검증을 수행한 보람이 있습니다.
- temperature 0 이라도 seed 고정이라도, LLM은 동일한 입력에 동일한 출력을 반환하지 않았다
- 가장 안정적인 로컬 모델에서도 100회 중 9회 어긋났고, API 경유는 절반 이상이 흔들렸다
- 원인은 샘플링 (Sampling)이 아니라, GPU 상의 부동 소수점 (Floating Point) 덧셈 순서와 배치 불변성 (Batch Invariance)의 결여
- 해결 방법 (배치 불변 커널)은 있지만, 약 62% 느려진다
- 테스트·평가·캐시에 실질적인 해악을 끼친다. 완전 일치 전제의 설계를 재검토해야 한다
마지막으로 하나 더. 이 비결정성은 "버그"라기보다, 현재의 고속 추론 메커니즘이 만들어내는 부작용입니다. 속도를 취할 것인가 재현성을 취할 것인가의 문제는, 우리가 상황에 따라 선택해야 할 설계 판단이 되었습니다. 어느 쪽이 정답이라는 이야기가 아닙니다. 무엇을 우선할지 의식하며 선택하면 됩니다. 그것을 알고 있느냐가 휘둘릴 것인가를 결정하는 분수령입니다.
LLM은 똑똑하지만, 변덕스럽습니다. 그 변덕의 정체를 알고 있으면 휘둘리지 않고 함께할 수 있습니다. 즐겁게 해봅시다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기