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
infodicts - 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 insimmode) is reseeded on everyreset; 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 withlog_latency=4is not comparable to one withlog_latency=0. The default is0.- JAX backend: the vectorized backend is a separate code path with its own PRNG keys; it is
reproducible under a fixed
jax.random.PRNGKeyand has a NumPy reference-parity test.