Skip to content

Add aimx trace distribution with tensor (histogram) output modes#15

Merged
blizhan merged 1 commit into
mainfrom
codex/github-mention-feature-request-trace-distribution-support
Apr 30, 2026
Merged

Add aimx trace distribution with tensor (histogram) output modes#15
blizhan merged 1 commit into
mainfrom
codex/github-mention-feature-request-trace-distribution-support

Conversation

@blizhan

@blizhan blizhan commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Provide a first-class trace distribution path so users can inspect Aim-tracked distribution sequences (histogram/tensor payloads) from the CLI.
  • Enable both human-friendly terminal inspection and script-friendly exports (CSV/JSON) of per-step histogram data.

Description

  • Add DistributionPoint and DistributionSeries types and implement collect_distribution_series(...) in aimx.aim_bridge.metric_stats that uses Repo.query_distributions(...) to collect per-step step, epoch, weights, and bin_edges data.
  • Add step-range filtering and sampling helpers filter_distribution_by_step_range and subsample_distribution, and keep metric helpers unchanged.
  • Extend the trace invocation parser to accept an explicit distribution target and route to a distribution-specific execution path in run_trace_command, preserving existing metric trace behavior.
  • Implement distribution renderers in src/aimx/rendering/trace_views.py: render_distribution_table, render_distribution_csv, and render_distribution_json, plus a small _format_tensor helper and labeling helper; update metric render helpers to coexist.
  • Update CLI help and README.md with trace distribution usage examples and add unit tests for parsing and renderers (tests/unit/test_trace_distribution_views.py) and adjust existing trace parsing tests.

Testing

  • Ran unit tests: uv run pytest tests/unit/test_trace_helpers.py tests/unit/test_trace_distribution_views.py tests/unit/test_router.py tests/unit/test_owned_commands.py, which completed successfully with 32 passed.
  • Ran a focused integration/contract subset: uv run pytest tests/integration/test_trace_command.py tests/contract/test_trace_contract.py, which completed with 1 passed, 12 skipped due to the sample-repo fixture behavior.
  • All automated tests executed for these changes passed or were skipped as expected.

Codex Task

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

Code Review

This pull request introduces support for Aim distribution sequences, allowing users to fetch and render histogram weights via the aimx trace distribution command. Key additions include new data models for distributions, collection logic from the Aim repository, and rendering capabilities for terminal tables, CSV, and JSON outputs. The review feedback highlights opportunities to optimize step range filtering into a single pass and suggests refactoring the command execution logic to reduce duplication between metric and distribution processing paths.

Comment on lines +344 to +347
if start is not None:
points = [point for point in points if point.step >= start]
if end is not None:
points = [point for point in points if point.step <= end]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The step range filtering can be optimized to a single pass. This avoids creating an intermediate list when both start and end are provided, which improves performance for series with many points.

    if start is not None or end is not None:
        points = [
            point for point in points
            if (start is None or point.step >= start) and (end is None or point.step <= end)
        ]

Comment on lines +171 to +252
if invocation.target == "distribution":
from aimx.aim_bridge.metric_stats import (
collect_distribution_series,
filter_distribution_by_step_range,
subsample_distribution,
)

if not series_list:
return QueryCommandResult(exit_status=0, output="No matching metrics found.")
series_list = collect_distribution_series(invocation.expression, normalized_repo_path)
if not series_list:
return QueryCommandResult(exit_status=0, output="No matching distributions found.")

# Step range filter is a hard constraint applied before density subsampling
if invocation.step_slice is not None:
step_start, step_end = parse_step_slice(invocation.step_slice)
series_list = [filter_by_step_range(s, step_start, step_end) for s in series_list]
# Drop empty series so they don't clutter plots
series_list = [s for s in series_list if s.count > 0]
if invocation.step_slice is not None:
step_start, step_end = parse_step_slice(invocation.step_slice)
series_list = [
filter_distribution_by_step_range(s, step_start, step_end) for s in series_list
]
series_list = [s for s in series_list if s.count > 0]

if not series_list:
return QueryCommandResult(exit_status=0, output="No data in the requested step range.")
if not series_list:
return QueryCommandResult(exit_status=0, output="No data in the requested step range.")

