Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ ROCprofiler-SDK is AMD’s new and improved tooling infrastructure, providing a

## Tool Support

rocprofv3 is the command line tool built using the rocprofiler-sdk library and shipped with the ROCm stack. To see details on
the command line options of rocprofv3, please see rocprofv3 user guide
rocprofv3 is the command line tool built using the rocprofiler-sdk library and shipped with the ROCm stack. It supports both launching applications with profiling enabled and attaching to already running processes for dynamic profiling using `--attach`/`--pid`/`-p` options.

To see details on the command line options of rocprofv3, please see rocprofv3 user guide
[Click Here](source/docs/how-to/using-rocprofv3.rst)

## Documentation
Expand Down
3 changes: 2 additions & 1 deletion cmake/rocprofiler_config_install.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ install(
install(
DIRECTORY ${PROJECT_SOURCE_DIR}/tests
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PACKAGE_NAME}
COMPONENT tests)
COMPONENT tests
USE_SOURCE_PERMISSIONS)

install(
FILES ${PROJECT_SOURCE_DIR}/requirements.txt
Expand Down
2 changes: 1 addition & 1 deletion cmake/rocprofiler_config_packaging.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ endif()
if(NOT NUM_ROCPROFILER_PACKAGING_COMPONENTS EQUAL EXPECTED_PACKAGING_COMPONENTS)
message(
FATAL_ERROR
"Error new install component needs COMPONENT_NAME_* and COMPONENT_SEP_* entries: ${ROCPROFILER_PACKAGING_COMPONENTS}"
"Error new install component needs COMPONENT_NAME_* , COMPONENT_DEP_* , and COMPONENT_DESC_* entries: ${ROCPROFILER_PACKAGING_COMPONENTS}"
)
endif()

Expand Down
11 changes: 11 additions & 0 deletions source/bin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@ install(
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ
WORLD_EXECUTE
COMPONENT tools)

configure_file(rocprofv3-attach.py
${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}/rocprofv3-attach COPYONLY)

install(
FILES ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}/rocprofv3-attach
DESTINATION ${CMAKE_INSTALL_BINDIR}
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ
WORLD_EXECUTE
COMPONENT tools)

88 changes: 88 additions & 0 deletions source/bin/rocprofv3-attach.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env python3

# MIT License
#
# Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import ctypes
import os
import signal
import sys
import time

ROCPROFV3_ATTACH_DIR = os.path.dirname(os.path.realpath(__file__))
ROCM_DIR = os.path.dirname(ROCPROFV3_ATTACH_DIR)
ROCPROF_ATTACH_TOOL_LIBRARY = f"{ROCM_DIR}/lib/rocprofiler-sdk/librocprofv3-attach.so"


def main(
pid=os.environ.get("ROCPROF_ATTACH_PID", None),
attach_library=os.environ.get(
"ROCPROF_ATTACH_TOOL_LIBRARY", ROCPROF_ATTACH_TOOL_LIBRARY
),
duration=os.environ.get("ROCPROF_ATTACH_DURATION", None),
):
if pid is None:
raise RuntimeError("rocprofv3_attach called with no PID specified")

print(f"Attaching to PID {pid} using library {attach_library}")

# Load the shared library into ctypes and attach
try:
c_lib = ctypes.CDLL(attach_library)
c_lib.attach.restype = ctypes.c_int
c_lib.attach.argtypes = [ctypes.c_uint]
attach_status = c_lib.attach(int(pid))
except Exception as e:
raise RuntimeError(f"Exception during library load and attachment: {e}")

if attach_status != 0:
raise RuntimeError(
f"Calling attach in {attach_library} returned non-zero status {attach_status}"
)

print(f"Attaching to PID {pid} using library {attach_library} :: success")

def detach():
try:
c_lib.detach()
except Exception as e:
print(f"Exception during detachment: {e}")

def signal_handler(sig, frame):
print("\nCaught signal SIGINT, detaching")
detach()
sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

if duration is None:
sys.stdout.write("Press Enter to detach...")
sys.stdout.flush() # Force the prompt to appear immediately
input() # Now wait for input
else:
time.sleep(int(duration) / 1000)

detach()


if __name__ == "__main__":
main()
143 changes: 118 additions & 25 deletions source/bin/rocprofv3.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ def __init__(self, d):
[dotdict(i) if isinstance(i, (dict)) else i for i in v],
)

def __getstate__(self):
return self.__dict__

def __setstate__(self, d):
self.__dict__ = d


