Skip to content

[Quantization] Fix per-tensor FP8 MoE parameter identity for EPLB hot-reload#19499

Open
Socratesa wants to merge 2 commits into
sgl-project:mainfrom
Socratesa:ds_coder_v2_lite_fp8_pr
Open

[Quantization] Fix per-tensor FP8 MoE parameter identity for EPLB hot-reload#19499
Socratesa wants to merge 2 commits into
sgl-project:mainfrom
Socratesa:ds_coder_v2_lite_fp8_pr

Conversation

@Socratesa

@Socratesa Socratesa commented Feb 27, 2026

Copy link
Copy Markdown

Motivation

When using per-tensor FP8 quantized MoE models (e.g., RedHatAI/DeepSeek-Coder-V2-Lite-Instruct-FP8) with Elastic EP Load Balancing (EPLB), the failover weight hot-reload crashes with:

AttributeError: 'Parameter' object has no attribute 'weight_loader'

followed by (after working around the above):

RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x2048 and 1536x2048)

This was also mentioned in #18191.

Issue 1 — Parameter identity loss in per-tensor FP8 MoE

Fp8MoEMethod.process_weights_after_loading() creates new Parameter objects via torch.nn.Parameter(...) for w13_input_scale, w2_input_scale, and w13_weight_scale. This destroys custom attributes (weight_loader, quant_method) that deepseek_weight_loader.py relies on during EPLB hot-reload.

The block-quant path was already fixed in #17449 via _copy_or_rebind(), but the per-tensor path was never updated.

Issue 2 — Double process_weights_after_loading on partial reload

update_weights_from_disk() calls load_weights_and_postprocess(), which runs process_weights_after_loading() on all quantized modules unconditionally — even when only expert weights were reloaded. For FP8 linear attention layers (e.g., q_proj), this means the weight gets transposed twice (once during initial load, once during reload), corrupting the shape from [K, N] back to [N, K].

Modifications

python/sglang/srt/layers/quantization/fp8.py

Dynamic-quant path (non-checkpoint FP8):

  • Use .data = for weight rebinding and .data[expert].fill_() for scale updates instead of Parameter replacement.

Checkpoint-FP8 path — input_scale merging:

  • Replace torch.nn.Parameter(max_val) with layer.w13_input_scale.data.fill_(max_val) to preserve the original Parameter object in-place.

Checkpoint-FP8 path — w13_weight_scale merging:

  • Instead of replacing the [E, 2] Parameter with a new [E] one, fill both columns with the same per-expert max in-place. The [E, 2] shape is preserved so weight_loader can still index param.data[expert_id][shard_idx] during reload.

apply() — DeepGEMM path:

  • Add ndim == 2 guard to collapse w13_weight_scale from [E, 2] to [E] via [:, 0] before expanding to block shape.

python/sglang/srt/model_executor/model_runner.py

Partial reload isolation:

  • When weight_name_filter is set (EPLB expert rebalancing), split load_weights and process_weights_after_loading into two steps, only calling the latter on modules whose names match the filter. This prevents double-processing of attention layers and other non-expert modules.

Accuracy Tests

Validated with test_mooncake_ep_small.py (use DeepSeek-Coder-V2-Lite-Instruct-FP8): 7/7 tests passed.

image

Benchmarking and Profiling

Checklist

Review Process

  1. Ping Merge Oncalls to start the PR flow. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • /tag-run-ci-label, /rerun-failed-ci, /tag-and-rerun-ci
  4. After green CI and required approvals, ask Merge Oncalls to merge.

@Socratesa
Socratesa requested a review from bingxche as a code owner February 27, 2026 11:11
@github-actions github-actions Bot added documentation Improvements or additions to documentation quant LLM Quantization deepseek npu diffusion SGLang Diffusion labels Feb 27, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request primarily focuses on enhancing the robustness and correctness of FP8 quantization within Mixture-of-Experts (MoE) models, particularly concerning hot-reloading and partial weight updates in distributed environments. It resolves critical issues related to parameter object identity and prevents unintended double-processing of weights, ensuring stable and accurate model operation during dynamic reconfigurations. Additionally, it includes significant refactoring of the MoriEP dispatcher and CUDA graph buffer management for improved efficiency and maintainability.

