메뉴
HN
Hacker News 30일 전

순수 C/CUDA로 제로부터 구축한 GPT-2 규모 모델, NanoEuler

IMP
7/10
핵심 요약

PyTorch나 자동 미분(autograd), 머신러닝 라이브러리 없이 순수 C/CUDA만으로 GPT-2 급의 언어 모델을 제로부터 구축한 프로젝트가 공개되었습니다. 순방향/역방향 패스부터 바이트 수준 BPE 토크나이저, CUDA 엔진, FlashAttention까지 전체 학습 파이프라인을 직접 손으로 작성 및 검증한 것이 특징입니다. 실사용 챗봇보다는 대형 언어 모델의 동작 원리와 엔지니어링 과정을 투명하게 이해하기 위한 교육 및 연구용으로 큰 의미가 있습니다.

번역된 본문

nanoeuler는 PyTorch, 자동 미분(autograd), 머신러닝 라이브러리 없이 순수 C/CUDA만으로 제로부터 완전히 구축된 GPT-2 급 언어 모델입니다. 순방향 및 역방향 패스는 직접 손으로 작성하고 검증되었으며, 전체 학습 파이프라인이 이 저장소에 담겨 있습니다. 여기에는 직접 작성한 바이트 수준 BPE 토크나이저, 서적 및 웹 말뭉치를 활용한 사전 학습, 그리고 챗 모델로의 지도 미세조정(SFT) 과정이 포함되어 있습니다 (향후 RLHF/DPO 적용을 계획 중입니다).

이 모델은 작은 데모용 모델의 경우 CPU(libm + OpenMP)에서 실행되며, 완전히 제로부터 작성된 CUDA 엔진 — cuBLAS 행렬 곱셈, 직접 작성한 FlashAttention, CPU 참조 구현과 대비한 전체 모델 기울기 검증(gradient check) — 을 통해 단일 RTX 4070 GPU에서 약 1억 1,600만 개의 매개변수를 가진 모델을 학습합니다.

현재 상태 및 솔직한 평가: 이 프로젝트는 공개적으로 개발된 연구 및 교육용 결과물입니다. 단일 소비자용 GPU로 학습된 약 1억 1,600만 개 매개변수 모델은 GPT-2-small의 정신을 계승하는 텍스트 생성기로, 어느 정도 유창한 영어는 구사하지만 실제 세상의 지식은 없습니다. 이것은 유능한 어시스턴트가 아닙니다. 챗 모델은 사전 학습에서 SFT로 이어지는 파이프라인이 엔드투엔드로 작동한다는 것을 보여줄 뿐, 유용한 챗봇은 아닙니다. 이 프로젝트의 핵심은 제로부터 시작하는 엔지니어링과 완전하고 이해 가능한 학습 파이프라인을 구축하는 데 있습니다.

make check # 역방향 패스 검증 (기울기 확인, 배정밀도) make # 학습 바이너리 빌드 ./nanoeuler train # 작은 데모용 모델 학습 (약 0.76M 매개변수) ./nanoeuler train big # 더 큰 모델 학습 (약 10M 매개변수; GPU 전용) ./nanoeuler chat # REPL: 프롬프트를 입력하면 모델이 이어서 작성

왜 "Euler(오일러)"인가? 잔차 블록(residual block)은 x = x + f(x)를 계산합니다. 이를 수치 적분의 한 단계로 읽을 수 있습니다. 전진 오일러 방법(forward-Euler method)은 상미분 방정식 dx/dt = f(x)를 x(t+Δt) = x(t) + Δt · f(x(t))로 진행시킵니다. 여기에 스텝 크기 Δt = 1을 적용하면 이것은 정확히 잔차 업데이트와 같습니다. 따라서 심층 잔차 네트워크는 이산화된 ODE입니다. 즉, 깊이가 적분 시간이며, 각 레이어는 은닉 상태를 한 오일러 스텝씩 앞으로 적분합니다. 이는 신경망 ODE(Neural ODEs)와 같은 연구의 기반이 되는 관점입니다(ResNet은 연속 흐름의 오일러 이산화입니다). 이 프로젝트는 그 적분 방법을 제공한 레온하르트 오일러(Leonhard Euler)의 이름을 따서 명명되었습니다.

예시 출력 서적 + 웹 말뭉치에 대한 부분적인 사전 학습 실행 후 약 1억 1,600만 매개변수 모델의 샘플입니다 (프롬프트: Alessandro eat a ): "Alessandro eat a icing textile: the satisfied by the servants in order to keep your weight [Using to a heated, collaborated young people that attend the metric process where the rank is authorized and to contain the sedentary. Some state lawyers were able to insert ..." 내용 자체는 의미가 없지만, 모델이 스스로 학습한 점에 주목하세요. 실제 문법, 긴 절, 그리고 웹 데이터에서 얻은 백과사전적인 어조를 띠고 있습니다. 이것이 단일 GPU로 학습된 소규모 모델의 예상된 동작입니다. 유창한 형태는 갖추었지만 내용이 얕습니다. 추가 학습과 (훨씬 더 많은) 데이터는 유창성을 향상시킵니다. 반면 세상에 대한 지식은 이 프로젝트가 염두에 두지 않은 대규모 확장(scale)을 필요로 합니다.

