3. MoE Expert LoRA (Dense → MoE 변환)
개요
moe_expert_lora는 EulerForge에서 가장 복잡하고 강력한 전략입니다. Dense 모델의 FFN 전체를 MoEFFN(E개의 전문가 복사본 + 라우터)으로 교체한 뒤, 각 전문가 내부 Linear에 LoRA를 주입합니다. 즉, Dense 모델을 MoE 모델로 구조 변환합니다.
- 적합한 용도: Dense-to-MoE 변환, 전문가 특화 학습, 고성능 파인튜닝
- 호환 모델: Qwen, LLaMA, Gemma 3 (Dense 모델만)
- 참조 프리셋:
configs/presets/qwen3.5_0.8b_moe_expert_lora_sft.yml - mixture_lora와의 핵심 차이: "LoRA만 MoE화" vs "FFN 전체를 MoE로 변환 + LoRA"
사전 요구 사항
- EulerForge 설치 완료 (시작 가이드 참조)
- 데이터 전처리 완료 (
data/sft_10k_raw.jsonl생성) mixture_lora튜토리얼을 먼저 읽으면 이해에 도움됩니다.- 충분한 VRAM (FFN이 E번 복제되므로 메모리 사용량 증가)
1. Where: 어디에 주입되는가?
이전 전략들이 FFN 내부의 개별 Linear를 타겟으로 했다면, 이 전략은 FFN 모듈 자체를 통째로 교체합니다.
탐색 과정
BackboneAdapter.find_transformer_layers(model)— 트랜스포머 블록 탐색BackboneAdapter.find_ffn_attr_name(block)— FFN 모듈의 속성명 획득 (예:"mlp")DenseMoEExpertLoRAInjection클래스가 FFN을MoEFFN으로 교체- 교체 후 각 전문가 내부 Linear에 LoRA 주입
- (선택) 어텐션 프로젝션에 일반 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_lora는 3페이즈 점진적 해동(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: ✓
│ │ │
│ └── 옵티마이저 └── 옵티마이저
│ 재구축 재구축
각 페이즈의 역할
-
페이즈 0 (라우터 웜업, 스텝 0~2000) - 라우터만 학습하여 안정적인 전문가 할당 패턴 형성 - 모든 전문가는 동일한 초기 가중치 (base FFN의 deep copy)
-
페이즈 1 (LoRA 훈련, 스텝 2000~8000) - LoRA 파라미터와 어텐션 LoRA를 학습 - 라우터는 동결 (안정된 라우팅 유지) - base FFN 가중치는 동결
-
페이즈 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 체크포인트를 로드하면:
resolved_config.json에서 injection 설정(backbone, num_experts, top_k 등) 읽기- base HF 모델 생성 →
replace_ffn_with_moe_inplace()로 MoE 구조 재구성 - LoRA가 있으면 expert 내에서만 병합 (expert/router 구조는 보존)
- 체크포인트 로드 → 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이면:
- 총 토큰: 1024
- expert 당 할당: 평균 ~512 (1024 × 2 / 4), 실제로는 불균등 분배
- expert마다 별도 FFN forward → 4번의 작은 matmul 발생
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
router_logits가 float32이면 정상 (autocast 또는 양자화된 router)source가y_buf와 동일 dtype이면 dtype contract가 정상 작동 중
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]
hist: expert별 토큰 수. 불균등하면 load balancing 조정 필요experts_ms: FFN 실행 시간이 전체 대부분이면 구조적으로 정상dispatch_ms가experts_ms에 근접하면 dispatch 오버헤드 과다
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 ...
- bf16도 느리면: 구조적 병목이 주 원인 → expert 수/top_k 조정
- int4만 느리면: 양자화 커널 오버헤드 → bf16 사용 권장
- int4가 최악이면: 4-bit 커널이 작은 batch MoE와 특히 불합
개선 옵션
| 방법 | 효과 | 적용 |
|---|---|---|
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 |
다음 단계
- 이미 MoE인 모델(Mixtral) 파인튜닝 → Native MoE Expert LoRA 튜토리얼
- 이 전략으로 DPO 훈련 → DPO 훈련 가이드