diff --git a/Makefile.in b/Makefile.in index c77a1c338..a753cfe90 100644 --- a/Makefile.in +++ b/Makefile.in @@ -517,6 +517,7 @@ PY_PROGRAMS = \ nodist/lint_py_test_filter \ nodist/log_changes \ nodist/uvm_pkg_packer \ + nodist/verilator_bisect \ nodist/verilator_saif_diff \ src/.gdbinit.py \ src/astgen \ diff --git a/docs/internals.rst b/docs/internals.rst index 140360d9d..02f6ed0c0 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -2133,6 +2133,47 @@ backtrace. You will typically see a frame sequence something like: visit() ... +Bisecting bad transformations +----------------------------- + +If a bad transformation in the internals of Verilator causes a failure only at +runtime, it can be found fairly automatically by only applying the transform a +limited number of times, then performing a bisection search over the limit to +pinpoint the exact transformation that introduces the failure. + +To facilitate this an instance of the ``V3DebugBisect`` class can be used in +conjunction with the ``verilator_bisect`` script. + +In the offending algorithm, create a static instance of ``V3DebugBisect``: + +:: + + static V3DebugBisect s_debugBisect{"TransformName"}; + +Call the ``stop`` method before applying a transformation, and do not proceed +if it returns ``false``. Then use ``verilator_bisect`` to search an interval of +values. You need to provide an arbitrary discriminator command, this should run +Verilator, then any necessary checks (e.g.: simulation) to detect that the +failure is still present. It should exit with a non-zero status if the failure +is still present. The discriminator command can otherwise be arbitrarily +complex, the actual search limit is passed via environment variables. E.g.: + +:: + + bin/verilator_bisect DfgPeephole 0 1000 test_regress/t/t_myothertest.py + +An additional command can be run before the discriminator command. E.g. this +will run RTLMeter, but first removes its working directory so the models are +recompiled on every step: + +:: + + bin/verilator_bisect --pre "rm -rf work-bisect" DfgPeephole 0 10000000 \ + rtlmeter run --cases "..." --workRoot=work-bisect + +When the bisection ends, the first value that makes the discriminator command +fail is printed, which identifies the exact offending application of the +transform. Adding a New Feature ==================== diff --git a/nodist/verilator_bisect b/nodist/verilator_bisect new file mode 100755 index 000000000..7f5d643b0 --- /dev/null +++ b/nodist/verilator_bisect @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# pylint: disable=C0103,C0114,C0116,W0613 +# +# This program is free software; you can redistribute it and/or modify the +# Verilator internals under the terms of either the GNU Lesser General +# Public License Version 3 or the Perl Artistic License Version 2.0. +# +# SPDX-FileCopyrightText: 2003-2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 +###################################################################### + +import argparse +import os +import subprocess +import sys + +try: + from termcolor import colored +except ModuleNotFoundError: + + def colored(msg, **kwargs): + return msg + + +def cprint(msg="", *, color=None, attrs=None, **kwargs): + print(colored(msg, color=color, attrs=attrs), **kwargs) + + +parser = argparse.ArgumentParser( + description='Binary search utility for debugging Verilator with V3DebugBisect', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +example: + %(prog)s DfgPeephole 0 1000 test_regress/t/t_foo.py --no-skip-identical + ''') + +parser.add_argument("--pre", type=str, help='Command to run before each execution') +parser.add_argument('name', help='Name of V3DebugBisect instance') +parser.add_argument('low', type=int, help='Bisection range low value, use 0 by default') +parser.add_argument('high', + type=int, + help='Bisection range high value, use a sufficiently high number') +parser.add_argument('cmd', + nargs=argparse.REMAINDER, + help='Discriminator command that should exit non-zero on failure') + +args = parser.parse_args() + +var = f"VERILATOR_DEBUG_BISECT_{args.name}" +passing = args.low - 1 +failing = args.high + 1 + +cprint() +cprint(f"Starting bisection serach for {var} in interval [{args.low}, {args.high}]", + attrs=["bold"]) + +while True: + cprint() + + passStr = str(passing) if passing >= args.low else '?' + failStr = str(failing) if failing < args.high else '?' + cprint(f"Current step Pass: {passStr} Fail: {failStr}", attrs=["bold"]) + + # Stop if found, or exhausted interval without finding both a pass and a fail + if failing == args.low: + cprint(f"The low endpoint of the search interval ({args.low}) fails. Suggest rerun with:", + color="yellow") + cprint(f" {sys.argv[0]} {args.name} 0 {args.low} ...", color="yellow") + sys.exit(1) + if passing == args.high: + cprint( + f"The high endpoint of the search interval ({args.high}) passes. Suggest rerun with:", + color="yellow") + cprint(f" {sys.argv[0]} {args.name} {args.high} {10*args.high} ...", color="yellow") + sys.exit(1) + if failing == passing + 1: + cprint(f"First faling value: {var}={failing}", attrs=["bold"]) + sys.exit(0) + + # Compute middle of interval to evaluate + mid = (failing + passing) // 2 + + # Run pre command if given: + if args.pre: + cprint("Running --pre command", attrs=["bold"]) + preResult = subprocess.run(args.pre, shell=True, check=False) + if preResult.returncode != 0: + cprint("Pre command failed", color="red") + sys.exit(2) + + # Set up environment variable + env = os.environ.copy() + env[var] = str(mid) + + # Run the discriminator command + cprint(f"Running with {var}={mid}", attrs=["bold"]) + result = subprocess.run(args.cmd, env=env, check=False) + + # Check status, update interval + if result.returncode != 0: + cprint(f"Run with {var}={mid}: Fail", color="red") + failing = mid + else: + cprint(f"Run with {var}={mid}: Pass", color="green") + passing = mid diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 92bd02219..5b1f6f8ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -71,6 +71,7 @@ set(HEADERS V3Coverage.h V3CoverageJoin.h V3Dead.h + V3DebugBisect.h V3Delayed.h V3Depth.h V3DepthBlock.h diff --git a/src/Makefile_obj.in b/src/Makefile_obj.in index 28f15e698..b2047d216 100644 --- a/src/Makefile_obj.in +++ b/src/Makefile_obj.in @@ -362,6 +362,7 @@ NON_STANDALONE_HEADERS = \ V3AstNodeExpr.h \ V3AstNodeOther.h \ V3AstNodeStmt.h \ + V3DebugBisect.h \ V3DfgVertices.h \ V3ThreadPool.h \ V3WidthRemove.h \ diff --git a/src/V3DebugBisect.h b/src/V3DebugBisect.h new file mode 100644 index 000000000..778b893df --- /dev/null +++ b/src/V3DebugBisect.h @@ -0,0 +1,50 @@ +// -*- mode: C++; c-file-style: "cc-mode" -*- +//************************************************************************* +// DESCRIPTION: Verilator: Bisection serach debugging utility +// +// Code available from: https://verilator.org +// +//************************************************************************* +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of either the GNU Lesser General Public License Version 3 +// or the Perl Artistic License Version 2.0. +// SPDX-FileCopyrightText: 2003-2026 Wilson Snyder +// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 +// +//************************************************************************* + +#ifndef VL_MT_DISABLED_CODE_UNIT +#error "V3DebugBisect.h uses global state and is not thread-safe" +#endif + +#include "V3Os.h" + +#include + +// Please see the inernals documentation for using this utility class with 'verilator_bisect' +class V3DebugBisect final { + // Name of instance + const char* const m_namep; + // Limit for stopping - 0 means no limit + const size_t m_limit = std::stoull(V3Os::getenvStr("VERILATOR_DEBUG_BISECT_"s + m_namep, "0")); + // Calls so far + size_t m_count = 0; + +public: + V3DebugBisect(const char* namep) + : m_namep{namep} {} + + // Returns 'false' up to m_limit invocations, then returns 'true'. + // Calls 'f' on the last invocation that returns 'false', which can be used for reporting. + template + bool stop(Callable&& f) { + if (VL_LIKELY(!m_limit)) return false; + ++m_count; + if (VL_UNLIKELY(m_count == m_limit)) f(); + return m_count > m_limit; + } + + // Returns true if the limit has been reached + bool isStopped() const { return m_count > m_limit; } +}; diff --git a/src/V3DfgPeephole.cpp b/src/V3DfgPeephole.cpp index 6d46af55d..a392d9547 100644 --- a/src/V3DfgPeephole.cpp +++ b/src/V3DfgPeephole.cpp @@ -140,11 +140,14 @@ class V3DfgPeephole final : public DfgVisitor { // Vertex lookup-table to avoid creating redundant vertices V3DfgCache m_cache{m_dfg}; + // Debug aid + static V3DebugBisect s_debugBisect; + #define APPLYING(id) if (checkApplying(VDfgPeepholePattern::id)) // METHODS bool checkApplying(VDfgPeepholePattern id) { - if (!m_ctx.m_enabled[id]) return false; + if (VL_UNLIKELY(!m_ctx.m_enabled[id] || s_debugBisect.isStopped())) return false; UINFO(9, "Applying DFG pattern " << id.ascii()); ++m_ctx.m_count[id]; return true; @@ -189,6 +192,16 @@ class V3DfgPeephole final : public DfgVisitor { } void replace(DfgVertex* vtxp, DfgVertex* replacementp) { + const auto debugCallback = [&]() -> void { + UINFO(0, "Problematic DfgPeephole replacement: " << vtxp << " -> " << replacementp); + m_dfg.sourceCone({vtxp, replacementp}); + const auto cone = m_dfg.sourceCone({vtxp, replacementp}); + m_dfg.dumpDotFilePrefixed("peephole-broken", [&](const DfgVertex& v) { // + return cone->count(&v); + }); + }; + if (VL_UNLIKELY(s_debugBisect.stop(debugCallback))) return; + // Add sinks of replaced vertex to the work list addSinksToWorkList(vtxp); // Add replacement to the work list @@ -1877,6 +1890,8 @@ public: static void apply(DfgGraph& dfg, V3DfgPeepholeContext& ctx) { V3DfgPeephole{dfg, ctx}; } }; +V3DebugBisect V3DfgPeephole::s_debugBisect{"DfgPeephole"}; + void V3DfgPasses::peephole(DfgGraph& dfg, V3DfgPeepholeContext& ctx) { if (!v3Global.opt.fDfgPeephole()) return; V3DfgPeephole::apply(dfg, ctx); diff --git a/src/V3DfgSynthesize.cpp b/src/V3DfgSynthesize.cpp index 643bb7141..8c31e51de 100644 --- a/src/V3DfgSynthesize.cpp +++ b/src/V3DfgSynthesize.cpp @@ -522,14 +522,8 @@ public: , m_ctx{ctx} {} }; -// For debugging, we can stop synthesizing after a certain number of vertices. -// for this we need a global counter (inside the template makes multiple copies) -static size_t s_dfgSynthDebugCount = 0; -// The number of vertices we stop after can be passed in through the environment -// you can then use a bisection search over this value and look at the dumps -// produced with the lowest failing value -static const size_t s_dfgSynthDebugLimit - = std::stoull(V3Os::getenvStr("VERILATOR_DFG_SYNTH_DEBUG", "0")); +// Debug aid - outisde 'AstToDfgSynthesize' as it is a template, but want one instance +V3DebugBisect s_dfgSynthDebugBisect{"DfgSynthesize"}; template class AstToDfgSynthesize final { @@ -1769,18 +1763,15 @@ class AstToDfgSynthesize final { UASSERT_OBJ(logicp->selectedForSynthesis(), logicp, "Unselected DfgLogic remains"); // Debug aid - if (VL_UNLIKELY(s_dfgSynthDebugLimit)) { - if (s_dfgSynthDebugCount == s_dfgSynthDebugLimit) break; - ++s_dfgSynthDebugCount; - if (s_dfgSynthDebugCount == s_dfgSynthDebugLimit) { - // This is the breaking logic - m_debugLogicp = logicp; - // Dump it - UINFOTREE(0, logicp->nodep(), "Problematic DfgLogic: " << logicp, " "); - V3EmitV::debugVerilogForTree(logicp->nodep(), std::cout); - debugDump("synth-lastok"); - } - } + const auto debugCallback = [&]() -> void { + // This is the breaking logic + m_debugLogicp = logicp; + // Dump it + UINFOTREE(0, logicp->nodep(), "Problematic DfgLogic: " << logicp, " "); + V3EmitV::debugVerilogForTree(logicp->nodep(), std::cout); + debugDump("synth-lastok"); + }; + if (VL_UNLIKELY(s_dfgSynthDebugBisect.stop(debugCallback))) break; // Synthesize it, if failed, enqueue for reversion if (!synthesize(*logicp)) { diff --git a/src/V3PchAstNoMT.h b/src/V3PchAstNoMT.h index d2ddc1d6e..e52164a8c 100644 --- a/src/V3PchAstNoMT.h +++ b/src/V3PchAstNoMT.h @@ -29,6 +29,7 @@ #include "V3Ast.h" #include "V3Broken.h" #include "V3Container.h" +#include "V3DebugBisect.h" #include "V3Error.h" #include "V3FileLine.h" #include "V3FunctionTraits.h"