S × K개의 토큰만 받게 만든다. 그러면 각 GPU의 계산량이 항상 똑같고, 메모리 모양이 고정되며, 가장 느린 GPU에 전체가 발목 잡히는 일이 사라진다.
MoonshotAI/MoonEP · 라이선스 MIT · 언어 Python(CuTe DSL) + CUDA · 코드 약 12,400줄 · 저자 Yutian Chen·Cong Li·Yucheng Wang·Ming Wei · TrendShift 데일리 #15 랭크 · 소스 직접 클론 분석)
먼저 문제 상황부터 그리자. 요즘 초거대 LLM은 대부분 MoE 구조다. 모델 안에 전문가(작은 신경망)가 수백 명 있고, 토큰마다 그중 몇 명(top-k)만 골라 계산한다. 전문가가 너무 많아 GPU 한 장에 다 안 들어가므로, 전문가들을 여러 GPU에 나눠서 배치한다. 이걸 전문가 병렬(Expert Parallelism, EP)이라고 부른다.
여기서 진짜 골칫거리는 쏠림(imbalance)이다. 라우터가 토큰을 전문가에게 배정할 때, 현실에서는 특정 전문가(예: "코드 담당")에게 토큰이 몰린다. 그러면 그 전문가가 사는 GPU만 일감이 폭증하고, 나머지 GPU는 놀면서 가장 바쁜 GPU가 끝날 때까지 전부 기다린다. 8장을 붙여도 1장 속도밖에 안 나는 셈이다.
전문가 = 택배 지점, GPU = 지점이 모여 있는 물류 센터(rank), 토큰 = 택배 상자라고 하자. 보통은 상자가 자기 담당 지점으로 흩어져 배송되는데, 어느 날 "강남 지점"에 물량이 3배로 몰리면 강남이 있는 센터만 야근하고 나머지 센터는 정시 퇴근한다. 전체 배송 완료는 강남에 묶인다.
MoonEP의 해법: 물량이 몰리는 걸 실시간으로 감지해, 강남 지점의 임시 분점을 다른 한가한 센터에 즉석에서 개설하고 상자를 반씩 나눠 보낸다(=인기 전문가 복제). 결과적으로 모든 센터가 정확히 같은 수의 상자를 처리한다. 이것이 "dynamic redundant experts(동적 중복 전문가)"다.
분점에서 처리한 실적(=gradient)은 하루 끝에 본점으로 합산해서 돌려준다. 그래서 학습 결과는 원래 강남 지점 하나로 학습한 것과 정확히 똑같다.
즉 MoonEP는 모델을 바꾸지 않는다. 라우팅 결과(어느 토큰이 어느 전문가로 가는지)는 그대로 두고, GPU 사이에 토큰을 실어 나르는 "통신 계층"만 갈아 끼운다. 학습 프레임워크 입장에서는 dispatch → 전문가 계산 → combine이라는 익숙한 3단계를 그대로 쓰되, 그 안에서 부하가 마법처럼 평평해진다.
2025년 DeepSeek가 DeepEP를 공개하면서 "MoE 통신 라이브러리"라는 카테고리가 사실상 표준이 됐다. MoonEP는 그 후속 경쟁자로, README가 처음부터 끝까지 DeepEP v2와의 비교로 채워져 있다. 주목 포인트는 세 가지다.
DeepEP는 부하가 몰리면(maxvio 증가) 반복 시간이 계속 올라가고, 활성 텐서 모양이 매 스텝 바뀌어 GPU 메모리가 조각나다가 결국 OOM(메모리 초과)으로 학습이 죽는다. MoonEP는 모든 GPU가 항상 정확히 S × K 토큰만 계산하므로 쏠림이 커져도 반복 시간이 평평하고, 메모리 모양이 완전히 고정이라 조각화도 OOM도 없다. H20 8장(EP=8) 실험에서 이 "평평함"을 그래프로 보여준다.
기존 방식은 토큰을 통신 버퍼로 받은 뒤, 전문가별로 다시 정렬(permute)해 사용자 버퍼로 복사한다. 이 복사가 뒷단(epilogue)의 시간을 잡아먹는다. MoonEP는 토큰을 원격 GPU의 "최종 전문가 위치"에 곧바로 써넣고, 그 통신 버퍼의 뷰(view)를 그대로 전문가 계산에 넘긴다. permute-in도 permute-out도, 버퍼 복사도 없다. 그래서 복제 전문가·가중치 프리페치 같은 추가 커널을 다 얹고도 총 dispatch 시간이 DeepEP의 dispatch만큼이고, combine은 모든 구간에서 더 빠르다고 한다.
토큰 개수가 스텝마다 바뀌면 CPU(호스트)가 매 MoE 레이어에서 "이번엔 몇 개지?"를 GPU에 물어보며 동기화해야 한다(느림). MoonEP는 버퍼 크기가 항상 고정 S × K라 모양이 컴파일 시점에 정해져, 레이어마다 호스트 동기화가 사라진다. 이게 실제 학습 처리량에 큰 영향을 준다.
DeepEP가 "빠른 배송 트럭"이라면, MoonEP는 "물량이 어디로 몰리든 모든 창고를 똑같이 채우도록 노선을 실시간 재설계하는 물류 시스템"이다. 트럭 속도 경쟁이 아니라, 애초에 정체가 안 생기게 판을 다시 짠 것이다.
덧붙여 이 레포가 MIT 라이선스에 코드가 완전 공개라는 점, 그리고 Moonshot이 방금 Kimi-K3라는 초거대 MoE를 낸 직후라는 맥락이 관심을 키웠다. "Kimi를 실제로 어떻게 그렇게 싸게 돌리는가"에 대한 실물 증거이기 때문이다.
MoonEP는 보통의 파이썬 라이브러리처럼 from moonep import Buffer로 쓰지만, 실제 일은 전부 GPU 위에서 도는 커널이 한다. 스택은 크게 3층이다.
csrc/)
bindings.cu + nvl_shared_buffer.cuh 딱 두 파일. 하는 일은 오직 하나, 여러 GPU의 메모리를 하나의 주소 공간처럼 잇는 것이다. CUDA의 저수준 VMM(Virtual Memory Management) API로 각 GPU 메모리를 POSIX 파일 디스크립터(fd)로 내보내고(nvl_dist_alloc), 다른 GPU가 그 fd를 받아 자기 주소 공간에 매핑한다(nvl_dist_map). 여기에 NVSwitch의 멀티캐스트(SHARP) 지원까지 얹는다.
moonep/planning.py, dispatch.py 등)
놀랍게도 GPU 커널의 대부분이 파이썬으로 쓰여 있다. NVIDIA의 CuTe DSL(nvidia-cutlass-dsl)을 쓰기 때문이다. 파이썬 문법으로 GPU 커널을 기술하면 컴파일러가 MLIR을 거쳐 실제 CUDA 커널로 낮춘다. planning.py(1,316줄)가 가장 크고, 부하 계획·중복 전문가 선정 같은 핵심 알고리즘이 전부 여기 들어 있다.
moonep/api.py, buffer.py)
프레임워크가 실제로 부르는 얼굴. Buffer 클래스 하나에 dispatch / combine / prefetch_weight / reduce_grad 네 메서드가 걸려 있다. api.py(1,083줄)가 이들을 오케스트레이션하고, buffer.py(287줄)가 대칭 메모리 할당·정렬을 담당한다.
| 계층 | 대표 파일 (줄 수) | 역할 |
|---|---|---|
| API | api.py (1083) | dispatch/combine/prefetch/reduce_grad 오케스트레이션 |
| API | buffer.py (287) | NVLink 대칭 메모리 할당·VMM 정렬·multicast |
| 커널 | planning.py (1316) | 온라인 부하 계획 + 중복 전문가 선정 (Phase A~D) |
| 커널 | dispatch.py (984) | 토큰을 원격 전문가 위치로 직송 (제로 카피) |
| 커널 | combine.py (654) | 전문가 출력을 토큰 순서로 되모음 |
| 커널 | grad_reduce.py (539) | 복제 전문가 gradient를 본점 rank로 합산 |
| 커널 | prefetch.py (385) | 가중치를 프리페치 슬롯으로 미리 당겨오기 |
| C++ | nvl_shared_buffer.cuh (418) | VMM alloc/map·multicast bind — GPU 메모리 잇기 |
| 테스트 | tests/*.py (약 2,700) | torchrun 8-GPU 다중 프로세스 커널 검증 |
주목할 점: 외부 의존성이 극단적으로 적다. setup.py의 install_requires는 nvidia-cutlass-dsl==4.4.2 하나뿐이고, 나머지는 PyTorch와 CUDA 툴체인이 전부다. 프레임워크에 종속되지 않은 순수 통신 부품으로 설계됐다는 뜻이다.
MoonEP의 한 MoE 레이어는 아래 흐름으로 돈다. 왼쪽이 순전파(forward), 오른쪽이 역전파(backward)다.
가장 비싼 연산(어디에 무엇을 복제할지 결정)은 순전파에서 딱 한 번 planning 커널이 계산한다. 그 결과 plan(MoonEPCommPlan)을 저장해 두고, prefetch·combine·양쪽 backward가 전부 이 plan을 그대로 재사용한다. backward에서는 planning을 다시 돌리지 않으므로 공짜다.
dst(목적지, 복제는 음수로 인코딩), cu_seqlens(전문가별 토큰 경계), experts_to_copy, zero_fill_ranges 등이 들어 있다. 한 번 만들면 그 레이어의 forward·backward 4개 통신이 공유한다.
버퍼는 항상 NvS(= S × K + 정렬 패딩) 크기로 잡힌다. 쏠림이 아무리 심해도 이 숫자는 안 변한다. 그래서 텐서 모양이 컴파일 시점에 확정되고, 메모리 조각화·호스트 동기화·OOM이 원천 차단된다. 대가는 약간의 패딩 낭비뿐이다 — MoonEP는 "예측 가능성"을 위해 이 낭비를 기꺼이 받아들인다.
학습(training)에서는 복제 전문가가 만든 gradient가 임시라서, 프레임워크의 정상 gradient와 섞이면 안 된다. MoonEP는 복제분을 별도 reduce buffer에 모아 두고, reduce_grad에서 각 rank가 자기 전문가의 조각들을 모든 rank의 reduce buffer에서 NVLink로 읽어와(remote read) 로컬 gradient에 더한다. 그래서 학습 결과는 복제가 전혀 없던 것과 수학적으로 동일하다.
MoonEP는 복제 전문가만큼 계산을 더 한다(중복이니까). 그런데도 전체가 빨라지는 이유는, 그 대가로 "가장 느린 rank가 전체를 잡아끄는" 최악의 병목을 없애기 때문이다. 평균이 아니라 최악(tail)을 잡는 것이 이 라이브러리의 철학이다.
프리페치 슬롯 수 B는 학습 시 반드시 B = E/R(전문가수 ÷ rank수)여야 group GEMM이 건드리는 모든 전문가가 로컬이 된다. 추론(inference)에서는 gradient가 없으므로 B = 3~4로 줄여도 되고, 슬롯이 모자라면 원격 전문가 가중치를 대칭 메모리로 직접 읽어(약간 느림) 정확성은 유지된다.
파일 구성이 곧 파이프라인 순서다: planning → dispatch(+epilogue) → prefetch → (전문가 계산) → combine(+prologue) → grad_reduce. 코드를 읽을 때 이 순서대로 따라가면 데이터가 어떻게 흐르는지 자연스럽게 잡힌다. planning_reference.py가 커널의 "정답"을 순수 파이썬으로 다시 구현해 둔 것도 학습자에게 큰 선물이다 — 복잡한 GPU 커널이 이해 안 되면 이 참조본과 대조하면 된다.
MoonEP는 규모가 작지만(1만2천 줄) 밀도가 높다. 여기서 건질 수 있는 개념을 난이도 순으로 정리한다.
논문에서 "load balancing loss" 한 줄로 넘어가던 개념이, 여기서는 "가장 바쁜 rank가 전체를 잡아끈다"는 물리적 병목으로 구체화된다. maxvio 정의와 벤치마크 그래프를 보면 쏠림이 성능에 얼마나 치명적인지 수치로 감이 잡힌다.
MoE 병렬화의 심장은 all-to-all 통신이다. 토큰을 흩뿌리고(dispatch) 다시 모으는(combine) 이 왕복은 분산 학습의 핵심 패턴이며, MoonEP의 API가 이를 가장 깔끔하게 보여준다. zero_copy=True 옵션으로 "복사를 없앤다"는 최적화의 실체도 코드로 확인할 수 있다.
csrc/는 "GPU들이 서로 메모리를 직접 읽는다"는 개념을 CUDA VMM API로 구현한 가장 짧고 명료한 예제다. cuMemGetAllocationGranularity, POSIX fd IPC, NVSwitch 멀티캐스트(SHARP)까지 실무 코드로 만난다.
"커널 = C++"이라는 고정관념을 깨는 자료. 배리어·원자적 연산·비트 조작(_common.py)까지 파이썬 함수로 표현돼 있어, 최신 GPU 프로그래밍의 흐름을 엿볼 수 있다.
tests/planning_reference.py는 GPU 없이도 돌아가는 순수 파이썬 계획 알고리즘이다. 이걸 먼저 읽고 "어떤 전문가를 왜 복제하는지"의 로직을 CPU에서 재현해 본 뒤, 대응하는 planning.py의 GPU 커널과 한 줄씩 대조하면 커널 코드가 훨씬 쉽게 읽힌다.
MoonEP는 태생이 데이터센터급 인프라다. README와 테스트가 요구하는 최소 환경은 다음과 같다.
| 항목 | 요구사항 |
|---|---|
| GPU | NVIDIA GPU 다수 (테스트·벤치마크는 8장 기준, EP=8) |
| 인터커넥트 | NVLink 필수 (멀티캐스트는 NVSwitch SHARP 지원 시) |
| 실측 장비 | README 벤치마크는 H20 8장에서 측정 |
| 런처 | torchrun --nproc_per_node=8 다중 프로세스 |
| 소프트웨어 | PyTorch + CUDA 툴체인(nvcc, C++20), nvidia-cutlass-dsl==4.4.2 |
| 기타 | Zhenwu PPU 지원은 "검토 중(coming soon)" |
테스트조차 "requires multiple GPUs + NVLink"라고 못 박혀 있다. NVLink로 묶인 GPU 서버가 없다면 코드 읽기·reference 구현 재현·설계 이해가 현실적인 학습 경로다. 클라우드에서 멀티 GPU 인스턴스를 잠깐 빌려 e2e 테스트를 돌려보는 것도 방법이지만 비용이 든다.
figure/comm_vs_deepep.png와 e2e_vs_deepep.png를 열어, x축(maxvio)이 커질 때 두 라이브러리의 곡선이 어떻게 갈라지는지 말로 설명해 보자. "왜 MoonEP는 평평한가"를 3문장으로 요약하면 핵심을 잡은 것이다.
tests/generate_topk_routing.py로 인위적으로 쏠린 라우팅을 만들고, planning_reference.py에 넣어 "어떤 전문가가 어디로 복제됐는지"를 출력해 보자. 쏠림 강도(maxvio)를 바꿔가며 복제 개수가 어떻게 변하는지 관찰한다. GPU 불필요.
개념 실습: 임의의 [S, H] 텐서를 dispatch → (아무 계산 안 함) → combine 했을 때 원본과 같아야 한다. 이 "왕복 = 항등"을 NumPy로 흉내 내는 미니 버전을 짜 보면, permute/unpermute의 대칭성이 손에 잡힌다.
멀티 GPU 서버가 있다면 benchmarks/bench_comm.py를 zero_copy=True/False로 각각 돌려 combine epilogue의 복사 비용이 실제로 얼마나 되는지 재 보자.
작은 MoE 레이어(전문가 몇 개)를 직접 만들고 README의 API 워크스루대로 dispatch→FFN→combine과 backward(reduce_grad 포함)를 연결해, gradient가 복제 없이 학습한 것과 일치하는지 수치로 검증한다. 이 레포를 진짜로 "쓸 수 있게" 되는 관문이다.
| 주차 | 주제 | 학습 내용 |
|---|---|---|
| 1주차 | MoE 기초 | Mixture-of-Experts, top-k 라우팅, 공유 전문가, load balancing loss. Mixtral·DeepSeek-MoE 논문 개요 |
| 2주차 | 병렬화 전략 | 데이터/텐서/파이프라인/전문가 병렬의 차이. all-to-all 통신이 왜 EP의 병목인가 |
| 3주차 | GPU 인터커넥트 | NVLink·NVSwitch·SHARP 멀티캐스트, CUDA VMM/IPC, 대칭 메모리 개념. DeepEP 소스와 비교 |
| 4주차 | 커널 프로그래밍 | CUTLASS/CuTe DSL, TMA·클러스터·grid barrier, group GEMM. MoonEP planning.py 정독 |
위에서 아래로 내려가는 걸 추천한다. "MoE가 왜 쏠리는가(1주)" → "쏠리면 왜 느린가(2주)" → "GPU들이 어떻게 데이터를 주고받는가(3주)" → "그 통신을 커널로 어떻게 쓰는가(4주)". 각 주차의 끝에서 MoonEP의 대응 파일을 한 번씩 열어 보면, 추상 개념과 실제 코드가 계속 연결된다.
| 키워드 | 뜻 |
|---|---|
| EP | Expert Parallelism. 전문가들을 여러 GPU에 나눠 배치하는 병렬화 |
| dispatch / combine | 토큰을 원격 전문가로 보내기 / 계산 결과를 토큰 순서로 되모으기 (MoE 통신의 왕복) |
| redundant experts | 과부하 전문가를 실시간 복제해 한가한 rank에 배치 — MoonEP의 핵심 |
| maxvio | 부하 쏠림 정도. 0이면 완벽 균등, 클수록 쏠림 심함 |
| zero copy | 통신 버퍼를 그대로 계산에 넘겨 중간 복사를 없애는 최적화 |
| static shapes | 버퍼 크기가 항상 고정(S×K)이라 호스트 동기화·OOM 제거 |
| S / K / E / R / B | rank당 토큰수 / top-k / 총 전문가수 / rank 수 / rank당 프리페치 슬롯 |
| NvS | dispatch된 토큰 슬롯 수 (S×K + VM 그룹별 패딩) |
| cu_seqlens | 전문가별 토큰 구간의 누적 경계 배열 (group GEMM이 구간 자를 때 사용) |
| symmetric memory | 모든 rank가 같은 레이아웃 버퍼를 갖고 서로 직접 read/write |
| VMM | CUDA Virtual Memory Management. 물리 메모리를 fd로 내보내 주소 공간 매핑 |
| SHARP | NVSwitch의 in-network 멀티캐스트/리듀스 가속 기능 |
| CuTe DSL | CUTLASS의 파이썬 커널 기술 언어 (MLIR로 낮춰 CUDA 커널 생성) |
| DeepEP | DeepSeek의 EP 통신 라이브러리 — MoonEP의 비교 기준 |
| group GEMM | 여러 전문가의 행렬곱을 한 번에 묶어 처리하는 연산 |
cuMemCreate/cuMemMap 계열