Skip to content

feat: enable multiple warmup and profiling phases in a single run#1150

Open
ilana-n wants to merge 6 commits into
mainfrom
ilana/aip-1004-1006-named-phase-kind-workflows
Open

feat: enable multiple warmup and profiling phases in a single run#1150
ilana-n wants to merge 6 commits into
mainfrom
ilana/aip-1004-1006-named-phase-kind-workflows

Conversation

@ilana-n

@ilana-n ilana-n commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Pull Request Summary

Adds named phase support to AIPerf so benchmark configs can model Rednote-style workflows with multiple warmup/profiling phases, phase-specific runtime metadata, cancellation behavior, adaptive behavior, and phase-scoped artifacts. This PR separates a phase’s workflow name from its semantic kind, threads phase identity through timing, credit, records, metrics, and export paths, and updates docs/tests for multi-phase targeting.

It also intentionally removes the unreleased adaptive-scale CLI surface. Adaptive scale is now a YAML-only phase feature because the CLI form becomes ambiguous once configs can contain multiple profiling/adaptive phases; YAML keeps the adaptive controller attached to the exact phase it controls.

Changes

  • Added PhaseKind = Literal["warmup", "profiling"] and separated phase name from phase semantic kind.
  • Added validation for unique phase names, reserved canonical names, strict path-safe identifiers, and concrete warmup/profiling kind assignment after normalization.
  • Updated timing config/runtime metadata to preserve phase_index, profiling_index, phase_name, and phase_kind.
  • Updated credit, callback, worker, and concurrency paths to key runtime behavior by concrete phase identity instead of collapsing all profiling phases together.
  • Added phase-aware cancellation and adaptive behavior plumbing for multi-phase workflows.
  • Updated records and metrics accumulator metadata so multiple profiling phases can be exported and summarized independently.
  • Added phase-aware export metadata through MetricRecordMetadata, ColumnStore, and ExportContext.
  • Updated metrics accumulator export masking to filter by phase_index before semantic phase, so two profiling-kind phases do not collapse into the same export.
  • Updated records manager summaries to pass concrete phase metadata into export contexts and generated metric outputs.
  • Added regression coverage proving multiple CreditPhase.PROFILING records with different phase_index values export separately.
  • Added phase-scoped adaptive scale artifacts under phases/<phase_name>/... plus an adaptive scale manifest.
  • Updated sweep/path resolution so explicit phase names and numeric phase indexes work, while legacy phases.profiling.* only resolves when unambiguous.
  • Updated YAML documentation with named phase examples, phase kind guidance, multi-phase sweep paths, phase-scoped adaptive artifacts, and phase-aware export behavior.
  • Removed unreleased adaptive-scale CLI flags and helper code:
    • --adaptive-scale
    • --adaptive-sustain-duration
    • --adaptive-assessment-period
    • --adaptive-scale-control
    • --adaptive-control-*
    • --adaptive-scale-sla
  • Updated adaptive-scale docs to describe adaptive scale as a YAML-only phase feature.
  • Updated tests to remove CLI-only adaptive coverage and keep YAML/model/runtime adaptive coverage.

Adaptive Scale CLI Removal

Adaptive scale was initially exposed through CLI flags for the simple one-phase case, but named phases make that interface ambiguous. A config can now have multiple profiling phases, and more than one of them may be adaptive. In that world, a command like --adaptive-sustain-duration 45 has no natural target unless we add another phase-selection mini-language to the CLI.

Rather than adding that complexity before release, this PR removes the adaptive-scale CLI surface entirely. Adaptive scale is more natural as YAML because the controller settings live directly on the phase they affect:

phases:
  - name: storm_1
    kind: profiling
    type: concurrency
    duration: 30m
    concurrency: 200
    adaptive_scale:
      enabled: true
      control:
        variable: concurrency
        min: 10
        max: 200
      assessment_period: 60
      sustain_duration: 10m
    sla:
      request_latency:
        p95:
          le: 30000

This avoids ambiguous targeting, keeps multi-phase configs readable, and leaves normal CLI overrides available for unambiguous non-adaptive loadgen knobs.

Export Behavior

This PR extends the export pipeline so phase identity survives beyond runtime execution. Metric records now carry concrete phase metadata, and the accumulator stores that metadata alongside metric columns. During export, ExportContext includes phase_index, phase_name, phase_kind, and profiling_index, allowing summaries and metric outputs to isolate a specific phase instead of grouping by semantic warmup/profiling alone.

That matters for named workflows where several phases are all kind: profiling. For example, low, storm, and recovery should not merge into one profiling export. The accumulator now prefers phase_index when present, then falls back to the legacy semantic phase filter for older/programmatic records.

Why This Is Important

Rednote-style workflows need more than a single canonical warmup plus profiling pair. They need ordered phases like setup, low-cancel, storm, recovery, and repeated profiling windows, each with distinct cancellation/adaptive behavior and separate runtime/result identity.

Without concrete phase metadata, several paths collapse all profiling work together: callback state can overwrite another profiling phase, records can lose which profiling phase produced a metric, and exports can merge distinct profiling windows into one result set. This PR makes phase identity explicit across config, runtime, records, metrics, artifacts, and export outputs.

The name / kind split keeps user workflows expressive while preserving the existing semantic distinction the credit and result pipelines actually need: warmup versus profiling.

How To Test

Run focused config and named phase tests:

uv run pytest tests/unit/config

Run adaptive scale strategy tests:

uv run pytest tests/unit/timing/strategies/test_adaptive_scale.py

Run records/metrics accumulator coverage:

uv run pytest \
  tests/unit/post_processors/test_metrics_accumulator.py \
  tests/unit/records/test_records_manager.py \
  tests/unit/records/test_dag_metadata_tagging.py

Run callback and timing race coverage:

uv run pytest \
  tests/unit/credit/test_callback_handler.py \
  tests/unit/timing/test_race_conditions.py

Run adaptive component integration with the marker enabled:

uv run pytest -m component_integration tests/component_integration/timing/test_adaptive_scale.py

Expected Output

  • Multi-phase runs preserve concrete phase metadata:

    • phase_index
    • profiling_index
    • phase_name
    • phase_kind
  • Metrics exports use concrete phase identity when available, so multiple profiling-kind phases export independently instead of merging into one profiling bucket.

  • Adaptive scale artifacts are phase-scoped:

phases/<phase_name>/adaptive_scale_events.jsonl
phases/<phase_name>/adaptive_scale_summary.json
adaptive_scale_manifest.json
  • Multiple profiling phases no longer overwrite each other in callback/concurrency/records/export paths.

Related Issues

Summary by CodeRabbit

  • New Features
    • Adaptive scaling is now YAML phase configuration only (adaptive-scale CLI options removed).
    • Adaptive scaling now writes phase-scoped event/summary artifacts plus an adaptive_scale_manifest.json.
    • Per-phase identity (index/name/kind) is preserved across exports and telemetry for phase-accurate filtering.
    • Added accuracy benchmarking support for mmlu_pro, with new --accuracy-enable-cot/--accuracy-no-enable-cot behavior.
    • Added FIXED_SCHEDULE_ONLY metric flag.
  • Bug Fixes
    • Improved correctness for multiple warmup/profiling phases with independent tracking and phase-specific concurrency behavior.
    • Stricter profiling-phase discovery/validation, including legacy kind inference and case-insensitive phase-name uniqueness.
  • Documentation
    • Updated adaptive-scale, YAML config, CLI/metrics, timing-mode references, and accuracy docs to reflect the YAML-only workflow and new artifact layout.

@copy-pr-bot

copy-pr-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Try out this PR

Quick install:

pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@cadd9242f8c23eb636f365a4f4de49ef84cc0cce

Recommended with virtual environment (using uv):

uv venv --python 3.12 && source .venv/bin/activate
uv pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@cadd9242f8c23eb636f365a4f4de49ef84cc0cce

Last updated for commit: cadd924Browse code

@github-actions github-actions Bot added the feat label Jul 14, 2026
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

The PR makes adaptive scale YAML-only, introduces explicit phase kind/name/index identity, supports repeated warmup and profiling phases, routes runtime state by phase instance, and adds MMLU-Pro accuracy benchmarking with phase-scoped artifacts and manifests.

Phase configuration and resolution

Layer / File(s) Summary
YAML phase configuration and resolution
docs/..., src/aiperf/config/..., tests/unit/config/...
Phase kinds, names, profiling selection, CLI overlays, sweep paths, and adaptive-scale configuration now use explicit YAML phase semantics.
Phase identity and runtime routing
src/aiperf/common/..., src/aiperf/credit/..., src/aiperf/timing/..., src/aiperf/workers/...
Phase metadata and runtime keys flow through timing configuration, credits, callbacks, concurrency controls, requests, and timing statistics.
Per-phase tracking, aggregation, and exports
src/aiperf/records/..., src/aiperf/metrics/..., src/aiperf/exporters/..., src/aiperf/gpu_telemetry/...
Completion, cancellation, metric filtering, GPU efficiency, per-phase summaries, and phase manifests now operate on phase instances.
Adaptive artifacts and accuracy benchmarking
src/aiperf/timing/strategies/..., src/aiperf/accuracy/..., src/aiperf/dataset/..., tests/...
Adaptive-scale artifacts use phase-scoped paths and manifests; MMLU-Pro loading, prompting, grading, accuracy metrics, and supporting dataset behavior are added or updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Poem

I’m a rabbit with phases to hop,
YAML guides every adaptive stop.
Names and indices keep trails bright,
Metrics bloom in folders right.
MMLU letters dance with glee—
A tidy benchmark burrow for me!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: support for multiple warmup and profiling phases in one run.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@datadog-official

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/aiperf/records/records_manager.py (2)

1852-1888: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope output_tokens_per_joule to the same phase window as energy. records_results is built without phase_index, so output_tokens_per_joule can use tokens aggregated across all profiling phases while _apply_gpu_efficiency_metrics() computes energy from only the final phase window. Thread a phase-scoped token source into the efficiency helper, or derive the numerator from phase_records_results instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/records/records_manager.py` around lines 1852 - 1888, Update
_apply_gpu_efficiency_metrics so its output_tokens_per_joule numerator is scoped
to the current phase, matching the energy window used for the final phase. Pass
a phase-indexed token source into the helper or derive tokens from
phase_records_results, and avoid using the unscoped records_results token total.

1155-1175: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mark the currently active profiling phase as cancelled. Hardcoding _final_results_phase_index leaves the interrupted phase’s tracker unmarked, so _summarize_per_phase_metric_records() can emit a per-phase artifact with was_cancelled=False for the phase that actually received Ctrl+C. Pass the active profiling phase_index here instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/records/records_manager.py` around lines 1155 - 1175, Update
_on_profile_cancel_command to use the currently active profiling phase_index
when calling mark_phase_cancelled and _process_results, instead of
_final_results_phase_index. Preserve the existing cancellation handling while
ensuring the interrupted phase is marked and summarized as cancelled.
🧹 Nitpick comments (8)
src/aiperf/credit/issuer.py (1)

96-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

_phase_key (phase-runtime-key) computation duplicated across credit/issuer.py and timing/phase/runner.py. Both independently compute phase_index if phase_index is not None else phase to key concurrency-manager and slot operations; the same pattern also appears (per graph evidence) in credit/callback_handler.py and records_manager.py. A single shared helper (e.g. phase_runtime_key(phase, phase_index) -> PhaseRuntimeKey) would remove the risk of these diverging as multi-phase concurrency isolation evolves — this key is central to correctness (wrong key = concurrency limits leaking across phases).

  • src/aiperf/credit/issuer.py#L96-L98: replace the inline _phase_key property body with a call to the shared helper.
  • src/aiperf/timing/phase/runner.py#L189-L195: replace the inline _phase_key property body with a call to the same shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/credit/issuer.py` around lines 96 - 98, The phase runtime-key
computation is duplicated across the credit issuer and phase runner. Add or
reuse a shared phase_runtime_key helper, then update _phase_key in
src/aiperf/credit/issuer.py at lines 96-98 and src/aiperf/timing/phase/runner.py
at lines 189-195 to delegate to it, preserving the phase_index-when-present,
otherwise phase behavior.
src/aiperf/config/phases.py (1)

255-269: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Legacy canonical-name → kind inference rule duplicated across config/phases.py and config/flags/resolver.py. Both implement the same "if kind is omitted and name is warmup/profiling, infer kind from name" rule independently (a third copy also exists in config/loader/normalizers.py::_infer_phase_kind, outside this review batch). Any future change to the canonical-name set or inference semantics would need to be kept in sync by hand across all copies.

  • src/aiperf/config/phases.py#L255-L269: extract the inference check (lines 258-259) into a shared helper (e.g. infer_legacy_phase_kind) usable by both the Pydantic validator and the raw-dict pre-merge pass.
  • src/aiperf/config/flags/resolver.py#L459-L487: replace the inline inference block (lines 470-473) with a call to the same shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/phases.py` around lines 255 - 269, Extract the
