Fix biased bit distribution under value < (1 << N) constraints (#7563) (#7684)

Fixes #7563
This commit is contained in:
Yilou Wang 2026-05-31 01:00:35 +08:00 committed by GitHub
parent 125bdb45f5
commit fa0f814686
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 199 additions and 8 deletions

View File

@ -30,6 +30,18 @@
#include <sstream>
#include <streambuf>
// Diversity (scalar rand vars): tie each free bit to a random target via a
// boolean assumption literal, then force the bits with (check-sat-assuming).
// If UNSAT, (get-unsat-assumptions) names the literals clashing with the
// feasible hard+soft base; drop one per round and recheck until SAT, which
// keeps the maximal set of bits compatible with the constraints. Only these
// private literals are assumed (never user constraints), and assumptions are
// ephemeral, so hard/soft/randc semantics are untouched and rounds need no
// push/pop or re-asserting. The surviving assumptions steer each free bit
// from the random target, spreading a wide range uniformly (fixing the
// boundary bias under `value < (1<<N)`, issue #7563) while keeping run-to-run
// diversity on tightly coupled bits. Array/queue rand vars skip pinning
// (per-element ranges aren't power-of-2 boundaries) and use the XOR rounds.
#define _VL_SOLVER_HASH_LEN 1
#define _VL_SOLVER_HASH_LEN_TOTAL 4
@ -516,6 +528,8 @@ bool VlRandomizer::next(VlRNG& rngr) {
// Try hard + soft[0..N-1], then hard + soft[1..N-1], ..., then hard only.
// First SAT phase wins. If hard-only is UNSAT, report via unsat-core.
os << "(set-option :produce-models true)\n";
// Lets the scalar pin path learn which free-bit assumptions conflict.
os << "(set-option :produce-unsat-assumptions true)\n";
os << "(set-logic QF_ABV)\n";
os << "(define-fun __Vbv ((b Bool)) (_ BitVec 1) (ite b #b1 #b0))\n";
os << "(define-fun __Vbool ((v (_ BitVec 1))) Bool (= #b1 v))\n";
@ -616,15 +630,66 @@ bool VlRandomizer::next(VlRNG& rngr) {
return false;
}
// Pinned vars: salting cannot diversify, only burn solver calls.
if (!m_checkOnly) {
for (int i = 0; i < _VL_SOLVER_HASH_LEN_TOTAL && sat; ++i) {
os << "(assert ";
randomConstraint(os, rngr, _VL_SOLVER_HASH_LEN);
os << ")\n";
os << "\n(check-sat)\n";
sat = parseSolution(os, false);
(void)sat;
bool hasArray = false;
for (const auto& var : m_vars) {
if (var.second->dimension() > 0) {
hasArray = true;
break;
}
}
if (!hasArray) {
// Tie each free bit to a fresh random target via a boolean
// assumption literal a_k <=> (bit_k == target_k), then force the
// bits with (check-sat-assuming ...). If UNSAT,
// (get-unsat-assumptions) names the literals clashing with the
// feasible base; drop ONE per round so the maximal compatible
// set survives -- dropping a whole conflicting group at once
// would collapse the diversity of tightly coupled bits (one-hot,
// 2-value sets) onto the solver's fixed default. Assumptions are
// ephemeral, so rounds need no push/pop or re-asserting and the
// solver keeps its learned clauses. Each round drops >= 1 -> ends
// in <= npins rounds.
std::vector<bool> targets;
int npins = 0;
for (const auto& var : m_vars) {
const int w = var.second->totalWidth();
for (int b = 0; b < w; b++) {
const bool target = (VL_RANDOM_RNG_I(rngr) & 1);
targets.push_back(target);
os << "(declare-fun a" << npins << " () Bool)\n";
os << "(assert (= a" << npins << " (=";
var.second->emitExtract(os, b);
os << " #b" << (target ? '1' : '0') << ")))\n";
++npins;
}
}
std::vector<bool> dropped(npins, false);
for (int round = 0; round <= npins; ++round) {
os << "(check-sat-assuming (";
for (int k = 0; k < npins; k++)
if (!dropped[k]) os << " a" << k;
os << "))\n";
if (parseSolution(os, false)) break;
// get-unsat-assumptions only echoes still-active literals,
// so the first in-range index is a live conflicting bit.
const std::vector<int> core = readUnsatAssumptions(os);
for (const int idx : core)
if (idx < npins) {
dropped[idx] = true;
break;
}
}
} else {
// Array present: original XOR-rounds path.
for (int i = 0; i < _VL_SOLVER_HASH_LEN_TOTAL && sat; ++i) {
os << "(assert ";
randomConstraint(os, rngr, _VL_SOLVER_HASH_LEN);
os << ")\n";
os << "\n(check-sat)\n";
sat = parseSolution(os, false);
(void)sat;
}
}
}
@ -643,6 +708,25 @@ bool VlRandomizer::checkSat(std::iostream& os) {
return result == "sat";
}
std::vector<int> VlRandomizer::readUnsatAssumptions(std::iostream& os) {
os << "(get-unsat-assumptions)\n";
std::string line;
do { std::getline(os, line); } while (line.empty());
// The response lists only "a<N>" literals; collect each full integer run.
std::vector<int> idxs;
std::string num;
for (const char c : line) {
if (std::isdigit(static_cast<unsigned char>(c))) {
num += c;
} else if (!num.empty()) {
idxs.push_back(std::stoi(num));
num.clear();
}
}
if (!num.empty()) idxs.push_back(std::stoi(num));
return idxs;
}
bool VlRandomizer::parseSolution(std::iostream& os, bool log) {
std::string sat;
do { std::getline(os, sat); } while (sat == "");

View File

@ -235,6 +235,8 @@ class VlRandomizer VL_NOT_FINAL {
void randomConstraint(std::ostream& os, VlRNG& rngr, int bits);
bool parseSolution(std::iostream& os, bool log = false);
bool checkSat(std::iostream& os);
// Indices of the "a<N>" literals named by (get-unsat-assumptions).
std::vector<int> readUnsatAssumptions(std::iostream& os);
void emitRandcExclusions(std::ostream& os) const; // Emit randc exclusion constraints
void recordRandcValues(); // Record solved randc values for future exclusion
size_t hashConstraints() const;

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# 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: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
if not test.have_solver:
test.skip("No constraint solver installed")
test.compile()
test.execute(all_run_flags=["+verilator+seed+1"])
test.passes()

View File

@ -0,0 +1,84 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 PlanV GmbH
// SPDX-License-Identifier: CC0-1.0
// verilog_format: off
`define stop $stop
`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0);
`define check_le(gotv,maxv) do if ((gotv) > (maxv)) begin $write("%%Error: %s:%0d: got=%0d exp<=%0d\n", `__FILE__,`__LINE__, (gotv), (maxv)); `stop; end while(0);
// verilog_format: on
typedef logic unsigned [63:0] uvm_reg_data_t;
class uvm_reg_field;
rand uvm_reg_data_t value;
int unsigned m_size;
int unsigned m_ones[64];
constraint c_field_valid {
if (64 > m_size) {
value < (64'h1 << m_size);
}
}
function void configure(int unsigned size);
value = 0;
m_size = size;
endfunction
function void tally;
for (int b = 0; b < 64; b++) if (value[b]) m_ones[b]++;
endfunction
endclass
class regA;
rand uvm_reg_field fa1, fa15, fa31, fa32;
function new;
fa1 = new;
fa15 = new;
fa31 = new;
fa32 = new;
fa1.configure(1);
fa15.configure(15);
fa31.configure(31);
fa32.configure(32);
endfunction
endclass
module t;
regA r;
int unsigned i;
// 200 trials over uvm_reg_field-shaped `value < (1<<m_size)`. Each free bit
// should be a fair coin flip; the pre-fix bug pinned them near the boundary
// K-1 (140-180 ones, 70-90%). Band [70, 130] = [35%, 65%] is ~4.2 sigma off
// the fair-50% mean (Binomial(200, 0.5)), so a uniform mechanism passes
// ~99.7% per run while the boundary bias overruns the upper bound.
localparam int unsigned TRIALS = 200;
localparam int unsigned HI = 130;
localparam int unsigned LO = 70;
initial begin
r = new;
for (int t = 0; t < TRIALS; t++) begin
i = r.randomize();
`checkd(i, 1);
r.fa1.tally;
r.fa15.tally;
r.fa31.tally;
r.fa32.tally;
end
// Symmetric 35-65% band per free bit. Master FAILs the upper bound.
for (int b = 0; b < 15; b++) `check_le(r.fa15.m_ones[b], HI);
for (int b = 0; b < 31; b++) `check_le(r.fa31.m_ones[b], HI);
for (int b = 0; b < 32; b++) `check_le(r.fa32.m_ones[b], HI);
for (int b = 0; b < 15; b++) if (r.fa15.m_ones[b] < LO) begin $write("%%Error: fa15[%0d] ones=%0d < %0d\n", b, r.fa15.m_ones[b], LO); `stop; end
for (int b = 0; b < 31; b++) if (r.fa31.m_ones[b] < LO) begin $write("%%Error: fa31[%0d] ones=%0d < %0d\n", b, r.fa31.m_ones[b], LO); `stop; end
for (int b = 0; b < 32; b++) if (r.fa32.m_ones[b] < LO) begin $write("%%Error: fa32[%0d] ones=%0d < %0d\n", b, r.fa32.m_ones[b], LO); `stop; end
// High bits beyond m_size must remain 0.
for (int b = 1; b < 64; b++) `checkd(r.fa1.m_ones[b], 0);
for (int b = 15; b < 64; b++) `checkd(r.fa15.m_ones[b], 0);
for (int b = 31; b < 64; b++) `checkd(r.fa31.m_ones[b], 0);
for (int b = 32; b < 64; b++) `checkd(r.fa32.m_ones[b], 0);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule