이 저장소가 대체 무엇인가.
요즘 LLM을 파인튜닝하려는 사람들은 크게 두 갈래 길을 걷는다. 하나는 Tinker(Thinking Machines의 매니지드 파인튜닝 API)처럼 "코드만 보내면 클라우드가 GPU를 대신 굴려주는" 서비스를 쓰는 길이고, 다른 하나는 직접 GPU 클러스터·분산학습 코드·추론 서버까지 전부 손수 짜는 길이다. 전자는 편하지만 데이터가 남의 서버로 나가고 요금이 붙는다. 후자는 자유롭지만 진입장벽이 매우 높다.
AReaL-MinT는 이 둘 사이의 세 번째 길이다. MinT라는 오픈소스 SDK가 정의한 "학습 스크립트가 호출하는 HTTP API 규격"을 그대로 흉내 내는 서버를 내가 직접 띄운다. 비유하면: MinT SDK 스크립트 = 택배 주문서, 클라우드 매니지드 서비스 = 택배회사 물류센터, AReaL-MinT = 내 창고에 그대로 옮겨 놓은 물류센터. 주문서(스크립트) 양식은 똑같이 쓰지만, 물건(모델 가중치)이 내 창고(내 GPU)를 벗어나지 않는다.
README 첫 문장은 이렇게 못박는다: "Open-source, self-hosted MinT training runtime on AReaL." 여기서 MinT는 LLM용 RL(강화학습) 학습 인프라로, "CPU 한 대짜리 스크립트가 데이터·손실함수·RL 환경을 정의하면, MinT이 그 계산을 GPU 클러스터에서 대신 실행"하는 게 핵심 아이디어다. 모델을 바꾸고 싶으면 문자열 하나(base_model="Qwen/Qwen3.6-27B")만 바꾸면 된다.
AReaL-MinT는 이 MinT과 똑같은 HTTP API를 노출하되, 내부적으로는 실제 GitHub 코드를 열어 보면 1,165줄짜리 FastAPI 앱(app.py)이 60개가 넘는 엔드포인트로 세션 생성·순전파·역전파·최적화 스텝·체크포인트·샘플링까지 전부 처리하고 있다. 학습 계산은 AReaL(중국 AI Lab 계열의 오픈소스 RL 학습 프레임워크, FSDP2 기반)에, 추론·샘플링은 SGLang(고성능 LLM 서빙 엔진)에 위임한다. 즉 "API 얼굴은 MinT, 몸통은 AReaL+SGLang"인 조합형 런타임이다.
"Tinker의 오픈소스 대안"이라는 포지셔닝 · Tinker 클라이언트 호환 · AReaL+SGLang 조합의 실전 검증.
AReaL-MinT가 공개 직후 TrendShift에 오른 이유는 세 가지 흐름이 겹치기 때문이다. 첫째, 2026년 들어 매니지드 파인튜닝 API(Tinker 등)가 "코드만 짜면 GPU를 신경 안 써도 된다"는 편의성으로 주목받으면서, 동시에 "그럼 이걸 오픈소스로 직접 돌릴 순 없나"라는 수요도 함께 커졌다. 둘째, RL 기반 파인튜닝(GRPO 등)이 SFT만으로는 못 잡는 추론 능력 향상의 표준 기법으로 자리잡았다. 셋째, AReaL과 SGLang이라는 각자 검증된 오픈소스 두 개를 하나의 서비스로 엮어 낸 통합형 접근 자체가 드물다.
| 비교 축 | Tinker류 매니지드 API | AReaL-MinT의 접근 |
|---|---|---|
| 실행 위치 | 제공사 클라우드 GPU | 내 소유 GPU 서버 — 데이터·가중치가 밖으로 안 나감 |
| 과금 | 사용량 기반 API 요금 | 전기·서버 비용만 (초기 GPU 투자는 별도) |
| API 표면 | Tinker 클라이언트 SDK | MinT SDK와 동일한 API — import mint as tinker 별칭으로 기존 코드 그대로 |
| 내부 학습 엔진 | 비공개(블랙박스) | 공개 — AReaL FSDP2, 코드로 직접 확인 가능 |
| 내부 추론 엔진 | 비공개 | 공개 — SGLang + LoRA 핫리로드 |
| 커스터마이징 | 제공사가 허용한 범위만 | 소스 전체 오픈 — 원하는 대로 패치·확장 가능 |
README는 이렇게 말한다: "MinT is compatible with the Tinker client surface. Existing Tinker code runs against AReaL-MinT unchanged by aliasing the import." 실제로 저장소의 examples/smoke_sft.py·smoke_grpo.py를 열어 보면 mint.ServiceClient → create_lora_training_client → forward_backward → optim_step → save_weights_and_get_sampling_client라는, 클라우드 매니지드 API를 쓸 때와 똑같은 호출 패턴이 로컬 서버를 향해 그대로 실행된다. "API가 같다"는 주장이 말뿐이 아니라 실행 가능한 스모크 테스트로 증명되어 있다는 점이 신뢰를 준다.
AReaL과 SGLang은 각각 이미 커뮤니티에서 검증된 프로젝트지만, 이 둘을 하나의 서비스로 자연스럽게 엮는 배선 코드(가중치 동기화, LoRA 키 이름 변환, 세션 생명주기 관리)를 만드는 일은 상당한 엔지니어링이다. 실제로 backends/areal.py 한 파일만 1,220줄이며, LoRA 어댑터를 Megatron 포맷에서 SGLang 포맷으로 키 이름을 재작성(rename)하는 함수까지 들어 있다 — "두 시스템을 이었다"는 말이 실제 통합의 디테일로 뒷받침된다.
이 저장소는 커밋이 단 1개("Release AReaL-MinT 0.1.0")뿐인 갓 공개된 알파 소프트웨어다. 지원 모델도 Qwen3.6 27B·35B-A3B 두 종류뿐이고, 실행하려면 8장의 NVIDIA GPU(학습 4장 + 추론 4장)가 필요하다 — 개인이 노트북에서 따라 하기는 사실상 불가능하다. 이 문서에서는 "GPU 없이도 개념과 코드 구조를 배우는" 방향으로 실습을 안내한다(8절·9절 참고).
control plane=FastAPI/uvicorn/pydantic, 학습=AReaL FSDP2/torch, 추론=SGLang+LoRA, 패키징=uv/pyproject.
pyproject.toml을 열어 보면 이 프로젝트가 세 겹의 레이어로 구성돼 있다는 게 바로 드러난다. ① 가벼운 기본 의존성(FastAPI 등, CPU만 있어도 설치됨), ② [gpu] 추가 의존성(swanlab 등 GPU 실습용 도구), ③ [system] 무거운 의존성(torch·AReaL·SGLang — 도커 베이스 이미지에서 상속받는 게 기본이라 명시적 설치는 선택). 이 분리 덕분에 "코드 구조만 읽어보고 싶다"면 무거운 GPU 스택 없이도 저장소를 열어 볼 수 있다.
| 레이어 | 기술 / 버전 | 역할 |
|---|---|---|
| 웹 프레임워크 | FastAPI ≥ 0.110 + uvicorn ≥ 0.29 | control plane 본체. 60개 이상 엔드포인트로 세션·학습·추론 요청 처리 |
| 스키마 검증 | Pydantic ≥ 2.0 | 모든 요청/응답 바디를 타입 안전하게 정의(schemas.py 634줄) |
| HTTP 클라이언트 | httpx + aiohttp + tenacity | SGLang 서버로의 프록시 요청. tenacity로 지수 백오프 재시도 |
| 비동기 루프 | uvloop(Linux 전용) | 표준 asyncio보다 빠른 이벤트 루프 구현 |
| 학습 엔진 | AReaL (main 브랜치) + torch ≥ 2.4 | FSDP2 기반 분산학습. PPOTrainer·GatewayTrainController 제공 |
| 추론 엔진 | SGLang 0.5.10.post1 + sglang-kernel | 고성능 LLM 서빙. LoRA 어댑터 핫스왑(/load_lora_adapter) 지원 |
| 모델 라이브러리 | transformers ≥ 5.0, ≤ 5.3 | Qwen3.6 계열 모델 로딩·토크나이저 |
| 클라이언트 SDK | mindlab-toolkit (MinT SDK) | 사용자가 학습 스크립트를 작성할 때 쓰는 import mint 패키지 (별도 저장소, git 의존) |
| 관측/로깅 | swanlab + colorlog | 학습 실험 추적 대시보드 + 색상 콘솔 로그 |
| OAI 호환 확장 | litellm + openai-agents | OpenAI 클라이언트로 이 서버를 그대로 호출할 수 있는 /oai/api/v1 경로 |
| 평가 도구 | math-verify | 수학 문제 RL 보상함수용 정답 검증 유틸 |
| 빌드/패키징 | hatchling + uv | PEP 517 빌드 백엔드. uv.lock으로 재현 가능한 의존성 고정(40만 자 규모) |
| 직렬화 | orjson(선택) | SGLang과의 대용량 JSON 페이로드를 표준 json보다 빠르게 처리 |
| 테스트/린트 | pytest ≥ 8.0 + ruff ≥ 0.4 | 17개 테스트 파일. ruff로 E/F/I/W 규칙 검사(line-length 120) |
| CLI 진입점 | project.scripts → areal-mint | areal_mint.cli:main — areal-mint serve 명령으로 서버 기동 |
FastAPI control plane의 디스패치 · future 폴링 세만틱 · FSDP2/SGLang GPU 분할 · LoRA 핫리로드 · SFT/GRPO 데이터 흐름.
README가 제시하는 아키텍처 다이어그램은 이렇다. 이 절에서는 실제 소스 코드(router.py·future_store.py·backends/areal.py·backends/areal_inf.py)를 근거로 이 다이어그램 뒤에서 정확히 무슨 일이 벌어지는지 파헤친다.
app.py의 모든 쓰기 엔드포인트(forward_backward·optim_step·asample 등)는 계산이 끝날 때까지 기다리지 않는다. 요청이 들어오면 _future(payload, **kwargs) 헬퍼가 future_store에 새 항목을 만들고 request_id만 즉시 반환한다. 실제 GPU 계산은 백그라운드에서 진행되고, 클라이언트는 /retrieve_future를 반복 호출(폴링)해 결과를 받는다. MinT SDK의 .result() 메서드가 이 폴링 루프를 클라이언트 쪽에서 감춰 준다.
future_store.py의 InMemoryFutureStore가 이를 스레드 세이프하게 구현한다 — TTL(기본 600초) 뒤에는 자동 만료·삭제된다.// future_store.py의 핵심 상태 전이 (실제 코드 발췌)
retrieve(request_id):
if payload is _PENDING: // 아직 안 끝남
raise PendingFutureError(queue_state, reason)
if pending_polls > 0: // 인위적 지연(테스트/큐 시뮬레이션용)
pending_polls -= 1; raise PendingFutureError(...)
if allow_try_again_once and not sent: // "한 번 더 물어보세요" 신호
return {"type": "try_again", ...}
if payload_size > metadata_only_threshold: // 너무 크면 메타데이터만
return {"status": "complete_metadata", "response_payload_size": ...}
del self._items[request_id] // 정상 조회 후 즉시 삭제(1회성)
return payload
식당에서 주문을 넣으면 점원이 "몇 번 손님, 준비되면 불러드릴게요"라며 진동벨(request_id)을 건넨다. 음식(학습 결과)이 나올 때까지 카운터 앞에 서서 기다릴 필요 없이, 자리에 앉아 진동벨이 울리길(폴링) 기다리면 된다. allow_try_again_once는 "지금 주방이 좀 밀려요, 조금 있다 다시 확인해 주세요"에 해당하고, metadata_only_threshold는 "포장이 너무 커서 한 번에 못 드리니 일단 무게만 알려드릴게요" 같은 대용량 응답 최적화다.
router.py의 BackendRouter는 백엔드 레지스트리(학습용 1개 + 추론용 1개, backend_id로 식별)와 세션 테이블(session_id → TrainingSessionRecord)을 관리한다. SDK가 create_lora_training_client()를 호출하면 open_training_session(spec)이 실행되어 AReaL 백엔드에 PPOTrainer 인스턴스가 하나 생성되고, 이후 그 session_id로 들어오는 모든 forward·forward_backward·save_checkpoint 호출이 같은 트레이너로 디스패치된다. GRPO의 롤아웃(샘플링) 세션도 마찬가지로 rollout_session_id → (inference_backend_id, training_session_id) 매핑을 유지해, "이 롤아웃이 어느 학습 세션의 정책을 쓰는지"를 추적한다.
흥미로운 디스패치 로직은 _rollout_train_op()에 있다. 클라이언트가 보낸 loss_fn 문자열("ppo"·"grpo"·"reverse_kl" 등)을 검사해 TrainOp.FORWARD_BACKWARD_PPO 또는 TrainOp.FORWARD_BACKWARD_REVERSE_KL로 매핑한다 — 즉 SDK 코드에서 loss_fn="importance_sampling" 또는 "ppo"라고만 지정하면, 서버가 알아서 올바른 학습 연산으로 라우팅한다.
ArealTrainingBackend(backends/areal.py)는 세션을 열 때 YAML 설정(examples/qwen3_6_27b.yaml)을 읽어 AReaL의 PPOTrainer를 구성한다. 실제 설정 파일을 보면 actor.backend: "fsdp:d4p1t1"(데이터 병렬 4, 파이프라인 1, 텐서 1 — GPU 4장에 데이터만 쪼갬)와 rollout.backend: "sglang:d1p1t4"(데이터 1, 텐서 4 — GPU 4장으로 하나의 모델을 텐서 병렬 서빙)로 4+4 GPU 분할이 코드 레벨에서 확정돼 있다. actor.use_lora: true·lora_rank: 16이 기본값이라, 27B 모델 전체가 아니라 LoRA 어댑터만 학습하는 게 기본 동작이다.
학습 데이터는 MintTrainingAdapter(backends/mint_training.py)가 SDK의 Datum 페이로드(model_input 토큰 + loss_fn_inputs의 target_tokens/weights/logprobs/advantages)를 AReaL이 이해하는 텐서 배치로 변환한다. GRPO에서 SDK가 이미 계산해 보낸 advantage 값이 있으면 그대로 쓰고(raw_advantages), 없으면 AReaL의 compute_advantages가 새로 계산하도록 흘려보낸다 — 보상함수·advantage 계산은 클라이언트(사용자 스크립트) 책임, 그 이후 PPO/GRPO 손실 계산과 그래디언트는 서버(AReaL) 책임이라는 경계가 명확하다.
README는 "SGLang inference + LoRA hot-reload"라고 짧게 설명하지만, 실제 areal_inf.py의 클래스 docstring을 보면 더 정교하다. AReaL v2의 기본 경로는 awex(NCCL in-place 가중치 전송)로, 학습 GPU에서 추론 GPU로 전체 가중치를 직접 복사해 LoRA 스왑 없이도 최신 가중치를 유지한다. 반면 MinT SDK 특유의 "이름 붙은 스냅샷 샘플러"(save_weights_and_get_sampling_client(name=...)) 요청이 오면, _save_lora_to_hf로 LoRA 어댑터를 HuggingFace 포맷으로 디스크에 저장하고, SGLang의 /load_lora_adapter API로 그 어댑터를 즉시 로드시킨다. 이때 Megatron/VLM 체크포인트의 파라미터 이름(model.language_model.*)과 SGLang이 기대하는 이름(model.*)이 달라, _rename_lora_keys_for_sglang()이 safetensors 파일을 열어 키 이름을 직접 재작성하는 실전적인 호환성 코드까지 들어 있다.
연속적인 온폴리시 RL 롤아웃에는 awex의 실시간 NCCL 전송이 유리하다(디스크 I/O 없이 즉시 반영). 반면 MinT SDK의 save_weights_and_get_sampling_client(name="policy-v0")처럼 "이 시점의 정책을 이름표 붙여 저장해두고 나중에도 다시 꺼내 쓰고 싶다"는 요구에는 디스크에 저장된 LoRA 어댑터가 맞다 — 체크포인트 재현성, 여러 버전 동시 서빙(max_loaded_loras: 8)이 가능해진다. 하나의 서버가 두 요구를 모두 만족시키려고 두 경로를 같이 갖고 있는 것이다.
GRPO의 핵심은 가치함수(value critic) 네트워크가 따로 없다는 점이다. PPO는 보통 "이 상태가 얼마나 좋은가"를 별도로 추정하는 크리틱 모델을 학습시켜야 하지만, GRPO는 같은 프롬프트에 대해 여러 응답을 뽑아(그룹) 그 그룹 내부의 평균·표준편차만으로 advantage를 정규화한다. 크리틱 모델 학습·저장·서빙이 통째로 사라지므로 인프라가 단순해진다 — qwen3_6_27b.yaml의 reward_norm: {mean_level: group, std_level: group} 설정이 이를 그대로 반영한다.
src/·examples/·scripts/·docs/·tests/ 실제 내용 기반.
파일 크기가 곧 "책임의 무게"를 보여준다. app.py(1165줄)가 "바깥세상과 대화하는 접수창구"라면, backends/areal.py(1220줄)는 "실제로 GPU를 굴리는 공장"이다. 이 둘 사이를 router.py·service.py·future_store.py가 이어주는 배관이고, schemas.py(634줄)는 접수창구에서 쓰는 "정해진 양식지" 뭉치다. patches/는 조금 특이한데, AReaL·SGLang·transformers라는 남의 라이브러리를 import 시점에 몰래 고쳐 끼우는(monkeypatch) 코드 모음이다 — 오픈소스를 조합할 때 버전 간 미묘한 API 불일치를 메꾸는 실전 기법을 보여준다. 처음 읽는다면 examples/smoke_sft.py(클라이언트 관점) → app.py의 /forward_backward 엔드포인트 → router.py → backends/areal.py 순서로, "요청이 어디를 거쳐 GPU까지 가는지" 한 번 따라가 보는 게 가장 빠르다.
이 저장소에서 실제로 무엇을 배울 수 있는가 + 각 주제 실습 아이디어.
router.py의 _rollout_train_op()과 examples/smoke_grpo.py를 나란히 읽으면, GRPO가 실무에서 어떤 형태의 코드로 구현되는지 감을 잡을 수 있다. "같은 프롬프트로 여러 응답을 뽑고, 그룹 내부 상대 비교로 advantage를 매긴다"는 아이디어가 왜 크리틱 네트워크를 없앨 수 있는지, 코드 레벨에서 확인해 보자.
실습: 종이 위에서 보상 4개 [0.8, 0.2, 0.5, 0.9]를 손으로 정규화해 advantage를 계산해 보고, PPO였다면 이 대신 무엇이 필요했을지 비교표를 만들어 보기.
examples/qwen3_6_27b.yaml의 actor.backend: "fsdp:d4p1t1"과 fsdp.memory_efficient_load: true 설정을 실마리로, PyTorch FSDP2 공식 문서를 함께 읽으면 "파라미터 샤딩·all-gather·reduce-scatter"가 실제 설정 파일에서 어떻게 한 줄짜리 표기법(d/p/t)으로 압축되는지 배울 수 있다.
실습: 작은 모델(예: GPT-2 124M)을 torch FSDP로 2개의 가상 GPU(또는 CPU 프로세스)에 쪼개 학습시켜, 단일 GPU 학습과 메모리 사용량을 비교.
backends/areal_inf.py는 SGLang을 "학습 엔진과 별개 프로세스로 띄워 놓고 HTTP로 프록시"하는 패턴을 보여준다. 왜 학습과 추론을 같은 프로세스에 두지 않았을까? generate() 메서드의 재시도 로직(tenacity)과 _build_payload()가 SGLang의 /generate API 스펙(sampling_params·lora_path 등)에 어떻게 맞춰졌는지 살펴보면 "추론 엔진을 남이 만든 걸 그대로 갖다 쓰는 법"을 배운다.
실습: SGLang(또는 vLLM) 서버를 로컬에 작은 모델로 띄우고, httpx로 직접 /generate를 호출해 응답 구조를 관찰.
lora_rank: 16 설정 하나가 실제로 무엇을 의미하는지, patches/areal_fsdp_engine.py의 _load_lora_from_hf(어댑터만 골라 로드)와 backends/areal.py의 _rename_lora_keys_for_sglang(포맷 변환)을 같이 읽으면 "LoRA 어댑터가 저장·전송·서빙되는 전체 파이프라인"을 코드로 체감할 수 있다.
실습: HuggingFace PEFT 라이브러리로 작은 모델에 LoRA(rank=8)를 붙여 파인튜닝하고, 원본 대비 학습 가능한 파라미터 개수를 직접 출력해 비교.
app.py의 미들웨어·예외 핸들러 패턴(@app.exception_handler(UnknownSessionError) 등)과, 거의 모든 엔드포인트에 /api/v1/xxx와 /xxx 두 경로를 동시에 등록하는 방식(하위 호환·클라이언트 유연성)은 실전 API 설계의 좋은 참고 자료다.
실습: 미니 FastAPI 앱을 만들어, 오래 걸리는 작업(예: time.sleep(10))을 즉시 request_id를 반환하고 백그라운드 태스크로 처리한 뒤 /status/{id}로 폴링하는 구조를 직접 구현.
future_store.py의 InMemoryFutureStore는 스레드 락·TTL 만료·"try_again" 신호·"메타데이터만 우선 반환"까지 갖춘, 실무에서 바로 참고할 만한 미니 구현체다. GPU 학습뿐 아니라 이메일 발송·이미지 처리·긴 배치 작업 등 어디에나 적용 가능한 패턴이다.
실습: future_store.py를 그대로(또는 단순화해) 복사해, 나만의 "긴 작업 큐 + 폴링 API"를 만들어 보고 TTL 만료가 실제로 동작하는지 테스트.
patches/_hook.py의 PostImportHook은 builtins.__import__를 감싸, "특정 모듈이 로드되는 순간"을 감지해 자동으로 패치 함수를 적용한다. AReaL·SGLang·transformers라는 각각 독자적으로 진화하는 오픈소스 셋을 하나로 엮을 때 생기는 버전 불일치·API 갭을 이런 식으로 메운다는 걸 배울 수 있다.
실습: 작은 파이썬 패키지를 만들어, 특정 표준 라이브러리 모듈이 import될 때 함수 하나를 몽키패치하는 미니 post-import hook을 직접 작성.
공식 요구사항 + 현실적인 개인 대안.
| 항목 | 공식 요구사항 |
|---|---|
| GPU | 8× NVIDIA(학습용 4장 + 추론용 4장, 텐서/데이터 병렬 분할) |
| Python | >=3.11, <3.13 |
| torch | >=2.4 + CUDA (도커 베이스 이미지에서 상속 권장) |
| AReaL | main 브랜치 (git 의존성) |
| SGLang | 0.5.10.post1 + sglang-kernel==0.4.1 |
| transformers | >=5.0.0, <=5.3.0 |
| 지원 모델 | Qwen3.6 27B(Dense) · Qwen3.6 35B-A3B(MoE) — 이 두 종류만 실행 가능 |
| 패키지 관리 | uv sync --extra gpu --extra system 또는 pip install -e '.[gpu]' |
| 기동 시간 | 모델 로드 약 3분, 검증 스크립트(smoke_e2e.sh) 27B 기준 약 15분 |
| 라이선스 | Apache-2.0 — 상업적 사용 가능 |
가정용 GPU(RTX 4090 등) 1~2장으로는 이 저장소를 있는 그대로 실행할 수 없다. 27B 모델 자체가 bfloat16 기준으로도 50GB가 넘는 가중치를 가지며, FSDP2로 쪼개더라도 학습에는 상당한 VRAM이 필요하다. "직접 돌려보기"보다는 아래 대안으로 접근하는 것을 권장한다.
① 코드 읽기 실습(GPU 불필요) — fake.py 백엔드는 GPU 없이 control plane 흐름을 로컬에서 그대로 체험할 수 있게 해 준다(areal-mint serve --training-backend fake). ② 소형 모델 대체 실습 — 이 저장소의 구조(FastAPI+future+백엔드 분리)를 참고해 GPT-2 124M·Qwen 0.5B 같은 소형 모델로 나만의 미니 학습 서버를 직접 만들어 보기. ③ 클라우드 스팟/시간 단위 대여 — RunPod·Lambda Labs·Vast.ai 등에서 A100 4~8장을 시간 단위로 빌려 smoke_e2e.sh를 한 번 돌려 보는 방식(비용 발생 주의, 사전 예산 확인 필수).
d4p1t1·d1p1t4 표기가 각각 데이터(d)·파이프라인(p)·텐서(t) 병렬 개수를 뜻한다. 데이터 병렬은 "같은 모델을 GPU마다 복사해 두고 서로 다른 데이터 배치를 처리"하는 방식(학습에서 흔함)이고, 텐서 병렬은 "모델 하나의 레이어 연산 자체를 여러 GPU가 쪼개서 나눠 계산"하는 방식(추론 지연시간 단축에 유리)이다. AReaL-MinT는 학습(actor)엔 데이터 병렬 4, 추론(sglang)엔 텐서 병렬 4를 각각 다르게 골라, 목적에 맞는 병렬화 전략을 나눠 쓴다.난이도별 5단계 — GPU 없이도 시작할 수 있는 개념 실습부터, 실제 배선을 뜯어보는 심화 과제까지.
GPU 없이도 가장 먼저 할 수 있는 실습이다. 저장소를 클론해 기본 의존성만 설치(pip install -e '.[dev]')한 뒤, areal-mint serve --training-backend fake로 서버를 띄운다. 그 다음 curl이나 httpx로 /healthz·/api/v1/server_info를 호출해 응답 구조를 확인하고, /create_session으로 가짜 세션을 열어 future_store의 폴링 흐름을 직접 눈으로 관찰한다.
# 로컬 실행 (GPU 불필요, fake 백엔드)
git clone https://github.com/areal-project/AReaL-MinT.git
cd AReaL-MinT
python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]' --no-deps
pip install fastapi pydantic uvicorn colorlog tenacity aiohttp aiofiles httpx requests flask
areal-mint serve --training-backend fake --port 8000
# 다른 터미널에서:
curl http://localhost:8000/healthz
curl http://localhost:8000/api/v1/server_info
tests/test_future_store.py를 열어 어떤 시나리오(정상 완료·TTL 만료·pending_polls·metadata_only)를 테스트하는지 파악한 뒤, "결과가 3번 폴링 후에야 준비되는" 새로운 시나리오를 직접 추가해 테스트를 통과시킨다. pytest tests/test_future_store.py -v로 검증.
docs/guide.md의 build_sft_datum 함수를 참고해, 토크나이저 없이도 정수 토큰 ID 리스트를 직접 만들어 fake 백엔드의 /forward_backward 엔드포인트에 POST 요청을 보내 본다. weights 배열이 prompt 구간은 0, completion 구간은 1이 되는 걸 직접 손으로 구성해 보면 "SFT loss mask"의 의미가 체화된다.
examples/smoke_helpers.py의 compute_group_advantages 함수를 참고하지 않고 직접 구현해 본다. 보상 리스트를 입력받아 (reward - group_mean) / group_std를 반환하는 순수 함수를 작성하고, 표준편차가 0일 때(모든 응답이 똑같은 보상을 받았을 때) 어떻게 나눗셈 오류를 막을지도 함께 고민해 본다.
// 직접 구현해 볼 시그니처
def compute_group_advantages(rewards: list[float]) -> list[float]:
# TODO: 그룹 평균·표준편차로 정규화
# 힌트: std가 0에 가까우면 1.0으로 클램프해 0-division 방지
...
assert compute_group_advantages([1.0, 1.0, 1.0, 1.0]) == [0.0, 0.0, 0.0, 0.0]
print(compute_group_advantages([0.9, 0.1, 0.5, 0.3]))
8×GPU 클라우드 인스턴스에 접근할 수 있다면, README의 Quick Start를 그대로 따라 mintctl start 27b로 서버를 띄우고 bash examples/smoke_e2e.sh를 실행해 SFT → GRPO → RL 수렴까지 이어지는 전체 파이프라인을 직접 관찰한다. mintctl logs -f로 실시간 로그를 보며, 4절에서 설명한 future 폴링·LoRA 핫로드가 실제 로그 라인으로 어떻게 나타나는지 확인해 보는 것이 목표.
RL 기초 → PPO/GRPO → 분산학습 → 추론 최적화 — 6주 계획.
| 주차 | 주제 | 할 일 |
|---|---|---|
| 1주차 | LLM 파인튜닝 기초 & SFT | 지도학습 파인튜닝(SFT) 개념, loss mask·프롬프트/완성 분리. docs/guide.md SFT 예제 정독 + 과제 3 완주 |
| 2주차 | 강화학습 기초 | 정책(policy)·보상(reward)·가치함수(value) 개념. OpenAI Spinning Up의 RL 입문 자료로 policy gradient 원리 익히기 |
| 3주차 | PPO → GRPO | PPO의 클리핑·중요도 샘플링 원리, GRPO가 크리틱을 없앤 이유. 과제 4(advantage 계산기) 완주 |
| 4주차 | 분산학습 (FSDP2) | 데이터/텐서/파이프라인 병렬의 차이. PyTorch FSDP2 튜토리얼 + qwen3_6_27b.yaml의 병렬 설정 재해석 |
| 5주차 | 추론 서빙 최적화 | 배치 처리·KV 캐시·프리픽스 캐싱(RadixAttention). SGLang 공식 문서 + areal_inf.py 프록시 코드 재독 |
| 6주차 | 제어 평면 설계 & LoRA | future/폴링 패턴, 비동기 API 설계. LoRA 원 논문 요약 + patches/ 폴더의 몽키패치 기법 정리. 과제 5(GPU 있으면) 도전 |
이 저장소는 "RL 이론 → 분산학습 시스템 → API 설계"라는 세 개의 서로 다른 전문 분야가 겹쳐 있다. 한 번에 다 이해하려 하면 좌절하기 쉽다. 먼저 클라이언트 관점(SDK가 뭘 보내는지) → 그다음 control plane(어떻게 라우팅하는지) → 마지막에 백엔드 내부(AReaL·SGLang이 실제로 뭘 하는지) 순서로 3단 겹겹이 파고드는 게 좋다. RL 이론이 약하다면 2~3주차를 건너뛰지 말고 먼저 다지고 오는 걸 추천한다 — GRPO 코드를 읽어도 "왜 그렇게 계산하는지"가 안 잡히면 암기가 되어버린다.
이 문서와 저장소에 나온 주요 용어 정리.
loss_fn="cross_entropy" 경로가 이에 해당한다.loss_fn="ppo" 경로로 GRPO의 두 번째 단계(클리핑 적용판)에도 쓰인다.lora_rank=16 정도면 극히 일부만 학습해 GPU 메모리·저장공간을 크게 아낄 수 있다.import mint as tinker로 그대로 재사용할 수 있다.공식 저장소 · 관련 프로젝트 · 논문.
| 구분 | 링크 |
|---|---|
| GitHub 저장소 | github.com/areal-project/AReaL-MinT |
| MinT SDK | github.com/MindLab-Research/mindlab-toolkit |
| MinT 문서 | github.com/MindLab-Research/mint-doc |
| MinT Quickstart | github.com/MindLab-Research/mint-quickstart |
| MinT Cookbook | github.com/MindLab-Research/mint-cookbook |
| AReaL (학습 엔진) | github.com/areal-project/AReaL |
| SGLang (추론 엔진) | github.com/sgl-project/sglang |
| Tinker 문서(비교 대상) | tinker-docs.thinkingmachines.ai |
| 논문 (MinT) | arXiv:2605.13779 — "MinT: Managed Infrastructure for Training and Serving Millions of LLMs" (Mind Lab, 2026) |
| 라이선스 | Apache-2.0 (상업적 이용 가능) |