canonical-name inference logic from PhaseConfig._validate_phase_constraints into
a shared infer_legacy_phase_kind helper, preserving the existing
warmup/profiling behavior and None handling; update src/aiperf/config/phases.py
lines 255-269 to use it, and replace the duplicated inline inference in
src/aiperf/config/flags/resolver.py lines 459-487 with the same helper so both
validation and raw-dict pre-merge paths share one implementation.
src/aiperf/records/records_manager.py (1)

1690-1732: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Per-phase artifacts never receive GPU efficiency metrics.

_summarize_per_phase_metric_records builds PhaseProfileResults for every configured warmup/profiling phase entry but never calls _apply_gpu_efficiency_metrics for any of them — total_gpu_power/total_gpu_energy/output_tokens_per_joule/energy_per_user only ever land on the root records. For a feature explicitly designed to compare named phases with different concurrency/config (e.g. a low-vs-storm profiling comparison), missing per-phase GPU efficiency numbers limits the usefulness of the new phases/<phase_name>/ artifacts for GPU-telemetry users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/records/records_manager.py` around lines 1690 - 1732, Update
_summarize_per_phase_metric_records to apply _apply_gpu_efficiency_metrics to
each constructed PhaseProfileResults before appending or returning it, ensuring
total_gpu_power, total_gpu_energy, output_tokens_per_joule, and energy_per_user
are populated for every phase artifact as they are for the root records.
tests/unit/common/models/test_credit_models.py (1)

87-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the repository’s required param(..., id=...) parametrization style.

As per coding guidelines, use “the param(..., id=...) style with # fmt: skip on the closing parenthesis line.”

Proposed update
+from pytest import param
+
-    `@pytest.mark.parametrize`("field", ["phase_index", "profiling_index"])
+    `@pytest.mark.parametrize`(
+        "field",
+        [
+            param("phase_index", id="phase-index"),
+            param("profiling_index", id="profiling-index"),
+        ],
+    )  # fmt: skip
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/common/models/test_credit_models.py` around lines 87 - 90, Update
the test_phase_indexes_must_be_non_negative parametrization to use pytest.param
entries with explicit id= values, and add # fmt: skip to the closing parenthesis
line as required by repository style. Preserve both phase_index and
profiling_index cases and the existing validation assertion.

Source: Coding guidelines

tests/unit/credit/test_callback_handler.py (1)

165-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the test to include the function under test.

test_registers_same_kind_phases_by_runtime_index does not follow the required test_<function>_<scenario>_<expected> convention. Consider a name such as test_register_phase_same_kind_phases_by_runtime_index.

As per coding guidelines, tests must be named test_<function>_<scenario>_<expected>.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/credit/test_callback_handler.py` around lines 165 - 172, Rename
the test method test_registers_same_kind_phases_by_runtime_index to follow the
required test_<function>_<scenario>_<expected> convention, using the function
under test register_phase while preserving the existing scenario and expected
behavior.

Source: Coding guidelines

tests/unit/config/test_converter_profiling_phase_routes.py (1)

260-269: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark the test constant as immutable class data.

Ruff reports RUF012 for the mutable class attribute. Use ClassVar and frozenset.

+from typing import ClassVar
+
 class TestAdaptiveScaleCliRemoval:
-    REMOVED_FIELDS = {
+    REMOVED_FIELDS: ClassVar[frozenset[str]] = frozenset({
         ...
-    }
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/config/test_converter_profiling_phase_routes.py` around lines 260
- 269, Update the REMOVED_FIELDS class constant to be annotated with ClassVar
and initialized as a frozenset, preserving all existing field names while
satisfying Ruff RUF012.

Source: Linters/SAST tools

tests/unit/exporters/test_exporter_manager.py (1)

87-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add return annotations to the new tests. The changed test functions omit the required -> None annotation.

  • tests/unit/exporters/test_exporter_manager.py#L87-L89: add -> None to test_export_writes_phase_metric_artifacts.
  • tests/unit/records/test_dag_metadata_tagging.py#L29-L29,L109-L111,L126-L126,L172-L172: add -> None to all four new test methods.

As per coding guidelines, annotate all functions with parameter and return types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/exporters/test_exporter_manager.py` around lines 87 - 89, Add the
required -> None return annotation to test_export_writes_phase_metric_artifacts
in tests/unit/exporters/test_exporter_manager.py and to all four new test
methods in tests/unit/records/test_dag_metadata_tagging.py, preserving their
existing parameters and bodies.

Source: Coding guidelines

src/aiperf/common/models/record_models.py (1)

198-200: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use a shared PhaseKind type for phase_kind. These fields still accept arbitrary strings, so the warmup/profiling contract can drift across the record and timing models. Make phase_kind use one shared enum/typed alias everywhere it is serialized.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/common/models/record_models.py` around lines 198 - 200, Replace
the free-form str annotation on phase_kind in the record model with the shared
PhaseKind enum or typed alias, and update the corresponding phase_kind fields in
the timing models to use the same type wherever they are serialized. Preserve
the existing optional default of None and the warmup/profiling values.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aiperf/common/models/record_models.py`:
- Around line 275-277: Update the profiling_index Field declaration to enforce a
minimum value of zero by adding the same non-negative validation used by
MetricRecordMetadata and RecordContext, while preserving its optional None
default and existing description.

In `@src/aiperf/exporters/exporter_manager.py`:
- Line 104: Wrap the _export_phase_metric_artifacts call with handling for
expected filesystem/path errors from directory creation and manifest writes,
then continue into the deferred exporter loop when those errors occur. Apply the
same protection to the corresponding calls at the other referenced locations
without suppressing unrelated exceptions.

In `@src/aiperf/records/records_tracker.py`:
- Around line 291-315: Update was_phase_cancelled, mark_phase_cancelled, and
check_and_set_all_records_received_for_phase to resolve phase_index through the
same deterministic latest-phase resolver used by create_stats_for_phase before
calling _get_phase_tracker. Preserve explicitly supplied indices, and ensure
index-less named-phase calls target the existing latest indexed tracker rather
than creating an enum-keyed tracker.

In `@src/aiperf/timing/config.py`:
- Around line 87-131: Update TimingConfig.from_run so default_cancellation
always remains a fresh RequestCancellationConfig() and is not overwritten from
any profiling phase. Preserve phase-local cancellation handling through
_build_phase_config and ensure phases omitting cancellation inherit the clean
default configuration.

In `@src/aiperf/workers/worker.py`:
- Around line 735-738: Update the dataset-manager failure path’s fallback
RequestInfo constructor to propagate phase_index, profiling_index, phase_name,
and phase_kind from the current credit, matching the metadata fields used in the
surrounding RequestInfo construction. Preserve the existing fallback error
behavior while ensuring later named-phase retrieval errors retain their phase
identity.

In `@tests/component_integration/timing/test_adaptive_scale.py`:
- Around line 72-75: Quote the config_path interpolation in the cli.run_sync
command so paths containing spaces remain a single --config argument. Update the
command construction around cli.run_sync without changing the surrounding
profiling options.

In `@tests/unit/timing/strategies/test_adaptive_scale.py`:
- Line 109: Annotate the tmp_path parameters with Path in both
test_setup_phase_writes_phase_scoped_manifest and the function at
tests/unit/timing/strategies/test_adaptive_scale.py lines 132-132, preserving
their existing return annotations and behavior.

---

Outside diff comments:
In `@src/aiperf/records/records_manager.py`:
- Around line 1852-1888: Update _apply_gpu_efficiency_metrics so its
output_tokens_per_joule numerator is scoped to the current phase, matching the
energy window used for the final phase. Pass a phase-indexed token source into
the helper or derive tokens from phase_records_results, and avoid using the
unscoped records_results token total.
- Around line 1155-1175: Update _on_profile_cancel_command to use the currently
active profiling phase_index when calling mark_phase_cancelled and
_process_results, instead of _final_results_phase_index. Preserve the existing
cancellation handling while ensuring the interrupted phase is marked and
summarized as cancelled.

---

Nitpick comments:
In `@src/aiperf/common/models/record_models.py`:
- Around line 198-200: Replace the free-form str annotation on phase_kind in the
record model with the shared PhaseKind enum or typed alias, and update the
corresponding phase_kind fields in the timing models to use the same type
wherever they are serialized. Preserve the existing optional default of None and
the warmup/profiling values.

In `@src/aiperf/config/phases.py`:
- Around line 255-269: Extract the canonical-name inference logic from
PhaseConfig._validate_phase_constraints into a shared infer_legacy_phase_kind
helper, preserving the existing warmup/profiling behavior and None handling;
update src/aiperf/config/phases.py lines 255-269 to use it, and replace the
duplicated inline inference in src/aiperf/config/flags/resolver.py lines 459-487
with the same helper so both validation and raw-dict pre-merge paths share one
implementation.

In `@src/aiperf/credit/issuer.py`:
- Around line 96-98: The phase runtime-key computation is duplicated across the
credit issuer and phase runner. Add or reuse a shared phase_runtime_key helper,
then update _phase_key in src/aiperf/credit/issuer.py at lines 96-98 and
src/aiperf/timing/phase/runner.py at lines 189-195 to delegate to it, preserving
the phase_index-when-present, otherwise phase behavior.

In `@src/aiperf/records/records_manager.py`:
- Around line 1690-1732: Update _summarize_per_phase_metric_records to apply
_apply_gpu_efficiency_metrics to each constructed PhaseProfileResults before
appending or returning it, ensuring total_gpu_power, total_gpu_energy,
output_tokens_per_joule, and energy_per_user are populated for every phase
artifact as they are for the root records.

In `@tests/unit/common/models/test_credit_models.py`:
- Around line 87-90: Update the test_phase_indexes_must_be_non_negative
parametrization to use pytest.param entries with explicit id= values, and add #
fmt: skip to the closing parenthesis line as required by repository style.
Preserve both phase_index and profiling_index cases and the existing validation
assertion.

In `@tests/unit/config/test_converter_profiling_phase_routes.py`:
- Around line 260-269: Update the REMOVED_FIELDS class constant to be annotated
with ClassVar and initialized as a frozenset, preserving all existing field
names while satisfying Ruff RUF012.

In `@tests/unit/credit/test_callback_handler.py`:
- Around line 165-172: Rename the test method
test_registers_same_kind_phases_by_runtime_index to follow the required
test_<function>_<scenario>_<expected> convention, using the function under test
register_phase while preserving the existing scenario and expected behavior.

In `@tests/unit/exporters/test_exporter_manager.py`:
- Around line 87-89: Add the required -> None return annotation to
test_export_writes_phase_metric_artifacts in
tests/unit/exporters/test_exporter_manager.py and to all four new test methods
in tests/unit/records/test_dag_metadata_tagging.py, preserving their existing
parameters and bodies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f2b0a1f4-c9c2-48eb-8eea-a33f255ab796

📥 Commits

Reviewing files that changed from the base of the PR and between cd59084 and c1b1984.

📒 Files selected for processing (56)
  • docs/benchmark-modes/timing-modes-reference.md
  • docs/cli-options.md
  • docs/dev/patterns.md
  • docs/tutorials/adaptive-scale.md
  • docs/tutorials/yaml-config.md
  • src/aiperf/common/accumulator_protocols.py
  • src/aiperf/common/models/__init__.py
  • src/aiperf/common/models/credit_models.py
  • src/aiperf/common/models/record_models.py
  • src/aiperf/config/config.py
  • src/aiperf/config/flags/_adaptive_control_cli.py
  • src/aiperf/config/flags/_converter_profiling.py
  • src/aiperf/config/flags/_resolver_adaptive.py
  • src/aiperf/config/flags/_section_fields.py
  • src/aiperf/config/flags/cli_config.py
  • src/aiperf/config/flags/converter.py
  • src/aiperf/config/flags/resolver.py
  • src/aiperf/config/loader/helpers.py
  • src/aiperf/config/loader/normalizers.py
  • src/aiperf/config/phases.py
  • src/aiperf/config/sweep/expand.py
  • src/aiperf/credit/callback_handler.py
  • src/aiperf/credit/issuer.py
  • src/aiperf/credit/messages.py
  • src/aiperf/credit/structs.py
  • src/aiperf/exporters/exporter_manager.py
  • src/aiperf/gpu_telemetry/accumulator.py
  • src/aiperf/gpu_telemetry/protocols.py
  • src/aiperf/metrics/accumulator.py
  • src/aiperf/post_processors/otel_metrics_results_processor.py
  • src/aiperf/records/record_processor_service.py
  • src/aiperf/records/records_manager.py
  • src/aiperf/records/records_tracker.py
  • src/aiperf/timing/concurrency.py
  • src/aiperf/timing/config.py
  • src/aiperf/timing/phase/progress_tracker.py
  • src/aiperf/timing/phase/runner.py
  • src/aiperf/timing/phase_orchestrator.py
  • src/aiperf/timing/strategies/adaptive_scale.py
  • src/aiperf/timing/strategies/adaptive_scale_artifacts.py
  • src/aiperf/timing/strategies/adaptive_scale_backends.py
  • src/aiperf/timing/strategies/adaptive_scale_runtime.py
  • src/aiperf/workers/worker.py
  • tests/component_integration/timing/test_adaptive_scale.py
  • tests/unit/common/models/test_credit_models.py
  • tests/unit/config/test_converter_profiling_phase_routes.py
  • tests/unit/config/test_named_phase_kind.py
  • tests/unit/config/test_v1_resolver_streaming_override.py
  • tests/unit/credit/test_callback_handler.py
  • tests/unit/exporters/test_exporter_manager.py
  • tests/unit/gpu_telemetry/test_accumulator.py
  • tests/unit/post_processors/test_metrics_accumulator.py
  • tests/unit/records/test_dag_metadata_tagging.py
  • tests/unit/records/test_records_manager.py
  • tests/unit/timing/strategies/test_adaptive_scale.py
  • tests/unit/timing/test_race_conditions.py
💤 Files with no reviewable changes (7)
  • src/aiperf/config/flags/_adaptive_control_cli.py
  • src/aiperf/config/flags/_section_fields.py
  • src/aiperf/config/flags/_converter_profiling.py
  • src/aiperf/config/flags/cli_config.py
  • docs/cli-options.md
  • tests/unit/config/test_v1_resolver_streaming_override.py
  • src/aiperf/config/flags/_resolver_adaptive.py

Comment thread src/aiperf/common/models/record_models.py
Comment thread src/aiperf/exporters/exporter_manager.py Outdated
Comment thread src/aiperf/records/records_tracker.py
Comment thread src/aiperf/timing/config.py
Comment thread src/aiperf/workers/worker.py
Comment thread tests/component_integration/timing/test_adaptive_scale.py Outdated
Comment thread tests/unit/timing/strategies/test_adaptive_scale.py Outdated
@ilana-n ilana-n force-pushed the ilana/aip-1004-1006-named-phase-kind-workflows branch from ca81100 to 5865129 Compare July 14, 2026 23:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/aiperf/config/flags/_converter_profiling.py (1)

328-337: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unhandled AttributeError if a JSONL line is valid JSON but not an object.

load_json_str(stripped) only raises ValueError/TypeError on parse failure, but a well-formed JSON line that isn't an object (e.g. a bare array or number) will pass parsing and then crash on data.get("timestamp") since non-dict JSON values don't support .get(). This exception isn't caught by the surrounding except OSError, so it propagates and would abort CLI config resolution instead of just returning False.

🐛 Proposed fix
                 try:
                     data = load_json_str(stripped)
                 except (ValueError, TypeError):
                     return False
-                return data.get("timestamp") is not None
+                return isinstance(data, dict) and data.get("timestamp") is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/flags/_converter_profiling.py` around lines 328 - 337,
Update the JSONL validation logic around load_json_str so parsed values are
verified to be mapping/object data before calling data.get("timestamp"). Return
False for valid non-object JSON values, while preserving the existing behavior
for parse failures and timestamp checks.
src/aiperf/dataset/composer/custom.py (1)

