Swift로 LLM 학습시키기: 행렬 연산 최적화
본 글은 Apple Silicon 환경에서 Swift를 사용하여 외부 프레임워크 없이 대규모 언어 모델(LLM)을 학습시키기 위한 행렬 곱셈 코드를 처음부터 직접 작성하고 극한으로 최적화하는 과정을 다룹니다. 저자는 Andrej Karpathy의 'llm.c' 프로젝트를 Swift로 포팅하고, CPU, SIMD, AMX, GPU 등 Apple Silicon의 다양한 연산 유닛을 활용해 기존 C언어 구현체보다 빠르게 만드는 실험을 진행합니다. 이를 통해 Swift 환경에서 ML 연산을 최적화하는 핵심 기법과 Apple 기기의 하드웨어적 성능 한계를 체감할 수 있는 귀중한 인사이트를 제공합니다.
이 글에서는 Swift로 대규모 언어 모델(LLM)을 학습시키기 위해 직접 작성한 행렬 곱셈(Matrix multiplication) 코드를 가능한 한 빠르게 실행되도록 최적화하는 과정을 다루고 있습니다. 이 글의 목적은 Swift에서 수학 연산 코드를 최적화하기 위한 핵심 단계에 대한 통찰력을 제공하는 것입니다. 또한 이러한 예제들이 Apple Silicon의 다양한 연산 장치(CPU, SIMD, AMX 및 GPU)의 성능에 대한 규모와 한계를 느끼게 해주길 바랍니다. 이 글은 Apple Silicon 환경에서 Swift로 신경망을 학습시키는 과정을 살펴보는 시리즈의 첫 번째 글입니다. 향후 기사에서는 Mac에서 기계 학습을 위해 Apple이 제공하는 (어쩌면 너무 많을지도 모르는) 다양한 프레임워크들을 살펴볼 것입니다. 여러분이 실제로 행렬 곱셈과 기계 학습에 사용해야 하는 것은 이미 검증된 프레임워크들입니다 (이 프레임워크들은 저보다 몇 년은 더 오랜 시간 동안 행렬 연산 커널을 최적화해 왔으니까요). 하지만 그때까지는 저만의 재미를 위해 '프레임워크도, 라이브러리도 없는' 순수 코드 방식으로 모든 것을 직접 작성해 보려고 합니다. 저는 단순히 행렬 곱셈 커널만 작성하는 것이 아닙니다. 샘플 앱은 완전한 LLM 구현의 일부로 이러한 커널들을 사용할 것이며, 제가 언급할 성능 수치 역시 순방향(forward) 및 역방향(backward) 학습 반복 전체에 대한 것입니다. 이 시리즈의 참조 구현은 Andrej Karpathy의 llm.c(GPT2 호환 모델의 순수 C 구현체)가 될 것입니다. 이는 꽤 기본적인 모델이지만 필요한 모든 구성 요소를 포함하고 있으며 실제 워크로드를 잘 대변합니다. 그럼 이제부터 제가 가장 좋아하는 게임을 시작하겠습니다. Swift가 C보다 빨라질 때까지 최적화하는 것이죠.
배경 스토리 약 2년 전, 저는 2000년대 초반에 작성했던 졸업 논문을 꺼내보았습니다. 신경망을 사용해 이미지를 분류하는 C++ 기반의 이미지 인식기였죠. 예전 코드를 다시 실행해 보고 싶었지만 오랫동안 ML 코드를 다루지 않았었습니다. 번거롭기도 하고 결국 포기했습니다. 2024년 초에 LLM을 둘러싼 많은 논의가 있었음에도 불구하고, Mac에서 신경망을 학습시키는 사람이 없는 것 같았습니다. 적어도 Swift와 같은 언어로는 말이죠. PyTorch나 TensorFlow 같은 Python 라이브러리를 사용해 보기도 했습니다. 하지만 Python은 직접 계산을 수행하는 것이 아니라 백그라운드에서 작동하는 다른 계산 엔진의 오케스트레이터 역할을 할 뿐이며, 이러한 분리된 구조는 제가 통제력을 느끼지 못하게 만들었습니다. 한 달 후, Andrej Karpathy가 llm.c를 공개했습니다. 이 프로젝트는 다른 기계 학습 콘텐츠들과 달리 제게 깊이 다가왔는데, 숨겨진 것이 하나도 없었기 때문입니다. 약 1,000줄의 순수 C 코드로 작성되었으며 (다소 알아보기 힘든 변수명들이 몇 가지 있긴 하지만) 비교적 읽기 쉬웠습니다. 그래서 당연히 저는 즉시 이것을 Swift로 다시 작성했고, 정말 재미있게 가지고 놀 수 있었습니다. 물론 코드를 제대로 돌려보려면 실행 속도를 높이는 작업이 필요했습니다. 여기서 약간의 복선을 깔아보자면, 초기 Swift 구현체는 정말 엄청나게 느렸습니다. 하지만 최적화는 끊임없는 과정입니다. 항상 시도해 볼 수 있는 더 많은 방법이 존재하니까요. 드디어 이 글에 도달하게 된 배경입니다. 저는 라이브러리를 사용하지 않고 LLM을 꽤 빠르게 학습시키기 위해 제가 작성했던 (그리고 지난주에 추가했던) 다양한 탐색 과정들을 단계별로 안내해 드릴 것입니다. 대부분의 코드는 Swift로 작성되겠지만 (마지막에는 Metal 구현체도 보여드릴 것입니다) 참고로 저는 신경망이나 LLM이 어떻게 작동하는지 자체를 설명하지는 않을 것입니다. 관심이 있다면 Karpathy의 영상 'Let's build GPT: from scratch, in code, spelled out.'이 GPT와 같은 LLM의 작동 원리를 배우기 위한 결정적인 가이드가 될 것입니다. 더 기초적인 학습을 원하신다면 'The spelled-out intro to language modeling: building makemore'로 시작하는 그의 초기 5부작 영상 시리즈가 다양한 기초 개념을 다루고 있으니 좋은 참고가 될 것입니다. 물론 두 영상 모두 Python을 사용하므로, Swift로 이러한 것들을 어떻게 할 수 있는지 보고 싶다면 꼭 다시 이곳으로 돌아와 주세요.
llm.c 기계 학습은 본질적으로 모델 가중치를 입력 데이터에 적용하는 과정(순방향 패스, 즉 추론이라고도 함)과, 그 후 오류 기울기(gradient)를 계산하고 해당 가중치를 업데이트하는 과정(역방향 패스)의 반복입니다. 우리는 일반적으로 이러한