A fingerprint is a short, content-addressed identifier for a NeuronScope object. Two fingerprints are equal if and only if the two objects would produce byte-identical recorded artifacts under the same runtime contract. Every model, dataset, prompt, environment, and experiment carries a fingerprint, and every artifact stores the fingerprints of its inputs.
Why fingerprints exist
Interpretability workflows re-run the same computation many times: across seeds, across commits, across machines, across weeks. The single most common failure mode is silent drift — the input looks identical but is not, and the resulting comparison is meaningless. Fingerprints turn "looks identical" into a checkable property.
- Comparability. A layer-wise diff between two experiments is only defined when their model, dataset, and prompt fingerprints match.
- Caching. The recorder skips work whose inputs are already fingerprinted into a cached artifact.
- Provenance. Every
.nszartifact carries the fingerprints of its inputs, so a downstream reader can verify what was recorded without re-executing the original code.
The shape of a fingerprint
A fingerprint is a 32-byte BLAKE3 digest, rendered as a 64-character lowercase hex string with a two-character namespace prefix. The namespace tells the reader which canonicalizer produced the digest.
fp:model:b3c2f0a1e8d7a2c6b91af4d0e5c7a1b0f3d2c9e8a1b4c5d6e7f8091a2b3c4d5eComputing a fingerprint
Every fingerprintable object exposes a .fingerprint() method that returns the string above. The library also exposes a top-level helper for the common case of fingerprinting an arbitrary Python object that implements the protocol.
import neuronscope as ns
from transformers import AutoModel
model = AutoModel.from_pretrained("google-bert/bert-base-uncased")
fp_model = ns.fingerprint(model)
fp_prompt = ns.fingerprint(ns.Prompt("The quick brown fox"))
fp_env = ns.fingerprint(ns.Environment.current())
print(fp_model)
# fp:model:b3c2f0a1e8d7a2c6b91af4d0e5c7a1b0f3d2c9e8a1b4c5d6e7f8091a2b3c4d5eCanonicalization rules
Every namespace has its own canonicalizer. The rules are deliberately narrow so that unrelated changes to a model or dataset do not change its fingerprint.
Model canonicalization
- Parameter tensors are hashed in parameter-name order, not module traversal order.
- Tensor bytes are hashed in row-major, native-dtype form after a fixed endianness cast.
- Buffer tensors (running means, RNG state) are included. Optimizer state is not.
- Non-persistent modules and hooks are excluded.
Dataset canonicalization
- Row order is normalised via a stable sort on the row's canonical bytes.
- Streaming datasets are fingerprinted over their schema and generator identity, not their materialised rows — see the ADR for the trade-off.
Environment canonicalization
- Only fields that materially affect numeric output are hashed: Python version, framework versions, BLAS provider, CUDA/ROCm version, and hardware family.
- The wall-clock, hostname, and user are not in the digest.
Stability contract
Comparing fingerprints
Fingerprints are opaque strings; compare them with ==. Higher-level helpers exist for the common case of asserting a match at the top of a script.
import neuronscope as ns
ns.assert_matches(model_a, model_b, kind="model")
# raises FingerprintMismatch with a diff summary when they differPitfalls
- In-place mutation. If you fingerprint a model, then call
model.eval(), then fingerprint it again, the digest may change ifeval()flips a persistent buffer. - Non-deterministic dataset order. A dataset built from a Python
setwill have a different fingerprint on every process. Use an ordered container. - Framework upgrades. Upgrading PyTorch across a minor version can change the environment fingerprint even if your code is identical. This is intentional — the numeric results may differ.