profiling.py 10.2 KB
Newer Older
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
from abc import ABC, abstractmethod
4
5
from collections.abc import Mapping
from dataclasses import dataclass, field
6
from typing import Generic, NamedTuple, TypeVar
7
8
9
10
11

import numpy as np
import numpy.typing as npt
from PIL import Image

12
13
14
15
16
17
from vllm.config.multimodal import (
    AudioDummyOptions,
    BaseDummyOptions,
    ImageDummyOptions,
    VideoDummyOptions,
)
18
19
from vllm.logger import init_logger

20
21
22
23
24
25
26
27
28
29
from .inputs import (
    MultiModalDataDict,
    MultiModalInputs,
    MultiModalKwargsItems,
    MultiModalPlaceholderDict,
)
from .processing import (
    BaseMultiModalProcessor,
    BaseProcessingInfo,
)
30
31
32
33
34
35

logger = init_logger(__name__)


@dataclass
class ProcessorInputs:
36
37
    """
    Represents the keyword arguments to
38
    [`vllm.multimodal.processing.BaseMultiModalProcessor.apply`][].
39
    """
40

41
    prompt: str | list[int]
42
43
    mm_data: MultiModalDataDict
    hf_processor_mm_kwargs: Mapping[str, object] = field(default_factory=dict)
44
    tokenization_kwargs: Mapping[str, object] = field(default_factory=dict)
45
46


47
48
49
50
51
52
53
54
55
56
class DummyEncoderData(NamedTuple):
    """Dummy data used for profiling."""

    prompt_token_ids: list[int]


class DummyDecoderData(NamedTuple):
    """Dummy data used for profiling."""

    prompt_token_ids: list[int]
57
    multi_modal_data: MultiModalKwargsItems
58
59
60
    multi_modal_placeholders: MultiModalPlaceholderDict


61
62
63
64
_I = TypeVar("_I", bound=BaseProcessingInfo)


class BaseDummyInputsBuilder(ABC, Generic[_I]):
65
    """
66
    Abstract base class that constructs the dummy data to profile
67
68
69
    multi-modal models.
    """

70
    def __init__(self, info: _I) -> None:
71
72
        super().__init__()

73
        self.info = info
74

75
    @abstractmethod
76
77
    def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
        """
78
        Build the text input corresponding to `mm_counts`.
79
        """
80
        raise NotImplementedError
81

82
    @abstractmethod
83
84
85
86
    def get_dummy_mm_data(
        self,
        seq_len: int,
        mm_counts: Mapping[str, int],
87
        mm_options: Mapping[str, BaseDummyOptions] | None = None,
88
89
90
91
    ) -> MultiModalDataDict:
        """
        Build the multimodal input which, after processing, results in
        the maximum possible number of placeholder tokens.
92
93
94
95
96
97

        Args:
            seq_len: Sequence length
            mm_counts: Count of items per modality
            mm_options: Configurable options per modality (optional).
                       If None, use model defaults for backward compatibility.
98
                       If provided, models can use these to customize dummy
99
                       data generation.
100
101
102
        """
        raise NotImplementedError

103
104
105
106
    def get_dummy_processor_inputs(
        self,
        seq_len: int,
        mm_counts: Mapping[str, int],
107
        mm_options: Mapping[str, BaseDummyOptions] | None = None,
108
109
    ) -> ProcessorInputs:
        """
110
        Build the input which, after processing, results in
111
        the maximum possible number of placeholder tokens.
112
113
114
115
116

        Args:
            seq_len: Sequence length
            mm_counts: Count of items per modality
            mm_options: Configurable options per modality (optional)
117
        """
118
        dummy_text = self.get_dummy_text(mm_counts)
119
120
121
122

        # Use the unified function for both legacy and configurable cases
        dummy_mm_data = self.get_dummy_mm_data(seq_len, mm_counts, mm_options)

123
        tokenization_kwargs = {"truncation": False}
124

125
126
127
128
129
        return ProcessorInputs(
            prompt=dummy_text,
            mm_data=dummy_mm_data,
            tokenization_kwargs=tokenization_kwargs,
        )
130
131
132
133
134
135

    def _get_dummy_audios(
        self,
        *,
        length: int,
        num_audios: int,
136
        overrides: AudioDummyOptions | None = None,
137
    ) -> list[npt.NDArray]:
138
139
        if num_audios == 0:
            return []
140
141
142
143
        if overrides and overrides.length:
            if overrides.length > length:
                logger.warning(
                    "audio.length override (%d) exceeds model's "
144
145
146
147
                    "maximum length (%d), will be ignored",
                    overrides.length,
                    length,
                )
148
            length = min(length, overrides.length)
149
        audio = np.zeros((length,))
150
151
152
153
154
155
156
157
        return [audio] * num_audios

    def _get_dummy_images(
        self,
        *,
        width: int,
        height: int,
        num_images: int,
158
        overrides: ImageDummyOptions | None = None,
159
    ) -> list[Image.Image]:
160
161
        if num_images == 0:
            return []
162
163
164
165
166
        if overrides:
            if overrides.width:
                if overrides.width > width:
                    logger.warning(
                        "image.width override (%d) exceeds model's "
167
168
169
170
                        "maximum width (%d), will be ignored",
                        overrides.width,
                        width,
                    )
171
172
173
174
175
176
                width = min(width, overrides.width)
            if overrides.height:
                if overrides.height > height:
                    logger.warning(
                        "image.height override (%d) exceeds model's "
                        "maximum height (%d), will be ignored",
177
178
179
                        overrides.height,
                        height,
                    )
