> EulerForge > 튜토리얼 > 3. MoE Expert LoRA (Dense → MoE 변환)

3. MoE Expert LoRA (Dense → MoE 변환)

개요

moe_expert_lora는 EulerForge에서 가장 복잡하고 강력한 전략입니다. Dense 모델의 FFN 전체를 MoEFFN(E개의 전문가 복사본 + 라우터)으로 교체한 뒤, 각 전문가 내부 Linear에 LoRA를 주입합니다. 즉, Dense 모델을 MoE 모델로 구조 변환합니다.


사전 요구 사항


1. Where: 어디에 주입되는가?

이전 전략들이 FFN 내부의 개별 Linear를 타겟으로 했다면, 이 전략은 FFN 모듈 자체를 통째로 교체합니다.

탐색 과정

  1. BackboneAdapter.find_transformer_layers(model) — 트랜스포머 블록 탐색
  2. BackboneAdapter.find_ffn_attr_name(block) — FFN 모듈의 속성명 획득 (예: "mlp")
  3. DenseMoEExpertLoRAInjection 클래스가 FFN을 MoEFFN으로 교체
  4. 교체 후 각 전문가 내부 Linear에 LoRA 주입
  5. (선택) 어텐션 프로젝션에 일반 LoRA 적용

타겟 모듈

영역 동작 결과
FFN (전체) setattr(block, "mlp", MoEFFN(...)) FFN → MoEFFN 교체
FFN 내부 Linear inject_lora_by_keywords_inplace(expert) 각 전문가 Linear에 LoRA
Attention inject_lora_by_keywords_inplace(block) LoRALinear 래핑

관련 설정

backbone: qwen3

injection:
  strategy: moe_expert_lora
  target_keywords: [gate_proj, up_proj, down_proj]  # 전문가 내부 LoRA 타겟
  start_layer: 0
  num_layers: 0
  attn_lora:
    enabled: true
    keywords: [q_proj, v_proj]

2. What: 무엇이 주입되는가?

2단계 변환이 순차적으로 수행됩니다.

단계 1: FFN → MoEFFN 교체

원본 FFN이 E개의 deep copy로 복제되고, 라우터가 추가됩니다.

변환 전:
  [block]
    └── [mlp (FFN)]
          ├── gate_proj: Linear
          ├── up_proj: Linear
          └── down_proj: Linear

변환 후:
  [block]
    └── [MoEFFN]
          ├── router: Linear(hidden_size → num_experts)     ← 학습 대상
          ├── expert[0]: FFN (deep copy)
          │     ├── gate_proj: Linear
          │     ├── up_proj: Linear
          │     └── down_proj: Linear
          ├── expert[1]: FFN (deep copy)
          ├── expert[2]: FFN (deep copy)
          └── expert[3]: FFN (deep copy)

단계 2: 각 전문가 내부에 LoRA 주입

lora_r > 0이면, 각 전문가 FFN 내부의 target_keywords 매칭 Linear에 LoRA를 주입합니다.

변환 후 (LoRA 주입 완료):
  [MoEFFN]
    ├── router: Linear(hidden → E)
    ├── expert[0]:
    │     ├── gate_proj: LoRALinear (base frozen + lora_A/B)
    │     ├── up_proj: LoRALinear
    │     └── down_proj: LoRALinear
    ├── expert[1]: (동일 구조)
    ├── expert[2]: (동일 구조)
    └── expert[3]: (동일 구조)

MoEFFN Forward 동작

[입력 x]
    │
    ├── router(x) → logits (batch, E)
    │   └── softmax → gate_prob (batch, E)
    │       └── top-k 선택 → 가중치 w_k, 인덱스 idx_k
    │
    └── 선택된 전문가만 실행:
        y = Σ (w_k * expert[idx_k](x))

각 expert(x)는 전체 FFN forward를 수행:
  expert(x) = down_proj(act_fn(gate_proj(x)) * up_proj(x))
  (gate_proj, up_proj, down_proj 각각에 LoRA가 적용됨)

관련 설정

