이 글은 현 시점에서 Claude code 와 Codex 최신 모델 간 바이브 코딩 성능 비교를 위해 간단한 LLM의 MoE구조를 개발하고 테스트해본 사례를 정리한다.
가급적 동일한 비교를 위해 아래 목적에 맞는 MoE 기반 LLM 개발을 위한 간단한 PRD를 우선 설계한다.
# PRD — nano MoE
문자 단위 Mixture-of-Experts 언어 모델 학습·실험 도구
| 항목 | 내용 |
|---|---|
| 프로젝트 | nano_moe |
| 버전 | 1.0 (2026-10-04 기준 구현 반영) |
| 상태 | 구현 완료 (모델 코어 · CLI · 웹 UX) |
| 실행 환경 | Windows, Python 3.12 (`venv_lmm`), PyTorch ≥ 2.5, Flask ≥ 3.0 |
---
## 1. 개요
nano MoE는 PyTorch만으로 구현한 소형 문자 단위 decoder-only Mixture-of-Experts(MoE) 언어 모델이다. MoE 학습 구조(top-k 라우팅, 전문가 용량, 균형 손실)를 작은 규모에서 직접 실행하며 학습하기 위한 교육·실험용 프로젝트이며, 범용 언어 모델 제작이 목적이 아니다.
CLI(`train.py`)로 학습·생성을 수행할 수 있고, Flask 기반 웹 UX(`webapp/`)로 데이터 설정 → 학습 → 로그/그래프 확인 → 모델 저장·로딩 → 챗봇 스타일 생성까지 한 화면에서 처리할 수 있다.
## 2. 목표 / 비목표
### 목표
- MoE 핵심 메커니즘(라우터, top-k 선택, 용량 제한, 균형 손실, z-loss)을 읽기 쉬운 최소 코드로 구현한다.
- CPU에서도 수 초 내 확인 가능한 소규모 학습 루프를 제공하고, CUDA가 있으면 자동 활용한다(bf16 autocast 포함).
- 비전문가도 브라우저에서 하이퍼파라미터 설정부터 텍스트 생성까지 전체 사이클을 수행할 수 있는 웹 UI를 제공한다.
### 비목표
- 대규모/분산 학습, 프로덕션 서빙, 멀티유저 동시 사용.
- 서브워드(BPE 등) 토크나이저 — 문자 단위 토큰화만 지원.
- 학습 데이터에 없는 문자를 포함한 프롬프트 처리.
## 3. 대상 사용자
- MoE 아키텍처 내부 동작을 코드로 확인하려는 개발자·학습자.
- 작은 텍스트로 학습→생성 사이클을 빠르게 반복 실험하려는 사용자.
## 4. 시스템 구성
```
nano_moe/
├─ nano_moe/ # 모델 패키지
│ ├─ model.py # ModelConfig, CausalAttention, Block, NanoMoE(forward/generate)
│ ├─ moe.py # Expert(FFN), MoELayer(top-k 라우터·용량·균형/z 손실)
│ └─ data.py # TextData: 문자 토큰화, 90/10 분할, 배치 샘플링
├─ train.py # CLI: train / generate 서브커맨드
├─ webapp/ # Flask 웹 UX
│ ├─ app.py # REST API + 백그라운드 학습 스레드
│ ├─ templates/index.html
│ └─ static/{style.css, app.js}
├─ tests/test_model.py # 단위 테스트 3종
├─ data/sample.txt # 예제 학습 텍스트
├─ checkpoints/ # 체크포인트 저장 위치 (기본 nano_moe.pt)
└─ run.bat # 웹 UI 원클릭 실행 (venv python 직접 호출 + 브라우저 자동 오픈)
```
## 5. 기능 요구사항
### 5.1 모델 코어 (`nano_moe/`)
| ID | 요구사항 | 구현 |
|---|---|---|
| M-1 | 문자 단위 토큰화: UTF-8 텍스트의 고유 문자 집합을 vocab으로 사용, train/validation 90/10 분할 | `data.py` TextData |
| M-2 | decoder-only Transformer: 토큰+위치 임베딩, 인과적 어텐션(SDPA `is_causal`), pre-LayerNorm 잔차 블록 | `model.py` |
| M-3 | `moe_every` 번째 블록의 FFN을 MoE 레이어로 교체, 나머지는 일반 FFN(Expert) | `model.py` Block |
| M-4 | top-k 라우팅: 라우터는 float32로 계산, 학습 중에만 노이즈 라우팅(softplus 스케일 가우시안) 적용 | `moe.py` MoELayer |
| M-5 | 전문가 용량 제한: `ceil(tokens × top_k × capacity_factor / experts)`, 초과분은 후순위 선택부터 드롭 | `moe.py` |
| M-6 | 보조 손실: 전문가 사용 균형 손실(가중치 0.01) + 라우터 z-loss(가중치 0.001)를 LM 손실에 합산 | `model.py` forward |
| M-7 | 자기회귀 생성: 온도 샘플링, 컨텍스트 길이 초과 시 최근 토큰으로 절단 | `model.py` generate |
| M-8 | 설정 검증: vocab/context/width/heads/layers 유효성, top-k ≤ experts 등 즉시 예외 | ModelConfig, MoELayer |
### 5.2 CLI (`train.py`)
| ID | 요구사항 | 구현 |
|---|---|---|
| C-1 | `train` 서브커맨드: 데이터·하이퍼파라미터 인자, 주기적 train/val 손실 출력, 체크포인트 저장(config+vocab+state_dict) | 기본 100스텝, `checkpoints/nano_moe.pt` |
| C-2 | `generate` 서브커맨드: 체크포인트 로딩 후 프롬프트 이어쓰기, vocab에 없는 문자는 에러 | 기본 80토큰, 온도 0.8 |
| C-3 | CUDA 자동 감지, bf16 지원 시 autocast, grad clip 1.0 | train() |
### 5.3 웹 UX (`webapp/`)
레이아웃: 다크 테마 단일 페이지, 좌/우 2패널. 헤더에서 한국어/영어 전환(localStorage 유지).
**왼쪽 패널 — 학습 관리**
| ID | 요구사항 | 구현 |
|---|---|---|
| W-1 | 학습 데이터 파일 경로 + 하이퍼파라미터 13종(스텝, 배치, 컨텍스트, 폭, 헤드, 레이어, MoE 주기, 전문가 수, top-k, 학습률, 로그 주기, 시드) 입력 폼 | `POST /api/train/start` |
| W-2 | 학습 시작/중지: 백그라운드 스레드 실행, 중복 실행 거부(409), 중지 플래그로 안전 중단 | `POST /api/train/stop` |
| W-3 | 실시간 로그: 1초 폴링으로 증분 수신(`since` 인덱스), 자동 스크롤 | `GET /api/status` |
| W-4 | 손실 그래프: train/val 손실을 스텝별 라인 차트로 표시 (Chart.js) | status의 history |
| W-5 | 모델 저장/로딩: 경로 지정 후 체크포인트 저장·로딩, 결과를 상태 표시줄에 피드백 | `POST /api/model/save`, `/api/model/load` |
| W-6 | 모델 상태 배지: 메모리 내 모델 유무를 헤더에 표시 | model_loaded |
**오른쪽 패널 — 챗봇 생성**
| ID | 요구사항 | 구현 |
|---|---|---|
| W-7 | 챗봇 스타일 UI: 사용자/모델 말풍선, Enter 전송(Shift+Enter 줄바꿈) | `POST /api/generate` |
| W-8 | 생성 파라미터: 토큰 수, 온도 조절 | 기본 80토큰 / 0.8 |
| W-9 | 에러 안내: 모델 없음, 빈 프롬프트, vocab에 없는 문자(해당 문자 목록 표시)를 구분해 현재 언어로 표시 | 에러 코드 기반 i18n |
### 5.4 실행 편의
| ID | 요구사항 | 구현 |
|---|---|---|
| R-1 | 원클릭 실행: conda 없이 venv python 절대경로로 서버 기동, 3초 후 브라우저 자동 오픈, 종료 시 에러 확인 가능하도록 창 유지 | `run.bat` |
## 6. 비기능 요구사항
- **성능**: 기본 설정(100스텝, width 64) 학습이 단일 GPU에서 수 초, CPU에서도 분 단위 내 완료. 생성 응답은 동기 처리.
- **동시성**: 단일 사용자 가정. 공유 상태는 lock으로 보호하고, 학습 중복 시작은 거부.
- **신뢰성**: 학습 스레드 예외는 로그로 노출하고 서버는 유지. 체크포인트는 `weights_only=True`로 로딩.
- **사용성**: 한/영 즉시 전환, 다크 테마 고정, 900px 이하에서 1열 반응형.
- **이식성**: 외부 의존성은 torch, flask 두 개. 프론트엔드는 Chart.js CDN 외 순수 HTML/CSS/JS.
## 7. 검증 (Definition of Done)
- `python -m unittest discover -s tests -v` — 라우터 그래디언트 존재, 인과성, 용량 제한·생성 shape 테스트 통과.
- CLI: `python train.py train --steps 100` 후 `python train.py generate` 로 생성 확인.
- 웹: 페이지 로드(200) → 학습 시작 → 로그·손실 히스토리 수신 → 저장 → 로딩 → 생성(지정 토큰 수) → 미지원 문자 400 응답까지 전체 플로우 검증 완료.
## 8. 제약 및 알려진 한계
- 프롬프트는 학습 데이터에 등장한 문자만 사용 가능(문자 단위 vocab의 구조적 제약). UI는 불가 문자를 명시해 안내한다.
- 체크포인트는 config·vocab을 함께 저장하므로 다른 데이터로 학습한 모델과 호환되지만, vocab이 다르면 생성 가능한 문자도 달라진다.
- Flask 개발 서버 사용 — 로컬 단일 사용자 용도이며 외부 노출·프로덕션 배포는 범위 밖.
- 학습 중 생성 요청은 직전 완료된 모델이 없으면 거부된다(학습 완료 시점에 모델이 교체됨).
## 9. 향후 과제 (범위 밖 제안)
- 학습 중 체크포인트 중간 저장 및 조기 중단 시 모델 보존.
- 데이터 파일 업로드 UI(현재는 서버 경로 입력 방식).
- 전문가별 라우팅 통계(사용률 히트맵) 시각화.
- 서브워드 토크나이저 옵션.
바이브 코딩 진행은 각 독립 폴더에 해당 도구를 CLI 모드에서 실행해서 모델을 다음과 같이 설정한다.
이후 PRD를 읽게 하고 프로젝트 개발하도록 하였다.
플젝 생성 결과는 다음과 같다.
다음과 같이 결과는 유사하게 실행된다. 다만, 명확히 지시하지 않았던 UX 테마 스타일이 달라 코덱스는 좀 화려한 형광색 위주로 생성되고, 페이블은 간략화된 다크테마 스타일로 생성된다.
알고리즘 차이는 클로드 코드는 라우팅 순위 우선(top-1 먼저), 코덱스는 시간 순 우선(앞 토큰 먼저 보존 후 생성 시 prefix 라우팅 인과성 보존 방식이며, 학습 안정성 및 재현성은 코덱스는 노이즈 라우터 0 초기화 + 1e-2 하한(초반 안정), std=0.02 초기화, 시드 고정 Generator로 손실 곡선 완전 재현하였으며, 클로드는 eval 배치가 매번 랜덤이라 val 곡선에 노이즈 섞였다.
코드 견고성은 코덱스가 신경써 개발되었고, 체크포인트 원자적 저장(tempfile+replace) 및 로딩 시 vocab 무결성 검증, utf-8-sig(BOM) 처리, 데이터 로딩 시점에 split 길이 즉시 검증, 웹은 전역 errorhandler, lock 2개로 학습/생성/로딩 직렬화, run_id로 세션 구분, CLI 인자를 dataclass에서 자동 생성을 고려하였다.
결론적으로 깊게 생각하는 차이(12분과 23분)를 고려한다면 결과를 고려할 때 바이브 코딩 모델 성능 간 큰 차이는 없어 보인다. 이 조건(생각하는 차이)에서도 결과물인 모델의 수렴 성능을 가르는 차이는 없고(실측 동일), 코덱스가 좀 더 오래 생각하며 방어 코드 재현성에서 더 촘촘하게 개발하였고, 클로드는 본 프로젝트 목적에 맞는 더 읽기 쉬운 미니멀한 코드 생성하였다.