Skip to content

Commit 5d15159

Browse files
committed
feat(eval): add count-aware threat GAM
The incumbent threat table cannot distinguish repeated high-order threat counts without widening the consumed V2a joint surface. Add zero-referenced count and pair residuals beside the unchanged 11-bit table, with fail-closed v8 loading and deterministic support isolation. Extend tuner export, parity, and fixed-scale evaluation support for the new component. Consume exact tail batches only for ThreatGam so the new recipe receives its frozen dose while pinned legacy replay retains full-batch-only optimizer semantics.
1 parent 8165a6a commit 5d15159

9 files changed

Lines changed: 844 additions & 151 deletions

File tree

Rapfi/command/tuning.cpp

Lines changed: 241 additions & 24 deletions
Large diffs are not rendered by default.

Rapfi/config.cpp

Lines changed: 162 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "search/searcher.h"
3232
#include "search/searchthread.h"
3333

34+
#include <algorithm>
3435
#include <array>
3536
#include <cpptoml.h>
3637
#include <cstdint>
@@ -58,20 +59,60 @@ constexpr uint32_t I(uint32_t y, uint32_t x)
5859
constexpr std::array<char, 8> ClassicalModelExtensionMagic =
5960
{'R', 'F', 'C', 'L', 'M', 'O', 'D', '1'};
6061
constexpr uint32_t ClassicalModelCompactP3Version = 7;
62+
constexpr uint32_t ClassicalModelThreatGamVersion = 8;
6163
constexpr uint32_t ModelPolicyTableCount = 1;
6264
constexpr uint32_t ModelPolicyContextCount = Evaluation::PolicyStoredContextCount;
6365
constexpr uint32_t ModelBlendComponentCount = 0;
6466
constexpr uint32_t PolicyCrossPatternCount = Evaluation::PolicyStoredPatternCount;
6567
constexpr uint32_t PolicyCrossPayloadBytes =
6668
ModelPolicyContextCount * PolicyCrossPatternCount * PolicyCrossPatternCount * sizeof(Score);
6769
constexpr uint32_t ClassicalModelCompactP3PayloadBytes = PolicyCrossPayloadBytes;
70+
constexpr uint32_t ThreatGamExtraPayloadBytes =
71+
(RULE_NB + 1)
72+
* (ThreatGamAxisCount * ThreatGamLevelCount
73+
+ ThreatGamPairCount * ThreatGamLevelCount * ThreatGamLevelCount)
74+
* sizeof(Eval);
6875

6976
static_assert(ModelPolicyContextCount == Search::QUIET);
7077
static_assert(Search::QUIET + 1 == Search::POLICY_CONTEXT_NB);
7178
static_assert(ModelPolicyContextCount < Evaluation::PolicyContextCount);
7279

7380
using StoredPolicyCross =
7481
Score[ModelPolicyContextCount][PolicyCrossPatternCount][PolicyCrossPatternCount];
82+
using StoredThreatGamCount = Eval[RULE_NB + 1][ThreatGamAxisCount][ThreatGamLevelCount];
83+
using StoredThreatGamPair =
84+
Eval[RULE_NB + 1][ThreatGamPairCount][ThreatGamLevelCount][ThreatGamLevelCount];
85+
86+
bool hasValidThreatGamReferences(const StoredThreatGamCount &count, const StoredThreatGamPair &pair)
87+
{
88+
for (size_t table = 0; table < RULE_NB + 1; table++) {
89+
for (size_t axis = 0; axis < ThreatGamAxisCount; axis++)
90+
if (count[table][axis][0] != 0 || count[table][axis][1] != 0)
91+
return false;
92+
for (size_t p = 0; p < ThreatGamPairCount; p++)
93+
for (size_t lhs = 0; lhs < ThreatGamLevelCount; lhs++)
94+
for (size_t rhs = 0; rhs < ThreatGamLevelCount; rhs++)
95+
if ((lhs < 2 || rhs < 2) && pair[table][p][lhs][rhs] != 0)
96+
return false;
97+
}
98+
return true;
99+
}
100+
101+
bool hasThreatGamValues(const StoredThreatGamCount &count, const StoredThreatGamPair &pair)
102+
{
103+
for (const auto &table : count)
104+
for (const auto &axis : table)
105+
for (Eval value : axis)
106+
if (value != 0)
107+
return true;
108+
for (const auto &table : pair)
109+
for (const auto &p : table)
110+
for (const auto &row : p)
111+
for (Eval value : row)
112+
if (value != 0)
113+
return true;
114+
return false;
115+
}
75116