injection:
  strategy: moe_expert_lora
  lora_r: 48                       # 전문가 내부 LoRA 랭크
  lora_alpha: 96                   # 스케일링 팩터
  lora_dropout: 0.05               # LoRA 드롭아웃
  num_experts: 4                   # FFN 전문가 복제 수 (E)
  top_k: 2                         # 토큰당 활성 전문가 수 (K)

메모리 주의: 원본 FFN이 E번 복제되므로, num_experts=4이면 FFN 파라미터가 약 4배가 됩니다. VRAM이 부족하면 num_experts를 줄이거나 양자화(model.load_precision.mode: int4)를 사용하세요.


3. When: 언제 어떤 파라미터를 훈련하는가?

moe_expert_lora3페이즈 점진적 해동(progressive unfreeze) 스케줄을 사용합니다.

페이즈 구성

training:
  phases:
    - step: 0                       # 페이즈 0: 라우터 웜업
      trainable: ["router"]
    - step: 2000                    # 페이즈 1: LoRA 훈련
      trainable: ["lora", "attn_lora"]
    - step: 8000                    # 페이즈 2: 전체 해동
      trainable: ["lora", "attn_lora", "router", "base_ffn"]
      base_ffn_keywords: ["gate_proj", "up_proj", "down_proj"]

팁: router 지속 학습 — 위 예시에서 페이즈 1은 LoRA만 학습하고 router는 freeze됩니다. router를 계속 학습하려면 trainable: ["router", "lora", "attn_lora"]로 지정하세요. trainable은 해당 phase에서 학습할 전체 목록이므로, 이전 phase의 그룹을 유지하려면 명시적으로 포함해야 합니다. mixture_lora도 동일합니다.

타임라인

Step 0 ──────> Step 2000 ──────> Step 8000 ──────────────> Step 15000
  │                │                  │
  │ 페이즈 0       │ 페이즈 1         │ 페이즈 2
  │ [router 웜업]  │ [LoRA 훈련]      │ [전체 해동]
  │                │                  │
  │ router: ✓      │ router: ✗        │ router: ✓
  │ lora: ✗        │ lora: ✓          │ lora: ✓
  │ attn_lora: ✗   │ attn_lora: ✓     │ attn_lora: ✓
  │ base_ffn: ✗    │ base_ffn: ✗      │ base_ffn: ✓
  │                │                  │
  │                └── 옵티마이저     └── 옵티마이저
  │                    재구축             재구축

각 페이즈의 역할

  1. 페이즈 0 (라우터 웜업, 스텝 0~2000) - 라우터만 학습하여 안정적인 전문가 할당 패턴 형성 - 모든 전문가는 동일한 초기 가중치 (base FFN의 deep copy)

  2. 페이즈 1 (LoRA 훈련, 스텝 2000~8000) - LoRA 파라미터와 어텐션 LoRA를 학습 - 라우터는 동결 (안정된 라우팅 유지) - base FFN 가중치는 동결

  3. 페이즈 2 (전체 해동, 스텝 8000~15000) - 모든 학습 가능 그룹을 동시 훈련 - base_ffn 그룹도 해동: 전문가 FFN의 원본 가중치도 미세 조정 - base_ffn_keywords필수: 어떤 가중치를 해동할지 지정

base_ffn_keywords 설명

base_ffn 그룹을 페이즈에 포함할 때, 반드시 base_ffn_keywords를 지정해야 합니다. 이 키워드와 매칭되는 파라미터만 해동됩니다.

# 페이즈별 지정
- step: 8000
  trainable: ["lora", "attn_lora", "router", "base_ffn"]
  base_ffn_keywords: ["gate_proj", "up_proj", "down_proj"]

또는 글로벌로 지정할 수도 있습니다:

training:
  phases:
    - step: 8000
      trainable: ["lora", "attn_lora", "router", "base_ffn"]
  phase_target_kwargs:
    base_ffn_keywords: ["gate_proj", "up_proj", "down_proj"]
    target_layers: [8, 9, 10, 11]    # 선택: 특정 레이어만 해동 (생략 시 injection 범위에서 자동 추출)

참고: target_layers를 생략하면 injection.start_layer/injection.num_layers에서 자동 추출됩니다. MoE가 주입된 레이어만 dequantize/unfreeze하므로, 대부분의 경우 명시적 지정이 불필요합니다.


