[LoRA][III] Add LoRA support for MoE layers and enable TP (#14105)
Co-authored-by: Yusheng Su <yushengsu.thu@gmail.com> Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com>
This commit is contained in:
co-authored by
Yusheng Su
Baizhou Zhang
parent
a3ed2e4d29
commit
a32e0d57e7
@@ -24,15 +24,25 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class MoeRunner:
|
class MoeRunner:
|
||||||
|
def __init__(
|
||||||
def __init__(self, runner_backend: MoeRunnerBackend, config: MoeRunnerConfig):
|
self,
|
||||||
|
runner_backend: MoeRunnerBackend,
|
||||||
|
config: MoeRunnerConfig,
|
||||||
|
lora_enabled: bool = False,
|
||||||
|
):
|
||||||
self.runner_backend = runner_backend
|
self.runner_backend = runner_backend
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.lora_enabled = lora_enabled
|
||||||
|
|
||||||
self.fused_func = None
|
self.fused_func = None
|
||||||
|
|
||||||
if runner_backend.is_triton():
|
if runner_backend.is_triton():
|
||||||
self.runner_core = TritonRunnerCore(config)
|
if lora_enabled:
|
||||||
|
from sglang.srt.lora.lora_moe_runners import TritonRunnerCoreWithLoRA
|
||||||
|
|
||||||
|
self.runner_core = TritonRunnerCoreWithLoRA(config)
|
||||||
|
else:
|
||||||
|
self.runner_core = TritonRunnerCore(config)
|
||||||
elif runner_backend.is_triton_kernels():
|
elif runner_backend.is_triton_kernels():
|
||||||
self.runner_core = TritonKernelsRunnerCore(config)
|
self.runner_core = TritonKernelsRunnerCore(config)
|
||||||
elif runner_backend.is_deep_gemm():
|
elif runner_backend.is_deep_gemm():
|
||||||
@@ -47,20 +57,22 @@ class MoeRunner:
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Unsupported runner backend: {runner_backend}")
|
raise NotImplementedError(f"Unsupported runner backend: {runner_backend}")
|
||||||
|
|
||||||
a2a_backend_name = get_moe_a2a_backend().value
|
# Skip fused func if LoRA is enabled (LoRA requires non-fused path)
|
||||||
runner_backend_name = runner_backend.value
|
if not lora_enabled:
|
||||||
|
a2a_backend_name = get_moe_a2a_backend().value
|
||||||
|
runner_backend_name = runner_backend.value
|
||||||
|
|
||||||
# TODO(cwan): add a server argument to disable fused func
|
# TODO(cwan): add a server argument to disable fused func
|
||||||
self.fused_func = FusedOpPool.get_fused_func(
|
self.fused_func = FusedOpPool.get_fused_func(
|
||||||
a2a_backend_name, runner_backend_name
|
a2a_backend_name, runner_backend_name
|
||||||
)
|
|
||||||
|
|
||||||
if self.runner_core is None and self.fused_func is None:
|
|
||||||
raise NotImplementedError(
|
|
||||||
f"Runner backend {runner_backend} requires a fused func for a2a backend "
|
|
||||||
f"{a2a_backend_name}, but none is registered."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.runner_core is None and self.fused_func is None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"Runner backend {runner_backend} requires a fused func for a2a backend "
|
||||||
|
f"{a2a_backend_name}, but none is registered."
|
||||||
|
)
|
||||||
|
|
||||||
self.down_gemm_overlap_args: Optional[DownGemmOverlapArgs] = None
|
self.down_gemm_overlap_args: Optional[DownGemmOverlapArgs] = None
|
||||||
self.meta_overlap_args: Optional[dict] = None
|
self.meta_overlap_args: Optional[dict] = None
|
||||||
|
|
||||||
@@ -74,10 +86,9 @@ class MoeRunner:
|
|||||||
self.fused_func = None
|
self.fused_func = None
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self, dispatch_output: DispatchOutput, quant_info: MoeQuantInfo
|
self, dispatch_output: DispatchOutput, quant_info: MoeQuantInfo, lora_info=None
|
||||||
) -> CombineInput:
|
) -> CombineInput:
|
||||||
|
if self.fused_func is not None and not self.lora_enabled:
|
||||||
if self.fused_func is not None:
|
|
||||||
return self.fused_func(dispatch_output, quant_info, self.config)
|
return self.fused_func(dispatch_output, quant_info, self.config)
|
||||||
|
|
||||||
assert self.runner_core is not None
|
assert self.runner_core is not None
|
||||||
@@ -96,7 +107,16 @@ class MoeRunner:
|
|||||||
runner_input = self.pre_permute_func(
|
runner_input = self.pre_permute_func(
|
||||||
dispatch_output, quant_info, self.config, running_state
|
dispatch_output, quant_info, self.config, running_state
|
||||||
)
|
)
|
||||||
runner_output = self.runner_core.run(runner_input, quant_info, running_state)
|
|
||||||
|
# Pass lora_info to runner_core if LoRA is enabled
|
||||||
|
if self.lora_enabled:
|
||||||
|
runner_output = self.runner_core.run(
|
||||||
|
runner_input, quant_info, running_state, lora_info
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
runner_output = self.runner_core.run(
|
||||||
|
runner_input, quant_info, running_state
|
||||||
|
)
|
||||||
|
|
||||||
runner_format = self.runner_core.runner_backend.value
|
runner_format = self.runner_core.runner_backend.value
|
||||||
combine_format = dispatch_output.format.value
|
combine_format = dispatch_output.format.value
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from sglang.srt.layers.linear import (
|
|||||||
QKVParallelLinear,
|
QKVParallelLinear,
|
||||||
RowParallelLinear,
|
RowParallelLinear,
|
||||||
)
|
)
|
||||||
|
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
||||||
|
from sglang.srt.layers.moe.topk import TopKOutput
|
||||||
from sglang.srt.layers.vocab_parallel_embedding import (
|
from sglang.srt.layers.vocab_parallel_embedding import (
|
||||||
ParallelLMHead,
|
ParallelLMHead,
|
||||||
VocabParallelEmbedding,
|
VocabParallelEmbedding,
|
||||||
@@ -689,11 +691,199 @@ class RowParallelLinearWithLoRA(BaseLayerWithLoRA):
|
|||||||
return B
|
return B
|
||||||
|
|
||||||
|
|
||||||
|
class FusedMoEWithLoRA(BaseLayerWithLoRA):
|
||||||
|
"""
|
||||||
|
Wrapper around FusedMoE that integrates LoRA into the MoE computation.
|
||||||
|
|
||||||
|
Design: LoRA deltas are added at specific points in the MoE forward pass:
|
||||||
|
1. After gate_up projection, BEFORE activation (halfway through)
|
||||||
|
2. After down projection, BEFORE final reduction
|
||||||
|
|
||||||
|
This follows the vLLM/HF approach where LoRA is fused into the computation
|
||||||
|
rather than computed independently and added at the end.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_layer: FusedMoE,
|
||||||
|
lora_backend: BaseLoRABackend,
|
||||||
|
):
|
||||||
|
# initializes FusedMoE with its own moe_runner for base path
|
||||||
|
super().__init__(base_layer, lora_backend)
|
||||||
|
|
||||||
|
self.tp_size = getattr(base_layer, "moe_tp_size", 1)
|
||||||
|
self.tp_rank = getattr(base_layer, "moe_tp_rank", 0)
|
||||||
|
self.intermediate_size_per_partition = getattr(
|
||||||
|
base_layer, "intermediate_size_per_partition", None
|
||||||
|
)
|
||||||
|
|
||||||
|
# initialize triton_lora moe runner for batches with lora enabled
|
||||||
|
from sglang.srt.layers.moe.moe_runner.runner import MoeRunner
|
||||||
|
from sglang.srt.layers.moe.moe_runner.triton import TritonMoeQuantInfo
|
||||||
|
|
||||||
|
self._lora_runner = MoeRunner(
|
||||||
|
base_layer.quant_method.runner.runner_backend,
|
||||||
|
base_layer.moe_runner_config,
|
||||||
|
lora_enabled=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pre-compute quant info for efficiency (weights don't change during inference)
|
||||||
|
self._quant_info = TritonMoeQuantInfo(
|
||||||
|
w13_weight=base_layer.w13_weight,
|
||||||
|
w2_weight=base_layer.w2_weight,
|
||||||
|
b13=getattr(base_layer, "w13_weight_bias", None),
|
||||||
|
b2=getattr(base_layer, "w2_weight_bias", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_lora_info(
|
||||||
|
self,
|
||||||
|
gate_up_lora_a_weights: torch.Tensor,
|
||||||
|
gate_up_lora_b_weights: torch.Tensor,
|
||||||
|
down_lora_a_weights: torch.Tensor = None,
|
||||||
|
down_lora_b_weights: torch.Tensor = None,
|
||||||
|
):
|
||||||
|
"""Set LoRA weight tensors from memory pool."""
|
||||||
|
self.set_lora = True
|
||||||
|
self.gate_up_lora_a_weights = gate_up_lora_a_weights
|
||||||
|
self.gate_up_lora_b_weights = gate_up_lora_b_weights
|
||||||
|
self.down_lora_a_weights = down_lora_a_weights
|
||||||
|
self.down_lora_b_weights = down_lora_b_weights
|
||||||
|
|
||||||
|
def _get_lora_info(self):
|
||||||
|
"""
|
||||||
|
Build LoRAInfo for the current batch.
|
||||||
|
|
||||||
|
Returns None if LoRA is not enabled or weights are not set.
|
||||||
|
"""
|
||||||
|
from sglang.srt.lora.lora_moe_runners import LoRAInfo
|
||||||
|
|
||||||
|
# Get LoRA batch info from backend
|
||||||
|
batch_info = self.lora_backend.batch_info
|
||||||
|
lora_ranks = batch_info.lora_ranks # [num_loras]
|
||||||
|
|
||||||
|
max_lora_rank = self.down_lora_a_weights.shape[2]
|
||||||
|
|
||||||
|
# Create adapter_enabled tensor for the current batch
|
||||||
|
# Only enable LoRA adapters that are actually used in this batch
|
||||||
|
# TODO: Jonahbernard: check that this doesn't slow down inference for this batch
|
||||||
|
adapter_enabled = torch.zeros(
|
||||||
|
len(lora_ranks), dtype=torch.int32, device=lora_ranks.device
|
||||||
|
)
|
||||||
|
adapter_enabled.index_fill_(0, batch_info.weight_indices.long(), 1)
|
||||||
|
|
||||||
|
return LoRAInfo(
|
||||||
|
gate_up_lora_a_weights=self.gate_up_lora_a_weights,
|
||||||
|
gate_up_lora_b_weights=self.gate_up_lora_b_weights,
|
||||||
|
down_lora_a_weights=self.down_lora_a_weights,
|
||||||
|
down_lora_b_weights=self.down_lora_b_weights,
|
||||||
|
seg_indptr=batch_info.seg_indptr,
|
||||||
|
req_to_lora=batch_info.weight_indices,
|
||||||
|
lora_ranks=lora_ranks,
|
||||||
|
adapter_enabled=adapter_enabled,
|
||||||
|
max_lora_rank=max_lora_rank,
|
||||||
|
num_experts=self.base_layer.num_experts,
|
||||||
|
tp_size=self.tp_size,
|
||||||
|
tp_rank=self.tp_rank,
|
||||||
|
hidden_size=getattr(self.base_layer, "hidden_size", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(self, hidden_states: torch.Tensor, topk_output: TopKOutput, **kwargs):
|
||||||
|
"""
|
||||||
|
Forward pass with integrated LoRA computation.
|
||||||
|
|
||||||
|
LoRA deltas are added at the correct points inside the MoE computation:
|
||||||
|
1. After gate_up projection, before activation
|
||||||
|
2. After down projection, before final reduction
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Build LoRA info for this batch
|
||||||
|
lora_info = self._get_lora_info()
|
||||||
|
|
||||||
|
# run lora moe_runner
|
||||||
|
return self._forward_with_lora(hidden_states, topk_output, lora_info, **kwargs)
|
||||||
|
|
||||||
|
def _forward_with_lora(
|
||||||
|
self,
|
||||||
|
hidden_states: torch.Tensor,
|
||||||
|
topk_output: TopKOutput,
|
||||||
|
lora_info,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Run MoE forward with LoRA integration at the correct points.
|
||||||
|
"""
|
||||||
|
# Get the base layer's dispatch and combine logic
|
||||||
|
base_layer = self.base_layer
|
||||||
|
|
||||||
|
# Dispatch tokens (doesn't do much in the LoRA case)
|
||||||
|
dispatch_output = base_layer.dispatcher.dispatch(
|
||||||
|
hidden_states=hidden_states, topk_output=topk_output
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use pre-computed quant info (doesn't change so not sure why we need to pass it in every time)
|
||||||
|
quant_info = self._quant_info
|
||||||
|
|
||||||
|
# Run the only lora moe runner (Triton)
|
||||||
|
combine_input = self._lora_runner.run(
|
||||||
|
dispatch_output, quant_info, lora_info=lora_info
|
||||||
|
)
|
||||||
|
|
||||||
|
final_hidden_states = base_layer.dispatcher.combine(combine_input=combine_input)
|
||||||
|
|
||||||
|
return final_hidden_states
|
||||||
|
|
||||||
|
def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
||||||
|
return A
|
||||||
|
|
||||||
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
||||||
|
return B
|
||||||
|
|
||||||
|
def slice_moe_lora_a_weights(
|
||||||
|
self, A: torch.Tensor, tp_rank: int, target_module: str
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Slice LoRA A weights for MoE with TP.
|
||||||
|
|
||||||
|
Per-expert weight shapes:
|
||||||
|
gate_up_proj_moe A: [rank, hidden_size] — input is full hidden_states, no slice
|
||||||
|
down_proj_moe A: [rank, intermediate_size] — input is sharded intermediate
|
||||||
|
"""
|
||||||
|
if self.tp_size <= 1:
|
||||||
|
return A
|
||||||
|
if target_module == "down_proj_moe":
|
||||||
|
shard_size = self.intermediate_size_per_partition
|
||||||
|
start = tp_rank * shard_size
|
||||||
|
end = start + shard_size
|
||||||
|
return A[:, start:end].contiguous()
|
||||||
|
return A
|
||||||
|
|
||||||
|
def slice_moe_lora_b_weights(
|
||||||
|
self, B: torch.Tensor, tp_rank: int, target_module: str
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Slice LoRA B weights for MoE with TP.
|
||||||
|
|
||||||
|
Per-expert weight shapes:
|
||||||
|
gate_up_proj_moe B: [intermediate_size*2, rank] — output matches sharded base w13
|
||||||
|
down_proj_moe B: [hidden_size, rank] — output is all-reduced, no slice
|
||||||
|
"""
|
||||||
|
if self.tp_size <= 1:
|
||||||
|
return B
|
||||||
|
if target_module == "gate_up_proj_moe":
|
||||||
|
shard_size = self.intermediate_size_per_partition
|
||||||
|
start = tp_rank * shard_size
|
||||||
|
end = start + shard_size
|
||||||
|
full_inter = B.shape[0] // 2
|
||||||
|
gate_b = B[start:end, :]
|
||||||
|
up_b = B[full_inter + start : full_inter + end, :]
|
||||||
|
return torch.cat([gate_b, up_b], dim=0).contiguous()
|
||||||
|
return B
|
||||||
|
|
||||||
|
|
||||||
def get_lora_layer(
|
def get_lora_layer(
|
||||||
layer: nn.Module, lora_backend: BaseLoRABackend
|
layer: nn.Module, lora_backend: BaseLoRABackend
|
||||||
) -> BaseLayerWithLoRA:
|
) -> BaseLayerWithLoRA:
|
||||||
supported_layer_types = {
|
supported_layer_types = {
|
||||||
# the order matters
|
# the order matters
|
||||||
|
FusedMoE: FusedMoEWithLoRA,
|
||||||
ParallelLMHead: ParallelLMHeadWithLoRA,
|
ParallelLMHead: ParallelLMHeadWithLoRA,
|
||||||
VocabParallelEmbedding: VocabParallelEmbeddingWithLoRA,
|
VocabParallelEmbedding: VocabParallelEmbeddingWithLoRA,
|
||||||
QKVParallelLinear: QKVParallelLinearWithLoRA,
|
QKVParallelLinear: QKVParallelLinearWithLoRA,
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class LoRALayer(nn.Module):
|
|||||||
|
|
||||||
|
|
||||||
class LoRAAdapter(nn.Module):
|
class LoRAAdapter(nn.Module):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
uid: str,
|
uid: str,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from typing import Dict, Iterable, List, Optional
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from sglang.srt.configs.load_config import LoadConfig
|
from sglang.srt.configs.load_config import LoadConfig
|
||||||
|
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
||||||
from sglang.srt.layers.utils import get_layer_id
|
from sglang.srt.layers.utils import get_layer_id
|
||||||
from sglang.srt.layers.vocab_parallel_embedding import (
|
from sglang.srt.layers.vocab_parallel_embedding import (
|
||||||
ParallelLMHead,
|
ParallelLMHead,
|
||||||
@@ -28,7 +29,7 @@ from sglang.srt.layers.vocab_parallel_embedding import (
|
|||||||
)
|
)
|
||||||
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
|
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
|
||||||
from sglang.srt.lora.backend.lora_registry import get_backend_from_name
|
from sglang.srt.lora.backend.lora_registry import get_backend_from_name
|
||||||
from sglang.srt.lora.layers import BaseLayerWithLoRA, get_lora_layer
|
from sglang.srt.lora.layers import BaseLayerWithLoRA, FusedMoEWithLoRA, get_lora_layer
|
||||||
from sglang.srt.lora.lora import LoRAAdapter
|
from sglang.srt.lora.lora import LoRAAdapter
|
||||||
from sglang.srt.lora.lora_config import LoRAConfig
|
from sglang.srt.lora.lora_config import LoRAConfig
|
||||||
from sglang.srt.lora.lora_registry import LoRARef
|
from sglang.srt.lora.lora_registry import LoRARef
|
||||||
@@ -297,9 +298,43 @@ class LoRAManager:
|
|||||||
"""
|
"""
|
||||||
for layer_id, layer_modules in enumerate(self.lora_modules):
|
for layer_id, layer_modules in enumerate(self.lora_modules):
|
||||||
for module_name, module in layer_modules.items():
|
for module_name, module in layer_modules.items():
|
||||||
|
# Hack for FusedMoE layer
|
||||||
|
if isinstance(module, FusedMoEWithLoRA) and all(
|
||||||
|
x in self.target_modules for x in ["gate_up_proj", "down_proj"]
|
||||||
|
):
|
||||||
|
gate_up_a = self.memory_pool.get_tensor(
|
||||||
|
target_module="gate_up_proj_moe",
|
||||||
|
layer_id=layer_id,
|
||||||
|
lora_type=LoRAType.LORA_A,
|
||||||
|
)
|
||||||
|
gate_up_b = self.memory_pool.get_tensor(
|
||||||
|
target_module="gate_up_proj_moe",
|
||||||
|
layer_id=layer_id,
|
||||||
|
lora_type=LoRAType.LORA_B,
|
||||||
|
)
|
||||||
|
down_a = self.memory_pool.get_tensor(
|
||||||
|
target_module="down_proj_moe",
|
||||||
|
layer_id=layer_id,
|
||||||
|
lora_type=LoRAType.LORA_A,
|
||||||
|
)
|
||||||
|
down_b = self.memory_pool.get_tensor(
|
||||||
|
target_module="down_proj_moe",
|
||||||
|
layer_id=layer_id,
|
||||||
|
lora_type=LoRAType.LORA_B,
|
||||||
|
)
|
||||||
|
|
||||||
|
module.set_lora_info(
|
||||||
|
gate_up_lora_a_weights=gate_up_a,
|
||||||
|
gate_up_lora_b_weights=gate_up_b,
|
||||||
|
down_lora_a_weights=down_a,
|
||||||
|
down_lora_b_weights=down_b,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
target_module = get_target_module_name(
|
target_module = get_target_module_name(
|
||||||
module_name, self.memory_pool.target_modules
|
module_name, self.memory_pool.target_modules
|
||||||
)
|
)
|
||||||
|
|
||||||
module.set_lora_info(
|
module.set_lora_info(
|
||||||
self.memory_pool.get_tensor(
|
self.memory_pool.get_tensor(
|
||||||
target_module=target_module,
|
target_module=target_module,
|
||||||
@@ -350,6 +385,7 @@ class LoRAManager:
|
|||||||
max_lora_rank=max_lora_rank,
|
max_lora_rank=max_lora_rank,
|
||||||
target_modules=target_modules,
|
target_modules=target_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.init_lora_modules()
|
self.init_lora_modules()
|
||||||
self.init_memory_pool()
|
self.init_memory_pool()
|
||||||
self.update_lora_info()
|
self.update_lora_info()
|
||||||
@@ -555,6 +591,7 @@ class LoRAManager:
|
|||||||
self.fetch_new_loras({None})
|
self.fetch_new_loras({None})
|
||||||
|
|
||||||
def set_lora_module(self, module_name, module):
|
def set_lora_module(self, module_name, module):
|
||||||
|
"""Wrap any module (standard or MoE) with LoRA support."""
|
||||||
lora_module = get_lora_layer(module, self.lora_backend)
|
lora_module = get_lora_layer(module, self.lora_backend)
|
||||||
replace_submodule(self.base_model, module_name, lora_module)
|
replace_submodule(self.base_model, module_name, lora_module)
|
||||||
return lora_module
|
return lora_module
|
||||||
@@ -613,6 +650,7 @@ class LoRAManager:
|
|||||||
) and not self.base_model.should_apply_lora(module_name):
|
) and not self.base_model.should_apply_lora(module_name):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Check if module should be wrapped with LoRA
|
||||||
# Handle embed_tokens
|
# Handle embed_tokens
|
||||||
if "embed_tokens" in module_name and "embed_tokens" in self.target_modules:
|
if "embed_tokens" in module_name and "embed_tokens" in self.target_modules:
|
||||||
if isinstance(module, VocabParallelEmbedding) and not isinstance(
|
if isinstance(module, VocabParallelEmbedding) and not isinstance(
|
||||||
@@ -637,3 +675,13 @@ class LoRAManager:
|
|||||||
self.lora_modules[layer_id][module_name] = self.set_lora_module(
|
self.lora_modules[layer_id][module_name] = self.set_lora_module(
|
||||||
module_name, module
|
module_name, module
|
||||||
)
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Temporarily workaround for FusedMoE layer
|
||||||
|
if isinstance(module, FusedMoE) and all(
|
||||||
|
x in self.target_modules for x in ["gate_up_proj", "down_proj"]
|
||||||
|
):
|
||||||
|
layer_id = get_layer_id(module_name)
|
||||||
|
self.lora_modules[layer_id][module_name] = self.set_lora_module(
|
||||||
|
module_name, module
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
# Copyright 2023-2025 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""LoRA-aware MoE runners that integrate LoRA deltas into the MoE computation.
|
||||||
|
|
||||||
|
The key insight is that LoRA deltas must be added at specific points:
|
||||||
|
1. After gate_up projection, BEFORE activation (halfway through)
|
||||||
|
2. After down projection, BEFORE final reduction (at the end)
|
||||||
|
|
||||||
|
This differs from computing LoRA independently and adding at the very end.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import triton.language as tl
|
||||||
|
|
||||||
|
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
|
||||||
|
from sglang.srt.layers.moe.moe_runner.triton import (
|
||||||
|
TritonMoeQuantInfo,
|
||||||
|
TritonRunnerCore,
|
||||||
|
TritonRunnerInput,
|
||||||
|
TritonRunnerOutput,
|
||||||
|
)
|
||||||
|
from sglang.srt.utils import cpu_has_amx_support, is_cpu, is_cuda, is_hip, is_xpu
|
||||||
|
|
||||||
|
_is_hip = is_hip()
|
||||||
|
_is_cuda = is_cuda()
|
||||||
|
_is_cpu_amx_available = cpu_has_amx_support()
|
||||||
|
_is_cpu = is_cpu()
|
||||||
|
_use_aiter = bool(int(os.getenv("SGLANG_USE_AITER", "0")))
|
||||||
|
_is_xpu = is_xpu()
|
||||||
|
_MOE_PADDING_SIZE = 128 if bool(int(os.getenv("SGLANG_MOE_PADDING", "0"))) else 0
|
||||||
|
|
||||||
|
|
||||||
|
if _is_cuda or _is_hip:
|
||||||
|
from sgl_kernel import gelu_and_mul, silu_and_mul
|
||||||
|
|
||||||
|
if _is_hip:
|
||||||
|
from vllm import _custom_ops as vllm_ops # moe_sum
|
||||||
|
elif _is_cpu and _is_cpu_amx_available:
|
||||||
|
pass
|
||||||
|
elif _is_xpu:
|
||||||
|
from sgl_kernel import silu_and_mul
|
||||||
|
|
||||||
|
|
||||||
|
if _is_cuda or _is_hip or _is_xpu:
|
||||||
|
from sgl_kernel import ( # noqa: F401
|
||||||
|
moe_align_block_size as sgl_moe_align_block_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
from sglang.jit_kernel.moe_lora_align import moe_lora_align_block_size
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoRAInfo:
|
||||||
|
"""LoRA weights and dispatch info for MoE computation."""
|
||||||
|
|
||||||
|
# LoRA weights: [num_loras, num_experts, dim1, dim2]
|
||||||
|
gate_up_lora_a_weights: (
|
||||||
|
torch.Tensor
|
||||||
|
) # [num_loras, num_experts, max_rank, hidden_dim]
|
||||||
|
gate_up_lora_b_weights: (
|
||||||
|
torch.Tensor
|
||||||
|
) # [num_loras, num_experts, gate_up_dim, max_rank]
|
||||||
|
down_lora_a_weights: (
|
||||||
|
torch.Tensor
|
||||||
|
) # [num_loras, num_experts, max_rank, intermediate_dim]
|
||||||
|
down_lora_b_weights: torch.Tensor # [num_loras, num_experts, hidden_dim, max_rank]
|
||||||
|
|
||||||
|
# Indice pointers of each segment in shape (num_segments + 1, )
|
||||||
|
seg_indptr: torch.Tensor
|
||||||
|
|
||||||
|
# The index of lora adapter used by each segment, in shape (num_segments,)
|
||||||
|
req_to_lora: torch.Tensor
|
||||||
|
|
||||||
|
# LoRA config per adapter
|
||||||
|
lora_ranks: torch.Tensor # [num_loras]
|
||||||
|
adapter_enabled: torch.Tensor # [num_loras] - which adapters are enabled
|
||||||
|
max_lora_rank: int # Maximum LoRA rank across all adapters
|
||||||
|
|
||||||
|
num_experts: int
|
||||||
|
|
||||||
|
fully_sharded: bool = False
|
||||||
|
tp_size: int = 1
|
||||||
|
tp_rank: int = 0
|
||||||
|
hidden_size: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class TritonRunnerCoreWithLoRA(TritonRunnerCore):
|
||||||
|
"""
|
||||||
|
LoRA-aware wrapper around TritonRunnerCore.
|
||||||
|
|
||||||
|
Integrates LoRA deltas at the correct points in the MoE forward pass:
|
||||||
|
1. Base gate_up projection + LoRA gate_up delta -> activation
|
||||||
|
2. Base down projection + LoRA down delta -> final reduction
|
||||||
|
|
||||||
|
This follows the vLLM/HF approach where LoRA is fused into the computation
|
||||||
|
rather than computed independently.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: MoeRunnerConfig):
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
runner_input: TritonRunnerInput,
|
||||||
|
quant_info: TritonMoeQuantInfo,
|
||||||
|
running_state: dict,
|
||||||
|
lora_info: Optional[LoRAInfo] = None,
|
||||||
|
) -> TritonRunnerOutput:
|
||||||
|
"""
|
||||||
|
Run MoE with integrated LoRA computation.
|
||||||
|
|
||||||
|
This method extends TritonRunnerCore.run() by inserting LoRA delta
|
||||||
|
computations at the correct points in the MoE forward pass.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
runner_input: Standard Triton runner input
|
||||||
|
quant_info: Quantization info for base weights
|
||||||
|
running_state: Running state dict
|
||||||
|
lora_info: Optional LoRA weights and dispatch info
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TritonRunnerOutput with combined base + LoRA output
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Extract common variables
|
||||||
|
hidden_states = runner_input.hidden_states
|
||||||
|
topk_weights = runner_input.topk_weights
|
||||||
|
topk_ids = runner_input.topk_ids
|
||||||
|
sorted_token_ids = runner_input.sorted_token_ids
|
||||||
|
expert_ids = runner_input.expert_ids
|
||||||
|
num_tokens_post_padded = runner_input.num_tokens_post_padded
|
||||||
|
|
||||||
|
w13 = quant_info.w13_weight
|
||||||
|
w2 = quant_info.w2_weight
|
||||||
|
b13 = quant_info.b13
|
||||||
|
b2 = quant_info.b2
|
||||||
|
a13_scale = quant_info.a13_scale
|
||||||
|
a2_scale = quant_info.a2_scale
|
||||||
|
w13_scale = quant_info.w13_scale
|
||||||
|
w2_scale = quant_info.w2_scale
|
||||||
|
w13_zp = quant_info.w13_zp
|
||||||
|
w2_zp = quant_info.w2_zp
|
||||||
|
block_shape = quant_info.block_shape
|
||||||
|
per_channel_quant = quant_info.per_channel_quant
|
||||||
|
use_fp8_w8a8 = quant_info.use_fp8_w8a8
|
||||||
|
use_int8_w8a8 = quant_info.use_int8_w8a8
|
||||||
|
use_int8_w8a16 = quant_info.use_int8_w8a16
|
||||||
|
use_int4_w4a16 = quant_info.use_int4_w4a16
|
||||||
|
|
||||||
|
activation = self.config.activation
|
||||||
|
no_combine = self.config.no_combine
|
||||||
|
inplace = self.config.inplace
|
||||||
|
gemm1_alpha = self.config.gemm1_alpha
|
||||||
|
gemm1_limit = self.config.gemm1_clamp_limit
|
||||||
|
routed_scaling_factor = self.config.routed_scaling_factor
|
||||||
|
apply_router_weight_on_input = self.config.apply_router_weight_on_input
|
||||||
|
|
||||||
|
assert self.config.is_gated, "Only gated MoEs are supported for Triton runner"
|
||||||
|
|
||||||
|
M = hidden_states.shape[0]
|
||||||
|
E, N, _ = w13.shape
|
||||||
|
compute_type = (
|
||||||
|
tl.bfloat16 if hidden_states.dtype == torch.bfloat16 else tl.float16
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: move these functions to the triton runner
|
||||||
|
from sglang.srt.layers.moe.fused_moe_triton.fused_moe import (
|
||||||
|
_swiglu_gpt_oss_sigmoid_alpha,
|
||||||
|
_swiglu_silu_clamp_mul,
|
||||||
|
invoke_fused_moe_kernel,
|
||||||
|
moe_sum_reduce_torch_compile,
|
||||||
|
moe_sum_reduce_triton,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Stage 1: Gate/Up projection (base)
|
||||||
|
# ============================================================
|
||||||
|
intermediate_cache1 = torch.empty(
|
||||||
|
(M, topk_ids.shape[1], N),
|
||||||
|
device=hidden_states.device,
|
||||||
|
dtype=hidden_states.dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
invoke_fused_moe_kernel(
|
||||||
|
hidden_states,
|
||||||
|
w13,
|
||||||
|
b13,
|
||||||
|
intermediate_cache1,
|
||||||
|
a13_scale,
|
||||||
|
w13_scale,
|
||||||
|
w13_zp,
|
||||||
|
topk_weights,
|
||||||
|
topk_ids,
|
||||||
|
sorted_token_ids,
|
||||||
|
expert_ids,
|
||||||
|
num_tokens_post_padded,
|
||||||
|
apply_router_weight_on_input,
|
||||||
|
topk_ids.shape[1],
|
||||||
|
running_state["config"],
|
||||||
|
compute_type=compute_type,
|
||||||
|
use_fp8_w8a8=use_fp8_w8a8,
|
||||||
|
use_int8_w8a8=use_int8_w8a8,
|
||||||
|
use_int8_w8a16=use_int8_w8a16,
|
||||||
|
use_int4_w4a16=use_int4_w4a16,
|
||||||
|
per_channel_quant=per_channel_quant,
|
||||||
|
block_shape=block_shape,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Perform LoRA alignment for both gate up and gate down operations
|
||||||
|
# Define shrink_config for LoRA alignment
|
||||||
|
# TODO: Add autotuning for block sizes across different GPU architectures and problem sizes
|
||||||
|
shrink_config = {"BLOCK_SIZE_M": 64}
|
||||||
|
|
||||||
|
# Prepare inputs for the kernel
|
||||||
|
block_size_m = shrink_config["BLOCK_SIZE_M"]
|
||||||
|
max_loras = len(lora_info.lora_ranks)
|
||||||
|
|
||||||
|
# Calculate max_num_tokens_padded
|
||||||
|
max_num_tokens_padded = topk_ids.numel() + lora_info.num_experts * (
|
||||||
|
block_size_m - 1
|
||||||
|
)
|
||||||
|
max_num_tokens_padded = (
|
||||||
|
(max_num_tokens_padded + block_size_m - 1) // block_size_m
|
||||||
|
) * block_size_m
|
||||||
|
max_num_m_blocks = (max_num_tokens_padded + block_size_m - 1) // block_size_m
|
||||||
|
|
||||||
|
# Initialize output tensors (using torch.empty like the reference implementation)
|
||||||
|
device = topk_ids.device
|
||||||
|
sorted_token_ids_lora = torch.empty(
|
||||||
|
(max_loras * max_num_tokens_padded,),
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
expert_ids_lora = torch.empty(
|
||||||
|
(max_loras * max_num_m_blocks,),
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
num_tokens_post_padded_lora = torch.empty(
|
||||||
|
(max_loras,), dtype=torch.int32, device=device
|
||||||
|
)
|
||||||
|
|
||||||
|
lora_ids = torch.arange(max_loras, dtype=torch.int32, device=device)
|
||||||
|
|
||||||
|
moe_lora_align_block_size(
|
||||||
|
topk_ids,
|
||||||
|
lora_info.seg_indptr,
|
||||||
|
lora_info.req_to_lora,
|
||||||
|
int(lora_info.num_experts),
|
||||||
|
int(block_size_m),
|
||||||
|
int(max_loras),
|
||||||
|
int(max_num_tokens_padded),
|
||||||
|
int(max_num_m_blocks),
|
||||||
|
sorted_token_ids_lora,
|
||||||
|
expert_ids_lora,
|
||||||
|
num_tokens_post_padded_lora,
|
||||||
|
lora_info.adapter_enabled,
|
||||||
|
lora_ids,
|
||||||
|
None, # expert_map
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the sorted tensors for fused_moe_lora (expects 2D: max_loras x max_num_tokens_padded)
|
||||||
|
sorted_token_ids_reshaped = sorted_token_ids_lora.view(max_loras, -1)
|
||||||
|
expert_ids_reshaped = expert_ids_lora.view(max_loras, -1)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Stage 1.5: Add LoRA gate_up delta BEFORE activation
|
||||||
|
# ============================================================
|
||||||
|
self._add_lora_gate_up_delta(
|
||||||
|
hidden_states=hidden_states,
|
||||||
|
intermediate_cache=intermediate_cache1,
|
||||||
|
topk_weights=topk_weights,
|
||||||
|
lora_info=lora_info,
|
||||||
|
sorted_token_ids_reshaped=sorted_token_ids_reshaped,
|
||||||
|
expert_ids_reshaped=expert_ids_reshaped,
|
||||||
|
num_tokens_post_padded_lora=num_tokens_post_padded_lora,
|
||||||
|
lora_ids=lora_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Stage 2: Activation (SiLU or GELU)
|
||||||
|
# ============================================================
|
||||||
|
intermediate_cache2 = torch.empty(
|
||||||
|
(M * topk_ids.shape[1], N // 2),
|
||||||
|
device=hidden_states.device,
|
||||||
|
dtype=hidden_states.dtype,
|
||||||
|
)
|
||||||
|
if activation == "silu":
|
||||||
|
if gemm1_alpha is not None:
|
||||||
|
assert gemm1_limit is not None
|
||||||
|
intermediate_cache2 = _swiglu_gpt_oss_sigmoid_alpha(
|
||||||
|
intermediate_cache1.view(-1, N), gemm1_alpha, gemm1_limit
|
||||||
|
)
|
||||||
|
elif gemm1_limit is not None:
|
||||||
|
intermediate_cache2 = _swiglu_silu_clamp_mul(
|
||||||
|
intermediate_cache1.view(-1, N), gemm1_limit
|
||||||
|
)
|
||||||
|
elif _is_cuda or _is_hip or _is_xpu:
|
||||||
|
silu_and_mul(intermediate_cache1.view(-1, N), intermediate_cache2)
|
||||||
|
else:
|
||||||
|
vllm_ops.silu_and_mul(
|
||||||
|
intermediate_cache2, intermediate_cache1.view(-1, N)
|
||||||
|
)
|
||||||
|
elif activation == "gelu":
|
||||||
|
assert gemm1_alpha is None, "gemm1_alpha is not supported for gelu"
|
||||||
|
assert gemm1_limit is None, "gemm1_limit is not supported for gelu"
|
||||||
|
if _is_cuda or _is_hip:
|
||||||
|
gelu_and_mul(intermediate_cache1.view(-1, N), intermediate_cache2)
|
||||||
|
else:
|
||||||
|
vllm_ops.gelu_and_mul(
|
||||||
|
intermediate_cache2, intermediate_cache1.view(-1, N)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported activation: {activation=}")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Stage 3: Down projection (base)
|
||||||
|
# ============================================================
|
||||||
|
intermediate_cache3 = torch.empty(
|
||||||
|
(M, topk_ids.shape[1], w2.shape[1]),
|
||||||
|
device=hidden_states.device,
|
||||||
|
dtype=hidden_states.dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
if no_combine:
|
||||||
|
assert not inplace
|
||||||
|
out_hidden_states = torch.empty(
|
||||||
|
(M, topk_ids.shape[1], w2.shape[1]),
|
||||||
|
device=hidden_states.device,
|
||||||
|
dtype=hidden_states.dtype,
|
||||||
|
)
|
||||||
|
elif inplace:
|
||||||
|
out_hidden_states = hidden_states
|
||||||
|
else:
|
||||||
|
out_hidden_states = torch.empty_like(hidden_states)
|
||||||
|
|
||||||
|
invoke_fused_moe_kernel(
|
||||||
|
intermediate_cache2,
|
||||||
|
w2,
|
||||||
|
b2,
|
||||||
|
intermediate_cache3,
|
||||||
|
a2_scale,
|
||||||
|
w2_scale,
|
||||||
|
w2_zp,
|
||||||
|
topk_weights,
|
||||||
|
topk_ids,
|
||||||
|
sorted_token_ids,
|
||||||
|
expert_ids,
|
||||||
|
num_tokens_post_padded,
|
||||||
|
not apply_router_weight_on_input,
|
||||||
|
1,
|
||||||
|
running_state["config"],
|
||||||
|
compute_type=compute_type,
|
||||||
|
use_fp8_w8a8=use_fp8_w8a8,
|
||||||
|
use_int8_w8a8=use_int8_w8a8,
|
||||||
|
use_int8_w8a16=use_int8_w8a16,
|
||||||
|
use_int4_w4a16=use_int4_w4a16,
|
||||||
|
per_channel_quant=per_channel_quant,
|
||||||
|
block_shape=block_shape,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Stage 3.5: Add LoRA down delta BEFORE final reduction
|
||||||
|
# ============================================================
|
||||||
|
self._add_lora_down_delta(
|
||||||
|
intermediate_input=intermediate_cache2,
|
||||||
|
intermediate_cache=intermediate_cache3,
|
||||||
|
topk_weights=topk_weights,
|
||||||
|
lora_info=lora_info,
|
||||||
|
sorted_token_ids_reshaped=sorted_token_ids_reshaped,
|
||||||
|
expert_ids_reshaped=expert_ids_reshaped,
|
||||||
|
num_tokens_post_padded_lora=num_tokens_post_padded_lora,
|
||||||
|
lora_ids=lora_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Stage 4: Final reduction (sum across top_k)
|
||||||
|
# ============================================================
|
||||||
|
if routed_scaling_factor is None:
|
||||||
|
routed_scaling_factor = 1.0
|
||||||
|
|
||||||
|
if no_combine:
|
||||||
|
pass
|
||||||
|
elif _is_cuda:
|
||||||
|
if topk_ids.shape[1] == 1 and routed_scaling_factor == 1.0:
|
||||||
|
out_hidden_states[:] = intermediate_cache3.squeeze(1)
|
||||||
|
elif topk_ids.shape[1] == 2 and routed_scaling_factor == 1.0:
|
||||||
|
torch.add(
|
||||||
|
intermediate_cache3[:, 0],
|
||||||
|
intermediate_cache3[:, 1],
|
||||||
|
out=out_hidden_states,
|
||||||
|
).squeeze(dim=1)
|
||||||
|
else:
|
||||||
|
if M <= 32:
|
||||||
|
moe_sum_reduce_torch_compile(
|
||||||
|
intermediate_cache3.view(*intermediate_cache3.shape),
|
||||||
|
out_hidden_states,
|
||||||
|
routed_scaling_factor,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
moe_sum_reduce_triton(
|
||||||
|
intermediate_cache3.view(*intermediate_cache3.shape),
|
||||||
|
out_hidden_states,
|
||||||
|
routed_scaling_factor,
|
||||||
|
)
|
||||||
|
elif _is_hip:
|
||||||
|
from vllm import _custom_ops as vllm_ops
|
||||||
|
|
||||||
|
vllm_ops.moe_sum(
|
||||||
|
intermediate_cache3.view(*intermediate_cache3.shape),
|
||||||
|
out_hidden_states,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from vllm import _custom_ops as vllm_ops
|
||||||
|
|
||||||
|
vllm_ops.moe_sum(
|
||||||
|
intermediate_cache3.view(*intermediate_cache3.shape),
|
||||||
|
out_hidden_states,
|
||||||
|
)
|
||||||
|
|
||||||
|
return TritonRunnerOutput(
|
||||||
|
hidden_states=out_hidden_states,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_lora_gate_up_delta(
|
||||||
|
self,
|
||||||
|
hidden_states: torch.Tensor, # [M, hidden_dim]
|
||||||
|
intermediate_cache: torch.Tensor, # [M, top_k, gate_up_dim]
|
||||||
|
topk_weights: torch.Tensor, # [M, top_k]
|
||||||
|
lora_info: LoRAInfo,
|
||||||
|
sorted_token_ids_reshaped: torch.Tensor,
|
||||||
|
expert_ids_reshaped: torch.Tensor,
|
||||||
|
num_tokens_post_padded_lora: torch.Tensor,
|
||||||
|
lora_ids: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Add LoRA gate_up delta to intermediate_cache in-place.
|
||||||
|
|
||||||
|
For each (token, expert) pair, computes:
|
||||||
|
delta = scaling * B @ (A @ hidden_states[token])
|
||||||
|
and adds it to intermediate_cache[token, k] where k is the top_k index.
|
||||||
|
"""
|
||||||
|
from sglang.srt.lora.triton_ops import fused_moe_lora
|
||||||
|
|
||||||
|
M, top_k, gate_up_dim = intermediate_cache.shape
|
||||||
|
|
||||||
|
# Skip LoRA computation if no LoRA adapters have non-zero rank
|
||||||
|
if lora_info.max_lora_rank == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
r = lora_info.max_lora_rank
|
||||||
|
gate_up_a = lora_info.gate_up_lora_a_weights
|
||||||
|
gate_up_b = lora_info.gate_up_lora_b_weights
|
||||||
|
inter_size = gate_up_b.shape[2] // 2
|
||||||
|
|
||||||
|
# Split packed gate_up weights into separate gate and up slices.
|
||||||
|
# gate_up_lora_a has shape [max_loras, num_experts, 2*r, hidden_dim]
|
||||||
|
# where the first r rows are gate_lora_a and the next r are up_lora_a.
|
||||||
|
# gate_up_lora_b has shape [max_loras, num_experts, 2*inter_size, r]
|
||||||
|
# where the first inter_size rows are gate_lora_b and the rest up_lora_b.
|
||||||
|
# Using num_slices=2 lets the kernel handle gate and up independently,
|
||||||
|
# keeping the rank dimension at r so shrink and expand both match.
|
||||||
|
lora_a_stacked = [gate_up_a[:, :, :r, :], gate_up_a[:, :, r : 2 * r, :]]
|
||||||
|
lora_b_stacked = [
|
||||||
|
gate_up_b[:, :, :inter_size, :],
|
||||||
|
gate_up_b[:, :, inter_size:, :],
|
||||||
|
]
|
||||||
|
|
||||||
|
fused_moe_lora(
|
||||||
|
output=intermediate_cache,
|
||||||
|
qcurr_hidden_states=hidden_states,
|
||||||
|
lora_a_stacked=lora_a_stacked,
|
||||||
|
lora_b_stacked=lora_b_stacked,
|
||||||
|
topk_weights=topk_weights,
|
||||||
|
sorted_token_ids=sorted_token_ids_reshaped,
|
||||||
|
expert_ids=expert_ids_reshaped,
|
||||||
|
num_tokens_post_padded=num_tokens_post_padded_lora,
|
||||||
|
max_lora_rank=r,
|
||||||
|
top_k_num=top_k,
|
||||||
|
lora_ids=lora_ids,
|
||||||
|
adapter_enabled=lora_info.adapter_enabled,
|
||||||
|
# TODO: Replace hardcoded block sizes with autotuned configs
|
||||||
|
shrink_block_size_m=64,
|
||||||
|
shrink_block_size_n=64,
|
||||||
|
shrink_block_size_k=64,
|
||||||
|
shrink_group_size_m=8,
|
||||||
|
shrink_num_warps=4,
|
||||||
|
shrink_num_stages=2,
|
||||||
|
shrink_split_k=1,
|
||||||
|
expand_block_size_m=64,
|
||||||
|
expand_block_size_n=64,
|
||||||
|
expand_block_size_k=64,
|
||||||
|
expand_group_size_m=8,
|
||||||
|
expand_num_warps=4,
|
||||||
|
expand_num_stages=2,
|
||||||
|
expand_split_k=1,
|
||||||
|
fully_sharded=lora_info.fully_sharded,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_lora_down_delta(
|
||||||
|
self,
|
||||||
|
intermediate_input: torch.Tensor, # [M * top_k, intermediate_dim]
|
||||||
|
intermediate_cache: torch.Tensor, # [M, top_k, hidden_dim]
|
||||||
|
topk_weights: torch.Tensor, # [M, top_k]
|
||||||
|
lora_info: LoRAInfo,
|
||||||
|
sorted_token_ids_reshaped: torch.Tensor,
|
||||||
|
expert_ids_reshaped: torch.Tensor,
|
||||||
|
num_tokens_post_padded_lora: torch.Tensor,
|
||||||
|
lora_ids: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Add LoRA down delta to intermediate_cache in-place.
|
||||||
|
|
||||||
|
For each (token, expert) pair, computes:
|
||||||
|
delta = scaling * B @ (A @ intermediate_input[dispatched_idx])
|
||||||
|
and adds it to intermediate_cache[token, k].
|
||||||
|
"""
|
||||||
|
from sglang.srt.lora.triton_ops import fused_moe_lora
|
||||||
|
|
||||||
|
M, top_k, hidden_dim = intermediate_cache.shape
|
||||||
|
|
||||||
|
# Skip LoRA computation if no LoRA adapters have non-zero rank
|
||||||
|
if lora_info.max_lora_rank == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
lora_a_stacked = [lora_info.down_lora_a_weights]
|
||||||
|
lora_b_stacked = [lora_info.down_lora_b_weights]
|
||||||
|
|
||||||
|
if lora_info.fully_sharded and lora_info.tp_size > 1:
|
||||||
|
shard_size = lora_info.hidden_size // lora_info.tp_size
|
||||||
|
offset = shard_size * lora_info.tp_rank
|
||||||
|
else:
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
fused_moe_lora(
|
||||||
|
output=intermediate_cache,
|
||||||
|
qcurr_hidden_states=intermediate_input,
|
||||||
|
lora_a_stacked=lora_a_stacked,
|
||||||
|
lora_b_stacked=lora_b_stacked,
|
||||||
|
topk_weights=topk_weights,
|
||||||
|
sorted_token_ids=sorted_token_ids_reshaped,
|
||||||
|
expert_ids=expert_ids_reshaped,
|
||||||
|
num_tokens_post_padded=num_tokens_post_padded_lora,
|
||||||
|
max_lora_rank=lora_info.max_lora_rank,
|
||||||
|
top_k_num=top_k,
|
||||||
|
lora_ids=lora_ids,
|
||||||
|
adapter_enabled=lora_info.adapter_enabled,
|
||||||
|
# TODO: Replace hardcoded block sizes with autotuned configs
|
||||||
|
shrink_block_size_m=64,
|
||||||
|
shrink_block_size_n=64,
|
||||||
|
shrink_block_size_k=64,
|
||||||
|
shrink_group_size_m=8,
|
||||||
|
shrink_num_warps=4,
|
||||||
|
shrink_num_stages=2,
|
||||||
|
shrink_split_k=1,
|
||||||
|
expand_block_size_m=64,
|
||||||
|
expand_block_size_n=64,
|
||||||
|
expand_block_size_k=64,
|
||||||
|
expand_group_size_m=8,
|
||||||
|
expand_num_warps=4,
|
||||||
|
expand_num_stages=2,
|
||||||
|
expand_split_k=1,
|
||||||
|
mul_routed_weight=True,
|
||||||
|
fully_sharded=lora_info.fully_sharded,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
@@ -74,10 +75,9 @@ class LoRAMemoryPool:
|
|||||||
self.eviction_policy = get_eviction_policy(eviction_policy)
|
self.eviction_policy = get_eviction_policy(eviction_policy)
|
||||||
|
|
||||||
# Both A_buffer and B_buffer maps lora weight names to its buffer space.
|
# Both A_buffer and B_buffer maps lora weight names to its buffer space.
|
||||||
# A_buffer contains num_layer number of row-major tensors with shape
|
# Standard LoRA (3D): [num_loras, rank, hidden_dim]
|
||||||
# (max_loras_per_batch, stacked_num * max_lora_dim, input_dim)
|
# MoE LoRA (4D): [num_loras, num_experts, rank, hidden_dim]
|
||||||
# B_buffer contains num_layer number of column-major tensors with shape
|
# The dimensionality is determined by the module type (MoE vs standard)
|
||||||
# (stacked_num, max_loras_per_batch, output_dim, max_lora_dim)
|
|
||||||
self.A_buffer: Dict[str, List[torch.Tensor]] = {}
|
self.A_buffer: Dict[str, List[torch.Tensor]] = {}
|
||||||
self.B_buffer: Dict[str, List[torch.Tensor]] = {}
|
self.B_buffer: Dict[str, List[torch.Tensor]] = {}
|
||||||
|
|
||||||
@@ -136,6 +136,26 @@ class LoRAMemoryPool:
|
|||||||
else:
|
else:
|
||||||
return all(_can_support(x) for x in config)
|
return all(_can_support(x) for x in config)
|
||||||
|
|
||||||
|
def is_moe_module(self, module_name: str) -> bool:
|
||||||
|
"""Check if module is part of MoE experts."""
|
||||||
|
return "moe" in module_name
|
||||||
|
|
||||||
|
def _get_standard_shape(
|
||||||
|
self,
|
||||||
|
module_name: str,
|
||||||
|
base_model: torch.nn.Module,
|
||||||
|
max_lora_dim: int,
|
||||||
|
layer_idx: int,
|
||||||
|
) -> Tuple[int]:
|
||||||
|
"""Get 3D shape for standard (non-MoE) modules."""
|
||||||
|
input_dim, _ = get_hidden_dim(
|
||||||
|
module_name, self.base_hf_config, base_model, layer_idx
|
||||||
|
)
|
||||||
|
c = get_stacked_multiply(module_name)
|
||||||
|
if self.tp_size > 1 and module_name in ROW_PARALLELISM_LINEAR_LORA_NAMES:
|
||||||
|
input_dim = divide(input_dim, self.tp_size)
|
||||||
|
return (self.max_loras_per_batch, max_lora_dim * c, input_dim)
|
||||||
|
|
||||||
def get_lora_A_shape(
|
def get_lora_A_shape(
|
||||||
self,
|
self,
|
||||||
module_name: str,
|
module_name: str,
|
||||||
@@ -144,7 +164,11 @@ class LoRAMemoryPool:
|
|||||||
layer_idx: int,
|
layer_idx: int,
|
||||||
) -> Tuple[int]:
|
) -> Tuple[int]:
|
||||||
"""
|
"""
|
||||||
Given a module_name (might be a stacked name), return the hidden dims of modules' input and output.
|
Get shape for LoRA A weights. Automatically returns 3D or 4D based on module type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Standard: [num_loras, rank, hidden_dim]
|
||||||
|
- MoE: [num_loras, num_experts, rank, hidden_dim]
|
||||||
"""
|
"""
|
||||||
input_dim, _ = get_hidden_dim(
|
input_dim, _ = get_hidden_dim(
|
||||||
module_name, self.base_hf_config, base_model, layer_idx
|
module_name, self.base_hf_config, base_model, layer_idx
|
||||||
@@ -152,11 +176,17 @@ class LoRAMemoryPool:
|
|||||||
c = get_stacked_multiply(module_name)
|
c = get_stacked_multiply(module_name)
|
||||||
if self.tp_size > 1 and module_name in ROW_PARALLELISM_LINEAR_LORA_NAMES:
|
if self.tp_size > 1 and module_name in ROW_PARALLELISM_LINEAR_LORA_NAMES:
|
||||||
input_dim = divide(input_dim, self.tp_size)
|
input_dim = divide(input_dim, self.tp_size)
|
||||||
return (
|
|
||||||
self.max_loras_per_batch,
|
if self.is_moe_module(module_name):
|
||||||
max_lora_dim * c,
|
num_experts = base_model.config.num_experts
|
||||||
input_dim,
|
return (
|
||||||
)
|
self.max_loras_per_batch,
|
||||||
|
num_experts,
|
||||||
|
max_lora_dim * c,
|
||||||
|
input_dim,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (self.max_loras_per_batch, max_lora_dim * c, input_dim)
|
||||||
|
|
||||||
def get_embedding_lora_A_shape(
|
def get_embedding_lora_A_shape(
|
||||||
self,
|
self,
|
||||||
@@ -184,18 +214,24 @@ class LoRAMemoryPool:
|
|||||||
layer_idx: int,
|
layer_idx: int,
|
||||||
) -> Tuple[int]:
|
) -> Tuple[int]:
|
||||||
"""
|
"""
|
||||||
Given a module_name (might be a stacked name), return the hidden dims of modules' input and output.
|
Get shape for LoRA B weights. Automatically returns 3D or 4D based on module type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Standard: [num_loras, output_dim, rank]
|
||||||
|
- MoE: [num_loras, num_experts, output_dim, rank]
|
||||||
"""
|
"""
|
||||||
_, output_dim = get_hidden_dim(
|
_, output_dim = get_hidden_dim(
|
||||||
module_name, self.base_hf_config, base_model, layer_idx
|
module_name, self.base_hf_config, base_model, layer_idx
|
||||||
)
|
)
|
||||||
if self.tp_size > 1 and module_name not in ROW_PARALLELISM_LINEAR_LORA_NAMES:
|
if self.tp_size > 1 and module_name not in ROW_PARALLELISM_LINEAR_LORA_NAMES:
|
||||||
output_dim = divide(output_dim, self.tp_size)
|
output_dim = divide(output_dim, self.tp_size)
|
||||||
return (
|
|
||||||
self.max_loras_per_batch,
|
# Check if MoE module and return appropriate shape
|
||||||
output_dim,
|
if self.is_moe_module(module_name):
|
||||||
max_lora_dim,
|
num_experts = base_model.config.num_experts
|
||||||
)
|
return (self.max_loras_per_batch, num_experts, output_dim, max_lora_dim)
|
||||||
|
else:
|
||||||
|
return (self.max_loras_per_batch, output_dim, max_lora_dim)
|
||||||
|
|
||||||
def get_embedding_lora_B_shape(
|
def get_embedding_lora_B_shape(
|
||||||
self,
|
self,
|
||||||
@@ -228,21 +264,60 @@ class LoRAMemoryPool:
|
|||||||
target_modules: Set[str],
|
target_modules: Set[str],
|
||||||
get_lora_shape_fn: Callable[[str, torch.nn.Module, int, int], Tuple[int]],
|
get_lora_shape_fn: Callable[[str, torch.nn.Module, int, int], Tuple[int]],
|
||||||
):
|
):
|
||||||
|
# Check if model has both shared experts and MoE experts
|
||||||
|
has_shared_experts = (
|
||||||
|
hasattr(base_model.config, "shared_expert_intermediate_size")
|
||||||
|
and base_model.config.shared_expert_intermediate_size > 0
|
||||||
|
)
|
||||||
|
has_moe = getattr(base_model.config, "num_experts", 1) > 1
|
||||||
|
|
||||||
|
# Shape functions automatically handle both 3D (standard) and 4D (MoE)
|
||||||
target_modules = target_modules - set(EMBEDDING_NAMES)
|
target_modules = target_modules - set(EMBEDDING_NAMES)
|
||||||
for module_name in target_modules:
|
for module_name in target_modules:
|
||||||
buffer[module_name] = [
|
# Special handling for ambiguous target modules that can be in different contexts
|
||||||
torch.empty(
|
ambiguous_modules = {"gate_up_proj", "down_proj"}
|
||||||
get_lora_shape_fn(
|
if module_name in ambiguous_modules and has_shared_experts and has_moe:
|
||||||
module_name,
|
# Allocate separate buffers for shared and MoE contexts
|
||||||
base_model,
|
# Shared expert version (3D)
|
||||||
self.max_lora_rank,
|
shared_key = module_name
|
||||||
idx,
|
buffer[shared_key] = [
|
||||||
),
|
torch.empty(
|
||||||
dtype=self.dtype,
|
get_lora_shape_fn(
|
||||||
device=device,
|
module_name, base_model, self.max_lora_rank, idx
|
||||||
)
|
),
|
||||||
for idx in range(self.num_layer)
|
dtype=self.dtype,
|
||||||
]
|
device=device,
|
||||||
|
)
|
||||||
|
for idx in range(self.num_layer)
|
||||||
|
]
|
||||||
|
|
||||||
|
# MoE expert version (4D)
|
||||||
|
moe_key = f"{module_name}_moe"
|
||||||
|
buffer[moe_key] = [
|
||||||
|
torch.empty(
|
||||||
|
get_lora_shape_fn(
|
||||||
|
moe_key, base_model, self.max_lora_rank, idx
|
||||||
|
),
|
||||||
|
dtype=self.dtype,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
for idx in range(self.num_layer)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# Standard allocation for unambiguous modules
|
||||||
|
buffer[module_name] = [
|
||||||
|
torch.empty(
|
||||||
|
get_lora_shape_fn(
|
||||||
|
module_name,
|
||||||
|
base_model,
|
||||||
|
self.max_lora_rank,
|
||||||
|
idx,
|
||||||
|
),
|
||||||
|
dtype=self.dtype,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
for idx in range(self.num_layer)
|
||||||
|
]
|
||||||
|
|
||||||
def init_embedding_buffer(
|
def init_embedding_buffer(
|
||||||
buffer: Dict[str, torch.Tensor],
|
buffer: Dict[str, torch.Tensor],
|
||||||
@@ -430,22 +505,72 @@ class LoRAMemoryPool:
|
|||||||
lora_rank = lora_adapter.config.r
|
lora_rank = lora_adapter.config.r
|
||||||
for layer_id in range(self.num_layer):
|
for layer_id in range(self.num_layer):
|
||||||
layer_weights = lora_adapter.layers[layer_id].weights
|
layer_weights = lora_adapter.layers[layer_id].weights
|
||||||
temp_A_buffer: Dict[str, Optional[torch.Tensor]] = {
|
# - Standard: module_name -> torch.Tensor
|
||||||
|
# - MoE: module_name -> Dict[expert_id -> torch.Tensor]
|
||||||
|
temp_A_buffer: Dict[str, Union[torch.Tensor, Dict[int, torch.Tensor]]] = {
|
||||||
target_module: None for target_module in self.A_buffer
|
target_module: None for target_module in self.A_buffer
|
||||||
}
|
}
|
||||||
temp_B_buffer: Dict[str, Optional[torch.Tensor]] = {
|
temp_B_buffer: Dict[str, Union[torch.Tensor, Dict[int, torch.Tensor]]] = {
|
||||||
target_module: None for target_module in self.B_buffer
|
target_module: None for target_module in self.B_buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, weights in layer_weights.items():
|
for name, weights in layer_weights.items():
|
||||||
target_module = get_target_module_name(name, self.target_modules)
|
target_module = get_target_module_name(name, self.target_modules)
|
||||||
if "lora_A" in name:
|
|
||||||
temp_A_buffer[target_module] = weights
|
# Check if this is an MoE weight (has expert index in name)
|
||||||
|
expert_match = re.search(r"experts\.(\d+)\.", name)
|
||||||
|
|
||||||
|
if expert_match:
|
||||||
|
target_module = target_module + "_moe"
|
||||||
|
# MoE weight - multiple tensors per module (one per expert)
|
||||||
|
if temp_A_buffer[target_module] is None:
|
||||||
|
temp_A_buffer[target_module] = {}
|
||||||
|
temp_B_buffer[target_module] = {}
|
||||||
|
|
||||||
|
expert_id = int(expert_match.group(1))
|
||||||
|
if "lora_A" in name:
|
||||||
|
temp_A_buffer[target_module][expert_id] = weights
|
||||||
|
else:
|
||||||
|
temp_B_buffer[target_module][expert_id] = weights
|
||||||
else:
|
else:
|
||||||
temp_B_buffer[target_module] = weights
|
# Standard weight - single tensor per module
|
||||||
|
if "lora_A" in name:
|
||||||
|
temp_A_buffer[target_module] = weights
|
||||||
|
else:
|
||||||
|
temp_B_buffer[target_module] = weights
|
||||||
|
|
||||||
if self.tp_size > 1:
|
if self.tp_size > 1:
|
||||||
cur_layer_modules = lora_modules[layer_id]
|
cur_layer_modules = lora_modules[layer_id]
|
||||||
for module_name, module in cur_layer_modules.items():
|
for module_name, module in cur_layer_modules.items():
|
||||||
|
# TODO (Jonahcb): check if the code can be refactored to avoid the special handling for FusedMoEWithLoRA
|
||||||
|
# Handle FusedMoEWithLoRA specially - it contains multiple target modules
|
||||||
|
from sglang.srt.lora.layers import FusedMoEWithLoRA
|
||||||
|
|
||||||
|
if isinstance(module, FusedMoEWithLoRA):
|
||||||
|
moe_target_modules = ["gate_up_proj_moe", "down_proj_moe"]
|
||||||
|
for target_module in moe_target_modules:
|
||||||
|
if temp_A_buffer[target_module] is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for expert_id in temp_A_buffer[target_module].keys():
|
||||||
|
temp_A_buffer[target_module][expert_id] = (
|
||||||
|
module.slice_moe_lora_a_weights(
|
||||||
|
temp_A_buffer[target_module][expert_id],
|
||||||
|
self.tp_rank,
|
||||||
|
target_module,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
temp_B_buffer[target_module][expert_id] = (
|
||||||
|
module.slice_moe_lora_b_weights(
|
||||||
|
temp_B_buffer[target_module][expert_id],
|
||||||
|
self.tp_rank,
|
||||||
|
target_module,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle regular modules
|
||||||
target_module = get_target_module_name(
|
target_module = get_target_module_name(
|
||||||
module_name, self.target_modules
|
module_name, self.target_modules
|
||||||
)
|
)
|
||||||
@@ -454,6 +579,7 @@ class LoRAMemoryPool:
|
|||||||
# Skip weight slicing if the weight is not present in the adapter
|
# Skip weight slicing if the weight is not present in the adapter
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Handle standard modules
|
||||||
temp_A_buffer[target_module] = module.slice_lora_a_weights(
|
temp_A_buffer[target_module] = module.slice_lora_a_weights(
|
||||||
temp_A_buffer[target_module], self.tp_rank
|
temp_A_buffer[target_module], self.tp_rank
|
||||||
)
|
)
|
||||||
@@ -461,19 +587,45 @@ class LoRAMemoryPool:
|
|||||||
temp_B_buffer[target_module], self.tp_rank
|
temp_B_buffer[target_module], self.tp_rank
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Load weights into buffers (handles both 3D standard and 4D MoE)
|
||||||
for name, weights in temp_A_buffer.items():
|
for name, weights in temp_A_buffer.items():
|
||||||
c = get_stacked_multiply(name)
|
c = get_stacked_multiply(name)
|
||||||
target_buffer = self.A_buffer[name][layer_id]
|
target_buffer = self.A_buffer[name][layer_id]
|
||||||
buffer_view = target_buffer[buffer_id, : lora_rank * c, :]
|
|
||||||
load_lora_weight_tensor(buffer_view, weights)
|
if name in ["gate_up_proj_moe", "down_proj_moe"]:
|
||||||
|
# MoE: multiple tensors per module (one per expert)
|
||||||
|
for expert_id, expert_weight in weights.items():
|
||||||
|
# Buffer shape: [num_loras, num_experts, max_rank, hidden_dim]
|
||||||
|
buffer_view = target_buffer[
|
||||||
|
buffer_id, expert_id, : lora_rank * c, :
|
||||||
|
]
|
||||||
|
load_lora_weight_tensor(buffer_view, expert_weight)
|
||||||
|
else:
|
||||||
|
# Standard: single tensor per module
|
||||||
|
c = get_stacked_multiply(name)
|
||||||
|
buffer_view = target_buffer[buffer_id, : lora_rank * c, :]
|
||||||
|
load_lora_weight_tensor(buffer_view, weights)
|
||||||
|
|
||||||
for name, weights in temp_B_buffer.items():
|
for name, weights in temp_B_buffer.items():
|
||||||
target_buffer = self.B_buffer[name][layer_id]
|
target_buffer = self.B_buffer[name][layer_id]
|
||||||
buffer_view = target_buffer[buffer_id, :, :lora_rank]
|
|
||||||
load_lora_weight_tensor(buffer_view, weights)
|
if name in ["gate_up_proj_moe", "down_proj_moe"]:
|
||||||
|
# MoE: multiple tensors per module (one per expert)
|
||||||
|
for expert_id, expert_weight in weights.items():
|
||||||
|
# Buffer shape: [num_loras, num_experts, intermediate_dim, max_rank]
|
||||||
|
buffer_view = target_buffer[buffer_id, expert_id, :, :lora_rank]
|
||||||
|
|
||||||
|
weight_to_load = expert_weight
|
||||||
|
if weight_to_load is not None:
|
||||||
|
weight_to_load = weight_to_load * lora_adapter.scaling
|
||||||
|
|
||||||
|
load_lora_weight_tensor(buffer_view, weight_to_load)
|
||||||
|
else:
|
||||||
|
# Standard: single tensor per module
|
||||||
|
buffer_view = target_buffer[buffer_id, :, :lora_rank]
|
||||||
|
load_lora_weight_tensor(buffer_view, weights)
|
||||||
|
|
||||||
if lora_adapter.embedding_layers:
|
if lora_adapter.embedding_layers:
|
||||||
|
|
||||||
org_vocab_size = self.base_hf_config.vocab_size
|
org_vocab_size = self.base_hf_config.vocab_size
|
||||||
lora_added_tokens_size = lora_adapter.config.lora_added_tokens_size
|
lora_added_tokens_size = lora_adapter.config.lora_added_tokens_size
|
||||||
# Only when LoRA is applied to the embedding layer will it have the extra-token issue that needs to be resolved.
|
# Only when LoRA is applied to the embedding layer will it have the extra-token issue that needs to be resolved.
|
||||||
@@ -599,11 +751,24 @@ class LoRAMemoryPool:
|
|||||||
def get_tensor(
|
def get_tensor(
|
||||||
self, target_module: str, layer_id: int, lora_type: LoRAType
|
self, target_module: str, layer_id: int, lora_type: LoRAType
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Get LoRA tensor buffer (automatically handles both 3D and 4D tensors).
|
||||||
|
|
||||||
if lora_type == LoRAType.LORA_A:
|
if lora_type == LoRAType.LORA_A:
|
||||||
return self.A_buffer[target_module][layer_id]
|
return self.A_buffer[target_module][layer_id]
|
||||||
|
|
||||||
return self.B_buffer[target_module][layer_id]
|
Args:
|
||||||
|
target_module: Target module name (e.g., 'gate_up_proj' or 'gate_up_proj_moe' for MoE)
|
||||||
|
layer_id: Layer index
|
||||||
|
lora_type: LoRAType.LORA_A or LoRAType.LORA_B
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- 3D tensor [num_loras, rank, hidden] for standard modules
|
||||||
|
- 4D tensor [num_loras, num_experts, rank, hidden] for MoE modules
|
||||||
|
"""
|
||||||
|
buffer_dict = self.A_buffer if lora_type == LoRAType.LORA_A else self.B_buffer
|
||||||
|
|
||||||
|
return buffer_dict[target_module][layer_id]
|
||||||
|
|
||||||
def get_buffer_id(self, lora_uid: str):
|
def get_buffer_id(self, lora_uid: str):
|
||||||
return self.uid_to_buffer_id[lora_uid]
|
return self.uid_to_buffer_id[lora_uid]
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ def _fused_moe_lora_shrink(
|
|||||||
num_warps: int,
|
num_warps: int,
|
||||||
num_stages: int,
|
num_stages: int,
|
||||||
split_k: int,
|
split_k: int,
|
||||||
|
top_k_divisor: int = None,
|
||||||
mul_routed_weight: bool = False,
|
mul_routed_weight: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
w1_lora_a_stacked = lora_a_stacked[0]
|
w1_lora_a_stacked = lora_a_stacked[0]
|
||||||
@@ -292,7 +293,11 @@ def _fused_moe_lora_shrink(
|
|||||||
slice_c_size=a_intermediate_cache1.numel() // num_slices,
|
slice_c_size=a_intermediate_cache1.numel() // num_slices,
|
||||||
num_slice_a=1,
|
num_slice_a=1,
|
||||||
num_slice_c=num_slices,
|
num_slice_c=num_slices,
|
||||||
top_k=1 if mul_routed_weight else top_k_num,
|
top_k=(
|
||||||
|
top_k_divisor
|
||||||
|
if top_k_divisor is not None
|
||||||
|
else (1 if mul_routed_weight else top_k_num)
|
||||||
|
),
|
||||||
MUL_ROUTED_WEIGHT=False,
|
MUL_ROUTED_WEIGHT=False,
|
||||||
IS_PRIMARY=True,
|
IS_PRIMARY=True,
|
||||||
**shrink_config,
|
**shrink_config,
|
||||||
@@ -464,6 +469,11 @@ def _fused_moe_lora(
|
|||||||
num_tokens = M * top_k_num
|
num_tokens = M * top_k_num
|
||||||
w1_output_dim_size = w1_lora_b_stacked.shape[2]
|
w1_output_dim_size = w1_lora_b_stacked.shape[2]
|
||||||
|
|
||||||
|
# Detect whether input is already expanded (down path: [M*top_k, dim])
|
||||||
|
# or not (gate_up path: [M, dim]). Down path needs divisor=1.
|
||||||
|
input_is_expanded = qcurr_hidden_states.shape[0] == M * top_k_num
|
||||||
|
shrink_top_k_divisor = 1 if input_is_expanded else top_k_num
|
||||||
|
|
||||||
a_intermediate_cache1 = torch.zeros(
|
a_intermediate_cache1 = torch.zeros(
|
||||||
(num_slices, M, top_k_num, max_lora_rank),
|
(num_slices, M, top_k_num, max_lora_rank),
|
||||||
dtype=output.dtype,
|
dtype=output.dtype,
|
||||||
@@ -503,6 +513,7 @@ def _fused_moe_lora(
|
|||||||
shrink_num_warps,
|
shrink_num_warps,
|
||||||
shrink_num_stages,
|
shrink_num_stages,
|
||||||
shrink_split_k,
|
shrink_split_k,
|
||||||
|
top_k_divisor=shrink_top_k_divisor,
|
||||||
mul_routed_weight=False,
|
mul_routed_weight=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ def get_hidden_dim(
|
|||||||
return config.hidden_size, config.intermediate_size * 2
|
return config.hidden_size, config.intermediate_size * 2
|
||||||
elif module_name == "down_proj":
|
elif module_name == "down_proj":
|
||||||
return config.intermediate_size, config.hidden_size
|
return config.intermediate_size, config.hidden_size
|
||||||
|
elif module_name == "gate_up_proj_moe":
|
||||||
|
return config.hidden_size, config.moe_intermediate_size * 2
|
||||||
|
elif module_name == "down_proj_moe":
|
||||||
|
return config.moe_intermediate_size, config.hidden_size
|
||||||
elif module_name == "embed_tokens":
|
elif module_name == "embed_tokens":
|
||||||
# For embedding: input is vocab_size (as embedding lookup), output is hidden_size
|
# For embedding: input is vocab_size (as embedding lookup), output is hidden_size
|
||||||
# if contain extra tokens will be added; otherwise is 0.
|
# if contain extra tokens will be added; otherwise is 0.
|
||||||
@@ -148,6 +152,7 @@ def get_stacked_multiply(module_name: str) -> int:
|
|||||||
stacked_rank = {
|
stacked_rank = {
|
||||||
"qkv_proj": 3,
|
"qkv_proj": 3,
|
||||||
"gate_up_proj": 2,
|
"gate_up_proj": 2,
|
||||||
|
"gate_up_proj_moe": 2,
|
||||||
}
|
}
|
||||||
return stacked_rank[module_name] if module_name in stacked_rank else 1
|
return stacked_rank[module_name] if module_name in stacked_rank else 1
|
||||||
|
|
||||||
@@ -168,7 +173,7 @@ def get_target_module_name(full_module_name: str, target_modules: Set[str]) -> s
|
|||||||
|
|
||||||
|
|
||||||
EMBEDDING_NAMES = ["embed_tokens", "lm_head"]
|
EMBEDDING_NAMES = ["embed_tokens", "lm_head"]
|
||||||
ROW_PARALLELISM_LINEAR_LORA_NAMES = ["o_proj", "down_proj"]
|
ROW_PARALLELISM_LINEAR_LORA_NAMES = ["o_proj", "down_proj", "down_proj_moe"]
|
||||||
|
|
||||||
|
|
||||||
def get_lm_head_lora_b_shard_size(output_dim: int, shard_indices=None) -> int:
|
def get_lm_head_lora_b_shard_size(output_dim: int, shard_indices=None) -> int:
|
||||||
|
|||||||
@@ -51,6 +51,33 @@ DEFAULT_PROMPTS = [
|
|||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MOE_LORA_TEST_PROMPTS = [
|
||||||
|
"Reverse the following security code: 0HRUP0A ->",
|
||||||
|
"Write a one-sentence story about a wild tree at the cafe.",
|
||||||
|
"Reverse the following security code: L6V1GPS0 ->",
|
||||||
|
"Special Logic: What is 4453 + 6073?",
|
||||||
|
"Special Logic: What is 7918 + 560?",
|
||||||
|
"Write a one-sentence story about a slow cat in a simulation.",
|
||||||
|
"Write a one-sentence story about a dusty dragon in a castle.",
|
||||||
|
"Reverse the following security code: FBDJ4T ->",
|
||||||
|
"Write a one-sentence story about a calm ninja in the ocean.",
|
||||||
|
"Write a one-sentence story about a glowing fairy in Paris.",
|
||||||
|
"Special Logic: What is 6200 + 7656?",
|
||||||
|
"Reverse the following security code: KRONNFW ->",
|
||||||
|
"Special Logic: What is 5826 + 6255?",
|
||||||
|
"Write a one-sentence story about a shiny robot in the jungle.",
|
||||||
|
"Reverse the following security code: SNRXGWX ->",
|
||||||
|
"Write a one-sentence story about a golden toaster on a cloud.",
|
||||||
|
"Special Logic: What is 5286 + 5653?",
|
||||||
|
"Write a one-sentence story about a brave cowboy in a time machine.",
|
||||||
|
"Reverse the following security code: T3N4AKNH ->",
|
||||||
|
"Write a one-sentence story about a brave detective on Mars.",
|
||||||
|
]
|
||||||
|
|
||||||
|
MOE_BASE_MODEL_PATH = "Qwen/Qwen1.5-MoE-A2.7B"
|
||||||
|
MOE_LORA_PATH = "jonahbernard/sglang-lora-moe-test-qwen1.5-MoE-A2.7B"
|
||||||
|
|
||||||
|
|
||||||
CI_LORA_MODELS = [
|
CI_LORA_MODELS = [
|
||||||
LoRAModelCase(
|
LoRAModelCase(
|
||||||
base="meta-llama/Llama-3.1-8B-Instruct",
|
base="meta-llama/Llama-3.1-8B-Instruct",
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Copyright 2023-2025 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
import multiprocessing as mp
|
||||||
|
import unittest
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cuda_ci
|
||||||
|
from sglang.test.lora_utils import (
|
||||||
|
MOE_BASE_MODEL_PATH,
|
||||||
|
MOE_LORA_PATH,
|
||||||
|
MOE_LORA_TEST_PROMPTS,
|
||||||
|
)
|
||||||
|
from sglang.test.runners import SRTRunner
|
||||||
|
from sglang.test.test_utils import (
|
||||||
|
DEFAULT_PORT_FOR_SRT_TEST_RUNNER,
|
||||||
|
CustomTestCase,
|
||||||
|
is_in_ci,
|
||||||
|
)
|
||||||
|
|
||||||
|
register_cuda_ci(
|
||||||
|
est_time=200,
|
||||||
|
suite="stage-b-test-2-gpu-large",
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGPROB_THRESHOLD = 5e-04
|
||||||
|
MAX_NEW_TOKENS = 10
|
||||||
|
|
||||||
|
|
||||||
|
def _run_sglang_moe_lora(
|
||||||
|
tp_size: int,
|
||||||
|
prompts: List[str],
|
||||||
|
port: int = DEFAULT_PORT_FOR_SRT_TEST_RUNNER,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
lora_paths_per_prompt = [MOE_LORA_PATH] * len(prompts)
|
||||||
|
|
||||||
|
with SRTRunner(
|
||||||
|
model_path=MOE_BASE_MODEL_PATH,
|
||||||
|
torch_dtype=torch.bfloat16,
|
||||||
|
model_type="generation",
|
||||||
|
tp_size=tp_size,
|
||||||
|
lora_paths=[MOE_LORA_PATH],
|
||||||
|
max_loras_per_batch=1,
|
||||||
|
trust_remote_code=True,
|
||||||
|
disable_radix_cache=True,
|
||||||
|
port=port,
|
||||||
|
attention_backend="flashinfer",
|
||||||
|
mem_fraction_static=0.80,
|
||||||
|
) as runner:
|
||||||
|
outputs = runner.forward(
|
||||||
|
prompts,
|
||||||
|
max_new_tokens=MAX_NEW_TOKENS,
|
||||||
|
lora_paths=lora_paths_per_prompt,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"top_input_logprobs": outputs.top_input_logprobs,
|
||||||
|
"top_output_logprobs": outputs.top_output_logprobs,
|
||||||
|
"output_strs": outputs.output_strs,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestMoELoRATP2Logprobs(CustomTestCase):
|
||||||
|
"""Compare TP=1 vs TP=2 MoE LoRA: output strings must match and logprobs
|
||||||
|
must stay within threshold."""
|
||||||
|
|
||||||
|
def _assert_tp_parity(
|
||||||
|
self,
|
||||||
|
prompts: List[str],
|
||||||
|
label: str,
|
||||||
|
):
|
||||||
|
print(f"\n{'=' * 100}")
|
||||||
|
print(f" {label}: running TP=1")
|
||||||
|
print(f"{'=' * 100}")
|
||||||
|
|
||||||
|
tp1 = _run_sglang_moe_lora(tp_size=1, prompts=prompts)
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
print(f"\n{'=' * 100}")
|
||||||
|
print(f" {label}: running TP=2")
|
||||||
|
print(f"{'=' * 100}")
|
||||||
|
|
||||||
|
tp2 = _run_sglang_moe_lora(tp_size=2, prompts=prompts)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 100}")
|
||||||
|
print(
|
||||||
|
f"{'ID':<4} | {'String':<8} | {'Decode Max Diff':<18} | "
|
||||||
|
f"{'Decode Mean Diff':<18} | {'Status':<8} | {'Output (TP1)'}"
|
||||||
|
)
|
||||||
|
print("-" * 100)
|
||||||
|
|
||||||
|
for i in range(len(prompts)):
|
||||||
|
tp1_str = tp1["output_strs"][i].strip()
|
||||||
|
tp2_str = tp2["output_strs"][i].strip()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
tp1_str,
|
||||||
|
tp2_str,
|
||||||
|
f"Output string mismatch on prompt {i}: "
|
||||||
|
f"TP1='{tp1_str}' vs TP2='{tp2_str}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
tp1_raw = tp1["top_output_logprobs"][i]
|
||||||
|
tp2_raw = tp2["top_output_logprobs"][i]
|
||||||
|
tp1_lps = torch.tensor(
|
||||||
|
[t[0] if isinstance(t, list) else t for t in tp1_raw]
|
||||||
|
)
|
||||||
|
tp2_lps = torch.tensor(
|
||||||
|
[t[0] if isinstance(t, list) else t for t in tp2_raw]
|
||||||
|
)
|
||||||
|
min_len = min(tp1_lps.shape[0], tp2_lps.shape[0])
|
||||||
|
diff = torch.abs(tp1_lps[:min_len] - tp2_lps[:min_len])
|
||||||
|
max_diff = torch.max(diff).item() if min_len > 0 else 0.0
|
||||||
|
mean_diff = torch.mean(diff).item() if min_len > 0 else 0.0
|
||||||
|
|
||||||
|
status = "PASS" if max_diff < LOGPROB_THRESHOLD else "FAIL"
|
||||||
|
print(
|
||||||
|
f"{i:<4} | {'OK':<8} | {max_diff:<18.6e} | "
|
||||||
|
f"{mean_diff:<18.6e} | {status:<8} | {tp1_str[:40]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertLessEqual(
|
||||||
|
max_diff,
|
||||||
|
LOGPROB_THRESHOLD,
|
||||||
|
f"Decode logprob diff too large on prompt {i}: "
|
||||||
|
f"max_diff={max_diff:.6e} > threshold={LOGPROB_THRESHOLD:.0e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
def test_moe_lora_tp2_vs_tp1_basic(self):
|
||||||
|
"""Basic TP=1 vs TP=2 parity with a small prompt set."""
|
||||||
|
self._assert_tp_parity(
|
||||||
|
prompts=MOE_LORA_TEST_PROMPTS[:5],
|
||||||
|
label="MoE LoRA TP parity (basic)",
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(is_in_ci(), "Skipping full test in CI")
|
||||||
|
def test_moe_lora_tp2_vs_tp1_full(self):
|
||||||
|
"""Full TP=1 vs TP=2 parity across all prompts."""
|
||||||
|
self._assert_tp_parity(
|
||||||
|
prompts=MOE_LORA_TEST_PROMPTS,
|
||||||
|
label="MoE LoRA TP parity (full)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
mp.set_start_method("spawn")
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
unittest.main(warnings="ignore", verbosity=2)
|
||||||
|
finally:
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
torch.cuda.synchronize()
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
"""
|
||||||
|
Regression test for MoE LoRA parity between SGLang and vLLM.
|
||||||
|
|
||||||
|
This test compares SGLang's logprobs and output strings against a hardcoded
|
||||||
|
baseline (VLLM_CACHED_RESULTS) generated using vLLM. It enforces strict
|
||||||
|
numerical accuracy by asserting that the maximum and mean logprob
|
||||||
|
divergences do not exceed the reference thresholds (REFERENCE_STATS).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python -m unittest test_lora_moe_vllm_sgl_logprob_diff.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import multiprocessing as mp
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cuda_ci
|
||||||
|
from sglang.test.lora_utils import (
|
||||||
|
MOE_BASE_MODEL_PATH,
|
||||||
|
MOE_LORA_PATH,
|
||||||
|
MOE_LORA_TEST_PROMPTS,
|
||||||
|
)
|
||||||
|
from sglang.test.runners import SRTRunner
|
||||||
|
|
||||||
|
register_cuda_ci(
|
||||||
|
est_time=25,
|
||||||
|
suite="stage-b-test-1-gpu-large",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format: [{"text": "result string", "lps": [0.1, 0.2, ...]}, ...]
|
||||||
|
VLLM_CACHED_RESULTS = [
|
||||||
|
{
|
||||||
|
"text": " A0PURH0",
|
||||||
|
"lps": [
|
||||||
|
-3.3378546504536644e-06,
|
||||||
|
-1.6331539882230572e-05,
|
||||||
|
-7.152555099310121e-07,
|
||||||
|
-5.054346183896996e-05,
|
||||||
|
-4.792098479811102e-05,
|
||||||
|
-3.302042750874534e-05,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " The wild tree jumped at the cafe and found a",
|
||||||
|
"lps": [
|
||||||
|
-9.417489309271332e-06,
|
||||||
|
-1.2636104656849056e-05,
|
||||||
|
-0.00018308870494365692,
|
||||||
|
-0.0006621075444854796,
|
||||||
|
-5.3165931603871286e-05,
|
||||||
|
-9.500529267825186e-05,
|
||||||
|
-0.0003022690652869642,
|
||||||
|
-6.9141146923357155e-06,
|
||||||
|
0.0,
|
||||||
|
-8.22540732769994e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " 0SPG1V6L",
|
||||||
|
"lps": [
|
||||||
|
-2.861018856492592e-06,
|
||||||
|
-6.8662193370983e-05,
|
||||||
|
-6.580135959666222e-05,
|
||||||
|
-5.6980417866725475e-05,
|
||||||
|
-8.916457591112703e-05,
|
||||||
|
-5.006777428206988e-06,
|
||||||
|
-1.8596476365928538e-05,
|
||||||
|
-2.396077979938127e-05,
|
||||||
|
-4.851700214203447e-05,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{"text": " Tango", "lps": [-5.960462772236497e-07, -9.536738616588991e-07]},
|
||||||
|
{"text": " Tensor", "lps": [-0.0002002515539061278, -5.960462772236497e-07]},
|
||||||
|
{
|
||||||
|
"text": " The slow cat coded in a simulation and found a",
|
||||||
|
"lps": [
|
||||||
|
0.0,
|
||||||
|
-4.672895011026412e-05,
|
||||||
|
-3.802703940891661e-05,
|
||||||
|
-3.1709168979432434e-05,
|
||||||
|
0.0,
|
||||||
|
-2.145764938177308e-06,
|
||||||
|
-4.565611743601039e-05,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-2.145764938177308e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " The dusty dragon slept in a castle and found a",
|
||||||
|
"lps": [
|
||||||
|
0.0,
|
||||||
|
-3.290122185717337e-05,
|
||||||
|
-1.1444026313256472e-05,
|
||||||
|
-6.544376083184034e-05,
|
||||||
|
-8.344646857949556e-07,
|
||||||
|
-2.276871418871451e-05,
|
||||||
|
-2.1576648578047752e-05,
|
||||||
|
-5.960462772236497e-07,
|
||||||
|
0.0,
|
||||||
|
-2.50339189733495e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " T4JDBF",
|
||||||
|
"lps": [
|
||||||
|
-5.960462772236497e-07,
|
||||||
|
-3.4450891689630225e-05,
|
||||||
|
-1.1324817933200393e-05,
|
||||||
|
-1.6689160474925302e-05,
|
||||||
|
-0.00020013237372040749,
|
||||||
|
-3.45700973412022e-05,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " The calm ninja painted in the ocean and found a",
|
||||||
|
"lps": [
|
||||||
|
0.0,
|
||||||
|
-3.731181277544238e-05,
|
||||||
|
-6.198863957251888e-06,
|
||||||
|
-3.576272320060525e-06,
|
||||||
|
-3.576278118089249e-07,
|
||||||
|
-3.814689989667386e-06,
|
||||||
|
-1.549708758830093e-05,
|
||||||
|
-1.1920928244535389e-07,
|
||||||
|
0.0,
|
||||||
|
-4.0531076592742465e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " The glowing fairy painted in Paris and found a secret",
|
||||||
|
"lps": [
|
||||||
|
-1.1920928244535389e-07,
|
||||||
|
-2.8132995794294402e-05,
|
||||||
|
-2.50339189733495e-06,
|
||||||
|
-4.446407547220588e-05,
|
||||||
|
-3.576278118089249e-07,
|
||||||
|
-8.201262971851975e-05,
|
||||||
|
-3.576278118089249e-07,
|
||||||
|
0.0,
|
||||||
|
-4.0531076592742465e-06,
|
||||||
|
-3.4570634852570947e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{"text": " Tensor", "lps": [-0.00014399446081370115, -2.622600959512056e-06]},
|
||||||
|
{
|
||||||
|
"text": " WFNNORK",
|
||||||
|
"lps": [
|
||||||
|
-0.0003231241717003286,
|
||||||
|
-3.71926071238704e-05,
|
||||||
|
-0.00011252723925281316,
|
||||||
|
-5.447716102935374e-05,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " Whiskey",
|
||||||
|
"lps": [
|
||||||
|
-5.531158240046352e-05,
|
||||||
|
-1.5497195136049413e-06,
|
||||||
|
-1.1920922133867862e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " The shiny robot built in the jungle and found a",
|
||||||
|
"lps": [
|
||||||
|
0.0,
|
||||||
|
-2.622600959512056e-06,
|
||||||
|
-5.018585216021165e-05,
|
||||||
|
-0.0015173362335190177,
|
||||||
|
0.0,
|
||||||
|
-6.198863957251888e-06,
|
||||||
|
-0.00036769305006600916,
|
||||||
|
-1.1920928244535389e-07,
|
||||||
|
0.0,
|
||||||
|
-3.099436753473128e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " XWGXRNS",
|
||||||
|
"lps": [
|
||||||
|
-2.5629668016335927e-05,
|
||||||
|
-4.0531076592742465e-06,
|
||||||
|
-0.0001616347290109843,
|
||||||
|
-5.018585216021165e-05,
|
||||||
|
-0.00011920218821614981,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " The golden toaster exploded on a cloud and found a",
|
||||||
|
"lps": [
|
||||||
|
0.0,
|
||||||
|
-8.630380034446716e-05,
|
||||||
|
0.0,
|
||||||
|
-2.4676019165781327e-05,
|
||||||
|
-1.0728830375228426e-06,
|
||||||
|
-1.5497195136049413e-06,
|
||||||
|
-6.794906312279636e-06,
|
||||||
|
-4.887569048150908e-06,
|
||||||
|
0.0,
|
||||||
|
-3.3378546504536644e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " Nebula",
|
||||||
|
"lps": [
|
||||||
|
-4.410734163684538e-06,
|
||||||
|
-7.986990567587782e-06,
|
||||||
|
-1.1920922133867862e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " The brave cowboy vanished in a time machine and found",
|
||||||
|
"lps": [
|
||||||
|
0.0,
|
||||||
|
-8.475421054754406e-05,
|
||||||
|
-0.00011932138295378536,
|
||||||
|
-0.00016735584358684719,
|
||||||
|
-2.3841855067985307e-07,
|
||||||
|
-2.312633478140924e-05,
|
||||||
|
-6.5205356804654e-05,
|
||||||
|
-0.00014423283573705703,
|
||||||
|
-1.4305104514278355e-06,
|
||||||
|
0.0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " HNKA4N3T",
|
||||||
|
"lps": [
|
||||||
|
-2.50339189733495e-06,
|
||||||
|
-1.1920928244535389e-07,
|
||||||
|
-5.006777428206988e-06,
|
||||||
|
-7.390948667307384e-06,
|
||||||
|
-0.00014327930693980306,
|
||||||
|
-2.3841855067985307e-07,
|
||||||
|
-0.00011062010162277147,
|
||||||
|
-1.2874520507466514e-05,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " The brave detective slept on Mars and found a secret",
|
||||||
|
"lps": [
|
||||||
|
-1.7881377516459906e-06,
|
||||||
|
-1.9788545614574105e-05,
|
||||||
|
-1.883488948806189e-05,
|
||||||
|
-1.4781842764932662e-05,
|
||||||
|
-3.576278118089249e-07,
|
||||||
|
-1.2755313036905136e-05,
|
||||||
|
-5.960462772236497e-07,
|
||||||
|
0.0,
|
||||||
|
-4.0531076592742465e-06,
|
||||||
|
-1.5497195136049413e-06,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
# ---------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
# Hardcoded reference stats from successful run. Corresponds to prompts below.
|
||||||
|
REFERENCE_STATS = {
|
||||||
|
0: {"max": 9.29792076931335e-06, "mean": 2.8410576836298182e-06},
|
||||||
|
1: {"max": 1.3818731531500816e-05, "mean": 3.753847045118164e-06},
|
||||||
|
2: {"max": 1.1205123882973567e-05, "mean": 2.410548404441215e-06},
|
||||||
|
3: {"max": 1.1920923270736239e-07, "mean": 1.1920920428565296e-07},
|
||||||
|
4: {"max": 1.0011601261794567e-05, "mean": 5.065405247250965e-06},
|
||||||
|
5: {"max": 5.602585588349029e-06, "mean": 1.6569420949963388e-06},
|
||||||
|
6: {"max": 2.9801594791933894e-06, "mean": 8.702030129370542e-07},
|
||||||
|
7: {"max": 1.6685822629369795e-05, "mean": 4.608787548932014e-06},
|
||||||
|
8: {"max": 2.384102117503062e-06, "mean": 5.721932211599778e-07},
|
||||||
|
9: {"max": 1.704567694105208e-05, "mean": 1.9787427085304897e-06},
|
||||||
|
10: {"max": 1.2515258276835084e-05, "mean": 6.37683808690781e-06},
|
||||||
|
11: {"max": 1.4900237147230655e-05, "mean": 1.0101463885803241e-05},
|
||||||
|
12: {"max": 1.6688391042407602e-06, "mean": 5.960160933682346e-07},
|
||||||
|
13: {"max": 9.04605258256197e-06, "mean": 1.2144706943217897e-06},
|
||||||
|
14: {"max": 2.181154559366405e-05, "mean": 6.102668112362153e-06},
|
||||||
|
15: {"max": 5.602370947599411e-06, "mean": 6.07920344464219e-07},
|
||||||
|
16: {"max": 2.2649692255072296e-06, "mean": 7.549897418357432e-07},
|
||||||
|
17: {"max": 1.990482269320637e-05, "mean": 3.3731695992855747e-06},
|
||||||
|
18: {"max": 1.6567864804528654e-05, "mean": 3.307691372356203e-06},
|
||||||
|
19: {"max": 2.5033668862306513e-06, "mean": 3.3378251487192754e-07},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestMoELoraRegression(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_sglang_moe_parity_strict(self):
|
||||||
|
|
||||||
|
with SRTRunner(
|
||||||
|
model_path=MOE_BASE_MODEL_PATH,
|
||||||
|
torch_dtype=torch.bfloat16,
|
||||||
|
model_type="generation",
|
||||||
|
lora_paths=[MOE_LORA_PATH],
|
||||||
|
max_loras_per_batch=1,
|
||||||
|
tp_size=1,
|
||||||
|
trust_remote_code=True,
|
||||||
|
disable_radix_cache=True,
|
||||||
|
attention_backend="flashinfer",
|
||||||
|
mem_fraction_static=0.80,
|
||||||
|
) as srt_runner:
|
||||||
|
|
||||||
|
srt_outputs = srt_runner.forward(
|
||||||
|
MOE_LORA_TEST_PROMPTS,
|
||||||
|
max_new_tokens=10,
|
||||||
|
lora_paths=[MOE_LORA_PATH] * len(MOE_LORA_TEST_PROMPTS),
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n" + "=" * 140)
|
||||||
|
print(
|
||||||
|
f"{'ID':<4} | {'Max Diff':<12} | {'Mean Diff':<12} | {'Status':<8} | {'Prompt'}"
|
||||||
|
)
|
||||||
|
print("-" * 140)
|
||||||
|
|
||||||
|
for i, prompt in enumerate(MOE_LORA_TEST_PROMPTS):
|
||||||
|
v_data = VLLM_CACHED_RESULTS[i]
|
||||||
|
v_lps = v_data["lps"]
|
||||||
|
v_text = v_data["text"].strip()
|
||||||
|
|
||||||
|
s_lps_raw = srt_outputs.top_output_logprobs[i]
|
||||||
|
s_lps = [
|
||||||
|
float(token[0]) if isinstance(token, list) else float(token)
|
||||||
|
for token in s_lps_raw
|
||||||
|
]
|
||||||
|
s_text = srt_outputs.output_strs[i].strip()
|
||||||
|
|
||||||
|
# Calculate actual stats
|
||||||
|
min_len = min(len(v_lps), len(s_lps))
|
||||||
|
diffs = [abs(v_lps[t] - s_lps[t]) for t in range(min_len)]
|
||||||
|
|
||||||
|
actual_max = max(diffs) if diffs else 0.0
|
||||||
|
actual_mean = sum(diffs) / len(diffs) if diffs else 0.0
|
||||||
|
|
||||||
|
ref = REFERENCE_STATS[i]
|
||||||
|
# Epsilon to allow room for different, but correct, implementations
|
||||||
|
eps = 1e-4
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertEqual(v_text, s_text, f"String mismatch on prompt {i}")
|
||||||
|
self.assertLessEqual(
|
||||||
|
actual_max, ref["max"] + eps, f"Max LogProb Diff exceeded on prompt {i}"
|
||||||
|
)
|
||||||
|
self.assertLessEqual(
|
||||||
|
actual_mean,
|
||||||
|
ref["mean"] + eps,
|
||||||
|
f"Mean LogProb Diff exceeded on prompt {i}",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{i:<4} | {actual_max:<12.6f} | {actual_mean:<12.6f} | {'✅ PASS':<8} | {prompt}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("=" * 140)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
mp.set_start_method("spawn")
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
unittest.main(warnings="ignore", verbosity=2)
|
||||||
|
finally:
|
||||||
|
# Final cleanup
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
torch.cuda.synchronize()
|
||||||
Reference in New Issue
Block a user