Files
2026-07-13 12:31:40 +08:00

239 lines
6.7 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING, Any
from fouroversix import ModelQuantizationConfig, ScaleRule
from ...resources import FOUROVERSIX_CACHE_PATH, app, cache_volume, hf_secret
from ..experiment import Experiment
from ..utils import PTQMethod
from .rtn import RTNEvaluatorImpl, rtn_img
if TYPE_CHECKING:
from pathlib import Path
from sqlalchemy.orm import Session
with rtn_img.imports():
import torch
import torch.nn as nn
from fouroversix import (
FourOverSixLinear,
QuantizedModule,
fp4_matmul,
quantize_model,
)
from transformers import AutoModelForCausalLM
ALPHA_CANDIDATES = [x / 10 for x in range(11)]
WIKITEXT_TRAIN = "wikitext_train"
class FourOverSixLinearWithSmoothing(FourOverSixLinear):
"""
Drop-in replacement for `FourOverSixLinear` that implements SmoothQuant-style
scaling.
"""
def __init__(
self,
*args: list[Any],
smoothquant_alpha: float,
**kwargs: dict[str, Any],
) -> None:
super().__init__(*args, **kwargs)
self.smoothquant_alpha = smoothquant_alpha
def apply_ptq(self) -> None:
"""
Override the parent method to do nothing, since we need the high-precision
weight when doing PTQ with SmoothQuant.
"""
def forward(self, input: torch.Tensor) -> torch.Tensor:
"""Forward pass with SmoothQuant-style scaling."""
out = torch.empty(
*input.shape[:-1],
self.weight.shape[0],
device=input.device,
dtype=self.config.output_dtype.torch_dtype(),
)
fprop_activation_config = self.config.get_activation_config()
fprop_weight_config = self.config.get_weight_config(
block_scale_2d=self.config.weight_scale_2d,
)
for i in range(input.shape[0]):
s = (input[i].abs().max(dim=0).values ** self.smoothquant_alpha) / (
self.weight.abs().max(dim=0).values ** (1 - self.smoothquant_alpha)
)
out[i] = fp4_matmul(
input[i] / s[None, :],
self.weight * s[None, :],
out_dtype=self.config.output_dtype,
input_config=fprop_activation_config,
other_config=fprop_weight_config,
)
if self.bias is not None:
out = out + self.bias
return out
@app.cls(
image=rtn_img,
gpu="B200",
secrets=[hf_secret],
timeout=24 * 60 * 60,
volumes={FOUROVERSIX_CACHE_PATH.as_posix(): cache_volume},
)
class SmoothQuantEvaluator(RTNEvaluatorImpl):
"""Evaluate a model using SmoothQuant."""
@classmethod
def get_calibration_tasks(
cls,
model_name: str,
session: Session,
**kwargs: dict[str, Any],
) -> list[dict[str, Any]]:
"""
Get the kwargs for tasks that should be used to calibrate the given model for
this PTQ method before running evaluation.
"""
smoothquant_alpha = get_smoothquant_alpha(
model_name,
kwargs.get("activation_scale_rule"),
kwargs.get("weight_scale_rule"),
session,
)
calibration_experiments = get_calibration_experiments(
model_name,
kwargs.get("activation_scale_rule"),
kwargs.get("weight_scale_rule"),
session,
)
if smoothquant_alpha is None:
return [
{
"smoothquant_alpha": candidate_alpha,
"tasks": [WIKITEXT_TRAIN],
}
for candidate_alpha in ALPHA_CANDIDATES
if not any(
experiment.smoothquant_alpha == candidate_alpha
for experiment in calibration_experiments
)
]
return []
@classmethod
def get_calibrated_kwargs(
cls,
model_name: str,
session: Session,
**kwargs: dict[str, Any],
) -> dict[str, Any]:
"""
Get the calibrated kwargs for the given model and scale rules. If this model
has not yet been calibrated with these scale rules, an error will be raised.
"""
smoothquant_alpha = get_smoothquant_alpha(
model_name,
kwargs.get("activation_scale_rule"),
kwargs.get("weight_scale_rule"),
session,
)
if smoothquant_alpha is None:
msg = (
"SmoothQuant has not been calibrated for this combination of model and "
"scale rules"
)
raise ValueError(msg)
return {"smoothquant_alpha": smoothquant_alpha}
def quantize_model(
self,
model_name: str,
*,
device: str,
save_path: Path, # noqa: ARG002
smoothquant_alpha: float,
quantization_config: ModelQuantizationConfig,
trust_remote_code: bool,
) -> AutoModelForCausalLM:
"""Quantize a model using SmoothQuant."""
# Replace FourOverSixLinear with FourOverSixLinearWithSmoothing
QuantizedModule.register(
nn.Linear,
replace_existing_modules_in_registry=True,
)(FourOverSixLinearWithSmoothing)
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map=device,
trust_remote_code=trust_remote_code,
)
quantize_model(model, quantization_config, smoothquant_alpha=smoothquant_alpha)
return model
def get_calibration_experiments(
model_name: str,
activation_scale_rule: ScaleRule,
weight_scale_rule: ScaleRule,
db_session: Session,
) -> list[Experiment]:
return (
db_session.query(Experiment)
.filter(
Experiment.ptq_method == PTQMethod.smoothquant.value,
Experiment.task == WIKITEXT_TRAIN,
Experiment.model_name == model_name,
Experiment.activation_scale_rule == activation_scale_rule.value,
Experiment.weight_scale_rule == weight_scale_rule.value,
Experiment.smoothquant_alpha.isnot(None),
)
.all()
)
def get_smoothquant_alpha(
model_name: str,
activation_scale_rule: ScaleRule,
weight_scale_rule: ScaleRule,
session: Session,
) -> float | None:
calibration_experiments = get_calibration_experiments(
model_name,
activation_scale_rule,
weight_scale_rule,
session,
)
if not all(
any(
experiment.smoothquant_alpha == alpha
for experiment in calibration_experiments
)
for alpha in ALPHA_CANDIDATES
):
return None
return min(calibration_experiments, key=lambda x: x.metric_value).smoothquant_alpha