Skip to content
Published on

Sparse Mixture of Experts(MoE) 아키텍처 심층 분석: 설계 원리부터 DeepSeek-V3·Qwen3까지

Authors
  • Name
    Twitter
Sparse MoE Architecture

들어가며

대규모 언어 모델(LLM)의 파라미터 수는 기하급수적으로 증가하고 있지만, 모든 파라미터를 매 토큰마다 활성화하는 Dense 모델은 연산 비용의 벽에 부딪혔다. GPT-4 수준의 Dense 모델을 학습하려면 수만 장의 GPU가 수개월간 가동되어야 하며, 추론 비용 역시 파라미터 수에 비례하여 증가한다. 이 근본적 비효율을 해결하기 위해 조건부 연산(Conditional Computation) 패러다임이 부상했고, 그 중심에 Sparse Mixture of Experts(MoE) 아키텍처가 있다.

MoE의 핵심 아이디어는 단순하다. 수백 개의 전문가(Expert) 네트워크를 두되, 각 입력 토큰에 대해 소수의 전문가만 활성화하여 연산량을 극적으로 줄이는 것이다. 전체 파라미터 수가 모델 용량(capacity)을 결정하고, 활성 파라미터 수가 실제 연산 비용을 결정하므로, 높은 품질과 낮은 비용을 동시에 달성할 수 있다. Mixtral 8x7B는 총 47B 파라미터에서 토큰당 13B만 활성화하며, DeepSeek-V3는 671B 파라미터 중 37B만 활성화한다.

이 글에서는 MoE의 수학적 기초부터 라우팅 전략, 로드 밸런싱, Switch Transformer에서 DeepSeek-V3와 Qwen3-235B-A22B까지의 아키텍처 진화, 그리고 학습과 추론의 실전 최적화 전략을 심층적으로 다룬다. 각 주제를 PyTorch 코드와 함께 설명하며, 운영 환경에서 발생하는 실패 사례와 복구 절차까지 포함한다.

MoE 기본 구조와 수학적 원리

Sparse Activation의 수학적 정의

MoE 레이어는 N개의 전문가 네트워크 E_1, E_2, ..., E_N과 게이팅 네트워크 G로 구성된다. 입력 토큰 x에 대한 MoE 레이어의 출력 y는 다음과 같이 정의된다.

y = sum_{i=1}^{N} G(x)_i * E_i(x)

여기서 G(x)는 게이팅 함수로, 입력 x에 대해 N차원 벡터를 출력한다. Dense MoE에서는 모든 G(x)_i가 0이 아닌 값을 가지지만, Sparse MoE에서는 Top-K 전문가만 선택하고 나머지의 게이팅 값을 0으로 만든다.

G(x)_i = softmax(W_g * x + noise)_i  (if i in TopK)
G(x)_i = 0                           (otherwise)

노이즈 항은 학습 시 탐색(exploration)을 촉진하여 전문가 활용의 다양성을 높이는 역할을 한다. 이 sparsity 덕분에 전체 파라미터 수는 N에 비례하여 증가하지만, 실제 연산량(FLOPs)은 K에 비례하여 Dense 모델 대비 N/K 배 효율적이다.

전문가 네트워크의 구조

각 전문가는 일반적으로 Transformer의 Feed-Forward Network(FFN)을 대체한다. Self-Attention은 모든 토큰이 공유하고, FFN 부분만 전문가별로 분리하는 것이 표준적인 설계다. 이는 Self-Attention이 토큰 간 관계를 포착하는 전역적 역할을 하고, FFN이 개별 토큰의 표현을 변환하는 지역적 역할을 하기 때문이다.

import torch
import torch.nn as nn
import torch.nn.functional as F
from dataclasses import dataclass

@dataclass
class MoEConfig:
    d_model: int = 1024
    d_ff: int = 4096
    num_experts: int = 8
    top_k: int = 2
    dropout: float = 0.1
    aux_loss_weight: float = 0.01

class SwiGLUExpert(nn.Module):
    """SwiGLU 활성화를 사용하는 전문가 FFN.
    LLaMA, Mistral 등 최신 모델에서 표준으로 채택된 구조."""

    def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
        super().__init__()
        self.w_gate = nn.Linear(d_model, d_ff, bias=False)
        self.w_up = nn.Linear(d_model, d_ff, bias=False)
        self.w_down = nn.Linear(d_ff, d_model, bias=False)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        gate = F.silu(self.w_gate(x))
        up = self.w_up(x)
        return self.w_down(self.dropout(gate * up))

