Skip to content

refactor(benchmark): split execute.py into execute/profiling/accuracy/pipeline - #421

Open
arekay-nv wants to merge 13 commits into
mainfrom
refactor/execute-split
Open

refactor(benchmark): split execute.py into execute/profiling/accuracy/pipeline#421
arekay-nv wants to merge 13 commits into
mainfrom
refactor/execute-split

Conversation

@arekay-nv

Copy link
Copy Markdown
Collaborator

Refactor: split commands/benchmark/execute.py into cohesive modules

Branch: refactor/execute-split · Scope: strictly behavior-preserving · Status: ready for review (3 review rounds complete, all findings addressed)

Why

src/inference_endpoint/commands/benchmark/execute.py had grown to 1691 lines and
concentrated almost all CLI benchmark orchestration in one file. Two functions dominated:
_run_benchmark_async (~403 lines) interleaved ZMQ setup, two subprocess launches, HTTP-client
construction, agentic wiring, profiling triggers, the session run, and a ~110-line finally
doing five unrelated teardown jobs; _score_accuracy (~172 lines) mixed tokenizer loading, uuid
bounding, scoring, response counts, and OSL. Duplicated tmpfs-salvage / AccuracyConfiguration
construction / profile start-stop blocks and best-effort except-sprawl compounded it.

The goal was to split the file along its natural seams without changing any observable
behavior
— same exception types (and therefore exit codes), same on-disk artifacts, same ZMQ
connect-before-bind and teardown ordering, same QPS/report semantics, same tmpfs ownership.

What changed

execute.py split into four modules under src/inference_endpoint/commands/benchmark/:

Module Lines Responsibility
execute.py 1691 → 1076 Thin orchestrator: setup_benchmark / run_benchmark_async / finalize_benchmark / run_benchmark, _run_benchmark_async, _build_phases, _load_datasets, _resolve_accuracy_components, _PerfPhaseTimeout, BenchmarkContext/BenchmarkResult/ResponseCollector, and new helpers (_create_issuer, _build_agentic_strategy, _wire_on_sample_complete, _write_report_artifacts, _summarize_and_log_metrics)
profiling.py (new) 222 Profiler-trigger protocol (_derive_profile_urls, _post_profile, _render_profile_status, _write_profiling_section) + new ProfileController (owns URL derivation + start/stop/payload lifecycle)
accuracy.py (new) 405 AccuracyConfiguration, _phase_osl_stats, _phase_response_counts, _accuracy_uuid_bound, _score_accuracy, new _load_osl_backend, new write_accuracy_results
pipeline.py (new) 345 new MetricsPipeline (ZMQ + publisher + subscriber + aggregator/event-logger subprocess lifecycle) + _build_aggregator_args, _build_event_logger_args, _build_report_from_snapshot, _load_final_snapshot_from_disk

Two new abstractions

  • ProfileController (profiling.py) — collapses the profile URL pre-derivation, the
    /start_profile (PERFORMANCE-phase-only) fire, the /stop_profile (only for status==200
    starts) fire, and the {engine, starts, stops} payload build into one object. Disabled and
    inert when engine is None; raises up-front (fail-before-run) when an engine is set but
    endpoints are empty.
  • MetricsPipeline (pipeline.py) — owns the ZMQ context, event publisher, snapshot
    subscriber, and the two service subprocesses via explicit lifecycle methods — deliberately
    not an async context manager (an early design that a review rejected). The run has three
    distinct teardown paths a single __aexit__ can't express cleanly:
    • start() — bring up, unwinding partial resources if service launch fails (no ZMQ leak);
    • drain_and_build_report() — graceful drain (publisher close → wait for services → source the
      final snapshot → build the Report), run on both clean-finish and session-failure;
    • abort() — connect-failure fast path: kill services without a graceful drain.
      close() exits the ZMQ scope idempotently.

Import contracts preserved

