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
20 changes: 20 additions & 0 deletions python/freetoken/layers/quantization/configs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, ClassVar

from ..linear import LinearConfig
Expand Down Expand Up @@ -36,8 +37,18 @@ def quantization_config_of(hf_config: Any) -> dict[str, Any] | None:
return dict(vars(q))


@dataclass(frozen=True)
class Stored:
"""One checkpoint tensor behind a role: its suffix, and whether it holds the quant-side scale whose reciprocal the layer wants."""

name: str
reciprocal: bool = False


class QuantConfig(ABC):
dialect: ClassVar[str]
# per kind the dialect exports, role -> the checkpoint tensor suffix (or Stored) that feeds it
STORAGE: ClassVar[dict[QuantKind, dict[str, str | Stored]]]

def __init__(self, name_map: NameMap | None = None, unquantized: tuple[str, ...] = ()):
self.name_map = name_map or NameMap()
Expand Down Expand Up @@ -65,6 +76,14 @@ def scheme_for(self, prefix: str) -> QuantScheme | None:
self._schemes[prefix] = scheme
return scheme

def stored_tensors(self, kind: QuantKind) -> dict[str, Stored]:
"""role -> checkpoint tensor for every role the dialect stores for ``kind``."""
return {role: entry if isinstance(entry, Stored) else Stored(entry) for role, entry in self.STORAGE[kind].items()}

def storage(self, scheme: QuantScheme) -> dict[str, Stored]:
"""role -> checkpoint tensor for one scheme's tensors."""
return {role: entry for role, entry in self.stored_tensors(scheme.kind).items() if scheme.has(role)}

def get_quant_method(self, layer: Any, prefix: str):
scheme = self.scheme_for(prefix)
layer_kind = layer.quant_layer_kind
Expand Down Expand Up @@ -101,6 +120,7 @@ class NoQuantConfig(QuantConfig):
"""No ``quantization_config``: every module is bf16."""

dialect = "none"
STORAGE: ClassVar[dict[QuantKind, dict[str, str | Stored]]] = {}

@classmethod
def claims(cls, q: dict[str, Any]) -> bool:
Expand Down
13 changes: 11 additions & 2 deletions python/freetoken/layers/quantization/configs/compressed_tensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from ..names import Matcher, ct_set
from ..registry import register_dialect
from ..scheme import QuantScheme
from ..scheme import QuantKind, QuantScheme
from ..scheme import fp8_block_scheme, fp8_tensor_scheme, nvfp4_scheme
from .base import QuantConfig
from .base import QuantConfig, Stored


@register_dialect
Expand All @@ -23,6 +23,15 @@ class CompressedTensorsConfig(QuantConfig):
"FP8_CHANNEL": fp8_tensor_scheme("bf16", per_row=True),
"FP8_BLOCK": fp8_block_scheme("float"),
}
# the NVFP4 globals are the quant-side scales (vLLM: alpha = 1 / (input_global_scale * weight_global_scale))
STORAGE: ClassVar[dict[QuantKind, dict[str, str | Stored]]] = {
QuantKind.NVFP4: {
"weight": "weight_packed", "weight_scale": "weight_scale",
"weight_global": Stored("weight_global_scale", reciprocal=True), "input_scale": Stored("input_global_scale", reciprocal=True),
},
QuantKind.FP8_TENSOR: {"weight": "weight", "weight_scale": "weight_scale", "input_scale": "input_scale"},
QuantKind.FP8_BLOCK: {"weight": "weight", "weight_scale_inv": "weight_scale"},
}

def __init__(self, q: dict[str, Any], hf_config: Any = None, *, name_map=None, unquantized=()):
super().__init__(name_map, unquantized)
Expand Down
16 changes: 14 additions & 2 deletions python/freetoken/layers/quantization/configs/fp8.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from ..names import is_routed_expert, name_set, substr_set
from ..registry import register_dialect
from ..scheme import QuantScheme
from ..scheme import QuantKind, QuantScheme
from ..scheme import FP8_BLOCK, fp8_block_scheme, fp8_tensor_scheme, mxfp4_scheme
from .base import QuantConfig, cfg_get
from .base import QuantConfig, Stored, cfg_get


@register_dialect
Expand All @@ -22,6 +22,12 @@ class Fp8BlockConfig(QuantConfig):
"TABLE": fp8_tensor_scheme("float"),
"EXPERT_MXFP4": mxfp4_scheme(),
}
# transformers' fp8 names; DeepSeek-V4's e8m0 export calls every scale ``scale`` (see storage)
STORAGE: ClassVar[dict[QuantKind, dict[str, str | Stored]]] = {
QuantKind.FP8_BLOCK: {"weight": "weight", "weight_scale_inv": "weight_scale_inv"},
QuantKind.FP8_TENSOR: {"weight": "weight", "weight_scale": "weight_scale"},
QuantKind.MXFP4: {"weight": "weight", "weight_scale": "scale"},
}

