자동 제목 요청 뒤 본 대화가 제목처럼 답하던 문제를 추적한 기록
에이전트 클라이언트가 자동 제목을 만든 뒤 본 대화에도 제목 같은 한 줄만 돌아오는 문제였습니다. 템플릿 해시로 프롬프트 문제를 배제하고, 8월과 9월 빌드를 대조해 조건부로 결론을 냈습니다.
2026-10-05 추가: 9월에 고친 것은 27B 서비스뿐이었고, 같은 서버의 다른 모델(Flash Next) 서비스는 문제의 8월 빌드로 계속 돌고 있었습니다. 10월 4일 그 서비스에서 같은 현상이 다시 나왔고, 앞 요청의 내용이 다른 클라이언트의 답에 섞일 수 있다는 점에서 보안 문제로 다뤘습니다. 글 끝의 "10월 재발" 절에 정리했습니다.
로컬 llama.cpp 서버(Qwen3.8 27B, gfx1151/HIP)에 코딩 에이전트 OpenCode를 붙였더니, 세션을 시작할 때 에이전트가 보내는 자동 제목 요청 뒤로 본 대화에도 "간단한 인사" 같은 제목 한 줄만 돌아왔습니다. 프롬프트, 클라이언트, 서버, kernel 중 어디서 생긴 문제인지 좁혀 간 과정입니다.
프롬프트 문제인지부터 확인
가장 먼저 의심한 것은 클라이언트가 제목 지시를 본 대화에 섞어 보냈을 가능성입니다. 프록시로 HTTP 요청을 관찰하고, 서버가 실제로 렌더링한 템플릿 문자열을 받아 해시로 비교했습니다.
- 본 대화의 system 프롬프트는 모든 조건에서 SHA256이 같았고(11,081자) 제목 지시가 없었습니다.
- 제목용 system(2,096자)은 별도 HTTP 요청에만 들어 있었습니다.
- 서버가 렌더링한 본 대화 문자열도 8월·9월 빌드에서 해시가 같았습니다.
요청에는 문제가 없었습니다. 그렇다면 앞선 요청의 상태가 서버 안에 남아 다음 요청에 영향을 준 것으로 봐야 했습니다.
조건을 하나씩 바꿔 보기
한 번에 하나씩 바꿔 각 1회 실행했습니다. 걸린 시간에는 큐 대기가 들어 있어 속도 비교로는 쓰지 않습니다.
| 조건 | 본 대화 결과 |
|---|---|
| 자동 제목 후 본 대화 | 제목처럼 답함 |
| 자동 제목 없이 (제목을 직접 지정) | 정상 인사 |
자동 제목 + cache_prompt=false |
제목처럼 답함 |
| 자동 제목 + fusion을 끄는 환경변수 | 제목처럼 답함 |
제목 요청이 먼저 있어야 문제가 생겼고, prompt cache를 꺼도 똑같아서 단순한 캐시 문제는 아니었습니다. fusion 환경변수로도 안 됐지만, 그 변수는 fused kernel 전부를 끄는 스위치가 아니어서 이 결과로 kernel 쪽을 배제하지는 않았습니다.
8월 빌드와 9월 빌드 대조
8월 말 빌드(90c26fcd4)와 9월 19일 빌드(e613ef2c8)를 같은 모델과 설정으로 비교했습니다.
| 빌드 / backend | 본 대화 |
|---|---|
| 8월 HIP | 제목처럼 답함 (반복 재현, greedy·seed 고정에서도) |
| 9월 HIP | 정상 3회 |
| 9월 Vulkan | 정상 2회 |
greedy와 seed를 고정한 비교에서 세 조건의 렌더링 템플릿 해시가 같았는데 응답만 달랐습니다. 문제는 프롬프트가 아니라 빌드에 따라 나타났습니다.
9월 빌드의 GDN kernel 수정이 고친 것처럼 보였지만, 확인해 보니 두 빌드의 해당 kernel 파일은 해시가 같았습니다. 달라진 곳은 graph 재사용과 scheduler 같은 다른 부분이고, bisect를 하지 않았으니 어느 커밋이 고쳤는지는 모릅니다. upstream에 비슷한 증상의 이슈가 있지만 다른 빌드에서 나온 확인되지 않은 보고라 같은 원인이라고 보지 않았습니다.
서비스 교체
원인 커밋을 몰라도 "8월 HIP 빌드에서는 재현되고, 9월 빌드는 HIP와 Vulkan 모두 이 시나리오를 통과한다"는 결론은 낼 수 있습니다. 이를 근거로 서비스를 9월 HIP 빌드로 바꿨습니다.
- 바꾸기 전 서비스 설정을 백업하고, 바꾼 뒤 실행 파일과 불러온 라이브러리가 새 빌드인지 확인했습니다.
- 한국어 산술, 이미지, 도구 호출 JSON, 도구 결과 전달, 별개의 인사 요청까지 회귀 검사 5건을 통과했습니다.
- 실제 클라이언트로 자동 제목 뒤 본 대화를 보내는 시나리오를 두 번 다시 돌려 정상 응답을 확인했습니다.
돌이켜 보면 도움이 된 것은 세 가지였습니다. 프롬프트가 원인이 아니라는 것을 렌더링 결과의 해시로 바로 확인할 수 있었습니다. 우회 실험이 실패했을 때 그 스위치가 정말 해당 경로를 끄는지 먼저 따져 봤습니다. 그리고 bisect 없이 "무엇이 고쳤다"고 쓰는 대신 "어느 빌드에서 재현되는가"로 결론을 적었습니다.
10월 재발: 다른 서비스, 다른 클라이언트
9월 교체는 27B 모델을 서비스하던 llama-server에만 했습니다. 같은 기계에서 Qwen3.8 Flash Next를 서비스하던 llama-server는 8월 빌드(90c26fc)로 계속 돌고 있었습니다. 10월 4일 Open WebUI에서 새 대화의 첫 메시지 "안녕하세요?"에 대한 답이 { "title": "한국어 인사" }로 저장됐습니다. Open WebUI도 첫 메시지 때 제목 생성 요청을 먼저 보내고 이어서 실제 대화 요청을 보내는데, 두 요청이 같은 슬롯에서 연달아 돌면서 같은 일이 일어났습니다.
처음에는 Open WebUI 쪽 버그를 의심했지만, 서버에 직접 요청을 보내 재현하면서 기각했습니다.
| 같은 슬롯에 순서대로 보낸 요청 | 결과 |
|---|---|
| A: 제목 생성 템플릿 + "안녕하세요?" (303토큰) | 제목 JSON, 정상 |
| B: 일반 system + 도구 목록(약 6K토큰) + "안녕하세요?" | 제목 JSON. reasoning에 A의 문구가 나옴 |
A → B, cache_prompt=false |
여전히 섞임 |
| A → 짧은 대화(system 한 줄) | 정상 |
9월에 몰랐던 것이 세 가지 드러났습니다. 첫째, 프롬프트 캐시를 꺼도 막히지 않았습니다. 둘째, 짧은 요청은 괜찮고 긴 요청에서만 섞였습니다. prefill이 여러 ubatch에 걸칠 때로 추정하지만 확인하지는 않았습니다. 셋째, 섞이는 것이 제목 지시만이 아니라 앞 요청의 내용이라서, 여러 클라이언트나 여러 사람이 한 서버를 쓰면 한 쪽이 보낸 내용이 다른 쪽의 답에 나올 수 있습니다. 그래서 고칠 때까지 서비스를 멈추고 민감한 입력을 피하도록 했습니다.
이번에는 db00347a4 빌드로 바꾸고 더 넓게 확인했습니다. 구·신 빌드에 보낸 HTTP 요청과 서버가 렌더링한 프롬프트가 바이트 단위로 같은 상태에서, 새 프로세스 두 개로 총 77개 요청을 보냈습니다. 위 A → B 조합, 서로 다른 문서 30개를 순서대로 보내기와 8개를 동시에 보내기, 문서 인용, 이미지, 도구 호출, temperature 0.7 요청까지 포함했고 내용이 섞인 경우는 없었습니다. 문서 식별자 38개를 모두 맞혔고 자기 문서 인용 6건도 정확했습니다. 다만 JSON만 출력하라는 지시는 6건 중 4건에서 코드펜스를 붙여서 지키지 않았으니, 출력 품질까지 77건 모두 통과했다고 하지는 않습니다.
원인 커밋은 여전히 모릅니다. 9월과 마찬가지로 구·신 빌드의 GDN kernel 파일에는 차이가 없고, 다른 상태·인덱서·그래프 처리 쪽에 변경이 많습니다. upstream의 비슷한 보고(#29092)는 GPU 한 층을 CPU로 옮겨 fused 연산을 끄면 막힌다고 했지만, 다른 빌드에서는 그렇게 해도 29건 중 1건이 섞였다는 댓글이 있어 그 방법을 쓰지 않았습니다.
이번 재발에서 얻은 교훈은 하나입니다. 9월에 "어느 빌드에서 재현되는가"로 결론을 냈다면, 그 빌드를 쓰는 서비스를 모두 찾아 바꿨어야 했습니다. 재현된 서비스 하나만 바꾸고 끝낸 탓에 같은 문제가 다른 이름으로 돌아왔습니다.
이 서버의 성능 측정은 Qwen3.8 upstream 재측정에 있습니다.