class TopKGating(nn.Module):
    """Top-K 게이팅 네트워크.
    Noisy Top-K Gating (Shazeer et al., 2017) 구현."""

    def __init__(self, d_model: int, num_experts: int, top_k: int = 2):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k
        self.gate = nn.Linear(d_model, num_experts, bias=False)
        self.noise_linear = nn.Linear(d_model, num_experts, bias=False)

    def forward(self, x: torch.Tensor):
        # x: (batch * seq_len, d_model)
        logits = self.gate(x)

        if self.training:
            noise = F.softplus(self.noise_linear(x))
            logits = logits + noise * torch.randn_like(logits)

        probs = F.softmax(logits, dim=-1)
        top_k_probs, top_k_indices = torch.topk(probs, self.top_k, dim=-1)
        # 재정규화: 선택된 전문가 가중치 합이 1이 되도록
        top_k_probs = top_k_probs / top_k_probs.sum(dim=-1, keepdim=True)

        return top_k_probs, top_k_indices, probs

위 코드에서 SwiGLUExpert는 LLaMA, Mistral, Qwen 등 최신 모델이 채택한 SwiGLU 활성화 함수를 사용하는 전문가다. 기존 ReLU나 GELU 대비 학습 효율이 높다는 것이 경험적으로 확인되었다. TopKGating은 Shazeer et al.(2017)이 제안한 Noisy Top-K Gating을 구현한 것으로, 학습 시 게이팅 로짓에 학습 가능한 노이즈를 추가하여 전문가 탐색을 촉진한다.

Dense vs Sparse MoE 정량 비교

항목Dense 70BSparse MoE 8x7B (Top-2)Sparse MoE 256x3B (Top-2)
총 파라미터70B47B768B
활성 파라미터70B13B6B
FLOPs/token140 TFLOPs26 TFLOPs12 TFLOPs
GPU 메모리 (FP16)140 GB94 GB1.5 TB
학습 비용 비율1.0x0.35x (FLOPs 기준)0.17x (FLOPs 기준)
추론 속도기준2-3x 빠름전문가 로딩 병목

주목할 점은 MoE 모델의 메모리 요구량이다. 활성 파라미터는 적지만 전체 파라미터를 메모리에 올려야 하므로, 전문가 수가 매우 많은 경우 메모리가 Dense 모델보다 오히려 더 클 수 있다. 이것이 Expert Parallelism과 Offloading 전략이 필요한 근본적 이유다.

라우팅 전략: Top-k, Expert Choice, Hash Routing

라우팅(Routing)은 MoE의 핵심이자 가장 어려운 설계 문제다. 어떤 전문가를 활성화할지 결정하는 전략에 따라 모델의 품질, 학습 안정성, 추론 효율이 크게 달라진다.

Top-K Routing

가장 전통적인 라우팅 방식으로, 게이팅 네트워크가 각 전문가에 대한 점수를 계산하고 상위 K개를 선택한다. Shazeer et al.(2017)이 Top-2를 제안했고, Switch Transformer(Fedus et al., 2022)가 Top-1으로 단순화하여 통신 비용을 절반으로 줄였다.

Top-1의 장점: 각 토큰이 정확히 하나의 전문가만 사용하므로 분산 환경에서 All-to-All 통신량이 최소화된다. 구현도 단순하다.

Top-1의 단점: 단일 전문가에 의존하므로 표현력이 제한되고, 게이팅 결정이 이산적(discrete)이라 학습 초기에 전문가 붕괴(expert collapse) 위험이 높다.

Top-2의 절충: Mixtral 8x7B와 다수의 최신 모델이 Top-2를 채택한다. 두 전문가의 출력을 가중합하므로 표현력이 풍부하고, 하나의 전문가가 불안정해도 다른 전문가가 보완한다.

Expert Choice Routing

Zhou et al.(2022)이 제안한 Expert Choice 라우팅은 관점을 전환한다. 토큰이 전문가를 선택하는 것이 아니라, 전문가가 처리할 토큰을 선택한다. 각 전문가가 자신에게 가장 적합한 토큰 K개를 선택하므로, 로드 밸런싱이 구조적으로 보장된다.

class ExpertChoiceGating(nn.Module):
    """Expert Choice Routing 구현.
    각 전문가가 처리할 토큰을 직접 선택하여
    로드 밸런싱을 구조적으로 보장한다."""

    def __init__(
        self,
        d_model: int,
        num_experts: int,
        capacity_factor: float = 1.0,
    ):
        super().__init__()
        self.num_experts = num_experts
        self.capacity_factor = capacity_factor
        self.gate = nn.Linear(d_model, num_experts, bias=False)

    def forward(self, x: torch.Tensor):
        # x: (num_tokens, d_model)
        num_tokens = x.shape[0]
        expert_capacity = int(
            num_tokens * self.capacity_factor / self.num_experts
        )

        # 게이팅 점수: (num_tokens, num_experts)
        gate_logits = self.gate(x)
        # 전문가 관점에서 점수 계산: (num_experts, num_tokens)
        gate_scores = F.softmax(gate_logits.T, dim=-1)

        # 각 전문가가 상위 capacity개 토큰 선택
        top_k_scores, top_k_indices = torch.topk(
            gate_scores, expert_capacity, dim=-1
        )  # (num_experts, capacity)

        # 디스패치 마스크 생성
        dispatch_mask = torch.zeros(
            self.num_experts, num_tokens,
            device=x.device, dtype=x.dtype,
        )
        dispatch_mask.scatter_(1, top_k_indices, top_k_scores)

        return dispatch_mask, top_k_indices, top_k_scores

Expert Choice의 핵심 장점은 보조 손실(auxiliary loss) 없이도 완벽한 로드 밸런싱을 달성한다는 것이다. 각 전문가가 동일한 수의 토큰을 처리하도록 강제되므로 전문가 붕괴 문제가 원천적으로 제거된다. 단, 하나의 토큰이 여러 전문가에게 선택되거나 아무 전문가에게도 선택되지 않을 수 있다는 비대칭성이 존재한다.

Hash Routing

Roller et al.(2021)이 제안한 Hash Routing은 학습 가능한 게이팅을 완전히 제거하고, 해시 함수로 토큰을 전문가에 배정한다. 게이팅 네트워크의 파라미터와 연산이 사라지므로 추론 시 오버헤드가 최소화된다. 그러나 고정된 배정 규칙이므로 입력 의미를 반영하지 못하고, 실전에서는 학습 가능한 라우팅 대비 품질이 낮아 주류로 채택되지는 않았다.

라우팅 전략 비교

전략로드 밸런싱표현력통신 비용구현 복잡도대표 모델
Top-1Aux Loss 필요낮음최소낮음Switch Transformer
Top-2Aux Loss 필요중간중간중간Mixtral 8x7B
Top-K (K=6,8)Aux Loss 필요높음높음중간DeepSeek-V3 (Top-8/256)
Expert Choice구조적 보장높음중간높음연구용 모델
Hash Routing완벽낮음최소최소연구용 모델

로드 밸런싱과 Auxiliary Loss

MoE 학습에서 가장 심각한 문제는 전문가 붕괴(Expert Collapse)다. 게이팅 네트워크가 소수의 전문가에만 토큰을 집중적으로 보내고, 나머지 전문가는 학습 기회를 잃어 사실상 죽은 파라미터가 되는 현상이다. 이를 방지하기 위해 다양한 로드 밸런싱 기법이 개발되었다.

Auxiliary Loss (보조 손실)

Switch Transformer가 제안한 표준적인 접근법이다. 전문가별 토큰 배분이 균등해지도록 유도하는 손실 항을 메인 언어 모델링 손실에 추가한다.

def compute_load_balancing_loss(
    gate_probs: torch.Tensor,
    top_k_indices: torch.Tensor,
    num_experts: int,
    top_k: int,
) -> torch.Tensor:
    """Switch Transformer 스타일의 로드 밸런싱 보조 손실 계산.

    Args:
        gate_probs: 게이팅 확률 (num_tokens, num_experts)
        top_k_indices: 선택된 전문가 인덱스 (num_tokens, top_k)
        num_experts: 전문가 수
        top_k: 선택되는 전문가 수

    Returns:
        보조 손실 스칼라 값
    """
    num_tokens = gate_probs.shape[0]

    # f_i: 전문가 i에 배정된 토큰의 비율
    expert_mask = F.one_hot(top_k_indices, num_experts).float()
    # (num_tokens, top_k, num_experts) -> (num_tokens, num_experts)
    expert_mask = expert_mask.sum(dim=1)
    tokens_per_expert = expert_mask.sum(dim=0)  # (num_experts,)
    f = tokens_per_expert / (num_tokens * top_k)

    # P_i: 전문가 i에 대한 평균 게이팅 확률
    P = gate_probs.mean(dim=0)  # (num_experts,)

    # 보조 손실: N * sum(f_i * P_i)
    # 균등 배분일 때 최소값을 가짐
    aux_loss = num_experts * (f * P).sum()

    return aux_loss

