Skip to content

llmcompressor.modifiers.autoround

Modules:

Classes:

Functions:

AutoRoundModifier

Bases: Modifier, QuantizationMixin

Implements the AutoRound algorithm from https://aclanthology.org/2024.findings-emnlp.662.pdf. This modifier leverages signed gradient descent (SignSGD) optimizer and block-wise loss to optimize rounding values and weight clipping in a few steps.

Sample yaml:

test_stage:
  modifiers:
    AutoRoundModifier:
      iters: 200
      config_groups:
        group_0:
          targets:
            - "Linear"
          input_activations: null
          output_activations: null
          weights:
            num_bits: 4
            type: "int"
            symmetric: true
            strategy: group
            group_size: 128

Lifecycle:

  • on_initialize
    • apply config to model
  • on_calibration_start
    • add input capture hooks to decoding layers
  • on_sequential_epoch_end
    • apply_autoround
    • post_autoround_cleanup
  • on_calibration_end
    • remove_hooks()
    • model.apply(freeze_module_quantization)

Parameters:

  • config_groups

    dictionary specifying quantization schemes to apply to target modules. Modules not matching a scheme target will NOT be quantized.

  • targets

    list of layer names to quantize if a scheme is provided. Defaults to Linear layers

  • ignore

    optional list of module class names or submodule names to not quantize even if they match a target in config_groups. Defaults to empty list.

  • scheme

    a single quantization scheme to apply to the model. This is a dictionary that supports all keys from QuantizationScheme except targets, which will be set to the targets parameter set at the modifier level.

  • iters

    number of tuning iterations per block (decoding layer). Higher values typically improve accuracy at the cost of longer tuning time. Defaults to 200.

  • enable_torch_compile

    whether to enable torch.compile to accelerate the tuning loop. Disable if your environment or model encounters compilation issues. Defaults to True.

  • batch_size

    calibration/tuning batch size used by AutoRound when optimizing rounding/clipping parameters. Larger values can improve stability but require more memory. Defaults to 8.

  • device_ids

    optional device map string for layer dispatch during tuning. Examples: "0,1" for cuda:0 and cuda:1, or "auto" to use all available GPUs. When None, no dispatching occurs and the model remains on its current device. Defaults to None.

Methods:

  • apply_autoround

    Applies AutoRound quantization tuning on the current decoding layer.

  • on_calibration_end

    Finish calibrating by removing observers and calibration hooks

  • on_initialize

    Initialize the model state for quantization and calibration.

  • start_calibration

    Register activation calibration hooks and enable quantization as we calibrate

apply_autoround

apply_autoround(state, modules)

Applies AutoRound quantization tuning on the current decoding layer.

The tuning logic is as follows: for iter in range(iters): quant_output = forward(layer, cached_inputs) loss = mse_loss(quant_output, original_output) loss.backward() optimizer.step() if loss < best_loss: best_params = update_params(layer)

For more details, please refer to the AutoRound repository: https://github.com/intel/auto-round/

Source code in src/llmcompressor/modifiers/autoround/base.py
def apply_autoround(self, state, modules):
    """
    Applies AutoRound quantization tuning on the current decoding layer.

    The tuning logic is as follows:
    for iter in range(iters):
        quant_output = forward(layer, cached_inputs)
        loss = mse_loss(quant_output, original_output)
        loss.backward()
        optimizer.step()
        if loss < best_loss:
            best_params = update_params(layer)

    For more details, please refer to the AutoRound repository:
    https://github.com/intel/auto-round/
    """
    modules = modules or []

    decoding_layers = [m for m in modules if self._is_decoding_layer(m)]
    if len(decoding_layers) == 0:
        return
    if len(decoding_layers) != 1:
        raise ValueError(
            "Only one decoding layer is expected in the modules list, "
            f"found {len(decoding_layers)}."
        )
    decoding_layer = decoding_layers[0]

    logger.info("Applying AutoRound on layer {}", decoding_layer._tmp_name)

    # Build wrapped_model for AutoRound initialization
    wrapped_model = _wrap_decoding_layer(decoding_layer)
    wrapped_model.name_or_path = state.model.name_or_path
    wrapped_model.config = state.model.config

    # Build kwargs for AutoRound initialization
    ar_quant_scheme = self._mapping_config_to_autoround()
    layer_config = self._build_layer_config_for_autoround(wrapped_model)
    ignore_layers = self.get_unquantized_layer_names(decoding_layer)
    kwargs = {
        "tokenizer": "",  # A placeholder
        "scheme": ar_quant_scheme,
        "layer_config": layer_config or None,
        "iters": self.iters,
        "lr": self.lr,
        "enable_torch_compile": self.enable_torch_compile,
        "batch_size": self.batch_size,
        "device_map": self.device_ids,
        "ignore_layers": ",".join(ignore_layers) if ignore_layers else "",
        "disable_opt_rtn": self.disable_opt_rtn,
    }

    llmc_registered_qparams = self._preprocess_qparams(decoding_layer)
    with (
        torch.enable_grad(),
        align_module_device(decoding_layer),
        suspend_offloading(wrapped_model),
    ):
        self._update_device_map_for_dp(kwargs)
        ar = AutoRound(
            model=wrapped_model,
            **kwargs,
        )
        ar.configure_layer_config(enable_gguf_official_mixed=False)
        ar.batch_dim = 0
        first_param = next(decoding_layer.parameters())
        device = first_param.device
        cur_inputs = self._all_module_input[decoding_layer._tmp_name]
        self._set_attention_masks(ar, decoding_layer, cur_inputs)
        decoding_layer.tuning_device = device
        # Only hand device placement to AutoRound when the caller explicitly
        # requested it or when a rank is configured to use a local GPU group.
        auto_offload = False
        needs_multi_gpu = (
            self.device_ids is not None or get_local_gpu_group_size() > 1
        )
        if needs_multi_gpu:
            # Let AutoRound own placement within the rank-local GPU group.
            device = get_main_device()
            decoding_layer.to("cpu")
            auto_offload = True

        # Ensure cached inputs are on the same device as the block.
        # Calibration forward may have run on a different GPU.
        cur_inputs = self._move_inputs_to(cur_inputs, device)
        ar_inputs = [((args, kwargs),) for args, kwargs in cur_inputs]

        q_input, _ = ar.quantize_block(
            block=decoding_layer,
            inputs=ar_inputs,
            q_input=self._q_input,
            device=str(device),
            auto_offload=auto_offload,
        )
        self._q_input = q_input

        decoding_layer = self._unwrapper_quantized_layer(decoding_layer)

    decoding_layer.eval()
    # Update offload parameters and remove temporary attributes
    self._postprocess_qparams(decoding_layer, llmc_registered_qparams)

