NeMo
Megatron-LM / megatron /core /quantization /quant_config.py
KexuanShi's picture
Upload folder using huggingface_hub
88e6849 verified
Raw
History Blame Contribute Delete
7.87 kB
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
"""
Provide base functionality for quantization purposes.
Usage comes from a user-provide YAML file, for example:
configs:
nvfp4:
$payload1
mxfp8:
$payload2
matchers:
fc1:
config: "nvfp4"
type: "glob"
pattern: "*fc1*"
enabled: True
fc2:
config: "nvfp4"
type: "glob"
pattern: "*fc2*"
enabled: True
default:
config: "mxfp8"
type: "glob"
pattern: "*"
enabled: True
The user-passed configuration is split into 2 distinct pieces:
* A set of quantization configs, describing *how* a given operator will be quantized
Note: This is consumed by the operator(s), and the particular operators being instantiated
are responsible for parsing this configuration if they support configurable quantization.
* An ordered collection of matchers that determine what quantization config (if any) is
applied to a given operator. The first matcher in the collection that successfully matches
the context determines the key from the configs dict. If a matcher doesn't match, the rest
of the matchers in the list are tested against.
Matchers define a type, or style of matching - "glob" is bash-style, but this
can be extended by inheriting from the abstract Matcher class to define a new match type.
The idea here is to provide an ability to define arbitrarily-complicated recipes in as
friendly a way as possible.
"""
import fnmatch
import logging
from abc import ABC, abstractmethod
from copy import deepcopy
from dataclasses import dataclass
from typing import Dict, List, Optional
from megatron.core.utils import log_single_rank
logger = logging.getLogger(__name__)
try:
import yaml
HAVE_YAML = True
except ImportError:
HAVE_YAML = False
@dataclass
class MatchContext:
"""Layer context that can be matched to a quantization config."""
module_path: str
layer_number: Optional[int]
class QuantizationConfig:
"""Wrapper around configuration dictionary for layer's numerics."""
def __init__(self, config: dict, match_input: MatchContext, config_key: str):
"""
Initialize the quantization config.
The configuration dictionary is copied to defend against modules that
mutate the configuration corrupting the configuration of other modules.
"""
self.config = deepcopy(config)
self.match_input = match_input
self.config_key = config_key
def __repr__(self) -> str:
return (
f"{type(self).__name__}(config={self.config}, "
f"match_input={self.match_input}, config_key={self.config_key})"
)
class Matcher(ABC):
"""Matcher interface to select layers."""
@abstractmethod
def match(self, context: MatchContext) -> Optional[str]:
"""
Match a layer based on its qualified name.
If it does not match, return None. If it matches,
return the configuration key to select for the layer.
"""
return None
class GlobMatcher(Matcher):
"""Pattern based matcher using fnmatch to compare the module path against a pattern.
fnmatch supplies glob-style matching similar to that used in bash, allowing for matches like:
match_str="*fc2*" - match anything which includes "fc2" anywhere in the string.
match_str="*fc2" - match anything which includes "fc2" at the end of the string.
match_str="*layers.10*" - match anything with "layers.10" (layer #) in the string.
"""
def __init__(self, pattern: str, config_key: str):
self.pattern = pattern
self.config_key = config_key
def match(self, context: MatchContext) -> Optional[str]:
"""Pattern based match."""
if fnmatch.fnmatch(context.module_path, self.pattern):
return self.config_key
return None
def __repr__(self) -> str:
return f"{type(self).__name__}(pattern={self.pattern}, config_key={self.config_key})"
class RecipeConfig:
"""Hold recipe information (matcher_fn) -> Configs)"""
def __init__(self, matchers: List[Matcher], config_dict: Dict[str, Dict]):
self.configs = config_dict
self.matchers = matchers
@staticmethod
def _build_matchers(matchers_dict: Dict | None) -> List[Matcher]:
# NOTE(slayton): We rely on order for matchers because it allows us to specify an
# override ordering from the yaml structure. Process matchers in order of
# definition, so we can have fallthrus.
matchers: List[Matcher] = []
if matchers_dict is None:
return matchers
for name, matcher in matchers_dict.items():
enabled = matcher.get("enabled", False)
if not enabled:
continue
match_type = matcher.get("type", None)
assert match_type is not None, f'Matcher must specify a "type" field'
if match_type == "glob":
pattern = matcher.get("pattern", None)
config = matcher.get("config", None)
assert pattern is not None, f'GlobMatcher must specify "pattern" field'
assert config is not None, f'GlobMatcher must specify "config" field'
m = GlobMatcher(pattern, config)
else:
raise NotImplementedError(f"Match type '{match_type}' not implemented")
matchers.append(m)
return matchers
@staticmethod
def from_yaml_file(recipe_yaml_path: str) -> "RecipeConfig":
"""Loads recipe from yaml configuration."""
if not HAVE_YAML:
raise ImportError("yaml is not installed. Please install it with `pip install pyyaml`.")
with open(recipe_yaml_path, "r") as f:
config = yaml.load(f, Loader=yaml.SafeLoader)
log_single_rank(
logger,
logging.INFO,
f"Loaded quantization recipe from path '{recipe_yaml_path}'. " f"Contents: '{config}'",
)
return RecipeConfig.from_config_dict(config)
@staticmethod
def from_config_dict(config: Dict) -> "RecipeConfig":
"""Loads recipe from dict configuration."""
matchers_config = config.get("matchers", None)
matchers = RecipeConfig._build_matchers(matchers_config)
config_dict = config.get("configs", {})
return RecipeConfig(matchers, config_dict)
def match_to_config_key(self, operator_context: MatchContext) -> str | None:
"""
Gives an operator's context, return a configuration key if
necessary, or sentinel (None) denoting no matchers matched.
"""
for matcher in self.matchers:
config_key = matcher.match(operator_context)
if config_key is not None:
log_single_rank(
logger,
logging.INFO,
f'Context ({operator_context}) matched to quant config "{config_key}"',
)
return config_key
log_single_rank(
logger, logging.INFO, f"No config key match found for Context ({operator_context})"
)
return None
def match(self, operator_context: MatchContext) -> QuantizationConfig | None:
"""
Gives an operator's context, return a QuantizationConfig if
necessary, or sentinel (None) denoting no matchers matched.
"""
config_key = self.match_to_config_key(operator_context)
if config_key is not None:
return QuantizationConfig(
self.configs[config_key], match_input=operator_context, config_key=config_key
)
return None
def __repr__(self) -> str:
s = f"{type(self).__name__}(\n"
for matcher in self.matchers:
s += f" matcher({repr(matcher)}\n"
s += ")"
return s