[FlashInfer v0.6.16] Support FlashInfer CuTe DSL NVFP4 MoE quantization (#28354)
This commit is contained in:
@@ -761,9 +761,14 @@ SGLang supports various environment variables that can be used to configure its
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION</code></td>
|
||||
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Enable FlashInfer TRTLLM NVFP4 per-token activation scaling for serialized <code>modelopt_fp4</code> checkpoints; checkpoint FP32 activation scales are treated as <code>1</code></td>
|
||||
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Enable FlashInfer TRT-LLM or CuTe DSL v2 (no A2A or FlashInfer A2A) per-token FP32 activation scaling for serialized <code>modelopt_fp4</code> checkpoints; checkpoint activation scales are treated as <code>1</code></td>
|
||||
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}><code>false</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_FLASHINFER_MOE_FUSED_FINALIZE</code></td>
|
||||
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Use FlashInfer's fused atomic CUTLASS and CuTe DSL MoE finalize for best performance. Deterministic inference overrides this to <code>false</code>.</td>
|
||||
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}><code>true</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>FLASHINFER_NVFP4_4OVER6</code></td>
|
||||
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Enable FlashInfer NVFP4 4over6 scaling for NVFP4 per-token activation and online NVFP4 MoE weight quantization paths</td>
|
||||
|
||||
@@ -2396,11 +2396,12 @@ def _moe_runner_backend_quant_constraints(view: Any) -> dict:
|
||||
elif moe_runner_backend not in [
|
||||
"flashinfer_trtllm",
|
||||
"flashinfer_trtllm_routed",
|
||||
"flashinfer_cutedsl",
|
||||
]:
|
||||
raise ValueError(
|
||||
"--quantization nvfp4_online supports only "
|
||||
"--moe-runner-backend flashinfer_trtllm or "
|
||||
"flashinfer_trtllm_routed."
|
||||
"flashinfer_trtllm_routed, or flashinfer_cutedsl."
|
||||
)
|
||||
# Ascend runs MXFP8 MoE on the Ascend runner; every backend selected below is
|
||||
# CUDA/ROCm-only. Forcing one here would not merely pick the wrong runner:
|
||||
|
||||
@@ -1466,7 +1466,7 @@ class ModelConfig:
|
||||
# so eligible MoE experts are requantized online.
|
||||
"modelopt_fp4": ["modelopt", "fp8"],
|
||||
"modelopt_mixed": ["modelopt"],
|
||||
"nvfp4_online": ["fp8"],
|
||||
"nvfp4_online": ["fp8", "modelopt_fp8"],
|
||||
"petit_nvfp4": ["modelopt"],
|
||||
"w8a8_int8": ["compressed-tensors", "compressed_tensors"],
|
||||
"w8a8_fp8": ["compressed-tensors", "compressed_tensors"],
|
||||
|
||||
@@ -866,11 +866,14 @@ class Envs:
|
||||
# Per-rank dispatch capacity of the FlashInfer MoE A2A dispatcher. Unset
|
||||
# means each call site keeps its own default.
|
||||
SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK = EnvInt(None)
|
||||
# Enable NVFP4 per-token activation scaling path for FlashInfer TRT-LLM MoE.
|
||||
# Enable per-token FP32 activation scaling for serialized ModelOpt FP4 with
|
||||
# FlashInfer TRT-LLM or CuTe DSL v2 MoE.
|
||||
SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION = EnvBool(False)
|
||||
# Launch the TRT-LLM MoE grouped GEMMs with PDL only at or below this
|
||||
# token count.
|
||||
SGLANG_TRTLLM_MOE_PDL_MAX_TOKENS = EnvInt(8192)
|
||||
# Use FlashInfer's fused atomic CUTLASS/CuTe DSL MoE finalize.
|
||||
SGLANG_FLASHINFER_MOE_FUSED_FINALIZE = EnvBool(True)
|
||||
# Master switch for the experimental TRT-LLM LoRA fast path; when OFF (default) every
|
||||
# fine-grained opt switch reads False, keeping non-experimental paths byte-identical.
|
||||
SGLANG_EXPERIMENTAL_LORA_OPTI = EnvBool(False)
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.moe.moe_runner.base import (
|
||||
MoeQuantInfo,
|
||||
MoeRunnerConfig,
|
||||
@@ -237,6 +238,64 @@ def _cutedsl_wrapper_activation_type(activation: str, activation_type_cls: Any)
|
||||
)
|
||||
|
||||
|
||||
def _make_per_token_global_scale(input_tensor: torch.Tensor) -> torch.Tensor:
|
||||
from flashinfer.quantization.nvfp4_quantization_utils import (
|
||||
current_nvfp4_4over6_config,
|
||||
make_nvfp4_global_scale,
|
||||
)
|
||||
|
||||
return make_nvfp4_global_scale(
|
||||
input_tensor,
|
||||
per_token_activation=True,
|
||||
nvfp4_4over6_config=current_nvfp4_4over6_config(),
|
||||
)
|
||||
|
||||
|
||||
def refresh_cutedsl_standard_scales_for_weight_update(
|
||||
layer: torch.nn.Module,
|
||||
) -> None:
|
||||
w1_alpha, fc2_input_scale, w2_alpha, used_input_scale = (
|
||||
resolve_cutedsl_standard_scales(layer)
|
||||
)
|
||||
if layer.quant_config.use_per_token_activation:
|
||||
used_input_scale = _make_per_token_global_scale(used_input_scale)
|
||||
|
||||
new_scales = (w1_alpha, fc2_input_scale, w2_alpha)
|
||||
current_scales = layer._cutedsl_scales
|
||||
current_input_scale = layer._cutedsl_input_scale
|
||||
|
||||
# Decode CUDA graphs capture these tensor addresses, so reloads must update
|
||||
# their values without replacing the tensors.
|
||||
if (
|
||||
not isinstance(current_scales, tuple)
|
||||
or len(current_scales) != len(new_scales)
|
||||
or not isinstance(current_input_scale, torch.Tensor)
|
||||
):
|
||||
raise RuntimeError(
|
||||
"CuTe DSL scale metadata changed during weight reload; "
|
||||
"CUDA graph recapture is required."
|
||||
)
|
||||
scale_pairs = (
|
||||
*zip(current_scales, new_scales),
|
||||
(current_input_scale, used_input_scale),
|
||||
)
|
||||
for current, new in scale_pairs:
|
||||
if (
|
||||
not isinstance(current, torch.Tensor)
|
||||
or current.shape != new.shape
|
||||
or current.dtype != new.dtype
|
||||
or current.device != new.device
|
||||
):
|
||||
raise RuntimeError(
|
||||
"CuTe DSL scale metadata changed during weight reload; "
|
||||
"CUDA graph recapture is required."
|
||||
)
|
||||
|
||||
with torch.no_grad():
|
||||
for current, new in scale_pairs:
|
||||
current.copy_(new)
|
||||
|
||||
|
||||
def ensure_cutedsl_wrapper(layer: torch.nn.Module) -> None:
|
||||
"""Lazily create CuteDslMoEWrapper and resolve scales on first forward.
|
||||
|
||||
@@ -246,9 +305,10 @@ def ensure_cutedsl_wrapper(layer: torch.nn.Module) -> None:
|
||||
typically runs during the autotune dummy forward under inference_mode().
|
||||
We wrap the creation in inference_mode(False) so that those pre-allocated
|
||||
buffers are normal tensors -- inference tensors cannot be inplace-updated
|
||||
during later CUDA graph capture, which runs outside inference_mode.
|
||||
during later CUDA graph capture, which runs outside inference_mode. The
|
||||
resolved scale tensors share this scope because reload updates them in place.
|
||||
"""
|
||||
if getattr(layer, "_cutedsl_wrapper", None) is not None:
|
||||
if layer._cutedsl_wrapper is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -296,14 +356,17 @@ def ensure_cutedsl_wrapper(layer: torch.nn.Module) -> None:
|
||||
local_expert_offset=layer.moe_ep_rank * layer.num_local_experts,
|
||||
output_dtype=layer.moe_runner_config.params_dtype,
|
||||
device=str(layer.w13_weight.device),
|
||||
use_fused_finalize=envs.SGLANG_FLASHINFER_MOE_FUSED_FINALIZE.get(),
|
||||
activation_type=_cutedsl_wrapper_activation_type(
|
||||
layer.moe_runner_config.activation, ActivationType
|
||||
),
|
||||
)
|
||||
|
||||
w1_alpha, fc2_input_scale, w2_alpha, used_input_scale = (
|
||||
resolve_cutedsl_standard_scales(layer)
|
||||
)
|
||||
w1_alpha, fc2_input_scale, w2_alpha, used_input_scale = (
|
||||
resolve_cutedsl_standard_scales(layer)
|
||||
)
|
||||
if layer.quant_config.use_per_token_activation:
|
||||
used_input_scale = _make_per_token_global_scale(used_input_scale)
|
||||
layer._cutedsl_scales = (w1_alpha, fc2_input_scale, w2_alpha)
|
||||
layer._cutedsl_input_scale = used_input_scale
|
||||
|
||||
@@ -356,6 +419,9 @@ class CuteDslFp4MoeQuantInfo(MoeQuantInfo):
|
||||
# v1 only: True when DeepEP pre-quantizes activations to NVFP4.
|
||||
use_nvfp4_dispatch: bool = False
|
||||
|
||||
# v2 only: quantize hidden states with per-token dynamic activation scales.
|
||||
use_per_token_activation: bool = False
|
||||
|
||||
# v1 only: SBO down-GEMM overlap args.
|
||||
down_gemm_overlap_args: Optional[DownGemmOverlapArgs] = None
|
||||
|
||||
@@ -385,11 +451,29 @@ def fused_experts_none_to_flashinfer_cutedsl_fp4(
|
||||
if topk_ids.dtype != torch.int32:
|
||||
topk_ids = topk_ids.to(torch.int32)
|
||||
|
||||
x_fp4, x_sf = fp4_quantize(
|
||||
hidden_states,
|
||||
quant_info.a1_scale,
|
||||
sf_vec_size=_FP4_SF_VEC_SIZE,
|
||||
is_sf_swizzled_layout=False,
|
||||
if quant_info.use_per_token_activation:
|
||||
from flashinfer import SfLayout, nvfp4_quantize
|
||||
|
||||
x_fp4, x_sf, per_token_scale = nvfp4_quantize(
|
||||
hidden_states,
|
||||
quant_info.a1_scale,
|
||||
sfLayout=SfLayout.layout_linear,
|
||||
per_token_activation=True,
|
||||
backend="cute-dsl",
|
||||
)
|
||||
else:
|
||||
x_fp4, x_sf = fp4_quantize(
|
||||
hidden_states,
|
||||
quant_info.a1_scale,
|
||||
sf_vec_size=_FP4_SF_VEC_SIZE,
|
||||
is_sf_swizzled_layout=False,
|
||||
)
|
||||
per_token_scale = None
|
||||
|
||||
seq_len, hidden_size = hidden_states.shape
|
||||
x_fp4 = x_fp4.reshape(seq_len, hidden_size // 2)
|
||||
x_sf = x_sf.view(torch.float8_e4m3fn).reshape(
|
||||
seq_len, hidden_size // _FP4_SF_VEC_SIZE
|
||||
)
|
||||
|
||||
output = quant_info.wrapper.run(
|
||||
@@ -404,6 +488,7 @@ def fused_experts_none_to_flashinfer_cutedsl_fp4(
|
||||
w2_weight=quant_info.w2_weight,
|
||||
w2_weight_sf=quant_info.w2_weight_sf,
|
||||
w2_alpha=quant_info.w2_alpha,
|
||||
per_token_scale=per_token_scale,
|
||||
)
|
||||
|
||||
return StandardCombineInput(hidden_states=output)
|
||||
@@ -444,14 +529,38 @@ def fused_experts_flashinfer_to_flashinfer_cutedsl_fp4(
|
||||
topk_ids = topk_ids.to(torch.int32)
|
||||
|
||||
if x_sf is not None:
|
||||
if quant_info.use_per_token_activation:
|
||||
raise ValueError(
|
||||
"flashinfer_cutedsl per-token activation requires BF16 dispatch "
|
||||
"so the runner can forward per_token_scale to FlashInfer."
|
||||
)
|
||||
# NVFP4 dispatch, inputs are already quantized.
|
||||
x_fp4 = hidden_states
|
||||
per_token_scale = None
|
||||
else:
|
||||
x_fp4, x_sf = fp4_quantize(
|
||||
hidden_states,
|
||||
quant_info.a1_scale,
|
||||
sf_vec_size=_FP4_SF_VEC_SIZE,
|
||||
is_sf_swizzled_layout=False,
|
||||
if quant_info.use_per_token_activation:
|
||||
from flashinfer import SfLayout, nvfp4_quantize
|
||||
|
||||
x_fp4, x_sf, per_token_scale = nvfp4_quantize(
|
||||
hidden_states,
|
||||
quant_info.a1_scale,
|
||||
sfLayout=SfLayout.layout_linear,
|
||||
per_token_activation=True,
|
||||
backend="cute-dsl",
|
||||
)
|
||||
else:
|
||||
x_fp4, x_sf = fp4_quantize(
|
||||
hidden_states,
|
||||
quant_info.a1_scale,
|
||||
sf_vec_size=_FP4_SF_VEC_SIZE,
|
||||
is_sf_swizzled_layout=False,
|
||||
)
|
||||
per_token_scale = None
|
||||
|
||||
seq_len, hidden_size = hidden_states.shape
|
||||
x_fp4 = x_fp4.reshape(seq_len, hidden_size // 2)
|
||||
x_sf = x_sf.view(torch.float8_e4m3fn).reshape(
|
||||
seq_len, hidden_size // _FP4_SF_VEC_SIZE
|
||||
)
|
||||
|
||||
output = quant_info.wrapper.run(
|
||||
@@ -466,6 +575,7 @@ def fused_experts_flashinfer_to_flashinfer_cutedsl_fp4(
|
||||
w2_weight=quant_info.w2_weight,
|
||||
w2_weight_sf=quant_info.w2_weight_sf,
|
||||
w2_alpha=quant_info.w2_alpha,
|
||||
per_token_scale=per_token_scale,
|
||||
)
|
||||
|
||||
# Note: output contains routed expert results; shared_expert is handled separately
|
||||
|
||||
@@ -18,6 +18,7 @@ from sglang.srt.distributed import get_tp_group
|
||||
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
||||
use_symmetric_memory,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.dp_attention import is_allocation_symmetric
|
||||
from sglang.srt.layers.moe.moe_runner.base import (
|
||||
MoeQuantInfo,
|
||||
@@ -235,6 +236,7 @@ def _run_flashinfer_cutlass(
|
||||
tune_max_num_tokens=next_power_of_2(x.shape[0]),
|
||||
activation_type=_activation_type(runner_config),
|
||||
enable_alltoall=enable_alltoall,
|
||||
use_fused_finalize=envs.SGLANG_FLASHINFER_MOE_FUSED_FINALIZE.get(),
|
||||
)[0]
|
||||
|
||||
if quant_info.quant_type in ("bf16", "fp8"):
|
||||
@@ -387,6 +389,7 @@ def fused_experts_none_to_flashinfer_mxfp4(
|
||||
activation_type=ActivationType.Swiglu,
|
||||
tune_max_num_tokens=next_power_of_2(x.shape[0]),
|
||||
output=out,
|
||||
use_fused_finalize=envs.SGLANG_FLASHINFER_MOE_FUSED_FINALIZE.get(),
|
||||
)
|
||||
|
||||
if do_pad:
|
||||
|
||||
@@ -185,6 +185,9 @@ class QuantizationConfig(ABC):
|
||||
if hf_quant_config is None:
|
||||
return None
|
||||
|
||||
if user_quant == "nvfp4_online":
|
||||
return None
|
||||
|
||||
# Check if this is a ModelOpt config
|
||||
quant_algo = hf_quant_config.get("quant_algo", "").upper()
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ class BaseKVCacheMethod(QuantizeMethodBase):
|
||||
if is_fp8_fnuz():
|
||||
k_scale *= 2
|
||||
v_scale *= 2
|
||||
elif layer.k_scale < 0.0 and layer.v_scale < 0.0:
|
||||
# If no scales were loaded (both scales are invalid negative
|
||||
elif layer.k_scale <= 0.0 and layer.v_scale <= 0.0:
|
||||
# If no scales were loaded (both scales are invalid non-positive
|
||||
# values), use the default value of 1.0
|
||||
k_scale = 1.0
|
||||
v_scale = 1.0
|
||||
|
||||
@@ -1330,7 +1330,8 @@ class ModelOptFp4Config(ModelOptQuantConfig):
|
||||
and checkpoint-provided scales.
|
||||
- Serialized + per-token FP32 activation scales: set
|
||||
`SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION=1`; use
|
||||
`flashinfer_trtllm` or `flashinfer_trtllm_routed`.
|
||||
`flashinfer_trtllm`, `flashinfer_trtllm_routed`, or `flashinfer_cutedsl`
|
||||
v2 with no A2A or FlashInfer A2A.
|
||||
- BF16/FP16/FP8 MoE + per-tensor FP32 activation scales: quantize expert
|
||||
weights on load, keep dense weights in source precision or FP8, and use
|
||||
1.0 when the checkpoint has no NVFP4 activation scale.
|
||||
@@ -2498,13 +2499,14 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
|
||||
)
|
||||
|
||||
# TODO: for flashinfer always do MOE_NVFP4_DISPATCH
|
||||
use_dispatch_fp4 = not self.quant_config.use_per_token_activation and (
|
||||
MOE_NVFP4_DISPATCH or should_use_flashinfer_cutlass_moe_fp4_allgather()
|
||||
)
|
||||
|
||||
layer.dispatcher.set_quant_config(
|
||||
{
|
||||
"input_global_scale": (
|
||||
layer.w13_input_scale_quant
|
||||
if MOE_NVFP4_DISPATCH
|
||||
or should_use_flashinfer_cutlass_moe_fp4_allgather()
|
||||
else None
|
||||
layer.w13_input_scale_quant if use_dispatch_fp4 else None
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -2565,17 +2567,19 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
|
||||
interleave_w13_halves,
|
||||
)
|
||||
|
||||
layer.w13_weight = Parameter(
|
||||
copy_or_rebind_param(
|
||||
layer,
|
||||
"w13_weight",
|
||||
interleave_w13_halves(
|
||||
layer.w13_weight.view(torch.uint8), group_size=64, dim=1
|
||||
).contiguous(),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.w13_weight_scale = Parameter(
|
||||
copy_or_rebind_param(
|
||||
layer,
|
||||
"w13_weight_scale",
|
||||
interleave_w13_halves(
|
||||
layer.w13_weight_scale, group_size=64, dim=1
|
||||
).contiguous(),
|
||||
requires_grad=False,
|
||||
)
|
||||
|
||||
# Process w13 weights
|
||||
@@ -2636,6 +2640,7 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
|
||||
|
||||
from sglang.srt.layers.moe.moe_runner.flashinfer_cutedsl import (
|
||||
_FP4_SF_VEC_SIZE,
|
||||
refresh_cutedsl_standard_scales_for_weight_update,
|
||||
)
|
||||
|
||||
sf_vec_size = _FP4_SF_VEC_SIZE
|
||||
@@ -2644,7 +2649,9 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
|
||||
w13_k = layer.w13_weight.shape[2] * 2
|
||||
w2_m = layer.w2_weight.shape[1]
|
||||
w2_k = layer.w2_weight.shape[2] * 2
|
||||
layer.w13_blockscale_mma = Parameter(
|
||||
copy_or_rebind_param(
|
||||
layer,
|
||||
"w13_blockscale_mma",
|
||||
convert_sf_to_mma_layout(
|
||||
layer.w13_blockscale_swizzled.contiguous()
|
||||
.view(torch.uint8)
|
||||
@@ -2654,9 +2661,10 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
|
||||
num_groups=num_local_experts,
|
||||
sf_vec_size=sf_vec_size,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.w2_blockscale_mma = Parameter(
|
||||
copy_or_rebind_param(
|
||||
layer,
|
||||
"w2_blockscale_mma",
|
||||
convert_sf_to_mma_layout(
|
||||
layer.w2_blockscale_swizzled.contiguous()
|
||||
.view(torch.uint8)
|
||||
@@ -2666,8 +2674,9 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
|
||||
num_groups=num_local_experts,
|
||||
sf_vec_size=sf_vec_size,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
if layer._cutedsl_wrapper is not None:
|
||||
refresh_cutedsl_standard_scales_for_weight_update(layer)
|
||||
|
||||
@property
|
||||
def load_up_proj_weight_first(self) -> bool:
|
||||
@@ -2696,6 +2705,8 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
|
||||
if moe_runner_backend.is_flashinfer_cutedsl():
|
||||
import sglang.srt.layers.moe.moe_runner.flashinfer_cutedsl # noqa: F401 – triggers @register_fused_func
|
||||
|
||||
layer._cutedsl_wrapper = None
|
||||
|
||||
if moe_runner_backend.is_flashinfer_cutlass():
|
||||
import sglang.srt.layers.moe.moe_runner.flashinfer_cutlass # noqa: F401
|
||||
|
||||
@@ -2840,6 +2851,7 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
|
||||
a1_scale=layer._cutedsl_input_scale,
|
||||
a2_scale=fc2_input_scale,
|
||||
wrapper=layer._cutedsl_wrapper,
|
||||
use_per_token_activation=self.quant_config.use_per_token_activation,
|
||||
)
|
||||
return self.runner.run(dispatch_output, quant_info)
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@ class NvFp4OnlineConfig(ModelOptQuantConfig):
|
||||
use_mxfp8: bool = False,
|
||||
) -> None:
|
||||
source_ignored_layers = self._normalize_ignored_layers(exclude_modules)
|
||||
fp4_ignored_layers = list(source_ignored_layers)
|
||||
fp4_ignored_layers = (
|
||||
[] if self._use_per_token_activation else list(source_ignored_layers)
|
||||
)
|
||||
if ignored_layers_str := envs.SGLANG_FP4_IGNORED_LAYERS.get():
|
||||
fp4_ignored_layers.extend(
|
||||
layer.strip()
|
||||
@@ -114,8 +116,10 @@ class NvFp4OnlineConfig(ModelOptQuantConfig):
|
||||
quant_method = str(config.get("quant_method", "")).lower()
|
||||
use_mxfp8 = "mxfp8" in quant_method
|
||||
is_checkpoint_fp8_serialized = "fp8" in quant_method or use_mxfp8
|
||||
ignored_layers = config.get("ignored_layers") or config.get(
|
||||
"modules_to_not_convert"
|
||||
ignored_layers = (
|
||||
config.get("ignore")
|
||||
or config.get("ignored_layers")
|
||||
or config.get("modules_to_not_convert")
|
||||
)
|
||||
if isinstance(ignored_layers, str):
|
||||
ignored_layers = [ignored_layers]
|
||||
@@ -142,14 +146,15 @@ class NvFp4OnlineConfig(ModelOptQuantConfig):
|
||||
return Fp8LinearMethod(self)
|
||||
return UnquantizedLinearMethod()
|
||||
if isinstance(layer, FusedMoE):
|
||||
if is_layer_skipped(
|
||||
source_layer_ignored = is_layer_skipped(
|
||||
prefix, self.exclude_modules, self.packed_modules_mapping
|
||||
) or self.is_layer_excluded(prefix):
|
||||
) or self.is_layer_excluded(prefix)
|
||||
if source_layer_ignored and not self.use_per_token_activation:
|
||||
return None
|
||||
if is_layer_skipped(
|
||||
prefix, self.fp4_ignored_layers, self.packed_modules_mapping
|
||||
):
|
||||
if self.is_checkpoint_fp8_serialized:
|
||||
if self.is_checkpoint_fp8_serialized and not source_layer_ignored:
|
||||
return Fp8MoEMethod(self)
|
||||
return None
|
||||
return ModelOptNvFp4OnlineFusedMoEMethod(self, prefix)
|
||||
@@ -201,15 +206,15 @@ class ModelOptNvFp4OnlineFusedMoEMethod(ModelOptNvFp4FusedMoEMethod):
|
||||
if layer_match is not None
|
||||
else layer_prefix
|
||||
)
|
||||
if (
|
||||
quant_config.use_per_token_activation
|
||||
and not self.enable_flashinfer_trtllm_moe
|
||||
if quant_config.use_per_token_activation and not (
|
||||
self.enable_flashinfer_trtllm_moe or self._is_cutedsl_v2_standard
|
||||
):
|
||||
raise ValueError(
|
||||
"--quantization nvfp4_online requires online per-token FP32 "
|
||||
"activation scales and supports only flashinfer_trtllm or "
|
||||
"flashinfer_trtllm_routed. Use --quantization modelopt_fp4 "
|
||||
"for per-tensor FP32 activation scales."
|
||||
"activation scales and supports flashinfer_trtllm, "
|
||||
"flashinfer_trtllm_routed, or flashinfer_cutedsl with no A2A "
|
||||
"or FlashInfer A2A. Use --quantization modelopt_fp4 for "
|
||||
"per-tensor FP32 activation scales."
|
||||
)
|
||||
|
||||
def prepare_weight_loader(self, layer, weight_loader):
|
||||
|
||||
@@ -6670,9 +6670,9 @@ class ServerArgs:
|
||||
if view.moe_runner_backend == "flashinfer_cutedsl":
|
||||
# modelopt_mixed with non-NVFP4 MoE layers is rejected at load time.
|
||||
assert (
|
||||
view.quantization in ["modelopt_fp4", "modelopt_mixed"]
|
||||
view.quantization in ["modelopt_fp4", "modelopt_mixed", "nvfp4_online"]
|
||||
or self.get_model_config().nvfp4_moe_meta is not None
|
||||
), f"Invalid quantization '{view.quantization}'. \nFlashInfer CuteDSL MOE currently supports only: 'modelopt_fp4', 'modelopt_mixed' (with NVFP4 MoE layers), or hybrid NVFP4 models."
|
||||
), f"Invalid quantization '{view.quantization}'. \nFlashInfer CuteDSL MOE currently supports only: 'modelopt_fp4', 'modelopt_mixed' (with NVFP4 MoE layers), 'nvfp4_online', or hybrid NVFP4 models."
|
||||
assert view.ep_size in [
|
||||
1,
|
||||
self.tp_size,
|
||||
@@ -6685,6 +6685,14 @@ class ServerArgs:
|
||||
f"flashinfer_cutedsl supports moe_a2a_backend='none', 'deepep', or 'flashinfer', "
|
||||
f"got '{view.moe_a2a_backend}'."
|
||||
)
|
||||
if view.moe_a2a_backend == "deepep" and (
|
||||
view.quantization == "nvfp4_online"
|
||||
or envs.SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION.get()
|
||||
):
|
||||
raise ValueError(
|
||||
"flashinfer_cutedsl per-token NVFP4 activation requires "
|
||||
"moe_a2a_backend='none' or 'flashinfer'."
|
||||
)
|
||||
|
||||
if view.moe_runner_backend in ["flashinfer_trtllm", "experimental_sgl_trtllm"]:
|
||||
assert view.quantization in [
|
||||
@@ -7983,6 +7991,8 @@ class ServerArgs:
|
||||
"1" if self.enable_deterministic_inference else "0"
|
||||
)
|
||||
self._handle_custom_all_reduce_v2_multinode()
|
||||
if self.enable_deterministic_inference:
|
||||
envs.SGLANG_FLASHINFER_MOE_FUSED_FINALIZE.set("0")
|
||||
if self.debug_cuda_graph:
|
||||
if not (is_cuda() or is_hip()):
|
||||
logger.warning(
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.run_eval import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=800, stage="nightly", runner_config="4-gpu-b200")
|
||||
|
||||
|
||||
class FlashinferNvFp4OnlineMoeBackendBase:
|
||||
backend = None
|
||||
model = None
|
||||
extra_args = []
|
||||
extra_env = {}
|
||||
eval_args = {}
|
||||
spec_accept_length_threshold = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
env={**os.environ, **cls.extra_env, "SGLANG_ENABLE_JIT_DEEPGEMM": "False"},
|
||||
other_args=[
|
||||
*cls.extra_args,
|
||||
"--moe-runner-backend",
|
||||
cls.backend,
|
||||
"--cuda-graph-max-bs-decode",
|
||||
"128",
|
||||
"--tp-size",
|
||||
"4",
|
||||
"--ep-size",
|
||||
"4",
|
||||
"--quantization",
|
||||
"nvfp4_online",
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_gsm8k(self):
|
||||
args = SimpleNamespace(
|
||||
base_url=self.base_url,
|
||||
model=self.model,
|
||||
eval_name="gsm8k",
|
||||
num_examples=200,
|
||||
num_threads=128,
|
||||
**self.eval_args,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"{metrics=}")
|
||||
self.assertGreater(metrics["score"], 0.90)
|
||||
if self.spec_accept_length_threshold is not None:
|
||||
server_info = requests.get(self.base_url + "/server_info").json()
|
||||
avg_spec_accept_length = server_info["internal_states"][0][
|
||||
"avg_spec_accept_length"
|
||||
]
|
||||
print(f"{avg_spec_accept_length=}")
|
||||
self.assertGreater(
|
||||
avg_spec_accept_length, self.spec_accept_length_threshold
|
||||
)
|
||||
|
||||
|
||||
# Only this class is affected, but the file runs with failfast, so leaving it
|
||||
# enabled also cuts off the class that sorts after it.
|
||||
@unittest.skip(
|
||||
"flashinfer-ai/flashinfer#4486: on SM100/SM103 the TRTLLM_GEN tile-192 BMM "
|
||||
"path returns non-finite MoE output from FlashInfer 0.6.16.post4 on, so the "
|
||||
"first real prefill trips the sampler NaN assert and gsm8k scores 0.0. "
|
||||
"See #34629 for the package bisect."
|
||||
)
|
||||
class TestFlashinferTrtllmGenMoeBackendNvFp4Online(
|
||||
FlashinferNvFp4OnlineMoeBackendBase, CustomTestCase
|
||||
):
|
||||
backend = "flashinfer_trtllm"
|
||||
model = "Qwen/Qwen3-30B-A3B-Instruct-2507-FP8"
|
||||
eval_args = {"api": "completion", "max_tokens": 512}
|
||||
extra_env = {
|
||||
"FLASHINFER_NVFP4_4OVER6": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6_ERR_MODE": "MSE",
|
||||
"FLASHINFER_NVFP4_4OVER6_ERR_USE_FAST_MATH": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6_E4M3_USE_256": "1",
|
||||
"SGLANG_FP4_IGNORED_LAYERS": ",".join(
|
||||
["shared_expert"]
|
||||
+ [f"model.layers.{layer_id}" for layer_id in range(40, 48)]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class TestFlashinferCuteDSLMoeBackendNvFp4Online(
|
||||
FlashinferNvFp4OnlineMoeBackendBase, CustomTestCase
|
||||
):
|
||||
backend = "flashinfer_cutedsl"
|
||||
model = "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8"
|
||||
extra_args = [
|
||||
"--reasoning-parser",
|
||||
"nemotron_3",
|
||||
"--tool-call-parser",
|
||||
"qwen3_coder",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
]
|
||||
eval_args = {"max_tokens": 16000, "temperature": 1.0, "top_p": 0.95}
|
||||
spec_accept_length_threshold = 2.5
|
||||
extra_env = {
|
||||
"FLASHINFER_NVFP4_4OVER6": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6_ERR_MODE": "MSE",
|
||||
"FLASHINFER_NVFP4_4OVER6_ERR_USE_FAST_MATH": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6_E4M3_USE_256": "1",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -243,58 +243,6 @@ class FlashinferTrtllmGenMoeBackendNVFP4Base:
|
||||
self.assertGreater(metrics["score"], 0.89)
|
||||
|
||||
|
||||
class FlashinferTrtllmGenMoeBackendNvFp4OnlineBase:
|
||||
backend = None
|
||||
extra_env = {}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = "Qwen/Qwen3-Next-80B-A3B-Instruct-FP8"
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
env={**os.environ, **cls.extra_env, "SGLANG_ENABLE_JIT_DEEPGEMM": "False"},
|
||||
other_args=[
|
||||
"--attention-backend",
|
||||
"triton",
|
||||
"--moe-runner-backend",
|
||||
cls.backend,
|
||||
"--cuda-graph-max-bs-decode",
|
||||
"128",
|
||||
"--tp-size",
|
||||
"4",
|
||||
"--ep-size",
|
||||
"2",
|
||||
"--quantization",
|
||||
"nvfp4_online",
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
"--mamba-ssm-dtype",
|
||||
"bfloat16",
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_gsm8k(self):
|
||||
args = SimpleNamespace(
|
||||
base_url=self.base_url,
|
||||
model=self.model,
|
||||
eval_name="gsm8k",
|
||||
api="completion",
|
||||
max_tokens=512,
|
||||
num_examples=200,
|
||||
num_threads=128,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"{metrics=}")
|
||||
self.assertGreater(metrics["score"], 0.90)
|
||||
|
||||
|
||||
class TestFlashinferTrtllmGenMoeBackendFP8(
|
||||
FlashinferTrtllmGenMoeBackendFP8Base, CustomTestCase
|
||||
):
|
||||
@@ -325,6 +273,12 @@ class TestFlashinferTrtllmGenMoeBackendBF16Routed(
|
||||
backend = "flashinfer_trtllm_routed"
|
||||
|
||||
|
||||
@unittest.skip(
|
||||
"flashinfer-ai/flashinfer#4486: on SM100/SM103 the TRTLLM_GEN tile-192 BMM "
|
||||
"path returns non-finite MoE output from FlashInfer 0.6.16.post4 on, so the "
|
||||
"first real prefill trips the sampler NaN assert and gsm8k scores 0.0. "
|
||||
"See #34629 for the package bisect."
|
||||
)
|
||||
class TestFlashinferTrtllmGenMoeBackendNvFp4PerTokenActivationRouted(
|
||||
FlashinferTrtllmGenMoeBackendNVFP4Base, CustomTestCase
|
||||
):
|
||||
@@ -332,29 +286,5 @@ class TestFlashinferTrtllmGenMoeBackendNvFp4PerTokenActivationRouted(
|
||||
backend = "flashinfer_trtllm_routed"
|
||||
|
||||
|
||||
# Only this class is affected, but the file runs with failfast, so leaving it
|
||||
# enabled also cuts off the two classes that sort after it.
|
||||
@unittest.skip(
|
||||
"flashinfer-ai/flashinfer#4486: on SM100/SM103 the TRTLLM_GEN tile-192 BMM "
|
||||
"path returns non-finite MoE output from FlashInfer 0.6.16.post4 on, so the "
|
||||
"first real prefill trips the sampler NaN assert and gsm8k scores 0.0. "
|
||||
"See #34629 for the package bisect."
|
||||
)
|
||||
class TestFlashinferTrtllmGenMoeBackendNvFp4Online(
|
||||
FlashinferTrtllmGenMoeBackendNvFp4OnlineBase, CustomTestCase
|
||||
):
|
||||
extra_env = {
|
||||
"FLASHINFER_NVFP4_4OVER6": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6_ERR_MODE": "MSE",
|
||||
"FLASHINFER_NVFP4_4OVER6_ERR_USE_FAST_MATH": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6_E4M3_USE_256": "1",
|
||||
"SGLANG_FP4_IGNORED_LAYERS": ",".join(
|
||||
["shared_expert"]
|
||||
+ [f"model.layers.{layer_id}" for layer_id in range(40, 48)]
|
||||
),
|
||||
}
|
||||
backend = "flashinfer_trtllm"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -7,6 +7,11 @@ import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.constants import (
|
||||
GPU_MEMORY_TYPE_CUDA_GRAPH,
|
||||
GPU_MEMORY_TYPE_KV_CACHE,
|
||||
GPU_MEMORY_TYPE_WEIGHTS,
|
||||
)
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
@@ -44,10 +49,17 @@ class UpdateWeightsFromDiskBase:
|
||||
)
|
||||
|
||||
def _launch_server(self, backend_test_suite):
|
||||
launch_kwargs = {}
|
||||
if self.launch_env is not None:
|
||||
launch_kwargs["env"] = self.launch_env
|
||||
other_args = backend_test_suite.get("other_args")
|
||||
launch_kwargs = {
|
||||
"env": {
|
||||
"SGLANG_MEMORY_SAVER_CUDA_GRAPH": "1",
|
||||
**(self.launch_env or {}),
|
||||
}
|
||||
}
|
||||
other_args = (
|
||||
*backend_test_suite.get("other_args", ()),
|
||||
"--enable-memory-saver",
|
||||
"--cuda-graph-backend-prefill=disabled",
|
||||
)
|
||||
return popen_launch_server(
|
||||
self.model,
|
||||
self.base_url,
|
||||
@@ -140,6 +152,18 @@ class UpdateWeightsFromDiskBase:
|
||||
timeout=self.update_timeout,
|
||||
)
|
||||
|
||||
def _offload_engine_and_resume_weights(self):
|
||||
self._post_json("/release_memory_occupation", {})
|
||||
self._post_json(
|
||||
"/resume_memory_occupation", {"tags": [GPU_MEMORY_TYPE_WEIGHTS]}
|
||||
)
|
||||
|
||||
def _resume_kv_cache_and_cuda_graph(self):
|
||||
self._post_json(
|
||||
"/resume_memory_occupation",
|
||||
{"tags": [GPU_MEMORY_TYPE_KV_CACHE, GPU_MEMORY_TYPE_CUDA_GRAPH]},
|
||||
)
|
||||
|
||||
def test_parameterized_update_weights_from_disk(self):
|
||||
for backend_test_suite in self.backend_test_suites:
|
||||
case_name = backend_test_suite.get("name", "default")
|
||||
@@ -154,6 +178,7 @@ class UpdateWeightsFromDiskBase:
|
||||
for update_test_suite in self.update_test_suites:
|
||||
with self.subTest(case_name=case_name, **update_test_suite):
|
||||
self._wait_until_idle()
|
||||
self._offload_engine_and_resume_weights()
|
||||
ret = self._run_update_weights(
|
||||
self.model,
|
||||
flush_cache=update_test_suite["flush_cache"],
|
||||
@@ -161,6 +186,7 @@ class UpdateWeightsFromDiskBase:
|
||||
"abort_all_requests"
|
||||
],
|
||||
)
|
||||
self._resume_kv_cache_and_cuda_graph()
|
||||
self.assertTrue(ret.get("success"), f"{ret=}")
|
||||
self.assertEqual(self._get_model_info(), self.model)
|
||||
self._assert_non_empty_decode()
|
||||
@@ -169,7 +195,7 @@ class UpdateWeightsFromDiskBase:
|
||||
baseline_sig, updated_sig
|
||||
)
|
||||
finally:
|
||||
kill_process_tree(process.pid)
|
||||
kill_process_tree(process.pid, wait_timeout=60)
|
||||
|
||||
|
||||
class TestServerUpdateWeightsFromDiskMXFP8(UpdateWeightsFromDiskBase, CustomTestCase):
|
||||
@@ -179,8 +205,6 @@ class TestServerUpdateWeightsFromDiskMXFP8(UpdateWeightsFromDiskBase, CustomTest
|
||||
{
|
||||
"name": "flashinfer_trtllm_routed_mxfp8",
|
||||
"other_args": (
|
||||
"--base-gpu-id",
|
||||
"0",
|
||||
"--tp-size",
|
||||
"4",
|
||||
"--dp-size",
|
||||
@@ -202,8 +226,6 @@ class TestServerUpdateWeightsFromDiskNVFP4(UpdateWeightsFromDiskBase, CustomTest
|
||||
{
|
||||
"name": "flashinfer_trtllm_nvfp4",
|
||||
"other_args": (
|
||||
"--base-gpu-id",
|
||||
"0",
|
||||
"--tp-size",
|
||||
"4",
|
||||
"--fp4-gemm-backend",
|
||||
@@ -215,5 +237,37 @@ class TestServerUpdateWeightsFromDiskNVFP4(UpdateWeightsFromDiskBase, CustomTest
|
||||
)
|
||||
|
||||
|
||||
class TestServerUpdateWeightsFromDiskNVFP4CuteDSL(
|
||||
UpdateWeightsFromDiskBase, CustomTestCase
|
||||
):
|
||||
model = "nvidia/Qwen3-30B-A3B-NVFP4"
|
||||
launch_env = {
|
||||
"SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION": "1",
|
||||
"SGLANG_FLASHINFER_MOE_FUSED_FINALIZE": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6_ERR_MODE": "MSE",
|
||||
"FLASHINFER_NVFP4_4OVER6_ERR_USE_FAST_MATH": "1",
|
||||
"FLASHINFER_NVFP4_4OVER6_E4M3_USE_256": "1",
|
||||
}
|
||||
backend_test_suites = (
|
||||
{
|
||||
"name": "flashinfer_cutedsl_nvfp4",
|
||||
"other_args": (
|
||||
"--tp-size",
|
||||
"4",
|
||||
"--ep-size",
|
||||
"4",
|
||||
"--fp4-gemm-backend",
|
||||
"flashinfer_cutedsl",
|
||||
"--moe-runner-backend",
|
||||
"flashinfer_cutedsl",
|
||||
"--moe-a2a-backend",
|
||||
"none",
|
||||
"--enable-deterministic-inference",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user