[Feature] Optimizations for JPEG input on NVIDIA GPU (#19749)
This commit is contained in:
@@ -435,8 +435,13 @@ class MMEncoder:
|
|||||||
return data
|
return data
|
||||||
try:
|
try:
|
||||||
if modality == Modality.IMAGE:
|
if modality == Modality.IMAGE:
|
||||||
img, _ = load_image(data)
|
img, _ = load_image(data, False)
|
||||||
if discard_alpha_channel and img.mode != "RGB":
|
if (
|
||||||
|
discard_alpha_channel
|
||||||
|
and not isinstance(img, torch.Tensor)
|
||||||
|
and img.mode != "RGB"
|
||||||
|
):
|
||||||
|
# Needed only when `img` is a PIL image
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
return img
|
return img
|
||||||
elif modality == Modality.VIDEO:
|
elif modality == Modality.VIDEO:
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ class MultimodalSpecialTokens:
|
|||||||
|
|
||||||
class BaseMultimodalProcessor(ABC):
|
class BaseMultimodalProcessor(ABC):
|
||||||
models = []
|
models = []
|
||||||
|
gpu_image_decode = True # Enable GPU decoding by default
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hf_config, server_args, _processor, transport_mode, *args, **kwargs
|
self, hf_config, server_args, _processor, transport_mode, *args, **kwargs
|
||||||
@@ -468,8 +469,9 @@ class BaseMultimodalProcessor(ABC):
|
|||||||
|
|
||||||
return estimated_frames_list
|
return estimated_frames_list
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _load_single_item(
|
def _load_single_item(
|
||||||
|
cls,
|
||||||
data,
|
data,
|
||||||
modality: Modality,
|
modality: Modality,
|
||||||
frame_count_limit=None,
|
frame_count_limit=None,
|
||||||
@@ -481,7 +483,8 @@ class BaseMultimodalProcessor(ABC):
|
|||||||
|
|
||||||
If data is processor_output or precomputed embedding, return directly.
|
If data is processor_output or precomputed embedding, return directly.
|
||||||
|
|
||||||
Static method that can be pickled for multiprocessing"""
|
Class method that can be pickled for multiprocessing
|
||||||
|
"""
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
data_format = data.get("format")
|
data_format = data.get("format")
|
||||||
if data_format in (
|
if data_format in (
|
||||||
@@ -493,8 +496,13 @@ class BaseMultimodalProcessor(ABC):
|
|||||||
return data
|
return data
|
||||||
try:
|
try:
|
||||||
if modality == Modality.IMAGE:
|
if modality == Modality.IMAGE:
|
||||||
img, _ = load_image(data)
|
img, _ = load_image(data, cls.gpu_image_decode)
|
||||||
if discard_alpha_channel and img.mode != "RGB":
|
if (
|
||||||
|
discard_alpha_channel
|
||||||
|
and not isinstance(img, torch.Tensor)
|
||||||
|
and img.mode != "RGB"
|
||||||
|
):
|
||||||
|
# Needed only when `img` is a PIL image
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
return img
|
return img
|
||||||
elif modality == Modality.VIDEO:
|
elif modality == Modality.VIDEO:
|
||||||
@@ -535,7 +543,7 @@ class BaseMultimodalProcessor(ABC):
|
|||||||
type(data),
|
type(data),
|
||||||
)
|
)
|
||||||
future = self.io_executor.submit(
|
future = self.io_executor.submit(
|
||||||
BaseMultimodalProcessor._load_single_item,
|
self.__class__._load_single_item,
|
||||||
data,
|
data,
|
||||||
modality,
|
modality,
|
||||||
None, # frame_count_limit: no consider for fast path
|
None, # frame_count_limit: no consider for fast path
|
||||||
@@ -595,7 +603,7 @@ class BaseMultimodalProcessor(ABC):
|
|||||||
|
|
||||||
futures.append(
|
futures.append(
|
||||||
self.io_executor.submit(
|
self.io_executor.submit(
|
||||||
BaseMultimodalProcessor._load_single_item,
|
self.__class__._load_single_item,
|
||||||
data,
|
data,
|
||||||
modality,
|
modality,
|
||||||
frame_count_limit,
|
frame_count_limit,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class InternVLProcessor(BaseMultimodalProcessor):
|
class InternVLProcessor(BaseMultimodalProcessor):
|
||||||
models = [InternVLChatModel, InternS1ForConditionalGeneration]
|
models = [InternVLChatModel, InternS1ForConditionalGeneration]
|
||||||
|
gpu_image_decode = False # InternVL HF processor does not support tensor inputs
|
||||||
|
|
||||||
IMAGENET_MEAN = [0.485, 0.456, 0.406]
|
IMAGENET_MEAN = [0.485, 0.456, 0.406]
|
||||||
IMAGENET_STD = [0.229, 0.224, 0.225]
|
IMAGENET_STD = [0.229, 0.224, 0.225]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from sglang.srt.multimodal.processors.base_processor import (
|
|||||||
# Compatible with KimiVLForConditionalGeneration
|
# Compatible with KimiVLForConditionalGeneration
|
||||||
class KimiK2_5VLImageProcessor(SGLangBaseProcessor):
|
class KimiK2_5VLImageProcessor(SGLangBaseProcessor):
|
||||||
models = [KimiK25ForConditionalGeneration]
|
models = [KimiK25ForConditionalGeneration]
|
||||||
|
gpu_image_decode = False # KimiK2.5VL HF processor does not support tensor inputs
|
||||||
|
|
||||||
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
||||||
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from sglang.srt.multimodal.processors.base_processor import (
|
|||||||
# Compatible with KimiVLForConditionalGeneration
|
# Compatible with KimiVLForConditionalGeneration
|
||||||
class KimiVLImageProcessor(SGLangBaseProcessor):
|
class KimiVLImageProcessor(SGLangBaseProcessor):
|
||||||
models = [KimiVLForConditionalGeneration]
|
models = [KimiVLForConditionalGeneration]
|
||||||
|
gpu_image_decode = False # KimiVL HF processor does not support tensor inputs
|
||||||
|
|
||||||
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
||||||
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class LlavaImageProcessor(BaseMultimodalProcessor):
|
|||||||
LlavaQwenForCausalLM,
|
LlavaQwenForCausalLM,
|
||||||
LlavaMistralForCausalLM,
|
LlavaMistralForCausalLM,
|
||||||
]
|
]
|
||||||
|
gpu_image_decode = False # Llava processes loaded image as PIL image explicitly
|
||||||
|
|
||||||
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
||||||
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
||||||
@@ -49,7 +50,7 @@ class LlavaImageProcessor(BaseMultimodalProcessor):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
url = image_data.url if isinstance(image_data, ImageData) else image_data
|
url = image_data.url if isinstance(image_data, ImageData) else image_data
|
||||||
image, image_size = load_image(url)
|
image, image_size = load_image(url, False)
|
||||||
if image_size is not None:
|
if image_size is not None:
|
||||||
# It is a video with multiple images
|
# It is a video with multiple images
|
||||||
image_hash = hash(url)
|
image_hash = hash(url)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from sglang.srt.multimodal.processors.base_processor import (
|
|||||||
class MiniCPMMultimodalProcessor(BaseMultimodalProcessor):
|
class MiniCPMMultimodalProcessor(BaseMultimodalProcessor):
|
||||||
models = [MiniCPMV, MiniCPMO]
|
models = [MiniCPMV, MiniCPMO]
|
||||||
support_dynamic_frame_expansion = True
|
support_dynamic_frame_expansion = True
|
||||||
|
gpu_image_decode = False # MiniCPM HF processor does not support tensor inputs
|
||||||
|
|
||||||
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
||||||
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ MAX_FRAMES = 128
|
|||||||
|
|
||||||
class NanoNemotronVLImageProcessor(BaseMultimodalProcessor):
|
class NanoNemotronVLImageProcessor(BaseMultimodalProcessor):
|
||||||
models = [NemotronH_Nano_VL_V2]
|
models = [NemotronH_Nano_VL_V2]
|
||||||
|
gpu_image_decode = (
|
||||||
|
False # NanoNemotronVL processes loaded image as PIL image explicitly
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, hf_config, server_args, _image_processor, *args, **kwargs):
|
def __init__(self, hf_config, server_args, _image_processor, *args, **kwargs):
|
||||||
super().__init__(hf_config, server_args, _image_processor, *args, **kwargs)
|
super().__init__(hf_config, server_args, _image_processor, *args, **kwargs)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from sglang.srt.multimodal.processors.base_processor import (
|
|||||||
|
|
||||||
class PixtralProcessor(BaseMultimodalProcessor):
|
class PixtralProcessor(BaseMultimodalProcessor):
|
||||||
models = [PixtralVisionModel, PixtralForConditionalGeneration]
|
models = [PixtralVisionModel, PixtralForConditionalGeneration]
|
||||||
|
gpu_image_decode = False # Pixtral processes loaded image as PIL image explicitly
|
||||||
|
|
||||||
PAD_TOKEN = "<pad>"
|
PAD_TOKEN = "<pad>"
|
||||||
DEFAULT_IMAGE_TOKEN = "[IMG]"
|
DEFAULT_IMAGE_TOKEN = "[IMG]"
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ from starlette.routing import Mount
|
|||||||
from torch import nn
|
from torch import nn
|
||||||
from torch.library import Library
|
from torch.library import Library
|
||||||
from torch.utils._contextlib import _DecoratorContextManager
|
from torch.utils._contextlib import _DecoratorContextManager
|
||||||
|
from torchvision.io import decode_jpeg
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
from sglang.srt.environ import envs
|
from sglang.srt.environ import envs
|
||||||
@@ -764,64 +765,109 @@ class ImageData:
|
|||||||
max_dynamic_patch: Optional[int] = None
|
max_dynamic_patch: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
image_extension_names = (".png", ".jpg", ".jpeg", ".webp", ".gif")
|
||||||
|
|
||||||
|
|
||||||
|
def is_jpeg_with_cuda(image_bytes: bytes = b"", gpu_image_decode: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
Check three conditions:
|
||||||
|
1. whether CUDA is available.
|
||||||
|
2. whether input is recognized as JPEG.
|
||||||
|
3. whether GPU image decode is enabled (some models such as CPM forcibly disable this).
|
||||||
|
"""
|
||||||
|
if not is_cuda() or not gpu_image_decode:
|
||||||
|
return False
|
||||||
|
if image_bytes != b"":
|
||||||
|
return image_bytes.startswith(b"\xff\xd8") and image_bytes.endswith(b"\xff\xd9")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _load_image(
|
||||||
|
image_bytes: bytes = b"",
|
||||||
|
image_file: str = "",
|
||||||
|
gpu_image_decode: bool = True,
|
||||||
|
) -> Union[torch.Tensor, Image.Image]:
|
||||||
|
"""
|
||||||
|
Try to decode JPEG with nvJPEG on GPU and return a torch device tensor,
|
||||||
|
otherwise fallback to decode with PIL on CPU and return a PIL Image.
|
||||||
|
Keep the fallback path since nvJPEG may fail on some JPEG images that are not strictly compliant with the standard, while PIL is more tolerant.
|
||||||
|
"""
|
||||||
|
if image_file != "":
|
||||||
|
image_bytes = get_image_bytes(image_file)
|
||||||
|
if is_jpeg_with_cuda(image_bytes, gpu_image_decode):
|
||||||
|
try:
|
||||||
|
encoded_image = torch.frombuffer(image_bytes, dtype=torch.uint8)
|
||||||
|
image_tensor = decode_jpeg(encoded_image, device="cuda")
|
||||||
|
return image_tensor
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to decode JPEG on GPU, falling back to CPU. Error: {e}"
|
||||||
|
)
|
||||||
|
return Image.open(BytesIO(image_bytes))
|
||||||
|
|
||||||
|
|
||||||
def load_image(
|
def load_image(
|
||||||
image_file: Union[Image.Image, str, ImageData, bytes],
|
image_file: Union[Image.Image, str, ImageData, bytes],
|
||||||
) -> tuple[Image.Image, tuple[int, int]]:
|
gpu_image_decode: bool = True,
|
||||||
|
) -> tuple[Union[torch.Tensor, Image.Image], Optional[tuple[int, int]]]:
|
||||||
|
"""
|
||||||
|
Load image from multiple input formats, including:
|
||||||
|
ImageData, PIL Image, bytes, URL, file path, or base64 string.
|
||||||
|
"""
|
||||||
if isinstance(image_file, ImageData):
|
if isinstance(image_file, ImageData):
|
||||||
image_file = image_file.url
|
image_file = image_file.url
|
||||||
|
|
||||||
image = image_size = None
|
image = None
|
||||||
|
image_size: Optional[tuple[int, int]] = None
|
||||||
if isinstance(image_file, Image.Image):
|
if isinstance(image_file, Image.Image):
|
||||||
image = image_file
|
image = image_file
|
||||||
image_size = (image.width, image.height)
|
image_size = (image.width, image.height)
|
||||||
elif isinstance(image_file, bytes):
|
elif isinstance(image_file, bytes):
|
||||||
image = Image.open(BytesIO(image_file))
|
image = _load_image(image_bytes=image_file, gpu_image_decode=gpu_image_decode)
|
||||||
elif image_file.startswith("http://") or image_file.startswith("https://"):
|
elif isinstance(image_file, str) and image_file.startswith(("http://", "https://")):
|
||||||
timeout = int(os.getenv("REQUEST_TIMEOUT", "3"))
|
image = _load_image(image_file=image_file, gpu_image_decode=gpu_image_decode)
|
||||||
response = requests.get(image_file, stream=True, timeout=timeout)
|
elif isinstance(image_file, str) and image_file.startswith("file://"):
|
||||||
try:
|
image = _load_image(
|
||||||
response.raise_for_status()
|
image_file=unquote(urlparse(image_file).path),
|
||||||
image = Image.open(response.raw)
|
gpu_image_decode=gpu_image_decode,
|
||||||
image.load() # Force loading to avoid issues after closing the stream
|
)
|
||||||
finally:
|
elif isinstance(image_file, str) and image_file.lower().endswith(
|
||||||
response.close()
|
image_extension_names
|
||||||
elif image_file.startswith("file://"):
|
):
|
||||||
image_file = unquote(urlparse(image_file).path)
|
image = _load_image(image_file=image_file, gpu_image_decode=gpu_image_decode)
|
||||||
image = Image.open(image_file)
|
elif isinstance(image_file, str) and image_file.startswith("data:"):
|
||||||
elif image_file.lower().endswith(("png", "jpg", "jpeg", "webp", "gif")):
|
image = _load_image(image_file=image_file, gpu_image_decode=gpu_image_decode)
|
||||||
image = Image.open(image_file)
|
elif isinstance(
|
||||||
elif image_file.startswith("data:"):
|
image_file, str
|
||||||
image_file = image_file.split(",")[1]
|
): # Other formats, try to decode as base64 by default
|
||||||
image = Image.open(BytesIO(pybase64.b64decode(image_file, validate=True)))
|
image = _load_image(image_file=image_file, gpu_image_decode=gpu_image_decode)
|
||||||
elif isinstance(image_file, str):
|
|
||||||
image = Image.open(BytesIO(pybase64.b64decode(image_file, validate=True)))
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid image: {image_file}")
|
raise ValueError(f"Invalid image: {image_file}")
|
||||||
|
|
||||||
return image, image_size
|
return image, image_size
|
||||||
|
|
||||||
|
|
||||||
def get_image_bytes(image_file: Union[str, bytes]):
|
def get_image_bytes(image_file: Union[str, bytes]) -> bytes:
|
||||||
|
"""Normalize various image inputs into raw bytes."""
|
||||||
if isinstance(image_file, bytes):
|
if isinstance(image_file, bytes):
|
||||||
return image_file
|
return image_file
|
||||||
elif image_file.startswith("http://") or image_file.startswith("https://"):
|
if image_file.startswith(("http://", "https://")):
|
||||||
timeout = int(os.getenv("REQUEST_TIMEOUT", "3"))
|
timeout = int(os.getenv("REQUEST_TIMEOUT", "3"))
|
||||||
response = requests.get(image_file, timeout=timeout)
|
response = requests.get(image_file, timeout=timeout)
|
||||||
return response.content
|
try:
|
||||||
elif image_file.startswith("file://"):
|
response.raise_for_status()
|
||||||
image_file = unquote(urlparse(image_file).path)
|
result = response.content
|
||||||
|
finally:
|
||||||
|
response.close()
|
||||||
|
return result
|
||||||
|
if image_file.startswith(("file://", "/")):
|
||||||
with open(image_file, "rb") as f:
|
with open(image_file, "rb") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
elif image_file.lower().endswith(("png", "jpg", "jpeg", "webp", "gif")):
|
if isinstance(image_file, str) and image_file.startswith("data:"):
|
||||||
with open(image_file, "rb") as f:
|
_, encoded = image_file.split(",", 1)
|
||||||
return f.read()
|
return pybase64.b64decode(encoded, validate=True)
|
||||||
elif image_file.startswith("data:"):
|
if isinstance(image_file, str):
|
||||||
image_file = image_file.split(",")[1]
|
|
||||||
return pybase64.b64decode(image_file, validate=True)
|
return pybase64.b64decode(image_file, validate=True)
|
||||||
elif isinstance(image_file, str):
|
raise NotImplementedError(f"Invalid image: {image_file}")
|
||||||
return pybase64.b64decode(image_file, validate=True)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(f"Invalid image: {image_file}")
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_video_input(
|
def _normalize_video_input(
|
||||||
|
|||||||
Reference in New Issue
Block a user