Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions unit_test/include/portable_test_random.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Copyright (c) 2026 Dan McLeran
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

#ifndef TINYMIND_PORTABLE_TEST_RANDOM_HPP
#define TINYMIND_PORTABLE_TEST_RANDOM_HPP

#include <cstdint>

// Test-only. Not part of the shipped library.
//
// Why this exists
// ---------------
// Several suites initialize network weights from a seeded RNG and then assert
// on what training converges to. That is only reproducible if the sequence is
// reproducible -- and std::uniform_real_distribution is NOT. The standard
// specifies the distribution, not the mapping from engine output to value, so
// libstdc++ and libc++ return different doubles from identical engine state.
// std::default_random_engine is likewise a typedef the implementation picks.
//
// The consequence was real: the nightly MSan job, which links an instrumented
// libc++, saw unit_test/nn fail two tolerance checks that pass under libstdc++
// -- not a defect, just a different draw sequence.
//
// Why it reproduces libstdc++ rather than picking something cleaner
// ----------------------------------------------------------------
// The obvious fix -- switch to std::mt19937, whose output IS specified, and do
// the scaling by hand -- was tried first and rejected on evidence. It is a
// perfectly good generator: statistically indistinguishable from the current
// one (mean ~0, mean|x| ~0.5 over 2000 draws). But it produces a different
// sequence, and two tests do not survive that:
//
// test_case_lstm_weight_serialization 0.605 against a 0.02 bound
// test_case_rmsprop_fixedpoint_xor average error 9 against a bound of 4
//
// Those tolerances are calibrated against the draws the tests have always seen.
// Loosening them to accommodate a new generator would weaken two real gates to
// settle a question neither is asking about.
//
// Making them robust to any initialization is the deeper fix and a much larger
// change: it means deciding what each of those tolerances should assert, which
// risks masking genuine regressions if done in bulk. Freezing the sequence the
// tests were written against gets the portability without touching a single
// tolerance, and leaves that larger cleanup as separate work.
//
// What is frozen
// --------------
// libstdc++'s std::default_random_engine is minstd_rand0: x = 16807x mod
// (2^31 - 1). Its uniform_real_distribution<double> goes through
// generate_canonical<double, 53>, which for this engine consumes exactly two
// draws (b = 53 bits, log2(r) = 30, so k = ceil(53/30) = 2) and forms
// (d1 + d2*r) / r^2 before scaling into [low, high).
//
// Verified bit-identical to libstdc++'s output over 20,000 draws: 0 mismatches,
// max difference 0. test_case_portable_uniform_real_matches_frozen_sequence in
// nn_unit_test.cpp locks the first draws in as an assertion, so a future edit
// cannot quietly change the sequence and silently re-tune every training test
// that depends on it.
class PortableUniformReal
{
public:
PortableUniformReal(const double low, const double high, const uint32_t s)
: mState(s != 0u ? s : 1u), mLow(low), mHigh(high)
{
}

double operator()()
{
// r = engine.max() - engine.min() + 1, i.e. (2^31 - 2) - 1 + 1.
const double r = 2147483646.0;
const double d1 = static_cast<double>(next() - 1u);
const double d2 = static_cast<double>(next() - 1u);
const double sum = d1 + (d2 * r);
const double canonical = sum / (r * r);

return (canonical * (mHigh - mLow)) + mLow;
}

void seed(const uint32_t s)
{
mState = (s != 0u) ? s : 1u;
}

private:
uint32_t next()
{
mState = static_cast<uint32_t>((16807ULL * static_cast<uint64_t>(mState)) % 2147483647ULL);

return mState;
}

uint32_t mState;
double mLow;
double mHigh;
};

