본문으로 건너뛰기

[llm-compressor] Modifier Factory: 문자열 이름에서 Modifier 인스턴스 생성

들어가며

Recipe DSL 글에서 본 것처럼, 레시피 YAML은 GPTQModifier: {block_size: 128, ...} 같은 문자열로 Modifier를 선언한다. 이 문자열을 실제 GPTQModifier 클래스의 인스턴스로 변환하는 곳이 src/llmcompressor/modifiers/factory.pyModifierFactory다. 이 팩토리는 패키지를 재귀 스캔해 "이름이 *Modifier로 끝나는 Modifier 서브클래스"를 자동으로 등록한다.

핵심 구조/코드 분석

세 개의 레지스트리

class ModifierFactory:
    _MAIN_PACKAGE_PATH = "llmcompressor.modifiers"
    _EXPERIMENTAL_PACKAGE_PATH = "llmcompressor.modifiers.experimental"

    _loaded: bool = False                                  # 초기 스캔 완료 여부
    _main_registry: dict[str, type[Modifier]] = {}         # 안정 버전 Modifier 들
    _experimental_registry: dict[str, type[Modifier]] = {} # 실험적 Modifier 들
    _registered_registry: dict[str, type[Modifier]] = {}   # 사용자 등록 Modifier 들
    _errors: dict[str, Exception] = {}                     # 로딩 실패 기록

팩토리는 세 개의 레지스트리를 관리한다.

  • main: llmcompressor.modifiers 아래 모든 모듈에서 자동 스캔된 Modifier
  • experimental: llmcompressor.modifiers.experimental 아래의 실험적 구현체
  • registered: ModifierFactory.register("MyMod", MyModifierClass)로 외부에서 등록한 것

이 셋을 분리하는 이유는 우선순위다. 사용자가 자체 Modifier를 등록했다면 내부 Modifier보다 우선해야 한다. 실험적 Modifier는 명시적으로 허용할 때만 쓰인다.

refresh: 패키지 스캔

@staticmethod
def refresh():
    ModifierFactory._main_registry = ModifierFactory.load_from_package(
        ModifierFactory._MAIN_PACKAGE_PATH
    )
    ModifierFactory._experimental_registry = ModifierFactory.load_from_package(
        ModifierFactory._EXPERIMENTAL_PACKAGE_PATH
    )
    ModifierFactory._loaded = True

refresh는 두 패키지 모두 재스캔해 레지스트리를 새로 만든다. Recipe DSLRecipe.from_dictif not ModifierFactory._loaded: ModifierFactory.refresh()로 지연 초기화한다. 한 번 스캔하면 프로세스 내내 재사용된다.

load_from_package: 재귀 스캔 로직

@staticmethod
def load_from_package(package_path: str) -> dict[str, type[Modifier]]:
    loaded = {}
    main_package = importlib.import_module(package_path)

    # 의도적으로 제외할 deprecated 경로들
    deprecated_packages = [
        "llmcompressor.modifiers.obcq",
        "llmcompressor.modifiers.obcq.sgpt_base",
        "llmcompressor.modifiers.quantization.gptq",
        "llmcompressor.modifiers.quantization.gptq.base",
        "llmcompressor.modifiers.quantization.gptq.gptq_quantize",
    ]

    # 1) pkgutil 로 패키지 내 모든 모듈 순회
    for _importer, modname, _is_pkg in pkgutil.walk_packages(
        main_package.__path__, package_path + "."
    ):
        if modname in deprecated_packages:
            continue
        try:
            module = importlib.import_module(modname)

            # 2) 모듈 내 모든 속성 검사
            for attribute_name in dir(module):
                if not attribute_name.endswith("Modifier"):
                    continue   # 네이밍 컨벤션: *Modifier 만 등록 대상

                try:
                    if attribute_name in loaded:
                        continue   # 이미 다른 경로에서 등록된 이름은 스킵

                    attr = getattr(module, attribute_name)

                    if not isinstance(attr, type):
                        raise ValueError(f"Attribute {attribute_name} is not a type")
                    if not issubclass(attr, Modifier):
                        raise ValueError(f"Attribute {attribute_name} is not a Modifier")

                    loaded[attribute_name] = attr
                except Exception as err:
                    ModifierFactory._errors[attribute_name] = err
        except Exception as module_err:
            print(module_err)

    return loaded

동작 요약:

  1. pkgutil.walk_packages로 패키지 안의 모든 모듈을 재귀적으로 찾는다.
  2. deprecated 경로는 건너뛴다. 이는 GPTQ처럼 여러 위치에 구현체가 있었던 역사적 이유 때문이다.
  3. 각 모듈의 dir()을 순회해 *Modifier로 끝나는 이름만 후보로 삼는다.
  4. 후보가 실제 Modifier 서브클래스인지 체크하고 등록.
  5. 예외는 _errors에 기록하고 스킵해서, 한 모듈의 import 실패가 다른 모듈 등록을 막지 않게 한다.

