# Copyright 2023-2024 SGLang Team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """Mega-MoE forward path and expert-weight prep shared by Deepseek V2/V4.""" from __future__ import annotations import os from contextlib import nullcontext from typing import TYPE_CHECKING, Optional import torch from sglang.jit_kernel.dsv4 import mega_moe_pre_dispatch from sglang.srt.environ import envs from sglang.srt.eplb.expert_location_dispatch import ExpertLocationDispatchInfo from sglang.srt.layers.dp_attention import get_dp_global_num_tokens from sglang.srt.layers.moe.utils import get_moe_a2a_backend from sglang.srt.model_executor.runner import get_is_capture_mode if TYPE_CHECKING: from deep_gemm import SymmBuffer from sglang.srt.model_executor.forward_batch_info import ForwardBatch from sglang.srt.models.deepseek_v2 import DeepseekV2MoE _MEGA_MOE_SYMM_BUFFER: dict = {} _MEGA_MOE_DG_ENV_APPLIED = False def _apply_mega_moe_dg_env() -> None: """Forward sglang's FP4/MXF4 opt-in flags to DeepGEMM via env vars. DeepGEMM reads `DG_USE_FP4_ACTS` (and `DG_USE_MXF4_KIND`) at host-function call time — both `get_symm_buffer_for_mega_moe` and `fp8_fp4_mega_moe`. Forwarding once at first use is sufficient (these are static config flags, not per-request state) and matches the `setdefault` pattern so explicit `DG_USE_*` overrides from outside still win. """ global _MEGA_MOE_DG_ENV_APPLIED if _MEGA_MOE_DG_ENV_APPLIED: return if envs.SGLANG_OPT_DEEPGEMM_MEGA_MOE_USE_FP4_ACTS.get(): os.environ.setdefault("DG_USE_FP4_ACTS", "1") if envs.SGLANG_OPT_DEEPGEMM_MEGA_MOE_USE_MXF4_KIND.get(): os.environ.setdefault("DG_USE_MXF4_KIND", "1") _MEGA_MOE_DG_ENV_APPLIED = True def _get_mega_moe_symm_buffer( group, num_experts: int, num_max_tokens_per_rank: int, num_topk: int, hidden: int, intermediate_hidden: int, ) -> SymmBuffer: import deep_gemm _apply_mega_moe_dg_env() key = ( id(group), num_max_tokens_per_rank, num_experts, num_topk, hidden, intermediate_hidden, ) buf = _MEGA_MOE_SYMM_BUFFER.get(key) if buf is None: buf = deep_gemm.get_symm_buffer_for_mega_moe( group, num_experts, num_max_tokens_per_rank, num_topk, hidden, intermediate_hidden, use_fp8_dispatch=True, activation="swiglu", ) _MEGA_MOE_SYMM_BUFFER[key] = buf return buf def should_use_mega_moe(moe: DeepseekV2MoE, hidden_states: torch.Tensor) -> bool: if not get_moe_a2a_backend().is_megamoe(): return False if not getattr(moe.experts, "_mega_moe_weights_built", False): return False if get_is_capture_mode(): return True global_num_tokens = get_dp_global_num_tokens() if global_num_tokens: max_tokens_per_rank = max(global_num_tokens) else: max_tokens_per_rank = hidden_states.shape[0] cap = envs.SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK.get() return max_tokens_per_rank <= cap def forward_mega_moe( moe: DeepseekV2MoE, hidden_states: torch.Tensor, forward_batch: Optional[ForwardBatch] = None, input_ids_global: Optional[torch.Tensor] = None, ) -> torch.Tensor: num_tokens = hidden_states.shape[0] sbo_overlap_flag = ( moe.alt_stream is not None and moe.num_fused_shared_experts == 0 and num_tokens > 0 and get_is_capture_mode() ) if sbo_overlap_flag: current_stream = torch.cuda.current_stream() moe.alt_stream.wait_stream(current_stream) shared_output = moe._forward_shared_experts(hidden_states) mega_stream_ctx = torch.cuda.stream(moe.alt_stream) else: shared_output = moe._forward_shared_experts(hidden_states) mega_stream_ctx = nullcontext() with mega_stream_ctx: y = _run_mega_routed( moe, hidden_states, forward_batch, input_ids_global, num_tokens ) if sbo_overlap_flag: current_stream.wait_stream(moe.alt_stream) if shared_output is not None: y.add_(shared_output) return y def _run_mega_routed( moe: DeepseekV2MoE, hidden_states: torch.Tensor, forward_batch: Optional[ForwardBatch], input_ids_global: Optional[torch.Tensor], num_tokens: int, ) -> torch.Tensor: import deep_gemm from sglang.srt.distributed.parallel_state import get_moe_ep_group hidden_size = moe.config.hidden_size if num_tokens > 0: router_logits = moe.gate(hidden_states, forward_batch=forward_batch) topk_kwargs = {"input_ids": input_ids_global} if moe.is_hash else {} topk_output = moe.topk( hidden_states, router_logits, num_token_non_padded=( forward_batch.num_token_non_padded if forward_batch is not None else None ), expert_location_dispatch_info=ExpertLocationDispatchInfo.init_new( layer_id=moe.layer_id, ), **topk_kwargs, ) topk_ids = topk_output.topk_ids topk_weights = topk_output.topk_weights else: topk_ids = None topk_weights = None ep_group = get_moe_ep_group().device_group num_experts = moe.experts.num_experts top_k = moe.config.num_experts_per_tok + moe.num_fused_shared_experts intermediate_size = moe.config.moe_intermediate_size num_max_tokens_per_rank = ( envs.SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK.get() ) assert num_tokens <= num_max_tokens_per_rank, ( f"mega MoE: num_tokens={num_tokens} exceeds cap " f"SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK=" f"{num_max_tokens_per_rank}; raise the env var or shrink " f"cuda_graph_max_bs / chunked_prefill_size accordingly" ) buf = _get_mega_moe_symm_buffer( ep_group, num_experts=num_experts, num_max_tokens_per_rank=num_max_tokens_per_rank, num_topk=top_k, hidden=hidden_size, intermediate_hidden=intermediate_size, ) if num_tokens > 0: topk_ids_in = topk_ids.to(torch.int32) topk_weights_in = topk_weights.to(torch.float32) else: topk_ids_in = hidden_states.new_empty((0, top_k), dtype=torch.int32) topk_weights_in = hidden_states.new_empty((0, top_k), dtype=torch.float32) use_fp4_acts = envs.SGLANG_OPT_DEEPGEMM_MEGA_MOE_USE_FP4_ACTS.get() if use_fp4_acts: # FP4 path goes through DeepGEMM's mega_moe_pre_dispatch which # handles the E2M1 packing variant. The jit implementation # only emits FP8. deep_gemm.mega_moe_pre_dispatch( hidden_states, topk_ids_in, topk_weights_in, buf.x, buf.x_sf, buf.topk_idx, buf.topk_weights, num_tokens=num_tokens, group_size=32, use_fp4_acts=True, ) else: mega_moe_pre_dispatch( hidden_states, topk_ids_in, topk_weights_in, buf.x, buf.x_sf, buf.topk_idx, buf.topk_weights, quant_group_size=32, ) # Allocate at least one row so y has a non-null CUDA data_ptr; # the DeepGEMM tvm-ffi binding rejects nullptr in convert_to_torch_tensor(). y = torch.empty( (max(num_tokens, 1), hidden_size), dtype=torch.bfloat16, device=hidden_states.device, ) swiglu_limit = getattr(moe.config, "swiglu_limit", None) deep_gemm.fp8_fp4_mega_moe( y, moe.experts.mega_l1_weights, moe.experts.mega_l2_weights, buf, recipe=(1, 1, 32), activation="swiglu", activation_clamp=swiglu_limit, fast_math=True, ) y = y[:num_tokens] if not moe.experts.should_fuse_routed_scaling_factor_in_topk: y.mul_(moe.routed_scaling_factor) return y def _interleave_mega_moe_gate_up(t: torch.Tensor, gran: int = 8) -> torch.Tensor: # Match DeepGEMM's L1 gate/up layout: # [gate: 0..7, up: 0..7, gate: 8..15, up: 8..15, ...]. num_groups, n, *rest = t.shape half = n // 2 gate = t[:, :half].reshape(num_groups, half // gran, gran, *rest) up = t[:, half:].reshape(num_groups, half // gran, gran, *rest) result = torch.stack([gate, up], dim=2).reshape(num_groups, n, *rest) return torch.empty_like(t).copy_(result) def _interleave_mega_moe_l1_weights( l1_weights: tuple[torch.Tensor, torch.Tensor], ) -> tuple[torch.Tensor, torch.Tensor]: return ( _interleave_mega_moe_gate_up(l1_weights[0]), _interleave_mega_moe_gate_up(l1_weights[1]), ) def _transpose_mega_moe_sf_for_utccp(sf: torch.Tensor) -> torch.Tensor: num_groups, mn, packed_sf_k = sf.shape assert sf.dtype == torch.int and mn % 128 == 0 result = ( sf.reshape(num_groups, -1, 4, 32, packed_sf_k) .transpose(2, 3) .reshape(num_groups, mn, packed_sf_k) ) return torch.empty_like(sf).copy_(result) def build_mega_moe_experts_weights(experts) -> None: from deep_gemm import ( transform_sf_into_required_layout, transform_weights_for_mega_moe, ) if getattr(experts, "_mega_moe_weights_built", False): return w13 = experts.w13_weight.data w13_sf_fp32 = experts.w13_weight_scale_inv.data w2 = experts.w2_weight.data w2_sf_fp32 = experts.w2_weight_scale_inv.data num_groups, n1, half_k1 = w13.shape k1 = half_k1 * 2 _, n2, half_k2 = w2.shape k2 = half_k2 * 2 w13_sf = transform_sf_into_required_layout( w13_sf_fp32, mn=n1, k=k1, recipe=(1, 32), num_groups=num_groups, disable_ue8m0_cast=False, ) w2_sf = transform_sf_into_required_layout( w2_sf_fp32, mn=n2, k=k2, recipe=(1, 32), num_groups=num_groups, disable_ue8m0_cast=False, ) if envs.SGLANG_OPT_FIX_MEGA_MOE_MEMORY.get(): # Build the interleaved L1 weight + scale once; share the weight buffer # between `w13_weight.data` (normal deep-ep path) and `mega_l1_weights[0]` # (mega moe path). Mega moe additionally needs a UTCCP-transposed scale; # the deep-ep path consumes the non-transposed interleaved scale and a # swizzle-aware activation kernel. L2 weight is untouched by the mega # transform, so the existing `w2_weight.data` is shared directly. w13_interleaved, w13_sf_interleaved = _interleave_mega_moe_l1_weights( (w13, w13_sf) ) w13_sf_utccp = _transpose_mega_moe_sf_for_utccp(w13_sf_interleaved) w2_sf_utccp = _transpose_mega_moe_sf_for_utccp(w2_sf) experts.w13_weight.data = w13_interleaved experts.w13_weight_scale_inv.data = w13_sf_interleaved experts.w2_weight_scale_inv.data = w2_sf experts.w13_weight_scale_inv.format_ue8m0 = True experts.w2_weight_scale_inv.format_ue8m0 = True experts.mega_l1_weights = (experts.w13_weight.data, w13_sf_utccp) experts.mega_l2_weights = (experts.w2_weight.data, w2_sf_utccp) else: l1_pair, l2_pair = transform_weights_for_mega_moe((w13, w13_sf), (w2, w2_sf)) experts.mega_l1_weights = l1_pair experts.mega_l2_weights = l2_pair experts._mega_moe_weights_built = True