2026년 11월 11일
같은 모델, 같은 입력, 다른 답이 나왔다
자동화와 인프라

같은 모델, 같은 입력, 다른 답이 나왔다

Caston 2026년 10월 16일 1 min read

같은 모델을 두 기기에서 돌렸더니 답이 달랐다. 처음엔 버전 차이나 캐시 문제겠거니 했다. 확인해 보니 모델 파일도 같고 프롬프트도 같았으며, 심지어 inference 코드도 동일한 버전이었다. 알고 나니 이건 버그가 아니라 구조 문제라는 생각이 들었다.

출력이 달라지는 지점

원인은 부동소수점 연산에 있다. 부동소수점은 실수를 이진수로 근사해서 저장하는 방식으로, 연산 순서가 조금만 달라도 결과가 미세하게 틀려질 수 있다. Apple Silicon의 GPU 계산 프레임워크인 Metal과 NVIDIA의 CUDA는 같은 행렬 연산을 내부적으로 다른 순서로 처리한다. 예를 들어 1024개의 값을 더할 때 Metal은 256개씩 네 묶음으로 나눠 합산하고, CUDA는 512개씩 두 묶음으로 나눠 합산할 수 있다. NVIDIA CUDA 부동소수점 문서에서도 병렬 환원 연산의 비결정성을 명시적으로 인정하며, 곱셈과 덧셈이 어떤 순서로 묶이느냐에 따라 마지막 몇 비트가 달라진다고 설명한다.

이 현상의 뿌리는 IEEE 754 부동소수점 표준의 한계에 있다. 표준은 개별 연산의 오차 한계를 정하지만 연산 순서까지는 강제하지 않는다. GPU는 수천 개의 스레드를 병렬로 돌리기 때문에 연산이 묶이는 순서가 실행마다, 하드웨어마다 달라지며, 특히 환원 합산처럼 순서에 민감한 연산에서 그 차이가 두드러진다. Metal과 CUDA는 내부 SIMD 폭도 다르고 퓨즈 연산 처리 방식도 다른데, SIMD는 하나의 명령으로 여러 데이터를 동시에 처리하는 구조를 말한다. 결국 두 플랫폼에서 동일한 float32 행렬 곱셈을 수행해도 수학적으로 동등하지만 수치적으로 동일하지 않은 결과가 나온다.

그 차이가 레이어 하나에선 티가 안 난다. 트랜스포머 구조는 어텐션 레이어와 피드포워드 레이어를 수십 번 쌓아서 통과시키며, 7B 파라미터 모델 기준으로 보통 32개의 레이어를 거친다. 각 레이어를 지날 때마다 약 1e-7 수준의 작은 오차가 실수값에 누적되어, 32번을 거치고 나면 최종 활성화 값이 원래와 수천 분의 몇 퍼센트 차이가 날 수 있다. 전체 레이어를 다 통과하고 나면 마지막 로짓 벡터의 값이 서로 조금씩 달라져 있으며, 로짓은 각 토큰이 다음에 올 가능성을 나타내는 원시 점수다.

Metal은 행렬 곱셈에 AMX 코프로세서를 쓰고, CUDA는 Tensor Core를 쓴다. 두 유닛은 내부 누산 정밀도도 다르고 퓨즈 곱-덧셈 연산을 처리하는 방식도 다른데, 퓨즈 곱-덧셈은 곱셈과 덧셈을 중간 반올림 없이 하나의 연산으로 처리하는 방식이다. Tensor Core는 FP16 입력을 FP32로 누산하는 반면, AMX는 누산 정밀도 설정이 다를 수 있어 같은 가중치를 읽더라도 누산 경로에서 값이 미세하게 갈린다. 이 차이가 모델 전체 추론 경로에 걸쳐 쌓인다.

샘플링에서 차이가 쌓이는 방식

로짓이 달라지면 소프트맥스 확률이 달라진다. 소프트맥스는 로짓을 전체 합이 1이 되는 확률 분포로 바꾸는 함수로, 로짓 값이 0.003 달라지면 확률도 그에 비례해서 미세하게 달라진다. 확률 분포가 달라지면 temperature나 top-p 같은 샘플링 파라미터가 다른 토큰을 선택하게 되는데, 예를 들어 “그래서”와 “따라서”가 각각 0.31과 0.29의 확률을 가졌다가 하드웨어 오차로 0.29와 0.31로 역전되면 첫 단어부터 달라진다. temperature는 분포를 얼마나 뾰족하게 혹은 평평하게 만들지 조절하는 값으로, 0에 가까울수록 가장 높은 확률의 토큰만 뽑히지만 조금만 올라가도 2위, 3위 토큰도 선택 대상이 된다.

