Safety-Critical AI

Bit-Perfect Reproducibility: Why It Matters and How to Prove It

What deterministic execution actually means and how to verify it across platforms

Published
January 15, 2026 20:15
Reading Time
11 min
Same input producing identical byte-for-byte output across x86, ARM, and RISC-V platforms

“Reproducible” means different things in different contexts. In research, it often means “similar results within experimental tolerance.” In most production systems, it means “the same classification most of the time.” In safety-critical systems, it can mean something far more stringent: identical output, byte for byte, on every execution.

This article examines what bit-perfect reproducibility actually requires, why it matters for certification, and how to design systems that achieve it. The techniques apply broadly, but the examples focus on neural network inference where reproducibility failures are particularly common and consequential.

Levels of Reproducibility

It helps to distinguish four levels of reproducibility, each stricter than the last:

Statistical reproducibility. Results fall within expected variance. Two training runs produce models with similar accuracy. This is the standard for research.

Classification reproducibility. The same input produces the same classification. The confidence scores may vary, but the final decision is stable. This suffices for many production systems.

Numerical reproducibility. The same input produces outputs that match within a tolerance (e.g., relative error < 10^-6). Intermediate calculations may differ, but final results are “close enough.”

Bit-perfect reproducibility. The same input produces byte-identical output. Every bit matches. There is no tolerance, no “close enough.” Either the outputs are identical or they are not.

Safety-critical certification increasingly demands the fourth level. When investigating an incident, “the system probably would have made the same decision” is less useful than “here is the exact computation, reproducible on demand.”

Why Bits Matter

Bit-perfect reproducibility enables capabilities that approximate reproducibility cannot provide.

Replay and Audit

If execution is bit-perfect, recorded inputs can reproduce exact behaviour indefinitely. An incident investigation can replay the precise sequence of computations that led to a decision. The replay is not a simulation or approximation; it is the same computation.

Without bit-perfect reproducibility, replay may diverge from original execution. Small numerical differences accumulate. The replayed system may make different decisions than the original, undermining the investigation’s validity.

Cryptographic Verification

Bit-identical outputs can be cryptographically hashed and compared:

// Compute output and its hash
inference(input, output);
uint8_t hash[32];
sha256(output, output_size, hash);

// Later verification
inference(input, output_verify);
uint8_t hash_verify[32];
sha256(output_verify, output_size, hash_verify);

// Exact match or failure - no tolerance
assert(memcmp(hash, hash_verify, 32) == 0);

If outputs match bit-for-bit, hashes match. If any bit differs, hashes differ completely. This enables tamper-evident logging, distributed verification, and cryptographic audit trails.

Certification Evidence

Certification standards require evidence that software behaves as specified. Bit-perfect reproducibility means test results from development transfer exactly to production. A test that passes on the verification platform passes on the deployment target, not because behaviour is “similar,” but because it is identical.

This simplifies the certification argument. Instead of demonstrating that platform differences are acceptably small, the argument becomes: there are no platform differences.

Design Property: Transportable Verification

If a system produces bit-identical outputs across platforms, verification evidence from one platform applies to all platforms. Testing on x86 provides evidence for ARM deployment without re-verification.

Sources of Non-Reproducibility

Achieving bit-perfect reproducibility requires understanding where differences originate. Most stem from a small number of sources.

Floating-Point Arithmetic

IEEE 754 floating-point permits variation in several areas:

Intermediate precision. x87 FPU uses 80-bit intermediates; ARM uses 32-bit or 64-bit. The same expression produces different rounding.

Operation ordering. Floating-point addition is not associative. (a + b) + c may differ from a + (b + c). Compiler optimisations that reorder operations change results.

Fused operations. FMA (fused multiply-add) computes a * b + c with one rounding instead of two. Whether FMA is used depends on compiler flags, target architecture, and optimisation level.

Transcendental functions. sin(), exp(), log() are not specified bit-exactly. Different math libraries use different approximations.

The solution explored in Fixed-Point Neural Networks is to avoid floating-point entirely for computation, using fixed-point integer arithmetic that behaves identically on all platforms.

Hash Table Iteration

Many languages implement hash tables with iteration order that depends on memory layout or randomised hash functions:

# Python dict iteration order can vary between runs
# (though CPython 3.7+ preserves insertion order)
for key in my_dict:
    process(key)  # Order may be non-deterministic

In C, pointer-based hash tables often iterate in pointer order, which varies with memory allocation:

// Dangerous: iteration order depends on pointer values
for (entry = table->first; entry; entry = entry->next) {
    process(entry);  // Order is non-deterministic
}

The solution is deterministic data structures that iterate in a defined order (insertion order, sorted order, or explicitly specified order).

Threading and Parallelism

Concurrent execution introduces non-determinism unless carefully controlled:

Thread scheduling. The OS decides when threads run. Two threads racing to update shared state may interleave differently on each execution.

Parallel reduction. Summing an array in parallel typically splits the work across threads. The order of partial sums varies, and with floating-point arithmetic, so does the result.

Lock acquisition order. When multiple threads contend for locks, acquisition order is non-deterministic.

For bit-perfect reproducibility, either avoid parallelism or use deterministic parallel patterns with explicit synchronisation that guarantees the same interleaving on every execution.

Memory Allocation

Dynamic allocation can introduce non-determinism through:

Address-dependent behaviour. Code that accidentally depends on pointer values (e.g., using pointers as hash keys) behaves differently when allocation returns different addresses.

Allocation order. Some allocators return memory in different orders depending on heap state, affecting programs that iterate over allocated objects by address.

Static allocation, as discussed in The Real Cost of Dynamic Memory, eliminates this source of variation.

System Calls

Calls that return environmental information introduce external non-determinism:

time_t t = time(NULL);       // Different every second
int r = rand();              // Different every call (unless seeded)
pid_t p = getpid();          // Different every process

Bit-perfect reproducibility requires either avoiding these calls or providing deterministic alternatives (e.g., injecting time as an input parameter rather than reading the system clock).

Designing for Determinism

Achieving bit-perfect reproducibility is an architectural decision, not a debugging task. It requires choosing deterministic alternatives for each potential source of variation.

Pure Functions

The foundation is pure functions: outputs depend only on inputs, with no side effects or external state access.

// Pure: output depends only on inputs
int add(int a, int b) {
    return a + b;
}

// Impure: output depends on external state
int add_with_timestamp(int a, int b) {
    return a + b + time(NULL);  // Non-deterministic
}

Pure functions compose deterministically. A pipeline of pure functions is itself pure. Testing pure functions is straightforward: supply inputs, check outputs.

Explicit State

Where state is necessary, make it explicit and contained:

typedef struct {
    fixed_t weights[MAX_WEIGHTS];
    fixed_t biases[MAX_BIASES];
    uint32_t rng_state;  // Explicit RNG state, not global
} model_state_t;

void infer(const model_state_t* state, 
           const fixed_t* input, 
           fixed_t* output) {
    // All state is explicit in parameters
    // No global variables, no system calls
}

Explicit state enables reproducibility by controlling all inputs to the computation.

Deterministic Algorithms

Some algorithms are inherently non-deterministic; others have deterministic and non-deterministic variants. Choose deliberately:

OperationNon-deterministicDeterministic
Sorting equal elementsQuicksort (unstable)Mergesort (stable)
Hash table iterationAddress orderInsertion or key order
Parallel reductionThread-arrival orderFixed tree reduction
Random samplingSystem RNGSeeded PRNG

The deterministic variant may be slower or use more memory. For safety-critical systems, the trade-off usually favours determinism.

Fixed-Point Arithmetic

As detailed in Fixed-Point Neural Networks, integer arithmetic is deterministic across platforms:

// Non-deterministic: floating-point
float result = a * b + c;  // FMA? Intermediate precision?

// Deterministic: fixed-point
int64_t product = (int64_t)a * (int64_t)b;
int32_t result = (int32_t)((product >> 16) + c);

The integer operations produce identical results on x86, ARM, RISC-V, and any other architecture with standard integer arithmetic.

Verifying Determinism

Design enables determinism; testing verifies it. Verification requires running the same computation on multiple platforms and comparing results bit-for-bit.

Cross-Platform Testing

The gold standard is identical binaries (where possible) or identical source compiled for each target:

# Compile for each platform
make ARCH=x86_64
make ARCH=aarch64
make ARCH=riscv64

# Run identical tests
./test_x86_64 > output_x86.bin
./test_aarch64 > output_arm.bin
./test_riscv64 > output_riscv.bin

# Compare byte-for-byte
diff output_x86.bin output_arm.bin
diff output_x86.bin output_riscv.bin

Any difference indicates a determinism failure. The test should produce no output if all platforms match.

Hash-Based Verification

For large outputs, compare hashes rather than raw bytes:

void test_determinism(void) {
    fixed_t input[INPUT_SIZE];
    fixed_t output[OUTPUT_SIZE];
    
    // Known test input
    load_test_vector(input);
    
    // Run inference
    inference(input, output);
    
    // Compute hash
    uint8_t hash[32];
    sha256(output, sizeof(output), hash);
    
    // Compare to expected hash (computed once, verified across platforms)
    const uint8_t expected[32] = { 0x3a, 0x7f, ... };
    assert(memcmp(hash, expected, 32) == 0);
}

If the hash matches the expected value, the output is bit-identical. If it differs, something has changed.

Regression Testing

Determinism tests should run on every build to catch regressions:

# CI configuration
test_determinism:
  matrix:
    platform: [x86_64, aarch64, riscv64]
  steps:
    - compile: make ARCH=$platform
    - run: ./determinism_tests
    - verify: diff output.bin golden/output.bin

