chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,305 @@
import torch
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
from sglang.srt.lora.utils import LoRABatchInfo
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.utils import is_npu
if is_npu():
import sgl_kernel_npu # noqa: F401
import torch_npu # noqa: F401
class AscendLoRABackend(BaseLoRABackend):
name = "ascend"
def __init__(
self,
max_loras_per_batch: int,
device: torch.device,
**kwargs,
):
super().__init__(max_loras_per_batch, device)
def run_lora_a_sgemm(
self, x: torch.Tensor, weights: torch.Tensor, *args, **kwargs
) -> torch.Tensor:
total_seq_len, _ = x.shape
_, weight_out_dim, _ = weights.shape
output_tensor = torch.zeros(
(total_seq_len, weight_out_dim), dtype=x.dtype, device=x.device
)
torch.ops.npu.sgmv_shrink(
x,
weights,
self.batch_info.weight_indices,
self.batch_info.seg_lens,
output_tensor,
1.0,
)
scaling = (
self.batch_info.scalings.gather(0, self.batch_info.weight_indices)
.repeat_interleave(self.batch_info.seg_lens, output_size=total_seq_len)
.unsqueeze(-1)
)
output_tensor *= scaling
return output_tensor
def run_lora_b_sgemm(
self,
x: torch.Tensor,
weights: torch.Tensor,
base_output: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
total_seq_len, _ = x.shape
_, weight_out_dim, _ = weights.shape
if base_output is None:
output_tensor = torch.zeros(
(total_seq_len, weight_out_dim), device=x.device, dtype=x.dtype
)
else:
output_tensor = base_output
torch.ops.npu.sgmv_expand(
x,
weights,
self.batch_info.weight_indices,
self.batch_info.seg_lens,
output_tensor,
0,
weight_out_dim,
)
return output_tensor
def run_qkv_lora(
self,
x: torch.Tensor,
qkv_lora_a: torch.Tensor,
qkv_lora_b: torch.Tensor,
output_offset: torch.Tensor,
output_offset_cpu: torch.Tensor,
max_qkv_out_dim: int,
base_output: torch.Tensor = None,
n_slices: int = 3,
*args,
**kwargs,
) -> torch.Tensor:
assert isinstance(qkv_lora_b, torch.Tensor)
total_seq_len, _ = x.shape
_, weight_intermediate_dim, _ = qkv_lora_a.shape
_, weight_out_dim, _ = qkv_lora_b.shape
max_rank = weight_intermediate_dim // n_slices
if base_output is None:
output_tensor = torch.zeros(
(total_seq_len, weight_out_dim), device=x.device, dtype=x.dtype
)
else:
output_tensor = base_output
lora_a_output = torch.zeros(
total_seq_len, weight_intermediate_dim, dtype=x.dtype, device=x.device
)
torch.ops.npu.sgmv_shrink(
x,
qkv_lora_a,
self.batch_info.weight_indices,
self.batch_info.seg_lens,
lora_a_output,
1.0,
)
scaling = (
self.batch_info.scalings.gather(0, self.batch_info.weight_indices)
.repeat_interleave(self.batch_info.seg_lens, output_size=total_seq_len)
.unsqueeze(-1)
)
lora_a_output *= scaling
for slice_id in range(n_slices):
slice_offset = output_offset_cpu[slice_id]
slice_offset_next = output_offset_cpu[slice_id + 1]
slice_size = slice_offset_next - slice_offset
torch.ops.npu.sgmv_expand(
lora_a_output[:, (max_rank * slice_id) : (max_rank * (slice_id + 1))],
qkv_lora_b[:, slice_offset:slice_offset_next],
self.batch_info.weight_indices,
self.batch_info.seg_lens,
output_tensor,
slice_offset,
slice_size,
)
return output_tensor
def run_gate_up_lora(
self,
x: torch.Tensor,
gate_up_lora_a: torch.Tensor,
gate_up_lora_b: torch.Tensor,
base_output: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
num_slices = 2
assert isinstance(gate_up_lora_b, torch.Tensor)
total_seq_len, _ = x.shape
_, weight_intermediate_dim, _ = gate_up_lora_a.shape
_, weight_out_dim, _ = gate_up_lora_b.shape
slice_size = weight_out_dim // num_slices
max_rank = weight_intermediate_dim // num_slices
if base_output is None:
output_tensor = torch.zeros(
(total_seq_len, weight_out_dim), device=x.device, dtype=x.dtype
)
else:
output_tensor = base_output
lora_a_output = torch.zeros(
total_seq_len, weight_intermediate_dim, dtype=x.dtype, device=x.device
)
torch.ops.npu.sgmv_shrink(
x,
gate_up_lora_a,
self.batch_info.weight_indices,
self.batch_info.seg_lens,
lora_a_output,
1.0,
)
scaling = (
self.batch_info.scalings.gather(0, self.batch_info.weight_indices)
.repeat_interleave(self.batch_info.seg_lens, output_size=total_seq_len)
.unsqueeze(-1)
)
lora_a_output *= scaling
slice_offset = 0
for slice_id in range(num_slices):
torch.ops.npu.sgmv_expand(
lora_a_output[:, (max_rank * slice_id) : (max_rank * (slice_id + 1))],
gate_up_lora_b[:, slice_offset : slice_offset + slice_size],
self.batch_info.weight_indices,
self.batch_info.seg_lens,
output_tensor,
slice_offset,
slice_size,
)
slice_offset += slice_size
return output_tensor
def init_cuda_graph_batch_info(
self,
max_bs_in_cuda_graph: int,
num_tokens_per_bs: int,
):
with torch.device("npu"):
self.npu_graph_batch_info = LoRABatchInfo(
bs=max_bs_in_cuda_graph,
use_cuda_graph=True,
num_segments=None,
seg_lens=torch.full(
(max_bs_in_cuda_graph,), num_tokens_per_bs, dtype=torch.int32
),
seg_indptr=torch.empty(max_bs_in_cuda_graph + 1, dtype=torch.int32),
max_len=num_tokens_per_bs,
weight_indices=torch.zeros(max_bs_in_cuda_graph, dtype=torch.int32),
lora_ranks=torch.zeros(self.max_loras_per_batch, dtype=torch.int32),
scalings=torch.zeros(self.max_loras_per_batch, dtype=torch.float),
permutation=None,
)
# Initialize seg_indptr for NPU graph as they remain constant
# across batches.
torch.cumsum(
self.npu_graph_batch_info.seg_lens[:max_bs_in_cuda_graph],
dim=0,
out=self.npu_graph_batch_info.seg_indptr[1 : max_bs_in_cuda_graph + 1],
)
def prepare_lora_batch(
self,
forward_batch: ForwardBatch,
weight_indices: list[int],
lora_ranks: list[int],
scalings: list[float],
use_cuda_graph: bool,
):
# Use pinned memory to avoid synchronizations during host-to-device transfer
weight_indices_tensor = torch.tensor(
weight_indices, dtype=torch.int32, pin_memory=True, device="cpu"
)
lora_ranks_tensor = torch.tensor(
lora_ranks, dtype=torch.int32, pin_memory=True, device="cpu"
)
scalings_tensor = torch.tensor(
scalings, dtype=torch.float, pin_memory=True, device="cpu"
)
bs = forward_batch.batch_size
if use_cuda_graph:
assert (
self.npu_graph_batch_info is not None
), "NPU Graph batch info is not initialized."
batch_info = self.npu_graph_batch_info
batch_info.bs = forward_batch.batch_size
batch_info.num_segments = forward_batch.batch_size
else:
max_len = (
# Calculate max_len from the CPU copy to avoid D2H transfer.
max(forward_batch.extend_seq_lens_cpu)
if forward_batch.forward_mode.is_extend()
else 1
)
seg_lens = (
forward_batch.extend_seq_lens
if forward_batch.forward_mode.is_extend()
else torch.ones(bs, dtype=torch.int32, device=self.device)
)
seg_indptr = torch.zeros((bs + 1,), dtype=torch.int32, device=self.device)
seg_indptr[1:] = torch.cumsum(seg_lens, dim=0)
batch_info = LoRABatchInfo(
bs=forward_batch.batch_size,
num_segments=forward_batch.batch_size,
max_len=max_len,
use_cuda_graph=False,
seg_lens=seg_lens,
seg_indptr=seg_indptr,
weight_indices=torch.empty(
(bs,), dtype=torch.int32, device=self.device
),
lora_ranks=torch.empty(
(self.max_loras_per_batch,), dtype=torch.int32, device=self.device
),
scalings=torch.empty(
(self.max_loras_per_batch,), dtype=torch.float, device=self.device
),
permutation=None,
)
# Copy to device asynchronously
batch_info.lora_ranks[: self.max_loras_per_batch].copy_(
lora_ranks_tensor, non_blocking=True
)
batch_info.scalings[: self.max_loras_per_batch].copy_(
scalings_tensor, non_blocking=True
)
batch_info.weight_indices[:bs].copy_(weight_indices_tensor, non_blocking=True)
batch_info = self._add_moe_lora_info(forward_batch, batch_info)
self.batch_info = batch_info
@@ -0,0 +1,447 @@
from typing import Tuple, Union
import torch
import triton
import triton.language as tl
from sglang.srt.lora.backend.lmhead_mixing import LoRABackendLmHeadMixing
from sglang.srt.lora.utils import LoRABatchInfo, MoELoRABatchInfo
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
class BaseLoRABackend(LoRABackendLmHeadMixing):
"""Base class for different Lora backends.
Each backend has its own implementation of Lora kernels.
Args:
max_loras_per_batch: maximum number of different lora weights
that can be applied in a single forward batch.
device: the device where the backend runs.
"""
def __init__(self, max_loras_per_batch: int, device: torch.device):
self.max_loras_per_batch = max_loras_per_batch
self.device = device
self.init_lm_head_config()
self._is_moe_lora = False
def run_lora_a_embedding(
self,
input_ids: torch.Tensor,
weights: torch.Tensor,
vocab_size: int,
extra_embeddings: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
"""Run LoRA A embedding lookup with CUDA graph support.
Args:
input_ids: token IDs with shape (s,), where s is the sum of all sequence lengths
weights: LoRA A embedding weights with shape (num_loras, rank, vocab_size)
vocab_size: base vocabulary size (tokens >= vocab_size are extra tokens)
extra_embeddings: extra token embeddings with shape (num_loras, num_extra_tokens, rank)
Only needed if there are added tokens beyond base vocabulary.
Returns:
result with shape (s, rank)
"""
pass
def run_extra_token_embedding(
self,
input_ids: torch.Tensor,
output: torch.Tensor,
extra_embeddings: torch.Tensor,
vocab_size: int,
*args,
**kwargs,
) -> torch.Tensor:
"""
Apply extra token embeddings to output in-place.
Args:
input_ids: (s,) token IDs
output: (s, embed_dim) output tensor to be modified
extra_embeddings: (num_loras, num_extra_tokens, embed_dim) extra embeddings
vocab_size: base vocabulary size
Returns:
output: modified output tensor
"""
raise NotImplementedError
def run_lora_a_sgemm(
self, x: torch.Tensor, weights: torch.Tensor, *args, **kwargs
) -> torch.Tensor:
"""Run segment Gemm of lora a modules with current backend.
The definition of segment Gemm can be referred to https://docs.flashinfer.ai/api/gemm.html.
Args:
x: input matrix with shape (s, input_dim), here s is the sum of all sequence lengths
weights: a set of lora weights with shape (num_lora, c * r, input_dim),
here r is lora rank, c is a multiplier for stacked modules (e.g., c=3 for qkv_proj, c=2 for gate_up_proj)
usually input_dim is much larger than r
Returns:
result with shape (s, c * r)
"""
pass
def run_lora_b_sgemm(
self, x: torch.Tensor, weights: torch.Tensor, *args, **kwargs
) -> torch.Tensor:
"""Run segment Gemm of lora b modules with current backend.
The definition of segment Gemm can be referred to https://docs.flashinfer.ai/api/gemm.html.
Args:
x: input matrix with shape (s, r), here s is the sum of all sequence lengths, r is lora rank
weights: a set of lora weights with shape (num_lora, output_dim, r)
usually output_dim is much larger than r
Returns:
result with shape (s, output_dim)
"""
pass
def run_qkv_lora(
self,
x: torch.Tensor,
qkv_lora_a: torch.Tensor,
qkv_lora_b: Union[torch.Tensor, Tuple[torch.Tensor]],
*args,
**kwargs,
) -> torch.Tensor:
"""Run the lora pass for QKV Layer.
Args:
x: input matrix with shape (s, input_dim), here s is the sum of all sequence lengths
qkv_lora_a: lora_a module for qkv, with shape (num_lora, 3 * r, input_dim)
qkv_lora_b: lora_b module for qkv.
If passed in as a tensor, its shape should be (num_lora,output_dim_q + 2 * output_dim_kv, r)
If passed in as a tuple of two tensors, it should contain:
a lora_b module for q, with shape (1, num_lora, output_dim_q, r)
and a combined lora_b module for kv, with shape (2, num_lora, output_dim_kv, r)
Returns:
result with shape (s, output_dim_q + 2 * output_dim_kv)
"""
pass
def run_gate_up_lora(
self,
x: torch.Tensor,
gate_up_lora_a: torch.Tensor,
gate_up_lora_b: Union[torch.Tensor, Tuple[torch.Tensor]],
*args,
**kwargs,
) -> torch.Tensor:
"""Run the lora pass for gate_up_proj, usually attached to MergedColumnParallelLayer.
Args:
x: input matrix with shape (s, input_dim), here s is the sum of all sequence lengths
gate_up_lora_a: lora_a module for gate_up_proj, with shape (num_lora, 2 * r, input_dim)
gate_up_lora_b: lora_b module for qkv.
If passed in as a tensor, its shape should be (num_lora, 2 * output_dim, r)
If passed in as a tuple, it should contain two tensors with shape (num_lora, output_dim, r)
Returns:
result with shape (s, 2 * output_dim)
"""
pass
def init_cuda_graph_batch_info(
self,
max_bs_in_cuda_graph: int,
num_tokens_per_bs: int,
):
"""Phase 2 of LoRA CUDA graph init: dense LoRA batch metadata.
Called during CudaGraphRunner.__init__(), after init_memory_pool().
Args:
max_bs_in_cuda_graph: maximum batch size for CUDA Graph mode
num_tokens_per_bs: number of tokens per sequence (1 for decoding, >1 for target_verify)
"""
pass
@property
def is_moe_lora(self) -> bool:
return self._is_moe_lora
@is_moe_lora.setter
def is_moe_lora(self, value: bool):
self._is_moe_lora = value
def init_cuda_graph_moe_buffers(
self,
max_bs: int,
max_loras: int,
compute_dtype: torch.dtype,
moe_layer,
):
"""Phase 1 of LoRA CUDA graph init: MoE intermediate buffers.
Called once before init_memory_pool() with a representative MoE layer
to extract dimensions. All FusedMoEWithLoRA layers share the same
buffers since they execute sequentially during forward.
This is backend-agnostic because MoE LoRA always uses the same
fused Triton kernel (TritonRunnerCoreWithLoRA) regardless of which
dense LoRA backend is selected.
"""
base = moe_layer.base_layer
top_k = base.top_k
qinfo = moe_layer._quant_info
E, N, _ = qinfo.w13_weight.shape
hidden_dim = qinfo.w2_weight.shape[1]
device = qinfo.w13_weight.device
dtype = compute_dtype
num_experts = base.num_experts
block_size_m = 64
max_num_tokens_padded = max_bs * top_k + 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
self.moe_cg_buffers = {
"intermediate_cache1": torch.empty(
(max_bs, top_k, N), device=device, dtype=dtype
),
"intermediate_cache2": torch.empty(
(max_bs * top_k, N // 2), device=device, dtype=dtype
),
"intermediate_cache3": torch.empty(
(max_bs, top_k, hidden_dim), device=device, dtype=dtype
),
"out_hidden_states": torch.empty(
(max_bs, hidden_dim), device=device, dtype=dtype
),
"sorted_token_ids_lora": torch.empty(
(max_loras * max_num_tokens_padded,),
device=device,
dtype=torch.int32,
),
"expert_ids_lora": torch.empty(
(max_loras * max_num_m_blocks,),
device=device,
dtype=torch.int32,
),
"num_tokens_post_padded_lora": torch.empty(
(max_loras,), device=device, dtype=torch.int32
),
"adapter_enabled": torch.zeros(max_loras, dtype=torch.int32, device=device),
# int64 copy of weight_indices for index_fill_(), which requires
# LongTensor. weight_indices itself must stay int32 because the
# CUDA moe_lora_align kernel casts it to int32_t*.
"weight_indices_long": torch.zeros(
max_bs, dtype=torch.int64, device=device
),
"lora_ids": torch.arange(max_loras, dtype=torch.int32, device=device),
"cumsum_buffer": torch.zeros(
max_loras * (num_experts + 1),
dtype=torch.int32,
device=device,
),
"token_mask": torch.empty(
(max_loras * max_bs * top_k,),
dtype=torch.int32,
device=device,
),
"max_num_tokens_padded": max_num_tokens_padded,
"max_num_m_blocks": max_num_m_blocks,
"token_lora_mapping": torch.full(
(max_bs,), -1, dtype=torch.int32, device=device
),
}
def _add_moe_lora_info(
self, forward_batch: ForwardBatch, batch_info: LoRABatchInfo
) -> LoRABatchInfo:
if not self.is_moe_lora:
return batch_info
if batch_info.use_cuda_graph:
adapter_enabled = self.moe_cg_buffers["adapter_enabled"]
token_lora_mapping = self.moe_cg_buffers["token_lora_mapping"]
else:
adapter_enabled = None
token_lora_mapping = None
num_tokens = (
sum(forward_batch.extend_seq_lens_cpu)
if forward_batch.forward_mode.is_extend()
else forward_batch.batch_size
)
max_len = (
max(forward_batch.extend_seq_lens_cpu)
if forward_batch.forward_mode.is_extend()
else 1
)
if (
batch_info.req_seg_indptr is not None
or batch_info.req_weight_indices is not None
):
assert batch_info.req_seg_indptr is not None
assert batch_info.req_weight_indices is not None
num_moe_segments = batch_info.bs
seg_indptr = batch_info.req_seg_indptr[: num_moe_segments + 1]
req_to_lora = batch_info.req_weight_indices[:num_moe_segments]
else:
num_moe_segments = batch_info.num_segments
seg_indptr = batch_info.seg_indptr[: num_moe_segments + 1]
req_to_lora = batch_info.weight_indices[:num_moe_segments]
adapter_enabled, token_lora_mapping = _compute_moe_lora_info(
num_tokens,
seg_indptr,
batch_info.lora_ranks,
req_to_lora,
adapter_enabled,
token_lora_mapping,
max_len=max_len,
)
batch_info.moe_lora_info = MoELoRABatchInfo(
seg_indptr=seg_indptr,
req_to_lora=req_to_lora,
adapter_enabled=adapter_enabled,
token_lora_mapping=token_lora_mapping,
)
return batch_info
def prepare_lora_batch(
self,
forward_batch: ForwardBatch,
weight_indices: list[int],
lora_ranks: list[int],
scalings: list[float],
use_cuda_graph: bool,
):
"""Prepare the lora weights and batch info for current forward batch.
This method provides a hook for each backend to conduct its own preparation
logic for each forward batch.
Args:
forward_batch: the ForwardBatch object for current forward pass
weight_indices: list of indices of lora weights to be applied for current batch
lora_ranks: list of lora ranks corresponding to weight_indices
scalings: list of scaling factors corresponding to weight_indices
use_cuda_graph: whether to use CUDA Graph for this batch
"""
pass
@triton.jit
def _compute_moe_lora_info_kernel(
seg_indptr_ptr,
lora_ranks_ptr,
weight_indices_ptr,
adapter_enabled_ptr,
token_lora_mapping_ptr,
num_segments,
max_len,
BLOCK_SIZE: tl.constexpr,
):
pid = tl.program_id(0)
num_pid_m = tl.cdiv(max_len, BLOCK_SIZE)
pid_seg = pid // num_pid_m
pid_m = pid % num_pid_m
seg_start = tl.load(seg_indptr_ptr + pid_seg)
seg_end = tl.load(seg_indptr_ptr + pid_seg + 1)
seg_len = seg_end - seg_start
offs = pid_m * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
valid = offs < seg_len
lora_id = tl.load(weight_indices_ptr + pid_seg)
lora_rank = tl.load(lora_ranks_ptr + lora_id)
tl.store(
adapter_enabled_ptr + lora_id,
(lora_rank > 0).to(tl.int32),
mask=pid_m == 0,
)
tl.store(token_lora_mapping_ptr + seg_start + offs, lora_id, mask=valid)
def _compute_moe_lora_info(
num_tokens: int,
seg_indptr: torch.Tensor,
lora_ranks: torch.Tensor,
weight_indices: torch.Tensor,
adapter_enabled: torch.Tensor | None,
token_lora_mapping: torch.Tensor | None,
max_len: int,
) -> tuple[torch.Tensor, torch.Tensor]:
if token_lora_mapping is not None:
assert (
num_tokens <= token_lora_mapping.shape[0]
), "num_tokens must be less than or equal to the shape of token_lora_mapping"
token_lora_mapping = token_lora_mapping[:num_tokens]
else:
token_lora_mapping = torch.empty(
(num_tokens,), dtype=torch.int32, device=seg_indptr.device
)
if adapter_enabled is not None:
assert (
len(lora_ranks) <= adapter_enabled.shape[0]
), "lora_ranks must be less than or equal to the shape of adapter_enabled"
else:
adapter_enabled = torch.empty(
len(lora_ranks), dtype=torch.int32, device=lora_ranks.device
)
adapter_enabled.zero_()
has_segments = weight_indices.numel() != 0
use_cuda_kernel = (
num_tokens != 0 and has_segments and seg_indptr.device.type == "cuda"
)
if use_cuda_kernel:
block_size = 256
tiles_per_segment = triton.cdiv(max_len, block_size)
grid_size = tiles_per_segment * weight_indices.numel()
assert grid_size * block_size >= num_tokens, (
f"MoE LoRA token-mapping launch under-covers tokens: "
f"{grid_size=} {block_size=} {num_tokens=}"
)
_compute_moe_lora_info_kernel[(grid_size,)](
seg_indptr,
lora_ranks,
weight_indices,
adapter_enabled,
token_lora_mapping,
weight_indices.numel(),
max_len,
BLOCK_SIZE=block_size,
)
return adapter_enabled, token_lora_mapping
if has_segments:
active_ranks = lora_ranks[weight_indices.long()]
adapter_enabled.scatter_(
0, weight_indices.long(), (active_ranks > 0).to(torch.int32)
)
if num_tokens == 0:
return adapter_enabled, token_lora_mapping
if not has_segments:
token_lora_mapping.fill_(-1)
return adapter_enabled, token_lora_mapping
token_positions = torch.arange(
num_tokens, device=seg_indptr.device, dtype=torch.int32
)
# There is a torch.compile bug so we can't use seg_indptr[1:] here.
# Instead we pass seg_indptr and then subtract 1 from the result.
# This works because seg_indptr[0] == 0.
req_indices = (
torch.searchsorted(seg_indptr.to(torch.int32), token_positions, right=True) - 1
)
token_lora_mapping = torch.index_select(
weight_indices.to(torch.int32), 0, req_indices, out=token_lora_mapping
)
return adapter_enabled, token_lora_mapping
@@ -0,0 +1,525 @@
import dataclasses
from typing import List, Optional, Tuple
import torch
from sglang.kernels.ops.gemm.chunked_embedding_lora_a import (
chunked_embedding_lora_a_forward,
)
from sglang.kernels.ops.gemm.chunked_sgmv_expand import chunked_sgmv_lora_expand_forward
from sglang.kernels.ops.gemm.chunked_sgmv_shrink import chunked_sgmv_lora_shrink_forward
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
from sglang.srt.lora.utils import (
LoRABatchInfo,
generate_sequence_lengths,
get_lm_head_pruned_lens,
merge_and_chunk_segments,
)
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.server_args import ServerArgs
MIN_CHUNK_SIZE = 16
class ChunkedSgmvLoRABackend(BaseLoRABackend):
"""
Chunked LoRA backend using segmented matrix-vector multiplication.
This backend is largely based on the SGMV (Segmented Gather Matrix-Vector multiplication) algorithm
introduced in the Punica paper (https://arxiv.org/pdf/2310.18547). One main variation made here is to
segment the input sequences into fixed-size chunks, which reduces excessive kernel launches especially
when the LoRA distribution is skewed.
"""
name = "csgmv"
def __init__(
self,
max_loras_per_batch: int,
device: torch.device,
server_args: ServerArgs,
):
super().__init__(max_loras_per_batch, device)
self.max_chunk_size = server_args.max_lora_chunk_size
def run_lora_a_embedding(
self,
input_ids: torch.Tensor,
weights: torch.Tensor,
vocab_size: int,
extra_embeddings: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
assert (
extra_embeddings is None
), "Extra embeddings for lora a is not supported yet in chunked backend"
return chunked_embedding_lora_a_forward(
input_ids=input_ids,
weights=weights,
batch_info=self.batch_info,
vocab_size=vocab_size,
)
def run_lora_a_sgemm(
self,
x: torch.Tensor,
weights: torch.Tensor,
pruned_batch_info: LoRABatchInfo = None,
stack_num: int = 1,
*args,
**kwargs,
) -> torch.Tensor:
batch_info = (
pruned_batch_info if pruned_batch_info is not None else self.batch_info
)
return chunked_sgmv_lora_shrink_forward(
x=x,
weights=weights,
batch_info=batch_info,
num_slices=stack_num,
)
def run_lora_b_sgemm(
self,
x: torch.Tensor,
weights: torch.Tensor,
output_offset: torch.Tensor,
base_output: torch.Tensor = None,
pruned_batch_info: LoRABatchInfo = None,
*args,
**kwargs,
) -> torch.Tensor:
# For simple lora B, we use slice offsets [0, output_dim]
output_dim = weights.shape[-2]
max_slice_size = output_dim
batch_info = (
pruned_batch_info if pruned_batch_info is not None else self.batch_info
)
return chunked_sgmv_lora_expand_forward(
x=x,
weights=weights,
batch_info=batch_info,
slice_offsets=output_offset,
max_slice_size=max_slice_size,
base_output=base_output,
)
def run_qkv_lora(
self,
x: torch.Tensor,
qkv_lora_a: torch.Tensor,
qkv_lora_b: torch.Tensor,
output_offset: torch.Tensor,
max_qkv_out_dim: int,
base_output: torch.Tensor = None,
n_slices: int = 3,
*args,
**kwargs,
) -> torch.Tensor:
# x: (s, input_dim)
# qkv_lora_a: (num_lora, n_slices * r, input_dim)
# qkv_lora_b: (num_lora, total_output_dim, r)
assert isinstance(qkv_lora_b, torch.Tensor)
lora_a_output = chunked_sgmv_lora_shrink_forward(
x=x,
weights=qkv_lora_a,
batch_info=self.batch_info,
num_slices=n_slices,
)
lora_output = chunked_sgmv_lora_expand_forward(
x=lora_a_output,
weights=qkv_lora_b,
batch_info=self.batch_info,
slice_offsets=output_offset,
max_slice_size=max_qkv_out_dim,
base_output=base_output,
)
return lora_output
def run_gate_up_lora(
self,
x: torch.Tensor,
gate_up_lora_a: torch.Tensor,
gate_up_lora_b: torch.Tensor,
output_offset: torch.Tensor,
base_output: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
# x: (s, input_dim)
# gate_up_lora_a: (num_lora, 2 * r, input_dim)
# gate_up_lora_b: (num_lora, 2 * output_dim, r)
assert isinstance(gate_up_lora_b, torch.Tensor)
output_dim = gate_up_lora_b.shape[-2] // 2
# lora_a_output: (s, 2 * r)
lora_a_output = chunked_sgmv_lora_shrink_forward(
x=x,
weights=gate_up_lora_a,
batch_info=self.batch_info,
num_slices=2,
)
lora_output = chunked_sgmv_lora_expand_forward(
x=lora_a_output,
weights=gate_up_lora_b,
batch_info=self.batch_info,
slice_offsets=output_offset,
max_slice_size=output_dim,
base_output=base_output,
)
return lora_output
def _determine_chunk_size(self, forward_batch: ForwardBatch) -> int:
"""
Heuristically determine the chunk size based on token token number in a batch.
Args:
forward_batch (ForwardBatch): The batch information containing sequence lengths.
Returns:
The determined chunk size
"""
num_tokens = (
forward_batch.extend_num_tokens
if forward_batch.forward_mode.is_extend()
else forward_batch.batch_size
)
return self._determine_chunk_size_for_tokens(num_tokens)
def _determine_chunk_size_for_tokens(self, num_tokens: int) -> int:
"""Determine chunk size given a token count directly."""
if self.max_chunk_size <= MIN_CHUNK_SIZE:
return MIN_CHUNK_SIZE
if num_tokens >= 256:
chunk_size = 128
elif num_tokens >= 64:
chunk_size = 32
else: # num_tokens < 64
chunk_size = 16
return min(self.max_chunk_size, chunk_size)
@staticmethod
def _build_req_seg_indptr(forward_batch: ForwardBatch) -> torch.Tensor:
"""Build per-request cumulative token boundaries on CPU (pinned)."""
bs = forward_batch.batch_size
if forward_batch.forward_mode.is_decode():
indptr = torch.arange(bs + 1, dtype=torch.int32, pin_memory=True)
else:
seg_lens = generate_sequence_lengths(forward_batch, device="cpu")
indptr = torch.zeros(bs + 1, dtype=torch.int32, pin_memory=True)
torch.cumsum(seg_lens, dim=0, out=indptr[1:])
return indptr
def init_cuda_graph_batch_info(
self,
max_bs_in_cuda_graph: int,
num_tokens_per_bs: int,
):
max_num_segments = (
(num_tokens_per_bs + MIN_CHUNK_SIZE - 1) // MIN_CHUNK_SIZE
) * max_bs_in_cuda_graph
max_num_tokens = max_bs_in_cuda_graph * num_tokens_per_bs
with torch.device("cuda"):
self.cuda_graph_batch_info = LoRABatchInfo(
bs=max_bs_in_cuda_graph,
use_cuda_graph=True,
seg_lens=torch.zeros(max_num_segments, dtype=torch.int32),
seg_indptr=torch.zeros(max_num_segments + 1, dtype=torch.int32),
weight_indices=torch.zeros(max_num_segments, dtype=torch.int32),
permutation=torch.zeros(max_num_tokens, dtype=torch.int32),
lora_ranks=torch.zeros(self.max_loras_per_batch, dtype=torch.int32),
scalings=torch.zeros(self.max_loras_per_batch, dtype=torch.float),
num_segments=None, # Set per batch
max_len=None, # Not used in CSGMV backend
req_seg_indptr=torch.zeros(max_bs_in_cuda_graph + 1, dtype=torch.int32),
req_weight_indices=torch.zeros(max_bs_in_cuda_graph, dtype=torch.int32),
)
def prepare_lora_batch(
self,
forward_batch: ForwardBatch,
weight_indices: list[int],
lora_ranks: list[int],
scalings: list[float],
use_cuda_graph: bool,
):
chunk_size = self._determine_chunk_size(forward_batch)
permutation, weight_indices_reordered = ChunkedSgmvLoRABackend._get_permutation(
seq_weight_indices=weight_indices,
forward_batch=forward_batch,
)
seg_weight_indices, seg_indptr = self._get_segments_info(
weights_reordered=weight_indices_reordered,
chunk_size=chunk_size,
)
num_segments = len(seg_weight_indices)
lora_ranks_tensor = torch.tensor(
lora_ranks, dtype=torch.int32, pin_memory=True, device="cpu"
)
scalings_tensor = torch.tensor(
scalings, dtype=torch.float, pin_memory=True, device="cpu"
)
bs = forward_batch.batch_size
req_wi_tensor = torch.tensor(
weight_indices, dtype=torch.int32, pin_memory=True, device="cpu"
)
req_seg_indptr_cpu = self._build_req_seg_indptr(forward_batch)
max_num_segments = 0
has_unused_cuda_graph_segments = False
if not use_cuda_graph:
batch_info = LoRABatchInfo(
bs=bs,
num_segments=num_segments,
max_len=chunk_size,
use_cuda_graph=False,
seg_indptr=torch.empty(
(num_segments + 1,), dtype=torch.int32, device=self.device
),
weight_indices=torch.empty(
(num_segments,), dtype=torch.int32, device=self.device
),
lora_ranks=torch.empty(
(self.max_loras_per_batch,), dtype=torch.int32, device=self.device
),
scalings=torch.empty(
(self.max_loras_per_batch,), dtype=torch.float, device=self.device
),
permutation=torch.empty(
(len(permutation),), dtype=torch.int32, device=self.device
),
seg_lens=None,
req_seg_indptr=torch.empty(
(bs + 1,), dtype=torch.int32, device=self.device
),
req_weight_indices=torch.empty(
(bs,), dtype=torch.int32, device=self.device
),
)
else:
batch_info = self.cuda_graph_batch_info
batch_info.bs = bs
batch_info.num_segments = num_segments
batch_info.max_len = chunk_size
max_num_segments = batch_info.weight_indices.shape[0]
has_unused_cuda_graph_segments = num_segments < max_num_segments
# Copy to device asynchronously
batch_info.lora_ranks[: self.max_loras_per_batch].copy_(
lora_ranks_tensor, non_blocking=True
)
batch_info.scalings[: self.max_loras_per_batch].copy_(
scalings_tensor, non_blocking=True
)
batch_info.weight_indices[:num_segments].copy_(
seg_weight_indices, non_blocking=True
)
if has_unused_cuda_graph_segments:
batch_info.weight_indices[num_segments:max_num_segments].zero_()
batch_info.seg_indptr[: num_segments + 1].copy_(seg_indptr, non_blocking=True)
if has_unused_cuda_graph_segments:
batch_info.seg_indptr[num_segments + 1 : max_num_segments + 1].fill_(
int(seg_indptr[-1])
)
batch_info.permutation[: len(permutation)].copy_(permutation, non_blocking=True)
batch_info.req_seg_indptr[: bs + 1].copy_(req_seg_indptr_cpu, non_blocking=True)
batch_info.req_weight_indices[:bs].copy_(req_wi_tensor, non_blocking=True)
batch_info = self._add_moe_lora_info(forward_batch, batch_info)
self.batch_info = batch_info
self.lm_head_batch_info, self.lm_head_pass_batch_infos = (
self._prepare_lm_head_batch_info(forward_batch, weight_indices, batch_info)
)
def _prepare_lm_head_batch_info(
self,
forward_batch: ForwardBatch,
weight_indices: list[int],
batch_info: LoRABatchInfo,
) -> Tuple[Optional[LoRABatchInfo], Optional[List[LoRABatchInfo]]]:
# Precompute lm_head_batch_info for pruned lm_head LoRA
pruned_lens = get_lm_head_pruned_lens(forward_batch)
lm_head_batch_info = None
lm_head_pass_batch_infos = None
if pruned_lens is not None:
pruned_total = sum(pruned_lens)
chunk_size = self._determine_chunk_size_for_tokens(pruned_total)
lm_head_segments = merge_and_chunk_segments(
weight_indices, pruned_lens, chunk_size=chunk_size
)
lm_head_batch_info = self._build_lm_head_batch_info(
lm_head_segments, batch_info, chunk_size, pruned_total
)
# Precompute per-pass batch_infos for logprobs chunking
pass_segments = self._get_lm_head_pass_segments(weight_indices, pruned_lens)
if pass_segments is not None:
lm_head_pass_batch_infos = []
for seg_wi, seg_lens_list in pass_segments:
pass_total = sum(seg_lens_list)
pass_chunk_size = self._determine_chunk_size_for_tokens(pass_total)
chunked_segments = merge_and_chunk_segments(
seg_wi, seg_lens_list, chunk_size=pass_chunk_size
)
lm_head_pass_batch_infos.append(
self._build_lm_head_batch_info(
chunked_segments,
batch_info,
pass_chunk_size,
pass_total,
)
)
return lm_head_batch_info, lm_head_pass_batch_infos
def _build_lm_head_batch_info(
self,
lm_head_segments: Tuple[List[int], List[int]],
batch_info: LoRABatchInfo,
chunk_size: int,
expected_tokens: int,
) -> LoRABatchInfo:
seg_weight_indices_cpu, seg_lens_cpu = lm_head_segments
pruned_total = sum(seg_lens_cpu)
num_segments = len(seg_weight_indices_cpu)
weight_indices = torch.tensor(
seg_weight_indices_cpu, dtype=torch.int32, device=self.device
)
seg_lens = torch.tensor(seg_lens_cpu, dtype=torch.int32, device=self.device)
seg_indptr = torch.zeros(
(num_segments + 1,), dtype=torch.int32, device=self.device
)
seg_indptr[1:] = torch.cumsum(seg_lens, dim=0)
# Identity permutation (lm_head tokens are in original order)
permutation = torch.arange(pruned_total, dtype=torch.int32, device=self.device)
return dataclasses.replace(
batch_info,
num_segments=num_segments,
max_len=chunk_size,
seg_indptr=seg_indptr,
weight_indices=weight_indices,
permutation=permutation,
expected_tokens=expected_tokens,
)
@staticmethod
def _get_permutation(seq_weight_indices, forward_batch: ForwardBatch):
"""
Computes permutation indices for reordering tokens by their LoRA adapter assignments.
This function implements the "gather" step in Chunked Segmented Gather Matrix Vector
multiplication by creating a permutation that groups tokens by their LoRA adapter.
Tokens using the same LoRA adapter are placed together to enable efficient batched
computation.
Example:
seq_weight_indices = [0, 1, 0] # 3 sequences using adapters [0, 1, 0]
extend_seq_lens = [2, 1, 3] # sequence lengths [2, 1, 3 tokens]
# Creates row_weight_indices: [0, 0, 1, 0, 0, 0] (6 tokens total)
# Returns permutation: [0, 1, 3, 4, 5, 2] (groups adapter 0 tokens together)
# weights_reordered: [0, 0, 0, 0, 0, 1] (sorted by adapter)
Args:
seq_weight_indices: List of LoRA adapter indices for each sequence
forward_batch (ForwardBatch): Batch information containing sequence lengths
Returns:
tuple: (permutation, weights_reordered) where:
- permutation: Token reordering indices to group by adapter
- weights_reordered: Sorted adapter indices for each token
"""
with torch.device("cpu"):
seq_weight_indices = torch.tensor(seq_weight_indices, dtype=torch.int32)
seg_lens_cpu = generate_sequence_lengths(forward_batch)
row_weight_indices = torch.repeat_interleave(
seq_weight_indices, seg_lens_cpu
)
permutation = torch.empty(
(len(row_weight_indices),), dtype=torch.long, pin_memory=True
)
torch.argsort(row_weight_indices, stable=True, out=permutation)
weights_reordered = row_weight_indices[permutation]
return permutation, weights_reordered
def _get_segments_info(self, weights_reordered: torch.Tensor, chunk_size: int):
"""
Computes segment information for chunked SGMV operations.
This function takes the reordered weight indices and creates segments of fixed size
(self.segment_size) for efficient kernel execution. Each segment contains tokens
that use the same LoRA adapter, enabling vectorized computation.
The segmentation is necessary because:
1. GPU kernels work efficiently on fixed-size blocks
2. Large groups of tokens using the same adapter are split into manageable chunks
3. Each segment can be processed independently in parallel
Example:
weights_reordered = [0, 0, 0, 0, 0, 1] # 5 tokens with adapter 0, 1 with adapter 1
segment_size = 3
# Creates segments:
# Segment 0: tokens 0-2 (adapter 0), length=3
# Segment 1: tokens 3-4 (adapter 0), length=2
# Segment 2: token 5 (adapter 1), length=1
# Returns:
# weight_indices_list: [0, 0, 1] (adapter for each segment)
# seg_indptr: [0, 3, 5, 6] (cumulative segment boundaries)
Args:
weights_reordered (torch.Tensor): Sorted adapter indices for each token
chunk_size (int): Fixed size for each segment
Returns:
tuple: (weight_indices_list, seg_indptr) where:
- weight_indices_list: LoRA adapter index for each segment
- seg_indptr: Cumulative segment boundaries (CSR-style indptr)
"""
with torch.device("cpu"):
unique_weights, counts = torch.unique_consecutive(
weights_reordered, return_counts=True
)
weight_indices_list = []
seg_lens_list = []
for weight_idx, group_len in zip(unique_weights, counts):
group_len = group_len.item()
num_segs = (group_len + chunk_size - 1) // chunk_size
weight_indices_list.extend([weight_idx.item()] * num_segs)
seg_lens_list.extend([chunk_size] * (num_segs - 1))
seg_lens_list.append(group_len - (num_segs - 1) * chunk_size)
seg_lens = torch.tensor(seg_lens_list, dtype=torch.int32)
weight_indices_list = torch.tensor(
weight_indices_list, dtype=torch.int32, pin_memory=True
)
seg_indptr = torch.empty(
(len(seg_lens) + 1,), dtype=torch.int32, pin_memory=True
)
seg_indptr[0] = 0
seg_indptr[1:] = torch.cumsum(seg_lens, dim=0)
return weight_indices_list, seg_indptr
@@ -0,0 +1,64 @@
from typing import List, Optional, Tuple
from sglang.srt.environ import envs
from sglang.srt.lora.utils import LoRABatchInfo, build_lm_head_pass_segments
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
class LoRABackendLmHeadMixing:
def init_lm_head_config(self):
self.lm_head_batch_info = None
# Precomputed per-pass lm_head batch_infos. When the logits processor
# calls lm_head in multiple passes (chunked logprobs), each pass gets
# its own batch_info from this list.
self.lm_head_pass_batch_infos = None
# Current pass index. When set, apply_lora uses
# lm_head_pass_batch_infos[idx] instead of lm_head_batch_info.
self._lm_head_pass_idx = None
def _get_lm_head_pass_segments(
self,
weight_indices: list[int],
pruned_lens: List[int],
) -> Optional[List[Tuple[List[int], List[int]]]]:
"""Compute per-pass segment info for lm_head LoRA logprobs chunking.
When LogitsProcessor splits pruned states into fixed-size passes,
each pass needs its own segmentation so that lm_head LoRA operates
on the correct adapter assignments. This method returns the generic
per-pass (seg_weight_indices, seg_lens) tuples; each backend is
responsible for converting them into backend-specific LoRABatchInfo.
Returns None if logprobs chunking is disabled or the pruned token
count does not exceed the logprobs chunk size.
"""
logprobs_chunk_size = envs.SGLANG_LOGITS_PROCESSER_CHUNK_SIZE.get()
enable_logprobs_chunk = envs.SGLANG_ENABLE_LOGITS_PROCESSER_CHUNK.get()
pruned_total = sum(pruned_lens)
if not enable_logprobs_chunk or pruned_total <= logprobs_chunk_size:
return None
return build_lm_head_pass_segments(
weight_indices, pruned_lens, logprobs_chunk_size
)
def _prepare_lm_head_batch_info(
self,
forward_batch: ForwardBatch,
weight_indices: list[int],
batch_info: LoRABatchInfo,
) -> Tuple[Optional[LoRABatchInfo], Optional[List[LoRABatchInfo]]]:
"""Prepare the lm_head batch info for the current forward batch."""
"""It returns a tuple of (lm_head_batch_info, lm_head_pass_batch_infos)."""
pass
def _build_lm_head_batch_info(
self,
lm_head_segments: Tuple[List[int], List[int]],
batch_info: LoRABatchInfo,
chunk_size: int,
expected_tokens: int,
) -> LoRABatchInfo:
"""Build a LoRABatchInfo for pruned lm_head input."""
pass
@@ -0,0 +1,61 @@
import logging
from typing import Type
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
logger = logging.getLogger(__name__)
LORA_SUPPORTED_BACKENDS = {}
def register_lora_backend(name):
def decorator(fn):
LORA_SUPPORTED_BACKENDS[name] = fn
return fn
return decorator
@register_lora_backend("triton")
def create_triton_backend():
from sglang.srt.lora.backend.triton_backend import TritonLoRABackend
return TritonLoRABackend
@register_lora_backend("csgmv")
def create_triton_csgmv_backend():
from sglang.srt.lora.backend.chunked_backend import ChunkedSgmvLoRABackend
return ChunkedSgmvLoRABackend
@register_lora_backend("ascend")
def create_ascend_backend():
from sglang.srt.lora.backend.ascend_backend import AscendLoRABackend
return AscendLoRABackend
@register_lora_backend("torch_native")
def create_torch_native_backend():
from sglang.srt.lora.backend.torch_backend import TorchNativeLoRABackend
return TorchNativeLoRABackend
@register_lora_backend("flashinfer")
def create_flashinfer_backend():
raise ValueError(
"FlashInfer LoRA backend has been deprecated, please use `triton` instead."
)
def get_backend_from_name(name: str) -> Type[BaseLoRABackend]:
"""
Get corresponding backend class from backend's name
"""
if name not in LORA_SUPPORTED_BACKENDS:
raise ValueError(f"Invalid backend: {name}")
lora_backend = LORA_SUPPORTED_BACKENDS[name]()
return lora_backend
@@ -0,0 +1,302 @@
from dataclasses import dataclass
from typing import Optional
import torch
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
from sglang.srt.lora.torch_ops import (
sgemm_lora_a_embedding_fwd,
sgemm_lora_a_fwd,
sgemm_lora_b_fwd,
)
from sglang.srt.lora.utils import LoRABatchInfo, generate_sequence_lengths
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
@dataclass
class TorchNativeLoRABatchInfo(LoRABatchInfo):
# ranks of each lora adapter, in shape (lora_num,) placed on cpu device
lora_ranks_cpu: Optional[torch.Tensor] = None
# Indice pointers of each segment in shape (num_segments + 1, ) placed on cpu device
seg_indptr_cpu: Optional[torch.Tensor] = None
# Lengths of each segments in shape (num_segments,) placed on cpu device
seg_lens_cpu: Optional[torch.Tensor] = None
# The index of lora adapter used by each segment, in shape (num_segments,) placed on cpu device
weight_indices_cpu: Optional[torch.Tensor] = None
# Scaling factors for each lora adapter, in shape (lora_num,) placed on cpu device
scalings_cpu: Optional[torch.Tensor] = None
class TorchNativeLoRABackend(BaseLoRABackend):
name = "torch_native"
def __init__(
self,
max_loras_per_batch: int,
device: torch.device,
**kwargs,
):
super().__init__(max_loras_per_batch, device)
def run_lora_a_embedding(
self,
input_ids: torch.Tensor,
weights: torch.Tensor,
vocab_size: int,
extra_embeddings: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
assert (
extra_embeddings is None
), "Extra embeddings for lora a is not supported yet in chunked backend"
output_tensor = sgemm_lora_a_embedding_fwd(
inputs=input_ids,
weights=weights,
batch_info=self.batch_info,
vocab_size=vocab_size,
)
return output_tensor
def run_lora_a_sgemm(
self,
x: torch.Tensor,
weights: torch.Tensor,
stack_num: int = 1,
*args,
**kwargs,
) -> torch.Tensor:
output_tensor = sgemm_lora_a_fwd(
inputs=x,
weights=weights,
batch_info=self.batch_info,
num_slices=stack_num,
)
return output_tensor
def run_lora_b_sgemm(
self,
x: torch.Tensor,
weights: torch.Tensor,
output_offset_cpu: torch.Tensor,
base_output: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
_, weight_out_dim, _ = weights.shape
output_tensor = sgemm_lora_b_fwd(
inputs=x,
weights=weights,
batch_info=self.batch_info,
slice_offsets=output_offset_cpu,
base_output=base_output,
)
return output_tensor
def run_qkv_lora(
self,
x: torch.Tensor,
qkv_lora_a: torch.Tensor,
qkv_lora_b: torch.Tensor,
output_offset: torch.Tensor,
output_offset_cpu: torch.Tensor,
max_qkv_out_dim: int,
base_output: torch.Tensor = None,
n_slices: int = 3,
*args,
**kwargs,
) -> torch.Tensor:
lora_a_output = sgemm_lora_a_fwd(
inputs=x,
weights=qkv_lora_a,
batch_info=self.batch_info,
num_slices=n_slices,
)
output_tensor = sgemm_lora_b_fwd(
inputs=lora_a_output,
weights=qkv_lora_b,
batch_info=self.batch_info,
slice_offsets=output_offset_cpu,
base_output=base_output,
)
return output_tensor
def run_gate_up_lora(
self,
x: torch.Tensor,
gate_up_lora_a: torch.Tensor,
gate_up_lora_b: torch.Tensor,
output_offset_cpu: torch.Tensor,
base_output: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
num_slices = len(output_offset_cpu) - 1
_, weight_out_dim, _ = gate_up_lora_b.shape
lora_a_output = sgemm_lora_a_fwd(
inputs=x,
weights=gate_up_lora_a,
batch_info=self.batch_info,
num_slices=num_slices,
)
output_tensor = sgemm_lora_b_fwd(
inputs=lora_a_output,
weights=gate_up_lora_b,
batch_info=self.batch_info,
slice_offsets=output_offset_cpu,
base_output=base_output,
)
return output_tensor
def init_cuda_graph_batch_info(
self,
max_bs_in_cuda_graph: int,
num_tokens_per_bs: int,
):
with torch.device("cuda"):
self.cuda_graph_batch_info = TorchNativeLoRABatchInfo(
use_cuda_graph=True,
bs=max_bs_in_cuda_graph,
num_segments=self.max_loras_per_batch,
seg_lens=torch.full(
(max_bs_in_cuda_graph,), num_tokens_per_bs, dtype=torch.int32
),
seg_indptr=torch.zeros(max_bs_in_cuda_graph + 1, dtype=torch.int32),
weight_indices=torch.zeros(max_bs_in_cuda_graph, dtype=torch.int32),
lora_ranks=torch.zeros(self.max_loras_per_batch, dtype=torch.int32),
scalings=torch.zeros(self.max_loras_per_batch, dtype=torch.float),
permutation=None,
max_len=num_tokens_per_bs,
)
# Initialize seg_indptr for CUDA graph as they remain constant
# across batches.
torch.cumsum(
self.cuda_graph_batch_info.seg_lens[:max_bs_in_cuda_graph],
dim=0,
out=self.cuda_graph_batch_info.seg_indptr[1 : max_bs_in_cuda_graph + 1],
)
def prepare_lora_batch(
self,
forward_batch: ForwardBatch,
weight_indices: list[int],
lora_ranks: list[int],
scalings: list[float],
use_cuda_graph: bool,
):
# Do not use merge optimization for graph mode
# Use pinned memory to avoid synchronizations during host-to-device transfer
original_seq_lens_cpu = generate_sequence_lengths(forward_batch, device="cpu")
if not use_cuda_graph:
original_weight_indices_tensor = torch.tensor(
weight_indices, dtype=torch.int32, device="cpu"
)
unique_weight_indices_tensor, inverse_weight_indices_tensor = (
torch.unique_consecutive(
original_weight_indices_tensor, return_inverse=True
)
)
seg_lens_cpu = (
torch.zeros_like(
unique_weight_indices_tensor, dtype=torch.int32, device="cpu"
)
.scatter_add_(
0,
inverse_weight_indices_tensor,
original_seq_lens_cpu,
)
.pin_memory()
)
weight_indices_tensor = unique_weight_indices_tensor.pin_memory()
else:
weight_indices_tensor = torch.repeat_interleave(
torch.tensor(weight_indices, dtype=torch.int32, device="cpu"),
original_seq_lens_cpu,
).pin_memory()
seg_lens_cpu = torch.ones_like(weight_indices_tensor).pin_memory()
seg_indptr_cpu = torch.zeros(
(len(seg_lens_cpu) + 1,), dtype=torch.int32, pin_memory=True
)
seg_indptr_cpu[1:] = torch.cumsum(seg_lens_cpu, dim=0)
lora_ranks_tensor = torch.tensor(
lora_ranks, dtype=torch.int32, pin_memory=True, device="cpu"
)
scalings_tensor = torch.tensor(
scalings, dtype=torch.float, pin_memory=True, device="cpu"
)
bs = forward_batch.batch_size
num_segments = len(weight_indices_tensor)
if use_cuda_graph:
assert (
self.cuda_graph_batch_info is not None
), "CUDA Graph batch info is not initialized."
batch_info = self.cuda_graph_batch_info
batch_info.bs = forward_batch.batch_size
batch_info.num_segments = num_segments
else:
max_len = max(seg_lens_cpu)
batch_info = TorchNativeLoRABatchInfo(
bs=forward_batch.batch_size,
num_segments=num_segments,
max_len=max_len,
use_cuda_graph=False,
seg_lens=torch.empty((bs,), dtype=torch.int32, device=self.device),
seg_indptr=torch.empty(
(bs + 1,), dtype=torch.int32, device=self.device
),
weight_indices=torch.empty(
(bs,), dtype=torch.int32, device=self.device
),
lora_ranks=torch.empty(
(self.max_loras_per_batch,), dtype=torch.int32, device=self.device
),
scalings=torch.empty(
(self.max_loras_per_batch,), dtype=torch.float, device=self.device
),
permutation=None,
)
# Copy to device asynchronously
batch_info.lora_ranks[: self.max_loras_per_batch].copy_(
lora_ranks_tensor, non_blocking=True
)
batch_info.scalings[: self.max_loras_per_batch].copy_(
scalings_tensor, non_blocking=True
)
batch_info.weight_indices[:num_segments].copy_(
weight_indices_tensor, non_blocking=True
)
batch_info.seg_indptr[: len(seg_indptr_cpu)].copy_(
seg_indptr_cpu, non_blocking=True
)
batch_info.seg_lens[: len(seg_lens_cpu)].copy_(seg_lens_cpu, non_blocking=True)
batch_info.lora_ranks_cpu = lora_ranks_tensor
batch_info.seg_indptr_cpu = seg_indptr_cpu
batch_info.seg_lens_cpu = seg_lens_cpu
batch_info.weight_indices_cpu = weight_indices_tensor
batch_info.scalings_cpu = scalings_tensor
batch_info = self._add_moe_lora_info(forward_batch, batch_info)
self.batch_info = batch_info
@@ -0,0 +1,376 @@
import dataclasses
from typing import List, Optional, Tuple
import torch
from sglang.kernels.ops.gemm.embedding_lora_a import embedding_lora_a_fwd
from sglang.kernels.ops.gemm.gate_up_lora_b import gate_up_lora_b_fwd
from sglang.kernels.ops.gemm.qkv_lora_b import qkv_lora_b_fwd
from sglang.kernels.ops.gemm.sgemm_lora_a import sgemm_lora_a_fwd
from sglang.kernels.ops.gemm.sgemm_lora_b import sgemm_lora_b_fwd
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
from sglang.srt.lora.utils import (
LoRABatchInfo,
get_lm_head_pruned_lens,
merge_and_chunk_segments,
)
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
class TritonLoRABackend(BaseLoRABackend):
name = "triton"
def __init__(
self,
max_loras_per_batch: int,
device: torch.device,
**kwargs,
):
super().__init__(max_loras_per_batch, device)
def run_lora_a_embedding(
self,
input_ids: torch.Tensor,
weights: torch.Tensor,
vocab_size: int,
extra_embeddings: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
"""Run LoRA A embedding lookup using Triton kernel."""
return embedding_lora_a_fwd(
input_ids=input_ids,
weights=weights,
batch_info=self.batch_info,
vocab_size=vocab_size,
extra_embeddings=extra_embeddings,
)
def _sgemm_info(self, pruned_batch_info=None):
"""Return the sgemm batch_info (merged segments when available)."""
if pruned_batch_info is not None:
return pruned_batch_info
return getattr(self, "sgemm_batch_info", None) or self.batch_info
def run_lora_a_sgemm(
self,
x: torch.Tensor,
weights: torch.Tensor,
pruned_batch_info: LoRABatchInfo = None,
stack_num: int = 1,
*args,
**kwargs,
) -> torch.Tensor:
return sgemm_lora_a_fwd(
x, weights, self._sgemm_info(pruned_batch_info), stack_num=stack_num
)
def run_lora_b_sgemm(
self,
x: torch.Tensor,
weights: torch.Tensor,
base_output: torch.Tensor = None,
pruned_batch_info: LoRABatchInfo = None,
*args,
**kwargs,
) -> torch.Tensor:
return sgemm_lora_b_fwd(
x, weights, self._sgemm_info(pruned_batch_info), base_output
)
def run_qkv_lora(
self,
x: torch.Tensor,
qkv_lora_a: torch.Tensor,
qkv_lora_b: torch.Tensor,
output_offset: torch.Tensor,
max_qkv_out_dim: int,
base_output: torch.Tensor = None,
n_slices: int = 3,
*args,
**kwargs,
) -> torch.Tensor:
# x: (s, input_dim)
# qkv_lora_a: (num_lora, n_slices * r, input_dim)
# qkv_lora_b: (num_lora, total_output_dim, r)
assert isinstance(qkv_lora_b, torch.Tensor)
sgemm_info = self._sgemm_info()
lora_a_output = sgemm_lora_a_fwd(x, qkv_lora_a, sgemm_info, stack_num=n_slices)
lora_output = qkv_lora_b_fwd(
lora_a_output,
qkv_lora_b,
sgemm_info,
output_offset,
max_qkv_out_dim,
base_output,
n_slices=n_slices,
)
return lora_output
def run_gate_up_lora(
self,
x: torch.Tensor,
gate_up_lora_a: torch.Tensor,
gate_up_lora_b: torch.Tensor,
base_output: torch.Tensor = None,
*args,
**kwargs,
) -> torch.Tensor:
# x: (s, input_dim)
# gate_up_lora_a: (num_lora, 2 * r, input_dim)
# gate_up_lora_b: (num_lora, 2 * output_dim, r)
assert isinstance(gate_up_lora_b, torch.Tensor)
output_dim = gate_up_lora_b.shape[-2] // 2
sgemm_info = self._sgemm_info()
# lora_a_output: (s, 2 * r)
lora_a_output = sgemm_lora_a_fwd(x, gate_up_lora_a, sgemm_info, stack_num=2)
lora_output = gate_up_lora_b_fwd(
lora_a_output,
gate_up_lora_b,
sgemm_info,
output_dim,
base_output,
)
return lora_output
def init_cuda_graph_batch_info(
self,
max_bs_in_cuda_graph: int,
num_tokens_per_bs: int,
):
max_tokens = max_bs_in_cuda_graph * num_tokens_per_bs
mlpb = self.max_loras_per_batch
with torch.device("cuda"):
self.cuda_graph_batch_info = LoRABatchInfo(
bs=max_bs_in_cuda_graph,
use_cuda_graph=True,
num_segments=None,
seg_lens=torch.full(
(max_bs_in_cuda_graph,), num_tokens_per_bs, dtype=torch.int32
),
seg_indptr=torch.zeros(max_bs_in_cuda_graph + 1, dtype=torch.int32),
max_len=num_tokens_per_bs,
weight_indices=torch.zeros(max_bs_in_cuda_graph, dtype=torch.int32),
lora_ranks=torch.zeros(mlpb, dtype=torch.int32),
scalings=torch.zeros(mlpb, dtype=torch.float),
permutation=None,
)
torch.cumsum(
self.cuda_graph_batch_info.seg_lens[:max_bs_in_cuda_graph],
dim=0,
out=self.cuda_graph_batch_info.seg_indptr[1 : max_bs_in_cuda_graph + 1],
)
# Sgemm batch_info with segments merged by adapter.
# Updated each batch by compute_sgemm_routing().
self.cuda_graph_sgemm_batch_info = LoRABatchInfo(
bs=mlpb,
use_cuda_graph=True,
num_segments=mlpb,
seg_lens=torch.zeros(mlpb, dtype=torch.int32),
seg_indptr=torch.zeros(mlpb + 1, dtype=torch.int32),
max_len=max_tokens,
weight_indices=torch.arange(mlpb, dtype=torch.int32),
lora_ranks=torch.zeros(mlpb, dtype=torch.int32),
scalings=torch.zeros(mlpb, dtype=torch.float),
permutation=torch.zeros(max_tokens, dtype=torch.int32),
)
def compute_sgemm_routing(self, use_cuda_graph: bool):
"""Sort tokens by adapter and build merged segments for sgemm LoRA."""
bi = self.batch_info
bs = bi.bs
mlpb = self.max_loras_per_batch
wi = bi.weight_indices[:bs]
perm = torch.argsort(wi, stable=True).to(torch.int32)
sorted_wi = wi[perm]
adapter_ids = torch.arange(mlpb, device=wi.device, dtype=torch.int32)
seg_starts = torch.searchsorted(sorted_wi, adapter_ids)
seg_ends = torch.searchsorted(sorted_wi, adapter_ids, right=True)
seg_lens = seg_ends - seg_starts
if use_cuda_graph:
sgemm = getattr(self, "cuda_graph_sgemm_batch_info", None)
if sgemm is None:
return
sgemm.permutation[:bs] = perm
sgemm.seg_lens[:] = seg_lens
sgemm.seg_indptr[0:1].zero_()
torch.cumsum(sgemm.seg_lens, dim=0, out=sgemm.seg_indptr[1:])
sgemm.max_len = bs
sgemm.lora_ranks[:mlpb] = bi.lora_ranks[:mlpb]
sgemm.scalings[:mlpb] = bi.scalings[:mlpb]
else:
seg_indptr = torch.zeros(mlpb + 1, dtype=torch.int32, device=wi.device)
seg_indptr[1:] = torch.cumsum(seg_lens, dim=0)
sgemm = LoRABatchInfo(
bs=mlpb,
use_cuda_graph=False,
num_segments=mlpb,
seg_lens=seg_lens,
seg_indptr=seg_indptr,
max_len=bs,
weight_indices=adapter_ids,
lora_ranks=bi.lora_ranks[:mlpb].clone(),
scalings=bi.scalings[:mlpb].clone(),
permutation=perm,
)
self.sgemm_batch_info = sgemm
def prepare_lora_batch(
self,
forward_batch: ForwardBatch,
weight_indices: list[int],
lora_ranks: list[int],
scalings: list[float],
use_cuda_graph: bool,
):
# Use pinned memory to avoid synchronizations during host-to-device transfer
weight_indices_tensor = torch.tensor(
weight_indices, dtype=torch.int32, pin_memory=True, device="cpu"
)
lora_ranks_tensor = torch.tensor(
lora_ranks, dtype=torch.int32, pin_memory=True, device="cpu"
)
scalings_tensor = torch.tensor(
scalings, dtype=torch.float, pin_memory=True, device="cpu"
)
bs = forward_batch.batch_size
if use_cuda_graph:
assert (
self.cuda_graph_batch_info is not None
), "CUDA Graph batch info is not initialized."
batch_info = self.cuda_graph_batch_info
batch_info.bs = forward_batch.batch_size
batch_info.num_segments = forward_batch.batch_size
else:
max_len = (
# Calculate max_len from the CPU copy to avoid D2H transfer.
max(forward_batch.extend_seq_lens_cpu)
if forward_batch.forward_mode.is_extend()
else 1
)
seg_lens = (
forward_batch.extend_seq_lens
if forward_batch.forward_mode.is_extend()
else torch.ones(bs, dtype=torch.int32, device=self.device)
)
seg_indptr = torch.zeros((bs + 1,), dtype=torch.int32, device=self.device)
seg_indptr[1:] = torch.cumsum(seg_lens, dim=0)
batch_info = LoRABatchInfo(
bs=forward_batch.batch_size,
num_segments=forward_batch.batch_size,
max_len=max_len,
use_cuda_graph=False,
seg_lens=seg_lens,
seg_indptr=seg_indptr,
weight_indices=torch.empty(
(bs,), dtype=torch.int32, device=self.device
),
lora_ranks=torch.empty(
(self.max_loras_per_batch,), dtype=torch.int64, device=self.device
),
scalings=torch.empty(
(self.max_loras_per_batch,), dtype=torch.float, device=self.device
),
permutation=None,
)
# Copy to device asynchronously
batch_info.lora_ranks[: self.max_loras_per_batch].copy_(
lora_ranks_tensor, non_blocking=True
)
batch_info.scalings[: self.max_loras_per_batch].copy_(
scalings_tensor, non_blocking=True
)
batch_info.weight_indices[:bs].copy_(weight_indices_tensor, non_blocking=True)
batch_info = self._add_moe_lora_info(forward_batch, batch_info)
self.batch_info = batch_info
# Biggest win is in decode.
is_decode = not forward_batch.forward_mode.is_extend()
if is_decode:
self.compute_sgemm_routing(use_cuda_graph)
else:
self.sgemm_batch_info = None
self.lm_head_batch_info, self.lm_head_pass_batch_infos = (
self._prepare_lm_head_batch_info(forward_batch, weight_indices, batch_info)
)
def _prepare_lm_head_batch_info(
self,
forward_batch: ForwardBatch,
weight_indices: list[int],
batch_info: LoRABatchInfo,
) -> Tuple[Optional[LoRABatchInfo], Optional[List[LoRABatchInfo]]]:
# Precompute lm_head_batch_info for pruned lm_head LoRA
pruned_lens = get_lm_head_pruned_lens(forward_batch)
lm_head_batch_info = None
lm_head_pass_batch_infos = None
if pruned_lens is not None:
pruned_total = sum(pruned_lens)
lm_head_segments = merge_and_chunk_segments(
weight_indices, pruned_lens, chunk_size=pruned_total
)
lm_head_batch_info = self._build_lm_head_batch_info(
lm_head_segments, batch_info, pruned_total
)
# Precompute per-pass batch_infos for logprobs chunking
pass_segments = self._get_lm_head_pass_segments(weight_indices, pruned_lens)
if pass_segments is not None:
lm_head_pass_batch_infos = []
for seg_wi, seg_lens_list in pass_segments:
pass_total = sum(seg_lens_list)
merged_segments = merge_and_chunk_segments(
seg_wi, seg_lens_list, chunk_size=pass_total
)
self.lm_head_pass_batch_infos.append(
self._build_lm_head_batch_info(
merged_segments, batch_info, pass_total
)
)
return lm_head_batch_info, lm_head_pass_batch_infos
def _build_lm_head_batch_info(
self,
lm_head_segments: Tuple[List[int], List[int]],
batch_info: LoRABatchInfo,
expected_tokens: int,
) -> LoRABatchInfo:
seg_weight_indices_cpu, seg_lens_cpu = lm_head_segments
num_segments = len(seg_weight_indices_cpu)
seg_lens = torch.tensor(seg_lens_cpu, dtype=torch.int32, device=self.device)
seg_indptr = torch.zeros(
(num_segments + 1,), dtype=torch.int32, device=self.device
)
seg_indptr[1:] = torch.cumsum(seg_lens, dim=0)
return dataclasses.replace(
batch_info,
bs=num_segments,
num_segments=num_segments,
max_len=max(seg_lens_cpu),
seg_lens=seg_lens,
seg_indptr=seg_indptr,
weight_indices=torch.tensor(
seg_weight_indices_cpu, dtype=torch.int32, device=self.device
),
expected_tokens=expected_tokens,
)
@@ -0,0 +1,117 @@
"""LoRA correction for absorbed-MLA ``kv_b_proj``.
The absorbed-MLA path in ``DeepseekV2AttentionMLA`` bypasses
``kv_b_proj.forward()`` and folds the K/V contribution into two BMMs against
the pre-computed ``w_kc`` / ``w_vc`` weights, so a standard
``ColumnParallelLinearWithLoRA`` wrapper would never see the activations and
the LoRA delta would silently be dropped. These helpers inject the missing
delta on top of the absorbed intermediates via the SGMM-style Triton kernels
in ``triton_ops/kv_b_lora_absorbed.py``.
Used from ``deepseek_common/attention_forward_methods/forward_mla.py``. Call
sites should gate the call with :func:`is_kv_b_lora_active` so non-LoRA
forwards take a single ``getattr`` and skip the helper entirely.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Tuple
import torch
from sglang.kernels.ops.gemm.kv_b_lora_absorbed import (
step_a_q_fwd,
step_a_v_fwd,
step_b_q_fwd,
step_b_v_fwd,
)
if TYPE_CHECKING:
from sglang.srt.lora.utils import LoRABatchInfo
from sglang.srt.models.deepseek_v2 import DeepseekV2AttentionMLA
def is_kv_b_lora_active(attn_module: DeepseekV2AttentionMLA) -> bool:
"""Cheap precondition check used at call sites in the attention forward
to skip the entire LoRA-correction path when no ``kv_b_proj`` adapter is
wrapped on this module (the common case)."""
return getattr(attn_module.kv_b_proj, "set_lora", False)
def _get_state(
attn_module: DeepseekV2AttentionMLA,
) -> Optional[Tuple[torch.Tensor, torch.Tensor, LoRABatchInfo]]:
if not is_kv_b_lora_active(attn_module):
return None
if not hasattr(attn_module.kv_b_proj, "A_buffer"):
return None
lora_backend = attn_module.kv_b_proj.lora_backend
if not hasattr(lora_backend, "batch_info"):
return None
batch_info = lora_backend.batch_info
if batch_info is None:
return None
# Triton backend exposes _sgemm_info() to group decode-shape repeats of
# the same adapter; csgmv-style backends just expose batch_info directly.
sgemm_info = getattr(lora_backend, "_sgemm_info", None)
if callable(sgemm_info):
batch_info = sgemm_info()
return attn_module.kv_b_proj.A_buffer, attn_module.kv_b_proj.B_buffer, batch_info
def apply_q_correction(
attn_module: DeepseekV2AttentionMLA,
q_nope: torch.Tensor,
q_nope_out: torch.Tensor,
) -> torch.Tensor:
"""LoRA correction for the absorbed ``q_nope @ w_kc`` path.
Computes ``q_nope_out += q_nope @ B_kc @ A * scaling`` per token, per
active LoRA slot via two SGMM-style Triton kernels. Factored along the
LoRA-A/B boundary so we never materialise ``B @ A`` (~268M FMAs per layer
per slot in the naive implementation)::
step A_q : ``(S,H,qk_nope) @ B_kc[slot, h] (qk_nope, rank) -> (S,H,rank)``
step B_q : ``(S,H,rank) @ A[slot] (rank, kv_lora_rank) -> += q_nope_out``
"""
state = _get_state(attn_module)
if state is None:
return q_nope_out
A_buf, B_buf, batch_info = state
full_K_per_head = attn_module.qk_nope_head_dim + attn_module.v_head_dim
q_lora_a = step_a_q_fwd(q_nope, B_buf, batch_info, full_K_per_head)
return step_b_q_fwd(q_lora_a, A_buf, batch_info, q_nope_out)
def apply_v_correction(
attn_module: DeepseekV2AttentionMLA,
attn_output: torch.Tensor,
attn_bmm_flat: torch.Tensor,
) -> torch.Tensor:
"""LoRA correction for the absorbed ``attn_output @ w_vc`` path.
Computes ``attn_bmm_flat += attn_output @ A.T @ B_vc.T * scaling`` per
token, per active LoRA slot. ``attn_bmm_flat`` is the flat
``(S, H*v_head_dim)`` view of the absorbed BMM result; we pass strides
matching the implicit ``(S, H, v_head_dim)`` layout to step B_v.
"""
state = _get_state(attn_module)
if state is None:
return attn_bmm_flat
A_buf, B_buf, batch_info = state
attn_lora_a = step_a_v_fwd(attn_output, A_buf, batch_info)
base_view = attn_bmm_flat.view(
-1, attn_module.num_local_heads, attn_module.v_head_dim
)
step_b_v_fwd(
attn_lora_a,
B_buf,
batch_info,
base_view,
attn_module.qk_nope_head_dim,
attn_module.v_head_dim,
)
return attn_bmm_flat
+139
View File
@@ -0,0 +1,139 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""
Eviction policies for LoRA adapter memory management.
"""
import logging
import time
from abc import ABC, abstractmethod
from collections import OrderedDict
from typing import Optional, Set
logger = logging.getLogger(__name__)
class EvictionPolicy(ABC):
"""Abstract base class for LoRA adapter eviction policies."""
@abstractmethod
def mark_used(self, uid: Optional[str]) -> None:
"""Marks an adapter as used."""
pass
@abstractmethod
def select_victim(self, candidates: Set[Optional[str]]) -> Optional[str]:
"""Selects an adapter to evict from candidates."""
pass
@abstractmethod
def remove(self, uid: Optional[str]) -> None:
"""Removes an adapter from the policy's tracking."""
pass
class LRUEvictionPolicy(EvictionPolicy):
"""LRU eviction policy - evicts the least recently used adapter."""
def __init__(self):
self.access_order = OrderedDict() # key=uid, value=last_access_time
self.total_accesses = 0
self.eviction_count = 0
def mark_used(self, uid: Optional[str]) -> None:
if uid is not None:
current_time = time.monotonic()
# Remove and re-add to move to end (most recent)
self.access_order.pop(uid, None)
self.access_order[uid] = current_time
self.total_accesses += 1
logger.debug(f"LoRA {uid} marked as used at {current_time}")
def select_victim(self, candidates: Set[Optional[str]]) -> Optional[str]:
"""Select the least recently used adapter from candidates."""
# Iterate through access_order (oldest first) to find LRU victim
for uid in list(self.access_order.keys()):
if uid in candidates:
logger.debug(f"Selected LoRA {uid} for eviction (LRU)")
self.eviction_count += 1
return uid
# If no tracked UID found in candidates, check if None is available
# This happens when the batch consists entirely of LoRA requests
# and None (base model) is the only eviction candidate
if None in candidates:
logger.debug("Selected None (base model) for eviction")
self.eviction_count += 1
return None
# Should never reach here if candidates is non-empty
assert False, f"Failed to select LRU victim from candidates: {candidates}"
def remove(self, uid: Optional[str]) -> None:
if uid is not None:
self.access_order.pop(uid, None)
logger.debug(f"Removed LoRA {uid} from LRU tracking")
class FIFOEvictionPolicy(EvictionPolicy):
"""FIFO eviction policy - for backward compatibility."""
def __init__(self):
self.insertion_order = (
OrderedDict()
) # key=uid, OrderedDict maintains insertion order
self.eviction_count = 0
def mark_used(self, uid: Optional[str]) -> None:
"""For FIFO, we only track insertion order (not access time)."""
if uid is not None and uid not in self.insertion_order:
self.insertion_order[uid] = (
True # Value unused, OrderedDict tracks insertion order
)
def select_victim(self, candidates: Set[Optional[str]]) -> Optional[str]:
"""Select the first inserted adapter from candidates."""
# Iterate through insertion_order (oldest first) to find FIFO victim
for uid in list(self.insertion_order.keys()):
if uid in candidates:
logger.debug(f"Selected LoRA {uid} for eviction (FIFO)")
self.eviction_count += 1
return uid
# If no tracked UID found in candidates, check if None is available
# This happens when the batch consists entirely of LoRA requests
# and None (base model) is the only eviction candidate
if None in candidates:
logger.debug("Selected None (base model) for eviction")
self.eviction_count += 1
return None
# Should never reach here if candidates is non-empty
assert False, f"Failed to select FIFO victim from candidates: {candidates}"
def remove(self, uid: Optional[str]) -> None:
if uid is not None:
self.insertion_order.pop(uid, None)
def get_eviction_policy(policy_name: str) -> EvictionPolicy:
"""Factory function to create eviction policy instances."""
policies = {
"fifo": FIFOEvictionPolicy,
"lru": LRUEvictionPolicy,
}
if policy_name not in policies:
raise ValueError(f"Unknown eviction policy: {policy_name}")
return policies[policy_name]()
File diff suppressed because it is too large Load Diff
+436
View File
@@ -0,0 +1,436 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# Integrates "S-LoRA: Serving Thousands of Concurrent LoRA Adapters"
# and "Punica: Multi-Tenant LoRA Serving"
# LoRA layers class inheritance adapted from:
# https://github.com/vllm-project/vllm/blob/4abf6336ec65c270343eb895e7b18786e9274176/vllm/lora/layers.py
import logging
import re
from typing import Dict, List, Optional
import torch
from torch import nn
from sglang.srt.configs.load_config import LoadConfig
from sglang.srt.layers.utils import get_layer_id
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
from sglang.srt.lora.lora_config import LoRAConfig
from sglang.srt.model_loader.loader import DefaultModelLoader
from sglang.srt.utils.hf_transformers_utils import AutoConfig
# Matches both per-expert keys ("...experts.0.<module>...") and shared-outer
# keys ("...experts.<module>..."), while excluding "shared_experts." (where the
# preceding char is "_", not ".").
_ROUTED_EXPERT_PATTERN = re.compile(r"\.experts\.")
logger = logging.getLogger(__name__)
class LoRALayer(nn.Module):
def __init__(self, config: LoRAConfig, base_hf_config: AutoConfig):
super().__init__()
self.config: LoRAConfig = config
self.base_hf_config: AutoConfig = base_hf_config
# lora weights in cpu. The weights are loaded from checkpoint.
self.weights: Dict[str, torch.Tensor] = {}
self.pinned_weights: Dict[str, torch.Tensor] = {}
class LoRAAdapter(nn.Module):
def __init__(
self,
uid: str,
config: LoRAConfig,
base_hf_config: AutoConfig,
load_config: LoadConfig,
lora_backend: BaseLoRABackend,
base_model: Optional[torch.nn.Module] = None,
):
super().__init__()
self.uid: str = uid
self.config: LoRAConfig = config
assert self.config.hf_config["peft_type"].lower() == "lora"
self.base_hf_config: AutoConfig = base_hf_config
self.load_config: LoadConfig = load_config
self.lora_backend: BaseLoRABackend = lora_backend
self.scaling: float = self.config.lora_alpha / self.config.r
# Bypass nn.Module.__setattr__ so the base model is held as a plain
# reference rather than auto-registered as a submodule (which would
# leak its parameters into our state_dict / parameters() / .to()).
object.__setattr__(self, "base_model", base_model)
object.__setattr__(
self,
"_moe_is_gated_by_layer",
self._build_moe_gated_map(base_model) if base_model is not None else {},
)
self.layers: List[LoRALayer] = nn.ModuleList(
[
LoRALayer(config, base_hf_config)
for _ in range(base_hf_config.num_hidden_layers)
]
)
self.embedding_layers: Dict[str, torch.Tensor] = {}
self.pinned_embedding_layers: Dict[str, torch.Tensor] = {}
self.added_tokens_embeddings: Dict[str, torch.Tensor] = {}
self.pinned_added_tokens_embeddings: Dict[str, torch.Tensor] = {}
@staticmethod
def _build_moe_gated_map(base_model: torch.nn.Module) -> Dict[int, bool]:
"""Map layer_id -> moe_runner_config.is_gated for FusedMoE base layers.
Only used by normalize_gate_up_proj to decide whether per-expert
gate_proj weights should be zero-padded and stacked (gated → c=2 buffer)
or just renamed (non-gated → c=1 buffer via model's get_stacked_multiply
override on gate_up_proj_moe).
Adapters can be loaded both before `init_lora_modules` (initial
--lora-paths) and after (dynamic API loads), so the FusedMoE may
appear either directly or under a `BaseLayerWithLoRA.base_layer`.
"""
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
gated_map: Dict[int, bool] = {}
for name, module in base_model.named_modules():
inner = (
module
if isinstance(module, FusedMoE)
else getattr(module, "base_layer", None)
)
if not isinstance(inner, FusedMoE):
continue
layer_id = get_layer_id(name)
if layer_id is not None:
gated_map[layer_id] = bool(inner.moe_runner_config.is_gated)
return gated_map
def _is_non_gated_moe_weight(self, weight_name: str) -> bool:
"""True iff this adapter weight targets a non-gated MoE expert.
Such weights flow into the `gate_up_proj_moe` buffer, which the model
overrides to stacked_multiply=1 — so the weight must be stored without
being stacked with a synthetic up_proj zero-pad.
Matches both adapter key conventions:
- per-expert: ``...experts.0.<module>...`` (one tensor per expert)
- shared-outer: ``...experts.<module>...`` (3D tensor with the expert
dim baked into the shape)
"""
if not _ROUTED_EXPERT_PATTERN.search(weight_name):
return False
layer_id = get_layer_id(weight_name)
if layer_id is None:
return False
return self._moe_is_gated_by_layer.get(layer_id) is False
def initialize_weights(self):
model_path = self.config.path
loader = DefaultModelLoader(self.load_config)
revision = getattr(self.config.hf_config, "revision", None)
# Get normalized target modules for filtering
for name, loaded_weight in loader._get_weights_iterator(
DefaultModelLoader.Source(
model_path, revision=revision, fall_back_to_pt=True
)
):
self._process_weight(name, loaded_weight)
self._normalize_weights()
def initialize_weights_from_tensors(self, tensors: Dict[str, torch.Tensor]):
for name, tensor in tensors.items():
self._process_weight(name, tensor)
self._normalize_weights()
def _process_weight(self, name: str, loaded_weight: torch.Tensor):
from sglang.srt.lora.utils import get_normalized_target_modules
normalized_target_modules = get_normalized_target_modules(
self.config.target_modules
)
# Remap PEFT "unembed_tokens" key to "lm_head" so the weight is
# recognized and loaded into the correct buffer.
if "unembed_tokens" in name:
name = name.replace("unembed_tokens", "lm_head")
layer_id = get_layer_id(name)
if layer_id is not None:
self.layers[layer_id].weights[name] = loaded_weight.cpu()
elif "embed_tokens" in name or "lm_head" in name:
# Check if this module is declared in target_modules before loading.
# When normalized_target_modules is {"all"} (e.g. target_modules was
# "all-linear"), we allow loading since the server-level
# --lora-target-modules will govern which modules are active.
module_name = "embed_tokens" if "embed_tokens" in name else "lm_head"
if (
"all" in normalized_target_modules
or module_name in normalized_target_modules
):
self.embedding_layers[name] = loaded_weight.cpu()
else:
logger.debug(
f"Skipping {name} as '{module_name}' is not in adapter's target_modules: {self.config.target_modules}"
)
elif "input_embeddings" in name or "output_embeddings" in name:
# added/extra token emb
self.added_tokens_embeddings[name] = loaded_weight.cpu()
assert loaded_weight.shape[0] == self.config.lora_added_tokens_size, (
f"LoRA adapter {self.uid} has lora_added_tokens_size {self.config.lora_added_tokens_size} specified in the config, "
f"but the loaded weight '{name}' has shape {loaded_weight.shape[0]} in first dimension"
)
def _normalize_weights(self):
for layer in self.layers:
weight_names = list(layer.weights.keys())
self.normalize_qkv_proj(weight_names, layer.weights)
self._rename_expert_w_to_proj(layer.weights)
# Stack gate_proj + x_proj → in_proj for Mamba layers (before gate_up normalization)
self._normalize_in_proj(layer.weights)
# Stack in_proj_q + in_proj_k + in_proj_v + in_proj_z → in_proj_qkvz for GDN layers
self._normalize_in_proj_qkvz(layer.weights)
weight_names = list(layer.weights.keys())
self.normalize_gate_up_proj(weight_names, layer.weights)
weight_names = list(layer.weights.keys())
self.normalize_fused_qkv_a_proj(weight_names, layer.weights)
def normalize_qkv_proj(
self, weight_names: List[str], weights: Dict[str, torch.Tensor]
):
# Collect target q/k/v modules. This process is necessary since there might be no lora attached to k_proj
target_module = set()
for weight_name in weight_names:
if "k_proj" in weight_name:
target_module.add("k_proj")
if "q_proj" in weight_name:
target_module.add("q_proj")
if "v_proj" in weight_name:
target_module.add("v_proj")
if "qkv_proj" in weight_name:
target_module.add("qkv_proj")
if len(target_module) == 0:
return
for weight_name in weight_names:
# We assume every lora adaptor should contain lora modules for q_proj
if "q_proj" in weight_name:
q_name = weight_name
k_name = weight_name.replace("q_proj", "k_proj")
v_name = weight_name.replace("q_proj", "v_proj")
qkv_name = weight_name.replace("q_proj", "qkv_proj")
# If k_proj doesn't have lora, initialize it to zero
k_proj_weight = (
weights[k_name]
if "k_proj" in target_module
else torch.zeros_like(weights[v_name])
)
weights[qkv_name] = torch.cat(
(
weights[q_name],
k_proj_weight,
weights[v_name],
),
0,
)
weights.pop(q_name)
if "k_proj" in target_module:
weights.pop(k_name)
weights.pop(v_name)
elif "qkv_proj" in weight_name:
# If qkv_proj is already stacked, we normalize it following the SGL convention.
qkv_name = weight_name
q_name = weight_name.replace("qkv_proj", "q_proj")
k_name = weight_name.replace("qkv_proj", "k_proj")
v_name = weight_name.replace("qkv_proj", "v_proj")
if "lora_A" in weight_name:
weights[qkv_name] = weights[qkv_name].repeat(3, 1)
# else: no-op as LoRA B weight is already stacked.
def _rename_expert_w_to_proj(self, weights: Dict[str, torch.Tensor]):
"""Rename w1 -> gate_proj, w3 -> up_proj, w2 -> down_proj so that
normalize_gate_up_proj can stack them into gate_up_proj."""
renames = {}
for name in list(weights.keys()):
new_name = name
if ".w1." in name:
new_name = name.replace(".w1.", ".gate_proj.")
elif ".w3." in name:
new_name = name.replace(".w3.", ".up_proj.")
elif ".w2." in name:
new_name = name.replace(".w2.", ".down_proj.")
if new_name != name:
renames[name] = new_name
for old_name, new_name in renames.items():
weights[new_name] = weights.pop(old_name)
def _normalize_in_proj(self, weights: Dict[str, torch.Tensor]):
"""Stack gate_proj + x_proj → in_proj for Mamba layers.
Detects Mamba layers by the presence of both gate_proj and x_proj.
Must run BEFORE normalize_gate_up_proj to prevent gate_proj from
being consumed by the gate+up stacking.
"""
# Find gate_proj weights that have a matching x_proj (Mamba pattern)
for weight_name in list(weights.keys()):
if "gate_proj" not in weight_name:
continue
x_name = weight_name.replace("gate_proj", "x_proj")
if x_name not in weights:
continue
# This is a Mamba layer: stack gate_proj + x_proj → in_proj
in_proj_name = weight_name.replace("gate_proj", "in_proj")
cat_dim = weights[weight_name].dim() - 2
weights[in_proj_name] = torch.cat(
(weights[weight_name], weights[x_name]), cat_dim
)
weights.pop(weight_name)
weights.pop(x_name)
def _normalize_in_proj_qkvz(self, weights: Dict[str, torch.Tensor]):
"""Normalize in_proj_qkvz weights for GDN (GatedDeltaNet) layers like
Qwen3.5.
Two adapter formats are handled:
1. Split: ``in_proj_q + in_proj_k + in_proj_v + in_proj_z`` are present
as separate weights → concatenate them into ``in_proj_qkvz``.
2. Already-merged: the adapter has a single ``in_proj_qkvz`` weight
(PEFT trained against SGLang's fused Linear). The stacked buffer
expects four per-slice ``A`` blocks, so repeat ``lora_A`` 4× along
the rank dim. ``lora_B`` is already full-output-dim and matches
the buffer directly.
"""
for weight_name in list(weights.keys()):
if "in_proj_q." in weight_name:
k_name = weight_name.replace("in_proj_q", "in_proj_k")
v_name = weight_name.replace("in_proj_q", "in_proj_v")
z_name = weight_name.replace("in_proj_q", "in_proj_z")
if (
k_name not in weights
or v_name not in weights
or z_name not in weights
):
continue
qkvz_name = weight_name.replace("in_proj_q", "in_proj_qkvz")
cat_dim = weights[weight_name].dim() - 2
weights[qkvz_name] = torch.cat(
(
weights[weight_name],
weights[k_name],
weights[v_name],
weights[z_name],
),
cat_dim,
)
weights.pop(weight_name)
weights.pop(k_name)
weights.pop(v_name)
weights.pop(z_name)
elif "in_proj_qkvz" in weight_name and "lora_A" in weight_name:
# Already-merged adapter: replicate the shared A across the 4
# stacked slots the buffer expects (q, k, v, z).
ndim = weights[weight_name].dim()
repeat_dims = [1] * ndim
repeat_dims[ndim - 2] = 4
weights[weight_name] = weights[weight_name].repeat(*repeat_dims)
# else (in_proj_qkvz lora_B, or unrelated): no-op.
def normalize_gate_up_proj(
self, weight_names: List[str], weights: Dict[str, torch.Tensor]
):
for weight_name in weight_names:
if "gate_proj" in weight_name:
up_name = weight_name.replace("gate_proj", "up_proj")
gate_up_name = weight_name.replace("gate_proj", "gate_up_proj")
# PEFT can ship up_proj in two forms when there's no real
# up_proj content: the key may be absent, or present as a
# numel-zero placeholder. Treat both as "no up_proj".
if up_name not in weights or weights[up_name].numel() == 0:
if self._is_non_gated_moe_weight(weight_name):
# Non-gated MoE expert: the gate_up_proj_moe buffer
# uses stacked_multiply=1 (per model override), so just
# rename without stacking.
weights[gate_up_name] = weights.pop(weight_name)
if up_name in weights:
weights.pop(up_name)
continue
# Gated path: buffer expects stacked [2r, hidden] (c=2);
# synthesize a properly-shaped zero up_proj.
weights[up_name] = torch.zeros_like(weights[weight_name])
cat_dim = weights[weight_name].dim() - 2
weights[gate_up_name] = torch.cat(
(weights[weight_name], weights[up_name]), cat_dim
)
weights.pop(weight_name)
weights.pop(up_name)
elif "gate_up_proj" in weight_name:
# If gate_up_proj is already stacked, we normalize it following the SGL convention
gate_up_name = weight_name
if "lora_A" in weight_name:
ndim = weights[gate_up_name].dim()
repeat_dims = [1] * ndim
repeat_dims[ndim - 2] = 2
weights[gate_up_name] = weights[gate_up_name].repeat(*repeat_dims)
# else: no-op as LoRA B weight is already stacked.
# Orphan up_proj weights (no matching gate_proj) are kept as-is.
# Models with non-gated MLP/shared-experts declare up_proj in
# supported_lora_modules so they get their own buffer and wrapping.
def normalize_fused_qkv_a_proj(
self, weight_names: List[str], weights: Dict[str, torch.Tensor]
):
"""Fuse separate q_a_proj and kv_a_proj_with_mqa LoRA weights into
a single fused_qkv_a_proj_with_mqa entry (concat along dim 0 for
both A and B), matching the DeepSeek MLA fused projection layout."""
for weight_name in weight_names:
if "q_a_proj" not in weight_name:
continue
if "fused_qkv_a_proj_with_mqa" in weight_name:
continue
q_a_name = weight_name
kv_a_name = weight_name.replace("q_a_proj", "kv_a_proj_with_mqa")
fused_name = weight_name.replace("q_a_proj", "fused_qkv_a_proj_with_mqa")
kv_a_weight = (
weights[kv_a_name]
if kv_a_name in weights
else torch.zeros_like(weights[q_a_name])
)
weights[fused_name] = torch.cat((weights[q_a_name], kv_a_weight), dim=0)
weights.pop(q_a_name)
if kv_a_name in weights:
weights.pop(kv_a_name)
def pin_weights_in_cpu(self):
for layer in self.layers:
for name, weight in layer.weights.items():
layer.weights[name] = weight.pin_memory()
for name, weight in self.embedding_layers.items():
self.embedding_layers[name] = weight.pin_memory()
for name, weight in self.added_tokens_embeddings.items():
self.added_tokens_embeddings[name] = weight.pin_memory()
+108
View File
@@ -0,0 +1,108 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
import json
import logging
import os
from typing import Dict, Optional
from huggingface_hub import snapshot_download
logger = logging.getLogger(__name__)
class LoRAConfig:
def __init__(
self,
path: Optional[str] = None,
config_dict: Optional[Dict] = None,
added_tokens_config: Optional[Dict] = None,
base_vocab_size: Optional[int] = None,
) -> None:
self.path = path
if config_dict is not None:
self.hf_config = config_dict
self.added_tokens_config = added_tokens_config
else:
self.hf_config = self.get_lora_config()
self.added_tokens_config = self.get_added_tokens_config()
self.target_modules = self.hf_config["target_modules"]
self.r = self.hf_config["r"]
self.lora_alpha = self.hf_config["lora_alpha"]
self.use_dora = self.hf_config.get("use_dora", False)
# Filter fake added tokens: tokens with ID < base_vocab_size are already
# part of the base vocabulary and should not be treated as added tokens.
# This commonly happens when added_tokens.json is copied from the base
# model's tokenizer.
if self.added_tokens_config and base_vocab_size is not None:
self.added_tokens_config = {
token: token_id
for token, token_id in self.added_tokens_config.items()
if token_id >= base_vocab_size
}
self.lora_added_tokens_size = (
len(self.added_tokens_config) if self.added_tokens_config is not None else 0
)
@classmethod
def from_dict(
cls,
config_dict: Dict,
added_tokens_config: Optional[Dict] = None,
base_vocab_size: Optional[int] = None,
) -> "LoRAConfig":
return cls(
config_dict=config_dict,
added_tokens_config=added_tokens_config,
base_vocab_size=base_vocab_size,
)
def get_lora_config(self, dummy=False):
if dummy:
raise NotImplementedError()
else:
if not os.path.isdir(self.path):
weights_dir = snapshot_download(self.path, allow_patterns=["*.json"])
else:
weights_dir = self.path
config_name = "adapter_config.json"
with open(os.path.join(weights_dir, config_name), "r") as f:
return json.load(f)
def get_added_tokens_config(self):
"""Load added tokens from the LoRA adapter if the file exists."""
# Determine the weights directory
if not os.path.isdir(self.path):
weights_dir = snapshot_download(self.path, allow_patterns=["*.json"])
else:
weights_dir = self.path
# Construct the path to added_tokens.json
added_tokens_path = os.path.join(weights_dir, "added_tokens.json")
# Return None if the file doesn't exist (optional for standard LoRA adapters)
if not os.path.exists(added_tokens_path):
return None
# Load and return the added tokens
try:
with open(added_tokens_path, "r") as f:
return json.load(f)
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse added_tokens.json: {e}")
return None
+191
View File
@@ -0,0 +1,191 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
import logging
import time
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, List, Optional
from sglang.srt.managers.schedule_batch import Req
logger = logging.getLogger(__name__)
DRAIN_SCHEDULE_TOLERANCE = 1.2
@dataclass
class AdapterStats:
num_waiting_reqs: int = 0
max_wait_time_secs: float = 0.0
max_remaining_tokens: int = 0
is_draining_for: Optional[str] = None
def _reset_stats(self):
self.num_waiting_reqs = 0
self.max_wait_time_secs = 0.0
self.max_remaining_tokens = 0
def is_starving(self, drain_wait_threshold: float):
return (
self.max_wait_time_secs > drain_wait_threshold and self.num_waiting_reqs > 0
)
class LoRADrainer:
"""
Drainer for LoRA requests that manages draining. It tracks:
- Number of waiting requests per adapter
- Maximum wait time for requests needing each adapter
- Maximum number of tokens needed for running requests for each adapter
"""
def __init__(self, max_loras_per_batch: int, max_wait_time_secs: float = 0.0):
self.max_loras_per_batch = max_loras_per_batch
self.max_wait_time_secs = max_wait_time_secs
self.adapter_to_stats: Dict[Optional[str], AdapterStats] = defaultdict(
AdapterStats
)
def update_draining_state(
self,
waiting_queue: List[Req],
running_reqs: List[Req],
) -> None:
"""
Update LoRA drainer state based on current waiting queue and running requests.
This method updates adapter statistics, identifies starving adapters that need
to be scheduled, and marks adapters for draining to make room for starving ones.
"""
self._update_adapter_stats(waiting_queue, running_reqs)
self._update_draining_loras(running_reqs)
self._update_fully_drained_loras(running_reqs)
def _update_adapter_stats(
self,
waiting_queue: List[Req],
running_reqs: List[Req],
) -> None:
for stats in self.adapter_to_stats.values():
stats._reset_stats()
for req in waiting_queue:
stats = self.adapter_to_stats[req.lora_id]
stats.num_waiting_reqs += 1
stats.max_wait_time_secs = max(
stats.max_wait_time_secs,
time.monotonic() - req.time_stats.wait_queue_entry_time,
)
for req in running_reqs:
stats = self.adapter_to_stats[req.lora_id]
stats.max_remaining_tokens = max(
stats.max_remaining_tokens,
req.sampling_params.max_new_tokens - len(req.output_ids),
)
def _update_draining_loras(self, running_reqs: List[Req]) -> None:
"""
Select LoRA adapters to drain based on starvation detection.
This method identifies adapters in the waiting queue that are "starving"
(waiting too long) and marks currently running adapters as "draining"
to make room for the starving adapters. Draining adapters will not
accept new requests, allowing them to complete and free up slots.
"""
running_adapter_ids = {req.lora_id for req in running_reqs}
if len(running_adapter_ids) < self.max_loras_per_batch:
return None
starving_adapters = set()
draining_for_adapters = set()
for adapter_id, stats in self.adapter_to_stats.items():
if stats.is_starving(self.max_wait_time_secs):
starving_adapters.add(adapter_id)
draining_for_adapter = stats.is_draining_for
if draining_for_adapter is not None:
draining_for_adapters.add(draining_for_adapter)
new_starving_adapters = starving_adapters - draining_for_adapters
if not new_starving_adapters:
return None
sorted_new_starving_adapters = sorted(
new_starving_adapters,
key=lambda adapter: self.adapter_to_stats[adapter].max_wait_time_secs,
reverse=True,
)
eligible_to_drain_adapters = {
adapter
for adapter in running_adapter_ids
if self.adapter_to_stats[adapter].is_draining_for is None
}
for starving_adapter in sorted_new_starving_adapters:
if not eligible_to_drain_adapters:
break
min_eligible_adapter = min(
eligible_to_drain_adapters,
key=lambda adapter_id: self.adapter_to_stats[
adapter_id
].max_remaining_tokens,
)
self.adapter_to_stats[min_eligible_adapter].is_draining_for = (
starving_adapter
)
logger.debug(
f"LoRA adapter {min_eligible_adapter} is draining for {starving_adapter}"
)
eligible_to_drain_adapters.remove(min_eligible_adapter)
def _update_fully_drained_loras(self, running_reqs: List[Req]) -> None:
"""
Clear draining state for adapters that have fully drained.
An adapter is considered fully drained when it was marked as draining
but no longer has any running requests.
"""
running_adapter_ids = {req.lora_id for req in running_reqs}
for adapter_id, stats in self.adapter_to_stats.items():
if stats.is_draining_for is None:
continue
if adapter_id not in running_adapter_ids:
logger.debug(f"LoRA adapter {adapter_id} finished draining")
stats.is_draining_for = None
def can_schedule(self, req: Req) -> bool:
"""
Check if a request can be scheduled based on draining state.
If the adapter for this request is currently draining, only allow
scheduling if the request's max_new_tokens is within tolerance of
the max remaining tokens for the draining adapter.
"""
stats = self.adapter_to_stats[req.lora_id]
if not stats.is_draining_for:
return True
return (
req.sampling_params.max_new_tokens
<= stats.max_remaining_tokens * DRAIN_SCHEDULE_TOLERANCE
)
+858
View File
@@ -0,0 +1,858 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# Integrates "S-LoRA: Serving Thousands of Concurrent LoRA Adapters"
# and "Punica: Multi-Tenant LoRA Serving"
import logging
from typing import Dict, Iterable, List, Optional
import torch
from sglang.srt.configs.load_config import LoadConfig
from sglang.srt.environ import envs
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
from sglang.srt.layers.utils import get_layer_id
from sglang.srt.layers.vocab_parallel_embedding import (
ParallelLMHead,
VocabParallelEmbedding,
)
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.layers import BaseLayerWithLoRA, FusedMoEWithLoRA, get_lora_layer
from sglang.srt.lora.lora import LoRAAdapter
from sglang.srt.lora.lora_config import LoRAConfig
from sglang.srt.lora.lora_registry import LoRARef
from sglang.srt.lora.mem_pool import LoRAMemoryPool
from sglang.srt.lora.utils import (
DSA_INDEXER_LORA_NAMES,
EMBEDDING_NAMES,
LoRAType,
auto_detect_lora_target_modules,
get_normalized_target_modules,
get_target_module_name,
)
from sglang.srt.managers.io_struct import LoRAUpdateOutput
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.server_args import ServerArgs
from sglang.srt.utils import replace_submodule
from sglang.srt.utils.hf_transformers_utils import AutoConfig
_SGLANG_EXPERIMENTAL_LORA_OPTI = envs.SGLANG_EXPERIMENTAL_LORA_OPTI.get()
logger = logging.getLogger(__name__)
class LoRAManager:
def __init__(
self,
base_model: torch.nn.Module,
base_hf_config: AutoConfig,
max_loras_per_batch: int,
load_config: LoadConfig,
dtype: torch.dtype,
server_args: ServerArgs,
lora_backend: str = "triton",
tp_size: int = 1,
tp_rank: int = 0,
max_lora_rank: Optional[int] = None,
target_modules: Optional[Iterable[str]] = None,
lora_paths: Optional[List[LoRARef]] = None,
):
self.base_model: torch.nn.Module = base_model
if hasattr(base_hf_config, "get_text_config"):
self.base_hf_config: AutoConfig = base_hf_config.get_text_config()
else:
self.base_hf_config: AutoConfig = base_hf_config
self.max_loras_per_batch: int = max_loras_per_batch
self.load_config: LoadConfig = load_config
self.dtype: torch.dtype = dtype
self.device: torch.device = next(self.base_model.parameters()).device
self.tp_size: int = tp_size
self.tp_rank: int = tp_rank
self.lora_added_tokens_size: Optional[int] = None
self.enable_lora_overlap_loading: Optional[bool] = (
server_args.enable_lora_overlap_loading
)
self.eviction_policy = server_args.lora_eviction_policy
self._experts_shared_outer_override: Optional[bool] = (
server_args.experts_shared_outer_loras
)
self.lora_use_virtual_experts: bool = server_args.lora_use_virtual_experts
self.lora_strict_loading: bool = getattr(
server_args, "lora_strict_loading", False
)
# LoRA backend for running sgemm kernels
logger.info(f"Using {lora_backend} as backend of LoRA kernels.")
backend_type = get_backend_from_name(lora_backend)
self.lora_backend: BaseLoRABackend = backend_type(
max_loras_per_batch=max_loras_per_batch,
device=self.device,
server_args=server_args,
)
# Initialize mutable internal state of the LoRAManager.
self.init_state(
max_lora_rank=max_lora_rank,
target_modules=target_modules,
lora_paths=lora_paths,
)
def init_cuda_graph_batch_info(
self, max_bs_in_cuda_graph: int, num_tokens_per_bs: int
):
"""Phase 2 of LoRA CUDA graph init: dense LoRA batch metadata.
Called during CudaGraphRunner.__init__(), after init_memory_pool().
Phase 1 (MoE buffers) is handled earlier via init_cuda_graph_moe_buffers().
"""
self.max_bs_in_cuda_graph = max_bs_in_cuda_graph
self.lora_backend.init_cuda_graph_batch_info(
max_bs_in_cuda_graph=max_bs_in_cuda_graph,
num_tokens_per_bs=num_tokens_per_bs,
)
# ===== TO BE REFACTORED ====
# Pre-create the experimental LoRA two-stream side stream now (gated) so the
# torch.cuda.Stream() call never lands inside a cuda-graph capture region.
if _SGLANG_EXPERIMENTAL_LORA_OPTI:
from sglang.srt.lora.trtllm_lora_temp import (
init_lora_two_stream_resources,
)
init_lora_two_stream_resources(self.device)
# ===== END TO BE REFACTORED ====
def init_cuda_graph_moe_buffers(
self, max_bs: int, max_loras: int, compute_dtype, moe_layer
):
"""Phase 1 of LoRA CUDA graph init: MoE intermediate buffers.
Called before init_memory_pool() so memory profiling accounts for them.
Phase 2 (dense batch metadata) is handled later via init_cuda_graph_batch_info().
"""
self.lora_backend.init_cuda_graph_moe_buffers(
max_bs=max_bs,
max_loras=max_loras,
compute_dtype=compute_dtype,
moe_layer=moe_layer,
)
def create_lora_update_result(
self, success: bool, error_message: str = ""
) -> LoRAUpdateOutput:
return LoRAUpdateOutput(
success=success,
error_message=error_message,
loaded_adapters={
lora_ref.lora_name: lora_ref.lora_path
for lora_ref in self.lora_refs.values()
},
)
def load_lora_adapter(self, lora_ref: LoRARef) -> LoRAUpdateOutput:
"""
Load a single LoRA adapter from the specified path.
Args:
lora_ref (LoRARef): The LoRARef object containing the LoRA name, path, and ID.
"""
assert (
lora_ref.lora_name is not None and lora_ref.lora_path is not None
), "LoRARef must have both lora_name and lora_path set for loading."
assert (
lora_ref.lora_id not in self.loras
), f"LoRA adapter with ID {lora_ref.lora_id} is already loaded. This should have been verified before request is sent to the backend."
try:
# load configs
new_adapter = LoRAConfig(
lora_ref.lora_path,
base_vocab_size=self.base_hf_config.vocab_size,
)
self.validate_new_adapter(new_adapter, lora_ref)
self.configs[lora_ref.lora_id] = new_adapter
# load weights
self.load_lora_weights(lora_ref)
# keep metadata for displayed messages
self.lora_refs[lora_ref.lora_id] = lora_ref
self.num_pinned_loras += int(lora_ref.pinned)
except Exception as e:
return self.create_lora_update_result(
success=False,
error_message=str(e),
)
return self.create_lora_update_result(success=True)
def validate_new_adapter(self, lora_config: LoRAConfig, lora_ref: LoRARef):
"""
Validate if an adapter can be loaded into the current LoRA memory pool and generate error if it is incompatible.
"""
if lora_config.lora_added_tokens_size > 0:
raise ValueError(
f"Failed to load {lora_ref.lora_name} because LoRA serving currently doesn't support adapters that add tokens to the vocabulary"
)
if lora_config.use_dora:
raise ValueError(
f"Failed to load {lora_ref.lora_name} because LoRA serving currently doesn't support DoRA adapters"
)
# Check if this LoRA adapter is already loaded
for existing_lora_ref in self.lora_refs.values():
if lora_ref.lora_name == existing_lora_ref.lora_name:
raise ValueError(
f"Failed to load LoRA adapter {lora_ref.lora_name} because it is already loaded"
)
if lora_ref.lora_path == existing_lora_ref.lora_path:
logger.warning(
f"{lora_ref.lora_path} is already loaded with name: {existing_lora_ref.lora_name}, "
f"but another copy is being loaded with name: {lora_ref.lora_name}"
)
# Check if the LoRA adapter shape is compatible with the current LoRA memory pool configuration.
memory_pool = getattr(self, "memory_pool", None)
incompatible = memory_pool and not memory_pool.can_support(lora_config)
if incompatible:
raise ValueError(
f"LoRA adapter {lora_ref.lora_name} with rank {lora_config.r} is incompatible with the current "
"LoRA memory pool configuration. Please ensure that the LoRA adapter's rank is within the configured "
"`--max-lora-rank` and that the target modules are included in `--lora-target-modules`."
)
# Ensure pinned LoRA adapters does not exceed maximal limit or cause starvation.
if lora_ref.pinned and self.num_pinned_loras >= self.max_loras_per_batch - 1:
raise ValueError(
f"Failed to load LoRA adapter {lora_ref.lora_name} as a pinned adapter. It is not allowed to pin all slots "
"in the LoRA memory pool to avoid starvation for unpinned adapters and base models. Please increase your "
"`--max-loras-per-batch` or load it as unpinned LoRA adapters."
)
def unload_lora_adapter(self, lora_ref: LoRARef) -> LoRAUpdateOutput:
"""
Unload LoRA adapters by their names. This will remove the adapters from the memory pool and
delete the corresponding LoRA modules.
"""
adapter = self.configs.get(lora_ref.lora_id)
lora_ref = self.lora_refs.get(lora_ref.lora_id)
assert (
adapter is not None and lora_ref is not None
), f"LoRA adapter with ID {lora_ref.lora_id} is not loaded. This should have been verified before request is sent to the backend."
try:
del self.configs[lora_ref.lora_id]
del self.loras[lora_ref.lora_id]
del self.lora_refs[lora_ref.lora_id]
self.num_pinned_loras -= int(lora_ref.pinned)
except Exception as e:
return self.create_lora_update_result(
success=False,
error_message=str(e),
)
return self.create_lora_update_result(success=True)
def validate_lora_batch(self, lora_ids: set[Optional[str]]) -> bool:
"""
Validate if the LoRA IDs in the batch can be loaded into the current LoRA memory pool.
"""
if len(lora_ids) > self.max_loras_per_batch:
return False
# skip pinned LoRA check if no pinned LoRA adapters are loaded.
if self.num_pinned_loras == 0:
return True
# counting the number of pinned LoRA adapters in the batch.
pinned_loras_in_batch = 0
for lora_id in lora_ids:
if lora_id is not None:
lora_ref = self.lora_refs.get(lora_id)
assert (
lora_ref is not None
), f"LoRA ID {lora_id} not found in lora_refs."
pinned_loras_in_batch += int(lora_ref.pinned)
assert pinned_loras_in_batch <= self.num_pinned_loras, (
f"Number of pinned LoRA adapters in the batch ({pinned_loras_in_batch}) exceeds the total number of pinned adapters "
f"({self.num_pinned_loras}). This indicates a bug in the LoRA loading logic."
)
required_slots = len(lora_ids) - pinned_loras_in_batch
mem_pool_vacancy = self.memory_pool.max_loras_per_batch - self.num_pinned_loras
return required_slots <= mem_pool_vacancy
def fetch_new_loras(
self, new_loras: set[Optional[str]], running_loras: set[Optional[str]] = set()
):
# Load active loras into lora memory pool
cur_uids = new_loras | running_loras
assert len(cur_uids) <= self.max_loras_per_batch
self.memory_pool.prepare_lora_batch(
cur_uids=cur_uids,
lora_adapters=self.loras,
lora_modules=self.lora_modules,
lora_refs=self.lora_refs.copy(), # copy snapshot of current lora_refs to avoid mutation during the batch preparation.
lora_embed_tokens_module=self.embed_tokens_module, # merge into embedding or lora module
lora_lm_head_module=self.lm_head_module, # merge into embedding or lora module
)
def prepare_lora_batch(self, forward_batch: ForwardBatch):
# set up batch info shared by all lora modules
bs = forward_batch.batch_size
use_cuda_graph = (
hasattr(self, "max_bs_in_cuda_graph")
and bs <= self.max_bs_in_cuda_graph
and forward_batch.forward_mode.is_cuda_graph()
)
weight_indices = [0] * len(forward_batch.lora_ids)
lora_ranks = [0] * self.max_loras_per_batch
scalings = [0] * self.max_loras_per_batch
for i, uid in enumerate(forward_batch.lora_ids):
if uid not in self.memory_pool.uid_to_buffer_id:
continue
weight_indices[i] = self.memory_pool.get_buffer_id(uid)
if uid is not None:
lora = self.loras[uid]
lora_ranks[weight_indices[i]] = lora.config.r
scalings[weight_indices[i]] = lora.scaling
# Do in-place updates when CUDA graph is enabled and the batch forward mode
# could use CUDA graph.
self.lora_backend.prepare_lora_batch(
forward_batch=forward_batch,
weight_indices=weight_indices,
lora_ranks=lora_ranks,
scalings=scalings,
use_cuda_graph=use_cuda_graph,
)
self.lora_backend.batch_info.has_active_lora = any(
lora_ranks[wi] > 0 for wi in weight_indices
)
def update_lora_info(self):
"""
Update all LoRA modules to associate them with the latest memory buffer.
"""
for layer_id, layer_modules in enumerate(self.lora_modules):
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_key = (
"gate_up_proj_moe"
if "gate_up_proj_moe" in self.memory_pool.A_buffer
else "gate_up_proj"
)
down_key = (
"down_proj_moe"
if "down_proj_moe" in self.memory_pool.A_buffer
else "down_proj"
)
gate_up_a = self.memory_pool.get_tensor(
target_module=gate_up_key,
layer_id=layer_id,
lora_type=LoRAType.LORA_A,
)
gate_up_b = self.memory_pool.get_tensor(
target_module=gate_up_key,
layer_id=layer_id,
lora_type=LoRAType.LORA_B,
)
down_a = self.memory_pool.get_tensor(
target_module=down_key,
layer_id=layer_id,
lora_type=LoRAType.LORA_A,
)
down_b = self.memory_pool.get_tensor(
target_module=down_key,
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(
module_name, self.memory_pool.target_modules
)
module.set_lora_info(
self.memory_pool.get_tensor(
target_module=target_module,
layer_id=layer_id,
lora_type=LoRAType.LORA_A,
),
self.memory_pool.get_tensor(
target_module=target_module,
layer_id=layer_id,
lora_type=LoRAType.LORA_B,
),
)
# Update embedding layer if present - gotta merge (refer to PR codebase)
if self.embed_tokens_module is not None:
self.embed_tokens_module.set_lora_info(
self.memory_pool.get_embedding_tensor("added_tokens", LoRAType.LORA_A),
self.memory_pool.get_embedding_tensor("embed_tokens", LoRAType.LORA_A),
self.memory_pool.get_embedding_tensor("embed_tokens", LoRAType.LORA_B),
)
# Update lm_head layer if present
if self.lm_head_module is not None:
self.lm_head_module.set_lora_info(
self.memory_pool.get_embedding_tensor("lm_head", LoRAType.LORA_A),
self.memory_pool.get_embedding_tensor("lm_head", LoRAType.LORA_B),
)
def init_state(
self,
max_lora_rank: Optional[int] = None,
target_modules: Optional[Iterable[str]] = None,
lora_paths: Optional[List[LoRARef]] = None,
):
"""
Initialize the internal (mutable) state of the LoRAManager.
When `lora_paths` is provided and not empty, it might be used for inferring LoRA shape info such as
the target modules and max_lora_rank.
"""
assert lora_paths or (
max_lora_rank is not None and target_modules is not None
), "When no initial --lora-paths is provided, you need to specify both --max-lora-rank and --lora-target-modules for LoRA initialization."
self.init_lora_adapters(lora_paths)
self.init_lora_shapes(
max_lora_rank=max_lora_rank,
target_modules=target_modules,
)
if self._experts_shared_outer_override is not None:
self.experts_shared_outer_loras = self._experts_shared_outer_override
else:
self.experts_shared_outer_loras = self._detect_shared_outer_loras()
if self.experts_shared_outer_loras:
logger.info(
"Shared outer LoRA mode enabled: gate_up lora_A and "
"down lora_B will be shared across experts (expert_dim=1)."
)
self.init_lora_modules()
self.init_memory_pool()
self.update_lora_info()
def init_lora_adapters(self, lora_paths: Optional[List[LoRARef]] = None):
# Configs of all active LoRA adapters, indexed by LoRA ID.
self.configs: Dict[str, LoRAConfig] = {}
# LoRA adapter weights cached in CPU memory, indexed by LoRA ID.
self.loras: Dict[str, LoRAAdapter] = {}
# Mapping from LoRA ID to LoRARef object.
self.lora_refs: Dict[str, LoRARef] = {}
# Count of pinned LoRA adapters.
self.num_pinned_loras: int = 0
if lora_paths:
for lora_ref in lora_paths:
result = self.load_lora_adapter(lora_ref)
if not result.success:
raise RuntimeError(
f"Failed to load LoRA adapter {lora_ref.lora_name}: {result.error_message}"
)
def _detect_shared_outer_loras(self) -> bool:
"""Auto-detect shared outer LoRA format from loaded adapter weights.
MoE adapters with shared outer experts store 3D tensors where
dim[0]=1 indicates weights shared across all experts, while
dim[0]=num_experts indicates per-expert weights.
Returns True if gate_up lora_A has expert_dim=1 (shared).
All loaded adapters that expose a 3D gate_up lora_A must agree;
mixed formats raise RuntimeError.
"""
shared_outer: Optional[bool] = None
for adapter_id, adapter in self.loras.items():
found = False
for layer in adapter.layers:
for name, weight in layer.weights.items():
if (
"gate_up_proj" in name
and "lora_A" in name
and weight.dim() == 3
):
is_shared = weight.shape[0] == 1
if shared_outer is None:
shared_outer = is_shared
elif shared_outer != is_shared:
raise RuntimeError(
"Mixed shared-outer LoRA formats detected across "
f"loaded adapters (conflict in adapter '{adapter_id}'). "
"All MoE adapters must either all use shared outer "
"experts (expert_dim=1) or all use per-expert weights."
)
found = True
break
if found:
break
return bool(shared_outer) if shared_outer is not None else False
def init_lora_shapes(
self,
max_lora_rank: Optional[int] = None,
target_modules: Optional[Iterable[str]] = None,
):
"""Infer LoRA target modules and max_lora_rank from loaded adapters if not provided."""
if target_modules and target_modules == {"all"}:
self.target_modules = auto_detect_lora_target_modules(self.base_model)
self.target_modules.update(EMBEDDING_NAMES)
logger.info(
"CLI --lora-target-modules='all' resolved to %s "
"by inspecting the base model.",
sorted(self.target_modules),
)
target_modules = self.target_modules
elif target_modules:
self.target_modules = get_normalized_target_modules(target_modules)
else:
self.target_modules = set()
for lora_id, config in self.configs.items():
# Handle PEFT shorthand strings like "all-linear" or "all".
if isinstance(config.target_modules, str):
if config.target_modules in ("all-linear", "all"):
if target_modules is not None:
# CLI --lora-target-modules already provided; skip
# per-adapter inference for this adapter.
continue
else:
# Resolve by scanning the base model for all
# LoRA-compatible linear modules.
adapter_target_modules = auto_detect_lora_target_modules(
self.base_model
)
logger.info(
"LoRA adapter '%s' uses target_modules='%s'. "
"Resolved to %s by inspecting the base model.",
self.lora_refs[lora_id].lora_name,
config.target_modules,
sorted(adapter_target_modules),
)
self.target_modules.update(adapter_target_modules)
continue
else:
raise ValueError(
f"SGLang does not recognize target_modules="
f"'{config.target_modules}'. Please use a list of module "
"name suffixes in the adapter's PEFT config, or explicitly "
"specify --lora-target-modules during server startup."
)
if not isinstance(config.target_modules, list):
raise ValueError(
f"SGLang currently only supports inferring LoRA target modules when a list of "
"suffixes is provided in `target_modules` field of PEFT config. Please explicitly "
"specify `--lora-target-modules` during server startup. You can specify `all` to "
"enable all support modules types. "
)
adapter_target_modules = get_normalized_target_modules(
config.target_modules
)
if target_modules is not None:
# When `--lora-target-modules` is provided, validate adapter target modules is a subset of the specified target modules.
if not adapter_target_modules.issubset(self.target_modules):
unsupported_modules = adapter_target_modules - self.target_modules
lora_name = self.lora_refs[lora_id].lora_name
raise ValueError(
f"LoRA adapter '{lora_name}' contains target modules {sorted(unsupported_modules)} "
f"that are not included in the specified --lora-target-modules {sorted(self.target_modules)}. "
f"Please update --lora-target-modules to include all required modules: "
f"{sorted(self.target_modules | adapter_target_modules)}, or use 'all' to enable all supported modules."
)
else:
# Otherwise, infer target_modules from adapter configs.
self.target_modules.update(adapter_target_modules)
# Fusion folds wk + weights_proj into wk_weights_proj, so the modules
# LoRA wraps are absent and an indexer-targeted adapter is silently dropped.
indexer_targets = self.target_modules & DSA_INDEXER_LORA_NAMES
if indexer_targets:
from sglang.srt.layers.attention.dsa.dsa_indexer import (
_use_dsa_indexer_fusion,
)
if _use_dsa_indexer_fusion:
raise ValueError(
f"LoRA targets the DSA indexer ({sorted(indexer_targets)}), which is "
"incompatible with DSA indexer Q/K fusion. Set "
"SGLANG_DISABLE_DSA_INDEXER_FUSION=1 to disable fusion and use indexer LoRA."
)
if max_lora_rank is not None:
self.max_lora_rank = max_lora_rank
else:
self.max_lora_rank = max(
[x.r for x in self.configs.values()],
default=0,
)
# Auto-infer self.lora_added_vocab_size from loaded LoRA configs
# This happens automatically without requiring user input
# if self.lora_added_vocab_size is None:
if self.lora_added_tokens_size is None:
inferred_extra_vocab_size = next(
(
x.lora_added_tokens_size
for x in self.configs.values()
if x.lora_added_tokens_size > 0
),
0,
)
if inferred_extra_vocab_size > 0:
logger.info(
f"self.lora_added_tokens_size={inferred_extra_vocab_size} from LoRA adapters."
)
self.lora_added_tokens_size = inferred_extra_vocab_size
def load_lora_weights(self, lora_ref: LoRARef):
"""
Load the weights of a LoRA adapter to CPU memory and conducts post-loading validation.
"""
lora_adapter = LoRAAdapter(
lora_ref.lora_id,
self.configs[lora_ref.lora_id],
self.base_hf_config,
self.load_config,
self.lora_backend,
base_model=self.base_model,
)
lora_adapter.initialize_weights()
self.loras[lora_ref.lora_id] = lora_adapter
def load_lora_weights_from_tensors(
self, lora_ref: LoRARef, tensors: Dict[str, torch.Tensor]
):
"""
Load the weights of a LoRA adapter from tensors to CPU memory.
"""
lora_adapter = LoRAAdapter(
lora_ref.lora_id,
self.configs[lora_ref.lora_id],
self.base_hf_config,
self.load_config,
self.lora_backend,
base_model=self.base_model,
)
lora_adapter.initialize_weights_from_tensors(tensors)
self.loras[lora_ref.lora_id] = lora_adapter
def load_lora_adapter_from_tensors(
self,
lora_ref: LoRARef,
tensors: Dict[str, torch.Tensor],
config_dict: Dict,
added_tokens_config: Optional[Dict] = None,
) -> LoRAUpdateOutput:
"""
Load a single LoRA adapter from tensors and config dict.
"""
assert (
lora_ref.lora_name is not None and lora_ref.lora_path is not None
), "LoRARef must have both lora_name and lora_path set for loading."
assert (
lora_ref.lora_id not in self.loras
), f"LoRA adapter with ID {lora_ref.lora_id} is already loaded. This should have been verified before request is sent to the backend."
try:
new_adapter = LoRAConfig.from_dict(
config_dict,
added_tokens_config,
base_vocab_size=self.base_hf_config.vocab_size,
)
self.validate_new_adapter(new_adapter, lora_ref)
self.configs[lora_ref.lora_id] = new_adapter
self.load_lora_weights_from_tensors(lora_ref, tensors)
self.lora_refs[lora_ref.lora_id] = lora_ref
self.num_pinned_loras += int(lora_ref.pinned)
except Exception as e:
return self.create_lora_update_result(
success=False,
error_message=str(e),
)
return self.create_lora_update_result(success=True)
def init_memory_pool(self):
"""(Re)initialize the LoRA memory pool based on the current configurations."""
self.memory_pool = LoRAMemoryPool(
base_hf_config=self.base_hf_config,
max_loras_per_batch=self.max_loras_per_batch,
dtype=self.dtype,
tp_size=self.tp_size,
tp_rank=self.tp_rank,
max_lora_rank=self.max_lora_rank,
target_modules=self.target_modules,
base_model=self.base_model,
eviction_policy=self.eviction_policy,
lora_added_tokens_size=self.lora_added_tokens_size,
experts_shared_outer_loras=self.experts_shared_outer_loras,
strict_loading=self.lora_strict_loading,
enable_lora_overlap_loading=self.enable_lora_overlap_loading,
)
# Initializing memory pool with base model
self.fetch_new_loras({None})
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)
replace_submodule(self.base_model, module_name, lora_module)
return lora_module
def init_lora_modules(self):
# Look-up table that essentially maps (layer_index, module_name) to the corresponding LoRA module.
self.lora_modules: List[Dict[str, BaseLayerWithLoRA]] = [
{} for _ in range(self.base_hf_config.num_hidden_layers)
]
self.embed_tokens_module: Optional[BaseLayerWithLoRA] = None
self.lm_head_module: Optional[BaseLayerWithLoRA] = None
# When tie_word_embeddings=True, lm_head is the same Python object as
# embed_tokens. PyTorch's named_modules() deduplicates by object identity,
# so lm_head will not appear as a separate entry in the scan below,
# preventing LoRA from wrapping it. To fix this, we create a new
# ParallelLMHead that shares the same base weight tensor (no extra GPU
# memory) so that named_modules() yields it as an independent module.
if "lm_head" in self.target_modules:
lm_head = getattr(self.base_model, "lm_head", None)
embed_tokens = None
for name, mod in self.base_model.named_modules():
if name.endswith("embed_tokens"):
embed_tokens = mod
break
if (
lm_head is not None
and embed_tokens is not None
and lm_head is embed_tokens
):
logger.info(
"lm_head is tied with embed_tokens. Creating a separate "
"ParallelLMHead that shares the base weight for LoRA support."
)
untied_lm_head = ParallelLMHead(
num_embeddings=embed_tokens.org_vocab_size,
embedding_dim=embed_tokens.embedding_dim,
params_dtype=embed_tokens.weight.dtype,
org_num_embeddings=embed_tokens.org_vocab_size,
)
# Share the base weight tensor — no additional GPU memory.
untied_lm_head.weight = embed_tokens.weight
# Replace the model attribute so named_modules() sees it
# independently.
self.base_model.lm_head = untied_lm_head
for module_name, module in self.base_model.named_modules():
# Handle embed_tokens and lm_head before the should_apply_lora gate,
# since VL models' should_apply_lora patterns only match language
# model layers and would incorrectly skip these.
# Handle embed_tokens
if "embed_tokens" in module_name and "embed_tokens" in self.target_modules:
if isinstance(module, VocabParallelEmbedding) and not isinstance(
module, BaseLayerWithLoRA
):
lora_module = self.set_lora_module(module_name, module)
self.embed_tokens_module = lora_module
continue
# Handle lm_head
if "lm_head" in module_name and "lm_head" in self.target_modules:
if isinstance(module, ParallelLMHead) and not isinstance(
module, BaseLayerWithLoRA
):
lora_module = self.set_lora_module(module_name, module)
self.lm_head_module = lora_module
continue
# Handle DeepSeek MLA fused projection: set the boundary
# between q_a and kv_a output partitions so the LoRA layer
# can apply separate B projections for each.
if (
"fused_qkv_a_proj_with_mqa" in self.target_modules
and module_name.endswith("fused_qkv_a_proj_with_mqa")
):
from sglang.srt.lora.layers import ReplicatedLinearWithLoRA
layer_id = get_layer_id(module_name)
if layer_id is None:
continue
lora_module = self.set_lora_module(module_name, module)
if isinstance(lora_module, ReplicatedLinearWithLoRA):
q_lora_rank = getattr(self.base_hf_config, "q_lora_rank", None) or 0
lora_module.first_output_dim = q_lora_rank
self.lora_modules[layer_id][module_name] = lora_module
continue
# The module should be converted if it is included in target_names
parts = module_name.split(".")
if (
parts[-1] in self.target_modules
or ".".join(parts[-2:]) in self.target_modules
):
layer_id = get_layer_id(module_name)
if layer_id is None:
continue
self.lora_modules[layer_id][module_name] = self.set_lora_module(
module_name, module
)
continue
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)
if layer_id is None:
# FusedMoE submodules outside the decoder layer hierarchy
# (e.g. nested helpers under non-".layers." prefixes) have
# no resolvable layer id; skip them so we don't index
# `self.lora_modules` with `None`.
continue
lora_module = self.set_lora_module(module_name, module)
lora_module.experts_shared_outer_loras = self.experts_shared_outer_loras
lora_module.lora_use_virtual_experts = self.lora_use_virtual_experts
self.lora_modules[layer_id][module_name] = lora_module
@@ -0,0 +1,200 @@
"""Marlin MoE runner core with hook support for LoRA injection.
Uses Marlin int4/int8 kernels for the base MoE projections.
LoRA deltas are injected via hooks.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
from sglang.srt.layers.moe.moe_runner.marlin import MarlinMoeQuantInfo
from sglang.srt.utils import is_cuda
if TYPE_CHECKING:
from sglang.srt.layers.moe.token_dispatcher import (
StandardCombineInput,
StandardDispatchOutput,
)
_is_cuda = is_cuda()
if _is_cuda:
from sglang.jit_kernel.moe_wna16_marlin import moe_wna16_marlin_gemm
from sglang.kernels.ops.activation import silu_and_mul
from sglang.srt.layers.moe.fused_moe_triton.fused_marlin_moe import (
get_scalar_type,
)
from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import (
moe_align_block_size,
)
from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe_triton_kernels import (
moe_sum_reduce_triton,
)
from sglang.srt.layers.quantization.marlin_utils import marlin_make_workspace
class MarlinLoraRunnerCore:
"""
MoE runner using Marlin kernels for base projections, with hooks for LoRA.
Pipeline:
1. moe_wna16_marlin_gemm (gate_up)
1.5. hooks.after_gate_up
2. silu_and_mul
3. moe_wna16_marlin_gemm (down)
3.5. hooks.after_down
4. moe_sum_reduce
"""
def __init__(self, config: MoeRunnerConfig):
self.config = config
def run_from_dispatch(
self,
dispatch_output: StandardDispatchOutput,
quant_info: MarlinMoeQuantInfo,
runner_config: MoeRunnerConfig,
hooks=None,
) -> StandardCombineInput:
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
assert hooks is not None, "hooks must be provided for MarlinLoraRunnerCore"
hidden_states = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
topk_weights = topk_output.topk_weights
topk_ids = topk_output.topk_ids
assert runner_config.activation == "silu", "Only SiLU activation is supported."
assert (
torch.cuda.get_device_capability(hidden_states.device)[0] >= 9
), "MarlinLoraRunnerCore requires CUDA compute capability >= 9"
inplace = runner_config.inplace
routed_scaling_factor = runner_config.routed_scaling_factor
M, K = hidden_states.shape
E = quant_info.w13_qweight.shape[0]
N = quant_info.w2_qweight.shape[1] * 16
topk = topk_ids.shape[1]
num_bits = quant_info.weight_bits
for block_size_m in [8, 16, 32, 48, 64]:
if M * topk / E / block_size_m < 0.9:
break
sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
topk_ids, block_size_m, E
)
from sglang.srt.runtime_context import get_resources
buffers = get_resources().buffers
workspace = buffers.get("marlin_lora_workspace")
if workspace is None or workspace.device != hidden_states.device:
workspace = marlin_make_workspace(hidden_states.device, max_blocks_per_sm=4)
buffers["marlin_lora_workspace"] = workspace
scalar_type1 = get_scalar_type(num_bits, quant_info.w13_qzeros is not None)
scalar_type2 = get_scalar_type(num_bits, quant_info.w2_qzeros is not None)
# Stage 1: Gate/Up (Marlin)
intermediate_cache1 = torch.empty(
(M * topk, 2 * N), device=hidden_states.device, dtype=hidden_states.dtype
)
intermediate_cache1 = moe_wna16_marlin_gemm(
hidden_states,
intermediate_cache1,
quant_info.w13_qweight,
None,
quant_info.w13_scales,
None,
quant_info.w13_qzeros,
quant_info.w13_g_idx,
quant_info.w13_g_idx_sort_indices,
workspace,
sorted_token_ids,
expert_ids,
num_tokens_post_padded,
topk_weights,
moe_block_size=block_size_m,
top_k=topk,
mul_topk_weights=False,
is_ep=quant_info.expert_map is not None,
b_q_type=scalar_type1,
size_m=M,
size_n=2 * N,
size_k=K,
is_k_full=quant_info.is_k_full,
use_atomic_add=True,
use_fp32_reduce=True,
is_zp_float=False,
)
# Hook: after gate_up
if hooks.after_gate_up:
intermediate_cache1_3d = intermediate_cache1.view(M, topk, 2 * N)
hooks.after_gate_up(
hidden_states, intermediate_cache1_3d, topk_weights, topk_ids
)
# Stage 2: Activation
intermediate_cache2 = torch.empty(
(M * topk, N), device=hidden_states.device, dtype=hidden_states.dtype
)
silu_and_mul(intermediate_cache1.view(-1, 2 * N), intermediate_cache2)
# Stage 3: Down (Marlin)
intermediate_cache3 = torch.empty(
(M * topk, K), device=hidden_states.device, dtype=hidden_states.dtype
)
if quant_info.expert_map is not None:
intermediate_cache3.zero_()
intermediate_cache3 = moe_wna16_marlin_gemm(
intermediate_cache2,
intermediate_cache3,
quant_info.w2_qweight,
None,
quant_info.w2_scales,
None,
quant_info.w2_qzeros,
quant_info.w2_g_idx,
quant_info.w2_g_idx_sort_indices,
workspace,
sorted_token_ids,
expert_ids,
num_tokens_post_padded,
topk_weights,
moe_block_size=block_size_m,
top_k=1,
mul_topk_weights=True,
is_ep=quant_info.expert_map is not None,
b_q_type=scalar_type2,
size_m=M * topk,
size_n=K,
size_k=N,
is_k_full=quant_info.is_k_full,
use_atomic_add=True,
use_fp32_reduce=True,
is_zp_float=False,
)
intermediate_cache3 = intermediate_cache3.view(M, topk, K)
# Hook: after down
if hooks.after_down:
hooks.after_down(
intermediate_cache2, intermediate_cache3, topk_weights, topk_ids
)
# Stage 4: Reduction
output = hidden_states if inplace else torch.empty_like(hidden_states)
if routed_scaling_factor is None:
routed_scaling_factor = 1.0
# NOTE: fusion opportunity here
moe_sum_reduce_triton(intermediate_cache3, output, routed_scaling_factor)
return StandardCombineInput(hidden_states=output)
+556
View File
@@ -0,0 +1,556 @@
# 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 hooks for MoE runners.
LoRA deltas are injected at two points in the MoE pipeline:
1. After gate_up projection, BEFORE activation
2. After down projection, BEFORE final reduction
This module provides hook closures that any MoE backend can call at those points,
without needing a per-backend LoRA runner subclass.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
import torch
from sglang.srt.model_executor.runner import get_is_capture_mode
from sglang.srt.utils import is_cuda, is_hip, is_xpu, next_power_of_2
_is_cuda = is_cuda()
_is_hip = is_hip()
_is_xpu = is_xpu()
if _is_cuda or _is_hip or _is_xpu:
from sglang.jit_kernel.moe_lora_align import moe_lora_align_block_size
def _get_moe_lora_block_config(max_lora_rank: int) -> dict:
"""Compute rank-aware block sizes for MoE LoRA kernels.
Shrink: output dim is the rank -> cap BLOCK_SIZE_N to avoid waste.
Expand: input dim is the rank -> cap BLOCK_SIZE_K similarly.
"""
if max_lora_rank <= 0:
rank_pow2 = 64
else:
rank_pow2 = next_power_of_2(max_lora_rank)
shrink_n = min(64, rank_pow2)
expand_k = max(16, min(64, rank_pow2))
return {
"shrink_block_size_n": shrink_n,
"expand_block_size_k": expand_k,
}
_SPARSITY_FACTOR = 8
def _naive_moe_lora_align_block_size(
topk_ids: torch.Tensor,
seg_indptr: torch.Tensor,
req_to_lora: torch.Tensor,
num_experts: int,
block_size_m: int,
max_loras: int,
max_num_tokens_padded: int,
max_num_m_blocks: int,
adapter_enabled: torch.Tensor,
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Construct LoRA token-expert alignment on CPU for small batches.
When the number of tokens is very small, the overhead of launching the
CUDA-based moe_lora_align_block_size kernel exceeds the actual
computation. This function builds the same data structures using simple
Python loops on CPU and transfers the result to GPU in one shot.
"""
M, top_k = topk_ids.shape
num_valid_tokens = M * top_k
sorted_token_ids = torch.full(
(max_loras * max_num_tokens_padded,),
num_valid_tokens,
dtype=torch.int32,
)
expert_ids_out = torch.full((max_loras * max_num_m_blocks,), -1, dtype=torch.int32)
num_tokens_post_padded = torch.zeros(max_loras, dtype=torch.int32)
seg_indptr_list = seg_indptr.cpu().tolist()
req_to_lora_list = req_to_lora.cpu().tolist()
topk_ids_list = topk_ids.cpu().tolist()
adapter_enabled_list = adapter_enabled.cpu().tolist()
for lora_id in range(max_loras):
if not adapter_enabled_list[lora_id]:
continue
pairs: list[tuple[int, int]] = []
for seg_idx in range(len(seg_indptr_list) - 1):
if req_to_lora_list[seg_idx] != lora_id:
continue
start = seg_indptr_list[seg_idx]
end = seg_indptr_list[seg_idx + 1]
for m in range(start, end):
for k in range(top_k):
pairs.append((topk_ids_list[m][k], m * top_k + k))
if not pairs:
continue
pairs.sort()
base_t = lora_id * max_num_tokens_padded
base_e = lora_id * max_num_m_blocks
pos = 0
block_idx = 0
i = 0
while i < len(pairs):
cur_expert = pairs[i][0]
group_start = pos
while i < len(pairs) and pairs[i][0] == cur_expert:
sorted_token_ids[base_t + pos] = pairs[i][1]
pos += 1
i += 1
group_len = pos - group_start
padded_len = ((group_len + block_size_m - 1) // block_size_m) * block_size_m
num_blocks = padded_len // block_size_m
for b in range(num_blocks):
expert_ids_out[base_e + block_idx + b] = cur_expert
block_idx += num_blocks
pos = group_start + padded_len
num_tokens_post_padded[lora_id] = pos
return (
sorted_token_ids.to(device),
expert_ids_out.to(device),
num_tokens_post_padded.to(device),
)
@dataclass
class LoRAInfo:
"""LoRA weights and dispatch info for MoE computation."""
# LoRA weights: [num_loras, num_experts_or_1, dim1, dim2]
# When experts_shared_outer_loras=True:
# gate_up_lora_a: [num_loras, 1, max_rank, hidden_dim] (shared)
# down_lora_b: [num_loras, 1, hidden_dim, max_rank] (shared)
gate_up_lora_a_weights: (
torch.Tensor
) # [num_loras, num_experts_or_1, 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_or_1, 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
token_lora_mapping: torch.Tensor # [num_tokens] - adapter used by each token
max_lora_rank: int # Maximum LoRA rank across all adapters
num_experts: int
has_active_lora: bool = True
experts_shared_outer_loras: bool = False
cg_buffers: dict | None = None
fully_sharded: bool = False
tp_size: int = 1
tp_rank: int = 0
hidden_size: int = 0
lora_use_virtual_experts: bool = False
@dataclass
class LoRAHooks:
"""Hook callbacks for injecting LoRA deltas into the MoE pipeline."""
after_gate_up: (
Callable[[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor], None] | None
) = None
after_down: (
Callable[[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor], None] | None
) = None
def _compute_lora_alignment(
topk_ids: torch.Tensor,
lora_info: LoRAInfo,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Compute LoRA alignment tensors for the non-virtual-expert (classic) path.
Returns: (sorted_token_ids_reshaped, expert_ids_reshaped, num_tokens_post_padded_lora, lora_ids)
"""
cg = lora_info.cg_buffers if get_is_capture_mode() else None
shrink_config = {"BLOCK_SIZE_M": 64}
M = topk_ids.shape[0]
block_size_m = shrink_config["BLOCK_SIZE_M"]
max_loras = len(lora_info.lora_ranks)
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
device = topk_ids.device
use_naive = (
cg is None
and M * topk_ids.shape[1] * _SPARSITY_FACTOR
<= lora_info.num_experts * max_loras
)
if use_naive:
sorted_token_ids_lora, expert_ids_lora, num_tokens_post_padded_lora = (
_naive_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),
lora_info.adapter_enabled,
device,
)
)
lora_ids = torch.arange(max_loras, dtype=torch.int32, device=device)
else:
if cg is not None:
sorted_token_ids_lora = cg["sorted_token_ids_lora"][
: max_loras * max_num_tokens_padded
]
expert_ids_lora = cg["expert_ids_lora"][: max_loras * max_num_m_blocks]
num_tokens_post_padded_lora = cg["num_tokens_post_padded_lora"][:max_loras]
else:
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
)
if cg is not None and "lora_ids" in cg:
lora_ids = cg["lora_ids"][:max_loras]
else:
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,
cumsum_buffer=cg.get("cumsum_buffer") if cg is not None else None,
token_mask=cg.get("token_mask") if cg is not None else None,
)
return (
sorted_token_ids_lora.view(max_loras, -1),
expert_ids_lora.view(max_loras, -1),
num_tokens_post_padded_lora,
lora_ids,
)
def _add_lora_gate_up_delta(
hidden_states: torch.Tensor,
intermediate_cache: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
lora_info: LoRAInfo,
token_lora_mapping: torch.Tensor | None,
sorted_token_ids_reshaped: torch.Tensor | None,
expert_ids_reshaped: torch.Tensor | None,
num_tokens_post_padded_lora: torch.Tensor | None,
lora_ids: torch.Tensor | None,
routing_cache: dict | None = None,
) -> None:
"""Add LoRA gate_up delta to intermediate_cache in-place."""
from sglang.kernels.ops.moe.fused_moe_lora_kernel import fused_moe_lora
from sglang.kernels.ops.moe.virtual_experts import merged_experts_fused_moe_lora_add
if lora_info is None or lora_info.max_lora_rank == 0:
return
if not get_is_capture_mode() and not lora_info.has_active_lora:
return
M, top_k, gate_up_dim = intermediate_cache.shape
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
if lora_info.experts_shared_outer_loras and not lora_info.lora_use_virtual_experts:
gate_up_a = gate_up_a.expand(-1, lora_info.num_experts, -1, -1)
# Detect gated vs non-gated from A buffer rank dimension.
# Gated: A has 2*r rows (gate + up). Non-gated: A has 1*r rows (w1 only).
is_gated = gate_up_a.shape[2] > r
if is_gated:
inter_size = gate_up_b.shape[2] // 2
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:, :],
]
else:
lora_a_stacked = [gate_up_a]
lora_b_stacked = [gate_up_b]
if lora_info.lora_use_virtual_experts:
merged_experts_fused_moe_lora_add(
output=intermediate_cache,
hidden_states=hidden_states,
lora_a=gate_up_a,
lora_b=gate_up_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=routing_cache,
)
else:
blk = _get_moe_lora_block_config(r)
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,
shrink_block_size_m=64,
shrink_block_size_n=blk["shrink_block_size_n"],
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=blk["expand_block_size_k"],
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(
intermediate_input: torch.Tensor,
intermediate_cache: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
lora_info: LoRAInfo,
token_lora_mapping: torch.Tensor | None,
sorted_token_ids_reshaped: torch.Tensor | None,
expert_ids_reshaped: torch.Tensor | None,
num_tokens_post_padded_lora: torch.Tensor | None,
lora_ids: torch.Tensor | None,
routing_cache: dict | None = None,
) -> None:
"""Add LoRA down delta to intermediate_cache in-place."""
from sglang.kernels.ops.moe.fused_moe_lora_kernel import fused_moe_lora
from sglang.kernels.ops.moe.virtual_experts import merged_experts_fused_moe_lora_add
if lora_info.max_lora_rank == 0:
return
M, top_k, hidden_dim = intermediate_cache.shape
down_lora_a = lora_info.down_lora_a_weights
down_lora_b = lora_info.down_lora_b_weights
if lora_info.experts_shared_outer_loras and not lora_info.lora_use_virtual_experts:
down_lora_b = down_lora_b.expand(-1, lora_info.num_experts, -1, -1)
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
if lora_info.lora_use_virtual_experts:
merged_experts_fused_moe_lora_add(
output=intermediate_cache,
hidden_states=intermediate_input,
lora_a=down_lora_a,
lora_b=down_lora_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=lora_info.experts_shared_outer_loras,
routing_cache=routing_cache,
)
else:
blk = _get_moe_lora_block_config(lora_info.max_lora_rank)
fused_moe_lora(
output=intermediate_cache,
qcurr_hidden_states=intermediate_input,
lora_a_stacked=[down_lora_a],
lora_b_stacked=[down_lora_b],
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,
shrink_block_size_m=64,
shrink_block_size_n=blk["shrink_block_size_n"],
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=blk["expand_block_size_k"],
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,
)
def build_lora_hooks(
hidden_states: torch.Tensor,
lora_info: LoRAInfo,
topk_ids: torch.Tensor,
) -> LoRAHooks:
"""Build LoRA hook closures for injection into any MoE runner.
Computes token_lora_mapping and alignment tensors once, then returns
closures that capture them for the two injection points.
"""
if lora_info is None or lora_info.max_lora_rank == 0:
return LoRAHooks()
# Skip alignment/mapping work entirely when the batch has no active adapter.
# During CUDA graph capture we still need to record the kernels into the
# graph (adapter_enabled is all-zero, kernels early-exit on GPU).
if not get_is_capture_mode() and not lora_info.has_active_lora:
return LoRAHooks()
# Compute alignment / mapping (once, shared by both hooks)
token_lora_mapping: torch.Tensor | None = None
sorted_token_ids_reshaped: torch.Tensor | None = None
expert_ids_reshaped: torch.Tensor | None = None
num_tokens_post_padded_lora: torch.Tensor | None = None
lora_ids: torch.Tensor | None = None
if lora_info.lora_use_virtual_experts:
token_lora_mapping = lora_info.token_lora_mapping
else:
(
sorted_token_ids_reshaped,
expert_ids_reshaped,
num_tokens_post_padded_lora,
lora_ids,
) = _compute_lora_alignment(topk_ids, lora_info)
# Shared routing cache: gate_up and down reuse routing for same (num_experts, shared_outer, block_size)
routing_cache: dict = {}
def after_gate_up(
hidden_states: torch.Tensor,
intermediate_cache1: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
) -> None:
_add_lora_gate_up_delta(
hidden_states,
intermediate_cache1,
topk_weights,
topk_ids,
lora_info,
token_lora_mapping,
sorted_token_ids_reshaped,
expert_ids_reshaped,
num_tokens_post_padded_lora,
lora_ids,
routing_cache=routing_cache,
)
def after_down(
intermediate_input: torch.Tensor,
intermediate_cache3: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
) -> None:
_add_lora_down_delta(
intermediate_input,
intermediate_cache3,
topk_weights,
topk_ids,
lora_info,
token_lora_mapping,
sorted_token_ids_reshaped,
expert_ids_reshaped,
num_tokens_post_padded_lora,
lora_ids,
routing_cache=routing_cache,
)
return LoRAHooks(after_gate_up=after_gate_up, after_down=after_down)
@@ -0,0 +1,93 @@
import logging
from enum import Enum, auto
from typing import Dict, Optional
import torch
from torch.cuda import Event as CudaEvent
from torch.cuda import Stream as CudaStream
from torch.cuda import StreamContext as CudaStreamContext
from sglang.srt.lora.lora_manager import LoRAManager
logger = logging.getLogger(__name__)
class LoRAOverlapLoadStatus(Enum):
LOADED = auto()
LOADING = auto()
NOT_LOADED = auto()
class LoRAOverlapLoader:
def __init__(self, lora_manager):
self.lora_manager: LoRAManager = lora_manager
self.device_module = torch.get_device_module(self.lora_manager.device)
self.load_stream: CudaStream = self.device_module.Stream()
self.load_stream_context: CudaStreamContext = self.device_module.stream(
self.load_stream
)
self.lora_to_overlap_load_event: Dict[Optional[str], CudaEvent] = {}
def try_overlap_load_lora(
self, lora_id: Optional[str], running_loras: set[Optional[str]]
) -> bool:
"""
Check a LoRA adapter's asynchronous load status, and try to load it if there's capacity
in the memory pool. Returns whether or not the adapter has been loaded.
"""
# Drain completed async loads before status/capacity checks so finished
# adapters no longer count as in-flight.
self._drain_completed_overlap_loads()
lora_pipeline_load_status = self._check_overlap_load_status(lora_id)
if lora_pipeline_load_status == LoRAOverlapLoadStatus.LOADING:
return False
elif lora_pipeline_load_status == LoRAOverlapLoadStatus.NOT_LOADED:
res = self._try_start_overlap_load(lora_id, running_loras)
if res:
logger.debug(f"Loading LoRA adapter {lora_id} asynchronously")
return False
else:
assert lora_pipeline_load_status == LoRAOverlapLoadStatus.LOADED
return True
def _check_overlap_load_status(
self, lora_id: Optional[str]
) -> LoRAOverlapLoadStatus:
if lora_id in self.lora_to_overlap_load_event:
return LoRAOverlapLoadStatus.LOADING
# After completed events have been drained, a memory-pool entry with no
# pending event is safe to use on the current stream.
if lora_id in self.lora_manager.memory_pool.uid_to_buffer_id:
return LoRAOverlapLoadStatus.LOADED
return LoRAOverlapLoadStatus.NOT_LOADED
def _drain_completed_overlap_loads(self) -> None:
completed_loads = [
(lora_id, event)
for lora_id, event in self.lora_to_overlap_load_event.items()
if event.query()
]
for lora_id, event in completed_loads:
torch.cuda.current_stream().wait_event(event)
del self.lora_to_overlap_load_event[lora_id]
def _try_start_overlap_load(
self, lora_id: Optional[str], running_loras: set[Optional[str]]
) -> bool:
loras_to_be_loaded = running_loras | self.lora_to_overlap_load_event.keys()
new_lora_set = {lora_id} | loras_to_be_loaded
if not self.lora_manager.validate_lora_batch(new_lora_set):
return False
with self.load_stream_context:
self.lora_manager.fetch_new_loras({lora_id}, loras_to_be_loaded)
event = self.device_module.Event()
event.record(self.load_stream)
self.lora_to_overlap_load_event[lora_id] = event
return True
+263
View File
@@ -0,0 +1,263 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
import asyncio
from collections import OrderedDict
from typing import Dict, List, Optional, Union
from uuid import NAMESPACE_URL, uuid4, uuid5
import msgspec
from msgspec.structs import fields
from sglang.srt.utils import ConcurrentCounter
from sglang.srt.utils.aio_rwlock import RWLock
class LoRARef(msgspec.Struct, frozen=True, array_like=True):
"""
Reference record for a LoRA model.
This object guarantees a unique ``lora_id`` and may include ``lora_name``, ``lora_path``, and ``pinned``.
The ID eliminates conflicts from reused LoRA names or paths and can be used to generate deterministic cache
keys (e.g., radix cache).
"""
lora_id: str = msgspec.field(default_factory=lambda: uuid4().hex)
lora_name: Optional[str] = None
lora_path: Optional[str] = None
pinned: Optional[bool] = None
def __post_init__(self):
if self.lora_id is None:
raise ValueError("lora_id cannot be None")
@staticmethod
def deterministic_id(lora_name: str, lora_path: str) -> str:
"""Stable ``lora_id`` for ``--lora-paths`` adapters.
Each node in a multi-node launch parses ``--lora-paths`` independently;
``uuid4`` would mint a different id per node for the same adapter,
breaking cross-node lookups when the master broadcasts a request id.
"""
return uuid5(NAMESPACE_URL, f"{lora_name}\0{lora_path}").hex
def __str__(self) -> str:
parts = [
f"{f.name}={value}"
for f in fields(self)
if (value := getattr(self, f.name)) is not None
]
return f"{self.__class__.__name__}({', '.join(parts)})"
class LoRARegistry:
"""
The central registry to keep track of available LoRA adapters and ongoing LoRA requests.
The `LoRARegistry` resides in the tokenizer manager process and acts as the single source of truth for all
available LoRA adapters. It supports concurrent inference and dynamic adapter updates through a two-phase
update / eventual consistency model between the tokenizer manager process and the scheduler processes.
"""
def __init__(self, lora_paths: Optional[List[LoRARef]] = None):
assert lora_paths is None or all(
isinstance(lora, LoRARef) for lora in lora_paths
), (
"server_args.lora_paths should have been normalized to LoRARef objects during server initialization. "
"Please file an issue if you see this error."
)
# A read-write lock to ensure adapters loading / unloading operations are exclusive.
# Please note that the counter increment/decrement operations are not synchronized through this
# lock, as they are designed to be non-blocking and can be performed concurrently.
self._registry_lock = RWLock()
# An ordered dictionary to hold LoRARef objects, mapping from LoRA name to LoRARef.
# The LoRARefs are stored in LRU order, such that LoRA adapters that have been
# most recently used are stored at the end. Note that lookups count for accesses.
# Ties are broken arbitrarily.
self._registry: OrderedDict[str, LoRARef] = OrderedDict()
# Counters for ongoing requests, mapping from LoRA ID to ConcurrentCounter.
self._counters: Dict[str, ConcurrentCounter] = {}
# Initialize the registry with provided LoRA paths, if present.
if lora_paths:
for lora_ref in lora_paths:
self._register_adapter(lora_ref)
async def register(self, lora_ref: LoRARef):
"""
Register a new LoRARef object in the registry.
Args:
lora_ref (LoRARef): The LoRARef object to register.
"""
async with self._registry_lock.writer_lock:
self._register_adapter(lora_ref)
async def unregister(self, lora_name: str) -> str:
"""
Unregister a LoRARef object from the registry and returns the removed LoRA ID.
Args:
lora_name (str): The name of the LoRA model to unregister.
"""
async with self._registry_lock.writer_lock:
lora_ref = self._registry.get(lora_name, None)
if lora_ref is None:
raise ValueError(
f"LoRA with name {lora_name} does not exist. Loaded LoRAs: {self._registry.keys()}"
)
del self._registry[lora_name]
return lora_ref.lora_id
async def acquire(self, lora_name: Union[str, List[str]]) -> Union[str, List[str]]:
"""
Queries registry for LoRA IDs based on LoRA names and start tracking the usage of the corresponding LoRA adapters
by incrementing its counter.
"""
def _lookup(name: str) -> str:
if name is None:
return None
lora_ref = self._registry.get(name, None)
if lora_ref is None:
raise ValueError(
f"The following requested LoRA adapters are not loaded: {name}\n"
f"Loaded adapters: {self._registry.keys()}."
)
self._registry.move_to_end(name)
return lora_ref.lora_id
if isinstance(lora_name, str):
async with self._registry_lock.writer_lock:
lora_id = _lookup(lora_name)
await self._counters[lora_id].increment(notify_all=False)
return lora_id
elif isinstance(lora_name, list):
async with self._registry_lock.writer_lock:
lora_ids = [_lookup(name) for name in lora_name]
# Increment the counters only after all IDs are looked up.
await asyncio.gather(
*[
self._counters[id].increment(notify_all=False)
for id in lora_ids
if id is not None
]
)
return lora_ids
else:
raise TypeError("lora_name must be either a string or a list of strings.")
async def release(self, lora_id: Union[str, List[str]]):
"""
Decrements the usage counter for a LoRA adapter, indicating that it is no longer in use.
"""
async with self._registry_lock.reader_lock:
if isinstance(lora_id, str):
await self._counters[lora_id].decrement()
elif isinstance(lora_id, list):
await asyncio.gather(
*[
self._counters[id].decrement()
for id in lora_id
if id is not None
]
)
else:
raise TypeError("lora_id must be either a string or a list of strings.")
async def wait_for_unload(self, lora_id: str):
"""
Waits until the usage counter for a LoRA adapter reaches zero, indicating that it is no longer in use.
This is useful for ensuring that a LoRA adapter can be safely unloaded.
This method itself is not synchronized, which is safe because it should only be called during LoRA unloading,
which itself is guaranteed to be sequential.
"""
assert (
lora_id not in self._registry
), "wait_for_unload should only be called after the LoRA adapter has been unregistered. "
assert (
lora_id in self._counters
), "The LoRA ID should still have a counter if it has been registered before."
# Wait until no requests are using this LoRA adapter.
await self._counters[lora_id].wait_for_zero()
del self._counters[lora_id]
async def get_unregistered_loras(self, lora_name: set[str]):
"""
Returns all LoRA adapters in lora_name that are not found in self._registry.
"""
async with self._registry_lock.writer_lock:
unregistered_loras = []
for name in lora_name:
if name in self._registry:
# This counts as a lookup, so we want to update the cache
self._registry.move_to_end(name)
else:
unregistered_loras.append(name)
return unregistered_loras
async def lru_lora_name(self, exclude_pinned=False):
"""
Returns the least recently used LoRA adapter.
If exclude_pinned is True, then return the LRU LoRA adapter that isn't pinned.
"""
async with self._registry_lock.reader_lock:
if not exclude_pinned:
return next(iter(self._registry), None)
for lora_name, lora_ref in self._registry.items():
if not lora_ref.pinned:
return lora_name
else:
return None
def _register_adapter(self, lora_ref: LoRARef):
"""
Internal helper method to register a LoRA adapter.
"""
if lora_ref.lora_name in self._registry:
raise ValueError(
f"LoRA with name {lora_ref.lora_name} already exists. Loaded LoRAs: {self._registry.keys()}"
)
self._registry[lora_ref.lora_name] = lora_ref
self._counters[lora_ref.lora_id] = ConcurrentCounter()
return lora_ref
@property
def num_registered_loras(self) -> int:
"""
Returns the total number of LoRA adapters currently registered.
"""
return len(self._registry)
def get_all_adapters(self) -> Dict[str, LoRARef]:
"""
Returns a dictionary of all registered LoRA adapters.
Returns:
Dict[str, LoRARef]: A dictionary mapping LoRA names to LoRARef objects.
"""
return dict(self._registry)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
from typing import Optional
import torch
from sglang.srt.lora.utils import LoRABatchInfo
from .graph_lora_ops import (
sgemm_lora_a_embedding_graph_fwd,
sgemm_lora_a_graph_fwd,
sgemm_lora_b_graph_fwd,
)
from .lora_ops import sgemm_lora_a_embedding_fwd as sgemm_lora_a_embedding_control_fwd
from .lora_ops import sgemm_lora_a_fwd as sgemm_lora_a_control_fwd
from .lora_ops import sgemm_lora_b_fwd as sgemm_lora_b_control_fwd
def sgemm_lora_a_embedding_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
batch_info: LoRABatchInfo,
vocab_size: int,
) -> torch.Tensor:
output: torch.Tensor
if batch_info.use_cuda_graph:
output = sgemm_lora_a_embedding_graph_fwd(
inputs,
weights,
batch_info.weight_indices,
batch_info.seg_lens,
batch_info.scalings,
vocab_size,
)
else:
output = sgemm_lora_a_embedding_control_fwd(
inputs,
weights,
batch_info.weight_indices_cpu,
batch_info.seg_lens_cpu,
batch_info.lora_ranks_cpu,
batch_info.scalings_cpu,
vocab_size,
)
return output
def sgemm_lora_a_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
batch_info: LoRABatchInfo,
num_slices: int = 1,
) -> torch.Tensor:
output: torch.Tensor
if batch_info.use_cuda_graph:
output = sgemm_lora_a_graph_fwd(
inputs,
weights,
batch_info.weight_indices,
batch_info.seg_lens,
batch_info.scalings,
num_slices,
)
else:
output = sgemm_lora_a_control_fwd(
inputs,
weights,
batch_info.weight_indices_cpu,
batch_info.seg_lens_cpu,
batch_info.lora_ranks_cpu,
batch_info.scalings_cpu,
num_slices,
)
return output
def sgemm_lora_b_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
batch_info: LoRABatchInfo,
slice_offsets: torch.Tensor,
base_output: Optional[torch.Tensor] = None,
) -> torch.Tensor:
output: torch.Tensor
if batch_info.use_cuda_graph:
output = sgemm_lora_b_graph_fwd(
inputs,
weights,
batch_info.weight_indices,
batch_info.seg_lens,
slice_offsets,
base_output,
)
else:
output = sgemm_lora_b_control_fwd(
inputs,
weights,
batch_info.weight_indices_cpu,
batch_info.seg_lens_cpu,
batch_info.lora_ranks_cpu,
slice_offsets,
base_output,
)
return output
__all__ = [
"sgemm_lora_a_embedding_fwd",
"sgemm_lora_a_fwd",
"sgemm_lora_b_fwd",
]
@@ -0,0 +1,120 @@
from typing import Optional
import torch
import torch.nn.functional as F
def sgemm_lora_a_embedding_graph_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
weight_indices: torch.Tensor,
seg_len_tensor: torch.Tensor,
scaling_tensor: torch.Tensor,
vocab_size: int,
) -> torch.Tensor:
total_seq_len = inputs.shape[0]
if weights.numel() == 0:
return torch.zeros(total_seq_len, 0, dtype=weights.dtype, device=weights.device)
num_loras, max_rank, _ = weights.shape
output = torch.zeros(
total_seq_len, max_rank, dtype=weights.dtype, device=weights.device
)
for lora_idx in range(num_loras):
batch_token_mask = weight_indices[:total_seq_len] == lora_idx
x_seq = torch.where(batch_token_mask, inputs, 0)
w_seq = weights[lora_idx]
output.add_(
scaling_tensor[lora_idx]
* torch.where(
batch_token_mask.unsqueeze(1), F.embedding(x_seq, w_seq.t()), 0
)
)
return output
def sgemm_lora_a_graph_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
weight_indices: torch.Tensor,
seg_len_tensor: torch.Tensor,
scaling_tensor: torch.Tensor,
num_slices: int = 1,
) -> torch.Tensor:
total_seq_len, input_dim = inputs.shape
if weights.numel() == 0:
return torch.zeros(total_seq_len, 0, dtype=inputs.dtype, device=inputs.device)
num_loras, weight_out_dim, _ = weights.shape
max_rank = weight_out_dim // num_slices
output = torch.zeros(
total_seq_len, num_slices * max_rank, dtype=inputs.dtype, device=inputs.device
)
for lora_idx in range(num_loras):
batch_token_mask = (weight_indices[:total_seq_len] == lora_idx).unsqueeze(1)
x_seq = torch.where(batch_token_mask, inputs, 0)
w_seq = weights[lora_idx]
output.add_(scaling_tensor[lora_idx] * torch.mm(x_seq, w_seq.t()))
return output
def sgemm_lora_b_graph_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
weight_indices: torch.Tensor,
seg_len_tensor: torch.Tensor,
slice_offsets: torch.Tensor,
base_output: Optional[torch.Tensor] = None,
) -> torch.Tensor:
total_seq_len, input_dim = inputs.shape
num_loras, weight_out_dim, _ = weights.shape
total_output_dim = slice_offsets[-1].item() if len(slice_offsets) > 0 else 0
if weights.numel() == 0:
return torch.zeros(
total_seq_len, total_output_dim, dtype=inputs.dtype, device=inputs.device
)
num_slices = len(slice_offsets) - 1
max_rank = input_dim // num_slices
if base_output is not None:
output = base_output
else:
output = torch.zeros(
total_seq_len, total_output_dim, dtype=inputs.dtype, device=inputs.device
)
for lora_idx in range(num_loras):
batch_token_mask = (weight_indices[:total_seq_len] == lora_idx).unsqueeze(1)
inputs_masked = torch.where(batch_token_mask, inputs, 0)
for slice_idx in range(num_slices):
slice_start_input = slice_idx * max_rank
slice_end_input = (slice_idx + 1) * max_rank
slice_start_output = slice_offsets[slice_idx]
slice_end_output = slice_offsets[slice_idx + 1]
x_slice = inputs_masked[..., slice_start_input:slice_end_input]
w_slice = weights[
lora_idx, slice_start_output:slice_end_output
] # (slice_dim, max_rank)
output[..., slice_start_output:slice_end_output].add_(
torch.mm(x_slice, w_slice.t())
)
return output
@@ -0,0 +1,146 @@
from typing import Optional
import torch
def sgemm_lora_a_embedding_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
weight_indices: torch.Tensor,
seg_len_tensor: torch.Tensor,
lora_ranks: torch.Tensor,
scaling_tensor: torch.Tensor,
vocab_size: int,
) -> torch.Tensor:
total_seq_len = inputs.shape[0]
if weights.numel() == 0:
return torch.zeros(total_seq_len, 0, dtype=weights.dtype, device=weights.device)
num_loras, max_rank, _ = weights.shape
output = torch.zeros(
total_seq_len, max_rank, dtype=weights.dtype, device=weights.device
)
token_offset = 0
for lora_idx, seq_len in zip(weight_indices, seg_len_tensor):
if seq_len == 0:
continue
rank = lora_ranks[lora_idx]
if rank > 0:
x_seq = inputs[token_offset : token_offset + seq_len]
w_seq = weights[lora_idx, :rank]
result = torch.nn.functional.embedding(x_seq, w_seq.T)
output[token_offset : token_offset + seq_len, :rank] = (
scaling_tensor[lora_idx].item() * result
)
token_offset += seq_len
return output
def sgemm_lora_a_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
weight_indices: torch.Tensor,
seg_len_tensor: torch.Tensor,
lora_ranks: torch.Tensor,
scaling_tensor: torch.Tensor,
num_slices: int = 1,
) -> torch.Tensor:
total_seq_len, input_dim = inputs.shape
if weights.numel() == 0:
return torch.zeros(total_seq_len, 0, dtype=inputs.dtype, device=inputs.device)
num_loras, weight_out_dim, _ = weights.shape
max_rank = weight_out_dim // num_slices
output = torch.zeros(
total_seq_len, num_slices * max_rank, dtype=inputs.dtype, device=inputs.device
)
token_offset = 0
for lora_idx, seq_len in zip(weight_indices, seg_len_tensor):
if seq_len == 0:
continue
rank = lora_ranks[lora_idx]
if rank > 0:
x_seq = inputs[token_offset : token_offset + seq_len]
w_seq = weights[lora_idx, : num_slices * rank]
output[token_offset : token_offset + seq_len, : num_slices * rank].addmm_(
x_seq,
w_seq.T,
beta=0,
alpha=scaling_tensor[lora_idx].item(),
)
token_offset += seq_len
return output
def sgemm_lora_b_fwd(
inputs: torch.Tensor,
weights: torch.Tensor,
weight_indices: torch.Tensor,
seg_len_tensor: torch.Tensor,
lora_ranks: torch.Tensor,
slice_offsets: torch.Tensor,
base_output: Optional[torch.Tensor] = None,
) -> torch.Tensor:
total_seq_len, _ = inputs.shape
num_loras, weight_out_dim, _ = weights.shape
total_output_dim = slice_offsets[-1].item() if len(slice_offsets) > 0 else 0
if weights.numel() == 0:
return torch.zeros(
total_seq_len, total_output_dim, dtype=inputs.dtype, device=inputs.device
)
num_slices = len(slice_offsets) - 1
if base_output is not None:
output = base_output
else:
output = torch.zeros(
total_seq_len, total_output_dim, dtype=inputs.dtype, device=inputs.device
)
token_offset = 0
for lora_idx, seq_len in zip(weight_indices, seg_len_tensor):
if seq_len == 0:
continue
rank = lora_ranks[lora_idx]
if rank > 0:
for slice_idx in range(num_slices):
slice_start_input = slice_idx * rank
slice_end_input = (slice_idx + 1) * rank
slice_start_output = slice_offsets[slice_idx]
slice_end_output = slice_offsets[slice_idx + 1]
x_slice = inputs[
token_offset : token_offset + seq_len,
slice_start_input:slice_end_input,
] # (seq_len, rank)
w_slice = weights[
lora_idx, slice_start_output:slice_end_output, :rank
] # (slice_dim, rank)
output[
token_offset : token_offset + seq_len,
slice_start_output:slice_end_output,
].addmm_(x_slice, w_slice.T)
token_offset += seq_len
return output
@@ -0,0 +1,224 @@
"""Two-stream LoRA overlap (O1 + O7 + O8 + O9) — installed as a monkey-patch.
Activates when env ``SGLANG_LORA_TWO_STREAM=1``. Triggered exactly once via
:func:`install_two_stream_overrides` (called at end of ``sglang/srt/lora/layers.py``).
When enabled, these call sites are redirected to side-stream-overlapped versions
defined entirely in this package:
* ``QKVParallelLinearWithLoRA.forward`` → :mod:`.attention.qkv_proj_lora_forward`
* ``RowParallelLinearWithLoRA.forward`` → :mod:`.attention.row_parallel_lora_forward`
* ``MergedColumnParallelLinearWithLoRA.forward`` → :mod:`.merged_column.merged_column_lora_forward`
* ``fused_experts_none_to_experimental_sgl_trtllm_fp8_lora`` →
:mod:`.moe_overlap.fused_experts_none_to_experimental_sgl_trtllm_fp8_lora_two_stream`
When disabled (env unset), ``install_two_stream_overrides`` is a no-op and all
the original functions / methods in ``sglang/srt/lora/layers.py`` and
``sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py`` run unchanged.
Per-batch gating still happens inside the patched callables — they fall back
to the saved-original implementation for non-decode batches (token count above
``SGLANG_TWO_STREAM_MAX_TOKENS`` default 256), so prefill stays on the serial
path even with the patch installed.
"""
from typing import Callable, Optional
import torch
from sglang.srt.environ import envs
from sglang.srt.lora.trtllm_lora_temp.environ import lora_envs
def is_two_stream_active(x: torch.Tensor) -> bool:
"""Per-batch gate (two-stream now always-on). True iff batch is decode-shaped (<= SGLANG_TWO_STREAM_MAX_TOKENS)."""
return x.shape[0] <= lora_envs.SGLANG_TWO_STREAM_MAX_TOKENS.get()
def get_lora_side_stream() -> torch.cuda.Stream:
"""Lazily allocate a single shared LoRA side stream.
Within one decode layer the three sites (qkv → attn → o_proj → moe_gate_up)
run sequentially, so one stream suffices and avoids extra graph-capture
nodes from per-site streams.
"""
from sglang.srt.runtime_context import get_stream
return get_stream("lora_side")
def init_lora_two_stream_resources(device: Optional[torch.device] = None) -> None:
"""Eagerly create the side stream before cuda-graph capture begins.
``torch.cuda.Stream()`` is a driver call that must not run inside a
cuda-graph capture region. Since :func:`get_lora_side_stream` is otherwise
lazy, the first eligible decode forward would create it — which can fall
inside capture if warmup didn't happen to exercise a two-stream batch.
Calling this from a pre-capture hook pins creation to init/warmup on the
correct device.
"""
if device is not None:
with torch.cuda.device(device):
get_lora_side_stream()
else:
get_lora_side_stream()
def lora_overlap_alloc_stream() -> Optional[torch.cuda.Stream]:
"""Stream to allocate side-stream LoRA-shrink OUTPUT buffers on, or None for default behavior.
A buffer allocated *inside* ``with torch.cuda.stream(side)`` is tagged to the side stream, so the
caching allocator may free/reuse it on the side stream's schedule — before the MAIN stream (the real
consumer, via the LoRA-B expand) is done. Under cuda-graph replay that's a premature-reuse WAR ->
qwen3.5 mamba decode garbage. With ``SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC`` this returns the MAIN
stream so the op allocates the output on the consumer stream (like the MoE O1 ``gate_up_delta``),
making a single shared side stream graph-safe. Call on the MAIN stream BEFORE forking to the side.
"""
if lora_envs.SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC.get():
return torch.cuda.current_stream()
return None
# References to the original implementations, captured at install time so the
# patched callables can defer to them for non-decode batches.
_ORIGINAL_QKV_FORWARD: Optional[Callable] = None
_ORIGINAL_ROW_FORWARD: Optional[Callable] = None
_ORIGINAL_MERGED_FORWARD: Optional[Callable] = None
_ORIGINAL_COLUMN_FORWARD: Optional[Callable] = None
_ORIGINAL_REPLICATED_FORWARD: Optional[Callable] = None
_ORIGINAL_MOE_LORA_FUNC: Optional[Callable] = None
_ORIGINAL_FP4_MOE_LORA_FUNC: Optional[Callable] = None
_ORIGINAL_BF16_MOE_LORA_FUNC: Optional[Callable] = None
_INSTALLED: bool = False
def get_original_qkv_forward() -> Callable:
return _ORIGINAL_QKV_FORWARD
def get_original_row_forward() -> Callable:
return _ORIGINAL_ROW_FORWARD
def get_original_merged_column_forward() -> Callable:
return _ORIGINAL_MERGED_FORWARD
def get_original_column_forward() -> Callable:
return _ORIGINAL_COLUMN_FORWARD
def get_original_replicated_forward() -> Callable:
return _ORIGINAL_REPLICATED_FORWARD
def get_original_moe_lora_func() -> Callable:
return _ORIGINAL_MOE_LORA_FUNC
def get_original_fp4_moe_lora_func() -> Callable:
return _ORIGINAL_FP4_MOE_LORA_FUNC
def get_original_bf16_moe_lora_func() -> Callable:
return _ORIGINAL_BF16_MOE_LORA_FUNC
def install_two_stream_overrides() -> None:
"""Install the side-stream overlapped overrides if ``SGLANG_LORA_TWO_STREAM=1``.
Idempotent: subsequent calls are a no-op. Patches:
1. ``QKVParallelLinearWithLoRA.forward`` (O7 — qkv LoRA shrink overlap)
2. ``RowParallelLinearWithLoRA.forward`` (O8 — o_proj LoRA shrink overlap)
3. ``MergedColumnParallelLinearWithLoRA.forward`` (O9 — merged-column LoRA
shrink overlap: dense gate_up + mamba in_proj_qkvz)
4. ``lora_dispatch.fused_experts_none_to_experimental_sgl_trtllm_fp8_lora``
(O1 — MoE gate_up LoRA overlap), plus its fp4 (O1-fp4) and bf16
(O1-bf16) siblings
The saved originals are exposed via :func:`get_original_qkv_forward`,
:func:`get_original_row_forward`, :func:`get_original_moe_lora_func` so the
new versions can fall back when their per-batch gate says single-stream.
"""
global _INSTALLED, _ORIGINAL_QKV_FORWARD, _ORIGINAL_ROW_FORWARD, _ORIGINAL_MERGED_FORWARD, _ORIGINAL_COLUMN_FORWARD, _ORIGINAL_REPLICATED_FORWARD, _ORIGINAL_MOE_LORA_FUNC, _ORIGINAL_FP4_MOE_LORA_FUNC, _ORIGINAL_BF16_MOE_LORA_FUNC
if _INSTALLED:
return
from sglang.srt.lora.layers import (
ColumnParallelLinearWithLoRA,
MergedColumnParallelLinearWithLoRA,
QKVParallelLinearWithLoRA,
ReplicatedLinearWithLoRA,
RowParallelLinearWithLoRA,
)
from sglang.srt.lora.trtllm_lora_temp.attention import (
column_parallel_lora_forward,
qkv_proj_lora_forward,
replicated_lora_forward,
row_parallel_lora_forward,
)
from sglang.srt.lora.trtllm_lora_temp.merged_column import (
merged_column_lora_forward,
)
# Capture all originals before patching: QKV / MergedColumn subclass
# ColumnParallel, so the plain-Column O10 patch must not clobber the
# subclasses' own (3-/2-slice) forwards captured here as their fallbacks.
_ORIGINAL_QKV_FORWARD = QKVParallelLinearWithLoRA.forward
_ORIGINAL_ROW_FORWARD = RowParallelLinearWithLoRA.forward
_ORIGINAL_MERGED_FORWARD = MergedColumnParallelLinearWithLoRA.forward
_ORIGINAL_COLUMN_FORWARD = ColumnParallelLinearWithLoRA.forward
_ORIGINAL_REPLICATED_FORWARD = ReplicatedLinearWithLoRA.forward
QKVParallelLinearWithLoRA.forward = qkv_proj_lora_forward
RowParallelLinearWithLoRA.forward = row_parallel_lora_forward
MergedColumnParallelLinearWithLoRA.forward = merged_column_lora_forward
# O10 (MLA q_b_proj / kv_b_proj) + O11 (MLA fused_qkv_a_proj_with_mqa).
ColumnParallelLinearWithLoRA.forward = column_parallel_lora_forward
ReplicatedLinearWithLoRA.forward = replicated_lora_forward
import sglang.srt.lora.trtllm_lora_temp.lora_dispatch as ft
from sglang.srt.lora.trtllm_lora_temp.moe_overlap import (
fused_experts_none_to_experimental_sgl_trtllm_bf16_lora_two_stream,
fused_experts_none_to_experimental_sgl_trtllm_fp4_lora_two_stream,
fused_experts_none_to_experimental_sgl_trtllm_fp8_lora_two_stream,
)
# O1 (FP8 Qwen) + O1-fp4 (NVFP4 Kimi) + O1-bf16 (unquantized Qwen): MoE gate_up
# LoRA overlap. Each patched fn falls back to its saved single-stream original
# for non-decode batches.
_ORIGINAL_MOE_LORA_FUNC = ft.fused_experts_none_to_experimental_sgl_trtllm_fp8_lora
_ORIGINAL_FP4_MOE_LORA_FUNC = (
ft.fused_experts_none_to_experimental_sgl_trtllm_fp4_lora
)
_ORIGINAL_BF16_MOE_LORA_FUNC = (
ft.fused_experts_none_to_experimental_sgl_trtllm_bf16_lora
)
ft.fused_experts_none_to_experimental_sgl_trtllm_fp8_lora = (
fused_experts_none_to_experimental_sgl_trtllm_fp8_lora_two_stream
)
ft.fused_experts_none_to_experimental_sgl_trtllm_fp4_lora = (
fused_experts_none_to_experimental_sgl_trtllm_fp4_lora_two_stream
)
ft.fused_experts_none_to_experimental_sgl_trtllm_bf16_lora = (
fused_experts_none_to_experimental_sgl_trtllm_bf16_lora_two_stream
)
_INSTALLED = True
__all__ = [
"is_two_stream_active",
"get_lora_side_stream",
"init_lora_two_stream_resources",
"get_original_qkv_forward",
"get_original_row_forward",
"get_original_merged_column_forward",
"get_original_column_forward",
"get_original_replicated_forward",
"get_original_moe_lora_func",
"get_original_fp4_moe_lora_func",
"get_original_bf16_moe_lora_func",
"install_two_stream_overrides",
]
@@ -0,0 +1,253 @@
"""Two-stream attention LoRA forward implementations (O7 + O8).
These are monkey-patched onto :class:`QKVParallelLinearWithLoRA` and
:class:`RowParallelLinearWithLoRA` by
:func:`sglang.srt.lora.trtllm_lora_temp.install_two_stream_overrides` when
``SGLANG_LORA_TWO_STREAM=1``. The saved-original forward methods are
preserved and called for batches where two-stream isn't active.
"""
import torch
from sglang.srt.distributed import (
split_tensor_along_last_dim,
tensor_model_parallel_all_gather,
tensor_model_parallel_all_reduce,
)
from sglang.srt.layers.moe.utils import should_skip_mlp_all_reduce
from sglang.srt.lora.trtllm_lora_temp import (
get_lora_side_stream,
get_original_column_forward,
get_original_qkv_forward,
get_original_replicated_forward,
get_original_row_forward,
is_two_stream_active,
lora_overlap_alloc_stream,
)
from sglang.srt.runtime_context import get_parallel
def qkv_proj_lora_forward(self, input_: torch.Tensor):
"""O7 — side-stream LoRA-A shrink ‖ base qkv_proj GEMM.
The shrink reads ``input_`` and the LoRA-A weights — same input as the
base GEMM, no write conflict. The expand needs the shrink intermediate
AND base_output, so it runs after the rejoin on the main stream.
"""
if not self.set_lora or not is_two_stream_active(input_):
return get_original_qkv_forward()(self, input_)
from sglang.kernels.ops.gemm.trtllm_lora_temp.qkv_lora_b import qkv_lora_b_fwd
from sglang.kernels.ops.gemm.trtllm_lora_temp.sgemm_lora_a import sgemm_lora_a_fwd
bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
side_stream = get_lora_side_stream()
# sgemm_info is host-side (LoRABatchInfo); compute once, share both calls.
sgemm_info = self.lora_backend._sgemm_info()
_alloc = lora_overlap_alloc_stream() # capture MAIN stream here (before the fork)
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
shrink_intermediate = sgemm_lora_a_fwd(
input_, self.A_buffer_qkv, sgemm_info, stack_num=3, out_alloc_stream=_alloc
)
# Base qkv_proj GEMM on main, concurrent with the side-stream shrink.
output_parallel = self.base_layer.quant_method.apply(self.base_layer, input_, bias)
# Rejoin: expand reads both side-produced shrink_intermediate and base_output.
torch.cuda.current_stream().wait_stream(side_stream)
output_parallel = qkv_lora_b_fwd(
shrink_intermediate,
self.B_buffer_qkv,
sgemm_info,
self.output_offset,
self.max_qkv_out_dim,
output_parallel,
n_slices=3,
)
if self.base_layer.gather_output:
output = tensor_model_parallel_all_gather(output_parallel)
else:
output = output_parallel
output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
return output, output_bias
def row_parallel_lora_forward(
self, input_: torch.Tensor, skip_all_reduce: bool = False, forward_batch=None
):
"""O8 — side-stream LoRA-A shrink ‖ base row-parallel (o_proj) GEMM.
Mirrors O7 but the row-parallel context adds: input split per TP rank
(when not already parallel), bias on rank 0 only, optional cross-rank
all-reduce on both base output and lora_a intermediate when reducing.
Falls back to the saved-original :meth:`forward` for non-decode batches
or when LoRA isn't set on this layer.
"""
# We need ``input_parallel`` to gate the per-batch decode check (its
# token-count drives the threshold, not the unsplit ``input_``).
if self.base_layer.input_is_parallel:
input_parallel = input_
else:
tp_rank = get_parallel().tp_rank
splitted_input = split_tensor_along_last_dim(
input_, num_partitions=self.base_layer.tp_size
)
input_parallel = splitted_input[tp_rank].contiguous()
if not self.set_lora or not is_two_stream_active(input_parallel):
return get_original_row_forward()(self, input_, skip_all_reduce, forward_batch)
bias_ = (
None
if (self.base_layer.tp_rank > 0 or self.base_layer.skip_bias_add)
else self.base_layer.bias
)
side_stream = get_lora_side_stream()
_alloc = lora_overlap_alloc_stream() # capture MAIN stream here (before the fork)
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
lora_a_output = self.lora_backend.run_lora_a_sgemm(
input_parallel, self.A_buffer, out_alloc_stream=_alloc
)
# Base row-parallel GEMM on main, concurrent with the side-stream shrink.
output_parallel = self.base_layer.quant_method.apply(
self.base_layer, input_parallel, bias=bias_
)
torch.cuda.current_stream().wait_stream(side_stream)
should_reduce = (
self.base_layer.reduce_results
and self.base_layer.tp_size > 1
and not skip_all_reduce
and not should_skip_mlp_all_reduce()
)
if should_reduce:
output_ = tensor_model_parallel_all_reduce(output_parallel)
lora_a_output = tensor_model_parallel_all_reduce(lora_a_output)
output_ = self.lora_backend.run_lora_b_sgemm(
x=lora_a_output,
weights=self.B_buffer,
output_offset=self.output_offset,
output_offset_cpu=self.output_offset_cpu,
base_output=output_,
)
else:
# Two-stream already produced lora_a_output on the side stream; finish
# the LoRA with just the expand atomic-add against output_parallel.
output_parallel = self.lora_backend.run_lora_b_sgemm(
x=lora_a_output,
weights=self.B_buffer,
output_offset=self.output_offset,
output_offset_cpu=self.output_offset_cpu,
base_output=output_parallel,
)
output_ = output_parallel
output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
return output_, output_bias
def column_parallel_lora_forward(self, input_: torch.Tensor):
"""O10 — side-stream LoRA-A shrink ‖ base ColumnParallel GEMM.
Covers DeepSeek/Kimi MLA's ``q_b_proj`` / ``kv_b_proj`` (plain
:class:`ColumnParallelLinearWithLoRA` — the merged/QKV subclasses keep their
own O9/O7 overrides). The shrink reads ``input_`` (same as the base GEMM, no
write conflict); the expand needs the shrink intermediate AND base_output,
so it runs after the rejoin. Byte-identical to the saved-original forward
for non-decode batches or when LoRA isn't set on this layer.
"""
if not self.set_lora or not is_two_stream_active(input_):
return get_original_column_forward()(self, input_)
bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
side_stream = get_lora_side_stream()
_alloc = lora_overlap_alloc_stream() # capture MAIN stream here (before the fork)
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
lora_a_output = self.lora_backend.run_lora_a_sgemm(
input_, self.A_buffer, out_alloc_stream=_alloc
)
# Base ColumnParallel GEMM on main, concurrent with the side-stream shrink.
output_parallel = self.base_layer.quant_method.apply(self.base_layer, input_, bias)
# Rejoin: expand reads both the side-produced shrink and base_output.
torch.cuda.current_stream().wait_stream(side_stream)
output_parallel = self.lora_backend.run_lora_b_sgemm(
x=lora_a_output,
weights=self.B_buffer,
output_offset=self.output_offset,
output_offset_cpu=self.output_offset_cpu,
base_output=output_parallel,
)
if self.base_layer.gather_output:
output = tensor_model_parallel_all_gather(output_parallel)
else:
output = output_parallel
output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
return output, output_bias
def replicated_lora_forward(self, x: torch.Tensor):
"""O11 — side-stream LoRA-A shrink ‖ base ReplicatedLinear GEMM.
Covers DeepSeek/Kimi MLA's ``fused_qkv_a_proj_with_mqa``
(:class:`ReplicatedLinearWithLoRA`, no TP sharding). Handles both the
single-projection (``first_output_dim == 0``) and the fused q_a+kv_a
(``> 0``, ``n_slices=2``) cases, splitting the same A-shrink / B-expand the
backend's ``run_qkv_lora`` composes internally — A on the side stream, B on
the main after the rejoin. Falls back to the saved-original otherwise.
"""
if not self.set_lora or not is_two_stream_active(x):
return get_original_replicated_forward()(self, x)
bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
side_stream = get_lora_side_stream()
first_dim = self.first_output_dim
_alloc = lora_overlap_alloc_stream() # capture MAIN stream here (before the fork)
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
lora_a_output = self.lora_backend.run_lora_a_sgemm(
x,
self.A_buffer,
stack_num=(2 if first_dim > 0 else 1),
out_alloc_stream=_alloc,
)
# Base ReplicatedLinear GEMM on main, concurrent with the side-stream shrink.
output = self.base_layer.quant_method.apply(self.base_layer, x, bias)
torch.cuda.current_stream().wait_stream(side_stream)
if first_dim == 0:
output = self.lora_backend.run_lora_b_sgemm(
x=lora_a_output,
weights=self.B_buffer,
output_offset=self._output_offset,
base_output=output,
)
else:
from sglang.kernels.ops.gemm.trtllm_lora_temp.qkv_lora_b import qkv_lora_b_fwd
output = qkv_lora_b_fwd(
lora_a_output,
self.B_buffer,
self.lora_backend._sgemm_info(),
self._output_offset,
self._max_out_dim,
output,
n_slices=2,
)
output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
return output, output_bias
@@ -0,0 +1,238 @@
"""LoRA correction for absorbed-MLA ``kv_b_proj``.
The absorbed-MLA path in ``DeepseekV2AttentionMLA`` bypasses
``kv_b_proj.forward()`` and folds the K/V contribution into two BMMs against
the pre-computed ``w_kc`` / ``w_vc`` weights, so a standard
``ColumnParallelLinearWithLoRA`` wrapper would never see the activations and
the LoRA delta would silently be dropped. These helpers inject the missing
delta on top of the absorbed intermediates via the SGMM-style Triton kernels
in ``triton_ops/kv_b_lora_absorbed.py``.
Used from ``deepseek_common/attention_forward_methods/forward_mla.py``. Call
sites should gate the call with :func:`is_kv_b_lora_active` so non-LoRA
forwards take a single ``getattr`` and skip the helper entirely.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Tuple
import torch
# The four step kernels live in triton_ops; importing it pulls the LoRA kernel
# modules (and specialized_expand) into the process. They are only ever reached
# after _get_state returns a non-None state (a kv_b LoRA adapter is wrapped), so
# defer the import to that success path: a no-LoRA forward never imports it here.
step_a_q_fwd = step_a_v_fwd = step_b_q_fwd = step_b_v_fwd = None
def _ensure_step_kernels() -> None:
global step_a_q_fwd, step_a_v_fwd, step_b_q_fwd, step_b_v_fwd
if step_a_q_fwd is None:
from sglang.kernels.ops.gemm.trtllm_lora_temp.kv_b_lora_absorbed import (
step_a_q_fwd as _aq,
)
from sglang.kernels.ops.gemm.trtllm_lora_temp.kv_b_lora_absorbed import (
step_a_v_fwd as _av,
)
from sglang.kernels.ops.gemm.trtllm_lora_temp.kv_b_lora_absorbed import (
step_b_q_fwd as _bq,
)
from sglang.kernels.ops.gemm.trtllm_lora_temp.kv_b_lora_absorbed import (
step_b_v_fwd as _bv,
)
step_a_q_fwd, step_a_v_fwd, step_b_q_fwd, step_b_v_fwd = _aq, _av, _bq, _bv
if TYPE_CHECKING:
from sglang.srt.lora.utils import LoRABatchInfo
from sglang.srt.models.deepseek_v2 import DeepseekV2AttentionMLA
def is_kv_b_lora_active(attn_module: DeepseekV2AttentionMLA) -> bool:
"""Cheap precondition check used at call sites in the attention forward
to skip the entire LoRA-correction path when no ``kv_b_proj`` adapter is
wrapped on this module (the common case)."""
return getattr(attn_module.kv_b_proj, "set_lora", False)
def _get_state(
attn_module: DeepseekV2AttentionMLA,
) -> Optional[Tuple[torch.Tensor, torch.Tensor, LoRABatchInfo]]:
if not is_kv_b_lora_active(attn_module):
return None
if not hasattr(attn_module.kv_b_proj, "A_buffer"):
return None
lora_backend = attn_module.kv_b_proj.lora_backend
if not hasattr(lora_backend, "batch_info"):
return None
batch_info = lora_backend.batch_info
if batch_info is None:
return None
# Triton backend exposes _sgemm_info() to group decode-shape repeats of
# the same adapter; csgmv-style backends just expose batch_info directly.
sgemm_info = getattr(lora_backend, "_sgemm_info", None)
if callable(sgemm_info):
batch_info = sgemm_info()
# Non-None state ⇒ a kv_b adapter is active here; load the step kernels now
# (cached after the first active forward). No-LoRA forwards return above and
# never import triton_ops.
_ensure_step_kernels()
return attn_module.kv_b_proj.A_buffer, attn_module.kv_b_proj.B_buffer, batch_info
def apply_q_correction(
attn_module: DeepseekV2AttentionMLA,
q_nope: torch.Tensor,
q_nope_out: torch.Tensor,
) -> torch.Tensor:
"""LoRA correction for the absorbed ``q_nope @ w_kc`` path.
Computes ``q_nope_out += q_nope @ B_kc @ A * scaling`` per token, per
active LoRA slot via two SGMM-style Triton kernels. Factored along the
LoRA-A/B boundary so we never materialise ``B @ A`` (~268M FMAs per layer
per slot in the naive implementation)::
step A_q : ``(S,H,qk_nope) @ B_kc[slot, h] (qk_nope, rank) -> (S,H,rank)``
step B_q : ``(S,H,rank) @ A[slot] (rank, kv_lora_rank) -> += q_nope_out``
"""
state = _get_state(attn_module)
if state is None:
return q_nope_out
A_buf, B_buf, batch_info = state
full_K_per_head = attn_module.qk_nope_head_dim + attn_module.v_head_dim
q_lora_a = step_a_q_fwd(q_nope, B_buf, batch_info, full_K_per_head)
return step_b_q_fwd(q_lora_a, A_buf, batch_info, q_nope_out)
def apply_v_correction(
attn_module: DeepseekV2AttentionMLA,
attn_output: torch.Tensor,
attn_bmm_flat: torch.Tensor,
) -> torch.Tensor:
"""LoRA correction for the absorbed ``attn_output @ w_vc`` path.
Computes ``attn_bmm_flat += attn_output @ A.T @ B_vc.T * scaling`` per
token, per active LoRA slot. ``attn_bmm_flat`` is the flat
``(S, H*v_head_dim)`` view of the absorbed BMM result; we pass strides
matching the implicit ``(S, H, v_head_dim)`` layout to step B_v.
"""
state = _get_state(attn_module)
if state is None:
return attn_bmm_flat
A_buf, B_buf, batch_info = state
attn_lora_a = step_a_v_fwd(attn_output, A_buf, batch_info)
base_view = attn_bmm_flat.view(
-1, attn_module.num_local_heads, attn_module.v_head_dim
)
step_b_v_fwd(
attn_lora_a,
B_buf,
batch_info,
base_view,
attn_module.qk_nope_head_dim,
attn_module.v_head_dim,
)
return attn_bmm_flat
# ---------------------------------------------------------------------------
# Two-stream overlap (O12) for the absorbed kv_b correction.
#
# Each correction factors into an input-only A-step (reads q_nope / attn_output,
# independent of the absorbed bmm output) and a B-step that adds into that bmm
# output. ``*_prepare`` forks the A-step onto the shared LoRA side stream so it
# overlaps the main-stream ``q_nope @ w_kc`` / ``attn_output @ w_vc`` bmm;
# ``*_apply`` rejoins and runs the B-step.
#
# Gated by ``SGLANG_LORA_TWO_STREAM`` (decode batches only) via
# ``is_two_stream_active``. When inactive, ``*_prepare`` returns None and
# ``*_apply`` falls back to the serial ``apply_*_correction`` (or a no-op when no
# kv_b adapter is wrapped), so the deepseek call sites stay byte-identical with
# two-stream off. Same fork/join (``wait_stream``) idiom as the O7/O8 attention
# overrides — cuda-graph-capture safe.
# ---------------------------------------------------------------------------
def _kv_b_two_stream_state(attn_module, x):
from sglang.srt.lora.trtllm_lora_temp import (
get_lora_side_stream,
is_two_stream_active,
)
if not is_two_stream_active(x):
return None
state = _get_state(attn_module)
if state is None:
return None
A_buf, B_buf, batch_info = state
return A_buf, B_buf, batch_info, get_lora_side_stream()
def kv_b_lora_q_prepare(attn_module, q_nope):
"""Fork the q-correction A-step onto the side stream (``step_a_q`` reads only
``q_nope``) so it overlaps the main-stream ``q_nope @ w_kc`` bmm. Returns a
handle for :func:`kv_b_lora_q_apply`, or None when two-stream is inactive."""
st = _kv_b_two_stream_state(attn_module, q_nope)
if st is None:
return None
A_buf, B_buf, batch_info, side_stream = st
full_K_per_head = attn_module.qk_nope_head_dim + attn_module.v_head_dim
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
q_lora_a = step_a_q_fwd(q_nope, B_buf, batch_info, full_K_per_head)
return q_lora_a, A_buf, batch_info, side_stream
def kv_b_lora_q_apply(attn_module, q_nope, q_nope_out, handle):
"""Finish the q-correction: two-stream (rejoin + B-step) when ``handle`` is
set, else the serial correction, else a no-op. Single call replacing the
``if is_kv_b_lora_active: apply_q_correction`` at the call site."""
if handle is not None:
q_lora_a, A_buf, batch_info, side_stream = handle
torch.cuda.current_stream().wait_stream(side_stream)
return step_b_q_fwd(q_lora_a, A_buf, batch_info, q_nope_out)
if is_kv_b_lora_active(attn_module):
return apply_q_correction(attn_module, q_nope, q_nope_out)
return q_nope_out
def kv_b_lora_v_prepare(attn_module, attn_output):
"""Fork the v-correction A-step onto the side stream (``step_a_v`` reads only
``attn_output``) so it overlaps the main-stream ``attn_output @ w_vc`` bmm.
Returns a handle for :func:`kv_b_lora_v_apply`, or None when inactive."""
st = _kv_b_two_stream_state(attn_module, attn_output)
if st is None:
return None
A_buf, B_buf, batch_info, side_stream = st
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
attn_lora_a = step_a_v_fwd(attn_output, A_buf, batch_info)
return attn_lora_a, B_buf, batch_info, side_stream
def kv_b_lora_v_apply(attn_module, attn_output, attn_bmm_flat, handle):
"""Finish the v-correction: two-stream (rejoin + B-step) when ``handle`` is
set, else the serial correction, else a no-op."""
if handle is not None:
attn_lora_a, B_buf, batch_info, side_stream = handle
torch.cuda.current_stream().wait_stream(side_stream)
base_view = attn_bmm_flat.view(
-1, attn_module.num_local_heads, attn_module.v_head_dim
)
step_b_v_fwd(
attn_lora_a,
B_buf,
batch_info,
base_view,
attn_module.qk_nope_head_dim,
attn_module.v_head_dim,
)
return attn_bmm_flat
if is_kv_b_lora_active(attn_module):
return apply_v_correction(attn_module, attn_output, attn_bmm_flat)
return attn_bmm_flat
@@ -0,0 +1,135 @@
"""Local env registry for the experimental TRT-LLM LoRA fast path.
Every flag here is gated by the single global master switch
``SGLANG_EXPERIMENTAL_LORA_OPTI`` (defined in ``sglang.srt.environ``). When the master
switch is OFF (the default), every flag reads ``False`` (its default is
suppressed), so the no-LoRA path, other MoE backends, and the default
(non-experimental) LoRA path are byte-identical to upstream.
Keeping these flags out of the global ``Envs`` class is deliberate: the only
sglang-global addition for this feature is ``SGLANG_EXPERIMENTAL_LORA_OPTI``; all the
fine-grained opt switches live here, next to the code that consumes them.
Default policy (applies only when ``SGLANG_EXPERIMENTAL_LORA_OPTI=1``):
* **common** flags — used by BOTH the qwen3.5 (FP8) and kimi (NVFP4) configs —
default ``True`` so they need not be repeated on every launch command.
* **non-shared** flags default ``False`` and must be set explicitly in the
launch environment for the model that needs them.
C++-getenv flags (read via ``getenv`` in the JIT launcher, not in Python) are
listed at the bottom for documentation only; set them in the launch env.
"""
import os
from sglang.srt.environ import envs
def experimental_lora_enabled() -> bool:
"""Master gate. All flags below are forced off unless this is set."""
return envs.SGLANG_EXPERIMENTAL_LORA_OPTI.get()
_TRUE = {"1", "true", "yes", "on"}
class _GatedBool:
def __init__(self, name: str, default: bool):
self._name = name
self._default = default
def get(self) -> bool:
if not experimental_lora_enabled():
return False
raw = os.environ.get(self._name)
if raw is None:
return self._default
return raw.strip().lower() in _TRUE
class _GatedInt:
def __init__(self, name: str, default: int):
self._name = name
self._default = default
def get(self) -> int:
# Only consulted on the experimental path; return the default otherwise.
if not experimental_lora_enabled():
return self._default
raw = os.environ.get(self._name)
return int(raw) if raw is not None else self._default
class _LoraEnvs:
# ---- common (qwen3.5 ∩ kimi): default True when experimental is on ----
SGLANG_ENABLE_LORA_SHRINK_SPLIT_K = _GatedBool(
"SGLANG_ENABLE_LORA_SHRINK_SPLIT_K", True
)
SGLANG_OPT_LORA_FUSED_MERGED_ALIGN = _GatedBool(
"SGLANG_OPT_LORA_FUSED_MERGED_ALIGN", True
)
SGLANG_OPT_LORA_FUSED_TOPK_PACK = _GatedBool(
"SGLANG_OPT_LORA_FUSED_TOPK_PACK", True
)
SGLANG_OPT_LORA_QKV_B_STORE = _GatedBool("SGLANG_OPT_LORA_QKV_B_STORE", True)
# F1-①: prefill routing reuse — unify the A (shrink) stage's routing BLOCK_SIZE_M with
# the B stage's at prefill (>=512 tokens) so the per-layer routing_cache key matches
# across stages and the Triton align/sort runs once per layer-forward instead of once
# per stage (4x at prefill). Dtype-agnostic (the chain is shared by fp8/nvfp4/bf16).
# Decode (<512) keeps the opt1 fused merged-align path and its tuned shrink block.
SGLANG_OPT_LORA_PREFILL_ROUTING_REUSE = _GatedBool(
"SGLANG_OPT_LORA_PREFILL_ROUTING_REUSE", True
)
# ---- correctness fixes: on by default when experimental ----
# gate_up gated-split fix (up_A shrink for the up half); set =0 only to A/B bisect.
SGLANG_ENABLE_LORA_MOE_GATEUP_GATED_SPLIT = _GatedBool(
"SGLANG_ENABLE_LORA_MOE_GATEUP_GATED_SPLIT", True
)
# feed bf16 router logits straight to the JIT kimi gate (bitwise-identical).
SGLANG_OPT_KIMI_GATE_BF16_INPUT = _GatedBool(
"SGLANG_OPT_KIMI_GATE_BF16_INPUT", True
)
# ---- non-shared: default False, set explicitly in the launch env ----
# kimi (NVFP4):
SGLANG_OPT_USE_JIT_KERNEL_KIMI_GATE = _GatedBool(
"SGLANG_OPT_USE_JIT_KERNEL_KIMI_GATE", False
)
SGLANG_OPT_USE_JIT_KERNEL_MOE_ALIGN = _GatedBool(
"SGLANG_OPT_USE_JIT_KERNEL_MOE_ALIGN", False
)
# qwen3.5 (FP8):
SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC = _GatedBool(
"SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC", False
)
# (SGLANG_OPT_LORA_DOWN_FINALIZE_OVERLAP removed: net-neutral + base/decode-corruption hazard; serial down-LoRA only.)
SGLANG_OPT_LORA_SHARED_ADD_OVERLAP = _GatedBool(
"SGLANG_OPT_LORA_SHARED_ADD_OVERLAP", False
)
SGLANG_OPT_LORA_CUBLAS = _GatedBool("SGLANG_OPT_LORA_CUBLAS", False)
SGLANG_OPT_LORA_CUBLAS_A = _GatedBool("SGLANG_OPT_LORA_CUBLAS_A", False)
SGLANG_OPT_LORA_CUBLAS_B = _GatedBool("SGLANG_OPT_LORA_CUBLAS_B", False)
SGLANG_OPT_LORA_CUBLAS_GATE_UP = _GatedBool("SGLANG_OPT_LORA_CUBLAS_GATE_UP", False)
SGLANG_OPT_LORA_CUBLAS_QKV = _GatedBool("SGLANG_OPT_LORA_CUBLAS_QKV", False)
SGLANG_OPT_LORA_CUBLAS_KV_B = _GatedBool("SGLANG_OPT_LORA_CUBLAS_KV_B", False)
# diagnostics / tuning:
SGLANG_OPT_LORA_SHRINK_TUNE = _GatedBool("SGLANG_OPT_LORA_SHRINK_TUNE", False)
# kimi NVFP4 permute+quant fuse — read in jit_kernel/trtllm_lora_temp/core.py (Python) to pass
# a bool to the kernel, AND C++-side via getenv in the launcher. Default off (kimi-only).
SGLANG_OPT_FUSED_PERMUTE_QUANT = _GatedBool("SGLANG_OPT_FUSED_PERMUTE_QUANT", False)
# ---- integer knob ----
# decode two-stream token ceiling (consulted only on the experimental path).
SGLANG_TWO_STREAM_MAX_TOKENS = _GatedInt("SGLANG_TWO_STREAM_MAX_TOKENS", 256)
lora_envs = _LoraEnvs()
# ---------------------------------------------------------------------------
# C++-getenv-only flags (read via getenv in jit_kernel .cu launchers, NOT in Python).
# Set them in the launch env on the model that needs them; default off:
# SGLANG_OPT_FUSED_MOE_ACTIVATION_QUANT_FUSE (kimi NVFP4 act+down-quant fuse)
# SGLANG_OPT_FUSED_MOE_ACTIVATION_VEC (kimi NVFP4 vectorized activation)
# ---------------------------------------------------------------------------
@@ -0,0 +1,203 @@
from typing import Optional
import torch
from sglang.srt.utils.custom_op import register_custom_op
def _fake_fp8_block_scale_moe(
routing_logits: torch.Tensor,
routing_bias: Optional[torch.Tensor],
hidden_states: torch.Tensor,
hidden_states_scale: torch.Tensor,
gemm1_weights: torch.Tensor,
gemm1_weights_scale: torch.Tensor,
gemm2_weights: torch.Tensor,
gemm2_weights_scale: torch.Tensor,
num_experts: int,
top_k: int,
n_group: Optional[int],
topk_group: Optional[int],
intermediate_size: int,
local_expert_offset: int,
local_num_experts: int,
routed_scaling_factor: Optional[float],
routing_method_type: int = 0,
use_shuffled_weight: bool = False,
weight_layout: int = 0,
enable_pdl: Optional[bool] = None,
tune_max_num_tokens: int = 8192,
fp8_quantization_type: Optional[int] = None,
activation_type: Optional[int] = None,
) -> torch.Tensor:
return torch.empty(
hidden_states.shape, dtype=torch.bfloat16, device=hidden_states.device
)
@register_custom_op(fake_impl=_fake_fp8_block_scale_moe)
def sgl_trtllm_fp8_block_scale_moe_wrapper(
routing_logits: torch.Tensor,
routing_bias: Optional[torch.Tensor],
hidden_states: torch.Tensor,
hidden_states_scale: torch.Tensor,
gemm1_weights: torch.Tensor,
gemm1_weights_scale: torch.Tensor,
gemm2_weights: torch.Tensor,
gemm2_weights_scale: torch.Tensor,
num_experts: int,
top_k: int,
n_group: Optional[int],
topk_group: Optional[int],
intermediate_size: int,
local_expert_offset: int,
local_num_experts: int,
routed_scaling_factor: Optional[float],
routing_method_type: int = 0,
use_shuffled_weight: bool = False,
weight_layout: int = 0,
enable_pdl: Optional[bool] = None,
tune_max_num_tokens: int = 8192,
fp8_quantization_type: Optional[int] = None,
activation_type: Optional[int] = None,
) -> torch.Tensor:
try:
from flashinfer.fused_moe import Fp8QuantizationType
from flashinfer.fused_moe.core import ActivationType
except ImportError as e:
raise ImportError(
"experimental_sgl_trtllm requires flashinfer-python to provide "
"TRTLLM enums and cubin-loader utilities."
) from e
from sglang.jit_kernel.trtllm_lora_temp import trtllm_fp8_block_scale_moe
kwargs = {
"routing_logits": routing_logits,
"routing_bias": routing_bias,
"hidden_states": hidden_states,
"hidden_states_scale": hidden_states_scale,
"gemm1_weights": gemm1_weights,
"gemm1_weights_scale": gemm1_weights_scale,
"gemm2_weights": gemm2_weights,
"gemm2_weights_scale": gemm2_weights_scale,
"num_experts": num_experts,
"top_k": top_k,
"n_group": n_group,
"topk_group": topk_group,
"intermediate_size": intermediate_size,
"local_expert_offset": local_expert_offset,
"local_num_experts": local_num_experts,
"routed_scaling_factor": routed_scaling_factor,
"routing_method_type": routing_method_type,
"use_shuffled_weight": use_shuffled_weight,
"weight_layout": weight_layout,
"enable_pdl": enable_pdl,
"tune_max_num_tokens": tune_max_num_tokens,
}
if fp8_quantization_type is not None:
kwargs["fp8_quantization_type"] = Fp8QuantizationType(fp8_quantization_type)
if activation_type is not None:
kwargs["activation_type"] = ActivationType(activation_type)
return trtllm_fp8_block_scale_moe(**kwargs)
def _fake_fp8_block_scale_routed_moe(
topk_ids: torch.Tensor,
routing_bias: Optional[torch.Tensor],
hidden_states: torch.Tensor,
hidden_states_scale: torch.Tensor,
gemm1_weights: torch.Tensor,
gemm1_weights_scale: torch.Tensor,
gemm2_weights: torch.Tensor,
gemm2_weights_scale: torch.Tensor,
num_experts: int,
top_k: int,
n_group: Optional[int],
topk_group: Optional[int],
intermediate_size: int,
local_expert_offset: int,
local_num_experts: int,
routed_scaling_factor: Optional[float],
routing_method_type: int = 0,
use_shuffled_weight: bool = False,
weight_layout: int = 0,
enable_pdl: Optional[bool] = None,
tune_max_num_tokens: int = 8192,
fp8_quantization_type: Optional[int] = None,
activation_type: Optional[int] = None,
) -> torch.Tensor:
return torch.empty(
hidden_states.shape, dtype=torch.bfloat16, device=hidden_states.device
)
@register_custom_op(fake_impl=_fake_fp8_block_scale_routed_moe)
def sgl_trtllm_fp8_block_scale_routed_moe_wrapper(
topk_ids: torch.Tensor,
routing_bias: Optional[torch.Tensor],
hidden_states: torch.Tensor,
hidden_states_scale: torch.Tensor,
gemm1_weights: torch.Tensor,
gemm1_weights_scale: torch.Tensor,
gemm2_weights: torch.Tensor,
gemm2_weights_scale: torch.Tensor,
num_experts: int,
top_k: int,
n_group: Optional[int],
topk_group: Optional[int],
intermediate_size: int,
local_expert_offset: int,
local_num_experts: int,
routed_scaling_factor: Optional[float],
routing_method_type: int = 0,
use_shuffled_weight: bool = False,
weight_layout: int = 0,
enable_pdl: Optional[bool] = None,
tune_max_num_tokens: int = 8192,
fp8_quantization_type: Optional[int] = None,
activation_type: Optional[int] = None,
) -> torch.Tensor:
try:
from flashinfer.fused_moe import Fp8QuantizationType
from flashinfer.fused_moe.core import ActivationType
except ImportError as e:
raise ImportError(
"experimental_sgl_trtllm requires flashinfer-python to provide "
"TRTLLM enums and cubin-loader utilities."
) from e
from sglang.jit_kernel.trtllm_lora_temp import (
trtllm_fp8_block_scale_routed_moe,
)
kwargs = {
"topk_ids": topk_ids,
"routing_bias": routing_bias,
"hidden_states": hidden_states,
"hidden_states_scale": hidden_states_scale,
"gemm1_weights": gemm1_weights,
"gemm1_weights_scale": gemm1_weights_scale,
"gemm2_weights": gemm2_weights,
"gemm2_weights_scale": gemm2_weights_scale,
"num_experts": num_experts,
"top_k": top_k,
"n_group": n_group,
"topk_group": topk_group,
"intermediate_size": intermediate_size,
"local_expert_offset": local_expert_offset,
"local_num_experts": local_num_experts,
"routed_scaling_factor": routed_scaling_factor,
"routing_method_type": routing_method_type,
"use_shuffled_weight": use_shuffled_weight,
"weight_layout": weight_layout,
"enable_pdl": enable_pdl,
"tune_max_num_tokens": tune_max_num_tokens,
}
if fp8_quantization_type is not None:
kwargs["fp8_quantization_type"] = Fp8QuantizationType(fp8_quantization_type)
if activation_type is not None:
kwargs["activation_type"] = ActivationType(activation_type)
return trtllm_fp8_block_scale_routed_moe(**kwargs)
@@ -0,0 +1,614 @@
"""experimental_sgl_trtllm MoE LoRA dispatch (original single-stream).
This is the LoRA-enabled fused-experts path added by the trtllm-lora work — it
was originally a function in ``layers/moe/moe_runner/flashinfer_trtllm.py`` and
is now hosted here so that file holds only a re-export. The function name
remains ``fused_experts_none_to_experimental_sgl_trtllm_fp8_lora`` for
import-site stability.
When ``SGLANG_LORA_TWO_STREAM=1`` is set, this is the function the
``install_two_stream_overrides()`` monkey-patch swaps for the side-stream
version in :mod:`sglang.srt.lora.trtllm_lora_temp.moe_overlap`. Otherwise it runs as
the active path.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.srt.distributed import get_tp_group
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.layers.dp_attention import is_allocation_symmetric
from sglang.srt.layers.quantization.fp8_kernel import per_token_group_quant_fp8
from sglang.srt.utils.common import next_power_of_2
if TYPE_CHECKING:
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
FlashInferTrtllmBf16MoeQuantInfo,
FlashInferTrtllmFp4MoeQuantInfo,
FlashInferTrtllmFp8MoeQuantInfo,
)
from sglang.srt.layers.moe.token_dispatcher import (
StandardCombineInput,
StandardDispatchOutput,
)
def fused_experts_none_to_experimental_sgl_trtllm_fp8_lora(
dispatch_output: StandardDispatchOutput,
quant_info: FlashInferTrtllmFp8MoeQuantInfo,
runner_config: MoeRunnerConfig,
lora_info,
) -> StandardCombineInput:
from flashinfer.fused_moe import Fp8QuantizationType
from sglang.jit_kernel.trtllm_lora_temp import (
trtllm_fp8_block_scale_moe_lora_finalize,
trtllm_fp8_block_scale_routed_moe_lora,
)
from sglang.jit_kernel.trtllm_lora_temp.topk_pack import fused_pack_topk
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
merged_experts_fused_moe_lora_add,
)
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
from sglang.srt.layers.moe.utils import RoutingMethodType
from sglang.srt.lora.lora_moe_runners import build_lora_hooks
from sglang.srt.lora.trtllm_lora_temp.sgl_fp8_moe import (
fused_experts_fp8_sgl,
)
from sglang.srt.lora.trtllm_lora_temp.shared_add_overlap import (
maybe_overlap_staged_shared_add,
)
from sglang.srt.model_executor.runner_utils.capture_mode import get_is_capture_mode
assert runner_config.activation == "silu" and runner_config.is_gated, (
"experimental_sgl_trtllm LoRA currently supports the gated SwiGLU FP8 "
"Qwen path only."
)
assert quant_info.block_quant and not quant_info.use_mxfp8, (
"experimental_sgl_trtllm LoRA currently supports DeepSeekFp8 block-quant "
"checkpoints only."
)
assert quant_info.weight_block_k is not None
assert quant_info.w13_weight_scale_inv is not None
assert quant_info.w2_weight_scale_inv is not None
hidden_states = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
assert TopKOutputChecker.format_is_standard(topk_output)
assert runner_config.top_k is not None
if not get_is_capture_mode() and not lora_info.has_active_lora:
return fused_experts_fp8_sgl(
dispatch_output,
quant_info,
runner_config,
use_routed_topk=True,
)
topk_ids = topk_output.topk_ids
topk_weights = topk_output.topk_weights
use_virtual_lora_store = bool(
lora_info.lora_use_virtual_experts and lora_info.max_lora_rank > 0
)
if use_virtual_lora_store:
hooks = None
token_lora_mapping = lora_info.token_lora_mapping
fused_lora_routing_cache: dict = {}
else:
hooks = build_lora_hooks(hidden_states, lora_info, topk_ids)
token_lora_mapping = None
fused_lora_routing_cache = {}
# Fuse the per-token scale transpose into the quant kernel (column-major scales) so the
# `.t()` is a free view -> drops the standalone ~2us transpose+copy. Byte/shape-identical.
a_q, a_sf = per_token_group_quant_fp8(
hidden_states, quant_info.weight_block_k, column_major_scales=True
)
a_sf_t = a_sf.t()
# EP-aware LoRA: under MoE EP each rank computes the delta only for the experts it
# owns (passed via local_expert_offset/local_num_experts below). gate_up_delta stays
# new_empty even though non-owned [token, k] slots are then left unwritten -- the
# trtllm MoE is itself EP-aware, so those slots never feed the all-reduced output.
gate_up_delta_shape = (
hidden_states.shape[0],
runner_config.top_k,
quant_info.w13_weight.shape[1],
)
gate_up_delta = (
hidden_states.new_empty(gate_up_delta_shape)
if use_virtual_lora_store
else hidden_states.new_zeros(gate_up_delta_shape)
)
if use_virtual_lora_store:
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
)
elif hooks.after_gate_up is not None:
hooks.after_gate_up(hidden_states, gate_up_delta, topk_weights, topk_ids)
activation_lora_input = torch.empty(
(hidden_states.shape[0], runner_config.top_k, quant_info.intermediate_size),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
# SGLANG_OPT_LORA_FUSED_TOPK_PACK: the routed pack may already have been produced
# fused inside the gating kernel (StandardTopKOutput.packed_topk_ids) — including
# the padded-region id=-1 mask. Fall back to the separate pack otherwise.
packed_topk_ids = getattr(topk_output, "packed_topk_ids", None)
if packed_topk_ids is None:
packed_topk_ids = fused_pack_topk(
topk_ids=topk_ids,
topk_weights=topk_weights,
)
direct_down_output = None
if use_virtual_lora_store:
with use_symmetric_memory(
get_tp_group(), disabled=not is_allocation_symmetric()
):
direct_down_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
moe_result = trtllm_fp8_block_scale_routed_moe_lora(
topk_ids=packed_topk_ids,
routing_bias=None,
hidden_states=a_q,
hidden_states_scale=a_sf_t,
gemm1_weights=quant_info.w13_weight,
gemm1_weights_scale=quant_info.w13_weight_scale_inv,
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale_inv,
gate_up_lora_delta=gate_up_delta,
activation_lora_input=activation_lora_input,
num_experts=quant_info.global_num_experts,
top_k=runner_config.top_k,
n_group=None,
topk_group=None,
intermediate_size=quant_info.intermediate_size,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
routing_method_type=(
RoutingMethodType.TopK
if quant_info.routing_method_type == RoutingMethodType.DeepSeekV3
else quant_info.routing_method_type
),
use_shuffled_weight=False,
do_finalize=use_virtual_lora_store,
output=(
direct_down_output
if direct_down_output is not None
else torch.empty_like(hidden_states)
),
tune_max_num_tokens=next_power_of_2(a_q.shape[0]),
fp8_quantization_type=Fp8QuantizationType.DeepSeekFp8,
activation_type=quant_info.activation_type,
)
if use_virtual_lora_store:
output = moe_result
# Shared-add overlap: the trtllm op above already finalized `output`, so the
# staged shared-expert add (if any) can run on the main stream concurrent with
# the down-LoRA shrink below; the expand waits on it via expand_wait_event.
shared_add_done = maybe_overlap_staged_shared_add(output)
merged_experts_fused_moe_lora_add(
output=output,
hidden_states=activation_lora_input.view(-1, quant_info.intermediate_size),
lora_a=lora_info.down_lora_a_weights,
lora_b=lora_info.down_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=lora_info.experts_shared_outer_loras,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
fuse_sum_all_reduce=True,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
expand_wait_event=shared_add_done,
)
return StandardCombineInput(hidden_states=output)
gemm2_output, expert_weights, expanded_idx_to_permuted_idx = moe_result
down_delta_shape = (
hidden_states.shape[0],
runner_config.top_k,
hidden_states.shape[1],
)
down_delta = (
hidden_states.new_empty(down_delta_shape)
if use_virtual_lora_store
else hidden_states.new_zeros(down_delta_shape)
)
if use_virtual_lora_store:
merged_experts_fused_moe_lora_add(
output=down_delta,
hidden_states=activation_lora_input.view(-1, quant_info.intermediate_size),
lora_a=lora_info.down_lora_a_weights,
lora_b=lora_info.down_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=lora_info.experts_shared_outer_loras,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
)
elif hooks.after_down is not None:
hooks.after_down(
activation_lora_input.view(-1, quant_info.intermediate_size),
down_delta,
topk_weights,
topk_ids,
)
with use_symmetric_memory(get_tp_group(), disabled=not is_allocation_symmetric()):
output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
output = trtllm_fp8_block_scale_moe_lora_finalize(
gemm2_output=gemm2_output,
expert_weights=expert_weights,
expanded_idx_to_permuted_idx=expanded_idx_to_permuted_idx,
down_lora_delta=down_delta,
output=output,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
)
return StandardCombineInput(hidden_states=output)
def fused_experts_none_to_experimental_sgl_trtllm_bf16_lora(
dispatch_output: StandardDispatchOutput,
quant_info: FlashInferTrtllmBf16MoeQuantInfo,
runner_config: MoeRunnerConfig,
lora_info,
) -> StandardCombineInput:
"""BF16 sibling of ``fused_experts_none_to_experimental_sgl_trtllm_fp8_lora``.
Decomposed (unfused-activation) MoE-LoRA, bf16 end-to-end (no quantization):
routing -> gather -> gate_up grouped GEMM (raw 2*inter, bf16) -> activation that
adds ``gate_up_lora_delta`` pre-SwiGLU and captures ``activation_lora_input`` ->
down grouped GEMM -> finalize, then the virtual-experts down-LoRA is merged into
the output. Single-stream version (no two-stream overlap yet — phase 2).
"""
from sglang.jit_kernel.trtllm_lora_temp import trtllm_bf16_routed_moe_lora
from sglang.jit_kernel.trtllm_lora_temp.topk_pack import fused_pack_topk
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
merged_experts_fused_moe_lora_add,
)
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
fused_experts_none_to_flashinfer_trtllm_bf16,
get_activation_type,
)
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
from sglang.srt.layers.moe.utils import RoutingMethodType
from sglang.srt.model_executor.runner_utils.capture_mode import get_is_capture_mode
assert (
runner_config.activation == "silu" and runner_config.is_gated
), "experimental_sgl_trtllm BF16 LoRA currently supports the gated SwiGLU path only."
hidden_states = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
assert TopKOutputChecker.format_is_standard(topk_output)
assert runner_config.top_k is not None
# No active LoRA in a non-capture decode -> plain (fast) bf16 path.
if not get_is_capture_mode() and not lora_info.has_active_lora:
return fused_experts_none_to_flashinfer_trtllm_bf16(
dispatch_output, quant_info, runner_config, use_routed_topk=True
)
topk_ids = topk_output.topk_ids
topk_weights = topk_output.topk_weights
use_virtual_lora_store = bool(
lora_info.lora_use_virtual_experts and lora_info.max_lora_rank > 0
)
assert use_virtual_lora_store, "BF16 trtllm LoRA requires virtual-experts."
token_lora_mapping = lora_info.token_lora_mapping
fused_lora_routing_cache: dict = {}
inter = runner_config.intermediate_size_per_partition
# Gated gate_up LoRA delta (same shape/semantics as the fp8/fp4 paths). EP args scope
# the delta to this rank's experts, matching the EP-aware trtllm MoE base.
gate_up_delta = hidden_states.new_empty(
(hidden_states.shape[0], runner_config.top_k, 2 * inter)
)
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=runner_config.num_local_experts,
)
activation_lora_input = torch.empty(
(hidden_states.shape[0], runner_config.top_k, inter),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
packed_topk_ids = getattr(topk_output, "packed_topk_ids", None)
if packed_topk_ids is None:
packed_topk_ids = fused_pack_topk(
topk_ids=topk_ids,
topk_weights=topk_weights,
)
routing_method_type = runner_config.routing_method_type
if routing_method_type is None:
routing_method_type = RoutingMethodType.Default
elif routing_method_type == RoutingMethodType.DeepSeekV3:
routing_method_type = RoutingMethodType.TopK
with use_symmetric_memory(get_tp_group(), disabled=not is_allocation_symmetric()):
direct_down_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
output = trtllm_bf16_routed_moe_lora(
topk_ids=packed_topk_ids,
routing_bias=None,
hidden_states=hidden_states,
gemm1_weights=quant_info.gemm1_weights,
gemm2_weights=quant_info.gemm2_weights,
gate_up_lora_delta=gate_up_delta,
activation_lora_input=activation_lora_input,
num_experts=quant_info.global_num_experts,
top_k=runner_config.top_k,
intermediate_size=inter,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=runner_config.num_local_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
routing_method_type=routing_method_type,
do_finalize=True,
output=direct_down_output,
activation_type=get_activation_type(
runner_config.activation, is_gated=runner_config.is_gated
),
)
merged_experts_fused_moe_lora_add(
output=output,
hidden_states=activation_lora_input.view(-1, inter),
lora_a=lora_info.down_lora_a_weights,
lora_b=lora_info.down_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=lora_info.experts_shared_outer_loras,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
fuse_sum_all_reduce=True,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=runner_config.num_local_experts,
)
return StandardCombineInput(hidden_states=output)
def fused_experts_none_to_experimental_sgl_trtllm_fp4_lora(
dispatch_output: StandardDispatchOutput,
quant_info: FlashInferTrtllmFp4MoeQuantInfo,
runner_config: MoeRunnerConfig,
lora_info,
) -> StandardCombineInput:
"""NVFP4 sibling of ``fused_experts_none_to_experimental_sgl_trtllm_fp8_lora``.
Decomposed (unfused-activation) MoE-LoRA: routing -> gather -> gate_up grouped
GEMM (raw 2*inter) -> activation that adds ``gate_up_lora_delta`` pre-SwiGLU and
captures ``activation_lora_input`` -> NvFP4 quant -> down grouped GEMM -> finalize,
then the virtual-experts down-LoRA is merged into the output. Single-stream
version; ``moe_overlap.py`` provides the two-stream variant.
"""
from sglang.jit_kernel.trtllm_lora_temp import (
trtllm_fp4_block_scale_routed_moe_lora,
)
from sglang.jit_kernel.trtllm_lora_temp.topk_pack import fused_pack_topk
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
merged_experts_fused_moe_lora_add,
)
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
fused_experts_none_to_flashinfer_trtllm_fp4,
)
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
from sglang.srt.model_executor.runner_utils.capture_mode import get_is_capture_mode
assert (
runner_config.activation == "silu" and runner_config.is_gated
), "experimental_sgl_trtllm NVFP4 LoRA currently supports the gated SwiGLU path only."
hidden_states = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
assert TopKOutputChecker.format_is_standard(topk_output)
assert runner_config.top_k is not None
# No active LoRA in a non-capture decode -> plain (fast) FP4 path.
if not get_is_capture_mode() and not lora_info.has_active_lora:
return fused_experts_none_to_flashinfer_trtllm_fp4(
dispatch_output, quant_info, runner_config, use_routed_topk=True
)
topk_ids = topk_output.topk_ids
topk_weights = topk_output.topk_weights
use_virtual_lora_store = bool(
lora_info.lora_use_virtual_experts and lora_info.max_lora_rank > 0
)
assert use_virtual_lora_store, "NVFP4 trtllm LoRA requires virtual-experts."
token_lora_mapping = lora_info.token_lora_mapping
fused_lora_routing_cache: dict = {}
inter = quant_info.intermediate_size_per_partition
# Path 3: feed bf16 hidden DIRECTLY to the op (no python pre-quant). The op permutes the
# bf16 hidden (moe::dev::permute, token->expert order) then NvFP4-quantizes ONCE with the
# 1/(448*6) global + per-token scale — eliminating the dequant->permute->requant round-trip
# (and its magnitude bug) that pre-quantized fp4 input forced. Requires the layer to be in
# per-token-activation mode (SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION=1) so that
# g1_scale_c == g1_alphas and g2_alphas == w2_weight_scale_2, making the decomposed
# gate_up(g1_alphas)/SwiGLU/down(g2_alphas) scale composition match the plain fused path.
gate_up_delta_shape = (
hidden_states.shape[0],
runner_config.top_k,
quant_info.w13_weight.shape[1],
)
# Gated gate_up LoRA delta: a single merged_experts call on the full stacked [gate_A; up_A]
# lora_a (rank 2r) and [gate_B; up_B] lora_b (rank r), via the rank-specialized direct expand
# (use_direct_expand_add, rank <= 64). EP args scope the delta to this rank's experts, matching
# the EP-aware trtllm MoE base.
gate_up_delta = hidden_states.new_empty(gate_up_delta_shape)
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
)
activation_lora_input = torch.empty(
(hidden_states.shape[0], runner_config.top_k, inter),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
packed_topk_ids = fused_pack_topk(
topk_ids=topk_ids,
topk_weights=topk_weights,
)
with use_symmetric_memory(get_tp_group(), disabled=not is_allocation_symmetric()):
direct_down_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
output = trtllm_fp4_block_scale_routed_moe_lora(
topk_ids=packed_topk_ids,
routing_bias=None,
hidden_states=hidden_states,
hidden_states_scale=None,
gemm1_weights=quant_info.w13_weight,
gemm1_weights_scale=quant_info.w13_weight_scale.view(torch.float8_e4m3fn),
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale.view(torch.float8_e4m3fn),
output1_scales_scalar=quant_info.g1_scale_c,
output1_scales_gate_scalar=quant_info.g1_alphas,
output2_scales_scalar=quant_info.g2_alphas,
gate_up_lora_delta=gate_up_delta,
activation_lora_input=activation_lora_input,
num_experts=quant_info.global_num_experts,
top_k=runner_config.top_k,
intermediate_size=inter,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
routing_method_type=quant_info.routing_method_type,
do_finalize=True,
output=direct_down_output,
)
merged_experts_fused_moe_lora_add(
output=output,
hidden_states=activation_lora_input.view(-1, inter),
lora_a=lora_info.down_lora_a_weights,
lora_b=lora_info.down_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=lora_info.experts_shared_outer_loras,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
fuse_sum_all_reduce=True,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
# EP-aware: scope the down delta to this rank's experts, matching the gate_up
# call above and the FP8 down call. Harmless at EP=1 (local==global, Kimi today);
# required for correctness if MoE-EP is turned on later (otherwise non-owned
# experts' deltas get over-counted by the fuse_sum_all_reduce).
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
)
return StandardCombineInput(hidden_states=output)
@@ -0,0 +1,188 @@
"""experimental_sgl_trtllm-specific bits of ``FusedMoEWithLoRA``.
This file holds the two trtllm-specific code blocks that used to be inlined
inside the ``FusedMoEWithLoRA`` class in ``lora/layers.py``:
- :func:`init_experimental_sgl_trtllm_lora` — builds the FP8 block-scale
``FlashInferTrtllmFp8MoeQuantInfo`` and stores it on the layer instance.
Called from ``FusedMoEWithLoRA.__init__`` when the runner backend is the
experimental_sgl_trtllm MoE.
- :func:`dispatch_experimental_sgl_trtllm_lora` — dispatches the LoRA fused
experts call. Called from ``FusedMoEWithLoRA.run`` for the same backend.
Keeping them here means ``lora/layers.py`` only has tiny ``if backend == ...:
init(self, base_layer)`` / ``dispatch(...)`` injection points for the new
trtllm path instead of ~70 lines of inlined logic.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sglang.srt.layers.moe.token_dispatcher import StandardCombineInput
def init_experimental_sgl_trtllm_lora(layer, base_layer) -> None:
"""Build and store the trtllm FP8 LoRA quant info on the layer.
Sets ``layer._lora_runner = None`` (trtllm path doesn't use ``MoeRunner``)
and ``layer._quant_info`` to a fully-populated
``FlashInferTrtllmFp8MoeQuantInfo`` (or ``FlashInferTrtllmFp4MoeQuantInfo`` for
NVFP4 / modelopt checkpoints like Kimi-K2.5-NVFP4).
"""
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
FlashInferTrtllmFp8MoeQuantInfo,
get_activation_type,
)
from sglang.srt.layers.moe.utils import RoutingMethodType
# ---- NVFP4 (modelopt) path ----
# The fp4 weight loader sets ``g1_scale_c`` on the FusedMoE layer (see
# ModelOptNvFp4FusedMoEMethod.apply). Mirror the non-LoRA construction in
# modelopt_quant.py (~L2099) so the fp4 LoRA dispatch gets the same payload.
# NOTE(w13 layout): the decomposed op runs the gate_up projection as a *non-gated*
# Gemm2-style GEMM and lets the activation kernel do the SwiGLU split (silu(first)*second),
# so w13 must be [Gate, Up] with shuffle_matrix_a but WITHOUT reorder_rows_for_gated_act_gemm.
# The TRTLLM fp4 path uses load_up_proj_weight_first=False (=> [Gate, Up]); verify the
# processed w13 layout against this at e2e (acc gate) and re-prep if mismatched.
if hasattr(base_layer, "g1_scale_c"):
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
FlashInferTrtllmFp4MoeQuantInfo,
)
layer._lora_runner = None
layer._quant_info = FlashInferTrtllmFp4MoeQuantInfo(
w13_weight=base_layer.w13_weight.data,
w2_weight=base_layer.w2_weight.data,
w13_weight_scale=base_layer.w13_weight_scale.data,
w2_weight_scale=base_layer.w2_weight_scale.data,
g1_scale_c=base_layer.g1_scale_c.data,
g1_alphas=base_layer.g1_alphas.data,
g2_alphas=base_layer.g2_alphas.data,
w13_input_scale_quant=base_layer.w13_input_scale_quant,
global_num_experts=int(base_layer.num_experts),
local_expert_offset=int(base_layer.moe_ep_rank)
* int(base_layer.num_local_experts),
local_num_experts=int(base_layer.num_local_experts),
intermediate_size_per_partition=int(
base_layer.intermediate_size_per_partition
),
routing_method_type=int(
getattr(base_layer, "routing_method_type", None)
or RoutingMethodType.DeepSeekV3
),
)
return
quant_method = base_layer.quant_method
quant_config = getattr(quant_method, "quant_config", None)
weight_block_size = getattr(quant_config, "weight_block_size", None)
if weight_block_size is None:
weight_block_size = getattr(quant_method, "weight_block_size", None)
use_mxfp8 = bool(getattr(quant_config, "use_mxfp8", False))
# ---- BF16 (unquantized) path ----
# No quant_config / block scales => the checkpoint is bf16. The bf16 LoRA dispatch
# runs the decomposed trtllm pipeline (sgl_trtllm_bf16_routed_moe_lora): permute ->
# raw gate_up GEMM -> LoRA-aware activation -> down GEMM, all bf16 — using the SAME
# prepared w13/w2 tensors (shuffled + BlockMajorK) the plain trtllm_bf16 path consumes.
# intermediate_size / local_num_experts / routing_method_type come from
# base_layer.moe_runner_config at dispatch time (the bf16 quant-info is minimal).
if quant_config is None and not getattr(quant_method, "block_quant", False):
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
FlashInferTrtllmBf16MoeQuantInfo,
)
layer._lora_runner = None
layer._quant_info = FlashInferTrtllmBf16MoeQuantInfo(
gemm1_weights=base_layer.w13_weight.data,
gemm2_weights=base_layer.w2_weight.data,
global_num_experts=int(base_layer.num_experts),
local_expert_offset=int(base_layer.moe_ep_rank)
* int(base_layer.num_local_experts),
)
# Expose w13_weight/w2_weight on the bf16 quant-info so the backend-agnostic
# cuda-graph MoE buffer init (BaseLoRABackend.init_cuda_graph_moe_buffers) reads
# expert dims uniformly with the FP8/FP4 quant-infos (which name them w13/w2).
# The bf16 BlockMajorK weights are 4-D [E, N, K/128, 128]; collapsing the inner
# dims to a 3-D [E, N, K] view (free for contiguous weights) makes the upstream
# `E, N, _ = w13_weight.shape` dim-extraction work without touching base_backend.
_g1 = layer._quant_info.gemm1_weights
_g2 = layer._quant_info.gemm2_weights
layer._quant_info.w13_weight = _g1.reshape(_g1.shape[0], _g1.shape[1], -1)
layer._quant_info.w2_weight = _g2.reshape(_g2.shape[0], _g2.shape[1], -1)
return
assert getattr(
quant_method, "block_quant", False
), "experimental_sgl_trtllm LoRA currently requires FP8 block quant."
assert (
not use_mxfp8
), "experimental_sgl_trtllm LoRA currently targets the non-MX FP8 Qwen path."
assert (
weight_block_size is not None
), "experimental_sgl_trtllm LoRA needs the FP8 weight block size."
w13_weight_scale = getattr(base_layer, "w13_weight_scale_inv", None)
if w13_weight_scale is None:
w13_weight_scale = getattr(base_layer, "w13_weight_scale", None)
w2_weight_scale = getattr(base_layer, "w2_weight_scale_inv", None)
if w2_weight_scale is None:
w2_weight_scale = getattr(base_layer, "w2_weight_scale", None)
assert w13_weight_scale is not None and w2_weight_scale is not None
layer._lora_runner = None
layer._quant_info = FlashInferTrtllmFp8MoeQuantInfo(
w13_weight=base_layer.w13_weight,
w2_weight=base_layer.w2_weight,
global_num_experts=int(base_layer.num_experts),
local_expert_offset=int(base_layer.moe_ep_rank)
* int(base_layer.num_local_experts),
local_num_experts=int(base_layer.num_local_experts),
intermediate_size=base_layer.w2_weight.shape[2],
routing_method_type=int(
getattr(base_layer, "routing_method_type", None)
or RoutingMethodType.DeepSeekV3
),
block_quant=True,
use_mxfp8=False,
weight_block_k=weight_block_size[1],
w13_weight_scale_inv=w13_weight_scale,
w2_weight_scale_inv=w2_weight_scale,
activation_type=get_activation_type(
base_layer.moe_runner_config.activation,
is_gated=base_layer.moe_runner_config.is_gated,
),
)
def dispatch_experimental_sgl_trtllm_lora(
dispatch_output, quant_info, base_layer, lora_info
) -> StandardCombineInput:
"""Call the trtllm fused-experts LoRA function for a single layer.
Looked up at call time so the install-time monkey-patch in
:mod:`sglang.srt.lora.trtllm_lora_temp` (the two-stream override) takes effect.
"""
import sglang.srt.lora.trtllm_lora_temp.lora_dispatch as ft
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
FlashInferTrtllmBf16MoeQuantInfo,
FlashInferTrtllmFp4MoeQuantInfo,
)
# Resolve the fused-experts fn on the module at CALL TIME so the install-time
# two-stream monkey-patch (sglang.srt.lora.trtllm_lora_temp) takes effect. Route by
# quant dtype: NVFP4 -> fp4 LoRA op, BF16 (unquantized) -> bf16 LoRA op, else FP8.
if isinstance(quant_info, FlashInferTrtllmFp4MoeQuantInfo):
fused_fn = ft.fused_experts_none_to_experimental_sgl_trtllm_fp4_lora
elif isinstance(quant_info, FlashInferTrtllmBf16MoeQuantInfo):
fused_fn = ft.fused_experts_none_to_experimental_sgl_trtllm_bf16_lora
else:
fused_fn = ft.fused_experts_none_to_experimental_sgl_trtllm_fp8_lora
return fused_fn(
dispatch_output,
quant_info,
base_layer.moe_runner_config,
lora_info,
)
@@ -0,0 +1,89 @@
"""Two-stream MergedColumnParallelLinear LoRA forward (O9).
Monkey-patched onto :class:`MergedColumnParallelLinearWithLoRA` by
:func:`sglang.srt.lora.trtllm_lora_temp.install_two_stream_overrides` when
``SGLANG_LORA_TWO_STREAM=1``. Covers the merged-column LoRA modules not handled
by O7 (QKV) / O8 (o_proj) / O1 (MoE experts):
* Qwen3.5 mamba ``in_proj_qkvz`` (a MergedColumnParallelLinear, every mamba layer)
* dense ``gate_up_proj`` MLP layers (e.g. Qwen3-VL non-expert MLP)
Same shape as O7: the LoRA-A shrink reads ``input_`` (same input as the base
GEMM, no write conflict) and runs on the side stream concurrent with the base
merged-column GEMM on the main stream; the LoRA-B expand needs both the shrink
output and base_output, so it runs after the rejoin on the main stream. The
expand mirrors ``MergedColumnParallelLinearWithLoRA.apply_lora`` — gate_up vs
general n-slice.
"""
import torch
from sglang.srt.distributed import tensor_model_parallel_all_gather
from sglang.srt.lora.trtllm_lora_temp import (
get_lora_side_stream,
get_original_merged_column_forward,
is_two_stream_active,
lora_overlap_alloc_stream,
)
def merged_column_lora_forward(self, input_: torch.Tensor):
"""O9 — side-stream LoRA-A shrink ‖ base merged-column GEMM."""
if not self.set_lora or not is_two_stream_active(input_):
return get_original_merged_column_forward()(self, input_)
from sglang.kernels.ops.gemm.trtllm_lora_temp.gate_up_lora_b import (
gate_up_lora_b_fwd,
)
from sglang.kernels.ops.gemm.trtllm_lora_temp.qkv_lora_b import qkv_lora_b_fwd
from sglang.kernels.ops.gemm.trtllm_lora_temp.sgemm_lora_a import sgemm_lora_a_fwd
bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
side_stream = get_lora_side_stream()
# sgemm_info is host-side (LoRABatchInfo); compute once, share both calls.
sgemm_info = self.lora_backend._sgemm_info()
lora_n_slices = self._get_lora_n_slices()
use_gate_up = lora_n_slices == 2 and self.use_gate_up_lora
# Shrink on side stream, concurrent with the base merged-column GEMM on main.
_alloc = lora_overlap_alloc_stream() # capture MAIN stream here (before the fork)
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
shrink_intermediate = sgemm_lora_a_fwd(
input_,
self.A_buffer,
sgemm_info,
stack_num=lora_n_slices,
out_alloc_stream=_alloc,
)
output_parallel = self.base_layer.quant_method.apply(self.base_layer, input_, bias)
# Rejoin: expand reads both side-produced shrink_intermediate and base_output.
torch.cuda.current_stream().wait_stream(side_stream)
if use_gate_up:
output_dim = self.B_buffer.shape[-2] // 2
output_parallel = gate_up_lora_b_fwd(
shrink_intermediate,
self.B_buffer,
sgemm_info,
output_dim,
output_parallel,
)
else:
output_parallel = qkv_lora_b_fwd(
shrink_intermediate,
self.B_buffer,
sgemm_info,
self.output_offset,
self.max_out_dim,
output_parallel,
n_slices=lora_n_slices,
)
if self.base_layer.gather_output:
output = tensor_model_parallel_all_gather(output_parallel)
else:
output = output_parallel
output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
return output, output_bias
@@ -0,0 +1,786 @@
"""Two-stream MoE LoRA dispatch (O1).
Monkey-patches ``fused_experts_none_to_experimental_sgl_trtllm_fp8_lora`` in
``layers/moe/moe_runner/flashinfer_trtllm.py`` (when
``SGLANG_LORA_TWO_STREAM=1``) so the gate_up LoRA shrink+expand runs on a
side stream concurrent with the main-stream FP8 quant.
Batches that don't qualify for two-stream (prefill / non-virtual-lora /
batch without active LoRA) fall through to the saved-original function so
their behavior is byte-identical to the unpatched code path.
"""
import torch
from sglang.srt.lora.trtllm_lora_temp import (
get_lora_side_stream,
get_original_bf16_moe_lora_func,
get_original_fp4_moe_lora_func,
get_original_moe_lora_func,
is_two_stream_active,
)
# GEMM1-LoRA overlap: keep LoRA-ready events recorded during cuda-graph capture alive so the
# captured cross-stream wait (resolved inside the trtllm op before activation) isn't torn down
# before graph instantiation. Only appended while capturing; eager runs rely on CUDA's
# deferred cudaEventDestroy.
_LORA_OVERLAP_EVENTS: list = []
def fused_experts_none_to_experimental_sgl_trtllm_fp8_lora_two_stream(
dispatch_output,
quant_info,
runner_config,
lora_info,
):
"""Drop-in replacement for the like-named function in flashinfer_trtllm.py.
Two-stream fast path: only fires when the batch is decode-shaped AND uses
virtual-experts LoRA. Everything else delegates to the original function.
"""
hidden_states = dispatch_output.hidden_states
use_virtual_lora_store = bool(
lora_info.lora_use_virtual_experts and lora_info.max_lora_rank > 0
)
# Two-stream requires virtual-experts LoRA AND a decode-shaped batch.
# Fall back to the original implementation for anything else (prefill,
# non-virtual LoRA, non-LoRA capture, etc.).
if not (use_virtual_lora_store and is_two_stream_active(hidden_states)):
return get_original_moe_lora_func()(
dispatch_output, quant_info, runner_config, lora_info
)
# ---- two-stream fast path ----
from flashinfer.fused_moe import Fp8QuantizationType
from sglang.jit_kernel.trtllm_lora_temp import (
trtllm_fp8_block_scale_routed_moe_lora,
)
from sglang.jit_kernel.trtllm_lora_temp.topk_pack import fused_pack_topk
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
merged_experts_fused_moe_lora_add,
)
from sglang.srt.distributed import get_tp_group
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.layers.dp_attention import is_allocation_symmetric
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
from sglang.srt.layers.moe.utils import RoutingMethodType
from sglang.srt.layers.quantization.fp8_kernel import per_token_group_quant_fp8
from sglang.srt.lora.trtllm_lora_temp.shared_add_overlap import (
maybe_overlap_staged_shared_add,
)
from sglang.srt.utils.common import next_power_of_2
assert runner_config.activation == "silu" and runner_config.is_gated, (
"experimental_sgl_trtllm LoRA currently supports the gated SwiGLU FP8 "
"Qwen path only."
)
assert quant_info.block_quant and not quant_info.use_mxfp8, (
"experimental_sgl_trtllm LoRA currently supports DeepSeekFp8 block-quant "
"checkpoints only."
)
assert quant_info.weight_block_k is not None
assert quant_info.w13_weight_scale_inv is not None
assert quant_info.w2_weight_scale_inv is not None
topk_output = dispatch_output.topk_output
assert TopKOutputChecker.format_is_standard(topk_output)
assert runner_config.top_k is not None
topk_ids = topk_output.topk_ids
topk_weights = topk_output.topk_weights
token_lora_mapping = lora_info.token_lora_mapping
fused_lora_routing_cache: dict = {}
side_stream = get_lora_side_stream()
# EP-aware LoRA: under MoE EP each rank computes the delta only for its owned experts
# (passed via local_expert_offset/local_num_experts below). gate_up_delta stays
# new_empty even though non-owned [token, k] slots are then left unwritten -- the
# trtllm MoE is itself EP-aware, so those slots never feed the all-reduced output.
gate_up_delta_shape = (
hidden_states.shape[0],
runner_config.top_k,
quant_info.w13_weight.shape[1],
)
gate_up_delta = hidden_states.new_empty(gate_up_delta_shape)
def _run_gate_up_lora():
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
intermediate_buffer=gate_up_lora_intermediate,
)
# GEMM1-LoRA overlap: fire the gate_up LoRA on the side stream + record an event; the
# trtllm op waits on it right before activation (the only consumer of gate_up_delta), so
# permute+GEMM1 overlap the side-stream LoRA shrink/expand instead of joining before the
# whole op.
lora_event = torch.cuda.Event()
# Hoist every side-chain allocation onto the MAIN stream (cuda-graph
# allocator safety -- see the "routing" stage in virtual_experts.py):
# pre-warm the routing cache and pre-allocate the shrink intermediate here,
# so the side-stream block below launches kernels only.
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
stage="routing",
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
)
gate_up_lora_intermediate = hidden_states.new_empty(
(
hidden_states.shape[0],
topk_ids.shape[1],
lora_info.gate_up_lora_a_weights.shape[2],
)
)
# O1 fork — gate_up shrink/expand on side stream concurrent with the main-stream
# per-token-group FP8 quant + the trtllm op's permute+GEMM1 below.
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
_run_gate_up_lora()
lora_event.record()
# Fuse the per-token scale transpose into the quant kernel: column-major scales make
# the `.t()` a free view, dropping the standalone ~2us transpose+copy. The trtllm MoE
# kernel wants the [K, M]-contiguous scale, which `.t()` of the column-major buffer is
# exactly -- byte/shape-identical to the old `a_sf.t().contiguous()`.
a_q, a_sf = per_token_group_quant_fp8(
hidden_states, quant_info.weight_block_k, column_major_scales=True
)
a_sf_t = a_sf.t()
activation_lora_input = torch.empty(
(hidden_states.shape[0], runner_config.top_k, quant_info.intermediate_size),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
# SGLANG_OPT_LORA_FUSED_TOPK_PACK: the routed pack may already have been produced
# fused inside the gating kernel (StandardTopKOutput.packed_topk_ids) — including
# the padded-region id=-1 mask. Fall back to the separate pack otherwise.
packed_topk_ids = getattr(topk_output, "packed_topk_ids", None)
if packed_topk_ids is None:
packed_topk_ids = fused_pack_topk(
topk_ids=topk_ids,
topk_weights=topk_weights,
)
with use_symmetric_memory(get_tp_group(), disabled=not is_allocation_symmetric()):
direct_down_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
# No pre-op join: the trtllm op waits on lora_event right before its activation kernel,
# so permute+GEMM1 run concurrent with the side-stream LoRA. Keep the event alive through
# cuda-graph capture so the captured cross-stream wait isn't torn down before instantiation.
if torch.cuda.is_current_stream_capturing():
_LORA_OVERLAP_EVENTS.append(lora_event)
lora_ready_handle = lora_event.cuda_event
# Down-LoRA/finalize overlap (env-gated): the op records gemm2_done_event right after the
# base down GEMM (before finalize); the side stream waits on it and runs ONLY the down-proj
# LoRA shrink (gemm A) + routing prep concurrent with finalizeKernel. The expand-add
# (gemm B) atomic-adds into `output` -- which finalize WRITES concurrently -- so it stays
# on the main stream after the op (post-finalize), exactly like the serial path.
# DISABLED: the down/finalize overlap is bench-verified net-neutral-to-negative AND
# corrupts the base/decode path — the captured gemm2_done cross-stream event under
# cuda-graph replay perturbs no-active-LoRA (base) requests (qwen base gsm8k 0.81 -> 0.56
# with it on; bisect-confirmed). The serial down-LoRA path below is used unconditionally.
down_overlap = False
gemm2_done_handle = 0
if down_overlap:
gemm2_done_event = torch.cuda.Event()
# Materialize the underlying cudaEvent (torch creates it lazily on first record) so
# .cuda_event is a real handle; the op re-records it after GEMM2.
gemm2_done_event.record()
gemm2_done_handle = gemm2_done_event.cuda_event
moe_result = trtllm_fp8_block_scale_routed_moe_lora(
topk_ids=packed_topk_ids,
routing_bias=None,
hidden_states=a_q,
hidden_states_scale=a_sf_t,
gemm1_weights=quant_info.w13_weight,
gemm1_weights_scale=quant_info.w13_weight_scale_inv,
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale_inv,
gate_up_lora_delta=gate_up_delta,
activation_lora_input=activation_lora_input,
lora_ready_event=lora_ready_handle,
num_experts=quant_info.global_num_experts,
top_k=runner_config.top_k,
n_group=None,
topk_group=None,
intermediate_size=quant_info.intermediate_size,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
routing_method_type=(
RoutingMethodType.TopK
if quant_info.routing_method_type == RoutingMethodType.DeepSeekV3
else quant_info.routing_method_type
),
use_shuffled_weight=False,
do_finalize=True,
output=direct_down_output,
tune_max_num_tokens=next_power_of_2(a_q.shape[0]),
fp8_quantization_type=Fp8QuantizationType.DeepSeekFp8,
activation_type=quant_info.activation_type,
gemm2_done_event=gemm2_done_handle,
)
output = moe_result
def _run_down_lora(
out, stage="all", intermediate_buffer=None, expand_wait_event=None
):
return merged_experts_fused_moe_lora_add(
output=out,
hidden_states=activation_lora_input.view(-1, quant_info.intermediate_size),
lora_a=lora_info.down_lora_a_weights,
lora_b=lora_info.down_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=lora_info.experts_shared_outer_loras,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
fuse_sum_all_reduce=True,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
stage=stage,
intermediate_buffer=intermediate_buffer,
expand_wait_event=expand_wait_event,
)
# Shared-add overlap: the trtllm op above already finalized `output`, so the
# staged shared-expert add (if any) can run on the producer (main) stream
# concurrent with the down-LoRA shrink below; the expand waits on it via
# expand_wait_event before atomic-adding into the same buffer.
shared_add_done = maybe_overlap_staged_shared_add(output)
if down_overlap:
# Fork at "base down GEMM done": ONLY the shrink (gemm A) + routing prep run on the
# side stream, concurrent with the main-stream finalizeKernel. The expand-add (gemm B)
# joins back on the MAIN stream after the op -- i.e. strictly after finalize wrote
# `output` -- and atomic-adds into it exactly like the serial path: same kernels,
# same buffers, identical numerics; the shrink just starts earlier.
# The shrink intermediate is allocated HERE (main = consumer stream of the expand),
# per the SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC lesson on side-stream allocations.
down_intermediate = hidden_states.new_empty(
(
hidden_states.shape[0],
topk_ids.shape[1],
lora_info.down_lora_a_weights.shape[2],
)
)
side_stream.wait_event(gemm2_done_event)
shrink_done_event = torch.cuda.Event()
with torch.cuda.stream(side_stream):
_run_down_lora(
output, stage="shrink", intermediate_buffer=down_intermediate
)
shrink_done_event.record()
if torch.cuda.is_current_stream_capturing():
_LORA_OVERLAP_EVENTS.append(gemm2_done_event)
_LORA_OVERLAP_EVENTS.append(shrink_done_event)
torch.cuda.current_stream().wait_event(shrink_done_event)
_run_down_lora(
output,
stage="expand",
intermediate_buffer=down_intermediate,
expand_wait_event=shared_add_done,
)
else:
_run_down_lora(output, expand_wait_event=shared_add_done)
return StandardCombineInput(hidden_states=output)
def fused_experts_none_to_experimental_sgl_trtllm_fp4_lora_two_stream(
dispatch_output,
quant_info,
runner_config,
lora_info,
):
"""Two-stream NVFP4 sibling of the FP8 two-stream MoE LoRA dispatch.
Fires only for virtual-experts LoRA + decode-shaped batches; everything else
delegates to the saved-original single-stream FP4 dispatch (byte-identical).
"""
hidden_states = dispatch_output.hidden_states
use_virtual_lora_store = bool(
lora_info.lora_use_virtual_experts and lora_info.max_lora_rank > 0
)
if not (use_virtual_lora_store and is_two_stream_active(hidden_states)):
return get_original_fp4_moe_lora_func()(
dispatch_output, quant_info, runner_config, lora_info
)
# ---- two-stream fast path ----
from sglang.jit_kernel.trtllm_lora_temp import (
trtllm_fp4_block_scale_routed_moe_lora,
)
from sglang.jit_kernel.trtllm_lora_temp.topk_pack import fused_pack_topk
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
merged_experts_fused_moe_lora_add,
)
from sglang.srt.distributed import get_tp_group
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.layers.dp_attention import is_allocation_symmetric
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
assert (
runner_config.activation == "silu" and runner_config.is_gated
), "experimental_sgl_trtllm NVFP4 LoRA currently supports the gated SwiGLU path only."
topk_output = dispatch_output.topk_output
assert TopKOutputChecker.format_is_standard(topk_output)
assert runner_config.top_k is not None
topk_ids = topk_output.topk_ids
topk_weights = topk_output.topk_weights
token_lora_mapping = lora_info.token_lora_mapping
fused_lora_routing_cache: dict = {}
# Down-proj LoRA runs serially on the main stream (after the trtllm op) by default. The old
# side-stream down-overlap was removed: bench-verified net-neutral-to-negative (the extra
# side-stream all-reduce cancels any overlap gain), AND its act_ready_event cross-stream sync
# corrupted decode state under sustained heavy LoRA load (cuda-graph replay -> persistent
# garbage). SGLANG_OPT_LORA_DOWN_FINALIZE_OVERLAP=1 re-introduces a more conservative variant:
# fork at gemm2_done (not act_ready), and only the SHRINK (gemm A) overlaps the finalize
# kernel -- the expand-add (gemm B) stays on the MAIN stream post-finalize (it writes the
# same `output` finalize writes), so its kernels/numerics match the serial path exactly.
inter = quant_info.intermediate_size_per_partition
side_stream = get_lora_side_stream()
gate_up_delta = hidden_states.new_empty(
(hidden_states.shape[0], runner_config.top_k, quant_info.w13_weight.shape[1])
)
def _run_gate_up_lora():
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
intermediate_buffer=gate_up_lora_intermediate,
)
# Hoist every side-chain allocation onto the MAIN stream (cuda-graph
# allocator safety -- see the "routing" stage in virtual_experts.py).
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
stage="routing",
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
)
gate_up_lora_intermediate = hidden_states.new_empty(
(
hidden_states.shape[0],
topk_ids.shape[1],
lora_info.gate_up_lora_a_weights.shape[2],
)
)
# O1-fp4 fork: gate_up shrink/expand on the side stream, concurrent with the
# FP4 op's permute + gate_up GEMM1 below. The op waits on lora_event right
# before its activation kernel (the only consumer of gate_up_delta).
lora_event = torch.cuda.Event()
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
_run_gate_up_lora()
lora_event.record()
activation_lora_input = torch.empty(
(hidden_states.shape[0], runner_config.top_k, inter),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
packed_topk_ids = fused_pack_topk(
topk_ids=topk_ids,
topk_weights=topk_weights,
)
with use_symmetric_memory(get_tp_group(), disabled=not is_allocation_symmetric()):
direct_down_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
# Keep the event alive through cuda-graph capture so the captured wait inside
# the FP4 op isn't torn down before instantiation (eager relies on deferred destroy).
if torch.cuda.is_current_stream_capturing():
_LORA_OVERLAP_EVENTS.append(lora_event)
lora_ready_handle = lora_event.cuda_event
# Down-LoRA/finalize overlap (env-gated, see the comment above): record gemm2_done inside
# the op; the side stream runs only the down-LoRA shrink + routing prep concurrent with
# finalize; the expand-add joins back on the main stream after the op.
# DISABLED: the down/finalize overlap is bench-verified net-neutral-to-negative AND
# corrupts the base/decode path — the captured gemm2_done cross-stream event under
# cuda-graph replay perturbs no-active-LoRA (base) requests (qwen base gsm8k 0.81 -> 0.56
# with it on; bisect-confirmed). The serial down-LoRA path below is used unconditionally.
down_overlap = False
gemm2_done_handle = 0
if down_overlap:
gemm2_done_event = torch.cuda.Event()
# Materialize the underlying cudaEvent (torch creates it lazily on first record) so
# .cuda_event is a real handle; the op re-records it after the down GEMM.
gemm2_done_event.record()
gemm2_done_handle = gemm2_done_event.cuda_event
output = trtllm_fp4_block_scale_routed_moe_lora(
topk_ids=packed_topk_ids,
routing_bias=None,
hidden_states=hidden_states,
hidden_states_scale=None,
gemm1_weights=quant_info.w13_weight,
gemm1_weights_scale=quant_info.w13_weight_scale.view(torch.float8_e4m3fn),
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale.view(torch.float8_e4m3fn),
output1_scales_scalar=quant_info.g1_scale_c,
output1_scales_gate_scalar=quant_info.g1_alphas,
output2_scales_scalar=quant_info.g2_alphas,
gate_up_lora_delta=gate_up_delta,
activation_lora_input=activation_lora_input,
lora_ready_event=lora_ready_handle,
num_experts=quant_info.global_num_experts,
top_k=runner_config.top_k,
intermediate_size=inter,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
routing_method_type=quant_info.routing_method_type,
do_finalize=True,
output=direct_down_output,
gemm2_done_event=gemm2_done_handle,
)
def _run_down_lora(out, stage="all", intermediate_buffer=None):
return merged_experts_fused_moe_lora_add(
output=out,
hidden_states=activation_lora_input.view(-1, inter),
lora_a=lora_info.down_lora_a_weights,
lora_b=lora_info.down_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=lora_info.experts_shared_outer_loras,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
fuse_sum_all_reduce=True,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
stage=stage,
intermediate_buffer=intermediate_buffer,
)
if down_overlap:
# Fork at "base down GEMM done": shrink (gemm A) + routing prep on the side stream
# concurrent with the main-stream finalize; the expand-add (gemm B) joins back on the
# MAIN stream after the op (strictly post-finalize) -- serial-path kernels/numerics.
# Intermediate allocated on main (= consumer stream of the expand), per the
# SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC lesson.
down_intermediate = hidden_states.new_empty(
(
hidden_states.shape[0],
topk_ids.shape[1],
lora_info.down_lora_a_weights.shape[2],
)
)
side_stream.wait_event(gemm2_done_event)
shrink_done_event = torch.cuda.Event()
with torch.cuda.stream(side_stream):
_run_down_lora(
output, stage="shrink", intermediate_buffer=down_intermediate
)
shrink_done_event.record()
if torch.cuda.is_current_stream_capturing():
_LORA_OVERLAP_EVENTS.append(gemm2_done_event)
_LORA_OVERLAP_EVENTS.append(shrink_done_event)
torch.cuda.current_stream().wait_event(shrink_done_event)
_run_down_lora(output, stage="expand", intermediate_buffer=down_intermediate)
else:
_run_down_lora(output)
return StandardCombineInput(hidden_states=output)
def fused_experts_none_to_experimental_sgl_trtllm_bf16_lora_two_stream(
dispatch_output,
quant_info,
runner_config,
lora_info,
):
"""Two-stream BF16 sibling of the FP8/FP4 two-stream MoE LoRA dispatches.
O1-bf16 fork: the gate_up LoRA shrink/expand runs on the side stream
concurrent with the bf16 op's routing + permute + gate_up GEMM; the op
waits on ``lora_ready_event`` right before its activation kernel (the only
consumer of ``gate_up_delta``). Fires only for virtual-experts LoRA +
decode-shaped batches; everything else delegates to the saved-original
single-stream bf16 dispatch (byte-identical). Down-LoRA stays serial on
the main stream — the down/finalize overlap was bench-verified
net-neutral-to-negative on the FP8/FP4 paths and corrupted the base
decode path under cuda-graph replay (see the comment in the FP4 variant).
"""
hidden_states = dispatch_output.hidden_states
use_virtual_lora_store = bool(
lora_info.lora_use_virtual_experts and lora_info.max_lora_rank > 0
)
if not (use_virtual_lora_store and is_two_stream_active(hidden_states)):
return get_original_bf16_moe_lora_func()(
dispatch_output, quant_info, runner_config, lora_info
)
# ---- two-stream fast path ----
from sglang.jit_kernel.trtllm_lora_temp import trtllm_bf16_routed_moe_lora
from sglang.jit_kernel.trtllm_lora_temp.topk_pack import fused_pack_topk
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
merged_experts_fused_moe_lora_add,
)
from sglang.srt.distributed import get_tp_group
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.layers.dp_attention import is_allocation_symmetric
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
get_activation_type,
)
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
from sglang.srt.layers.moe.utils import RoutingMethodType
assert (
runner_config.activation == "silu" and runner_config.is_gated
), "experimental_sgl_trtllm BF16 LoRA currently supports the gated SwiGLU path only."
topk_output = dispatch_output.topk_output
assert TopKOutputChecker.format_is_standard(topk_output)
assert runner_config.top_k is not None
topk_ids = topk_output.topk_ids
topk_weights = topk_output.topk_weights
token_lora_mapping = lora_info.token_lora_mapping
fused_lora_routing_cache: dict = {}
inter = runner_config.intermediate_size_per_partition
side_stream = get_lora_side_stream()
gate_up_delta = hidden_states.new_empty(
(hidden_states.shape[0], runner_config.top_k, 2 * inter)
)
def _run_gate_up_lora():
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=runner_config.num_local_experts,
intermediate_buffer=gate_up_lora_intermediate,
)
# O1-bf16 fork: gate_up shrink/expand on the side stream, concurrent with the
# bf16 op's routing + permute + gate_up GEMM below. The op waits on lora_event
# right before its activation kernel (the only consumer of gate_up_delta).
lora_event = torch.cuda.Event()
# Hoist every side-chain allocation onto the MAIN stream (cuda-graph allocator
# safety -- see the "routing" stage in virtual_experts.py): pre-warm the routing
# cache and pre-allocate the shrink intermediate here, so the side-stream block
# below launches kernels only. Without this, the routing tensors + shrink
# intermediate get allocated inside the side-stream context during cuda-graph
# capture, where cross-stream tracking is off -> pool blocks reused with no graph
# edge -> '!!!!' decode corruption at max-loras>=2 (matches the fp8/fp4 fix).
merged_experts_fused_moe_lora_add(
output=gate_up_delta,
hidden_states=hidden_states,
lora_a=lora_info.gate_up_lora_a_weights,
lora_b=lora_info.gate_up_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=lora_info.experts_shared_outer_loras,
experts_shared_outer_loras_b=False,
routing_cache=fused_lora_routing_cache,
stage="routing",
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=runner_config.num_local_experts,
)
gate_up_lora_intermediate = hidden_states.new_empty(
(
hidden_states.shape[0],
topk_ids.shape[1],
lora_info.gate_up_lora_a_weights.shape[2],
)
)
side_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side_stream):
_run_gate_up_lora()
lora_event.record()
activation_lora_input = torch.empty(
(hidden_states.shape[0], runner_config.top_k, inter),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
packed_topk_ids = getattr(topk_output, "packed_topk_ids", None)
if packed_topk_ids is None:
packed_topk_ids = fused_pack_topk(
topk_ids=topk_ids,
topk_weights=topk_weights,
)
routing_method_type = runner_config.routing_method_type
if routing_method_type is None:
routing_method_type = RoutingMethodType.Default
elif routing_method_type == RoutingMethodType.DeepSeekV3:
routing_method_type = RoutingMethodType.TopK
with use_symmetric_memory(get_tp_group(), disabled=not is_allocation_symmetric()):
direct_down_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
# Keep the event alive through cuda-graph capture so the captured wait inside
# the bf16 op isn't torn down before instantiation (eager relies on deferred destroy).
if torch.cuda.is_current_stream_capturing():
_LORA_OVERLAP_EVENTS.append(lora_event)
lora_ready_handle = lora_event.cuda_event
output = trtllm_bf16_routed_moe_lora(
topk_ids=packed_topk_ids,
routing_bias=None,
hidden_states=hidden_states,
gemm1_weights=quant_info.gemm1_weights,
gemm2_weights=quant_info.gemm2_weights,
gate_up_lora_delta=gate_up_delta,
activation_lora_input=activation_lora_input,
num_experts=quant_info.global_num_experts,
top_k=runner_config.top_k,
intermediate_size=inter,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=runner_config.num_local_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
routing_method_type=routing_method_type,
do_finalize=True,
output=direct_down_output,
activation_type=get_activation_type(
runner_config.activation, is_gated=runner_config.is_gated
),
lora_ready_event=lora_ready_handle,
# Down-LoRA/finalize overlap intentionally NOT wired (gemm2_done_event=0):
# bench-verified net-neutral-to-negative on FP8/FP4 and corrupts the base
# decode path under cuda-graph replay. Serial down-LoRA below.
gemm2_done_event=0,
)
merged_experts_fused_moe_lora_add(
output=output,
hidden_states=activation_lora_input.view(-1, inter),
lora_a=lora_info.down_lora_a_weights,
lora_b=lora_info.down_lora_b_weights,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=token_lora_mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=lora_info.experts_shared_outer_loras,
routing_cache=fused_lora_routing_cache,
fuse_add_to_output=False,
fuse_sum_all_reduce=True,
use_direct_expand_add=lora_info.max_lora_rank <= 64,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=runner_config.num_local_experts,
)
return StandardCombineInput(hidden_states=output)
@@ -0,0 +1,33 @@
"""Registers the ``experimental_sgl_trtllm`` MoE fused-func.
``MoeRunner.__init__`` requires a registered fused-func at CONSTRUCTION time even
for the LoRA case, because LoRA is attached *after* the MoE layer is built (so
``lora_enabled`` is False inside ``MoeRunner.__init__``). At run time the runner
skips this for the LoRA path; the no-LoRA path delegates entirely to the upstream
flashinfer_trtllm dispatch (all quant types), so no-LoRA is identical to the stock backend.
Registration fires at model-build time via a one-line import of this module in
``moe_runner/flashinfer_trtllm.py`` (the module already imported there for the
trtllm weight-prep). Keeping the dispatch body here keeps that file otherwise
pristine; the sgl FP8 LoRA dispatch lives in ``sgl_fp8_moe.py`` (used only by the LoRA path).
"""
from sglang.srt.layers.moe.moe_runner.base import register_fused_func
@register_fused_func("none", "experimental_sgl_trtllm")
def fused_experts_none_to_experimental_sgl_trtllm(
dispatch_output, quant_info, runner_config
):
# No-LoRA on the experimental_sgl_trtllm backend == upstream flashinfer_trtllm for EVERY
# quant type (FP8 / NVFP4 / bf16). When LoRA is disabled the runner calls this fused-func,
# so delegating entirely to upstream keeps the no-LoRA path byte-identical to the stock
# backend. The new sgl kernels (sgl_fp8_moe, trtllm_*_routed_moe_lora) run ONLY on the LoRA
# dispatch (lora_dispatch.py), never here.
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
fused_experts_none_to_flashinfer_trtllm,
)
return fused_experts_none_to_flashinfer_trtllm(
dispatch_output, quant_info, runner_config
)
@@ -0,0 +1,248 @@
"""Copy of upstream flashinfer-trtllm FP8 MoE dispatch, wired to experimental_sgl_trtllm_moe
block-scale wrappers (LoRA-capable) so moe_runner/flashinfer_trtllm.py stays pristine. Body is
verbatim from upstream; helper imports are call-time (cycle-safe); two FP8 wrappers shadowed.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
if TYPE_CHECKING:
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
FlashInferTrtllmFp8MoeQuantInfo,
)
from sglang.srt.layers.moe.token_dispatcher.standard import (
StandardCombineInput,
StandardDispatchOutput,
)
def fused_experts_fp8_sgl(
dispatch_output: StandardDispatchOutput,
quant_info: FlashInferTrtllmFp8MoeQuantInfo,
runner_config: MoeRunnerConfig,
use_routed_topk: bool = False,
) -> StandardCombineInput:
# Lazy (call-time) imports so this module never triggers the flashinfer_trtllm
# <-> quantization import cycle at load time.
from flashinfer.fused_moe import Fp8QuantizationType
from sglang.jit_kernel.trtllm_lora_temp.topk_pack import fused_pack_topk
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
get_tp_group,
is_allocation_symmetric,
next_power_of_2,
per_token_group_quant_fp8,
scaled_fp8_quant,
trtllm_fp8_per_tensor_scale_moe_wrapper,
use_symmetric_memory,
)
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
from sglang.srt.layers.moe.utils import RoutingMethodType
from sglang.srt.lora.trtllm_lora_temp.experimental_sgl_trtllm_moe import (
sgl_trtllm_fp8_block_scale_moe_wrapper as trtllm_fp8_block_scale_moe_wrapper,
)
from sglang.srt.lora.trtllm_lora_temp.experimental_sgl_trtllm_moe import (
sgl_trtllm_fp8_block_scale_routed_moe_wrapper as trtllm_fp8_block_scale_routed_moe_wrapper,
)
_SUPPORTED_FP8_ACTIVATIONS = {"silu", "relu2"}
assert runner_config.activation in _SUPPORTED_FP8_ACTIVATIONS, (
f"Only {_SUPPORTED_FP8_ACTIVATIONS} are supported for FP8 MoE, "
f"got '{runner_config.activation}'."
)
assert not runner_config.no_combine, "no_combine is not supported for flashinfer."
hidden_states = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
if TopKOutputChecker.format_is_bypassed(topk_output):
router_logits = topk_output.router_logits
topk_config = topk_output.topk_config
correction_bias = (
None
if topk_config.correction_bias is None
else topk_config.correction_bias.to(hidden_states.dtype)
)
else:
router_logits = None
topk_config = None
correction_bias = None
routing_method_type = quant_info.routing_method_type
fp8_quantization_type = (
Fp8QuantizationType.MxFp8
if quant_info.use_mxfp8
else Fp8QuantizationType.DeepSeekFp8
)
use_shuffled_weight = quant_info.use_mxfp8
if quant_info.block_quant:
assert quant_info.weight_block_k is not None
assert quant_info.w13_weight_scale_inv is not None
assert quant_info.w2_weight_scale_inv is not None
if quant_info.use_mxfp8:
assert quant_info.weight_block_k == 32
from flashinfer import mxfp8_quantize
a_q, a_sf = mxfp8_quantize(hidden_states, False)
# FlashInfer TRT-LLM MxFP8 expects token-major activation scales:
# [num_tokens, hidden_size // 32] (no transpose).
a_sf_t = a_sf.view(torch.uint8).reshape(hidden_states.shape[0], -1)
else:
a_q, a_sf = per_token_group_quant_fp8(
hidden_states, quant_info.weight_block_k
)
a_sf_t = a_sf.t().contiguous()
# Allocate output inside symmetric memory context
with use_symmetric_memory(
get_tp_group(), disabled=not is_allocation_symmetric()
):
symm_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
# Move kernel call outside context manager to avoid graph breaks
# during torch.compile for piecewise cuda graph.
# Use custom op wrapper for torch.compile compatibility.
if use_routed_topk:
assert (
runner_config.top_k is not None
), "runner_config.top_k is required for flashinfer_trtllm_routed."
assert TopKOutputChecker.format_is_standard(topk_output)
packed_topk_ids = fused_pack_topk(
topk_ids=topk_output.topk_ids,
topk_weights=topk_output.topk_weights,
)
output = trtllm_fp8_block_scale_routed_moe_wrapper(
topk_ids=packed_topk_ids,
routing_bias=None,
hidden_states=a_q,
hidden_states_scale=a_sf_t,
gemm1_weights=quant_info.w13_weight,
gemm1_weights_scale=quant_info.w13_weight_scale_inv,
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale_inv,
num_experts=quant_info.global_num_experts,
top_k=runner_config.top_k,
n_group=None,
topk_group=None,
intermediate_size=quant_info.intermediate_size,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
routing_method_type=(
RoutingMethodType.TopK
if routing_method_type == RoutingMethodType.DeepSeekV3
else routing_method_type
),
use_shuffled_weight=use_shuffled_weight,
tune_max_num_tokens=next_power_of_2(a_q.shape[0]),
fp8_quantization_type=int(fp8_quantization_type),
activation_type=quant_info.activation_type,
)
else:
assert TopKOutputChecker.format_is_bypassed(topk_output)
output = trtllm_fp8_block_scale_moe_wrapper(
routing_logits=router_logits,
routing_bias=correction_bias,
hidden_states=a_q,
hidden_states_scale=a_sf_t,
gemm1_weights=quant_info.w13_weight,
gemm1_weights_scale=quant_info.w13_weight_scale_inv,
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale_inv,
num_experts=quant_info.global_num_experts,
top_k=topk_config.top_k,
n_group=topk_config.num_expert_group,
topk_group=topk_config.topk_group,
intermediate_size=quant_info.intermediate_size,
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
routing_method_type=routing_method_type,
use_shuffled_weight=use_shuffled_weight,
tune_max_num_tokens=next_power_of_2(a_q.shape[0]),
fp8_quantization_type=int(fp8_quantization_type),
activation_type=quant_info.activation_type,
)
# TODO: Once https://github.com/flashinfer-ai/flashinfer/issues/2703 is fixed, pass output to moe kernel and remove this copy.
symm_output.copy_(output)
output = symm_output
else:
assert TopKOutputChecker.format_is_bypassed(topk_output)
assert quant_info.w13_input_scale is not None
assert quant_info.output1_scales_scalar is not None
assert quant_info.output1_scales_gate_scalar is not None
assert quant_info.output2_scales_scalar is not None
a_q, _ = scaled_fp8_quant(hidden_states, quant_info.w13_input_scale)
routing_bias_cast = (
None if correction_bias is None else correction_bias.to(torch.bfloat16)
)
# Allocate output inside symmetric memory context
with use_symmetric_memory(
get_tp_group(), disabled=not is_allocation_symmetric()
):
symm_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=torch.bfloat16,
device=hidden_states.device,
)
# Move kernel call outside context manager to avoid graph breaks
# during torch.compile for piecewise cuda graph.
# Use custom op wrapper for torch.compile compatibility.
router_logits = router_logits.to(torch.bfloat16)
output = trtllm_fp8_per_tensor_scale_moe_wrapper(
routing_logits=router_logits,
routing_bias=routing_bias_cast,
hidden_states=a_q,
gemm1_weights=quant_info.w13_weight,
output1_scales_scalar=quant_info.output1_scales_scalar,
output1_scales_gate_scalar=quant_info.output1_scales_gate_scalar,
gemm2_weights=quant_info.w2_weight,
output2_scales_scalar=quant_info.output2_scales_scalar,
num_experts=quant_info.global_num_experts,
top_k=topk_config.top_k,
n_group=topk_config.num_expert_group,
topk_group=topk_config.topk_group,
intermediate_size=int(quant_info.w2_weight.shape[2]),
local_expert_offset=quant_info.local_expert_offset,
local_num_experts=quant_info.local_num_experts,
routed_scaling_factor=(
runner_config.routed_scaling_factor
if runner_config.routed_scaling_factor is not None
else 1.0
),
use_routing_scales_on_input=False,
routing_method_type=routing_method_type,
tune_max_num_tokens=next_power_of_2(a_q.shape[0]),
activation_type=quant_info.activation_type,
)
symm_output.copy_(output)
output = symm_output
return StandardCombineInput(hidden_states=output)
@@ -0,0 +1,123 @@
"""Overlap the MoE shared-expert add with the down-LoRA shrink (gemm A).
In the Qwen dual-stream decode path (``Qwen2MoeSparseMoeBlock.forward_normal_dual_stream``)
the shared expert runs on the main stream while the routed experts + trtllm LoRA
MoE run on the alt stream; ``final_hidden_states += shared_output`` then runs on
the main stream only after the WHOLE alt-stream chain (base MoE -> finalize ->
down-LoRA shrink -> down-LoRA expand) joins — putting a ~2us elementwise add at
the tail of every MoE layer's critical path.
The add's real dependency is only the base-MoE finalize (the trtllm op with
``do_finalize=True`` writing the output buffer): the down-LoRA shrink writes a
separate intermediate, and the down-LoRA expand atomic-adds into the same output
buffer, so addition order is commutative — the only hard constraint is that the
non-atomic shared add and the expand must not run CONCURRENTLY on the buffer.
(The cross-rank ``tensor_model_parallel_all_reduce`` happens after all of this
at the model layer, unchanged.)
Protocol (gated by ``SGLANG_OPT_LORA_SHARED_ADD_OVERLAP``):
1. The model layer stages ``(shared_output, producer_stream)`` via
:func:`stage_shared_expert_add` after computing the shared expert and before
forking the routed experts to the alt stream.
2. The LoRA dispatch, right after the trtllm op returns (finalize done), calls
:func:`maybe_overlap_staged_shared_add(output)`: it records ``base_ready`` on
the current (alt) stream, enqueues ``wait(base_ready); output += shared;
record(add_done)`` on the producer (main) stream — main-stream program order
already guarantees ``shared_output`` is ready there — and returns ``add_done``.
3. The down-LoRA ``merged_experts_fused_moe_lora_add`` waits on ``add_done``
right before launching the expand kernel, so the shrink (+ stage-B routing)
overlaps the add and the expand never races it.
4. After the dual-stream join the model layer calls
:func:`unstage_shared_expert_add`; if the dispatch never consumed the staging
(prefill / non-virtual-store / fallback paths) it gets the tensor back and
performs the original add itself — byte-identical fallback behavior.
The state is a single slot: MoE layers run sequentially within one scheduler
process, and the stage/consume pair lives within a single layer forward.
"""
from typing import Optional, Tuple
import torch
from sglang.srt.lora.trtllm_lora_temp.environ import lora_envs
_PENDING: Optional[Tuple[torch.Tensor, torch.cuda.Stream]] = None
# Keep events recorded during cuda-graph capture alive so the captured
# cross-stream waits aren't torn down before graph instantiation (same pattern
# as moe_overlap._LORA_OVERLAP_EVENTS). Eager runs rely on deferred destroy.
_SHARED_ADD_EVENTS: list = []
def shared_add_overlap_enabled() -> bool:
return lora_envs.SGLANG_OPT_LORA_SHARED_ADD_OVERLAP.get()
def stage_shared_expert_add(
shared_output: torch.Tensor, producer_stream: torch.cuda.Stream
) -> None:
"""Stage the shared-expert output for the LoRA dispatch to add.
``producer_stream`` is the stream ``shared_output`` was computed on (the
main stream); the overlapped add is enqueued there so its data dependency
on the shared expert is carried by stream program order.
"""
global _PENDING
_PENDING = (shared_output, producer_stream)
def unstage_shared_expert_add() -> Optional[torch.Tensor]:
"""Reclaim a staged-but-unconsumed shared add (fallback paths).
Returns the staged tensor if the dispatch did NOT consume it (the model
layer must then do the add itself), or None if it was consumed (the add is
already enqueued on the producer stream).
"""
global _PENDING
if _PENDING is None:
return None
shared_output, _ = _PENDING
_PENDING = None
return shared_output
def maybe_overlap_staged_shared_add(output: torch.Tensor) -> Optional[torch.cuda.Event]:
"""Enqueue the staged shared-expert add overlapped with the down-LoRA shrink.
Call from the LoRA dispatch right after the base-MoE finalize has been
enqueued on the current stream with ``output`` fully written. Returns the
``add_done`` event the down-LoRA expand must wait on before atomic-adding
into ``output``, or None when nothing was staged.
"""
global _PENDING
if _PENDING is None:
return None
shared_output, producer_stream = _PENDING
current_stream = torch.cuda.current_stream()
if producer_stream == current_stream:
# Single-stream caller: nothing to overlap. Leave the staging in place
# so the model layer reclaims it and does the add as before.
return None
if torch.cuda.is_current_stream_capturing():
# The cross-stream producer-stream add_ (ordered via base_ready/add_done
# events) is NOT cuda-graph-capture-safe: it corrupts `output` on replay.
# Fall back to the serial caller-side add -- leave the staging so the model
# layer reclaims it via unstage_shared_expert_add and adds shared_output
# after current_stream.wait_stream(alt_stream).
return None
_PENDING = None
base_ready = torch.cuda.Event()
base_ready.record(current_stream)
add_done = torch.cuda.Event()
with torch.cuda.stream(producer_stream):
producer_stream.wait_event(base_ready)
output.add_(shared_output)
add_done.record(producer_stream)
if torch.cuda.is_current_stream_capturing():
_SHARED_ADD_EVENTS.append(base_ready)
_SHARED_ADD_EVENTS.append(add_done)
return add_done
@@ -0,0 +1,234 @@
"""Rank-specialized LoRA-B expand for virtual-expert LoRA.
The kernel here was originally a chunk in ``lora/triton_ops/virtual_experts.py``.
It is rank-specialized: the ``R`` dimension (LoRA rank) is a triton
``constexpr``, so each rank value used at runtime gets its own JIT-compiled
specialization (R=16, R=32, R=64 are all supported up to the ``R <= 64`` assert,
with no perf interaction between them — each gets its own kernel).
Called from :mod:`sglang.kernels.ops.moe.virtual_experts` when
``use_direct_expand_add=True`` (the trtllm-lora path uses this when
``max_lora_rank <= 64``); the generic ``invoke_fused_moe_kernel`` is used
when that flag is False (incl. ranks above 64).
"""
from typing import Any
import torch
import triton
import triton.language as tl
from sglang.srt.lora.trtllm_lora_temp.environ import lora_envs
@triton.jit
def _moe_lora_expand_add_kernel(
# Pointers
a_ptr, # [num_tokens * top_k, rank]
b_ptr, # [num_virtual_experts, N, rank]
c_ptr, # [num_tokens, N]
topk_weights_ptr,
sorted_token_ids_ptr,
expert_ids_ptr,
num_tokens_post_padded_ptr,
# Dimensions
N,
R: tl.constexpr,
num_valid_tokens,
# Strides
stride_am,
stride_ar,
stride_be,
stride_bn,
stride_br,
stride_cm,
stride_cn,
# Constexprs
router_topk: tl.constexpr,
MUL_ROUTED_WEIGHT: tl.constexpr,
FUSE_SUM_ALL_REDUCE: tl.constexpr,
BLOCK_SIZE_M: tl.constexpr,
BLOCK_SIZE_N: tl.constexpr,
BLOCK_SIZE_R: tl.constexpr,
GROUP_SIZE_M: tl.constexpr,
GATED_A_HALF: tl.constexpr,
):
"""Rank-specialized LoRA-B expand for virtual-expert LoRA.
``GATED_A_HALF`` > 0 enables the gate/up split for a gated (SwiGLU) gate_up
LoRA: the intermediate (A) has ``2*R`` columns (gate-shrink ``[0:R]`` then
up-shrink ``[R:2R]``) and the output has ``2*GATED_A_HALF`` columns (gate
then up). Output tiles in the up half (column >= ``GATED_A_HALF``) read the
up-shrink columns ``[R:2R]`` instead of ``[0:R]``. ``GATED_A_HALF`` must be
a multiple of ``BLOCK_SIZE_N`` so no tile straddles the gate/up boundary.
``GATED_A_HALF == 0`` is the non-gated path (read ``[0:R]`` for all tiles).
"""
pid = tl.program_id(0)
num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
num_pid_m = tl.cdiv(num_tokens_post_padded, BLOCK_SIZE_M)
num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
num_pid_in_group = GROUP_SIZE_M * num_pid_n
group_id = pid // num_pid_in_group
first_pid_m = group_id * GROUP_SIZE_M
group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
pid_n = (pid % num_pid_in_group) // group_size_m
if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded:
return
offs_token_id = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
offs_token = tl.load(sorted_token_ids_ptr + offs_token_id).to(tl.int64)
token_mask = offs_token < num_valid_tokens
off_expert = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
if off_expert == -1:
if not FUSE_SUM_ALL_REDUCE:
offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)
c_ptrs = (
c_ptr + offs_token[:, None] * stride_cm + offs_n[None, :] * stride_cn
)
c_mask = token_mask[:, None] & (offs_n[None, :] < N)
zeros = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=c_ptr.dtype.element_ty)
tl.store(c_ptrs, zeros, mask=c_mask)
return
offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)
offs_r = tl.arange(0, BLOCK_SIZE_R).to(tl.int64)
rank_mask = offs_r < R
# Gated gate_up split: the up-half output tiles read up-shrink (A columns [R:2R]); gate-half
# tiles read gate-shrink (A columns [0:R]). GATED_A_HALF == 0 -> always read [0:R] (non-gated).
a_col = offs_r
if GATED_A_HALF > 0:
a_col = offs_r + tl.where(pid_n * BLOCK_SIZE_N >= GATED_A_HALF, R, 0)
a = tl.load(
a_ptr + offs_token[:, None] * stride_am + a_col[None, :] * stride_ar,
mask=token_mask[:, None] & rank_mask[None, :],
other=0.0,
)
b = tl.load(
b_ptr
+ off_expert * stride_be
+ offs_n[None, :] * stride_bn
+ offs_r[:, None] * stride_br,
mask=(offs_n[None, :] < N) & rank_mask[:, None],
other=0.0,
)
accumulator = tl.dot(a, b, out_dtype=tl.float32)
if MUL_ROUTED_WEIGHT:
moe_weight = tl.load(topk_weights_ptr + offs_token, mask=token_mask, other=0.0)
accumulator *= moe_weight[:, None]
if FUSE_SUM_ALL_REDUCE:
offs_token_out = offs_token // router_topk
else:
offs_token_out = offs_token
c_ptrs = c_ptr + offs_token_out[:, None] * stride_cm + offs_n[None, :] * stride_cn
c_mask = token_mask[:, None] & (offs_n[None, :] < N)
if FUSE_SUM_ALL_REDUCE:
tl.atomic_add(c_ptrs, accumulator.to(c_ptr.dtype.element_ty), mask=c_mask)
else:
tl.store(c_ptrs, accumulator.to(c_ptr.dtype.element_ty), mask=c_mask)
def _invoke_moe_lora_expand_add(
intermediate: torch.Tensor,
weight: torch.Tensor,
output: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
sorted_token_ids: torch.Tensor,
expert_ids: torch.Tensor,
num_tokens_post_padded: torch.Tensor,
config: "dict[str, Any]",
mul_routed_weight: bool,
fuse_sum_all_reduce: bool,
force_block_size_n: "int | None" = None,
) -> None:
"""Launch the rank-specialized LoRA-B expand kernel.
``R`` (= ``weight.shape[2]``) up to 64 is supported. ``BLOCK_SIZE_R`` is
set to ``next_power_of_2(R)`` so each rank value pairs with the smallest
tile that covers it (R=16 → BSR=16, R=32 → BSR=32, R=64 → BSR=64).
Triton compiles a separate specialization per (R, BLOCK_SIZE_R) combo
so different ranks don't interfere with each other's perf.
"""
N = weight.shape[1]
R = weight.shape[2]
assert R <= 64, f"direct LoRA expand/add expects rank <= 64, got {R}"
block_size_m = config["BLOCK_SIZE_M"]
# BLOCK_SIZE_N defaults to 128 when N % 128 == 0 (a good N-divisible tile that also keeps
# the gated gate_up split aligned). ``force_block_size_n`` lets a tuner/bench override it;
# down-proj has no gate/up boundary, so any divisor of N is valid there.
if force_block_size_n is not None:
block_size_n = force_block_size_n
else:
block_size_n = 128 if N % 128 == 0 else config["BLOCK_SIZE_N"]
group_size_m = config.get("GROUP_SIZE_M", 1)
block_size_r = triton.next_power_of_2(R)
# gate_up LoRA: the shrink stacks gate_A and up_A, so the intermediate has 2*R columns
# ([0:R] = gate-shrink x@gate_A^T, [R:2R] = up-shrink x@up_A^T). The up output half
# (column >= N/2) must contract the up-shrink [R:2R], not gate_A's [0:R]. Reading [0:R]
# for both halves (the previous hardcode) computed the up delta from gate_A and dropped
# up_A -- wrong whenever gate_A != up_A (the normal independently-trained gate/up case;
# verified >100% rel error vs a PEFT reference on the real Qwen3.5 adapter). The earlier
# "vs cutlass" justification for reading [0:R] was unreliable (the cutlass reference shared
# the same bug). Detect the gated layout from the intermediate width and split in-kernel.
inter_width = intermediate.shape[1]
assert inter_width in (R, 2 * R), (
f"LoRA expand intermediate width must be R ({R}, non-gated) or 2*R "
f"({2 * R}, gated gate_up), got {inter_width}"
)
# Lazy import to avoid the trtllm_moe <-> triton_ops package import cycle at load time.
gated = inter_width == 2 * R
use_gated_split = (
gated and lora_envs.SGLANG_ENABLE_LORA_MOE_GATEUP_GATED_SPLIT.get()
)
gated_a_half = (N // 2) if use_gated_split else 0
if use_gated_split:
assert N % 2 == 0 and (N // 2) % block_size_n == 0, (
f"gated gate_up split needs N/2 ({N // 2}) divisible by BLOCK_SIZE_N "
f"({block_size_n})"
)
grid = (
triton.cdiv(sorted_token_ids.shape[0], block_size_m)
* triton.cdiv(N, block_size_n),
)
_moe_lora_expand_add_kernel[grid](
intermediate,
weight,
output,
topk_weights,
sorted_token_ids,
expert_ids,
num_tokens_post_padded,
N,
R,
topk_ids.numel(),
intermediate.stride(0),
intermediate.stride(1),
weight.stride(0),
weight.stride(1),
weight.stride(2),
output.stride(-2),
output.stride(-1),
router_topk=topk_ids.shape[1],
MUL_ROUTED_WEIGHT=mul_routed_weight,
FUSE_SUM_ALL_REDUCE=fuse_sum_all_reduce,
BLOCK_SIZE_M=block_size_m,
BLOCK_SIZE_N=block_size_n,
BLOCK_SIZE_R=block_size_r,
GROUP_SIZE_M=group_size_m,
GATED_A_HALF=gated_a_half,
num_warps=config.get("num_warps", 4),
num_stages=1,
)
+584
View File
@@ -0,0 +1,584 @@
from dataclasses import dataclass
from enum import Enum
from typing import Iterable, List, Optional, Set, Tuple, Union
import torch
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.utils.hf_transformers_utils import AutoConfig
@dataclass
class MoELoRABatchInfo:
# Per-request segment indptrs used by MoE LoRA routing, shape (bs + 1,).
seg_indptr: torch.Tensor
# Per-request adapter index used by MoE LoRA routing, shape (bs,).
req_to_lora: torch.Tensor
# A mask indicating if lora adapter is enabled. Shape (num_loras,)
adapter_enabled: torch.Tensor
# A mapping of which lora adapter is used for each token. Shape (num_tokens,)
# If a token has no lora adapter, the value is -1.
token_lora_mapping: torch.Tensor
@dataclass
class LoRABatchInfo:
# The forward mode is using CUDA Graph.
use_cuda_graph: bool
# Batch size
bs: int
# Number of segments. For triton backend, it is equal to batch size.
num_segments: int
# 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,)
weight_indices: torch.Tensor
# ranks of each lora adapter, in shape (lora_num,)
lora_ranks: torch.Tensor
# scaling of each lora adapter, in shape (lora_num,)
scalings: torch.Tensor
# Maximum segment length of current batch
max_len: Optional[int]
# Lengths of each segments in shape (num_segments,)
seg_lens: Optional[torch.Tensor]
# The logical (re)ordering of input rows (tokens), in shape (num_tokens,)
permutation: Optional[torch.Tensor]
# Total number of tokens this batch info expects (host-side int).
# Used by lm_head LoRA to validate input shape without GPU sync.
expected_tokens: Optional[int] = None
# CPU-side flag: True when at least one request uses a LoRA adapter.
# Computed from Python lists in prepare_lora_batch to avoid GPU sync.
has_active_lora: bool = False
# Per-request segment indptrs, shape (bs + 1,). Required by MoE virtual
# experts which map tokens to requests regardless of the dense-LoRA
# backend's internal segmentation. For the triton backend these are
# identical to seg_indptr/weight_indices; for csgmv they differ because
# its segments are chunked across adapters.
req_seg_indptr: Optional[torch.Tensor] = None
# Per-request adapter index, shape (bs,).
req_weight_indices: Optional[torch.Tensor] = None
# MoE LoRA batch info
moe_lora_info: Optional[MoELoRABatchInfo] = None
class LoRAType(Enum):
LORA_A = 0
LORA_B = 1
def copy_weight_into_buffer(
buffer_view: torch.Tensor,
weight: torch.Tensor,
) -> None:
"""
Copy a LoRA weight tensor into a destination buffer.
When a pinned CPU source has a dtype mismatch with a device destination,
cast on the destination device instead of doing the conversion on CPU.
"""
if weight.dtype == buffer_view.dtype:
buffer_view.copy_(weight, non_blocking=True)
return
if weight.device.type == "cpu" and buffer_view.device.type != "cpu":
weight = weight.to(device=buffer_view.device, non_blocking=True)
buffer_view.copy_(weight.to(dtype=buffer_view.dtype), non_blocking=True)
def get_hidden_dim(
module_name: str,
config: AutoConfig,
base_model: torch.nn.Module,
layer_idx: int,
lora_added_vocab_size: int = 0,
) -> Tuple[int]:
"""
Given a module_name (might be a stacked name), return the hidden dims of modules' input and output.
"""
if hasattr(base_model, "get_hidden_dim"):
return base_model.get_hidden_dim(module_name, layer_idx)
else:
"""
WARNING: get_hidden_dim() is not defined,
which is used to get the hidden dim for different lora modules
Use the default one, but please check if it is correct for your model.
Please implement the function in the model class if it is not.
You can reference this function in llama.py.
"""
head_dim = getattr(
config, "head_dim", config.hidden_size // config.num_attention_heads
)
if module_name == "qkv_proj":
return config.hidden_size, head_dim * (
config.num_attention_heads + config.num_key_value_heads * 2
)
elif module_name == "o_proj":
o_head_dim = getattr(config, "v_head_dim", None) or head_dim
return (
o_head_dim * config.num_attention_heads,
config.hidden_size,
)
elif module_name == "gate_up_proj":
inter = config.intermediate_size
first_k = getattr(config, "first_k_dense_replace", None)
moe_freq = getattr(config, "moe_layer_freq", 1)
if (
first_k is not None
and layer_idx >= first_k
and layer_idx % moe_freq == 0
):
moe_inter = getattr(config, "moe_intermediate_size", None)
n_shared = getattr(config, "n_shared_experts", None)
if moe_inter is not None and n_shared is not None:
inter = moe_inter * n_shared
return config.hidden_size, inter * 2
elif module_name == "down_proj":
inter = config.intermediate_size
first_k = getattr(config, "first_k_dense_replace", None)
moe_freq = getattr(config, "moe_layer_freq", 1)
if (
first_k is not None
and layer_idx >= first_k
and layer_idx % moe_freq == 0
):
moe_inter = getattr(config, "moe_intermediate_size", None)
n_shared = getattr(config, "n_shared_experts", None)
if moe_inter is not None and n_shared is not None:
inter = moe_inter * n_shared
return inter, config.hidden_size
elif module_name == "fused_qkv_a_proj_with_mqa":
q_lora_rank = getattr(config, "q_lora_rank", None) or 0
kv_lora_rank = config.kv_lora_rank
qk_rope_head_dim = config.qk_rope_head_dim
return (
config.hidden_size,
q_lora_rank + kv_lora_rank + qk_rope_head_dim,
)
elif module_name == "q_b_proj":
return (
config.q_lora_rank,
config.num_attention_heads
* (config.qk_nope_head_dim + config.qk_rope_head_dim),
)
elif module_name == "kv_b_proj":
return (
config.kv_lora_rank,
config.num_attention_heads
* (config.qk_nope_head_dim + config.v_head_dim),
)
elif module_name in DSA_INDEXER_LORA_NAMES:
from sglang.srt.configs.model_config import (
get_dsa_index_head_dim,
get_dsa_index_n_heads,
)
if module_name == "indexer.wq_b":
return (
config.q_lora_rank,
get_dsa_index_n_heads(config) * get_dsa_index_head_dim(config),
)
elif module_name == "indexer.wk":
return config.hidden_size, get_dsa_index_head_dim(config)
else: # indexer.weights_proj
return config.hidden_size, get_dsa_index_n_heads(config)
elif module_name == "gate_up_proj_moe":
moe_inter = (
getattr(config, "moe_intermediate_size", None)
or config.intermediate_size
)
return config.hidden_size, moe_inter * 2
elif module_name == "down_proj_moe":
moe_inter = (
getattr(config, "moe_intermediate_size", None)
or config.intermediate_size
)
return moe_inter, config.hidden_size
elif module_name == "embed_tokens":
# For embedding: input is vocab_size (as embedding lookup), output is hidden_size
# if contain extra tokens will be added; otherwise is 0.
return config.vocab_size + lora_added_vocab_size, config.hidden_size
elif module_name == "lm_head":
# For lm_head: input is hidden_size, output is vocab_size
# if contain extra tokens will be added; otherwise is 0.
return config.hidden_size, config.vocab_size + lora_added_vocab_size
else:
raise NotImplementedError(
"get_hidden_dim not implemented for " + module_name
)
def get_normalized_target_modules(
target_modules: Union[str, Iterable[str]],
) -> set[str]:
"""
Mapping a list of target module name to names of the normalized LoRA weights.
Handles both base module names (e.g., "gate_proj") and prefixed module names (e.g., "feed_forward.gate_proj").
Also handles PEFT shorthand strings like "all-linear" or "all" by returning
{"all"} as a sentinel value. Callers that need a concrete module set
should use :func:`auto_detect_lora_target_modules` to resolve the shorthand
against the loaded base model.
"""
# Handle PEFT shorthand strings — return {"all"} as sentinel.
# Callers can resolve to concrete names via auto_detect_lora_target_modules().
if isinstance(target_modules, str):
if target_modules not in ["all", "all-linear"]:
raise ValueError(
"Only 'all' or 'all-linear' can be used as the string for target module"
)
return {"all"}
params_mapping = {
"q_proj": "qkv_proj",
"k_proj": "qkv_proj",
"v_proj": "qkv_proj",
"gate_proj": "gate_up_proj",
"up_proj": "gate_up_proj",
"out_proj": "out_proj",
"embed_tokens": "embed_tokens",
"vocab_emb": "embed_tokens",
"embeddings": "embed_tokens",
"word_embeddings": "embed_tokens",
"lm_head": "lm_head",
"output": "lm_head",
"unembed_tokens": "lm_head",
"q_a_proj": "fused_qkv_a_proj_with_mqa",
"kv_a_proj_with_mqa": "fused_qkv_a_proj_with_mqa",
# DSA indexer projections are qualified with their parent module name
# because the bare leaf names collide with unrelated modules in other
# models (e.g. DeepSeek-V4 attention `wq_b`, Pixtral vision `wk`).
"wq_b": "indexer.wq_b",
"wk": "indexer.wk",
"weights_proj": "indexer.weights_proj",
}
result = set()
for name in target_modules:
base_name = name.split(".")[-1]
normalized_name = params_mapping.get(base_name, base_name)
result.add(normalized_name)
return result
def get_stacked_multiply(
module_name: str, base_model: Optional[torch.nn.Module] = None
) -> int:
"""
Mapping a lora module name to its magnification at output dimension.
Models can override via a get_stacked_multiply(module_name) method.
"""
if base_model is not None and hasattr(base_model, "get_stacked_multiply"):
return base_model.get_stacked_multiply(module_name)
stacked_rank = {
"qkv_proj": 3,
"in_proj_qkvz": 4, # GDN packed input projection
"gate_up_proj": 2,
"gate_up_proj_moe": 2,
"in_proj": 2,
"fused_qkv_a_proj_with_mqa": 2,
}
return stacked_rank[module_name] if module_name in stacked_rank else 1
def get_target_module_name(full_module_name: str, target_modules: Set[str]) -> str:
"""
Get the target module name in target_modules that can match full_module_name.
If there is a target module name in target_modules that can match full_module_name, return this name
Else raise ValueError.
When multiple target modules match (e.g. both "up_proj" and "gate_up_proj"
are substrings), the longest match wins to avoid ambiguity.
"""
best = None
for target_module in target_modules:
if target_module in full_module_name:
if best is None or len(target_module) > len(best):
best = target_module
if best is not None:
return best
raise ValueError(
f"Cannot find target module name for {full_module_name} in {target_modules}"
)
EMBEDDING_NAMES = ["embed_tokens", "lm_head"]
ROW_PARALLELISM_LINEAR_LORA_NAMES = ["o_proj", "out_proj", "down_proj", "down_proj_moe"]
DSA_INDEXER_LORA_NAMES = frozenset(
{"indexer.wq_b", "indexer.wk", "indexer.weights_proj"}
)
REPLICATED_LINEAR_LORA_NAMES = [
"fused_qkv_a_proj_with_mqa",
"fc1_latent_proj",
"fc2_latent_proj",
*DSA_INDEXER_LORA_NAMES,
]
# Normalized module names that the LoRA system fully supports
# (i.e. get_hidden_dim, init_buffers, and init_lora_modules can handle them).
_KNOWN_LORA_TARGET_MODULES = frozenset(
{
"qkv_proj",
"o_proj",
"out_proj",
"in_proj",
"in_proj_qkvz",
"up_proj",
"gate_up_proj",
"down_proj",
"fc1_latent_proj",
"fc2_latent_proj",
"embed_tokens",
"lm_head",
"fused_qkv_a_proj_with_mqa",
"q_b_proj",
"kv_b_proj",
}
| DSA_INDEXER_LORA_NAMES
)
def auto_detect_lora_target_modules(model: "torch.nn.Module") -> set:
"""Discover LoRA-compatible modules by inspecting the base model.
Walks the model graph and returns the set of *normalized* target-module
names that (a) actually exist in the model and (b) the LoRA memory pool
can handle. This is used to resolve PEFT shorthands like ``"all-linear"``
without requiring the user to enumerate modules on the CLI.
"""
from sglang.srt.layers.linear import LinearBase
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
from sglang.srt.layers.vocab_parallel_embedding import (
ParallelLMHead,
VocabParallelEmbedding,
)
raw_names: set = set()
dsa_indexer_leaf_names = {
target_name.split(".")[-1] for target_name in DSA_INDEXER_LORA_NAMES
}
for name, module in model.named_modules():
if isinstance(module, FusedMoE):
raw_names.add("gate_up_proj")
raw_names.add("down_proj")
elif isinstance(module, ParallelLMHead):
raw_names.add("lm_head")
elif isinstance(module, VocabParallelEmbedding):
raw_names.add("embed_tokens")
elif isinstance(module, LinearBase):
parts = name.split(".")
leaf_name = parts[-1]
parent_qualified_name = ".".join(parts[-2:])
if parent_qualified_name in DSA_INDEXER_LORA_NAMES:
raw_names.add(parent_qualified_name)
elif leaf_name in dsa_indexer_leaf_names:
# Bare DSA indexer leaf names are ambiguous across model
# families. Only auto-detect them when the actual module path
# proves they are under an `indexer` parent.
continue
else:
raw_names.add(leaf_name)
normalized = get_normalized_target_modules(raw_names)
result = normalized & _KNOWN_LORA_TARGET_MODULES
# Allow models to declare additional LoRA-compatible modules that
# cannot be auto-discovered or need to bypass normalization
# (e.g. Mamba in_proj, non-gated up_proj).
if hasattr(model, "supported_lora_modules"):
result.update(set(model.supported_lora_modules) & _KNOWN_LORA_TARGET_MODULES)
return result
def get_lm_head_lora_b_shard_size(output_dim: int, shard_indices=None) -> int:
"""Get the LoRA B output dimension for lm_head, accounting for TP.
lm_head is column-parallel, so its LoRA B must be sharded along the
vocab dimension to match the base output. When shard_indices is
provided, the returned size reflects the base model's actual per-rank
vocab partition.
Args:
output_dim: Full (unsharded) output dimension (vocab_size).
shard_indices: VocabParallelEmbeddingShardIndices from the base
ParallelLMHead layer. When provided, returns the per-rank
org vocab size from the base model's actual sharding.
"""
if shard_indices is not None:
return shard_indices.num_org_elements
return output_dim
def generate_sequence_lengths(
forward_batch: ForwardBatch, device: Optional[torch.device] = None
) -> torch.Tensor:
device = torch.get_default_device() if device is None else device
with torch.device(device):
if forward_batch.forward_mode.is_decode():
seg_lens = torch.ones(forward_batch.batch_size, dtype=torch.int32)
elif forward_batch.forward_mode.is_target_verify():
seg_lens = torch.full(
size=(forward_batch.batch_size,),
fill_value=forward_batch.spec_info.draft_token_num,
dtype=torch.int32,
)
elif forward_batch.forward_mode.is_extend():
seg_lens = (
forward_batch.extend_seq_lens
if forward_batch.extend_seq_lens.device == device
else torch.tensor(
forward_batch.extend_seq_lens_cpu,
dtype=torch.int32,
)
)
else:
raise ValueError(f"Unsupported forward mode: {forward_batch.forward_mode}")
return seg_lens
def get_lm_head_pruned_lens(
forward_batch: ForwardBatch,
) -> Optional[List[int]]:
"""
Compute per-sequence pruned lengths for lm_head LoRA.
Returns a list of pruned lengths (one per sequence) if pruning applies,
or None if lm_head pruning is not applicable for this batch.
Pruning rules:
- Extend without logprobs: 1 token per sequence
- Extend with logprobs: max(extend_len - logprob_start_len, 1) per sequence
- Decode / target_verify / draft_extend_v2: no pruning
IMPORTANT: This must stay in sync with LogitsProcessor._get_pruned_states()
in sglang/srt/layers/logits_processor.py, which determines how many tokens
per sequence are passed to lm_head. If the pruning conditions or lengths
there change, this function must be updated to match, otherwise the
lm_head LoRA will operate on incorrectly shaped inputs.
"""
lm_head_pruning = (
forward_batch.forward_mode.is_extend()
and not forward_batch.forward_mode.is_target_verify()
and not forward_batch.forward_mode.is_draft_extend_v2()
)
if not lm_head_pruning:
return None
if forward_batch.return_logprob:
pruned_lens = []
for ext_len, start_len in zip(
forward_batch.extend_seq_lens_cpu,
forward_batch.extend_logprob_start_lens_cpu,
):
pruned_lens.append(1 if ext_len == start_len else ext_len - start_len)
else:
pruned_lens = [1] * forward_batch.batch_size
return pruned_lens
def merge_and_chunk_segments(
weight_indices: list[int],
pruned_lens: List[int],
chunk_size: int,
) -> Tuple[List[int], List[int]]:
"""
Merge consecutive same-adapter sequences and chunk at chunk_size boundaries.
Merges consecutive sequences that use the same adapter into single
segments, splitting any segment that exceeds chunk_size.
Args:
weight_indices: Per-sequence adapter indices.
pruned_lens: Per-sequence pruned token counts.
chunk_size: Maximum segment length before splitting.
Returns:
(seg_weight_indices, seg_lens): Merged and chunked segments.
"""
seg_weight_indices: List[int] = []
seg_lens: List[int] = []
for wi, pl in zip(weight_indices, pruned_lens):
if seg_weight_indices and seg_weight_indices[-1] == wi:
seg_lens[-1] += pl
else:
seg_weight_indices.append(wi)
seg_lens.append(pl)
# Split the last segment if it exceeds chunk_size
while seg_lens[-1] > chunk_size:
remainder = seg_lens[-1] - chunk_size
seg_lens[-1] = chunk_size
seg_weight_indices.append(wi)
seg_lens.append(remainder)
return seg_weight_indices, seg_lens
def build_lm_head_pass_segments(
weight_indices: List[int],
pruned_lens: List[int],
logprobs_chunk_size: int,
) -> List[Tuple[List[int], List[int]]]:
"""
Precompute per-pass segment info for lm_head LoRA logprobs processing.
When LogitsProcessor uses chunked logprobs processing
(process_input_logprobs_by_chunk), pruned hidden states are split into
fixed-size passes. Each pass needs its own segmentation
(weight_indices, seg_lens) so that lm_head LoRA operates on the
correct adapter assignments per pass.
Args:
weight_indices: Per-sequence adapter indices.
pruned_lens: Per-sequence pruned token counts.
logprobs_chunk_size: Fixed pass size used by LogitsProcessor.
Returns:
List of (seg_weight_indices, seg_lens) tuples, one per pass.
"""
# Expand to per-token weight index
token_wi: List[int] = []
for wi, pl in zip(weight_indices, pruned_lens):
token_wi.extend([wi] * pl)
total = len(token_wi)
num_passes = (total + logprobs_chunk_size - 1) // logprobs_chunk_size
result: List[Tuple[List[int], List[int]]] = []
for i in range(num_passes):
start = i * logprobs_chunk_size
end = min((i + 1) * logprobs_chunk_size, total)
# Run-length encode the pass's adapter indices
seg_wi: List[int] = []
seg_lens: List[int] = []
for t in range(start, end):
if seg_wi and seg_wi[-1] == token_wi[t]:
seg_lens[-1] += 1
else:
seg_wi.append(token_wi[t])
seg_lens.append(1)
result.append((seg_wi, seg_lens))
return result