[Test] Route GEMM backend UTs through real layer modules and weight loaders (#33615)
This commit is contained in:
@@ -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)
|
||||||
@@ -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 (
|
from sglang.srt.debug_utils.tensor_dump_forward_hook import (
|
||||||
register_forward_hook_for_model,
|
register_forward_hook_for_model,
|
||||||
)
|
)
|
||||||
from sglang.srt.distributed.parallel_state import (
|
from sglang.srt.distributed.parallel_state import get_default_distributed_backend
|
||||||
get_default_distributed_backend,
|
|
||||||
init_distributed_environment,
|
|
||||||
initialize_model_parallel,
|
|
||||||
)
|
|
||||||
from sglang.srt.layers.layernorm import RMSNorm
|
from sglang.srt.layers.layernorm import RMSNorm
|
||||||
from sglang.srt.layers.linear import LinearBase
|
from sglang.srt.layers.linear import LinearBase
|
||||||
from sglang.srt.models.qwen2 import Qwen2MLP
|
from sglang.srt.models.qwen2 import Qwen2MLP
|
||||||
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
|
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
|
||||||
from sglang.srt.utils import add_prefix, get_device
|
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.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||||
|
from sglang.test.layer_ut_utils import init_single_process_dist
|
||||||
|
|
||||||
register_cuda_ci(
|
register_cuda_ci(
|
||||||
est_time=9,
|
est_time=9,
|
||||||
@@ -80,15 +77,7 @@ def init_weights(module):
|
|||||||
def test_model_forward_dump(tmp_path):
|
def test_model_forward_dump(tmp_path):
|
||||||
set_global_server_args_for_scheduler(ServerArgs(model_path="dummy"))
|
set_global_server_args_for_scheduler(ServerArgs(model_path="dummy"))
|
||||||
device = get_device()
|
device = get_device()
|
||||||
backend = get_default_distributed_backend(device)
|
init_single_process_dist(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()
|
|
||||||
model = MockCausalLM()
|
model = MockCausalLM()
|
||||||
model.apply(init_weights)
|
model.apply(init_weights)
|
||||||
model = model.to(device=device, dtype=torch.bfloat16)
|
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.srt.layers.moe.topk import TopKConfig, select_experts
|
||||||
from sglang.test.ci.ci_register import register_cuda_ci
|
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)
|
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,
|
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):
|
def compute_routing(router_logits: torch.Tensor, top_k: int):
|
||||||
routing_weights = torch.softmax(router_logits, dim=1, dtype=torch.float)
|
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_blockscale,
|
||||||
inter_gs,
|
inter_gs,
|
||||||
dtype=inter.dtype,
|
dtype=inter.dtype,
|
||||||
device=inter.device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
).cuda()
|
).cuda()
|
||||||
out[mask] = inter @ w2[i].transpose(0, 1)
|
out[mask] = inter @ w2[i].transpose(0, 1)
|
||||||
@@ -319,7 +263,6 @@ def check_moe(
|
|||||||
a_scale_interleaved,
|
a_scale_interleaved,
|
||||||
a_global_scale,
|
a_global_scale,
|
||||||
dtype=a.dtype,
|
dtype=a.dtype,
|
||||||
device=a.device,
|
|
||||||
block_size=quant_blocksize,
|
block_size=quant_blocksize,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -332,7 +275,6 @@ def check_moe(
|
|||||||
w1_blockscale[idx],
|
w1_blockscale[idx],
|
||||||
w1_gs[idx],
|
w1_gs[idx],
|
||||||
dtype=w1.dtype,
|
dtype=w1.dtype,
|
||||||
device=w1.device,
|
|
||||||
block_size=quant_blocksize,
|
block_size=quant_blocksize,
|
||||||
)
|
)
|
||||||
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
||||||
@@ -340,7 +282,6 @@ def check_moe(
|
|||||||
w2_blockscale[idx],
|
w2_blockscale[idx],
|
||||||
w2_gs[idx],
|
w2_gs[idx],
|
||||||
dtype=w2.dtype,
|
dtype=w2.dtype,
|
||||||
device=w2.device,
|
|
||||||
block_size=quant_blocksize,
|
block_size=quant_blocksize,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ from torch.nn import functional as F
|
|||||||
from sglang.srt.layers.activation import SiluAndMul
|
from sglang.srt.layers.activation import SiluAndMul
|
||||||
from sglang.srt.layers.moe.flashinfer_cutedsl_moe import flashinfer_cutedsl_moe_masked
|
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.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:
|
try:
|
||||||
from flashinfer import CuteDslMoEWrapper
|
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_TEST = torch.cuda.get_device_capability() < (10, 0)
|
||||||
SKIP_REASON = "Nvfp4 Requires compute capability of 10 or above."
|
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(
|
def _interleave_w13_halves(
|
||||||
x: torch.Tensor, group_size: int = 64, dim: int = -1
|
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_blockscale,
|
||||||
inter_gs,
|
inter_gs,
|
||||||
dtype=inter.dtype,
|
dtype=inter.dtype,
|
||||||
device=inter.device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
).cuda()
|
).cuda()
|
||||||
out[mask] = inter @ w2[i].transpose(0, 1)
|
out[mask] = inter @ w2[i].transpose(0, 1)
|
||||||
@@ -916,7 +860,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
a_scale_interleaved,
|
a_scale_interleaved,
|
||||||
a_global_scale,
|
a_global_scale,
|
||||||
dtype=hidden_states.dtype,
|
dtype=hidden_states.dtype,
|
||||||
device=hidden_states.device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
w1_d = torch.empty(
|
w1_d = torch.empty(
|
||||||
@@ -942,7 +885,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
w1_blockscale_sliced,
|
w1_blockscale_sliced,
|
||||||
w1_global_scale[idx],
|
w1_global_scale[idx],
|
||||||
dtype=w1.dtype,
|
dtype=w1.dtype,
|
||||||
device=w1.device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
||||||
@@ -950,7 +892,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
w2_blockscale_sliced,
|
w2_blockscale_sliced,
|
||||||
w2_global_scale[idx],
|
w2_global_scale[idx],
|
||||||
dtype=w2.dtype,
|
dtype=w2.dtype,
|
||||||
device=w2.device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1093,7 +1034,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
a_scale_interleaved,
|
a_scale_interleaved,
|
||||||
a_global_scale,
|
a_global_scale,
|
||||||
dtype=hidden_states.dtype,
|
dtype=hidden_states.dtype,
|
||||||
device=device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
w1_d = torch.empty(
|
w1_d = torch.empty(
|
||||||
@@ -1117,7 +1057,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
w1_blockscale_sliced,
|
w1_blockscale_sliced,
|
||||||
w1_global_scale[idx],
|
w1_global_scale[idx],
|
||||||
dtype=w1.dtype,
|
dtype=w1.dtype,
|
||||||
device=device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
||||||
@@ -1125,7 +1064,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
w2_blockscale_sliced,
|
w2_blockscale_sliced,
|
||||||
w2_global_scale[idx],
|
w2_global_scale[idx],
|
||||||
dtype=w2.dtype,
|
dtype=w2.dtype,
|
||||||
device=device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1268,7 +1206,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
a_scale,
|
a_scale,
|
||||||
a_gs,
|
a_gs,
|
||||||
dtype=torch.bfloat16,
|
dtype=torch.bfloat16,
|
||||||
device=device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
w1_d = torch.empty(
|
w1_d = torch.empty(
|
||||||
@@ -1287,7 +1224,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
w1_blockscale_sliced,
|
w1_blockscale_sliced,
|
||||||
w1_gs[idx],
|
w1_gs[idx],
|
||||||
dtype=w1.dtype,
|
dtype=w1.dtype,
|
||||||
device=device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
w2_d[idx] = dequantize_nvfp4_to_dtype(
|
||||||
@@ -1295,7 +1231,6 @@ class TestCuteDslV1(unittest.TestCase):
|
|||||||
w2_blockscale_sliced,
|
w2_blockscale_sliced,
|
||||||
w2_gs[idx],
|
w2_gs[idx],
|
||||||
dtype=w2.dtype,
|
dtype=w2.dtype,
|
||||||
device=device,
|
|
||||||
block_size=16,
|
block_size=16,
|
||||||
)
|
)
|
||||||
ref = torch_moe_nvfp4(
|
ref = torch_moe_nvfp4(
|
||||||
|
|||||||
@@ -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).
|
is not installed or the GPU is not SM90 (the kernels ship sm90a only).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import torch
|
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 import MoeRunnerConfig
|
||||||
from sglang.srt.layers.moe.moe_runner.hpc_ops import (
|
from sglang.srt.layers.moe.moe_runner.hpc_ops import (
|
||||||
HpcOpsMoeQuantInfo,
|
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.layers.moe.topk import StandardTopKOutput
|
||||||
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
|
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.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
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
register_cuda_ci(est_time=60, stage="base-b", runner_config="1-gpu-large")
|
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:
|
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
|
``use_symmetric_memory(get_tp_group(), ...)``, which requires the TP
|
||||||
group even when symmetric allocation is disabled.
|
group even when symmetric allocation is disabled.
|
||||||
"""
|
"""
|
||||||
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
|
init_single_process_dist(master_port=29633)
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _quant_blockwise(w: torch.Tensor, block: int = 128):
|
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.device_config import DeviceConfig
|
||||||
from sglang.srt.configs.load_config import LoadConfig
|
from sglang.srt.configs.load_config import LoadConfig
|
||||||
from sglang.srt.configs.model_config import ModelConfig
|
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.distributed.parallel_state import monkey_patch_vllm_parallel_state
|
||||||
from sglang.srt.layers.quantization.utils import get_dynamic_override
|
from sglang.srt.layers.quantization.utils import get_dynamic_override
|
||||||
from sglang.srt.model_loader import get_model
|
from sglang.srt.model_loader import get_model
|
||||||
from sglang.srt.server_args import ServerArgs
|
from sglang.srt.server_args import ServerArgs
|
||||||
|
from sglang.test.layer_ut_utils import init_single_process_dist
|
||||||
|
|
||||||
try:
|
init_single_process_dist(backend="nccl")
|
||||||
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()
|
monkey_patch_vllm_parallel_state()
|
||||||
except AssertionError:
|
|
||||||
# ignore this error: tensor model parallel group is already initialized
|
|
||||||
pass
|
|
||||||
|
|
||||||
server_args = ServerArgs(model_path=model_path, dtype=torch.float16)
|
server_args = ServerArgs(model_path=model_path, dtype=torch.float16)
|
||||||
set_global_server_args_for_scheduler(server_args)
|
set_global_server_args_for_scheduler(server_args)
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
"""Numerics for the FP8 dense-linear GEMM backends (--fp8-gemm-backend).
|
"""Numerics for the FP8 dense-linear GEMM backends (--fp8-gemm-backend).
|
||||||
|
|
||||||
Runs the quant-method layer path (create_weights ->
|
Real layer path vs a dequantized-reference matmul, in three formats: FP8
|
||||||
process_weights_after_loading -> apply) against a dequantized-reference
|
blockwise, MXFP8, and per-tensor FP8 (auto dispatch). Backend sets adapt to
|
||||||
matmul, covering the per-backend weight preparation (e.g. UE8M0 scale requant
|
the device SM, so one file covers SM90 / SM100 / SM120.
|
||||||
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).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
@@ -16,14 +11,17 @@ from unittest import mock
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from sglang.srt.layers.quantization import fp8_utils
|
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.fp8_utils import Fp8GemmRunnerBackend
|
||||||
from sglang.srt.layers.quantization.modelopt_quant import (
|
from sglang.srt.layers.quantization.modelopt_quant import ModelOptFp8Config
|
||||||
ModelOptFp8Config,
|
|
||||||
ModelOptFp8LinearMethod,
|
|
||||||
)
|
|
||||||
from sglang.srt.utils import get_device_sm
|
from sglang.srt.utils import get_device_sm
|
||||||
from sglang.test.ci.ci_register import register_cuda_ci
|
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
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
register_cuda_ci(est_time=120, stage="base-b", runner_config="4-gpu-b200")
|
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
|
return w_fp8.reshape(n, k), scale_e8m0, w_dequant
|
||||||
|
|
||||||
|
|
||||||
def _create_weights(method, n: int, k: int, device: str = "cuda"):
|
def _make_linear(quant_config, n: int, k: int):
|
||||||
layer = torch.nn.Module()
|
return make_tp1_column_parallel_linear(
|
||||||
kwargs = {}
|
quant_config, n, k, skip_block_quant_check=True
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
return layer.to(device)
|
|
||||||
|
|
||||||
|
|
||||||
class _LinearBackendCheck(CustomTestCase):
|
class _LinearBackendCheck(CustomTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
init_single_process_dist()
|
||||||
|
|
||||||
def _check_backend(self, backend: str, allowed, shapes, build_layer):
|
def _check_backend(self, backend: str, allowed, shapes, build_layer):
|
||||||
if backend not in allowed:
|
if backend not in allowed:
|
||||||
self.skipTest(f"{backend} not in SM{get_device_sm()} backend set")
|
self.skipTest(f"{backend} not in SM{get_device_sm()} backend set")
|
||||||
@@ -130,21 +119,16 @@ class _LinearBackendCheck(CustomTestCase):
|
|||||||
"FP8_GEMM_RUNNER_BACKEND",
|
"FP8_GEMM_RUNNER_BACKEND",
|
||||||
Fp8GemmRunnerBackend(backend),
|
Fp8GemmRunnerBackend(backend),
|
||||||
):
|
):
|
||||||
method, layer, w_dequant = build_layer(n, k)
|
layer, w_dequant = build_layer(n, k)
|
||||||
method.process_weights_after_loading(layer)
|
layer.quant_method.process_weights_after_loading(layer)
|
||||||
|
|
||||||
x = torch.randn((m, k), device="cuda", dtype=torch.bfloat16) / 10
|
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
|
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
|
# atol covers single-element UE8M0 scale-rounding outliers
|
||||||
# (deep_gemm); a wrong kernel/layout fails by orders more.
|
# (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+")
|
@unittest.skipIf(get_device_sm() < 90, "FP8 GEMM backends require SM90+")
|
||||||
@@ -156,13 +140,11 @@ class TestFp8BlockwiseLinearBackends(_LinearBackendCheck):
|
|||||||
activation_scheme="dynamic",
|
activation_scheme="dynamic",
|
||||||
weight_block_size=[128, 128],
|
weight_block_size=[128, 128],
|
||||||
)
|
)
|
||||||
method = Fp8LinearMethod(quant_config)
|
layer = _make_linear(quant_config, n, k)
|
||||||
layer = _create_weights(method, n, k)
|
|
||||||
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
|
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
|
||||||
w_fp8, scale_inv, w_dequant = _quantize_fp8_blockwise(w)
|
w_fp8, scale_inv, w_dequant = _quantize_fp8_blockwise(w)
|
||||||
layer.weight.data.copy_(w_fp8)
|
load_linear_weights(layer, weight=w_fp8, weight_scale_inv=scale_inv)
|
||||||
layer.weight_scale_inv.data.copy_(scale_inv)
|
return layer, w_dequant
|
||||||
return method, layer, w_dequant
|
|
||||||
|
|
||||||
def _run(self, backend: str):
|
def _run(self, backend: str):
|
||||||
self._check_backend(
|
self._check_backend(
|
||||||
@@ -197,13 +179,11 @@ class TestMxfp8LinearBackends(_LinearBackendCheck):
|
|||||||
activation_scheme="dynamic",
|
activation_scheme="dynamic",
|
||||||
use_mxfp8=True,
|
use_mxfp8=True,
|
||||||
)
|
)
|
||||||
method = Fp8LinearMethod(quant_config)
|
layer = _make_linear(quant_config, n, k)
|
||||||
layer = _create_weights(method, n, k)
|
|
||||||
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
|
w = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
|
||||||
w_fp8, scale_e8m0, w_dequant = _quantize_mxfp8(w)
|
w_fp8, scale_e8m0, w_dequant = _quantize_mxfp8(w)
|
||||||
layer.weight.data.copy_(w_fp8)
|
load_linear_weights(layer, weight=w_fp8, weight_scale_inv=scale_e8m0)
|
||||||
layer.weight_scale_inv.data.copy_(scale_e8m0)
|
return layer, w_dequant
|
||||||
return method, layer, w_dequant
|
|
||||||
|
|
||||||
def _run(self, backend: str):
|
def _run(self, backend: str):
|
||||||
self._check_backend(backend, _mxfp8_backends(), MXFP8_SHAPES, self._build_layer)
|
self._check_backend(backend, _mxfp8_backends(), MXFP8_SHAPES, self._build_layer)
|
||||||
@@ -225,17 +205,22 @@ class TestModeloptFp8PerTensorLinear(_LinearBackendCheck):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_layer(n: int, k: int):
|
def _build_layer(n: int, k: int):
|
||||||
quant_config = ModelOptFp8Config(is_checkpoint_fp8_serialized=True)
|
quant_config = ModelOptFp8Config(
|
||||||
method = ModelOptFp8LinearMethod(quant_config)
|
is_checkpoint_fp8_serialized=True, packed_modules_mapping={}
|
||||||
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 = torch.randn((n, k), device="cuda", dtype=torch.bfloat16) / 10
|
||||||
scale = (w.float().abs().max() / FP8_MAX).clamp(min=1e-12)
|
scale = (w.float().abs().max() / FP8_MAX).clamp(min=1e-12)
|
||||||
w_fp8 = (w.float() / scale).to(torch.float8_e4m3fn)
|
w_fp8 = (w.float() / scale).to(torch.float8_e4m3fn)
|
||||||
layer.weight.data.copy_(w_fp8)
|
# 0-dim scales exercise weight_loader_v2's scalar reshape branch.
|
||||||
layer.weight_scale.data.fill_(scale)
|
load_linear_weights(
|
||||||
layer.input_scale.data.fill_(1.0 / FP8_MAX)
|
layer,
|
||||||
|
weight=w_fp8,
|
||||||
|
weight_scale=scale,
|
||||||
|
input_scale=torch.tensor(1.0 / FP8_MAX, device="cuda"),
|
||||||
|
)
|
||||||
w_dequant = w_fp8.float() * scale
|
w_dequant = w_fp8.float() * scale
|
||||||
return method, layer, w_dequant
|
return layer, w_dequant
|
||||||
|
|
||||||
def test_auto(self):
|
def test_auto(self):
|
||||||
self._check_backend("auto", ["auto"], PER_TENSOR_SHAPES, self._build_layer)
|
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).
|
"""Numerics for the NVFP4 dense-linear GEMM backends (--fp4-gemm-backend).
|
||||||
|
|
||||||
Runs ModelOptFp4LinearMethod end to end (create_weights ->
|
Real layer path (ColumnParallelLinear -> weight_loader -> weight processing
|
||||||
process_weights_after_loading -> apply) for each SM100 backend choice and
|
-> forward) per SM100 backend vs a dequantized-reference matmul; a merged
|
||||||
checks the output against a dequantized-reference matmul. This covers both
|
two-shard case guards the per-partition scale gathering of fused layers.
|
||||||
the per-backend weight preparation (padding / interleave / TRTLLM shuffle)
|
|
||||||
and the GEMM kernel dispatch.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
@@ -15,22 +13,25 @@ from flashinfer import fp4_quantize
|
|||||||
|
|
||||||
from sglang.srt.layers.quantization import fp4_utils
|
from sglang.srt.layers.quantization import fp4_utils
|
||||||
from sglang.srt.layers.quantization.fp4_utils import Fp4GemmRunnerBackend
|
from sglang.srt.layers.quantization.fp4_utils import Fp4GemmRunnerBackend
|
||||||
from sglang.srt.layers.quantization.modelopt_quant import (
|
from sglang.srt.layers.quantization.modelopt_quant import ModelOptFp4Config
|
||||||
ModelOptFp4Config,
|
|
||||||
ModelOptFp4LinearMethod,
|
|
||||||
)
|
|
||||||
from sglang.srt.utils import get_device_sm
|
from sglang.srt.utils import get_device_sm
|
||||||
from sglang.test.ci.ci_register import register_cuda_ci
|
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
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
register_cuda_ci(est_time=120, stage="base-b", runner_config="4-gpu-b200")
|
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
|
# (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
|
# 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).
|
# K pad) nor K/16 a multiple of 4 (TRTLLM scale pad).
|
||||||
@@ -40,96 +41,88 @@ SHAPES = [
|
|||||||
(128, 1024, 1024),
|
(128, 1024, 1024),
|
||||||
]
|
]
|
||||||
|
|
||||||
BACKENDS = [
|
ACT_SCALE = 1.0 / (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX)
|
||||||
"flashinfer_cutedsl",
|
|
||||||
"flashinfer_cutlass",
|
|
||||||
"flashinfer_cudnn",
|
|
||||||
"flashinfer_trtllm",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def convert_swizzled_to_linear(a_sf_swizzled: torch.Tensor, m, k, block_size):
|
def _make_quantized_layer(n: int, k: int):
|
||||||
m_tiles = (m + 128 - 1) // 128
|
"""NVFP4 checkpoint-format weights through the real weight_loader."""
|
||||||
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."""
|
|
||||||
quant_config = ModelOptFp4Config(
|
quant_config = ModelOptFp4Config(
|
||||||
is_checkpoint_nvfp4_serialized=True,
|
is_checkpoint_nvfp4_serialized=True,
|
||||||
group_size=16,
|
group_size=16,
|
||||||
use_per_token_activation=False,
|
use_per_token_activation=False,
|
||||||
|
packed_modules_mapping={},
|
||||||
)
|
)
|
||||||
method = ModelOptFp4LinearMethod(quant_config)
|
layer = make_tp1_column_parallel_linear(quant_config, n, k)
|
||||||
layer = torch.nn.Module()
|
|
||||||
method.create_weights(
|
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,
|
layer,
|
||||||
input_size_per_partition=k,
|
weight=w_q,
|
||||||
output_partition_sizes=[n],
|
weight_scale=sf_linear,
|
||||||
input_size=k,
|
weight_scale_2=(1.0 / gs).clone(),
|
||||||
output_size=n,
|
|
||||||
params_dtype=torch.bfloat16,
|
|
||||||
weight_loader=lambda *args, **kwargs: None,
|
|
||||||
)
|
|
||||||
layer = layer.to(device)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
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).
|
# Calibrated activation amax stand-in (inputs are randn/10).
|
||||||
layer.input_scale.data.fill_(1.0 / (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX))
|
input_scale=torch.tensor(ACT_SCALE, device="cuda"),
|
||||||
return method, layer, w_dequant
|
)
|
||||||
|
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_sizes=[n_half, n_half],
|
||||||
|
bias=False,
|
||||||
|
params_dtype=torch.bfloat16,
|
||||||
|
quant_config=quant_config,
|
||||||
|
prefix="model.layers.0.mlp.gate_up_proj",
|
||||||
|
tp_rank=0,
|
||||||
|
tp_size=1,
|
||||||
|
).cuda()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
)
|
||||||
|
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+")
|
@unittest.skipIf(get_device_sm() < 100, "NVFP4 dense GEMM backends require SM100+")
|
||||||
class TestNvFp4LinearBackends(CustomTestCase):
|
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)
|
torch.manual_seed(7)
|
||||||
for m, n, k in SHAPES:
|
for m, n, k in SHAPES:
|
||||||
with self.subTest(backend=backend, shape=(m, n, k)):
|
with self.subTest(backend=backend, shape=(m, n, k)):
|
||||||
@@ -138,25 +131,32 @@ class TestNvFp4LinearBackends(CustomTestCase):
|
|||||||
"FP4_GEMM_RUNNER_BACKEND",
|
"FP4_GEMM_RUNNER_BACKEND",
|
||||||
Fp4GemmRunnerBackend(backend),
|
Fp4GemmRunnerBackend(backend),
|
||||||
):
|
):
|
||||||
method, layer, w_dequant = _make_quantized_layer(n, k)
|
layer, w_dequant = build_layer(n, k)
|
||||||
method.process_weights_after_loading(layer)
|
layer.quant_method.process_weights_after_loading(layer)
|
||||||
|
|
||||||
x = torch.randn((m, k), device="cuda", dtype=torch.bfloat16) / 10
|
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)
|
||||||
|
|
||||||
|
def _assert_matches(self, layer, x, out, w_dequant):
|
||||||
x_gs = layer.input_scale_inv.data.float()
|
x_gs = layer.input_scale_inv.data.float()
|
||||||
x_q, x_sf = fp4_quantize(x, x_gs)
|
x_q, x_sf = fp4_quantize(x, x_gs)
|
||||||
x_dequant = dequantize_nvfp4_to_dtype(
|
x_dequant = dequantize_nvfp4_to_dtype(x_q, x_sf, x_gs, torch.float32)
|
||||||
x_q, x_sf, x_gs, torch.float32, x.device
|
|
||||||
)
|
|
||||||
ref = x_dequant @ w_dequant.T
|
ref = x_dequant @ w_dequant.T
|
||||||
|
assert_output_close(self, out, ref, rtol=5e-2, atol=5e-2)
|
||||||
|
|
||||||
self.assertEqual(out.shape, (m, n))
|
def test_merged_shards(self):
|
||||||
cos = torch.nn.functional.cosine_similarity(
|
torch.manual_seed(7)
|
||||||
out.float().flatten(), ref.flatten(), dim=0
|
with mock.patch.object(
|
||||||
).item()
|
fp4_utils,
|
||||||
self.assertGreater(cos, 0.99)
|
"FP4_GEMM_RUNNER_BACKEND",
|
||||||
torch.testing.assert_close(out.float(), ref, rtol=5e-2, atol=5e-2)
|
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):
|
def test_flashinfer_cutedsl(self):
|
||||||
self._run_backend("flashinfer_cutedsl")
|
self._run_backend("flashinfer_cutedsl")
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
"""Numerics for the NVFP4 FusedMoE runner backends (--moe-runner-backend).
|
"""Numerics for the NVFP4 FusedMoE runner backends (--moe-runner-backend).
|
||||||
|
|
||||||
Runs the real FusedMoE layer path (construct -> fill NVFP4 checkpoint-format
|
Real FusedMoE path (NVFP4 checkpoint shards through the real weight_loader
|
||||||
weights -> process_weights_after_loading -> forward) per backend against a
|
-> weight processing -> forward) per backend vs a dequantized torch MoE
|
||||||
dequantized torch MoE reference, covering the per-backend weight preparation
|
reference. Single GPU, tp=ep=1.
|
||||||
(TRTLLM shuffle / CUTLASS swizzle / CuteDSL v2 interleave + MMA blockscales)
|
|
||||||
and the MoE runner dispatch. Single GPU, tp=ep=1.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import torch
|
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.runtime_context import get_context, get_flags, get_parallel
|
||||||
from sglang.srt.utils import get_device_sm
|
from sglang.srt.utils import get_device_sm
|
||||||
from sglang.test.ci.ci_register import register_cuda_ci
|
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
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
register_cuda_ci(est_time=120, stage="base-b", runner_config="4-gpu-b200")
|
register_cuda_ci(est_time=120, stage="base-b", runner_config="4-gpu-b200")
|
||||||
|
|
||||||
E, H, I, TOPK, M = 8, 1024, 1024, 2, 32
|
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+")
|
@unittest.skipIf(get_device_sm() < 100, "NVFP4 MoE backends require SM100+")
|
||||||
class TestNvFp4MoeBackends(CustomTestCase):
|
class TestNvFp4MoeBackends(CustomTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
_init_single_process_dist()
|
init_single_process_dist(master_port=29631)
|
||||||
torch.set_default_device("cuda")
|
torch.set_default_device("cuda")
|
||||||
|
|
||||||
def _run_backend(self, backend: str):
|
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.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)
|
torch.manual_seed(7)
|
||||||
quant_config = ModelOptFp4Config(
|
quant_config = ModelOptFp4Config(
|
||||||
@@ -122,93 +66,78 @@ class TestNvFp4MoeBackends(CustomTestCase):
|
|||||||
gate_up_interleaved=False,
|
gate_up_interleaved=False,
|
||||||
).cuda()
|
).cuda()
|
||||||
|
|
||||||
w13_ref = torch.zeros(E, 2 * I, H, dtype=torch.float32, device="cuda")
|
# Checkpoint-format shards through the real weight_loader;
|
||||||
w2_ref = torch.zeros(E, H, I, dtype=torch.float32, device="cuda")
|
# gate/up placement stays the loader's job.
|
||||||
for e in range(E):
|
refs = {
|
||||||
w13 = torch.randn(2 * I, H, dtype=torch.bfloat16, device="cuda") / 10
|
"w1": torch.zeros(E, I, H, dtype=torch.float32, device="cuda"),
|
||||||
w2 = torch.randn(H, I, dtype=torch.bfloat16, device="cuda") / 10
|
"w3": torch.zeros(E, I, H, dtype=torch.float32, device="cuda"),
|
||||||
w13_gs = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / w13.abs().max().float()
|
"w2": torch.zeros(E, H, I, dtype=torch.float32, device="cuda"),
|
||||||
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)
|
|
||||||
act_scale = 1.0 / (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX)
|
act_scale = 1.0 / (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX)
|
||||||
layer.w13_input_scale.data.fill_(act_scale)
|
for e in range(E):
|
||||||
layer.w2_input_scale.data.fill_(act_scale)
|
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)
|
layer.quant_method.process_weights_after_loading(layer)
|
||||||
|
|
||||||
x = torch.randn(M, H, dtype=torch.bfloat16, device="cuda") / 10
|
x = torch.randn(M, H, dtype=torch.bfloat16, device="cuda") / 10
|
||||||
router_logits = torch.randn(M, E, dtype=torch.float32, device="cuda")
|
router_logits = torch.randn(M, E, dtype=torch.float32, device="cuda")
|
||||||
weights = torch.softmax(router_logits, dim=-1)
|
# Route through the real topk compute path, not a hand-rolled one.
|
||||||
topk_weights, topk_ids = torch.topk(weights, TOPK, dim=-1)
|
topk_output = select_experts(
|
||||||
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
|
hidden_states=x,
|
||||||
|
|
||||||
out = layer.forward(
|
|
||||||
x,
|
|
||||||
StandardTopKOutput(
|
|
||||||
topk_weights=topk_weights,
|
|
||||||
topk_ids=topk_ids.to(torch.int32),
|
|
||||||
router_logits=router_logits,
|
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):
|
if not isinstance(out, torch.Tensor):
|
||||||
out = out[0] if isinstance(out, tuple) else out.hidden_states
|
out = out[0] if isinstance(out, tuple) else out.hidden_states
|
||||||
|
|
||||||
ref = self._torch_moe_reference(
|
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"]
|
||||||
)
|
)
|
||||||
|
assert_output_close(self, out, ref)
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@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
|
from flashinfer import fp4_quantize
|
||||||
|
|
||||||
def quant_roundtrip(t2d, gs):
|
def quant_roundtrip(t2d, gs):
|
||||||
q, sf = fp4_quantize(t2d.to(torch.bfloat16), 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
|
# Mirror the kernels' two fp4 activation round trips (input and
|
||||||
# NVFP4; mirror both round trips or the comparison carries ~7% noise.
|
# GEMM1->GEMM2) or the comparison carries ~7% noise. The reference
|
||||||
|
# stays in checkpoint semantics (w1=gate, w3=up).
|
||||||
act_gs = torch.tensor(
|
act_gs = torch.tensor(
|
||||||
FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX, dtype=torch.float32, device="cuda"
|
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)
|
x_dq = quant_roundtrip(x.float(), act_gs)
|
||||||
m, h = x.shape
|
m, h = x.shape
|
||||||
ref = torch.zeros(m, h, dtype=torch.float32, device="cuda")
|
ref = torch.zeros(m, h, dtype=torch.float32, device="cuda")
|
||||||
for t in range(m):
|
for t in range(m):
|
||||||
for j in range(topk_ids.shape[1]):
|
for j in range(topk_ids.shape[1]):
|
||||||
e = int(topk_ids[t, j])
|
e = int(topk_ids[t, j])
|
||||||
gu = x_dq[t] @ w13_ref[e].T
|
gate = x_dq[t] @ w1_ref[e].T
|
||||||
if up_first:
|
up = x_dq[t] @ w3_ref[e].T
|
||||||
up, gate = gu[:I], gu[I:]
|
|
||||||
else:
|
|
||||||
gate, up = gu[:I], gu[I:]
|
|
||||||
act = torch.nn.functional.silu(gate) * up
|
act = torch.nn.functional.silu(gate) * up
|
||||||
act_dq = quant_roundtrip(act.unsqueeze(0), act_gs)[0]
|
act_dq = quant_roundtrip(act.unsqueeze(0), act_gs)[0]
|
||||||
ref[t] += float(topk_weights[t, j]) * (act_dq @ w2_ref[e].T)
|
ref[t] += float(topk_weights[t, j]) * (act_dq @ w2_ref[e].T)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ GPU dependency. State is stored in a mock centralized pool that mirrors the
|
|||||||
``HybridReqToTokenPool`` / ``MambaPool`` interface used at serving time.
|
``HybridReqToTokenPool`` / ``MambaPool`` interface used at serving time.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import unittest
|
import unittest
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -31,55 +30,16 @@ from typing import List, Optional
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from sglang.test.ci.ci_register import register_cpu_ci
|
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
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
register_cpu_ci(est_time=30, suite="base-a-test-cpu")
|
register_cpu_ci(est_time=30, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_dist_initialized() -> None:
|
def _ensure_dist_initialized() -> None:
|
||||||
"""Set up a minimal single-rank gloo distributed environment plus the
|
"""CCA reads the TP rank / world size inside ``__init__`` to size its
|
||||||
SGLang model-parallel groups (TP=1, PP=1, EP=1). The CCA module reads
|
head-parallel projections, so the groups must exist before construction."""
|
||||||
``get_tensor_model_parallel_rank()`` / ``get_tensor_model_parallel_world_size()``
|
init_single_process_dist()
|
||||||
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
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -202,11 +162,6 @@ def _mock_pool_context(pool: _MockReqToTokenPool):
|
|||||||
set_forward_context(prev)
|
set_forward_context(prev)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Helper factories
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def _make_forward_batch(
|
def _make_forward_batch(
|
||||||
*,
|
*,
|
||||||
is_decode: bool,
|
is_decode: bool,
|
||||||
|
|||||||
Reference in New Issue
Block a user