on_calibration_end

on_calibration_end(state: State, event: Event, **kwargs)

Finish calibrating by removing observers and calibration hooks

Source code in src/llmcompressor/modifiers/autoround/base.py
def on_calibration_end(self, state: State, event: Event, **kwargs):
    """
    Finish calibrating by removing observers and calibration hooks
    """
    QuantizationMixin.end_calibration(self, state.model)
    self._remove_temporary_names(state.model)
    self.remove_hooks()
    self._q_input = None

on_initialize

on_initialize(state: State, **kwargs) -> bool

Initialize the model state for quantization and calibration.

Parameters:

  • state (State) –

    session state storing input model and calibration data

Source code in src/llmcompressor/modifiers/autoround/base.py
def on_initialize(self, state: State, **kwargs) -> bool:
    """
    Initialize the model state for quantization and calibration.

    :param state: session state storing input model and calibration data
    """
    # apply config to model and prepare calibration hooks.
    # Wrap in disable_onloading to suppress DistributedCPUCache's
    # per-param broadcast+barrier when creating quant params (scale,
    # zero_point). With LLMCOMPRESSOR_GPUS_PER_GROUP > 1, modules have varying GPU
    # execution devices, causing GPU→CPU copy timing to vary between
    # ranks → broadcast deadlock. Quant params are deterministic —
    # each rank computes identical values, no sync needed.
    if QuantizationMixin.has_config(self):
        from compressed_tensors.offload import disable_onloading

        with disable_onloading():
            QuantizationMixin.initialize_quantization(self, state.model)

    # prepare module names
    self._add_temporary_names(state.model)
    # freeze all model parameters
    for _, param in state.model.named_parameters():
        param.requires_grad_(False)

    self._sequential_targets = infer_sequential_targets(
        state.model, sequential_targets=kwargs.get("sequential_targets")
    )
    return True

start_calibration

start_calibration(model: Module)

Register activation calibration hooks and enable quantization as we calibrate

Parameters:

  • model (Module) –

    model to prepare for calibration

Source code in src/llmcompressor/modifiers/autoround/base.py
def start_calibration(self, model: torch.nn.Module):
    """
    Register activation calibration hooks and enable quantization as we calibrate

    :param model: model to prepare for calibration
    """
    targets = match_named_modules(model, self.targets, self.ignore)
    if targets_embeddings(model, targets):
        untie_word_embeddings(model)

    for _, module in match_named_modules(model, self.targets, self.ignore):
        # skip register observers for auto-round
        apply_calibration_status(module)

    model.apply(enable_quantization)  # quantize at the same time as calibrate

fix_attention_mask

fix_attention_mask(
    mask: Tensor | list[int] | list[list[int]],
) -> torch.Tensor

Normalize attention masks for AutoRound custom datasets.

AutoRound expects at least one masked position when the calibration mask is fully dense. When every token is marked valid, set the final position to 0 while preserving the original dtype and shape. More details can be found here: https://github.com/intel/auto-round/blob/50ee58c9e176e9da2a744dbe6ed220f26e80eccd/auto_round/calibration/llm.py#L315-L355

