Update LoRA Weights via Tensor (#16226)
Co-authored-by: PopSoda2002 <zhouhp.me@gmail.com>
This commit is contained in:
@@ -47,6 +47,7 @@ from sglang.srt.managers.io_struct import (
|
|||||||
GenerateReqInput,
|
GenerateReqInput,
|
||||||
GetWeightsByNameReqInput,
|
GetWeightsByNameReqInput,
|
||||||
InitWeightsUpdateGroupReqInput,
|
InitWeightsUpdateGroupReqInput,
|
||||||
|
LoadLoRAAdapterFromTensorsReqInput,
|
||||||
LoadLoRAAdapterReqInput,
|
LoadLoRAAdapterReqInput,
|
||||||
MultimodalDataInputFormat,
|
MultimodalDataInputFormat,
|
||||||
ReleaseMemoryOccupationReqInput,
|
ReleaseMemoryOccupationReqInput,
|
||||||
@@ -600,6 +601,22 @@ class Engine(EngineBase):
|
|||||||
self.tokenizer_manager.get_weights_by_name(obj, None)
|
self.tokenizer_manager.get_weights_by_name(obj, None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def load_lora_adapter_from_tensors(
|
||||||
|
self, lora_name: str, tensors: List[Tuple[str, torch.Tensor]], config_dict: Dict
|
||||||
|
):
|
||||||
|
# Load LoRA adapter again
|
||||||
|
serialized_tensors = MultiprocessingSerializer.serialize(
|
||||||
|
tensors, output_str=True
|
||||||
|
)
|
||||||
|
lora_req = LoadLoRAAdapterFromTensorsReqInput(
|
||||||
|
lora_name=lora_name,
|
||||||
|
config_dict=config_dict,
|
||||||
|
serialized_tensors=serialized_tensors,
|
||||||
|
)
|
||||||
|
return self.loop.run_until_complete(
|
||||||
|
self.tokenizer_manager.load_lora_adapter_from_tensors(lora_req, None)
|
||||||
|
)
|
||||||
|
|
||||||
def load_lora_adapter(self, lora_name: str, lora_path: str, pinned: bool = False):
|
def load_lora_adapter(self, lora_name: str, lora_path: str, pinned: bool = False):
|
||||||
"""Load a new LoRA adapter without re-launching the engine."""
|
"""Load a new LoRA adapter without re-launching the engine."""
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ from sglang.srt.managers.io_struct import (
|
|||||||
GetWeightsByNameReqInput,
|
GetWeightsByNameReqInput,
|
||||||
InitWeightsSendGroupForRemoteInstanceReqInput,
|
InitWeightsSendGroupForRemoteInstanceReqInput,
|
||||||
InitWeightsUpdateGroupReqInput,
|
InitWeightsUpdateGroupReqInput,
|
||||||
|
LoadLoRAAdapterFromTensorsReqInput,
|
||||||
LoadLoRAAdapterReqInput,
|
LoadLoRAAdapterReqInput,
|
||||||
OpenSessionReqInput,
|
OpenSessionReqInput,
|
||||||
ParseFunctionCallReq,
|
ParseFunctionCallReq,
|
||||||
@@ -1062,6 +1063,21 @@ async def load_lora_adapter(obj: LoadLoRAAdapterReqInput, request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.api_route("/load_lora_adapter_from_tensors", methods=["POST"])
|
||||||
|
async def load_lora_adapter_from_tensors(
|
||||||
|
obj: LoadLoRAAdapterFromTensorsReqInput, request: Request
|
||||||
|
):
|
||||||
|
"""Load a new LoRA adapter from tensors without re-launching the server."""
|
||||||
|
result = await _global_state.tokenizer_manager.load_lora_adapter_from_tensors(
|
||||||
|
obj, request
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
return ORJSONResponse(result, status_code=HTTPStatus.OK)
|
||||||
|
else:
|
||||||
|
return ORJSONResponse(result, status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
@app.api_route("/unload_lora_adapter", methods=["POST"])
|
@app.api_route("/unload_lora_adapter", methods=["POST"])
|
||||||
async def unload_lora_adapter(obj: UnloadLoRAAdapterReqInput, request: Request):
|
async def unload_lora_adapter(obj: UnloadLoRAAdapterReqInput, request: Request):
|
||||||
"""Load a new LoRA adapter without re-launching the server."""
|
"""Load a new LoRA adapter without re-launching the server."""
|
||||||
|
|||||||
@@ -74,24 +74,34 @@ class LoRAAdapter(nn.Module):
|
|||||||
self.embedding_layers: Dict[str, torch.Tensor] = {}
|
self.embedding_layers: Dict[str, torch.Tensor] = {}
|
||||||
self.added_tokens_embeddings: Dict[str, torch.Tensor] = {}
|
self.added_tokens_embeddings: Dict[str, torch.Tensor] = {}
|
||||||
|
|
||||||
# initialize the LoRA weights to cpu
|
|
||||||
def initialize_weights(self):
|
def initialize_weights(self):
|
||||||
model_path = self.config.path
|
model_path = self.config.path
|
||||||
loader = DefaultModelLoader(self.load_config)
|
loader = DefaultModelLoader(self.load_config)
|
||||||
revision = getattr(self.config.hf_config, "revision", None)
|
revision = getattr(self.config.hf_config, "revision", None)
|
||||||
|
|
||||||
# Get normalized target modules for filtering
|
# Get normalized target modules for filtering
|
||||||
|
for name, loaded_weight in loader._get_weights_iterator(
|
||||||
|
DefaultModelLoader.Source(
|
||||||
|
model_path, revision=revision, fall_back_to_pt=True
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self._process_weight(name, loaded_weight)
|
||||||
|
|
||||||
|
self._normalize_weights()
|
||||||
|
|
||||||
|
def initialize_weights_from_tensors(self, tensors: Dict[str, torch.Tensor]):
|
||||||
|
for name, tensor in tensors.items():
|
||||||
|
self._process_weight(name, tensor)
|
||||||
|
|
||||||
|
self._normalize_weights()
|
||||||
|
|
||||||
|
def _process_weight(self, name: str, loaded_weight: torch.Tensor):
|
||||||
from sglang.srt.lora.utils import get_normalized_target_modules
|
from sglang.srt.lora.utils import get_normalized_target_modules
|
||||||
|
|
||||||
normalized_target_modules = get_normalized_target_modules(
|
normalized_target_modules = get_normalized_target_modules(
|
||||||
self.config.target_modules
|
self.config.target_modules
|
||||||
)
|
)
|
||||||
|
|
||||||
for name, loaded_weight in loader._get_weights_iterator(
|
|
||||||
DefaultModelLoader.Source(
|
|
||||||
model_path, revision=revision, fall_back_to_pt=True
|
|
||||||
)
|
|
||||||
):
|
|
||||||
layer_id = get_layer_id(name)
|
layer_id = get_layer_id(name)
|
||||||
if layer_id is not None:
|
if layer_id is not None:
|
||||||
self.layers[layer_id].weights[name] = loaded_weight.cpu()
|
self.layers[layer_id].weights[name] = loaded_weight.cpu()
|
||||||
@@ -112,6 +122,7 @@ class LoRAAdapter(nn.Module):
|
|||||||
f"but the loaded weight has {loaded_weight.shape[0]} extra vocab size"
|
f"but the loaded weight has {loaded_weight.shape[0]} extra vocab size"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _normalize_weights(self):
|
||||||
# normalize kv_proj and gate_up_proj
|
# normalize kv_proj and gate_up_proj
|
||||||
for layer in self.layers:
|
for layer in self.layers:
|
||||||
weight_names = list(layer.weights.keys())
|
weight_names = list(layer.weights.keys())
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from huggingface_hub import snapshot_download
|
from huggingface_hub import snapshot_download
|
||||||
|
|
||||||
@@ -21,20 +22,34 @@ from huggingface_hub import snapshot_download
|
|||||||
class LoRAConfig:
|
class LoRAConfig:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: str,
|
path: Optional[str] = None,
|
||||||
|
config_dict: Optional[Dict] = None,
|
||||||
|
added_tokens_config: Optional[Dict] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.hf_config = self.get_lora_config()
|
|
||||||
self.target_modules = self.hf_config["target_modules"]
|
|
||||||
|
|
||||||
|
if config_dict is not None:
|
||||||
|
self.hf_config = config_dict
|
||||||
|
self.added_tokens_config = added_tokens_config
|
||||||
|
else:
|
||||||
|
self.hf_config = self.get_lora_config()
|
||||||
|
self.added_tokens_config = self.get_added_tokens_config()
|
||||||
|
|
||||||
|
self.target_modules = self.hf_config["target_modules"]
|
||||||
self.r = self.hf_config["r"]
|
self.r = self.hf_config["r"]
|
||||||
self.lora_alpha = self.hf_config["lora_alpha"]
|
self.lora_alpha = self.hf_config["lora_alpha"]
|
||||||
|
|
||||||
self.added_tokens_config = self.get_added_tokens_config()
|
|
||||||
self.lora_added_tokens_size = (
|
self.lora_added_tokens_size = (
|
||||||
len(self.added_tokens_config) if self.added_tokens_config is not None else 0
|
len(self.added_tokens_config) if self.added_tokens_config is not None else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(
|
||||||
|
cls,
|
||||||
|
config_dict: Dict,
|
||||||
|
added_tokens_config: Optional[Dict] = None,
|
||||||
|
) -> "LoRAConfig":
|
||||||
|
return cls(config_dict=config_dict, added_tokens_config=added_tokens_config)
|
||||||
|
|
||||||
def get_lora_config(self, dummy=False):
|
def get_lora_config(self, dummy=False):
|
||||||
if dummy:
|
if dummy:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -444,6 +444,56 @@ class LoRAManager:
|
|||||||
lora_adapter.initialize_weights()
|
lora_adapter.initialize_weights()
|
||||||
self.loras[lora_ref.lora_id] = lora_adapter
|
self.loras[lora_ref.lora_id] = lora_adapter
|
||||||
|
|
||||||
|
def load_lora_weights_from_tensors(
|
||||||
|
self, lora_ref: LoRARef, tensors: Dict[str, torch.Tensor]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Load the weights of a LoRA adapter from tensors to CPU memory.
|
||||||
|
"""
|
||||||
|
lora_adapter = LoRAAdapter(
|
||||||
|
lora_ref.lora_id,
|
||||||
|
self.configs[lora_ref.lora_id],
|
||||||
|
self.base_hf_config,
|
||||||
|
self.load_config,
|
||||||
|
self.lora_backend,
|
||||||
|
)
|
||||||
|
lora_adapter.initialize_weights_from_tensors(tensors)
|
||||||
|
self.loras[lora_ref.lora_id] = lora_adapter
|
||||||
|
|
||||||
|
def load_lora_adapter_from_tensors(
|
||||||
|
self,
|
||||||
|
lora_ref: LoRARef,
|
||||||
|
tensors: Dict[str, torch.Tensor],
|
||||||
|
config_dict: Dict,
|
||||||
|
added_tokens_config: Optional[Dict] = None,
|
||||||
|
) -> LoRAUpdateOutput:
|
||||||
|
"""
|
||||||
|
Load a single LoRA adapter from tensors and config dict.
|
||||||
|
"""
|
||||||
|
assert (
|
||||||
|
lora_ref.lora_name is not None and lora_ref.lora_path is not None
|
||||||
|
), "LoRARef must have both lora_name and lora_path set for loading."
|
||||||
|
assert (
|
||||||
|
lora_ref.lora_id not in self.loras
|
||||||
|
), f"LoRA adapter with ID {lora_ref.lora_id} is already loaded. This should have been verified before request is sent to the backend."
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_adapter = LoRAConfig.from_dict(config_dict, added_tokens_config)
|
||||||
|
self.validate_new_adapter(new_adapter, lora_ref)
|
||||||
|
self.configs[lora_ref.lora_id] = new_adapter
|
||||||
|
|
||||||
|
self.load_lora_weights_from_tensors(lora_ref, tensors)
|
||||||
|
|
||||||
|
self.lora_refs[lora_ref.lora_id] = lora_ref
|
||||||
|
self.num_pinned_loras += int(lora_ref.pinned)
|
||||||
|
except Exception as e:
|
||||||
|
return self.create_lora_update_result(
|
||||||
|
success=False,
|
||||||
|
error_message=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.create_lora_update_result(success=True)
|
||||||
|
|
||||||
def init_memory_pool(self):
|
def init_memory_pool(self):
|
||||||
"""(Re)initialize the LoRA memory pool based on the current configurations."""
|
"""(Re)initialize the LoRA memory pool based on the current configurations."""
|
||||||
self.memory_pool = LoRAMemoryPool(
|
self.memory_pool = LoRAMemoryPool(
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ def get_hidden_dim(
|
|||||||
# if contain extra tokens will be added; otherwise is 0.
|
# if contain extra tokens will be added; otherwise is 0.
|
||||||
return config.hidden_size, config.vocab_size + lora_added_vocab_size
|
return config.hidden_size, config.vocab_size + lora_added_vocab_size
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError(
|
||||||
|
"get_hidden_dim not implemented for " + module_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_normalized_target_modules(
|
def get_normalized_target_modules(
|
||||||
|
|||||||
@@ -1643,6 +1643,24 @@ class UnloadLoRAAdapterReqInput(BaseReq):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoadLoRAAdapterFromTensorsReqInput(BaseReq):
|
||||||
|
lora_name: str
|
||||||
|
config_dict: Dict[str, Any]
|
||||||
|
serialized_tensors: str
|
||||||
|
pinned: bool = False
|
||||||
|
added_tokens_config: Optional[Dict[str, Any]] = None
|
||||||
|
lora_id: Optional[str] = None
|
||||||
|
|
||||||
|
def to_ref(self) -> LoRARef:
|
||||||
|
return LoRARef(
|
||||||
|
lora_id=self.lora_id,
|
||||||
|
lora_name=self.lora_name,
|
||||||
|
lora_path="__tensor__",
|
||||||
|
pinned=self.pinned,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LoRAUpdateOutput(BaseReq):
|
class LoRAUpdateOutput(BaseReq):
|
||||||
success: bool
|
success: bool
|
||||||
@@ -1650,7 +1668,9 @@ class LoRAUpdateOutput(BaseReq):
|
|||||||
loaded_adapters: Optional[Dict[str, LoRARef]] = None
|
loaded_adapters: Optional[Dict[str, LoRARef]] = None
|
||||||
|
|
||||||
|
|
||||||
LoadLoRAAdapterReqOutput = UnloadLoRAAdapterReqOutput = LoRAUpdateOutput
|
LoadLoRAAdapterReqOutput = UnloadLoRAAdapterReqOutput = (
|
||||||
|
LoadLoRAAdapterFromTensorsReqOutput
|
||||||
|
) = LoRAUpdateOutput
|
||||||
|
|
||||||
|
|
||||||
class BlockReqType(Enum):
|
class BlockReqType(Enum):
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ from sglang.srt.managers.io_struct import (
|
|||||||
InitWeightsSendGroupForRemoteInstanceReqInput,
|
InitWeightsSendGroupForRemoteInstanceReqInput,
|
||||||
InitWeightsSendGroupForRemoteInstanceReqOutput,
|
InitWeightsSendGroupForRemoteInstanceReqOutput,
|
||||||
InitWeightsUpdateGroupReqInput,
|
InitWeightsUpdateGroupReqInput,
|
||||||
|
LoadLoRAAdapterFromTensorsReqInput,
|
||||||
|
LoadLoRAAdapterFromTensorsReqOutput,
|
||||||
LoadLoRAAdapterReqInput,
|
LoadLoRAAdapterReqInput,
|
||||||
LoadLoRAAdapterReqOutput,
|
LoadLoRAAdapterReqOutput,
|
||||||
OpenSessionReqInput,
|
OpenSessionReqInput,
|
||||||
@@ -1052,6 +1054,10 @@ class Scheduler(
|
|||||||
(RpcReqInput, self.handle_rpc_request),
|
(RpcReqInput, self.handle_rpc_request),
|
||||||
(ExpertDistributionReq, self.expert_distribution_handle),
|
(ExpertDistributionReq, self.expert_distribution_handle),
|
||||||
(LoadLoRAAdapterReqInput, self.load_lora_adapter),
|
(LoadLoRAAdapterReqInput, self.load_lora_adapter),
|
||||||
|
(
|
||||||
|
LoadLoRAAdapterFromTensorsReqInput,
|
||||||
|
self.load_lora_adapter_from_tensors,
|
||||||
|
),
|
||||||
(UnloadLoRAAdapterReqInput, self.unload_lora_adapter),
|
(UnloadLoRAAdapterReqInput, self.unload_lora_adapter),
|
||||||
(GetLoadReqInput, self.get_load),
|
(GetLoadReqInput, self.get_load),
|
||||||
(PauseGenerationReqInput, self.pause_generation),
|
(PauseGenerationReqInput, self.pause_generation),
|
||||||
@@ -2703,6 +2709,14 @@ class Scheduler(
|
|||||||
result = self.tp_worker.load_lora_adapter(recv_req)
|
result = self.tp_worker.load_lora_adapter(recv_req)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def load_lora_adapter_from_tensors(
|
||||||
|
self, recv_req: LoadLoRAAdapterFromTensorsReqInput
|
||||||
|
) -> LoadLoRAAdapterFromTensorsReqOutput:
|
||||||
|
"""In-place loading a new lora adapter from serialized tensors."""
|
||||||
|
|
||||||
|
result = self.tp_worker.load_lora_adapter_from_tensors(recv_req)
|
||||||
|
return result
|
||||||
|
|
||||||
def unload_lora_adapter(
|
def unload_lora_adapter(
|
||||||
self, recv_req: UnloadLoRAAdapterReqInput
|
self, recv_req: UnloadLoRAAdapterReqInput
|
||||||
) -> UnloadLoRAAdapterReqOutput:
|
) -> UnloadLoRAAdapterReqOutput:
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ from sglang.srt.managers.io_struct import (
|
|||||||
InitWeightsSendGroupForRemoteInstanceReqOutput,
|
InitWeightsSendGroupForRemoteInstanceReqOutput,
|
||||||
InitWeightsUpdateGroupReqInput,
|
InitWeightsUpdateGroupReqInput,
|
||||||
InitWeightsUpdateGroupReqOutput,
|
InitWeightsUpdateGroupReqOutput,
|
||||||
|
LoadLoRAAdapterFromTensorsReqInput,
|
||||||
|
LoadLoRAAdapterFromTensorsReqOutput,
|
||||||
LoadLoRAAdapterReqInput,
|
LoadLoRAAdapterReqInput,
|
||||||
LoadLoRAAdapterReqOutput,
|
LoadLoRAAdapterReqOutput,
|
||||||
LoRAUpdateOutput,
|
LoRAUpdateOutput,
|
||||||
@@ -617,6 +619,76 @@ class TokenizerCommunicatorMixin:
|
|||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def load_lora_adapter_from_tensors(
|
||||||
|
self: TokenizerManager,
|
||||||
|
obj: LoadLoRAAdapterFromTensorsReqInput,
|
||||||
|
_: Optional[fastapi.Request] = None,
|
||||||
|
) -> LoadLoRAAdapterFromTensorsReqOutput:
|
||||||
|
self.auto_create_handle_loop()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.server_args.enable_lora:
|
||||||
|
raise ValueError(
|
||||||
|
"LoRA is not enabled. Please set `--enable-lora` to enable LoRA."
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
self.server_args.dp_size == 1
|
||||||
|
), "dp_size must be 1 for dynamic lora loading"
|
||||||
|
logger.info(
|
||||||
|
"Start load Lora adapter from tensors. Lora name=%s",
|
||||||
|
obj.lora_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
async with self.lora_update_lock:
|
||||||
|
new_adapter = LoRARef(
|
||||||
|
lora_name=obj.lora_name,
|
||||||
|
lora_path="__tensor__",
|
||||||
|
pinned=obj.pinned,
|
||||||
|
)
|
||||||
|
obj.lora_id = new_adapter.lora_id
|
||||||
|
result = (await self.update_lora_adapter_communicator(obj))[0]
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
await self.lora_registry.register(new_adapter)
|
||||||
|
self.lora_ref_cache[obj.lora_name] = new_adapter
|
||||||
|
if self.server_args.max_loaded_loras is not None:
|
||||||
|
while (
|
||||||
|
self.lora_registry.num_registered_loras
|
||||||
|
> self.server_args.max_loaded_loras
|
||||||
|
):
|
||||||
|
lru_lora_name = await self.lora_registry.lru_lora_name(
|
||||||
|
exclude_pinned=True
|
||||||
|
)
|
||||||
|
if lru_lora_name is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Didn't find any LoRA adapters when trying to evict LRU LoRA adapter. "
|
||||||
|
f"LoRA registry is: {self.lora_registry._registry}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Unloading least recently used LoRA adapter '{lru_lora_name}' "
|
||||||
|
f"(current number of adapters: {self.lora_registry.num_registered_loras}, "
|
||||||
|
f"max allowed: {self.server_args.max_loaded_loras})"
|
||||||
|
)
|
||||||
|
|
||||||
|
unload_result = await self._unload_lora_adapter_locked(
|
||||||
|
UnloadLoRAAdapterReqInput(lora_name=lru_lora_name)
|
||||||
|
)
|
||||||
|
if not unload_result.success:
|
||||||
|
raise ValueError(
|
||||||
|
f"Error while unloading LRU LoRA adapter '{lru_lora_name}': "
|
||||||
|
f"{unload_result.error_message}"
|
||||||
|
)
|
||||||
|
del result.loaded_adapters[lru_lora_name]
|
||||||
|
|
||||||
|
return result
|
||||||
|
except ValueError as e:
|
||||||
|
return LoadLoRAAdapterFromTensorsReqOutput(
|
||||||
|
success=False,
|
||||||
|
error_message=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
async def unload_lora_adapter(
|
async def unload_lora_adapter(
|
||||||
self: TokenizerManager,
|
self: TokenizerManager,
|
||||||
obj: UnloadLoRAAdapterReqInput,
|
obj: UnloadLoRAAdapterReqInput,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from sglang.srt.managers.io_struct import (
|
|||||||
GetWeightsByNameReqInput,
|
GetWeightsByNameReqInput,
|
||||||
InitWeightsSendGroupForRemoteInstanceReqInput,
|
InitWeightsSendGroupForRemoteInstanceReqInput,
|
||||||
InitWeightsUpdateGroupReqInput,
|
InitWeightsUpdateGroupReqInput,
|
||||||
|
LoadLoRAAdapterFromTensorsReqInput,
|
||||||
LoadLoRAAdapterReqInput,
|
LoadLoRAAdapterReqInput,
|
||||||
SendWeightsToRemoteInstanceReqInput,
|
SendWeightsToRemoteInstanceReqInput,
|
||||||
UnloadLoRAAdapterReqInput,
|
UnloadLoRAAdapterReqInput,
|
||||||
@@ -189,6 +190,20 @@ class BaseTpWorker(ABC):
|
|||||||
result = self.model_runner.unload_lora_adapter(recv_req.to_ref())
|
result = self.model_runner.unload_lora_adapter(recv_req.to_ref())
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def load_lora_adapter_from_tensors(
|
||||||
|
self, recv_req: LoadLoRAAdapterFromTensorsReqInput
|
||||||
|
):
|
||||||
|
# The LoRA code handles TP sharding internally using slice_lora_a_weights
|
||||||
|
# and slice_lora_b_weights methods (see lora/layers.py:46-49, mem_pool.py:437-440).
|
||||||
|
tensors = MultiprocessingSerializer.deserialize(recv_req.serialized_tensors)
|
||||||
|
result = self.model_runner.load_lora_adapter_from_tensors(
|
||||||
|
recv_req.to_ref(),
|
||||||
|
tensors,
|
||||||
|
recv_req.config_dict,
|
||||||
|
recv_req.added_tokens_config,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def can_run_lora_batch(self, lora_ids: list[str]) -> bool:
|
def can_run_lora_batch(self, lora_ids: list[str]) -> bool:
|
||||||
lora_ids_set = set(lora_ids) if isinstance(lora_ids, list) else lora_ids
|
lora_ids_set = set(lora_ids) if isinstance(lora_ids, list) else lora_ids
|
||||||
return self.model_runner.lora_manager.validate_lora_batch(lora_ids_set)
|
return self.model_runner.lora_manager.validate_lora_batch(lora_ids_set)
|
||||||
|
|||||||
@@ -1434,6 +1434,16 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def load_lora_adapter_from_tensors(
|
||||||
|
self, lora_ref: LoRARef, tensors, config_dict, added_tokens_config=None
|
||||||
|
):
|
||||||
|
logger.info(f"LoRA adapter loading from tensors starts: {lora_ref}.")
|
||||||
|
result = self.lora_manager.load_lora_adapter_from_tensors(
|
||||||
|
lora_ref, tensors, config_dict, added_tokens_config
|
||||||
|
)
|
||||||
|
logger.info(f"LoRA adapter loading from tensors completes: {lora_ref}.")
|
||||||
|
return result
|
||||||
|
|
||||||
def unload_lora_adapter(self, lora_ref: LoRARef):
|
def unload_lora_adapter(self, lora_ref: LoRARef):
|
||||||
"""Unload a lora adapter that was previously loaded during initialization or dynamic loading."""
|
"""Unload a lora adapter that was previously loaded during initialization or dynamic loading."""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||||
|
|
||||||
|
register_cuda_ci(est_time=90, suite="stage-b-test-small-1-gpu")
|
||||||
|
register_amd_ci(est_time=90, suite="stage-b-test-small-1-gpu")
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from huggingface_hub import snapshot_download
|
||||||
|
from safetensors.torch import load_file
|
||||||
|
|
||||||
|
import sglang as sgl
|
||||||
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
|
MODEL_PATH = "Qwen/Qwen3-0.6B"
|
||||||
|
LORA_REPO = "charent/self_cognition_Alice"
|
||||||
|
TEST_PROMPT = "Hello, my name is"
|
||||||
|
EXPECTED_OUTPUT = (
|
||||||
|
" Alice, and I am a software engineer. I am excited to share my journey"
|
||||||
|
)
|
||||||
|
MAX_NEW_TOKENS = 16
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoRALoadFromTensor(CustomTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.engine = sgl.Engine(
|
||||||
|
model_path=MODEL_PATH,
|
||||||
|
enable_lora=True,
|
||||||
|
max_lora_rank=64,
|
||||||
|
lora_target_modules=[
|
||||||
|
"q_proj",
|
||||||
|
"k_proj",
|
||||||
|
"v_proj",
|
||||||
|
"o_proj",
|
||||||
|
"gate_proj",
|
||||||
|
"up_proj",
|
||||||
|
"down_proj",
|
||||||
|
],
|
||||||
|
mem_fraction_static=0.6,
|
||||||
|
log_level="error",
|
||||||
|
)
|
||||||
|
|
||||||
|
lora_adapter = snapshot_download(
|
||||||
|
repo_id=LORA_REPO,
|
||||||
|
allow_patterns=["adapter_model.safetensors", "adapter_config.json"],
|
||||||
|
)
|
||||||
|
# Load tensors and config from downloaded adapter
|
||||||
|
cls.lora_tensors = load_file(
|
||||||
|
os.path.join(lora_adapter, "adapter_model.safetensors")
|
||||||
|
)
|
||||||
|
with open(os.path.join(lora_adapter, "adapter_config.json"), "r") as f:
|
||||||
|
cls.lora_config_dict = json.load(f)
|
||||||
|
|
||||||
|
def test_lora_lru_eviction(self):
|
||||||
|
print("[Test]Testing LRU LoRA eviction...")
|
||||||
|
MAX_LOADED_LORAS = 8
|
||||||
|
print(f"[Test]Max loaded LoRAs: {MAX_LOADED_LORAS}")
|
||||||
|
test_engine = sgl.Engine(
|
||||||
|
model_path=MODEL_PATH,
|
||||||
|
enable_lora=True,
|
||||||
|
max_lora_rank=64,
|
||||||
|
lora_target_modules=[
|
||||||
|
"q_proj",
|
||||||
|
"k_proj",
|
||||||
|
"v_proj",
|
||||||
|
"o_proj",
|
||||||
|
"gate_proj",
|
||||||
|
"up_proj",
|
||||||
|
"down_proj",
|
||||||
|
],
|
||||||
|
mem_fraction_static=0.6,
|
||||||
|
log_level="error",
|
||||||
|
max_loaded_loras=MAX_LOADED_LORAS,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load 10 LoRA adapters, max allowed is 8
|
||||||
|
# This should trigger LRU eviction when we exceed the limit
|
||||||
|
TEST_LORA_COUNT = 10
|
||||||
|
for i in range(TEST_LORA_COUNT):
|
||||||
|
print(f"[Test]Loading LoRA adapter {i+1}/10: self_cognition_Alice_{i}")
|
||||||
|
result = test_engine.load_lora_adapter_from_tensors(
|
||||||
|
lora_name=f"self_cognition_Alice_{i}",
|
||||||
|
tensors=self.lora_tensors,
|
||||||
|
config_dict=self.lora_config_dict,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
result.success,
|
||||||
|
f"Failed to load LoRA adapter {i}: {result.error_message}",
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"[Test]Successfully loaded LoRA {i+1}, current loaded adapters: {list(result.loaded_adapters.keys())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
EXPECTED_LORA_ADAPTERS = [
|
||||||
|
"self_cognition_Alice_2",
|
||||||
|
"self_cognition_Alice_3",
|
||||||
|
"self_cognition_Alice_4",
|
||||||
|
"self_cognition_Alice_5",
|
||||||
|
"self_cognition_Alice_6",
|
||||||
|
"self_cognition_Alice_7",
|
||||||
|
"self_cognition_Alice_8",
|
||||||
|
"self_cognition_Alice_9",
|
||||||
|
]
|
||||||
|
EXPECTED_LORA_COUNT = 8
|
||||||
|
self.assertEqual(
|
||||||
|
len(result.loaded_adapters),
|
||||||
|
EXPECTED_LORA_COUNT,
|
||||||
|
f"Loaded adapters count does not match expected result: {len(result.loaded_adapters)} != {EXPECTED_LORA_COUNT}",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(result.loaded_adapters.keys()),
|
||||||
|
EXPECTED_LORA_ADAPTERS,
|
||||||
|
f"Loaded adapters do not match expected result: {list(result.loaded_adapters.keys())} != {EXPECTED_LORA_ADAPTERS}",
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"[Test]LRU eviction test passed! Final loaded adapters: {len(result.loaded_adapters)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_lora_e2e_load_from_tensor_params(self):
|
||||||
|
print("[Test]Testing LoRA load from tensor params...")
|
||||||
|
|
||||||
|
result = self.engine.load_lora_adapter_from_tensors(
|
||||||
|
lora_name="self_cognition_Alice",
|
||||||
|
tensors=self.lora_tensors,
|
||||||
|
config_dict=self.lora_config_dict,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
result.success,
|
||||||
|
f"Failed to load LoRA from tensors: {result.error_message}",
|
||||||
|
)
|
||||||
|
|
||||||
|
output_without_lora = self.engine.generate(
|
||||||
|
prompt=[TEST_PROMPT],
|
||||||
|
sampling_params={
|
||||||
|
"max_new_tokens": MAX_NEW_TOKENS,
|
||||||
|
"temperature": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
output_lora = self.engine.generate(
|
||||||
|
prompt=[TEST_PROMPT],
|
||||||
|
sampling_params={
|
||||||
|
"max_new_tokens": MAX_NEW_TOKENS,
|
||||||
|
"temperature": 0.0,
|
||||||
|
},
|
||||||
|
lora_path=["self_cognition_Alice"],
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[Without LoRA] {output_without_lora[0]}")
|
||||||
|
print(f"[With LoRA] {output_lora[0]}")
|
||||||
|
self.assertNotEqual(
|
||||||
|
output_without_lora[0]["text"][: len(EXPECTED_OUTPUT)],
|
||||||
|
EXPECTED_OUTPUT,
|
||||||
|
"Output before applying LoRA should not match expected result",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
output_lora[0]["text"][: len(EXPECTED_OUTPUT)],
|
||||||
|
EXPECTED_OUTPUT,
|
||||||
|
"Output after applying LoRA does not match expected result",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_lora_load_unload_load_from_tensor_params(self):
|
||||||
|
print("[Test]Testing LoRA load, unload, load from tensor params...")
|
||||||
|
|
||||||
|
# Load LoRA adapter from tensors
|
||||||
|
result = self.engine.load_lora_adapter_from_tensors(
|
||||||
|
lora_name="self_cognition_Alice_multiple",
|
||||||
|
tensors=self.lora_tensors,
|
||||||
|
config_dict=self.lora_config_dict,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
result.success,
|
||||||
|
f"Failed to load LoRA from tensors: {result.error_message}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unload LoRA adapter
|
||||||
|
result = self.engine.unload_lora_adapter("self_cognition_Alice_multiple")
|
||||||
|
self.assertTrue(
|
||||||
|
result.success, f"Failed to unload LoRA: {result.error_message}"
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
output_lora = self.engine.generate(
|
||||||
|
prompt=[TEST_PROMPT],
|
||||||
|
sampling_params={
|
||||||
|
"max_new_tokens": MAX_NEW_TOKENS,
|
||||||
|
"temperature": 0.0,
|
||||||
|
},
|
||||||
|
lora_path=["self_cognition_Alice_multiple"],
|
||||||
|
)
|
||||||
|
# Load LoRA adapter again
|
||||||
|
result_again = self.engine.load_lora_adapter_from_tensors(
|
||||||
|
lora_name="self_cognition_Alice_multiple",
|
||||||
|
tensors=self.lora_tensors,
|
||||||
|
config_dict=self.lora_config_dict,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
result_again.success,
|
||||||
|
f"Failed to load LoRA from tensors: {result_again.error_message}",
|
||||||
|
)
|
||||||
|
output_lora_loaded_again = self.engine.generate(
|
||||||
|
prompt=[TEST_PROMPT],
|
||||||
|
sampling_params={
|
||||||
|
"max_new_tokens": MAX_NEW_TOKENS,
|
||||||
|
"temperature": 0.0,
|
||||||
|
},
|
||||||
|
lora_path=["self_cognition_Alice_multiple"],
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[With LoRA Loaded again] {output_lora_loaded_again[0]}")
|
||||||
|
self.assertEqual(
|
||||||
|
output_lora_loaded_again[0]["text"][: len(EXPECTED_OUTPUT)],
|
||||||
|
EXPECTED_OUTPUT,
|
||||||
|
"Output after applying LoRA does not match expected result",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_lora_logp_diff_with_huggingface(self):
|
||||||
|
"""
|
||||||
|
Test comparing SGLang and HuggingFace LoRA logprobs when loading LoRA from tensors.
|
||||||
|
This verifies that loading LoRA adapters from tensors produces consistent logprobs
|
||||||
|
with HuggingFace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sglang.test.runners import HFRunner, SRTRunner
|
||||||
|
from sglang.test.test_utils import DEFAULT_PORT_FOR_SRT_TEST_RUNNER
|
||||||
|
|
||||||
|
print("[Test]Testing LoRA logprob difference with HuggingFace...")
|
||||||
|
|
||||||
|
lora_name = "self_cognition_Alice_logprob_test"
|
||||||
|
prompts = [TEST_PROMPT]
|
||||||
|
|
||||||
|
# Step 1: Run SGLang with LoRA loaded from tensors
|
||||||
|
print("[Test]Running SGLang with LoRA from tensors...")
|
||||||
|
with SRTRunner(
|
||||||
|
MODEL_PATH,
|
||||||
|
torch_dtype=torch.float16,
|
||||||
|
model_type="generation",
|
||||||
|
tp_size=1,
|
||||||
|
max_loras_per_batch=1,
|
||||||
|
lora_backend="triton",
|
||||||
|
disable_cuda_graph=False,
|
||||||
|
disable_radix_cache=True,
|
||||||
|
port=DEFAULT_PORT_FOR_SRT_TEST_RUNNER,
|
||||||
|
mem_fraction_static=0.6,
|
||||||
|
enable_lora=True,
|
||||||
|
max_lora_rank=64,
|
||||||
|
lora_target_modules=[
|
||||||
|
"q_proj",
|
||||||
|
"k_proj",
|
||||||
|
"v_proj",
|
||||||
|
"o_proj",
|
||||||
|
"gate_proj",
|
||||||
|
"up_proj",
|
||||||
|
"down_proj",
|
||||||
|
],
|
||||||
|
) as srt_runner:
|
||||||
|
result = srt_runner.engine.load_lora_adapter_from_tensors(
|
||||||
|
lora_name=lora_name,
|
||||||
|
tensors=self.lora_tensors,
|
||||||
|
config_dict=self.lora_config_dict,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
result.success,
|
||||||
|
f"Failed to load LoRA from tensors: {result.error_message}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run inference with loaded LoRA
|
||||||
|
srt_outputs = srt_runner.forward(
|
||||||
|
prompts,
|
||||||
|
max_new_tokens=MAX_NEW_TOKENS,
|
||||||
|
lora_paths=[lora_name],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Run HuggingFace with LoRA
|
||||||
|
print("[Test]Running HuggingFace with LoRA...")
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
with HFRunner(
|
||||||
|
MODEL_PATH,
|
||||||
|
torch_dtype=torch.float16,
|
||||||
|
model_type="generation",
|
||||||
|
patch_model_do_sample_false=True,
|
||||||
|
) as hf_runner:
|
||||||
|
hf_outputs = hf_runner.forward(
|
||||||
|
prompts,
|
||||||
|
max_new_tokens=MAX_NEW_TOKENS,
|
||||||
|
lora_paths=[LORA_REPO],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 3: Compare results
|
||||||
|
sglang_text = srt_outputs.output_strs[0]
|
||||||
|
hf_text = hf_outputs.output_strs[0]
|
||||||
|
|
||||||
|
print(f"[Text Output]")
|
||||||
|
print(f" SGLang: {sglang_text}")
|
||||||
|
print(f" HuggingFace: {hf_text}")
|
||||||
|
|
||||||
|
# Compare prefill (input) logprobs
|
||||||
|
sglang_prefill = torch.tensor(srt_outputs.top_input_logprobs[0])
|
||||||
|
hf_prefill = torch.tensor(hf_outputs.top_input_logprobs[0])
|
||||||
|
|
||||||
|
prefill_diff = torch.abs(sglang_prefill - hf_prefill)
|
||||||
|
prefill_max_diff = torch.max(prefill_diff).item()
|
||||||
|
prefill_mean_diff = torch.mean(prefill_diff).item()
|
||||||
|
|
||||||
|
print(f"\n[Prefill Logprob Comparison]")
|
||||||
|
print(f" Shape: {list(sglang_prefill.shape)}")
|
||||||
|
print(f" Max difference: {prefill_max_diff:.6e}")
|
||||||
|
print(f" Mean difference: {prefill_mean_diff:.6e}")
|
||||||
|
|
||||||
|
# Compare decode (output) logprobs
|
||||||
|
sglang_decode = torch.tensor(srt_outputs.top_output_logprobs[0])
|
||||||
|
hf_decode = torch.tensor(hf_outputs.top_output_logprobs[0])
|
||||||
|
|
||||||
|
decode_diff = torch.abs(sglang_decode - hf_decode)
|
||||||
|
decode_max_diff = torch.max(decode_diff).item()
|
||||||
|
decode_mean_diff = torch.mean(decode_diff).item()
|
||||||
|
|
||||||
|
print(f"\n[Decode Logprob Comparison]")
|
||||||
|
print(f" Shape: {list(sglang_decode.shape)}")
|
||||||
|
print(f" Max difference: {decode_max_diff:.6e}")
|
||||||
|
print(f" Mean difference: {decode_mean_diff:.6e}")
|
||||||
|
|
||||||
|
# Assert logprobs are close (threshold 1e-1)
|
||||||
|
LOGPROB_THRESHOLD = 1e-1
|
||||||
|
self.assertLess(
|
||||||
|
prefill_max_diff,
|
||||||
|
LOGPROB_THRESHOLD,
|
||||||
|
f"Prefill logprob max difference too large: {prefill_max_diff:.6e} > {LOGPROB_THRESHOLD:.0e}",
|
||||||
|
)
|
||||||
|
self.assertLess(
|
||||||
|
decode_max_diff,
|
||||||
|
LOGPROB_THRESHOLD,
|
||||||
|
f"Decode logprob max difference too large: {decode_max_diff:.6e} > {LOGPROB_THRESHOLD:.0e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify text outputs match expected
|
||||||
|
self.assertEqual(
|
||||||
|
sglang_text[: len(EXPECTED_OUTPUT)],
|
||||||
|
EXPECTED_OUTPUT,
|
||||||
|
"SGLang output does not match expected result",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n[Test]LoRA logprob comparison test passed!")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls.engine.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user