Expand description
Neural Fictitious Self-Play (NFSP) trainer (issue #106). Neural Fictitious Self-Play (NFSP) trainer.
Burn-native implementation of NFSP (Heinrich & Silver 2016, arXiv:1603.01121). The original paper proves ε-Nash convergence in the 2-agent zero-sum setting (§3 Algorithm 1, Theorem 3). Tracking issue: #106.
§N-player (“approximate NFSP”) caveat
As of #119, the trainer accepts joint_config.num_agents > 2. The
Vitter Algorithm R uniformity invariant on each per-agent reservoir
survives unchanged (the per-agent reservoirs are independent and
Algorithm R’s correctness does not depend on the number of agents).
However, the §3 Algorithm 1 ε-Nash convergence guarantee does
NOT generalize to N > 2 — Heinrich & Silver §5 explicitly flags
“extending the convergence theory to N-player general-sum games”
as future work. For N > 2 this implementation is therefore best
described as an “approximate NFSP” smoke trainer: the BR/AP
mechanics, η-anticipatory mixing, and reservoir sampling are all
exercised, and empirically the average policy converges on
symmetric mixed-Nash games like
crate::env::games::n_player_matching_pennies::NPlayerMatchingPennies,
but no formal guarantee is shipped beyond the per-agent reservoir
uniformity.
§Pseudocode (Heinrich & Silver §3 Algorithm 1)
for each iteration k:
for each rollout step t:
draw c ~ Bernoulli(η) # η-anticipatory mixing
if c == 1:
a_t ~ best_response_policy # BR (PPO actor)
reservoir.push((s_t, a_t)) # ← reservoir-sampled
else:
a_t ~ average_policy # AP (supervised actor)
# NOTE: do NOT push AP actions into the reservoir
train BR with PPO on the rollout (freeze-N-1 via JointMultiAgentTrainer)
sample minibatch from reservoir; train AP via cross-entropy on (s, a)
endThe reservoir is Vitter’s Algorithm R (paper §3.2 footnote 3, see also Vitter 1985 “Random Sampling with a Reservoir”). Using a FIFO or sliding window instead defeats the convergence guarantee: NFSP depends on the supervised target being a uniform sample over the history of best-response actions so the AP converges to the time-averaged BR policy.
§η-anticipatory mixing
At every rollout step the trainer flips a Bernoulli(η) coin. With
probability η it samples from the best-response (BR) policy and
pushes the observation/action pair into the reservoir buffer. With
probability 1 − η it samples from the average (AP) policy and
does not push to the reservoir. Appending AP actions to the
reservoir collapses NFSP to vanilla fictitious play and prevents
convergence to NE — this is the critical invariant the paper’s
footnote calls out. Default η = 0.1 per Heinrich & Silver §3.
§Reservoir vs FIFO
Heinrich & Silver §3.2 footnote 3: “Reservoir sampling avoids the policy distribution drift caused by uniformly drawing from a more recent window.” A FIFO buffer biases the AP towards recent BR actions, which is roughly equivalent to learning a recency-weighted mixture — that mixture is not the time-average and need not match any fictitious-play fixed point. Vitter’s Algorithm R keeps the held-items’ distribution uniform across the entire history of pushes regardless of stream length.
§Per-agent observation handling
NFSP builds on top of
crate::multi_agent::joint::JointMultiAgentTrainer, which records
a per-agent observation stream in
JointRollout::observations_per_agent. Agent i’s reservoir
receives agent i’s observation (not agent 0’s), so partial-
observability envs supervise the AP module on the correct view.
Matching pennies returns identical observations to both agents,
which keeps the regression tests bit-stable through the per-agent
refactor (PR #118).
§Determinism dependency on #109
The reservoir buffer’s eviction RNG, the η-anticipatory coin flips,
and the average-policy minibatch sampler are all seeded from
NfspConfig::seed via an internal StdRng. However, the inner
crate::multi_agent::joint::JointMultiAgentTrainer::update_with_active_agents
still uses rand::rng() for its per-epoch shuffle (see
src/multi_agent/joint.rs:662), which means same-seed PSRO and
NFSP runs are not yet bit-identical across wall-clock invocations.
Issue #109 tracks the fix. The in-module determinism here is
sufficient for the reservoir-uniformity and η-mixing unit tests in
this PR.
Structs§
- Nfsp
Config - NFSP trainer configuration.
- Nfsp
Iteration Stats - Per-iteration NFSP statistics.
- Nfsp
Stats - Aggregate NFSP trainer statistics returned by
NfspTrainer::run. - Nfsp
Trainer - NFSP outer-loop trainer.
- Reservoir
Buffer - Reservoir-sampled buffer backing the NFSP average-policy supervised
dataset. Implements Vitter (1985) Algorithm R: every distinct item
streamed through
pushis retained with probabilitycapacity / stream_index(1-indexed across the full lifetime of the buffer). Under capacity, all pushes are kept; at capacity, each push at stream indexi ≥ capacity + 1is inserted with probabilitycapacity / ireplacing a uniformly-random existing slot.