Bad benchmarks and evals: Senior SWE-Bench, napkin math, and winter tires

Dan Luu

엉터리 벤치마크와 평가: Senior SWE-Bench, 냅킨 계산법, 그리고 윈터 타이어

원문은 Dan Luu님이 에 게재했습니다. 이 블로그 구독하기

세 가지 종류의 벤치마크를 살펴볼 것이다. 성능에 대한 ‘냅킨 계산법’ 추정을 위한 기준 수치 계산 한 세트, AI 모델 평가 한 세트, 그리고 자동차 타이어에 대한 것이다. 나는 무언가에 대한 직관을 쌓을 때 설명을 보기 전에 먼저 스스로 생각해 보는 걸 좋아한다. 그래서 이 글에서는 벤치마크 정보를 먼저 제시하고 설명은 뒤에 두었다. 내 생각을 보기 전에 직접 답을 고민해 보고 싶다면 그렇게 해 보길 바란다.

29. 내 친구 한 명이 컴퓨터 성능 인터뷰를 준비하며 성능의 자릿수(order of magnitude)를 복습하다가 https://github.com/sirupsen/napkin-math (스타 5.4k)가 최상위 검색 결과로 뜨는 것을 발견했다. README의 표에는 다음과 같은 내용이 포함되어 있다:

냅킨 계산법 성능 추정치
연산지연 시간처리량1 MiB1 GiB
순차 메모리 읽기/쓰기 (64 bytes)0.5 ns
├ 싱글 스레드20 GiB/s50 μs50 ms
├ 멀티 스레드200 GiB/s5 μs5 ms
동일 존 네트워크10 GiB/s100 μs100 ms
├ VPC 내부10 GiB/s100 μs100 ms
├ VPC 외부3 GiB/s300 μs300 ms
해싱, 암호학적 안전성 없음 (64 bytes)10 ns5 GiB/s200 μs200 ms
랜덤 메모리 읽기/쓰기 (64 bytes)20 ns3 GiB/s300 μs300 ms
고속 직렬화 [8] [9]N/A1 GiB/s1 ms1s
고속 역직렬화 [8] [9]N/A1 GiB/s1 ms1s
시스템 콜300 nsN/AN/AN/A
해싱, 암호학적 안전성 있음 (64 bytes)100 ns1 GiB/s1 ms1s
순차 SSD 읽기 (8 KiB)1 μs8 GiB/s100 μs100 ms
컨텍스트 스위치 [1] [2]10 μsN/AN/AN/A
순차 SSD 쓰기, -fsync (8KiB)2 μs3 GiB/s300 μs300 ms
TCP 에코 서버 (32 KiB)50 μs500 MiB/s2 ms2s
랜덤 SSD 읽기 (8 KiB)100 μs70 MiB/s15 ms15s
압축 해제 [11]N/A1 GiB/s1 ms1s
압축 [11]N/A500 MiB/s2 ms2s
정렬 (64비트 정수)N/A500 MiB/s2 ms2s
프록시: Envoy/ProxySQL/Nginx/HAProxy50 μs???
동일 리전 내 네트워크250 μs2 GiB/s500 μs500 ms
프리미엄 네트워크 (존/VPC 내)250 μs25 GiB/s50 μs40 ms
순차 SSD 쓰기, +fsync (8KiB)300 μs30 MiB/s30 ms30s
{MySQL, Memcached, Redis, ..} 쿼리500 μs???
직렬화 [8] [9]N/A100 MiB/s10 ms10s
역직렬화 [8] [9]N/A100 MiB/s10 ms10s
순차 HDD 읽기 (8 KiB)10 ms250 MiB/s2 ms2s
랜덤 HDD 읽기 (8 KiB)10 ms0.7 MiB/s2 s30m
Blob Storage GET, if-not-match 30430 ms
Blob Storage GET, 1 연결 (128KiB)80 ms100 MiB/s10 ms10s
Blob Storage GET, n 연결 (offsets)80 msNW limit
Blob Storage LIST100 ms
Blob Storage PUT, 1 연결 (128KiB)200 ms100 MiB/s10 ms10s
Blob Storage PUT, n 연결 (multipart)200 msNW limit10 ms10s
리전 간 네트워크 [6]Varies25 MiB/s40 ms40s
네트워크 NA Central <-> East25 ms25 MiB/s40 ms40s
네트워크 NA Central <-> West40 ms25 MiB/s40 ms40s
네트워크 NA East <-> West60 ms25 MiB/s40 ms40s
네트워크 EU West <-> NA East80 ms25 MiB/s40 ms40s
네트워크 EU West <-> NA Central100 ms25 MiB/s40 ms40s
네트워크 NA West <-> Singapore180 ms25 MiB/s40 ms40s
네트워크 EU West <-> Singapore160 ms25 MiB/s40 ms40s

이 벤치마크의 문제점은 무엇일까?

30. DeepSWE와 Senior SWE-Bench를 인용해 자신이 좋아하는 모델이 다른 사람이 좋아하는 모델보다 낫다거나, 일반적으로 좋은 벤치마크라는 것을 ‘증명’하려는 사람들을 계속 보게 된다. 예를 들면 다음과 같은 경우다.

다양한 모델과 노력 수준에 대해 점수와 작업당 평균 비용을 표시한 DeepSWE 리더보드Claude Fable 5, Claude Opus 4.8, GPT-5.6 Sol이 상위 3개 모델로 나타난 Senior SWE-Bench 리더보드

이 벤치마크들의 문제점은 무엇일까?

31. 사람들은 추운 날씨에 윈터 타이어가 사계절 타이어보다 우수하다고 자주 말한다. 예를 들어 “all season tires during winter cold”(따옴표 없이)를 구글에 검색하면, Google AI 요약은 다음과 같은 문장으로 시작한다.

사계절 타이어는 영하의 겨울 기온에서 접지력을 잃고 굳어진다. 타이어의 고무 컴파운드는 따뜻한 날씨용으로 설계되어 7°C(45°F) 이하에서 단단해져 제동 거리가 크게 늘어나고 접지력이 떨어진다 … 사계절 타이어의 고무는 영하의 온도에서 유연성을 유지할 수 없어 눈과 얼음 위에서는 단단한 플라스틱처럼 작동한다.

학습 데이터에 인터넷 댓글이 많이 들어 있다는 점을 감안하면, 이는 그럴듯한 답변이다. 실제로 어떤 타이어를 써야 하는지에 대한 토론에서 이와 비슷한 말을 자주 보기 때문이다.

이 벤치마크의 문제점은 무엇일까?

29. 냅킨 계산법 수치

랜덤 메모리 접근 지연 시간

내 친구(Jamie)가 보기에 즉시 이상하게 느껴진 것은 랜덤 메모리 읽기/쓰기가 20ns로 표기된 부분이었다. 랜덤 메모리 읽기/쓰기는 (캐시 히트가 아닌) 실제 DRAM 읽기인 것으로 암시되는데, 그는 자릿수 추정으로는 100ns 정도가 되어야 한다고 생각했다.

이에 대해 이야기를 나누던 중, 그는 README가 실제로는 지연 시간이 아닌 것들에 대해서도 ‘지연 시간’이라는 용어를 쓰고 있다고 지적했다. 그러다 랜덤 메모리 읽기 지연 시간에 대한 코드를 열어본 그는 다음과 같은 내용을 발견했다 (또 다른 연습 문제로 삼고 싶다면, 아래 설명을 읽기 전에 이 코드의 문제점이 무엇인지 생각해 보라):

  while test.i < test.vec.len() {                                                       
      let random_index = test.order[test.i];                                            
      black_box(test.vec[random_index]);                                                                                                                                         
      test.i += 1;                                                                                                                                                               
  }     

Jamie는 루프 반복 사이에 데이터 의존성이 없으므로 여기서 메모리 읽기가 병렬로 일어난다고 지적했다. 주장된 지연 시간 수치는 접근당 평균 시간으로 계산되는데, CPU가 동시에 여러 로드를 진행할 수 있기 때문에 이는 잘못된 방식이다. 이런 식으로 지연 시간을 측정하려면 로드 사이에 의존성을 넣어 겹치는 접근을 막아야 한다(관련 주제는 이 시리즈 4부에서 다룬 연습 문제 19에서 논의한 바 있다).

랜덤 SSD 읽기

