[llm-compressor] Observers Base: 스케일/제로포인트 계산의 추상 기반
들어가며
Observer는 "가중치/활성화를 보고 양자화 파라미터를 계산하는" 컴포넌트다. 대부분의 양자화 알고리즘은 한 레이어당 "스케일(scale)"과 "제로포인트(zero point)" 두 값이 필요한데, 이 두 값을 어떻게 결정하느냐가 정확도와 속도의 핵심이다. llm-compressor는 src/llmcompressor/observers/base.py의 Observer 베이스 클래스를 통해 여러 계산 방식(min-max, MSE, moving average, imatrix)을 같은 인터페이스로 통합한다.
공식 문서
핵심 구조/코드 분석
Observer 베이스 클래스
class Observer(InternalModule, RegistryMixin):
"""
Base class for observers which compute quantization parameters given
observations of weights, activations, or attention states.
"""
def __init__(
self,
base_name: str, # 관측자 속성 이름 (예: "weight", "input")
args: QuantizationArgs, # 양자화 설정 (num_bits, symmetric, ...)
module: Optional[torch.nn.Module] = None, # 연결된 모듈 (global_scale/g_idx 공유용)
**observer_kwargs, # 서브클래스별 추가 파라미터
):
super().__init__()
self.module = ref(module) if module is not None else None # 약참조로 메모리 누수 방지
self.base_name = base_name
self.args = args
self.args.observer_kwargs = self.args.observer_kwargs or {}
self.args.observer_kwargs.update(observer_kwargs)
| 생성자 인자 | 의미 |
|---|---|
base_name |
관측 대상 이름. 한 모듈에 weight observer와 input observer가 공존할 수 있어 이름으로 구분 |
args |
QuantizationArgs 객체 — num_bits, symmetric, strategy(channel/group/token) 등 |
module |
이 observer가 붙은 nn.Module. global_scale, g_idx 같은 양자화 보조 파라미터를 공유하기 위한 약참조 |
**observer_kwargs |
MSE observer의 maxshrink, patience 같은 서브클래스 전용 파라미터 |
InternalModule을 상속하는 이유는 observer가 nn.Module처럼 다뤄지되 "일반 모델의 일부가 아닌 내부 구현물"임을 표시하기 위함이다. state_dict 저장 시 제외되는 등 특수 처리가 가능하다.
추상 메서드: get_min_max
@abstractmethod
def get_min_max(self, observed: torch.Tensor) -> MinMaxTuple:
"""
Calculate min and max values from observed value
:param observed: value of shape (num_observations, *qparam_shape, group_size)
:return: minimum value and maximum value whose shapes are (*qparam_shape,)
"""
raise NotImplementedError()
@abstractmethod
def get_global_min_max(self, observed: torch.Tensor) -> MinMaxTuple:
"""
For microscale schemes (NVFP4, MXFP4) which need global scale
"""
raise NotImplementedError()
자식 observer는 이 두 메서드만 구현하면 된다. 나머지(forward, get_global_scale 등)는 베이스가 기본 구현을 제공한다.
get_min_max: 관측 텐서에서 채널(또는 그룹)별 min/max 페어를 반환. 이 페어가 나중에calculate_qparams로 전달되어 스케일과 제로포인트가 된다.get_global_min_max: 마이크로스케일 스킴(NVFP4/MXFP4)에서는 그룹별 스케일 외에 전체에 곱해지는 전역 스케일이 하나 더 필요하다. 이 메서드가 그 전역 스케일용 min/max를 반환.
forward: 스케일/제로포인트 계산
@torch.no_grad
def forward(self, observed: torch.Tensor) -> ScaleZpTuple:
scales, zero_points, _min, _max = self._forward_with_minmax(observed)
return (scales, zero_points)
def _forward_with_minmax(self, observed):
g_idx = self._get_module_param("g_idx") # 그룹 인덱스 (GPTQ act-order 용)
global_scale = self._get_module_param("global_scale") # 전역 스케일 (micro 스킴)
self._check_has_global_scale(global_scale)
# qparams_shape 에 맞게 평탄화 (채널/그룹/토큰 단위로 분할)
observed = flatten_for_calibration(observed, self.base_name, self.args, g_idx)
# 서브클래스 훅 — min/max 계산 방식은 서브클래스가 결정
min_vals, max_vals = self.get_min_max(observed)
# compressed-tensors 의 표준 스케일 계산
scales, zero_points = calculate_qparams(
min_vals=min_vals,
max_vals=max_vals,
quantization_args=self.args,
global_scale=global_scale,
)
return scales, zero_points, min_vals, max_vals
forward의 흐름은 세 단계다.
- 평탄화.
flatten_for_calibration이 관측 텐서를(num_observations, *qparam_shape, group_size)형태로 변형한다. 예를 들어 channel 단위 양자화라면*qparam_shape가 출력 채널 차원, group 단위라면(out_channels, num_groups)이 된다. - 서브클래스 훅 호출.
get_min_max를 호출해 min/max를 받는다. 이 부분이 observer 간 차이를 만드는 유일한 지점이다. - qparams 계산.
compressed-tensors의calculate_qparams가 min/max → scale/zero_point로 변환한다. 이 계산은args의num_bits,symmetric,strategy설정에 따라 다르다.
즉 "min/max 계산 정책"만 교체하면 동일한 calculate_qparams가 다양한 스킴에 적용된다. 이 구조 덕분에 Observer 서브클래스는 수십 줄로 작성할 수 있다.
get_global_scale: 마이크로스케일 지원
@torch.no_grad
def get_global_scale(self, observed: torch.Tensor) -> torch.Tensor:
global_scale, _min, _max = self._get_global_scale_with_minmax(observed)
return global_scale
NVFP4/MXFP4 같은 마이크로스케일 포맷은 "블록별 스케일"과 "전역 스케일"을 모두 사용한다. 블록 스케일은 4~8비트 정수로 패킹되고, 전역 스케일은 FP32로 별도 저장된다. generate_gparam 함수가 get_global_min_max의 결과로부터 전역 스케일을 산출한다.
RegistryMixin: 이름으로 검색
observer = Observer.load_from_registry("minmax", base_name="weight", args=...)
RegistryMixin은 compressed_tensors의 레지스트리 믹스인이다. 각 observer는 @Observer.register("minmax") 데코레이터로 등록되고, 나중에 문자열 이름으로 검색된다. 이는 Pipeline Registry와 같은 패턴이다. 사용자가 레시피 YAML에 observer_type: "mse"로 지정하면 이 레지스트리가 해당 클래스를 반환한다.
왜 이 설계인가
1. 핵심 훅 get_min_max 분리. observer 간 차이의 전부가 "어떻게 min/max를 결정하는가"로 귀결된다. MinMax는 단순 amin/amax, MSE는 grid search, moving average는 지수 평균을 쓴다. 이 부분만 추상 메서드로 분리하면 나머지 로직(평탄화, qparams 계산)을 공유할 수 있다.
2. calculate_qparams 위임. min/max → scale/zero_point 변환은 양자화 스킴(symmetric/asymmetric, channel/group/token)에 따라 다양한 케이스를 다룬다. 이 로직을 compressed-tensors에 두고 llm-compressor는 호출만 하는 것이 유지보수 면에서 효율적이다. 두 프로젝트 공용 로직이기도 하다.
3. 약참조 ref(module). Observer가 연결된 모듈을 강참조로 보관하면 순환 참조(module → observer → module)가 되어 GC가 어려워진다. 약참조로 바꾸면 모듈이 해제될 때 observer도 자동으로 정리된다.
4. base_name으로 다중 관측 지원. 한 모듈이 "가중치 관측자"와 "활성화 관측자"를 동시에 가질 수 있다. 각자의 base_name이 다르므로 같은 모듈에 여러 observer가 붙어도 이름 충돌이 없다.
5. RegistryMixin 기반 확장. 사용자가 자체 observer를 구현해 @Observer.register("my_obs")로 등록하면, 레시피 YAML의 observer 이름을 바꾸는 것만으로 전체 파이프라인에 적용된다.
마무리
Observer Base는 "양자화의 산술 핵심을 한 곳에 모은" 프레임워크다. 네 가지 내장 observer(min_max, mse, moving_average, imatrix)가 모두 이 베이스 위에서 get_min_max 훅만 바꿔 구현된다. 다음 글부터는 각 구현체를 순서대로 본다.
참고 자료
관련 포스트
llm-compressor 의 다른글
- 이전글 [llm-compressor] Modifier Interface: 추상 계약과 타입 체크
- 현재글 : [llm-compressor] Observers Base: 스케일/제로포인트 계산의 추상 기반
- 다음글 [llm-compressor] MinMax Observer: 세 가지 min/max 계산 정책
댓글