4. MoE 안정성 설정

moe_expert_lora 전략은 moe 섹션이 필수입니다. mixture_lora와 동일한 구성을 사용합니다.

moe:
  router_z_loss_coef: 0.001        # 라우터 로짓 오버플로우 방지
  load_balance:
    type: aux_loss                  # 보조 손실 기반 부하 분산
    aux_loss_coef: 0.01             # 보조 손실 가중치
  router_dtype: float32             # 라우터 연산 정밀도
파라미터 역할 권장값
router_z_loss_coef softmax 오버플로우 방지 (ST-MoE 논문) 0.001
load_balance.type 전문가 부하 분산 방식 aux_loss
load_balance.aux_loss_coef 부하 분산 보조 손실 가중치 0.01
router_dtype 라우터 softmax 연산 정밀도 float32

5. 전체 설정 파일 해설

configs/presets/qwen3.5_0.8b_moe_expert_lora_sft.yml 전문:

# ── 모델 정보 ──
device: cuda:0                              # GPU 디바이스
backbone: qwen3                             # [Where] Qwen3Adapter
model_name: Qwen/Qwen3.5-0.8B-Base           # HuggingFace 모델 ID

# ── 인젝션 설정 ──
injection:
  strategy: moe_expert_lora                 # [What] Dense-to-MoE 변환 + Expert LoRA
  lora_r: 48                                # [What] 전문가 내부 LoRA 랭크
  lora_alpha: 96                            # [What] 스케일링 (96/48 = 2.0)
  lora_dropout: 0.05                        # [What] LoRA 드롭아웃
  num_experts: 4                            # [What] FFN 전문가 수 (4배 복제)
  top_k: 2                                  # [What] 토큰당 활성 전문가
  target_keywords: [gate_proj, up_proj, down_proj]  # [Where] LoRA 타겟 키워드
  start_layer: 0                            # [Where] 시작 레이어
  num_layers: 0                             # [Where] 0 = 전체
  attn_lora:                                # [Where] 어텐션 LoRA
    enabled: true
    keywords: [q_proj, v_proj]

# ── MoE 안정성 설정 ──
moe:
  router_z_loss_coef: 0.001                 # z-loss
  load_balance:
    type: aux_loss                          # 부하 분산
    aux_loss_coef: 0.01
  router_dtype: float32                     # 라우터 정밀도

# ── 훈련 설정 ──
training:
  type: sft                                 # SFT 훈련
  phases:                                   # [When] 3페이즈 점진적 해동
    - step: 0                               # 페이즈 0: 라우터 웜업
      trainable: ["router"]
    - step: 2000                            # 페이즈 1: LoRA 훈련
      trainable: ["lora", "attn_lora"]
    - step: 8000                            # 페이즈 2: 전체 해동
      trainable: ["lora", "attn_lora", "router", "base_ffn"]
      base_ffn_keywords: ["gate_proj", "up_proj", "down_proj"]
  lr: 1.0e-5
  weight_decay: 0.01
  warmup_steps: 200
  max_train_steps: 15000                    # 3페이즈에 충분한 스텝
  batch_size: 4
  grad_accum_steps: 4
  max_grad_norm: 1.0
  log_steps: 50
  save_steps: 1000
  val_steps: 500

6. 체크포인트 저장 구조

훈련 완료 시 N개 expert FFN + router 구조가 그대로 저장됩니다. 이것이 mixture_lora와의 가장 중요한 차이입니다.

기본 저장 (handoff 없음)

각 expert의 base weight와 LoRA가 모두 저장됩니다.

체크포인트 구조:
├── layer.N.mlp.router.weight                          ← MLP 레벨 라우터
├── layer.N.mlp.experts.0.gate_proj.base_layer.weight   ← Expert 0 base weight
├── layer.N.mlp.experts.0.gate_proj.lora_A              ← Expert 0 LoRA
├── layer.N.mlp.experts.0.gate_proj.lora_B
├── layer.N.mlp.experts.0.up_proj.base_layer.weight
├── layer.N.mlp.experts.0.up_proj.lora_A
├── layer.N.mlp.experts.0.up_proj.lora_B
├── layer.N.mlp.experts.0.down_proj.*                   ← 동일 패턴
├── layer.N.mlp.experts.1.*                             ← Expert 1 (독립 weight)
├── layer.N.mlp.experts.2.*                             ← Expert 2
├── layer.N.mlp.experts.3.*                             ← Expert 3
└── (attn_lora: 단일 LoRA 패턴)