나는 Jamie의 의견에 모두 동의한다. 다만 나는 ‘지연 시간’이라는 용어 사용을 딱히 지적하지는 않았는데, 어쩌면 어떤 경우에는 지연 시간을 줄여 쓴 것이고 다른 경우에는 (역처리량 같은) 지연 시간과 비슷한 무언가를 뜻하는 것일 수도 있어 표를 더 단순하게 만들려는 의도일 수 있기 때문이다.

메모리 지연 시간 수치 외에도 내 눈에 가장 먼저 이상하게 들어온 것은 다른 몇몇 수치였다. 예를 들어 랜덤 SSD 읽기는 100us / 70MB/s로 표기되어 있다. SSD는 이보다 훨씬 빠른 것도, 훨씬 느린 것도 있다. 예를 들어 빠르고 (특수한, 예컨대 Optane 같은 것이 아닌) 일반적인 장치라면 40us 미만의 지연 시간을 볼 수도 있다. 예를 들어 Kioxia CD9P-R은 여기서 ~30us로 측정되었다. 사소한 스크립트 외에는 디스크 성능이 중요한 작업을 해본 적이 없어서 어떤 수치를 염두에 두어야 할지에 대한 직관이 없다1. 하지만 랜덤 읽기 지연 시간과 처리량에 대해 단일 수치를 갖는 것이 DRAM 접근에 대한 것보다 덜 유용하지 않을까 싶기도 하다. 내가 디스크 벤치마크를 볼 때마다 읽기 크기, 큐 깊이, 작업 수에 따라 결과가 엄청나게 넓게 퍼져 있는 것 같다(예: Kioxia CD9P-R에 대한 이전 링크 참조). 물론 DRAM 지연 시간과 대역폭에 영향을 주는 유사한 요인들도 있지만, 메모리 접근을 생각할 때는 한두 개 수치만 알아도 도움이 되는 영역에 훨씬 더 자주 있게 되는 것 같다. 나는 디스크 성능에 대해 아는 것이 전무하므로 Postgres 디스크 성능 작업을 한 Peter Geoghegan에게 물어봤고, 그는 동의했으며 디스크 성능의 복잡성에 대해 추가적인 의견을 아래에 남겨 주었다.

이 SSD 랜덤 읽기 수치에 대한 코드를 보면, Jamie가 랜덤 메모리 읽기 코드에서 느낀 것과 같은 위화감이 든다. 코드는 오프셋을 다음과 같이 생성한다.

for i in 0..(buffer.len() / page_size) {
    pages.push((i * page_size + 1) as u64);
}

그리고 나서 8KiB 읽기(오프셋은 셔플되어 랜덤 읽기를 만든다)를 수행한다. 이상하게 느껴지는 몇 가지 점은 다음과 같다:

  1. +1 때문에 모든 읽기가 비정렬(unaligned)이 된다. 페이지 크기가 4KiB라면, 한 번의 읽기가 3개 페이지에 걸치게 된다
  2. 서로 다른 오프셋이 같은 페이지를 겹쳐서 읽을 수 있어, 의도치 않게 페이지 캐시에서 읽어오는 경우가 생긴다
  3. 페이지 크기에 따라서는 읽기가 파일 끝을 넘어 패닉을 일으킬 수도 있다

“buffer.len() / page_size” 구조는 접근이 범위 내에 머물도록 하려는 의도로 보이지만, 이는 접근 길이와는 무관하다. 정확하게 고민하기 귀찮다고 치고 4GiB 같은 엄청나게 큰 접근 길이를 생각해 보자(버퍼 크기는 8GiB다). 그러면 분명히 오버플로가 난다. 더 정확하게 하자면 오버플로 경우는 4KiB 페이지에 8KiB 접근 길이를 쓰는 경우와 비슷하겠지만, 아이디어는 마찬가지다.

가장 마지막 오프셋은 SIZE - 4096 + 1이 된다. 이렇게 하면 접근 가능한 바이트는 4095바이트뿐인데, 우리는 8192바이트를 접근하려 한다. 벤치마크가 5초 동안만 실행되기 때문에 실제로 EOF를 넘어 읽으려다 실패할 수도 있고 아닐 수도 있지만, 주어진 실행에서 무작위로 실패하든 안 하든 어쨌든 여기에 버그는 존재한다.

순차 SSD 읽기

코드만 봐도 많은 부분이 영 석연치 않다. 예를 들어 순차 8KiB SSD 읽기를 생성하는 데 쓰인 코드를 보자. 이 수치는 지연 시간 1us, 처리량 8GiB/s로 되어 있다. 말했듯이 나는 디스크 성능이 중요한 문제를 다뤄본 적이 없어서 이런 수치가 그럴듯한지 아닌지에 대한 직관이 없지만, 코드는 어딘가 이상하게 느껴진다. 코드는 1GiB 파일을 만들고 이를 플러시한 뒤 반복해서 다시 읽는다. 그러면 캐시되지 않은 읽기 한 번 뒤에 캐시된 읽기들이 뒤따르게 된다. 여기서는 캐시되지 않은 읽기를 측정하려는 의도로 보이지만, 캐시된 읽기를 측정하려는 의도였다 해도 코드는 그것을 하고 있지 않다(이 문제는 fsync된 읽기 3GiB/s 같은 다른 수치들에서도 나타나는 것으로 보인다). 캐시되지 않은 읽기 한 번 뒤에 캐시된 읽기가 뒤따르는 것이 현실적이라고 주장할 수도 있지만, 정확히 동일한 워크로드가 아닌 사람이 1번의 캐시되지 않은 읽기와 N번의 캐시된 읽기를 합친 집계 수치를 가지고 무엇을 해야 할지 명확하지 않다. N이 눈에 띄게 명시되어 있지도 않으니 자신이 같은 워크로드를 가졌는지조차 알 수 없다.

여기서 수치가 어떠해야 할지 전혀 감이 없으니, 수치를 찾아보도록 하자. 측정은 c4-standard-48-lssd에서 수행되었다고 한다. 해당 인스턴스에 대한 Google 문서에 따르면 연결된 8개 디스크 전체의 최대 처리량은 5000MiB/s(Google 표에 따르면 이는 연결된 디스크 수에 따라 비례하며 디스크당 625MiB/s)라고 한다. 디스크 벤치마크에 대해 내가 아는 아주 적은 지식으로 보면, 피크 처리량 수치는 일반적으로 더 큰 읽기를 사용할 때 측정되므로 8GiB/s는 과도해 보이고, 코드에서 뭔가 이상하다는 느낌이 맞는 것 같다. 그리고 다른 수치들을 봐도, 특정 읽기 크기에 대한 몇 개의 단일 수치가 디스크 성능 전반을 대표하지 못한다는 더 넓은 요점이 드러난다.

대표성

하지만 이런 종류의 ‘냅킨 계산법’ 뒤에 있는 아이디어는 일반적으로 특정 클라우드 인스턴스 하나가 정확히 어떻게 동작하는지를 아는 것이 아니다. 여러 방식으로 성능을 추정하는 데 쓸 수 있는 기본적인 수치 몇 개를 얻는 것이다. Kioxia CD9P 벤치마크로 돌아가 보면, 다양한 파라미터에서 그보다 더 높은 대역폭을 보이는 읽기 벤치마크가 많이 있고(물론 더 낮은 대역폭을 보이는 경우도 많이 있다). 지연 시간의 경우, 지연 시간을 최소화하는 설정에서도 순차 읽기조차 지연 시간이 더 높다(벤치마크 내 다른 디스크들도 마찬가지다). 이는 sirupsen 벤치마크가 의도치 않게 캐시에서 읽고 있다는 또 다른 신호지만, 수치가 정확하다고 해도 그 수치로 무엇을 해야 할지 명확하지 않다.

sirupsen 코드에는 캐싱과 프리페칭을 방지하려는 시도가 있는 것으로 보인다. Linux에서 실행 중임을 감지하면 조언성(advisory) POSIX_FADV_RANDOM을 설정하고, 테스트 시작 전에 조언성 POSIX_FADV_DONTNEED를 설정한다. 하지만 이 중 어느 것도 이 벤치마크에서 OS 수준의 캐싱을 막지 못하며, SSD 내부 같은 더 낮은 수준의 캐싱을 막을 것으로 기대해서도 안 된다. Mac에서는 벤치마크가 사전에 Command::new("sudo").arg("purge").output().expect("failed to flush page cache")를 호출하지만, POSIX_FADV_RANDOM에 상응하는 것이 없고, 다른 OS(예: BSD나 Windows)에서는 아무 것도 하는 것으로 보이지 않는다.

