URL: https://www.youtube.com/watch?v=btxG75rNJC4 날짜: 2026-09-20 채널: aiDotEngineer 발표자: Asaf Gardin, Yuval Belfer (AI21) 원제: Two Bugs That Hid in Plain Sight: A vLLM Debugging Detective Story — Asaf Gardin & Yuval Belfer
📌 핵심 질문 / 이 영상이 다루는 핵심 논점
==모델이 충돌하지도, 경고를 내지도 않는데 자신 있게 틀린 출력을 내놓을 때, 품질 문제가 아니라 추론 엔진의 상태·메모리·스케줄링을 어떻게 추적할 것인가?==
- 첫 번째 버그는 vLLM 스케줄러가 Mamba 요청을
prefill보다 먼저decode로 보내면서 이전 요청의 stale state를 읽은 문제다. - 두 번째 버그는 Mamba 커널의
uint32인덱스가 약 40억을 넘을 때 예외 없이 래핑되어 잘못된 상태 캐시 위치를 가리킨 문제다. - 두 버그 모두 Mamba state cache와 메모리 압력에서 증상이 드러났고, log probability 비교·부하 조절·요청 ID 전파가 원인을 좁히는 핵심 도구가 됐다.
AI21은 Transformer와 Mamba(SSM)를 결합한 Jamba 모델을 학습하고 서비스하면서, 정상처럼 보이는 출력 뒤에 숨어 있던 두 가지 엔지니어링 버그를 만났다. 이런 문제는 모델 품질을 재학습으로 개선할 수 있는 종류가 아니다. 같은 입력인데 특정 요청 또는 특정 RL 단계에서만 결과가 바뀌므로, 엔진 내부의 상태와 메모리 접근을 직접 관찰해야 한다.
1. 배경: 정상처럼 보이는 실패를 정의하기
1.1. AI21과 Jamba의 실행 환경
-
모델과 조직의 맥락
- AI21: AI 연구소로 시작해 foundation model을 만들었고, 발표자들은 모델 학습과 프로덕션 서빙을 함께 다뤘다.
- Jamba: Transformer와 Mamba를 결합한 hybrid architecture다. Mamba는 state-space model(SSM) 계열로, attention과 달리 요청별 지속 상태를 관리한다.
-
문제가 생긴 지점
- 전체 경로: 프롬프트 tokenization → forward pass의 prefill → 생성 중 decode → detokenization 순서로 요청이 텍스트가 된다.
- 엔진의 복잡성: vLLM은 기본 모델 계산 외에도 배칭·스케줄링·KV cache·Mamba state cache·CUDA 커널을 조정하므로, 모델 자체가 맞아도 실행 순서나 버퍼 인덱스가 틀릴 수 있다.
1.2. 이 종류의 버그가 특히 어려운 이유
-
관측 가능한 실패 신호가 없다
- 프로세스 crash가 없고 warning이나 error도 없다.
- 출력의 confidence가 높고, 품질 문제라고 단정할 근거도 약하다. 즉 시스템이 틀린 답을 낮은 확률로 내는 것이 아니라, 잘못된 상태를 정상적인 계산처럼 사용한다.
-
재현 조건이 좁다
- 소수의 요청만 보내서는 발생하지 않고 일정한 workload와 동시성이 필요하다.
- vLLM에서만 나타나고 다른 inference framework에서는 나타나지 않았으며, 수천 건 단위의 요청을 거쳐야 재현되는 경우가 있어 단순한 프롬프트 테스트로는 발견하기 어렵다.
2. 사건 1 — Imposter Request: 요청이 자기 것이 아닌 상태를 읽다
2.1. 증상과 재현 조건
-
RL 학습에서 시작된 이상 출력
- Jamba 모델에 GRPO 계열의 RL training을 수행하고 프로덕션 부하를 흘리던 중, 정상 응답 사이에 의미 없는 gibberish가 섞였다.
- 이 현상은 발표자들이 “one-in-a-thousand gibberish”라고 부를 정도로 드물었다. 쉽게 복제할 만큼 흔하지는 않지만, 제품에 싣기에는 너무 자주 발생하는 빈도였다.
-
빠른 재현을 위한 압력 조절
- 처음에는 프롬프트와 작은 배치를 바꿔 가며 실행했지만 문제가 재현되지 않았다.
- vLLM의
gpu_memory_utilization을 기본 약 90%에서 20%로 낮추고 많은 요청을 동시에 보냈다. - 그 결과 예시 요청 번호 8,854에서 gibberish가 발생했다. temperature를 0으로 설정해 같은 요청이 매번 같은 이상 출력을 내도록 만들었다.
2.2. 모델 문제가 아님을 분리하는 방법
-
Hugging Face Transformers를 기준선으로 사용
- Transformers는 Mamba 커널을 비교적 vanilla한 방식으로 구현한 기준선이고, vLLM은 여러 기능을 지원하기 위해 커널과 엔진을 상당히 변경한 구현이다.
- 동일한 prompt와 생성 결과를 vLLM에서 얻고, 출력과 함께 반환되는 token별 log probability를 보존했다.
-
prefill logit 분포를 대조
- vLLM의 prompt와 generation 전체 시퀀스를 Transformers의 forward pass에 넣되, 비교 단계에서는 prefill만 실행했다.
- 두 결과의 logits에 softmax를 적용하고 token별 logprob 분포 차이를 계산했다. 이 비교는 “모델이 이상한가”와 “엔진이 같은 모델을 다르게 실행하는가”를 분리하는 장치다.
- 추론 품질을 감으로 판단하지 않고, 기준선과의 분포 divergence를 통해 첫 번째로 달라지는 token과 실행 구간을 찾을 수 있었다.
2.3. 첫 번째 용의자에서 실행 흐름까지
-
커널과 메모리 안전성 점검
- Mamba CUDA prefill kernel의 수식과 입력·출력 tensor를 호출 전후로 검사했지만 계산은 정상으로 보였다.
- NVIDIA Compute Sanitizer로 out-of-bounds memory access와 다른 메모리 오류를 검사했지만 이상이 나오지 않았다.
-
prefill과 decode를 분리한 실험
- Mamba에서 가능한 범위 내에서 계산을 prefill kernel만 통과시키고 decode kernel 호출을 우회했다.
- 그러자 gibberish가 사라졌고, 처음에는 decode kernel이 범인이라고 결론 내리기 쉬웠다. 그러나 이 결론은 너무 빨랐고, 커널 자체보다 “언제 어떤 요청에 호출되는가”가 핵심이었다.
2.4. 결정적 단서: 요청 ID를 forward pass까지 운반하기
-
식별자가 사라지는 문제
- 요청이 forward pass에 도착한 뒤 prefill·decode kernel로 내려가면, 내부에는 tensor·number·matrix만 남아 어떤 prompt에 해당하는지 알기 어렵다.
- 따라서 특정 요청을 조건부 breakpoint에서 멈추려 해도 요청과 커널 실행의 연결 고리가 없었다.
-
관찰 가능성(observability) 추가
- 발표자들은 request ID를
forward context라는 클래스에 넣고, 상위 엔진에서 Mamba forward pass 직전까지 전파했다. - gibberish를 만든 요청 ID에만 걸리는 조건문과 breakpoint를 추가해 해당 요청의 metadata, scheduler 결정, kernel 경로를 직접 검사했다.
- 발표자들은 request ID를
2.5. 근본 원인: Mamba의 상태가 prefill 전에 decode됐다
-
정상적인 요청 순서가 뒤집힘
- 새 prompt는 먼저 전체 prompt token을 처리하는 prefill을 거쳐 state를 만들고, 그 뒤 한 token씩 생성하는 decode를 수행해야 한다.
- 문제의 요청은 scheduler 결정에 의해 첫 forward pass에서 prefill보다 decode를 먼저 수행했다.
-
이전 요청의 state가 섞인 이유
- 많은 요청이 이미 계산된 뒤 decode-first 요청이 실행되자, Mamba decode kernel은 해당 요청을 위해 새로 만들어진 state가 아니라 이전 요청의 stale state를 읽었다.
- attention의 KV cache는 일반적으로 token을 쓰고 읽는 구조라 stale 값이 있어도 새 값이 덮어써질 수 있지만, Mamba decode는 state를 먼저 읽은 뒤 그 위에서 계산한다.
- 결과적으로 kernel 수식은 틀리지 않았다. 정상 kernel이 잘못된 시점에 잘못된 요청에 호출되어, 이전 요청의 내부 상태로부터 gibberish를 생성했다.
2.6. 수정 방법
- scheduler가 요청을 처음 분류할 때 아직 계산된 token이 하나도 없는 요청을 식별한다.
- 해당 요청의 상태 값을 0으로 표시해 prefill로 강제한다.
- forward pass에 들어온 요청이 decode나 chunked 경로가 아니라 prefill 경로를 타는지 보장한다.
- 이 수정은 새 모델 학습이나 커널 수식 변경이 아니라, 요청 lifecycle의 invariant를 scheduler 단계에서 보장하는 작은 변경이었다.
3. 사건 2 — Logprob Spike: 32비트 인덱스가 조용히 되감기다
3.1. RL 평가에서 발견된 불일치
-
동일 입력·동일 가중치인데 결과가 달라짐
- post-training RL을 실행하고 benchmark를 확인하던 중 rollout 단계와 후속 FSDP 단계 사이에 logprob spike가 관찰됐다.
- 아직 weight update가 일어나기 전이므로, 동일한 입력과 동일한 weight라면 두 logprob는 같아야 했다.
-
주기성
- spike는 정확히 매 12 step마다 반복됐다.
- 매번 발생하는 것이 아니라 일정한 누적량이나 메모리 위치에 도달할 때만 나타났기 때문에, 단순한 모델 품질 변동이 아니라 엔진 내부 자원과 연결된 신호로 볼 수 있었다.
3.2. 실패 형태를 바꾸는 knob 찾기
-
오류 횟수보다 실패의 모양을 바꾸는 실험
- 단순히 “얼마나 자주 실패하는가”를 측정하는 것보다, 설정값을 바꿨을 때 실패 시점·공간·위치가 어떻게 이동하는지 보는 것이 원인 추적에 유용하다.
- 기본 RL 엔진은 prompt당 8개의 rollout을 생성했으므로, 이를 16, 32, 64, 128로 늘렸다.
-
롤아웃 수와 재현 속도의 관계
- rollout을 늘릴수록 12, 24 step을 기다리지 않아도 되는 방향으로 실패 시점이 앞당겨졌다.
- 128 rollout/prompt에서는 첫 step부터 문제가 재현됐다. 이 조절은 버그를 고친 것이 아니라, 긴 실험을 짧은 디버깅 feedback loop로 압축한 것이다.
3.3. 잘못된 레버: GPU 메모리를 줄이면 증상이 사라졌다
-
겉보기에는 유효한 가설
- 첫 사건에서 0.9에서 0.2로 GPU memory utilization을 줄였을 때 문제가 사라졌으므로, 메모리 압력이 원인일 가능성을 의심했다.
- 그러나 rollout을 늘린 사건에서도 같은 설정을 적용하자 오류가 없어졌다. 이 변화는 원인을 해결한 것이 아니라 오류가 발생할 주소 범위에 도달하지 못하게 만든 것이었다.
-
압력 실험의 해석
- 작은 메모리 사용률은 vLLM이 더 작은 state buffer를 할당하게 했다.
- 그 결과 cache index가 문제의 먼 위치까지 커지지 않아 overflow 지점을 건드리지 않았다.
- 즉 “메모리를 줄여서 안정화됐다”가 아니라 “잘못된 32비트 인덱스가 임계 위치까지 가지 못해 잠복했다”가 정확한 해석이다.
3.4. 근본 원인: uint32 포인터 오프셋 오버플로
-
Mamba kernel의 인덱스 타입
- 해당 Mamba kernel은 offset을
uint32index/pointer 패턴으로 다루고 있었다. - offset이 약 4 billion, 즉 32비트 unsigned 정수의 범위를 넘어가면 예외를 던지지 않고 0부터 다시 시작하는 wraparound가 발생한다.
- 해당 Mamba kernel은 offset을
-
왜 logprob가 조용히 틀렸는가
- wraparound한 인덱스는 유효하지 않은 state cache 위치를 가리키지만, 하드웨어나 커널 관점에서 반드시 crash가 되는 것은 아니다.
- 모델은 오류를 보고하지 않고 엉뚱한 state를 읽어 계산을 계속하므로, 일부 token의 logprob만 튀거나 그 결과 gibberish가 나올 수 있다.
- 메모리 사용량·rollout 수·동시 배치가 커질수록 offset이 임계값에 빨리 도달해 주기적 spike가 앞당겨졌다.
3.5. 수정 방법
- 해당 변수의 타입을
uint32에서size_t로 변경했다. - 현대 하드웨어에서
size_t는 일반적으로 64비트 unsigned 정수로 해석되므로, 현실적인 workload에서 도달할 수 없는 훨씬 넓은 주소 범위를 표현한다. - 64비트로 바꾸자 주소 wraparound가 일어나지 않았고, logprob spike와 이에 따른 gibberish가 사라졌다.
- 핵심 수정은 한 단어의 자료형 변경이었지만, 그 한 단어를 찾기까지 재현 조건을 조절하고 logprob·커널·캐시 주소를 함께 대조해야 했다.
4. 두 사건을 관통하는 디버깅 원리
4.1. 같은 증상, 다른 범죄 현장
-
공통된 표면 증상
- 두 사건 모두 crash나 명시적 error가 아니라 silent gibberish 또는 silent logprob spike로 나타났다.
- 둘 다 Mamba state cache 주변에서 발생했고, GPU 메모리 압력이나 누적 workload가 커질 때 드러났다.
-
서로 다른 실제 원인
- 사건 1은 상태 초기화·scheduler ordering 문제였다. decode kernel을 잘못된 요청에 먼저 호출했다.
- 사건 2는 주소 표현 범위 문제였다.
uint32offset이 overflow한 뒤 다른 메모리 위치를 가리켰다. - 따라서 “Mamba cache가 의심스럽다”는 공통 가설은 출발점일 뿐이며, 실행 순서와 메모리 주소를 분리해서 검증해야 한다.
4.2. 효과적인 조사 순서
-
재현성을 먼저 만든다
- temperature를 0으로 고정해 동일 입력에서 동일한 실패 요청을 확보한다.
- 작은 테스트에서 안 보이면 GPU memory utilization, 동시 요청 수, rollout 수, 배치 규모 같은 engine knob를 바꿔 실패의 시간과 위치를 이동시킨다.
-
기준선과 수치 비교를 만든다
- Hugging Face Transformers처럼 단순한 구현을 baseline으로 두고 동일 prompt의 logits·softmax 분포·token별 logprob를 비교한다.
- 품질 평가나 최종 문자열만 보지 말고, divergence가 처음 생기는 token과 prefill/decode 경계를 기록한다.
-
정적·동적 안전성 점검을 병행한다
- 커널의 수학, tensor input/output, prefill·decode 경로를 각각 검사한다.
- NVIDIA Compute Sanitizer로 out-of-bounds를 확인하되, 도구가 이상을 보고하지 않았다고 silent state corruption 가능성까지 배제하지 않는다.
-
데이터에 신원을 붙인다
- tensor만 보면 어떤 요청의 상태인지 알 수 없으므로 request ID를 forward context에 실어 커널 직전까지 전달한다.
- 문제 요청만 조건부 breakpoint로 멈춰 scheduler metadata, cache index, state 초기화 여부, 커널 순서를 확인한다.
4.3. 실행 가능한 체크리스트
- logprob forensics: 자체 inference framework와 독립 baseline의 token별 logprob 비교 스크립트를 만든다.
- stress reproduction: 메모리를 제한하고 동시성·배치·rollout 규모를 단계적으로 올려 재현 시간을 줄인다.
- failure-shape mapping: 설정을 바꿀 때 실패 횟수뿐 아니라 timing, memory space, cache location, 요청 위치가 어떻게 움직이는지 기록한다.
- state lifecycle invariant: 새 요청은 반드시 prefill 후 decode를 거치고, state가 없는 요청을 decode로 보내지 않는지 검증한다.
- index-width audit: GPU 커널의 offset·pointer·cache index가 32비트로 제한되어 있지 않은지, 누적 workload에서 wraparound가 가능한지 계산한다.
- 소스 코드 직접 확인: 복잡한 vLLM 같은 프레임워크도 추상화에만 의존하지 말고 scheduler와 kernel 코드를 직접 읽고 필요한 경우 instrumentation을 추가한다.
주요 발언 모음
“이런 종류의 버그에는 crash도, warning도, error도 없다.”
“커널이 잘못한 것이 아니라, 잘못된 요청에 잘못된 시간에 호출된 것이다.”
“상태를 가진 추론 시스템은 크게 실패하지 않고, 자신 있게 거짓말한다.”
“실패가 얼마나 많이 일어나는지만 보지 말고, 실패의 모양과 시점, 공간, 위치를 움직이는 레버를 찾아라.”
“복잡한 프레임워크의 코드를 직접 파고드는 것을 두려워하지 말라. 직접 눈으로 확인하지 않으면 무슨 일이 일어나는지 완전히 이해할 수 없다.”
핵심 데이터 & 수치
- 약 1,000건당 한 번: 첫 번째 gibberish가 불규칙하게 발생한 빈도다. 너무 드물어 재현이 어렵지만 제품 출하에는 너무 흔하다.
- 요청 8,854: GPU memory utilization을 90%에서 20%로 낮추고 동시 요청을 보냈을 때 결정적으로 재현한 예시 요청 번호다.
- 90% → 20%: 첫 번째 사건의 재현을 위해 vLLM
gpu_memory_utilization을 낮춘 범위다. - temperature 0: 실패 요청을 결정론적으로 재현하고 동일한 gibberish를 반복 확인하기 위해 사용했다.
- 매 12 step: 두 번째 사건에서 weight update 전에 logprob spike가 나타난 주기다.
- 8 → 16 → 32 → 64 → 128 rollout/prompt: rollout 수를 키워 12 step의 실패를 첫 step까지 앞당긴 실험 범위다.
- 약 40억(2³²):
uint32offset이 표현할 수 있는 한계를 넘으면 wraparound가 시작되는 규모다. uint32→size_t: 두 번째 버그를 수정한 핵심 자료형 변경이다.
결론 및 시사점
- 정상 응답률이나 프로세스 생존 여부만으로 추론 시스템이 정상이라고 판단하면 안 된다. 상태 캐시와 메모리 인덱스가 틀려도 시스템은 높은 confidence로 결과를 반환할 수 있다.
- prefill과 decode는 단순한 성능 단계가 아니라 상태 초기화 순서를 결정하는 correctness invariant다. 새 요청을 decode로 보내지 않는지 scheduler에서 보장해야 한다.
- Mamba처럼 상태를 읽고 갱신하는 구조는 attention의 KV cache와 오류 양상이 다르다. 이전 요청의 stale state가 다음 요청을 오염시킬 수 있으므로 요청별 state lifecycle을 추적해야 한다.
- 메모리 압력을 낮춰 문제가 사라지는 것은 해결이 아니라 주소·버퍼 경계를 피한 것일 수 있다. 설정 변경은 원인 제거가 아니라 실패 표면을 이동시키는 실험으로 해석해야 한다.
- 독립 baseline과 token-level logprob 비교는 출력 문자열만으로 발견하기 어려운 inference engine 회귀를 조기에 드러낸다.
- 가장 복잡해 보이는 문제도 request ID를 커널까지 전파하고, 재현 조건을 줄이며, 자료형과 주소 계산을 직접 확인하면 작은 수정으로 해결할 수 있다.
20줄 핵심 요약
-
AI21은 Transformer와 Mamba를 결합한 Jamba를 학습·서빙하며 조용히 틀리는 두 추론 엔진 버그를 만났다.
-
두 버그는 crash·warning·error 없이 높은 confidence의 gibberish와 logprob 이상으로 드러났다.
-
첫 번째 사건은 약 1,000건당 한 번 발생해 단순한 프롬프트 테스트로는 재현하기 어려웠다.
-
vLLM에서만 나타났고 일정한 동시 요청과 workload가 쌓여야 증상이 표면화됐다.
-
GPU memory utilization을 약 90%에서 20%로 낮추고 동시 요청을 늘려 재현 시간을 압축했다.
-
temperature 0과 반복 실행으로 예시 요청 8,854의 gibberish를 결정론적으로 고정했다.
-
Transformers를 baseline으로 삼아 동일 시퀀스의 prefill logits와 token별 logprob를 비교했다.
-
CUDA prefill 계산과 NVIDIA Compute Sanitizer 검사는 명확한 메모리 오류를 찾지 못했다.
-
prefill만 사용하면 gibberish가 사라져 처음에는 decode kernel 자체가 의심됐다.
-
request ID를 forward context로 전달하자 문제 요청이 prefill보다 decode를 먼저 탄 사실이 드러났다.
-
Mamba decode는 state를 먼저 읽으므로 새 state 없는 요청이 이전 요청의 stale state를 사용했다.
-
attention KV cache와 달리 Mamba state는 잘못된 순서에서 자동으로 새 값으로 덮어써지지 않았다.
-
수정은 계산되지 않은 token을 가진 새 요청을 scheduler에서 prefill로 표시하고 decode를 막는 방식이었다.
-
두 번째 사건은 weight update 전 rollout과 후속 FSDP 단계 사이에서 매 12 step logprob가 튀는 현상이었다.
-
prompt당 rollout을 8에서 16·32·64·128로 늘리자 128에서 첫 step부터 문제가 재현됐다.
-
GPU 메모리를 줄이면 증상이 사라졌지만, 이는 원인을 고친 것이 아니라 문제 주소에 도달하지 못하게 한 결과였다.
-
Mamba 커널의
uint32offset이 약 40억을 넘으면서 예외 없이 wraparound를 일으켰다. -
잘못된 cache 위치를 읽은 뒤에도 계산이 계속되어 logprob spike와 gibberish가 조용히 반환됐다.
-
uint32를size_t로 바꿔 64비트 주소 범위를 사용하자 오버플로가 사라졌다. -
logprob 비교·부하 조절·요청 ID 전파·직접 코드 검사가 상태ful 추론 디버깅의 재사용 가능한 기본 도구다.