Everything commands/audit.py imports from execute (BenchmarkResult, TestMode,
_salvage_tmpfs, finalize_benchmark, run_benchmark_async, setup_benchmark) and cli.py's
imports (resolve_report_dir, run_benchmark) stay in executeaudit.py, cli.py, the
package __init__, and the TEST04 audit tests are unchanged.
Runtime import direction is
one-way (execute → {profiling, accuracy, pipeline}); the sibling modules reference
BenchmarkContext only under TYPE_CHECKING, so there is no cycle.

Three unit test files were repointed to the new module paths (imports / mock.patch targets
only — no assertion changes): test_benchmark.py, test_score_accuracy.py,
test_benchmark_final_snapshot.py.

Review process (3 rounds, Codex + Claude in parallel each round)

Every round ran two independent reviewers against the diff vs main; each finding was fixed and
re-verified before the next round.

# Finding Sev Reviewer Fix
F1 Publisher double-closed on drain path (safe — close() is idempotent — but violated the null-after-drain invariant) Low Claude R1 Null publisher/subscriber after closing in drain_and_build_report
F2 ProfileController built before the run try, so a misconfig ValueError bypassed tmpfs/pbar cleanup Low Claude R1 Construct it as the first statement inside the try (also closes a latent service-subprocess leak that existed in the original)
F3 tmpfs-salvage vs pbar/zmq ordering on the failure path Info Claude R1 Confirmed benign — no change
F4 tmpfs/event-log/metrics mkdirs ran before the cleanup try; a mkdir failure after tmpfs_dir existed would leak it Med Codex R1 Compute paths before the try, move the mkdirs inside it
F5 Flat finally: pbar.close(); pipe.close() — if pbar.close() raised, ZMQ leaked and tmpfs wasn't salvaged (the original guaranteed both via the ZMQ with) Med Codex R2 Nested try: <body> finally: (try: pbar.close() finally: pipe.close()) wrapped by the outer except BaseException salvage
Stale execute_mod comment/var name in test_score_accuracy.py Nit Claude R3 Renamed to scoring_mod, comment corrected

Round 3 verdict (both reviewers): clean / ready to commit. Both traced the ZMQ scope exiting
exactly once on all five teardown paths (clean success, session ExecutionError, connect
SetupErrorabort(), launcher.launch raising, mid-run KeyboardInterrupt), confirmed
pipe.close() survives a pbar.close() failure, that cleanup exceptions route to tmpfs salvage,
and that no result/profiler/publisher/http_client is possibly-unbound at any reachable use.

Verification

This was developed on macOS, where a chunk of the suite fails for
platform reasons unrelated to this change (pin_loadgen() raises
UnsupportedPlatformError on darwin; the metrics-aggregator/sched_* paths are
Linux-only). To separate refactor regressions from environment noise, every suite
was run both with the change and against a clean main tree (git stash) — the
pass/fail profile is identical, so the refactor adds zero failures.

  • Affected unit tests (test_benchmark, test_score_accuracy,
    test_benchmark_final_snapshot, compliance test_output_caching): 233 passed
    these all run on macOS and cover the moved code directly.
  • Full unit suite: main 58 failed / 1542 passed → this branch 58 failed / 1542
    passed
    (identical). All 58 failures are in metrics_aggregator/ (44) and
    endpoint_client/test_cpu_affinity.py (14) — untouched modules that fail on macOS
    only; none is in commands/benchmark/.
  • Integration commands/ (drives run_benchmark end-to-end): main 29 failed / 8
    passed → this branch 29 failed / 8 passed (identical). The failures are the
    pin_loadgen()-requires-Linux path, which the original setup_benchmark invoked the
    same way.
  • End-to-end smoke (echo server, --no-cpu-affinity): the full refactored path runs
    clean and writes every artifact — config.yaml, events.jsonl, sample_idx_map.json,
    report.txt, metrics/final_snapshot.json, performance/result_summary.json
    (qps≈10630, n_samples_issued=7000, 7000/7000 successful, state=complete,
    complete=True). Confirms MetricsPipeline bring-up + drain (final_snapshot.json
    sourcing → Report), tmpfs event-log salvage, and clean teardown.
  • mypy: clean on all four files. (The pre-commit mypy hook reports 3 errors in
    cpu_affinity.py / token_metrics.py — the same macOS-only sched_setaffinity /
    sched_getaffinity false positives in untouched files; they pass in CI/Linux, and are
    why the hook needs --no-verify locally on macOS.)
  • ruff / ruff-format / license headers / whitespace: all pass.

