[weight checker] refactor: add precision branch; allow ULP quant err; used chunked compare (#28974)

This commit is contained in:
Yueming Yuan
2026-06-28 18:44:05 -07:00
committed by GitHub
parent bd3b252e0a
commit e1ca92a7fd
7 changed files with 706 additions and 189 deletions
+1
View File
@@ -1712,6 +1712,7 @@ class ResumeMemoryOccupationReqOutput(BaseReq, kw_only=True):
class CheckWeightsReqInput(BaseReq, kw_only=True): class CheckWeightsReqInput(BaseReq, kw_only=True):
action: str = "checksum" action: str = "checksum"
allow_quant_error: bool = False
class CheckWeightsReqOutput(BaseReq, kw_only=True): class CheckWeightsReqOutput(BaseReq, kw_only=True):
@@ -268,12 +268,17 @@ class SchedulerWeightUpdaterManager:
def check_weights(self, recv_req: CheckWeightsReqInput): def check_weights(self, recv_req: CheckWeightsReqInput):
try: try:
payload = self.tp_worker.model_runner.check_weights(action=recv_req.action) payload = self.tp_worker.model_runner.check_weights(
action=recv_req.action, allow_quant_error=recv_req.allow_quant_error
)
if self.draft_worker is not None: if self.draft_worker is not None:
draft_runner = _get_draft_model_runner(self.draft_worker) draft_runner = _get_draft_model_runner(self.draft_worker)
if draft_runner is not None: if draft_runner is not None:
draft_payload = draft_runner.check_weights(action=recv_req.action) draft_payload = draft_runner.check_weights(
action=recv_req.action,
allow_quant_error=recv_req.allow_quant_error,
)
if payload is not None and draft_payload is not None: if payload is not None and draft_payload is not None:
payload = _merge_checksum_payloads(payload, draft_payload) payload = _merge_checksum_payloads(payload, draft_payload)
@@ -3205,8 +3205,10 @@ class ModelRunner(ModelRunnerKVCacheMixin):
) )
ShardedStateLoader.save_model(self.model, path, pattern, max_size) ShardedStateLoader.save_model(self.model, path, pattern, max_size)
def check_weights(self, action: str): def check_weights(self, action: str, allow_quant_error: bool = False):
return self._weight_checker.handle(action=action) return self._weight_checker.handle(
action=action, allow_quant_error=allow_quant_error
)
def update_weights_from_ipc(self, recv_req): def update_weights_from_ipc(self, recv_req):
"""Update weights from IPC for checkpoint-engine integration.""" """Update weights from IPC for checkpoint-engine integration."""
+113 -97
View File
@@ -1,17 +1,20 @@
import hashlib import hashlib
import logging import logging
import time import time
from typing import Dict, Iterable, Optional, Set, Tuple from typing import Dict, Iterable, NamedTuple, Optional, Set
import torch import torch
import torch.distributed as dist import torch.distributed as dist
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sglang.srt.layers.quantization.fp8_utils import (
block_quant_dequant,
inverse_transform_scale_ue8m0,
)
from sglang.srt.managers.mm_utils import tensor_hash from sglang.srt.managers.mm_utils import tensor_hash
from sglang.srt.utils.weight_checker_comparator import (
CHUNK_NUMEL,
ComparableWeight,
RawComparable,
compare_weights,
select_comparable_weight,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -37,6 +40,17 @@ class ChecksumInfo(_StrictBaseModel):
parallelism_info: ParallelismInfo parallelism_info: ParallelismInfo
class CheckEntry(NamedTuple):
name: str
should_compare: bool
comparable: ComparableWeight
class QuantizedWeight(NamedTuple):
comparable_cls: type[ComparableWeight]
scale_name: str
_NON_PERSISTENT_BUFFER_PATTERNS = ( _NON_PERSISTENT_BUFFER_PATTERNS = (
"cos_sin_cache", "cos_sin_cache",
"inv_freq", "inv_freq",
@@ -54,14 +68,16 @@ class WeightChecker:
self._model_runner = model_runner self._model_runner = model_runner
self._snapshot_tensors = None self._snapshot_tensors = None
def handle(self, action: str) -> Optional[Dict]: def handle(self, action: str, allow_quant_error: bool = False) -> Optional[Dict]:
logger.info(f"[WeightChecker] handle action={action}") logger.info(
f"[WeightChecker] handle action={action} allow_quant_error={allow_quant_error}"
)
if action == "snapshot": if action == "snapshot":
return self._snapshot() return self._snapshot()
elif action == "reset_tensors": elif action == "reset_tensors":
return self._reset_tensors() return self._reset_tensors()
elif action == "compare": elif action == "compare":
return self._compare() return self._compare(allow_quant_error=allow_quant_error)
elif action == "checksum": elif action == "checksum":
return self._compute_checksum() return self._compute_checksum()
else: else:
@@ -82,43 +98,44 @@ class WeightChecker:
continue continue
param.copy_(_random_like(param)) param.copy_(_random_like(param))
def _compare(self): def _compare(self, allow_quant_error: bool = False):
assert self._snapshot_tensors is not None assert self._snapshot_tensors is not None
quantized_set = _build_quantized_set(self._model_runner.model)
skip_compare_names = { skip_compare_names = {
name name
for name, param in self._model_state() for name, param in self._model_state()
if getattr(param, "_skip_weight_check", False) if getattr(param, "_skip_weight_check", False)
} }
_check_tensors( _check_tensors(
expect_tensors=_postprocess_tensors( expect_tensors=_build_check_entries(
self._snapshot_tensors, skip_compare_names self._snapshot_tensors, skip_compare_names, quantized_set
), ),
actual_tensors=_postprocess_tensors( actual_tensors=_build_check_entries(
dict(self._model_state()), skip_compare_names dict(self._model_state()), skip_compare_names, quantized_set
), ),
allow_quant_error=allow_quant_error,
) )
def _compute_checksum(self) -> Dict: def _compute_checksum(self) -> Dict:
torch.cuda.synchronize() torch.cuda.synchronize()
start = time.perf_counter() start = time.perf_counter()
quantized_set = _build_quantized_set(self._model_runner.model)
skip_compare_names = { skip_compare_names = {
name name
for name, param in self._model_state() for name, param in self._model_state()
if getattr(param, "_skip_weight_check", False) if getattr(param, "_skip_weight_check", False)
} }
# Reuse the snapshot/compare postprocess pipeline so fp8 weights are # Hash the dequantized weight so two (qweight, scale) pairs with the same
# dequantized to bf16 before hashing — two (qweight, scale) pairs that # bf16 hash equal.
# produce the same bf16 must produce the same checksum. checksums = {}
checksums = { for name, should_compare, comparable in _build_check_entries(
name: _hash_tensor(tensor.data) dict(self._model_state()), skip_compare_names, quantized_set
for name, should_compare, tensor in _postprocess_tensors( ):
dict(self._model_state()), skip_compare_names if should_compare:
) checksums[name] = _hash_tensor(comparable.dequantize().data)
if should_compare
}
h = hashlib.sha256() h = hashlib.sha256()
for name in sorted(checksums): for name in sorted(checksums):
@@ -162,42 +179,50 @@ def _hash_tensor(t: torch.Tensor) -> str:
def _check_tensors( def _check_tensors(
expect_tensors: Iterable[Tuple[str, bool, torch.Tensor]], expect_tensors: Iterable[CheckEntry],
actual_tensors: Iterable[Tuple[str, bool, torch.Tensor]], actual_tensors: Iterable[CheckEntry],
allow_quant_error: bool = False,
): ):
from sglang.srt.debug_utils.dumper import get_tensor_info
good_names = [] good_names = []
error_messages = [] error_messages = []
info_messages = [] info_messages = []
for (expect_name, expect_should_compare, expect), ( for (expect_name, should_compare, expect_comparable), (
actual_name, actual_name,
actual_should_compare, actual_should_compare,
actual, actual_comparable,
) in zip(expect_tensors, actual_tensors, strict=True): ) in zip(expect_tensors, actual_tensors, strict=True):
assert expect_name == actual_name, f"{expect_name=} {actual_name=}" assert expect_name == actual_name, f"{expect_name=} {actual_name=}"
assert ( assert (
expect_should_compare == actual_should_compare should_compare == actual_should_compare
), f"{expect_should_compare=} {actual_should_compare=}" ), f"{should_compare=} {actual_should_compare=}"
name = expect_name name = expect_name
should_compare = expect_should_compare
expect = expect.cuda() try:
actual = actual.cuda() equal, max_abs_err, mean_abs_err, num_exceed = compare_weights(
expect_comparable, actual_comparable
if torch.all(expect == actual):
good_names.append(name)
else:
abs_diff = (actual.float() - expect.float()).abs()
msg = (
f"name={name} "
f"max_abs_err={abs_diff.max()} "
f"mean_abs_err={abs_diff.mean()} "
f"{get_tensor_info(expect)=} "
f"{get_tensor_info(actual)=} "
) )
(error_messages if should_compare else info_messages).append(msg) except Exception as e:
e.add_note(
f"when handling {name=} expect={expect_comparable!r} actual={actual_comparable!r}"
)
raise
if equal:
good_names.append(name)
continue
msg = (
f"name={name} "
f"max_abs_err={max_abs_err} "
f"mean_abs_err={mean_abs_err} "
f"num_exceed={num_exceed} "
f"expect={expect_comparable!r} actual={actual_comparable!r} "
)
if not should_compare:
info_messages.append(msg)
elif allow_quant_error and num_exceed == 0:
info_messages.append(msg + "(within quantization ULP tolerance)")
else:
error_messages.append(msg)
logger.info(f"[check_tensors] equal tensors: {good_names}") logger.info(f"[check_tensors] equal tensors: {good_names}")
if len(info_messages) > 0: if len(info_messages) > 0:
@@ -212,7 +237,12 @@ def _random_like(t: torch.Tensor):
dtype = t.dtype dtype = t.dtype
if dtype.is_floating_point: if dtype.is_floating_point:
return torch.rand(shape, device=device, dtype=torch.float32).to(dtype) out = torch.empty(shape, device=device, dtype=dtype)
for chunk in out.view(-1).split(CHUNK_NUMEL):
chunk.copy_(
torch.rand(chunk.shape, device=device, dtype=torch.float32).to(dtype)
)
return out
if dtype == torch.bool: if dtype == torch.bool:
return torch.rand(shape, device=device) > 0.5 return torch.rand(shape, device=device) > 0.5
@@ -223,58 +253,44 @@ def _random_like(t: torch.Tensor):
) )
def _postprocess_tensors( def _build_quantized_set(model) -> Dict[str, QuantizedWeight]:
"""Run the router over the model: {weight_name: QuantizedWeight} for each
quantized weight; weights absent from the set compare raw."""
quantized_set = {}
for module_name, module in model.named_modules():
comparable_cls = select_comparable_weight(getattr(module, "quant_method", None))
if comparable_cls is None:
continue
prefix = f"{module_name}." if module_name else ""
own = {name for name, _ in module.named_parameters(recurse=False)}
for name in own:
scale = name.replace("weight", "weight_scale_inv")
if name.endswith("weight") and scale in own:
quantized_set[prefix + name] = QuantizedWeight(
comparable_cls, prefix + scale
)
return quantized_set
def _build_check_entries(
raw: Dict[str, torch.Tensor], raw: Dict[str, torch.Tensor],
skip_compare_names: Set[str], skip_compare_names: Set[str],
) -> Iterable[Tuple[str, bool, torch.Tensor]]: quantized_set: Optional[Dict[str, QuantizedWeight]] = None,
from sglang.srt.debug_utils.dumper import get_tensor_info ) -> Iterable[CheckEntry]:
"""Yields a CheckEntry per weight; quantized weights consume their scale, everything
else is raw."""
skip_compare_names = set(skip_compare_names) skip_compare_names = set(skip_compare_names)
quantized_set = quantized_set or {}
scale_names = {qw.scale_name for qw in quantized_set.values()}
# Skip non-persistent buffers (registered with persistent=False; recomputed for name, tensor in raw.items():
# after weight load and not part of the synced payload). if name in scale_names:
for name in raw: continue # compared via its weight's comparable
if _is_non_persistent_buffer_name(name): if name in quantized_set:
skip_compare_names.add(name) qw = quantized_set[name]
logger.info(f"[check_tensors] Skipping non-persistent buffer: {name}") yield CheckEntry(name, True, qw.comparable_cls(tensor, raw[qw.scale_name]))
else:
# dequant fp8 should_compare = name not in skip_compare_names and (
quant_names = [ not _is_non_persistent_buffer_name(name)
name
for name in raw
# Match: `something.weight`, `something.experts.w2_weight`
if name.endswith("weight") and name.replace("weight", "weight_scale_inv") in raw
]
quant_scale_names = [
name.replace("weight", "weight_scale_inv") for name in quant_names
]
skip_compare_names.update(quant_names)
skip_compare_names.update(quant_scale_names)
for name in quant_names:
w_q = raw[name]
w_s = raw[name.replace("weight", "weight_scale_inv")]
try:
if w_s.dtype == torch.int32:
# UE8M0 packed format (Blackwell DeepGEMM)
w_s_for_dequant = inverse_transform_scale_ue8m0(w_s, mn=w_q.shape[-2])
else:
w_s_for_dequant = w_s
w_dequant = block_quant_dequant(
w_q,
w_s_for_dequant,
# TODO do not hardcode
block_size=[128, 128],
dtype=torch.bfloat16,
) )
yield name, True, w_dequant yield CheckEntry(name, should_compare, RawComparable(tensor))
except Exception as e:
e.add_note(
f"when handling {name=} {get_tensor_info(w_q)=} {get_tensor_info(w_s)=}"
)
raise
for name in raw:
should_compare = name not in skip_compare_names
yield name, should_compare, raw[name]
@@ -0,0 +1,166 @@
from typing import Iterable, NamedTuple, Optional, Tuple
import torch
from sglang.srt.layers.quantization.fp8 import Fp8LinearMethod, Fp8MoEMethod
from sglang.srt.layers.quantization.fp8_utils import (
block_quant_dequant,
inverse_transform_scale_ue8m0,
)
from sglang.srt.layers.quantization.modelopt_quant import (
ModelOptFp4LinearMethod,
ModelOptNvFp4FusedMoEMethod,
)
# chunk to avoid too high GPU memory peak
CHUNK_NUMEL = 64 * 1024 * 1024
class CompareResult(NamedTuple):
equal: bool
max_abs_err: float
mean_abs_err: float
num_exceed: int # elements past the combined per-side tolerance
class ComparableWeight:
"""Base comparable-weight class; one subclass per precision or raw tensor."""
@staticmethod
def _quant_ulp(w_q: torch.Tensor) -> torch.Tensor:
"""Per-element ULP of w_q in its own dtype."""
finfo = torch.finfo(w_q.dtype)
x = w_q.to(torch.float32).abs()
# frexp: x = m * 2^e, m in [0.5, 1), so 2^(e-1) is x's binade base.
_, exponent = torch.frexp(x)
binade = torch.exp2((exponent - 1).to(torch.float32))
# Zeros and subnormals share the spacing of the smallest normal binade.
binade = binade.masked_fill(x < finfo.smallest_normal, finfo.smallest_normal)
return binade * finfo.eps
def iter_chunks(self) -> Iterable[Tuple[torch.Tensor, Optional[torch.Tensor]]]:
raise NotImplementedError
def dequantize(self, dtype: torch.dtype = torch.bfloat16) -> torch.Tensor:
raise NotImplementedError
class Fp8BlockComparable(ComparableWeight):
"""Deepseek-style FP8 quantization."""
def __init__(self, w_q: torch.Tensor, w_s: torch.Tensor):
self.w_q = w_q
self.w_s = w_s
def __repr__(self) -> str:
return f"fp8_block(shape={tuple(self.w_q.shape)} dtype={self.w_q.dtype})"
@staticmethod
def _normalize_scale(w_q: torch.Tensor, w_s: torch.Tensor) -> torch.Tensor:
if w_s.dtype == torch.int32:
w_s = inverse_transform_scale_ue8m0(w_s, mn=w_q.shape[-2])
return w_s.to(torch.float32)
@staticmethod
def _infer_block_size(w_q: torch.Tensor, w_s: torch.Tensor) -> list:
k, s_k = w_q.shape[-1], w_s.shape[-1]
assert k % s_k == 0, f"cannot infer block size from {w_q.shape=} {w_s.shape=}"
block = k // s_k
return [block, block]
@staticmethod
def _iter_quant_chunks(w_q: torch.Tensor, w_s: torch.Tensor, block_n: int):
"""Yields block-row-aligned (q_slice, s_slice) pairs of bounded size."""
q3 = w_q.reshape(-1, *w_q.shape[-2:])
s3 = w_s.reshape(-1, *w_s.shape[-2:])
n, k = q3.shape[-2:]
rows = max(block_n, CHUNK_NUMEL // k // block_n * block_n)
for b in range(q3.shape[0]):
for r0 in range(0, n, rows):
r1 = min(r0 + rows, n)
yield q3[b, r0:r1], s3[b, r0 // block_n : -(-r1 // block_n)]
def _scale_and_block_size(self):
s = self._normalize_scale(self.w_q, self.w_s)
return s, self._infer_block_size(self.w_q, s)
def iter_chunks(self):
s, block_size = self._scale_and_block_size()
for q, s_chunk in self._iter_quant_chunks(self.w_q, s, block_size[0]):
q, s_chunk = q.cuda(), s_chunk.cuda()
yield (
block_quant_dequant(q, s_chunk, block_size, dtype=torch.bfloat16),
block_quant_dequant(
self._quant_ulp(q), s_chunk, block_size, dtype=torch.float32
),
)
def dequantize(self, dtype: torch.dtype = torch.bfloat16) -> torch.Tensor:
s, block_size = self._scale_and_block_size()
return block_quant_dequant(self.w_q, s, block_size, dtype=dtype)
class RawComparable(ComparableWeight):
"""Bitwise equal compare on raw tensor."""
def __init__(self, tensor: torch.Tensor):
self.tensor = tensor
def __repr__(self) -> str:
return f"raw(shape={tuple(self.tensor.shape)} dtype={self.tensor.dtype})"
def iter_chunks(self):
flat = self.tensor.reshape(-1)
for start in range(0, flat.numel(), CHUNK_NUMEL):
yield flat[start : start + CHUNK_NUMEL].cuda(), None
def dequantize(self, dtype: torch.dtype = torch.bfloat16) -> torch.Tensor:
return self.tensor
def compare_weights(
expect: ComparableWeight, actual: ComparableWeight
) -> CompareResult:
"""Chunked element-wise compare in ComparableWeight space."""
equal = True
max_abs_err = torch.zeros((), dtype=torch.float32)
sum_abs_err = 0.0
num_exceed = 0
numel = 0
for (expect_dq, expect_tol), (actual_dq, actual_tol) in zip(
expect.iter_chunks(), actual.iter_chunks(), strict=True
):
assert (
expect_dq.shape == actual_dq.shape
), f"{expect_dq.shape=} {actual_dq.shape=}"
numel += expect_dq.numel()
abs_diff = (actual_dq.float() - expect_dq.float()).abs()
if torch.all(abs_diff == 0):
continue
equal = False
# |actual_dq - expect_dq| ≤ |actual_dq - w| + |expect_dq - w| ≤ actual_tol + expect_tol
tol = (
0.0 if expect_tol is None or actual_tol is None else expect_tol + actual_tol
)
max_abs_err = torch.maximum(max_abs_err, abs_diff.max().cpu())
sum_abs_err += abs_diff.sum().item()
# `~(diff <= tol)` instead of `diff > tol` so NaN counts as exceeding.
num_exceed += int((~(abs_diff <= tol)).sum())
return CompareResult(
equal, max_abs_err.item(), sum_abs_err / max(numel, 1), num_exceed
)
def select_comparable_weight(quant_method) -> Optional[type]:
"""Map a module's quant_method to its ComparableWeight. None means raw (bitwise equal) compare."""
if (
isinstance(quant_method, (Fp8LinearMethod, Fp8MoEMethod))
and quant_method.block_quant
and not quant_method.use_mxfp8
):
return Fp8BlockComparable
if isinstance(quant_method, (ModelOptFp4LinearMethod, ModelOptNvFp4FusedMoEMethod)):
raise NotImplementedError(
f"weight checker has no ComparableWeight for {type(quant_method).__name__}"
)
return None
+209 -88
View File
@@ -14,27 +14,33 @@
"""Unit tests for sglang/srt/utils/weight_checker.py.""" """Unit tests for sglang/srt/utils/weight_checker.py."""
import unittest import unittest
from typing import Iterable, List, Tuple from typing import Iterable, List
from unittest.mock import patch from unittest.mock import patch
import torch import torch
from torch import nn from torch import nn
from sglang.srt.layers.quantization.fp8_utils import ( from sglang.srt.layers.quantization.fp8_utils import (
block_quant_dequant,
quant_weight_ue8m0, quant_weight_ue8m0,
transform_scale_ue8m0, transform_scale_ue8m0,
) )
from sglang.srt.utils.weight_checker import ( from sglang.srt.utils.weight_checker import (
CheckEntry,
ChecksumInfo, ChecksumInfo,
ParallelismInfo, ParallelismInfo,
QuantizedWeight,
WeightChecker, WeightChecker,
_build_check_entries,
_build_quantized_set,
_check_tensors, _check_tensors,
_hash_tensor, _hash_tensor,
_is_non_persistent_buffer_name, _is_non_persistent_buffer_name,
_postprocess_tensors,
_random_like, _random_like,
) )
from sglang.srt.utils.weight_checker_comparator import (
Fp8BlockComparable,
RawComparable,
)
from sglang.test.ci.ci_register import register_cuda_ci from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase from sglang.test.test_utils import CustomTestCase
@@ -46,31 +52,39 @@ register_cuda_ci(est_time=30, stage="base-b", runner_config="1-gpu-small")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
Triple = Tuple[str, bool, torch.Tensor] def _assert_entries_close(
actual: Iterable[CheckEntry], expected: Iterable[CheckEntry]
) -> None:
def _assert_triples_close(actual: Iterable[Triple], expected: Iterable[Triple]) -> None: """Compare two streams of (name, should_compare, ComparableWeight)."""
"""Compare two streams of (name, should_compare, tensor); element-wise tensor close.""" actual_list: List[CheckEntry] = list(actual)
actual_list: List[Triple] = list(actual) expected_list: List[CheckEntry] = list(expected)
expected_list: List[Triple] = list(expected)
assert len(actual_list) == len( assert len(actual_list) == len(
expected_list expected_list
), f"length mismatch: actual={len(actual_list)} expected={len(expected_list)}" ), f"length mismatch: actual={len(actual_list)} expected={len(expected_list)}"
for i, ((a_name, a_flag, a_t), (e_name, e_flag, e_t)) in enumerate( for i, ((a_name, a_flag, a_ref), (e_name, e_flag, e_ref)) in enumerate(
zip(actual_list, expected_list) zip(actual_list, expected_list)
): ):
assert a_name == e_name, f"[{i}] name: {a_name!r} != {e_name!r}" assert a_name == e_name, f"[{i}] name: {a_name!r} != {e_name!r}"
assert a_flag == e_flag, f"[{i}] should_compare: {a_flag} != {e_flag}" assert a_flag == e_flag, f"[{i}] should_compare: {a_flag} != {e_flag}"
torch.testing.assert_close( assert type(a_ref) is type(e_ref), f"[{i}] kind mismatch for {a_name!r}"
a_t, e_t, msg=f"[{i}] tensor mismatch for {a_name!r}" if isinstance(a_ref, Fp8BlockComparable):
) torch.testing.assert_close(
a_ref.w_q, e_ref.w_q, msg=f"[{i}] w_q {a_name!r}"
)
torch.testing.assert_close(
a_ref.w_s, e_ref.w_s, msg=f"[{i}] w_s {a_name!r}"
)
else:
torch.testing.assert_close(
a_ref.tensor, e_ref.tensor, msg=f"[{i}] tensor {a_name!r}"
)
def _build_fp8_quant_pair(device: str = "cuda"): def _build_fp8_quant_pair(device: str = "cuda"):
"""Construct a real fp8-quantized weight + matching fp32 + ue8m0-packed scales. """Construct a real fp8-quantized weight + matching fp32 + ue8m0-packed scales.
Returns (qweight, sf_fp32, sf_packed_int32) so callers can pick which scale dtype Returns (qweight, sf_fp32, sf_packed_int32) so callers can pick which scale dtype
drives the _postprocess_tensors branch under test. drives the _build_check_entries branch under test.
""" """
weight_bf16 = torch.randn((256, 128), dtype=torch.bfloat16, device=device) weight_bf16 = torch.randn((256, 128), dtype=torch.bfloat16, device=device)
block_size = [128, 128] block_size = [128, 128]
@@ -87,7 +101,7 @@ def _build_fp8_quant_pair(device: str = "cuda"):
class _TinyModel(nn.Module): class _TinyModel(nn.Module):
"""Mimics the buffer naming patterns _reset_tensors / _postprocess_tensors care about.""" """Mimics the buffer naming patterns _reset_tensors / _build_check_entries care about."""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -173,9 +187,19 @@ class TestRandomLike(CustomTestCase):
_random_like(t) _random_like(t)
torch.testing.assert_close(t, before) torch.testing.assert_close(t, before)
def test_floating_point_chunked_generation(self):
with patch("sglang.srt.utils.weight_checker.CHUNK_NUMEL", 8):
out = _random_like(torch.zeros(64, dtype=torch.bfloat16))
self.assertEqual(out.dtype, torch.bfloat16)
self.assertEqual(out.shape, (64,))
self.assertGreater(out.unique().numel(), 8)
self.assertGreaterEqual(out.float().min().item(), 0.0)
# bf16 rounding may carry values just below 1.0 up to exactly 1.0
self.assertLessEqual(out.float().max().item(), 1.0)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# _postprocess_tensors # _build_check_entries
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -187,15 +211,17 @@ class TestPostprocessTensors(CustomTestCase):
a = torch.randn(4) a = torch.randn(4)
b = torch.randn(4) b = torch.randn(4)
raw = {"a.weight": a, "b.bias": b} raw = {"a.weight": a, "b.bias": b}
_assert_triples_close( _assert_entries_close(
_postprocess_tensors(raw, set()), _build_check_entries(raw, set()),
[("a.weight", True, a), ("b.bias", True, b)], [("a.weight", True, RawComparable(a)), ("b.bias", True, RawComparable(b))],
) )
def test_weight_alone_without_scale_inv_does_not_trigger_dequant(self): def test_weight_alone_without_scale_inv_does_not_trigger_dequant(self):
w = torch.randn(4) w = torch.randn(4)
raw = {"x.weight": w} raw = {"x.weight": w}
_assert_triples_close(_postprocess_tensors(raw, set()), [("x.weight", True, w)]) _assert_entries_close(
_build_check_entries(raw, set()), [("x.weight", True, RawComparable(w))]
)
# --- non-persistent buffer skip --- # --- non-persistent buffer skip ---
@@ -206,70 +232,49 @@ class TestPostprocessTensors(CustomTestCase):
"model.rotary_emb.cos_sin_cache": cache, "model.rotary_emb.cos_sin_cache": cache,
"model.layers.0.weight": plain, "model.layers.0.weight": plain,
} }
_assert_triples_close( _assert_entries_close(
_postprocess_tensors(raw, set()), _build_check_entries(raw, set()),
[ [
("model.rotary_emb.cos_sin_cache", False, cache), ("model.rotary_emb.cos_sin_cache", False, RawComparable(cache)),
("model.layers.0.weight", True, plain), ("model.layers.0.weight", True, RawComparable(plain)),
], ],
) )
def test_skips_inv_freq_substring(self): def test_skips_inv_freq_substring(self):
t = torch.randn(4) t = torch.randn(4)
_assert_triples_close( _assert_entries_close(
_postprocess_tensors({"model.rotary_emb.inv_freq": t}, set()), _build_check_entries({"model.rotary_emb.inv_freq": t}, set()),
[("model.rotary_emb.inv_freq", False, t)], [("model.rotary_emb.inv_freq", False, RawComparable(t))],
) )
def test_skips_weight_fp32_substring(self): def test_skips_weight_fp32_substring(self):
t = torch.randn(4) t = torch.randn(4)
_assert_triples_close( _assert_entries_close(
_postprocess_tensors({"model.layers.0.mlp.gate._weight_fp32": t}, set()), _build_check_entries({"model.layers.0.mlp.gate._weight_fp32": t}, set()),
[("model.layers.0.mlp.gate._weight_fp32", False, t)], [("model.layers.0.mlp.gate._weight_fp32", False, RawComparable(t))],
) )
def test_substring_match_not_endswith(self): def test_substring_match_not_endswith(self):
# Pattern can appear anywhere in the name, not just at the end. # Pattern can appear anywhere in the name, not just at the end.
t = torch.randn(4) t = torch.randn(4)
_assert_triples_close( _assert_entries_close(
_postprocess_tensors({"weird.cos_sin_cache.foo.bar": t}, set()), _build_check_entries({"weird.cos_sin_cache.foo.bar": t}, set()),
[("weird.cos_sin_cache.foo.bar", False, t)], [("weird.cos_sin_cache.foo.bar", False, RawComparable(t))],
) )
# --- fp8 quant pair (real dequant on real fp8 tensors) --- # --- fp8 quant pair (real dequant on real fp8 tensors) ---
def test_fp8_quant_pair_with_int32_scale_dequants_via_ue8m0(self): def test_fp8_quant_pair_yields_lazy_pair(self):
qweight, sf_fp32, sf_packed_int32 = _build_fp8_quant_pair() qweight, sf_fp32, sf_packed_int32 = _build_fp8_quant_pair()
raw = {"x.weight": qweight, "x.weight_scale_inv": sf_packed_int32} raw = {"x.weight": qweight, "x.weight_scale_inv": sf_packed_int32}
# Reference: ue8m0 path inside _postprocess_tensors should eventually ref = Fp8BlockComparable(qweight, sf_packed_int32)
# call block_quant_dequant with the unpacked fp32 scale. quantized_set = {
expected_dequant = block_quant_dequant( "x.weight": QuantizedWeight(Fp8BlockComparable, "x.weight_scale_inv")
qweight, sf_fp32, block_size=[128, 128], dtype=torch.bfloat16 }
) _assert_entries_close(
_assert_triples_close( _build_check_entries(raw, set(), quantized_set),
_postprocess_tensors(raw, set()), [("x.weight", True, ref)],
[
("x.weight", True, expected_dequant),
("x.weight", False, qweight),
("x.weight_scale_inv", False, sf_packed_int32),
],
)
def test_fp8_quant_pair_with_fp32_scale_dequants_directly(self):
qweight, sf_fp32, _ = _build_fp8_quant_pair()
raw = {"x.weight": qweight, "x.weight_scale_inv": sf_fp32}
expected_dequant = block_quant_dequant(
qweight, sf_fp32, block_size=[128, 128], dtype=torch.bfloat16
)
_assert_triples_close(
_postprocess_tensors(raw, set()),
[
("x.weight", True, expected_dequant),
("x.weight", False, qweight),
("x.weight_scale_inv", False, sf_fp32),
],
) )
def test_fp8_quant_pair_yield_order_alongside_other_entries(self): def test_fp8_quant_pair_yield_order_alongside_other_entries(self):
@@ -280,17 +285,16 @@ class TestPostprocessTensors(CustomTestCase):
"x.weight_scale_inv": sf_fp32, "x.weight_scale_inv": sf_fp32,
"y.bias": bias, "y.bias": bias,
} }
expected_dequant = block_quant_dequant( # scale_inv is consumed by its weight's comparable; y.bias stays raw.
qweight, sf_fp32, block_size=[128, 128], dtype=torch.bfloat16 ref = Fp8BlockComparable(qweight, sf_fp32)
) quantized_set = {
# All dequant entries come first, then a raw pass over every key. "x.weight": QuantizedWeight(Fp8BlockComparable, "x.weight_scale_inv")
_assert_triples_close( }
_postprocess_tensors(raw, set()), _assert_entries_close(
_build_check_entries(raw, set(), quantized_set),
[ [
("x.weight", True, expected_dequant), ("x.weight", True, ref),
("x.weight", False, qweight), ("y.bias", True, RawComparable(bias)),
("x.weight_scale_inv", False, sf_fp32),
("y.bias", True, bias),
], ],
) )
@@ -298,9 +302,9 @@ class TestPostprocessTensors(CustomTestCase):
# Without the matching `.weight`, no quant pair forms; the scale_inv flows # Without the matching `.weight`, no quant pair forms; the scale_inv flows
# through as a normal entry with should_compare=True. # through as a normal entry with should_compare=True.
s = torch.zeros(1, 1, dtype=torch.int32) s = torch.zeros(1, 1, dtype=torch.int32)
_assert_triples_close( _assert_entries_close(
_postprocess_tensors({"x.weight_scale_inv": s}, set()), _build_check_entries({"x.weight_scale_inv": s}, set()),
[("x.weight_scale_inv", True, s)], [("x.weight_scale_inv", True, RawComparable(s))],
) )
@@ -313,13 +317,19 @@ class TestCheckTensors(CustomTestCase):
def test_passes_when_all_equal(self): def test_passes_when_all_equal(self):
t = torch.ones(2, 2) t = torch.ones(2, 2)
expect = [("a", True, t.clone()), ("b", True, t.clone())] expect = [
actual = [("a", True, t.clone()), ("b", True, t.clone())] ("a", True, RawComparable(t.clone())),
("b", True, RawComparable(t.clone())),
]
actual = [
("a", True, RawComparable(t.clone())),
("b", True, RawComparable(t.clone())),
]
_check_tensors(expect_tensors=expect, actual_tensors=actual) _check_tensors(expect_tensors=expect, actual_tensors=actual)
def test_raises_when_should_compare_true_and_diff(self): def test_raises_when_should_compare_true_and_diff(self):
expect = [("a", True, torch.ones(2, 2))] expect = [("a", True, RawComparable(torch.ones(2, 2)))]
actual = [("a", True, torch.zeros(2, 2))] actual = [("a", True, RawComparable(torch.zeros(2, 2)))]
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
_check_tensors(expect_tensors=expect, actual_tensors=actual) _check_tensors(expect_tensors=expect, actual_tensors=actual)
msg = str(ctx.exception) msg = str(ctx.exception)
@@ -328,30 +338,141 @@ class TestCheckTensors(CustomTestCase):
def test_passes_when_should_compare_false_even_if_diff(self): def test_passes_when_should_compare_false_even_if_diff(self):
# should_compare=False -> diff is logged, not raised. # should_compare=False -> diff is logged, not raised.
expect = [("a", False, torch.ones(2, 2))] expect = [("a", False, RawComparable(torch.ones(2, 2)))]
actual = [("a", False, torch.zeros(2, 2))] actual = [("a", False, RawComparable(torch.zeros(2, 2)))]
_check_tensors(expect_tensors=expect, actual_tensors=actual) _check_tensors(expect_tensors=expect, actual_tensors=actual)
def test_asserts_on_name_mismatch(self): def test_asserts_on_name_mismatch(self):
expect = [("a", True, torch.ones(2, 2))] expect = [("a", True, RawComparable(torch.ones(2, 2)))]
actual = [("b", True, torch.ones(2, 2))] actual = [("b", True, RawComparable(torch.ones(2, 2)))]
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
_check_tensors(expect_tensors=expect, actual_tensors=actual) _check_tensors(expect_tensors=expect, actual_tensors=actual)
def test_asserts_on_should_compare_mismatch(self): def test_asserts_on_should_compare_mismatch(self):
expect = [("a", True, torch.ones(2, 2))] expect = [("a", True, RawComparable(torch.ones(2, 2)))]
actual = [("a", False, torch.ones(2, 2))] actual = [("a", False, RawComparable(torch.ones(2, 2)))]
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
_check_tensors(expect_tensors=expect, actual_tensors=actual) _check_tensors(expect_tensors=expect, actual_tensors=actual)
def test_chunked_raw_stats_match_unchunked(self):
expect = [("a", True, RawComparable(torch.zeros(10)))]
actual = [("a", True, RawComparable(torch.arange(10.0)))]
with patch("sglang.srt.utils.weight_checker_comparator.CHUNK_NUMEL", 3):
with self.assertRaises(Exception) as ctx:
_check_tensors(expect_tensors=expect, actual_tensors=actual)
self.assertIn("max_abs_err=9.0", str(ctx.exception))
self.assertIn("mean_abs_err=4.5", str(ctx.exception))
def test_zip_strict_raises_on_length_mismatch(self): def test_zip_strict_raises_on_length_mismatch(self):
t = torch.ones(2, 2) t = torch.ones(2, 2)
expect = [("a", True, t.clone()), ("b", True, t.clone())] expect = [
actual = [("a", True, t.clone())] ("a", True, RawComparable(t.clone())),
("b", True, RawComparable(t.clone())),
]
actual = [("a", True, RawComparable(t.clone()))]
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
_check_tensors(expect_tensors=expect, actual_tensors=actual) _check_tensors(expect_tensors=expect, actual_tensors=actual)
# ---------------------------------------------------------------------------
# _check_tensors + allow_quant_error
# ---------------------------------------------------------------------------
def _quantize_block_fp8(weight: torch.Tensor, scale_margin: float):
"""Blockwise 128x128 fp8 quantization with a tweakable scale convention."""
n, k = weight.shape
blocks = weight.float().view(n // 128, 128, k // 128, 128).permute(0, 2, 1, 3)
scale = blocks.abs().amax(dim=(-1, -2)) / 448.0 * scale_margin
q = (blocks / scale[:, :, None, None]).to(torch.float8_e4m3fn)
q = q.permute(0, 2, 1, 3).reshape(n, k)
return q, scale
class TestCheckTensorsAllowQuantError(CustomTestCase):
def setUp(self):
torch.manual_seed(0)
weight = torch.randn(256, 256, device="cuda") * 0.02
self.e_raw = self._as_raw(*_quantize_block_fp8(weight, 1.0))
self.a_raw = self._as_raw(*_quantize_block_fp8(weight, 1.001))
@staticmethod
def _as_raw(q, s):
return {"x.weight": q, "x.weight_scale_inv": s}
def _check(self, expect_raw, actual_raw, **kwargs):
quantized_set = {
"x.weight": QuantizedWeight(Fp8BlockComparable, "x.weight_scale_inv")
}
_check_tensors(
expect_tensors=_build_check_entries(expect_raw, set(), quantized_set),
actual_tensors=_build_check_entries(actual_raw, set(), quantized_set),
**kwargs,
)
def test_within_tolerance_passes_with_flag(self):
self._check(self.e_raw, self.a_raw, allow_quant_error=True)
def test_within_tolerance_fails_without_flag(self):
with self.assertRaises(Exception) as ctx:
self._check(self.e_raw, self.a_raw)
self.assertIn("name=x.weight", str(ctx.exception))
def test_exceeding_tolerance_fails_with_flag(self):
bad_q = self.a_raw["x.weight"].clone().view(torch.uint8)
bad_q[::50] += 8
bad = self._as_raw(
bad_q.view(torch.float8_e4m3fn), self.a_raw["x.weight_scale_inv"]
)
with self.assertRaises(Exception) as ctx:
self._check(self.e_raw, bad, allow_quant_error=True)
self.assertIn("num_exceed", str(ctx.exception))
def test_flag_does_not_relax_non_quant_tensors(self):
expect = [("a", True, RawComparable(torch.ones(2, 2)))]
actual = [("a", True, RawComparable(torch.ones(2, 2) + 0.5))]
with self.assertRaises(Exception):
_check_tensors(
expect_tensors=expect, actual_tensors=actual, allow_quant_error=True
)
# ---------------------------------------------------------------------------
# _build_quantized_set
# ---------------------------------------------------------------------------
class TestBuildQuantizedSet(CustomTestCase):
def test_fp8_block_module_pairs_weight_and_scale(self):
from sglang.srt.layers.quantization.fp8 import Fp8LinearMethod
method = Fp8LinearMethod.__new__(Fp8LinearMethod)
method.block_quant = True
method.use_mxfp8 = False
model = nn.Module()
model.proj = nn.Module()
model.proj.quant_method = method
model.proj.register_parameter(
"weight", nn.Parameter(torch.zeros(4, 4), requires_grad=False)
)
model.proj.register_parameter(
"weight_scale_inv", nn.Parameter(torch.zeros(1, 1), requires_grad=False)
)
self.assertEqual(
_build_quantized_set(model),
{
"proj.weight": QuantizedWeight(
Fp8BlockComparable, "proj.weight_scale_inv"
)
},
)
def test_no_quant_method_yields_empty_plan(self):
self.assertEqual(_build_quantized_set(_TinyModel()), {})
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# WeightChecker class # WeightChecker class
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -0,0 +1,206 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Unit tests for sglang/srt/utils/weight_checker_comparator.py."""
import unittest
from unittest.mock import patch
import torch
from sglang.srt.layers.quantization.fp8_utils import (
quant_weight_ue8m0,
transform_scale_ue8m0,
)
from sglang.srt.utils.weight_checker_comparator import (
ComparableWeight,
Fp8BlockComparable,
compare_weights,
select_comparable_weight,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=15, stage="base-b", runner_config="1-gpu-small")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _compare_quant_pair(expect_q, expect_s, actual_q, actual_s):
return compare_weights(
Fp8BlockComparable(expect_q, expect_s), Fp8BlockComparable(actual_q, actual_s)
)
def _build_fp8_quant_pair(device: str = "cuda"):
"""Returns (qweight, fp32 scale, ue8m0-packed int32 scale) for one random weight."""
weight_bf16 = torch.randn((256, 128), dtype=torch.bfloat16, device=device)
block_size = [128, 128]
qweight, sf_fp32 = quant_weight_ue8m0(
weight_dequant=weight_bf16, weight_block_size=block_size
)
sf_packed_int32 = transform_scale_ue8m0(sf_fp32, mn=qweight.shape[-2])
return qweight, sf_fp32, sf_packed_int32
# ---------------------------------------------------------------------------
# _quant_ulp
# ---------------------------------------------------------------------------
class TestQuantUlp(CustomTestCase):
def test_matches_bruteforce_spacing_for_fp8(self):
for dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
all_bits = torch.arange(256, dtype=torch.uint8).view(dtype)
vals = all_bits.to(torch.float32)
magnitudes = torch.unique(vals[torch.isfinite(vals) & (vals >= 0)])
# Brute-force ULP: spacing to the next representable magnitude
# (the largest magnitude reuses the spacing below it).
spacing = magnitudes[1:] - magnitudes[:-1]
expected = torch.cat([spacing, spacing[-1:]])
got = ComparableWeight._quant_ulp(magnitudes.to(dtype))
torch.testing.assert_close(got, expected, rtol=0, atol=0)
# ---------------------------------------------------------------------------
# compare_weights
# ---------------------------------------------------------------------------
class TestCompareQuantPair(CustomTestCase):
"""Chunked dequantized-space comparison of block-quantized pairs."""
@staticmethod
def _quantize(weight: torch.Tensor, scale_margin: float):
"""Blockwise 128x128 fp8 quantization with a tweakable scale convention."""
n, k = weight.shape
blocks = weight.float().view(n // 128, 128, k // 128, 128).permute(0, 2, 1, 3)
scale = blocks.abs().amax(dim=(-1, -2)) / 448.0 * scale_margin
q = (blocks / scale[:, :, None, None]).to(torch.float8_e4m3fn)
q = q.permute(0, 2, 1, 3).reshape(n, k)
return q, scale
def setUp(self):
torch.manual_seed(0)
self.weight = torch.randn(256, 256, device="cuda") * 0.02
self.e_q, self.e_s = self._quantize(self.weight, 1.0)
self.a_q, self.a_s = self._quantize(self.weight, 1.001)
def test_identical_pair_is_equal(self):
equal, max_err, mean_err, num_exceed = _compare_quant_pair(
self.e_q, self.e_s, self.e_q.clone(), self.e_s.clone()
)
self.assertTrue(equal)
self.assertEqual((max_err, mean_err, num_exceed), (0.0, 0.0, 0))
def test_ue8m0_packed_scale_equals_unpacked_scale(self):
qweight, sf_fp32, sf_packed_int32 = _build_fp8_quant_pair()
equal, *_ = _compare_quant_pair(qweight, sf_packed_int32, qweight, sf_fp32)
self.assertTrue(equal)
def test_two_quantizations_stay_within_ulp_tolerance(self):
equal, max_err, mean_err, num_exceed = _compare_quant_pair(
self.e_q, self.e_s, self.a_q, self.a_s
)
self.assertFalse(equal)
self.assertGreater(max_err, 0.0)
self.assertEqual(num_exceed, 0)
def test_corruption_and_fp8_nan_exceed_tolerance(self):
bad_q = self.a_q.clone().view(torch.uint8)
bad_q[::50] += 8 # jumps a full binade; some bytes become fp8 NaN
equal, max_err, mean_err, num_exceed = _compare_quant_pair(
self.e_q, self.e_s, bad_q.view(torch.float8_e4m3fn), self.a_s
)
self.assertFalse(equal)
self.assertGreater(num_exceed, 0)
def test_chunked_result_matches_unchunked(self):
reference = _compare_quant_pair(self.e_q, self.e_s, self.a_q, self.a_s)
with patch("sglang.srt.utils.weight_checker_comparator.CHUNK_NUMEL", 128 * 128):
chunked = _compare_quant_pair(self.e_q, self.e_s, self.a_q, self.a_s)
self.assertEqual(chunked, reference)
@staticmethod
def _quantize_partial(weight: torch.Tensor, scale_margin: float):
"""128x128 block quant where the last block per dim may be partial."""
n, k = weight.shape
s_n, s_k = -(-n // 128), -(-k // 128)
q = torch.empty(n, k, dtype=torch.float8_e4m3fn, device=weight.device)
scale = torch.empty(s_n, s_k, device=weight.device)
for i in range(s_n):
for j in range(s_k):
blk = weight[i * 128 : (i + 1) * 128, j * 128 : (j + 1) * 128].float()
s = blk.abs().amax() / 448.0 * scale_margin
s = s if s > 0 else weight.new_ones(())
scale[i, j] = s
q[i * 128 : (i + 1) * 128, j * 128 : (j + 1) * 128] = (blk / s).to(
torch.float8_e4m3fn
)
return q, scale
def test_partial_last_block_infers_true_block_size(self):
# fused_qkv_a_proj_with_mqa out-dim is not a multiple of 128 (e.g. 2112 =
# 16*128 + 64), so the last row-block is partial. ceil(dim/num_blocks)
# would infer 125, misaligning scales; the true block size is 128.
n, k = 3 * 128 + 64, 256
weight = torch.randn(n, k, device="cuda") * 0.02
e_q, e_s = self._quantize_partial(weight, 1.0)
a_q, a_s = self._quantize_partial(weight, 1.001)
self.assertEqual(list(e_s.shape), [4, 2]) # ceil(448/128)=4, 256/128=2
self.assertEqual(Fp8BlockComparable._infer_block_size(e_q, e_s), [128, 128])
equal, _, _, num_exceed = _compare_quant_pair(e_q, e_s, a_q, a_s)
self.assertFalse(equal)
self.assertEqual(num_exceed, 0)
def test_3d_expert_tensor(self):
q3 = self.e_q.reshape(2, 128, 256).contiguous()
s3 = self.e_s.reshape(2, 1, 2)
equal, *_ = _compare_quant_pair(q3, s3, q3.clone(), s3.clone())
self.assertTrue(equal)
# ---------------------------------------------------------------------------
# select_comparable_weight
# ---------------------------------------------------------------------------
class TestSelectComparableWeight(CustomTestCase):
def test_returns_none_when_not_a_quant_method(self):
self.assertIsNone(select_comparable_weight(None))
def test_returns_none_for_raw_safe_method(self):
from sglang.srt.layers.quantization.unquant import UnquantizedLinearMethod
# unquantized / int4 / mxfp8 all route to raw (None).
fake = UnquantizedLinearMethod.__new__(UnquantizedLinearMethod)
self.assertIsNone(select_comparable_weight(fake))
def test_raises_on_nvfp4(self):
from sglang.srt.layers.quantization.modelopt_quant import (
ModelOptFp4LinearMethod,
)
# nvfp4 has no ComparableWeight yet -> must raise, not silently raw-compare.
fake = ModelOptFp4LinearMethod.__new__(ModelOptFp4LinearMethod)
with self.assertRaises(NotImplementedError):
select_comparable_weight(fake)
if __name__ == "__main__":
unittest.main()