Skip to content

feat: add inference_speed benchmark with throughput score#148

Open
lwalew wants to merge 5 commits into
developfrom
feat/scaling-benchmark-revamp
Open

feat: add inference_speed benchmark with throughput score#148
lwalew wants to merge 5 commits into
developfrom
feat/scaling-benchmark-revamp

Conversation

@lwalew

@lwalew lwalew commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Adds a new inference_speed benchmark. The existing scaling benchmark is left completely untouched (verified: no diff vs develop).

What it does

Measures MD throughput and how it scales with system size, and turns it into a score.

  • Reuses the scaling dataset via a new dataset_name hook on the base Benchmark — no duplicate data shipped.
  • Speed score: Hill function 1 / (1 + (t/t₀)ᵏ) on the per-atom step time (size-normalised), averaged over systems → faster models score higher. Contributes to the overall model score (scaling still does not).
  • New GUI page: throughput (ns/day) on log–log axes (toggle), power-law fit lines, per-episode variance error bars, and a summary table (scaling exponent, R², throughput, largest system).
  • Records per-episode times and the MD timestep on the result.

Caveats

  • ⚠️ The score t₀ (SCORE_PER_ATOM_STEP_TIME_MIDPOINT in inference_speed.py) is a documented placeholder calibrated for H100 — needs tuning against a real run so scores spread sensibly.
  • The score is wall-clock based ⇒ only comparable across models run on the same GPU (documented).

Tests added for the new benchmark + compute_speed_score; full suite (132) + ruff + mypy green.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Coverage

Tests Skipped Failures Errors Time
156 0 💤 0 ❌ 0 🔥 24.507s ⏱️

@lwalew lwalew force-pushed the feat/scaling-benchmark-revamp branch from 9420481 to 4ec5c8d Compare June 26, 2026 15:22
@lwalew lwalew changed the title feat: revamp scaling benchmark with throughput score and richer UI feat: add inference_speed benchmark with throughput score Jun 26, 2026
@lwalew lwalew added the claude-review Automatically request a review from Claude label Jul 8, 2026
lwalew and others added 2 commits July 9, 2026 17:34
Adds a new `inference_speed` benchmark (leaving `scaling` untouched) that measures
MD throughput and how it scales with system size, and turns it into a score.

- Reuses the `scaling` dataset via a new `dataset_name` hook on the base Benchmark,
  so no duplicate data is shipped.
- Produces a Hill-function speed score 1 / (1 + (t / t0)^k) on the per-atom step
  time, averaged over systems; faster models score higher. The score is wall-clock
  based and only comparable across models run on the same GPU. It contributes to the
  overall model score.
- New GUI page: throughput (ns/day) with log-log axes, power-law fit lines,
  per-episode variance error bars, and a per-model summary table.
- Records per-episode times and the MD timestep on the result.

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

# Conflicts:
#	CHANGELOG.md
#	docs/source/api_reference/index.rst
#	docs/source/benchmarks/general/index.rst
#	src/mlipaudit/benchmark.py
inference_speed now reports two complementary speeds per system:
- Model throughput (atoms/s): the raw forward pass, engine-independent. For mlip
  models this times the pure network forward on a pre-built graph (mirroring
  mlip-jax scripts/time_inference.py); for external ASE calculators it forces a
  recompute on the pre-built atoms (includes their neighbour-list build). Uses
  warm-up, block-until-ready/numpy-read sync, and slowest-20% outlier trimming.
- MD throughput (ns/day): the existing end-to-end short MD run.

The speed score now uses the per-atom model forward time (engine-independent)
rather than the MD step time. The GUI gains a metric selector across both
families and a graph-cutoff column in the summary table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lwalew lwalew force-pushed the feat/scaling-benchmark-revamp branch from b0ace79 to fe38174 Compare July 9, 2026 15:35
@claude

claude Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude finished @lwalew's task in 3m 59s —— View job


Review: inference_speed benchmark

Nice, well-documented benchmark — clean separation of the two throughput metrics (engine-independent forward pass vs. end-to-end MD), thorough docstrings, license headers, docs, and CHANGELOG all present. However, there's a blocking bug that means the benchmark cannot run (or be analyzed) as written.


🔴 Blocking: self._dataset_name is never defined → AttributeError

inference_speed.py defines a class attribute dataset_name = "scaling" (line 177) but then reads self._dataset_name (note the leading underscore) in two places:

  • run_modelsrc/mlipaudit/benchmarks/inference_speed/inference_speed.py:194
  • _structure_filenamesinference_speed.py:445 (reached from analyze via _structure_names)

