TECH 으로 돌아가기
TECH HACKER NEWS 오늘 7분 읽기 26 READS

셰이더가 한 대에서만 끊긴 이유: 부동소수점 소수부가 남긴 디버깅 기록

셰이더가 한 대에서만 끊긴 이유: 부동소수점 소수부가 남긴 디버깅 기록
SOURCE IMAGE · HACKER NEWS

게임 배경이나 뮤직비디오 영상에 쓰이는 프로시저럴 그래픽은 대개 GPU에서 픽셀 단위로 실행되는 짧은 셰이더 코드 몇 줄로 만들어진다. 한 개발자가 보로노이(Voronoi) 다이어그램 셰이더를 만들어 음악 프로젝트 배경으로 쓰려던 과정에서, 여러 대의 컴퓨터 중 단 한 대에서만 애니메이션이 미세하게 끊기는 현상을 발견했다. 이 글은 그 원인을 일주일간 추적한 디버깅 기록으로, 겉보기엔 사소한 버그가 어떻게 GPU 컴파일러와 부동소수점 처리라는 저수준 영역까지 파고들게 만드는지를 잘 보여준다.

재현되지 않는 버그의 함정

문제의 셰이더는 그리 복잡하지 않았다. 공간을 균등한 타일로 나누고 각 타일 안에 무작위 점(보로노이 중심)을 배치한 뒤, 각 픽셀에서 가장 가까운 9개 중심까지의 거리를 계산해 셀을 구분하는 방식이다. 여기에 노이즈를 이용한 공간 왜곡과 팔레트 순환 효과를 더했다. 문제는 브라우저(파이어폭스·크롬·엣지)를 가리지 않고, 그러나 테스트한 5개 기기 중 오직 한 대에서만 첫 프레임부터 셀이 튀는 현상이 나타났다는 점이다. 특정 환경에서만 재현되는 버그는 디버깅에서 가장 까다로운 유형이다. 상태를 저장하지 않는 순수 함수에 가까운 프래그먼트 셰이더 특성상, 입력이 같으면 출력도 같아야 하는데 하드웨어에 따라 결과가 달라졌기 때문이다.

LLM이 찾아준 해법, 그러나 틀린 진단

주변 그래픽 프로그래머들이 시간을 내주지 못하자 개발자는 LLM에 코드를 통째로 넘겼다. 모델(Kimi K3)은 노이즈 함수 초반의 fract 호출을 지목했고, 엔비디아 40번대 드라이버에서 유사 불만이 보고된 적이 있다는 점을 근거로 fract(x)를 수학적으로 동일한 x - floor(x)로 바꿔볼 것을 제안했다. 실제로 이 치환은 문제를 없앴다. LLM은 "드라이버가 fract()를 잘못 컴파일해 큰 값에서 소수부가 시간에 따라 불연속적으로 튄다"는 그럴듯한 근본 원인까지 제시했다.

하지만 개발자는 이 설명을 납득하지 못하고 최소 재현 코드를 만들려 했는데, 여기서 진단이 무너졌다. LLM은 여러 모델(상용 Opus 포함)을 바꿔가며 시도했지만 문제를 재현하는 단 하나의 프로그램도 만들지 못했다. fract가 특정 입력 범위에서 이상값을 낸다는 가설로 만든 테스트들은 모든 기기에서 차이가 없거나, 문제없는 기기에서도 오차가 나타났다. 결국 드라이버 miscompile 가설은 무효였고, LLM은 원인을 이해해서가 아니라 우연히 해법을 맞춘 셈이었다.

진짜 단서는 소수점 한 자리에

직접 값을 바꿔가며 실험하던 중 결정적 단서가 나왔다. 노이즈 함수에 넘기는 인자에 소수부가 있는 부동소수점 곱셈이 하나라도 포함되면 셰이더가 정상 동작한 것이다. 원본의 스칼라 곱수를 398.0에서 398.1로 바꾸자, fract 코드를 그대로 둔 채로도 끊김이 사라졌다. 반대로 1.0을 곱하거나 곱셈을 아예 없애면 문제가 재발했고, 1.001처럼 소수부를 살리면 다시 정상이 됐다. 이는 문제의 본질이 fract 자체가 아니라, 리터럴 값 하나에 따라 GPU 컴파일러가 생성하는 기계어가 달라지는 데 있음을 시사한다.

원인을 확인하기 위해 개발자는 오픈소스 그래픽 디버거 RenderDoc으로 컴파일된 셰이더를 뜯어보기로 했다. 흥미롭게도 같은 코드를 네이티브 OpenGL 래퍼로 돌렸을 때는 버그가 재현되지 않아, 브라우저의 WebGL 프레임을 직접 캡처해야 했다. 여러 자식 프로세스를 띄우는 크로미움을 캡처 설정으로 실행해 두 번째 자식 프로세스에서 문제의 드로우 콜을 찾아냈다. 그리고 여기서 중요한 사실이 드러난다. 디컴파일된 셰이더가 OpenGL이 아니라 DirectX 11용 ps_5_0 포맷이었다는 점이다. 브라우저가 WebGL을 ANGLE 등을 통해 D3D로 변환하고 있었던 것으로, 곱수 하나만 바꾼 두 버전의 컴파일 결과는 서로 크게 달랐다.

실무자가 얻을 교훈

이 기록은 아직 결론에 도달하기 전에 끊기지만, 실무적으로 여러 시사점을 남긴다. 첫째, 웹에서 셰이더를 배포할 때 개발자가 마주하는 것은 순수한 OpenGL이 아니라 브라우저·ANGLE·그래픽 드라이버를 거친 여러 번역 계층이며, 최적화 과정에서 리터럴 상수 하나가 코드 경로를 바꿔 부동소수점 정밀도 문제를 드러낼 수 있다는 점이다. 둘째, LLM은 유용한 가설과 심지어 동작하는 수정안까지 내놓을 수 있지만, 그 진단이 옳다는 보장은 없다. 최소 재현 코드로 가설을 검증하지 않으면 우연히 맞은 해법을 근본 원인으로 오해하기 쉽다.

셋째, 특정 하드웨어에서만 나타나는 버그는 추측보다 관측이 우선이다. 이 사례처럼 값을 하나씩 바꿔가며 문제 경계를 좁히고, 필요하면 컴파일된 기계어 수준까지 내려가 확인하는 과정이 결국 진실에 가까워지는 유일한 길이었다. GPU 아키텍처나 어셈블리에 대한 깊은 지식이 없어도, 두 컴파일 결과를 나란히 비교하는 것만으로 상당한 정보를 얻을 수 있다는 접근 자세 역시 참고할 만하다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://crocidb.com/post/when-the-fractional-part-of-a-float...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...