76117
} // namespace
77118

@@ -84,13 +125,38 @@ float ScalingFactor = 200.0f;
84125
// Classical evaluation and score tables
85126
// Note that Renju has asymmetry eval and score
86127

87-
Eval EVALS[RULE_NB + 1][PCODE_NB];
88-
Eval EVALS_THREAT[RULE_NB + 1][THREAT_NB];
128+
Eval EVALS[RULE_NB + 1][PCODE_NB];
129+
Eval EVALS_THREAT[RULE_NB + 1][THREAT_NB];
130+
Eval THREAT_GAM_COUNT[RULE_NB + 1][ThreatGamAxisCount][ThreatGamLevelCount] = {};
131+
Eval THREAT_GAM_PAIR[RULE_NB + 1][ThreatGamPairCount][ThreatGamLevelCount][ThreatGamLevelCount] =
132+
{};
133+
bool THREAT_GAM_ACTIVE[RULE_NB + 1] = {};
89134
MoveScorePair P4SCORES[RULE_NB + 1][PCODE_NB];
90135
Score POLICY_CROSS[RULE_NB + 1][PolicyContextCount][PATTERN4_NB][PATTERN4_NB];
91136
uint8_t POLICY_CROSS_ACTIVE_MASK[RULE_NB + 1] = {};
92137
bool POLICY_CROSS_PRESENT = false;
93138

