profiling

5 개의 포스트

KernelEvolve: 메타의 랭킹 엔지니어 에이전트가 AI 인프라를 최적화하는 방법 (새 탭에서 열림)

Meta는 하드웨어와 모델 아키텍처의 급격한 다양화로 인해 발생하는 커널 최적화 병목 현상을 해결하기 위해 에이전트 기반 시스템인 'KernelEvolve'를 도입했습니다. KernelEvolve는 숙련된 엔지니어가 수주간 작업해야 했던 커널 튜닝 및 최적화 과정을 단 몇 시간 만의 자동화된 탐색으로 단축하며, NVIDIA GPU부터 자체 칩인 MTIA에 이르기까지 다양한 이기종 하드웨어에서 성능을 극대화합니다. 이를 통해 Meta는 수조 건의 일일 추론 요청을 효율적으로 처리하고 복잡한 ML 모델 혁신 속도를 가속화하고 있습니다. **폭발적인 커널 요구량과 수동 최적화의 한계** * **복잡도의 증가:** 최적화가 필요한 커널의 수는 {하드웨어 종류 및 세대 × 모델 아키텍처 × 연산자 수}의 곱에 비례하여 기하급수적으로 증가하며, 수천 개의 고유 구성을 생성합니다. * **하드웨어 이질성:** NVIDIA 및 AMD GPU, Meta 자체 칩인 MTIA는 각기 다른 메모리 계층, 명령어 집합, 실행 모델을 가집니다. 특정 플랫폼에 최적화된 커널이 다른 플랫폼에서는 성능이 저하되거나 작동하지 않는 문제가 발생합니다. * **모델 아키텍처의 진화:** 초기 임베딩 중심 모델에서 시퀀스 학습 및 어텐션 기반 모델을 거쳐, 최신 생성형 광고 추천 모델(GEM)과 대규모 추론 모델에 이르기까지 연산자의 유형이 끊임없이 변화하고 있습니다. * **확장성 부족:** 전문가에 의존하는 수동 튜닝 방식은 하드웨어와 모델의 빠른 진화 속도를 따라잡지 못해 모델 배포를 늦추는 결정적인 병목 구간이 됩니다. **KernelEvolve: 에이전트 기반 커널 저작 시스템** * **탐색 중심의 최적화:** 단순한 일회성 코드 생성이 아니라, 커널 최적화를 '탐색 문제'로 정의하여 수백 개의 대안 구현을 시도하고 최적의 솔루션을 식별합니다. * **피드백 루프 아키텍처:** LLM이 생성한 커널 후보를 전용 작업 하네스(Job-harness)에서 평가하고, 실행 결과 및 진단 정보를 다시 LLM에 피드백하여 지속적으로 개선하는 구조를 갖췄습니다. * **광범위한 언어 및 하드웨어 지원:** Triton, Cute DSL, FlyDSL과 같은 고수준 DSL은 물론 CUDA, HIP, MTIA C++ 등 저수준 언어까지 생성할 수 있어 공개 및 독점 하드웨어를 모두 지원합니다. **성능 혁신 및 실질적 도입 성과** * **처리량(Throughput) 대폭 향상:** NVIDIA GPU 기반 Andromeda 광고 모델의 추론 처리량을 60% 이상 개선했으며, Meta 자체 MTIA 칩 환경에서도 광고 모델 학습 처리량을 25% 이상 높였습니다. * **개발 주기 단축:** 프로파일링, 최적화, 교차 하드웨어 디버깅 등 수주가 소요되던 전문가의 엔지니어링 작업을 단 몇 시간의 자동 탐색으로 대체했습니다. * **실제 서비스 적용:** KernelEvolve가 최적화한 코드는 현재 Meta의 프로덕션 환경에서 매일 수조 건의 추론 요청을 처리하는 데 사용되고 있습니다. KernelEvolve는 커널 개발을 수동 프로세스에서 자동화된 적응형 시스템으로 전환함으로써 소프트웨어와 하드웨어 간의 결합 방식을 근본적으로 바꾸고 있습니다. 하드웨어 포트폴리오가 다양해질수록 이러한 에이전트 기반 인프라 최적화는 새로운 칩을 통합하는 데 필요한 엔지니어링 노력을 획기적으로 줄여줄 것입니다.

.NET 지속적 프로파일러: 예외 및 락 경합 (새 탭에서 열림)

