refactor(benchmark): split execute.py into execute/profiling/accuracy/pipeline#421
refactor(benchmark): split execute.py into execute/profiling/accuracy/pipeline#421arekay-nv wants to merge 2 commits into
Conversation
…/pipeline
execute.py (1691 ln) concentrated all CLI benchmark orchestration; the ~403-line
_run_benchmark_async and ~172-line _score_accuracy dominated its complexity. Split
along natural seams into four cohesive modules, strictly behavior-preserving:
- 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 (explicit start/drain_and_build_report/abort/close) + snapshot→Report
- execute.py: thin orchestrator (1691 → 1076 ln)
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
(imports / mock.patch targets only — no assertion changes).
Verified: 233 affected unit tests pass; full unit and integration/commands failure
profiles are identical to main (macOS-only pin_loadgen/sched_setaffinity/aggregator-socket
failures, none in commands/benchmark); e2e echo-server smoke writes all run artifacts
(final_snapshot.json, result_summary.json, report.txt; qps≈10630, 7000/7000 successful,
state=complete). mypy/ruff/ruff-format/license clean on the four files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅ |
There was a problem hiding this comment.
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.
| if self.publisher is not None: | ||
| try: | ||
| self.publisher.close() | ||
| except Exception as e: # noqa: BLE001 — teardown best-effort | ||
| logger.warning("Publisher close error: %s", e) | ||
| if kill and self._launcher is not None: | ||
| self._launcher.kill_all() |
There was a problem hiding this comment.
If the pipeline is closed without having successfully completed drain_and_build_report (for example, due to an initialization error or an unhandled exception during the run), self.publisher will still be active. In this case, close() is called with kill=False, which prevents self._launcher.kill_all() from running and leaks the background service subprocesses. Forcing kill = True when self.publisher is not None ensures that active subprocesses are always cleaned up on abnormal termination.
| if self.publisher is not None: | |
| try: | |
| self.publisher.close() | |
| except Exception as e: # noqa: BLE001 — teardown best-effort | |
| logger.warning("Publisher close error: %s", e) | |
| if kill and self._launcher is not None: | |
| self._launcher.kill_all() | |
| if self.publisher is not None: | |
| kill = True | |
| try: | |
| self.publisher.close() | |
| except Exception as e: # noqa: BLE001 — teardown best-effort | |
| logger.warning("Publisher close error: %s", e) | |
| if kill and self._launcher is not None: | |
| self._launcher.kill_all() |
| _salvage_tmpfs(ctx.report_dir, tmpfs_dir) | ||
| shutil.rmtree(tmpfs_dir, ignore_errors=True) |
There was a problem hiding this comment.
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)| # re-close them (close() is idempotent, but this keeps the invariant clean). | ||
| self.publisher = None | ||
| if self.subscriber is not None: |
There was a problem hiding this comment.
In drain_and_build_report, self.subscriber.close() is called without exception handling. If closing the subscriber raises an exception, it will abort the graceful drain and prevent the benchmark report from being built. Wrapping it in a try...except block (similar to the implementation in _teardown) ensures robust finalization.
| # re-close them (close() is idempotent, but this keeps the invariant clean). | |
| self.publisher = None | |
| if self.subscriber is not None: | |
| if self.subscriber is not None: | |
| try: | |
| self.subscriber.close() | |
| except Exception as e: | |
| logger.warning("Subscriber close error during drain: %s", e) | |
| self.subscriber = None |
Refactor: split
commands/benchmark/execute.pyinto cohesive modulesBranch:
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.pyhad grown to 1691 lines andconcentrated almost all CLI benchmark orchestration in one file. Two functions dominated:
_run_benchmark_async(~403 lines) interleaved ZMQ setup, two subprocess launches, HTTP-clientconstruction, agentic wiring, profiling triggers, the session run, and a ~110-line
finallydoing five unrelated teardown jobs;
_score_accuracy(~172 lines) mixed tokenizer loading, uuidbounding, scoring, response counts, and OSL. Duplicated tmpfs-salvage /
AccuracyConfigurationconstruction / 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.pysplit into four modules undersrc/inference_endpoint/commands/benchmark/:execute.pysetup_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)_derive_profile_urls,_post_profile,_render_profile_status,_write_profiling_section) + newProfileController(owns URL derivation + start/stop/payload lifecycle)accuracy.py(new)AccuracyConfiguration,_phase_osl_stats,_phase_response_counts,_accuracy_uuid_bound,_score_accuracy, new_load_osl_backend, newwrite_accuracy_resultspipeline.py(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_diskTwo new abstractions
ProfileController(profiling.py) — collapses the profile URL pre-derivation, the/start_profile(PERFORMANCE-phase-only) fire, the/stop_profile(only forstatus==200starts) fire, and the
{engine, starts, stops}payload build into one object. Disabled andinert when
engine is None; raises up-front (fail-before-run) when an engine is set butendpoints are empty.
MetricsPipeline(pipeline.py) — owns the ZMQ context, event publisher, snapshotsubscriber, 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 thefinal 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.pyimports fromexecute(BenchmarkResult,TestMode,_salvage_tmpfs,finalize_benchmark,run_benchmark_async,setup_benchmark) andcli.py'simports (
resolve_report_dir,run_benchmark) stay inexecute—audit.py,cli.py, thepackage
__init__, and the TEST04 audit tests are unchanged. Runtime import direction isone-way (
execute → {profiling, accuracy, pipeline}); the sibling modules referenceBenchmarkContextonly underTYPE_CHECKING, so there is no cycle.Three unit test files were repointed to the new module paths (imports /
mock.patchtargetsonly — 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 andre-verified before the next round.
close()is idempotent — but violated the null-after-drain invariant)publisher/subscriberafter closing indrain_and_build_reportProfileControllerbuilt before the runtry, so a misconfigValueErrorbypassed tmpfs/pbar cleanuptry(also closes a latent service-subprocess leak that existed in the original)mkdirs ran before the cleanuptry; a mkdir failure aftertmpfs_direxisted would leak ittry, move themkdirs inside itfinally: pbar.close(); pipe.close()— ifpbar.close()raised, ZMQ leaked and tmpfs wasn't salvaged (the original guaranteed both via the ZMQwith)try: <body> finally: (try: pbar.close() finally: pipe.close())wrapped by the outerexcept BaseExceptionsalvageexecute_modcomment/var name intest_score_accuracy.pyscoring_mod, comment correctedRound 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, connectSetupError→abort(),launcher.launchraising, mid-runKeyboardInterrupt), confirmedpipe.close()survives apbar.close()failure, that cleanup exceptions route to tmpfs salvage,and that no
result/profiler/publisher/http_clientis possibly-unbound at any reachable use.Verification
test_benchmark,test_score_accuracy,test_benchmark_final_snapshot, compliancetest_output_caching): 233 passed —these all run on macOS and cover the moved code directly.
main58 failed / 1542 passed → this branch 58 failed / 1542passed (identical). All 58 failures are in
metrics_aggregator/(44) andendpoint_client/test_cpu_affinity.py(14) — untouched modules that fail on macOSonly; none is in
commands/benchmark/.commands/(drivesrun_benchmarkend-to-end):main29 failed / 8passed → this branch 29 failed / 8 passed (identical). The failures are the
pin_loadgen()-requires-Linux path, which the originalsetup_benchmarkinvoked thesame way.
--no-cpu-affinity): the full refactored path runsclean 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). ConfirmsMetricsPipelinebring-up + drain (final_snapshot.jsonsourcing → Report), tmpfs event-log salvage, and clean teardown.
mypyhook reports 3 errors incpu_affinity.py/token_metrics.py— the same macOS-onlysched_setaffinity/sched_getaffinityfalse positives in untouched files; they pass in CI/Linux, and arewhy the hook needs
--no-verifylocally on macOS.)Reproduce
What to focus on in review
MetricsPipelineteardown (pipeline.py) and its three call sites inexecute._run_benchmark_async— the ZMQ-scope-exactly-once invariant across all five paths._run_benchmark_async(F5) — thatpipe.close()alwaysruns and cleanup failures still salvage tmpfs.
accuracy._score_accuracy/write_accuracy_resultsequivalence to the original inlinescoring +
accuracy_results.jsonblock (OSL fast-backend gating, lazyuuid_to_text, numpycoercion, finalize write order: report artifacts before accuracy results).
🤖 Generated with Claude Code