Highlights

  • FP8 MoE Parameter Identity Fix: Resolved an issue where per-tensor FP8 quantized MoE models crashed during Elastic EP Load Balancing (EPLB) hot-reload due to Parameter object identity loss. This was fixed by using in-place updates (.data = and .data[expert].fill_()) instead of creating new Parameter objects, preserving custom attributes like weight_loader.
  • Partial Reload Weight Processing Isolation: Addressed a bug where process_weights_after_loading() was unconditionally called on all quantized modules during partial reloads (e.g., EPLB expert rebalancing). This could lead to double processing and corruption of non-expert weights. The fix isolates this step, applying it only to modules whose weights were actually updated.
  • MoriEP Dispatcher Refactoring: Simplified the MoriEP dispatcher implementation by removing low-latency specific paths, CommStreamPool, and related asynchronous event handling. The dispatcher now focuses on a single 'normal' mode, streamlining the code and removing unused complexity.
  • CUDA Graph Buffer Management Refinement: Refactored CUDA graph input buffer management by removing redundant ForwardInputBuffers and DecodeInputBuffers classes, consolidating logic into GraphInputBuffers, and directly managing buffers as attributes within the runner classes. This reduces overhead and simplifies buffer sharing.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • docs/advanced_features/server_arguments.md
    • Removed 'mori' from the --moe-a2a-backend options.
  • python/sglang/jit_kernel/benchmark/bench_per_token_group_quant_8bit.py
    • Removed the file.
  • python/sglang/jit_kernel/csrc/gemm/per_token_group_quant_8bit.cuh
    • Removed the file.
  • python/sglang/jit_kernel/per_token_group_quant_8bit.py
    • Removed the file.
  • python/sglang/jit_kernel/tests/test_per_token_group_quant_8bit.py
    • Removed the file.
  • python/sglang/jit_kernel/utils.py
    • Added a blank line for formatting.
    • Removed torch.float8_e4m3fn and torch.int8 from CPP_DTYPE_MAP.
  • python/sglang/multimodal_gen/test/server/perf_baselines.json
    • Updated denoise_step_ms value for step 2.
  • python/sglang/srt/batch_overlap/operations_strategy.py
    • Removed is_hip import and related conditional logic.
  • python/sglang/srt/batch_overlap/two_batch_overlap.py
    • Removed MoriEPDispatcher import and associated logic.
  • python/sglang/srt/disaggregation/mooncake/conn.py
    • Changed dtype for page indices and tokens per page from np.int32 to np.int64.
  • python/sglang/srt/disaggregation/utils.py
    • Changed bootstrap_room dtype from torch.int64 to torch.uint64.
  • python/sglang/srt/entrypoints/openai/serving_chat.py
    • Modified tool handling to use item.model_dump() for tool list creation.
    • Adjusted Jinja template tool schema fallback to flatten function-only format.
  • python/sglang/srt/function_call/kimik2_detector.py
    • Added re.DOTALL flag to tool call regex patterns.
    • Changed logger.info to logger.debug for function name logging.
  • python/sglang/srt/layers/attention/aiter_backend.py
    • Removed idle mode condition from init_forward_metadata.
    • Removed unused variable initializations in init_forward_metadata_replay_cuda_graph.
    • Removed MLA-related logic and ForwardMetadata creation from init_forward_metadata_replay_cuda_graph.
    • Added MLA metadata creation within forward_extend and forward_decode methods, conditional on layer ID and MLA kernel usage.
  • python/sglang/srt/layers/attention/nsa/utils.py
    • Modified pad_nsa_cache_seqlens to use explicit padding flags.
  • python/sglang/srt/layers/moe/ep_moe/layer.py
    • Removed MoriEPLLCombineInput import.
    • Removed _is_hip check from DeepGEMM assertion.
    • Simplified combine_input_wrapper creation and removed DispatchOutputChecker import.
    • Refactored forward method to integrate run_moe_core logic directly.
    • Removed hidden_states.shape[0] == 0 check and _use_aiter conditional logic.
  • python/sglang/srt/layers/moe/fused_moe_triton/layer.py
    • Simplified group argument for MaybeTboDeepEPDispatcher.
    • Re-added MoriEPDispatcher instantiation with deepep_mode.
  • python/sglang/srt/layers/moe/token_dispatcher/init.py
    • Removed MoriEPLLCombineInput and MoriEPLLDispatchOutput from imports and __all__.
  • python/sglang/srt/layers/moe/token_dispatcher/moriep.py
    • Removed DeepEPPDispatchHooks import and is_tbo_enabled.
    • Removed MoriEPPDispatchHooks, MoriEPLLDispatchOutput, and MoriEPLLCombineInput classes.
    • Removed LOW_LATENCY from EpMode enum.
    • Changed lru_cache maxsize for init_mori_op from 2 to 1.
    • Removed deepep_mode argument from init_mori_op call.
    • Removed CommStreamPool class.
    • Added return_recv_hook to _MoriEPDispatcherImplBase.__init__.
    • Removed async_finish and stream-related attributes from _MoriEPDispatcherImplNormal.
    • Simplified dispatch_a and dispatch_b in _MoriEPDispatcherImplNormal.
    • Removed _MoriEPDispatcherImplLowLatency class.
    • Changed mori_op.dispatch_send to mori_op.dispatch in _dispatch_core.
    • Simplified combine_a and combine_b in _MoriEPDispatcherImplNormal.
    • Removed _low_latency_dispatcher and _deepep_dispatch_hooks from MoriEPDispatcher.
    • Added overlap_args to combine and combine_a in MoriEPDispatcher.
    • Removed set_overlap_args and clear_overlap_args from MoriEPDispatcher.
  • python/sglang/srt/layers/quantization/fp8.py
    • Modified process_weights_after_loading to use .data = for weight rebinding in dynamic-quant path.
    • Updated w13_weight_scale and w2_weight_scale updates to use .data[expert].fill_() for in-place modification.
    • Modified w13_input_scale and w2_input_scale merging to use .data.fill_() for in-place updates.
    • Implemented in-place filling for w13_weight_scale in checkpoint-FP8 path to preserve object identity.
    • Added ndim == 2 guard in apply() for DeepGEMM path to collapse w13_weight_scale.
  • python/sglang/srt/layers/quantization/fp8_kernel.py
    • Removed import of sglang.jit_kernel.per_token_group_quant_8bit.
    • Simplified sglang_per_token_group_quant_fp8 by removing enable_v2 conditional for JIT kernel.
  • python/sglang/srt/model_executor/cuda_graph_runner.py
    • Removed dataclass and compute_local_num_token_non_padded imports.
    • Renamed ForwardInputBuffers to GraphInputBuffers and removed DecodeInputBuffers class.
    • Removed buffers.share_buffers() call.
    • Modified populate_from_forward_batch to return seq_lens_cpu.
    • Updated type hints for buffers to GraphInputBuffers.
  • python/sglang/srt/model_executor/input_buffers.py
    • Renamed ForwardInputBuffers to GraphInputBuffers.
    • Removed _forward_input_buffer_pool and buffer sharing methods.
    • Moved populate_from_forward_batch into GraphInputBuffers and adjusted its return type.
  • python/sglang/srt/model_executor/model_runner.py
    • Removed DecodeInputBuffers import.
    • Imported GraphInputBuffers and device_loading_context.
    • Modified model_load_weights to selectively call process_weights_after_loading for partial reloads.
    • Updated type hint for buffers to GraphInputBuffers in _dummy_run.
  • python/sglang/srt/model_executor/piecewise_cuda_graph_runner.py
    • Removed dataclass import and PrefillInputBuffers class.
    • Replaced self.buffers.attribute with direct self.attribute access.
  • python/sglang/srt/models/deepseek_v2.py
    • Removed _use_aiter conditional logic in _post_combine_hook and op_output.
    • Removed num_tokens from state in op_comm_prepare_attn.
  • python/sglang/srt/server_args.py
    • Automatically set deepep_mode='normal' for MORI EP and added a warning.
    • Removed conditional check for chunked_prefill_size validation in MORI EP.
  • python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py
    • Removed dataclass and ForwardInputBuffers imports.
    • Removed EagleDraftInputBuffers class.
    • Replaced buffers.attribute with direct self.attribute access.
  • python/sglang/srt/speculative/eagle_draft_extend_cuda_graph_runner.py
    • Removed dataclass and ForwardInputBuffers imports.
    • Removed EagleDraftExtendInputBuffers class.
    • Replaced buffers.attribute with direct self.attribute access.
  • python/sglang/srt/speculative/multi_layer_eagle_draft_extend_cuda_graph_runner.py
    • Removed dataclass and ForwardInputBuffers imports.
    • Removed MultiLayerEagleDraftExtendInputBuffers class.
    • Replaced buffers.attribute with direct self.attribute access.
    • Adjusted type hints for self.runners and buffer_len_list.
  • python/sglang/srt/speculative/multi_layer_eagle_worker_v2.py
    • Replaced last_cuda_graph_runner.buffers.attribute with direct last_cuda_graph_runner.attribute access.
  • python/sglang/test/bench_one_batch_server_internal.py
    • Simplified token capacity calculation and dp_size retrieval.
  • python/sglang/test/tool_call_test_runner.py
    • Commented out test_auto, test_reasoning_usage, and test_streaming_parallel tests.
  • test/registered/openai_server/basic/test_serving_chat.py
    • Added new test cases for Jinja chat template tool schema handling.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/nightly-test-nvidia.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request primarily addresses issues with per-tensor FP8 MoE parameter identity during EPLB hot-reloading, which caused crashes. The core fixes involve using in-place updates for weights and scales to preserve Parameter object identity and its custom attributes, and ensuring that weight post-processing is only applied to the reloaded expert modules. These changes are well-implemented and correctly solve the described problems.

