Internals: Add utility to perform bisection search for debugging (#7294)

This commit is contained in:
Geza Lore 2026-03-21 10:13:27 +00:00 committed by GitHub
parent 086bf351f2
commit 416b30d884
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 227 additions and 21 deletions

View File

@ -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 \

View File

@ -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
====================

105
nodist/verilator_bisect Executable file
View File

@ -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

View File

@ -71,6 +71,7 @@ set(HEADERS
V3Coverage.h
V3CoverageJoin.h
V3Dead.h
V3DebugBisect.h
V3Delayed.h
V3Depth.h
V3DepthBlock.h

View File

@ -362,6 +362,7 @@ NON_STANDALONE_HEADERS = \
V3AstNodeExpr.h \
V3AstNodeOther.h \
V3AstNodeStmt.h \
V3DebugBisect.h \
V3DfgVertices.h \
V3ThreadPool.h \
V3WidthRemove.h \

50
src/V3DebugBisect.h Normal file
View File

@ -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; }
};

View File

@ -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);

View File

@ -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)) {

View File

@ -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"