Source code in src/llmcompressor/modifiers/autoround/utils.py
def fix_attention_mask(
    mask: torch.Tensor | list[int] | list[list[int]],
) -> torch.Tensor:
    """
    Normalize attention masks for AutoRound custom datasets.

    AutoRound expects at least one masked position when the calibration mask is fully
    dense. When every token is marked valid, set the final position to 0 while
    preserving the original dtype and shape.
    More details can be found here: https://github.com/intel/auto-round/blob/50ee58c9e176e9da2a744dbe6ed220f26e80eccd/auto_round/calibration/llm.py#L315-L355
    """
    normalized_mask = torch.as_tensor(mask).clone()
    if normalized_mask.shape[-1] == 0:
        return normalized_mask

    if (
        normalized_mask.ndim == 4
        and normalized_mask.shape[1] == 1
        and normalized_mask.shape[2] == 1
    ):
        normalized_mask = normalized_mask.squeeze(2).squeeze(1)

    if normalized_mask.ndim in (3, 4):
        normalized_mask = _collapse_causal_attention_mask(normalized_mask)

    if normalized_mask.ndim == 1:
        if torch.all(normalized_mask == 1):
            normalized_mask[-1] = 0
        return normalized_mask

    if normalized_mask.ndim == 2:
        all_ones_rows = torch.all(normalized_mask == 1, dim=1)
        if torch.any(all_ones_rows):
            normalized_mask[all_ones_rows, -1] = 0
        return normalized_mask

    raise ValueError(
        "Unsupported attention mask shape for AutoRound: "
        f"{tuple(normalized_mask.shape)}"
    )

fix_batch_if_needed

fix_batch_if_needed(
    batch: dict[str, list[int] | list[list[int]]],
) -> dict[str, list[int] | list[list[int]]]

Normalize custom calibration batches so their attention masks work with AutoRound.

Source code in src/llmcompressor/modifiers/autoround/base.py
def fix_batch_if_needed(
    batch: dict[str, list[int] | list[list[int]]],
) -> dict[str, list[int] | list[list[int]]]:
    """
    Normalize custom calibration batches so their attention masks work with AutoRound.
    """
    attention_mask = batch.get("attention_mask")
    if attention_mask is None:
        return batch

    batch["attention_mask"] = fix_attention_mask(attention_mask).tolist()
    return batch

init_gpu_group_dist

init_gpu_group_dist(
    gpus_per_group: int | None = None,
) -> tuple[int, int, int]

Initialize DDP for AutoRound's rank-local GPU grouping.

Standard DDP binds each rank to LOCAL_RANK. AutoRound can instead use a rank-local group of GPUs to tune a single decoding block, so the process group must be initialized on LOCAL_RANK * gpus_per_group from the start.

Source code in src/llmcompressor/modifiers/autoround/utils.py
def init_gpu_group_dist(gpus_per_group: int | None = None) -> tuple[int, int, int]:
    """
    Initialize DDP for AutoRound's rank-local GPU grouping.

    Standard DDP binds each rank to ``LOCAL_RANK``. AutoRound can instead use a
    rank-local group of GPUs to tune a single decoding block, so the process group
    must be initialized on ``LOCAL_RANK * gpus_per_group`` from the start.
    """
    gpus_per_group = (
        get_local_gpu_group_size() if gpus_per_group is None else gpus_per_group
    )
    if gpus_per_group < 1:
        raise ValueError(
            f"LLMCOMPRESSOR_GPUS_PER_GROUP must be >= 1, got {gpus_per_group}"
        )

    if gpus_per_group == 1:
        init_dist()
        rank = dist.get_rank()
        world_size = dist.get_world_size()
        return rank, world_size, int(os.environ["LOCAL_RANK"])

    if "TORCHELASTIC_RUN_ID" not in os.environ:
        raise ValueError(
            "Cannot find distributed environment. "
            "Please make sure you are using `torchrun --nproc-per-node ...`."
        )

    rank = int(os.environ["RANK"])
    local_rank = int(os.environ["LOCAL_RANK"])
    world_size = int(os.environ["WORLD_SIZE"])
    main_gpu = local_rank * gpus_per_group
    device_count = torch.accelerator.device_count()

    if main_gpu + gpus_per_group > device_count:
        raise ValueError(
            "Requested GPU group exceeds local visible devices: "
            f"main_gpu={main_gpu}, gpus_per_group={gpus_per_group}, "
            f"device_count={device_count}"
        )

    accel_type = torch.accelerator.current_accelerator().type
    if accel_type == "cuda":
        backend = "nccl"
    elif accel_type == "xpu":
        backend = "xccl"
    else:
        backend = "gloo"

    torch.accelerator.set_device_index(main_gpu)
    dist.init_process_group(
        backend=backend,
        init_method="env://",
        rank=rank,
        world_size=world_size,
        device_id=torch.device(f"{accel_type}:{main_gpu}"),
    )
    dist.barrier()
    return rank, world_size, main_gpu