아키텍처 현재 모델에서 사용되는 공통 빌딩 블록을 갖춘 디코더 전용(Decoder-only) 트랜스포머입니다:

  • RMSNorm (사전 정규화, 편향 없음)
  • 쿼리와 키에 적용된 회전 위치 임베딩 (RoPE)
  • SwiGLU 피드포워드: down(silu(gate(x)) * up(x))
  • 그룹화된 쿼리 어텐션 (GQA): 쿼리 헤드가 더 작은 키/값 헤드 세트를 공유
  • 다중 토큰 예측 (MTP): K개의 출력 헤드가 다음 K개의 토큰을 예측합니다. 이 보조 헤드들은 학습된 표현을 개선하고 추측 디코딩(speculative decoding)을 가능하게 합니다. 텍스트 생성 시에는 헤드 0을 사용합니다.
  • 어디에도 편향(Bias)을 사용하지 않습니다.

GPT-2 스타일의 사전 토큰화를 적용하여 직접 작성한 바이트 수준 BPE 토크나이저 (단일 선행 공백이 다음 단어에 결합되어, 공백이 독립적인 토큰으로 낭비되지 않도록 했습니다). 병합(merges)은 말뭉치 샘플에서 학습되며, GPU 모델은 4096 토큰 어휘를 사용합니다 (영어 기준 토큰당 약 3.4바이트).

각 블록은 x = x + attn(rmsnorm(x)) 이후에 x = x + swiglu(rmsnorm(x))가 따릅니다. 잔차 연결인 x = x + f(x)는 ODE dx/dt = f(x)에 대한 전진 오일러 방법의 한 단계입니다. 이것이 프로젝트 이름의 유래이며, 레온하르트 오일러에게 경의를 표하는 이유이기도 합니다.

