Files
nvbench/docs/nvbench_compare.md
Oleksandr Pavlyk 4fabe3fde9 Harden nvbench-compare plotting and filter docs
Skip UNKNOWN rows when collecting summary plot entries so non-numeric
fractional differences cannot reach the plotting path. Add a regression test
that exercises compare_benches(..., plot=True) with an UNKNOWN row.

Document the supported pow2 axis-filter syntax and update the CLI help example
to use NAME[pow2]=EXP, matching the parser behavior for axes displayed as 2^N.

* Document when status ???? (UNKNOWN) is emitted
* Clarify --no-color behavior

* nvbench_compare.md: clarify --no-color behavior, fix example

* Document display options in nvbench_compare.md

* Small mention of plotting capabilities in nvbench_compare.md
* Call out that example requires shell with process substitution capabilities
2026-07-02 07:26:49 -05:00

570 lines
16 KiB
Markdown

# NVBench Compare
`nvbench-compare` compares two NVBench JSON outputs and classifies matching
benchmark states as `SAME`, `FAST`, `SLOW`, `AMBG`, or `????`.
NVBench treats benchmark performance data as describing a timing interval over
which measured timings varied. The interval is not intended as a precise
statistical confidence interval; it is an intuitive representation of the
observed timing range used to decide whether two benchmark results are clearly
separated, clearly compatible, or ambiguous.
The comparison is intentionally conservative. It reports `FAST` or `SLOW` only
when the timing intervals have a clear gap and the gap is confirmed in cycle
space when clock information is available. Ambiguous cases stay `AMBG`
instead of forcing a pass or regression.
## Common Invocations
Compare two JSON files:
```bash
nvbench-compare reference.json compare.json
```
Limit the comparison to one benchmark:
```bash
nvbench-compare --benchmark copy_type_sweep reference.json compare.json
```
Limit the comparison to one benchmark and one axis value:
```bash
nvbench-compare \
--benchmark copy_type_sweep \
--axis T=F32 \
reference.json compare.json
```
Choose a table display mode. The default `intervals` mode shows timing centers
with compact intervals and the status. `legacy` shows the older time/noise/diff
columns. `explain` adds explicit low/center/high interval endpoints and decision
reason codes:
```bash
nvbench-compare --display intervals reference.json compare.json
nvbench-compare --display legacy reference.json compare.json
nvbench-compare --display explain reference.json compare.json
```
Plot the comparison summary, or plot timings along a positive numeric axis. Add
`--dark` to the summary plot when it should use a dark theme:
```bash
nvbench-compare --plot --dark reference.json compare.json
nvbench-compare --plot-along "Elements{io}" reference.json compare.json
```
Generate Python code with bulk sample/frequency filenames for every displayed
row:
```bash
nvbench-compare --bulk-debug-python /path/to/output.py reference.json compare.json
```
Compare selected devices. Device filters are paired by position, so this
compares reference device `0` against compare device `1`:
```bash
nvbench-compare \
--reference-devices 0 \
--compare-devices 1 \
reference.json compare.json
```
Disable ANSI color codes. In this mode, status values are prefixed with emoji
markers so copied output still carries the status category:
```bash
nvbench-compare --no-color reference.json compare.json
```
Use a built-in comparison preset:
```bash
nvbench-compare --preset permissive reference.json compare.json
```
Use custom settings from TOML:
```bash
nvbench-compare --config compare.toml reference.json compare.json
```
Use a CLI preset as the base while preserving explicit TOML overrides:
```bash
nvbench-compare --config compare.toml --preset strict reference.json compare.json
```
Print the effective default configuration:
```bash
nvbench-compare --dump-config
```
Print the effective configuration for a built-in preset:
```bash
nvbench-compare --preset permissive --dump-config
```
## Matching Inputs
`nvbench-compare` matches benchmark states by benchmark name, device pairing,
axis filters, and state occurrence order within each device section.
Device sections must match unless `--ignore-devices` is specified or explicit
device filters are used:
```bash
nvbench-compare \
--ignore-devices \
reference.json compare.json
```
```bash
nvbench-compare \
--reference-devices 0 \
--compare-devices 1 \
reference.json compare.json
```
The device filter value may be `all`, one non-negative integer device id, or a
comma-separated list of non-negative integer ids. Filtered reference and compare
device lists must have the same length; devices are paired by position.
Benchmark and axis filters follow NVBench CLI scoping:
```bash
nvbench-compare -b copy_type_sweep -a T=F32 reference.json compare.json
```
For integer axes displayed with NVBench `pow2` formatting, filter by exponent
with `NAME[pow2]=EXP`. For example, an axis value displayed as `2^20` is
selected with:
```bash
nvbench-compare -b base -a "Elements{io}[pow2]=20" reference.json compare.json
```
`-a` / `--axis` applies to the most recent `-b` / `--benchmark`, or to all
benchmarks if it appears before any benchmark filter.
## Timing Data Used
For each matched state, `nvbench-compare` extracts GPU timing summaries emitted
by NVBench cold measurements:
- `min`
- `max`
- `mean`
- `stdev/absolute`
- `stdev/relative`
- `q1`
- `median`
- `q3`
- `iqr/absolute`
- `iqr/relative`
- `sm_clock_rate/mean`
When JSON output is generated with the NVBench `--jsonbin` option,
sample-time and sample-frequency binary data are loaded lazily and used for
bulk-data confirmation.
Missing or empty bulk data are treated as unavailable. Bulk files that are
present and non-empty but fail lazy loading or validation are treated as
unusable evidence and reported as warnings.
## Bulk Debug Python Output
`--bulk-debug-python /path/to/output.py` writes a Python script to the specified
file. The generated script contains a `bulk_rows` list. Each entry corresponds
to one row that `nvbench-compare` prints in its display tables after all
benchmark, axis, device, and threshold filters are applied.
Use `stdout` instead of a file path to print the generated Python code:
```bash
nvbench-compare --bulk-debug-python stdout reference.json compare.json
```
Generated bulk-debug Python is enclosed in comment markers:
```python
# NVB-BULK-BEGIN
...
# NVB-BULK-END
```
Because the markers are valid Python comments, the generated helpers can be
filtered directly into the standard Python REPL. This example uses process
substitution, which requires a shell such as Bash, Zsh, or Ksh:
```bash
python -i <(
nvbench-compare --bulk-debug-python stdout reference.json compare.json \
| sed -n '/^# NVB-BULK-BEGIN$/,/^# NVB-BULK-END$/p'
)
```
IPython does not reliably accept process-substitution paths as startup files.
For IPython, write the generated code to a temporary file directly:
```bash
tmp=$(mktemp "${TMPDIR:-/tmp}/nvbench-bulk.XXXXXX")
nvbench-compare --bulk-debug-python "$tmp" reference.json compare.json
ipython -i "$tmp"
rm -f "$tmp"
```
Each `bulk_rows` entry includes:
- `row_index`: zero-based index among displayed comparison rows
- `table_row_index`: zero-based index within the displayed table for a device
section
- `benchmark`
- `reference_json` and `compare_json`
- `reference_device_id` and `compare_device_id`
- `state_key`
- `occurrence` and `occurrence_count`, which disambiguate duplicate states
- `axis_values`
- `status`, `reason`, and `reason_message`
- sample and frequency filenames and counts for reference and compare data
The generated script also defines `load_bulk_data(row)`, which reads the
float32 sample and frequency files for a selected row.
Select the first displayed row:
```python
row = bulk_rows[0]
arrays = load_bulk_data(row)
```
Select the second ambiguous row:
```python
ambiguous = [row for row in bulk_rows if row["status"] == "AMBG"]
row = ambiguous[1]
arrays = load_bulk_data(row)
```
If `-b` and `-a` narrow the report to one comparison of interest, the desired
entry is usually available positionally as `bulk_rows[0]`. If duplicate states
remain after filtering, use `occurrence` to distinguish them.
## Time Estimates And Intervals
`nvbench-compare` prefers robust timing summaries when both sides provide them:
- center: `median`
- relative dispersion: `iqr/relative`, or `iqr/absolute` / `median`
- interval: `[min, q3]`
If robust summaries are not available on both sides, it falls back to classical
summaries:
- center: `mean`
- relative dispersion: `stdev/relative`, or `stdev/absolute` / `mean`
- interval: `[max(min, mean - stdev), min(max, mean + stdev)]`
Centers and interval endpoints must be positive and finite. States with unusable
centers are not compared.
Rows with `????` status could not form a valid timing comparison input. This
status is emitted for skipped benchmark states, missing GPU timing summaries,
or timing centers that are missing, non-finite, or non-positive. These rows are
included in the total match count so data-collection issues remain visible.
## Decision Tree
The comparison logic starts from `AMBG` and upgrades only when enough
evidence is available.
### 1. Check For A Clear Gap
The reference and compare intervals are checked for non-overlap.
`FAST` is possible when the compare interval is entirely below the reference
interval:
```text
cmp.upper < ref.lower
(ref.lower - cmp.upper) / cmp.upper >= clear_gap.relative
```
`SLOW` is possible when the compare interval is entirely above the reference
interval:
```text
cmp.lower > ref.upper
(cmp.lower - ref.upper) / ref.upper >= clear_gap.relative
```
These ratios are algebraically equivalent to checking a log-scale relative gap,
but avoid evaluating logarithms for every row.
### 2. Confirm Clear Gap In Cycle Space
If sample times and frequencies are available, `nvbench-compare` computes:
```text
cycles = sample_time * sample_frequency
```
It then builds cycle intervals from the bulk cycle samples and requires the
cycle interval comparison to agree with the timing interval comparison. A timing
gap that is not confirmed by bulk cycle intervals is `AMBG`.
If bulk data are missing or empty, `nvbench-compare` falls back to summary
clock-rate confirmation using `sm_clock_rate/mean`. If non-empty bulk data are
present but fail lazy loading or validation, the clear-gap decision remains
`AMBG` instead of falling back. If the clock-rate summary is missing or invalid,
the clear-gap decision also remains `AMBG`.
### 3. Check Bulk-Data Compatibility For SAME
When there is no clear gap and bulk sample/frequency data are available,
`nvbench-compare` compares both time samples and cycle samples using symmetric
nearest-neighbor coverage in log space.
For each unique value in one run, the nearest unique value in the other run is
found. A value is covered when the nearest-neighbor log distance is within:
```text
log(1 + same.center_relative)
```
Both directions must pass:
- sample-weight coverage must be at least `bulk.sample_coverage`
- unique-support coverage must be at least `bulk.support_coverage`
Sample-weight coverage uses occurrence counts. Unique-support coverage treats
each retained unique value equally.
### 4. Fall Back To Summary SAME
If bulk data are unavailable, summary data can still support `SAME` when all of
the following are true:
- both relative dispersion values are finite
- `max(ref_noise, cmp_noise) <= same.relative_dispersion_ceiling`
- centers are close:
```text
abs(ref.center - cmp.center) / min(ref.center, cmp.center)
<= same.center_relative
```
- intervals overlap strongly enough:
```text
overlap_fraction >= same.overlap_fraction
```
If `sm_clock_rate/mean` is available on both sides, the same check must also be
confirmed in summary cycle space. If clock-rate summaries are unavailable, the
summary timing decision can still report `SAME`.
### 5. Otherwise Report AMBG
If none of the clear-gap or same-result paths has enough evidence,
`nvbench-compare` reports `AMBG` and records a reason in the summary.
## What To Do With AMBG Results
`AMBG` does not mean a benchmark improved or regressed. It means
`nvbench-compare` did not find enough evidence to classify the result as
`SAME`, `FAST`, or `SLOW`.
Useful next steps are:
- Re-run both measurements and collect JSON with bulk sample data:
```bash
./benchmark --jsonbin reference.json
./benchmark --jsonbin compare.json
nvbench-compare reference.json compare.json
```
Here `./benchmark` is the NVBench-instrumented executable or Python script that
uses `cuda.bench`.
- Use `--display explain` to inspect the interval, noise, and decision reason
for each compared state.
- Use `--bulk-debug-python /path/to/output.py` to generate Python code that
identifies sample and frequency files for every displayed row.
- If cold-start effects are expected, adjust cold warmup controls such as
`--cold-warmup-runs` and `--cold-max-warmup-walltime`.
- Try a different stopping criterion when the default does not collect useful
evidence. For example, use `--stopping-criterion entropy`, or use
`--stopping-criterion sample-count` with an explicit `--target-samples`
value.
- After collecting stable data, use `--dump-config` as a starting point for a
TOML config if the default comparison thresholds are not appropriate for the
benchmark or machine.
## Configuration
Configuration files are TOML. The current format version is `1`.
```toml
version = 1
[preset]
name = "default"
[clear_gap]
relative = 0.005
[same]
center_relative = 0.005
overlap_fraction = 0.5
relative_dispersion_ceiling = 0.02
[bulk]
sample_coverage = 0.97
support_coverage = 0.8
[bulk.rare_support]
sample_fraction = 0.001
max_removed_sample_fraction = 0.01
```
The parser is strict. Unknown top-level tables, unknown keys, wrong nesting,
unsupported versions, invalid types, non-finite values, and out-of-range values
are rejected.
TOML parsing is lazy. Python 3.11 and newer use the standard-library
`tomllib`; Python 3.10 requires the optional `tomli` package only when
`--config` is used.
## Preset And Config Precedence
Preset resolution is:
1. Use `default` when neither TOML nor CLI selects a preset.
2. Use `[preset] name = "..."` from TOML as the base preset when present.
3. Use `--preset ...` as the base preset when present, overriding the TOML
preset selection.
4. Apply explicit TOML threshold values over whichever base preset was selected.
For example, with this config:
```toml
version = 1
[preset]
name = "permissive"
[bulk]
sample_coverage = 0.99
```
This command uses the `permissive` preset as the base and overrides only
`bulk.sample_coverage`:
```bash
nvbench-compare --config compare.toml reference.json compare.json
```
This command uses the `strict` preset as the base, but still overrides
`bulk.sample_coverage` from TOML:
```bash
nvbench-compare --config compare.toml --preset strict reference.json compare.json
```
## Built-In Presets
Built-in presets are available through `--preset`. To inspect the exact values
for the default configuration, run:
```bash
nvbench-compare --dump-config
```
To inspect a named preset, combine `--preset` with `--dump-config`:
```bash
nvbench-compare --preset strict --dump-config
nvbench-compare --preset permissive --dump-config
```
This avoids duplicating preset values in documentation and keeps the displayed
configuration tied to the installed `nvbench-compare` version.
## Configuration Keys
### `clear_gap.relative`
Valid range: `>= 0`
Minimum relative gap required before a non-overlapping timing interval can be
classified as `FAST` or `SLOW`.
### `same.center_relative`
Valid range: `>= 0`
Maximum relative center difference for summary `SAME` decisions. This value is
also used as the log-space tolerance for bulk nearest-neighbor coverage:
```text
log(1 + same.center_relative)
```
### `same.overlap_fraction`
Valid range: `0 <= value <= 1`
Minimum interval overlap fraction required for summary `SAME` decisions. The
overlap is measured relative to the narrower interval.
### `same.relative_dispersion_ceiling`
Valid range: `>= 0`
Maximum allowed relative dispersion for summary `SAME` decisions.
### `bulk.sample_coverage`
Valid range: `0 <= value <= 1`
Minimum sample-weight coverage for bulk `SAME` decisions. This check uses
counts of repeated sample values, so common values carry more weight.
### `bulk.support_coverage`
Valid range: `0 <= value <= 1`
Minimum unique-support coverage for bulk `SAME` decisions. This check treats
each retained unique value equally.
### `bulk.rare_support.sample_fraction`
Valid range: `0 <= value <= 1`
Unique values with count below:
```text
max(2, ceil(sample_fraction * total_sample_count))
```
are considered rare for support coverage.
This filter only affects unique-support coverage. Sample-weight coverage always
uses all samples.
### `bulk.rare_support.max_removed_sample_fraction`
Valid range: `0 <= value <= 1`
Maximum sample mass that may be removed from unique-support coverage by the rare
value filter. If filtering would remove more sample mass than this, remove every
unique value, or operate on an all-unique dataset, support coverage falls back
to the full unique support.