_dataset_name is not defined anywhere — not as a class attribute, not on the base Benchmark, and not in mocked_benchmark_init (which only patches _download_data). So any call to run_model() or analyze() raises AttributeError: 'InferenceSpeedBenchmark' object has no attribute '_dataset_name'. The test tests/inference_speed/test_inference_speed.py:56 (assert ..._dataset_name == "scaling") hits the same error.

The root cause is that benchmark.py was not modified in this PR (no diff vs develop), yet the CHANGELOG claims "Benchmarks can now reuse another benchmark's dataset via the dataset_name attribute." The base class already provides this via data_name + the data_dir property (benchmark.py:112, :235-241), and dataset_name/_dataset_name are not wired into it at all. A second consequence: because data_name is left unset, _download_data (benchmark.py:244) will try to fetch inference_speed.zip from HuggingFace — which does not exist — instead of reusing scaling.

Suggested fix — use the existing base-class mechanism rather than inventing a new attribute:

# class body
data_name = "scaling"          # instead of `dataset_name = "scaling"`

# run_model (line ~194)
atoms = ase_read(self.data_dir / f"{structure_name}.xyz")

# _structure_filenames (line ~445)
os.listdir(self.data_dir)

and update the test to assert benchmark.data_name == "scaling" (or benchmark.data_dir.name == "scaling"), plus the docstring/CHANGELOG wording.

Fix this →

Note: the CI status comment above (133 passing) is dated 2026-06-26, but both commits on this branch are dated 2026-07-09, so that green check does not cover the current code. Worth re-running the suite.


🟠 Minor: early-failure return omits required structures field

inference_speed.py:383-386:

if len(self.model_output.simulation_states) == 0:
    return InferenceSpeedResult(structure_names=self._structure_names, failed=True)

InferenceSpeedResult.structures has no default, so this raises a Pydantic ValidationError on the empty-run path. (ScalingResult has the same latent issue, so this is copied behaviour rather than new — but worth fixing here by passing structures=structure_results.)


🟡 Minor: fit lines ignore the per-structure timestep

_build_fit_lines (src/mlipaudit/ui/inference_speed.py:192) converts fitted times with DEFAULT_TIMESTEP_FS (1.0), while the plotted points use each structure's actual timestep_fs (inference_speed.py:125). For the MD ns/day metric these will disagree if a result was ever recorded with a timestep ≠ 1 fs, offsetting the fit line from its points. Today all configs use 1 fs so the impact is nil, but it's an inconsistency — consider threading the structures' timestep through, or documenting the assumption.


✅ Looks good

  • NS_PER_DAY_FACTOR = 0.0864 (= 86400 × 1e-6) and the ns/day conversion are correct.
  • Per-atom normalisation in _compute_score is sound; the placeholder t₀ is clearly flagged with a TODO and the hardware-relative caveat is well documented.
  • mlip vs. ASE forward-pass timing (pre-built graph + jax.block_until_ready, vs. all_changes recompute + result read for device sync) is a reasonable, clearly-commented distinction.
  • Logging via the mlipaudit logger, Google-style docstrings, license headers, UI/API/benchmark docs, and CHANGELOG entry are all in place.

Once the _dataset_name issue is resolved and the suite re-run, this is close to mergeable.

The rebase onto develop dropped this PR's benchmark.py change (develop had
independently added dataset reuse as `data_name` + the `data_dir` property), so
inference_speed still referenced the now-undefined `self._dataset_name`,
raising AttributeError in run_model/analyze.

- Use the base-class mechanism: `data_name = "scaling"` and `self.data_dir`.
- Also pass `structures` on the empty-run early return (Pydantic requires it).
- Restore the CHANGELOG entry lost in the rebase.

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

This comment has been minimized.

…peed

Each structure now records model forward throughput plus MD throughput on every
backend the model can natively run: mlip models on both JAX-MD and ASE (the latter
via MLIPForceFieldASECalculator), external models on ASE only. ASE is the shared
backend, giving an apples-to-apples MD comparison across JAX and external models,
while the JAX-MD-vs-ASE gap isolates backend overhead.

- Result schema: per-structure `md: dict[backend, MDBackendResult]` replacing the
  single MD field; model output carries per-structure `md_episode_times` keyed by
  backend. Score still uses the engine-independent model forward time.
- GUI metric selector gains ASE and JAX-MD variants; summary keeps the graph-cutoff
  column.

Decided against torch->JAX conversion (e.g. tojax) for external models: it targets
correctness/portability over native performance and breaks on the dynamic shapes /
views that MLIP message passing needs, so timings would not reflect real speed.

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

This comment has been minimized.

