Skip to content

test(profiling): verify the TraceLens and dynolog tools without a GPU - #170

Open
Cemberk wants to merge 1 commit into
cem/tracelens-integrationfrom
cem/tracelens-dummy-tests
Open

test(profiling): verify the TraceLens and dynolog tools without a GPU#170
Cemberk wants to merge 1 commit into
cem/tracelens-integrationfrom
cem/tracelens-dummy-tests

Conversation

@Cemberk

@Cemberk Cemberk commented Aug 12, 2026

Copy link
Copy Markdown

Summary

Stacked on #167 and targets its branch, so merging this folds the commit into that PR.

Neither tool added in #167 could be exercised in CI: TraceLens pins protobuf and xprof and reports only on recorded GPU traces, and dynolog ships as a GitHub release asset and attaches to a running PyTorch workload. This adds stand-ins for both, so the real scripts run against fabricated trace artifacts with no GPU, no Docker, no network, and no TraceLens install.

  • tests/fixtures/dummy_tracelens/ — a stand-in TraceLens package placed on PYTHONPATH. Its seven report entry points declare exactly the flags madengine passes, fail loudly if one is renamed or dropped, write the report files they promise, and log every invocation.
  • tests/e2e/test_tracelens_dummy_pipeline.py (20 tests) — drives the real analyzer, the real in-container post-script, and real madengine report tracelens: per-kind routing, report layout, the summary CSV, the multi-rank collective report, .db skip guidance, env-var config, and non-fatality.
  • tests/e2e/test_dynolog_dummy_pipeline.py (11 tests) — dummy dyno/dynolog binaries plus the real start/trigger/stop scripts: daemon lifecycle, the retry loop that waits for the workload to register, the request flags, and the give-up diagnostics.

Two defects the new tests found, both fixed here:

  • The analyzer's module fallback ran main() and discarded its return value, so whenever TraceLens' console scripts were absent every failed report was recorded as SUCCESS. It now exits with that value, the way pip's console scripts do.
  • Stopping dynolog burned its full 20s grace period and then claimed the daemon "did not stop gracefully". Both processes are reparented to PID 1 when the pre-script exits, and a container's PID 1 is the model command rather than an init that reaps children, so a terminated process lingers as a zombie that kill -0 still reports as alive. Real runs save 20s each.

Substituting TraceLens is not only convenience: the real one reports only on kernels it can link back to the runtime calls that launched them, so no fabricated trace produces a report however well shaped. Real analysis stays covered by the GPU-gated tests in test_tracelens_workflows.py.

Test plan

  • pytest tests/e2e/test_tracelens_dummy_pipeline.py tests/e2e/test_dynolog_dummy_pipeline.py — 31 passed (Linux, Python 3.12)
  • Full TraceLens and dynolog suites — 103 passed, 4 skipped (GPU / real-TraceLens gated)
  • Real TraceLens installed in a container from the pinned commit by the actual pre-script: all seven entry points present, and all 27 flags madengine passes accepted by the real CLI
  • black, isort, and flake8 clean at the repo-pinned versions
  • On a GPU node: the GPU-gated tests in tests/e2e/test_tracelens_workflows.py

The new tests are POSIX-only (the in-container half is shell) and skip on Windows.

Neither tool could be exercised in CI: TraceLens pins protobuf and xprof and
reports only on recorded GPU traces, and dynolog ships as a GitHub release asset
and attaches to a running PyTorch workload. Stand-ins for both now let the real
scripts run against fabricated trace artifacts, covering the half of the
integration madengine owns -- which report generator each trace kind is routed
to, the flags it receives, the retry loop that waits for the workload to
register, and the guarantee that a failed analysis never fails a model run.

Two defects the new tests found:

The analyzer's module fallback ran main() and discarded its return value, so
whenever TraceLens' console scripts were absent every failed report was recorded
as a success. It now exits with that value, the way pip's console scripts do.