Note on CI: these platform failures are macOS-only. The full suite should be run on
Linux (CI) to confirm a green board; locally on macOS the baseline-diff above is the
meaningful signal.

Reproduce

# The moved code, all macOS-runnable:
uv run pytest tests/unit/commands/test_benchmark.py tests/unit/commands/test_score_accuracy.py \
  tests/unit/commands/test_benchmark_final_snapshot.py tests/unit/compliance/test_output_caching.py  # 233 pass
# Full suite on Linux/CI for a green board; on macOS compare against a stashed `main`
# tree instead (identical failure profile = no regression):
uv run pytest                                              # Linux/CI
uv run pre-commit run --files src/inference_endpoint/commands/benchmark/{execute,profiling,accuracy,pipeline}.py
# echo-server smoke — report dir gets config.yaml, events.jsonl, sample_idx_map.json,
# metrics/final_snapshot.json, performance/result_summary.json, report.txt.
# On macOS pass --no-cpu-affinity (pin_loadgen is Linux-only); dummy_1k.jsonl needs parser.prompt.
uv run python -m inference_endpoint.testing.echo_server --port 8765 &
uv run inference-endpoint benchmark offline --endpoints http://localhost:8765 --model test-model \
  --dataset "tests/assets/datasets/dummy_1k.jsonl,samples=40,parser.prompt=text_input" \
  --workers 2 --no-cpu-affinity --max-connections 16

What to focus on in review

  1. MetricsPipeline teardown (pipeline.py) and its three call sites in
    execute._run_benchmark_async — the ZMQ-scope-exactly-once invariant across all five paths.
  2. The nested cleanup structure in _run_benchmark_async (F5) — that pipe.close() always
    runs and cleanup failures still salvage tmpfs.
  3. accuracy._score_accuracy / write_accuracy_results equivalence to the original inline
    scoring + accuracy_results.json block (OSL fast-backend gating, lazy uuid_to_text, numpy
    coercion, finalize write order: report artifacts before accuracy results).

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅

@github-actions
github-actions Bot requested a review from nvzhihanj July 17, 2026 02:42

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the benchmark execution logic by modularizing cohesive sub-concerns from execute.py into sibling modules: accuracy.py for scoring, pipeline.py for managing the metrics and event-log service pipeline, and profiling.py for handling profiler triggers. Unit tests have been updated accordingly to reflect these new module boundaries. The review feedback highlights three robust error-handling improvements: ensuring background subprocesses are killed if the pipeline is closed with an active publisher, wrapping the _salvage_tmpfs call in a try-except block to prevent masking original exceptions during cleanup, and adding exception handling around self.subscriber.close() during the graceful drain process.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/inference_endpoint/commands/benchmark/pipeline.py Outdated
Comment on lines 870 to 871
_salvage_tmpfs(ctx.report_dir, tmpfs_dir)
shutil.rmtree(tmpfs_dir, ignore_errors=True)

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

During exception handling in the except BaseException: block, if _salvage_tmpfs raises an exception (e.g., due to disk full or permission issues during shutil.copy2), it will propagate and mask the original exception, while also skipping the shutil.rmtree cleanup. Wrapping the salvage call in a try...except block ensures that cleanup always completes and the original exception is preserved.

            try:
                _salvage_tmpfs(ctx.report_dir, tmpfs_dir)
            except Exception as e:
                logger.warning(f"Failed to salvage tmpfs: {e}")
            shutil.rmtree(tmpfs_dir, ignore_errors=True)