# Density subsampling for visualisation
needs_sample = any(
x is not None for x in (invocation.head, invocation.tail, invocation.every)
)
if needs_sample:
series_list = [
subsample(s, head=invocation.head, tail=invocation.tail, every=invocation.every)
for s in series_list
]

if invocation.mode == "json":
output = render_trace_json(series_list)
elif invocation.mode == "csv":
output = render_csv(series_list)
elif invocation.mode == "table":
output = render_trace_table(series_list, no_color=effective_no_color)
needs_sample = any(
x is not None for x in (invocation.head, invocation.tail, invocation.every)
)
if needs_sample:
series_list = [
subsample_distribution(
s, head=invocation.head, tail=invocation.tail, every=invocation.every
)
for s in series_list
]

if invocation.mode == "json":
output = render_distribution_json(series_list)
elif invocation.mode == "csv":
output = render_distribution_csv(series_list)
else:
output = render_distribution_table(series_list, no_color=effective_no_color)
else:
output = render_plot(
series_list,
width=invocation.width,
height=invocation.height,
from aimx.aim_bridge.metric_stats import (
collect_metric_series,
filter_by_step_range,
subsample,
)

series_list = collect_metric_series(invocation.expression, normalized_repo_path)

if not series_list:
return QueryCommandResult(exit_status=0, output="No matching metrics found.")

# Step range filter is a hard constraint applied before density subsampling
if invocation.step_slice is not None:
step_start, step_end = parse_step_slice(invocation.step_slice)
series_list = [filter_by_step_range(s, step_start, step_end) for s in series_list]
# Drop empty series so they don't clutter plots
series_list = [s for s in series_list if s.count > 0]

if not series_list:
return QueryCommandResult(exit_status=0, output="No data in the requested step range.")

# Density subsampling for visualisation
needs_sample = any(
x is not None for x in (invocation.head, invocation.tail, invocation.every)
)
if needs_sample:
series_list = [
subsample(s, head=invocation.head, tail=invocation.tail, every=invocation.every)
for s in series_list
]

if invocation.mode == "json":
output = render_trace_json(series_list)
elif invocation.mode == "csv":
output = render_csv(series_list)
elif invocation.mode == "table":
output = render_trace_table(series_list, no_color=effective_no_color)
else:
output = render_plot(
series_list,
width=invocation.width,
height=invocation.height,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There is significant code duplication between the distribution and metrics execution paths in run_trace_command. Both paths follow the same sequence: collect series, filter by step range, subsample, and render based on the mode.

Consider refactoring this into a unified pipeline. You could define a set of functions (collect, filter, subsample, render) based on the invocation.target and then execute the pipeline once. This would improve maintainability and ensure that improvements to the tracing logic (like new filters or sampling methods) are automatically applied to both metrics and distributions.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 589f405094

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/aimx/commands/help.py
" Repo defaults to the current directory.",
" Short run hashes in the expression are transparently expanded.",
" Example: aimx trace \"metric.name=='loss'\" --repo data --steps 100:500",
" Example: aimx trace distribution \"distribution.name=='weights'\" --repo data --json",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use distributions in trace-distribution expressions

This example advertises distribution.name, but Aim’s Repo.query_distributions(...) binds the sequence variable as distributions (plural). When users copy this command, the query variable is undefined and the new trace path reports no matches, making the feature look broken even when data exists. Update the expression to distributions.name == 'weights' (and align related docs/tests) or add alias rewriting before executing the query.

Useful? React with 👍 / 👎.

@blizhan

blizhan commented Apr 26, 2026

Copy link
Copy Markdown
Owner Author

@codex 审核代码

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 589f405094

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +275 to +276
query_result = repo.query_distributions(
expression, report_mode=QueryReportMode.DISABLED

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize distribution query variable before execution

collect_distribution_series forwards the user expression directly to Repo.query_distributions, but Aim’s distribution sequence variable is distributions (see aim.sdk.sequences.distribution_sequence.Distributions.sequence_name()), while this change’s own examples/tests use distribution.name == .... In that common case, Aim treats distribution as undefined and the query silently yields no matches, so aimx trace distribution reports “No matching distributions found” even when data exists. Please rewrite/alias the singular form before calling query_distributions (or reject it with a clear error) so documented expressions actually return results.

Useful? React with 👍 / 👎.

@blizhan blizhan merged commit 589f405 into main Apr 30, 2026
1 check passed
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.

1 participant