Datadog의 .NET 컨티뉴어스 프로파일러는 애플리케이션의 성능에 보이지 않는 영향을 미치는 예외(Exception)와 락 경합(Lock Contention)을 정밀하게 추적합니다. 단순히 발생 횟수를 세는 것을 넘어, 로우 레벨 CLR 콜백과 메타데이터 분석을 통해 예외 메시지와 락 유지 시간 등 구체적인 컨텍스트를 제공하여 성능 병목의 원인을 정확히 파악하도록 돕습니다. 이를 통해 개발자는 무분별한 예외 발생으로 인한 CPU 낭비와 병렬 알고리즘의 지연 시간을 효과적으로 최적화할 수 있습니다. **예외 발생 데이터 수집 및 타입 분석** * 예외가 던져질 때 CLR은 `ICorProfilerCallback::ExceptionThrown`을 호출하며, 프로파일러는 이를 통해 예외 객체의 `ObjectID`를 획득합니다. * `ICorProfilerInfo::GetClassFromObject`를 사용하여 예외 인스턴스의 `ClassID`를 구하고, 이를 `FrameStore`와 연동하여 클래스 이름을 확인하고 캐싱합니다. * 예외 처리는 `catch` 및 `finally` 블록의 실행을 보장하기 위해 런타임에서 많은 CPU 사이클을 소모하므로, 발생 지점과 타입별 통계를 파악하는 것이 중요합니다. **System.Exception 메시지 추출을 위한 메타데이터 탐색** * 예외의 세부 내용을 파악하기 위해 `_message` 필드의 값을 읽어야 하며, 이를 위해서는 해당 필드의 정확한 메모리 오프셋을 알아야 합니다. * .NET 버전(Framework 또는 Core)에 따라 `mscorlib`나 `System.Private.CoreLib` 모듈을 식별한 후, `IMetaDataImport`를 통해 `System.Exception` 클래스의 메타데이터 토큰을 찾습니다. * `ICorProfilerInfo2::GetClassLayout`을 호출하여 클래스의 필드 레이아웃 정보를 얻고, `_message` 필드의 `COR_FIELD_OFFSET`을 계산하여 문자열 버퍼의 위치를 특정합니다. **락 경합의 지속 시간 및 원인 식별** * .NET 런타임은 `Monitor.Enter` 등을 사용하는 락 패턴에 대해 `ContentionStart`와 `ContentionStop` 이벤트를 발생시킵니다. * .NET Framework와 같이 이벤트 자체에서 지속 시간을 제공하지 않는 경우, 프로파일러가 직접 스레드별로 시작 시점의 타임스탬프를 관리하여 대기 시간을 계산합니다. * .NET 8부터는 `ContentionStart` 이벤트에 락의 `ObjectID`와 현재 락을 점유 중인 스레드 정보가 포함되어, 어떤 스레드가 다른 스레드를 대기하게 만드는지 구체적으로 가시화할 수 있습니다. **EventPipe를 통한 효율적인 실시간 이벤트 모니터링** * .NET 5 이상에서는 `ICorProfilerCallback10::EventPipeEventDelivered` 메서드를 통해 CLR 이벤트를 동기적으로 수신할 수 있습니다. * `ClrEventParser` 클래스는 이벤트 페이로드에서 ID와 키워드를 기반으로 필요한 필드만 추출하여 성능 부하를 최소화합니다. * 이러한 메커니즘을 통해 애플리케이션 실행에 거의 영향을 주지 않으면서도(Negligible impact) 상세한 프로파일링 데이터를 확보합니다. 성능 최적화를 위해서는 `Parse` 대신 `TryParse`를 사용하여 불필요한 예외 비용을 줄이고, 프로파일러가 제공하는 락 지속 시간 데이터를 바탕으로 과도한 동기화 구문을 개선하는 실용적인 접근이 필요합니다.

Profiling improvements in Go 1.18 (새 탭에서 열림)

