Published on

Transformers 로컬 LLM RoPE 스케일링 오류 해결

Authors

서버 없이 로컬에서 LLM을 돌리는 흐름이 보편화되면서, transformers 기반으로 모델을 로드해 추론하는 경우가 많아졌습니다. 그런데 Llama 계열을 포함한 다수의 최신 모델은 RoPE(Rotary Position Embedding)를 쓰고, 긴 컨텍스트를 위해 RoPE 스케일링(rope scaling)을 적용하는 경우가 많다 보니 모델 로딩 단계에서 바로 터지거나, 추론 중 위치 임베딩 관련 에러가 발생하는 일이 잦습니다.

이 글에서는 로컬 LLM에서 흔히 발생하는 RoPE 스케일링 오류를 “증상별”로 분류하고, transformers에서의 올바른 설정 방식과 버전 호환, 그리고 안전한 디버깅 순서를 정리합니다.

RoPE 스케일링이 왜 문제를 일으키나

RoPE는 토큰의 위치 정보를 회전 행렬 형태로 주입하는 방식입니다. 기본적으로 모델은 학습 시점에 정해진 max_position_embeddings 범위 내에서 안정적으로 동작하도록 학습됩니다.

긴 컨텍스트(예: 8k, 16k, 32k)를 지원하려면 다음 중 하나가 필요합니다.

  • 애초에 긴 컨텍스트로 학습된 체크포인트 사용
  • 학습은 짧게 했지만 추론에서 RoPE 스케일링으로 “위치 주파수”를 재매핑해 확장

문제는 이 확장 로직이 모델 패밀리/버전마다 다르고, transformers가 이를 config.jsonrope_scaling 필드로 처리하는 과정에서 형식 불일치, 지원하지 않는 타입, 키 누락, 라이브러리 버전 미지원이 쉽게 발생한다는 점입니다.

대표 증상 1: 모델 로딩 시 rope_scaling 관련 ValueError

가장 흔한 케이스는 from_pretrained() 시점에 아래 계열의 오류가 나는 것입니다.

  • ValueError: rope_scaling must be a dictionary with keys ...
  • ValueError: 'type' must be one of ...
  • KeyError: rope_scaling
  • Unexpected keyword argument 'rope_scaling'

원인 A: 모델 config.jsonrope_scaling 스키마가 Transformers 버전과 불일치

모델 허브에 올라온 config.json은 최신 스키마를 쓰는데, 로컬 환경의 transformers가 구버전이면 해당 필드를 파싱하지 못합니다. 반대로, 구형 스키마를 가진 모델을 최신 transformers에서 로드할 때도 경고/에러가 발생할 수 있습니다.

해결 1) 우선 버전 확인 및 업데이트

로컬 LLM은 대체로 CUDA, torch, transformers, accelerate 조합이 맞아야 합니다. 먼저 아래처럼 버전을 확인합니다.

python -c "import transformers, torch; print('transformers', transformers.__version__); print('torch', torch.__version__)"

그 다음 transformers를 업데이트합니다.

pip install -U transformers accelerate

환경 고정이 필요한 경우라면, 최소한 모델이 요구하는 transformers 버전을 릴리즈 노트 또는 모델 카드에서 확인하고 그에 맞춰 핀ning 하세요.

원인 B: rope_scaling 딕셔너리 키/값이 잘못됨

모델에 따라 rope_scaling은 보통 아래 형태 중 하나입니다.

  • {"type": "linear", "factor": 2.0}
  • {"type": "dynamic", "factor": 4.0}

그런데 일부 체크포인트는 커스텀 키(예: rope_type, original_max_position_embeddings)를 포함하거나, factor를 문자열로 저장하는 등 비정상 값이 섞여 있습니다.

해결 2) 로드 전에 config를 강제로 정규화

AutoConfig로 먼저 읽고, rope_scaling을 정리한 뒤 모델을 로드합니다.

from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
import torch

model_id = "your-local-or-hf-model"

config = AutoConfig.from_pretrained(model_id, trust_remote_code=True)

# rope_scaling이 존재하지만 형식이 이상한 경우를 방어
rope_scaling = getattr(config, "rope_scaling", None)
if rope_scaling is not None:
    # 예: factor가 문자열이면 float로 변환
    if "factor" in rope_scaling and isinstance(rope_scaling["factor"], str):
        rope_scaling["factor"] = float(rope_scaling["factor"])

    # type 키가 없고 rope_type 같은 키만 있는 경우를 보정(모델별로 다름)
    if "type" not in rope_scaling and "rope_type" in rope_scaling:
        rope_scaling["type"] = rope_scaling.pop("rope_type")

    config.rope_scaling = rope_scaling