The MD timing assumed a JAX-MD-style episode structure (mean episode time divided
by num_steps//num_episodes). That is wrong for the ASE engine, which has no
episodes: it calls loggers every log_interval steps and its first logged chunk
includes compilation. So ASE step times (and thus the ns/day of every external
model, and the ASE backend for mlip models) were miscalculated.

Time instead against the cumulative `state.step` reported at each logger call:
each chunk's step time is its wall-time delta over its step-count delta, and the
compilation chunk is dropped (opening interval when the first sample is at step 0
for ASE; JAX-MD's first call already lands after the compile episode). Correct for
both backends without assuming a chunk size; results store per-chunk step-time
samples directly.

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

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Claude finished @lwalew's task in 1m 39s —— View job


Re-review: inference_speed benchmark (head d913522)

  • Read latest diff vs origin/develop (new commit: real step-count MD timing)
  • Verify previously-flagged issues (ASE step-time normalisation, CHANGELOG dup, fit-line timestep)
  • Fresh pass over changed code
  • Post review

The new commit d913522 resolves the one substantive risk from the previous review — the ASE step-time normalisation — cleanly and correctly. One flagged item (the CHANGELOG duplicate) is still open.


✅ Resolved: ASE-backend step-time normalisation

The previous review asked whether dividing every backend's episode time by a fixed num_steps // num_episodes = 200 was valid for ASE (which has no episode structure). It wasn't, and d913522 fixes it properly. Timing is now derived from the cumulative state.step reported at each logger call rather than an assumed chunk size:

# inference_speed.py:_step_times_from_samples
start = 1 if samples and samples[0][0] == 0 else 0
for (step0, time0), (step1, time1) in zip(samples[start:], samples[start + 1:]):
    if step1 > step0:
        step_times.append((time1 - time0) / (step1 - step0))

This is backend-agnostic and the compilation-exclusion logic is robust for both cases:

  • ASE logs at step 0, so start=1 drops the opening 0 → log_interval chunk (which contains compilation).
  • JAX-MD first logs at the end of episode 1 (step > 0), so there's simply no 0 → … chunk to include — the compilation episode is implicitly excluded either way.

Since each chunk divides its own wall-time delta by its own step-count delta, JAX-MD and ASE step times are now directly comparable on the shared axes — which was the concern. The new test_step_times_from_samples covers both the ASE (step-0) and JAX-MD (step>0) paths plus the too-little-data case. Nicely done. The UI was updated to consume step_time_samples directly, and I confirmed there are no stale episode_times / num_steps // num_episodes normalisation paths left in either the benchmark or the UI.


🟠 Still open: CHANGELOG has a duplicated inference_speed entry + wrong attribute name

Flagged in the last two reviews and still not fixed. The ## Unreleased entry (CHANGELOG.md:5-13) is good, but the duplicate under the already-released ## Release 0.1.5 section remains:

  • CHANGELOG.md:29-35 — a second inference_speed bullet under a shipped release.
  • CHANGELOG.md:36"Benchmarks can now reuse another benchmark's dataset via the dataset_name attribute." The attribute is data_name, not dataset_name, and this reuse mechanism is already documented by the 0.1.5 bullet at :20-21, so the line is both misnamed and redundant.

New work shouldn't be appended to a released section. Delete lines 29-36, keeping only the ## Unreleased entry.

Fix this →


🟡 Minor: fit lines still ignore the per-structure timestep (unchanged)

_build_fit_lines (src/mlipaudit/ui/inference_speed.py:202) converts fitted times with DEFAULT_TIMESTEP_FS (1.0), whereas the plotted points use structure.timestep_fs or DEFAULT_TIMESTEP_FS (:135). Nil impact today (all configs use 1 fs), but the fit line would offset from its points for any result recorded with a timestep ≠ 1 fs. Thread the structures' timestep through or drop a short comment. (Non-blocking.)


🟡 Minor: num_steps / num_episodes are now unused on the result

With the timing rework, InferenceSpeedStructureResult.num_steps/num_episodes (:437) are no longer read anywhere — the UI's old num_steps / num_episodes normalisation was removed. Harmless as recorded metadata, but if they're not intended for display they're now dead fields worth pruning or a comment noting they're kept for provenance. (Non-blocking.)


🟡 Test coverage: backend dispatch still untested (unchanged)

_md_backends (mlip ⇒ [jax_md, ase], external ⇒ [ase]) and the ASE-wrapping branch in _time_md remain uncovered — tests still mock model_output and exercise only analyze. The new _step_times_from_samples test is a good addition; a small _md_backends unit test would close the remaining gap cheaply. (Non-blocking.)


Summary

The blocking timing correctness issue is resolved and well-tested. The only remaining item worth doing before merge is the CHANGELOG cleanup (delete :29-36); the rest are minor/non-blocking. Looks good once that's done.
· branch feat/scaling-benchmark-revamp

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

Labels

claude-review Automatically request a review from Claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant