94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
568 lines
21 KiB
Python
568 lines
21 KiB
Python
import glob
|
|
import json
|
|
import os
|
|
import re
|
|
import struct
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from safetensors import safe_open
|
|
|
|
from sglang.multimodal_gen.runtime.layers.quantization import (
|
|
QuantizationConfig,
|
|
get_quantization_config,
|
|
)
|
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
def normalize_flat_modelopt_quant_config(
|
|
quant_cfg: dict[str, Any] | None,
|
|
) -> dict[str, Any] | None:
|
|
"""Fill required diffusers fields for flat ModelOpt component configs."""
|
|
if not isinstance(quant_cfg, dict) or quant_cfg.get("quant_method") != "modelopt":
|
|
return quant_cfg
|
|
|
|
quant_algo = str(
|
|
quant_cfg.get("quant_algo")
|
|
or quant_cfg.get("quantization", {}).get("quant_algo")
|
|
or ""
|
|
).upper()
|
|
if not quant_algo:
|
|
return quant_cfg
|
|
|
|
normalized = dict(quant_cfg)
|
|
normalized.setdefault("quant_type", quant_algo)
|
|
return normalized
|
|
|
|
|
|
def _infer_nvfp4_group_size_from_tensors(weight, scale) -> Optional[int]:
|
|
"""Infer NVFP4 group_size from serialized weight/scale tensor shapes."""
|
|
return _infer_nvfp4_group_size_from_shapes(
|
|
getattr(weight, "shape", ()),
|
|
getattr(scale, "shape", ()),
|
|
)
|
|
|
|
|
|
def _infer_nvfp4_group_size_from_shapes(weight_shape, scale_shape) -> Optional[int]:
|
|
weight_shape = tuple(weight_shape or ())
|
|
scale_shape = tuple(scale_shape or ())
|
|
if len(weight_shape) < 2:
|
|
return None
|
|
|
|
input_size = int(weight_shape[1]) * 2
|
|
if input_size <= 0:
|
|
return None
|
|
|
|
candidate_num_groups: list[int] = []
|
|
if len(scale_shape) >= 2:
|
|
candidate_num_groups.append(int(scale_shape[-1]))
|
|
elif len(scale_shape) == 1:
|
|
scale_len = int(scale_shape[0])
|
|
if scale_len == int(weight_shape[0]):
|
|
candidate_num_groups.append(1)
|
|
candidate_num_groups.append(scale_len)
|
|
else:
|
|
candidate_num_groups.append(1)
|
|
|
|
for num_groups in candidate_num_groups:
|
|
if num_groups <= 0:
|
|
continue
|
|
if input_size % num_groups == 0:
|
|
return input_size // num_groups
|
|
|
|
return None
|
|
|
|
|
|
def _read_safetensors_tensor_metadata(file_path: str) -> dict[str, dict[str, Any]]:
|
|
with open(file_path, "rb") as f:
|
|
header_len = struct.unpack("<Q", f.read(8))[0]
|
|
header = json.loads(f.read(header_len))
|
|
header.pop("__metadata__", None)
|
|
return header
|
|
|
|
|
|
def _is_nvfp4_tensor_family(
|
|
module_name: str,
|
|
tensor_metadata: dict[str, dict[str, Any]],
|
|
) -> bool:
|
|
weight_metadata = tensor_metadata.get(f"{module_name}.weight")
|
|
scale_metadata = tensor_metadata.get(f"{module_name}.weight_scale")
|
|
if weight_metadata is None or scale_metadata is None:
|
|
return False
|
|
|
|
weight_dtype = str(weight_metadata.get("dtype", "")).upper()
|
|
scale_dtype = str(scale_metadata.get("dtype", "")).upper()
|
|
scale_shape = scale_metadata.get("shape", [])
|
|
return weight_dtype == "U8" and "F8_E4M3" in scale_dtype and len(scale_shape) >= 2
|
|
|
|
|
|
def _resolve_quant_method_name(quant_cfg: dict) -> str:
|
|
quant_cfg = normalize_flat_modelopt_quant_config(quant_cfg) or quant_cfg
|
|
quant_method = quant_cfg.get("quant_method")
|
|
if quant_method == "bitsandbytes":
|
|
return "bitsandbytes"
|
|
if quant_method != "modelopt":
|
|
return quant_method
|
|
|
|
quant_algo = (
|
|
quant_cfg.get("quant_algo")
|
|
or quant_cfg.get("quantization", {}).get("quant_algo")
|
|
or ""
|
|
).upper()
|
|
if quant_algo == "MIXED_PRECISION":
|
|
raise ValueError(
|
|
"ModelOpt mixed precision is not supported by the current SGLang diffusion runtime."
|
|
)
|
|
if "FP8" in quant_algo:
|
|
return "modelopt_fp8"
|
|
if "FP4" in quant_algo or "NVFP4" in quant_algo:
|
|
return "modelopt_fp4"
|
|
raise ValueError(f"Unsupported ModelOpt quant_algo for diffusion: {quant_algo}")
|
|
|
|
|
|
def _load_quant_cls(quant_cfg: dict):
|
|
quant_method = _resolve_quant_method_name(quant_cfg)
|
|
if not quant_method:
|
|
raise ValueError("Missing quant_method in quantization config.")
|
|
return get_quantization_config(quant_method)
|
|
|
|
|
|
def find_quant_modelslim_config(model_config, component_model_path):
|
|
# Try exact name first, then glob for variant filenames (e.g. after repack)
|
|
quant_config_file = Path(component_model_path, "quant_model_description.json")
|
|
if not quant_config_file.is_file():
|
|
candidates = sorted(
|
|
Path(component_model_path).glob("quant_model_description*.json")
|
|
)
|
|
quant_config_file = candidates[0] if candidates else None
|
|
|
|
quant_cfg = None
|
|
if quant_config_file is not None and Path(quant_config_file).is_file():
|
|
with open(quant_config_file) as f:
|
|
quant_cfg = json.load(f)
|
|
# This field is required for flagless model loading but is not present in
|
|
# modelslim model description, so we're adding it here manually.
|
|
quant_cfg["quant_method"] = "modelslim"
|
|
|
|
return quant_cfg
|
|
|
|
|
|
def replace_prefix(key: str, prefix_mapping: dict[str, str]) -> str:
|
|
for prefix, new_prefix in prefix_mapping.items():
|
|
if key.startswith(prefix):
|
|
key = key.replace(prefix, new_prefix, 1)
|
|
return key
|
|
|
|
|
|
def get_quant_config(
|
|
model_config,
|
|
component_model_path: str,
|
|
packed_modules_mapping: Dict[str, List[str]] = {},
|
|
reverse_param_names_mapping: Dict[str, List[str]] = {},
|
|
remap_prefix: Dict[str, str] | None = None,
|
|
quant_ignore_remap: Optional[Dict[str, str]] = None,
|
|
) -> QuantizationConfig:
|
|
quant_cfg = find_quant_modelslim_config(model_config, component_model_path)
|
|
if quant_cfg is not None:
|
|
quant_cls = _load_quant_cls(quant_cfg)
|
|
return quant_cls.from_config(quant_cfg, reverse_param_names_mapping)
|
|
|
|
if "quantization_config" not in model_config:
|
|
return None
|
|
|
|
hf_quant_config = normalize_flat_modelopt_quant_config(
|
|
model_config["quantization_config"]
|
|
)
|
|
if hf_quant_config is not None and not isinstance(hf_quant_config, dict):
|
|
hf_quant_config = hf_quant_config.to_dict()
|
|
quant_cls = _load_quant_cls(hf_quant_config)
|
|
|
|
# GGUF doesn't have config file
|
|
if hf_quant_config["quant_method"] == "gguf":
|
|
return quant_cls.from_config({})
|
|
|
|
# some vision model may keep quantization_config in their text_config
|
|
hf_text_config = getattr(model_config, "text_config", None)
|
|
if hf_quant_config is None and hf_text_config is not None:
|
|
hf_quant_config = getattr(hf_text_config, "quantization_config", None)
|
|
if hf_quant_config is None:
|
|
# compressed-tensors uses a compressions_config
|
|
hf_quant_config = getattr(model_config, "compression_config", None)
|
|
if hf_quant_config is not None:
|
|
hf_quant_config["packed_modules_mapping"] = packed_modules_mapping
|
|
is_modelopt_fp8 = (
|
|
hf_quant_config.get("quant_method") == "modelopt"
|
|
and "FP8" in str(hf_quant_config.get("quant_algo", "")).upper()
|
|
)
|
|
extra_kwargs = (
|
|
{"ignore_remap": quant_ignore_remap}
|
|
if quant_ignore_remap and is_modelopt_fp8
|
|
else {}
|
|
)
|
|
return quant_cls.from_config(hf_quant_config, **extra_kwargs)
|
|
|
|
model_name_or_path = model_config["model_path"]
|
|
hf_folder = model_name_or_path
|
|
|
|
possible_config_filenames = quant_cls.get_config_filenames()
|
|
|
|
# If the quantization config is not found, use the default config.
|
|
if not possible_config_filenames:
|
|
return quant_cls()
|
|
|
|
config_files = glob.glob(os.path.join(hf_folder, "*.json"))
|
|
|
|
quant_config_files = [
|
|
f for f in config_files if any(f.endswith(x) for x in possible_config_filenames)
|
|
]
|
|
if len(quant_config_files) == 0:
|
|
raise ValueError(
|
|
f"Cannot find the config file for {model_config['quantization_config']['quant_method']}"
|
|
)
|
|
if len(quant_config_files) > 1:
|
|
raise ValueError(
|
|
f"Found multiple config files for {model_config['quantization_config']['quant_method']}: "
|
|
f"{quant_config_files}"
|
|
)
|
|
|
|
quant_config_file = quant_config_files[0]
|
|
with open(quant_config_file) as f:
|
|
config = json.load(f)
|
|
if remap_prefix is not None and "quantization" in config:
|
|
exclude_modules = [
|
|
replace_prefix(key, remap_prefix)
|
|
for key in config["quantization"]["exclude_modules"]
|
|
]
|
|
config["quantization"]["exclude_modules"] = exclude_modules
|
|
config["packed_modules_mapping"] = packed_modules_mapping
|
|
return quant_cls.from_config(config)
|
|
|
|
|
|
def handle_fp8_metadata_format(quant_config_dict):
|
|
layers = quant_config_dict.get("layers", {})
|
|
if any(
|
|
isinstance(v, dict) and "float8" in v.get("format", "") for v in layers.values()
|
|
):
|
|
quant_config_dict["quant_method"] = "fp8"
|
|
quant_config_dict["activation_scheme"] = "dynamic"
|
|
return quant_config_dict
|
|
|
|
|
|
def get_quant_config_from_safetensors_metadata(
|
|
file_path: str,
|
|
) -> Optional[QuantizationConfig]:
|
|
"""Extract quantization config from a safetensors file's metadata header.
|
|
Returns None if no recognizable quantization metadata is found.
|
|
"""
|
|
metadata = get_metadata_from_safetensors_file(file_path)
|
|
if not metadata:
|
|
return None
|
|
|
|
quant_config_str = metadata.get("_quantization_metadata")
|
|
quant_config_dict = None
|
|
if quant_config_str:
|
|
try:
|
|
quant_config_dict = json.loads(quant_config_str)
|
|
except Exception:
|
|
quant_config_dict = None
|
|
|
|
if quant_config_dict is None:
|
|
quant_config_str = metadata.get("quantization_config")
|
|
if not quant_config_str:
|
|
return None
|
|
try:
|
|
quant_config_dict = json.loads(quant_config_str)
|
|
except Exception:
|
|
return None
|
|
|
|
if not quant_config_dict:
|
|
return None
|
|
|
|
# handle diffusers fp8 safetensors metadata format
|
|
if (
|
|
"quant_method" not in quant_config_dict
|
|
and "format_version" in quant_config_dict
|
|
and "layers" in quant_config_dict
|
|
):
|
|
quant_config_dict = handle_fp8_metadata_format(quant_config_dict)
|
|
|
|
quant_method = quant_config_dict.get("quant_method")
|
|
if not quant_method:
|
|
return None
|
|
|
|
try:
|
|
quant_cls = _load_quant_cls(quant_config_dict)
|
|
config = quant_cls.from_config(quant_config_dict)
|
|
logger.debug(f"Get quantization config from safetensors file: {file_path}")
|
|
return config
|
|
except Exception as _e:
|
|
return None
|
|
|
|
|
|
def get_metadata_from_safetensors_file(file_path: str):
|
|
try:
|
|
with safe_open(file_path, framework="pt", device="cpu") as f:
|
|
metadata = f.metadata()
|
|
return metadata
|
|
except Exception as e:
|
|
logger.warning(e)
|
|
|
|
|
|
def _canonicalize_modulation_exclude(module_name: str) -> str:
|
|
"""Map a serialized modulation weight's parent to the runtime linear prefix.
|
|
|
|
Qwen-Image wraps the modulation projection in ``nn.Sequential(SiLU, Linear)``,
|
|
so its weights serialize as ``...img_mod.1.weight`` while the runtime
|
|
ReplicatedLinear advertises ``...img_mod`` as its quant/exclusion prefix.
|
|
Strip the trailing Sequential index so a safetensors-inferred BF16 exclude
|
|
entry actually matches the linear (mirrors the ModelOpt FP8 converter, which
|
|
canonicalizes ``.img_mod.1``/``.txt_mod.1`` to ``.img_mod``/``.txt_mod``).
|
|
No-op for any other module name.
|
|
"""
|
|
if module_name.endswith((".img_mod.1", ".txt_mod.1")):
|
|
return module_name.removesuffix(".1")
|
|
return module_name
|
|
|
|
|
|
def _build_nvfp4_config_from_safetensors_files(
|
|
file_paths: list[str],
|
|
param_names_mapping_dict: Optional[dict] = None,
|
|
reverse_param_names_mapping_dict: Optional[dict] = None,
|
|
fallback_group_size: Optional[int] = None,
|
|
) -> Optional[QuantizationConfig]:
|
|
"""Build a single NVFP4 config by aggregating metadata across multiple files.
|
|
|
|
Some checkpoints split BF16 fallback layers and NVFP4 layers across multiple
|
|
safetensors. Building the config from only the first matching file can
|
|
incorrectly exclude layers that are quantized in a later shard.
|
|
"""
|
|
group_size = None
|
|
quantized_bfl_modules: set[str] = set()
|
|
non_quantized_bfl_modules: set[str] = set()
|
|
files_with_nvfp4_signal: list[str] = []
|
|
checkpoint_uses_packed_qkv = False
|
|
checkpoint_uses_comfy_quant = False
|
|
packed_qkv_pattern = re.compile(
|
|
r"^(double_blocks\.\d+\.(img|txt)_attn\.qkv|single_blocks\.\d+\.linear1)\."
|
|
)
|
|
|
|
for file_path in file_paths:
|
|
metadata = get_metadata_from_safetensors_file(file_path)
|
|
quant_config_dict = None
|
|
metadata_signals_nvfp4 = False
|
|
if metadata:
|
|
quant_config_str = metadata.get("_quantization_metadata")
|
|
if quant_config_str:
|
|
try:
|
|
quant_config_dict = json.loads(quant_config_str)
|
|
except json.JSONDecodeError:
|
|
quant_config_dict = None
|
|
else:
|
|
quant_algo = str(quant_config_dict.get("quant_algo", "")).upper()
|
|
quant_type = str(quant_config_dict.get("quant_type", "")).upper()
|
|
metadata_signals_nvfp4 = (
|
|
"NVFP4" in quant_algo
|
|
or "FP4" in quant_algo
|
|
or "NVFP4" in quant_type
|
|
)
|
|
|
|
file_quantized_modules: set[str] = set()
|
|
if (
|
|
quant_config_dict is not None
|
|
and "format_version" in quant_config_dict
|
|
and "layers" in quant_config_dict
|
|
):
|
|
layers = quant_config_dict.get("layers", {})
|
|
file_quantized_modules.update(
|
|
layer_name
|
|
for layer_name, layer_cfg in layers.items()
|
|
if isinstance(layer_cfg, dict) and layer_cfg.get("format") == "nvfp4"
|
|
)
|
|
|
|
tensor_metadata = _read_safetensors_tensor_metadata(file_path)
|
|
with safe_open(file_path, framework="pt", device="cpu") as f:
|
|
all_keys = set(f.keys())
|
|
if any(packed_qkv_pattern.match(k) for k in all_keys):
|
|
checkpoint_uses_packed_qkv = True
|
|
if any(k.endswith(".comfy_quant") for k in all_keys):
|
|
checkpoint_uses_comfy_quant = True
|
|
|
|
# Some ModelOpt NVFP4 exports only store a flat config.json plus
|
|
# per-file metadata without the diffusers `layers` section. Infer
|
|
# quantized modules directly from tensor families in that case.
|
|
# Mixed checkpoints may also contain FP8 fallback layers with scalar
|
|
# `.weight_scale`, so require packed uint8 weights and block scales.
|
|
file_quantized_modules.update(
|
|
key[: -len(".weight_scale")]
|
|
for key in all_keys
|
|
if key.endswith(".weight_scale")
|
|
and _is_nvfp4_tensor_family(
|
|
key[: -len(".weight_scale")], tensor_metadata
|
|
)
|
|
)
|
|
|
|
if file_quantized_modules or metadata_signals_nvfp4:
|
|
files_with_nvfp4_signal.append(file_path)
|
|
quantized_bfl_modules.update(file_quantized_modules)
|
|
|
|
if group_size is None:
|
|
for layer_name in sorted(file_quantized_modules):
|
|
weight_key = f"{layer_name}.weight"
|
|
scale_key = f"{layer_name}.weight_scale"
|
|
weight_metadata = tensor_metadata.get(weight_key)
|
|
scale_metadata = tensor_metadata.get(scale_key)
|
|
if weight_metadata is not None and scale_metadata is not None:
|
|
group_size = _infer_nvfp4_group_size_from_shapes(
|
|
weight_metadata.get("shape"),
|
|
scale_metadata.get("shape"),
|
|
)
|
|
if group_size is not None:
|
|
break
|
|
|
|
for k in sorted(all_keys):
|
|
if not k.endswith(".weight"):
|
|
continue
|
|
module_name = k[: -len(".weight")]
|
|
if module_name not in file_quantized_modules:
|
|
non_quantized_bfl_modules.add(module_name)
|
|
|
|
if not files_with_nvfp4_signal:
|
|
return None
|
|
|
|
if (
|
|
group_size is not None
|
|
and fallback_group_size is not None
|
|
and group_size != fallback_group_size
|
|
):
|
|
logger.warning(
|
|
"NVFP4 group_size inferred from safetensors (%d) does not match config (%d); "
|
|
"preferring safetensors.",
|
|
group_size,
|
|
fallback_group_size,
|
|
)
|
|
|
|
if group_size is None and fallback_group_size is not None:
|
|
logger.info(
|
|
"Falling back to config-derived NVFP4 group_size=%d for %s",
|
|
fallback_group_size,
|
|
", ".join(files_with_nvfp4_signal),
|
|
)
|
|
group_size = fallback_group_size
|
|
|
|
if group_size is None:
|
|
logger.warning(
|
|
"Could not infer group_size from NVFP4 safetensors: %s",
|
|
", ".join(files_with_nvfp4_signal),
|
|
)
|
|
return None
|
|
|
|
exclude_bfl_modules = sorted(non_quantized_bfl_modules - quantized_bfl_modules)
|
|
|
|
exclude_modules = []
|
|
mapping_fn = None
|
|
reverse_mapping_fn = None
|
|
if param_names_mapping_dict or reverse_param_names_mapping_dict:
|
|
from sglang.multimodal_gen.runtime.loader.utils import get_param_names_mapping
|
|
|
|
if param_names_mapping_dict:
|
|
mapping_fn = get_param_names_mapping(param_names_mapping_dict)
|
|
if reverse_param_names_mapping_dict:
|
|
reverse_mapping_fn = get_param_names_mapping(
|
|
reverse_param_names_mapping_dict
|
|
)
|
|
|
|
for module_bfl in exclude_bfl_modules:
|
|
raw_weight_name = f"{module_bfl}.weight"
|
|
if mapping_fn is not None:
|
|
mapped, _, _ = mapping_fn(raw_weight_name)
|
|
if mapped != raw_weight_name:
|
|
exclude_modules.append(
|
|
mapped[: -len(".weight")] if mapped.endswith(".weight") else mapped
|
|
)
|
|
continue
|
|
|
|
if reverse_mapping_fn is not None:
|
|
reverse_mapped, _, _ = reverse_mapping_fn(raw_weight_name)
|
|
if reverse_mapped != raw_weight_name:
|
|
exclude_modules.append(
|
|
reverse_mapped[: -len(".weight")]
|
|
if reverse_mapped.endswith(".weight")
|
|
else reverse_mapped
|
|
)
|
|
continue
|
|
|
|
exclude_modules.append(module_bfl)
|
|
|
|
exclude_modules = sorted(
|
|
{_canonicalize_modulation_exclude(m) for m in exclude_modules}
|
|
)
|
|
|
|
try:
|
|
quant_cls = get_quantization_config("modelopt_fp4")
|
|
checkpoint_uses_swizzled_scales = (
|
|
checkpoint_uses_packed_qkv or checkpoint_uses_comfy_quant
|
|
)
|
|
result = quant_cls.from_config(
|
|
{
|
|
"quant_algo": "NVFP4",
|
|
"group_size": group_size,
|
|
"ignore": exclude_modules,
|
|
"checkpoint_uses_packed_qkv": checkpoint_uses_packed_qkv,
|
|
# packed-QKV and Comfy NVFP4 checkpoints store serialized
|
|
# weights/scales in the FlashInfer/CUTLASS checkpoint layout
|
|
"checkpoint_weight_scale_layout": (
|
|
"swizzled" if checkpoint_uses_swizzled_scales else "linear"
|
|
),
|
|
"swap_weight_nibbles": checkpoint_uses_swizzled_scales,
|
|
}
|
|
)
|
|
logger.info(
|
|
"Built NVFP4 quant config from %d safetensors: group_size=%d, %d excluded modules, packed_qkv=%s, comfy_quant=%s, scale_layout=%s, swap_nibbles=%s",
|
|
len(files_with_nvfp4_signal),
|
|
group_size,
|
|
len(exclude_modules),
|
|
checkpoint_uses_packed_qkv,
|
|
checkpoint_uses_comfy_quant,
|
|
getattr(result, "checkpoint_weight_scale_layout", "linear"),
|
|
getattr(result, "swap_weight_nibbles", False),
|
|
)
|
|
return result
|
|
except Exception as e:
|
|
logger.warning(
|
|
"Failed to build NVFP4 config from %s: %s",
|
|
", ".join(files_with_nvfp4_signal),
|
|
e,
|
|
)
|
|
return None
|
|
|
|
|
|
def build_nvfp4_config_from_safetensors(
|
|
file_path: str,
|
|
param_names_mapping_dict: Optional[dict] = None,
|
|
reverse_param_names_mapping_dict: Optional[dict] = None,
|
|
fallback_group_size: Optional[int] = None,
|
|
) -> Optional[QuantizationConfig]:
|
|
"""Backward-compatible wrapper for a single safetensors file."""
|
|
return _build_nvfp4_config_from_safetensors_files(
|
|
[file_path],
|
|
param_names_mapping_dict,
|
|
reverse_param_names_mapping_dict,
|
|
fallback_group_size,
|
|
)
|
|
|
|
|
|
def build_nvfp4_config_from_safetensors_list(
|
|
file_paths: list[str],
|
|
param_names_mapping_dict: Optional[dict] = None,
|
|
reverse_param_names_mapping_dict: Optional[dict] = None,
|
|
fallback_group_size: Optional[int] = None,
|
|
) -> Optional[QuantizationConfig]:
|
|
return _build_nvfp4_config_from_safetensors_files(
|
|
file_paths,
|
|
param_names_mapping_dict,
|
|
reverse_param_names_mapping_dict,
|
|
fallback_group_size,
|
|
)
|