127-150: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

UnicodeDecodeError needs to wrap the file read, not just JSON parsing.
for line in f: can raise UnicodeDecodeError before load_json_str() runs, and that exception is a ValueError, so the outer handler logs and re-raises instead of falling back to filename-based detection. Catch the decode/read step too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/dataset/composer/custom.py` around lines 127 - 150, Update the
file-reading logic in the dataset type inference method around the with open
block so UnicodeDecodeError raised while iterating through f is handled by the
filename-based fallback, not the outer ValueError handler. Wrap the line-reading
iteration together with load_json_str processing in the existing decode-error
handling, preserving the current behavior for valid JSON and other non-JSON
files.
src/aiperf/config/schema/aiperf-config.schema.json (1)

3358-3414: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Mirror the templated-string variants into the singular dataset: FileDataset schema. traceSessionSampleRatio, interTurnDelayCapSeconds, maxIdleGapCapSeconds, and replaySpeedup accept Jinja2/env-var strings in the canonical FileDataset copy, but the singular shorthand still exposes only numeric/null variants, so the same config shape is treated differently depending on whether it uses dataset: or datasets:.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/schema/aiperf-config.schema.json` around lines 3358 - 3414,
Update the singular dataset FileDataset schema definitions for
traceSessionSampleRatio, interTurnDelayCapSeconds, maxIdleGapCapSeconds, and
replaySpeedup to match the templated-string variants already present in the
canonical FileDataset schema. Preserve the existing numeric/null validation and
defaults while adding the corresponding Jinja2/env-var string alternatives so
dataset: and datasets: accept the same config shapes.
🧹 Nitpick comments (5)
src/aiperf/post_processors/metric_results_processor.py (1)

57-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

setdefault default is always constructed and discarded.

_instances_map is already populated for every registry tag before this comprehension runs (lines 50-52), and _setup_metrics(MetricType.DERIVED) only returns tags that are a subset of MetricRegistry.all_tags(). So metric.tag always already exists in _instances_map, meaning type(metric)() is evaluated and thrown away on every derived metric, every time a processor is constructed — setdefault's default argument is evaluated eagerly regardless of whether the key exists.