139+
void resetThreatGam()
140+
{
141+
std::memset(THREAT_GAM_COUNT, 0, sizeof(THREAT_GAM_COUNT));
142+
std::memset(THREAT_GAM_PAIR, 0, sizeof(THREAT_GAM_PAIR));
143+
std::memset(THREAT_GAM_ACTIVE, 0, sizeof(THREAT_GAM_ACTIVE));
144+
}
145+
146+
void refreshThreatGamActivation()
147+
{
148+
for (size_t table = 0; table < RULE_NB + 1; table++) {
149+
THREAT_GAM_ACTIVE[table] = false;
150+
for (const auto &axis : THREAT_GAM_COUNT[table])
151+
for (Eval value : axis)
152+
THREAT_GAM_ACTIVE[table] |= value != 0;
153+
for (const auto &pair : THREAT_GAM_PAIR[table])
154+
for (const auto &row : pair)
155+
for (Eval value : row)
156+
THREAT_GAM_ACTIVE[table] |= value != 0;
157+
}
158+
}
159+
94160
void resetPolicyCross()
95161
{
96162
std::memset(POLICY_CROSS, 0, sizeof(POLICY_CROSS));
@@ -270,7 +336,7 @@ void Config::readRequirement(const cpptoml::table &t)
270336
{
271337
auto [major, minor, revision] = getVersionNumbers();
272338
uint64_t rapVer = ((uint64_t)major << 32) | ((uint64_t)minor << 16) | (uint64_t)revision;
273-
auto composeVersion = [](const std::vector<int64_t> &ver, const char *key) {
339+
auto composeVersion = [](const std::vector<int64_t> &ver, const char *key) {
274340
if (ver.size() != 3)
275341
throw std::runtime_error(std::string("illegal ") + key);
276342
for (int64_t component : ver)
@@ -480,6 +546,7 @@ void Config::readModel(const cpptoml::table &t, PendingConfig &pending)
480546
// extensions. Do not let rows from a previously loaded binary model leak
481547
// across an in-process config reload.
482548
Evaluation::resetPolicyCross();
549+
Evaluation::resetThreatGam();
483550
std::memset(Evaluation::EVALS_THREAT, 0, sizeof(Evaluation::EVALS_THREAT));
484551

485552
// Read Eval & Score
@@ -854,22 +921,35 @@ bool Config::loadModel(std::istream &inStream)
854921
// Decode the whole payload into local staging buffers and publish to the
855922
// live tables only after every check has passed, so a truncated or corrupt
856923
// model file leaves the current evaluation state untouched.
857-
double scalingFactorF64;
858-
Eval evals[RULE_NB + 1][PCODE_NB];
859-
Eval evalsThreat[RULE_NB + 1][THREAT_NB];
860-
Score scores[RULE_NB + 1][PCODE_NB][2];
924+
double scalingFactorF64;
925+
Eval evals[RULE_NB + 1][PCODE_NB];
926+
Eval evalsThreat[RULE_NB + 1][THREAT_NB];
927+
Score scores[RULE_NB + 1][PCODE_NB][2];
928+
StoredThreatGamCount threatGamCount {};
929+
StoredThreatGamPair threatGamPair {};
861930
in->read(reinterpret_cast<char *>(&scalingFactorF64), sizeof(scalingFactorF64));
862931
in->read(reinterpret_cast<char *>(evals), sizeof(evals));
863932
in->read(reinterpret_cast<char *>(evalsThreat), sizeof(evalsThreat));
864933
in->read(reinterpret_cast<char *>(scores), sizeof(scores));
865934

866935
if (!*in)
867936
return false;
937+
float scalingFactor = static_cast<float>(scalingFactorF64);
938+
if (!std::isfinite(scalingFactorF64) || scalingFactorF64 <= 0.0 || !std::isfinite(scalingFactor)
939+
|| scalingFactor <= 0.0f)
940+
return false;
868941

869942
auto publishBaseTables = [&]() {
870-
Evaluation::ScalingFactor = scalingFactorF64;
943+
Evaluation::ScalingFactor = scalingFactor;
871944
std::memcpy(Evaluation::EVALS, evals, sizeof(Evaluation::EVALS));
872945
std::memcpy(Evaluation::EVALS_THREAT, evalsThreat, sizeof(Evaluation::EVALS_THREAT));
946+
std::memcpy(Evaluation::THREAT_GAM_COUNT,
947+
threatGamCount,
948+
sizeof(Evaluation::THREAT_GAM_COUNT));
949+
std::memcpy(Evaluation::THREAT_GAM_PAIR,
950+
threatGamPair,
951+
sizeof(Evaluation::THREAT_GAM_PAIR));
952+
Evaluation::refreshThreatGamActivation();
873953

874954
// Set score table to P4SCORES
875955
for (int rule = 0; rule < RULE_NB + 1; rule++)
@@ -898,29 +978,56 @@ bool Config::loadModel(std::istream &inStream)
898978
in->read(reinterpret_cast<char *>(&contextCount), sizeof(contextCount));
899979
in->read(reinterpret_cast<char *>(&componentCount), sizeof(componentCount));
900980
in->read(reinterpret_cast<char *>(&patternCount), sizeof(patternCount));
901-
if (!*in || magic != ClassicalModelExtensionMagic || version != ClassicalModelCompactP3Version
902-
|| payloadBytes != ClassicalModelCompactP3PayloadBytes
903-
|| tableCount != ModelPolicyTableCount || contextCount != ModelPolicyContextCount
904-
|| componentCount != ModelBlendComponentCount || patternCount != PolicyCrossPatternCount)
981+
if (!*in || magic != ClassicalModelExtensionMagic)
905982
return false;
906983

907-
StoredPolicyCross policyCross;
908-
in->read(reinterpret_cast<char *>(policyCross), sizeof(policyCross));
909-
if (!*in)
984+
bool hasPolicyCross = false;
985+
StoredPolicyCross policyCross {};
986+
if (version == ClassicalModelCompactP3Version) {
987+
if (payloadBytes != ClassicalModelCompactP3PayloadBytes
988+
|| tableCount != ModelPolicyTableCount || contextCount != ModelPolicyContextCount
989+
|| componentCount != ModelBlendComponentCount
990+
|| patternCount != PolicyCrossPatternCount)
991+
return false;
992+
in->read(reinterpret_cast<char *>(policyCross), sizeof(policyCross));
993+
hasPolicyCross = true;
994+
}
995+
else if (version == ClassicalModelThreatGamVersion) {
996+
bool validPolicyHeader = tableCount == 0 ? contextCount == 0 && patternCount == 0
997+
: tableCount == ModelPolicyTableCount
998+
&& contextCount == ModelPolicyContextCount
999+
&& patternCount == PolicyCrossPatternCount;
1000+
uint32_t expectedPayloadBytes =
1001+
ThreatGamExtraPayloadBytes + (tableCount ? PolicyCrossPayloadBytes : 0);
1002+
if (!validPolicyHeader || componentCount != ModelBlendComponentCount
1003+
|| payloadBytes != expectedPayloadBytes)
1004+
return false;
1005+
if (tableCount) {
1006+
in->read(reinterpret_cast<char *>(policyCross), sizeof(policyCross));
1007+
hasPolicyCross = true;
1008+
}
1009+
in->read(reinterpret_cast<char *>(threatGamCount), sizeof(threatGamCount));
1010+
in->read(reinterpret_cast<char *>(threatGamPair), sizeof(threatGamPair));
1011+
if (!hasValidThreatGamReferences(threatGamCount, threatGamPair))
1012+
return false;
1013+
}
1014+
else
9101015
return false;
9111016

912-
if (in->peek() != std::ios::traits_type::eof() || in->bad())
1017+
if (!*in || in->peek() != std::ios::traits_type::eof() || in->bad())
9131018
return false;
9141019
publishBaseTables();
9151020
Evaluation::resetPolicyCross();
916-
for (size_t context = 0; context < ModelPolicyContextCount; context++)
917-
for (size_t self = 0; self < PolicyCrossPatternCount; self++)
918-
for (size_t opponent = 0; opponent < PolicyCrossPatternCount; opponent++)
919-
Evaluation::POLICY_CROSS[FREESTYLE][context]
920-
[Evaluation::policyPatternFromStorageIndex(self)]
921-
[Evaluation::policyPatternFromStorageIndex(opponent)] =
922-
policyCross[context][self][opponent];
923-
Evaluation::activatePolicyCross();
1021+
if (hasPolicyCross) {
1022+
for (size_t context = 0; context < ModelPolicyContextCount; context++)
1023+
for (size_t self = 0; self < PolicyCrossPatternCount; self++)
1024+
for (size_t opponent = 0; opponent < PolicyCrossPatternCount; opponent++)
1025+
Evaluation::POLICY_CROSS[FREESTYLE][context]
1026+
[Evaluation::policyPatternFromStorageIndex(self)]
1027+
[Evaluation::policyPatternFromStorageIndex(opponent)] =
1028+
policyCross[context][self][opponent];
1029+
Evaluation::activatePolicyCross();
1030+
}
9241031
return true;
9251032
}
9261033

@@ -949,29 +1056,45 @@ void Config::exportModel(std::ostream &outStream)
9491056

9501057
bool writePolicyCross =
9511058
Evaluation::isPolicyCrossPresent() || !Evaluation::hasDefaultPolicyCross();
952-
if (!writePolicyCross)
1059+
bool writeThreatGam =
1060+
hasThreatGamValues(Evaluation::THREAT_GAM_COUNT, Evaluation::THREAT_GAM_PAIR);
1061+
if (!writePolicyCross && !writeThreatGam)
9531062
return;
9541063
out->write(ClassicalModelExtensionMagic.data(), ClassicalModelExtensionMagic.size());
955-
uint32_t version = ClassicalModelCompactP3Version;
956-
uint32_t payloadBytes = ClassicalModelCompactP3PayloadBytes;
957-
uint32_t tableCount = ModelPolicyTableCount;
958-
uint32_t contextCount = ModelPolicyContextCount;
1064+
uint32_t version =
1065+
writeThreatGam ? ClassicalModelThreatGamVersion : ClassicalModelCompactP3Version;
1066+
uint32_t payloadBytes = writeThreatGam ? ThreatGamExtraPayloadBytes
1067+
+ (writePolicyCross ? PolicyCrossPayloadBytes : 0)
1068+
: ClassicalModelCompactP3PayloadBytes;
1069+
uint32_t tableCount = writePolicyCross ? ModelPolicyTableCount : 0;
1070+
uint32_t contextCount = writePolicyCross ? ModelPolicyContextCount : 0;
9591071
uint32_t componentCount = ModelBlendComponentCount;
960-
uint32_t patternCount = PolicyCrossPatternCount;
1072+
uint32_t patternCount = writePolicyCross ? PolicyCrossPatternCount : 0;
9611073
out->write(reinterpret_cast<const char *>(&version), sizeof(version));
9621074
out->write(reinterpret_cast<const char *>(&payloadBytes), sizeof(payloadBytes));
9631075
out->write(reinterpret_cast<const char *>(&tableCount), sizeof(tableCount));
9641076
out->write(reinterpret_cast<const char *>(&contextCount), sizeof(contextCount));
9651077
out->write(reinterpret_cast<const char *>(&componentCount), sizeof(componentCount));
9661078
out->write(reinterpret_cast<const char *>(&patternCount), sizeof(patternCount));
9671079

968-
StoredPolicyCross policyCross;
969-
for (size_t context = 0; context < ModelPolicyContextCount; context++)
970-
for (size_t self = 0; self < PolicyCrossPatternCount; self++)
971-
for (size_t opponent = 0; opponent < PolicyCrossPatternCount; opponent++)
972-
policyCross[context][self][opponent] =
973-
Evaluation::POLICY_CROSS[FREESTYLE][context]
974-
[Evaluation::policyPatternFromStorageIndex(self)]
975-
[Evaluation::policyPatternFromStorageIndex(opponent)];
976-
out->write(reinterpret_cast<const char *>(policyCross), sizeof(policyCross));
1080+
if (writePolicyCross) {
1081+
StoredPolicyCross policyCross;
1082+
for (size_t context = 0; context < ModelPolicyContextCount; context++)
1083+
for (size_t self = 0; self < PolicyCrossPatternCount; self++)
1084+
for (size_t opponent = 0; opponent < PolicyCrossPatternCount; opponent++)
1085+
policyCross[context][self][opponent] =
1086+
Evaluation::POLICY_CROSS[FREESTYLE][context]
1087+
[Evaluation::policyPatternFromStorageIndex(self)]
1088+
[Evaluation::policyPatternFromStorageIndex(
1089+
opponent)];
1090+
out->write(reinterpret_cast<const char *>(policyCross), sizeof(policyCross));
1091+
}
1092+
if (writeThreatGam) {
1093+
if (!hasValidThreatGamReferences(Evaluation::THREAT_GAM_COUNT, Evaluation::THREAT_GAM_PAIR))
1094+
throw std::runtime_error("threat GAM reference cells must remain zero");
1095+
out->write(reinterpret_cast<const char *>(Evaluation::THREAT_GAM_COUNT),
1096+
sizeof(Evaluation::THREAT_GAM_COUNT));
1097+
out->write(reinterpret_cast<const char *>(Evaluation::THREAT_GAM_PAIR),
1098+
sizeof(Evaluation::THREAT_GAM_PAIR));
1099+
}
9771100
}

Rapfi/eval/eval.cpp

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ using Evaluation::EvalCfg;
2929

3030
namespace {
3131

32+
constexpr uint8_t quantizeThreatCount(uint16_t count)
33+
{
34+
return static_cast<uint8_t>(count < 3 ? count : 3);
35+
}
36+
37+
std::array<uint8_t, ThreatGamAxisCount> makeThreatCounts(const StateInfo &st, Color self)
38+
{
39+
Color oppo = ~self;
40+
return {quantizeThreatCount(st.p4Count[oppo][E_BLOCK4]),
41+
quantizeThreatCount(st.p4Count[oppo][H_FLEX3]),
42+
quantizeThreatCount(st.p4Count[self][H_FLEX3]),
43+
quantizeThreatCount(st.p4Count[self][E_BLOCK4])};
44+
}
45+
3246
/// Makes threat mask according current pattern4 counts on board.
3347
int makeThreatMask(const StateInfo &st, Color self)
3448
{
@@ -58,7 +72,6 @@ int makeThreatMask(const StateInfo &st, Color self)
5872
mask |= 0b100000000 & -int(oppoFour);
5973
mask |= 0b1000000000 & -int(oppoThreePlus);
6074
mask |= 0b10000000000 & -int(oppoThree);
61-
6275
assert(0 <= mask && mask < THREAT_NB);
6376
return mask;
6477
}
@@ -68,8 +81,18 @@ int makeThreatMask(const StateInfo &st, Color self)
6881
template <Rule R>
6982
inline Value evaluateThreat(const StateInfo &st, Color self)
7083
{
71-
return (
72-
Value)Evaluation::EVALS_THREAT[Evaluation::tableIndex(R, self)][makeThreatMask(st, self)];
84+
const size_t table = Evaluation::tableIndex(R, self);
85+
int value = Evaluation::EVALS_THREAT[table][makeThreatMask(st, self)];
86+
if (Evaluation::THREAT_GAM_ACTIVE[table]) {
87+
auto counts = makeThreatCounts(st, self);
88+
for (size_t axis = 0; axis < ThreatGamAxisCount; axis++)
89+
value += Evaluation::THREAT_GAM_COUNT[table][axis][counts[axis]];
90+
for (size_t pair = 0; pair < ThreatGamPairCount; pair++) {
91+
auto [lhs, rhs] = ThreatGamPairs[pair];
92+
value += Evaluation::THREAT_GAM_PAIR[table][pair][counts[lhs]][counts[rhs]];
93+
}
94+
}
95+
return static_cast<Value>(value);
7396
}
7497

7598
/// Evaluates basic patterns on board.
@@ -171,6 +194,7 @@ EvalInfo::EvalInfo(const Board &board, Rule rule)
171194
: plyBack {0}
172195
, self(board.sideToMove())
173196
, threatMask(makeThreatMask(board.stateInfo(), self))
197+
, threatCounts(makeThreatCounts(board.stateInfo(), self))
174198
{
175199
Board &b = const_cast<Board &>(board);
176200
Pos undoHistory[2];

Rapfi/eval/eval.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include "../core/types.h"
2222
#include "scoretables.h"
2323

24+
#include <array>
25+
2426
class Board;
2527

2628
namespace Evaluation {
@@ -39,8 +41,9 @@ struct EvalInfo
3941
{
4042
uint16_t pcodeCount[SIDE_NB][PCODE_NB];
4143
} plyBack[2];
42-
Color self;
43-
int threatMask;
44+
Color self;
45+
int threatMask;
46+
std::array<uint8_t, ThreatGamAxisCount> threatCounts;
4447

4548
EvalInfo(const Board &board, Rule rule);
4649
};

0 commit comments

Comments
 (0)