diff --git a/docs/Changelog.md b/docs/Changelog.md index 198aa0b73db..08c6c94a7dc 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -33093,6 +33093,87 @@ 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 "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 + 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: + 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 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 + +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 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)
+
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..a8654086c5e 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,59 @@ 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 "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 + 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: + 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 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 -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 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)
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 +27590,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), 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), np.float64) +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,), 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), np.float32, low=5.0, high=10.0) +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..f1b3b8e87df 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), 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), np.float64) +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,), 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), np.float32, low=5.0, high=10.0) +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/case/node/randomuniform.py b/onnx/backend/test/case/node/randomuniform.py new file mode 100644 index 00000000000..20fed5bda9f --- /dev/null +++ b/onnx/backend/test/case/node/randomuniform.py @@ -0,0 +1,140 @@ +# 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, dtype, low=0.0, high=1.0): + """Independent implementation of RandomUniform with generator="mersenne_twister". + + 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. + """ + 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): + @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), 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), np.float32, low=5.0, high=10.0) + 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), np.float64) + 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,), np.float16) + expect( + node, + inputs=[], + outputs=[y], + name="test_randomuniform_mersenne_twister_float16", + ) 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 00000000000..345260208df Binary files /dev/null and b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister/model.onnx differ 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 new file mode 100644 index 00000000000..eb853968023 --- /dev/null +++ b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister/test_data_set_0/output_0.pb @@ -0,0 +1 @@ +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_double/model.onnx b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/model.onnx new file mode 100644 index 00000000000..59d43b2ffdf Binary files /dev/null and b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/model.onnx differ diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/test_data_set_0/output_0.pb b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/test_data_set_0/output_0.pb new file mode 100644 index 00000000000..907ad3fd924 --- /dev/null +++ b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_double/test_data_set_0/output_0.pb @@ -0,0 +1 @@ + ByJ@õÚ¾yIæ?*‚m[PÒ?DÝ ëw Í?ðhàÞ^¤á?áçöÉãç?0=-Û?[ɤ™kbï?a›Q êå? \ No newline at end of file diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/model.onnx b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/model.onnx new file mode 100644 index 00000000000..7f0ad00d4bd Binary files /dev/null and b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/model.onnx differ 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 00000000000..78f984daf3b --- /dev/null +++ b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_float16/test_data_set_0/output_0.pb @@ -0,0 +1,3 @@ + + +ByJà,D3=:57Ó;É9J7Ò;ì4 \ No newline at end of file diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_low_high/model.onnx b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_low_high/model.onnx new file mode 100644 index 00000000000..9c85669d7cf Binary files /dev/null and b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_low_high/model.onnx differ diff --git a/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_low_high/test_data_set_0/output_0.pb b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_low_high/test_data_set_0/output_0.pb new file mode 100644 index 00000000000..cea18a45c97 Binary files /dev/null and b/onnx/backend/test/data/node/test_randomuniform_mersenne_twister_low_high/test_data_set_0/output_0.pb differ diff --git a/onnx/defs/doc_strings.cc b/onnx/defs/doc_strings.cc index abab3015f08..b07b0bb46b6 100644 --- a/onnx/defs/doc_strings.cc +++ b/onnx/defs/doc_strings.cc @@ -154,6 +154,49 @@ 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 "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 +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: +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 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. +)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 +1361,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..657f5a90190 100644 --- a/onnx/defs/generator/defs.cc +++ b/onnx/defs/generator/defs.cc @@ -150,16 +150,28 @@ 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. \"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.", + AttributeProto::STRING, + std::string("unspecified")) .Attr( "dtype", "The data type for the elements of the output tensor. If not specified, default is TensorProto::FLOAT.", @@ -170,6 +182,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 != "unspecified" && generator != "mersenne_twister") { + fail_shape_inference( + "Attribute 'generator' must be one of 'unspecified' or 'mersenne_twister', got '", generator, "'."); + } + if (generator != "unspecified" && 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..c565887e1c5 100644 --- a/onnx/reference/ops/_op_common_random.py +++ b/onnx/reference/ops/_op_common_random.py @@ -9,6 +9,77 @@ 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 + + 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): OpRun.__init__(self, onnx_node, run_params) @@ -53,3 +124,33 @@ def _get_state(seed): else: state = np.random.RandomState(seed=int(seed)) return state + + @staticmethod + 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( + f"Unsupported value {generator!r} for attribute 'generator' " + f"(expected 'unspecified' 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)) + 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 be6a74b3ac2..b5f9c170924 100644 --- a/onnx/reference/ops/op_random_uniform.py +++ b/onnx/reference/ops/op_random_uniform.py @@ -3,12 +3,22 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations +import numpy as np + from onnx.reference.ops._op_common_random import _CommonRandom 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, "unspecified"): + 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 c2cefbca4c9..4491dd68fbd 100644 --- a/onnx/test/reference_evaluator_test.py +++ b/onnx/test/reference_evaluator_test.py @@ -1477,6 +1477,84 @@ 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] + # 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.374540091, 0.796542943, 0.95071429], + [0.183434784, 0.731993914, 0.779690981], + ], + 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. + 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 ( # noqa: PLC0415 + _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..62a593476f3 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"unspecified") + self.assertFalse(generator.required) + # The operator stays non-deterministic at the schema level: with the + # "unspecified" 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..2807c5913b4 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="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) + + # 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..4957c652fc3 --- /dev/null +++ b/onnx/version_converter/adapters/random_uniform_28_27.h @@ -0,0 +1,42 @@ +// 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)) { + // "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) == "unspecified", + "Attribute 'generator' of operator '", + name(), + "' must be 'unspecified' 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..f0ef91996e2 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="unspecified" can be downgraded. + registerAdapter(std::make_unique()); } ModelProto convert_version(const ModelProto& mp_in, const OpSetID& initial_version, const OpSetID& target_version)