Skip to content

Split View: RTX 5090 한 장으로 작은 모델들 직접 굴려보기 — microGPT·OCR·음악 생성

|

RTX 5090 한 장으로 작은 모델들 직접 굴려보기 — microGPT·OCR·음악 생성

들어가며 — 큰 모델 말고, 작은 모델을 손으로

요즘 이야기는 죄다 700B 파라미터, 수천 장 GPU입니다. 하지만 컴퓨터공학을 배우기에는 정반대가 낫습니다 — 손바닥만 한 모델을 GPU 한 장에서 직접 학습시키고, 뜯어보고, 한계를 만나는 것. 마침 RTX 5090(Blackwell, 32GB) 한 장이 SSH 너머에 있어서, 작은 모델 셋을 직접 굴려봤습니다: 밑바닥부터 학습하는 char-level GPT, 전용 OCR vs 소형 VLM 대결, 그리고 텍스트-투-뮤직. 모든 수치는 실측입니다. 그리고 그 과정에서 만난 정직한 함정들이, 사실 결과보다 더 좋은 교재였습니다.

0부 — 첫 번째 교훈: 워밍업 없는 벤치마크는 38배 거짓말을 한다

모델을 돌리기 전, GPU가 제대로 도는지 bf16 행렬곱으로 확인했습니다. 첫 측정값은 6.1 TFLOP/s. 5090이 이럴 리가 없는데... 하고 워밍업 루프를 넣고 다시 쟀습니다.

                 처음(워밍업 없음)     제대로 측정
bf16 matmul       6.1 TFLOP/s          231.8 TFLOP/s     ← 38배 차이

범인은 첫 호출에 cuBLAS 초기화·커널 오토튜닝이 통째로 들어간 것입니다. GPU 벤치마크의 철칙 — 몇 번 예열(warmup)하고, torch.cuda.synchronize()로 커널이 실제로 끝나기를 기다린 뒤에 재야 합니다. 231.8 TFLOP/s는 5090의 bf16 dense 스펙(~210 TFLOPS)과도 맞아떨어집니다. 이 글의 모든 지연(latency) 측정에는 이 워밍업이 들어가 있습니다.

참고로 이 서버를 쓰기 위한 사전 삽질도 있었습니다: 5090은 Blackwell sm_120 아키텍처라, 구버전 PyTorch는 "CUDA 사용 가능"이라 말해 놓고 커널이 없어 조용히 실패합니다. torch 2.10.0+cu128(CUDA 12.8 빌드)에서야 sm_120이 아키텍처 목록에 잡혔습니다. 최신 GPU를 쓸 때 절반은 이 환경 싸움입니다.

1부 — microGPT: GPT를 밑바닥부터, 28초에

먼저 Karpathy의 nanoGPT 계보를 따라, char-level GPT를 순수 PyTorch로 밑바닥부터 학습시켰습니다. 셰익스피어 텍스트 1MB를 글자 단위(vocab 65개)로 먹이고, 6층·6헤드·384차원 트랜스포머(약 10.75M 파라미터)를 2000스텝 돌렸습니다.

iter     1 | train 3.7921 | val 3.8103 |   10.0k tok/s
iter   500 | train 1.5798 | val 1.7738 | 1006.2k tok/s
iter  1000 | train 1.2769 | val 1.5302 | 1119.1k tok/s
iter  1750 | train 1.1221 | val 1.4425 | 1174.8k tok/s   ← val 최저점
iter  2000 | train 1.1150 | val 1.4552 | 1184.7k tok/s   ← val 반등(과적합 시작)

trained 2000 iters in 28.0s | peak VRAM 1.73 GB | 1.17M tokens/s

28초, VRAM 1.73GB. loss는 3.79에서 1.12로 떨어졌고, 생성 샘플은 이런 셰익스피어풍이 나옵니다:

The people, could be speak'd of this course,
Who shall be at bold well away and a chief.

Second Citizen:
This in the morniest hour of Turningh; down princ