# 토크나이저/모델 로드
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    config=config,
    torch_dtype=torch.float16,
    device_map="auto",
    trust_remote_code=True,
)

핵심은 “모델이 제공한 config.json을 그대로 믿지 말고, 로컬에서 파싱 가능한 형태로 정규화”하는 것입니다.

대표 증상 2: 컨텍스트 길이를 늘렸더니 품질 급락 또는 반복 출력

에러가 아니라 더 난감한 케이스로, RoPE 스케일링을 적용했더니 다음 증상이 나타납니다.

  • 답변이 같은 문장을 반복
  • 중반 이후 문맥이 붕괴
  • 특정 길이 이상에서 급격히 헛소리

원인: “긴 컨텍스트 지원”과 “RoPE 스케일링 적용”은 동일하지 않음

RoPE 스케일링은 어디까지나 추론 시점 보정입니다. 모델이 긴 컨텍스트에서 안정적으로 동작하려면 학습/파인튜닝 단계에서 해당 컨텍스트에 대한 적응이 되어 있거나, 최소한 모델이 의도한 방식의 스케일링을 써야 합니다.

예를 들어,

  • 모델 카드에 8k 지원이라고 되어 있는데 임의로 factor=4로 32k를 만들면 품질이 급락할 수 있습니다.
  • lineardynamic은 동작이 다르고, 모델이 기대한 타입과 다르면 결과가 크게 흔들립니다.

해결: 모델 카드의 권장 설정을 우선 적용

가능하면 모델 카드에 명시된 rope_scaling 혹은 max_position_embeddings 권장값을 그대로 사용하세요. 임의로 확장하려면 다음 순서로 검증하는 것을 추천합니다.

  1. 기본 설정(스케일링 없음)으로 정상 출력 확인
  2. 모델이 공식 지원하는 컨텍스트 길이까지 확장
  3. 그 이후 확장은 샘플링 파라미터(temperature, top_p)까지 포함해 회귀 테스트

이 접근은 운영 장애에서 “원인 격리”를 하는 방식과 유사합니다. 재현 가능한 단위로 좁혀가는 디버깅이 중요합니다. 같은 맥락에서 장애 원인을 체계적으로 좁히는 방법론은 Nginx에서 JWT 401 간헐 발생 - 시계오차 해결 같은 글의 트러블슈팅 흐름도 참고가 됩니다.

대표 증상 3: max_position_embeddings를 늘렸는데도 길이 제한이 그대로

간혹 config에서 max_position_embeddings를 늘려도, 실제로는 입력이 잘리거나 경고가 뜹니다.

원인: 토크나이저/서빙 코드에서 model_max_length가 제한

tokenizer.model_max_length가 보수적으로 잡혀 있으면, 토크나이저가 알아서 잘라버리거나 경고를 냅니다.

해결: 토크나이저의 최대 길이를 명시적으로 맞추기

from transformers import AutoTokenizer

model_id = "your-local-or-hf-model"

tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)

# 모델이 실제로 감당 가능한 길이로 설정(무작정 크게 잡지 말 것)
tokenizer.model_max_length = 8192

tokens = tokenizer(
    "긴 입력...",
    return_tensors="pt",
    truncation=True,
    max_length=8192,
)

이때 max_length만 늘려도 모델이 감당 못 하면 OOM이 나거나 품질이 깨지므로, VRAM과 배치 전략을 함께 봐야 합니다.

대표 증상 4: 로컬에서만 터지는 경우(서버/다른 PC에서는 정상)

같은 모델이 다른 환경에서는 잘 도는데, 내 PC에서만 RoPE 관련 오류가 나는 경우가 있습니다.

원인: 캐시된 모델 파일/구 config가 섞임

transformers는 모델을 캐시합니다. 모델이 업데이트되었거나, 이전에 받아둔 config.json이 남아 있으면 로컬에서만 스키마 불일치가 발생할 수 있습니다.

해결: 캐시 삭제 후 재다운로드

# 리눅스/맥 예시
rm -rf ~/.cache/huggingface/hub

또는 모델 단위로 캐시 폴더를 찾아 지우고 다시 받는 방식도 가능합니다.

실전: RoPE 스케일링 오류를 빠르게 잡는 체크리스트

