[Kimi K3] optimize: preprocess cpu-transport images on the vision owner (#33921)

This commit is contained in:
Mick
2026-08-09 16:13:20 +08:00
committed by GitHub
parent d0aa37b49b
commit 22e003580b
6 changed files with 470 additions and 82 deletions
@@ -680,6 +680,138 @@ def test_kimi_k3_epd_rebuild_uses_the_same_media_contract():
)
def test_kimi_k3_cpu_transport_defers_gpu_preprocessing():
from sglang.srt.multimodal.kimi_k3_image_processing import (
DEFERRED_PREPROCESSING_KEY,
)
processor = object.__new__(KimiK3ImageProcessor)
processor.mm_tokens = SimpleNamespace(image_token_id=99)
processor.mm_feature_transport = "cpu"
processor.use_cuda_ipc = False
processor._processor = SimpleNamespace(
_patch_size=2,
prepare_deferred=Mock(
return_value=(
torch.tensor([[1, 99, 99, 2, 99, 3]]),
[
{
"num_tokens": 2,
"new_width": 4,
"new_height": 2,
"pad_width": 0,
"pad_height": 2,
},
{
"num_tokens": 1,
"new_width": 2,
"new_height": 2,
"pad_width": 2,
"pad_height": 2,
},
],
{
"image_mean": [0.5, 0.5, 0.5],
"image_std": [0.5, 0.5, 0.5],
"transparent_bg_config": None,
},
)
),
)
images = [
torch.arange(3 * 2 * 4, dtype=torch.uint8).reshape(3, 2, 4),
torch.arange(3 * 2 * 2, dtype=torch.uint8).reshape(3, 2, 2),
]
base_output = SimpleNamespace(
input_text="prompt", images=images, input_ids=[1, 99, 2, 99, 3]
)
output = processor._build_deferred_output(base_output)
assert output.input_ids == [1, 99, 99, 2, 99, 3]
assert [item.offsets for item in output.mm_items] == [[(1, 2)], [(4, 4)]]
assert [item.feature.dtype for item in output.mm_items] == [
torch.uint8,
torch.uint8,
]
assert [item.feature.shape for item in output.mm_items] == [
torch.Size([3, 2, 4]),
torch.Size([3, 2, 2]),
]
assert [item.image_grid_thw.tolist() for item in output.mm_items] == [
[[1, 2, 2]],
[[1, 2, 2]],
]
assert all(item.hash is not None for item in output.mm_items)
assert all(item.pad_value is not None for item in output.mm_items)
assert all(
DEFERRED_PREPROCESSING_KEY in item.model_specific_data
for item in output.mm_items
)
@pytest.mark.parametrize(
("image_shape", "in_patch_limit", "expected"),
[((3, 32, 32), 65536, True), ((3, 1024, 1024), 1, False)],
)
def test_kimi_k3_defers_only_when_raw_transport_is_smaller(
image_shape, in_patch_limit, expected
):
processor = object.__new__(KimiK3ImageProcessor)
processor.mm_feature_transport = "cpu"
processor._processor = SimpleNamespace(
_patch_size=14,
_merge_kernel_size=2,
_in_patch_limit=in_patch_limit,
_patch_limit_on_one_side=512,
_fixed_output_tokens=None,
)
image = torch.zeros(image_shape, dtype=torch.uint8)
with patch("sglang.srt.multimodal.processors.kimi_k3.is_cuda", return_value=True):
assert processor._should_defer_gpu_preprocessing([image]) is expected
def test_kimi_k3_does_not_defer_non_uint8_tensor_preprocessing():
processor = object.__new__(KimiK3ImageProcessor)
processor.mm_feature_transport = "cpu"
with patch("sglang.srt.multimodal.processors.kimi_k3.is_cuda", return_value=True):
assert not processor._should_defer_gpu_preprocessing(
[torch.zeros((3, 32, 32), dtype=torch.float32)]
)
def test_kimi_k3_does_not_defer_empty_image_batch():
processor = object.__new__(KimiK3ImageProcessor)
processor.mm_feature_transport = "cpu"
with patch("sglang.srt.multimodal.processors.kimi_k3.is_cuda", return_value=True):
assert not processor._should_defer_gpu_preprocessing([])
def test_kimi_k3_eager_preprocessing_preserves_float_tensor_support():
from sglang.srt.multimodal.processors.kimi_k3 import _k3_to_cuda_chw
image = torch.zeros((1, 4, 4), dtype=torch.float32)
with patch.object(torch.Tensor, "cuda", lambda self: self):
output = _k3_to_cuda_chw(image)
assert output.dtype == torch.float32
assert output.shape == (3, 4, 4)
@pytest.mark.parametrize("transport", ["cuda_ipc", "fabric"])
def test_kimi_k3_keeps_gpu_transport_preprocessing_eager(transport):
processor = object.__new__(KimiK3ImageProcessor)
processor.mm_feature_transport = transport
with patch("sglang.srt.multimodal.processors.kimi_k3.is_cuda", return_value=True):
assert not processor._should_defer_gpu_preprocessing(
[torch.zeros((3, 32, 32), dtype=torch.uint8)]
)
def test_kimi_k3_rejects_silently_dropped_images():
processor = object.__new__(KimiK3ImageProcessor)
processor.mm_tokens = Mock()
@@ -699,6 +831,8 @@ def test_kimi_k3_uses_token_ids_to_preserve_media_boundaries():
processor = object.__new__(KimiK3ImageProcessor)
processor.mm_feature_transport = "cpu"
processor.mm_tokens = SimpleNamespace(image_token_id=99)
processor.mm_feature_transport = "cuda_ipc"
processor.use_cuda_ipc = True
processor.fast_load_mm_data = AsyncMock(
return_value=SimpleNamespace(
images=[object(), object()], input_ids=[1, 99, 2, 99, 3]
@@ -167,7 +167,7 @@ def test_kimi_k3_vision_tower_reuses_prepared_forward_metadata(monkeypatch):
)
assert len(actual) == len(reference) == 1
assert torch.equal(actual[0], reference[0])
torch.testing.assert_close(actual[0], reference[0], rtol=0, atol=0, equal_nan=True)
def test_kimi_k3_dp_helper_passes_host_grid_list_to_capable_tower():
@@ -424,6 +424,7 @@ if __name__ == "__main__":
class _K3TowerStub:
device = torch.device("cpu")
merge_kernel_size = (2, 2)
patch_size = 2
def __init__(self):
self.config = SimpleNamespace(hidden_size=2)
@@ -467,14 +468,11 @@ def test_kimi_k3_encoder_dp_defers_feature_materialization(monkeypatch):
"sglang.srt.multimodal.mm_utils.run_dp_sharded_mrope_vision_model",
return_value=sharded_embeddings,
) as run_dp, mock_patch(
"sglang.srt.models.kimi_k3.get_server_args",
return_value=SimpleNamespace(tp_size=1),
), mock_patch(
"sglang.srt.models.kimi_k3.get_parallel",
return_value=SimpleNamespace(attn_tp_size=1),
return_value=SimpleNamespace(tp_size=1, attn_tp_size=1),
):
output = model.get_image_feature(items)
# exercise the loader inside the patch scope: it reads server args
# Exercise the loader while the runtime topology is patched.
loader_in_scope = run_dp.call_args.kwargs["load_local_pixel_values"]
local = loader_in_scope([1])
both = loader_in_scope([0, 1])
@@ -502,6 +500,80 @@ def test_kimi_k3_encoder_dp_defers_feature_materialization(monkeypatch):
)
def test_kimi_k3_preprocesses_only_dp_owner_images(monkeypatch):
from unittest.mock import patch as mock_patch
from sglang.srt.managers.schedule_batch import Modality, MultimodalDataItem
from sglang.srt.models.kimi_k3 import KimiK3ForConditionalGeneration
from sglang.srt.multimodal.kimi_k3_image_processing import (
DEFERRED_PREPROCESSING_KEY,
)
model = KimiK3ForConditionalGeneration.__new__(KimiK3ForConditionalGeneration)
torch.nn.Module.__init__(model)
model.use_data_parallel = True
model.vision_tower = _K3TowerStub()
model.mm_projector = lambda image_embeds: image_embeds
deferred_config = {
"image_mean": [0.5, 0.5, 0.5],
"image_std": [0.5, 0.5, 0.5],
"transparent_bg_config": None,
"resize_config": {
"num_tokens": 1,
"new_width": 2,
"new_height": 2,
"pad_width": 0,
"pad_height": 0,
},
}
items = [
MultimodalDataItem(
modality=Modality.IMAGE,
offsets=[(index, index)],
feature=torch.full((3, 2, 2), index, dtype=torch.uint8),
model_specific_data={
"image_grid_thw": torch.tensor([[1, 1, 1]]),
DEFERRED_PREPROCESSING_KEY: deferred_config,
},
)
for index in range(2)
]
calls = []
def fake_preprocess(images, resize_configs, *args, **kwargs):
calls.append([int(image[0, 0, 0]) for image in images])
return torch.tensor([[float(calls[-1][0]), 0.0]]), torch.tensor([[1, 1, 1]])
with mock_patch(
"sglang.srt.multimodal.mm_utils.run_dp_sharded_mrope_vision_model",
return_value=torch.zeros(1, 2),
) as run_dp, mock_patch(
"sglang.srt.models.kimi_k3.get_parallel",
return_value=SimpleNamespace(tp_size=1, attn_tp_size=1),
), mock_patch(
"sglang.srt.multimodal.processors.kimi_k25._gpu_preprocess_images",
side_effect=fake_preprocess,
):
model.get_image_feature(items)
loader = run_dp.call_args.kwargs["load_local_pixel_values"]
one = loader([1])
assert calls == [[1]]
assert one.dtype == torch.float32
assert one.tolist() == [[1.0, 0.0]]
def test_kimi_k3_scheduler_leaves_feature_placement_to_dp_owner():
from sglang.srt.managers.mm_schedule import _can_skip_pre_embed_feature_move
from sglang.srt.models.kimi_k3 import KimiK3ForConditionalGeneration
model = KimiK3ForConditionalGeneration.__new__(KimiK3ForConditionalGeneration)
torch.nn.Module.__init__(model)
assert _can_skip_pre_embed_feature_move(model.get_image_feature)
def test_kimi_k3_rejects_aggregated_items():
"""One item must carry exactly one logical image: the DP owner
assignment and the bounded CUDA-IPC lease accounting are per-item, so