[diffusion] CI: Add LoRA support to diffusion server configuration and test cases (#14697)
This commit is contained in:
@@ -261,7 +261,6 @@ class LoRAPipeline(ComposedPipelineBase):
|
||||
else:
|
||||
continue
|
||||
|
||||
print(f"{name} -> {target_name}")
|
||||
if target_name in self.lora_adapters[lora_nickname]:
|
||||
raise ValueError(
|
||||
f"Dit target weight name {target_name} already exists in lora_adapters[{lora_nickname}]"
|
||||
|
||||
@@ -1013,6 +1013,199 @@
|
||||
"expected_e2e_ms": 814884.71,
|
||||
"expected_avg_denoise_ms": 16062.92,
|
||||
"expected_median_denoise_ms": 16039.62
|
||||
},
|
||||
"wan2_2_t2v_a14b_lora_2gpu": {
|
||||
"stages_ms": {
|
||||
"InputValidationStage": 0.06,
|
||||
"TextEncodingStage": 2582.35,
|
||||
"ConditioningStage": 0.02,
|
||||
"TimestepPreparationStage": 2.11,
|
||||
"LatentPreparationStage": 1.45,
|
||||
"DenoisingStage": 80688.12,
|
||||
"DecodingStage": 1346.2,
|
||||
"per_frame_generation": null
|
||||
},
|
||||
"denoise_step_ms": {
|
||||
"0": 19129.79,
|
||||
"1": 770.1,
|
||||
"2": 895.12,
|
||||
"3": 756.23,
|
||||
"4": 761.9,
|
||||
"5": 758.9,
|
||||
"6": 760.59,
|
||||
"7": 756.3,
|
||||
"8": 761.7,
|
||||
"9": 754.23,
|
||||
"10": 756.11,
|
||||
"11": 755.59,
|
||||
"12": 756.8,
|
||||
"13": 763.12,
|
||||
"14": 757.43,
|
||||
"15": 760.82,
|
||||
"16": 758.13,
|
||||
"17": 759.67,
|
||||
"18": 756.83,
|
||||
"19": 757.86,
|
||||
"20": 757.75,
|
||||
"21": 757.41,
|
||||
"22": 755.53,
|
||||
"23": 759.37,
|
||||
"24": 758.09,
|
||||
"25": 756.58,
|
||||
"26": 32148.89,
|
||||
"27": 760.46,
|
||||
"28": 756.69,
|
||||
"29": 756.47,
|
||||
"30": 759.02,
|
||||
"31": 757.31,
|
||||
"32": 754.43,
|
||||
"33": 759.34,
|
||||
"34": 760.11,
|
||||
"35": 758.23,
|
||||
"36": 763.78,
|
||||
"37": 758.4,
|
||||
"38": 758.26,
|
||||
"39": 758.46
|
||||
},
|
||||
"expected_e2e_ms": 84633.71,
|
||||
"expected_avg_denoise_ms": 2006.04,
|
||||
"expected_median_denoise_ms": 758.24
|
||||
},
|
||||
"wan2_1_t2v_1_3b_lora_1gpu": {
|
||||
"stages_ms": {
|
||||
"InputValidationStage": 0.08,
|
||||
"TextEncodingStage": 2392.95,
|
||||
"ConditioningStage": 0.02,
|
||||
"TimestepPreparationStage": 2.19,
|
||||
"LatentPreparationStage": 1.28,
|
||||
"DenoisingStage": 8752.65,
|
||||
"DecodingStage": 743.42,
|
||||
"per_frame_generation": null
|
||||
},
|
||||
"denoise_step_ms": {
|
||||
"0": 1782.77,
|
||||
"1": 149.53,
|
||||
"2": 147.3,
|
||||
"3": 143.89,
|
||||
"4": 143.48,
|
||||
"5": 142.72,
|
||||
"6": 141.11,
|
||||
"7": 142.73,
|
||||
"8": 138.91,
|
||||
"9": 143.41,
|
||||
"10": 142.53,
|
||||
"11": 139.12,
|
||||
"12": 142.67,
|
||||
"13": 143.35,
|
||||
"14": 142.36,
|
||||
"15": 139.34,
|
||||
"16": 142.94,
|
||||
"17": 141.88,
|
||||
"18": 138.48,
|
||||
"19": 148.75,
|
||||
"20": 138.57,
|
||||
"21": 138.3,
|
||||
"22": 138.4,
|
||||
"23": 137.78,
|
||||
"24": 138.1,
|
||||
"25": 138.35,
|
||||
"26": 138.75,
|
||||
"27": 138.31,
|
||||
"28": 138.48,
|
||||
"29": 137.58,
|
||||
"30": 137.96,
|
||||
"31": 145.2,
|
||||
"32": 145.89,
|
||||
"33": 143.23,
|
||||
"34": 144.19,
|
||||
"35": 142.49,
|
||||
"36": 141.82,
|
||||
"37": 142.4,
|
||||
"38": 144.86,
|
||||
"39": 144.42,
|
||||
"40": 142.97,
|
||||
"41": 142.26,
|
||||
"42": 142.75,
|
||||
"43": 142.45,
|
||||
"44": 142.62,
|
||||
"45": 145.97,
|
||||
"46": 147.18,
|
||||
"47": 143.28,
|
||||
"48": 142.66,
|
||||
"49": 142.32
|
||||
},
|
||||
"expected_e2e_ms": 11905.69,
|
||||
"expected_avg_denoise_ms": 174.94,
|
||||
"expected_median_denoise_ms": 142.57
|
||||
},
|
||||
"wan2_1_i2v_14b_lora_2gpu": {
|
||||
"stages_ms": {
|
||||
"InputValidationStage": 37.9,
|
||||
"TextEncodingStage": 2581.79,
|
||||
"ImageEncodingStage": 1607.56,
|
||||
"ConditioningStage": 0.01,
|
||||
"TimestepPreparationStage": 2.13,
|
||||
"LatentPreparationStage": 0.13,
|
||||
"ImageVAEEncodingStage": 2405.17,
|
||||
"DenoisingStage": 188146.58,
|
||||
"DecodingStage": 3392.12,
|
||||
"per_frame_generation": null
|
||||
},
|
||||
"denoise_step_ms": {
|
||||
"0": 5373.79,
|
||||
"1": 3671.13,
|
||||
"2": 3676.21,
|
||||
"3": 3678.12,
|
||||
"4": 3687.34,
|
||||
"5": 3693.02,
|
||||
"6": 3699.22,
|
||||
"7": 3701.94,
|
||||
"8": 3709.51,
|
||||
"9": 3711.35,
|
||||
"10": 3713.63,
|
||||
"11": 3714.26,
|
||||
"12": 3732.48,
|
||||
"13": 3734.85,
|
||||
"14": 3718.94,
|
||||
"15": 3719.6,
|
||||
"16": 3724.61,
|
||||
"17": 3725.0,
|
||||
"18": 3727.77,
|
||||
"19": 3727.89,
|
||||
"20": 3726.51,
|
||||
"21": 3727.26,
|
||||
"22": 3726.27,
|
||||
"23": 3726.55,
|
||||
"24": 3725.91,
|
||||
"25": 3726.53,
|
||||
"26": 3725.54,
|
||||
"27": 3728.39,
|
||||
"28": 3724.19,
|
||||
"29": 3727.76,
|
||||
"30": 3720.69,
|
||||
"31": 3724.38,
|
||||
"32": 3723.14,
|
||||
"33": 3723.73,
|
||||
"34": 3728.07,
|
||||
"35": 3728.38,
|
||||
"36": 3745.83,
|
||||
"37": 3733.19,
|
||||
"38": 3724.01,
|
||||
"39": 3722.51,
|
||||
"40": 3733.28,
|
||||
"41": 3723.43,
|
||||
"42": 3724.11,
|
||||
"43": 3725.56,
|
||||
"44": 3720.57,
|
||||
"45": 3719.53,
|
||||
"46": 3713.17,
|
||||
"47": 3721.05,
|
||||
"48": 3721.72,
|
||||
"49": 3715.61
|
||||
},
|
||||
"expected_e2e_ms": 198187.89,
|
||||
"expected_avg_denoise_ms": 3751.95,
|
||||
"expected_median_denoise_ms": 3724.06
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any, Callable
|
||||
|
||||
import openai
|
||||
import pytest
|
||||
import requests
|
||||
from openai import OpenAI
|
||||
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
@@ -54,6 +55,9 @@ def diffusion_server(case: DiffusionTestCase) -> ServerContext:
|
||||
extra_args += (
|
||||
f" --num-gpus {server_args.num_gpus} --ulysses-degree {server_args.num_gpus}"
|
||||
)
|
||||
# LoRA support
|
||||
if server_args.lora_path:
|
||||
extra_args += f" --lora-path {server_args.lora_path}"
|
||||
|
||||
# start server
|
||||
manager = ServerManager(
|
||||
@@ -358,6 +362,112 @@ Consider updating perf_baselines.json with the snippets below:
|
||||
"""
|
||||
logger.error(output)
|
||||
|
||||
def _test_lora_api_functionality(
|
||||
self,
|
||||
ctx: ServerContext,
|
||||
case: DiffusionTestCase,
|
||||
generate_fn: Callable[[str, openai.Client], str],
|
||||
) -> None:
|
||||
"""
|
||||
Test LoRA API functionality with end-to-end validation: merge, unmerge, and set_lora.
|
||||
This test verifies that each API call succeeds AND that generation works after each operation.
|
||||
"""
|
||||
base_url = f"http://localhost:{ctx.port}/v1"
|
||||
client = OpenAI(base_url=base_url, api_key="dummy")
|
||||
|
||||
# Test 1: unmerge_lora_weights - API should succeed and generation should work
|
||||
logger.info("[LoRA E2E] Testing unmerge_lora_weights for %s", case.id)
|
||||
resp = requests.post(f"{base_url}/unmerge_lora_weights")
|
||||
assert resp.status_code == 200, f"unmerge_lora_weights failed: {resp.text}"
|
||||
|
||||
logger.info("[LoRA E2E] Verifying generation after unmerge for %s", case.id)
|
||||
output_after_unmerge = generate_fn(case.id, client)
|
||||
assert output_after_unmerge is not None, "Generation after unmerge failed"
|
||||
logger.info("[LoRA E2E] Generation after unmerge succeeded")
|
||||
|
||||
# Test 2: merge_lora_weights - API should succeed and generation should work
|
||||
logger.info("[LoRA E2E] Testing merge_lora_weights for %s", case.id)
|
||||
resp = requests.post(f"{base_url}/merge_lora_weights")
|
||||
assert resp.status_code == 200, f"merge_lora_weights failed: {resp.text}"
|
||||
|
||||
logger.info("[LoRA E2E] Verifying generation after re-merge for %s", case.id)
|
||||
output_after_merge = generate_fn(case.id, client)
|
||||
assert output_after_merge is not None, "Generation after merge failed"
|
||||
logger.info("[LoRA E2E] Generation after merge succeeded")
|
||||
|
||||
# Test 3: set_lora (re-set the same adapter) - API should succeed and generation should work
|
||||
logger.info("[LoRA E2E] Testing set_lora for %s", case.id)
|
||||
resp = requests.post(f"{base_url}/set_lora", json={"lora_nickname": "default"})
|
||||
assert resp.status_code == 200, f"set_lora failed: {resp.text}"
|
||||
|
||||
logger.info("[LoRA E2E] Verifying generation after set_lora for %s", case.id)
|
||||
output_after_set = generate_fn(case.id, client)
|
||||
assert output_after_set is not None, "Generation after set_lora failed"
|
||||
logger.info("[LoRA E2E] Generation after set_lora succeeded")
|
||||
|
||||
logger.info("[LoRA E2E] All LoRA API E2E tests passed for %s", case.id)
|
||||
|
||||
def _test_lora_dynamic_switch_e2e(
|
||||
self,
|
||||
ctx: ServerContext,
|
||||
case: DiffusionTestCase,
|
||||
generate_fn: Callable[[str, openai.Client], str],
|
||||
second_lora_path: str,
|
||||
) -> None:
|
||||
"""
|
||||
Test dynamic LoRA switching with end-to-end validation.
|
||||
This test verifies that switching between LoRA adapters works correctly
|
||||
and generation succeeds after each switch.
|
||||
"""
|
||||
base_url = f"http://localhost:{ctx.port}/v1"
|
||||
client = OpenAI(base_url=base_url, api_key="dummy")
|
||||
|
||||
# Test 1: Generate with initial LoRA
|
||||
logger.info(
|
||||
"[LoRA Switch E2E] Testing generation with initial LoRA for %s", case.id
|
||||
)
|
||||
output_initial = generate_fn(case.id, client)
|
||||
assert output_initial is not None, "Generation with initial LoRA failed"
|
||||
logger.info("[LoRA Switch E2E] Generation with initial LoRA succeeded")
|
||||
|
||||
# Test 2: Switch to second LoRA and generate
|
||||
logger.info(
|
||||
"[LoRA Switch E2E] Switching to second LoRA adapter for %s", case.id
|
||||
)
|
||||
resp = requests.post(
|
||||
f"{base_url}/set_lora",
|
||||
json={"lora_nickname": "lora2", "lora_path": second_lora_path},
|
||||
)
|
||||
assert (
|
||||
resp.status_code == 200
|
||||
), f"set_lora to second adapter failed: {resp.text}"
|
||||
|
||||
logger.info(
|
||||
"[LoRA Switch E2E] Verifying generation with second LoRA for %s", case.id
|
||||
)
|
||||
output_second = generate_fn(case.id, client)
|
||||
assert output_second is not None, "Generation with second LoRA failed"
|
||||
logger.info("[LoRA Switch E2E] Generation with second LoRA succeeded")
|
||||
|
||||
# Test 3: Switch back to original LoRA and generate
|
||||
logger.info("[LoRA Switch E2E] Switching back to original LoRA for %s", case.id)
|
||||
resp = requests.post(f"{base_url}/set_lora", json={"lora_nickname": "default"})
|
||||
assert resp.status_code == 200, f"set_lora back to default failed: {resp.text}"
|
||||
|
||||
logger.info(
|
||||
"[LoRA Switch E2E] Verifying generation after switching back for %s",
|
||||
case.id,
|
||||
)
|
||||
output_switched_back = generate_fn(case.id, client)
|
||||
assert (
|
||||
output_switched_back is not None
|
||||
), "Generation after switching back failed"
|
||||
logger.info("[LoRA Switch E2E] Generation after switching back succeeded")
|
||||
|
||||
logger.info(
|
||||
"[LoRA Switch E2E] All dynamic switch E2E tests passed for %s", case.id
|
||||
)
|
||||
|
||||
def test_diffusion_perf(
|
||||
self,
|
||||
case: DiffusionTestCase,
|
||||
@@ -382,3 +492,7 @@ Consider updating perf_baselines.json with the snippets below:
|
||||
generate_fn,
|
||||
)
|
||||
self._validate_and_record(case, perf_record)
|
||||
|
||||
# LoRA API functionality test with E2E validation (only for LoRA-enabled cases)
|
||||
if case.server_args.lora_path:
|
||||
self._test_lora_api_functionality(diffusion_server, case, generate_fn)
|
||||
|
||||
@@ -119,6 +119,8 @@ class DiffusionServerArgs:
|
||||
custom_validator: str | None = None # optional custom validator name
|
||||
# resources
|
||||
num_gpus: int = 1
|
||||
# LoRA
|
||||
lora_path: str | None = None # LoRA adapter path (HF repo or local path)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -300,6 +302,24 @@ ONE_GPU_CASES_B: list[DiffusionTestCase] = [
|
||||
output_size="848x480",
|
||||
),
|
||||
),
|
||||
# LoRA test case for single transformer + merge/unmerge API test
|
||||
DiffusionTestCase(
|
||||
"wan2_1_t2v_1_3b_lora_1gpu",
|
||||
DiffusionServerArgs(
|
||||
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
modality="video",
|
||||
warmup_text=0,
|
||||
warmup_edit=0,
|
||||
custom_validator="video",
|
||||
num_gpus=1,
|
||||
lora_path="Cseti/Wan-LoRA-Arcane-Jinx-v1",
|
||||
),
|
||||
DiffusionSamplingParams(
|
||||
prompt="csetiarcane Nfj1nx with blue hair, a woman walking in a cyberpunk city at night",
|
||||
output_size="480x320",
|
||||
num_frames=8,
|
||||
),
|
||||
),
|
||||
# NOTE(mick): flaky
|
||||
# DiffusionTestCase(
|
||||
# id="hunyuan_video",
|
||||
@@ -402,6 +422,23 @@ TWO_GPU_CASES_A = [
|
||||
output_size="720x480",
|
||||
),
|
||||
),
|
||||
# LoRA test case for transformer_2 support
|
||||
DiffusionTestCase(
|
||||
"wan2_2_t2v_a14b_lora_2gpu",
|
||||
DiffusionServerArgs(
|
||||
model_path="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
modality="video",
|
||||
warmup_text=0,
|
||||
warmup_edit=0,
|
||||
custom_validator="video",
|
||||
num_gpus=2,
|
||||
lora_path="Cseti/wan2.2-14B-Arcane_Jinx-lora-v1",
|
||||
),
|
||||
DiffusionSamplingParams(
|
||||
prompt="Nfj1nx with blue hair, a woman walking in a cyberpunk city at night",
|
||||
output_size="720x480",
|
||||
),
|
||||
),
|
||||
DiffusionTestCase(
|
||||
"wan2_1_t2v_14b_2gpu",
|
||||
DiffusionServerArgs(
|
||||
@@ -436,6 +473,24 @@ TWO_GPU_CASES_B = [
|
||||
image_path="https://github.com/lm-sys/lm-sys.github.io/releases/download/test/TI2I_Qwen_Image_Edit_Input.jpg",
|
||||
),
|
||||
),
|
||||
# I2V LoRA test case
|
||||
DiffusionTestCase(
|
||||
"wan2_1_i2v_14b_lora_2gpu",
|
||||
DiffusionServerArgs(
|
||||
model_path="Wan-AI/Wan2.1-I2V-14B-720P-Diffusers",
|
||||
modality="video",
|
||||
warmup_text=0,
|
||||
warmup_edit=0,
|
||||
custom_validator="video",
|
||||
num_gpus=2,
|
||||
lora_path="starsfriday/Wan2.1-Divine-Power-LoRA",
|
||||
),
|
||||
DiffusionSamplingParams(
|
||||
prompt="faxiang, the person raises hands, a giant translucent golden figure appears behind",
|
||||
image_path="https://github.com/lm-sys/lm-sys.github.io/releases/download/test/TI2I_Qwen_Image_Edit_Input.jpg",
|
||||
output_size="832x1104",
|
||||
),
|
||||
),
|
||||
DiffusionTestCase(
|
||||
"wan2_1_i2v_14b_720P_2gpu",
|
||||
DiffusionServerArgs(
|
||||
|
||||
Reference in New Issue
Block a user