아래 순서대로 보면 대부분의 케이스가 해결됩니다.

  1. transformers 버전 확인 후 업데이트
  2. 캐시 삭제 후 재다운로드
  3. AutoConfig를 먼저 로드해서 config.rope_scaling 값을 출력
  4. rope_scaling이 dict인지, typefactor가 유효한지 확인
  5. 토크나이저의 model_max_length와 실제 입력 max_length 정합성 점검
  6. 긴 컨텍스트 확장 시 “품질 회귀 테스트”를 짧은 프롬프트 세트로 자동화

운영에서 재시도/백오프/큐로 장애를 완화하듯, LLM 로컬 추론도 “실패를 전제로 한 설계”가 도움이 됩니다. 예를 들어 모델 로드 실패 시 다른 체크포인트로 폴백하거나, 요청을 큐잉해 재처리하는 패턴은 OpenAI API 429 RateLimit 재시도와 큐 설계에서 설명한 방식과 유사한 면이 있습니다.

재현 가능한 최소 코드: RoPE 스케일링 설정을 안전하게 적용

아래 코드는 (1) config를 먼저 읽고, (2) rope_scaling을 정규화한 뒤, (3) 추론까지 수행하는 최소 예시입니다.

import torch
from transformers import AutoConfig, AutoTokenizer, AutoModelForCausalLM

model_id = "your-local-or-hf-model"

def normalize_rope_scaling(cfg):
    rope_scaling = getattr(cfg, "rope_scaling", None)
    if rope_scaling is None:
        return cfg

    if not isinstance(rope_scaling, dict):
        raise ValueError("config.rope_scaling must be a dict")

    # 필수 키 보정
    if "type" not in rope_scaling:
        if "rope_type" in rope_scaling:
            rope_scaling["type"] = rope_scaling.pop("rope_type")

    # 값 보정
    if "factor" in rope_scaling and isinstance(rope_scaling["factor"], str):
        rope_scaling["factor"] = float(rope_scaling["factor"])

    cfg.rope_scaling = rope_scaling
    return cfg

config = AutoConfig.from_pretrained(model_id, trust_remote_code=True)
config = normalize_rope_scaling(config)

print("rope_scaling:", getattr(config, "rope_scaling", None))
print("max_position_embeddings:", getattr(config, "max_position_embeddings", None))

tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    config=config,
    device_map="auto",
    torch_dtype=torch.float16,
    trust_remote_code=True,
)

prompt = "RoPE scaling 설정이 올바르면 이 문장을 자연스럽게 이어서 설명해줘."
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

with torch.no_grad():
    out = model.generate(
        **inputs,
        max_new_tokens=128,
        do_sample=True,
        temperature=0.7,
        top_p=0.9,
    )

print(tokenizer.decode(out[0], skip_special_tokens=True))

이 코드는 “무조건 rope_scaling을 덮어쓴다”가 아니라, 존재할 때만 최소한의 스키마 정리를 합니다. 모델마다 의도한 스케일링 방식이 다르므로, 임의로 type이나 factor를 바꾸는 것은 마지막 수단으로 남겨두는 편이 안전합니다.

trust_remote_code=True를 켰을 때의 주의점

RoPE 관련 커스텀 로직이 modeling_*.py에 구현된 모델은 trust_remote_code=True 없이는 정상 로드가 안 되기도 합니다. 다만 이는 원격 코드를 실행하는 옵션이므로, 다음을 권장합니다.

  • 가능한 한 신뢰할 수 있는 출처의 모델만 사용
  • 모델 파일을 로컬에 고정(핀)하고 해시 기반으로 검증
  • 운영 환경에서는 별도 컨테이너/권한 격리

마무리

Transformers 로컬 LLM에서 RoPE 스케일링 오류는 대부분 “모델 config 스키마”와 “라이브러리 버전/캐시”의 엇갈림에서 시작합니다. 먼저 버전과 캐시를 정리하고, 그 다음 AutoConfig를 선로딩해 rope_scaling을 눈으로 확인한 뒤, 필요한 최소한의 정규화만 적용하는 흐름이 가장 빠르고 안전합니다.

긴 컨텍스트 확장은 숫자만 키운다고 해결되지 않습니다. 모델이 의도한 스케일링 타입과 지원 길이를 존중하면서, 짧은 회귀 테스트 세트를 만들어 단계적으로 늘려가면 “에러는 없는데 품질이 망가지는” 구간도 훨씬 빨리 잡아낼 수 있습니다.