diff --git a/dace/codegen/instrumentation/data/data_dump.py b/dace/codegen/instrumentation/data/data_dump.py index 5646213827..5f9f78e740 100644 --- a/dace/codegen/instrumentation/data/data_dump.py +++ b/dace/codegen/instrumentation/data/data_dump.py @@ -98,6 +98,11 @@ def __init__(self): self.uses_gpu = False self.framecode: 'DaCeCodeGenerator' = None + def writes_to_report(self) -> bool: + # Data instrumentation dumps arrays to a separate directory and never touches + # __state->report. + return False + def on_sdfg_begin(self, sdfg: SDFG, local_stream: CodeIOStream, global_stream: CodeIOStream, codegen: 'DaCeCodeGenerator'): # Initialize serializer versioning object @@ -202,6 +207,11 @@ def __init__(self): self.uses_gpu = False self.framecode: 'DaCeCodeGenerator' = None + def writes_to_report(self) -> bool: + # Data instrumentation dumps arrays to a separate directory and never touches + # __state->report. + return False + def _generate_report_setter(self, sdfg: SDFG) -> str: return f''' DACE_EXPORTED void __dace_set_instrumented_data_report({cpp.mangle_dace_state_struct_name(sdfg)} *__state, const char *dirpath) {{ diff --git a/dace/codegen/instrumentation/gpu_events.py b/dace/codegen/instrumentation/gpu_events.py index f2eb82ede6..ea690d5285 100644 --- a/dace/codegen/instrumentation/gpu_events.py +++ b/dace/codegen/instrumentation/gpu_events.py @@ -17,6 +17,9 @@ def __init__(self): self.backend = common.get_gpu_backend() super().__init__() + def writes_to_report(self) -> bool: + return True + def on_sdfg_begin(self, sdfg: SDFG, local_stream: CodeIOStream, global_stream: CodeIOStream, codegen) -> None: if self.backend == 'cuda': header_name = 'cuda_runtime.h' diff --git a/dace/codegen/instrumentation/gpu_tx_markers.py b/dace/codegen/instrumentation/gpu_tx_markers.py index a3b87d4ce8..e9454cfa95 100644 --- a/dace/codegen/instrumentation/gpu_tx_markers.py +++ b/dace/codegen/instrumentation/gpu_tx_markers.py @@ -34,6 +34,9 @@ def __init__(self): self.include_generated = False super().__init__() + def writes_to_report(self) -> bool: + return False + def _print_include(self, sdfg: SDFG) -> None: """ Prints the include statement for the NVTX/rocTX library for a given SDFG. """ if self.include_generated: diff --git a/dace/codegen/instrumentation/likwid.py b/dace/codegen/instrumentation/likwid.py index 4c80ffb413..f06a00030f 100644 --- a/dace/codegen/instrumentation/likwid.py +++ b/dace/codegen/instrumentation/likwid.py @@ -122,6 +122,9 @@ def __init__(self): except KeyError: self._default_events = "CLOCK" + def writes_to_report(self) -> bool: + return True + def on_sdfg_begin(self, sdfg: SDFG, local_stream: CodeIOStream, global_stream: CodeIOStream, codegen) -> None: if sdfg.parent is not None: return @@ -371,6 +374,9 @@ def __init__(self): except KeyError: self._default_events = "FLOPS_SP" + def writes_to_report(self) -> bool: + return True + def on_sdfg_begin(self, sdfg: SDFG, local_stream: CodeIOStream, global_stream: CodeIOStream, codegen) -> None: if sdfg.parent is not None: return diff --git a/dace/codegen/instrumentation/papi.py b/dace/codegen/instrumentation/papi.py index 988517ad99..ec19399b82 100644 --- a/dace/codegen/instrumentation/papi.py +++ b/dace/codegen/instrumentation/papi.py @@ -90,6 +90,9 @@ def __init__(self): PAPIInstrumentation._counters = PAPIInstrumentation._counters or set( ast.literal_eval(Config.get('instrumentation', 'papi', 'default_counters'))) + def writes_to_report(self) -> bool: + return True + def get_unique_number(self): ret = self._unique_counter self._unique_counter += 1 diff --git a/dace/codegen/instrumentation/provider.py b/dace/codegen/instrumentation/provider.py index 4c14e7a98a..29f6b9e61a 100644 --- a/dace/codegen/instrumentation/provider.py +++ b/dace/codegen/instrumentation/provider.py @@ -32,6 +32,12 @@ class types, given the currently-registered extensions of this class. return result + def writes_to_report(self) -> bool: + """ Returns whether this provider writes to the runtime performance report + (``__state->report``). Every concrete provider must override this. + """ + raise NotImplementedError(f'{type(self).__name__} must implement writes_to_report()') + def _idstr(self, cfg: ControlFlowRegion, state: SDFGState, node: nodes.Node) -> str: """ Returns a unique identifier string from a node or state. """ result = str(cfg.cfg_id) diff --git a/dace/codegen/instrumentation/timer.py b/dace/codegen/instrumentation/timer.py index bf756b0937..2f90557d8d 100644 --- a/dace/codegen/instrumentation/timer.py +++ b/dace/codegen/instrumentation/timer.py @@ -10,6 +10,9 @@ class TimerProvider(InstrumentationProvider): """ Timing instrumentation that reports wall-clock time directly after timed execution is complete. """ + def writes_to_report(self) -> bool: + return True + def on_sdfg_begin(self, sdfg, local_stream, global_stream, codegen): global_stream.write('#include ') diff --git a/dace/codegen/targets/framecode.py b/dace/codegen/targets/framecode.py index 50bb6db8f1..2ceda84209 100644 --- a/dace/codegen/targets/framecode.py +++ b/dace/codegen/targets/framecode.py @@ -222,7 +222,9 @@ def generate_header(self, sdfg: SDFG, global_stream: CodeIOStream, callsite_stre self.statestruct.extend(env.state_fields) # Instrumentation preamble - if len(self._dispatcher.instrumentation) > 2: + # NOTE: Some instrumentation providers (e.g. GPU_TX_MARKERS) never write to + # __state->report, so skip the report machinery unless at least one active provider does. + if any(i is not None and i.writes_to_report() for i in self._dispatcher.instrumentation.values()): self.statestruct.append('dace::perf::Report report;') # Reset report if written every invocation if config.Config.get_bool('instrumentation', 'report_each_invocation'): @@ -252,7 +254,7 @@ def generate_footer(self, sdfg: SDFG, global_stream: CodeIOStream, callsite_stre # Instrumentation saving if (config.Config.get_bool('instrumentation', 'report_each_invocation') - and len(self._dispatcher.instrumentation) > 2): + and any(i is not None and i.writes_to_report() for i in self._dispatcher.instrumentation.values())): callsite_stream.write( '__state->report.save("%s", __HASH_%s);' % (pathlib.Path(sdfg.build_folder) / "perf", sdfg.name), sdfg) @@ -362,7 +364,7 @@ def generate_footer(self, sdfg: SDFG, global_stream: CodeIOStream, callsite_stre # Instrumentation saving if (not config.Config.get_bool('instrumentation', 'report_each_invocation') - and len(self._dispatcher.instrumentation) > 2): + and any(i is not None and i.writes_to_report() for i in self._dispatcher.instrumentation.values())): callsite_stream.write( '__state->report.save("%s", __HASH_%s);' % (pathlib.Path(sdfg.build_folder) / "perf", sdfg.name), sdfg) diff --git a/tests/codegen/data_instrumentation_test.py b/tests/codegen/data_instrumentation_test.py index 528f826166..445eb77f8a 100644 --- a/tests/codegen/data_instrumentation_test.py +++ b/tests/codegen/data_instrumentation_test.py @@ -93,6 +93,27 @@ def tester(A: dace.float64[20, 20]): assert all('DeviceSynchronize' in chunk for chunk in before_each_save), code +def test_dump_no_report(): + """Data instrumentation (Save/RestoreProvider) never writes to __state->report, so the + performance report struct and its save calls must not appear when it's the only + instrumentation in use. + """ + + @dace.program + def tester(A: dace.float64[20, 20]): + tmp = A + 1 + return tmp + 5 + + sdfg = tester.to_sdfg(simplify=True) + _instrument(sdfg, dace.DataInstrumentationType.Save) + + for each_invocation in (True, False): + with dace.config.set_temporary('instrumentation', 'report_each_invocation', value=each_invocation): + code = sdfg.generate_code()[0].clean_code + assert 'dace::perf::Report report;' not in code + assert '__state->report' not in code + + @pytest.mark.datainstrument def test_restore(): @@ -412,6 +433,7 @@ def dinstr(A: dace.float64[20]): test_symbol_dump_conditional() test_dump_gpu() test_dump_gpu_synchronizes() + test_dump_no_report() test_restore() test_symbol_restore() test_restore_gpu() diff --git a/tests/instrumentation_test.py b/tests/instrumentation_test.py index 120e025812..c287b99903 100644 --- a/tests/instrumentation_test.py +++ b/tests/instrumentation_test.py @@ -13,6 +13,9 @@ N = dace.symbol('N') +# State struct field that carries the instrumentation report +REPORT_DECL = 'dace::perf::Report report;' + @dace.program def slowmm(A: dace.float64[N, N], B: dace.float64[N, N], C: dace.float64[N, N]): @@ -77,6 +80,15 @@ def onetest(instrumentation: dace.InstrumentationType, size=128): range_pop = re.search(r'(nvtx|roctx)RangePop\b', code) assert range_pop is not None + # GPU_TX_MARKERS never writes to the report, so it must not be declared nor used when it is + # the only instrumentation in use. Both save sites are checked, as they are guarded by the + # `instrumentation.report_each_invocation` configuration entry. + for each_invocation in (True, False): + with dace.config.set_temporary('instrumentation', 'report_each_invocation', value=each_invocation): + code = sdfg.generate_code()[0].clean_code + assert REPORT_DECL not in code + assert re.search(r'__state->report\b', code) is None + def test_timer(): onetest(dace.InstrumentationType.Timer) @@ -98,9 +110,35 @@ def test_gpu_tx_markers(): onetest(dace.InstrumentationType.GPU_TX_MARKERS) +@pytest.mark.gpu +def test_gpu_tx_markers_with_timer(): + """ The report is still needed when another instrumentation type is used next to GPU_TX_MARKERS. """ + sdfg: dace.SDFG = slowmm.to_sdfg() + sdfg.name = 'instrumentation_test_GPU_TX_MARKERS_with_timer' + sdfg.simplify() + + # Mark the map with GPU_TX_MARKERS and the state containing it with a timer + sdfg.instrument = dace.InstrumentationType.GPU_TX_MARKERS + for node, state in sdfg.all_nodes_recursive(): + if isinstance(node, nodes.MapEntry) and node.map.label == 'mult': + node.map.instrument = dace.InstrumentationType.GPU_TX_MARKERS + state.instrument = dace.InstrumentationType.Timer + + sdfg.apply_transformations(GPUTransformSDFG) + + # Both providers are in use, so the ranges are emitted and the report is kept at either save site + for each_invocation in (True, False): + with dace.config.set_temporary('instrumentation', 'report_each_invocation', value=each_invocation): + code = sdfg.generate_code()[0].clean_code + assert re.search(r'(nvtx|roctx)RangePush\(', code) is not None + assert REPORT_DECL in code + assert re.search(r'__state->report\b', code) is not None + + if __name__ == '__main__': test_timer() test_papi() if len(sys.argv) > 1 and sys.argv[1] == 'gpu': test_gpu_events() test_gpu_tx_markers() + test_gpu_tx_markers_with_timer()