Internals: Add utility to perform bisection search for debugging (#7294)
This commit is contained in:
parent
086bf351f2
commit
416b30d884
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
====================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -71,6 +71,7 @@ set(HEADERS
|
|||
V3Coverage.h
|
||||
V3CoverageJoin.h
|
||||
V3Dead.h
|
||||
V3DebugBisect.h
|
||||
V3Delayed.h
|
||||
V3Depth.h
|
||||
V3DepthBlock.h
|
||||
|
|
|
|||
|
|
@ -362,6 +362,7 @@ NON_STANDALONE_HEADERS = \
|
|||
V3AstNodeExpr.h \
|
||||
V3AstNodeOther.h \
|
||||
V3AstNodeStmt.h \
|
||||
V3DebugBisect.h \
|
||||
V3DfgVertices.h \
|
||||
V3ThreadPool.h \
|
||||
V3WidthRemove.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 <string>
|
||||
|
||||
// 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 <typename Callable>
|
||||
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; }
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 <bool T_Scoped>
|
||||
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)) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue