- LoadFFTNetModel / FFTNetGenerate ComfyUI nodes - FFTNet architecture (SpectralFilter, LocalWindowMixing, FFTNetMixer) - upgrade/: replace LLM self-attention with FFTNet + MPS-safe fine-tuning script - Auto-detects self_attn return count (1/2/3) for transformers version compat - MPS FFT CPU offload workaround for Apple Silicon Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
8.1 KiB
Python
199 lines
8.1 KiB
Python
"""
|
|
기존 HuggingFace LLM 의 Self-Attention 레이어를 FFTNetAttentionWrapper 로 교체하는 유틸리티.
|
|
|
|
지원 모델 계열 (동일한 .self_attn 구조):
|
|
LLaMA, LLaMA-3, Mistral, Qwen2, Falcon, OPT, ...
|
|
(decoder layer 에 self_attn 속성이 있는 모든 모델)
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import torch
|
|
from .fftnet_attention import FFTNetAttentionWrapper, _decoder_return_count
|
|
|
|
FFTNET_MARKER = "fftnet_upgraded" # 체크포인트 타입 식별자
|
|
|
|
|
|
# ── 내부 헬퍼 ─────────────────────────────────────────────────────────────────
|
|
|
|
def _get_decoder_layers(model):
|
|
"""
|
|
모델에서 decoder layer 리스트를 반환.
|
|
model.model.layers (LLaMA, Mistral, Qwen2 …)
|
|
model.layers (일부 소형 모델)
|
|
model.transformer.h (GPT-2 계열)
|
|
"""
|
|
if hasattr(model, "model") and hasattr(model.model, "layers"):
|
|
return model.model.layers
|
|
if hasattr(model, "layers"):
|
|
return model.layers
|
|
if hasattr(model, "transformer") and hasattr(model.transformer, "h"):
|
|
return model.transformer.h
|
|
raise ValueError(
|
|
"decoder layer 를 찾을 수 없습니다. "
|
|
"model.model.layers / model.layers / model.transformer.h 중 하나여야 합니다."
|
|
)
|
|
|
|
|
|
# ── 공개 API ──────────────────────────────────────────────────────────────────
|
|
|
|
def replace_with_fftnet(model, window_size: int = 4) -> int:
|
|
"""
|
|
모델의 모든 Self-Attention 레이어를 FFTNetAttentionWrapper 로 교체.
|
|
|
|
Returns
|
|
-------
|
|
replaced : 교체된 레이어 수
|
|
"""
|
|
hidden_size = model.config.hidden_size
|
|
max_seq_len = getattr(model.config, "max_position_embeddings", 2048)
|
|
|
|
layers = _get_decoder_layers(model)
|
|
replaced = 0
|
|
|
|
for layer in layers:
|
|
if not hasattr(layer, "self_attn"):
|
|
continue
|
|
ret_count = _decoder_return_count(layer)
|
|
wrapper = FFTNetAttentionWrapper(
|
|
hidden_size = hidden_size,
|
|
max_seq_len = max_seq_len,
|
|
window_size = window_size,
|
|
return_count = ret_count,
|
|
)
|
|
print(f" layer {replaced}: return_count={ret_count}")
|
|
layer.self_attn = wrapper
|
|
replaced += 1
|
|
|
|
print(f"[replace_attention] {replaced} attention layer → FFTNetMixer 교체 완료")
|
|
return replaced
|
|
|
|
|
|
def freeze_non_fftnet(model) -> tuple[int, int]:
|
|
"""
|
|
FFTNet 믹서 파라미터만 학습 가능하게 두고 나머지를 freeze.
|
|
|
|
Returns
|
|
-------
|
|
(trainable, frozen) : 파라미터 수 쌍
|
|
"""
|
|
trainable = frozen = 0
|
|
for name, param in model.named_parameters():
|
|
is_fftnet = "self_attn" in name # 교체된 레이어는 여전히 self_attn 이름 유지
|
|
param.requires_grad = is_fftnet
|
|
if is_fftnet:
|
|
trainable += param.numel()
|
|
else:
|
|
frozen += param.numel()
|
|
|
|
print(
|
|
f"[replace_attention] 학습 파라미터: {trainable:,} "
|
|
f"| Freeze: {frozen:,} "
|
|
f"| 비율: {trainable / (trainable + frozen) * 100:.1f}%"
|
|
)
|
|
return trainable, frozen
|
|
|
|
|
|
def save_upgraded_checkpoint(model, tokenizer, output_dir: str, fftnet_config: dict):
|
|
"""
|
|
학습 완료된 업그레이드 모델을 저장.
|
|
|
|
output_dir/
|
|
├── fftnet_upgraded.pt ← state_dict + 설정
|
|
└── tokenizer/ ← HuggingFace 토크나이저 파일
|
|
"""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
payload = {
|
|
"type": FFTNET_MARKER,
|
|
"llama_config": model.config.to_dict(),
|
|
"fftnet_config": fftnet_config,
|
|
"state_dict": model.state_dict(),
|
|
}
|
|
ckpt_path = os.path.join(output_dir, "fftnet_upgraded.pt")
|
|
torch.save(payload, ckpt_path)
|
|
print(f"[replace_attention] 체크포인트 저장: {ckpt_path}")
|
|
|
|
tok_dir = os.path.join(output_dir, "tokenizer")
|
|
tokenizer.save_pretrained(tok_dir)
|
|
print(f"[replace_attention] 토크나이저 저장: {tok_dir}")
|
|
|
|
|
|
def load_upgraded_model(checkpoint_path: str, device: torch.device, window_size: int = 4):
|
|
"""
|
|
save_upgraded_checkpoint() 로 저장된 체크포인트를 로드.
|
|
|
|
Returns
|
|
-------
|
|
model, tokenizer, fftnet_config
|
|
"""
|
|
from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
|
|
from transformers import LlamaConfig
|
|
|
|
ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
|
|
|
if ckpt.get("type") != FFTNET_MARKER:
|
|
raise ValueError(f"[load_upgraded_model] 이 파일은 {FFTNET_MARKER} 체크포인트가 아닙니다.")
|
|
|
|
fftnet_cfg = ckpt["fftnet_config"]
|
|
llama_cfg_d = ckpt["llama_config"]
|
|
state_dict = ckpt["state_dict"]
|
|
|
|
# ── Config 복원 (Hub 접속 불필요) ────────────────────────────────────────
|
|
model_type = llama_cfg_d.get("model_type", "llama")
|
|
|
|
# 저장 시 생긴 내부 키(_name_or_path 등)를 보존하면서 Config 재구성
|
|
try:
|
|
cfg_cls = AutoConfig.for_model(model_type)
|
|
# 모르는 키는 조용히 무시
|
|
import dataclasses, inspect as _inspect
|
|
known = set(_inspect.signature(cfg_cls.__init__).parameters.keys()) - {"self", "kwargs"}
|
|
init_kw = {k: v for k, v in llama_cfg_d.items() if k in known or k.startswith("_")}
|
|
config = cfg_cls(**init_kw)
|
|
# 나머지 속성도 덮어쓰기 (vocab_size 등 중요한 것 포함)
|
|
for k, v in llama_cfg_d.items():
|
|
try:
|
|
setattr(config, k, v)
|
|
except Exception:
|
|
pass
|
|
except Exception as e:
|
|
print(f"[load_upgraded_model] Config 복원 fallback: {e}")
|
|
config = LlamaConfig(**{k: v for k, v in llama_cfg_d.items()
|
|
if not k.startswith("transformers_version")})
|
|
|
|
# ── 모델 구조 생성 (CPU, 실제 텐서) ──────────────────────────────────────
|
|
# torch.device("meta") + to_empty() 방식은 환경에 따라 불안정하므로
|
|
# 직접 CPU 에 빈 모델을 생성 후 state_dict 로 채운다.
|
|
print(f"[load_upgraded_model] 모델 구조 생성 중 ({model_type}) ...")
|
|
model = AutoModelForCausalLM.from_config(config)
|
|
|
|
# ── Attention → FFTNet 교체 (가중치 로드 전에 구조를 맞춤) ────────────────
|
|
replace_with_fftnet(model, window_size=fftnet_cfg.get("window_size", window_size))
|
|
|
|
# ── 가중치 로드 ───────────────────────────────────────────────────────────
|
|
result = model.load_state_dict(state_dict, strict=False)
|
|
missing, unexpected = result.missing_keys, result.unexpected_keys
|
|
if missing:
|
|
print(f"[load_upgraded_model] missing keys (first 5): {missing[:5]}")
|
|
if unexpected:
|
|
print(f"[load_upgraded_model] unexpected keys (first 5): {unexpected[:5]}")
|
|
|
|
model.to(device)
|
|
model.eval()
|
|
print(f"[load_upgraded_model] 로드 완료 ({sum(p.numel() for p in model.parameters()):,} params)")
|
|
|
|
# ── 토크나이저 ────────────────────────────────────────────────────────────
|
|
tok_dir = os.path.join(os.path.dirname(checkpoint_path), "tokenizer")
|
|
if os.path.isdir(tok_dir):
|
|
tokenizer = AutoTokenizer.from_pretrained(tok_dir)
|
|
else:
|
|
base_name = llama_cfg_d.get("_name_or_path", "")
|
|
if base_name:
|
|
print(f"[load_upgraded_model] 토크나이저를 {base_name} 에서 로드 중 ...")
|
|
tokenizer = AutoTokenizer.from_pretrained(base_name)
|
|
else:
|
|
tokenizer = None
|
|
print("[load_upgraded_model] 토크나이저를 찾을 수 없습니다. tokenizer_path 를 직접 지정하세요.")
|
|
|
|
return model, tokenizer, fftnet_cfg
|