네이밍 컨벤션이 스캔의 핵심이다. 모든 Modifier 클래스는 이름이 Modifier로 끝나야 자동 등록된다. 이 컨벤션 덕에 설정 파일 없이 새 Modifier를 추가해도 레시피에서 바로 쓸 수 있다.

create: 인스턴스 생성

@staticmethod
def create(
    type_: str,                                  # "GPTQModifier" 같은 클래스 이름
    allow_registered: bool,                      # 사용자 등록 Modifier 허용
    allow_experimental: bool,                    # 실험적 Modifier 허용
    **kwargs,                                    # Modifier 생성자 인자
) -> Modifier:
    # 과거 로딩 실패가 있었다면 그 예외를 재발생
    if type_ in ModifierFactory._errors:
        raise ModifierFactory._errors[type_]

    # 우선순위 1: 사용자 등록
    if type_ in ModifierFactory._registered_registry:
        if allow_registered:
            return ModifierFactory._registered_registry[type_](**kwargs)

    # 우선순위 2: 실험적
    if type_ in ModifierFactory._experimental_registry:
        if allow_experimental:
            return ModifierFactory._experimental_registry[type_](**kwargs)

    # 우선순위 3: 메인
    if type_ in ModifierFactory._main_registry:
        return ModifierFactory._main_registry[type_](**kwargs)

    raise ValueError(f"No modifier of type '{type_}' found.")
우선순위 레지스트리 언제 허용
1 _registered_registry allow_registered=True
2 _experimental_registry allow_experimental=True
3 _main_registry 항상

메인 레지스트리는 마지막이다. 사용자가 같은 이름으로 자체 Modifier를 등록했다면 그것이 우선된다. 이는 오픈소스 프로젝트의 내장 구현을 사용자가 교체할 수 있게 하는 확장 포인트다.

register: 외부 Modifier 등록

@staticmethod
def register(type_: str, modifier_class: type[Modifier]):
    if not issubclass(modifier_class, Modifier):
        raise ValueError("The provided class does not subclass the Modifier base class.")
    if not isinstance(modifier_class, type):
        raise ValueError("The provided class is not a type.")

    ModifierFactory._registered_registry[type_] = modifier_class

사용자는 자신의 Modifier 클래스를 다음과 같이 등록할 수 있다.

from llmcompressor.modifiers import Modifier, ModifierFactory

class MyCustomModifier(Modifier):
    def on_initialize(self, state, **kwargs):
        # 사용자 정의 로직
        return True

ModifierFactory.register("MyCustomModifier", MyCustomModifier)

이후 레시피 YAML에 MyCustomModifier: 키로 이 Modifier를 사용할 수 있다. 플러그인 설치 없이도 런타임에 등록이 가능해 실험이 편하다.

왜 이 설계인가

1. 네이밍 컨벤션 기반 자동 등록. 패키지 안의 *Modifier 클래스를 자동으로 찾으므로, 새 Modifier를 추가할 때 중앙 레지스트리 파일을 수정할 필요가 없다. src/llmcompressor/modifiers/my_algo/my_algo_modifier.pyMyAlgoModifier(Modifier)를 만들면 즉시 레시피에서 쓸 수 있다.

2. 세 레지스트리의 우선순위. 사용자 등록 > 실험적 > 메인 순서로 오버라이드가 가능하다. 이는 "내장 Modifier가 맘에 안 들면 사용자가 교체할 수 있다"는 확장성이다.

3. 실험적 Modifier 격리. allow_experimental 플래그로 실험적 Modifier 사용을 명시적으로 opt-in해야 한다. 안정 버전만 쓰고 싶은 사용자는 실수로 실험적 구현체에 접근하지 않는다.

4. 로딩 실패 내성. 한 모듈의 import 실패가 전체 팩토리를 망가뜨리지 않는다. 실패한 Modifier는 _errors에 기록되고 나머지는 정상 등록된다. 나중에 그 Modifier를 create로 요청하면 기록된 예외가 다시 던져진다.

5. Deprecated 경로 제외. 코드 이력상 GPTQ 같은 Modifier는 여러 위치에 구현되었다. 스캔 시 과거 경로를 제외해 "같은 이름의 두 클래스 중 어느 것이 선택될까" 하는 모호성을 제거한다.

마무리

Modifier Factory는 Recipe YAML과 Python 클래스를 잇는 다리다. 레시피 파싱, 자동 스캔, 우선순위 디스패치, 사용자 등록, 실패 격리라는 다섯 가지 기능이 120줄 안에 들어 있다. 다음 글은 Modifier Interface를 통해 Modifier의 추상 계약을 본다.

참고 자료

댓글

관련 포스트

llm-compressor 의 다른글