본문으로 건너뛰기

[llm-compressor] Data-Free & Independent Pipeline: 데이터 없는 파이프라인과 Modifier별 개별 실행

들어가며

지금까지 본 Basic PipelineSequential Pipeline은 모두 캘리브레이션 데이터를 forward로 순회한다. 하지만 어떤 Modifier들은 데이터가 전혀 필요 없다. FP8 weight-only, 단순 min-max 기반 INT8 weight-only 같은 스킴은 가중치 분포만 보고 스케일을 결정할 수 있다. 이런 경우를 위한 것이 DataFreePipeline이다.

또 하나 흥미로운 파이프라인은 IndependentPipeline이다. 레시피에 서로 다른 성격의 Modifier가 여러 개 있을 때, 각 Modifier를 자체 파이프라인으로 따로 돌린다. 이 글에서 두 파이프라인을 함께 다룬다.

DataFreePipeline 분석

src/llmcompressor/pipelines/data_free/pipeline.py의 코드는 정말로 짧다.

@CalibrationPipeline.register("datafree")
class DataFreePipeline(CalibrationPipeline):
    @staticmethod
    def __call__(
        model: torch.nn.Module,
        dataloader: Optional[DataLoader],      # None 이어도 상관 없음
        dataset_args: "DatasetArguments",
    ):
        # Modifier 가 가중치를 만질 수 있도록 GPU 에 배치
        dispatch_model(model)

        # 두 이벤트만 발생 — 루프 없음
        LifecycleCallbacks.calibration_epoch_start()
        LifecycleCallbacks.calibration_epoch_end()
호출 의미
dispatch_model(model) 모델을 자동으로 디바이스에 배치. GPU 가능하면 GPU
CALIBRATION_EPOCH_START 이벤트 Modifier의 on_initialize 이후 호출되는 훅
CALIBRATION_EPOCH_END 이벤트 Modifier에게 "캘리브레이션 끝났다, 최종화해라" 신호

포워드 루프가 없다. 데이터로더가 None이어도 파이프라인이 돈다. 실제 작업은 이벤트 훅을 받은 Modifier가 수행한다. 예를 들어 weight-only FP8 QuantizationModifieron_finalize에서 각 Linear 모듈의 가중치 max-abs를 계산하고 스케일을 결정한 뒤 양자화된 텐서로 교체한다. 데이터가 필요 없다.

언제 자동 선택되는가

Pipeline Registry_infer_pipeline을 다시 보면,

if len(modifiers) == 1 and isinstance(modifiers[0], QuantizationModifier):
    config = modifiers[0].resolve_quantization_config()
    if not config.requires_calibration_data():
        return "datafree"
return "sequential"

조건은 단 하나: 단일 QuantizationModifier + 캘리브레이션 데이터 불필요 설정. W8A8(가중치와 활성화 둘 다)은 활성화 통계가 필요하므로 requires_calibration_data()==True라서 data_free가 선택되지 않는다. 반면 W8A16, FP8_WEIGHT_ONLY, NVFP4_WEIGHT 같은 스킴은 False라서 data_free가 추천된다.

IndependentPipeline 분석

src/llmcompressor/pipelines/independent/pipeline.py는 조금 더 흥미롭다. 이 파이프라인은 자신이 직접 캘리브레이션을 하지 않고, 각 Modifier를 개별 파이프라인으로 돌린다.

@CalibrationPipeline.register("independent")
class IndependentPipeline(CalibrationPipeline):
    @staticmethod
    def __call__(
        model: torch.nn.Module,
        dataloader: DataLoader,
        dataset_args: "DatasetArguments",
    ):
        _logger = logger.patch(lambda r: r.update(function="IndependentPipeline"))

        session = active_session()
        modifiers = session.lifecycle.recipe.modifiers

        # 현재 레시피의 modifiers 필드를 일시적으로 None 으로 패치
        with patch_attr(session.lifecycle.recipe, "modifiers", None):
            for modifier in modifiers:
                mod_type = type(modifier).__name__
                session.lifecycle.recipe.modifiers = [modifier]  # 이 Modifier 하나만 활성

                # 이 Modifier 에 맞는 파이프라인을 재귀적으로 찾는다
                pipeline = CalibrationPipeline.from_modifiers([modifier])
                pipeline_name = pipeline.__class__.__name__
                _logger.info(f"Inferred `{pipeline_name}` for `{mod_type}`")

                pipeline(model, dataloader, dataset_args)

            # 컨텍스트 종료 시 원래 modifiers 리스트 복원

