[Test] Route GEMM backend UTs through real layer modules and weight loaders (#33615)

This commit is contained in:
Liangsheng Yin
2026-08-04 20:53:26 -07:00
committed by GitHub
parent 1033cae8d5
commit 198a3bc29b
11 changed files with 368 additions and 524 deletions
+74
View File
@@ -0,0 +1,74 @@
"""Shared fixture plumbing for layer-level backend parity UTs.
Hand-written quantization references (oracle side) live in quant_ref_utils.
"""
import os
import torch
def init_single_process_dist(master_port: int = 29632, backend: str = "gloo"):
"""world=1 dist + model-parallel groups; srt layers require them even
at tp=1."""
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
os.environ.setdefault("MASTER_PORT", str(master_port))
os.environ.setdefault("RANK", "0")
os.environ.setdefault("WORLD_SIZE", "1")
os.environ.setdefault("LOCAL_RANK", "0")
from sglang.srt.distributed.parallel_state import (
init_distributed_environment,
initialize_model_parallel,
model_parallel_is_initialized,
)
if not torch.distributed.is_initialized():
init_distributed_environment(
world_size=1, rank=0, local_rank=0, backend=backend
)
if not model_parallel_is_initialized():
# kwargs only: a positional backend would land in the
# attention_data_parallel_size slot and explode on int // str.
initialize_model_parallel(
tensor_model_parallel_size=1,
expert_model_parallel_size=1,
pipeline_model_parallel_size=1,
backend=backend,
)
def make_tp1_column_parallel_linear(
quant_config, n: int, k: int, prefix: str = "model.layers.0.mlp.up_proj", **kwargs
):
from sglang.srt.layers.linear import ColumnParallelLinear
return ColumnParallelLinear(
input_size=k,
output_size=n,
bias=False,
params_dtype=torch.bfloat16,
quant_config=quant_config,
prefix=prefix,
tp_rank=0,
tp_size=1,
**kwargs,
).cuda()
def load_linear_weights(layer, shard_id=None, **named_weights):
"""Feed checkpoint-format tensors through the real weight_loader."""
for name, loaded in named_weights.items():
if shard_id is None:
layer.weight_loader_v2(getattr(layer, name), loaded)
else:
layer.weight_loader_v2(getattr(layer, name), loaded, shard_id)
def assert_output_close(tc, out, ref, cos_threshold=0.99, rtol=None, atol=None):
tc.assertEqual(tuple(out.shape), tuple(ref.shape))
cos = torch.nn.functional.cosine_similarity(
out.float().flatten(), ref.flatten(), dim=0
).item()
tc.assertGreater(cos, cos_threshold)
if rtol is not None:
torch.testing.assert_close(out.float(), ref, rtol=rtol, atol=atol)
+70
View File
@@ -0,0 +1,70 @@
"""Hand-written quantization-format references for backend parity UTs.
Deliberately independent of sglang.srt -- never replace with srt imports;
the tests use these to check srt.
"""
import torch
FLOAT8_E4M3_MAX = 448.0
FLOAT4_E2M1_MAX = 6.0
kE2M1ToFloat = torch.tensor(
[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=torch.float32
)
def convert_swizzled_to_linear(a_sf_swizzled: torch.Tensor, m, k, block_size=16):
m_tiles = (m + 128 - 1) // 128
f = block_size * 4
k_tiles = (k + f - 1) // f
tmp = torch.reshape(a_sf_swizzled, (1, m_tiles, k_tiles, 32, 4, 4))
tmp = torch.permute(tmp, (0, 1, 4, 3, 2, 5))
out = tmp.reshape(m_tiles * 128, k_tiles * f // block_size)
# Crop the K-tile padding too: k // block_size scale columns, not k.
return out[0:m, 0 : k // block_size]
def break_fp4_bytes(a, dtype=torch.float32):
assert a.dtype == torch.uint8
m, n = a.shape
a_flat = a.flatten()
high = (a_flat & 0xF0) >> 4
low = a_flat & 0x0F
combined = torch.stack((low, high), dim=1).flatten()
signs = (combined & 0x08).to(torch.bool)
abs_vals = (combined & 0x07).to(torch.long)
kE2M1 = kE2M1ToFloat.to(device=a.device)
values = kE2M1[abs_vals] * torch.where(signs, -1.0, 1.0)
return values.reshape(m, n * 2).to(dtype=dtype)
def dequantize_nvfp4_to_dtype(
tensor_fp4, tensor_sf, global_scale, dtype, block_size=16
):
assert tensor_fp4.dtype == torch.uint8
m, packed_k = tensor_fp4.shape
k = packed_k * 2
tensor_f32 = break_fp4_bytes(tensor_fp4, torch.float32)
tensor_f32 = tensor_f32.reshape(m, k // block_size, block_size)
tensor_sf = tensor_sf.view(torch.float8_e4m3fn)
tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
tensor_sf_dtype = tensor_sf.to(torch.float32) / global_scale
out = (tensor_f32 * tensor_sf_dtype.unsqueeze(-1)).reshape(m, k)
return out.to(dtype=dtype)
def quantize_nvfp4_shard(w: torch.Tensor, gs=None):
"""NVFP4-quantize one checkpoint shard; returns (packed, linear sf,
global scale, fp32 dequant reference)."""
from flashinfer import fp4_quantize
n, k = w.shape
if gs is None:
gs = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / w.abs().max().to(torch.float32)
w_q, w_sf_swizzled = fp4_quantize(w, gs)
sf_linear = convert_swizzled_to_linear(
w_sf_swizzled.view(torch.float8_e4m3fn), n, k, 16
)
w_dequant = dequantize_nvfp4_to_dtype(w_q, w_sf_swizzled, gs, torch.float32)
return w_q, sf_linear, gs, w_dequant
@@ -6,17 +6,14 @@ from torch import nn
from sglang.srt.debug_utils.tensor_dump_forward_hook import (
register_forward_hook_for_model,
)
from sglang.srt.distributed.parallel_state import (
get_default_distributed_backend,
init_distributed_environment,
initialize_model_parallel,
)
from sglang.srt.distributed.parallel_state import get_default_distributed_backend
from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.layers.linear import LinearBase
from sglang.srt.models.qwen2 import Qwen2MLP
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
from sglang.srt.utils import add_prefix, get_device
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.layer_ut_utils import init_single_process_dist
register_cuda_ci(
est_time=9,
@@ -80,15 +77,7 @@ def init_weights(module):
def test_model_forward_dump(tmp_path):
set_global_server_args_for_scheduler(ServerArgs(model_path="dummy"))
device = get_device()
backend = get_default_distributed_backend(device)
init_distributed_environment(
backend=backend,
world_size=1,
rank=0,
local_rank=0,
distributed_init_method="tcp://127.0.0.1:2646",
)
initialize_model_parallel()
init_single_process_dist(backend=get_default_distributed_backend(device))
model = MockCausalLM()
model.apply(init_weights)
model = model.to(device=device, dtype=torch.bfloat16)
@@ -10,6 +10,11 @@ from torch.nn import functional as F
from sglang.srt.layers.moe.topk import TopKConfig, select_experts
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.quant_ref_utils import (
FLOAT4_E2M1_MAX,
FLOAT8_E4M3_MAX,
dequantize_nvfp4_to_dtype,
)
register_cuda_ci(est_time=300, suite="nightly-4-gpu-b200", nightly=True)
@@ -19,66 +24,6 @@ if torch.cuda.get_device_capability() < (10, 0):
allow_module_level=True,
)
kE2M1ToFloat = torch.tensor(
[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=torch.float32
)
FLOAT8_E4M3_MAX = 448.0
FLOAT4_E2M1_MAX = 6.0
def convert_swizzled_to_linear(a_sf_swizzled: torch.Tensor, m, k, block_size):
m_tiles = (m + 128 - 1) // 128
f = block_size * 4
k_tiles = (k + f - 1) // f
tmp = torch.reshape(a_sf_swizzled, (1, m_tiles, k_tiles, 32, 4, 4))
tmp = torch.permute(tmp, (0, 1, 4, 3, 2, 5))
out = tmp.reshape(m_tiles * 128, k_tiles * f // block_size)
return out[0:m, 0:k]
def dequantize_nvfp4_to_dtype(
tensor_fp4, tensor_sf, global_scale, dtype, device, block_size=16
):
"""Dequantize the fp4 tensor back to high precision."""
# Two fp4 values are packed into one uint8.
assert tensor_fp4.dtype == torch.uint8
m, packed_k = tensor_fp4.shape
k = packed_k * 2
tensor_f32 = break_fp4_bytes(tensor_fp4, dtype)
tensor_f32 = tensor_f32.reshape(m, k // block_size, block_size)
tensor_sf = tensor_sf.view(torch.float8_e4m3fn)
tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
tensor_sf_dtype = tensor_sf.to(torch.float32) / global_scale
# scale the tensor
out = (tensor_f32 * tensor_sf_dtype.unsqueeze(-1)).reshape(m, k)
return out.to(dtype=dtype)
def break_fp4_bytes(a, dtype):
assert a.dtype == torch.uint8
m, n = a.shape
# Vectorized nibble processing
a_flat = a.flatten()
high = (a_flat & 0xF0) >> 4 # Upper nibbles
low = a_flat & 0x0F # Lower nibbles
# Combine nibbles for batch processing
combined = torch.stack((low, high), dim=1).flatten()
# Vectorized sign and magnitude extraction
signs = (combined & 0x08).to(torch.bool) # Sign bits
abs_vals = (combined & 0x07).to(torch.long) # Magnitude indices
# Device-aware lookup and sign application
kE2M1 = kE2M1ToFloat.to(device=a.device)
values = kE2M1[abs_vals] * torch.where(signs, -1.0, 1.0)
# Reshape to final form
return values.reshape(m, n * 2).to(dtype=dtype)
def compute_routing(router_logits: torch.Tensor, top_k: int):
routing_weights = torch.softmax(router_logits, dim=1, dtype=torch.float)
@@ -170,7 +115,6 @@ def torch_moe_nvfp4(a, w1, w2, topk, topk_weight, topk_ids):
inter_blockscale,
inter_gs,
dtype=inter.dtype,
device=inter.device,
block_size=16,
).cuda()
out[mask] = inter @ w2[i].transpose(0, 1)
@@ -319,7 +263,6 @@ def check_moe(
a_scale_interleaved,
a_global_scale,
dtype=a.dtype,
device=a.device,
block_size=quant_blocksize,
)
@@ -332,7 +275,6 @@ def check_moe(
w1_blockscale[idx],
w1_gs[idx],
dtype=w1.dtype,
device=w1.device,
block_size=quant_blocksize,
)
w2_d[idx] = dequantize_nvfp4_to_dtype(
@@ -340,7 +282,6 @@ def check_moe(
w2_blockscale[idx],
w2_gs[idx],
dtype=w2.dtype,
device=w2.device,
block_size=quant_blocksize,
)
+5 -70
View File
@@ -8,6 +8,11 @@ from torch.nn import functional as F
from sglang.srt.layers.activation import SiluAndMul
from sglang.srt.layers.moe.flashinfer_cutedsl_moe import flashinfer_cutedsl_moe_masked
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.quant_ref_utils import (
FLOAT4_E2M1_MAX,
FLOAT8_E4M3_MAX,
dequantize_nvfp4_to_dtype,
)
try:
from flashinfer import CuteDslMoEWrapper
@@ -21,66 +26,6 @@ register_cuda_ci(est_time=24, stage="extra-b", runner_config="4-gpu-b200")
SKIP_TEST = torch.cuda.get_device_capability() < (10, 0)
SKIP_REASON = "Nvfp4 Requires compute capability of 10 or above."
kE2M1ToFloat = torch.tensor(
[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=torch.float32
)
FLOAT8_E4M3_MAX = 448.0
FLOAT4_E2M1_MAX = 6.0
def convert_swizzled_to_linear(a_sf_swizzled: torch.Tensor, m, k, block_size):
m_tiles = (m + 128 - 1) // 128
f = block_size * 4
k_tiles = (k + f - 1) // f
tmp = torch.reshape(a_sf_swizzled, (1, m_tiles, k_tiles, 32, 4, 4))
tmp = torch.permute(tmp, (0, 1, 4, 3, 2, 5))
out = tmp.reshape(m_tiles * 128, k_tiles * f // block_size)
return out[0:m, 0:k]
def dequantize_nvfp4_to_dtype(
tensor_fp4, tensor_sf, global_scale, dtype, device, block_size=16
):
"""Dequantize the fp4 tensor back to high precision."""
# Two fp4 values are packed into one uint8.
assert tensor_fp4.dtype == torch.uint8
m, packed_k = tensor_fp4.shape
k = packed_k * 2
tensor_f32 = break_fp4_bytes(tensor_fp4, dtype)
tensor_f32 = tensor_f32.reshape(m, k // block_size, block_size)
tensor_sf = tensor_sf.view(torch.float8_e4m3fn)
tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
tensor_sf_dtype = tensor_sf.to(torch.float32) / global_scale
# scale the tensor
out = (tensor_f32 * tensor_sf_dtype.unsqueeze(-1)).reshape(m, k)
return out.to(dtype=dtype)
def break_fp4_bytes(a, dtype):
assert a.dtype == torch.uint8
m, n = a.shape
# Vectorized nibble processing
a_flat = a.flatten()
high = (a_flat & 0xF0) >> 4 # Upper nibbles
low = a_flat & 0x0F # Lower nibbles
# Combine nibbles for batch processing
combined = torch.stack((low, high), dim=1).flatten()
# Vectorized sign and magnitude extraction
signs = (combined & 0x08).to(torch.bool) # Sign bits
abs_vals = (combined & 0x07).to(torch.long) # Magnitude indices
# Device-aware lookup and sign application
kE2M1 = kE2M1ToFloat.to(device=a.device)
values = kE2M1[abs_vals] * torch.where(signs, -1.0, 1.0)
# Reshape to final form
return values.reshape(m, n * 2).to(dtype=dtype)
def _interleave_w13_halves(
x: torch.Tensor, group_size: int = 64, dim: int = -1
@@ -464,7 +409,6 @@ def torch_moe_nvfp4(a, w1, w2, topk, topk_weight, topk_ids):
inter_blockscale,
inter_gs,
dtype=inter.dtype,
device=inter.device,
block_size=16,
).cuda()
out[mask] = inter @ w2[i].transpose(0, 1)
@@ -916,7 +860,6 @@ class TestCuteDslV1(unittest.TestCase):
a_scale_interleaved,
a_global_scale,
dtype=hidden_states.dtype,
device=hidden_states.device,
block_size=16,
)
w1_d = torch.empty(
@@ -942,7 +885,6 @@ class TestCuteDslV1(unittest.TestCase):
w1_blockscale_sliced,
w1_global_scale[idx],
dtype=w1.dtype,
device=w1.device,
block_size=16,
)
w2_d[idx] = dequantize_nvfp4_to_dtype(
@@ -950,7 +892,6 @@ class TestCuteDslV1(unittest.TestCase):
w2_blockscale_sliced,
w2_global_scale[idx],
dtype=w2.dtype,
device=w2.device,
block_size=16,
)
@@ -1093,7 +1034,6 @@ class TestCuteDslV1(unittest.TestCase):
a_scale_interleaved,
a_global_scale,
dtype=hidden_states.dtype,
device=device,
block_size=16,
)
w1_d = torch.empty(
@@ -1117,7 +1057,6 @@ class TestCuteDslV1(unittest.TestCase):
w1_blockscale_sliced,
w1_global_scale[idx],
dtype=w1.dtype,
device=device,
block_size=16,
)
w2_d[idx] = dequantize_nvfp4_to_dtype(
@@ -1125,7 +1064,6 @@ class TestCuteDslV1(unittest.TestCase):
w2_blockscale_sliced,
w2_global_scale[idx],
dtype=w2.dtype,
device=device,
block_size=16,
)
@@ -1268,7 +1206,6 @@ class TestCuteDslV1(unittest.TestCase):
a_scale,
a_gs,
dtype=torch.bfloat16,
device=device,
block_size=16,
)
w1_d = torch.empty(
@@ -1287,7 +1224,6 @@ class TestCuteDslV1(unittest.TestCase):
w1_blockscale_sliced,
w1_gs[idx],
dtype=w1.dtype,
device=device,
block_size=16,
)
w2_d[idx] = dequantize_nvfp4_to_dtype(
@@ -1295,7 +1231,6 @@ class TestCuteDslV1(unittest.TestCase):
w2_blockscale_sliced,
w2_gs[idx],
dtype=w2.dtype,
device=device,
block_size=16,
)
ref = torch_moe_nvfp4(
+3 -23
View File
@@ -6,16 +6,10 @@ FP8 quantized weights. Skipped when HPC-Ops (https://github.com/Tencent/hpc-ops)
is not installed or the GPU is not SM90 (the kernels ship sm90a only).
"""
import os
import unittest
import torch
from sglang.srt.distributed.parallel_state import (
init_distributed_environment,
initialize_model_parallel,
model_parallel_is_initialized,
)
from sglang.srt.layers.moe.moe_runner import MoeRunnerConfig
from sglang.srt.layers.moe.moe_runner.hpc_ops import (
HpcOpsMoeQuantInfo,
@@ -27,6 +21,7 @@ from sglang.srt.layers.moe.token_dispatcher.standard import StandardDispatchOutp
from sglang.srt.layers.moe.topk import StandardTopKOutput
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.layer_ut_utils import init_single_process_dist
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=60, stage="base-b", runner_config="1-gpu-large")
@@ -43,26 +38,11 @@ def _sm90() -> bool:
def _ensure_dist_initialized() -> None:
"""Single-rank gloo distributed + model-parallel groups (TP=1, EP=1).
The triton fused_experts reference allocates its output under
"""The triton fused_experts reference allocates its output under
``use_symmetric_memory(get_tp_group(), ...)``, which requires the TP
group even when symmetric allocation is disabled.
"""
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
os.environ.setdefault("MASTER_PORT", "29633")
os.environ.setdefault("RANK", "0")
os.environ.setdefault("WORLD_SIZE", "1")
os.environ.setdefault("LOCAL_RANK", "0")
if not torch.distributed.is_initialized():
init_distributed_environment(world_size=1, rank=0, local_rank=0, backend="gloo")
if not model_parallel_is_initialized():
initialize_model_parallel(
tensor_model_parallel_size=1,
expert_model_parallel_size=1,
pipeline_model_parallel_size=1,
backend="gloo",
)
init_single_process_dist(master_port=29633)
def _quant_blockwise(w: torch.Tensor, block: int = 128):
@@ -21,28 +21,14 @@ def check_quant_method(model_path: str, use_marlin_kernel: bool):
from sglang.srt.configs.device_config import DeviceConfig
from sglang.srt.configs.load_config import LoadConfig
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.distributed import (
init_distributed_environment,
initialize_model_parallel,
)
from sglang.srt.distributed.parallel_state import monkey_patch_vllm_parallel_state
from sglang.srt.layers.quantization.utils import get_dynamic_override
from sglang.srt.model_loader import get_model
from sglang.srt.server_args import ServerArgs
from sglang.test.layer_ut_utils import init_single_process_dist
try:
init_distributed_environment(
backend="nccl",
world_size=1,
rank=0,
local_rank=0,
distributed_init_method="tcp://127.0.0.1:2646",
)
initialize_model_parallel(tensor_model_parallel_size=1)
monkey_patch_vllm_parallel_state()
except AssertionError:
# ignore this error: tensor model parallel group is already initialized
pass
init_single_process_dist(backend="nccl")
monkey_patch_vllm_parallel_state()
server_args = ServerArgs(model_path=model_path, dtype=torch.float16)
set_global_server_args_for_scheduler(server_args)
@@ -1,13 +1,8 @@
"""Numerics for the FP8 dense-linear GEMM backends (--fp8-gemm-backend).
Runs the quant-method layer path (create_weights ->
process_weights_after_loading -> apply) against a dequantized-reference
matmul, covering the per-backend weight preparation (e.g. UE8M0 scale requant
for DeepGEMM, per-backend MXFP8 scale packing) and the GEMM dispatch.
Three formats: FP8 blockwise (Fp8LinearMethod), MXFP8 (Fp8LinearMethod with
use_mxfp8), and per-tensor FP8 (ModelOptFp8LinearMethod, auto dispatch).
The backend set adapts to the device SM version, so the same file covers
Hopper (SM90), B200-class (SM100/103), and consumer Blackwell (SM120).
Real layer path vs a dequantized-reference matmul, in three formats: FP8
blockwise, MXFP8, and per-tensor FP8 (auto dispatch). Backend sets adapt to
the device SM, so one file covers SM90 / SM100 / SM120.
"""
import unittest
@@ -16,14 +11,17 @@ from unittest import mock
import torch
from sglang.srt.layers.quantization import fp8_utils
from sglang.srt.layers.quantization.fp8 import Fp8Config, Fp8LinearMethod
from sglang.srt.layers.quantization.fp8 import Fp8Config
from sglang.srt.layers.quantization.fp8_utils import Fp8GemmRunnerBackend
from sglang.srt.layers.quantization.modelopt_quant import (
ModelOptFp8Config,
ModelOptFp8LinearMethod,
)
from sglang.srt.layers.quantization.modelopt_quant import ModelOptFp8Config
from sglang.srt.utils import get_device_sm
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.layer_ut_utils import (
assert_output_close,
init_single_process_dist,
load_linear_weights,
make_tp1_column_parallel_linear,
)
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=120, stage="base-b", runner_config="4-gpu-b200")
@@ -99,26 +97,17 @@ def _quantize_mxfp8(w: torch.Tensor, block: int = 32):
return w_fp8.reshape(n, k), scale_e8m0, w_dequant
def _create_weights(method, n: int, k: int, device: str = "cuda"):
layer = torch.nn.Module()
kwargs = {}
if isinstance(method, Fp8LinearMethod):
# The shape check reads TP world size (needs distributed init); skip it here.
kwargs["skip_block_quant_check"] = True
method.create_weights(
layer,
input_size_per_partition=k,
output_partition_sizes=[n],
input_size=k,
output_size=n,
params_dtype=torch.bfloat16,
weight_loader=lambda *args, **kw: None,
**kwargs,
def _make_linear(quant_config, n: int, k: int):
return make_tp1_column_parallel_linear(
quant_config, n, k, skip_block_quant_check=True
)
return layer.to(device)
class _LinearBackendCheck(CustomTestCase):
@classmethod
def setUpClass(cls):
init_single_process_dist()
def _check_backend(self, backend: str, allowed, shapes, build_layer):
if backend not in allowed:
self.skipTest(f"{backend} not in SM{get_device_sm()} backend set")
@@ -130,21 +119,16 @@ class _LinearBackendCheck(CustomTestCase):
"FP8_GEMM_RUNNER_BACKEND",
Fp8GemmRunnerBackend(backend),
):
method, layer, w_dequant = build_layer(n, k)
method.process_weights_after_loading(layer)
layer, w_dequant = build_layer(n, k)
layer.quant_method.process_weights_after_loading(layer)
x = torch.randn((m, k), device="cuda", dtype=torch.bfloat16) / 10
out = method.apply(layer, x)
out, _ = layer(x)
ref = x.float() @ w_dequant.T
self.assertEqual(out.shape, (m, n))
cos = torch.nn.functional.cosine_similarity(
out.float().flatten(), ref.flatten(), dim=0
).item()
self.assertGreater(cos, 0.99)
# atol covers single-element UE8M0 scale-rounding outliers
# (deep_gemm); a wrong kernel/layout fails by orders more.
torch.testing.assert_close(out.float(), ref, rtol=5e-2, atol=1e-1)
assert_output_close(self, out, ref, rtol=5e-2, atol=1e-1)
@unittest.skipIf(get_device_sm() < 90, "FP8 GEMM backends require SM90+")
@@ -156,13 +140,11 @@ class TestFp8BlockwiseLinearBackends(_LinearBackendCheck):
activation_scheme="dynamic",
weight_block_size=[128, 128],
)
method = Fp8LinearMethod(quant_config)
layer = _create_weights(method, n, k)
layer = _make_linear(quant_config, n, k)
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
w_fp8, scale_inv, w_dequant = _quantize_fp8_blockwise(w)
layer.weight.data.copy_(w_fp8)
layer.weight_scale_inv.data.copy_(scale_inv)
return method, layer, w_dequant
load_linear_weights(layer, weight=w_fp8, weight_scale_inv=scale_inv)
return layer, w_dequant
def _run(self, backend: str):
self._check_backend(
@@ -197,13 +179,11 @@ class TestMxfp8LinearBackends(_LinearBackendCheck):
activation_scheme="dynamic",
use_mxfp8=True,
)
method = Fp8LinearMethod(quant_config)
layer = _create_weights(method, n, k)
layer = _make_linear(quant_config, n, k)
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
w_fp8, scale_e8m0, w_dequant = _quantize_mxfp8(w)
layer.weight.data.copy_(w_fp8)
layer.weight_scale_inv.data.copy_(scale_e8m0)
return method, layer, w_dequant
load_linear_weights(layer, weight=w_fp8, weight_scale_inv=scale_e8m0)
return layer, w_dequant
def _run(self, backend: str):
self._check_backend(backend, _mxfp8_backends(), MXFP8_SHAPES, self._build_layer)
@@ -225,17 +205,22 @@ class TestModeloptFp8PerTensorLinear(_LinearBackendCheck):
@staticmethod
def _build_layer(n: int, k: int):
quant_config = ModelOptFp8Config(is_checkpoint_fp8_serialized=True)
method = ModelOptFp8LinearMethod(quant_config)
layer = _create_weights(method, n, k)
quant_config = ModelOptFp8Config(
is_checkpoint_fp8_serialized=True, packed_modules_mapping={}
)
layer = _make_linear(quant_config, n, k)
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
scale = (w.float().abs().max() / FP8_MAX).clamp(min=1e-12)
w_fp8 = (w.float() / scale).to(torch.float8_e4m3fn)
layer.weight.data.copy_(w_fp8)
layer.weight_scale.data.fill_(scale)
layer.input_scale.data.fill_(1.0 / FP8_MAX)
# 0-dim scales exercise weight_loader_v2's scalar reshape branch.
load_linear_weights(
layer,
weight=w_fp8,
weight_scale=scale,
input_scale=torch.tensor(1.0 / FP8_MAX, device="cuda"),
)
w_dequant = w_fp8.float() * scale
return method, layer, w_dequant
return layer, w_dequant
def test_auto(self):
self._check_backend("auto", ["auto"], PER_TENSOR_SHAPES, self._build_layer)
@@ -1,10 +1,8 @@
"""Numerics for the NVFP4 dense-linear GEMM backends (--fp4-gemm-backend).
Runs ModelOptFp4LinearMethod end to end (create_weights ->
process_weights_after_loading -> apply) for each SM100 backend choice and
checks the output against a dequantized-reference matmul. This covers both
the per-backend weight preparation (padding / interleave / TRTLLM shuffle)
and the GEMM kernel dispatch.
Real layer path (ColumnParallelLinear -> weight_loader -> weight processing
-> forward) per SM100 backend vs a dequantized-reference matmul; a merged
two-shard case guards the per-partition scale gathering of fused layers.
"""
import unittest
@@ -15,22 +13,25 @@ from flashinfer import fp4_quantize
from sglang.srt.layers.quantization import fp4_utils
from sglang.srt.layers.quantization.fp4_utils import Fp4GemmRunnerBackend
from sglang.srt.layers.quantization.modelopt_quant import (
ModelOptFp4Config,
ModelOptFp4LinearMethod,
)
from sglang.srt.layers.quantization.modelopt_quant import ModelOptFp4Config
from sglang.srt.utils import get_device_sm
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.layer_ut_utils import (
assert_output_close,
init_single_process_dist,
load_linear_weights,
make_tp1_column_parallel_linear,
)
from sglang.test.quant_ref_utils import (
FLOAT4_E2M1_MAX,
FLOAT8_E4M3_MAX,
dequantize_nvfp4_to_dtype,
quantize_nvfp4_shard,
)
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=120, stage="base-b", runner_config="4-gpu-b200")
kE2M1ToFloat = torch.tensor(
[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=torch.float32
)
FLOAT8_E4M3_MAX = 448.0
FLOAT4_E2M1_MAX = 6.0
# (M, N, K). The second shape hits the padding paths: N=160 is not a multiple
# of 128 (TRTLLM shuffle pad) and K=336 is neither a multiple of 32 (CUTLASS
# K pad) nor K/16 a multiple of 4 (TRTLLM scale pad).
@@ -40,96 +41,88 @@ SHAPES = [
(128, 1024, 1024),
]
BACKENDS = [
"flashinfer_cutedsl",
"flashinfer_cutlass",
"flashinfer_cudnn",
"flashinfer_trtllm",
]
ACT_SCALE = 1.0 / (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX)
def convert_swizzled_to_linear(a_sf_swizzled: torch.Tensor, m, k, block_size):
m_tiles = (m + 128 - 1) // 128
f = block_size * 4
k_tiles = (k + f - 1) // f
tmp = torch.reshape(a_sf_swizzled, (1, m_tiles, k_tiles, 32, 4, 4))
tmp = torch.permute(tmp, (0, 1, 4, 3, 2, 5))
out = tmp.reshape(m_tiles * 128, k_tiles * f // block_size)
# Crop the K-tile padding too: k // block_size scale columns, not k.
return out[0:m, 0 : k // block_size]
def break_fp4_bytes(a, dtype):
assert a.dtype == torch.uint8
m, n = a.shape
a_flat = a.flatten()
high = (a_flat & 0xF0) >> 4
low = a_flat & 0x0F
combined = torch.stack((low, high), dim=1).flatten()
signs = (combined & 0x08).to(torch.bool)
abs_vals = (combined & 0x07).to(torch.long)
kE2M1 = kE2M1ToFloat.to(device=a.device)
values = kE2M1[abs_vals] * torch.where(signs, -1.0, 1.0)
return values.reshape(m, n * 2).to(dtype=dtype)
def dequantize_nvfp4_to_dtype(
tensor_fp4, tensor_sf, global_scale, dtype, device, block_size=16
):
assert tensor_fp4.dtype == torch.uint8
m, packed_k = tensor_fp4.shape
k = packed_k * 2
tensor_f32 = break_fp4_bytes(tensor_fp4, torch.float32)
tensor_f32 = tensor_f32.reshape(m, k // block_size, block_size)
tensor_sf = tensor_sf.view(torch.float8_e4m3fn)
tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
tensor_sf_dtype = tensor_sf.to(torch.float32) / global_scale
out = (tensor_f32 * tensor_sf_dtype.unsqueeze(-1)).reshape(m, k)
return out.to(dtype=dtype)
def _make_quantized_layer(n: int, k: int, device: str = "cuda"):
"""Build a linear layer holding NVFP4 checkpoint-format weights; returns
(method, layer, w_dequant) with w_dequant the fp32 quant->dequant reference."""
def _make_quantized_layer(n: int, k: int):
"""NVFP4 checkpoint-format weights through the real weight_loader."""
quant_config = ModelOptFp4Config(
is_checkpoint_nvfp4_serialized=True,
group_size=16,
use_per_token_activation=False,
packed_modules_mapping={},
)
method = ModelOptFp4LinearMethod(quant_config)
layer = torch.nn.Module()
method.create_weights(
layer = make_tp1_column_parallel_linear(quant_config, n, k)
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
w_q, sf_linear, gs, w_dequant = quantize_nvfp4_shard(w)
load_linear_weights(
layer,
input_size_per_partition=k,
output_partition_sizes=[n],
weight=w_q,
weight_scale=sf_linear,
weight_scale_2=(1.0 / gs).clone(),
# Calibrated activation amax stand-in (inputs are randn/10).
input_scale=torch.tensor(ACT_SCALE, device="cuda"),
)
return layer, w_dequant
def _make_merged_layer(n_half: int, k: int):
"""Two fused output shards (gate_up_proj) loaded per shard; exercises the
per-partition scale_2 / input_scale gathering that fused-QKV regressions hit."""
from sglang.srt.layers.linear import MergedColumnParallelLinear
quant_config = ModelOptFp4Config(
is_checkpoint_nvfp4_serialized=True,
group_size=16,
use_per_token_activation=False,
packed_modules_mapping={"gate_up_proj": ["gate_proj", "up_proj"]},
)
layer = MergedColumnParallelLinear(
input_size=k,
output_size=n,
output_sizes=[n_half, n_half],
bias=False,
params_dtype=torch.bfloat16,
weight_loader=lambda *args, **kwargs: None,
)
layer = layer.to(device)
quant_config=quant_config,
prefix="model.layers.0.mlp.gate_up_proj",
tp_rank=0,
tp_size=1,
).cuda()
w = torch.randn((n, k), device=device, dtype=torch.bfloat16) / 10
w_gs = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / w.abs().max().to(torch.float32)
w_q, w_sf_swizzled = fp4_quantize(w, w_gs)
w_sf_linear = convert_swizzled_to_linear(
w_sf_swizzled.view(torch.float8_e4m3fn), n, k, 16
# process_weights_after_loading collapses shard scale_2 with max() without
# requanting block scales, so shards must share one gs (modelopt fused
# exports ship equal scale_2).
shards = [
torch.randn((n_half, k), device="cuda", dtype=torch.bfloat16) / 10
for _ in (0, 1)
]
shared_gs = (
FLOAT8_E4M3_MAX
* FLOAT4_E2M1_MAX
/ max(w.abs().max().to(torch.float32) for w in shards)
)
w_dequant = dequantize_nvfp4_to_dtype(
w_q, w_sf_swizzled, w_gs, torch.float32, device
)
layer.weight.data.copy_(w_q)
layer.weight_scale.data.copy_(w_sf_linear)
layer.weight_scale_2.data.fill_(1.0 / w_gs)
# Calibrated activation amax stand-in (inputs are randn/10).
layer.input_scale.data.fill_(1.0 / (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX))
return method, layer, w_dequant
dequants = []
for shard_id, w in enumerate(shards):
w_q, sf_linear, gs, w_dequant = quantize_nvfp4_shard(w, gs=shared_gs)
load_linear_weights(
layer,
shard_id=shard_id,
weight=w_q,
weight_scale=sf_linear,
weight_scale_2=(1.0 / gs).clone(),
input_scale=torch.tensor(ACT_SCALE, device="cuda"),
)
dequants.append(w_dequant)
return layer, torch.cat(dequants, dim=0)
@unittest.skipIf(get_device_sm() < 100, "NVFP4 dense GEMM backends require SM100+")
class TestNvFp4LinearBackends(CustomTestCase):
def _run_backend(self, backend: str):
@classmethod
def setUpClass(cls):
init_single_process_dist()
def _run_backend(self, backend: str, build_layer=_make_quantized_layer):
torch.manual_seed(7)
for m, n, k in SHAPES:
with self.subTest(backend=backend, shape=(m, n, k)):
@@ -138,25 +131,32 @@ class TestNvFp4LinearBackends(CustomTestCase):
"FP4_GEMM_RUNNER_BACKEND",
Fp4GemmRunnerBackend(backend),
):
method, layer, w_dequant = _make_quantized_layer(n, k)
method.process_weights_after_loading(layer)
layer, w_dequant = build_layer(n, k)
layer.quant_method.process_weights_after_loading(layer)
x = torch.randn((m, k), device="cuda", dtype=torch.bfloat16) / 10
out = method.apply(layer, x)
out, _ = layer(x)
self._assert_matches(layer, x, out, w_dequant)
x_gs = layer.input_scale_inv.data.float()
x_q, x_sf = fp4_quantize(x, x_gs)
x_dequant = dequantize_nvfp4_to_dtype(
x_q, x_sf, x_gs, torch.float32, x.device
)
ref = x_dequant @ w_dequant.T
def _assert_matches(self, layer, x, out, w_dequant):
x_gs = layer.input_scale_inv.data.float()
x_q, x_sf = fp4_quantize(x, x_gs)
x_dequant = dequantize_nvfp4_to_dtype(x_q, x_sf, x_gs, torch.float32)
ref = x_dequant @ w_dequant.T
assert_output_close(self, out, ref, rtol=5e-2, atol=5e-2)
self.assertEqual(out.shape, (m, n))
cos = torch.nn.functional.cosine_similarity(
out.float().flatten(), ref.flatten(), dim=0
).item()
self.assertGreater(cos, 0.99)
torch.testing.assert_close(out.float(), ref, rtol=5e-2, atol=5e-2)
def test_merged_shards(self):
torch.manual_seed(7)
with mock.patch.object(
fp4_utils,
"FP4_GEMM_RUNNER_BACKEND",
Fp4GemmRunnerBackend("flashinfer_cutedsl"),
):
layer, w_dequant = _make_merged_layer(256, 512)
layer.quant_method.process_weights_after_loading(layer)
x = torch.randn((16, 512), device="cuda", dtype=torch.bfloat16) / 10
out, _ = layer(x)
self._assert_matches(layer, x, out, w_dequant)
def test_flashinfer_cutedsl(self):
self._run_backend("flashinfer_cutedsl")
@@ -1,13 +1,10 @@
"""Numerics for the NVFP4 FusedMoE runner backends (--moe-runner-backend).
Runs the real FusedMoE layer path (construct -> fill NVFP4 checkpoint-format
weights -> process_weights_after_loading -> forward) per backend against a
dequantized torch MoE reference, covering the per-backend weight preparation
(TRTLLM shuffle / CUTLASS swizzle / CuteDSL v2 interleave + MMA blockscales)
and the MoE runner dispatch. Single GPU, tp=ep=1.
Real FusedMoE path (NVFP4 checkpoint shards through the real weight_loader
-> weight processing -> forward) per backend vs a dequantized torch MoE
reference. Single GPU, tp=ep=1.
"""
import os
import unittest
import torch
@@ -17,83 +14,30 @@ from sglang.srt.layers.quantization.modelopt_quant import ModelOptFp4Config
from sglang.srt.runtime_context import get_context, get_flags, get_parallel
from sglang.srt.utils import get_device_sm
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.layer_ut_utils import assert_output_close, init_single_process_dist
from sglang.test.quant_ref_utils import (
FLOAT4_E2M1_MAX,
FLOAT8_E4M3_MAX,
dequantize_nvfp4_to_dtype,
quantize_nvfp4_shard,
)
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=120, stage="base-b", runner_config="4-gpu-b200")
E, H, I, TOPK, M = 8, 1024, 1024, 2, 32
FLOAT8_E4M3_MAX = 448.0
FLOAT4_E2M1_MAX = 6.0
kE2M1ToFloat = torch.tensor(
[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=torch.float32
)
def _init_single_process_dist():
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
os.environ.setdefault("MASTER_PORT", "29631")
os.environ.setdefault("RANK", "0")
os.environ.setdefault("WORLD_SIZE", "1")
os.environ.setdefault("LOCAL_RANK", "0")
from sglang.srt.distributed.parallel_state import (
init_distributed_environment,
initialize_model_parallel,
model_parallel_is_initialized,
)
if not torch.distributed.is_initialized():
init_distributed_environment(world_size=1, rank=0, local_rank=0, backend="gloo")
if not model_parallel_is_initialized():
initialize_model_parallel(
tensor_model_parallel_size=1,
expert_model_parallel_size=1,
pipeline_model_parallel_size=1,
backend="gloo",
)
def convert_swizzled_to_linear(a_sf_swizzled, m, k, block_size=16):
m_tiles = (m + 128 - 1) // 128
f = block_size * 4
k_tiles = (k + f - 1) // f
tmp = torch.reshape(a_sf_swizzled, (1, m_tiles, k_tiles, 32, 4, 4))
tmp = torch.permute(tmp, (0, 1, 4, 3, 2, 5))
out = tmp.reshape(m_tiles * 128, k_tiles * f // block_size)
return out[0:m, 0 : k // block_size]
def break_fp4_bytes(a):
m, n = a.shape
a_flat = a.flatten()
high = (a_flat & 0xF0) >> 4
low = a_flat & 0x0F
combined = torch.stack((low, high), dim=1).flatten()
signs = (combined & 0x08).to(torch.bool)
abs_vals = (combined & 0x07).to(torch.long)
kE2M1 = kE2M1ToFloat.to(device=a.device)
values = kE2M1[abs_vals] * torch.where(signs, -1.0, 1.0)
return values.reshape(m, n * 2).to(dtype=torch.float32)
def dequant_nvfp4(w_q, sf_swizzled, gs, n, k):
w_f32 = break_fp4_bytes(w_q).reshape(n, k // 16, 16)
sf = convert_swizzled_to_linear(sf_swizzled.view(torch.float8_e4m3fn), n, k)
return (w_f32 * (sf.float() / gs).unsqueeze(-1)).reshape(n, k)
@unittest.skipIf(get_device_sm() < 100, "NVFP4 MoE backends require SM100+")
class TestNvFp4MoeBackends(CustomTestCase):
@classmethod
def setUpClass(cls):
_init_single_process_dist()
init_single_process_dist(master_port=29631)
torch.set_default_device("cuda")
def _run_backend(self, backend: str):
from flashinfer import fp4_quantize
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
from sglang.srt.layers.moe.topk import StandardTopKOutput
from sglang.srt.layers.moe.topk import TopKConfig, select_experts
torch.manual_seed(7)
quant_config = ModelOptFp4Config(
@@ -122,93 +66,78 @@ class TestNvFp4MoeBackends(CustomTestCase):
gate_up_interleaved=False,
).cuda()
w13_ref = torch.zeros(E, 2 * I, H, dtype=torch.float32, device="cuda")
w2_ref = torch.zeros(E, H, I, dtype=torch.float32, device="cuda")
for e in range(E):
w13 = torch.randn(2 * I, H, dtype=torch.bfloat16, device="cuda") / 10
w2 = torch.randn(H, I, dtype=torch.bfloat16, device="cuda") / 10
w13_gs = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / w13.abs().max().float()
w2_gs = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / w2.abs().max().float()
w13_q, w13_sf = fp4_quantize(w13, w13_gs)
w2_q, w2_sf = fp4_quantize(w2, w2_gs)
layer.w13_weight.data[e].copy_(w13_q)
layer.w2_weight.data[e].copy_(w2_q)
layer.w13_weight_scale.data[e].copy_(
convert_swizzled_to_linear(
w13_sf.view(torch.float8_e4m3fn), 2 * I, H
)
)
layer.w2_weight_scale.data[e].copy_(
convert_swizzled_to_linear(w2_sf.view(torch.float8_e4m3fn), H, I)
)
layer.w13_weight_scale_2.data[e].fill_(1.0 / w13_gs)
layer.w2_weight_scale_2.data[e].fill_(1.0 / w2_gs)
w13_ref[e] = dequant_nvfp4(w13_q, w13_sf, w13_gs, 2 * I, H)
w2_ref[e] = dequant_nvfp4(w2_q, w2_sf, w2_gs, H, I)
# Checkpoint-format shards through the real weight_loader;
# gate/up placement stays the loader's job.
refs = {
"w1": torch.zeros(E, I, H, dtype=torch.float32, device="cuda"),
"w3": torch.zeros(E, I, H, dtype=torch.float32, device="cuda"),
"w2": torch.zeros(E, H, I, dtype=torch.float32, device="cuda"),
}
act_scale = 1.0 / (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX)
layer.w13_input_scale.data.fill_(act_scale)
layer.w2_input_scale.data.fill_(act_scale)
for e in range(E):
for shard_id in ("w1", "w3", "w2"):
rows, cols = (I, H) if shard_id in ("w1", "w3") else (H, I)
w = (
torch.randn(rows, cols, dtype=torch.bfloat16, device="cuda")
/ 10
)
w_q, sf_linear, gs, w_dequant = quantize_nvfp4_shard(w)
prefix = "w13" if shard_id in ("w1", "w3") else "w2"
for suffix, loaded in (
("weight", w_q),
("weight_scale", sf_linear),
("weight_scale_2", (1.0 / gs).clone()),
("input_scale", torch.tensor(act_scale, device="cuda")),
):
name = f"{prefix}_{suffix}"
param = getattr(layer, name)
layer.weight_loader(
param, loaded, name, shard_id=shard_id, expert_id=e
)
refs[shard_id][e] = w_dequant
layer.quant_method.process_weights_after_loading(layer)
x = torch.randn(M, H, dtype=torch.bfloat16, device="cuda") / 10
router_logits = torch.randn(M, E, dtype=torch.float32, device="cuda")
weights = torch.softmax(router_logits, dim=-1)
topk_weights, topk_ids = torch.topk(weights, TOPK, dim=-1)
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
out = layer.forward(
x,
StandardTopKOutput(
topk_weights=topk_weights,
topk_ids=topk_ids.to(torch.int32),
router_logits=router_logits,
),
# Route through the real topk compute path, not a hand-rolled one.
topk_output = select_experts(
hidden_states=x,
router_logits=router_logits,
topk_config=TopKConfig(top_k=TOPK, renormalize=True),
)
topk_weights, topk_ids = topk_output.topk_weights, topk_output.topk_ids
out = layer.forward(x, topk_output)
if not isinstance(out, torch.Tensor):
out = out[0] if isinstance(out, tuple) else out.hidden_states
ref = self._torch_moe_reference(
layer, x, topk_weights, topk_ids, w13_ref, w2_ref
x, topk_weights, topk_ids, refs["w1"], refs["w3"], refs["w2"]
)
self.assertEqual(out.shape, (M, H))
cos = torch.nn.functional.cosine_similarity(
out.float().flatten(), ref.flatten(), dim=0
).item()
self.assertGreater(cos, 0.99)
assert_output_close(self, out, ref)
@staticmethod
def _torch_moe_reference(layer, x, topk_weights, topk_ids, w13_ref, w2_ref):
def _torch_moe_reference(x, topk_weights, topk_ids, w1_ref, w3_ref, w2_ref):
from flashinfer import fp4_quantize
def quant_roundtrip(t2d, gs):
q, sf = fp4_quantize(t2d.to(torch.bfloat16), gs)
return dequant_nvfp4(q, sf, gs, t2d.shape[0], t2d.shape[1])
return dequantize_nvfp4_to_dtype(q, sf, gs, torch.float32)
# The kernels quantize the input and the GEMM1->GEMM2 intermediate to
# NVFP4; mirror both round trips or the comparison carries ~7% noise.
# Mirror the kernels' two fp4 activation round trips (input and
# GEMM1->GEMM2) or the comparison carries ~7% noise. The reference
# stays in checkpoint semantics (w1=gate, w3=up).
act_gs = torch.tensor(
FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX, dtype=torch.float32, device="cuda"
)
# TRTLLM consumes w13 as [up; gate] (GEMM1 scales are applied on that
# assumption); CUTLASS / CuteDSL-v2 load up first as well via
# load_up_proj_weight_first.
up_first = (
layer.quant_method.load_up_proj_weight_first
or layer.quant_method.enable_flashinfer_trtllm_moe
)
x_dq = quant_roundtrip(x.float(), act_gs)
m, h = x.shape
ref = torch.zeros(m, h, dtype=torch.float32, device="cuda")
for t in range(m):
for j in range(topk_ids.shape[1]):
e = int(topk_ids[t, j])
gu = x_dq[t] @ w13_ref[e].T
if up_first:
up, gate = gu[:I], gu[I:]
else:
gate, up = gu[:I], gu[I:]
gate = x_dq[t] @ w1_ref[e].T
up = x_dq[t] @ w3_ref[e].T
act = torch.nn.functional.silu(gate) * up
act_dq = quant_roundtrip(act.unsqueeze(0), act_gs)[0]
ref[t] += float(topk_weights[t, j]) * (act_dq @ w2_ref[e].T)
+4 -49
View File
@@ -21,7 +21,6 @@ GPU dependency. State is stored in a mock centralized pool that mirrors the
``HybridReqToTokenPool`` / ``MambaPool`` interface used at serving time.
"""
import os
import unittest
from contextlib import contextmanager
from dataclasses import dataclass
@@ -31,55 +30,16 @@ from typing import List, Optional
import torch
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.layer_ut_utils import init_single_process_dist
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=30, suite="base-a-test-cpu")
def _ensure_dist_initialized() -> None:
"""Set up a minimal single-rank gloo distributed environment plus the
SGLang model-parallel groups (TP=1, PP=1, EP=1). The CCA module reads
``get_tensor_model_parallel_rank()`` / ``get_tensor_model_parallel_world_size()``
inside ``__init__`` to size its head-parallel projections, so the world
group and model parallel groups must both be initialized before any CCA
construction.
"""
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
os.environ.setdefault("MASTER_PORT", "29632")
os.environ.setdefault("RANK", "0")
os.environ.setdefault("WORLD_SIZE", "1")
os.environ.setdefault("LOCAL_RANK", "0")
from sglang.srt.distributed.parallel_state import (
init_distributed_environment,
initialize_model_parallel,
model_parallel_is_initialized,
)
if not torch.distributed.is_initialized():
init_distributed_environment(
world_size=1,
rank=0,
local_rank=0,
backend="gloo",
)
if not model_parallel_is_initialized():
# Pass arguments as kwargs because ``ensure_model_parallel_initialized``
# forwards positional ``backend`` into the ``attention_data_parallel_size``
# slot of ``initialize_model_parallel``, which then explodes on
# ``int // str``. Using kwargs avoids that footgun.
initialize_model_parallel(
tensor_model_parallel_size=1,
expert_model_parallel_size=1,
pipeline_model_parallel_size=1,
backend="gloo",
)
# ---------------------------------------------------------------------------
# Mock centralized pool
# ---------------------------------------------------------------------------
"""CCA reads the TP rank / world size inside ``__init__`` to size its
head-parallel projections, so the groups must exist before construction."""
init_single_process_dist()
@dataclass(frozen=True)
@@ -202,11 +162,6 @@ def _mock_pool_context(pool: _MockReqToTokenPool):
set_forward_context(prev)
# ---------------------------------------------------------------------------
# Helper factories
# ---------------------------------------------------------------------------
def _make_forward_batch(
*,
is_decode: bool,