From 6f2a5a9646f87fef69dd844306373bc0aa574b2d Mon Sep 17 00:00:00 2001 From: "U, Srihari" Date: Wed, 9 Jul 2025 21:05:45 +0530 Subject: [PATCH] Add perfetto support for scratch memory (#303) * Add perfetto support for scratch memory * Updated tests and docs. * Update docs data * Added underflow check * Record all free events to 0 bytes * Add format * Address review comment * updated tests for scratch memory * update scratch-memory tests. --- CHANGELOG.md | 2 + source/docs/data/perfetto_scratch_memory.png | Bin 0 -> 63288 bytes source/docs/how-to/using-rocprofv3.rst | 30 +++++ .../include/rocprofiler-sdk/buffer_tracing.h | 1 + .../include/rocprofiler-sdk/cxx/perfetto.hpp | 6 +- .../cxx/serialization/save.hpp | 1 + source/lib/output/csv.hpp | 2 +- source/lib/output/generateCSV.cpp | 8 +- source/lib/output/generatePerfetto.cpp | 109 ++++++++++++++++-- .../rocprofiler-sdk/hsa/scratch_memory.cpp | 9 ++ tests/pytest-packages/tests/rocprofv3.py | 2 + tests/rocprofv3/scratch-memory/validate.py | 72 ++++++++++++ tests/scratch-memory-tracing/validate.py | 2 + 13 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 source/docs/data/perfetto_scratch_memory.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b2004b05..aba0370ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -191,6 +191,8 @@ Full documentation for ROCprofiler-SDK is available at [rocm.docs.amd.com/projec - See [using thread trace with rocprofv3](https://rocm.docs.amd.com/projects/rocprofiler-sdk/en/amd-mainline/how-to/using-thread-trace.html) - Requires the ROCprof Trace Decoder plugin installed (see above). - Added `rocpd` output format documentation + - Requires the ROCprof Trace Decoder plugin installed (see above) +- Added perfetto support for scratch memory. ### Changed diff --git a/source/docs/data/perfetto_scratch_memory.png b/source/docs/data/perfetto_scratch_memory.png new file mode 100644 index 0000000000000000000000000000000000000000..c8317c70a5bcdcc92f40bc73bd9ab9468c36c4ee GIT binary patch literal 63288 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

-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 + +In the Perfetto UI, scratch memory appears as counter tracks that show: + +- **Allocation peaks**: Each peak represents scratch memory allocation for a kernel execution +- **Memory usage over time**: The height of each peak indicates the amount of memory allocated (typically in KB) +- **Allocation/deallocation pattern**: You can observe when memory is allocated at kernel start and freed at kernel end + +For applications with multiple kernel iterations, you'll see multiple peaks in the scratch memory track, with each peak corresponding to a kernel execution. This visualization helps identify scratch memory usage patterns and potential optimization opportunities. + +.. image:: /data/perfetto_scratch_memory.png + :width: 100% + :align: center + +For comprehensive GPU execution insights, combine scratch memory tracing with kernel tracing: + +.. code-block:: bash + + rocprofv3 --kernel-trace --scratch-memory-trace --output-format pftrace -- + +This allows you to correlate scratch memory allocation patterns with specific kernel executions in the Perfetto visualization. Agent info ----------- diff --git a/source/include/rocprofiler-sdk/buffer_tracing.h b/source/include/rocprofiler-sdk/buffer_tracing.h index ba4e9dfae2..7f976bc62b 100644 --- a/source/include/rocprofiler-sdk/buffer_tracing.h +++ b/source/include/rocprofiler-sdk/buffer_tracing.h @@ -509,6 +509,7 @@ typedef struct rocprofiler_buffer_tracing_scratch_memory_record_t rocprofiler_timestamp_t start_timestamp; ///< start time in nanoseconds rocprofiler_timestamp_t end_timestamp; ///< end time in nanoseconds rocprofiler_scratch_alloc_flag_t flags; + uint64_t allocation_size; ///< size of scratch memory allocation in bytes } rocprofiler_buffer_tracing_scratch_memory_record_t; /** diff --git a/source/include/rocprofiler-sdk/cxx/perfetto.hpp b/source/include/rocprofiler-sdk/cxx/perfetto.hpp index 64006765b2..a83fb2cd37 100644 --- a/source/include/rocprofiler-sdk/cxx/perfetto.hpp +++ b/source/include/rocprofiler-sdk/cxx/perfetto.hpp @@ -102,6 +102,7 @@ ROCPROFILER_DEFINE_CATEGORY(category, rocdecode_api, "rocDecode API function") ROCPROFILER_DEFINE_CATEGORY(category, rocjpeg_api, "rocJPEG API function") ROCPROFILER_DEFINE_CATEGORY(category, counter_collection, "Counter Collection") ROCPROFILER_DEFINE_CATEGORY(category, kfd_events, "KFD events collection") +ROCPROFILER_DEFINE_CATEGORY(category, scratch_memory, "Scratch Memory Allocation") ROCPROFILER_DEFINE_CATEGORY(category, none, "Unknown category") #define ROCPROFILER_PERFETTO_CATEGORIES \ @@ -116,6 +117,7 @@ ROCPROFILER_DEFINE_CATEGORY(category, none, "Unknown category") ROCPROFILER_PERFETTO_CATEGORY(category::memory_allocation), \ ROCPROFILER_PERFETTO_CATEGORY(category::rocdecode_api), \ ROCPROFILER_PERFETTO_CATEGORY(category::rocjpeg_api), \ + ROCPROFILER_PERFETTO_CATEGORY(category::scratch_memory), \ ROCPROFILER_PERFETTO_CATEGORY(category::none) #include @@ -217,7 +219,7 @@ ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(MARKER_NAME_API, marker_api) ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(MEMORY_COPY, memory_copy) ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(MEMORY_ALLOCATION, memory_allocation) ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(KERNEL_DISPATCH, kernel_dispatch) -ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(SCRATCH_MEMORY, memory_allocation) +ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(SCRATCH_MEMORY, scratch_memory) ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(CORRELATION_ID_RETIREMENT, none) ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(RCCL_API, rccl_api) ROCPROFILER_PERFETTO_BUFFER_TRACING_CATEGORY(OMPT, openmp) @@ -249,7 +251,7 @@ ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(MARKER_CORE_RANGE_API, marker_api ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(MARKER_CONTROL_API, marker_api) ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(MARKER_NAME_API, marker_api) ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(CODE_OBJECT, none) -ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(SCRATCH_MEMORY, memory_allocation) +ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(SCRATCH_MEMORY, scratch_memory) ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(KERNEL_DISPATCH, kernel_dispatch) ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(MEMORY_COPY, memory_copy) ROCPROFILER_PERFETTO_CALLBACK_TRACING_CATEGORY(MEMORY_ALLOCATION, memory_allocation) diff --git a/source/include/rocprofiler-sdk/cxx/serialization/save.hpp b/source/include/rocprofiler-sdk/cxx/serialization/save.hpp index d3bea2a421..d2edc5c274 100644 --- a/source/include/rocprofiler-sdk/cxx/serialization/save.hpp +++ b/source/include/rocprofiler-sdk/cxx/serialization/save.hpp @@ -848,6 +848,7 @@ save(ArchiveT& ar, rocprofiler_buffer_tracing_scratch_memory_record_t data) ROCP_SDK_SAVE_DATA_FIELD(end_timestamp); ROCP_SDK_SAVE_DATA_FIELD(correlation_id); ROCP_SDK_SAVE_DATA_FIELD(flags); + ROCP_SDK_SAVE_DATA_FIELD(allocation_size); } template diff --git a/source/lib/output/csv.hpp b/source/lib/output/csv.hpp index 4bd2d1b45e..90a7c98f05 100644 --- a/source/lib/output/csv.hpp +++ b/source/lib/output/csv.hpp @@ -106,7 +106,7 @@ using memory_allocation_csv_encoder = csv_encoder<8>; using marker_csv_encoder = csv_encoder<7>; using list_basic_metrics_csv_encoder = csv_encoder<5>; using list_derived_metrics_csv_encoder = csv_encoder<5>; -using scratch_memory_encoder = csv_encoder<8>; +using scratch_memory_encoder = csv_encoder<9>; using stats_csv_encoder = csv_encoder<8>; using pc_sampling_host_trap_csv_encoder = csv_encoder<6>; using kernel_trace_with_stream_csv_encoder = csv_encoder<22>; diff --git a/source/lib/output/generateCSV.cpp b/source/lib/output/generateCSV.cpp index e8dacc9511..bb68d12017 100644 --- a/source/lib/output/generateCSV.cpp +++ b/source/lib/output/generateCSV.cpp @@ -472,11 +472,11 @@ generate_csv(const output_config& {"Kind", "Operation", "Agent_Id", - "Allocation_Size", "Address", "Correlation_Id", "Start_Timestamp", - "End_Timestamp"}}; + "End_Timestamp", + "Allocation_Size"}}; for(auto ditr : data) { for(auto record : data.get(ditr)) @@ -677,6 +677,7 @@ generate_csv(const output_config& "Alloc_Flags", "Start_Timestamp", "End_Timestamp", + "Allocation_Size", }}; for(auto ditr : data) @@ -696,7 +697,8 @@ generate_csv(const output_config& record.thread_id, record.flags, record.start_timestamp, - record.end_timestamp); + record.end_timestamp, + record.allocation_size); ofs << row_ss.str(); } diff --git a/source/lib/output/generatePerfetto.cpp b/source/lib/output/generatePerfetto.cpp index cea6feb910..56e365819c 100644 --- a/source/lib/output/generatePerfetto.cpp +++ b/source/lib/output/generatePerfetto.cpp @@ -65,16 +65,16 @@ get_hash_id(Tp&& _val) void write_perfetto( - const output_config& ocfg, - const metadata& tool_metadata, - std::vector agent_data, - const generator& hip_api_gen, - const generator& hsa_api_gen, - const generator& kernel_dispatch_gen, - const generator& memory_copy_gen, - const generator& counter_collection_gen, - const generator& marker_api_gen, - const generator& /*scratch_memory_gen*/, + const output_config& ocfg, + const metadata& tool_metadata, + std::vector agent_data, + const generator& hip_api_gen, + const generator& hsa_api_gen, + const generator& kernel_dispatch_gen, + const generator& memory_copy_gen, + const generator& counter_collection_gen, + const generator& marker_api_gen, + const generator& scratch_memory_gen, const generator& rccl_api_gen, const generator& memory_allocation_gen, const generator& rocdecode_api_gen, @@ -1010,6 +1010,95 @@ write_perfetto( tracing_session->FlushBlocking(); } } + + // scratch memory counter track + auto scratch_mem_endpoints = + std::unordered_map>{}; + auto scratch_mem_extremes = std::pair{ + std::numeric_limits::max(), std::numeric_limits::min()}; + + // Load scratch memory usage endpoints + for(auto ditr : scratch_memory_gen) + for(auto itr : scratch_memory_gen.get(ditr)) + { + // Track start and end timestamps for this scratch memory record + scratch_mem_endpoints[itr.agent_id].emplace(itr.start_timestamp, 0); + scratch_mem_endpoints[itr.agent_id].emplace(itr.end_timestamp, 0); + + // Update overall time range + scratch_mem_extremes = + std::make_pair(std::min(scratch_mem_extremes.first, itr.start_timestamp), + std::max(scratch_mem_extremes.second, itr.end_timestamp)); + } + + // Load values at each endpoint + for(auto ditr : scratch_memory_gen) + for(auto itr : scratch_memory_gen.get(ditr)) + { + // For each timestamp in the range of this record + auto begin = + scratch_mem_endpoints.at(itr.agent_id).lower_bound(itr.start_timestamp); + auto end = scratch_mem_endpoints.at(itr.agent_id).upper_bound(itr.end_timestamp); + + for(auto mitr = begin; mitr != end; ++mitr) + { + // Add scratch memory size to the counter value at this timestamp + if(itr.operation == ROCPROFILER_SCRATCH_MEMORY_ALLOC) + mitr->second = itr.allocation_size; + else if(itr.operation == ROCPROFILER_SCRATCH_MEMORY_FREE) + mitr->second = 0; // For all free events current allocation drops to 0. + } + } + + // Create counter tracks for visualization + auto scratch_mem_tracks = + std::unordered_map{}; + auto scratch_mem_names = std::vector{}; + scratch_mem_names.reserve(scratch_mem_endpoints.size()); + + for(auto& mitr : scratch_mem_endpoints) + { + // Add buffer timestamps for better visualization + if(!mitr.second.empty()) + { + scratch_mem_endpoints[mitr.first].emplace( + scratch_mem_extremes.first - extremes_endpoint_buffer, 0); + scratch_mem_endpoints[mitr.first].emplace( + scratch_mem_extremes.second + extremes_endpoint_buffer, 0); + + auto _track_name = std::stringstream{}; + const auto* _agent = _get_agent(mitr.first); + auto agent_index_info = + tool_metadata.get_agent_index(_agent->id, ocfg.agent_index_value); + _track_name << "SCRATCH MEMORY on " << agent_index_info.label << " [" + << agent_index_info.index << "] (" << agent_index_info.type << ")"; + + constexpr auto _unit = ::perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES; + auto& _name = scratch_mem_names.emplace_back(_track_name.str()); + scratch_mem_tracks.emplace(mitr.first, + ::perfetto::CounterTrack{_name.c_str()} + .set_unit(_unit) + .set_unit_multiplier(bytes_multiplier) + .set_is_incremental(false)); + } + } + + // Write counter values to perfetto trace + for(auto& mitr : scratch_mem_endpoints) + { + if(scratch_mem_tracks.count(mitr.first) > 0) + { + for(auto itr : mitr.second) + { + TRACE_COUNTER(sdk::perfetto_category::name, + scratch_mem_tracks.at(mitr.first), + itr.first, + itr.second / bytes_multiplier); + tracing_session->FlushBlocking(); + } + } + } } // Create counter tracks per agent diff --git a/source/lib/rocprofiler-sdk/hsa/scratch_memory.cpp b/source/lib/rocprofiler-sdk/hsa/scratch_memory.cpp index 75834fc241..2eb3fd36e0 100644 --- a/source/lib/rocprofiler-sdk/hsa/scratch_memory.cpp +++ b/source/lib/rocprofiler-sdk/hsa/scratch_memory.cpp @@ -522,6 +522,7 @@ impl(Args... args) tls.buffered_data.queue_id = {event_data.scratch_alloc_start->queue->id}; tls.buffered_data.thread_id = thr_id; tls.buffered_data.start_timestamp = common::timestamp_ns(); + tls.buffered_data.allocation_size = 0; } } else if constexpr(OpPhase == ROCPROFILER_CALLBACK_PHASE_EXIT) @@ -530,6 +531,14 @@ impl(Args... args) { tls.buffered_data.flags = get_flags(event_data); tls.buffered_data.end_timestamp = common::timestamp_ns(); + if constexpr(OpIdx == ROCPROFILER_SCRATCH_MEMORY_ALLOC) + { + tls.buffered_data.allocation_size = event_data.scratch_alloc_end->size; + } + else if constexpr(OpIdx == ROCPROFILER_SCRATCH_MEMORY_FREE) + { + tls.buffered_data.allocation_size = 0; + } } if(!tls.callback_contexts.empty()) diff --git a/tests/pytest-packages/tests/rocprofv3.py b/tests/pytest-packages/tests/rocprofv3.py index 9ece7ceae3..48f018eb0d 100644 --- a/tests/pytest-packages/tests/rocprofv3.py +++ b/tests/pytest-packages/tests/rocprofv3.py @@ -36,6 +36,7 @@ def test_perfetto_data( "rocdecode_api", "rocjpeg_api", "counter_collection", + "scratch_memory", ), ): @@ -49,6 +50,7 @@ def test_perfetto_data( "rocdecode_api": ("rocdecode_api", "rocdecode_api"), "rocjpeg_api": ("rocjpeg_api", "rocjpeg_api"), "counter_collection": ("counter_collection", "counter_collection"), + "scratch_memory": ("scratch_memory", "scratch_memory"), } # make sure they specified valid categories diff --git a/tests/rocprofv3/scratch-memory/validate.py b/tests/rocprofv3/scratch-memory/validate.py index d04214c11e..105694afeb 100755 --- a/tests/rocprofv3/scratch-memory/validate.py +++ b/tests/rocprofv3/scratch-memory/validate.py @@ -74,6 +74,7 @@ def test_scratch_memory(json_input_data, csv_input_data): assert "thread_id" in node assert "end_timestamp" in node assert "start_timestamp" in node + assert "allocation_size" in node assert "queue_id" in node assert "agent_id" in node @@ -88,6 +89,22 @@ def test_scratch_memory(json_input_data, csv_input_data): assert node.end_timestamp > 0 assert node.start_timestamp < node.end_timestamp + # validation for allocation size based on operation + operation = bf_op_names[node["operation"]] + if operation == "SCRATCH_MEMORY_FREE": + # For free events, allocation size must be exactly 0 + assert ( + node["allocation_size"] == 0 + ), f"Free operation should have allocation_size=0, got {node['allocation_size']}" + elif operation == "SCRATCH_MEMORY_ALLOC": + # Fixme: For alloc events, must be > 0 and < 32GB + assert ( + node["allocation_size"] > 0 + ), f"Alloc operation should have allocation_size > 0, got {node['allocation_size']}" + assert ( + node["allocation_size"] < 32000000000 + ), f"Alloc operation size should be < 32GB, got {node['allocation_size']}" + assert data.strings.buffer_records[node.kind].kind == "SCRATCH_MEMORY" assert ( data.strings.buffer_records[node.kind].operations[node.operation] @@ -96,6 +113,7 @@ def test_scratch_memory(json_input_data, csv_input_data): scratch_reported_agent_ids.add(node["agent_id"]["handle"]) + verify_scratch_memory_alternating_pattern(scratch_memory_data, bf_op_names) assert 2**64 - 1 not in scratch_reported_agent_ids assert scratch_reported_agent_ids == detected_agents_ids @@ -115,6 +133,9 @@ def test_scratch_memory(json_input_data, csv_input_data): assert ( "Thread_Id" in row ), "Thread_Id header not present in csv for scratch memory trace." + assert ( + "Allocation_Size" in row + ), "Allocation_Size header not present in csv for scratch memory trace." assert ( "Alloc_Flags" in row ), "Alloc_Flags header not present in csv for scratch memory trace." @@ -130,11 +151,62 @@ def test_scratch_memory(json_input_data, csv_input_data): assert int(row["Agent_Id"].split(" ")[-1]) >= 0 assert int(row["Queue_Id"]) > 0 assert int(row["Thread_Id"]) > 0 + assert int(row["Allocation_Size"]) >= 0 assert int(row["Start_Timestamp"]) > 0 assert int(row["End_Timestamp"]) > 0 assert int(row["Start_Timestamp"]) < int(row["End_Timestamp"]) +def verify_scratch_memory_alternating_pattern(scratch_memory_data, bf_op_names): + """ + Verify that operations follow ALLOC→FREE→ALLOC→FREE pattern per (thread, flags) combination. + """ + # Track operations by thread and flags + thread_flag_operations = {} + + for node in scratch_memory_data: + thread_id = node["thread_id"] + operation = node["operation"] # Numeric (1=ALLOC, 2=FREE) + flags = node["flags"] + timestamp = node["start_timestamp"] + + key = (thread_id, flags) + if key not in thread_flag_operations: + thread_flag_operations[key] = [] + + thread_flag_operations[key].append((timestamp, operation)) + + # Verify proper alternating sequence for each thread+flags combination + for (thread_id, flags), operations in thread_flag_operations.items(): + # Sort by timestamp to ensure chronological order + sorted_ops = [op for _, op in sorted(operations)] + + # Must start with ALLOC (operation code 1) + if sorted_ops and sorted_ops[0] != 1: + raise AssertionError( + f"Thread {thread_id}, Flags {flags}: Must start with ALLOC, found operation code {sorted_ops[0]}" + ) + + # Check for alternating pattern - expected pattern is ALLOC→FREE→ALLOC→FREE + for i in range(len(sorted_ops)): + expected = 1 if i % 2 == 0 else 2 # 1=ALLOC, 2=FREE + if sorted_ops[i] != expected: + op_name = ( + bf_op_names[sorted_ops[i]] + if sorted_ops[i] < len(bf_op_names) + else f"Unknown({sorted_ops[i]})" + ) + expected_name = ( + bf_op_names[expected] + if expected < len(bf_op_names) + else f"Unknown({expected})" + ) + + raise AssertionError( + f"Thread {thread_id}, Flags {flags}: Operation #{i+1} should be {expected_name} (code {expected}), found {op_name} (code {sorted_ops[i]})" + ) + + if __name__ == "__main__": exit_code = pytest.main(["-x", __file__] + sys.argv[1:]) sys.exit(exit_code) diff --git a/tests/scratch-memory-tracing/validate.py b/tests/scratch-memory-tracing/validate.py index da9065880c..2d871ec52b 100755 --- a/tests/scratch-memory-tracing/validate.py +++ b/tests/scratch-memory-tracing/validate.py @@ -232,6 +232,8 @@ def test_scratch_memory_tracking(input_data): assert "operation" in node assert "handle" in node["queue_id"] + assert "allocation_size" in node + assert node["start_timestamp"] > 0 assert node["start_timestamp"] < node["end_timestamp"]