원문 보기
원문 보기 (영어)
nanoeuler A GPT-2-class language model built entirely from scratch in C/CUDA — no PyTorch, no autograd, no ML libraries. The forward and backward passes are written and verified by hand, and the whole training pipeline lives in this repo: a hand-written byte-level BPE tokenizer , pretraining on a books + web corpus, and supervised fine-tuning into a chat model (RLHF/DPO planned). It runs on CPU ( libm + OpenMP) for a small showcase model, and a full from-scratch CUDA engine — cuBLAS matmuls, a hand-written FlashAttention , validated against a CPU reference by a full-model gradient check — trains a ~116M-parameter model on a single RTX 4070. Status & honesty. This is a research/educational artifact, built in public. At ~116M parameters trained on a single consumer GPU, it is a text generator in the spirit of GPT-2-small : fluent-ish English, no real world knowledge . It is not a capable assistant — the chat model demonstrates that the pretrain→SFT pipeline works end to end, it is not a useful chatbot. The point of the project is the from-scratch engineering and the complete, understandable training pipeline. make check # verify the backward pass (gradient check, double precision) make # build the training binary ./nanoeuler train # train the small showcase model (~0.76M params) ./nanoeuler train big # train the larger model (~10M params; meant for a GPU) ./nanoeuler chat # REPL: type a prompt, the model continues it Why "Euler"? A residual block computes x = x + f(x) Read it as a step of numerical integration. The forward-Euler method advances an ordinary differential equation dx/dt = f(x) by x(t+Δt) = x(t) + Δt · f(x(t)) With step size Δt = 1 this is exactly the residual update. So a deep residual network is a discretized ODE : depth is integration time , and each layer integrates the hidden state forward by one Euler step. This is the view behind work like Neural ODEs (a ResNet is the Euler discretization of a continuous flow). The project is named after Leonhard Euler , who gave us that integration method. Example output A sample from the ~116M model after a partial pretraining run on the books + web corpus (prompt Alessandro eat a ): Alessandro eat a icing textile: the satisfied by the servants in order to keep your weight [Using to a heated, collaborated young people that attend the metric process where the rank is authorized and to contain the sedentary. Some state lawyers were able to insert ... The content is not meaningful, but notice what it learned on its own: real grammar, long clauses, and an encyclopedic register picked up from the web data. This is the expected behaviour of a small model trained on a single GPU — fluent shape, shallow substance. More training and (far) more data improve fluency; world knowledge needs scale this project does not pretend to have. Architecture Decoder-only transformer with the building blocks common to current models: RMSNorm (pre-norm, no bias) Rotary position embeddings (RoPE) applied to queries and keys SwiGLU feed-forward: down(silu(gate(x)) * up(x)) Grouped-query attention (GQA) : query heads share a smaller set of key/value heads Multi-token prediction (MTP) : K output heads predict the next K tokens; the auxiliary heads improve the learned representation and enable speculative decoding. Generation uses head 0. No biases anywhere. Byte-level BPE tokenizer , hand-written, with GPT-2-style pretokenization (a single leading space attaches to the following word, so spaces are not wasted as standalone tokens). Merges are learned on a sample of the corpus; the GPU model uses a 4096-token vocabulary (~3.4 bytes/token on English). Each block is x = x + attn(rmsnorm(x)) followed by x = x + swiglu(rmsnorm(x)) . A residual connection x = x + f(x) is one step of the forward-Euler method for the ODE dx/dt = f(x) — hence the name, and a nod to Leonhard Euler. Configurations: where dim q/kv heads layers context vocab params small (CPU, nanoeuler.c ) 128 4 / 2 4 128 512 ~1.05M GPU pipeline ( cuda/ , run_train ) 768 12 / 4 16 512 4096 ~116M The CPU small model trains in a few hours on 12 cores and is a self-contained showcase. The ~116M GPU model is the real pipeline: it pretrains on the books + web mix and is then fine-tuned into a chat model (see below). The head size is 64 ( 768/12 ), which fits the FlashAttention kernel. Verified backward pass Hand-written back-propagation is easy to get subtly wrong, so every analytic gradient is compared against a central finite difference. The check runs in double precision so floating-point cancellation does not hide correct gradients: $ make check tok : max rel err 1.02e-04 qkvw : max rel err 7.20e-07 gatew : max rel err 6.86e-08 ... max relative error: 1.02e-04 >>> backward OK (error < 1e-2) Every parameter tensor is checked, including the less obvious backward passes of RoPE, SwiGLU, GQA, and MTP. Build and performance make builds with -O3 -march=native -ffast-math -fopenmp . Matrix multiplies and attention are parallelized with OpenMP and vectorized; on a 12-core machine the training loop uses all cores. make check builds a separate double-precision binary used only for the gradient check. No external dependencies. Tested with gcc 13 on Linux. Scope This is a from-scratch text generator and a complete, understandable training pipeline — not a product. A model of this size trained on one GPU produces fluent-looking English with little real knowledge; the fine-tuned chat model answers in assistant form but its content is shallow. A usable conversational model needs orders of magnitude more parameters, data and compute (a ~135M model only becomes a basic assistant after ~600B training tokens; this repo trains on a far smaller corpus on a single GPU). The goal is to own every piece — every parameter, every gradient, the tokenizer, the kernels, the pretraining and the fine-tuning. GPU engine (CUDA) cuda/nanoeuler_cuda.cu is a full from-scratch CUDA port — forward, backward, training and inference on the GPU. Every kernel is validated on the device against a CPU reference, and the whole model has a GPU gradient check (GPU grads vs CPU grads to ~1e-6). Kernels: matmul (delegated to cuBLAS with TF32 tensor cores), RMSNorm, RoPE, grouped-query attention with a hand-written FlashAttention (tiled, online softmax, no T×T matrix in memory), SwiGLU, softmax/cross-entropy and AdamW. FlashAttention made the training step about 3× faster. Build (RTX 40-series = Ada = sm_89 ; the host-compiler flag avoids a gcc ICE on the large file): cd cuda nvcc -O3 -arch=sm_89 -Xcompiler -fno-tree-reassoc,-fno-tree-copy-prop nanoeuler_cuda.cu -o nanoeuler_cuda -lcublas Modes: ./nanoeuler_cuda # run all kernel self-tests (GPU vs CPU) ./nanoeuler_cuda g # full-model gradient check (GPU grads vs CPU) ./nanoeuler_cuda t # pretrain from scratch, checkpoint to ../nanoeuler.bin every 5k steps ./nanoeuler_cuda tr # resume pretraining from the latest ../nanoeuler.bin checkpoint ./nanoeuler_cuda i "It was" # autoregressive generation on GPU ./nanoeuler_cuda s # supervised fine-tune on Alpaca, save ../nanoeuler_chat.bin ./nanoeuler_cuda c # interactive chat with the fine-tuned model Training checkpoints every 5000 steps, so a long run can be stopped (Ctrl-C) and resumed with tr . A model trained on the GPU is saved in the CPU program's format, so ./nanoeuler chat can also load and run it. Chatbot: pretrain then fine-tune (SFT) The chat pipeline is two stages. First pretrain the ~116M base on the books + web mix ( ./nanoeuler_cuda t , resumable with tr ). Then supervised fine-tuning turns it into an assistant: ./nanoeuler_cuda s loads the pretrained base, renders each Alpaca example with the standard instruction template, and trains with the loss masked to the response tokens only (prompt and padding positions get a target of -1 , which the cross-entropy kernel turns into zero gradient). The result is saved to nanoeuler_chat.bin ; ./nanoeuler_cuda c then wraps each line you type in the same template and samples a