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.
Files changed (36) hide show
  1. lightvl/__init__.py +1 -0
  2. lightvl/backends/__init__.py +49 -0
  3. lightvl/backends/base_backend.py +156 -0
  4. lightvl/backends/data_utils.py +206 -0
  5. lightvl/backends/transformers_backend.py +537 -0
  6. lightvl/backends/vllm_backend.py +793 -0
  7. lightvl/clients/__init__.py +0 -0
  8. lightvl/clients/embedding_client.py +253 -0
  9. lightvl/clients/mmmu_pro_eval.py +290 -0
  10. lightvl/clients/vl_client.py +310 -0
  11. lightvl/config/__init__.py +4 -0
  12. lightvl/config/config.py +48 -0
  13. lightvl/entrypoints/__init__.py +6 -0
  14. lightvl/entrypoints/main.py +373 -0
  15. lightvl/metrics/__init__.py +13 -0
  16. lightvl/metrics/metrics.py +948 -0
  17. lightvl/quant_vl/__init__.py +4 -0
  18. lightvl/quant_vl/mixed_quant.py +189 -0
  19. lightvl/quant_vl/model_config.py +140 -0
  20. lightvl/quant_vl/quant_scheme.py +85 -0
  21. lightvl/quant_vl/quantizer/__init__.py +7 -0
  22. lightvl/quant_vl/quantizer/fp8_block_quantizer.py +290 -0
  23. lightvl/quant_vl/quantizer/fp8_quantizer.py +151 -0
  24. lightvl/quant_vl/quantizer/int4_quantizer.py +17 -0
  25. lightvl/quant_vl/quantizer/int8_quantizer.py +159 -0
  26. lightvl/quant_vl/quantizer/quant_tensors.py +222 -0
  27. lightvl/quant_vl/quantizer/quantizer.py +9 -0
  28. lightvl/quant_vl/quantizer/register_quant.py +16 -0
  29. lightvl/quant_vl/utils.py +180 -0
  30. lightvl/simpler/__init__.py +4 -0
  31. lightvl/simpler/model_simpler.py +40 -0
  32. lightvl-0.0.3.dist-info/METADATA +7 -0
  33. lightvl-0.0.3.dist-info/RECORD +36 -0
  34. lightvl-0.0.3.dist-info/WHEEL +5 -0
  35. lightvl-0.0.3.dist-info/entry_points.txt +2 -0
  36. 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