def __init__(self, q: dict[str, Any], hf_config: Any = None, *, name_map=None, unquantized=()):
super().__init__(name_map, unquantized)
Expand All @@ -36,6 +42,12 @@ def __init__(self, q: dict[str, Any], hf_config: Any = None, *, name_map=None, u
self.e8m0 = str(q.get("scale_fmt") or "").lower() == "ue8m0"
self.expert_fp4 = str(cfg_get(hf_config, "expert_dtype") or "").lower() == "fp4"

def storage(self, scheme: QuantScheme) -> dict[str, Stored]:
names = super().storage(scheme)
if self.e8m0 and scheme.kind is QuantKind.FP8_BLOCK:
names["weight_scale_inv"] = Stored("scale")
return names

def scheme_for_name(self, name: str) -> QuantScheme | None:
if self.convert_tables(name):
return self.SCHEMES["TABLE"]
Expand Down
10 changes: 8 additions & 2 deletions python/freetoken/layers/quantization/configs/modelopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from ..names import ancestors, name_set
from ..registry import register_dialect
from ..scheme import QuantScheme
from ..scheme import QuantKind, QuantScheme
from ..scheme import fp8_block_scheme, fp8_tensor_scheme, mxfp8_scheme, nvfp4_scheme
from .base import QuantConfig
from .base import QuantConfig, Stored


@register_dialect
Expand All @@ -24,6 +24,12 @@ class ModelOptConfig(QuantConfig):
"FP8_PB_WO": fp8_block_scheme("fp32"),
"MXFP8": mxfp8_scheme(),
}
STORAGE: ClassVar[dict[QuantKind, dict[str, str | Stored]]] = {
QuantKind.FP8_TENSOR: {"weight": "weight", "weight_scale": "weight_scale", "input_scale": "input_scale"},
QuantKind.FP8_BLOCK: {"weight": "weight", "weight_scale_inv": "weight_scale_inv"},
QuantKind.MXFP8: {"weight": "weight", "weight_scale_inv": "weight_scale_inv"},
QuantKind.NVFP4: {"weight": "weight", "weight_scale": "weight_scale", "weight_global": "weight_scale_2", "input_scale": "input_scale"},
}

@classmethod
def claims(cls, q: dict[str, Any]) -> bool:
Expand Down
6 changes: 4 additions & 2 deletions python/freetoken/layers/quantization/configs/mxfp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from ..names import name_set
from ..registry import register_dialect
from ..scheme import QuantScheme
from ..scheme import QuantKind, QuantScheme
from ..scheme import mxfp4_scheme
from .base import QuantConfig
from .base import QuantConfig, Stored


@register_dialect
Expand All @@ -16,6 +16,8 @@ class Mxfp4Config(QuantConfig):
dialect = "mxfp4"

SCHEME: ClassVar[QuantScheme] = mxfp4_scheme()
# the experts are stacked per layer (``gate_up_proj_blocks`` / ``_scales``), not per-Linear tensors; gpt_oss reads them itself
STORAGE: ClassVar[dict[QuantKind, dict[str, str | Stored]]] = {}

def __init__(self, q: dict[str, Any], hf_config: Any = None, *, name_map=None, unquantized=()):
super().__init__(name_map, unquantized)
Expand Down
3 changes: 2 additions & 1 deletion python/freetoken/layers/quantization/linear/nvfp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,5 @@ def create_weights(self, layer: Any) -> None:
layer.weight = torch.empty(g.out_features, g.in_features // 2, dtype=torch.uint8)
layer.weight_scale = torch.empty(g.out_features, g.in_features // GROUP, dtype=FP8)
layer.weight_global = torch.empty(g.out_features, dtype=torch.float16)
# input_scale stays undeclared: the W4A16 kernels never read it and today's readers drop it
# no W4A16 kernel reads it; declared so a W4A4 checkpoint loads complete and a W4A4 kernel finds it in place
layer.input_scale = torch.empty((), dtype=torch.float32) if self.scheme.has("input_scale") else None
12 changes: 9 additions & 3 deletions python/freetoken/layers/quantization/names.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ def substr_set(patterns: tuple[str, ...]) -> Matcher:


def ct_set(patterns: tuple[str, ...], *, class_names: bool) -> Matcher:
"""compressed-tensors ``targets`` / ``ignore``: module names, ``re:`` regexes, or the class name Linear."""
names = name_set(tuple(p for p in patterns if not p.startswith("re:") and p != "Linear"))
"""compressed-tensors ``targets`` / ``ignore``: module names, ``re:`` regexes, or the class name Linear.

A name covers that module alone, not its children: llm-compressor lists every skipped module, containers included, so an ``ignore`` entry for ``linear_attn`` says nothing about ``linear_attn.in_proj_qkv``."""
names = frozenset(p for p in patterns if not p.startswith("re:") and p != "Linear")
# a class name other than Linear cannot be matched from a module name alone; fail here rather than serve the module bf16
unknown = [p for p in names if "." not in p and p[:1].isupper()] if class_names else []
if unknown:
raise NotImplementedError(f"compressed-tensors target class {unknown[0]!r} is not supported; only Linear is")
regexes = [p[3:] for p in patterns if p.startswith("re:")]
rx = re.compile("|".join(f"(?:{r})" for r in regexes)) if regexes else None
any_linear = class_names and "Linear" in patterns
return lambda name: any_linear or names(name) or (rx is not None and rx.match(name) is not None)
return lambda name: any_linear or name in names or (rx is not None and rx.match(name) is not None)


_ROUTED_EXPERT = re.compile(r"\.experts\.\d+(\.|$)")
Expand Down
Loading