diff --git a/python/sglang/srt/entrypoints/http_server.py b/python/sglang/srt/entrypoints/http_server.py index 9e35f2263..9a1aa72c5 100644 --- a/python/sglang/srt/entrypoints/http_server.py +++ b/python/sglang/srt/entrypoints/http_server.py @@ -2023,6 +2023,46 @@ def _admin_api_key_missing_response( # Minimal 32x32 black PNG (base64, GLM4v requires at least 32x32 sized image) MINIMUM_PNG_PICTURE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAbUlEQVRYhe3VsQ2AMAxE0Y/lIgNQULD/OqyCMgCihCKSG4yRuKuiNH6JLsoEbMACOGBcua9HOR7Y6w6swBwMy0qLTpkeI77qdEBpBFAHBBDAGH8WrwJKI4AAegUCfAKgEgpQDvh3CR3oQCuav58qlAw73kKCSgAAAABJRU5ErkJggg==" +# Kimi K2.5/K2.7 runs its MoonViT position interpolation through torch.compile. +# The minimal image above does not exercise a representative image shape and +# leaves the first client request paying the compilation cost. Keep this +# narrowly scoped: a larger default warmup image would unnecessarily lengthen +# startup for VLMs whose encoders do not have this behavior. +KIMI_VLM_WARMUP_PNG_PICTURE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAADEUlEQVR42u3BgQAAAADDoPlTX+EAVQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMBvArQAAf/YBFAAAAAASUVORK5CYII=" + +# Kimi K3's native vision preprocessing uses a 14x14 spatial patch size. A +# 448x448 image exercises a representative 32x32 patch grid without padding. +KIMI_K3_VLM_WARMUP_PNG_PICTURE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAcAAAAHACAIAAAC6Ry8kAAACX0lEQVR42u3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAtwEyRwAB32QPDQAAAABJRU5ErkJggg==" + + +def _get_vlm_warmup_image_base64(model_info: dict) -> str: + """Choose the VLM image used by the startup warmup request. + + A 512x512 image triggers Kimi K2.5/K2.7's representative compiled + position-interpolation path during startup. Kimi K3 uses a 448x448 image + matching its native vision patch grid. This keeps one-time vision setup + work out of the first external image request. + Other VLMs retain the minimal image to avoid changing their startup cost. + """ + + architectures = model_info.get("architectures") or [] + if ( + "KimiK3ForConditionalGeneration" in architectures + or model_info.get("model_type") == "kimi_k3" + ): + logger.info( + "Using a 448x448 image for Kimi K3 VLM startup warmup to exercise " + "its native 32x32 vision patch grid." + ) + return KIMI_K3_VLM_WARMUP_PNG_PICTURE_BASE64 + if "KimiK25ForConditionalGeneration" in architectures: + logger.info( + "Using a 512x512 image for Kimi VLM startup warmup to compile " + "MoonViT position interpolation." + ) + return KIMI_VLM_WARMUP_PNG_PICTURE_BASE64 + return MINIMUM_PNG_PICTURE_BASE64 + async def _send_disaggregation_warmup_requests( server_args: ServerArgs, @@ -2137,7 +2177,8 @@ def _execute_server_warmup(server_args: ServerArgs): { "type": "image_url", "image_url": { - "url": f"data:image/png;base64,{MINIMUM_PNG_PICTURE_BASE64}" + "url": "data:image/png;base64," + f"{_get_vlm_warmup_image_base64(model_info)}" }, }, { diff --git a/test/registered/unit/entrypoints/test_server_warmup.py b/test/registered/unit/entrypoints/test_server_warmup.py new file mode 100644 index 000000000..293564277 --- /dev/null +++ b/test/registered/unit/entrypoints/test_server_warmup.py @@ -0,0 +1,59 @@ +"""Unit tests for model-specific server warmup inputs.""" + +import base64 +import struct +import unittest + +from sglang.srt.entrypoints.http_server import ( + KIMI_K3_VLM_WARMUP_PNG_PICTURE_BASE64, + KIMI_VLM_WARMUP_PNG_PICTURE_BASE64, + MINIMUM_PNG_PICTURE_BASE64, + _get_vlm_warmup_image_base64, +) +from sglang.test.ci.ci_register import register_cpu_ci +from sglang.test.test_utils import CustomTestCase + +register_cpu_ci(est_time=5, suite="base-a-test-cpu") + + +class TestVlmWarmupImage(CustomTestCase): + def test_kimi_k2_uses_representative_vision_image(self): + image_base64 = _get_vlm_warmup_image_base64( + {"architectures": ["KimiK25ForConditionalGeneration"]} + ) + self.assertEqual(image_base64, KIMI_VLM_WARMUP_PNG_PICTURE_BASE64) + + png = base64.b64decode(KIMI_VLM_WARMUP_PNG_PICTURE_BASE64) + self.assertEqual(png[:8], b"\x89PNG\r\n\x1a\n") + self.assertEqual(struct.unpack(">II", png[16:24]), (512, 512)) + + def test_kimi_k3_uses_native_patch_grid_image(self): + for model_info in ( + {"architectures": ["KimiK3ForConditionalGeneration"]}, + {"architectures": None, "model_type": "kimi_k3"}, + ): + with self.subTest(model_info=model_info): + self.assertEqual( + _get_vlm_warmup_image_base64(model_info), + KIMI_K3_VLM_WARMUP_PNG_PICTURE_BASE64, + ) + + png = base64.b64decode(KIMI_K3_VLM_WARMUP_PNG_PICTURE_BASE64) + self.assertEqual(png[:8], b"\x89PNG\r\n\x1a\n") + self.assertEqual(struct.unpack(">II", png[16:24]), (448, 448)) + + def test_other_vlms_keep_minimal_startup_image(self): + self.assertEqual( + _get_vlm_warmup_image_base64( + {"architectures": ["Qwen3VLForConditionalGeneration"]} + ), + MINIMUM_PNG_PICTURE_BASE64, + ) + self.assertEqual( + _get_vlm_warmup_image_base64({"architectures": None}), + MINIMUM_PNG_PICTURE_BASE64, + ) + + +if __name__ == "__main__": + unittest.main()