Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Change log

## [v1.3.3] 2026-09-03

### Enhancement

- Add device mode synchronization between ModelConfig and QEPConfig.

### Bug Fix

- Stop saving perm in `GPTQLinear` when `actorder=True`, as it is unnecessary for inference and causes vLLM serving errors.
- Support retrying Cholesky decomposition with an increased damping coefficient during the QEP loop.

## [v1.3.2] 2026-08-24

### Bug Fix
Expand Down
3 changes: 1 addition & 2 deletions docs/algorithms/lpcd.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ lpcd_config = LPCDConfig(
perccorr=0.5,
percdamp=0.01,
use_closed_form=True,
device="cuda:0",
)

runner = Runner(
Expand Down Expand Up @@ -131,7 +130,7 @@ You can use LPCD without QEP, but the common setup in OneComp is `GPTQ + QEP + L
| `gd_steps` | `int` | Gradient-descent steps per sub-problem | `20` |
| `gd_batch_size` | `int` | Effective batch size for gradient accumulation | `16` |
| `gd_base_lr` | `float` | Base learning rate for gradient solver | `1e-4` |
| `device` | `str` | Device for LPCD optimization | `"cuda:0"` |
| `device` | `str` | Device for LPCD optimization | `None` |

## Current Support

Expand Down
3 changes: 1 addition & 2 deletions docs/algorithms/qep.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ qep_config = QEPConfig(
general=False, # Architecture-aware (default)
percdamp=0.01, # Hessian damping
perccorr=0.5, # Correction strength
device="cuda:0", # GPU for QEP computation
exclude_layer_keywords=["mlp.down_proj"],
)

Expand Down Expand Up @@ -117,7 +116,7 @@ runner.run()
| `general` | `bool` | Use generic (architecture-independent) QEP | `False` |
| `percdamp` | `float` | Damping percentage for Hessian regularization | `0.01` |
| `perccorr` | `float` | Correction strength (0 = no correction, 1 = full)| `0.5` |
| `device` | `str` | GPU device for QEP computation | `"cuda:0"` |
| `device` | `str` | GPU device for QEP computation | `None` |
| `exclude_layer_keywords` | `list[str]` | Layer keywords excluded from error propagation | `["mlp.down_proj"]` |

!!! note
Expand Down
4 changes: 2 additions & 2 deletions docs/user-guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ qep_config = QEPConfig(
| `general` | `bool` | Use generic (architecture-independent) QEP | `False` |
| `percdamp` | `float` | Damping percentage for Hessian regularization | `0.01` |
| `perccorr` | `float` | Correction percentage for error propagation | `0.5` |
| `device` | `str` | Device for QEP computations (`"cuda"`, `"mps"`, `"cpu"`) | `"cuda:0"` |
| `device` | `str` | Device for QEP computations (`"cuda"`, `"mps"`, `"cpu"`) | `None` |
| `exclude_layer_keywords` | `list[str]` | Layer keywords excluded from error propagation | `["mlp.down_proj"]` |

!!! tip
Expand Down Expand Up @@ -188,7 +188,7 @@ lpcd_config = LPCDConfig(
| `gd_steps` | `int` | Gradient-descent steps per sub-problem | `20` |
| `gd_batch_size` | `int` | Effective batch size for gradient accumulation | `16` |
| `gd_base_lr` | `float` | Base learning rate for gradient solver | `1e-4` |
| `device` | `str` | Device for LPCD computation | `"cuda:0"` |
| `device` | `str` | Device for LPCD computation | `None` |

!!! tip
`LPCDConfig()` defaults to residual-only refinement, which is the fastest
Expand Down
2 changes: 1 addition & 1 deletion onecomp/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

"""

__version__ = "1.3.2"
__version__ = "1.3.3"
5 changes: 3 additions & 2 deletions onecomp/lpcd/_lpcd_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class LPCDConfig:
gd_steps: Number of gradient-descent epochs per sub-problem.
gd_batch_size: Effective batch size for gradient accumulation.
gd_base_lr: Base learning rate for gradient-descent solver.
device: Device to perform LPCD optimisation on.
device: Device to perform LPCD optimisation on. Default is None.
When None, Runner synchronises it with ModelConfig.device.

Examples:
Minimal (residual correction only, fast)::
Expand Down Expand Up @@ -53,4 +54,4 @@ class LPCDConfig:
gd_steps: int = 20
gd_batch_size: int = 16
gd_base_lr: float = 1e-4
device: str = "cuda:0"
device: str = None
4 changes: 2 additions & 2 deletions onecomp/qep/_qep_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class QEPConfig:
Default is 0.5.
device (str): Device to use for QEP computations
(e.g., "cuda", "mps", "cpu").
Default is "cuda:0".
Default is None. When None, Runner synchronises it with ModelConfig.device.
exclude_layer_keywords (list[str]): List of keywords to identify
layers excluded from error propagation. Layers whose names
contain any of these keywords will be excluded.
Expand All @@ -52,6 +52,6 @@ class QEPConfig:
general: bool = False
percdamp: float = 0.01
perccorr: float = 0.5
device: str = "cuda:0"
device: str = None
exclude_layer_keywords: list[str] = field(default_factory=lambda: ["mlp.down_proj"])
# TODO: exclude_layer_keywords depends on the architecture and needs to be fixed
32 changes: 26 additions & 6 deletions onecomp/quantizer/_quantizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,12 +400,32 @@ def adjust_weight(
weight[:, dead] = 0

# QEP correction
damp = percdamp * torch.mean(torch.diag(hessian))
diag = torch.arange(hessian.shape[0], device=hessian.device)
hessian[diag, diag] += damp
rhs = weight @ delta_hatX
delta_weight = _safe_cholesky_and_solve(hessian, rhs).t()
weight = weight + (perccorr * delta_weight)
damp_scale = 1.0
max_retries = 5
for attempt in range(max_retries):
try:
damp = percdamp * torch.mean(torch.diag(hessian))
diag = torch.arange(hessian.shape[0], device=hessian.device)
hessian[diag, diag] += damp
rhs = weight @ delta_hatX
delta_weight = _safe_cholesky_and_solve(hessian, rhs).t()
weight = weight + (perccorr * delta_weight)
break
except torch._C._LinAlgError:
damp_scale *= 10.0
extra = damp_scale * damp
hessian[diag, diag] += extra
self.logger.warning(
"Cholesky failed (attempt %d/%d); adding extra damping %.2e",
attempt + 1,
max_retries,
extra,
)
else:
raise RuntimeError(
"Cholesky decomposition failed after %d damping attempts. "
"The Hessian may be severely ill-conditioned." % max_retries
)

if isinstance(module, Conv1d):
weight = weight.t()
Expand Down
6 changes: 0 additions & 6 deletions onecomp/quantizer/gptq/gptq_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,12 +414,6 @@ def _get_zero_int():
if self._gemlite_layer is not None:
self.using_gemlite = True

# Permutation order
if perm is not None and actorder:
self.register_buffer("perm", perm.to(device))
else:
self.perm = None

# Bias
if bias is not None:
self.register_buffer("bias", bias.to(torch.float16).to(device))
Expand Down
4 changes: 4 additions & 0 deletions onecomp/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,13 @@ def __init__(
self.qep_config = None
if qep:
self.qep_config = qep_config if qep_config is not None else QEPConfig()
if self.qep_config.device is None and self.model_config is not None:
self.qep_config.device = str(self.model_config.get_device())
self.lpcd_config = None
if lpcd:
self.lpcd_config = lpcd_config if lpcd_config is not None else LPCDConfig()
if self.lpcd_config.device is None and self.model_config is not None:
self.lpcd_config.device = str(self.model_config.get_device())
self.report_progress = report_progress

def check(self):
Expand Down
5 changes: 2 additions & 3 deletions tests/onecomp/lpcd/test_lpcd_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ def test_default_solver_params(self):
assert cfg.gd_base_lr > 0.0

def test_default_device(self):
"""Default device is a CUDA device string."""
"""Default device is None."""
cfg = LPCDConfig()
assert isinstance(cfg.device, str)
assert cfg.device.startswith("cuda")
assert cfg.device is None


class TestLPCDConfigCustomValues:
Expand Down
7 changes: 6 additions & 1 deletion tests/onecomp/quantizer/autobit/test_autobit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import pytest

from onecomp import CalibrationConfig, ModelConfig, Runner
from onecomp import CalibrationConfig, ModelConfig, QEPConfig, Runner
from onecomp.quantizer.autobit._autobit import AutoBitQuantizer
from onecomp.quantizer.gptq import GPTQ
from onecomp.utils import estimate_wbits_from_vram
Expand Down Expand Up @@ -54,6 +54,7 @@ def test_autobit_small_model_ilp_with_groupsize():
quantizer=quantizer,
calibration_config=CalibrationConfig(max_length=512, num_calibration_samples=128),
qep=True,
qep_config=QEPConfig(device="cuda:0"),
)
runner.run()

Expand All @@ -73,6 +74,7 @@ def test_autobit_small_model_ilp():
quantizer=quantizer,
calibration_config=CalibrationConfig(max_length=512, num_calibration_samples=128),
qep=True,
qep_config=QEPConfig(device="cuda:0"),
)
runner.run()

Expand All @@ -95,6 +97,7 @@ def test_autobit_small_model_dbf():
quantizer=quantizer,
calibration_config=CalibrationConfig(max_length=512, num_calibration_samples=128),
qep=True,
qep_config=QEPConfig(device="cuda:0"),
)
runner.run()

Expand All @@ -121,6 +124,7 @@ def test_autobit_small_model_error():
quantizer=quantizer,
calibration_config=CalibrationConfig(max_length=512, num_calibration_samples=128),
qep=True,
qep_config=QEPConfig(device="cuda:0"),
)
with pytest.raises(ValueError, match="target_bit=.* is below 1.0 bpw"):
runner.run()
Expand All @@ -142,5 +146,6 @@ def test_autobit_large_model():
quantizer=quantizer,
calibration_config=CalibrationConfig(max_length=512, num_calibration_samples=128),
qep=True,
qep_config=QEPConfig(device="cuda:0"),
)
runner.run()
68 changes: 68 additions & 0 deletions tests/onecomp/test_qep_damping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from unittest.mock import patch

import pytest
import torch

import onecomp.quantizer._quantizer as quantizer_module
from onecomp.quantizer._quantizer import Quantizer


class FakeQuantizer(Quantizer):
def quantize_layer(self, *args, **kwargs):
raise NotImplementedError


@pytest.fixture
def fake_quantizer():
return FakeQuantizer()


def test_adjust_weight_succeeds_on_first_cholesky_attempt(fake_quantizer):
module = torch.nn.Linear(2, 2, bias=False)
original_solve = quantizer_module._safe_cholesky_and_solve

with patch.object(
quantizer_module,
"_safe_cholesky_and_solve",
wraps=original_solve,
) as solve:
Quantizer.adjust_weight(
fake_quantizer,
module,
quant_input_activation=None,
original_input_activation=None,
original_hessian=torch.eye(2),
original_delta_hatX=torch.zeros(2, 2),
)

solve.assert_called_once()


def test_adjust_weight_retries_cholesky_with_increased_damping(fake_quantizer):
module = torch.nn.Linear(2, 2, bias=False)

# Set up a Hessian that is not positive definite, which will cause the first Cholesky attempt to fail.
hessian = torch.tensor(
[
[1.0, 0.0],
[0.0, -0.015],
]
)

original_solve = quantizer_module._safe_cholesky_and_solve

with patch.object(
quantizer_module,
"_safe_cholesky_and_solve",
wraps=original_solve,
) as solve:
Quantizer.adjust_weight(
fake_quantizer,
module,
quant_input_activation=None,
original_input_activation=None,
original_hessian=hessian,
original_delta_hatX=torch.zeros(2, 2),
)

assert solve.call_count > 1
46 changes: 46 additions & 0 deletions tests/vllm_plugins/test_enable_actorder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Regression tests for actorder GPTQLinear checkpoint state."""

import torch
from torch import nn

from onecomp.quantizer.gptq.gptq_layer import GPTQLinear


def _make_actorder_layer(in_features, out_features, groupsize, perm):
num_groups = in_features // groupsize
return GPTQLinear(
in_features=in_features,
out_features=out_features,
wbits=4,
groupsize=groupsize,
actorder=True,
quantized_weight=torch.ones(out_features, in_features, dtype=torch.int32),
scale=torch.ones(num_groups, out_features),
zero=torch.ones(num_groups, out_features),
perm=perm,
bias=torch.zeros(out_features),
device="cpu",
pack_weights=False,
use_gemlite=False,
)


def test_actorder_network_state_dict_keeps_g_idx_but_not_perm():
first_perm = torch.tensor([2, 0, 3, 1, 6, 4, 7, 5])
second_perm = torch.tensor([1, 3, 0, 2])
network = nn.Sequential(
_make_actorder_layer(8, 4, 2, first_perm),
nn.ReLU(),
_make_actorder_layer(4, 3, 2, second_perm),
)

output = network(torch.randn(2, 8))

assert output.shape == (2, 3)
assert torch.isfinite(output).all()
for layer, perm in zip((network[0], network[2]), (first_perm, second_perm)):
state_dict = layer.state_dict()
assert "perm" not in state_dict
assert "g_idx" in state_dict
expected_g_idx = torch.argsort(perm) // layer.groupsize
assert torch.equal(state_dict["g_idx"], expected_g_idx)