TECH 으로 돌아가기
TECH HACKER NEWS 오늘 9분 읽기 27 READS

룽슨 CPU의 사라진 원자적 덧셈: 반년을 끈 하드웨어 버그 추적기

룽슨 CPU의 사라진 원자적 덧셈: 반년을 끈 하드웨어 버그 추적기
SOURCE IMAGE · HACKER NEWS

2026년 2월, loong13(데비안 13 stable의 LoongArch 포팅) 메인테이너인 왕먀오는 수학 소프트웨어 normaliz를 패키징하다 이상한 현상을 만났다. 내장 테스트가 무한 루프에 빠져 빌드가 타임아웃된 것이다. 코드를 따라가 보니 문제는 지극히 평범한 연산, 즉 OpenMP의 #pragma omp atomic으로 공유 변수를 누적하는 부분을 가리켰다. 루프의 종료 조건은 누적값이 특정 수치와 같아지는 것이었는데, 누적 결과가 늘 그 수치보다 작아 루프를 빠져나오지 못했다. 프로그램이 크고 복잡해 사람이 이해할 수 있는 최소 재현 코드로 줄이지 못한 채, 이 문제는 일단 보류됐다.

반년이 지난 8월, 팀은 접근 방식을 바꿨다. 사람이 직접 원인을 짚는 대신, 사람이 방향을 지시하고 AI가 최소 재현 사례를 찾도록 한 것이다. 약 이틀 만에 안정적인 재현기가 나왔고, 그제서야 근본 원인이 드러났다. CPU의 원자적 덧셈 명령이 특정 조건에서 원자성을 잃는다는 것, 즉 새로운 CPU 에라타(erratum)였다. 룽슨은 이 사실을 접한 뒤 2주 만에 성능 손실이 거의 없는 수정안을 찾아 테스트 펌웨어를 제공했고, 팀은 이 펌웨어가 문제를 해결함을 확인했다. 룽슨은 정식 펌웨어를 국경절(10월 1일) 전에 배포할 예정이라고 밝혔다.

카운터가 어긋난다

normaliz는 격자점(LatticePoint) 목록을 OpenMP로 병렬 처리하며, 일부 점은 임시로 건너뛰고 여러 번 반복해 모든 점을 처리한다. 전체 점 수(nr_to_match)와 처리 완료 수(nr_points_matched)가 같아지면 루프가 끝나야 하지만, 후자가 결코 전자에 도달하지 못했다. gdb로 보면 모든 점이 이미 처리 완료로 표시됐는데도 카운터 값이 실제 처리 수와 어긋났다. 매 라운드마다 함께 원자적으로 증가하도록 짜인 두 카운터의 값이 미묘하게, 그리고 실행할 때마다 다르게 벌어졌다.

팀은 먼저 메모리 순서(memory ordering) 문제를 배제했다. 이 코드는 원자 변수로 다른 변수를 동기화하지 않고 원자 변수 자체만 읽고 쓰기 때문에 논리적으로는 옳았다. 다음으로 의심한 것은 OpenMP의 원자 연산 구현이었다. 디스어셈블 결과 컴파일러는 예상대로 LoongArch64의 amadd.d 명령을 생성하고 있었다. 검증을 위해 std::atomic 카운터 두 개를 대조군으로 추가해 네 개의 카운터를 함께 돌렸는데, 이론상 일치해야 할 네 값이 무작위로 어긋났다. 원자적 덧셈 명령이 특정 조건에서 갱신을 잃어버린다는 정황이었다. 다만 단순한 원자 덧셈 프로그램으로는 이 손실이 재현되지 않았고, 계산 단계를 대부분 주석 처리해도 문제가 사라지지 않는 기이한 현상 탓에 2월에는 최소 재현 코드를 만들지 못했다.

memcpy와 벡터 명령이라는 열쇠

8월 재조사에서 AI는 처음에 문제 루프는 인지했지만 원자 덧셈을 원인으로 지목하지 못했다. 문제가 LoongArch에서만 나타난다는 힌트를 줘도 마찬가지였다. 결국 원인을 원자 덧셈으로 이미 좁혔다는 사실을 알려주고 최소 재현기를 요구하자, AI는 처리 함수 안의 memcpy 호출에 주목했다. glibc의 memcpy는 하드웨어 기능에 따라 최적 구현을 고르는데, LoongArch에서 벡터 명령셋(LSX/LASX)을 지원하면 그 벡터 명령으로 메모리 복사를 가속한다. 바로 이 벡터화된 메모리 복사가 방아쇠였던 것이다.