문법은 엉성해도 대사 형식·인물 이름·고어투를 글자 단위로 배운 게 보입니다. 그리고 여기 숨은 교훈 하나 — val loss가 1750스텝의 1.4425에서 2000스텝에 1.4552로 반등했습니다. train loss는 계속 떨어지는데 val이 오르는 것, 교과서적인 과적합의 시작입니다. 1MB 데이터에 10M 파라미터면 금세 외우기 시작하죠. 조기 종료(early stopping)를 실물로 보는 순간입니다. 트랜스포머 구조를 더 큰 그림에서 보고 싶다면 AI 모델 개발 라이프사이클 편과 짝지어 읽어 보세요.

핵심 코드는 요즘 PyTorch답게 단출합니다 — 어텐션은 손으로 마스킹하지 않고 flash attention 커널에 맡깁니다:

# 인과적 셀프 어텐션: is_causal=True 가 삼각 마스크를 대신한다
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)

2부 — OCR 대결: 전용 모델 vs 범용 VLM

가장 재미있는 실험입니다. 같은 이미지에 전용 OCR(Microsoft TrOCR)소형 비전-언어 모델(Qwen2-VL-2B) 을 붙여 맞대결시켰습니다. 지표는 CER(Character Error Rate, 낮을수록 좋음). 테스트 이미지는 영어 한 줄, 한글 한 줄, 그리고 한·영·숫자가 섞인 영수증입니다.

모델                  이미지     지연      CER      읽은 결과
────────────────────  ────────  ───────  ──────  ──────────────────────────────
TrOCR-printed         영어      69ms     0.791   THE QUICK BROWN FOX ... LAY DOG
TrOCR-printed         한글      40ms     1.429   CHANGE @ SUBJECT EXCLIP
TrOCR-handwritten     영어      44ms     0.000   The quick brown fox ... lazy dog
Qwen2-VL-2B (VLM)     영어      90ms     0.000   The quick brown fox ... lazy dog
Qwen2-VL-2B (VLM)     한글     143ms     0.071   다람쥐 헌 챗바퀴에 타고파
Qwen2-VL-2B (VLM)     영수증   453ms      —      (한·영·숫자 다중 라인 거의 완벽)

여기서 세 가지 이야기가 나옵니다.

첫째, 지표를 곧이곧대로 믿지 마세요. TrOCR-printed의 영어 CER이 0.791이라 "형편없다"고 결론 내리면 틀립니다. 실제 출력은 THE QUICK BROWN FOX JUMPS OVER THE LAY DOG — 거의 다 맞게 읽고 전부 대문자로 냈을 뿐입니다(그리고 lazy의 z 하나를 놓쳤죠). CER은 대소문자를 다른 글자로 세기 때문에, 대문자화 하나가 점수를 0.79까지 뻥튀기한 겁니다. 대소문자를 무시하면 CER은 0.05 수준입니다. 지표가 나쁘다고 모델이 나쁜 게 아니라, 지표가 무엇을 세는지를 알아야 합니다.

둘째, 전용 모델은 좁습니다. TrOCR은 라틴 문자로만 학습돼서, 한글 이미지엔 CHANGE @ SUBJECT EXCLIP 같은 완전한 헛것을 냅니다(CER 1.429 — 삽입 오류 탓에 1을 넘김). 모델이 "모른다"고 말하지 못하고 아는 문자로 우겨넣는 것, 도메인 밖 입력의 전형적인 실패입니다.

셋째, 재미있는 반전 — "손글씨용" TrOCR-handwritten이 인쇄체 영어를 CER 0.000으로 완벽하게, 심지어 대소문자까지 맞게 읽었습니다. "인쇄체용" 모델이 대문자로 뭉갠 걸 "손글씨용"이 정확히 맞춘 것. 이름표(모델 이름)가 성능을 보장하지 않습니다 — 실제 데이터로 재보기 전까지는 모릅니다.

그리고 범용 VLM의 압승: Qwen2-VL-2B는 영어를 완벽히, 한글은 한 글자(챗/쳇)만 틀리고, 무엇보다 한·영·숫자가 섞인 영수증을 줄바꿈까지 살려 거의 그대로 옮겼습니다. 게다가 "이 영수증을 그대로 옮겨줘"라는 자연어 지시를 알아듣습니다 — 전용 OCR은 못 하는 일이죠.