Handoff 저장 (end_scale=0) — Experimental

Handoff는 연구/고급 기능입니다. DPO와 비호환. 상세: 14_lora_handoff.md

LoRA가 base에 병합된 후 제거됩니다. Expert + router 구조는 유지됩니다.

체크포인트 구조:
├── layer.N.mlp.router.weight                          ← 라우터 유지
├── layer.N.mlp.experts.0.gate_proj.weight              ← Expert 0 (LoRA 병합 완료)
├── layer.N.mlp.experts.0.up_proj.weight
├── layer.N.mlp.experts.0.down_proj.weight
├── layer.N.mlp.experts.1.*                             ← Expert 1
├── layer.N.mlp.experts.2.*                             ← Expert 2
└── layer.N.mlp.experts.3.*                             ← Expert 3
    (base_layer 접두사 없음, lora_A/B 없음)

mixture_lora와의 비교

항목 moe_expert_lora mixture_lora
expert 단위 FFN 전체 (gate+up+down_proj) LoRA branch만 (lora_A + lora_B)
base weight N개 (expert마다 독립 사본) 1개 (공유)
router 위치 MLP 레벨 (FFN 전체 선택) per-Linear (projection별 선택)
저장 후 구조 MoE 유지 (N expert + router) dense (평균 후 router 제거)
bench 추론 MoE로 추론 (expert routing 보존) dense로 추론 (expert 평균화)

핵심: moe_expert_lora는 handoff 여부에 관계없이 N개 expert + router 구조를 항상 유지합니다. Bench에서 expert를 평균하여 단일 FFN으로 축소하지 않습니다. 학습된 routing 패턴이 추론에서도 그대로 활용됩니다.

Bench 로딩

eulerforge bench에서 moe_expert_lora 체크포인트를 로드하면:

  1. resolved_config.json에서 injection 설정(backbone, num_experts, top_k 등) 읽기
  2. base HF 모델 생성 → replace_ffn_with_moe_inplace()로 MoE 구조 재구성
  3. LoRA가 있으면 expert 내에서만 병합 (expert/router 구조는 보존)
  4. 체크포인트 로드 → MoE 모델로 추론 (학습된 expert routing 그대로 사용)

상세: LoRA Handoff 튜토리얼, Bench 튜토리얼


7. 실행하기

기본 실행

eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft.yml \
    --set data.format=raw \
    --set data.task=sft \
    --set data.path=data/sft_10k_raw.jsonl \
    --set data.max_length=512

메모리 절약 실행

# 4비트 양자화 + 전문가 수 감소
eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft.yml \
    --set data.format=raw \
    --set data.task=sft \
    --set data.path=data/sft_10k_raw.jsonl \
    --set data.max_length=512 \
    --set model.load_precision.mode=int4 \
    --set injection.num_experts=2

프리플라이트 검사

3페이즈 구성이 올바른지 훈련 전 확인:

eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft.yml \
    --preflight

디버그 모드로 페이즈 전환 확인

eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft.yml \
    --debug \
    --debug-trainable-names \
    --debug-every 10 \
    --set data.format=raw \
    --set data.task=sft \
    --set data.path=data/sft_10k_raw.jsonl \
    --set data.max_length=512

8. 디버깅 및 트러블슈팅