코드의 다른 부분에도 여러 문제가 있지만, 모든 구체적인 문제와 제시된 대부분의 수치를 일일이 파고드는 대신, 서로 다른 영역에서 여러 수치의 범위를 파악하고 싶어 하는 경우가 있다는 아이디어로 돌아가 보자. 그런 경우가 꽤 많다. 다른 예를 들면, README는 “압축 해제”를 1GiB/s, “압축”을 500MiB/s로 인용한다. 물론 어떤 냅킨 계산법도 정확할 수는 없지만, 서로 다른 zstd 압축 옵션을 이것저것 만져만 봐도 압축 속도에서 두 자릿수 이상의 차이가 나고, 고속 압축에 더 특화된 알고리즘이 있어 그 범위는 더욱 커진다(물론 더 많은 노력을 들여 더 느리지만 더 높은 압축률을 얻을 수도 있다).

디스크 예제로 돌아가면, 디스크 수치는 8개 디스크가 달린 VM 구성에서 나왔다고 언급했다. 수치는 틀린 것으로 보이지만, 수치가 정확하다고 해도 단일 디스크 버전의 VM을 썼다면 읽기 대역폭 같은 수치는 당연히 달라질 것이다. 페이지 캐시에서 읽는 것이 아니라면 읽기 벤치마크의 읽기 대역폭은 대략 1/8 수준이 될 것으로 예상된다. GCP의 특정 8디스크 구성 하나에 대한 읽기 대역폭 수치를 냅킨 계산법 수치로 외워 두는 것이 왜 특별히 유용한지 명확하지 않다.

무엇을 배우는 게 유용할까?

전반적으로 나는 이런 종류의 수치를 아는 것이 유용하다고 생각하지만, 굳이 표를 찾아서 수치를 보고 싶지는 않다(인터뷰에서 물어볼 것 같아 외워야 한다는 합리적인 믿음이 있다면 인터뷰 준비용으로 잠깐 보는 경우는 예외로, 어차피 인터뷰 직후 바로 잊어버릴 것이다). 일반적으로 이런 수치를 아는 것이 말이 되는 일을 하고 있다면, 그냥 쓰다 보면 자연스럽게 익히게 된다. 예를 들어 나는 표준 단일 모드 광섬유에서의 분산이 17ps / nm * km라는 것을 아직도 기억하는데, 20년 전에 광학/포토닉스 작업을 했기 때문이다. 이는 대략적인 계산에서 자주 등장하므로 충분히 쓰다 보면 언젠가 그냥 기억하게 된다. 마찬가지로 2의 거듭제곱(예: 2^8 = 256, 2^16 = 65536 등)도 외우려고 한 적은 없지만, 이런 숫자를 다루는 코딩을 충분히 하다 보니 자주 쓰이는 숫자는 결국 기억하게 되었다.

링크된 냅킨 계산법 레포는 “숫자는 암기를 위해 반올림되었다”고 적고 있어, 이를 외우는 것이 타당하다는 것을 암시한다. 위에서 언급한 것 외에도, 이런 수치 중 많은 것은 유도해 낼 수 있으며, 내 생각에는 업무에 활용한다면 대략적인 수치를 외우고 있더라도 그 유도 과정을 이해하는 것이 종종 타당하다. 예를 들어 우리는 4부에서 Sandy Bridge 프로세서의 싱글 코어 메모리 대역폭 수치를 몇 가지 기본 파라미터로부터 유도했다. 코드가 얼마나 빨리 실행될지 알고 싶을 뿐이라면 일반적으로 모든 것을第一 원리부터 다시 유도할 필요는 없다. 하지만 무언가를 바꿨을 때의 함의를 이해하려 한다면, 어떤 메커니즘이 작용하고 어떻게 상호작용하는지를 아는 것이 도움이 되며, 이는 몇 개의 수치를 외운다고 해서 얻을 수 있는 것이 아니다.

이 질문의 맥락으로 돌아가면, 인터뷰를 준비하던 내 친구가 마지막으로 우려한 점은 이 레포가 매우 인기가 많아서, 면접관이 나열된 수치 대부분이 틀렸다는 사실을 모른 채 이 자료를 사용할 수도 있다는 것이었다.

보너스 정보: 시간에 따른 메모리 지연 시간

참고로, 실제 시스템에서 메모리 지연 시간이 실제로 어떻게 관측되는지 궁금해서 instlatx64 사이트의 데이터를 그래프로 그려 보았는데, 결과는 다음과 같다:

여기 그래프가 두 개인 이유는 서로 비교할 수 없는 두 가지 방법론이 사용되었기 때문이다. 원래 방법론은 메모리 지연 시간을 찾기 위해 1024바이트 스트라이드로 접근했는데, 이는 구형 프로세서에서 충분히 큰 데이터셋에 대해선 잘 동작했다. 신형 프로세서는 이를 순수 DRAM 접근이 아니게 만들 수 있는 메커니즘을 추가했기 때문에, 더 새로운 방법론은 메모리 지연 시간을 찾기 위해 랜덤 접근을 사용한다(구 방법론을 사용한 후기 수치 중 일부는 랜덤 메모리 접근 시간으로 생각한다면 실제로 유효하지 않다). 지연 시간은 instlatx64 사이트에서 가져온 것이고, CPU 출시 연도는 결과를 검증하지 않고 codex에서 GPT-5.6 Sol ultra에게 물어 찾아낸 것이므로 연도 중 일부는 틀렸을 가능성이 있다.

그래프를 눈대중으로만 봐도, 메모리 지연 시간은 한동안 엄청나게 개선되다가 결국 정체되었고, 이 글의 범위를 벗어나는 이유들로 인해 오히려 시간이 지날수록 관측된 지연 시간이 높아지는 것을 볼 수 있다.

690ns?

90년대 결과 중에는 극단적인 아웃라이어도 보인다. 이 결과들을 자세히 들여다보지 않고서는 결과가 틀렸다고 단정하기는 어렵다. Intel 쪽에서 가장 큰 아웃라이어는 83MHz Intel Pentium Overdrive다. 다른 오래된 Intel 결과는 모두 Overdrive가 아닌 Pentium이다.

Overdrive Pentium은 이전 세대 CPU용 메인보드에 끼워 넣을 수 있는 칩이었다. 테스트는 ALi M1489/M1487 칩셋을 33MHz 버스 속도로 설정한 Gigabyte GA-5486AL 메인보드에서 실행된 것으로 보인다. ALi M1489/M1487 데이터시트에 따르면 DRAM 읽기 타이밍은 네 가지가 가능하다. “normal” 설정으로 구성되어 있다면, 읽기 페이지 미스는 CP+84-4-4 읽기 타이밍을 갖는다. 나는 디스크 성능만큼이나 486 버스 타이밍에도 익숙하지 않아서 LLM에게 물어봤고, LLM은 이것이 맞으며 여기서 메모리 접근에 21, 22 또는 23 사이클을 예상해야 한다고 했다. LLM에게 이 숫자들이 도대체 무슨 의미인지 물어보니 첫 번째 워드 뒤에 추가 워드마다의 사이클이라는 점에서 이는 뭔가 맞지 않게 느껴진다. 따라서 그 사이클 수는 전체 캐시 라인 필(cache line fill)에 대한 것이다. 그러면 워드 접근에 대한 실제 load-to-use 지연 시간은 앞부분인 11 버스 사이클이 되어야 하지만, 전체 캐시 라인에 대한 시간을 원한다면 그 수치는 벤치마크로서 그럴듯하게 맞는 범위에 있는 것 같다.

아웃라이어인 AMD K5 PR166 결과를 보면 뭔가 조금 이상한 점이 있지만, 현대 컴퓨터 성능에 대한 질문으로서는 이미 너무 깊이 들어간 것이므로, 이건 시리즈 후반부의 또 다른 질문으로 남겨두자.

30. DeepSWE / Senior SWE-Bench

전반적인 그럴듯함

이 벤치마크들의 방법론을 보기 전에 결과만 놓고 보면, DeepSWE나 Senior SWE-Bench 모두 코딩 에이전트가 전반적으로 얼마나 잘 동작하는지에 대한 요약으로 그럴듯하게 느껴지지 않는다. DeepSWE 홈페이지를 표면적으로 읽으면 OpenAI의 이전 세대 모델(GPT-5.5)이 Anthropic의 현 세대 모델(Fable 5)만큼 좋다는 것이고, Senior SWE-Bench를 표면적으로 읽으면 Anthropic의 이전 세대 모델(Opus 4.8)이 OpenAI의 현 세대 모델(GPT 5.6)보다 낫다는 것이다. 일반적으로 표면적인 읽기가 대부분의 사람들이 받아들이는 것이며, 이는 내가 이 벤치마크들이 사용되는 것을 일반적으로 보는 방식이기도 하다(예: 회사 슬랙에서, 사람들이 직접 내게 보내올 때 등).

방법론적 문제

어떻게 만들어졌는지를 들여다보면, 공개된 벤치마크 중 코딩 에이전트가 얼마나 좋은지에 대한 전반적인 인상을 얻기 위해 신뢰할 만한 것은 거의 없어 보인다. 방법론 측면에서, 벤치마크는 일반화 가능한 결과를 얻기 위해 측정해야 할 것과 관련해 그다지 말이 되지 않는다. 디스크 성능에 대해 아는 것이 없는 것처럼, 나는 AI에 대해서도 아는 것이 없으므로 한동안 Anthropic에서 평가(evals) 팀을 이끌었던 사람(Aaron Levin)에게 추론과 결론을 검토해 달라고 부탁했고, 그는 전반적인 아이디어와 추론에 동의했다. 디스크 성능 전문가와의 상담에서처럼, 여기서 요점은 전문가가 동의하니까 당신도 동의해야 한다는 것이 아니다. 이런 경우들에서는 해당 분야에 대한 전문 지식이 없어도 전문가가 도달할 결론과 같은 결론에 도달할 수 있다는 것이다. 벤치마킹이나 실험 설계 문제를 평가할 때 쓸 법한 일반적인 추론을 적용하기만 하면 된다.

요약 점수의 대표성

지난 글에서 우리는 단일 요약 점수가 사실상 어떤 말이든 할 수 있다는 큰 아이디어를 논의했다. 서브 벤치마크 결과를 보면 모델 X가 모델 Y보다 유리한 경우가 충분히 많이 나오고, 일반적으로 밖에 있는 작업 분포를 샘플링해 벤치마크 A가 더 대표성이 있기 때문에 벤치마크 B보다 낫다고 말할 특별히 좋은 방법이 없기 때문이다.

이 벤치마크들의 세부 내용을 더 들여다보면, DeepSWE의 경우 113개 작업(어쨌든 codex가 그렇게 알려줬다)이 있고 각 작업은 네 번씩 실행되며, 대체로 합격/불합격 점수로 보인다(모델들은 각 작업에서 0%, 25%, 50%, 75% 또는 100%를 받는 것으로 보인다). 그래프에서 GPT-5.5가 Opus 4.8보다 훨씬 낫다는 것을 볼 수 있다. GPT-5.5와 Opus 4.8 간의 차이는 Opus 4.8과 Gemini-3.5 Flash 간의 차이만큼이나 크다. 위에서 언급했듯이, 이 모델들을 써 봤다면 이는 전반적으로 나나 내가 신뢰하는 사람들의 경험과 그다지 맞지 않는다(물론 특정 작업이나 서브 벤치마크에서는 이것이 사실인 경우도 있다).

왜 그렇다고 주장되는지 살펴보면, GPT-5.5 xhigh가 Opus 4.8 xhigh보다 약간 저렴하면서 훨씬 낫다고 한다(67% 대 54%). 113개 작업 중 모델이 동점인 경우는 34개, GPT-5.5 xhigh가 이긴 경우는 57개, Opus 4.8이 이긴 경우는 22개다. 나나 다른 프로그래머에게는 이 작업들이 내가 하거나 다른 프로그래머가 하는 작업을 대표한다면 의미가 있을 수 있다. 113개 작업(혹은 다른 결과가 나온 79개 작업만이라도)은 이 글에서 자세히 살펴보기에는 너무 많지만, 작업 이름들을 보면 내 작업과 관련된 것은 거의 또는 전혀 없어 보인다. 그리고 언어로 보면, 결과가 다른 작업 중 내가 코딩 에이전트를 자주 쓰는 언어(Rust)로 된 작업은 4개뿐이고, 나머지 작업은 내가 코딩 에이전트를 쓰지 않거나 어떤 모델이든 괜찮은 사소한 문제에만 쓰는 언어들로 되어 있다2.

결과가 다른 네 개의 Rust 작업은 다음과 같다:

Boa에서의 계층적 평가 취소(Hierarchical evaluation cancellation in Boa) (https://deepswe.datacurve.ai/data/v1.1/tasks/boa-hierarchical-evaluation-cancellation), fd에서의 결정적 다중 키 정렬(Deterministic multi-key sorting in fd) (https://deepswe.datacurve.ai/data/v1.1/tasks/fd-deterministic-multi-key-sorting), oxvg에서의 스타일시트-셀렉터 구조 보존(Preserve stylesheet-selector structure in oxvg) (https://deepswe.datacurve.ai/data/v1.1/tasks/oxvg-structural-selector-preservation), 그리고 wasmi에서의 트랩 코어덤프 생성(Trap coredump generation in wasmi) (https://deepswe.datacurve.ai/data/v1.1/tasks/wasmi-trap-coredumps). 이들 중 어느 것도 내가 코딩 에이전트를 쓰는 일과 그다지 관련 있어 보이지 않으므로, 나에게는 무가치하다.

이 중 하나는 지난 1년 동안 내가 했던 일과 어렴풋이 비슷한 것 같고 나머지 세 개는 그렇지 않다. 개별 벤치마크를 보면 벤치마크마다 결과의 분산이 크다는 것을 안다(예를 들어 지난 글의 Optimization 1 벤치마크에서는 어렴풋이 DeepSWE와 유사한 모델 순위가 나오지만, GameAI에서는 Senior SWE-Bench와 유사한 순위가 나온다. 하지만 그 글에서도 관찰했듯이, 우리가 신경 쓰는 작업과 명목상 비슷해 보이는 벤치마크 하나가 실제 작업에서 보는 것과 정반대의 결과를 줄 수도 있다. 다시 말하지만 분산이 매우 크기 때문이다). 113개 작업 중 1개가 내가 했던 작업과 어렴풋이 비슷하다는 것은 DeepSWE 벤치마크 점수가 개인적으로는 무의미하다는 뜻이다.

Senior SWE-Bench

다른 벤치마크로 넘어가면, Senior SWE-Bench는 위에서 언급한 모든 문제를 가지고 있으며, 결과도 더 오해의 소지가 있게 제시하고 더 주관적인 결과 채점이라는 추가 문제가 있다. 이런 것을 하나하나 길게 뜯어보고 싶지는 않지만, 그중 한 가지 문제를 보자면, “tasteful solve(취향에 맞는 해결)”로 인정받으려면 해결책이 여러 기준을 충족해야 하며, 여기에는 루브릭에서 일정 점수 이상을 받는 것과 레퍼런스 결과 길이의 2배 이상이 되지 않는 결과가 포함된다.

자의적이고 주관적인 채점 함수

더 깊이 들여다보지 않아도, 이는 전형적인 https://danluu.com/discontinuities/ 상황임을 알 수 있다. 벤치마크는 연속적인 점수를 가지고 있으면서 엄격한 컷오프를 요구함으로써 임계값 효과를 도입한다. 내가 본 바로는 이런 방식이 종종 일을 더 단순하게 만들기 때문에 행해지지만, 근본적인 기준이 중요하다고 믿는다면 일반적으로 점수 X는 합격이고 점수 X-epsilon은 불합격이라고 말하고 싶지 않을 것이다. 대신 점수는 불연속적이지 않은 방식으로 집계되어야 한다. 나는 이런 공식을 적으려는 시도가 종종 가중치가 자의적이고 점수가 무의미하다는 것을 명백하게 만들기 때문에 사람들이 이를 꺼린다고 생각한다. 레퍼런스가 R LOC일 때 1 LOC짜리 해결책에 대해 N점의 추가 점수를 주고 싶지 않다는 것은 아마 알 것이다. 그래서 그곳에서 가치를 제한할 어떤 함수가 필요하다. 어쩌면 (2R)/(R+N) 같은 것으로 보너스를 2로 제한할 수 있을 것이다. 어쩌면 이것이 큰 함수를 충분히 페널티 주지 못하므로 (2R^2)/(R^2+N^2)로 바꿔야 할 수도 있다. 이를 1+tanh(ln(R/N))로 쓰면 동작을 이해하기 더 쉬울 수도 있으니, 선호한다면 그렇게 정신적으로 치환해도 좋다. 그런 다음 이를 다른 점수들과 결합해야 하므로, M개의 공식 중 최소 M-1개의 상대적 가중치를 정하기 위해 최소 M-1개의 공식을 더해야 한다.

이는 분명히 정당화하기 어려운 자의적인 공식일 것이다. 하지만 실제로 사용된, 불연속성을 가진 공식 역시 또 다른 완전히 자의적인 함수이며, 정당화하기를 더 어렵게 만드는 더 나쁜 속성들을 가지고 있다! 단지 이를 적는 사람이 이를 공식으로 생각할 필요가 없으니 얼마나 자의적인지에 대해 고민하지 않아도 될 뿐이다.

임계값 효과

Senior SWE-Bench에서 정의된 LOC 측정을 구체적으로 보면, 당연히 임계값 효과가 보인다. 예를 들어 https://senior-swe-bench.snorkel.ai/tasks/paperless-ngx-perf-workflow-queries에서 GLM-5.2는 레퍼런스 61 LOC 대비 121 LOC로 tasteful 판정을 받는다. 여기서 딱 1 LOC만 더 많았어서 122, 즉 두 배가 되었다면 GLM-5.2는 통과가 아니라 실패가 되었을 것이다. 링크에서 볼 수 있듯이 벤치마크는 조건당 한 번씩만 실행되었다. LLM을 써 본 사람이라면 누구나 알듯이, 그리고 지난 글에서 봤듯이 실행 간 분산이 엄청나다(지난 글에서 관찰했듯이, 실행 간 분산이 서로 다른 모델과 노력 수준 간의 분산보다 더 큰 경우가 꽤 흔하다). 이는 어떤 합리적인 연속 점수로 채점할 때도 단일 실행이 그다지 의미가 없게 만든다. 이런 잡음 많은 지표에서 임계값 효과로 정보가 제거되면 결과는 더욱 무의미해진다.

그것도 LOC 점수 측면에서 특별히 문제가 많은 벤치마크는 아니다. plausible-fix-top-pages-comparison은 더 심한데, 레퍼런스 해결책이 1 LOC이기 때문이다(추가와 삭제가 각각 1 LOC로 계산되므로 이는 2 LOC로 채점된다). 이 때문에 tasteful한 해결책의 최대 크기는 3 LOC가 된다. 추가와 삭제가 모두 일어난다면 이는 1 LOC 삭제와 2 LOC 추가, 혹은 그 반대여야 한다.

코드 품질

실제 결과를 보면 말이 되지 않는다. 이 작업에서 Opus 4.8은 “tasteful” 판정을 받지만 Opus 4.7과 Fable 5는 받지 못한다. 실제 diff를 보고 레퍼런스 해결책과 비교하면 다음과 같다(LOC 기준에서는 실제 코드에 대한 변경만 카운트되며, 테스트 LOC, 주석 등은 카운트되지 않는다는 점에 유의하라).

레퍼런스
  --- a/lib/plausible_web/controllers/api/stats_controller.ex
  +++ b/lib/plausible_web/controllers/api/stats_controller.ex
  @@ -723,7 +723,7 @@ defmodule PlausibleWeb.Api.StatsController do
       else
         json(conn, %{
           results: pages,
  -        meta: Map.merge(meta, Stats.Breakdown.formatted_date_ranges(query)),
  +        meta: Map.new(meta.values) |> Map.merge(Stats.Breakdown.formatted_date_ranges(query)),
           skip_imported_reason: meta[:imports_skip_reason]
         })
       end
Opus 4.8 (통과)
  --- CHANGELOG.md                                                                                                                         
  +++ CHANGELOG.md                                                                                                                         
  +- Fixed blank comparison dates in row tooltips on the Top Pages report                                                                 

  --- lib/plausible_web/controllers/api/stats_controller.ex
  +++ lib/plausible_web/controllers/api/stats_controller.ex
  -        meta: Map.merge(meta, Stats.Breakdown.formatted_date_ranges(query)),                      
  +        meta: Map.merge(Map.new(meta), Stats.Breakdown.formatted_date_ranges(query)),

  --- test/plausible_web/controllers/api/stats_controller/pages_test.exs
  +++ test/plausible_web/controllers/api/stats_controller/pages_test.exs                     
  +      assert json_response(conn, 200)["meta"] == %{                                                                                                                                     
  +               "date_range_label" => "2 Jan 2021",                                                
  +               "comparison_date_range_label" => "1 Jan 2021"                                                                                                                                            
  +             }                                                                                    
Opus 4.7 (실패)
  --- lib/plausible_web/controllers/api/stats_controller.ex                                          
  +++ lib/plausible_web/controllers/api/stats_controller.ex                                                                                                                                                
  -        meta: Map.merge(meta, Stats.Breakdown.formatted_date_ranges(query)),
  +        meta:                                                                                                                                                                                           
  +          meta.values                                                                                                                                                                                   
  +          |> Map.new()                                                                                                                                                                                  
  +          |> Map.merge(Stats.Breakdown.formatted_date_ranges(query)),                                                                                                                                   

  --- test/plausible_web/controllers/api/stats_controller/pages_test.exs
  +++ test/plausible_web/controllers/api/stats_controller/pages_test.exs                                                                                                                                   
  +      assert json_response(conn, 200)["meta"]["date_range_label"] == "2 Jan 2021"
  +      assert json_response(conn, 200)["meta"]["comparison_date_range_label"] == "1 Jan 2021"   
Fable 5 (실패)
  --- lib/plausible/stats/breakdown.ex                                                               
  +++ lib/plausible/stats/breakdown.ex                                                               
       %{                                                                                            
         results: build_breakdown_result(results, query_with_metrics, metrics),
  -      meta: meta
  +      # `meta` is a `Jason.OrderedObject` - convert it to a plain map so that
  +      # callers can safely `Map.merge/2` extra fields into it. Merging into the
  +      # struct would add keys outside of its `values` field, which the JSON
  +      # encoder for `Jason.OrderedObject` silently ignores.
  +      meta: Map.new(meta.values)
       }

  --- lib/plausible_web/controllers/api/external_stats_controller.ex
  +++ lib/plausible_web/controllers/api/external_stats_controller.ex
  -  defp maybe_add_warning(payload, %Jason.OrderedObject{} = meta) do
  +  defp maybe_add_warning(payload, meta) do

  --- test/plausible_web/controllers/api/stats_controller/pages_test.exs
  +++ test/plausible_web/controllers/api/stats_controller/pages_test.exs
  +      assert json_response(conn, 200)["meta"] == %{
  +               "date_range_label" => "2 Jan 2021",
  +               "comparison_date_range_label" => "1 Jan 2021"
  +             }

나는 Elixir 프로그래머도 아니고 이 코드베이스에도 익숙하지 않지만, 코드만 봐도 실패한, “tasteful하지 않은” Opus 4.7 해결책은 레퍼런스 해결책과 의미상 동일해 보인다. 유일한 차이는 가독성을 위해 파이프라인을 여러 줄로 펼쳤다는 것이다. Elixir를 모른다고 해도, 이를 “취향에 맞지 않는다”는 이유로 실패 처리하는 것은 터무니없게 느껴진다.

나는 파이프 연산자를 흔히 쓰는 다른 언어(예: F#이나 tidyverse를 쓰는 R)를 써 봤는데, Opus 4.7의 변경을 “취향에 맞지 않는다”며 거부하는 사람을 본 적이 없는 것 같다(무엇을 여러 줄로 펼쳐야 하고 무엇을 펼치지 않아야 하는지에 대해 엄격한 규칙을 가진 스타일 가이드가 있다면 모를까. 하지만 그런 경우라면 포맷팅은 autoformatter가 처리해야 하며 해결책의 포맷팅은 무관하다).

Fable 5 해결책은 변경 범위를 너무 넓혔다는 이유로 거부되어야 한다고 주장할 수도 있지만, 다른 이유로 거부되어야 하는지와는 별개로 길이 때문에 추가로 “취향에 맞지 않는다”며 거부하는 것은 잘못된 것 같다.

채점자 분산

LLM 분산은 채점 자체에도 적용된다. 물론 단일 실행 결과를 LLM 채점자에게 여러 번 넣으면, 같은 문제를 여러 번 풀도록 LLM에 요청했을 때 흔히 매우 다른 결과가 나오는 것과 같은 이유로 서로 다른 점수를 얻게 되는 것은 당연하다. 나는 동네 코딩 에이전트를 시켜 GPT-5.6 Sol과 Opus 4.8이 테스트된 각 조건에 대해 채점을 10번씩 다시 실행해 보았다(codex에 따르면 채점은 Sonnet 4.6을 사용해 실행되었으므로 그렇게 다시 실행했다). 동일한 모델과 노력 수준을 사용할 때 예상되는 LLM 채점 tastefulness 결과는 공식 결과에서 23% 확률로 뒤집힌다(세부 결과로는 relative taste가 32% 경우에, practice alignment가 5% 경우에, task rubric이 3% 경우에 뒤집힌다). 대신 공식 결과가 전형적인/중앙값 결과와 달랐던 비율을 보면 전체적으로 21% 차이가 난다(relative taste 27%, practice alignment 3%, task rubric 2%). 전체 뒤집힘 비율이 개별 뒤집힘 비율보다 낮은 이유는, 어떤 경우에는 전체 점수가 이미 untasteful이었을 때 서브 점수 하나가 tasteful에서 untasteful로 뒤집혔기 때문이다.

분명히 하자면, 이는 실행 간 분산이 아니다. 이는 단일 실행에 대해 LLM 채점을 사용했을 때의 분산으로, 공개된 GPT-5.6 Sol과 Opus 4.8 벤치마크 전반에서 약 20% 확률로 잘못된 결과를 내는 것으로 보인다(측정 대상이 애초에 정확하고 측정할 만한 것이며, 가장 가능성 높은 Sonnet 점수가 정답 점수라고 가정한다면).

물론 다른 모델로 채점하면 다른 결과도 나온다. Sonnet 4.6 대신 GPT-5.6 Sol로 다시 채점하면, tasteful로 판정되는 해결책의 수는 두 모델 모두에서 절반 이상 줄어든다. 어느 쪽이 더 정확한가? 누가 알겠는가?

전반적인 타당성

때로는 벤치마크를 보면서 개별 결과 중 일부는 틀렸지만 전체적으로는 잡음이 상쇄되어 전체 결과가 말이 된다고 말할 수도 있다. 여기서는 그렇다고 생각하지 않는다. 나는 Senior SWE-Bench를 돌려보는 사람들을 많이 봤는데, 아마도 이 벤치마크가 현실적인 문제를 제시하고 합리적인 방식으로 채점한다고 주장하기 때문인 것 같다. 우리는 이미 표면적으로 결과가 그럴듯해 보이지 않는다고 언급했고, 방법론을 살펴본 결과 역시 결과가 의미 없다는 표면적인 생각이 지지된다는 것을 확인했다3.

결과의 제시 방식도 아쉬운 점이 있다. 내가 있는 한 Slack에서 누군가 이를 링크했는데, 다음과 같은 미리보기 스니펫이 떴다:

  • Claude Fable 5: 29.1%
  • Claude Opus 4.8: 25.0%
  • GPT-5.6 Sol: 24.4%

그는 이것이 다른 벤치마크보다 더 현실적이라며 호평을 남겼다(GPT-5.5를 Opus 4.8보다 높게 두는 수많은 벤치마크 중 하나를 언급한 것이다). 실제로 결과를 들여다보면 25.0%와 24.4% 사이의 차이는 사실상 무의미하다는 것이 분명하지만, 결과는 마치 의미 있는 차이인 것처럼 제시된다. 페이지에서는 GPT-5.6 Sol이 측정상 Opus 4.8보다 훨씬 저렴하다는 것을 명확히 하고 있지만, 내가 본 Senior SWE-Bench를 언급하는 대부분의 논의는 이를 생략하고 헤드라인 결과만 언급한다. 헤드라인 결과가 Fable, Opus, Sonnet에는 max를, GPT-5.6, GPT-5.5, GPT-5.4에는 xhigh를 사용하는 것도 이상해 보인다.

31. 추운 날씨의 타이어 성능

사람들은 흔히 사계절 타이어가 7C / 45F에서 하키 퍽처럼 단단해져 접지력이 떨어진다고 말하지만, 벤치마크는 없다! 이는 이 시리즈의 공통된 주제였다. 사람들이 측정에 근거가 없어 보이는 주장을 반복한다는 것이다4.

다행히도, 우리가 플랫폼과 수익화에 대한 글에서 논의했듯이, Jonathan Benson은 타이어에 대한 심층 탐구를 수익화할 수 있었고, 그 결과 공개 타이어 벤치마크에서는 전례 없는 수준의 디테일이 나오게 되었다. 그는 다양한 종류의 타이어가 서로 다른 온도와 조건에서 얼마나 잘 수행되는지를 테스트했다. 타이어 제조사들은 분명 이런 종류의 테스트를 많이 가지고 있겠지만, 내가 아는 한 이렇게 포괄적인 방식으로 공개적으로 수행된 적은 이전에 없었다(흠, 이건 코딩 에이전트에 대한 공개 벤치마크와 크게 다르지 않아 보인다).

Benson의 테스트에서 그는 건조한 조건에서는 썸머 타이어가 0C / 32F까지 가장 좋은 접지력을 보였고(더 추운 조건은 테스트하지 않았다), 그 다음이 사계절, 윈터가 썸머와 사계절 모두보다 꽤 큰 차이로 뒤처진다는 것을 발견했다. 젖은 조건에서는 0C에서는 젖은 상태가 아니라 빙판 상태가 되므로 2C까지만 테스트했다. 순위는 약간 다른데, 젖은 노면 2C에서는 사계절 타이어가 썸머 타이어를 압도적으로 능가했지만, 썸머 타이어는 여전히 윈터 타이어보다 앞섰다.

영상에서 Benson이 윈터 타이어라고 부르는 것은 UHP 윈터 타이어인데, 나는 미국이나 캐나다에서 사람들이 이를 쓰는 것을 거의 본 적이 없다(내가 사는 지역의 조건에 맞기 때문에 나는 윈터 타이어로 그것을 쓰긴 한다). 그가 “노르딕” 타이어라고 부르는 것이 내가 살아온 이곳 현지와 내가 살아본 다른 모든 곳에서 대부분의 사람들이 윈터 타이어로 쓰는 것이다. 그 모든 곳은 산으로 운전을 많이 하거나 얼음 위에서 운전을 많이 하지 않는 한 그런 종류의 타이어가 별로 말이 되지 않는 곳이다(그리고 그 경우에도 내가 살아온 대부분의 사람들에게는 여전히 올바른 선택이 아닐 것이다). 하지만 사계절 대비 UHP 윈터 타이어 결과를 보더라도, 0C 이상 건조하거나 젖은 조건에서는 사계절이 여전히 더 낫다는 것은 사실이다. 다만 그 차이의 크기는 미국에서 대부분이 쓰는 “노르딕” 윈터 타이어 대비보다는 훨씬 작다(그가 쓰는 용어는 유럽에서 더 흔한 것 같다?).

물론 타이어마다 성능은 다를 것이고 타이어에 따라 결과에 약간의 변동이 있을 것이며, 윈터 타이어가 사계절이나 썸머 타이어보다 나은 조건도 분명 많이 있지만, 사계절 타이어가 너무 단단해져 접지력을 잃기 때문에 추위만으로 윈터 타이어가 필요하다는 생각은 명백히 틀렸다.

타이어가 뭐가 중요하길래?

참고로, 왜 타이어에 신경 써야 하는지 궁금하다면, 평균적으로 자동차 사고는 꽤 주요한 사망 원인이다. 그리고 속도가 사고 심각도에 미치는 영향을 보면 그 영향이 꽤 크므로, 더 빨리 제동하거나 조금 더 잘 코너링해서 사고를 피하거나 충격을 약간 비껴가게 해주는 타이어를 갖는 것이 사고 심각도에 상당한 영향을 미칠 것이라고 생각하는 것이 합리적이다. 나는 이런 것에 정말 좋은 데이터가 있다고 생각하지 않는다(무작위 대조 시험을 실행하기 매우 어렵고 관찰 데이터는 일반적으로 매우 교란될 것이다). 하지만 작년에 내가 한 분석의 일환으로 실제 충돌 테스트 데이터에서 HIC와 속도 사이의 관계를 찾으려 했다. 놀랍게도 이를 수행한 논문을 찾지 못했다(다만 이 시리즈의 연습 문제로 쓸 수 있는 논문은 몇 개 찾았다). 하지만 단순한 분석 결과 그 관계는 대략 4제곱에 비례했다. 나는 이를 충돌 테스트에 대한 이 다른 글과 같은 글로 작성해야 하는데, 특히 다양한 차량의 HIC와 뇌진탕 위험에 대해 말이다! 어쨌든 나는 거주 지역에 맞는 타이어를 장착한 차를 운전하려 노력한다. 왜냐하면 달러나 노력 대비 내 안전을 위해 할 수 있는 영향력이 더 큰 개입 중 하나일 것 같기 때문이다. 하지만 내 좋은 타이어가 차이를 만든 아찔한 상황에 처한 적은 한 번도 없었고, 안전을 위해 올바른 타이어를 찾으려는 사람 자체가 애초에 사고를 낼 가능성이 더 낮을 수도 있으니, 이는 전혀 중요하지 않은 어리석은 취미일지도 모른다.

벤치마킹과 평가의 더 많은 문제들

이 글이 마음에 들었다면, 이는 벤치마킹, 평가, 실험 설계에 대한 일련의 연습 문제 중 일부이다(1, 2, 3, 4, 5, 6)5.

의견/교정/토론에 도움을 준 Peter Geoghegan, Aaron Levin, Luke Burton, Em Chu, Jamie Brandon, Yossi Kreinin, Jeshua Smith, Ikhwan Lee에게 감사드립니다.

부록: 디스크 성능에 대해 더 알아보기

다음은 실제로 디스크 성능에 대해 아는 Peter Geoghegan의 추가 의견이다:

나는 특정 접근 패턴에 대해 어느 정도 비교 가능한 SSD들 사이에서도 성능의 상당한 변동을 봐 왔다. 이는 아마도 FTL/펌웨어 수준의 차이 때문일 것이다. 분명 일부 SSD는 OS의 read ahead(direct IO 사용 시)와 무관하게 뒤로 순차 읽기를 하는 데 훨씬 더 낫다. Postgres에서 인덱스 스캔을 위한 IO 프리페칭 작업을 함께 하고 있는 분이 쓴 이 블로그 글을 보라: https://vondra.me/posts/fun-and-weirdness-with-ssds.

나는 이런 것들이 여전히 OS/파일시스템에는 불투명하다고 확신한다. 다소 오래된 이 LWN.net 기사는 이에 대한 근거를 제공한다: https://lwn.net/Articles/353411, “파일 시스템 개발자들에게 전하는 메시지는 우리가 더 많은 SSD 구현 정보를 요구할 때마다 ‘그냥 우리를 믿으세요’ 그리고 ‘예쁜 시스템 프로그래머 머리로 걱정하지 마세요’이다”.

나는 2023년에 Linux 해커인 Matthew Wilcox에게 이에 대해 물어봤다. 그는 상황이 거의 동일하다고 했고, 마이크로 벤치마킹 목적으로 성능 변동을 고려하고 싶다면 가장 좋은 방법은 여전히 프로비저닝에 매우 방어적으로 접근하고, TRIM을 정기적으로 실행하는 것 등이라고 했다.

한때(2015년쯤으로 기억한다) 나는 CPU 성능에 대한 연습 문제나 튜토리얼로 만들 생각으로 코드를 작성했다. napkin math 레포와 비슷하지만 훨씬 더 좁은 범위였다. 아이디어는 다음과 같은 질문을 가질 수 있다는 것이었다:

  1. CPU X가 있다. 이 루프가 얼마나 빠른지 알고 싶다면 어떤 파라미터를 알아야 하는가?
  2. 이 파라미터들이 주어졌을 때 루프는 얼마나 빨라야 하는가?

나는 여러 항목에 대해 원하는 코드를 가지고 있었지만, 어떤 이유에서인지 내가 작성한 코드는 DRAM 열린 페이지 접근과 닫힌 페이지 접근 사이의 차이를 드러내지 않았고, 그러다 다른 일에 정신이 팔려 결국 글로 작성하지 않았다. LLM 이전에는 이런 종류의 작업을 제대로 하는 것이 꽤 시간이 많이 걸렸다. 제대로 하려면 어떤 메커니즘이 작용하는지 충분히 알고, 코드를 작성할 때 주의를 기울이며, 그것이 무엇을 하는지 확인해야 하기 때문이다. 그리고 뭔가 잘못되어 디버깅할 시간을 충분히 만들지 못했기 때문에, 내 코드에 최소 한 가지 문제가 있음을 암시하는 미스터리가 남아 있는 상태에서 글을 쓰고 싶지 않아 결국 연습 문제를 작성하지 않았다.

어쨌든 디스크는 훨씬 더 복잡하며 좋은 수치를 얻으려면 훨씬 더 많은 주의가 필요하다. LLM을 사용하면 이제는 많은 시간을 들이지 않고도 할 수 있을 것 같지만, 여전히 약간의 주의는 필요하다.

P.S. (29)에서 언급된 내 친구는 Jamie Brandon으로, 현재 인터뷰를 진행 중이며 구직 중이다. 그는 데이터베이스(쿼리 엔진)와 스트리밍 시스템에 대해 상당한 작업을 해 왔다. 그의 가장 잘 알려진 글은 아마 Against SQL일 것이지만, 스트리밍 시스템 일관성 버그에 대한 분석 같은 내가 좋아하는 다른 글들도 꽤 많이 썼다. 그는 주로 밴쿠버 현지 일자리나 원격 일자리를 찾고 있다. 그와 이야기하고 싶다면 [email protected]으로 연락하면 된다.


  1. 나는 종종 성능 엔지니어로 오해받지만, 내 경우는 하드웨어 배경 때문에 프로그래머로서는 벤치마킹/평가/실험 설계에 대한 경험이 이례적으로 많고(소프트웨어보다 하드웨어에서 더 성숙한 분야다, 여기서 논의했듯이), 이런 경우, 혹은 이런 경우처럼 달러 가치와 쉽게 연결될 수 있는 문제를 쫓는 경향이 있어 가끔 성능 문제를 해결하는 것에 가깝다고 생각한다. 하지만 나는 다른 문제를 해결할 때만큼 성능 문제를 해결할 가능성이 높으며, 매일 성능 작업을 하는 사람들에 비해 성능 문제에 대해 특별히 깊거나 넓은 지식을 가지고 있지는 않다. [return]
  2. 언어별로 필터링하는 것이 타당한지에 대해서는, 이 글에서 언어의 토큰 효율성에 대한 글을 본 뒤 살펴보았다. 그 글의 결과는 자명하지 않은 작업에서는 재현되지 않았지만, 언어 간에는 필터링하는 것이 타당할 만큼 충분히 실제 차이가 있는 것으로 보였다. 특히 에이전트가 무언가를 구현하지 못할 때, 특히 낮은 노력 수준에서는 언어의 특이한 잘못된 사용 때문에 실패하는 경우가 많다. 예를 들어 그 글의 zstd 평가에서 Clojure를 쓰는 에이전트는 바이트 변환의 잘못된 의미론에 매우 자주 의존했지만, 근본적으로 동일한 연산을 사용할 수 있는 Java를 쓰는 에이전트는 그런 실수를 하지 않았다. [return]
  3. 메타 수준에서, 내가 대화하는 사람들 중 다른 주제에 대해 합리적이라고 생각하는 의견들을 가진 사람들은 일반적으로 이러한 헤드라인/요약 결과를 그다지 심각하게 받아들이지 않는다.

    예를 들어, 이러한 벤치마크의 유용성에 대한 논평에서 Em Chu는 이렇게 말했다:

    트위터/해커 뉴스 분위기, 적어도 오해의 소지가 있을 만큼 정밀하지는 않은 분위기가 모델이 유용한지 아닌지를 판단하는 더 나은 방법처럼 느껴진다. 아무리 이상하게 들려도 말이다(유감스럽게도 해커 뉴스 글을 많이 읽어야 해서 추천할 수는 없다). 나는 LLM 벤치마크처럼 보이는 것은 읽을 가치가 거의 0에 가깝기 때문에 보통 눈이 그냥 건너뛰게 된다. (해커 뉴스 댓글에 대해서도 그렇게 했으면 좋겠다.)

    내가 신뢰하는 대부분의 사람들도 비슷한 접근을 한다(때로는 온라인 분위기 대신 아는 사람들의 의견을 대신하기도 한다). 이에 대한 예외는 일반적으로 해당 분야에서 일하며 수많은 벤치마크를 보고 벤치마크 전체를 정신적으로 집계하는 사람들이다. 예를 들어 RL 환경 스타트업을 운영하는 Max Bitker와 이야기할 때, 그는 겉보기에 모든 공개 벤치마크에 익숙하고 모델 출시 몇 주 후 분위기가 어떨지를 밖에 있는 모든 벤치마크의 전체 지형에 대한 자신의 정신적 모델을 기반으로 예측할 수 있는 것 같다. 하지만 이는 요약 점수 지표를 보는 것과는 매우 다른 것이며, AI 분야에서 일하지 않는 한 이는 모델 효용성을 평가하기 위해 합리적으로 할 일이라기보다는 취미적 관심사에 가깝다(취미적 관심사에 반대하는 것은 아니다. 나도 취미적 관심사가 많다).

    이러한 벤치마크 결과를 심각하게 받아들이는 것이 실제 세계에서 관찰되는 것과 비교해 어떤 모습인지에 대한 구체적인 예로, 이 스레드는 누군가 DeepSWE 결과를 이용해 당시 새로 나온 5.6 Sol/Terra/Luna 대 5.5의 효용 대 비용 표를 만든 것이다. 누군가(내가 동의하는 사람이지만, 나는 다르게 표현했을 것이다)는 이렇게 답했다

    헛소리. 실제로 모델을 써는 봤나, 아니면 자위하고 있는 건가? 이 표에 따르면 5.6-sol xhigh가 5.5 xhigh보다 저렴하면서도 더 낫다는 건데. 그게 실제로 사실인 현실이 어디에 있나?

    다른 사람은 그에게 이렇게 답했다

    어디에도 없다. 벤치마크 작업이 정말 간단한 경우에는 표가 맞을 수도 있다고 생각한다.

    나는 그게 아주 공정하다고 생각하지는 않는다(그 말이 맞는 것 같은 작업을 시도해 봤다). 하지만 일반적으로 사람들은 공개 벤치마크가 보여주는 것과는 매우 다른 경험을 많이 한다. 이는 꽤 많은 사람들이 이해하고 있는 것으로 보인다. 직접 아는 사람들부터 무작위 인터넷 댓글 작성자들까지 말이다. 하지만 보편적인 것은 아니어서, 나는 여전히 사람들이 일부 모델과 노력 수준을 사용하는 이유를 설명하기 위해 이러한 점수를 돌려보는 것을 본다. 이는 일반적으로 정당화되지 않는 것 같다.

    [return]
  4. 일반적으로 나는 이런 사례가 여러 번 보이는 흔한 주장인 경우가 아니면 연습 문제로 만들지 않는다. 왜냐하면 완전히 근거 없고 틀린 주장은 너무 자주 일어나서 일반적인 경우에는 그다지 흥미롭지 않기 때문이다. [return]
  5. 나는 Patreon에 이런 글들을 게시해 왔는데, 딱히 강한 이유는 없었다. Patreon으로 약간의 돈을 벌긴 하지만, 돈을 최적화하려는 거라면 잠재적인 일자리를 통해 얻을 수 있는 잠재적 수입 차이가 Patreon을 통해 직접 벌 수 있는 금액을 훨씬 웃돌기 때문에 그냥 모든 것을 공개적으로 게시하는 것이 당연히 옳은 선택이라고 생각한다. 내가 인터뷰를 매우 못한다는 점을 고려하면 이는 두 배로 그렇다(나는 인터뷰가 형식적인 경우에만 거의 일자리를 얻어 왔고, 그렇지 않으면 인터뷰를 통과할 확률은 거의 0에 가깝다. 마지막으로 인터뷰를 했을 때 나는 leetcode 스타일 질문에서 전화 스크리닝을 통과하지 못했고, 그것을 통과하더라도 진짜 인터뷰라면 나중에 최종 인터뷰에서 탈락하는 것이 일반적이다).

    나는 원래 너무 작거나 사소해서 “제대로 된” 블로그 글로 만들기에는 부족하다고 생각한 것들을 Patreon에 게시하기 시작했다. 그러다 Patreon에 게시하는 것이 습관이 되었고 한동안 공개적으로 글을 많이 쓰지 않았다.

    일련의 연습 문제 중 일부인 이런 종류의 글은 주 블로그에 올리기에는 너무 작고 사소해 보이는 것들에 정확히 해당한다. 이에 대한 의견이 있다면 듣고 싶다.

    이 시리즈 뒤에 있는 아이디어는 벤치마킹과 평가를 더 잘할 수 있도록 돕는 어떤 종류의 튜토리얼이나 블로그 글을 쓰고 싶었다는 것이었다. 하지만 평가에 대한 내 생각은 특정 프로세스를 따르는 것보다 실수를 피하는 것이 더 중요하다는 것이어서, 일반적인 경우에 통하는 단계별 가이드 형식은 실제로 존재하지 않는다. 인과 그래프를 그리고 그래프를 보고 잠재적 문제를 파악하는 식의 실험 설계 접근법이 있다는 것을 안다. 예컨대 충돌자 편향(collider bias) 같은 것이다. 사람들이 이런 기법을 배우기 전과 후의 데이터 분석을 보면, 평균적으로 큰 차이를 만들지 않는다고 생각한다(비록 몇몇 사람들은 이를 매우 유용하다고 생각한다).

    어디선가(아마도 Andrew Gelman에게서) 일반화된 방식으로 실험 설계 문제를 피하는 방법으로서 이런 접근에 대한 비판을 본 적이 있는데, 문제는 모든 것이 모든 것과 관련되어 있으므로 인과 그래프를 만들 때도 여전히 자신의 판단을 적용한다는 것이다. 그래프가 만들어진 뒤에는 기계적으로 문제를 볼 수 있게 되지만, 애초에 잘못된 그래프를 그리는 것을 막지는 못한다.

    내가 수년 전 McElreath의 Statistical Rethinking의 첫 부분을 읽으며 엄격한 통계 분석으로 이어지는 어떤 프로세스를 배우고자 했을 때 본, 어렴풋이 관련된 아이디어는 그런 프로세스는 실제로 존재하지 않으며 결국 무언가가 말이 되는지 아닌지를 판단하기 위해 자신의 판단을 사용해야 한다는 것이었다.

    그렇다면 일련의 연습 문제가 통할 수도 있겠다고 생각해서, 50개나 100개의 연습 문제를 하나의 글에 담아 쓰자는 아이디어를 가졌다. 작은 연습 문제라면 꽤 해볼 만해 보이지만, 사람들이 다양한 것을 배우는 과정을 보면 작은 연습 문제를 잔뜩 주고 나서 사람들이 그 기법을 더 크고 복잡한 연습 문제에 일반화하기를 기대하는 것은 보통 그리 잘 통하지 않는 것이 분명하다. 더 크고 복잡한 연습 문제를 추가하기 시작하면, 이 블로그의 기준으로도 긴 글의 길이를 금방 넘어서게 된다. 이 블로그에는 2011~2012년 Google에 대한 FTC 조사에서 FTC가 잘못한 점에 대한 32,000단어 분량의 글이 있다(참고로 일반적인 소설은 종종 80,000~100,000단어라고 한다).

    일반적으로 나는 블로그에 여러 편으로 나누어 글을 올리는 것을 피해 왔다. 독자로서 나는 여러 편으로 나누어진 시리즈보다 모든 것이 한 글에 있는 것을 훨씬 선호하기 때문이다. 저자들이 종종 여러 편으로 나누는 것을 선호하는 이유는 일반적으로 더 많은 트래픽을 가져오고, 글이 소셜 미디어에서 바이럴 될 확률이 높아지는 등 때문이라는 것을 이해하지만, 나는 항상 이 블로그를 페이지 뷰를 최대화하는 것보다 내가 읽고 싶은 것에 더 가깝게 최적화해 왔다. 이 경우 단일 글 버전은 쉽게 문고판 판타지 소설만큼 길어질 수 있다(참고로 Brandon Sanderson의 Stormlight Archive 책들은 약 450,000단어라고 한다). 이 글은 3개의 연습 문제와 약 7,000단어인 것에 비하면, 그 속도라면 64개의 글이 필요하고 나는 이제 7개밖에 하지 않았지만, 64개의 글을 쓸 만큼 문제는 충분히 많이 있으며 단지 시간을 내는 문제일 뿐이다.

    [return]

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글