Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,11 @@ Content type for request body serialization. By default, requests are sent as 'a

HTTP header name used to carry the per-session affinity identifier. When set, replaces the default `X-Correlation-ID` header with the provided name (e.g., `--session-header X-Session-ID`).

#### `--uuid-and-strip`

Enable AIPerf-managed image stripping for vLLM's multimodal processor cache. Dataset-authored image UUIDs, including UUID-only cache references, always pass through on the chat endpoint; this flag only strips repeated content after AIPerf observes it in the same session. Automatic stripping supports only single_turn datasets with session_id-grouped rows; multi_turn is rejected. The server cache must cover the working set, and requests in a session must reach a replica that retains earlier UUIDs.
<br/>_Flag (no value required)_

### Tokenizer

#### `--tokenizer` `<str>`
Expand Down Expand Up @@ -1837,6 +1842,11 @@ Content type for request body serialization. By default, requests are sent as 'a

HTTP header name used to carry the per-session affinity identifier. When set, replaces the default `X-Correlation-ID` header with the provided name (e.g., `--session-header X-Session-ID`).

#### `--uuid-and-strip`

Enable AIPerf-managed image stripping for vLLM's multimodal processor cache. Dataset-authored image UUIDs, including UUID-only cache references, always pass through on the chat endpoint; this flag only strips repeated content after AIPerf observes it in the same session. Automatic stripping supports only single_turn datasets with session_id-grouped rows; multi_turn is rejected. The server cache must cover the working set, and requests in a session must reach a replica that retains earlier UUIDs.
<br/>_Flag (no value required)_

### Tokenizer

#### `--tokenizer` `<str>`
Expand Down
6 changes: 4 additions & 2 deletions docs/metrics-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,15 +522,17 @@ total_token_throughput = (total_isl + total_osl) / benchmark_duration_seconds

**Type:** [Record Metric](#record-metrics)

The number of images in the request, summed across all turns. This is the foundation metric used by Image Throughput and Image Latency.
The number of logical image references in the wire request. This is the foundation metric used by Image Throughput and Image Latency.

**Formula:**
```python
num_images = sum(len(image.contents) for turn in request.turns for image in turn.images)
num_images = count_image_content_parts(wire_payload)
```

**Notes:**
- Requires at least one image in at least one turn.
- Counts UUID cache-only references as logical images even when their URL is empty.
- Does not measure uploaded image bytes or cache misses.
- Not displayed in console output (`console_group = MetricConsoleGroup.NONE`).

---
Expand Down
37 changes: 33 additions & 4 deletions src/aiperf/common/models/dataset_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
from typing import Any, ClassVar

from pydantic import Field, field_validator
from pydantic import Field, field_validator, model_validator

from aiperf.common.enums import (
ConversationBranchMode,
Expand Down Expand Up @@ -99,6 +99,29 @@ class Image(Media):

media_type: ClassVar[MediaTypeT] = MediaType.IMAGE

uuids: list[str] = Field(
default_factory=list,
description="Optional cache UUIDs aligned 1:1 with `contents`. "
"UUID-only references normalize omitted contents to empty strings; "
"otherwise lengths must match. "
"vLLM-extension only: opaque IDs that let the server reuse a cached "
"processed image embedding across requests. Authored UUIDs pass through "
"on the chat endpoint regardless of automatic stripping.",
)

@model_validator(mode="after")
def _validate_uuid_alignment(self) -> "Image":
if self.uuids and not self.contents:
self.contents = [""] * len(self.uuids)
elif self.uuids and len(self.uuids) != len(self.contents):
raise ValueError(
f"Image.uuids length ({len(self.uuids)}) must match "
f"contents length ({len(self.contents)}) when set."
)
if any(uuid == "" for uuid in self.uuids):
raise ValueError("Image.uuids must not contain empty strings")
return self


class Audio(Media):
"""Media that contains audio data."""
Expand Down Expand Up @@ -254,8 +277,10 @@ def copy_with_stripped_media(self) -> "Turn":

This preserves text data (needed for tokenization) and raw messages/tools
(needed for API payload reconstruction) but replaces potentially large
image/audio/video contents with small placeholder strings. This is
more efficient than a full deep copy followed by stripping.
image/audio/video contents with small placeholder strings. Empty image
slots are preserved so cache-only UUID references remain distinguishable
from images whose content was present on the wire. This is more efficient
than a full deep copy followed by stripping.

Returns:
A new Turn with stripped multimodal contents and messages.
Expand All @@ -275,7 +300,11 @@ def copy_with_stripped_media(self) -> "Turn":
images=[
Image(
name=img.name,
contents=[f"image_{i}" for i in range(len(img.contents))],
contents=[
f"image_{i}" if content else ""
for i, content in enumerate(img.contents)
],
uuids=list(img.uuids),
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
)
for img in self.images
],
Expand Down
8 changes: 8 additions & 0 deletions src/aiperf/common/models/model_endpoint_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ def _redact_headers(self, value: list[tuple[str, str]]) -> list[tuple[str, str]]
description="Custom template configuration for template endpoints. "
"Provides the Jinja2 request body and JMESPath response_field used by TemplateEndpoint.",
)
uuid_and_strip: bool = Field(
default=EndpointDefaults.UUID_AND_STRIP,
description="Enable AIPerf-managed stripping of repeated image content. "
"Dataset-authored UUIDs pass through independently of this setting.",
)

@property
def base_url(self) -> str:
Expand Down Expand Up @@ -213,6 +218,9 @@ def from_run(cls, run: BenchmarkRun) -> ModelEndpointInfo:
collect_trace_chunks=False,
template=getattr(ep, "template", None),
session_header=getattr(ep, "session_header", None),
uuid_and_strip=getattr(
ep, "uuid_and_strip", EndpointDefaults.UUID_AND_STRIP
),
),
transport=ep.transport,
)
Expand Down
25 changes: 25 additions & 0 deletions src/aiperf/config/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class EndpointDefaults:
WAIT_FOR_MODEL_TIMEOUT = 0.0
WAIT_FOR_MODEL_INTERVAL = 5.0
WAIT_FOR_MODEL_MODE = "inference"
UUID_AND_STRIP = False


