lightvl 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightvl/__init__.py +1 -0
- lightvl/backends/__init__.py +49 -0
- lightvl/backends/base_backend.py +156 -0
- lightvl/backends/data_utils.py +206 -0
- lightvl/backends/transformers_backend.py +537 -0
- lightvl/backends/vllm_backend.py +793 -0
- lightvl/clients/__init__.py +0 -0
- lightvl/clients/embedding_client.py +253 -0
- lightvl/clients/mmmu_pro_eval.py +290 -0
- lightvl/clients/vl_client.py +310 -0
- lightvl/config/__init__.py +4 -0
- lightvl/config/config.py +48 -0
- lightvl/entrypoints/__init__.py +6 -0
- lightvl/entrypoints/main.py +373 -0
- lightvl/metrics/__init__.py +13 -0
- lightvl/metrics/metrics.py +948 -0
- lightvl/quant_vl/__init__.py +4 -0
- lightvl/quant_vl/mixed_quant.py +189 -0
- lightvl/quant_vl/model_config.py +140 -0
- lightvl/quant_vl/quant_scheme.py +85 -0
- lightvl/quant_vl/quantizer/__init__.py +7 -0
- lightvl/quant_vl/quantizer/fp8_block_quantizer.py +290 -0
- lightvl/quant_vl/quantizer/fp8_quantizer.py +151 -0
- lightvl/quant_vl/quantizer/int4_quantizer.py +17 -0
- lightvl/quant_vl/quantizer/int8_quantizer.py +159 -0
- lightvl/quant_vl/quantizer/quant_tensors.py +222 -0
- lightvl/quant_vl/quantizer/quantizer.py +9 -0
- lightvl/quant_vl/quantizer/register_quant.py +16 -0
- lightvl/quant_vl/utils.py +180 -0
- lightvl/simpler/__init__.py +4 -0
- lightvl/simpler/model_simpler.py +40 -0
- lightvl-0.0.3.dist-info/METADATA +7 -0
- lightvl-0.0.3.dist-info/RECORD +36 -0
- lightvl-0.0.3.dist-info/WHEEL +5 -0
- lightvl-0.0.3.dist-info/entry_points.txt +2 -0
- lightvl-0.0.3.dist-info/top_level.txt +1 -0
lightvl/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#lightvl
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Inference backends
|
|
2
|
+
|
|
3
|
+
from .base_backend import BaseBackend
|
|
4
|
+
from .data_utils import (
|
|
5
|
+
load_dataset,
|
|
6
|
+
load_image_as_pil,
|
|
7
|
+
preprocess_image,
|
|
8
|
+
process_image_url,
|
|
9
|
+
get_sample_param_from_config,
|
|
10
|
+
get_model_type,
|
|
11
|
+
parse_max_img_size,
|
|
12
|
+
)
|
|
13
|
+
from .transformers_backend import (
|
|
14
|
+
TransformersBackend,
|
|
15
|
+
run_transformers_inference,
|
|
16
|
+
)
|
|
17
|
+
from .vllm_backend import (
|
|
18
|
+
VLLMBackend,
|
|
19
|
+
run_vllm_inference,
|
|
20
|
+
run_models_on_dataset,
|
|
21
|
+
cleanup_vllm_processes,
|
|
22
|
+
start_vllm_serve,
|
|
23
|
+
stop_vllm_serve,
|
|
24
|
+
wait_for_vllm_ready,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# Base
|
|
29
|
+
"BaseBackend",
|
|
30
|
+
# Data utils
|
|
31
|
+
"load_dataset",
|
|
32
|
+
"load_image_as_pil",
|
|
33
|
+
"preprocess_image",
|
|
34
|
+
"process_image_url",
|
|
35
|
+
"get_sample_param_from_config",
|
|
36
|
+
"get_model_type",
|
|
37
|
+
"parse_max_img_size",
|
|
38
|
+
# Transformers
|
|
39
|
+
"TransformersBackend",
|
|
40
|
+
"run_transformers_inference",
|
|
41
|
+
# vLLM
|
|
42
|
+
"VLLMBackend",
|
|
43
|
+
"run_vllm_inference",
|
|
44
|
+
"run_models_on_dataset",
|
|
45
|
+
"cleanup_vllm_processes",
|
|
46
|
+
"start_vllm_serve",
|
|
47
|
+
"stop_vllm_serve",
|
|
48
|
+
"wait_for_vllm_ready",
|
|
49
|
+
]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Abstract base backend for Vision-Language model inference."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from PIL import Image as PILImage
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
|
|
12
|
+
from lightvl.backends.data_utils import (
|
|
13
|
+
load_dataset,
|
|
14
|
+
load_image_as_pil,
|
|
15
|
+
get_sample_param_from_config,
|
|
16
|
+
parse_max_img_size,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseBackend(ABC):
|
|
21
|
+
"""
|
|
22
|
+
Abstract base class for inference backends.
|
|
23
|
+
|
|
24
|
+
Subclasses implement:
|
|
25
|
+
- load_model() -> (model, processor/tokenizer)
|
|
26
|
+
- infer_sample() -> (output_text, prompt_tokens, completion_tokens)
|
|
27
|
+
- cleanup() -> release GPU resources
|
|
28
|
+
|
|
29
|
+
The shared run_inference() handles dataset loading, progress bar,
|
|
30
|
+
per-sample image loading + timing, error handling, and result formatting.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, model_path: str, config: Optional[Dict[str, Any]] = None):
|
|
34
|
+
self.model_path = model_path
|
|
35
|
+
self.config = config or {}
|
|
36
|
+
self.model = None
|
|
37
|
+
self.processor = None
|
|
38
|
+
|
|
39
|
+
# Parsed config values
|
|
40
|
+
self.sampling_params = get_sample_param_from_config(self.config)
|
|
41
|
+
self.sys_prompt = self.config.get("sys_prompt", "")
|
|
42
|
+
self.usr_prompt = self.config.get("usr_prompt", "")
|
|
43
|
+
self.max_img_size = parse_max_img_size(self.config)
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def load_model(self) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Load model and processor/tokenizer.
|
|
49
|
+
Sets self.model and self.processor.
|
|
50
|
+
Must be called before run_inference().
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def infer_sample(
|
|
56
|
+
self,
|
|
57
|
+
pil_image: Optional[PILImage.Image],
|
|
58
|
+
prompt: str,
|
|
59
|
+
) -> Tuple[str, int, int]:
|
|
60
|
+
"""
|
|
61
|
+
Run inference on a single sample.
|
|
62
|
+
Returns (output_text, prompt_tokens, completion_tokens).
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def get_cmd_str(self) -> str:
|
|
68
|
+
"""Return a human-readable command string describing how the model was loaded."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def run_inference(self, dataset_path: str) -> Tuple[List[Dict[str, Any]], str]:
|
|
72
|
+
"""
|
|
73
|
+
Run inference on all samples in the dataset.
|
|
74
|
+
|
|
75
|
+
Shared implementation handles:
|
|
76
|
+
- Dataset loading
|
|
77
|
+
- Per-sample image loading (excluded from latency)
|
|
78
|
+
- Inference timing
|
|
79
|
+
- Progress bar display
|
|
80
|
+
- Error handling and result formatting
|
|
81
|
+
|
|
82
|
+
:return: (results_list, command_str)
|
|
83
|
+
"""
|
|
84
|
+
data_list = load_dataset(dataset_path)
|
|
85
|
+
logger.info("Running inference on {} samples", len(data_list))
|
|
86
|
+
|
|
87
|
+
results = []
|
|
88
|
+
time.sleep(0.1) # allow GPU to settle after model load
|
|
89
|
+
|
|
90
|
+
for idx, item in enumerate(
|
|
91
|
+
tqdm(data_list, desc=f"{self.__class__.__name__} inference", unit="sample")
|
|
92
|
+
):
|
|
93
|
+
prompt = item.get("prompt", "") or self.usr_prompt
|
|
94
|
+
image_url = item.get("image_url", "")
|
|
95
|
+
ground_truth = item.get("ground_truth", "")
|
|
96
|
+
|
|
97
|
+
start_time = None
|
|
98
|
+
try:
|
|
99
|
+
# Image loading is NOT counted in inference latency
|
|
100
|
+
pil_image = None
|
|
101
|
+
if image_url:
|
|
102
|
+
pil_image = load_image_as_pil(image_url, self.max_img_size)
|
|
103
|
+
|
|
104
|
+
start_time = time.time()
|
|
105
|
+
|
|
106
|
+
output_text, prompt_tokens, completion_tokens = self.infer_sample(
|
|
107
|
+
pil_image, prompt
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Read first-token logits if backend provides them
|
|
111
|
+
first_token_logits = getattr(self, '_last_first_token_logits', None)
|
|
112
|
+
|
|
113
|
+
latency_ms = (time.time() - start_time) * 1000
|
|
114
|
+
ttft_ms = 0.0 # Not measurable without streaming
|
|
115
|
+
tpot_ms = 0.0 # Not measurable without streaming
|
|
116
|
+
|
|
117
|
+
result = {
|
|
118
|
+
"index": idx,
|
|
119
|
+
"input": {
|
|
120
|
+
"image_url": image_url,
|
|
121
|
+
"prompt": prompt,
|
|
122
|
+
"ground_truth": ground_truth,
|
|
123
|
+
},
|
|
124
|
+
"output": output_text,
|
|
125
|
+
"latency_ms": latency_ms,
|
|
126
|
+
"ttft_ms": ttft_ms,
|
|
127
|
+
"tpot_ms": tpot_ms,
|
|
128
|
+
"prompt_tokens": prompt_tokens,
|
|
129
|
+
"completion_tokens": completion_tokens,
|
|
130
|
+
"first_token_logits": first_token_logits,
|
|
131
|
+
}
|
|
132
|
+
except Exception as e:
|
|
133
|
+
latency_ms = (time.time() - start_time) * 1000 if start_time is not None else 0
|
|
134
|
+
logger.error("Inference failed for sample {}: {}", idx, str(e))
|
|
135
|
+
result = {
|
|
136
|
+
"index": idx,
|
|
137
|
+
"input": {
|
|
138
|
+
"image_url": image_url,
|
|
139
|
+
"prompt": prompt,
|
|
140
|
+
"ground_truth": ground_truth,
|
|
141
|
+
},
|
|
142
|
+
"output": None,
|
|
143
|
+
"latency_ms": latency_ms,
|
|
144
|
+
"ttft_ms": 0.0,
|
|
145
|
+
"tpot_ms": 0.0,
|
|
146
|
+
"prompt_tokens": 0,
|
|
147
|
+
"completion_tokens": 0,
|
|
148
|
+
"error": str(e),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
results.append(result)
|
|
152
|
+
|
|
153
|
+
cmd_str = self.get_cmd_str()
|
|
154
|
+
logger.info("{} inference completed: {} results", self.__class__.__name__, len(results))
|
|
155
|
+
|
|
156
|
+
return results, cmd_str
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Shared data loading, image preprocessing, and config utilities for all backends."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import csv
|
|
5
|
+
import io
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Dict, List, Tuple
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from PIL import Image
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ===================== Dataset Loading =====================
|
|
18
|
+
|
|
19
|
+
def load_dataset(dataset_path: str) -> List[Dict[str, Any]]:
|
|
20
|
+
"""
|
|
21
|
+
Load dataset from CSV file with unified format for both VL and embedding tasks.
|
|
22
|
+
CSV format: prompt, image_url, ground_truth
|
|
23
|
+
:param dataset_path: Dataset path (CSV file)
|
|
24
|
+
:return: Data list with unified format: {"image_url": str, "prompt": str, "ground_truth": str}
|
|
25
|
+
"""
|
|
26
|
+
data_list = []
|
|
27
|
+
logger.info("Loading dataset file: {}", dataset_path)
|
|
28
|
+
|
|
29
|
+
if dataset_path.endswith(".csv"):
|
|
30
|
+
with open(dataset_path, "r", encoding="utf-8") as f:
|
|
31
|
+
reader = csv.DictReader(f)
|
|
32
|
+
for row in reader:
|
|
33
|
+
item = {
|
|
34
|
+
"image_url": row.get("image_url", ""),
|
|
35
|
+
"prompt": row.get("prompt", ""),
|
|
36
|
+
"ground_truth": row.get("ground_truth", ""),
|
|
37
|
+
}
|
|
38
|
+
data_list.append(item)
|
|
39
|
+
|
|
40
|
+
# Fill empty prompts with the first row's prompt
|
|
41
|
+
if data_list:
|
|
42
|
+
first_prompt = data_list[0].get("prompt", "")
|
|
43
|
+
if first_prompt:
|
|
44
|
+
for item in data_list:
|
|
45
|
+
if not item.get("prompt", ""):
|
|
46
|
+
item["prompt"] = first_prompt
|
|
47
|
+
|
|
48
|
+
logger.info("Loaded {} data entries", len(data_list))
|
|
49
|
+
return data_list
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ===================== Image Preprocessing =====================
|
|
53
|
+
|
|
54
|
+
def preprocess_image(
|
|
55
|
+
image: Image.Image,
|
|
56
|
+
target_size: Tuple[int, int],
|
|
57
|
+
min_size: Tuple[int, int] = (32, 32),
|
|
58
|
+
) -> Image.Image:
|
|
59
|
+
"""
|
|
60
|
+
Pre-process an image by scaling it to fit within pixel bounds.
|
|
61
|
+
|
|
62
|
+
If the image exceeds ``target_size[0] * target_size[1]`` pixels, scale it
|
|
63
|
+
down proportionally. If it is smaller than ``min_size[0] * min_size[1]``
|
|
64
|
+
pixels, scale it up proportionally. Always returns an RGB image.
|
|
65
|
+
|
|
66
|
+
:param image: PIL Image
|
|
67
|
+
:param target_size: Maximum pixel dimensions (width, height). The product
|
|
68
|
+
defines **image_max_pixels**.
|
|
69
|
+
:param min_size: Minimum pixel dimensions (width, height). The product
|
|
70
|
+
defines **image_min_pixels**.
|
|
71
|
+
:return: Pre-processed PIL Image in RGB mode
|
|
72
|
+
"""
|
|
73
|
+
image_max_pixels = target_size[0] * target_size[1]
|
|
74
|
+
image_min_pixels = min_size[0] * min_size[1]
|
|
75
|
+
|
|
76
|
+
img_w = image.width
|
|
77
|
+
img_h = image.height
|
|
78
|
+
img_pixels = img_w * img_h
|
|
79
|
+
|
|
80
|
+
if img_pixels > image_max_pixels:
|
|
81
|
+
resize_factor = math.sqrt(image_max_pixels / img_pixels)
|
|
82
|
+
new_w, new_h = int(img_w * resize_factor), int(img_h * resize_factor)
|
|
83
|
+
image = image.resize((new_w, new_h), Image.NEAREST)
|
|
84
|
+
|
|
85
|
+
if img_pixels < image_min_pixels:
|
|
86
|
+
resize_factor = math.sqrt(image_min_pixels / img_pixels)
|
|
87
|
+
new_w, new_h = int(img_w * resize_factor), int(img_h * resize_factor)
|
|
88
|
+
image = image.resize((new_w, new_h), Image.NEAREST)
|
|
89
|
+
|
|
90
|
+
if image.mode != "RGB":
|
|
91
|
+
image = image.convert("RGB")
|
|
92
|
+
|
|
93
|
+
return image
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_image_as_pil(image_url: str, target_size: Tuple[int, int] = None) -> Image.Image:
|
|
97
|
+
"""
|
|
98
|
+
Download image or load local image, resize and return PIL Image.
|
|
99
|
+
|
|
100
|
+
Uses :func:`preprocess_image` for pixel-bound-aware resizing.
|
|
101
|
+
|
|
102
|
+
:param image_url: Image URL or local file path
|
|
103
|
+
:param target_size: Target size tuple (width, height), e.g. (512, 512).
|
|
104
|
+
Used to compute max_pixels = w * h.
|
|
105
|
+
:return: PIL Image in RGB mode
|
|
106
|
+
:raises ValueError: If image source is invalid
|
|
107
|
+
"""
|
|
108
|
+
image_bytes = None
|
|
109
|
+
|
|
110
|
+
if os.path.isfile(image_url):
|
|
111
|
+
logger.debug("Loading local image: {}", image_url)
|
|
112
|
+
with open(image_url, "rb") as f:
|
|
113
|
+
image_bytes = f.read()
|
|
114
|
+
elif image_url.startswith(("http://", "https://")):
|
|
115
|
+
response = requests.get(image_url, timeout=10)
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
image_bytes = response.content
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"Invalid image source: not a valid URL or local file path: {image_url}")
|
|
120
|
+
|
|
121
|
+
image = Image.open(io.BytesIO(image_bytes))
|
|
122
|
+
|
|
123
|
+
if target_size is not None:
|
|
124
|
+
image = preprocess_image(image, target_size)
|
|
125
|
+
elif image.mode != "RGB":
|
|
126
|
+
image = image.convert("RGB")
|
|
127
|
+
|
|
128
|
+
return image
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def process_image_url(image_url: str, target_size: Tuple[int, int] = None) -> str:
|
|
132
|
+
"""
|
|
133
|
+
Download image or load local image, resize and convert to base64 format.
|
|
134
|
+
:param image_url: Image URL or local file path
|
|
135
|
+
:param target_size: Target size, default 512x512
|
|
136
|
+
:return: Base64 encoded image data (usable by vLLM)
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
image = load_image_as_pil(image_url, target_size)
|
|
140
|
+
|
|
141
|
+
buffer = io.BytesIO()
|
|
142
|
+
image.save(buffer, format="PNG")
|
|
143
|
+
image_bytes = buffer.getvalue()
|
|
144
|
+
base64_str = base64.b64encode(image_bytes).decode("utf-8")
|
|
145
|
+
|
|
146
|
+
return f"data:image/png;base64,{base64_str}"
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error("Image processing failed: {}", str(e))
|
|
150
|
+
return ""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ===================== Config Helpers =====================
|
|
154
|
+
|
|
155
|
+
def get_sample_param_from_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
156
|
+
"""
|
|
157
|
+
Extract sampling parameters from config dict.
|
|
158
|
+
:param config: Configuration dictionary
|
|
159
|
+
:return: Sampling parameters dict
|
|
160
|
+
"""
|
|
161
|
+
return {
|
|
162
|
+
"temperature": config.get("temperature", 0.0),
|
|
163
|
+
"top_p": config.get("top_p", 1.0),
|
|
164
|
+
"n": config.get("n", 1),
|
|
165
|
+
"top_k": config.get("top_k", 1),
|
|
166
|
+
"repetition_penalty": config.get("repetition_penalty", 1.0),
|
|
167
|
+
"max_tokens": config.get("max_tokens", 512),
|
|
168
|
+
"stop": config.get("stop", []),
|
|
169
|
+
"skip_special_tokens": config.get("skip_special_tokens", False),
|
|
170
|
+
"stream": config.get("stream", False),
|
|
171
|
+
"enable_thinking": config.get("enable_thinking", False),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_model_type(model_path: str) -> str:
|
|
176
|
+
"""Read model_type from config.json in the model directory."""
|
|
177
|
+
config_path = os.path.join(model_path, "config.json")
|
|
178
|
+
if not os.path.exists(config_path):
|
|
179
|
+
raise FileNotFoundError(f"config.json not found in {model_path}")
|
|
180
|
+
|
|
181
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
182
|
+
config = json.load(f)
|
|
183
|
+
|
|
184
|
+
model_type = config.get("model_type", "")
|
|
185
|
+
if not model_type:
|
|
186
|
+
raise ValueError(f"model_type not found in config.json for {model_path}")
|
|
187
|
+
|
|
188
|
+
return model_type
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def parse_max_img_size(config: Dict[str, Any]) -> Tuple[int, int]:
|
|
192
|
+
"""
|
|
193
|
+
Parse max_img_size from config, supporting both string and tuple formats.
|
|
194
|
+
:param config: Configuration dictionary
|
|
195
|
+
:return: (width, height) tuple
|
|
196
|
+
"""
|
|
197
|
+
max_img_size = config.get("max_img_size", None)
|
|
198
|
+
if max_img_size:
|
|
199
|
+
try:
|
|
200
|
+
max_img_size = ast.literal_eval(max_img_size)
|
|
201
|
+
except Exception:
|
|
202
|
+
max_img_size = (512, 512)
|
|
203
|
+
else:
|
|
204
|
+
max_img_size = (512, 512)
|
|
205
|
+
|
|
206
|
+
return max_img_size
|