NNUE Evaluation

The classical evaluation of Ch. Evaluation is a hand-written sum of material, piece-square, pawn-structure and mobility terms. NNUE — efficiently updatable neural network — replaces or corrects that judgement with a small neural network evaluated at every leaf. Kirin supports its own network format, .knnue, with a training and conversion pipeline in kirin/tools/.

Note

NNUE is off by default, and turning it on is a two-step operation. Reading only Use NNUE would leave you with an engine that appears to ignore the setting — see §The one thing that catches everyone, which is the single most important section of this chapter.

Why a second evaluation at all

A classical evaluation is a set of human hypotheses about what matters, each with a hand-tuned weight. It is fast, debuggable, and permanently limited by the fact that somebody had to think of every term. A network trained on evaluated positions learns interactions nobody enumerated — the value of a knight outpost given a specific pawn structure and king position — and it does so at a cost the search can afford, because the accumulator is updated incrementally as moves are made and unmade rather than recomputed from scratch.

The trade is that a network is opaque. When a classical term misjudges a position you can read the term; when a network does, you retrain. This is why Kirin keeps both, and why the two are combined rather than switched between (§Blend and residual: how the two evaluations combine).

The .knnue format

A network is a flat little-endian binary. The layout is fixed by kirin/src/engine/nnue.h:

Field

Type

Meaning

magic

char[8]

the literal "KIRINNUE"

version

u32

format version, currently 1

feature count

u32

selects the feature set (below)

hidden size

u32

neurons in the hidden layer

activation limit

i32

clipped-ReLU ceiling

output bias

i32

scalar added to the output

hidden biases

i32[]

one per hidden neuron

output weights

i32[]

one per hidden neuron

feature weights

i32[]

featureCount $\times$ hiddenSize, feature-major

Weights are integers throughout. There is no floating point in the inference path, which keeps evaluation deterministic across machines — a property the reproducibility argument in Ch. Measuring Strength depends on, and one that a float network would quietly cost.

Feature sets

The feature count is not merely a size; it selects how the position is encoded:

Count

Feature set

Encoding

768

piece-square

$12 \times 64$: one input per (piece, square)

13 056

king bucket

piece-square, bucketed by the side-to-move king’s square

99 072

HalfKA

piece-square relative to both kings, dual perspective

The progression is the standard one. Plain piece-square knows nothing about king safety context: a knight on f5 is scored identically whether the enemy king is on g8 or a1. King-bucketing conditions the whole board on where the king stands. HalfKA carries a separate perspective for each side, which is what lets the network learn asymmetric king-safety judgements properly, at roughly $129\times$ the weight count of the flat set.

Loading a network

setoption name EvalFile value networks/kirin-lichess.knnue
setoption name Use NNUE value true
setoption name NNUE Blend value 100

If loading fails the engine does not silently fall back to a broken state: getNNUELastError() carries the reason and getNNUESource() reports what is actually loaded. A bootstrap network — format-valid but deliberately not strong — can be generated with cmake --build build --target bootstrap_nnue; it exists so the loader can be smoke-tested without shipping a real net, and it should never be mistaken for one.

Blend and residual: how the two evaluations combine

Kirin does not choose between classical and NNUE. It combines them, in one of two modes.

Blend mode (default)

A straight linear interpolation, in evaluation.cpp:

return (classicalScore * (100 - nnueBlendPercent)
        + nnueScore * nnueBlendPercent) / 100;

NNUE Blend is a percentage: 0 is pure classical, 100 is pure network, and intermediate values mix. Blending is useful while a network is being evaluated — a net that is strong in the middlegame but unreliable in endgames can still contribute at 40 % without being trusted outright.

Residual mode

Setting NNUE Residual changes the network’s role from evaluator to corrector. The classical score becomes the base and the network supplies an adjustment:

static int applyNNUEPolicy(int classicalScore, int nnueScore) {
    int correction = nnueScore;

    if (nnueResidualClamp > 0) {
        if (correction >  nnueResidualClamp) correction =  nnueResidualClamp;
        if (correction < -nnueResidualClamp) correction = -nnueResidualClamp;
    }
    if (nnueClassicalLimit > 0
        && std::abs(classicalScore) > nnueClassicalLimit) {
        correction = 0;
    }
    if (nnuePhaseScale) {
        correction = (correction * nonPawnMaterialPhase()) / 100;
    }
    return classicalScore + (correction * nnueBlendPercent) / 100;
}

Three guards shape the correction, and each answers a specific failure mode:

  • NNUE Residual Clamp bounds how far the network may move the score. A network that is wrong is usually wrong by a lot; a clamp turns a catastrophic misjudgement into a survivable one.

  • NNUE Classical Limit zeroes the correction entirely once the classical score already exceeds a threshold. In a position that is plainly winning by three pawns, the network has nothing useful to add and everything to risk.

  • NNUE Phase Scale scales the correction by remaining non-pawn material, fading the network out as the endgame approaches. Endgames are where networks are weakest and where exact classical rules (passed pawns, opposition) are strongest.

The one thing that catches everyone

Warning

NNUE Blend is the master gain in both modes, and its default is 0. Setting Use NNUE true on its own changes nothing observable: blend mode interpolates 0 % of the network, and residual mode multiplies the correction by $0/100$. Both collapse to exactly the classical score. A network that appears to have no effect is almost always this, not a bad net or a failed load.

Verify with getNNUESource() that a net is loaded, then confirm NNUE Blend is non-zero. The two settings are independent on purpose — it is what allows a net to be loaded and staged without being trusted — but the interaction is not discoverable from the option list alone.

Training pipeline

The pipeline lives in kirin/tools/ and is Python. It is not part of the engine build and is not required to run Kirin — only to produce a new network.

Tool

Role

pgn_to_training_csv.py

PGN games $\rightarrow$ labelled position CSV

generate_leaf_training_csv.py

synthesise leaf-like positions (search-consistent labels)

filter_training_csv.py

drop noisy or unrepresentative labels

train_nnue.py

train the network

convert_nnue.py

export to the .knnue binary

nnue_format.py

shared format reader/writer

write_bootstrap_nnue.py

deterministic format-valid stub net

nnue_policy_sweep.py

sweep blend/residual/clamp settings

Two points are worth drawing out, because they are where the pipeline’s design differs from a naive “train on game results” approach:

Leaf-like positions matter more than game positions. The network is evaluated at search leaves, which are quiet positions reached after quiescence — not the positions that appear in a game score. Training on game positions teaches the network to evaluate a distribution it will never actually see. generate_leaf_training_csv.py exists to close that gap.

The policy sweep is a distinct step from training. Once a net exists, the blend, residual, clamp, classical-limit and phase-scale settings form a configuration space that is searched independently. A mediocre network with good policy settings routinely beats a better network used naively, and nnue_policy_sweep.py automates finding that combination. Both are then confirmed by SPRT — Ch. Measuring Strength.

Note

The step-by-step training procedure, including data formats and command lines, is documented in docs/engine-docs/nnue_training.md. That file is the operational reference and is kept alongside the tools; this chapter covers what the format is, how the two evaluations combine, and why.