JDK Vector API로 추천 시스템 최적화
핵심 내용
넷플릭스는 배칭과 flat buffer, JDK Vector API로 추천 점수 계산의 CPU 사용량을 줄였다.
자세히 보기
Netflix의 Ranker는 홈 화면 개인화 row를 구동하는 대규모 서비스이고, 그중 video serendipity scoring이 노드당 CPU의 약 **7.5%**를 차지했다. 핵심 질문은 “새 타이틀이 지금까지 본 콘텐츠와 얼마나 다른가”였고, 기존 방식은 후보와 시청 이력 사이의 코사인 유사도를 M×N nested loop로 계산해 확장성이 좋지 않았다.
처음에는 후보와 이력 임베딩을 모아 행렬 곱으로 바꾸는 배칭을 도입했다. 요청의 약 **98%**는 단건이었지만 나머지 **2%**의 대형 배치가 전체 처리량의 절반가량을 차지해, 배칭 최적화가 충분히 의미가 있었다.
하지만 첫 구현은 성능을 오히려 약 5% 악화시켰다. 이유는 두 가지였다.
- 매 요청마다
double[][]를 새로 만들며 GC pressure가 커졌고, 2차원 배열의 비연속 메모리 구조 때문에 캐시 효율도 나빴다. - 자바 기반 행렬곱이 단순한 scalar 구현이라 SIMD를 활용하지 못했다.
다음 단계에서는 데이터를 flat double[] buffer로 바꾸고, ThreadLocal<BufferHolder>로 후보용·이력용 버퍼를 재사용했다. 버퍼는 필요할 때만 커지고 줄어들지 않도록 해 요청당 할당을 줄였고, row-major의 연속 메모리 덕분에 접근 패턴도 예측 가능해졌다.
행렬곱 엔진으로는 먼저 BLAS를 검토했지만, 실전 경로에서는 기대만큼 이득이 없었다. netlib-java의 F2J 경로, JNI 전환 비용, column-major와의 배치 차이, 그리고 임베딩 계산과 맞물린 추가 복사와 할당이 발목을 잡았다.
최종 해법은 JDK Vector API였다. 이 기능은 pure Java로 SIMD를 표현할 수 있어 native 의존성이나 JNI 없이 DoubleVector.SPECIES_PREFERRED로 호스트 CPU에 맞는 lane 폭을 선택하고, fma()로 dot product를 누적한다. 벡터 API가 없을 때는 scalar 경로로 떨어지되, 그마저도 루프 언롤링된 최적화 구현을 사용하도록 MatMulFactory로 분기했다.
결과적으로 이 작업은 단순한 알고리즘 변경이 아니라, 배칭 + flat buffer + ThreadLocal 재사용 + SIMD 커널을 함께 맞춘 최적화였다. 같은 serendipity score를 유지하면서도 요청당 CPU 비용을 낮춰 클러스터 풋프린트를 줄이는 방향으로 정리됐다.
이 한국어 요약은 AI가 자동으로 만들었습니다. 원문의 주장과 맥락은 원문에서 확인해 주세요. 저작권은 원저작자에게 있습니다.