class MoELayerWithAuxLoss(nn.Module):
    """보조 손실이 포함된 완전한 MoE 레이어 구현."""

    def __init__(self, config: MoEConfig):
        super().__init__()
        self.config = config
        self.experts = nn.ModuleList([
            SwiGLUExpert(config.d_model, config.d_ff, config.dropout)
            for _ in range(config.num_experts)
        ])
        self.gating = TopKGating(
            config.d_model, config.num_experts, config.top_k
        )
        self.aux_loss_weight = config.aux_loss_weight

    def forward(self, x: torch.Tensor):
        batch_size, seq_len, d_model = x.shape
        x_flat = x.view(-1, d_model)

        top_k_probs, top_k_indices, gate_probs = self.gating(x_flat)

        # 보조 손실 계산
        aux_loss = self.aux_loss_weight * compute_load_balancing_loss(
            gate_probs, top_k_indices,
            self.config.num_experts, self.config.top_k,
        )

        # 전문가 출력 계산
        output = torch.zeros_like(x_flat)
        for k in range(self.config.top_k):
            expert_indices = top_k_indices[:, k]  # (num_tokens,)
            expert_weights = top_k_probs[:, k]    # (num_tokens,)

            for i in range(self.config.num_experts):
                mask = (expert_indices == i)
                if mask.any():
                    expert_input = x_flat[mask]
                    expert_output = self.experts[i](expert_input)
                    output[mask] += expert_weights[mask].unsqueeze(-1) * expert_output

        output = output.view(batch_size, seq_len, d_model)
        return output, aux_loss

Auxiliary Loss의 가중치 alpha는 매우 민감한 하이퍼파라미터다. Switch Transformer는 alpha=0.01을 권장했으나, 모델 규모와 전문가 수에 따라 조정이 필요하다. alpha가 너무 크면 언어 모델링 품질이 저하되고, 너무 작으면 로드 밸런싱 효과가 미미하다. ST-MoE(Zoph et al., 2022)는 라우터 z-loss를 추가하여 게이팅 로짓의 크기 자체를 제약하는 방법을 제안했다.

DeepSeek의 Auxiliary-Loss-Free 전략

DeepSeek-V3(2024)는 보조 손실 없이 로드 밸런싱을 달성하는 혁신적 방법을 제안했다. 각 전문가에 학습 불가능한 바이어스 항을 추가하고, 학습 중 토큰이 과도하게 집중되는 전문가의 바이어스를 낮추고 활용이 부족한 전문가의 바이어스를 높이는 동적 조정 메커니즘을 사용한다. 이 접근법은 보조 손실이 메인 학습 목적 함수에 간섭하는 문제를 완전히 제거하며, 학습 안정성과 최종 모델 품질 모두에서 이점을 보였다.

Switch Transformer에서 DeepSeek-V3까지 진화

Switch Transformer (Fedus et al., 2022)

Switch Transformer는 MoE 라우팅을 Top-1으로 단순화한 핵심 논문이다. 기존 Top-2 라우팅의 통신 비용을 절반으로 줄이면서, 적절한 capacity factor와 auxiliary loss로 학습 안정성을 확보했다. 1.6T 파라미터 모델을 T5-XXL 대비 4배 빠르게 학습하며 동등한 품질을 달성했다.

핵심 설계 결정:

  • Top-1 라우팅: 통신 비용 최소화
  • Capacity Factor: 전문가 버퍼 크기를 동적 조절하여 토큰 드롭 방지
  • Selective Precision: 게이팅은 FP32, 전문가 연산은 BF16으로 혼합하여 안정성과 효율성 동시 달성

GShard (Lepikhin et al., 2021)

Google이 제안한 GShard는 600B 파라미터 MoE 모델의 분산 학습 파이프라인을 확립했다. Top-2 라우팅과 Group-level 밸런싱을 사용했으며, SPMD(Single Program Multiple Data) 프로그래밍 모델로 수천 개의 TPU에서 효율적으로 학습할 수 있는 프레임워크를 제시했다.

Mixtral 8x7B (Jiang et al., 2024)

Mistral AI의 Mixtral은 오픈소스 MoE 모델의 실용성을 증명한 이정표다. 8개 전문가 중 Top-2를 선택하는 구조로, 총 47B 파라미터에서 활성 13B를 사용한다. LLaMA-2 70B와 동등하거나 우수한 벤치마크 성능을 보이면서, 추론 FLOPs는 70B의 1/3 수준이다.

DeepSeek-V3 (DeepSeek, 2024)

