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
6 changes: 5 additions & 1 deletion src/aiperf/common/messages/baseline_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from pydantic import Field

from aiperf.common.enums import BaselineKind, MessageType
from aiperf.common.enums import BaselineKind, CreditPhase, MessageType
from aiperf.common.messages.base_messages import Message
from aiperf.common.types import MessageTypeT, PhaseKind

Expand Down Expand Up @@ -34,6 +34,10 @@ class PhaseBaselineRequestMessage(Message):
phase_kind: PhaseKind | None = Field(
default=None, description="Semantic phase kind: warmup or profiling."
)
phase: CreditPhase | None = Field(
default=None,
description="Credit phase of the boundary; used when phase_kind is unset.",
)
kind: BaselineKind = Field(
..., description="START before credits are issued; END after returns drain."
)
14 changes: 9 additions & 5 deletions src/aiperf/server_metrics/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,15 @@ async def collect_baseline(self, message: PhaseBaselineRequestMessage) -> None:
"""Capture a one-shot server-metrics scrape for a phase boundary."""
if self._server_metrics_disabled or not self._collectors:
return
boundary_phase = (
CreditPhase.WARMUP
if message.phase_kind == "warmup"
else CreditPhase.PROFILING
)
# Tag the scrape with its own phase. Falling back to PROFILING for an
# unset kind put a warmup-start scrape into the profiling series, so
# counters that already existed then counted the whole warmup.
if message.phase_kind == "warmup":
boundary_phase = CreditPhase.WARMUP
elif message.phase_kind == "profiling":
boundary_phase = CreditPhase.PROFILING
else:
boundary_phase = message.phase or CreditPhase.PROFILING
errors: list[str] = []
for endpoint_url, collector in list(self._collectors.items()):
try:
Expand Down
3 changes: 3 additions & 0 deletions src/aiperf/timing/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,9 @@ def _build_agentic_warmup_config(phase: PhaseConfig) -> CreditPhaseConfig | None
total_expected_requests = concurrency
return CreditPhaseConfig(
phase=CreditPhase.WARMUP,
# Synthesized phases have no PhaseConfig.kind; consumers that key on
# phase_kind (server-metrics phase-boundary baselines) need it set.
phase_kind="warmup",
timing_mode=TimingMode.AGENTIC_REPLAY,
# Duration mode is strategy-terminated by its timer. Count mode uses
# the generic request-count stop condition as a global backstop while
Expand Down
1 change: 1 addition & 0 deletions src/aiperf/timing/phase/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ async def publish_phase_baseline_request(
phase_name=config.phase_name
or f"{config.phase.value}_{config.phase_index}",
phase_kind=config.phase_kind,
phase=config.phase,
kind=kind,
)
await self._pub_client.publish(msg)
Expand Down
49 changes: 48 additions & 1 deletion tests/unit/server_metrics/test_server_metrics_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
CreditPhaseStartMessage,
)
from aiperf.plugin.enums import EndpointType, TimingMode
from aiperf.server_metrics.manager import ServerMetricsManager
from aiperf.server_metrics.manager import (
_SERVER_METRICS_SCRAPE_PHASE,
ServerMetricsManager,
)
from aiperf.timing.config import CreditPhaseConfig
from tests.unit.conftest import make_run_from_cli

Expand Down Expand Up @@ -667,6 +670,50 @@ async def slow_scrape():

assert manager._active_phase == CreditPhase.WARMUP

@pytest.mark.asyncio
@pytest.mark.parametrize(
("phase_kind", "phase", "expected"),
[
("warmup", None, CreditPhase.WARMUP),
("profiling", None, CreditPhase.PROFILING),
# Synthesized phases (agentic auto-warmup) may carry no kind; the
# credit phase must decide, not a PROFILING default.
(None, CreditPhase.WARMUP, CreditPhase.WARMUP),
(None, CreditPhase.PROFILING, CreditPhase.PROFILING),
],
)
async def test_baseline_scrape_is_tagged_with_boundary_phase(
self,
cfg_with_endpoint: CLIConfig,
phase_kind,
phase,
expected,
):
manager = ServerMetricsManager(
run=make_run_from_cli(cfg_with_endpoint),
)
seen: list[CreditPhase | None] = []

async def scrape():
seen.append(_SERVER_METRICS_SCRAPE_PHASE.get())

mock_collector = MagicMock()
mock_collector.collect_and_process_metrics = AsyncMock(side_effect=scrape)
manager._collectors = {"http://localhost:8000/metrics": mock_collector}

await manager.collect_baseline(
PhaseBaselineRequestMessage(
service_id="timing-manager",
phase_id="phase-0",
phase_name="warmup_0",
phase_kind=phase_kind,
phase=phase,
kind=BaselineKind.START,
)
)

assert seen == [expected]


class TestDisabledServerMetrics:
"""Test server metrics disabled scenarios."""
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/timing/test_phase_config_agentic_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ def test_warmup_config_uses_agentic_replay_when_top_level_is_agentic_replay() ->
assert warmup is not None
assert warmup.timing_mode == TimingMode.AGENTIC_REPLAY
assert warmup.phase == CreditPhase.WARMUP
# Server-metrics phase-boundary baselines key on phase_kind; an unset kind
# tagged the warmup-start scrape as profiling.
assert warmup.phase_kind == "warmup"


def test_profiling_config_propagates_cap() -> None:
Expand Down
Loading