94057c3d3e
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
1213 lines
46 KiB
Python
1213 lines
46 KiB
Python
from typing import Dict, Optional, Union
|
|
|
|
import torch
|
|
import torch.nn.functional as F
|
|
from torch import nn
|
|
|
|
from sglang.srt.distributed import (
|
|
split_tensor_along_last_dim,
|
|
tensor_model_parallel_all_gather,
|
|
tensor_model_parallel_all_reduce,
|
|
)
|
|
from sglang.srt.environ import envs
|
|
from sglang.srt.layers.linear import (
|
|
ColumnParallelLinear,
|
|
MergedColumnParallelLinear,
|
|
QKVParallelLinear,
|
|
ReplicatedLinear,
|
|
RowParallelLinear,
|
|
)
|
|
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
|
from sglang.srt.layers.moe.topk import TopKOutput
|
|
from sglang.srt.layers.moe.utils import should_skip_mlp_all_reduce
|
|
from sglang.srt.layers.vocab_parallel_embedding import (
|
|
ParallelLMHead,
|
|
VocabParallelEmbedding,
|
|
)
|
|
from sglang.srt.lora.backend.base_backend import BaseLoRABackend
|
|
from sglang.srt.lora.utils import LoRABatchInfo, get_lm_head_lora_b_shard_size
|
|
from sglang.srt.runtime_context import get_parallel
|
|
|
|
_SGLANG_EXPERIMENTAL_LORA_OPTI = envs.SGLANG_EXPERIMENTAL_LORA_OPTI.get()
|
|
|
|
|
|
class BaseLayerWithLoRA(nn.Module):
|
|
def __init__(
|
|
self,
|
|
base_layer: nn.Module,
|
|
lora_backend: BaseLoRABackend,
|
|
):
|
|
super().__init__()
|
|
self.base_layer: nn.Module = base_layer
|
|
self.set_lora: bool = False
|
|
self.lora_backend: BaseLoRABackend = lora_backend
|
|
if hasattr(self.base_layer, "weight"):
|
|
self.weight = self.base_layer.weight
|
|
if hasattr(self.base_layer, "bias") and self.base_layer.bias is not None:
|
|
self.bias = self.base_layer.bias
|
|
|
|
def forward(self, x: torch.Tensor):
|
|
return self.base_layer.forward(x)
|
|
|
|
def set_lora_info(self, *args):
|
|
pass
|
|
|
|
def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
pass
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
|
pass
|
|
|
|
|
|
class VocabParallelEmbeddingWithLoRA(BaseLayerWithLoRA):
|
|
"""
|
|
Vocab parallel embedding layer with LoRA support (simplified for TP=1, no extra tokens).
|
|
|
|
For embedding layers: output = base_embedding(x) + lora_B @ lora_A[x]
|
|
where lora_A[x] is direct embedding lookup from lora_A weights.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
base_layer: VocabParallelEmbedding,
|
|
lora_backend: BaseLoRABackend,
|
|
) -> None:
|
|
super().__init__(base_layer, lora_backend)
|
|
self.weight = base_layer.weight
|
|
self.embed_dim = base_layer.embedding_dim
|
|
self.vocab_size = base_layer.org_vocab_size
|
|
self.num_embeddings = base_layer.num_embeddings
|
|
|
|
# Embedding LoRA with TP > 1 keeps weights fully replicated
|
|
# (unsharded) on every rank. This works correctly because the
|
|
# base VocabParallelEmbedding all-reduces its output before the
|
|
# LoRA delta is added, but it means each rank holds the full
|
|
# LoRA A (rank, vocab_size) and LoRA B (embed_dim, rank) tensors,
|
|
# which may cause OOM on large vocabularies or high LoRA ranks.
|
|
#
|
|
# input_scattered mode (DeepSeek-v2 MLA) skips the base
|
|
# all-reduce, making the unsharded LoRA approach mathematically
|
|
# incorrect — a sharded LoRA kernel would be needed.
|
|
if hasattr(base_layer, "tp_size") and base_layer.tp_size > 1:
|
|
from sglang.srt.layers.communicator import get_attn_tp_context
|
|
|
|
assert (
|
|
not get_attn_tp_context().allow_input_scattered
|
|
), "VocabParallelEmbeddingWithLoRA with TP > 1 under input_scattered mode (e.g., DeepSeek-v2 MLA with --enable-attn-tp-input-scattered) is not fully supported and may produce incorrect results. Consider disabling input_scattered or removing embed_tokens from LoRA target modules."
|
|
offsets = [0, self.embed_dim]
|
|
self.output_offset = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device=next(base_layer.parameters()).device,
|
|
)
|
|
self.output_offset_cpu = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device="cpu",
|
|
pin_memory=True,
|
|
)
|
|
|
|
def set_lora_info(
|
|
self,
|
|
new_embeddings_buffer: Optional[torch.Tensor], # For extra tokens
|
|
embedding_A_buffer: torch.Tensor,
|
|
embedding_B_buffer: torch.Tensor,
|
|
):
|
|
"""Set LoRA buffers for embedding layer."""
|
|
self.set_lora = True
|
|
self.new_embeddings_buffer = new_embeddings_buffer
|
|
self.embedding_A_buffer = embedding_A_buffer # (num_loras, rank, vocab_size)
|
|
self.embedding_B_buffer = embedding_B_buffer # (num_loras, embed_dim, rank)
|
|
|
|
def apply_lora(
|
|
self, base_output: torch.Tensor, input_: torch.Tensor, batch_info
|
|
) -> torch.Tensor:
|
|
"""
|
|
Apply LoRA to base embedding output.
|
|
Formula: output = base_output + lora_B @ lora_A_embedding(input_)
|
|
"""
|
|
|
|
# Efficient embedding lookup for LoRA A (already support extra token embedding process)
|
|
lora_a_output = self.run_lora_a_embedding(input_, batch_info)
|
|
|
|
# Apply LoRA B weights using backend
|
|
lora_output = self.lora_backend.run_lora_b_sgemm(
|
|
x=lora_a_output,
|
|
weights=self.embedding_B_buffer,
|
|
output_offset=self.output_offset,
|
|
output_offset_cpu=self.output_offset_cpu,
|
|
base_output=base_output,
|
|
)
|
|
return lora_output
|
|
|
|
def run_lora_a_embedding(
|
|
self, input_: torch.Tensor, batch_info: LoRABatchInfo
|
|
) -> torch.Tensor:
|
|
"""
|
|
Apply LoRA A weights using efficient embedding lookup with CUDA graph support.
|
|
Maps tokens to their corresponding LoRA adapters internally.
|
|
It also includes added/extra token processing.
|
|
"""
|
|
# Efficient embedding lookup for LoRA A (already support extra token embedding process)
|
|
lora_a_output = self.lora_backend.run_lora_a_embedding(
|
|
input_ids=input_,
|
|
weights=self.embedding_A_buffer,
|
|
vocab_size=self.vocab_size,
|
|
extra_embeddings=(
|
|
self.new_embeddings_buffer
|
|
if hasattr(self, "new_embeddings_buffer")
|
|
and self.new_embeddings_buffer is not None
|
|
else None
|
|
),
|
|
)
|
|
|
|
return lora_a_output
|
|
|
|
def extra_token_embedding(
|
|
self, input_: torch.Tensor, base_output: torch.Tensor
|
|
) -> torch.Tensor:
|
|
"""
|
|
Need to impl:
|
|
|
|
Process extra tokens (tokens >= vocab_size) by looking up their embeddings
|
|
from the new_embeddings_buffer and replacing them in base_output.
|
|
|
|
Args:
|
|
input_: (s,) token IDs
|
|
base_output: (s, embed_dim) base embedding output to be modified in-place
|
|
|
|
Returns:
|
|
base_output: (s, embed_dim) modified input base_output (tensor[0,0,0,...]) with extra token embeddings
|
|
"""
|
|
# return base_output
|
|
raise NotImplementedError(
|
|
"Error in sglang/python/sglang/srt/lora/layers.py - VocabParallelEmbeddingWithLoRA \n"
|
|
"Current SGLang codebase did not support tuned lora with extra/added tokens. \n"
|
|
"[TODO]: \n"
|
|
"1. Refer to this commit: https://github.com/yushengsu-thu/sglang/commit/90415211eee8a28a316de262583d4d33fa615d10#diff-191177438bcc223837963de63c005850371f8c8a860acb153b26744b66ecc623 to complete \n"
|
|
"2. And then you need to modified the en/decoder tokenizer - tokenizer_manager.py to support extra_token_embedding in-place. \n"
|
|
)
|
|
|
|
def forward(self, input_: torch.Tensor):
|
|
"""
|
|
Forward pass with LoRA support and CUDA graph compatibility.
|
|
|
|
Extra tokens (tokens >= vocab_size) are now handled efficiently
|
|
in the backend's run_lora_a_embedding method.
|
|
"""
|
|
batch_info = self.lora_backend.batch_info
|
|
|
|
# Get base embedding output
|
|
# For tokens >= vocab_size, base_layer will clamp or handle them
|
|
# We mask them to 0 to avoid out-of-bounds access
|
|
added_tokens_mask = input_ > self.vocab_size - 1
|
|
base_output = self.base_layer.forward(input_.masked_fill(added_tokens_mask, 0))
|
|
|
|
# [TODO] SGLang did not support extra/added token process; thus, self.extra_token_embedding only return original input_ now
|
|
# Extra tokens - It will replace extra token embedding with self.new_embeddings_buffer's emb (Default is 0)
|
|
if (
|
|
hasattr(self, "new_embeddings_buffer")
|
|
and self.new_embeddings_buffer is not None
|
|
):
|
|
base_output = self.extra_token_embedding(input_, base_output)
|
|
|
|
# Apply LoRA if configured
|
|
if self.set_lora:
|
|
# The backend's run_lora_a_embedding now handles both regular
|
|
# and extra tokens efficiently with CUDA graph support
|
|
base_output = self.apply_lora(base_output, input_, batch_info)
|
|
|
|
return base_output
|
|
|
|
def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
# LoRA A weights (rank, vocab_size) are kept unsharded.
|
|
# Each rank does a full embedding lookup; the result is complete
|
|
# on every rank and added to the already all-reduced base output.
|
|
return A
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
|
# LoRA B weights (embedding_dim, rank) are kept unsharded.
|
|
# The base embedding output is all-reduced (full embedding_dim),
|
|
# so LoRA B must also produce full embedding_dim.
|
|
return B
|
|
|
|
|
|
class ParallelLMHeadWithLoRA(BaseLayerWithLoRA):
|
|
"""
|
|
Parallel LM Head layer with LoRA support.
|
|
|
|
The LM head computes logits = hidden_states @ (W + B @ A)^T
|
|
|
|
With TP > 1, lm_head is column-parallel: each rank holds
|
|
weight (vocab_size/tp_size, hidden_size) and produces a shard
|
|
of logits. LoRA A is kept unsharded (rank, hidden_size) while
|
|
LoRA B is sliced along the vocab dimension to (vocab_size/tp_size, rank).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
base_layer: ParallelLMHead,
|
|
lora_backend: BaseLoRABackend,
|
|
) -> None:
|
|
super().__init__(base_layer, lora_backend)
|
|
self.weight = base_layer.weight
|
|
self.embed_dim = base_layer.embedding_dim
|
|
self.vocab_size = base_layer.org_vocab_size
|
|
|
|
offsets = [0, self.vocab_size]
|
|
|
|
tp_size = base_layer.tp_size if hasattr(base_layer, "tp_size") else 1
|
|
|
|
# lm_head LoRA keeps A unsharded and shards B along the vocab
|
|
# dimension, matching the column-parallel base output. This is
|
|
# incompatible with input_scattered mode where the all-reduce is
|
|
# skipped.
|
|
if tp_size > 1:
|
|
from sglang.srt.layers.communicator import get_attn_tp_context
|
|
|
|
if get_attn_tp_context().allow_input_scattered:
|
|
raise ValueError(
|
|
"ParallelLMHeadWithLoRA is not compatible with "
|
|
"input_scattered mode (e.g., DeepSeek-v2 MLA with "
|
|
"--enable-attn-tp-input-scattered). Please disable "
|
|
"input_scattered or remove lm_head from LoRA "
|
|
"target modules."
|
|
)
|
|
|
|
self.shard_vocab_size = get_lm_head_lora_b_shard_size(
|
|
self.vocab_size,
|
|
shard_indices=base_layer.shard_indices,
|
|
)
|
|
offsets = [0, self.shard_vocab_size]
|
|
|
|
self.output_offset = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device=next(base_layer.parameters()).device,
|
|
)
|
|
self.output_offset_cpu = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device="cpu",
|
|
pin_memory=True,
|
|
)
|
|
|
|
def set_lora_info(
|
|
self,
|
|
lm_head_A_buffer: torch.Tensor,
|
|
lm_head_B_buffer: torch.Tensor,
|
|
):
|
|
"""Set LoRA buffers for LM head layer."""
|
|
self.set_lora = True
|
|
self.lm_head_A_buffer = lm_head_A_buffer # (num_loras, rank, hidden_dim)
|
|
self.lm_head_B_buffer = lm_head_B_buffer # (num_loras, vocab_size, rank)
|
|
|
|
def _get_lm_head_batch_info(self, num_tokens: int):
|
|
"""Resolve and validate the active lm_head batch_info.
|
|
|
|
When the logits processor calls lm_head in multiple passes
|
|
(chunked logprobs), _lm_head_pass_idx selects a precomputed
|
|
per-pass batch_info. Otherwise the full-pruned batch_info is used.
|
|
|
|
Returns None when no lm_head pruning applies (decode, no LoRA, etc.).
|
|
"""
|
|
pass_idx = self.lora_backend._lm_head_pass_idx
|
|
if (
|
|
pass_idx is not None
|
|
and self.lora_backend.lm_head_pass_batch_infos is not None
|
|
):
|
|
batch_info = self.lora_backend.lm_head_pass_batch_infos[pass_idx]
|
|
else:
|
|
batch_info = self.lora_backend.lm_head_batch_info
|
|
|
|
if batch_info is not None:
|
|
if batch_info.use_cuda_graph:
|
|
raise RuntimeError(
|
|
"lm_head LoRA with pruned batch info is not supported "
|
|
"under CUDA graph. lm_head pruning should only occur "
|
|
"during extend, which does not use CUDA graph."
|
|
)
|
|
if num_tokens != batch_info.expected_tokens:
|
|
raise RuntimeError(
|
|
f"lm_head LoRA input token count mismatch: got "
|
|
f"{num_tokens} tokens but lm_head_batch_info expects "
|
|
f"{batch_info.expected_tokens}. This likely means "
|
|
f"a pruning step in LogitsProcessor._get_pruned_states is "
|
|
f"not reflected in get_lm_head_pruned_lens()."
|
|
)
|
|
|
|
return batch_info
|
|
|
|
def apply_lora(
|
|
self,
|
|
base_output: torch.Tensor,
|
|
hidden_states: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
"""
|
|
Apply LoRA to LM head layer.
|
|
|
|
For LM head: output = hidden @ (W + B @ A)^T
|
|
= hidden @ W^T + hidden @ A^T @ B^T
|
|
= base_output + (hidden @ A^T) @ B^T
|
|
"""
|
|
lm_head_batch_info = self._get_lm_head_batch_info(hidden_states.shape[0])
|
|
|
|
# Apply lora_A^T: hidden_states @ A^T
|
|
lora_a_output = self.lora_backend.run_lora_a_sgemm(
|
|
hidden_states,
|
|
self.lm_head_A_buffer,
|
|
pruned_batch_info=lm_head_batch_info,
|
|
)
|
|
|
|
# Apply lora_B^T: lora_a_output @ B^T
|
|
lora_output = self.lora_backend.run_lora_b_sgemm(
|
|
x=lora_a_output,
|
|
weights=self.lm_head_B_buffer,
|
|
output_offset=self.output_offset,
|
|
output_offset_cpu=self.output_offset_cpu,
|
|
base_output=base_output,
|
|
pruned_batch_info=lm_head_batch_info,
|
|
)
|
|
|
|
return lora_output
|
|
|
|
def forward(self, hidden_states: torch.Tensor):
|
|
# Apply base linear transformation
|
|
base_output = F.linear(
|
|
hidden_states, self.weight, bias=getattr(self.base_layer, "bias", None)
|
|
)
|
|
|
|
# Apply LoRA if set
|
|
if self.set_lora:
|
|
base_output = self.apply_lora(base_output, hidden_states)
|
|
|
|
return base_output
|
|
|
|
# ------------------------------------------------------------------
|
|
# Multi-pass lm_head support (chunked logprobs)
|
|
# ------------------------------------------------------------------
|
|
|
|
def set_lm_head_pass(self, pass_idx: int):
|
|
"""Set the active lm_head pass index before a logprobs chunk.
|
|
|
|
Called by LogitsProcessor.process_input_logprobs_by_chunk() before
|
|
each chunk's _get_logits call. _get_lm_head_batch_info() will
|
|
resolve to lm_head_pass_batch_infos[pass_idx].
|
|
"""
|
|
self.lora_backend._lm_head_pass_idx = pass_idx
|
|
|
|
def reset_lm_head_pass(self):
|
|
"""Reset the lm_head pass index after all passes are done."""
|
|
self.lora_backend._lm_head_pass_idx = None
|
|
|
|
def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
# LoRA A weights (rank, hidden_size) are kept unsharded.
|
|
# Each rank receives full hidden_states, so A operates on full input.
|
|
return A
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
|
# lm_head is column-parallel: each rank produces vocab_size/tp_size (shard_vocab_size)
|
|
# logits. LoRA B (vocab_size, rank) must be sliced along the vocab
|
|
# dimension to match the sharded base output.
|
|
# Uses the base layer's shard_indices for the actual vocab range on
|
|
# this rank, staying consistent with base model weight sharding.
|
|
tp_size = self.base_layer.tp_size if hasattr(self.base_layer, "tp_size") else 1
|
|
if tp_size <= 1:
|
|
return B
|
|
start_idx = self.base_layer.shard_indices.org_vocab_start_index
|
|
end_idx = self.base_layer.shard_indices.org_vocab_end_index
|
|
return B[start_idx:end_idx, :]
|
|
|
|
|
|
class ColumnParallelLinearWithLoRA(BaseLayerWithLoRA):
|
|
def __init__(
|
|
self,
|
|
base_layer: ColumnParallelLinear,
|
|
lora_backend: BaseLoRABackend,
|
|
) -> None:
|
|
super().__init__(base_layer, lora_backend)
|
|
shard_size = self.base_layer.output_partition_sizes[0]
|
|
offsets = [0, shard_size]
|
|
self.output_offset = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device=next(self.base_layer.parameters()).device,
|
|
)
|
|
self.output_offset_cpu = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device="cpu",
|
|
pin_memory=True,
|
|
)
|
|
|
|
def set_lora_info(
|
|
self,
|
|
A_buffer: torch.Tensor,
|
|
B_buffer: torch.Tensor,
|
|
):
|
|
self.set_lora = True
|
|
self.A_buffer = A_buffer
|
|
self.B_buffer = B_buffer
|
|
|
|
def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
|
|
lora_a_output = self.lora_backend.run_lora_a_sgemm(x, self.A_buffer)
|
|
lora_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=base_output,
|
|
)
|
|
return lora_output
|
|
|
|
def forward(self, input_: torch.Tensor):
|
|
# duplicate the logic in ColumnParallelLinear
|
|
bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
|
|
output_parallel = self.base_layer.quant_method.apply(
|
|
self.base_layer, input_, bias
|
|
)
|
|
|
|
if self.set_lora:
|
|
output_parallel = self.apply_lora(output_parallel, input_)
|
|
|
|
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 slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
return A
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
|
shard_size = self.base_layer.output_partition_sizes[0]
|
|
start_idx = tp_rank * shard_size
|
|
end_idx = (tp_rank + 1) * shard_size
|
|
B = B[start_idx:end_idx, :]
|
|
return B
|
|
|
|
|
|
class MergedColumnParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
|
|
def __init__(
|
|
self,
|
|
base_layer: MergedColumnParallelLinear,
|
|
lora_backend: BaseLoRABackend,
|
|
) -> None:
|
|
super().__init__(base_layer, lora_backend)
|
|
self.n_slices = len(self.base_layer.output_partition_sizes)
|
|
|
|
def set_lora_info(
|
|
self,
|
|
A_buffer: torch.Tensor,
|
|
B_buffer: torch.Tensor,
|
|
):
|
|
self.set_lora = True
|
|
self.A_buffer = A_buffer
|
|
self.B_buffer = B_buffer
|
|
|
|
# Build cumulative output offsets from the first `lora_n_slices`
|
|
# base partitions. `lora_n_slices` may be smaller than self.n_slices
|
|
# when only a subset of partitions are LoRA'd (e.g. Mamba in_proj
|
|
# has 5 partitions but stacked_multiply=2), so we can't precompute
|
|
# these in __init__.
|
|
lora_n_slices = self._get_lora_n_slices()
|
|
if lora_n_slices <= 0 or lora_n_slices > self.n_slices:
|
|
raise ValueError(
|
|
f"Invalid LoRA slice count {lora_n_slices} for "
|
|
f"{self.n_slices} base output partitions."
|
|
)
|
|
partition_sizes = list(self.base_layer.output_partition_sizes[:lora_n_slices])
|
|
offsets = [0]
|
|
for ps in partition_sizes:
|
|
offsets.append(offsets[-1] + ps)
|
|
if offsets[-1] != B_buffer.shape[-2]:
|
|
raise ValueError(
|
|
f"LoRA B output dim {B_buffer.shape[-2]} does not match "
|
|
f"base partition prefix dim {offsets[-1]} for {lora_n_slices} slices."
|
|
)
|
|
self.output_offset = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device=next(self.base_layer.parameters()).device,
|
|
)
|
|
self.output_offset_cpu = self.output_offset.cpu().pin_memory()
|
|
self.max_out_dim = max(partition_sizes)
|
|
self.use_gate_up_lora = (
|
|
lora_n_slices == 2 and partition_sizes[0] == partition_sizes[1]
|
|
)
|
|
|
|
def _get_lora_n_slices(self) -> int:
|
|
"""Actual number of LoRA slices from the buffer shapes.
|
|
|
|
May differ from self.n_slices (base layer partitions) when only a
|
|
subset of partitions are LoRA'd (e.g. Mamba in_proj has 5 partitions
|
|
but stacked_multiply=2).
|
|
"""
|
|
lora_rank = self.B_buffer.shape[-1]
|
|
if lora_rank == 0:
|
|
return self.n_slices
|
|
return self.A_buffer.shape[-2] // lora_rank
|
|
|
|
def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
|
|
lora_n_slices = self._get_lora_n_slices()
|
|
if lora_n_slices == 2 and self.use_gate_up_lora:
|
|
lora_output = self.lora_backend.run_gate_up_lora(
|
|
x=x,
|
|
gate_up_lora_a=self.A_buffer,
|
|
gate_up_lora_b=self.B_buffer,
|
|
output_offset=self.output_offset,
|
|
output_offset_cpu=self.output_offset_cpu,
|
|
base_output=base_output,
|
|
)
|
|
else:
|
|
lora_output = self.lora_backend.run_qkv_lora(
|
|
x=x,
|
|
qkv_lora_a=self.A_buffer,
|
|
qkv_lora_b=self.B_buffer,
|
|
output_offset=self.output_offset,
|
|
output_offset_cpu=self.output_offset_cpu,
|
|
max_qkv_out_dim=self.max_out_dim,
|
|
base_output=base_output,
|
|
n_slices=lora_n_slices,
|
|
)
|
|
return lora_output
|
|
|
|
def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
return A
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
|
partition_sizes = self.base_layer.output_partition_sizes
|
|
output_sizes = self.base_layer.output_sizes
|
|
slices = []
|
|
offset = 0
|
|
for full_size, part_size in zip(output_sizes, partition_sizes):
|
|
start_idx = tp_rank * part_size
|
|
end_idx = start_idx + part_size
|
|
slices.append(B[offset + start_idx : offset + end_idx, :])
|
|
offset += full_size
|
|
return torch.concat(slices, dim=0)
|
|
|
|
|
|
class QKVParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
|
|
def __init__(
|
|
self,
|
|
base_layer: QKVParallelLinear,
|
|
lora_backend: BaseLoRABackend,
|
|
) -> None:
|
|
super().__init__(base_layer, lora_backend)
|
|
q_proj_shard_size = self.base_layer.q_proj_shard_size
|
|
kv_proj_shard_size = self.base_layer.kv_proj_shard_size
|
|
offsets = [
|
|
0,
|
|
q_proj_shard_size,
|
|
q_proj_shard_size + kv_proj_shard_size,
|
|
q_proj_shard_size + 2 * kv_proj_shard_size,
|
|
]
|
|
self.output_offset = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device=next(self.base_layer.parameters()).device,
|
|
)
|
|
self.output_offset_cpu = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device="cpu",
|
|
pin_memory=True,
|
|
)
|
|
|
|
# For computing number of launched blocks
|
|
self.max_qkv_out_dim = max(q_proj_shard_size, kv_proj_shard_size)
|
|
|
|
def set_lora_info(
|
|
self,
|
|
A_buffer_qkv: torch.Tensor,
|
|
B_buffer_qkv: torch.Tensor,
|
|
):
|
|
self.set_lora = True
|
|
self.A_buffer_qkv = A_buffer_qkv
|
|
self.B_buffer_qkv = B_buffer_qkv
|
|
|
|
def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
|
|
lora_output = self.lora_backend.run_qkv_lora(
|
|
x=x,
|
|
qkv_lora_a=self.A_buffer_qkv,
|
|
qkv_lora_b=self.B_buffer_qkv,
|
|
base_output=base_output,
|
|
output_offset=self.output_offset,
|
|
output_offset_cpu=self.output_offset_cpu,
|
|
max_qkv_out_dim=self.max_qkv_out_dim,
|
|
)
|
|
|
|
return lora_output
|
|
|
|
def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
return A
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int) -> torch.Tensor:
|
|
base_layer = self.base_layer
|
|
q_proj_shard_size = base_layer.q_proj_shard_size
|
|
kv_proj_shard_size = base_layer.kv_proj_shard_size
|
|
num_kv_head_replicas = base_layer.num_kv_head_replicas
|
|
|
|
q_start_idx = q_proj_shard_size * tp_rank
|
|
q_end_idx = q_start_idx + q_proj_shard_size
|
|
|
|
kv_shard_id = tp_rank // num_kv_head_replicas
|
|
kv_start_idx = kv_proj_shard_size * kv_shard_id
|
|
kv_end_idx = kv_start_idx + kv_proj_shard_size
|
|
|
|
q_size = base_layer.output_sizes[0]
|
|
k_size = base_layer.output_sizes[1] // num_kv_head_replicas
|
|
B_q_shard = B[q_start_idx:q_end_idx, :]
|
|
B_k_shard = B[q_size + kv_start_idx : q_size + kv_end_idx, :]
|
|
B_v_shard = B[q_size + k_size + kv_start_idx : q_size + k_size + kv_end_idx, :]
|
|
|
|
return torch.concat(
|
|
(
|
|
B_q_shard,
|
|
B_k_shard,
|
|
B_v_shard,
|
|
),
|
|
dim=0,
|
|
)
|
|
|
|
|
|
class RowParallelLinearWithLoRA(BaseLayerWithLoRA):
|
|
def __init__(
|
|
self,
|
|
base_layer: RowParallelLinear,
|
|
lora_backend: BaseLoRABackend,
|
|
) -> None:
|
|
super().__init__(base_layer, lora_backend)
|
|
|
|
def set_lora_info(self, A_buffer: torch.Tensor, B_buffer: torch.Tensor):
|
|
self.set_lora = True
|
|
self.A_buffer = A_buffer
|
|
self.B_buffer = B_buffer
|
|
output_size = self.base_layer.output_size
|
|
offsets = [0, output_size]
|
|
self.output_offset = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device=next(self.base_layer.parameters()).device,
|
|
)
|
|
self.output_offset_cpu = torch.tensor(
|
|
offsets,
|
|
dtype=torch.int32,
|
|
device="cpu",
|
|
pin_memory=True,
|
|
)
|
|
|
|
def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
|
|
lora_a_output = self.lora_backend.run_lora_a_sgemm(x, self.A_buffer)
|
|
lora_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=base_output,
|
|
)
|
|
return lora_output
|
|
|
|
def forward(self, input_: torch.Tensor, skip_all_reduce=False, forward_batch=None):
|
|
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()
|
|
|
|
bias_ = (
|
|
None
|
|
if (self.base_layer.tp_rank > 0 or self.base_layer.skip_bias_add)
|
|
else self.base_layer.bias
|
|
)
|
|
output_parallel = self.base_layer.quant_method.apply(
|
|
self.base_layer, input_parallel, bias=bias_
|
|
)
|
|
|
|
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 self.set_lora and should_reduce:
|
|
lora_a_output = self.lora_backend.run_lora_a_sgemm(
|
|
input_parallel, self.A_buffer
|
|
)
|
|
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:
|
|
if self.set_lora:
|
|
output_parallel = self.apply_lora(output_parallel, input_parallel)
|
|
if should_reduce:
|
|
output_ = tensor_model_parallel_all_reduce(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 slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
shard_size = self.base_layer.input_size_per_partition
|
|
start_idx = tp_rank * shard_size
|
|
end_idx = (tp_rank + 1) * shard_size
|
|
A = A[:, start_idx:end_idx].contiguous()
|
|
return A
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
|
return B
|
|
|
|
|
|
class ReplicatedLinearWithLoRA(BaseLayerWithLoRA):
|
|
"""LoRA wrapper for ReplicatedLinear (no TP sharding).
|
|
|
|
Used for DeepSeek MLA's fused_qkv_a_proj_with_mqa, which fuses
|
|
q_a_proj and kv_a_proj_with_mqa into a single replicated linear.
|
|
The two sub-projections have unequal output dimensions, so we use
|
|
the N-component fused kernel (run_qkv_lora) with n_slices=2 to
|
|
handle the split inside the triton kernel rather than in Python.
|
|
|
|
``first_output_dim`` (set by LoRAManager after construction) marks the
|
|
boundary between the first and second sub-projection in the output.
|
|
"""
|
|
|
|
first_output_dim: int = 0
|
|
|
|
def __init__(
|
|
self,
|
|
base_layer: ReplicatedLinear,
|
|
lora_backend: BaseLoRABackend,
|
|
) -> None:
|
|
super().__init__(base_layer, lora_backend)
|
|
self.output_size = base_layer.output_size
|
|
|
|
def set_lora_info(self, A_buffer: torch.Tensor, B_buffer: torch.Tensor):
|
|
self.set_lora = True
|
|
self.A_buffer = A_buffer
|
|
self.B_buffer = B_buffer
|
|
first_dim = self.first_output_dim
|
|
if first_dim > 0:
|
|
second_dim = B_buffer.shape[-2] - first_dim
|
|
self._output_offset = torch.tensor(
|
|
[0, first_dim, first_dim + second_dim],
|
|
dtype=torch.int32,
|
|
device=B_buffer.device,
|
|
)
|
|
self._output_offset_cpu = self._output_offset.cpu()
|
|
self._max_out_dim = max(first_dim, second_dim)
|
|
else:
|
|
# Single-projection path: csgmv backend requires an explicit
|
|
# slice_offsets tensor of shape [0, output_dim].
|
|
self._output_offset = torch.tensor(
|
|
[0, B_buffer.shape[-2]],
|
|
dtype=torch.int32,
|
|
device=B_buffer.device,
|
|
)
|
|
|
|
def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
|
|
first_dim = self.first_output_dim
|
|
|
|
if first_dim == 0:
|
|
# Simple single-projection (e.g. fc1_latent_proj, fc2_latent_proj)
|
|
lora_a_output = self.lora_backend.run_lora_a_sgemm(x, self.A_buffer)
|
|
lora_output = self.lora_backend.run_lora_b_sgemm(
|
|
x=lora_a_output,
|
|
weights=self.B_buffer,
|
|
output_offset=self._output_offset,
|
|
base_output=base_output,
|
|
)
|
|
return lora_output
|
|
|
|
# Use the fused N-component kernel with n_slices=2 to handle the
|
|
# split inside the triton kernel, avoiding Python-level splitting
|
|
# which breaks when adapter rank < max_lora_rank.
|
|
lora_output = self.lora_backend.run_qkv_lora(
|
|
x=x,
|
|
qkv_lora_a=self.A_buffer,
|
|
qkv_lora_b=self.B_buffer,
|
|
output_offset=self._output_offset,
|
|
output_offset_cpu=self._output_offset_cpu,
|
|
max_qkv_out_dim=self._max_out_dim,
|
|
base_output=base_output,
|
|
n_slices=2,
|
|
)
|
|
return lora_output
|
|
|
|
def forward(self, x: torch.Tensor):
|
|
bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
|
|
output = self.base_layer.quant_method.apply(self.base_layer, x, bias)
|
|
if self.set_lora:
|
|
output = self.apply_lora(output, x)
|
|
output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
|
|
return output, output_bias
|
|
|
|
def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
return A
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
|
return B
|
|
|
|
|
|
class FusedMoEWithLoRA(BaseLayerWithLoRA):
|
|
"""
|
|
Wrapper around FusedMoE that integrates LoRA into the MoE computation.
|
|
|
|
Design: LoRA deltas are added at specific points in the MoE forward pass:
|
|
1. After gate_up projection, BEFORE activation (halfway through)
|
|
2. After down projection, BEFORE final reduction
|
|
|
|
This follows the vLLM/HF approach where LoRA is fused into the computation
|
|
rather than computed independently and added at the end.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
base_layer: FusedMoE,
|
|
lora_backend: BaseLoRABackend,
|
|
):
|
|
# initializes FusedMoE with its own moe_runner for base path
|
|
super().__init__(base_layer, lora_backend)
|
|
|
|
lora_backend.is_moe_lora = True
|
|
|
|
self.experts_shared_outer_loras: bool = False
|
|
self.lora_use_virtual_experts: bool = False
|
|
self.quant_method = base_layer.quant_method
|
|
self.moe_runner_config = base_layer.moe_runner_config
|
|
self.dispatcher = base_layer.dispatcher
|
|
self.num_local_experts = base_layer.num_local_experts
|
|
self.should_fuse_routed_scaling_factor_in_topk = (
|
|
base_layer.should_fuse_routed_scaling_factor_in_topk
|
|
)
|
|
|
|
self.tp_size = getattr(base_layer, "moe_tp_size", 1)
|
|
self.tp_rank = getattr(base_layer, "moe_tp_rank", 0)
|
|
self.intermediate_size_per_partition = getattr(
|
|
base_layer, "intermediate_size_per_partition", None
|
|
)
|
|
self._uses_interleaved_gate_up = (
|
|
getattr(base_layer.moe_runner_config, "gemm1_alpha", None) is not None
|
|
)
|
|
|
|
# Initialize triton_lora moe runner for batches with lora enabled
|
|
from sglang.srt.layers.moe import MoeRunnerBackend
|
|
from sglang.srt.layers.moe.moe_runner.runner import MoeRunner
|
|
from sglang.srt.layers.moe.utils import get_moe_runner_backend
|
|
|
|
# Determine runner backend: prefer server arg, fall back to quant method's runner
|
|
global_backend = get_moe_runner_backend()
|
|
if not global_backend.is_auto():
|
|
runner_backend = global_backend
|
|
elif (
|
|
hasattr(base_layer.quant_method, "runner")
|
|
and base_layer.quant_method.runner is not None
|
|
):
|
|
runner_backend = base_layer.quant_method.runner.runner_backend
|
|
else:
|
|
runner_backend = MoeRunnerBackend.TRITON
|
|
|
|
# ===== TO BE REFACTORED ====
|
|
self._lora_runner_backend = runner_backend
|
|
if runner_backend.is_experimental_sgl_trtllm():
|
|
from sglang.srt.lora.trtllm_lora_temp.lora_layer import (
|
|
init_experimental_sgl_trtllm_lora,
|
|
)
|
|
|
|
init_experimental_sgl_trtllm_lora(self, base_layer)
|
|
return
|
|
# ===== END TO BE REFACTORED ====
|
|
|
|
self._lora_runner = MoeRunner(
|
|
runner_backend,
|
|
base_layer.moe_runner_config,
|
|
lora_enabled=True,
|
|
)
|
|
|
|
if runner_backend.is_marlin():
|
|
from sglang.srt.layers.quantization.compressed_tensors.compressed_tensors import (
|
|
CompressedTensorsFusedMoEMethod,
|
|
)
|
|
|
|
assert isinstance(
|
|
base_layer.quant_method, CompressedTensorsFusedMoEMethod
|
|
), (
|
|
f"Marlin MoE backend requires CompressedTensorsFusedMoEMethod, "
|
|
f"got {type(base_layer.quant_method).__name__}"
|
|
)
|
|
self._quant_info = base_layer.quant_method.get_marlin_quant_info(base_layer)
|
|
elif runner_backend.is_triton():
|
|
assert base_layer.quant_method is not None, "Quant method must be set"
|
|
self._quant_info = base_layer.quant_method.get_triton_quant_info(base_layer)
|
|
else:
|
|
raise NotImplementedError(
|
|
f"LoRA MoE not supported for backend {runner_backend}"
|
|
)
|
|
|
|
def set_lora_info(
|
|
self,
|
|
gate_up_lora_a_weights: torch.Tensor,
|
|
gate_up_lora_b_weights: torch.Tensor,
|
|
down_lora_a_weights: torch.Tensor = None,
|
|
down_lora_b_weights: torch.Tensor = None,
|
|
):
|
|
"""Set LoRA weight tensors from memory pool."""
|
|
self.set_lora = True
|
|
self.gate_up_lora_a_weights = gate_up_lora_a_weights
|
|
self.gate_up_lora_b_weights = gate_up_lora_b_weights
|
|
self.down_lora_a_weights = down_lora_a_weights
|
|
self.down_lora_b_weights = down_lora_b_weights
|
|
|
|
def _get_lora_info(self):
|
|
"""Build LoRAInfo for the current batch."""
|
|
from sglang.srt.lora.lora_moe_runners import LoRAInfo
|
|
|
|
batch_info = self.lora_backend.batch_info
|
|
|
|
lora_ranks = batch_info.lora_ranks
|
|
max_lora_rank = self.down_lora_a_weights.shape[2]
|
|
cg_buffers = getattr(self.lora_backend, "moe_cg_buffers", None)
|
|
moe_lora_info = batch_info.moe_lora_info
|
|
assert moe_lora_info is not None
|
|
|
|
# Single source of truth: lora_manager precomputes this per-batch from
|
|
# the Python weight_indices list, no GPU sync needed.
|
|
has_active_lora = bool(getattr(batch_info, "has_active_lora", False))
|
|
|
|
if self._lora_runner_backend.is_experimental_sgl_trtllm():
|
|
# Per-rank (local) expert count the LoRA buffers are indexed by, so
|
|
# virtual-experts indexing matches the buffers under EP.
|
|
num_experts = (
|
|
self.down_lora_a_weights.shape[1]
|
|
if self.down_lora_a_weights is not None
|
|
else self.base_layer.num_local_experts
|
|
)
|
|
else:
|
|
num_experts = self.base_layer.num_experts
|
|
|
|
return LoRAInfo(
|
|
gate_up_lora_a_weights=self.gate_up_lora_a_weights,
|
|
gate_up_lora_b_weights=self.gate_up_lora_b_weights,
|
|
down_lora_a_weights=self.down_lora_a_weights,
|
|
down_lora_b_weights=self.down_lora_b_weights,
|
|
seg_indptr=moe_lora_info.seg_indptr,
|
|
req_to_lora=moe_lora_info.req_to_lora,
|
|
lora_ranks=lora_ranks,
|
|
adapter_enabled=moe_lora_info.adapter_enabled,
|
|
token_lora_mapping=moe_lora_info.token_lora_mapping,
|
|
max_lora_rank=max_lora_rank,
|
|
num_experts=num_experts,
|
|
has_active_lora=has_active_lora,
|
|
experts_shared_outer_loras=self.experts_shared_outer_loras,
|
|
cg_buffers=cg_buffers,
|
|
tp_size=self.tp_size,
|
|
tp_rank=self.tp_rank,
|
|
hidden_size=getattr(self.base_layer, "hidden_size", 0),
|
|
lora_use_virtual_experts=self.lora_use_virtual_experts,
|
|
)
|
|
|
|
def forward(self, hidden_states: torch.Tensor, topk_output: TopKOutput, **kwargs):
|
|
"""
|
|
Forward pass with integrated LoRA computation.
|
|
|
|
LoRA deltas are added at the correct points inside the MoE computation:
|
|
1. After gate_up projection, before activation
|
|
2. After down projection, before final reduction
|
|
"""
|
|
|
|
# Build LoRA info for this batch
|
|
lora_info = self._get_lora_info()
|
|
|
|
# run lora moe_runner
|
|
return self._forward_with_lora(hidden_states, topk_output, lora_info, **kwargs)
|
|
|
|
def _forward_with_lora(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
topk_output: TopKOutput,
|
|
lora_info,
|
|
**kwargs,
|
|
):
|
|
"""
|
|
Run MoE forward with LoRA integration at the correct points.
|
|
"""
|
|
# Get the base layer's dispatch and combine logic
|
|
base_layer = self.base_layer
|
|
|
|
# Dispatch tokens (doesn't do much in the LoRA case)
|
|
dispatch_output = base_layer.dispatcher.dispatch(
|
|
hidden_states=hidden_states, topk_output=topk_output
|
|
)
|
|
|
|
# Use pre-computed quant info (doesn't change so not sure why we need to pass it in every time)
|
|
quant_info = self._quant_info
|
|
|
|
# ===== TO BE REFACTORED ====
|
|
if self._lora_runner_backend.is_experimental_sgl_trtllm():
|
|
from sglang.srt.lora.trtllm_lora_temp.lora_layer import (
|
|
dispatch_experimental_sgl_trtllm_lora,
|
|
)
|
|
|
|
combine_input = dispatch_experimental_sgl_trtllm_lora(
|
|
dispatch_output, quant_info, base_layer, lora_info
|
|
)
|
|
# ===== END TO BE REFACTORED ====
|
|
else:
|
|
combine_input = self._lora_runner.run(
|
|
dispatch_output, quant_info, lora_info=lora_info
|
|
)
|
|
|
|
final_hidden_states = base_layer.dispatcher.combine(combine_input=combine_input)
|
|
|
|
return final_hidden_states
|
|
|
|
def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
|
|
return A
|
|
|
|
def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
|
|
return B
|
|
|
|
def slice_moe_lora_a_weights(
|
|
self,
|
|
A: Union[torch.Tensor, Dict[int, torch.Tensor]],
|
|
tp_rank: int,
|
|
target_module: str,
|
|
):
|
|
"""Slice LoRA A weights for MoE with TP.
|
|
|
|
Accepts:
|
|
- 2D tensor [rank, hidden] (single expert)
|
|
- 3D tensor [num_experts_or_1, rank, hidden]
|
|
- dict {expert_id: 2D tensor}
|
|
|
|
Per-expert weight shapes:
|
|
gate_up_proj_moe A: [rank, hidden_size] — input is full hidden_states, no slice
|
|
down_proj_moe A: [rank, intermediate_size] — input is sharded intermediate
|
|
"""
|
|
if self.tp_size <= 1:
|
|
return A
|
|
if target_module != "down_proj_moe":
|
|
return A
|
|
if isinstance(A, dict):
|
|
return {
|
|
eid: self._slice_moe_a(w, tp_rank, target_module)
|
|
for eid, w in A.items()
|
|
}
|
|
return self._slice_moe_a(A, tp_rank, target_module)
|
|
|
|
def _slice_moe_a(
|
|
self, A: torch.Tensor, tp_rank: int, target_module: str
|
|
) -> torch.Tensor:
|
|
shard_size = self.intermediate_size_per_partition
|
|
start = tp_rank * shard_size
|
|
end = start + shard_size
|
|
return A[..., start:end].contiguous()
|
|
|
|
def slice_moe_lora_b_weights(
|
|
self,
|
|
B: Union[torch.Tensor, Dict[int, torch.Tensor]],
|
|
tp_rank: int,
|
|
target_module: str,
|
|
):
|
|
"""Slice LoRA B weights for MoE with TP.
|
|
|
|
Accepts:
|
|
- 2D tensor [output_dim, rank] (single expert)
|
|
- 3D tensor [num_experts_or_1, output_dim, rank]
|
|
- dict {expert_id: 2D tensor}
|
|
|
|
Per-expert weight shapes:
|
|
gate_up_proj_moe B: [intermediate_size*2, rank] — output matches sharded base w13
|
|
down_proj_moe B: [hidden_size, rank] — output is all-reduced, no slice
|
|
"""
|
|
needs_processing = (self.tp_size > 1) or (
|
|
target_module == "gate_up_proj_moe" and self._uses_interleaved_gate_up
|
|
)
|
|
if not needs_processing:
|
|
return B
|
|
if target_module != "gate_up_proj_moe":
|
|
return B
|
|
if isinstance(B, dict):
|
|
return {
|
|
eid: self._slice_moe_b_2d(w, tp_rank, target_module)
|
|
for eid, w in B.items()
|
|
}
|
|
if isinstance(B, torch.Tensor) and B.dim() == 3:
|
|
return torch.stack(
|
|
[
|
|
self._slice_moe_b_2d(B[i], tp_rank, target_module)
|
|
for i in range(B.shape[0])
|
|
]
|
|
)
|
|
return self._slice_moe_b_2d(B, tp_rank, target_module)
|
|
|
|
def _slice_moe_b_2d(
|
|
self, B: torch.Tensor, tp_rank: int, target_module: str
|
|
) -> torch.Tensor:
|
|
if target_module == "gate_up_proj_moe":
|
|
# Non-gated MoE (e.g. Nemotron-H): only w1, no w3.
|
|
# B has shape [intermediate_size, rank] — TP-shard directly.
|
|
is_gated = self.base_layer.moe_runner_config.is_gated
|
|
if not is_gated:
|
|
if self.tp_size > 1:
|
|
shard_size = self.intermediate_size_per_partition
|
|
start = tp_rank * shard_size
|
|
end = start + shard_size
|
|
return B[start:end, :]
|
|
return B
|
|
|
|
shard_size = self.intermediate_size_per_partition
|
|
start = tp_rank * shard_size
|
|
end = start + shard_size
|
|
full_inter = B.shape[0] // 2
|
|
gate_b = B[start:end, :]
|
|
up_b = B[full_inter + start : full_inter + end, :]
|
|
if self._uses_interleaved_gate_up:
|
|
return torch.stack([gate_b, up_b], dim=1).reshape(-1, B.shape[-1])
|
|
return torch.cat([gate_b, up_b], dim=0).contiguous()
|
|
return B
|
|
|
|
|
|
def get_lora_layer(
|
|
layer: nn.Module, lora_backend: BaseLoRABackend
|
|
) -> BaseLayerWithLoRA:
|
|
supported_layer_types = {
|
|
# the order matters
|
|
FusedMoE: FusedMoEWithLoRA,
|
|
ParallelLMHead: ParallelLMHeadWithLoRA,
|
|
VocabParallelEmbedding: VocabParallelEmbeddingWithLoRA,
|
|
ReplicatedLinear: ReplicatedLinearWithLoRA,
|
|
QKVParallelLinear: QKVParallelLinearWithLoRA,
|
|
MergedColumnParallelLinear: MergedColumnParallelLinearWithLoRA,
|
|
ColumnParallelLinear: ColumnParallelLinearWithLoRA,
|
|
RowParallelLinear: RowParallelLinearWithLoRA,
|
|
}
|
|
for src_layer_type, lora_layer_type in supported_layer_types.items():
|
|
if isinstance(layer, src_layer_type): # pylint: disable=unidiomatic-typecheck
|
|
ret = lora_layer_type(layer, lora_backend)
|
|
return ret
|
|
raise Exception(f"No corresponding LoRA layer supported for {type(layer)}.")
|
|
|
|
|
|
# ===== TO BE REFACTORED ====
|
|
# Experimental two-stream LoRA overlap; installed only under the master switch, else no-op.
|
|
if _SGLANG_EXPERIMENTAL_LORA_OPTI:
|
|
from sglang.srt.lora.trtllm_lora_temp import ( # noqa: E402
|
|
install_two_stream_overrides as _install_lora_two_stream,
|
|
)
|
|
|
|
_install_lora_two_stream()
|
|
# ===== END TO BE REFACTORED ====
|