// Not covered here
// ----------------
// unit_test/qlearn still uses std::default_random_engine with
// std::uniform_int_distribution to pick maze states and actions. That mapping
// is implementation-defined too, so the sequence differs across libraries in
// principle. It is left alone deliberately: the suite passes under the
// instrumented libc++ in the nightly MSan job, so there is no observed problem
// to fix, and changing the draw sequence of a passing suite risks breaking its
// assertions for no benefit. Worth revisiting only if it actually diverges.

#endif // TINYMIND_PORTABLE_TEST_RANDOM_HPP
2 changes: 1 addition & 1 deletion unit_test/kan/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ CC=g++
WARN=-Wall -Wextra -Werror -Wpedantic
SOURCES=kan_unit_test.cpp ../../cpp/lookupTables.cpp
DEFINES=-DTINYMIND_ENABLE_OSTREAMS=1 -DTINYMIND_ENABLE_FLOAT=1 -DTINYMIND_ENABLE_STD=1 -DTINYMIND_ENABLE_HOSTED_RAND=1 -DTINYMIND_USE_SIGMOID_8_8=1
INCLUDES=-I ../../include -I../../cpp -I../../cpp/include -I../../include -I${BOOST_HOME}
INCLUDES=-I../include -I ../../include -I../../cpp -I../../cpp/include -I../../include -I${BOOST_HOME}
OUT=./output/kan_unit_test
kan_unit_test:
$(MKDIR)
Expand Down
15 changes: 9 additions & 6 deletions unit_test/kan/kan_unit_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ TINYMIND_DISABLE_WARNING_POP
#include <numeric>
#include <random>

#include "portable_test_random.hpp"
#include "qformat.hpp"
#include "bspline.hpp"
#include "kan.hpp"
Expand Down Expand Up @@ -384,9 +385,10 @@ struct DoubleRandomNumberGenerator
{
static double generateRandomWeight()
{
static std::default_random_engine generator(RANDOM_SEED);
static std::uniform_real_distribution<double> distribution(-0.5, 0.5);
return distribution(generator);
// Implementation-independent sequence -- see
// unit_test/include/portable_test_random.hpp.
static PortableUniformReal generator(-0.5, 0.5, RANDOM_SEED);
return generator();
}
};

Expand Down Expand Up @@ -575,9 +577,10 @@ struct SinusoidRandomNumberGenerator
{
static double generateRandomWeight()
{
static std::default_random_engine generator(RANDOM_SEED);
static std::uniform_real_distribution<double> distribution(-0.1, 0.1);
return distribution(generator);
// Implementation-independent sequence -- see
// unit_test/include/portable_test_random.hpp.
static PortableUniformReal generator(-0.1, 0.1, RANDOM_SEED);
return generator();
}
};

Expand Down
2 changes: 1 addition & 1 deletion unit_test/nn/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ CC=g++
WARN=-Wall -Wextra -Werror -Wpedantic
SOURCES=nn_unit_test.cpp ../../cpp/lookupTables.cpp
DEFINES=-DTINYMIND_ENABLE_OSTREAMS=1 -DTINYMIND_ENABLE_FLOAT=1 -DTINYMIND_ENABLE_STD=1 -DTINYMIND_ENABLE_HOSTED_IO=1 -DTINYMIND_ENABLE_HOSTED_RAND=1 -DTINYMIND_USE_SIGMOID_8_8=1 -DTINYMIND_USE_SIGMOID_16_16=1 -DTINYMIND_USE_LOG_16_16=1 -DTINYMIND_USE_TANH_8_8=1 -DTINYMIND_USE_TANH_8_24=1 -DTINYMIND_USE_TANH_16_16=1 -DTINYMIND_USE_EXP_16_16=1 -DTINYMIND_USE_EXP_8_8=1
INCLUDES=-I ../../include -I../../cpp -I../../cpp/include -I../../include -I${BOOST_HOME}
INCLUDES=-I../include -I ../../include -I../../cpp -I../../cpp/include -I../../include -I${BOOST_HOME}
OUT=./output/nn_unit_test
nn_unit_test:
$(MKDIR)
Expand Down
Loading
Loading