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
214 lines
11 KiB
Markdown
214 lines
11 KiB
Markdown
# Write rocpd from the native tool via profiler-hub
|
|
|
|
## Motivation
|
|
|
|
During a profiling run rocprofiler-compute produces data from two in-process libraries,
|
|
both loaded via `LD_PRELOAD` into the target application
|
|
(`src/rocprof_compute_profile/profiler_rocprofiler_sdk.py`):
|
|
|
|
1. The rocprofiler-sdk "SDK tool", run with `ROCPROF_OUTPUT_FORMAT=rocpd`, writes a rocpd
|
|
SQLite database (kernel dispatches, agents, kernel symbols, an `rocpd_info_pmc` table,
|
|
and an empty `rocpd_pmc_event` table). It is told NOT to collect counters
|
|
(`ROCPROF_COUNTER_COLLECTION=0`).
|
|
2. The rocprofiler-compute "native tool" (`src/lib/rocprofiler_compute_tool`) collects the
|
|
hardware counters and currently writes a CSV (`<pid>_native_counter_collection.csv`),
|
|
which the Python layer parses and injects row by row into the SDK rocpd's
|
|
`rocpd_pmc_event` table (`src/utils/rocpd_data.py`, `src/utils/utils_profile.py`).
|
|
|
|
This replaces the CSV round-trip: the native tool writes its own rocpd directly via
|
|
profiler-hub, and the merge into the SDK rocpd moves to analyze mode as a single set-based
|
|
SQL statement instead of bespoke CSV parsing in profile mode. Each tool keeps its own
|
|
rocpd; they are never co-mingled into one schema.
|
|
|
|
This builds on separate, in-flight work that consolidates each tool's per-PID rocpds into a
|
|
single rocpd per tool; that work must land first. This design takes the two consolidated
|
|
rocpds (one per tool, per application-replay pass) as input and does not change how they
|
|
are produced.
|
|
|
|
Scope is counters only. ISA / code-object collection stays as the existing JSON output:
|
|
it backs PC sampling, which rocpd has no schema or profiler-hub writer support for yet, so
|
|
there is nowhere for that data to land.
|
|
|
|
## Data flow
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
App["target application<br/>(both libs via LD_PRELOAD)"]
|
|
App --> SDK["rocprofiler-sdk 'SDK tool'<br/>ROCPROF_COUNTER_COLLECTION=0"]
|
|
App --> NAT["rocprofiler-compute 'native tool'<br/>(HW counters)"]
|
|
|
|
SDK -->|writes rocpd| SDKDB[("consolidated SDK rocpd (per pass)<br/>kernel_dispatch (pid, dispatch_id, event_id)<br/>info_pmc<br/>pmc_event [EMPTY]")]
|
|
NAT -->|writes rocpd via profiler-hub| NATDB[("consolidated native rocpd (per pass)<br/>info_pmc (symbol, target_arch)<br/>event (pid, correlation_id = dispatch_id)<br/>pmc_event (value)")]
|
|
|
|
SDKDB --> MERGE["analyze merge (per pass)<br/>ATTACH + INSERT...SELECT<br/>(pid, dispatch_id) -> SDK event_id<br/>symbol + target_arch -> SDK pmc_id"]
|
|
NATDB --> MERGE
|
|
|
|
MERGE --> OUT[("SDK pmc_event populated<br/>-> counters_collection view -> pmc_perf.csv")]
|
|
```
|
|
|
|
## profiler-hub intro
|
|
|
|
[profiler-hub](https://github.com/ROCm/rocm-systems/tree/develop/profilers/profiler-hub)
|
|
is a standalone library for writing rocpd (SQLite) databases. Its public API
|
|
is `storage_t` plus `writer_t` (register/insert calls for the rocpd tables).
|
|
|
|
Key behavior: `writer_t` mints a fresh `rocpd_event` per insert, so a pmc event and a
|
|
dispatch cannot share an `event_id` through the API; dispatch linkage is carried on
|
|
`event_data_t.correlation_id`, not a shared PK.
|
|
|
|
Nothing consumes profiler-hub yet and it is not part of the TheRock build;
|
|
rocprofiler-compute will be its first consumer.
|
|
|
|
## Native tool C++ changes
|
|
|
|
Add `RocpdCountersWriter : public CountersWriter` (the existing abstraction in
|
|
`counters_writer.h`) as the default `g_counters_writer`, demoting the CSV writer, which
|
|
stays for tests. The native tool always writes a `_native_counter_collection.db` rocpd (no
|
|
env-var gating).
|
|
|
|
`write_counters` opens `storage_t` + `writer_t`, registers the FK-target info rows once
|
|
(node, process, agent per distinct `agent_id`, pmc per distinct counter into
|
|
`rocpd_info_pmc`), then per counter record calls `insert_pmc_event_data` with
|
|
`correlation_id = dispatch_id`. It writes ONLY the counter tables; kernel_dispatch,
|
|
kernel_symbol, and code_object come from the SDK rocpd at merge time.
|
|
|
|
Agent and counter metadata must be captured in the callbacks, which today keep only the
|
|
agent handle and counter name. `register_agent_info` / `register_pmc_info` need more (agent
|
|
type/name/node, counter `target_arch`): wrap `rocprofiler_query_available_agents` for the
|
|
agent fields and retain the full `rocprofiler_counter_info_v0_t` already queried for
|
|
counters. v0 has no `event_code`, so that column stays null as the SDK tool leaves it
|
|
(confirmed null for every counter in the verified rocflop rocpd).
|
|
|
|
The native `register_pmc_info` must write the same `target_arch` the SDK tool uses so the
|
|
merge join matches. In the verified rocflop rocpd that value is the literal string `GPU`
|
|
for hardware counters, not a gfx arch, so the native writer sets `target_arch = "GPU"`.
|
|
|
|
## Analyze layer changes
|
|
|
|
Replace the profile-mode CSV read + row-by-row insert (`update_rocpd_pmc_events` in
|
|
`src/utils/rocpd_data.py`) with a set-based merge that runs in analyze mode. For each
|
|
application-replay pass, given the consolidated SDK and native rocpds:
|
|
|
|
```sql
|
|
ATTACH DATABASE :native_db AS native;
|
|
|
|
INSERT INTO "<sdk_pmc_event_table>" (guid, event_id, pmc_id, value)
|
|
SELECT
|
|
sdk_kd.guid,
|
|
sdk_kd.event_id, -- remapped to SDK event id space
|
|
sdk_pmc.id, -- remapped to SDK info_pmc id space
|
|
n_pmc.value
|
|
FROM native.rocpd_pmc_event AS n_pmc
|
|
JOIN native.rocpd_event AS n_ev
|
|
ON n_ev.id = n_pmc.event_id
|
|
JOIN rocpd_kernel_dispatch AS sdk_kd
|
|
ON sdk_kd.dispatch_id = n_ev.correlation_id -- dispatch_id carrier
|
|
AND sdk_kd.pid = n_ev.pid -- pid scopes the dispatch
|
|
JOIN native.rocpd_info_pmc AS n_info
|
|
ON n_info.id = n_pmc.pmc_id
|
|
JOIN rocpd_info_pmc AS sdk_pmc
|
|
ON sdk_pmc.symbol = n_info.symbol
|
|
AND sdk_pmc.target_arch IS n_info.target_arch;
|
|
|
|
DETACH DATABASE native;
|
|
```
|
|
|
|
The read side uses the per-db base-name views (`rocpd_pmc_event`, `rocpd_event`,
|
|
`rocpd_kernel_dispatch`, `rocpd_info_pmc`) that the rocpd schema bakes into every db as
|
|
`SELECT * FROM <table>_<guid>`. Both the SDK db and the native db carry them (profiler-hub
|
|
writes the same rocpd schema), so the merge never computes a guid suffix to read. The two
|
|
dbs stay distinct through the `native.` qualifier versus the main schema, which is required
|
|
so the `symbol` to SDK `pmc_id` remap joins native `rocpd_info_pmc` against the SDK
|
|
`rocpd_info_pmc` instead of collapsing them. This is also why the rocpd Python package is
|
|
not used for the merge: its `connect()` unions same-base tables across all inputs, which
|
|
would merge the two `rocpd_info_pmc` tables and destroy that cross-tool identity.
|
|
|
|
These base-name views are plain `SELECT *` views with no INSERT triggers, so they are not
|
|
writable. The INSERT target therefore stays the physical SDK `rocpd_pmc_event_<guid>` table.
|
|
|
|
The remap is needed because `event_id` is assigned independently by each tool, while
|
|
`(pid, dispatch_id)` is shared, so it is the stable key to recover the SDK `event_id`. The
|
|
merge relies on two verified SDK-rocpd facts: `rocpd_pmc_event` and `rocpd_kernel_dispatch`
|
|
both reference `rocpd_event` via `event_id` and `rocpd_kernel_dispatch` carries
|
|
`dispatch_id`; and counter identity lives in `rocpd_info_pmc` (referenced by `pmc_id`),
|
|
matched across tools on `symbol` + `target_arch`. The analysis counters view joins pmc to
|
|
dispatch on the shared `event_id`, so the merge must land SDK `event_id` values.
|
|
|
|
After the merge, read the SDK `counters_collection` view and concatenate the result across
|
|
all passes into `pmc_perf.csv`; the rest of the analyze pipeline is unchanged. Profile mode
|
|
drops its native-counter injection and the now-dead CSV plumbing
|
|
(`src/utils/utils_profile_csv.py`).
|
|
|
|
The `symbol` + `target_arch` join key is unique: in the verified rocflop rocpd all 15
|
|
`rocpd_info_pmc` rows have distinct `symbol` values.
|
|
|
|
## Packaging changes
|
|
|
|
Build profiler-hub from source in-repo via `ExternalProject_Add`, linked statically into
|
|
the single `librocprofiler-compute-tool.so`. `add_subdirectory` (one CMake configure) will
|
|
not work: profiler-hub FetchContent's its own fmt, spdlog, nlohmann_json, and sqlite3,
|
|
which collide with the fmt, json, and googletest under `src/lib/external/` and duplicate
|
|
those targets. A separate CMake invocation isolates its dependency resolution.
|
|
|
|
The sparse-checkout guard and `ExternalProject_Add` go in `src/lib/CMakeLists.txt`, not the
|
|
top-level `CMakeLists.txt`, because two configure entry points build the native tool and
|
|
only `src/lib` is common to both:
|
|
|
|
- Full / install build (TheRock CI): top-level `CMakeLists.txt` reaches it via
|
|
`add_subdirectory(src/lib)`.
|
|
- Runtime fallback build: the Python layer configures `src/lib` directly
|
|
(`src/utils/native_tool_finder.py`) when no installed library is found, never running the
|
|
top-level `CMakeLists.txt`.
|
|
|
|
`src/lib/CMakeLists.txt` is a standalone `project()`, so it configures either way, and the
|
|
guard resolves the monorepo root via `git rev-parse --show-toplevel` regardless of entry
|
|
point. The guard materializes `profilers/profiler-hub` for sparse checkouts (a no-op for
|
|
full checkouts) before `ExternalProject_Add` builds it; requires cone-mode sparse checkout
|
|
(git >= 2.27 default):
|
|
|
|
```cmake
|
|
set(PROFILER_HUB_PATH profilers/profiler-hub)
|
|
|
|
execute_process(
|
|
COMMAND ${GIT_EXECUTABLE} rev-parse --show-toplevel
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
OUTPUT_VARIABLE REPO_ROOT OUTPUT_STRIP_TRAILING_WHITESPACE)
|
|
|
|
execute_process(
|
|
COMMAND ${GIT_EXECUTABLE} config --get core.sparseCheckout
|
|
WORKING_DIRECTORY ${REPO_ROOT}
|
|
OUTPUT_VARIABLE SPARSE_ENABLED OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
|
|
|
if(SPARSE_ENABLED STREQUAL "true" AND NOT EXISTS "${REPO_ROOT}/${PROFILER_HUB_PATH}/CMakeLists.txt")
|
|
message(STATUS "Adding profiler-hub to sparse-checkout...")
|
|
execute_process(
|
|
COMMAND ${GIT_EXECUTABLE} sparse-checkout add ${PROFILER_HUB_PATH}
|
|
WORKING_DIRECTORY ${REPO_ROOT} RESULT_VARIABLE rc)
|
|
if(NOT rc EQUAL 0)
|
|
message(FATAL_ERROR "Failed to expand sparse-checkout for profiler-hub")
|
|
endif()
|
|
endif()
|
|
# source now guaranteed on disk; ExternalProject_Add below builds it in isolation.
|
|
```
|
|
|
|
- In `src/lib/CMakeLists.txt`, add an `ExternalProject_Add` (or equivalent) that builds
|
|
profiler-hub from `profilers/profiler-hub` with `PROFILER_HUB_BUILD_TESTS=OFF`,
|
|
`PROFILER_HUB_BUILD_BENCHMARKS=OFF`, default bundled schema, producing
|
|
`libprofiler-hub.a`.
|
|
- Link `profiler-hub-static` into `rocprofiler-compute-tool`
|
|
(`src/lib/rocprofiler_compute_tool/CMakeLists.txt`).
|
|
|
|
## Building and testing
|
|
|
|
- Clean-from-scratch build, confirming the ExternalProject wiring and dependency isolation
|
|
hold.
|
|
- Run a small workload (e.g. `tests/vcopy`) with the native tool preloaded; confirm the
|
|
`_native_counter_collection.db` has `rocpd_info_pmc`, `rocpd_event` (with
|
|
`correlation_id`), and `rocpd_pmc_event` populated.
|
|
- Run the analyze-mode merge against real consolidated rocpds; confirm `rocpd_pmc_event`
|
|
fills, the counters view joins cleanly, and `pmc_perf.csv` matches expected dispatch x
|
|
counter rows.
|
|
- Cover the pure logic (merge SQL, per-pass native/SDK db pairing) with new unit tests.
|
|
- Run the full `ctest` suite and ensure it passes, to catch regressions in the existing
|
|
CSV-era and analysis paths the merge replaces.
|