각 Modifier 하나만 활성인 상태로 from_modifiers를 호출한다. 그러면 레지스트리가 그 Modifier 하나에 맞는 최적 파이프라인을 고른다. 예를 들어 레시피가 [SmoothQuantModifier, GPTQModifier]라면, Independent는 다음과 같이 돈다.

  1. SmoothQuant 만 활성 → from_modifierssequential 반환 (SmoothQuant는 활성화 통계가 필요) → sequential로 실행
  2. GPTQ 만 활성 → from_modifierssequential 반환 → sequential로 실행

패치 컨텍스트 매니저 patch_attr(session.lifecycle.recipe, "modifiers", None)는 종료 시 원래 리스트를 복원한다. 이렇게 해야 나중에 Compression Save 단계에서 전체 레시피 기반으로 가중치를 직렬화할 수 있다.

언제 Independent를 쓰는가

independent는 추론으로 선택되지 않는다. 사용자가 oneshot(..., pipeline="independent")로 명시해야 한다. 흥미롭게도 oneshot()pipeline 기본값은 "independent"다. 즉 사용자는 기본적으로 Independent를 받는다.

이 기본값은 타당하다. Independent는 "레시피의 각 Modifier에게 가장 어울리는 파이프라인을 골라준다"는 가장 안전한 선택이기 때문이다. GPTQ 하나만 있으면 sequential로, FP8 weight-only 하나만 있으면 data_free로, 복합 레시피면 각자에게 맞게 분할해 실행한다.

네 파이프라인 비교 표

파이프라인 포워드 데이터 필요 대표 Modifier
basic 1회 필요 SmoothQuant (명시적 선택 시)
sequential 서브그래프별 2회 필요 GPTQ, AWQ, SparseGPT, AutoRound
data_free 없음 불필요 FP8 weight-only, W8A16
independent Modifier별로 달라짐 혼합 멀티 Modifier 레시피

왜 이 설계인가

1. data_free의 최소주의. 포워드 루프가 없으므로 수십 라인에 끝난다. Modifier 의 on_initialize/on_finalize 훅이 전부를 담당한다. 가장 빠른 파이프라인이자 가장 단순한 인터페이스.

2. independent의 자동 분해. 사용자가 복합 레시피를 만들어도 파이프라인 선택을 신경 쓸 필요가 없다. 각 Modifier가 자기에게 최적인 파이프라인을 받는다. "잘 모르겠으면 independent를 써라"가 llm-compressor의 권장이다.

3. patch_attr로 안전한 상태 교체. Independent는 session.lifecycle.recipe.modifiers를 일시적으로 한 Modifier 리스트로 교체한다. 컨텍스트 매니저가 종료 시 원복을 보장하므로 예외 발생 시에도 안전하다.

4. Optional[DataLoader] 시그니처. DataFree는 dataloader=None이어도 동작한다. 이 유연성 덕에 사용자가 데이터 없이 oneshot()을 호출해도 에러가 나지 않고, 자연스럽게 data_free 경로로 흐른다.

5. 일관된 CALIBRATION_EPOCH_* 이벤트. 네 파이프라인 모두 같은 이벤트 쌍을 호출한다. Modifier 입장에서는 "어떤 파이프라인에서 실행되는지" 신경 쓸 필요가 없고, 자신이 훅을 받는 순간에 필요한 일을 하면 된다.

마무리

Data-Free와 Independent는 파이프라인 시스템의 양 끝을 보여준다. 하나는 "아무것도 안 해도 되는" 최소주의, 다른 하나는 "각 Modifier가 자기 방식대로"의 메타 파이프라인이다. 다음 글은 Sequential Pipeline이 의존하는 Intermediates Cache를 본다.

참고 자료

댓글

관련 포스트

llm-compressor 의 다른글