class TemplateConfig(BaseConfig):
Expand Down Expand Up @@ -316,6 +317,23 @@ def _redact_headers(self, value: dict[str, str]) -> dict[str, str]:
),
]

uuid_and_strip: Annotated[
bool,
Field(
default=EndpointDefaults.UUID_AND_STRIP,
description=(
"Enable AIPerf-managed image stripping for vLLM's multimodal processor "
"cache. Dataset-authored image UUIDs, including UUID-only cache "
"references, always pass through on the chat endpoint; this flag only "
"strips repeated content after AIPerf observes it in the same session. "
"Automatic stripping supports only single_turn datasets with "
"session_id-grouped rows; multi_turn is rejected. The server cache must "
"cover the working set, and requests in a session must reach a replica "
"that retains earlier UUIDs."
),
),
]

wait_for_model_timeout: Annotated[
float,
Field(
Expand Down Expand Up @@ -465,6 +483,13 @@ def _validate_template_required(self) -> Self:
raise ValueError("template is required when endpoint type is 'template'")
return self

@model_validator(mode="after")
def _validate_uuid_and_strip(self) -> Self:
"""Require image UUID reuse to use the Chat Completions endpoint."""
if self.uuid_and_strip and self.type != EndpointType.CHAT:
raise ValueError("--uuid-and-strip requires endpoint type 'chat'")
return self

@model_validator(mode="after")
def _validate_wait_for_model_coherent(self) -> Self:
"""Reject configurations where probe sub-options are set to non-default
Expand Down
1 change: 1 addition & 0 deletions src/aiperf/config/flags/_converter_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def _endpoint_template_fallback(endpoint: dict[str, Any]) -> None:
"download_video_content": "download_video_content",
"request_content_type": "request_content_type",
"session_header": "session_header",
"uuid_and_strip": "uuid_and_strip",
}


Expand Down
1 change: 1 addition & 0 deletions src/aiperf/config/flags/_section_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"urls",
"use_legacy_max_tokens",
"use_server_token_count",
"uuid_and_strip",
"wait_for_model_interval",
"wait_for_model_mode",
"wait_for_model_timeout",
Expand Down
20 changes: 20 additions & 0 deletions src/aiperf/config/flags/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,26 @@ class CLIConfig(BaseConfig):
),
] = None

uuid_and_strip: Annotated[
bool,
Field(
description=(
"Enable AIPerf-managed image stripping for vLLM's multimodal processor "
"cache. Dataset-authored image UUIDs, including UUID-only cache "
"references, always pass through on the chat endpoint; this flag only "
"strips repeated content after AIPerf observes it in the same session. "
"Automatic stripping supports only single_turn datasets with "
"session_id-grouped rows; multi_turn is rejected. The server cache must "
"cover the working set, and requests in a session must reach a replica "
"that retains earlier UUIDs."
),
),
CLIParameter(
name=("--uuid-and-strip",),
group=Groups.ENDPOINT,
),
] = EndpointDefaults.UUID_AND_STRIP

@property
def url(self) -> str:
"""Return the first URL for backward compatibility."""
Expand Down
6 changes: 6 additions & 0 deletions src/aiperf/config/schema/aiperf-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9286,6 +9286,12 @@
"description": "HTTP header name used to carry the per-session affinity identifier. When set, replaces the default `X-Correlation-ID` header. Useful when the inference server expects a custom session-affinity header (e.g. `--session-header X-Session-ID`).",
"title": "Sessionheader"
},
"uuidAndStrip": {
"default": false,
"description": "Enable AIPerf-managed image stripping for vLLM's multimodal processor cache. Dataset-authored image UUIDs, including UUID-only cache references, always pass through on the chat endpoint; this flag only strips repeated content after AIPerf observes it in the same session. Automatic stripping supports only single_turn datasets with session_id-grouped rows; multi_turn is rejected. The server cache must cover the working set, and requests in a session must reach a replica that retains earlier UUIDs.",
"title": "Uuidandstrip",
"type": "boolean"
},
"waitForModelTimeout": {
"default": 0.0,
"description": "Enable a pre-flight readiness probe by setting this to a positive value (seconds). aiperf applies this timeout to each URL/model probe before starting the benchmark, aborting with a non-zero exit if any probe times out. For multiple URLs or models, worst-case wall-clock time can be roughly this timeout multiplied by the number of URL/model probes. The probe strategy is controlled by `--wait-for-model-mode`, which defaults to sending a 1-token inference request. 0 (default) disables the probe. Eliminates the need for external shell-based readiness loops in containers and Kubernetes recipes.",
Expand Down
11 changes: 10 additions & 1 deletion src/aiperf/dataset/loader/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,19 @@ def _convert_to_media_objects(
if values is None or not isinstance(values, Iterable):
return []

# If already correct media objects, return as is
# If already correct media objects, return as is. Per-Image `uuids`
# (when supplied via the embedded-object form) ride along on the model.
if all(isinstance(v, media_class) for v in values):
return values

image_uuids = getattr(data, "image_uuids", None)
if field == MediaType.IMAGE and image_uuids:
contents = [
self._handle_media_content(v, media_type=MediaType.IMAGE) if v else ""
for v in values
]
return [media_class(name=name, contents=contents, uuids=list(image_uuids))]

# Handle media content (encode local files to base64)
if field in [MediaType.IMAGE, MediaType.VIDEO, MediaType.AUDIO]:
values = [
Expand Down
35 changes: 35 additions & 0 deletions src/aiperf/dataset/loader/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ class SingleTurn(AIPerfBaseModel):
None,
description="List of image strings or Image objects format",
)
image_uuids: list[str] | None = Field(
Comment thread
ajcasagrande marked this conversation as resolved.
None,
description="Optional cache UUIDs aligned 1:1 with string-form `images`. "
"When `images` is omitted, UUID-only references normalize to empty "
"image slots. "
"Only supported when `images` is `list[str]`; for `Image` objects, "
"set `Image.uuids` directly instead. The singular `image` field is "
"not supported. "
"vLLM-extension only: opaque IDs that let the server reuse cached "
"image embeddings across requests. Authored UUIDs pass through on the "
"chat endpoint; `--uuid-and-strip` additionally strips repeated content.",
)
audio: str | None = Field(None, description="Simple audio string content")
audios: list[str] | list[Audio] | None = Field(
None,
Expand Down Expand Up @@ -121,6 +133,29 @@ def validate_mutually_exclusive_fields(self) -> "SingleTurn":
raise ValueError("timestamp and delay cannot be set together")
return self

@model_validator(mode="after")
def validate_image_uuids_alignment(self) -> "SingleTurn":
"""Normalize UUID-only images and reject ambiguous UUID mappings."""
if self.image_uuids is None:
return self
if self.image is not None:
raise ValueError("image_uuids cannot be used with the singular image field")
if not self.images:
self.images = [""] * len(self.image_uuids)
if any(isinstance(img, Image) for img in self.images):
raise ValueError(
"image_uuids cannot be set when images is provided as Image "
"objects; use Image.uuids on each Image instead."
)
if len(self.image_uuids) != len(self.images):
raise ValueError(
f"image_uuids length ({len(self.image_uuids)}) must match "
f"images length ({len(self.images)})"
)
if any(uuid == "" for uuid in self.image_uuids):
raise ValueError("image_uuids must not contain empty strings")
return self
Comment thread
furionw marked this conversation as resolved.

@model_validator(mode="after")
def validate_at_least_one_modality(self) -> "SingleTurn":
"""Ensure at least one modality is provided"""
Expand Down
9 changes: 9 additions & 0 deletions src/aiperf/dataset/loader/multi_turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ def convert_to_conversations(
Returns:
A list of conversations.
"""
if self.run.cfg.endpoint.uuid_and_strip:
raise NotImplementedError(
"--uuid-and-strip is not supported with "
"--custom-dataset-type multi_turn. Load-time dedup of "
"repeated images is only implemented for the single_turn "
"loader. Use --custom-dataset-type single_turn (with "
"session_id-grouped rows) for cache-reuse benchmarks."
)
Comment thread
furionw marked this conversation as resolved.
Comment thread
ajcasagrande marked this conversation as resolved.

conversations = []
for session_id, multi_turns in data.items():
conversation = Conversation(session_id=session_id)
Expand Down
28 changes: 28 additions & 0 deletions src/aiperf/dataset/loader/single_turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,16 @@ def convert_to_conversations(
) -> list[Conversation]:
"""Convert single turn data to conversation objects.

When `endpoint.uuid_and_strip` is set, images repeated in later turns
of a conversation keep their UUID but drop their payload.

Args:
data: A dictionary mapping session_id to list of SingleTurn objects.

Returns:
A list of conversations.
"""
uuid_and_strip = self.run.cfg.endpoint.uuid_and_strip
conversations = []
for session_id, single_turns in data.items():
conversation = Conversation(
Expand All @@ -139,5 +143,29 @@ def convert_to_conversations(
extra_body=single_turn.extra,
)
)
if uuid_and_strip:
Comment thread
furionw marked this conversation as resolved.
self._dedup_repeated_images_inplace(conversation)
Comment thread
matthewkotila marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
conversations.append(conversation)
return conversations
Comment thread
furionw marked this conversation as resolved.

@staticmethod
def _dedup_repeated_images_inplace(conversation: Conversation) -> None:
"""Drop image bytes for repeated UUIDs within one conversation.

Images repeated within one turn retain their payload because the server
resolves that request's cache misses before populating its cache. Only
UUIDs whose content AIPerf observed in an earlier turn are stripped.
Explicit cache-only references pass through regardless of local history.
"""
seen: set[str] = set()
for turn in conversation.turns:
new_uuids: set[str] = set()
for image in turn.images:
if not image.uuids:
continue
for i, uuid in enumerate(image.uuids):
if uuid in seen:
image.contents[i] = ""
elif image.contents[i]:
new_uuids.add(uuid)
seen.update(new_uuids)
9 changes: 8 additions & 1 deletion src/aiperf/endpoints/base_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
BaseResponseData,
EmbeddingResponseData,
ExtractedPayload,
Image,
InferenceServerResponse,
Media,
ModelEndpointInfo,
Expand Down Expand Up @@ -224,11 +225,17 @@ def _render_turn_content(self, turn: Turn) -> str | list[dict[str, Any]]:

parts: list[dict[str, Any]] = []
self._extend_parts(parts, turn.texts, self._render_text_part)
self._extend_parts(parts, turn.images, self._render_image_part)
self._extend_image_parts(parts, turn.images)
self._extend_parts(parts, turn.audios, self._render_audio_part)
self._extend_parts(parts, turn.videos, self._render_video_part)
return parts

def _extend_image_parts(
self, parts: list[dict[str, Any]], images: list[Image]
) -> None:
"""Append rendered image parts for each non-empty content string."""
self._extend_parts(parts, images, self._render_image_part)

@staticmethod
def _extend_parts(
parts: list[dict[str, Any]],
Expand Down
Loading
Loading