[AMD][Quantization] Online MXFP4 quantization 4/N - NVFP4 to MXFP4 Online Requantization on AMD GPUs (#29328)
This commit is contained in:
@@ -6,6 +6,7 @@ import unittest
|
||||
from sglang.test.ci.ci_register import register_amd_ci
|
||||
|
||||
register_amd_ci(est_time=106, suite="stage-b-test-1-gpu-small-amd-mi35x")
|
||||
import os
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -87,20 +88,10 @@ class TestOnlineQuantizationMemoryLoad(CustomTestCase):
|
||||
raise RuntimeError(f"Server {url} failed to start in {timeout}s")
|
||||
time.sleep(1)
|
||||
|
||||
# # Extract and display peak GPU memory from logs
|
||||
combined_output = cls.stdout.getvalue() + cls.stderr.getvalue()
|
||||
|
||||
peak_memory_before_load = cls._extract_peak_memory_before_load(combined_output)
|
||||
if is_cuda_alike() and not peak_memory_before_load:
|
||||
raise ValueError("Should have found peak memory")
|
||||
cls.peak_memory_before_load = float(peak_memory_before_load)
|
||||
|
||||
memory_increase_load_weights = cls._extract_memory_increase_load_weights(
|
||||
combined_output
|
||||
)
|
||||
if is_cuda_alike() and not memory_increase_load_weights:
|
||||
raise ValueError("Should have found memory increase in load_weights")
|
||||
cls.memory_increase_load_weights = float(memory_increase_load_weights)
|
||||
# Keep the raw server for memory numbers, which are parsed lazily by
|
||||
# _test_peak_memory so subclasses that don't test memory (e.g. the
|
||||
# NVFP4->MXFP4 accuracy-only class) don't require these log lines.
|
||||
cls.combined_output = cls.stdout.getvalue() + cls.stderr.getvalue()
|
||||
|
||||
@classmethod
|
||||
def _extract_peak_memory_before_load(cls, log_output):
|
||||
@@ -115,8 +106,11 @@ class TestOnlineQuantizationMemoryLoad(CustomTestCase):
|
||||
@classmethod
|
||||
def _extract_memory_increase_load_weights(cls, log_output):
|
||||
"""Extract memory increase during load_weights call."""
|
||||
# Search for the log message pattern
|
||||
pattern = r"Memory increase during load_weights:\s+([\d.]+)\s+GiB"
|
||||
# Signed: the value is (free_before - free_after) around load_weights.
|
||||
# When the on-device source representation is larger than the loaded
|
||||
# result (e.g. requantizing to a more compact format), loading frees
|
||||
# net memory and the reported increase is negative.
|
||||
pattern = r"Memory increase during load_weights:\s+(-?[\d.]+)\s+GiB"
|
||||
match = re.search(pattern, log_output)
|
||||
if match:
|
||||
return match.group(1)
|
||||
@@ -138,21 +132,33 @@ class TestOnlineQuantizationMemoryLoad(CustomTestCase):
|
||||
if not is_cuda_alike():
|
||||
self.skipTest("not is_cuda_alike")
|
||||
|
||||
peak_memory_before_load = self._extract_peak_memory_before_load(
|
||||
self.combined_output
|
||||
)
|
||||
if not peak_memory_before_load:
|
||||
raise ValueError("Should have found peak memory")
|
||||
peak_memory_before_load = float(peak_memory_before_load)
|
||||
|
||||
memory_increase_load_weights = self._extract_memory_increase_load_weights(
|
||||
self.combined_output
|
||||
)
|
||||
if not memory_increase_load_weights:
|
||||
raise ValueError("Should have found memory increase in load_weights")
|
||||
memory_increase_load_weights = float(memory_increase_load_weights)
|
||||
|
||||
# NOTE: We can not simply rely on peak memory after `load_weights` as functions used
|
||||
# in-between (e.g. FP8->MXFP4 requantization) during weight loading may have a higher peak memory footprint
|
||||
# in-between (e.g. NVFP4->MXFP4 requantization) during weight loading may have a higher peak memory footprint
|
||||
# than simply the allocated weights.
|
||||
if add_peak_memory_before_load:
|
||||
reference_gib = (
|
||||
self.memory_increase_load_weights + self.peak_memory_before_load
|
||||
)
|
||||
reference_gib = memory_increase_load_weights + peak_memory_before_load
|
||||
else:
|
||||
reference_gib = self.memory_increase_load_weights
|
||||
reference_gib = memory_increase_load_weights
|
||||
|
||||
assert reference_gib < threshold
|
||||
|
||||
if test_start:
|
||||
# Weights initialized on meta device (not for dense BF16->MXFP4)
|
||||
assert self.peak_memory_before_load < 5
|
||||
assert peak_memory_before_load < 5
|
||||
|
||||
def _test_gsm8k(self, accuracy_threshold):
|
||||
"""Helper method to test GSM8K accuracy against a threshold."""
|
||||
@@ -205,6 +211,39 @@ class TestOnlineQuantizationMemoryLoadMOE(TestOnlineQuantizationMemoryLoad):
|
||||
self._test_gsm8k(accuracy_threshold=0.89)
|
||||
|
||||
|
||||
class TestNVFP4ToMXFP4MOETP1(TestOnlineQuantizationMemoryLoad):
|
||||
# ModelOpt NVFP4 export (quant_method="modelopt", quant_algo="NVFP4") =>
|
||||
# Nvfp4SourceConfig(). Exercises the NVFP4 -> MXFP4 MoE requantization path:
|
||||
# the per-expert dequantize_nvfp4 + dynamic_mxfp4_quant requant, and the w13
|
||||
# gate/up weight_scale_2 split in _requantize_nvfp4_to_mxfp4.
|
||||
model = "nvidia/Qwen3-30B-A3B-NVFP4" # NVFP4 model
|
||||
tp = 1
|
||||
|
||||
def test_gsm8k(self):
|
||||
# Requantized NVFP4 -> MXFP4 observed accuracy: ~0.88
|
||||
# (BF16 Qwen/Qwen3-30B-A3B reference: ~0.94).
|
||||
self._test_gsm8k(accuracy_threshold=0.85)
|
||||
|
||||
|
||||
@unittest.skipIf(is_in_ci(), "local test only")
|
||||
class TestDeepSeekR10528NVFP4ToMXFP4(TestOnlineQuantizationMemoryLoad):
|
||||
# NVFP4 to MXFP4 online requantization for DeepSeek-R1-0528-NVFP4 on TP=8.
|
||||
# Exercises the MLA attention path (attention_backend=aiter), multi-threaded
|
||||
# weight loading, and the per-expert NVFP4 MoE requantization path.
|
||||
model = "nvidia/DeepSeek-R1-0528-NVFP4" # NVFP4 model
|
||||
tp = 8
|
||||
runner_args = [
|
||||
"--attention-backend",
|
||||
"aiter",
|
||||
"--model-loader-extra-config",
|
||||
'{"enable_multithread_load": true}',
|
||||
]
|
||||
|
||||
def test_gsm8k(self):
|
||||
# Requantized NVFP4 -> MXFP4 observed accuracy: ~0.95.
|
||||
self._test_gsm8k(accuracy_threshold=0.90)
|
||||
|
||||
|
||||
class TestFP8ToMXFP4DenseTP1(TestOnlineQuantizationMemoryLoad):
|
||||
tp = 1
|
||||
model = "Qwen/Qwen3-8B-FP8"
|
||||
|
||||
@@ -7,7 +7,15 @@ register_cpu_ci(est_time=5, suite="base-a-test-cpu")
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from sglang.srt.layers.quantization.quark.quark import QuarkConfig
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.quantization.quark.quark import (
|
||||
QuarkConfig,
|
||||
_build_mixed_precision_layer_quant_config,
|
||||
_mixed_precision_layer_map,
|
||||
_parse_nvfp4_excludes,
|
||||
)
|
||||
from sglang.srt.layers.quantization.quark.utils import check_equal_or_regex_match
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
_GET_CAP = "sglang.srt.layers.quantization.quark.quark.get_device_capability"
|
||||
@@ -83,5 +91,121 @@ class TestCheckSchemeSupportedError(CustomTestCase):
|
||||
self.assertFalse(ok)
|
||||
|
||||
|
||||
class TestMixedPrecisionLayerConfig(CustomTestCase):
|
||||
"""NVFP4-only-experts + FP8-elsewhere online requant (quark_mxfp4).
|
||||
|
||||
A MIXED_PRECISION NVFP4 checkpoint (e.g. nvidia/Qwen3.5-397B-A17B-NVFP4-V2)
|
||||
keeps some layers in NVFP4 while others in FP8. Online requant must send
|
||||
only the NVFP4 layers through the dequant->MXFP4 path and load the FP8 layers
|
||||
as FP8.
|
||||
"""
|
||||
|
||||
_LAYER_MAP_SRC = {
|
||||
"quant_algo": "MIXED_PRECISION",
|
||||
"quantized_layers": {
|
||||
"model.language_model.layers.0.self_attn.q_proj": {"quant_algo": "FP8"},
|
||||
"model.language_model.layers.0.self_attn.k_proj": {"quant_algo": "FP8"},
|
||||
"model.language_model.layers.0.self_attn.v_proj": {"quant_algo": "FP8"},
|
||||
"model.language_model.layers.0.self_attn.o_proj": {"quant_algo": "FP8"},
|
||||
"model.language_model.layers.0.mlp.shared_expert.gate_proj": {
|
||||
"quant_algo": "FP8"
|
||||
},
|
||||
"model.language_model.layers.0.mlp.shared_expert.down_proj": {
|
||||
"quant_algo": "FP8"
|
||||
},
|
||||
"model.language_model.layers.0.mlp.experts": {
|
||||
"quant_algo": "NVFP4",
|
||||
"group_size": 16,
|
||||
},
|
||||
"model.language_model.layers.1.mlp.experts": {
|
||||
"quant_algo": "NVFP4",
|
||||
"group_size": 16,
|
||||
},
|
||||
"model.language_model.layers.1.self_attn.q_proj": {"quant_algo": "FP8"},
|
||||
},
|
||||
}
|
||||
|
||||
def _build_bare_config(self) -> QuarkConfig:
|
||||
layer_map = _mixed_precision_layer_map(self._LAYER_MAP_SRC)
|
||||
layer_quant_config, has_nvfp4 = _build_mixed_precision_layer_quant_config(
|
||||
layer_map
|
||||
)
|
||||
self.assertTrue(has_nvfp4)
|
||||
synth_config = QuarkConfig._create_online_mxfp4_config(
|
||||
model_type="qwen3_5_moe",
|
||||
layer_quant_config=layer_quant_config,
|
||||
)
|
||||
synth_config["packed_modules_mapping"] = {
|
||||
"qkv_proj": ["q_proj", "k_proj", "v_proj"],
|
||||
}
|
||||
quark_config = _bare_config()
|
||||
quark_config.quant_config = synth_config
|
||||
quark_config.packed_modules_mapping = synth_config["packed_modules_mapping"]
|
||||
quark_config.exclude_layers = synth_config["exclude"]
|
||||
return quark_config
|
||||
|
||||
def test_experts_route_to_mxfp4_requant(self):
|
||||
# fnmatch keys (not `re:`) must match the sglang module path so experts
|
||||
# hit the fp4 target, not fall through to the global config
|
||||
quark_config = self._build_bare_config()
|
||||
matched = quark_config._find_matched_config(
|
||||
"model.layers.0.mlp.experts", torch.nn.Module()
|
||||
)
|
||||
self.assertEqual(matched["weight"]["dtype"], "fp4")
|
||||
self.assertEqual(matched["weight"]["group_size"], 32)
|
||||
|
||||
def test_fp8_layers_not_requantized(self):
|
||||
quark_config = self._build_bare_config()
|
||||
for name in (
|
||||
"model.layers.0.self_attn.o_proj",
|
||||
"model.layers.0.mlp.shared_expert.gate_proj",
|
||||
"model.layers.0.mlp.shared_expert.down_proj",
|
||||
):
|
||||
matched = quark_config._find_matched_config(name, torch.nn.Module())
|
||||
self.assertEqual(matched["weight"]["dtype"], "fp8_e4m3", msg=name)
|
||||
self.assertEqual(matched["weight"]["qscheme"], "per_tensor", msg=name)
|
||||
|
||||
def test_fused_qkv_shards_share_fp8_scheme(self):
|
||||
# _find_matched_config expands qkv_proj -> q/k/v shards and requires a
|
||||
# consistent scheme; all three are FP8 so this must resolve
|
||||
quark_config = self._build_bare_config()
|
||||
matched = quark_config._find_matched_config(
|
||||
"model.layers.0.self_attn.qkv_proj", torch.nn.Module()
|
||||
)
|
||||
self.assertEqual(matched["weight"]["dtype"], "fp8_e4m3")
|
||||
|
||||
def test_shared_expert_fusion_disabled_on_precision_mismatch(self):
|
||||
quark_config = self._build_bare_config()
|
||||
self.assertFalse(quark_config.can_fuse_shared_expert())
|
||||
|
||||
def test_mixed_precision_skips_model_type_default_excludes(self):
|
||||
quark_config = self._build_bare_config()
|
||||
self.assertNotIn("re:.*shared_expert", quark_config.exclude_layers)
|
||||
self.assertNotIn("re:.*o_proj", quark_config.exclude_layers)
|
||||
|
||||
def test_non_mixed_config_returns_none(self):
|
||||
self.assertIsNone(_mixed_precision_layer_map({"quant_algo": "NVFP4"}))
|
||||
|
||||
|
||||
class TestParseNvfp4Excludes(CustomTestCase):
|
||||
"""ModelOpt `ignore` lists mix `re:`-prefixed regexes with fnmatch globs."""
|
||||
|
||||
def test_already_regex_entries_pass_through_and_match(self):
|
||||
# wrapping an already-`re:` entry with another `re:` +
|
||||
# fnmatch.translate produced `re:(?s:re:\\..*...)` which never matches,
|
||||
excludes = _parse_nvfp4_excludes(
|
||||
{"ignore": [r"re:.*linear_attn\.in_proj_a$", "mtp*"]}
|
||||
)
|
||||
self.assertTrue(
|
||||
check_equal_or_regex_match("model.layers.0.linear_attn.in_proj_a", excludes)
|
||||
)
|
||||
# fnmatch glob still translated and matches.
|
||||
self.assertTrue(check_equal_or_regex_match("mtp.layers.0.foo", excludes))
|
||||
# A quantized layer stays un-excluded.
|
||||
self.assertFalse(
|
||||
check_equal_or_regex_match("model.layers.0.mlp.experts", excludes)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user