From 105505a0f3b6da7c6ed59d9e09858b22e87103da Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 13:48:57 +0000 Subject: [PATCH 1/5] Add RandomUniform-28 with deterministic generator attribute RandomUniform is non-deterministic, which makes its output unverifiable in backend node tests (the operator had no node tests at all). This adds an optional string attribute 'generator' (default "default") that can select a fully specified pseudo-random number generator, making the operator optionally deterministic and therefore testable. - "default": implementation-defined generator, previous behavior - "mersenne_twister": standard 32-bit MT19937 seeded with init_genrand (the std::mt19937 seeding) from the mandatory 'seed' attribute, doubles drawn with the genrand_res53 method in row-major order, scaled to [low, high) in double precision, then cast to dtype The value list is extensible: more algorithms can be added in future opset versions. - defs.cc: RandomUniform-28 schema with generator attribute and attribute validation in the inference function - old.cc: RandomUniform-22 schema preserved - operator_sets.h: register RandomUniform in OpSet_Onnx_ver28 - convert.h: 27->28 CompatibleAdapter; 28->27 adapter that drops generator="default" and rejects deterministic generators - reference implementation: _MT19937 class (verified against the canonical MT19937 test vector and std::mt19937) wired into RandomUniform - node test cases with exact expected outputs plus schema, shape inference, version converter and reference evaluator tests Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Claude --- onnx/backend/test/case/node/randomuniform.py | 104 ++++++++++++++++++ onnx/defs/doc_strings.cc | 32 ++++++ onnx/defs/doc_strings.h | 1 + onnx/defs/generator/defs.cc | 27 ++++- onnx/defs/generator/old.cc | 26 +++++ onnx/defs/operator_sets.h | 2 + onnx/reference/ops/_op_common_random.py | 74 +++++++++++++ onnx/reference/ops/op_random_uniform.py | 6 +- onnx/test/reference_evaluator_test.py | 75 +++++++++++++ onnx/test/schema_test.py | 12 ++ onnx/test/shape_inference_test.py | 53 +++++++++ onnx/test/version_converter_test.py | 33 ++++++ .../version_converter/adapters/CMakeLists.txt | 1 + .../adapters/random_uniform_28_27.h | 41 +++++++ onnx/version_converter/convert.h | 4 + 15 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 onnx/backend/test/case/node/randomuniform.py create mode 100644 onnx/version_converter/adapters/random_uniform_28_27.h diff --git a/onnx/backend/test/case/node/randomuniform.py b/onnx/backend/test/case/node/randomuniform.py new file mode 100644 index 00000000000..73e2c154883 --- /dev/null +++ b/onnx/backend/test/case/node/randomuniform.py @@ -0,0 +1,104 @@ +# Copyright (c) ONNX Project Contributors +# +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +import numpy as np + +import onnx +from onnx.backend.test.case.base import Base +from onnx.backend.test.case.node import expect + + +def mersenne_twister_uniform(seed, shape, low=0.0, high=1.0): + """Reference values for RandomUniform with generator="mersenne_twister". + + The operator specifies MT19937 seeded with ``init_genrand`` and doubles + drawn with the 53-bit ``genrand_res53`` method. For scalar seeds below + 2**32 numpy's legacy ``RandomState`` implements exactly this algorithm, + so it can serve as an independent reference here. + """ + state = np.random.RandomState(int(seed) & 0xFFFFFFFF) + return low + state.random_sample(shape) * (high - low) + + +class RandomUniform(Base): + @staticmethod + def export_randomuniform_mersenne_twister() -> None: + node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + shape=[3, 4], + seed=42.0, + generator="mersenne_twister", + ) + + y = mersenne_twister_uniform(42, (3, 4)).astype(np.float32) + expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister", + ) + + @staticmethod + def export_randomuniform_mersenne_twister_low_high() -> None: + node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + low=5.0, + high=10.0, + shape=[2, 3], + seed=0.0, + generator="mersenne_twister", + ) + + y = mersenne_twister_uniform(0, (2, 3), low=5.0, high=10.0).astype(np.float32) + expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_low_high", + ) + + @staticmethod + def export_randomuniform_mersenne_twister_double() -> None: + node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + dtype=onnx.TensorProto.DOUBLE, + shape=[2, 4], + seed=123.0, + generator="mersenne_twister", + ) + + y = mersenne_twister_uniform(123, (2, 4)) + expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_double", + ) + + @staticmethod + def export_randomuniform_mersenne_twister_float16() -> None: + node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + dtype=onnx.TensorProto.FLOAT16, + shape=[10], + seed=7.0, + generator="mersenne_twister", + ) + + y = mersenne_twister_uniform(7, (10,)).astype(np.float16) + expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_float16", + ) diff --git a/onnx/defs/doc_strings.cc b/onnx/defs/doc_strings.cc index abab3015f08..8477622e9ac 100644 --- a/onnx/defs/doc_strings.cc +++ b/onnx/defs/doc_strings.cc @@ -154,6 +154,37 @@ be one of the data types specified in the 'DataType' enum field in the TensorProto message. )DOC"; +const char kDoc_RandomUniform_ver28[] = R"DOC( +Generate a tensor with random values drawn from a uniform distribution. The shape +of the tensor is specified by the `shape` argument and the range by `low` and `high`. + +The data type is specified by the 'dtype' argument. The 'dtype' argument must +be one of the data types specified in the 'DataType' enum field in the +TensorProto message. + +The `generator` attribute selects the pseudo-random number generator algorithm. +With the default value "default", the choice of generator is left to the +implementation and results are generally not reproducible across implementations, +even when `seed` is specified. Setting `generator` to "mersenne_twister" fully +specifies the generated values: given the same `seed`, every conforming +implementation must produce bit-identical results, which makes the operator +deterministic and testable. More algorithms may be added in future opset versions. + +When `generator` is "mersenne_twister", the `seed` attribute must be specified +and the output is computed as follows: +1. Initialize a standard 32-bit Mersenne Twister (MT19937) state using the + `init_genrand` seeding routine from the reference implementation of Matsumoto + and Nishimura (the seeding also used by C++ `std::mt19937`), with the seed + value obtained by truncating `seed` toward zero and converting it to an + unsigned 32-bit integer (modulo 2^32). +2. For each output element, in row-major order, draw two consecutive 32-bit + outputs `a` and `b` from the generator and form the double-precision value + `r = (floor(a / 2^5) * 2^26 + floor(b / 2^6)) / 2^53` (the `genrand_res53` + method), which lies in the interval [0, 1). +3. The element value is `low + r * (high - low)`, computed in double precision + and then cast to `dtype`. +)DOC"; + const char kDoc_DequantizeLinear_ver24[] = R"DOC( The linear dequantization operator. It consumes a quantized tensor, a scale, and a zero point to compute the full-precision tensor. The dequantization formula is `y = (x - x_zero_point) * x_scale`. `x_scale` and `x_zero_point` @@ -1318,6 +1349,7 @@ const char kDoc_Squeeze_ver24[] = ""; const char kDoc_MaxUnpool_ver11[] = ""; const char kDoc_Size_ver24[] = ""; const char kDoc_RandomUniform_ver1[] = ""; +const char kDoc_RandomUniform_ver28[] = ""; const char kDoc_Range_ver11[] = ""; const char kDoc_Range_ver27[] = ""; const char kDoc_DequantizeLinear_ver24[] = ""; diff --git a/onnx/defs/doc_strings.h b/onnx/defs/doc_strings.h index 5ebb7875924..ab3d5d7cc87 100644 --- a/onnx/defs/doc_strings.h +++ b/onnx/defs/doc_strings.h @@ -54,6 +54,7 @@ extern const char kDoc_PRelu_ver7[]; extern const char kDoc_RandomNormal_ver1[]; extern const char kDoc_RandomNormalLike_ver1[]; extern const char kDoc_RandomUniform_ver1[]; +extern const char kDoc_RandomUniform_ver28[]; extern const char kDoc_Range_ver11[]; extern const char kDoc_Range_ver27[]; extern const char kDoc_RandomUniformLike_ver1[]; diff --git a/onnx/defs/generator/defs.cc b/onnx/defs/generator/defs.cc index 4272b7b8f8c..0c43095070c 100644 --- a/onnx/defs/generator/defs.cc +++ b/onnx/defs/generator/defs.cc @@ -150,16 +150,26 @@ ONNX_OPERATOR_SET_SCHEMA( ONNX_OPERATOR_SET_SCHEMA( RandomUniform, - 22, + 28, OpSchema() - .SetDoc(kDoc_RandomUniform_ver1) + .SetDoc(kDoc_RandomUniform_ver28) .Attr("low", "Lower boundary of the output values.", AttributeProto::FLOAT, 0.0f) .Attr("high", "Upper boundary of the output values.", AttributeProto::FLOAT, 1.0f) .Attr( "seed", - "(Optional) Seed to the random generator, if not specified we will auto generate one.", + "(Optional) Seed to the random generator, if not specified we will auto generate one. " + "Must be specified when `generator` is \"mersenne_twister\".", AttributeProto::FLOAT, OPTIONAL_VALUE) + .Attr( + "generator", + "(Optional) The pseudo-random number generator algorithm. \"default\" leaves the choice of " + "generator to the implementation; results are then not reproducible across implementations, " + "even when `seed` is specified. \"mersenne_twister\" selects the fully specified MT19937 " + "algorithm described in the operator documentation, making the output deterministic for a " + "given `seed`. More algorithms may be added in future opset versions.", + AttributeProto::STRING, + std::string("default")) .Attr( "dtype", "The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.", @@ -170,6 +180,17 @@ ONNX_OPERATOR_SET_SCHEMA( .TypeConstraint("T", OpSchema::all_float_types_ir4(), "Constrain output types to float tensors.") .SetNodeDeterminism(OpSchema::NodeDeterminism::NonDeterministic) .TypeAndShapeInferenceFunction([](InferenceContext& ctx) { + const auto* generator_attr = ctx.getAttribute("generator"); + if (generator_attr != nullptr) { + const std::string& generator = generator_attr->s(); + if (generator != "default" && generator != "mersenne_twister") { + fail_shape_inference( + "Attribute 'generator' must be one of 'default' or 'mersenne_twister', got '", generator, "'."); + } + if (generator != "default" && ctx.getAttribute("seed") == nullptr) { + fail_shape_inference("Attribute 'seed' must be specified when 'generator' is '", generator, "'."); + } + } propagateElemTypeFromAttributeToOutput(ctx, "dtype", 0, TensorProto::FLOAT); propagateShapeFromAttributeToOutput(ctx, "shape", 0); })); diff --git a/onnx/defs/generator/old.cc b/onnx/defs/generator/old.cc index 0d2c356425f..6e50f0d64e2 100644 --- a/onnx/defs/generator/old.cc +++ b/onnx/defs/generator/old.cc @@ -229,6 +229,32 @@ ONNX_OPERATOR_SET_SCHEMA( propagateShapeFromAttributeToOutput(ctx, "shape", 0); })); +ONNX_OPERATOR_SET_SCHEMA( + RandomUniform, + 22, + OpSchema() + .SetDoc(kDoc_RandomUniform_ver1) + .Attr("low", "Lower boundary of the output values.", AttributeProto::FLOAT, 0.0f) + .Attr("high", "Upper boundary of the output values.", AttributeProto::FLOAT, 1.0f) + .Attr( + "seed", + "(Optional) Seed to the random generator, if not specified we will auto generate one.", + AttributeProto::FLOAT, + OPTIONAL_VALUE) + .Attr( + "dtype", + "The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.", + AttributeProto::INT, + static_cast(TensorProto::FLOAT)) + .Attr("shape", "The shape of the output tensor.", AttributeProto::INTS) + .Output(0, "output", "Output tensor of random values drawn from uniform distribution", "T") + .TypeConstraint("T", OpSchema::all_float_types_ir4(), "Constrain output types to float tensors.") + .SetNodeDeterminism(OpSchema::NodeDeterminism::NonDeterministic) + .TypeAndShapeInferenceFunction([](InferenceContext& ctx) { + propagateElemTypeFromAttributeToOutput(ctx, "dtype", 0, TensorProto::FLOAT); + propagateShapeFromAttributeToOutput(ctx, "shape", 0); + })); + ONNX_OPERATOR_SET_SCHEMA( RandomUniform, 1, diff --git a/onnx/defs/operator_sets.h b/onnx/defs/operator_sets.h index de677584826..d429656b408 100644 --- a/onnx/defs/operator_sets.h +++ b/onnx/defs/operator_sets.h @@ -1471,12 +1471,14 @@ class OpSet_Onnx_ver27 { // Forward declarations for ai.onnx version 28 class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Onnx, 28, Celu); +class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Onnx, 28, RandomUniform); // Iterate over schema from ai.onnx version 28 class OpSet_Onnx_ver28 { public: static void ForEachSchema(const std::function& fn) { fn(GetOpSchema()); + fn(GetOpSchema()); } }; diff --git a/onnx/reference/ops/_op_common_random.py b/onnx/reference/ops/_op_common_random.py index c7b011e2929..37c2be14068 100644 --- a/onnx/reference/ops/_op_common_random.py +++ b/onnx/reference/ops/_op_common_random.py @@ -9,6 +9,59 @@ from onnx.reference.op_run import OpRun +class _MT19937: + """Standard 32-bit Mersenne Twister (MT19937). + + Implements the ``init_genrand`` seeding routine and the ``genrand_res53`` + double generation method from the reference implementation of Matsumoto + and Nishimura (mt19937ar.c). The 32-bit output stream matches C++ + ``std::mt19937`` seeded with the same value. This is the algorithm + selected by the ``generator="mersenne_twister"`` attribute of the random + operators, which fully specifies their output for a given seed. + """ + + _N = 624 + _M = 397 + _MATRIX_A = 0x9908B0DF + _UPPER_MASK = 0x80000000 + _LOWER_MASK = 0x7FFFFFFF + + def __init__(self, seed: int): + mt = [0] * self._N + mt[0] = seed & 0xFFFFFFFF + for i in range(1, self._N): + mt[i] = (1812433253 * (mt[i - 1] ^ (mt[i - 1] >> 30)) + i) & 0xFFFFFFFF + self._mt = mt + self._index = self._N + + def _twist(self) -> None: + mt = self._mt + for i in range(self._N): + y = (mt[i] & self._UPPER_MASK) | (mt[(i + 1) % self._N] & self._LOWER_MASK) + mt[i] = mt[(i + self._M) % self._N] ^ (y >> 1) ^ (self._MATRIX_A if y & 1 else 0) + self._index = 0 + + def next_uint32(self) -> int: + if self._index >= self._N: + self._twist() + y = self._mt[self._index] + self._index += 1 + y ^= y >> 11 + y ^= (y << 7) & 0x9D2C5680 + y ^= (y << 15) & 0xEFC60000 + y ^= y >> 18 + return y & 0xFFFFFFFF + + def random_res53(self, num: int) -> np.ndarray: + """Draw `num` doubles in [0, 1) with 53-bit resolution (genrand_res53).""" + res = np.empty(num, dtype=np.float64) + for k in range(num): + a = self.next_uint32() >> 5 + b = self.next_uint32() >> 6 + res[k] = (a * 67108864.0 + b) * (1.0 / 9007199254740992.0) + return res + + class _CommonRandom(OpRun): def __init__(self, onnx_node, run_params): OpRun.__init__(self, onnx_node, run_params) @@ -53,3 +106,24 @@ def _get_state(seed): else: state = np.random.RandomState(seed=int(seed)) return state + + @staticmethod + def _deterministic_uniform(generator, seed, shape): + """Draw uniform doubles in [0, 1) with the fully specified generator. + + Unlike the "default" generator, the result is bit-identical across + implementations for a given seed (see the operator specification). + """ + if generator != "mersenne_twister": + raise ValueError( + f"Unsupported value {generator!r} for attribute 'generator' " + f"(expected 'default' or 'mersenne_twister')." + ) + if seed is None or np.isnan(seed): + raise ValueError( + "Attribute 'seed' must be specified when 'generator' is " + "'mersenne_twister'." + ) + state = _MT19937(int(seed) & 0xFFFFFFFF) + num = int(np.prod(shape)) + return state.random_res53(num).reshape(shape) diff --git a/onnx/reference/ops/op_random_uniform.py b/onnx/reference/ops/op_random_uniform.py index be6a74b3ac2..457a698f582 100644 --- a/onnx/reference/ops/op_random_uniform.py +++ b/onnx/reference/ops/op_random_uniform.py @@ -7,8 +7,12 @@ class RandomUniform(_CommonRandom): - def _run(self, dtype=None, high=None, low=None, seed=None, shape=None): + def _run(self, dtype=None, generator=None, high=None, low=None, seed=None, shape=None): dtype = self._dtype(dtype=dtype) + if generator not in (None, "default"): + res = self._deterministic_uniform(generator, seed, shape) + res = res * (high - low) + low + return (res.astype(dtype),) state = self._get_state(seed) res = state.rand(*shape).astype(dtype) res *= high - low diff --git a/onnx/test/reference_evaluator_test.py b/onnx/test/reference_evaluator_test.py index c2cefbca4c9..d0b3960db1b 100644 --- a/onnx/test/reference_evaluator_test.py +++ b/onnx/test/reference_evaluator_test.py @@ -1477,6 +1477,81 @@ def test_onnxt_runtime_random_uniform(self): self.assertGreater(got.min(), 0) self.assertLess(got.max(), 1) + def test_onnxt_runtime_random_uniform_mersenne_twister(self): + Y = make_tensor_value_info("Y", TensorProto.FLOAT, [None]) + node1 = make_node( + "RandomUniform", + [], + ["Y"], + seed=42.0, + shape=[2, 3], + generator="mersenne_twister", + ) + graph = make_graph([node1], "g", [], [Y]) + onnx_model = make_model(graph) + check_model(onnx_model) + sess = ReferenceEvaluator(onnx_model) + got = sess.run(None, {})[0] + # First six genrand_res53 doubles of MT19937 seeded with + # init_genrand(42), as produced by C++ std::mt19937. + expected = np.array( + [ + [0.3745401188473625, 0.9507143064099162, 0.7319939418114051], + [0.5986584841970366, 0.15601864044243652, 0.15599452033620265], + ], + dtype=np.float64, + ).astype(np.float32) + assert_allclose(got, expected, rtol=0, atol=0) + self.assertEqual(got.dtype, np.float32) + # A second run must produce bit-identical values. + assert_allclose(sess.run(None, {})[0], expected, rtol=0, atol=0) + + def test_onnxt_runtime_random_uniform_mersenne_twister_low_high(self): + Y = make_tensor_value_info("Y", TensorProto.DOUBLE, [None]) + node1 = make_node( + "RandomUniform", + [], + ["Y"], + seed=42.0, + low=5.0, + high=10.0, + dtype=TensorProto.DOUBLE, + shape=[3], + generator="mersenne_twister", + ) + graph = make_graph([node1], "g", [], [Y]) + onnx_model = make_model(graph) + check_model(onnx_model) + sess = ReferenceEvaluator(onnx_model) + got = sess.run(None, {})[0] + expected = 5.0 + np.array( + [0.3745401188473625, 0.9507143064099162, 0.7319939418114051], + dtype=np.float64, + ) * (10.0 - 5.0) + assert_allclose(got, expected, rtol=0, atol=0) + self.assertEqual(got.dtype, np.float64) + + def test_onnxt_runtime_random_uniform_mersenne_twister_no_seed_raises(self): + Y = make_tensor_value_info("Y", TensorProto.FLOAT, [None]) + node1 = make_node( + "RandomUniform", [], ["Y"], shape=[2, 3], generator="mersenne_twister" + ) + graph = make_graph([node1], "g", [], [Y]) + onnx_model = make_model(graph) + sess = ReferenceEvaluator(onnx_model) + with self.assertRaises(ValueError): + sess.run(None, {}) + + def test_mt19937_canonical_test_vector(self): + # The 10000th output of MT19937 seeded with init_genrand(5489) is + # 4123659995 (Matsumoto & Nishimura; also std::mt19937 in C++11). + from onnx.reference.ops._op_common_random import _MT19937 + + gen = _MT19937(5489) + for _ in range(9999): + gen.next_uint32() + self.assertEqual(gen.next_uint32(), 4123659995) + def test_onnxt_runtime_random_uniform_like(self): X = make_tensor_value_info("X", TensorProto.FLOAT, [None]) Y = make_tensor_value_info("Y", TensorProto.FLOAT, [None]) diff --git a/onnx/test/schema_test.py b/onnx/test/schema_test.py index f135f56fa9b..53e617f03dc 100644 --- a/onnx/test/schema_test.py +++ b/onnx/test/schema_test.py @@ -78,6 +78,18 @@ def allowed(schema): self.assertTrue(celu28.has_function) self.assertEqual(allowed(defs.get_schema("Celu", 12)), {"tensor(float)"}) + def test_randomuniform_generator_attribute(self) -> None: + schema28 = defs.get_schema("RandomUniform", 28) + self.assertIn("generator", schema28.attributes) + generator = schema28.attributes["generator"] + self.assertEqual(generator.type, defs.OpSchema.AttrType.STRING) + self.assertEqual(generator.default_value.s, b"default") + self.assertFalse(generator.required) + # The operator stays non-deterministic at the schema level: with the + # default generator the output is still implementation-defined. + self.assertTrue(schema28.non_deterministic) + self.assertNotIn("generator", defs.get_schema("RandomUniform", 22).attributes) + def test_range_supported_types(self) -> None: """Test Range operator supports all expected numeric types.""" range_schema = defs.get_schema("Range") diff --git a/onnx/test/shape_inference_test.py b/onnx/test/shape_inference_test.py index a2a500d8425..9194c831536 100644 --- a/onnx/test/shape_inference_test.py +++ b/onnx/test/shape_inference_test.py @@ -4404,6 +4404,59 @@ def test_random_normal(self) -> None: graph, [make_tensor_value_info("out", TensorProto.DOUBLE, (3, 4, 5))] ) + def test_random_uniform_mersenne_twister(self) -> None: + graph = self._make_graph( + [], + [ + make_node( + "RandomUniform", + [], + ["out"], + dtype=TensorProto.DOUBLE, + shape=(3, 4), + seed=42.0, + generator="mersenne_twister", + ) + ], + [], + ) + self._assert_inferred( + graph, [make_tensor_value_info("out", TensorProto.DOUBLE, (3, 4))] + ) + + def test_random_uniform_unknown_generator_fails(self) -> None: + graph = self._make_graph( + [], + [ + make_node( + "RandomUniform", + [], + ["out"], + shape=(3, 4), + seed=0.0, + generator="xorshift", + ) + ], + [], + ) + self.assertRaises(onnx.shape_inference.InferenceError, self._inferred, graph) + + def test_random_uniform_mersenne_twister_without_seed_fails(self) -> None: + graph = self._make_graph( + [], + [ + make_node( + "RandomUniform", + [], + ["out"], + shape=(3, 4), + generator="mersenne_twister", + ) + ], + [], + ) + self.assertRaises(onnx.shape_inference.InferenceError, self._inferred, graph) + def test_random_normal_like(self) -> None: graph = self._make_graph( [("X", TensorProto.FLOAT, (2, 3, 4))], diff --git a/onnx/test/version_converter_test.py b/onnx/test/version_converter_test.py index 2d667a91aa2..12dd444ecf3 100644 --- a/onnx/test/version_converter_test.py +++ b/onnx/test/version_converter_test.py @@ -2900,3 +2900,36 @@ def test_celu_float_27_28_and_28_27(self) -> None: ) def test_celu_28_27_unsupported_type_fails(self, _: str, dtype: int) -> None: self.assertRaises(RuntimeError, lambda: self._celu_converted(dtype, 28, 27)) + + def _randomuniform_converted(self, src: int, dst: int, **attrs) -> ModelProto: + node = helper.make_node("RandomUniform", [], ["Y"], shape=[2, 3], **attrs) + graph = helper.make_graph( + [node], + "randomuniform", + [], + [helper.make_tensor_value_info("Y", TensorProto.FLOAT, [2, 3])], + ) + return self._converted(graph, helper.make_operatorsetid("", src), dst) + + # RandomUniform 27 -> 28: CompatibleAdapter (generator attribute has a default) + def test_randomuniform_27_28(self) -> None: + converted = self._randomuniform_converted(27, 28, seed=0.0) + assert converted.opset_import[0].version == 28 + + # RandomUniform 28 -> 27: generator="default" matches the old unspecified + # behavior, so the attribute is dropped on downgrade + def test_randomuniform_28_27_default_generator_removed(self) -> None: + converted = self._randomuniform_converted(28, 27, generator="default") + assert converted.opset_import[0].version == 27 + node = next(n for n in converted.graph.node if n.op_type == "RandomUniform") + assert not any(a.name == "generator" for a in node.attribute) + + # RandomUniform 28 -> 27: a deterministic generator cannot be expressed in + # older opsets and must be rejected + def test_randomuniform_28_27_mersenne_twister_fails(self) -> None: + self.assertRaises( + RuntimeError, + lambda: self._randomuniform_converted( + 28, 27, generator="mersenne_twister", seed=42.0 + ), + ) diff --git a/onnx/version_converter/adapters/CMakeLists.txt b/onnx/version_converter/adapters/CMakeLists.txt index d4afa967b36..2be3e2c7035 100644 --- a/onnx/version_converter/adapters/CMakeLists.txt +++ b/onnx/version_converter/adapters/CMakeLists.txt @@ -24,6 +24,7 @@ target_sources(onnx PRIVATE no_previous_version.h pad_10_11.h q_dq_21_20.h + random_uniform_28_27.h remove_consumed_inputs.h reshape_4_5.h reshape_5_4.h diff --git a/onnx/version_converter/adapters/random_uniform_28_27.h b/onnx/version_converter/adapters/random_uniform_28_27.h new file mode 100644 index 00000000000..829572b8cf1 --- /dev/null +++ b/onnx/version_converter/adapters/random_uniform_28_27.h @@ -0,0 +1,41 @@ +// Copyright (c) ONNX Project Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +// Adapter for RandomUniform in default domain from version 28 to 27 + +#pragma once + +#include +#include + +#include "onnx/version_converter/adapters/adapter.h" + +namespace ONNX_NAMESPACE { +namespace version_conversion { + +class RandomUniform_28_27 final : public Adapter { + public: + RandomUniform_28_27() : Adapter("RandomUniform", OpSetID(28), OpSetID(27)) {} + + Node* adapt(std::shared_ptr /*graph*/, Node* node) const override { + const Symbol generator("generator"); + if (node->hasAttribute(generator)) { + // "default" matches the unspecified behavior of RandomUniform v22, so the + // attribute can simply be dropped. Any other generator selects fully + // specified deterministic output, which older versions cannot express. + ONNX_ASSERTM( + node->s(generator) == "default", + "Attribute 'generator' of operator '", + name(), + "' must be 'default' in Opset Version ", + static_cast(target_version().version()), + "."); + node->removeAttribute(generator); + } + return node; + } +}; + +} // namespace version_conversion +} // namespace ONNX_NAMESPACE diff --git a/onnx/version_converter/convert.h b/onnx/version_converter/convert.h index 6ad1e7d1d6c..12f9b0c7e3a 100644 --- a/onnx/version_converter/convert.h +++ b/onnx/version_converter/convert.h @@ -38,6 +38,7 @@ #include "onnx/version_converter/adapters/no_previous_version.h" #include "onnx/version_converter/adapters/pad_10_11.h" #include "onnx/version_converter/adapters/q_dq_21_20.h" +#include "onnx/version_converter/adapters/random_uniform_28_27.h" #include "onnx/version_converter/adapters/range_27_26.h" #include "onnx/version_converter/adapters/reshape_4_5.h" #include "onnx/version_converter/adapters/reshape_5_4.h" @@ -981,12 +982,15 @@ class DefaultVersionConverter : public BaseVersionConverter { /******** 27 -> 28 ********/ registerAdapter(std::make_unique("Celu", OpSetID(27), OpSetID(28))); + registerAdapter(std::make_unique("RandomUniform", OpSetID(27), OpSetID(28))); /******** 28 -> 27 ********/ // Celu v28 widened T to all_float_types_ir4(); Celu v12 (opset 27) supports only FLOAT. const std::vector celu_28_unallowed_types = { TensorProto_DataType_FLOAT16, TensorProto_DataType_BFLOAT16, TensorProto_DataType_DOUBLE}; registerAdapter(std::make_unique("Celu", OpSetID(28), OpSetID(27), celu_28_unallowed_types)); + // RandomUniform v28 added the generator attribute; only generator="default" can be downgraded. + registerAdapter(std::make_unique()); } ModelProto convert_version(const ModelProto& mp_in, const OpSetID& initial_version, const OpSetID& target_version) From 5bd3d0257bda31832c9fa7595d9e0a5568885dbe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 13:53:34 +0000 Subject: [PATCH 2/5] Regenerate docs and backend test data for RandomUniform-28 - docs/Operators.md, docs/Changelog.md via onnx/defs/gen_doc.py - docs/TestCoverage.md via onnx/backend/test/stat_coverage.py - backend node test data via cmd_tools.py generate-data -t RandomUniform - ruff format on the reference implementation files Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Claude --- docs/Changelog.md | 69 +++++++++ docs/Operators.md | 138 +++++++++++++++++- docs/TestCoverage.md | 105 ++++++++++++- .../model.onnx | Bin 0 -> 169 bytes .../test_data_set_0/output_0.pb | 1 + .../model.onnx | Bin 0 -> 190 bytes .../test_data_set_0/output_0.pb | 1 + .../model.onnx | Bin 0 -> 185 bytes .../test_data_set_0/output_0.pb | Bin 0 -> 29 bytes .../model.onnx | Bin 0 -> 209 bytes .../test_data_set_0/output_0.pb | Bin 0 -> 35 bytes onnx/reference/ops/_op_common_random.py | 6 +- onnx/reference/ops/op_random_uniform.py | 4 +- onnx/test/reference_evaluator_test.py | 4 +- 14 files changed, 317 insertions(+), 11 deletions(-) create mode 100644 onnx/backend/test/data/node/test_randomuniform_mersenne_twister/model.onnx create mode 100644 onnx/backend/test/data/node/test_randomuniform_mersenne_twister/test_data_set_0/output_0.pb create mode 100644 onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/model.onnx create mode 100644 onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/test_data_set_0/output_0.pb create mode 100644 onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/model.onnx create mode 100644 onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/test_data_set_0/output_0.pb create mode 100644 onnx/backend/test/data/node/test_randomuniform_mersenne_twister_low_high/model.onnx create mode 100644 onnx/backend/test/data/node/test_randomuniform_mersenne_twister_low_high/test_data_set_0/output_0.pb diff --git a/docs/Changelog.md b/docs/Changelog.md index 198aa0b73db..382f1731fab 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -33093,6 +33093,75 @@ This version of the operator has been available since version 28 of the default
Constrain input and output types to float tensors.
+### **RandomUniform-28** + + Generate a tensor with random values drawn from a uniform distribution. The shape + of the tensor is specified by the `shape` argument and the range by `low` and `high`. + + The data type is specified by the 'dtype' argument. The 'dtype' argument must + be one of the data types specified in the 'DataType' enum field in the + TensorProto message. + + The `generator` attribute selects the pseudo-random number generator algorithm. + With the default value "default", the choice of generator is left to the + implementation and results are generally not reproducible across implementations, + even when `seed` is specified. Setting `generator` to "mersenne_twister" fully + specifies the generated values: given the same `seed`, every conforming + implementation must produce bit-identical results, which makes the operator + deterministic and testable. More algorithms may be added in future opset versions. + + When `generator` is "mersenne_twister", the `seed` attribute must be specified + and the output is computed as follows: + 1. Initialize a standard 32-bit Mersenne Twister (MT19937) state using the + `init_genrand` seeding routine from the reference implementation of Matsumoto + and Nishimura (the seeding also used by C++ `std::mt19937`), with the seed + value obtained by truncating `seed` toward zero and converting it to an + unsigned 32-bit integer (modulo 2^32). + 2. For each output element, in row-major order, draw two consecutive 32-bit + outputs `a` and `b` from the generator and form the double-precision value + `r = (floor(a / 2^5) * 2^26 + floor(b / 2^6)) / 2^53` (the `genrand_res53` + method), which lies in the interval [0, 1). + 3. The element value is `low + r * (high - low)`, computed in double precision + and then cast to `dtype`. + +#### Version + +This version of the operator has been available since version 28 of the default ONNX operator set. + +#### Attributes + +
+
dtype : int (default is 1)
+
The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.
+
generator : string (default is default)
+
(Optional) The pseudo-random number generator algorithm. "default" leaves the choice of generator to the implementation; results are then not reproducible across implementations, even when `seed` is specified. "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
+
high : float (default is 1.0)
+
Upper boundary of the output values.
+
low : float (default is 0.0)
+
Lower boundary of the output values.
+
seed : float
+
(Optional) Seed to the random generator, if not specified we will auto generate one. Must be specified when `generator` is "mersenne_twister".
+
shape : list of ints (required)
+
The shape of the output tensor.
+
+ +#### Inputs + + +#### Outputs + +
+
output : T
+
Output tensor of random values drawn from uniform distribution
+
+ +#### Type Constraints + +
+
T : tensor(bfloat16), tensor(float16), tensor(float), tensor(double)
+
Constrain output types to float tensors.
+
+ # ai.onnx.preview ## Version 1 of the 'ai.onnx.preview' operator set ### **ai.onnx.preview.FlexAttention-1** diff --git a/docs/Operators.md b/docs/Operators.md index d7aa4ec05bc..874c0e1cc21 100644 --- a/docs/Operators.md +++ b/docs/Operators.md @@ -113,7 +113,7 @@ For an operator input/output's differentiability, it can be differentiable, |RNN|22, 14, 7, 1| |RandomNormal|22, 1| |RandomNormalLike|22, 1| -|RandomUniform|22, 1| +|RandomUniform|28, 22, 1| |RandomUniformLike|22, 1| |Reciprocal|13, 6, 1| |ReduceMax|20, 18, 13, 12, 11, 1| @@ -27515,23 +27515,47 @@ Other versions of this operator: 1 be one of the data types specified in the 'DataType' enum field in the TensorProto message. + The `generator` attribute selects the pseudo-random number generator algorithm. + With the default value "default", the choice of generator is left to the + implementation and results are generally not reproducible across implementations, + even when `seed` is specified. Setting `generator` to "mersenne_twister" fully + specifies the generated values: given the same `seed`, every conforming + implementation must produce bit-identical results, which makes the operator + deterministic and testable. More algorithms may be added in future opset versions. + + When `generator` is "mersenne_twister", the `seed` attribute must be specified + and the output is computed as follows: + 1. Initialize a standard 32-bit Mersenne Twister (MT19937) state using the + `init_genrand` seeding routine from the reference implementation of Matsumoto + and Nishimura (the seeding also used by C++ `std::mt19937`), with the seed + value obtained by truncating `seed` toward zero and converting it to an + unsigned 32-bit integer (modulo 2^32). + 2. For each output element, in row-major order, draw two consecutive 32-bit + outputs `a` and `b` from the generator and form the double-precision value + `r = (floor(a / 2^5) * 2^26 + floor(b / 2^6)) / 2^53` (the `genrand_res53` + method), which lies in the interval [0, 1). + 3. The element value is `low + r * (high - low)`, computed in double precision + and then cast to `dtype`. + #### Version -This version of the operator has been available since version 22 of the default ONNX operator set. +This version of the operator has been available since version 28 of the default ONNX operator set. -Other versions of this operator: 1 +Other versions of this operator: 1, 22 #### Attributes
dtype : int (default is 1)
The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.
+
generator : string (default is default)
+
(Optional) The pseudo-random number generator algorithm. "default" leaves the choice of generator to the implementation; results are then not reproducible across implementations, even when `seed` is specified. "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
high : float (default is 1.0)
Upper boundary of the output values.
low : float (default is 0.0)
Lower boundary of the output values.
seed : float
-
(Optional) Seed to the random generator, if not specified we will auto generate one.
+
(Optional) Seed to the random generator, if not specified we will auto generate one. Must be specified when `generator` is "mersenne_twister".
shape : list of ints (required)
The shape of the output tensor.
@@ -27554,6 +27578,112 @@ Other versions of this operator: 1 +#### Examples + +
+randomuniform_mersenne_twister + +```python +node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + shape=[3, 4], + seed=42.0, + generator="mersenne_twister", +) + +y = mersenne_twister_uniform(42, (3, 4)).astype(np.float32) +expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister", +) +``` + +
+ + +
+randomuniform_mersenne_twister_double + +```python +node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + dtype=onnx.TensorProto.DOUBLE, + shape=[2, 4], + seed=123.0, + generator="mersenne_twister", +) + +y = mersenne_twister_uniform(123, (2, 4)) +expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_double", +) +``` + +
+ + +
+randomuniform_mersenne_twister_float16 + +```python +node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + dtype=onnx.TensorProto.FLOAT16, + shape=[10], + seed=7.0, + generator="mersenne_twister", +) + +y = mersenne_twister_uniform(7, (10,)).astype(np.float16) +expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_float16", +) +``` + +
+ + +
+randomuniform_mersenne_twister_low_high + +```python +node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + low=5.0, + high=10.0, + shape=[2, 3], + seed=0.0, + generator="mersenne_twister", +) + +y = mersenne_twister_uniform(0, (2, 3), low=5.0, high=10.0).astype(np.float32) +expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_low_high", +) +``` + +
+ + ### **RandomUniformLike** Generate a tensor with random values drawn from a uniform distribution. diff --git a/docs/TestCoverage.md b/docs/TestCoverage.md index 3073d6dea79..7b3f87ccfc8 100644 --- a/docs/TestCoverage.md +++ b/docs/TestCoverage.md @@ -6,7 +6,7 @@ * [Overall Test Coverage](#overall-test-coverage) # Node Test Coverage ## Summary -Node tests have covered 189/201 (94.03%, 5 generators excluded) common operators. +Node tests have covered 190/202 (94.06%, 4 generators excluded) common operators. Node tests have covered 1/1 (100.00%, 0 generators excluded) experimental operators. @@ -19846,6 +19846,106 @@ expect( +### RandomUniform +There are 4 test cases, listed as following: +
+randomuniform_mersenne_twister + +```python +node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + shape=[3, 4], + seed=42.0, + generator="mersenne_twister", +) + +y = mersenne_twister_uniform(42, (3, 4)).astype(np.float32) +expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister", +) +``` + +
+
+randomuniform_mersenne_twister_double + +```python +node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + dtype=onnx.TensorProto.DOUBLE, + shape=[2, 4], + seed=123.0, + generator="mersenne_twister", +) + +y = mersenne_twister_uniform(123, (2, 4)) +expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_double", +) +``` + +
+
+randomuniform_mersenne_twister_float16 + +```python +node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + dtype=onnx.TensorProto.FLOAT16, + shape=[10], + seed=7.0, + generator="mersenne_twister", +) + +y = mersenne_twister_uniform(7, (10,)).astype(np.float16) +expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_float16", +) +``` + +
+
+randomuniform_mersenne_twister_low_high + +```python +node = onnx.helper.make_node( + "RandomUniform", + inputs=[], + outputs=["y"], + low=5.0, + high=10.0, + shape=[2, 3], + seed=0.0, + generator="mersenne_twister", +) + +y = mersenne_twister_uniform(0, (2, 3), low=5.0, high=10.0).astype(np.float32) +expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_low_high", +) +``` + +
+ + ### Range There are 4 test cases, listed as following:
@@ -30721,9 +30821,6 @@ expect(node, inputs=[x, y], outputs=[z], name="test_xor_bcast4v4d") ### RandomNormalLike (random generator operator) -### RandomUniform (random generator operator) - - ### RandomUniformLike (random generator operator) diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister/model.onnx b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister/model.onnx new file mode 100644 index 0000000000000000000000000000000000000000..345260208dfdf52cea99d905b9fad6465be5b2bc GIT binary patch literal 169 zcmdbs?ôc;?¯A?ZÃ>½>ém=¸½]?¬â??D5?Р¨<Lx? \ No newline at end of file diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/model.onnx b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/model.onnx new file mode 100644 index 0000000000000000000000000000000000000000..59d43b2ffdf556ce38f14eb1b4964c94e1b47a94 GIT binary patch literal 190 zcmZ9_y$ZrG7=_^`rWn&7NJ?KTXl0d!@Hb^ zCOmHH(+!l|Ik0a3M6BUx%#%Z{r0$NYJ!{j6n1#}fJ**>-u-LN21r(UtX_IFi7z;{4 zGqDE)Gt_BbYeXVjoJBmb5j7+w^E2MbXUz{QpN2 z7MJ3DLnYS^t=oA5Cq}v~IS3_nb5wPuO_Rqkl&))W4xWVh62=!)XoS-yOB*y6l|t^G zwRLDb8RQd)ETWtR!K?6u^u4``7K=|mVym9w<|{mY0{_48>aK;`Y|AN-jsZ{#EP{~2 JK7t@!ya8nIHS+)f literal 0 HcmV?d00001 diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/test_data_set_0/output_0.pb b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/test_data_set_0/output_0.pb new file mode 100644 index 0000000000000000000000000000000000000000..2e3e22fd336071bf064aed4de70a79f574f2cc86 GIT binary patch literal 29 kcmdCXnB_U$)wFH9?5P=+OTXvy}I&h?DGe<=TFxPQneq}ot4 TvXsUIF+xKU`oZUj2J77y None: mt = self._mt for i in range(self._N): y = (mt[i] & self._UPPER_MASK) | (mt[(i + 1) % self._N] & self._LOWER_MASK) - mt[i] = mt[(i + self._M) % self._N] ^ (y >> 1) ^ (self._MATRIX_A if y & 1 else 0) + mt[i] = ( + mt[(i + self._M) % self._N] + ^ (y >> 1) + ^ (self._MATRIX_A if y & 1 else 0) + ) self._index = 0 def next_uint32(self) -> int: diff --git a/onnx/reference/ops/op_random_uniform.py b/onnx/reference/ops/op_random_uniform.py index 457a698f582..650fad5109d 100644 --- a/onnx/reference/ops/op_random_uniform.py +++ b/onnx/reference/ops/op_random_uniform.py @@ -7,7 +7,9 @@ class RandomUniform(_CommonRandom): - def _run(self, dtype=None, generator=None, high=None, low=None, seed=None, shape=None): + def _run( + self, dtype=None, generator=None, high=None, low=None, seed=None, shape=None + ): dtype = self._dtype(dtype=dtype) if generator not in (None, "default"): res = self._deterministic_uniform(generator, seed, shape) diff --git a/onnx/test/reference_evaluator_test.py b/onnx/test/reference_evaluator_test.py index d0b3960db1b..c0cdfbbd85f 100644 --- a/onnx/test/reference_evaluator_test.py +++ b/onnx/test/reference_evaluator_test.py @@ -1545,7 +1545,9 @@ def test_onnxt_runtime_random_uniform_mersenne_twister_no_seed_raises(self): def test_mt19937_canonical_test_vector(self): # The 10000th output of MT19937 seeded with init_genrand(5489) is # 4123659995 (Matsumoto & Nishimura; also std::mt19937 in C++11). - from onnx.reference.ops._op_common_random import _MT19937 + from onnx.reference.ops._op_common_random import ( # noqa: PLC0415 + _MT19937, + ) gen = _MT19937(5489) for _ in range(9999): From 086b6ab5e350f97ff1575e8e0527ada75bb61bc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 15:39:56 +0000 Subject: [PATCH 3/5] Clarify that generator="default" gives no determinism guarantee The previous wording only said results are not reproducible across implementations. Make explicit that in "default" mode an implementation may produce reproducible results but is not required to, even for a fixed seed and even across runs of the same implementation. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Claude --- docs/Changelog.md | 15 +++++++++------ docs/Operators.md | 15 +++++++++------ onnx/defs/doc_strings.cc | 13 ++++++++----- onnx/defs/generator/defs.cc | 10 ++++++---- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index 382f1731fab..1bf912e0b6e 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -33104,11 +33104,14 @@ This version of the operator has been available since version 28 of the default The `generator` attribute selects the pseudo-random number generator algorithm. With the default value "default", the choice of generator is left to the - implementation and results are generally not reproducible across implementations, - even when `seed` is specified. Setting `generator` to "mersenne_twister" fully - specifies the generated values: given the same `seed`, every conforming - implementation must produce bit-identical results, which makes the operator - deterministic and testable. More algorithms may be added in future opset versions. + implementation and no determinism guarantee is given: results may differ across + implementations and even across runs of the same implementation, even when + `seed` is specified. An implementation may produce reproducible results in this + mode (for example for a fixed `seed`), but it is not required to. Setting + `generator` to "mersenne_twister" fully specifies the generated values: given + the same `seed`, every conforming implementation must produce bit-identical + results, which makes the operator deterministic and testable. More algorithms + may be added in future opset versions. When `generator` is "mersenne_twister", the `seed` attribute must be specified and the output is computed as follows: @@ -33134,7 +33137,7 @@ This version of the operator has been available since version 28 of the default
dtype : int (default is 1)
The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.
generator : string (default is default)
-
(Optional) The pseudo-random number generator algorithm. "default" leaves the choice of generator to the implementation; results are then not reproducible across implementations, even when `seed` is specified. "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
+
(Optional) The pseudo-random number generator algorithm. "default" leaves the choice of generator to the implementation and provides no determinism guarantee: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified (an implementation may produce reproducible results, but is not required to). "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
high : float (default is 1.0)
Upper boundary of the output values.
low : float (default is 0.0)
diff --git a/docs/Operators.md b/docs/Operators.md index 874c0e1cc21..87c9cecfd08 100644 --- a/docs/Operators.md +++ b/docs/Operators.md @@ -27517,11 +27517,14 @@ Other versions of this operator: 1 The `generator` attribute selects the pseudo-random number generator algorithm. With the default value "default", the choice of generator is left to the - implementation and results are generally not reproducible across implementations, - even when `seed` is specified. Setting `generator` to "mersenne_twister" fully - specifies the generated values: given the same `seed`, every conforming - implementation must produce bit-identical results, which makes the operator - deterministic and testable. More algorithms may be added in future opset versions. + implementation and no determinism guarantee is given: results may differ across + implementations and even across runs of the same implementation, even when + `seed` is specified. An implementation may produce reproducible results in this + mode (for example for a fixed `seed`), but it is not required to. Setting + `generator` to "mersenne_twister" fully specifies the generated values: given + the same `seed`, every conforming implementation must produce bit-identical + results, which makes the operator deterministic and testable. More algorithms + may be added in future opset versions. When `generator` is "mersenne_twister", the `seed` attribute must be specified and the output is computed as follows: @@ -27549,7 +27552,7 @@ Other versions of this operator: 1, <
dtype : int (default is 1)
The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.
generator : string (default is default)
-
(Optional) The pseudo-random number generator algorithm. "default" leaves the choice of generator to the implementation; results are then not reproducible across implementations, even when `seed` is specified. "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
+
(Optional) The pseudo-random number generator algorithm. "default" leaves the choice of generator to the implementation and provides no determinism guarantee: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified (an implementation may produce reproducible results, but is not required to). "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
high : float (default is 1.0)
Upper boundary of the output values.
low : float (default is 0.0)
diff --git a/onnx/defs/doc_strings.cc b/onnx/defs/doc_strings.cc index 8477622e9ac..ff3a16ca468 100644 --- a/onnx/defs/doc_strings.cc +++ b/onnx/defs/doc_strings.cc @@ -164,11 +164,14 @@ TensorProto message. The `generator` attribute selects the pseudo-random number generator algorithm. With the default value "default", the choice of generator is left to the -implementation and results are generally not reproducible across implementations, -even when `seed` is specified. Setting `generator` to "mersenne_twister" fully -specifies the generated values: given the same `seed`, every conforming -implementation must produce bit-identical results, which makes the operator -deterministic and testable. More algorithms may be added in future opset versions. +implementation and no determinism guarantee is given: results may differ across +implementations and even across runs of the same implementation, even when +`seed` is specified. An implementation may produce reproducible results in this +mode (for example for a fixed `seed`), but it is not required to. Setting +`generator` to "mersenne_twister" fully specifies the generated values: given +the same `seed`, every conforming implementation must produce bit-identical +results, which makes the operator deterministic and testable. More algorithms +may be added in future opset versions. When `generator` is "mersenne_twister", the `seed` attribute must be specified and the output is computed as follows: diff --git a/onnx/defs/generator/defs.cc b/onnx/defs/generator/defs.cc index 0c43095070c..e040efa686b 100644 --- a/onnx/defs/generator/defs.cc +++ b/onnx/defs/generator/defs.cc @@ -164,10 +164,12 @@ ONNX_OPERATOR_SET_SCHEMA( .Attr( "generator", "(Optional) The pseudo-random number generator algorithm. \"default\" leaves the choice of " - "generator to the implementation; results are then not reproducible across implementations, " - "even when `seed` is specified. \"mersenne_twister\" selects the fully specified MT19937 " - "algorithm described in the operator documentation, making the output deterministic for a " - "given `seed`. More algorithms may be added in future opset versions.", + "generator to the implementation and provides no determinism guarantee: results may differ " + "across implementations and even across runs of the same implementation, even when `seed` is " + "specified (an implementation may produce reproducible results, but is not required to). " + "\"mersenne_twister\" selects the fully specified MT19937 algorithm described in the operator " + "documentation, making the output deterministic for a given `seed`. More algorithms may be " + "added in future opset versions.", AttributeProto::STRING, std::string("default")) .Attr( From f6f1400dfa76e88d879865e1e332c95fd40982ba Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 15:49:51 +0000 Subject: [PATCH 4/5] Rename generator value "default" to "unspecified" "default" only described that the value is the attribute default, not its meaning. "unspecified" states the semantics directly: the generator algorithm is left unspecified and implementation-defined, with no determinism guarantee. It also stays accurate if a future opset ever recommends a different generator as the default choice. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Claude --- docs/Changelog.md | 6 +++--- docs/Operators.md | 6 +++--- onnx/defs/doc_strings.cc | 2 +- onnx/defs/generator/defs.cc | 10 +++++----- onnx/reference/ops/_op_common_random.py | 4 ++-- onnx/reference/ops/op_random_uniform.py | 2 +- onnx/test/schema_test.py | 4 ++-- onnx/test/version_converter_test.py | 8 ++++---- .../version_converter/adapters/random_uniform_28_27.h | 11 ++++++----- onnx/version_converter/convert.h | 2 +- 10 files changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index 1bf912e0b6e..d06448b5e70 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -33103,7 +33103,7 @@ This version of the operator has been available since version 28 of the default TensorProto message. The `generator` attribute selects the pseudo-random number generator algorithm. - With the default value "default", the choice of generator is left to the + With the default value "unspecified", the choice of generator is left to the implementation and no determinism guarantee is given: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified. An implementation may produce reproducible results in this @@ -33136,8 +33136,8 @@ This version of the operator has been available since version 28 of the default
dtype : int (default is 1)
The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.
-
generator : string (default is default)
-
(Optional) The pseudo-random number generator algorithm. "default" leaves the choice of generator to the implementation and provides no determinism guarantee: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified (an implementation may produce reproducible results, but is not required to). "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
+
generator : string (default is unspecified)
+
(Optional) The pseudo-random number generator algorithm. "unspecified" leaves the choice of generator to the implementation and provides no determinism guarantee: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified (an implementation may produce reproducible results, but is not required to). "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
high : float (default is 1.0)
Upper boundary of the output values.
low : float (default is 0.0)
diff --git a/docs/Operators.md b/docs/Operators.md index 87c9cecfd08..4212d9a84bc 100644 --- a/docs/Operators.md +++ b/docs/Operators.md @@ -27516,7 +27516,7 @@ Other versions of this operator: 1 TensorProto message. The `generator` attribute selects the pseudo-random number generator algorithm. - With the default value "default", the choice of generator is left to the + With the default value "unspecified", the choice of generator is left to the implementation and no determinism guarantee is given: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified. An implementation may produce reproducible results in this @@ -27551,8 +27551,8 @@ Other versions of this operator: 1, <
dtype : int (default is 1)
The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.
-
generator : string (default is default)
-
(Optional) The pseudo-random number generator algorithm. "default" leaves the choice of generator to the implementation and provides no determinism guarantee: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified (an implementation may produce reproducible results, but is not required to). "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
+
generator : string (default is unspecified)
+
(Optional) The pseudo-random number generator algorithm. "unspecified" leaves the choice of generator to the implementation and provides no determinism guarantee: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified (an implementation may produce reproducible results, but is not required to). "mersenne_twister" selects the fully specified MT19937 algorithm described in the operator documentation, making the output deterministic for a given `seed`. More algorithms may be added in future opset versions.
high : float (default is 1.0)
Upper boundary of the output values.
low : float (default is 0.0)
diff --git a/onnx/defs/doc_strings.cc b/onnx/defs/doc_strings.cc index ff3a16ca468..4ad4cda3905 100644 --- a/onnx/defs/doc_strings.cc +++ b/onnx/defs/doc_strings.cc @@ -163,7 +163,7 @@ be one of the data types specified in the 'DataType' enum field in the TensorProto message. The `generator` attribute selects the pseudo-random number generator algorithm. -With the default value "default", the choice of generator is left to the +With the default value "unspecified", the choice of generator is left to the implementation and no determinism guarantee is given: results may differ across implementations and even across runs of the same implementation, even when `seed` is specified. An implementation may produce reproducible results in this diff --git a/onnx/defs/generator/defs.cc b/onnx/defs/generator/defs.cc index e040efa686b..657f5a90190 100644 --- a/onnx/defs/generator/defs.cc +++ b/onnx/defs/generator/defs.cc @@ -163,7 +163,7 @@ ONNX_OPERATOR_SET_SCHEMA( OPTIONAL_VALUE) .Attr( "generator", - "(Optional) The pseudo-random number generator algorithm. \"default\" leaves the choice of " + "(Optional) The pseudo-random number generator algorithm. \"unspecified\" leaves the choice of " "generator to the implementation and provides no determinism guarantee: results may differ " "across implementations and even across runs of the same implementation, even when `seed` is " "specified (an implementation may produce reproducible results, but is not required to). " @@ -171,7 +171,7 @@ ONNX_OPERATOR_SET_SCHEMA( "documentation, making the output deterministic for a given `seed`. More algorithms may be " "added in future opset versions.", AttributeProto::STRING, - std::string("default")) + std::string("unspecified")) .Attr( "dtype", "The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.", @@ -185,11 +185,11 @@ ONNX_OPERATOR_SET_SCHEMA( const auto* generator_attr = ctx.getAttribute("generator"); if (generator_attr != nullptr) { const std::string& generator = generator_attr->s(); - if (generator != "default" && generator != "mersenne_twister") { + if (generator != "unspecified" && generator != "mersenne_twister") { fail_shape_inference( - "Attribute 'generator' must be one of 'default' or 'mersenne_twister', got '", generator, "'."); + "Attribute 'generator' must be one of 'unspecified' or 'mersenne_twister', got '", generator, "'."); } - if (generator != "default" && ctx.getAttribute("seed") == nullptr) { + if (generator != "unspecified" && ctx.getAttribute("seed") == nullptr) { fail_shape_inference("Attribute 'seed' must be specified when 'generator' is '", generator, "'."); } } diff --git a/onnx/reference/ops/_op_common_random.py b/onnx/reference/ops/_op_common_random.py index 2947884a80e..0b2c67fe631 100644 --- a/onnx/reference/ops/_op_common_random.py +++ b/onnx/reference/ops/_op_common_random.py @@ -115,13 +115,13 @@ def _get_state(seed): def _deterministic_uniform(generator, seed, shape): """Draw uniform doubles in [0, 1) with the fully specified generator. - Unlike the "default" generator, the result is bit-identical across + Unlike the "unspecified" generator, the result is bit-identical across implementations for a given seed (see the operator specification). """ if generator != "mersenne_twister": raise ValueError( f"Unsupported value {generator!r} for attribute 'generator' " - f"(expected 'default' or 'mersenne_twister')." + f"(expected 'unspecified' or 'mersenne_twister')." ) if seed is None or np.isnan(seed): raise ValueError( diff --git a/onnx/reference/ops/op_random_uniform.py b/onnx/reference/ops/op_random_uniform.py index 650fad5109d..69965d5b243 100644 --- a/onnx/reference/ops/op_random_uniform.py +++ b/onnx/reference/ops/op_random_uniform.py @@ -11,7 +11,7 @@ def _run( self, dtype=None, generator=None, high=None, low=None, seed=None, shape=None ): dtype = self._dtype(dtype=dtype) - if generator not in (None, "default"): + if generator not in (None, "unspecified"): res = self._deterministic_uniform(generator, seed, shape) res = res * (high - low) + low return (res.astype(dtype),) diff --git a/onnx/test/schema_test.py b/onnx/test/schema_test.py index 53e617f03dc..62a593476f3 100644 --- a/onnx/test/schema_test.py +++ b/onnx/test/schema_test.py @@ -83,10 +83,10 @@ def test_randomuniform_generator_attribute(self) -> None: self.assertIn("generator", schema28.attributes) generator = schema28.attributes["generator"] self.assertEqual(generator.type, defs.OpSchema.AttrType.STRING) - self.assertEqual(generator.default_value.s, b"default") + self.assertEqual(generator.default_value.s, b"unspecified") self.assertFalse(generator.required) # The operator stays non-deterministic at the schema level: with the - # default generator the output is still implementation-defined. + # "unspecified" generator the output is still implementation-defined. self.assertTrue(schema28.non_deterministic) self.assertNotIn("generator", defs.get_schema("RandomUniform", 22).attributes) diff --git a/onnx/test/version_converter_test.py b/onnx/test/version_converter_test.py index 12dd444ecf3..2807c5913b4 100644 --- a/onnx/test/version_converter_test.py +++ b/onnx/test/version_converter_test.py @@ -2916,10 +2916,10 @@ def test_randomuniform_27_28(self) -> None: converted = self._randomuniform_converted(27, 28, seed=0.0) assert converted.opset_import[0].version == 28 - # RandomUniform 28 -> 27: generator="default" matches the old unspecified - # behavior, so the attribute is dropped on downgrade - def test_randomuniform_28_27_default_generator_removed(self) -> None: - converted = self._randomuniform_converted(28, 27, generator="default") + # RandomUniform 28 -> 27: generator="unspecified" matches the old + # implementation-defined behavior, so the attribute is dropped on downgrade + def test_randomuniform_28_27_unspecified_generator_removed(self) -> None: + converted = self._randomuniform_converted(28, 27, generator="unspecified") assert converted.opset_import[0].version == 27 node = next(n for n in converted.graph.node if n.op_type == "RandomUniform") assert not any(a.name == "generator" for a in node.attribute) diff --git a/onnx/version_converter/adapters/random_uniform_28_27.h b/onnx/version_converter/adapters/random_uniform_28_27.h index 829572b8cf1..4957c652fc3 100644 --- a/onnx/version_converter/adapters/random_uniform_28_27.h +++ b/onnx/version_converter/adapters/random_uniform_28_27.h @@ -21,14 +21,15 @@ class RandomUniform_28_27 final : public Adapter { Node* adapt(std::shared_ptr /*graph*/, Node* node) const override { const Symbol generator("generator"); if (node->hasAttribute(generator)) { - // "default" matches the unspecified behavior of RandomUniform v22, so the - // attribute can simply be dropped. Any other generator selects fully - // specified deterministic output, which older versions cannot express. + // "unspecified" matches the implementation-defined behavior of + // RandomUniform v22, so the attribute can simply be dropped. Any other + // generator selects fully specified deterministic output, which older + // versions cannot express. ONNX_ASSERTM( - node->s(generator) == "default", + node->s(generator) == "unspecified", "Attribute 'generator' of operator '", name(), - "' must be 'default' in Opset Version ", + "' must be 'unspecified' in Opset Version ", static_cast(target_version().version()), "."); node->removeAttribute(generator); diff --git a/onnx/version_converter/convert.h b/onnx/version_converter/convert.h index 12f9b0c7e3a..f0ef91996e2 100644 --- a/onnx/version_converter/convert.h +++ b/onnx/version_converter/convert.h @@ -989,7 +989,7 @@ class DefaultVersionConverter : public BaseVersionConverter { const std::vector celu_28_unallowed_types = { TensorProto_DataType_FLOAT16, TensorProto_DataType_BFLOAT16, TensorProto_DataType_DOUBLE}; registerAdapter(std::make_unique("Celu", OpSetID(28), OpSetID(27), celu_28_unallowed_types)); - // RandomUniform v28 added the generator attribute; only generator="default" can be downgraded. + // RandomUniform v28 added the generator attribute; only generator="unspecified" can be downgraded. registerAdapter(std::make_unique()); } From 3bfb86f645da24d59580936355204346f8dd9da1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 15:56:12 +0000 Subject: [PATCH 5/5] Compute mersenne_twister values in the target data type Double precision is now only used for dtype=double (two 32-bit outputs via genrand_res53, as before). For bfloat16/float16/float, each element draws a single 32-bit output and forms r = (a >> (32-p)) / 2^p with p significand bits, which is exactly representable in the target type. low + r * (high - low) is evaluated in the target type with IEEE 754 round-to-nearest-even instead of double-then-cast, so implementations never need double arithmetic unless dtype is double. The double test data is unchanged; float32/float16 expected values are regenerated. The node test case now carries its own independent MT19937 implementation to cross-check the reference runtime. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Claude --- docs/Changelog.md | 21 ++++-- docs/Operators.md | 29 ++++++--- docs/TestCoverage.md | 8 +-- onnx/backend/test/case/node/randomuniform.py | 60 ++++++++++++++---- .../test_data_set_0/output_0.pb | 2 +- .../test_data_set_0/output_0.pb | Bin 29 -> 29 bytes .../test_data_set_0/output_0.pb | Bin 35 -> 35 bytes onnx/defs/doc_strings.cc | 21 ++++-- onnx/reference/ops/_op_common_random.py | 29 ++++++++- onnx/reference/ops/op_random_uniform.py | 10 ++- onnx/test/reference_evaluator_test.py | 13 ++-- 11 files changed, 142 insertions(+), 51 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index d06448b5e70..08c6c94a7dc 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -33120,12 +33120,21 @@ This version of the operator has been available since version 28 of the default and Nishimura (the seeding also used by C++ `std::mt19937`), with the seed value obtained by truncating `seed` toward zero and converting it to an unsigned 32-bit integer (modulo 2^32). - 2. For each output element, in row-major order, draw two consecutive 32-bit - outputs `a` and `b` from the generator and form the double-precision value - `r = (floor(a / 2^5) * 2^26 + floor(b / 2^6)) / 2^53` (the `genrand_res53` - method), which lies in the interval [0, 1). - 3. The element value is `low + r * (high - low)`, computed in double precision - and then cast to `dtype`. + 2. For each output element, in row-major order, draw a value `r` in the + interval [0, 1) whose resolution matches the precision of `dtype`. Let `p` + be the number of significand bits of `dtype`, including the implicit bit + (8 for bfloat16, 11 for float16, 24 for float, 53 for double): + - If `dtype` is double, draw two consecutive 32-bit outputs `a` and `b` and + form `r = (floor(a / 2^5) * 2^26 + floor(b / 2^6)) / 2^53` (the + `genrand_res53` method). + - Otherwise, draw one 32-bit output `a` and form + `r = floor(a / 2^(32-p)) / 2^p`, which is exactly representable in + `dtype`. + 3. The element value is `low + r * (high - low)`, where `low` and `high` are + first converted to `dtype` and the subtraction, multiplication, and + addition are performed in `dtype` with IEEE 754 round-to-nearest-even + semantics. Note that due to this rounding, the result may equal `high` for + low-precision types. #### Version diff --git a/docs/Operators.md b/docs/Operators.md index 4212d9a84bc..a8654086c5e 100644 --- a/docs/Operators.md +++ b/docs/Operators.md @@ -27533,12 +27533,21 @@ Other versions of this operator: 1 and Nishimura (the seeding also used by C++ `std::mt19937`), with the seed value obtained by truncating `seed` toward zero and converting it to an unsigned 32-bit integer (modulo 2^32). - 2. For each output element, in row-major order, draw two consecutive 32-bit - outputs `a` and `b` from the generator and form the double-precision value - `r = (floor(a / 2^5) * 2^26 + floor(b / 2^6)) / 2^53` (the `genrand_res53` - method), which lies in the interval [0, 1). - 3. The element value is `low + r * (high - low)`, computed in double precision - and then cast to `dtype`. + 2. For each output element, in row-major order, draw a value `r` in the + interval [0, 1) whose resolution matches the precision of `dtype`. Let `p` + be the number of significand bits of `dtype`, including the implicit bit + (8 for bfloat16, 11 for float16, 24 for float, 53 for double): + - If `dtype` is double, draw two consecutive 32-bit outputs `a` and `b` and + form `r = (floor(a / 2^5) * 2^26 + floor(b / 2^6)) / 2^53` (the + `genrand_res53` method). + - Otherwise, draw one 32-bit output `a` and form + `r = floor(a / 2^(32-p)) / 2^p`, which is exactly representable in + `dtype`. + 3. The element value is `low + r * (high - low)`, where `low` and `high` are + first converted to `dtype` and the subtraction, multiplication, and + addition are performed in `dtype` with IEEE 754 round-to-nearest-even + semantics. Note that due to this rounding, the result may equal `high` for + low-precision types. #### Version @@ -27596,7 +27605,7 @@ node = onnx.helper.make_node( generator="mersenne_twister", ) -y = mersenne_twister_uniform(42, (3, 4)).astype(np.float32) +y = mersenne_twister_uniform(42, (3, 4), np.float32) expect( node, inputs=[], @@ -27622,7 +27631,7 @@ node = onnx.helper.make_node( generator="mersenne_twister", ) -y = mersenne_twister_uniform(123, (2, 4)) +y = mersenne_twister_uniform(123, (2, 4), np.float64) expect( node, inputs=[], @@ -27648,7 +27657,7 @@ node = onnx.helper.make_node( generator="mersenne_twister", ) -y = mersenne_twister_uniform(7, (10,)).astype(np.float16) +y = mersenne_twister_uniform(7, (10,), np.float16) expect( node, inputs=[], @@ -27675,7 +27684,7 @@ node = onnx.helper.make_node( generator="mersenne_twister", ) -y = mersenne_twister_uniform(0, (2, 3), low=5.0, high=10.0).astype(np.float32) +y = mersenne_twister_uniform(0, (2, 3), np.float32, low=5.0, high=10.0) expect( node, inputs=[], diff --git a/docs/TestCoverage.md b/docs/TestCoverage.md index 7b3f87ccfc8..f1b3b8e87df 100644 --- a/docs/TestCoverage.md +++ b/docs/TestCoverage.md @@ -19861,7 +19861,7 @@ node = onnx.helper.make_node( generator="mersenne_twister", ) -y = mersenne_twister_uniform(42, (3, 4)).astype(np.float32) +y = mersenne_twister_uniform(42, (3, 4), np.float32) expect( node, inputs=[], @@ -19885,7 +19885,7 @@ node = onnx.helper.make_node( generator="mersenne_twister", ) -y = mersenne_twister_uniform(123, (2, 4)) +y = mersenne_twister_uniform(123, (2, 4), np.float64) expect( node, inputs=[], @@ -19909,7 +19909,7 @@ node = onnx.helper.make_node( generator="mersenne_twister", ) -y = mersenne_twister_uniform(7, (10,)).astype(np.float16) +y = mersenne_twister_uniform(7, (10,), np.float16) expect( node, inputs=[], @@ -19934,7 +19934,7 @@ node = onnx.helper.make_node( generator="mersenne_twister", ) -y = mersenne_twister_uniform(0, (2, 3), low=5.0, high=10.0).astype(np.float32) +y = mersenne_twister_uniform(0, (2, 3), np.float32, low=5.0, high=10.0) expect( node, inputs=[], diff --git a/onnx/backend/test/case/node/randomuniform.py b/onnx/backend/test/case/node/randomuniform.py index 73e2c154883..20fed5bda9f 100644 --- a/onnx/backend/test/case/node/randomuniform.py +++ b/onnx/backend/test/case/node/randomuniform.py @@ -10,16 +10,52 @@ from onnx.backend.test.case.node import expect -def mersenne_twister_uniform(seed, shape, low=0.0, high=1.0): - """Reference values for RandomUniform with generator="mersenne_twister". +def mersenne_twister_uniform(seed, shape, dtype, low=0.0, high=1.0): + """Independent implementation of RandomUniform with generator="mersenne_twister". - The operator specifies MT19937 seeded with ``init_genrand`` and doubles - drawn with the 53-bit ``genrand_res53`` method. For scalar seeds below - 2**32 numpy's legacy ``RandomState`` implements exactly this algorithm, - so it can serve as an independent reference here. + Follows the operator specification: MT19937 seeded with ``init_genrand``, + per-element values in [0, 1) with a resolution matching the precision of + `dtype` (two 32-bit outputs via ``genrand_res53`` for double, one 32-bit + output otherwise), and ``low + r * (high - low)`` evaluated in `dtype`. + Kept separate from onnx.reference so the generated test data cross-checks + the reference implementation. """ - state = np.random.RandomState(int(seed) & 0xFFFFFFFF) - return low + state.random_sample(shape) * (high - low) + n, m = 624, 397 + mt = [0] * n + mt[0] = int(seed) & 0xFFFFFFFF + for i in range(1, n): + mt[i] = (1812433253 * (mt[i - 1] ^ (mt[i - 1] >> 30)) + i) & 0xFFFFFFFF + index = n + + def next_uint32(): + nonlocal index + if index >= n: + for i in range(n): + y = (mt[i] & 0x80000000) | (mt[(i + 1) % n] & 0x7FFFFFFF) + mt[i] = mt[(i + m) % n] ^ (y >> 1) ^ (0x9908B0DF if y & 1 else 0) + index = 0 + y = mt[index] + index += 1 + y ^= y >> 11 + y ^= (y << 7) & 0x9D2C5680 + y ^= (y << 15) & 0xEFC60000 + y ^= y >> 18 + return y & 0xFFFFFFFF + + num = int(np.prod(shape)) + if np.dtype(dtype) == np.float64: + r = [ + ((next_uint32() >> 5) * 67108864.0 + (next_uint32() >> 6)) + / 9007199254740992.0 + for _ in range(num) + ] + else: + p = np.finfo(dtype).nmant + 1 + r = [(next_uint32() >> (32 - p)) / (1 << p) for _ in range(num)] + r = np.array(r, dtype=np.float64).reshape(shape).astype(dtype) + low = np.asarray(low, dtype=dtype) + high = np.asarray(high, dtype=dtype) + return r * (high - low) + low class RandomUniform(Base): @@ -34,7 +70,7 @@ def export_randomuniform_mersenne_twister() -> None: generator="mersenne_twister", ) - y = mersenne_twister_uniform(42, (3, 4)).astype(np.float32) + y = mersenne_twister_uniform(42, (3, 4), np.float32) expect( node, inputs=[], @@ -55,7 +91,7 @@ def export_randomuniform_mersenne_twister_low_high() -> None: generator="mersenne_twister", ) - y = mersenne_twister_uniform(0, (2, 3), low=5.0, high=10.0).astype(np.float32) + y = mersenne_twister_uniform(0, (2, 3), np.float32, low=5.0, high=10.0) expect( node, inputs=[], @@ -75,7 +111,7 @@ def export_randomuniform_mersenne_twister_double() -> None: generator="mersenne_twister", ) - y = mersenne_twister_uniform(123, (2, 4)) + y = mersenne_twister_uniform(123, (2, 4), np.float64) expect( node, inputs=[], @@ -95,7 +131,7 @@ def export_randomuniform_mersenne_twister_float16() -> None: generator="mersenne_twister", ) - y = mersenne_twister_uniform(7, (10,)).astype(np.float16) + y = mersenne_twister_uniform(7, (10,), np.float16) expect( node, inputs=[], diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister/test_data_set_0/output_0.pb b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister/test_data_set_0/output_0.pb index 736d80b5848..eb853968023 100644 --- a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister/test_data_set_0/output_0.pb +++ b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister/test_data_set_0/output_0.pb @@ -1 +1 @@ -ByJ0¹Ã¿>bs?ôc;?¯A?ZÃ>½>ém=¸½]?¬â??D5?Р¨<Lx? \ No newline at end of file +ByJ0¸Ã¿>=êK?bs?TÖ;>ôc;?Ô™G?®A?,Ë?XÃ>0Dä>½> ¿Ì= \ No newline at end of file diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/test_data_set_0/output_0.pb b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/test_data_set_0/output_0.pb index 2e3e22fd336071bf064aed4de70a79f574f2cc86..78f984daf3bc50b957504502996040d2e6f05373 100644 GIT binary patch literal 29 kcmd np.ndarray: res[k] = (a * 67108864.0 + b) * (1.0 / 9007199254740992.0) return res + def random_res(self, num: int, precision: int) -> np.ndarray: + """Draw `num` values in [0, 1) with `precision` significand bits. + + Each value uses one 32-bit output: ``(next_uint32() >> (32 - p)) / 2^p``. + The results are exactly representable in any binary float type with at + least `precision` significand bits. + """ + res = np.empty(num, dtype=np.float64) + shift = 32 - precision + scale = 1.0 / (1 << precision) + for k in range(num): + res[k] = (self.next_uint32() >> shift) * scale + return res + class _CommonRandom(OpRun): def __init__(self, onnx_node, run_params): @@ -112,11 +126,15 @@ def _get_state(seed): return state @staticmethod - def _deterministic_uniform(generator, seed, shape): - """Draw uniform doubles in [0, 1) with the fully specified generator. + def _deterministic_uniform(generator, seed, shape, dtype): + """Draw uniform values in [0, 1) with the fully specified generator. Unlike the "unspecified" generator, the result is bit-identical across implementations for a given seed (see the operator specification). + The resolution of the values matches the precision of `dtype`: double + uses the two-word genrand_res53 method, all other float types use one + 32-bit output per element, keeping every value exactly representable + in `dtype`. """ if generator != "mersenne_twister": raise ValueError( @@ -130,4 +148,9 @@ def _deterministic_uniform(generator, seed, shape): ) state = _MT19937(int(seed) & 0xFFFFFFFF) num = int(np.prod(shape)) - return state.random_res53(num).reshape(shape) + if np.dtype(dtype) == np.float64: + res = state.random_res53(num) + else: + precision = np.finfo(dtype).nmant + 1 + res = state.random_res(num, precision) + return res.reshape(shape).astype(dtype) diff --git a/onnx/reference/ops/op_random_uniform.py b/onnx/reference/ops/op_random_uniform.py index 69965d5b243..b5f9c170924 100644 --- a/onnx/reference/ops/op_random_uniform.py +++ b/onnx/reference/ops/op_random_uniform.py @@ -3,6 +3,8 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations +import numpy as np + from onnx.reference.ops._op_common_random import _CommonRandom @@ -12,9 +14,11 @@ def _run( ): dtype = self._dtype(dtype=dtype) if generator not in (None, "unspecified"): - res = self._deterministic_uniform(generator, seed, shape) - res = res * (high - low) + low - return (res.astype(dtype),) + res = self._deterministic_uniform(generator, seed, shape, dtype) + # low + r * (high - low), evaluated in the target data type + low_t = np.asarray(low, dtype=dtype) + high_t = np.asarray(high, dtype=dtype) + return (res * (high_t - low_t) + low_t,) state = self._get_state(seed) res = state.rand(*shape).astype(dtype) res *= high - low diff --git a/onnx/test/reference_evaluator_test.py b/onnx/test/reference_evaluator_test.py index c0cdfbbd85f..4491dd68fbd 100644 --- a/onnx/test/reference_evaluator_test.py +++ b/onnx/test/reference_evaluator_test.py @@ -1492,15 +1492,16 @@ def test_onnxt_runtime_random_uniform_mersenne_twister(self): check_model(onnx_model) sess = ReferenceEvaluator(onnx_model) got = sess.run(None, {})[0] - # First six genrand_res53 doubles of MT19937 seeded with - # init_genrand(42), as produced by C++ std::mt19937. + # For float32, each element uses one 32-bit output of MT19937 seeded + # with init_genrand(42): r = (a >> 8) / 2^24, as produced by C++ + # std::mt19937. expected = np.array( [ - [0.3745401188473625, 0.9507143064099162, 0.7319939418114051], - [0.5986584841970366, 0.15601864044243652, 0.15599452033620265], + [0.374540091, 0.796542943, 0.95071429], + [0.183434784, 0.731993914, 0.779690981], ], - dtype=np.float64, - ).astype(np.float32) + dtype=np.float32, + ) assert_allclose(got, expected, rtol=0, atol=0) self.assertEqual(got.dtype, np.float32) # A second run must produce bit-identical values.