94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
662 lines
24 KiB
Python
662 lines
24 KiB
Python
# Copyright 2023-2024 SGLang Team
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ==============================================================================
|
|
|
|
# Source: https://github.com/LLaVA-VL/LLaVA-NeXT/blob/main/llava/mm_utils.py
|
|
"""
|
|
Utilities for multi-modal models.
|
|
|
|
This python file mainly contains utilities that were used in the
|
|
image processing logic of llava-next including operations such as
|
|
anyres and anyres_max
|
|
|
|
Currently supports the anyres and anyres_max operation for CLIP and
|
|
SigLip. For more information, you may refer to the paper or the blog
|
|
|
|
LLaVA-NeXT : https://llava-vl.github.io/blog/2024-01-30-llava-next/
|
|
LLaVA-Onevision : https://arxiv.org/pdf/2408.03326
|
|
|
|
"""
|
|
|
|
import ast
|
|
import itertools
|
|
import math
|
|
import re
|
|
from io import BytesIO
|
|
from typing import Literal
|
|
|
|
import numpy as np
|
|
import pybase64
|
|
import torch
|
|
from PIL import Image
|
|
|
|
from sglang.srt.distributed.communication_op import tensor_model_parallel_all_gather
|
|
from sglang.srt.runtime_context import get_parallel
|
|
from sglang.srt.utils import flatten_nested_list
|
|
|
|
|
|
def ensure_numpy(x):
|
|
"""Convert torch.Tensor to numpy array if needed (v5 compat)."""
|
|
return x.numpy() if isinstance(x, torch.Tensor) else x
|
|
|
|
|
|
def has_valid_data(data) -> bool:
|
|
if data is None:
|
|
return False
|
|
if isinstance(data, list):
|
|
return any(has_valid_data(item) for item in flatten_nested_list(data))
|
|
return True
|
|
|
|
|
|
def select_best_resolution(original_size, possible_resolutions):
|
|
"""
|
|
Selects the best resolution from a list of possible resolutions based on the original size.
|
|
|
|
Args:
|
|
original_size (tuple): The original size of the image in the format (width, height).
|
|
possible_resolutions (list): A list of possible resolutions in the format [(width1, height1), (width2, height2), ...].
|
|
|
|
Returns:
|
|
tuple: The best fit resolution in the format (width, height).
|
|
"""
|
|
original_width, original_height = original_size
|
|
best_fit = None
|
|
max_effective_resolution = 0
|
|
min_wasted_resolution = float("inf")
|
|
|
|
for width, height in possible_resolutions:
|
|
# Calculate the downscaled size to keep the aspect ratio
|
|
scale = min(width / original_width, height / original_height)
|
|
downscaled_width, downscaled_height = int(original_width * scale), int(
|
|
original_height * scale
|
|
)
|
|
|
|
# Calculate effective and wasted resolutions
|
|
effective_resolution = min(
|
|
downscaled_width * downscaled_height, original_width * original_height
|
|
)
|
|
wasted_resolution = (width * height) - effective_resolution
|
|
|
|
if effective_resolution > max_effective_resolution or (
|
|
effective_resolution == max_effective_resolution
|
|
and wasted_resolution < min_wasted_resolution
|
|
):
|
|
max_effective_resolution = effective_resolution
|
|
min_wasted_resolution = wasted_resolution
|
|
best_fit = (width, height)
|
|
|
|
return best_fit
|
|
|
|
|
|
def resize_and_pad_image(image, target_resolution):
|
|
"""
|
|
Resize and pad an image to a target resolution while maintaining aspect ratio.
|
|
|
|
Args:
|
|
image (PIL.Image.Image): The input image.
|
|
target_resolution (tuple): The target resolution (width, height) of the image.
|
|
|
|
Returns:
|
|
PIL.Image.Image: The resized and padded image.
|
|
"""
|
|
original_width, original_height = image.size
|
|
target_width, target_height = target_resolution
|
|
|
|
scale_w = target_width / original_width
|
|
scale_h = target_height / original_height
|
|
|
|
if scale_w < scale_h:
|
|
new_width = target_width
|
|
new_height = min(math.ceil(original_height * scale_w), target_height)
|
|
else:
|
|
new_height = target_height
|
|
new_width = min(math.ceil(original_width * scale_h), target_width)
|
|
|
|
# Resize the image
|
|
resized_image = image.resize((new_width, new_height))
|
|
|
|
new_image = Image.new("RGB", (target_width, target_height), (0, 0, 0))
|
|
paste_x = (target_width - new_width) // 2
|
|
paste_y = (target_height - new_height) // 2
|
|
new_image.paste(resized_image, (paste_x, paste_y))
|
|
|
|
return new_image
|
|
|
|
|
|
def divide_to_patches(image, patch_size):
|
|
"""
|
|
Divides an image into patches of a specified size.
|
|
|
|
Args:
|
|
image (PIL.Image.Image): The input image.
|
|
patch_size (int): The size of each patch.
|
|
|
|
Returns:
|
|
list: A list of PIL.Image.Image objects representing the patches.
|
|
"""
|
|
patches = []
|
|
width, height = image.size
|
|
for i in range(0, height, patch_size):
|
|
for j in range(0, width, patch_size):
|
|
box = (j, i, j + patch_size, i + patch_size)
|
|
patch = image.crop(box)
|
|
patches.append(patch)
|
|
|
|
return patches
|
|
|
|
|
|
def get_anyres_image_grid_shape(image_size, grid_pinpoints, patch_size):
|
|
"""
|
|
Calculate the shape of the image patch grid after the preprocessing for images of any resolution.
|
|
|
|
Args:
|
|
image_size (tuple): The size of the input image in the format (width, height).
|
|
grid_pinpoints (str): A string representation of a list of possible resolutions.
|
|
patch_size (int): The size of each image patch.
|
|
|
|
Returns:
|
|
tuple: The shape of the image patch grid in the format (width, height).
|
|
"""
|
|
if isinstance(grid_pinpoints, str) and "x" in grid_pinpoints:
|
|
assert patch_size in [
|
|
224,
|
|
336,
|
|
384,
|
|
448,
|
|
512,
|
|
], "patch_size should be in [224, 336, 384, 448, 512]"
|
|
# Use regex to extract the range from the input string
|
|
matches = re.findall(r"\((\d+)x(\d+)\)", grid_pinpoints)
|
|
range_start = tuple(map(int, matches[0]))
|
|
range_end = tuple(map(int, matches[-1]))
|
|
# Generate a matrix of tuples from (range_start[0], range_start[1]) to (range_end[0], range_end[1])
|
|
grid_pinpoints = [
|
|
(i, j)
|
|
for i in range(range_start[0], range_end[0] + 1)
|
|
for j in range(range_start[1], range_end[1] + 1)
|
|
]
|
|
# Multiply all elements by patch_size
|
|
grid_pinpoints = [[dim * patch_size for dim in pair] for pair in grid_pinpoints]
|
|
if type(grid_pinpoints) is list:
|
|
possible_resolutions = grid_pinpoints
|
|
else:
|
|
possible_resolutions = ast.literal_eval(grid_pinpoints)
|
|
width, height = select_best_resolution(image_size, possible_resolutions)
|
|
return width // patch_size, height // patch_size
|
|
|
|
|
|
def process_anyres_image(image, processor, grid_pinpoints):
|
|
"""
|
|
Process an image with variable resolutions.
|
|
|
|
Args:
|
|
image (PIL.Image.Image): The input image to be processed.
|
|
processor: The image processor object.
|
|
grid_pinpoints (str): A string representation of a list of possible resolutions.
|
|
|
|
Returns:
|
|
np.array: An np array containing the processed image patches.
|
|
"""
|
|
if isinstance(grid_pinpoints, str) and "x" in grid_pinpoints:
|
|
try:
|
|
patch_size = processor.size[0]
|
|
except Exception:
|
|
patch_size = processor.size["shortest_edge"]
|
|
assert patch_size in [
|
|
224,
|
|
336,
|
|
384,
|
|
448,
|
|
512,
|
|
], "patch_size should be in [224, 336, 384, 448, 512]"
|
|
# Use regex to extract the range from the input string
|
|
matches = re.findall(r"\((\d+)x(\d+)\)", grid_pinpoints)
|
|
range_start = tuple(map(int, matches[0]))
|
|
range_end = tuple(map(int, matches[-1]))
|
|
# Generate a matrix of tuples from (range_start[0], range_start[1]) to (range_end[0], range_end[1])
|
|
grid_pinpoints = [
|
|
(i, j)
|
|
for i in range(range_start[0], range_end[0] + 1)
|
|
for j in range(range_start[1], range_end[1] + 1)
|
|
]
|
|
# Multiply all elements by patch_size
|
|
grid_pinpoints = [[dim * patch_size for dim in pair] for pair in grid_pinpoints]
|
|
|
|
if type(grid_pinpoints) is list:
|
|
possible_resolutions = grid_pinpoints
|
|
else:
|
|
possible_resolutions = ast.literal_eval(grid_pinpoints)
|
|
best_resolution = select_best_resolution(image.size, possible_resolutions)
|
|
image_padded = resize_and_pad_image(image, best_resolution)
|
|
|
|
# For Siglip processor, only have size but no crop size.
|
|
# In transformers v5, crop_size may exist but be None.
|
|
crop_size = (
|
|
processor.crop_size["height"]
|
|
if getattr(processor, "crop_size", None) is not None
|
|
else processor.size["height"]
|
|
)
|
|
shortest_edge = (
|
|
processor.size["shortest_edge"]
|
|
if "shortest_edge" in processor.size
|
|
else processor.size["height"]
|
|
)
|
|
patches = divide_to_patches(image_padded, crop_size)
|
|
|
|
image_original_resize = image.resize((shortest_edge, shortest_edge))
|
|
|
|
image_patches = [image_original_resize] + patches
|
|
image_patches = [
|
|
processor.preprocess(image_patch.convert("RGB"))["pixel_values"][0]
|
|
for image_patch in image_patches
|
|
]
|
|
# In transformers v5, image processors may return torch.Tensor instead of numpy arrays
|
|
image_patches = [ensure_numpy(p) for p in image_patches]
|
|
return np.stack(image_patches, axis=0)
|
|
|
|
|
|
def load_image_from_base64(image):
|
|
return Image.open(BytesIO(pybase64.b64decode(image, validate=True)))
|
|
|
|
|
|
def expand2square(pil_img, background_color):
|
|
width, height = pil_img.size
|
|
if width == height:
|
|
return pil_img
|
|
if pil_img.mode == "L":
|
|
pil_img = pil_img.convert("RGB")
|
|
if width > height:
|
|
result = Image.new(pil_img.mode, (width, width), background_color)
|
|
result.paste(pil_img, (0, (width - height) // 2))
|
|
return result
|
|
else:
|
|
result = Image.new(pil_img.mode, (height, height), background_color)
|
|
result.paste(pil_img, ((height - width) // 2, 0))
|
|
return result
|
|
|
|
|
|
def unpad_image(tensor, original_size):
|
|
"""
|
|
Unpads a PyTorch tensor of a padded and resized image.
|
|
|
|
Args:
|
|
tensor (torch.Tensor): The image tensor, assumed to be in CxHxW format.
|
|
original_size (tuple): The original size of the image (height, width).
|
|
|
|
Returns:
|
|
torch.Tensor: The unpadded image tensor.
|
|
"""
|
|
original_width, original_height = original_size
|
|
current_height, current_width = tensor.shape[1:]
|
|
|
|
original_aspect_ratio = original_width / original_height
|
|
current_aspect_ratio = current_width / current_height
|
|
|
|
if original_aspect_ratio > current_aspect_ratio:
|
|
scale_factor = current_width / original_width
|
|
new_height = int(original_height * scale_factor)
|
|
padding = (current_height - new_height) // 2
|
|
unpadded_tensor = tensor[:, padding : current_height - padding, :]
|
|
else:
|
|
scale_factor = current_height / original_height
|
|
new_width = int(original_width * scale_factor)
|
|
padding = (current_width - new_width) // 2
|
|
unpadded_tensor = tensor[:, :, padding : current_width - padding]
|
|
|
|
return unpadded_tensor
|
|
|
|
|
|
def unpad_image_shape(current_height, current_width, original_size):
|
|
"""
|
|
Unpads a PyTorch tensor of a padded and resized image
|
|
and returns the new shape.
|
|
"""
|
|
original_width, original_height = original_size
|
|
|
|
original_aspect_ratio = original_width / original_height
|
|
current_aspect_ratio = current_width / current_height
|
|
|
|
if original_aspect_ratio > current_aspect_ratio:
|
|
scale_factor = current_width / original_width
|
|
new_height = int(original_height * scale_factor)
|
|
padding = (current_height - new_height) // 2
|
|
new_shape = (current_height - 2 * padding, current_width)
|
|
else:
|
|
scale_factor = current_height / original_height
|
|
new_width = int(original_width * scale_factor)
|
|
padding = (current_width - new_width) // 2
|
|
new_shape = (current_height, current_width - 2 * padding)
|
|
|
|
return new_shape
|
|
|
|
|
|
def process_images(images, image_processor, model_cfg):
|
|
image_aspect_ratio = getattr(model_cfg, "image_aspect_ratio", None)
|
|
new_images = []
|
|
if image_aspect_ratio == "pad":
|
|
for image in images:
|
|
image = expand2square(
|
|
image, tuple(int(x * 255) for x in image_processor.image_mean)
|
|
)
|
|
image = image_processor.preprocess(image)["pixel_values"][0]
|
|
new_images.append(image)
|
|
elif "anyres" in image_aspect_ratio:
|
|
for image in images:
|
|
image = process_anyres_image(
|
|
image, image_processor, model_cfg.image_grid_pinpoints
|
|
)
|
|
new_images.append(image)
|
|
else:
|
|
return image_processor(images)["pixel_values"]
|
|
if all(x.shape == new_images[0].shape for x in new_images):
|
|
new_images = np.stack(new_images, axis=0)
|
|
return new_images
|
|
|
|
|
|
# Adapted from https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/models/vision.py
|
|
def get_dp_encoder_lb_assignment(
|
|
sizes: list[int],
|
|
num_gpus: int = 2,
|
|
) -> tuple[list[int], list[int], list[int]]:
|
|
"""
|
|
Generate load balancing assignment and metadata
|
|
for distributing data across GPUs.
|
|
The load is determined by the total image sizes,
|
|
not the number of images.
|
|
|
|
Args:
|
|
sizes: The size of each image
|
|
num_gpus: Number of GPUs to balance across
|
|
|
|
Returns:
|
|
shuffle_indices:
|
|
Indices to reorder data for balanced loading
|
|
gpu_sample_counts:
|
|
Number of samples assigned to each GPU
|
|
grouped_sizes_per_gpu:
|
|
Total size assigned to each GPU
|
|
|
|
Example:
|
|
```
|
|
sizes = [1000, 100, 200, 50]
|
|
num_gpus = 2
|
|
```
|
|
|
|
"""
|
|
|
|
n_samples = len(sizes)
|
|
|
|
# Handle edge cases
|
|
if n_samples == 0:
|
|
return [], [0] * num_gpus, [0] * num_gpus
|
|
|
|
# Use greedy algorithm - balance by total size, not sample count
|
|
gpu_assignments = [list[int]() for _ in range(num_gpus)]
|
|
gpu_loads = [0] * num_gpus # This tracks total SIZE, not sample count
|
|
|
|
# Sort indices by size (largest first for better load balancing)
|
|
# sizes = [1000, 100, 200, 50]
|
|
# large_to_small_indices = [0, 2, 1, 3]
|
|
large_to_small_indices = sorted(
|
|
range(n_samples), key=lambda i: sizes[i], reverse=True
|
|
)
|
|
|
|
for idx in large_to_small_indices:
|
|
# Find GPU with minimum current load (by total size)
|
|
min_gpu = min(range(num_gpus), key=lambda i: gpu_loads[i])
|
|
gpu_assignments[min_gpu].append(idx)
|
|
gpu_loads[min_gpu] += sizes[idx]
|
|
|
|
# Create shuffle indices and counts
|
|
shuffle_indices = list[int]()
|
|
gpu_sample_counts = list[int]()
|
|
for gpu_id in range(num_gpus):
|
|
# GPU_0 = [1000] = [0]
|
|
# GPU_1 = [200, 100, 50] = [2, 1, 3]
|
|
# shuffle_indices = [0, 2, 1, 3]
|
|
shuffle_indices.extend(gpu_assignments[gpu_id])
|
|
# GPU_0 = [1]
|
|
# GPU_1 = [3]
|
|
# gpu_sample_counts = [1, 3]
|
|
gpu_sample_counts.append(len(gpu_assignments[gpu_id]))
|
|
|
|
return (shuffle_indices, gpu_sample_counts, gpu_loads)
|
|
|
|
|
|
# Adapted from https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/models/vision.py
|
|
def run_dp_sharded_vision_model(
|
|
image_input: torch.Tensor, vision_model: torch.nn.Module
|
|
) -> torch.Tensor:
|
|
"""Run a vision model with data parallelism (DP) sharding. The function
|
|
will shard the input image tensor on the first dimension and run the vision
|
|
model
|
|
|
|
Args:
|
|
image_input (torch.Tensor): Image input tensor.
|
|
vision_model (torch.nn.Module): Vision model.
|
|
Returns:
|
|
torch.Tensor: Output image embeddings
|
|
"""
|
|
|
|
num_chunks = image_input.shape[0]
|
|
mp_world_size = get_parallel().tp_size
|
|
num_chunks_per_rank = (num_chunks + mp_world_size - 1) // mp_world_size
|
|
num_padded_chunks = num_chunks_per_rank * mp_world_size - num_chunks
|
|
pad = (0,) * (2 * (image_input.dim() - 1)) + (0, num_padded_chunks)
|
|
image_input_padded = torch.nn.functional.pad(image_input, pad)
|
|
rank = get_parallel().tp_rank
|
|
image_input_per_rank = image_input_padded[
|
|
rank * num_chunks_per_rank : (rank + 1) * num_chunks_per_rank, ...
|
|
]
|
|
|
|
vision_embeddings = vision_model(image_input_per_rank)
|
|
# Ensure tensor is contiguous before all_gather
|
|
vision_embeddings = vision_embeddings.last_hidden_state.contiguous()
|
|
vision_embeddings = tensor_model_parallel_all_gather(vision_embeddings, dim=0)
|
|
vision_embeddings = vision_embeddings[:num_chunks, ...]
|
|
return vision_embeddings
|
|
|
|
|
|
# Adapted from https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/models/vision.py
|
|
def run_dp_sharded_mrope_vision_model(
|
|
vision_model: torch.nn.Module,
|
|
pixel_values: torch.Tensor,
|
|
grid_thw_list: list,
|
|
*,
|
|
rope_type: Literal["rope_3d", "rope_2d"],
|
|
):
|
|
"""Run a vision model with data parallelism (DP) sharding.
|
|
The function will shard the input image tensor on the
|
|
first dimension and run the vision model.
|
|
This function is used to run the vision model with mrope.
|
|
|
|
Args:
|
|
vision_model (torch.nn.Module): Vision model.
|
|
pixel_values (torch.Tensor): Image/Video input tensor.
|
|
grid_thw_list: List of grid dimensions for each image
|
|
rope_type: Type of rope used in the vision model.
|
|
Different rope types have different dimension to do ViT.
|
|
"rope_3d" for 3D rope (e.g., Qwen2.5-VL)
|
|
"rope_2d" for 2D rope (e.g., Kimi-VL)
|
|
Returns:
|
|
torch.Tensor: Output image embeddings
|
|
|
|
Example:
|
|
```
|
|
vision_model.out_hidden_size = 64
|
|
vision_model.spatial_merge_size = 2
|
|
pixel_values.shape = (1350, channel)
|
|
grid_thw_list = [[1, 10, 100], [1, 10, 10], [1, 10, 20], [1, 50]]
|
|
tp_size = 2
|
|
```
|
|
|
|
"""
|
|
tp_size = get_parallel().attn_tp_size
|
|
if tp_size == 1:
|
|
return vision_model(pixel_values, grid_thw=torch.tensor(grid_thw_list))
|
|
|
|
# GPU_0 tp_rank_local = 0
|
|
# GPU_1 tp_rank_local = 1
|
|
tp_rank_local = get_parallel().attn_tp_rank
|
|
|
|
# patches_per_image = [1000, 100, 200, 50]
|
|
patches_per_image = [math.prod(grid_thw) for grid_thw in grid_thw_list]
|
|
# print(f"{patches_per_image = }")
|
|
# patches_per_image = [0, 1000, 1100, 1300, 1350]
|
|
cum_patches_per_image = [0, *itertools.accumulate(patches_per_image)]
|
|
|
|
# Get load balancing assignment with all metadata
|
|
# image_to_tp_rank = [0, 2, 1, 3]
|
|
# gpu_sample_counts = [1, 3]
|
|
# grouped_pixel_values_len = [1000, 350]
|
|
image_to_tp_rank, gpu_sample_counts, grouped_pixel_values_len = (
|
|
get_dp_encoder_lb_assignment(patches_per_image, tp_size)
|
|
)
|
|
|
|
# cu_gpu_sample_counts = [0, 1, 4]
|
|
cum_gpu_sample_counts = [0, *itertools.accumulate(gpu_sample_counts)]
|
|
|
|
# GPU_0 image_idxs_local = [0]
|
|
# GPU_1 image_idxs_local = [2, 1, 3]
|
|
image_idxs_local = image_to_tp_rank[
|
|
cum_gpu_sample_counts[tp_rank_local] : cum_gpu_sample_counts[tp_rank_local + 1]
|
|
]
|
|
|
|
# Get the pixel values for the local images based on the image_idxs_local
|
|
if len(image_idxs_local) > 0:
|
|
pixel_values_local = torch.cat(
|
|
[
|
|
pixel_values[cum_patches_per_image[i] : cum_patches_per_image[i + 1]]
|
|
for i in image_idxs_local
|
|
]
|
|
)
|
|
else:
|
|
# Handle case where this rank has no images
|
|
pixel_values_local = torch.empty(
|
|
(0, pixel_values.shape[1]),
|
|
device=pixel_values.device,
|
|
dtype=pixel_values.dtype,
|
|
)
|
|
# embed_dim_reduction_factor = 2 * 2
|
|
if rope_type == "rope_2d":
|
|
embed_dim_reduction_factor = (
|
|
vision_model.merge_kernel_size[0] * vision_model.merge_kernel_size[1]
|
|
)
|
|
else:
|
|
embed_dim_reduction_factor = (
|
|
vision_model.spatial_merge_size * vision_model.spatial_merge_size
|
|
)
|
|
|
|
# Find the max length across all ranks
|
|
# The output embedding of every DP rank has to be
|
|
# padded to this length for tensor_model_parallel_all_gather
|
|
# to work
|
|
max_len_per_rank = max(grouped_pixel_values_len) // embed_dim_reduction_factor
|
|
local_grid_thw_list = [grid_thw_list[i] for i in image_idxs_local]
|
|
|
|
# Run the vision model on the local pixel_values_local
|
|
if rope_type == "rope_2d":
|
|
if pixel_values_local.shape[0] > 0:
|
|
image_embeds_local = vision_model(
|
|
pixel_values_local, torch.tensor(local_grid_thw_list)
|
|
)
|
|
if isinstance(image_embeds_local, list):
|
|
image_embeds_local = torch.cat(image_embeds_local, dim=0)
|
|
else:
|
|
out_dim = getattr(vision_model.config, "hidden_size", None)
|
|
image_embeds_local = torch.empty(
|
|
(0, embed_dim_reduction_factor, out_dim),
|
|
device=pixel_values.device,
|
|
dtype=pixel_values.dtype,
|
|
)
|
|
else:
|
|
if pixel_values_local.shape[0] > 0:
|
|
# print(f"{local_grid_thw_list = }", flush=True)
|
|
image_embeds_local = vision_model(
|
|
pixel_values_local, torch.tensor(local_grid_thw_list)
|
|
)
|
|
else:
|
|
# Handle empty case
|
|
image_embeds_local = torch.empty(
|
|
(0, vision_model.out_hidden_size),
|
|
device=pixel_values.device,
|
|
dtype=pixel_values.dtype,
|
|
)
|
|
|
|
# Pad the output based on max_len_per_rank
|
|
# for tensor_model_parallel_all_gather to work
|
|
current_len = image_embeds_local.shape[0]
|
|
if current_len < max_len_per_rank:
|
|
padding_size = max_len_per_rank - current_len
|
|
if rope_type == "rope_2d":
|
|
padding = torch.empty(
|
|
(
|
|
padding_size,
|
|
image_embeds_local.shape[1],
|
|
image_embeds_local.shape[2],
|
|
),
|
|
dtype=image_embeds_local.dtype,
|
|
device=image_embeds_local.device,
|
|
)
|
|
else:
|
|
padding = torch.empty(
|
|
(padding_size, image_embeds_local.shape[1]),
|
|
dtype=image_embeds_local.dtype,
|
|
device=image_embeds_local.device,
|
|
)
|
|
image_embeds_local_padded = torch.cat([image_embeds_local, padding], dim=0)
|
|
else:
|
|
image_embeds_local_padded = image_embeds_local
|
|
|
|
# Do all_gather to collect embeddings from all ranks
|
|
gathered_embeds = get_parallel().attn_tp_group.all_gather(
|
|
image_embeds_local_padded, dim=0
|
|
)
|
|
|
|
# Remove padding and reconstruct per-rank embeddings
|
|
rank_embeddings = list[torch.Tensor]()
|
|
for rank in range(tp_size):
|
|
start_idx = rank * max_len_per_rank
|
|
end_idx = start_idx + (
|
|
grouped_pixel_values_len[rank] // embed_dim_reduction_factor
|
|
)
|
|
rank_embeddings.append(gathered_embeds[start_idx:end_idx])
|
|
|
|
patches_per_output_image = [
|
|
(patch_size // embed_dim_reduction_factor) for patch_size in patches_per_image
|
|
]
|
|
|
|
# Reconstruct embeddings in the original order
|
|
original_order_embeddings = [None] * len(grid_thw_list)
|
|
current_idx = 0
|
|
for rank in range(tp_size):
|
|
count = gpu_sample_counts[rank]
|
|
if count > 0:
|
|
# Get images assigned to this rank in shuffled order
|
|
# GPU_0 = image_idxs_local [0]
|
|
# GPU_1 = image_idxs_local [2, 1, 3]
|
|
rank_images = image_to_tp_rank[current_idx : current_idx + count]
|
|
|
|
rank_embed = rank_embeddings[rank]
|
|
# Split rank embeddings back to individual images
|
|
embed_start = 0
|
|
for img_idx in rank_images:
|
|
img_patches = patches_per_output_image[img_idx]
|
|
original_order_embeddings[img_idx] = rank_embed[
|
|
embed_start : embed_start + img_patches
|
|
]
|
|
embed_start += img_patches
|
|
current_idx += count
|
|
out_embeddings = torch.cat(original_order_embeddings, dim=0)
|
|
return out_embeddings
|