Golden outputs are generated once and stored in version control. Any change to the output indicates either a bug or an intentional change that requires updating the golden files.

Fuzzing for Determinism

Random input testing can find edge cases where determinism breaks:

void fuzz_determinism(int iterations) {
    for (int i = 0; i < iterations; i++) {
        // Generate random input (seeded for reproducibility)
        fixed_t input[INPUT_SIZE];
        generate_random_input(input, seed + i);
        
        // Run twice
        fixed_t output1[OUTPUT_SIZE];
        fixed_t output2[OUTPUT_SIZE];
        inference(input, output1);
        inference(input, output2);
        
        // Must match exactly
        assert(memcmp(output1, output2, sizeof(output1)) == 0);
    }
}

This catches cases where internal state leaks between invocations or where certain input patterns trigger non-deterministic code paths.

Common Pitfalls

Even systems designed for determinism can fail through subtle issues.

Uninitialised Memory

Reading uninitialised memory produces undefined values:

fixed_t buffer[SIZE];
// buffer contains garbage - uninitialised
process(buffer);  // Non-deterministic!

Always initialise memory explicitly:

fixed_t buffer[SIZE] = {0};  // Zero-initialised
// or
memset(buffer, 0, sizeof(buffer));

Padding Bytes

Struct padding may contain garbage:

typedef struct {
    int8_t a;
    // 3 bytes padding
    int32_t b;
} padded_t;

padded_t x;
x.a = 1;
x.b = 2;
// Padding bytes are uninitialised

hash(&x, sizeof(x));  // Hash includes garbage padding!

Zero the entire struct before use, or hash only the meaningful fields.

Compiler Optimisations

Aggressive optimisation can reorder floating-point operations:

// Source code
float sum = a + b + c + d;

// Compiler may emit
float sum = (a + b) + (c + d);  // Different rounding!

For floating-point code, use -ffp-contract=off and similar flags to disable operation fusion. For fixed-point code, this is not a concern since integer operations are not reordered in ways that change results.

Library Differences

Even deterministic code can produce different results if linked against different libraries:

// If libc differs between platforms, results may differ
qsort(array, n, sizeof(int), compare);  // Stable? Ordering of equal elements?

Either use libraries with guaranteed behaviour, or implement critical functions directly.

Practical Considerations

Performance Trade-offs

Deterministic alternatives are sometimes slower:

PatternNon-deterministicDeterministicOverhead
Parallel sumThread-order reductionFixed tree~10-20%
Hash iterationPointer orderSorted orderO(n log n)
Memory layoutAllocator-dependentStatic/fixedMinimal
Floating-pointHardware FMASoftware emulation2-5x

For safety-critical systems, the overhead is typically acceptable. Correctness and auditability outweigh raw performance.

Scope of Determinism

Not everything needs to be deterministic. A system can have:

  • Deterministic inference (same input → same output)
  • Non-deterministic logging (timestamps, which don’t affect computation)
  • Non-deterministic scheduling (order of independent operations)

The key is that non-determinism must not affect the auditable computation path.

Versioning

Bit-perfect reproducibility is relative to a specific version. Changing the code changes the outputs. Version management must track:

  • Source code version
  • Compiler version
  • Library versions
  • Target architecture

Reproducibility claims are valid only within a specific configuration. Changing any component may change outputs.

Implementation Reference

The certifiable-inference project demonstrates bit-perfect reproducibility:

  • Fixed-point arithmetic throughout
  • No floating-point in computation paths
  • Static allocation only
  • Deterministic algorithms
  • Cross-platform verification tests

The test suite includes golden outputs verified across x86, ARM, and RISC-V. Any platform difference causes test failure.

A live simulator demonstrates the deterministic inference pipeline interactively.

Conclusion

Bit-perfect reproducibility is achievable but requires deliberate design. The primary sources of non-determinism—floating-point arithmetic, hash table iteration, threading, memory allocation, and system calls—each have deterministic alternatives.

The cost is reduced flexibility and sometimes reduced performance. The benefit is absolute certainty: the same input produces the same output, every time, on every platform. This enables cryptographic verification, exact replay, and transportable certification evidence.

For safety-critical systems, where incident investigation may require reproducing exact behaviour months or years later, bit-perfect reproducibility is not merely convenient—it may be essential.

As with any architectural approach, suitability depends on system requirements and verification objectives. Not all systems need bit-perfect reproducibility. But for those that do, achieving it is a matter of engineering discipline, not luck.


For a working implementation demonstrating bit-perfect reproducibility across platforms, see certifiable-inference or try the live simulator.

About the Author

William Murray is a Regenerative Systems Architect with 30 years of UNIX infrastructure experience, specializing in deterministic computing for safety-critical systems. Based in the Scottish Highlands, he operates SpeyTech and maintains several open-source projects including C-Sentinel and c-from-scratch.

Discuss This Perspective

For technical discussions or acquisition inquiries, contact SpeyTech directly.

Get in touch
← Back to Insights