♻️ Proposed simplification
         self.derive_funcs: dict[
             MetricTagT, Callable[[MetricResultsDict], MetricValueTypeT]
         ] = {
-            metric.tag: self._instances_map.setdefault(
-                metric.tag, type(metric)()
-            ).derive_value  # type: ignore
+            metric.tag: self._instances_map[metric.tag].derive_value  # type: ignore
             for metric in self._setup_metrics(MetricType.DERIVED)
             if metric.type == MetricType.DERIVED
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/post_processors/metric_results_processor.py` around lines 57 - 65,
Update the derived-metric comprehension initializing self.derive_funcs to
retrieve each existing instance directly from _instances_map by metric.tag
instead of calling setdefault with type(metric)(). Preserve the existing
derive_value callback mapping and filtering for MetricType.DERIVED.
tests/unit/post_processors/test_derived_metric_instance_scope.py (1)

24-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate fixed_schedule_run fixture across two test files. Both files independently define the same BenchmarkRun construction for a single fixed-schedule profiling phase; the shared root cause is a missing common fixture for this scenario.

  • tests/unit/post_processors/test_derived_metric_instance_scope.py#L24-L46: keep this definition (or move it) and import it as the shared fixture.
  • tests/unit/post_processors/test_base_metrics_processor.py#L349-L374: remove this local copy and reuse the shared fixture (e.g. via tests/unit/post_processors/conftest.py) instead of redefining it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/post_processors/test_derived_metric_instance_scope.py` around
lines 24 - 46, The fixed_schedule_run fixture is duplicated across both test
modules. In
tests/unit/post_processors/test_derived_metric_instance_scope.py#L24-L46, retain
or move the existing fixture into tests/unit/post_processors/conftest.py as the
shared fixture; in
tests/unit/post_processors/test_base_metrics_processor.py#L349-L374, remove the
local definition and reuse the shared fixed_schedule_run fixture.
tests/unit/dataset/loader/test_baseten_offline_suite.py (1)

164-178: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: use itertools.pairwise() for successive-pair iteration.

Static analysis (RUF007) flags zip(ts, ts[1:], strict=False) at lines 169 and 177; itertools.pairwise(ts) is the more idiomatic equivalent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dataset/loader/test_baseten_offline_suite.py` around lines 164 -
178, The successive-timestamp gap calculations in test_global_idle_gap_capped
and test_gap_cap_is_opt_in trigger RUF007; import and use itertools.pairwise(ts)
instead of zip(ts, ts[1:], strict=False), preserving the existing gap
assertions.

Source: Linters/SAST tools

tests/unit/dataset/loader/test_baseten_replay_timemodel.py (1)

61-68: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer itertools.pairwise over zip(..., strict=False).

Ruff RUF007 flags this pattern; itertools.pairwise(in_time_order) is clearer for consecutive-pair iteration.

♻️ Suggested diff
+from itertools import pairwise
+
     def test_monotonic_nondecreasing_in_time_order(self):
         ts = [0, 100, 99999, 100050, 500000]
         out = reflow_idle_gaps(ts, 2000)
         in_time_order = [out[i] for i in sorted(range(len(ts)), key=lambda i: ts[i])]
         assert in_time_order == sorted(in_time_order)
         # every consecutive gap <= cap
-        gaps = [b - a for a, b in zip(in_time_order, in_time_order[1:], strict=False)]
+        gaps = [b - a for a, b in pairwise(in_time_order)]
         assert all(g <= 2000 for g in gaps)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dataset/loader/test_baseten_replay_timemodel.py` around lines 61 -
68, Update test_monotonic_nondecreasing_in_time_order to import and use
itertools.pairwise for consecutive gap calculation instead of zip with
strict=False, preserving the existing gap assertion.

Source: Linters/SAST tools

src/aiperf/config/dataset/resolver.py (1)

23-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the baseten-only replay fields into a shared constant

The resolver warning list duplicates the same baseten-only fields already guarded in _converter_dataset.py; centralize them so the warning and reject paths stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/dataset/resolver.py` around lines 23 - 58, Move the
baseten_trace-only field tuple from the resolver into a shared constant module
or existing shared location, then import and reuse it in both
_warn_ignored_baseten_only_fields and the validation logic in
_converter_dataset.py. Remove the duplicate field list and preserve the current
warning and rejection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/metrics-reference.md`:
- Around line 85-89: Update the “Individual Flags” table in
docs/metrics-reference.md to include MetricFlags.FIXED_SCHEDULE_ONLY, alongside
the existing metric flag entries, while preserving the table’s established
format and descriptions.

In `@src/aiperf/dataset/loader/base_trace_loader.py`:
- Around line 289-301: Update the `extra_body` assignment in the trace-to-`Turn`
conversion to distinguish an explicitly provided empty `request_body` from an
absent value: use the declared-field lookup result whenever it is not `None`,
and fall back to `trace.extra` only when no declared `request_body` is
available.

In `@tests/unit/config/test_baseten_replay_flags.py`:
- Around line 23-24: Guard the module-level pyarrow imports in the test module
so unavailable Windows ARM64 environments skip only the parquet-dependent
coverage instead of failing collection. Use pytest.importorskip("pyarrow")
before accessing pyarrow, or move both imports into trace_parquet while
preserving existing parquet behavior.

---

Outside diff comments:
In `@src/aiperf/config/flags/_converter_profiling.py`:
- Around line 328-337: Update the JSONL validation logic around load_json_str so
parsed values are verified to be mapping/object data before calling
data.get("timestamp"). Return False for valid non-object JSON values, while
preserving the existing behavior for parse failures and timestamp checks.

In `@src/aiperf/config/schema/aiperf-config.schema.json`:
- Around line 3358-3414: Update the singular dataset FileDataset schema
definitions for traceSessionSampleRatio, interTurnDelayCapSeconds,
maxIdleGapCapSeconds, and replaySpeedup to match the templated-string variants
already present in the canonical FileDataset schema. Preserve the existing
numeric/null validation and defaults while adding the corresponding
Jinja2/env-var string alternatives so dataset: and datasets: accept the same
config shapes.

In `@src/aiperf/dataset/composer/custom.py`:
- Around line 127-150: Update the file-reading logic in the dataset type
inference method around the with open block so UnicodeDecodeError raised while
iterating through f is handled by the filename-based fallback, not the outer
ValueError handler. Wrap the line-reading iteration together with load_json_str
processing in the existing decode-error handling, preserving the current
behavior for valid JSON and other non-JSON files.

---

Nitpick comments:
In `@src/aiperf/config/dataset/resolver.py`:
- Around line 23-58: Move the baseten_trace-only field tuple from the resolver
into a shared constant module or existing shared location, then import and reuse
it in both _warn_ignored_baseten_only_fields and the validation logic in
_converter_dataset.py. Remove the duplicate field list and preserve the current
warning and rejection behavior.

In `@src/aiperf/post_processors/metric_results_processor.py`:
- Around line 57-65: Update the derived-metric comprehension initializing
self.derive_funcs to retrieve each existing instance directly from
_instances_map by metric.tag instead of calling setdefault with type(metric)().
Preserve the existing derive_value callback mapping and filtering for
MetricType.DERIVED.

In `@tests/unit/dataset/loader/test_baseten_offline_suite.py`:
- Around line 164-178: The successive-timestamp gap calculations in
test_global_idle_gap_capped and test_gap_cap_is_opt_in trigger RUF007; import
and use itertools.pairwise(ts) instead of zip(ts, ts[1:], strict=False),
preserving the existing gap assertions.

In `@tests/unit/dataset/loader/test_baseten_replay_timemodel.py`:
- Around line 61-68: Update test_monotonic_nondecreasing_in_time_order to import
and use itertools.pairwise for consecutive gap calculation instead of zip with
strict=False, preserving the existing gap assertion.

In `@tests/unit/post_processors/test_derived_metric_instance_scope.py`:
- Around line 24-46: The fixed_schedule_run fixture is duplicated across both
test modules. In
tests/unit/post_processors/test_derived_metric_instance_scope.py#L24-L46, retain
or move the existing fixture into tests/unit/post_processors/conftest.py as the
shared fixture; in
tests/unit/post_processors/test_base_metrics_processor.py#L349-L374, remove the
local definition and reuse the shared fixed_schedule_run fixture.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fc6971fc-d85d-42b4-87d2-cd10d8b1adcd

📥 Commits

Reviewing files that changed from the base of the PR and between c1b1984 and ca81100.

📒 Files selected for processing (54)
  • README.md
  • docs/benchmark-datasets.md
  • docs/cli-options.md
  • docs/genai-perf-feature-comparison.md
  • docs/index.yml
  • docs/metrics-reference.md
  • docs/tutorials/baseten-trace.md
  • docs/tutorials/custom-dataset.md
  • src/aiperf/common/enums/metric_enums.py
  • src/aiperf/common/models/dataset_models.py
  • src/aiperf/common/models/record_models.py
  • src/aiperf/config/dataset/config.py
  • src/aiperf/config/dataset/defaults.py
  • src/aiperf/config/dataset/resolver.py
  • src/aiperf/config/flags/_converter_dataset.py
  • src/aiperf/config/flags/_converter_profiling.py
  • src/aiperf/config/flags/cli_config.py
  • src/aiperf/config/schema/aiperf-config.schema.json
  • src/aiperf/dataset/agentic_code_gen/datasets/1k_sessions_200k_ctx/README.md
  • src/aiperf/dataset/composer/custom.py
  • src/aiperf/dataset/generator/video.py
  • src/aiperf/dataset/loader/_baseten_replay_timemodel.py
  • src/aiperf/dataset/loader/base_trace_loader.py
  • src/aiperf/dataset/loader/baseten_trace.py
  • src/aiperf/dataset/loader/models.py
  • src/aiperf/endpoints/openai_completions.py
  • src/aiperf/exporters/exporter_manager.py
  • src/aiperf/metrics/base_derived_metric.py
  • src/aiperf/metrics/types/replay_sched_lag_metrics.py
  • src/aiperf/plugin/enums.py
  • src/aiperf/plugin/plugins.yaml
  • src/aiperf/post_processors/base_metrics_processor.py
  • src/aiperf/post_processors/metric_results_processor.py
  • src/aiperf/post_processors/timeslice_metric_results_processor.py
  • tests/component_integration/dataset/test_baseten_trace_replay.py
  • tests/integration/test_video.py
  • tests/integration/utils.py
  • tests/unit/common/config/test_input_config.py
  • tests/unit/config/test_baseten_replay_flags.py
  • tests/unit/dataset/generator/test_video_generator.py
  • tests/unit/dataset/loader/test_bailian_trace.py
  • tests/unit/dataset/loader/test_baseten_offline_suite.py
  • tests/unit/dataset/loader/test_baseten_replay_timemodel.py
  • tests/unit/dataset/loader/test_baseten_trace.py
  • tests/unit/dataset/loader/test_baseten_trace_optional_pyarrow.py
  • tests/unit/dataset/loader/test_can_load.py
  • tests/unit/endpoints/test_chat_endpoint.py
  • tests/unit/endpoints/test_completions_endpoint.py
  • tests/unit/endpoints/test_openai_completions.py
  • tests/unit/metrics/test_replay_sched_lag_metrics.py
  • tests/unit/post_processors/test_base_metrics_processor.py
  • tests/unit/post_processors/test_derived_metric_instance_scope.py
  • tests/unit/post_processors/test_timeslice_metric_results_processor.py
  • tests/unit/records/test_records_manager_process_results.py
💤 Files with no reviewable changes (1)
  • src/aiperf/dataset/agentic_code_gen/datasets/1k_sessions_200k_ctx/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/aiperf/exporters/exporter_manager.py
  • src/aiperf/common/models/record_models.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/aiperf/config/flags/_converter_profiling.py (1)

328-337: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unhandled AttributeError if a JSONL line is valid JSON but not an object.

load_json_str(stripped) only raises ValueError/TypeError on parse failure, but a well-formed JSON line that isn't an object (e.g. a bare array or number) will pass parsing and then crash on data.get("timestamp") since non-dict JSON values don't support .get(). This exception isn't caught by the surrounding except OSError, so it propagates and would abort CLI config resolution instead of just returning False.

🐛 Proposed fix
                 try:
                     data = load_json_str(stripped)
                 except (ValueError, TypeError):
                     return False
-                return data.get("timestamp") is not None
+                return isinstance(data, dict) and data.get("timestamp") is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/flags/_converter_profiling.py` around lines 328 - 337,
Update the JSONL validation logic around load_json_str so parsed values are
verified to be mapping/object data before calling data.get("timestamp"). Return
False for valid non-object JSON values, while preserving the existing behavior
for parse failures and timestamp checks.
src/aiperf/dataset/composer/custom.py (1)

127-150: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

UnicodeDecodeError needs to wrap the file read, not just JSON parsing.
for line in f: can raise UnicodeDecodeError before load_json_str() runs, and that exception is a ValueError, so the outer handler logs and re-raises instead of falling back to filename-based detection. Catch the decode/read step too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/dataset/composer/custom.py` around lines 127 - 150, Update the
file-reading logic in the dataset type inference method around the with open
block so UnicodeDecodeError raised while iterating through f is handled by the
filename-based fallback, not the outer ValueError handler. Wrap the line-reading
iteration together with load_json_str processing in the existing decode-error
handling, preserving the current behavior for valid JSON and other non-JSON
files.
src/aiperf/config/schema/aiperf-config.schema.json (1)

3358-3414: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Mirror the templated-string variants into the singular dataset: FileDataset schema. traceSessionSampleRatio, interTurnDelayCapSeconds, maxIdleGapCapSeconds, and replaySpeedup accept Jinja2/env-var strings in the canonical FileDataset copy, but the singular shorthand still exposes only numeric/null variants, so the same config shape is treated differently depending on whether it uses dataset: or datasets:.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/schema/aiperf-config.schema.json` around lines 3358 - 3414,
Update the singular dataset FileDataset schema definitions for
traceSessionSampleRatio, interTurnDelayCapSeconds, maxIdleGapCapSeconds, and
replaySpeedup to match the templated-string variants already present in the
canonical FileDataset schema. Preserve the existing numeric/null validation and
defaults while adding the corresponding Jinja2/env-var string alternatives so
dataset: and datasets: accept the same config shapes.
🧹 Nitpick comments (5)
src/aiperf/post_processors/metric_results_processor.py (1)

57-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

setdefault default is always constructed and discarded.

_instances_map is already populated for every registry tag before this comprehension runs (lines 50-52), and _setup_metrics(MetricType.DERIVED) only returns tags that are a subset of MetricRegistry.all_tags(). So metric.tag always already exists in _instances_map, meaning type(metric)() is evaluated and thrown away on every derived metric, every time a processor is constructed — setdefault's default argument is evaluated eagerly regardless of whether the key exists.

♻️ Proposed simplification
         self.derive_funcs: dict[
             MetricTagT, Callable[[MetricResultsDict], MetricValueTypeT]
         ] = {
-            metric.tag: self._instances_map.setdefault(
-                metric.tag, type(metric)()
-            ).derive_value  # type: ignore
+            metric.tag: self._instances_map[metric.tag].derive_value  # type: ignore
             for metric in self._setup_metrics(MetricType.DERIVED)
             if metric.type == MetricType.DERIVED
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/post_processors/metric_results_processor.py` around lines 57 - 65,
Update the derived-metric comprehension initializing self.derive_funcs to
retrieve each existing instance directly from _instances_map by metric.tag
instead of calling setdefault with type(metric)(). Preserve the existing
derive_value callback mapping and filtering for MetricType.DERIVED.
tests/unit/post_processors/test_derived_metric_instance_scope.py (1)

24-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate fixed_schedule_run fixture across two test files. Both files independently define the same BenchmarkRun construction for a single fixed-schedule profiling phase; the shared root cause is a missing common fixture for this scenario.

  • tests/unit/post_processors/test_derived_metric_instance_scope.py#L24-L46: keep this definition (or move it) and import it as the shared fixture.
  • tests/unit/post_processors/test_base_metrics_processor.py#L349-L374: remove this local copy and reuse the shared fixture (e.g. via tests/unit/post_processors/conftest.py) instead of redefining it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/post_processors/test_derived_metric_instance_scope.py` around
lines 24 - 46, The fixed_schedule_run fixture is duplicated across both test
modules. In
tests/unit/post_processors/test_derived_metric_instance_scope.py#L24-L46, retain
or move the existing fixture into tests/unit/post_processors/conftest.py as the
shared fixture; in
tests/unit/post_processors/test_base_metrics_processor.py#L349-L374, remove the
local definition and reuse the shared fixed_schedule_run fixture.
tests/unit/dataset/loader/test_baseten_offline_suite.py (1)

164-178: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: use itertools.pairwise() for successive-pair iteration.

Static analysis (RUF007) flags zip(ts, ts[1:], strict=False) at lines 169 and 177; itertools.pairwise(ts) is the more idiomatic equivalent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dataset/loader/test_baseten_offline_suite.py` around lines 164 -
178, The successive-timestamp gap calculations in test_global_idle_gap_capped
and test_gap_cap_is_opt_in trigger RUF007; import and use itertools.pairwise(ts)
instead of zip(ts, ts[1:], strict=False), preserving the existing gap
assertions.

Source: Linters/SAST tools

tests/unit/dataset/loader/test_baseten_replay_timemodel.py (1)

61-68: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer itertools.pairwise over zip(..., strict=False).

Ruff RUF007 flags this pattern; itertools.pairwise(in_time_order) is clearer for consecutive-pair iteration.

♻️ Suggested diff
+from itertools import pairwise
+
     def test_monotonic_nondecreasing_in_time_order(self):
         ts = [0, 100, 99999, 100050, 500000]
         out = reflow_idle_gaps(ts, 2000)
         in_time_order = [out[i] for i in sorted(range(len(ts)), key=lambda i: ts[i])]
         assert in_time_order == sorted(in_time_order)
         # every consecutive gap <= cap
-        gaps = [b - a for a, b in zip(in_time_order, in_time_order[1:], strict=False)]
+        gaps = [b - a for a, b in pairwise(in_time_order)]
         assert all(g <= 2000 for g in gaps)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dataset/loader/test_baseten_replay_timemodel.py` around lines 61 -
68, Update test_monotonic_nondecreasing_in_time_order to import and use
itertools.pairwise for consecutive gap calculation instead of zip with
strict=False, preserving the existing gap assertion.

Source: Linters/SAST tools

src/aiperf/config/dataset/resolver.py (1)

23-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the baseten-only replay fields into a shared constant

The resolver warning list duplicates the same baseten-only fields already guarded in _converter_dataset.py; centralize them so the warning and reject paths stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/dataset/resolver.py` around lines 23 - 58, Move the
baseten_trace-only field tuple from the resolver into a shared constant module
or existing shared location, then import and reuse it in both
_warn_ignored_baseten_only_fields and the validation logic in
_converter_dataset.py. Remove the duplicate field list and preserve the current
warning and rejection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/metrics-reference.md`:
- Around line 85-89: Update the “Individual Flags” table in
docs/metrics-reference.md to include MetricFlags.FIXED_SCHEDULE_ONLY, alongside
the existing metric flag entries, while preserving the table’s established
format and descriptions.

In `@src/aiperf/dataset/loader/base_trace_loader.py`:
- Around line 289-301: Update the `extra_body` assignment in the trace-to-`Turn`
conversion to distinguish an explicitly provided empty `request_body` from an
absent value: use the declared-field lookup result whenever it is not `None`,
and fall back to `trace.extra` only when no declared `request_body` is
available.

In `@tests/unit/config/test_baseten_replay_flags.py`:
- Around line 23-24: Guard the module-level pyarrow imports in the test module
so unavailable Windows ARM64 environments skip only the parquet-dependent
coverage instead of failing collection. Use pytest.importorskip("pyarrow")
before accessing pyarrow, or move both imports into trace_parquet while
preserving existing parquet behavior.

---

Outside diff comments:
In `@src/aiperf/config/flags/_converter_profiling.py`:
- Around line 328-337: Update the JSONL validation logic around load_json_str so
parsed values are verified to be mapping/object data before calling
data.get("timestamp"). Return False for valid non-object JSON values, while
preserving the existing behavior for parse failures and timestamp checks.

In `@src/aiperf/config/schema/aiperf-config.schema.json`:
- Around line 3358-3414: Update the singular dataset FileDataset schema
definitions for traceSessionSampleRatio, interTurnDelayCapSeconds,
maxIdleGapCapSeconds, and replaySpeedup to match the templated-string variants
already present in the canonical FileDataset schema. Preserve the existing
numeric/null validation and defaults while adding the corresponding
Jinja2/env-var string alternatives so dataset: and datasets: accept the same
config shapes.

In `@src/aiperf/dataset/composer/custom.py`:
- Around line 127-150: Update the file-reading logic in the dataset type
inference method around the with open block so UnicodeDecodeError raised while
iterating through f is handled by the filename-based fallback, not the outer
ValueError handler. Wrap the line-reading iteration together with load_json_str
processing in the existing decode-error handling, preserving the current
behavior for valid JSON and other non-JSON files.

---

Nitpick comments:
In `@src/aiperf/config/dataset/resolver.py`:
- Around line 23-58: Move the baseten_trace-only field tuple from the resolver
into a shared constant module or existing shared location, then import and reuse
it in both _warn_ignored_baseten_only_fields and the validation logic in
_converter_dataset.py. Remove the duplicate field list and preserve the current
warning and rejection behavior.

In `@src/aiperf/post_processors/metric_results_processor.py`:
- Around line 57-65: Update the derived-metric comprehension initializing
self.derive_funcs to retrieve each existing instance directly from
_instances_map by metric.tag instead of calling setdefault with type(metric)().
Preserve the existing derive_value callback mapping and filtering for
MetricType.DERIVED.

In `@tests/unit/dataset/loader/test_baseten_offline_suite.py`:
- Around line 164-178: The successive-timestamp gap calculations in
test_global_idle_gap_capped and test_gap_cap_is_opt_in trigger RUF007; import
and use itertools.pairwise(ts) instead of zip(ts, ts[1:], strict=False),
preserving the existing gap assertions.

In `@tests/unit/dataset/loader/test_baseten_replay_timemodel.py`:
- Around line 61-68: Update test_monotonic_nondecreasing_in_time_order to import
and use itertools.pairwise for consecutive gap calculation instead of zip with
strict=False, preserving the existing gap assertion.

In `@tests/unit/post_processors/test_derived_metric_instance_scope.py`:
- Around line 24-46: The fixed_schedule_run fixture is duplicated across both
test modules. In
tests/unit/post_processors/test_derived_metric_instance_scope.py#L24-L46, retain
or move the existing fixture into tests/unit/post_processors/conftest.py as the
shared fixture; in
tests/unit/post_processors/test_base_metrics_processor.py#L349-L374, remove the
local definition and reuse the shared fixed_schedule_run fixture.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fc6971fc-d85d-42b4-87d2-cd10d8b1adcd

📥 Commits

Reviewing files that changed from the base of the PR and between c1b1984 and ca81100.

📒 Files selected for processing (54)
  • README.md
  • docs/benchmark-datasets.md
  • docs/cli-options.md
  • docs/genai-perf-feature-comparison.md
  • docs/index.yml
  • docs/metrics-reference.md
  • docs/tutorials/baseten-trace.md
  • docs/tutorials/custom-dataset.md
  • src/aiperf/common/enums/metric_enums.py
  • src/aiperf/common/models/dataset_models.py
  • src/aiperf/common/models/record_models.py
  • src/aiperf/config/dataset/config.py
  • src/aiperf/config/dataset/defaults.py
  • src/aiperf/config/dataset/resolver.py
  • src/aiperf/config/flags/_converter_dataset.py
  • src/aiperf/config/flags/_converter_profiling.py
  • src/aiperf/config/flags/cli_config.py
  • src/aiperf/config/schema/aiperf-config.schema.json
  • src/aiperf/dataset/agentic_code_gen/datasets/1k_sessions_200k_ctx/README.md
  • src/aiperf/dataset/composer/custom.py
  • src/aiperf/dataset/generator/video.py
  • src/aiperf/dataset/loader/_baseten_replay_timemodel.py
  • src/aiperf/dataset/loader/base_trace_loader.py
  • src/aiperf/dataset/loader/baseten_trace.py
  • src/aiperf/dataset/loader/models.py
  • src/aiperf/endpoints/openai_completions.py
  • src/aiperf/exporters/exporter_manager.py
  • src/aiperf/metrics/base_derived_metric.py
  • src/aiperf/metrics/types/replay_sched_lag_metrics.py
  • src/aiperf/plugin/enums.py
  • src/aiperf/plugin/plugins.yaml
  • src/aiperf/post_processors/base_metrics_processor.py
  • src/aiperf/post_processors/metric_results_processor.py
  • src/aiperf/post_processors/timeslice_metric_results_processor.py
  • tests/component_integration/dataset/test_baseten_trace_replay.py
  • tests/integration/test_video.py
  • tests/integration/utils.py
  • tests/unit/common/config/test_input_config.py
  • tests/unit/config/test_baseten_replay_flags.py
  • tests/unit/dataset/generator/test_video_generator.py
  • tests/unit/dataset/loader/test_bailian_trace.py
  • tests/unit/dataset/loader/test_baseten_offline_suite.py
  • tests/unit/dataset/loader/test_baseten_replay_timemodel.py
  • tests/unit/dataset/loader/test_baseten_trace.py
  • tests/unit/dataset/loader/test_baseten_trace_optional_pyarrow.py
  • tests/unit/dataset/loader/test_can_load.py
  • tests/unit/endpoints/test_chat_endpoint.py
  • tests/unit/endpoints/test_completions_endpoint.py
  • tests/unit/endpoints/test_openai_completions.py
  • tests/unit/metrics/test_replay_sched_lag_metrics.py
  • tests/unit/post_processors/test_base_metrics_processor.py
  • tests/unit/post_processors/test_derived_metric_instance_scope.py
  • tests/unit/post_processors/test_timeslice_metric_results_processor.py
  • tests/unit/records/test_records_manager_process_results.py
💤 Files with no reviewable changes (1)
  • src/aiperf/dataset/agentic_code_gen/datasets/1k_sessions_200k_ctx/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/aiperf/exporters/exporter_manager.py
  • src/aiperf/common/models/record_models.py
🛑 Comments failed to post (3)
docs/metrics-reference.md (1)

85-89: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A5 'FIXED_SCHEDULE_ONLY|Metric Flags Reference' docs/metrics-reference.md

Repository: ai-dynamo/aiperf

Length of output: 1976


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '2005,2085p' docs/metrics-reference.md
printf '\n---\n'
rg -n 'FIXED_SCHEDULE_ONLY' -A3 -B3 .

Repository: ai-dynamo/aiperf

Length of output: 18104


Add FIXED_SCHEDULE_ONLY to the Metric Flags Reference
docs/metrics-reference.md should list MetricFlags.FIXED_SCHEDULE_ONLY in the “Individual Flags” table, since the new replay schedule metrics already rely on it and the section currently omits it. docs/metrics-reference.md#L2005-L2030

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/metrics-reference.md` around lines 85 - 89, Update the “Individual
Flags” table in docs/metrics-reference.md to include
MetricFlags.FIXED_SCHEDULE_ONLY, alongside the existing metric flag entries,
while preserving the table’s established format and descriptions.
src/aiperf/dataset/loader/base_trace_loader.py (1)

289-301: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Falsy check lets an empty request_body fall back to extra.

request_body or getattr(trace, "extra", None) treats an explicitly-set-but-empty request_body (e.g. {} from BasetenTraceDatasetLoader._set_request_body when force_min_tokens=False and omit_kv_hints=True) the same as "not declared", silently falling back to trace.extra instead of the intended empty dict. Currently benign for BasetenTrace (no extra field), but fragile for any future trace type that declares both fields.

🐛 Proposed fix: check identity, not truthiness
         return Turn(
             timestamp=getattr(trace, "timestamp", None),
             delay=getattr(trace, "delay", None),
             texts=[Text(name="text", contents=[prompt])],
             max_tokens=getattr(trace, "output_length", None),
-            extra_body=request_body or getattr(trace, "extra", None),
+            extra_body=(
+                request_body
+                if request_body is not None
+                else getattr(trace, "extra", None)
+            ),
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        # extra="allow" surfaces stray row keys as attributes; only a DECLARED
        # request_body field (e.g. BasetenTrace) may override the row's extra.
        request_body = (
            getattr(trace, "request_body", None)
            if "request_body" in type(trace).model_fields
            else None
        )
        return Turn(
            timestamp=getattr(trace, "timestamp", None),
            delay=getattr(trace, "delay", None),
            texts=[Text(name="text", contents=[prompt])],
            max_tokens=getattr(trace, "output_length", None),
            extra_body=(
                request_body
                if request_body is not None
                else getattr(trace, "extra", None)
            ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/dataset/loader/base_trace_loader.py` around lines 289 - 301,
Update the `extra_body` assignment in the trace-to-`Turn` conversion to
distinguish an explicitly provided empty `request_body` from an absent value:
use the declared-field lookup result whenever it is not `None`, and fall back to
`trace.extra` only when no declared `request_body` is available.
tests/unit/config/test_baseten_replay_flags.py (1)

23-24: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'pyarrow' pyproject.toml

Repository: ai-dynamo/aiperf

Length of output: 405


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the test file and the nearby optional-pyarrow test for context.
sed -n '1,120p' tests/unit/config/test_baseten_replay_flags.py
printf '\n--- optional pyarrow test ---\n'
sed -n '1,200p' tests/unit/dataset/loader/test_baseten_trace_optional_pyarrow.py

printf '\n--- pyproject pyarrow context ---\n'
sed -n '20,70p' pyproject.toml

Repository: ai-dynamo/aiperf

Length of output: 8573


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether this test module is the only place importing pyarrow at module scope.
rg -n '^\s*(import pyarrow|from pyarrow)' tests/unit -g '*.py'

# Find any skips/importorskip patterns used around pyarrow.
rg -n 'importorskip|pyarrow' tests/unit -g '*.py'

Repository: ai-dynamo/aiperf

Length of output: 9256


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file size to decide whether to read more surrounding lines if needed.
wc -l tests/unit/config/test_baseten_replay_flags.py

Repository: ai-dynamo/aiperf

Length of output: 205


Guard the module-level pyarrow import. pyarrow is intentionally absent on Windows ARM64, so importing it at module scope can make this whole test module fail to collect there and skip the unrelated CLI-plumbing coverage. Move the import behind pytest.importorskip("pyarrow") or into trace_parquet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/config/test_baseten_replay_flags.py` around lines 23 - 24, Guard
the module-level pyarrow imports in the test module so unavailable Windows ARM64
environments skip only the parquet-dependent coverage instead of failing
collection. Use pytest.importorskip("pyarrow") before accessing pyarrow, or move
both imports into trace_parquet while preserving existing parquet behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/aiperf/config/flags/_converter_profiling.py (1)

328-339: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unhandled AttributeError on non-dict JSON line.

data.get("timestamp") runs outside the except (ValueError, TypeError) guard. If the first non-empty JSONL line is valid JSON but not an object (e.g. a list/scalar), this raises an uncaught AttributeError instead of returning False, crashing config resolution on a malformed-but-parseable trace file.

🐛 Proposed fix
                 try:
                     data = load_json_str(stripped)
                 except (ValueError, TypeError):
                     return False
-                return data.get("timestamp") is not None
+                return isinstance(data, dict) and data.get("timestamp") is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/flags/_converter_profiling.py` around lines 328 - 339,
Update the JSONL validation logic around load_json_str in the profiling
converter so a successfully parsed non-dict value returns False instead of
raising when accessing data.get("timestamp"). Validate the parsed data type
before the timestamp lookup, while preserving the existing behavior for
malformed JSON, missing timestamps, and file access errors.
🧹 Nitpick comments (1)
src/aiperf/exporters/exporter_manager.py (1)

169-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reaching into exporter internals via _generate_content().

_write_phase_export calls the leading-underscore exporter._generate_content() across a class boundary. Consider exposing a small public API (e.g. generate_content()) on the exporters for this cross-class use rather than depending on a private method.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/exporters/exporter_manager.py` around lines 169 - 204, Add a
public content-generation API on MetricsJsonExporter and MetricsCsvExporter,
such as generate_content(), and update _write_phase_export to call it instead of
the private _generate_content() method. Preserve the existing generated content
and file-writing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aiperf/config/flags/_converter_profiling.py`:
- Around line 343-371: Update _count_dataset_records so errors reading or
decoding an individual JSONL file inside the directory loop are handled per
file, skipping only that file while preserving the accumulated total from
successfully processed files. Keep the existing behavior for directory discovery
and non-directory file handling unchanged.

In `@src/aiperf/exporters/exporter_manager.py`:
- Around line 120-163: Update _export_phase_metric_artifacts to isolate each
phase’s directory creation and export operations within a per-phase exception
boundary, including phase_dir.mkdir and both _write_phase_export calls. When one
phase raises an OSError or export error, record the failure consistently and
continue processing subsequent phase_records instead of aborting the loop.

In `@tests/unit/common/models/test_credit_models.py`:
- Around line 87-96: Update test_phase_indexes_must_be_non_negative to import
param directly from pytest and replace pytest.param calls with param. Move the
fmt: skip comment from the parameter list’s closing bracket to the closing
parenthesis of the `@pytest.mark.parametrize` call.

In `@tests/unit/post_processors/test_metrics_accumulator.py`:
- Around line 204-207: Update
test_export_results_filters_same_kind_records_by_phase_index with explicit type
annotations for every fixture parameter, including mock_metric_registry and
mock_run, and retain the async test’s None return annotation.

---

Outside diff comments:
In `@src/aiperf/config/flags/_converter_profiling.py`:
- Around line 328-339: Update the JSONL validation logic around load_json_str in
the profiling converter so a successfully parsed non-dict value returns False
instead of raising when accessing data.get("timestamp"). Validate the parsed
data type before the timestamp lookup, while preserving the existing behavior
for malformed JSON, missing timestamps, and file access errors.

---

Nitpick comments:
In `@src/aiperf/exporters/exporter_manager.py`:
- Around line 169-204: Add a public content-generation API on
MetricsJsonExporter and MetricsCsvExporter, such as generate_content(), and
update _write_phase_export to call it instead of the private _generate_content()
method. Preserve the existing generated content and file-writing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fe0daed4-c94a-42ba-9043-cdc30216d187

📥 Commits

Reviewing files that changed from the base of the PR and between ca81100 and 7b31a36.

📒 Files selected for processing (59)
  • docs/benchmark-modes/timing-modes-reference.md
  • docs/cli-options.md
  • docs/dev/patterns.md
  • docs/tutorials/adaptive-scale.md
  • docs/tutorials/yaml-config.md
  • src/aiperf/common/accumulator_protocols.py
  • src/aiperf/common/models/__init__.py
  • src/aiperf/common/models/credit_models.py
  • src/aiperf/common/models/record_models.py
  • src/aiperf/common/phase.py
  • src/aiperf/common/types.py
  • src/aiperf/config/config.py
  • src/aiperf/config/flags/_adaptive_control_cli.py
  • src/aiperf/config/flags/_converter_profiling.py
  • src/aiperf/config/flags/_resolver_adaptive.py
  • src/aiperf/config/flags/_section_fields.py
  • src/aiperf/config/flags/cli_config.py
  • src/aiperf/config/flags/converter.py
  • src/aiperf/config/flags/resolver.py
  • src/aiperf/config/loader/helpers.py
  • src/aiperf/config/loader/normalizers.py
  • src/aiperf/config/phases.py
  • src/aiperf/config/sweep/expand.py
  • src/aiperf/credit/callback_handler.py
  • src/aiperf/credit/issuer.py
  • src/aiperf/credit/messages.py
  • src/aiperf/credit/structs.py
  • src/aiperf/exporters/exporter_manager.py
  • src/aiperf/gpu_telemetry/accumulator.py
  • src/aiperf/gpu_telemetry/protocols.py
  • src/aiperf/metrics/accumulator.py
  • src/aiperf/post_processors/otel_metrics_results_processor.py
  • src/aiperf/records/record_processor_service.py
  • src/aiperf/records/records_manager.py
  • src/aiperf/records/records_tracker.py
  • src/aiperf/timing/concurrency.py
  • src/aiperf/timing/config.py
  • src/aiperf/timing/phase/progress_tracker.py
  • src/aiperf/timing/phase/runner.py
  • src/aiperf/timing/phase_orchestrator.py
  • src/aiperf/timing/strategies/adaptive_scale.py
  • src/aiperf/timing/strategies/adaptive_scale_artifacts.py
  • src/aiperf/timing/strategies/adaptive_scale_backends.py
  • src/aiperf/timing/strategies/adaptive_scale_runtime.py
  • src/aiperf/workers/worker.py
  • tests/component_integration/timing/test_adaptive_scale.py
  • tests/unit/common/models/test_credit_models.py
  • tests/unit/config/test_converter_profiling_phase_routes.py
  • tests/unit/config/test_named_phase_kind.py
  • tests/unit/config/test_v1_resolver_streaming_override.py
  • tests/unit/credit/test_callback_handler.py
  • tests/unit/exporters/test_exporter_manager.py
  • tests/unit/gpu_telemetry/test_accumulator.py
  • tests/unit/post_processors/test_metrics_accumulator.py
  • tests/unit/records/test_dag_metadata_tagging.py
  • tests/unit/records/test_records_manager.py
  • tests/unit/records/test_records_manager_process_results.py
  • tests/unit/timing/strategies/test_adaptive_scale.py
  • tests/unit/timing/test_race_conditions.py
💤 Files with no reviewable changes (4)
  • src/aiperf/config/flags/_section_fields.py
  • src/aiperf/config/flags/_resolver_adaptive.py
  • src/aiperf/config/flags/_adaptive_control_cli.py
  • tests/unit/config/test_v1_resolver_streaming_override.py
🚧 Files skipped from review as they are similar to previous changes (39)
  • docs/benchmark-modes/timing-modes-reference.md
  • docs/tutorials/adaptive-scale.md
  • src/aiperf/records/record_processor_service.py
  • src/aiperf/workers/worker.py
  • src/aiperf/common/models/init.py
  • src/aiperf/credit/messages.py
  • src/aiperf/credit/structs.py
  • src/aiperf/post_processors/otel_metrics_results_processor.py
  • src/aiperf/gpu_telemetry/protocols.py
  • docs/dev/patterns.md
  • src/aiperf/config/loader/helpers.py
  • src/aiperf/timing/phase_orchestrator.py
  • tests/unit/timing/test_race_conditions.py
  • src/aiperf/timing/strategies/adaptive_scale_runtime.py
  • src/aiperf/timing/strategies/adaptive_scale.py
  • src/aiperf/common/models/credit_models.py
  • src/aiperf/common/accumulator_protocols.py
  • tests/unit/exporters/test_exporter_manager.py
  • src/aiperf/timing/strategies/adaptive_scale_artifacts.py
  • src/aiperf/config/sweep/expand.py
  • tests/unit/credit/test_callback_handler.py
  • tests/component_integration/timing/test_adaptive_scale.py
  • src/aiperf/config/phases.py
  • src/aiperf/timing/phase/progress_tracker.py
  • src/aiperf/timing/strategies/adaptive_scale_backends.py
  • src/aiperf/gpu_telemetry/accumulator.py
  • src/aiperf/config/config.py
  • tests/unit/records/test_records_manager_process_results.py
  • src/aiperf/config/loader/normalizers.py
  • src/aiperf/credit/issuer.py
  • src/aiperf/timing/phase/runner.py
  • tests/unit/config/test_converter_profiling_phase_routes.py
  • tests/unit/records/test_dag_metadata_tagging.py
  • src/aiperf/records/records_tracker.py
  • src/aiperf/records/records_manager.py
  • src/aiperf/config/flags/resolver.py
  • src/aiperf/credit/callback_handler.py
  • tests/unit/records/test_records_manager.py
  • tests/unit/timing/strategies/test_adaptive_scale.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/aiperf/config/flags/_converter_profiling.py (1)

328-339: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unhandled AttributeError on non-dict JSON line.

data.get("timestamp") runs outside the except (ValueError, TypeError) guard. If the first non-empty JSONL line is valid JSON but not an object (e.g. a list/scalar), this raises an uncaught AttributeError instead of returning False, crashing config resolution on a malformed-but-parseable trace file.

🐛 Proposed fix
                 try:
                     data = load_json_str(stripped)
                 except (ValueError, TypeError):
                     return False
-                return data.get("timestamp") is not None
+                return isinstance(data, dict) and data.get("timestamp") is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/flags/_converter_profiling.py` around lines 328 - 339,
Update the JSONL validation logic around load_json_str in the profiling
converter so a successfully parsed non-dict value returns False instead of
raising when accessing data.get("timestamp"). Validate the parsed data type
before the timestamp lookup, while preserving the existing behavior for
malformed JSON, missing timestamps, and file access errors.
🧹 Nitpick comments (1)
src/aiperf/exporters/exporter_manager.py (1)

169-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reaching into exporter internals via _generate_content().

_write_phase_export calls the leading-underscore exporter._generate_content() across a class boundary. Consider exposing a small public API (e.g. generate_content()) on the exporters for this cross-class use rather than depending on a private method.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/exporters/exporter_manager.py` around lines 169 - 204, Add a
public content-generation API on MetricsJsonExporter and MetricsCsvExporter,
such as generate_content(), and update _write_phase_export to call it instead of
the private _generate_content() method. Preserve the existing generated content
and file-writing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aiperf/config/flags/_converter_profiling.py`:
- Around line 343-371: Update _count_dataset_records so errors reading or
decoding an individual JSONL file inside the directory loop are handled per
file, skipping only that file while preserving the accumulated total from
successfully processed files. Keep the existing behavior for directory discovery
and non-directory file handling unchanged.

In `@src/aiperf/exporters/exporter_manager.py`:
- Around line 120-163: Update _export_phase_metric_artifacts to isolate each
phase’s directory creation and export operations within a per-phase exception
boundary, including phase_dir.mkdir and both _write_phase_export calls. When one
phase raises an OSError or export error, record the failure consistently and
continue processing subsequent phase_records instead of aborting the loop.

In `@tests/unit/common/models/test_credit_models.py`:
- Around line 87-96: Update test_phase_indexes_must_be_non_negative to import
param directly from pytest and replace pytest.param calls with param. Move the
fmt: skip comment from the parameter list’s closing bracket to the closing
parenthesis of the `@pytest.mark.parametrize` call.

In `@tests/unit/post_processors/test_metrics_accumulator.py`:
- Around line 204-207: Update
test_export_results_filters_same_kind_records_by_phase_index with explicit type
annotations for every fixture parameter, including mock_metric_registry and
mock_run, and retain the async test’s None return annotation.

---

Outside diff comments:
In `@src/aiperf/config/flags/_converter_profiling.py`:
- Around line 328-339: Update the JSONL validation logic around load_json_str in
the profiling converter so a successfully parsed non-dict value returns False
instead of raising when accessing data.get("timestamp"). Validate the parsed
data type before the timestamp lookup, while preserving the existing behavior
for malformed JSON, missing timestamps, and file access errors.

---

Nitpick comments:
In `@src/aiperf/exporters/exporter_manager.py`:
- Around line 169-204: Add a public content-generation API on
MetricsJsonExporter and MetricsCsvExporter, such as generate_content(), and
update _write_phase_export to call it instead of the private _generate_content()
method. Preserve the existing generated content and file-writing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fe0daed4-c94a-42ba-9043-cdc30216d187

📥 Commits

Reviewing files that changed from the base of the PR and between ca81100 and 7b31a36.

📒 Files selected for processing (59)
  • docs/benchmark-modes/timing-modes-reference.md
  • docs/cli-options.md
  • docs/dev/patterns.md
  • docs/tutorials/adaptive-scale.md
  • docs/tutorials/yaml-config.md
  • src/aiperf/common/accumulator_protocols.py
  • src/aiperf/common/models/__init__.py
  • src/aiperf/common/models/credit_models.py
  • src/aiperf/common/models/record_models.py
  • src/aiperf/common/phase.py
  • src/aiperf/common/types.py
  • src/aiperf/config/config.py
  • src/aiperf/config/flags/_adaptive_control_cli.py
  • src/aiperf/config/flags/_converter_profiling.py
  • src/aiperf/config/flags/_resolver_adaptive.py
  • src/aiperf/config/flags/_section_fields.py
  • src/aiperf/config/flags/cli_config.py
  • src/aiperf/config/flags/converter.py
  • src/aiperf/config/flags/resolver.py
  • src/aiperf/config/loader/helpers.py
  • src/aiperf/config/loader/normalizers.py
  • src/aiperf/config/phases.py
  • src/aiperf/config/sweep/expand.py
  • src/aiperf/credit/callback_handler.py
  • src/aiperf/credit/issuer.py
  • src/aiperf/credit/messages.py
  • src/aiperf/credit/structs.py
  • src/aiperf/exporters/exporter_manager.py
  • src/aiperf/gpu_telemetry/accumulator.py
  • src/aiperf/gpu_telemetry/protocols.py
  • src/aiperf/metrics/accumulator.py
  • src/aiperf/post_processors/otel_metrics_results_processor.py
  • src/aiperf/records/record_processor_service.py
  • src/aiperf/records/records_manager.py
  • src/aiperf/records/records_tracker.py
  • src/aiperf/timing/concurrency.py
  • src/aiperf/timing/config.py
  • src/aiperf/timing/phase/progress_tracker.py
  • src/aiperf/timing/phase/runner.py
  • src/aiperf/timing/phase_orchestrator.py
  • src/aiperf/timing/strategies/adaptive_scale.py
  • src/aiperf/timing/strategies/adaptive_scale_artifacts.py
  • src/aiperf/timing/strategies/adaptive_scale_backends.py
  • src/aiperf/timing/strategies/adaptive_scale_runtime.py
  • src/aiperf/workers/worker.py
  • tests/component_integration/timing/test_adaptive_scale.py
  • tests/unit/common/models/test_credit_models.py
  • tests/unit/config/test_converter_profiling_phase_routes.py
  • tests/unit/config/test_named_phase_kind.py
  • tests/unit/config/test_v1_resolver_streaming_override.py
  • tests/unit/credit/test_callback_handler.py
  • tests/unit/exporters/test_exporter_manager.py
  • tests/unit/gpu_telemetry/test_accumulator.py
  • tests/unit/post_processors/test_metrics_accumulator.py
  • tests/unit/records/test_dag_metadata_tagging.py
  • tests/unit/records/test_records_manager.py
  • tests/unit/records/test_records_manager_process_results.py
  • tests/unit/timing/strategies/test_adaptive_scale.py
  • tests/unit/timing/test_race_conditions.py
💤 Files with no reviewable changes (4)
  • src/aiperf/config/flags/_section_fields.py
  • src/aiperf/config/flags/_resolver_adaptive.py
  • src/aiperf/config/flags/_adaptive_control_cli.py
  • tests/unit/config/test_v1_resolver_streaming_override.py
🚧 Files skipped from review as they are similar to previous changes (39)
  • docs/benchmark-modes/timing-modes-reference.md
  • docs/tutorials/adaptive-scale.md
  • src/aiperf/records/record_processor_service.py
  • src/aiperf/workers/worker.py
  • src/aiperf/common/models/init.py
  • src/aiperf/credit/messages.py
  • src/aiperf/credit/structs.py
  • src/aiperf/post_processors/otel_metrics_results_processor.py
  • src/aiperf/gpu_telemetry/protocols.py
  • docs/dev/patterns.md
  • src/aiperf/config/loader/helpers.py
  • src/aiperf/timing/phase_orchestrator.py
  • tests/unit/timing/test_race_conditions.py
  • src/aiperf/timing/strategies/adaptive_scale_runtime.py
  • src/aiperf/timing/strategies/adaptive_scale.py
  • src/aiperf/common/models/credit_models.py
  • src/aiperf/common/accumulator_protocols.py
  • tests/unit/exporters/test_exporter_manager.py
  • src/aiperf/timing/strategies/adaptive_scale_artifacts.py
  • src/aiperf/config/sweep/expand.py
  • tests/unit/credit/test_callback_handler.py
  • tests/component_integration/timing/test_adaptive_scale.py
  • src/aiperf/config/phases.py
  • src/aiperf/timing/phase/progress_tracker.py
  • src/aiperf/timing/strategies/adaptive_scale_backends.py
  • src/aiperf/gpu_telemetry/accumulator.py
  • src/aiperf/config/config.py
  • tests/unit/records/test_records_manager_process_results.py
  • src/aiperf/config/loader/normalizers.py
  • src/aiperf/credit/issuer.py
  • src/aiperf/timing/phase/runner.py
  • tests/unit/config/test_converter_profiling_phase_routes.py
  • tests/unit/records/test_dag_metadata_tagging.py
  • src/aiperf/records/records_tracker.py
  • src/aiperf/records/records_manager.py
  • src/aiperf/config/flags/resolver.py
  • src/aiperf/credit/callback_handler.py
  • tests/unit/records/test_records_manager.py
  • tests/unit/timing/strategies/test_adaptive_scale.py
🛑 Comments failed to post (4)
src/aiperf/config/flags/_converter_profiling.py (1)

343-371: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

One bad JSONL file zeroes the whole directory count.

The single outer try/except (OSError, UnicodeDecodeError): return 0 spans the entire rglob("*.jsonl") loop. A decode error in any one file among many discards the total already accumulated from prior files and returns 0 for the whole directory, understating the dataset size rather than just skipping the offending file.

🐛 Proposed fix — isolate per-file failures
         if path.is_dir():
             total = 0
             for jsonl in path.rglob("*.jsonl"):
-                with open(jsonl, encoding="utf-8") as f:
-                    total += sum(1 for line in f if line.strip())
+                try:
+                    with open(jsonl, encoding="utf-8") as f:
+                        total += sum(1 for line in f if line.strip())
+                except (OSError, UnicodeDecodeError):
+                    continue
             return total
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

def _count_dataset_records(file_path: object) -> int:
    """Count records across a JSONL file/directory or Parquet trace file."""
    from pathlib import Path

    path = Path(file_path)
    try:
        if path.is_dir():
            total = 0
            for jsonl in path.rglob("*.jsonl"):
                try:
                    with open(jsonl, encoding="utf-8") as f:
                        total += sum(1 for line in f if line.strip())
                except (OSError, UnicodeDecodeError):
                    continue
            return total
        if path.suffix.lower() == ".parquet" and path.is_file():
            try:
                import pyarrow as pa
                import pyarrow.parquet as pq
            except ImportError:
                return 0

            try:
                return pq.ParquetFile(path).metadata.num_rows
            except (OSError, pa.ArrowException):
                return 0
        if path.is_file():
            with open(path, encoding="utf-8") as f:
                return sum(1 for line in f if line.strip())
    except (OSError, UnicodeDecodeError):
        return 0
    return 0
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 351-351: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(jsonl, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 366-366: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(path, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/flags/_converter_profiling.py` around lines 343 - 371,
Update _count_dataset_records so errors reading or decoding an individual JSONL
file inside the directory loop are handled per file, skipping only that file
while preserving the accumulated total from successfully processed files. Keep
the existing behavior for directory discovery and non-directory file handling
unchanged.
src/aiperf/exporters/exporter_manager.py (1)

120-163: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Isolate per-phase failures inside the loop, not just around the whole method.

phase_dir.mkdir(...) (line 127) is unguarded; an OSError for one phase's directory aborts the entire for phase_result in phase_records loop, so every phase after the failing one silently loses its metric artifacts even though the outer try/except (lines 105-108) prevents a hard crash.

🛡️ Proposed fix
         for phase_result in phase_records:
             phase_dir = self._run.cfg.artifacts.dir / "phases" / phase_result.phase_name
-            await asyncio.to_thread(phase_dir.mkdir, parents=True, exist_ok=True)
+            try:
+                await asyncio.to_thread(phase_dir.mkdir, parents=True, exist_ok=True)
+            except OSError as exc:
+                self.warning(
+                    f"Failed to create phase artifact dir {phase_dir}: {exc}"
+                )
+                continue
             completed = (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    async def _export_phase_metric_artifacts(self) -> None:
        phase_records = getattr(self._results, "phase_records", None) or []
        if not phase_records:
            return
        manifest_entries: list[dict[str, Any]] = []
        for phase_result in phase_records:
            phase_dir = self._run.cfg.artifacts.dir / "phases" / phase_result.phase_name
            try:
                await asyncio.to_thread(phase_dir.mkdir, parents=True, exist_ok=True)
            except OSError as exc:
                self.warning(
                    f"Failed to create phase artifact dir {phase_dir}: {exc}"
                )
                continue
            completed = (
                phase_result.successful_request_count + phase_result.error_request_count
            )
            phase_profile = ProfileResults(
                records=phase_result.records,
                completed=completed,
                start_ns=phase_result.start_ns or self._results.start_ns,
                end_ns=phase_result.end_ns or self._results.end_ns,
                was_cancelled=phase_result.was_cancelled,
                successful_request_count=phase_result.successful_request_count,
                error_request_count=phase_result.error_request_count,
                error_summary=[],
            )
            entry: dict[str, Any] = {
                "phase_index": phase_result.phase_index,
                "profiling_index": phase_result.profiling_index,
                "phase_name": phase_result.phase_name,
                "phase_kind": phase_result.phase_kind,
            }
            await self._write_phase_export(
                exporter_cls=MetricsJsonExporter,
                phase_profile=phase_profile,
                file_path=phase_dir
                / self._run.cfg.artifacts.profile_export_json_file.name,
                manifest_entry=entry,
                manifest_key="metrics_json",
            )
            await self._write_phase_export(
                exporter_cls=MetricsCsvExporter,
                phase_profile=phase_profile,
                file_path=phase_dir
                / self._run.cfg.artifacts.profile_export_csv_file.name,
                manifest_entry=entry,
                manifest_key="metrics_csv",
            )
            manifest_entries.append(entry)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/exporters/exporter_manager.py` around lines 120 - 163, Update
_export_phase_metric_artifacts to isolate each phase’s directory creation and
export operations within a per-phase exception boundary, including
phase_dir.mkdir and both _write_phase_export calls. When one phase raises an
OSError or export error, record the failure consistently and continue processing
subsequent phase_records instead of aborting the loop.
tests/unit/common/models/test_credit_models.py (1)

87-96: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

pytest.param import style and misplaced # fmt: skip.

Uses pytest.param(...) instead of importing param, and places # fmt: skip on the closing ] of the list (line 92) instead of the closing ) of @pytest.mark.parametrize(...) (line 93) — Ruff RUF028 confirms the comment is ineffective there.

🔧 Proposed fix
+from pytest import param
+
     `@pytest.mark.parametrize`(
         "field",
         [
-            pytest.param("phase_index", id="phase-index"),
-            pytest.param("profiling_index", id="profiling-index"),
-        ],  # fmt: skip
-    )
+            param("phase_index", id="phase-index"),
+            param("profiling_index", id="profiling-index"),
+        ],
+    )  # fmt: skip
     def test_phase_indexes_must_be_non_negative(self, field: str) -> None:
As per path instructions, "When using pytest.param, import it with from pytest import param and place # fmt: skip on the closing ) line of the parametrization block."
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 92-92: This suppression comment is invalid because it cannot be in an expression, pattern, argument list, or other non-statement

Remove this comment

(RUF028)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/common/models/test_credit_models.py` around lines 87 - 96, Update
test_phase_indexes_must_be_non_negative to import param directly from pytest and
replace pytest.param calls with param. Move the fmt: skip comment from the
parameter list’s closing bracket to the closing parenthesis of the
`@pytest.mark.parametrize` call.

Sources: Path instructions, Linters/SAST tools

tests/unit/post_processors/test_metrics_accumulator.py (1)

204-207: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required test annotations.

Annotate the fixture parameters and return type for this new async test. As per coding guidelines, “Annotate all functions with type hints for every parameter and return value.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/post_processors/test_metrics_accumulator.py` around lines 204 -
207, Update test_export_results_filters_same_kind_records_by_phase_index with
explicit type annotations for every fixture parameter, including
mock_metric_registry and mock_run, and retain the async test’s None return
annotation.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/aiperf/config/schema/aiperf-config.schema.json (1)

1043-1050: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Update the phase schema to allow named workflows
src/aiperf/config/schema/aiperf-config.schema.json still constrains phase name to warmup/profiling and doesn’t expose kind, so schema-based validation will reject the new arbitrary phase names. Regenerate or patch the schema to match src/aiperf/config/phases.py.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/config/schema/aiperf-config.schema.json` around lines 1043 - 1050,
Update the phase definition schema in aiperf-config.schema.json to match
phases.py: allow arbitrary string values for phase name instead of restricting
it to warmup/profiling, and add the phase kind property with the same validation
and requiredness as the Python model. Regenerate the schema if supported;
otherwise patch the corresponding phase schema directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/post_processors/conftest.py`:
- Around line 71-76: Move uuid, BenchmarkConfig, and BenchmarkRun imports from
fixed_schedule_run to the module-level import block, then annotate
fixed_schedule_run with -> BenchmarkRun while preserving its existing fixture
behavior.

---

Outside diff comments:
In `@src/aiperf/config/schema/aiperf-config.schema.json`:
- Around line 1043-1050: Update the phase definition schema in
aiperf-config.schema.json to match phases.py: allow arbitrary string values for
phase name instead of restricting it to warmup/profiling, and add the phase kind
property with the same validation and requiredness as the Python model.
Regenerate the schema if supported; otherwise patch the corresponding phase
schema directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 87470b2e-642e-4c3c-b1f0-d845c577e384

📥 Commits

Reviewing files that changed from the base of the PR and between 7b31a36 and 56f3b0d.

📒 Files selected for processing (16)
  • docs/metrics-reference.md
  • src/aiperf/config/dataset/constants.py
  • src/aiperf/config/dataset/resolver.py
  • src/aiperf/config/flags/_converter_dataset.py
  • src/aiperf/config/flags/_converter_profiling.py
  • src/aiperf/config/schema/aiperf-config.schema.json
  • src/aiperf/config/sweep/expand.py
  • src/aiperf/dataset/composer/custom.py
  • src/aiperf/dataset/loader/base_trace_loader.py
  • src/aiperf/post_processors/metric_results_processor.py
  • tests/unit/config/test_baseten_replay_flags.py
  • tests/unit/dataset/loader/test_baseten_offline_suite.py
  • tests/unit/dataset/loader/test_baseten_replay_timemodel.py
  • tests/unit/post_processors/conftest.py
  • tests/unit/post_processors/test_base_metrics_processor.py
  • tests/unit/post_processors/test_derived_metric_instance_scope.py
💤 Files with no reviewable changes (2)
  • tests/unit/post_processors/test_base_metrics_processor.py
  • tests/unit/post_processors/test_derived_metric_instance_scope.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/aiperf/config/sweep/expand.py
  • src/aiperf/config/flags/_converter_profiling.py

Comment thread tests/unit/post_processors/conftest.py Outdated
@ilana-n ilana-n changed the title feat: add named phase workflows feat: enable multiple warmup and profiling phases in a single run Jul 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/aiperf/accuracy/benchmarks/mmlu.py (1)

271-280: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated CoT instruction-building logic.

The base-instruction + conditional CoT clause + trailing blank line block is duplicated verbatim between _format_prompt and _build_chat_messages. Extracting a shared helper (e.g. _build_instruction(subject, enable_cot)) would avoid the two copies drifting if the CoT wording is tweaked later.

♻️ Proposed refactor
+    def _build_instruction(self, subject: str, enable_cot: bool) -> str:
+        instruction = (
+            "The following are multiple choice questions (with answers) "
+            f"about {subject.replace('_', ' ')}."
+        )
+        if enable_cot:
+            instruction += (
+                " Think step by step and then output the answer in the format of "
+                '"The answer is (X)" at the end.'
+            )
+        instruction += "\n\n"
+        return instruction

Then in _format_prompt and _build_chat_messages, replace the inline block with instruction = self._build_instruction(subject, enable_cot).

Also applies to: 315-324

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/accuracy/benchmarks/mmlu.py` around lines 271 - 280, Extract the
duplicated instruction construction from _format_prompt and _build_chat_messages
into a shared _build_instruction(subject, enable_cot) helper, preserving the
subject formatting, conditional CoT wording, and trailing blank line. Replace
both inline blocks with calls to this helper so future wording changes remain
centralized.
src/aiperf/plugin/plugins.yaml (1)

1515-1521: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add metadata for the new grader plugin.

mmlu_pro defines class and description but omits metadata. Add a metadata mapping consistent with the plugin registry contract. As per coding guidelines, “For new plugins, add an entry with class, description, and metadata.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/plugin/plugins.yaml` around lines 1515 - 1521, Add a metadata
mapping to the mmlu_pro plugin entry alongside its existing class and
description fields, following the structure and values required by the
surrounding plugin registry entries and contract. Preserve the current class and
description unchanged.

Source: Coding guidelines

tests/unit/accuracy/test_mmlu_pro_grader.py (1)

24-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the required test-name structure.

Rename the new tests to test_<function>_<scenario>_<expected> so failures identify the exercised method and intended outcome.

  • tests/unit/accuracy/test_mmlu_pro_grader.py#L24-L63: rename the new grade() tests, e.g. test_grade_tier1_correct_clean.
  • tests/unit/accuracy/test_multiple_choice_grader.py#L127-L154: rename the CoT fallback tests with an explicit expected outcome, e.g. test_grade_cot_answer_marker_clean.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/accuracy/test_mmlu_pro_grader.py` around lines 24 - 63, Rename the
new grade() tests in tests/unit/accuracy/test_mmlu_pro_grader.py lines 24-63 to
the required test_grade_<scenario>_<expected> structure, using names that
identify the exercised scenario and outcome. Also rename the CoT fallback tests
in tests/unit/accuracy/test_multiple_choice_grader.py lines 127-154 to include
the grade method, scenario, and explicit expected result; change test names only
and preserve test behavior.

Source: Coding guidelines

src/aiperf/accuracy/graders/mmlu_pro.py (1)

36-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid annotating dynamic **kwargs as Any.

These methods do not consume a defined keyword contract; leave **kwargs untyped or replace it with explicit parameters. Based on learnings, “avoid adding type annotations to **kwargs like **kwargs: Any.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/accuracy/graders/mmlu_pro.py` around lines 36 - 42, Remove the Any
annotation from the dynamic **kwargs parameters in extract_answer and grade, or
replace them with explicit keyword parameters if a defined contract exists. Keep
the existing method behavior and return annotations unchanged.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/test_adaptive_scale.py`:
- Around line 122-127: Strengthen the assertions in the adaptive-scale artifact
test so adaptive_phase["events_path"] and adaptive_phase["summary_path"] are
verified to reside under the phases/profiling/ directory, preferably by
asserting their exact expected relative paths. Retain the existing existence
checks and top-level artifact absence checks.
- Around line 112-115: Update the async test’s artifact checks around
manifest_path and the later existence checks to avoid synchronous Path.exists()
and read_bytes() calls. Use the project’s async file API, or await
asyncio.to_thread for each filesystem operation, while preserving the existing
manifest parsing and schema_version assertion.

---

Nitpick comments:
In `@src/aiperf/accuracy/benchmarks/mmlu.py`:
- Around line 271-280: Extract the duplicated instruction construction from
_format_prompt and _build_chat_messages into a shared
_build_instruction(subject, enable_cot) helper, preserving the subject
formatting, conditional CoT wording, and trailing blank line. Replace both
inline blocks with calls to this helper so future wording changes remain
centralized.

In `@src/aiperf/accuracy/graders/mmlu_pro.py`:
- Around line 36-42: Remove the Any annotation from the dynamic **kwargs
parameters in extract_answer and grade, or replace them with explicit keyword
parameters if a defined contract exists. Keep the existing method behavior and
return annotations unchanged.

In `@src/aiperf/plugin/plugins.yaml`:
- Around line 1515-1521: Add a metadata mapping to the mmlu_pro plugin entry
alongside its existing class and description fields, following the structure and
values required by the surrounding plugin registry entries and contract.
Preserve the current class and description unchanged.

In `@tests/unit/accuracy/test_mmlu_pro_grader.py`:
- Around line 24-63: Rename the new grade() tests in
tests/unit/accuracy/test_mmlu_pro_grader.py lines 24-63 to the required
test_grade_<scenario>_<expected> structure, using names that identify the
exercised scenario and outcome. Also rename the CoT fallback tests in
tests/unit/accuracy/test_multiple_choice_grader.py lines 127-154 to include the
grade method, scenario, and explicit expected result; change test names only and
preserve test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: afd366d2-a912-4439-91ee-33a0681b0dfc

📥 Commits

Reviewing files that changed from the base of the PR and between 56f3b0d and f4ccb93.

📒 Files selected for processing (36)
  • docs/accuracy/accuracy-benchmarking.md
  • docs/cli-options.md
  • docs/tutorials/adaptive-scale.md
  • docs/tutorials/yaml-config.md
  • src/aiperf/accuracy/accuracy_record_processor.py
  • src/aiperf/accuracy/accuracy_results_processor.py
  • src/aiperf/accuracy/benchmarks/mmlu.py
  • src/aiperf/accuracy/benchmarks/mmlu_pro.py
  • src/aiperf/accuracy/graders/_choice_extract.py
  • src/aiperf/accuracy/graders/mmlu_pro.py
  • src/aiperf/accuracy/graders/multiple_choice.py
  • src/aiperf/accuracy/models.py
  • src/aiperf/common/enums/metric_enums.py
  • src/aiperf/config/flags/cli_config.py
  • src/aiperf/config/schema/aiperf-config.schema.json
  • src/aiperf/dataset/loader/accuracy_dataset_loader.py
  • src/aiperf/metrics/types/accuracy_metrics.py
  • src/aiperf/metrics/types/osl_mismatch_metrics.py
  • src/aiperf/plugin/enums.py
  • src/aiperf/plugin/plugins.yaml
  • src/aiperf/post_processors/base_metrics_processor.py
  • src/aiperf/records/records_manager.py
  • src/aiperf/timing/config.py
  • tests/integration/test_adaptive_scale.py
  • tests/unit/accuracy/test_accuracy_record_processor.py
  • tests/unit/accuracy/test_choice_extract.py
  • tests/unit/accuracy/test_hf_benchmark_datasets.py
  • tests/unit/accuracy/test_mmlu_cot.py
  • tests/unit/accuracy/test_mmlu_pro_benchmark.py
  • tests/unit/accuracy/test_mmlu_pro_grader.py
  • tests/unit/accuracy/test_multiple_choice_grader.py
  • tests/unit/config/test_accuracy_enable_cot_flag.py
  • tests/unit/post_processors/conftest.py
  • tests/unit/records/test_records_manager.py
  • tests/unit/records/test_records_manager_process_results.py
  • tests/unit/timing/test_timing_config.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/unit/records/test_records_manager_process_results.py
  • src/aiperf/config/flags/cli_config.py
  • docs/tutorials/adaptive-scale.md
  • tests/unit/records/test_records_manager.py
  • src/aiperf/timing/config.py
  • src/aiperf/records/records_manager.py

Comment thread tests/integration/test_adaptive_scale.py
Comment thread tests/integration/test_adaptive_scale.py Outdated
@ilana-n ilana-n force-pushed the ilana/aip-1004-1006-named-phase-kind-workflows branch from df3e1d1 to ed07d76 Compare July 15, 2026 20:31
Signed-off-by: inguyen <inguyen@nvidia.com>
@ilana-n ilana-n force-pushed the ilana/aip-1004-1006-named-phase-kind-workflows branch from ed07d76 to 7a96498 Compare July 15, 2026 20:39
ilana-n and others added 4 commits July 15, 2026 20:56
Signed-off-by: inguyen <inguyen@nvidia.com>
Signed-off-by: inguyen <inguyen@nvidia.com>
Signed-off-by: inguyen <inguyen@nvidia.com>
@ilana-n ilana-n marked this pull request as ready for review July 15, 2026 22:48
Signed-off-by: inguyen <inguyen@nvidia.com>

@ajcasagrande ajcasagrande left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AIPerf multi-phase review — solid, coherent feature; all 2089 targeted unit tests pass and the per-phase artifact pipeline works end-to-end (verified against the in-repo mock server with a warmup + 3-profiling-phase config). Two real correctness defects found, both confined to the new multi-phase surface — single-profiling-phase and CLI paths are unaffected. Both reproduced at runtime.

Fix order:

  1. (High) Root/top-level export silently reflects only the last profiling phase's metric distributions while counts are summed across all phases → internally inconsistent primary artifact.
  2. (Medium) Per-phase cancellation doesn't inherit the run default the docstring promises; the run-level value and PhaseOrchestrator._cancellation_policy are dead code.

Working well: the name/kind split and validation; concurrency/credit slot keying round-trip (no leak/over-release); adaptive backend keying; final-phase deferral (no stranded results); NaN-safe accumulator masking; clean adaptive-scale CLI removal (no dangling refs); path-safe phase-scoped artifacts.

start_ns=phase_stats.start_ns,
end_ns=phase_stats.requests_end_ns,
phase=phase,
phase_index=phase_stats.phase_index,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: root export collapses to only the last profiling phase. _process_results (line 1920) passes phase_index=None for the PROFILING summary intending to aggregate all profiling phases via the categorical benchmark_phase mask. But here create_stats_for_phase(phase, phase_index=None) resolves via _latest_tracker_for_phase to the last profiling tracker, so ctx.phase_index becomes that concrete int (not None), and accumulator._mask_for_export_context filters to only that phase's records.

Reproduced (warmup + low/storm/recovery, 20 req each): root profile_export_aiperf.json reports request_count=20 and is byte-identical to recovery, while completed (from _aggregate_records_stats_for_phase) sums to 60 — an inconsistent summary. The same defect collapses the warmup aggregate. Suggest ExportContext(..., phase_index=phase_index, ...) (honor the requested arg) plus a regression test that exercises the real column-store masking with ≥2 profiling phases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, this is important thanks for catching it - thought I had fixed it but I see not entirely!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gonna wait on #1144 since that will be involved with the fix here

_build_phase_config(
phase,
artifact_dir=artifact_dir,
default_cancellation=default_cancellation,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: omitted per-phase cancellation gets NO cancellation, not the documented run default. The threaded default_cancellation (line 101) is an empty RequestCancellationConfig(); the real run default run_cancellation (line 102) is only stored on TimingConfig.request_cancellation, which PhaseOrchestrator no longer reads (it builds a fresh per-phase simulator), so run_cancellation and _cancellation_policy are dead. The from_run docstring (line 95) says "omitted per-phase cancellation inherits the run default."

Reproduced (two profiling phases: cancel_phase rate 100, inherit_phase omitted): cancel_phase cancels 20/20, inherit_phase cancels 0/20 — inheritance never happens. Also _default_cancellation_config now scans warmup phases (ordering change vs get_profiling_phases()). Please pick one semantic and make code + docstring agree, and drop the dead run-level policy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! will address

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants