diff --git a/source/lib/output/generateRocpd.cpp b/source/lib/output/generateRocpd.cpp index 6899919ade..71ac2340cf 100644 --- a/source/lib/output/generateRocpd.cpp +++ b/source/lib/output/generateRocpd.cpp @@ -1167,8 +1167,16 @@ write_rocpd( } }; + struct agent_and_size + { + uint64_t agent_abs_index = {}; + uint64_t size = {0}; + }; + auto address_to_agent_and_size = std::unordered_map{}; + auto insert_memory_alloc_data = - [&conn, &tool_metadata, &string_entries, node_id, this_pid](const auto& _gen) { + [&conn, &tool_metadata, &string_entries, node_id, this_pid, &address_to_agent_and_size]( + const auto& _gen) { for(auto pitr : _gen) { auto _deferred = sql::deferred_transaction{conn}; @@ -1188,17 +1196,60 @@ write_rocpd( ROCP_FATAL_IF(_level != "REAL" && _level != "VIRTUAL" && _level != "SCRATCH") << "erroneous db level: " << _level; - auto _node_id = std::optional{}; - if(_type == "ALLOC") - { - _node_id = tool_metadata.get_agent(itr.agent_id)->node_id; - } - auto _stream_id = get_stream_id(extract_stream_field(itr)); auto _queue_id = get_queue_id(extract_queue_field(itr)); auto _address = extract_address_field(itr); auto _allocation_size = extract_allocation_size_field(itr); + // memory allocation counter track + struct free_memory_information + { + rocprofiler_timestamp_t start_timestamp = 0; + rocprofiler_timestamp_t end_timestamp = 0; + rocprofiler_address_t address = {.handle = 0}; + }; + + auto _node_id = std::optional{}; + auto free_mem_info = std::vector{}; + if(_type == "ALLOC") + { + _node_id = tool_metadata.get_agent(itr.agent_id)->node_id; + address_to_agent_and_size.emplace( + rocprofiler_address_t{.handle = _address.handle}, + agent_and_size{_node_id.value(), _allocation_size}); + } + else if(_type == "FREE") + { + // Store free memory operations in seperate vector to pair with agent + // and allocation size in following loop + free_mem_info.push_back(free_memory_information{ + itr.start_timestamp, itr.end_timestamp, _address}); + if(address_to_agent_and_size.count(_address) == 0) + { + if(_address.handle == 0) + { + // Freeing null pointers is expected behavior and is occurs in HSA + // functions like hipStreamDestroy + ROCP_INFO << "null pointer freed due to HSA operation"; + } + else + { + // Following should not occur + ROCP_INFO << "Unpaired free operation occurred"; + } + } + else + { + auto [agent_abs_index, size] = address_to_agent_and_size[_address]; + _node_id = agent_abs_index; + _allocation_size = 0; + } + } + else + { + ROCP_CI_LOG(WARNING) << "unhandled memory allocation type " << _type; + } + auto evt_id = create_event( conn, { diff --git a/source/lib/python/rocpd/libpyrocpd.cpp b/source/lib/python/rocpd/libpyrocpd.cpp index 0f62325724..3cd578ebf5 100644 --- a/source/lib/python/rocpd/libpyrocpd.cpp +++ b/source/lib/python/rocpd/libpyrocpd.cpp @@ -460,6 +460,9 @@ PYBIND11_MODULE(libpyrocpd, pyrocpd) auto memory_copies = rocpd::sql_generator{ conn, select_guid_nid_pid("memory_copies")}; + + auto scratch_memory = rocpd::sql_generator{ + conn, select_guid_nid_pid("scratch_memory")}; auto counters = rocpd::sql_generator{ conn, select_guid_nid_pid("counters_collection")}; @@ -496,6 +499,7 @@ PYBIND11_MODULE(libpyrocpd, pyrocpd) samples, kernels, memory_copies, + scratch_memory, memory_allocations, counters); } diff --git a/source/lib/python/rocpd/source/perfetto.cpp b/source/lib/python/rocpd/source/perfetto.cpp index 95d08b7719..55ffa2f41e 100644 --- a/source/lib/python/rocpd/source/perfetto.cpp +++ b/source/lib/python/rocpd/source/perfetto.cpp @@ -174,6 +174,7 @@ write_perfetto( const tool::generator& sample_gen, const tool::generator& kernel_dispatch_gen, const tool::generator& memory_copy_gen, + const tool::generator& scratch_memory_gen, const tool::generator& memory_allocation_gen, const tool::generator& counter_collection_gen) { @@ -687,13 +688,15 @@ write_perfetto( rocprofiler_timestamp_t start_timestamp = 0; rocprofiler_timestamp_t end_timestamp = 0; rocprofiler_address_t address = {.handle = 0}; + rocprofiler_queue_id_t queue = {.handle = 0}; }; struct memory_information { - uint64_t alloc_size = {0}; - rocprofiler_address_t address = {.handle = 0}; - bool is_alloc_op = {false}; + uint64_t alloc_size = {0}; + rocprofiler_address_t address = {.handle = 0}; + rocprofiler_queue_id_t queue = {.handle = 0}; + bool is_alloc_op = {false}; }; struct agent_and_size @@ -708,7 +711,8 @@ write_perfetto( std::numeric_limits::max(), std::numeric_limits::min()}; auto address_to_agent_and_size = std::unordered_map{}; - auto free_mem_info = std::vector{}; + auto queue_to_agent_and_size = std::unordered_map{}; + auto free_mem_info = std::vector{}; // Load memory allocation endpoints for(auto ditr : memory_allocation_gen) @@ -719,24 +723,47 @@ write_perfetto( { LOG_IF(FATAL, itr.agent_name.empty()) << "Missing agent id for memory allocation trace"; - mem_alloc_endpoints[itr.agent_abs_index].emplace( - itr.start, - memory_information{ - itr.size, rocprofiler_address_t{.handle = itr.address}, true}); - mem_alloc_endpoints[itr.agent_abs_index].emplace( - itr.end, - memory_information{ - itr.size, rocprofiler_address_t{.handle = itr.address}, true}); - address_to_agent_and_size.emplace( - rocprofiler_address_t{.handle = itr.address}, - agent_and_size{itr.agent_abs_index, itr.size}); + + if(itr.level == "REAL") + { + mem_alloc_endpoints[itr.agent_abs_index].emplace( + itr.start, + memory_information{itr.size, + rocprofiler_address_t{.handle = itr.address}, + rocprofiler_queue_id_t{.handle = itr.queue_id}, + true}); + mem_alloc_endpoints[itr.agent_abs_index].emplace( + itr.end, + memory_information{itr.size, + rocprofiler_address_t{.handle = itr.address}, + rocprofiler_queue_id_t{.handle = itr.queue_id}, + true}); + + address_to_agent_and_size.emplace( + rocprofiler_address_t{.handle = itr.address}, + agent_and_size{itr.agent_abs_index, itr.size}); + } + // Scratch memory operations operations are indexed by queue id as agent + // id is not available + else if(itr.level == "SCRATCH") + { + queue_to_agent_and_size.emplace( + rocprofiler_queue_id_t{.handle = itr.queue_id}, + agent_and_size{itr.agent_abs_index, itr.size}); + } } else if(itr.type == "FREE") { // Store free memory operations in seperate vector to pair with agent // and allocation size in following loop - free_mem_info.push_back(free_memory_information{ - itr.start, itr.end, rocprofiler_address_t{.handle = itr.address}}); + if(itr.level == "REAL") + { + free_mem_info.push_back(free_memory_information{ + itr.start, + itr.end, + rocprofiler_address_t{.handle = itr.address}, + rocprofiler_queue_id_t{.handle = itr.queue_id}}); + } } else { @@ -765,9 +792,9 @@ write_perfetto( } auto [agent_abs_index, size] = address_to_agent_and_size[itr.address]; mem_alloc_endpoints[agent_abs_index].emplace( - itr.start_timestamp, memory_information{size, itr.address, false}); + itr.start_timestamp, memory_information{size, itr.address, itr.queue, false}); mem_alloc_endpoints[agent_abs_index].emplace( - itr.end_timestamp, memory_information{size, itr.address, false}); + itr.end_timestamp, memory_information{size, itr.address, itr.queue, false}); } // Create running sum of allocated memory for(auto& [_, endpoint_map] : mem_alloc_endpoints) @@ -817,10 +844,10 @@ write_perfetto( { mem_alloc_endpoints[abs_index].emplace( mem_alloc_extremes.first - extremes_endpoint_buffer, - memory_information{0, {0}, false}); + memory_information{0, {0}, {0}, false}); mem_alloc_endpoints[abs_index].emplace( mem_alloc_extremes.second + extremes_endpoint_buffer, - memory_information{0, {0}, false}); + memory_information{0, {0}, {0}, false}); auto _track_name = std::stringstream{}; @@ -853,9 +880,110 @@ write_perfetto( mem_alloc_tracks.at(alloc_itr.first), itr.first, itr.second.alloc_size / bytes_multiplier); + 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)) + { + auto agent_abs_index = itr.agent_abs_index; + if(itr.operation == "FREE") + { + auto [agent_index, size] = + queue_to_agent_and_size[rocprofiler_queue_id_t{.handle = itr.queue_id}]; + agent_abs_index = agent_index; + } + + // Track start and end timestamps for this scratch memory record + scratch_mem_endpoints[agent_abs_index].emplace(itr.start, 0); + scratch_mem_endpoints[agent_abs_index].emplace(itr.end, 0); + + // Update overall time range + scratch_mem_extremes = + std::make_pair(std::min(scratch_mem_extremes.first, itr.start), + std::max(scratch_mem_extremes.second, itr.end)); + } + + // Load values at each endpoint + for(auto ditr : scratch_memory_gen) + for(auto itr : scratch_memory_gen.get(ditr)) + { + auto agent_abs_index = itr.agent_abs_index; + if(itr.operation == "FREE") + { + auto [agent_index, size] = + queue_to_agent_and_size[rocprofiler_queue_id_t{.handle = itr.queue_id}]; + agent_abs_index = agent_index; + } + // For each timestamp in the range of this record + auto begin = scratch_mem_endpoints.at(agent_abs_index).lower_bound(itr.start); + auto end = scratch_mem_endpoints.at(agent_abs_index).upper_bound(itr.end); + + for(auto mitr = begin; mitr != end; ++mitr) + { + // Add scratch memory size to the counter value at this timestamp + if(itr.operation == "ALLOC") + mitr->second = itr.size; + else if(itr.operation == "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& [abs_index, ts_map] : scratch_mem_endpoints) + { + // Add buffer timestamps for better visualization + if(!ts_map.empty()) + { + scratch_mem_endpoints[abs_index].emplace( + scratch_mem_extremes.first - extremes_endpoint_buffer, 0); + scratch_mem_endpoints[abs_index].emplace( + scratch_mem_extremes.second + extremes_endpoint_buffer, 0); + + auto _track_name = std::stringstream{}; + const auto _agent = agent_data.at(abs_index).first; + auto agent_index_info = agent_data.at(abs_index).second; + + _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(abs_index, + ::perfetto::CounterTrack{_name.c_str(), this_pid_track} + .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(); + } } } - tracing_session->FlushBlocking(); } // Create counter tracks per agent diff --git a/source/lib/python/rocpd/source/perfetto.hpp b/source/lib/python/rocpd/source/perfetto.hpp index 601a83edd9..79394239b1 100644 --- a/source/lib/python/rocpd/source/perfetto.hpp +++ b/source/lib/python/rocpd/source/perfetto.hpp @@ -63,6 +63,7 @@ write_perfetto( const tool::generator& sample_gen, const tool::generator& kernel_dispatch_gen, const tool::generator& memory_copy_gen, + const tool::generator& scratch_memory_gen, const tool::generator& memory_allocation_gen, const tool::generator& counter_collection_gen); } // namespace output diff --git a/tests/pytest-packages/pytest_utils/perfetto_reader.py b/tests/pytest-packages/pytest_utils/perfetto_reader.py index 22b7ac6611..fcf556c0cf 100644 --- a/tests/pytest-packages/pytest_utils/perfetto_reader.py +++ b/tests/pytest-packages/pytest_utils/perfetto_reader.py @@ -288,7 +288,11 @@ def extract_tp_data(self, **kwargs): counter_track.id as slice_id, counter.track_id, counter_track.name as track_name, - 'counter_collection' as category, + CASE + WHEN counter_track.name LIKE '%SCRATCH MEMORY%' THEN 'scratch_memory' + WHEN counter_track.name LIKE '%ALLOCATE BYTES%' THEN 'memory_allocation' + ELSE 'counter_collection' + END as category, 0 as depth, 0 as stack_id, 0 as parent_stack_id, @@ -297,7 +301,9 @@ def extract_tp_data(self, **kwargs): counter_track.name as name FROM counter_track JOIN counter ON counter.track_id = counter_track.id - WHERE counter_track.name LIKE 'AGENT%' + WHERE (counter_track.name LIKE 'AGENT%' + OR counter_track.name LIKE '%SCRATCH MEMORY%' + OR counter_track.name LIKE '%ALLOCATE BYTES%') AND counter.value > 0 GROUP BY counter.track_id""" ) @@ -328,7 +334,7 @@ def extract_tp_data(self, **kwargs): "tp_index": counter_df["tp_index"], "slice_id": counter_df["slice_id"], "track_id": counter_df["track_id"], - "category": "counter_collection", + "category": counter_df["category"], "depth": 0, "stack_id": 0, "parent_stack_id": 0, diff --git a/tests/rocprofv3/CMakeLists.txt b/tests/rocprofv3/CMakeLists.txt index e1699d90b6..d20acba025 100644 --- a/tests/rocprofv3/CMakeLists.txt +++ b/tests/rocprofv3/CMakeLists.txt @@ -46,3 +46,4 @@ add_subdirectory(minimum-bytes) add_subdirectory(conversion-script) add_subdirectory(python-bindings) add_subdirectory(rocpd) +add_subdirectory(rocpd-scratch) diff --git a/tests/rocprofv3/rocpd-scratch/CMakeLists.txt b/tests/rocprofv3/rocpd-scratch/CMakeLists.txt new file mode 100644 index 0000000000..ceda3f9d7c --- /dev/null +++ b/tests/rocprofv3/rocpd-scratch/CMakeLists.txt @@ -0,0 +1,108 @@ +# +# rocprofv3 rocpd tests +# +cmake_minimum_required(VERSION 3.21.0 FATAL_ERROR) + +project( + rocprofiler-sdk-tests-rocprofv3-rocpd-scratch + LANGUAGES CXX + VERSION 0.0.0) + +find_package(rocprofiler-sdk REQUIRED) + +set(rocprofv3-rocpd-scratch-env + "${ROCPROFILER_MEMCHECK_PRELOAD_ENV}" + "PYTHONPATH=${rocprofiler-sdk_LIB_DIR}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages" + "OMPI_ALLOW_RUN_AS_ROOT=1" + "OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1") + +rocprofiler_configure_pytest_files(CONFIG pytest.ini COPY conftest.py validate.py) + +find_package(Python3 REQUIRED) + +######################################################################################### +# +# generate rocpd database and json for comparison +# +######################################################################################### + +add_test( + NAME rocprofv3-test-rocpd-scratch-execute + COMMAND + $ --scratch-memory-trace -d + ${CMAKE_CURRENT_BINARY_DIR}/rocpd-input-data -o out --output-format rocpd json + --runtime-trace --kernel-rename --output-config -- $) + +set_tests_properties( + rocprofv3-test-rocpd-scratch-execute + PROPERTIES TIMEOUT + 120 + LABELS + "integration-tests;rocpd" + ENVIRONMENT + "${rocprofv3-rocpd-scratch-env}" + FAIL_REGULAR_EXPRESSION + "${ROCPROFILER_DEFAULT_FAIL_REGEX}" + FIXTURES_SETUP + rocprofv3-test-rocpd-scratch) + +######################################################################################### +# +# perfetto generate +# +######################################################################################### + +add_test( + NAME rocprofv3-test-rocpd-scratch-perfetto-generation + COMMAND + ${Python3_EXECUTABLE} -m rocpd convert -f pftrace --kernel-rename -d + ${CMAKE_CURRENT_BINARY_DIR}/rocpd-output-data -i + ${CMAKE_CURRENT_BINARY_DIR}/rocpd-input-data/out_results.db) + +set_tests_properties( + rocprofv3-test-rocpd-scratch-perfetto-generation + PROPERTIES TIMEOUT + 120 + LABELS + "integration-tests;rocpd" + ENVIRONMENT + "${rocprofv3-rocpd-scratch-env}" + DEPENDS + "rocprofv3-test-rocpd-scratch-execute" + FAIL_REGULAR_EXPRESSION + "${ROCPROFILER_DEFAULT_FAIL_REGEX}" + FIXTURES_SETUP + rocprofv3-test-rocpd-scratch-generation + FIXTURES_REQUIRED + rocprofv3-test-rocpd-scratch) + +######################################################################################### +# +# Validation +# +######################################################################################### + +set(VALIDATION_DEPENDS + rocprofv3-test-rocpd-scratch-perfetto-generation) + +add_test( + NAME rocprofv3-test-rocpd-scratch-validation + COMMAND + ${Python3_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/validate.py --json-input + ${CMAKE_CURRENT_BINARY_DIR}/rocpd-input-data/out_results.json --pftrace-input + ${CMAKE_CURRENT_BINARY_DIR}/rocpd-output-data/out_results.pftrace) + +set_tests_properties( + rocprofv3-test-rocpd-scratch-validation + PROPERTIES TIMEOUT + 120 + LABELS + "integration-tests;rocpd" + ENVIRONMENT + "${rocprofv3-rocpd-scratch-env}" + DEPENDS + rocprofv3-test-rocpd-scratch-perfetto-generation + FAIL_REGULAR_EXPRESSION + "${ROCPROFILER_DEFAULT_FAIL_REGEX}" + FIXTURES_REQUIRED + rocprofv3-test-rocpd-scratch-generation) diff --git a/tests/rocprofv3/rocpd-scratch/conftest.py b/tests/rocprofv3/rocpd-scratch/conftest.py new file mode 100644 index 0000000000..9f12f32602 --- /dev/null +++ b/tests/rocprofv3/rocpd-scratch/conftest.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2024-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 pandas as pd +import pytest +import json +import os +import io + +from rocprofiler_sdk.pytest_utils.dotdict import dotdict +from rocprofiler_sdk.pytest_utils import collapse_dict_list +from rocprofiler_sdk.pytest_utils.perfetto_reader import PerfettoReader +from rocprofiler_sdk.pytest_utils.otf2_reader import OTF2Reader + + +def pytest_addoption(parser): + parser.addoption( + "--json-input", + action="store", + help="Path to JSON file.", + ) + parser.addoption( + "--pftrace-input", + action="store", + help="Path to Perfetto trace file.", + ) + + pd.set_option("display.width", 2000) + # increase debug display of pandas dataframes + for itr in ["rows", "columns", "colwidth"]: + pd.set_option(f"display.max_{itr}", None) + + +@pytest.fixture +def json_data(request): + filename = request.config.getoption("--json-input") + with open(filename, "r") as inp: + return dotdict(collapse_dict_list(json.load(inp))) + + +@pytest.fixture +def pftrace_data(request): + filename = request.config.getoption("--pftrace-input") + return PerfettoReader(filename).read()[0] \ No newline at end of file diff --git a/tests/rocprofv3/rocpd-scratch/pytest.ini b/tests/rocprofv3/rocpd-scratch/pytest.ini new file mode 100644 index 0000000000..5e1e1c14a0 --- /dev/null +++ b/tests/rocprofv3/rocpd-scratch/pytest.ini @@ -0,0 +1,5 @@ + +[pytest] +addopts = --durations=20 -rA -s -vv +testpaths = validate.py +pythonpath = @ROCPROFILER_SDK_TESTS_BINARY_DIR@/pytest-packages diff --git a/tests/rocprofv3/rocpd-scratch/validate.py b/tests/rocprofv3/rocpd-scratch/validate.py new file mode 100644 index 0000000000..ae67312348 --- /dev/null +++ b/tests/rocprofv3/rocpd-scratch/validate.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2024-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 sys +import pytest + + +def test_perfetto_data(pftrace_data, json_data): + import rocprofiler_sdk.tests.rocprofv3 as rocprofv3 + + rocprofv3.test_perfetto_data( + pftrace_data, + json_data, + ("scratch_memory",), + ) + +if __name__ == "__main__": + exit_code = pytest.main(["-x", __file__] + sys.argv[1:]) + sys.exit(exit_code)