문제는 첫 토큰이 달라지면 그다음 문맥도 달라진다는 데 있다. 모델은 앞서 생성된 토큰들을 입력으로 받아서 다음 토큰을 예측하기 때문에, 첫 번째 자리에 “그러니까”가 놓인 문장과 “반면에”가 놓인 문장은 그다음 예측 분포 자체가 달라진다. 첫 토큰이 달라지면 그 달라진 토큰이 다시 입력으로 들어가고, 그 위에서 또 다른 토큰이 생성되는 방식으로 오차가 시간축을 따라 연쇄된다. PyTorch 재현성 문서에서도 GPU 연산의 비결정성이 누적될 수 있음을 명확히 설명하며, 특히 어텐션 계산처럼 순서 의존적인 연산에서 더 강하게 나타난다고 한다.

temperature를 0으로 내려도 완전히 막히지 않는다. greedy decoding처럼 매번 가장 높은 확률의 토큰만 뽑더라도, 로짓 자체가 다르면 argmax가 다른 인덱스를 가리킬 수 있다. 1위와 2위 로짓이 각각 3.4821과 3.4819처럼 소수점 네 번째 자리부터 달라지는 상황이 그렇다. 두 값이 아주 가까울 때 하드웨어 연산 오차 하나로 순위가 바뀌며, 이는 greedy decoding이 실제로는 완전히 결정적인 전략이 아님을 의미한다.

이 발산이 짧은 응답에서는 거의 안 보인다. 한두 문장으로 끝나는 답변은 1위 토큰이 2위를 큰 폭으로 앞서는 경우가 많아서, 예를 들어 “네” 혹은 “아니요”를 고르는 이진 판단 프롬프트에서는 로짓 차이가 수십 배에 달하기도 한다. 반면 여러 문장을 생성하거나 여러 선택지 중 하나를 골라야 하는 형태의 프롬프트, 혹은 감정 분석처럼 경계가 모호한 판단에서는 발산이 훨씬 자주 일어난다. 생성이 길어질수록 초기 오차가 쌓일 기회가 많아진다.

파이프라인에서 생기는 문제

자동화 흐름에서 이 문제가 조용히 터진다. n8n처럼 노드 기반으로 흐름을 짜는 도구는 각 단계가 결정적으로 작동한다고 가정할 때 잘 돌아가며, 결정적이라는 건 같은 입력을 주면 항상 같은 출력이 나온다는 뜻이다. LLM을 노드 하나로 붙여 넣으면 이 전제가 흔들리고, 파이프라인 전체의 신뢰도 가정이 조용히 무너진다.

분류 노드를 예로 들면 이렇다. 텍스트를 넣으면 “긍정”이나 “부정”을 내놓는 구조인데, 개발 환경이 맥북이고 배포 환경이 리눅스 서버라면 같은 프롬프트를 처리하더라도 간헐적으로 다른 레이블이 나올 수 있다. 예컨대 “이 제품은 가격 대비 만족스럽지만 배송이 느렸다”처럼 긍부정이 혼재된 문장은 로짓 차이 하나로 레이블이 바뀌기 쉽다. 이 차이가 얼마나 자주 발생하는지 미리 확인하지 않으면 파이프라인이 어디서 틀어졌는지 알기 어려우며, 분류 결과가 다운스트림 노드의 조건 분기를 결정한다면 영향이 꽤 넓게 퍼진다.

재현 가능성을 전제로 짠 모니터링도 문제가 된다. 어제 통과한 프롬프트가 오늘 다른 서버에서 다른 답을 내놓으면, 그게 실제 회귀인지 하드웨어 차이인지 구분이 안 된다. 회귀는 코드나 모델이 바뀌어서 출력이 달라지는 현상이고, 하드웨어 차이는 코드가 그대로인데 실행 환경이 달라서 생기는 비결정성이다. 두 원인이 섞이면 알림이 울릴 때마다 원인 분류부터 해야 해서 디버깅 시간이 크게 늘어난다.