정리하면 전용 vs 범용의 고전적 트레이드오프입니다:

              전용 OCR (TrOCR)          범용 VLM (Qwen2-VL-2B)
──────────  ───────────────────────  ─────────────────────────────
속도          40~69ms (빠름)            90~453ms (느림)
VRAM          1.39 GB (작음)            4.49 GB (3배)
언어          라틴 전용                 다국어
레이아웃      한 줄만                   다중 라인·표·혼용
지시          불가                      자연어로 지시 가능

한 줄짜리 라틴 문서를 초저지연·저비용으로 대량 처리하면 전용 OCR, 다국어·복잡 레이아웃·유연함이 필요하면 VLM — 감각은 이렇습니다.

3부 — 음악 생성: 8초 음악을 1.9초에

마지막은 텍스트-투-뮤직입니다. Meta의 MusicGen-small(587M)에 프롬프트 두 개를 던졌습니다.

프롬프트                                          생성시간   음악길이   실시간 배율
────────────────────────────────────────────────  ────────  ────────  ──────────
"upbeat lofi hip hop beat, warm piano, relaxing"   1.90s     7.9s       4.19x
"epic orchestral cinematic trailer, drums, brass"  1.85s     7.9s       4.30x

peak VRAM 1.37 GB

8초짜리 음악을 1.9초에 — 실시간의 4.2배 속도로 생성했습니다. MusicGen은 오디오를 EnCodec 토큰(초당 약 50개)으로 바꿔 언어 모델처럼 자기회귀로 뽑아내고, 마지막에 다시 파형으로 디코딩합니다. LLM이 텍스트 토큰을 뽑듯 음악을 "토큰"으로 뽑는 것 — 생성 AI의 문법이 여기서도 그대로입니다. 결과물은 32kHz 모노 WAV로 저장돼, 실제로 재생하면 프롬프트대로 로파이 비트와 오케스트라가 나옵니다.

마치며 — 작은 모델의 시대, GPU 한 장이면 충분하다

세 실험의 공통점: 전부 5GB 미만 VRAM에서, 초 단위로 끝났습니다.

실험            파라미터   VRAM        핵심 수치
────────────  ─────────  ─────────  ────────────────────────
microGPT       10.75M     1.73 GB    2000스텝 학습 28초
TrOCR          334M       1.39 GB    한 줄 읽기 40~69ms
Qwen2-VL-2B    2B         4.49 GB    영수증 전사 453ms
MusicGen       587M       1.37 GB    8초 음악을 1.9초에

큰 모델이 헤드라인을 가져가지만, 배우고 실험하기 좋은 곳은 이 작은 모델들입니다. GPT를 밑바닥부터 학습시키며 과적합을 눈으로 보고, OCR 대결로 "전용 vs 범용"과 "지표의 함정"을 몸으로 익히고, 음악 생성으로 생성 AI의 토큰 문법을 확인하는 것 — 이 모든 게 노트북이 아니라 30초, GPU 한 장이면 됩니다. 멀티 GPU로 넘어가는 다음 단계가 궁금하다면 분산 학습 플랫폼 편이 이어지는 이야기입니다. 그리고 손으로 직접 굴려본 모델만이 진짜 감각으로 남습니다 — 벤치마크 숫자 하나도, 워밍업 없이는 38배 거짓말을 하니까요.

참고 자료

Running Small Models Hands-On with a Single RTX 5090 — microGPT, OCR, Music Generation

Introduction — Not the big models, the small ones, by hand

The whole conversation these days is 700B parameters and thousands of GPUs. But for learning computer science, the opposite is better — training a palm-sized model on a single GPU by hand, taking it apart, and hitting its limits. As it happened, a single RTX 5090 (Blackwell, 32GB) was sitting on the other side of an SSH connection, so I ran a trio of small models by hand: a char-level GPT trained from scratch, a dedicated OCR vs. small VLM showdown, and text-to-music. Every number is measured. And the honest traps I ran into along the way turned out to be better teaching material than the results themselves.

Part 0 — First lesson: a benchmark without warmup lies by 38x