Comment thread src/inference_endpoint/commands/benchmark/pipeline.py Outdated
…/pipeline

execute.py concentrated all CLI benchmark orchestration; the large
_run_benchmark_async and _score_accuracy dominated its complexity. Split along
natural seams into four cohesive modules:

- profiling.py (new): profile-trigger protocol (vLLM /start_profile,/stop_profile)
  + ProfileController (URL derivation + start/stop/payload lifecycle)
- accuracy.py (new): AccuracyConfiguration, _score_accuracy, _load_osl_backend,
  write_accuracy_results
- pipeline.py (new): MetricsPipeline — ZMQ + metrics-aggregator/event-logger
  subprocess lifecycle (start/drain_and_build_report/abort/close) + snapshot→Report
- execute.py: thin orchestrator

Everything audit.py/cli.py import stays in execute (TestMode, BenchmarkResult,
_salvage_tmpfs, setup/run/finalize, run_benchmark, resolve_report_dir); one-way
import graph (execute → {profiling, accuracy, pipeline}). Three unit test files
repointed.

Rebased onto main after the SWE-bench scorer (#342). #342's execute.py accuracy
changes are routed into their post-split homes: AccuracyConfiguration
model_params/endpoint_config fields, _effective_external_sample_count,
_accuracy_uuid_bound (None-on-unavailable), and SKIP_ENDPOINT_PHASE handling in
_score_accuracy land in accuracy.py; _validate_accuracy_config_for_scorer, the
external-scorer skips/logging, and the dataset-loader/preflight wiring land in
execute.py.

Also restores the early-stopping aggregator flag the split had dropped:
"--early-stopping" is now threaded through pipeline._build_aggregator_args (gated
on settings.early_stopping.enabled), replacing an orphaned append to a variable
that no longer existed in _run_benchmark_async after the split.

Verified: unit suite green except pre-existing macOS-only failures
(sched_setaffinity / IPC-socket-path in cpu_affinity/token_metrics/metrics_aggregator,
none in commands/benchmark). ruff/ruff-format/prettier/license clean; mypy clean on
the changed files (remaining mypy errors are the macOS-only os.sched_* attr-defined
false-positives in untouched files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arekay-nv
arekay-nv force-pushed the refactor/execute-split branch from 85c7f5d to 57d9a66 Compare July 28, 2026 03:07
arekay-nv and others added 2 commits July 27, 2026 22:38
A setup failure between MetricsPipeline.start() (which launches the aggregator
+ event-logger subprocesses) and session.run — e.g. _build_phases rejecting an
agentic accuracy dataset, or BenchmarkSession construction raising — never
reached the graceful drain, and the finally called only pipe.close()
(_teardown(kill=False)). The launched subprocesses were thus neither drained
nor killed; with the aggregator drain-timeout defaulting to unlimited they
could linger indefinitely waiting for an ENDED that never arrives.

Track whether the drain ran; if not, the finally now aborts (kill=True) so the
services are killed. The connect-failure path already did this via pipe.abort()
for SetupError; this extends the guarantee to every other post-start() setup
failure. _teardown is idempotent (_closed guard), so the existing
SetupError->abort path double-calls abort() harmlessly.

Pre-existing latent gap (the monolith's ZMQ `with` __exit__ likewise never
killed the launcher), surfaced by the review council on the execute.py split.
Adds a regression test that drives a post-launch setup failure and asserts
kill_all() runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The setup-failure abort added in the previous commit runs `pipe.abort()` →
`_teardown(kill=True)` from a `finally` while an exception is propagating.
`kill_all()` sat unguarded between the try/except-wrapped publisher and
subscriber closes; a raise there (e.g. a child exiting between `poll()` and
`kill()`) would mask the original setup error and skip the ZMQ `__exit__` —
and since `_teardown` already set `_closed`, the trailing `pipe.close()` would
no-op, leaking the ZMQ scope (violating the finally's "close must always run"
invariant).

Wrap `kill_all()` in the same best-effort try/except as the neighbouring
closes so teardown always reaches the ZMQ `__exit__` and never masks the
in-flight exception. Surfaced by the review council (Cursor + Claude) on the
prior fix.

Also adds a companion test: a SetupError after launch hits both abort() sites
(explicit except + finally); asserts kill_all runs exactly once, locking in the
`_closed` idempotency guard the finally-abort depends on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arekay-nv
arekay-nv marked this pull request as ready for review July 28, 2026 23:53
@arekay-nv
arekay-nv requested a review from a team July 28, 2026 23:53
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@arekay-nv
arekay-nv requested a review from nv-alicheng July 28, 2026 23:53

@nv-alicheng nv-alicheng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Council — Multi-AI Code Review

Reviewed by: Claude + Code-Quality | Depth: thorough

See the summary comment for the tiered breakdown. codex was unavailable in this environment, so this is a Claude + Code-Quality review.

Comment thread src/inference_endpoint/commands/benchmark/execute.py Outdated
Comment thread src/inference_endpoint/commands/benchmark/execute.py Outdated
)
self._starts.append(rec)

def stop(self, completed_normally: bool) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Claude] medium (testing): ProfileController.start/stop have no coverage. Every _run_benchmark_async test in test_benchmark.py raises before session.run completes, so profiler.start() (execute.py:897) and profiler.stop() (execute.py:912) never execute. The refactor moved real branching into stop() — the non-200 / i >= len(self._stop_urls) skip (line 195) and the stop_reason abort-vs-phase_end selection (line 193) — none of it exercised. Add a direct ProfileController unit test covering the start→stop index mapping.

