diff options
| author | Scott Gasch <[email protected]> | 2026-10-20 18:37:20 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-20 18:37:20 -0700 |
| commit | 9cf14e14e6999420f234cda951894337e034f57c (patch) | |
| tree | d4845d0cf127ef4226a5cd2e60472ee750213614 /src/root.c | |
| parent | c8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80 (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.c | 53 |
1 files changed, 53 insertions, 0 deletions
@@ -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" |