증상 원인 해결
OOM (메모리 부족) FFN이 E번 복제되어 VRAM 크게 증가 model.load_precision.mode: int4, num_experts 감소, batch_size 감소
"requires 'base_ffn_keywords'" 페이즈에 base_ffn이 있는데 키워드 미지정 base_ffn_keywords 추가
"base_ffn_keywords is empty" 빈 리스트 지정 ["gate_proj", "up_proj", "down_proj"] 추가
페이즈 전환 안 됨 스텝이 max_train_steps 범위 밖 스텝 값 확인 (8000 < 15000 필수)
"0 parameters" 프리플라이트 에러 페이즈가 파라미터 없는 그룹 참조 --preflight로 그룹별 파라미터 수 확인
base_ffn 파라미터가 학습 안 됨 페이즈 2에 도달하지 않음 max_train_steps가 8000보다 큰지 확인
bench에서 "누락된 키 84개" / "예기치 않은 키 392개" MoE 체크포인트를 dense LoRA로 병합 시도 최신 버전에서는 MoE 자동 감지 후 MoE 구조를 재구성하여 그대로 추론 (expert 보존)
Gemma 3 4B+ token_type_ids 에러 Gemma3ForConditionalGeneration은 training 시 token_type_ids 필수 EulerForge가 자동 감지하여 torch.zeros_like(input_ids) 전달. 상세: troubleshooting
Gemma 3 4B+ Router hidden_size 불일치 model.config.hidden_size가 None (multimodal wrapper) text_config.hidden_size fallback 자동 적용. 상세: troubleshooting
GPU util 20~30%, batch_size=2 Expert별 토큰 fragmentation (작은 matmul 다발) 아래 "small batch 성능" 섹션 참조
int4/int8가 bf16보다 느림 양자화 커널 + 작은 batch MoE 결합 시 오버헤드 bf16 perf preset으로 비교 실험
index_add_(): self (BFloat16) and source (Float) MoE merge 지점에서 router/expert output dtype 불일치. autocast 또는 양자화된 router가 float32 weights를 생성 EulerForge 최신 버전으로 업데이트 (자동 dtype 정규화 적용). 디버깅: EULERFORGE_MOE_DTYPE_DEBUG=1

9. Small Batch에서의 구조적 비효율

왜 느릴 수 있는가

moe_expert_lora는 각 토큰을 top_k개 expert로 라우팅합니다. batch_size=2, seq_len=512, num_experts=4, top_k=2이면:

GPU는 큰 행렬곱에 최적화되어 있으므로, 작은 matmul 여러 번은 처리량이 낮습니다.

MoEDType 디버그

dtype 관련 문제가 의심되면 EULERFORGE_MOE_DTYPE_DEBUG=1로 merge 지점의 dtype 흐름을 확인하세요:

EULERFORGE_MOE_DTYPE_DEBUG=1 eulerforge train --preset ...

로그 예시:

[MoEDType] hidden=torch.bfloat16 router_logits=torch.float32 topk_weights=torch.float32 y_buf=torch.bfloat16 layer=31
[MoEDType] expert=0 expert_out=torch.bfloat16 weights=torch.float32 source=torch.bfloat16 y_buf=torch.bfloat16

MoEPerf 프로파일링

환경변수 EULERFORGE_MOE_PERF=1로 상세 분해를 확인하세요:

EULERFORGE_MOE_PERF=1 eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft.yml \
    --set data.format=raw --set data.task=sft --set data.path=data/sft_10k_raw.jsonl

로그 예시:

[MoEPerf] layer=... | router=0.12ms dispatch=0.05ms experts=3.45ms total=3.62ms |
  tokens=1024 avg_per_expert=512.0 nonempty=4/4 hist=[480, 520, 510, 514]

bf16 vs int4/int8 비교

perf preset 3종으로 비교 실험:

# bf16 (기준선)
EULERFORGE_MOE_PERF=1 eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft_bf16_perf.yml ...
# int8
EULERFORGE_MOE_PERF=1 eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft_int8_perf.yml ...
# int4
EULERFORGE_MOE_PERF=1 eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft_int4_perf.yml ...

개선 옵션

방법 효과 적용
batch_size 증가 expert당 토큰 증가 → GPU 활용도 ↑ YAML training.batch_size
num_experts 감소 expert당 토큰 증가 injection.num_experts: 2
bf16 모드 양자화 오버헤드 제거 model.load_precision.mode: bf16
top_k: 1 dispatch 횟수 절반 injection.top_k: 1

다음 단계