DeepSeek-V3는 MoE 아키텍처의 여러 설계 영역에서 혁신을 이뤘다.

  • Fine-grained Expert Segmentation: 256개의 소규모 전문가를 두고 Top-8을 선택한다. 전문가 수를 늘리고 크기를 줄이면 전문화(specialization)가 심화되어 모델 품질이 향상된다.
  • Shared Expert: 1개의 공유 전문가가 모든 토큰을 처리하여 공통 지식을 담당하고, 라우팅된 전문가는 특화 지식을 담당한다.
  • Auxiliary-Loss-Free Load Balancing: 앞서 설명한 바이어스 기반 동적 밸런싱으로 보조 손실의 부작용을 제거했다.
  • Multi-Token Prediction (MTP): 한 번에 여러 토큰을 예측하는 학습 목표를 추가하여 데이터 효율을 높였다.
  • FP8 학습: 671B 파라미터 모델을 2048대의 H800 GPU에서 FP8 정밀도로 학습하여 비용을 극적으로 절감했다.

Qwen3-235B-A22B (Alibaba, 2025)

Qwen3-235B-A22B는 총 235B 파라미터 중 22B만 활성화하는 MoE 모델로, 128개의 전문가 중 Top-8을 선택한다. 기존 Qwen2.5 시리즈의 Dense 아키텍처를 MoE로 전환하면서 GPT-4o 수준의 성능을 10분의 1 수준의 추론 비용으로 달성했다.

MoE 모델 비교

모델총 파라미터활성 파라미터전문가 수Top-K공유 전문가라우팅 전략
Switch Transformer1.6T약 100B1281없음Learned Top-1
GShard600B약 20B20482없음Learned Top-2
Mixtral 8x7B47B13B82없음Learned Top-2
DeepSeek-V3671B37B256+181개Bias-adjusted
Qwen3-235B-A22B235B22B1288있음Learned Top-K
DBRX132B36B164없음Learned Top-4

학습 안정성과 트러블슈팅

전문가 붕괴(Expert Collapse) 진단

전문가 붕괴는 MoE 학습에서 가장 흔하고 치명적인 문제다. 게이팅 네트워크가 특정 전문가들에만 토큰을 집중적으로 보내면, 나머지 전문가의 그래디언트가 0에 수렴하여 학습이 멈추고, 이 불균형이 자기 강화(self-reinforcing) 루프를 형성하여 악화된다.

import logging
from collections import defaultdict

logger = logging.getLogger(__name__)

class ExpertUtilizationMonitor:
    """전문가 활용 모니터링 및 붕괴 탐지 도구.

    학습 중 각 전문가의 활용률을 추적하고,
    붕괴 징후를 조기에 감지한다.
    """

    def __init__(
        self,
        num_experts: int,
        collapse_threshold: float = 0.01,
        window_size: int = 100,
    ):
        self.num_experts = num_experts
        self.collapse_threshold = collapse_threshold
        self.window_size = window_size
        self.history: list[dict[int, float]] = []

    def record(self, expert_counts: dict[int, int], total_tokens: int):
        """배치별 전문가 활용 기록."""
        utilization = {
            i: expert_counts.get(i, 0) / max(total_tokens, 1)
            for i in range(self.num_experts)
        }
        self.history.append(utilization)

        if len(self.history) > self.window_size:
            self.history = self.history[-self.window_size:]

    def detect_collapse(self) -> list[int]:
        """전문가 붕괴 탐지. 활용률이 threshold 이하인 전문가 반환."""
        if len(self.history) < self.window_size // 2:
            return []

        collapsed = []
        for expert_id in range(self.num_experts):
            recent_util = [
                h[expert_id] for h in self.history[-self.window_size:]
            ]
            avg_util = sum(recent_util) / len(recent_util)
            if avg_util < self.collapse_threshold:
                collapsed.append(expert_id)

        if collapsed:
            logger.warning(
                f"Expert collapse detected! "
                f"Experts {collapsed} have utilization below "
                f"{self.collapse_threshold:.2%}. "
                f"Consider increasing aux_loss_weight or "
                f"reinitializing collapsed experts."
            )

        return collapsed

    def get_load_imbalance_ratio(self) -> float:
        """로드 불균형 비율 계산.
        1.0이면 완벽한 균형, 값이 클수록 불균형."""
        if not self.history:
            return 0.0

        latest = self.history[-1]
        utils = list(latest.values())
        max_util = max(utils) if utils else 0
        min_util = min(utils) if utils else 0
        avg_util = sum(utils) / len(utils) if utils else 0

        if avg_util == 0:
            return float("inf")

        return max_util / avg_util

학습 불안정 원인과 대응