Go 1.18은 제네릭과 퍼징(Fuzzing) 외에도 프로파일링 측면에서 비약적인 발전을 이루었으며, 특히 리눅스 환경에서의 CPU 프로파일링 정확도를 획기적으로 개선했습니다. 기존 버전에서 멀티코어 시스템의 CPU 사용량을 실제보다 낮게 측정하던 고질적인 버그를 해결하고, 프로파일러 레이블(pprof labels)의 신뢰성을 높인 것이 핵심입니다. 이러한 변화 덕분에 개발자들은 고부하 분산 시스템에서도 더욱 정밀하게 성능 병목 지점을 파악할 수 있게 되었습니다. ### 리눅스 CPU 프로파일링의 정확도 향상 * **기존 방식의 한계**: Go 1.17까지는 `setitimer(2)` 시스템 콜을 사용하여 10ms마다 `SIGPROF` 신호를 발생시켰으나, POSIX 신호의 특성상 큐에 쌓이지 않아 신호가 처리되기 전 다른 신호가 오면 유실되는 문제가 있었습니다. * **멀티코어에서의 과소측정**: 커널의 시간 측정 단위인 '지피(jiffy)' 해상도 한계로 인해 여러 코어에서 발생한 신호가 특정 시점에 몰리게 되며, 이 과정에서 대량의 신호가 누락되어 실제 CPU 사용량(예: 20코어)보다 훨씬 적은 수치(예: 2.4코어)만 기록되는 현상이 발생했습니다. * **timer_create(2) 도입**: Go 1.18은 스레드별로 신호를 관리할 수 있는 `timer_create(2)`를 도입하여 신호 유실을 방지했습니다. 이를 통해 멀티코어 시스템에서도 모든 CPU 버스트를 정확하게 포착할 수 있습니다. * **cgo 스레드 대응**: Go 런타임이 생성한 스레드뿐만 아니라 cgo 코드에서 생성된 스레드까지 아우르기 위해 `timer_create(2)`와 `setitimer(2)`를 정교하게 조합하여 구현했습니다. ### 프로파일러 레이블(pprof labels) 버그 수정 * **레이블 누락 문제**: 고루틴에 특정 키/값 쌍을 할당하여 프로파일을 분류할 수 있게 해주는 pprof 레이블이 간혹 스택 트레이스에서 누락되는 현상이 발견되었습니다. * **근본 원인 해결**: CPU 프로파일러가 레이블 정보를 수집할 때 엉뚱한 고루틴 객체를 참조하던 로직을 발견했습니다. 이를 현재 스레드에서 실제로 실행 중인 고루틴(`gp.m.curg`)을 정확히 가리키도록 수정하여 데이터의 일관성을 확보했습니다. * **트레이싱 연동 강화**: 이번 수정을 통해 프로파일링 데이터를 분산 트레이싱(Tracing)과 연결하여 분석하는 작업의 신뢰도가 크게 향상되었습니다. Go 1.18은 고성능 멀티코어 서비스를 운영하는 환경에서 필수적인 업데이트입니다. 특히 리눅스 서버에서 Go 애플리케이션의 성능을 분석할 때 이전 버전보다 훨씬 신뢰할 수 있는 데이터를 제공하므로, CPU 프로파일링 기반의 최적화를 진행 중이라면 즉시 업데이트할 것을 권장합니다.

How we optimized our Akka application using Datadog’s Continuous Profiler (새 탭에서 열림)

Datadog은 Akka 프레임워크 기반 서비스에서 예상치 못한 20~30%의 CPU 과점유 문제를 발견했으며, 이는 성능 프로파일링 도구를 통해 `ForkJoinPool` 내부의 비효율성 때문인 것으로 밝혀졌습니다. 불규칙한 작업 부하를 가진 액터가 기본 디스패처에서 실행될 때, 스레드의 빈번한 생성과 정지(`park`/`unpark`)가 반복되면서 과도한 네이티브 호출 오버헤드가 발생한 것이 원인이었습니다. 이를 안정적인 작업 흐름을 가진 전용 디스패처로 분리함으로써 시스템 전반의 CPU 효율을 획기적으로 개선할 수 있었습니다. **성능 병목 현상의 발견과 프로파일링** * 로그 파싱 알고리즘을 최적화했음에도 불구하고 CPU 사용량이 줄어들지 않고 오히려 성능이 저하되는 기현상이 발생했습니다. * Datadog Continuous Profiler의 플레임 그래프 분석 결과, 예상 밖으로 `ForkJoinPool.scan()` 및 `Unsafe.park()` 메서드에서 전체 CPU의 상당 부분이 소모되고 있었습니다. * 상세 분석 결과, 특정 작업 풀(Work pool)이 아닌 Akka의 기본 디스패처(Default Dispatcher) 소속 스레드들이 대부분의 CPU 시간을 점유하고 있었으며, 이는 주로 지표를 보고하는 `LatencyReportActor`와 연관되어 있었습니다. **ForkJoinPool 오버헤드의 근본 원인** * `ForkJoinPool`은 대기 중인 작업량에 따라 내부 워커 스레드를 동적으로 추가하거나 `Unsafe.park()`/`unpark()`를 호출하여 스레드를 일시 중지 및 재개합니다. * 문제가 된 액터는 매초 수백 개의 이벤트를 순식간에 처리한 뒤 다음 배치가 올 때까지 대기하는 불규칙한(Irregular) 작업 흐름을 가지고 있었습니다. * 이로 인해 매초 가용한 모든 코어(예: 32코어)만큼의 스레드가 동시에 깨어났다가 작업 완료 후 즉시 잠드는 과정이 반복되었고, 이 과정에서 발생하는 네이티브 호출 비용이 실제 작업 비용보다 커지는 결과가 초래되었습니다. **디스패처 분리를 통한 문제 해결** * 불규칙한 부하를 가진 액터를 기본 디스패처에서 분리하여, 부하가 일정하게 유지되는 메인 "work" 디스패처로 이동시키는 단 한 줄의 설정 변경을 적용했습니다. * 수정 후 서비스 전체 CPU 사용량이 평균 30% 감소했으며, 프로파일링 결과 `ForkJoinPool.scan()`에서 소모되던 시간이 거의 사라진 것을 확인했습니다. * 기본 디스패처의 스레드 풀 크기가 32개에서 2개로 줄어들며 스레드 관리 효율이 극대화되었습니다. **안정적인 성능 유지를 위한 권장 사항** * `ForkJoinPool.scan()` 메서드의 CPU 점유율이 10~15%를 초과한다면 스레드 풀 설정과 부하의 안정성을 반드시 점검해야 합니다. * Akka 사용 시 액터 인스턴스 수를 제한하거나 스레드 풀의 최대 크기를 적절히 설정하여 무분별한 스레드 확장을 방지해야 합니다. * 작업 큐를 활용해 급격한 작업 스파이크를 완만하게 조절하거나, 사용되는 스레드 풀의 수를 줄여 부하 밀집도를 높임으로써 활성 스레드 수를 일정하게 유지하는 것이 중요합니다.

