Skip to content

Reproducibility & Determinism

NetForge is designed so a fixed seed replays an episode to the bit — not just rewards, but observations, SIEM embeddings, info dicts, and logs. This is a hard requirement for a benchmark: without it, reported numbers are not comparable.

The guarantee

For a single environment in a single process, two rollouts with the same seed and the same actions produce byte-identical:

  • observation arrays (obs, siem_embedding, adj_matrix, blue_comm, …)
  • per-agent rewards
  • per-agent info dicts
  • SIEM log buffers

This is enforced by tests/parity/test_seeded_observation_replay.py, which hashes the full observable stream across three scenarios and asserts same-seed identity, cross-seed divergence, and independence of two envs interleaved in one process.

import hashlib, numpy as np
from netforge_rl.environment.parallel_env import NetForgeRLEnv

def digest(seed):
    env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 30})
    env.reset(seed=seed)
    rng, h = np.random.default_rng(seed), hashlib.sha256()
    while env.agents:
        acts = {a: np.array([rng.integers(0, n) for n in env.action_space(a).nvec])
                for a in env.agents}
        obs, r, term, trunc, _ = env.step(acts)
        for a in sorted(obs):
            for k in sorted(obs[a]):
                h.update(np.ascontiguousarray(obs[a][k]).tobytes())
        if all(term.values()) or all(trunc.values()):
            break
    return h.hexdigest()

assert digest(123) == digest(123)   # reproducible
assert digest(1) != digest(2)       # seed-sensitive

How it is achieved

Every stochastic source is seeded from the episode seed at reset(seed):

Source Seeding
Topology generation NetworkGenerator.generate(seed)
Stochastic actions (exploit success, …) GlobalNetworkState.rng seeded per episode
Exploit hardware/sim outcomes MockHypervisor reseeded on every reset
SIEM log selection SIEMLogger(seed=…)
SIEM template text (ports, jitter, choices) per-call RNG threaded from the logger/green agent
Timestamps derived from a fixed epoch + seeded jitter, never wall-clock
Green-agent background noise env-local random.Random(seed)
Topology / physics / correlator / pcap each reset(seed)

Two subtle points that were explicitly engineered:

  • No shared module-global RNG. SIEM event templates take a per-call rng, so two environments running in the same process never perturb each other's telemetry. The interleaved-envs test guards this.
  • The exploit path is seeded end-to-end. The sim MockHypervisor (which decides exploit success in sim mode) is reseeded on every reset; earlier it was seeded once at construction and leaked outcomes across episodes.

Golden trajectory

A committed golden fingerprint (tests/parity/test_golden_trajectory.py) hashes rewards/terminations/truncations/step-count for a fixed seed. If a change alters environment dynamics, this test fails and the fingerprint must be updated deliberately with a changelog entry — so dynamics never drift silently.

Caveats

  • Vectorized Python envs: reproducibility holds per process. Running many envs in one process is supported (they are mutually independent), but if you use OS-level process forking, seed each worker explicitly.
  • log_latency: enabling telemetry delay changes the observation stream (by design). It is deterministic under seed, but a run with log_latency=4 is not comparable to one with log_latency=0. The default is 0.
  • JAX backend: the vectorized backend is a separate code path with its own PRNG keys; it is reproducible under a fixed jax.random.PRNGKey and has a NumPy reference-parity test.