Comment thread src/inference_endpoint/commands/benchmark/profiling.py Outdated
and load_pattern.use_legacy_loadgen_qps_metrics
),
)
if not report.complete:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Claude] low (testing): _build_report_from_snapshot moved two warning branches out of the monolith that are now untested: the incomplete-report warning (if not report.complete:, 170-175) and the legacy-loadgen-QPS deprecation warning (176-183), plus the swallow-to-None except at 185. test_benchmark_final_snapshot.py only tests _load_final_snapshot_from_disk; _build_report_from_snapshot is never called from tests.

# Wall-clock of just this phase's tokenization (seconds);
# summed across datasets for the accuracy report's total.
entry["osl_tokenize_s"] = round(time.perf_counter() - t0, 3)
except Exception as e: # noqa: BLE001 - optional blocks; never fail scoring

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Claude] low (testing): In the moved _score_accuracy, the get_raw_outputs build-once branch (342-355) and the broad except Exception that drops response-counts/OSL without failing scoring (370-375) are load-bearing. test_score_accuracy.py drives _score_accuracy with fake backends but no test forces get_raw_outputs/tokenize to raise and asserts the entry is still appended with score present and the counts/OSL keys simply absent — the docstring's 'a read failure only drops these blocks, never fails scoring' invariant is unverified.

Comment thread src/inference_endpoint/commands/benchmark/profiling.py Outdated
Comment thread src/inference_endpoint/commands/benchmark/profiling.py Outdated
Comment thread src/inference_endpoint/commands/benchmark/execute.py Outdated
Comment thread src/inference_endpoint/commands/benchmark/accuracy.py Outdated
@nv-alicheng

Copy link
Copy Markdown
Collaborator

Review Council — Multi-AI Code Review

Reviewed by: Claude + Code-Quality | Depth: thorough
(codex CLI was unavailable in this environment — Claude + Code-Quality only.)

Found 12 issues across 4 files. The refactor is largely faithful — imports, call sites, operation ordering, and error paths are preserved across the split. No critical/high defects; findings are testing gaps, maintainability debt on the new module boundaries, and one dead accessor. 11 posted inline; 1 (execute.py:1082) is summary-only (outside the diff hunk).

🔴 Must Fix (critical/high)

None.

🟡 Should Fix (medium)