Before running any models, I checked that the GPU was actually working with a bf16 matmul. The first measurement was 6.1 TFLOP/s. There's no way a 5090 does that... so I added a warmup loop and measured again.

                 First (no warmup)    Measured properly
bf16 matmul       6.1 TFLOP/s          231.8 TFLOP/s     ← 38x difference

The culprit was that the first call swept in the entire cost of cuBLAS initialization and kernel autotuning. The iron rule of GPU benchmarking — warm up a few times, wait for the kernels to actually finish with torch.cuda.synchronize(), and then measure. 231.8 TFLOP/s also lines up with the 5090's bf16 dense spec (~210 TFLOPS). Every latency measurement in this post includes this warmup.

For the record, there was also some upfront slog just to use this server: the 5090 is a Blackwell sm_120 architecture, so older PyTorch says "CUDA available" but then silently fails because the kernels don't exist. Only with torch 2.10.0+cu128 (the CUDA 12.8 build) did sm_120 show up in the architecture list. Half the battle of using the newest GPU is this environment fight.

Part 1 — microGPT: GPT from scratch, in 28 seconds

First, following Karpathy's nanoGPT lineage, I trained a char-level GPT from scratch in pure PyTorch. I fed it 1MB of Shakespeare text at the character level (vocab of 65) and ran a 6-layer, 6-head, 384-dimensional transformer (about 10.75M parameters) for 2000 steps.

iter     1 | train 3.7921 | val 3.8103 |   10.0k tok/s
iter   500 | train 1.5798 | val 1.7738 | 1006.2k tok/s
iter  1000 | train 1.2769 | val 1.5302 | 1119.1k tok/s
iter  1750 | train 1.1221 | val 1.4425 | 1174.8k tok/s   ← val minimum
iter  2000 | train 1.1150 | val 1.4552 | 1184.7k tok/s   ← val rebounds (overfitting begins)

trained 2000 iters in 28.0s | peak VRAM 1.73 GB | 1.17M tokens/s

28 seconds, 1.73GB of VRAM. Loss dropped from 3.79 to 1.12, and generated samples come out in this Shakespearean style:

The people, could be speak'd of this course,
Who shall be at bold well away and a chief.

Second Citizen:
This in the morniest hour of Turningh; down princ

The grammar is sloppy, but you can see it learned the dialogue format, character names, and archaic diction at the character level. And there's a hidden lesson here — val loss rebounded from 1.4425 at step 1750 to 1.4552 at step 2000. Train loss keeps falling while val rises: this is the textbook start of overfitting. With 10M parameters on 1MB of data, it starts memorizing quickly. It's the moment you see early stopping in the flesh. If you want to see the transformer architecture in a bigger picture, read this alongside the AI Model Development Lifecycle post.

The core code is spare, as befits modern PyTorch — attention isn't masked by hand but left to the flash attention kernel:

# Causal self-attention: is_causal=True replaces the triangular mask
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)

Part 2 — OCR showdown: dedicated model vs. general-purpose VLM

This is the most fun experiment. I put a dedicated OCR model (Microsoft TrOCR) and a small vision-language model (Qwen2-VL-2B) head-to-head on the same images. The metric is CER (Character Error Rate, lower is better). The test images are one line of English, one line of Korean, and a receipt mixing Korean, English, and numbers.

Model                 Image     Latency  CER      Read result
────────────────────  ────────  ───────  ──────  ──────────────────────────────
TrOCR-printed         English   69ms     0.791   THE QUICK BROWN FOX ... LAY DOG
TrOCR-printed         Korean    40ms     1.429   CHANGE @ SUBJECT EXCLIP
TrOCR-handwritten     English   44ms     0.000   The quick brown fox ... lazy dog
Qwen2-VL-2B (VLM)     English   90ms     0.000   The quick brown fox ... lazy dog
Qwen2-VL-2B (VLM)     Korean   143ms     0.071   다람쥐 헌 챗바퀴에 타고파
Qwen2-VL-2B (VLM)     Receipt  453ms      —      (KR/EN/num multi-line nearly perfect)

Three stories come out of this.

First, don't take a metric at face value. If you see TrOCR-printed's English CER of 0.791 and conclude it's "terrible," you're wrong. The actual output was THE QUICK BROWN FOX JUMPS OVER THE LAY DOG — it read almost everything correctly and just put it out in all caps (and dropped one z in lazy). Because CER counts uppercase and lowercase as different characters, a single capitalization inflated the score to 0.79. Ignore case and the CER is around 0.05. A bad metric doesn't mean a bad model — you have to know what the metric is counting.

Second, dedicated models are narrow. TrOCR was trained only on Latin script, so on a Korean image it produces complete nonsense like CHANGE @ SUBJECT EXCLIP (CER 1.429 — it exceeds 1 because of insertion errors). The model can't say "I don't know" and instead forces the input into characters it does know: the classic failure of out-of-domain input.

Third, an entertaining twist — the "handwriting" model TrOCR-handwritten read printed English perfectly at CER 0.000, even getting the case right. The "print" model smeared it into all caps while the "handwriting" one nailed it exactly. A name tag (the model name) doesn't guarantee performance — you don't know until you measure on real data.

And the general-purpose VLM's landslide win: Qwen2-VL-2B read English perfectly, missed only one character in Korean (챗/쳇), and above all transcribed the receipt mixing Korean, English, and numbers nearly verbatim, preserving even the line breaks. On top of that, it understands a natural-language instruction like "transcribe this receipt as-is" — something a dedicated OCR model can't do.

To sum up, it's the classic dedicated vs. general-purpose trade-off:

              Dedicated OCR (TrOCR)    General-purpose VLM (Qwen2-VL-2B)
──────────  ───────────────────────  ─────────────────────────────
Speed         40~69ms (fast)           90~453ms (slow)
VRAM          1.39 GB (small)          4.49 GB (3x)
Language      Latin only               Multilingual
Layout        Single line only         Multi-line, tables, mixed
Instruction   Not possible             Instructable in natural language

If you're mass-processing single-line Latin documents at ultra-low latency and low cost, go with dedicated OCR; if you need multilingual support, complex layouts, and flexibility, go with a VLM — that's the intuition.

Part 3 — Music generation: 8 seconds of music in 1.9 seconds

Last is text-to-music. I threw two prompts at Meta's MusicGen-small (587M).

Prompt                                            Gen time   Music len   Real-time factor
────────────────────────────────────────────────  ────────  ────────  ──────────
"upbeat lofi hip hop beat, warm piano, relaxing"   1.90s     7.9s       4.19x
"epic orchestral cinematic trailer, drums, brass"  1.85s     7.9s       4.30x

peak VRAM 1.37 GB

8 seconds of music in 1.9 seconds — generated at 4.2x real-time speed. MusicGen converts audio into EnCodec tokens (about 50 per second), produces them autoregressively like a language model, and finally decodes back into a waveform. It pulls out music as "tokens" the way an LLM pulls out text tokens — the grammar of generative AI carries over here intact. The output is saved as 32kHz mono WAV, and when you actually play it, you get the lofi beat and orchestra as prompted.

Closing — The age of small models: a single GPU is enough

What the three experiments have in common: all of them ran in under 5GB of VRAM and finished in seconds.

Experiment      Parameters  VRAM        Key figure
────────────  ─────────  ─────────  ────────────────────────
microGPT       10.75M     1.73 GB    2000-step training in 28s
TrOCR          334M       1.39 GB    single line read 40~69ms
Qwen2-VL-2B    2B         4.49 GB    receipt transcription 453ms
MusicGen       587M       1.37 GB    8s of music in 1.9s

Big models grab the headlines, but the best place to learn and experiment is these small models. Training a GPT from scratch and watching overfitting with your own eyes, learning "dedicated vs. general-purpose" and "the trap of metrics" in your bones through an OCR showdown, confirming the token grammar of generative AI through music generation — all of it takes not a notebook but 30 seconds and a single GPU. If you're curious about the next step of moving to multiple GPUs, the Distributed Training Platforms post is the continuing story. And only the models you've run by hand stay with you as real intuition — because even a single benchmark number lies by 38x without warmup.

References