기타 Lobsters · 4일 전

"GPU에 올렸더니 성능 1000배 떡락"... 개발자가 결국 꼼수로 때운 사연

핵심 요약
  • GPU 병렬 언어 Futhark에서 순차 알고리즘인 그래프 컬러링을 돌리자 GPU 메모리 접근 병목으로 순차 C 대비 1000배 이상 느려지는 문제가 발생했습니다.
  • 전체 최적화 컴파일러를 새로 짜는 정석 대신, 해당 함수만 CPU 메모리와 연산으로 강제하는 `#[cpu_function]` 꼼수 어트리뷰트를 추가해 해결했습니다.
요약 고성능 함수형 병렬 프로그래밍 언어인 푸사르크(Futhark) 개발 블로그에 GPU와 CPU의 메모리 격차를 다룬 기술적 일화가 공개되었습니다. 발단은 한 학부생(Elias Smedegaard)의 학사 논문 프로젝트였습니다. 희소 야코비 행렬(sparse Jacobian matrix)을 효율적으로 계산하는 과정에서 행렬 희소성 패턴에 맞는 그래프 컬러링 알고리즘이 필요했는데, 이 탐욕(greedy) distance-2 컬러링 알고리즘은 특성상 순차적(sequential)으로 실행되어야 했습니다. 전체 프로그램 구조는 1단계에서 순차 알고리즘으로 그래프를 칠하고, 2단계에서 그 결과를 바탕으로 대규모 병렬 계산을 GPU에서 수행하는 식이었습니다. 문제는 푸사르크의 단순한 컴파일러 모델이었습니다. GPU 파이프라인 백엔드를 선택하면 모든 배열이 자동으로 GPU 메모리에 할당됩니다. 순차 루프 안에서 GPU 배열 요소에 접근하게 되면, CPU가 매번 통신 오버헤드를 안고 단일 요소를 복사해와야 해 극단적인 지연(요소당 수 마이크로초)이 발생합니다. 그 결과 distance-2 컬러링 코드를 GPU 백엔드로 빌드했을 때 순차 C 백엔드 대비 성능이 무려 3자릿수(1,000배) 이상 폭락하는 사태가 벌어졌습니다. 그렇다고 C 백엔드로 돌리면 2단계의 대규모 병렬 계산까지 CPU에서 순차적으로 돌아가버리는 딜레마에 빠진 것입니다. 가장 이상적인 해결책은 컴파일러가 배열 사용 위치를 분석해 자동으로 CPU와 GPU 메모리에 최적으로 배치하는 것이지만, 이는 메모리 복사 및 유지 비용을 세밀하게 제어해야 해 당장 구현하기에는 작업 규모가 너무 컸습니다. 결국 개발자는 복잡한 정석 최적화 대신 임시방편(hack)으로 `#[cpu_function]`이라는 새 어트리뷰트를 도입했습니다. 이 어트리뷰트를 비인라인 함수에 지정하면, GPU 백엔드로 컴파일하더라도 해당 함수 본체와 입출력 배열을 CPU 코드 및 CPU 메모리에 강제로 할당하도록 만들어 병목을 우회했습니다.
Sponsored · 광고