# File Line Category Reviewer Summary
1 benchmark/execute.py 55 code-quality Code-Quality _effective_external_sample_count/_score_accuracy are now cross-module API but kept _-private — rename public (shared root cause with #2)
2 benchmark/execute.py 62 code-quality Code-Quality _write_profiling_section is de facto public (called at execute.py:1027) — drop the underscore
3 benchmark/profiling.py 186 testing Claude ProfileController.start/stop never execute in the suite (all _run_benchmark_async tests raise first); stop() branching untested

🔵 Consider (low)

# File Line Category Reviewer Summary
4 benchmark/profiling.py 170 design Claude ProfileController.enabled is dead code (no references) — wire in or drop
5 benchmark/pipeline.py 170 testing Claude _build_report_from_snapshot warning branches + swallow-to-None except untested
6 benchmark/pipeline.py 300 testing Claude drain_and_build_report subscriber-fallback (SIGKILL/OOM recovery path) untested
7 benchmark/accuracy.py 370 testing Claude _score_accuracy broad-except "drop counts, never fail scoring" invariant unverified
8 benchmark/profiling.py 47 code-quality Code-Quality _derive_profile_urls(action: str) stringly-typed — use Literal["start","stop"]
9 benchmark/profiling.py 82 code-quality Code-Quality urlopen(timeout=2) magic constant — name it
10 benchmark/execute.py 767 code-quality Code-Quality _wire_on_sample_complete(publisher: Any) — annotate as EventPublisherService
11 benchmark/accuracy.py 58 code-quality Code-Quality AccuracyConfiguration never mutated — @dataclass(frozen=True)
12 benchmark/execute.py 1082 error-handling Claude profiling.json write is non-atomic/unguarded and first in finalize_benchmark — an OSError sinks the already-built perf report. Moved verbatim (not a regression). (outside diff hunk — not postable inline)

Cross-cutting note: #1/#2 share a root cause — the split left helpers that are now cross-module API with private (_) names. Renaming them public is the durable fix; the rename spans importer + definition + call sites, so no inline suggestion was attached.

Existing gemini-code-assist bot comments (publisher-leak on abnormal close(), _salvage_tmpfs masking the original exception, subscriber.close() unguarded in drain) were not duplicated — they cover different lines/concerns and remain valid.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Comment thread src/inference_endpoint/commands/benchmark/pipeline.py Outdated
Comment thread src/inference_endpoint/commands/benchmark/execute.py Outdated
arekay-nv and others added 4 commits July 29, 2026 21:06
…eanups

Address PR #421 review comments (renames + one-liners; no behavior change):

- Rename cross-module helpers public (drop the `_` prefix now that the
  execute.py split imports them across a package boundary):
  effective_external_sample_count, score_accuracy, write_profiling_section
- profiling: type `action` as Literal["start", "stop"]; name the profile-POST
  timeout constant (_PROFILE_POST_TIMEOUT_S); drop the dead
  ProfileController.enabled property
- execute: annotate _wire_on_sample_complete `publisher` as EventPublisherService
  (TYPE_CHECKING import)
- accuracy: mark AccuracyConfiguration frozen=True (build-once/read-many)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rors

In run_benchmark_async's teardown finally, an exception raised by pbar.close()
(e.g. BrokenPipeError on a closed stderr) would replace the exception being
unwound — and on an otherwise-clean run would propagate to the outer
except → tmpfs salvage, turning a successful benchmark into a failure that
never returns its BenchmarkResult. Catch and log it; the surrounding finally
still guarantees pipe.abort()/pipe.close() run.

Addresses PR #421 Codex review comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hand-rolled start()/abort()/close()/_teardown() lifecycle with a
real async context manager, addressing PR #421 review (r3677374272 CM smell +
the _teardown BaseException-leak it hides).

pipeline.py:
- __aenter__ = start(); __aexit__ releases via a contextlib.ExitStack that owns
  the ZMQ scope + publisher/subscriber closers, so every step runs even under a
  BaseException (Ctrl-C/SystemExit) mid-teardown and exceptions chain — no more
  skipped ZMQ __exit__ or leaked service children. No hand-called __enter__/__exit__.
- drain_and_build_report() nulls self.publisher on success; __aexit__ reads that
  as the drained-cleanly signal to release-only, else kills the services (they'd
  otherwise linger on the aggregator's unlimited drain-timeout).
- Removed abort()/close()/_teardown()/_zmq_cm/_closed; added _close_publisher/
  _close_subscriber ExitStack callbacks.

execute.py:
- Consumer uses `async with pipe:`; drops the `drained` flag, the explicit
  `except SetupError: pipe.abort()`, and the `if not drained: abort(); close()`
  finally. Setup/connect/session failures skip the drain, so __aexit__ kills.

Closes the hvagadia _teardown/BaseException high, the Gemini subscriber.close-in-
drain and publisher-leak items, and the CM-smell design comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…filer/pipeline gaps

Address the remaining PR #421 review items.

Correctness (execute.py):
- _salvage_tmpfs is now best-effort at both sites (the run_benchmark_async
  except-BaseException and the run_benchmark finally): a raising salvage no
  longer masks the exception being unwound or skips the rmtree cleanup.
- profiling.json is written after the report artifacts and guarded (OSError →
  warn), so it can no longer be first-and-unguarded and sink the built perf
  report on an IO error.

Tests:
- ProfileController.start/stop: start→stop index mapping, non-200 start skips
  its stop, abort vs phase_end stop_reason, disabled no-op, stop-without-start.
- _build_report_from_snapshot: incomplete-report and legacy-QPS warnings, and
  the swallow-to-None on a malformed snapshot.
- drain_and_build_report snapshot fallback: disk-missing → subscriber.latest,
  and no-snapshot → None (the SIGKILL/OOM recovery path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread tests/unit/commands/test_benchmark_final_snapshot.py Fixed
arekay-nv and others added 3 commits July 29, 2026 23:25
…ver __aexit__ policy

Address review-council findings on the execute.py/pipeline.py split.

- execute.py: bind http_client at function scope and shut it down in the outer
  finally (which always runs) instead of only in the session finally. A setup
  error after _create_issuer (e.g. _build_agentic_strategy/_build_phases raising)
  previously skipped the session finally and leaked the client's worker
  subprocesses; shutdown_async() is idempotent so the clean-path call is a no-op.
- execute.py: make the end-of-run drain best-effort so a teardown error
  (publisher.close / wait_for_exit) can't replace the run's in-flight exception
  on the session-failure path (matches the sibling client-shutdown guard).
- AGENTS.md: MetricsPipeline is an async context manager now — drop the stale
  abort/close from its Code Organization entry.

Tests:
- HTTP client is shut down when setup raises after _create_issuer.
- __aexit__ kill policy: clean drain (publisher nulled) does NOT kill the
  services; never-drained (publisher still set) kills exactly once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review-council follow-ups on the execute.py split.

- execute.py: the end-of-run drain except is now conditional. On a clean run
  (session_completed_normally) a drain / report-build failure propagates so the
  run fails loudly — an unconditional swallow would exit 0 with report=None and
  no perf artifacts. On the session-failure path it still swallows, so a teardown
  error can't replace the run's in-flight exception. (Refines the earlier
  best-effort-drain change, which over-corrected.)
- test_benchmark.py: a clean session with a failing drain propagates the error.
- test_benchmark_final_snapshot.py: drop the duplicate `import ... as pipeline_mod`
  (kept the `from ... import` style) and patch via string targets — resolves the
  dual-import lint on the file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fail clean benchmark sessions when metrics draining yields no usable report instead of exiting successfully without performance artifacts.

Run service termination through the pipeline ExitStack so publisher, subscriber, and ZMQ cleanup still execute when teardown is interrupted. Add regressions for missing reports and repeated interrupts during both normal and partial-start teardown.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants