CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
879 lines
30 KiB
Python
879 lines
30 KiB
Python
# Copyright (c) Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
|
|
import common
|
|
import pytest
|
|
|
|
from rocprof_compute_base import RocProfCompute
|
|
from rocprof_compute_profile.profiler_base import RocProfCompute_Base
|
|
from rocprof_compute_profile.profiler_rocprof_v3 import rocprof_v3_profiler
|
|
from rocprof_compute_profile.profiler_rocprofiler_sdk import rocprofiler_sdk_profiler
|
|
from utils.utils_exceptions import (
|
|
ExecutableNotFoundError,
|
|
NoScriptInCommandError,
|
|
PythonScriptNotFoundError,
|
|
)
|
|
|
|
|
|
def _make_sanitize_args(remaining, torch_trace=False, **overrides):
|
|
"""Build a minimal argparse.Namespace for profiler_base unit tests.
|
|
|
|
Defaults satisfy sanitize(); pass **overrides to set or replace fields the
|
|
run_profiling() tests need (e.g. output_directory, no_native_tool, kernel,
|
|
or a pre-joined string ``remaining``).
|
|
"""
|
|
defaults = dict(
|
|
filter_blocks=[],
|
|
set_selected=None,
|
|
roof_only=False,
|
|
output_directory="/tmp/test_workload",
|
|
no_native_tool=False,
|
|
iteration_multiplexing=None,
|
|
attach_pid=None,
|
|
attach_duration_msec=None,
|
|
remaining=["--"] + remaining,
|
|
torch_trace=torch_trace,
|
|
dispatch=None,
|
|
kernel=None,
|
|
)
|
|
defaults.update(overrides)
|
|
return argparse.Namespace(**defaults)
|
|
|
|
|
|
def _setup_test_files(tmp_path, remaining, setup):
|
|
"""Create temporary files and substitute placeholders in remaining."""
|
|
if setup == "script":
|
|
script = tmp_path / "good_script.py"
|
|
script.write_text("print('ok')\n")
|
|
return [s.replace("{script}", str(script)) for s in remaining]
|
|
elif setup == "exec_script":
|
|
script = tmp_path / "main.py"
|
|
script.write_text("print('ok')\n")
|
|
script.chmod(0o755)
|
|
return [s.replace("{exec_script}", str(script)) for s in remaining]
|
|
elif setup == "binary":
|
|
binary = tmp_path / "my_binary"
|
|
binary.write_text("#!/bin/sh\necho hello\n")
|
|
binary.chmod(0o755)
|
|
return [s.replace("{binary}", str(binary)) for s in remaining]
|
|
return remaining
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# sanitize() with --torch-trace
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.torch_trace
|
|
@pytest.mark.parametrize(
|
|
"remaining, expected_exception, setup",
|
|
[
|
|
# Should raise
|
|
pytest.param(
|
|
["python3", "nonexistent_script_abc.py"],
|
|
PythonScriptNotFoundError,
|
|
None,
|
|
id="missing_script",
|
|
),
|
|
pytest.param(
|
|
["python3"],
|
|
NoScriptInCommandError,
|
|
None,
|
|
id="bare_interpreter",
|
|
),
|
|
pytest.param(
|
|
["python3", "-u", "-v"],
|
|
NoScriptInCommandError,
|
|
None,
|
|
id="flags_only",
|
|
),
|
|
pytest.param(
|
|
["python3", "-u", "nonexistent_script_abc.py"],
|
|
PythonScriptNotFoundError,
|
|
None,
|
|
id="missing_script_after_flags",
|
|
),
|
|
pytest.param(
|
|
["nonexistentpython3", "script.py"],
|
|
ExecutableNotFoundError,
|
|
None,
|
|
id="nonexistent_executable",
|
|
),
|
|
pytest.param(
|
|
["./no_such_binary"],
|
|
ExecutableNotFoundError,
|
|
None,
|
|
id="nonexistent_binary",
|
|
),
|
|
# Should not raise
|
|
pytest.param(
|
|
["python3", "-c", "print(1)"],
|
|
None,
|
|
None,
|
|
id="dash_c",
|
|
),
|
|
pytest.param(
|
|
["python3", "-m", "json.tool", "--help"],
|
|
None,
|
|
None,
|
|
id="dash_m",
|
|
),
|
|
pytest.param(
|
|
["python3", "-u", "{script}"],
|
|
None,
|
|
"script",
|
|
id="script_after_single_flag",
|
|
),
|
|
pytest.param(
|
|
["python3", "-W", "ignore", "-u", "{script}"],
|
|
None,
|
|
"script",
|
|
id="script_after_multi_flags",
|
|
),
|
|
pytest.param(
|
|
["{exec_script}"],
|
|
None,
|
|
"exec_script",
|
|
id="direct_py_script",
|
|
),
|
|
pytest.param(
|
|
["{binary}"],
|
|
None,
|
|
"binary",
|
|
id="non_python_binary",
|
|
),
|
|
],
|
|
)
|
|
def test_sanitize_torch_trace(tmp_path, remaining, expected_exception, setup):
|
|
"""Unit test: sanitize() behavior with --torch-trace enabled."""
|
|
remaining = _setup_test_files(tmp_path, remaining, setup)
|
|
args = _make_sanitize_args(remaining, torch_trace=True)
|
|
profiler = RocProfCompute_Base(args, profiler_mode="rocprofiler-sdk", soc=None)
|
|
if expected_exception:
|
|
with pytest.raises(expected_exception):
|
|
profiler.sanitize()
|
|
else:
|
|
profiler.sanitize()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# sanitize() without --torch-trace
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.parametrize(
|
|
"remaining, expected_exception, setup",
|
|
[
|
|
# Should raise
|
|
pytest.param(
|
|
["python3"],
|
|
NoScriptInCommandError,
|
|
None,
|
|
id="bare_interpreter",
|
|
),
|
|
pytest.param(
|
|
["python3", "-u", "-v"],
|
|
NoScriptInCommandError,
|
|
None,
|
|
id="flags_only",
|
|
),
|
|
pytest.param(
|
|
["nonexistentpython3", "script.py"],
|
|
ExecutableNotFoundError,
|
|
None,
|
|
id="nonexistent_executable",
|
|
),
|
|
pytest.param(
|
|
["./no_such_binary"],
|
|
ExecutableNotFoundError,
|
|
None,
|
|
id="nonexistent_binary",
|
|
),
|
|
# Should not raise
|
|
pytest.param(
|
|
["python3", "-c", "print(1)"],
|
|
None,
|
|
None,
|
|
id="dash_c",
|
|
),
|
|
pytest.param(
|
|
["python3", "-m", "json.tool", "--help"],
|
|
None,
|
|
None,
|
|
id="dash_m",
|
|
),
|
|
pytest.param(
|
|
["python3", "nonexistent_script_abc.py"],
|
|
None,
|
|
None,
|
|
id="missing_script",
|
|
),
|
|
pytest.param(
|
|
["python3", "-u", "nonexistent_script_abc.py"],
|
|
None,
|
|
None,
|
|
id="missing_script_after_flags",
|
|
),
|
|
pytest.param(
|
|
["python3", "-u", "{script}"],
|
|
None,
|
|
"script",
|
|
id="script_after_single_flag",
|
|
),
|
|
pytest.param(
|
|
["python3", "-W", "ignore", "-u", "{script}"],
|
|
None,
|
|
"script",
|
|
id="script_after_multi_flags",
|
|
),
|
|
],
|
|
)
|
|
def test_sanitize_no_torch_trace(tmp_path, remaining, expected_exception, setup):
|
|
"""Unit test: sanitize() behavior without --torch-trace."""
|
|
remaining = _setup_test_files(tmp_path, remaining, setup)
|
|
args = _make_sanitize_args(remaining, torch_trace=False)
|
|
profiler = RocProfCompute_Base(args, profiler_mode="rocprofiler-sdk", soc=None)
|
|
if expected_exception:
|
|
with pytest.raises(expected_exception):
|
|
profiler.sanitize()
|
|
else:
|
|
profiler.sanitize()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_profiler_options(): live-attach library resolution with fallback
|
|
# ---------------------------------------------------------------------------
|
|
def test_attach_library_resolution_with_fallback():
|
|
"""Unit test: attach branch picks new lib first, falls back to old, errors if
|
|
neither exists. resolve_rocm_library_path is mocked so the actual library
|
|
locations are controlled by the test, independent of the configured tool path."""
|
|
output_dir = Path(common.get_output_dir())
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
args = argparse.Namespace(
|
|
remaining="-- /bin/true",
|
|
rocprofiler_sdk_tool_path="/opt/rocm/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so",
|
|
format_rocprof_output="csv",
|
|
output_directory=str(output_dir),
|
|
iteration_multiplexing=None,
|
|
attach_pid=12345,
|
|
attach_duration_msec=None,
|
|
kokkos_trace=False,
|
|
kernel=None,
|
|
dispatch=None,
|
|
torch_trace=False,
|
|
)
|
|
profiler = rocprofiler_sdk_profiler(args, profiler_mode="rocprofiler-sdk", soc=None)
|
|
resolve_target = (
|
|
"rocprof_compute_profile.profiler_rocprofiler_sdk.resolve_rocm_library_path"
|
|
)
|
|
new_lib = output_dir / "librocprofiler-sdk-rocattach.so"
|
|
old_lib = output_dir / "librocprofv3-attach.so"
|
|
|
|
# Case 1: new library present -> selected, fallback lookup never happens.
|
|
new_lib.write_text("")
|
|
with patch(
|
|
resolve_target, side_effect=[str(new_lib), str(old_lib)]
|
|
) as mock_resolve:
|
|
options = profiler.get_profiler_options()
|
|
assert options["ROCPROF_ATTACH_LIBRARY"] == str(new_lib)
|
|
assert options["ROCPROF_ATTACH_OUTPUT_GENERATION_SYNC"] == "1"
|
|
assert mock_resolve.call_count == 1
|
|
|
|
# Case 2: only old library present -> falls back to it.
|
|
new_lib.unlink()
|
|
old_lib.write_text("")
|
|
with patch(
|
|
resolve_target, side_effect=[str(new_lib), str(old_lib)]
|
|
) as mock_resolve:
|
|
options = profiler.get_profiler_options()
|
|
assert options["ROCPROF_ATTACH_LIBRARY"] == str(old_lib)
|
|
assert options["ROCPROF_ATTACH_OUTPUT_GENERATION_SYNC"] == "1"
|
|
assert mock_resolve.call_count == 2
|
|
|
|
# Case 3: neither library present -> console_error exits the process.
|
|
old_lib.unlink()
|
|
with patch(resolve_target, side_effect=[str(new_lib), str(old_lib)]):
|
|
with pytest.raises(SystemExit):
|
|
profiler.get_profiler_options()
|
|
|
|
common.clean_output_dir(True, str(output_dir))
|
|
|
|
|
|
def test_rocprofv3_live_attach_uses_sync_output():
|
|
"""Unit test: rocprofv3 live attach requests synchronous output generation."""
|
|
args = _make_sanitize_args(
|
|
["/bin/true"],
|
|
attach_pid="12345",
|
|
attach_duration_msec="500",
|
|
format_rocprof_output="csv",
|
|
kokkos_trace=False,
|
|
)
|
|
args.remaining = "-- /bin/true"
|
|
profiler = rocprof_v3_profiler(args, profiler_mode="rocprofv3", soc=None)
|
|
|
|
options = profiler.get_profiler_options()
|
|
|
|
assert "--attach-sync-output" in options
|
|
pid_idx = options.index("--pid")
|
|
assert options[pid_idx + 1] == "12345"
|
|
duration_idx = options.index("--attach-duration-msec")
|
|
assert options[duration_idx + 1] == "500"
|
|
assert "--" not in options
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_pc_sampling_profiler_options(): sdk + v3 backends
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.parametrize(
|
|
"native_tool_path, method, expected_unit, expected_ld_preload",
|
|
[
|
|
pytest.param(None, "host_trap", "time", ["/opt/sdk/tool.so"], id="sdk_only"),
|
|
pytest.param(
|
|
"/n/native.so",
|
|
"stochastic",
|
|
"cycles",
|
|
["/opt/sdk/tool.so", "/n/native.so"],
|
|
id="native_appended",
|
|
),
|
|
],
|
|
)
|
|
def test_sdk_pc_sampling_options(
|
|
tmp_path, native_tool_path, method, expected_unit, expected_ld_preload
|
|
):
|
|
"""sdk PC sampling options set the PC sampling env, json/ps_file output, and
|
|
append the native tool (when given) to the upstream LD_PRELOAD."""
|
|
args = _make_sanitize_args(
|
|
["/bin/true"],
|
|
rocprofiler_sdk_tool_path="/opt/sdk/tool.so",
|
|
format_rocprof_output="csv",
|
|
output_directory=str(tmp_path),
|
|
pc_sampling_method=method,
|
|
pc_sampling_interval=1000,
|
|
kokkos_trace=False,
|
|
)
|
|
args.remaining = "/bin/true"
|
|
profiler = rocprofiler_sdk_profiler(args, profiler_mode="rocprofiler-sdk", soc=None)
|
|
|
|
options = profiler.get_pc_sampling_profiler_options(
|
|
native_tool_path=native_tool_path
|
|
)
|
|
|
|
assert options["ROCPROF_COUNTER_COLLECTION"] == "0"
|
|
assert options["ROCPROF_KERNEL_TRACE"] == "1"
|
|
assert options["ROCPROF_OUTPUT_FORMAT"] == "json"
|
|
assert options["ROCPROF_OUTPUT_PATH"] == str(tmp_path)
|
|
assert options["ROCPROF_OUTPUT_FILE_NAME"] == "ps_file"
|
|
assert options["ROCPROFILER_PC_SAMPLING_BETA_ENABLED"] == "1"
|
|
assert options["ROCPROF_PC_SAMPLING_METHOD"] == method
|
|
assert options["ROCPROF_PC_SAMPLING_UNIT"] == expected_unit
|
|
assert options["ROCPROF_PC_SAMPLING_INTERVAL"] == "1000"
|
|
ld_preload = options["LD_PRELOAD"].split(":")
|
|
assert all(part in ld_preload for part in expected_ld_preload)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"method, expected_unit, attach_pid, attach_duration_msec",
|
|
[
|
|
pytest.param("host_trap", "time", None, None, id="app_cmd"),
|
|
pytest.param("stochastic", "cycles", "1234", "500", id="live_attach"),
|
|
],
|
|
)
|
|
def test_v3_pc_sampling_options(
|
|
tmp_path, method, expected_unit, attach_pid, attach_duration_msec
|
|
):
|
|
"""v3 PC sampling options build the CLI flags and append either the -- app
|
|
cmd or the live-attach flags."""
|
|
args = _make_sanitize_args(
|
|
["./myapp", "arg1"],
|
|
output_directory=str(tmp_path),
|
|
pc_sampling_method=method,
|
|
pc_sampling_interval=1000,
|
|
attach_pid=attach_pid,
|
|
attach_duration_msec=attach_duration_msec,
|
|
)
|
|
args.remaining = "./myapp arg1"
|
|
profiler = rocprof_v3_profiler(args, profiler_mode="rocprofv3", soc=None)
|
|
|
|
options = profiler.get_pc_sampling_profiler_options()
|
|
|
|
assert "--kernel-trace" in options
|
|
assert "--pc-sampling-beta-enabled" in options
|
|
assert options[options.index("--pc-sampling-method") + 1] == method
|
|
assert options[options.index("--pc-sampling-unit") + 1] == expected_unit
|
|
assert options[options.index("--pc-sampling-interval") + 1] == "1000"
|
|
assert options[options.index("-d") + 1] == str(tmp_path)
|
|
assert options[options.index("-o") + 1] == "ps_file"
|
|
if attach_pid:
|
|
assert "--attach-sync-output" in options
|
|
assert options[options.index("--pid") + 1] == attach_pid
|
|
assert options[options.index("--attach-duration-msec") + 1] == (
|
|
attach_duration_msec
|
|
)
|
|
assert "--" not in options
|
|
else:
|
|
sep = options.index("--")
|
|
assert options[sep:] == ["--", "./myapp", "arg1"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# RocProfCompute.sanitize(): block 21 / block 30 experimental-gating
|
|
# ---------------------------------------------------------------------------
|
|
def _make_rpc_args(
|
|
*,
|
|
filter_blocks=None,
|
|
filter_metrics=None,
|
|
pc_sampling=False,
|
|
pc_sampling_method="stochastic",
|
|
pc_sampling_interval=None,
|
|
membw_analysis=False,
|
|
experimental=False,
|
|
mode="profile",
|
|
) -> argparse.Namespace:
|
|
"""Build a minimal Namespace for RocProfCompute.sanitize() unit tests."""
|
|
return argparse.Namespace(
|
|
mode=mode,
|
|
list_metrics=None,
|
|
list_blocks=None,
|
|
list_available_metrics=False,
|
|
list_sets=False,
|
|
specs=False,
|
|
filter_blocks=filter_blocks,
|
|
filter_metrics=filter_metrics,
|
|
pc_sampling=pc_sampling,
|
|
pc_sampling_method=pc_sampling_method,
|
|
pc_sampling_interval=pc_sampling_interval,
|
|
membw_analysis=membw_analysis,
|
|
experimental=experimental,
|
|
set_selected=None,
|
|
roof_only=False,
|
|
bench_only=False,
|
|
no_roof=False,
|
|
format_rocprof_output="csv",
|
|
name="unit-test",
|
|
output_directory="/tmp/unit-test",
|
|
)
|
|
|
|
|
|
def _make_rpc_with_args(args: argparse.Namespace) -> RocProfCompute:
|
|
"""Construct a RocProfCompute without invoking __init__."""
|
|
instance = RocProfCompute.__new__(RocProfCompute)
|
|
# Name-mangled private attributes consumed by sanitize().
|
|
instance._RocProfCompute__args = args
|
|
instance._RocProfCompute__mode = args.mode
|
|
return instance
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"args, expect_error, expected_filter_blocks",
|
|
[
|
|
pytest.param(
|
|
_make_rpc_args(filter_blocks=["21"]),
|
|
True,
|
|
None,
|
|
id="block21_without_pc_sampling_errors",
|
|
),
|
|
pytest.param(
|
|
_make_rpc_args(filter_blocks=["21"], pc_sampling=True),
|
|
True,
|
|
None,
|
|
id="pc_sampling_without_experimental_errors",
|
|
),
|
|
pytest.param(
|
|
_make_rpc_args(filter_blocks=["pc_sampling"]),
|
|
True,
|
|
None,
|
|
id="block_alias_without_pc_sampling_errors",
|
|
),
|
|
pytest.param(
|
|
_make_rpc_args(pc_sampling=True, experimental=True, filter_blocks=[]),
|
|
False,
|
|
["21"],
|
|
id="pc_sampling_with_experimental_injects_21",
|
|
),
|
|
pytest.param(
|
|
_make_rpc_args(filter_blocks=["21"], pc_sampling=True, experimental=True),
|
|
False,
|
|
["21"],
|
|
id="block21_with_pc_sampling_experimental_passes",
|
|
),
|
|
pytest.param(
|
|
_make_rpc_args(filter_blocks=["30"]),
|
|
True,
|
|
None,
|
|
id="block30_without_membw_analysis_errors",
|
|
),
|
|
pytest.param(
|
|
_make_rpc_args(filter_blocks=["30"], membw_analysis=True),
|
|
True,
|
|
None,
|
|
id="membw_analysis_without_experimental_errors",
|
|
),
|
|
pytest.param(
|
|
_make_rpc_args(
|
|
filter_blocks=["30"], membw_analysis=True, experimental=True
|
|
),
|
|
False,
|
|
["30"],
|
|
id="block30_with_membw_analysis_experimental_passes",
|
|
),
|
|
],
|
|
)
|
|
def test_sanitize_block_experimental_gating(args, expect_error, expected_filter_blocks):
|
|
"""Unit test: block 21 and block 30 require their experimental flags."""
|
|
instance = _make_rpc_with_args(args)
|
|
if expect_error:
|
|
with pytest.raises(SystemExit):
|
|
instance.sanitize()
|
|
else:
|
|
instance.sanitize()
|
|
assert args.filter_blocks == expected_filter_blocks
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_profiling(): PC sampling gating / increment / delegation
|
|
# ---------------------------------------------------------------------------
|
|
def _make_run_profiling_profiler(tmp_path, filter_blocks, perfmon_files=0):
|
|
"""Build a RocProfCompute_Base ready to drive run_profiling() in isolation.
|
|
|
|
Uses the rocprofv3 profiler mode so the native-tool path resolves to None
|
|
without touching the filesystem, and seeds `perfmon_files` empty pmc_perf
|
|
yaml files so total_runs reflects the counter-collection pass count.
|
|
"""
|
|
if perfmon_files:
|
|
perfmon_dir = Path(tmp_path) / "perfmon"
|
|
perfmon_dir.mkdir(parents=True, exist_ok=True)
|
|
for i in range(perfmon_files):
|
|
(perfmon_dir / f"pmc_perf_{i}.yaml").write_text("")
|
|
|
|
args = _make_sanitize_args(
|
|
["./app"],
|
|
filter_blocks=filter_blocks,
|
|
output_directory=str(tmp_path),
|
|
no_native_tool=True,
|
|
)
|
|
# run_profiling() consumes remaining as the already-joined command string.
|
|
args.remaining = "-- ./app"
|
|
soc = SimpleNamespace(_mspec=SimpleNamespace(gpu_model="MI300"))
|
|
profiler = RocProfCompute_Base(args, profiler_mode="rocprofv3", soc=soc)
|
|
profiler._filter_blocks = filter_blocks
|
|
return profiler
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"filter_blocks, perfmon_files, is_requested, ranks, "
|
|
"expect_run, expect_skip_warning, expect_multirank_warning",
|
|
[
|
|
# A requested PC sampling block delegates to PCSamplingProfile.run()
|
|
# and suppresses the skip warning.
|
|
pytest.param(
|
|
["pc_sampling"],
|
|
0,
|
|
True,
|
|
(None, None),
|
|
True,
|
|
False,
|
|
False,
|
|
id="delegates_when_requested",
|
|
),
|
|
# No PC sampling block requested -> skip warning, run() never called.
|
|
pytest.param(
|
|
["2"],
|
|
1,
|
|
False,
|
|
(None, None),
|
|
False,
|
|
True,
|
|
False,
|
|
id="skips_when_not_requested",
|
|
),
|
|
# Requested PC sampling adds a workload run, so 1 counter pass + 1 PC
|
|
# sampling pass = 2 runs with >=2 ranks -> multi-rank warning.
|
|
pytest.param(
|
|
["21", "2"],
|
|
1,
|
|
True,
|
|
("0", 2),
|
|
True,
|
|
False,
|
|
True,
|
|
id="multirank_warns_with_pc_sampling",
|
|
),
|
|
# Not requested -> only the single counter pass -> no multi-rank warning.
|
|
pytest.param(
|
|
["2"],
|
|
1,
|
|
False,
|
|
("0", 2),
|
|
False,
|
|
True,
|
|
False,
|
|
id="multirank_silent_without_pc_sampling",
|
|
),
|
|
],
|
|
)
|
|
def test_run_profiling_pc_sampling_gating(
|
|
tmp_path,
|
|
monkeypatch,
|
|
filter_blocks,
|
|
perfmon_files,
|
|
is_requested,
|
|
ranks,
|
|
expect_run,
|
|
expect_skip_warning,
|
|
expect_multirank_warning,
|
|
):
|
|
"""run_profiling() delegates to PCSamplingProfile.run() only when a PC
|
|
sampling block is requested, emits the skip warning otherwise, and counts a
|
|
requested PC sampling pass as an extra workload run for the multi-rank
|
|
warning."""
|
|
profiler = _make_run_profiling_profiler(
|
|
tmp_path, filter_blocks, perfmon_files=perfmon_files
|
|
)
|
|
base = "rocprof_compute_profile.profiler_base"
|
|
|
|
mock_pc_cls = Mock()
|
|
instance = mock_pc_cls.return_value
|
|
instance.is_requested.return_value = is_requested
|
|
mock_warning = Mock()
|
|
|
|
monkeypatch.setattr(f"{base}.PCSamplingProfile", mock_pc_cls)
|
|
monkeypatch.setattr(f"{base}.print_status", Mock())
|
|
common.patch_console(
|
|
monkeypatch, base, "log", "debug", "warning", warning=mock_warning
|
|
)
|
|
monkeypatch.setattr(f"{base}.get_job_rank_and_size", Mock(return_value=ranks))
|
|
monkeypatch.setattr(RocProfCompute_Base, "profile", Mock(return_value=0.0))
|
|
monkeypatch.setattr(
|
|
RocProfCompute_Base,
|
|
"get_pc_sampling_profiler_options",
|
|
Mock(return_value=[]),
|
|
raising=False,
|
|
)
|
|
|
|
profiler.run_profiling(version="1.0.0", prog="rocprof-compute")
|
|
|
|
assert instance.run.called is expect_run
|
|
|
|
skip_warned = any(
|
|
"PC sampling data collection skipped" in str(call)
|
|
for call in mock_warning.call_args_list
|
|
)
|
|
assert skip_warned is expect_skip_warning
|
|
|
|
multirank_warned = any(
|
|
"Multi-rank application detected" in str(call)
|
|
for call in mock_warning.call_args_list
|
|
)
|
|
assert multirank_warned is expect_multirank_warning
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"method, interval, expect_error, expected_interval",
|
|
[
|
|
pytest.param("host_trap", None, False, 512, id="host_trap_unset_default"),
|
|
pytest.param("stochastic", None, False, 1048576, id="stochastic_unset_default"),
|
|
pytest.param("stochastic", 65536, False, 65536, id="stochastic_min_accepted"),
|
|
pytest.param("stochastic", 12345, True, None, id="stochastic_not_pow2"),
|
|
pytest.param("stochastic", 32768, True, None, id="stochastic_below_min"),
|
|
pytest.param("host_trap", 100, False, 100, id="host_trap_positive_accepted"),
|
|
pytest.param("host_trap", 0, True, None, id="host_trap_zero_rejected"),
|
|
pytest.param("host_trap", -1, True, None, id="host_trap_negative_rejected"),
|
|
],
|
|
)
|
|
def test_sanitize_pc_sampling_interval(
|
|
method, interval, expect_error, expected_interval
|
|
):
|
|
"""Unit test: --pc-sampling-interval default and stochastic validation."""
|
|
args = _make_rpc_args(
|
|
pc_sampling=True,
|
|
experimental=True,
|
|
filter_blocks=[],
|
|
pc_sampling_method=method,
|
|
pc_sampling_interval=interval,
|
|
)
|
|
instance = _make_rpc_with_args(args)
|
|
if expect_error:
|
|
with pytest.raises(SystemExit):
|
|
instance.sanitize()
|
|
else:
|
|
instance.sanitize()
|
|
assert args.pc_sampling_interval == expected_interval
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_profiling(): native_tool_path reaches get_pc_sampling_profiler_options
|
|
# ---------------------------------------------------------------------------
|
|
def _make_sdk_run_profiling_profiler(
|
|
tmp_path,
|
|
monkeypatch,
|
|
*,
|
|
filter_blocks,
|
|
perfmon_files=0,
|
|
rocm_version="7.0.0",
|
|
profiler_mode="rocprofiler-sdk",
|
|
no_native_tool=False,
|
|
attach_pid=None,
|
|
native_finder_raises=False,
|
|
):
|
|
"""Drive run_profiling() on the sdk native-tool path; returns (profiler, mocks)."""
|
|
if perfmon_files:
|
|
perfmon_dir = Path(tmp_path) / "perfmon"
|
|
perfmon_dir.mkdir(parents=True, exist_ok=True)
|
|
for i in range(perfmon_files):
|
|
(perfmon_dir / f"pmc_perf_{i}.yaml").write_text("")
|
|
|
|
args = _make_sanitize_args(
|
|
["./app"],
|
|
filter_blocks=filter_blocks,
|
|
output_directory=str(tmp_path),
|
|
no_native_tool=no_native_tool,
|
|
attach_pid=attach_pid,
|
|
pc_sampling_method="host_trap",
|
|
pc_sampling_interval=1000,
|
|
)
|
|
args.remaining = "-- ./app"
|
|
soc = SimpleNamespace(
|
|
_mspec=SimpleNamespace(gpu_model="MI300", rocm_version=rocm_version)
|
|
)
|
|
profiler = RocProfCompute_Base(args, profiler_mode=profiler_mode, soc=soc)
|
|
profiler._filter_blocks = filter_blocks
|
|
|
|
base = "rocprof_compute_profile.profiler_base"
|
|
|
|
mock_pc_cls = Mock()
|
|
mock_pc_cls.return_value.is_requested.return_value = any(
|
|
block in ("21", "pc_sampling") for block in filter_blocks
|
|
)
|
|
|
|
mock_finder_cls = Mock()
|
|
finder_instance = mock_finder_cls.return_value
|
|
if native_finder_raises:
|
|
finder_instance.get_collector_library_path.side_effect = RuntimeError("boom")
|
|
else:
|
|
finder_instance.get_collector_library_path.return_value = "/n/native.so"
|
|
|
|
mock_profile = Mock(return_value=0.0)
|
|
|
|
monkeypatch.setattr(f"{base}.PCSamplingProfile", mock_pc_cls)
|
|
monkeypatch.setattr(f"{base}.NativeToolFinder", mock_finder_cls)
|
|
monkeypatch.setattr(f"{base}.print_status", Mock())
|
|
monkeypatch.setattr(
|
|
f"{base}.get_job_rank_and_size", Mock(return_value=(None, None))
|
|
)
|
|
consoles = common.patch_console(
|
|
monkeypatch, base, "log", "debug", "warning", "error"
|
|
)
|
|
monkeypatch.setattr(RocProfCompute_Base, "profile", mock_profile)
|
|
# Base get_profiler_options ignores native_tool_path kwarg passed in sdk mode.
|
|
monkeypatch.setattr(
|
|
RocProfCompute_Base,
|
|
"get_profiler_options",
|
|
lambda self, *a, **k: {},
|
|
)
|
|
mock_pc_options = Mock(return_value={})
|
|
monkeypatch.setattr(
|
|
RocProfCompute_Base,
|
|
"get_pc_sampling_profiler_options",
|
|
mock_pc_options,
|
|
raising=False,
|
|
)
|
|
|
|
mocks = SimpleNamespace(
|
|
pc_cls=mock_pc_cls,
|
|
pc_options=mock_pc_options,
|
|
finder_cls=mock_finder_cls,
|
|
profile=mock_profile,
|
|
console_error=consoles["error"],
|
|
)
|
|
return profiler, mocks
|
|
|
|
|
|
def _pc_native_tool_path(mock_pc_options):
|
|
"""native_tool_path passed to get_pc_sampling_profiler_options (or None)."""
|
|
assert mock_pc_options.called, "get_pc_sampling_profiler_options was never called"
|
|
return mock_pc_options.call_args.kwargs.get("native_tool_path")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"kwargs, expect_native_path, expect_profile_called, expect_error",
|
|
[
|
|
# PC-sampling-only: native tool threaded, no counter pass.
|
|
pytest.param(
|
|
dict(filter_blocks=["21"], perfmon_files=0),
|
|
True,
|
|
False,
|
|
None,
|
|
id="pc_only_threads_path_no_counter_pass",
|
|
),
|
|
# Mixed blocks: native tool threaded and the counter pass runs.
|
|
pytest.param(
|
|
dict(filter_blocks=["2", "21"], perfmon_files=1),
|
|
True,
|
|
True,
|
|
None,
|
|
id="mixed_threads_and_runs_counter_pass",
|
|
),
|
|
# --no-native-tool: native tool not threaded.
|
|
pytest.param(
|
|
dict(filter_blocks=["21"], perfmon_files=0, no_native_tool=True),
|
|
False,
|
|
None,
|
|
None,
|
|
id="no_native_tool_threads_none",
|
|
),
|
|
# Live attach: native tool not threaded.
|
|
pytest.param(
|
|
dict(filter_blocks=["2", "21"], perfmon_files=1, attach_pid="123"),
|
|
False,
|
|
None,
|
|
None,
|
|
id="attach_pid_threads_none",
|
|
),
|
|
# ROCm major < 7: native tool not threaded.
|
|
pytest.param(
|
|
dict(filter_blocks=["2", "21"], perfmon_files=1, rocm_version="6.4.0"),
|
|
False,
|
|
None,
|
|
None,
|
|
id="rocm_major_lt_7_threads_none",
|
|
),
|
|
# rocprofv3 backend: native tool not threaded.
|
|
pytest.param(
|
|
dict(filter_blocks=["2", "21"], perfmon_files=1, profiler_mode="rocprofv3"),
|
|
False,
|
|
None,
|
|
None,
|
|
id="rocprofv3_threads_none",
|
|
),
|
|
# Native finder failure: console_error fires.
|
|
pytest.param(
|
|
dict(filter_blocks=["2", "21"], perfmon_files=1, native_finder_raises=True),
|
|
None,
|
|
None,
|
|
True,
|
|
id="native_resolution_failure_errors",
|
|
),
|
|
],
|
|
)
|
|
def test_run_profiling_native_tool_path(
|
|
tmp_path,
|
|
monkeypatch,
|
|
kwargs,
|
|
expect_native_path,
|
|
expect_profile_called,
|
|
expect_error,
|
|
):
|
|
"""run_profiling() threads native_tool_path only on the supported sdk path,
|
|
runs the counter pass for non-PC blocks, and errors on finder failure."""
|
|
profiler, mocks = _make_sdk_run_profiling_profiler(tmp_path, monkeypatch, **kwargs)
|
|
profiler.run_profiling(version="1.0.0", prog="rocprof-compute")
|
|
|
|
if expect_native_path is not None:
|
|
path = _pc_native_tool_path(mocks.pc_options)
|
|
assert (path is not None) is expect_native_path
|
|
if expect_profile_called is not None:
|
|
assert mocks.profile.called is expect_profile_called
|
|
if expect_error is not None:
|
|
assert mocks.console_error.called is expect_error
|