|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +"""Unit tests for nsight.transformation.aggregate_data (no GPU required).""" |
| 5 | + |
| 6 | +from typing import Any |
| 7 | + |
| 8 | +import pandas as pd |
| 9 | + |
| 10 | +from nsight import transformation |
| 11 | + |
| 12 | + |
| 13 | +def _one_arg(config: Any) -> None: |
| 14 | + """A function with a single config parameter (num_args == 1).""" |
| 15 | + return None |
| 16 | + |
| 17 | + |
| 18 | +def _raw_df(config_values: list[Any], *, param_name: str = "config") -> pd.DataFrame: |
| 19 | + """Synthetic raw frame; the config column is last, as aggregate_data expects.""" |
| 20 | + n = len(config_values) |
| 21 | + data: dict[str, Any] = { |
| 22 | + "Annotation": ["matmul"] * n, |
| 23 | + "Metric": ["gpu__time_duration.sum"] * n, |
| 24 | + "Value": [float(i + 1) for i in range(n)], |
| 25 | + "Kernel": ["ampere_sgemm_128x128"] * n, |
| 26 | + param_name: config_values, |
| 27 | + } |
| 28 | + return pd.DataFrame(data) |
| 29 | + |
| 30 | + |
| 31 | +def test_dict_config_single_row_does_not_crash() -> None: |
| 32 | + # Regression: a single dict-config row used to crash groupby().agg() with |
| 33 | + # "unhashable type: 'dict'". The output must keep the dict as a real dict. |
| 34 | + dims = {"M": 512, "K": 512, "N": 512} |
| 35 | + result = transformation.aggregate_data(_raw_df([dims]), _one_arg, None, False) |
| 36 | + assert len(result) == 1 |
| 37 | + assert result["config"].iloc[0] == dims |
| 38 | + assert isinstance(result["config"].iloc[0], dict) |
| 39 | + assert result["config"].iloc[0]["M"] == 512 # usable as a dict, no parsing |
| 40 | + assert result["NumRuns"].iloc[0] == 1 |
| 41 | + assert result["AvgValue"].iloc[0] == 1.0 |
| 42 | + # The temporary sort key must not leak into the output. |
| 43 | + assert not any("sortkey" in c for c in result.columns) |
| 44 | + |
| 45 | + |
| 46 | +def test_dict_config_independent_of_run_count() -> None: |
| 47 | + # Bumping runs only masked the bug (it added rows); every count must work. |
| 48 | + dims = {"M": 256, "K": 256, "N": 256} |
| 49 | + for runs in (1, 2, 5): |
| 50 | + result = transformation.aggregate_data( |
| 51 | + _raw_df([dims] * runs), _one_arg, None, False |
| 52 | + ) |
| 53 | + assert len(result) == 1 |
| 54 | + assert result["NumRuns"].iloc[0] == runs |
| 55 | + assert result["config"].iloc[0] == dims |
| 56 | + |
| 57 | + |
| 58 | +def test_distinct_dict_configs_stay_distinct() -> None: |
| 59 | + # Different dicts form different groups, each preserved as a real dict. |
| 60 | + d1 = {"M": 512, "K": 512, "N": 512} |
| 61 | + d2 = {"M": 1024, "K": 1024, "N": 1024} |
| 62 | + result = transformation.aggregate_data(_raw_df([d1, d2]), _one_arg, None, False) |
| 63 | + assert len(result) == 2 |
| 64 | + configs = list(result["config"]) |
| 65 | + assert all(isinstance(c, dict) for c in configs) |
| 66 | + assert d1 in configs and d2 in configs |
| 67 | + |
| 68 | + |
| 69 | +def test_list_config_single_row_does_not_crash() -> None: |
| 70 | + # Lists are unhashable too, and must also be preserved as lists. |
| 71 | + shape = [512, 512] |
| 72 | + result = transformation.aggregate_data(_raw_df([shape]), _one_arg, None, False) |
| 73 | + assert len(result) == 1 |
| 74 | + assert result["config"].iloc[0] == shape |
| 75 | + assert isinstance(result["config"].iloc[0], list) |
| 76 | + |
| 77 | + |
| 78 | +def test_numeric_config_keeps_native_dtype() -> None: |
| 79 | + # Sortable, hashable configs must not be needlessly coerced to strings. |
| 80 | + result = transformation.aggregate_data(_raw_df([128]), _one_arg, None, False) |
| 81 | + assert result["config"].iloc[0] == 128 |
| 82 | + assert pd.api.types.is_integer_dtype(result["config"]) |
0 commit comments