Additionally, the PR includes significant refactoring and cleanup, such as the removal of the mori backend and old JIT kernels, and a reorganization of CUDA graph input buffers. These changes improve code structure and maintainability. I have one suggestion regarding the calculation of skip_token_capacity_threshold in the benchmarking script, which appears to have been simplified incorrectly.

Comment on lines +794 to +797
internal_state = server_info.get("internal_states", [{}])
dp_size = internal_state[0].get("dp_size", None) or 1
skip_token_capacity_threshold = (
internal_state[0].get("memory_usage", {}).get("token_capacity", 1000000000)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The calculation for skip_token_capacity_threshold now only considers the token capacity of the first DP rank. Since batch_size in should_skip_due_to_token_capacity refers to the total batch size, skip_token_capacity_threshold should represent the total capacity across all DP ranks. This change may cause benchmarks to be skipped incorrectly. It would be more accurate to sum the capacities from all DP ranks.

Suggested change
internal_state = server_info.get("internal_states", [{}])
dp_size = internal_state[0].get("dp_size", None) or 1
skip_token_capacity_threshold = (
internal_state[0].get("memory_usage", {}).get("token_capacity", 1000000000)
)
internal_state = server_info.get("internal_states", [{}])
skip_token_capacity_threshold = sum(
state.get("memory_usage", {}).get("token_capacity", 1000000000)
for state in internal_state
)

@Socratesa
Socratesa force-pushed the ds_coder_v2_lite_fp8_pr branch from 3f98267 to 4f219f8 Compare February 27, 2026 11:19
The per-tensor FP8 MoE path in process_weights_after_loading() replaced
Parameter objects with new ones via torch.nn.Parameter(), destroying
custom attributes (weight_loader, quant_method) needed by EPLB weight
hot-reload.  The block-quant path was already fixed (PR sgl-project#17449) but the
per-tensor path was missed.

Additionally, update_weights_from_disk() unconditionally called
process_weights_after_loading() on ALL modules even during partial
reloads (e.g. EPLB expert rebalancing), causing non-expert layers like
FP8 attention to be double-processed (double transpose -> shape mismatch).

Changes in fp8.py:
- Dynamic-quant path: use .data= for weight rebinding and
  .data[expert].fill_() for scale updates instead of Parameter replacement.
- Checkpoint-FP8 path: use .data.fill_() for input_scale merging;
  fill both columns of the [E,2] w13_weight_scale in-place instead of
  replacing with a new [E] Parameter.
- DeepGemm apply(): add ndim==2 guard to collapse w13_weight_scale
  from [E,2] to [E] via [:,0] before expanding to block shape.

Changes in model_runner.py:
- When weight_name_filter is set (EPLB expert rebalancing), split
  load_weights and process_weights_after_loading into two steps,
  only calling the latter on modules whose names match the filter.

Signed-off-by: Socratesa <lihaode@zju.edu.cn>
@Socratesa
Socratesa force-pushed the ds_coder_v2_lite_fp8_pr branch from 4f219f8 to 7ce24c3 Compare February 27, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deepseek diffusion SGLang Diffusion documentation Improvements or additions to documentation npu quant LLM Quantization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant