14. LoRA Handoff 스케줄링
전제 조건: 튜토리얼 03 (FFN MoE Expert LoRA) 완료
연구/고급 기능 (Experimental): LoRA Handoff는 LoRA → base FFN 지식 전이를 위한 연구용 고급 기능입니다. 대부분의 경우 표준 3-Phase 훈련 (Handoff 없음)으로 충분합니다. 상세: checkpoint_lora_lifecycle.md §7
전략 제한:
moe_expert_lora전략에서만 사용 가능합니다.훈련 타입 제한: DPO + Handoff 조합은 구조적으로 비호환입니다. LoRA가 freeze되면 policy = reference → 학습 불가. Handoff는 SFT 또는 ORPO에서만 사용하세요.
dense_lora/mixture_lora에서는 base weight가 고정(freeze)되어 "지식 이전"이 성립하지 않습니다.
목표
moe_expert_lora 전략에서 LoRA를 점진적으로 줄여(fade) base FFN으로
지식을 이전하는 방법을 학습합니다.
- 훈련 전반부: LoRA delta로 빠르게 수렴
- 훈련 후반부: LoRA scale을 1.0 → 0.0으로 줄이면서 base FFN 학습 강화
- 최종 체크포인트: LoRA 파라미터 없이 순수 base 모델만 저장 가능
1. 왜 LoRA Handoff인가?
moe_expert_lora 전략은 FFN을 MoE로 확장하고, 각 expert에 LoRA를 감쌉니다.
이 접근법은 효율적이지만, 최종 모델에 LoRA adapter가 남아 있어:
- 추론 시 추가 연산 (base + LoRA delta)
- 체크포인트 크기 증가
- 배포 복잡도 증가
LoRA Handoff는 이 문제를 해결합니다:
- 초기에 LoRA로 빠르게 수렴 시작
base_ffnunfreeze 후 점진적으로 LoRA 영향력 감소 (lora_scale: 1.0 → 0.0)- 동시에 base_ffn LR을 올려 base weight가 LoRA의 역할을 흡수
- 최종적으로 LoRA 없는 순수 모델 획득
왜 moe_expert_lora만인가?
| 전략 | base_ffn 학습 가능? | Handoff 의미 |
|---|---|---|
dense_lora |
freeze (기본) | scale=0 → 원본 base로 복귀. 이전 없음 |
mixture_lora |
freeze (기본) | 동일. base 1개, 학습 안 됨 |
moe_expert_lora |
각 expert 사본별 unfreeze 가능 | scale↓ + base LR↑ → 지식 이전 성립 |
dense_lora/mixture_lora에서 lora_scale=0 + remove를 하면, "지식 이전"이 아니라
변화가 사라져 원본 base로 돌아가는 것입니다. 따라서 training.lora_handoff는
injection.strategy=moe_expert_lora에서만 허용됩니다.
2. 기본 설정 예시
# configs/presets/qwen3.5_0.8b_moe_expert_lora_sft_handoff.yml 기반
device: cuda:0
backbone: qwen3
model_name: Qwen/Qwen3.5-0.8B-Base
injection:
strategy: moe_expert_lora # ← 필수: handoff는 이 전략에서만 동작
lora_r: 48
lora_alpha: 96
lora_dropout: 0.05
num_experts: 4
top_k: 2
target_keywords: [gate_proj, up_proj, down_proj]
start_layer: 0
num_layers: 0
attn_lora:
enabled: true
keywords: [q_proj, v_proj]
moe:
router_z_loss_coef: 0.001
load_balance:
type: aux_loss
aux_loss_coef: 0.01
router_dtype: float32
training:
type: sft
phases:
- step: 0
trainable: ["router"] # Phase 0: 라우터 워밍업
- step: 500
trainable: ["lora", "attn_lora"] # Phase 1: LoRA 학습
- step: 4000
trainable: ["lora", "attn_lora", "router", "base_ffn"] # Phase 2: base_ffn unfreeze
base_ffn_keywords: ["gate_proj", "up_proj", "down_proj"]
# target_layers 생략 → injection.start_layer/num_layers에서 자동 추출
lr: 1.0e-5
weight_decay: 0.01
warmup_steps: 200
max_train_steps: 10000
batch_size: 4
grad_accum_steps: 4
max_grad_norm: 1.0
log_steps: 50
save_steps: 2000
val_steps: 1000
# ── LoRA Handoff ──
lora_handoff:
expert_lora:
start_step: 4000 # base_ffn unfreeze와 동시에 fade 시작
duration_steps: 4000 # 4000 스텝에 걸쳐 점진적 감소
end_scale: 0.0 # 완전 제거
curve: cosine # cosine 감쇠 (초반 완만, 후반 급격)
end_action: freeze # fade 완료 후 LoRA 파라미터 동결
attn_lora:
start_step: 5000
duration_steps: 3000
end_scale: 0.0
curve: linear
end_action: freeze
base_ffn_ramp:
start_step: 4000
end_step: 8000
start_multiplier: 1.0
end_multiplier: 3.0
export:
remove_adapters_if_zero_scale: true
전제 조건 (Preconditions)
Validator가 자동으로 검사합니다:
| 조건 | 위반 시 |
|---|---|
P1: injection.strategy = moe_expert_lora |
ConfigValidationError |
P2: training.phases에 base_ffn이 최소 1개 포함 |
ConfigValidationError |
P3: lora_handoff.attn_lora → injection.attn_lora.enabled: true |
ConfigValidationError |
3. 훈련 로그에서 Handoff 확인
Handoff가 활성화되면 훈련 로그에 다음 이벤트가 자동 출력됩니다:
[Train] LoRA Handoff Scheduler enabled: ['expert_lora', 'attn_lora', 'base_ffn_ramp', 'export']
[Handoff] Schedule: expert_lora(step 4000→6000, curve=cosine, end_scale=0.0, end_action=freeze), attn_lora(step 4000→6000, curve=linear, ...), base_ffn_ramp(step 2000→4000, LR ×1.0→×3.0)
...
[Handoff] base_ffn_ramp 시작 at step 2000 (LR ×1.0→×3.0)
...
[Handoff] base_ffn_ramp 완료 at step 4000 (LR ×3.0)
[Handoff] expert_lora fade 시작 at step 4000 (curve=cosine, end_scale=0.00, duration=2000 steps)
[Handoff] attn_lora fade 시작 at step 4000 (curve=linear, end_scale=0.00, duration=2000 steps)
...
[Phase2] Step 480/600 ... | Handoff[expert_lora=0.50, attn_lora=0.50, ffn_lr_mult=3.00]
...
[Handoff] expert_lora fade 완료 at step 6000 (scale=0.0000)
[Handoff] expert_lora frozen at step 6000 (scale=0.0000)
[Handoff] attn_lora fade 완료 at step 6000 (scale=0.0000)
[Handoff] attn_lora frozen at step 6000 (scale=0.0000)
- 스케줄 요약: 훈련 시작 시 전체 스케줄이 한 줄로 출력됩니다
- 마일스톤 이벤트: fade 시작/완료, freeze, ramp 시작/완료 시 각 1회 출력
- 주기적 상태:
log_steps마다 현재 scale과 LR multiplier가 로그 라인에 추가됩니다
4. Fade 곡선 이해
Linear Fade
lora_scale
1.0 ┤████████████
│ ╲
0.5 ┤ ╲
│ ╲
0.0 ┤ ████████
└──┬──────┬──────┬──────→ step
start mid end
균일한 속도로 감소. 단순하고 예측 가능.
Cosine Fade
lora_scale
1.0 ┤████████████╲
│ ╲
0.5 ┤ ╲
│ ╲
0.0 ┤ ╲██████
└──┬──────┬──────┬──────→ step
start mid end
초반 완만 → 후반 급격. LoRA 의존도를 부드럽게 줄이고 싶을 때 권장.
5. base_ffn_ramp (LR 보상)
LoRA가 줄어들면서 base FFN이 그 역할을 대신해야 합니다.
LoRA 기여분이 줄어드는 동안 base FFN이 이를 빠르게 흡수하지 못하면 loss spike가
발생합니다. base_ffn_ramp는 이 구간의 base_ffn LR을 점진적으로 올려 지식 전이를
가속합니다.
구간 설정 원칙
핵심: ramp 구간 = expert_lora fade 구간
expert_lora: start_step=4000, duration_steps=4000 → fade 구간 4000~8000
base_ffn_ramp: start_step=4000, end_step=8000 → 동일 구간에 정렬
ramp.start_step = expert_lora.start_step— LoRA가 줄기 시작할 때 LR 보상도 시작ramp.end_step = expert_lora.start_step + duration_steps— fade 완료와 동시에 ramp 종료- fade 완료 후에는 LoRA 기여분이 0이므로 추가 LR 보상이 불필요하며, 과도한 LR은 오히려 발산을 유발합니다
타이밍 시각화 (총 10k step):
Step 0───500───4000───────────8000──────10000
│ │ │ │ │
│ │ ├─ expert_lora fade (1.0→0.0, cosine)
│ │ ├─ base_ffn_ramp (LR ×1.0→×3.0)
│ │ │ │
Phase0 Phase1 Phase2 ├─ Handoff 완료
router LoRA base_ffn └─ 남은 2k step: base만으로 안정화
훈련 unfreeze
안티패턴
| 설정 | 문제 |
|---|---|
| ramp가 fade보다 일찍 끝남 (예: 4k~6k) | fade 후반부에서 LR 보상 없이 LoRA가 계속 줄어듦 → loss spike |
| ramp가 fade보다 늦게 끝남 (예: 4k~10k) | fade 완료 이후에도 LR이 계속 올라감 → 발산 위험 |
| end_multiplier ×5 이상 | gradient explosion 위험 (권장: ×2~×3) |
YAML 예시
training:
phases:
- step: 0
trainable: ["router"]
- step: 500
trainable: ["lora", "attn_lora"]
- step: 4000
trainable: ["lora", "attn_lora", "router", "base_ffn"] # ← base_ffn 필수
base_ffn_keywords: ["gate_proj", "up_proj", "down_proj"]
lora_handoff:
expert_lora:
start_step: 4000
duration_steps: 4000 # fade 구간: 4000~8000
end_scale: 0.0
curve: cosine
end_action: freeze
base_ffn_ramp:
start_step: 4000 # expert_lora fade와 동시에 시작
end_step: 8000 # expert_lora fade 완료와 동시에 종료
start_multiplier: 1.0 # 기존 LR 유지
end_multiplier: 3.0 # fade 구간 동안 3배까지 점진 증가
주의:
base_ffn_ramp를 사용하려면training.phases에서base_ffn그룹이 활성화되어 있어야 합니다 (P2).
Validator 자동 검증
validate_config()가 다음을 자동으로 검사합니다:
| 검사 | 수준 | 설명 |
|---|---|---|
| fade start < base_ffn phase | 에러 | base_ffn이 아직 동결 상태에서 fade가 시작됨 |
| fade start ≥ max_train_steps | 에러 | fade가 실행되지 않음 |
| ramp가 fade보다 일찍 끝남 | 경고 | fade 후반부에 LR 보상 없음 → loss spike 위험 |
| ramp가 fade보다 늦게 끝남 | 경고 | 불필요한 LR 부스트 → 발산 위험 |
| fade/ramp가 max_train_steps 초과 | 경고 | 훈련 내에 완료되지 않음 |
잘못된 설정 시 훈련 시작 전 즉시 에러 또는 경고가 출력됩니다.
6. Export 옵션
remove_adapters_if_zero_scale: true
훈련 완료 시 lora_scale=0.0인 LoRA 모듈을 plain nn.Linear로 교체합니다.
결과: LoRA 파라미터가 없는 순수 모델 저장.
merge_adapters_on_export: true
LoRA delta를 base weight에 merge한 후 plain nn.Linear로 교체합니다.
end_scale > 0일 때 유용 (부분적 LoRA를 base에 흡수).
export:
merge_adapters_on_export: true # LoRA를 base에 merge
7. 체크포인트 저장 구조와 Bench 호환성
Handoff 전후 체크포인트 비교
| 항목 | handoff 없음 (기본) | handoff (end_scale=0) |
|---|---|---|
| expert FFN | N개 (각각 base_layer + lora_A/B) | N개 (LoRA 병합 후 weight만) |
| router | 있음 | 있음 |
| LoRA 파라미터 | 있음 | 없음 (병합 후 제거) |
| 체크포인트 크기 | 큼 (LoRA 포함) | 작음 (LoRA 제거) |
handoff 전:
├── experts.0.gate_proj.base_layer.weight + lora_A + lora_B
├── experts.1.gate_proj.base_layer.weight + lora_A + lora_B
├── ...
└── router.weight
handoff 후 (end_scale=0, remove_adapters_if_zero_scale=true):
├── experts.0.gate_proj.weight ← LoRA가 base에 병합됨
├── experts.1.gate_proj.weight
├── ...
└── router.weight ← 라우터 유지
핵심: 두 경우 모두 N개 expert + router 구조는 그대로 유지됩니다. Handoff는 LoRA 제거만 수행하며, MoE 구조를 축소하지 않습니다.
Bench 로딩
LoRA Handoff 체크포인트는 자동으로 bench에서 인식됩니다:
LocalHFClient가resolved_config.json에서 injection 설정 읽기- base HF 모델에
replace_ffn_with_moe_inplace()로 MoE 구조 재구성 - Handoff 체크포인트: LoRA 키 없음 → MoE 모델에 직접 로드
- 비-Handoff 체크포인트: expert 내 LoRA 병합 후 로드
- MoE 구조로 추론 (학습된 expert routing 패턴 보존)
별도 설정 불필요:
eulerforge bench \
--preset configs/bench/sft_target_only.yml \
--target-output-dir outputs/run_handoff/
8. 부분 Fade (end_scale > 0)
완전 제거 대신 LoRA 영향력을 일부만 남기고 싶을 때:
lora_handoff:
expert_lora:
start_step: 4000
duration_steps: 4000
end_scale: 0.3 # 30% 유지
end_action: keep # 계속 학습 가능
이 경우 export.merge_adapters_on_export: true로 부분 merge 가능.
9. 검증
설정 검증은 validate_config()에서 자동으로 수행됩니다 (규칙 H1~H7).
주요 검증 항목:
- P1:
injection.strategy가moe_expert_lora가 아니면 에러 - P2:
training.phases에base_ffn이 없으면 에러 start_step >= 0,duration_steps > 0end_scale는[0, 1]범위curve는linear또는cosineend_action은keep또는freezebase_ffn_ramp.end_step > start_stepattn_lora스케줄 시injection.attn_lora.enabled필수
# 검증만 수행
eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft_handoff.yml --validate-only
10. 프리셋
| 프리셋 | 모델 | 설명 |
|---|---|---|
qwen3.5_0.8b_moe_expert_lora_sft_handoff.yml |
Qwen3.5-0.8B | 3-phase + handoff |
llama3_1b_moe_expert_lora_sft_handoff.yml |
Llama-3.2-1B | 3-phase + handoff |