chore: import upstream snapshot with attribution
This commit is contained in:
+818
@@ -0,0 +1,818 @@
|
||||
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
# You may not use this file except in compliance with the License.
|
||||
# To view a copy of this license, visit http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# No warranties are given. The work is provided "AS IS", without warranty of any kind, express or implied.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import importlib
|
||||
import inspect
|
||||
from contextlib import nullcontext
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from typing import Any
|
||||
import warnings
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from fouroversix import (
|
||||
DataType,
|
||||
ModelQuantizationConfig,
|
||||
QuantizationConfig,
|
||||
QuantizedTensor,
|
||||
RoundStyle,
|
||||
ScaleRule,
|
||||
quantize_model,
|
||||
quantize_to_fp4,
|
||||
)
|
||||
from fouroversix.quantize.quantized_tensor import from_blocked
|
||||
|
||||
from utils.nvfp4_kernel import fp4_dequantize
|
||||
|
||||
_FUSED_KV_DEQUANT_DISABLED = False
|
||||
_FUSED_KV_DEQUANT_WARNED = False
|
||||
|
||||
QUANTIZATION_TYPE = {
|
||||
"weight": "weight",
|
||||
"activation": "activation",
|
||||
"kv": "kv",
|
||||
}
|
||||
|
||||
DEFAULT_GENERATOR_FILTERED_MODULES = [
|
||||
"text_embedding.0",
|
||||
"text_embedding.2",
|
||||
"patch_embedding",
|
||||
"time_projection.1",
|
||||
"time_embedding.0",
|
||||
"time_embedding.2",
|
||||
"head.head",
|
||||
"head.modulation",
|
||||
"re:.*norm_k$",
|
||||
"re:.*norm_q$",
|
||||
"re:.*norm1$",
|
||||
"re:.*norm2$",
|
||||
"re:.*norm3$"
|
||||
]
|
||||
|
||||
DEFAULT_REAL_SCORE_FILTERED_MODULES = list(DEFAULT_GENERATOR_FILTERED_MODULES)
|
||||
DEFAULT_FAKE_SCORE_FILTERED_MODULES = list(DEFAULT_GENERATOR_FILTERED_MODULES)
|
||||
DEFAULT_FILTERED_MODULES = list(DEFAULT_GENERATOR_FILTERED_MODULES)
|
||||
|
||||
FILTER_PROFILE_ALIASES = {
|
||||
"generator": "generator",
|
||||
"student": "generator",
|
||||
"real_score": "real_score",
|
||||
"teacher": "real_score",
|
||||
"fake_score": "fake_score",
|
||||
"critic": "fake_score",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LongLiveQuantizationConfig(QuantizationConfig):
|
||||
type: str = "weight"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
|
||||
if not isinstance(self.type, str):
|
||||
raise TypeError("Quantization type must be a string.")
|
||||
if self.type not in QUANTIZATION_TYPE:
|
||||
allowed = ", ".join(QUANTIZATION_TYPE.keys())
|
||||
raise ValueError(f"Unknown quantization type '{self.type}'. Expected one of: {allowed}.")
|
||||
|
||||
self.type = QUANTIZATION_TYPE[self.type]
|
||||
|
||||
|
||||
def _resolve_modules_to_not_convert(
|
||||
model: nn.Module,
|
||||
filtered_modules: list[str] | None,
|
||||
) -> list[str]:
|
||||
if not filtered_modules:
|
||||
return []
|
||||
|
||||
exact_names = set()
|
||||
regex_patterns = []
|
||||
for pattern in filtered_modules:
|
||||
if not isinstance(pattern, str):
|
||||
raise TypeError("Each filtered module pattern must be a string.")
|
||||
if pattern.startswith("re:"):
|
||||
regex_patterns.append(re.compile(pattern[3:]))
|
||||
else:
|
||||
exact_names.add(pattern)
|
||||
|
||||
resolved = []
|
||||
for module_name, _ in model.named_modules():
|
||||
if not module_name:
|
||||
continue
|
||||
if module_name in exact_names or any(
|
||||
regex.search(module_name) for regex in regex_patterns
|
||||
):
|
||||
resolved.append(module_name)
|
||||
|
||||
return sorted(set(resolved))
|
||||
|
||||
|
||||
def _get_default_filtered_modules(filter_profile: str | None) -> list[str]:
|
||||
if filter_profile is None:
|
||||
return list(DEFAULT_FILTERED_MODULES)
|
||||
|
||||
normalized = FILTER_PROFILE_ALIASES.get(filter_profile, filter_profile)
|
||||
if normalized == "generator":
|
||||
return list(DEFAULT_GENERATOR_FILTERED_MODULES)
|
||||
if normalized == "real_score":
|
||||
return list(DEFAULT_REAL_SCORE_FILTERED_MODULES)
|
||||
if normalized == "fake_score":
|
||||
return list(DEFAULT_FAKE_SCORE_FILTERED_MODULES)
|
||||
|
||||
allowed = ", ".join(sorted(FILTER_PROFILE_ALIASES))
|
||||
raise ValueError(
|
||||
f"Unknown filter_profile '{filter_profile}'. Expected one of: {allowed}.",
|
||||
)
|
||||
|
||||
|
||||
def _warn_for_te_config_mismatch(model_quant_config: ModelQuantizationConfig) -> None:
|
||||
config_entries = [("default", model_quant_config)]
|
||||
module_overrides = getattr(model_quant_config, "module_config_overrides", None) or {}
|
||||
config_entries.extend(sorted(module_overrides.items()))
|
||||
|
||||
mismatched_rules = []
|
||||
for module_name, module_config in config_entries:
|
||||
if getattr(module_config, "dtype", DataType.nvfp4) != DataType.nvfp4:
|
||||
raise NotImplementedError(
|
||||
"TransformerEngine replacement currently only supports NVFP4."
|
||||
)
|
||||
|
||||
for attr_name in (
|
||||
"scale_rule",
|
||||
"activation_scale_rule",
|
||||
"weight_scale_rule",
|
||||
"gradient_scale_rule",
|
||||
):
|
||||
rule = getattr(module_config, attr_name, None)
|
||||
if rule is not None and rule != ScaleRule.static_6:
|
||||
mismatched_rules.append(f"{module_name}:{attr_name}={rule}")
|
||||
|
||||
if mismatched_rules:
|
||||
preview = ", ".join(mismatched_rules[:8])
|
||||
if len(mismatched_rules) > 8:
|
||||
preview += ", ..."
|
||||
warnings.warn(
|
||||
"TransformerEngine NVFP4 path maps to `NVFP4BlockScaling` and does not "
|
||||
"replicate FourOverSix non-`static_6` scale rules exactly. "
|
||||
f"Mismatched config entries: {preview}",
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
|
||||
def _build_te_recipe(module_config: Any, te_recipe_kwargs: dict[str, Any] | None = None):
|
||||
recipe_module = importlib.import_module("transformer_engine.common.recipe")
|
||||
NVFP4BlockScaling = recipe_module.NVFP4BlockScaling
|
||||
|
||||
recipe_kwargs = {
|
||||
"disable_2d_quantization": not getattr(module_config, "weight_scale_2d", False),
|
||||
"disable_stochastic_rounding": (
|
||||
getattr(module_config, "gradient_round_style", RoundStyle.nearest)
|
||||
!= RoundStyle.stochastic
|
||||
),
|
||||
# FourOverSix only uses RHT in specific training paths, so keep TE conservative
|
||||
# by default and let callers override via `te_recipe_kwargs`.
|
||||
"disable_rht": True,
|
||||
}
|
||||
if te_recipe_kwargs:
|
||||
recipe_kwargs.update(te_recipe_kwargs)
|
||||
return NVFP4BlockScaling(**recipe_kwargs)
|
||||
|
||||
|
||||
class TransformerEngineLinear(nn.Module):
|
||||
"""A lightweight wrapper that routes a linear layer through TransformerEngine."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
module: nn.Linear,
|
||||
module_name: str,
|
||||
module_config: Any,
|
||||
inference_only: bool = False,
|
||||
low_precision_weights: bool = False,
|
||||
te_recipe_kwargs: dict[str, Any] | None = None,
|
||||
te_module_kwargs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
try:
|
||||
te = importlib.import_module("transformer_engine.pytorch")
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"TransformerEngine is not installed, but `use_transformer_engine=True` "
|
||||
"was requested."
|
||||
) from exc
|
||||
|
||||
if module.weight.device.type != "cuda":
|
||||
raise ValueError(
|
||||
"TransformerEngine replacement requires CUDA modules. "
|
||||
f"Module `{module_name}` is on `{module.weight.device}`."
|
||||
)
|
||||
|
||||
self.module_name = module_name
|
||||
self.in_features = module.in_features
|
||||
self.out_features = module.out_features
|
||||
self.inference_only = inference_only
|
||||
self.low_precision_weights = low_precision_weights
|
||||
self._te = te
|
||||
self._recipe = _build_te_recipe(
|
||||
module_config=module_config,
|
||||
te_recipe_kwargs=te_recipe_kwargs,
|
||||
)
|
||||
|
||||
module_kwargs = dict(te_module_kwargs or {})
|
||||
module_kwargs.setdefault("device", module.weight.device)
|
||||
module_kwargs.setdefault("params_dtype", module.weight.dtype)
|
||||
module_kwargs.setdefault("name", module_name)
|
||||
|
||||
fp8_model_init_fn = getattr(te, "fp8_model_init", None)
|
||||
if self.low_precision_weights and fp8_model_init_fn is None:
|
||||
warnings.warn(
|
||||
"TransformerEngine low-precision parameter init requested, but "
|
||||
"`fp8_model_init` is unavailable. Falling back to regular TE parameter "
|
||||
"storage for this inference path.",
|
||||
stacklevel=2,
|
||||
)
|
||||
self.low_precision_weights = False
|
||||
|
||||
model_init_context = (
|
||||
fp8_model_init_fn(
|
||||
enabled=True,
|
||||
recipe=self._recipe,
|
||||
preserve_high_precision_init_val=False,
|
||||
)
|
||||
if self.low_precision_weights
|
||||
else nullcontext()
|
||||
)
|
||||
with model_init_context:
|
||||
self.linear = te.Linear(
|
||||
module.in_features,
|
||||
module.out_features,
|
||||
bias=module.bias is not None,
|
||||
**module_kwargs,
|
||||
)
|
||||
|
||||
self._load_from_linear(module)
|
||||
|
||||
if self.inference_only:
|
||||
self.linear.requires_grad_(False)
|
||||
self.train(False)
|
||||
else:
|
||||
self.linear.weight.requires_grad_(module.weight.requires_grad)
|
||||
if self.linear.bias is not None and module.bias is not None:
|
||||
self.linear.bias.requires_grad_(module.bias.requires_grad)
|
||||
self.train(module.training)
|
||||
|
||||
def _copy_tensor_into_parameter(
|
||||
self,
|
||||
destination: torch.Tensor,
|
||||
source: torch.Tensor,
|
||||
) -> None:
|
||||
source = source.detach().to(device=destination.device)
|
||||
try:
|
||||
destination.copy_(source)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
destination.copy_(source.to(dtype=destination.dtype))
|
||||
|
||||
def _load_from_linear(self, module: nn.Linear) -> None:
|
||||
with torch.no_grad():
|
||||
try:
|
||||
self._copy_tensor_into_parameter(self.linear.weight, module.weight)
|
||||
if module.bias is not None and self.linear.bias is not None:
|
||||
self._copy_tensor_into_parameter(self.linear.bias, module.bias)
|
||||
return
|
||||
except Exception as copy_exc:
|
||||
state_dict = {
|
||||
"weight": module.weight.detach().to(device=self.linear.weight.device),
|
||||
}
|
||||
if module.bias is not None:
|
||||
state_dict["bias"] = module.bias.detach().to(
|
||||
device=self.linear.weight.device,
|
||||
)
|
||||
incompatible_keys = self.linear.load_state_dict(state_dict, strict=False)
|
||||
missing_keys = [
|
||||
key
|
||||
for key in getattr(incompatible_keys, "missing_keys", [])
|
||||
if key != "_extra_state"
|
||||
]
|
||||
unexpected_keys = list(getattr(incompatible_keys, "unexpected_keys", []))
|
||||
if missing_keys or unexpected_keys:
|
||||
raise RuntimeError(
|
||||
"Failed to load weights into TransformerEngine linear "
|
||||
f"`{self.module_name}`. missing_keys={missing_keys}, "
|
||||
f"unexpected_keys={unexpected_keys}"
|
||||
) from copy_exc
|
||||
|
||||
@property
|
||||
def weight(self) -> torch.Tensor:
|
||||
return self.linear.weight
|
||||
|
||||
@property
|
||||
def bias(self) -> torch.Tensor | None:
|
||||
return self.linear.bias
|
||||
|
||||
def _autocast_context(self):
|
||||
autocast_fn = getattr(self._te, "autocast", None)
|
||||
if autocast_fn is not None:
|
||||
return autocast_fn(enabled=True, recipe=self._recipe)
|
||||
fp8_autocast_fn = getattr(self._te, "fp8_autocast", None)
|
||||
if fp8_autocast_fn is None:
|
||||
raise AttributeError(
|
||||
"TransformerEngine does not expose `autocast` or `fp8_autocast`."
|
||||
)
|
||||
return fp8_autocast_fn(enabled=True, fp8_recipe=self._recipe)
|
||||
|
||||
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
with self._autocast_context():
|
||||
return self.linear(input)
|
||||
|
||||
def extra_repr(self) -> str:
|
||||
return (
|
||||
f"in_features={self.in_features}, "
|
||||
f"out_features={self.out_features}, "
|
||||
f"bias={self.bias is not None}, "
|
||||
f"inference_only={self.inference_only}, "
|
||||
f"low_precision_weights={self.low_precision_weights}, "
|
||||
"backend=transformer_engine"
|
||||
)
|
||||
|
||||
|
||||
def quantize_model_with_optional_te(
|
||||
model: nn.Module,
|
||||
model_quant_config: ModelQuantizationConfig,
|
||||
*,
|
||||
use_transformer_engine: bool = False,
|
||||
te_inference_only: bool = False,
|
||||
te_low_precision_weights: bool | None = None,
|
||||
te_recipe_kwargs: dict[str, Any] | None = None,
|
||||
te_module_kwargs: dict[str, Any] | None = None,
|
||||
te_fallback_to_fouroversix: bool = False,
|
||||
**kwargs,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Quantize a model with FourOverSix by default, or replace `nn.Linear` with
|
||||
TransformerEngine wrappers when `use_transformer_engine=True`.
|
||||
"""
|
||||
if not use_transformer_engine:
|
||||
quantize_model(model, model_quant_config, **kwargs)
|
||||
return []
|
||||
|
||||
if te_low_precision_weights is None:
|
||||
te_low_precision_weights = te_inference_only
|
||||
|
||||
if kwargs:
|
||||
if te_fallback_to_fouroversix:
|
||||
warnings.warn(
|
||||
"Additional kwargs passed to `quantize_model_with_optional_te` will "
|
||||
"only be forwarded to the fallback FourOverSix pass after "
|
||||
f"TransformerEngine replacement: {sorted(kwargs)}",
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
warnings.warn(
|
||||
"Additional kwargs passed to `quantize_model` are ignored in the "
|
||||
f"TransformerEngine path: {sorted(kwargs)}",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
_warn_for_te_config_mismatch(model_quant_config)
|
||||
|
||||
replaced_modules = []
|
||||
for module_name, module in list(model.named_modules()):
|
||||
if (
|
||||
module_name == ""
|
||||
or module_name in model_quant_config.modules_to_not_convert
|
||||
or not isinstance(module, nn.Linear)
|
||||
):
|
||||
continue
|
||||
|
||||
model.set_submodule(
|
||||
module_name,
|
||||
TransformerEngineLinear(
|
||||
module=module,
|
||||
module_name=module_name,
|
||||
module_config=model_quant_config.get_module_config(module_name),
|
||||
inference_only=te_inference_only,
|
||||
low_precision_weights=te_low_precision_weights,
|
||||
te_recipe_kwargs=te_recipe_kwargs,
|
||||
te_module_kwargs=te_module_kwargs,
|
||||
),
|
||||
)
|
||||
replaced_modules.append(module_name)
|
||||
|
||||
if te_fallback_to_fouroversix:
|
||||
quantize_model(model, model_quant_config, **kwargs)
|
||||
|
||||
return replaced_modules
|
||||
|
||||
|
||||
def _tensor_nbytes(tensor: torch.Tensor | None) -> int:
|
||||
if tensor is None:
|
||||
return 0
|
||||
return tensor.numel() * tensor.element_size()
|
||||
|
||||
|
||||
def _materialize_transformer_engine_weights_for_inference(
|
||||
model: nn.Module,
|
||||
target_device: torch.device | str | None = None,
|
||||
cache_transposed_weights: bool = False,
|
||||
) -> tuple[list[str], int, int]:
|
||||
del cache_transposed_weights
|
||||
|
||||
materialized_modules = []
|
||||
master_weight_bytes = 0
|
||||
quantized_weight_bytes = 0
|
||||
|
||||
for module_name, module in model.named_modules():
|
||||
if not isinstance(module, TransformerEngineLinear):
|
||||
continue
|
||||
|
||||
if target_device is not None:
|
||||
module.to(device=torch.device(target_device))
|
||||
|
||||
quantized_weight_bytes += _tensor_nbytes(module.weight)
|
||||
quantized_weight_bytes += _tensor_nbytes(module.bias)
|
||||
materialized_modules.append(module_name)
|
||||
|
||||
return materialized_modules, master_weight_bytes, quantized_weight_bytes
|
||||
|
||||
|
||||
def _materialize_mixed_quantized_weights_for_inference(
|
||||
model: nn.Module,
|
||||
target_device: torch.device | str | None = None,
|
||||
cache_transposed_weights: bool = False,
|
||||
) -> tuple[list[str], int, int]:
|
||||
te_modules, te_master_bytes, te_quantized_bytes = (
|
||||
_materialize_transformer_engine_weights_for_inference(
|
||||
model,
|
||||
target_device=target_device,
|
||||
cache_transposed_weights=cache_transposed_weights,
|
||||
)
|
||||
)
|
||||
f46_modules, f46_master_bytes, f46_quantized_bytes = (
|
||||
_materialize_quantized_weights_for_inference(
|
||||
model,
|
||||
target_device=target_device,
|
||||
cache_transposed_weights=cache_transposed_weights,
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
sorted(set(te_modules + f46_modules)),
|
||||
te_master_bytes + f46_master_bytes,
|
||||
te_quantized_bytes + f46_quantized_bytes,
|
||||
)
|
||||
|
||||
|
||||
def _materialize_quantized_weights_for_inference(
|
||||
model: nn.Module,
|
||||
target_device: torch.device | str | None = None,
|
||||
cache_transposed_weights: bool = False,
|
||||
) -> tuple[list[str], int, int]:
|
||||
"""
|
||||
Materialize quantized weights and drop master weights.
|
||||
|
||||
Optionally cache an additional transposed quantized layout for training paths that
|
||||
still require dgrad after the master weight is deleted (e.g. NVFP4 + LoRA).
|
||||
|
||||
This function expects modules replaced by `fouroversix.quantize_model`.
|
||||
"""
|
||||
materialized_modules = []
|
||||
master_weight_bytes = 0
|
||||
quantized_weight_bytes = 0
|
||||
|
||||
for module_name, module in model.named_modules():
|
||||
if not hasattr(module, "parameters_to_quantize") or not hasattr(
|
||||
module, "get_quantized_parameters",
|
||||
):
|
||||
continue
|
||||
|
||||
parameters_to_quantize = getattr(module, "parameters_to_quantize", ())
|
||||
if callable(parameters_to_quantize):
|
||||
parameters_to_quantize = parameters_to_quantize()
|
||||
if not parameters_to_quantize:
|
||||
continue
|
||||
|
||||
did_materialize = False
|
||||
for parameter_name in parameters_to_quantize:
|
||||
parameter = getattr(module, parameter_name, None)
|
||||
if parameter is None:
|
||||
continue
|
||||
|
||||
if isinstance(parameter, nn.Parameter):
|
||||
parameter_tensor = parameter.data
|
||||
elif isinstance(parameter, torch.Tensor):
|
||||
parameter_tensor = parameter
|
||||
else:
|
||||
continue
|
||||
|
||||
master_weight_bytes += parameter_tensor.numel() * parameter_tensor.element_size()
|
||||
get_quantized_parameters = module.get_quantized_parameters
|
||||
if (
|
||||
cache_transposed_weights
|
||||
and "include_transposed" in inspect.signature(
|
||||
get_quantized_parameters,
|
||||
).parameters
|
||||
):
|
||||
quantized_params = get_quantized_parameters(
|
||||
parameter_name,
|
||||
parameter_tensor,
|
||||
include_transposed=True,
|
||||
)
|
||||
else:
|
||||
quantized_params = get_quantized_parameters(
|
||||
parameter_name,
|
||||
parameter_tensor,
|
||||
)
|
||||
|
||||
for quantized_name, quantized_tensor in quantized_params.items():
|
||||
if not isinstance(quantized_tensor, torch.Tensor):
|
||||
continue
|
||||
|
||||
existing = getattr(module, quantized_name, None)
|
||||
dst_dtype = (
|
||||
existing.dtype
|
||||
if isinstance(existing, torch.Tensor)
|
||||
else quantized_tensor.dtype
|
||||
)
|
||||
if target_device is not None:
|
||||
dst_device = torch.device(target_device)
|
||||
elif isinstance(existing, torch.Tensor):
|
||||
dst_device = existing.device
|
||||
else:
|
||||
dst_device = quantized_tensor.device
|
||||
|
||||
quantized_tensor = quantized_tensor.to(
|
||||
device=dst_device,
|
||||
dtype=dst_dtype,
|
||||
)
|
||||
setattr(module, quantized_name, quantized_tensor)
|
||||
quantized_weight_bytes += (
|
||||
quantized_tensor.numel() * quantized_tensor.element_size()
|
||||
)
|
||||
|
||||
# Drop high-precision master weight once quantized weights are materialized.
|
||||
if isinstance(getattr(module, parameter_name, None), nn.Parameter):
|
||||
module.register_parameter(parameter_name, None)
|
||||
else:
|
||||
setattr(module, parameter_name, None)
|
||||
did_materialize = True
|
||||
|
||||
if did_materialize:
|
||||
if hasattr(module, "_quantized_weight"):
|
||||
delattr(module, "_quantized_weight")
|
||||
if hasattr(module, "_quantized_weight_transposed"):
|
||||
delattr(module, "_quantized_weight_transposed")
|
||||
if hasattr(module, "_quantized_weights"):
|
||||
delattr(module, "_quantized_weights")
|
||||
if hasattr(module, "config") and hasattr(module.config, "keep_master_weights"):
|
||||
module.config.keep_master_weights = False
|
||||
materialized_modules.append(module_name)
|
||||
|
||||
return materialized_modules, master_weight_bytes, quantized_weight_bytes
|
||||
|
||||
|
||||
def quantize_model_with_filter(
|
||||
model: nn.Module,
|
||||
quant_config: ModelQuantizationConfig | dict | None = None,
|
||||
filtered_modules: list[str] | None = None,
|
||||
filter_profile: str | None = None,
|
||||
use_default_filtered_modules: bool = False,
|
||||
cast_model_to_bf16: bool = True,
|
||||
materialize_for_inference: bool = False,
|
||||
materialize_target_device: torch.device | str | None = None,
|
||||
use_transformer_engine: bool = False,
|
||||
te_inference_only: bool = False,
|
||||
te_low_precision_weights: bool | None = None,
|
||||
te_recipe_kwargs: dict[str, Any] | None = None,
|
||||
te_module_kwargs: dict[str, Any] | None = None,
|
||||
te_fallback_to_fouroversix: bool = False,
|
||||
verbose: bool = True,
|
||||
**kwargs,
|
||||
) -> tuple[nn.Module, list[str]]:
|
||||
"""
|
||||
Quantize model with FourOverSix and optionally skip selected modules.
|
||||
|
||||
`filtered_modules` supports:
|
||||
- Exact module names, e.g. "head.head"
|
||||
- Regex patterns prefixed with "re:", e.g. "re:.*norm1$"
|
||||
|
||||
`filter_profile` selects which built-in filtered module profile to use when
|
||||
`use_default_filtered_modules=True`. Supported values:
|
||||
"generator"/"student" and "real_score"/"teacher".
|
||||
"""
|
||||
if quant_config is None:
|
||||
model_quant_config = ModelQuantizationConfig()
|
||||
elif isinstance(quant_config, dict):
|
||||
model_quant_config = ModelQuantizationConfig(**quant_config)
|
||||
elif isinstance(quant_config, ModelQuantizationConfig):
|
||||
model_quant_config = deepcopy(quant_config)
|
||||
else:
|
||||
raise TypeError(
|
||||
"quant_config must be ModelQuantizationConfig, dict, or None.",
|
||||
)
|
||||
|
||||
patterns = list(filtered_modules or [])
|
||||
if use_default_filtered_modules:
|
||||
patterns = _get_default_filtered_modules(filter_profile) + patterns
|
||||
|
||||
matched_modules = _resolve_modules_to_not_convert(model, patterns)
|
||||
modules_to_not_convert = set(model_quant_config.modules_to_not_convert or [])
|
||||
modules_to_not_convert.update(matched_modules)
|
||||
model_quant_config.modules_to_not_convert = sorted(modules_to_not_convert)
|
||||
|
||||
if cast_model_to_bf16:
|
||||
model.to(torch.bfloat16)
|
||||
|
||||
resolved_te_low_precision_weights = (
|
||||
te_inference_only if te_low_precision_weights is None else te_low_precision_weights
|
||||
)
|
||||
|
||||
te_replaced_modules = quantize_model_with_optional_te(
|
||||
model,
|
||||
model_quant_config,
|
||||
use_transformer_engine=use_transformer_engine,
|
||||
te_inference_only=te_inference_only,
|
||||
te_low_precision_weights=resolved_te_low_precision_weights,
|
||||
te_recipe_kwargs=te_recipe_kwargs,
|
||||
te_module_kwargs=te_module_kwargs,
|
||||
te_fallback_to_fouroversix=te_fallback_to_fouroversix,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if materialize_for_inference:
|
||||
materialize_fn = _materialize_quantized_weights_for_inference
|
||||
if use_transformer_engine and te_fallback_to_fouroversix:
|
||||
materialize_fn = _materialize_mixed_quantized_weights_for_inference
|
||||
elif use_transformer_engine:
|
||||
materialize_fn = _materialize_transformer_engine_weights_for_inference
|
||||
|
||||
materialized_modules, master_bytes, quantized_bytes = materialize_fn(
|
||||
model,
|
||||
target_device=materialize_target_device,
|
||||
)
|
||||
if verbose:
|
||||
print(
|
||||
"[quantize_model_with_filter] "
|
||||
f"materialized_modules={len(materialized_modules)}, "
|
||||
f"master_weight={master_bytes / (1024 ** 3):.3f} GiB, "
|
||||
f"quantized_weight={quantized_bytes / (1024 ** 3):.3f} GiB",
|
||||
)
|
||||
|
||||
if verbose:
|
||||
profile_label = filter_profile or "default"
|
||||
print(
|
||||
"[quantize_model_with_filter] "
|
||||
f"profile={profile_label}, "
|
||||
f"matched={len(matched_modules)}, "
|
||||
f"total_excluded={len(model_quant_config.modules_to_not_convert)}",
|
||||
)
|
||||
if use_transformer_engine:
|
||||
print(
|
||||
"[quantize_model_with_filter] "
|
||||
f"transformer_engine_replaced={len(te_replaced_modules)}, "
|
||||
f"inference_only={te_inference_only}, "
|
||||
f"low_precision_weights={resolved_te_low_precision_weights}, "
|
||||
f"fallback_to_fouroversix={te_fallback_to_fouroversix}",
|
||||
)
|
||||
|
||||
return model, matched_modules
|
||||
|
||||
|
||||
def _dequantize_kv_cache_fused_cuda(kv_list, max_blocks, num_heads, block_token_size, dtype):
|
||||
global _FUSED_KV_DEQUANT_DISABLED, _FUSED_KV_DEQUANT_WARNED
|
||||
|
||||
if _FUSED_KV_DEQUANT_DISABLED or max_blocks <= 0:
|
||||
return None
|
||||
|
||||
first_qt = kv_list[0]
|
||||
if first_qt.values.device.type != "cuda":
|
||||
return None
|
||||
|
||||
try:
|
||||
from utils.kernel.kv_dequant import dequantize_kv_cache_fp4
|
||||
|
||||
blocks = kv_list[:max_blocks]
|
||||
values = [qt.values for qt in blocks]
|
||||
scale_factors = [qt.scale_factors for qt in blocks]
|
||||
amax = [qt.amax for qt in blocks]
|
||||
|
||||
return dequantize_kv_cache_fp4(
|
||||
values,
|
||||
scale_factors,
|
||||
amax,
|
||||
num_heads=num_heads,
|
||||
block_token_size=block_token_size,
|
||||
dtype=dtype,
|
||||
scale_rule=first_qt.scale_rule,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - exercised only when extension is stale/missing
|
||||
_FUSED_KV_DEQUANT_DISABLED = True
|
||||
if not _FUSED_KV_DEQUANT_WARNED:
|
||||
warnings.warn(
|
||||
"Fused CUDA KV-cache dequantization is unavailable; falling back to "
|
||||
f"the Triton per-block path. Reason: {exc}",
|
||||
stacklevel=2,
|
||||
)
|
||||
_FUSED_KV_DEQUANT_WARNED = True
|
||||
return None
|
||||
|
||||
|
||||
def dequantize_kv_cache(kv_list, max_blocks, num_heads, block_token_size, dtype, device):
|
||||
"""
|
||||
Dequantize list of QuantizedTensor to a contiguous bf16 tensor.
|
||||
kv_list[block_idx] -> QuantizedTensor(block_token_size * num_heads, 128)
|
||||
Returns: [1, max_blocks * block_token_size, num_heads, 128]
|
||||
"""
|
||||
fused_result = _dequantize_kv_cache_fused_cuda(
|
||||
kv_list, max_blocks, num_heads, block_token_size, dtype,
|
||||
)
|
||||
if fused_result is not None:
|
||||
return fused_result
|
||||
|
||||
total_tokens = max_blocks * block_token_size
|
||||
result = torch.zeros([1, total_tokens, num_heads, 128], dtype=dtype, device=device)
|
||||
for block_idx in range(max_blocks):
|
||||
t_start = block_idx * block_token_size
|
||||
t_end = t_start + block_token_size
|
||||
# deq = kv_list[block_idx].dequantize(dtype)
|
||||
# triton fp4_dequantize
|
||||
qt = kv_list[block_idx]
|
||||
padded_shape = qt.padded_shape
|
||||
scales_2d = from_blocked(
|
||||
qt.scale_factors,
|
||||
(padded_shape[0], padded_shape[1] // 16),
|
||||
)
|
||||
global_scale = qt.amax / (
|
||||
qt.scale_rule.max_allowed_e2m1_value()
|
||||
* qt.scale_rule.max_allowed_e4m3_value()
|
||||
)
|
||||
deq = fp4_dequantize(
|
||||
kv_list[block_idx].values,
|
||||
scales_2d,
|
||||
global_scale,
|
||||
block_size=16,
|
||||
dtype=dtype,
|
||||
)
|
||||
result[0, t_start:t_end, :, :] = deq.view(block_token_size, num_heads, 128)
|
||||
return result
|
||||
|
||||
def clone_quantized_tensor(qt):
|
||||
"""Clone a QuantizedTensor by cloning its internal tensors."""
|
||||
return QuantizedTensor(
|
||||
values=qt.values.clone(),
|
||||
scale_factors=qt.scale_factors.clone(),
|
||||
amax=qt.amax.clone() if qt.amax is not None else None,
|
||||
dtype=qt.dtype,
|
||||
original_shape=qt.original_shape,
|
||||
scale_rule=qt.scale_rule,
|
||||
padded_shape=qt.padded_shape,
|
||||
)
|
||||
|
||||
|
||||
def copy_quantized_into(slot: QuantizedTensor, src: QuantizedTensor) -> None:
|
||||
"""In-place copy a QuantizedTensor's data into a pre-allocated slot.
|
||||
|
||||
Keeps the slot's `values`/`scale_factors`/`amax` buffers persistent
|
||||
(their addresses don't change) so cudagraph allocator does not see them
|
||||
as fresh outputs that can be reused across step boundaries. Used by the
|
||||
quantized KV cache rolling/insert paths.
|
||||
"""
|
||||
slot.values.copy_(src.values)
|
||||
slot.scale_factors.copy_(src.scale_factors)
|
||||
if src.amax is not None and slot.amax is not None:
|
||||
slot.amax.copy_(src.amax)
|
||||
|
||||
|
||||
def k_smooth(k: torch.Tensor) -> torch.Tensor:
|
||||
return k - k.mean(dim=-1, keepdim=True)
|
||||
|
||||
def quantize_kv(k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
|
||||
B, S, H, D = k.shape
|
||||
# B is always 1
|
||||
# S is the number of tokens
|
||||
# H is the number of heads
|
||||
# D is the dimension of the key and value
|
||||
|
||||
config = QuantizationConfig(scale_rule="mse", backend="cuda")
|
||||
# per head quantization
|
||||
for head in range(H):
|
||||
k_head = k[:, :, head, :]
|
||||
v_head = v[:, :, head, :]
|
||||
k_head = k_smooth(k_head)
|
||||
v_head = v_head
|
||||
k_head = quantize_to_fp4(k_head, config)
|
||||
v_head = quantize_to_fp4(v_head, config)
|
||||
k[:, :, head, :] = k_head
|
||||
v[:, :, head, :] = v_head
|
||||
return k, v
|
||||
Reference in New Issue
Block a user