이 구조를 알고 나면 파이프라인 설계 방식이 달라진다. LLM 노드의 출력을 그대로 분기 조건으로 쓰는 대신, 후처리 단계에서 정규화하거나 같은 입력으로 3회 샘플링해 다수결을 쓰는 방향을 생각하게 된다. 혹은 분류 레이블 외에 신뢰도 점수도 함께 출력하게 하고, 점수가 낮은 경우에만 재실행하는 방어 로직을 넣을 수도 있다. 완전히 없앨 수는 없지만 영향 범위를 좁히는 방법은 있으며, 어느 노드가 취약한지 파악하는 것만으로도 디버깅 범위가 줄어든다.

또 다른 문제는 환경 간 테스트다. 개발 환경과 운영 환경이 다른 하드웨어를 쓰면 로컬에서 통과한 프롬프트 테스트가 운영에서 의미를 잃으며, 이를 모르고 로컬 테스트만 믿으면 운영 환경에서 간헐적으로 틀린 분류가 나오는데 원인을 찾기가 어렵다. 처음부터 두 환경에서 같은 프롬프트 세트를 돌려보고 불일치율을 측정해 두면, 이후 회귀 알림이 울렸을 때 기준선과 비교해 실제 문제인지 노이즈인지 빠르게 판단할 수 있다.

두 기기에서 나온 수치

맥북 M2와 NVIDIA Jetson Orin에 같은 7B 모델을 올리고 같은 프롬프트 세트를 반복 실행한 결과가 문서화되어 있다. temperature 0.7로 50개 프롬프트를 각각 5회씩 돌렸을 때, 같은 기기 안에서는 답변 불일치율이 12%였다. 두 기기 사이에서 비교하면 38%로 뛰었으며, 같은 기기 내 불일치는 샘플링 확률 자체의 변동이고 기기 간 불일치는 로짓 계산 차이까지 더해진 결과다. 달리 말하면 기기를 바꾸는 것만으로 불일치 원인이 하나 더 추가되어 총 불확실성이 세 배 이상으로 커진다.

temperature를 0으로 내렸을 때도 기기 간 불일치가 완전히 사라지지 않았다. 프롬프트 50개 중 4개에서 다른 답이 나왔으며, 그 4개를 들여다보면 두 기기의 1위 로짓과 2위 로짓 차이가 0.01 미만이었다. 나머지 46개에서는 1위와 2위 로짓 차이가 평균 0.3 이상으로, 이 정도 차이라면 하드웨어 오차가 순위를 뒤집기에 충분하지 않다. 결국 temperature 0은 로짓 차이가 충분히 큰 경우에만 결정성을 보장하는 방어책이다.

답변 길이 차이도 눈에 띄었다. 같은 프롬프트를 두 기기에서 돌렸을 때 첫 토큰이 달라진 경우 최종 응답의 평균 길이 차이가 47 토큰이었고, 첫 토큰이 같았던 경우엔 평균 3 토큰 차이였다. 이는 초기 발산이 단순히 단어 선택을 바꾸는 데 그치지 않고, 문장 구조 자체를 다른 방향으로 전개시킨다는 것을 뜻한다. 초기 발산이 이후 생성 전체의 방향을 바꾼다는 걸 수치로 확인할 수 있는 부분이다.

이 수치가 모든 모델과 프롬프트에 다 해당되진 않는다. “수도가 어디야?” 처럼 답이 거의 정해진 사실 질문에서는 불일치가 드물었고, “이 문단을 자연스럽게 다듬어줘”처럼 정답이 열려 있는 프롬프트에서 불일치가 집중됐다. 긴 문장 생성이나 여러 선택지 중 하나를 고르는 형태도 취약한 프롬프트 유형에 해당한다. 어떤 종류의 프롬프트가 취약한지 파악하면 어느 노드를 더 신경 써야 하는지 좁혀진다.

모델 크기도 변수다. 같은 실험을 13B 모델로 반복했을 때 기기 간 불일치율이 31%로 낮아졌으며, 모델이 클수록 1위 로짓과 2위 로짓의 차이가 평균적으로 커지기 때문에 하드웨어 오차가 순위를 뒤집는 상황이 줄어드는 것으로 보인다. 파라미터가 많을수록 특정 토큰으로의 확률 집중도가 높아지는 경향이 있어, 로짓 1위와 2위 사이의 격차 자체가 커지는 효과가 있다. 작은 모델일수록 비결정성에 더 민감하다는 뜻이며, 리소스 절약을 위해 작은 모델을 쓸수록 파이프라인 안정성에 더 신경 써야 한다.

Leave a Comment