[Perf] Optimize Qwen3-VL unique-image serving on H100 (#36411)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,11 @@ import torch
|
||||
|
||||
from sglang.srt.managers import mm_schedule
|
||||
from sglang.srt.managers.schedule_batch import Modality, MultimodalDataItem
|
||||
from sglang.srt.multimodal.transport.cuda_ipc import (
|
||||
BORROW_CUDA_IPC_FEATURE_KEY,
|
||||
DEFER_CUDA_IPC_FEATURE_RECONSTRUCTION_KEY,
|
||||
CudaIpcTensorTransportProxy,
|
||||
)
|
||||
from sglang.srt.runtime_context import get_context, get_parallel
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
|
||||
@@ -235,6 +240,30 @@ def test_batched_mismatched_cache_entry_is_reencoded():
|
||||
encoder.assert_called_once()
|
||||
|
||||
|
||||
def test_full_deferred_ipc_item_is_marked_for_borrow():
|
||||
mm_schedule.init_mm_embedding_cache(1 << 30)
|
||||
proxy = object.__new__(CudaIpcTensorTransportProxy)
|
||||
item = MultimodalDataItem(
|
||||
modality=Modality.IMAGE,
|
||||
hash=1000,
|
||||
pad_value=1000,
|
||||
feature=proxy,
|
||||
offsets=[ITEM_OFFSETS[0]],
|
||||
model_specific_data={DEFER_CUDA_IPC_FEATURE_RECONSTRUCTION_KEY: True},
|
||||
)
|
||||
request = mm_schedule.PerImageRequestInfo(
|
||||
req_idx=0,
|
||||
items=[item],
|
||||
items_offset=[ITEM_OFFSETS[0]],
|
||||
extend_prefix_len=0,
|
||||
extend_seq_len=TOTAL_LEN,
|
||||
)
|
||||
|
||||
mm_schedule._batch_encode_per_image_misses(_encoder_list, [request], _CPU)
|
||||
|
||||
assert item.model_specific_data[BORROW_CUDA_IPC_FEATURE_KEY]
|
||||
|
||||
|
||||
def test_batched_colliding_hashes_with_different_lengths_are_not_deduplicated():
|
||||
mm_schedule.init_mm_embedding_cache(1 << 30)
|
||||
items = _make_items()
|
||||
|
||||
@@ -6,7 +6,7 @@ or
|
||||
python -m unittest discover -s tests -p "test_*unit.py" -v
|
||||
"""
|
||||
|
||||
from sglang.test.test_utils import enter_override, maybe_stub_sgl_kernel
|
||||
from sglang.test.test_utils import CustomTestCase, enter_override, maybe_stub_sgl_kernel
|
||||
|
||||
maybe_stub_sgl_kernel() # must precede any import that pulls in sgl_kernel
|
||||
|
||||
@@ -195,6 +195,81 @@ class _MockTemplateManager:
|
||||
self.jinja_template_may_reorder_tool_results = False
|
||||
|
||||
|
||||
class TestChatTemplateCache(CustomTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
reset_context()
|
||||
self.addCleanup(reset_context)
|
||||
publish(
|
||||
ServerArgs(model_path="dummy", default_chat_template_kwargs=None),
|
||||
role="tokenizer",
|
||||
)
|
||||
self.tokenizer_manager = _MockTokenizerManager()
|
||||
self.chat = OpenAIServingChat(
|
||||
self.tokenizer_manager,
|
||||
_MockTemplateManager(),
|
||||
)
|
||||
self.tokenizer_manager.tokenizer.apply_chat_template.return_value = "rendered"
|
||||
self.tokenizer_manager.tokenizer.encode.return_value = [11, 12]
|
||||
self.tokenizer_manager.tokenizer.decode.return_value = "decoded"
|
||||
self.tokenizer_manager.tokenizer.reset_mock()
|
||||
|
||||
def _render(self, **overrides):
|
||||
kwargs = {
|
||||
"messages": [{"role": "user", "content": "same text prefix"}],
|
||||
"tools": None,
|
||||
"template_kwargs": {"enable_thinking": False},
|
||||
"encode_kwargs": {"add_special_tokens": False},
|
||||
"use_cache": True,
|
||||
}
|
||||
kwargs.update(overrides)
|
||||
return self.chat._render_and_encode_chat_template(**kwargs)
|
||||
|
||||
def test_cache_hit_reuses_render_encode_and_returns_an_owned_id_list(self):
|
||||
first = self._render()
|
||||
first[1].append(99)
|
||||
second = self._render()
|
||||
|
||||
self.assertEqual(second, ("rendered", [11, 12], "decoded"))
|
||||
self.tokenizer_manager.tokenizer.apply_chat_template.assert_called_once()
|
||||
self.tokenizer_manager.tokenizer.encode.assert_called_once()
|
||||
self.tokenizer_manager.tokenizer.decode.assert_called_once()
|
||||
|
||||
def test_cache_key_includes_template_and_encode_options(self):
|
||||
self._render()
|
||||
self._render(template_kwargs={"enable_thinking": True})
|
||||
self._render(encode_kwargs={"add_special_tokens": True})
|
||||
|
||||
self.assertEqual(
|
||||
self.tokenizer_manager.tokenizer.apply_chat_template.call_count,
|
||||
3,
|
||||
)
|
||||
self.assertEqual(self.tokenizer_manager.tokenizer.encode.call_count, 3)
|
||||
|
||||
def test_cache_key_tracks_tokenizer_chat_template_updates(self):
|
||||
self.tokenizer_manager.tokenizer.chat_template = "template-v1"
|
||||
self._render()
|
||||
self.tokenizer_manager.tokenizer.chat_template = "template-v2"
|
||||
self._render()
|
||||
|
||||
self.assertEqual(
|
||||
self.tokenizer_manager.tokenizer.apply_chat_template.call_count,
|
||||
2,
|
||||
)
|
||||
|
||||
def test_non_serializable_input_bypasses_cache(self):
|
||||
messages = [{"role": "user", "content": object()}]
|
||||
self._render(messages=messages)
|
||||
self._render(messages=messages)
|
||||
|
||||
self.assertEqual(
|
||||
self.tokenizer_manager.tokenizer.apply_chat_template.call_count,
|
||||
2,
|
||||
)
|
||||
self.assertEqual(self.tokenizer_manager.tokenizer.encode.call_count, 2)
|
||||
self.tokenizer_manager.tokenizer.decode.assert_not_called()
|
||||
|
||||
|
||||
class ServingChatTestCase(unittest.TestCase):
|
||||
# ------------- common fixtures -------------
|
||||
def setUp(self):
|
||||
@@ -386,9 +461,7 @@ class ServingChatTestCase(unittest.TestCase):
|
||||
)
|
||||
self.tm.tokenizer.apply_chat_template.reset_mock()
|
||||
self.chat._apply_jinja_template(ordered_request, None, is_multimodal=True)
|
||||
self.assertEqual(
|
||||
rendered_messages, self.tm.tokenizer.apply_chat_template.call_args[0][0]
|
||||
)
|
||||
self.tm.tokenizer.apply_chat_template.assert_not_called()
|
||||
|
||||
self.template_manager.jinja_template_may_reorder_tool_results = False
|
||||
self.tm.tokenizer.apply_chat_template.reset_mock()
|
||||
|
||||
@@ -50,6 +50,7 @@ from sglang.srt.mem_cache.events import KVCacheEventRecorder
|
||||
from sglang.srt.mem_cache.mamba_radix_cache import TreeNode as MambaTreeNode
|
||||
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey, TreeNode
|
||||
from sglang.srt.utils import get_device
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
# Test constants
|
||||
DEFAULT_PAGE_SIZE = 4
|
||||
@@ -368,7 +369,7 @@ class TestTreeNode(unittest.TestCase):
|
||||
self.assertEqual(n4.get_prefix_hash_values(n3), ["h1", "h2", "h3"])
|
||||
|
||||
|
||||
class TestRadixCache(unittest.TestCase):
|
||||
class TestRadixCache(CustomTestCase):
|
||||
"""Test cases for RadixCache class."""
|
||||
|
||||
def setUp(self):
|
||||
@@ -540,6 +541,52 @@ class TestRadixCache(unittest.TestCase):
|
||||
cache.req_to_token_pool.req_to_token[0], tree_indices
|
||||
)
|
||||
|
||||
def test_finished_request_splits_prompt_from_output_for_eviction(self):
|
||||
class ReqToTokenPool:
|
||||
def __init__(self, row):
|
||||
self.req_to_token = row.unsqueeze(0)
|
||||
|
||||
allocator = TokenToKVPoolAllocator(
|
||||
size=16,
|
||||
dtype=torch.float16,
|
||||
device="cpu",
|
||||
kvcache=None,
|
||||
need_sort=False,
|
||||
)
|
||||
cache = RadixCache.create_simulated(mock_allocator=allocator)
|
||||
prompt_ids = array("q", [1, 2, 3])
|
||||
output_ids = array("q", [4, 5])
|
||||
kv_indices = allocator.alloc(len(prompt_ids) + len(output_ids))
|
||||
self.assertIsNotNone(kv_indices)
|
||||
cache.req_to_token_pool = ReqToTokenPool(kv_indices)
|
||||
req = unittest.mock.Mock(
|
||||
origin_input_ids=prompt_ids,
|
||||
output_ids=output_ids,
|
||||
kv=ReqKvInfo(req_pool_idx=0, cache_protected_len=0),
|
||||
extra_key=None,
|
||||
cache_salt=None,
|
||||
priority=0,
|
||||
last_node=cache.root_node,
|
||||
)
|
||||
|
||||
cache.cache_finished_req(
|
||||
req,
|
||||
is_insert=True,
|
||||
kv_len_to_handle=len(prompt_ids) + len(output_ids),
|
||||
)
|
||||
|
||||
(prompt_node,) = cache.root_node.children.values()
|
||||
(output_node,) = prompt_node.children.values()
|
||||
self.assertEqual(len(prompt_node.key), len(prompt_ids))
|
||||
self.assertEqual(len(output_node.key), len(output_ids))
|
||||
|
||||
result = cache.evict(EvictParams(num_tokens=len(output_ids)))
|
||||
self.assertEqual(result.num_tokens_evicted, len(output_ids))
|
||||
match = cache.match_prefix(
|
||||
MatchPrefixParams(key=RadixKey(prompt_ids + output_ids))
|
||||
)
|
||||
self.assertEqual(len(match.device_indices), len(prompt_ids))
|
||||
|
||||
def test_kv_cache_events(self):
|
||||
"""Test KV cache events functionality."""
|
||||
test_cases = [
|
||||
|
||||
@@ -1558,6 +1558,28 @@ class UnifiedRadixCacheSuite:
|
||||
MatchPrefixParams(key=RadixKey(array("q", all_ids[:aligned_len])))
|
||||
)
|
||||
self.assertEqual(len(m.device_indices), aligned_len)
|
||||
|
||||
prompt_aligned_len = (len(input_ids) // ps) * ps
|
||||
if self.cfg.components == (ComponentType.FULL,):
|
||||
(prompt_node,) = _node_children(cache, cache.root_node_handle())
|
||||
(output_node,) = _node_children(cache, prompt_node)
|
||||
self.assertEqual(_node_key_length(cache, prompt_node), prompt_aligned_len)
|
||||
self.assertEqual(
|
||||
_node_key_length(cache, output_node),
|
||||
aligned_len - prompt_aligned_len,
|
||||
)
|
||||
|
||||
result = cache.evict(
|
||||
EvictParams(num_tokens=aligned_len - prompt_aligned_len)
|
||||
)
|
||||
self.assertEqual(
|
||||
result.num_tokens_evicted,
|
||||
aligned_len - prompt_aligned_len,
|
||||
)
|
||||
prompt_only = cache.match_prefix(
|
||||
MatchPrefixParams(key=RadixKey(array("q", all_ids[:aligned_len])))
|
||||
)
|
||||
self.assertEqual(len(prompt_only.device_indices), prompt_aligned_len)
|
||||
cache.sanity_check()
|
||||
|
||||
def test_cache_finished_req_strips_thinking(self):
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
"""Regression tests for Qwen3-VL multimodal feature materialization."""
|
||||
|
||||
import unittest
|
||||
from contextlib import nullcontext
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.managers.schedule_batch import Modality, MultimodalDataItem
|
||||
from sglang.srt.managers.schedule_batch import (
|
||||
Modality,
|
||||
MultimodalDataItem,
|
||||
MultimodalInputs,
|
||||
)
|
||||
from sglang.srt.models.qwen3_vl import Qwen3VLForConditionalGeneration
|
||||
from sglang.srt.multimodal.processors.qwen_vl import QwenVLImageProcessor
|
||||
from sglang.srt.multimodal.transport.cuda_ipc import (
|
||||
BORROW_CUDA_IPC_FEATURE_KEY,
|
||||
CUDA_IPC_FEATURE_COPY_EVENT_KEY,
|
||||
DEFER_CUDA_IPC_FEATURE_RECONSTRUCTION_KEY,
|
||||
CudaIpcTensorTransportProxy,
|
||||
)
|
||||
from sglang.srt.runtime_context import get_context
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
@@ -69,7 +77,7 @@ class TestQwen3VLFeatureMaterialization(CustomTestCase):
|
||||
MultimodalDataItem(modality=Modality.AUDIO),
|
||||
]
|
||||
|
||||
processor._mark_dp_encoder_features_for_deferred_reconstruction(items)
|
||||
processor._mark_cuda_ipc_features_for_deferred_reconstruction(items)
|
||||
|
||||
self.assertTrue(
|
||||
items[0].model_specific_data[
|
||||
@@ -87,19 +95,104 @@ class TestQwen3VLFeatureMaterialization(CustomTestCase):
|
||||
)
|
||||
|
||||
def test_processor_does_not_defer_cpu_transport(self):
|
||||
processor = QwenVLImageProcessor.__new__(QwenVLImageProcessor)
|
||||
processor.mm_feature_transport = "cpu"
|
||||
processor.server_args = SimpleNamespace(mm_enable_dp_encoder=True)
|
||||
processor.model_type = "qwen3_vl"
|
||||
item = MultimodalDataItem(modality=Modality.IMAGE)
|
||||
with get_context().override_server_args(mm_enable_dp_encoder=True):
|
||||
processor = QwenVLImageProcessor.__new__(QwenVLImageProcessor)
|
||||
processor.mm_feature_transport = "cpu"
|
||||
processor.model_type = "qwen3_vl"
|
||||
item = MultimodalDataItem(modality=Modality.IMAGE)
|
||||
|
||||
processor._mark_dp_encoder_features_for_deferred_reconstruction([item])
|
||||
processor._mark_cuda_ipc_features_for_deferred_reconstruction([item])
|
||||
|
||||
self.assertNotIn(
|
||||
DEFER_CUDA_IPC_FEATURE_RECONSTRUCTION_KEY,
|
||||
item.model_specific_data,
|
||||
)
|
||||
|
||||
def test_processor_defers_cuda_ipc_for_single_tp_qwen3_vl(self):
|
||||
processor = QwenVLImageProcessor.__new__(QwenVLImageProcessor)
|
||||
processor.mm_feature_transport = "cuda_ipc"
|
||||
processor.model_type = "qwen3_vl"
|
||||
item = MultimodalDataItem(modality=Modality.IMAGE)
|
||||
|
||||
processor._mark_cuda_ipc_features_for_deferred_reconstruction([item])
|
||||
|
||||
self.assertTrue(
|
||||
item.model_specific_data[DEFER_CUDA_IPC_FEATURE_RECONSTRUCTION_KEY]
|
||||
)
|
||||
|
||||
def test_retract_reprefill_waits_for_preserved_visual_input(self):
|
||||
visual = Mock()
|
||||
visual.device = torch.device("cuda:0")
|
||||
visual.dtype = torch.bfloat16
|
||||
visual.side_effect = lambda pixel_values, *, grid_thw: pixel_values
|
||||
model = self._model(visual, use_data_parallel=False)
|
||||
|
||||
proxy = CudaIpcTensorTransportProxy.__new__(CudaIpcTensorTransportProxy)
|
||||
proxy.total_consumer_count = 1
|
||||
borrowed_feature = torch.ones(2, 3)
|
||||
packed_ready = Mock()
|
||||
host_ready = Mock()
|
||||
current_stream = Mock()
|
||||
copy_stream = Mock()
|
||||
proxy.reconstruct_on_target_device = Mock()
|
||||
proxy.borrow_on_target_device = Mock(return_value=borrowed_feature)
|
||||
proxy.release_borrowed_on_current_stream = Mock()
|
||||
proxy.release_without_reconstruction = Mock()
|
||||
item = MultimodalDataItem(
|
||||
modality=Modality.IMAGE,
|
||||
feature=proxy,
|
||||
model_specific_data={BORROW_CUDA_IPC_FEATURE_KEY: True},
|
||||
)
|
||||
item.image_grid_thw = torch.tensor([[1, 1, 2]])
|
||||
|
||||
with (
|
||||
patch(
|
||||
"sglang.srt.models.qwen3_vl.get_parallel",
|
||||
return_value=SimpleNamespace(tp_size=1),
|
||||
),
|
||||
patch(
|
||||
"sglang.srt.models.qwen3_vl.materialize_multimodal_features",
|
||||
side_effect=lambda features, **_kwargs: torch.cat(features),
|
||||
),
|
||||
patch(
|
||||
"sglang.srt.models.qwen3_vl.torch.cuda.current_stream",
|
||||
return_value=current_stream,
|
||||
),
|
||||
patch(
|
||||
"sglang.srt.models.qwen3_vl.torch.cuda.Event",
|
||||
side_effect=(packed_ready, host_ready),
|
||||
),
|
||||
patch(
|
||||
"sglang.srt.models.qwen3_vl.torch.cuda.Stream",
|
||||
return_value=copy_stream,
|
||||
),
|
||||
patch(
|
||||
"sglang.srt.models.qwen3_vl.torch.cuda.stream",
|
||||
return_value=nullcontext(),
|
||||
),
|
||||
):
|
||||
first = model.get_image_feature([item])
|
||||
second = model.get_image_feature([item])
|
||||
|
||||
self.assertIsNot(item.feature, borrowed_feature)
|
||||
self.assertTrue(torch.equal(item.feature, borrowed_feature))
|
||||
self.assertTrue(torch.equal(first, second))
|
||||
proxy.reconstruct_on_target_device.assert_not_called()
|
||||
proxy.borrow_on_target_device.assert_called_once_with(0)
|
||||
proxy.release_borrowed_on_current_stream.assert_called_once_with()
|
||||
proxy.release_without_reconstruction.assert_not_called()
|
||||
packed_ready.record.assert_called_once_with(current_stream)
|
||||
copy_stream.wait_event.assert_called_once_with(packed_ready)
|
||||
host_ready.record.assert_called_once_with(copy_stream)
|
||||
current_stream.wait_event.assert_called_once_with(host_ready)
|
||||
self.assertEqual(visual.call_count, 2)
|
||||
|
||||
MultimodalInputs(mm_items=[item]).release_features()
|
||||
|
||||
proxy.release_without_reconstruction.assert_not_called()
|
||||
self.assertIsNone(item.feature)
|
||||
self.assertNotIn(CUDA_IPC_FEATURE_COPY_EVENT_KEY, item.model_specific_data)
|
||||
|
||||
def test_image_features_are_packed_on_the_visual_device(self):
|
||||
visual = _RecordingVisual()
|
||||
model = self._model(visual, use_data_parallel=False)
|
||||
|
||||
@@ -71,6 +71,7 @@ def make_processor(case, config, image_processor_cls=None):
|
||||
model_impl="sglang",
|
||||
keep_mm_feature_on_device=False,
|
||||
mm_feature_transport="cpu",
|
||||
mm_enable_dp_encoder=False,
|
||||
image_processor_backend="auto",
|
||||
disable_fast_image_processor=True,
|
||||
skip_tokenizer_init=False,
|
||||
|
||||
@@ -128,6 +128,61 @@ class TestCudaIpcTransport(CustomTestCase):
|
||||
producer.join(timeout=10)
|
||||
self.assertEqual(producer.exitcode, 0)
|
||||
|
||||
def test_borrowed_tensor_keeps_lease_until_explicit_release(self):
|
||||
ctx = mp.get_context("spawn")
|
||||
proxy_queue = ctx.Queue()
|
||||
producer_results = ctx.Queue()
|
||||
consumer_done = ctx.Event()
|
||||
producer = ctx.Process(
|
||||
target=_produce_pooled_tensor,
|
||||
args=(proxy_queue, consumer_done, producer_results),
|
||||
)
|
||||
producer.start()
|
||||
proxy = borrowed = consumed = None
|
||||
producer_result = None
|
||||
try:
|
||||
try:
|
||||
proxy, expected = proxy_queue.get(timeout=60)
|
||||
except queue.Empty:
|
||||
producer_result = producer_results.get(timeout=5)
|
||||
_status, payload = producer_result
|
||||
self.fail(
|
||||
f"CUDA IPC producer failed before sending its proxy: {payload}"
|
||||
)
|
||||
|
||||
borrowed = proxy.borrow_on_target_device(0)
|
||||
self.assertIsNotNone(borrowed)
|
||||
consumed = borrowed + 1
|
||||
torch.cuda.synchronize()
|
||||
|
||||
self.assertFalse(proxy._consumer_acknowledged)
|
||||
proxy.release_without_reconstruction()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
self.assertEqual(
|
||||
consumed.cpu().tolist(),
|
||||
(torch.tensor(expected) + 1).tolist(),
|
||||
)
|
||||
self.assertTrue(proxy._consumer_acknowledged)
|
||||
self.assertIsNone(proxy._borrowed_storage)
|
||||
finally:
|
||||
del consumed, borrowed, proxy
|
||||
_pool_handle_cache_clear()
|
||||
gc.collect()
|
||||
torch.cuda.ipc_collect()
|
||||
consumer_done.set()
|
||||
producer.join(timeout=60)
|
||||
try:
|
||||
if producer_result is None:
|
||||
producer_result = producer_results.get(timeout=5)
|
||||
status, payload = producer_result
|
||||
self.assertEqual(status, "ok", payload)
|
||||
finally:
|
||||
if producer.is_alive():
|
||||
producer.terminate()
|
||||
producer.join(timeout=10)
|
||||
self.assertEqual(producer.exitcode, 0)
|
||||
|
||||
def test_failed_reconstruction_releases_pooled_tensor(self):
|
||||
ctx = mp.get_context("spawn")
|
||||
proxy_queue = ctx.Queue()
|
||||
|
||||
@@ -15,6 +15,7 @@ from sglang.srt.multimodal.media_artifacts import (
|
||||
MediaArtifactInput,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cpu_ci(est_time=11, suite="base-a-test-cpu")
|
||||
|
||||
@@ -88,7 +89,7 @@ class _Processor(MediaArtifactCacheMixin):
|
||||
self.io_executor.shutdown()
|
||||
|
||||
|
||||
class TestMediaArtifactProcessor(unittest.TestCase):
|
||||
class TestMediaArtifactProcessor(CustomTestCase):
|
||||
def test_default_image_decoder_rejects_lazy_pil_failure(self):
|
||||
malformed_png = base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFgAI/ScLJSwAAAABJRU5ErkJggg=="
|
||||
@@ -253,6 +254,39 @@ class TestMediaArtifactProcessor(unittest.TestCase):
|
||||
self.assertEqual(artifacts[0].feature, b"fresh")
|
||||
self.assertEqual(len(processor.batches), 1)
|
||||
|
||||
def test_without_cache_builds_request_local_artifacts_and_does_not_retain(self):
|
||||
processor = _Processor()
|
||||
digest = snapshot_media(b"image").content_digest
|
||||
key = processor._artifact_key(digest, b"image")
|
||||
cached = _Artifact(digest, key, 1, b"cached")
|
||||
processor.mm_preprocess_cache.put(key, cached)
|
||||
|
||||
try:
|
||||
artifacts = asyncio.run(
|
||||
processor.prepare_media_artifacts_without_cache([b"image", b"image"])
|
||||
)
|
||||
cached_after = asyncio.run(processor.prepare_media_artifacts([b"image"]))
|
||||
finally:
|
||||
processor.close()
|
||||
|
||||
self.assertEqual([artifact.feature for artifact in artifacts], [b"image"] * 2)
|
||||
self.assertEqual(len(processor.batches), 1)
|
||||
self.assertEqual(len(processor.batches[0]), 2)
|
||||
self.assertIs(cached_after[0], cached)
|
||||
|
||||
def test_without_cache_still_validates_caller_content_hash(self):
|
||||
processor = _Processor()
|
||||
try:
|
||||
with self.assertRaisesRegex(ValueError, "content hash mismatch"):
|
||||
asyncio.run(
|
||||
processor.prepare_media_artifacts_without_cache(
|
||||
[b"image"],
|
||||
content_hashes=[snapshot_media(b"different").content_digest],
|
||||
)
|
||||
)
|
||||
finally:
|
||||
processor.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -111,6 +111,25 @@ _mock_device.start()
|
||||
|
||||
|
||||
class TestPrepareServerArgs(CustomTestCase):
|
||||
def test_radix_eviction_policy_explicitness_is_preserved(self):
|
||||
omitted = prepare_server_args(["--model-path", "dummy"])
|
||||
separated = prepare_server_args(
|
||||
["--model-path", "dummy", "--radix-eviction-policy", "lru"]
|
||||
)
|
||||
joined = prepare_server_args(
|
||||
["--model-path", "dummy", "--radix-eviction-policy=lru"]
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||
f.write("model-path: dummy\nradix-eviction-policy: lru\n")
|
||||
config_path = f.name
|
||||
self.addCleanup(os.unlink, config_path)
|
||||
configured = prepare_server_args(["--config", config_path])
|
||||
|
||||
self.assertFalse(omitted._radix_eviction_policy_explicitly_set)
|
||||
self.assertTrue(separated._radix_eviction_policy_explicitly_set)
|
||||
self.assertTrue(joined._radix_eviction_policy_explicitly_set)
|
||||
self.assertTrue(configured._radix_eviction_policy_explicitly_set)
|
||||
|
||||
def test_ple_embedding_offload_rejects_generic_weight_offload(self):
|
||||
for generic_offload in (
|
||||
{"cpu_offload_gb": 1},
|
||||
|
||||
@@ -20,6 +20,7 @@ from sglang.srt.arg_groups import overrides as overrides_module
|
||||
from sglang.srt.arg_groups.arg_utils import A, Arg, resolvable_fields
|
||||
from sglang.srt.arg_groups.model_overrides import minicpm as minicpm_module
|
||||
from sglang.srt.arg_groups.model_overrides import qwen3_5 as qwen3_5_module
|
||||
from sglang.srt.arg_groups.model_overrides import qwen3_vl as qwen3_vl_module
|
||||
from sglang.srt.arg_groups.overrides import (
|
||||
collect_model_override_declarations,
|
||||
register_model_override,
|
||||
@@ -108,6 +109,10 @@ class TestModelOverridableWhitelist(CustomTestCase):
|
||||
"enable_symm_mem",
|
||||
"speculative_attention_mode",
|
||||
"speculative_draft_attention_backend",
|
||||
"prefill_decode_interval",
|
||||
"radix_eviction_policy",
|
||||
"mm_preprocess_cache_size_mb",
|
||||
"mm_feature_transport",
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -3107,5 +3112,107 @@ class TestDeclarationValidation(CustomTestCase):
|
||||
validate_declarations(args, [("src", {"nope": 1})])
|
||||
|
||||
|
||||
class TestQwen3VLHopperServingOverrides(CustomTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.addCleanup(envs.SGLANG_VLM_CACHE_SIZE_MB.clear)
|
||||
self.addCleanup(envs.SGLANG_MM_FEATURE_CACHE_MB.clear)
|
||||
envs.SGLANG_VLM_CACHE_SIZE_MB.clear()
|
||||
envs.SGLANG_MM_FEATURE_CACHE_MB.clear()
|
||||
|
||||
@staticmethod
|
||||
def _args(**overrides):
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
values = {
|
||||
"mm_preprocess_cache_size_mb": None,
|
||||
"mm_feature_transport": None,
|
||||
"max_running_requests": 400,
|
||||
"radix_eviction_policy": "lru",
|
||||
"prefill_decode_interval": None,
|
||||
"attention_backend": None,
|
||||
"decode_attention_backend": None,
|
||||
}
|
||||
values.update(overrides)
|
||||
return ServerArgs(model_path="dummy", **values)
|
||||
|
||||
@patch.object(
|
||||
qwen3_vl_module,
|
||||
"large_hopper_qwen3_vl_model_type",
|
||||
return_value="qwen3_vl",
|
||||
)
|
||||
def test_profiled_defaults_are_valid_model_overrides(self, _mock_model_type):
|
||||
server_args = self._args()
|
||||
updates = qwen3_vl_module._qwen3vl_hopper_serving_overrides(server_args, None)
|
||||
|
||||
self.assertEqual(
|
||||
updates,
|
||||
{
|
||||
"mm_preprocess_cache_size_mb": 0,
|
||||
"mm_feature_transport": "cuda_ipc",
|
||||
"radix_eviction_policy": "priority",
|
||||
"prefill_decode_interval": 22,
|
||||
"decode_attention_backend": "flashinfer",
|
||||
},
|
||||
)
|
||||
validate_declarations(
|
||||
server_args,
|
||||
[("_qwen3vl_hopper_serving_overrides", updates)],
|
||||
)
|
||||
self.assertEqual(envs.SGLANG_VLM_CACHE_SIZE_MB.get(), 0)
|
||||
self.assertEqual(envs.SGLANG_MM_FEATURE_CACHE_MB.get(), 3 * 1024)
|
||||
|
||||
@patch.object(
|
||||
qwen3_vl_module,
|
||||
"large_hopper_qwen3_vl_model_type",
|
||||
return_value="qwen3_vl",
|
||||
)
|
||||
def test_multinode_does_not_auto_select_cuda_ipc(self, _mock_model_type):
|
||||
updates = qwen3_vl_module._qwen3vl_hopper_serving_overrides(
|
||||
self._args(nnodes=2), None
|
||||
)
|
||||
|
||||
self.assertNotIn("mm_feature_transport", updates)
|
||||
self.assertFalse(envs.SGLANG_MM_FEATURE_CACHE_MB.is_set())
|
||||
|
||||
@patch.object(
|
||||
qwen3_vl_module,
|
||||
"large_hopper_qwen3_vl_model_type",
|
||||
side_effect=AssertionError("must not load model config without GPU memory"),
|
||||
)
|
||||
def test_decode_graph_expansion_skips_unknown_gpu_memory(self, _mock_model_type):
|
||||
decode_config = SimpleNamespace(max_bs=256)
|
||||
|
||||
qwen3_vl_module.expand_multimodal_decode_graph_to_running_limit(
|
||||
self._args(), decode_config, gpu_mem=None
|
||||
)
|
||||
|
||||
self.assertEqual(decode_config.max_bs, 256)
|
||||
|
||||
@patch.object(
|
||||
qwen3_vl_module,
|
||||
"large_hopper_qwen3_vl_model_type",
|
||||
return_value="qwen3_vl",
|
||||
)
|
||||
def test_explicit_choices_are_not_replaced(self, _mock_model_type):
|
||||
envs.SGLANG_VLM_CACHE_SIZE_MB.set(512)
|
||||
envs.SGLANG_MM_FEATURE_CACHE_MB.set(2048)
|
||||
updates = qwen3_vl_module._qwen3vl_hopper_serving_overrides(
|
||||
self._args(
|
||||
mm_preprocess_cache_size_mb=256,
|
||||
mm_feature_transport="cpu",
|
||||
radix_eviction_policy="lru",
|
||||
_radix_eviction_policy_explicitly_set=True,
|
||||
prefill_decode_interval=0,
|
||||
decode_attention_backend="fa3",
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
self.assertEqual(updates, {})
|
||||
self.assertEqual(envs.SGLANG_VLM_CACHE_SIZE_MB.get(), 512)
|
||||
self.assertEqual(envs.SGLANG_MM_FEATURE_CACHE_MB.get(), 2048)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user