갑자기 왜 floor와 ceil 이야기인가요?
그래픽 프로그래밍이나 수치 계산을 하다 보면 floor()(내림)와 ceil()(올림) 같은 기본적인 함수를 정말 자주 쓰게 되거든요. 픽셀 좌표 계산할 때도, 텍스처 샘플링할 때도, 셰이더 안에서도요. 그런데 이런 "당연히 정확하게 동작할 거라고 믿었던" 함수가 사실은 CPU와 GPU에서 미묘하게 다른 결과를 낸다는 사실을 알고 계셨나요? Adam Sawicki(AMD에서 그래픽 드라이버를 다루는 엔지니어로 유명한 분이에요)가 이번에 그 미묘한 차이를 파고들었는데, 읽다 보면 부동소수점이 얼마나 함정이 많은 녀석인지 새삼 느끼게 됩니다.
핵심 키워드는 denormal number(비정규화 수) 라는 녀석이에요. 이게 뭐냐면, 부동소수점이 표현할 수 있는 "아주 아주 작은 숫자" 영역인데요. 일반적인 float는 1.xxxx × 2^지수 형태로 표현하잖아요. 그런데 표현 가능한 가장 작은 지수보다 더 작은 숫자가 필요하면, 정상적인 형식을 깨고 0.xxxx × 2^최소지수 형태로 억지로 표현해요. 이게 denormal인데, 정확도는 떨어지지만 "0과 가장 작은 정규 수 사이의 빈 공간"을 메워주는 역할을 합니다.
CPU에서는 어떻게 동작하나요?
CPU에서 floor(x)를 호출하면, x가 denormal이든 정규 수이든 IEEE 754 표준대로 정확하게 동작해요. 예를 들어 아주 작은 양수 denormal 값을 floor()에 넣으면 결과는 0이 나오고, 음수 denormal이면 -1이 나오는 식이죠. 표준을 따르니까 예측 가능하고 안심할 수 있어요.
그런데 여기서 함정이 있어요. CPU에서도 SSE 명령어를 쓸 때 DAZ(Denormals Are Zero) 와 FTZ(Flush To Zero) 라는 플래그를 켤 수 있는데요. 이걸 켜면 "denormal이 들어오면 그냥 0으로 취급"하거나 "denormal 결과가 나오면 0으로 밀어버려" 동작이 됩니다. 게임 엔진이나 오디오 처리 같은 곳에서 성능 때문에 일부러 켜놓는 경우가 많아요. denormal 연산이 정규 수 연산보다 수십 배에서 수백 배 느려질 수 있거든요. 이 플래그를 켠 상태라면 floor(아주 작은 수)의 결과가 표준과 달라질 수 있어요.
GPU에서는 더 흥미로워집니다
GPU는 그래픽과 머신러닝을 위해 만들어진 하드웨어이다 보니, 성능 최적화를 위해 "표준을 약간 어기는 것"이 허용돼요. DirectX와 Vulkan 명세를 보면 floor와 ceil 같은 함수가 denormal 입력에 대해 어떻게 동작해야 하는지 명확하게 정의돼 있지 않은 부분이 있어요. 그래서 GPU 벤더마다, 심지어 같은 벤더의 다른 세대 GPU에서도 결과가 달라질 수 있습니다.
저자가 실제로 여러 GPU에서 테스트해봤는데, 어떤 GPU는 denormal을 입력받으면 그냥 0으로 처리해버리고, 어떤 GPU는 표준대로 처리해요. 즉, 같은 셰이더 코드를 NVIDIA, AMD, Intel GPU에서 돌렸을 때 미세하게 다른 결과가 나올 수 있다는 거죠. 평소엔 눈에 안 띌 정도로 작은 차이지만, 절차적 텍스처 생성이나 정밀한 시뮬레이션, 또는 머신러닝 추론처럼 작은 오차가 누적되는 작업에서는 문제가 될 수 있어요.
그래서 우리는 뭘 조심해야 하나요?
첫째, "내 코드는 모든 플랫폼에서 똑같이 동작할 거야"라는 가정을 버리세요. 특히 GPU 코드는 더더욱이요. 셰이더에서 floor(uv * resolution) 같은 표현을 자주 쓰는데, uv가 아주 작은 값일 때 GPU마다 다른 픽셀을 가리킬 수도 있어요.
둘째, denormal 영역에 절대 들어가지 않도록 입력값을 미리 정리하는 방어 코드를 짜두는 게 좋아요. 예를 들어 abs(x) < 1e-30이면 그냥 0으로 처리한다거나, 입력을 적절한 스케일로 정규화한다거나 하는 식이에요. 이렇게 하면 플랫폼 간 차이도 줄고 성능도 좋아져요.
셋째, 테스트를 여러 GPU에서 돌려보세요. 한 회사 GPU에서만 테스트하면 다른 GPU 사용자에게서 "이상한 시각 버그"가 보고될 수 있어요. 크로스 플랫폼 게임이나 그래픽 툴을 만든다면 더더욱 중요합니다.
한국 개발자에게 주는 시사점
게임이나 그래픽 엔진을 만드는 분, 그리고 최근에는 ML 추론을 GPU에서 돌리는 분들이 많아졌잖아요. 특히 모바일 GPU(Mali, Adreno 등)는 데스크톱 GPU보다 부동소수점 정확도가 더 낮은 경우가 많아서, 한국에서 모바일 게임을 개발하는 팀이라면 이런 디테일을 한 번쯤 점검해볼 가치가 있어요. "왜 아이폰에서는 잘 되는데 갤럭시에서는 미세하게 깜빡거리지?" 같은 버그의 원인이 이런 부동소수점 차이일 수 있거든요.
또 ML 추론에서도 양자화(quantization)된 모델을 다룰 때 작은 값이 denormal 영역으로 들어가면 디바이스마다 다른 결과가 나올 수 있어서, 모델 검증 단계에서 여러 하드웨어로 돌려보는 습관이 필요합니다.
마무리
부동소수점은 우리가 "숫자"라고 부르는 추상화 뒤에 숨겨진 수많은 약속과 타협의 결과물이에요. 그 약속이 플랫폼마다 미묘하게 다르다는 걸 알고 코드를 짜는 사람과 모르고 짜는 사람의 차이는 결국 디버깅 시간에서 드러나죠.
여러분은 GPU 코드를 짤 때 부동소수점의 미묘한 차이로 디버깅에 시간을 쏟아본 적이 있나요? 어떤 상황이었고 어떻게 해결하셨는지 댓글로 이야기해주세요.
🔗 출처: Hacker News
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공