증상원인대응 방법
Loss spike (손실 급등)게이팅 로짓 폭발Router z-loss 추가, 게이팅 FP32 유지
전문가 붕괴Aux loss 부족, LR 과다Aux loss 가중치 증가, LR warm-up 연장
전문가 간 중복초기화 유사성전문가 직교 초기화, 다양성 정규화
토큰 드롭Capacity factor 부족Capacity factor 1.25-1.5로 증가
게이팅 진동학습률 과대게이팅 LR을 메인 LR의 0.1배로 분리

안정적 학습을 위한 하이퍼파라미터 가이드

학습 안정성을 위한 핵심 원칙은 다음과 같다. 첫째, 게이팅 네트워크의 연산은 반드시 FP32로 수행한다. BF16이나 FP16에서는 softmax의 수치 불안정으로 라우팅이 진동한다. 둘째, 학습률 워밍업을 Dense 모델보다 2-3배 길게 가져간다. 게이팅이 안정되기 전에 높은 학습률을 적용하면 전문가 붕괴가 발생한다. 셋째, 배치 크기를 가능한 한 크게 설정한다. 작은 배치에서는 게이팅의 토큰 분배가 노이즈에 민감하여 불안정하다.

추론 최적화: Expert Parallelism, Offloading

MoE 모델의 추론은 Dense 모델과 근본적으로 다른 도전을 제기한다. 활성 파라미터는 적지만 전체 파라미터를 접근 가능한 상태로 유지해야 하므로, 메모리 관리와 전문가 배치 전략이 핵심이다.

Expert Parallelism

Expert Parallelism(EP)은 전문가를 여러 GPU에 분산 배치하는 전략이다. N개의 전문가를 P개의 GPU에 분배하면, 각 GPU는 N/P개의 전문가만 저장한다. 토큰이 특정 전문가에 라우팅되면 All-to-All 통신으로 해당 GPU에 토큰을 전송하고, 연산 결과를 다시 원래 GPU로 반환한다.

import torch
import torch.distributed as dist
from typing import Optional

class ExpertParallelRouter:
    """Expert Parallelism을 위한 토큰 디스패치/수집 구현.

    각 GPU가 전문가 일부를 담당하고,
    All-to-All 통신으로 토큰을 라우팅한다.
    """

    def __init__(
        self,
        num_experts: int,
        ep_group: Optional[dist.ProcessGroup] = None,
    ):
        self.num_experts = num_experts
        self.ep_group = ep_group
        self.ep_size = dist.get_world_size(ep_group) if ep_group else 1
        self.ep_rank = dist.get_rank(ep_group) if ep_group else 0
        self.experts_per_rank = num_experts // self.ep_size

    def dispatch(
        self,
        tokens: torch.Tensor,
        expert_indices: torch.Tensor,
    ) -> tuple[torch.Tensor, torch.Tensor]:
        """토큰을 담당 GPU로 디스패치.

        Args:
            tokens: (num_tokens, d_model) 입력 토큰
            expert_indices: (num_tokens,) 각 토큰의 대상 전문가 인덱스

        Returns:
            dispatched_tokens: 이 GPU가 처리할 토큰
            recv_counts: 각 GPU에서 받은 토큰 수
        """
        # 각 GPU로 보낼 토큰 수 계산
        send_counts = torch.zeros(
            self.ep_size, dtype=torch.long, device=tokens.device
        )
        for rank in range(self.ep_size):
            start_expert = rank * self.experts_per_rank
            end_expert = start_expert + self.experts_per_rank
            mask = (expert_indices >= start_expert) & (
                expert_indices < end_expert
            )
            send_counts[rank] = mask.sum()

        # All-to-All로 수신 카운트 교환
        recv_counts = torch.zeros_like(send_counts)
        dist.all_to_all_single(
            recv_counts, send_counts, group=self.ep_group
        )

        # 토큰 정렬 및 All-to-All 전송
        sorted_indices = torch.argsort(expert_indices)
        sorted_tokens = tokens[sorted_indices]

        send_splits = send_counts.tolist()
        recv_splits = recv_counts.tolist()

        dispatched_tokens = torch.zeros(
            int(recv_counts.sum()), tokens.shape[1],
            dtype=tokens.dtype, device=tokens.device,
        )
        dist.all_to_all_single(
            dispatched_tokens, sorted_tokens,
            output_split_sizes=recv_splits,
            input_split_sizes=send_splits,
            group=self.ep_group,
        )

        return dispatched_tokens, recv_counts

Expert Offloading

GPU 메모리가 부족할 때, 비활성 전문가를 CPU 메모리나 NVMe SSD에 저장하고 필요할 때만 GPU로 로드하는 전략이다. DeepSpeed-MoE와 Mixtral의 추론 최적화에서 핵심적으로 활용된다.