180
                height = min(height, overrides.height)
181
        image = Image.new("RGB", (width, height), color=255)
182
183
184
185
186
187
188
189
190
        return [image] * num_images

    def _get_dummy_videos(
        self,
        *,
        width: int,
        height: int,
        num_frames: int,
        num_videos: int,
191
        overrides: VideoDummyOptions | None = None,
192
    ) -> list[npt.NDArray]:
193
194
        if num_videos == 0:
            return []
195
196
197
198
199
200
        if overrides:
            if overrides.num_frames:
                if overrides.num_frames > num_frames:
                    logger.warning(
                        "video.num_frames override (%d) exceeds model's "
                        "maximum number of frames (%d), will be ignored",
201
202
203
                        overrides.num_frames,
                        num_frames,
                    )
204
205
206
207
208
                num_frames = min(num_frames, overrides.num_frames)
            if overrides.width:
                if overrides.width > width:
                    logger.warning(
                        "video.width override (%d) exceeds model's "
209
210
211
212
                        "maximum width (%d), will be ignored",
                        overrides.width,
                        width,
                    )
213
214
215
216
217
218
                width = min(width, overrides.width)
            if overrides.height:
                if overrides.height > height:
                    logger.warning(
                        "video.height override (%d) exceeds model's "
                        "maximum height (%d), will be ignored",
219
220
221
                        overrides.height,
                        height,
                    )
222
                height = min(height, overrides.height)
223
        video = np.full((num_frames, width, height, 3), 255, dtype=np.uint8)
224
225
        return [video] * num_videos

226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247

class MultiModalProfiler(Generic[_I]):
    """
    Contains code for running memory profiling for multi-modal models.
    """

    def __init__(
        self,
        processor: BaseMultiModalProcessor[_I],
    ) -> None:
        super().__init__()

        self.processor = processor

    @property
    def processing_info(self) -> BaseProcessingInfo:
        return self.processor.info

    @property
    def dummy_inputs(self) -> BaseDummyInputsBuilder[_I]:
        return self.processor.dummy_inputs

248
    def get_mm_limits(self) -> Mapping[str, int]:
249
        return self.processor.allowed_mm_limits
250
251
252
253

    def _get_dummy_mm_inputs(
        self,
        seq_len: int,
254
255
        mm_counts: Mapping[str, int] | None = None,
        mm_options: Mapping[str, BaseDummyOptions] | None = None,
256
    ) -> MultiModalInputs:
257
258
259
        if mm_counts is None:
            mm_counts = self.get_mm_limits()

260
261
        factory = self.dummy_inputs
        processor_inputs = factory.get_dummy_processor_inputs(
262
263
            seq_len, mm_counts, mm_options
        )
264
265

        return self.processor.apply(
266
            prompt=processor_inputs.prompt,
267
268
            mm_data=processor_inputs.mm_data,
            hf_processor_mm_kwargs=processor_inputs.hf_processor_mm_kwargs,
269
            tokenization_kwargs=processor_inputs.tokenization_kwargs,
270
271
        )

272
    def _get_mm_num_tokens(
273
        self,
274
275
        mm_inputs: MultiModalInputs,
    ) -> Mapping[str, int]:
276
277
        placeholders_by_modality = mm_inputs["mm_placeholders"]

278
        return {
279
            modality: sum(item.get_num_embeds for item in placeholders)
280
281
            for modality, placeholders in placeholders_by_modality.items()
        }
282

283
284
285
    def get_decoder_dummy_data(
        self,
        seq_len: int,
286
287
        mm_counts: Mapping[str, int] | None = None,
        mm_options: Mapping[str, BaseDummyOptions] | None = None,
288
    ) -> DummyDecoderData:
289
        mm_inputs = self._get_dummy_mm_inputs(seq_len, mm_counts, mm_options)
290

291
        prompt_token_ids = mm_inputs["prompt_token_ids"]
292
293
        total_len = len(prompt_token_ids)

294
295
        if total_len < seq_len:
            prompt_token_ids.extend([0] * (seq_len - total_len))
296

297
298
        return DummyDecoderData(
            prompt_token_ids=prompt_token_ids,
299
            multi_modal_data=mm_inputs["mm_kwargs"].require_data(),
300
            multi_modal_placeholders=mm_inputs["mm_placeholders"],
301
        )
302

303
    def get_mm_max_tokens(
304
305
        self,
        seq_len: int,
306
        mm_counts: Mapping[str, int] | None = None,
307
    ) -> Mapping[str, int]:
308
309
310
311
        """
        Returns the maximum number of embeddings per item of each modality, excluding
        any break/text tokens in-between multimodal embeddings/encoder outputs.
        """
312
313
314
315
316
317
318
        if mm_counts is None:
            mm_counts = self.get_mm_limits()

        max_tokens_per_item = self.processing_info.get_mm_max_tokens_per_item(
            seq_len=seq_len,
            mm_counts=mm_counts,
        )
319
        if max_tokens_per_item is not None:
320
321
322
323
324
            return {
                modality: max_tokens
                for modality, max_tokens in max_tokens_per_item.items()
                if mm_counts.get(modality, 0) > 0
            }
325

326
        mm_inputs = self._get_dummy_mm_inputs(seq_len, mm_counts)
327
        return self._get_mm_num_tokens(mm_inputs)