이후 조사에서 영향 범위가 넓어졌다. CAS(amcas) 역시 같은 조건에서 갱신을 잃었고, swap·max·min·AND·OR 같은 다른 원자 명령들도 연산 결과를 일일이 기록해 검증하는 방식으로 확인한 결과 모두 갱신 손실이 나타났다. 예컨대 병렬로 1부터 n까지 원자적 max를 취할 때, 최댓값을 갱신한 연산들의 반환값은 원자성 덕분에 중복될 수 없어야 하는데, 중복이 관측되면 곧 손실이 일어났다는 뜻이다. 트리거 조건도 복잡했다. 처음에는 LASX 벡터 읽기(xvld)만이 문제를 유발하고 스칼라 읽기나 LSX 읽기(vld)는 유발하지 않는 것으로 보였으나, 룽 '맨틀' 바오가 원자 변수 주소와 읽기 주소가 특정 위치 관계에 있으면 평범한 스칼라 읽기로도, 다만 더 낮은 확률로 유발됨을 밝혔다. 이 까다로운 조건들이 버그가 오래 숨어 있던 이유다.

영향받는 것은 LA664 코어를 쓰는 3C6000/S와 3A6000이며, 그 이전의 LA464 코어(3A5000 등)에는 이 문제가 없다. LASX는 256비트, LSX는 128비트 SIMD 확장이고, xvld는 한 번에 32바이트를 벡터 레지스터로 읽는다. 최소 재현기는 점 하나가 2208바이트(uint64_t 276개)인 normaliz 데이터를 두 스레드가 나눠 순회하며, 각 점마다 벡터화 memcpy 후 세 개의 공유 카운터에 데이터 배리어 없는 relaxed 원자 덧셈을 한 뒤 세 값이 같은지 확인하는 구조다. 3C6000/S에서 서로 다른 물리 코어(CPU0, CPU2)로 200라운드·30회 시행한 결과, 두 스레드 모두 LASX 읽기를 할 때 실패율이 거의 100%였고, 반대로 데이터 배리어를 갖춘 amcas_db.d는 같은 조건에서 언제나 0%였다.

배포판마다 갈린 재현 여부

가장 흥미로운 대목은 2월과 8월 사이 배포판별 재현 결과가 달라졌다는 점이다. 2월엔 AOSC OS와 데비안 모두 무한 루프를 재현했지만, 8월엔 AOSC에서 아무리 해도 재현되지 않고 데비안에서만 재현됐다. 원인은 memcpy에 있었다. AOSC가 2월 이후 배포한 Core 13 릴리스가 glibc에서 --enable-multi-arch 옵션을 실수로 비활성화해 시스템 memcpy가 LASX 가속 경로를 타지 않게 된 것이다. 반면 데비안 glibc는 벡터 가속을 정상적으로 켜 memcpy가 LASX를 사용했다. 실제로 데비안에서도 GLIBC_TUNABLES=glibc.cpu.hwcaps=-LASX로 LASX 가속을 끄면 문제가 멈췄다.

실무적으로 이 사례가 주는 시사점은 분명하다. 약한 메모리 모델을 쓰는 아키텍처에서 원자 연산이 어긋날 때, 소프트웨어의 경쟁 상태나 메모리 순서를 먼저 의심하는 것이 자연스럽지만 실제 원인이 하드웨어 명령 자체일 수 있다는 것이다. 또한 이 버그는 원자 변수와 무관해 보이는 벡터화 memcpy의 존재, 두 스레드의 접근 패턴, 특정 주소 정렬이라는 세 조건이 겹칠 때만 드러났다. 이는 라이브러리 최적화 경로(glibc의 하드웨어 기능 감지)가 상위 애플리케이션의 정확성에까지 영향을 미칠 수 있음을 보여준다. 해당 CPU를 운용 중이라면 정식 펌웨어가 배포되는 대로 업데이트하는 것이 정공법이며, 그 전까지는 LASX 가속 비활성화나 데이터 배리어가 있는 원자 명령이 임시 완화책이 될 수 있다. 다만 이런 우회책은 성능이나 호환성 대가를 동반할 수 있어 펌웨어 수정을 대체하지는 못한다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://jia.je/hardware/2026/09/24/loongson-cpu-erratum-en/
SHARE
NEXT · CHOOSE

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

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

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