Update LoRA Weights via Tensor (#16226)

Co-authored-by: PopSoda2002 <zhouhp.me@gmail.com>
This commit is contained in:
lg(x)
2026-01-10 17:36:43 +08:00
committed by GitHub
co-authored by PopSoda2002
parent aeb480c11f
commit 3a8b44fe89
12 changed files with 628 additions and 31 deletions
@@ -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()