summaryrefslogtreecommitdiff
path: root/src/root.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-20 18:37:20 -0700
committerScott Gasch <[email protected]>2026-10-20 18:37:20 -0700
commit9cf14e14e6999420f234cda951894337e034f57c (patch)
treed4845d0cf127ef4226a5cd2e60472ee750213614 /src/root.c
parentc8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80 (diff)
NNUE: re-fit raw-to-score calibration, NNU4 weight format, super-lazy default-on, lazy probe measured net-negative
Fits an affine (a + b*raw) correction into every exported .nnue file (export_nnue_weights.py's CalibrateScoreMapping) so the engine's consumed centipawn output tracks Stockfish's real score scale rather than the net's nominal raw WDL-sigmoid output -- training variance alone shifts this mapping across retrains even with an identical recipe, so it's re-fit and baked in per export rather than assumed constant. Weight format bumped to "NNU4" to carry the two new floats; older NNU1/2/3 files are no longer readable. Super-lazy (material-only) pre-filter promoted to default-on (g_fUseNnueSuperLazy = TRUE) with a recalibrated per-material-bucket margin table (measured against the recalibrated net, 16-shard cross_sectional_200k.ep_ sample) -- real, measured nps win across all three curated suites. The separate lazy-probe gate (accumulator-based early exit) is measured net-negative on real search traffic at this net's probe-vs-forward latency ratio and stays off by default (g_fUseNnueLazyProbe = FALSE); code kept live since margin tuning could still flip this, per docs/NNUE.md. Also: direct per-call latency instrumentation for NnueProbeScore/ NnueForward (root.c, EVAL_TIME build) to make the lazy-probe breakeven math measurable directly instead of inferred; QSearch futility margins given real-data-derived NNUE_FULL/NNUE_LAZY starting points. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/root.c')
-rw-r--r--src/root.c53
1 files changed, 53 insertions, 0 deletions
diff --git a/src/root.c b/src/root.c
index 47fd103..f686efa 100644
--- a/src/root.c
+++ b/src/root.c
@@ -628,6 +628,59 @@ Return value:
#endif
}
#endif
+#ifdef EVAL_TIME
+ // 2026-10-20: NNUE's own per-call latency, independent of the
+ // LAZY_EVAL block above (NNUE is a runtime g_fUseNnueEval switch,
+ // not a #ifdef LAZY_EVAL path, so it never fed that block's
+ // counters). Direct answer to "is the lazy-probe gate paying for
+ // itself": the gate costs u64CyclesNnueProbeCall on EVERY attempt
+ // (hit or miss -- u64NnueProbeAttempts, not just u64LazyEvals'
+ // hit count) and only SAVES a full-forward call on a hit, so the
+ // gate is worth it only if (hit rate) * (forward cost - probe
+ // cost) exceeds (miss rate) * (probe cost).
+ //
+ // Caveat printed alongside rather than silently baked in: each
+ // TIMED_EVAL_CALL is a pair of back-to-back SystemReadTimeStamp-
+ // Counter() (rdtsc) reads, and rdtsc itself isn't free (commonly
+ // cited around ~80 cycles, more with the serializing behavior
+ // some cores give it) -- for NnueForward's thousands of cycles
+ // that overhead is noise, but for NnueProbeScore's much smaller
+ // 512-wide dot product it may not be. Measure the pair's own
+ // overhead right here (an empty back-to-back read, no call in
+ // between) so the raw numbers below can be read against it rather
+ // than taken as exact.
+ {
+ UINT64 u64OverheadStart = SystemReadTimeStampCounter();
+ UINT64 u64OverheadEnd = SystemReadTimeStampCounter();
+ UINT64 u64PairOverhead = u64OverheadEnd - u64OverheadStart;
+ UINT64 u64ProbeCyc = ctx->sCounters.tree.u64CyclesNnueProbeCall;
+ UINT64 u64ProbeAttempts = ctx->sCounters.tree.u64NnueProbeAttempts;
+ UINT64 u64FwdCyc = ctx->sCounters.tree.u64CyclesNnueForwardCall;
+ UINT64 u64FwdCalls = ctx->sCounters.tree.u64FullEvals;
+ double dProbeAvg = u64ProbeAttempts ?
+ (double)u64ProbeCyc / (double)u64ProbeAttempts : 0.0;
+ double dFwdAvg = u64FwdCalls ?
+ (double)u64FwdCyc / (double)u64FwdCalls : 0.0;
+
+ if ((0 != u64ProbeAttempts) || (0 != u64FwdCalls))
+ {
+ Trace("NNUE eval primitive latency (rdtsc pair overhead ~%"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT" cyc, subtract for a "
+ "rough net estimate):\n"
+ " NnueProbeScore: %10.1f cyc/call (raw, %"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT" attempts, net ~%10.1f)\n"
+ " NnueForward: %10.1f cyc/call (raw, %"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT" calls, net ~%10.1f)\n",
+ u64PairOverhead,
+ dProbeAvg, u64ProbeAttempts,
+ (dProbeAvg > (double)u64PairOverhead) ?
+ (dProbeAvg - (double)u64PairOverhead) : 0.0,
+ dFwdAvg, u64FwdCalls,
+ (dFwdAvg > (double)u64PairOverhead) ?
+ (dFwdAvg - (double)u64PairOverhead) : 0.0);
+ }
+ }
+#endif
Trace("Extensions: (%u +, %u q+, %u 1mv, %u !kmvs, %u mult+, %u pawn\n"
" %u threat, %u zug, %u sing, %u endg, %u bm, %u recap)\n"
" check by amount (1/4,1/2,3/4,1 ply): %u %u %u %u\n"