How we wrote a Python profiler (새 탭에서 열림)

Datadog은 Java의 효율적인 상시 가동형 프로파일러에서 영감을 얻어, 서비스 성능 저하 없이 운영 환경에서 지속적으로 실행 가능한 Python용 통계적 프로파일러를 개발했습니다. 기존의 결정론적 프로파일러는 높은 오버헤드로 인해 실서비스 적용이 불가능했으나, 통계적 샘플링 기법과 모듈화된 설계를 통해 실제 사용자 부하가 걸리는 운영 환경의 성능 데이터를 정밀하게 분석할 수 있게 되었습니다. ## 결정론적 프로파일러의 한계와 실서비스 적용의 어려움 * Python의 표준 도구인 `cProfile`은 모든 함수 호출을 추적하는 결정론적(Deterministic) 방식을 사용하지만, 이는 운영 환경에 적합하지 않습니다. * 모든 함수 호출을 기록할 경우 오버헤드가 2~3배까지 증가하여 실제 서비스 성능을 심각하게 저하시킬 수 있습니다. * 반대로 함수 단위가 크고 단순한 코드에서는 추적할 데이터가 부족하여 프로그램의 동작을 제대로 이해하기 어렵다는 단점이 있습니다. * 결과적으로 운영 시스템에서 항상 켜놓고(Always-on) 사용할 수 있는 가벼운 프로파일링 도구가 Python 생태계에는 부족했습니다. ## 프로덕션 환경 프로파일링의 필요성 * 개발용 노트북과 실제 프로덕션 환경은 하드웨어, 데이터 타입, 동시성 수준 등 모든 면에서 다르기 때문에 운영 환경에서의 데이터 수집이 필수적입니다. * "우리가 무엇을 모르는지"를 파악하기 위해서는 실제 워크로드를 처리하는 애플리케이션의 동작을 직접 관찰해야 합니다. * 실제 데이터에 기반하지 않은 최적화는 오히려 성능을 해치는 "조기 최적화(Premature optimization)"의 함정에 빠질 위험이 큽니다. ## 통계적 프로파일링의 메커니즘과 장점 * 통계적 프로파일링은 모든 이벤트를 기록하는 대신, 수 밀리초 단위로 프로그램 상태를 간헐적으로 샘플링하여 관찰합니다. * 개별 함수 호출을 놓칠 수는 있지만, 장시간 수집된 데이터는 프로그램의 리소스 소비 현황을 매우 정확하고 신뢰성 있게 대변합니다. * 오버헤드가 극도로 낮기 때문에 프로파일링을 하지 않을 때와 거의 동일한 환경에서 애플리케이션의 실제 성능을 측정할 수 있습니다. ## Datadog Python 프로파일러의 구조와 특징 * **JDK Flight Recorder 기반 설계**: Recorder(데이터 저장), Collector(데이터 수집), Exporter(외부 전송), Scheduler(주기 관리)로 구성 요소를 모듈화했습니다. * **스택 컬렉터(Stack Collector)**: 핵심 컴포넌트로, 초당 100회씩 모든 Python 스레드의 실행 스택, CPU 사용 시간, 예외 처리 정보 등을 수집합니다. * **낮은 오버헤드 유지**: 프로파일러가 스스로의 성능 소비를 측정하여 시스템 부하를 능동적으로 조절함으로써 서비스 영향을 최소화합니다. * **확장성**: CPU 사용량뿐만 아니라 메모리 할당 등 다양한 데이터를 수집할 수 있도록 확장 가능한 수집기 구조를 채택했습니다. 최적화의 방향을 잡지 못해 고민 중이라면, 개발 환경의 짐작이 아닌 프로덕션 환경의 실제 데이터를 보여주는 통계적 프로파일러 도입을 권장합니다. 이는 서비스 성능 저하 없이 코드 수준의 병목 지점을 파악할 수 있는 가장 확실한 방법입니다.