[lora] Fix WAR race: never write MoE runner output into hidden_states in place (#31870)

This commit is contained in:
Sam Shleifer
2026-07-24 00:58:55 -07:00
committed by GitHub
parent d059b0f56e
commit b8bb1b4e5a
2 changed files with 121 additions and 0 deletions
+5
View File
@@ -924,6 +924,11 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
self.lora_use_virtual_experts: bool = False
self.quant_method = base_layer.quant_method
self.moe_runner_config = base_layer.moe_runner_config
# Don't let the MoE runner overwrite hidden_states with its output:
# dual-stream forwards (e.g. DeepseekV2MoE.forward_normal_dual_stream)
# read hidden_states for the shared experts on the alt stream, and
# losing that race corrupts long generations.
self.moe_runner_config.inplace = False
self.dispatcher = base_layer.dispatcher
self.num_local_experts = base_layer.num_local_experts
self.should_fuse_routed_scaling_factor_in_topk = (
@@ -0,0 +1,116 @@
"""Regression test: LoRA MoE layers must never run their runner in-place.
The LoRA MoE runner cores write their final reduction into ``output =
hidden_states if inplace else torch.empty_like(hidden_states)``. Dual-stream
MoE forwards (e.g. ``DeepseekV2MoE.forward_normal_dual_stream``) read
``hidden_states`` for the shared experts on the alt stream with no ordering
edge against that write — with ``inplace=True`` this is a write-after-read
race inside the decode cuda graph that intermittently corrupts long LoRA
generations (fixed by forcing ``inplace=False`` at ``FusedMoEWithLoRA``
construction).
These tests execute the real ``FusedMoEWithLoRA.__init__`` against a config
that was explicitly constructed with ``inplace=True`` — exactly how
``FusedMoE.__init__`` builds it from its own signature default — and assert
the constructor forces it off. Asserting on the *shared config object* (not a
copy) also guards the historical footgun where flipping the
``MoeRunnerConfig`` dataclass default silently changed nothing because
``FusedMoE.__init__`` passes ``inplace`` explicitly.
Usage:
python -m pytest test/registered/unit/lora/test_lora_moe_inplace_unit.py -v
"""
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
# CPU-only unit test; no CUDA/distributed dependencies.
register_cuda_ci(est_time=9, stage="base-b", runner_config="1-gpu-small")
register_amd_ci(est_time=9, suite="stage-b-test-1-gpu-small-amd")
import types
import unittest
import unittest.mock as mock
import torch
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
def _make_base_layer(quant_method=None) -> types.SimpleNamespace:
"""The attribute surface FusedMoEWithLoRA.__init__ reads off its base FusedMoE."""
config = MoeRunnerConfig(
num_experts=8,
num_local_experts=8,
hidden_size=64,
intermediate_size_per_partition=32,
layer_id=0,
top_k=2,
num_fused_shared_experts=0,
params_dtype=torch.bfloat16,
activation="silu",
# FusedMoE.__init__ passes its own `inplace: bool = True` signature
# default into the config explicitly; mirror that here.
inplace=True,
)
if quant_method is None:
# `.runner = None` -> AUTO backend falls back to triton; quant-info
# getters auto-mock.
quant_method = mock.MagicMock()
quant_method.runner = None
return types.SimpleNamespace(
quant_method=quant_method,
moe_runner_config=config,
dispatcher=None,
num_local_experts=8,
should_fuse_routed_scaling_factor_in_topk=False,
moe_tp_size=1,
moe_tp_rank=0,
intermediate_size_per_partition=32,
)
class FusedMoEWithLoRAInplaceTest(unittest.TestCase):
def _construct(self, quant_method=None):
from sglang.srt.lora.layers import FusedMoEWithLoRA
base_layer = _make_base_layer(quant_method)
lora_backend = types.SimpleNamespace()
self.assertTrue(base_layer.moe_runner_config.inplace)
layer = FusedMoEWithLoRA(base_layer, lora_backend)
return layer, base_layer
def test_constructor_forces_inplace_off(self):
layer, base_layer = self._construct()
# The wrapper and the base layer share one config object; both views
# (and therefore the MoeRunner built from it) must see inplace=False.
self.assertIs(layer.moe_runner_config, base_layer.moe_runner_config)
self.assertFalse(layer.moe_runner_config.inplace)
def test_marlin_lora_runner_core_sees_non_inplace_config(self):
# The marlin LoRA runner core is the code that aliases its output onto
# hidden_states when inplace=True; make sure the core the layer builds
# is wired to the (now non-inplace) shared config.
from sglang.srt.layers.moe import MoeRunnerBackend
from sglang.srt.layers.quantization.compressed_tensors.compressed_tensors import (
CompressedTensorsFusedMoEMethod,
)
with mock.patch(
"sglang.srt.layers.moe.utils.get_moe_runner_backend",
return_value=MoeRunnerBackend.MARLIN,
):
layer, base_layer = self._construct(
quant_method=mock.MagicMock(spec=CompressedTensorsFusedMoEMethod)
)
core = getattr(layer._lora_runner, "runner_core", None)
self.assertIsNotNone(core)
core_config = getattr(core, "config", None) or getattr(
core, "runner_config", None
)
self.assertIsNotNone(core_config)
self.assertIs(core_config, base_layer.moe_runner_config)
self.assertFalse(core_config.inplace)
if __name__ == "__main__":
unittest.main()