Stopping dynolog burned its full 20s grace period and then claimed the daemon
"did not stop gracefully". Both processes are reparented to PID 1 when the
pre-script exits, and a container's PID 1 is the model command rather than an
init that reaps children, so a terminated process lingers as a zombie that
kill -0 still reports as alive.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Cemberk
Cemberk requested a review from gargrahul as a code owner August 12, 2026 22:31
Copilot AI lite review requested due to automatic review settings August 12, 2026 22:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds CPU/CI-friendly end-to-end coverage for the TraceLens and dynolog profiling integrations by substituting dummy tool implementations, and fixes two real defects uncovered by those tests (TraceLens module fallback exit code propagation; dynolog stop behavior around zombies).

Changes:

  • Add a dummy TraceLens Python package fixture (with all expected report entry-point modules) plus a shared implementation that validates madengine’s CLI contract and writes placeholder report outputs.
  • Add POSIX-only e2e tests that run the real madengine analyzer/scripts against fabricated trace artifacts and dummy dynolog binaries (no GPU/Docker/network required).
  • Fix tracelens_analyze.py to propagate exit codes when falling back to module invocation, and update dynolog_stop.sh to avoid waiting out the full grace period on zombie processes.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/fixtures/dummy_tracelens/TraceLens/Reporting/generate_perf_report_rocprof.py Dummy entry-point module for rocprof JSON report generation.
tests/fixtures/dummy_tracelens/TraceLens/Reporting/generate_perf_report_pytorch.py Dummy entry-point module for PyTorch trace report generation.
tests/fixtures/dummy_tracelens/TraceLens/Reporting/generate_perf_report_pftrace_memory_copy.py Dummy entry-point module for pftrace memory copy report generation.
tests/fixtures/dummy_tracelens/TraceLens/Reporting/generate_perf_report_pftrace_hip_api.py Dummy entry-point module for pftrace HIP API report generation.
tests/fixtures/dummy_tracelens/TraceLens/Reporting/generate_perf_report_pftrace_hip_activity.py Dummy entry-point module for pftrace HIP activity report generation.
tests/fixtures/dummy_tracelens/TraceLens/Reporting/generate_multi_rank_collective_report_pytorch.py Dummy entry-point module for multi-rank collective report generation.
tests/fixtures/dummy_tracelens/TraceLens/Reporting/compare_perf_reports_pytorch.py Dummy entry-point module for report comparison workflow.
tests/fixtures/dummy_tracelens/TraceLens/Reporting/_dummy.py Shared dummy TraceLens implementation: strict flag contract + report artifact writing + invocation logging.
tests/fixtures/dummy_tracelens/TraceLens/Reporting/init.py Package init for dummy TraceLens reporting modules.
tests/fixtures/dummy_tracelens/TraceLens/init.py Dummy TraceLens package root used via PYTHONPATH in tests.
tests/e2e/test_tracelens_dummy_pipeline.py End-to-end tests covering analyzer routing, output layout, summary recording, collective report behavior, env-var config, and non-fatal failure handling.
tests/e2e/test_dynolog_dummy_pipeline.py End-to-end tests covering dynolog daemon lifecycle, trigger retry behavior, request flags, and post-run diagnostics using dummy dynolog/dyno.
src/madengine/scripts/common/tools/tracelens_analyze.py Fix module-fallback invocation to exit with main()’s return value (don’t swallow failures).
src/madengine/scripts/common/post_scripts/dynolog_stop.sh Improve stop logic to treat zombies as stopped by checking /proc/<pid>/stat state.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +118 to +126
def cleanup(self) -> None:
"""Kill anything left running and clear the handoff files."""
for pid_file in (TRIGGER_PID_FILE, DYNOLOG_PID_FILE):
if pid_file.is_file():
with contextlib.suppress(ValueError, OSError):
os.kill(int(pid_file.read_text().strip()), signal.SIGKILL)
for path in HANDOFF_FILES:
with contextlib.suppress(OSError):
path.unlink()
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.

2 participants