Offloading의 핵심은 프리페칭(prefetching)이다. 현재 레이어의 전문가 연산과 동시에 다음 레이어에서 활성화될 전문가를 비동기적으로 GPU에 로드하면, 전문가 교체 지연을 숨길 수 있다. PCIe 4.0 x16 기준으로 약 32 GB/s의 대역폭이 가능하므로, 전문가 하나(수백 MB)를 수 밀리초 내에 전송할 수 있다.

추론 최적화 전략 비교

전략GPU 메모리추론 지연처리량적합 시나리오
Full Model on GPU최대최소최대고사양 멀티GPU 서버
Expert Parallelism분산통신 오버헤드높음멀티GPU 클러스터
CPU Offloading최소로딩 지연중간제한된 GPU 환경
NVMe Offloading최소높은 로딩 지연낮음단일 GPU 환경
Speculative Expert Prefetch중간중간높음배치 추론 서버

운영 체크리스트

MoE 모델을 프로덕션에 배포할 때 반드시 확인해야 할 항목들을 정리한다.

학습 단계

  1. 게이팅 정밀도 확인: 게이팅 네트워크의 forward/backward가 FP32로 수행되는지 검증한다. BF16 게이팅은 학습 초기에는 정상적으로 보이지만 수만 스텝 이후 불안정을 유발할 수 있다.
  2. 로드 밸런싱 메트릭 대시보드 구축: 전문가별 토큰 할당량, 최대/최소 활용 비율, 보조 손실 값을 실시간으로 모니터링한다.
  3. 체크포인트 전략: 전문가 병렬화 환경에서는 체크포인트가 GPU별로 분리 저장될 수 있다. 전체 모델을 하나로 병합(consolidate)하는 스크립트를 미리 준비한다.
  4. Capacity Factor 튜닝: 토큰 드롭률이 1% 이상이면 capacity factor를 높인다. 드롭된 토큰은 residual connection을 통해서만 전달되므로 품질이 저하된다.
  5. 전문가 붕괴 알림 설정: 특정 전문가의 활용률이 평균의 10% 이하로 떨어지면 알림을 발생시키고, 필요시 해당 전문가를 재초기화한다.

추론/배포 단계

  1. 메모리 프로파일링: 전체 파라미터의 GPU 메모리 적재 가능 여부를 확인하고, 불가능하면 EP 또는 Offloading 전략을 선택한다.
  2. 배치 크기 최적화: MoE 추론에서 배치 크기는 전문가 활용 효율에 직접적으로 영향을 미친다. 작은 배치에서는 일부 전문가만 활성화되어 GPU 활용률이 저하된다.
  3. KV Cache 관리: MoE 모델도 Attention 레이어는 Dense와 동일하므로 KV Cache 관리가 필요하다. PagedAttention(vLLM)과 결합하면 효율적이다.
  4. 라우팅 일관성 테스트: 동일 입력에 대해 동일한 전문가가 선택되는지 확인한다. 특히 Tensor Parallelism과 Expert Parallelism을 혼합할 때 수치 오차로 라우팅이 달라질 수 있다.
  5. Fallback 전략: 특정 전문가가 로딩에 실패하면 차순위 전문가로 대체하는 fallback 로직을 구현한다.
  6. A/B 테스트 파이프라인: Dense 모델 대비 MoE 모델의 품질 동등성을 서빙 환경에서 검증한다.

실패 사례와 복구

사례 1: 전문가 붕괴로 인한 품질 저하

증상: 학습 3만 스텝 이후 갑자기 벤치마크 점수가 하락한다. Loss 자체는 정상적으로 감소하지만, 생성 품질이 떨어진다.

원인 분석: 모니터링 결과, 8개 전문가 중 2개가 전체 토큰의 60% 이상을 처리하고, 3개 전문가는 활용률 2% 미만이었다. 보조 손실 가중치(alpha=0.001)가 너무 낮아 밸런싱 효과가 부족했다.

복구 절차:

  1. 전문가 붕괴 직전의 체크포인트(2만 스텝)로 롤백
  2. 보조 손실 가중치를 0.001에서 0.01로 10배 증가
  3. 붕괴된 전문가의 파라미터를 활성 전문가의 파라미터로 재초기화
  4. 게이팅 네트워크의 학습률을 메인 학습률의 0.1배로 분리 설정
  5. 재학습 후 전문가 활용률이 균등(평균 12.5% 기준 8-17% 범위)해질 때까지 모니터링

사례 2: All-to-All 통신 병목

증상: Expert Parallelism으로 64개 GPU에서 학습할 때, GPU 활용률이 40%로 급감한다. 프로파일러에서 All-to-All 통신이 전체 학습 시간의 45%를 차지하는 것으로 확인된다.

원인 분석: 네트워크 토폴로지 분석 결과, 전문가 배치가 네트워크 구조를 고려하지 않아 노드 간 통신이 과도하게 발생했다. 같은 노드 내 GPU 간 통신(NVLink, 900 GB/s)과 노드 간 통신(InfiniBand, 400 Gb/s)의 대역폭 차이가 20배 이상이었다.

복구 절차:

  1. Hierarchical All-to-All로 전환: 노드 내 통신과 노드 간 통신을 2단계로 분리
  2. 전문가 배치를 토폴로지 인지(topology-aware)로 재배치: 자주 함께 활성화되는 전문가를 같은 노드에 배치
  3. 통신-연산 중첩(overlap): 전문가 연산과 다음 배치의 토큰 디스패치를 파이프라인으로 중첩

사례 3: 추론 시 전문가 로딩 지연

증상: CPU Offloading으로 Mixtral 8x7B를 단일 GPU(24GB)에서 서빙할 때, 첫 토큰 지연(TTFT)이 5초를 초과한다.

원인 분석: 각 레이어에서 2개의 전문가를 CPU에서 GPU로 로드할 때마다 100-200ms가 소요되고, 32개 레이어를 순차적으로 처리하므로 누적 지연이 3.2-6.4초에 달한다.

복구 절차:

  1. 전문가 프리페칭 구현: 현재 레이어 처리 중 다음 레이어의 게이팅 점수를 사전 계산하고, 필요한 전문가를 비동기 로드
  2. 핫 전문가 캐싱: 활성화 빈도가 높은 상위 2-3개 전문가를 GPU에 상주시킴
  3. 전문가 가중치 양자화: INT4 양자화로 전문가 크기를 75% 축소하여 전송 시간 단축
  4. PCIe 대역폭이 병목인 경우, pinned memory 사용으로 CPU-GPU 전송을 최적화

사례 4: 학습 중 Loss Spike 발생

증상: 대규모 MoE 모델(100B 이상) 학습 시 수천 스텝마다 반복적으로 loss가 급등한다. 각 spike 이후 회복은 되지만 학습 시간이 낭비된다.

원인 분석: 게이팅 네트워크의 softmax 입력 로짓이 간헐적으로 매우 큰 값을 가져 수치 불안정을 유발한다. 특히 BF16 학습 시 게이팅 로짓의 범위가 FP32 대비 좁아 overflow가 발생하기 쉽다.

복구 절차:

  1. Router z-loss 추가로 게이팅 로짓의 크기를 직접 제약한다.
def router_z_loss(gate_logits: torch.Tensor) -> torch.Tensor:
    """ST-MoE 스타일의 Router z-loss.
    게이팅 로짓의 크기를 제약하여 수치 안정성을 높인다.

    Args:
        gate_logits: (num_tokens, num_experts) 게이팅 로짓

    Returns:
        z_loss 스칼라
    """
    log_z = torch.logsumexp(gate_logits, dim=-1)  # (num_tokens,)
    z_loss = (log_z ** 2).mean()
    return z_loss
  1. 게이팅 연산을 FP32로 강제하여 수치 안정성을 확보한다.
  2. 그래디언트 클리핑을 게이팅 네트워크에 별도로 적용(max_norm=1.0)한다.
  3. 학습률 워밍업 기간을 전체 학습의 5-10%까지 확장한다.

참고자료

  1. Fedus, W., Zoph, B., & Shazeer, N. (2022). Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity. JMLR, 23(120), 1-39. https://arxiv.org/abs/2101.03961

  2. DeepSeek-AI. (2024). DeepSeek-V3 Technical Report. https://arxiv.org/abs/2401.06066

  3. Cai, W. et al. (2024). A Survey on Mixture of Experts. https://arxiv.org/abs/2407.10671

  4. FriendliAI. (2024). MoE Models Comparison: Architectures and Performance. https://friendli.ai/blog/moe-models-comparison

  5. Zilliz. (2024). What is Mixture of Experts? A Complete Guide. https://zilliz.com/learn/what-is-mixture-of-experts

  6. Wikipedia. Mixture of Experts. https://en.wikipedia.org/wiki/Mixture_of_experts

  7. Shazeer, N. et al. (2017). Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer. ICLR 2017. https://arxiv.org/abs/1701.06538

  8. Jiang, A. Q. et al. (2024). Mixtral of Experts. https://arxiv.org/abs/2401.04088