From 2a9a272b3a79a8e00897013f94ca0fa7bec6975b Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Wed, 29 Jul 2026 10:16:51 -0500 Subject: [PATCH 1/4] feat: add deterministic agentic warmup budget Signed-off-by: Cam Quilici --- docs/cli-options.md | 10 + docs/tutorials/agentx-mvp.md | 10 + src/aiperf/config/config.py | 26 +- .../config/flags/_converter_profiling.py | 1 + src/aiperf/config/flags/cli_config.py | 16 + src/aiperf/config/phases.py | 14 + .../config/schema/aiperf-config.schema.json | 402 ++++++++++++++++++ src/aiperf/credit/issuer.py | 26 ++ src/aiperf/orchestrator/strategies.py | 1 + src/aiperf/timing/config.py | 30 +- src/aiperf/timing/phase/runner.py | 30 +- .../timing/strategies/agentic_replay.py | 79 ++++ .../test_agentic_replay_phase_override.py | 15 + tests/unit/config/test_validators.py | 22 + tests/unit/credit/test_issuer.py | 43 ++ tests/unit/orchestrator/test_strategies.py | 12 + ...est_runner_agentic_replay_warmup_target.py | 17 + .../timing/strategies/test_agentic_replay.py | 42 ++ .../test_phase_config_agentic_replay.py | 22 + 19 files changed, 794 insertions(+), 24 deletions(-) diff --git a/docs/cli-options.md b/docs/cli-options.md index 2b21c65073..0aa322032c 100644 --- a/docs/cli-options.md +++ b/docs/cli-options.md @@ -1128,6 +1128,11 @@ The maximum duration in seconds for the warmup phase. If not set, it will use th Additional agentic replay warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs, then drains and resumes profiling from the resulting trajectory state using each live stream's residual next-turn delay.
_Constraints: > 0_ +#### `--agentic-cache-warmup-requests-per-lane` `` + +Deterministic agentic cache-pressure warmup request budget per concurrency lane. For example, 10 with concurrency 16 caps warmup at 160 wire requests, including initial snapshot priming. Requires --agentic-cache-warmup-duration, which remains the safety deadline. +
_Constraints: > 0_ + #### `--agentic-warmup-grace-period` `` AGENTIC_REPLAY only: grace period in seconds the auto-synthesized warmup barrier waits for in-flight priming requests after the warmup burst sends. The agentic warmup is synthesized from the profiling phase rather than a user-declared warmup phase, so it does NOT honor `--warmup-grace-period` (which requires `--warmup-duration`). If not set, the warmup barrier waits indefinitely until every primed trajectory returns. @@ -2658,6 +2663,11 @@ The maximum duration in seconds for the warmup phase. If not set, it will use th Additional agentic replay warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs, then drains and resumes profiling from the resulting trajectory state using each live stream's residual next-turn delay.
_Constraints: > 0_ +#### `--agentic-cache-warmup-requests-per-lane` `` + +Deterministic agentic cache-pressure warmup request budget per concurrency lane. For example, 10 with concurrency 16 caps warmup at 160 wire requests, including initial snapshot priming. Requires --agentic-cache-warmup-duration, which remains the safety deadline. +
_Constraints: > 0_ + #### `--agentic-warmup-grace-period` `` AGENTIC_REPLAY only: grace period in seconds the auto-synthesized warmup barrier waits for in-flight priming requests after the warmup burst sends. The agentic warmup is synthesized from the profiling phase rather than a user-declared warmup phase, so it does NOT honor `--warmup-grace-period` (which requires `--warmup-duration`). If not set, the warmup barrier waits indefinitely until every primed trajectory returns. diff --git a/docs/tutorials/agentx-mvp.md b/docs/tutorials/agentx-mvp.md index 36a743e2da..1df5088f47 100644 --- a/docs/tutorials/agentx-mvp.md +++ b/docs/tutorials/agentx-mvp.md @@ -417,6 +417,16 @@ duration expires, it stops issuing new requests, drains requests already on the wire, snapshots each live root, subagent, and unresolved join, and starts profiling from that exact state. +For repeatable warmup depth, also set +`--agentic-cache-warmup-requests-per-lane REQUESTS`. Each concurrency lane is +then allowed exactly that many warmup wire requests, including its initial +snapshot-priming requests. For example, `--concurrency 16` with +`--agentic-cache-warmup-requests-per-lane 10` targets 160 warmup requests, +with a strict 10-request quota on every lane. The duration is still required as +a safety deadline: issuance stops at the per-lane quotas or the duration, +whichever comes first. If the duration wins, a slow run sends fewer than the +configured request budget. + These requests remain part of warmup, so they are excluded from exported request metrics. diff --git a/src/aiperf/config/config.py b/src/aiperf/config/config.py index d9c5bfd96b..239f95cdf8 100644 --- a/src/aiperf/config/config.py +++ b/src/aiperf/config/config.py @@ -609,7 +609,8 @@ def validate_cache_bust_compatibility(self) -> Self: def validate_agentic_cache_warmup(self) -> Self: """Restrict accelerated cache warmup to the agentic_replay timing mode. - ``--agentic-cache-warmup-duration`` is consumed solely by + ``--agentic-cache-warmup-duration`` and its optional deterministic + request budget are consumed solely by ``aiperf.timing.config._build_agentic_warmup_config``, which only runs when the profiling phases resolve to AGENTIC_REPLAY. On any other run the value is silently dropped, so an unguarded flag is a no-op the user @@ -630,19 +631,34 @@ def validate_agentic_cache_warmup(self) -> Self: from aiperf.timing.config import _is_agentic_replay profiling_phases = self.get_profiling_phases() - if not any( + has_duration = any( getattr(phase, "agentic_cache_warmup_duration", None) is not None for phase in profiling_phases - ): + ) + has_request_budget = any( + getattr(phase, "agentic_cache_warmup_requests_per_lane", None) is not None + for phase in profiling_phases + ) + if not has_duration and not has_request_budget: return self + if any( + getattr(phase, "agentic_cache_warmup_requests_per_lane", None) is not None + and getattr(phase, "agentic_cache_warmup_duration", None) is None + for phase in profiling_phases + ): + raise ValueError( + "--agentic-cache-warmup-requests-per-lane requires " + "--agentic-cache-warmup-duration as a safety deadline." + ) + if self.scenario is not None: from aiperf.common.scenario.registry import get_scenario scenario_timing_mode = get_scenario(self.scenario).timing_mode if scenario_timing_mode != TimingMode.AGENTIC_REPLAY: raise ValueError( - "--agentic-cache-warmup-duration requires the agentic_replay " + "agentic cache warmup requires the agentic_replay " f"timing mode; scenario {self.scenario!r} locks " f"timing_mode={scenario_timing_mode}." ) @@ -650,7 +666,7 @@ def validate_agentic_cache_warmup(self) -> Self: if not _is_agentic_replay(profiling_phases): raise ValueError( - "--agentic-cache-warmup-duration requires the agentic_replay " + "agentic cache warmup requires the agentic_replay " "timing mode (set today by --scenario inferencex-agentx-mvp); " "the profiling phase(s) are not agentic_replay." ) diff --git a/src/aiperf/config/flags/_converter_profiling.py b/src/aiperf/config/flags/_converter_profiling.py index c65a161d14..86e9daa9aa 100644 --- a/src/aiperf/config/flags/_converter_profiling.py +++ b/src/aiperf/config/flags/_converter_profiling.py @@ -54,6 +54,7 @@ "burst_phase_starts", "system_idle_gap_cap_seconds", "agentic_cache_warmup_duration", + "agentic_cache_warmup_requests_per_lane", "agentic_warmup_grace_period", ) diff --git a/src/aiperf/config/flags/cli_config.py b/src/aiperf/config/flags/cli_config.py index 847e77f508..b418312901 100644 --- a/src/aiperf/config/flags/cli_config.py +++ b/src/aiperf/config/flags/cli_config.py @@ -2288,6 +2288,22 @@ def url(self) -> str: ), ] = None + agentic_cache_warmup_requests_per_lane: Annotated[ + int | None, + Field( + gt=0, + description="Deterministic agentic cache-pressure warmup request " + "budget per concurrency lane. For example, 10 with concurrency 16 " + "caps warmup at 160 wire requests, including initial snapshot " + "priming. Requires --agentic-cache-warmup-duration, which remains " + "the safety deadline.", + ), + CLIParameter( + name=("--agentic-cache-warmup-requests-per-lane",), + group=Groups.WARMUP, + ), + ] = None + agentic_warmup_grace_period: Annotated[ float | None, Field( diff --git a/src/aiperf/config/phases.py b/src/aiperf/config/phases.py index ed547e6eef..337e738dd8 100644 --- a/src/aiperf/config/phases.py +++ b/src/aiperf/config/phases.py @@ -364,6 +364,20 @@ class BasePhaseConfig(AdaptiveScalePhaseMixin, BaseConfig): ), ] + agentic_cache_warmup_requests_per_lane: Annotated[ + int | None, + Field( + default=None, + gt=0, + description="AGENTIC_REPLAY only: deterministic cache-pressure " + "warmup request budget per concurrency lane. The total warmup " + "wire-request cap is this value multiplied by the number of live " + "trajectory lanes, including the initial snapshot-priming " + "requests. Requires agentic_cache_warmup_duration, which remains " + "the safety deadline; warmup stops when either limit is reached.", + ), + ] + agentic_warmup_grace_period: Annotated[ float | None, Field( diff --git a/src/aiperf/config/schema/aiperf-config.schema.json b/src/aiperf/config/schema/aiperf-config.schema.json index 816887b645..22bfc8ee7f 100644 --- a/src/aiperf/config/schema/aiperf-config.schema.json +++ b/src/aiperf/config/schema/aiperf-config.schema.json @@ -1316,6 +1316,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -1750,6 +1764,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -2252,6 +2280,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -2768,6 +2810,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -3270,6 +3326,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -3749,6 +3819,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -5102,6 +5186,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -5536,6 +5634,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -6038,6 +6150,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -6554,6 +6680,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -7056,6 +7196,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -7535,6 +7689,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -8009,6 +8177,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -8443,6 +8625,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -8945,6 +9141,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -9461,6 +9671,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -9963,6 +10187,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -10442,6 +10680,20 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane" + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -11337,6 +11589,31 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + }, + { + "type": "string", + "pattern": ".*\\{\\{.*\\}\\}.*", + "description": "Jinja2 template (e.g., '{{ variable }}')." + }, + { + "type": "string", + "pattern": ".*\\$\\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\\}.*", + "description": "Environment variable (e.g., '${VAR}' or '${VAR:default}')." + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane", + "x-jinja2-supported": true + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -12073,6 +12350,31 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + }, + { + "type": "string", + "pattern": ".*\\{\\{.*\\}\\}.*", + "description": "Jinja2 template (e.g., '{{ variable }}')." + }, + { + "type": "string", + "pattern": ".*\\$\\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\\}.*", + "description": "Environment variable (e.g., '${VAR}' or '${VAR:default}')." + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane", + "x-jinja2-supported": true + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -14064,6 +14366,31 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + }, + { + "type": "string", + "pattern": ".*\\{\\{.*\\}\\}.*", + "description": "Jinja2 template (e.g., '{{ variable }}')." + }, + { + "type": "string", + "pattern": ".*\\$\\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\\}.*", + "description": "Environment variable (e.g., '${VAR}' or '${VAR:default}')." + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane", + "x-jinja2-supported": true + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -14861,6 +15188,31 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + }, + { + "type": "string", + "pattern": ".*\\{\\{.*\\}\\}.*", + "description": "Jinja2 template (e.g., '{{ variable }}')." + }, + { + "type": "string", + "pattern": ".*\\$\\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\\}.*", + "description": "Environment variable (e.g., '${VAR}' or '${VAR:default}')." + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane", + "x-jinja2-supported": true + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -17455,6 +17807,31 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + }, + { + "type": "string", + "pattern": ".*\\{\\{.*\\}\\}.*", + "description": "Jinja2 template (e.g., '{{ variable }}')." + }, + { + "type": "string", + "pattern": ".*\\$\\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\\}.*", + "description": "Environment variable (e.g., '${VAR}' or '${VAR:default}')." + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane", + "x-jinja2-supported": true + }, "agenticWarmupGracePeriod": { "anyOf": [ { @@ -21059,6 +21436,31 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, + "agenticCacheWarmupRequestsPerLane": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + }, + { + "type": "string", + "pattern": ".*\\{\\{.*\\}\\}.*", + "description": "Jinja2 template (e.g., '{{ variable }}')." + }, + { + "type": "string", + "pattern": ".*\\$\\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\\}.*", + "description": "Environment variable (e.g., '${VAR}' or '${VAR:default}')." + } + ], + "default": null, + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "title": "Agenticcachewarmuprequestsperlane", + "x-jinja2-supported": true + }, "agenticWarmupGracePeriod": { "anyOf": [ { diff --git a/src/aiperf/credit/issuer.py b/src/aiperf/credit/issuer.py index 2023f25d9c..4a899239c3 100644 --- a/src/aiperf/credit/issuer.py +++ b/src/aiperf/credit/issuer.py @@ -15,6 +15,7 @@ from __future__ import annotations import time +from collections.abc import Callable from typing import TYPE_CHECKING from msgspec.structs import replace as _struct_replace @@ -125,8 +126,18 @@ def __init__( ) self._issuing_stopped = False self._max_tokens_override: int | None = None + self._turn_admission: Callable[[TurnToSend], bool] | None = None self.replay_gate = ReplayIssueGate(replay_barrier) + def set_turn_admission(self, callback: Callable[[TurnToSend], bool]) -> None: + """Install a synchronous final admission check for every turn.""" + self._turn_admission = callback + + def _is_turn_admitted(self, turn: TurnToSend) -> bool: + """Run the optional final admission check.""" + callback = getattr(self, "_turn_admission", None) + return callback is None or callback(turn) + def set_max_tokens_override(self, max_tokens: int | None) -> None: """Override generation length for every subsequently issued credit.""" self._max_tokens_override = max_tokens @@ -342,6 +353,12 @@ async def _issue_credit_ready(self, turn: TurnToSend) -> bool: self._concurrency_manager.release_session_slot(self._phase_key) return False + if not self._is_turn_admitted(turn): + self._concurrency_manager.release_prefill_slot(self._phase_key) + if needs_session_slot: + self._concurrency_manager.release_session_slot(self._phase_key) + return False + # Both slots held: register the tree before issuing so drain/teardown # own the slot release. Must not run before prefill succeeds. if needs_session_slot: @@ -400,6 +417,12 @@ async def try_issue_credit(self, turn: TurnToSend) -> bool | None: self._concurrency_manager.release_session_slot(self._phase_key) return None # No slot - credit not issued + if not self._is_turn_admitted(turn): + self._concurrency_manager.release_prefill_slot(self._phase_key) + if needs_session_slot: + self._concurrency_manager.release_session_slot(self._phase_key) + return False + if needs_session_slot: self._open_session_tree(turn) @@ -520,6 +543,9 @@ async def _dispatch_child_turn_ready(self, turn: TurnToSend) -> bool: self._phase_key, can_proceed_fn ): return False + if not self._is_turn_admitted(turn): + self._concurrency_manager.release_prefill_slot(self._phase_key) + return False if turn.counts_toward_phase_target: turn = _struct_replace(turn, counts_toward_phase_target=False) await self._issue_credit_internal(turn) diff --git a/src/aiperf/orchestrator/strategies.py b/src/aiperf/orchestrator/strategies.py index ada0694182..338d7fa92b 100644 --- a/src/aiperf/orchestrator/strategies.py +++ b/src/aiperf/orchestrator/strategies.py @@ -321,6 +321,7 @@ def _disable_warmup(self, config: BenchmarkConfig) -> BenchmarkConfig: config.phases = [p for p in config.phases if not p.exclude_from_results] for phase in config.get_profiling_phases(): phase.agentic_cache_warmup_duration = None + phase.agentic_cache_warmup_requests_per_lane = None return config diff --git a/src/aiperf/timing/config.py b/src/aiperf/timing/config.py index aaa85ed328..91e6fb6893 100644 --- a/src/aiperf/timing/config.py +++ b/src/aiperf/timing/config.py @@ -351,6 +351,12 @@ class CreditPhaseConfig(AIPerfBaseModel): description="Duration of the accelerated cache-pressure substage for " "agentic replay warmup.", ) + agentic_cache_warmup_requests_per_lane: int | None = Field( + default=None, + gt=0, + description="Deterministic cache-pressure warmup wire-request budget " + "per live agentic replay lane.", + ) artifact_dir: Path | None = Field( default=None, @@ -614,15 +620,26 @@ def _build_agentic_warmup_config(phase: PhaseConfig) -> CreditPhaseConfig | None concurrency = getattr(phase, "concurrency", None) grace_period = _agentic_warmup_grace_period(phase) cache_warmup_duration = getattr(phase, "agentic_cache_warmup_duration", None) + requests_per_lane = getattr(phase, "agentic_cache_warmup_requests_per_lane", None) + cache_warmup_request_cap = ( + concurrency * requests_per_lane + if concurrency is not None and requests_per_lane is not None + else None + ) + if cache_warmup_request_cap is not None: + total_expected_requests = cache_warmup_request_cap + elif cache_warmup_duration is not None: + total_expected_requests = None + else: + total_expected_requests = concurrency return CreditPhaseConfig( phase=CreditPhase.WARMUP, timing_mode=TimingMode.AGENTIC_REPLAY, - # An accelerated cache-pressure warmup is strategy-terminated (the - # strategy emits ``mark_sending_complete`` when the duration elapses), - # so leave the request cap open instead of sizing it to concurrency. - total_expected_requests=( - None if cache_warmup_duration is not None else concurrency - ), + # Without a deterministic budget, accelerated cache-pressure warmup is + # strategy-terminated when its duration elapses. With a budget, the + # generic request-count stop condition provides the exact wire cap and + # the duration remains a safety deadline. + total_expected_requests=total_expected_requests, expected_duration_sec=None, expected_num_sessions=None, concurrency=concurrency, @@ -633,6 +650,7 @@ def _build_agentic_warmup_config(phase: PhaseConfig) -> CreditPhaseConfig | None seamless=False, grace_period_sec=grace_period if grace_period is not None else float("inf"), agentic_cache_warmup_duration_sec=cache_warmup_duration, + agentic_cache_warmup_requests_per_lane=requests_per_lane, ) diff --git a/src/aiperf/timing/phase/runner.py b/src/aiperf/timing/phase/runner.py index b27de01b71..5a4e0e44a9 100644 --- a/src/aiperf/timing/phase/runner.py +++ b/src/aiperf/timing/phase/runner.py @@ -150,22 +150,26 @@ def __init__( elif ( config.timing_mode == TimingMode.AGENTIC_REPLAY and config.phase == CreditPhase.WARMUP - and not self._cache_warmup_enabled ): - # AGENTIC_REPLAY warmup dispatches one priming credit per warmable - # stream (root + each mid-flight subagent at t*), which exceeds the - # `concurrency` placeholder when lanes hold multiple streams. Without - # this re-anchor the concurrency-sized barrier fires early and cancels - # the closest-to-t* priming credits -- under-priming the server cache - # and masking warmup failures for the cancelled streams. Re-anchor the - # barrier to the actual dispatch count (``warmup_credit_count`` - # promises exactly this). Single-stream lanes already equal - # concurrency, so this is a no-op for them. - warmup_count = getattr(conversation_source, "warmup_credit_count", None) - if warmup_count: + requests_per_lane = getattr( + config, "agentic_cache_warmup_requests_per_lane", None + ) + if self._cache_warmup_enabled and requests_per_lane is not None: + lane_count = len(getattr(conversation_source, "trajectories", ())) + request_cap = requests_per_lane * lane_count self._config = config.model_copy( - update={"total_expected_requests": warmup_count} + update={"total_expected_requests": request_cap} ) + elif not self._cache_warmup_enabled: + # AGENTIC_REPLAY warmup dispatches one priming credit per + # warmable stream (root + each mid-flight subagent at t*), which + # exceeds the `concurrency` placeholder when lanes hold multiple + # streams. Re-anchor the barrier to the actual dispatch count. + warmup_count = getattr(conversation_source, "warmup_credit_count", None) + if warmup_count: + self._config = config.model_copy( + update={"total_expected_requests": warmup_count} + ) self._phase_publisher = phase_publisher self._credit_router = credit_router self._concurrency_manager = concurrency_manager diff --git a/src/aiperf/timing/strategies/agentic_replay.py b/src/aiperf/timing/strategies/agentic_replay.py index ccb11cd8cd..607b0a0ffc 100644 --- a/src/aiperf/timing/strategies/agentic_replay.py +++ b/src/aiperf/timing/strategies/agentic_replay.py @@ -179,6 +179,17 @@ def __init__( if isinstance(cache_warmup_duration, int | float) else None ) + cache_warmup_requests_per_lane = getattr( + config, "agentic_cache_warmup_requests_per_lane", None + ) + self._cache_warmup_requests_per_lane: int | None = ( + int(cache_warmup_requests_per_lane) + if isinstance(cache_warmup_requests_per_lane, int) + else None + ) + self._cache_warmup_requests_by_lane: Counter[int] = Counter() + self._cache_warmup_request_budget_reached = False + self._quota_handoff_starts: dict[str, TurnToSend] = {} self._baseline_warmup_returns: dict[str, Credit] = {} self._baseline_correlations: set[str] = set() self._accelerated_warmup_started = False @@ -398,6 +409,11 @@ async def setup_phase(self) -> None: """ if self._has_tree_registry: self._session_tree_registry.set_drain_callback(self._on_tree_drained) + if ( + self.config.phase == CreditPhase.WARMUP + and self._cache_warmup_requests_per_lane is not None + ): + self.credit_issuer.set_turn_admission(self._admit_cache_warmup_turn) if self.config.phase == CreditPhase.PROFILING: for trajectory in self.conversation_source.trajectories: self._seed_trajectory_replay_prefix(trajectory) @@ -422,6 +438,49 @@ async def setup_phase(self) -> None: f"fresh root once their background subagents drain" ) + def _cache_warmup_lane(self, turn_or_credit: TurnToSend | Credit) -> int: + """Resolve a warmup turn or credit to its stable trajectory lane.""" + lane = self._root_to_lane.get(turn_or_credit.effective_root_correlation_id) + if lane is None: + lane = self._correlation_to_lane.get(turn_or_credit.x_correlation_id) + if lane is None: + raise RuntimeError( + "Agentic cache warmup could not resolve a request to a " + f"trajectory lane: correlation_id={turn_or_credit.x_correlation_id!r}, " + "root_correlation_id=" + f"{turn_or_credit.effective_root_correlation_id!r}" + ) + return lane + + def _admit_cache_warmup_turn(self, turn: TurnToSend) -> bool: + """Atomically reserve one request from a lane's deterministic quota.""" + assert self._cache_warmup_requests_per_lane is not None + lane = self._cache_warmup_lane(turn) + if ( + self._cache_warmup_requests_by_lane[lane] + >= self._cache_warmup_requests_per_lane + ): + if turn.agent_depth == 0 and ( + turn.turn_index == 0 or turn.is_session_start + ): + self._quota_handoff_starts[turn.effective_root_correlation_id] = turn + return False + self._cache_warmup_requests_by_lane[lane] += 1 + if not self._cache_warmup_request_budget_reached and all( + self._cache_warmup_requests_by_lane[lane_index] + >= self._cache_warmup_requests_per_lane + for lane_index in range(len(self.conversation_source.trajectories)) + ): + self._cache_warmup_request_budget_reached = True + self.credit_issuer.replay_gate.pause_releases() + self.info( + "WARMUP cache pressure request budget reached: " + f"{self._cache_warmup_requests_per_lane} requests on each of " + f"{len(self.conversation_source.trajectories)} lanes; " + "draining requests" + ) + return True + async def execute_phase(self) -> None: """Dispatch initial credits for the phase.""" if self.config.phase == CreditPhase.WARMUP: @@ -591,6 +650,23 @@ async def _execute_warmup(self) -> None: self._baseline_correlations.add(turn.x_correlation_id) self._root_to_lane[turn.effective_root_correlation_id] = lane + if self._cache_warmup_requests_per_lane is not None: + baseline_counts = Counter( + self._cache_warmup_lane(turn) for turn, _ in prepared + ) + oversized = { + lane: count + for lane, count in baseline_counts.items() + if count > self._cache_warmup_requests_per_lane + } + if oversized: + raise ValueError( + "Agentic cache warmup requests-per-lane budget is smaller " + "than the required snapshot-priming dispatch count for " + f"lane(s) {oversized}; increase " + "--agentic-cache-warmup-requests-per-lane." + ) + # Nothing to warm: every lane's first request is at/after t* (no turn # precedes t*), so no warmup credit will dispatch. The count path that # normally drives completion is triggered by credit dispatch/return, so @@ -960,6 +1036,9 @@ def _pending_handoff_turns_by_root(self) -> dict[str, tuple[TurnToSend, ...]]: pending_by_root = dict(pending_by_root_getter()) else: pending_by_root = {} + for root_correlation_id, turn in self._quota_handoff_starts.items(): + pending_by_root.setdefault(root_correlation_id, ()) + pending_by_root[root_correlation_id] += (turn,) pending_turns_getter = getattr( self.credit_issuer.replay_gate, "pending_turns", None ) diff --git a/tests/unit/cli_runner/test_agentic_replay_phase_override.py b/tests/unit/cli_runner/test_agentic_replay_phase_override.py index 5b3c550854..8913a5e1fc 100644 --- a/tests/unit/cli_runner/test_agentic_replay_phase_override.py +++ b/tests/unit/cli_runner/test_agentic_replay_phase_override.py @@ -68,6 +68,21 @@ def test_agentic_cache_warmup_duration_overrides_yaml_profiling_phase( assert _profiling_phase(cfg).agentic_cache_warmup_duration == 30.0 +def test_agentic_cache_warmup_request_budget_overrides_yaml_profiling_phase( + tmp_path: pathlib.Path, +) -> None: + cfg = resolve_config( + _cli( + agentic_cache_warmup_duration=30.0, + agentic_cache_warmup_requests_per_lane=10, + ), + _agentic_yaml(tmp_path), + ) + phase = _profiling_phase(cfg) + assert phase.agentic_cache_warmup_duration == 30.0 + assert phase.agentic_cache_warmup_requests_per_lane == 10 + + def test_agentic_replay_sibling_flags_override_yaml_profiling_phase() -> None: """The four sibling agentic-replay phase flags overlay the profiling phase.""" cfg = resolve_config( diff --git a/tests/unit/config/test_validators.py b/tests/unit/config/test_validators.py index 70140fe8c1..2d534a88c7 100644 --- a/tests/unit/config/test_validators.py +++ b/tests/unit/config/test_validators.py @@ -263,6 +263,28 @@ def test_agentic_cache_warmup_with_explicit_agentic_timing_mode_accepted() -> No assert cfg.benchmark.phases[0].agentic_cache_warmup_duration == 30.0 +def test_agentic_cache_warmup_request_budget_requires_duration() -> None: + with pytest.raises(ValueError, match="requires.*duration"): + _make( + phases=_agentic_phase( + agentic_cache_warmup_requests_per_lane=10, + timing_mode="agentic_replay", + ) + ) + + +def test_agentic_cache_warmup_request_budget_with_duration_accepted() -> None: + cfg = _make( + phases=_agentic_phase( + agentic_cache_warmup_duration=30.0, + agentic_cache_warmup_requests_per_lane=10, + timing_mode="agentic_replay", + ) + ) + phase = cfg.benchmark.phases[0] + assert phase.agentic_cache_warmup_requests_per_lane == 10 + + def test_no_agentic_cache_warmup_duration_accepted() -> None: cfg = _make(phases=_agentic_phase()) assert cfg.benchmark.phases[0].agentic_cache_warmup_duration is None diff --git a/tests/unit/credit/test_issuer.py b/tests/unit/credit/test_issuer.py index d316d13e94..3e5de4ae4e 100644 --- a/tests/unit/credit/test_issuer.py +++ b/tests/unit/credit/test_issuer.py @@ -216,6 +216,49 @@ async def test_issue_credit_returns_false_when_final_credit( assert result is False + async def test_turn_admission_refusal_releases_acquired_slots( + self, credit_issuer, mock_concurrency, mock_progress, mock_router + ): + """A quota refusal happens after acquisition and must release both slots.""" + credit_issuer.set_turn_admission(lambda _turn: False) + + result = await credit_issuer.issue_credit(make_turn()) + + assert result is False + mock_concurrency.release_prefill_slot.assert_called_once_with( + CreditPhase.PROFILING + ) + mock_concurrency.release_session_slot.assert_called_once_with( + CreditPhase.PROFILING + ) + mock_progress.increment_sent.assert_not_called() + mock_router.send_credit.assert_not_called() + + async def test_child_turn_admission_refusal_releases_prefill_slot( + self, credit_issuer, mock_concurrency, mock_progress, mock_router + ): + """DAG children share the same quota gate without owning a session slot.""" + credit_issuer.set_turn_admission(lambda _turn: False) + child = TurnToSend( + conversation_id="child", + x_correlation_id="child-corr", + turn_index=0, + num_turns=1, + agent_depth=1, + parent_correlation_id="parent-corr", + root_correlation_id="root-corr", + ) + + result = await credit_issuer.dispatch_child_turn(child) + + assert result is False + mock_concurrency.release_prefill_slot.assert_called_once_with( + CreditPhase.PROFILING + ) + mock_concurrency.release_session_slot.assert_not_called() + mock_progress.increment_sent.assert_not_called() + mock_router.send_credit.assert_not_called() + # ============================================================================= # Test: Slot Acquisition Failures diff --git a/tests/unit/orchestrator/test_strategies.py b/tests/unit/orchestrator/test_strategies.py index 72dbf38b7f..4c968b8b94 100644 --- a/tests/unit/orchestrator/test_strategies.py +++ b/tests/unit/orchestrator/test_strategies.py @@ -312,6 +312,7 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): "concurrency": 1, "timing_mode": TimingMode.AGENTIC_REPLAY, "agentic_cache_warmup_duration": 30.0, + "agentic_cache_warmup_requests_per_lane": 10, }, ], ) @@ -321,6 +322,12 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): assert ( first_config.get_profiling_phases()[0].agentic_cache_warmup_duration == 30.0 ) + assert ( + first_config.get_profiling_phases()[ + 0 + ].agentic_cache_warmup_requests_per_lane + == 10 + ) results = [ RunResult( @@ -335,9 +342,14 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): second_config = strategy.get_next_config(config, results) for phase in second_config.get_profiling_phases(): assert phase.agentic_cache_warmup_duration is None + assert phase.agentic_cache_warmup_requests_per_lane is None # Original config untouched (deep copy). assert config.get_profiling_phases()[0].agentic_cache_warmup_duration == 30.0 + assert ( + config.get_profiling_phases()[0].agentic_cache_warmup_requests_per_lane + == 10 + ) def test_get_run_path(self): """Test get_run_path returns correct path structure.""" diff --git a/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py b/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py index cf11975dae..9e0ff9ff72 100644 --- a/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py +++ b/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py @@ -158,6 +158,23 @@ async def test_warmup_target_reanchored_to_warmup_credit_count(self) -> None: runner = _make_runner(_warmup_config(concurrency=2), src) assert runner._config.total_expected_requests == 6 + async def test_cache_warmup_target_uses_actual_lane_count(self) -> None: + """The placeholder is re-anchored to the actual wrap-filled trajectory lanes.""" + src = MagicMock() + src.dataset_metadata = None + src.trajectories = [MagicMock(), MagicMock(), MagicMock()] + config = _warmup_config(concurrency=4).model_copy( + update={ + "agentic_cache_warmup_duration_sec": 600.0, + "agentic_cache_warmup_requests_per_lane": 10, + "total_expected_requests": 40, + } + ) + + runner = _make_runner(config, src) + + assert runner._config.total_expected_requests == 30 + async def test_profiling_not_reanchored_to_warmup_count(self) -> None: """PROFILING must NOT be re-anchored to warmup_credit_count.""" src = MagicMock() diff --git a/tests/unit/timing/strategies/test_agentic_replay.py b/tests/unit/timing/strategies/test_agentic_replay.py index 9ff5530017..e84ed6e328 100644 --- a/tests/unit/timing/strategies/test_agentic_replay.py +++ b/tests/unit/timing/strategies/test_agentic_replay.py @@ -76,6 +76,7 @@ def _make_strategy( run: object | None = None, dataset: DatasetMetadata | None = None, cache_warmup_duration: float | None = None, + cache_warmup_requests_per_lane: int | None = None, progress: MagicMock | None = None, ) -> tuple[ AgenticReplayStrategy, AsyncMock, LoopScheduler | MagicMock, TrajectorySource @@ -87,6 +88,7 @@ def _make_strategy( cfg.phase = phase cfg.concurrency = len(trajectories) cfg.agentic_cache_warmup_duration_sec = cache_warmup_duration + cfg.agentic_cache_warmup_requests_per_lane = cache_warmup_requests_per_lane issuer = issuer if issuer is not None else AsyncMock() issuer.replay_gate = MagicMock() issuer.replay_gate.completed_prefixes.return_value = () @@ -284,6 +286,46 @@ async def test_cache_warmup_starts_after_baseline_and_removes_idle_delay(): assert scheduler.schedule_later.call_args.args[0] == 600.0 +@pytest.mark.asyncio +async def test_cache_warmup_request_budget_is_enforced_per_lane(): + trajectories = [ + Trajectory(conversation_id=f"trace_{i}", start_turn_index=0) for i in range(2) + ] + strategy, issuer, _, _ = _make_strategy( + phase=CreditPhase.WARMUP, + trajectories=trajectories, + cache_warmup_duration=600.0, + cache_warmup_requests_per_lane=2, + ) + issuer.set_turn_admission = MagicMock() + + await strategy.setup_phase() + + admission = issuer.set_turn_admission.call_args.args[0] + lane_0 = TurnToSend( + conversation_id="trace_0", + x_correlation_id=trajectories[0].x_correlation_id, + turn_index=0, + num_turns=4, + ) + lane_1 = TurnToSend( + conversation_id="trace_1", + x_correlation_id=trajectories[1].x_correlation_id, + turn_index=0, + num_turns=4, + ) + strategy._correlation_to_lane[lane_0.x_correlation_id] = 0 + strategy._correlation_to_lane[lane_1.x_correlation_id] = 1 + + assert admission(lane_0) is True + assert admission(lane_0) is True + assert admission(lane_0) is False + assert admission(lane_1) is True + assert admission(lane_1) is True + assert admission(lane_1) is False + issuer.replay_gate.pause_releases.assert_called_once_with() + + @pytest.mark.asyncio async def test_cache_warmup_cutoff_stops_issuer_and_persists_next_turn(): trajectory = Trajectory(conversation_id="trace_0", start_turn_index=1) diff --git a/tests/unit/timing/test_phase_config_agentic_replay.py b/tests/unit/timing/test_phase_config_agentic_replay.py index 6a5b6ef0e0..c16275cc14 100644 --- a/tests/unit/timing/test_phase_config_agentic_replay.py +++ b/tests/unit/timing/test_phase_config_agentic_replay.py @@ -148,6 +148,28 @@ def test_cache_warmup_uses_strategy_controlled_stop() -> None: assert warmup.grace_period_sec == 300.0 +def test_cache_warmup_request_budget_scales_with_concurrency() -> None: + """The deterministic budget becomes an exact global backstop while the strategy enforces the same quota independently on each lane.""" + phase = _PHASE_ADAPTER.validate_python( + { + "name": "profiling", + "type": "concurrency", + "concurrency": 16, + "duration": 900, + "timing_mode": TimingMode.AGENTIC_REPLAY, + "agentic_cache_warmup_duration": 600.0, + "agentic_cache_warmup_requests_per_lane": 10, + } + ) + + warmup = _build_agentic_warmup_config(phase) + + assert warmup is not None + assert warmup.total_expected_requests == 160 + assert warmup.agentic_cache_warmup_duration_sec == 600.0 + assert warmup.agentic_cache_warmup_requests_per_lane == 10 + + def test_cache_warmup_grace_uses_short_duration_without_benchmark_grace() -> None: phase = _PHASE_ADAPTER.validate_python( { From cba477c7f8e327c90d34896741f896a839f97d12 Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Wed, 29 Jul 2026 10:20:43 -0500 Subject: [PATCH 2/4] rename warmup per-lane option Signed-off-by: Cam Quilici --- docs/cli-options.md | 4 +- docs/tutorials/agentx-mvp.md | 4 +- src/aiperf/config/config.py | 6 +- .../config/flags/_converter_profiling.py | 2 +- src/aiperf/config/flags/cli_config.py | 4 +- src/aiperf/config/phases.py | 2 +- .../config/schema/aiperf-config.schema.json | 96 +++++++++---------- src/aiperf/orchestrator/strategies.py | 2 +- src/aiperf/timing/config.py | 6 +- src/aiperf/timing/phase/runner.py | 4 +- .../timing/strategies/agentic_replay.py | 4 +- .../test_agentic_replay_phase_override.py | 4 +- tests/unit/config/test_validators.py | 6 +- tests/unit/orchestrator/test_strategies.py | 16 +--- ...est_runner_agentic_replay_warmup_target.py | 2 +- .../timing/strategies/test_agentic_replay.py | 2 +- .../test_phase_config_agentic_replay.py | 4 +- 17 files changed, 79 insertions(+), 89 deletions(-) diff --git a/docs/cli-options.md b/docs/cli-options.md index 0aa322032c..a1ed59ae80 100644 --- a/docs/cli-options.md +++ b/docs/cli-options.md @@ -1128,7 +1128,7 @@ The maximum duration in seconds for the warmup phase. If not set, it will use th Additional agentic replay warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs, then drains and resumes profiling from the resulting trajectory state using each live stream's residual next-turn delay.
_Constraints: > 0_ -#### `--agentic-cache-warmup-requests-per-lane` `` +#### `--warmup-requests-per-lane` `` Deterministic agentic cache-pressure warmup request budget per concurrency lane. For example, 10 with concurrency 16 caps warmup at 160 wire requests, including initial snapshot priming. Requires --agentic-cache-warmup-duration, which remains the safety deadline.
_Constraints: > 0_ @@ -2663,7 +2663,7 @@ The maximum duration in seconds for the warmup phase. If not set, it will use th Additional agentic replay warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs, then drains and resumes profiling from the resulting trajectory state using each live stream's residual next-turn delay.
_Constraints: > 0_ -#### `--agentic-cache-warmup-requests-per-lane` `` +#### `--warmup-requests-per-lane` `` Deterministic agentic cache-pressure warmup request budget per concurrency lane. For example, 10 with concurrency 16 caps warmup at 160 wire requests, including initial snapshot priming. Requires --agentic-cache-warmup-duration, which remains the safety deadline.
_Constraints: > 0_ diff --git a/docs/tutorials/agentx-mvp.md b/docs/tutorials/agentx-mvp.md index 1df5088f47..6dec56f25d 100644 --- a/docs/tutorials/agentx-mvp.md +++ b/docs/tutorials/agentx-mvp.md @@ -418,10 +418,10 @@ the wire, snapshots each live root, subagent, and unresolved join, and starts profiling from that exact state. For repeatable warmup depth, also set -`--agentic-cache-warmup-requests-per-lane REQUESTS`. Each concurrency lane is +`--warmup-requests-per-lane REQUESTS`. Each concurrency lane is then allowed exactly that many warmup wire requests, including its initial snapshot-priming requests. For example, `--concurrency 16` with -`--agentic-cache-warmup-requests-per-lane 10` targets 160 warmup requests, +`--warmup-requests-per-lane 10` targets 160 warmup requests, with a strict 10-request quota on every lane. The duration is still required as a safety deadline: issuance stops at the per-lane quotas or the duration, whichever comes first. If the duration wins, a slow run sends fewer than the diff --git a/src/aiperf/config/config.py b/src/aiperf/config/config.py index 239f95cdf8..ce7e8aead0 100644 --- a/src/aiperf/config/config.py +++ b/src/aiperf/config/config.py @@ -636,19 +636,19 @@ def validate_agentic_cache_warmup(self) -> Self: for phase in profiling_phases ) has_request_budget = any( - getattr(phase, "agentic_cache_warmup_requests_per_lane", None) is not None + getattr(phase, "warmup_requests_per_lane", None) is not None for phase in profiling_phases ) if not has_duration and not has_request_budget: return self if any( - getattr(phase, "agentic_cache_warmup_requests_per_lane", None) is not None + getattr(phase, "warmup_requests_per_lane", None) is not None and getattr(phase, "agentic_cache_warmup_duration", None) is None for phase in profiling_phases ): raise ValueError( - "--agentic-cache-warmup-requests-per-lane requires " + "--warmup-requests-per-lane requires " "--agentic-cache-warmup-duration as a safety deadline." ) diff --git a/src/aiperf/config/flags/_converter_profiling.py b/src/aiperf/config/flags/_converter_profiling.py index 86e9daa9aa..6243eaf8a8 100644 --- a/src/aiperf/config/flags/_converter_profiling.py +++ b/src/aiperf/config/flags/_converter_profiling.py @@ -54,7 +54,7 @@ "burst_phase_starts", "system_idle_gap_cap_seconds", "agentic_cache_warmup_duration", - "agentic_cache_warmup_requests_per_lane", + "warmup_requests_per_lane", "agentic_warmup_grace_period", ) diff --git a/src/aiperf/config/flags/cli_config.py b/src/aiperf/config/flags/cli_config.py index b418312901..dfe360ce24 100644 --- a/src/aiperf/config/flags/cli_config.py +++ b/src/aiperf/config/flags/cli_config.py @@ -2288,7 +2288,7 @@ def url(self) -> str: ), ] = None - agentic_cache_warmup_requests_per_lane: Annotated[ + warmup_requests_per_lane: Annotated[ int | None, Field( gt=0, @@ -2299,7 +2299,7 @@ def url(self) -> str: "the safety deadline.", ), CLIParameter( - name=("--agentic-cache-warmup-requests-per-lane",), + name=("--warmup-requests-per-lane",), group=Groups.WARMUP, ), ] = None diff --git a/src/aiperf/config/phases.py b/src/aiperf/config/phases.py index 337e738dd8..3c063042de 100644 --- a/src/aiperf/config/phases.py +++ b/src/aiperf/config/phases.py @@ -364,7 +364,7 @@ class BasePhaseConfig(AdaptiveScalePhaseMixin, BaseConfig): ), ] - agentic_cache_warmup_requests_per_lane: Annotated[ + warmup_requests_per_lane: Annotated[ int | None, Field( default=None, diff --git a/src/aiperf/config/schema/aiperf-config.schema.json b/src/aiperf/config/schema/aiperf-config.schema.json index 22bfc8ee7f..4cf05bb814 100644 --- a/src/aiperf/config/schema/aiperf-config.schema.json +++ b/src/aiperf/config/schema/aiperf-config.schema.json @@ -1316,7 +1316,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -1328,7 +1328,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -1764,7 +1764,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -1776,7 +1776,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -2280,7 +2280,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -2292,7 +2292,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -2810,7 +2810,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -2822,7 +2822,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -3326,7 +3326,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -3338,7 +3338,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -3819,7 +3819,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -3831,7 +3831,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -5186,7 +5186,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -5198,7 +5198,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -5634,7 +5634,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -5646,7 +5646,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -6150,7 +6150,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -6162,7 +6162,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -6680,7 +6680,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -6692,7 +6692,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -7196,7 +7196,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -7208,7 +7208,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -7689,7 +7689,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -7701,7 +7701,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -8177,7 +8177,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -8189,7 +8189,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -8625,7 +8625,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -8637,7 +8637,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -9141,7 +9141,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -9153,7 +9153,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -9671,7 +9671,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -9683,7 +9683,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -10187,7 +10187,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -10199,7 +10199,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -10680,7 +10680,7 @@ "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", "title": "Agenticcachewarmupduration" }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -10692,7 +10692,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane" + "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { "anyOf": [ @@ -11589,7 +11589,7 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -11611,7 +11611,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane", + "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, "agenticWarmupGracePeriod": { @@ -12350,7 +12350,7 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -12372,7 +12372,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane", + "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, "agenticWarmupGracePeriod": { @@ -14366,7 +14366,7 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -14388,7 +14388,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane", + "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, "agenticWarmupGracePeriod": { @@ -15188,7 +15188,7 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -15210,7 +15210,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane", + "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, "agenticWarmupGracePeriod": { @@ -17807,7 +17807,7 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -17829,7 +17829,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane", + "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, "agenticWarmupGracePeriod": { @@ -21436,7 +21436,7 @@ "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, - "agenticCacheWarmupRequestsPerLane": { + "warmupRequestsPerLane": { "anyOf": [ { "exclusiveMinimum": 0, @@ -21458,7 +21458,7 @@ ], "default": null, "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", - "title": "Agenticcachewarmuprequestsperlane", + "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, "agenticWarmupGracePeriod": { diff --git a/src/aiperf/orchestrator/strategies.py b/src/aiperf/orchestrator/strategies.py index 338d7fa92b..1dae177069 100644 --- a/src/aiperf/orchestrator/strategies.py +++ b/src/aiperf/orchestrator/strategies.py @@ -321,7 +321,7 @@ def _disable_warmup(self, config: BenchmarkConfig) -> BenchmarkConfig: config.phases = [p for p in config.phases if not p.exclude_from_results] for phase in config.get_profiling_phases(): phase.agentic_cache_warmup_duration = None - phase.agentic_cache_warmup_requests_per_lane = None + phase.warmup_requests_per_lane = None return config diff --git a/src/aiperf/timing/config.py b/src/aiperf/timing/config.py index 91e6fb6893..7670e77b71 100644 --- a/src/aiperf/timing/config.py +++ b/src/aiperf/timing/config.py @@ -351,7 +351,7 @@ class CreditPhaseConfig(AIPerfBaseModel): description="Duration of the accelerated cache-pressure substage for " "agentic replay warmup.", ) - agentic_cache_warmup_requests_per_lane: int | None = Field( + warmup_requests_per_lane: int | None = Field( default=None, gt=0, description="Deterministic cache-pressure warmup wire-request budget " @@ -620,7 +620,7 @@ def _build_agentic_warmup_config(phase: PhaseConfig) -> CreditPhaseConfig | None concurrency = getattr(phase, "concurrency", None) grace_period = _agentic_warmup_grace_period(phase) cache_warmup_duration = getattr(phase, "agentic_cache_warmup_duration", None) - requests_per_lane = getattr(phase, "agentic_cache_warmup_requests_per_lane", None) + requests_per_lane = getattr(phase, "warmup_requests_per_lane", None) cache_warmup_request_cap = ( concurrency * requests_per_lane if concurrency is not None and requests_per_lane is not None @@ -650,7 +650,7 @@ def _build_agentic_warmup_config(phase: PhaseConfig) -> CreditPhaseConfig | None seamless=False, grace_period_sec=grace_period if grace_period is not None else float("inf"), agentic_cache_warmup_duration_sec=cache_warmup_duration, - agentic_cache_warmup_requests_per_lane=requests_per_lane, + warmup_requests_per_lane=requests_per_lane, ) diff --git a/src/aiperf/timing/phase/runner.py b/src/aiperf/timing/phase/runner.py index 5a4e0e44a9..a4c4966aa9 100644 --- a/src/aiperf/timing/phase/runner.py +++ b/src/aiperf/timing/phase/runner.py @@ -151,9 +151,7 @@ def __init__( config.timing_mode == TimingMode.AGENTIC_REPLAY and config.phase == CreditPhase.WARMUP ): - requests_per_lane = getattr( - config, "agentic_cache_warmup_requests_per_lane", None - ) + requests_per_lane = getattr(config, "warmup_requests_per_lane", None) if self._cache_warmup_enabled and requests_per_lane is not None: lane_count = len(getattr(conversation_source, "trajectories", ())) request_cap = requests_per_lane * lane_count diff --git a/src/aiperf/timing/strategies/agentic_replay.py b/src/aiperf/timing/strategies/agentic_replay.py index 607b0a0ffc..cf5200acdc 100644 --- a/src/aiperf/timing/strategies/agentic_replay.py +++ b/src/aiperf/timing/strategies/agentic_replay.py @@ -180,7 +180,7 @@ def __init__( else None ) cache_warmup_requests_per_lane = getattr( - config, "agentic_cache_warmup_requests_per_lane", None + config, "warmup_requests_per_lane", None ) self._cache_warmup_requests_per_lane: int | None = ( int(cache_warmup_requests_per_lane) @@ -664,7 +664,7 @@ async def _execute_warmup(self) -> None: "Agentic cache warmup requests-per-lane budget is smaller " "than the required snapshot-priming dispatch count for " f"lane(s) {oversized}; increase " - "--agentic-cache-warmup-requests-per-lane." + "--warmup-requests-per-lane." ) # Nothing to warm: every lane's first request is at/after t* (no turn diff --git a/tests/unit/cli_runner/test_agentic_replay_phase_override.py b/tests/unit/cli_runner/test_agentic_replay_phase_override.py index 8913a5e1fc..ab67503e17 100644 --- a/tests/unit/cli_runner/test_agentic_replay_phase_override.py +++ b/tests/unit/cli_runner/test_agentic_replay_phase_override.py @@ -74,13 +74,13 @@ def test_agentic_cache_warmup_request_budget_overrides_yaml_profiling_phase( cfg = resolve_config( _cli( agentic_cache_warmup_duration=30.0, - agentic_cache_warmup_requests_per_lane=10, + warmup_requests_per_lane=10, ), _agentic_yaml(tmp_path), ) phase = _profiling_phase(cfg) assert phase.agentic_cache_warmup_duration == 30.0 - assert phase.agentic_cache_warmup_requests_per_lane == 10 + assert phase.warmup_requests_per_lane == 10 def test_agentic_replay_sibling_flags_override_yaml_profiling_phase() -> None: diff --git a/tests/unit/config/test_validators.py b/tests/unit/config/test_validators.py index 2d534a88c7..280554128b 100644 --- a/tests/unit/config/test_validators.py +++ b/tests/unit/config/test_validators.py @@ -267,7 +267,7 @@ def test_agentic_cache_warmup_request_budget_requires_duration() -> None: with pytest.raises(ValueError, match="requires.*duration"): _make( phases=_agentic_phase( - agentic_cache_warmup_requests_per_lane=10, + warmup_requests_per_lane=10, timing_mode="agentic_replay", ) ) @@ -277,12 +277,12 @@ def test_agentic_cache_warmup_request_budget_with_duration_accepted() -> None: cfg = _make( phases=_agentic_phase( agentic_cache_warmup_duration=30.0, - agentic_cache_warmup_requests_per_lane=10, + warmup_requests_per_lane=10, timing_mode="agentic_replay", ) ) phase = cfg.benchmark.phases[0] - assert phase.agentic_cache_warmup_requests_per_lane == 10 + assert phase.warmup_requests_per_lane == 10 def test_no_agentic_cache_warmup_duration_accepted() -> None: diff --git a/tests/unit/orchestrator/test_strategies.py b/tests/unit/orchestrator/test_strategies.py index 4c968b8b94..1c60853893 100644 --- a/tests/unit/orchestrator/test_strategies.py +++ b/tests/unit/orchestrator/test_strategies.py @@ -312,7 +312,7 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): "concurrency": 1, "timing_mode": TimingMode.AGENTIC_REPLAY, "agentic_cache_warmup_duration": 30.0, - "agentic_cache_warmup_requests_per_lane": 10, + "warmup_requests_per_lane": 10, }, ], ) @@ -322,12 +322,7 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): assert ( first_config.get_profiling_phases()[0].agentic_cache_warmup_duration == 30.0 ) - assert ( - first_config.get_profiling_phases()[ - 0 - ].agentic_cache_warmup_requests_per_lane - == 10 - ) + assert first_config.get_profiling_phases()[0].warmup_requests_per_lane == 10 results = [ RunResult( @@ -342,14 +337,11 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): second_config = strategy.get_next_config(config, results) for phase in second_config.get_profiling_phases(): assert phase.agentic_cache_warmup_duration is None - assert phase.agentic_cache_warmup_requests_per_lane is None + assert phase.warmup_requests_per_lane is None # Original config untouched (deep copy). assert config.get_profiling_phases()[0].agentic_cache_warmup_duration == 30.0 - assert ( - config.get_profiling_phases()[0].agentic_cache_warmup_requests_per_lane - == 10 - ) + assert config.get_profiling_phases()[0].warmup_requests_per_lane == 10 def test_get_run_path(self): """Test get_run_path returns correct path structure.""" diff --git a/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py b/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py index 9e0ff9ff72..98b6a1d6e2 100644 --- a/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py +++ b/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py @@ -166,7 +166,7 @@ async def test_cache_warmup_target_uses_actual_lane_count(self) -> None: config = _warmup_config(concurrency=4).model_copy( update={ "agentic_cache_warmup_duration_sec": 600.0, - "agentic_cache_warmup_requests_per_lane": 10, + "warmup_requests_per_lane": 10, "total_expected_requests": 40, } ) diff --git a/tests/unit/timing/strategies/test_agentic_replay.py b/tests/unit/timing/strategies/test_agentic_replay.py index e84ed6e328..da0b615bdf 100644 --- a/tests/unit/timing/strategies/test_agentic_replay.py +++ b/tests/unit/timing/strategies/test_agentic_replay.py @@ -88,7 +88,7 @@ def _make_strategy( cfg.phase = phase cfg.concurrency = len(trajectories) cfg.agentic_cache_warmup_duration_sec = cache_warmup_duration - cfg.agentic_cache_warmup_requests_per_lane = cache_warmup_requests_per_lane + cfg.warmup_requests_per_lane = cache_warmup_requests_per_lane issuer = issuer if issuer is not None else AsyncMock() issuer.replay_gate = MagicMock() issuer.replay_gate.completed_prefixes.return_value = () diff --git a/tests/unit/timing/test_phase_config_agentic_replay.py b/tests/unit/timing/test_phase_config_agentic_replay.py index c16275cc14..41555a1a22 100644 --- a/tests/unit/timing/test_phase_config_agentic_replay.py +++ b/tests/unit/timing/test_phase_config_agentic_replay.py @@ -158,7 +158,7 @@ def test_cache_warmup_request_budget_scales_with_concurrency() -> None: "duration": 900, "timing_mode": TimingMode.AGENTIC_REPLAY, "agentic_cache_warmup_duration": 600.0, - "agentic_cache_warmup_requests_per_lane": 10, + "warmup_requests_per_lane": 10, } ) @@ -167,7 +167,7 @@ def test_cache_warmup_request_budget_scales_with_concurrency() -> None: assert warmup is not None assert warmup.total_expected_requests == 160 assert warmup.agentic_cache_warmup_duration_sec == 600.0 - assert warmup.agentic_cache_warmup_requests_per_lane == 10 + assert warmup.warmup_requests_per_lane == 10 def test_cache_warmup_grace_uses_short_duration_without_benchmark_grace() -> None: From 4437cbe37f60e63ce59dc53be253102f38747dc9 Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Wed, 29 Jul 2026 10:32:48 -0500 Subject: [PATCH 3/4] make agentic warmup limits exclusive Signed-off-by: Cam Quilici --- docs/cli-options.md | 8 +- docs/tutorials/agentx-mvp.md | 19 ++-- src/aiperf/config/config.py | 10 +- src/aiperf/config/flags/cli_config.py | 7 +- src/aiperf/config/phases.py | 6 +- .../config/schema/aiperf-config.schema.json | 96 +++++++++---------- src/aiperf/timing/config.py | 10 +- src/aiperf/timing/phase/runner.py | 9 +- .../timing/strategies/agentic_replay.py | 54 ++++++----- .../test_agentic_replay_cli_e2e.py | 12 ++- .../test_agentic_replay_phase_override.py | 7 +- tests/unit/config/test_validators.py | 27 +++--- tests/unit/orchestrator/test_strategies.py | 35 ++++++- ...est_runner_agentic_replay_warmup_target.py | 1 - .../timing/strategies/test_agentic_replay.py | 31 +++++- .../test_phase_config_agentic_replay.py | 4 +- 16 files changed, 205 insertions(+), 131 deletions(-) diff --git a/docs/cli-options.md b/docs/cli-options.md index a1ed59ae80..60b4bb83da 100644 --- a/docs/cli-options.md +++ b/docs/cli-options.md @@ -1125,12 +1125,12 @@ The maximum duration in seconds for the warmup phase. If not set, it will use th #### `--agentic-cache-warmup-duration` `` -Additional agentic replay warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs, then drains and resumes profiling from the resulting trajectory state using each live stream's residual next-turn delay. +Additional agentic replay warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs, then drains and resumes profiling from the resulting trajectory state using each live stream's residual next-turn delay. Mutually exclusive with --warmup-requests-per-lane.
_Constraints: > 0_ #### `--warmup-requests-per-lane` `` -Deterministic agentic cache-pressure warmup request budget per concurrency lane. For example, 10 with concurrency 16 caps warmup at 160 wire requests, including initial snapshot priming. Requires --agentic-cache-warmup-duration, which remains the safety deadline. +Deterministic agentic cache-pressure warmup request budget per concurrency lane. For example, 10 with concurrency 16 caps warmup at 160 wire requests, including initial snapshot priming. Mutually exclusive with --agentic-cache-warmup-duration.
_Constraints: > 0_ #### `--agentic-warmup-grace-period` `` @@ -2660,12 +2660,12 @@ The maximum duration in seconds for the warmup phase. If not set, it will use th #### `--agentic-cache-warmup-duration` `` -Additional agentic replay warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs, then drains and resumes profiling from the resulting trajectory state using each live stream's residual next-turn delay. +Additional agentic replay warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs, then drains and resumes profiling from the resulting trajectory state using each live stream's residual next-turn delay. Mutually exclusive with --warmup-requests-per-lane.
_Constraints: > 0_ #### `--warmup-requests-per-lane` `` -Deterministic agentic cache-pressure warmup request budget per concurrency lane. For example, 10 with concurrency 16 caps warmup at 160 wire requests, including initial snapshot priming. Requires --agentic-cache-warmup-duration, which remains the safety deadline. +Deterministic agentic cache-pressure warmup request budget per concurrency lane. For example, 10 with concurrency 16 caps warmup at 160 wire requests, including initial snapshot priming. Mutually exclusive with --agentic-cache-warmup-duration.
_Constraints: > 0_ #### `--agentic-warmup-grace-period` `` diff --git a/docs/tutorials/agentx-mvp.md b/docs/tutorials/agentx-mvp.md index 6dec56f25d..778452ce36 100644 --- a/docs/tutorials/agentx-mvp.md +++ b/docs/tutorials/agentx-mvp.md @@ -417,15 +417,16 @@ duration expires, it stops issuing new requests, drains requests already on the wire, snapshots each live root, subagent, and unresolved join, and starts profiling from that exact state. -For repeatable warmup depth, also set -`--warmup-requests-per-lane REQUESTS`. Each concurrency lane is -then allowed exactly that many warmup wire requests, including its initial -snapshot-priming requests. For example, `--concurrency 16` with -`--warmup-requests-per-lane 10` targets 160 warmup requests, -with a strict 10-request quota on every lane. The duration is still required as -a safety deadline: issuance stops at the per-lane quotas or the duration, -whichever comes first. If the duration wins, a slow run sends fewer than the -configured request budget. +For repeatable warmup depth, use +`--warmup-requests-per-lane REQUESTS` instead. Each concurrency lane is allowed +exactly that many warmup wire requests, including its initial snapshot-priming +requests. For example, `--concurrency 16` with +`--warmup-requests-per-lane 10` produces 160 warmup requests, with a strict +10-request quota on every lane. + +`--agentic-cache-warmup-duration` and `--warmup-requests-per-lane` are mutually +exclusive: choose a time-bounded warmup or a deterministic request-bounded +warmup. These requests remain part of warmup, so they are excluded from exported request metrics. diff --git a/src/aiperf/config/config.py b/src/aiperf/config/config.py index ce7e8aead0..331b847621 100644 --- a/src/aiperf/config/config.py +++ b/src/aiperf/config/config.py @@ -609,8 +609,8 @@ def validate_cache_bust_compatibility(self) -> Self: def validate_agentic_cache_warmup(self) -> Self: """Restrict accelerated cache warmup to the agentic_replay timing mode. - ``--agentic-cache-warmup-duration`` and its optional deterministic - request budget are consumed solely by + The mutually exclusive duration and deterministic request-budget modes + are consumed solely by ``aiperf.timing.config._build_agentic_warmup_config``, which only runs when the profiling phases resolve to AGENTIC_REPLAY. On any other run the value is silently dropped, so an unguarded flag is a no-op the user @@ -644,12 +644,12 @@ def validate_agentic_cache_warmup(self) -> Self: if any( getattr(phase, "warmup_requests_per_lane", None) is not None - and getattr(phase, "agentic_cache_warmup_duration", None) is None + and getattr(phase, "agentic_cache_warmup_duration", None) is not None for phase in profiling_phases ): raise ValueError( - "--warmup-requests-per-lane requires " - "--agentic-cache-warmup-duration as a safety deadline." + "--warmup-requests-per-lane and " + "--agentic-cache-warmup-duration are mutually exclusive." ) if self.scenario is not None: diff --git a/src/aiperf/config/flags/cli_config.py b/src/aiperf/config/flags/cli_config.py index dfe360ce24..ee4e0b78b8 100644 --- a/src/aiperf/config/flags/cli_config.py +++ b/src/aiperf/config/flags/cli_config.py @@ -2280,7 +2280,8 @@ def url(self) -> str: "After the normal snapshot warmup drains, AIPerf continues the live " "trajectories without recorded idle delays and with one-token outputs, " "then drains and resumes profiling from the resulting trajectory state " - "using each live stream's residual next-turn delay.", + "using each live stream's residual next-turn delay. Mutually exclusive " + "with --warmup-requests-per-lane.", ), CLIParameter( name=("--agentic-cache-warmup-duration",), @@ -2295,8 +2296,8 @@ def url(self) -> str: description="Deterministic agentic cache-pressure warmup request " "budget per concurrency lane. For example, 10 with concurrency 16 " "caps warmup at 160 wire requests, including initial snapshot " - "priming. Requires --agentic-cache-warmup-duration, which remains " - "the safety deadline.", + "priming. Mutually exclusive with " + "--agentic-cache-warmup-duration.", ), CLIParameter( name=("--warmup-requests-per-lane",), diff --git a/src/aiperf/config/phases.py b/src/aiperf/config/phases.py index 3c063042de..1633b59222 100644 --- a/src/aiperf/config/phases.py +++ b/src/aiperf/config/phases.py @@ -360,7 +360,8 @@ class BasePhaseConfig(AdaptiveScalePhaseMixin, BaseConfig): "continues the live trajectories without recorded idle delays and with " "one-token outputs for this long, then drains and resumes profiling " "from the resulting trajectory state. Read off the profiling phase by " - "``timing.config._build_agentic_warmup_config``. None disables it.", + "``timing.config._build_agentic_warmup_config``. Mutually exclusive " + "with warmup_requests_per_lane. None disables it.", ), ] @@ -373,8 +374,7 @@ class BasePhaseConfig(AdaptiveScalePhaseMixin, BaseConfig): "warmup request budget per concurrency lane. The total warmup " "wire-request cap is this value multiplied by the number of live " "trajectory lanes, including the initial snapshot-priming " - "requests. Requires agentic_cache_warmup_duration, which remains " - "the safety deadline; warmup stops when either limit is reached.", + "requests. Mutually exclusive with agentic_cache_warmup_duration.", ), ] diff --git a/src/aiperf/config/schema/aiperf-config.schema.json b/src/aiperf/config/schema/aiperf-config.schema.json index 4cf05bb814..5a4ae0eed7 100644 --- a/src/aiperf/config/schema/aiperf-config.schema.json +++ b/src/aiperf/config/schema/aiperf-config.schema.json @@ -1313,7 +1313,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -1327,7 +1327,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -1761,7 +1761,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -1775,7 +1775,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -2277,7 +2277,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -2291,7 +2291,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -2807,7 +2807,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -2821,7 +2821,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -3323,7 +3323,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -3337,7 +3337,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -3816,7 +3816,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -3830,7 +3830,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -5183,7 +5183,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -5197,7 +5197,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -5631,7 +5631,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -5645,7 +5645,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -6147,7 +6147,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -6161,7 +6161,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -6677,7 +6677,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -6691,7 +6691,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -7193,7 +7193,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -7207,7 +7207,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -7686,7 +7686,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -7700,7 +7700,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -8174,7 +8174,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -8188,7 +8188,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -8622,7 +8622,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -8636,7 +8636,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -9138,7 +9138,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -9152,7 +9152,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -9668,7 +9668,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -9682,7 +9682,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -10184,7 +10184,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -10198,7 +10198,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -10677,7 +10677,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration" }, "warmupRequestsPerLane": { @@ -10691,7 +10691,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane" }, "agenticWarmupGracePeriod": { @@ -11585,7 +11585,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, @@ -11610,7 +11610,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, @@ -12346,7 +12346,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, @@ -12371,7 +12371,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, @@ -14362,7 +14362,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, @@ -14387,7 +14387,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, @@ -15184,7 +15184,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, @@ -15209,7 +15209,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, @@ -17803,7 +17803,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, @@ -17828,7 +17828,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, @@ -21432,7 +21432,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. None disables it.", + "description": "AGENTIC_REPLAY only: additional cache-pressure warmup duration in seconds. After the normal snapshot warmup drains, AIPerf continues the live trajectories without recorded idle delays and with one-token outputs for this long, then drains and resumes profiling from the resulting trajectory state. Read off the profiling phase by ``timing.config._build_agentic_warmup_config``. Mutually exclusive with warmup_requests_per_lane. None disables it.", "title": "Agenticcachewarmupduration", "x-jinja2-supported": true }, @@ -21457,7 +21457,7 @@ } ], "default": null, - "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Requires agentic_cache_warmup_duration, which remains the safety deadline; warmup stops when either limit is reached.", + "description": "AGENTIC_REPLAY only: deterministic cache-pressure warmup request budget per concurrency lane. The total warmup wire-request cap is this value multiplied by the number of live trajectory lanes, including the initial snapshot-priming requests. Mutually exclusive with agentic_cache_warmup_duration.", "title": "Warmuprequestsperlane", "x-jinja2-supported": true }, diff --git a/src/aiperf/timing/config.py b/src/aiperf/timing/config.py index 7670e77b71..c0a982d7a9 100644 --- a/src/aiperf/timing/config.py +++ b/src/aiperf/timing/config.py @@ -355,7 +355,8 @@ class CreditPhaseConfig(AIPerfBaseModel): default=None, gt=0, description="Deterministic cache-pressure warmup wire-request budget " - "per live agentic replay lane.", + "per live agentic replay lane. Mutually exclusive with " + "agentic_cache_warmup_duration_sec.", ) artifact_dir: Path | None = Field( @@ -635,10 +636,9 @@ def _build_agentic_warmup_config(phase: PhaseConfig) -> CreditPhaseConfig | None return CreditPhaseConfig( phase=CreditPhase.WARMUP, timing_mode=TimingMode.AGENTIC_REPLAY, - # Without a deterministic budget, accelerated cache-pressure warmup is - # strategy-terminated when its duration elapses. With a budget, the - # generic request-count stop condition provides the exact wire cap and - # the duration remains a safety deadline. + # Duration mode is strategy-terminated by its timer. Count mode uses + # the generic request-count stop condition as a global backstop while + # the agentic strategy independently enforces each lane's quota. total_expected_requests=total_expected_requests, expected_duration_sec=None, expected_num_sessions=None, diff --git a/src/aiperf/timing/phase/runner.py b/src/aiperf/timing/phase/runner.py index a4c4966aa9..5d012657d0 100644 --- a/src/aiperf/timing/phase/runner.py +++ b/src/aiperf/timing/phase/runner.py @@ -131,9 +131,12 @@ def __init__( self._branch_orchestrator = branch_orchestrator self._run = run self._session_tree_registry = session_tree_registry - self._cache_warmup_enabled = isinstance( - getattr(config, "agentic_cache_warmup_duration_sec", None), - int | float, + self._cache_warmup_enabled = ( + isinstance( + getattr(config, "agentic_cache_warmup_duration_sec", None), + int | float, + ) + or getattr(config, "warmup_requests_per_lane", None) is not None ) # For FIXED_SCHEDULE mode, use actual dataset size instead of config values. diff --git a/src/aiperf/timing/strategies/agentic_replay.py b/src/aiperf/timing/strategies/agentic_replay.py index cf5200acdc..2e66ec3d08 100644 --- a/src/aiperf/timing/strategies/agentic_replay.py +++ b/src/aiperf/timing/strategies/agentic_replay.py @@ -286,6 +286,14 @@ def __init__( sum(self._lanes_per_trace.values()), ) + @property + def _cache_warmup_enabled(self) -> bool: + """Whether either accelerated cache-pressure warmup mode is active.""" + return ( + self._cache_warmup_duration is not None + or self._cache_warmup_requests_per_lane is not None + ) + @property def _has_tree_registry(self) -> bool: """True when per-tree session-slot accounting is engaged. @@ -294,17 +302,13 @@ def _has_tree_registry(self) -> bool: engages it because it opens trees and spawns descendants during WARMUP. """ return self._session_tree_registry is not None and ( - self.config.phase == CreditPhase.PROFILING - or self._cache_warmup_duration is not None + self.config.phase == CreditPhase.PROFILING or self._cache_warmup_enabled ) @property def wants_returns_after_sending_complete(self) -> bool: """Pressure warmup returns must be observed to build the handoff state.""" - return ( - self.config.phase == CreditPhase.WARMUP - and self._cache_warmup_duration is not None - ) + return self.config.phase == CreditPhase.WARMUP and self._cache_warmup_enabled @property def allows_pending_branch_handoff_after_sending_complete(self) -> bool: @@ -673,7 +677,7 @@ async def _execute_warmup(self) -> None: # with zero credits it would never fire and the warmup phase would hang # waiting on a barrier sized to concurrency. Finalize immediately. if not prepared: - if self._cache_warmup_duration is not None: + if self._cache_warmup_enabled: # No baseline priming to wait for; jump straight to the # accelerated cache-pressure substage. await self._start_accelerated_warmup() @@ -720,37 +724,41 @@ async def _execute_warmup(self) -> None: async def _finish_initial_warmup_dispatch(self) -> None: """Mark sending complete for burst warmup with no cache-pressure stage. - When a cache-pressure duration is set the accelerated substage is driven - by baseline credit returns (``_handle_warmup_return``), so there is - nothing to finalize here in that case. + When either cache-pressure mode is set, the accelerated substage is + driven by baseline credit returns (``_handle_warmup_return``), so there + is nothing to finalize here. """ - if ( - self._cache_warmup_duration is None - and not self.lifecycle.is_sending_complete - ): + if not self._cache_warmup_enabled and not self.lifecycle.is_sending_complete: self.lifecycle.mark_sending_complete() async def _start_accelerated_warmup(self) -> None: """Continue the sampled trajectories under compressed warmup traffic.""" if self._accelerated_warmup_started: return - assert self._cache_warmup_duration is not None + assert self._cache_warmup_enabled self._accelerated_warmup_started = True self.credit_issuer.set_max_tokens_override(_WARMUP_MAX_TOKENS) for trajectory in self.conversation_source.trajectories: self._seed_trajectory_replay_prefix(trajectory) self.credit_issuer.replay_gate.activate() + if self._cache_warmup_duration is not None: + limit = f"for {self._cache_warmup_duration:.1f}s" + else: + limit = ( + f"until each lane reaches " + f"{self._cache_warmup_requests_per_lane} requests" + ) self.info( - "WARMUP cache pressure: replaying live trajectories for " - f"{self._cache_warmup_duration:.1f}s with zero idle delay and " - f"max_tokens={_WARMUP_MAX_TOKENS}" + "WARMUP cache pressure: replaying live trajectories " + f"{limit} with zero idle delay and max_tokens={_WARMUP_MAX_TOKENS}" ) if self.branch_orchestrator is not None: self.branch_orchestrator.start_accelerated_warmup() - self.scheduler.schedule_later( - self._cache_warmup_duration, - self._finish_accelerated_warmup(), - ) + if self._cache_warmup_duration is not None: + self.scheduler.schedule_later( + self._cache_warmup_duration, + self._finish_accelerated_warmup(), + ) results = await asyncio.gather( *( self._dispatch_accelerated_trajectory(trajectory, lane) @@ -1435,7 +1443,7 @@ async def handle_credit_return( async def _handle_warmup_return(self, credit: Credit) -> None: """Advance baseline warmup into the optional cache-pressure stage.""" - if self._cache_warmup_duration is None: + if not self._cache_warmup_enabled: return if self._accelerated_warmup_started: await self._handle_accelerated_warmup_return(credit) diff --git a/tests/component_integration/test_agentic_replay_cli_e2e.py b/tests/component_integration/test_agentic_replay_cli_e2e.py index d92019be7c..d610aaa95c 100644 --- a/tests/component_integration/test_agentic_replay_cli_e2e.py +++ b/tests/component_integration/test_agentic_replay_cli_e2e.py @@ -189,13 +189,21 @@ def test_agentic_replay_cli_scenario_unsafe_override_runs_to_completion( @pytest.mark.component_integration +@pytest.mark.parametrize( + "warmup_option", + [ + "--agentic-cache-warmup-duration 2", + "--warmup-requests-per-lane 2", + ], +) def test_agentic_replay_cli_cache_warmup_runs_to_completion( cli: AIPerfCLI, weka_small_dir: Path, + warmup_option: str, ) -> None: - """E2E smoke that ``--agentic-cache-warmup-duration`` runs the warmup substage, drain, and profiling handoff without deadlocking the replay barriers.""" + """Both cache-pressure modes complete their warmup, drain, and handoff.""" cmd = _build_command(weka_small_dir, scenario=True, unsafe_override=True) - cmd += " --agentic-cache-warmup-duration 2" + cmd += f" {warmup_option}" result = cli.run_sync(cmd, timeout=defaults.timeout) assert result.exit_code == 0, ( diff --git a/tests/unit/cli_runner/test_agentic_replay_phase_override.py b/tests/unit/cli_runner/test_agentic_replay_phase_override.py index ab67503e17..cfecc46f20 100644 --- a/tests/unit/cli_runner/test_agentic_replay_phase_override.py +++ b/tests/unit/cli_runner/test_agentic_replay_phase_override.py @@ -72,14 +72,11 @@ def test_agentic_cache_warmup_request_budget_overrides_yaml_profiling_phase( tmp_path: pathlib.Path, ) -> None: cfg = resolve_config( - _cli( - agentic_cache_warmup_duration=30.0, - warmup_requests_per_lane=10, - ), + _cli(warmup_requests_per_lane=10), _agentic_yaml(tmp_path), ) phase = _profiling_phase(cfg) - assert phase.agentic_cache_warmup_duration == 30.0 + assert phase.agentic_cache_warmup_duration is None assert phase.warmup_requests_per_lane == 10 diff --git a/tests/unit/config/test_validators.py b/tests/unit/config/test_validators.py index 280554128b..40ec8efade 100644 --- a/tests/unit/config/test_validators.py +++ b/tests/unit/config/test_validators.py @@ -263,26 +263,25 @@ def test_agentic_cache_warmup_with_explicit_agentic_timing_mode_accepted() -> No assert cfg.benchmark.phases[0].agentic_cache_warmup_duration == 30.0 -def test_agentic_cache_warmup_request_budget_requires_duration() -> None: - with pytest.raises(ValueError, match="requires.*duration"): - _make( - phases=_agentic_phase( - warmup_requests_per_lane=10, - timing_mode="agentic_replay", - ) - ) - - -def test_agentic_cache_warmup_request_budget_with_duration_accepted() -> None: +def test_agentic_cache_warmup_request_budget_without_duration_accepted() -> None: cfg = _make( phases=_agentic_phase( - agentic_cache_warmup_duration=30.0, warmup_requests_per_lane=10, timing_mode="agentic_replay", ) ) - phase = cfg.benchmark.phases[0] - assert phase.warmup_requests_per_lane == 10 + assert cfg.benchmark.phases[0].warmup_requests_per_lane == 10 + + +def test_agentic_cache_warmup_modes_are_mutually_exclusive() -> None: + with pytest.raises(ValueError, match="mutually exclusive"): + _make( + phases=_agentic_phase( + agentic_cache_warmup_duration=30.0, + warmup_requests_per_lane=10, + timing_mode="agentic_replay", + ) + ) def test_no_agentic_cache_warmup_duration_accepted() -> None: diff --git a/tests/unit/orchestrator/test_strategies.py b/tests/unit/orchestrator/test_strategies.py index 1c60853893..c26cbaf130 100644 --- a/tests/unit/orchestrator/test_strategies.py +++ b/tests/unit/orchestrator/test_strategies.py @@ -312,7 +312,6 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): "concurrency": 1, "timing_mode": TimingMode.AGENTIC_REPLAY, "agentic_cache_warmup_duration": 30.0, - "warmup_requests_per_lane": 10, }, ], ) @@ -322,7 +321,6 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): assert ( first_config.get_profiling_phases()[0].agentic_cache_warmup_duration == 30.0 ) - assert first_config.get_profiling_phases()[0].warmup_requests_per_lane == 10 results = [ RunResult( @@ -337,10 +335,41 @@ def test_disable_warmup_clears_agentic_cache_warmup_duration(self): second_config = strategy.get_next_config(config, results) for phase in second_config.get_profiling_phases(): assert phase.agentic_cache_warmup_duration is None - assert phase.warmup_requests_per_lane is None # Original config untouched (deep copy). assert config.get_profiling_phases()[0].agentic_cache_warmup_duration == 30.0 + + def test_disable_warmup_clears_per_lane_request_budget(self): + from aiperf.plugin.enums import TimingMode + + strategy = FixedTrialsStrategy(num_trials=3, disable_warmup_after_first=True) + config = _make_config( + phases=[ + { + "name": "profiling", + "type": "concurrency", + "requests": 100, + "concurrency": 1, + "timing_mode": TimingMode.AGENTIC_REPLAY, + "warmup_requests_per_lane": 10, + }, + ], + ) + + first_config = strategy.get_next_config(config, []) + assert first_config.get_profiling_phases()[0].warmup_requests_per_lane == 10 + + results = [ + RunResult( + label="run_0001", + success=True, + summary_metrics={"ttft": JsonMetricResult(unit="ms", avg=100.0)}, + artifacts_path=Path("/tmp/run_0001"), + ) + ] + second_config = strategy.get_next_config(config, results) + + assert second_config.get_profiling_phases()[0].warmup_requests_per_lane is None assert config.get_profiling_phases()[0].warmup_requests_per_lane == 10 def test_get_run_path(self): diff --git a/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py b/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py index 98b6a1d6e2..24fb2ed124 100644 --- a/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py +++ b/tests/unit/timing/phase/test_runner_agentic_replay_warmup_target.py @@ -165,7 +165,6 @@ async def test_cache_warmup_target_uses_actual_lane_count(self) -> None: src.trajectories = [MagicMock(), MagicMock(), MagicMock()] config = _warmup_config(concurrency=4).model_copy( update={ - "agentic_cache_warmup_duration_sec": 600.0, "warmup_requests_per_lane": 10, "total_expected_requests": 40, } diff --git a/tests/unit/timing/strategies/test_agentic_replay.py b/tests/unit/timing/strategies/test_agentic_replay.py index da0b615bdf..df83cefc57 100644 --- a/tests/unit/timing/strategies/test_agentic_replay.py +++ b/tests/unit/timing/strategies/test_agentic_replay.py @@ -294,7 +294,6 @@ async def test_cache_warmup_request_budget_is_enforced_per_lane(): strategy, issuer, _, _ = _make_strategy( phase=CreditPhase.WARMUP, trajectories=trajectories, - cache_warmup_duration=600.0, cache_warmup_requests_per_lane=2, ) issuer.set_turn_admission = MagicMock() @@ -326,6 +325,36 @@ async def test_cache_warmup_request_budget_is_enforced_per_lane(): issuer.replay_gate.pause_releases.assert_called_once_with() +@pytest.mark.asyncio +async def test_count_cache_warmup_starts_without_duration_timer(): + trajectory = Trajectory(conversation_id="trace_0", start_turn_index=1) + strategy, issuer, scheduler, _ = _make_strategy( + phase=CreditPhase.WARMUP, + trajectories=[trajectory], + cache_warmup_requests_per_lane=3, + ) + issuer.set_turn_admission = MagicMock() + + await strategy.setup_phase() + await strategy.execute_phase() + baseline = issuer.issue_credit.await_args_list[0].args[0] + await strategy.handle_credit_return( + _make_credit( + conversation_id="trace_0", + x_correlation_id=baseline.x_correlation_id, + turn_index=1, + num_turns=4, + phase=CreditPhase.WARMUP, + ) + ) + + pressure = issuer.issue_credit.await_args_list[1].args[0] + assert pressure.turn_index == 2 + assert pressure.max_tokens_override == 1 + issuer.set_max_tokens_override.assert_called_once_with(1) + scheduler.schedule_later.assert_not_called() + + @pytest.mark.asyncio async def test_cache_warmup_cutoff_stops_issuer_and_persists_next_turn(): trajectory = Trajectory(conversation_id="trace_0", start_turn_index=1) diff --git a/tests/unit/timing/test_phase_config_agentic_replay.py b/tests/unit/timing/test_phase_config_agentic_replay.py index 41555a1a22..7baf752b74 100644 --- a/tests/unit/timing/test_phase_config_agentic_replay.py +++ b/tests/unit/timing/test_phase_config_agentic_replay.py @@ -157,7 +157,6 @@ def test_cache_warmup_request_budget_scales_with_concurrency() -> None: "concurrency": 16, "duration": 900, "timing_mode": TimingMode.AGENTIC_REPLAY, - "agentic_cache_warmup_duration": 600.0, "warmup_requests_per_lane": 10, } ) @@ -166,8 +165,9 @@ def test_cache_warmup_request_budget_scales_with_concurrency() -> None: assert warmup is not None assert warmup.total_expected_requests == 160 - assert warmup.agentic_cache_warmup_duration_sec == 600.0 + assert warmup.agentic_cache_warmup_duration_sec is None assert warmup.warmup_requests_per_lane == 10 + assert warmup.grace_period_sec == float("inf") def test_cache_warmup_grace_uses_short_duration_without_benchmark_grace() -> None: From 29749372a46386280f9d049f8cbd8762f894b9a3 Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Wed, 29 Jul 2026 11:22:24 -0500 Subject: [PATCH 4/4] fix: make agentic warmup handoff robust Signed-off-by: Cam Quilici --- src/aiperf/credit/callback_handler.py | 26 +++++----- src/aiperf/dataset/loader/weka_synth_buf.py | 17 +++++-- .../timing/strategies/agentic_replay.py | 48 +++++++++++++------ tests/unit/credit/test_callback_handler.py | 36 ++++++++++++++ .../dataset/loader/test_weka_pathological.py | 23 +++++++++ .../loader/test_weka_synth_buf_turn_delta.py | 30 ++++++++++++ .../timing/strategies/test_agentic_replay.py | 6 ++- .../test_agentic_replay_child_continuation.py | 24 ++++++++++ 8 files changed, 179 insertions(+), 31 deletions(-) diff --git a/src/aiperf/credit/callback_handler.py b/src/aiperf/credit/callback_handler.py index 972f7a5608..8b7053d31f 100644 --- a/src/aiperf/credit/callback_handler.py +++ b/src/aiperf/credit/callback_handler.py @@ -492,23 +492,27 @@ async def on_credit_return( # behind ``can_send_child_turn`` instead — the phase-level # sending-complete flag is driven by root sampling exhaustion, not # by DAG work, but the global ``--request-count`` cap still - # applies. When the cap blocks a non-final child continuation, we - # notify the orchestrator (``on_child_stopped``) so the parent's - # join still drains instead of deadlocking on a child whose - # remaining turns will never be issued. Final-turn child returns - # are always passed through (the strategy is a no-op for them, but - # observer hooks still need to fire). + # applies. In terminal phases, a blocked non-final child notifies the + # orchestrator (``on_child_stopped``) so the parent's join can drain. + # Strategies requesting stopped returns (such as quota warmup) instead + # preserve resumable children across a phase handoff. Final-turn child + # returns are always passed through (the strategy is a no-op for them, + # but observer hooks still need to fire). + wants_stopped_returns = ( + getattr(handler.strategy, "wants_returns_after_sending_complete", False) + is True + ) is_child = credit.agent_depth > 0 if not is_child: - wants_stopped_returns = ( - getattr(handler.strategy, "wants_returns_after_sending_complete", False) - is True - ) if handler.stop_checker.can_send_any_turn() or wants_stopped_returns: await handler.strategy.handle_credit_return( credit, error=credit_return.error ) - elif credit.is_final_turn or handler.stop_checker.can_send_child_turn(): + elif ( + credit.is_final_turn + or handler.stop_checker.can_send_child_turn() + or wants_stopped_returns + ): await handler.strategy.handle_credit_return( credit, error=credit_return.error ) diff --git a/src/aiperf/dataset/loader/weka_synth_buf.py b/src/aiperf/dataset/loader/weka_synth_buf.py index 1b7b49ba65..329a47d752 100644 --- a/src/aiperf/dataset/loader/weka_synth_buf.py +++ b/src/aiperf/dataset/loader/weka_synth_buf.py @@ -431,7 +431,7 @@ def _assert_trailing_user(self) -> None: def turn_delta(self) -> TurnDelta: """Compute the raw_messages to emit for the just-completed turn. - Three cases: + Four cases: 1. First call after ``init_turn_0`` (``_emitted_segment_count == 0``): emit ALL current segments, ``reset_context=False``. This is turn 0's baseline state. @@ -441,6 +441,10 @@ def turn_delta(self) -> TurnDelta: 3. Disturbance touched a previously-emitted segment (index ``< _emitted_segment_count``): emit ALL current segments, ``reset_context=True``. + 4. An equal-context retry appended no segments: re-emit ALL current + segments with ``reset_context=True``. An empty delta would + otherwise render as an invalid empty user message instead of the + recorded repeated request. Updates ``_emitted_segment_count`` to ``len(self._segments)`` on return. Clears ``_last_disturbance_at`` to ``None``. @@ -449,9 +453,16 @@ def turn_delta(self) -> TurnDelta: self._last_disturbance_at is not None and self._last_disturbance_at < self._emitted_segment_count ) - if self._emitted_segment_count == 0 or disturbed_emitted: + unchanged_retry = ( + self._emitted_segment_count > 0 + and self._emitted_segment_count == len(self._segments) + and not disturbed_emitted + ) + if self._emitted_segment_count == 0 or disturbed_emitted or unchanged_retry: source = self._segments - reset = self._emitted_segment_count != 0 and disturbed_emitted + reset = self._emitted_segment_count != 0 and ( + disturbed_emitted or unchanged_retry + ) else: source = self._segments[self._emitted_segment_count :] reset = False diff --git a/src/aiperf/timing/strategies/agentic_replay.py b/src/aiperf/timing/strategies/agentic_replay.py index 2e66ec3d08..bacc7b9927 100644 --- a/src/aiperf/timing/strategies/agentic_replay.py +++ b/src/aiperf/timing/strategies/agentic_replay.py @@ -254,8 +254,9 @@ def __init__( self.scheduler.set_drain_observer(self.enforce_system_idle_cap) # Idle-gap cap (ms) for the t* boundary the load-time warp can't see (t* # is the sampling instant, not a request). Consumed two ways: - # - WARMUP: clamps each warmup lead so priming doesn't start hours - # early (``_capped_warmup_lead_ms``); priming spacing is meaningless. + # - WARMUP: this cap and the global system-idle cap both clamp each + # warmup lead so priming doesn't start hours early + # (``_capped_warmup_lead_ms``); priming spacing is meaningless. # - PROFILING: a single uniform shift caps the leading idle (t* -> # earliest stream) while preserving recorded inter-stream spacing # (``_leading_idle_shift_ms``); a per-stream clamp would collapse @@ -548,9 +549,19 @@ def _capped_warmup_lead_ms(self, lead_ms: float) -> float: PROFILING dispatch offsets are NOT clamped this way -- see :meth:`_leading_idle_shift_ms`. """ - if self._phase_offset_cap_ms is not None: - return min(lead_ms, self._phase_offset_cap_ms) - return lead_ms + caps_ms = [ + cap + for cap in ( + self._phase_offset_cap_ms, + ( + self._system_idle_gap_cap_seconds * MILLIS_PER_SECOND + if self._system_idle_gap_cap_seconds is not None + else None + ), + ) + if cap is not None + ] + return min(lead_ms, *caps_ms) if caps_ms else lead_ms def _leading_idle_shift_ms(self, offsets: Iterable[float]) -> float: """Excess to subtract UNIFORMLY from every PROFILING dispatch offset so @@ -878,15 +889,21 @@ async def _handle_accelerated_warmup_return(self, credit: Credit) -> None: await self.credit_issuer.issue_credit(turn) async def _issue_child_continuation_or_drain(self, turn: TurnToSend) -> None: - """Dispatch a DAG child continuation, draining the join on refusal. + """Dispatch a DAG child continuation, draining terminal refusals. ``dispatch_child_turn`` returns True iff the turn reached the wire; on - any refusal (e.g. the ``--request-count`` cap) notify the orchestrator - so the parent's join drains deterministically instead of deadlocking on - a child whose remaining turns will never be issued. + a terminal refusal notify the orchestrator so the parent's join drains + deterministically instead of deadlocking on a child whose remaining + turns will never be issued. Accelerated warmup refusals are different: + the remaining child and its active join are persisted for profiling, so + marking the child stopped here would release the parent prematurely. """ on_wire = await self.credit_issuer.dispatch_child_turn(turn) - if not on_wire and self.branch_orchestrator is not None: + if ( + not on_wire + and self.branch_orchestrator is not None + and not self.allows_pending_branch_handoff_after_sending_complete + ): await self.branch_orchestrator.on_child_stopped(turn.x_correlation_id) async def finalize_phase(self) -> None: @@ -1460,11 +1477,12 @@ async def _dispatch_next_turn(self, credit: Credit) -> None: DAG child continuations (``agent_depth > 0``) go through the single child-issuance chokepoint (``_issue_child_continuation_or_drain``) so a - ``--request-count`` cap refusal is routed to ``on_child_stopped`` (drain - the parent join) instead of being silently swallowed by the discarded - ``issue_credit`` return -- including on the delayed (``delay_ms``) path, - where the refusal would otherwise fire long after the callback handler - decided the child could proceed. Root continuations keep ``issue_credit``. + terminal refusal is routed to ``on_child_stopped`` (drain the parent + join) instead of being silently swallowed by the discarded + ``issue_credit`` return. A warmup cutoff is preserved for profiling + handoff instead. This applies equally to delayed continuations, whose + refusal may happen after the callback handler decided the child could + proceed. Root continuations keep ``issue_credit``. """ next_meta = self.conversation_source.get_next_turn_metadata(credit) turn = TurnToSend.from_previous_credit(credit, next_meta) diff --git a/tests/unit/credit/test_callback_handler.py b/tests/unit/credit/test_callback_handler.py index 7aa13f1c08..eabd9b99ed 100644 --- a/tests/unit/credit/test_callback_handler.py +++ b/tests/unit/credit/test_callback_handler.py @@ -790,6 +790,42 @@ async def test_cache_warmup_handoff_allows_paused_dag_work( mock_branch_orchestrator.has_pending_branch_work.assert_called_once_with() +@pytest.mark.asyncio +async def test_cache_warmup_handoff_preserves_non_final_child( + callback_handler, + mock_progress, + mock_lifecycle, + mock_stop_checker, + mock_strategy, + mock_branch_orchestrator, +): + """A quota-stopped warmup child remains live for profiling handoff.""" + mock_stop_checker.can_send_child_turn = MagicMock(return_value=False) + mock_strategy.wants_returns_after_sending_complete = True + mock_branch_orchestrator.has_pending_branch_work = MagicMock(return_value=True) + mock_branch_orchestrator.intercept = AsyncMock(return_value=False) + mock_branch_orchestrator.on_child_stopped = AsyncMock() + callback_handler.set_branch_orchestrator(mock_branch_orchestrator) + callback_handler.register_phase( + phase=CreditPhase.WARMUP, + progress=mock_progress, + lifecycle=mock_lifecycle, + stop_checker=mock_stop_checker, + strategy=mock_strategy, + ) + + credit = make_credit( + phase=CreditPhase.WARMUP, + turn_index=1, + num_turns=7, + agent_depth=1, + ) + await callback_handler.on_credit_return("worker-1", make_credit_return(credit)) + + mock_strategy.handle_credit_return.assert_awaited_once_with(credit, error=None) + mock_branch_orchestrator.on_child_stopped.assert_not_awaited() + + # ============================================================================= # Test: Credit Return - Unregistered/Complete Phase # ============================================================================= diff --git a/tests/unit/dataset/loader/test_weka_pathological.py b/tests/unit/dataset/loader/test_weka_pathological.py index 93829601f2..dd295aae42 100644 --- a/tests/unit/dataset/loader/test_weka_pathological.py +++ b/tests/unit/dataset/loader/test_weka_pathological.py @@ -501,6 +501,29 @@ def test_duplicate_hash_ids_in_request_inflate_theoretical_hit_to_full(tmp_path) ) +def test_equal_context_retry_reemits_nonempty_full_prompt(tmp_path): + """An identical retry must not become an invalid empty user message.""" + trace = _base_trace( + [ + _normal(0.0, [1, 2], in_tokens=128), + _normal(1.0, [1, 2], in_tokens=128), + ], + trace_id="equal_retry", + ) + path = tmp_path / "t.json" + path.write_text(json.dumps(trace)) + loader = _make_loader(path, _mk_user_config()) + + convs = loader.convert_to_conversations(loader.load_dataset()) + first, retry = convs[0].turns + + assert first.raw_messages + assert retry.raw_messages == first.raw_messages + assert retry.reset_context is True + assert retry.raw_messages[-1]["role"] == "user" + assert retry.raw_messages[-1]["content"] + + def test_empty_requests_trace_reconstructs_empty_conversation(tmp_path): """A trace with zero requests yields a single empty Conversation, no crash.""" trace = _base_trace([], trace_id="empty_trace") diff --git a/tests/unit/dataset/loader/test_weka_synth_buf_turn_delta.py b/tests/unit/dataset/loader/test_weka_synth_buf_turn_delta.py index ec1a353399..0f52818109 100644 --- a/tests/unit/dataset/loader/test_weka_synth_buf_turn_delta.py +++ b/tests/unit/dataset/loader/test_weka_synth_buf_turn_delta.py @@ -119,6 +119,36 @@ def test_turn_delta_case_1_strict_append_emits_only_new_segments(): assert r._last_disturbance_at is None +def test_turn_delta_equal_context_retry_reemits_full_context(): + """An unchanged retry resets to the full prompt instead of emitting ``[]``.""" + r = _make_recon() + hash_ids = [1, 2] + in_tokens = 2 * BLOCK_SIZE + r.init_turn_0( + hash_ids=hash_ids, + in_tokens=in_tokens, + tool_tokens=0, + system_tokens=0, + seed="t:0", + ) + first = r.turn_delta() + + r.advance_turn( + prev_hash_ids=hash_ids, + prev_in_tokens=in_tokens, + prev_out_tokens=BLOCK_SIZE, + curr_hash_ids=hash_ids, + curr_in_tokens=in_tokens, + seed="t:1", + ) + retry = r.turn_delta() + + assert retry.reset_context is True + assert retry.delta_messages == first.delta_messages + assert retry.delta_messages + assert retry.delta_messages[-1]["role"] == "user" + + def test_turn_delta_case_1_strict_append_three_turns_chain(): """Three sequential strict-append advances: each delta is incremental.""" r = _make_recon() diff --git a/tests/unit/timing/strategies/test_agentic_replay.py b/tests/unit/timing/strategies/test_agentic_replay.py index df83cefc57..eecb656255 100644 --- a/tests/unit/timing/strategies/test_agentic_replay.py +++ b/tests/unit/timing/strategies/test_agentic_replay.py @@ -818,8 +818,10 @@ async def capture(turn): credit_issuer=issuer, lifecycle=lifecycle, ) - # Idle-gap cap of 60s (what the agentx scenario sets). - strategy._phase_offset_cap_ms = 60_000.0 + # The AgentX scenario sets the global system-idle cap, not the per-trace + # timestamp-warp cap. Warmup priming must honor that real configuration. + strategy._phase_offset_cap_ms = None + strategy._system_idle_gap_cap_seconds = 60.0 await strategy.setup_phase() await strategy.execute_phase() diff --git a/tests/unit/timing/strategies/test_agentic_replay_child_continuation.py b/tests/unit/timing/strategies/test_agentic_replay_child_continuation.py index e4c7bb45e2..039fb24905 100644 --- a/tests/unit/timing/strategies/test_agentic_replay_child_continuation.py +++ b/tests/unit/timing/strategies/test_agentic_replay_child_continuation.py @@ -19,9 +19,14 @@ def _make_strategy( branch_orchestrator: MagicMock | None = None, dispatch_result: bool = True, delay_ms: float | None = None, + phase: CreditPhase = CreditPhase.PROFILING, + accelerated_warmup: bool = False, ) -> tuple[AgenticReplayStrategy, MagicMock, MagicMock]: """Build a strategy with only the attributes ``_dispatch_next_turn`` reads.""" strategy = AgenticReplayStrategy.__new__(AgenticReplayStrategy) + strategy.config = MagicMock(phase=phase) + strategy._cache_warmup_duration = None + strategy._cache_warmup_requests_per_lane = 10 if accelerated_warmup else None conversation_source = MagicMock() conversation_source.get_next_turn_metadata.return_value = TurnMetadata( @@ -104,6 +109,25 @@ async def test_child_at_cap_routes_to_on_child_stopped() -> None: orch.on_child_stopped.assert_awaited_once_with("child-xcid") +@pytest.mark.asyncio +async def test_child_at_warmup_quota_is_preserved_for_profiling_handoff() -> None: + """A resumable warmup refusal must not prematurely satisfy the parent join.""" + orch = MagicMock() + orch.on_child_stopped = AsyncMock() + strategy, issuer, _ = _make_strategy( + branch_orchestrator=orch, + dispatch_result=False, + phase=CreditPhase.WARMUP, + accelerated_warmup=True, + ) + + await strategy._dispatch_next_turn(_child_credit()) + + issuer.dispatch_child_turn.assert_awaited_once() + issuer.issue_credit.assert_not_called() + orch.on_child_stopped.assert_not_called() + + @pytest.mark.asyncio async def test_child_at_cap_without_orchestrator_swallows_silently() -> None: """No orchestrator wired: refusal must not raise."""