본문으로 건너뛰기

[llm-compressor] Sentinel & Typing: 센티넬 객체와 타입 별칭

들어가며

src/llmcompressor/sentinel.pysrc/llmcompressor/typing.py는 파일 크기로는 가장 작지만, 프로젝트 전반에서 쓰이는 기본 도구다. 이 글에서는 이 두 파일의 역할과, 왜 별도로 필요한지를 살펴본다.

핵심 구조/코드 분석

sentinel.py: 센티넬 객체 패턴

class Sentinel:
    """
    A sentinel value used to distinguish "not provided" from "explicitly set to None".
    """

    def __init__(self, name: str):
        self.name = name

    def __repr__(self) -> str:
        return f"Sentinel({self.name!r})"

    def __eq__(self, other) -> bool:
        if isinstance(other, Sentinel):
            return self.name == other.name
        return False

    def __hash__(self) -> int:
        return hash(("Sentinel", self.name))

Sentinel은 특수 마커 객체다. None과 혼동되지 않는 "유니크한 기본값"을 만드는 데 쓰인다.

None으로 부족한가?

파이썬의 기본값 패턴은 흔히 None을 쓴다.

def f(x=None):
    if x is None:
        x = compute_default()
    return x

하지만 이 패턴은 "사용자가 명시적으로 None을 전달한 경우"와 "아무것도 전달하지 않은 경우"를 구별할 수 없다. GPTQactorder 파라미터가 이 문제를 겪었다.

class GPTQModifier(Modifier, QuantizationMixin):
    # 문제: None 과 "기본값 static" 을 구별할 수 없음
    actorder: Optional[Union[ActivationOrdering, Sentinel]] = Sentinel("static")

사용자가 GPTQModifier(actorder=None)으로 호출하면 "activation ordering을 비활성"한다는 의미다. 반면 GPTQModifier()로 호출하면 "기본값 static을 쓴다"는 의미다. 두 경우가 완전히 다른 동작이어야 한다.

Sentinel("static")을 기본값으로 두면 actorder is Noneactorder == Sentinel("static")이 다른 조건이 되어 구별 가능하다.

def resolve_actorder(existing):
    if self.actorder == Sentinel("static"):
        # 센티넬 → 기본값 static 이 호출되지 않은 경우에만 적용
        return ActivationOrdering.STATIC if existing is None else existing

    # 사용자가 명시적으로 값을 전달한 경우 (None 포함)
    if existing is None or self.actorder == existing:
        return self.actorder

    raise ValueError("Conflict between modifier actorder and scheme actorder")

이 로직 덕에 사용자는 "기본값을 사용"과 "None으로 비활성화"를 구별해 표현할 수 있다.

Sentinel의 비교 의미

def __eq__(self, other) -> bool:
    if isinstance(other, Sentinel):
        return self.name == other.name
    return False

Sentinel("static") == Sentinel("static")True이지만, Sentinel("static") == "static"False다. 이름이 같은 두 Sentinel 인스턴스는 서로 교환 가능하다(싱글톤처럼 동작). 반면 일반 문자열과는 구별된다.

이 엄격한 비교는 "센티넬이 다른 타입으로 오해되지 않도록" 보장한다. if x == "static": 같은 코드에서 센티넬이 문자열로 취급되는 버그를 방지.

typing.py: 프로젝트 공통 타입 별칭

from typing import Union

from torch import nn
from torch.utils.data import DataLoader
from transformers import PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin

# 타입 별칭들
Processor = Union[PreTrainedTokenizerBase, ProcessorMixin]
ModelType = Union[nn.Module, PreTrainedModel]
DatasetType = Union[DataLoader, list, dict]   # 실제 정의와 다를 수 있음

이 파일은 "프로젝트 전반에서 쓰이는 유니온 타입"에 이름을 준다. 각 Modifier나 함수가 Union[PreTrainedTokenizerBase, ProcessorMixin]을 반복하는 대신 Processor라는 별칭 하나를 쓰면 된다.

Processor 별칭의 의미

Processor = Union[PreTrainedTokenizerBase, ProcessorMixin]은 llm-compressor의 "토크나이저-프로세서 통합 개념"을 표현한다.

  • 텍스트 전용 모델: PreTrainedTokenizerBase (예: LlamaTokenizer)
  • 멀티모달 모델: ProcessorMixin (예: LlavaProcessor, Qwen2VLProcessor)

두 타입은 인터페이스가 다르지만 llm-compressor는 둘을 "입력 처리 객체"로 통합해 다룬다. 타입 별칭이 이 추상화를 명시한다.

왜 이 설계인가

1. Sentinel로 기본값과 None 구별. 파이썬 기본 관용구만으로는 해결할 수 없는 문제다. Sentinel 객체로 명시적 구별이 가능해진다. GPTQ의 actorder 같은 3상(third-state) 파라미터에 필수다.

2. 클래스 기반 Sentinel vs 단일 객체. _MISSING = object() 같은 단순 센티넬도 쓸 수 있지만, Sentinel 클래스는 reprname을 가져 디버깅 시 "어떤 센티넬인지" 알 수 있다. Sentinel("static")은 출력할 때 Sentinel('static')으로 표시된다.

3. Pydantic 필드와의 호환. Sentinel이 hashable이고 __eq__가 제대로 정의되어 있어 Pydantic 필드의 기본값으로 쓸 수 있다. Pydantic이 기본값을 해시해 캐시하므로 hashability가 중요하다.

4. 타입 별칭의 가독성. def quantize(model: ModelType, processor: Processor):def quantize(model: Union[nn.Module, PreTrainedModel], processor: Union[PreTrainedTokenizerBase, ProcessorMixin]):보다 훨씬 읽기 쉽다.

5. 단일 import 경로. from llmcompressor.typing import Processor 한 줄로 프로젝트 공통 타입을 가져올 수 있다. 타입 정의가 분산되면 일관성이 떨어진다.

마무리

Sentinel & Typing은 llm-compressor의 "타입 시스템 기반 인프라"다. 코드 크기는 작지만 생각보다 많은 곳에서 의존한다. 이로써 시리즈의 45개 포스트가 모두 끝났다.

전체 시리즈의 지도는 프로젝트 아키텍처 개요에서 확인할 수 있다. 목차 페이지에 이 모든 포스트의 링크가 있어 원하는 주제로 바로 넘어갈 수 있다.

참고 자료

댓글

관련 포스트

llm-compressor 의 다른글