def patch_message(msg, *args):
msg = textwrap.dedent(msg)
Expand All @@ -72,14 +78,14 @@ def patch_message(msg, *args):

def fatal_error(msg, *args, exit_code=1):
msg = patch_message(msg, *args)
sys.stderr.write(f"Fatal error: {msg}\n")
sys.stderr.write(f"[rocprofv3] Fatal error: {msg}\n")
sys.stderr.flush()
sys.exit(exit_code)


def warning(msg, *args):
msg = patch_message(msg, *args)
sys.stderr.write(f"Warning: {msg}\n")
sys.stderr.write(f"[rocprofv3] Warning: {msg}\n")
sys.stderr.flush()


Expand Down Expand Up @@ -224,6 +230,11 @@ def parse_arguments(args=None):

$ mpirun -n 4 rocprofv3 --hip-trace -- ./mympiapp

For attachment profiling of running processes:

$ rocprofv3 --attach <PID> --hip-trace --kernel-trace
$ rocprofv3 --attach 1234 --attach-duration 10 --hsa-trace

"""

# Create the parser
Expand Down Expand Up @@ -715,13 +726,19 @@ def add_parser_bool_argument(gparser, *args, **kwargs):
metavar="KB",
)

reserved_options = parser.add_argument_group("Reserved options")
reserved_options.add_argument(
advanced_options.add_argument(
"-p",
"--pid",
help=argparse.SUPPRESS,
type=str,
nargs="+",
"--attach",
help="""Attach to a target process by pid and execute as a tool from within said process.""",
type=int,
default=None,
)

advanced_options.add_argument(
"--attach-duration-msec",
help="""When --pid is used, sets the amount of time in milliseconds the profiler will be attached before detaching. When unset, the profiler will wait until Enter is pressed to detach.""",
type=int,
default=None,
)

Expand Down Expand Up @@ -923,18 +940,27 @@ def patch_args(data):
return data


def get_args(cmd_args, inp_args):
def get_args(cmd_args, inp_args, filter=[]):
def ensure_type(name, var, type_id):
if not isinstance(var, type_id):
raise TypeError(
f"{name} is of type {type(var).__name__}, expected {type(type_id).__name__}"
f"{name} is of type {type(var).__name__}, expected {type_id.__name__}"
)

ensure_type("cmd_args", cmd_args, argparse.Namespace)
ensure_type("inp_args", inp_args, dotdict)
if isinstance(cmd_args, argparse.Namespace):
ensure_type("cmd_args", cmd_args, argparse.Namespace)
ensure_type("inp_args", inp_args, dotdict)

cmd_keys = list(cmd_args.__dict__.keys())
inp_keys = list(inp_args.keys())

else:
ensure_type("cmd_args", cmd_args, dotdict)
ensure_type("inp_args", inp_args, dotdict)

cmd_keys = list(cmd_args.keys())
inp_keys = list(inp_args.keys())

cmd_keys = list(cmd_args.__dict__.keys())
inp_keys = list(inp_args.keys())
data = {}

def get_attr(key):
Expand All @@ -950,9 +976,30 @@ def get_attr(key):
and has_set_attr(inp_args, itr)
and getattr(cmd_args, itr) != getattr(inp_args, itr)
):
raise RuntimeError(
f"conflicting value for {itr} : {getattr(cmd_args, itr)} vs {getattr(inp_args, itr)}"
)
should_raise = True
if filter:
is_filtered = False
for fitr in filter:
import re

if re.match(fitr, itr):
is_filtered = True
break

if not is_filtered:
warning(
f"Option '{itr}' has been modified. {itr}={getattr(cmd_args, itr)} (previously {itr}={getattr(inp_args, itr)})"
)
should_raise = False

# should raise error if not in filter list
if should_raise:
raise RuntimeError(
f"conflicting value for {itr} : {getattr(cmd_args, itr)} vs {getattr(inp_args, itr)}"
)
else:
# has preference towards command line args
data[itr] = get_attr(itr)
else:
data[itr] = get_attr(itr)

Expand All @@ -965,13 +1012,6 @@ def run(app_args, args, **kwargs):
use_execv = kwargs.get("use_execv", True)
app_pass = kwargs.get("pass_id", None)

if args.pid is not None:
fatal_error(
"""The -p shorthand option for --collection-period is now an upper-case -P
In the future, rocprofv3 plans to support debugger-like process attachment and -p
is de-facto standard shorthand option for this feature"""
)

def setattrifnone(obj, attr, value):
if getattr(obj, f"{attr}") is None:
setattr(obj, f"{attr}", value)
Expand Down Expand Up @@ -1058,6 +1098,7 @@ def _write_env_value():
ROCPROF_LIST_AVAIL_TOOL_LIBRARY = (
f"{ROCM_DIR}/libexec/rocprofiler-sdk/librocprofv3-list-avail.so"
)
ROCPROF_ATTACH_TOOL_LIBRARY = f"{ROCM_DIR}/lib/rocprofiler-sdk/librocprofv3-attach.so"

ROCPROF_TOOL_LIBRARY = resolve_library_path(ROCPROF_TOOL_LIBRARY, args)
ROCPROF_SDK_LIBRARY = resolve_library_path(ROCPROF_SDK_LIBRARY, args)
Expand All @@ -1066,15 +1107,17 @@ def _write_env_value():
ROCPROF_LIST_AVAIL_TOOL_LIBRARY = resolve_library_path(
ROCPROF_LIST_AVAIL_TOOL_LIBRARY, args
)
ROCPROF_ATTACH_TOOL_LIBRARY = resolve_library_path(ROCPROF_ATTACH_TOOL_LIBRARY, args)

prepend_preload = [itr for itr in args.preload if itr]
append_preload = [
ROCPROF_TOOL_LIBRARY,
ROCPROF_SDK_LIBRARY,
]

update_env("LD_PRELOAD", ":".join(prepend_preload), prepend=True)
update_env("LD_PRELOAD", ":".join(append_preload), append=True)
if not args.pid:
update_env("LD_PRELOAD", ":".join(prepend_preload), prepend=True)
update_env("LD_PRELOAD", ":".join(append_preload), append=True)

update_env(
"ROCP_TOOL_LIBRARIES",
Expand Down Expand Up @@ -1281,6 +1324,13 @@ def _write_env_value():
overwrite_if_true=True,
)

if args.pid:
update_env(
"ROCPROF_ATTACH_TOOL_LIBRARY",
ROCPROF_ATTACH_TOOL_LIBRARY,
overwrite_if_true=True,
)

if args.collection_period:
factors = {
"hour": 60 * 60 * 1e9,
Expand Down Expand Up @@ -1408,6 +1458,16 @@ def log_config(_env):
env=app_env,
)

elif args.pid:
update_env("ROCPROF_ATTACH_PID", args.pid)
if args.attach_duration_msec is not None:
update_env("ROCPROF_ATTACH_DURATION", f"{args.attach_duration_msec}")
path = os.path.join(f"{ROCM_DIR}", "bin/rocprofv3-attach")
if app_args:
exit_code = subprocess.check_call([sys.executable, path], env=app_env)
else:
app_args = [sys.executable, path]

elif not app_args and not args.echo:
log_config(app_env)
fatal_error("No application provided")
Expand Down Expand Up @@ -1637,6 +1697,39 @@ def main(argv=None):

if len(inp_args) == 1:
args = get_args(cmd_args, inp_args[0])

if args.pid:
import pickle

if args.collection_period:
fatal_error("--collection-period is not compatible with attach mode")

fname = f"/tmp/rocprofv3_attach_{args.pid}.pkl"
if os.path.exists(fname):
# load the configuration from the previous attachment
with open(fname, "rb") as ifs:
if args.log_level in ("config", "info", "trace"):
print(f"Loading attach configuration from {fname}...")
prev_args = pickle.load(ifs)

args = get_args(
args,
dotdict(prev_args),
filter=[
".*_trace",
"^pc_sampling_.*$",
"^att_.*$",
"^(pmc|pmc_groups|output_config|extra_counters)$",
"^kernel_(include_regex|exclude_regex|iteration_range)$",
],
)

# write the configuration for future attachments
with open(fname, "wb") as ofs:
if args.log_level in ("config", "info", "trace"):
print(f"Saving attach configuration to {fname}...")
pickle.dump(args, ofs)

pass_idx = None
if has_set_attr(args, "pmc") and len(args.pmc) > 0:
pass_idx = 1
Expand Down
2 changes: 2 additions & 0 deletions source/docs/_toc.yml.in
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ subtrees:
title: Tool library
- file: api-reference/intercept_table
title: Runtime intercept tables
- file: api-reference/process_attachment
title: Process attachment
- file: api-reference/buffered_services
title: Buffered services
- file: api-reference/callback_services
Expand Down
Loading