Files
frankenstallm/source/PLAN_hybrid_3b_fixes.md

499 lines
16 KiB
Markdown
Raw Normal View History

# FRANKENSTALLM-H 3B Hybrid Model — 점검 결과 및 수정 실행 가이드
> **작성일**: 2026-03-05
> **목적**: Phase 2 검증 전, 발견된 이슈 6건을 수정하고 바로 실행 가능한 상태로 만든다.
> **다음 세션에서 이 문서를 참조하여 바로 실행할 것.**
---
## 이슈 요약 (6건)
| # | 심각도 | 이슈 | 파일 | 영향 |
|---|--------|------|------|------|
| 1 | **CRITICAL** | Mamba 블록에 FFN(channel mixer) 없음 | `model/mamba_block.py` | 37/40 레이어 capacity 부족 |
| 2 | **HIGH** | `n_groups=1` (Nemotron 표준은 8) | `configs/hybrid_3b.yaml` | B/C projection 표현력 저하 |
| 3 | **HIGH** | Hybrid 아키텍처 startup 로그 없음 | `train/pretrain.py` | 디버깅·모니터링 곤란 |
| 4 | **MEDIUM** | 체크포인트 resume 시 아키텍처 검증 없음 | `train/utils.py` | 잘못된 가중치 로드 가능 |
| 5 | **MEDIUM** | selective_scan에 NaN/Inf 감지 없음 | `model/mamba_block.py` | 수치 불안정 진단 불가 |
| 6 | **LOW** | selective_scan 입력 shape 검증 없음 | `model/mamba_block.py` | 모호한 에러 메시지 |
---
## 구현 순서 및 의존성
```
Step 1 (FFN 추가) ← 가장 먼저, 아키텍처 변경
├── 1a. model/config.py: mamba_d_ffn 필드 추가
├── 1b. model/mamba_block.py: FFN sublayer 추가
├── 1c. model/transformer.py: 생성자 인자 전달 + _init_weights 수정
└── 1d. configs/hybrid_3b.yaml: mamba_d_ffn=4608 추가
Step 2 (n_groups) ← Step 1과 독립, 병렬 가능
└── configs/hybrid_3b.yaml: n_groups=8
Step 3 (로그) ← Step 1 완료 후 (파라미터 수 정확해야)
└── train/pretrain.py: startup 배너에 hybrid 정보 추가
Step 4 (체크포인트 검증) ← 독립
└── train/utils.py: load_checkpoint에 config 비교 로직
Step 5-6 (NaN 감지 + shape 검증) ← 독립
└── model/mamba_block.py: selective_scan 함수
```
**병렬 가능**: Step 1 + Step 2는 YAML만 겹침 (마지막에 합치면 됨).
Step 4, Step 5-6도 독립적으로 병렬 실행 가능.
---
## Step 1: Mamba2Block에 FFN 추가 (CRITICAL)
### 배경
- Mamba2Block은 SSM(sequence mixer)만 있고 FFN(channel mixer)이 없음
- Nemotron-H에서는 모든 Mamba 레이어 뒤에 MLP가 따라옴
- 현재 37/40 레이어에 FFN이 없어 feature mixing이 불가능
- **확정**: `mamba_d_ffn = 4608` (d_model × 1.5), 총 파라미터 ~4.5B, VRAM ~80GB/GPU
### 1a. `model/config.py` 수정
**위치**: LMConfig dataclass 내부 (line 61 이후)
**추가할 필드** (기존 `mamba_chunk_size` 뒤에):
```python
mamba_d_ffn: Optional[int] = None # FFN dim for Mamba blocks (None → d_ffn)
```
**`__post_init__` 추가** (line 86, hybrid validation 블록 뒤에):
```python
# Mamba FFN dimension: default to d_ffn if not specified
if self.mamba_d_ffn is None:
self.mamba_d_ffn = self.d_ffn
```
**`to_dict()` 추가** (기존 mamba_chunk_size 뒤에):
```python
"mamba_d_ffn": self.mamba_d_ffn,
```
### 1b. `model/mamba_block.py` 수정
**Import 변경** (line 19):
```python
# 변경 전:
from .layers import RMSNorm
# 변경 후:
from .layers import RMSNorm, SwiGLU
```
**`Mamba2Block.__init__` 시그니처 변경** (line 128-137):
```python
# 변경 전:
def __init__(
self,
d_model: int,
d_state: int = 128,
head_dim: int = 64,
expand: int = 2,
conv_kernel: int = 4,
n_groups: int = 1,
chunk_size: int = 256,
) -> None:
# 변경 후:
def __init__(
self,
d_model: int,
d_state: int = 128,
head_dim: int = 64,
expand: int = 2,
conv_kernel: int = 4,
n_groups: int = 1,
chunk_size: int = 256,
d_ffn: int = 0,
bias: bool = False,
) -> None:
```
**FFN 서브레이어 추가** (line 192, `self.out_proj` 뒤에):
```python
# --- FFN sublayer (channel mixer) ---
if d_ffn > 0:
self.ffn_norm = RMSNorm(d_model)
self.ffn = SwiGLU(d_model, d_ffn, bias=bias)
else:
self.ffn_norm = None
self.ffn = None
```
**`forward()` 수정** (line 280):
```python
# 변경 전:
return residual + self.out_proj(y)
# 변경 후:
x = residual + self.out_proj(y)
# FFN sublayer (channel mixer)
if self.ffn is not None:
x = x + self.ffn(self.ffn_norm(x))
return x
```
### 1c. `model/transformer.py` 수정
**Mamba2Block 생성자 호출 변경** (line 124-132):
```python
# 변경 전:
layers.append(Mamba2Block(
d_model=config.d_model,
d_state=config.mamba_d_state,
head_dim=config.mamba_head_dim,
expand=config.mamba_expand,
conv_kernel=config.mamba_conv_kernel,
n_groups=config.mamba_n_groups,
chunk_size=config.mamba_chunk_size,
))
# 변경 후:
layers.append(Mamba2Block(
d_model=config.d_model,
d_state=config.mamba_d_state,
head_dim=config.mamba_head_dim,
expand=config.mamba_expand,
conv_kernel=config.mamba_conv_kernel,
n_groups=config.mamba_n_groups,
chunk_size=config.mamba_chunk_size,
d_ffn=config.mamba_d_ffn,
bias=config.bias,
))
```
**`_init_weights` 수정** (line 180-182):
```python
# 변경 전:
# Mamba2Block handles its own parameter init (A_log, D, dt_bias, etc.)
if isinstance(module, Mamba2Block):
return
# 변경 후 (이 3줄을 삭제):
# 삭제 이유: FFN 추가 후 내부 SwiGLU의 nn.Linear가 init 필요.
# A_log, D, dt_bias는 nn.Parameter이므로 isinstance(nn.Linear) 체크에 걸리지 않아
# 자동으로 스킵됨 (Mamba2Block.__init__에서 직접 초기화됨).
```
### 1d. `configs/hybrid_3b.yaml` 수정
```yaml
# mamba_chunk_size: 256 뒤에 추가:
mamba_d_ffn: 4608
```
### Step 1 검증
```bash
cd /PROJECT/0325120031_A/ghong/taketimes/llm-bang
CUDA_VISIBLE_DEVICES=0 python -c "
import torch, sys
sys.path.insert(0, '.')
from model import LLM, LMConfig
config = LMConfig.from_yaml('configs/hybrid_3b.yaml')
print(f'mamba_d_ffn = {config.mamba_d_ffn}')
model = LLM(config)
total = sum(p.numel() for p in model.parameters())
print(f'Total params: {total:,} ({total/1e9:.2f}B)')
# Forward test
x = torch.randint(0, 64000, (1, 128))
logits, loss = model(x, targets=x)
print(f'Forward OK: logits shape={logits.shape}, loss={loss.item():.4f}')
# Backward test
loss.backward()
grads_ok = all(p.grad is not None for p in model.parameters() if p.requires_grad)
print(f'Backward OK: all grads exist = {grads_ok}')
"
# 예상 출력: Total params ~4.5B, Forward/Backward OK
```
---
## Step 2: n_groups 수정
### `configs/hybrid_3b.yaml`
```yaml
# 변경 전:
mamba_n_groups: 1
# 변경 후:
mamba_n_groups: 8
```
### 검증
n_heads(= d_inner / head_dim = 6144 / 64 = 96) % 8 == 0 ✓
Step 1 검증 스크립트에서 함께 확인됨 (assertion이 `__init__`에 있음).
---
## Step 3: 하이브리드 아키텍처 startup 로그 추가
### `train/pretrain.py` 수정
**위치**: line 296-297 (모델 파라미터 출력 부분) 뒤에 추가
```python
if is_main_process():
total_params = sum(p.numel() for p in model.parameters())
print(f"Model parameters: {total_params:,}")
print(f"LMConfig: {lm_config}")
# --- 여기부터 추가 ---
if lm_config.use_hybrid:
pattern = lm_config.hybrid_pattern.split()
m_count = sum(1 for p in pattern if p == 'M')
a_count = sum(1 for p in pattern if p == 'A')
mamba_params = sum(
p.numel() for n, p in model.named_parameters()
if 'layers.' in n and pattern[int(n.split('.')[1])] == 'M'
)
attn_params = sum(
p.numel() for n, p in model.named_parameters()
if 'layers.' in n and pattern[int(n.split('.')[1])] == 'A'
)
other_params = total_params - mamba_params - attn_params
print(
f" arch : Hybrid Mamba-Transformer\n"
f" layers : {m_count} Mamba + {a_count} Attention = {len(pattern)} total\n"
f" params : Mamba {mamba_params/1e6:.0f}M + "
f"Attn {attn_params/1e6:.0f}M + Other {other_params/1e6:.0f}M\n"
f" mamba cfg: d_state={lm_config.mamba_d_state}, "
f"head_dim={lm_config.mamba_head_dim}, "
f"expand={lm_config.mamba_expand}, "
f"n_groups={lm_config.mamba_n_groups}, "
f"d_ffn={lm_config.mamba_d_ffn}"
)
# --- 추가 끝 ---
```
### 검증
Step 1 검증 실행 시 로그에 hybrid 정보가 출력되는지 확인.
---
## Step 4: 체크포인트 resume 아키텍처 검증
### `train/utils.py` — `load_checkpoint()` 수정
**위치**: line 179 (`raw_model.load_state_dict(...)`) 직전에 추가
```python
# --- Architecture validation ---
config_path = ckpt_dir / "config.yaml"
if config_path.exists() and hasattr(raw_model, "config"):
with open(config_path, "r", encoding="utf-8") as f:
saved_cfg = yaml.safe_load(f)
current_cfg = raw_model.config.to_dict()
critical_keys = [
"d_model", "n_layers", "n_heads", "n_kv_heads", "vocab_size",
"use_hybrid", "hybrid_pattern",
]
mismatches = []
for key in critical_keys:
saved_val = saved_cfg.get(key)
current_val = current_cfg.get(key)
if saved_val is not None and saved_val != current_val:
mismatches.append(
f" {key}: checkpoint={saved_val} vs current={current_val}"
)
if mismatches:
raise ValueError(
f"Checkpoint architecture mismatch!\n"
f"Checkpoint dir: {ckpt_dir}\n"
+ "\n".join(mismatches)
+ "\nUse --config matching the checkpoint, or start fresh."
)
# --- End architecture validation ---
```
**참고**: `yaml`은 이미 `train/utils.py` line 23에서 import 되어 있음.
### 검증
```bash
# 의도적으로 다른 config로 resume 시도
CUDA_VISIBLE_DEVICES=0 python train/pretrain.py \
--config configs/small.yaml \
--train_data data/3b_train.bin \
--resume checkpoints/hybrid_3b_run1/checkpoint-0001000
# 예상: ValueError "Checkpoint architecture mismatch!" 출력
```
---
## Step 5: selective_scan NaN/Inf 감지
### `model/mamba_block.py` — `selective_scan()` 수정
**위치**: line 94 (`y[:, t, :, :] = y_t.to(x.dtype)`) 뒤에 추가
```python
# Periodic NaN/Inf check (every 512 steps, < 1% overhead)
if t % 512 == 511:
if not torch.isfinite(h).all():
raise RuntimeError(
f"NaN/Inf in Mamba SSM state at timestep {t}/{seq_len}. "
f"h stats: min={h.min().item():.4e}, max={h.max().item():.4e}, "
f"A_log range=[{A_log.min().item():.4f}, {A_log.max().item():.4f}]"
)
```
### 검증
```bash
CUDA_VISIBLE_DEVICES=0 python -c "
import torch, sys
sys.path.insert(0, '.')
from model.mamba_block import Mamba2Block
block = Mamba2Block(d_model=256, d_state=64, head_dim=32, d_ffn=384)
x = torch.randn(1, 1024, 256)
# 정상 케이스
y = block(x)
print(f'Normal: output shape={y.shape}, finite={torch.isfinite(y).all()}')
# NaN 주입 테스트
block.A_log.data.fill_(100.0) # 매우 큰 값 → exp(100) overflow
try:
y = block(x)
print('WARNING: NaN not detected!')
except RuntimeError as e:
print(f'NaN correctly detected: {e}')
"
```
---
## Step 6: selective_scan 입력 shape 검증
### `model/mamba_block.py` — `selective_scan()` 수정
**위치**: line 49 (`batch, seq_len, n_heads, head_dim = x.shape`) 직전에 추가
```python
# Input shape validation
assert x.ndim == 4, f"x expected 4D (B,L,n_heads,head_dim), got {x.shape}"
assert dt.ndim == 3, f"dt expected 3D (B,L,n_heads), got {dt.shape}"
assert B.ndim == 4, f"B expected 4D (B,L,n_groups,d_state), got {B.shape}"
assert C.ndim == 4, f"C expected 4D (B,L,n_groups,d_state), got {C.shape}"
```
---
## 최종 검증 절차 (모든 Step 완료 후)
### 1. 모델 생성 + Forward/Backward (단일 GPU)
```bash
cd /PROJECT/0325120031_A/ghong/taketimes/llm-bang
CUDA_VISIBLE_DEVICES=0 python -c "
import torch, sys
sys.path.insert(0, '.')
from model import LLM, LMConfig
config = LMConfig.from_yaml('configs/hybrid_3b.yaml')
model = LLM(config).cuda()
total = sum(p.numel() for p in model.parameters())
print(f'Total params: {total:,} ({total/1e9:.2f}B)')
assert 4.0e9 < total < 5.0e9, f'Expected ~4.5B params, got {total/1e9:.2f}B'
# Forward
x = torch.randint(0, 64000, (2, 512)).cuda()
logits, loss = model(x, targets=x)
print(f'Forward: logits={logits.shape}, loss={loss.item():.4f}')
# Backward
loss.backward()
no_grad = [n for n, p in model.named_parameters() if p.requires_grad and p.grad is None]
assert len(no_grad) == 0, f'Missing gradients: {no_grad}'
print(f'Backward: all {sum(1 for p in model.parameters() if p.requires_grad)} params have grad')
# VRAM
print(f'VRAM: {torch.cuda.memory_allocated()/1e9:.1f}GB allocated')
"
```
### 2. DDP 8-GPU 테스트 (10 steps)
```bash
cd /PROJECT/0325120031_A/ghong/taketimes/llm-bang
torchrun --nproc_per_node=8 --master_port=29501 train/pretrain.py \
--config configs/hybrid_3b.yaml \
--train_data data/3b_train.bin \
--batch_size 2 \
--lr 1e-4 \
--warmup_steps 5 \
--grad_accum 1 \
--max_steps 10 \
--checkpoint_dir /tmp/hybrid_test_ckpt \
--use_fp8
# 예상: 10 steps 완료, 체크포인트 저장, startup 배너에 hybrid 정보 출력
```
### 3. 체크포인트 Resume 테스트
```bash
# Step 2 체크포인트에서 resume
torchrun --nproc_per_node=8 --master_port=29501 train/pretrain.py \
--config configs/hybrid_3b.yaml \
--train_data data/3b_train.bin \
--batch_size 2 \
--lr 1e-4 \
--warmup_steps 5 \
--grad_accum 1 \
--max_steps 20 \
--checkpoint_dir /tmp/hybrid_test_ckpt \
--resume /tmp/hybrid_test_ckpt/checkpoint-0000010 \
--use_fp8
# 예상: step 10에서 이어서 step 20까지 학습
```
---
## 수정하지 않는 것들 (의도적 제외)
- **sequential scan 성능**: Python for-loop는 느리지만 구조 변경이 큼. 별도 태스크로 chunked SSD 구현
- **FP8 + Mamba 혼합**: 현재 설계(Mamba=bf16, Attention=FP8)가 올바름. te.fp8_autocast는 te 모듈만 영향
- **DDP 설정**: find_unused_parameters=False, gradient_as_bucket_view=True 모두 정상
- **pure Transformer 모드**: use_hybrid=False면 기존 동작 유지 (하위 호환)
---
## 수정 대상 파일 요약
| 파일 | Step | 변경 내용 |
|------|------|----------|
| `model/config.py` | 1a | `mamba_d_ffn` 필드 + `__post_init__` + `to_dict()` |
| `model/mamba_block.py` | 1b, 5, 6 | SwiGLU import, FFN sublayer, NaN 감지, shape 검증 |
| `model/transformer.py` | 1c | Mamba2Block 생성자에 d_ffn/bias 전달, `_init_weights` 수정 |
| `configs/hybrid_3b.yaml` | 1d, 2 | `mamba_d_ffn: 4608`, `mamba_n_groups: 8` |
| `train/pretrain.py` | 3 | Hybrid startup 로그 |
| `train/utils.py` | 4 | `load_checkpoint()` 아키텍처 검증 |
---
## 실행 지시 (다음 세션용)
이 문서를 참조하여 다음 명령을 내리면 됩니다:
> "이 문서(hashed-drifting-harp.md)의 Step 1~6을 순서대로 실행해 줘.
> Step 1+2는 병렬로, Step 3~6은 독립적으로 진행.
> 각 Step 완료 후 해당 검증을 실행하고,
> 전체 완료 후 최종 검증 절차 3단계를 모두 실행해 줘."