Files
bemoregtandClaude Sonnet 4.6 9a0c3dc1a0 Initial release: ComfyUI custom nodes for FFTNet inference and upgrade fine-tuning
- 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>
2026-03-17 12:15:18 +09:00

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