From 2328dcfabd9229d6a53334288d2a09f9d9ccd055 Mon Sep 17 00:00:00 2001 From: Eddie Liao Date: Wed, 10 Jun 2026 21:35:51 +0000 Subject: [PATCH 1/7] Add prototype using std::uncaught_exceptions() --- src/program.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/program.cpp b/src/program.cpp index 4e1caa81fa1..d32713b1933 100644 --- a/src/program.cpp +++ b/src/program.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -484,6 +485,34 @@ static bool is_compatible_shape(const shape& actual, const shape& expected) } #endif +namespace { +// Logs the debug symbols of the instruction currently being evaluated, but only if the stack is +// being unwound by an exception thrown from that evaluation. This lets a runtime failure be traced +// back to its originating ONNX node (debug symbols are the parsed node names) without catching or +// modifying the exception, so its original throw location and propagation are left untouched for +// debuggers and the driver. +struct log_debug_symbols_on_throw +{ + instruction_ref ins; + int uncaught = std::uncaught_exceptions(); + + ~log_debug_symbols_on_throw() + { + if(std::uncaught_exceptions() <= uncaught or ins->get_debug_symbols().empty()) + return; + try + { + log::error() << "Exception thrown while evaluating instruction '" << ins->name() + << "' with debug symbols: " + << join_strings(ins->get_debug_symbols(), ", "); + } + catch(...) // a destructor must not throw while the stack is unwinding + { + } + } +}; +} // namespace + template static std::vector generic_eval(const module* mod, std::vector& ctx, @@ -500,6 +529,8 @@ static std::vector generic_eval(const module* mod, #ifndef NDEBUG results.emplace(ins, argument{}); #endif + // Report the failing instruction's debug symbols if its evaluation throws. + log_debug_symbols_on_throw symbol_log_guard{ins}; const auto& name = ins->name(); if(name == "@literal") { From 98614b9cdf76972daa19a499d199bf6ee1442f1d Mon Sep 17 00:00:00 2001 From: Eddie Liao Date: Tue, 16 Jun 2026 23:06:11 +0000 Subject: [PATCH 2/7] Use scope guard instead of try/catch --- src/include/migraphx/instruction.hpp | 4 ++ src/include/migraphx/scope_guard.hpp | 64 +++++++++++++++++++++++++++ src/instruction.cpp | 24 +++++++++- src/module.cpp | 8 +++- src/onnx/onnx_parser.cpp | 18 +++++++- src/program.cpp | 66 +++++++--------------------- 6 files changed, 130 insertions(+), 54 deletions(-) create mode 100644 src/include/migraphx/scope_guard.hpp diff --git a/src/include/migraphx/instruction.hpp b/src/include/migraphx/instruction.hpp index c78f14e842e..347d5e09bbc 100644 --- a/src/include/migraphx/instruction.hpp +++ b/src/include/migraphx/instruction.hpp @@ -214,6 +214,10 @@ struct MIGRAPHX_EXPORT instruction std::size_t target_id = 0; }; +/// Logs the instruction's debug symbols (if any) to help trace a failure back to its source. +/// Intended to be invoked from a scope-fail guard during stack unwinding; never throws. +MIGRAPHX_EXPORT void log_debug_symbols_on_exception(const instruction& ins) noexcept; + } // namespace MIGRAPHX_INLINE_NS } // namespace migraphx diff --git a/src/include/migraphx/scope_guard.hpp b/src/include/migraphx/scope_guard.hpp new file mode 100644 index 00000000000..a1c290571a4 --- /dev/null +++ b/src/include/migraphx/scope_guard.hpp @@ -0,0 +1,64 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015-2026 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. + */ +#ifndef MIGRAPHX_GUARD_MIGRAPHX_SCOPE_GUARD_HPP +#define MIGRAPHX_GUARD_MIGRAPHX_SCOPE_GUARD_HPP + +#include +#include +#include +#include + +namespace migraphx { +inline namespace MIGRAPHX_INLINE_NS { + +// Runs the action only if the scope is exited via an exception. +template +struct scope_fail_guard +{ + static_assert(std::is_nothrow_invocable_v, "scope_fail action must be noexcept"); + + F action; + int uncaught = std::uncaught_exceptions(); + + explicit scope_fail_guard(F f) : action(std::move(f)) {} + + scope_fail_guard(const scope_fail_guard&) = delete; + + ~scope_fail_guard() + { + if(std::uncaught_exceptions() > uncaught) + action(); + } +}; + +template +scope_fail_guard on_scope_fail(F f) +{ + return scope_fail_guard{std::move(f)}; +} + +} // namespace MIGRAPHX_INLINE_NS +} // namespace migraphx + +#endif // MIGRAPHX_GUARD_MIGRAPHX_SCOPE_GUARD_HPP diff --git a/src/instruction.cpp b/src/instruction.cpp index 161072ef944..26d37b75ba1 100644 --- a/src/instruction.cpp +++ b/src/instruction.cpp @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include #include #include @@ -99,6 +101,7 @@ void instruction::replace(const shape& r) instruction_ref ins = q.top(); q.pop(); assert(ins->name() == "@return" or ins->name().front() != '@'); + auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*ins); }); shape new_r = compute_shape(ins->op, ins->arguments, ins->module_args); if(new_r != ins->result) { @@ -116,7 +119,11 @@ void instruction::replace(operation o) recompute_shape(); } -void instruction::recompute_shape() { replace(compute_shape(op, arguments, module_args)); } +void instruction::recompute_shape() +{ + auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*this); }); + replace(compute_shape(op, arguments, module_args)); +} void instruction::clear_arguments() { @@ -632,6 +639,21 @@ std::vector get_added_instructions(const std::vector::iterator& ins) noexcept { return iterator_address(ins); diff --git a/src/module.cpp b/src/module.cpp index 58fdbc44c96..3b7717a2eee 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -487,6 +488,7 @@ instruction_ref module::replace_instruction(instruction_ref ins, impl->changed.notify(); assert(has_instruction(ins)); assert(not starts_with(op.name(), "@")); + auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*ins); }); auto out_shape = compute_shape(op, args, module_args); std::vector prev_args; if(has_debug_symbols()) @@ -572,6 +574,8 @@ module::batch_replace_instruction(const std::vector& re { prev_args = replacer.ins->inputs(); } + auto guard = + on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*replacer.ins); }); auto out_shape = compute_shape(replacer.op, replacer.args, replacer.module_args); instruction::replace( replacer.ins, replacer.op, out_shape, replacer.args, replacer.module_args); @@ -844,7 +848,8 @@ instruction_ref module::replace_return(std::vector args) return this->add_return(args); } - shape r = compute_shape(last->get_operator(), args); + auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*last); }); + shape r = compute_shape(last->get_operator(), args); instruction::replace(last, last->get_operator(), r, std::move(args)); assert(last->valid(begin())); @@ -1039,6 +1044,7 @@ std::vector module::compute_shapes(const std::vector& inputs, [&](auto in) { return ins_shapes.at(in); }); if(ins->name() == "@return") return input_shapes; + auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*ins); }); ins_shapes[ins] = ins->get_operator().compute_shape(input_shapes, ins->module_inputs()); } } diff --git a/src/onnx/onnx_parser.cpp b/src/onnx/onnx_parser.cpp index bf07881f571..158c1873d1e 100644 --- a/src/onnx/onnx_parser.cpp +++ b/src/onnx/onnx_parser.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -621,6 +622,21 @@ onnx_parser::parse_graph(module* mod, const onnx::GraphProto& graph, bool inlini std::vector result; std::size_t output_num = node.output().size(); std::string node_name = node.op_type() + "_" + std::to_string(mod->size()); + std::string debug_symbol; + if(this->use_debug_symbols) + debug_symbol = node.name().empty() ? "migx_uid:" + node_name : node.name(); + auto guard = on_scope_fail([&]() noexcept { + if(debug_symbol.empty()) + return; + try + { + log::debug() << "Exception thrown while parsing node '" << node.op_type() + << "' with debug symbols: " << debug_symbol; + } + catch(...) // logging must not replace the original exception + { + } + }); if(ops.count(node.op_type()) == 0) { if(skip_unknown_operators) @@ -646,8 +662,6 @@ onnx_parser::parse_graph(module* mod, const onnx::GraphProto& graph, bool inlini } if(this->use_debug_symbols) { - std::string debug_symbol = - node.name().empty() ? std::string("migx_uid:") + node_name : node.name(); for(auto ins : added_instructions) { mod->add_debug_symbols(ins, {debug_symbol}); diff --git a/src/program.cpp b/src/program.cpp index d32713b1933..6a7d1a30031 100644 --- a/src/program.cpp +++ b/src/program.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -49,7 +50,6 @@ #include #include #include -#include #include #include #include @@ -485,34 +485,6 @@ static bool is_compatible_shape(const shape& actual, const shape& expected) } #endif -namespace { -// Logs the debug symbols of the instruction currently being evaluated, but only if the stack is -// being unwound by an exception thrown from that evaluation. This lets a runtime failure be traced -// back to its originating ONNX node (debug symbols are the parsed node names) without catching or -// modifying the exception, so its original throw location and propagation are left untouched for -// debuggers and the driver. -struct log_debug_symbols_on_throw -{ - instruction_ref ins; - int uncaught = std::uncaught_exceptions(); - - ~log_debug_symbols_on_throw() - { - if(std::uncaught_exceptions() <= uncaught or ins->get_debug_symbols().empty()) - return; - try - { - log::error() << "Exception thrown while evaluating instruction '" << ins->name() - << "' with debug symbols: " - << join_strings(ins->get_debug_symbols(), ", "); - } - catch(...) // a destructor must not throw while the stack is unwinding - { - } - } -}; -} // namespace - template static std::vector generic_eval(const module* mod, std::vector& ctx, @@ -529,9 +501,21 @@ static std::vector generic_eval(const module* mod, #ifndef NDEBUG results.emplace(ins, argument{}); #endif - // Report the failing instruction's debug symbols if its evaluation throws. - log_debug_symbols_on_throw symbol_log_guard{ins}; const auto& name = ins->name(); + if(name == "@return") + { + std::vector prog_outputs; + std::transform(ins->inputs().begin(), + ins->inputs().end(), + std::back_inserter(prog_outputs), + [&](instruction_ref i) { + assert(results.find(i) != results.end()); + return results[i]; + }); + + return prog_outputs; + } + auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*ins); }); if(name == "@literal") { results.insert_or_assign(ins, @@ -556,29 +540,11 @@ static std::vector generic_eval(const module* mod, return param; })); } - else if(name == "@outline") - { - results.insert_or_assign( - ins, trace(ins, [&] { return argument{ins->get_shape(), nullptr}; })); - } - else if(name == "@comment") + else if(name == "@outline" or name == "@comment") { results.insert_or_assign( ins, trace(ins, [&] { return argument{ins->get_shape(), nullptr}; })); } - else if(name == "@return") - { - std::vector prog_outputs; - std::transform(ins->inputs().begin(), - ins->inputs().end(), - std::back_inserter(prog_outputs), - [&](instruction_ref i) { - assert(results.find(i) != results.end()); - return results[i]; - }); - - return prog_outputs; - } else { values.resize(ins->inputs().size()); From 957935a9753398014cf9c47bd6f9d12b33d10901 Mon Sep 17 00:00:00 2001 From: Eddie Liao Date: Tue, 16 Jun 2026 23:16:40 +0000 Subject: [PATCH 3/7] Revert unintended changes --- src/program.cpp | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/program.cpp b/src/program.cpp index 6a7d1a30031..c77d67fc9bc 100644 --- a/src/program.cpp +++ b/src/program.cpp @@ -502,19 +502,6 @@ static std::vector generic_eval(const module* mod, results.emplace(ins, argument{}); #endif const auto& name = ins->name(); - if(name == "@return") - { - std::vector prog_outputs; - std::transform(ins->inputs().begin(), - ins->inputs().end(), - std::back_inserter(prog_outputs), - [&](instruction_ref i) { - assert(results.find(i) != results.end()); - return results[i]; - }); - - return prog_outputs; - } auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*ins); }); if(name == "@literal") { @@ -540,11 +527,29 @@ static std::vector generic_eval(const module* mod, return param; })); } - else if(name == "@outline" or name == "@comment") + else if(name == "@outline") + { + results.insert_or_assign( + ins, trace(ins, [&] { return argument{ins->get_shape(), nullptr}; })); + } + else if(name == "@comment") { results.insert_or_assign( ins, trace(ins, [&] { return argument{ins->get_shape(), nullptr}; })); } + else if(name == "@return") + { + std::vector prog_outputs; + std::transform(ins->inputs().begin(), + ins->inputs().end(), + std::back_inserter(prog_outputs), + [&](instruction_ref i) { + assert(results.find(i) != results.end()); + return results[i]; + }); + + return prog_outputs; + } else { values.resize(ins->inputs().size()); From 7df1ea4087e1f4f0fa7d9a58efb80a09f097bc8b Mon Sep 17 00:00:00 2001 From: Eddie Liao <54926923+eddieliao@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:23:47 -0700 Subject: [PATCH 4/7] Formatting Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/program.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/program.cpp b/src/program.cpp index c77d67fc9bc..c4753f03240 100644 --- a/src/program.cpp +++ b/src/program.cpp @@ -502,7 +502,7 @@ static std::vector generic_eval(const module* mod, results.emplace(ins, argument{}); #endif const auto& name = ins->name(); - auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*ins); }); + auto guard = on_scope_fail([&]() noexcept { log_debug_symbols_on_exception(*ins); }); if(name == "@literal") { results.insert_or_assign(ins, From c1222e29369a512e26eef19e2abcdd0c15e970e6 Mon Sep 17 00:00:00 2001 From: Eddie Liao Date: Tue, 16 Jun 2026 23:35:50 +0000 Subject: [PATCH 5/7] Address copilot comments --- src/include/migraphx/scope_guard.hpp | 2 +- src/instruction.cpp | 6 +++--- src/onnx/onnx_parser.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/include/migraphx/scope_guard.hpp b/src/include/migraphx/scope_guard.hpp index a1c290571a4..8397043e78c 100644 --- a/src/include/migraphx/scope_guard.hpp +++ b/src/include/migraphx/scope_guard.hpp @@ -36,7 +36,7 @@ inline namespace MIGRAPHX_INLINE_NS { template struct scope_fail_guard { - static_assert(std::is_nothrow_invocable_v, "scope_fail action must be noexcept"); + static_assert(std::is_nothrow_invocable{}, "scope_fail action must be noexcept"); F action; int uncaught = std::uncaught_exceptions(); diff --git a/src/instruction.cpp b/src/instruction.cpp index 26d37b75ba1..166300ce86b 100644 --- a/src/instruction.cpp +++ b/src/instruction.cpp @@ -641,11 +641,11 @@ std::vector get_added_instructions(const std::vectoruse_debug_symbols) debug_symbol = node.name().empty() ? "migx_uid:" + node_name : node.name(); auto guard = on_scope_fail([&]() noexcept { - if(debug_symbol.empty()) - return; try { + if(debug_symbol.empty()) + return; log::debug() << "Exception thrown while parsing node '" << node.op_type() << "' with debug symbols: " << debug_symbol; } From 37e8a9463064eac045bb7cb9fccebd96cb1f231c Mon Sep 17 00:00:00 2001 From: Eddie Liao Date: Tue, 16 Jun 2026 23:38:51 +0000 Subject: [PATCH 6/7] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47fec6d3b38..51f6b108485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Full documentation for MIGraphX is available at * Added driver warnings when inputs dimensions and/or values are not set (#4850). * Added documentation for using debug symbols (#4945). * Added `--log-stdout` flag to migraphx-driver to log to stdout instead of stderr (#4959). +* Added logging of debug symbols on exception thrown (#4978). ### Changed From 647468bb3b73a3743fad1564841e7a854659bced Mon Sep 17 00:00:00 2001 From: Eddie Liao Date: Wed, 17 Jun 2026 02:32:32 +0000 Subject: [PATCH 7/7] Fix cppcheck and tidy --- src/instruction.cpp | 1 + src/onnx/onnx_parser.cpp | 47 +++++++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/instruction.cpp b/src/instruction.cpp index 166300ce86b..94028b7ee69 100644 --- a/src/instruction.cpp +++ b/src/instruction.cpp @@ -649,6 +649,7 @@ void log_debug_symbols_on_exception(const instruction& ins) noexcept log::debug() << "Exception thrown for instruction '" << ins.name() << "' with debug symbols: " << join_strings(symbols, ", "); } + // cppcheck-suppress migraphx-EmptyCatchStatement catch(...) // logging must not replace the original exception { } diff --git a/src/onnx/onnx_parser.cpp b/src/onnx/onnx_parser.cpp index d9866fc366d..5e4232e9137 100644 --- a/src/onnx/onnx_parser.cpp +++ b/src/onnx/onnx_parser.cpp @@ -569,6 +569,31 @@ static void set_return_ins_debug_symbols(module* mod, mod->add_debug_symbols(ret_ins, output_symbols); } +static std::string make_node_debug_symbol(bool use_debug_symbols, + const onnx::NodeProto& node, + const std::string& node_name) +{ + if(not use_debug_symbols) + return {}; + return node.name().empty() ? "migx_uid:" + node_name : node.name(); +} + +static void log_node_parse_exception(const onnx::NodeProto& node, + const std::string& debug_symbol) noexcept +{ + try + { + if(debug_symbol.empty()) + return; + log::debug() << "Exception thrown while parsing node '" << node.op_type() + << "' with debug symbols: " << debug_symbol; + } + // cppcheck-suppress migraphx-EmptyCatchStatement + catch(...) // logging must not replace the original exception + { + } +} + std::vector onnx_parser::parse_graph(module* mod, const onnx::GraphProto& graph, bool inlining) { @@ -620,23 +645,11 @@ onnx_parser::parse_graph(module* mod, const onnx::GraphProto& graph, bool inlini } std::vector result; - std::size_t output_num = node.output().size(); - std::string node_name = node.op_type() + "_" + std::to_string(mod->size()); - std::string debug_symbol; - if(this->use_debug_symbols) - debug_symbol = node.name().empty() ? "migx_uid:" + node_name : node.name(); - auto guard = on_scope_fail([&]() noexcept { - try - { - if(debug_symbol.empty()) - return; - log::debug() << "Exception thrown while parsing node '" << node.op_type() - << "' with debug symbols: " << debug_symbol; - } - catch(...) // logging must not replace the original exception - { - } - }); + std::size_t output_num = node.output().size(); + std::string node_name = node.op_type() + "_" + std::to_string(mod->size()); + std::string debug_symbol = make_node_debug_symbol(this->use_debug_symbols, node, node_name); + auto guard = + on_scope_fail([&]() noexcept { log_node_parse_exception(node, debug_symbol); }); if(ops.count(node.op_type()) == 0) { if(skip_unknown_operators)