Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1389 lines
60 KiB
Python

import logging
import re
from typing import (
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Set,
Tuple,
Union,
overload,
)
import torch
from sglang.srt.distributed import (
divide,
get_pp_group,
)
from sglang.srt.environ import envs
from sglang.srt.lora.eviction_policy import get_eviction_policy
from sglang.srt.lora.layers import BaseLayerWithLoRA
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.utils import (
EMBEDDING_NAMES,
REPLICATED_LINEAR_LORA_NAMES,
ROW_PARALLELISM_LINEAR_LORA_NAMES,
LoRAType,
copy_weight_into_buffer,
get_hidden_dim,
get_lm_head_lora_b_shard_size,
get_normalized_target_modules,
get_stacked_multiply,
get_target_module_name,
)
from sglang.srt.runtime_context import get_parallel
from sglang.srt.utils import is_pin_memory_available
from sglang.srt.utils.hf_transformers_utils import AutoConfig
_SGLANG_EXPERIMENTAL_LORA_OPTI = envs.SGLANG_EXPERIMENTAL_LORA_OPTI.get()
logger = logging.getLogger(__name__)
class EmptySlot:
"""
Singleton class to represent an empty slot in the memory pool.
This is used to improve readability by not using special str as a placeholder.
"""
__slots__ = ()
def __repr__(self):
return "|EMPTY|"
def __new__(cls):
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
return cls._instance
EMPTY_SLOT = EmptySlot()
@overload
def append_cache_key_suffix(cache_keys: str, suffix: str) -> str: ...
@overload
def append_cache_key_suffix(
cache_keys: Dict[int, str], suffix: str
) -> Dict[int, str]: ...
def append_cache_key_suffix(
cache_keys: Union[str, Dict[int, str]],
suffix: str,
) -> Union[str, Dict[int, str]]:
if isinstance(cache_keys, dict):
return {
expert_id: f"{cache_key}#{suffix}"
for expert_id, cache_key in cache_keys.items()
}
return f"{cache_keys}#{suffix}"
def _get_moe_ep_context() -> Tuple[int, int]:
"""Return `(moe_ep_size, moe_ep_rank)`, or `(1, 0)` if the MoE EP group
is not initialized (hermetic tests or pure-TP launches)."""
try:
return get_parallel().moe_ep_size, get_parallel().moe_ep_rank
except Exception: # pragma: no cover - MoE EP group not initialized
return 1, 0
def _get_moe_tp_context() -> Tuple[int, int]:
"""Return `(moe_tp_size, moe_tp_rank)`, or `(1, 0)` if the MoE TP group
is not initialized. Under `--tp N --ep N` the outer attention TP group
is consumed entirely by EP, leaving `moe_tp_size == 1`, so per-expert
MoE weights are NOT sharded along their inner dim even though attention
weights are."""
try:
return get_parallel().moe_tp_size, get_parallel().moe_tp_rank
except Exception: # pragma: no cover - MoE TP group not initialized
return 1, 0
def _moe_runner_keeps_global_expert_ids() -> bool:
"""True if the active MoE runner keeps global `topk_ids` instead of
remapping to local IDs. Mirrors the predicate in `StandardDispatcher`."""
try:
from sglang.srt.layers.moe.utils import get_moe_runner_backend
b = get_moe_runner_backend()
return (
b.is_flashinfer_cutlass()
or b.is_flashinfer_cutedsl()
or b.is_experimental_sgl_trtllm()
or b.is_flashinfer_trtllm_routed()
)
except Exception: # pragma: no cover - backend not initialized
return False
class LoRAMemoryPool:
"""Class for memory pool management of lora modules"""
def __init__(
self,
base_hf_config: AutoConfig,
max_loras_per_batch: int,
dtype: torch.dtype,
tp_size: int,
tp_rank: int,
max_lora_rank: int,
target_modules: Set[str],
base_model: torch.nn.Module,
eviction_policy: str,
lora_added_tokens_size: int,
experts_shared_outer_loras: bool = False,
strict_loading: bool = False,
enable_lora_overlap_loading: bool = False,
):
self.base_hf_config: AutoConfig = base_hf_config
self.num_layer: int = base_hf_config.num_hidden_layers
self.max_loras_per_batch: int = max_loras_per_batch
self.dtype: torch.dtype = dtype
self.tp_size: int = tp_size
self.tp_rank: int = tp_rank
self.lora_added_tokens_size: int = lora_added_tokens_size
self.max_lora_rank: int = max_lora_rank
self.target_modules: Set[str] = target_modules
self.experts_shared_outer_loras: bool = experts_shared_outer_loras
self.strict_loading: bool = strict_loading
self.enable_lora_overlap_loading: bool = enable_lora_overlap_loading
self.pin_memory_available: bool = is_pin_memory_available()
# Under EP with a Triton/DeepGEMM runner, `StandardDispatcher` remaps
# global `topk_ids` -> local expert IDs before the MoE kernel, so
# per-expert LoRA buffers must be sized and keyed by the local slice.
# FlashInfer CUTLASS/CuteDSL/TRTLLM-routed keep global IDs, and an
# uneven expert split (`num_experts % moe_ep_size != 0`, shouldn't
# happen in practice) is also treated as globally-keyed so we don't
# silently truncate experts.
self.moe_ep_size, self.moe_ep_rank = _get_moe_ep_context()
num_experts_global = self._get_num_experts(base_model)
self.moe_use_local_expert_ids = (
self.moe_ep_size > 1
and not _moe_runner_keeps_global_expert_ids()
and num_experts_global % self.moe_ep_size == 0
)
# Per-expert MoE weights are sharded by `moe_tp_size`, NOT the outer
# `tp_size`: `moe_tp_size = tp_size // ep_size // dp_size`, so under
# e.g. `--tp 4 --ep 4` each rank holds full-width expert weights
# (`moe_tp_size == 1`). Sizing per-expert LoRA buffers by `tp_size`
# here would yield a 4x-narrower inner dim than the adapter weight
# (which `FusedMoEWithLoRA.slice_moe_lora_{a,b}_weights` correctly
# skip-slices when `moe_tp_size <= 1`), producing a shape-mismatch
# assert during weight load. Non-MoE modules still shard by
# `tp_size` because attention TP is unchanged.
self.moe_tp_size, self.moe_tp_rank = _get_moe_tp_context()
# Initialize eviction policy
self.eviction_policy = get_eviction_policy(eviction_policy)
# Both A_buffer and B_buffer maps lora weight names to its buffer space.
# Standard LoRA (3D): [num_loras, rank, hidden_dim]
# MoE LoRA (4D): [num_loras, num_experts, rank, hidden_dim]
# The dimensionality is determined by the module type (MoE vs standard)
self.A_buffer: Dict[str, List[torch.Tensor]] = {}
self.B_buffer: Dict[str, List[torch.Tensor]] = {}
self.embedding_A_buffer: Dict[str, torch.Tensor] = {}
self.embedding_B_buffer: Dict[str, torch.Tensor] = {}
self.lm_head_A_buffer: Dict[str, torch.Tensor] = {}
self.lm_head_B_buffer: Dict[str, torch.Tensor] = {}
self.new_embeddings_buffer: Dict[str, torch.Tensor] = {}
self.embedding_dim: int = self.base_hf_config.hidden_size
# Lora uid -> buffer idx in memory pool
self.uid_to_buffer_id: Dict[Optional[str], int] = {}
# Buffer idx -> lora uid in memory pool
# All uids are initialized as `EmptySlot` for empty buffer slots
# Here we don't initialize to None since None is a valid uid
self.buffer_id_to_uid: List[Union[str, None, EmptySlot]] = [
EMPTY_SLOT
] * self.max_loras_per_batch
# Cache lm_head shard_indices from the base model so that buffer
# allocation uses the same sharding as the base ParallelLMHead layer.
self.lm_head_shard_indices = None
if "lm_head" in target_modules and tp_size > 1:
from sglang.srt.layers.vocab_parallel_embedding import ParallelLMHead
for _, module in base_model.named_modules():
if isinstance(module, ParallelLMHead):
self.lm_head_shard_indices = module.shard_indices
break
self.init_buffers(base_model)
def can_support(self, config: Union[LoRAConfig, Iterable[LoRAConfig]]) -> bool:
"""
Check if the memory pool can support the given LoRA adapters.
"""
def _can_support(config: LoRAConfig) -> bool:
"""
Check if the memory pool can support a single LoRA adapter.
"""
if config.r > self.max_lora_rank:
return False
if config.lora_added_tokens_size > self.lora_added_tokens_size:
return False
target_module_names = get_normalized_target_modules(config.target_modules)
if "all" in target_module_names:
return True
return target_module_names.issubset(self.target_modules)
if isinstance(config, LoRAConfig):
return _can_support(config)
else:
return all(_can_support(x) for x in config)
def is_moe_module(self, module_name: str) -> bool:
"""Check if module is part of MoE experts."""
return "moe" in module_name
@staticmethod
def _get_num_experts(base_model: torch.nn.Module) -> int:
cfg = base_model.config
if hasattr(cfg, "get_text_config"):
cfg = cfg.get_text_config()
return (
getattr(cfg, "num_experts", None)
or getattr(cfg, "num_local_experts", None)
or getattr(cfg, "n_routed_experts", None)
or 1
)
@staticmethod
def _has_moe_module(base_model: torch.nn.Module) -> bool:
# Config-only detection isn't reliable: some dense configs (e.g.
# `Qwen3_5TextConfig`) inherit `num_experts > 1` from an MoE parent.
# Walk the loaded model for an actual FusedMoE instance before we
# commit to allocating 4D per-expert LoRA buffers.
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
return any(isinstance(m, FusedMoE) for m in base_model.modules())
def _get_num_local_experts(self, base_model: torch.nn.Module) -> int:
"""Experts owned by this rank. Equals the global count when EP is
off, the runner keeps global IDs, or the split isn't even (all
three cases fold into `moe_use_local_expert_ids == False`)."""
total = self._get_num_experts(base_model)
if not self.moe_use_local_expert_ids:
return total
return total // self.moe_ep_size
def _global_to_local_expert_id(self, global_eid: int) -> Optional[int]:
"""Map a global expert id to this rank's local id, or `None` if
the expert is not owned by this rank. Pass-through when buffers
are globally-keyed."""
if not self.moe_use_local_expert_ids:
return global_eid
local = global_eid - self.moe_ep_rank * self._num_experts_local
return local if 0 <= local < self._num_experts_local else None
def _iter_local_expert_weights(
self,
weights: Union[torch.Tensor, Dict[int, torch.Tensor]],
cache_keys: Union[str, Dict[int, str]],
) -> Iterator[Tuple[int, torch.Tensor, str]]:
"""Yield `(local_expert_id, weight, cache_key)` triples for per-expert
MoE LoRA inputs, filtered/remapped to this rank's slice. Accepts either
a `{global_eid: 2D tensor}` dict or a 3D `[num_experts, *, *]` tensor."""
if isinstance(weights, dict):
assert isinstance(cache_keys, dict)
for gid, w in weights.items():
lid = self._global_to_local_expert_id(gid)
if lid is not None:
yield lid, w, cache_keys[gid]
return
if isinstance(weights, torch.Tensor) and weights.dim() == 3:
assert isinstance(cache_keys, str)
total = weights.shape[0]
if self.moe_use_local_expert_ids:
start = self.moe_ep_rank * self._num_experts_local
count = max(0, min(self._num_experts_local, total - start))
else:
start, count = 0, total
for i in range(count):
yield (
i,
weights[start + i],
append_cache_key_suffix(cache_keys, f"expert{start + i}"),
)
return
raise TypeError(
f"Expected dict or 3D torch.Tensor, got {type(weights).__name__}."
)
def _get_standard_shape(
self,
module_name: str,
base_model: torch.nn.Module,
max_lora_dim: int,
layer_idx: int,
) -> Tuple[int]:
"""Get 3D shape for standard (non-MoE) modules."""
input_dim, _ = get_hidden_dim(
module_name, self.base_hf_config, base_model, layer_idx
)
c = get_stacked_multiply(module_name, base_model)
if self.tp_size > 1 and module_name in ROW_PARALLELISM_LINEAR_LORA_NAMES:
input_dim = divide(input_dim, self.tp_size)
return (self.max_loras_per_batch, max_lora_dim * c, input_dim)
def get_lora_A_shape(
self,
module_name: str,
base_model: torch.nn.Module,
max_lora_dim: int,
layer_idx: int,
) -> Tuple[int]:
"""
Get shape for LoRA A weights. Automatically returns 3D or 4D based on module type.
Returns:
- Standard: [num_loras, rank, hidden_dim]
- MoE: [num_loras, num_experts, rank, hidden_dim]
"""
input_dim, _ = get_hidden_dim(
module_name, self.base_hf_config, base_model, layer_idx
)
c = get_stacked_multiply(module_name, base_model)
# MoE modules shard along `moe_tp_size`, not the outer `tp_size`.
effective_tp_size = (
self.moe_tp_size if self.is_moe_module(module_name) else self.tp_size
)
if (
effective_tp_size > 1
and module_name in ROW_PARALLELISM_LINEAR_LORA_NAMES
and module_name not in REPLICATED_LINEAR_LORA_NAMES
):
input_dim = divide(input_dim, effective_tp_size)
if self.is_moe_module(module_name):
expert_dim = self._get_num_local_experts(base_model)
if self.experts_shared_outer_loras and module_name == "gate_up_proj_moe":
expert_dim = 1
return (
self.max_loras_per_batch,
expert_dim,
max_lora_dim * c,
input_dim,
)
else:
return (self.max_loras_per_batch, max_lora_dim * c, input_dim)
def get_embedding_lora_A_shape(
self,
module_name: str,
base_model: torch.nn.Module,
max_lora_dim: int,
layer_idx: int,
) -> Tuple[int]:
input_dim, _ = get_hidden_dim(
module_name, self.base_hf_config, base_model, 0, self.lora_added_tokens_size
)
# Embedding LoRA A is kept unsharded (full vocab) across TP ranks.
# Each rank does a full lookup; no vocab-dimension splitting needed.
return (
self.max_loras_per_batch,
max_lora_dim,
input_dim,
)
def _column_parallel_lora_b_per_rank_dim(
self,
module_name: str,
total_output_dim: int,
effective_tp_size: int,
) -> int:
"""Per-rank LoRA B output dim for column-parallel modules.
For most modules this is just an even split. For ``qkv_proj`` when
``effective_tp_size > num_key_value_heads``, the underlying
:class:`QKVParallelLinear` *replicates* each KV head across
``tp_size // num_kv_heads`` ranks instead of dividing further, so
each rank owns ``head_dim`` of K/V (not ``head_dim * num_kv_heads
/ tp_size``). A naive ``divide(total, tp_size)`` undersizes the
buffer and produces a shape mismatch when the
:meth:`QKVParallelLinearWithLoRA.slice_lora_b_weights` slice runs.
"""
if module_name != "qkv_proj":
return divide(total_output_dim, effective_tp_size)
cfg = self.base_hf_config
if hasattr(cfg, "get_text_config"):
cfg = cfg.get_text_config()
num_kv_heads = getattr(cfg, "num_key_value_heads", None)
if num_kv_heads is None or num_kv_heads >= effective_tp_size:
return divide(total_output_dim, effective_tp_size)
head_dim = getattr(cfg, "head_dim", None) or (
cfg.hidden_size // cfg.num_attention_heads
)
kv_dim_total = 2 * num_kv_heads * head_dim
q_dim_total = total_output_dim - kv_dim_total
q_per_rank = divide(q_dim_total, effective_tp_size)
return q_per_rank + 2 * head_dim
def get_lora_B_shape(
self,
module_name: str,
base_model: torch.nn.Module,
max_lora_dim: int,
layer_idx: int,
) -> Tuple[int]:
"""
Get shape for LoRA B weights. Automatically returns 3D or 4D based on module type.
Returns:
- Standard: [num_loras, output_dim, rank]
- MoE: [num_loras, num_experts, output_dim, rank]
"""
_, output_dim = get_hidden_dim(
module_name, self.base_hf_config, base_model, layer_idx
)
# MoE modules shard along `moe_tp_size`, not the outer `tp_size`.
effective_tp_size = (
self.moe_tp_size if self.is_moe_module(module_name) else self.tp_size
)
if (
effective_tp_size > 1
and module_name not in ROW_PARALLELISM_LINEAR_LORA_NAMES
and module_name not in REPLICATED_LINEAR_LORA_NAMES
):
output_dim = self._column_parallel_lora_b_per_rank_dim(
module_name, output_dim, effective_tp_size
)
# Check if MoE module and return appropriate shape
if self.is_moe_module(module_name):
expert_dim = self._get_num_local_experts(base_model)
if self.experts_shared_outer_loras and module_name == "down_proj_moe":
expert_dim = 1
return (self.max_loras_per_batch, expert_dim, output_dim, max_lora_dim)
else:
return (self.max_loras_per_batch, output_dim, max_lora_dim)
def get_embedding_lora_B_shape(
self,
module_name: str,
base_model: torch.nn.Module,
max_lora_dim: int,
layer_idx: int,
) -> Tuple[int]:
_, output_dim = get_hidden_dim(
module_name, self.base_hf_config, base_model, 0, self.lora_added_tokens_size
)
# lm_head is column-parallel so B is sharded; embed_tokens B stays
# unsharded (base output is all-reduced to full embed_dim).
if module_name == "lm_head":
output_dim = get_lm_head_lora_b_shard_size(
output_dim,
shard_indices=self.lm_head_shard_indices,
)
return (
self.max_loras_per_batch,
output_dim,
max_lora_dim,
)
def init_buffers(self, base_model: torch.nn.Module):
self.base_model = base_model
device = next(base_model.parameters()).device
# Cached once so the per-expert load path doesn't re-walk the HF
# config for every adapter.
self._num_experts_local: int = self._get_num_local_experts(base_model)
def init_buffer(
buffer: Dict[str, List[torch.Tensor]],
target_modules: Set[str],
get_lora_shape_fn: Callable[[str, torch.nn.Module, int, int], Tuple[int]],
):
cfg = base_model.config
if hasattr(cfg, "get_text_config"):
cfg = cfg.get_text_config()
has_shared_experts = (
hasattr(cfg, "shared_expert_intermediate_size")
and cfg.shared_expert_intermediate_size > 0
) or (getattr(cfg, "n_shared_experts", 0) or 0) > 0
has_moe = self._has_moe_module(base_model)
# Shape functions automatically handle both 3D (standard) and 4D (MoE)
target_modules = target_modules - set(EMBEDDING_NAMES)
for module_name in target_modules:
# Special handling for ambiguous target modules that can be in different contexts
ambiguous_modules = {"gate_up_proj", "down_proj"}
if module_name in ambiguous_modules and has_moe:
# Allocate shared expert version (3D) only when model has shared experts
if has_shared_experts:
buffer[module_name] = [
torch.zeros(
get_lora_shape_fn(
module_name, base_model, self.max_lora_rank, idx
),
dtype=self.dtype,
device=device,
)
for idx in range(self.num_layer)
]
# MoE expert version (4D)
moe_key = f"{module_name}_moe"
buffer[moe_key] = [
torch.zeros(
get_lora_shape_fn(
moe_key, base_model, self.max_lora_rank, idx
),
dtype=self.dtype,
device=device,
)
for idx in range(self.num_layer)
]
else:
# Standard allocation for unambiguous modules
buffer[module_name] = [
torch.zeros(
get_lora_shape_fn(
module_name,
base_model,
self.max_lora_rank,
idx,
),
dtype=self.dtype,
device=device,
)
for idx in range(self.num_layer)
]
def init_embedding_buffer(
buffer: Dict[str, torch.Tensor],
target_modules: Set[str],
get_lora_shape_fn: Callable[[int], Tuple[int]],
):
target_modules = target_modules & set(EMBEDDING_NAMES)
for module_name in target_modules:
buffer[module_name] = torch.zeros(
get_lora_shape_fn(
module_name,
base_model,
self.max_lora_rank,
0,
),
dtype=self.dtype,
device=device,
)
if self.lora_added_tokens_size > 0:
self.new_embeddings_buffer["input_embeddings"] = torch.zeros(
(
self.max_loras_per_batch,
self.lora_added_tokens_size,
self.embedding_dim,
),
dtype=self.dtype,
device=device,
)
if "embed_tokens" in self.target_modules:
init_embedding_buffer(
self.embedding_A_buffer,
self.target_modules,
self.get_embedding_lora_A_shape,
)
init_embedding_buffer(
self.embedding_B_buffer,
self.target_modules,
self.get_embedding_lora_B_shape,
)
if "lm_head" in self.target_modules:
init_embedding_buffer(
self.lm_head_A_buffer,
self.target_modules,
self.get_embedding_lora_A_shape,
)
init_embedding_buffer(
self.lm_head_B_buffer,
self.target_modules,
self.get_embedding_lora_B_shape,
)
init_buffer(
self.A_buffer,
self.target_modules,
self.get_lora_A_shape,
)
init_buffer(
self.B_buffer,
self.target_modules,
self.get_lora_B_shape,
)
def _get_maybe_cached_weight_for_transfer(
self,
pinned_weight_store: Dict[str, torch.Tensor],
cache_key: str,
weight: torch.Tensor,
) -> torch.Tensor:
if (
not self.pin_memory_available
or weight.device.type != "cpu"
or weight.is_pinned()
):
return weight
if not self.enable_lora_overlap_loading:
return weight.pin_memory()
cached_weight = pinned_weight_store.get(cache_key)
if cached_weight is None:
cached_weight = weight.pin_memory()
pinned_weight_store[cache_key] = cached_weight
elif cached_weight.shape != weight.shape or cached_weight.dtype != weight.dtype:
raise ValueError(
f"LoRA pinned weight cache key collision for {cache_key!r}: "
f"cached shape={cached_weight.shape}, dtype={cached_weight.dtype}; "
f"new shape={weight.shape}, dtype={weight.dtype}."
)
return cached_weight
def prepare_lora_batch(
self,
cur_uids: Set[Optional[str]],
lora_adapters: Dict[str, LoRAAdapter],
lora_modules: List[Dict[str, BaseLayerWithLoRA]],
lora_refs: Dict[str, LoRARef],
lora_embed_tokens_module: Optional[BaseLayerWithLoRA],
lora_lm_head_module: Optional[BaseLayerWithLoRA],
):
def get_available_buffer_slot():
# 1. Prioritize empty slots
for buffer_id in range(self.max_loras_per_batch):
if self.buffer_id_to_uid[buffer_id] == EMPTY_SLOT:
return buffer_id
# 2. Memory pool is full, need to evict using policy
candidates = set()
for buffer_id in range(self.max_loras_per_batch):
uid = self.buffer_id_to_uid[buffer_id]
# Skip if this adapter is needed by current batch
if uid in cur_uids:
continue
# Skip if this adapter is pinned
if uid is not None:
lora_ref = lora_refs.get(uid)
if lora_ref and lora_ref.pinned:
continue
candidates.add(uid)
if not candidates:
raise ValueError(
"No available buffer slots found. Please ensure the number of active (pinned) loras is less than max_loras_per_batch."
)
# Prefer evicting LoRA adapters over the base model (None).
# Only evict None when the batch consists entirely of LoRA requests
# and no other adapters can be evicted.
non_none_candidates = candidates - {None}
if non_none_candidates:
# Prioritize evicting actual LoRA adapters
candidates_to_use = non_none_candidates
else:
# Only None is available for eviction (batch is all LoRA requests)
candidates_to_use = candidates
# Select victim using eviction policy
victim_uid = self.eviction_policy.select_victim(candidates_to_use)
# Evict the selected victim
victim_buffer_id = self.uid_to_buffer_id[victim_uid]
self.uid_to_buffer_id.pop(victim_uid)
self.eviction_policy.remove(victim_uid)
self.buffer_id_to_uid[victim_buffer_id] = EMPTY_SLOT
logger.debug(
f"Evicting LoRA {victim_uid} from buffer slot {victim_buffer_id}."
)
return victim_buffer_id
# Mark all adapters in current batch as used (for LRU tracking)
for uid in cur_uids:
self.eviction_policy.mark_used(uid)
for uid in cur_uids:
if uid not in self.uid_to_buffer_id:
buffer_id = get_available_buffer_slot()
lora_adapter = lora_adapters.get(uid, None)
self.load_lora_weight_to_buffer(
uid,
buffer_id,
lora_adapter,
lora_modules,
lora_embed_tokens_module,
lora_lm_head_module,
)
self.uid_to_buffer_id[uid] = buffer_id
self.buffer_id_to_uid[buffer_id] = uid
def load_lora_weight_to_buffer(
self,
uid: str,
buffer_id: int,
lora_adapter: LoRAAdapter,
lora_modules: List[Dict[str, BaseLayerWithLoRA]],
lora_embed_tokens_module: Optional[BaseLayerWithLoRA],
lora_lm_head_module: Optional[BaseLayerWithLoRA],
):
def load_lora_weight_tensor(
buffer_view: torch.Tensor, weight: Optional[torch.Tensor]
):
if weight is None:
# If the particular weight is not present in the adapter, we initialize the buffer to zero
# to avoid contamination from the residual weight of the evicted adapters.
buffer_view.zero_()
else:
assert (
buffer_view.shape == weight.shape
), f"LoRA buffer shape {buffer_view.shape} does not match weight shape {weight.shape}."
copy_weight_into_buffer(buffer_view, weight)
if uid is None:
for i in range(self.num_layer):
for k in self.A_buffer.keys():
self.A_buffer[k][i][buffer_id] = 0
for k in self.embedding_A_buffer.keys():
self.embedding_A_buffer[k][buffer_id] = 0
for k in self.lm_head_A_buffer.keys():
self.lm_head_A_buffer[k][buffer_id] = 0
return
assert lora_adapter is not None
lora_rank = lora_adapter.config.r
# Pre-validate weight names against target modules across all layers
# and embedding weights. This catches mismatches before any GPU
# buffers are mutated.
skipped_weight_names: set = set()
matched_modules: set = set()
all_weight_names: list = []
for layer in lora_adapter.layers:
all_weight_names.extend(layer.weights.keys())
if lora_adapter.embedding_layers:
all_weight_names.extend(lora_adapter.embedding_layers.keys())
for name in all_weight_names:
try:
target_module = get_target_module_name(name, self.target_modules)
matched_modules.add(target_module)
except ValueError:
skipped_weight_names.add(name)
if matched_modules:
logger.info(
"LoRA adapter '%s': loaded weights for target modules %s.",
uid,
sorted(matched_modules),
)
if skipped_weight_names:
msg = (
f"LoRA adapter '{uid}': {len(skipped_weight_names)} weight(s) "
f"skipped because they did not match any target module in "
f"{sorted(self.target_modules)}. Skipped weights: "
f"{sorted(skipped_weight_names)}. This likely indicates a "
f"mismatch between the adapter's target modules and the base "
f"model architecture."
)
if self.strict_loading:
raise ValueError(msg)
else:
logger.warning(msg)
for layer_id in range(self.num_layer):
layer = lora_adapter.layers[layer_id]
layer_weights = layer.weights
pinned_layer_weights = layer.pinned_weights
# - Standard: module_name -> torch.Tensor
# - MoE: module_name -> Dict[expert_id -> torch.Tensor]
temp_A_buffer: Dict[str, Union[torch.Tensor, Dict[int, torch.Tensor]]] = {
target_module: None for target_module in self.A_buffer
}
temp_B_buffer: Dict[str, Union[torch.Tensor, Dict[int, torch.Tensor]]] = {
target_module: None for target_module in self.B_buffer
}
temp_A_cache_keys: Dict[str, Optional[Union[str, Dict[int, str]]]] = {
target_module: None for target_module in self.A_buffer
}
temp_B_cache_keys: Dict[str, Optional[Union[str, Dict[int, str]]]] = {
target_module: None for target_module in self.B_buffer
}
for name, weights in layer_weights.items():
target_module = get_target_module_name(name, self.target_modules)
# Check if this is an MoE weight (has expert index in name)
expert_match = re.search(r"experts\.(\d+)\.", name)
if expert_match:
# Per-expert MoE weight — 2D tensors, one per expert
target_module = target_module + "_moe"
if temp_A_buffer[target_module] is None:
temp_A_buffer[target_module] = {}
temp_B_buffer[target_module] = {}
temp_A_cache_keys[target_module] = {}
temp_B_cache_keys[target_module] = {}
expert_id = int(expert_match.group(1))
if "lora_A" in name:
temp_A_buffer[target_module][expert_id] = weights
temp_A_cache_keys[target_module][expert_id] = name
else:
temp_B_buffer[target_module][expert_id] = weights
temp_B_cache_keys[target_module][expert_id] = name
elif "experts" in name and weights.dim() == 3:
# Shared outer MoE weight — 3D tensor [expert_dim, rank, hidden]
target_module = target_module + "_moe"
if "lora_A" in name:
temp_A_buffer[target_module] = weights
temp_A_cache_keys[target_module] = name
else:
temp_B_buffer[target_module] = weights
temp_B_cache_keys[target_module] = name
else:
# Standard weight — single tensor per module
if "lora_A" in name:
temp_A_buffer[target_module] = weights
temp_A_cache_keys[target_module] = name
else:
temp_B_buffer[target_module] = weights
temp_B_cache_keys[target_module] = name
# Track which buffer keys correspond to a real wrapped module on
# this layer. `temp_A/B_buffer` is seeded with every key in the
# global `A/B_buffer` (union across all layer types), but a
# hybrid-architecture layer (e.g. Qwen3.5 linear-attn vs full-attn,
# or first-k-dense MoE) only owns a subset of those modules. The
# buffer-copy loops below skip non-owned keys to avoid the
# redundant zero-fills on slots no `update_lora_info` ever points
# a forward-time module at.
active_target_modules: Set[str] = set()
cur_layer_modules = lora_modules[layer_id]
for module_name, module in cur_layer_modules.items():
# TODO (Jonahcb): check if the code can be refactored to avoid the special handling for FusedMoEWithLoRA
# Handle FusedMoEWithLoRA specially - it contains multiple target modules
from sglang.srt.lora.layers import FusedMoEWithLoRA
if isinstance(module, FusedMoEWithLoRA):
# Per-expert MoE weights are sharded along `moe_tp_size`
# (= tp_size // ep_size // dp_size), so the slice index
# must be `moe_tp_rank`. Passing the outer `tp_rank` here
# produces an off-the-end slice when ep_size < tp_size
# (e.g. tp=4 ep=2 → ranks 2,3 slice past intermediate_size).
moe_target_modules = ["gate_up_proj_moe", "down_proj_moe"]
for target_module in moe_target_modules:
active_target_modules.add(target_module)
if temp_A_buffer.get(target_module) is not None:
temp_A_buffer[target_module] = (
module.slice_moe_lora_a_weights(
temp_A_buffer[target_module],
self.moe_tp_rank,
target_module,
)
)
cache_keys = temp_A_cache_keys[target_module]
assert cache_keys is not None
temp_A_cache_keys[target_module] = append_cache_key_suffix(
cache_keys,
f"moe_tp{self.moe_tp_rank}",
)
if temp_B_buffer.get(target_module) is not None:
temp_B_buffer[target_module] = (
module.slice_moe_lora_b_weights(
temp_B_buffer[target_module],
self.moe_tp_rank,
target_module,
)
)
cache_keys = temp_B_cache_keys[target_module]
assert cache_keys is not None
temp_B_cache_keys[target_module] = append_cache_key_suffix(
cache_keys,
f"moe_tp{self.moe_tp_rank}",
)
continue
# Handle regular modules
target_module = get_target_module_name(module_name, self.target_modules)
# Mark active even if the adapter has no weights for this
# module on this layer — the buffer still needs to be zeroed
# (so a previously-evicted adapter's weights don't leak into
# the new slot) and the wrapped layer module will read it.
active_target_modules.add(target_module)
if temp_A_buffer[target_module] is None:
# Skip weight slicing if the weight is not present in the adapter
continue
# Handle standard modules
temp_A_buffer[target_module] = module.slice_lora_a_weights(
temp_A_buffer[target_module], self.tp_rank
)
cache_keys = temp_A_cache_keys[target_module]
assert cache_keys is not None
temp_A_cache_keys[target_module] = append_cache_key_suffix(
cache_keys,
f"tp{self.tp_rank}",
)
temp_B_buffer[target_module] = module.slice_lora_b_weights(
temp_B_buffer[target_module], self.tp_rank
)
cache_keys = temp_B_cache_keys[target_module]
assert cache_keys is not None
temp_B_cache_keys[target_module] = append_cache_key_suffix(
cache_keys,
f"tp{self.tp_rank}",
)
for name, weights in temp_A_buffer.items():
if name not in active_target_modules:
continue
c = get_stacked_multiply(name, self.base_model)
max_r = self.max_lora_rank
target_buffer = self.A_buffer[name][layer_id]
weights_cache_key = temp_A_cache_keys[name]
if name in ["gate_up_proj_moe", "down_proj_moe"]:
if self.experts_shared_outer_loras and name == "gate_up_proj_moe":
if weights is None:
representative_weight = None
buffer_view = target_buffer[
buffer_id, 0, : lora_rank * c, :
]
load_lora_weight_tensor(buffer_view, None)
elif isinstance(weights, torch.Tensor) and weights.dim() == 3:
if weights.shape[0] != 1:
raise ValueError(
f"experts_shared_outer_loras is enabled but "
f"gate_up_proj_moe lora_A has expert_dim="
f"{weights.shape[0]} (expected 1)."
)
assert isinstance(weights_cache_key, str)
weights = self._get_maybe_cached_weight_for_transfer(
pinned_layer_weights,
weights_cache_key,
weights,
)
representative_weight = weights[0]
buffer_view = target_buffer[
buffer_id, 0, : lora_rank * c, :
]
load_lora_weight_tensor(buffer_view, weights[0])
elif isinstance(weights, dict) and len(weights) > 0:
if len(weights) != 1:
raise ValueError(
f"experts_shared_outer_loras is enabled but "
f"gate_up_proj_moe lora_A dict has "
f"{len(weights)} entries (expected 1)."
)
rep = next(iter(weights.values()))
assert isinstance(weights_cache_key, dict)
rep_cache_key = next(iter(weights_cache_key.values()))
rep = self._get_maybe_cached_weight_for_transfer(
pinned_layer_weights, rep_cache_key, rep
)
representative_weight = rep
buffer_view = target_buffer[
buffer_id, 0, : lora_rank * c, :
]
load_lora_weight_tensor(buffer_view, rep)
else:
raise ValueError(
f"Unexpected weight format for shared outer gate_up_proj_moe lora_A: "
f"type={type(weights)}, "
f"shape={weights.shape if isinstance(weights, torch.Tensor) else 'N/A'}"
)
# Place each stacked component at max_rank-spaced
# positions so the kernel's [:max_r] / [max_r:2*max_r]
# slicing is correct.
target_buffer[buffer_id, 0].zero_()
if representative_weight is not None:
for ci in range(c):
buffer_view = target_buffer[
buffer_id, 0, ci * max_r : ci * max_r + lora_rank, :
]
load_lora_weight_tensor(
buffer_view,
representative_weight[
ci * lora_rank : (ci + 1) * lora_rank, :
],
)
elif isinstance(weights, (torch.Tensor, dict)):
# Zero first so any local-expert slot the adapter
# doesn't fill (e.g. out-of-rank under EP) is clean;
# then load owned slots at max_rank-spaced offsets so
# the MoE kernel's [:max_r] / [max_r:2*max_r] slicing
# is correct.
target_buffer[buffer_id].zero_()
assert isinstance(weights_cache_key, (str, dict))
for (
local_eid,
expert_weight,
expert_cache_key,
) in self._iter_local_expert_weights(
weights, weights_cache_key
):
if expert_weight is None:
continue
expert_weight = self._get_maybe_cached_weight_for_transfer(
pinned_layer_weights,
expert_cache_key,
expert_weight,
)
for ci in range(c):
buffer_view = target_buffer[
buffer_id,
local_eid,
ci * max_r : ci * max_r + lora_rank,
:,
]
load_lora_weight_tensor(
buffer_view,
expert_weight[
ci * lora_rank : (ci + 1) * lora_rank, :
],
)
else:
buffer_view = target_buffer[buffer_id, : lora_rank * c, :]
if weights is not None:
assert isinstance(weights_cache_key, str)
weights = self._get_maybe_cached_weight_for_transfer(
pinned_layer_weights,
weights_cache_key,
weights,
)
load_lora_weight_tensor(buffer_view, weights)
for name, weights in temp_B_buffer.items():
if name not in active_target_modules:
continue
target_buffer = self.B_buffer[name][layer_id]
weights_cache_key = temp_B_cache_keys[name]
if name in ["gate_up_proj_moe", "down_proj_moe"]:
if self.experts_shared_outer_loras and name == "down_proj_moe":
if weights is None:
buffer_view = target_buffer[buffer_id, 0, :, :lora_rank]
load_lora_weight_tensor(buffer_view, None)
elif isinstance(weights, torch.Tensor) and weights.dim() == 3:
if weights.shape[0] != 1:
raise ValueError(
f"experts_shared_outer_loras is enabled but "
f"down_proj_moe lora_B has expert_dim="
f"{weights.shape[0]} (expected 1)."
)
buffer_view = target_buffer[buffer_id, 0, :, :lora_rank]
w = weights[0]
assert isinstance(weights_cache_key, str)
if w is not None:
w = w * lora_adapter.scaling
w = self._get_maybe_cached_weight_for_transfer(
pinned_layer_weights,
append_cache_key_suffix(
weights_cache_key, "expert0"
),
w,
)
load_lora_weight_tensor(buffer_view, w)
elif isinstance(weights, dict) and len(weights) > 0:
if len(weights) != 1:
raise ValueError(
f"experts_shared_outer_loras is enabled but "
f"down_proj_moe lora_B dict has "
f"{len(weights)} entries (expected 1)."
)
rep = next(iter(weights.values()))
assert isinstance(weights_cache_key, dict)
rep_cache_key = next(iter(weights_cache_key.values()))
buffer_view = target_buffer[buffer_id, 0, :, :lora_rank]
if rep is not None:
rep = rep * lora_adapter.scaling
rep = self._get_maybe_cached_weight_for_transfer(
pinned_layer_weights,
rep_cache_key,
rep,
)
load_lora_weight_tensor(buffer_view, rep)
else:
raise ValueError(
f"Unexpected weight format for shared outer down_proj_moe lora_B: "
f"type={type(weights)}, "
f"shape={weights.shape if isinstance(weights, torch.Tensor) else 'N/A'}"
)
# Zero beyond loaded rank — MoE kernel reads full max_rank.
target_buffer[buffer_id, 0, :, lora_rank:].zero_()
elif isinstance(weights, (torch.Tensor, dict)):
# Zero out slots this rank owns but the adapter
# doesn't fill (padded-out / out-of-rank experts);
# then scale+load the ones it does.
target_buffer[buffer_id].zero_()
assert isinstance(weights_cache_key, (str, dict))
for (
local_eid,
w,
w_cache_key,
) in self._iter_local_expert_weights(
weights, weights_cache_key
):
if w is not None:
w = w * lora_adapter.scaling
w = self._get_maybe_cached_weight_for_transfer(
pinned_layer_weights,
w_cache_key,
w,
)
buffer_view = target_buffer[
buffer_id, local_eid, :, :lora_rank
]
load_lora_weight_tensor(buffer_view, w)
else:
buffer_view = target_buffer[buffer_id, :, :lora_rank]
if weights is not None:
assert isinstance(weights_cache_key, str)
weights = self._get_maybe_cached_weight_for_transfer(
pinned_layer_weights,
weights_cache_key,
weights,
)
load_lora_weight_tensor(buffer_view, weights)
if _SGLANG_EXPERIMENTAL_LORA_OPTI:
# Zero beyond loaded rank: the experimental dense LoRA-B kernel
# contracts over the full padded max_rank, so the tail must be clean.
target_buffer[buffer_id, :, lora_rank:].zero_()
if lora_adapter.embedding_layers:
org_vocab_size = self.base_hf_config.vocab_size
lora_added_tokens_size = lora_adapter.config.lora_added_tokens_size
pinned_embedding_layers = lora_adapter.pinned_embedding_layers
pinned_added_tokens_embeddings = lora_adapter.pinned_added_tokens_embeddings
# Only when LoRA is applied to the embedding layer will it have the extra-token issue that needs to be resolved.
# Load embeddings weights for extra tokens to buffer
if lora_adapter.added_tokens_embeddings:
for name, weights in lora_adapter.added_tokens_embeddings.items():
if "input_embeddings" in name:
buffer_view = self.new_embeddings_buffer["input_embeddings"][
buffer_id, :lora_added_tokens_size
]
weights = self._get_maybe_cached_weight_for_transfer(
pinned_added_tokens_embeddings,
name,
weights,
)
load_lora_weight_tensor(buffer_view, weights)
# load vocab_emb and lm_head
for name, weights in lora_adapter.embedding_layers.items():
target_module = get_target_module_name(name, self.target_modules)
if (
target_module == "embed_tokens"
and "embed_tokens" in name
and ("lora_embedding_A" in name or "lora_A" in name)
):
buffer_view = self.embedding_A_buffer[target_module][
buffer_id,
:lora_rank,
: (org_vocab_size + lora_added_tokens_size),
]
weights = self._get_maybe_cached_weight_for_transfer(
pinned_embedding_layers,
name,
weights,
)
load_lora_weight_tensor(buffer_view, weights)
elif (
target_module == "embed_tokens"
and "embed_tokens" in name
and ("lora_embedding_B" in name or "lora_B" in name)
):
lora_b_weights = weights
# TP is supported by keeping embedding LoRA B unsharded;
# no slicing needed.
buffer_view = self.embedding_B_buffer[target_module][
buffer_id, :, :lora_rank
]
lora_b_weights = self._get_maybe_cached_weight_for_transfer(
pinned_embedding_layers,
name,
lora_b_weights,
)
load_lora_weight_tensor(buffer_view, lora_b_weights)
elif (
target_module == "lm_head"
and lora_lm_head_module is not None
and "lm_head" in name
and ("lora_embedding_A" in name or "lora_A" in name)
):
buffer_view = self.lm_head_A_buffer[target_module][
# buffer_id, :, :lora_rank
buffer_id,
:lora_rank,
:,
]
weights = self._get_maybe_cached_weight_for_transfer(
pinned_embedding_layers,
name,
weights,
)
load_lora_weight_tensor(buffer_view, weights)
elif (
target_module == "lm_head"
and lora_lm_head_module is not None
and "lm_head" in name
and ("lora_embedding_B" in name or "lora_B" in name)
):
assert lora_lm_head_module is not None
lora_b_weights = weights
# Slice B along vocab dimension for this TP rank
if self.tp_size > 1:
lora_b_weights = lora_lm_head_module.slice_lora_b_weights(
lora_b_weights, self.tp_rank
)
cache_key = append_cache_key_suffix(name, f"tp{self.tp_rank}")
else:
cache_key = name
buffer_view = self.lm_head_B_buffer[target_module][
buffer_id,
: lora_b_weights.shape[0],
:lora_rank,
]
lora_b_weights = self._get_maybe_cached_weight_for_transfer(
pinned_embedding_layers,
cache_key,
lora_b_weights,
)
load_lora_weight_tensor(buffer_view, lora_b_weights)
elif (
target_module == "lm_head"
and "lm_head" in name
and (
"lora_embedding_A" in name
or "lora_A" in name
or "lora_embedding_B" in name
or "lora_B" in name
)
):
# Only assert for genuine LoRA A/B deltas. Non-LoRA adapter
# entries (e.g. `base_layer.weight` emitted by PEFT for
# tied-embedding lm_head) fall through and are handled by
# the base weight loader, mirroring embed_tokens behavior.
# Non-last PP stages do not own lm_head, so adapters can
# legitimately contain lm_head LoRA weights with no local
# module to load them into, otherwise we should have been able to load this weight.
assert (
not get_pp_group().is_last_rank
), f"Failed to load lm_head LoRA weight: {name}, this is only expected to happen on non-last PP stages."
continue
else:
# Zero out embedding/lm_head buffers for adapters without embedding LoRA
# to avoid using garbage values from uninitialized memory
for k in self.embedding_A_buffer.keys():
self.embedding_A_buffer[k][buffer_id].zero_()
for k in self.embedding_B_buffer.keys():
self.embedding_B_buffer[k][buffer_id].zero_()
for k in self.lm_head_A_buffer.keys():
self.lm_head_A_buffer[k][buffer_id].zero_()
for k in self.lm_head_B_buffer.keys():
self.lm_head_B_buffer[k][buffer_id].zero_()
if (
self.lora_added_tokens_size > 0
and "input_embeddings" in self.new_embeddings_buffer
):
self.new_embeddings_buffer["input_embeddings"][buffer_id].zero_()
def get_embedding_tensor(
self, target_module: str, lora_type: LoRAType
) -> Optional[torch.Tensor]:
"""
Get LoRA tensor for non-layer modules (embed_tokens, lm_head).
Args:
target_module: Module name, either "embed_tokens" or "lm_head"
lora_type: Either LoRAType.LORA_A or LoRAType.LORA_B
Returns:
The corresponding buffer tensor, or None if not available
"""
if target_module == "added_tokens":
if (
self.lora_added_tokens_size is not None
and self.lora_added_tokens_size > 0
):
return self.new_embeddings_buffer["input_embeddings"]
return None
elif target_module == "embed_tokens":
if lora_type == LoRAType.LORA_A:
return self.embedding_A_buffer[target_module]
return self.embedding_B_buffer[target_module]
elif target_module == "lm_head":
if lora_type == LoRAType.LORA_A:
return self.lm_head_A_buffer[target_module]
return self.lm_head_B_buffer[target_module]
raise ValueError(
f"Invalid target_module '{target_module}'. "
f"Expected 'embed_tokens' or 'lm_head'."
)
def get_tensor(
self, target_module: str, layer_id: int, lora_type: LoRAType
) -> torch.Tensor:
"""
Get LoRA tensor buffer (automatically handles both 3D and 4D tensors).
if lora_type == LoRAType.LORA_A:
return self.A_buffer[target_module][layer_id]
Args:
target_module: Target module name (e.g., 'gate_up_proj' or 'gate_up_proj_moe' for MoE)
layer_id: Layer index
lora_type: LoRAType.LORA_A or LoRAType.LORA_B
Returns:
- 3D tensor [num_loras, rank, hidden] for standard modules
- 4D tensor [num_loras, num_experts, rank, hidden] for MoE modules
"""
buffer_dict = self.A_buffer if lora_type == LoRAType.LORA_A else self.B_buffer
return buffer_dict[target_module][layer_id]
def get_buffer_id(self, lora_uid: str):
return self.uid_to_buffer_id[lora_uid]